Skip to content

feat(claude-code): Claude Code as an iii worker - #243

Merged
rohitg00 merged 25 commits into
mainfrom
claude-code-worker
Jun 15, 2026
Merged

feat(claude-code): Claude Code as an iii worker#243
rohitg00 merged 25 commits into
mainfrom
claude-code-worker

Conversation

@rohitg00

@rohitg00 rohitg00 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

New Node worker that drives headless Claude Code turns over the iii bus.

Screenshot 2026-06-11 at 15 54 49

Summary by CodeRabbit

  • New Features

    • Added a claude-code worker exposing run/start/stop/status/sessions APIs with session persistence, raw Claude message mirroring, and translated agent event streaming.
  • Documentation

    • Comprehensive README, skill guide, and usage/observability details, including prompts, session/resume, and permission-mode workflows.
  • Configuration

    • Runtime defaults, event stream wiring, executable resolution, and an optional approval-gate for tool permissions.
  • Tests

    • Extensive tests covering config, event emission, executable resolution, mapping, run flows, state, and approval-gate behavior.
  • Chores

    • Packaging, build/bundle tooling, linting, and test runner configuration added.

rohitg00 added 11 commits June 11, 2026 12:09
New Node worker that drives headless Claude Code turns over the iii bus.

- claude::run / claude::start / claude::stop / claude::status /
  claude::sessions::list, plus run::start_and_wait so the canonical brain
  contract (console, acp) drives Claude Code unchanged
- AgentEvent subset (message_complete, function_execution_start/end,
  turn_end, agent_end) streamed onto agent::events via stream::set
- in-process MCP bridge exposes engine functions_list / functions_info /
  trigger to the running turn
- session resume via engine state scope claude_sessions (iii session_id
  to Claude session id mapping)
- optional approval_gate config routes tool permissions through
  policy::check_permissions, fail-closed
- deploy: bundle with esbuild single-file build; claude_executable
  config plus PATH fallback covers the bundled SDK losing its native CLI
- vitest unit tests for payload parsing, event mapping, config,
  executable resolution
…opt-in bus bridge

- options field on claude::run forwards any Agent SDK Options key
  verbatim (forkSession, includePartialMessages, fallbackModel,
  addDirs, mcpServers, ...)
- every SDK message mirrors verbatim onto the claude::events stream
  (system/init, assistant, user, result, stream_event partials),
  alongside the translated AgentEvent view on agent::events
- expose_iii_bridge now defaults to false: the worker is a plain
  Claude Code API surface unless the operator opts into the
  mcp__iii__* bus tools; user MCP servers pass through options
- zero-to-turn quickstart in README (install engine, worker add, iii)
The worker exposes the Claude Code API and nothing else. The in-process
MCP server that handed Claude mcp__iii__* bus tools was an extra feature
on top of that surface; users who want MCP servers in a turn pass them
through options.mcpServers exactly as in the Agent SDK.

Removes src/bridge.ts, the expose_iii_bridge config option, and all
related wiring and docs.
Capabilities beyond Claude Code itself come from other iii workers on
the bus, not from MCP wiring.
- SKILL.md leads with what the worker exposes and how to call it:
  payload shapes, both streams, session resume, options pass-through
- removes 'brain' / 'canonical brain contract' wording everywhere
  (skill, README, function descriptions, source comments, test names);
  run::start_and_wait described as the entrypoint the console and acp
  worker drive
- fixes two stale claims: concurrent runs on one session race (no
  queueing), and token deltas exist on claude::events when
  includePartialMessages is set
Brings the suite to the level of the other workers' tests: handlers
exercised at the engine's unknown boundary with an in-memory bus fake
and a scripted Agent SDK query mock.

- tests/_helpers: fake ISdk (state::get/set/list semantics, stream::set
  capture, registerFunction capture) and scripted query() fixtures
