elanous 0.2.4 → 0.2.5
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/README.md +3 -3
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +1 -0
- package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 1751.50b96ea9c37b49b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +25 -0
- package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +5 -0
- package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 4daa2798-1363d725c79f3593.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 5348-bbf838e119661eee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5391-8793f88b03b30161.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 8617.465ca22a319c95b3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8918-482b30ce2a8ebf4c.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 9704-3221c102623c8d73.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-9fa879ebf5959096.js → page-0ac5d053f0444d21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-4472ddf1e2458e69.js → page-72bd384898d88955.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-112f7e477bda77fd.js → page-bccce0babceae30f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6f3850d-7f324c6566002113.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-2bf59c720fe9542c.js} +1 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
- package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_buildManifest.js +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +29 -27
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +29 -27
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +29 -27
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +26 -24
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +26 -24
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +26 -24
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +29 -27
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +29 -27
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +29 -27
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/icons/elanous-16.png +0 -0
- package/apps/pwa/out/icons/elanous-180.png +0 -0
- package/apps/pwa/out/icons/elanous-192.png +0 -0
- package/apps/pwa/out/icons/elanous-32.png +0 -0
- package/apps/pwa/out/icons/elanous-512.png +0 -0
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +26 -24
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +29 -27
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +29 -27
- package/apps/pwa/out/manifest.webmanifest +5 -2
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +26 -24
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +26 -24
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +29 -27
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +29 -27
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +27 -25
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +29 -27
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +26 -24
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +29 -27
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +29 -27
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +29 -27
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +29 -27
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +29 -27
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +26 -24
- package/apps/pwa/out/sw-strategies.js +44 -0
- package/apps/pwa/out/sw.js +6 -44
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +29 -27
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +29 -27
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +29 -27
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +29 -27
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +29 -27
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +29 -27
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +29 -27
- package/catalog/resources.yaml +6 -6
- package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
- package/graphs/landing/landing-heal.yaml +1 -1
- package/graphs/release/recipes.yaml +8 -1
- package/graphs/release/release-loop.yaml +6 -1
- package/graphs/video/README.md +1 -0
- package/graphs/video/explainer-line.yaml +58 -0
- package/package.json +3 -1
- package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
- package/plugins/job-coach/README.md +12 -0
- package/plugins/job-coach/connectors/ncs/server.ts +4 -3
- package/plugins/job-coach/graphs/gap-judge.ts +44 -0
- package/plugins/job-coach/graphs/recipes.yaml +27 -0
- package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
- package/plugins/job-coach/graphs/run-step.ts +211 -17
- package/plugins/job-coach/plugin.json +16 -3
- package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
- package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
- package/scripts/backup/elanous-backup.sh +4 -1
- package/scripts/build-docs-site.ts +4 -3
- package/scripts/coord-channel-watch.sh +23 -4
- package/scripts/coord-post.sh +49 -12
- package/scripts/coord-tracks.json +5 -5
- package/scripts/eval-bot-vs-grok.ts +79 -0
- package/scripts/eval-grok-live-baseline.ts +36 -0
- package/scripts/fixtures/front-route-corpus.sample.json +17 -0
- package/scripts/fleet/node-setup.sh +46 -7
- package/scripts/landing-heal/must-fix-match.ts +26 -0
- package/scripts/landing-heal/run-mission.ts +114 -20
- package/scripts/lib/front-route-measurement.ts +131 -0
- package/scripts/lib/tui-sim-keys.ts +42 -0
- package/scripts/measure-front-route-classifier.ts +56 -0
- package/scripts/media/ocr-leak-scan.sh +21 -0
- package/scripts/public-export.ts +2 -3
- package/scripts/public-import-pr.ts +80 -6
- package/scripts/publish-internal-dist.ts +2 -3
- package/scripts/red-sweep-diff.ts +1 -1
- package/scripts/release-loop/auto-approve-node.ts +72 -0
- package/scripts/release-loop/gate-node.ts +332 -44
- package/scripts/release-loop/publish-node.ts +7 -3
- package/scripts/release-loop/release-docs.ts +20 -13
- package/scripts/release-loop/test-durations.ts +63 -0
- package/scripts/release-loop/upgrade-node.ts +3 -2
- package/scripts/test-audit-coverage-per-file.sh +16 -0
- package/scripts/test-audit-meaningless.ts +88 -0
- package/scripts/test-audit-overlap-and-weak.ts +73 -0
- package/scripts/test-deterministic.ts +27 -5
- package/scripts/tui-sim.ts +59 -14
- package/scripts/ux/bump-installed.sh +20 -0
- package/scripts/ux/lib/cdp.ts +72 -0
- package/scripts/ux/record-page.ts +50 -0
- package/skills/explainer-video/SKILL.md +99 -0
- package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
- package/skills/explainer-video/engine/build.mjs +431 -0
- package/skills/explainer-video/engine/vo.ts +37 -0
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
- package/src/agent/nested-agent-env.ts +1 -0
- package/src/agent-mission/driver.ts +95 -16
- package/src/agent-mission/mission-cli.ts +6 -0
- package/src/agent-mission/resource-ladder.ts +73 -25
- package/src/auth/acp-token.ts +16 -0
- package/src/auth/github-app-token.ts +112 -0
- package/src/autopilot/gh-rest-fallback.ts +177 -0
- package/src/autopilot/pr-manager.ts +51 -4
- package/src/boot/acp-server.ts +2 -16
- package/src/boot/daemon-tools/web-search.ts +2 -1
- package/src/boot/ws-bridge.ts +11 -0
- package/src/chat/index.ts +37 -8
- package/src/chat/log-scroll-key.ts +12 -0
- package/src/chat/submit-trace.ts +30 -0
- package/src/cli/agent-cli.ts +7 -2
- package/src/cli/decisions-cli.ts +94 -0
- package/src/cli/doctor-cli.ts +40 -54
- package/src/cli/doctor-readiness.ts +16 -24
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/mcp-call.ts +9 -0
- package/src/cli/mcp-gateway-cli.ts +7 -3
- package/src/cli/plugin-cli.ts +109 -11
- package/src/cli/pr-lineage.ts +2 -1
- package/src/cli/pwa-start.ts +3 -0
- package/src/cli/release-cli.ts +94 -7
- package/src/cli/self-update.ts +7 -4
- package/src/cli/start-cli.ts +33 -14
- package/src/dashboard/acp-boot.ts +3 -0
- package/src/dashboard/index.ts +62 -13
- package/src/dashboard/input/no-provider-reply.ts +13 -0
- package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
- package/src/dashboard/stale-install-check.ts +77 -0
- package/src/decisions/decision-ledger.ts +404 -0
- package/src/domains/outbound-alert.ts +8 -2
- package/src/domains/telegram-kind-route.ts +21 -0
- package/src/execution-loop/dispatch-task.ts +139 -0
- package/src/execution-loop/gate-decision-summary.ts +29 -0
- package/src/execution-loop/gates.ts +197 -0
- package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
- package/src/graph-kinds/plugin-nodes.ts +10 -2
- package/src/graph-runner/runner.ts +35 -5
- package/src/grounding/token.ts +12 -6
- package/src/harness/harness-cli-command.ts +68 -9
- package/src/harness/harness-pod-dispatch.ts +8 -2
- package/src/harness/harness-stop.ts +147 -18
- package/src/harness/self-send-decide.ts +26 -17
- package/src/harness/self-send-target.ts +22 -1
- package/src/harness/worktree-branch-prefix.ts +4 -3
- package/src/index.ts +56 -13
- package/src/instance/resolve.ts +6 -0
- package/src/intake-plane/collect-telegram-saved.ts +5 -0
- package/src/intake-plane/digest.ts +11 -2
- package/src/live/detail-switch.ts +8 -3
- package/src/llm/usable-llm.ts +32 -0
- package/src/loops/registry.ts +27 -5
- package/src/market/publish.ts +59 -3
- package/src/native-tool-catalog.ts +2 -2
- package/src/nexus/api/graph-approvals.ts +125 -0
- package/src/nexus/api/http-server.ts +60 -6
- package/src/nexus/api/log-fabric.ts +10 -1
- package/src/nexus/api/mcp-oauth-hint.ts +10 -9
- package/src/nexus/api/merge-approvals.ts +2 -1
- package/src/nexus/api/meta-api.ts +14 -0
- package/src/nexus/api/plugins-credentials-api.ts +47 -0
- package/src/nexus/api/plugins-market.ts +87 -3
- package/src/nexus/api/pod-credential-api.ts +68 -14
- package/src/nexus/api/public-routes.ts +7 -0
- package/src/nexus/api/trace.ts +57 -6
- package/src/nexus/boot/register-mcp-clients.ts +99 -14
- package/src/nexus/index.ts +33 -32
- package/src/nexus/outbound/router.ts +1 -1
- package/src/onboarding.ts +25 -2
- package/src/plugins/core/host.ts +14 -0
- package/src/plugins/install/market-fetch.ts +4 -3
- package/src/plugins/install/plugin-credentials.ts +106 -0
- package/src/plugins/install/plugin-install.ts +163 -32
- package/src/prompt-library/universal-preamble.ts +8 -2
- package/src/release-loop/checklist.ts +191 -0
- package/src/release-loop/release-note.ts +6 -3
- package/src/self-dev/dev-cli.ts +40 -20
- package/src/self-dev/dev-pipeline.ts +77 -1
- package/src/self-implement/gate-baseline.ts +3 -1
- package/src/self-implement/goal-author.ts +18 -1
- package/src/self-implement/host-regate.ts +6 -2
- package/src/self-implement/orchestrator.ts +7 -2
- package/src/self-implement/seams.ts +4 -2
- package/src/skills/tools/dev-harness.ts +42 -0
- package/src/steward/steward-cards.ts +45 -0
- package/src/steward/triage.ts +50 -14
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
- package/src/task-orchestrator/surfaces/pod-command-job.ts +27 -6
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
- package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
- package/src/telegram-report.ts +12 -6
- package/src/tui-client/acp-transport-ws-client.ts +72 -5
- package/src/tui.ts +3 -2
- package/src/user-config.ts +28 -2
- package/src/util/spawn-sync-output.ts +15 -0
- package/src/version/code-revision.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/src/web-search/free.ts +77 -0
- package/src/web-search/index.ts +2 -0
- package/src/workflow-runtime/deps-bridge.ts +4 -3
- package/src/workflow-runtime/plugin-kind-node.ts +32 -4
- package/src/workflow-runtime/types.ts +2 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
- package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-011e1f474efb8b33.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
- /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_ssgManifest.js +0 -0
|
@@ -28,13 +28,21 @@
|
|
|
28
28
|
// daemon exit.
|
|
29
29
|
|
|
30
30
|
import type { McpServerSpec } from '../../user-config.js';
|
|
31
|
-
import { McpClient, McpConnectionError } from '../../mcp/client.js';
|
|
31
|
+
import { McpClient, McpConnectionError, type McpToolCallResult } from '../../mcp/client.js';
|
|
32
32
|
import {
|
|
33
33
|
createMcpProxyRuntime,
|
|
34
34
|
createMcpToolAuthorizer,
|
|
35
35
|
} from '../../mcp/proxy-runtime.js';
|
|
36
36
|
import { registerToolRuntime, unregisterToolRuntime } from '../../tool-runtime/registry.js';
|
|
37
37
|
import { debug, redactSecretText } from '../../debug/log.js';
|
|
38
|
+
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
39
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
40
|
+
import { listNodeKinds } from '../../graph-kinds/registry.js';
|
|
41
|
+
import { syncInstalledPluginNodes } from '../../graph-kinds/installed-plugin-nodes.js';
|
|
42
|
+
import { listInstalledPlugins } from '../../plugins/install/plugin-install.js';
|
|
43
|
+
import { pluginEnv, credentialStatus } from '../../plugins/install/plugin-credentials.js';
|
|
44
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
45
|
+
import { resolve, sep } from 'node:path';
|
|
38
46
|
|
|
39
47
|
export interface McpClientsHandle {
|
|
40
48
|
/** Live clients (one per successfully started server). */
|
|
@@ -172,24 +180,64 @@ export async function registerMcpClients(
|
|
|
172
180
|
// breadcrumbs when a child (e.g. xcrun mcpbridge) deadlocked elanous's
|
|
173
181
|
// boot. The trace category prefix `mcp.client.boot.*` keeps these
|
|
174
182
|
// distinct from any per-call instrumentation a future PR adds.
|
|
183
|
+
const injectedValuesByServer = new Map<string, string[]>();
|
|
184
|
+
const redactPluginValues = (text: string): string => {
|
|
185
|
+
try {
|
|
186
|
+
const root = elanousStateRoot();
|
|
187
|
+
const current = [...new Set(listInstalledPlugins(root).map(item => item.name))]
|
|
188
|
+
.flatMap(name => Object.values(pluginEnv(name, root)));
|
|
189
|
+
return redactValues(text, [...current, ...injectedValuesByServer.values()].flat());
|
|
190
|
+
} catch { return '[REDACTED]'; }
|
|
191
|
+
};
|
|
175
192
|
const bootLogger = (id: string) => (event: string, data?: Record<string, unknown>) => {
|
|
176
193
|
try {
|
|
177
|
-
|
|
194
|
+
const safe = data ? Object.fromEntries(Object.entries(data).map(([key, value]) =>
|
|
195
|
+
[key, typeof value === 'string' ? redactPluginValues(value) : value])) : {};
|
|
196
|
+
debug.log('mcp.client.boot', event, { id, ...safe });
|
|
178
197
|
} catch { /* logger must never throw the boot */ }
|
|
179
198
|
};
|
|
180
199
|
const createClient =
|
|
181
200
|
opts.createClient ??
|
|
182
|
-
((spec: McpServerSpec) =>
|
|
183
|
-
spec.transport === 'http'
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
201
|
+
((spec: McpServerSpec) => {
|
|
202
|
+
if (spec.transport === 'http') return new McpClient({
|
|
203
|
+
id: spec.id, url: spec.url, logger: bootLogger(spec.id),
|
|
204
|
+
...(spec.oauthIssuer ? { oauthIssuer: spec.oauthIssuer } : {}),
|
|
205
|
+
...(spec.oauthTokenEndpoint ? { oauthTokenEndpoint: spec.oauthTokenEndpoint } : {}),
|
|
206
|
+
...(spec.bearerTokenEnv ? { bearerTokenEnv: spec.bearerTokenEnv } : {}),
|
|
207
|
+
});
|
|
208
|
+
const root = elanousStateRoot();
|
|
209
|
+
const installed = listInstalledPlugins(root);
|
|
210
|
+
const owners = [...new Set(listNodeKinds('workflow').filter(entry =>
|
|
211
|
+
!entry.core && entry.plugin && entry.run && 'mcp' in entry.run && entry.run.mcp.server === spec.id,
|
|
212
|
+
).map(entry => entry.plugin!))];
|
|
213
|
+
const commandOwners = [...new Set(installed.filter(item => {
|
|
214
|
+
const dir = realpathSync(item.path);
|
|
215
|
+
return spec.command.some(arg => {
|
|
216
|
+
if (typeof arg !== 'string' || !existsSync(arg)) return false;
|
|
217
|
+
try { return realpathSync(resolve(arg)).startsWith(`${dir}${sep}`); }
|
|
218
|
+
catch { return false; }
|
|
219
|
+
});
|
|
220
|
+
}).map(item => item.name))];
|
|
221
|
+
const selected = commandOwners.length === 1 && owners.length === 1 && owners[0] === commandOwners[0]
|
|
222
|
+
? commandOwners[0] : undefined;
|
|
223
|
+
return new McpClient({ id: spec.id, command: spec.command, logger: bootLogger(spec.id),
|
|
224
|
+
spawn: (cmd, args) => {
|
|
225
|
+
const declaredEnv = [...new Set(listInstalledPlugins(root).map(item => item.name))]
|
|
226
|
+
.flatMap(name => credentialStatus(name, root).fields.map(field => field.env));
|
|
227
|
+
const childEnv = { ...process.env };
|
|
228
|
+
for (const key of declaredEnv) delete childEnv[key];
|
|
229
|
+
injectedValuesByServer.delete(spec.id);
|
|
230
|
+
if (selected) {
|
|
231
|
+
const env = pluginEnv(selected, root);
|
|
232
|
+
const fields = credentialStatus(selected, root).fields.filter(field => field.set).map(field => field.name);
|
|
233
|
+
debug.log('plugin.credentials', 'injected', { plugin: selected, fields, count: fields.length });
|
|
234
|
+
Object.assign(childEnv, env);
|
|
235
|
+
injectedValuesByServer.set(spec.id, Object.values(env));
|
|
236
|
+
}
|
|
237
|
+
return nodeSpawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'], env: childEnv });
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
});
|
|
193
241
|
const handshakeTimeoutMs = opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
194
242
|
const setTimeoutFn = opts.setTimeoutFn
|
|
195
243
|
?? ((cb: () => void, ms: number) => {
|
|
@@ -204,6 +252,11 @@ export async function registerMcpClients(
|
|
|
204
252
|
const perServer: Record<string, McpServerBootResult> = {};
|
|
205
253
|
let registered = 0;
|
|
206
254
|
|
|
255
|
+
if (opts.servers.some(spec => spec.transport === 'stdio') && !opts.createClient) {
|
|
256
|
+
try { syncInstalledPluginNodes(elanousStateRoot()); }
|
|
257
|
+
catch (error) { debug.log('graph.kinds', 'installed-sync-failed', { reason: String(error) }); }
|
|
258
|
+
}
|
|
259
|
+
|
|
207
260
|
for (const spec of opts.servers) {
|
|
208
261
|
const serverHandshakeTimeoutMs = spec.handshakeTimeoutMs ?? handshakeTimeoutMs;
|
|
209
262
|
if (spec.enabled === false) {
|
|
@@ -213,6 +266,20 @@ export async function registerMcpClients(
|
|
|
213
266
|
let client: McpClient | undefined;
|
|
214
267
|
try {
|
|
215
268
|
client = createClient(spec) as McpClient;
|
|
269
|
+
if (!opts.createClient && spec.transport === 'stdio') {
|
|
270
|
+
const originalCallTool = client.callTool.bind(client);
|
|
271
|
+
client.callTool = async (name: string, args: Record<string, unknown>): Promise<McpToolCallResult> => {
|
|
272
|
+
const values = injectedValuesByServer.get(spec.id) ?? [];
|
|
273
|
+
try {
|
|
274
|
+
const result = await originalCallTool(name, args);
|
|
275
|
+
return maskInjectedValues(result, values) as McpToolCallResult;
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (err instanceof Error) err.message = redactValues(err.message, values);
|
|
278
|
+
else throw new Error(redactValues(String(err), values));
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
216
283
|
await withTimeout(`mcp.${spec.id}.start`, () => client!.start(), serverHandshakeTimeoutMs, setTimeoutFn, clearTimeoutFn);
|
|
217
284
|
const tools = await withTimeout(
|
|
218
285
|
`mcp.${spec.id}.listTools`,
|
|
@@ -287,7 +354,7 @@ export async function registerMcpClients(
|
|
|
287
354
|
}
|
|
288
355
|
const msg = err instanceof Error ? err.message : String(err);
|
|
289
356
|
const reasonClass = classifyMcpConnectFailure(err);
|
|
290
|
-
const detail = redactSecretText(msg);
|
|
357
|
+
const detail = redactPluginValues(redactSecretText(msg));
|
|
291
358
|
perServer[spec.id] = { status: 'failed', toolCount: 0, reason: detail, reasonClass };
|
|
292
359
|
try {
|
|
293
360
|
debug.log('mcp.client.boot', 'connect-failed', { id: spec.id, reason: reasonClass, detail });
|
|
@@ -303,6 +370,7 @@ export async function registerMcpClients(
|
|
|
303
370
|
if (client) {
|
|
304
371
|
try { await client.dispose(); } catch { /* best-effort */ }
|
|
305
372
|
}
|
|
373
|
+
injectedValuesByServer.delete(spec.id);
|
|
306
374
|
}
|
|
307
375
|
}
|
|
308
376
|
|
|
@@ -328,6 +396,7 @@ export async function registerMcpClients(
|
|
|
328
396
|
}),
|
|
329
397
|
),
|
|
330
398
|
);
|
|
399
|
+
injectedValuesByServer.clear();
|
|
331
400
|
},
|
|
332
401
|
};
|
|
333
402
|
}
|
|
@@ -338,3 +407,19 @@ function defaultLogger(): McpBootLogger {
|
|
|
338
407
|
warn: (line) => console.warn(line),
|
|
339
408
|
};
|
|
340
409
|
}
|
|
410
|
+
|
|
411
|
+
function redactValues(text: string, values: readonly string[]): string {
|
|
412
|
+
return values.filter(value => value.length > 0)
|
|
413
|
+
.reduce((safe, value) => safe.replaceAll(value, '[REDACTED]'), text);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Mask injected credential values anywhere in an MCP result — string values **and object keys**
|
|
417
|
+
* (a server can echo a secret back as a key, e.g. `structuredContent: { [secret]: "ok" }`). */
|
|
418
|
+
export function maskInjectedValues(value: unknown, values: readonly string[]): unknown {
|
|
419
|
+
if (typeof value === 'string') return redactValues(value, values);
|
|
420
|
+
if (Array.isArray(value)) return value.map(item => maskInjectedValues(item, values));
|
|
421
|
+
if (value && typeof value === 'object') {
|
|
422
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [redactValues(key, values), maskInjectedValues(item, values)]));
|
|
423
|
+
}
|
|
424
|
+
return value;
|
|
425
|
+
}
|
package/src/nexus/index.ts
CHANGED
|
@@ -79,6 +79,8 @@ import { type PtyBackend, createWebtermSpawn, resolveWebtermCwd } from './webter
|
|
|
79
79
|
import { SettingsTabController } from './config/settings-controller.js';
|
|
80
80
|
import { tryRegisterSettings } from './boot/register-settings.js';
|
|
81
81
|
import { registerMcpClients, type McpClientsHandle } from './boot/register-mcp-clients.js';
|
|
82
|
+
import { syncInstalledPluginNodes } from '../graph-kinds/installed-plugin-nodes.js';
|
|
83
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
82
84
|
import type { McpReloadOutcome, McpReloadServerResult } from './api/admin-mcp-reload.js';
|
|
83
85
|
import { loadOrCreateSigningKey, NonceStore } from './api/edit-in-pwa-core.js';
|
|
84
86
|
import { subscribeErrorSnapshotWriter } from './supervisor/error-snapshot.js';
|
|
@@ -238,9 +240,8 @@ import {
|
|
|
238
240
|
type PwaVoiceAdapter,
|
|
239
241
|
} from '../voice/channel-adapters/pwa-voice-adapter.js';
|
|
240
242
|
import { getIntakeStore } from '../intake-plane/runtime.js';
|
|
241
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
242
243
|
import { join as joinPath } from 'node:path';
|
|
243
|
-
import {
|
|
244
|
+
import { ensureAuthToken } from '../auth/acp-token.js';
|
|
244
245
|
import type { SupervisorSpawnBackend } from './supervisor/spawn.js';
|
|
245
246
|
import type { HealthProbeBackend } from './supervisor/health.js';
|
|
246
247
|
import { gracefulExit, cleanExit } from './supervisor/graceful-exit.js';
|
|
@@ -264,8 +265,10 @@ import { createGlobalSubagentCallable } from '../agent/subagent-callable.js';
|
|
|
264
265
|
import { setAgentHopCap } from '../agent/registry.js';
|
|
265
266
|
import type { SidebarTabSurface } from '../ui/widgets/sidebar-tab-surface.js';
|
|
266
267
|
import { defaultIO, runOnboardingStep, type WizardIO } from '../onboarding.js';
|
|
268
|
+
import { packageVersion } from '../version/code-revision.js';
|
|
267
269
|
|
|
268
|
-
|
|
270
|
+
// 데몬 배너·`nexus status`·연결 라벨이 보이는 판 = 제품 판(`package.json`) — 옛 내부 상수 `0.17.0` 이 0.2.x 제품 옆에 떴다(🅞 M5 · M5c).
|
|
271
|
+
export const NEXUS_VERSION = packageVersion();
|
|
269
272
|
export const NEXUS_PHASE = 'N-5 PR χ (graceful exit + restart pending)';
|
|
270
273
|
|
|
271
274
|
type TelegramAgentRunTurnFactory = typeof import('../telegram-agent.js').makeTelegramAgentRunTurn;
|
|
@@ -397,6 +400,8 @@ export interface RunNexusOptions {
|
|
|
397
400
|
* registry stays empty so the dispatcher's webterm interception
|
|
398
401
|
* is a no-op). */
|
|
399
402
|
webtermSpawn?: (spec: { id: string; cwd?: string }) => PtyBackend;
|
|
403
|
+
/** Explicitly disable HTTP and ACP websocket token auth for this run. */
|
|
404
|
+
noAuth?: boolean;
|
|
400
405
|
/** Skip the HTTP API server boot. Default: true in tests via
|
|
401
406
|
* detachForTesting; production runs always start the server. Tests
|
|
402
407
|
* that DO want the server can pass false explicitly. */
|
|
@@ -653,7 +658,7 @@ export interface RunNexusHandle {
|
|
|
653
658
|
/** ACP WS auth gate for the NEXUS HTTP bridge.
|
|
654
659
|
* Same shape as `src/boot/acp-server.ts` websocket auth: pass
|
|
655
660
|
* `createAuthVerifier` only when a bearer token is configured;
|
|
656
|
-
*
|
|
661
|
+
* explicit no-auth opt-out omits the verifier.
|
|
657
662
|
* ⛔ `noAuth` was declared on WsBridgeOpts but the bridge NEVER read it
|
|
658
663
|
* (0 executable references) while this file assigned it — a value that
|
|
659
664
|
* flowed in and died. We removed the field rather than leave a knob that
|
|
@@ -710,7 +715,7 @@ export function buildNexusWsBridgeAuth(
|
|
|
710
715
|
bearerToken: string | undefined,
|
|
711
716
|
paths?: TokenStorePaths,
|
|
712
717
|
): Pick<NexusWsBridgeInit, 'wsAuthVerifier'> {
|
|
713
|
-
//
|
|
718
|
+
// Explicit opt-out supplies no boot token, so the bridge has no verifier.
|
|
714
719
|
if (!bearerToken) return {};
|
|
715
720
|
return {
|
|
716
721
|
// ⭐ 배열이 아니라 «공급자» — 매 핸드셰이크마다 봉투를 다시 읽는다.
|
|
@@ -764,6 +769,13 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
764
769
|
}
|
|
765
770
|
|
|
766
771
|
const leasePortFromLauncher = checkLauncherLeaseBoot(opts, process.env, () => resolveCurrentInstance().kind);
|
|
772
|
+
const noAuth = opts.noAuth === true || process.env.ELANOUS_NEXUS_NO_AUTH === '1';
|
|
773
|
+
// Resolve before acquiring the daemon lock or binding HTTP. A broken token
|
|
774
|
+
// store must fail closed without leaving a half-booted Nexus behind.
|
|
775
|
+
const httpBootEnabled = !(opts.skipHttpServer ?? !!opts.detachForTesting);
|
|
776
|
+
const needsToken = httpBootEnabled && !(opts.skipRuntimeApi ?? !!opts.detachForTesting) && !noAuth;
|
|
777
|
+
const bearerToken = needsToken ? ensureAuthToken().token : undefined;
|
|
778
|
+
if (needsToken && !bearerToken) throw new Error('Nexus auth token is empty');
|
|
767
779
|
|
|
768
780
|
// 설치본 전환 RFC 0b — 키는 launchd plist 가 아니라 키 캐시에서(캐시 우선 · 자식도 물려받는다). 값은 안 찍고 «이름만».
|
|
769
781
|
if (!opts.detachForTesting) {
|
|
@@ -973,6 +985,11 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
973
985
|
});
|
|
974
986
|
const startLevel = resolvedLevel.level;
|
|
975
987
|
dbgMod.debug.setLevel(startLevel);
|
|
988
|
+
if (httpBootEnabled && noAuth) {
|
|
989
|
+
dbgMod.debug.log('nexus.auth', 'explicit-opt-out', {
|
|
990
|
+
source: opts.noAuth === true ? '--no-auth' : 'ELANOUS_NEXUS_NO_AUTH',
|
|
991
|
+
}, { level: 'warn' });
|
|
992
|
+
}
|
|
976
993
|
dbgMod.debug.log('logging.level', 'startup-resolved', {
|
|
977
994
|
level: startLevel, source: resolvedLevel.source,
|
|
978
995
|
gateOpen: scopedMod.hotPathGateOpen(startLevel), surface: 'nexus',
|
|
@@ -1340,6 +1357,10 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
1340
1357
|
current: () => false,
|
|
1341
1358
|
};
|
|
1342
1359
|
|
|
1360
|
+
// Installed graph kinds do not depend on the runtime API or MCP server configuration.
|
|
1361
|
+
try { syncInstalledPluginNodes(elanousStateRoot()); }
|
|
1362
|
+
catch (error) { debug.log('graph.kinds', 'installed-sync-failed', { reason: String(error) }); }
|
|
1363
|
+
|
|
1343
1364
|
if (!skipRuntime) {
|
|
1344
1365
|
// Tool surface — flag · env · UserConfig switch · switch default.
|
|
1345
1366
|
// See resolveToolsKind() jsdoc for resolution order.
|
|
@@ -2000,11 +2021,6 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
2000
2021
|
let metaApiOpts: import('./api/meta-api.js').MetaApiOpts | undefined;
|
|
2001
2022
|
|
|
2002
2023
|
if (!skipRuntime && runtimeHistory) {
|
|
2003
|
-
// Bearer token — read from getElanousConfigDir()/acp-token (the
|
|
2004
|
-
// canonical store `elanous serve` uses). Loopback default is
|
|
2005
|
-
// noAuth; tests opt in by writing a token file before boot.
|
|
2006
|
-
const bearerToken = readAcpToken();
|
|
2007
|
-
|
|
2008
2024
|
// Voice REST + adapter. createVoiceRestHandler is stateless;
|
|
2009
2025
|
// the STT provider singleton (getDaemonSttProvider) returns
|
|
2010
2026
|
// null until `initDaemonSttProvider()` is called — fine, the
|
|
@@ -2081,9 +2097,8 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
2081
2097
|
const surfaceForOpts = toolSurface(toolsKind);
|
|
2082
2098
|
const toolCwdForOpts = resolveToolCwd({ tools: toolsKind, toolCwd: opts.toolCwd });
|
|
2083
2099
|
|
|
2084
|
-
//
|
|
2085
|
-
//
|
|
2086
|
-
// entirely for first-boot dogfood (tokenless ACP upgrade).
|
|
2100
|
+
// ACP websocket and REST share the same boot token; explicit
|
|
2101
|
+
// opt-out omits the websocket verifier as well.
|
|
2087
2102
|
wsBridgeOpts = {
|
|
2088
2103
|
...(acpHandlerRef ? {
|
|
2089
2104
|
acpOnConnection: async (conn) => {
|
|
@@ -2099,7 +2114,7 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
2099
2114
|
metaApiOpts = {
|
|
2100
2115
|
voiceRest,
|
|
2101
2116
|
...(bearerToken ? { bearerToken } : {}),
|
|
2102
|
-
noAuth
|
|
2117
|
+
noAuth,
|
|
2103
2118
|
history: runtimeHistory,
|
|
2104
2119
|
intakeStore: getIntakeStore(),
|
|
2105
2120
|
toolSurface: surfaceForOpts,
|
|
@@ -3048,7 +3063,7 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
3048
3063
|
}
|
|
3049
3064
|
runtime.httpPort = httpServer.port;
|
|
3050
3065
|
runtime.httpHost = httpServer.hostname;
|
|
3051
|
-
runtime.httpAuth = 'off';
|
|
3066
|
+
runtime.httpAuth = metaApiOpts && !metaApiOpts.noAuth ? 'on' : 'off';
|
|
3052
3067
|
// The actual bound port (not the requested port or registry entry) is the member endpoint.
|
|
3053
3068
|
try {
|
|
3054
3069
|
const configured = (getUserConfig().raw?.nexus as { primary?: unknown } | undefined)?.primary;
|
|
@@ -3475,6 +3490,9 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
3475
3490
|
}
|
|
3476
3491
|
|
|
3477
3492
|
printBootBanner(runtime, pwaStaticDir);
|
|
3493
|
+
if (runtime.httpPort && noAuth) {
|
|
3494
|
+
console.warn(' ⚠ NEXUS AUTH DISABLED (--no-auth / ELANOUS_NEXUS_NO_AUTH=1) — HTTP API and ACP websocket are unprotected.');
|
|
3495
|
+
}
|
|
3478
3496
|
|
|
3479
3497
|
// P.3 — first-boot PWA share wizard. Only runs in interactive TUI mode
|
|
3480
3498
|
// when the switch is still 'ask'. Headless / non-TTY boots skip it (the
|
|
@@ -3784,23 +3802,6 @@ function shouldAskPwaShareSwitch(): boolean {
|
|
|
3784
3802
|
}
|
|
3785
3803
|
}
|
|
3786
3804
|
|
|
3787
|
-
/** PR k — load the persistent ACP bearer token at
|
|
3788
|
-
* `getElanousConfigDir()/acp-token`. Mirrors the helper in
|
|
3789
|
-
* `boot/acp-server.ts` that `elanous serve` uses, so the same token
|
|
3790
|
-
* gates daemon and NEXUS HTTP. Returns undefined when the token
|
|
3791
|
-
* file is absent (loopback noAuth default — first-boot dogfood
|
|
3792
|
-
* works without configuration). Read failures are swallowed. */
|
|
3793
|
-
export function readAcpToken(): string | undefined {
|
|
3794
|
-
try {
|
|
3795
|
-
const tokenPath = joinPath(getElanousConfigDir(), 'acp-token');
|
|
3796
|
-
if (existsSync(tokenPath)) {
|
|
3797
|
-
const tok = readFileSync(tokenPath, 'utf-8').trim();
|
|
3798
|
-
return tok || undefined;
|
|
3799
|
-
}
|
|
3800
|
-
} catch { /* swallow */ }
|
|
3801
|
-
return undefined;
|
|
3802
|
-
}
|
|
3803
|
-
|
|
3804
3805
|
function printBootBanner(runtime: NexusRuntimeMeta, pwaStaticDir?: string): void {
|
|
3805
3806
|
const lines = [
|
|
3806
3807
|
'',
|
|
@@ -141,7 +141,7 @@ export function resolveChannels(
|
|
|
141
141
|
async function deliverTelegram(cfg: UserConfig, msg: OutboundMsg, deps: RouterDeps): Promise<ChannelResult> {
|
|
142
142
|
const fmt = formatForChannel('telegram', msg);
|
|
143
143
|
const send = deps.telegramSend ?? sendTelegramReport;
|
|
144
|
-
const ok = await send(cfg, fmt.text, { markdown: fmt.markdown ?? msg.markdown, fetchImpl: deps.fetchImpl });
|
|
144
|
+
const ok = await send(cfg, fmt.text, { markdown: fmt.markdown ?? msg.markdown, fetchImpl: deps.fetchImpl, kind: msg.kind });
|
|
145
145
|
return ok ? { type: 'telegram', ok: true } : { type: 'telegram', ok: false, error: 'not-configured' };
|
|
146
146
|
}
|
|
147
147
|
|
package/src/onboarding.ts
CHANGED
|
@@ -1193,6 +1193,27 @@ export function shouldRefuseInteractiveOnboarding(
|
|
|
1193
1193
|
return o.ctx !== 'production' || !o.stdinIsTTY;
|
|
1194
1194
|
}
|
|
1195
1195
|
|
|
1196
|
+
export class OnboardingRefusedError extends Error {
|
|
1197
|
+
readonly code = 'onboarding-refused' as const;
|
|
1198
|
+
|
|
1199
|
+
constructor(message: string, readonly reason: 'non-tty' | 'autonomous-empty-universe') {
|
|
1200
|
+
super(message);
|
|
1201
|
+
this.name = 'OnboardingRefusedError';
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** Handle only typed refusals; the agent setup hint applies to its non-TTY path alone. */
|
|
1206
|
+
export function handleOnboardingRefusal(error: unknown, entrance: 'agent' | 'other'): boolean {
|
|
1207
|
+
if (!(error instanceof OnboardingRefusedError) || error.code !== 'onboarding-refused') return false;
|
|
1208
|
+
if (entrance === 'agent' && error.reason === 'non-tty') {
|
|
1209
|
+
console.error('elanous agent needs a configured LLM. Run `elanous setup` in a terminal, or `elanous setup --non-interactive --config <path>` for unattended setup.');
|
|
1210
|
+
} else {
|
|
1211
|
+
console.error(error.message);
|
|
1212
|
+
}
|
|
1213
|
+
process.exitCode = 2;
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1196
1217
|
function refuseInteractiveOnboardingIfNeeded(opts: RunWizardOpts, path: string): void {
|
|
1197
1218
|
const ctx = getRunContext();
|
|
1198
1219
|
const stdinIsTTY = !!input.isTTY;
|
|
@@ -1208,15 +1229,17 @@ function refuseInteractiveOnboardingIfNeeded(opts: RunWizardOpts, path: string):
|
|
|
1208
1229
|
: '자율 컨텍스트에서 대화형 온보딩 요청 — 우주가 비어 있다(물질화 누락)',
|
|
1209
1230
|
}, { level: 'error' });
|
|
1210
1231
|
if (nonTTY) {
|
|
1211
|
-
throw new
|
|
1232
|
+
throw new OnboardingRefusedError(
|
|
1212
1233
|
'대화형 온보딩은 stdin TTY가 있는 자리에서만 실행할 수 있다. '
|
|
1213
1234
|
+ '무인 설정은 `elanous setup --non-interactive --config <path>`를 사용하라.',
|
|
1235
|
+
'non-tty',
|
|
1214
1236
|
);
|
|
1215
1237
|
}
|
|
1216
|
-
throw new
|
|
1238
|
+
throw new OnboardingRefusedError(
|
|
1217
1239
|
`온보딩 마법사는 자율 컨텍스트(${ctx})에서 뜰 수 없다 — config 가 비어 있다(${path}). `
|
|
1218
1240
|
+ '자식 우주가 물질화되지 않았다는 뜻이다: `elanous config sync-test --state-dir <그 우주>` '
|
|
1219
1241
|
+ '로 깔거나, 스포너가 provisionDerivedUniverse 를 부르는지 확인하라.',
|
|
1242
|
+
'autonomous-empty-universe',
|
|
1220
1243
|
);
|
|
1221
1244
|
}
|
|
1222
1245
|
|
package/src/plugins/core/host.ts
CHANGED
|
@@ -233,6 +233,14 @@ export const BUILTIN_DIR = resolve(import.meta.dir, '..', '..', '..', 'plugins')
|
|
|
233
233
|
export const PACKS_DIR = resolve(import.meta.dir, '..', '..', '..', 'packs');
|
|
234
234
|
export const USER_DIR = join(homedir(), '.claude', 'plugins');
|
|
235
235
|
|
|
236
|
+
/** 코드 없이 스킬·그래프만 싣는 팩인가 — 진입점이 기본값(`./plugin.ts`)이고 파일이 없으며, 스킬·그래프를 선언하거나 `skills/` 폴더가 있다. */
|
|
237
|
+
export function isSkillOnlyPack(pluginDir: string, manifest: { main: string; contributes?: { skills?: unknown[]; graphs?: unknown[] } }, hasLegacyEntry: boolean): boolean {
|
|
238
|
+
if (hasLegacyEntry) return false;
|
|
239
|
+
if (manifest.main !== './plugin.ts') return false; // 명시한 진입점이 없으면 그건 결함이다 — 경고를 유지한다.
|
|
240
|
+
const declares = (manifest.contributes?.skills?.length ?? 0) > 0 || (manifest.contributes?.graphs?.length ?? 0) > 0;
|
|
241
|
+
return declares || existsSync(join(pluginDir, 'skills'));
|
|
242
|
+
}
|
|
243
|
+
|
|
236
244
|
export class PluginHost {
|
|
237
245
|
private available = new Map<string, PluginEntry>();
|
|
238
246
|
private discoveredNodeKinds = new Map<string, NodeKindEntry>();
|
|
@@ -614,6 +622,12 @@ export class PluginHost {
|
|
|
614
622
|
// ~/.claude/plugins/{cache,data,marketplaces} internal state)
|
|
615
623
|
// are silently skipped: they never intended to be plugins.
|
|
616
624
|
const looksLikePlugin = hasLegacyEntry || !manifestLoad.inferred;
|
|
625
|
+
// 스킬·그래프만 싣는 팩(공식 `elanous-basics` 등)은 코드 진입점이 없다 — 매니페스트가 `main` 을
|
|
626
|
+
// 적지 않아 기본값 `./plugin.ts` 가 들어온 것뿐이다. 부팅마다 «main not found» 경고를 내지 않는다.
|
|
627
|
+
if (looksLikePlugin && isSkillOnlyPack(pluginDir, manifestLoad.manifest, hasLegacyEntry)) {
|
|
628
|
+
debug.log('plugin.discovery', 'skill-only', { plugin: manifestLoad.manifest.id });
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
617
631
|
if (looksLikePlugin) {
|
|
618
632
|
this.hooks.log(`⚠ plugin "${manifestLoad.manifest.id}" main not found: ${manifestLoad.manifest.main}`);
|
|
619
633
|
debug.log('plugin.discovery', 'error', {
|
|
@@ -186,9 +186,10 @@ async function fetchMarketIndex(name: string, opts: MarketFetchOptions): Promise
|
|
|
186
186
|
const load = async (file: string): Promise<Uint8Array> => {
|
|
187
187
|
const url = new URL(file, market.url);
|
|
188
188
|
let response: Response;
|
|
189
|
-
try { response = await fetcher(url.href); }
|
|
190
|
-
catch
|
|
189
|
+
try { response = await fetcher(url.href, { redirect: 'error' }); }
|
|
190
|
+
catch { throw new MarketFetchError('io', `market fetch failed (${file}): network error`); }
|
|
191
191
|
if (!response.ok) throw new MarketFetchError('io', `market fetch failed (${file}): HTTP ${response.status}`);
|
|
192
|
+
if (response.redirected || (response.url && response.url !== url.href)) throw new MarketFetchError('io', `market fetch was redirected (${file})`);
|
|
192
193
|
if (!response.body) throw new MarketFetchError('io', `market fetch failed (${file}): missing response body`);
|
|
193
194
|
const reader = response.body.getReader();
|
|
194
195
|
const chunks: Uint8Array[] = [];
|
|
@@ -203,7 +204,7 @@ async function fetchMarketIndex(name: string, opts: MarketFetchOptions): Promise
|
|
|
203
204
|
}
|
|
204
205
|
} catch (error) {
|
|
205
206
|
if (error instanceof MarketFetchError) throw error;
|
|
206
|
-
throw new MarketFetchError('io', `market fetch failed (${file}):
|
|
207
|
+
throw new MarketFetchError('io', `market fetch failed (${file}): network error`);
|
|
207
208
|
} finally {
|
|
208
209
|
void reader.cancel().catch(() => {});
|
|
209
210
|
reader.releaseLock();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { closeSync, existsSync, fchmodSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
5
|
+
import { loadPluginManifestFromDir } from '../core/manifest.js';
|
|
6
|
+
import { listInstalledPlugins, withLedgerLock } from './plugin-install.js';
|
|
7
|
+
|
|
8
|
+
const PLUGIN_NAME = /^[a-z0-9][a-z0-9-]{1,39}$/;
|
|
9
|
+
const FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
10
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11
|
+
|
|
12
|
+
type FieldStatus = { name: string; env: string; set: boolean };
|
|
13
|
+
|
|
14
|
+
function credentialPath(name: string, root: string): string {
|
|
15
|
+
if (!PLUGIN_NAME.test(name)) throw new Error('invalid plugin name');
|
|
16
|
+
return join(root, 'plugins', 'credentials', `${name}.json`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function declaredFields(name: string, root: string): Array<{ name: string; env: string }> {
|
|
20
|
+
if (!PLUGIN_NAME.test(name)) throw new Error('invalid plugin name');
|
|
21
|
+
const installed = listInstalledPlugins(root).filter(plugin => plugin.name === name);
|
|
22
|
+
if (installed.length === 0) throw new Error(`plugin not installed: ${name}`);
|
|
23
|
+
const fields = new Map<string, string>();
|
|
24
|
+
for (const plugin of installed) {
|
|
25
|
+
const manifest = loadPluginManifestFromDir(plugin.path, { id: plugin.name }).manifest;
|
|
26
|
+
for (const connector of manifest.contributes.connectors ?? []) {
|
|
27
|
+
for (const field of [
|
|
28
|
+
...(connector.fields ?? []).map(item => ({ name: item.name, env: item.env })),
|
|
29
|
+
...(connector.userConfig ?? []).map(item => ({ name: item.key, env: item.env })),
|
|
30
|
+
]) {
|
|
31
|
+
if (!FIELD_NAME.test(field.name)) throw new Error('invalid plugin credential field name');
|
|
32
|
+
const env = field.env === undefined
|
|
33
|
+
? `ELANOUS_PLUGIN_${name.replaceAll('-', '_').toUpperCase()}_${field.name.replaceAll('-', '_').toUpperCase()}`
|
|
34
|
+
: field.env;
|
|
35
|
+
if (typeof env !== 'string' || !ENV_NAME.test(env)) throw new Error('invalid plugin credential environment name');
|
|
36
|
+
if (fields.has(field.name) && fields.get(field.name) !== env) throw new Error('conflicting plugin credential environment name');
|
|
37
|
+
fields.set(field.name, env);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (new Set(fields.values()).size !== fields.size) throw new Error('duplicate plugin credential environment name');
|
|
42
|
+
return [...fields].map(([fieldName, env]) => ({ name: fieldName, env })).sort((a, b) => a.name.localeCompare(b.name));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readCredentials(path: string): Record<string, string> {
|
|
46
|
+
if (!existsSync(path)) return Object.create(null) as Record<string, string>;
|
|
47
|
+
if (!lstatSync(path).isFile()) throw new Error('invalid plugin credential file');
|
|
48
|
+
let parsed: unknown;
|
|
49
|
+
try { parsed = JSON.parse(readFileSync(path, 'utf8')); }
|
|
50
|
+
catch { throw new Error('invalid plugin credential file'); }
|
|
51
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)
|
|
52
|
+
|| Object.entries(parsed).some(([key, value]) => !FIELD_NAME.test(key) || typeof value !== 'string')) {
|
|
53
|
+
throw new Error('invalid plugin credential file');
|
|
54
|
+
}
|
|
55
|
+
return Object.assign(Object.create(null) as Record<string, string>, parsed);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Update one installed plugin's declared fields. null removes a field; omitted fields remain intact. */
|
|
59
|
+
export function setPluginCredentials(name: string, fields: Record<string, string | null>, root = elanousStateRoot()): { set: string[] } {
|
|
60
|
+
const path = credentialPath(name, root);
|
|
61
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields)) throw new Error('invalid plugin credential fields');
|
|
62
|
+
const allowed = new Set(declaredFields(name, root).map(field => field.name));
|
|
63
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
64
|
+
if (!allowed.has(key) || (typeof value !== 'string' && value !== null) || (typeof value === 'string' && value.includes('\0'))) {
|
|
65
|
+
throw new Error('invalid plugin credential field');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return withLedgerLock(root, () => {
|
|
69
|
+
const current = readCredentials(path);
|
|
70
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
71
|
+
if (value === null) delete current[key];
|
|
72
|
+
else current[key] = value;
|
|
73
|
+
}
|
|
74
|
+
if (Object.keys(fields).length === 0) return { set: [] };
|
|
75
|
+
mkdirSync(join(root, 'plugins', 'credentials'), { recursive: true, mode: 0o700 });
|
|
76
|
+
const temp = `${path}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
77
|
+
try {
|
|
78
|
+
const fd = openSync(temp, 'wx', 0o600);
|
|
79
|
+
try {
|
|
80
|
+
fchmodSync(fd, 0o600);
|
|
81
|
+
writeFileSync(fd, JSON.stringify(current));
|
|
82
|
+
fsyncSync(fd);
|
|
83
|
+
} finally { closeSync(fd); }
|
|
84
|
+
renameSync(temp, path);
|
|
85
|
+
} finally { rmSync(temp, { force: true }); }
|
|
86
|
+
return { set: Object.keys(fields).filter(key => Object.hasOwn(current, key)).sort() };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Public metadata only: values never leave the credential store. */
|
|
91
|
+
export function credentialStatus(name: string, root = elanousStateRoot()): { fields: FieldStatus[] } {
|
|
92
|
+
const declared = declaredFields(name, root);
|
|
93
|
+
const saved = readCredentials(credentialPath(name, root));
|
|
94
|
+
return { fields: declared.map(field => ({ ...field, set: Object.hasOwn(saved, field.name) })) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Environment entries intended only for this plugin's child process. */
|
|
98
|
+
export function pluginEnv(name: string, root = elanousStateRoot()): Record<string, string> {
|
|
99
|
+
const declared = declaredFields(name, root);
|
|
100
|
+
const saved = readCredentials(credentialPath(name, root));
|
|
101
|
+
const env = Object.create(null) as Record<string, string>;
|
|
102
|
+
for (const field of declared) {
|
|
103
|
+
if (Object.hasOwn(saved, field.name)) env[field.env] = saved[field.name]!;
|
|
104
|
+
}
|
|
105
|
+
return env;
|
|
106
|
+
}
|