Compare commits
6 Commits
284cf5ec90
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 92a2849376 | |||
| f6d0efdfc0 | |||
| 8547d6ea33 | |||
| efc4b70e13 | |||
| cb555a41b2 | |||
| 5552014329 |
@@ -5,14 +5,16 @@
|
||||
## Layout
|
||||
|
||||
- `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.
|
||||
|
||||
## Rules
|
||||
|
||||
- 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.
|
||||
- The installer only creates symlinks and must refuse collisions. Do not add a registry,
|
||||
network calls, package-manager dependency, prompts, copies, or hidden state.
|
||||
- Run `sh -n bin/ink-skills` and `sh test/install-smoke.sh` after changes.
|
||||
- `link` only creates symlinks. `import` may materialize only a caller-supplied,
|
||||
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
|
||||
`sh test/skills-smoke.sh` after changes.
|
||||
|
||||
@@ -7,54 +7,71 @@ This repository owns reusable Ink judgment. Ink owns runtime enforcement and
|
||||
policy. [`toolset`](https://git.tmk241.com/tmk241/toolset) owns compiled external
|
||||
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
|
||||
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
|
||||
/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
|
||||
ink-skills install 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
|
||||
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
|
||||
```
|
||||
|
||||
The installer 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 `$INK_SKILLS_HOME`) to this
|
||||
checkout. Pulling the repository updates installed skills; restarting Ink freezes
|
||||
the new bytes into the next startup snapshot.
|
||||
`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`.
|
||||
|
||||
`ink-skills list` emits TSV. `ink-skills --help` is the complete command manual.
|
||||
Existing paths and foreign symlinks are refused rather than overwritten.
|
||||
The same artifact works whether it arrived via curl, scp, USB, a browser download,
|
||||
or `git archive`. `ink-skills` deliberately has no URL, Git, credential, branch,
|
||||
release, or update logic.
|
||||
|
||||
Ink discovers `$HOME/.ink/skills`, `$CWD/.ink/skills`, and colon-separated
|
||||
`INK_SKILLS_DIRS`. The installer-only `$INK_SKILLS_HOME` overrides the user link
|
||||
target. `ink-skills list` emits TSV; `ink-skills --help` is the complete command
|
||||
manual. Existing paths and foreign symlinks are refused rather than overwritten.
|
||||
|
||||
## Skills
|
||||
|
||||
| Skill | Job |
|
||||
|---|---|
|
||||
| `ink-cli` | Audit and explain the Ink host without crossing the host/guest boundary. |
|
||||
| `configure-ink-agent` | Create or audit one Ink agent definition, access class, and relative policy conjunct. |
|
||||
| `create-ink-agent-cli-tool` | Build one inspectable permission-bearing executable suitable for Ink policy admission. |
|
||||
| `audit-ink-cli` | Audit and explain the Ink host without crossing the host/guest boundary. |
|
||||
| `configure-ink-agent` | Create or audit one Ink agent definition, access class, and effective-policy boundary. |
|
||||
| `create-ink-tool` | Build one inspectable permission-bearing executable suitable for Ink policy admission. |
|
||||
|
||||
## Agent definitions and policy
|
||||
|
||||
Agent definitions live in `$INK_AGENT_HOME` (default `$HOME/.ink/agents`) or
|
||||
project `.ink/agents` directories:
|
||||
Agent definitions live in `$HOME/.ink/agents` or the exact current project's
|
||||
`.ink/agents` directory:
|
||||
|
||||
```text
|
||||
name: Frontend specialist
|
||||
@@ -65,24 +82,31 @@ policy: frontend.policy
|
||||
Implement the bounded frontend task and return proof.
|
||||
```
|
||||
|
||||
The policy path is relative to the definition. It is a normal Ink policy file and
|
||||
narrows the frozen parent snapshot conjunctively. `access: read|write` selects
|
||||
reader/writer scheduling; it does not grant commands or tools. Definitions and
|
||||
policy files are frozen at startup, so restart Ink after changing either.
|
||||
`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.
|
||||
|
||||
Use the `configure-ink-agent` skill for the complete decision boundary.
|
||||
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
|
||||
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.
|
||||
|
||||
## Verify
|
||||
|
||||
```sh
|
||||
sh -n bin/ink-skills
|
||||
sh test/install-smoke.sh
|
||||
sh test/skills-smoke.sh
|
||||
```
|
||||
|
||||
## Refusals
|
||||
|
||||
- No npm package merely to create symlinks.
|
||||
- No skill registry or update daemon.
|
||||
- No skill registry, automatic updater, or network daemon.
|
||||
- No policy mutation during installation.
|
||||
- No bundled binaries; those belong in `toolset`.
|
||||
- No automatic installation by Ink itself.
|
||||
|
||||
+283
-82
@@ -4,28 +4,41 @@ set -eu
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
usage: ink-skills list
|
||||
ink-skills install [--user | --project DIR] [SKILL ...]
|
||||
ink-skills link [--user | --project DIR] [SKILL_DIR ...]
|
||||
ink-skills import [--user | --project DIR] sha256:HASH ARCHIVE [PATH ...]
|
||||
|
||||
Install Ink skills from this checkout as symlinks.
|
||||
Link local Ink skills or import a verified skill archive.
|
||||
|
||||
Commands:
|
||||
list List available skill names and source paths as TSV.
|
||||
install Link named skills; with no names, link every skill.
|
||||
list List skills shipped by this checkout as TSV.
|
||||
link Symlink local skill directories. With no directories, link every
|
||||
skill shipped by this checkout.
|
||||
import Verify ARCHIVE against the required SHA-256, safely materialize its
|
||||
immutable tree, then link selected skill PATHs. PATH defaults to
|
||||
every skills/*/SKILL.md directory in the archive.
|
||||
|
||||
Targets:
|
||||
--user $INK_SKILLS_HOME or $HOME/.ink/skills (default)
|
||||
--project DIR DIR/.ink/skills
|
||||
|
||||
Artifact store:
|
||||
$INK_SKILLS_STORE or $XDG_DATA_HOME/ink-skills, otherwise
|
||||
$HOME/.local/share/ink-skills.
|
||||
|
||||
Transport is deliberately external:
|
||||
curl -fLo skills.tar URL
|
||||
ink-skills import sha256:HASH skills.tar
|
||||
|
||||
Output:
|
||||
TSV with SKILL, TARGET, and ACTION columns.
|
||||
TSV with SKILL, TARGET, ACTION, SOURCE, and SHA256 columns.
|
||||
|
||||
Exit status:
|
||||
0 success; 2 usage error; 3 target collision or invalid skill.
|
||||
0 success; 2 usage error; 3 invalid skill, collision, digest mismatch, or
|
||||
unsafe archive.
|
||||
|
||||
Examples:
|
||||
ink-skills list
|
||||
ink-skills install ink-cli configure-ink-agent
|
||||
ink-skills install --project . create-ink-agent-cli-tool
|
||||
Requirements:
|
||||
POSIX sh and standard text tools. `import` additionally needs tar and one of
|
||||
sha256sum, shasum, or openssl.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -36,22 +49,171 @@ die() {
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
repo_dir=$(CDPATH= cd -- "$script_dir/.." && pwd -P)
|
||||
skills_dir=$repo_dir/skills
|
||||
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
|
||||
|
||||
list_skills() {
|
||||
printf 'SKILL\tSOURCE\n'
|
||||
for path in "$skills_dir"/*; do
|
||||
[ -d "$path" ] || continue
|
||||
[ -f "$path/SKILL.md" ] || continue
|
||||
printf '%s\t%s\n' "$(basename -- "$path")" "$path"
|
||||
reject_record_breaks() {
|
||||
case $1 in
|
||||
*" "*|*"
|
||||
"*) die "tabs and newlines are not allowed: $1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
frontmatter_name() {
|
||||
sed -n 's/^name:[[:space:]]*//p' "$1/SKILL.md" | sed -n '1p'
|
||||
}
|
||||
|
||||
validate_skill_dir() {
|
||||
skill_dir=$1
|
||||
[ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ] || die "not a skill directory: $skill_dir"
|
||||
[ ! -L "$skill_dir" ] && [ ! -L "$skill_dir/SKILL.md" ] || die "skill root and SKILL.md must not be symlinks: $skill_dir"
|
||||
skill_name=$(frontmatter_name "$skill_dir")
|
||||
[ -n "$skill_name" ] || die "missing frontmatter name: $skill_dir/SKILL.md"
|
||||
[ "$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() {
|
||||
case $target_mode in
|
||||
user)
|
||||
if [ -n "${INK_SKILLS_HOME:-}" ]; then
|
||||
target=$INK_SKILLS_HOME
|
||||
else
|
||||
[ -n "${HOME:-}" ] || die 'HOME is unset; set HOME or INK_SKILLS_HOME'
|
||||
target=$HOME/.ink/skills
|
||||
fi
|
||||
;;
|
||||
project)
|
||||
project=$(CDPATH= cd -- "$target_arg" 2>/dev/null && pwd -P) || die "project directory not found: $target_arg"
|
||||
target=$project/.ink/skills
|
||||
;;
|
||||
esac
|
||||
mkdir -p -- "$target"
|
||||
}
|
||||
|
||||
parse_target_options() {
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case $1 in
|
||||
--user)
|
||||
target_mode=user
|
||||
shift
|
||||
;;
|
||||
--project)
|
||||
[ "$#" -ge 2 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
target_mode=project
|
||||
target_arg=$2
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
*) break ;;
|
||||
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() {
|
||||
name=$1
|
||||
source_path=$2
|
||||
source_label=$3
|
||||
digest=$4
|
||||
destination=$target/$name
|
||||
if [ -L "$destination" ]; then
|
||||
linked=$(readlink "$destination")
|
||||
[ "$linked" = "$source_path" ] || die "refusing foreign symlink: $destination -> $linked"
|
||||
action=unchanged
|
||||
elif [ -e "$destination" ]; then
|
||||
die "refusing existing path: $destination"
|
||||
else
|
||||
ln -s -- "$source_path" "$destination"
|
||||
action=linked
|
||||
fi
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "$name" "$destination" "$action" "$source_label" "$digest"
|
||||
}
|
||||
|
||||
manifest_check() {
|
||||
name=$1
|
||||
digest=$2
|
||||
archive_path=$3
|
||||
manifest=$target/.ink-skills.tsv
|
||||
record_needed=yes
|
||||
[ -f "$manifest" ] || return 0
|
||||
if awk -F '\t' -v name="$name" 'NR > 1 && $1 == name { found = 1 } END { exit !found }' "$manifest"; then
|
||||
existing=$(awk -F '\t' -v name="$name" 'NR > 1 && $1 == name { print $0; exit }' "$manifest")
|
||||
wanted=$(printf '%s\t%s\t%s' "$name" "$digest" "$archive_path")
|
||||
[ "$existing" = "$wanted" ] || die "provenance collision for installed skill: $name"
|
||||
record_needed=no
|
||||
fi
|
||||
}
|
||||
|
||||
manifest_append() {
|
||||
[ "$record_needed" = yes ] || return 0
|
||||
manifest=$target/.ink-skills.tsv
|
||||
[ -e "$manifest" ] || printf 'SKILL\tSHA256\tPATH\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
|
||||
}
|
||||
|
||||
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 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
command=$1
|
||||
shift
|
||||
case $command in
|
||||
@@ -64,10 +226,14 @@ case $command in
|
||||
usage >&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
|
||||
;;
|
||||
install) ;;
|
||||
link|import) ;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
@@ -76,76 +242,111 @@ esac
|
||||
|
||||
target_mode=user
|
||||
target_arg=
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case $1 in
|
||||
--user)
|
||||
target_mode=user
|
||||
shift
|
||||
;;
|
||||
--project)
|
||||
[ "$#" -ge 2 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
target_mode=project
|
||||
target_arg=$2
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
work_root=${TMPDIR:-/tmp}/ink-skills-args-$$
|
||||
(umask 077 && mkdir "$work_root") || die "cannot create temporary directory: $work_root"
|
||||
cleanup_root=$work_root
|
||||
work_args=$work_root/args
|
||||
parse_target_options "$@"
|
||||
set --
|
||||
while IFS= read -r arg; do set -- "$@" "$arg"; done <"$work_args"
|
||||
select_target
|
||||
|
||||
case $target_mode in
|
||||
user)
|
||||
if [ -n "${INK_SKILLS_HOME:-}" ]; then
|
||||
target=$INK_SKILLS_HOME
|
||||
else
|
||||
[ -n "${HOME:-}" ] || die 'HOME is unset; set HOME or INK_SKILLS_HOME'
|
||||
target=$HOME/.ink/skills
|
||||
fi
|
||||
;;
|
||||
project)
|
||||
project=$(CDPATH= cd -- "$target_arg" 2>/dev/null && pwd -P) || die "project directory not found: $target_arg"
|
||||
target=$project/.ink/skills
|
||||
;;
|
||||
printf 'SKILL\tTARGET\tACTION\tSOURCE\tSHA256\n'
|
||||
|
||||
if [ "$command" = link ]; then
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set --
|
||||
for path in "$bundled_dir"/*; do
|
||||
[ -d "$path" ] && [ -f "$path/SKILL.md" ] || continue
|
||||
set -- "$@" "$path"
|
||||
done
|
||||
fi
|
||||
for source_path do
|
||||
case $source_path in
|
||||
/*) ;;
|
||||
*) 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
|
||||
validate_skill_dir "$source_path"
|
||||
link_skill "$skill_name" "$source_path" local -
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ "$#" -ge 2 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
digest_spec=$1
|
||||
archive=$2
|
||||
shift 2
|
||||
case $digest_spec in
|
||||
sha256:*) digest=${digest_spec#sha256:} ;;
|
||||
*) die 'digest must use sha256:HASH' ;;
|
||||
esac
|
||||
digest=$(printf '%s' "$digest" | tr 'A-F' 'a-f')
|
||||
case $digest in
|
||||
*[!0-9a-f]*|'') die 'SHA-256 must contain 64 hexadecimal characters' ;;
|
||||
esac
|
||||
[ "${#digest}" -eq 64 ] || die 'SHA-256 must contain 64 hexadecimal characters'
|
||||
[ -f "$archive" ] || die "archive not found: $archive"
|
||||
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"
|
||||
|
||||
mkdir -p -- "$target"
|
||||
if [ -n "${INK_SKILLS_STORE:-}" ]; then
|
||||
store=$INK_SKILLS_STORE
|
||||
elif [ -n "${XDG_DATA_HOME:-}" ]; then
|
||||
store=$XDG_DATA_HOME/ink-skills
|
||||
else
|
||||
[ -n "${HOME:-}" ] || die 'HOME is unset; set HOME, XDG_DATA_HOME, or INK_SKILLS_STORE'
|
||||
store=$HOME/.local/share/ink-skills
|
||||
fi
|
||||
artifact=$store/sha256/$digest
|
||||
tree=$artifact/tree
|
||||
if [ -d "$artifact" ]; then
|
||||
[ -f "$artifact/complete" ] && [ "$(cat "$artifact/complete")" = "$digest" ] || die "incomplete artifact store entry: $artifact"
|
||||
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
|
||||
if find "$temporary/tree" ! -type d ! -type f -print | grep . >/dev/null 2>&1; then
|
||||
die 'archive extracted non-file entries'
|
||||
fi
|
||||
printf '%s\n' "$digest" >"$temporary/complete"
|
||||
chmod -R a-w "$temporary"
|
||||
mv "$temporary" "$artifact"
|
||||
cleanup_artifact=
|
||||
rmdir "$lock"
|
||||
cleanup_lock=
|
||||
fi
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set --
|
||||
for path in "$skills_dir"/*; do
|
||||
[ -d "$path" ] || continue
|
||||
[ -f "$path/SKILL.md" ] || continue
|
||||
set -- "$@" "$(basename -- "$path")"
|
||||
for skill_dir in "$tree"/skills/*; do
|
||||
[ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ] || continue
|
||||
set -- "$@" "skills/$(basename -- "$skill_dir")"
|
||||
done
|
||||
[ "$#" -gt 0 ] || die 'archive has no skills/*/SKILL.md directories'
|
||||
fi
|
||||
|
||||
printf 'SKILL\tTARGET\tACTION\n'
|
||||
for skill do
|
||||
case $skill in
|
||||
''|.*|*/*) die "invalid skill name: $skill" ;;
|
||||
for archive_path do
|
||||
case $archive_path in
|
||||
''|/*|*:*|..|../*|*/../*|*/..) die "invalid archive skill path: $archive_path" ;;
|
||||
esac
|
||||
source=$skills_dir/$skill
|
||||
[ -d "$source" ] && [ -f "$source/SKILL.md" ] || die "unknown skill: $skill"
|
||||
destination=$target/$skill
|
||||
if [ -L "$destination" ]; then
|
||||
linked=$(readlink "$destination")
|
||||
[ "$linked" = "$source" ] || die "refusing foreign symlink: $destination -> $linked"
|
||||
action=unchanged
|
||||
elif [ -e "$destination" ]; then
|
||||
die "refusing existing path: $destination"
|
||||
else
|
||||
ln -s -- "$source" "$destination"
|
||||
action=linked
|
||||
fi
|
||||
printf '%s\t%s\t%s\n' "$skill" "$destination" "$action"
|
||||
source_path=$tree/$archive_path
|
||||
validate_skill_dir "$source_path"
|
||||
name=$skill_name
|
||||
manifest_check "$name" "$digest" "$archive_path"
|
||||
link_skill "$name" "$source_path" artifact "$digest"
|
||||
manifest_append
|
||||
done
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: ink-cli
|
||||
name: audit-ink-cli
|
||||
description: >-
|
||||
Use when the user explicitly asks an agent to audit, troubleshoot, or explain
|
||||
the Ink host CLI, its frozen policy, tools, skills, sessions, or context from
|
||||
@@ -26,7 +26,7 @@ Load this skill for explicit questions about the `ink` command,
|
||||
selection, context handover, startup snapshots, or gaps in those surfaces.
|
||||
|
||||
Do not load it merely because ordinary work runs under Ink. External executables
|
||||
intended for Ink admission belong to `create-ink-agent-cli-tool`. Ink source
|
||||
intended for Ink admission belong to `create-ink-tool`. Ink source
|
||||
changes belong to Ink's repository authority and implementation workflow.
|
||||
|
||||
## Host boundary and authority
|
||||
@@ -37,26 +37,31 @@ changes belong to Ink's repository authority and implementation workflow.
|
||||
- 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.
|
||||
- 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
|
||||
evidence rather than executed runtime proof.
|
||||
- Global and project policy files explain only their contribution. Effective
|
||||
authority also depends on all policy layers, pinned executable and contract
|
||||
bytes, startup freezing, and session decisions.
|
||||
- Approved global and ancestor-project rows are additive alternatives; each file
|
||||
explains only its contribution. Effective authority also depends on the approved
|
||||
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
|
||||
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.
|
||||
|
||||
Distinguish three surfaces explicitly:
|
||||
|
||||
1. **Operator CLI commands** such as top-level `ink sessions` or `ink context`.
|
||||
1. **Operator CLI commands** such as top-level `ink sessions`, `ink skills`,
|
||||
`ink agent catalog`, and `ink policy help`.
|
||||
2. **Model-callable Ink built-ins** exposed directly to the hosted agent.
|
||||
3. **External executables** admitted through Ink's `run` policy.
|
||||
|
||||
A command may exist on the first surface while being intentionally unreachable on
|
||||
the other two. Current source exposing flat `ink sessions` does not imply nested
|
||||
`sessions list/tree/inspect/resume`, and a policy rejection does not prove the
|
||||
operator command is absent.
|
||||
the other two. Current source exposing flat `ink sessions`, `ink skills`, and
|
||||
`ink tools` does not imply invented nested verbs, and a policy rejection does not
|
||||
prove the operator command is absent. The current operator agent catalogue uses
|
||||
`ink agent catalog` and `ink agent resolve NAME`; role launch remains governed by
|
||||
the frozen `tool delegate` subject.
|
||||
|
||||
## Decision loop
|
||||
|
||||
@@ -68,7 +73,7 @@ operator command is absent.
|
||||
**source-confirmed**, **missing**, **stale claim**, or **not proven**.
|
||||
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.
|
||||
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
|
||||
the host boundary.
|
||||
|
||||
@@ -77,7 +82,8 @@ operator command is absent.
|
||||
- Do not edit policy to admit `ink`, call Ink recursively, log in, approve a
|
||||
digest, resume or clear a session, or dump host context from the agent.
|
||||
- Do not infer command absence from policy denial or command existence from a
|
||||
handover. In particular, challenge invented nested session verbs.
|
||||
handover. Challenge invented nested session or tool-verification verbs and
|
||||
verify current public help/source before suggesting operator argv.
|
||||
- Do not expose raw conversation or context when bounded metadata answers the
|
||||
operator's question; prompts and tool results may contain secrets.
|
||||
- Do not weaken path, origin, account, or repository selectors merely to make an
|
||||
@@ -89,11 +95,11 @@ operator command is absent.
|
||||
EVIDENCE: <runtime output, installed artifact, matching source, or limitation>
|
||||
SURFACE: <operator CLI, model built-in, or admitted external command>
|
||||
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?”
|
||||
loads this skill, confirms the operator/model boundary, and does not alter policy.
|
||||
|
||||
Negative smoke: “Build a selector-aware GitHub reader for Ink” routes to
|
||||
`create-ink-agent-cli-tool`.
|
||||
`create-ink-tool`.
|
||||
@@ -2,10 +2,10 @@
|
||||
name: configure-ink-agent
|
||||
description: >-
|
||||
Use when the user asks to create, configure, audit, or explain an Ink subagent
|
||||
definition, including its model, read/write scheduling class, or conjunctive
|
||||
policy file. Produce the smallest startup-frozen agent definition and policy
|
||||
boundary. Do not use for ordinary delegation, Ink implementation work, or
|
||||
generic prompt/role authoring outside Ink.
|
||||
definition, including its model, read/write scheduling class, or named policy
|
||||
boundary. Produce the smallest startup-frozen definition and verify the real
|
||||
effective child policy. Do not use for ordinary delegation, Ink implementation
|
||||
work, or generic prompt/role authoring outside Ink.
|
||||
---
|
||||
|
||||
# Configure an Ink agent
|
||||
@@ -16,23 +16,22 @@ Define one inspectable Ink subagent identity without confusing scheduling class,
|
||||
model choice, and authority.
|
||||
|
||||
```text
|
||||
agent definition -> access class -> relative policy conjunct -> frozen child snapshot
|
||||
agent definition -> access class -> effective child policy -> frozen child snapshot
|
||||
```
|
||||
|
||||
## Trigger boundary
|
||||
|
||||
Use for explicit requests about files in `$INK_AGENT_HOME` (default
|
||||
`$HOME/.ink/agents`) or a project `.ink/agents` directory, or when deciding the
|
||||
policy of a named Ink child.
|
||||
Use for explicit requests about agent files in `$HOME/.ink/agents` or the current
|
||||
project's `.ink/agents` directory, or when deciding the policy of a named Ink
|
||||
child.
|
||||
|
||||
Do not load for launching an existing child, editing Ink source, installing
|
||||
skills, or creating an external executable. `ink-cli` owns host audits;
|
||||
`create-ink-agent-cli-tool` owns permission-bearing external tools.
|
||||
skills, or creating an external executable. `audit-ink-cli` owns host audits;
|
||||
`create-ink-tool` owns permission-bearing external tools.
|
||||
|
||||
## Contract
|
||||
## Current definition contract
|
||||
|
||||
An agent definition is a plain text file with headers followed by one prompt
|
||||
body:
|
||||
An agent definition is a plain text file whose filename stem is the catalog key:
|
||||
|
||||
```text
|
||||
name: Frontend specialist
|
||||
@@ -44,54 +43,76 @@ Implement the bounded frontend task and return proof.
|
||||
```
|
||||
|
||||
- `name` is the human-facing identity.
|
||||
- `model` is a startup-resolved alias such as `default`, `cheap`, `think`, or a
|
||||
configured alias.
|
||||
- `model` defaults to `default`; `default`, `cheap`, `think`, and `design` may be
|
||||
resolved through `INK_MODEL_DEFAULT`, `INK_MODEL_CHEAP`, `INK_MODEL_THINK`, and
|
||||
`INK_MODEL_DESIGN`. Other values are literal model names.
|
||||
- `access` is mandatory: `read` or `write`.
|
||||
- `policy` is optional and relative to the definition file. Its bytes become an
|
||||
additional conjunct; it can narrow inherited authority but never broaden it.
|
||||
- Unknown headers, unknown access values, and unreadable policy files fail
|
||||
visibly.
|
||||
- `policy` is optional metadata intended to name a role-specific policy.
|
||||
- Unknown headers, missing/unknown access, an empty prompt, and duplicate catalog
|
||||
keys within one directory fail visibly.
|
||||
|
||||
Ink loads built-ins, then `$HOME/.ink/agents`, then `$CWD/.ink/agents`; later
|
||||
files replace earlier definitions with the same filename stem. The resulting
|
||||
catalog is frozen at startup. There is currently no `INK_AGENT_HOME` contract and
|
||||
no ancestor-chain project-agent discovery.
|
||||
|
||||
## Authority versus scheduling
|
||||
|
||||
`access` schedules actors; it does not grant tools:
|
||||
|
||||
- `read` children may overlap and receive an immutable host floor with no command,
|
||||
file-mutation, lifecycle-mutation, or delegation authority.
|
||||
- `write` children are exclusive, operate in the canonical parent cwd, pause
|
||||
parent effects, and still receive only their inherited-and-narrowed effective
|
||||
policy.
|
||||
parent mutations, and still receive only their effective frozen policy.
|
||||
|
||||
Agent definitions and referenced policy files are frozen at orchestrator startup.
|
||||
Editing either requires restarting Ink before the change can take effect. Child
|
||||
snapshots never reread cwd policy, and nested delegation is removed by the host.
|
||||
Never infer effective authority from `access`, prompt text, or a `policy:` label.
|
||||
Use the child policy snapshot and a real allowed/denied smoke.
|
||||
|
||||
## Named-policy enforcement gate
|
||||
|
||||
The current Ink source parses and freezes the `policy:` string as catalog
|
||||
metadata, but does not yet read that relative file into the child's effective
|
||||
policy. Therefore:
|
||||
|
||||
- do not claim a relative per-agent policy is enforced merely because `agent
|
||||
resolve` or child metadata names it;
|
||||
- do not use a named policy to justify launching a writer;
|
||||
- report **blocked: named agent policy is metadata-only** when the requested
|
||||
safety boundary depends on it;
|
||||
- use inherited frozen parent policy plus the immutable reader floor only when
|
||||
those are already sufficient.
|
||||
|
||||
This gate may be removed only after the provider launch path proves that the
|
||||
referenced bytes are pinned and applied as a restriction to the child's inherited
|
||||
approved durable policy.
|
||||
|
||||
## 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.
|
||||
3. Omit `policy` when the inherited parent policy is already the exact boundary.
|
||||
4. Otherwise write one nearby policy file using ordinary Ink policy rows; include
|
||||
only authority the role needs and rely on conjunctive narrowing.
|
||||
5. Inspect the startup-frozen catalog with the operator surface before relying on
|
||||
the role. Restart Ink after definition or policy changes.
|
||||
3. Put the definition in user scope or exact project cwd according to intended
|
||||
precedence.
|
||||
4. Restart Ink after definition changes.
|
||||
5. Inspect the frozen operator catalog and child metadata.
|
||||
6. Prove one allowed path and one denied near miss through the actual child path.
|
||||
7. If safety depends on `policy:`, stop at the named-policy enforcement gate.
|
||||
|
||||
## Refusals
|
||||
|
||||
- Do not put policy rows in the prompt body.
|
||||
- Do not use `access: write` as a substitute for command/tool policy.
|
||||
- Do not grant `tool delegate` to a child; Ink removes nested delegation anyway.
|
||||
- Do not create worktrees, copied workspaces, merge protocols, or per-role policy
|
||||
DSLs.
|
||||
- Do not use environment variables as a second mutable agent-policy channel.
|
||||
- Do not grant `tool delegate` to a child; Ink removes nested delegation.
|
||||
- Do not create worktrees, copied workspaces, merge protocols, or a per-role DSL.
|
||||
- Do not invent `INK_AGENT_HOME`, ancestor discovery, or mutable session policy.
|
||||
|
||||
## Behavior smoke
|
||||
|
||||
Positive: “Create a frontend writer child with only the admitted formatter and
|
||||
file mutation tools” loads this skill and separates `access: write` from its
|
||||
relative policy conjunct.
|
||||
Positive: “Create a frontend writer child with only formatter and file mutation
|
||||
authority” loads this skill and blocks until the named restrictive policy is
|
||||
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
|
||||
skill; it is ordinary delegation.
|
||||
|
||||
Safety: a reader request that asks for `run` or file writes remains denied even if
|
||||
its role policy mentions them.
|
||||
its prompt or `policy:` metadata says otherwise.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: create-ink-agent-cli-tool
|
||||
name: create-ink-tool
|
||||
description: >-
|
||||
Use when creating, implementing, splitting, or reviewing a compiled,
|
||||
permission-bearing executable intended for admission through Ink policy.
|
||||
@@ -8,12 +8,12 @@ description: >-
|
||||
ordinary or one-off CLIs/scripts, or skill authoring.
|
||||
---
|
||||
|
||||
# Create Ink agent CLI tool
|
||||
# Create an Ink tool
|
||||
|
||||
## One job
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
- the exact job and every reachable side effect;
|
||||
- Ink's current requirements, policy grammar, and mutation protocol;
|
||||
- the exact job and every reachable mutation;
|
||||
- Ink's current specification, policy grammar, and mutation protocol;
|
||||
- neighboring tools and existing executables that may already satisfy the job;
|
||||
- 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,
|
||||
recovery rules, or external contracts are blockers—not adapter opportunities.
|
||||
|
||||
```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
|
||||
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.
|
||||
3. Define argv, selectors, streams, errors, dependencies, exhaustive versus
|
||||
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,
|
||||
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
|
||||
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.
|
||||
Implement the coherent missing subset, not a compatibility facade or renamed
|
||||
wrapper.
|
||||
@@ -125,8 +125,8 @@ tool apply <id> <hash> # revalidate; perform once
|
||||
|
||||
Required properties:
|
||||
|
||||
- `stage` performs no external effect and resolves no secrets;
|
||||
- the manifest pins executable identity, canonical effect, authority subject,
|
||||
- `stage` performs no mutation and resolves no secrets;
|
||||
- the manifest pins executable identity, canonical mutation, authority subject,
|
||||
non-secret inputs, and drift-sensitive hashes;
|
||||
- selectors are variable semantic facts, never argv prefixes, shell text, or
|
||||
executable invariants;
|
||||
@@ -149,7 +149,7 @@ layout; the tool owns meaning.
|
||||
|
||||
Use the repository's elected projection envelope and bounds exactly; never invent
|
||||
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;
|
||||
- ordered key/value facts;
|
||||
@@ -159,7 +159,7 @@ vocabulary rather than universal effect kinds:
|
||||
Filesystem, HTTP, cloud, and infrastructure tools express their own semantics
|
||||
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
|
||||
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
|
||||
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
|
||||
bytes with the manifest;
|
||||
- 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;
|
||||
- the receipt identifies the exact staged manifest and may add only outcome and
|
||||
evidence facts; it cannot rewrite approved records;
|
||||
@@ -189,12 +189,14 @@ otherwise expose manifests, hashes, encoded payloads, or provider internals.
|
||||
|
||||
## 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
|
||||
semantics, and one realistic pipeline. Explicit help succeeds on stdout; usage
|
||||
errors fail on stderr. Selector help gives value grammar, canonicalization, and a
|
||||
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
|
||||
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:
|
||||
|
||||
- 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 mutator has admitted, approval-required, refused, drift, duplicate, and
|
||||
indeterminate/recovery outcomes as applicable;
|
||||
@@ -278,7 +280,7 @@ Do not:
|
||||
- wrap or partially clone a utility whose whole admitted surface already fits;
|
||||
- combine read and mutation for code reuse;
|
||||
- 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;
|
||||
- let tools choose terminal styling or let presentation metadata grant 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
|
||||
body summary from one staged operation; Ink renders the exact stored projection
|
||||
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,
|
||||
region, resource, requested change, outcome, and evidence through the elected
|
||||
generic vocabulary.
|
||||
+82
-13
@@ -3,31 +3,100 @@ set -eu
|
||||
|
||||
repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
|
||||
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"
|
||||
|
||||
list=$($repo/bin/ink-skills list)
|
||||
printf '%s\n' "$list" | grep '^SKILL' >/dev/null
|
||||
printf '%s\n' "$list" | grep '^ink-cli' >/dev/null
|
||||
printf '%s\n' "$list" | grep '^audit-ink-cli' >/dev/null
|
||||
printf '%s\n' "$list" | grep '^configure-ink-agent' >/dev/null
|
||||
|
||||
HOME=$tmp/home $repo/bin/ink-skills install ink-cli >"$tmp/install.tsv"
|
||||
grep "ink-cli.*linked" "$tmp/install.tsv" >/dev/null
|
||||
[ -L "$tmp/home/.ink/skills/ink-cli" ]
|
||||
[ "$(readlink "$tmp/home/.ink/skills/ink-cli")" = "$repo/skills/ink-cli" ]
|
||||
HOME=$tmp/home $repo/bin/ink-skills link >"$tmp/link.tsv"
|
||||
grep "audit-ink-cli.*linked.*local" "$tmp/link.tsv" >/dev/null
|
||||
[ -L "$tmp/home/.ink/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 ink-cli >"$tmp/reinstall.tsv"
|
||||
grep "ink-cli.*unchanged" "$tmp/reinstall.tsv" >/dev/null
|
||||
HOME=$tmp/home $repo/bin/ink-skills link "$repo/skills/audit-ink-cli" >"$tmp/relink.tsv"
|
||||
grep "audit-ink-cli.*unchanged" "$tmp/relink.tsv" >/dev/null
|
||||
|
||||
mkdir -p "$tmp/home/.ink/skills/configure-ink-agent"
|
||||
if HOME=$tmp/home $repo/bin/ink-skills install configure-ink-agent >/dev/null 2>"$tmp/collision.err"; then
|
||||
mkdir -p "$tmp/home/.ink/skills/collision"
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
grep 'refusing existing path' "$tmp/collision.err" >/dev/null
|
||||
|
||||
HOME=$tmp/home $repo/bin/ink-skills install --project "$tmp/project" create-ink-agent-cli-tool >"$tmp/project.tsv"
|
||||
[ -L "$tmp/project/.ink/skills/create-ink-agent-cli-tool" ]
|
||||
grep "create-ink-agent-cli-tool.*linked" "$tmp/project.tsv" >/dev/null
|
||||
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" ]
|
||||
grep "create-ink-tool.*linked.*local" "$tmp/project.tsv" >/dev/null
|
||||
|
||||
mkdir -p "$tmp/archive-tree/skills/remote-review"
|
||||
cat >"$tmp/archive-tree/skills/remote-review/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: remote-review
|
||||
description: Review one remote fixture.
|
||||
---
|
||||
|
||||
# Remote review
|
||||
EOF
|
||||
tar -cf "$tmp/skills.tar" -C "$tmp/archive-tree" skills
|
||||
digest=$(sha256sum "$tmp/skills.tar" | awk '{print $1}')
|
||||
|
||||
mkdir -p "$tmp/import-home"
|
||||
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/import-home/.ink/skills/remote-review
|
||||
[ -L "$remote_link" ]
|
||||
[ "$(readlink "$remote_link")" = "$tmp/store/sha256/$digest/tree/skills/remote-review" ]
|
||||
grep "remote-review.*linked.*artifact.*$digest" "$tmp/import.tsv" >/dev/null
|
||||
manifest=$tmp/import-home/.ink/skills/.ink-skills.tsv
|
||||
[ "$(wc -l <"$manifest")" -eq 2 ]
|
||||
awk -F '\t' -v digest="$digest" 'NR == 2 { exit !($1 == "remote-review" && $2 == digest && $3 == "skills/remote-review") }' "$manifest"
|
||||
|
||||
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/reimport.tsv" >/dev/null
|
||||
[ "$(wc -l <"$manifest")" -eq 2 ]
|
||||
|
||||
zero_digest=$(awk 'BEGIN { for (i = 0; i < 64; i++) printf "0" }')
|
||||
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
|
||||
echo 'expected digest mismatch' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep 'SHA-256 mismatch' "$tmp/digest.err" >/dev/null
|
||||
|
||||
printf '\nchanged\n' >>"$tmp/archive-tree/skills/remote-review/SKILL.md"
|
||||
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
|
||||
fi
|
||||
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'
|
||||
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
|
||||
count=0
|
||||
for skill in "$repo"/skills/*/SKILL.md; do
|
||||
[ -f "$skill" ] || continue
|
||||
directory=$(basename -- "$(dirname -- "$skill")")
|
||||
name=$(sed -n 's/^name:[[:space:]]*//p' "$skill" | sed -n '1p')
|
||||
[ "$name" = "$directory" ] || {
|
||||
printf 'name mismatch: %s != %s\n' "$name" "$directory" >&2
|
||||
exit 1
|
||||
}
|
||||
grep '^description:' "$skill" >/dev/null
|
||||
count=$((count + 1))
|
||||
done
|
||||
[ "$count" -eq 3 ] || {
|
||||
printf 'expected 3 skills, found %s\n' "$count" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
configure=$repo/skills/configure-ink-agent/SKILL.md
|
||||
grep 'access.*read.*write' "$configure" >/dev/null
|
||||
grep 'named agent policy is metadata-only' "$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
|
||||
grep 'ink agent catalog' "$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
|
||||
grep 'stage/match/apply' "$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'
|
||||
Reference in New Issue
Block a user