- run.test.ts: full executeRun flow — result/usage/cost mapping,
  working->done record lifecycle, verbatim claude::events mirror,
  AgentEvent sequence with function_execution pairs, named-field and
  raw-options forwarding, resume for known sessions, SDK throw path,
  non-success stop reasons; approval gate allow/deny/fail-closed/off
- register.test.ts: all six function ids, boundary parse rejection,
  claude::start background completion, claude::stop on live and ghost
  runs, status and sessions::list
- events.test.ts: frame shape, per-session monotonic item_ids,
  stream::set failures swallowed
- state.test.ts: scope round-trip, direct-value state::get reply
  (regression for the live resume bug), non-array list tolerance

57 tests, up from 26.
iii trigger <fn> --help previously showed '(no request schema
published)'. Each registerFunction call now carries request_format
derived from the zod payload schema via zod 4's native z.toJSONSchema,
with per-field descriptions, so the CLI and engine catalog print the
full parameter table.
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
harness Error Error Jun 15, 2026 5:06pm
workers Ready Ready Preview, Comment Jun 15, 2026 5:06pm

Request Review

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 19 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Node-based claude-code worker that shells out to the host Claude CLI, exposes claude::* functions (run/start/stop/status/sessions::list and run::start_and_wait), persists sessions, mirrors raw Claude frames to claude::events, streams mapped AgentEvent frames to agent::events, and includes config, bundling, docs, and tests.

Changes

claude-code Worker Implementation

Layer / File(s) Summary
Wire types and event contracts
claude-code/src/types.ts
Core TypeScript types defining ContentBlock variants, AgentMessage union, AgentEvent discriminated union, SessionRecord, and Usage accounting.
Message mapping and transformation
claude-code/src/map.ts, claude-code/tests/map.test.ts
Maps Claude SDK content/tool results into wire ContentBlocks, normalizes usage, constructs AssistantMessage/FunctionResultMessage, and records ToolCallIndex.
Configuration and project setup
claude-code/src/config.ts, claude-code/config.yaml, claude-code/package.json, claude-code/tsconfig.json, claude-code/biome.json, claude-code/iii.worker.yaml, claude-code/.gitignore, claude-code/tests/config.test.ts
Zod-backed runtime config with defaults and approval_gate, YAML config template, Node ESM package setup, TypeScript/biome/vitest configs, ignore rules, and config loading tests.
Build and bundling
claude-code/scripts/build-bundle.mjs
esbuild script that bundles the worker entrypoint, inlines iii-sdk package.json lookup, and adds an ESM-compatible createRequire banner.
CLI entrypoint and executable resolution
claude-code/src/index.ts, claude-code/src/executable.ts, claude-code/tests/executable.test.ts
Bootstraps worker from CLI flags, loads config, resolves claude CLI path (config override or PATH scan), registers handlers, and handles shutdown.
Session persistence
claude-code/src/state.ts, claude-code/tests/state.test.ts
Helpers to load, save, and list SessionRecord via iii state triggers with tolerant fallbacks and tests.
Event emission to streams
claude-code/src/events.ts, claude-code/tests/events.test.ts
Emitter factory that writes session-grouped stream::set frames with unique monotonic item_ids and swallows/logs trigger failures.
Core execution and function registration
claude-code/src/run.ts, claude-code/tests/run-payload.test.ts, claude-code/tests/register.test.ts, claude-code/tests/run.test.ts
Run payload schemas, prompt extraction, approval-gated tool checks, executeRun orchestration (streaming raw and agent events, usage/cost tracking, session lifecycle), and registration of claude::* handlers with request validation and extensive tests.
Test infrastructure and helpers
claude-code/tests/_helpers/fake-iii.ts, claude-code/tests/_helpers/fake-query.ts
In-memory fake iii SDK, scripted query async-iterator with interrupt, and fixtures for unit/integration tests.
Documentation and skill definitions
README.md, claude-code/README.md, claude-code/skills/SKILL.md
Root Modules listing, worker README with quickstart/config/mapping, and SKILL.md documenting function surface, boundaries, and operational semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • andersonleal
  • ytallo
  • sergiofilhowz

