Compare commits

...

3 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
8 changed files with 346 additions and 277 deletions
+5 -4
View File
@@ -5,15 +5,16 @@
## Layout ## Layout
- `skills/<name>/SKILL.md` — one narrowly triggered on-demand behavior. - `skills/<name>/SKILL.md` — one narrowly triggered on-demand behavior.
- `bin/ink-skills` — dependency-free installer; stdout is TSV, diagnostics stderr. - `bin/ink-skills` — POSIX linker/importer; stdout is TSV, diagnostics stderr.
- `test/install-smoke.sh` — installer contract smoke. - `test/install-smoke.sh` — installer contract smoke.
## Rules ## Rules
- A skill owns reusable judgment, never runtime policy or repeatable mechanics. - A skill owns reusable judgment, never runtime policy or repeatable mechanics.
- Ink source and `REQUIREMENTS.md` own host behavior; `toolset` owns external executables. - Ink source and `SPEC.md` own host behavior; `toolset` owns external executables.
- Keep skill names lowercase and hyphenated; directory and frontmatter name must match. - Keep skill names lowercase and hyphenated; directory and frontmatter name must match.
- The installer only creates symlinks and must refuse collisions. Do not add a registry, - `link` only creates symlinks. `import` may materialize only a caller-supplied,
network calls, package-manager dependency, prompts, copies, or hidden state. SHA-256-verified local archive in the content-addressed store. Do not add URL,
Git, credential, registry, package-manager, prompt, or updater behavior.
- Run `sh -n bin/ink-skills`, `sh test/install-smoke.sh`, and - Run `sh -n bin/ink-skills`, `sh test/install-smoke.sh`, and
`sh test/skills-smoke.sh` after changes. `sh test/skills-smoke.sh` after changes.
+38 -35
View File
@@ -7,61 +7,58 @@ This repository owns reusable Ink judgment. Ink owns runtime enforcement and
policy. [`toolset`](https://git.tmk241.com/tmk241/toolset) owns compiled external policy. [`toolset`](https://git.tmk241.com/tmk241/toolset) owns compiled external
executables. Skills never grant authority by themselves. executables. Skills never grant authority by themselves.
## Install ## Link local skills
Clone once, then symlink the skills you want: Clone once, then link every skill shipped by this checkout:
```sh ```sh
git clone git@git.tmk241.com:tmk241/ink-skills.git git clone git@git.tmk241.com:tmk241/ink-skills.git
ink-skills/bin/ink-skills install ink-skills/bin/ink-skills link
``` ```
If the repository is already under `/opt/repositories`: Link selected directories or target one project:
```sh ```sh
/opt/repositories/ink-skills/bin/ink-skills install ink-skills link /opt/repositories/ink-skills/skills/audit-ink-cli
ink-skills link --project /path/to/project skills/configure-ink-agent
``` ```
Install selected skills only: `link` only creates absolute symlinks. It performs no network access, copies,
prompts, registry lookup, or policy mutation. Pulling a linked checkout changes
its bytes; restart Ink to freeze the updated skill snapshot.
## Import verified artifacts
Transport and authentication remain ordinary shell jobs:
```sh ```sh
ink-skills install audit-ink-cli configure-ink-agent curl -fLo skills.tar https://example/skills.tar
git archive --format=tar HEAD >skills.tar
``` ```
Install into one project instead of the user catalogue: The publisher communicates the expected digest out of band. Import only after you
have that value:
```sh ```sh
ink-skills install --project /path/to/project configure-ink-agent ink-skills import sha256:012345... skills.tar
ink-skills import --project /path/to/project sha256:012345... skills.tar skills/review-sql
``` ```
Install from any Git repository your normal Git credentials can read: `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`.
```sh The same artifact works whether it arrived via curl, scp, USB, a browser download,
ink-skills add --ref main git@git.example:team/skills.git or `git archive`. `ink-skills` deliberately has no URL, Git, credential, branch,
ink-skills add https://git.example/team/skills.git skills/review-sql release, or update logic.
```
`add` resolves the ref to one commit, exports it into a content-addressed store, Ink discovers `$HOME/.ink/skills`, `$CWD/.ink/skills`, and colon-separated
computes a SHA-256 over each selected skill tree, and symlinks that immutable `INK_SKILLS_DIRS`. The installer-only `$INK_SKILLS_HOME` overrides the user link
artifact. Provenance is recorded next to installed links in `.ink-skills.tsv`: target. `ink-skills list` emits TSV; `ink-skills --help` is the complete command
source, requested ref, resolved commit, path, and SHA-256. It deliberately does manual. Existing paths and foreign symlinks are refused rather than overwritten.
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.
## Skills ## Skills
@@ -88,6 +85,12 @@ Implement the bounded frontend task and return proof.
`access: read|write` selects reader/writer scheduling; it does not grant commands `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. or tools. Definitions are frozen at startup, so restart Ink after changing one.
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 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. 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. Use the `configure-ink-agent` skill for the exact boundary and blocker.
+202 -169
View File
@@ -4,41 +4,41 @@ set -eu
usage() { usage() {
cat <<'EOF' cat <<'EOF'
usage: ink-skills list usage: ink-skills list
ink-skills install [--user | --project DIR] [SKILL ...] ink-skills link [--user | --project DIR] [SKILL_DIR ...]
ink-skills add [--user | --project DIR] [--ref REF] REPOSITORY [PATH ...] ink-skills import [--user | --project DIR] sha256:HASH ARCHIVE [PATH ...]
Install Ink skills as symlinks from this checkout or a pinned Git artifact. Link local Ink skills or import a verified skill archive.
Commands: Commands:
list List skills in this checkout as TSV. list List skills shipped by this checkout as TSV.
install Link named local skills; with no names, link every skill. link Symlink local skill directories. With no directories, link every
add Fetch REPOSITORY, pin REF to a commit, materialize an skill shipped by this checkout.
immutable snapshot, and link skill PATHs from it. PATH import Verify ARCHIVE against the required SHA-256, safely materialize its
defaults to every skills/*/SKILL.md directory. immutable tree, then link selected skill PATHs. PATH defaults to
every skills/*/SKILL.md directory in the archive.
Targets: Targets:
--user $INK_SKILLS_HOME or $HOME/.ink/skills (default) --user $INK_SKILLS_HOME or $HOME/.ink/skills (default)
--project DIR DIR/.ink/skills --project DIR DIR/.ink/skills
Git storage: Artifact store:
$INK_SKILLS_STORE or $XDG_DATA_HOME/ink-skills, otherwise $INK_SKILLS_STORE or $XDG_DATA_HOME/ink-skills, otherwise
$HOME/.local/share/ink-skills. Git, tar, and sha256sum are required by `add`. $HOME/.local/share/ink-skills.
Authentication belongs to Git's SSH agent or credential helper; credentialed
HTTP URLs are refused. Installed provenance is written to Transport is deliberately external:
TARGET/.ink-skills.tsv without modifying SKILL.md. curl -fLo skills.tar URL
ink-skills import sha256:HASH skills.tar
Output: Output:
TSV with SKILL, TARGET, ACTION, SOURCE, COMMIT, and SHA256 columns. TSV with SKILL, TARGET, ACTION, SOURCE, and SHA256 columns.
Exit status: Exit status:
0 success; 2 usage error; 3 collision, invalid source, or fetch failure. 0 success; 2 usage error; 3 invalid skill, collision, digest mismatch, or
unsafe archive.
Examples: Requirements:
ink-skills list POSIX sh and standard text tools. `import` additionally needs tar and one of
ink-skills install audit-ink-cli configure-ink-agent sha256sum, shasum, or openssl.
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 EOF
} }
@@ -47,6 +47,22 @@ die() {
exit 3 exit 3
} }
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
repo_dir=$(CDPATH= cd -- "$script_dir/.." && pwd -P)
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
reject_record_breaks() { reject_record_breaks() {
case $1 in case $1 in
*" "*|*" *" "*|*"
@@ -54,17 +70,20 @@ reject_record_breaks() {
esac esac
} }
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) frontmatter_name() {
repo_dir=$(CDPATH= cd -- "$script_dir/.." && pwd -P) sed -n 's/^name:[[:space:]]*//p' "$1/SKILL.md" | sed -n '1p'
skills_dir=$repo_dir/skills }
list_skills() { validate_skill_dir() {
printf 'SKILL\tSOURCE\n' skill_dir=$1
for path in "$skills_dir"/*; do [ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ] || die "not a skill directory: $skill_dir"
[ -d "$path" ] || continue [ ! -L "$skill_dir" ] && [ ! -L "$skill_dir/SKILL.md" ] || die "skill root and SKILL.md must not be symlinks: $skill_dir"
[ -f "$path/SKILL.md" ] || continue skill_name=$(frontmatter_name "$skill_dir")
printf '%s\t%s\n' "$(basename -- "$path")" "$path" [ -n "$skill_name" ] || die "missing frontmatter name: $skill_dir/SKILL.md"
done [ "$skill_name" = "$(basename -- "$skill_dir")" ] || die "frontmatter name does not match directory: $skill_dir"
case $skill_name in
''|.*|*/*) die "invalid skill name: $skill_name" ;;
esac
} }
select_target() { select_target() {
@@ -85,48 +104,48 @@ select_target() {
mkdir -p -- "$target" mkdir -p -- "$target"
} }
parse_target_option() { parse_target_options() {
case $1 in while [ "$#" -gt 0 ]; do
--user) case $1 in
target_mode=user --user)
shift_count=1 target_mode=user
;; shift
--project) ;;
[ "$#" -ge 2 ] || { --project)
[ "$#" -ge 2 ] || {
usage >&2
exit 2
}
target_mode=project
target_arg=$2
shift 2
;;
--)
shift
break
;;
-*)
usage >&2 usage >&2
exit 2 exit 2
} ;;
target_mode=project *) break ;;
target_arg=$2 esac
shift_count=2 done
;; remaining_count=$#
*) shift_count=0 ;; remaining_file=$work_args
esac : >"$remaining_file"
} for arg do
reject_record_breaks "$arg"
frontmatter_name() { printf '%s\n' "$arg" >>"$remaining_file"
sed -n 's/^name:[[:space:]]*//p' "$1/SKILL.md" | sed -n '1p' done
}
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() { link_skill() {
skill_name=$1 name=$1
source_path=$2 source_path=$2
source_label=$3 source_label=$3
commit=$4 digest=$4
digest=$5 destination=$target/$name
destination=$target/$skill_name
if [ -L "$destination" ]; then if [ -L "$destination" ]; then
linked=$(readlink "$destination") linked=$(readlink "$destination")
[ "$linked" = "$source_path" ] || die "refusing foreign symlink: $destination -> $linked" [ "$linked" = "$source_path" ] || die "refusing foreign symlink: $destination -> $linked"
@@ -137,34 +156,58 @@ link_skill() {
ln -s -- "$source_path" "$destination" ln -s -- "$source_path" "$destination"
action=linked action=linked
fi fi
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$skill_name" "$destination" "$action" "$source_label" "$commit" "$digest" printf '%s\t%s\t%s\t%s\t%s\n' "$name" "$destination" "$action" "$source_label" "$digest"
} }
check_source_record() { manifest_check() {
skill_name=$1 name=$1
source_label=$2 digest=$2
ref=$3 archive_path=$3
commit=$4
skill_path=$5
digest=$6
manifest=$target/.ink-skills.tsv manifest=$target/.ink-skills.tsv
record_needed=yes record_needed=yes
[ -e "$manifest" ] || return 0 [ -f "$manifest" ] || return 0
if awk -F '\t' -v skill="$skill_name" 'NR > 1 && $1 == skill { found = 1 } END { exit !found }' "$manifest"; then if awk -F '\t' -v name="$name" 'NR > 1 && $1 == name { found = 1 } END { exit !found }' "$manifest"; then
existing=$(awk -F '\t' -v skill="$skill_name" 'NR > 1 && $1 == skill { print $0; exit }' "$manifest") existing=$(awk -F '\t' -v name="$name" 'NR > 1 && $1 == name { 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") wanted=$(printf '%s\t%s\t%s' "$name" "$digest" "$archive_path")
[ "$existing" = "$wanted" ] || die "provenance collision for installed skill: $skill_name" [ "$existing" = "$wanted" ] || die "provenance collision for installed skill: $name"
record_needed=no record_needed=no
fi fi
} }
append_source_record() { manifest_append() {
[ "$record_needed" = yes ] || return 0 [ "$record_needed" = yes ] || return 0
manifest=$target/.ink-skills.tsv manifest=$target/.ink-skills.tsv
if [ ! -e "$manifest" ]; then [ -e "$manifest" ] || printf 'SKILL\tSHA256\tPATH\n' >"$manifest"
printf 'SKILL\tSOURCE\tREF\tCOMMIT\tPATH\tSHA256\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 fi
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$skill_name" "$repository" "$ref" "$commit" "$skill_path" "$digest" >>"$manifest" }
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 ] || { [ "$#" -gt 0 ] || {
@@ -183,10 +226,14 @@ case $command in
usage >&2 usage >&2
exit 2 exit 2
} }
list_skills printf 'SKILL\tSOURCE\n'
for path in "$bundled_dir"/*; do
[ -d "$path" ] && [ -f "$path/SKILL.md" ] || continue
printf '%s\t%s\n' "$(basename -- "$path")" "$path"
done
exit 0 exit 0
;; ;;
install|add) ;; link|import) ;;
*) *)
usage >&2 usage >&2
exit 2 exit 2
@@ -195,74 +242,57 @@ esac
target_mode=user target_mode=user
target_arg= target_arg=
ref=HEAD work_root=${TMPDIR:-/tmp}/ink-skills-args-$$
while [ "$#" -gt 0 ]; do (umask 077 && mkdir "$work_root") || die "cannot create temporary directory: $work_root"
parse_target_option "$@" cleanup_root=$work_root
if [ "$shift_count" -gt 0 ]; then work_args=$work_root/args
shift "$shift_count" parse_target_options "$@"
continue set --
fi while IFS= read -r arg; do set -- "$@" "$arg"; done <"$work_args"
case $1 in
--ref)
[ "$command" = add ] && [ "$#" -ge 2 ] || {
usage >&2
exit 2
}
ref=$2
shift 2
;;
--)
shift
break
;;
-*)
usage >&2
exit 2
;;
*) break ;;
esac
done
select_target select_target
printf 'SKILL\tTARGET\tACTION\tSOURCE\tCOMMIT\tSHA256\n' printf 'SKILL\tTARGET\tACTION\tSOURCE\tSHA256\n'
if [ "$command" = install ]; then if [ "$command" = link ]; then
if [ "$#" -eq 0 ]; then if [ "$#" -eq 0 ]; then
set -- set --
for path in "$skills_dir"/*; do for path in "$bundled_dir"/*; do
[ -d "$path" ] || continue [ -d "$path" ] && [ -f "$path/SKILL.md" ] || continue
[ -f "$path/SKILL.md" ] || continue set -- "$@" "$path"
set -- "$@" "$(basename -- "$path")"
done done
fi fi
for skill do for source_path do
case $skill in case $source_path in
''|.*|*/*) die "invalid skill name: $skill" ;; /*) ;;
*) 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 esac
source_path=$skills_dir/$skill
validate_skill_dir "$source_path" validate_skill_dir "$source_path"
link_skill "$skill_name" "$source_path" local - - link_skill "$skill_name" "$source_path" local -
done done
exit 0 exit 0
fi fi
[ "$#" -gt 0 ] || { [ "$#" -ge 2 ] || {
usage >&2 usage >&2
exit 2 exit 2
} }
repository=$1 digest_spec=$1
shift archive=$2
reject_record_breaks "$repository" shift 2
reject_record_breaks "$ref" case $digest_spec in
case $ref in sha256:*) digest=${digest_spec#sha256:} ;;
-*) die "invalid ref: $ref" ;; *) die 'digest must use sha256:HASH' ;;
esac esac
case $repository in digest=$(printf '%s' "$digest" | tr 'A-F' 'a-f')
http://*@*|https://*@*) die 'credentialed HTTP URLs are refused; use an SSH agent or Git credential helper' ;; case $digest in
*[!0-9a-f]*|'') die 'SHA-256 must contain 64 hexadecimal characters' ;;
esac esac
command -v git >/dev/null 2>&1 || die 'git is required by add' [ "${#digest}" -eq 64 ] || die 'SHA-256 must contain 64 hexadecimal characters'
command -v tar >/dev/null 2>&1 || die 'tar is required by add' [ -f "$archive" ] || die "archive not found: $archive"
command -v sha256sum >/dev/null 2>&1 || die 'sha256sum is required by add' 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 if [ -n "${INK_SKILLS_STORE:-}" ]; then
store=$INK_SKILLS_STORE store=$INK_SKILLS_STORE
@@ -272,48 +302,51 @@ else
[ -n "${HOME:-}" ] || die 'HOME is unset; set HOME, XDG_DATA_HOME, or INK_SKILLS_STORE' [ -n "${HOME:-}" ] || die 'HOME is unset; set HOME, XDG_DATA_HOME, or INK_SKILLS_STORE'
store=$HOME/.local/share/ink-skills store=$HOME/.local/share/ink-skills
fi fi
repo_key=$(printf '%s' "$repository" | git hash-object --stdin) || die 'cannot hash repository identity' artifact=$store/sha256/$digest
mirror=$store/git/$repo_key.git tree=$artifact/tree
mkdir -p -- "$store/git" "$store/artifacts/$repo_key" if [ -d "$artifact" ]; then
if [ ! -d "$mirror" ]; then [ -f "$artifact/complete" ] && [ "$(cat "$artifact/complete")" = "$digest" ] || die "incomplete artifact store entry: $artifact"
git clone --quiet --mirror -- "$repository" "$mirror" || die "cannot clone repository: $repository" else
fi mkdir -p -- "$store/sha256"
git --git-dir="$mirror" fetch --quiet --force origin "$ref" || die "cannot fetch ref: $ref" lock=$store/sha256/.$digest.lock
commit=$(git --git-dir="$mirror" rev-parse --verify 'FETCH_HEAD^{commit}') || die "ref does not resolve to a commit: $ref" mkdir "$lock" 2>/dev/null || die "artifact import already in progress: $digest"
artifact=$store/artifacts/$repo_key/$commit temporary=$store/sha256/.$digest.tmp.$$
if [ ! -d "$artifact" ]; then cleanup_lock=$lock
temporary=$artifact.tmp.$$ cleanup_artifact=$temporary
rm -rf -- "$temporary" mkdir -p -- "$temporary/tree"
mkdir -p -- "$temporary" validate_archive_listing "$archive" "$work_root/names" "$work_root/types"
if ! git --git-dir="$mirror" archive "$commit" | tar -x -C "$temporary"; then tar -xf "$archive" -C "$temporary/tree" || die "cannot extract archive: $archive"
rm -rf -- "$temporary" if find "$temporary/tree" -type l -print | grep . >/dev/null 2>&1; then
die "cannot materialize commit: $commit" die 'archive extracted symlinks'
fi fi
mv -- "$temporary" "$artifact" 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 fi
if [ "$#" -eq 0 ]; then if [ "$#" -eq 0 ]; then
set -- set --
for source_path in "$artifact"/skills/*; do for skill_dir in "$tree"/skills/*; do
[ -d "$source_path" ] || continue [ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ] || continue
[ -f "$source_path/SKILL.md" ] || continue set -- "$@" "skills/$(basename -- "$skill_dir")"
set -- "$@" "skills/$(basename -- "$source_path")"
done done
[ "$#" -gt 0 ] || die "repository has no skills/*/SKILL.md at $commit" [ "$#" -gt 0 ] || die 'archive has no skills/*/SKILL.md directories'
fi fi
for skill_path do for archive_path do
reject_record_breaks "$skill_path" case $archive_path in
case $skill_path in ''|/*|*:*|..|../*|*/../*|*/..) die "invalid archive skill path: $archive_path" ;;
''|/*|*:*|..|../*|*/../*|*/..) die "invalid repository skill path: $skill_path" ;;
esac esac
source_path=$artifact/$skill_path source_path=$tree/$archive_path
validate_skill_dir "$source_path" validate_skill_dir "$source_path"
if find "$source_path" -type l -print | grep . >/dev/null 2>&1; then name=$skill_name
die "remote skill contains symlinks: $skill_path" manifest_check "$name" "$digest" "$archive_path"
fi link_skill "$name" "$source_path" artifact "$digest"
digest=$(git --git-dir="$mirror" archive "$commit:$skill_path" | sha256sum | awk '{print $1}') || die "cannot hash skill artifact: $skill_path" manifest_append
check_source_record "$skill_name" "$repository" "$ref" "$commit" "$skill_path" "$digest"
link_skill "$skill_name" "$source_path" "$repository" "$commit" "$digest"
append_source_record
done done
+9 -7
View File
@@ -37,14 +37,16 @@ changes belong to Ink's repository authority and implementation workflow.
- A human/operator may invoke Ink outside the governed agent. Give a copyable - A human/operator may invoke Ink outside the governed agent. Give a copyable
operator command only when the user asks and its public help contract is proven. operator command only when the user asks and its public help contract is proven.
- Use user-provided runtime output as runtime evidence. Otherwise inspect the - Use user-provided runtime output as runtime evidence. Otherwise inspect the
installed artifact identity and a demonstrably matching checkout's requirements, installed artifact identity and a demonstrably matching checkout's specification,
tests, public help text, and source; label those findings as contract/source tests, public help text, and source; label those findings as contract/source
evidence rather than executed runtime proof. evidence rather than executed runtime proof.
- Global and project policy files explain only their contribution. Effective - Approved global and ancestor-project rows are additive alternatives; each file
authority also depends on all policy layers, pinned executable and contract explains only its contribution. Effective authority also depends on the approved
bytes, startup freezing, and session decisions. normalized digest, restrictive child/session policy, the host floor, pinned
executable and contract bytes, startup freezing, and session decisions. Never
interpret a project file as automatically trusted.
- Treat handovers, READMEs, examples, hidden source branches, and remembered argv - Treat handovers, READMEs, examples, hidden source branches, and remembered argv
as leads. Public help owns operator-facing commands; requirements own intended as leads. Public help owns operator-facing commands; the specification owns intended
behavior; tests and source establish current checkout behavior. behavior; tests and source establish current checkout behavior.
Distinguish three surfaces explicitly: Distinguish three surfaces explicitly:
@@ -71,7 +73,7 @@ the frozen `tool delegate` subject.
**source-confirmed**, **missing**, **stale claim**, or **not proven**. **source-confirmed**, **missing**, **stale claim**, or **not proven**.
4. For an operator action, explain that the user—not the hosted agent—must run it. 4. For an operator action, explain that the user—not the hosted agent—must run it.
Do not inspect or mutate session state as a substitute. Do not inspect or mutate session state as a substitute.
5. For a missing capability, require Ink's requirements authority before source 5. For a missing capability, require Ink's specification authority before source
implementation. Do not model it as a new external executable merely to bypass implementation. Do not model it as a new external executable merely to bypass
the host boundary. the host boundary.
@@ -93,7 +95,7 @@ the frozen `tool delegate` subject.
EVIDENCE: <runtime output, installed artifact, matching source, or limitation> EVIDENCE: <runtime output, installed artifact, matching source, or limitation>
SURFACE: <operator CLI, model built-in, or admitted external command> SURFACE: <operator CLI, model built-in, or admitted external command>
FINDING: <observed/source-confirmed/missing/stale/not proven> FINDING: <observed/source-confirmed/missing/stale/not proven>
ACTION: <operator step, requirements step, or none> ACTION: <operator step, specification step, or none>
``` ```
Positive smoke: “Does Ink have a sessions command, and why can’t you run it?” Positive smoke: “Does Ink have a sessions command, and why can’t you run it?”
+6 -5
View File
@@ -63,7 +63,7 @@ no ancestor-chain project-agent discovery.
- `read` children may overlap and receive an immutable host floor with no command, - `read` children may overlap and receive an immutable host floor with no command,
file-mutation, lifecycle-mutation, or delegation authority. file-mutation, lifecycle-mutation, or delegation authority.
- `write` children are exclusive, operate in the canonical parent cwd, pause - `write` children are exclusive, operate in the canonical parent cwd, pause
parent effects, and still receive only their effective frozen policy. parent mutations, and still receive only their effective frozen policy.
Never infer effective authority from `access`, prompt text, or a `policy:` label. Never infer effective authority from `access`, prompt text, or a `policy:` label.
Use the child policy snapshot and a real allowed/denied smoke. Use the child policy snapshot and a real allowed/denied smoke.
@@ -83,11 +83,12 @@ policy. Therefore:
those are already sufficient. those are already sufficient.
This gate may be removed only after the provider launch path proves that the 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. referenced bytes are pinned and applied as a restriction to the child's inherited
approved durable policy.
## Decision loop ## Decision loop
1. Choose `read` unless the child must produce a real effect. 1. Choose `read` unless the child must perform a real mutation.
2. Choose the smallest model alias that fits the specialist job. 2. Choose the smallest model alias that fits the specialist job.
3. Put the definition in user scope or exact project cwd according to intended 3. Put the definition in user scope or exact project cwd according to intended
precedence. precedence.
@@ -107,8 +108,8 @@ referenced bytes are pinned and conjoined into the child effective policy.
## Behavior smoke ## Behavior smoke
Positive: “Create a frontend writer child with only formatter and file mutation Positive: “Create a frontend writer child with only formatter and file mutation
authority” loads this skill and blocks until the named policy is actually authority” loads this skill and blocks until the named restrictive policy is
conjoined or the parent frozen policy already supplies that exact boundary. actually enforced or the parent frozen policy already supplies that exact boundary.
Negative: “Ask the existing reviewer to inspect this diff” does not load this Negative: “Ask the existing reviewer to inspect this diff” does not load this
skill; it is ordinary delegation. skill; it is ordinary delegation.
+20 -18
View File
@@ -13,7 +13,7 @@ description: >-
## One job ## One job
Design a **permission-sized executable** whose name and argv expose its reachable Design a **permission-sized executable** whose name and argv expose its reachable
effects so Ink can discover, digest-pin, and grant it without granting a platform. reads and mutations so Ink can discover, digest-pin, and grant it without granting a platform.
One executable need not mean one source file: share private build-time modules One executable need not mean one source file: share private build-time modules
when that does not widen runtime authority. when that does not widen runtime authority.
@@ -21,26 +21,26 @@ when that does not widen runtime authority.
Read only what can change the boundary: Read only what can change the boundary:
- the exact job and every reachable side effect; - the exact job and every reachable mutation;
- Ink's current requirements, policy grammar, and mutation protocol; - Ink's current specification, policy grammar, and mutation protocol;
- neighboring tools and existing executables that may already satisfy the job; - neighboring tools and existing executables that may already satisfy the job;
- resource, credential, selector, target, packaging, and proof contracts; - resource, credential, selector, target, packaging, and proof contracts;
- repository requirements and tests. - repository specification and tests.
Project authority outranks this skill. Missing selector semantics, credentials, Project authority outranks this skill. Missing selector semantics, credentials,
recovery rules, or external contracts are blockers—not adapter opportunities. recovery rules, or external contracts are blockers—not adapter opportunities.
```text ```text
JOB -> REUSE? -> EFFECTS -> BOUNDARY -> CONTRACT -> ARTIFACT -> PROOF JOB -> REUSE? -> READS/MUTATIONS -> BOUNDARY -> CONTRACT -> ARTIFACT -> PROOF
``` ```
1. Reuse a directly inspectable executable only when granting its whole reachable 1. Reuse a directly inspectable executable only when granting its whole reachable
surface is honest; otherwise build the coherent missing boundary, not a wrapper. surface is honest; otherwise build the coherent missing boundary, not a wrapper.
2. Enumerate reads, mutations, network effects, secrets, state, children, and 2. Enumerate reads, mutations, network calls, secrets, state, children, and
config/plugin discovery; split where approval, blast radius, or recovery differ. config/plugin discovery; split where approval, blast radius, or recovery differ.
3. Define argv, selectors, streams, errors, dependencies, exhaustive versus 3. Define argv, selectors, streams, errors, dependencies, exhaustive versus
bounded output, and any tool-owned semantic presentation without recreating bounded output, and any tool-owned semantic presentation without recreating
shell grammar or a host-wide effect ontology. shell grammar or a host-wide mutation ontology.
4. State the runtime artifact honestly, then falsify its boundary, behavior, 4. State the runtime artifact honestly, then falsify its boundary, behavior,
presentation, and portability claims through Ink's real run entry point. presentation, and portability claims through Ink's real run entry point.
@@ -108,7 +108,7 @@ Shell globs, path filters, and content patterns are different contracts.
`find`/`fd` forms that execute or delete and `rg` forms that launch preprocessors `find`/`fd` forms that execute or delete and `rg` forms that launch preprocessors
are not read boundaries merely because one intended invocation only searches. A are not read boundaries merely because one intended invocation only searches. A
narrow native search tool is justified when it removes reachable effects, adds narrow native search tool is justified when it removes reachable mutations, adds
canonical path selectors, or supplies the required static portable artifact. canonical path selectors, or supplies the required static portable artifact.
Implement the coherent missing subset, not a compatibility facade or renamed Implement the coherent missing subset, not a compatibility facade or renamed
wrapper. wrapper.
@@ -125,8 +125,8 @@ tool apply <id> <hash> # revalidate; perform once
Required properties: Required properties:
- `stage` performs no external effect and resolves no secrets; - `stage` performs no mutation and resolves no secrets;
- the manifest pins executable identity, canonical effect, authority subject, - the manifest pins executable identity, canonical mutation, authority subject,
non-secret inputs, and drift-sensitive hashes; non-secret inputs, and drift-sensitive hashes;
- selectors are variable semantic facts, never argv prefixes, shell text, or - selectors are variable semantic facts, never argv prefixes, shell text, or
executable invariants; executable invariants;
@@ -149,7 +149,7 @@ layout; the tool owns meaning.
Use the repository's elected projection envelope and bounds exactly; never invent Use the repository's elected projection envelope and bounds exactly; never invent
per-tool presentation formats. Within that contract, use a small display per-tool presentation formats. Within that contract, use a small display
vocabulary rather than universal effect kinds: vocabulary rather than universal mutation kinds:
- headline and canonical target; - headline and canonical target;
- ordered key/value facts; - ordered key/value facts;
@@ -159,7 +159,7 @@ vocabulary rather than universal effect kinds:
Filesystem, HTTP, cloud, and infrastructure tools express their own semantics Filesystem, HTTP, cloud, and infrastructure tools express their own semantics
with those primitives. For example, an HTTP mutator supplies method, canonical with those primitives. For example, an HTTP mutator supplies method, canonical
origin/path, bounded body summary, status, and final URL as facts; it does not ask origin/path, bounded body summary, status, and final URL as facts; it does not ask
Ink to understand an `http` effect type. An infrastructure tool supplies account, Ink to understand an `http` mutation type. An infrastructure tool supplies account,
resource, region, and requested change as facts; it does not create a renderer resource, region, and requested change as facts; it does not create a renderer
branch for its provider. branch for its provider.
@@ -169,7 +169,7 @@ The projection is presentation evidence, never authority:
- derive projection records purely from that value and hash their exact semantic - derive projection records purely from that value and hash their exact semantic
bytes with the manifest; bytes with the manifest;
- render approval from the exact staged projection stored under that identity; - render approval from the exact staged projection stored under that identity;
- apply accepts only staged id plus hash, never replacement target, body, effect, - apply accepts only staged id plus hash, never replacement target, body, mutation,
or projection inputs; or projection inputs;
- the receipt identifies the exact staged manifest and may add only outcome and - the receipt identifies the exact staged manifest and may add only outcome and
evidence facts; it cannot rewrite approved records; evidence facts; it cannot rewrite approved records;
@@ -189,12 +189,14 @@ otherwise expose manifests, hashes, encoded payloads, or provider internals.
## CLI and stream contract ## CLI and stream contract
`tool --help` is the tested human contract: effects, argv, streams, ordering, `tool --help` is the tested human contract: mutations, argv, streams, ordering,
exits, environment/config precedence, credential timing, dependencies, pattern exits, environment/config precedence, credential timing, dependencies, pattern
semantics, and one realistic pipeline. Explicit help succeeds on stdout; usage semantics, and one realistic pipeline. Explicit help succeeds on stdout; usage
errors fail on stderr. Selector help gives value grammar, canonicalization, and a errors fail on stderr. Selector help gives value grammar, canonicalization, and a
least-authority policy row, including AND within one row and alternatives across least-authority policy row, including AND within one row and alternatives across
rows. approved global and ancestor-project rows. Project rows are not automatically
trusted: Ink freezes the normalized effective policy and requires digest approval;
child/session policy and the host floor may only narrow it.
Keep argv unsurprising: options before operands, `--` ends options, `-` denotes a Keep argv unsurprising: options before operands, `--` ends options, `-` denotes a
natural stream, secrets never enter argv, and unknown, incompatible, or trailing natural stream, secrets never enter argv, and unknown, incompatible, or trailing
@@ -250,7 +252,7 @@ interpreters, or generated-client machinery unless they are the named job.
Use the smallest matrix that can falsify the actual claims: Use the smallest matrix that can falsify the actual claims:
- help/contract agree with accepted argv and selectors; - help/contract agree with accepted argv and selectors;
- main path and one forbidden near miss with zero unintended effect; - main path and one forbidden near miss with zero unintended mutation;
- each reader selector has an adjacent no-match before access; - each reader selector has an adjacent no-match before access;
- each mutator has admitted, approval-required, refused, drift, duplicate, and - each mutator has admitted, approval-required, refused, drift, duplicate, and
indeterminate/recovery outcomes as applicable; indeterminate/recovery outcomes as applicable;
@@ -278,7 +280,7 @@ Do not:
- wrap or partially clone a utility whose whole admitted surface already fits; - wrap or partially clone a utility whose whole admitted surface already fits;
- combine read and mutation for code reuse; - combine read and mutation for code reuse;
- expose a generic request/admin/registry platform; - expose a generic request/admin/registry platform;
- invent invariant selectors, a universal effect ontology, or executable-name - invent invariant selectors, a universal mutation ontology, or executable-name
branches in Ink's renderer; branches in Ink's renderer;
- let tools choose terminal styling or let presentation metadata grant authority; - let tools choose terminal styling or let presentation metadata grant authority;
- treat argv prefixes, globs, or regexes as canonical path authority; - treat argv prefixes, globs, or regexes as canonical path authority;
@@ -312,7 +314,7 @@ Report only:
- **Presentation:** an HTTP mutator derives method, canonical target, and bounded - **Presentation:** an HTTP mutator derives method, canonical target, and bounded
body summary from one staged operation; Ink renders the exact stored projection body summary from one staged operation; Ink renders the exact stored projection
without an `httpsend` branch and overlays receipt-bound status/final URL only. without an `httpsend` branch and overlays receipt-bound status/final URL only.
- **Presentation sprawl:** “support AWS changes” does not add an AWS effect enum or - **Presentation sprawl:** “support AWS changes” does not add an AWS mutation enum or
renderer branch; the permission-sized infrastructure tool projects account, renderer branch; the permission-sized infrastructure tool projects account,
region, resource, requested change, outcome, and evidence through the elected region, resource, requested change, outcome, and evidence through the elected
generic vocabulary. generic vocabulary.
+59 -39
View File
@@ -3,7 +3,11 @@ set -eu
repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P) repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
tmp=${TMPDIR:-/tmp}/ink-skills-smoke-$$ tmp=${TMPDIR:-/tmp}/ink-skills-smoke-$$
trap 'rm -rf "$tmp"' EXIT HUP INT TERM cleanup() {
chmod -R u+w "$tmp" 2>/dev/null || :
rm -rf "$tmp"
}
trap cleanup EXIT HUP INT TERM
mkdir -p "$tmp/home" "$tmp/project" mkdir -p "$tmp/home" "$tmp/project"
list=$($repo/bin/ink-skills list) list=$($repo/bin/ink-skills list)
@@ -11,28 +15,34 @@ printf '%s\n' "$list" | grep '^SKILL' >/dev/null
printf '%s\n' "$list" | grep '^audit-ink-cli' >/dev/null printf '%s\n' "$list" | grep '^audit-ink-cli' >/dev/null
printf '%s\n' "$list" | grep '^configure-ink-agent' >/dev/null printf '%s\n' "$list" | grep '^configure-ink-agent' >/dev/null
HOME=$tmp/home $repo/bin/ink-skills install audit-ink-cli >"$tmp/install.tsv" HOME=$tmp/home $repo/bin/ink-skills link >"$tmp/link.tsv"
grep "audit-ink-cli.*linked" "$tmp/install.tsv" >/dev/null grep "audit-ink-cli.*linked.*local" "$tmp/link.tsv" >/dev/null
[ -L "$tmp/home/.ink/skills/audit-ink-cli" ] [ -L "$tmp/home/.ink/skills/audit-ink-cli" ]
[ "$(readlink "$tmp/home/.ink/skills/audit-ink-cli")" = "$repo/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 audit-ink-cli >"$tmp/reinstall.tsv" HOME=$tmp/home $repo/bin/ink-skills link "$repo/skills/audit-ink-cli" >"$tmp/relink.tsv"
grep "audit-ink-cli.*unchanged" "$tmp/reinstall.tsv" >/dev/null grep "audit-ink-cli.*unchanged" "$tmp/relink.tsv" >/dev/null
mkdir -p "$tmp/home/.ink/skills/configure-ink-agent" mkdir -p "$tmp/home/.ink/skills/collision"
if HOME=$tmp/home $repo/bin/ink-skills install configure-ink-agent >/dev/null 2>"$tmp/collision.err"; then mkdir -p "$tmp/collision"
cat >"$tmp/collision/SKILL.md" <<'EOF'
---
name: collision
description: Fixture.
---
EOF
if HOME=$tmp/home $repo/bin/ink-skills link "$tmp/collision" >/dev/null 2>"$tmp/collision.err"; then
echo 'expected collision refusal' >&2 echo 'expected collision refusal' >&2
exit 1 exit 1
fi fi
grep 'refusing existing path' "$tmp/collision.err" >/dev/null grep 'refusing existing path' "$tmp/collision.err" >/dev/null
HOME=$tmp/home $repo/bin/ink-skills install --project "$tmp/project" create-ink-tool >"$tmp/project.tsv" HOME=$tmp/home $repo/bin/ink-skills link --project "$tmp/project" "$repo/skills/create-ink-tool" >"$tmp/project.tsv"
[ -L "$tmp/project/.ink/skills/create-ink-tool" ] [ -L "$tmp/project/.ink/skills/create-ink-tool" ]
grep "create-ink-tool.*linked" "$tmp/project.tsv" >/dev/null grep "create-ink-tool.*linked.*local" "$tmp/project.tsv" >/dev/null
remote=$tmp/remote mkdir -p "$tmp/archive-tree/skills/remote-review"
mkdir -p "$remote/skills/remote-review" cat >"$tmp/archive-tree/skills/remote-review/SKILL.md" <<'EOF'
cat >"$remote/skills/remote-review/SKILL.md" <<'EOF'
--- ---
name: remote-review name: remote-review
description: Review one remote fixture. description: Review one remote fixture.
@@ -40,43 +50,53 @@ description: Review one remote fixture.
# Remote review # Remote review
EOF EOF
git -C "$remote" init -q tar -cf "$tmp/skills.tar" -C "$tmp/archive-tree" skills
git -C "$remote" config user.email ink-skills@example.invalid digest=$(sha256sum "$tmp/skills.tar" | awk '{print $1}')
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" mkdir -p "$tmp/import-home"
HOME=$tmp/remote-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills add --ref HEAD "$remote" skills/remote-review >"$tmp/add.tsv" 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/remote-home/.ink/skills/remote-review remote_link=$tmp/import-home/.ink/skills/remote-review
[ -L "$remote_link" ] [ -L "$remote_link" ]
case $(readlink "$remote_link") in [ "$(readlink "$remote_link")" = "$tmp/store/sha256/$digest/tree/skills/remote-review" ]
"$tmp/store/artifacts/"*"/$commit/skills/remote-review") ;; grep "remote-review.*linked.*artifact.*$digest" "$tmp/import.tsv" >/dev/null
*) echo 'remote link is not pinned to resolved commit' >&2; exit 1 ;; manifest=$tmp/import-home/.ink/skills/.ink-skills.tsv
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 ] [ "$(wc -l <"$manifest")" -eq 2 ]
awk -F '\t' -v commit="$commit" 'NR == 2 { exit !($1 == "remote-review" && $4 == commit && length($6) == 64) }' "$manifest" awk -F '\t' -v digest="$digest" 'NR == 2 { exit !($1 == "remote-review" && $2 == digest && $3 == "skills/remote-review") }' "$manifest"
HOME=$tmp/remote-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills add --ref HEAD "$remote" skills/remote-review >"$tmp/readd.tsv" 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/readd.tsv" >/dev/null grep 'remote-review.*unchanged' "$tmp/reimport.tsv" >/dev/null
[ "$(wc -l <"$manifest")" -eq 2 ] [ "$(wc -l <"$manifest")" -eq 2 ]
printf '\nchanged\n' >>"$remote/skills/remote-review/SKILL.md" zero_digest=$(awk 'BEGIN { for (i = 0; i < 64; i++) printf "0" }')
git -C "$remote" add skills/remote-review/SKILL.md 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
git -C "$remote" commit -qm changed echo 'expected digest mismatch' >&2
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 exit 1
fi fi
grep 'provenance collision' "$tmp/pin.err" >/dev/null grep 'SHA-256 mismatch' "$tmp/digest.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 printf '\nchanged\n' >>"$tmp/archive-tree/skills/remote-review/SKILL.md"
echo 'expected credentialed URL refusal' >&2 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 exit 1
fi fi
grep 'credentialed HTTP URLs are refused' "$tmp/credential.err" >/dev/null grep 'provenance collision' "$tmp/provenance.err" >/dev/null
mkdir -p "$tmp/unsafe/skills/unsafe"
cat >"$tmp/unsafe/skills/unsafe/SKILL.md" <<'EOF'
---
name: unsafe
description: Unsafe fixture.
---
EOF
ln -s /etc/passwd "$tmp/unsafe/skills/unsafe/passwd"
tar -cf "$tmp/unsafe.tar" -C "$tmp/unsafe" skills
unsafe_digest=$(sha256sum "$tmp/unsafe.tar" | awk '{print $1}')
if HOME=$tmp/import-home INK_SKILLS_STORE=$tmp/store $repo/bin/ink-skills import "sha256:$unsafe_digest" "$tmp/unsafe.tar" >/dev/null 2>"$tmp/unsafe.err"; then
echo 'expected symlink archive refusal' >&2
exit 1
fi
grep 'regular files and directories' "$tmp/unsafe.err" >/dev/null
printf 'ok\n' printf 'ok\n'
+7
View File
@@ -23,13 +23,20 @@ configure=$repo/skills/configure-ink-agent/SKILL.md
grep 'access.*read.*write' "$configure" >/dev/null grep 'access.*read.*write' "$configure" >/dev/null
grep 'named agent policy is metadata-only' "$configure" >/dev/null grep 'named agent policy is metadata-only' "$configure" >/dev/null
grep 'no `INK_AGENT_HOME` contract' "$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 audit_cli=$repo/skills/audit-ink-cli/SKILL.md
grep 'ink agent catalog' "$audit_cli" >/dev/null grep 'ink agent catalog' "$audit_cli" >/dev/null
grep 'frozen `tool delegate` subject' "$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 create_tool=$repo/skills/create-ink-tool/SKILL.md
grep 'stage/match/apply' "$create_tool" >/dev/null grep 'stage/match/apply' "$create_tool" >/dev/null
grep 'projection' "$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' printf 'ok\n'