@intentic/sandbox-contract 1.230.3 → 1.232.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 (72) hide show
  1. package/README.md +11 -0
  2. package/dist/contracts/agent.contract.d.ts +53 -6
  3. package/dist/contracts/agent.contract.d.ts.map +1 -1
  4. package/dist/contracts/agent.contract.js +10 -1
  5. package/dist/contracts/agent.contract.js.map +1 -1
  6. package/dist/contracts/agents.contract.d.ts +6 -1
  7. package/dist/contracts/agents.contract.d.ts.map +1 -1
  8. package/dist/contracts/capabilities.contract.d.ts +293 -0
  9. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  10. package/dist/contracts/capabilities.contract.js +10 -1
  11. package/dist/contracts/capabilities.contract.js.map +1 -1
  12. package/dist/contracts/extensions.contract.d.ts +15 -0
  13. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  14. package/dist/contracts/git.contract.d.ts +12 -2
  15. package/dist/contracts/git.contract.d.ts.map +1 -1
  16. package/dist/contracts/history.contract.d.ts +6 -1
  17. package/dist/contracts/history.contract.d.ts.map +1 -1
  18. package/dist/contracts/host.contract.d.ts +4 -0
  19. package/dist/contracts/host.contract.d.ts.map +1 -1
  20. package/dist/contracts/panels.contract.d.ts +1 -0
  21. package/dist/contracts/panels.contract.d.ts.map +1 -1
  22. package/dist/contracts/runner.contract.d.ts +876 -0
  23. package/dist/contracts/runner.contract.d.ts.map +1 -0
  24. package/dist/contracts/runner.contract.js +17 -0
  25. package/dist/contracts/runner.contract.js.map +1 -0
  26. package/dist/contracts/system.contract.d.ts +6 -3
  27. package/dist/contracts/system.contract.d.ts.map +1 -1
  28. package/dist/definition.d.ts +961 -0
  29. package/dist/definition.d.ts.map +1 -0
  30. package/dist/definition.js +71 -0
  31. package/dist/definition.js.map +1 -0
  32. package/dist/documents.d.ts +7 -0
  33. package/dist/documents.d.ts.map +1 -0
  34. package/dist/documents.js +27 -0
  35. package/dist/documents.js.map +1 -0
  36. package/dist/events.d.ts +66 -6
  37. package/dist/events.d.ts.map +1 -1
  38. package/dist/events.js +17 -1
  39. package/dist/events.js.map +1 -1
  40. package/dist/history-state.d.ts.map +1 -1
  41. package/dist/history-state.js +3 -0
  42. package/dist/history-state.js.map +1 -1
  43. package/dist/index.d.ts +396 -13
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/runner-protocol.d.ts +134 -0
  48. package/dist/runner-protocol.d.ts.map +1 -0
  49. package/dist/runner-protocol.js +95 -0
  50. package/dist/runner-protocol.js.map +1 -0
  51. package/dist/schemas.d.ts +194 -32
  52. package/dist/schemas.d.ts.map +1 -1
  53. package/dist/schemas.js +80 -25
  54. package/dist/schemas.js.map +1 -1
  55. package/dist/workspace-state.d.ts +9 -0
  56. package/dist/workspace-state.d.ts.map +1 -1
  57. package/dist/workspace-state.js +2 -0
  58. package/dist/workspace-state.js.map +1 -1
  59. package/package.json +6 -6
  60. package/src/contracts/agent.contract.ts +19 -0
  61. package/src/contracts/capabilities.contract.ts +16 -0
  62. package/src/contracts/runner.contract.ts +49 -0
  63. package/src/definition.ts +171 -0
  64. package/src/documents.test.ts +66 -0
  65. package/src/documents.ts +71 -0
  66. package/src/events.ts +88 -2
  67. package/src/history-state.ts +7 -0
  68. package/src/index.ts +5 -0
  69. package/src/runner-protocol.ts +209 -0
  70. package/src/schemas.ts +225 -65
  71. package/src/workspace-state.test.ts +4 -0
  72. package/src/workspace-state.ts +19 -0
package/src/events.ts CHANGED
@@ -434,6 +434,30 @@ export const SharePayloadSchema = z.object({
434
434
  });
435
435
  export type SharePayload = z.infer<typeof SharePayloadSchema>;
436
436
 
437
+ /* WHAT A PARKED CARD IS ABOUT: the document the turn wrote and is now asking a question against.
438
+ *
439
+ * A card asks for a decision; until this it carried no SUBJECT. The commonest shape of a real decision is "I
440
+ * analysed this and wrote it up, now choose", and the write-up went into a file whose card had already folded
441
+ * itself into `Write · +135 −0` twenty tool calls back. So the reader was asked to choose between options
442
+ * describing a document the chat had never shown them.
443
+ *
444
+ * Carried BY VALUE rather than as a path, for the same reason the diff on a tool call is: the bytes are already
445
+ * in hand when the card is raised, a path would make the card's meaning depend on a file that keeps changing
446
+ * under it, and a restored or published transcript has no workspace to go read. The path rides along anyway, so
447
+ * a document past the wire cap still has somewhere to send the reader.
448
+ *
449
+ * Nothing is asked of the MODEL for this. It calls `ask` exactly as before; the daemon knows what the turn
450
+ * wrote, because every write came past it as a frame (documents.ts decides which of them is a document). A
451
+ * harness that can see the answer must not spend prompt on asking the model to repeat it. */
452
+ export const CardDocumentSchema = z.object({
453
+ path: z.string().describe("Where it lives, as a workspace path."),
454
+ title: z.string().describe("What it is called: its opening heading, or its file name."),
455
+ markdown: z.string().describe("The document itself."),
456
+ truncated: z.boolean().optional().describe("It was clipped at the wire cap; the file on disk has more."),
457
+ plan: z.boolean().optional().describe("It is one of the CLI's plan files, written to be approved rather than merely read."),
458
+ });
459
+ export type CardDocument = z.infer<typeof CardDocumentSchema>;
460
+
437
461
  /* THE THREE RESTORABLE CARDS, named so the turn journal can hold them verbatim: a parked turn's raised cards
438
462
  * are written down beside its prompt (sandbox turn-journal.ts), and a daemon death under the park restores the
439
463
  * very same frames instead of ending the turn `interrupted`, the card the user was about to answer survives
@@ -444,11 +468,15 @@ const PlanCardSchema = z.object({
444
468
  kind: z.literal("plan").describe("The agent has written a plan and is waiting for a yes."),
445
469
  requestId: z.string().describe("What to send back when you answer."),
446
470
  text: z.string().describe("The plan itself."),
471
+ // Present when the plan text POINTS at a document instead of being one: the model wrote the real plan to a
472
+ // file and summarised it here. Absent when the text already is the whole plan, which is the ordinary case.
473
+ document: CardDocumentSchema.optional().describe("The write-up this plan refers to, when the plan itself is a pointer to one."),
447
474
  });
448
475
  const QuestionCardSchema = z.object({
449
476
  kind: z.literal("question").describe("The agent has asked you something and is waiting."),
450
477
  requestId: z.string().describe("What to send back when you answer."),
451
478
  questions: z.array(AskQuestionSchema).describe("What it wants to know."),
479
+ document: CardDocumentSchema.optional().describe("The document this turn wrote and is asking about, so the choice can be read beside it."),
452
480
  });
453
481
  const PermissionCardSchema = PermissionAskSchema.extend({
454
482
  kind: z.literal("permission").describe("The agent wants to use a tool it needs permission for."),
@@ -493,6 +521,9 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
493
521
  base: z.string(),
494
522
  unenforced: z.boolean().optional(),
495
523
  sync: z.object({ commits: z.number(), blocked: z.array(z.string()) }).optional(),
524
+ // The runner this turn executes on, when the conversation is placed remotely (runners/): the
525
+ // transcript's own statement of where the work is happening. Absent ⇒ this sandbox.
526
+ remote: z.string().optional(),
496
527
  }),
497
528
  // Emitted after a clean isolated turn whose delta auto-landed (or failed to): landed ⇒ the work is now
498
529
  // UNCOMMITTED changes in the main tree (the Changes panel is the review); conflicts ⇒ it stayed safely in
@@ -621,9 +652,10 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
621
652
  agentType: z.string().optional(),
622
653
  description: z.string().optional(),
623
654
  model: z.string().optional(),
655
+ // Which provider serves a `spawned` child (SubagentSessionSchema.provider), absent for an SDK
656
+ // subagent, whose provider is its parent's.
657
+ provider: z.string().optional(),
624
658
  background: z.boolean().optional(),
625
- // A delegation's tmux session, the one live view a subagent doesn't have (SubagentSessionSchema).
626
- terminal: z.string().optional(),
627
659
  }),
628
660
  z.object({
629
661
  kind: z.literal("subagent_update"),
@@ -913,6 +945,16 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
913
945
  "context-window-too-small",
914
946
  "subscription-required",
915
947
  "agent-busy",
948
+ /* THE SANDBOX HAS NO MEMORY LEFT TO RUN THIS TURN, refused before anything was spawned
949
+ * (platform/memory-admission.ts). Its own code because it is the only refusal here that is
950
+ * about the BOX rather than the request: the prompt, the model and the credential are all
951
+ * fine, and the identical request succeeds once something inside frees room, which is the
952
+ * opposite of context-window-too-small next door. Transient without being on a clock, so
953
+ * there is no resetsAt to offer — what changes the outcome is a turn finishing or a session
954
+ * closing, and the message says so. The client HOLDS the words for the same reason
955
+ * context-window-too-small does: they never reached a provider, and losing them to a
956
+ * capacity fact the user did not cause would be ours to answer for. */
957
+ "sandbox-memory-low",
916
958
  ])
917
959
  .optional(),
918
960
  // rate_limit only: when the exhausted window reopens (epoch seconds, from the stream's own
@@ -929,6 +971,25 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
929
971
  // Absent means there is nothing automatic to resume: a spent usage limit never has one,
930
972
  // and a refused credential has none once re-minting it has already been tried and failed.
931
973
  autoResume: z.enum(["scheduled", "available"]).optional(),
974
+ /* THE DAEMON IS STILL HOLDING THIS EXACT TURN, so the way on is to RE-RUN it rather than to send
975
+ * something after it. rate_limit only, and the counterpart to `autoResume` rather than a member of it:
976
+ * that field answers "is a machine bringing this back", and a spent allowance is the one failure where
977
+ * the answer is deliberately no (the allowance is the user's own budget to spend, turn-resume.ts). This
978
+ * answers the question that was never asked, "and if the user says go, what happens", which had exactly
979
+ * one possible answer for as long as it went unasked: a new user message reading "Continue".
980
+ *
981
+ * That answer was wrong in a way the chat could not show. The press is not a new instruction, it is the
982
+ * same one again, and appending it said otherwise to the only reader that matters: the provider session
983
+ * grew one "Continue" per press, each with a synthetic "No response requested." above it, so a chat that
984
+ * bounced four times handed the model four turns in which it appeared to have declined to answer. With
985
+ * this field the press re-runs the held turn instead, which is idempotent by construction (a second press
986
+ * finds a live turn and supersedes nothing) and leaves the transcript one row for one press.
987
+ *
988
+ * `ran` is whether the held turn got anywhere before it was refused, and it changes both what the model
989
+ * is told (RESUME_NOTES.limit vs .refused, and telling a model to carry on from work that never happened
990
+ * is how it comes to invent some) and what the strip can honestly say. A spent allowance refuses the
991
+ * FIRST request most of the time, so false is the common case, not the corner. */
992
+ held: z.object({ ran: z.boolean() }).optional(),
932
993
  /* provider-outage only: the shape of the wait. `retryAt` (epoch seconds) is when the next attempt is
933
994
  * due, not a fixed cadence, because an outage has no reset instant to aim at and hammering a provider
934
995
  * that is down only spends tokens on refusals, so each attempt waits longer than the last
@@ -993,6 +1054,23 @@ export const RESUME_NOTES = {
993
1054
  auth: `The Claude credential that interrupted this conversation has been renewed, and this turn resumed automatically. ${REPEATED}`,
994
1055
  outage: `The model provider was briefly unavailable and interrupted this conversation; this turn resumed automatically. ${REPEATED}`,
995
1056
  restart: `The sandbox restarted while this turn was running, which stopped it, and this turn resumed automatically once it came back. ${REPEATED}`,
1057
+ /* A SPENT ALLOWANCE STRANDS A TURN IN TWO SHAPES, and they must not share a note.
1058
+ *
1059
+ * `limit` is the mid-turn one and reads like its three neighbours above: the session holds real work, and
1060
+ * carrying on from it is exactly right.
1061
+ *
1062
+ * `refused` is the turn the provider turned away at the door, before the model read one word of it, and it
1063
+ * is the COMMONER of the two, because an allowance that is already spent refuses the first request it is
1064
+ * asked. REPEATED is actively wrong for it: "part of it was already completed in this session, continue from
1065
+ * that point instead of starting over" is an instruction to continue from work that does not exist, and a
1066
+ * model handed that instruction answers it by inventing the work. So it says the opposite, plainly.
1067
+ *
1068
+ * Both are unlike their neighbours in one way worth stating: nothing resumed automatically. A spent
1069
+ * allowance is the user's own budget and stays their call to spend (turn-resume.ts), so what re-ran this
1070
+ * turn was a person pressing Continue. */
1071
+ limit: `The model provider's usage allowance ran out while this turn was running, which stopped it, and it has been sent again. ${REPEATED}`,
1072
+ refused:
1073
+ "The model provider refused the previous attempt at this request outright, because its usage allowance was spent: no part of the request below was read or acted on, and nothing has been done towards it. It has been sent again, and starts from the beginning.",
996
1074
  // A turn that was PARKED on the user when the daemon died: nothing re-runs at boot, the card is restored
997
1075
  // instead, and this is the turn their answer starts (turn-resume.ts). What rides below the note is the
998
1076
  // answer itself, so the model picks the session back up at exactly the decision it had handed over.
@@ -1038,6 +1116,14 @@ const RESUME_DISCLOSURES: Record<ResumeReason, ResumeDisclosure> = {
1038
1116
  auth: { kind: "notice", text: "Claude sign-in renewed, this turn picked up where it left off." },
1039
1117
  outage: { kind: "notice", text: "The model provider came back, this turn picked up where it left off." },
1040
1118
  restart: { kind: "notice", text: "The sandbox came back, this turn picked up where it left off." },
1119
+ /* THE ONE PAIR NOBODY AUTOMATED, said in the passive voice the other three earn honestly and these two do
1120
+ * not: a person pressed Continue. Which is the whole reason these rows exist at all. A press used to append
1121
+ * the word "Continue" as a message of its own, so a chat that bounced off a spent allowance four times read
1122
+ * back as the user saying "Continue" four times to an agent that had answered none of them, and the provider
1123
+ * session the model actually reads accumulated all four (plus a synthetic "No response requested." per
1124
+ * press). One row for one press was never the problem; a row that claims the user said something new is. */
1125
+ limit: { kind: "notice", text: "Sent again after the allowance ran out mid-turn, picking up where it left off." },
1126
+ refused: { kind: "notice", text: "Sent again after the allowance refused it: nothing had run." },
1041
1127
  answered: { kind: "note", note: { title: "Picked back up after a sandbox restart", text: RESUME_NOTES.answered } },
1042
1128
  };
1043
1129
 
@@ -131,5 +131,12 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
131
131
  // The burn list for setup-time computer pairings. Identity, like sync's beside it, and for a sharper reason:
132
132
  // carrying it into another sandbox would mark that sandbox's own fresh pairing as already spent.
133
133
  { path: "host-pair-consumed.json", portability: "identity" },
134
+ // A runner's enrollment names THIS sandbox as its parent (runners/runners-store.ts): in another sandbox the
135
+ // digest would admit a socket whose runner still dials the old parent. Identity, both files, hosts' reasons.
136
+ { path: "runner-enrollments.json", portability: "identity" },
137
+ { path: "runner-pair-consumed.json", portability: "identity" },
138
+ // The runner-SIDE half: who this container belongs to and the token its reconnects present
139
+ // (runners/runner-identity.ts). Carried into another box it would dial the parent as this runner.
140
+ { path: "runner-identity.json", portability: "identity" },
134
141
  { path: "local-cert/", portability: "identity" },
135
142
  ];
package/src/index.ts CHANGED
@@ -59,6 +59,8 @@ export { historyContract } from "./contracts/history.contract.js";
59
59
  /* Deliberately NOT part of `sandboxContract` below: that map is the daemon's own HTTP surface, and this one is
60
60
  * spoken the other way round, over a connected computer's WebSocket, with the MACHINE implementing it. */
61
61
  export { hostContract } from "./contracts/host.contract.js";
62
+ // Same inversion, other direction of trust: spoken over a RUNNER's WebSocket, with the runner implementing it.
63
+ export { runnerContract } from "./contracts/runner.contract.js";
62
64
  export { intenticContract } from "./contracts/intentic.contract.js";
63
65
  export { inventoryContract } from "./contracts/inventory.contract.js";
64
66
  export { logsContract } from "./contracts/logs.contract.js";
@@ -104,8 +106,10 @@ export * from "./agent-run-model.js";
104
106
  export * from "./capability-env.js";
105
107
  export * from "./capability-secrets.js";
106
108
  export * from "./conversation-ids.js";
109
+ export * from "./documents.js";
107
110
  export * from "./fast-tier.js";
108
111
  export * from "./host-protocol.js";
112
+ export * from "./runner-protocol.js";
109
113
  export * from "./listener-protocol.js";
110
114
  export * from "./hostnames.js";
111
115
  export * from "./model-order.js";
@@ -115,6 +119,7 @@ export * from "./quick-model.js";
115
119
  export * from "./output-fields.js";
116
120
  export * from "./publish-drafts.js";
117
121
  export * from "./schemas.js";
122
+ export * from "./definition.js";
118
123
  export * from "./search-globs.js";
119
124
  export * from "./starter.js";
120
125
  export * from "./terminal-protocol.js";
@@ -0,0 +1,209 @@
1
+ import { z } from "zod";
2
+
3
+ /* THE RUNNER LINK'S EDGES: the handshake on /system/runners/connect, the env a runner boots with, and the
4
+ * placement value a turn request carries. The procedures spoken over the link once it exists live in
5
+ * contracts/runner.contract.ts; the design is docs/remote-runners-plan.md (workspace root).
6
+ *
7
+ * A runner is the sandbox image booted in runner mode: no browser owner, no tunnel, no public name. It dials
8
+ * its PARENT sandbox and executes turns the parent dispatches; the parent keeps owning the conversation. The
9
+ * host link (host-protocol.ts) is the pattern for everything here, one outbound socket that authenticates in
10
+ * its first frame, because a runner sits behind the same NATs and private networks a laptop does. */
11
+
12
+ // The env the runner container boots with: who to dial, and the single-use pairing that gets it enrolled.
13
+ // Named here so `ic runner up`, the Fly provisioner and the daemon's boot detection spell them identically.
14
+ export const RUNNER_PARENT_URL_ENV = "RUNNER_PARENT_URL";
15
+ export const RUNNER_PAIR_TOKEN_ENV = "RUNNER_PAIR_TOKEN";
16
+
17
+ /* The first frame on /system/runners/connect, plain JSON, never oRPC, for host-protocol.ts's reason: a socket
18
+ * must prove whose it is before a typed link attaches, and the proof cannot be a call on a link that does not
19
+ * exist yet. The token rides the FRAME, never the URL (edge logs). */
20
+ export const RunnerHelloSchema = z.object({
21
+ type: z.literal("runner-hello"),
22
+ // The runner's durable token, redeemed once from the pairing over /system/runners/enroll.
23
+ token: z.string(),
24
+ // The daemon build the runner runs, surfaced per runner so an old image is visible rather than
25
+ // mysteriously wrong.
26
+ version: z.string(),
27
+ /* Parity, reported rather than enforced: the parent knows what IT runs (image, channel, overlay hash) and
28
+ * shows a runner that drifted as "outdated" with a rebuild action. An outdated runner still runs turns, a
29
+ * stale toolchain is a fact the user can weigh, where a refusal would strand work. */
30
+ image: z.string(),
31
+ channel: z.string().optional(),
32
+ overlayHash: z.string().optional(),
33
+ });
34
+ export type RunnerHello = z.infer<typeof RunnerHelloSchema>;
35
+
36
+ // The URL a runner dials, given its parent's public URL. One builder, so `ic`, the Fly provisioner and the
37
+ // daemon route cannot disagree about where the door is (hostConnectUrl's rule).
38
+ export const runnerConnectUrl = (parentUrl: string): string => `${parentUrl.replace(/^http/, "ws").replace(/\/$/, "")}/system/runners/connect`;
39
+
40
+ // Where a runner redeems its pairing for the durable token, once, over plain HTTPS.
41
+ export const runnerEnrollUrl = (parentUrl: string): string => `${parentUrl.replace(/\/$/, "")}/system/runners/enroll`;
42
+
43
+ /* The parent's git door for ONE repository: stock `git fetch`/`git push` against the smart-HTTP pair the
44
+ * parent serves from its real git dirs (<historyRoot>/gits/<encoded id>), authenticated by the runner's own
45
+ * token as a bearer. One builder because three parties spell it: the parent's route, the runner's sync, and
46
+ * anyone debugging with a hand-typed clone. */
47
+ export const runnerGitUrl = (parentUrl: string, repo: string): string => `${parentUrl.replace(/\/$/, "")}/system/runners/git/${encodeURIComponent(repo)}`;
48
+
49
+ /* Where a runner's push LANDS in the parent's git dirs. Never refs/heads/agent/<id> directly: that branch is
50
+ * checked out in the parent's mirror worktree, and git itself refuses updating a checked-out ref, which is a
51
+ * safety property worth keeping rather than configuring away. The parent moves the branch by hard-resetting
52
+ * the mirror worktree to this ref, which advances the checked-out branch through the door git sanctions. */
53
+ export const runnerIncomingRef = (conversationId: string): string => `refs/runner-incoming/${conversationId}`;
54
+
55
+ /* THE CREDENTIAL DOORS: a runner's turns spend the ORIGIN sandbox's model providers, never accounts of their
56
+ * own. The shape is a service, not a sync: the parent resolves each turn's credential with the same code its
57
+ * local turns use and answers with the least that travels — an access token minted for the turn, or a route.
58
+ * Refresh tokens never leave the parent, which is what closes the rotation race two daemons refreshing one
59
+ * account would otherwise run. Design: docs/remote-runners-plan.md §8 (workspace root). */
60
+ export const runnerCredentialsUrl = (parentUrl: string): string => `${parentUrl.replace(/\/$/, "")}/system/runners/credentials`;
61
+ export const runnerCredentialRefreshUrl = (parentUrl: string): string => `${parentUrl.replace(/\/$/, "")}/system/runners/credentials/refresh`;
62
+
63
+ /* The parent's model translator, re-served to runners: subscription-routed turns (codex/grok/kimi under the
64
+ * Claude Code harness, OpenAI-protocol endpoints, the trial) authenticate against a translator whose auth
65
+ * files live on the PARENT's /history. Rather than syncing those (the same rotation race), the runner's
66
+ * harness dials this authenticated proxy and the parent forwards to its loopback translator. The bearer is
67
+ * the runner's own token. */
68
+ export const runnerTranslatorPath = "/system/runners/translator";
69
+ export const runnerTranslatorUrl = (parentUrl: string): string => `${parentUrl.replace(/\/$/, "")}${runnerTranslatorPath}`;
70
+
71
+ export const RunnerCredentialRequestSchema = z.object({
72
+ // The provider as the turn names it (absent = claude), an open vocabulary: endpoint/<id> included.
73
+ agent: z.string().optional(),
74
+ account: z.string().optional(),
75
+ model: z.string().optional(),
76
+ });
77
+ export type RunnerCredentialRequest = z.infer<typeof RunnerCredentialRequestSchema>;
78
+
79
+ /* What travels back, by kind:
80
+ * oauth — a native-Claude access token minted for this turn; `account` present when it is a
81
+ * stored account (then the refresh door re-mints mid-turn), absent for the parent's
82
+ * container-env fallback, which has nothing to rotate.
83
+ * parent-translator — run against the parent's translator through the proxy above; the runner supplies its
84
+ * own token as the bearer (the parent never echoes a credential it only holds hashed).
85
+ * endpoint — a foreign endpoint the runner can dial directly (an anthropic-protocol Endpoint
86
+ * capability), with the bearer that endpoint wants.
87
+ * A refusal is a value with the same codes local resolution uses, so the composer's connect gates read a
88
+ * remote refusal exactly as a local one. */
89
+ export const RunnerCredentialSchema = z.union([
90
+ z.object({ ok: z.literal(true), kind: z.literal("oauth"), accessToken: z.string(), account: z.string().optional() }),
91
+ z.object({ ok: z.literal(true), kind: z.literal("parent-translator"), model: z.string(), trial: z.boolean().optional() }),
92
+ z.object({
93
+ ok: z.literal(true),
94
+ kind: z.literal("endpoint"),
95
+ baseUrl: z.string(),
96
+ authToken: z.string(),
97
+ model: z.string(),
98
+ trial: z.boolean().optional(),
99
+ }),
100
+ z.object({
101
+ ok: z.literal(false),
102
+ code: z.enum(["subscription-required", "claude-reauth", "trial-unavailable"]).optional(),
103
+ message: z.string(),
104
+ }),
105
+ ]);
106
+ export type RunnerCredential = z.infer<typeof RunnerCredentialSchema>;
107
+
108
+ export const RunnerCredentialRefreshRequestSchema = z.object({
109
+ account: z.string().min(1),
110
+ // The token the harness was refused with, so the parent's rotation supersedes exactly that one and a
111
+ // token another turn already rotated is adopted, never re-refreshed (claude-credentials' own rule).
112
+ rejected: z.string().min(1),
113
+ });
114
+ export type RunnerCredentialRefreshRequest = z.infer<typeof RunnerCredentialRefreshRequestSchema>;
115
+
116
+ // `accessToken` absent ⇒ the parent could not re-mint (a revoked account); the harness gives up exactly as a
117
+ // local turn whose refresh returned nothing does.
118
+ export const RunnerCredentialRefreshSchema = z.object({ accessToken: z.string().optional() });
119
+ export type RunnerCredentialRefresh = z.infer<typeof RunnerCredentialRefreshSchema>;
120
+
121
+ // What a runner is, hardware-wise: what the placement picker shows and what a future scheduler weighs.
122
+ export const RunnerFactsSchema = z.object({
123
+ cpus: z.number().int().positive(),
124
+ memoryMb: z.number().int().positive(),
125
+ freeDiskMb: z.number().int().nonnegative(),
126
+ // 0..1 of the last minute, the coarse "is it busy" a picker needs, not a metrics feed.
127
+ load: z.number().nonnegative(),
128
+ });
129
+ export type RunnerFacts = z.infer<typeof RunnerFactsSchema>;
130
+
131
+ // One runner as the owner's views list it: enrolled state plus whatever the hub knows right now, the
132
+ // HostSummary shape retold for a runner (no platform/scopes, parity instead).
133
+ export const RunnerSummarySchema = z.object({
134
+ id: z.string(),
135
+ online: z.boolean(),
136
+ version: z.string().optional(),
137
+ image: z.string().optional(),
138
+ channel: z.string().optional(),
139
+ overlayHash: z.string().optional(),
140
+ facts: RunnerFactsSchema.optional(),
141
+ lastSeen: z.number().optional(),
142
+ });
143
+ export type RunnerSummary = z.infer<typeof RunnerSummarySchema>;
144
+
145
+ /* A workspace sync, narrated as it happens: a first contact clones whole repositories, and a person may be
146
+ * watching the "preparing runner" state, so the lines travel while they are produced (runSandboxFlow's
147
+ * argument). `op` says which direction: `pull` brings the runner's checkout of the conversation's branch (and
148
+ * each repo's main line) up to date before a turn, `push` returns the branch after one.
149
+ *
150
+ * `repos` is the conversation's composition as the parent recorded it, because only the parent can know it:
151
+ * the runner's own discovery would see whatever its mirror held from LAST time, and a repo added to the
152
+ * workspace since would silently fall out of the conversation. Each entry names the repo id (the git-door
153
+ * address), the workspace-relative dir the checkout lives at, and the repo's own main branch name. */
154
+ export const RunnerSyncSchema = z.object({
155
+ op: z.enum(["pull", "push"]),
156
+ conversationId: z.string().min(1),
157
+ branch: z.string().min(1),
158
+ repos: z.array(
159
+ z.object({
160
+ repo: z.string().min(1),
161
+ // "" for the workspace root itself; every other repo sits at its root-relative dir.
162
+ dir: z.string(),
163
+ mainBranch: z.string().min(1),
164
+ }),
165
+ ),
166
+ });
167
+ export type RunnerSync = z.infer<typeof RunnerSyncSchema>;
168
+
169
+ export const RunnerSyncLineSchema = z.union([
170
+ z.object({ kind: z.literal("line"), text: z.string() }),
171
+ z.object({ kind: z.literal("done"), ok: z.boolean(), detail: z.string().optional() }),
172
+ ]);
173
+ export type RunnerSyncLine = z.infer<typeof RunnerSyncLineSchema>;
174
+
175
+ /* One turn, as dispatched. Deliberately NOT AgentTurnSchema: that schema is the browser's request to the
176
+ * parent, full of fields the parent resolves before anything executes (persona, account, title, forks). What
177
+ * crosses the link is the residue after resolution, what the runner's harness spawn actually needs, plus the
178
+ * attachments inline, because the runner has no route onto the parent's attachment store. */
179
+ export const RunnerTurnSchema = z.object({
180
+ conversationId: z.string().min(1),
181
+ branch: z.string().min(1),
182
+ prompt: z.string(),
183
+ provider: z.string(),
184
+ harness: z.string(),
185
+ model: z.string().optional(),
186
+ effort: z.string().optional(),
187
+ thinking: z.boolean().optional(),
188
+ fast: z.boolean().optional(),
189
+ // Which of the ORIGIN sandbox's connected accounts pays for the turn: the runner resolves credentials
190
+ // against the parent (the credential doors below), so this names an account THERE.
191
+ account: z.string().optional(),
192
+ sessionId: z.string().optional(),
193
+ // Base64 because frames are JSON: small by policy (the request schema already caps attachment count), and
194
+ // a turn's attachments are the one payload with no git road to travel. `path` is the workspace-relative
195
+ // path the prompt already names; the runner writes the bytes there so the words and the file agree.
196
+ attachments: z.array(z.object({ path: z.string().min(1), bytesBase64: z.string() })).optional(),
197
+ });
198
+ export type RunnerTurn = z.infer<typeof RunnerTurnSchema>;
199
+
200
+ /* WHERE A CONVERSATION RUNS, decided on its first turn and owned by the conversation from then on, exactly as
201
+ * `isolated` placement already works (agent.routes.ts): later turns follow the registry entry, not whichever
202
+ * client sends them. `runner` implies isolation, a remote conversation is always branch-anchored, because its
203
+ * branch is the unit that moves between machines. Absent means local, which is why this is optional
204
+ * everywhere: the default costs nothing to anyone who never uses it. */
205
+ export const AgentPlacementSchema = z.union([
206
+ z.object({ kind: z.literal("local") }),
207
+ z.object({ kind: z.literal("runner"), id: z.string().min(1) }),
208
+ ]);
209
+ export type AgentPlacement = z.infer<typeof AgentPlacementSchema>;