"🐰
I hop and bind the CLI’s bright code,
Streams and sessions watch the road,
Raw frames mirror, agent frames sing,
Approval gates and tests in spring,
Hooray — the worker's set to go!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature: adding Claude Code as a worker for the iii platform, which aligns with the substantial changeset introducing a new claude-code worker module.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude-code-worker

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🤖 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 `@claude-code/biome.json`:
- Line 2: The Biome schema version in claude-code/biome.json ("$schema":
"https://biomejs.dev/schemas/2.4.16/schema.json") is newer than the pinned CLI
version in claude-code/package.json (`@biomejs/biome`@2.4.10); either downgrade
the schema reference to 2.4.10 or upgrade the package pin so both match—update
the "$schema" value to "https://biomejs.dev/schemas/2.4.10/schema.json" if you
want to keep `@biomejs/biome`@2.4.10, or bump the `@biomejs/biome` version in
package.json to 2.4.16 to match the current schema.

In `@claude-code/iii.worker.yaml`:
- Line 6: Update the manifest description line in iii.worker.yaml to remove the
reference to "in-process MCP bridge" and instead state that MCP servers are
provided via options (e.g., "expects MCP servers to be passed via options");
also update the package.json "description" field to match this new wording so
both descriptions remain consistent. Locate and edit the description value in
iii.worker.yaml and the description property in package.json to reflect the
bridge removal and the new behavior.

In `@claude-code/scripts/build-bundle.mjs`:
- Around line 32-36: The plugin currently calls readFile(join(root,
'node_modules/iii-sdk/package.json')) without verifying the file exists; update
the b.onLoad handler (the async callback that uses args, root, readFile, and
join) to first check for the package.json presence (e.g., using fs.existsSync or
try/catch around readFile) and handle the missing file by either returning a
clear processLogger/error message or providing a safe fallback value so the
build fails with a descriptive error instead of an unhandled exception. Ensure
the updated logic still reads the original source (readFile(args.path, 'utf8'))
and only attempts to parse package.json when present, referencing the same
join(root, 'node_modules/iii-sdk/package.json') path used now.

In `@claude-code/src/config.ts`:
- Around line 30-34: The try/catch in config.ts around the parse(await
readFile(path, 'utf8')) call is currently swallowing all errors; change the
catch to accept an error parameter and only ignore missing-file errors
(error.code === 'ENOENT' or equivalent) and rethrow every other error (including
YAML parse errors and permission errors) so the worker fails fast; update the
block that assigns raw to ensure non-ENOENT exceptions are rethrown while
preserving the existing fallback-to-defaults behavior for a missing file.

In `@claude-code/src/events.ts`:
- Around line 10-15: seqBySession is a global Map that accumulates per-session
counters and is never pruned; update makeEmitter/emit to remove the session
entry when the session reaches a terminal event (e.g., detect event type or
property indicating "agent_end" or similar) by calling
seqBySession.delete(session_id) after handling that terminal event; if terminal
detection requires different shapes, add a small helper inside makeEmitter (or
use an exported function) to determine terminal state from the passed event and
ensure deletion is performed exactly once when terminal is seen to prevent
memory leak for long‑lived workers.

In `@claude-code/src/index.ts`:
- Around line 34-37: The shutdown handler currently awaits iii.shutdown and may
never call process.exit(0) if iii.shutdown rejects; modify the shutdown function
(named shutdown) to call iii.shutdown inside a try/catch or try block and use a
finally block to always call process.exit(0) (and in the catch optionally log
the error via your logger) so the process exits deterministically even if
iii.shutdown throws.

