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
- `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.
+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
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
View File
@@ -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,47 +49,63 @@ 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"
done
reject_record_breaks() {
case $1 in
*" "*|*"
"*) die "tabs and newlines are not allowed: $1" ;;
esac
}
[ "$#" -gt 0 ] || {
usage >&2
exit 2
frontmatter_name() {
sed -n 's/^name:[[:space:]]*//p' "$1/SKILL.md" | sed -n '1p'
}
command=$1
shift
case $command in
-h|--help|help)
usage
exit 0
;;
list)
[ "$#" -eq 0 ] || {
usage >&2
exit 2
}
list_skills
exit 0
;;
install) ;;
*)
usage >&2
exit 2
;;
esac
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
}
target_mode=user
target_arg=
while [ "$#" -gt 0 ]; do
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
@@ -101,51 +130,223 @@ while [ "$#" -gt 0 ]; do
;;
*) break ;;
esac
done
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"
if [ "$#" -eq 0 ]; then
set --
for path in "$skills_dir"/*; do
[ -d "$path" ] || continue
[ -f "$path/SKILL.md" ] || continue
set -- "$@" "$(basename -- "$path")"
done
fi
remaining_count=$#
remaining_file=$work_args
: >"$remaining_file"
for arg do
reject_record_breaks "$arg"
printf '%s\n' "$arg" >>"$remaining_file"
done
}
printf 'SKILL\tTARGET\tACTION\n'
for skill do
case $skill in
''|.*|*/*) die "invalid skill name: $skill" ;;
esac
source=$skills_dir/$skill
[ -d "$source" ] && [ -f "$source/SKILL.md" ] || die "unknown skill: $skill"
destination=$target/$skill
link_skill() {
name=$1
source_path=$2
source_label=$3
digest=$4
destination=$target/$name
if [ -L "$destination" ]; then
linked=$(readlink "$destination")
[ "$linked" = "$source" ] || die "refusing foreign symlink: $destination -> $linked"
[ "$linked" = "$source_path" ] || die "refusing foreign symlink: $destination -> $linked"
action=unchanged
elif [ -e "$destination" ]; then
die "refusing existing path: $destination"
else
ln -s -- "$source" "$destination"
ln -s -- "$source_path" "$destination"
action=linked
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
@@ -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`.
+59 -38
View File
@@ -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
View File
@@ -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'
+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'