@jr2/orchestrator 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/console/components/app.ts +55 -15
  2. package/console/components/bottom-drawer.ts +259 -0
  3. package/console/components/drawer.ts +14 -6
  4. package/console/components/fleet.ts +36 -13
  5. package/console/components/machine-pane.ts +61 -5
  6. package/console/components/nav.ts +54 -8
  7. package/console/conversation.ts +161 -0
  8. package/console/login.ts +123 -0
  9. package/console/main.ts +600 -60
  10. package/console/store.ts +611 -64
  11. package/console/style.css +231 -0
  12. package/docs/agents-block.md +2 -2
  13. package/docs/concepts/agents-and-turns.md +86 -9
  14. package/docs/concepts/architecture.md +41 -28
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +7 -5
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +4 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +50 -22
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +5 -4
  23. package/docs/concepts/pools-and-sources.md +187 -11
  24. package/docs/concepts/secrets-and-trust.md +73 -33
  25. package/docs/concepts/the-menu.md +11 -0
  26. package/docs/concepts/workspaces.md +6 -5
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
  29. package/docs/get-started/connect-a-model.md +1 -1
  30. package/docs/get-started/create-an-instance.md +17 -14
  31. package/docs/guides/add-tools-to-the-sandbox.md +55 -11
  32. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  33. package/docs/guides/answer-a-gate.md +74 -37
  34. package/docs/guides/bind-open-parts.md +24 -8
  35. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  36. package/docs/guides/cancel-a-run.md +26 -16
  37. package/docs/guides/fix-a-failed-converge.md +18 -6
  38. package/docs/guides/give-a-caller-a-scope.md +203 -0
  39. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  41. package/docs/guides/let-an-agent-write-a-chart.md +9 -6
  42. package/docs/guides/look-inside-a-sandbox.md +101 -31
  43. package/docs/guides/open-the-console.md +63 -14
  44. package/docs/guides/read-a-conversation.md +225 -0
  45. package/docs/guides/run-a-chart-by-name.md +17 -16
  46. package/docs/guides/run-one-worker-per-item.md +15 -9
  47. package/docs/guides/set-an-agents-access-and-dials.md +2 -0
  48. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  49. package/docs/guides/start-a-run-from-a-run.md +18 -16
  50. package/docs/guides/start-a-run.md +24 -18
  51. package/docs/guides/steer-an-agent.md +332 -0
  52. package/docs/guides/use-a-hosted-provider.md +1 -1
  53. package/docs/guides/watch-a-run.md +127 -121
  54. package/docs/guides/write-a-hook-kind.md +238 -0
  55. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  56. package/docs/guides/write-a-workflow.md +17 -7
  57. package/docs/llms.txt +81 -15
  58. package/docs/reference/authoring-surface/agent.md +1 -1
  59. package/docs/reference/authoring-surface/answer.md +1 -1
  60. package/docs/reference/authoring-surface/arg.md +1 -1
  61. package/docs/reference/authoring-surface/basic.md +1 -1
  62. package/docs/reference/authoring-surface/bearer.md +1 -1
  63. package/docs/reference/authoring-surface/branchname.md +1 -1
  64. package/docs/reference/authoring-surface/carry.md +23 -0
  65. package/docs/reference/authoring-surface/chartschema.md +1 -1
  66. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  67. package/docs/reference/authoring-surface/command.md +1 -1
  68. package/docs/reference/authoring-surface/customize.md +4 -1
  69. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  70. package/docs/reference/authoring-surface/defineevent.md +1 -1
  71. package/docs/reference/authoring-surface/dispatch.md +5 -4
  72. package/docs/reference/authoring-surface/fail.md +1 -1
  73. package/docs/reference/authoring-surface/file.md +1 -1
  74. package/docs/reference/authoring-surface/frompick.md +1 -1
  75. package/docs/reference/authoring-surface/gate.md +118 -0
  76. package/docs/reference/authoring-surface/group-agents.md +15 -0
  77. package/docs/reference/authoring-surface/group-charts.md +16 -0
  78. package/docs/reference/authoring-surface/group-composition.md +25 -0
  79. package/docs/reference/authoring-surface/group-config.md +17 -0
  80. package/docs/reference/authoring-surface/group-effects.md +48 -0
  81. package/docs/reference/authoring-surface/group-failure.md +13 -0
  82. package/docs/reference/authoring-surface/group-gates.md +16 -0
  83. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  84. package/docs/reference/authoring-surface/group-steering.md +12 -0
  85. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  86. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  87. package/docs/reference/authoring-surface/http.md +1 -1
  88. package/docs/reference/authoring-surface/isfailure.md +1 -1
  89. package/docs/reference/authoring-surface/isready.md +1 -1
  90. package/docs/reference/authoring-surface/jr2setup.md +13 -3
  91. package/docs/reference/authoring-surface/mcp.md +107 -0
  92. package/docs/reference/authoring-surface/open.md +5 -3
  93. package/docs/reference/authoring-surface/openbody.md +1 -1
  94. package/docs/reference/authoring-surface/paramsof.md +1 -1
  95. package/docs/reference/authoring-surface/pool.md +77 -9
  96. package/docs/reference/authoring-surface/push.md +1 -1
  97. package/docs/reference/authoring-surface/readyof.md +1 -1
  98. package/docs/reference/authoring-surface/reread.md +19 -0
  99. package/docs/reference/authoring-surface/secret.md +1 -1
  100. package/docs/reference/authoring-surface/sentby.md +1 -1
  101. package/docs/reference/authoring-surface/skip.md +22 -0
  102. package/docs/reference/authoring-surface/source.md +56 -1
  103. package/docs/reference/authoring-surface/steer.md +93 -0
  104. package/docs/reference/authoring-surface/text.md +1 -1
  105. package/docs/reference/authoring-surface/type-agentdefinition.md +14 -1
  106. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  107. package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
  108. package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
  109. package/docs/reference/authoring-surface/type-binding.md +1 -1
  110. package/docs/reference/authoring-surface/type-caller.md +40 -0
  111. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  112. package/docs/reference/authoring-surface/type-carry.md +19 -0
  113. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  114. package/docs/reference/authoring-surface/type-commandoutput.md +1 -1
  115. package/docs/reference/authoring-surface/type-customize.md +16 -1
  116. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  117. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  118. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  119. package/docs/reference/authoring-surface/type-dispatchoutput.md +1 -1
  120. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  121. package/docs/reference/authoring-surface/type-framepath.md +1 -1
  122. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  123. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  124. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  125. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  126. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  127. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  128. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  129. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  130. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  131. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  132. package/docs/reference/authoring-surface/type-hostinjectedinput.md +1 -1
  133. package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
  134. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  135. package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
  136. package/docs/reference/authoring-surface/type-lends.md +5 -1
  137. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  138. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  139. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  140. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  141. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  142. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  143. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  144. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  145. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  146. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  147. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  148. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  149. package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
  150. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  151. package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
  152. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  153. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  154. package/docs/reference/authoring-surface/type-reposlot.md +1 -1
  155. package/docs/reference/authoring-surface/type-reread.md +18 -0
  156. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  157. package/docs/reference/authoring-surface/type-sender.md +26 -1
  158. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  159. package/docs/reference/authoring-surface/type-size.md +1 -1
  160. package/docs/reference/authoring-surface/type-skip.md +19 -0
  161. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  162. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  163. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  164. package/docs/reference/authoring-surface/type-sourcespec.md +16 -7
  165. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  166. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  167. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  168. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  169. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  170. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  171. package/docs/reference/authoring-surface/type-workspaced.md +1 -1
  172. package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
  173. package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
  174. package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
  175. package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
  176. package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
  177. package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
  178. package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
  179. package/docs/reference/authoring-surface/workspace.md +1 -1
  180. package/docs/reference/authoring-surface/worktree.md +1 -1
  181. package/docs/reference/authoring-surface.md +88 -167
  182. package/docs/reference/cli.md +413 -74
  183. package/docs/reference/config.md +235 -2
  184. package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
  185. package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
  186. package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
  187. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  188. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  189. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  190. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  191. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +9 -2
  192. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
  193. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  194. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +18 -14
  195. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
  196. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  197. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +32 -18
  198. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
  199. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  200. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +116 -34
  201. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
  202. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  203. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +7 -6
  204. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
  205. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  206. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  207. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -18
  208. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  209. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +4 -3
  210. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  211. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +5 -4
  212. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
  213. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  214. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  215. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  216. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +7 -0
  217. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
  218. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
  219. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
  220. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
  221. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  222. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  223. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +51 -23
  224. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
  225. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
  226. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  227. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +17 -13
  228. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  229. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  230. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
  231. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
  232. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  233. package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
  234. package/docs/reference/decisions.md +6 -4
  235. package/docs/reference/glossary.md +96 -42
  236. package/docs/reference/hooks.md +1077 -0
  237. package/docs/reference/http-api.md +976 -149
  238. package/docs/why.md +2 -1
  239. package/package.json +10 -3
  240. package/src/actor.ts +36 -10
  241. package/src/agent.ts +50 -0
  242. package/src/assembly-checks.ts +530 -4
  243. package/src/callers.ts +245 -0
  244. package/src/chart.ts +52 -7
  245. package/src/config.ts +550 -1
  246. package/src/conversations.ts +453 -0
  247. package/src/custodian.ts +8 -2
  248. package/src/customize.ts +143 -4
  249. package/src/dispatch-effect.ts +142 -26
  250. package/src/durability.ts +2 -0
  251. package/src/duration.ts +20 -0
  252. package/src/effect-checks.ts +2 -1
  253. package/src/effect.ts +60 -7
  254. package/src/effector-wire.ts +272 -27
  255. package/src/gate-brand.ts +16 -0
  256. package/src/gate.ts +634 -48
  257. package/src/harness-client.ts +10 -4
  258. package/src/held-secrets.ts +72 -23
  259. package/src/holds.ts +2 -2
  260. package/src/hook-kinds/discord.ts +92 -0
  261. package/src/hook-kinds/door.ts +107 -0
  262. package/src/hook-kinds/github.ts +89 -0
  263. package/src/hook-kinds/gitlab.ts +72 -0
  264. package/src/hook-kinds/hook.ts +7 -0
  265. package/src/hook-kinds/index.ts +465 -0
  266. package/src/hook-kinds/slack.ts +151 -0
  267. package/src/hook-kinds/webhook.ts +113 -0
  268. package/src/hooks.ts +506 -0
  269. package/src/http-effect.ts +76 -38
  270. package/src/http.ts +889 -94
  271. package/src/index.ts +16 -5
  272. package/src/instance.ts +196 -26
  273. package/src/internal.ts +22 -3
  274. package/src/machine-doc.ts +26 -1
  275. package/src/mcp-effect.ts +424 -0
  276. package/src/mount-digest.ts +41 -7
  277. package/src/names.ts +64 -2
  278. package/src/notices.ts +138 -10
  279. package/src/open-body.ts +25 -11
  280. package/src/open-effect.ts +17 -4
  281. package/src/open.ts +4 -2
  282. package/src/parts.ts +87 -9
  283. package/src/party.ts +2 -1
  284. package/src/pick.ts +2 -2
  285. package/src/pool.ts +1142 -163
  286. package/src/registration.ts +203 -13
  287. package/src/run-host.ts +1509 -171
  288. package/src/run-list.ts +191 -0
  289. package/src/sandbox-kube.ts +53 -5
  290. package/src/secret-placement.ts +239 -33
  291. package/src/server.ts +53 -4
  292. package/src/setup.ts +72 -32
  293. package/src/snapshot-store.ts +565 -48
  294. package/src/steer.ts +111 -0
  295. package/src/stub-harness.ts +6 -2
  296. package/src/tokens.ts +263 -22
  297. package/src/wire.ts +17 -0
  298. package/src/workspace.ts +50 -6
  299. package/src/worktree.ts +3 -3
  300. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  301. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
