Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
run: bash skills/delegate-kit/tests/caps.sh
- name: Routing policy
run: bash skills/delegate-kit/tests/route.sh
- name: Safety gate
run: bash skills/delegate-kit/tests/gate.sh
- name: SKILL.md stays a policy, not a manual
run: |
size=$(wc -c < skills/delegate-kit/SKILL.md)
Expand Down
12 changes: 12 additions & 0 deletions skills/delegate-kit/hooks/gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ INPUT=$(cat)
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
[ -z "$CMD" ] && exit 0

# Delegation depth is 1. External workers get DELEGATE_KIT_DEPTH from agent-run; a native
# subagent gets no such variable but its hook input carries agent_type (Claude Code sets
# agent_id/agent_type on tool events fired inside a subagent). Starting a worker or taking
# a worktree lock from there is denied outright — no confirmation prefix reopens it, so this
# check sits before the confirmation bypass below.
AGENT=$(printf '%s' "$INPUT" | jq -r '.agent_type // empty' 2>/dev/null || true)
if [ -n "$AGENT" ] && printf '%s' "$CMD" | grep -Eq '(^|[;&|(`[:space:]/"'"'"'])(agent-run["'"'"']?[[:space:]]+(run|resume)|agent-wt["'"'"']?[[:space:]]+lock)([[:space:]]|$)'; then
msg="delegate-kit gate: delegation depth is 1 — a worker ($AGENT) does not start workers or take worktree locks. Return what you have; the coordinator dispatches."
jq -cn --arg m "$msg" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$m}}'
exit 0
fi

