Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efc4b70e13 | |||
| cb555a41b2 | |||
| 5552014329 |
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+220
-52
@@ -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,50 +222,98 @@ 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
|
||||
;;
|
||||
printf 'SKILL\tTARGET\tACTION\tSOURCE\tCOMMIT\tSHA256\n'
|
||||
|
||||
if [ "$command" = install ]; then
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set --
|
||||
for path in "$skills_dir"/*; do
|
||||
[ -d "$path" ] || continue
|
||||
[ -f "$path/SKILL.md" ] || continue
|
||||
set -- "$@" "$(basename -- "$path")"
|
||||
done
|
||||
fi
|
||||
for skill do
|
||||
case $skill in
|
||||
''|.*|*/*) die "invalid skill name: $skill" ;;
|
||||
esac
|
||||
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'
|
||||
|
||||
mkdir -p -- "$target"
|
||||
if [ -n "${INK_SKILLS_STORE:-}" ]; then
|
||||
store=$INK_SKILLS_STORE
|
||||
elif [ -n "${XDG_DATA_HOME:-}" ]; then
|
||||
store=$XDG_DATA_HOME/ink-skills
|
||||
else
|
||||
[ -n "${HOME:-}" ] || die 'HOME is unset; set HOME, XDG_DATA_HOME, or INK_SKILLS_STORE'
|
||||
store=$HOME/.local/share/ink-skills
|
||||
fi
|
||||
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 path in "$skills_dir"/*; do
|
||||
[ -d "$path" ] || continue
|
||||
[ -f "$path/SKILL.md" ] || continue
|
||||
set -- "$@" "$(basename -- "$path")"
|
||||
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
|
||||
|
||||
printf 'SKILL\tTARGET\tACTION\n'
|
||||
for skill do
|
||||
case $skill in
|
||||
''|.*|*/*) die "invalid skill name: $skill" ;;
|
||||
for skill_path do
|
||||
reject_record_breaks "$skill_path"
|
||||
case $skill_path in
|
||||
''|/*|*:*|..|../*|*/../*|*/..) die "invalid repository skill path: $skill_path" ;;
|
||||
esac
|
||||
source=$skills_dir/$skill
|
||||
[ -d "$source" ] && [ -f "$source/SKILL.md" ] || die "unknown skill: $skill"
|
||||
destination=$target/$skill
|
||||
if [ -L "$destination" ]; then
|
||||
linked=$(readlink "$destination")
|
||||
[ "$linked" = "$source" ] || die "refusing foreign symlink: $destination -> $linked"
|
||||
action=unchanged
|
||||
elif [ -e "$destination" ]; then
|
||||
die "refusing existing path: $destination"
|
||||
else
|
||||
ln -s -- "$source" "$destination"
|
||||
action=linked
|
||||
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
|
||||
printf '%s\t%s\t%s\n' "$skill" "$destination" "$action"
|
||||
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`.
|
||||
@@ -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
@@ -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'
|
||||
|
||||
Executable
+35
@@ -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'
|
||||
Reference in New Issue
Block a user