synabun 2.0.10 → 2.1.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 +86 -0
- package/README.md +25 -2
- package/THIRD-PARTY-LICENSES.md +22 -0
- package/bin/synabun.js +71 -24
- package/docs/whatsapp.md +2 -2
- 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 +17 -8
- package/lib/desktop-app.js +7 -1
- package/lib/external-tools.js +1 -1
- 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 +228 -0
- package/lib/virustotal.mjs +192 -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/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/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/neural-interface.d.ts +16 -10
- 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 +20 -62
- 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/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 +10 -10
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +22 -13
- 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/image-tools.d.ts +1 -1
- package/mcp-server/dist/tools/image-tools.js +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 +639 -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/codex-config-heal.ts +80 -29
- package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
- package/mcp-server/src/services/neural-interface.ts +79 -20
- package/mcp-server/src/services/server-instructions.ts +27 -63
- 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/whiteboard-geometry.ts +94 -9
- package/mcp-server/src/tools/agents.ts +22 -14
- package/mcp-server/src/tools/image-tools.ts +1 -1
- package/mcp-server/src/tools/skill-tools.ts +589 -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 +3463 -200
- package/neural-interface/i18n/pt-BR.json +3411 -151
- package/neural-interface/lib/assistant-api.js +127 -28
- 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-catalog.js +6 -7
- package/neural-interface/lib/assistant-dispatch.js +871 -156
- package/neural-interface/lib/assistant-envelope.js +3 -1
- package/neural-interface/lib/assistant-persona.js +80 -11
- package/neural-interface/lib/assistant-plan-permissions.js +10 -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 +13 -8
- package/neural-interface/lib/assistant-runtime.js +56 -18
- package/neural-interface/lib/assistant-task-prompt.js +78 -14
- package/neural-interface/lib/assistant-tools.js +51 -0
- package/neural-interface/lib/backup-service.js +620 -3
- 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 +83 -0
- package/neural-interface/lib/browser-network-guard.js +284 -0
- package/neural-interface/lib/browser-policy-roots.js +64 -0
- package/neural-interface/lib/browser-tool-policy.js +186 -31
- package/neural-interface/lib/claude-agent-bridge.js +4 -2
- package/neural-interface/lib/claude-hooks-status.js +103 -0
- package/neural-interface/lib/claude-temporary.js +5 -11
- package/neural-interface/lib/cli-install.js +18 -138
- package/neural-interface/lib/cli-resolver.js +4 -1
- package/neural-interface/lib/cli-update-planner.js +32 -31
- package/neural-interface/lib/cloudflared/tunnel.js +42 -2
- package/neural-interface/lib/codex-mcp-approvals.js +11 -0
- package/neural-interface/lib/codex-worker-app-server.js +172 -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/exec-loop-command.js +4 -8
- 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 +0 -29
- package/neural-interface/lib/mcp-external-servers.js +323 -0
- package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
- package/neural-interface/lib/morelogin.js +30 -0
- package/neural-interface/lib/native-loop-providers.js +547 -47
- package/neural-interface/lib/native-loop-runtime.js +268 -41
- 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 +44 -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/peer-identity.js +763 -0
- package/neural-interface/lib/request-gate.js +17 -2
- package/neural-interface/lib/rulesets/api.js +1 -1
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +92 -27
- package/neural-interface/lib/rulesets/json-edit.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +17 -30
- 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 +294 -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 +267 -0
- package/neural-interface/lib/skills/creation.js +843 -0
- package/neural-interface/lib/skills/deploy.js +564 -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 +214 -0
- package/neural-interface/lib/skills/eval-confinement.js +397 -0
- package/neural-interface/lib/skills/eval-sandbox.js +280 -0
- package/neural-interface/lib/skills/eval-trace.js +119 -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 +200 -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 +387 -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 +262 -0
- package/neural-interface/lib/skills/runtime-checks.js +355 -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 +522 -0
- package/neural-interface/lib/skills/telemetry.js +113 -0
- package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
- package/neural-interface/lib/tool-lists.js +124 -0
- package/neural-interface/lib/whatsapp/bridge.js +6 -3
- package/neural-interface/lib/worker-confinement.js +1192 -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-relay-ports.js +227 -0
- package/neural-interface/lib/worker-resume.js +229 -0
- package/neural-interface/lib/worker-sandbox.js +299 -0
- package/neural-interface/package-lock.json +18 -12
- package/neural-interface/package.json +2 -2
- package/neural-interface/public/index.html +3 -1
- package/neural-interface/public/index2d.html +19 -11
- package/neural-interface/public/onboarding.html +2 -2
- package/neural-interface/public/shared/account-menu.js +8 -5
- package/neural-interface/public/shared/agent-runtime-options.js +35 -19
- 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 +33 -2
- 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 +49 -35
- package/neural-interface/public/shared/assistant/asst-control.js +157 -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-limit-dock.js +40 -0
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -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 +106 -11
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
- package/neural-interface/public/shared/assistant/asst-render.js +401 -77
- package/neural-interface/public/shared/assistant/asst-route.js +83 -12
- package/neural-interface/public/shared/assistant/asst-scenes.js +2 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
- package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
- package/neural-interface/public/shared/assistant/asst-state.js +11 -15
- package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +59 -21
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +156 -16
- 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 +8 -7
- package/neural-interface/public/shared/cdx/cdx-panel.js +45 -149
- package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
- package/neural-interface/public/shared/cdx/cdx-render.js +47 -29
- package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
- package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +11 -3
- package/neural-interface/public/shared/cdx/cdx-tabs.js +52 -39
- package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
- package/neural-interface/public/shared/cli-install-view.js +4 -8
- package/neural-interface/public/shared/cli-status.js +2 -2
- package/neural-interface/public/shared/constants.js +2 -2
- 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 +366 -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/crash-notice-view.js +168 -0
- package/neural-interface/public/shared/desktop-update-view.js +20 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
- package/neural-interface/public/shared/html-shell.js +9 -11
- 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 +47 -252
- 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-render.js +6 -21
- 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 +0 -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 +14 -47
- package/neural-interface/public/shared/page-session.js +96 -0
- 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-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/skills/ss-args.js +96 -0
- package/neural-interface/public/shared/skills/ss-creation.js +219 -0
- package/neural-interface/public/shared/skills/ss-dialog.js +130 -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 +141 -0
- package/neural-interface/public/shared/skills/ss-presets.js +117 -0
- package/neural-interface/public/shared/skills/ss-profiles.js +475 -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 +236 -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 +229 -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-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 +279 -0
- package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
- package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
- package/neural-interface/public/shared/skills/studio/sx-main.js +681 -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 +248 -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 +425 -0
- package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
- package/neural-interface/public/shared/styles.css +1525 -1779
- package/neural-interface/public/shared/synabun-mascot.js +1115 -9
- 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/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-automation-studio.js +91 -128
- package/neural-interface/public/shared/ui-claude-panel.js +77 -5
- 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-image-gallery.js +50 -47
- package/neural-interface/public/shared/ui-keybinds.js +26 -1
- package/neural-interface/public/shared/ui-menubar.js +8 -18
- package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +7 -1
- package/neural-interface/public/shared/ui-navbar.js +4 -24
- package/neural-interface/public/shared/ui-panels.js +96 -65
- package/neural-interface/public/shared/ui-schedules-studio.js +507 -423
- package/neural-interface/public/shared/ui-sessions.js +5 -3
- package/neural-interface/public/shared/ui-settings.js +594 -462
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +1 -1
- package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
- package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
- 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 +124 -180
- package/neural-interface/public/shared/ui-tool-updates.js +3 -1
- package/neural-interface/public/shared/ui-tooltip.js +10 -5
- 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 +311 -58
- package/neural-interface/public/shared/ui-worker-end.js +4 -0
- 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 +1 -1
- 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-restart.js +12 -0
- package/neural-interface/public/sidepanel-session.html +1 -1
- 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 +3934 -2454
- package/neural-interface/templates/rulesets/core.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
- package/neural-interface/templates/rulesets/manifest.json +8 -8
- package/npm-shrinkwrap.json +18 -12
- package/package.json +16 -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 +4 -4
- 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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — dispatched workers across a server restart
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// A worker's provider process is a child of the Neural Interface, so every
|
|
6
|
+
// restart of the server (`node --watch` on an edit of its code, an in-app
|
|
7
|
+
// restart, an update, Ctrl+C, a crash) used to end the run as interrupted. The
|
|
8
|
+
// dispatcher (assistant-dispatch.js) now suspends such a run at the shutdown and
|
|
9
|
+
// resumes its provider session on the next start, as the same run. This module is
|
|
10
|
+
// what that decision and the resume need besides the dispatcher's own state:
|
|
11
|
+
//
|
|
12
|
+
// resumeRefusal why a run may not be resumed, or null
|
|
13
|
+
// readCostState what the resumed CLI restores from its transcript (its cost
|
|
14
|
+
// counter and per-model usage), so its first turn is charged
|
|
15
|
+
// only for itself
|
|
16
|
+
// endOrphanProcess a CLI that outlived its server (a crash: no shutdown ended
|
|
17
|
+
// it), ended only once its command line shows it is that run's
|
|
18
|
+
//
|
|
19
|
+
// Measured with the SDK and the Claude Code SynaBun runs (0.3.288 / 2.1.310):
|
|
20
|
+
// a session whose CLI was killed in the middle of a tool call resumes, the CLI
|
|
21
|
+
// records that call as interrupted with its outcome unknown, and the next model
|
|
22
|
+
// call goes through; a CLI that gets SIGTERM ends the tool's process tree,
|
|
23
|
+
// records the call's result and its cost state, and exits within a second.
|
|
24
|
+
|
|
25
|
+
import { execFileSync } from 'node:child_process';
|
|
26
|
+
import { closeSync, existsSync, fstatSync, openSync, readSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
29
|
+
|
|
30
|
+
/** A run is resumed when the server is back within this long of its last update, and at most this many times. */
|
|
31
|
+
export const RESUME_WINDOW_MS = 15 * 60_000;
|
|
32
|
+
export const MAX_RESUMES = 3;
|
|
33
|
+
|
|
34
|
+
/** Rebuild OS isolation without dropping an earlier denial or relaxing strict local networking.
|
|
35
|
+
* The channel is a fresh listener/token each boot; only that listener replaces the old allow-port.
|
|
36
|
+
*/
|
|
37
|
+
export function resumeSandboxPlan(current, previous, channelPort) {
|
|
38
|
+
if (!current || !previous) throw new Error('the original worker confinement is unavailable');
|
|
39
|
+
const paths = (a) => [...new Set(a || [])].sort();
|
|
40
|
+
if (JSON.stringify(paths(current.allowPaths)) !== JSON.stringify(paths(previous.allowPaths))) {
|
|
41
|
+
throw new Error('the worker confinement folders changed');
|
|
42
|
+
}
|
|
43
|
+
const ranges = [current.denyPortRange, previous.denyPortRange].filter(Boolean);
|
|
44
|
+
return {
|
|
45
|
+
...current,
|
|
46
|
+
denyPorts: [...new Set([...(current.denyPorts || []), ...(previous.denyPorts || [])])],
|
|
47
|
+
denyLocalPorts: [...new Set([...(current.denyLocalPorts || []), ...(previous.denyLocalPorts || [])])],
|
|
48
|
+
denyPaths: [...new Set([...(current.denyPaths || []), ...(previous.denyPaths || [])])],
|
|
49
|
+
denyPortRange: ranges.length ? { from: Math.min(...ranges.map(r => r.from)), to: Math.max(...ranges.map(r => r.to)) } : null,
|
|
50
|
+
strictLocal: previous.strictLocal || current.strictLocal ? { allowPorts: [channelPort].filter(Boolean) } : null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Why `entry` (a dispatcher registry entry) cannot be resumed, or null. `atShutdown`: only what is known while the
|
|
56
|
+
* server goes down (the rest is checked at the next start: the time it was away, its folder, its account).
|
|
57
|
+
*
|
|
58
|
+
* @param {object} entry
|
|
59
|
+
* @param {object} o
|
|
60
|
+
* @param {number} [o.now]
|
|
61
|
+
* @param {number} [o.workLeftMs] the run's work time left (max_minutes)
|
|
62
|
+
* @param {boolean} [o.atShutdown]
|
|
63
|
+
* @param {(path: string) => boolean} [o.isDirectory]
|
|
64
|
+
* @param {(accountId: string) => string} [o.accountUnusable] '' when the Claude account can be used
|
|
65
|
+
*/
|
|
66
|
+
export function resumeRefusal(entry, { now = Date.now(), workLeftMs = Infinity, atShutdown = false, isDirectory = () => true, accountUnusable = () => '' } = {}) {
|
|
67
|
+
if (!entry) return 'the run is not known';
|
|
68
|
+
if (!['claude-code', 'codex', 'opencode'].includes(entry.provider)) return 'this provider cannot continue after a restart';
|
|
69
|
+
if (entry.confinement?.kind === 'skill-eval') return 'a skill evaluation run is a measurement and starts over instead';
|
|
70
|
+
if (entry.confinement) return 'a skill drafting run is not resumed';
|
|
71
|
+
if (entry.removedAt) return 'the run was removed';
|
|
72
|
+
if (!(entry.provider === 'codex' ? entry.providerThreadId : entry.providerSessionId)) return `its ${entry.provider === 'codex' ? 'Codex thread' : entry.provider === 'opencode' ? 'OpenCode session' : 'Claude session'} had not started yet`;
|
|
73
|
+
if (entry.provider === 'opencode' && !entry.resumeInfo?.persistentOpenCode) return 'its OpenCode session has no durable run data';
|
|
74
|
+
const resumes = Number(entry.resumes) || 0;
|
|
75
|
+
if (resumes >= MAX_RESUMES) return `it was resumed ${resumes} times already`;
|
|
76
|
+
if (!(workLeftMs > 0)) return entry.maxMinutes ? `its ${entry.maxMinutes} minutes of work time are used up` : 'its work time is used up';
|
|
77
|
+
if (atShutdown) return null;
|
|
78
|
+
const since = Date.parse(entry.suspendedAt || '') || Date.parse(entry.updatedAt || '') || 0;
|
|
79
|
+
if (since && now - since > RESUME_WINDOW_MS) return `the server was back ${Math.round((now - since) / 60_000)} minutes later (more than ${Math.round(RESUME_WINDOW_MS / 60_000)})`;
|
|
80
|
+
if (!entry.cwd || !isDirectory(entry.cwd)) return `its working directory is gone (${entry.cwd || 'none'})`;
|
|
81
|
+
let unusable = '';
|
|
82
|
+
try { unusable = entry.provider === 'opencode' ? '' : String(accountUnusable((entry.provider === 'codex' ? entry.codexAccountId : entry.claudeAccountId) || 'default', entry.provider, entry) || ''); } catch (error) { unusable = error?.message || 'it could not be read'; }
|
|
83
|
+
if (unusable) return `its ${entry.provider === 'codex' ? 'Codex' : 'Claude'} account (${entry.codexAccountId || entry.claudeAccountId || 'default'}) cannot be used: ${unusable}`;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Whether `path` is `root` or inside it (both resolved; a missing path is compared as written). */
|
|
88
|
+
export function isInside(path, root) {
|
|
89
|
+
if (!path || !root) return false;
|
|
90
|
+
const real = (value) => { try { return realpathSync(value); } catch { return resolve(String(value)); } };
|
|
91
|
+
const rel = relative(real(root), real(path));
|
|
92
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !isAbsolute(rel));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** "14:05": a time of day on this computer's clock, or '' for no time. */
|
|
96
|
+
export function clockTime(value) {
|
|
97
|
+
const ms = typeof value === 'number' ? value : Date.parse(value || '');
|
|
98
|
+
if (!Number.isFinite(ms) || ms <= 0) return '';
|
|
99
|
+
const date = new Date(ms);
|
|
100
|
+
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The transcript of a Claude session: <config dir>/projects/<project folder>/<sessionId>.jsonl, the config dir being
|
|
105
|
+
* the run's account's (CLAUDE_CONFIG_DIR) or ~/.claude. Every project folder is looked in (the CLI names it after
|
|
106
|
+
* the real path of the working directory). null when there is none.
|
|
107
|
+
*/
|
|
108
|
+
export function findClaudeTranscript({ sessionId, configDir = null, home = homedir() } = {}) {
|
|
109
|
+
const id = String(sessionId || '');
|
|
110
|
+
if (!/^[0-9a-f-]{8,64}$/i.test(id)) return null;
|
|
111
|
+
const projects = join(configDir || join(home, '.claude'), 'projects');
|
|
112
|
+
let folders = [];
|
|
113
|
+
try { folders = readdirSync(projects, { withFileTypes: true }).filter((item) => item.isDirectory()).map((item) => item.name); } catch { return null; }
|
|
114
|
+
for (const folder of folders) {
|
|
115
|
+
const path = join(projects, folder, `${id}.jsonl`);
|
|
116
|
+
try { if (statSync(path).isFile()) return path; } catch { /* not here */ }
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The last `maxBytes` of a file as text (a partial first line is dropped). */
|
|
122
|
+
function readTail(path, maxBytes = 1024 * 1024) {
|
|
123
|
+
let fd = null;
|
|
124
|
+
try {
|
|
125
|
+
fd = openSync(path, 'r');
|
|
126
|
+
const size = fstatSync(fd).size;
|
|
127
|
+
const length = Math.min(size, maxBytes);
|
|
128
|
+
const buffer = Buffer.alloc(length);
|
|
129
|
+
readSync(fd, buffer, 0, length, size - length);
|
|
130
|
+
const text = buffer.toString('utf8');
|
|
131
|
+
return size > length ? text.slice(text.indexOf('\n') + 1) : text;
|
|
132
|
+
} catch { return ''; } finally { if (fd !== null) { try { closeSync(fd); } catch { /* closed */ } } }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The cost state a resumed CLI starts from: the last `cost-state` line of the session's transcript, which the CLI
|
|
137
|
+
* writes when it exits cleanly (a SIGTERM included) and reads back when the session is resumed. →
|
|
138
|
+
* { totalCostUsd, modelUsage } (modelUsage as result.modelUsage spells it), or null when there is none (a CLI that
|
|
139
|
+
* was killed outright starts its counters at zero).
|
|
140
|
+
*/
|
|
141
|
+
export function readCostState(path, sessionId = null) {
|
|
142
|
+
if (!path || !existsSync(path)) return null;
|
|
143
|
+
const lines = readTail(path).split('\n');
|
|
144
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
145
|
+
const line = lines[i];
|
|
146
|
+
if (!line.includes('"cost-state"')) continue;
|
|
147
|
+
let row = null;
|
|
148
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
149
|
+
if (row?.type !== 'cost-state') continue;
|
|
150
|
+
if (sessionId && row.sessionId && row.sessionId !== sessionId) continue;
|
|
151
|
+
const total = Number(row.totalCostUSD);
|
|
152
|
+
return {
|
|
153
|
+
totalCostUsd: Number.isFinite(total) && total > 0 ? total : 0,
|
|
154
|
+
modelUsage: row.modelUsage && typeof row.modelUsage === 'object' && !Array.isArray(row.modelUsage) ? row.modelUsage : null,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Whether a process with this pid exists (one of another user's counts as existing). */
|
|
161
|
+
export function processExists(pid) {
|
|
162
|
+
const id = Number(pid);
|
|
163
|
+
if (!Number.isInteger(id) || id <= 0) return false;
|
|
164
|
+
try { process.kill(id, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The command line of a process, or null when it cannot be read (no such process, no `ps`, Windows without CIM). */
|
|
168
|
+
export function processCommandLine(pid, { platform = process.platform, run = execFileSync } = {}) {
|
|
169
|
+
const id = Number(pid);
|
|
170
|
+
if (!Number.isInteger(id) || id <= 0) return null;
|
|
171
|
+
try {
|
|
172
|
+
if (platform === 'win32') {
|
|
173
|
+
const out = run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${id}").CommandLine`], { encoding: 'utf8', timeout: 5000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
174
|
+
return String(out || '').trim() || null;
|
|
175
|
+
}
|
|
176
|
+
const out = run('ps', ['-o', 'command=', '-p', String(id)], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
177
|
+
return String(out || '').trim() || null;
|
|
178
|
+
} catch { return null; }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A run's own Claude Code CLI: started by the Agent SDK (stream-json both ways) with the run's id in its SynaBun MCP
|
|
183
|
+
* headers (X-Synabun-Terminal). Anything else with the pid is left alone.
|
|
184
|
+
*/
|
|
185
|
+
export function isRunCliCommand(command, runId) {
|
|
186
|
+
const text = String(command || '');
|
|
187
|
+
return !!runId && text.includes(String(runId)) && ((text.includes('--input-format') && text.includes('stream-json')) || (text.includes('app-server') && text.includes('synabun_worker_run=')));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Windows: end a process and every process it started (taskkill /T /F, as the server's own shutdown does). */
|
|
191
|
+
export function endProcessTree(pid, { run = execFileSync } = {}) {
|
|
192
|
+
run('taskkill', ['/pid', String(pid), '/T', '/F'], { timeout: 5000, windowsHide: true, stdio: 'ignore' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* End the CLI a run had before the server went down, when it is still there (a crash ended no process). It is ended
|
|
197
|
+
* only when its command line shows it is that run's CLI: SIGTERM, then SIGKILL after `graceMs` once the pid still
|
|
198
|
+
* shows that command line (it may have gone to another process meanwhile). Windows has no SIGTERM (a signal stops the
|
|
199
|
+
* CLI alone and leaves its tool's processes, a dev server keeps its port): the tree is ended with taskkill.
|
|
200
|
+
* → { state: 'gone' | 'ended' | 'killed' | 'still_running' | 'not_ours' | 'unknown' }.
|
|
201
|
+
* 'unknown': a process has the pid but its command line cannot be read, so it is not touched.
|
|
202
|
+
*/
|
|
203
|
+
export async function endOrphanProcess({ pid, runId, graceMs = 2000, platform = process.platform, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), readCommand = processCommandLine, exists = processExists, kill = (id, signal) => process.kill(id, signal), killTree = endProcessTree } = {}) {
|
|
204
|
+
const id = Number(pid);
|
|
205
|
+
if (!Number.isInteger(id) || id <= 0 || id === process.pid || !exists(id)) return { state: 'gone' };
|
|
206
|
+
const command = readCommand(id);
|
|
207
|
+
// No command line: a process that exited meanwhile is gone; one still there is not touched.
|
|
208
|
+
if (command === null) return { state: exists(id) ? 'unknown' : 'gone' };
|
|
209
|
+
if (!isRunCliCommand(command, runId)) return { state: 'not_ours' };
|
|
210
|
+
if (platform === 'win32') {
|
|
211
|
+
try { killTree(id); } catch { /* gone meanwhile, or not allowed: checked below */ }
|
|
212
|
+
for (let waited = 0; waited < graceMs; waited += 100) {
|
|
213
|
+
await sleep(100);
|
|
214
|
+
if (!exists(id)) return { state: 'killed' };
|
|
215
|
+
}
|
|
216
|
+
return { state: 'still_running' };
|
|
217
|
+
}
|
|
218
|
+
try { kill(id, 'SIGTERM'); } catch { /* gone meanwhile */ }
|
|
219
|
+
for (let waited = 0; waited < graceMs; waited += 100) {
|
|
220
|
+
await sleep(100);
|
|
221
|
+
if (!exists(id)) return { state: 'ended' };
|
|
222
|
+
}
|
|
223
|
+
const again = readCommand(id);
|
|
224
|
+
if (again === null) return { state: exists(id) ? 'still_running' : 'ended' };
|
|
225
|
+
if (!isRunCliCommand(again, runId)) return { state: 'ended' };
|
|
226
|
+
try { kill(id, 'SIGKILL'); } catch { /* gone meanwhile */ }
|
|
227
|
+
await sleep(200);
|
|
228
|
+
return { state: exists(id) ? 'still_running' : 'killed' };
|
|
229
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — macOS Seatbelt isolation for guarded worker trees (pure + small fs)
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// A dispatched worker (capability read-only / workspace, Protect workers = Enforce) can reach SynaBun's own control
|
|
6
|
+
// listener on localhost:<NEURAL_PORT> from any program it runs — node -e, python, a script, a detached process —
|
|
7
|
+
// fetch the owner's page cookie, and act as the owner (dispatch a full worker, turn Protect workers off, install a
|
|
8
|
+
// skill). The in-code shell guard is only a speed bump: arbitrary code is opaque to it.
|
|
9
|
+
//
|
|
10
|
+
// On macOS the real boundary is the OS: run the worker's whole process tree under sandbox-exec with a profile that
|
|
11
|
+
// blocks outbound connections to SynaBun's control port (and the other local endpoints that can relay commands or
|
|
12
|
+
// owner authority), while leaving everything legitimate working — the internet, reads anywhere, writes per
|
|
13
|
+
// capability, the project's tests, and the worker's own SynaBun MCP tools, which reach SynaBun through a worker-only
|
|
14
|
+
// channel on a port the profile leaves open (see lib/worker-mcp-channel.js).
|
|
15
|
+
//
|
|
16
|
+
// Proven on macOS 26 (scratch probes, 2026-10-23 / 2026-10-24), the facts this module relies on:
|
|
17
|
+
// • `(deny network-outbound (remote ip "*:<port>"))` blocks that port on EVERY host — 127.0.0.1, localhost, ::1,
|
|
18
|
+
// [::ffff:127.0.0.1] (the IPv4-mapped IPv6 form a dual-stack app.listen answers on), 0.0.0.0, this Mac's LAN /
|
|
19
|
+
// interface IPs and its hostname — while a different port and the public internet stay open. The earlier
|
|
20
|
+
// `localhost:<port>` literal did NOT cover [::ffff:127.0.0.1] (a verified full bypass of the whole layer on the
|
|
21
|
+
// dual-stack listener): Seatbelt's `localhost` resolves to 127.0.0.1 + ::1 only. The host literal must be `*`
|
|
22
|
+
// or `localhost` (a numeric IP literal is rejected by the parser); `*` is the only form that is address-proof.
|
|
23
|
+
// Seatbelt has no port-range syntax (`*:4120-4400` is rejected), so a reserved range is emitted one port a line.
|
|
24
|
+
// • `(deny file-read* file-write* (subpath "<canonical>"))` blocks reads AND writes under a path; a later
|
|
25
|
+
// `(allow file-read* file-write* (subpath "<canonical>"))` wins (Seatbelt is last-match-wins within a filter),
|
|
26
|
+
// so SynaBun's whole data home can be denied and the run's own scratch / cwd / config folder allowed back after
|
|
27
|
+
// it. Seatbelt matches the RESOLVED path, so every literal must be canonicalised (/tmp is a symlink to
|
|
28
|
+
// /private/tmp; an un-resolved literal never matches, and an allow-back must resolve the same way as its deny).
|
|
29
|
+
// • The sandbox is inherited by detached / nohup grandchildren, so a worker cannot escape it by backgrounding.
|
|
30
|
+
// • A same-user `KERN_PROCARGS2` read (ps -E / a tiny C program) CANNOT be blocked by Seatbelt (it is a same-uid
|
|
31
|
+
// kernel allowance, not a sandbox-governed operation). So a worker inside the sandbox can still read another
|
|
32
|
+
// same-user process's argv+env — including a brain's SYNABUN_BRAIN_TOKEN, or a relay's password in env. That
|
|
33
|
+
// theft is harmless here because the token/password is useless without reaching a port this profile blocks on
|
|
34
|
+
// every address; a Codex worker (which this module cannot wrap — nested sandbox-exec is refused) is held by
|
|
35
|
+
// running with its network off instead.
|
|
36
|
+
//
|
|
37
|
+
// This module is macOS-only. On every other platform sandboxSupported() is false; and even on macOS the caller
|
|
38
|
+
// gates on sandboxExecAvailable() (a one-time boot probe that /usr/bin/sandbox-exec exists and runs a trivial
|
|
39
|
+
// allow-default profile), falling back to the in-code guard alone when it does not. Pure but for canonicalising
|
|
40
|
+
// paths, the boot probe, and writing the profile / wrapper into a run's own scratch folder; unit-tested in
|
|
41
|
+
// tests/worker-sandbox.test.mjs.
|
|
42
|
+
//
|
|
43
|
+
// NOT EQUIVALENT OFF macOS (stated plainly; HIGH elsewhere). Where this module does not apply — Linux, Windows, or a
|
|
44
|
+
// macOS host whose sandbox-exec is unavailable — a protected (Enforce, read-only/workspace) worker is held only by the
|
|
45
|
+
// in-code guard (the shell heuristic + the tool hooks), which reads the command line only and so is a lexical
|
|
46
|
+
// speed-bump, not a boundary: a script file (`node /path/x.js`), hidden-address inline code, or any program doing its
|
|
47
|
+
// own networking slips past it. Two things the macOS profile closes that nothing else does there: (a) SynaBun's
|
|
48
|
+
// OpenCode serve band 4096-4400 is unblocked, so a worker's script can connect to a serve; (b) a SHARED (non-protected)
|
|
49
|
+
// serve keeps its OPENCODE_PASSWORD in its process environment (lib/opencode-protocol.js serveSpecFor env mode; only a
|
|
50
|
+
// protected worker's OWN isolated serve hides it via passwordFrom 'stdout'), and a same-uid process can read another
|
|
51
|
+
// process's environment (KERN_PROCARGS2 / /proc/<pid>/environ) — a read no sandbox blocks — so a prompt-injected worker
|
|
52
|
+
// off macOS could read that password and open a full-control session on the shared serve. On macOS under Enforce the
|
|
53
|
+
// 4096-4400 deny (plus the data-home/credential denies below) is the real boundary that stops both. The protected-
|
|
54
|
+
// worker IN-FOLDER confinement off macOS is therefore best-effort, not a guarantee. See docs/assistant.md → Worker
|
|
55
|
+
// confinement → "Protected-worker in-folder confinement off macOS is NOT equivalent to macOS".
|
|
56
|
+
|
|
57
|
+
import { chmodSync, existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
58
|
+
import { spawnSync } from 'node:child_process';
|
|
59
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
60
|
+
|
|
61
|
+
export const SANDBOX_EXEC = '/usr/bin/sandbox-exec';
|
|
62
|
+
/**
|
|
63
|
+
* The ports the internet needs (SSH, DNS, HTTP, HTTPS). A relay found listening on one of them is denied on this
|
|
64
|
+
* computer's own addresses only (`localhost:<port>`: 127.0.0.1, ::1, every interface address and the hostname —
|
|
65
|
+
* not the IPv4-mapped spelling, which only `*` covers), never with `*:<port>`, which would cut the worker's internet.
|
|
66
|
+
* The strict profile (detection failed) allows exactly these, plus the worker channel.
|
|
67
|
+
*/
|
|
68
|
+
export const INTERNET_PORTS = Object.freeze([22, 53, 80, 443]);
|
|
69
|
+
/** Local ports no guarded worker has a reason to open: SSH to this computer (Remote Login) is a shell outside the
|
|
70
|
+
* sandbox. Denied on this computer's own addresses in every guarded profile; ssh to other hosts is untouched. */
|
|
71
|
+
export const ALWAYS_LOCAL_DENY_PORTS = Object.freeze([22]);
|
|
72
|
+
|
|
73
|
+
/** macOS only: the platform where the Seatbelt primitives above hold. A cheap pure check for tests and early gates. */
|
|
74
|
+
export function sandboxSupported(platform = process.platform) {
|
|
75
|
+
return platform === 'darwin';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Whether /usr/bin/sandbox-exec exists AND runs a trivial allow-default profile: probed once (cheap, ~15 ms) and
|
|
79
|
+
// cached, so a host where Seatbelt is missing or broken falls back to the in-code guard instead of failing every
|
|
80
|
+
// guarded worker. null = not yet probed.
|
|
81
|
+
let _sandboxExecProbe = null;
|
|
82
|
+
/**
|
|
83
|
+
* macOS AND sandbox-exec works. Cached after the first probe. The caller (server.js) gates OS isolation on this,
|
|
84
|
+
* never on sandboxSupported() alone, so a missing / broken sandbox-exec is a visible fall-back, not a launch failure.
|
|
85
|
+
*/
|
|
86
|
+
export function sandboxExecAvailable({ platform = process.platform, spawn = spawnSync, force = false } = {}) {
|
|
87
|
+
if (!sandboxSupported(platform)) return false;
|
|
88
|
+
if (_sandboxExecProbe !== null && !force) return _sandboxExecProbe;
|
|
89
|
+
let ok = false;
|
|
90
|
+
try {
|
|
91
|
+
if (existsSync(SANDBOX_EXEC)) {
|
|
92
|
+
const r = spawn(SANDBOX_EXEC, ['-p', '(version 1)(allow default)', '/usr/bin/true'], { timeout: 5000, stdio: 'ignore' });
|
|
93
|
+
ok = !r.error && r.status === 0 && !r.signal;
|
|
94
|
+
}
|
|
95
|
+
} catch { ok = false; }
|
|
96
|
+
_sandboxExecProbe = ok;
|
|
97
|
+
return ok;
|
|
98
|
+
}
|
|
99
|
+
/** Tests only: forget the cached probe result. */
|
|
100
|
+
export function _resetSandboxExecProbe() { _sandboxExecProbe = null; }
|
|
101
|
+
|
|
102
|
+
/** A Seatbelt string literal: double-quoted, with `\` and `"` escaped. */
|
|
103
|
+
function sexpr(text) {
|
|
104
|
+
return `"${String(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Where a path really is (Seatbelt matches the resolved path); the deepest existing ancestor resolved, rest as named. */
|
|
108
|
+
export function canonicalPath(abs) {
|
|
109
|
+
let probe = isAbsolute(abs) ? abs : resolve(abs);
|
|
110
|
+
const rest = [];
|
|
111
|
+
for (let i = 0; i < 256; i++) {
|
|
112
|
+
try { const real = realpathSync(probe); return rest.length ? join(real, ...rest.reverse()) : real; }
|
|
113
|
+
catch { const parent = dirname(probe); if (parent === probe) return resolve(abs); rest.push(probe.slice(parent.length).replace(/^[/\\]+/, '')); probe = parent; }
|
|
114
|
+
}
|
|
115
|
+
return resolve(abs);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Valid, deduped, sorted ports from a list. */
|
|
119
|
+
function cleanPorts(ports) {
|
|
120
|
+
return [...new Set((ports || []).map(Number).filter((p) => Number.isInteger(p) && p > 0 && p < 65536))].sort((a, b) => a - b);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The ancestor DIRECTORIES of each allow-back that fall inside a denied tree. `(deny file-read* (subpath X))` denies
|
|
125
|
+
* metadata too, so when a run's scratch folder is allowed back under a denied data home, node cannot even `lstat`
|
|
126
|
+
* the denied ancestors (/dataHome, /dataHome/data, /dataHome/data/scratch) to resolve a path there — it fails EPERM
|
|
127
|
+
* before it runs a script. Granting `file-read-metadata` on those exact directory nodes (a literal, not a subpath:
|
|
128
|
+
* the directory itself, never its contents) lets the lookup through without exposing any file in the denied tree.
|
|
129
|
+
* Canonical in, canonical out (the allow paths are already canonical, so their dirname chain is too).
|
|
130
|
+
*/
|
|
131
|
+
function metadataAncestors(allowPaths, denyPaths) {
|
|
132
|
+
const denies = denyPaths.map((p) => (p.endsWith('/') ? p.slice(0, -1) : p)).filter(Boolean);
|
|
133
|
+
const insideADeny = (dir) => denies.some((d) => dir === d || dir.startsWith(d + '/'));
|
|
134
|
+
const out = new Set();
|
|
135
|
+
for (const allow of allowPaths) {
|
|
136
|
+
let dir = dirname(allow);
|
|
137
|
+
for (let i = 0; i < 256; i++) {
|
|
138
|
+
if (insideADeny(dir)) out.add(dir);
|
|
139
|
+
const parent = dirname(dir);
|
|
140
|
+
if (parent === dir) break;
|
|
141
|
+
dir = parent;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return [...out];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The Seatbelt profile for a guarded worker tree.
|
|
149
|
+
* @param {object} o
|
|
150
|
+
* @param {number[]} o.denyPorts individual ports to block OUTBOUND to, on EVERY host (NEURAL_PORT + its WebSockets,
|
|
151
|
+
* the launch beacon, the shared OpenCode serve). The worker-only MCP channel port is
|
|
152
|
+
* NOT here — the profile must leave it open.
|
|
153
|
+
* @param {{from:number,to:number}} [o.denyPortRange] a reserved range (SynaBun's isolated OpenCode serves) blocked
|
|
154
|
+
* in FULL, so a relay started after this profile is written is already denied. Each
|
|
155
|
+
* port in it is a line (Seatbelt has no range syntax).
|
|
156
|
+
* @param {string[]} [o.denyPaths] folders / files whose reads AND writes are denied (SynaBun's whole data home, the
|
|
157
|
+
* other runtimes' credential files this run does not use). Each is canonicalised.
|
|
158
|
+
* @param {string[]} [o.allowPaths] folders allowed back AFTER the denies (the run's own scratch / cwd / config), so
|
|
159
|
+
* a path under a denied tree the run truly needs survives. Canonicalised; placed last
|
|
160
|
+
* (last match wins).
|
|
161
|
+
* @param {number[]} [o.denyLocalPorts] ports blocked OUTBOUND on this computer's own addresses only (`localhost:<port>`):
|
|
162
|
+
* a relay found on an internet port (22, 53, 80, 443), and local SSH.
|
|
163
|
+
* @param {{ allowPorts:number[] }} [o.strictLocal] relay detection FAILED for this run: no outbound connection at all
|
|
164
|
+
* but the internet's ports (INTERNET_PORTS, on other hosts) and the worker channel
|
|
165
|
+
* (`allowPorts`, on this computer). `*:*` is the only rule that covers every local
|
|
166
|
+
* spelling (the IPv4-mapped one included); Seatbelt's `localhost` misses [::ffff:…],
|
|
167
|
+
* and mixing `ip6` rules with `localhost` ones silently re-opens IPv4 loopback (verified
|
|
168
|
+
* on macOS 26), so this is written as deny-all-then-allow, never as a loopback deny.
|
|
169
|
+
* @param {string} [o.note] a short comment line (the run id), for readability in a log.
|
|
170
|
+
* @returns {string} the profile text (ends with a newline).
|
|
171
|
+
*/
|
|
172
|
+
export function buildWorkerSandboxProfile({ denyPorts = [], denyLocalPorts = [], denyPortRange = null, denyPaths = [], allowPaths = [], note = '', strictLocal = null } = {}) {
|
|
173
|
+
const localOnly = new Set(cleanPorts(denyLocalPorts));
|
|
174
|
+
// A port the internet needs is never denied on every host: it goes to the local-only list instead.
|
|
175
|
+
const ports = cleanPorts(denyPorts).filter((p) => { if (INTERNET_PORTS.includes(p)) { localOnly.add(p); return false; } return true; });
|
|
176
|
+
const range = denyPortRange && Number.isInteger(Number(denyPortRange.from)) && Number.isInteger(Number(denyPortRange.to))
|
|
177
|
+
? { from: Math.max(1, Number(denyPortRange.from)), to: Math.min(65535, Number(denyPortRange.to)) } : null;
|
|
178
|
+
const denies = [...new Set((denyPaths || []).filter((p) => typeof p === 'string' && p).map(canonicalPath))];
|
|
179
|
+
const allows = [...new Set((allowPaths || []).filter((p) => typeof p === 'string' && p).map(canonicalPath))];
|
|
180
|
+
const lines = ['(version 1)'];
|
|
181
|
+
if (note) lines.push(`; ${String(note).replace(/[\r\n]+/g, ' ').slice(0, 200)}`);
|
|
182
|
+
lines.push('(allow default)');
|
|
183
|
+
if (strictLocal) {
|
|
184
|
+
// Detection failed: nothing local is known to be safe, so nothing local is reachable but the worker channel.
|
|
185
|
+
// Deny every outbound IP connection, allow the internet's ports back on every host, take them away again on this
|
|
186
|
+
// computer's own addresses, and allow the channel last (last match wins).
|
|
187
|
+
lines.push('; strict: the relay detection failed for this run, so no local port is reachable but the worker channel');
|
|
188
|
+
lines.push(`(deny network-outbound (remote ip ${sexpr('*:*')}))`);
|
|
189
|
+
for (const port of INTERNET_PORTS) lines.push(`(allow network-outbound (remote ip ${sexpr(`*:${port}`)}))`);
|
|
190
|
+
for (const port of INTERNET_PORTS) if (port !== 53) localOnly.add(port); // a local DNS resolver stays reachable
|
|
191
|
+
for (const port of cleanPorts(strictLocal.allowPorts)) {
|
|
192
|
+
localOnly.delete(port);
|
|
193
|
+
lines.push(`(allow network-outbound (remote ip ${sexpr(`localhost:${port}`)}))`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// The control ports: a worker's own programs may not open them (its MCP tools are the way in). `*:<port>` blocks
|
|
197
|
+
// the port on every address (127.0.0.1, ::1, [::ffff:127.0.0.1], 0.0.0.0, the LAN IPs, the hostname); the public
|
|
198
|
+
// internet and every other port stay open.
|
|
199
|
+
const seen = new Set();
|
|
200
|
+
for (const port of ports) { seen.add(port); lines.push(`(deny network-outbound (remote ip ${sexpr(`*:${port}`)}))`); }
|
|
201
|
+
// Local-only denies (an internet port a relay holds here, local SSH): this computer's own addresses, never the internet.
|
|
202
|
+
for (const port of [...new Set([...localOnly, ...ALWAYS_LOCAL_DENY_PORTS])].sort((x, y) => x - y)) {
|
|
203
|
+
if (strictLocal && cleanPorts(strictLocal.allowPorts).includes(port)) continue;
|
|
204
|
+
lines.push(`(deny network-outbound (remote ip ${sexpr(`localhost:${port}`)}))`);
|
|
205
|
+
}
|
|
206
|
+
// The reserved relay range in full: every isolated OpenCode serve, whether it started before or after this profile.
|
|
207
|
+
if (range) for (let p = range.from; p <= range.to; p++) { if (!seen.has(p)) { seen.add(p); lines.push(`(deny network-outbound (remote ip ${sexpr(`*:${p}`)}))`); } }
|
|
208
|
+
// SynaBun's whole data home (its auth secrets, other runs' task text, the memory DB) and other runtimes' creds:
|
|
209
|
+
// a worker neither reads nor writes them.
|
|
210
|
+
for (const path of denies) lines.push(`(deny file-read* file-write* (subpath ${sexpr(path)}))`);
|
|
211
|
+
// Allowed back LAST (last match wins): the run's own scratch folder, its cwd when that is inside the data home
|
|
212
|
+
// (skill-drafting workspaces), and its OpenCode config folder — everything the run legitimately needs there.
|
|
213
|
+
for (const path of allows) lines.push(`(allow file-read* file-write* (subpath ${sexpr(path)}))`);
|
|
214
|
+
// Metadata-only on the denied ancestors of each allow-back, so `node scratch/probe.mjs` can lstat its way in
|
|
215
|
+
// (without this, a denied data-home ancestor fails EPERM before the script runs). Directory nodes only (literal),
|
|
216
|
+
// placed after the file allow-backs so this metadata grant is the last match for those paths.
|
|
217
|
+
for (const dir of metadataAncestors(allows, denies)) lines.push(`(allow file-read-metadata (literal ${sexpr(dir)}))`);
|
|
218
|
+
return `${lines.join('\n')}\n`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** sandbox-exec argv to run `command args…` under a written profile (OpenCode's serve launch). */
|
|
222
|
+
export function sandboxExecArgv(profilePath, command, args = []) {
|
|
223
|
+
return [SANDBOX_EXEC, '-f', profilePath, command, ...(Array.isArray(args) ? args : [])];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Write a run's Seatbelt profile and (for a CLI the Agent SDK spawns directly) a wrapper script that execs the real
|
|
228
|
+
* executable under it. The SDK uses pathToClaudeCodeExecutable verbatim with shell:false, so the wrapper is a plain
|
|
229
|
+
* shebang script that forwards its arguments; the sandbox is inherited by everything the CLI then starts.
|
|
230
|
+
*
|
|
231
|
+
* @param {object} o
|
|
232
|
+
* @param {string} o.dir a folder the run owns (its scratch folder): the profile and wrapper live here.
|
|
233
|
+
* @param {string} o.profile the profile text (buildWorkerSandboxProfile).
|
|
234
|
+
* @param {string} [o.realExecutable] the executable the wrapper should exec (for Claude); omit for OpenCode.
|
|
235
|
+
* @param {string} [o.nodeRunner] the node to run a `.js` entry with (default process.execPath); only used when the
|
|
236
|
+
* real executable is a script the Agent SDK would itself have run via node (.js / .mjs / …).
|
|
237
|
+
* @param {string} [o.name] a basename stem (default 'worker').
|
|
238
|
+
* @returns {{ profilePath: string, wrapperPath: string|null }}
|
|
239
|
+
*
|
|
240
|
+
* The wrapper must be named so the Agent SDK spawns it DIRECTLY (not via node): the SDK runs a
|
|
241
|
+
* pathToClaudeCodeExecutable that ends in .js/.mjs/.ts/.tsx/.jsx through `node`, anything else directly. `.sh`
|
|
242
|
+
* is spawned directly, so the wrapper execs sandbox-exec itself. If the REAL executable is such a script, the
|
|
243
|
+
* wrapper runs it under node (what the SDK would have done), so the sandbox still wraps the whole tree.
|
|
244
|
+
*/
|
|
245
|
+
export function writeWorkerSandbox({ dir, profile, realExecutable = null, nodeRunner = process.execPath, name = 'worker' }) {
|
|
246
|
+
if (!dir) throw new Error('writeWorkerSandbox needs a directory the run owns');
|
|
247
|
+
mkdirSync(dir, { recursive: true });
|
|
248
|
+
const stem = String(name).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 64) || 'worker';
|
|
249
|
+
const profilePath = join(dir, `${stem}.sb`);
|
|
250
|
+
writeFileSync(profilePath, profile, { mode: 0o600 });
|
|
251
|
+
let wrapperPath = null;
|
|
252
|
+
if (realExecutable) {
|
|
253
|
+
wrapperPath = join(dir, `${stem}-wrap.sh`);
|
|
254
|
+
// exec, so the wrapper process is replaced by sandbox-exec (no extra shell in the tree). "$@" forwards argv
|
|
255
|
+
// verbatim. The real executable (and node, when the entry is a script) and the profile are single-quoted.
|
|
256
|
+
const sh = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
257
|
+
const isScript = /\.(?:mjs|cjs|js|ts|tsx|jsx)$/i.test(realExecutable);
|
|
258
|
+
const runTarget = isScript ? `${sh(nodeRunner)} ${sh(realExecutable)}` : sh(realExecutable);
|
|
259
|
+
writeFileSync(wrapperPath, `#!/bin/sh\nexec ${sh(SANDBOX_EXEC)} -f ${sh(profilePath)} ${runTarget} "$@"\n`, { mode: 0o700 });
|
|
260
|
+
chmodSync(wrapperPath, 0o700);
|
|
261
|
+
}
|
|
262
|
+
return { profilePath, wrapperPath };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Everything a guarded Claude / OpenCode run needs for OS isolation, or null when it does not apply (not macOS, no
|
|
267
|
+
* guard, warn / off, or the capability is full). The caller gathers the data-home trees and other-runtime creds to
|
|
268
|
+
* deny and the run's own folders to allow back; this assembles the port set and gates on an enforced guard. Writing
|
|
269
|
+
* is left to the caller (writeWorkerSandbox) so a failure to write never throws on the launch path — the caller
|
|
270
|
+
* falls back to the in-code guard and says so.
|
|
271
|
+
* @param {{ allowPorts:number[] }} [o.strictLocal] the relay detection failed: the strict profile (see
|
|
272
|
+
* buildWorkerSandboxProfile), with `allowPorts` the worker channel.
|
|
273
|
+
* @returns {{ denyPorts:number[], denyLocalPorts:number[], denyPortRange:{from,to}|null, denyPaths:string[], allowPaths:string[], note:string, strictLocal:object|null } | null}
|
|
274
|
+
*/
|
|
275
|
+
export function workerSandboxPlan({ confinement, serverPort = null, reservedPortRange = null, extraDenyPorts = [], denyPaths = [], allowPaths = [], strictLocal = null, platform = process.platform } = {}) {
|
|
276
|
+
if (!sandboxSupported(platform)) return null;
|
|
277
|
+
if (!confinement || !confinement.enforced) return null; // only an enforced guard (read-only / workspace) is wrapped
|
|
278
|
+
const denyPorts = [];
|
|
279
|
+
const denyLocalPorts = [];
|
|
280
|
+
const own = Number(serverPort || confinement.serverPort) || null;
|
|
281
|
+
if (own) denyPorts.push(own);
|
|
282
|
+
for (const p of extraDenyPorts || []) {
|
|
283
|
+
const n = Number(p);
|
|
284
|
+
if (!(n > 0)) continue;
|
|
285
|
+
// A relay on an internet port is denied on this computer's addresses only; the internet keeps the port.
|
|
286
|
+
if (INTERNET_PORTS.includes(n)) denyLocalPorts.push(n); else denyPorts.push(n);
|
|
287
|
+
}
|
|
288
|
+
const strictAllow = strictLocal ? cleanPorts(strictLocal.allowPorts) : [];
|
|
289
|
+
return {
|
|
290
|
+
denyPorts: [...new Set(denyPorts)],
|
|
291
|
+
denyLocalPorts: [...new Set(denyLocalPorts)],
|
|
292
|
+
denyPortRange: reservedPortRange && Number(reservedPortRange.from) > 0 && Number(reservedPortRange.to) > 0
|
|
293
|
+
? { from: Number(reservedPortRange.from), to: Number(reservedPortRange.to) } : null,
|
|
294
|
+
denyPaths: [...new Set((denyPaths || []).filter(Boolean))],
|
|
295
|
+
allowPaths: [...new Set((allowPaths || []).filter(Boolean))],
|
|
296
|
+
note: `synabun worker ${confinement.capability} (${confinement.mode})${strictLocal ? ' strict' : ''}`,
|
|
297
|
+
strictLocal: strictLocal ? { allowPorts: strictAllow } : null,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neural-memory-interface",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "neural-memory-interface",
|
|
9
|
-
"version": "2.0
|
|
9
|
+
"version": "2.1.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "Apache-2.0",
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@anthropic-ai/claude-agent-sdk": "0.3.288",
|
|
14
14
|
"@huggingface/transformers": "^3.0.0",
|
|
15
15
|
"@openai/codex-sdk": "^0.160.0",
|
|
16
|
-
"@opencode-ai/sdk": "1.18.34",
|
|
17
16
|
"@xterm/addon-fit": "0.11.0",
|
|
18
17
|
"@xterm/addon-search": "0.16.0",
|
|
19
18
|
"@xterm/addon-serialize": "0.14.0",
|
|
@@ -31,6 +30,7 @@
|
|
|
31
30
|
"playwright": "^1.59.1",
|
|
32
31
|
"undici": "^8.5.0",
|
|
33
32
|
"ws": "^8.21.0",
|
|
33
|
+
"yaml": "2.9.1",
|
|
34
34
|
"zod": "^4.4.3"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
@@ -2205,15 +2205,6 @@
|
|
|
2205
2205
|
"node": ">=18"
|
|
2206
2206
|
}
|
|
2207
2207
|
},
|
|
2208
|
-
"node_modules/@opencode-ai/sdk": {
|
|
2209
|
-
"version": "1.18.34",
|
|
2210
|
-
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.34.tgz",
|
|
2211
|
-
"integrity": "sha512-R9QWUpcngtdQ6FCixz7NC6n1WsSho6CEuRUzYHz/CzZcp3miSklDzXkm0oDwXVCnuEu+ANoeM0f6RDp51kOMzQ==",
|
|
2212
|
-
"license": "MIT",
|
|
2213
|
-
"dependencies": {
|
|
2214
|
-
"cross-spawn": "7.0.6"
|
|
2215
|
-
}
|
|
2216
|
-
},
|
|
2217
2208
|
"node_modules/@pkgjs/parseargs": {
|
|
2218
2209
|
"version": "0.11.0",
|
|
2219
2210
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
|
@@ -6391,6 +6382,21 @@
|
|
|
6391
6382
|
"node": ">=18"
|
|
6392
6383
|
}
|
|
6393
6384
|
},
|
|
6385
|
+
"node_modules/yaml": {
|
|
6386
|
+
"version": "2.9.1",
|
|
6387
|
+
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
|
|
6388
|
+
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
|
|
6389
|
+
"license": "ISC",
|
|
6390
|
+
"bin": {
|
|
6391
|
+
"yaml": "bin.mjs"
|
|
6392
|
+
},
|
|
6393
|
+
"engines": {
|
|
6394
|
+
"node": ">= 14.6"
|
|
6395
|
+
},
|
|
6396
|
+
"funding": {
|
|
6397
|
+
"url": "https://github.com/sponsors/eemeli"
|
|
6398
|
+
}
|
|
6399
|
+
},
|
|
6394
6400
|
"node_modules/zip-stream": {
|
|
6395
6401
|
"version": "6.0.1",
|
|
6396
6402
|
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neural-memory-interface",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Private local SynaBun interface for memory visualization and multi-agent workflows",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
"@anthropic-ai/claude-agent-sdk": "0.3.288",
|
|
25
25
|
"@huggingface/transformers": "^3.0.0",
|
|
26
26
|
"@openai/codex-sdk": "^0.160.0",
|
|
27
|
-
"@opencode-ai/sdk": "1.18.34",
|
|
28
27
|
"@xterm/addon-fit": "0.11.0",
|
|
29
28
|
"@xterm/addon-search": "0.16.0",
|
|
30
29
|
"@xterm/addon-serialize": "0.14.0",
|
|
@@ -42,6 +41,7 @@
|
|
|
42
41
|
"playwright": "^1.59.1",
|
|
43
42
|
"undici": "^8.5.0",
|
|
44
43
|
"ws": "^8.21.0",
|
|
44
|
+
"yaml": "2.9.1",
|
|
45
45
|
"zod": "^4.4.3"
|
|
46
46
|
},
|
|
47
47
|
"overrides": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
<link rel="manifest" href="/manifest.json">
|
|
14
14
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
15
15
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16
|
-
<link
|
|
16
|
+
<link rel="stylesheet" href="/vendor/caveat/caveat.css">
|
|
17
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
|
17
18
|
<link href="https://db.onlinewebfonts.com/c/3284c3a32cf946466fb963369b3c5b2d?family=Disket+Mono" rel="stylesheet">
|
|
18
19
|
<link rel="stylesheet" href="/shared/styles.css">
|
|
19
20
|
<!-- three.js is vendored (scripts/build-vendor.mjs); only the map renderer imports it.
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
<!-- html2canvas for card screenshot MCP tool (vendored: public/vendor/html2canvas/) -->
|
|
60
61
|
<script src="/vendor/html2canvas/html2canvas-1.4.1.min.js" defer></script>
|
|
61
62
|
|
|
63
|
+
<script type="module" src="/shared/page-session.js"></script>
|
|
62
64
|
<script type="module" src="/variant/3d/main.js"></script>
|
|
63
65
|
</body>
|
|
64
66
|
</html>
|