Skip to content

Build the GraphCode Nod runtime (graphcode-nod) - #590

Merged
scgopi merged 14 commits into
mainfrom
feat/nod-runtime
Oct 2, 2026
Merged

scgopi merged 14 commits into
mainfrom
feat/nod-runtime

Conversation

@scgopi

@scgopi scgopi commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

The GraphCode Nod runtime: a TypeScript package in NodRuntime/ that builds the graphcode-nod executable, with the Claude Agent SDK and the GitHub Copilot SDK behind one engine interface. Stacked on #583 (feat/nod-foundation). Please retarget to main before #583's branch is deleted.

Changes

  • Event log and control socket. events.jsonl is append-only, and seq continues across resumes. A torn last line is tolerated. control.sock takes one NodCommand per line and replies {"ok":…} in order. A plain PTY line is a queued send.
  • One engine interface (src/engine.ts). The Claude adapter runs one long-lived streaming query per session. A PreToolUse ask forces every Bash, edit, web and MCP call through canUseTool, so the human's own Claude allow rules can't bypass the gate. The Copilot adapter puts the gate in its single permission handler.
  • Steering and queueing. Queued messages wait for the turn to end. A steer is delivered at the next tool boundary through PostToolUse additionalContext on both engines. stop interrupts the turn, rejects pending hunks, denies open asks and clears the queue.
  • Staged hunks. A Myers diff is split into git-style hunks, and an edit is held until every hunk has a decision.
    • All accepted: the agent's tool writes the edit.
    • Only some accepted: the runtime writes those to the file as it is now, and the tool is refused with the reviewer's notes.
    • Auto mode: hunks arrive already accepted. A later reject reverse-applies the hunk until the turn ends.
  • Permission gate over NodSettings and the allowlist.
    • Allowlist patterns support * and a|b|c. A compound command must be allowlisted in every part, and command substitution is never allowlisted.
    • A network command is asked about as network. Containment is checked on real paths.
    • Unattended loops (timed, and composite children with --unattended) fail with permissionUnavailable instead of waiting.
  • Goal evaluator. On every stop the goal is re-split into clauses and each is judged with evidence. Unreadable verdicts count as not met. Not met queues a goalCheck turn naming the unmet clauses, with a limit of 20 in a row. markGoalDone is supported.
  • Presence. The runtime writes the same presence, activity (same encoding) and usage zmx labels and sessions/<node>.id/.history as PresenceHooks.
  • Spend cap per run for unattended loops. Claude cost is exact at each turn end and estimated mid-turn. Also emits usage/context events, auto-compacts at 95%, and emits failure events (signInExpired, contextFull, spendCap, permissionUnavailable, engineError).
  • Resume with --resume, --inherit briefs (turn 1 as a handoff; Claude forks the parent conversation), and -p print mode.
  • Sign-in (policy #1190): the Claude engine uses only the anthropic-api-key item in Keychain service app.graphcode.nod. Claude Code runs with inherited credentials stripped and its own CLAUDE_CONFIG_DIR. A rejected key fails within seconds. Copilot uses the github-token item, or else the Copilot CLI's own login.
  • Packaging, decided and documented in the README. scripts/package.sh builds the Contents/Helpers/nod/ folder: bun build --compile (graphcode-nod, ~64 MB), the Claude Code the Agent SDK bundles (~228 MB, Anthropic's signature kept), and the Copilot runtime (~86 MB). It has hardened-runtime JIT entitlements. Agent runtimes are located explicitly, because a compiled binary can't reach the SDKs' platform packages. The runtime takes NOD_STATE and NOD_NODE_ID from the launcher (agreed with NodLaunch).
  • No wire-contract change. test/contract.test.ts compiles the real NodProtocol.swift and checks both directions: Swift decodes all 18 event types the runtime writes, and the runtime parses all 10 commands Swift encodes.
  • CI: .github/workflows/nod-runtime.yml runs typecheck, bun test and a compile on NodRuntime changes.

Not in this PR (agreed with NodGraphLayer): wiring the graphcode MCP server, sendDraft and inbound mail classification from feat/nod-graph-layer. Until then the runtime answers fork, sendDraft and composite runPlan with ok:false.

Smoke runs against real engines

Copilot engine, packaged binary (/tmp/nodpkg/graphcode-nod, head cbc6f0c9). Goal loop in a throwaway git repo, review mode, driven over control.sock (accept every hunk, allow every ask, one steer). 27 records written:

● Nod · default via GitHub Copilot SDK
› Use your Write tool (not the shell) to create hello.txt containing the line `hello nod`. Then run `ls -la` and then `cat hello.txt`.
  ▸ Edit hello.txt
  ± hello.txt +1 −0 · awaiting review
↳ steer Also: keep your final answer to one short sentence.
    hunk h1 accept
    ✓ Tool "create" succeeded. · 0.1s
  ▸ ls -la && echo '---' && cat hello.txt
  ? Nod asks to run ls -la && echo '---' && cat hello.txt — Not in this project's allowlist. Answer in the chat pane.
    allowed
    ✓ <shellId: 0 completed with exit code 0> · 2s
Done.
  1 file · +1 −0
  ◎ Goal check · holds · 2/2

The goalCheck record carried evidence for each clause, for example "evidence":"The bash output includes hello nod, matching the output of cat hello.txt.". Copilot print mode from the packaged binary, with the SDK platform package hidden, printed pkg-copilot-ok.

Claude engine. No Anthropic API key exists on this Mac, so I couldn't run a full session under the policy.

  • I planted a deliberately invalid key in app.graphcode.nod and removed it afterwards (trap-guarded). The packaged binary launched its shipped claude, with the SDK platform package hidden and no claude on PATH. The result was The Anthropic API key was rejected in 4 s, exit 1, and a signInExpired failure event in session mode. Any fallback to the human's Claude login would have produced an answer instead.
  • Before the policy change, the same goal-loop smoke ran green on the Claude engine (haiku): staged hunk accepted, ask answered, steer delivered, goal 2/2. That run used the Claude Code login path, which has since been removed.

Test plan

Real-engine smoke on the Claude engine found the gate classifying /private/tmp/... edits as outside a worktree opened as /tmp/.... The test below was run against the previous gate, then the fixed one.

RED: cd NodRuntime && bun test test/permissions.test.ts with src/permissions.ts from fa789cf^ -> (fail) classification - worktree containment compares real paths, for files that don't exist yet too; 19 pass, 1 fail
GREEN: cd NodRuntime && bun test test/permissions.test.ts -> pass, 20 tests
REGRESSION: cd NodRuntime && bun test -> pass, 91 tests across 8 files, 0 fail; tsc --noEmit exit 0

The contract test fails when a TypeScript event field drifts (files instead of filesChanged gives Swift DecodingError.keyNotFound).

xcodebuild gate, private DerivedData, in progress on a6942057; numbers to follow in a comment. On the pre-rebase head (same NodRuntime, foundation before 9766e57): xcodebuild test exit 0 (2009 tests in 216 suites passed), graphcode-cli build exit 0, graphcoded build exit 0. This PR changes no app Swift sources; NodRuntime/test/swift/main.swift is compiled standalone by the contract test.

Checklist

  • I have read the Contributing Guidelines
  • I have signed off my commits (git commit -s) per the DCO
  • Tests pass locally (bun test in NodRuntime; xcodebuild gate above)
  • Code follows the existing style
  • I added the test/contract before the implementation and observed the intended RED failure (only for the real-path fix; the rest of the suite was written alongside the new package)

scgopi and others added 12 commits October 1, 2026 21:57
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
…, honour --inherit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Policy #1190: no claude.ai login. Claude Code runs with inherited credentials stripped and a
config directory of Nod's own, ships beside graphcode-nod, and a rejected key fails the turn
instead of retrying.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
@scgopi

scgopi commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

Gate on a6942057 (= remote head), private DerivedData: xcodebuild test exit 0, 2011 tests in 217 suites passed; graphcode-cli build exit 0; graphcoded build exit 0. CI on the same head: bun test + typecheck (macOS, includes the NodProtocol.swift contract test) pass, Linux build pass, validate pass, DCO pass.

Follow-up commit adds only NodRuntime/scripts/smoke.ts, the reusable real-engine driver behind the recorded smoke: bun scripts/smoke.ts copilot /path/to/graphcode-nod.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
@scgopi
scgopi changed the base branch from feat/nod-foundation to main October 2, 2026 14:50
Signed-off-by: scgopi <scgopireddy@gmail.com>
@scgopi
scgopi merged commit 63e682c into main Oct 2, 2026
25 checks passed
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