feat: per-role transport (access: harness|api) — planner via direct API - #2
Conversation
Add a per-role `access` transport ("harness" default, or "api") so a role can be
routed over a direct provider API call — billed to that provider's own key, off
the interactive subscription — while the other role stays a normal harness
subagent. Expressed in roles.config.json so every adapter can honor it.
Config/schema:
- roles.<role>.access: harness|api; providers.<p>.apiKeyFile (path, not secret);
top-level classIds map for alias -> concrete model id resolution.
- config stays version 1, additive only; planner remains access=harness here.
apply.py:
- validate() returns (errors, warnings); warns when an api role can't resolve a
concrete model id. New resolve_api_model(); role_view gains access/api_model/
api_key_file. New `resolve` (env|json role facts) and `prompt` (rendered role
charter) subcommands. `set --access harness|api`. print_table/generic show
transport. install_claude() renders a subagent only for harness roles, removes a
stale generated one for api roles (leaves hand-written files), and two-pass
renders /pb and /pbg from per-transport dispatch fragments.
Wrapper + provisioning (committed, not generated):
- bin/role-call: run an api-transport role headless via the claude CLI; key
exported only into the child process, never argv/logs; prints result to stdout
and role/model/cost/session to stderr; --task-file/-/--ping/--dry-run/--force.
- bin/set-api-key: interactive, terminal-only key intake to a 0600 keyfile; never
echoes the secret; optional --verify makes one tiny real call.
Templates/docs:
- dispatch/{planner,builder}-{harness,api}.md fragments; pb/pbg use them; pbg-*
switch commands document transport. PRIMITIVE.md "Role transports" section +
flip runbook; README config/quickstart/layout/harness-table updated.
Regenerating the real ~/.claude from the (still harness) config leaves the
planner/builder subagents byte-identical; /pb and /pbg differ only cosmetically
(line-wrap) or additively (transport docs), so nothing breaks before the flip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis change adds per-role ChangesPer-role transport support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant apply_py
participant Claude_adapter
participant role_call
participant Provider_API
User->>apply_py: set role --access api
apply_py->>Claude_adapter: regenerate transport-specific commands
Claude_adapter->>role_call: dispatch planner or builder task
role_call->>Provider_API: submit role charter and task with API key
Provider_API-->>role_call: return result and usage
role_call-->>User: print result and billing information
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@adapters/claude-code/dispatch/planner-api.md`:
- Line 1: Remove the positional {{BUILDER_MODEL}} argument from the planner
command in adapters/claude-code/dispatch/planner-api.md:1-1 and from the
corresponding builder command in
adapters/claude-code/dispatch/builder-api.md:1-1 so transport fallback does not
rewrite model configuration. Update both remedy commands to use only set <role>
--access harness, preserving the existing planner and builder roles
respectively.
In `@adapters/claude-code/pbg-builder.md.tmpl`:
- Around line 29-34: The transport commands in
adapters/claude-code/pbg-builder.md.tmpl lines 29-34 and
adapters/claude-code/pbg-planner.md.tmpl lines 29-34 are hardcoded to one
installation path. Replace /home/dyadmin/dev-primitive in both templates with
the quoted {{DEV_PRIMITIVE_DIR}} placeholder while preserving the existing set
builder/planner commands and access behavior.
In `@adapters/claude-code/pbg.md.tmpl`:
- Line 35: Update the Step 0 planner instruction in the pbg template so it
delegates through {{PLANNER_REF}} rather than directly to planner, ensuring the
no-until flow remains transport-aware when access=api removes the planner
subagent file.
In `@bin/role-call`:
- Around line 155-157: Update the child invocation in the role-call command to
clear ANTHROPIC_AUTH_TOKEN and CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX,
and CLAUDE_CODE_USE_FOUNDRY before exporting API_KEY_ENV and launching CMD.
Preserve the existing stderr capture and argument handling while ensuring the
child selects the exported API key.
- Around line 29-30: Update the read-only role configuration in bin/role-call so
it uses the read-only/plan permission mode and removes the destructive Bash
path, including Bash(find:*), from READONLY_ALLOWED_TOOLS. Keep only the
narrowly scoped inspection commands needed by read-only roles.
- Around line 187-193: The auth validation flow must not infer billing source
from role-call’s informational total_cost_usd field: update bin/role-call lines
187-193 to report cost without treating its presence or absence as
authentication evidence. Update bin/set-api-key lines 89-107 so validation does
not hardcode role-call planner --ping; exercise the configured provider or
otherwise validate each relevant $PROVIDER value while preserving the existing
API-key setup behavior. Use the existing role-call and provider-selection
symbols to keep both paths consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a494a8f0-0780-451a-a538-32cab4e038ef
📒 Files selected for processing (15)
PRIMITIVE.mdREADME.mdadapters/claude-code/dispatch/builder-api.mdadapters/claude-code/dispatch/builder-harness.mdadapters/claude-code/dispatch/planner-api.mdadapters/claude-code/dispatch/planner-harness.mdadapters/claude-code/pb.md.tmpladapters/claude-code/pbg-builder.md.tmpladapters/claude-code/pbg-planner.md.tmpladapters/claude-code/pbg.md.tmplapply.pybin/role-callbin/set-api-keyroles.config.jsonroles.schema.json
| @@ -0,0 +1 @@ | |||
| Run the planner over the direct API (billed to the configured Anthropic key, not this session's subscription). Write the task plus any relevant context to a temp file, then run via Bash: `{{DEV_PRIMITIVE_DIR}}/bin/role-call planner --task-file <that file>`. Planner turns can run many minutes — run it in the background and wait for completion, or use the maximum Bash timeout (600000 ms). Its stdout is the plan; the stderr `[role-call]` line reports the run's API cost — include that figure in your final report. If it exits non-zero, surface its error message to the user verbatim and offer the remedies it names (provision the key with `{{DEV_PRIMITIVE_DIR}}/bin/set-api-key`, or temporarily re-route planning with `python3 {{DEV_PRIMITIVE_DIR}}/apply.py set planner {{BUILDER_MODEL}} --access harness`). Do not silently do the planning yourself. | |||
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Transport fallback should not rewrite model configuration.
adapters/claude-code/dispatch/planner-api.md#L1-L1: remove the positional{{BUILDER_MODEL}}; it replaces the planner model.adapters/claude-code/dispatch/builder-api.md#L1-L1: remove the positional{{BUILDER_MODEL}}; it can convert a pin into an unpinned class.
Both remedies should run only set <role> --access harness.
📍 Affects 2 files
adapters/claude-code/dispatch/planner-api.md#L1-L1(this comment)adapters/claude-code/dispatch/builder-api.md#L1-L1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@adapters/claude-code/dispatch/planner-api.md` at line 1, Remove the
positional {{BUILDER_MODEL}} argument from the planner command in
adapters/claude-code/dispatch/planner-api.md:1-1 and from the corresponding
builder command in adapters/claude-code/dispatch/builder-api.md:1-1 so transport
fallback does not rewrite model configuration. Update both remedy commands to
use only set <role> --access harness, preserving the existing planner and
builder roles respectively.
| **Transport.** The builder's current transport is `{{BUILDER_ACCESS}}`. You can also | ||
| switch *how* the role is reached, not just which model: | ||
| `python3 /home/dyadmin/dev-primitive/apply.py set builder --access api|harness`. | ||
| Switching to `api` runs the builder as a direct API call (its own key, off the | ||
| subscription) and **removes the builder subagent file**; switching back to `harness` | ||
| recreates it. See PRIMITIVE.md "Role transports (`access`)". |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
The generated transport commands are tied to one machine.
adapters/claude-code/pbg-builder.md.tmpl#L29-L34: replace/home/dyadmin/dev-primitivewith quoted{{DEV_PRIMITIVE_DIR}}.adapters/claude-code/pbg-planner.md.tmpl#L29-L34: make the same installation-aware substitution.
📍 Affects 2 files
adapters/claude-code/pbg-builder.md.tmpl#L29-L34(this comment)adapters/claude-code/pbg-planner.md.tmpl#L29-L34
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@adapters/claude-code/pbg-builder.md.tmpl` around lines 29 - 34, The transport
commands in adapters/claude-code/pbg-builder.md.tmpl lines 29-34 and
adapters/claude-code/pbg-planner.md.tmpl lines 29-34 are hardcoded to one
installation path. Replace /home/dyadmin/dev-primitive in both templates with
the quoted {{DEV_PRIMITIVE_DIR}} placeholder while preserving the existing set
builder/planner commands and access behavior.
| not vague. | ||
|
|
||
| 1. **Plan ({{PLANNER_MODEL}}).** Delegate to the `planner` subagent with the task, | ||
| 1. **Plan ({{PLANNER_MODEL}}).** Delegate to {{PLANNER_REF}} with the task, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Make the Step 0 planner call transport-aware too.
Line 28 still says to delegate directly to planner. With access=api, that subagent file is removed. Use {{PLANNER_REF}} there as well so /pbg without until: can derive acceptance criteria.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@adapters/claude-code/pbg.md.tmpl` at line 35, Update the Step 0 planner
instruction in the pbg template so it delegates through {{PLANNER_REF}} rather
than directly to planner, ensuring the no-until flow remains transport-aware
when access=api removes the planner subagent file.
| # Read-only tool allowlist for read-only roles (e.g. planner): inspection only, no mutation. | ||
| READONLY_ALLOWED_TOOLS='Read,Grep,Glob,WebFetch,WebSearch,TodoWrite,Bash(git log:*),Bash(git diff:*),Bash(git show:*),Bash(git status:*),Bash(ls:*),Bash(cat:*),Bash(head:*),Bash(tail:*),Bash(rg:*),Bash(grep:*),Bash(find:*),Bash(wc:*)' |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
claude --help | grep -E -- '--tools|--allowedTools|--permission-mode' || trueRepository: dyuhaus/dev-primitive
Length of output: 199
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files bin/role-call
wc -l bin/role-call
cat -n bin/role-call | sed -n '1,220p'
printf '\n--- references ---\n'
rg -n --hidden --no-messages -- '--allowedTools|--tools|--permission-mode|READONLY_ALLOWED_TOOLS|role-call' .Repository: dyuhaus/dev-primitive
Length of output: 16296
🌐 Web query:
Anthropic Claude Code CLI --allowedTools --tools --permission-mode documentation semantics auto-approve restrict tool set read-only plan mode
💡 Result:
Claude Code CLI offers robust mechanisms to manage agent permissions, tool access, and operational modes, enabling users to balance automation with security [1][2][3]. Permission Modes Permission modes control how Claude Code approves tool calls [1][3]. You can set the mode via the --permission-mode flag or in your settings files [1][3]. - default: Enables read-only exploration; interactive approval for edits and sensitive actions [1][3]. - acceptEdits: Allows reads, file edits, and common filesystem commands (e.g., mkdir, touch, mv) without prompting [2][3]. - plan: Allows only read-only operations to explore and propose changes; prevents actual file edits [1][3]. - auto: Uses background safety checks to auto-approve tool calls that align with your request [1][3]. - dontAsk: Denies all tools unless explicitly pre-approved via permissions rules (useful for CI/CD) [1][3]. - bypassPermissions: Skips all permission prompts (except for explicit security circuit breakers like rm -rf /), effectively auto-approving all tool calls [1][4][3]. Tool Control Flags - --allowedTools: Accepts a comma-separated list of tools to auto-approve for the session [2]. It corresponds to permissions.allow settings [5][6]. Note that in bypassPermissions mode, allowedTools does not restrict the agent from using other tools; it only pre-approves the ones listed [6]. - --disallowedTools: Removes tools from the agent's context entirely, preventing them from being used [5][6]. This is the most effective way to block specific tools even in permissive modes [6]. Semantics and Best Practices - Permissions are enforced by the CLI, not the model [1][7]. - To lock down an agent, pair --allowedTools with --permission-mode dontAsk [6][2]. - If you require autonomous execution without prompts but want to restrict tools, avoid bypassPermissions. Instead, use a combination of default or dontAsk modes, or explicitly define allowed/disallowed lists [4][6]. - You can manage permission rules interactively during a session using the /permissions command [1][8]. Documentation for these features is centralized at code.claude.com [1][9][8].
Citations:
- 1: https://code.claude.com/docs/en/permissions
- 2: https://code.claude.com/docs/en/headless
- 3: https://code.claude.com/docs/en/permission-modes?_rsc=4Vrbm42DZc3Y7r9j
- 4: Is --allowedTools with --permission-mode bypassPermissions behavior expected? anthropics/claude-code#12232
- 5: [DOCS] Explicitly link
--allowedToolsCLI flags topermissionssettings anthropics/claude-code#18973 - 6: https://code.claude.com/docs/en/agent-sdk/permissions
- 7: https://code.claude.com/docs/en/permissions.md
- 8: https://code.claude.com/docs/en/commands
- 9: https://code.claude.com/docs/en/cli
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n --hidden --no-messages -- '--allowedTools|--tools|--permission-mode|read-only|plan mode|readonly' README.md PRIMITIVE.md adapters bin apply.pyRepository: dyuhaus/dev-primitive
Length of output: 967
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n --hidden --no-messages -- 'READ_ONLY|read_only|readonly|access:' roles.config.json apply.py README.md PRIMITIVE.md bin/role-call adaptersRepository: dyuhaus/dev-primitive
Length of output: 1461
Tighten the read-only role permissions
--allowedTools only pre-approves calls; it does not make this role read-only. Bash(find:*) still leaves a destructive shell path open, so read-only roles should use a read-only/plan permission mode and a narrower allowlist.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/role-call` around lines 29 - 30, Update the read-only role configuration
in bin/role-call so it uses the read-only/plan permission mode and removes the
destructive Bash path, including Bash(find:*), from READONLY_ALLOWED_TOOLS. Keep
only the narrowly scoped inspection commands needed by read-only roles.
| # --- invoke: key exported only into the child; never in argv -------------------- | ||
| ERRFILE="$(mktemp)" | ||
| OUT="$( export "$API_KEY_ENV=$KEY"; "${CMD[@]}" 2>"$ERRFILE" )" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
env | sed 's/=.*//' |
grep -E '^(CLAUDE_CODE_USE_(BEDROCK|VERTEX|FOUNDRY)|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_API_KEY)$' || true
claude auth status --text || trueRepository: dyuhaus/dev-primitive
Length of output: 199
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the script and inspect the relevant section.
git ls-files | rg '^bin/role-call$'
wc -l bin/role-call
sed -n '130,190p' bin/role-call
# Search for credential handling around the wrapper.
rg -n 'ANTHROPIC_AUTH_TOKEN|ANTHROPIC_API_KEY|CLAUDE_CODE_USE_(BEDROCK|VERTEX|FOUNDRY)|export "$API_KEY_ENV|env -i|unset ' bin/role-callRepository: dyuhaus/dev-primitive
Length of output: 2967
🏁 Script executed:
#!/bin/bash
sed -n '1,130p' bin/role-call
rg -n 'CLAUDE_CODE_USE_(BEDROCK|VERTEX|FOUNDRY)|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_API_KEY|env -i|unset ' bin/role-callRepository: dyuhaus/dev-primitive
Length of output: 5779
🌐 Web query:
Claude Code authentication precedence ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY CLAUDE_CODE_USE_BEDROCK CLAUDE_CODE_USE_VERTEX CLAUDE_CODE_USE_FOUNDRY
💡 Result:
Claude Code determines authentication using a strict order of precedence. When multiple authentication methods are present, the CLI selects the first valid method found in the following order, from highest to lowest priority: [1][2] 1. Cloud Provider Credentials: If any of the environment variables CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY are set to 1, the CLI uses the corresponding cloud provider's authentication mechanism [1][3][4]. Only one of these can be active at a time [4]. 2. ANTHROPIC_AUTH_TOKEN: This environment variable is checked next [1][2]. It is used for routing requests through an LLM gateway or proxy and is sent as an Authorization: Bearer header [1][5]. 3. ANTHROPIC_API_KEY: This environment variable is checked third [1][2]. It is used for direct access to the Anthropic API and is sent as an X-Api-Key header [1][6]. In interactive mode, this key requires a one-time approval, which is then remembered; in non-interactive mode (-p), it is used automatically if present [1][5]. 4. apiKeyHelper: If defined, this mechanism runs a script to dynamically retrieve credentials (often used for rotating keys or vault-based management) [5][2]. 5. OAuth/Subscription Credentials: If no higher-precedence methods are configured, Claude Code defaults to using stored OAuth credentials from your Claude subscription or Console login (initialized via the claude auth login command) [1][2][7]. Important notes on precedence: - Precedence shadowing: If you have an active Claude subscription but also have ANTHROPIC_API_KEY set in your environment, the API key takes precedence once approved [1][2]. This can cause authentication failures if the key belongs to an expired or disabled organization; in such cases, running unset ANTHROPIC_API_KEY allows the CLI to fall back to your subscription [1][2]. - Status checking: You can verify which authentication method is currently active by running the command claude auth status [1][2][8]. - Configuration: You can manage settings, including custom API keys, via the /config command within an interactive session [1]. Settings can also be defined in configuration files (e.g., ~/.claude/settings.json) [5][9].
Citations:
- 1: https://code.claude.com/docs/en/authentication
- 2: https://recombobulate.dev/tips/use-authentication-precedence-to-debug-login-issues
- 3: https://github.com/anilatambharii/claude-auth-setup/blob/main/CONFIGURATION_EXAMPLES.md
- 4: https://github.com/anthropics/claude-code-action/blob/6cad158a/base-action/src/validate-env.ts
- 5: https://code.claude.com/docs/en/llm-gateway-connect
- 6: https://www.requesty.ai/blog/claude-code-environment-variables-anthropic-base-url-auth-token
- 7: https://code.claude.com/docs/en/quickstart?_rsc=1tg4o
- 8: https://code.claude.com/docs/en/cli
- 9: https://code.claude.com/docs/en/headless
🏁 Script executed:
rg -n 'ANTHROPIC_AUTH_TOKEN|CLAUDE_CODE_USE_BEDROCK|CLAUDE_CODE_USE_VERTEX|CLAUDE_CODE_USE_FOUNDRY|ANTHROPIC_API_KEY' .Repository: dyuhaus/dev-primitive
Length of output: 222
Clear inherited auth vars before launching claude.
ANTHROPIC_AUTH_TOKEN and CLAUDE_CODE_USE_{BEDROCK,VERTEX,FOUNDRY} outrank the exported ANTHROPIC_API_KEY, so the child can still pick the wrong credential source. Unset the conflicting vars (or run the child under env -u ...) here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/role-call` around lines 155 - 157, Update the child invocation in the
role-call command to clear ANTHROPIC_AUTH_TOKEN and CLAUDE_CODE_USE_BEDROCK,
CLAUDE_CODE_USE_VERTEX, and CLAUDE_CODE_USE_FOUNDRY before exporting API_KEY_ENV
and launching CMD. Preserve the existing stderr capture and argument handling
while ensuring the child selects the exported API key.
Derive a direct-API engine from the provider `type` so an api-transport role
can run over any OpenAI-compatible or local endpoint, not just Anthropic:
- anthropic -> `claude-cli` (headless, agentic `claude -p`, unchanged/verbatim)
- openai|local -> `chat` (single-shot POST {baseUrl}/chat/completions, Bearer
key exported only into the python child, never in argv)
- google -> no direct engine; role-call and validate point to Gemini's
OpenAI-compatible endpoint as a type:openai provider
apply.py emits ENGINE/PROVIDER/BASE_URL(_ENV) facts (append-only), defaults the
keyfile to ~/appdata/<provider>/api-key, adds provider-scoped classIds
("<provider>:<class>") lookup, api base-url/engine warnings, and a chat
no-tools context note in the /pb /pbg dispatch fragments. set-api-key defaults
the keyfile, keys its shape check by provider/type, and verifies against the
first api role's engine. Active roles stay anthropic/harness; regenerating
~/.claude is a no-op.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
bin/role-call (1)
85-90: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winPlace fallible assignments inside conditionals under
set -e.Both manual error paths are currently unreachable when their commands fail.
bin/role-call#L85-L90: assignFACTSwithin theifcondition so resolver diagnostics are printed and cleaned up.bin/role-call#L178-L187: assignOUTwithin anif/elseso the original exit status is captured andERRFILEis processed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/role-call` around lines 85 - 90, The fallible command substitutions in bin/role-call at lines 85-90 and 178-187 must be placed inside conditional contexts so set -e does not bypass their manual error handling. Update the FACTS assignment to run within an if condition while preserving resolver diagnostics cleanup, and restructure the OUT assignment within an if/else so the original exit status is captured and ERRFILE processing remains reachable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@bin/role-call`:
- Around line 285-293: Update the request flow around urllib.request.urlopen so
redirects are rejected before the request carrying the Authorization header can
be resent. Add a redirect-handling mechanism for the existing Request that
raises or otherwise aborts on 30x responses, while preserving the current
timeout and POST behavior.
- Around line 177-179: Update the child-environment setup in the role-call
invocation around ERRFILE, OUT, and CMD so it exports
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 alongside the API key before launching
claude, ensuring subprocesses, hooks, and MCP servers cannot inherit the key.
- Line 150: Update the command construction in bin/role-call so the combined
prompt is provided to claude through stdin rather than passed via the -p
argument. Preserve the existing timeout, model, and JSON output options while
removing the prompt from argv to avoid process-list exposure and argument-length
limits.
In `@PRIMITIVE.md`:
- Line 138: Remove the leading space from the inline code span in PRIMITIVE.md,
changing the documented cost suffix from ` cost=$<usage.cost>` to
`cost=$<usage.cost>` while preserving the surrounding wording.
---
Outside diff comments:
In `@bin/role-call`:
- Around line 85-90: The fallible command substitutions in bin/role-call at
lines 85-90 and 178-187 must be placed inside conditional contexts so set -e
does not bypass their manual error handling. Update the FACTS assignment to run
within an if condition while preserving resolver diagnostics cleanup, and
restructure the OUT assignment within an if/else so the original exit status is
captured and ERRFILE processing remains reachable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e61fb0bb-16c5-4824-8165-93eca8b13e57
📒 Files selected for processing (10)
PRIMITIVE.mdREADME.mdadapters/claude-code/dispatch/builder-api.mdadapters/claude-code/dispatch/planner-api.mdadapters/pi/README.mdapply.pybin/role-callbin/set-api-keyroles.config.jsonroles.schema.json
🚧 Files skipped from review as they are similar to previous changes (6)
- adapters/claude-code/dispatch/planner-api.md
- adapters/claude-code/dispatch/builder-api.md
- bin/set-api-key
- roles.schema.json
- README.md
- apply.py
| $TASK" | ||
|
|
||
| # --- build the claude command ---------------------------------------------- | ||
| CMD=( timeout "$TIMEOUT" claude -p "$PROMPT" --model "$MODEL" --output-format json ) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant area of bin/role-call with line numbers.
sed -n '120,190p' bin/role-call | cat -n
# Find where claude is invoked and how PROMPT is constructed.
rg -n --no-heading 'claude -p|PROMPT=|TIMEOUT|CMD=\(' bin/role-call
# Look for any mention of stdin usage or headless/headless docs in the repo.
rg -n --no-heading 'stdin|headless|output-format json|claude -p' .Repository: dyuhaus/dev-primitive
Length of output: 6268
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '130,175p' bin/role-call | cat -nRepository: dyuhaus/dev-primitive
Length of output: 2557
🌐 Web query:
Anthropic Claude headless mode stdin prompt -p documentation
💡 Result:
Anthropic provides "headless mode" for Claude Code via the -p (or --print) flag, which allows the tool to run non-interactively as a standard Unix process [1][2]. This mode is designed for automation, CI/CD pipelines, and scripted tasks [3][4]. Key mechanisms for headless mode include: - Execution: Add the -p flag followed by your prompt to run Claude Code as a single batch operation [1][3]. The process executes the agent loop, outputs the result, and terminates immediately [2][3]. - Stdin Input: Claude Code in headless mode reads from stdin, allowing you to pipe data directly into the prompt [1][2]. You can pipe content using standard shell patterns like cat file.txt | claude -p 'analyze this' or claude -p 'analyze this' < file.txt [2][5][3]. - Output Formatting: Use the --output-format flag to control the response, with options such as json, text, or stream-json for machine-readable parsing [1][2][3]. - Tool Permissions: Since there is no human in the loop to approve actions, you must use --allowedTools to pre-approve specific tools (e.g., Bash, Read, Edit) to prevent the agent from pausing for permission [1][2][3]. - Best Practices: - Since headless mode cannot handle follow-up conversational turns, include all necessary context and instructions within the initial prompt [5][3]. - Piped stdin input is subject to a size cap (10MB as of recent versions) [1][2]. For larger datasets, reference file paths directly in the prompt instead of piping the raw content [1][2]. - Use --bare to reduce startup time by skipping unnecessary hook, plugin, and memory auto-discovery, which is recommended for scripts and SDK calls [1]. For programmatic SDK-level control, advanced protocols exist that enable JSON-based stdin/stdout streams (--input-format stream-json and --output-format stream-json), allowing external applications to maintain session state and handle permission requests via a custom control protocol [6][4]. Top results: [1][2][6][3]
Citations:
- 1: https://code.claude.com/docs/en/headless
- 2: https://www.buildthisnow.com/blog/guide/development/claude-code-headless-mode
- 3: https://hidekazu-konishi.com/entry/claude_code_cicd_and_headless_automation.html
- 4: https://www.linkedin.com/posts/paoloperrone_anthropic-just-showed-how-to-run-claude-code-activity-7477119231004770305-YCT9
- 5: https://cybercorsairs.com/most-people-use-claude-code-like-a-chatbot-it-also-runs-headless/
- 6: https://github.com/Roasbeef/claude-agent-sdk-go/blob/main/docs/cli-protocol.md
Pipe the prompt through stdin instead of argv. bin/role-call:150 sends the combined prompt as a single command-line argument, exposing it in process listings and risking argv-length failures. Claude headless mode accepts stdin, so this can stay out of argv.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/role-call` at line 150, Update the command construction in bin/role-call
so the combined prompt is provided to claude through stdin rather than passed
via the -p argument. Preserve the existing timeout, model, and JSON output
options while removing the prompt from argv to avoid process-list exposure and
argument-length limits.
| # --- invoke: key exported only into the child; never in argv --------------- | ||
| ERRFILE="$(mktemp)" | ||
| OUT="$( export "$API_KEY_ENV=$KEY"; "${CMD[@]}" 2>"$ERRFILE" )" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant script and search for the env var in the repo.
git ls-files | rg '^bin/role-call$|CLAUDE_CODE_SUBPROCESS_ENV_SCRUB|Anthropic|Claude|API_KEY_ENV'
printf '\n--- bin/role-call excerpt ---\n'
sed -n '150,210p' bin/role-call
printf '\n--- repo references to CLAUDE_CODE_SUBPROCESS_ENV_SCRUB ---\n'
rg -n "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB" .Repository: dyuhaus/dev-primitive
Length of output: 2641
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Pull a broader context around subprocess/env handling in bin/role-call.
ast-grep outline bin/role-call --view expanded || true
printf '\n--- surrounding lines ---\n'
sed -n '1,260p' bin/role-callRepository: dyuhaus/dev-primitive
Length of output: 11239
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify whether the env var is documented upstream and what it is intended to do.
python3 - <<'PY'
import urllib.request, re
urls = [
"https://code.claude.com/docs/en/env-vars",
]
for url in urls:
try:
with urllib.request.urlopen(url, timeout=20) as r:
text = r.read().decode('utf-8', 'replace')
print("URL:", url)
for m in re.finditer(r'CLAUDE_CODE_SUBPROCESS_ENV_SCRUB.{0,180}', text):
print(m.group(0))
break
else:
print("No direct mention found in fetched HTML.")
except Exception as e:
print("URL:", url, "ERROR:", e)
PYRepository: dyuhaus/dev-primitive
Length of output: 346
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the current line contents precisely in case surrounding context matters.
nl -ba bin/role-call | sed -n '168,186p'Repository: dyuhaus/dev-primitive
Length of output: 199
🌐 Web query:
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB Claude Code subprocess env scrub documentation
💡 Result:
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB is an environment variable for Claude Code used to enhance security by restricting the information exposed to subprocesses [1][2]. When set to 1, this variable triggers several security controls for subprocesses such as the Bash tool, hooks, and MCP stdio servers [1][2]: 1. Environment Variable Scrubbing: It strips Anthropic API keys (e.g., ANTHROPIC_API_KEY) and various cloud provider credentials (e.g., AWS, GCP, Azure environment variables) from the environment inherited by child subprocesses [1][3][2]. While the parent Claude process retains these credentials for necessary API calls, child processes are prevented from accessing them, which mitigates risks related to prompt injection attacks attempting to exfiltrate secrets via shell expansion [1][2]. 2. PID Namespace Isolation (Linux only): On Linux systems, it executes Bash subprocesses within an isolated PID namespace [1][2]. This prevents the subprocess from viewing or signaling host processes via /proc or standard tools like ps, pgrep, and kill [1][2]. Consequently, any background daemons launched by the subprocess are confined and cannot persist beyond the session [4]. It is recommended to use this variable as part of a defense-in-depth strategy, particularly when running Claude Code in environments where untrusted users might trigger actions [3]. Note that in some versions, enabling this flag may impose stricter default permission modes [5]. For more granular control over specific app-level secrets (like GITHUB_TOKEN or NPM_TOKEN), it is often used in combination with sandbox.credentials [6].
Citations:
- 1: [DOCS]
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1environment variable is missing from the docs anthropics/claude-code#38559 - 2: https://claude-code.mintlify.app/en/env-vars
- 3: Auto-set subprocess env scrub when allowed_non_write_users is configured anthropics/claude-code-action#1093
- 4: https://agentpatterns.ai/security/subprocess-pid-namespace-sandboxing/
- 5: CLAUDE_CODE_SUBPROCESS_ENV_SCRUB should not override permission mode anthropics/claude-code#51258
- 6: https://claudelab.net/en/articles/claude-code/claude-code-sandbox-credentials-deny-secret-read-surface
Scrub the Anthropic key from Claude subprocesses at bin/role-call:177-179. Set CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 in the child environment before invoking claude; otherwise Bash tools, hooks, and stdio MCP servers can inherit and exfiltrate the key via prompt injection.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/role-call` around lines 177 - 179, Update the child-environment setup in
the role-call invocation around ERRFILE, OUT, and CMD so it exports
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 alongside the API key before launching
claude, ensuring subprocesses, hooks, and MCP servers cannot inherit the key.
| req = urllib.request.Request(url, data=json.dumps(body).encode("utf-8"), method="POST") | ||
| req.add_header("Authorization", "Bearer " + key) | ||
| req.add_header("Content-Type", "application/json") | ||
| try: | ||
| timeout = float(os.environ.get("RC_TIMEOUT", "") or 3600) | ||
| except ValueError: | ||
| timeout = 3600 | ||
| try: | ||
| resp = urllib.request.urlopen(req, timeout=timeout) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the file and inspect the relevant area.
wc -l bin/role-call
sed -n '240,330p' bin/role-call
# Find any redirect handling or urllib opener customization in this file.
rg -n "HTTPRedirectHandler|NoRedirect|build_opener|urlopen|add_header|redirect" bin/role-callRepository: dyuhaus/dev-primitive
Length of output: 3783
🏁 Script executed:
python3 - <<'PY'
import inspect, urllib.request, urllib.error
print("HTTPRedirectHandler.redirect_request source:")
print(inspect.getsource(urllib.request.HTTPRedirectHandler.redirect_request))
print("\nRequest.add_header source:")
print(inspect.getsource(urllib.request.Request.add_header))
PYRepository: dyuhaus/dev-primitive
Length of output: 2020
🏁 Script executed:
python3 - <<'PY'
import inspect, urllib.request
print(inspect.getsource(urllib.request.HTTPRedirectHandler.redirect_request))
PYRepository: dyuhaus/dev-primitive
Length of output: 1814
Reject redirects before sending the bearer token. urllib.request copies Authorization onto redirected requests, so a 30x from the provider can leak the API key to the redirect target. The POST body is dropped, but the credential still needs redirect handling here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/role-call` around lines 285 - 293, Update the request flow around
urllib.request.urlopen so redirects are rejected before the request carrying the
Authorization header can be resent. Add a redirect-handling mechanism for the
existing Request that raises or otherwise aborts on 30x responses, while
preserving the current timeout and POST behavior.
| A missing `total_cost_usd` is the tell that the call may have billed the | ||
| **subscription** rather than the API key — that warning is specific to this engine. | ||
| - `chat`: `role=… engine=chat model=<response model or requested> tokens=<prompt>+<completion>`, | ||
| with ` cost=$<usage.cost>` appended only when the provider returns a cost, and |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove the leading space inside the code span.
Change ` cost=$<usage.cost>` to `cost=$<usage.cost>` to satisfy MD038 and avoid rendering an unintended leading space.
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)
[warning] 138-138: Spaces inside code span elements
(MD038, no-space-in-code)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@PRIMITIVE.md` at line 138, Remove the leading space from the inline code span
in PRIMITIVE.md, changing the documented cost suffix from ` cost=$<usage.cost>`
to `cost=$<usage.cost>` while preserving the surrounding wording.
Source: Linters/SAST tools
Reverts #2 (`d2a641d`). David decided to drop the dual-source (API-credit + subscription) transport for `/pb` and keep planning on the Claude subscription only. ## Why - The transport code was **dormant** — no API key was ever provisioned and both roles ran `harness`/subscription, so `/pb` never actually used API credits. - The credit-drain David observed came from the *separate* fable-cutoff probe (in the homelab repo, now removed and its PR closed), whose 6h `claude --model fable` calls consumed his metered subscription Fable allowance → intermittent HTTP 429 "Usage credits are required for this model". - Per David's decision, removing the transport capability entirely rather than leaving inert code in the public repo. ## Effect - `roles.config.json` returns to the original two-role setup (planner=fable, builder=opus, no `access`/`classIds`/`apiKeyFile`). - `bin/role-call`, `bin/set-api-key`, dispatch fragments, and the `access`/provider-engine additions are removed. - `/pb` and `/pbg` return to the original subscription subagent flow. - `apply.py validate` passes on the reverted tree. ## After merge Pull main on both checkouts and run `python3 apply.py claude` to regenerate the local adapter from the reverted config (behavior is unchanged — subscription-only). 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Changes** - Simplified role configuration to focus on selecting planner and builder models. - Removed direct provider API execution and API-key setup commands. - Reduced configuration commands to supported model-setting workflows. - Claude Code integrations now consistently use planner and builder subagents. - Updated planning, building, and verification guidance for a clearer workflow. - **Documentation** - Revised setup, usage, portability, and adapter documentation to reflect the streamlined configuration. - Added a dry-run example and clarified options such as `--id` and `--no-apply`. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: dyuhaus-agent <yuhaus.david@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What & why
Fable-over-subscription is going away. This adds a per-role transport so a
role can run over the direct Anthropic API (billed to David's own key, off the
interactive subscription) while the other role stays a normal harness subagent.
The concept lives in
roles.config.json(access: "harness" | "api") so everyadapter — Claude Code,
generic, programmatic, the live pi adapter — can honor it.This PR is additive and safe to merge before any key exists. The planner stays
access: "harness"; behavior is unchanged until someone runs the flip. No key isplaced anywhere in the repo.
What changed
roles.<role>.access(defaultharness),providers.<p>.apiKeyFile(a path, never the secret), top-level
classIds({"fable":"claude-fable-5"}) sothe API path can map an alias to a concrete
--modelid. Staysversion: 1.apply.py—validate()now returns(errors, warnings)and warns when anapi role can't resolve a concrete model id; new
resolve_api_model();role_viewexposes
access/api_model/api_key_file; newresolve(machine-readablerole facts,
env|json) andprompt(rendered role charter) subcommands;set --access harness|api;print_table/genericshow transport.install_claude()renders a subagent only for harness roles, removes a stale generated one
for api roles (a hand-written file without the marker is left with a warning), and
two-pass renders
/pband/pbgfrom per-transport dispatch fragments.bin/role-call(committed) — runs an api role headless via theclaudeCLI:composes charter + task, applies the read-only allowlist for read-only roles, runs
with the key exported only into the child process (never argv/logs), prints the
result to stdout and
role=… model=… cost=$… session=…to stderr. Supports--task-file/-/--ping/--dry-run/--force. Exit codes:2no key,4role is harness-transport.bin/set-api-key(committed) — interactive, terminal-only key intake to a0600keyfile (~/appdata/anthropic/api-key); refuses non-tty, echoes nothing,prints only length + short fingerprint;
--verifymakes one tiny real call.adapters/claude-code/dispatch/{planner,builder}-{harness,api}.md;/pband/pbguse them;/pbg-planner&/pbg-builderdocument transport switching.PRIMITIVE.md gains a "Role transports (
access)" section + flip runbook; READMEconfig snippet / quickstart / layout / harness table updated.
Verification (all no-cost; done on scratch copies)
apply.py validate— valid, no spurious warnings (planner still harness).~/.claudefrom the (harness) config:planner.mdandbuilder.mdsubagents byte-identical to pre-change;/pbdiffers only byline-wrap;
/pbgnormalizes "thebuilder" → "thebuildersubagent";/pbg-*gain an additive transport paragraph. Nothing breaks before the flip.
planner access=api:agents/planner.mdremoved (marker-bearing),agents/builder.mdkept,/pbrewritten to callbin/role-call plannerwithabsolute paths; a marker-less
planner.mdis left in place with a warning;--dry-runprints "would remove …".
set planner --access api/--dry-run, real set, and set-back to harness — all correct;api set with no key present prints an advisory WARNING (never fails).
role-call planner --dry-run: command shown,key_source=MISSING, modelclaude-fable-5, read-only allowlist; withANTHROPIC_API_KEY=<dummy>→from env;with a
0600keyfile →from <keyfile>and a real (fake-claude) invoke returnsthe result on stdout + the
cost=$…line on stderr; a non-600keyfile warns; aresponse lacking
total_cost_usdemits the subscription-billing WARNING.role-call builder …(harness) → exit 4;--forceoverrides.apply.py generic(api config) prints the per-role "Reached via …" lines.git diffscanned for key material — none (gitleaks pre-commit also clean).Flip runbook (Step 12 — do this once the key exists; NOT done here)
bin/set-api-key— David provisions the Anthropic key (interactive, in a real terminal).bin/role-call planner --ping— confirm acost=$…line appears (proves it bills the API key).python3 apply.py set planner --access api— flips transport + regenerates (removes~/.claude/agents/planner.md)./pbsmoke on a tiny task — confirm the orchestrator invokesrole-call plannerand relays the cost.AGENTS.md+CLAUDE.mdsentences and the memory note (see below).After the flip there is no
plannersubagent. Any orchestrator guidance thatsays "delegate to the planner subagent" must be updated in the same session to
"run
dev-primitive/bin/role-call planner", or/pb//pbgwill have nothing todelegate to. The regenerated
/pb//pbgalready say this; harness-level docs must bebrought in line at flip time.
Deferred to flip time (NOT edited in this PR — checklist)
/home/dyadmin/AGENTS.md— Model Routing: note planning may be routed viathe direct API, in which case there is no planner subagent and
/pb///pbginvoke
dev-primitive/bin/role-call planner./home/dyadmin/CLAUDE.md— same one-sentence note so the Claude entrypointmatches the neutral contract.
🤖 Generated with Claude Code
Update — now provider-generic (2nd commit)
The api transport is no longer Anthropic-only.
bin/role-callnow picks adirect-API engine from the provider
type, so anaccess: "api"role can runover any OpenAI-compatible or local endpoint:
typeanthropicclaude-cliclaude -p(moved verbatim; unchanged behavior)openai,localchatPOST {baseUrl}/chat/completions,Authorization: Bearer, charter = system msg, task = user msg; no toolsgoogletype:"openai"providerapply.py— engine derived inrole_viewand emitted as append-onlyresolvefacts (
ENGINE/PROVIDER/BASE_URL/BASE_URL_ENV; existing keys/order untouched);keyfile defaults to
~/appdata/<provider>/api-key; provider-scopedclassIds["<provider>:<class>"]lookup (id → scoped → global → literal); newnon-error warnings for google-type and for openai/local with no base URL; a chat
"no tools — inline all context" note injected into the
/pb/pbgdispatch text.bin/role-call—case "$ENGINE"; chat engine does base-URL precedence(
$baseUrlEnv→ literalbaseUrl→ hard error), writes charter/task to two tempfiles, and calls a stdlib-
urllibpython child with the key exported only asRC_KEY(never argv — verified withps). Per-engine stderr contract:claude-clikeepscost=$<total_cost_usd>+ the subscription-billing warning;chatprintstokens=<prompt>+<completion>(+ cost=$…when returned,tokens=?when usage absent). Exit codes 0/1/2/4 preserved; the exit-2 no-key message now
names the provider.
bin/set-api-key— defaults the keyfile, keys its shape check by provider thentype (
anthropic→sk-ant-,openrouter→sk-or-,openai→sk-), and--verifytargets the first api role's engine (chat success = a
tokens=line; noted that forchat this proves key validity, not billing attribution).
providers.<p>.baseUrl(schema + demoopenrouterentry) and ademo
"openrouter:sonnet"scoped classId; PRIMITIVE.md gains a "Provider engines"section (engine table, base-URL precedence, non-agentic rule, per-engine cost
contract, Gemini recipe, worked OpenRouter example); README + pi adapter note updated.
Active wiring untouched: both roles stay
anthropic/harness; regenerating thelive
~/.claudefrom the config is a byte-for-byte no-op (verified). All Step-8 testspass with no keys and no spend (fake
chat/completionsserver end-to-end:--pingand full path return
OKwithtokens=12+2; keyfile fallback; exit-2 no-key; missingbase URL; HTTP 500 body excerpt; google guidance;
--pingbody carriesmax_tokens;psshows no key in any argv).Follow-up — live-key smoke (deferred; needs a real provider key, NOT done here)
bin/set-api-key openrouterthenbin/role-call planner --pingagainst the real endpoint returns atokens=line.--id(e.g.anthropic/claude-sonnet-5)is one the endpoint actually serves.
usage.cost— check whether the provider returnsusage.costso thecost=$…suffix appears (OpenRouter does; some endpoints omit it →tokens=?)./pb— flip a role to a chat-engine provider and confirm theorchestrator runs
bin/role-calland relays the token/cost figures.Summary by CodeRabbit
harnessvs direct provider API) with transport-aware role switching.bin/role-callfor runningapiroles via direct provider endpoints with cost diagnostics and safe key handling.bin/set-api-keyfor interactive API-key provisioning (with optional verification).apply.pywith--access, plusresolveandpromptactions for role facts and rendered charters.apiworkflow, templates, portability, and verification steps.