Compare commits

...

3 Commits

Author SHA1 Message Date
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 425 additions and 127 deletions
+2 -1
View File
@@ -15,4 +15,5 @@
- 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.
- Run `sh -n bin/ink-skills`, `sh test/install-smoke.sh`, and
`sh test/skills-smoke.sh` after changes.
+38 -17
View File
@@ -25,7 +25,7 @@ If the repository is already under `/opt/repositories`:
Install selected skills only:
```sh
ink-skills install ink-cli configure-ink-agent
ink-skills install audit-ink-cli configure-ink-agent
```
Install into one project instead of the user catalogue:
@@ -34,11 +34,31 @@ Install into one project instead of the user catalogue:
ink-skills install --project /path/to/project configure-ink-agent
```
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.
Install from any Git repository your normal Git credentials can read:
```sh
ink-skills add --ref main git@git.example:team/skills.git
ink-skills add https://git.example/team/skills.git skills/review-sql
```
`add` resolves the ref to one commit, exports it into a content-addressed store,
computes a SHA-256 over each selected skill tree, and symlinks that immutable
artifact. Provenance is recorded next to installed links in `.ink-skills.tsv`:
source, requested ref, resolved commit, path, and SHA-256. It deliberately does
not edit `SKILL.md` comments. A moved branch does not silently update an installed
skill; the existing pin causes a visible provenance collision.
Git SSH agents and credential helpers remain Git's job. Credential-bearing HTTP
URLs are refused so secrets do not enter manifests or process listings. Remote
skill trees containing symlinks are also refused.
The local `install` path is intentionally smaller than `npx skills`: no registry,
package manager, network access, copies, prompts, lockfile, or hidden state. It
creates absolute symlinks from `$HOME/.ink/skills` (or the installer-only
`$INK_SKILLS_HOME` target override) to this checkout. Pulling the repository
updates locally installed skills; restarting Ink freezes the new bytes into the
next startup snapshot. Ink itself discovers `$HOME/.ink/skills`,
`$CWD/.ink/skills`, and colon-separated `INK_SKILLS_DIRS`.
`ink-skills list` emits TSV. `ink-skills --help` is the complete command manual.
Existing paths and foreign symlinks are refused rather than overwritten.
@@ -47,14 +67,14 @@ Existing paths and foreign symlinks are refused rather than overwritten.
| 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 +85,25 @@ 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.
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.
+217 -49
View File
@@ -5,27 +5,40 @@ usage() {
cat <<'EOF'
usage: ink-skills list
ink-skills install [--user | --project DIR] [SKILL ...]
ink-skills add [--user | --project DIR] [--ref REF] REPOSITORY [PATH ...]
Install Ink skills from this checkout as symlinks.
Install Ink skills as symlinks from this checkout or a pinned Git artifact.
Commands:
list List available skill names and source paths as TSV.
install Link named skills; with no names, link every skill.
list List skills in this checkout as TSV.
install Link named local skills; with no names, link every skill.
add Fetch REPOSITORY, pin REF to a commit, materialize an
immutable snapshot, and link skill PATHs from it. PATH
defaults to every skills/*/SKILL.md directory.
Targets:
--user $INK_SKILLS_HOME or $HOME/.ink/skills (default)
--project DIR DIR/.ink/skills
Git storage:
$INK_SKILLS_STORE or $XDG_DATA_HOME/ink-skills, otherwise
$HOME/.local/share/ink-skills. Git, tar, and sha256sum are required by `add`.
Authentication belongs to Git's SSH agent or credential helper; credentialed
HTTP URLs are refused. Installed provenance is written to
TARGET/.ink-skills.tsv without modifying SKILL.md.
Output:
TSV with SKILL, TARGET, and ACTION columns.
TSV with SKILL, TARGET, ACTION, SOURCE, COMMIT, and SHA256 columns.
Exit status:
0 success; 2 usage error; 3 target collision or invalid skill.
0 success; 2 usage error; 3 collision, invalid source, or fetch failure.
Examples:
ink-skills list
ink-skills install ink-cli configure-ink-agent
ink-skills install --project . create-ink-agent-cli-tool
ink-skills install audit-ink-cli configure-ink-agent
ink-skills install --project . create-ink-tool
ink-skills add --ref main git@git.example:team/skills.git
ink-skills add https://git.example/team/skills.git skills/review-sql
EOF
}
@@ -34,6 +47,13 @@ die() {
exit 3
}
reject_record_breaks() {
case $1 in
*" "*|*"
"*) die "tabs and newlines are not allowed: $1" ;;
esac
}
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
repo_dir=$(CDPATH= cd -- "$script_dir/.." && pwd -P)
skills_dir=$repo_dir/skills
@@ -47,11 +67,110 @@ list_skills() {
done
}
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_option() {
case $1 in
--user)
target_mode=user
shift_count=1
;;
--project)
[ "$#" -ge 2 ] || {
usage >&2
exit 2
}
target_mode=project
target_arg=$2
shift_count=2
;;
*) shift_count=0 ;;
esac
}
frontmatter_name() {
sed -n 's/^name:[[:space:]]*//p' "$1/SKILL.md" | sed -n '1p'
}
validate_skill_dir() {
source_path=$1
[ -d "$source_path" ] && [ -f "$source_path/SKILL.md" ] || die "not a skill directory: $source_path"
[ ! -L "$source_path" ] && [ ! -L "$source_path/SKILL.md" ] || die "skill root and SKILL.md must not be symlinks: $source_path"
skill_name=$(frontmatter_name "$source_path")
[ -n "$skill_name" ] || die "missing frontmatter name: $source_path/SKILL.md"
[ "$skill_name" = "$(basename -- "$source_path")" ] || die "frontmatter name does not match directory: $source_path"
case $skill_name in
''|.*|*/*) die "invalid skill name: $skill_name" ;;
esac
}
link_skill() {
skill_name=$1
source_path=$2
source_label=$3
commit=$4
digest=$5
destination=$target/$skill_name
if [ -L "$destination" ]; then
linked=$(readlink "$destination")
[ "$linked" = "$source_path" ] || die "refusing foreign symlink: $destination -> $linked"
action=unchanged
elif [ -e "$destination" ]; then
die "refusing existing path: $destination"
else
ln -s -- "$source_path" "$destination"
action=linked
fi
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$skill_name" "$destination" "$action" "$source_label" "$commit" "$digest"
}
check_source_record() {
skill_name=$1
source_label=$2
ref=$3
commit=$4
skill_path=$5
digest=$6
manifest=$target/.ink-skills.tsv
record_needed=yes
[ -e "$manifest" ] || return 0
if awk -F '\t' -v skill="$skill_name" 'NR > 1 && $1 == skill { found = 1 } END { exit !found }' "$manifest"; then
existing=$(awk -F '\t' -v skill="$skill_name" 'NR > 1 && $1 == skill { print $0; exit }' "$manifest")
wanted=$(printf '%s\t%s\t%s\t%s\t%s\t%s' "$skill_name" "$source_label" "$ref" "$commit" "$skill_path" "$digest")
[ "$existing" = "$wanted" ] || die "provenance collision for installed skill: $skill_name"
record_needed=no
fi
}
append_source_record() {
[ "$record_needed" = yes ] || return 0
manifest=$target/.ink-skills.tsv
if [ ! -e "$manifest" ]; then
printf 'SKILL\tSOURCE\tREF\tCOMMIT\tPATH\tSHA256\n' >"$manifest"
fi
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$skill_name" "$repository" "$ref" "$commit" "$skill_path" "$digest" >>"$manifest"
}
[ "$#" -gt 0 ] || {
usage >&2
exit 2
}
command=$1
shift
case $command in
@@ -67,7 +186,7 @@ case $command in
list_skills
exit 0
;;
install) ;;
install|add) ;;
*)
usage >&2
exit 2
@@ -76,19 +195,20 @@ esac
target_mode=user
target_arg=
ref=HEAD
while [ "$#" -gt 0 ]; do
parse_target_option "$@"
if [ "$shift_count" -gt 0 ]; then
shift "$shift_count"
continue
fi
case $1 in
--user)
target_mode=user
shift
;;
--project)
[ "$#" -ge 2 ] || {
--ref)
[ "$command" = add ] && [ "$#" -ge 2 ] || {
usage >&2
exit 2
}
target_mode=project
target_arg=$2
ref=$2
shift 2
;;
--)
@@ -102,24 +222,11 @@ while [ "$#" -gt 0 ]; do
*) break ;;
esac
done
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"
printf 'SKILL\tTARGET\tACTION\tSOURCE\tCOMMIT\tSHA256\n'
if [ "$command" = install ]; then
if [ "$#" -eq 0 ]; then
set --
for path in "$skills_dir"/*; do
@@ -128,24 +235,85 @@ if [ "$#" -eq 0 ]; then
set -- "$@" "$(basename -- "$path")"
done
fi
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
if [ -L "$destination" ]; then
linked=$(readlink "$destination")
[ "$linked" = "$source" ] || die "refusing foreign symlink: $destination -> $linked"
action=unchanged
elif [ -e "$destination" ]; then
die "refusing existing path: $destination"
else
ln -s -- "$source" "$destination"
action=linked
fi
printf '%s\t%s\t%s\n' "$skill" "$destination" "$action"
source_path=$skills_dir/$skill
validate_skill_dir "$source_path"
link_skill "$skill_name" "$source_path" local - -
done
exit 0
fi
[ "$#" -gt 0 ] || {
usage >&2
exit 2
}
repository=$1
shift
reject_record_breaks "$repository"
reject_record_breaks "$ref"
case $ref in
-*) die "invalid ref: $ref" ;;
esac
case $repository in
http://*@*|https://*@*) die 'credentialed HTTP URLs are refused; use an SSH agent or Git credential helper' ;;
esac
command -v git >/dev/null 2>&1 || die 'git is required by add'
command -v tar >/dev/null 2>&1 || die 'tar is required by add'
command -v sha256sum >/dev/null 2>&1 || die 'sha256sum is required by add'
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
repo_key=$(printf '%s' "$repository" | git hash-object --stdin) || die 'cannot hash repository identity'
mirror=$store/git/$repo_key.git
mkdir -p -- "$store/git" "$store/artifacts/$repo_key"
if [ ! -d "$mirror" ]; then
git clone --quiet --mirror -- "$repository" "$mirror" || die "cannot clone repository: $repository"
fi
git --git-dir="$mirror" fetch --quiet --force origin "$ref" || die "cannot fetch ref: $ref"
commit=$(git --git-dir="$mirror" rev-parse --verify 'FETCH_HEAD^{commit}') || die "ref does not resolve to a commit: $ref"
artifact=$store/artifacts/$repo_key/$commit
if [ ! -d "$artifact" ]; then
temporary=$artifact.tmp.$$
rm -rf -- "$temporary"
mkdir -p -- "$temporary"
if ! git --git-dir="$mirror" archive "$commit" | tar -x -C "$temporary"; then
rm -rf -- "$temporary"
die "cannot materialize commit: $commit"
fi
mv -- "$temporary" "$artifact"
fi
if [ "$#" -eq 0 ]; then
set --
for source_path in "$artifact"/skills/*; do
[ -d "$source_path" ] || continue
[ -f "$source_path/SKILL.md" ] || continue
set -- "$@" "skills/$(basename -- "$source_path")"
done
[ "$#" -gt 0 ] || die "repository has no skills/*/SKILL.md at $commit"
fi
for skill_path do
reject_record_breaks "$skill_path"
case $skill_path in
''|/*|*:*|..|../*|*/../*|*/..) die "invalid repository skill path: $skill_path" ;;
esac
source_path=$artifact/$skill_path
validate_skill_dir "$source_path"
if find "$source_path" -type l -print | grep . >/dev/null 2>&1; then
die "remote skill contains symlinks: $skill_path"
fi
digest=$(git --git-dir="$mirror" archive "$commit:$skill_path" | sha256sum | awk '{print $1}') || die "cannot hash skill artifact: $skill_path"
check_source_record "$skill_name" "$repository" "$ref" "$commit" "$skill_path" "$digest"
link_skill "$skill_name" "$source_path" "$repository" "$commit" "$digest"
append_source_record
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
@@ -49,14 +49,17 @@ changes belong to Ink's repository authority and implementation workflow.
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
@@ -77,7 +80,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
@@ -96,4 +100,4 @@ 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`.
+57 -37
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,75 @@ 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 effects, 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 conjoined into the child effective policy.
## Decision loop
1. Choose `read` unless the child must produce a real effect.
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 policy is actually
conjoined 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,7 +8,7 @@ description: >-
ordinary or one-off CLIs/scripts, or skill authoring.
---
# Create Ink agent CLI tool
# Create an Ink tool
## One job
+59 -10
View File
@@ -8,16 +8,16 @@ 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 install audit-ink-cli >"$tmp/install.tsv"
grep "audit-ink-cli.*linked" "$tmp/install.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 install audit-ink-cli >"$tmp/reinstall.tsv"
grep "audit-ink-cli.*unchanged" "$tmp/reinstall.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
@@ -26,8 +26,57 @@ if HOME=$tmp/home $repo/bin/ink-skills install configure-ink-agent >/dev/null 2>
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 install --project "$tmp/project" create-ink-tool >"$tmp/project.tsv"
[ -L "$tmp/project/.ink/skills/create-ink-tool" ]
grep "create-ink-tool.*linked" "$tmp/project.tsv" >/dev/null
remote=$tmp/remote
mkdir -p "$remote/skills/remote-review"
cat >"$remote/skills/remote-review/SKILL.md" <<'EOF'
---
name: remote-review
description: Review one remote fixture.
---
# Remote review
EOF
git -C "$remote" init -q
git -C "$remote" config user.email ink-skills@example.invalid
git -C "$remote" config user.name 'Ink Skills Test'
git -C "$remote" add skills/remote-review/SKILL.md
git -C "$remote" commit -qm fixture
commit=$(git -C "$remote" rev-parse HEAD)
mkdir -p "$tmp/remote-home"
HOME=$tmp/remote-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills add --ref HEAD "$remote" skills/remote-review >"$tmp/add.tsv"
remote_link=$tmp/remote-home/.ink/skills/remote-review
[ -L "$remote_link" ]
case $(readlink "$remote_link") in
"$tmp/store/artifacts/"*"/$commit/skills/remote-review") ;;
*) echo 'remote link is not pinned to resolved commit' >&2; exit 1 ;;
esac
grep "remote-review.*linked.*$commit" "$tmp/add.tsv" >/dev/null
manifest=$tmp/remote-home/.ink/skills/.ink-skills.tsv
[ "$(wc -l <"$manifest")" -eq 2 ]
awk -F '\t' -v commit="$commit" 'NR == 2 { exit !($1 == "remote-review" && $4 == commit && length($6) == 64) }' "$manifest"
HOME=$tmp/remote-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills add --ref HEAD "$remote" skills/remote-review >"$tmp/readd.tsv"
grep 'remote-review.*unchanged' "$tmp/readd.tsv" >/dev/null
[ "$(wc -l <"$manifest")" -eq 2 ]
printf '\nchanged\n' >>"$remote/skills/remote-review/SKILL.md"
git -C "$remote" add skills/remote-review/SKILL.md
git -C "$remote" commit -qm changed
if HOME=$tmp/remote-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills add --ref HEAD "$remote" skills/remote-review >/dev/null 2>"$tmp/pin.err"; then
echo 'expected provenance collision after ref moves' >&2
exit 1
fi
grep 'provenance collision' "$tmp/pin.err" >/dev/null
if HOME=$tmp/remote-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills add 'https://user:secret@example.invalid/skills.git' >/dev/null 2>"$tmp/credential.err"; then
echo 'expected credentialed URL refusal' >&2
exit 1
fi
grep 'credentialed HTTP URLs are refused' "$tmp/credential.err" >/dev/null
printf 'ok\n'
+35
View File
@@ -0,0 +1,35 @@
#!/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
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
create_tool=$repo/skills/create-ink-tool/SKILL.md
grep 'stage/match/apply' "$create_tool" >/dev/null
grep 'projection' "$create_tool" >/dev/null
printf 'ok\n'