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
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — who is at the other end of a loopback connection (owner authority by peer-process identity)
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// The owner's authority over SynaBun is the page secret (lib/page-session.js): an HttpOnly cookie the server hands
|
|
6
|
+
// to the app's own pages and asks of every owner write. Markers (a cookie and a header SynaBun's automation browser
|
|
7
|
+
// carries) and port denylists (lib/worker-sandbox.js, lib/worker-relay-ports.js) keep a worker from getting it, and
|
|
8
|
+
// each kept leaking at an edge: a host spelling the marker test missed, a service worker the browser's request
|
|
9
|
+
// interception never sees, a local proxy the denylist did not know. This module answers the question all of those
|
|
10
|
+
// approximate, at the one place nothing can spoof it: the kernel's TCP table. For a request on a connection from
|
|
11
|
+
// this computer, it finds the process that owns the client end of the socket, walks its parents, and says whether
|
|
12
|
+
// that process may hold or use the owner's page secret:
|
|
13
|
+
//
|
|
14
|
+
// • refused: SynaBun's automation browser and any browser started for automation (--remote-debugging-port or
|
|
15
|
+
// --remote-debugging-pipe, MoreLogin's Chromium, Playwright's WebKit / Firefox pipes), whatever host or port it
|
|
16
|
+
// used and whether or not a service worker made the request (a service worker's fetch leaves from the browser's
|
|
17
|
+
// own network process);
|
|
18
|
+
// • refused: an AI agent's process tree (a Claude Code CLI, Codex, OpenCode, …) — every dispatched worker, its
|
|
19
|
+
// shell, its scripts, a detached child — and another SynaBun server;
|
|
20
|
+
// • refused: a relay — ssh / sshd, socat, nc, a proxy or tunnel program by name (privoxy, mitmproxy, Clash, Surge,
|
|
21
|
+
// sing-box, cloudflared, ngrok, …), and any other program that itself listens on a TCP port and then calls
|
|
22
|
+
// SynaBun (a dev server's proxy, a small node SOCKS server): a relay hides who really asked;
|
|
23
|
+
// • allowed: everything else — the owner's own browser (Safari's WebKit networking, Chrome's network service,
|
|
24
|
+
// Firefox, Arc…), the SynaBun app's window, the owner's own terminal.
|
|
25
|
+
//
|
|
26
|
+
// No allowlist: nothing the owner uses has to be known for it to keep working. When the lookup itself cannot run
|
|
27
|
+
// (netstat or ps missing, slow or unreadable, another platform) the answer is "unknown" and the caller keeps its
|
|
28
|
+
// rules from before this module (the sandbox stays the boundary for workers): the owner is never locked out by a
|
|
29
|
+
// failed lookup. A client socket that is no longer in the table when it is looked up (the client closed it before
|
|
30
|
+
// its request was read) is refused: a browser waiting for its answer still holds its socket.
|
|
31
|
+
//
|
|
32
|
+
// Cheap: one `netstat -anvl -p tcp` (~4 ms on macOS, one sysctl) per NEW connection, shared by every lookup that is
|
|
33
|
+
// waiting when it starts; the process table (`ps`, ~20 ms) is read only for a process not seen in the last few
|
|
34
|
+
// minutes; the answer is kept on the socket, so keep-alive requests reuse it for free. Requests that carry no page
|
|
35
|
+
// secret and no brain credential and ask for none (hooks, MCP, static files) are never looked up at all.
|
|
36
|
+
//
|
|
37
|
+
// The same lookup proves a brain credential (X-Synabun-Brain-Token) is used by the brain it was minted for: the
|
|
38
|
+
// credential is in that brain's own process environment only (SYNABUN_BRAIN_TOKEN, assistant-runtime.js), so a
|
|
39
|
+
// request carrying it must come from a process that holds it in its environment, or a descendant of one (the
|
|
40
|
+
// Claude brain's CLI, the Codex / OpenCode brain's stdio MCP child) — or from this server itself (the in-process
|
|
41
|
+
// MCP server relays a brain's /mcp call, whose own connection was checked when it arrived). A credential read out
|
|
42
|
+
// of a brain's environment (`ps -E` cannot be blocked between processes of one user) and sent through a relay is
|
|
43
|
+
// dropped from the request, which then has no brain authority.
|
|
44
|
+
//
|
|
45
|
+
// macOS only (netstat's process:pid column, ps -E). Elsewhere every answer is "unknown" (the rules from before).
|
|
46
|
+
// Parsers and the classifier are pure and unit-tested (tests/peer-identity.test.mjs); the reader takes an injected
|
|
47
|
+
// `run` so tests drive it with fixed tables.
|
|
48
|
+
|
|
49
|
+
import { execFile } from 'node:child_process';
|
|
50
|
+
import { realpathSync } from 'node:fs';
|
|
51
|
+
import { isIP } from 'node:net';
|
|
52
|
+
import { networkInterfaces } from 'node:os';
|
|
53
|
+
import { posix } from 'node:path';
|
|
54
|
+
|
|
55
|
+
/** Where the verdict for a request is kept (on the request object). */
|
|
56
|
+
export const PEER_VERDICT = Symbol.for('synabun.peerVerdict');
|
|
57
|
+
/** The brain credential header (assistant-api.js BRAIN_CREDENTIAL_HEADER). */
|
|
58
|
+
export const BRAIN_HEADER = 'x-synabun-brain-token';
|
|
59
|
+
|
|
60
|
+
const NETSTAT = '/usr/sbin/netstat';
|
|
61
|
+
/** `netstat -anvl -p tcp`: every TCP socket, numeric, with its process (-v), IPv6 addresses written in full (-l). */
|
|
62
|
+
export const NETSTAT_ARGS = Object.freeze(['-anvl', '-p', 'tcp']);
|
|
63
|
+
const PS = '/bin/ps';
|
|
64
|
+
const LSOF = '/usr/sbin/lsof';
|
|
65
|
+
/** `lsof -a -p <pid> -d cwd -Fn`: one process's working directory, as field output (lines `p…`, `fcwd`, `n<path>`). */
|
|
66
|
+
export const lsofCwdArgs = (pid) => ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'];
|
|
67
|
+
const TABLE_TIMEOUT_MS = 1500;
|
|
68
|
+
const PS_TIMEOUT_MS = 2000;
|
|
69
|
+
const LOOKUP_CAP_MS = 2500;
|
|
70
|
+
const PS_SNAPSHOT_TTL_MS = 2000;
|
|
71
|
+
// A process's parents and command line do not change while it lives (a parent that exits leaves it to launchd, which
|
|
72
|
+
// only ever takes an agent or a browser out of the chain, never puts one in): kept half an hour, checked against
|
|
73
|
+
// netstat's kernel name on every lookup (a reused pid under another name is read again).
|
|
74
|
+
const CHAIN_TTL_MS = 30 * 60 * 1000;
|
|
75
|
+
const ENV_TTL_MS = 10 * 60 * 1000;
|
|
76
|
+
const LOCAL_ADDR_TTL_MS = 10 * 1000;
|
|
77
|
+
const LOG_EVERY_MS = 60 * 1000;
|
|
78
|
+
const MAX_CHAIN = 64;
|
|
79
|
+
|
|
80
|
+
// ── Addresses ────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* An IPv6 literal → its eight 16-bit groups, or null. Takes brackets, a zone id, `::` and a dotted IPv4 tail
|
|
84
|
+
* (::ffff:127.0.0.1), in any case.
|
|
85
|
+
*/
|
|
86
|
+
export function ipv6Groups(text) {
|
|
87
|
+
let s = String(text ?? '').trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
88
|
+
const zone = s.indexOf('%');
|
|
89
|
+
if (zone >= 0) s = s.slice(0, zone);
|
|
90
|
+
if (!s.includes(':')) return null;
|
|
91
|
+
let tail = [];
|
|
92
|
+
const dotted = /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
|
|
93
|
+
if (dotted) {
|
|
94
|
+
const parts = dotted.slice(1).map(Number);
|
|
95
|
+
if (parts.some((n) => n > 255)) return null;
|
|
96
|
+
tail = [(parts[0] << 8) | parts[1], (parts[2] << 8) | parts[3]];
|
|
97
|
+
s = s.slice(0, -dotted[0].length);
|
|
98
|
+
if (!s.endsWith(':')) return null;
|
|
99
|
+
if (!s.endsWith('::')) s = s.slice(0, -1);
|
|
100
|
+
}
|
|
101
|
+
const halves = s.split('::');
|
|
102
|
+
if (halves.length > 2) return null;
|
|
103
|
+
const groupsOf = (h) => (h ? h.split(':') : []).map((g) => (/^[0-9a-f]{1,4}$/.test(g) ? parseInt(g, 16) : NaN));
|
|
104
|
+
const head = groupsOf(halves[0]);
|
|
105
|
+
const rest = halves.length === 2 ? groupsOf(halves[1]) : [];
|
|
106
|
+
const given = head.length + rest.length + tail.length;
|
|
107
|
+
if (halves.length === 1 && given !== 8) return null;
|
|
108
|
+
if (halves.length === 2 && given > 7) return null;
|
|
109
|
+
const groups = [...head, ...new Array(8 - given).fill(0), ...rest, ...tail];
|
|
110
|
+
return groups.length === 8 && groups.every((g) => Number.isInteger(g) && g >= 0 && g <= 0xffff) ? groups : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The IPv4 address an IPv4-mapped IPv6 literal (::ffff:a.b.c.d, ::ffff:7f00:1) stands for, or null. */
|
|
114
|
+
export function mappedIPv4(text) {
|
|
115
|
+
const g = ipv6Groups(text);
|
|
116
|
+
if (!g || !g.slice(0, 5).every((x) => x === 0) || g[5] !== 0xffff) return null;
|
|
117
|
+
return `${g[6] >> 8}.${g[6] & 255}.${g[7] >> 8}.${g[7] & 255}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* An address as both sides print it, for comparing them: lowercase, no brackets, no zone id; an IPv4-mapped IPv6
|
|
122
|
+
* address (::ffff:127.0.0.1, the dual-stack listener's view of an IPv4 client) as its IPv4 form; any other IPv6
|
|
123
|
+
* address as its eight groups written out (so "::1" and "0:0:0:0:0:0:0:1" are one address).
|
|
124
|
+
*/
|
|
125
|
+
export function normalizeAddress(addr) {
|
|
126
|
+
const a = String(addr ?? '').trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
127
|
+
if (!a.includes(':')) return a;
|
|
128
|
+
const v4 = mappedIPv4(a);
|
|
129
|
+
if (v4) return v4;
|
|
130
|
+
const g = ipv6Groups(a);
|
|
131
|
+
return g ? g.map((x) => x.toString(16)).join(':') : a.replace(/%.*$/, '');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** One netstat endpoint ("127.0.0.1.54683", "::1.3344", "fe80::1%lo0.80", "*.3344", "*.*") → { addr, port }. */
|
|
135
|
+
export function parseNetstatEndpoint(text) {
|
|
136
|
+
const t = String(text || '');
|
|
137
|
+
const dot = t.lastIndexOf('.');
|
|
138
|
+
if (dot <= 0) return null;
|
|
139
|
+
const port = Number(t.slice(dot + 1));
|
|
140
|
+
return { addr: normalizeAddress(t.slice(0, dot)), port: Number.isInteger(port) && port > 0 && port < 65536 ? port : null };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `netstat -anvl -p tcp` (macOS) → { rows: [{ local, foreign, state, pid, name }], listening: Set<pid> }.
|
|
145
|
+
* Two layouts are read: the current one, whose `process:pid` column sits before eight fixed columns (a process name
|
|
146
|
+
* may hold spaces, "Code Helper (Plu:55264"), and the older one with separate `pid` and `epid` columns (found by
|
|
147
|
+
* the header). A row without a pid (a socket no process owns any more) keeps pid 0.
|
|
148
|
+
*/
|
|
149
|
+
export function parseNetstatTcp(text) {
|
|
150
|
+
const rows = [];
|
|
151
|
+
const listening = new Set();
|
|
152
|
+
const lines = String(text || '').split('\n');
|
|
153
|
+
let layout = 'process';
|
|
154
|
+
let pidIndex = -1;
|
|
155
|
+
for (const raw of lines) {
|
|
156
|
+
const line = raw.trim();
|
|
157
|
+
if (!line) continue;
|
|
158
|
+
if (/^Proto\b/.test(line)) {
|
|
159
|
+
const head = line.replace(/Local Address/, 'LocalAddress').replace(/Foreign Address/, 'ForeignAddress').split(/\s+/);
|
|
160
|
+
if (head.includes('process:pid')) layout = 'process';
|
|
161
|
+
else if (head.includes('pid')) { layout = 'pid'; pidIndex = head.indexOf('pid'); }
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!/^tcp/.test(line)) continue;
|
|
165
|
+
const tokens = line.split(/\s+/);
|
|
166
|
+
if (tokens.length < 6) continue;
|
|
167
|
+
const local = parseNetstatEndpoint(tokens[3]);
|
|
168
|
+
const foreign = parseNetstatEndpoint(tokens[4]);
|
|
169
|
+
if (!local) continue;
|
|
170
|
+
const state = tokens[5];
|
|
171
|
+
let pid = 0;
|
|
172
|
+
let name = '';
|
|
173
|
+
if (layout === 'process') {
|
|
174
|
+
// …shiwat <process name, maybe with spaces>:<pid> state options gencnt flags flags1 usecnt rtncnt fltrs
|
|
175
|
+
if (tokens.length >= 19) {
|
|
176
|
+
const procToken = tokens[tokens.length - 9];
|
|
177
|
+
const m = /:(\d+)$/.exec(procToken);
|
|
178
|
+
if (m) {
|
|
179
|
+
pid = Number(m[1]);
|
|
180
|
+
name = tokens.slice(10, tokens.length - 8).join(' ').replace(/:\d+$/, '');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else if (pidIndex >= 0) {
|
|
184
|
+
// The older layout: header columns map one to one once the two-word address headers are joined.
|
|
185
|
+
const n = Number(tokens[pidIndex]);
|
|
186
|
+
if (Number.isInteger(n) && n > 0) pid = n;
|
|
187
|
+
}
|
|
188
|
+
if (!Number.isInteger(pid) || pid < 0) pid = 0;
|
|
189
|
+
rows.push({ local, foreign, state, pid, name });
|
|
190
|
+
if (state === 'LISTEN' && pid > 0) listening.add(pid);
|
|
191
|
+
}
|
|
192
|
+
return { rows, listening };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** `ps -Ao pid=,ppid=,ucomm=` → Map<pid, { ppid, ucomm }> (the kernel's own process name, which a title cannot change). */
|
|
196
|
+
export function parsePsParents(text) {
|
|
197
|
+
const out = new Map();
|
|
198
|
+
for (const line of String(text || '').split('\n')) {
|
|
199
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(.*?)\s*$/.exec(line);
|
|
200
|
+
if (!m) continue;
|
|
201
|
+
const pid = Number(m[1]);
|
|
202
|
+
if (pid > 0) out.set(pid, { ppid: Number(m[2]), ucomm: m[3] });
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** `ps -A -o pid=,<column>=` → Map<pid, string> (everything after the pid; paths with spaces included). */
|
|
208
|
+
export function parsePsColumn(text) {
|
|
209
|
+
const out = new Map();
|
|
210
|
+
for (const line of String(text || '').split('\n')) {
|
|
211
|
+
const m = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
212
|
+
if (!m) continue;
|
|
213
|
+
const pid = Number(m[1]);
|
|
214
|
+
if (pid > 0) out.set(pid, m[2]);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** `lsof -a -p <pid> -d cwd -Fn` → the working directory (the `n` line of the `fcwd` file), or null. */
|
|
220
|
+
export function parseLsofCwd(text) {
|
|
221
|
+
let cwd = false;
|
|
222
|
+
for (const line of String(text || '').split('\n')) {
|
|
223
|
+
if (line.startsWith('f')) cwd = line === 'fcwd';
|
|
224
|
+
else if (cwd && line.startsWith('n/')) return line.slice(1);
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Process identities ───────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
const lower = (s) => String(s ?? '').toLowerCase();
|
|
232
|
+
const baseName = (p) => { const s = String(p ?? '').replace(/\/+$/, ''); return lower(s.slice(s.lastIndexOf('/') + 1)); };
|
|
233
|
+
|
|
234
|
+
/** Programs whose whole job is to carry a connection somewhere else (matched on the kernel name or argv[0]'s name). */
|
|
235
|
+
export const RELAY_NAMES = Object.freeze([
|
|
236
|
+
'ssh', 'autossh', 'sshuttle', 'socat', 'nc', 'ncat', 'netcat', 'stunnel', 'spiped',
|
|
237
|
+
'privoxy', 'squid', 'tinyproxy', 'polipo', 'haproxy',
|
|
238
|
+
'mitmproxy', 'mitmdump', 'mitmweb',
|
|
239
|
+
'clash', 'clashx', 'clash-meta', 'clash-verge', 'mihomo', 'verge-mihomo', 'sing-box', 'v2ray', 'xray',
|
|
240
|
+
'ss-local', 'sslocal', 'ss-server', 'ssserver', 'ss-redir', 'obfs-local', 'trojan', 'trojan-go', 'hysteria', 'hysteria2',
|
|
241
|
+
'tuic-client', 'naive', 'gost', '3proxy', 'sockd', 'danted', 'microsocks', 'redsocks', 'tun2socks', 'proxychains4',
|
|
242
|
+
'cloudflared', 'ngrok', 'frpc', 'frps', 'chisel', 'bore', 'rathole', 'tailscaled', 'zrok', 'pinggy',
|
|
243
|
+
]);
|
|
244
|
+
/** Proxy / tunnel apps found by their bundle (their helpers carry other names). */
|
|
245
|
+
const RELAY_BUNDLE_RE = /\/(?:charles|proxyman|surge(?: mac)?|clashx[^/]*|clash verge|clash for windows|clash nyanpasu|flclash|shadowrocket|shadowsocksx[^/]*|v2rayu|v2rayx|qv2ray|v2rayn|stash|quantumult x|loon|proxifier|mitmproxy|ngrok|tailscale|cloudflare warp)\.app\//i;
|
|
246
|
+
/** The agents whose process trees never hold the owner's authority (argv[0]'s name). */
|
|
247
|
+
// Antigravity's CLI runs as `antigravity` (the kernel's name for the binary Homebrew links as `agy`). Gemini CLI stays on
|
|
248
|
+
// the list though SynaBun no longer drives it: an agent the user still runs never holds the owner's authority either.
|
|
249
|
+
export const AGENT_NAMES = Object.freeze(['claude', 'codex', 'opencode', 'agy', 'antigravity', 'gemini', 'cursor-agent', 'aider', 'goose', 'qwen', 'crush']);
|
|
250
|
+
/** The agent backend of the Antigravity app and IDE (a `language_server` inside their bundles: Codeium's name, so only by its place). */
|
|
251
|
+
const ANTIGRAVITY_SERVER_RE = /\/antigravity(?: ide)?\.app\/contents\/resources\/(?:app\/extensions\/[^/]+\/)?bin\/language_server/i;
|
|
252
|
+
/** Browsers the owner browses with (their bundle or their network process): never taken for a relay because they
|
|
253
|
+
* happen to hold a listening socket; an automation flag on them is still refused. */
|
|
254
|
+
const BROWSER_BUNDLE_RE = /\/(?:safari|google chrome(?: beta| dev| canary)?|chromium|firefox(?: developer edition| nightly)?|arc|brave browser(?: beta| nightly)?|microsoft edge(?: beta| dev| canary)?|opera(?: gx)?|vivaldi|orion|zen(?: browser)?|thorium|comet|dia|sigmaos|yandex|duckduckgo|sidekick|wavebox|island)\.app\/|\/synabun\.app\/contents\/(?:macos\/synabun(?:\s|$)|frameworks\/synabun helper)/i;
|
|
255
|
+
/** MoreLogin's executables: its app bundle, and the browser it keeps under ~/Library/Application Support/MoreLogin. */
|
|
256
|
+
export const MORELOGIN_PATH_RE = /\/morelogin(?: [^/]*)?\.app\/|\/application support\/morelogin\//i;
|
|
257
|
+
/** Kernel names of interpreters and shells: whatever their argv says, they are not a browser. */
|
|
258
|
+
const INTERPRETER_RE = /^(?:node|nodejs|bun|deno|python[0-9.]*|pypy[0-9.]*|ruby|perl[0-9.]*|php|java|lua|tclsh|osascript|sh|bash|zsh|dash|fish|ksh|tcsh|csh)$/;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* What a process is, from its kernel name (`ucomm`), argv[0] (`comm`) and its whole command line (`args`):
|
|
262
|
+
* { automation, agent, synabun, remoteLogin, relay, browser } (each a short reason string or false).
|
|
263
|
+
*/
|
|
264
|
+
export function processKinds({ ucomm = '', comm = '', args = '' } = {}) {
|
|
265
|
+
const u = lower(ucomm).trim();
|
|
266
|
+
const c = String(comm ?? '');
|
|
267
|
+
const a = String(args ?? '');
|
|
268
|
+
const la = lower(a);
|
|
269
|
+
const exe = baseName(c.split(/\s+--?[a-z]/i)[0] || c) || baseName(a.split(/\s+/)[0]);
|
|
270
|
+
const names = new Set([u, exe].filter(Boolean));
|
|
271
|
+
const has = (list) => list.find((n) => names.has(n)) || false;
|
|
272
|
+
|
|
273
|
+
let automation = false;
|
|
274
|
+
if (/(?:^|\s)--remote-debugging-(?:port|pipe)(?:[=\s]|$)/.test(la)) automation = 'remote debugging';
|
|
275
|
+
// MoreLogin by where its executable is (its app, its bundled browser and their helpers live in a MoreLogin folder or
|
|
276
|
+
// bundle) or by its kernel name — never by a word in the arguments (a test runner lists tests/morelogin*.test.mjs).
|
|
277
|
+
else if (MORELOGIN_PATH_RE.test(c) || u.startsWith('morelogin')) automation = 'MoreLogin';
|
|
278
|
+
else if (/(?:^|\s)--inspector-pipe(?:\s|$)/.test(la) || /\/pw_run\.sh(?:\s|$)/.test(la)) automation = 'Playwright WebKit';
|
|
279
|
+
else if (/(?:^|\s)-juggler(?:-pipe)?(?:\s|$)/.test(la) || /(?:^|\s)--?marionette(?:\s|$)/.test(la)) automation = 'Firefox automation';
|
|
280
|
+
|
|
281
|
+
let agent = has(AGENT_NAMES);
|
|
282
|
+
if (agent === 'agy') agent = 'antigravity';
|
|
283
|
+
if (!agent) {
|
|
284
|
+
if (/\/claude\/versions\/[^/\s]+(?:\s|$)/.test(la) || /@anthropic-ai\/claude-code\/cli\.m?js(?:\s|$)/.test(la) || /@anthropic-ai\/claude-agent-sdk[^/\s]*\/claude(?:\s|$)/.test(la)) agent = 'claude';
|
|
285
|
+
else if (/@openai\/codex\/bin\/codex(?:\.js)?(?:\s|$)/.test(la) || /(?:^|\s|\/)codex\s+(?:app-server|exec|mcp(?:-server)?|proto)\b/.test(la)) agent = 'codex';
|
|
286
|
+
else if (/(?:^|\s|\/)opencode\s+serve\b/.test(la)) agent = 'opencode';
|
|
287
|
+
else if (/(?:^|\s|\/)(?:agy|antigravity)\s+acp\b/.test(la) || ANTIGRAVITY_SERVER_RE.test(c || a)) agent = 'antigravity';
|
|
288
|
+
else if (/@google\/gemini-cli\//.test(la)) agent = 'gemini';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const synabun = /neural-interface[/\\]server\.js(?:\s|$)/.test(a) ? 'SynaBun server' : false;
|
|
292
|
+
const remoteLogin = (/^sshd(?:-session|-auth)?$/.test(u) || /^sshd(?:-session|-auth)?$/.test(exe) || /^sshd(?::|\s|$)/.test(lower(c).trim())) ? 'sshd' : false;
|
|
293
|
+
let relay = remoteLogin ? false : has(RELAY_NAMES);
|
|
294
|
+
if (!relay && RELAY_BUNDLE_RE.test(c || a)) relay = (RELAY_BUNDLE_RE.exec(c || a)?.[0] || 'proxy app').replace(/^\/|\.app\/$/gi, '');
|
|
295
|
+
// A browser of the owner's, by its executable's place (argv[0]: the helper and network processes live inside the
|
|
296
|
+
// app's bundle) — never by its arguments, and never when the kernel's own name for the process is an interpreter:
|
|
297
|
+
// a script can rewrite its argv (process.title) to look like a browser, it cannot rename what the kernel ran.
|
|
298
|
+
const browser = !automation && !INTERPRETER_RE.test(u)
|
|
299
|
+
&& (BROWSER_BUNDLE_RE.test(c) || /com\.apple\.webkit\.networking/.test(lower(c)) || u === 'com.apple.webkit')
|
|
300
|
+
? 'browser' : false;
|
|
301
|
+
return { automation, agent, synabun, remoteLogin, relay, browser };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── SynaBun's own MCP server over HTTP ───────────────────────────────────────
|
|
305
|
+
//
|
|
306
|
+
// `node mcp-server/dist/http.js` (MCP_HTTP_PORT, 3345 by default; `npm run start:http` in mcp-server/ runs
|
|
307
|
+
// `node dist/preload-http.js`, which loads it) listens for MCP clients and calls this server's REST routes for their
|
|
308
|
+
// tools, with the same pin the stdio MCP server sends (X-Synabun-Terminal). It is a relay by design, so the listener
|
|
309
|
+
// rule alone would refuse every gated call of its tools. It is told apart by what the kernel ran (node, by its kernel
|
|
310
|
+
// name) and its main script: this install's mcp-server/dist/http.js or preload-http.js, written in full or relative to
|
|
311
|
+
// its working directory — never by a word elsewhere on its command line, never a copy outside this install. Its calls
|
|
312
|
+
// are then taken like an agent's (lib/owner-authority.js: pinned, its route's rules). What keeps a sandboxed worker
|
|
313
|
+
// from using it as a way in is that the worker cannot reach its port (lib/worker-relay-ports.js denies it, as it
|
|
314
|
+
// denies every relay identity; server.js denies the configured port as well), and a worker that can reach it is a
|
|
315
|
+
// full worker, which can reach SynaBun's own port anyway: a lookalike started by a worker gains nothing either way.
|
|
316
|
+
|
|
317
|
+
/** The main scripts of SynaBun's MCP server over HTTP, under a SynaBun install's root. */
|
|
318
|
+
export const MCP_HTTP_SCRIPTS = Object.freeze(['mcp-server/dist/http.js', 'mcp-server/dist/preload-http.js']);
|
|
319
|
+
const MCP_HTTP_TAIL_RE = /^(\/.*?\/mcp-server\/dist\/(?:preload-)?http\.js)(?:\s|$)/;
|
|
320
|
+
const MCP_HTTP_RELATIVE_RE = /^(?:\.\/)?(?:[^/\s]+\/)*dist\/(?:preload-)?http\.js$/;
|
|
321
|
+
/** node's options that take the next word as their value (the main script is the first word that is neither). */
|
|
322
|
+
const NODE_VALUE_OPTIONS = new Set([
|
|
323
|
+
'-r', '--require', '--import', '--loader', '--experimental-loader', '-C', '--conditions', '--title', '--input-type',
|
|
324
|
+
'--env-file', '--env-file-if-exists', '--disable-warning', '--watch-path', '--redirect-warnings', '--openssl-config',
|
|
325
|
+
'--icu-data-dir', '--diagnostic-dir', '--report-dir', '--report-filename', '--cpu-prof-dir', '--heap-prof-dir',
|
|
326
|
+
'--stack-trace-limit', '--secure-heap', '--secure-heap-min', '--max-http-header-size', '--test-reporter',
|
|
327
|
+
'--test-reporter-destination', '--trace-event-categories', '--trace-event-file-pattern',
|
|
328
|
+
]);
|
|
329
|
+
/** node's options after which no script file is the main module. */
|
|
330
|
+
const NODE_NO_SCRIPT = new Set(['-e', '--eval', '-p', '--print', '-i', '--interactive']);
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The main script of a `node …` command line: the first word after node's own options, as written (maybe relative),
|
|
334
|
+
* and the command line from it on (`rest`: a full path may hold spaces). `args` is ps's command line (argv joined by
|
|
335
|
+
* spaces), `comm` argv[0]. null when node runs no script file (-e, -p, a REPL).
|
|
336
|
+
*/
|
|
337
|
+
export function nodeMainScript({ comm = '', args = '' } = {}) {
|
|
338
|
+
const a = String(args ?? '');
|
|
339
|
+
const c = String(comm ?? '');
|
|
340
|
+
let at = 0;
|
|
341
|
+
if (c && a.startsWith(c)) at = c.length;
|
|
342
|
+
else { const first = /^\s*\S+/.exec(a); at = first ? first[0].length : 0; }
|
|
343
|
+
const word = /\S+/g;
|
|
344
|
+
word.lastIndex = at;
|
|
345
|
+
let valueNext = false;
|
|
346
|
+
for (let m = word.exec(a); m; m = word.exec(a)) {
|
|
347
|
+
const w = m[0];
|
|
348
|
+
if (valueNext) { valueNext = false; continue; }
|
|
349
|
+
if (w === '--') { const next = word.exec(a); return next ? { script: next[0], rest: a.slice(next.index) } : null; }
|
|
350
|
+
if (w.startsWith('-')) {
|
|
351
|
+
if (NODE_NO_SCRIPT.has(w.split('=')[0])) return null;
|
|
352
|
+
if (!w.includes('=') && NODE_VALUE_OPTIONS.has(w)) valueNext = true;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
return { script: w, rest: a.slice(m.index) };
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Whether a process's main script is a relative `…dist/http.js` / `…dist/preload-http.js`: where it runs decides. */
|
|
361
|
+
export function mcpHttpNeedsCwd({ ucomm = '', comm = '', args = '' } = {}) {
|
|
362
|
+
if (!/^node$/i.test(String(ucomm || '').trim())) return false;
|
|
363
|
+
const main = nodeMainScript({ comm, args });
|
|
364
|
+
return !!main && !main.script.startsWith('/') && MCP_HTTP_RELATIVE_RE.test(main.script);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The full path of a SynaBun MCP-over-HTTP main script a process runs (any SynaBun install: …/mcp-server/dist/http.js
|
|
369
|
+
* or …/preload-http.js), or null. The kernel must have run node (its kernel name, which a title cannot change). A
|
|
370
|
+
* relative script is resolved against `cwd` (null without one).
|
|
371
|
+
*/
|
|
372
|
+
export function mcpHttpScriptPath({ ucomm = '', comm = '', args = '' } = {}, { cwd = null } = {}) {
|
|
373
|
+
if (!/^node$/i.test(String(ucomm || '').trim())) return null;
|
|
374
|
+
const main = nodeMainScript({ comm, args });
|
|
375
|
+
if (!main) return null;
|
|
376
|
+
if (main.script.startsWith('/')) {
|
|
377
|
+
const written = posix.normalize(main.script);
|
|
378
|
+
if (/\/mcp-server\/dist\/(?:preload-)?http\.js$/.test(written)) return written;
|
|
379
|
+
// A first word that is a whole script file of its own is another program (that may name the file further on).
|
|
380
|
+
if (/\.[cm]?js$/i.test(main.script)) return null;
|
|
381
|
+
// A full path with spaces: the command line from the script on starts with it.
|
|
382
|
+
const m = MCP_HTTP_TAIL_RE.exec(main.rest);
|
|
383
|
+
return m ? posix.normalize(m[1]) : null;
|
|
384
|
+
}
|
|
385
|
+
if (!MCP_HTTP_RELATIVE_RE.test(main.script) || typeof cwd !== 'string' || !cwd.startsWith('/')) return null;
|
|
386
|
+
const full = posix.resolve(cwd, main.script);
|
|
387
|
+
return /\/mcp-server\/dist\/(?:preload-)?http\.js$/.test(full) ? full : null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Whether a process is SynaBun's own MCP server over HTTP from one of `roots` (this server's install, as written and
|
|
392
|
+
* as its real path): its main script is <root>/mcp-server/dist/http.js or …/preload-http.js, or a link to one.
|
|
393
|
+
* @param {{ucomm:string, comm:string, args:string}} proc
|
|
394
|
+
* @param {{ roots?: string[], cwd?: string|null, realpath?: (p:string) => string|null }} [o]
|
|
395
|
+
*/
|
|
396
|
+
export function isSynabunMcpServer(proc, { roots = [], cwd = null, realpath = null } = {}) {
|
|
397
|
+
const script = mcpHttpScriptPath(proc, { cwd });
|
|
398
|
+
if (!script) return false;
|
|
399
|
+
const targets = new Set();
|
|
400
|
+
for (const root of roots || []) {
|
|
401
|
+
if (typeof root !== 'string' || !root.startsWith('/')) continue;
|
|
402
|
+
for (const rel of MCP_HTTP_SCRIPTS) targets.add(posix.join(root, rel));
|
|
403
|
+
}
|
|
404
|
+
if (targets.has(script)) return true;
|
|
405
|
+
if (typeof realpath !== 'function') return false;
|
|
406
|
+
let real = null;
|
|
407
|
+
try { real = realpath(script); } catch { real = null; }
|
|
408
|
+
return typeof real === 'string' && targets.has(real);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Processes SynaBun starts for an agent ────────────────────────────────────
|
|
412
|
+
//
|
|
413
|
+
// A command an agent runs through SynaBun (the Antigravity panel's client terminals, lib/antigravity/acp-client-host.js)
|
|
414
|
+
// is a child of this server, not of the agent, so no agent is in its process tree by name. Its pid is registered here
|
|
415
|
+
// while it runs, and a chain that passes through one is an agent's tree. Like the name rule, a command that detaches
|
|
416
|
+
// from its parent (and so from the chain) is not seen.
|
|
417
|
+
const AGENT_PROCESSES = new Map(); // pid -> label
|
|
418
|
+
|
|
419
|
+
/** A process SynaBun started on an agent's behalf: its tree never holds the owner's authority while it runs. */
|
|
420
|
+
export function registerAgentProcess(pid, label = 'agent command') {
|
|
421
|
+
if (Number.isInteger(pid) && pid > 1) AGENT_PROCESSES.set(pid, String(label || 'agent command').slice(0, 80));
|
|
422
|
+
}
|
|
423
|
+
/** That process ended (its pid can be reused). */
|
|
424
|
+
export function forgetAgentProcess(pid) {
|
|
425
|
+
AGENT_PROCESSES.delete(pid);
|
|
426
|
+
}
|
|
427
|
+
/** The registered processes (for tests and diagnostics). */
|
|
428
|
+
export function agentProcesses() {
|
|
429
|
+
return new Map(AGENT_PROCESSES);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Whether the process at the client end of a connection, and its parents, may hold the owner's authority.
|
|
434
|
+
* @param {Array<{pid:number, ppid:number, ucomm:string, comm:string, args:string}>} chain the peer first, then its
|
|
435
|
+
* parent, and so on (stops before this server, launchd or a loop)
|
|
436
|
+
* @param {{ selfPid:number, listening:Set<number>, synabunMcp?:number }} o `synabunMcp`: the peer's pid when it is
|
|
437
|
+
* SynaBun's own MCP server over HTTP (isSynabunMcpServer): not refused as a relay, refused like an agent's tree
|
|
438
|
+
* (kind 'synabun-mcp': lib/owner-authority.js takes its pinned calls as an agent's own)
|
|
439
|
+
* `agentPids`: processes SynaBun started for an agent (registerAgentProcess), refused like an agent's tree
|
|
440
|
+
* @returns {{ self:boolean, refuse: null | { why:string, kind:string, pid:number, name:string } }}
|
|
441
|
+
*/
|
|
442
|
+
export function classifyPeerChain(chain, { selfPid = process.pid, listening = new Set(), synabunMcp = 0, agentPids = AGENT_PROCESSES } = {}) {
|
|
443
|
+
const list = Array.isArray(chain) ? chain : [];
|
|
444
|
+
if (!list.length) return { self: false, refuse: null };
|
|
445
|
+
if (list[0].pid === selfPid) return { self: true, refuse: null };
|
|
446
|
+
const nameOf = (p) => String(p.ucomm || baseName(p.comm) || `pid ${p.pid}`).trim();
|
|
447
|
+
const refuse = (p, kind, why) => ({ self: false, refuse: { kind, why, pid: p.pid, name: nameOf(p) } });
|
|
448
|
+
const peerKinds = processKinds(list[0]);
|
|
449
|
+
const mcp = Number(synabunMcp) > 0 && list[0].pid === Number(synabunMcp);
|
|
450
|
+
for (let i = 0; i < list.length; i++) {
|
|
451
|
+
const p = list[i];
|
|
452
|
+
if (!p || p.pid === selfPid || p.pid <= 1) break; // this server's own ancestry, and launchd, are not the peer's
|
|
453
|
+
const k = i === 0 ? peerKinds : processKinds(p);
|
|
454
|
+
if (k.automation) return refuse(p, 'automation-browser', `a browser started for automation (${k.automation})`);
|
|
455
|
+
if (k.agent) return refuse(p, 'agent', `an AI agent's process tree (${k.agent})`);
|
|
456
|
+
const startedFor = agentPids?.get?.(p.pid);
|
|
457
|
+
if (startedFor) return refuse(p, 'agent', `an AI agent's process tree (${startedFor}, started by SynaBun)`);
|
|
458
|
+
if (k.synabun) return refuse(p, 'synabun-instance', 'another SynaBun server');
|
|
459
|
+
if (k.remoteLogin) return refuse(p, 'remote-login', 'a remote login session (sshd)');
|
|
460
|
+
// SynaBun's own MCP server over HTTP relays for its MCP clients by design: not refused as a relay or a listener
|
|
461
|
+
// (its parents still are), its calls taken like an agent's (below).
|
|
462
|
+
if (i === 0 && mcp) continue;
|
|
463
|
+
if (i <= 1 && k.relay) return refuse(p, 'relay', `a relay or proxy program (${k.relay})`);
|
|
464
|
+
// A program that serves TCP itself and calls SynaBun is relaying for whoever reached it (a dev server's proxy,
|
|
465
|
+
// a small SOCKS / HTTP proxy). The owner's browsers are left out of this one rule (their network process, or a
|
|
466
|
+
// browser that holds a socket for its own reasons): their automation flags are what is asked of them.
|
|
467
|
+
if (i <= 1 && !peerKinds.browser && listening.has(p.pid)) return refuse(p, 'listener', 'a program that also listens for connections (a relay or proxy)');
|
|
468
|
+
}
|
|
469
|
+
if (mcp) return refuse(list[0], 'synabun-mcp', 'SynaBun\'s own MCP server over HTTP (it calls for whichever MCP client reached it)');
|
|
470
|
+
return { self: false, refuse: null };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** The environment part of `ps -E` output for one process, given the same process's line without -E. */
|
|
474
|
+
export function envPart(withEnv, plain) {
|
|
475
|
+
const w = String(withEnv ?? '');
|
|
476
|
+
const p = String(plain ?? '');
|
|
477
|
+
if (p && w.startsWith(p)) return w.slice(p.length);
|
|
478
|
+
return null; // the two reads disagree (the process changed in between): nothing is taken from it
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Whether an environment block (ps -E's tail) holds SYNABUN_BRAIN_TOKEN=<credential> as a whole variable. */
|
|
482
|
+
export function envHoldsCredential(env, credential) {
|
|
483
|
+
if (typeof env !== 'string' || typeof credential !== 'string' || !/^sbb_[A-Za-z0-9_-]{43}$/.test(credential)) return false;
|
|
484
|
+
const needle = `SYNABUN_BRAIN_TOKEN=${credential}`;
|
|
485
|
+
let from = 0;
|
|
486
|
+
for (;;) {
|
|
487
|
+
const at = env.indexOf(needle, from);
|
|
488
|
+
if (at < 0) return false;
|
|
489
|
+
const before = at === 0 ? ' ' : env[at - 1];
|
|
490
|
+
const after = env[at + needle.length];
|
|
491
|
+
if (/\s/.test(before) && (after === undefined || /\s/.test(after))) return true;
|
|
492
|
+
from = at + 1;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── The reader ───────────────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
function defaultRun(file, args, { timeout }) {
|
|
499
|
+
return new Promise((resolve, reject) => {
|
|
500
|
+
try {
|
|
501
|
+
execFile(file, args, { encoding: 'utf8', timeout, maxBuffer: 64 * 1024 * 1024, windowsHide: true, env: { PATH: '/usr/bin:/bin:/usr/sbin:/sbin', LANG: 'C', LC_ALL: 'C' } }, (error, stdout) => {
|
|
502
|
+
if (error) reject(error); else resolve(String(stdout || ''));
|
|
503
|
+
});
|
|
504
|
+
} catch (error) { reject(error); }
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function thisMachineAddresses() {
|
|
509
|
+
const out = new Set(['127.0.0.1', '::1']);
|
|
510
|
+
try {
|
|
511
|
+
for (const list of Object.values(networkInterfaces())) for (const ni of list || []) if (ni?.address) out.add(normalizeAddress(ni.address));
|
|
512
|
+
} catch { /* no interfaces readable: loopback only */ }
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* @param {object} [o]
|
|
518
|
+
* @param {string} [o.platform] process.platform; only darwin is looked up (everything else is "unknown").
|
|
519
|
+
* @param {number} [o.selfPid] this server's pid (its own in-process calls are trusted).
|
|
520
|
+
* @param {(file:string, args:string[], opts:{timeout:number}) => Promise<string>} [o.run] runs a program (execFile-shaped).
|
|
521
|
+
* @param {() => Set<string>} [o.localAddresses] this computer's addresses (normalized).
|
|
522
|
+
* @param {(line:string) => void} [o.log]
|
|
523
|
+
* @param {() => number} [o.now]
|
|
524
|
+
* @param {boolean} [o.disabled] SYNABUN_PEER_IDENTITY=off: nothing is looked up (the rules from before), said once.
|
|
525
|
+
* @param {string[]} [o.synabunRoots] this server's install (the folder holding mcp-server/): its MCP server over HTTP
|
|
526
|
+
* is told apart from other listening programs (isSynabunMcpServer). None: it is refused as a listener, as any relay.
|
|
527
|
+
* @param {(path:string) => string} [o.realpath] fs.realpathSync (tests inject one).
|
|
528
|
+
*/
|
|
529
|
+
export function createPeerIdentity({ platform = process.platform, selfPid = process.pid, run = defaultRun, localAddresses = null, log = () => {}, now = Date.now, disabled = false, synabunRoots = [], realpath = realpathSync } = {}) {
|
|
530
|
+
const supported = platform === 'darwin' && !disabled;
|
|
531
|
+
if (disabled) { try { log('[peer-identity] off (SYNABUN_PEER_IDENTITY=off): the page secret\'s rules from before apply, with no peer lookup'); } catch { /* logger */ } }
|
|
532
|
+
const bySocket = new WeakMap();
|
|
533
|
+
const chains = new Map(); // pid → { at, ucomm, chain }
|
|
534
|
+
const envs = new Map(); // `${pid}\0${credential}` → { at, holds }
|
|
535
|
+
const mcpServers = new Map(); // `${pid}\0${args}` → { at, yes }
|
|
536
|
+
// This install's root as written and as its real path (a linked install names either).
|
|
537
|
+
const roots = [...new Set((Array.isArray(synabunRoots) ? synabunRoots : []).filter((r) => typeof r === 'string' && r.startsWith('/')).flatMap((r) => {
|
|
538
|
+
const written = posix.normalize(r).replace(/(.)\/+$/, '$1');
|
|
539
|
+
let real = null;
|
|
540
|
+
try { real = typeof realpath === 'function' ? realpath(written) : null; } catch { real = null; }
|
|
541
|
+
return typeof real === 'string' && real !== written ? [written, real] : [written];
|
|
542
|
+
}))];
|
|
543
|
+
const logged = new Map(); // key → last time
|
|
544
|
+
const stats = { lookups: 0, tableReads: 0, psReads: 0, envReads: 0, refused: 0, unknown: 0, gone: 0, totalMs: 0, maxMs: 0 };
|
|
545
|
+
let addrCache = null;
|
|
546
|
+
let addrAt = 0;
|
|
547
|
+
|
|
548
|
+
const note = (key, line) => {
|
|
549
|
+
const last = logged.get(key) || 0;
|
|
550
|
+
if (now() - last < LOG_EVERY_MS) return;
|
|
551
|
+
logged.set(key, now());
|
|
552
|
+
if (logged.size > 500) logged.delete(logged.keys().next().value);
|
|
553
|
+
try { log(line); } catch { /* a logger's own failure */ }
|
|
554
|
+
};
|
|
555
|
+
const isThisMachine = (addr) => {
|
|
556
|
+
if (typeof localAddresses === 'function') { try { return localAddresses().has(addr); } catch { return false; } }
|
|
557
|
+
if (!addrCache || now() - addrAt > LOCAL_ADDR_TTL_MS) { addrCache = thisMachineAddresses(); addrAt = now(); }
|
|
558
|
+
return addrCache.has(addr);
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// One TCP table read serves every lookup that was waiting when it STARTED (a socket accepted after a read began
|
|
562
|
+
// may not be in it, so its lookup waits for the next read).
|
|
563
|
+
let tableRunning = null;
|
|
564
|
+
let tableNext = null;
|
|
565
|
+
function tcpTable() {
|
|
566
|
+
if (tableNext) return tableNext;
|
|
567
|
+
const after = tableRunning || Promise.resolve();
|
|
568
|
+
tableNext = after.catch(() => {}).then(() => {
|
|
569
|
+
tableNext = null;
|
|
570
|
+
stats.tableReads++;
|
|
571
|
+
const startedAt = now();
|
|
572
|
+
// -l: full IPv6 addresses (without it macOS cuts a long one, fe80::105e:de82:…, and the client's row would not
|
|
573
|
+
// be found: a connection taken for one that closed, refused).
|
|
574
|
+
const read = run(NETSTAT, NETSTAT_ARGS, { timeout: TABLE_TIMEOUT_MS }).then((text) => {
|
|
575
|
+
const table = parseNetstatTcp(text);
|
|
576
|
+
if (!table.rows.length) throw new Error('netstat listed no TCP sockets');
|
|
577
|
+
// A layout this parser does not read gives no process at all: that is a lookup that cannot run (the rules from
|
|
578
|
+
// before), never "every client left".
|
|
579
|
+
if (!table.rows.some((r) => r.pid > 0)) throw new Error('netstat named no processes (an unknown output layout)');
|
|
580
|
+
return { ...table, startedAt };
|
|
581
|
+
});
|
|
582
|
+
tableRunning = read;
|
|
583
|
+
read.catch(() => {}).then(() => { if (tableRunning === read) tableRunning = null; });
|
|
584
|
+
return read;
|
|
585
|
+
});
|
|
586
|
+
return tableNext;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// The process table (pid, ppid, kernel name, argv[0], command line), kept a moment so a burst of lookups reads it
|
|
590
|
+
// once. Each read remembers when it STARTED: a process that owned a socket when a netstat read started is in every
|
|
591
|
+
// ps read that started after it (unless it has exited since), which is what `notBefore` asks for.
|
|
592
|
+
let psSnap = null;
|
|
593
|
+
let psPending = null;
|
|
594
|
+
function processTable({ notBefore = null } = {}) {
|
|
595
|
+
if (psSnap && (notBefore === null ? now() - psSnap.startedAt < PS_SNAPSHOT_TTL_MS : psSnap.startedAt >= notBefore)) return Promise.resolve(psSnap);
|
|
596
|
+
if (psPending && (notBefore === null || psPending.startedAt >= notBefore)) return psPending.promise;
|
|
597
|
+
stats.psReads++;
|
|
598
|
+
const startedAt = now();
|
|
599
|
+
const promise = Promise.all([
|
|
600
|
+
run(PS, ['-Ao', 'pid=,ppid=,ucomm='], { timeout: PS_TIMEOUT_MS }),
|
|
601
|
+
run(PS, ['-Aww', '-o', 'pid=,comm='], { timeout: PS_TIMEOUT_MS }),
|
|
602
|
+
run(PS, ['-Aww', '-o', 'pid=,args='], { timeout: PS_TIMEOUT_MS }),
|
|
603
|
+
]).then(([parents, comms, args]) => {
|
|
604
|
+
const snap = { parents: parsePsParents(parents), comm: parsePsColumn(comms), args: parsePsColumn(args), startedAt };
|
|
605
|
+
if (!snap.parents.size) throw new Error('ps listed no processes');
|
|
606
|
+
if (!psSnap || psSnap.startedAt <= startedAt) psSnap = snap;
|
|
607
|
+
return snap;
|
|
608
|
+
}).finally(() => { if (psPending?.promise === promise) psPending = null; });
|
|
609
|
+
psPending = { promise, startedAt };
|
|
610
|
+
return promise;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function chainFrom(snap, pid) {
|
|
614
|
+
const chain = [];
|
|
615
|
+
const seen = new Set();
|
|
616
|
+
let at = pid;
|
|
617
|
+
while (at > 0 && !seen.has(at) && chain.length < MAX_CHAIN) {
|
|
618
|
+
seen.add(at);
|
|
619
|
+
const row = snap.parents.get(at);
|
|
620
|
+
if (!row) break;
|
|
621
|
+
chain.push({ pid: at, ppid: row.ppid, ucomm: row.ucomm, comm: snap.comm.get(at) || '', args: snap.args.get(at) || '' });
|
|
622
|
+
if (at === selfPid || at <= 1) break;
|
|
623
|
+
at = row.ppid;
|
|
624
|
+
}
|
|
625
|
+
return chain;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** The peer's chain; null when a ps read that started after `seenAt` (the netstat read that named it) lacks it. */
|
|
629
|
+
async function chainOf(pid, kernelName, seenAt) {
|
|
630
|
+
const hit = chains.get(pid);
|
|
631
|
+
if (hit && now() - hit.at < CHAIN_TTL_MS && (!kernelName || !hit.ucomm || kernelName.startsWith(hit.ucomm.slice(0, 15)) || hit.ucomm.startsWith(kernelName.slice(0, 15)))) return hit.chain;
|
|
632
|
+
let snap = await processTable();
|
|
633
|
+
if (!snap.parents.has(pid)) snap = await processTable({ notBefore: Number.isFinite(seenAt) ? seenAt : now() });
|
|
634
|
+
if (!snap.parents.has(pid)) return null;
|
|
635
|
+
const chain = chainFrom(snap, pid);
|
|
636
|
+
chains.set(pid, { at: now(), ucomm: chain[0]?.ucomm || '', chain });
|
|
637
|
+
if (chains.size > 2000) chains.delete(chains.keys().next().value);
|
|
638
|
+
return chain;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* The peer's pid when it is SynaBun's own MCP server over HTTP (it listens, the kernel ran node, its main script is
|
|
643
|
+
* this install's dist/http.js or preload-http.js), else 0. Its working directory is read (lsof) only for a relative
|
|
644
|
+
* script. Kept with the process's command line: a pid reused by another command is asked again.
|
|
645
|
+
*/
|
|
646
|
+
async function synabunMcpPid(peer, listening) {
|
|
647
|
+
if (!roots.length || !peer || !(peer.pid > 1) || !listening.has(peer.pid) || !/^node$/i.test(String(peer.ucomm || '').trim())) return 0;
|
|
648
|
+
const key = `${peer.pid}\0${peer.args || ''}`;
|
|
649
|
+
const hit = mcpServers.get(key);
|
|
650
|
+
if (hit && now() - hit.at < CHAIN_TTL_MS) return hit.yes ? peer.pid : 0;
|
|
651
|
+
let cwd = null;
|
|
652
|
+
if (mcpHttpNeedsCwd(peer)) {
|
|
653
|
+
try { cwd = parseLsofCwd(await run(LSOF, lsofCwdArgs(peer.pid), { timeout: PS_TIMEOUT_MS })); } catch { cwd = null; }
|
|
654
|
+
}
|
|
655
|
+
const yes = isSynabunMcpServer(peer, { roots, cwd, realpath });
|
|
656
|
+
mcpServers.set(key, { at: now(), yes });
|
|
657
|
+
if (mcpServers.size > 500) mcpServers.delete(mcpServers.keys().next().value);
|
|
658
|
+
return yes ? peer.pid : 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function identify(socket) {
|
|
662
|
+
const started = now();
|
|
663
|
+
const remote = normalizeAddress(socket?.remoteAddress);
|
|
664
|
+
const remotePort = Number(socket?.remotePort);
|
|
665
|
+
const local = normalizeAddress(socket?.localAddress);
|
|
666
|
+
const localPort = Number(socket?.localPort);
|
|
667
|
+
if (!remote || !remotePort || !localPort) return { status: 'unknown', reason: 'the connection has no address' };
|
|
668
|
+
if (!isThisMachine(remote)) return { status: 'remote' };
|
|
669
|
+
if (!supported) return { status: 'unknown', reason: `peer lookup is not available on ${platform}` };
|
|
670
|
+
let table;
|
|
671
|
+
try { table = await tcpTable(); } catch (error) { return { status: 'unknown', reason: `netstat: ${error?.code || error?.message || error}` }; }
|
|
672
|
+
const row = table.rows.find((r) => r.local.port === remotePort && r.local.addr === remote && r.foreign.port === localPort && (r.foreign.addr === local || !local));
|
|
673
|
+
if (!row || !row.pid) return { status: 'gone', reason: 'the connection closed before its request was identified', ms: now() - started };
|
|
674
|
+
if (row.pid === selfPid) return { status: 'ok', self: true, pid: row.pid, name: row.name, chain: [], refuse: null, ms: now() - started };
|
|
675
|
+
let chain;
|
|
676
|
+
try { chain = await chainOf(row.pid, row.name, table.startedAt); } catch (error) { return { status: 'unknown', reason: `ps: ${error?.code || error?.message || error}`, pid: row.pid }; }
|
|
677
|
+
// The process table was read and the client is not in it: it sent its request and exited without waiting for the
|
|
678
|
+
// answer (a one-shot relay). A browser or a brain waits for its answer; this is treated like a closed socket.
|
|
679
|
+
if (!chain || !chain.length) return { status: 'gone', reason: 'the client process exited before it was identified', pid: row.pid, ms: now() - started };
|
|
680
|
+
let synabunMcp = 0;
|
|
681
|
+
try { synabunMcp = await synabunMcpPid(chain[0], table.listening); } catch { synabunMcp = 0; }
|
|
682
|
+
const verdict = classifyPeerChain(chain, { selfPid, listening: table.listening, synabunMcp });
|
|
683
|
+
return { status: 'ok', self: verdict.self, pid: row.pid, name: row.name || chain[0].ucomm, chain, refuse: verdict.refuse, ms: now() - started };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** The peer of a request's connection, looked up once per connection (keep-alive requests share it). Never rejects. */
|
|
687
|
+
function lookup(req) {
|
|
688
|
+
const socket = req?.socket || req?.connection || null;
|
|
689
|
+
if (!socket || typeof socket !== 'object') return Promise.resolve({ status: 'unknown', reason: 'no connection' });
|
|
690
|
+
let p = bySocket.get(socket);
|
|
691
|
+
if (!p) {
|
|
692
|
+
let timer = null;
|
|
693
|
+
const cap = new Promise((resolve) => { timer = setTimeout(() => resolve({ status: 'unknown', reason: `the lookup took over ${LOOKUP_CAP_MS} ms` }), LOOKUP_CAP_MS); timer.unref?.(); });
|
|
694
|
+
p = Promise.race([identify(socket).catch((error) => ({ status: 'unknown', reason: error?.message || String(error) })), cap]).finally(() => clearTimeout(timer));
|
|
695
|
+
bySocket.set(socket, p);
|
|
696
|
+
}
|
|
697
|
+
return p;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** Whether some process in `chain` (the peer up to this server) holds the credential in its own environment. */
|
|
701
|
+
async function chainHoldsCredential(chain, credential) {
|
|
702
|
+
const pids = chain.filter((p) => p.pid > 1 && p.pid !== selfPid).map((p) => p.pid);
|
|
703
|
+
if (!pids.length) return false;
|
|
704
|
+
const key = (pid) => `${pid}\0${credential}`;
|
|
705
|
+
const known = pids.map((pid) => envs.get(key(pid))).filter((e) => e && now() - e.at < ENV_TTL_MS);
|
|
706
|
+
if (known.some((e) => e.holds)) return true;
|
|
707
|
+
const ask = pids.filter((pid) => { const e = envs.get(key(pid)); return !e || now() - e.at >= ENV_TTL_MS; });
|
|
708
|
+
if (!ask.length) return false;
|
|
709
|
+
stats.envReads++;
|
|
710
|
+
const list = ask.join(',');
|
|
711
|
+
const [withEnv, plain] = await Promise.all([
|
|
712
|
+
run(PS, ['-E', '-ww', '-o', 'pid=,command=', '-p', list], { timeout: PS_TIMEOUT_MS }),
|
|
713
|
+
run(PS, ['-ww', '-o', 'pid=,command=', '-p', list], { timeout: PS_TIMEOUT_MS }),
|
|
714
|
+
]);
|
|
715
|
+
const e = parsePsColumn(withEnv);
|
|
716
|
+
const p = parsePsColumn(plain);
|
|
717
|
+
let holds = false;
|
|
718
|
+
for (const pid of ask) {
|
|
719
|
+
const h = envHoldsCredential(envPart(e.get(pid), p.get(pid)), credential);
|
|
720
|
+
envs.set(key(pid), { at: now(), holds: h });
|
|
721
|
+
if (h) holds = true;
|
|
722
|
+
}
|
|
723
|
+
if (envs.size > 2000) envs.delete(envs.keys().next().value);
|
|
724
|
+
return holds;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Express middleware: looks the peer up when `needs(req)` says the request asks for, or carries, the owner's
|
|
729
|
+
* authority, and keeps the verdict on the request (refusal() reads it). A brain credential from a peer outside the
|
|
730
|
+
* brain's own process tree is removed from the request. Never fails a request: a lookup that cannot run leaves the
|
|
731
|
+
* rules from before in force.
|
|
732
|
+
*/
|
|
733
|
+
function middleware({ needs = () => false } = {}) {
|
|
734
|
+
// Express 4 does not catch a rejected promise: the decision is made apart from next(), which is called exactly
|
|
735
|
+
// once whatever happens in it (an unexpected error leaves the rules from before, and is said once).
|
|
736
|
+
return function peerIdentity(req, res, next) {
|
|
737
|
+
if (disabled) return next();
|
|
738
|
+
let wanted = false;
|
|
739
|
+
try { wanted = needs(req) === true; } catch { wanted = false; }
|
|
740
|
+
const credential = typeof req.headers?.[BRAIN_HEADER] === 'string' ? req.headers[BRAIN_HEADER].trim() : null;
|
|
741
|
+
if (!wanted && !credential) return next();
|
|
742
|
+
decide(req, wanted, credential).then(() => next(), (error) => {
|
|
743
|
+
note(`error:${error?.message}`, `[peer-identity] the lookup failed (${error?.message || error}); the page secret's rules from before apply`);
|
|
744
|
+
next();
|
|
745
|
+
});
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
/** The lookup for one request: the verdict kept on it, a brain credential from outside its brain's tree dropped. */
|
|
749
|
+
async function decide(req, wanted, credential) {
|
|
750
|
+
const t0 = now();
|
|
751
|
+
let peer;
|
|
752
|
+
try { peer = await lookup(req); } catch (error) { peer = { status: 'unknown', reason: error?.message || String(error) }; }
|
|
753
|
+
const ms = now() - t0;
|
|
754
|
+
stats.lookups++; stats.totalMs += ms; if (ms > stats.maxMs) stats.maxMs = ms;
|
|
755
|
+
req[PEER_VERDICT] = peer;
|
|
756
|
+
if (peer.status === 'unknown') { stats.unknown++; note(`unknown:${peer.reason}`, `[peer-identity] could not tell who is asking (${peer.reason}); the page secret's rules from before apply`); }
|
|
757
|
+
if (peer.status === 'gone') { stats.gone++; note(`gone`, `[peer-identity] ${peer.reason || 'a connection closed before its request was identified'}: no owner authority for it`); }
|
|
758
|
+
if (peer.status === 'ok' && peer.refuse && wanted) {
|
|
759
|
+
stats.refused++;
|
|
760
|
+
note(`refuse:${peer.refuse.pid}:${peer.refuse.kind}`, `[peer-identity] no page secret / owner authority for pid ${peer.refuse.pid} (${peer.refuse.name}): ${peer.refuse.why}`);
|
|
761
|
+
}
|
|
762
|
+
if (!credential) return;
|
|
763
|
+
let keep = true;
|
|
764
|
+
if (peer.status === 'gone') keep = false;
|
|
765
|
+
else if (peer.status === 'ok' && !peer.self) {
|
|
766
|
+
try { keep = await chainHoldsCredential(peer.chain || [], credential); } catch (error) {
|
|
767
|
+
keep = true; // the environment could not be read: the rules from before (the credential alone)
|
|
768
|
+
note(`env:${error?.code || error?.message}`, `[peer-identity] could not read a brain's environment (${error?.code || error?.message || error}); the brain credential is taken as before`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (!keep) {
|
|
772
|
+
delete req.headers[BRAIN_HEADER];
|
|
773
|
+
note(`brain:${peer.pid || 'gone'}`, `[peer-identity] a brain credential came from pid ${peer.pid || '?'} (${peer.name || 'closed connection'}), outside the brain's own process tree: it was dropped from the request`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Why this request may not hold or use the owner's page secret (a short sentence), or null. Null as well when the
|
|
779
|
+
* peer could not be identified or was never looked up: the caller's rules from before decide.
|
|
780
|
+
*/
|
|
781
|
+
function refusal(req) {
|
|
782
|
+
const peer = req?.[PEER_VERDICT];
|
|
783
|
+
if (!peer) return null;
|
|
784
|
+
if (peer.status === 'gone') return peer.reason || 'the connection closed before its request was identified';
|
|
785
|
+
if (peer.status === 'ok' && peer.refuse) return `${peer.refuse.why}: pid ${peer.refuse.pid} (${peer.refuse.name})`;
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return { supported, lookup, middleware, refusal, chainHoldsCredential, stats: () => ({ ...stats, avgMs: stats.lookups ? Math.round((stats.totalMs / stats.lookups) * 10) / 10 : 0 }) };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** Exposed for tests: whether an address is a literal IP (4 or 6). */
|
|
793
|
+
export function isIpLiteral(addr) { return isIP(normalizeAddress(addr)) !== 0; }
|