@@ -1,11 +1,17 @@
1
1
  // The nav: breadcrumb (the address, made clickable), the Machine's id, the zoom controls, the
2
- // fleet-wide attention badge, the feed state, and the token box (ADR-0032). The token INPUT is
2
+ // fleet-wide attention badge, the feed state, and the credential box (ADR-0032). The token INPUT is
3
3
  // uncontrolled — its value is the reader's secret, never state — seeded once from sessionStorage
4
4
  // and committed on the change event; the store carries only the token's STATE, which is what the
5
- // badge renders.
5
+ // badge renders, and the Caller that `GET /whoami` said it is.
6
+ //
7
+ // With no live credential the box offers the field and a login button for each issuer the Instance
8
+ // declares for a login. With one, it shows who the page acts as (its scopes in the title) and a
9
+ // sign-out button in place of the field.
6
10
 
7
11
  import { h, type JSX } from "preact";
8
- import { gateCount, type Store, type TokenState } from "../store.ts";
12
+ import { useRef } from "preact/hooks";
13
+ import { callerLabel, callerScopes, gateCount, may, type Store, type TokenState } from "../store.ts";
14
+ import type { LoginIssuer } from "../login.ts";
9
15
  import type { AppApi } from "./app.ts";
10
16
 
11
17
  const TOKEN_BADGES: Record<TokenState, string> = { none: "", checking: "…", live: "live", invalid: "invalid" };
