@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
package/src/effect.ts CHANGED
@@ -39,7 +39,7 @@ import { randomUUID } from "node:crypto";
39
39
  import type { ActorLogic, AnyActorRef, EventObject, Snapshot } from "xstate";
40
40
  import { ambientHandlesFor, type AmbientHandles } from "./ambient.ts";
41
41
  import { boundRun, type AnyActorSystem } from "./registration.ts";
42
- import type { HttpMethod, HttpOutput, TextPart, WireAuth } from "./effector-wire.ts";
42
+ import type { HttpMethod, HttpOutput, McpOutput, TextPart, WireAuth } from "./effector-wire.ts";
43
43
  import type { ExecOutcome, ExecRead, ExecRequest, SyncAnswer } from "./wire.ts";
44
44
  import { bindOpenSecretIn, type EffectorPortFactory, type OpenSecretIn } from "./http-effect.ts";
45
45
  import { commandEnvParts, credentialedCommandWith, sandboxEffectorPort, type CommandEnv } from "./sandbox-effect.ts";
@@ -240,6 +240,51 @@ export type DispatchParts = {
240
240
  readonly openSecret: boolean;
241
241
  };
242
242
 
243
+ /** What an `mcp` Effect asks of its server (ADR-0079): call a tool, read a resource, or get a
244
+ * prompt. */
245
+ export type McpOp = "call" | "read" | "prompt";
246
+
247
+ /**
248
+ * An `mcp` Effect's plain data (ADR-0067, ADR-0079): what `jr2 up` and the Machine doc read — the
249
+ * operation, the server by its name in the `mcp` block of `jr2.config.ts`, the tool (a call), the
250
+ * resource's uri (a read) or the prompt's name (a prompt), the input's keys, the author's timeout,
251
+ * the bound on the output, and whether a restore may send it again. The server's url and secret
252
+ * are the config's, never a part: `jr2 up` reads them from there, and the Instance Effector holds
253
+ * them in its `effector.json` and its secrets.
254
+ */
255
+ export type McpParts = {
256
+ readonly kind: "mcp";
257
+ readonly op: McpOp;
258
+ readonly server: string;
259
+ /** `mcp.call`'s tool. */
260
+ readonly tool?: string;
261
+ /** `mcp.read`'s resource uri. */
262
+ readonly uri?: string;
263
+ /** `mcp.prompt`'s prompt name. */
264
+ readonly prompt?: string;
265
+ /** The keys of the input schema: what a pick param may fill (ADR-0067's load walk reads them). */
266
+ readonly inputKeys: readonly string[];
267
+ /** The author's timeout, in milliseconds. Absent: the server's `timeout` in `jr2.config.ts`. */
268
+ readonly timeoutMs?: number;
269
+ /** The most bytes of `text`, and of `structured` as JSON, in the output. */
270
+ readonly maxBody: number;
271
+ /** Marked by the author: a restore that cannot find the Effect sends it again. The server's
272
+ * `idempotentHint` is not read (ADR-0079). */
273
+ readonly idempotent: boolean;
274
+ /** Always empty: an `mcp` Effect names no secret of its own. Its server's secret is placed in
275
+ * the Instance Effector by `jr2 up`, from the config (ADR-0079). */
276
+ readonly secrets: readonly string[];
277
+ /** Never: the server, not a `secret(open)`, is what a package leaves Open (ADR-0054). */
278
+ readonly openSecret?: never;
279
+ };
280
+
281
+ /** An `mcp` Effect whose server or tool a package left Open (ADR-0054, ADR-0079): the sentinel in
282
+ * that place, until the composer binds it with `customize()`. */
283
+ export type OpenMcpParts = Omit<McpParts, "server" | "tool"> & {
284
+ readonly server: string | typeof open;
285
+ readonly tool?: string | typeof open;
286
+ };
287
+
243
288
  /**
244
289
  * An Effect whose url or argv a package left Open (ADR-0054): the same parts with the sentinel in
245
290
  * that one place. The kind, the placement, the timeout and the secrets the other parts name are
@@ -264,10 +309,12 @@ export type EffectParts =
264
309
  | OpenCommandParts
265
310
  | OpenHttpParts
266
311
  | DispatchParts
267
- | OpenDispatchParts;
312
+ | OpenDispatchParts
313
+ | McpParts
314
+ | OpenMcpParts;
268
315
 
269
316
  /** The kinds, as their parts name them. */