# explicit confirmation prefix (visible in the approval UI / transcript)
if printf '%s' "$CMD" | grep -Eq '^[[:space:]]*DELEGATE_KIT_CONFIRMED=1[[:space:]]'; then exit 0; fi
[ "${DELEGATE_KIT_CONFIRMED:-}" = "1" ] && exit 0
Expand Down
2 changes: 1 addition & 1 deletion skills/delegate-kit/references/external.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Per-role defaults for one user, in the same file — the shipped table in `scrip

## Limits and safety

- **Delegation depth is 1.** `agent-run` disables subagents on both CLIs; the shipped `dk-*` definitions carry no `Agent` tool.
- **Delegation depth is 1.** `agent-run` disables subagents on both CLIs; the shipped `dk-*` definitions carry no `Agent` tool; and `hooks/gate.sh` denies `agent-run run|resume` and `agent-wt lock` from inside a Claude Code subagent, which it recognises by the `agent_type` field of the hook input. Under a Codex parent the native roles hold that line by their instructions only.
- **Writer cap 3, ceiling 8; workers = writers + 3**, so a `panel` or `led` review fits beside a full set of writers. `agent-run run` counts external writers machine-wide plus the native locks (`agent-wt lock`) of the repository it writes into; `agent-wt lock` counts the locked worktrees of its repository. The cap is raised per task: the coordinator states the **partition** — one ticket per writer, disjoint write scopes — the user says yes, and `--max-writers N` on the run or the lock carries it (`DELEGATE_KIT_MAX_WRITERS` for the session; `DELEGATE_KIT_MAX_WORKERS` overrides the total, floored at writers + 1). The ceiling holds against every override; past it the work goes in waves. A run refused by a cap or a locked worktree fails before anything is spawned, `--detach` included: the parent prints the reason. A refusal that lands inside the supervisor (a race) is recorded as `failed`, so `status` shows it. Counting and taking a slot happen under one mutex, so concurrent starts respect the cap too. N sessions on one subscription hit the rate limit together; `--fallback none` keeps a fleet from all retrying on the other family at once.
- **Writers** run in a worktree under the backend's own sandbox (`workspace-write` / `acceptEdits`); the dangerous modes are outside this skill. `agent-run` refuses a worktree locked for a native writer, and the reverse.
- **Read-only roles** run under `codex -s read-only` / `claude --permission-mode plan` — the enforced boundary a native role lacks. When it matters (an untrusted diff, a risk zone), dispatch that role externally even inside the family.
Expand Down
2 changes: 1 addition & 1 deletion skills/delegate-kit/references/hosts.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Serial is a dispatch mode, not a failure: one long-lived worker resumed with eac

Choose external even inside the family when one matters:

- **An enforced sandbox.** External read-only roles run under `claude --permission-mode plan` / `codex -s read-only`. A native role is read-only by instruction and tool list — fine for a reviewer you dispatched yourself, thin as the boundary around an untrusted diff.
- **An enforced sandbox.** External read-only roles run under `claude --permission-mode plan` / `codex -s read-only`. A native role is read-only by instruction and tool list — fine for a reviewer you dispatched yourself, thin as the boundary around an untrusted diff. What the gate does enforce for a native role: no `agent-run run|resume`, no `agent-wt lock` (`external.md`).
- **The strict result schema, ledger, run id, timeout, quota fallback.** All live in `agent-run`.
- **The write-lock for free.** `agent-run --cwd` takes it; a native writer needs `agent-wt lock` before and `agent-wt release` after — the lock is also what the writer cap counts (`external.md`).

Expand Down
55 changes: 55 additions & 0 deletions skills/delegate-kit/tests/gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/bash
# Стенд hooks/gate.sh: что просит подтверждения, что пропускает, и правило
# глубины делегирования — субагент (agent_type во входе хука) не запускает
# воркеров и не берёт lock на worktree, префикс подтверждения этого не открывает.
#
# ./gate.sh
set -u
GATE="$(cd "$(dirname "$0")/../hooks" && pwd)/gate.sh"
LAST="${TMPDIR:-/tmp}/dk-gate-test.$$.json"; trap 'rm -f "$LAST"' EXIT
PASS=0; FAIL=0
ok(){ if [ "$2" = "$3" ]; then echo " ✔ $1"; PASS=$((PASS+1)); else echo " ✘ $1: ожидалось [$3], получено [$2]"; FAIL=$((FAIL+1)); fi; }
# gate HARNESS AGENT_TYPE COMMAND → decision (allow | ask | deny); сырой ответ в $LAST для reason
gate(){
jq -cn --arg c "$3" --arg a "$2" '{hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:$c}} + (if $a == "" then {} else {agent_type:$a} end)' \
| DELEGATE_KIT_CONFIRMED= bash "$GATE" --harness "$1" > "$LAST"
[ -s "$LAST" ] || echo '{}' > "$LAST"
jq -r '.hookSpecificOutput.permissionDecision // "allow"' "$LAST"
}
reason(){ jq -r '.hookSpecificOutput.permissionDecisionReason // ""' "$LAST"; }

echo "── опасные команды: ask на Claude, deny с инструкцией на Codex"
ok "rm -rf → ask" "$(gate claude "" 'rm -rf build')" "ask"
ok "причина названа" "$(reason | grep -c 'rm -rf')" "1"
ok "sudo → ask" "$(gate claude "" 'sudo systemctl restart nginx')" "ask"
ok "git push --force → ask" "$(gate claude "" 'git push --force origin main')" "ask"
ok "DROP TABLE → ask" "$(gate claude "" 'psql -c "DROP TABLE users"')" "ask"
ok "codex: deny с префиксом подтверждения" "$(gate codex "" 'rm -rf build')" "deny"
ok "codex: инструкция про DELEGATE_KIT_CONFIRMED" "$(reason | grep -c 'DELEGATE_KIT_CONFIRMED=1')" "1"
ok "префикс подтверждения пропускает" "$(gate codex "" 'DELEGATE_KIT_CONFIRMED=1 rm -rf build')" "allow"

echo "── обычные команды проходят"
ok "ls" "$(gate claude "" 'ls -la')" "allow"
ok "git push без force" "$(gate claude "" 'git push origin feature')" "allow"
ok "rm одного файла" "$(gate claude "" 'rm build/out.txt')" "allow"
ok "не Bash-вход (нет command)" "$(printf '{"tool_name":"Read","tool_input":{"file_path":"x"}}' | bash "$GATE" --harness claude; echo "exit $?")" "exit 0"

echo "── глубина делегирования: субагент не запускает воркеров"
ok "координатор: agent-run run проходит" "$(gate claude "" 'agent-run run --role reviewer --backend codex --brief b.md')" "allow"
ok "субагент: agent-run run → deny" "$(gate claude "dk-implementer" 'agent-run run --role reviewer --backend codex --brief b.md')" "deny"
ok "причина — depth 1 с именем агента" "$(reason | grep -c 'depth is 1.*dk-implementer')" "1"
ok "субагент: agent-run resume → deny" "$(gate claude "dk-planner" 'agent-run resume 2026-x --brief n.md')" "deny"
ok "субагент: agent-wt lock → deny" "$(gate claude "dk-implementer" 'agent-wt lock slice-a')" "deny"
ok "субагент: путь к скрипту тоже" "$(gate claude "dk-implementer" '~/.claude/skills/delegate-kit/scripts/agent-run run --role planner --brief b.md')" "deny"
ok "субагент: исполняемый файл в кавычках" "$(gate claude "dk-implementer" '"agent-run" run --role planner --brief b.md')" "deny"
ok "субагент: путь в кавычках" "$(gate claude "dk-implementer" "'/home/me/bin/agent-wt' lock task")" "deny"
ok "субагент: через env/node" "$(gate claude "dk-implementer" 'env DELEGATE_KIT_PRESET=auto node /x/agent-run run --role planner --brief b.md')" "deny"
ok "субагент: в цепочке команд" "$(gate claude "dk-implementer" 'cd /repo && agent-run run --role planner --brief b.md')" "deny"
ok "субагент: префикс подтверждения не открывает" "$(gate claude "dk-implementer" 'DELEGATE_KIT_CONFIRMED=1 agent-run run --role planner --brief b.md')" "deny"
ok "субагент на Codex тоже deny" "$(gate codex "dk-reviewer" 'agent-run run --role verifier --brief b.md')" "deny"
ok "субагент: status/list/wait проходят" "$(gate claude "dk-reviewer" 'agent-run status 2026-x; agent-run list; agent-wt status s')" "allow"
ok "субагент: agent-wt diff проходит" "$(gate claude "dk-reviewer" 'agent-wt diff slice-a > review.diff')" "allow"
ok "субагент: обычные команды проходят" "$(gate claude "dk-implementer" 'npm test')" "allow"

echo; echo "Пройдено: $PASS, провалено: $FAIL"
exit $((FAIL > 0))
Loading