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.
Files changed (455) hide show
  1. package/README.md +7 -6
  2. package/apps/pwa/out/404/index.html +1 -1
  3. package/apps/pwa/out/404.html +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
  9. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 4240-7153881b0b377306.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
  19. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7633.0654fe6ea259c1ee.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 91ff6dda-3b33d42d20657429.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_buildManifest.js +1 -1
  93. package/apps/pwa/out/approvals/index.html +1 -1
  94. package/apps/pwa/out/approvals/index.txt +29 -27
  95. package/apps/pwa/out/autopilot/index.html +1 -1
  96. package/apps/pwa/out/autopilot/index.txt +29 -27
  97. package/apps/pwa/out/board/index.html +1 -1
  98. package/apps/pwa/out/board/index.txt +29 -27
  99. package/apps/pwa/out/botlab/index.html +1 -1
  100. package/apps/pwa/out/botlab/index.txt +26 -24
  101. package/apps/pwa/out/bots/index.html +1 -1
  102. package/apps/pwa/out/bots/index.txt +26 -24
  103. package/apps/pwa/out/chat/index.html +1 -1
  104. package/apps/pwa/out/chat/index.txt +26 -24
  105. package/apps/pwa/out/control/index.html +1 -1
  106. package/apps/pwa/out/control/index.txt +29 -27
  107. package/apps/pwa/out/design-check/index.html +1 -1
  108. package/apps/pwa/out/design-check/index.txt +29 -27
  109. package/apps/pwa/out/editor/index.html +1 -1
  110. package/apps/pwa/out/editor/index.txt +29 -27
  111. package/apps/pwa/out/healthz +1 -1
  112. package/apps/pwa/out/icons/elanous-16.png +0 -0
  113. package/apps/pwa/out/icons/elanous-180.png +0 -0
  114. package/apps/pwa/out/icons/elanous-192.png +0 -0
  115. package/apps/pwa/out/icons/elanous-32.png +0 -0
  116. package/apps/pwa/out/icons/elanous-512.png +0 -0
  117. package/apps/pwa/out/index.html +1 -1
  118. package/apps/pwa/out/index.txt +26 -24
  119. package/apps/pwa/out/intake/index.html +1 -1
  120. package/apps/pwa/out/intake/index.txt +29 -27
  121. package/apps/pwa/out/live/index.html +1 -1
  122. package/apps/pwa/out/live/index.txt +29 -27
  123. package/apps/pwa/out/manifest.webmanifest +5 -2
  124. package/apps/pwa/out/market/index.html +1 -1
  125. package/apps/pwa/out/market/index.txt +26 -24
  126. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  127. package/apps/pwa/out/missions/placeholder/index.txt +26 -24
  128. package/apps/pwa/out/morning/index.html +1 -1
  129. package/apps/pwa/out/morning/index.txt +29 -27
  130. package/apps/pwa/out/observatory/index.html +1 -1
  131. package/apps/pwa/out/observatory/index.txt +29 -27
  132. package/apps/pwa/out/reflection/index.html +1 -1
  133. package/apps/pwa/out/reflection/index.txt +27 -25
  134. package/apps/pwa/out/scheduler/index.html +1 -1
  135. package/apps/pwa/out/scheduler/index.txt +29 -27
  136. package/apps/pwa/out/sessions/index.html +1 -1
  137. package/apps/pwa/out/sessions/index.txt +26 -24
  138. package/apps/pwa/out/settings/devices/index.html +1 -1
  139. package/apps/pwa/out/settings/devices/index.txt +29 -27
  140. package/apps/pwa/out/settings/index.html +1 -1
  141. package/apps/pwa/out/settings/index.txt +29 -27
  142. package/apps/pwa/out/setup/done/index.html +1 -1
  143. package/apps/pwa/out/setup/done/index.txt +29 -27
  144. package/apps/pwa/out/setup/index.html +1 -1
  145. package/apps/pwa/out/setup/index.txt +29 -27
  146. package/apps/pwa/out/share/index.html +1 -1
  147. package/apps/pwa/out/share/index.txt +29 -27
  148. package/apps/pwa/out/showroom/index.html +1 -1
  149. package/apps/pwa/out/showroom/index.txt +26 -24
  150. package/apps/pwa/out/sw-strategies.js +44 -0
  151. package/apps/pwa/out/sw.js +6 -44
  152. package/apps/pwa/out/tasks/index.html +1 -1
  153. package/apps/pwa/out/tasks/index.txt +29 -27
  154. package/apps/pwa/out/term/index.html +1 -1
  155. package/apps/pwa/out/term/index.txt +29 -27
  156. package/apps/pwa/out/trace/index.html +1 -1
  157. package/apps/pwa/out/trace/index.txt +29 -27
  158. package/apps/pwa/out/vault/index.html +1 -1
  159. package/apps/pwa/out/vault/index.txt +29 -27
  160. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  161. package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
  162. package/apps/pwa/out/workflows/index.html +1 -1
  163. package/apps/pwa/out/workflows/index.txt +29 -27
  164. package/apps/pwa/out/workspace/index.html +1 -1
  165. package/apps/pwa/out/workspace/index.txt +29 -27
  166. package/apps/pwa/out/worktrees/index.html +1 -1
  167. package/apps/pwa/out/worktrees/index.txt +29 -27
  168. package/catalog/resources.yaml +6 -6
  169. package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
  170. package/graphs/landing/landing-heal.yaml +1 -1
  171. package/graphs/release/recipes.yaml +11 -1
  172. package/graphs/release/release-loop.yaml +9 -2
  173. package/graphs/video/README.md +1 -0
  174. package/graphs/video/explainer-line.yaml +58 -0
  175. package/package.json +3 -1
  176. package/plugins/card-followup/README.md +33 -0
  177. package/plugins/card-followup/examples/card-sample.json +7 -0
  178. package/plugins/card-followup/graphs/card-followup.yaml +16 -0
  179. package/plugins/card-followup/graphs/recipes.yaml +12 -0
  180. package/plugins/card-followup/graphs/run-step.ts +172 -0
  181. package/plugins/card-followup/plugin.json +25 -0
  182. package/plugins/geo-check/README.md +13 -0
  183. package/plugins/geo-check/examples/brand-sample.json +8 -0
  184. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  185. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  186. package/plugins/geo-check/graphs/run-step.ts +177 -0
  187. package/plugins/geo-check/plugin.json +24 -0
  188. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  189. package/plugins/job-coach/README.md +42 -0
  190. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  191. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  192. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  193. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  194. package/plugins/job-coach/graphs/run-step.ts +211 -17
  195. package/plugins/job-coach/plugin.json +16 -3
  196. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  197. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  198. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  199. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  200. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  201. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  202. package/scripts/backup/elanous-backup.sh +4 -1
  203. package/scripts/build-docs-site.ts +4 -3
  204. package/scripts/coord-channel-watch.sh +23 -4
  205. package/scripts/coord-post.sh +49 -12
  206. package/scripts/coord-tracks.json +5 -5
  207. package/scripts/daemon-port-baseline.txt +1 -2
  208. package/scripts/eval-bot-vs-grok.ts +79 -0
  209. package/scripts/eval-grok-live-baseline.ts +36 -0
  210. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  211. package/scripts/fleet/node-setup.sh +46 -7
  212. package/scripts/landing-heal/must-fix-match.ts +26 -0
  213. package/scripts/landing-heal/run-mission.ts +114 -20
  214. package/scripts/lib/android-test-device.ts +32 -0
  215. package/scripts/lib/front-route-measurement.ts +131 -0
  216. package/scripts/lib/tui-sim-keys.ts +42 -0
  217. package/scripts/measure-front-route-classifier.ts +56 -0
  218. package/scripts/media/ocr-leak-scan.sh +21 -0
  219. package/scripts/public-export.ts +2 -3
  220. package/scripts/public-import-pr.ts +80 -6
  221. package/scripts/publish-internal-dist.ts +2 -3
  222. package/scripts/red-sweep-diff.ts +1 -1
  223. package/scripts/release-loop/auto-approve-node.ts +72 -0
  224. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  225. package/scripts/release-loop/gate-diff.ts +29 -0
  226. package/scripts/release-loop/gate-node.ts +366 -45
  227. package/scripts/release-loop/publish-node.ts +7 -3
  228. package/scripts/release-loop/release-docs.ts +20 -13
  229. package/scripts/release-loop/shard-plan.ts +47 -0
  230. package/scripts/release-loop/test-durations.ts +63 -0
  231. package/scripts/release-loop/upgrade-node.ts +3 -2
  232. package/scripts/test-audit-coverage-per-file.sh +16 -0
  233. package/scripts/test-audit-meaningless.ts +88 -0
  234. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  235. package/scripts/test-deterministic.ts +27 -5
  236. package/scripts/tui-sim.ts +59 -14
  237. package/scripts/ux/bump-installed.sh +20 -0
  238. package/scripts/ux/lib/cdp.ts +72 -0
  239. package/scripts/ux/record-page.ts +53 -0
  240. package/skills/explainer-video/SKILL.md +117 -0
  241. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  242. package/skills/explainer-video/engine/build.mjs +431 -0
  243. package/skills/explainer-video/engine/reel.mjs +128 -0
  244. package/skills/explainer-video/engine/reel.sh +8 -0
  245. package/skills/explainer-video/engine/vo.ts +79 -0
  246. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  247. package/src/agent/nested-agent-env.ts +1 -0
  248. package/src/agent-mission/driver.ts +95 -16
  249. package/src/agent-mission/mission-cli.ts +6 -0
  250. package/src/agent-mission/resource-ladder.ts +73 -25
  251. package/src/auth/acp-token.ts +16 -0
  252. package/src/auth/github-app-token.ts +112 -0
  253. package/src/autopilot/gh-rest-fallback.ts +177 -0
  254. package/src/autopilot/mission-origin.ts +20 -4
  255. package/src/autopilot/mission-phase-track.ts +11 -1
  256. package/src/autopilot/pr-manager.ts +51 -4
  257. package/src/autopilot/track-agent.ts +3 -3
  258. package/src/autopilot/track-courier.ts +2 -2
  259. package/src/boot/acp-server.ts +2 -16
  260. package/src/boot/daemon-tools/web-search.ts +2 -1
  261. package/src/boot/ws-bridge.ts +11 -0
  262. package/src/chat/index.ts +37 -8
  263. package/src/chat/log-scroll-key.ts +12 -0
  264. package/src/chat/submit-trace.ts +30 -0
  265. package/src/cli/agent-cli.ts +7 -2
  266. package/src/cli/decisions-cli.ts +94 -0
  267. package/src/cli/doctor-cli.ts +40 -54
  268. package/src/cli/doctor-readiness.ts +16 -24
  269. package/src/cli/intake-cli.ts +1 -1
  270. package/src/cli/mcp-call.ts +9 -0
  271. package/src/cli/mcp-gateway-cli.ts +7 -3
  272. package/src/cli/nexus-ios-bind.ts +25 -8
  273. package/src/cli/phone-link.ts +117 -0
  274. package/src/cli/plugin-cli.ts +136 -11
  275. package/src/cli/pr-lineage.ts +2 -1
  276. package/src/cli/private-ledger-commands.ts +31 -0
  277. package/src/cli/pwa-start.ts +3 -0
  278. package/src/cli/release-cli.ts +97 -7
  279. package/src/cli/self-update.ts +7 -4
  280. package/src/cli/start-cli.ts +33 -14
  281. package/src/dashboard/acp-boot.ts +3 -0
  282. package/src/dashboard/index.ts +62 -13
  283. package/src/dashboard/input/no-provider-reply.ts +13 -0
  284. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  285. package/src/dashboard/stale-install-check.ts +77 -0
  286. package/src/decisions/decision-ledger.ts +404 -0
  287. package/src/domains/outbound-alert.ts +13 -7
  288. package/src/domains/telegram-kind-route.ts +21 -0
  289. package/src/execution-loop/dispatch-task.ts +139 -0
  290. package/src/execution-loop/gate-decision-summary.ts +29 -0
  291. package/src/execution-loop/gates.ts +197 -0
  292. package/src/field/field-media.ts +245 -0
  293. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  294. package/src/graph-kinds/plugin-nodes.ts +10 -2
  295. package/src/graph-runner/runner.ts +35 -5
  296. package/src/grounding/token.ts +12 -6
  297. package/src/harness/harness-cli-command.ts +68 -9
  298. package/src/harness/harness-pod-dispatch.ts +8 -2
  299. package/src/harness/harness-stop.ts +147 -18
  300. package/src/harness/self-send-decide.ts +26 -17
  301. package/src/harness/self-send-target.ts +22 -1
  302. package/src/harness/worktree-branch-prefix.ts +4 -3
  303. package/src/index.ts +50 -13
  304. package/src/instance/resolve.ts +6 -0
  305. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  306. package/src/intake-plane/digest.ts +11 -2
  307. package/src/live/detail-switch.ts +8 -3
  308. package/src/llm/usable-llm.ts +32 -0
  309. package/src/loops/registry.ts +27 -5
  310. package/src/market/publish.ts +59 -3
  311. package/src/native-tool-catalog.ts +2 -2
  312. package/src/nexus/api/field-uploads.ts +143 -0
  313. package/src/nexus/api/graph-approvals.ts +174 -0
  314. package/src/nexus/api/harness-api.ts +219 -5
  315. package/src/nexus/api/http-server.ts +69 -6
  316. package/src/nexus/api/log-fabric.ts +10 -1
  317. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  318. package/src/nexus/api/merge-approvals.ts +2 -1
  319. package/src/nexus/api/meta-api.ts +14 -0
  320. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  321. package/src/nexus/api/plugins-market.ts +87 -3
  322. package/src/nexus/api/pod-credential-api.ts +68 -14
  323. package/src/nexus/api/public-routes.ts +7 -0
  324. package/src/nexus/api/trace.ts +57 -6
  325. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  326. package/src/nexus/index.ts +33 -32
  327. package/src/nexus/outbound/router.ts +1 -1
  328. package/src/notify/llm-budget-alert.ts +119 -0
  329. package/src/onboarding.ts +25 -2
  330. package/src/plugins/core/host.ts +14 -0
  331. package/src/plugins/install/market-fetch.ts +4 -3
  332. package/src/plugins/install/plugin-credentials.ts +106 -0
  333. package/src/plugins/install/plugin-install.ts +163 -32
  334. package/src/plugins/maker/plugin-maker.ts +208 -0
  335. package/src/prompt-library/universal-preamble.ts +8 -2
  336. package/src/relay/channel.ts +184 -0
  337. package/src/relay/mux.ts +182 -0
  338. package/src/relay/test-vectors.json +29 -0
  339. package/src/release-loop/checklist.ts +217 -0
  340. package/src/release-loop/release-note.ts +6 -3
  341. package/src/seat-address/seat-address.ts +34 -0
  342. package/src/self-dev/dev-cli.ts +40 -20
  343. package/src/self-dev/dev-pipeline.ts +77 -1
  344. package/src/self-implement/gate-baseline.ts +3 -1
  345. package/src/self-implement/goal-author.ts +18 -1
  346. package/src/self-implement/host-regate.ts +6 -2
  347. package/src/self-implement/orchestrator.ts +7 -2
  348. package/src/self-implement/run-ledger-compact.ts +1 -1
  349. package/src/self-implement/seams.ts +4 -2
  350. package/src/session/index.ts +23 -10
  351. package/src/skills/tools/dev-harness.ts +42 -0
  352. package/src/steward/steward-cards.ts +45 -0
  353. package/src/steward/triage.ts +50 -14
  354. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  355. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  356. package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -6
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  358. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  359. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  360. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  361. package/src/telegram-report.ts +12 -6
  362. package/src/telegram.ts +108 -1
  363. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  364. package/src/tui.ts +3 -2
  365. package/src/user-config.ts +32 -2
  366. package/src/util/spawn-sync-output.ts +15 -0
  367. package/src/version/code-revision.ts +1 -1
  368. package/src/version/packed-revision.json +1 -1
  369. package/src/video-pipeline/recipes/explainer.ts +213 -0
  370. package/src/video-pipeline/walk-line.ts +3 -2
  371. package/src/web-search/free.ts +77 -0
  372. package/src/web-search/index.ts +2 -0
  373. package/src/workflow-runtime/deps-bridge.ts +4 -3
  374. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  375. package/src/workflow-runtime/types.ts +2 -1
  376. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  388. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/botlab/page-9fa879ebf5959096.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/control/page-4472ddf1e2458e69.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/morning/page-112f7e477bda77fd.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  454. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  455. /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 { handlePluginsIndexGet, handlePluginsGet } from './plugins-market.js';
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
- if (!opts.metaApi || !checkAuth(req, opts.metaApi)) {
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 const MCP_AUTH_HINT =
8
- 'Elanous 는 OAuth 를 쓰지 않는다(고정 Bearer 토큰). 권장: stdio 로 등록 — `claude mcp add elanous -- elanous mcp serve`. '
9
- + 'HTTP 가 필요하면 ~/.elanous/acp-token 의 값을 Authorization 헤더로: '
10
- + '`claude mcp add --transport http elanous http://127.0.0.1:31415/v1/mcp --header "Authorization: Bearer <acp-token>"`';
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: MCP_AUTH_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: MCP_AUTH_HINT }), {
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().then((result) => { if (!result.ok) readCache.delete(key); return result; });
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: 'marketplace.json: not found', plugins: [] });
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 { verifyGroundingToken, type GroundingTokenDeps } from '../../grounding/token.js';
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(hitsByRun.get(runId)?.stamps ?? [], now);
48
- const global = prune(globalHits, now);
49
- globalHits.length = 0;
50
- globalHits.push(...global);
51
- if (prev.length >= POD_CREDENTIAL_PER_RUN_PER_MIN || globalHits.length >= POD_CREDENTIAL_GLOBAL_PER_MIN) {
52
- hitsByRun.set(runId, { stamps: prev });
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
- globalHits.push(now);
57
- hitsByRun.set(runId, { stamps: prev });
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: 'llm-credential' });
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',