@@ -15,10 +21,14 @@ export function Nav(props: {
15
21
  machineId: string | null;
16
22
  zoomPct: number;
17
23
  initialToken: string;
24
+ logins: readonly LoginIssuer[];
25
+ loginNote: string | null;
18
26
  onChooseTab: (tab: "attention") => void;
19
27
  api: AppApi;
20
28
  }): JSX.Element {
21
- const { store, machineId, zoomPct, initialToken, onChooseTab, api } = props;
29
+ const { store, machineId, zoomPct, initialToken, logins, loginNote, onChooseTab, api } = props;
30
+ const field = useRef<HTMLInputElement>(null);
31
+ const actingAs = callerLabel(store);
22
32
  const connection = store.workflow ? store.connection : "idle";
23
33
  const count = gateCount(store);
24
34
  return h(
@@ -56,7 +66,7 @@ export function Nav(props: {
56
66
  "button",
57
67
  {
58
68
  id: "attention-badge",
59
- hidden: store.token !== "live" || count === 0,
69
+ hidden: !may(store, "answer") || count === 0,
60
70
  title: "open gates across the fleet",
61
71
  onClick: () => onChooseTab("attention"),
62
72
  },
@@ -65,17 +75,53 @@ export function Nav(props: {
65
75
  ),
66
76
  h("span", { id: "connection", class: `conn conn--${connection}`, title: "feed connection" }, connection),
67
77
  h(
68
- "label",
69
- { id: "token-box", title: "Instance token — unlocks start & gates (ADR-0032)" },
78
+ "div",
79
+ { id: "token-box" },
80
+ h(
81
+ "span",
82
+ { id: "caller", hidden: actingAs === null, title: callerScopes(store.caller) },
83
+ actingAs === null ? "" : `as ${actingAs}`,
84
+ ),
85
+ h(
86
+ "button",
87
+ {
88
+ id: "sign-out",
89
+ hidden: actingAs === null,
90
+ title: "forget the credential: the page goes back to observer",
91
+ onClick: () => {
92
+ if (field.current) field.current.value = "";
93
+ api.setToken("");
94
+ },
95
+ },
96
+ "sign out",
97
+ ),
70
98
  h("input", {
71
99
  id: "token",
100
+ ref: field,
72
101
  type: "password",
73
- placeholder: "instance token",
102
+ hidden: actingAs !== null,
103
+ placeholder: "Caller token",
104
+ title: "a Caller's token — unlocks what its scopes allow (ADR-0032)",
74
105
  autocomplete: "off",
75
106
  defaultValue: initialToken,
76
107
  onChange: (e: Event) => api.setToken((e.currentTarget as HTMLInputElement).value.trim()),
77
108
  }),
109
+ actingAs === null
110
+ ? logins.map((issuer) =>
111
+ h(
112
+ "button",
113
+ {
114
+ key: issuer.issuer,
115
+ class: "login",
116
+ title: `log in at ${issuer.issuer}`,
117
+ onClick: () => api.login(issuer),
118
+ },
119
+ logins.length === 1 ? "log in" : `log in · ${new URL(issuer.issuer).host}`,
120
+ ),
121
+ )
122
+ : null,
78
123
  h("span", { id: "token-state", class: `token-state token-state--${store.token}` }, TOKEN_BADGES[store.token]),
124
+ h("span", { id: "login-note", hidden: loginNote === null, title: loginNote ?? "" }, loginNote ?? ""),
79
125
  ),
80
126
  );
81
127
  }
@@ -0,0 +1,161 @@
1
+ // A kept conversation as the Console's bottom drawer shows it (ADR-0023, ADR-0032): the entries
2
+ // that the conversation routes serve, folded into the items a person reads, and the reader of the
3
+ // `/events` stream. Pure, with no DOM and no fetch, so the test suite reads it as it reads store.ts.
4
+ //
5
+ // An entry is one body of the Harness's print: its label is `kind` and the rest is `text`. The
6
+ // print's cut is the record's cut (ADR-0023), so a Working tool's result is never here: a tool call
7
+ // with no answer is a Working tool's, and a call that an `<name> answer` or `<name> error` entry
8
+ // follows is a Menu pick.
9
+
10
+ /** One kept entry, as `GET /runs/:id/conversations/:iid` gives it — mirrors `ConversationEntry` in
11
+ * conversations.ts. */
12
+ export type ConversationEntry = { id: string; seq: number; at: string; agent: string; kind: string; text: string };
13
+
14
+ /** One kept conversation, as `GET /runs/:id/conversations` lists it (ADR-0023, ADR-0065). `open`
15
+ * says that a Turn of it is registered on the live run. */
16
+ export type ConversationRow = {
17
+ agent: string;
18
+ /** The Agent slot from the run's admission ledger: the address that a Steering message takes. */
19
+ slot?: string;
20
+ instanceId: string;
21
+ open: boolean;
22
+ bytes: number;
23
+ cut: boolean;
24
+ /** The space guard deleted the entries (ADR-0065); the row stays. */
25
+ swept?: string;
26
+ };
27
+
28
+ /** One Agent slot of a run, as `GET /runs/:id/agents` lists it (ADR-0002): whether it has an open
29
+ * Turn, and whether a Caller may steer it. */
30
+ export type AgentLine = { agent: string; open: boolean; steerable: boolean };
31
+
32
+ /** What a person reads of a conversation, one item for each entry, with a tool call and its answer
33
+ * folded into one item. */
34
+ export type ConversationItem =
35
+ | { type: "prompt" | "steering" | "notice"; at: string; text: string }
36
+ | { type: "text" | "thinking"; at: string; text: string }
37
+ | {
38
+ type: "call";
39
+ at: string;
40
+ tool: string;
41
+ input: string;
42
+ /** The pick's answer, or its refusal (`error`). Absent for a Working tool, whose result is
43
+ * never kept, and for a pick that has no answer yet. */
44
+ result?: { text: string; error: boolean };
45
+ }
46
+ | { type: "compacted" | "cut" | "mark"; at: string; label: string; text: string };
47
+
48
+ /** The `kind` of the marker entry that a cut leaves (ADR-0023) — `CUT_KIND` in conversations.ts. */
49
+ const CUT_KIND = "cut";
50
+
51
+ /** The first words of a Steering message's frame (ADR-0002), in each author's form. */
52
+ const STEERING = /^Steering message /;
53
+ /** The lines that a Steering message carries after its frame: the quoted text, and the closing line
54
+ * of a peer's message. */
55
+ const STEERING_MORE = /^(>|End of the message from )/;
56
+ /** The first word of a notice (ADR-0062). */
57
+ const NOTICE = /^Notice: /;
58
+ /** The lines that delimit the Turn part (ADR-0029): alone, they say nothing to a person. */
59
+ const TURN_TAG = /^<\/?jr2-turn>$/;
60
+
61
+ /** Cut a prompt at the lines that jr2 frames: each Steering message and each notice is an item of
62
+ * its own, and the text around them stays a prompt. A part that holds only the Turn part's tags
63
+ * is dropped. */
64
+ function splitPrompt(at: string, text: string): ConversationItem[] {
65
+ const parts: Array<{ type: "prompt" | "steering" | "notice"; lines: string[] }> = [];
66
+ for (const line of text.split("\n")) {
67
+ const last = parts[parts.length - 1];
68
+ const type = STEERING.test(line) ? "steering" : NOTICE.test(line) ? "notice" : null;
69
+ if (type) parts.push({ type, lines: [line] });
70
+ else if (last?.type === "steering" && STEERING_MORE.test(line)) last.lines.push(line);
71
+ else if (last?.type === "prompt") last.lines.push(line);
72
+ else parts.push({ type: "prompt", lines: [line] });
73
+ }
74
+ return parts
75
+ .filter((p) => p.type !== "prompt" || p.lines.some((l) => l.trim() !== "" && !TURN_TAG.test(l)))
76
+ .map((p) => ({ type: p.type, at, text: p.lines.join("\n") }));
77
+ }
78
+
79
+ /**
80
+ * Fold the entries of one conversation into the items a person reads. A call's answer joins the
81
+ * newest call of the same name that has no answer yet; an answer with no call shows as a call
82
+ * with no input. A prompt is cut at each Steering message and each notice that it carries, so each
83
+ * shows as its own item; the frame of each is jr2's own text, so this reads it and changes nothing.
84
+ */
85
+ export function foldEntries(entries: readonly ConversationEntry[]): ConversationItem[] {
86
+ const items: ConversationItem[] = [];
87
+ const waiting = new Map<string, Extract<ConversationItem, { type: "call" }>[]>();
88
+ for (const entry of entries) {
89
+ const { kind, text, at } = entry;
90
+ const answer = /^(.+) (answer|error)$/.exec(kind);
91
+ if (answer) {
92
+ const tool = answer[1]!;
93
+ const result = { text, error: answer[2] === "error" };
94
+ const call = waiting.get(tool)?.pop();
95
+ if (call) call.result = result;
96
+ else items.push({ type: "call", at, tool, input: "", result });
97
+ continue;
98
+ }
99
+ if (kind === "prompt") {
100
+ items.push(...splitPrompt(at, text));
101
+ } else if (kind === "text" || kind === "thinking") {
102
+ items.push({ type: kind, at, text });
103
+ } else if (kind === "compacted" || kind === CUT_KIND) {
104
+ items.push({ type: kind, at, label: kind, text });
105
+ } else if (kind === "line" || kind.includes(" ")) {
106
+ // A body with no label, or a mechanic line such as `[thinking level]`.
107
+ items.push({ type: "mark", at, label: kind === "line" ? "" : kind, text });
108
+ } else {
109
+ const call: Extract<ConversationItem, { type: "call" }> = { type: "call", at, tool: kind, input: text };
110
+ items.push(call);
111
+ waiting.set(kind, [...(waiting.get(kind) ?? []), call]);
112
+ }
113
+ }
114
+ return items;
115
+ }
116
+
117
+ /** Add `more` to `kept`, in sequence order, each sequence number once: a page and the stream can
118
+ * give the same entry, and the Harness sends a batch again after a failed send. */
119
+ export function mergeEntries(
120
+ kept: readonly ConversationEntry[],
121
+ more: readonly ConversationEntry[],
122
+ ): ConversationEntry[] {
123
+ if (!more.length) return kept as ConversationEntry[];
124
+ const last = kept.length ? kept[kept.length - 1]!.seq : -Infinity;
125
+ if (more.every((e, i) => e.seq > last && (i === 0 || e.seq > more[i - 1]!.seq))) return [...kept, ...more];
126
+ const bySeq = new Map<number, ConversationEntry>();
127
+ for (const e of kept) bySeq.set(e.seq, e);
128
+ for (const e of more) if (!bySeq.has(e.seq)) bySeq.set(e.seq, e);
129
+ return [...bySeq.values()].sort((a, b) => a.seq - b.seq);
130
+ }
131
+
132
+ /** One event of a `text/event-stream`: its name (`message` when none) and its data. */
133
+ export type SseEvent = { event: string; data: string };
134
+
135
+ /**
136
+ * Read the complete events out of a stream's text so far, and give back the rest, which a later
137
+ * chunk completes. The Console reads the guarded streams with `fetch`, because an `EventSource`
138
+ * cannot send the credential (ADR-0032). Comments (`: ping`) and fields other than `event` and
139
+ * `data` are ignored.
140
+ */
141
+ export function readSse(buffer: string): { events: SseEvent[]; rest: string } {
142
+ const events: SseEvent[] = [];
143
+ const text = buffer.replace(/\r\n?/g, "\n");
144
+ let start = 0;
145
+ for (let end = text.indexOf("\n\n"); end !== -1; end = text.indexOf("\n\n", start)) {
146
+ const block = text.slice(start, end);
147
+ start = end + 2;
148
+ let event = "message";
149
+ const data: string[] = [];
150
+ for (const line of block.split("\n")) {
151
+ if (line === "" || line.startsWith(":")) continue;
152
+ const colon = line.indexOf(":");
153
+ const field = colon === -1 ? line : line.slice(0, colon);
154
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
155
+ if (field === "event") event = value;
156
+ else if (field === "data") data.push(value);
157
+ }
158
+ if (data.length) events.push({ event, data: data.join("\n") });
159
+ }
160
+ return { events, rest: text.slice(start) };
161
+ }
@@ -0,0 +1,123 @@
1
+ // The Console's login (ADR-0032, ADR-0078): OIDC, the authorization code flow with PKCE, as a public
2
+ // client. The login adds no authenticator — it is a way for a person to get a JWT, which the page
3
+ // then sends as it sends a typed token. The JWT authenticator checks it like any other.
4
+ //
5
+ // What the page needs before the login is open (`GET /console/config`): each issuer that offers a
6
+ // login and its client id. Both are public values: a public client has no secret, and the issuer's
7
+ // URL is in every JWT that it signs.
8
+ //
9
+ // This file is the pure half — no fetch, no storage, no location — so `node:test` checks it. The
10
+ // bootstrap (main.ts) holds the rest: the redirect, the code exchange and sessionStorage. One `.ts`
11
+ // for both consumers (ADR-0034), as store.ts.
12
+
13
+ /** One issuer the Console offers a login against, as `GET /console/config` gives it. */
14
+ export type LoginIssuer = { issuer: string; clientId: string };
15
+
16
+ /** The answer of `GET /console/config`. */
17
+ export type ConsoleConfig = { login: LoginIssuer[] };
18
+
19
+ /** What the page keeps in sessionStorage between the redirect to the issuer and the return. */
20
+ export type PendingLogin = {
21
+ issuer: string;
22
+ clientId: string;
23
+ state: string;
24
+ verifier: string;
25
+ tokenEndpoint: string;
26
+ /** The page address to go back to after the return (the return itself always lands on `/`). */
27
+ returnTo: string;
28
+ };
29
+
30
+ /** The scopes a login asks for, when the issuer supports each: `openid` always, then the claims the
31
+ * JWT authenticator reads by default (`email` for the name jr2 shows, `groups` for the scopes).
32
+ * An issuer that lists its scopes (`scopes_supported`) gets only those it lists, because some
33
+ * providers refuse a request with a scope they do not know. */
34
+ export function loginScopes(supported: readonly string[] | undefined): string {
35
+ const wanted = ["openid", "profile", "email", "groups"];
36
+ return wanted.filter((s) => s === "openid" || !supported || supported.includes(s)).join(" ");
37
+ }
38
+
39
+ /** The one return address of a login: the Console's root. One address, so an issuer needs one
40
+ * redirect URI registered for its client, whatever page the person logged in from. */
41
+ export function redirectUri(origin: string): string {
42
+ return `${origin}/`;
43
+ }
44
+
45
+ /** The issuer's authorization request: the code flow with an S256 PKCE challenge. */
46
+ export function authorizeUrl(opts: {
47
+ endpoint: string;
48
+ clientId: string;
49
+ redirectUri: string;
50
+ scope: string;
51
+ state: string;
52
+ challenge: string;
53
+ }): string {
54
+ const url = new URL(opts.endpoint);
55
+ url.searchParams.set("response_type", "code");
56
+ url.searchParams.set("client_id", opts.clientId);
57
+ url.searchParams.set("redirect_uri", opts.redirectUri);
58
+ url.searchParams.set("scope", opts.scope);
59
+ url.searchParams.set("state", opts.state);
60
+ url.searchParams.set("code_challenge", opts.challenge);
61
+ url.searchParams.set("code_challenge_method", "S256");
62
+ return url.toString();
63
+ }
64
+
65
+ /** What a return from the issuer carries in the address: a code with its state, an error with the
66
+ * issuer's description, or nothing (an ordinary page load). */
67
+ export type LoginReturn = { code: string; state: string } | { error: string; state: string | null } | null;
68
+
69
+ export function loginReturn(search: string): LoginReturn {
70
+ const params = new URLSearchParams(search);
71
+ const state = params.get("state");
72
+ const error = params.get("error");
73
+ if (error) {
74
+ const description = params.get("error_description");
75
+ return { error: description ? `${error}: ${description}` : error, state };
76
+ }
77
+ const code = params.get("code");
78
+ if (code && state) return { code, state };
79
+ return null;
80
+ }
81
+
82
+ /** The body of the token request that exchanges the code (form-encoded, no client secret). */
83
+ export function tokenRequestBody(opts: {
84
+ code: string;
85
+ redirectUri: string;
86
+ clientId: string;
87
+ verifier: string;
88
+ }): string {
89
+ return new URLSearchParams({
90
+ grant_type: "authorization_code",
91
+ code: opts.code,
92
+ redirect_uri: opts.redirectUri,
93
+ client_id: opts.clientId,
94
+ code_verifier: opts.verifier,
95
+ }).toString();
96
+ }
97
+
98
+ /** The credential out of the token answer: the ID token. Its audience is the client id, which is
99
+ * the `audience` that the JWT authenticator checks; an access token's audience and form differ
100
+ * from one provider to the next. */
101
+ export function credentialOf(answer: unknown): string | null {
102
+ const idToken = (answer as { id_token?: unknown } | null)?.id_token;
103
+ return typeof idToken === "string" && idToken.length > 0 ? idToken : null;
104
+ }
105
+
106
+ /** base64url with no padding, as PKCE and `state` use it. */
107
+ export function base64url(bytes: Uint8Array): string {
108
+ let text = "";
109
+ for (const b of bytes) text += String.fromCharCode(b);
110
+ return btoa(text).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
111
+ }
112
+
113
+ /** A random value for a PKCE verifier or a `state`: 32 bytes, 43 characters. */
114
+ export function randomValue(): string {
115
+ return base64url(crypto.getRandomValues(new Uint8Array(32)));
116
+ }
117
+
118
+ /** The S256 challenge of a verifier (RFC 7636): base64url(sha-256(verifier)). `crypto.subtle` is
119
+ * only in a secure context, so a login needs the Console on https or on localhost. */
120
+ export async function pkceChallenge(verifier: string): Promise<string> {
121
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
122
+ return base64url(new Uint8Array(digest));
123
+ }