agents: add safe cross-model delegation
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -62,6 +62,8 @@ gotools
|
||||
!/dotfiles/claude/settings.json
|
||||
!/dotfiles/claude/settings.local.json
|
||||
!/dotfiles/claude/settings.local.json.example
|
||||
!/dotfiles/claude/agents/
|
||||
!/dotfiles/claude/agents/codex-delegator.md
|
||||
# Expose the shared agent skills library to Claude Code, which only reads
|
||||
# ~/.claude/skills. This is a symlink to ../agents/skills (the canonical
|
||||
# store, also surfaced at ~/.agents/skills); without the allowlist the
|
||||
@@ -70,8 +72,10 @@ gotools
|
||||
|
||||
# Same story for Codex: ~/.codex resolves into dotfiles/codex on nix-darwin,
|
||||
# so the codex-history repo and live Codex state nest inside this worktree.
|
||||
# Allowlist only the HM-managed config.
|
||||
# Allowlist only HM-managed configuration and custom agents.
|
||||
/dotfiles/codex/*
|
||||
!/dotfiles/codex/AGENTS.md
|
||||
!/dotfiles/codex/agents/
|
||||
!/dotfiles/codex/agents/claude-delegator.toml
|
||||
!/dotfiles/codex/config.toml
|
||||
!/dotfiles/codex/skills
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
# Agentic Session Preferences
|
||||
|
||||
## Delegating coding work to subagents
|
||||
- Delegate all coding tasks to subagents (via the Agent tool) rather than editing code directly in the main session. The main session orchestrates: it plans, delegates, reviews, and integrates.
|
||||
- When the primary model is Fable, strongly prefer delegating coding tasks to subagents (via the Agent tool). Fable should usually act as the orchestrator: planning, delegating, reviewing, and integrating. It may still work directly when delegation would add disproportionate overhead or the task cannot be usefully separated.
|
||||
- For other primary models, subagent delegation is optional rather than required. Use judgment: delegate when work is meaningfully parallelizable, independently scoped, or benefits from a separate implementation/review pass; work directly when that is simpler and more efficient.
|
||||
- The primary agent remains responsible for reviewing and integrating delegated work.
|
||||
- Use judgement to select the model tier per task. Opus/medium is sufficient for simple, well-specified tasks. For harder tasks where architecture and design taste matter, prefer a stronger tier (e.g. fable).
|
||||
- Whenever you pick a model tier for an agent, record a one-line justification for that choice (in your reasoning/CoT or a brief note in the delegating message) so the decision is auditable. Tie the justification to what the agent must actually decide at execution time, not just the topic's importance — a task specified tightly enough that the taste is already discharged doesn't need the stronger tier. If you can't articulate why the cheaper tier is insufficient, default to it.
|
||||
- This applies to writing, editing, and refactoring code. Non-coding work (reading, searching, planning, running commands, answering questions) does not need to be delegated.
|
||||
- These guidelines apply to writing, editing, and refactoring code. Non-coding work (reading, searching, planning, running commands, answering questions) does not need to be delegated.
|
||||
|
||||
## Cross-model delegation
|
||||
- Use cross-model delegation only when the user requests it or model diversity or an independent check would be useful; it is never mandatory.
|
||||
- Codex should prefer the `claude_delegator` agent, and Claude should prefer the `codex-delegator` agent. Use `$cross-agent-delegation` or its `ask-claude` and `ask-codex` wrappers when direct invocation is simpler.
|
||||
- Permit at most one cross-model handoff and never recursively delegate. Keep the child read-only and advisory by default, with only one writer per worktree.
|
||||
- The parent agent owns review, verification, and integration of the child's output.
|
||||
|
||||
## Sharing dev-server / preview links
|
||||
- When sharing a local server or preview URL, always prefer this machine's Tailscale address over `127.0.0.1`/`localhost`/LAN IPs, so the link opens from any device on the tailnet.
|
||||
|
||||
18
dotfiles/agents/skills/cross-agent-delegation/SKILL.md
Normal file
18
dotfiles/agents/skills/cross-agent-delegation/SKILL.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: cross-agent-delegation
|
||||
description: Safely delegate between Codex and Claude through read-only CLI wrappers. Use when the user explicitly asks Codex or Claude to delegate to the other, or when a workflow explicitly calls for an independent cross-model second opinion.
|
||||
---
|
||||
|
||||
# Cross-Agent Delegation
|
||||
|
||||
Use a cheap native driver agent when available. Invoke the wrapper directly when that is simpler:
|
||||
|
||||
- Pipe a self-contained prompt to `ask-claude` or `ask-codex` on stdin.
|
||||
- Reserve `--base64 '<payload>'` for native driver agents as injection-safe transport. A driver must encode the complete task itself and never embed raw delegated task text in shell source.
|
||||
- Treat the returned plain text as advisory output; the wrappers are read-only and ephemeral.
|
||||
- Review and verify the output in the parent agent before using it.
|
||||
- Never include credentials, secrets, or unnecessary personal data in the prompt.
|
||||
- Permit at most one cross-model handoff. Never ask the child to invoke either CLI or delegate again.
|
||||
- Keep one writer per worktree. Have the parent make any resulting edits.
|
||||
|
||||
Environment variables documented in the scripts may override their conservative model, effort, timeout, turn, and budget defaults.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Cross-agent delegation"
|
||||
short_description: "Safely consult the other coding agent"
|
||||
default_prompt: "Use $cross-agent-delegation to get an independent cross-model second opinion."
|
||||
135
dotfiles/agents/skills/cross-agent-delegation/scripts/ask-claude
Executable file
135
dotfiles/agents/skills/cross-agent-delegation/scripts/ask-claude
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
input_mode=stdin
|
||||
base64_payload=
|
||||
case "$#" in
|
||||
0) ;;
|
||||
2)
|
||||
if [ "$1" != "--base64" ]; then
|
||||
echo "ask-claude: expected a prompt on stdin or --base64 <payload>" >&2
|
||||
exit 2
|
||||
fi
|
||||
input_mode=base64
|
||||
base64_payload=$2
|
||||
;;
|
||||
*)
|
||||
echo "ask-claude: expected a prompt on stdin or --base64 <payload>" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$input_mode" = base64 ]; then
|
||||
if [ -z "$base64_payload" ] || [[ ! "$base64_payload" =~ ^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$ ]]; then
|
||||
echo "ask-claude: invalid RFC4648 base64 payload or padding" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${CROSS_AGENT_DEPTH:-0}" != "0" ]; then
|
||||
echo "ask-claude: recursive cross-agent delegation is not allowed" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for required_command in claude timeout mktemp jq cat chmod rm; do
|
||||
if ! command -v "$required_command" >/dev/null 2>&1; then
|
||||
echo "ask-claude: required command not found: $required_command" >&2
|
||||
exit 127
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$input_mode" = base64 ] && ! command -v base64 >/dev/null 2>&1; then
|
||||
echo "ask-claude: required command not found: base64" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
export CROSS_AGENT_DEPTH=1
|
||||
|
||||
timeout_value=${CROSS_AGENT_TIMEOUT:-10m}
|
||||
model=${CROSS_AGENT_CLAUDE_MODEL:-haiku}
|
||||
effort=${CROSS_AGENT_CLAUDE_EFFORT:-low}
|
||||
max_turns=${CROSS_AGENT_CLAUDE_MAX_TURNS:-4}
|
||||
max_budget_usd=${CROSS_AGENT_CLAUDE_MAX_BUDGET_USD:-0.25}
|
||||
|
||||
umask 077
|
||||
tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/ask-claude.XXXXXX")
|
||||
cleanup() {
|
||||
rm -rf -- "$tmp_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 129' HUP
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
prompt_file="$tmp_dir/prompt"
|
||||
result_file="$tmp_dir/result.json"
|
||||
if [ "$input_mode" = base64 ]; then
|
||||
if base64 --decode </dev/null >/dev/null 2>&1; then
|
||||
base64_decode=(base64 --decode)
|
||||
elif base64 -D </dev/null >/dev/null 2>&1; then
|
||||
base64_decode=(base64 -D)
|
||||
else
|
||||
echo "ask-claude: base64 command supports neither --decode nor -D" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf '%s' "$base64_payload" | "${base64_decode[@]}" >"$prompt_file"; then
|
||||
echo "ask-claude: failed to decode base64 payload" >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
cat >"$prompt_file"
|
||||
fi
|
||||
chmod 600 "$prompt_file"
|
||||
|
||||
if [ ! -s "$prompt_file" ]; then
|
||||
echo "ask-claude: prompt on stdin must not be empty" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
guard_instruction='Act only as a read-only advisory child. Do not invoke Codex or Claude, directly or indirectly. Do not spawn, delegate to, or communicate with any other agent. Do not edit, create, delete, move, or rename files. Return only your analysis or recommendation to the parent.'
|
||||
|
||||
set +e
|
||||
timeout --foreground "$timeout_value" \
|
||||
claude -p \
|
||||
--model "$model" \
|
||||
--effort "$effort" \
|
||||
--max-turns "$max_turns" \
|
||||
--max-budget-usd "$max_budget_usd" \
|
||||
--output-format json \
|
||||
--permission-mode dontAsk \
|
||||
--tools Read,Grep,Glob \
|
||||
--allowedTools Read,Grep,Glob \
|
||||
--no-session-persistence \
|
||||
--no-chrome \
|
||||
--append-system-prompt "$guard_instruction" \
|
||||
<"$prompt_file" >"$result_file"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
if [ "$status" -eq 124 ]; then
|
||||
echo "ask-claude: timed out after $timeout_value" >&2
|
||||
else
|
||||
echo "ask-claude: claude exited with status $status" >&2
|
||||
fi
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
if ! jq -e . "$result_file" >/dev/null 2>&1; then
|
||||
echo "ask-claude: claude returned invalid JSON" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if jq -e '(.is_error? // false) == true' "$result_file" >/dev/null; then
|
||||
echo "ask-claude: claude reported an error" >&2
|
||||
jq -r '.result? // empty' "$result_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! jq -e '.result? | type == "string"' "$result_file" >/dev/null; then
|
||||
echo "ask-claude: claude response does not contain a string result" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jq -r '.result' "$result_file"
|
||||
123
dotfiles/agents/skills/cross-agent-delegation/scripts/ask-codex
Executable file
123
dotfiles/agents/skills/cross-agent-delegation/scripts/ask-codex
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
input_mode=stdin
|
||||
base64_payload=
|
||||
case "$#" in
|
||||
0) ;;
|
||||
2)
|
||||
if [ "$1" != "--base64" ]; then
|
||||
echo "ask-codex: expected a prompt on stdin or --base64 <payload>" >&2
|
||||
exit 2
|
||||
fi
|
||||
input_mode=base64
|
||||
base64_payload=$2
|
||||
;;
|
||||
*)
|
||||
echo "ask-codex: expected a prompt on stdin or --base64 <payload>" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$input_mode" = base64 ]; then
|
||||
if [ -z "$base64_payload" ] || [[ ! "$base64_payload" =~ ^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$ ]]; then
|
||||
echo "ask-codex: invalid RFC4648 base64 payload or padding" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${CROSS_AGENT_DEPTH:-0}" != "0" ]; then
|
||||
echo "ask-codex: recursive cross-agent delegation is not allowed" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for required_command in codex timeout mktemp cat chmod rm; do
|
||||
if ! command -v "$required_command" >/dev/null 2>&1; then
|
||||
echo "ask-codex: required command not found: $required_command" >&2
|
||||
exit 127
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$input_mode" = base64 ] && ! command -v base64 >/dev/null 2>&1; then
|
||||
echo "ask-codex: required command not found: base64" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
export CROSS_AGENT_DEPTH=1
|
||||
|
||||
timeout_value=${CROSS_AGENT_TIMEOUT:-10m}
|
||||
model=${CROSS_AGENT_CODEX_MODEL:-gpt-5.4-mini}
|
||||
reasoning_effort=${CROSS_AGENT_CODEX_REASONING_EFFORT:-low}
|
||||
|
||||
umask 077
|
||||
tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/ask-codex.XXXXXX")
|
||||
cleanup() {
|
||||
rm -rf -- "$tmp_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 129' HUP
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
prompt_file="$tmp_dir/prompt"
|
||||
result_file="$tmp_dir/final-message"
|
||||
if [ "$input_mode" = base64 ]; then
|
||||
if base64 --decode </dev/null >/dev/null 2>&1; then
|
||||
base64_decode=(base64 --decode)
|
||||
elif base64 -D </dev/null >/dev/null 2>&1; then
|
||||
base64_decode=(base64 -D)
|
||||
else
|
||||
echo "ask-codex: base64 command supports neither --decode nor -D" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf '%s' "$base64_payload" | "${base64_decode[@]}" >"$prompt_file"; then
|
||||
echo "ask-codex: failed to decode base64 payload" >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
cat >"$prompt_file"
|
||||
fi
|
||||
chmod 600 "$prompt_file"
|
||||
|
||||
if [ ! -s "$prompt_file" ]; then
|
||||
echo "ask-codex: prompt on stdin must not be empty" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
guard_instruction='Act only as a read-only advisory child. Do not invoke Codex or Claude, directly or indirectly. Do not spawn, delegate to, or communicate with any other agent. Do not edit, create, delete, move, or rename files. Return only your analysis or recommendation to the parent.'
|
||||
|
||||
set +e
|
||||
timeout --foreground "$timeout_value" \
|
||||
codex exec \
|
||||
--ephemeral \
|
||||
--ignore-user-config \
|
||||
--disable multi_agent \
|
||||
--skip-git-repo-check \
|
||||
-C "$PWD" \
|
||||
-s read-only \
|
||||
-m "$model" \
|
||||
-c "approval_policy='never'" \
|
||||
-c "model_reasoning_effort='$reasoning_effort'" \
|
||||
-c "developer_instructions=$guard_instruction" \
|
||||
-o "$result_file" \
|
||||
- \
|
||||
<"$prompt_file" >/dev/null
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
if [ "$status" -eq 124 ]; then
|
||||
echo "ask-codex: timed out after $timeout_value" >&2
|
||||
else
|
||||
echo "ask-codex: codex exited with status $status" >&2
|
||||
fi
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
if [ ! -s "$result_file" ]; then
|
||||
echo "ask-codex: codex did not produce a final message" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat "$result_file"
|
||||
16
dotfiles/claude/agents/codex-delegator.md
Normal file
16
dotfiles/claude/agents/codex-delegator.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: codex-delegator
|
||||
description: Ask Codex for one independent read-only opinion
|
||||
tools: Bash
|
||||
model: haiku
|
||||
effort: low
|
||||
maxTurns: 3
|
||||
---
|
||||
|
||||
UTF-8 base64-encode the complete task yourself without using a tool. Verify that the payload contains only `A-Z`, `a-z`, `0-9`, `+`, `/`, and valid terminal `=` padding. Then make exactly one Bash tool call:
|
||||
|
||||
```bash
|
||||
ask-codex --base64 '<payload>'
|
||||
```
|
||||
|
||||
Never place raw task text in shell source. Return the command's stdout verbatim. Do not investigate, reason about the task, edit files, call any other command, or call or delegate to any other agent.
|
||||
7
dotfiles/codex/agents/claude-delegator.toml
Normal file
7
dotfiles/codex/agents/claude-delegator.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
name = "claude_delegator"
|
||||
description = "Ask Claude for one independent read-only opinion"
|
||||
model = "gpt-5.4-mini"
|
||||
model_reasoning_effort = "low"
|
||||
developer_instructions = """
|
||||
UTF-8 base64-encode the complete task yourself without using a tool. Verify that the payload contains only `A-Z`, `a-z`, `0-9`, `+`, `/`, and valid terminal `=` padding. Then make exactly one shell tool call: `ask-claude --base64 '<payload>'`. Never place raw task text in shell source. Return the command's stdout verbatim. Do not investigate or reason about the task yourself, edit files, run any other command, or call, spawn, communicate with, or delegate to any other agent.
|
||||
"""
|
||||
11
dotfiles/lib/bin/ask-claude
Executable file
11
dotfiles/lib/bin/ask-claude
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
target=${AGENTS_HOME:-$HOME/.agents}/skills/cross-agent-delegation/scripts/ask-claude
|
||||
|
||||
if [ ! -x "$target" ]; then
|
||||
echo "ask-claude: canonical script is not executable: $target" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
exec "$target" "$@"
|
||||
11
dotfiles/lib/bin/ask-codex
Executable file
11
dotfiles/lib/bin/ask-codex
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
target=${AGENTS_HOME:-$HOME/.agents}/skills/cross-agent-delegation/scripts/ask-codex
|
||||
|
||||
if [ ! -x "$target" ]; then
|
||||
echo "ask-codex: canonical script is not executable: $target" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
exec "$target" "$@"
|
||||
@@ -69,6 +69,11 @@ in {
|
||||
force = true;
|
||||
source = oos "${cfg.worktreeCodexDir}/AGENTS.md";
|
||||
};
|
||||
|
||||
".codex/agents/claude-delegator.toml" = {
|
||||
force = true;
|
||||
source = oos "${cfg.worktreeCodexDir}/agents/claude-delegator.toml";
|
||||
};
|
||||
};
|
||||
|
||||
home.activation.prepareCodexDirectory = lib.hm.dag.entryBefore ["checkLinkTargets"] ''
|
||||
|
||||
Reference in New Issue
Block a user