In `@claude-code/src/map.ts`:
- Around line 114-119: The function lastAssistant currently can return undefined
for an empty messages array; update lastAssistant to guard the empty-input case
at the top (e.g., if messages.length === 0) and return a safe default
AgentMessage instead of undefined. Construct the default minimal AgentMessage
with role: 'assistant' and empty/neutral fields required by your AgentMessage
type (e.g., content: '' and any required ids/metadata), then keep the existing
reverse-search logic for non-empty arrays so callers always receive a valid
AgentMessage.
- Around line 57-64: In the Array.isArray(raw) branch, don't filter out scalar
entries; instead iterate over raw and for each item produce an SdkContentBlock:
if the item is an object (and not null) treat it as before (type:'text', text =
item.type === 'text' ? item.text ?? '' : JSON.stringify(item)), otherwise
convert the scalar (including null/undefined) to a string via
JSON.stringify(item) (or String(item) if preferred) and return a text block for
it so scalar array elements are preserved rather than dropped.

In `@claude-code/src/run.ts`:
- Around line 128-133: The current logic reuses a prior SessionRecord (symbol:
prior) into record and then later sources model/cwd from record, which ignores
per-turn overrides in payload; fix by merging payload overrides into the resumed
record so payload.cwd and payload.model take precedence when present (i.e., when
constructing record from prior, set record.cwd = payload.cwd ?? prior.cwd and
record.model = payload.model ?? prior.model) or alternatively adjust the later
uses (where model/cwd are read) to use payload.model ?? record.model and
payload.cwd ?? record.cwd; update the code paths that reference record, prior,
payload, session_id, claude_session_id, cwd, model and status to reflect this
merge so resumed sessions honor per-turn overrides.
- Around line 162-163: The live map currently stores a single handler per
session_id (live.set(session_id, { interrupt: () => q.interrupt() })), causing a
race when multiple runs share the same session: later runs overwrite earlier
handlers and an earlier run's finally block can delete the key and detach the
later run. Fix by making the value for each session_id track handlers per-run
(e.g., a Set/Map of handlers) or by associating a unique run token with the
stored handler and only removing the handler you created in your finally block;
specifically, when creating the handler for q.interrupt() generate a unique
runId/token, add the handler under live.get(session_id) (append to a collection
or store {runId, interrupt}), and in the finally block remove only that run's
handler (or delete the session entry only if the collection becomes empty)
rather than unconditionally calling live.delete(session_id).
- Around line 88-92: The code treats an empty-string prompt as missing because
it uses a truthy check on payload.prompt; change the check to explicitly accept
an explicit string (including empty string) by returning payload.prompt when
it's present (e.g. if (payload.prompt !== undefined && payload.prompt !== null)
return payload.prompt; or better if (typeof payload.prompt === 'string') return
payload.prompt;), so that payload.prompt === "" is returned instead of falling
through to messages; keep the subsequent logic that falls back to
payload.messages and the typeof last.content check unchanged.
- Around line 157-159: The build of the options object currently spreads
payload.options after adding approval-gate-derived fields, allowing callers to
override enforcement; fix by preventing permission-related keys in
payload.options from shadowing approval logic—either move the spread of
payload.options before applying the approval-gate override so canUseTool and
related fields always win, or explicitly filter out enforcement keys (e.g.,
permissionMode, allowedTools, disallowedTools, canUseTool) from payload.options
before merging; update the code paths referencing gatedCanUseTool,
approval_gate, payload.options, iii, and session_id accordingly.

In `@claude-code/src/state.ts`:
- Around line 12-33: The state trigger calls in loadSession, saveSession, and
listSessions are currently unprotected and will throw on transient failures;
wrap each iii.trigger invocation in a try/catch: in loadSession (function name
loadSession) catch errors, log the error with context (including session_id) and
return null; in saveSession (function name saveSession) catch errors, log the
error with context (including record.session_id) and return without rethrowing
(best-effort save); in listSessions (function name listSessions) catch errors,
log the error and return an empty array; ensure logs include the caught error
object/details for debugging.

