elanous 0.2.4 → 0.2.6
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 +7 -6
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 4240-7153881b0b377306.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7633.0654fe6ea259c1ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 91ff6dda-3b33d42d20657429.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +1 -0
- package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
- package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_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 +11 -1
- package/graphs/release/release-loop.yaml +9 -2
- package/graphs/video/README.md +1 -0
- package/graphs/video/explainer-line.yaml +58 -0
- package/package.json +3 -1
- package/plugins/card-followup/README.md +33 -0
- package/plugins/card-followup/examples/card-sample.json +7 -0
- package/plugins/card-followup/graphs/card-followup.yaml +16 -0
- package/plugins/card-followup/graphs/recipes.yaml +12 -0
- package/plugins/card-followup/graphs/run-step.ts +172 -0
- package/plugins/card-followup/plugin.json +25 -0
- package/plugins/geo-check/README.md +13 -0
- package/plugins/geo-check/examples/brand-sample.json +8 -0
- package/plugins/geo-check/graphs/geo-check.yaml +20 -0
- package/plugins/geo-check/graphs/recipes.yaml +18 -0
- package/plugins/geo-check/graphs/run-step.ts +177 -0
- package/plugins/geo-check/plugin.json +24 -0
- package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
- package/plugins/job-coach/README.md +42 -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/daemon-port-baseline.txt +1 -2
- 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/android-test-device.ts +32 -0
- 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/checklist-gate-node.ts +35 -0
- package/scripts/release-loop/gate-diff.ts +29 -0
- package/scripts/release-loop/gate-node.ts +366 -45
- package/scripts/release-loop/publish-node.ts +7 -3
- package/scripts/release-loop/release-docs.ts +20 -13
- package/scripts/release-loop/shard-plan.ts +47 -0
- 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 +53 -0
- package/skills/explainer-video/SKILL.md +117 -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/reel.mjs +128 -0
- package/skills/explainer-video/engine/reel.sh +8 -0
- package/skills/explainer-video/engine/vo.ts +79 -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/mission-origin.ts +20 -4
- package/src/autopilot/mission-phase-track.ts +11 -1
- package/src/autopilot/pr-manager.ts +51 -4
- package/src/autopilot/track-agent.ts +3 -3
- package/src/autopilot/track-courier.ts +2 -2
- 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/nexus-ios-bind.ts +25 -8
- package/src/cli/phone-link.ts +117 -0
- package/src/cli/plugin-cli.ts +136 -11
- package/src/cli/pr-lineage.ts +2 -1
- package/src/cli/private-ledger-commands.ts +31 -0
- package/src/cli/pwa-start.ts +3 -0
- package/src/cli/release-cli.ts +97 -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 +13 -7
- 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/field/field-media.ts +245 -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 +50 -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/field-uploads.ts +143 -0
- package/src/nexus/api/graph-approvals.ts +174 -0
- package/src/nexus/api/harness-api.ts +219 -5
- package/src/nexus/api/http-server.ts +69 -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/notify/llm-budget-alert.ts +119 -0
- 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/plugins/maker/plugin-maker.ts +208 -0
- package/src/prompt-library/universal-preamble.ts +8 -2
- package/src/relay/channel.ts +184 -0
- package/src/relay/mux.ts +182 -0
- package/src/relay/test-vectors.json +29 -0
- package/src/release-loop/checklist.ts +217 -0
- package/src/release-loop/release-note.ts +6 -3
- package/src/seat-address/seat-address.ts +34 -0
- 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/run-ledger-compact.ts +1 -1
- package/src/self-implement/seams.ts +4 -2
- package/src/session/index.ts +23 -10
- 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-bun-cache.ts +18 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -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/telegram.ts +108 -1
- package/src/tui-client/acp-transport-ws-client.ts +72 -5
- package/src/tui.ts +3 -2
- package/src/user-config.ts +32 -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/video-pipeline/recipes/explainer.ts +213 -0
- package/src/video-pipeline/walk-line.ts +3 -2
- 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/2415.ed6428ff334c7ce2.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/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/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/botlab/page-9fa879ebf5959096.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/control/page-4472ddf1e2458e69.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/morning/page-112f7e477bda77fd.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 → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import type { TelegramEvent } from '../../workflow-runtime/triggers/telegram-source.js';
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { compareTokenConstTime } from '../../acp/transport/auth.js';
|
|
21
|
+
import { ensureAuthToken } from '../../auth/acp-token.js';
|
|
20
22
|
import type { NexusState } from '../state/state.js';
|
|
21
23
|
import { DEFAULT_NEXUS_HTTP_PORT } from '../default-port.js';
|
|
22
24
|
export { DEFAULT_NEXUS_HTTP_PORT } from '../default-port.js';
|
|
@@ -195,6 +197,8 @@ import { handleMcpResourceGet, MCP_RESOURCE_ROUTE_PATH } from './mcp-resource-ro
|
|
|
195
197
|
// ⛔ 라우트 상수를 «디스패처»가 안 쓰고 문자열로 베끼고 있었다(16차 실측) — 잎에서 읽는다.
|
|
196
198
|
import { APPROVALS_MERGES_PATH, DESIGN_DIRECTION_PATH, DESIGN_PREVIEW_PATH_PREFIX, DESIGN_PREVIEWS_PATH, DESIGN_SYSTEM_PATH, IPA_PATH_PREFIX, MANIFEST_PATH } from './rest-route-paths.js';
|
|
197
199
|
import { handleMergeApprovals } from './merge-approvals.js';
|
|
200
|
+
import { GRAPH_APPROVALS_PATH, handleGraphApprovals } from './graph-approvals.js';
|
|
201
|
+
import { FIELD_SERVER_MAX_BODY_BYTES, FIELD_UPLOADS_PATH, handleFieldUploads } from './field-uploads.js';
|
|
198
202
|
import { handleLiveDetail, LIVE_DETAIL_PATH } from './live-detail.js';
|
|
199
203
|
import { handleLiveShipped, LIVE_SHIPPED_PATH } from './live-shipped.js';
|
|
200
204
|
import { handleMcpWidgetCall, MCP_WIDGET_CALL_ROUTE_PATH, persistWidgetTurnToSessionStore } from './mcp-widget-call-route.js';
|
|
@@ -300,6 +304,7 @@ import {
|
|
|
300
304
|
handleVoiceCost,
|
|
301
305
|
handleVoiceTranscribe,
|
|
302
306
|
checkAuth,
|
|
307
|
+
bearerCredential,
|
|
303
308
|
registerAuthPeerAddress,
|
|
304
309
|
type MetaApiOpts,
|
|
305
310
|
} from './meta-api.js';
|
|
@@ -332,7 +337,10 @@ import { handleTriggersSnapshot } from './triggers.js';
|
|
|
332
337
|
import { handleWorkflowTemplatesList } from './workflow-templates.js';
|
|
333
338
|
import { handleGraphsGet, handleGraphsMutation } from './graphs-api.js';
|
|
334
339
|
import { handleGraphKindsGet, handleGraphsValidatePost } from './graph-kinds.js';
|
|
335
|
-
import {
|
|
340
|
+
import { syncInstalledPluginNodes } from '../../graph-kinds/installed-plugin-nodes.js';
|
|
341
|
+
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
342
|
+
import { handlePluginsIndexGet, handlePluginsGet, handlePluginsMarketRefresh, handlePluginsInstall, handlePluginsRemove } from './plugins-market.js';
|
|
343
|
+
import { handlePluginsCredentialsGet, handlePluginsCredentialsPut } from './plugins-credentials-api.js';
|
|
336
344
|
import {
|
|
337
345
|
handleWorkflowsList,
|
|
338
346
|
} from './workflows.js';
|
|
@@ -369,7 +377,7 @@ import {
|
|
|
369
377
|
parseAgentCliSessionPath,
|
|
370
378
|
} from './agent-cli.js';
|
|
371
379
|
import { handleSessionTurnControl, SESSION_TURN_CONTROL_PATH } from '../../session/session-turn-control.js';
|
|
372
|
-
import { authenticatePodCredential, handlePodGrokCredential, POD_CREDENTIAL_GROK_PATH } from './pod-credential-api.js';
|
|
380
|
+
import { authenticatePodCredential, handlePodGrokCredential, handlePodGithubCredential, POD_CREDENTIAL_GROK_PATH, POD_CREDENTIAL_GITHUB_PATH } from './pod-credential-api.js';
|
|
373
381
|
import { handleOutboundReport } from './outbound-report.js';
|
|
374
382
|
import { handleSelfEvent } from './self-event.js';
|
|
375
383
|
/** 매매 «실행»은 애드온이다(대표 09-20 결정 ③ · 별도 상용 저장소 · release/trading-export.yaml) — 공개 코어엔 없다.
|
|
@@ -436,6 +444,8 @@ export function probeNexusHttpPort(
|
|
|
436
444
|
export type NexusWsBridgeInit = Omit<WsBridgeOpts, 'hostname' | 'port'>;
|
|
437
445
|
|
|
438
446
|
export interface NexusHttpServerOpts {
|
|
447
|
+
/** Optional installer state root for isolated API consumers and tests. */
|
|
448
|
+
pluginStateRoot?: string;
|
|
439
449
|
state: NexusState;
|
|
440
450
|
registry: TabRegistry;
|
|
441
451
|
eventBus: NexusEventBus;
|
|
@@ -730,6 +740,8 @@ export function startNexusHttpServer(opts: NexusHttpServerOpts): NexusHttpServer
|
|
|
730
740
|
port,
|
|
731
741
|
hostname,
|
|
732
742
|
idleTimeout: 255,
|
|
743
|
+
// Bun 기본 128 MB 로는 현장 업로드(파일 200 MB · 요청 500 MB · JSON base64)가 못 들어온다.
|
|
744
|
+
maxRequestBodySize: FIELD_SERVER_MAX_BODY_BYTES,
|
|
733
745
|
fetch: async (req: Request, srv: unknown) =>
|
|
734
746
|
(await routeRequest(req, opts, srv as BunServerLike, bridge, devProxyRef, { hostname, port })) ??
|
|
735
747
|
new Response(null, { status: 101 }),
|
|
@@ -822,19 +834,27 @@ export async function routeRequest(
|
|
|
822
834
|
// Per-route checkAuth calls below stay in place.
|
|
823
835
|
// OPTIONS is a CORS preflight and cannot carry Authorization; existing
|
|
824
836
|
// handlers answer it themselves, so the gate does not swallow it.
|
|
837
|
+
// A runtime-less tab mutation still needs authentication before the
|
|
838
|
+
// dispatcher can return its 503. Do not enable this fallback for other paths.
|
|
839
|
+
const runtimeLessTabMutation = !opts.metaApi && method !== 'GET' && method !== 'OPTIONS'
|
|
840
|
+
&& (pathname === '/v1/nexus/tabs' || pathname.startsWith('/v1/nexus/tabs/'));
|
|
825
841
|
if (
|
|
826
842
|
method !== 'OPTIONS'
|
|
827
843
|
&& pathname.startsWith('/v1/')
|
|
828
844
|
&& !isPublicRoute(method, pathname, { setupMode: false })
|
|
829
845
|
) {
|
|
830
|
-
|
|
846
|
+
const fallbackToken = runtimeLessTabMutation ? ensureAuthToken().token : undefined;
|
|
847
|
+
const offered = req.headers.get('authorization');
|
|
848
|
+
const fallbackAuthorized = fallbackToken !== undefined && offered?.startsWith('Bearer ') === true
|
|
849
|
+
&& compareTokenConstTime(offered.slice('Bearer '.length).trim(), fallbackToken);
|
|
850
|
+
if (!(opts.metaApi ? checkAuth(req, opts.metaApi) : fallbackAuthorized)) {
|
|
831
851
|
const name = method === 'POST' && pathname === '/v1/tasks' && opts.metaApi
|
|
832
852
|
? matchIngestAuthorization(req)?.name
|
|
833
853
|
: undefined;
|
|
834
854
|
if (name === undefined) {
|
|
835
855
|
debug.log('nexus.auth', 'default-deny', { method, pathname });
|
|
836
856
|
// MCP 클라이언트(Claude Code)에게는 «할 일»을 말한다 — 안 그러면 OAuth 로 가서 /register 405 를 만난다.
|
|
837
|
-
if (pathname === '/v1/mcp') return mcpUnauthorizedResponse();
|
|
857
|
+
if (pathname === '/v1/mcp') return mcpUnauthorizedResponse(new URL(req.url).origin);
|
|
838
858
|
return jsonResponse({ error: 'unauthorized' }, 401);
|
|
839
859
|
}
|
|
840
860
|
debug.log('nexus.auth', 'ingest-token', { name, pathname });
|
|
@@ -843,7 +863,7 @@ export async function routeRequest(
|
|
|
843
863
|
// OAuth 동적 클라이언트 등록(RFC 7591) — Elanous 는 OAuth 를 쓰지 않는다. 405 대신 «이렇게 등록하라»를 돌려준다.
|
|
844
864
|
if (method === 'POST' && pathname === '/register') {
|
|
845
865
|
debug.log('nexus.auth', 'oauth-register-rejected', { pathname });
|
|
846
|
-
return oauthRegistrationRejected();
|
|
866
|
+
return oauthRegistrationRejected(new URL(req.url).origin);
|
|
847
867
|
}
|
|
848
868
|
|
|
849
869
|
// FU2 (2026-05-12) — root-level browser conveniences. The PWA serves
|
|
@@ -899,6 +919,18 @@ export async function routeRequest(
|
|
|
899
919
|
});
|
|
900
920
|
}
|
|
901
921
|
|
|
922
|
+
if (pathname === GRAPH_APPROVALS_PATH || pathname.startsWith(`${GRAPH_APPROVALS_PATH}/`)) {
|
|
923
|
+
return handleGraphApprovals(req, {
|
|
924
|
+
authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
925
|
+
authReason: (request) => (opts.metaApi ? bearerCredential(request, opts.metaApi) : undefined),
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
// 현장 업로드(owner 전용) — GET 목록 · POST 저장. 저장 규칙 정본 = src/field/field-media.ts.
|
|
929
|
+
if (pathname === FIELD_UPLOADS_PATH && (method === 'GET' || method === 'POST')) {
|
|
930
|
+
return handleFieldUploads(req, {
|
|
931
|
+
authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
932
|
+
});
|
|
933
|
+
}
|
|
902
934
|
if (pathname === APPROVALS_MERGES_PATH || pathname.startsWith(`${APPROVALS_MERGES_PATH}/`)) {
|
|
903
935
|
return handleMergeApprovals(req, {
|
|
904
936
|
authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
@@ -1189,6 +1221,25 @@ export async function routeRequest(
|
|
|
1189
1221
|
// Templates POST handled here too (PR κ).
|
|
1190
1222
|
// Config / secrets PUT/POST/DELETE here too (PR μ).
|
|
1191
1223
|
if (method !== 'GET') {
|
|
1224
|
+
if (method === 'POST' && pathname === '/v1/plugins/install') {
|
|
1225
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1226
|
+
return handlePluginsInstall(req, opts.metaApi, opts.pluginStateRoot);
|
|
1227
|
+
}
|
|
1228
|
+
const marketRefresh = /^\/v1\/plugins\/markets\/([^/]+)\/refresh$/.exec(pathname);
|
|
1229
|
+
if (method === 'POST' && marketRefresh) {
|
|
1230
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1231
|
+
return handlePluginsMarketRefresh(req, marketRefresh[1]!, opts.metaApi, { root: opts.pluginStateRoot });
|
|
1232
|
+
}
|
|
1233
|
+
const pluginCredentials = /^\/v1\/plugins\/([^/]+)\/credentials$/.exec(pathname);
|
|
1234
|
+
if (method === 'PUT' && pluginCredentials) {
|
|
1235
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1236
|
+
return handlePluginsCredentialsPut(req, pluginCredentials[1]!, opts.metaApi, opts.pluginStateRoot, opts.reloadMcpClients);
|
|
1237
|
+
}
|
|
1238
|
+
const pluginRemove = /^\/v1\/plugins\/([^/]+)$/.exec(pathname);
|
|
1239
|
+
if (method === 'DELETE' && pluginRemove) {
|
|
1240
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1241
|
+
return handlePluginsRemove(req, pluginRemove[1]!, opts.metaApi, opts.pluginStateRoot);
|
|
1242
|
+
}
|
|
1192
1243
|
if ((method === 'PUT' || method === 'POST') && pathname.startsWith('/v1/graphs/')) {
|
|
1193
1244
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1194
1245
|
if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
@@ -1215,6 +1266,11 @@ export async function routeRequest(
|
|
|
1215
1266
|
if (!gate.ok) return gate.response;
|
|
1216
1267
|
return handlePodGrokCredential(req);
|
|
1217
1268
|
}
|
|
1269
|
+
if (pathname === POD_CREDENTIAL_GITHUB_PATH && method === 'POST') {
|
|
1270
|
+
const gate = await authenticatePodCredential(req, {}, 'gh-credential');
|
|
1271
|
+
if (!gate.ok) return gate.response;
|
|
1272
|
+
return handlePodGithubCredential(req);
|
|
1273
|
+
}
|
|
1218
1274
|
if (pathname === '/v1/harness/ask' && method === 'POST') {
|
|
1219
1275
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-runtime-not-wired' }, 503);
|
|
1220
1276
|
if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
@@ -2431,12 +2487,19 @@ export async function routeRequest(
|
|
|
2431
2487
|
// surface (`/v1/workflows` · `~/.elanous/workflows-runs/`) covers the same
|
|
2432
2488
|
// user need.
|
|
2433
2489
|
|
|
2490
|
+
const pluginCredentialsGet = /^\/v1\/plugins\/([^/]+)\/credentials$/.exec(pathname);
|
|
2491
|
+
if (method === 'GET' && pluginCredentialsGet) {
|
|
2492
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
2493
|
+
return handlePluginsCredentialsGet(req, pluginCredentialsGet[1]!, opts.metaApi, opts.pluginStateRoot);
|
|
2494
|
+
}
|
|
2434
2495
|
if (method === 'GET' && (pathname === '/v1/plugins/index' || pathname === '/v1/plugins')) {
|
|
2435
2496
|
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
2436
|
-
return pathname === '/v1/plugins/index' ? handlePluginsIndexGet(req, opts.metaApi) : handlePluginsGet(req, opts.metaApi);
|
|
2497
|
+
return pathname === '/v1/plugins/index' ? handlePluginsIndexGet(req, opts.metaApi, opts.pluginStateRoot) : handlePluginsGet(req, opts.metaApi, opts.pluginStateRoot);
|
|
2437
2498
|
}
|
|
2438
2499
|
if (method === 'GET' && pathname === '/v1/graph/kinds') {
|
|
2439
2500
|
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
2501
|
+
try { syncInstalledPluginNodes(opts.pluginStateRoot ?? elanousStateRoot()); }
|
|
2502
|
+
catch (error) { debug.log('graph.kinds', 'installed-sync-failed', { reason: String(error) }); }
|
|
2440
2503
|
return handleGraphKindsGet(req, opts.metaApi);
|
|
2441
2504
|
}
|
|
2442
2505
|
// F-M1 read + P-F1 raw YAML. Writes live in the mutation block and never touch core graphs/.
|
|
@@ -301,6 +301,15 @@ export function activeStoreNames(deps: LogFabricDeps, nowMs = Date.now()): strin
|
|
|
301
301
|
return names;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
/** 등록된 로그 저장소 수(이 인스턴스 ⊕ logs.db 가 있는 우주) — `@active` 가 연 수의 «분모». 못 세면 `null`(0 으로 접지 않는다). */
|
|
305
|
+
export function registeredLogStoreCount(deps: Pick<LogFabricDeps, 'instances'> = {}): number | null {
|
|
306
|
+
try {
|
|
307
|
+
const names = new Set((deps.instances ?? readLogInstances)().filter((view) => view.dbExists).map((view) => view.name));
|
|
308
|
+
names.add(resolveLogInstanceName());
|
|
309
|
+
return names.size;
|
|
310
|
+
} catch { return null; }
|
|
311
|
+
}
|
|
312
|
+
|
|
304
313
|
export function handleLogsQuery(req: Request, opts: MetaApiOpts, deps: LogFabricDeps = {}): Response {
|
|
305
314
|
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
306
315
|
const url = new URL(req.url);
|
|
@@ -374,7 +383,7 @@ export function handleLogsQuery(req: Request, opts: MetaApiOpts, deps: LogFabric
|
|
|
374
383
|
ts: new Date().toISOString(),
|
|
375
384
|
...(failedStores.length ? { failedStores } : {}),
|
|
376
385
|
...(selection.storeLimitReached ? { storeLimitReached: true } : {}),
|
|
377
|
-
...(activeStores ? { stores: activeStores } : {}),
|
|
386
|
+
...(activeStores ? { stores: activeStores, registeredStores: registeredLogStoreCount(deps) } : {}),
|
|
378
387
|
}, 200);
|
|
379
388
|
}
|
|
380
389
|
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
// 사람은 무엇을 해야 할지 몰랐다(🅢 09-28 · 대표 승인 수리). Elanous 는 OAuth 를 쓰지 않는다 — 고정 Bearer 토큰이다.
|
|
4
4
|
// ⇒ 두 자리에서 «할 일»을 말한다: ① 401 의 `WWW-Authenticate` ② `/register` 의 RFC 7591 오류 본문(Claude Code 가 그대로 보여 준다).
|
|
5
5
|
|
|
6
|
-
/** 토큰 «값»은 절대 싣지 않는다 — 파일 경로와 명령만. */
|
|
7
|
-
export
|
|
8
|
-
'Elanous 는 OAuth 를 쓰지 않는다(고정 Bearer 토큰). 권장: stdio 로 등록 — `claude mcp add elanous -- elanous mcp serve`. '
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
/** 토큰 «값»은 절대 싣지 않는다 — 파일 경로와 명령만. 주소는 포트를 짐작하지 않고 «요청이 닿은 origin»을 쓴다(대표 09-28 통합관제). */
|
|
7
|
+
export function mcpAuthHint(origin: string): string {
|
|
8
|
+
return 'Elanous 는 OAuth 를 쓰지 않는다(고정 Bearer 토큰). 권장: stdio 로 등록 — `claude mcp add elanous -- elanous mcp serve`. '
|
|
9
|
+
+ 'HTTP 가 필요하면 ~/.elanous/acp-token 의 값을 Authorization 헤더로: '
|
|
10
|
+
+ `\`claude mcp add --transport http elanous ${origin}/v1/mcp --header "Authorization: Bearer <acp-token>"\``;
|
|
11
|
+
}
|
|
11
12
|
|
|
12
13
|
/** `/v1/mcp` 401 — 무엇을 해야 하는지 헤더로도 말한다(WWW-Authenticate 의 error_description 은 ASCII 만). */
|
|
13
|
-
export function mcpUnauthorizedResponse(): Response {
|
|
14
|
-
return new Response(JSON.stringify({ error: 'unauthorized', hint:
|
|
14
|
+
export function mcpUnauthorizedResponse(origin: string): Response {
|
|
15
|
+
return new Response(JSON.stringify({ error: 'unauthorized', hint: mcpAuthHint(origin) }), {
|
|
15
16
|
status: 401,
|
|
16
17
|
headers: {
|
|
17
18
|
'content-type': 'application/json; charset=utf-8',
|
|
@@ -22,8 +23,8 @@ export function mcpUnauthorizedResponse(): Response {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/** OAuth 동적 클라이언트 등록 거절 — RFC 7591 §3.2.2 오류 형식(Claude Code 가 본문을 사람에게 보여 준다). */
|
|
25
|
-
export function oauthRegistrationRejected(): Response {
|
|
26
|
-
return new Response(JSON.stringify({ error: 'invalid_client_metadata', error_description:
|
|
26
|
+
export function oauthRegistrationRejected(origin: string): Response {
|
|
27
|
+
return new Response(JSON.stringify({ error: 'invalid_client_metadata', error_description: mcpAuthHint(origin) }), {
|
|
27
28
|
status: 400,
|
|
28
29
|
headers: { 'content-type': 'application/json; charset=utf-8', 'access-control-allow-origin': '*' },
|
|
29
30
|
});
|
|
@@ -98,13 +98,14 @@ function asItem(pr: RawPr) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// A page load fires the list twice (the daemon client settles) and polls cards — identical reads share one gh call.
|
|
101
|
+
// Failures are shared too: during a GitHub rate-limit window a retry per request only deepens the limit (GIT-S83).
|
|
101
102
|
const readCache = new Map<string, { at: number; value: Promise<GhResult> }>();
|
|
102
103
|
export function clearApprovalsReadCache(): void { readCache.clear(); }
|
|
103
104
|
function cachedRead(args: string[], ttlMs: number, now: number, call: () => Promise<GhResult>): Promise<GhResult> {
|
|
104
105
|
const key = args.join('\u0000');
|
|
105
106
|
const hit = readCache.get(key);
|
|
106
107
|
if (hit && now - hit.at < ttlMs) return hit.value;
|
|
107
|
-
const value = call()
|
|
108
|
+
const value = call();
|
|
108
109
|
readCache.set(key, { at: now, value });
|
|
109
110
|
return value;
|
|
110
111
|
}
|
|
@@ -254,6 +254,10 @@ function decideAuth(req: Request, opts: MetaApiOpts): { ok: boolean; reason: Aut
|
|
|
254
254
|
if (isSameOriginRequest(req, peerAddress)) return { ok: true, reason: 'same-origin' };
|
|
255
255
|
const peerRejected = req.headers.get('sec-fetch-site') === 'same-origin'
|
|
256
256
|
&& !isTrustedSameOriginPeer(peerAddress);
|
|
257
|
+
return decideBearer(req, opts, peerRejected);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function decideBearer(req: Request, opts: MetaApiOpts, peerRejected = false): { ok: boolean; reason: AuthReason } {
|
|
257
261
|
const token = opts.bearerToken;
|
|
258
262
|
if (!token) return { ok: false, reason: peerRejected ? 'untrusted-same-origin-peer' : 'no-bearer-configured' };
|
|
259
263
|
const auth = req.headers.get('authorization') ?? '';
|
|
@@ -273,6 +277,16 @@ function decideAuth(req: Request, opts: MetaApiOpts): { ok: boolean; reason: Aut
|
|
|
273
277
|
: { ok: false, reason: 'bearer-mismatch' };
|
|
274
278
|
}
|
|
275
279
|
|
|
280
|
+
/** Bearer-only credential check that ignores the same-origin shortcut. `checkAuth` returns
|
|
281
|
+
* `same-origin` before it ever looks at the Authorization header, so a paired PWA that sends its
|
|
282
|
+
* token is still reported as same-origin — write paths that must not trust same-origin alone
|
|
283
|
+
* (graph approvals) ask this instead. Returns the credential kind, or undefined if none matched. */
|
|
284
|
+
export function bearerCredential(req: Request, opts: MetaApiOpts): 'noauth' | 'bearer-match' | 'temp-token' | undefined {
|
|
285
|
+
if (opts.noAuth) return 'noauth';
|
|
286
|
+
const decision = decideBearer(req, opts);
|
|
287
|
+
return decision.ok && (decision.reason === 'bearer-match' || decision.reason === 'temp-token') ? decision.reason : undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
276
290
|
// Exported so sibling NEXUS API modules (e.g. T5.G `tasks-scheduler.ts`)
|
|
277
291
|
// can reuse the exact same gate — same-origin + constant-time bearer
|
|
278
292
|
// check — without duplicating security-sensitive code.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { debug } from '../../debug/log.js';
|
|
2
|
+
import { credentialStatus, setPluginCredentials } from '../../plugins/install/plugin-credentials.js';
|
|
3
|
+
import { checkAuth, type MetaApiOpts } from './meta-api.js';
|
|
4
|
+
import { jsonResponse } from './json-response.js';
|
|
5
|
+
|
|
6
|
+
const NAME = /^[a-z0-9][a-z0-9-]{1,39}$/;
|
|
7
|
+
|
|
8
|
+
function failure(error: unknown): Response {
|
|
9
|
+
if (error instanceof Error && error.message.startsWith('plugin not installed:')) return jsonResponse({ error: 'not_found' }, 404);
|
|
10
|
+
if (error instanceof Error && (error.message === 'invalid plugin name' || error.message === 'invalid plugin credential field'
|
|
11
|
+
|| error.message === 'invalid plugin credential fields')) {
|
|
12
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
13
|
+
}
|
|
14
|
+
return jsonResponse({ error: 'credential_error' }, 500);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function handlePluginsCredentialsGet(req: Request, name: string, opts: MetaApiOpts, root?: string): Response {
|
|
18
|
+
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
19
|
+
if (!NAME.test(name)) return jsonResponse({ error: 'bad_request' }, 400);
|
|
20
|
+
try { return jsonResponse(credentialStatus(name, root), 200); }
|
|
21
|
+
catch (error) { return failure(error); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `reload` re-spawns the daemon's MCP clients so a running plugin server picks up the new values
|
|
25
|
+
* (the spawn reads `pluginEnv` fresh). A failed reload never fails the write — the reply says so. */
|
|
26
|
+
export async function handlePluginsCredentialsPut(req: Request, name: string, opts: MetaApiOpts, root?: string, reload?: () => Promise<unknown>): Promise<Response> {
|
|
27
|
+
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
28
|
+
if (!NAME.test(name)) return jsonResponse({ error: 'bad_request' }, 400);
|
|
29
|
+
let body: unknown;
|
|
30
|
+
try { body = await req.json(); }
|
|
31
|
+
catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
32
|
+
const fields = body && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>).fields : undefined;
|
|
33
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields)
|
|
34
|
+
|| Object.values(fields).some(value => typeof value !== 'string' && value !== null)) {
|
|
35
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const result = setPluginCredentials(name, fields as Record<string, string | null>, root);
|
|
39
|
+
debug.log('plugin.credentials', 'set', { plugin: name, fields: Object.keys(fields), count: Object.keys(fields).length });
|
|
40
|
+
let reloaded = false;
|
|
41
|
+
if (reload) {
|
|
42
|
+
try { await reload(); reloaded = true; }
|
|
43
|
+
catch { debug.log('plugin.credentials', 'reload-failed', { plugin: name }, { level: 'warn' }); }
|
|
44
|
+
}
|
|
45
|
+
return jsonResponse({ ...result, reloaded }, 200);
|
|
46
|
+
} catch (error) { return failure(error); }
|
|
47
|
+
}
|
|
@@ -4,7 +4,8 @@ import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
|
4
4
|
import { debug } from '../../debug/log.js';
|
|
5
5
|
import { OFFICIAL_INDEX_KEYS } from '../../market/official-keys.js';
|
|
6
6
|
import { verifyIndex, type MarketplaceIndex } from '../../market/signed-index.js';
|
|
7
|
-
import { listInstalledPlugins } from '../../plugins/install/plugin-install.js';
|
|
7
|
+
import { installPlugin, listInstalledPlugins, removePlugin, type InstallEvent } from '../../plugins/install/plugin-install.js';
|
|
8
|
+
import { updateMarket, type MarketFetchOptions } from '../../plugins/install/market-fetch.js';
|
|
8
9
|
import { userConfigPath } from '../../user-config.js';
|
|
9
10
|
import { jsonResponse } from './json-response.js';
|
|
10
11
|
import { checkAuth, type MetaApiOpts } from './meta-api.js';
|
|
@@ -66,7 +67,7 @@ export function readMarketIndex(root = elanousStateRoot(), trustedKeys: Readonly
|
|
|
66
67
|
const dir = join(base, entry.name);
|
|
67
68
|
const indexPath = [join(dir, 'marketplace.json'), join(dir, '.agents', 'plugins', 'marketplace.json')].find(existsSync);
|
|
68
69
|
if (!indexPath) {
|
|
69
|
-
markets.push({ name: entry.name, signature: 'malformed', detail: '
|
|
70
|
+
markets.push({ name: entry.name, signature: 'malformed', detail: '마켓 인덱스를 찾지 못했습니다.', plugins: [] });
|
|
70
71
|
continue;
|
|
71
72
|
}
|
|
72
73
|
let signature: MarketSignature = 'missing';
|
|
@@ -93,7 +94,7 @@ export function readMarketIndex(root = elanousStateRoot(), trustedKeys: Readonly
|
|
|
93
94
|
signature = 'malformed';
|
|
94
95
|
detail = 'marketplace.json: invalid market index';
|
|
95
96
|
}
|
|
96
|
-
markets.push({ name: entry.name, signature, ...(detail ? { detail } : {}), plugins });
|
|
97
|
+
markets.push({ name: entry.name, signature, ...(detail ? { detail: '마켓 인덱스를 확인하지 못했습니다.' } : {}), plugins });
|
|
97
98
|
}
|
|
98
99
|
markets.sort((a, b) => a.name.localeCompare(b.name));
|
|
99
100
|
debug.log('nexus.plugins-market', 'index', { markets: markets.length, ok: markets.filter(m => m.signature === 'ok').length, notOk: markets.filter(m => m.signature !== 'ok').length });
|
|
@@ -123,3 +124,86 @@ export function handlePluginsGet(req: Request, opts: MetaApiOpts, root?: string)
|
|
|
123
124
|
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
124
125
|
return jsonResponse(listInstalledPlugins(root));
|
|
125
126
|
}
|
|
127
|
+
|
|
128
|
+
const MARKET_NAME = /^[a-z0-9][a-z0-9-]{1,39}$/;
|
|
129
|
+
const MARKET_SPEC = /^[a-z0-9][a-z0-9-]{1,39}@[a-z0-9][a-z0-9-]{1,39}$/;
|
|
130
|
+
|
|
131
|
+
/** 실패 원인 한 줄 — 사용자가 «왜» 를 볼 수 있게(2026-09-29 실물: «설치 중 오류가 발생했습니다» 만 보여 아티팩트 누락을 못 봤다). 홈 경로는 `~` 로 · 200자 · 한 줄. */
|
|
132
|
+
export function safeDetail(error: unknown): string | undefined {
|
|
133
|
+
const message = error instanceof Error ? error.message : typeof error === 'string' ? error : undefined;
|
|
134
|
+
if (!message) return undefined;
|
|
135
|
+
return message.replace(/\/(?:Users|home)\/[^/\s]+/g, '~').replace(/\s+/g, ' ').trim().slice(0, 200) || undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function safeReason(error: unknown): string {
|
|
139
|
+
const reason = error && typeof error === 'object' && 'reason' in error ? error.reason : undefined;
|
|
140
|
+
return typeof reason === 'string' && ['signature', 'scan', 'consent-denied', 'credentials', 'conflict', 'io'].includes(reason)
|
|
141
|
+
? reason : 'io';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function handlePluginsMarketRefresh(req: Request, name: string, opts: MetaApiOpts, fetchOptions: MarketFetchOptions = {}): Promise<Response> {
|
|
145
|
+
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
146
|
+
if (!MARKET_NAME.test(name)) return jsonResponse({ ok: false, reason: 'invalid-market' }, 400);
|
|
147
|
+
try {
|
|
148
|
+
const result = await updateMarket(name, fetchOptions);
|
|
149
|
+
const plugins = projectPlugins(result.index, name);
|
|
150
|
+
debug.log('nexus.plugins-market', 'refresh', { name, ok: true, count: plugins.length });
|
|
151
|
+
return jsonResponse({ ok: true, plugins });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const reason = safeReason(error);
|
|
154
|
+
debug.log('nexus.plugins-market', 'refresh', { name, ok: false, reason });
|
|
155
|
+
return jsonResponse({ ok: false, reason });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function handlePluginsInstall(req: Request, opts: MetaApiOpts, root?: string, configPath?: string): Promise<Response> {
|
|
160
|
+
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
161
|
+
let body: unknown;
|
|
162
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
163
|
+
const input = body as { spec?: unknown; acceptedCapabilities?: unknown } | null;
|
|
164
|
+
if (!input || typeof input.spec !== 'string' || !MARKET_SPEC.test(input.spec) ||
|
|
165
|
+
!Array.isArray(input.acceptedCapabilities) || !input.acceptedCapabilities.every(cap => typeof cap === 'string')) {
|
|
166
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
167
|
+
}
|
|
168
|
+
const spec = input.spec;
|
|
169
|
+
const accepted = new Set(input.acceptedCapabilities as string[]);
|
|
170
|
+
const trustedKeys = configuredTrustedKeys(configPath);
|
|
171
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
172
|
+
async start(controller) {
|
|
173
|
+
const encoder = new TextEncoder();
|
|
174
|
+
const send = (value: Record<string, unknown>) => controller.enqueue(encoder.encode(`${JSON.stringify(value)}\n`));
|
|
175
|
+
const onEvent = (event: InstallEvent) => {
|
|
176
|
+
if (event.event === 'consent') send({ event: 'consent', capabilities: event.capabilities, required: event.required });
|
|
177
|
+
else if (event.event === 'credentials') send({ event: 'credentials', required: event.connectors.some(c => c.fields.length > 0) });
|
|
178
|
+
else send({ event: event.event });
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
await installPlugin(spec, {
|
|
182
|
+
...(root ? { root } : {}), trustedKeys: [...OFFICIAL_INDEX_KEYS, ...trustedKeys],
|
|
183
|
+
onEvent, consent: capabilities => capabilities.every(cap => accepted.has(cap)),
|
|
184
|
+
});
|
|
185
|
+
debug.log('nexus.plugins-market', 'install', { spec, ok: true });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const reason = safeReason(error);
|
|
188
|
+
const detail = safeDetail(error);
|
|
189
|
+
debug.log('nexus.plugins-market', 'install', { spec, ok: false, reason, ...(detail ? { detail } : {}) });
|
|
190
|
+
send({ event: 'failed', reason, ...(detail ? { detail } : {}) });
|
|
191
|
+
} finally { controller.close(); }
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
return new Response(stream, { headers: { 'content-type': 'application/x-ndjson; charset=utf-8', 'cache-control': 'no-store' } });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function handlePluginsRemove(req: Request, name: string, opts: MetaApiOpts, root?: string): Response {
|
|
198
|
+
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
199
|
+
if (!MARKET_NAME.test(name)) return jsonResponse({ error: 'bad_request' }, 400);
|
|
200
|
+
try {
|
|
201
|
+
const removed = removePlugin(name, root);
|
|
202
|
+
debug.log('nexus.plugins-market', 'remove', { name, removed });
|
|
203
|
+
return jsonResponse({ removed });
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const reason = safeReason(error);
|
|
206
|
+
debug.log('nexus.plugins-market', 'remove', { name, reason });
|
|
207
|
+
return jsonResponse({ error: reason }, 500);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
* 응답 키는 `key`·`expires_at` 둘뿐. refresh·계정 id·파일 경로는 응답·로그·에러에 싣지 않는다.
|
|
5
5
|
*/
|
|
6
6
|
import { debug } from '../../debug/log.js';
|
|
7
|
-
import {
|
|
7
|
+
import { githubInstallationCredential } from '../../auth/github-app-token.js';
|
|
8
|
+
import { verifyGroundingToken, type GroundingTokenDeps, type GroundingTokenScope } from '../../grounding/token.js';
|
|
8
9
|
import { isGrokSubscriptionExpiring, readGrokSubscriptionAccess, refreshGrokSubscriptionToken } from '../../grok/credential.js';
|
|
9
10
|
|
|
10
11
|
export const POD_CREDENTIAL_GROK_PATH = '/v1/pod/credential/grok';
|
|
12
|
+
export const POD_CREDENTIAL_GITHUB_PATH = '/v1/pod/credential/github';
|
|
13
|
+
export const POD_GITHUB_CREDENTIAL_TOKEN_ENV = 'ELANOUS_POD_GITHUB_CREDENTIAL_TOKEN';
|
|
14
|
+
export const POD_GITHUB_CREDENTIAL_URL_ENV = 'ELANOUS_POD_GITHUB_CREDENTIAL_URL';
|
|
11
15
|
/** 만료까지 이 안이면 기존 호스트 갱신(`refreshGrokSubscriptionToken`)을 한 번 부른다. */
|
|
12
16
|
export const POD_CREDENTIAL_HOST_REFRESH_WINDOW_MS = 10 * 60 * 1000;
|
|
13
17
|
/** runId 당 분당 1회. */
|
|
@@ -27,34 +31,45 @@ export interface PodCredentialDeps {
|
|
|
27
31
|
readonly loadAccess?: (home: string | undefined, env: NodeJS.ProcessEnv) => { key: string; expiresAt: string } | null;
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
export interface PodGithubCredentialDeps {
|
|
35
|
+
readonly token?: GroundingTokenDeps;
|
|
36
|
+
readonly now?: () => number;
|
|
37
|
+
readonly mintInstallation?: (repository: string) => { token: string; expires_at: string } | null;
|
|
38
|
+
readonly installationRepositories?: (token: string) => Promise<readonly string[] | null>;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
interface HitWindow {
|
|
31
42
|
readonly stamps: number[];
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
const hitsByRun = new Map<string, HitWindow>();
|
|
35
46
|
const globalHits: number[] = [];
|
|
47
|
+
const githubHitsByRun = new Map<string, HitWindow>();
|
|
48
|
+
const githubGlobalHits: number[] = [];
|
|
36
49
|
|
|
37
50
|
export function resetPodCredentialRateForTesting(): void {
|
|
38
51
|
hitsByRun.clear();
|
|
39
52
|
globalHits.length = 0;
|
|
53
|
+
githubHitsByRun.clear();
|
|
54
|
+
githubGlobalHits.length = 0;
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
function prune(stamps: number[], now: number): number[] {
|
|
43
58
|
return stamps.filter((t) => now - t < 60_000);
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
function takeRate(runId: string, now: number): boolean {
|
|
47
|
-
const prev = prune(
|
|
48
|
-
const global = prune(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (prev.length >= POD_CREDENTIAL_PER_RUN_PER_MIN ||
|
|
52
|
-
|
|
61
|
+
function takeRate(runId: string, now: number, byRun = hitsByRun, all = globalHits): boolean {
|
|
62
|
+
const prev = prune(byRun.get(runId)?.stamps ?? [], now);
|
|
63
|
+
const global = prune(all, now);
|
|
64
|
+
all.length = 0;
|
|
65
|
+
all.push(...global);
|
|
66
|
+
if (prev.length >= POD_CREDENTIAL_PER_RUN_PER_MIN || all.length >= POD_CREDENTIAL_GLOBAL_PER_MIN) {
|
|
67
|
+
byRun.set(runId, { stamps: prev });
|
|
53
68
|
return false;
|
|
54
69
|
}
|
|
55
70
|
prev.push(now);
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
all.push(now);
|
|
72
|
+
byRun.set(runId, { stamps: prev });
|
|
58
73
|
return true;
|
|
59
74
|
}
|
|
60
75
|
|
|
@@ -109,25 +124,64 @@ export async function handlePodGrokCredential(req: Request, deps: PodCredentialD
|
|
|
109
124
|
return Response.json({ key: loaded.key, expires_at: expiresAt });
|
|
110
125
|
}
|
|
111
126
|
|
|
127
|
+
export async function installationRepositories(token: string): Promise<readonly string[] | null> {
|
|
128
|
+
const response = await fetch('https://api.github.com/installation/repositories?per_page=100', {
|
|
129
|
+
headers: { authorization: `Bearer ${token}`, accept: 'application/vnd.github+json', 'x-github-api-version': '2022-11-28' },
|
|
130
|
+
signal: AbortSignal.timeout(10_000),
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) return null;
|
|
133
|
+
const data: unknown = await response.json();
|
|
134
|
+
if (!data || typeof data !== 'object' || Array.isArray(data) || !Array.isArray((data as { repositories?: unknown }).repositories)) return null;
|
|
135
|
+
const body = data as { total_count?: unknown; repositories: unknown[] };
|
|
136
|
+
if (body.repositories.length !== 1 || body.total_count !== 1) return null;
|
|
137
|
+
const name = body.repositories[0] && typeof body.repositories[0] === 'object' && !Array.isArray(body.repositories[0]) ? (body.repositories[0] as { full_name?: unknown }).full_name : undefined;
|
|
138
|
+
return typeof name === 'string' ? [name] : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** No repository is accepted from the request body: the host signed its owner/name into the run token. */
|
|
142
|
+
export async function handlePodGithubCredential(req: Request, deps: PodGithubCredentialDeps = {}): Promise<Response> {
|
|
143
|
+
const gate = await authenticatePodCredential(req, deps, 'gh-credential');
|
|
144
|
+
if (!gate.ok) return gate.response;
|
|
145
|
+
if (!gate.repository || !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(gate.repository)) return Response.json({ error: 'unauthorized' }, { status: 401 });
|
|
146
|
+
if (!takeRate(gate.runId, (deps.now ?? Date.now)(), githubHitsByRun, githubGlobalHits)) return reasonResponse(429, 'rate');
|
|
147
|
+
|
|
148
|
+
let issued: { token: string; expires_at: string } | null;
|
|
149
|
+
try {
|
|
150
|
+
issued = (deps.mintInstallation ?? ((repository) => githubInstallationCredential({ scope: { repository: repository.split('/')[1]! } })))(gate.repository);
|
|
151
|
+
} catch {
|
|
152
|
+
return reasonResponse(503, 'unavailable');
|
|
153
|
+
}
|
|
154
|
+
if (!issued?.token || typeof issued.expires_at !== 'string' || !Number.isFinite(Date.parse(issued.expires_at)) || Date.parse(issued.expires_at) <= (deps.now ?? Date.now)()) return reasonResponse(503, 'unavailable');
|
|
155
|
+
try {
|
|
156
|
+
const available = await (deps.installationRepositories ?? installationRepositories)(issued.token);
|
|
157
|
+
if (available?.length !== 1 || available[0]?.toLowerCase() !== gate.repository.toLowerCase()) return reasonResponse(503, 'unavailable');
|
|
158
|
+
} catch {
|
|
159
|
+
return reasonResponse(503, 'unavailable');
|
|
160
|
+
}
|
|
161
|
+
debug.log('pod.credential', 'github-issued', { runId: gate.runId, job: gate.job, expiresAt: issued.expires_at });
|
|
162
|
+
return Response.json({ token: issued.token, expires_at: issued.expires_at });
|
|
163
|
+
}
|
|
164
|
+
|
|
112
165
|
/**
|
|
113
166
|
* 쓰기 관문. 본문을 읽지 않는다. 토큰이 없거나 서명이 깨지면 401,
|
|
114
167
|
* scope·만료·회수는 403 과 사유만.
|
|
115
168
|
*/
|
|
116
169
|
export async function authenticatePodCredential(
|
|
117
170
|
req: Request,
|
|
118
|
-
deps: PodCredentialDeps = {},
|
|
171
|
+
deps: Pick<PodCredentialDeps, 'token'> = {},
|
|
172
|
+
expectedScope: GroundingTokenScope = 'llm-credential',
|
|
119
173
|
): Promise<
|
|
120
|
-
| { readonly ok: true; readonly runId: string; readonly job: string; readonly exp: number }
|
|
174
|
+
| { readonly ok: true; readonly runId: string; readonly job: string; readonly repository?: string; readonly exp: number }
|
|
121
175
|
| { readonly ok: false; readonly response: Response }
|
|
122
176
|
> {
|
|
123
177
|
const token = bearerOf(req);
|
|
124
178
|
if (!token) return { ok: false, response: Response.json({ error: 'unauthorized' }, { status: 401 }) };
|
|
125
|
-
const verified = await verifyGroundingToken(token, { ...(deps.token ?? {}), expectedScope
|
|
179
|
+
const verified = await verifyGroundingToken(token, { ...(deps.token ?? {}), expectedScope });
|
|
126
180
|
if (!verified.ok) {
|
|
127
181
|
if (verified.reason === 'expired' || verified.reason === 'revoked' || verified.reason === 'scope') {
|
|
128
182
|
return { ok: false, response: reasonResponse(403, verified.reason) };
|
|
129
183
|
}
|
|
130
184
|
return { ok: false, response: Response.json({ error: 'unauthorized' }, { status: 401 }) };
|
|
131
185
|
}
|
|
132
|
-
return { ok: true, runId: verified.runId, job: verified.job, exp: verified.exp };
|
|
186
|
+
return { ok: true, runId: verified.runId, job: verified.job, ...(verified.repository ? { repository: verified.repository } : {}), exp: verified.exp };
|
|
133
187
|
}
|
|
@@ -49,6 +49,13 @@ export const PUBLIC_ROUTES: Array<PublicRoute> = [
|
|
|
49
49
|
selfVerified: true,
|
|
50
50
|
why: 'Pod grok access relay (#20789); Pods hold no nexus bearer — pod-credential-api.ts verifies its own llm-credential token (verifyGroundingToken expectedScope) before reading the body',
|
|
51
51
|
},
|
|
52
|
+
{
|
|
53
|
+
method: 'POST',
|
|
54
|
+
match: 'exact',
|
|
55
|
+
path: '/v1/pod/credential/github',
|
|
56
|
+
selfVerified: true,
|
|
57
|
+
why: 'Pod GitHub App credential relay verifies its own run-scoped gh-credential token before minting a repository-scoped installation token',
|
|
58
|
+
},
|
|
52
59
|
{
|
|
53
60
|
method: 'GET',
|
|
54
61
|
match: 'prefix',
|