Compare commits
3 Commits
efc4b70e13
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 92a2849376 | |||
| f6d0efdfc0 | |||
| 8547d6ea33 |
@@ -5,15 +5,16 @@
|
|||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- `skills/<name>/SKILL.md` — one narrowly triggered on-demand behavior.
|
- `skills/<name>/SKILL.md` — one narrowly triggered on-demand behavior.
|
||||||
- `bin/ink-skills` — dependency-free installer; stdout is TSV, diagnostics stderr.
|
- `bin/ink-skills` — POSIX linker/importer; stdout is TSV, diagnostics stderr.
|
||||||
- `test/install-smoke.sh` — installer contract smoke.
|
- `test/install-smoke.sh` — installer contract smoke.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- A skill owns reusable judgment, never runtime policy or repeatable mechanics.
|
- A skill owns reusable judgment, never runtime policy or repeatable mechanics.
|
||||||
- Ink source and `REQUIREMENTS.md` own host behavior; `toolset` owns external executables.
|
- Ink source and `SPEC.md` own host behavior; `toolset` owns external executables.
|
||||||
- Keep skill names lowercase and hyphenated; directory and frontmatter name must match.
|
- Keep skill names lowercase and hyphenated; directory and frontmatter name must match.
|
||||||
- The installer only creates symlinks and must refuse collisions. Do not add a registry,
|
- `link` only creates symlinks. `import` may materialize only a caller-supplied,
|
||||||
network calls, package-manager dependency, prompts, copies, or hidden state.
|
SHA-256-verified local archive in the content-addressed store. Do not add URL,
|
||||||
|
Git, credential, registry, package-manager, prompt, or updater behavior.
|
||||||
- Run `sh -n bin/ink-skills`, `sh test/install-smoke.sh`, and
|
- Run `sh -n bin/ink-skills`, `sh test/install-smoke.sh`, and
|
||||||
`sh test/skills-smoke.sh` after changes.
|
`sh test/skills-smoke.sh` after changes.
|
||||||
|
|||||||
@@ -7,61 +7,58 @@ This repository owns reusable Ink judgment. Ink owns runtime enforcement and
|
|||||||
policy. [`toolset`](https://git.tmk241.com/tmk241/toolset) owns compiled external
|
policy. [`toolset`](https://git.tmk241.com/tmk241/toolset) owns compiled external
|
||||||
executables. Skills never grant authority by themselves.
|
executables. Skills never grant authority by themselves.
|
||||||
|
|
||||||
## Install
|
## Link local skills
|
||||||
|
|
||||||
Clone once, then symlink the skills you want:
|
Clone once, then link every skill shipped by this checkout:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone git@git.tmk241.com:tmk241/ink-skills.git
|
git clone git@git.tmk241.com:tmk241/ink-skills.git
|
||||||
ink-skills/bin/ink-skills install
|
ink-skills/bin/ink-skills link
|
||||||
```
|
```
|
||||||
|
|
||||||
If the repository is already under `/opt/repositories`:
|
Link selected directories or target one project:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
/opt/repositories/ink-skills/bin/ink-skills install
|
ink-skills link /opt/repositories/ink-skills/skills/audit-ink-cli
|
||||||
|
ink-skills link --project /path/to/project skills/configure-ink-agent
|
||||||
```
|
```
|
||||||
|
|
||||||
Install selected skills only:
|
`link` only creates absolute symlinks. It performs no network access, copies,
|
||||||
|
prompts, registry lookup, or policy mutation. Pulling a linked checkout changes
|
||||||
|
its bytes; restart Ink to freeze the updated skill snapshot.
|
||||||
|
|
||||||
|
## Import verified artifacts
|
||||||
|
|
||||||
|
Transport and authentication remain ordinary shell jobs:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ink-skills install audit-ink-cli configure-ink-agent
|
curl -fLo skills.tar https://example/skills.tar
|
||||||
|
git archive --format=tar HEAD >skills.tar
|
||||||
```
|
```
|
||||||
|
|
||||||
Install into one project instead of the user catalogue:
|
The publisher communicates the expected digest out of band. Import only after you
|
||||||
|
have that value:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ink-skills install --project /path/to/project configure-ink-agent
|
ink-skills import sha256:012345... skills.tar
|
||||||
|
ink-skills import --project /path/to/project sha256:012345... skills.tar skills/review-sql
|
||||||
```
|
```
|
||||||
|
|
||||||
Install from any Git repository your normal Git credentials can read:
|
`import` verifies the complete archive before extraction, accepts only regular
|
||||||
|
files and directories with safe relative paths, and rejects symlinks and special
|
||||||
|
files. It materializes the tree under
|
||||||
|
`$INK_SKILLS_STORE/sha256/HASH` (or the XDG/default data path), then links skills
|
||||||
|
from that immutable content-addressed location. `.ink-skills.tsv` records each
|
||||||
|
installed skill's archive digest and path without modifying `SKILL.md`.
|
||||||
|
|
||||||
```sh
|
The same artifact works whether it arrived via curl, scp, USB, a browser download,
|
||||||
ink-skills add --ref main git@git.example:team/skills.git
|
or `git archive`. `ink-skills` deliberately has no URL, Git, credential, branch,
|
||||||
ink-skills add https://git.example/team/skills.git skills/review-sql
|
release, or update logic.
|
||||||
```
|
|
||||||
|
|
||||||
`add` resolves the ref to one commit, exports it into a content-addressed store,
|
Ink discovers `$HOME/.ink/skills`, `$CWD/.ink/skills`, and colon-separated
|
||||||
computes a SHA-256 over each selected skill tree, and symlinks that immutable
|
`INK_SKILLS_DIRS`. The installer-only `$INK_SKILLS_HOME` overrides the user link
|
||||||
artifact. Provenance is recorded next to installed links in `.ink-skills.tsv`:
|
target. `ink-skills list` emits TSV; `ink-skills --help` is the complete command
|
||||||
source, requested ref, resolved commit, path, and SHA-256. It deliberately does
|
manual. Existing paths and foreign symlinks are refused rather than overwritten.
|
||||||
not edit `SKILL.md` comments. A moved branch does not silently update an installed
|
|
||||||
skill; the existing pin causes a visible provenance collision.
|
|
||||||
|
|
||||||
Git SSH agents and credential helpers remain Git's job. Credential-bearing HTTP
|
|
||||||
URLs are refused so secrets do not enter manifests or process listings. Remote
|
|
||||||
skill trees containing symlinks are also refused.
|
|
||||||
|
|
||||||
The local `install` path is intentionally smaller than `npx skills`: no registry,
|
|
||||||
package manager, network access, copies, prompts, lockfile, or hidden state. It
|
|
||||||
creates absolute symlinks from `$HOME/.ink/skills` (or the installer-only
|
|
||||||
`$INK_SKILLS_HOME` target override) to this checkout. Pulling the repository
|
|
||||||
updates locally installed skills; restarting Ink freezes the new bytes into the
|
|
||||||
next startup snapshot. Ink itself discovers `$HOME/.ink/skills`,
|
|
||||||
`$CWD/.ink/skills`, and colon-separated `INK_SKILLS_DIRS`.
|
|
||||||
|
|
||||||
`ink-skills list` emits TSV. `ink-skills --help` is the complete command manual.
|
|
||||||
Existing paths and foreign symlinks are refused rather than overwritten.
|
|
||||||
|
|
||||||
## Skills
|
## Skills
|
||||||
|
|
||||||
@@ -88,6 +85,12 @@ Implement the bounded frontend task and return proof.
|
|||||||
`access: read|write` selects reader/writer scheduling; it does not grant commands
|
`access: read|write` selects reader/writer scheduling; it does not grant commands
|
||||||
or tools. Definitions are frozen at startup, so restart Ink after changing one.
|
or tools. Definitions are frozen at startup, so restart Ink after changing one.
|
||||||
|
|
||||||
|
Approved rows from `$HOME/.ink/policy` and ancestor project `.ink/policy` files are
|
||||||
|
additive alternatives. A project file can introduce a command only through the
|
||||||
|
normalized effective-policy digest approval; it is never trusted merely because it
|
||||||
|
exists. Empty project policy adds nothing. Child/session policy and the immutable
|
||||||
|
host floor may only narrow the approved durable rows.
|
||||||
|
|
||||||
Current caveat: Ink records `policy:` as frozen metadata but does not yet read the
|
Current caveat: Ink records `policy:` as frozen metadata but does not yet read the
|
||||||
named relative file into the child effective policy. Do not treat it as enforced.
|
named relative file into the child effective policy. Do not treat it as enforced.
|
||||||
Use the `configure-ink-agent` skill for the exact boundary and blocker.
|
Use the `configure-ink-agent` skill for the exact boundary and blocker.
|
||||||
|
|||||||
+190
-157
@@ -4,41 +4,41 @@ set -eu
|
|||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
usage: ink-skills list
|
usage: ink-skills list
|
||||||
ink-skills install [--user | --project DIR] [SKILL ...]
|
ink-skills link [--user | --project DIR] [SKILL_DIR ...]
|
||||||
ink-skills add [--user | --project DIR] [--ref REF] REPOSITORY [PATH ...]
|
ink-skills import [--user | --project DIR] sha256:HASH ARCHIVE [PATH ...]
|
||||||
|
|
||||||
Install Ink skills as symlinks from this checkout or a pinned Git artifact.
|
Link local Ink skills or import a verified skill archive.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
list List skills in this checkout as TSV.
|
list List skills shipped by this checkout as TSV.
|
||||||
install Link named local skills; with no names, link every skill.
|
link Symlink local skill directories. With no directories, link every
|
||||||
add Fetch REPOSITORY, pin REF to a commit, materialize an
|
skill shipped by this checkout.
|
||||||
immutable snapshot, and link skill PATHs from it. PATH
|
import Verify ARCHIVE against the required SHA-256, safely materialize its
|
||||||
defaults to every skills/*/SKILL.md directory.
|
immutable tree, then link selected skill PATHs. PATH defaults to
|
||||||
|
every skills/*/SKILL.md directory in the archive.
|
||||||
|
|
||||||
Targets:
|
Targets:
|
||||||
--user $INK_SKILLS_HOME or $HOME/.ink/skills (default)
|
--user $INK_SKILLS_HOME or $HOME/.ink/skills (default)
|
||||||
--project DIR DIR/.ink/skills
|
--project DIR DIR/.ink/skills
|
||||||
|
|
||||||
Git storage:
|
Artifact store:
|
||||||
$INK_SKILLS_STORE or $XDG_DATA_HOME/ink-skills, otherwise
|
$INK_SKILLS_STORE or $XDG_DATA_HOME/ink-skills, otherwise
|
||||||
$HOME/.local/share/ink-skills. Git, tar, and sha256sum are required by `add`.
|
$HOME/.local/share/ink-skills.
|
||||||
Authentication belongs to Git's SSH agent or credential helper; credentialed
|
|
||||||
HTTP URLs are refused. Installed provenance is written to
|
Transport is deliberately external:
|
||||||
TARGET/.ink-skills.tsv without modifying SKILL.md.
|
curl -fLo skills.tar URL
|
||||||
|
ink-skills import sha256:HASH skills.tar
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
TSV with SKILL, TARGET, ACTION, SOURCE, COMMIT, and SHA256 columns.
|
TSV with SKILL, TARGET, ACTION, SOURCE, and SHA256 columns.
|
||||||
|
|
||||||
Exit status:
|
Exit status:
|
||||||
0 success; 2 usage error; 3 collision, invalid source, or fetch failure.
|
0 success; 2 usage error; 3 invalid skill, collision, digest mismatch, or
|
||||||
|
unsafe archive.
|
||||||
|
|
||||||
Examples:
|
Requirements:
|
||||||
ink-skills list
|
POSIX sh and standard text tools. `import` additionally needs tar and one of
|
||||||
ink-skills install audit-ink-cli configure-ink-agent
|
sha256sum, shasum, or openssl.
|
||||||
ink-skills install --project . create-ink-tool
|
|
||||||
ink-skills add --ref main git@git.example:team/skills.git
|
|
||||||
ink-skills add https://git.example/team/skills.git skills/review-sql
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +47,22 @@ die() {
|
|||||||
exit 3
|
exit 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||||
|
repo_dir=$(CDPATH= cd -- "$script_dir/.." && pwd -P)
|
||||||
|
bundled_dir=$repo_dir/skills
|
||||||
|
cleanup_root=
|
||||||
|
cleanup_artifact=
|
||||||
|
cleanup_lock=
|
||||||
|
cleanup_all() {
|
||||||
|
if [ -n "$cleanup_artifact" ]; then
|
||||||
|
chmod -R u+w "$cleanup_artifact" 2>/dev/null || :
|
||||||
|
rm -rf "$cleanup_artifact"
|
||||||
|
fi
|
||||||
|
[ -z "$cleanup_lock" ] || rmdir "$cleanup_lock" 2>/dev/null || :
|
||||||
|
[ -z "$cleanup_root" ] || rm -rf "$cleanup_root"
|
||||||
|
}
|
||||||
|
trap cleanup_all EXIT HUP INT TERM
|
||||||
|
|
||||||
reject_record_breaks() {
|
reject_record_breaks() {
|
||||||
case $1 in
|
case $1 in
|
||||||
*" "*|*"
|
*" "*|*"
|
||||||
@@ -54,17 +70,20 @@ reject_record_breaks() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
frontmatter_name() {
|
||||||
repo_dir=$(CDPATH= cd -- "$script_dir/.." && pwd -P)
|
sed -n 's/^name:[[:space:]]*//p' "$1/SKILL.md" | sed -n '1p'
|
||||||
skills_dir=$repo_dir/skills
|
}
|
||||||
|
|
||||||
list_skills() {
|
validate_skill_dir() {
|
||||||
printf 'SKILL\tSOURCE\n'
|
skill_dir=$1
|
||||||
for path in "$skills_dir"/*; do
|
[ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ] || die "not a skill directory: $skill_dir"
|
||||||
[ -d "$path" ] || continue
|
[ ! -L "$skill_dir" ] && [ ! -L "$skill_dir/SKILL.md" ] || die "skill root and SKILL.md must not be symlinks: $skill_dir"
|
||||||
[ -f "$path/SKILL.md" ] || continue
|
skill_name=$(frontmatter_name "$skill_dir")
|
||||||
printf '%s\t%s\n' "$(basename -- "$path")" "$path"
|
[ -n "$skill_name" ] || die "missing frontmatter name: $skill_dir/SKILL.md"
|
||||||
done
|
[ "$skill_name" = "$(basename -- "$skill_dir")" ] || die "frontmatter name does not match directory: $skill_dir"
|
||||||
|
case $skill_name in
|
||||||
|
''|.*|*/*) die "invalid skill name: $skill_name" ;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
select_target() {
|
select_target() {
|
||||||
@@ -85,11 +104,12 @@ select_target() {
|
|||||||
mkdir -p -- "$target"
|
mkdir -p -- "$target"
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_target_option() {
|
parse_target_options() {
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--user)
|
--user)
|
||||||
target_mode=user
|
target_mode=user
|
||||||
shift_count=1
|
shift
|
||||||
;;
|
;;
|
||||||
--project)
|
--project)
|
||||||
[ "$#" -ge 2 ] || {
|
[ "$#" -ge 2 ] || {
|
||||||
@@ -98,35 +118,34 @@ parse_target_option() {
|
|||||||
}
|
}
|
||||||
target_mode=project
|
target_mode=project
|
||||||
target_arg=$2
|
target_arg=$2
|
||||||
shift_count=2
|
shift 2
|
||||||
;;
|
;;
|
||||||
*) shift_count=0 ;;
|
--)
|
||||||
esac
|
shift
|
||||||
}
|
break
|
||||||
|
;;
|
||||||
frontmatter_name() {
|
-*)
|
||||||
sed -n 's/^name:[[:space:]]*//p' "$1/SKILL.md" | sed -n '1p'
|
usage >&2
|
||||||
}
|
exit 2
|
||||||
|
;;
|
||||||
validate_skill_dir() {
|
*) break ;;
|
||||||
source_path=$1
|
|
||||||
[ -d "$source_path" ] && [ -f "$source_path/SKILL.md" ] || die "not a skill directory: $source_path"
|
|
||||||
[ ! -L "$source_path" ] && [ ! -L "$source_path/SKILL.md" ] || die "skill root and SKILL.md must not be symlinks: $source_path"
|
|
||||||
skill_name=$(frontmatter_name "$source_path")
|
|
||||||
[ -n "$skill_name" ] || die "missing frontmatter name: $source_path/SKILL.md"
|
|
||||||
[ "$skill_name" = "$(basename -- "$source_path")" ] || die "frontmatter name does not match directory: $source_path"
|
|
||||||
case $skill_name in
|
|
||||||
''|.*|*/*) die "invalid skill name: $skill_name" ;;
|
|
||||||
esac
|
esac
|
||||||
|
done
|
||||||
|
remaining_count=$#
|
||||||
|
remaining_file=$work_args
|
||||||
|
: >"$remaining_file"
|
||||||
|
for arg do
|
||||||
|
reject_record_breaks "$arg"
|
||||||
|
printf '%s\n' "$arg" >>"$remaining_file"
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
link_skill() {
|
link_skill() {
|
||||||
skill_name=$1
|
name=$1
|
||||||
source_path=$2
|
source_path=$2
|
||||||
source_label=$3
|
source_label=$3
|
||||||
commit=$4
|
digest=$4
|
||||||
digest=$5
|
destination=$target/$name
|
||||||
destination=$target/$skill_name
|
|
||||||
if [ -L "$destination" ]; then
|
if [ -L "$destination" ]; then
|
||||||
linked=$(readlink "$destination")
|
linked=$(readlink "$destination")
|
||||||
[ "$linked" = "$source_path" ] || die "refusing foreign symlink: $destination -> $linked"
|
[ "$linked" = "$source_path" ] || die "refusing foreign symlink: $destination -> $linked"
|
||||||
@@ -137,34 +156,58 @@ link_skill() {
|
|||||||
ln -s -- "$source_path" "$destination"
|
ln -s -- "$source_path" "$destination"
|
||||||
action=linked
|
action=linked
|
||||||
fi
|
fi
|
||||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$skill_name" "$destination" "$action" "$source_label" "$commit" "$digest"
|
printf '%s\t%s\t%s\t%s\t%s\n' "$name" "$destination" "$action" "$source_label" "$digest"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_source_record() {
|
manifest_check() {
|
||||||
skill_name=$1
|
name=$1
|
||||||
source_label=$2
|
digest=$2
|
||||||
ref=$3
|
archive_path=$3
|
||||||
commit=$4
|
|
||||||
skill_path=$5
|
|
||||||
digest=$6
|
|
||||||
manifest=$target/.ink-skills.tsv
|
manifest=$target/.ink-skills.tsv
|
||||||
record_needed=yes
|
record_needed=yes
|
||||||
[ -e "$manifest" ] || return 0
|
[ -f "$manifest" ] || return 0
|
||||||
if awk -F '\t' -v skill="$skill_name" 'NR > 1 && $1 == skill { found = 1 } END { exit !found }' "$manifest"; then
|
if awk -F '\t' -v name="$name" 'NR > 1 && $1 == name { found = 1 } END { exit !found }' "$manifest"; then
|
||||||
existing=$(awk -F '\t' -v skill="$skill_name" 'NR > 1 && $1 == skill { print $0; exit }' "$manifest")
|
existing=$(awk -F '\t' -v name="$name" 'NR > 1 && $1 == name { print $0; exit }' "$manifest")
|
||||||
wanted=$(printf '%s\t%s\t%s\t%s\t%s\t%s' "$skill_name" "$source_label" "$ref" "$commit" "$skill_path" "$digest")
|
wanted=$(printf '%s\t%s\t%s' "$name" "$digest" "$archive_path")
|
||||||
[ "$existing" = "$wanted" ] || die "provenance collision for installed skill: $skill_name"
|
[ "$existing" = "$wanted" ] || die "provenance collision for installed skill: $name"
|
||||||
record_needed=no
|
record_needed=no
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
append_source_record() {
|
manifest_append() {
|
||||||
[ "$record_needed" = yes ] || return 0
|
[ "$record_needed" = yes ] || return 0
|
||||||
manifest=$target/.ink-skills.tsv
|
manifest=$target/.ink-skills.tsv
|
||||||
if [ ! -e "$manifest" ]; then
|
[ -e "$manifest" ] || printf 'SKILL\tSHA256\tPATH\n' >"$manifest"
|
||||||
printf 'SKILL\tSOURCE\tREF\tCOMMIT\tPATH\tSHA256\n' >"$manifest"
|
printf '%s\t%s\t%s\n' "$name" "$digest" "$archive_path" >>"$manifest"
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256_file() {
|
||||||
|
file=$1
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum "$file" | awk '{print $1}'
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then
|
||||||
|
shasum -a 256 "$file" | awk '{print $1}'
|
||||||
|
elif command -v openssl >/dev/null 2>&1; then
|
||||||
|
openssl dgst -sha256 "$file" | sed 's/^.*= //'
|
||||||
|
else
|
||||||
|
die 'import requires sha256sum, shasum, or openssl'
|
||||||
fi
|
fi
|
||||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$skill_name" "$repository" "$ref" "$commit" "$skill_path" "$digest" >>"$manifest"
|
}
|
||||||
|
|
||||||
|
validate_archive_listing() {
|
||||||
|
archive=$1
|
||||||
|
names=$2
|
||||||
|
types=$3
|
||||||
|
tar -tf "$archive" >"$names" || die "cannot list archive: $archive"
|
||||||
|
[ -s "$names" ] || die 'archive is empty'
|
||||||
|
while IFS= read -r member; do
|
||||||
|
reject_record_breaks "$member"
|
||||||
|
case $member in
|
||||||
|
''|/*|..|../*|*/../*|*/..) die "unsafe archive path: $member" ;;
|
||||||
|
esac
|
||||||
|
done <"$names"
|
||||||
|
LC_ALL=C tar -tvf "$archive" >"$types" || die "cannot inspect archive: $archive"
|
||||||
|
awk 'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }' "$types" || die 'archive may contain only regular files and directories'
|
||||||
}
|
}
|
||||||
|
|
||||||
[ "$#" -gt 0 ] || {
|
[ "$#" -gt 0 ] || {
|
||||||
@@ -183,10 +226,14 @@ case $command in
|
|||||||
usage >&2
|
usage >&2
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
list_skills
|
printf 'SKILL\tSOURCE\n'
|
||||||
|
for path in "$bundled_dir"/*; do
|
||||||
|
[ -d "$path" ] && [ -f "$path/SKILL.md" ] || continue
|
||||||
|
printf '%s\t%s\n' "$(basename -- "$path")" "$path"
|
||||||
|
done
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
install|add) ;;
|
link|import) ;;
|
||||||
*)
|
*)
|
||||||
usage >&2
|
usage >&2
|
||||||
exit 2
|
exit 2
|
||||||
@@ -195,74 +242,57 @@ esac
|
|||||||
|
|
||||||
target_mode=user
|
target_mode=user
|
||||||
target_arg=
|
target_arg=
|
||||||
ref=HEAD
|
work_root=${TMPDIR:-/tmp}/ink-skills-args-$$
|
||||||
while [ "$#" -gt 0 ]; do
|
(umask 077 && mkdir "$work_root") || die "cannot create temporary directory: $work_root"
|
||||||
parse_target_option "$@"
|
cleanup_root=$work_root
|
||||||
if [ "$shift_count" -gt 0 ]; then
|
work_args=$work_root/args
|
||||||
shift "$shift_count"
|
parse_target_options "$@"
|
||||||
continue
|
set --
|
||||||
fi
|
while IFS= read -r arg; do set -- "$@" "$arg"; done <"$work_args"
|
||||||
case $1 in
|
|
||||||
--ref)
|
|
||||||
[ "$command" = add ] && [ "$#" -ge 2 ] || {
|
|
||||||
usage >&2
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
ref=$2
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--)
|
|
||||||
shift
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
-*)
|
|
||||||
usage >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
*) break ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
select_target
|
select_target
|
||||||
|
|
||||||
printf 'SKILL\tTARGET\tACTION\tSOURCE\tCOMMIT\tSHA256\n'
|
printf 'SKILL\tTARGET\tACTION\tSOURCE\tSHA256\n'
|
||||||
|
|
||||||
if [ "$command" = install ]; then
|
if [ "$command" = link ]; then
|
||||||
if [ "$#" -eq 0 ]; then
|
if [ "$#" -eq 0 ]; then
|
||||||
set --
|
set --
|
||||||
for path in "$skills_dir"/*; do
|
for path in "$bundled_dir"/*; do
|
||||||
[ -d "$path" ] || continue
|
[ -d "$path" ] && [ -f "$path/SKILL.md" ] || continue
|
||||||
[ -f "$path/SKILL.md" ] || continue
|
set -- "$@" "$path"
|
||||||
set -- "$@" "$(basename -- "$path")"
|
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
for skill do
|
for source_path do
|
||||||
case $skill in
|
case $source_path in
|
||||||
''|.*|*/*) die "invalid skill name: $skill" ;;
|
/*) ;;
|
||||||
|
*) source_path=$(CDPATH= cd -- "$(dirname -- "$source_path")" 2>/dev/null && printf '%s/%s\n' "$PWD" "$(basename -- "$source_path")") || die "skill directory not found: $source_path" ;;
|
||||||
esac
|
esac
|
||||||
source_path=$skills_dir/$skill
|
|
||||||
validate_skill_dir "$source_path"
|
validate_skill_dir "$source_path"
|
||||||
link_skill "$skill_name" "$source_path" local - -
|
link_skill "$skill_name" "$source_path" local -
|
||||||
done
|
done
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ "$#" -gt 0 ] || {
|
[ "$#" -ge 2 ] || {
|
||||||
usage >&2
|
usage >&2
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
repository=$1
|
digest_spec=$1
|
||||||
shift
|
archive=$2
|
||||||
reject_record_breaks "$repository"
|
shift 2
|
||||||
reject_record_breaks "$ref"
|
case $digest_spec in
|
||||||
case $ref in
|
sha256:*) digest=${digest_spec#sha256:} ;;
|
||||||
-*) die "invalid ref: $ref" ;;
|
*) die 'digest must use sha256:HASH' ;;
|
||||||
esac
|
esac
|
||||||
case $repository in
|
digest=$(printf '%s' "$digest" | tr 'A-F' 'a-f')
|
||||||
http://*@*|https://*@*) die 'credentialed HTTP URLs are refused; use an SSH agent or Git credential helper' ;;
|
case $digest in
|
||||||
|
*[!0-9a-f]*|'') die 'SHA-256 must contain 64 hexadecimal characters' ;;
|
||||||
esac
|
esac
|
||||||
command -v git >/dev/null 2>&1 || die 'git is required by add'
|
[ "${#digest}" -eq 64 ] || die 'SHA-256 must contain 64 hexadecimal characters'
|
||||||
command -v tar >/dev/null 2>&1 || die 'tar is required by add'
|
[ -f "$archive" ] || die "archive not found: $archive"
|
||||||
command -v sha256sum >/dev/null 2>&1 || die 'sha256sum is required by add'
|
command -v tar >/dev/null 2>&1 || die 'import requires tar'
|
||||||
|
actual=$(sha256_file "$archive")
|
||||||
|
actual=$(printf '%s' "$actual" | tr 'A-F' 'a-f')
|
||||||
|
[ "$actual" = "$digest" ] || die "SHA-256 mismatch: expected $digest, got $actual"
|
||||||
|
|
||||||
if [ -n "${INK_SKILLS_STORE:-}" ]; then
|
if [ -n "${INK_SKILLS_STORE:-}" ]; then
|
||||||
store=$INK_SKILLS_STORE
|
store=$INK_SKILLS_STORE
|
||||||
@@ -272,48 +302,51 @@ else
|
|||||||
[ -n "${HOME:-}" ] || die 'HOME is unset; set HOME, XDG_DATA_HOME, or INK_SKILLS_STORE'
|
[ -n "${HOME:-}" ] || die 'HOME is unset; set HOME, XDG_DATA_HOME, or INK_SKILLS_STORE'
|
||||||
store=$HOME/.local/share/ink-skills
|
store=$HOME/.local/share/ink-skills
|
||||||
fi
|
fi
|
||||||
repo_key=$(printf '%s' "$repository" | git hash-object --stdin) || die 'cannot hash repository identity'
|
artifact=$store/sha256/$digest
|
||||||
mirror=$store/git/$repo_key.git
|
tree=$artifact/tree
|
||||||
mkdir -p -- "$store/git" "$store/artifacts/$repo_key"
|
if [ -d "$artifact" ]; then
|
||||||
if [ ! -d "$mirror" ]; then
|
[ -f "$artifact/complete" ] && [ "$(cat "$artifact/complete")" = "$digest" ] || die "incomplete artifact store entry: $artifact"
|
||||||
git clone --quiet --mirror -- "$repository" "$mirror" || die "cannot clone repository: $repository"
|
else
|
||||||
|
mkdir -p -- "$store/sha256"
|
||||||
|
lock=$store/sha256/.$digest.lock
|
||||||
|
mkdir "$lock" 2>/dev/null || die "artifact import already in progress: $digest"
|
||||||
|
temporary=$store/sha256/.$digest.tmp.$$
|
||||||
|
cleanup_lock=$lock
|
||||||
|
cleanup_artifact=$temporary
|
||||||
|
mkdir -p -- "$temporary/tree"
|
||||||
|
validate_archive_listing "$archive" "$work_root/names" "$work_root/types"
|
||||||
|
tar -xf "$archive" -C "$temporary/tree" || die "cannot extract archive: $archive"
|
||||||
|
if find "$temporary/tree" -type l -print | grep . >/dev/null 2>&1; then
|
||||||
|
die 'archive extracted symlinks'
|
||||||
fi
|
fi
|
||||||
git --git-dir="$mirror" fetch --quiet --force origin "$ref" || die "cannot fetch ref: $ref"
|
if find "$temporary/tree" ! -type d ! -type f -print | grep . >/dev/null 2>&1; then
|
||||||
commit=$(git --git-dir="$mirror" rev-parse --verify 'FETCH_HEAD^{commit}') || die "ref does not resolve to a commit: $ref"
|
die 'archive extracted non-file entries'
|
||||||
artifact=$store/artifacts/$repo_key/$commit
|
|
||||||
if [ ! -d "$artifact" ]; then
|
|
||||||
temporary=$artifact.tmp.$$
|
|
||||||
rm -rf -- "$temporary"
|
|
||||||
mkdir -p -- "$temporary"
|
|
||||||
if ! git --git-dir="$mirror" archive "$commit" | tar -x -C "$temporary"; then
|
|
||||||
rm -rf -- "$temporary"
|
|
||||||
die "cannot materialize commit: $commit"
|
|
||||||
fi
|
fi
|
||||||
mv -- "$temporary" "$artifact"
|
printf '%s\n' "$digest" >"$temporary/complete"
|
||||||
|
chmod -R a-w "$temporary"
|
||||||
|
mv "$temporary" "$artifact"
|
||||||
|
cleanup_artifact=
|
||||||
|
rmdir "$lock"
|
||||||
|
cleanup_lock=
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$#" -eq 0 ]; then
|
if [ "$#" -eq 0 ]; then
|
||||||
set --
|
set --
|
||||||
for source_path in "$artifact"/skills/*; do
|
for skill_dir in "$tree"/skills/*; do
|
||||||
[ -d "$source_path" ] || continue
|
[ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ] || continue
|
||||||
[ -f "$source_path/SKILL.md" ] || continue
|
set -- "$@" "skills/$(basename -- "$skill_dir")"
|
||||||
set -- "$@" "skills/$(basename -- "$source_path")"
|
|
||||||
done
|
done
|
||||||
[ "$#" -gt 0 ] || die "repository has no skills/*/SKILL.md at $commit"
|
[ "$#" -gt 0 ] || die 'archive has no skills/*/SKILL.md directories'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for skill_path do
|
for archive_path do
|
||||||
reject_record_breaks "$skill_path"
|
case $archive_path in
|
||||||
case $skill_path in
|
''|/*|*:*|..|../*|*/../*|*/..) die "invalid archive skill path: $archive_path" ;;
|
||||||
''|/*|*:*|..|../*|*/../*|*/..) die "invalid repository skill path: $skill_path" ;;
|
|
||||||
esac
|
esac
|
||||||
source_path=$artifact/$skill_path
|
source_path=$tree/$archive_path
|
||||||
validate_skill_dir "$source_path"
|
validate_skill_dir "$source_path"
|
||||||
if find "$source_path" -type l -print | grep . >/dev/null 2>&1; then
|
name=$skill_name
|
||||||
die "remote skill contains symlinks: $skill_path"
|
manifest_check "$name" "$digest" "$archive_path"
|
||||||
fi
|
link_skill "$name" "$source_path" artifact "$digest"
|
||||||
digest=$(git --git-dir="$mirror" archive "$commit:$skill_path" | sha256sum | awk '{print $1}') || die "cannot hash skill artifact: $skill_path"
|
manifest_append
|
||||||
check_source_record "$skill_name" "$repository" "$ref" "$commit" "$skill_path" "$digest"
|
|
||||||
link_skill "$skill_name" "$source_path" "$repository" "$commit" "$digest"
|
|
||||||
append_source_record
|
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -37,14 +37,16 @@ changes belong to Ink's repository authority and implementation workflow.
|
|||||||
- A human/operator may invoke Ink outside the governed agent. Give a copyable
|
- A human/operator may invoke Ink outside the governed agent. Give a copyable
|
||||||
operator command only when the user asks and its public help contract is proven.
|
operator command only when the user asks and its public help contract is proven.
|
||||||
- Use user-provided runtime output as runtime evidence. Otherwise inspect the
|
- Use user-provided runtime output as runtime evidence. Otherwise inspect the
|
||||||
installed artifact identity and a demonstrably matching checkout's requirements,
|
installed artifact identity and a demonstrably matching checkout's specification,
|
||||||
tests, public help text, and source; label those findings as contract/source
|
tests, public help text, and source; label those findings as contract/source
|
||||||
evidence rather than executed runtime proof.
|
evidence rather than executed runtime proof.
|
||||||
- Global and project policy files explain only their contribution. Effective
|
- Approved global and ancestor-project rows are additive alternatives; each file
|
||||||
authority also depends on all policy layers, pinned executable and contract
|
explains only its contribution. Effective authority also depends on the approved
|
||||||
bytes, startup freezing, and session decisions.
|
normalized digest, restrictive child/session policy, the host floor, pinned
|
||||||
|
executable and contract bytes, startup freezing, and session decisions. Never
|
||||||
|
interpret a project file as automatically trusted.
|
||||||
- Treat handovers, READMEs, examples, hidden source branches, and remembered argv
|
- Treat handovers, READMEs, examples, hidden source branches, and remembered argv
|
||||||
as leads. Public help owns operator-facing commands; requirements own intended
|
as leads. Public help owns operator-facing commands; the specification owns intended
|
||||||
behavior; tests and source establish current checkout behavior.
|
behavior; tests and source establish current checkout behavior.
|
||||||
|
|
||||||
Distinguish three surfaces explicitly:
|
Distinguish three surfaces explicitly:
|
||||||
@@ -71,7 +73,7 @@ the frozen `tool delegate` subject.
|
|||||||
**source-confirmed**, **missing**, **stale claim**, or **not proven**.
|
**source-confirmed**, **missing**, **stale claim**, or **not proven**.
|
||||||
4. For an operator action, explain that the user—not the hosted agent—must run it.
|
4. For an operator action, explain that the user—not the hosted agent—must run it.
|
||||||
Do not inspect or mutate session state as a substitute.
|
Do not inspect or mutate session state as a substitute.
|
||||||
5. For a missing capability, require Ink's requirements authority before source
|
5. For a missing capability, require Ink's specification authority before source
|
||||||
implementation. Do not model it as a new external executable merely to bypass
|
implementation. Do not model it as a new external executable merely to bypass
|
||||||
the host boundary.
|
the host boundary.
|
||||||
|
|
||||||
@@ -93,7 +95,7 @@ the frozen `tool delegate` subject.
|
|||||||
EVIDENCE: <runtime output, installed artifact, matching source, or limitation>
|
EVIDENCE: <runtime output, installed artifact, matching source, or limitation>
|
||||||
SURFACE: <operator CLI, model built-in, or admitted external command>
|
SURFACE: <operator CLI, model built-in, or admitted external command>
|
||||||
FINDING: <observed/source-confirmed/missing/stale/not proven>
|
FINDING: <observed/source-confirmed/missing/stale/not proven>
|
||||||
ACTION: <operator step, requirements step, or none>
|
ACTION: <operator step, specification step, or none>
|
||||||
```
|
```
|
||||||
|
|
||||||
Positive smoke: “Does Ink have a sessions command, and why can’t you run it?”
|
Positive smoke: “Does Ink have a sessions command, and why can’t you run it?”
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ no ancestor-chain project-agent discovery.
|
|||||||
- `read` children may overlap and receive an immutable host floor with no command,
|
- `read` children may overlap and receive an immutable host floor with no command,
|
||||||
file-mutation, lifecycle-mutation, or delegation authority.
|
file-mutation, lifecycle-mutation, or delegation authority.
|
||||||
- `write` children are exclusive, operate in the canonical parent cwd, pause
|
- `write` children are exclusive, operate in the canonical parent cwd, pause
|
||||||
parent effects, and still receive only their effective frozen policy.
|
parent mutations, and still receive only their effective frozen policy.
|
||||||
|
|
||||||
Never infer effective authority from `access`, prompt text, or a `policy:` label.
|
Never infer effective authority from `access`, prompt text, or a `policy:` label.
|
||||||
Use the child policy snapshot and a real allowed/denied smoke.
|
Use the child policy snapshot and a real allowed/denied smoke.
|
||||||
@@ -83,11 +83,12 @@ policy. Therefore:
|
|||||||
those are already sufficient.
|
those are already sufficient.
|
||||||
|
|
||||||
This gate may be removed only after the provider launch path proves that the
|
This gate may be removed only after the provider launch path proves that the
|
||||||
referenced bytes are pinned and conjoined into the child effective policy.
|
referenced bytes are pinned and applied as a restriction to the child's inherited
|
||||||
|
approved durable policy.
|
||||||
|
|
||||||
## Decision loop
|
## Decision loop
|
||||||
|
|
||||||
1. Choose `read` unless the child must produce a real effect.
|
1. Choose `read` unless the child must perform a real mutation.
|
||||||
2. Choose the smallest model alias that fits the specialist job.
|
2. Choose the smallest model alias that fits the specialist job.
|
||||||
3. Put the definition in user scope or exact project cwd according to intended
|
3. Put the definition in user scope or exact project cwd according to intended
|
||||||
precedence.
|
precedence.
|
||||||
@@ -107,8 +108,8 @@ referenced bytes are pinned and conjoined into the child effective policy.
|
|||||||
## Behavior smoke
|
## Behavior smoke
|
||||||
|
|
||||||
Positive: “Create a frontend writer child with only formatter and file mutation
|
Positive: “Create a frontend writer child with only formatter and file mutation
|
||||||
authority” loads this skill and blocks until the named policy is actually
|
authority” loads this skill and blocks until the named restrictive policy is
|
||||||
conjoined or the parent frozen policy already supplies that exact boundary.
|
actually enforced or the parent frozen policy already supplies that exact boundary.
|
||||||
|
|
||||||
Negative: “Ask the existing reviewer to inspect this diff” does not load this
|
Negative: “Ask the existing reviewer to inspect this diff” does not load this
|
||||||
skill; it is ordinary delegation.
|
skill; it is ordinary delegation.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ description: >-
|
|||||||
## One job
|
## One job
|
||||||
|
|
||||||
Design a **permission-sized executable** whose name and argv expose its reachable
|
Design a **permission-sized executable** whose name and argv expose its reachable
|
||||||
effects so Ink can discover, digest-pin, and grant it without granting a platform.
|
reads and mutations so Ink can discover, digest-pin, and grant it without granting a platform.
|
||||||
One executable need not mean one source file: share private build-time modules
|
One executable need not mean one source file: share private build-time modules
|
||||||
when that does not widen runtime authority.
|
when that does not widen runtime authority.
|
||||||
|
|
||||||
@@ -21,26 +21,26 @@ when that does not widen runtime authority.
|
|||||||
|
|
||||||
Read only what can change the boundary:
|
Read only what can change the boundary:
|
||||||
|
|
||||||
- the exact job and every reachable side effect;
|
- the exact job and every reachable mutation;
|
||||||
- Ink's current requirements, policy grammar, and mutation protocol;
|
- Ink's current specification, policy grammar, and mutation protocol;
|
||||||
- neighboring tools and existing executables that may already satisfy the job;
|
- neighboring tools and existing executables that may already satisfy the job;
|
||||||
- resource, credential, selector, target, packaging, and proof contracts;
|
- resource, credential, selector, target, packaging, and proof contracts;
|
||||||
- repository requirements and tests.
|
- repository specification and tests.
|
||||||
|
|
||||||
Project authority outranks this skill. Missing selector semantics, credentials,
|
Project authority outranks this skill. Missing selector semantics, credentials,
|
||||||
recovery rules, or external contracts are blockers—not adapter opportunities.
|
recovery rules, or external contracts are blockers—not adapter opportunities.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
JOB -> REUSE? -> EFFECTS -> BOUNDARY -> CONTRACT -> ARTIFACT -> PROOF
|
JOB -> REUSE? -> READS/MUTATIONS -> BOUNDARY -> CONTRACT -> ARTIFACT -> PROOF
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Reuse a directly inspectable executable only when granting its whole reachable
|
1. Reuse a directly inspectable executable only when granting its whole reachable
|
||||||
surface is honest; otherwise build the coherent missing boundary, not a wrapper.
|
surface is honest; otherwise build the coherent missing boundary, not a wrapper.
|
||||||
2. Enumerate reads, mutations, network effects, secrets, state, children, and
|
2. Enumerate reads, mutations, network calls, secrets, state, children, and
|
||||||
config/plugin discovery; split where approval, blast radius, or recovery differ.
|
config/plugin discovery; split where approval, blast radius, or recovery differ.
|
||||||
3. Define argv, selectors, streams, errors, dependencies, exhaustive versus
|
3. Define argv, selectors, streams, errors, dependencies, exhaustive versus
|
||||||
bounded output, and any tool-owned semantic presentation without recreating
|
bounded output, and any tool-owned semantic presentation without recreating
|
||||||
shell grammar or a host-wide effect ontology.
|
shell grammar or a host-wide mutation ontology.
|
||||||
4. State the runtime artifact honestly, then falsify its boundary, behavior,
|
4. State the runtime artifact honestly, then falsify its boundary, behavior,
|
||||||
presentation, and portability claims through Ink's real run entry point.
|
presentation, and portability claims through Ink's real run entry point.
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ Shell globs, path filters, and content patterns are different contracts.
|
|||||||
|
|
||||||
`find`/`fd` forms that execute or delete and `rg` forms that launch preprocessors
|
`find`/`fd` forms that execute or delete and `rg` forms that launch preprocessors
|
||||||
are not read boundaries merely because one intended invocation only searches. A
|
are not read boundaries merely because one intended invocation only searches. A
|
||||||
narrow native search tool is justified when it removes reachable effects, adds
|
narrow native search tool is justified when it removes reachable mutations, adds
|
||||||
canonical path selectors, or supplies the required static portable artifact.
|
canonical path selectors, or supplies the required static portable artifact.
|
||||||
Implement the coherent missing subset, not a compatibility facade or renamed
|
Implement the coherent missing subset, not a compatibility facade or renamed
|
||||||
wrapper.
|
wrapper.
|
||||||
@@ -125,8 +125,8 @@ tool apply <id> <hash> # revalidate; perform once
|
|||||||
|
|
||||||
Required properties:
|
Required properties:
|
||||||
|
|
||||||
- `stage` performs no external effect and resolves no secrets;
|
- `stage` performs no mutation and resolves no secrets;
|
||||||
- the manifest pins executable identity, canonical effect, authority subject,
|
- the manifest pins executable identity, canonical mutation, authority subject,
|
||||||
non-secret inputs, and drift-sensitive hashes;
|
non-secret inputs, and drift-sensitive hashes;
|
||||||
- selectors are variable semantic facts, never argv prefixes, shell text, or
|
- selectors are variable semantic facts, never argv prefixes, shell text, or
|
||||||
executable invariants;
|
executable invariants;
|
||||||
@@ -149,7 +149,7 @@ layout; the tool owns meaning.
|
|||||||
|
|
||||||
Use the repository's elected projection envelope and bounds exactly; never invent
|
Use the repository's elected projection envelope and bounds exactly; never invent
|
||||||
per-tool presentation formats. Within that contract, use a small display
|
per-tool presentation formats. Within that contract, use a small display
|
||||||
vocabulary rather than universal effect kinds:
|
vocabulary rather than universal mutation kinds:
|
||||||
|
|
||||||
- headline and canonical target;
|
- headline and canonical target;
|
||||||
- ordered key/value facts;
|
- ordered key/value facts;
|
||||||
@@ -159,7 +159,7 @@ vocabulary rather than universal effect kinds:
|
|||||||
Filesystem, HTTP, cloud, and infrastructure tools express their own semantics
|
Filesystem, HTTP, cloud, and infrastructure tools express their own semantics
|
||||||
with those primitives. For example, an HTTP mutator supplies method, canonical
|
with those primitives. For example, an HTTP mutator supplies method, canonical
|
||||||
origin/path, bounded body summary, status, and final URL as facts; it does not ask
|
origin/path, bounded body summary, status, and final URL as facts; it does not ask
|
||||||
Ink to understand an `http` effect type. An infrastructure tool supplies account,
|
Ink to understand an `http` mutation type. An infrastructure tool supplies account,
|
||||||
resource, region, and requested change as facts; it does not create a renderer
|
resource, region, and requested change as facts; it does not create a renderer
|
||||||
branch for its provider.
|
branch for its provider.
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ The projection is presentation evidence, never authority:
|
|||||||
- derive projection records purely from that value and hash their exact semantic
|
- derive projection records purely from that value and hash their exact semantic
|
||||||
bytes with the manifest;
|
bytes with the manifest;
|
||||||
- render approval from the exact staged projection stored under that identity;
|
- render approval from the exact staged projection stored under that identity;
|
||||||
- apply accepts only staged id plus hash, never replacement target, body, effect,
|
- apply accepts only staged id plus hash, never replacement target, body, mutation,
|
||||||
or projection inputs;
|
or projection inputs;
|
||||||
- the receipt identifies the exact staged manifest and may add only outcome and
|
- the receipt identifies the exact staged manifest and may add only outcome and
|
||||||
evidence facts; it cannot rewrite approved records;
|
evidence facts; it cannot rewrite approved records;
|
||||||
@@ -189,12 +189,14 @@ otherwise expose manifests, hashes, encoded payloads, or provider internals.
|
|||||||
|
|
||||||
## CLI and stream contract
|
## CLI and stream contract
|
||||||
|
|
||||||
`tool --help` is the tested human contract: effects, argv, streams, ordering,
|
`tool --help` is the tested human contract: mutations, argv, streams, ordering,
|
||||||
exits, environment/config precedence, credential timing, dependencies, pattern
|
exits, environment/config precedence, credential timing, dependencies, pattern
|
||||||
semantics, and one realistic pipeline. Explicit help succeeds on stdout; usage
|
semantics, and one realistic pipeline. Explicit help succeeds on stdout; usage
|
||||||
errors fail on stderr. Selector help gives value grammar, canonicalization, and a
|
errors fail on stderr. Selector help gives value grammar, canonicalization, and a
|
||||||
least-authority policy row, including AND within one row and alternatives across
|
least-authority policy row, including AND within one row and alternatives across
|
||||||
rows.
|
approved global and ancestor-project rows. Project rows are not automatically
|
||||||
|
trusted: Ink freezes the normalized effective policy and requires digest approval;
|
||||||
|
child/session policy and the host floor may only narrow it.
|
||||||
|
|
||||||
Keep argv unsurprising: options before operands, `--` ends options, `-` denotes a
|
Keep argv unsurprising: options before operands, `--` ends options, `-` denotes a
|
||||||
natural stream, secrets never enter argv, and unknown, incompatible, or trailing
|
natural stream, secrets never enter argv, and unknown, incompatible, or trailing
|
||||||
@@ -250,7 +252,7 @@ interpreters, or generated-client machinery unless they are the named job.
|
|||||||
Use the smallest matrix that can falsify the actual claims:
|
Use the smallest matrix that can falsify the actual claims:
|
||||||
|
|
||||||
- help/contract agree with accepted argv and selectors;
|
- help/contract agree with accepted argv and selectors;
|
||||||
- main path and one forbidden near miss with zero unintended effect;
|
- main path and one forbidden near miss with zero unintended mutation;
|
||||||
- each reader selector has an adjacent no-match before access;
|
- each reader selector has an adjacent no-match before access;
|
||||||
- each mutator has admitted, approval-required, refused, drift, duplicate, and
|
- each mutator has admitted, approval-required, refused, drift, duplicate, and
|
||||||
indeterminate/recovery outcomes as applicable;
|
indeterminate/recovery outcomes as applicable;
|
||||||
@@ -278,7 +280,7 @@ Do not:
|
|||||||
- wrap or partially clone a utility whose whole admitted surface already fits;
|
- wrap or partially clone a utility whose whole admitted surface already fits;
|
||||||
- combine read and mutation for code reuse;
|
- combine read and mutation for code reuse;
|
||||||
- expose a generic request/admin/registry platform;
|
- expose a generic request/admin/registry platform;
|
||||||
- invent invariant selectors, a universal effect ontology, or executable-name
|
- invent invariant selectors, a universal mutation ontology, or executable-name
|
||||||
branches in Ink's renderer;
|
branches in Ink's renderer;
|
||||||
- let tools choose terminal styling or let presentation metadata grant authority;
|
- let tools choose terminal styling or let presentation metadata grant authority;
|
||||||
- treat argv prefixes, globs, or regexes as canonical path authority;
|
- treat argv prefixes, globs, or regexes as canonical path authority;
|
||||||
@@ -312,7 +314,7 @@ Report only:
|
|||||||
- **Presentation:** an HTTP mutator derives method, canonical target, and bounded
|
- **Presentation:** an HTTP mutator derives method, canonical target, and bounded
|
||||||
body summary from one staged operation; Ink renders the exact stored projection
|
body summary from one staged operation; Ink renders the exact stored projection
|
||||||
without an `httpsend` branch and overlays receipt-bound status/final URL only.
|
without an `httpsend` branch and overlays receipt-bound status/final URL only.
|
||||||
- **Presentation sprawl:** “support AWS changes” does not add an AWS effect enum or
|
- **Presentation sprawl:** “support AWS changes” does not add an AWS mutation enum or
|
||||||
renderer branch; the permission-sized infrastructure tool projects account,
|
renderer branch; the permission-sized infrastructure tool projects account,
|
||||||
region, resource, requested change, outcome, and evidence through the elected
|
region, resource, requested change, outcome, and evidence through the elected
|
||||||
generic vocabulary.
|
generic vocabulary.
|
||||||
|
|||||||
+59
-39
@@ -3,7 +3,11 @@ set -eu
|
|||||||
|
|
||||||
repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
|
repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
|
||||||
tmp=${TMPDIR:-/tmp}/ink-skills-smoke-$$
|
tmp=${TMPDIR:-/tmp}/ink-skills-smoke-$$
|
||||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
cleanup() {
|
||||||
|
chmod -R u+w "$tmp" 2>/dev/null || :
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
mkdir -p "$tmp/home" "$tmp/project"
|
mkdir -p "$tmp/home" "$tmp/project"
|
||||||
|
|
||||||
list=$($repo/bin/ink-skills list)
|
list=$($repo/bin/ink-skills list)
|
||||||
@@ -11,28 +15,34 @@ printf '%s\n' "$list" | grep '^SKILL' >/dev/null
|
|||||||
printf '%s\n' "$list" | grep '^audit-ink-cli' >/dev/null
|
printf '%s\n' "$list" | grep '^audit-ink-cli' >/dev/null
|
||||||
printf '%s\n' "$list" | grep '^configure-ink-agent' >/dev/null
|
printf '%s\n' "$list" | grep '^configure-ink-agent' >/dev/null
|
||||||
|
|
||||||
HOME=$tmp/home $repo/bin/ink-skills install audit-ink-cli >"$tmp/install.tsv"
|
HOME=$tmp/home $repo/bin/ink-skills link >"$tmp/link.tsv"
|
||||||
grep "audit-ink-cli.*linked" "$tmp/install.tsv" >/dev/null
|
grep "audit-ink-cli.*linked.*local" "$tmp/link.tsv" >/dev/null
|
||||||
[ -L "$tmp/home/.ink/skills/audit-ink-cli" ]
|
[ -L "$tmp/home/.ink/skills/audit-ink-cli" ]
|
||||||
[ "$(readlink "$tmp/home/.ink/skills/audit-ink-cli")" = "$repo/skills/audit-ink-cli" ]
|
[ "$(readlink "$tmp/home/.ink/skills/audit-ink-cli")" = "$repo/skills/audit-ink-cli" ]
|
||||||
|
|
||||||
HOME=$tmp/home $repo/bin/ink-skills install audit-ink-cli >"$tmp/reinstall.tsv"
|
HOME=$tmp/home $repo/bin/ink-skills link "$repo/skills/audit-ink-cli" >"$tmp/relink.tsv"
|
||||||
grep "audit-ink-cli.*unchanged" "$tmp/reinstall.tsv" >/dev/null
|
grep "audit-ink-cli.*unchanged" "$tmp/relink.tsv" >/dev/null
|
||||||
|
|
||||||
mkdir -p "$tmp/home/.ink/skills/configure-ink-agent"
|
mkdir -p "$tmp/home/.ink/skills/collision"
|
||||||
if HOME=$tmp/home $repo/bin/ink-skills install configure-ink-agent >/dev/null 2>"$tmp/collision.err"; then
|
mkdir -p "$tmp/collision"
|
||||||
|
cat >"$tmp/collision/SKILL.md" <<'EOF'
|
||||||
|
---
|
||||||
|
name: collision
|
||||||
|
description: Fixture.
|
||||||
|
---
|
||||||
|
EOF
|
||||||
|
if HOME=$tmp/home $repo/bin/ink-skills link "$tmp/collision" >/dev/null 2>"$tmp/collision.err"; then
|
||||||
echo 'expected collision refusal' >&2
|
echo 'expected collision refusal' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
grep 'refusing existing path' "$tmp/collision.err" >/dev/null
|
grep 'refusing existing path' "$tmp/collision.err" >/dev/null
|
||||||
|
|
||||||
HOME=$tmp/home $repo/bin/ink-skills install --project "$tmp/project" create-ink-tool >"$tmp/project.tsv"
|
HOME=$tmp/home $repo/bin/ink-skills link --project "$tmp/project" "$repo/skills/create-ink-tool" >"$tmp/project.tsv"
|
||||||
[ -L "$tmp/project/.ink/skills/create-ink-tool" ]
|
[ -L "$tmp/project/.ink/skills/create-ink-tool" ]
|
||||||
grep "create-ink-tool.*linked" "$tmp/project.tsv" >/dev/null
|
grep "create-ink-tool.*linked.*local" "$tmp/project.tsv" >/dev/null
|
||||||
|
|
||||||
remote=$tmp/remote
|
mkdir -p "$tmp/archive-tree/skills/remote-review"
|
||||||
mkdir -p "$remote/skills/remote-review"
|
cat >"$tmp/archive-tree/skills/remote-review/SKILL.md" <<'EOF'
|
||||||
cat >"$remote/skills/remote-review/SKILL.md" <<'EOF'
|
|
||||||
---
|
---
|
||||||
name: remote-review
|
name: remote-review
|
||||||
description: Review one remote fixture.
|
description: Review one remote fixture.
|
||||||
@@ -40,43 +50,53 @@ description: Review one remote fixture.
|
|||||||
|
|
||||||
# Remote review
|
# Remote review
|
||||||
EOF
|
EOF
|
||||||
git -C "$remote" init -q
|
tar -cf "$tmp/skills.tar" -C "$tmp/archive-tree" skills
|
||||||
git -C "$remote" config user.email ink-skills@example.invalid
|
digest=$(sha256sum "$tmp/skills.tar" | awk '{print $1}')
|
||||||
git -C "$remote" config user.name 'Ink Skills Test'
|
|
||||||
git -C "$remote" add skills/remote-review/SKILL.md
|
|
||||||
git -C "$remote" commit -qm fixture
|
|
||||||
commit=$(git -C "$remote" rev-parse HEAD)
|
|
||||||
|
|
||||||
mkdir -p "$tmp/remote-home"
|
mkdir -p "$tmp/import-home"
|
||||||
HOME=$tmp/remote-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills add --ref HEAD "$remote" skills/remote-review >"$tmp/add.tsv"
|
HOME=$tmp/import-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills import "sha256:$digest" "$tmp/skills.tar" >"$tmp/import.tsv"
|
||||||
remote_link=$tmp/remote-home/.ink/skills/remote-review
|
remote_link=$tmp/import-home/.ink/skills/remote-review
|
||||||
[ -L "$remote_link" ]
|
[ -L "$remote_link" ]
|
||||||
case $(readlink "$remote_link") in
|
[ "$(readlink "$remote_link")" = "$tmp/store/sha256/$digest/tree/skills/remote-review" ]
|
||||||
"$tmp/store/artifacts/"*"/$commit/skills/remote-review") ;;
|
grep "remote-review.*linked.*artifact.*$digest" "$tmp/import.tsv" >/dev/null
|
||||||
*) echo 'remote link is not pinned to resolved commit' >&2; exit 1 ;;
|
manifest=$tmp/import-home/.ink/skills/.ink-skills.tsv
|
||||||
esac
|
|
||||||
grep "remote-review.*linked.*$commit" "$tmp/add.tsv" >/dev/null
|
|
||||||
manifest=$tmp/remote-home/.ink/skills/.ink-skills.tsv
|
|
||||||
[ "$(wc -l <"$manifest")" -eq 2 ]
|
[ "$(wc -l <"$manifest")" -eq 2 ]
|
||||||
awk -F '\t' -v commit="$commit" 'NR == 2 { exit !($1 == "remote-review" && $4 == commit && length($6) == 64) }' "$manifest"
|
awk -F '\t' -v digest="$digest" 'NR == 2 { exit !($1 == "remote-review" && $2 == digest && $3 == "skills/remote-review") }' "$manifest"
|
||||||
|
|
||||||
HOME=$tmp/remote-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills add --ref HEAD "$remote" skills/remote-review >"$tmp/readd.tsv"
|
HOME=$tmp/import-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills import "sha256:$digest" "$tmp/skills.tar" skills/remote-review >"$tmp/reimport.tsv"
|
||||||
grep 'remote-review.*unchanged' "$tmp/readd.tsv" >/dev/null
|
grep 'remote-review.*unchanged' "$tmp/reimport.tsv" >/dev/null
|
||||||
[ "$(wc -l <"$manifest")" -eq 2 ]
|
[ "$(wc -l <"$manifest")" -eq 2 ]
|
||||||
|
|
||||||
printf '\nchanged\n' >>"$remote/skills/remote-review/SKILL.md"
|
zero_digest=$(awk 'BEGIN { for (i = 0; i < 64; i++) printf "0" }')
|
||||||
git -C "$remote" add skills/remote-review/SKILL.md
|
if HOME=$tmp/import-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills import "sha256:$zero_digest" "$tmp/skills.tar" >/dev/null 2>"$tmp/digest.err"; then
|
||||||
git -C "$remote" commit -qm changed
|
echo 'expected digest mismatch' >&2
|
||||||
if HOME=$tmp/remote-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills add --ref HEAD "$remote" skills/remote-review >/dev/null 2>"$tmp/pin.err"; then
|
|
||||||
echo 'expected provenance collision after ref moves' >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
grep 'provenance collision' "$tmp/pin.err" >/dev/null
|
grep 'SHA-256 mismatch' "$tmp/digest.err" >/dev/null
|
||||||
|
|
||||||
if HOME=$tmp/remote-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills add 'https://user:secret@example.invalid/skills.git' >/dev/null 2>"$tmp/credential.err"; then
|
printf '\nchanged\n' >>"$tmp/archive-tree/skills/remote-review/SKILL.md"
|
||||||
echo 'expected credentialed URL refusal' >&2
|
tar -cf "$tmp/changed.tar" -C "$tmp/archive-tree" skills
|
||||||
|
changed_digest=$(sha256sum "$tmp/changed.tar" | awk '{print $1}')
|
||||||
|
if HOME=$tmp/import-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills import "sha256:$changed_digest" "$tmp/changed.tar" >/dev/null 2>"$tmp/provenance.err"; then
|
||||||
|
echo 'expected provenance collision after artifact changes' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
grep 'credentialed HTTP URLs are refused' "$tmp/credential.err" >/dev/null
|
grep 'provenance collision' "$tmp/provenance.err" >/dev/null
|
||||||
|
|
||||||
|
mkdir -p "$tmp/unsafe/skills/unsafe"
|
||||||
|
cat >"$tmp/unsafe/skills/unsafe/SKILL.md" <<'EOF'
|
||||||
|
---
|
||||||
|
name: unsafe
|
||||||
|
description: Unsafe fixture.
|
||||||
|
---
|
||||||
|
EOF
|
||||||
|
ln -s /etc/passwd "$tmp/unsafe/skills/unsafe/passwd"
|
||||||
|
tar -cf "$tmp/unsafe.tar" -C "$tmp/unsafe" skills
|
||||||
|
unsafe_digest=$(sha256sum "$tmp/unsafe.tar" | awk '{print $1}')
|
||||||
|
if HOME=$tmp/import-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills import "sha256:$unsafe_digest" "$tmp/unsafe.tar" >/dev/null 2>"$tmp/unsafe.err"; then
|
||||||
|
echo 'expected symlink archive refusal' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep 'regular files and directories' "$tmp/unsafe.err" >/dev/null
|
||||||
|
|
||||||
printf 'ok\n'
|
printf 'ok\n'
|
||||||
|
|||||||
@@ -23,13 +23,20 @@ configure=$repo/skills/configure-ink-agent/SKILL.md
|
|||||||
grep 'access.*read.*write' "$configure" >/dev/null
|
grep 'access.*read.*write' "$configure" >/dev/null
|
||||||
grep 'named agent policy is metadata-only' "$configure" >/dev/null
|
grep 'named agent policy is metadata-only' "$configure" >/dev/null
|
||||||
grep 'no `INK_AGENT_HOME` contract' "$configure" >/dev/null
|
grep 'no `INK_AGENT_HOME` contract' "$configure" >/dev/null
|
||||||
|
grep 'applied as a restriction' "$configure" >/dev/null
|
||||||
|
|
||||||
audit_cli=$repo/skills/audit-ink-cli/SKILL.md
|
audit_cli=$repo/skills/audit-ink-cli/SKILL.md
|
||||||
grep 'ink agent catalog' "$audit_cli" >/dev/null
|
grep 'ink agent catalog' "$audit_cli" >/dev/null
|
||||||
grep 'frozen `tool delegate` subject' "$audit_cli" >/dev/null
|
grep 'frozen `tool delegate` subject' "$audit_cli" >/dev/null
|
||||||
|
grep 'additive alternatives' "$audit_cli" >/dev/null
|
||||||
|
grep 'automatically trusted' "$audit_cli" >/dev/null
|
||||||
|
|
||||||
create_tool=$repo/skills/create-ink-tool/SKILL.md
|
create_tool=$repo/skills/create-ink-tool/SKILL.md
|
||||||
grep 'stage/match/apply' "$create_tool" >/dev/null
|
grep 'stage/match/apply' "$create_tool" >/dev/null
|
||||||
grep 'projection' "$create_tool" >/dev/null
|
grep 'projection' "$create_tool" >/dev/null
|
||||||
|
grep 'approved global and ancestor-project rows' "$create_tool" >/dev/null
|
||||||
|
grep 'requires digest approval' "$create_tool" >/dev/null
|
||||||
|
|
||||||
|
grep 'Empty project policy adds nothing' "$repo/README.md" >/dev/null
|
||||||
|
|
||||||
printf 'ok\n'
|
printf 'ok\n'
|
||||||
|
|||||||
Reference in New Issue
Block a user