In `@claude-code/tests/config.test.ts`:
- Line 9: The test currently uses a hard-coded platform-dependent path
'/nonexistent/config.yaml' when calling loadConfig; replace that with a
guaranteed-missing temporary path by generating a random filename in the system
temp directory (e.g., via os.tmpdir()/path.join or a tmp utility) and ensure the
file is not created before calling loadConfig so the missing-file behavior is
deterministic; update the test that invokes loadConfig(...) in
tests/config.test.ts to compute this temp path instead of using the hard-coded
literal.

In `@claude-code/tests/events.test.ts`:
- Around line 39-44: The console.warn spy (warn) created before calling
makeEmitter/emit can leak if an assertion throws; wrap the spy lifecycle in a
try/finally so warn.mockRestore() always runs (or use suite-level auto-restore
with vi.restoreAllMocks()), e.g., create the spy (const warn = vi.spyOn(console,
'warn')...), run the test logic that calls makeEmitter/emit and assertions
inside the try block, and call warn.mockRestore() in the finally block to ensure
the spy is always restored.

In `@README.md`:
- Line 43: Update the README table entry for `claude-code` to remove the stale
claim about an "in-process MCP" bridge and any wording that it "bridges the
engine catalog back to Claude" or streams frames onto `agent::events`; instead
state that `claude-code` exposes a pure Claude Code API surface (e.g., keep
reference to `claude::*` running headless Claude Code turns but remove
MCP/bridge/agent::events mentions) so the description matches the current
implementation.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d798685-490e-4fa3-854a-735093d93d08

📥 Commits

Reviewing files that changed from the base of the PR and between 1f66003 and fb1112c.

⛔ Files ignored due to path filters (5)
  • claude-code/assets/cli-help.png is excluded by !**/*.png
  • claude-code/assets/cli-run.png is excluded by !**/*.png
  • claude-code/assets/cli-status.png is excluded by !**/*.png
  • claude-code/assets/console-traces.png is excluded by !**/*.png
  • claude-code/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (29)
  • README.md
  • claude-code/.gitignore
  • claude-code/README.md
  • claude-code/biome.json
  • claude-code/config.yaml
  • claude-code/iii.worker.yaml
  • claude-code/package.json
  • claude-code/scripts/build-bundle.mjs
  • claude-code/skills/SKILL.md
  • claude-code/src/config.ts
  • claude-code/src/events.ts
  • claude-code/src/executable.ts
  • claude-code/src/index.ts
  • claude-code/src/map.ts
  • claude-code/src/run.ts
  • claude-code/src/state.ts
  • claude-code/src/types.ts
  • claude-code/tests/_helpers/fake-iii.ts
  • claude-code/tests/_helpers/fake-query.ts
  • claude-code/tests/config.test.ts
  • claude-code/tests/events.test.ts
  • claude-code/tests/executable.test.ts
  • claude-code/tests/map.test.ts
  • claude-code/tests/register.test.ts
  • claude-code/tests/run-payload.test.ts
  • claude-code/tests/run.test.ts
  • claude-code/tests/state.test.ts
  • claude-code/tsconfig.json
  • claude-code/vitest.config.ts

Comment thread claude-code/biome.json Outdated
Comment thread claude-code/iii.worker.yaml Outdated
Comment thread claude-code/scripts/build-bundle.mjs
Comment thread claude-code/src/config.ts
Comment thread claude-code/src/events.ts
Comment thread claude-code/src/run.ts Outdated
Comment thread claude-code/src/state.ts
Comment thread claude-code/tests/config.test.ts
Comment thread claude-code/tests/events.test.ts
Comment thread README.md Outdated
- biome.json schema version matches the pinned CLI (2.4.10)
- iii.worker.yaml, package.json, and root README descriptions drop the
  removed MCP-bridge wording
- loadConfig only swallows ENOENT; YAML parse and permission errors
  fail the worker fast
