Compare commits

..

6 Commits

Author SHA1 Message Date
tmk241 92a2849376 align skills with additive project policy 2026-08-16 22:14:57 +02:00
tmk241 f6d0efdfc0 Align Ink skills with frozen delegation contracts 2026-08-12 01:05:49 +02:00
tmk241 8547d6ea33 Import verified skill archives 2026-08-11 17:04:37 +02:00
tmk241 efc4b70e13 Install pinned skills from Git repositories 2026-08-11 16:54:17 +02:00
tmk241 cb555a41b2 Use verb-object skill names 2026-08-11 16:47:12 +02:00
tmk241 5552014329 Align skills with frozen Ink contracts 2026-08-11 16:42:45 +02:00
8 changed files with 567 additions and 200 deletions
+7 -5
View File
@@ -5,14 +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,
- Run `sh -n bin/ink-skills` and `sh test/install-smoke.sh` after changes. 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.
+51 -27
View File
@@ -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 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 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
``` ```
The installer is intentionally smaller than `npx skills`: no registry, package `import` verifies the complete archive before extraction, accepts only regular
manager, network access, copies, prompts, lockfile, or hidden state. It creates files and directories with safe relative paths, and rejects symlinks and special
absolute symlinks from `$HOME/.ink/skills` (or `$INK_SKILLS_HOME`) to this files. It materializes the tree under
checkout. Pulling the repository updates installed skills; restarting Ink freezes `$INK_SKILLS_STORE/sha256/HASH` (or the XDG/default data path), then links skills
the new bytes into the next startup snapshot. 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. The same artifact works whether it arrived via curl, scp, USB, a browser download,
Existing paths and foreign symlinks are refused rather than overwritten. 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 ## Skills
| Skill | Job | | Skill | Job |
|---|---| |---|---|
| `ink-cli` | Audit and explain the Ink host without crossing the host/guest boundary. | | `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 relative policy conjunct. | | `configure-ink-agent` | Create or audit one Ink agent definition, access class, and effective-policy boundary. |
| `create-ink-agent-cli-tool` | Build one inspectable permission-bearing executable suitable for Ink policy admission. | | `create-ink-tool` | Build one inspectable permission-bearing executable suitable for Ink policy admission. |
## Agent definitions and policy ## Agent definitions and policy
Agent definitions live in `$INK_AGENT_HOME` (default `$HOME/.ink/agents`) or Agent definitions live in `$HOME/.ink/agents` or the exact current project's
project `.ink/agents` directories: `.ink/agents` directory:
```text ```text
name: Frontend specialist name: Frontend specialist
@@ -65,24 +82,31 @@ policy: frontend.policy
Implement the bounded frontend task and return proof. Implement the bounded frontend task and return proof.
``` ```
The policy path is relative to the definition. It is a normal Ink policy file and `access: read|write` selects reader/writer scheduling; it does not grant commands
narrows the frozen parent snapshot conjunctively. `access: read|write` selects or tools. Definitions are frozen at startup, so restart Ink after changing one.
reader/writer scheduling; it does not grant commands or tools. Definitions and
policy files are frozen at startup, so restart Ink after changing either.
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 ## Verify
```sh ```sh
sh -n bin/ink-skills sh -n bin/ink-skills
sh test/install-smoke.sh sh test/install-smoke.sh
sh test/skills-smoke.sh
``` ```
## Refusals ## Refusals
- No npm package merely to create symlinks. - 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 policy mutation during installation.
- No bundled binaries; those belong in `toolset`. - No bundled binaries; those belong in `toolset`.
- No automatic installation by Ink itself. - No automatic installation by Ink itself.
+278 -77
View File
@@ -4,28 +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 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: Commands:
list List available skill names and source paths as TSV. list List skills shipped by this checkout as TSV.
install Link named skills; with no names, link every skill. 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: 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
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: Output:
TSV with SKILL, TARGET, and ACTION columns. TSV with SKILL, TARGET, ACTION, SOURCE, and SHA256 columns.
Exit status: 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: Requirements:
ink-skills list POSIX sh and standard text tools. `import` additionally needs tar and one of
ink-skills install ink-cli configure-ink-agent sha256sum, shasum, or openssl.
ink-skills install --project . create-ink-agent-cli-tool
EOF EOF
} }
@@ -36,46 +49,62 @@ die() {
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
repo_dir=$(CDPATH= cd -- "$script_dir/.." && 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() { reject_record_breaks() {
printf 'SKILL\tSOURCE\n' case $1 in
for path in "$skills_dir"/*; do *" "*|*"
[ -d "$path" ] || continue "*) die "tabs and newlines are not allowed: $1" ;;
[ -f "$path/SKILL.md" ] || continue esac
printf '%s\t%s\n' "$(basename -- "$path")" "$path"
done
} }
[ "$#" -gt 0 ] || { frontmatter_name() {
usage >&2 sed -n 's/^name:[[:space:]]*//p' "$1/SKILL.md" | sed -n '1p'
exit 2
} }
command=$1 validate_skill_dir() {
shift skill_dir=$1
case $command in [ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ] || die "not a skill directory: $skill_dir"
-h|--help|help) [ ! -L "$skill_dir" ] && [ ! -L "$skill_dir/SKILL.md" ] || die "skill root and SKILL.md must not be symlinks: $skill_dir"
usage skill_name=$(frontmatter_name "$skill_dir")
exit 0 [ -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
;; ;;
list) project)
[ "$#" -eq 0 ] || { project=$(CDPATH= cd -- "$target_arg" 2>/dev/null && pwd -P) || die "project directory not found: $target_arg"
usage >&2 target=$project/.ink/skills
exit 2
}
list_skills
exit 0
;;
install) ;;
*)
usage >&2
exit 2
;; ;;
esac esac
mkdir -p -- "$target"
}
target_mode=user parse_target_options() {
target_arg=
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
case $1 in case $1 in
--user) --user)
@@ -102,50 +131,222 @@ while [ "$#" -gt 0 ]; do
*) break ;; *) break ;;
esac esac
done done
remaining_count=$#
case $target_mode in remaining_file=$work_args
user) : >"$remaining_file"
if [ -n "${INK_SKILLS_HOME:-}" ]; then for arg do
target=$INK_SKILLS_HOME reject_record_breaks "$arg"
else printf '%s\n' "$arg" >>"$remaining_file"
[ -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"
if [ "$#" -eq 0 ]; then
set --
for path in "$skills_dir"/*; do
[ -d "$path" ] || continue
[ -f "$path/SKILL.md" ] || continue
set -- "$@" "$(basename -- "$path")"
done done
fi }
printf 'SKILL\tTARGET\tACTION\n' link_skill() {
for skill do name=$1
case $skill in source_path=$2
''|.*|*/*) die "invalid skill name: $skill" ;; source_label=$3
esac digest=$4
source=$skills_dir/$skill destination=$target/$name
[ -d "$source" ] && [ -f "$source/SKILL.md" ] || die "unknown skill: $skill"
destination=$target/$skill
if [ -L "$destination" ]; then if [ -L "$destination" ]; then
linked=$(readlink "$destination") linked=$(readlink "$destination")
[ "$linked" = "$source" ] || die "refusing foreign symlink: $destination -> $linked" [ "$linked" = "$source_path" ] || die "refusing foreign symlink: $destination -> $linked"
action=unchanged action=unchanged
elif [ -e "$destination" ]; then elif [ -e "$destination" ]; then
die "refusing existing path: $destination" die "refusing existing path: $destination"
else else
ln -s -- "$source" "$destination" ln -s -- "$source_path" "$destination"
action=linked action=linked
fi fi
printf '%s\t%s\t%s\n' "$skill" "$destination" "$action" 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
-h|--help|help)
usage
exit 0
;;
list)
[ "$#" -eq 0 ] || {
usage >&2
exit 2
}
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
;;
link|import) ;;
*)
usage >&2
exit 2
;;
esac
target_mode=user
target_arg=
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
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"
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 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
for archive_path do
case $archive_path in
''|/*|*:*|..|../*|*/../*|*/..) die "invalid archive skill path: $archive_path" ;;
esac
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 done
@@ -1,5 +1,5 @@
--- ---
name: ink-cli name: audit-ink-cli
description: >- description: >-
Use when the user explicitly asks an agent to audit, troubleshoot, or explain 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 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. selection, context handover, startup snapshots, or gaps in those surfaces.
Do not load it merely because ordinary work runs under Ink. External executables 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. changes belong to Ink's repository authority and implementation workflow.
## Host boundary and authority ## 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 - 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:
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. 2. **Model-callable Ink built-ins** exposed directly to the hosted agent.
3. **External executables** admitted through Ink's `run` policy. 3. **External executables** admitted through Ink's `run` policy.
A command may exist on the first surface while being intentionally unreachable on 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 the other two. Current source exposing flat `ink sessions`, `ink skills`, and
`sessions list/tree/inspect/resume`, and a policy rejection does not prove the `ink tools` does not imply invented nested verbs, and a policy rejection does not
operator command is absent. 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 ## Decision loop
@@ -68,7 +73,7 @@ operator command is absent.
**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.
@@ -77,7 +82,8 @@ operator command is absent.
- Do not edit policy to admit `ink`, call Ink recursively, log in, approve a - 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. 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 - 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 - Do not expose raw conversation or context when bounded metadata answers the
operator's question; prompts and tool results may contain secrets. operator's question; prompts and tool results may contain secrets.
- Do not weaken path, origin, account, or repository selectors merely to make an - 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> 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?”
loads this skill, confirms the operator/model boundary, and does not alter policy. 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 Negative smoke: “Build a selector-aware GitHub reader for Ink” routes to
`create-ink-agent-cli-tool`. `create-ink-tool`.
+59 -38
View File
@@ -2,10 +2,10 @@
name: configure-ink-agent name: configure-ink-agent
description: >- description: >-
Use when the user asks to create, configure, audit, or explain an Ink subagent Use when the user asks to create, configure, audit, or explain an Ink subagent
definition, including its model, read/write scheduling class, or conjunctive definition, including its model, read/write scheduling class, or named policy
policy file. Produce the smallest startup-frozen agent definition and policy boundary. Produce the smallest startup-frozen definition and verify the real
boundary. Do not use for ordinary delegation, Ink implementation work, or effective child policy. Do not use for ordinary delegation, Ink implementation
generic prompt/role authoring outside Ink. work, or generic prompt/role authoring outside Ink.
--- ---
# Configure an Ink agent # Configure an Ink agent
@@ -16,23 +16,22 @@ Define one inspectable Ink subagent identity without confusing scheduling class,
model choice, and authority. model choice, and authority.
```text ```text
agent definition -> access class -> relative policy conjunct -> frozen child snapshot agent definition -> access class -> effective child policy -> frozen child snapshot
``` ```
## Trigger boundary ## Trigger boundary
Use for explicit requests about files in `$INK_AGENT_HOME` (default Use for explicit requests about agent files in `$HOME/.ink/agents` or the current
`$HOME/.ink/agents`) or a project `.ink/agents` directory, or when deciding the project's `.ink/agents` directory, or when deciding the policy of a named Ink
policy of a named Ink child. child.
Do not load for launching an existing child, editing Ink source, installing Do not load for launching an existing child, editing Ink source, installing
skills, or creating an external executable. `ink-cli` owns host audits; skills, or creating an external executable. `audit-ink-cli` owns host audits;
`create-ink-agent-cli-tool` owns permission-bearing external tools. `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 An agent definition is a plain text file whose filename stem is the catalog key:
body:
```text ```text
name: Frontend specialist name: Frontend specialist
@@ -44,54 +43,76 @@ Implement the bounded frontend task and return proof.
``` ```
- `name` is the human-facing identity. - `name` is the human-facing identity.
- `model` is a startup-resolved alias such as `default`, `cheap`, `think`, or a - `model` defaults to `default`; `default`, `cheap`, `think`, and `design` may be
configured alias. 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`. - `access` is mandatory: `read` or `write`.
- `policy` is optional and relative to the definition file. Its bytes become an - `policy` is optional metadata intended to name a role-specific policy.
additional conjunct; it can narrow inherited authority but never broaden it. - Unknown headers, missing/unknown access, an empty prompt, and duplicate catalog
- Unknown headers, unknown access values, and unreadable policy files fail keys within one directory fail visibly.
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: `access` schedules actors; it does not grant tools:
- `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 inherited-and-narrowed effective parent mutations, and still receive only their effective frozen policy.
policy.
Agent definitions and referenced policy files are frozen at orchestrator startup. Never infer effective authority from `access`, prompt text, or a `policy:` label.
Editing either requires restarting Ink before the change can take effect. Child Use the child policy snapshot and a real allowed/denied smoke.
snapshots never reread cwd policy, and nested delegation is removed by the host.
## 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 ## 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. Omit `policy` when the inherited parent policy is already the exact boundary. 3. Put the definition in user scope or exact project cwd according to intended
4. Otherwise write one nearby policy file using ordinary Ink policy rows; include precedence.
only authority the role needs and rely on conjunctive narrowing. 4. Restart Ink after definition changes.
5. Inspect the startup-frozen catalog with the operator surface before relying on 5. Inspect the frozen operator catalog and child metadata.
the role. Restart Ink after definition or policy changes.
6. Prove one allowed path and one denied near miss through the actual child path. 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 ## Refusals
- Do not put policy rows in the prompt body. - Do not put policy rows in the prompt body.
- Do not use `access: write` as a substitute for command/tool policy. - 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 grant `tool delegate` to a child; Ink removes nested delegation.
- Do not create worktrees, copied workspaces, merge protocols, or per-role policy - Do not create worktrees, copied workspaces, merge protocols, or a per-role DSL.
DSLs. - Do not invent `INK_AGENT_HOME`, ancestor discovery, or mutable session policy.
- Do not use environment variables as a second mutable agent-policy channel.
## Behavior smoke ## Behavior smoke
Positive: “Create a frontend writer child with only the admitted formatter and Positive: “Create a frontend writer child with only formatter and file mutation
file mutation tools” loads this skill and separates `access: write` from its authority” loads this skill and blocks until the named restrictive policy is
relative policy conjunct. 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.
Safety: a reader request that asks for `run` or file writes remains denied even if 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: >- description: >-
Use when creating, implementing, splitting, or reviewing a compiled, Use when creating, implementing, splitting, or reviewing a compiled,
permission-bearing executable intended for admission through Ink policy. permission-bearing executable intended for admission through Ink policy.
@@ -8,12 +8,12 @@ description: >-
ordinary or one-off CLIs/scripts, or skill authoring. ordinary or one-off CLIs/scripts, or skill authoring.
--- ---
# Create Ink agent CLI tool # Create an Ink tool
## 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.
+82 -13
View File
@@ -3,31 +3,100 @@ 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)
printf '%s\n' "$list" | grep '^SKILL' >/dev/null 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 printf '%s\n' "$list" | grep '^configure-ink-agent' >/dev/null
HOME=$tmp/home $repo/bin/ink-skills install ink-cli >"$tmp/install.tsv" HOME=$tmp/home $repo/bin/ink-skills link >"$tmp/link.tsv"
grep "ink-cli.*linked" "$tmp/install.tsv" >/dev/null grep "audit-ink-cli.*linked.*local" "$tmp/link.tsv" >/dev/null
[ -L "$tmp/home/.ink/skills/ink-cli" ] [ -L "$tmp/home/.ink/skills/audit-ink-cli" ]
[ "$(readlink "$tmp/home/.ink/skills/ink-cli")" = "$repo/skills/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" HOME=$tmp/home $repo/bin/ink-skills link "$repo/skills/audit-ink-cli" >"$tmp/relink.tsv"
grep "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-agent-cli-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-agent-cli-tool" ] [ -L "$tmp/project/.ink/skills/create-ink-tool" ]
grep "create-ink-agent-cli-tool.*linked" "$tmp/project.tsv" >/dev/null 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' printf 'ok\n'
+42
View File
@@ -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'