270
- export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push", "dispatch"]);
317
+ export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push", "dispatch", "mcp"]);
271
318
 
272
319
  /**
273
320
  * Where an Effect runs, derived from its kind and what it names, never authored (ADR-0067,
@@ -275,7 +322,8 @@ export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "pu
275
322
  * `http` Effect with no Worktree file in the Instance Effector; an `http` whose body is a Worktree
276
323
  * file, a `push` and a `command` that names a secret in the Sandbox Effector of its Workspace; a
277
324
  * `dispatch` with no target in the Orchestrator's own process, and one with a target (an Open one
278
- * included: it has one once bound) in the Instance Effector (ADR-0071).
325
+ * included: it has one once bound) in the Instance Effector (ADR-0071); an `mcp` Effect in the
326
+ * Instance Effector, which calls remote servers only (ADR-0079).
279
327
  */
280
328
  export type Placement = "harness" | "instance" | "sandbox" | "orchestrator";
281
329
  export function placementOf(parts: EffectParts): Placement {
@@ -286,6 +334,8 @@ export function placementOf(parts: EffectParts): Placement {
286
334
  return parts.file ? "sandbox" : "instance";
287
335
  case "push":
288
336
  return "sandbox";
337
+ case "mcp":
338
+ return "instance";
289
339
  case "command":
290
340
  return (parts.secrets?.length ?? 0) > 0 || parts.openSecret ? "sandbox" : "harness";
291
341
  }
@@ -338,7 +388,7 @@ export function inputShapeOf(parts: EffectParts): InputShape | undefined {
338
388
  if (parts.kind === "dispatch") {
339
389
  return parts.inputKeys === undefined ? undefined : { required: [...parts.inputKeys], optional: [] };
340
390
  }
341
- if (parts.kind === "push") return { required: [...parts.inputKeys], optional: [] };
391
+ if (parts.kind === "push" || parts.kind === "mcp") return { required: [...parts.inputKeys], optional: [] };
342
392
  if (parts.kind === "http") {
343
393
  return { required: [...parts.inputKeys], optional: parts.file ? ["worktree"] : [] };
344
394
  }
@@ -492,7 +542,7 @@ export type EffectRow =
492
542
  | { state: "intent"; kind: "command"; slot: string; endpoint: string; placement: string; at: string }
493
543
  /** An http Effect's intent names its placement and no replica (ADR-0068): a restore asks every
494
544
  * replica. `idempotent` is the author's mark, which the restore rule reads. */
495
- | { state: "intent"; kind: "http"; slot: string; placement: "instance"; idempotent: boolean; at: string }
545
+ | { state: "intent"; kind: "http" | "mcp"; slot: string; placement: "instance"; idempotent: boolean; at: string }
496
546
  /** An Effect of the Sandbox Effector (ADR-0067): its Sandbox, and — for a push or a credentialed
497
547
  * command — the commit it runs against, written once the start's answer names it. A restore
498
548
  * re-attaches by id, or sends a push or a credentialed command again with only that commit, and
@@ -590,6 +640,9 @@ const TAILS: Record<EffectRow["kind"], readonly string[]> = {
590
640
  // settled marker carries both whole. The row keeps the outcome, the run id, its status and the
591
641
  // door's HTTP status (ADR-0071).
592
642
  dispatch: ["output", "report"],
643
+ // An mcp answer's text and its structured value: the feed's settled marker carries both whole.
644
+ // The row keeps the outcome, `isError` and `truncated` (ADR-0079).
645
+ mcp: ["text", "structured"],
593
646
  };
594
647
 
595
648
  /** A kind's tail for one output. The NEWEST push of its Repo Slot whose sync did not finish (`busy`,
@@ -702,7 +755,7 @@ export type EffectMarker = {
702
755
  effect: string;
703
756
  stage: "started" | "output" | "settled" | "aborted";
704
757
  text?: string;
705
- output?: CommandOutput | WorktreeOutput | HttpOutput | DispatchOutput;
758
+ output?: CommandOutput | WorktreeOutput | HttpOutput | DispatchOutput | McpOutput;
706
759
  };
707
760
 
708
761
  // --- The port -----------------------------------------------------------------------------------
@@ -182,37 +182,90 @@ export const DispatchedByShape = z.strictObject({
182
182
  * A `dispatch` start (ADR-0071), which only the Instance Effector serves: a run of `workflow` on the
183
183
  * Orchestrator at `url` (its origin, one text part), the body `input` whole, the Instance token as a
184
184
  * bearer by name, `dispatchedBy` as the `jr2-dispatched-by` header — `instance` set, since the
185
- * caller is foreign by construction — and, with `wait`, a poll of `GET /runs/<id>` until the run
185
+ * caller is foreign by construction — `onBehalfOf` as the `jr2-on-behalf-of` header, and, with
186
+ * `wait`, a poll of `GET /runs/<id>` until the run
186
187
  * ends or `timeoutMs` passes.
188
+ *
189
+ * `keyOnly` in place of `input`: a key-only ask, sent when the slot's schema refuses the body. The
190
+ * Effector sends the `jr2-key-only` header and no body; the door answers the run under the key, or
191
+ * `{ existing: false }`, which the Effector settles as `absent`. One of the two is set, never both.
187
192
  */
188
- export const DispatchStartShape = z.strictObject({
189
- kind: z.literal("dispatch"),
190
- url: TextShape.refine(
191
- (parts) => parts.length === 1 && "text" in parts[0]! && ORIGIN.test(parts[0].text),
192
- "a dispatch url is one text part, an origin: http(s)://host[:port]",
193
- ),
194
- workflow: z.string().regex(WORKFLOW_NAME),
195
- input: z.record(z.string(), z.unknown()),
196
- auth: z.strictObject({ bearer: SecretRefShape }),
197
- // Every id non-empty, as the door's header shape requires (http.ts): an empty one would be the
198
- // door's 400, which reads as a defect of the input. The effect is a ledger row id, the key. The Go
199
- // side's `validateDispatch` holds the same rule.
200
- dispatchedBy: DispatchedByShape.extend({
201
- run: z.string().min(1),
202
- slot: z.string().min(1),
203
- effect: z
193
+ export const DispatchStartShape = z
194
+ .strictObject({
195
+ kind: z.literal("dispatch"),
196
+ url: TextShape.refine(
197
+ (parts) => parts.length === 1 && "text" in parts[0]! && ORIGIN.test(parts[0].text),
198
+ "a dispatch url is one text part, an origin: http(s)://host[:port]",
199
+ ),
200
+ workflow: z.string().regex(WORKFLOW_NAME),
201
+ input: z.record(z.string(), z.unknown()).optional(),
202
+ keyOnly: z.literal(true).optional(),
203
+ auth: z.strictObject({ bearer: SecretRefShape }),
204
+ // Every id non-empty, as the door's header shape requires (http.ts): an empty one would be the
205
+ // door's 400, which reads as a defect of the input. The effect is a ledger row id, the key. The Go
206
+ // side's `validateDispatch` holds the same rule.
207
+ dispatchedBy: DispatchedByShape.extend({
208
+ run: z.string().min(1),
209
+ slot: z.string().min(1),
210
+ effect: z
211
+ .string()
212
+ .regex(/^[A-Za-z0-9._-]{1,128}$/)
213
+ .refine((id) => id !== "." && id !== ".."),
214
+ instance: z.string().min(1),
215
+ }),
216
+ wait: z.boolean(),
217
+ timeoutMs: z.number().int().positive(),
218
+ // The first Caller of the calling run's chain (ADR-0071, ADR-0078), sent as the
219
+ // `jr2-on-behalf-of` header: printable ASCII, as a header value is, and at most what the door
220
+ // takes. The other Instance records it and does not check it.
221
+ onBehalfOf: z
204
222
  .string()
205
- .regex(/^[A-Za-z0-9._-]{1,128}$/)
206
- .refine((id) => id !== "." && id !== ".."),
207
- instance: z.string().min(1),
208
- }),
209
- wait: z.boolean(),
210
- timeoutMs: z.number().int().positive(),
211
- });
223
+ .regex(/^[\x20-\x7e]{1,256}$/)
224
+ .optional(),
225
+ })
226
+ .refine(
227
+ (req) => (req.input === undefined) !== (req.keyOnly === undefined),
228
+ "a dispatch start carries its input, or keyOnly and no input",
229
+ );
212
230
  export type DispatchStartRequest = z.infer<typeof DispatchStartShape>;
213
231
 
214
- /** Every start: the http kinds, a push, a credentialed command, a dispatch. */
215
- export const StartRequestShape = z.union([HttpStartShape, PushStartShape, CommandStartShape, DispatchStartShape]);
232
+ /** An MCP server's name (ADR-0079), as the `mcp` block of `jr2.config.ts` keys it. The Go side's
233
+ * `mcpServer` is the same rule. */
234
+ export const MCP_SERVER = /^[a-z][a-z0-9-]*$/;
235
+
236
+ /**
237
+ * An `mcp` start (ADR-0067, ADR-0079), which only the Instance Effector serves: the server by its
238
+ * name — the Effector reads its url and the name of its secret from `effector.json`, so neither is
239
+ * on the wire — and one request: `call` a tool with its arguments, `read` a resource, or get a
240
+ * `prompt` with its arguments. `timeoutMs` is the author's; absent, the server's `timeout` from
241
+ * `effector.json` applies. `maxBody` bounds the answer's `text` and its `structured` value.
242
+ */
243
+ export const McpStartShape = z.strictObject({
244
+ kind: z.literal("mcp"),
245
+ mcp: z
246
+ .strictObject({
247
+ server: z.string().regex(MCP_SERVER),
248
+ call: z.strictObject({ tool: z.string().min(1), arguments: z.record(z.string(), z.unknown()) }).optional(),
249
+ read: z.strictObject({ uri: z.string().min(1) }).optional(),
250
+ prompt: z.strictObject({ name: z.string().min(1), arguments: z.record(z.string(), z.string()) }).optional(),
251
+ })
252
+ .refine(
253
+ (m) => [m.call, m.read, m.prompt].filter((x) => x !== undefined).length === 1,
254
+ "an mcp start carries exactly one of call, read, prompt",
255
+ ),
256
+ timeoutMs: z.number().int().positive().optional(),
257
+ maxBody: z.number().int().positive(),
258
+ });
259
+ export type McpStartRequest = z.infer<typeof McpStartShape>;
260
+
261
+ /** Every start: the http kinds, a push, a credentialed command, a dispatch, an mcp request. */
262
+ export const StartRequestShape = z.union([
263
+ HttpStartShape,
264
+ PushStartShape,
265
+ CommandStartShape,
266
+ DispatchStartShape,
267
+ McpStartShape,
268
+ ]);
216
269
  export type StartRequest = z.infer<typeof StartRequestShape>;
217
270
 
218
271
  /** The most bytes of a start's body the Effector reads (its `maxRequestBody`). A longer body
@@ -379,9 +432,92 @@ export const DispatchOutputShape = z.discriminatedUnion("outcome", [
379
432
  z.strictObject({ outcome: z.literal("refused"), status: z.number().int(), report: z.array(z.string()) }),
380
433
  z.strictObject({ outcome: z.literal("unreachable"), reason: z.string() }),
381
434
  z.strictObject({ outcome: z.literal("unknown"), reason: z.string() }),
435
+ // The answer to a key-only ask when no run has the key (ADR-0071): nothing was started. Wire
436
+ // only: the driver makes it the invoke's error, the slot's schema refusal, and the Machine never
437
+ // reads it.
438
+ z.strictObject({ outcome: z.literal("absent") }),
382
439
  ]);
383
440
  export type DispatchWireOutput = z.infer<typeof DispatchOutputShape>;
384
441
 
442
+ /**
443
+ * An `mcp` Effect's output on the wire (ADR-0067, ADR-0079): `returned`, with `isError` for a call
444
+ * alone; or a reason for `refused`, `unreachable`, `input-required` and `unknown`.
445
+ */
446
+ export const McpOutputShape = z.union([
447
+ z.strictObject({
448
+ outcome: z.literal("returned"),
449
+ isError: z.boolean().optional(),
450
+ text: z.string(),
451
+ structured: z.unknown().optional(),
452
+ truncated: z.boolean().optional(),
453
+ }),
454
+ z.strictObject({
455
+ outcome: z.enum(["refused", "unreachable", "input-required", "unknown"]),
456
+ reason: z.string(),
457
+ }),
458
+ ]);
459
+
460
+ /** The outcomes that an `mcp` Effect gives besides `returned`. Each one has a `reason` in words. */
461
+ type McpNotReturned =
462
+ | { outcome: "refused"; reason: string }
463
+ | { outcome: "unreachable"; reason: string }
464
+ | { outcome: "input-required"; reason: string }
465
+ | { outcome: "unknown"; reason: string }
466
+ | LaterOutcome;
467
+
468
+ /**
469
+ * The output of an `mcp.call` Effect: what the `onDone` of its state gets as `event.output`. Every
470
+ * outcome is output. Route on `outcome`:
471
+ *
472
+ * - `returned`: the server answered the call. `isError` is the tool's own `isError`: the tool ran
473
+ * and says that it failed. `text` joins the text of the result's content; a part that is not text
474
+ * is one line that names its type. `structured` is the result's `structuredContent`, when it has
475
+ * one. `text` and `structured` each have the byte bound of the Effect's `maxBody`: past it, jr2
476
+ * cuts `text`, drops `structured`, and sets `truncated`.
477
+ * - `refused`: the request did not run. The server answered an HTTP 3xx or 4xx status (a 401 or a
478
+ * 403 included) at any step, the handshake too; it refused the handshake; or it answered the
479
+ * request with the JSON-RPC error `-32700`, `-32600`, `-32601` or `-32602`: a request that it
480
+ * could not read, an unknown method, an unknown tool, or bad arguments; or it answered a read with
481
+ * `-32002`: the resource does not exist. It is also the Effector's
482
+ * own refusal, with nothing sent: the server or its secret is not in its `effector.json`.
483
+ * - `unreachable`: nothing of the request was sent: the name did not resolve, or the connection,
484
+ * TLS or the handshake failed otherwise (a 5xx, the timeout, an answer that is not MCP's, a
485
+ * revision of MCP that jr2 does not speak).
486
+ * - `input-required`: the server asked for input, which jr2 does not give. After the error
487
+ * `-32042`, the tool did not run. After an `elicitation/create` request, which comes while the
488
+ * tool runs, the tool may have begun. jr2 does not send the request again: a Machine that
489
+ * invokes the Effect again after a Gate can run a part of the tool two times.
490
+ * - `unknown`: the request was sent and no answer came (the timeout, a reset, a stream that ended
491
+ * first), or the server answered with a 5xx or another JSON-RPC error. The tool may have acted.
492
+ * jr2 does not send it again unless it is marked `idempotent`.
493
+ *
494
+ * jr2 replaces each value of an Effect secret that the Effector holds with `[secret:<name>]`, in
495
+ * every string of the output. The union is open ({@link LaterOutcome}): give the rest a last arm
496
+ * with no guard.
497
+ *
498
+ * @see docs/concepts/effects.md
499
+ * @see docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md
500
+ */
501
+ export type McpCallOutput =
502
+ | { outcome: "returned"; isError: boolean; text: string; structured?: unknown; truncated?: boolean }
503
+ | McpNotReturned;
504
+
505
+ /**
506
+ * The output of an `mcp.read` or an `mcp.prompt` Effect. Its outcomes are those of
507
+ * {@link McpCallOutput}, with no `isError`. For a read, `text` joins the text of the resource's
508
+ * contents, and `structured` is the `contents` list: a blob is given only as its `mimeType` and its
509
+ * size. For a prompt, `text` joins the text of the prompt's messages, and `structured` is the
510
+ * `messages` list. Past the bound, `structured` is dropped and `truncated` is set.
511
+ *
512
+ * @see docs/concepts/effects.md
513
+ */
514
+ export type McpReadOutput =
515
+ | { outcome: "returned"; text: string; structured?: unknown; truncated?: boolean }
516
+ | McpNotReturned;
517
+
518
+ /** The output of any `mcp` Effect: a call's, a read's or a prompt's. */
519
+ export type McpOutput = McpCallOutput | McpReadOutput;
520
+
385
521
  /** The replica that holds an Effect, and where to poll it. */
386
522
  export const ReplicaShape = z.strictObject({ name: z.string(), address: z.string() });
387
523
  export type Replica = z.infer<typeof ReplicaShape>;
@@ -397,13 +533,122 @@ export const EffectStateShape = z.strictObject({
397
533
  /** The run a dispatch started, once the door answered and while its wait runs: the Orchestrator
398
534
  * writes it to the ledger's intent row, so the caller names its callee during the wait (ADR-0071). */
399
535
  run: z.string().optional(),
400
- output: z.union([HttpOutputShape, PushOutputShape, CommandOutputShape, DispatchOutputShape]).optional(),
536
+ output: z
537
+ .union([HttpOutputShape, PushOutputShape, CommandOutputShape, DispatchOutputShape, McpOutputShape])
538
+ .optional(),
401
539
  });
402
540
  export type EffectState = z.infer<typeof EffectStateShape>;
403
541
 
404
542
  /** A refusal's body: the reason, and the replica when it names one. */
405
543
  export const RefusalShape = z.strictObject({ error: z.string(), replica: ReplicaShape.optional() });
406
544
 
545
+ // --- The `mac` check (POST /mac, ADR-0068, ADR-0078) ---------------------------------------------
546
+ //
547
+ // A Hook's `mac` verifier: the Orchestrator builds the bytes that the service signed, and the
548
+ // Instance Effector, which holds the secret, computes the HMAC over them and compares it with the
549
+ // signature in constant time. The answer is "match" or "no match", and nothing more: no HMAC and no
550
+ // secret ever comes back. The Orchestrator makes every check that needs no secret (the window)
551
+ // before it asks.
552
+
553
+ /** The HMAC algorithms of a `mac` check. */
554
+ export const MAC_ALGS = ["sha256", "sha1", "sha512"] as const;
555
+
556
+ /** A `mac` check: the NAME of the secret, the algorithm, the signed bytes and the signature, each
557
+ * in base64. */
558
+ export const MacRequestShape = z.strictObject({
559
+ // A secret's name, as the Effector reads it: a file under its secrets mount.
560
+ secret: z
561
+ .string()
562
+ .regex(/^[A-Za-z0-9._-]{1,253}$/)
563
+ .refine((name) => !name.startsWith("."), "a secret's name does not start with ."),
564
+ alg: z.enum(MAC_ALGS),
565
+ bytes: z.base64(),
566
+ signature: z.base64(),
567
+ });
568
+ export type MacRequest = z.infer<typeof MacRequestShape>;
569
+
570
+ /** The answer to a `mac` check: whether the HMAC of the bytes with the secret is the signature. */
571
+ export const MacAnswerShape = z.strictObject({ match: z.boolean() });
572
+ export type MacAnswer = z.infer<typeof MacAnswerShape>;
573
+
574
+ /** The path of a `mac` check on the Instance Effector. */
575
+ export const MAC_PATH = "/mac";
576
+
577
+ /** The `mac` check, as the Hook's door asks it: `true` for a match. It rejects when no Effector can
578
+ * answer, and the door then answers 503. */
579
+ export type HookMac = (req: {
580
+ secret: string;
581
+ alg: (typeof MAC_ALGS)[number];
582
+ bytes: Uint8Array;
583
+ signature: Uint8Array;
584
+ }) => Promise<boolean>;
585
+
586
+ /**
587
+ * The `mac` check over the Instance Effector's signed wire (ADR-0068): one replica, any one, since
588
+ * each holds the same secrets. A replica that was never reached is passed over for the next. An
589
+ * answer is trusted only with the Effector's signature, as an Effect's is.
590
+ */
591
+ export function createMacClient(opts: {
592
+ key: Buffer | string;
593
+ transport: EffectorTransport;
594
+ now?: () => number;
595
+ }): HookMac {
596
+ const now = opts.now ?? Date.now;
597
+ return async ({ secret, alg, bytes, signature }) => {
598
+ const body = JSON.stringify({
599
+ secret,
600
+ alg,
601
+ bytes: Buffer.from(bytes).toString("base64"),
602
+ signature: Buffer.from(signature).toString("base64"),
603
+ } satisfies MacRequest);
604
+ const addresses = shuffle(await opts.transport.replicas().catch(notSent));
605
+ if (addresses.length === 0) throw new EffectorNotSent("no Effector replica is serving");
606
+ let last: unknown;
607
+ for (const address of addresses) {
608
+ const time = String(now());
609
+ const nonce = randomBytes(16).toString("hex");
610
+ const headers = {
611
+ "content-type": "application/json",
612
+ [HEADER_TIME]: time,
613
+ [HEADER_NONCE]: nonce,
614
+ [HEADER_SIGNATURE]: requestMac(opts.key, "POST", address, MAC_PATH, time, nonce, body),
615
+ };
616
+ let answer: RawAnswer;
617
+ try {
618
+ answer = await opts.transport.send(address, "POST", MAC_PATH, headers, body);
619
+ } catch (err) {
620
+ if (neverReached(err)) {
621
+ last = err;
622
+ continue;
623
+ }
624
+ throw err;
625
+ }
626
+ const want = responseMac(opts.key, answer.status, "POST", MAC_PATH, nonce, answer.body);
627
+ if (!sameString(answer.headers[HEADER_SIGNATURE] ?? "", want)) {
628
+ throw new EffectorForged(
629
+ `the answer to POST ${MAC_PATH} from ${address} does not carry the Effector's signature`,
630
+ );
631
+ }
632
+ if (answer.status === 503) {
633
+ last = new EffectorNotSent(`the Effector replica at ${address} is stopping`);
634
+ continue;
635
+ }
636
+ if (answer.status !== 200) {
637
+ const refusal = RefusalShape.safeParse(parseJson(answer.body));
638
+ throw new Error(
639
+ `the Effector answered a mac check with ${answer.status}: ${refusal.success ? refusal.data.error : "no reason"}`,
640
+ );
641
+ }
642
+ const parsed = MacAnswerShape.safeParse(parseJson(answer.body));
643
+ if (!parsed.success) throw new EffectorMalformed("the Effector's answer to a mac check is not the wire's shape");
644
+ return parsed.data.match;
645
+ }
646
+ throw last instanceof EffectorNotSent
647
+ ? last
648
+ : new EffectorNotSent(`no Effector replica answered: ${(last as Error | undefined)?.message ?? "none"}`);
649
+ };
650
+ }
651
+
407
652
  // --- Signatures ----------------------------------------------------------------------------------
408
653
 
409
654
  export const HEADER_TIME = "x-jr2-time";
@@ -0,0 +1,16 @@
1
+ // The brand of a Gate slot (ADR-0011), alone in its module: the walk (setup.ts, party.ts,
2
+ // effect-checks.ts) and the Chart door read it, and gate.ts imports the registration table, which
3
+ // imports party.ts. A leaf with no imports lets each of them ask without a cycle.
4
+
5
+ /** Is this actor logic a Gate slot (`gate({ … })`)? A duck test on the brand, as `isAgent` is. */
6
+ export function isGateSlotLogic(logic: unknown): boolean {
7
+ if (typeof logic !== "object" || logic === null) return false;
8
+ const parts = (logic as { gateSlot?: { delivers?: unknown } }).gateSlot;
9
+ return typeof parts === "object" && parts !== null && Array.isArray(parts.delivers);
10
+ }
11
+
12
+ /** Does this invoke run a Gate: the plain `gate`, or a Gate slot? */
13
+ export function isGateInvoke(inv: { src?: unknown } | undefined, actors: Record<string, unknown>): boolean {
14
+ if (inv?.src === "gate") return true;
15
+ return typeof inv?.src === "string" && isGateSlotLogic(actors[inv.src]);
16
+ }