synabun 2.0.10 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +207 -0
- package/README.md +25 -2
- package/THIRD-PARTY-LICENSES.md +22 -0
- package/bin/synabun.js +71 -24
- package/docs/whatsapp.md +13 -13
- package/hooks/claude-code/pre-websearch.mjs +12 -1
- package/hooks/claude-code/shared.mjs +5 -0
- package/hooks/claude-code/stop.mjs +1 -1
- package/lib/claude-hooks.js +172 -18
- package/lib/client-config-repair.js +30 -17
- package/lib/desktop-app.js +7 -1
- package/lib/external-tools.js +7 -6
- package/lib/first-launch-protection.js +57 -12
- package/lib/mcp-dist.js +176 -0
- package/lib/release-npm-ci.mjs +138 -0
- package/lib/release-publish.mjs +91 -0
- package/lib/release-scan.mjs +50 -0
- package/lib/server-crash-record.js +123 -0
- package/lib/server-exit.js +318 -0
- package/lib/server-supervisor.js +508 -0
- package/lib/start-launcher.js +37 -4
- package/lib/tools-cli.js +45 -0
- package/lib/update-snapshot.js +302 -0
- package/lib/virustotal.mjs +203 -0
- package/lib/windows-crash-event.js +269 -0
- package/mcp-server/dist/http.d.ts +20 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +59 -12
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts +33 -27
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +128 -143
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/preload.js +1 -1
- package/mcp-server/dist/preload.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
- package/mcp-server/dist/services/budget-judgments.js +5 -3
- package/mcp-server/dist/services/budget-judgments.js.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +81 -28
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +2 -2
- package/mcp-server/dist/services/identity.js +2 -2
- package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
- package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-sessions.js +28 -2
- package/mcp-server/dist/services/memory-sessions.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +18 -11
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +67 -11
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/server-instructions.d.ts +7 -2
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
- package/mcp-server/dist/services/server-instructions.js +22 -63
- package/mcp-server/dist/services/server-instructions.js.map +1 -1
- package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
- package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-catalog.js +339 -0
- package/mcp-server/dist/services/tool-catalog.js.map +1 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
- package/mcp-server/dist/services/tool-list.d.ts +108 -0
- package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-list.js +334 -0
- package/mcp-server/dist/services/tool-list.js.map +1 -0
- package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
- package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-runtime.js +400 -0
- package/mcp-server/dist/services/tool-runtime.js.map +1 -0
- package/mcp-server/dist/services/typesafe-config.js +1 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
- package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
- package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
- package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +14 -14
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +29 -19
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
- package/mcp-server/dist/tools/choice.d.ts +6 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +18 -13
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +2 -2
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts +1 -1
- package/mcp-server/dist/tools/image-tools.js +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +9 -6
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
- package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/skill-tools.js +642 -0
- package/mcp-server/dist/tools/skill-tools.js.map +1 -0
- package/mcp-server/dist/tools/skill.d.ts +7 -0
- package/mcp-server/dist/tools/skill.d.ts.map +1 -0
- package/mcp-server/dist/tools/skill.js +11 -0
- package/mcp-server/dist/tools/skill.js.map +1 -0
- package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
- package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
- package/mcp-server/dist/tools/tool-groups.js +147 -0
- package/mcp-server/dist/tools/tool-groups.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-format.js +7 -4
- package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/http.ts +75 -12
- package/mcp-server/src/index.ts +147 -154
- package/mcp-server/src/preload.ts +12 -12
- package/mcp-server/src/services/budget-judgments.ts +5 -3
- package/mcp-server/src/services/codex-config-heal.ts +80 -29
- package/mcp-server/src/services/identity.ts +2 -2
- package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
- package/mcp-server/src/services/memory-sessions.ts +24 -3
- package/mcp-server/src/services/neural-interface.ts +81 -21
- package/mcp-server/src/services/server-instructions.ts +29 -64
- package/mcp-server/src/services/tool-catalog.ts +403 -0
- package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
- package/mcp-server/src/services/tool-list.ts +410 -0
- package/mcp-server/src/services/tool-runtime.ts +457 -0
- package/mcp-server/src/services/typesafe-config.ts +1 -1
- package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
- package/mcp-server/src/tools/agents.ts +29 -20
- package/mcp-server/src/tools/choice.ts +17 -12
- package/mcp-server/src/tools/git-tools.ts +2 -2
- package/mcp-server/src/tools/image-tools.ts +1 -1
- package/mcp-server/src/tools/morelogin.ts +6 -4
- package/mcp-server/src/tools/skill-tools.ts +592 -0
- package/mcp-server/src/tools/skill.ts +13 -0
- package/mcp-server/src/tools/tool-groups.ts +164 -0
- package/mcp-server/src/tools/whiteboard-format.ts +9 -3
- package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
- package/neural-interface/i18n/en.json +3953 -228
- package/neural-interface/i18n/pt-BR.json +3901 -179
- package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
- package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
- package/neural-interface/lib/antigravity/acp-connection.js +299 -0
- package/neural-interface/lib/antigravity/bypass.js +100 -0
- package/neural-interface/lib/antigravity/catalog.js +273 -0
- package/neural-interface/lib/antigravity/home.js +260 -0
- package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
- package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
- package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
- package/neural-interface/lib/antigravity/policy.js +74 -0
- package/neural-interface/lib/antigravity/tool-call.js +178 -0
- package/neural-interface/lib/antigravity/transcripts.js +252 -0
- package/neural-interface/lib/antigravity/usage.js +43 -0
- package/neural-interface/lib/assistant-api.js +128 -28
- package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
- package/neural-interface/lib/assistant-brains/claude.js +4 -3
- package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
- package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
- package/neural-interface/lib/assistant-brains/codex.js +9 -5
- package/neural-interface/lib/assistant-brains/opencode.js +120 -54
- package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
- package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
- package/neural-interface/lib/assistant-budget-planner.js +7 -3
- package/neural-interface/lib/assistant-budget-split.js +42 -15
- package/neural-interface/lib/assistant-catalog.js +81 -15
- package/neural-interface/lib/assistant-config.js +6 -3
- package/neural-interface/lib/assistant-dispatch.js +1073 -200
- package/neural-interface/lib/assistant-envelope.js +156 -2
- package/neural-interface/lib/assistant-evidence.js +80 -0
- package/neural-interface/lib/assistant-hidden-models.js +7 -5
- package/neural-interface/lib/assistant-persona.js +93 -19
- package/neural-interface/lib/assistant-plan-permissions.js +25 -9
- package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
- package/neural-interface/lib/assistant-route-gate.js +6 -4
- package/neural-interface/lib/assistant-router.js +245 -38
- package/neural-interface/lib/assistant-runtime.js +214 -30
- package/neural-interface/lib/assistant-task-prompt.js +87 -21
- package/neural-interface/lib/assistant-tools.js +51 -0
- package/neural-interface/lib/assistant-usage.js +29 -0
- package/neural-interface/lib/backup-service.js +626 -4
- package/neural-interface/lib/backup-zip-reader.js +10 -5
- package/neural-interface/lib/backup-zip-writer.js +257 -0
- package/neural-interface/lib/browser-execution.js +10 -2
- package/neural-interface/lib/browser-guard-cdp.js +84 -0
- package/neural-interface/lib/browser-network-guard.js +476 -0
- package/neural-interface/lib/browser-policy-roots.js +64 -0
- package/neural-interface/lib/browser-tool-policy.js +198 -38
- package/neural-interface/lib/claude-agent-bridge.js +11 -2
- package/neural-interface/lib/claude-history-cache.js +210 -0
- package/neural-interface/lib/claude-hooks-status.js +103 -0
- package/neural-interface/lib/claude-model-catalog.js +5 -1
- package/neural-interface/lib/claude-temporary.js +5 -11
- package/neural-interface/lib/cli-install-api.js +1 -1
- package/neural-interface/lib/cli-install.js +163 -154
- package/neural-interface/lib/cli-resolver.js +13 -3
- package/neural-interface/lib/cli-update-planner.js +52 -41
- package/neural-interface/lib/cloudflared/tunnel.js +42 -2
- package/neural-interface/lib/codex-mcp-approvals.js +11 -0
- package/neural-interface/lib/codex-rollout-reader.js +52 -0
- package/neural-interface/lib/codex-worker-app-server.js +191 -0
- package/neural-interface/lib/confined-fs.js +30 -3
- package/neural-interface/lib/crash-notice-api.js +98 -0
- package/neural-interface/lib/editor-completion-opencode.js +68 -68
- package/neural-interface/lib/editor-git-api.js +3 -20
- package/neural-interface/lib/exec-loop-command.js +13 -10
- package/neural-interface/lib/file-explorer-api.js +74 -40
- package/neural-interface/lib/file-explorer-watch.js +9 -3
- package/neural-interface/lib/git-bash/install.js +52 -6
- package/neural-interface/lib/git-bash/manager.js +59 -17
- package/neural-interface/lib/git-bash/pins.js +12 -0
- package/neural-interface/lib/git-read.js +2 -2
- package/neural-interface/lib/git-status-runner.js +390 -0
- package/neural-interface/lib/git-status-watchdog.js +40 -0
- package/neural-interface/lib/greeting/budgets.js +1 -0
- package/neural-interface/lib/greeting/engine.js +2 -2
- package/neural-interface/lib/greeting/sources.js +10 -12
- package/neural-interface/lib/greeting/writer.js +1 -1
- package/neural-interface/lib/http-guards.js +8 -2
- package/neural-interface/lib/install-details.js +4 -1
- package/neural-interface/lib/mcp-agent-servers.js +67 -0
- package/neural-interface/lib/mcp-client-config.js +35 -33
- package/neural-interface/lib/mcp-external-servers.js +332 -0
- package/neural-interface/lib/mcp-installer.js +2 -1
- package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
- package/neural-interface/lib/memory-session-capture.js +17 -3
- package/neural-interface/lib/morelogin.js +177 -25
- package/neural-interface/lib/native-loop-providers.js +965 -66
- package/neural-interface/lib/native-loop-runtime.js +290 -42
- package/neural-interface/lib/opencode-config.js +557 -0
- package/neural-interface/lib/opencode-db-schema.js +17 -34
- package/neural-interface/lib/opencode-history.js +48 -218
- package/neural-interface/lib/opencode-mcp-ready.js +108 -0
- package/neural-interface/lib/opencode-native-boundary.js +53 -0
- package/neural-interface/lib/opencode-protocol.js +249 -112
- package/neural-interface/lib/opencode-runtime-config.js +32 -22
- package/neural-interface/lib/opencode-serve-records.js +39 -21
- package/neural-interface/lib/opencode-v2-client.js +48 -153
- package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
- package/neural-interface/lib/opencode-worker-data.js +27 -0
- package/neural-interface/lib/opencode2-client.js +73 -37
- package/neural-interface/lib/opencode2-proxy.js +23 -63
- package/neural-interface/lib/opencode2-translate.js +51 -2
- package/neural-interface/lib/owner-authority.js +249 -0
- package/neural-interface/lib/page-session.js +275 -0
- package/neural-interface/lib/path-confine.js +12 -9
- package/neural-interface/lib/peer-identity.js +793 -0
- package/neural-interface/lib/pty-host/core.js +28 -1
- package/neural-interface/lib/pty-host/lifetime.js +12 -0
- package/neural-interface/lib/pty-host/manager.js +12 -1
- package/neural-interface/lib/remote-policy.js +1 -1
- package/neural-interface/lib/request-gate.js +20 -2
- package/neural-interface/lib/rulesets/api.js +7 -6
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +180 -36
- package/neural-interface/lib/rulesets/json-edit.js +1 -1
- package/neural-interface/lib/rulesets/render.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +81 -31
- package/neural-interface/lib/session-title-generator.js +58 -36
- package/neural-interface/lib/skills/api-v2.js +495 -0
- package/neural-interface/lib/skills/bundle.js +418 -0
- package/neural-interface/lib/skills/catalog.js +298 -0
- package/neural-interface/lib/skills/creation-api.js +191 -0
- package/neural-interface/lib/skills/creation-brief.js +247 -0
- package/neural-interface/lib/skills/creation-sources.js +285 -0
- package/neural-interface/lib/skills/creation.js +843 -0
- package/neural-interface/lib/skills/deploy.js +569 -0
- package/neural-interface/lib/skills/digest.js +119 -0
- package/neural-interface/lib/skills/discover.js +111 -0
- package/neural-interface/lib/skills/draft-confinement.js +216 -0
- package/neural-interface/lib/skills/eval-confinement.js +399 -0
- package/neural-interface/lib/skills/eval-sandbox.js +280 -0
- package/neural-interface/lib/skills/eval-trace.js +135 -0
- package/neural-interface/lib/skills/evals-api.js +212 -0
- package/neural-interface/lib/skills/evals.js +1573 -0
- package/neural-interface/lib/skills/export.js +203 -0
- package/neural-interface/lib/skills/frontmatter.js +64 -0
- package/neural-interface/lib/skills/health.js +75 -0
- package/neural-interface/lib/skills/json-edit.js +165 -0
- package/neural-interface/lib/skills/known-bridge-digests.json +441 -0
- package/neural-interface/lib/skills/known-bridges.js +96 -0
- package/neural-interface/lib/skills/library.js +823 -0
- package/neural-interface/lib/skills/local-store.js +21 -0
- package/neural-interface/lib/skills/migration.js +433 -0
- package/neural-interface/lib/skills/operations.js +45 -0
- package/neural-interface/lib/skills/ownership.js +427 -0
- package/neural-interface/lib/skills/packages.js +88 -0
- package/neural-interface/lib/skills/reviews.js +212 -0
- package/neural-interface/lib/skills/revision.js +80 -0
- package/neural-interface/lib/skills/roots.js +269 -0
- package/neural-interface/lib/skills/runtime-checks.js +358 -0
- package/neural-interface/lib/skills/scan-assets.js +252 -0
- package/neural-interface/lib/skills/scan-context.js +26 -0
- package/neural-interface/lib/skills/scan-shell.js +324 -0
- package/neural-interface/lib/skills/scan.js +364 -0
- package/neural-interface/lib/skills/scanners.js +54 -0
- package/neural-interface/lib/skills/service.js +530 -0
- package/neural-interface/lib/skills/telemetry.js +119 -0
- package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
- package/neural-interface/lib/synabun-version.js +1 -1
- package/neural-interface/lib/system-activity.js +1 -1
- package/neural-interface/lib/tool-lists.js +124 -0
- package/neural-interface/lib/whatsapp/brain.js +7 -3
- package/neural-interface/lib/whatsapp/bridge.js +6 -3
- package/neural-interface/lib/whatsapp/cards.js +22 -9
- package/neural-interface/lib/whatsapp/config.js +1 -1
- package/neural-interface/lib/worker-confinement.js +1268 -0
- package/neural-interface/lib/worker-guard-setting.js +56 -0
- package/neural-interface/lib/worker-mcp-channel.js +175 -0
- package/neural-interface/lib/worker-question.js +74 -0
- package/neural-interface/lib/worker-relay-ports.js +227 -0
- package/neural-interface/lib/worker-resume.js +238 -0
- package/neural-interface/lib/worker-sandbox.js +329 -0
- package/neural-interface/package-lock.json +34 -12
- package/neural-interface/package.json +3 -2
- package/neural-interface/public/index.html +3 -1
- package/neural-interface/public/index2d.html +19 -11
- package/neural-interface/public/onboarding.html +3 -3
- package/neural-interface/public/shared/account-menu.js +8 -5
- package/neural-interface/public/shared/agent-runtime-options.js +125 -31
- package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
- package/neural-interface/public/shared/agy/agy-client.js +266 -0
- package/neural-interface/public/shared/agy/agy-commands.js +120 -0
- package/neural-interface/public/shared/agy/agy-copy.js +326 -0
- package/neural-interface/public/shared/agy/agy-diff.js +161 -0
- package/neural-interface/public/shared/agy/agy-events.js +469 -0
- package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
- package/neural-interface/public/shared/agy/agy-options.js +232 -0
- package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
- package/neural-interface/public/shared/agy/agy-render.js +392 -0
- package/neural-interface/public/shared/agy/agy-setup.js +149 -0
- package/neural-interface/public/shared/agy/agy-styles.js +604 -0
- package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
- package/neural-interface/public/shared/api.js +1527 -1336
- package/neural-interface/public/shared/app-frame.js +192 -0
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +36 -6
- package/neural-interface/public/shared/assistant/asst-api.js +1 -1
- package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
- package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
- package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +105 -44
- package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
- package/neural-interface/public/shared/assistant/asst-control.js +205 -26
- package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
- package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
- package/neural-interface/public/shared/assistant/asst-limit-dock.js +45 -0
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +327 -0
- package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
- package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
- package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
- package/neural-interface/public/shared/assistant/asst-panel.js +151 -19
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
- package/neural-interface/public/shared/assistant/asst-render.js +445 -82
- package/neural-interface/public/shared/assistant/asst-route.js +304 -53
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
- package/neural-interface/public/shared/assistant/asst-scenes.js +56 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +117 -10
- package/neural-interface/public/shared/assistant/asst-slash.js +17 -9
- package/neural-interface/public/shared/assistant/asst-state.js +87 -24
- package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +93 -21
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +190 -23
- package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
- package/neural-interface/public/shared/automation-context.js +5 -2
- package/neural-interface/public/shared/cc-dropdown.js +89 -19
- package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
- package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
- package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
- package/neural-interface/public/shared/cdx/cdx-output.js +34 -10
- package/neural-interface/public/shared/cdx/cdx-panel.js +59 -151
- package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +39 -11
- package/neural-interface/public/shared/cdx/cdx-render.js +57 -38
- package/neural-interface/public/shared/cdx/cdx-requests.js +14 -4
- package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
- package/neural-interface/public/shared/cdx/cdx-tabs.js +127 -44
- package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
- package/neural-interface/public/shared/cli-install-controller.js +218 -0
- package/neural-interface/public/shared/cli-install-view.js +11 -12
- package/neural-interface/public/shared/cli-status.js +11 -10
- package/neural-interface/public/shared/constants.js +5 -4
- package/neural-interface/public/shared/context-menu-areas.js +34 -0
- package/neural-interface/public/shared/context-menu-logic.js +243 -0
- package/neural-interface/public/shared/context-menu-model.js +367 -0
- package/neural-interface/public/shared/context-menu.js +372 -0
- package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
- package/neural-interface/public/shared/cp/cp-restore.js +162 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
- package/neural-interface/public/shared/crash-notice-view.js +168 -0
- package/neural-interface/public/shared/desktop-bridge.js +17 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
- package/neural-interface/public/shared/desktop-titlebar.js +13 -4
- package/neural-interface/public/shared/desktop-update-view.js +21 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
- package/neural-interface/public/shared/explorer/fx-api.js +4 -4
- package/neural-interface/public/shared/explorer/fx-links.js +3 -3
- package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +48 -16
- package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
- package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
- package/neural-interface/public/shared/html-shell.js +30 -15
- package/neural-interface/public/shared/i18n.js +2 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +74 -259
- package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +13 -22
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
- package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
- package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -21
- package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +60 -50
- package/neural-interface/public/shared/page-session.js +96 -0
- package/neural-interface/public/shared/provider-icons.js +4 -2
- package/neural-interface/public/shared/registry.js +5 -3
- package/neural-interface/public/shared/retired-2d.js +3 -0
- package/neural-interface/public/shared/schedules-view.js +195 -61
- package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
- package/neural-interface/public/shared/settings/settings-data.js +3 -1
- package/neural-interface/public/shared/settings/settings-ia.js +6 -6
- package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
- package/neural-interface/public/shared/settings/settings-rules.js +2 -2
- package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
- package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
- package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
- package/neural-interface/public/shared/showcase-privacy.js +6 -1
- package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
- package/neural-interface/public/shared/skills/ss-args.js +96 -0
- package/neural-interface/public/shared/skills/ss-creation.js +222 -0
- package/neural-interface/public/shared/skills/ss-dialog.js +133 -0
- package/neural-interface/public/shared/skills/ss-doc.js +173 -0
- package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
- package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
- package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
- package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
- package/neural-interface/public/shared/skills/ss-evals.js +417 -0
- package/neural-interface/public/shared/skills/ss-form.js +159 -0
- package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
- package/neural-interface/public/shared/skills/ss-ids.js +32 -0
- package/neural-interface/public/shared/skills/ss-lint.js +149 -0
- package/neural-interface/public/shared/skills/ss-presets.js +117 -0
- package/neural-interface/public/shared/skills/ss-profiles.js +513 -0
- package/neural-interface/public/shared/skills/ss-resources.js +165 -0
- package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
- package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
- package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
- package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
- package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
- package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
- package/neural-interface/public/shared/skills/studio/sx-create-ai.js +237 -0
- package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
- package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
- package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
- package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
- package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
- package/neural-interface/public/shared/skills/studio/sx-dom.js +230 -0
- package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
- package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
- package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
- package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
- package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
- package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
- package/neural-interface/public/shared/skills/studio/sx-install-flow.js +282 -0
- package/neural-interface/public/shared/skills/studio/sx-install.js +313 -0
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +748 -0
- package/neural-interface/public/shared/skills/studio/sx-library.js +479 -0
- package/neural-interface/public/shared/skills/studio/sx-main.js +690 -0
- package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
- package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
- package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
- package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
- package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
- package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
- package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
- package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
- package/neural-interface/public/shared/skills/studio/sx-subject.js +249 -0
- package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
- package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
- package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
- package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +426 -0
- package/neural-interface/public/shared/state.js +1 -1
- package/neural-interface/public/shared/streaming-markdown.js +58 -0
- package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
- package/neural-interface/public/shared/styles.css +1822 -1756
- package/neural-interface/public/shared/synabun-mascot.js +1115 -9
- package/neural-interface/public/shared/text-cache.js +29 -0
- package/neural-interface/public/shared/tool-list-picker.js +55 -0
- package/neural-interface/public/shared/tool-list-storage.js +24 -0
- package/neural-interface/public/shared/tool-list-view.js +112 -0
- package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
- package/neural-interface/public/shared/tour/tour-actor.js +268 -0
- package/neural-interface/public/shared/tour/tour-beats.js +336 -0
- package/neural-interface/public/shared/tour/tour-engine.js +884 -0
- package/neural-interface/public/shared/tour/tour-ink.js +390 -0
- package/neural-interface/public/shared/tour/tour-stage.js +440 -0
- package/neural-interface/public/shared/tour/tour-state.js +40 -0
- package/neural-interface/public/shared/tour/tour-tips.js +99 -0
- package/neural-interface/public/shared/tour/tour.css +144 -0
- package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
- package/neural-interface/public/shared/ui-assistant.js +2 -0
- package/neural-interface/public/shared/ui-automation-studio.js +111 -140
- package/neural-interface/public/shared/ui-claude-panel.js +376 -82
- package/neural-interface/public/shared/ui-cli-banner.js +23 -167
- package/neural-interface/public/shared/ui-command-runner.js +6 -3
- package/neural-interface/public/shared/ui-context-menu.js +295 -0
- package/neural-interface/public/shared/ui-cost-widget.js +57 -59
- package/neural-interface/public/shared/ui-crash-notice.js +187 -0
- package/neural-interface/public/shared/ui-detail.js +15 -3
- package/neural-interface/public/shared/ui-file-explorer.js +48 -6
- package/neural-interface/public/shared/ui-file-links.js +2 -2
- package/neural-interface/public/shared/ui-games.js +2 -2
- package/neural-interface/public/shared/ui-image-gallery.js +50 -47
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +33 -2
- package/neural-interface/public/shared/ui-link.js +3 -3
- package/neural-interface/public/shared/ui-menubar.js +10 -20
- package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +11 -1
- package/neural-interface/public/shared/ui-navbar.js +49 -24
- package/neural-interface/public/shared/ui-notifications.js +1 -0
- package/neural-interface/public/shared/ui-panels.js +96 -65
- package/neural-interface/public/shared/ui-resume.js +9 -7
- package/neural-interface/public/shared/ui-schedules-studio.js +546 -431
- package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
- package/neural-interface/public/shared/ui-sessions.js +27 -15
- package/neural-interface/public/shared/ui-settings.js +683 -526
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +4 -4
- package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
- package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
- package/neural-interface/public/shared/ui-sidepanel-windows.js +18 -13
- package/neural-interface/public/shared/ui-skills.js +33 -2830
- package/neural-interface/public/shared/ui-styleguide.js +5 -2
- package/neural-interface/public/shared/ui-sync.js +15 -5
- package/neural-interface/public/shared/ui-terminal.js +270 -190
- package/neural-interface/public/shared/ui-tool-updates.js +6 -4
- package/neural-interface/public/shared/ui-tooltip.js +81 -51
- package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
- package/neural-interface/public/shared/ui-trash.js +6 -4
- package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
- package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
- package/neural-interface/public/shared/ui-tutorial.css +47 -100
- package/neural-interface/public/shared/ui-tutorial.js +285 -509
- package/neural-interface/public/shared/ui-update.js +38 -2
- package/neural-interface/public/shared/ui-whiteboard.js +312 -59
- package/neural-interface/public/shared/ui-worker-end.js +5 -1
- package/neural-interface/public/shared/ui-workspaces.js +3 -2
- package/neural-interface/public/shared/view-variant.js +66 -0
- package/neural-interface/public/shared/whatsapp/wa-view.js +3 -3
- package/neural-interface/public/shared/whiteboard-list.js +172 -0
- package/neural-interface/public/shared/worker-end.js +10 -3
- package/neural-interface/public/shared/worker-questions.js +34 -0
- package/neural-interface/public/shared/worker-restart.js +12 -0
- package/neural-interface/public/sidepanel-session.html +2 -2
- package/neural-interface/public/variant/2d/background.js +2 -1
- package/neural-interface/public/variant/2d/context-menu.js +8 -2
- package/neural-interface/public/variant/2d/graph.js +42 -20
- package/neural-interface/public/variant/2d/hulls.js +14 -9
- package/neural-interface/public/variant/2d/lasso.js +11 -7
- package/neural-interface/public/variant/2d/main.js +32 -1
- package/neural-interface/public/variant/2d/minimap.js +11 -13
- package/neural-interface/public/variant/3d/main.js +69 -6
- package/neural-interface/public/variant/3d/map-controls.js +24 -9
- package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
- package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
- package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
- package/neural-interface/public/vendor/caveat/caveat.css +17 -0
- package/neural-interface/public/vendor/caveat/manifest.json +12 -0
- package/neural-interface/public/vendor/yaml/LICENSE +13 -0
- package/neural-interface/public/vendor/yaml/manifest.json +4 -0
- package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
- package/neural-interface/scripts/build-vendor.mjs +32 -0
- package/neural-interface/server.js +4553 -2855
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/neural-interface/templates/rulesets/core.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +23 -5
- package/neural-interface/templates/rulesets/manifest.json +8 -8
- package/npm-shrinkwrap.json +34 -12
- package/package.json +17 -3
- package/postinstall.js +9 -4
- package/preuninstall.js +1 -1
- package/setup.js +97 -322
- package/skills/skill-forge/SKILL.md +177 -0
- package/skills/skill-forge/evals/evals.json +54 -0
- package/skills/skill-forge/references/package.md +105 -0
- package/skills/skill-forge/references/review-and-recovery.md +63 -0
- package/skills/skill-forge/references/runtime-profiles.md +20 -0
- package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
- package/skills/synabun/SKILL.md +10 -7
- package/skills/synabun/modules/audit.md +1 -5
- package/skills/synabun/modules/changelog.md +1 -5
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/memorize.md +6 -9
- package/skills/synabun/modules/schedule.md +6 -6
- package/mcp-server/dist/services/profiles.d.ts +0 -120
- package/mcp-server/dist/services/profiles.d.ts.map +0 -1
- package/mcp-server/dist/services/profiles.js +0 -387
- package/mcp-server/dist/services/profiles.js.map +0 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
- package/mcp-server/dist/tools/leonardo.d.ts +0 -13
- package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
- package/mcp-server/dist/tools/leonardo.js +0 -21
- package/mcp-server/dist/tools/leonardo.js.map +0 -1
- package/mcp-server/dist/tools/profile.d.ts +0 -17
- package/mcp-server/dist/tools/profile.d.ts.map +0 -1
- package/mcp-server/dist/tools/profile.js +0 -114
- package/mcp-server/dist/tools/profile.js.map +0 -1
- package/mcp-server/src/services/profiles.ts +0 -428
- package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
- package/mcp-server/src/tools/leonardo.ts +0 -30
- package/mcp-server/src/tools/profile.ts +0 -122
- package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
- package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
- package/neural-interface/lib/mcp-profile-servers.js +0 -120
- package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
- package/skills/leonardo/SKILL.md +0 -168
- package/skills/leonardo/icon.svg +0 -1
- package/skills/leonardo/modules/browser-guide.md +0 -378
- package/skills/leonardo/modules/image-prompter.md +0 -283
- package/skills/leonardo/modules/model-advisor.md +0 -100
- package/skills/leonardo/modules/prompt-library.md +0 -110
- package/skills/leonardo/modules/style-guide.md +0 -186
- package/skills/leonardo/modules/video-prompter.md +0 -317
- /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
|
@@ -1,16 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { codexWorkerReply, CODEX_QUESTIONS, elicitationReply, openCodeAnswers, workerQuestions } from './worker-question.js';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { externalServerInventory, selectExternalServers, codexMcpEnabledOverride, referenceClaudeMap, waitForClaudeMcpScope, redactMcpValues, writePrivateMcpConfig, mcpDefinitionValues } from './mcp-external-servers.js';
|
|
5
|
+
import { resolveStoredTools, runtimeToolsValue, readDefaultTools } from './tool-lists.js';
|
|
6
|
+
import { waitForSynabunTools, synabunToolsTimeoutText } from './opencode-mcp-ready.js';
|
|
7
|
+
import { config as memoryConfig, detectProject as memoryProject } from '../../mcp-server/dist/config.js';
|
|
2
8
|
import { Codex } from '@openai/codex-sdk';
|
|
9
|
+
import { CodexWorkerAppServer } from './codex-worker-app-server.js';
|
|
3
10
|
import { query as claudeQuery } from '@anthropic-ai/claude-agent-sdk';
|
|
4
11
|
import { CLAUDE_EFFORT_LEVELS } from './claude-model-catalog.js';
|
|
5
12
|
import { CLAUDE_NOT_INSTALLED, resolveClaudeSdkExecutable } from './claude-executable.js';
|
|
6
13
|
import { extractOpenCodeText } from './session-title-generator.js';
|
|
14
|
+
import { withServeEnv } from './opencode-protocol.js';
|
|
7
15
|
import { patchCodexLineSplitting } from './jsonl-lines.js';
|
|
8
|
-
import { dirname } from 'node:path';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
9
17
|
import { codexReasoningEffort } from './effort-levels.js';
|
|
18
|
+
import { spawn as spawnProcess } from 'node:child_process';
|
|
10
19
|
import { codexStreamMedia, collectCodexMedia, openCodeMediaPart, ownCodexImages } from './assistant-media.js';
|
|
11
20
|
import { browserToolDenial } from './browser-tool-policy.js';
|
|
12
21
|
import { CODEX_BROWSER_POLICY_UNTRUSTED_NOTE, codexBrowserPolicyHook } from './assistant-route-gate-codex.js';
|
|
13
22
|
import { codexWindowsSandbox, codexWindowsSandboxConfig } from './agent-sandbox-elevation.js';
|
|
23
|
+
import { DRAFT_DISALLOWED_TOOLS, DRAFT_TOOLS, codexDraftOptions, draftOpenCodeDecision, draftPreToolUseHook, draftToolDecision, openCodeDraftConfig, resolveDraftConfinement } from './skills/draft-confinement.js';
|
|
24
|
+
import { EVAL_CONFINEMENT, codexEvalOptions, evalClaudeOptions, evalGitSafetyEnv, evalOpenCodeDecision, evalPreToolUseHook, evalToolDecision, openCodeEvalConfig, openCodeEvalEnv, placeOpenCodeEvalSkill, resolveEvalConfinement } from './skills/eval-confinement.js';
|
|
25
|
+
import { WORKER_GUARD_DEFAULT, acpPermissionDecision, codexSandboxFor, confinementEvent, openCodeCodeModeDecision, openCodeCodeModeNote, openCodePermissionDecision, openCodePermissionsFor, resolveWorkerConfinement, workerShellDecision, workerToolDecision } from './worker-confinement.js';
|
|
26
|
+
import { createAcpConnection } from './antigravity/acp-connection.js';
|
|
27
|
+
import { acpTurnUsage } from './antigravity/usage.js';
|
|
28
|
+
import { assertSafeAntigravityProject, antigravityHomeReadPolicy, antigravityHomeSettings, policyHookCommand, prepareAntigravityHome, toAntigravityMcpEntry, userAntigravityMcpServers, userAntigravitySettings, userGeminiDir } from './antigravity/home.js';
|
|
29
|
+
import { acpCall, acpPolicyInput, acpReadOnlyKind, acpToolName, acpToolPaths, acpWebTool } from './antigravity/tool-call.js';
|
|
30
|
+
import { splitAntigravityModel } from './antigravity/catalog.js';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
|
|
33
|
+
function nativeToolSelection(options, provider) { return resolveStoredTools(options, { source: options.runMode === 'task' ? 'dispatch' : 'launch', provider, defaultTools: options.runMode === 'task' ? undefined : readDefaultTools(memoryConfig.dataHome) }); }
|
|
14
34
|
|
|
15
35
|
function stringEnv(source) {
|
|
16
36
|
const out = {};
|
|
@@ -112,6 +132,12 @@ export function normalizeOpenCodeModel(model) {
|
|
|
112
132
|
export const PERMISSION_POLICIES = new Set(['auto', 'ask', 'restricted']);
|
|
113
133
|
export const CAPABILITIES = new Set(['read-only', 'workspace', 'full']);
|
|
114
134
|
export const CLAUDE_READ_ONLY_DISALLOWED_TOOLS = ['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'Bash'];
|
|
135
|
+
// The owner's live data home (its real memory DB). A worker's shell must never inherit these: a temp server or a
|
|
136
|
+
// test suite started from the worker would otherwise open the owner's real store (a reviewer's temp boot did,
|
|
137
|
+
// briefly). The worker's own SynaBun MCP child gets the DB through its own env block / DOTENV_PATH (server.js
|
|
138
|
+
// buildCodexMcpEnv, the OpenCode serve config), not from the shell; the Claude worker's MCP is in-process HTTP and
|
|
139
|
+
// needs nothing here. Stripped for every worker (guarded and full alike): a worker never opens the live DB directly.
|
|
140
|
+
export const LIVE_DATA_ENV = Object.freeze(['SQLITE_DB_PATH', 'MEMORY_DATA_DIR']);
|
|
115
141
|
|
|
116
142
|
export function normalizePermissionPolicy(policy) {
|
|
117
143
|
const value = String(policy || 'auto').trim().toLowerCase();
|
|
@@ -136,6 +162,99 @@ function policyUnsupportedError(provider, policy) {
|
|
|
136
162
|
return error;
|
|
137
163
|
}
|
|
138
164
|
|
|
165
|
+
/** A confined run (lib/skills/draft-confinement.js) that cannot be set up on its provider: it never starts unconfined. */
|
|
166
|
+
function confinementUnsupportedError(provider, reason, what = 'a drafting folder') {
|
|
167
|
+
const error = new Error(`${provider} cannot run confined to ${what}${reason ? ` (${reason})` : ''}: the run did not start.`);
|
|
168
|
+
error.code = 'CONFINEMENT_UNSUPPORTED';
|
|
169
|
+
return error;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Claude's built-in writers, kept off the tool list of an enforced read-only run (the hook refuses them too; Bash is the hook's). */
|
|
173
|
+
export const CLAUDE_READ_ONLY_WRITE_TOOLS = Object.freeze(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The worker confinement of a run (lib/worker-confinement.js), or null: capability full, the Protect workers
|
|
177
|
+
* setting off, or a skill-drafting run (which has its own, stricter confinement). Throws when the working
|
|
178
|
+
* directory cannot be resolved: the run must not start on a guess.
|
|
179
|
+
*/
|
|
180
|
+
function workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement, serverPort = null, localPorts = null, localPortRange = null }) {
|
|
181
|
+
if (confinement) return null;
|
|
182
|
+
// Never rooted where the server runs: a run without a cwd is rooted at its scratch folder, or does not start.
|
|
183
|
+
return resolveWorkerConfinement({ capability, cwd: cwd || null, mode: guardMode, scratchDir: scratchDir || null, serverPort, localPorts, localPortRange });
|
|
184
|
+
}
|
|
185
|
+
/** The port of SynaBun's own server, from a run's MCP address (a guarded worker's shell may not call it). */
|
|
186
|
+
function serverPortOf(mcpUrl, fallback = null) {
|
|
187
|
+
try { const port = Number(new URL(String(mcpUrl)).port); if (port > 0) return port; } catch { /* no address */ }
|
|
188
|
+
return Number(fallback) > 0 ? Number(fallback) : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* What an OpenCode run's own config carries for its confinement (server.js setupOpencodeLoopConfig): null for an
|
|
193
|
+
* unguarded run (today's rules), else { permissions (OpenCode 2's rules), mcp: 'off'?, directMcp: true? }. A
|
|
194
|
+
* skill-drafting run has no shell, web, subagent or skill tool and no MCP server; a guarded worker has the rules its
|
|
195
|
+
* adapter answers by policy, and under Enforce no Code Mode: `directMcp` sets `codemode: false` on every MCP server of
|
|
196
|
+
* the run's config, which OpenCode 2.1.4 otherwise reaches only through Code Mode's `execute` (denied by the rules).
|
|
197
|
+
*/
|
|
198
|
+
export function openCodeRunConfigFor(state = {}) {
|
|
199
|
+
if (state.confinement?.kind === EVAL_CONFINEMENT) return openCodeEvalConfig({ role: state.confinement.role, shell: state.confinement.shell === true });
|
|
200
|
+
if (state.confinement) return openCodeDraftConfig();
|
|
201
|
+
const guard = workerGuardFor({ capability: normalizeCapability(state.capability), cwd: state.cwd, guardMode: state.guardMode ?? WORKER_GUARD_DEFAULT, scratchDir: state.scratchDir, confinement: null, serverPort: state.serverPort || null });
|
|
202
|
+
if (!guard) return null;
|
|
203
|
+
return { permissions: openCodePermissionsFor(guard), ...(guard.enforced ? { directMcp: true } : {}) };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Whether an OpenCode run is a protected worker: an Assistant task worker with capability read-only or workspace
|
|
208
|
+
* under Protect workers = Enforce (never a drafting or evaluation run, which have confinements of their own; never
|
|
209
|
+
* Warn, Off or full). Its serve reads no project configuration (openCodeRunEnvFor) and keeps its password out of every
|
|
210
|
+
* environment (server.js ensureIsolatedServe `hidePassword`: lib/opencode-protocol.js serveSpecFor passwordFrom
|
|
211
|
+
* 'stdout'). Throws like resolveWorkerConfinement when a guarded run has no folder.
|
|
212
|
+
*/
|
|
213
|
+
export function openCodeProtectedWorker(state = {}) {
|
|
214
|
+
if (state.confinement) return false;
|
|
215
|
+
const guard = workerGuardFor({ capability: normalizeCapability(state.capability), cwd: state.cwd, guardMode: state.guardMode ?? WORKER_GUARD_DEFAULT, scratchDir: state.scratchDir, confinement: null, serverPort: state.serverPort || null });
|
|
216
|
+
return !!guard?.enforced;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* What a protected worker's serve environment holds (openCodeProtectedWorker): no project configuration at all, so
|
|
221
|
+
* nothing the worker writes in its folder runs in the serve (no opencode.json with plugins or MCP servers, no
|
|
222
|
+
* .opencode plugin, agent, command or tool, no project AGENTS.md: OPENCODE_DISABLE_PROJECT_CONFIG, and
|
|
223
|
+
* OPENCODE_CONFIG_PROJECT_DISABLE too, which OpenCode 2 reads first), and no inherited OPENCODE_CONFIG_DIR (OpenCode
|
|
224
|
+
* reads that folder in place of the run's own configuration, whose rules, `codemode: false` and SynaBun MCP pin would
|
|
225
|
+
* then be gone). Every inherited GIT_* variable is removed too: repository, index and object paths could point git
|
|
226
|
+
* outside the folder, and GIT_CONFIG_PARAMETERS or GIT_CONFIG_COUNT/KEY_n/VALUE_n could run an fsmonitor or pager.
|
|
227
|
+
* `GIT_*: null` clears the prefix before deliberate settings are applied (lib/opencode-protocol.js withServeEnv).
|
|
228
|
+
*/
|
|
229
|
+
export const OPENCODE_PROTECTED_WORKER_ENV = Object.freeze({
|
|
230
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: '1', OPENCODE_CONFIG_PROJECT_DISABLE: '1', OPENCODE_CONFIG_DIR: null,
|
|
231
|
+
'GIT_*': null,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* What an OpenCode run's serve environment carries for its confinement (server.js ensureIsolatedServe): an evaluation
|
|
236
|
+
* run's (no project configuration, no file watcher, git's safety settings: lib/skills/eval-confinement.js
|
|
237
|
+
* openCodeEvalEnv), a protected worker's (OPENCODE_PROTECTED_WORKER_ENV), else null. A value of null removes that
|
|
238
|
+
* variable from the serve's environment. Throws when the evaluation folder cannot be resolved: the serve must not start
|
|
239
|
+
* unconfined.
|
|
240
|
+
*/
|
|
241
|
+
export function openCodeRunEnvFor(state = {}) {
|
|
242
|
+
if (state.confinement?.kind === EVAL_CONFINEMENT) return openCodeEvalEnv(resolveEvalConfinement(state.confinement, state.cwd));
|
|
243
|
+
if (openCodeProtectedWorker(state)) return { ...OPENCODE_PROTECTED_WORKER_ENV };
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* An evaluation trial with the skill: its copy placed in the run's own OpenCode configuration folder (`configDir`,
|
|
249
|
+
* server.js setupOpencodeLoopConfig's, removed with the run's serve), where OpenCode reads it as a global skill
|
|
250
|
+
* (lib/skills/eval-confinement.js placeOpenCodeEvalSkill). → the folder, or null (not an evaluation, a baseline, a
|
|
251
|
+
* grader). Throws when it cannot be placed: the run must not start without the skill it tests.
|
|
252
|
+
*/
|
|
253
|
+
export function openCodeEvalSkillFor(state = {}, configDir) {
|
|
254
|
+
if (state.confinement?.kind !== EVAL_CONFINEMENT) return null;
|
|
255
|
+
return placeOpenCodeEvalSkill(resolveEvalConfinement(state.confinement, state.cwd), configDir);
|
|
256
|
+
}
|
|
257
|
+
|
|
139
258
|
/** Final assistant text carried by a Codex SDK stream event, or null. */
|
|
140
259
|
export function codexAgentMessageText(event) {
|
|
141
260
|
if (!event) return null;
|
|
@@ -241,33 +360,55 @@ function codexStructuredOutput(text, schema) {
|
|
|
241
360
|
export async function createCodexNativeLoopAdapter(options = {}) {
|
|
242
361
|
const {
|
|
243
362
|
runId, cwd, model, effort, codexHome, codexPath, browserSessionId,
|
|
244
|
-
browserTabId,
|
|
363
|
+
browserTabId, onIdentity = () => {}, onEvent = () => {},
|
|
245
364
|
CodexClass = Codex,
|
|
246
|
-
extraEnv = {}, permissionPolicy: rawPolicy = 'auto', capability: rawCapability = 'full',
|
|
365
|
+
extraEnv = {}, permissionPolicy: rawPolicy = 'auto', permissionBroker = null, capability: rawCapability = 'full',
|
|
247
366
|
outputSchema = null, desktopGrantProvider = null, modelContextWindow = null, contextMode = null,
|
|
248
367
|
// Image creation runs (assistant dispatch): 'image' → each turn returns the images it generated.
|
|
249
368
|
collectMedia = null,
|
|
250
|
-
// Assistant task runs: the browser policy's hook and the run's notes (a function: never in the state file)
|
|
251
|
-
|
|
369
|
+
// Assistant task runs: the browser policy's hook and the run's notes (a function: never in the state file), and
|
|
370
|
+
// the registered code projects its test runners may also run in (lib/browser-policy-roots.js, read as the run starts).
|
|
371
|
+
runMode = 'loop', onNote = null, browserPolicyTrust = null, policyRoots = [],
|
|
252
372
|
// Inherited variables to remove before extraEnv applies (the Codex account's launch:
|
|
253
373
|
// lib/codex-accounts.js launchFor() — inherited OpenAI/Codex credentials never reach the run).
|
|
254
374
|
unsetEnv = [],
|
|
255
375
|
// The Admin sandbox setting when the run started (lib/agent-sandbox-elevation.js): the Windows
|
|
256
376
|
// sandbox every `codex exec` of the thread may use. The run keeps it while it lives.
|
|
257
377
|
adminSandbox = false,
|
|
378
|
+
// A skill-drafting run's confinement (lib/skills/draft-confinement.js codexDraftOptions): the sandbox around the
|
|
379
|
+
// workspace, no network, no MCP server.
|
|
380
|
+
confinement = null,
|
|
381
|
+
// The Protect workers setting as the run starts (lib/worker-confinement.js), the run's own scratch folder and
|
|
382
|
+
// SynaBun's own port (a guarded worker's shell may not call it).
|
|
383
|
+
guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null,
|
|
258
384
|
} = options;
|
|
259
385
|
const permissionPolicy = normalizePermissionPolicy(rawPolicy);
|
|
260
386
|
const capability = normalizeCapability(rawCapability);
|
|
387
|
+
// Resolved before anything starts: a confinement that cannot be set up stops the run (never an unconfined start).
|
|
388
|
+
let drafted = null;
|
|
389
|
+
let evaluated = null;
|
|
390
|
+
if (confinement?.kind === EVAL_CONFINEMENT) {
|
|
391
|
+
// A skill-evaluation run (lib/skills/eval-confinement.js): the sandbox around its job folder (read-only for a
|
|
392
|
+
// grader, a judge or a proposer), no network, no web search, every MCP server disabled.
|
|
393
|
+
let held;
|
|
394
|
+
try { held = resolveEvalConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('Codex', error?.message || error, 'an evaluation folder'); }
|
|
395
|
+
evaluated = { held, ...codexEvalOptions(held, { codexHome, projectDir: cwd }) };
|
|
396
|
+
} else if (confinement) {
|
|
397
|
+
let confined;
|
|
398
|
+
try { confined = resolveDraftConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('Codex', error?.message || error); }
|
|
399
|
+
drafted = codexDraftOptions(confined, { codexHome, projectDir: cwd });
|
|
400
|
+
}
|
|
401
|
+
// Either confinement's options (the thread's sandbox and the config's MCP switches): one shape.
|
|
402
|
+
const boxed = drafted || evaluated;
|
|
403
|
+
const guard = boxed ? null : workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: null, serverPort, localPortRange });
|
|
261
404
|
// "<id>[extended]" (the Assistant's extended-context row) runs <id> with the
|
|
262
405
|
// larger window the dispatcher read from the catalog; never silently at 272k.
|
|
263
406
|
const legacyExtended = /\[extended\]$/i.test(String(model || ''));
|
|
264
407
|
const extended = contextMode === 'extended' || legacyExtended;
|
|
265
408
|
const runModel = legacyExtended ? String(model).replace(/\[extended\]$/i, '') : model;
|
|
266
409
|
if (extended && !(Number(modelContextWindow) > 0)) throw new Error(`Extended context is unavailable for ${runModel}.`);
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
// offer 'restricted' instead.
|
|
270
|
-
if (permissionPolicy === 'ask') throw policyUnsupportedError('Codex', permissionPolicy);
|
|
410
|
+
// Legacy exec loops have no approval channel. Task workers use an app-server.
|
|
411
|
+
if (permissionPolicy === 'ask' && runMode !== 'task') throw policyUnsupportedError('Codex', permissionPolicy);
|
|
271
412
|
// Assistant task runs: SynaBun's browser policy as a PreToolUse hook the
|
|
272
413
|
// user's Codex trusts (the brain's route-gate mechanism), and no web search
|
|
273
414
|
// of Codex's own. Without a trusted hook the policy is instructions only
|
|
@@ -280,19 +421,41 @@ export async function createCodexNativeLoopAdapter(options = {}) {
|
|
|
280
421
|
}
|
|
281
422
|
const pins = stringEnv({
|
|
282
423
|
SYNABUN_TERMINAL_SESSION: runId,
|
|
424
|
+
SYNABUN_RUN_MODE: options.runMode === 'task' ? 'task' : undefined,
|
|
283
425
|
SYNABUN_PROJECT: cwd ? memoryProject(cwd) : 'global',
|
|
284
426
|
SYNABUN_BROWSER_SESSION: browserSessionId,
|
|
285
427
|
SYNABUN_BROWSER_TAB: browserTabId,
|
|
286
|
-
|
|
428
|
+
SYNABUN_TOOLS: runtimeToolsValue((() => { const list = nativeToolSelection(options, 'codex'); return list.items.includes('all') ? list.items : [...list.groups, ...list.servers]; })()),
|
|
287
429
|
SYNABUN_TOOL_CATALOG_MODE: 'deferred',
|
|
288
430
|
// Computer-use workers: the grant unlocks the `computer` group in the MCP child.
|
|
289
431
|
SYNABUN_DESKTOP_GRANT: typeof desktopGrantProvider === 'function' ? desktopGrantProvider() || undefined : undefined,
|
|
290
432
|
});
|
|
291
433
|
const inherited = { ...process.env };
|
|
434
|
+
for (const key of ['SYNABUN_PROFILE', 'SYNABUN_RUNTIME_PROFILE_PATH', 'SYNABUN_TOOLS', 'SYNABUN_TOOLS_PATH']) delete inherited[key];
|
|
292
435
|
for (const key of Array.isArray(unsetEnv) ? unsetEnv : []) delete inherited[key];
|
|
293
|
-
|
|
436
|
+
// A worker never holds a brain's launch credential (the Assistant's own identity) or an owner's page secret.
|
|
437
|
+
delete inherited.SYNABUN_BRAIN_TOKEN;
|
|
438
|
+
// The worker's shell must not inherit the owner's live-data paths (its MCP child gets them through its own
|
|
439
|
+
// config: server.js buildCodexMcpEnv). A worker that boots a temp server or runs the suite stays off the live DB.
|
|
440
|
+
for (const key of LIVE_DATA_ENV) delete inherited[key];
|
|
441
|
+
// The hook reads the run's project and the other projects its test runners may run in (a JSON list) from its env.
|
|
442
|
+
const policyEnv = browserPolicy ? { SYNABUN_BROWSER_POLICY_CWD: cwd || process.cwd(), SYNABUN_BROWSER_POLICY_ROOTS: JSON.stringify(Array.isArray(policyRoots) ? policyRoots : []) } : {};
|
|
443
|
+
const env = withServeEnv(stringEnv({ ...inherited, ...pins, CODEX_HOME: codexHome, ...policyEnv, ...stringEnv(extraEnv) }), evaluated ? evalGitSafetyEnv(evaluated.held.root) : null);
|
|
294
444
|
const config = { ...codexWindowsSandboxConfig(adminSandbox) };
|
|
295
|
-
if (Object.keys(pins).length) config.mcp_servers = { SynaBun: { env: pins } };
|
|
445
|
+
if (Object.keys(pins).length) config.mcp_servers = { SynaBun: { env: pins, ...(task ? { tool_timeout_sec: Math.ceil((options.questionTimeoutMs || 30 * 60_000) / 1000) + 30 } : {}) } };
|
|
446
|
+
if (!boxed) {
|
|
447
|
+
const inventory = externalServerInventory({ provider: 'codex', cwd, codexHome, env, home: env.HOME, dataHome: memoryConfig.dataHome, ...options.externalMcpContext });
|
|
448
|
+
const list = nativeToolSelection(options, 'codex');
|
|
449
|
+
const scope = selectExternalServers(list.servers, inventory);
|
|
450
|
+
// Leaf overrides preserve native transports/env. Codex treats quotes in a
|
|
451
|
+
// path literally; reject unaddressable names rather than launch a wider scope.
|
|
452
|
+
for (const name of scope.disabled) {
|
|
453
|
+
if (inventory.servers.find(row => row.name === name)?.reason === 'disabled-in-provider') continue;
|
|
454
|
+
config[codexMcpEnabledOverride(name)] = false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// A drafting or evaluation run: every MCP server disabled (SynaBun's keeps its pins beside the switch; they never start).
|
|
458
|
+
if (boxed) config.mcp_servers = { ...(config.mcp_servers || {}), ...Object.fromEntries(Object.entries(boxed.config.mcp_servers).map(([name, value]) => [name, { ...(config.mcp_servers?.[name] || {}), ...value }])) };
|
|
296
459
|
if (extended) config.model_context_window = Math.floor(Number(modelContextWindow));
|
|
297
460
|
if (browserPolicy) Object.assign(config, browserPolicy.config);
|
|
298
461
|
// The SDK is always told which Codex to start: the user's own installation.
|
|
@@ -303,7 +466,16 @@ export async function createCodexNativeLoopAdapter(options = {}) {
|
|
|
303
466
|
error.code = 'CODEX_NOT_INSTALLED';
|
|
304
467
|
throw error;
|
|
305
468
|
}
|
|
306
|
-
const
|
|
469
|
+
const WorkerCodex = task && !boxed && CodexClass === Codex ? CodexWorkerAppServer : CodexClass;
|
|
470
|
+
const codex = new WorkerCodex({
|
|
471
|
+
permissionPolicy,
|
|
472
|
+
onRequest: permissionBroker && !boxed ? async (packet, signal) => {
|
|
473
|
+
const question = CODEX_QUESTIONS.has(packet.method);
|
|
474
|
+
if (!question && permissionPolicy !== 'ask') return { error: { code: -32601, message: 'Unexpected worker approval request' } };
|
|
475
|
+
const reply = await permissionBroker.request({ kind: question ? 'ask' : 'tool', toolName: packet.method, input: packet.params, questions: workerQuestions(packet.params), protocol: { provider: 'codex', method: packet.method, id: packet.id }, signal });
|
|
476
|
+
return codexWorkerReply(packet, reply);
|
|
477
|
+
} : null,
|
|
478
|
+
onProcess: options.onProcess, signal: options.signal, apiKey: options.apiKey, configArgs: options.configArgs,
|
|
307
479
|
...(codexPath ? { codexPathOverride: codexPath } : {}),
|
|
308
480
|
env,
|
|
309
481
|
config,
|
|
@@ -312,23 +484,67 @@ export async function createCodexNativeLoopAdapter(options = {}) {
|
|
|
312
484
|
// U+2028 / U+2029: a command printing one ended the run with "Failed to parse item".
|
|
313
485
|
patchCodexLineSplitting(codex);
|
|
314
486
|
const parent = dirname(cwd || process.cwd());
|
|
315
|
-
|
|
487
|
+
// The sandbox: a drafting run's (the workspace, no network); an enforced guard's (lib/worker-confinement.js
|
|
488
|
+
// codexSandboxFor: read-only, or workspace-write with the working directory and the scratch folder, never the
|
|
489
|
+
// parent folder); else today's (the capability's mode, the parent folder writable too).
|
|
490
|
+
const sandboxed = guard?.enforced ? codexSandboxFor(guard, { parentDir: parent && parent !== cwd ? parent : null, scratchDir }) : null;
|
|
491
|
+
// A guarded Codex worker (Protect workers = Enforce) runs with network OFF in Codex's own sandbox. Codex's
|
|
492
|
+
// sandbox network is all-or-nothing (no per-host/loopback denial on this CLI), so SynaBun cannot wrap it in an
|
|
493
|
+
// outer Seatbelt (nested sandbox-exec is refused): turning the network off is the only way its shell commands
|
|
494
|
+
// (node -e, curl, a script) cannot reach SynaBun's own control port or any other local endpoint. The owner has
|
|
495
|
+
// accepted that read-only / workspace Codex workers lose internet (HTTPS, npm, git fetch) for their shell. The
|
|
496
|
+
// worker's SynaBun MCP child runs outside this command sandbox, so remember / recall / browser tools still work.
|
|
497
|
+
const codexNetworkOff = !!(boxed || guard?.enforced);
|
|
498
|
+
const threadOptions = {
|
|
316
499
|
workingDirectory: cwd || process.cwd(),
|
|
317
500
|
model: runModel || undefined,
|
|
318
501
|
modelReasoningEffort: normalizeCodexEffort(effort),
|
|
319
502
|
approvalPolicy: 'never',
|
|
320
|
-
sandboxMode: codexSandboxModeFor({ permissionPolicy, capability }),
|
|
503
|
+
sandboxMode: boxed ? boxed.thread.sandboxMode : sandboxed ? sandboxed.sandboxMode : codexSandboxModeFor({ permissionPolicy, capability }),
|
|
321
504
|
skipGitRepoCheck: true,
|
|
322
|
-
networkAccessEnabled:
|
|
323
|
-
...(task ? { webSearchMode: 'disabled' } : {}),
|
|
324
|
-
additionalDirectories: parent && parent !== cwd ? [parent] : undefined,
|
|
325
|
-
}
|
|
505
|
+
networkAccessEnabled: !codexNetworkOff,
|
|
506
|
+
...(task || boxed ? { webSearchMode: 'disabled' } : {}),
|
|
507
|
+
additionalDirectories: boxed ? undefined : sandboxed ? sandboxed.additionalDirectories : parent && parent !== cwd ? [parent] : undefined,
|
|
508
|
+
};
|
|
509
|
+
const thread = options.resumeThreadId ? codex.resumeThread(options.resumeThreadId, threadOptions) : codex.startThread(threadOptions);
|
|
510
|
+
try { await thread.ready?.(); } catch (error) { await codex.dispose?.(); throw error; }
|
|
511
|
+
if (thread.id) onIdentity({ providerThreadId: thread.id, providerSessionId: thread.id });
|
|
326
512
|
let alive = true;
|
|
327
513
|
let activeAbort = null;
|
|
328
514
|
let activeTurn = null;
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
|
|
515
|
+
// A guarded run says at its start what holds it: Codex's sandbox never reports to SynaBun by itself, so this is
|
|
516
|
+
// the one place the note is written before any command (the run's notes, then its events).
|
|
517
|
+
if (guard) {
|
|
518
|
+
try { onNote?.(`worker confinement (${guard.mode}, capability ${guard.capability}): Codex sandbox ${sandboxed ? sandboxed.sandboxMode : codexSandboxModeFor({ permissionPolicy, capability })}${guard.enforced ? `, writable: ${guard.writable.join(', ')}` : ' as before (warn: what Enforce would refuse is noted)'}`); } catch {}
|
|
519
|
+
// Network-off is the one way a Codex worker's shell cannot reach SynaBun's control port (Codex runs its own
|
|
520
|
+
// sandbox, which SynaBun cannot wrap, and its network is all-or-nothing). Say it plainly, once, at the start.
|
|
521
|
+
if (guard.enforced) { try { onNote?.('This protected Codex worker has NO internet for its shell commands: Codex\'s sandbox network is all-or-nothing, so to keep its commands off SynaBun\'s control port (and every local port) the network is off. HTTPS, npm install and git fetch will fail in the shell; your SynaBun MCP tools (remember, recall, browser) still work. A run that needs internet in its shell must use capability "full".'); } catch {} }
|
|
522
|
+
else { try { onNote?.('warn mode: Enforce would also turn this Codex worker\'s shell network off (no internet), so its commands could not reach SynaBun\'s control port.'); } catch {} }
|
|
523
|
+
}
|
|
524
|
+
if (drafted) { try { onNote?.('skill-draft confinement: Codex sandbox workspace-write around the drafting folder, no network, no web search, every MCP server disabled; the shell stays, inside the sandbox'); } catch {} }
|
|
525
|
+
if (evaluated) { try { onNote?.(`skill-eval confinement (${evaluated.held.role}): Codex sandbox ${evaluated.thread.sandboxMode} around the evaluation folder, no network, no web search, every MCP server disabled; the shell stays, inside the sandbox`); } catch {} }
|
|
526
|
+
/**
|
|
527
|
+
* A command Codex ran (item.completed command_execution): under warn, what Enforce would have refused is noted;
|
|
528
|
+
* under enforce, a sandbox denial in its output is explained in SynaBun's words (Codex's sandbox refuses by itself
|
|
529
|
+
* and says nothing to SynaBun).
|
|
530
|
+
*/
|
|
531
|
+
const noteCommand = (item, meta) => {
|
|
532
|
+
if (!guard || item?.type !== 'command_execution') return;
|
|
533
|
+
const command = typeof item.command === 'string' ? item.command : Array.isArray(item.command) ? item.command.join(' ') : '';
|
|
534
|
+
if (!command) return;
|
|
535
|
+
const decision = workerShellDecision(command, guard);
|
|
536
|
+
const output = String(item.aggregated_output || item.output || '');
|
|
537
|
+
const denied = Number(item.exit_code) !== 0 && /operation not permitted|permission denied|read-only file system|sandbox|EPERM|EACCES|EROFS/i.test(output);
|
|
538
|
+
if (!decision && !denied) return;
|
|
539
|
+
const event = decision
|
|
540
|
+
? confinementEvent(decision, { tool: 'shell', confinement: guard, provider: 'codex' })
|
|
541
|
+
: { type: 'synabun.confinement', action: 'refused', capability: guard.capability, tool: 'shell', rule: 'sandbox', reason: `Codex's sandbox refused "${command.slice(0, 160)}": this run (capability ${guard.capability}) writes inside ${guard.writable.join(', ')} only${guard.capability === 'read-only' ? ' (read-only: nothing at all, the temp folder included)' : ''}.`, provider: 'codex' };
|
|
542
|
+
if (decision && guard.enforced && !denied) event.action = 'noted'; // the sandbox decides; the heuristic's view is a note
|
|
543
|
+
onEvent({ provider: 'codex', runId, iteration: meta?.iteration, event });
|
|
544
|
+
};
|
|
545
|
+
// The sidepanel keys a turn's items by this sequence, including across a restart.
|
|
546
|
+
// Scheduled SDK turns restart their item IDs; app-server workers keep their thread IDs.
|
|
547
|
+
let providerTurn = Number(options.providerTurnBase) || 0;
|
|
332
548
|
// Generated images already returned by an earlier turn, and when the thread began (its rollout's day).
|
|
333
549
|
const mediaSeen = new Set();
|
|
334
550
|
let threadStartedMs = 0;
|
|
@@ -337,9 +553,13 @@ export async function createCodexNativeLoopAdapter(options = {}) {
|
|
|
337
553
|
identity: () => ({ providerThreadId: thread.id, providerSessionId: thread.id }),
|
|
338
554
|
isAlive: () => alive,
|
|
339
555
|
describe: () => ({
|
|
340
|
-
provider: 'codex', permissionPolicy, capability, sandboxMode: codexSandboxModeFor({ permissionPolicy, capability }),
|
|
556
|
+
provider: 'codex', permissionPolicy, capability, sandboxMode: boxed ? boxed.thread.sandboxMode : sandboxed ? sandboxed.sandboxMode : codexSandboxModeFor({ permissionPolicy, capability }),
|
|
557
|
+
networkOff: codexNetworkOff,
|
|
341
558
|
windowsSandbox: codexWindowsSandbox(adminSandbox),
|
|
342
559
|
...(task ? { browserPolicy: browserPolicy ? 'hook' : 'instructions' } : {}),
|
|
560
|
+
...(drafted ? { confinement: 'skill-draft', root: cwd } : {}),
|
|
561
|
+
...(evaluated ? { confinement: EVAL_CONFINEMENT, role: evaluated.held.role, root: cwd } : {}),
|
|
562
|
+
...(guard ? { guard: { mode: guard.mode, capability: guard.capability, writable: [...guard.writable] } } : {}),
|
|
343
563
|
}),
|
|
344
564
|
async runTurn(prompt, meta = {}) {
|
|
345
565
|
if (!alive) throw new Error('Codex native loop is closed');
|
|
@@ -377,6 +597,7 @@ export async function createCodexNativeLoopAdapter(options = {}) {
|
|
|
377
597
|
if (agentText !== null) text = agentText;
|
|
378
598
|
const saved = codexStreamMedia(event);
|
|
379
599
|
if (saved) media.push(saved);
|
|
600
|
+
if (event?.type === 'item.completed') { try { noteCommand(event.item, meta); } catch { /* a note, never the turn */ } }
|
|
380
601
|
if (event?.type === 'turn.failed') failure = codexFailureMessage(event.error);
|
|
381
602
|
if (event?.type === 'turn.completed') { completed = true; if (event.usage) usage = event.usage; }
|
|
382
603
|
if (event?.type === 'error') failure = codexFailureMessage(event);
|
|
@@ -415,6 +636,7 @@ export async function createCodexNativeLoopAdapter(options = {}) {
|
|
|
415
636
|
async dispose() {
|
|
416
637
|
alive = false;
|
|
417
638
|
if (activeAbort) activeAbort.abort(asAbortError('disposed'));
|
|
639
|
+
await codex.dispose?.();
|
|
418
640
|
},
|
|
419
641
|
};
|
|
420
642
|
}
|
|
@@ -438,28 +660,61 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
438
660
|
unsetEnv = [],
|
|
439
661
|
// The host's last word on the environment: on Windows the Git Bash it names (lib/git-bash/).
|
|
440
662
|
childEnv = null,
|
|
663
|
+
// A skill-drafting run (lib/skills/draft-confinement.js): file tools in its folder only, nothing else.
|
|
664
|
+
confinement = null,
|
|
665
|
+
// The Protect workers setting as the run starts (lib/worker-confinement.js), the run's own scratch folder and
|
|
666
|
+
// SynaBun's own port (a guarded worker's shell may not call it; read from mcpUrl when not given).
|
|
667
|
+
guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null,
|
|
668
|
+
// Assistant task runs: the registered code projects the browser policy lets test runners run in besides the
|
|
669
|
+
// run's own (lib/browser-policy-roots.js, read as the run starts).
|
|
670
|
+
policyRoots = [],
|
|
671
|
+
// A run resumed after a server restart (assistant-dispatch.js resumeRun): the Claude session to continue, and the
|
|
672
|
+
// cost counter the resumed CLI starts from (it restores its total from the transcript's last `cost-state` line,
|
|
673
|
+
// lib/worker-resume.js readCostState), so the first turn after it is charged only for itself.
|
|
674
|
+
resumeSessionId = null, costBaseUsd = 0,
|
|
675
|
+
// Told the CLI's process once it is spawned ({ pid, startedAt, command }): a run that outlives this server is
|
|
676
|
+
// found by it (the next start ends an orphan before it resumes the session).
|
|
677
|
+
onProcess = null,
|
|
441
678
|
} = options;
|
|
442
679
|
const permissionPolicy = normalizePermissionPolicy(rawPolicy);
|
|
443
680
|
const capability = normalizeCapability(rawCapability);
|
|
444
|
-
|
|
681
|
+
// Resolved before anything starts: a confinement that cannot be set up stops the run (never an unconfined start).
|
|
682
|
+
let evaluated = null;
|
|
683
|
+
if (confinement?.kind === EVAL_CONFINEMENT) {
|
|
684
|
+
try { evaluated = resolveEvalConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('Claude Code', error?.message || error, 'an evaluation folder'); }
|
|
685
|
+
}
|
|
686
|
+
const confined = confinement && !evaluated ? resolveDraftConfinement(confinement, cwd) : null;
|
|
687
|
+
// Either confinement (drafting, evaluation): no worker guard, nothing asked, never bypassPermissions.
|
|
688
|
+
const held = confined || evaluated;
|
|
689
|
+
const evalOptions = evaluated ? evalClaudeOptions(evaluated) : null;
|
|
690
|
+
// The worker confinement of a read-only / workspace run: a PreToolUse hook on every call (subagents' included),
|
|
691
|
+
// which Claude Code runs before any permission mode, so the run keeps bypassPermissions and nothing waits.
|
|
692
|
+
const guard = workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: held, serverPort: serverPortOf(mcpUrl, serverPort), localPortRange });
|
|
693
|
+
const askMode = !held && permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
|
|
445
694
|
const input = createInputQueue();
|
|
446
695
|
const abortController = new AbortController();
|
|
447
696
|
let alive = true;
|
|
448
697
|
let sessionId = null;
|
|
449
698
|
let activeTurn = null;
|
|
450
699
|
const inherited = cleanClaudeEnvironment();
|
|
700
|
+
for (const key of ['SYNABUN_PROFILE', 'SYNABUN_RUNTIME_PROFILE_PATH', 'SYNABUN_TOOLS', 'SYNABUN_TOOLS_PATH']) delete inherited[key];
|
|
451
701
|
for (const key of Array.isArray(unsetEnv) ? unsetEnv : []) delete inherited[key];
|
|
702
|
+
// A worker never holds a brain's launch credential (the Assistant's own identity) or an owner's page secret.
|
|
703
|
+
delete inherited.SYNABUN_BRAIN_TOKEN;
|
|
704
|
+
// The worker's shell must not inherit the owner's live-data paths: a Claude worker's MCP is in-process HTTP (it
|
|
705
|
+
// reaches the DB through the Neural Interface, never from this env), so nothing here needs them.
|
|
706
|
+
for (const key of LIVE_DATA_ENV) delete inherited[key];
|
|
452
707
|
const ownEnv = stringEnv({
|
|
453
708
|
...inherited,
|
|
454
709
|
SYNABUN_TERMINAL_SESSION: runId,
|
|
710
|
+
SYNABUN_RUN_MODE: options.runMode === 'task' ? 'task' : undefined,
|
|
455
711
|
SYNABUN_PROJECT: cwd ? memoryProject(cwd) : 'global',
|
|
456
|
-
SYNABUN_PROFILE: 'full',
|
|
457
712
|
SYNABUN_BROWSER_SESSION: browserSessionId,
|
|
458
713
|
SYNABUN_BROWSER_TAB: browserTabId,
|
|
459
|
-
SYNABUN_RUN_MODE: runMode === 'task' ? 'task' : undefined,
|
|
460
714
|
...stringEnv(extraEnv),
|
|
461
715
|
});
|
|
462
|
-
|
|
716
|
+
// Apply after every inherited/child environment overlay: drop ALL GIT_* first, then SynaBun's own safety entries.
|
|
717
|
+
const env = withServeEnv(typeof childEnv === 'function' ? stringEnv(childEnv(ownEnv)) : ownEnv, evalOptions?.env);
|
|
463
718
|
const brokerRequest = async (request) => {
|
|
464
719
|
try {
|
|
465
720
|
const reply = await permissionBroker.request({
|
|
@@ -474,27 +729,46 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
474
729
|
'X-Synabun-Terminal': runId,
|
|
475
730
|
'X-Synabun-Project': cwd ? memoryProject(cwd) : 'global',
|
|
476
731
|
'X-Synabun-Memory-Session': runId,
|
|
732
|
+
'X-Synabun-Tools': runtimeToolsValue(resolveStoredTools(options, { source: options.runMode === 'task' ? 'dispatch' : 'launch', provider: 'claude-code' }).groups),
|
|
477
733
|
'X-Synabun-Browser-Session': browserSessionId,
|
|
478
734
|
'X-Synabun-Browser-Tab': browserTabId,
|
|
479
735
|
// Computer-use workers: the grant unlocks the `computer` MCP group.
|
|
480
736
|
'X-Synabun-Desktop-Grant': typeof desktopGrantProvider === 'function' ? desktopGrantProvider() || undefined : undefined,
|
|
481
737
|
});
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
const denyMatcher =
|
|
486
|
-
const denyMessage =
|
|
487
|
-
|
|
488
|
-
|
|
738
|
+
// Questions use the broker under every policy, independently of tool approvals.
|
|
739
|
+
// ExitPlanMode stays denied: asking does not need a separate plan turn.
|
|
740
|
+
const questionMode = !held && typeof permissionBroker?.request === 'function';
|
|
741
|
+
const denyMatcher = questionMode ? 'ExitPlanMode' : 'AskUserQuestion|ExitPlanMode';
|
|
742
|
+
const denyMessage = 'Dispatched workers do not use plan mode; questions use the live run sidepanel.';
|
|
743
|
+
// A confined run asks canUseTool for every write (permission mode 'default') on top of its PreToolUse hook, and
|
|
744
|
+
// loads no settings file: no user hooks (they would ask it for memories it has no tool to write), no allow rules.
|
|
745
|
+
const bypass = !askMode && !held;
|
|
746
|
+
// The guard's decision on a tool call: refused (enforce) or noted (warn), always written into the run's events.
|
|
747
|
+
const guardHook = guard ? async (input) => {
|
|
748
|
+
const decision = workerToolDecision(input?.tool_name, input?.tool_input || {}, guard);
|
|
749
|
+
if (!decision) return {};
|
|
750
|
+
onEvent({ provider: 'claude-code', runId, event: confinementEvent(decision, { tool: input?.tool_name, confinement: guard, provider: 'claude-code' }) });
|
|
751
|
+
if (!guard.enforced) return {};
|
|
752
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: decision.reason } };
|
|
753
|
+
} : null;
|
|
489
754
|
const queryOptions = {
|
|
490
755
|
cwd: cwd || process.cwd(),
|
|
491
|
-
permissionMode:
|
|
492
|
-
...(
|
|
756
|
+
permissionMode: bypass ? 'bypassPermissions' : 'default',
|
|
757
|
+
...(bypass ? { allowDangerouslySkipPermissions: true } : {}),
|
|
493
758
|
includePartialMessages: !!includePartialMessages,
|
|
494
759
|
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
495
|
-
|
|
760
|
+
// A drafting run and every evaluation run read no settings file at all (an evaluation's skill under test comes as
|
|
761
|
+
// its one plugin, below); everything else the user's, the project's and the local ones.
|
|
762
|
+
settingSources: evalOptions ? evalOptions.settingSources : confined ? [] : ['user', 'project', 'local'],
|
|
496
763
|
hooks: {
|
|
497
|
-
PreToolUse: [
|
|
764
|
+
PreToolUse: [
|
|
765
|
+
// A skill-drafting run: every call, before any permission mode (lib/skills/draft-confinement.js).
|
|
766
|
+
...(confined ? [{ hooks: [draftPreToolUseHook(confined)] }] : []),
|
|
767
|
+
// A skill-evaluation run: every call, the same way (lib/skills/eval-confinement.js).
|
|
768
|
+
...(evaluated ? [{ hooks: [evalPreToolUseHook(evaluated)] }] : []),
|
|
769
|
+
// A guarded worker (capability read-only / workspace): every call, subagents' included (lib/worker-confinement.js).
|
|
770
|
+
...(guardHook ? [{ hooks: [guardHook] }] : []),
|
|
771
|
+
{
|
|
498
772
|
matcher: denyMatcher,
|
|
499
773
|
hooks: [async () => ({
|
|
500
774
|
hookSpecificOutput: {
|
|
@@ -504,11 +778,12 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
504
778
|
},
|
|
505
779
|
})],
|
|
506
780
|
},
|
|
781
|
+
...(questionMode ? [{ matcher: 'AskUserQuestion', hooks: [async () => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask', permissionDecisionReason: 'The user answers in this run’s sidepanel.' } })] }] : []),
|
|
507
782
|
// Assistant task runs: SynaBun's browser policy (browser-tool-policy.js)
|
|
508
783
|
// on every call, subagents' included, before the bypass and canUseTool.
|
|
509
784
|
...(runMode === 'task' ? [{
|
|
510
785
|
hooks: [async (input) => {
|
|
511
|
-
const reason = browserToolDenial(input?.tool_name, input?.tool_input || {}, { host: 'claude', cwd: cwd || process.cwd() });
|
|
786
|
+
const reason = browserToolDenial(input?.tool_name, input?.tool_input || {}, { host: 'claude', cwd: cwd || process.cwd(), roots: policyRoots });
|
|
512
787
|
return reason ? { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } } : {};
|
|
513
788
|
}],
|
|
514
789
|
}] : [])],
|
|
@@ -516,9 +791,17 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
516
791
|
env,
|
|
517
792
|
abortController,
|
|
518
793
|
canUseTool: async (toolName, toolInput) => {
|
|
794
|
+
if (confined) {
|
|
795
|
+
const reason = draftToolDecision(toolName, toolInput, confined);
|
|
796
|
+
return reason ? { behavior: 'deny', message: reason } : { behavior: 'allow', updatedInput: toolInput };
|
|
797
|
+
}
|
|
798
|
+
if (evaluated) {
|
|
799
|
+
const reason = evalToolDecision(toolName, toolInput, evaluated);
|
|
800
|
+
return reason ? { behavior: 'deny', message: reason } : { behavior: 'allow', updatedInput: toolInput };
|
|
801
|
+
}
|
|
519
802
|
if (toolName === 'ExitPlanMode') return { behavior: 'deny', message: denyMessage };
|
|
520
803
|
if (toolName === 'AskUserQuestion') {
|
|
521
|
-
if (!
|
|
804
|
+
if (!questionMode) return { behavior: 'deny', message: denyMessage };
|
|
522
805
|
const reply = await brokerRequest({ kind: 'ask', toolName, input: toolInput, questions: toolInput?.questions || [] });
|
|
523
806
|
if (reply.behavior === 'allow') {
|
|
524
807
|
const answers = reply.answers || reply.updatedInput?.answers || {};
|
|
@@ -526,6 +809,8 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
526
809
|
}
|
|
527
810
|
return { behavior: 'deny', message: reply.message || 'The question was not answered.' };
|
|
528
811
|
}
|
|
812
|
+
// Loading a deferred question schema is a read, never a permission decision.
|
|
813
|
+
if (toolName === 'ToolSearch') return { behavior: 'allow', updatedInput: toolInput };
|
|
529
814
|
// Computer use is fully autonomous: its guards live in the desktop service,
|
|
530
815
|
// so the orchestrator is never asked to approve individual actions.
|
|
531
816
|
if (askMode && /^mcp__SynaBun__computer(_apps|_ax|_status)?$/.test(String(toolName))) return { behavior: 'allow', updatedInput: toolInput };
|
|
@@ -541,12 +826,105 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
541
826
|
return { behavior: 'allow', updatedInput: toolInput };
|
|
542
827
|
},
|
|
543
828
|
};
|
|
544
|
-
if (
|
|
829
|
+
if (confined) {
|
|
830
|
+
// No MCP server at all (SynaBun's or the user's: strict), and only the file tools.
|
|
831
|
+
queryOptions.mcpServers = {};
|
|
832
|
+
queryOptions.strictMcpConfig = true;
|
|
833
|
+
queryOptions.tools = [...DRAFT_TOOLS];
|
|
834
|
+
queryOptions.disallowedTools = [...DRAFT_DISALLOWED_TOOLS];
|
|
835
|
+
} else if (evalOptions) {
|
|
836
|
+
// No MCP server at all, the role's tools only, and the skills it may use: the one under test (a baseline: none),
|
|
837
|
+
// loaded as the run's one local plugin; no hook of any settings file, plugin or skill and no inline shell.
|
|
838
|
+
queryOptions.mcpServers = {};
|
|
839
|
+
queryOptions.strictMcpConfig = true;
|
|
840
|
+
queryOptions.tools = [...evalOptions.tools];
|
|
841
|
+
queryOptions.disallowedTools = [...evalOptions.disallowedTools];
|
|
842
|
+
queryOptions.skills = [...evalOptions.skills];
|
|
843
|
+
queryOptions.settings = { ...evalOptions.settings };
|
|
844
|
+
if (evalOptions.plugins.length) queryOptions.plugins = evalOptions.plugins.map((plugin) => ({ ...plugin }));
|
|
845
|
+
} else if (mcpUrl) queryOptions.mcpServers = { SynaBun: { type: 'http', url: mcpUrl, headers } };
|
|
846
|
+
let externalScopeNames = [];
|
|
847
|
+
let externalValues = [];
|
|
848
|
+
let privateMcpServers = null;
|
|
849
|
+
let privateMcpDir = null;
|
|
850
|
+
if (!held) {
|
|
851
|
+
const inventory = externalServerInventory({ provider: 'claude-code', cwd, env, home: env.HOME, dataHome: memoryConfig.dataHome, ...options.externalMcpContext });
|
|
852
|
+
const list = nativeToolSelection(options, 'claude-code');
|
|
853
|
+
const scope = selectExternalServers(list.servers, inventory);
|
|
854
|
+
const definitions = Object.fromEntries(scope.names.map(name => [name, inventory.definitionFor(name)]));
|
|
855
|
+
if (mcpUrl) definitions.SynaBun = { type: 'http', url: mcpUrl, headers };
|
|
856
|
+
const referenced = referenceClaudeMap(definitions, env);
|
|
857
|
+
queryOptions.mcpServers = referenced.mcpServers;
|
|
858
|
+
queryOptions.strictMcpConfig = true;
|
|
859
|
+
Object.assign(env, referenced.env);
|
|
860
|
+
// OAuth and dynamic header helpers are not expanded by the CLI's generic ${VAR} pass.
|
|
861
|
+
// Keep them intact in a private file; the SDK's JSON is replaced before the OS spawn boundary.
|
|
862
|
+
privateMcpServers = structuredClone(referenced.mcpServers);
|
|
863
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
864
|
+
if (definition.oauth) privateMcpServers[name].oauth = structuredClone(definition.oauth);
|
|
865
|
+
if (definition.headersHelper) privateMcpServers[name].headersHelper = definition.headersHelper;
|
|
866
|
+
}
|
|
867
|
+
externalValues = [...Object.values(referenced.env), ...mcpDefinitionValues(definitions)];
|
|
868
|
+
externalScopeNames = Object.keys(definitions);
|
|
869
|
+
}
|
|
545
870
|
if (model) queryOptions.model = model;
|
|
546
871
|
if (effort && CLAUDE_EFFORT_LEVELS.includes(effort)) queryOptions.effort = effort;
|
|
547
|
-
|
|
872
|
+
// Read-only: with the guard enforced the shell stays, limited to read-only commands by the hook, and only the
|
|
873
|
+
// built-in writers are off the list; otherwise (warn, off) today's list, Bash included.
|
|
874
|
+
if (capability === 'read-only' && !held) queryOptions.disallowedTools = guard?.enforced ? [...CLAUDE_READ_ONLY_WRITE_TOOLS] : [...CLAUDE_READ_ONLY_DISALLOWED_TOOLS];
|
|
548
875
|
if (Number(maxBudgetUsd) > 0) queryOptions.maxBudgetUsd = Number(maxBudgetUsd);
|
|
549
876
|
if (outputSchema && typeof outputSchema === 'object') queryOptions.outputFormat = { type: 'json_schema', schema: outputSchema };
|
|
877
|
+
// A run resumed after a server restart continues its own Claude session. A tool call the restart cut off is in its
|
|
878
|
+
// transcript without a result: the CLI records it as interrupted (outcome unknown) and the next call goes through.
|
|
879
|
+
if (resumeSessionId) queryOptions.resume = String(resumeSessionId);
|
|
880
|
+
// The CLI's process is spawned here, not inside the SDK, so its pid is known: a server shutdown ends it at once
|
|
881
|
+
// (endProcess), and the next start can tell an orphan from another program. Left to the SDK, a CLI whose server
|
|
882
|
+
// went down only gets its stdin closed: it finishes the tool call in progress (a minute of `sleep` measured) and
|
|
883
|
+
// keeps writing to its transcript, because the SDK's SIGTERM waits 2 s on a timer that dies with the server. The
|
|
884
|
+
// SDK reads no stderr from a process it did not spawn: it is drained here, its tail kept for a failure's message.
|
|
885
|
+
let child = null;
|
|
886
|
+
let stderrTail = '';
|
|
887
|
+
let terminating = false;
|
|
888
|
+
queryOptions.spawnClaudeCodeProcess = (spawnOptions) => {
|
|
889
|
+
if (!alive || abortController.signal.aborted) throw Object.assign(new Error('Claude MCP launch was stopped'), { code: 'MCP_SCOPE_UNENFORCED' });
|
|
890
|
+
const args = [...spawnOptions.args];
|
|
891
|
+
if (privateMcpServers) for (let index = 0; index < args.length - 1; index++) if (args[index] === '--mcp-config') {
|
|
892
|
+
privateMcpDir ||= mkdtempSync(join(tmpdir(), 'synabun-claude-mcp-'));
|
|
893
|
+
args[index + 1] = writePrivateMcpConfig(join(privateMcpDir, 'mcp.json'), privateMcpServers);
|
|
894
|
+
}
|
|
895
|
+
const proc = (options.spawnFactory || spawnProcess)(spawnOptions.command, args, {
|
|
896
|
+
cwd: spawnOptions.cwd, env: spawnOptions.env, signal: spawnOptions.signal,
|
|
897
|
+
stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
|
|
898
|
+
});
|
|
899
|
+
child = proc;
|
|
900
|
+
proc.stderr?.setEncoding?.('utf8');
|
|
901
|
+
proc.stderr?.on('data', (chunk) => { stderrTail = redactMcpValues(`${stderrTail}${chunk}`, externalValues).slice(-2048); });
|
|
902
|
+
proc.stderr?.on('error', () => {});
|
|
903
|
+
if (proc.pid) { try { onProcess?.({ pid: proc.pid, startedAt: new Date().toISOString(), command: String(spawnOptions.command || '') }); } catch { /* a note, never the run */ } }
|
|
904
|
+
return proc;
|
|
905
|
+
};
|
|
906
|
+
const processEnded = () => !child || child.exitCode !== null || (child.signalCode !== null && child.signalCode !== undefined);
|
|
907
|
+
/**
|
|
908
|
+
* End the CLI now (a server shutdown): SIGTERM, which makes it stop the tool call in progress (its process tree
|
|
909
|
+
* included), record that call's result and its cost state, and exit within a second; SIGKILL when it has not exited
|
|
910
|
+
* by `graceMs` (a killed CLI leaves its tool's processes behind, so it is the fallback only). Windows has no SIGTERM
|
|
911
|
+
* (a signal stops the CLI alone, and a dev server its tool started keeps its port): the tree is ended with taskkill.
|
|
912
|
+
* The CLI writes no cost state then; the resume reads what the CLI restores either way (lib/worker-resume.js).
|
|
913
|
+
*/
|
|
914
|
+
async function endProcess(graceMs = 1500) {
|
|
915
|
+
const proc = child;
|
|
916
|
+
if (!proc || processEnded()) return;
|
|
917
|
+
const exited = new Promise((resolveExit) => { proc.once('exit', () => resolveExit()); });
|
|
918
|
+
if (process.platform === 'win32') {
|
|
919
|
+
try { spawnProcess('taskkill', ['/pid', String(proc.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }).on?.('error', () => {}); } catch { /* gone */ }
|
|
920
|
+
} else {
|
|
921
|
+
try { proc.kill('SIGTERM'); } catch { /* gone */ }
|
|
922
|
+
}
|
|
923
|
+
await Promise.race([exited, new Promise((resolveWait) => setTimeout(resolveWait, Math.max(100, graceMs)))]);
|
|
924
|
+
if (processEnded()) return;
|
|
925
|
+
try { proc.kill('SIGKILL'); } catch { /* gone */ }
|
|
926
|
+
await Promise.race([exited, new Promise((resolveWait) => setTimeout(resolveWait, 500))]);
|
|
927
|
+
}
|
|
550
928
|
// The loop runs the user's own Claude Code: SynaBun carries none, and the SDK
|
|
551
929
|
// is never left to look for one. The SDK uses pathToClaudeCodeExecutable
|
|
552
930
|
// verbatim with no validation, so the path was vetted where it was resolved
|
|
@@ -565,9 +943,11 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
565
943
|
throw error;
|
|
566
944
|
}
|
|
567
945
|
|
|
568
|
-
|
|
946
|
+
let q;
|
|
947
|
+
try { q = queryFactory({ prompt: input, options: queryOptions }); }
|
|
948
|
+
catch (error) { if (privateMcpDir) rmSync(privateMcpDir, { recursive: true, force: true }); throw error; }
|
|
569
949
|
const deltaBuffer = new Map();
|
|
570
|
-
const forwardEvent = (event) => onEvent({ provider: 'claude-code', runId, event });
|
|
950
|
+
const forwardEvent = (event) => onEvent({ provider: 'claude-code', runId, event: redactMcpValues(event, externalValues) });
|
|
571
951
|
const flushDelta = (key) => {
|
|
572
952
|
const slot = deltaBuffer.get(key);
|
|
573
953
|
if (!slot) return;
|
|
@@ -605,8 +985,9 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
605
985
|
};
|
|
606
986
|
// total_cost_usd is this CLI process's running total, not the turn's charge:
|
|
607
987
|
// a warm task run reported $7.87 after turn 1 and $7.99 after a $0.12 turn 2.
|
|
608
|
-
// A turn resolves with its own charge (costUsd) beside the total.
|
|
609
|
-
|
|
988
|
+
// A turn resolves with its own charge (costUsd) beside the total. A resumed
|
|
989
|
+
// session's CLI starts from the total its transcript recorded (costBaseUsd).
|
|
990
|
+
let reportedCostUsd = Math.max(0, Number(costBaseUsd) || 0);
|
|
610
991
|
const turnCost = (event) => {
|
|
611
992
|
const total = Number(event?.total_cost_usd);
|
|
612
993
|
if (!Number.isFinite(total) || total <= 0) return { costUsd: 0, totalCostUsd: reportedCostUsd };
|
|
@@ -618,7 +999,8 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
618
999
|
let pumpError = null;
|
|
619
1000
|
const pump = (async () => {
|
|
620
1001
|
try {
|
|
621
|
-
for await (const
|
|
1002
|
+
for await (const rawEvent of q) {
|
|
1003
|
+
const event = redactMcpValues(rawEvent, externalValues);
|
|
622
1004
|
if (!alive) break;
|
|
623
1005
|
const eventSessionId = event?.session_id || null;
|
|
624
1006
|
if (eventSessionId && eventSessionId !== sessionId) {
|
|
@@ -653,6 +1035,12 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
653
1035
|
}
|
|
654
1036
|
}
|
|
655
1037
|
} catch (error) {
|
|
1038
|
+
// A process that exited is reported without what it said on stderr (the SDK keeps that only for a process it
|
|
1039
|
+
// spawned itself): its tail goes with the message.
|
|
1040
|
+
const message = String(error?.message || '');
|
|
1041
|
+
if (stderrTail.trim() && /Claude Code process (exited|terminated)/.test(message) && !/stderr:/.test(message)) {
|
|
1042
|
+
try { error.message = `${message}. stderr: ${stderrTail.trim().slice(-1000)}`; } catch { /* read-only message */ }
|
|
1043
|
+
}
|
|
656
1044
|
pumpError = error;
|
|
657
1045
|
} finally {
|
|
658
1046
|
flushDeltas();
|
|
@@ -670,7 +1058,8 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
670
1058
|
pump.catch((error) => {
|
|
671
1059
|
// dispose() aborts the query when the run ends, however it ended, and the
|
|
672
1060
|
// SDK throws "Claude Code process aborted by user" for it: not a failure.
|
|
673
|
-
|
|
1061
|
+
// Neither is the exit of a CLI a server shutdown ended (dispose with terminate).
|
|
1062
|
+
if (abortController.signal.aborted || terminating) return;
|
|
674
1063
|
onEvent({
|
|
675
1064
|
provider: 'claude-code',
|
|
676
1065
|
runId,
|
|
@@ -681,8 +1070,14 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
681
1070
|
return {
|
|
682
1071
|
identity: () => ({ providerSessionId: sessionId }),
|
|
683
1072
|
isAlive: () => alive,
|
|
684
|
-
|
|
685
|
-
|
|
1073
|
+
// The CLI's process while it runs ({ pid }), else null.
|
|
1074
|
+
processInfo: () => (child?.pid && !processEnded() ? { pid: child.pid } : null),
|
|
1075
|
+
describe: () => ({ provider: 'claude-code', permissionPolicy, capability, askMode, ...(held ? { confinement: held.kind, root: held.root, ...(evaluated ? { role: evaluated.role, skills: [...evalOptions.skills], plugins: evalOptions.plugins.map((plugin) => plugin.path) } : {}) } : {}), ...(guard ? { guard: { mode: guard.mode, capability: guard.capability, writable: [...guard.writable] } } : {}) }),
|
|
1076
|
+
async runTurn(prompt, meta = {}) {
|
|
1077
|
+
if (!held) {
|
|
1078
|
+
try { await waitForClaudeMcpScope(q, externalScopeNames); }
|
|
1079
|
+
catch { input.close(); q.close?.(); await endProcess(); throw Object.assign(new Error('Claude MCP scope could not be enforced before the first turn'), { code: 'MCP_SCOPE_UNENFORCED' }); }
|
|
1080
|
+
}
|
|
686
1081
|
if (!alive) return Promise.reject(new Error('Claude native loop is closed'));
|
|
687
1082
|
if (activeTurn) return Promise.reject(new Error('Claude native loop already has an active turn'));
|
|
688
1083
|
onEvent({
|
|
@@ -718,16 +1113,24 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
|
|
|
718
1113
|
} catch {}
|
|
719
1114
|
return true;
|
|
720
1115
|
},
|
|
721
|
-
|
|
1116
|
+
// `terminate` (a server shutdown, lib/native-loop-runtime.js): the CLI is ended before the query is closed
|
|
1117
|
+
// (endProcess), so nothing of it outlives the server. The events it sends while it stops (the cut-off tool call's
|
|
1118
|
+
// result) still reach the run; its exit is not reported as a failure.
|
|
1119
|
+
async dispose(options = {}) {
|
|
722
1120
|
if (activeTurn) {
|
|
723
1121
|
const turn = activeTurn; activeTurn = null;
|
|
724
1122
|
turn.reject(asAbortError('disposed'));
|
|
725
1123
|
}
|
|
1124
|
+
if (options?.terminate === true) {
|
|
1125
|
+
terminating = true;
|
|
1126
|
+
await endProcess(Number(options.graceMs) > 0 ? Number(options.graceMs) : 1500);
|
|
1127
|
+
}
|
|
726
1128
|
alive = false;
|
|
727
1129
|
input.close();
|
|
728
1130
|
flushDeltas();
|
|
729
1131
|
try { abortController.abort(asAbortError('disposed')); } catch {}
|
|
730
1132
|
await Promise.race([pump, new Promise((resolveWait) => setTimeout(resolveWait, 1000))]).catch(() => {});
|
|
1133
|
+
if (privateMcpDir) { rmSync(privateMcpDir, { recursive: true, force: true }); privateMcpDir = null; }
|
|
731
1134
|
},
|
|
732
1135
|
};
|
|
733
1136
|
}
|
|
@@ -743,13 +1146,89 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
743
1146
|
agent = null, effort = null, runMode = 'loop',
|
|
744
1147
|
// Image / video creation runs: 'image' | 'video' → each turn returns the files its model generated.
|
|
745
1148
|
collectMedia = null,
|
|
1149
|
+
// A skill-drafting run's confinement (lib/skills/draft-confinement.js openCodeDraftConfig): the serve's config has
|
|
1150
|
+
// no shell, web, subagent or skill tool and no MCP server; every edit is asked and answered inside the folder.
|
|
1151
|
+
confinement = null,
|
|
1152
|
+
// The Protect workers setting as the run starts (lib/worker-confinement.js), the run's own scratch folder,
|
|
1153
|
+
// SynaBun's own port, the run's own serve port and the band SynaBun keeps for its serves (a guarded worker's shell
|
|
1154
|
+
// may call none of them).
|
|
1155
|
+
guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null, servePort = null,
|
|
1156
|
+
onNote = null,
|
|
746
1157
|
} = options;
|
|
747
1158
|
if (!client?.session?.create || (!client?.session?.promptAsync && !client?.session?.prompt)) {
|
|
748
1159
|
throw new Error('OpenCode native loop requires an isolated SDK client');
|
|
749
1160
|
}
|
|
750
1161
|
const permissionPolicy = normalizePermissionPolicy(rawPolicy);
|
|
751
1162
|
const capability = normalizeCapability(rawCapability);
|
|
752
|
-
|
|
1163
|
+
// Resolved before anything starts: a confinement that cannot be set up stops the run (never an unconfined start).
|
|
1164
|
+
let confined = null;
|
|
1165
|
+
let evaluated = null;
|
|
1166
|
+
if (confinement?.kind === EVAL_CONFINEMENT) {
|
|
1167
|
+
try { evaluated = resolveEvalConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('OpenCode', error?.message || error, 'an evaluation folder'); }
|
|
1168
|
+
} else if (confinement) {
|
|
1169
|
+
try { confined = resolveDraftConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('OpenCode', error?.message || error); }
|
|
1170
|
+
}
|
|
1171
|
+
const held = confined || evaluated;
|
|
1172
|
+
const guard = workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: held, serverPort, localPorts: servePort ? [servePort] : null, localPortRange });
|
|
1173
|
+
const askMode = !held && permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
|
|
1174
|
+
// Code Mode (lib/worker-confinement.js OPENCODE_CODE_MODE_TOOL): off under Enforce, noted call by call under Warn.
|
|
1175
|
+
if (guard) { try { onNote?.(openCodeCodeModeNote(guard)); } catch {} }
|
|
1176
|
+
const codeModeCalls = new Set();
|
|
1177
|
+
const noteCodeModeCall = (part) => {
|
|
1178
|
+
if (!guard || part?.type !== 'tool') return;
|
|
1179
|
+
const decision = openCodeCodeModeDecision(part.tool, guard);
|
|
1180
|
+
const callID = part.callID || part.id || '';
|
|
1181
|
+
if (!decision || codeModeCalls.has(callID)) return;
|
|
1182
|
+
codeModeCalls.add(callID);
|
|
1183
|
+
onEvent({ provider: 'opencode', runId, eventType: 'synabun.confinement', event: confinementEvent(decision, { tool: part.tool, confinement: guard, provider: 'opencode' }) });
|
|
1184
|
+
};
|
|
1185
|
+
// An external_directory ask identifies its originating call, not whether it reads or writes. Resolve that exact
|
|
1186
|
+
// message/call pair from SSE or the message API; never infer a read from the requested path or arbitrary metadata.
|
|
1187
|
+
const permissionTools = new Map();
|
|
1188
|
+
const rememberPermissionTool = (part) => {
|
|
1189
|
+
if (part?.type === 'tool' && part.messageID && part.callID && part.tool) permissionTools.set(`${part.messageID}:${part.callID}`, part.tool);
|
|
1190
|
+
};
|
|
1191
|
+
const permissionToolName = async (event) => {
|
|
1192
|
+
const { messageID, callID } = event?.tool || {};
|
|
1193
|
+
if (!messageID || !callID) return null;
|
|
1194
|
+
const key = `${messageID}:${callID}`;
|
|
1195
|
+
if (!permissionTools.has(key) && typeof client.session?.message === 'function') {
|
|
1196
|
+
try {
|
|
1197
|
+
const row = await settleWithin(client.session.message({ sessionID: sessionId, messageID, directory: cwd || undefined }), 5000);
|
|
1198
|
+
for (const part of row?.data?.parts || []) if (part.messageID === messageID) rememberPermissionTool(part);
|
|
1199
|
+
} catch { /* unknown calls fail closed */ }
|
|
1200
|
+
}
|
|
1201
|
+
return permissionTools.get(key) || null;
|
|
1202
|
+
};
|
|
1203
|
+
/**
|
|
1204
|
+
* A permission ask under the run's confinement: { reply: 'once' | 'reject', message }, the decision written into
|
|
1205
|
+
* the run's events. A refusal carries its reason as the reply's message: OpenCode hands it to the model as the
|
|
1206
|
+
* tool's error (a reject without a message is what OpenCode's own client follows with an interrupt).
|
|
1207
|
+
*/
|
|
1208
|
+
const policyReply = async (event) => {
|
|
1209
|
+
if (evaluated) {
|
|
1210
|
+
const reason = evalOpenCodeDecision(event, evaluated);
|
|
1211
|
+
if (!reason) return { reply: 'once' };
|
|
1212
|
+
onEvent({ provider: 'opencode', runId, eventType: 'synabun.confinement', event: { type: 'synabun.confinement', action: 'refused', capability: 'workspace', tool: event.permission || event.type || 'permission', rule: 'skill-eval', reason, provider: 'opencode' } });
|
|
1213
|
+
return { reply: 'reject', message: reason };
|
|
1214
|
+
}
|
|
1215
|
+
if (confined) {
|
|
1216
|
+
const reason = draftOpenCodeDecision(event, confined);
|
|
1217
|
+
if (!reason) return { reply: 'once' };
|
|
1218
|
+
onEvent({ provider: 'opencode', runId, eventType: 'synabun.confinement', event: { type: 'synabun.confinement', action: 'refused', capability: 'workspace', tool: event.permission || event.type || 'permission', rule: 'skill-draft', reason, provider: 'opencode' } });
|
|
1219
|
+
return { reply: 'reject', message: reason };
|
|
1220
|
+
}
|
|
1221
|
+
if (guard) {
|
|
1222
|
+
const toolName = String(event?.permission || event?.type || '').toLowerCase() === 'external_directory' ? await permissionToolName(event) : null;
|
|
1223
|
+
const decision = openCodePermissionDecision(event, guard, { toolName });
|
|
1224
|
+
if (!decision) return { reply: 'once' };
|
|
1225
|
+
onEvent({ provider: 'opencode', runId, eventType: 'synabun.confinement', event: confinementEvent(decision, { tool: event.permission || event.type || 'permission', confinement: guard, provider: 'opencode' }) });
|
|
1226
|
+
return guard.enforced ? { reply: 'reject', message: decision.reason } : { reply: 'once' };
|
|
1227
|
+
}
|
|
1228
|
+
// Unattended and unguarded: allowed, one call at a time. Never "always": OpenCode 2 saves an "always" answer for
|
|
1229
|
+
// the whole project (GET /api/permission/saved), where it would stop the user's own sessions there from asking.
|
|
1230
|
+
return { reply: 'once' };
|
|
1231
|
+
};
|
|
753
1232
|
let alive = true;
|
|
754
1233
|
let activeAbort = null;
|
|
755
1234
|
let activeTurn = null;
|
|
@@ -828,6 +1307,7 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
828
1307
|
} catch {}
|
|
829
1308
|
return '';
|
|
830
1309
|
};
|
|
1310
|
+
const pendingQuestions = new Set();
|
|
831
1311
|
const brokerRequest = async (request) => {
|
|
832
1312
|
try {
|
|
833
1313
|
const reply = await permissionBroker.request({ runId, provider: 'opencode', signal: activeAbort?.signal, ...request });
|
|
@@ -845,7 +1325,8 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
845
1325
|
};
|
|
846
1326
|
const finishTurnWithText = () => {
|
|
847
1327
|
const turn = activeTurn;
|
|
848
|
-
if (
|
|
1328
|
+
if (pendingQuestions.size) return;
|
|
1329
|
+
if (!turn || turn.mode !== 'async' || turn.sent === false || turn.settled || turn.finishing) return;
|
|
849
1330
|
turn.finishing = true;
|
|
850
1331
|
fetchAssistantText()
|
|
851
1332
|
.then((text) => settleActiveTurn(null, { text, result: { text }, ...turnMedia() }))
|
|
@@ -867,19 +1348,27 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
867
1348
|
return;
|
|
868
1349
|
}
|
|
869
1350
|
if (/^message[.:]updated$/i.test(eventType)) rememberMessage(event.info || event.message?.info || null);
|
|
870
|
-
if (/^message[.:]part[.:]updated$/i.test(eventType)) rememberPart(event.part || null);
|
|
1351
|
+
if (/^message[.:]part[.:]updated$/i.test(eventType)) { rememberPart(event.part || null); rememberPermissionTool(event.part); noteCodeModeCall(event.part); }
|
|
871
1352
|
if (/^message[.:]part[.:]delta$/i.test(eventType)) rememberDelta(event);
|
|
872
1353
|
if (/question\.asked/i.test(eventType)) {
|
|
873
1354
|
const requestID = event.id || event.requestID;
|
|
874
|
-
if (requestID &&
|
|
875
|
-
|
|
1355
|
+
if (requestID && !held && permissionBroker?.request && client.question?.reply) {
|
|
1356
|
+
if (pendingQuestions.has(requestID)) return;
|
|
1357
|
+
pendingQuestions.add(requestID);
|
|
1358
|
+
brokerRequest({ kind: 'ask', toolName: 'question', input: event, questions: event.questions || [], protocol: { provider: 'opencode', id: requestID } })
|
|
876
1359
|
.then((reply) => {
|
|
1360
|
+
// The tool is now answered; an idle SSE arriving before the HTTP ack is valid.
|
|
1361
|
+
pendingQuestions.delete(requestID);
|
|
877
1362
|
if (reply.behavior === 'allow') {
|
|
878
|
-
return client.question.reply({ requestID, answers:
|
|
1363
|
+
return client.question.reply({ requestID, answers: openCodeAnswers(event.questions || [], reply.answers), directory: cwd || undefined });
|
|
879
1364
|
}
|
|
880
1365
|
return client.question?.reject?.({ requestID, directory: cwd || undefined });
|
|
881
1366
|
})
|
|
882
|
-
.catch(() => {
|
|
1367
|
+
.catch(async (error) => {
|
|
1368
|
+
try { await client.question?.reject?.({ requestID, directory: cwd || undefined }); } catch {}
|
|
1369
|
+
settleActiveTurn(new Error(`OpenCode question reply failed: ${providerErrorMessage(error)}`));
|
|
1370
|
+
})
|
|
1371
|
+
.finally(() => pendingQuestions.delete(requestID));
|
|
883
1372
|
} else if (requestID && client.question?.reject) {
|
|
884
1373
|
client.question.reject({ requestID, directory: cwd || undefined }).catch(() => {});
|
|
885
1374
|
}
|
|
@@ -894,7 +1383,11 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
894
1383
|
})
|
|
895
1384
|
.catch(() => {});
|
|
896
1385
|
} else if (requestID && client.permission?.reply) {
|
|
897
|
-
|
|
1386
|
+
// Unattended: by the run's confinement (a drafting run, a guarded worker), else allowed once.
|
|
1387
|
+
policyReply(event).then((answer) => {
|
|
1388
|
+
if (!alive) return;
|
|
1389
|
+
return client.permission.reply({ requestID, reply: answer.reply, ...(answer.message ? { message: answer.message } : {}), directory: cwd || undefined });
|
|
1390
|
+
}).catch(() => {});
|
|
898
1391
|
}
|
|
899
1392
|
}
|
|
900
1393
|
// promptAsync acknowledges immediately, so the durable SSE stream is the
|
|
@@ -912,11 +1405,24 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
912
1405
|
}
|
|
913
1406
|
onEvent({ provider: 'opencode', runId, eventType, event });
|
|
914
1407
|
}) || (() => {});
|
|
1408
|
+
// The rules of a confined run ride the session too (lib/opencode2-client.js session.create sends them as the
|
|
1409
|
+
// session's own `permissions`), on top of the serve's config.
|
|
1410
|
+
let sessionRules = confined ? openCodeDraftConfig().permissions : evaluated ? openCodeEvalConfig(evaluated).permissions : guard ? openCodePermissionsFor(guard) : null;
|
|
1411
|
+
if (runMode === 'task' && !held) sessionRules = [...(sessionRules || []), { action: 'question', resource: '*', effect: 'allow' }];
|
|
915
1412
|
try {
|
|
916
1413
|
await client.waitUntilConnected?.(10_000);
|
|
917
|
-
const created =
|
|
1414
|
+
const created = options.resumeSessionId
|
|
1415
|
+
? await client.session.get({ sessionID: options.resumeSessionId })
|
|
1416
|
+
: await client.session.create({ directory: cwd || undefined, title: title || 'SynaBun automation', ...(sessionRules ? { permission: sessionRules } : {}) });
|
|
1417
|
+
if (created?.error) throw new Error(`OpenCode session unavailable: ${providerErrorMessage(created.error)}`);
|
|
918
1418
|
sessionId = created?.data?.id || created?.data?.sessionID || created?.id || created?.sessionID;
|
|
919
1419
|
if (!sessionId) throw new Error('OpenCode did not return a session id');
|
|
1420
|
+
if (options.resumeSessionId && sessionId !== options.resumeSessionId) throw new Error('OpenCode returned a different session on resume');
|
|
1421
|
+
if (options.resumeSessionId && sessionRules) {
|
|
1422
|
+
if (!client.session.update) throw new Error('OpenCode cannot restore the worker permission rules');
|
|
1423
|
+
const updated = await client.session.update({ sessionID: sessionId, permission: sessionRules });
|
|
1424
|
+
if (!updated || updated.error) throw new Error('OpenCode could not restore the worker permission rules');
|
|
1425
|
+
}
|
|
920
1426
|
onIdentity({ providerSessionId: sessionId });
|
|
921
1427
|
} catch (error) {
|
|
922
1428
|
try { unsubscribe(); } catch {}
|
|
@@ -943,7 +1449,7 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
943
1449
|
return {
|
|
944
1450
|
identity: () => ({ providerSessionId: sessionId }),
|
|
945
1451
|
isAlive: () => alive,
|
|
946
|
-
describe: () => ({ provider: 'opencode', permissionPolicy, capability, askMode }),
|
|
1452
|
+
describe: () => ({ provider: 'opencode', permissionPolicy, capability, askMode, ...(held ? { confinement: held.kind, root: held.root, ...(evaluated ? { role: evaluated.role } : {}) } : {}), ...(guard ? { guard: { mode: guard.mode, capability: guard.capability, writable: [...guard.writable] } } : {}) }),
|
|
947
1453
|
/** { children(sessionId) → sessions, messages(sessionId) → [{ info }] } on this run's own serve. */
|
|
948
1454
|
usageFetchers: () => ({
|
|
949
1455
|
children: (id) => storedRows('children', id),
|
|
@@ -952,13 +1458,29 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
952
1458
|
async runTurn(prompt, meta = {}) {
|
|
953
1459
|
if (!alive) throw new Error('OpenCode native loop is closed');
|
|
954
1460
|
if (activeTurn) throw new Error('OpenCode native loop already has an active turn');
|
|
955
|
-
|
|
956
|
-
activeAbort =
|
|
1461
|
+
const controller = new AbortController();
|
|
1462
|
+
activeAbort = controller;
|
|
1463
|
+
const checkReady = async () => {
|
|
1464
|
+
await client.waitUntilConnected?.(10_000);
|
|
1465
|
+
if (!alive || controller.signal.aborted) throw asAbortError('aborted');
|
|
1466
|
+
const ready = await waitForSynabunTools(client, {
|
|
1467
|
+
...(options.mcpWait || {}), directory: cwd || undefined,
|
|
1468
|
+
expected: options.synabunExpected !== false && !held, signal: controller.signal,
|
|
1469
|
+
stopped: () => !alive,
|
|
1470
|
+
onTimeout: (ms) => {
|
|
1471
|
+
const text = synabunToolsTimeoutText(ms, 'worker');
|
|
1472
|
+
onNote?.(text);
|
|
1473
|
+
onEvent({ provider: 'opencode', runId, eventType: 'synabun.mcp.status', event: { sessionID: sessionId, text } });
|
|
1474
|
+
},
|
|
1475
|
+
});
|
|
1476
|
+
if (!ready || controller.signal.aborted || !alive) throw asAbortError('aborted');
|
|
1477
|
+
};
|
|
957
1478
|
const request = {
|
|
958
1479
|
sessionID: sessionId,
|
|
959
1480
|
parts: [{ type: 'text', text: prompt }],
|
|
960
1481
|
model: normalizeOpenCodeModel(model),
|
|
961
|
-
|
|
1482
|
+
// Capability is enforced by session rules, the adapter and sandbox, not OpenCode's planning workflow.
|
|
1483
|
+
agent: agent ? String(agent) : 'build',
|
|
962
1484
|
directory: cwd || undefined,
|
|
963
1485
|
signal: activeAbort.signal,
|
|
964
1486
|
};
|
|
@@ -994,11 +1516,13 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
994
1516
|
// reported as an unhandled promise before runTurn begins awaiting it.
|
|
995
1517
|
completion.catch(() => {});
|
|
996
1518
|
const turn = {
|
|
997
|
-
mode: 'async', completion,
|
|
1519
|
+
mode: 'async', completion, sent: false,
|
|
998
1520
|
resolve: resolveTurn, reject: rejectTurn, settled: false,
|
|
999
1521
|
};
|
|
1000
1522
|
activeTurn = turn;
|
|
1001
1523
|
try {
|
|
1524
|
+
await checkReady();
|
|
1525
|
+
turn.sent = true;
|
|
1002
1526
|
const response = await client.session.promptAsync(request);
|
|
1003
1527
|
validateResponse(response);
|
|
1004
1528
|
return await completion;
|
|
@@ -1011,6 +1535,7 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
1011
1535
|
const turn = { mode: 'sync' };
|
|
1012
1536
|
activeTurn = turn;
|
|
1013
1537
|
try {
|
|
1538
|
+
await checkReady();
|
|
1014
1539
|
const response = await client.session.prompt(request);
|
|
1015
1540
|
validateResponse(response);
|
|
1016
1541
|
const text = extractOpenCodeText(response);
|
|
@@ -1038,3 +1563,377 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
|
|
|
1038
1563
|
},
|
|
1039
1564
|
};
|
|
1040
1565
|
}
|
|
1566
|
+
|
|
1567
|
+
// ── Antigravity (Google's `agy` CLI, over ACP) ──────────────────────────────
|
|
1568
|
+
//
|
|
1569
|
+
// One `agy acp` process and one ACP session per run (lib/antigravity/acp-connection.js), every turn a session/prompt,
|
|
1570
|
+
// in the run's own Antigravity home (`--gemini-dir`, lib/antigravity/home.js): its MCP servers are exactly SynaBun's
|
|
1571
|
+
// and the external servers its tool list names, and its PreToolUse hook asks SynaBun about every call before it runs
|
|
1572
|
+
// (lib/antigravity/policy.js → antigravityRunPolicy: the browser policy, the run's MCP scope, the worker's
|
|
1573
|
+
// confinement). A refusal reaches the model with its reason ("tool call denied by pre-tool hook: …") and the turn goes
|
|
1574
|
+
// on; an ACP permission refusal would end the turn without one. Approvals stay with ACP's session/request_permission:
|
|
1575
|
+
// the Assistant's broker answers them under 'ask', else they are allowed once. Never "always": Antigravity would save
|
|
1576
|
+
// it for later sessions. The model and thinking level are the session's own config options (set only to a value the
|
|
1577
|
+
// session offers). The home keeps the conversation, so a run resumes after a restart (session/resume, no replay).
|
|
1578
|
+
// SynaBun's MCP child gets the run's pins from the agent's environment, which agy's MCP children inherit.
|
|
1579
|
+
|
|
1580
|
+
/** The hook script every SynaBun home runs before a tool call. */
|
|
1581
|
+
export const ANTIGRAVITY_POLICY_HOOK = fileURLToPath(new URL('./antigravity/policy-hook.mjs', import.meta.url));
|
|
1582
|
+
|
|
1583
|
+
/** The ACP permission option for an answer: explicit kinds only, never allow-always. */
|
|
1584
|
+
export function acpPermissionOption(options, wanted) {
|
|
1585
|
+
const list = Array.isArray(options) ? options.filter((option) => option && typeof option.optionId === 'string') : [];
|
|
1586
|
+
for (const kind of wanted) {
|
|
1587
|
+
if (kind === 'allow_always') continue;
|
|
1588
|
+
const found = list.find((option) => option.kind === kind);
|
|
1589
|
+
if (found) return found.optionId;
|
|
1590
|
+
}
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/** The last JSON object in a reply (an output schema's answer, which ACP has no channel for), or null. */
|
|
1595
|
+
export function lastJsonObject(text) {
|
|
1596
|
+
const source = String(text || '');
|
|
1597
|
+
const fenced = [...source.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)].map((m) => m[1]).reverse();
|
|
1598
|
+
for (const candidate of [...fenced, source]) {
|
|
1599
|
+
const trimmed = candidate.trim();
|
|
1600
|
+
for (let start = trimmed.lastIndexOf('{'); start >= 0; start = trimmed.lastIndexOf('{', start - 1)) {
|
|
1601
|
+
const end = trimmed.lastIndexOf('}');
|
|
1602
|
+
if (end <= start) continue;
|
|
1603
|
+
try {
|
|
1604
|
+
const value = JSON.parse(trimmed.slice(start, end + 1));
|
|
1605
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
|
1606
|
+
} catch { /* try an earlier brace */ }
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* One Antigravity run's policy for one tool call (acpCall's shape): what its hook answers, and what a permission
|
|
1614
|
+
* request is checked against again. { decision: 'deny', reason, event? } | { decision: 'ask' | 'allow', event? }.
|
|
1615
|
+
* - MCP tool schemas in the run's home may be read; home configuration is refused;
|
|
1616
|
+
* - the browser policy (lib/browser-tool-policy.js: Antigravity's own web search, URL reading and browser, a shell
|
|
1617
|
+
* that starts a browser or browser code, another server's browser tools);
|
|
1618
|
+
* - the run's MCP scope (`allowedServers`: SynaBun and the external servers its tool list names;
|
|
1619
|
+
* enabled project MCP servers are refused before agy starts);
|
|
1620
|
+
* - the worker's confinement (lib/worker-confinement.js acpPermissionDecision; warn mode reports and allows);
|
|
1621
|
+
* - then the approval: 'ask' for anything but a read under the 'ask' policy, else 'allow'.
|
|
1622
|
+
*/
|
|
1623
|
+
export function antigravityRunPolicy(call, { homeDir = null, cwd = null, allowedServers = null, guard = null, askMode = false, policyRoots = [] } = {}) {
|
|
1624
|
+
if (!call || (!call.name && !call.title && (!call.kind || call.kind === 'other'))) return { decision: 'deny', reason: 'SynaBun received no valid tool call, so it is refused.' };
|
|
1625
|
+
const approval = () => (askMode && !acpReadOnlyKind(call.kind) ? 'ask' : 'allow');
|
|
1626
|
+
const name = acpToolName(call);
|
|
1627
|
+
const input = acpPolicyInput(call);
|
|
1628
|
+
const browser = browserToolDenial(acpWebTool(call) && !name.startsWith('mcp__') ? (call.name || 'search_web') : name, input, { host: 'antigravity', cwd, roots: policyRoots });
|
|
1629
|
+
if (browser) return { decision: 'deny', reason: browser, event: { type: 'synabun.browser_policy', action: 'refused', tool: name, reason: browser } };
|
|
1630
|
+
if (call.mcp && allowedServers && !allowedServers.has(call.mcp.server)) {
|
|
1631
|
+
const reason = `SynaBun: the MCP server ${call.mcp.server} is not one of this run's servers (${[...allowedServers].join(', ') || 'none'}), so ${call.mcp.tool} is refused. Use the tools this run was given.`;
|
|
1632
|
+
return { decision: 'deny', reason, event: { type: 'synabun.mcp_scope', action: 'refused', server: call.mcp.server, tool: call.mcp.tool, reason } };
|
|
1633
|
+
}
|
|
1634
|
+
const homeRead = antigravityHomeReadPolicy(call, { homeDir, cwd });
|
|
1635
|
+
if (homeRead) return homeRead;
|
|
1636
|
+
if (guard) {
|
|
1637
|
+
const decision = acpPermissionDecision(call, guard);
|
|
1638
|
+
if (decision) {
|
|
1639
|
+
const event = confinementEvent(decision, { tool: call.title || name, confinement: guard, provider: 'antigravity' });
|
|
1640
|
+
if (guard.enforced) return { decision: 'deny', reason: decision.reason, event };
|
|
1641
|
+
return { decision: approval(), event };
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return { decision: approval() };
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/** The MCP servers of a run's home: SynaBun's own entry, then the external servers its tool list names. */
|
|
1648
|
+
function antigravityRunServers({ tools, synabunMcpEntry, userGemini, onNote }) {
|
|
1649
|
+
const servers = {};
|
|
1650
|
+
let inventory = null;
|
|
1651
|
+
let names = [];
|
|
1652
|
+
try {
|
|
1653
|
+
inventory = externalServerInventory({ provider: 'antigravity', antigravityConfigPath: join(userGemini, 'config', 'mcp_config.json'), dataHome: memoryConfig.dataHome });
|
|
1654
|
+
names = selectExternalServers(tools.items, inventory).names;
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
if (error?.code === 'TOOLS_INVALID') throw error;
|
|
1657
|
+
onNote?.(`Antigravity: the external MCP servers could not be read (${error?.message || error}); this run has SynaBun's tools only.`);
|
|
1658
|
+
}
|
|
1659
|
+
const own = synabunMcpEntry || userAntigravityMcpServers(userGemini).SynaBun || null;
|
|
1660
|
+
if (own) servers.SynaBun = toAntigravityMcpEntry(own);
|
|
1661
|
+
for (const name of names) {
|
|
1662
|
+
const definition = inventory?.definitionFor(name);
|
|
1663
|
+
if (definition && name !== 'SynaBun') servers[name] = toAntigravityMcpEntry(definition);
|
|
1664
|
+
}
|
|
1665
|
+
return servers;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
export async function createAntigravityNativeLoopAdapter(options = {}) {
|
|
1669
|
+
const {
|
|
1670
|
+
runId, cwd, model = null, effort = null, launch = null,
|
|
1671
|
+
onIdentity = () => {}, onEvent = () => {}, onProcess = null, onNote = null,
|
|
1672
|
+
permissionPolicy: rawPolicy = 'auto', permissionBroker = null,
|
|
1673
|
+
capability: rawCapability = 'full', outputSchema = null, runMode = 'loop',
|
|
1674
|
+
browserSessionId = null, browserTabId = null, desktopGrantProvider = null,
|
|
1675
|
+
extraEnv = {}, unsetEnv = [], resumeSessionId = null,
|
|
1676
|
+
confinement = null,
|
|
1677
|
+
guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null,
|
|
1678
|
+
connect = createAcpConnection,
|
|
1679
|
+
// The run's Antigravity home (lib/antigravity/home.js). null: agy runs in the user's own ~/.gemini, with every
|
|
1680
|
+
// server of theirs and no hook (an older server's wiring, a test of the bare protocol).
|
|
1681
|
+
homeDir = null, synabunMcpEntry = null, userGemini = userGeminiDir(),
|
|
1682
|
+
// The hook's way back (lib/antigravity/policy.js): the registry, the loopback address of POST /api/antigravity/policy.
|
|
1683
|
+
policy = null, policyUrl = null, policyHookScript = ANTIGRAVITY_POLICY_HOOK, policyRoots = [],
|
|
1684
|
+
// ACP counts tokens per session: what the session had counted when the run was suspended (a resume books only
|
|
1685
|
+
// what follows), and where each turn's count is kept for the next resume.
|
|
1686
|
+
sessionUsageBase = null, onSessionUsage = null,
|
|
1687
|
+
} = options;
|
|
1688
|
+
if (!launch?.command) {
|
|
1689
|
+
const error = new Error('The Antigravity CLI (agy) is not installed or could not be resolved.');
|
|
1690
|
+
error.code = 'CLI_MISSING';
|
|
1691
|
+
throw error;
|
|
1692
|
+
}
|
|
1693
|
+
// Skill drafting and evaluation runs hold their own confinement; Antigravity has no setup for one, and a confined
|
|
1694
|
+
// run never starts unconfined.
|
|
1695
|
+
if (confinement) throw confinementUnsupportedError('Antigravity', 'skill drafting and evaluation run on Claude Code, Codex and OpenCode', confinement?.kind === EVAL_CONFINEMENT ? 'an evaluation folder' : 'a drafting folder');
|
|
1696
|
+
assertSafeAntigravityProject(cwd);
|
|
1697
|
+
const permissionPolicy = normalizePermissionPolicy(rawPolicy);
|
|
1698
|
+
const capability = normalizeCapability(rawCapability);
|
|
1699
|
+
const guard = workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: null, serverPort, localPortRange });
|
|
1700
|
+
const askMode = permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
|
|
1701
|
+
const tools = nativeToolSelection(options, 'antigravity');
|
|
1702
|
+
|
|
1703
|
+
// The run's home: its servers, its hook, its settings, the user's rules and skills.
|
|
1704
|
+
let allowedServers = null;
|
|
1705
|
+
let hookCommand = null;
|
|
1706
|
+
if (homeDir) {
|
|
1707
|
+
const servers = antigravityRunServers({ tools, synabunMcpEntry, userGemini, onNote });
|
|
1708
|
+
allowedServers = new Set(Object.keys(servers));
|
|
1709
|
+
if (typeof policy?.register === 'function' && policyUrl) hookCommand = policyHookCommand({ script: policyHookScript });
|
|
1710
|
+
else onNote?.('Antigravity: SynaBun\'s tool-call hook is not available in this server, so the browser policy and the run\'s MCP scope are checked only when Antigravity asks for approval.');
|
|
1711
|
+
prepareAntigravityHome({ dir: homeDir, mcpServers: servers, hookCommand, settings: antigravityHomeSettings({ userSettings: userAntigravitySettings(userGemini) }), userGemini });
|
|
1712
|
+
}
|
|
1713
|
+
const policyOptions = { homeDir, cwd, allowedServers, guard, askMode, policyRoots };
|
|
1714
|
+
|
|
1715
|
+
const pins = stringEnv({
|
|
1716
|
+
SYNABUN_TERMINAL_SESSION: runId,
|
|
1717
|
+
SYNABUN_RUN_MODE: options.runMode === 'task' ? 'task' : undefined,
|
|
1718
|
+
SYNABUN_PROJECT: cwd ? memoryProject(cwd) : 'global',
|
|
1719
|
+
SYNABUN_BROWSER_SESSION: browserSessionId,
|
|
1720
|
+
SYNABUN_BROWSER_TAB: browserTabId,
|
|
1721
|
+
SYNABUN_TOOLS: runtimeToolsValue(tools.items.includes('all') ? tools.items : [...tools.groups, ...tools.servers]),
|
|
1722
|
+
SYNABUN_DESKTOP_GRANT: typeof desktopGrantProvider === 'function' ? desktopGrantProvider() || undefined : undefined,
|
|
1723
|
+
});
|
|
1724
|
+
const inherited = { ...process.env };
|
|
1725
|
+
for (const key of ['SYNABUN_PROFILE', 'SYNABUN_RUNTIME_PROFILE_PATH', 'SYNABUN_TOOLS', 'SYNABUN_TOOLS_PATH', 'SYNABUN_TOOL_CATALOG_MODE', 'SYNABUN_ROLE', 'SYNABUN_AGY_POLICY_URL', 'SYNABUN_AGY_POLICY_TOKEN']) delete inherited[key];
|
|
1726
|
+
for (const key of Array.isArray(unsetEnv) ? unsetEnv : []) delete inherited[key];
|
|
1727
|
+
delete inherited.SYNABUN_BRAIN_TOKEN;
|
|
1728
|
+
for (const key of LIVE_DATA_ENV) delete inherited[key];
|
|
1729
|
+
|
|
1730
|
+
let alive = true;
|
|
1731
|
+
let sessionId = null;
|
|
1732
|
+
let providerTurn = Number(options.providerTurnBase) || 0;
|
|
1733
|
+
let activeTurn = null; // { iteration, providerTurn, text, controller }
|
|
1734
|
+
let lastSessionUsage = sessionUsageBase && typeof sessionUsageBase === 'object' ? sessionUsageBase : null;
|
|
1735
|
+
const openPermissions = new Set(); // resolvers of permission answers still waiting
|
|
1736
|
+
const startedAt = Date.now();
|
|
1737
|
+
|
|
1738
|
+
const emit = (eventType, event, extra = {}) => {
|
|
1739
|
+
try { onEvent({ provider: 'antigravity', runId, eventType, event, ...extra }); } catch { /* observers never break a run */ }
|
|
1740
|
+
};
|
|
1741
|
+
const turnMeta = () => (activeTurn ? { iteration: activeTurn.iteration, providerTurn: activeTurn.providerTurn } : {});
|
|
1742
|
+
// The hook's question (and, without a hook, the permission request's): the answer, its event in the run's events.
|
|
1743
|
+
const checkCall = (call) => {
|
|
1744
|
+
const answer = antigravityRunPolicy(call, policyOptions);
|
|
1745
|
+
if (answer.event) emit(answer.event.type, answer.event, turnMeta());
|
|
1746
|
+
return answer;
|
|
1747
|
+
};
|
|
1748
|
+
const registration = hookCommand ? policy.register((call) => (alive ? checkCall(call) : { decision: 'deny', reason: 'This run has ended.' }), { label: `run ${runId}` }) : null;
|
|
1749
|
+
const env = stringEnv({
|
|
1750
|
+
...inherited, ...pins, ...stringEnv(extraEnv),
|
|
1751
|
+
...(registration ? { SYNABUN_AGY_POLICY_URL: policyUrl, SYNABUN_AGY_POLICY_TOKEN: registration.token } : {}),
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
const decidePermission = async (params, signal) => {
|
|
1755
|
+
const call = acpCall(params);
|
|
1756
|
+
const options = params?.options || [];
|
|
1757
|
+
const allowOnce = () => ({ outcome: { outcome: 'selected', optionId: acpPermissionOption(options, ['allow_once']) } });
|
|
1758
|
+
const rejectOnce = () => ({ outcome: { outcome: 'selected', optionId: acpPermissionOption(options, ['reject_once', 'reject_always']) } });
|
|
1759
|
+
const settle = (answer) => (answer.outcome.optionId ? answer : { outcome: { outcome: 'cancelled' } });
|
|
1760
|
+
if (!alive) return { outcome: { outcome: 'cancelled' } };
|
|
1761
|
+
// With the hook the call was checked before Antigravity asked: checked again here without reporting it twice
|
|
1762
|
+
// (a refusal here ends the turn, which only a call the hook never saw can earn).
|
|
1763
|
+
const verdict = registration ? antigravityRunPolicy(call, policyOptions) : checkCall(call);
|
|
1764
|
+
if (verdict.decision === 'deny') return settle(rejectOnce());
|
|
1765
|
+
if (askMode && !acpReadOnlyKind(call.kind)) {
|
|
1766
|
+
try {
|
|
1767
|
+
// Allowed once whatever the answer said about "always": Antigravity would save an "always" for every later
|
|
1768
|
+
// session of the user's, outside this run (the dispatcher drops it for Antigravity too).
|
|
1769
|
+
const reply = await permissionBroker.request({ runId, provider: 'antigravity', signal, kind: 'tool', toolName: call.mcp ? `${call.mcp.server}_${call.mcp.tool}` : (call.title || call.name || call.kind || 'tool'), input: params?.toolCall || {} });
|
|
1770
|
+
if (reply?.behavior === 'allow') return settle(allowOnce());
|
|
1771
|
+
return settle(rejectOnce());
|
|
1772
|
+
} catch {
|
|
1773
|
+
return settle(rejectOnce());
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return settle(allowOnce());
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1779
|
+
const conn = connect({
|
|
1780
|
+
command: launch.command,
|
|
1781
|
+
args: launch.args || [],
|
|
1782
|
+
acpArgs: homeDir ? ['--gemini-dir', homeDir] : [],
|
|
1783
|
+
cwd: cwd || undefined,
|
|
1784
|
+
env,
|
|
1785
|
+
clientInfo: { name: 'synabun', title: 'SynaBun', version: '0' },
|
|
1786
|
+
onUpdate: (params) => {
|
|
1787
|
+
if (!params || params.sessionId !== sessionId || !params.update) return;
|
|
1788
|
+
const update = params.update;
|
|
1789
|
+
if (activeTurn && update.sessionUpdate === 'agent_message_chunk' && update.content?.type === 'text') activeTurn.text += update.content.text || '';
|
|
1790
|
+
emit(update.sessionUpdate || 'session/update', update, turnMeta());
|
|
1791
|
+
},
|
|
1792
|
+
onElicitation: async (params, { signal } = {}) => {
|
|
1793
|
+
if (!alive || !activeTurn || params.sessionId !== sessionId || !permissionBroker?.request) return { action: 'cancel' };
|
|
1794
|
+
const reply = await permissionBroker.request({ runId, provider: 'antigravity', signal: AbortSignal.any([activeTurn?.controller?.signal, signal].filter(Boolean)), kind: 'ask', toolName: 'choice', input: params, questions: workerQuestions(params), protocol: { provider: 'antigravity', method: 'elicitation/create' } });
|
|
1795
|
+
return elicitationReply(params, reply);
|
|
1796
|
+
},
|
|
1797
|
+
onPermission: (params, { signal } = {}) => new Promise((resolve) => {
|
|
1798
|
+
const done = (answer) => { openPermissions.delete(done); resolve(answer); };
|
|
1799
|
+
openPermissions.add(done);
|
|
1800
|
+
decidePermission(params, signal).then(done, () => done({ outcome: { outcome: 'cancelled' } }));
|
|
1801
|
+
}),
|
|
1802
|
+
onExit: (info) => {
|
|
1803
|
+
alive = false;
|
|
1804
|
+
for (const done of [...openPermissions]) done({ outcome: { outcome: 'cancelled' } });
|
|
1805
|
+
if (info?.error || (info?.code && info.code !== 0)) emit('synabun.provider_exit', { type: 'synabun.provider_exit', code: info.code, signal: info.signal, error: info.error || null });
|
|
1806
|
+
},
|
|
1807
|
+
});
|
|
1808
|
+
try { onProcess?.({ pid: conn.pid, startedAt, home: homeDir || null }); } catch { /* informational */ }
|
|
1809
|
+
|
|
1810
|
+
const fail = async (error) => {
|
|
1811
|
+
alive = false;
|
|
1812
|
+
registration?.dispose();
|
|
1813
|
+
try { await conn.close(); } catch { /* gone */ }
|
|
1814
|
+
throw error;
|
|
1815
|
+
};
|
|
1816
|
+
let session;
|
|
1817
|
+
try {
|
|
1818
|
+
const init = await conn.initialize({ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, elicitation: { form: {} } } });
|
|
1819
|
+
if (resumeSessionId) {
|
|
1820
|
+
// session/resume picks the conversation up without replaying it; an agent without it replays (session/load),
|
|
1821
|
+
// which onUpdate drops (sessionId is set only once the load answered).
|
|
1822
|
+
const caps = init?.agentCapabilities?.sessionCapabilities || {};
|
|
1823
|
+
const method = caps.resume ? 'session/resume' : 'session/load';
|
|
1824
|
+
session = await conn.request(method, { sessionId: String(resumeSessionId), cwd, mcpServers: [] });
|
|
1825
|
+
sessionId = String(session?.sessionId || resumeSessionId);
|
|
1826
|
+
} else {
|
|
1827
|
+
session = await conn.request('session/new', { cwd, mcpServers: [] });
|
|
1828
|
+
sessionId = session.sessionId;
|
|
1829
|
+
}
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
const message = Number(error?.code) === -32000
|
|
1832
|
+
? 'Antigravity is not signed in: open the Antigravity panel (or run `agy` in a terminal) and sign in, then try again.'
|
|
1833
|
+
: providerErrorMessage(error, resumeSessionId ? 'Antigravity could not resume the run\'s session' : 'Antigravity could not start a session');
|
|
1834
|
+
return fail(Object.assign(new Error(message), { code: Number(error?.code) === -32000 ? 'AUTH_REQUIRED' : (error?.code || 'START_FAILED') }));
|
|
1835
|
+
}
|
|
1836
|
+
onIdentity({ providerSessionId: sessionId, ...(homeDir ? { antigravityHome: homeDir } : {}) });
|
|
1837
|
+
|
|
1838
|
+
// The model and the thinking level, as the session's own config options name them. A model id with its level
|
|
1839
|
+
// (gemini-4-argon-high, as `agy models` and ACP's model list spell it) is that model at that level.
|
|
1840
|
+
let configOptions = Array.isArray(session?.configOptions) ? session.configOptions : [];
|
|
1841
|
+
const offered = (category) => {
|
|
1842
|
+
const option = configOptions.find((o) => o?.category === category);
|
|
1843
|
+
const accepted = [];
|
|
1844
|
+
for (const entry of option?.options || []) for (const v of Array.isArray(entry?.options) ? entry.options : [entry]) if (v && v.value !== undefined) accepted.push(String(v.value));
|
|
1845
|
+
return { option, accepted };
|
|
1846
|
+
};
|
|
1847
|
+
const setOption = async (category, value, label) => {
|
|
1848
|
+
if (!value) return;
|
|
1849
|
+
const { option, accepted } = offered(category);
|
|
1850
|
+
if (!option) {
|
|
1851
|
+
if (category === 'model') {
|
|
1852
|
+
try { await conn.request('session/set_model', { sessionId, modelId: String(value) }); } catch (error) { onNote?.(`Antigravity kept its default model: ${providerErrorMessage(error, 'set_model failed')}`); }
|
|
1853
|
+
}
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (!accepted.includes(String(value))) {
|
|
1857
|
+
if (category === 'model') throw Object.assign(new Error(`Antigravity does not offer the model ${value} (it offers ${accepted.join(', ') || 'none'}).`), { code: 'MODEL_UNAVAILABLE' });
|
|
1858
|
+
onNote?.(`Antigravity has no ${label} "${value}" for this model; it keeps ${option.currentValue}.`);
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (String(option.currentValue) === String(value)) return;
|
|
1862
|
+
const answer = await conn.request('session/set_config_option', { sessionId, configId: option.id, value: String(value) });
|
|
1863
|
+
if (Array.isArray(answer?.configOptions)) configOptions = answer.configOptions;
|
|
1864
|
+
};
|
|
1865
|
+
const wanted = splitAntigravityModel(model, offered('model').accepted);
|
|
1866
|
+
const level = effort && !['off', 'default', 'auto', 'none'].includes(String(effort).toLowerCase()) ? String(effort) : wanted.effort;
|
|
1867
|
+
try {
|
|
1868
|
+
await setOption('model', wanted.model, 'model');
|
|
1869
|
+
await setOption('thought_level', level, 'thinking level');
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
return fail(error);
|
|
1872
|
+
}
|
|
1873
|
+
const optionValue = (category) => configOptions.find((o) => o?.category === category)?.currentValue ?? null;
|
|
1874
|
+
|
|
1875
|
+
return {
|
|
1876
|
+
identity: () => ({ providerSessionId: sessionId, ...(homeDir ? { antigravityHome: homeDir } : {}) }),
|
|
1877
|
+
isAlive: () => alive && conn.running,
|
|
1878
|
+
processInfo: () => ({ pid: conn.pid, startedAt, home: homeDir || null }),
|
|
1879
|
+
describe: () => ({
|
|
1880
|
+
provider: 'antigravity', permissionPolicy, capability, askMode, model: optionValue('model'), effort: optionValue('thought_level'),
|
|
1881
|
+
...(homeDir ? { home: homeDir, servers: [...(allowedServers || [])], hook: !!registration } : {}),
|
|
1882
|
+
...(guard ? { guard: { mode: guard.mode, capability: guard.capability, writable: [...guard.writable] } } : {}),
|
|
1883
|
+
}),
|
|
1884
|
+
async runTurn(prompt, meta = {}) {
|
|
1885
|
+
if (!alive || !conn.running) throw new Error('Antigravity native loop is closed');
|
|
1886
|
+
if (activeTurn) throw new Error('Antigravity native loop already has an active turn');
|
|
1887
|
+
providerTurn += 1;
|
|
1888
|
+
const turn = { iteration: meta.iteration, providerTurn, text: '', cancelled: false, controller: new AbortController() };
|
|
1889
|
+
activeTurn = turn;
|
|
1890
|
+
emit('synabun.user_prompt', { type: 'synabun.user_prompt', text: String(prompt || '') }, { iteration: meta.iteration, providerTurn });
|
|
1891
|
+
const schema = meta.outputSchema || outputSchema;
|
|
1892
|
+
const body = schema && typeof schema === 'object'
|
|
1893
|
+
? `${String(prompt || '')}\n\nEnd your reply with one JSON object (in a \`\`\`json block) that matches this JSON Schema:\n${JSON.stringify(schema)}`
|
|
1894
|
+
: String(prompt || '');
|
|
1895
|
+
try {
|
|
1896
|
+
const answer = await conn.request('session/prompt', { sessionId, prompt: [{ type: 'text', text: body }] }, { timeoutMs: 0 });
|
|
1897
|
+
// The turn's own tokens, whatever its end (a stopped turn spent them too).
|
|
1898
|
+
const usage = acpTurnUsage(answer?.usage, lastSessionUsage);
|
|
1899
|
+
if (answer?.usage) {
|
|
1900
|
+
lastSessionUsage = answer.usage;
|
|
1901
|
+
try { onSessionUsage?.(answer.usage); } catch { /* bookkeeping */ }
|
|
1902
|
+
}
|
|
1903
|
+
if (answer?.stopReason === 'cancelled' || turn.cancelled) throw Object.assign(asAbortError('aborted'), { usage, sessionUsage: answer?.usage || null, providerSessionId: sessionId });
|
|
1904
|
+
const text = turn.text;
|
|
1905
|
+
const result = { providerSessionId: sessionId, text, usage, sessionUsage: answer?.usage || null, stopReason: answer?.stopReason || 'end_turn', result: { text, usage } };
|
|
1906
|
+
if (answer?.stopReason === 'refusal') result.refused = true;
|
|
1907
|
+
if (schema && typeof schema === 'object') {
|
|
1908
|
+
const structured = lastJsonObject(text);
|
|
1909
|
+
if (structured) result.structured = structured;
|
|
1910
|
+
}
|
|
1911
|
+
return result;
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
if (error?.name === 'AbortError') throw error;
|
|
1914
|
+
if (!alive || !conn.running) throw new Error(`Antigravity stopped during the turn${conn.stderrTail() ? `: ${conn.stderrTail().slice(-400)}` : ''}`);
|
|
1915
|
+
throw new Error(providerErrorMessage(error, 'Antigravity turn failed'));
|
|
1916
|
+
} finally {
|
|
1917
|
+
turn.controller.abort();
|
|
1918
|
+
if (activeTurn === turn) activeTurn = null;
|
|
1919
|
+
}
|
|
1920
|
+
},
|
|
1921
|
+
async abort() {
|
|
1922
|
+
if (activeTurn) { activeTurn.cancelled = true; activeTurn.controller.abort(); }
|
|
1923
|
+
for (const done of [...openPermissions]) done({ outcome: { outcome: 'cancelled' } });
|
|
1924
|
+
try { if (sessionId && conn.running) await settleWithin(conn.notify('session/cancel', { sessionId })); } catch {}
|
|
1925
|
+
return true;
|
|
1926
|
+
},
|
|
1927
|
+
// The process is the run's own: it always ends with the run. Its conversation stays in the run's home, where a
|
|
1928
|
+
// resume after a restart finds it (the dispatcher's prune removes the home once the run is gone).
|
|
1929
|
+
async dispose() {
|
|
1930
|
+
const wasAlive = alive;
|
|
1931
|
+
alive = false;
|
|
1932
|
+
activeTurn?.controller.abort();
|
|
1933
|
+
for (const done of [...openPermissions]) done({ outcome: { outcome: 'cancelled' } });
|
|
1934
|
+
if (wasAlive && activeTurn && sessionId && conn.running) { try { await settleWithin(conn.notify('session/cancel', { sessionId })); } catch {} }
|
|
1935
|
+
await settleWithin(conn.close(), 3000);
|
|
1936
|
+
registration?.dispose();
|
|
1937
|
+
},
|
|
1938
|
+
};
|
|
1939
|
+
}
|