- shutdown always exits even when iii.shutdown rejects
- lastAssistant returns a synthetic assistant message for an empty
  transcript instead of undefined
- mapToolResultContent preserves scalar array entries
- resumed sessions honor per-turn cwd/model overrides
- live-run registry deletes only its own handle, so a stale run cannot
  detach a newer run for the same session
- explicit empty-string prompt is forwarded instead of falling through
  to messages
- approval gate wins over caller options: canUseTool/permissionMode in
  the options pass-through cannot shadow enforcement

62 tests (5 new regression tests).
Every turn's system prompt now carries the iii runtime block, the same
engine-grounded discovery rules as the harness identity prompts
retargeted to the iii CLI the agent reaches through its shell:
discover ids via engine::functions::list, fetch the contract with
iii trigger <fn> --help before the first call, install from the
public registry when nothing registered fits, never invent ids or
fields from memory. The matching Bash(iii *) allow rule is added
automatically so those calls run headless.

Caller-supplied system_prompt still wins verbatim; iii_context: false
disables per turn or in config.yaml. 65 tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 `@claude-code/src/iii-prompt.ts`:
- Around line 43-50: Update the prompt text and runtime guards so registry
installs cannot be executed headlessly: replace the direct "iii trigger
worker::add --json ..." recommendation with an explicit approval step and
allowlist check (e.g., require a user confirmation prompt and validate against a
TRUSTED_REGISTRIES/allowlist) and ensure the headless runner (Bash(iii *)) and
any auto-appended "iii-prompt" content will not call "iii trigger worker::add"
unless confirmation succeeds; keep the guidance to verify installation with
"engine::functions::list" and "iii trigger directory::registry::workers::info",
but stop auto-execution by adding a confirmation/allowlist gate before invoking
"iii trigger worker::add".
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a355df47-a4db-49f4-adea-655172add6a0

📥 Commits

Reviewing files that changed from the base of the PR and between 14f9bb1 and 881a77c.

📒 Files selected for processing (8)
  • claude-code/README.md
  • claude-code/config.yaml
  • claude-code/skills/SKILL.md
  • claude-code/src/config.ts
  • claude-code/src/iii-prompt.ts
  • claude-code/src/run.ts
  • claude-code/tests/config.test.ts
  • claude-code/tests/run.test.ts
✅ Files skipped from review due to trivial changes (1)
  • claude-code/README.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • claude-code/config.yaml
  • claude-code/src/config.ts
  • claude-code/tests/config.test.ts
  • claude-code/tests/run.test.ts
  • claude-code/src/run.ts
  • claude-code/skills/SKILL.md

