@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.
- package/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +86 -9
- package/docs/concepts/architecture.md +41 -28
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +7 -5
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +4 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +50 -22
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +5 -4
- package/docs/concepts/pools-and-sources.md +187 -11
- package/docs/concepts/secrets-and-trust.md +73 -33
- package/docs/concepts/the-menu.md +11 -0
- package/docs/concepts/workspaces.md +6 -5
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/guides/add-tools-to-the-sandbox.md +55 -11
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +24 -8
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/fix-a-failed-converge.md +18 -6
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +9 -6
- package/docs/guides/look-inside-a-sandbox.md +101 -31
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-one-worker-per-item.md +15 -9
- package/docs/guides/set-an-agents-access-and-dials.md +2 -0
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +1 -1
- package/docs/guides/watch-a-run.md +127 -121
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +17 -7
- package/docs/llms.txt +81 -15
- package/docs/reference/authoring-surface/agent.md +1 -1
- package/docs/reference/authoring-surface/answer.md +1 -1
- package/docs/reference/authoring-surface/arg.md +1 -1
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +1 -1
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +1 -1
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +1 -1
- package/docs/reference/authoring-surface/file.md +1 -1
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +1 -1
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +1 -1
- package/docs/reference/authoring-surface/jr2setup.md +13 -3
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +5 -3
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +1 -1
- package/docs/reference/authoring-surface/pool.md +77 -9
- package/docs/reference/authoring-surface/push.md +1 -1
- package/docs/reference/authoring-surface/readyof.md +1 -1
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +1 -1
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +56 -1
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +14 -1
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
- package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-binding.md +1 -1
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandoutput.md +1 -1
- package/docs/reference/authoring-surface/type-customize.md +16 -1
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +1 -1
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-framepath.md +1 -1
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +1 -1
- package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +1 -1
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +16 -7
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +1 -1
- package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
- package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
- package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
- package/docs/reference/authoring-surface/workspace.md +1 -1
- package/docs/reference/authoring-surface/worktree.md +1 -1
- package/docs/reference/authoring-surface.md +88 -167
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +235 -2
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
- package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +9 -2
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +18 -14
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +32 -18
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +116 -34
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +7 -6
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -18
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +4 -3
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +5 -4
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +7 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- 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
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +17 -13
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- 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
- package/docs/reference/decisions.md +6 -4
- package/docs/reference/glossary.md +96 -42
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +976 -149
- package/docs/why.md +2 -1
- package/package.json +10 -3
- package/src/actor.ts +36 -10
- package/src/agent.ts +50 -0
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +52 -7
- package/src/config.ts +550 -1
- package/src/conversations.ts +453 -0
- package/src/custodian.ts +8 -2
- package/src/customize.ts +143 -4
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +2 -1
- package/src/effect.ts +60 -7
- package/src/effector-wire.ts +272 -27
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +10 -4
- package/src/held-secrets.ts +72 -23
- package/src/holds.ts +2 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +76 -38
- package/src/http.ts +889 -94
- package/src/index.ts +16 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +22 -3
- package/src/machine-doc.ts +26 -1
- package/src/mcp-effect.ts +424 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +64 -2
- package/src/notices.ts +138 -10
- package/src/open-body.ts +25 -11
- package/src/open-effect.ts +17 -4
- package/src/open.ts +4 -2
- package/src/parts.ts +87 -9
- package/src/party.ts +2 -1
- package/src/pick.ts +2 -2
- package/src/pool.ts +1142 -163
- package/src/registration.ts +203 -13
- package/src/run-host.ts +1509 -171
- package/src/run-list.ts +191 -0
- package/src/sandbox-kube.ts +53 -5
- package/src/secret-placement.ts +239 -33
- package/src/server.ts +53 -4
- package/src/setup.ts +72 -32
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +263 -22
- package/src/wire.ts +17 -0
- package/src/workspace.ts +50 -6
- package/src/worktree.ts +3 -3
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Write a Hook kind
|
|
3
|
+
description: Write the kind of a service that jr2 ships no kind for, declare a Hook of it, and hear it at a Gate.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Write a Hook kind
|
|
7
|
+
|
|
8
|
+
Write a [Hook kind](../reference/glossary.md#hook-kind) for an outside service that jr2 ships no kind for, and that
|
|
9
|
+
[`webhook()`](../reference/hooks.md#webhook) cannot read.
|
|
10
|
+
|
|
11
|
+
`webhook()` reads a service that sends JSON and signs only the raw body. Write a kind with
|
|
12
|
+
[`defineHookKind()`](../reference/hooks.md#definehookkind) when the service does one of these:
|
|
13
|
+
|
|
14
|
+
- It signs more than the body, such as a time and the body.
|
|
15
|
+
- It sends a body that is not JSON, such as a form.
|
|
16
|
+
- It sends a handshake that needs an answer.
|
|
17
|
+
- A person reads the answer, as in a chat command.
|
|
18
|
+
|
|
19
|
+
The example is a kind for Stripe. Stripe signs `<t>.<raw body>` with the signing secret of the endpoint, and sends the
|
|
20
|
+
time `t` and the signature in the header `Stripe-Signature: t=<seconds>,v1=<hex>`. The type of the event is the event
|
|
21
|
+
key, and the id of the event is the id of the delivery.
|
|
22
|
+
|
|
23
|
+
The interface of a kind is `@jr2/orchestrator/hooks`. It is part of `@jr2/orchestrator`, so your
|
|
24
|
+
[Instance](../reference/glossary.md#instance) installs nothing more.
|
|
25
|
+
|
|
26
|
+
## Write the kind
|
|
27
|
+
|
|
28
|
+
Save the kind as `hooks/stripe.ts`, beside `workflows/`. A kind is not a Workflow, so it does not go in `workflows/`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import { defineHookKind, json } from "@jr2/orchestrator/hooks";
|
|
33
|
+
|
|
34
|
+
// The payload of each event key that a handler hears. Name only the fields that the handlers read:
|
|
35
|
+
// a loose object keeps the other fields, so a new field from Stripe breaks nothing.
|
|
36
|
+
const invoice = z.looseObject({
|
|
37
|
+
data: z.looseObject({ object: z.looseObject({ id: z.string(), customer: z.string() }) }),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const stripeEvents = {
|
|
41
|
+
"invoice.paid": invoice,
|
|
42
|
+
"invoice.payment_failed": invoice,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const stripe = defineHookKind({
|
|
46
|
+
api: 1,
|
|
47
|
+
id: "stripe",
|
|
48
|
+
events: stripeEvents,
|
|
49
|
+
verify: {
|
|
50
|
+
mac: {
|
|
51
|
+
alg: "sha256",
|
|
52
|
+
// Stripe-Signature: t=<seconds>,v1=<hex>. Stripe signs "<t>.<raw body>". The door checks t
|
|
53
|
+
// against its clock, and the Instance Effector checks the HMAC.
|
|
54
|
+
signed: (req) => {
|
|
55
|
+
const fields = (req.headers["stripe-signature"] ?? "").split(",").map((f) => f.split("="));
|
|
56
|
+
const t = fields.find(([name]) => name === "t")?.[1];
|
|
57
|
+
const v1 = fields.find(([name]) => name === "v1")?.[1];
|
|
58
|
+
if (!t || !/^\d+$/.test(t) || !v1) return { unsigned: "no t and v1 in the Stripe-Signature header" };
|
|
59
|
+
return { bytes: [t, ".", req.body], signature: { hex: v1 }, at: Number(t) };
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
// The event's type is the event key, and the event's id is the id of the delivery.
|
|
64
|
+
read: (req) => {
|
|
65
|
+
const event = json(req) as { readonly id?: unknown; readonly type?: unknown } | null;
|
|
66
|
+
if (typeof event?.id !== "string" || typeof event.type !== "string") {
|
|
67
|
+
throw new Error("the body is not a Stripe event");
|
|
68
|
+
}
|
|
69
|
+
return { key: event.type, id: event.id, payload: event };
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// The kind's id and payloads, for the types of the handlers.
|
|
74
|
+
declare module "@jr2/orchestrator/hooks" {
|
|
75
|
+
interface HookKinds {
|
|
76
|
+
stripe: typeof stripe;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A kind has these parts:
|
|
82
|
+
|
|
83
|
+
- `api: 1`: the version of the interface of a kind.
|
|
84
|
+
- `id`: the key of the kind in the `hooks` of a Gate or a Source. It is lowercase letters, digits and `-`, and it starts
|
|
85
|
+
with a letter. One id is one kind in an Assembly.
|
|
86
|
+
- `events`: a zod schema for each event key that a handler can hear. The door parses each payload with the schema of its
|
|
87
|
+
key before a handler gets it. Use `z.looseObject`, and name only the fields that the handlers read. A key that ends in
|
|
88
|
+
`.*` gives the schema of each key with that prefix.
|
|
89
|
+
- `verify`: one of `mac`, `token` or `ed25519`. For `mac` and `ed25519`, `signed` gives the bytes that the service
|
|
90
|
+
signed, the signature without its prefix, and `at`, the time of the delivery in seconds. Give `at` only when the
|
|
91
|
+
signature covers it. The door refuses a time that is more than 300 seconds from its clock. When the request does not
|
|
92
|
+
have a signature, give `{ unsigned: "<why>" }`. For `token`, give the header of the token.
|
|
93
|
+
- `read`: the request after the verifier, as a delivery or as a handshake. A delivery has the event key, the id, the
|
|
94
|
+
payload, and `onBehalfOf` when the service names the party that acted. A handshake is `{ handshake: { body } }`, and
|
|
95
|
+
goes to no handler. A throw is the status 400.
|
|
96
|
+
|
|
97
|
+
A kind whose answer a person reads also has `answer`, the envelope of the text, and `maxText`, the limit of the service
|
|
98
|
+
on its length. Its `read` gives `readBy: "person"`. When the answer gives a follow-up post, `posts` lists the URLs that
|
|
99
|
+
the post can go to. See the [`slack` kind](../reference/hooks.md#slack).
|
|
100
|
+
|
|
101
|
+
A kind is pure and synchronous: it does no I/O, gives no Promise, and reads no clock. It never gets the secret. The
|
|
102
|
+
`declare module` block adds the kind to `HookKinds`, so that the handlers of a Gate or a Source get the types of its
|
|
103
|
+
payloads.
|
|
104
|
+
|
|
105
|
+
## Declare a Hook of the kind
|
|
106
|
+
|
|
107
|
+
Add the Hook to `jr2.config.ts`, and its secret to the `secrets` list:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { defineConfig, secret } from "@jr2/orchestrator";
|
|
111
|
+
import { stripe } from "./hooks/stripe.ts";
|
|
112
|
+
|
|
113
|
+
export default defineConfig({
|
|
114
|
+
// The signing secret of the Stripe endpoint. jr2 up places it in the Instance Effector.
|
|
115
|
+
secrets: [{ name: "STRIPE_HOOK", value: process.env.STRIPE_HOOK }],
|
|
116
|
+
hooks: {
|
|
117
|
+
// POST /hooks/billing. The Hook may answer the Gates of dunning alone.
|
|
118
|
+
billing: stripe({ secret: secret("STRIPE_HOOK"), may: { answer: ["dunning"] } }),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The kind is also the constructor of its Hooks. It takes the credential and `may.answer`. For a `mac` kind,
|
|
124
|
+
[`jr2 up`](../reference/cli.md#up) puts the secret in the
|
|
125
|
+
[Instance Effector](../reference/glossary.md#instance-effector) only, which checks each signature. Put the value in
|
|
126
|
+
`.env`:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
STRIPE_HOOK=<signing secret>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`<signing secret>` is the signing secret that Stripe shows for the endpoint. Give the endpoint the URL
|
|
133
|
+
`https://<host>/hooks/billing`.
|
|
134
|
+
|
|
135
|
+
## Hear it at a Gate
|
|
136
|
+
|
|
137
|
+
Import the kind's types in the workflow module, and give the Gate slot a handler for each event key that it hears:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { z } from "zod";
|
|
141
|
+
import { defineEvent, fail, gate, jr2Setup } from "@jr2/orchestrator";
|
|
142
|
+
import type {} from "../hooks/stripe.ts";
|
|
143
|
+
|
|
144
|
+
const paid = defineEvent({
|
|
145
|
+
name: "paid",
|
|
146
|
+
description: "The customer paid the invoice.",
|
|
147
|
+
input: z.object({}),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// The Gate hears the stripe kind. A payment of another invoice is not for this Gate.
|
|
151
|
+
const payment = gate({
|
|
152
|
+
meta: z.object({ invoice: z.string() }),
|
|
153
|
+
delivers: [paid],
|
|
154
|
+
hooks: {
|
|
155
|
+
stripe: {
|
|
156
|
+
"invoice.paid": ({ payload, meta }) => (payload.data.object.id === meta.invoice ? { type: "paid" } : undefined),
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export const machine = jr2Setup({
|
|
162
|
+
types: {} as { input: { invoice: string }; context: { invoice: string } },
|
|
163
|
+
events: [paid],
|
|
164
|
+
actors: { payment, fail: fail() },
|
|
165
|
+
}).createMachine({
|
|
166
|
+
id: "dunning",
|
|
167
|
+
input: z.object({ invoice: z.string() }),
|
|
168
|
+
context: ({ input }) => ({ invoice: input.invoice }),
|
|
169
|
+
initial: "waiting",
|
|
170
|
+
states: {
|
|
171
|
+
waiting: {
|
|
172
|
+
invoke: { src: "payment", input: ({ context }) => ({ meta: { invoice: context.invoice } }) },
|
|
173
|
+
on: { paid: "paid" },
|
|
174
|
+
// After seven days with no payment, the run stops waiting. A lost delivery cannot stop the
|
|
175
|
+
// run for longer than that.
|
|
176
|
+
after: { 604800000: "unpaid" },
|
|
177
|
+
},
|
|
178
|
+
paid: { type: "final" },
|
|
179
|
+
// Not a final state: the run ends `failed`, with the reason.
|
|
180
|
+
unpaid: { invoke: { src: "fail", input: () => ({ reason: "the invoice is not paid after seven days" }) } },
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A handler for a key that the kind does not give does not compile, and a handler reads only the fields that the schema
|
|
186
|
+
names. [Answer a Gate from a forge](answer-a-gate-from-a-forge.md) tells the other parts of a Gate slot, such as the
|
|
187
|
+
look. A Source hears a kind in the same way ([Start a run for each alert](start-a-run-for-each-alert.md)).
|
|
188
|
+
|
|
189
|
+
## Put the kind in a package
|
|
190
|
+
|
|
191
|
+
A kind in a package of its own uses the same interface. Its package has a peer dependency on `@jr2/orchestrator` and
|
|
192
|
+
states `api: 1`, and its `jr2.config.ts` and its workflow modules import it as they import a local file. `jr2 up`
|
|
193
|
+
bundles the kind into the [Assembly](../reference/glossary.md#assembly). jr2 finds no kind in a folder, and loads none
|
|
194
|
+
while the Instance runs.
|
|
195
|
+
|
|
196
|
+
## Rules
|
|
197
|
+
|
|
198
|
+
- `defineHookKind()` refuses an `id` of another form:
|
|
199
|
+
`a Hook kind's id is lowercase letters, digits and "-", and starts with a letter: <id>`
|
|
200
|
+
- `defineHookKind()` refuses an `api` that it does not know:
|
|
201
|
+
`Hook kind <id> states api <api>; this @jr2/orchestrator/hooks knows api 1 only`
|
|
202
|
+
- `defineHookKind()` refuses an event key with no zod schema: `Hook kind <id>: the event key <key> has no zod schema`
|
|
203
|
+
- `defineHookKind()` refuses a `verify` that is not one of the three:
|
|
204
|
+
`Hook kind <id>: verify is one of mac, ed25519 or token`
|
|
205
|
+
- `defineHookKind()` refuses a `mac` verifier without an algorithm or a `signed` function:
|
|
206
|
+
`Hook kind <id>: a mac verifier has an alg (sha256, sha1, sha512) and a signed function`
|
|
207
|
+
- `defineHookKind()` refuses a `token` header that is not lowercase:
|
|
208
|
+
`Hook kind <id>: a token verifier names its header in lowercase`
|
|
209
|
+
- `defineHookKind()` refuses a `posts` entry that is not an `https://` URL that ends in `/`:
|
|
210
|
+
`Hook kind <id>: a posts entry is an https:// url that ends in "/": <entry>`
|
|
211
|
+
- `defineHookKind()` refuses a `maxText` that is not a positive integer: `Hook kind <id>: maxText is a positive integer`
|
|
212
|
+
- The constructor of a `mac` or `token` kind refuses a credential that is not a named secret:
|
|
213
|
+
`a <id> Hook takes secret: a named secret, secret("NAME")`
|
|
214
|
+
- The constructor of an `ed25519` kind refuses a public key that is not 64 hex characters:
|
|
215
|
+
`a <id> Hook takes publicKey: the Ed25519 public key in hex (64 characters)`
|
|
216
|
+
- The constructor refuses a `may.answer` that is not `"all"` or a list of Workflow names:
|
|
217
|
+
`a <id> Hook's may.answer is "all" or a list of Workflow names`
|
|
218
|
+
- Two kinds with one id do not compile (`Subsequent property declarations must have the same type`), and the config load
|
|
219
|
+
refuses them:
|
|
220
|
+
`<file> at hooks: two different Hook kinds have the id <id>: one id is one kind in an Assembly (ADR-0078)`
|
|
221
|
+
- The type of `read` refuses `readBy: "person"` in a kind that has no `answer`.
|
|
222
|
+
- A request that `signed` gives `unsigned` for, or whose signature does not match, gets 401. A request that `read`
|
|
223
|
+
throws for gets 400: `the <kind> kind cannot read the delivery: <error>`
|
|
224
|
+
- `jr2 up` refuses a `mac` Hook whose secret is not in the `secrets` list:
|
|
225
|
+
``hooks.<name> names secret("<NAME>"), which the `secrets` list does not hold — add it: secrets: [{ name: "<NAME>", value: process.env.<NAME> }] (ADR-0068)``
|
|
226
|
+
|
|
227
|
+
## Read next
|
|
228
|
+
|
|
229
|
+
- [Callers and Hooks](../concepts/callers-and-hooks.md)
|
|
230
|
+
- [Answer a Gate from a forge](answer-a-gate-from-a-forge.md) and
|
|
231
|
+
[Start a run for each alert](start-a-run-for-each-alert.md)
|
|
232
|
+
- Reference: [`@jr2/orchestrator/hooks`](../reference/hooks.md), [`HookKind`](../reference/hooks.md#type-hookkind),
|
|
233
|
+
[`Signed`](../reference/hooks.md#type-signed), [`Delivery`](../reference/hooks.md#type-delivery) and the
|
|
234
|
+
[`hooks`](../reference/config.md#hooks) field of `jr2.config.ts`
|
|
235
|
+
- [ADR-0078](../reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md): a
|
|
236
|
+
Hook kind, the three verifiers, and the window
|
|
237
|
+
- [ADR-0068](../reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md): the Instance
|
|
238
|
+
Effector holds a `mac` secret
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Write a tool as an MCP server
|
|
3
|
+
description: Write a small MCP server in TypeScript, put it in the Sandbox Image, and give its tool to an Agent.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Write a tool as an MCP server
|
|
7
|
+
|
|
8
|
+
Write a tool for your [Agents](../reference/glossary.md#agent) as a small MCP server in TypeScript, put it in the
|
|
9
|
+
[Sandbox Image](../reference/glossary.md#sandbox-image), and give it to an Agent as a stdio server.
|
|
10
|
+
|
|
11
|
+
jr2 has no tool API of its own. A tool that you write is an MCP server: a remote server that you run yourself, or a
|
|
12
|
+
stdio server, which is a program in the Sandbox Image. An Agent in a [Sandbox](../reference/glossary.md#sandbox) gets
|
|
13
|
+
each tool of a stdio server as one of its [MCP tools](../reference/glossary.md#mcp-tools).
|
|
14
|
+
|
|
15
|
+
The server on this page has one tool, `next_version`. It gives the next version number after a release. It uses only the
|
|
16
|
+
modules of Node, so it needs no package. Save it as `images/default/semver-mcp.ts` in your
|
|
17
|
+
[Instance](../reference/glossary.md#instance), beside the `Dockerfile` that `jr2 init` wrote:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// A stdio MCP server with one tool, next_version. It reads one JSON-RPC message on each line of
|
|
21
|
+
// stdin, and writes each answer as one line on stdout. Node runs this file as it is.
|
|
22
|
+
import { createInterface } from "node:readline";
|
|
23
|
+
|
|
24
|
+
type Message = { id?: number | string; method?: string; params?: { name?: string; arguments?: unknown } };
|
|
25
|
+
|
|
26
|
+
const tools = [
|
|
27
|
+
{
|
|
28
|
+
name: "next_version",
|
|
29
|
+
description: "The next version number after a release. Give the current version and the kind of change.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
version: { type: "string", description: "The current version, such as 1.4.2." },
|
|
34
|
+
change: { type: "string", enum: ["major", "minor", "patch"] },
|
|
35
|
+
},
|
|
36
|
+
required: ["version", "change"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// The tool itself: a function of its arguments.
|
|
42
|
+
function nextVersion(version: string, change: string): string {
|
|
43
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
44
|
+
if (match === null) throw new Error(`${version} is not a version such as 1.4.2`);
|
|
45
|
+
const [major, minor, patch] = match.slice(1).map(Number) as [number, number, number];
|
|
46
|
+
if (change === "major") return `${major + 1}.0.0`;
|
|
47
|
+
if (change === "minor") return `${major}.${minor + 1}.0`;
|
|
48
|
+
if (change === "patch") return `${major}.${minor}.${patch + 1}`;
|
|
49
|
+
throw new Error(`${change} is not major, minor or patch`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function call(name: string | undefined, args: unknown): object {
|
|
53
|
+
const { version, change } = (args ?? {}) as { version?: unknown; change?: unknown };
|
|
54
|
+
if (name !== "next_version") return { error: { code: -32602, message: `Unknown tool: ${name}` } };
|
|
55
|
+
try {
|
|
56
|
+
const text = nextVersion(String(version), String(change));
|
|
57
|
+
return { result: { content: [{ type: "text", text }] } };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// A tool's own error: the model reads the text, and the call is an error.
|
|
60
|
+
return { result: { content: [{ type: "text", text: (err as Error).message }], isError: true } };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handle(message: Message): object {
|
|
65
|
+
switch (message.method) {
|
|
66
|
+
case "initialize":
|
|
67
|
+
return {
|
|
68
|
+
result: {
|
|
69
|
+
protocolVersion: "2025-11-25",
|
|
70
|
+
capabilities: { tools: {} },
|
|
71
|
+
serverInfo: { name: "semver", version: "1.0.0" },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
case "tools/list":
|
|
75
|
+
return { result: { tools } };
|
|
76
|
+
case "tools/call":
|
|
77
|
+
return call(message.params?.name, message.params?.arguments);
|
|
78
|
+
default:
|
|
79
|
+
return { error: { code: -32601, message: `Method not found: ${message.method}` } };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
createInterface({ input: process.stdin }).on("line", (line) => {
|
|
84
|
+
if (line.trim() === "") return;
|
|
85
|
+
const message = JSON.parse(line) as Message;
|
|
86
|
+
// A notification has no id, and gets no answer.
|
|
87
|
+
if (message.id === undefined) return;
|
|
88
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, ...handle(message) })}\n`);
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## What the server does
|
|
93
|
+
|
|
94
|
+
A stdio server reads one JSON-RPC message on each line of its stdin. It writes each answer as one line on its stdout. It
|
|
95
|
+
answers these methods:
|
|
96
|
+
|
|
97
|
+
- `initialize`: the revision of MCP that it speaks, `2025-11-25`, and `capabilities: { tools: {} }`.
|
|
98
|
+
- `tools/list`: its tools. Each tool has a `name`, a `description` and an `inputSchema`, which is JSON Schema. The model
|
|
99
|
+
reads the description and the schema.
|
|
100
|
+
- `tools/call`: the result of one call, as `content`. A tool that fails gives a result with `isError: true` and the
|
|
101
|
+
reason as text. The model reads the reason.
|
|
102
|
+
|
|
103
|
+
A message with no `id` is a notification, such as `notifications/initialized`, and gets no answer. A method that the
|
|
104
|
+
server does not have gets the JSON-RPC error `-32601`. Write only messages on stdout: the Harness reads each line as a
|
|
105
|
+
message, and drops a line that is not one. The Harness does not read stderr.
|
|
106
|
+
|
|
107
|
+
## Try the server on your computer
|
|
108
|
+
|
|
109
|
+
Send it the messages that the Harness sends, with Node 24 or later:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
printf '%s\n' \
|
|
113
|
+
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' \
|
|
114
|
+
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
|
115
|
+
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
|
|
116
|
+
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"next_version","arguments":{"version":"1.4.2","change":"minor"}}}' \
|
|
117
|
+
'{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"next_version","arguments":{"version":"x","change":"minor"}}}' \
|
|
118
|
+
| node images/default/semver-mcp.ts
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{}},"serverInfo":{"name":"semver","version":"1.0.0"}}}
|
|
123
|
+
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"next_version","description":"The next version number after a release. Give the current version and the kind of change.","inputSchema":{"type":"object","properties":{"version":{"type":"string","description":"The current version, such as 1.4.2."},"change":{"type":"string","enum":["major","minor","patch"]}},"required":["version","change"]}}]}}
|
|
124
|
+
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"1.5.0"}]}}
|
|
125
|
+
{"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"x is not a version such as 1.4.2"}],"isError":true}}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The notification gets no line. The last call is the tool's own error.
|
|
129
|
+
|
|
130
|
+
## Put the server in the Sandbox Image
|
|
131
|
+
|
|
132
|
+
Copy the file into the image. The folder of the `Dockerfile` is the context of the build, so `COPY` finds the file
|
|
133
|
+
there:
|
|
134
|
+
|
|
135
|
+
```dockerfile
|
|
136
|
+
FROM node:24-slim
|
|
137
|
+
|
|
138
|
+
RUN apt-get update \
|
|
139
|
+
&& apt-get install -y --no-install-recommends git ca-certificates \
|
|
140
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
141
|
+
|
|
142
|
+
# Your agents' toolchain goes here — compilers, CLIs, language servers, dotfiles.
|
|
143
|
+
|
|
144
|
+
# The MCP server. Node 24 runs a .ts file as it is, so the image needs no build step.
|
|
145
|
+
COPY semver-mcp.ts /opt/tools/semver-mcp.ts
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The example does not show the comments at the start of the file. Keep them in your file. Node 24 runs a `.ts` file that
|
|
149
|
+
has only types that it can remove, so the image needs no build step. For an MCP server from a package, install the
|
|
150
|
+
package at a fixed version in the `Dockerfile` instead, such as `RUN npm install -g <package>@<version>`.
|
|
151
|
+
|
|
152
|
+
[`jr2 up`](../reference/cli.md#up) builds the image, because a file in its folder changed.
|
|
153
|
+
[Add tools to the Sandbox](add-tools-to-the-sandbox.md) tells how `jr2 up` builds and loads it.
|
|
154
|
+
|
|
155
|
+
## Declare the server and give it to an Agent
|
|
156
|
+
|
|
157
|
+
Declare the server in the [`mcp`](../reference/config.md#mcp) block of `jr2.config.ts`, beside the fields that are
|
|
158
|
+
there. `command` and `args` are the program and its arguments in the image:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { defineConfig } from "@jr2/orchestrator";
|
|
162
|
+
|
|
163
|
+
export default defineConfig({
|
|
164
|
+
// A stdio server: a program of the Sandbox Image. The Harness starts it for each conversation.
|
|
165
|
+
mcp: { semver: { command: "node", args: ["/opt/tools/semver-mcp.ts"] } },
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Then name the server in the `mcp` list of an Agent that works in a Sandbox. In this
|
|
170
|
+
[Workflow](../reference/glossary.md#workflow), a releaser reads the changes of a release, asks `next_version` for the
|
|
171
|
+
next version, and picks `decided`. Save the file as `workflows/next-version.ts`:
|
|
172
|
+
|
|
173
|
+
**Anthropic**
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { assign } from "xstate";
|
|
177
|
+
import { z } from "zod";
|
|
178
|
+
import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
|
|
179
|
+
|
|
180
|
+
const decided = defineEvent({
|
|
181
|
+
name: "decided",
|
|
182
|
+
description: "Call this with the next version, after the next_version tool gave it.",
|
|
183
|
+
input: z.object({ version: z.string().describe("The next version, such as 1.5.0.") }),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const runInput = z.object({
|
|
187
|
+
version: z.string().describe("The current version, such as 1.4.2."),
|
|
188
|
+
changes: z.string().describe("The changes since that version, one on each line."),
|
|
189
|
+
});
|
|
190
|
+
// `never`: the Workspace has no Repo Slot.
|
|
191
|
+
type BodyInput = Workspaced<z.infer<typeof runInput>, never>;
|
|
192
|
+
type Ctx = BodyInput & { next?: string };
|
|
193
|
+
|
|
194
|
+
const body = jr2Setup({
|
|
195
|
+
types: {} as { context: Ctx; input: BodyInput; output: { version: string } },
|
|
196
|
+
events: [decided],
|
|
197
|
+
actors: {
|
|
198
|
+
releaser: agent({
|
|
199
|
+
model: "anthropic/claude-haiku-5-5",
|
|
200
|
+
instructions: "You decide how large a release is: major, minor or patch. You use semantic versioning.",
|
|
201
|
+
workspace: "read",
|
|
202
|
+
// A stdio server takes no tools list: the Agent gets every tool of the server.
|
|
203
|
+
mcp: [{ server: "semver" }],
|
|
204
|
+
}),
|
|
205
|
+
fail: fail(),
|
|
206
|
+
},
|
|
207
|
+
}).createMachine({
|
|
208
|
+
id: "next-version",
|
|
209
|
+
context: ({ input }) => input,
|
|
210
|
+
on: { "agent.fault": ".failed", "workspace.lost": ".failed" },
|
|
211
|
+
initial: "deciding",
|
|
212
|
+
states: {
|
|
213
|
+
// The Turn frames no `cwd`, so it works in /work. The server starts there too.
|
|
214
|
+
deciding: {
|
|
215
|
+
invoke: {
|
|
216
|
+
src: "releaser",
|
|
217
|
+
input: ({ context }) => ({
|
|
218
|
+
prompt: `The current version is ${context.version}. The changes are:\n${context.changes}`,
|
|
219
|
+
}),
|
|
220
|
+
},
|
|
221
|
+
on: { decided: { target: "done", actions: assign({ next: ({ event }) => event.version }) } },
|
|
222
|
+
},
|
|
223
|
+
done: { type: "final" },
|
|
224
|
+
// Not a final state: the run ends `failed`, with the reason.
|
|
225
|
+
failed: { invoke: { src: "fail", input: { reason: "the releaser could not decide" } } },
|
|
226
|
+
},
|
|
227
|
+
output: ({ context }) => ({ version: context.next ?? "" }),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// No `repos`: the Workspace clones nothing. The Sandbox Image holds the server.
|
|
231
|
+
export const machine = workspace(body, { input: runInput });
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Local**
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { assign } from "xstate";
|
|
238
|
+
import { z } from "zod";
|
|
239
|
+
import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
|
|
240
|
+
|
|
241
|
+
const decided = defineEvent({
|
|
242
|
+
name: "decided",
|
|
243
|
+
description: "Call this with the next version, after the next_version tool gave it.",
|
|
244
|
+
input: z.object({ version: z.string().describe("The next version, such as 1.5.0.") }),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const runInput = z.object({
|
|
248
|
+
version: z.string().describe("The current version, such as 1.4.2."),
|
|
249
|
+
changes: z.string().describe("The changes since that version, one on each line."),
|
|
250
|
+
});
|
|
251
|
+
// `never`: the Workspace has no Repo Slot.
|
|
252
|
+
type BodyInput = Workspaced<z.infer<typeof runInput>, never>;
|
|
253
|
+
type Ctx = BodyInput & { next?: string };
|
|
254
|
+
|
|
255
|
+
const body = jr2Setup({
|
|
256
|
+
types: {} as { context: Ctx; input: BodyInput; output: { version: string } },
|
|
257
|
+
events: [decided],
|
|
258
|
+
actors: {
|
|
259
|
+
releaser: agent({
|
|
260
|
+
model: "local/qwen3.6-35b-a3b",
|
|
261
|
+
instructions: "You decide how large a release is: major, minor or patch. You use semantic versioning.",
|
|
262
|
+
workspace: "read",
|
|
263
|
+
// A stdio server takes no tools list: the Agent gets every tool of the server.
|
|
264
|
+
mcp: [{ server: "semver" }],
|
|
265
|
+
}),
|
|
266
|
+
fail: fail(),
|
|
267
|
+
},
|
|
268
|
+
}).createMachine({
|
|
269
|
+
id: "next-version",
|
|
270
|
+
context: ({ input }) => input,
|
|
271
|
+
on: { "agent.fault": ".failed", "workspace.lost": ".failed" },
|
|
272
|
+
initial: "deciding",
|
|
273
|
+
states: {
|
|
274
|
+
// The Turn frames no `cwd`, so it works in /work. The server starts there too.
|
|
275
|
+
deciding: {
|
|
276
|
+
invoke: {
|
|
277
|
+
src: "releaser",
|
|
278
|
+
input: ({ context }) => ({
|
|
279
|
+
prompt: `The current version is ${context.version}. The changes are:\n${context.changes}`,
|
|
280
|
+
}),
|
|
281
|
+
},
|
|
282
|
+
on: { decided: { target: "done", actions: assign({ next: ({ event }) => event.version }) } },
|
|
283
|
+
},
|
|
284
|
+
done: { type: "final" },
|
|
285
|
+
// Not a final state: the run ends `failed`, with the reason.
|
|
286
|
+
failed: { invoke: { src: "fail", input: { reason: "the releaser could not decide" } } },
|
|
287
|
+
},
|
|
288
|
+
output: ({ context }) => ({ version: context.next ?? "" }),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// No `repos`: the Workspace clones nothing. The Sandbox Image holds the server.
|
|
292
|
+
export const machine = workspace(body, { input: runInput });
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The releaser uses the model of the tab. The model reads the tool as `mcp__semver__next_version`. A stdio server takes no
|
|
296
|
+
`tools` list: the Agent gets each tool of the server. Run `jr2 up`, then start a run:
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
jr2 run next-version --input '{"version":"1.4.2","changes":"Add the --json flag.\nFix the help text."}'
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## What the server can reach
|
|
303
|
+
|
|
304
|
+
A stdio server is [Workspace code](../reference/glossary.md#workspace-code), as the Agent's shell is:
|
|
305
|
+
|
|
306
|
+
- The Harness starts it through the spawner, inside the Landlock confinement of the Sandbox, in the directory of the
|
|
307
|
+
[Turn](../reference/glossary.md#turn). It cannot read `/run/jr2`, and it cannot read the control Stand-in of the
|
|
308
|
+
Harness.
|
|
309
|
+
- Its env is the env of the confined programs. It holds only the [Stand-ins](../reference/glossary.md#stand-in) of the
|
|
310
|
+
[Held secrets](../reference/glossary.md#held-secret) that the
|
|
311
|
+
[`workspace()`](../reference/authoring-surface/workspace.md) lists under `secrets`. A server that calls an API sends
|
|
312
|
+
the Stand-in, and the [Custodian](../reference/glossary.md#custodian) puts the value in its place.
|
|
313
|
+
- It runs as one process for each conversation of the Agent, from the first Turn to the end of the conversation. When it
|
|
314
|
+
stops, its call fails, and the next call starts it again.
|
|
315
|
+
|
|
316
|
+
The Agent's other programs run in the same confinement. They can trace the server and write to its stdin. Thus trust the
|
|
317
|
+
result of the server as much as the output of a command that the Agent runs, and no more.
|
|
318
|
+
[Secrets and trust](../concepts/secrets-and-trust.md#confined-programs) tells what the confinement stops.
|
|
319
|
+
|
|
320
|
+
A Menu-only Agent cannot use a stdio server: its Turns run on the
|
|
321
|
+
[Instance Harness](../reference/glossary.md#instance-harness), where no Workspace code runs. An `mcp` Effect cannot use
|
|
322
|
+
one either. To use a tool from those, run it as a remote server, and declare its `url`.
|
|
323
|
+
|
|
324
|
+
## Rules
|
|
325
|
+
|
|
326
|
+
- At the first Turn of a conversation, the server must answer `initialize` and `tools/list`. When it fails, the Turn
|
|
327
|
+
fails before the model reads anything, and the Machine gets
|
|
328
|
+
[`agent.fault`](../reference/mechanism-events.md#agent-fault) with the reason `<server> did not answer: <reason>`.
|
|
329
|
+
- A line on stdout that is longer than 16 MiB stops the server.
|
|
330
|
+
- `jr2 up` refuses a `tools` list on a stdio server:
|
|
331
|
+
`refusing: workflow "<workflow>": Agent "<slot>" gives a tools list for the stdio MCP server "<server>" — a stdio server takes none: the Agent gets every tool, because the server runs as Workspace code, and a list would guard nothing. Write { server: "<server>" } (ADR-0079)`
|
|
332
|
+
- `jr2 up` refuses a stdio server on a Menu-only Agent:
|
|
333
|
+
`refusing: workflow "<workflow>": Agent "<slot>" has workspace: "none" and names the stdio MCP server "<server>" — a stdio server runs as Workspace code, inside a Sandbox's confinement, and a "none" Agent's Turns run on the Instance Harness, which has none: name a remote server, or give the Agent a Workspace (ADR-0079, ADR-0031)`
|
|
334
|
+
- `jr2 up` does not check the tools of a stdio server, because the program runs only in a Sandbox.
|
|
335
|
+
- A tool whose name, as `mcp__<server>__<tool>`, has a character other than letters, digits, `_` and `-`, or more than
|
|
336
|
+
128 characters, is not given to the model. The Harness writes one line in its log:
|
|
337
|
+
`the MCP server <server> lists the tool "<tool>", and "mcp__<server>__<tool>" does not match ^[A-Za-z0-9_-]{1,128}$: the tool is not offered (ADR-0079)`
|
|
338
|
+
|
|
339
|
+
[Give an Agent MCP tools](give-an-agent-mcp-tools.md#rules) gives the refusals of the `mcp` block.
|
|
340
|
+
|
|
341
|
+
## Read next
|
|
342
|
+
|
|
343
|
+
- [Agents and Turns](../concepts/agents-and-turns.md#the-mcp-tools): the MCP tools of an Agent.
|
|
344
|
+
- [Workspaces](../concepts/workspaces.md): the Sandbox Image.
|
|
345
|
+
- [Give an Agent MCP tools](give-an-agent-mcp-tools.md): remote servers, and the allowlist.
|
|
346
|
+
- Reference: [`mcp`](../reference/config.md#mcp) and
|
|
347
|
+
[`AgentMcpServer`](../reference/authoring-surface/type-agentmcpserver.md).
|
|
348
|
+
- [ADR-0079](../reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md):
|
|
349
|
+
MCP is the one contract for tools, and a stdio server is Workspace code.
|
|
350
|
+
- [ADR-0076](../reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md): the
|
|
351
|
+
confinement of Workspace code.
|
|
@@ -184,7 +184,7 @@ jr2 run rollout --input '{"service":"billing","version":"1.4.0"}'
|
|
|
184
184
|
|
|
185
185
|
```text
|
|
186
186
|
→ context kind-jr2 / namespace my-instance
|
|
187
|
-
run
|
|
187
|
+
run a5bafe11-80bf-426b-905f-6fb19952c093 (rollout) — attached
|
|
188
188
|
→ active "canary"
|
|
189
189
|
```
|
|
190
190
|
|
|
@@ -198,7 +198,7 @@ jr2 send <run id> --gate canary --event roll_back --input '{"reason":"The error
|
|
|
198
198
|
|
|
199
199
|
```text
|
|
200
200
|
→ context kind-jr2 / namespace my-instance
|
|
201
|
-
delivered roll_back to gate "canary" on
|
|
201
|
+
delivered roll_back to gate "canary" on a5bafe11-80bf-426b-905f-6fb19952c093
|
|
202
202
|
```
|
|
203
203
|
|
|
204
204
|
In the first terminal, the run ends. A `roll_back` ends the rollout, so the run ends `failed`, with the reason of the
|
|
@@ -210,7 +210,7 @@ that jr2 added, and the `reason`:
|
|
|
210
210
|
→ active "rolledBack"
|
|
211
211
|
→ failed "rolledBack"
|
|
212
212
|
failed: The error rate went up.
|
|
213
|
-
{"runId":"
|
|
213
|
+
{"runId":"a5bafe11-80bf-426b-905f-6fb19952c093","workflow":"rollout","instanceId":"f779766e-49c3-49ad-92bd-10208710843b","startedAt":"2026-11-06T19:17:57.979Z","startedBy":{"kind":"caller","via":"token","name":"instance","onBehalfOf":"kubernetes-admin"},"status":"failed","value":"rolledBack","context":{"service":"billing","version":"1.4.0","canaryPercent":10,"instanceId":"f779766e-49c3-49ad-92bd-10208710843b","reason":"The error rate went up."},"children":[],"waiting":[],"held":[],"answers":[{"gate":"canary","type":"roll_back","at":"2026-11-06T19:18:06.269Z","answeredBy":{"via":"token","name":"instance","onBehalfOf":"kubernetes-admin"}}],"assembly":"bedde6f8c51c","output":{"reason":"The error rate went up."}}
|
|
214
214
|
```
|
|
215
215
|
|
|
216
216
|
Your instance id and your `assembly` hash are different.
|
|
@@ -218,12 +218,22 @@ Your instance id and your `assembly` hash are different.
|
|
|
218
218
|
[Start a run](start-a-run.md) gives the other ways to start a run. [Answer a Gate](answer-a-gate.md) gives the other
|
|
219
219
|
ways to answer one.
|
|
220
220
|
|
|
221
|
-
##
|
|
221
|
+
## Lay out the Instance
|
|
222
|
+
|
|
223
|
+
jr2 finds the Workflows in `workflows/`, and `jr2.config.ts` declares the rest. Other code goes in folders of your own
|
|
224
|
+
choice. `jr2 up` bundles the whole folder of the Instance, so a Workflow and the config can import from each folder:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
my-instance/
|
|
228
|
+
jr2.config.ts the config: secrets, Hooks, Callers, the Harness
|
|
229
|
+
workflows/ one .ts file is one Workflow
|
|
230
|
+
hooks/ your Hook kinds
|
|
231
|
+
lib/ code that two Workflows share
|
|
232
|
+
```
|
|
222
233
|
|
|
223
234
|
jr2 registers each `.ts` file in `workflows/`, also a file that a different Workflow imports. It does not register a
|
|
224
|
-
file whose name starts with `_`, or a `.d.ts` file. Thus put the code that two Workflows share
|
|
225
|
-
`
|
|
226
|
-
`import { promote } from "./_events.ts";`.
|
|
235
|
+
file whose name starts with `_`, or a `.d.ts` file. Thus put the code that two Workflows share outside `workflows/`,
|
|
236
|
+
such as in `lib/events.ts`. Import it with its `.ts` extension: `import { promote } from "../lib/events.ts";`.
|
|
227
237
|
|
|
228
238
|
## Rules
|
|
229
239
|
|