@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.
- package/README.md +11 -0
- package/dist/contracts/agent.contract.d.ts +53 -6
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +10 -1
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +6 -1
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +293 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.js +10 -1
- package/dist/contracts/capabilities.contract.js.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +15 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +12 -2
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/history.contract.d.ts +6 -1
- package/dist/contracts/history.contract.d.ts.map +1 -1
- package/dist/contracts/host.contract.d.ts +4 -0
- package/dist/contracts/host.contract.d.ts.map +1 -1
- package/dist/contracts/panels.contract.d.ts +1 -0
- package/dist/contracts/panels.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +876 -0
- package/dist/contracts/runner.contract.d.ts.map +1 -0
- package/dist/contracts/runner.contract.js +17 -0
- package/dist/contracts/runner.contract.js.map +1 -0
- package/dist/contracts/system.contract.d.ts +6 -3
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/definition.d.ts +961 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +71 -0
- package/dist/definition.js.map +1 -0
- package/dist/documents.d.ts +7 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +27 -0
- package/dist/documents.js.map +1 -0
- package/dist/events.d.ts +66 -6
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +17 -1
- package/dist/events.js.map +1 -1
- package/dist/history-state.d.ts.map +1 -1
- package/dist/history-state.js +3 -0
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +396 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/runner-protocol.d.ts +134 -0
- package/dist/runner-protocol.d.ts.map +1 -0
- package/dist/runner-protocol.js +95 -0
- package/dist/runner-protocol.js.map +1 -0
- package/dist/schemas.d.ts +194 -32
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +80 -25
- package/dist/schemas.js.map +1 -1
- package/dist/workspace-state.d.ts +9 -0
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +2 -0
- package/dist/workspace-state.js.map +1 -1
- package/package.json +6 -6
- package/src/contracts/agent.contract.ts +19 -0
- package/src/contracts/capabilities.contract.ts +16 -0
- package/src/contracts/runner.contract.ts +49 -0
- package/src/definition.ts +171 -0
- package/src/documents.test.ts +66 -0
- package/src/documents.ts +71 -0
- package/src/events.ts +88 -2
- package/src/history-state.ts +7 -0
- package/src/index.ts +5 -0
- package/src/runner-protocol.ts +209 -0
- package/src/schemas.ts +225 -65
- package/src/workspace-state.test.ts +4 -0
- 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
|
|
package/src/history-state.ts
CHANGED
|
@@ -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>;
|