Comment on lines +43 to +50
Need a backend capability? Check what is already registered FIRST — it is usually one call
away. When nothing fits, search the public registry before building anything:
\`iii trigger directory::registry::workers::list --json '{"search":"<capability>"}'\` pages the
published catalogue and \`iii trigger directory::registry::workers::info name=<name>\` returns
one worker's full detail. Say what you are about to install and why, install with
\`iii trigger worker::add --json '{"source":{"kind":"registry","name":"<name>"}}'\`, then
confirm the new ids appear via \`engine::functions::list\` with that prefix and fetch each
contract with \`--help\` as usual.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require explicit approval before registry installs.

This section instructs autonomous installation from the public registry (worker::add) without requiring explicit user approval or a trust allowlist. Given this prompt is auto-appended in the run path and paired with headless Bash(iii *), this materially increases supply-chain and prompt-injection blast radius.

Suggested prompt hardening
-Need a backend capability? Check what is already registered FIRST — it is usually one call
-away. When nothing fits, search the public registry before building anything:
+Need a backend capability? Check what is already registered FIRST — it is usually one call
+away. When nothing fits, search the public registry before building anything, but DO NOT
+install anything until the caller explicitly approves the exact worker name and source:
 ...
-`iii trigger worker::add --json '{"source":{"kind":"registry","name":"<name>"}}'`, then
+`iii trigger worker::add --json '{"source":{"kind":"registry","name":"<name>"}}'` only after
+explicit caller approval (or preconfigured allowlist), then
🤖 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 `@claude-code/src/iii-prompt.ts` around lines 43 - 50, Update the prompt text
and runtime guards so registry installs cannot be executed headlessly: replace
the direct "iii trigger worker::add --json ..." recommendation with an explicit
approval step and allowlist check (e.g., require a user confirmation prompt and
validate against a TRUSTED_REGISTRIES/allowlist) and ensure the headless runner
(Bash(iii *)) and any auto-appended "iii-prompt" content will not call "iii
trigger worker::add" unless confirmation succeeds; keep the guidance to verify
installation with "engine::functions::list" and "iii trigger
directory::registry::workers::info", but stop auto-execution by adding a
confirmation/allowlist gate before invoking "iii trigger worker::add".

…e defaults

- approval gate now forces permissionMode to 'default' so the named
  permission_mode field can't smuggle bypassPermissions past the gate;
  canUseTool stays authoritative for every tool call
- claude::start marks the session 'error' best-effort when a background
  run throws, so a failed terminal save never leaves it stuck 'working'
- mapUsage defaults absent cache token fields to 0 instead of undefined

From an /improve audit. 70 tests (3 new).
- reject a run when one is already live for the session (live.has guard):
  the in-process handle is what claude::stop targets, so a second run
  would clobber it and race the shared record
- persist 'working' only after the query + live handle exist, so a throw
  during setup never leaves the record stuck in 'working'

71 tests (1 new).
Read-only status / sessions::list allow-listed; run/start/stop (and the
run::start_and_wait alias) stay at needs_approval since they spawn a full
Claude Code agent with host fs+shell.
Migrate off file-only config.yaml to the configuration-worker pattern:
config.yaml is the seed (initial_value); the live value is authoritative
and hot-reloads on configuration:updated.

- src/configuration.ts: registerClaudeConfig / fetchRuntime / bindConfigTrigger
  (reconcile now + on every update) using the configuration:: builtins.
- config.ts: RuntimeConfigSchema = ConfigSchema without engine_url
  (bootstrap, stays on --url/seed); runtimeJsonSchema + toRuntime helpers.
- index.ts: load seed -> registerClaudeConfig -> bindConfigTrigger (fetch +
  reconcile into a live holder, re-resolving claude_executable) -> register
  with a () => Config getter so handlers read the live snapshot.
- stream names are read once at boot for the emitters (a change needs a
  restart); every other field hot-reloads.

74 tests (3 new). Verified live: schema registered, configuration::get
returns the value, claude::run runs, configuration::set accepted.
rohitg00 added 2 commits June 15, 2026 17:15
- add claude-code to create-tag.yml options and release.yml tag patterns
  (SOP § 6 — without it a release tag triggers nothing)
- registerClaudeConfig is now best-effort: a configuration-worker hiccup at
  boot logs + continues with the seed instead of crashing the process
- deny the internal claude::on-config-change from agent calls

Merged latest main. 74 tests, biome clean, bundle builds.
From an adversarial review. Two real races in executeRun:

- TOCTOU: live.has() and live.set() were split by the loadSession await,
  so two concurrent same-session calls both passed the guard and the
  second clobbered the first's interrupt handle. Now the check + set are
  synchronous with no await between them (atomic on JS's single thread).
- handle leak: live.set() ran before the try, so a rejected working-save
  left the session stuck reporting busy forever. The body moved into
  runReserved(), wrapped by a try/finally that always releases the slot,
  setup throws included.

75 tests (1 new: slot released when the working-save rejects).
@rohitg00
rohitg00 merged commit 1ae7497 into main Jun 15, 2026
12 checks passed
@rohitg00
rohitg00 deleted the claude-code-worker branch June 15, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant