@intentic/sandbox-contract 1.345.0 → 1.346.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 +26 -14
- package/dist/contracts/accounts.contract.d.ts +0 -80
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +33 -2
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +910 -29
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +43 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/device.contract.d.ts +2 -1
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/endpoints.contract.d.ts +0 -1
- package/dist/contracts/endpoints.contract.d.ts.map +1 -1
- package/dist/contracts/providers.contract.d.ts +0 -1
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +134 -123
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +4 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +102 -1
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +0 -80
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +43 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +42 -37
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/failure-rows.d.ts +14 -0
- package/dist/events/failure-rows.d.ts.map +1 -0
- package/dist/events/failure-rows.js +109 -0
- package/dist/events/failure-rows.js.map +1 -0
- package/dist/events/sandbox-notice.d.ts +16 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -1
- package/dist/events/sandbox-notice.js +1 -0
- package/dist/events/sandbox-notice.js.map +1 -1
- package/dist/events/system-events.d.ts +68 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/system-events.js +7 -1
- package/dist/events/system-events.js.map +1 -1
- package/dist/index.d.ts +1158 -303
- package/dist/index.d.ts.map +1 -1
- package/dist/models/agent-lanes.d.ts.map +1 -1
- package/dist/models/agent-lanes.js +3 -5
- package/dist/models/agent-lanes.js.map +1 -1
- package/dist/models/agent-standing.d.ts +3 -20
- package/dist/models/agent-standing.d.ts.map +1 -1
- package/dist/models/agent-standing.js +12 -37
- package/dist/models/agent-standing.js.map +1 -1
- package/dist/models/model-pins.d.ts +3 -0
- package/dist/models/model-pins.d.ts.map +1 -1
- package/dist/models/model-pins.js +2 -0
- package/dist/models/model-pins.js.map +1 -1
- package/dist/models/plan-pools.d.ts +23 -5
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +116 -33
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/protocol/doors/runner-protocol.d.ts +1 -0
- package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/runner-protocol.js +1 -0
- package/dist/protocol/doors/runner-protocol.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +3 -1
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +4 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +1 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/schemas/agent-next.d.ts +21 -0
- package/dist/schemas/agent-next.d.ts.map +1 -0
- package/dist/schemas/agent-next.js +21 -0
- package/dist/schemas/agent-next.js.map +1 -0
- package/dist/schemas/agent-search.d.ts +45 -0
- package/dist/schemas/agent-search.d.ts.map +1 -0
- package/dist/schemas/agent-search.js +28 -0
- package/dist/schemas/agent-search.js.map +1 -0
- package/dist/schemas/agent.d.ts +9 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +14 -10
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents-list.d.ts +16 -0
- package/dist/schemas/agents-list.d.ts.map +1 -1
- package/dist/schemas/agents.d.ts +48 -43
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +10 -33
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/compose-overlay.d.ts +9 -0
- package/dist/schemas/compose-overlay.d.ts.map +1 -0
- package/dist/schemas/compose-overlay.js +11 -0
- package/dist/schemas/compose-overlay.js.map +1 -0
- package/dist/schemas/composer-drafts.d.ts +169 -0
- package/dist/schemas/composer-drafts.d.ts.map +1 -0
- package/dist/schemas/composer-drafts.js +60 -0
- package/dist/schemas/composer-drafts.js.map +1 -0
- package/dist/schemas/device-commands.d.ts +63 -0
- package/dist/schemas/device-commands.d.ts.map +1 -0
- package/dist/schemas/device-commands.js +42 -0
- package/dist/schemas/device-commands.js.map +1 -0
- package/dist/schemas/device-folder.d.ts +11 -0
- package/dist/schemas/device-folder.d.ts.map +1 -1
- package/dist/schemas/device-folder.js +1 -0
- package/dist/schemas/device-folder.js.map +1 -1
- package/dist/schemas/devices.d.ts +14 -67
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +7 -45
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/hosts.d.ts +27 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +21 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/privacy-egress.d.ts +41 -0
- package/dist/schemas/privacy-egress.d.ts.map +1 -0
- package/dist/schemas/privacy-egress.js +30 -0
- package/dist/schemas/privacy-egress.js.map +1 -0
- package/dist/schemas/privacy.d.ts +1 -0
- package/dist/schemas/privacy.d.ts.map +1 -1
- package/dist/schemas/privacy.js +1 -0
- package/dist/schemas/privacy.js.map +1 -1
- package/dist/schemas/providers/plan-limits.d.ts +2 -130
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +5 -23
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +0 -82
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/provider-oauth.js +1 -6
- package/dist/schemas/providers/provider-oauth.js.map +1 -1
- package/dist/schemas/providers/refusals.d.ts +64 -0
- package/dist/schemas/providers/refusals.d.ts.map +1 -0
- package/dist/schemas/providers/refusals.js +33 -0
- package/dist/schemas/providers/refusals.js.map +1 -0
- package/dist/schemas/sessions.js +1 -1
- package/dist/schemas/sessions.js.map +1 -1
- package/dist/schemas/settings.d.ts +4 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +2 -0
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +16 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +11 -0
- package/dist/schemas/system.js.map +1 -1
- package/dist/state/definition.d.ts +8 -0
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +1 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/sandbox-build.d.ts +20 -0
- package/dist/state/sandbox-build.d.ts.map +1 -0
- package/dist/state/sandbox-build.js +43 -0
- package/dist/state/sandbox-build.js.map +1 -0
- package/dist/state/ways-back.d.ts +4 -0
- package/dist/state/ways-back.d.ts.map +1 -0
- package/dist/state/ways-back.js +8 -0
- package/dist/state/ways-back.js.map +1 -0
- package/dist/text/land-rows.d.ts +14 -0
- package/dist/text/land-rows.d.ts.map +1 -0
- package/dist/text/land-rows.js +86 -0
- package/dist/text/land-rows.js.map +1 -0
- package/dist/text/transcript-fold.d.ts +4 -2
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +21 -187
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +6 -6
- package/src/contracts/agents.contract.ts +49 -25
- package/src/events/agent-events.ts +95 -78
- package/src/events/failure-rows.ts +148 -0
- package/src/events/sandbox-notice.ts +11 -2
- package/src/events/system-events.ts +14 -4
- package/src/ids/project-dir.test.ts +2 -2
- package/src/ids/project-dir.ts +4 -4
- package/src/models/agent-lanes.ts +7 -21
- package/src/models/agent-runtimes.ts +2 -2
- package/src/models/agent-standing.ts +31 -96
- package/src/models/model-pins.test.ts +31 -1
- package/src/models/model-pins.ts +17 -0
- package/src/models/plan-pools.test.ts +159 -24
- package/src/models/plan-pools.ts +215 -83
- package/src/protocol/doors/runner-protocol.ts +4 -1
- package/src/protocol/raw/raw-routes.ts +1 -0
- package/src/schemas/agent-next.ts +34 -0
- package/src/schemas/agent-search.ts +51 -0
- package/src/schemas/agent.ts +21 -12
- package/src/schemas/agents.ts +24 -63
- package/src/schemas/compose-overlay.test.ts +35 -0
- package/src/schemas/compose-overlay.ts +19 -0
- package/src/schemas/composer-drafts.ts +86 -0
- package/src/schemas/device-commands.ts +89 -0
- package/src/schemas/device-folder.ts +9 -0
- package/src/schemas/devices.ts +24 -106
- package/src/schemas/environment.ts +8 -9
- package/src/schemas/hosts.ts +56 -0
- package/src/schemas/machine-presence.test.ts +33 -0
- package/src/schemas/privacy-egress.test.ts +83 -0
- package/src/schemas/privacy-egress.ts +86 -0
- package/src/schemas/privacy.ts +4 -0
- package/src/schemas/project-delivery.ts +1 -1
- package/src/schemas/providers/plan-limits.test.ts +16 -0
- package/src/schemas/providers/plan-limits.ts +8 -49
- package/src/schemas/providers/provider-oauth.ts +1 -13
- package/src/schemas/providers/refusals.ts +62 -0
- package/src/schemas/sessions.ts +1 -1
- package/src/schemas/settings.ts +2 -0
- package/src/schemas/system.ts +15 -0
- package/src/state/runtime-state.ts +5 -0
- package/src/state/sandbox-build.test.ts +95 -0
- package/src/state/sandbox-build.ts +96 -0
- package/src/state/ways-back.test.ts +36 -0
- package/src/state/ways-back.ts +19 -0
- package/src/state/workspace-state.ts +1 -1
- package/src/text/land-rows.ts +111 -0
- package/src/text/transcript-fold.test.ts +38 -9
- package/src/text/transcript-fold.ts +30 -232
- package/dist/events/undelivered.d.ts +0 -10
- package/dist/events/undelivered.d.ts.map +0 -1
- package/dist/events/undelivered.js +0 -22
- package/dist/events/undelivered.js.map +0 -1
- package/dist/state/history-state.d.ts +0 -3
- package/dist/state/history-state.d.ts.map +0 -1
- package/dist/state/history-state.js +0 -153
- package/dist/state/history-state.js.map +0 -1
- package/src/events/undelivered.ts +0 -33
- package/src/state/history-state.ts +0 -313
package/src/schemas/agents.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// agents: the conversation fleet
|
|
2
2
|
import { LandingDeliverySchema } from "./project-delivery.js";
|
|
3
|
+
import { AgentNextSchema } from "./agent-next.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { AgentHarnessSchema, AgentOriginSchema, AgentProviderSchema, ConversationQueueSchema, ForkedFromSchema, FreshSessionSchema } from "./agent.js";
|
|
5
6
|
import { LoopStateSchema } from "./loops.js";
|
|
@@ -294,21 +295,27 @@ export const AgentSummarySchema = z.object({
|
|
|
294
295
|
.describe(
|
|
295
296
|
"Which kind of failure it was, as the turn's own error frame coded it. Absent for a failure nothing could classify, which reads as the plain red line it is.",
|
|
296
297
|
),
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
limitScheduled: z.boolean().optional().describe("Whether the held turn is already booked to go again at the reset, so nobody has to press anything."),
|
|
302
|
-
// Account a booked move is sending the held turn to; set alongside `limitScheduled`, cleared by the move's own turn.
|
|
303
|
-
limitMoving: z.string().optional().describe("The account the held turn is being moved to by the owner's policy, while that move is booked."),
|
|
304
|
-
// The resume pass's own instant for the booked re-run, the one it fires by (limit-due.ts), so a card states when it
|
|
305
|
-
// goes rather than inferring it from the reset: they differ once the account it goes on is still spent at the reset.
|
|
306
|
-
limitNextAt: z
|
|
307
|
-
.number()
|
|
298
|
+
// Who worded `failure`, as the turn's error frame said (its `own`): a card leads with those words only when they are
|
|
299
|
+
// the sandbox's own sentence, as the chat's notice does (failureView), and with its kind's line otherwise.
|
|
300
|
+
failureOwn: z
|
|
301
|
+
.boolean()
|
|
308
302
|
.optional()
|
|
309
303
|
.describe(
|
|
310
|
-
"
|
|
304
|
+
"Whether `failure` is the sandbox's own sentence rather than the provider's or the harness's words. Absent for theirs, and for an ending recorded before it was said.",
|
|
311
305
|
),
|
|
306
|
+
// Epoch seconds the spent allowance reopens; absent means genuinely unknown (Grok, Cursor) — never guess one.
|
|
307
|
+
limitResetsAt: z.number().optional().describe("When the spent allowance reopens, in epoch seconds. Absent when the provider publishes no instant."),
|
|
308
|
+
limitHeld: z.boolean().optional().describe("Whether the refused turn is held whole, so sending again re-runs it instead of appending to it."),
|
|
309
|
+
// What a sandbox older than `next` (before 1.343.0) said in its place, read so the editor can derive `next` from it
|
|
310
|
+
// (agentStatus-standing.ts, withNext): the hosted editor meets such a sandbox for as long as it stays off the release
|
|
311
|
+
// that dropped them, which carried a Breaking-Note and so is never taken by itself. No sandbox sends them since.
|
|
312
|
+
limitScheduled: z.boolean().optional().describe("Sent only by sandboxes older than `next`: whether the held turn was booked to go again by itself."),
|
|
313
|
+
limitMoving: z.string().optional().describe("Sent only by sandboxes older than `next`: the account a booked move was taking the held turn to."),
|
|
314
|
+
limitNextAt: z.number().optional().describe("Sent only by sandboxes older than `next`: when the booked re-run went, in epoch seconds."),
|
|
315
|
+
// The one answer to "does it go by itself, and when" (agent-next.ts), derived from what it holds every time it is read.
|
|
316
|
+
next: AgentNextSchema.optional().describe(
|
|
317
|
+
"What carries it on next with nobody pressing anything, and when: a held turn re-run, a message held for memory, or messages booked for later. Absent when only a press moves it.",
|
|
318
|
+
),
|
|
312
319
|
provider: AgentProviderSchema.describe("Which model provider it runs on."),
|
|
313
320
|
harness: AgentHarnessSchema.describe("Which agentic loop it runs on."),
|
|
314
321
|
// Latched with the conversation, so the card can say where work runs without asking anything else.
|
|
@@ -475,7 +482,8 @@ export const AgentSummarySchema = z.object({
|
|
|
475
482
|
.describe(
|
|
476
483
|
"When somebody last opened it, in milliseconds. Newer activity than this is what makes it unread. Kept by the sandbox rather than by a browser, so clearing site data or picking up a phone does not resurrect every badge.",
|
|
477
484
|
),
|
|
478
|
-
//
|
|
485
|
+
// Derived from the drafts the sandbox holds (composer-drafts.ts), or reported by a client that keeps its own, so the
|
|
486
|
+
// unattended archive sweep never files away a conversation somebody is still writing to.
|
|
479
487
|
unsentAt: z
|
|
480
488
|
.number()
|
|
481
489
|
.optional()
|
|
@@ -512,10 +520,10 @@ export const AgentSummarySchema = z.object({
|
|
|
512
520
|
.describe(
|
|
513
521
|
"The last attempt to bring its work into the workspace failed, and why. Cleared by the next land that goes through. Absent when nothing failed, and from a sandbox older than it.",
|
|
514
522
|
),
|
|
515
|
-
// The land's own last word on the owner's folder, for a
|
|
523
|
+
// The land's own last word on the owner's folder, for a sandbox made for one folder: written there,
|
|
516
524
|
// partly kept out, waiting for the computer, or refused (schemas/project-delivery.ts).
|
|
517
525
|
delivery: LandingDeliverySchema.optional().describe(
|
|
518
|
-
"What became of its last land in the folder on the owner's computer, for a
|
|
526
|
+
"What became of its last land in the folder on the owner's computer, for a sandbox made for one folder. Absent for any other land, and from a sandbox older than it.",
|
|
519
527
|
),
|
|
520
528
|
// Only the causes that still hold, re-read live (conversations/land/standing.ts), never the stored report's own list: a
|
|
521
529
|
// blocker the user has since cleared is not something to offer them an action about.
|
|
@@ -809,53 +817,6 @@ export const AgentsRemovedSchema = z.object({
|
|
|
809
817
|
),
|
|
810
818
|
});
|
|
811
819
|
export type AgentsRemoved = z.infer<typeof AgentsRemovedSchema>;
|
|
812
|
-
// Searches only speech, both sides of the conversation, never thinking, tool output or protocol text, which would
|
|
813
|
-
// otherwise match nearly the whole board. Two-char minimum; `caseSensitive` off means case-insensitive, matching
|
|
814
|
-
// workspace search's own switch.
|
|
815
|
-
export const AgentSearchQuerySchema = z.object({
|
|
816
|
-
query: z
|
|
817
|
-
.string()
|
|
818
|
-
.trim()
|
|
819
|
-
.min(2)
|
|
820
|
-
.describe(
|
|
821
|
-
"What to look for. Searched against what was said, both sides of the conversation, and nothing else: not the thinking, not the tool output, which between them name nearly every identifier in the workspace and would return most of the board.",
|
|
822
|
-
),
|
|
823
|
-
caseSensitive: z.stringbool().optional().describe("Whether capitals matter."),
|
|
824
|
-
});
|
|
825
|
-
// Why a row survived the filter: the matched line, windowed, and who said it. `speaker` rides with the text since agent
|
|
826
|
-
// prose alone can read as something the user typed.
|
|
827
|
-
export const SpeakerSchema = z.enum(["user", "agent"]);
|
|
828
|
-
export type Speaker = z.infer<typeof SpeakerSchema>;
|
|
829
|
-
export const MatchSnippetSchema = z.object({
|
|
830
|
-
text: z.string().describe("The matching line, with a little either side of it."),
|
|
831
|
-
speaker: SpeakerSchema.describe(
|
|
832
|
-
"Who said it. Carried with the words rather than beside them, because a line of the agent's prose under a card reads as something you typed until the row says otherwise.",
|
|
833
|
-
),
|
|
834
|
-
});
|
|
835
|
-
export type MatchSnippet = z.infer<typeof MatchSnippetSchema>;
|
|
836
|
-
// One matching agent and its evidence; `snippet` is absent for a title match, which the card already shows.
|
|
837
|
-
export const AgentMatchSchema = z.object({
|
|
838
|
-
id: z.string().describe("Which conversation matched."),
|
|
839
|
-
snippet: MatchSnippetSchema.optional().describe(
|
|
840
|
-
"Why, in its own words. Absent when the title was the match, which the card already shows: repeating it underneath is noise where evidence was wanted.",
|
|
841
|
-
),
|
|
842
|
-
});
|
|
843
|
-
export type AgentMatch = z.infer<typeof AgentMatchSchema>;
|
|
844
|
-
// How many agents were actually read, so the board can say a search saw less than the whole fleet.
|
|
845
|
-
export const AgentSearchResultSchema = z.object({
|
|
846
|
-
matches: z.array(AgentMatchSchema).describe("What matched, from the live fleet and the archive together."),
|
|
847
|
-
scanned: z
|
|
848
|
-
.number()
|
|
849
|
-
.describe(
|
|
850
|
-
"How many conversations were actually read, so a screen can say when a search saw less than everything rather than implying it saw all of it.",
|
|
851
|
-
),
|
|
852
|
-
indexing: z
|
|
853
|
-
.boolean()
|
|
854
|
-
.describe(
|
|
855
|
-
"Whether what was said is still being read in the background. True means this answer can still grow, so a screen must say it is incomplete rather than presenting it as the whole list.",
|
|
856
|
-
),
|
|
857
|
-
});
|
|
858
|
-
export type AgentSearchResult = z.infer<typeof AgentSearchResultSchema>;
|
|
859
820
|
// The user-chosen display title, bounded like sanitizeTitle's own cap.
|
|
860
821
|
export const AgentRenameSchema = z.object({
|
|
861
822
|
id: z.string().min(1).describe("Which conversation."),
|
|
@@ -890,7 +851,7 @@ export const AgentAutoLandSchema = z.object({
|
|
|
890
851
|
"Whether its work merges automatically when a turn finishes. Null clears the override and goes back to following the sandbox-wide setting, so a conversation does not sit holding a frozen copy of a default it has quietly stopped following.",
|
|
891
852
|
),
|
|
892
853
|
});
|
|
893
|
-
// A composer's unsent words, reported by
|
|
854
|
+
// A composer's unsent words, reported by a client that keeps them to itself rather than writing them as a draft.
|
|
894
855
|
export const AgentUnsentSchema = z.object({
|
|
895
856
|
id: z.string().min(1).describe("Which conversation."),
|
|
896
857
|
at: z
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { packageRoot } from "@intentic/constants/node";
|
|
5
|
+
import { ComposedOverlayLineSchema } from "./compose-overlay.js";
|
|
6
|
+
|
|
7
|
+
// The lines a target image's composer prints, as examples: golden/compose-overlay.json, which ic's reader is held to
|
|
8
|
+
// (_sandbox/ic/src/sandbox/composition.rs). This is the TS half: each is a line the contract reads, its hash its overlay's.
|
|
9
|
+
|
|
10
|
+
interface ComposeCase {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly base: string;
|
|
13
|
+
readonly line: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// SAFETY: the golden is this package's own file; the assertions below fail on any field it lacks.
|
|
17
|
+
const GOLDEN = JSON.parse(readFileSync(join(packageRoot(import.meta.url), "golden", "compose-overlay.json"), "utf8")) as { readonly cases: readonly ComposeCase[] };
|
|
18
|
+
|
|
19
|
+
test("every golden line is a composer's line, its hash the overlay's own and its FROM the base it was asked for", () => {
|
|
20
|
+
expect(GOLDEN.cases.length).toBeGreaterThan(2);
|
|
21
|
+
for (const { base, line } of GOLDEN.cases) {
|
|
22
|
+
const read = ComposedOverlayLineSchema.parse(line);
|
|
23
|
+
expect(line).toEqual(read);
|
|
24
|
+
if (read.overlay === null) {
|
|
25
|
+
expect(read.hash).toBeUndefined();
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
expect(read.hash).toBe(createHash("sha256").update(read.overlay).digest("hex"));
|
|
29
|
+
expect(read.overlay).toContain(`\nFROM ${base}\n`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("a line in another format is not one this contract reads", () => {
|
|
34
|
+
expect(ComposedOverlayLineSchema.safeParse({ compose: 2, overlay: null }).success).toBe(false);
|
|
35
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// WHAT A TARGET IMAGE COMPOSES FOR A SANDBOX, as the host reads it: the one line the image's own composer prints
|
|
4
|
+
// (`node /opt/sandbox/dist/environment/compose-overlay.js --base <image>`, _sandbox/sandbox/src/environment/compose-overlay.ts) over read-only
|
|
5
|
+
// mounts of the sandbox's volumes before a swap onto it, which `ic` builds instead of the overlay the running daemon
|
|
6
|
+
// composed (_sandbox/ic/src/sandbox/composition.rs). golden/compose-overlay.json holds examples the Rust test reads.
|
|
7
|
+
|
|
8
|
+
// The one format `ic` reads; any other, or none, sends it back to the running daemon's overlay.
|
|
9
|
+
export const COMPOSE_OVERLAY_FORMAT = 1;
|
|
10
|
+
|
|
11
|
+
export const ComposedOverlayLineSchema = z.object({
|
|
12
|
+
compose: z.literal(COMPOSE_OVERLAY_FORMAT).describe("The format of this line. A reader takes no other."),
|
|
13
|
+
overlay: z
|
|
14
|
+
.string()
|
|
15
|
+
.nullable()
|
|
16
|
+
.describe("The Dockerfile the image's daemon composes on its first boot, byte for byte; null when the sandbox wants no overlay on it."),
|
|
17
|
+
hash: z.string().optional().describe("sha256 of `overlay`, what the new container is stamped with (SANDBOX_ENVIRONMENT_HASH). Absent with no overlay."),
|
|
18
|
+
});
|
|
19
|
+
export type ComposedOverlayLine = z.infer<typeof ComposedOverlayLineSchema>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ATTACHMENT_LIMIT, ConversationIdSchema, ForkOfSchema, FreshOfSchema, QueuedServingSchema } from "./agent.js";
|
|
3
|
+
|
|
4
|
+
// A PERSON'S UNSENT WORDS FOR ONE CONVERSATION, held by the sandbox. Until 2026-10-20 a composer's words lived only in the
|
|
5
|
+
// browser: in each window's tab snapshot, moved into a per-browser "closed drafts" set when the tab closed and back out
|
|
6
|
+
// when it reopened, and broadcast between windows as previews. Every move was a chance to lose or duplicate them: a
|
|
7
|
+
// popped-out chat and the board each held a copy for a round trip, two windows wrote the same chat's words over each
|
|
8
|
+
// other, and another device never saw them at all. Now the sandbox holds the one record, per person and conversation, and
|
|
9
|
+
// a browser keeps only an optimistic copy of it. A draft never goes by itself (AgentSummary.next never names one), and it
|
|
10
|
+
// is not egress: no model reads it until the person sends it, when it becomes the message the privacy shield judges.
|
|
11
|
+
|
|
12
|
+
// The words as the composer holds them: what the message will say, its files, and who it will go to when it opens a
|
|
13
|
+
// conversation (the pick a draft for a conversation the sandbox has never seen has nowhere else to live).
|
|
14
|
+
const draftWords = {
|
|
15
|
+
text: z.string().max(200_000).describe("The words, exactly as typed."),
|
|
16
|
+
attachments: z
|
|
17
|
+
.array(z.string().min(1))
|
|
18
|
+
.max(ATTACHMENT_LIMIT)
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Files staged with it, as workspace paths already uploaded."),
|
|
21
|
+
serves: QueuedServingSchema.optional().describe(
|
|
22
|
+
"Who the message is to go to: the provider, loop, model, effort and account picked in the composer. Absent when nothing was picked.",
|
|
23
|
+
),
|
|
24
|
+
forkOf: ForkOfSchema.optional().describe("Where the conversation this draft opens is cut from, when it is a fork no turn has started yet."),
|
|
25
|
+
freshOf: FreshOfSchema.optional().describe("The work a fresh session this draft opens starts from, when no turn has started it yet."),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const ComposerDraftSchema = z.object({
|
|
29
|
+
conversationId: ConversationIdSchema.describe("The conversation the words are for. It may not exist yet: a new chat's words are a draft before its first turn."),
|
|
30
|
+
...draftWords,
|
|
31
|
+
since: z.number().describe("When the composer first held these words unsent, in milliseconds."),
|
|
32
|
+
revision: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.positive()
|
|
36
|
+
.describe("Moves with every write of any of your drafts. A write or a clear naming an older one is answered with the newer draft instead."),
|
|
37
|
+
});
|
|
38
|
+
export type ComposerDraft = z.infer<typeof ComposerDraftSchema>;
|
|
39
|
+
|
|
40
|
+
export const ComposerDraftsSchema = z.object({
|
|
41
|
+
drafts: z.array(ComposerDraftSchema).describe("Every draft you hold in this sandbox, newest first. Only your own: another member's unsent words are theirs."),
|
|
42
|
+
revision: z
|
|
43
|
+
.number()
|
|
44
|
+
.int()
|
|
45
|
+
.nonnegative()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe(
|
|
48
|
+
"The newest revision handed out when this list was read. A draft you hold at a revision past it was written after the read, so its absence here says nothing about it.",
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
export type ComposerDrafts = z.infer<typeof ComposerDraftsSchema>;
|
|
52
|
+
|
|
53
|
+
export const DraftWriteSchema = z.object({
|
|
54
|
+
id: ConversationIdSchema.describe("The conversation the words are for."),
|
|
55
|
+
...draftWords,
|
|
56
|
+
since: z.number().optional().describe("When the words were first held unsent. Leave it out to keep the draft's own, or to start it now."),
|
|
57
|
+
base: z
|
|
58
|
+
.number()
|
|
59
|
+
.int()
|
|
60
|
+
.positive()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe(
|
|
63
|
+
"The draft's revision these words were edited from. When the draft has been written since (another window, another device), nothing is written and the newer draft comes back with `conflict`, so no one's words are overwritten unseen. Leave it out to write a draft you have never read.",
|
|
64
|
+
),
|
|
65
|
+
});
|
|
66
|
+
export type DraftWrite = z.infer<typeof DraftWriteSchema>;
|
|
67
|
+
|
|
68
|
+
export const DraftWrittenSchema = z.object({
|
|
69
|
+
draft: ComposerDraftSchema.optional().describe("The draft as it now stands. Absent when there is none: it was cleared, or sent, before this write arrived."),
|
|
70
|
+
conflict: z
|
|
71
|
+
.literal(true)
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Nothing was written: the draft had moved past `base`. Merge your words into this one and write again from its revision."),
|
|
74
|
+
});
|
|
75
|
+
export type DraftWritten = z.infer<typeof DraftWrittenSchema>;
|
|
76
|
+
|
|
77
|
+
export const DraftClearSchema = z.object({
|
|
78
|
+
id: ConversationIdSchema.describe("The conversation whose draft to clear."),
|
|
79
|
+
revision: z
|
|
80
|
+
.number()
|
|
81
|
+
.int()
|
|
82
|
+
.positive()
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("Clear it only while it is still this revision, so words written since in another window survive. Leave it out to clear whatever is there."),
|
|
85
|
+
});
|
|
86
|
+
export type DraftClear = z.infer<typeof DraftClearSchema>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Closed set of command names; the daemon builds argv from the name alone (hosts/device-commands.ts). Never a free-text
|
|
4
|
+
// command: the same socket carries `run_command`, which would grant a shell on the user's machine.
|
|
5
|
+
// `sync-unpair` runs the machine's own `sync uninstall --sandbox` (terminates both Mutagen sessions, drops the pairing,
|
|
6
|
+
// self-revokes enrollment); revoking from the sandbox side is a different route.
|
|
7
|
+
// `dev-restart`, `dev-rebuild` and `sync-install` are the three whose argv the daemon fills from what only it knows — the
|
|
8
|
+
// dev checkout on that machine, a freshly minted pairing token — which is also why none takes a path or a token from the
|
|
9
|
+
// caller. The two dev commands are also why a machine-side op is the wrong shape for them: this door carries a COMMAND
|
|
10
|
+
// STRING to a tool every released agent already has, so a sandbox can drive a machine whose agent predates the feature.
|
|
11
|
+
// `dev-rebuild-log` is the read side of `dev-rebuild`: the build is detached out there, so its log is the only place its
|
|
12
|
+
// progress exists, and polling this is what turns a fired-and-forgotten command into something with a progress bar.
|
|
13
|
+
// `sync-clean` removes the build output left in directories the sandbox has deleted, which is what stops those
|
|
14
|
+
// deletions from ever landing. It deletes only content the session already ignores, so it is the one command here whose
|
|
15
|
+
// worst outcome is a rebuild — which is why it is a button and not a turn.
|
|
16
|
+
// `mirror-ignore`/`mirror-unignore` are `mirror-off`'s per-port form, and the reason they exist: a port number that is
|
|
17
|
+
// permanently taken on one machine's localhost has no answer in an all-or-nothing switch, so the choice lives where the
|
|
18
|
+
// conflict does — one port, one device, durable — instead of muting every port the pairing serves.
|
|
19
|
+
export const DeviceCommandSchema = z.enum([
|
|
20
|
+
"mirror-off",
|
|
21
|
+
"mirror-on",
|
|
22
|
+
"mirror-ignore",
|
|
23
|
+
"mirror-unignore",
|
|
24
|
+
"sync-pause",
|
|
25
|
+
"sync-resume",
|
|
26
|
+
"sync-unpair",
|
|
27
|
+
"sync-clean",
|
|
28
|
+
"dev-restart",
|
|
29
|
+
"dev-rebuild",
|
|
30
|
+
"dev-rebuild-log",
|
|
31
|
+
"sync-install",
|
|
32
|
+
]);
|
|
33
|
+
export type DeviceCommand = z.infer<typeof DeviceCommandSchema>;
|
|
34
|
+
// The reversible sync switches: the subset a device's own row drives with a pair of buttons, as opposed to the two
|
|
35
|
+
// commands a card elsewhere issues once. Its own type so those button tables stay total without carrying entries for
|
|
36
|
+
// commands they can never send.
|
|
37
|
+
export const DeviceSyncSwitchSchema = DeviceCommandSchema.exclude(["dev-restart", "dev-rebuild", "dev-rebuild-log", "sync-install", "sync-clean"]);
|
|
38
|
+
export type DeviceSyncSwitch = z.infer<typeof DeviceSyncSwitchSchema>;
|
|
39
|
+
|
|
40
|
+
// Where a detached rebuild reports itself, and the reader of that report (dev-rebuild-log.ts).
|
|
41
|
+
export { DEV_REBUILD_EXIT_MARK, DEV_REBUILD_QUIET_MARK, type DevRebuildLog, devRebuildLogPath, readDevRebuildLog } from "./dev-rebuild-log.js";
|
|
42
|
+
|
|
43
|
+
// Machine's sandbox id (absent = every paired sandbox); must start alphanumeric, else it parses as a CLI flag.
|
|
44
|
+
export const DeviceSandboxIdSchema = z
|
|
45
|
+
.string()
|
|
46
|
+
.max(200)
|
|
47
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
|
|
48
|
+
// A folder on the DEVICE for `sync-install` to keep in step. The only caller-supplied string that reaches a command
|
|
49
|
+
// line, so its shape is the guard: `~`, an absolute POSIX path or a drive letter, and none of the characters that would
|
|
50
|
+
// end the argument it sits in. `~` is expanded by the daemon, which is why `$` is not allowed to arrive here. The line
|
|
51
|
+
// may be PowerShell, which reads the typographic quotes “ ” „ as double quotes and ‘ ’ ‚ ‛ as single quotes beside the
|
|
52
|
+
// ASCII ones, so those are refused too: the daemon quotes the folder, and this keeps a second guard where one quoter
|
|
53
|
+
// misses a character.
|
|
54
|
+
export const DeviceLocalDirSchema = z
|
|
55
|
+
.string()
|
|
56
|
+
.min(1)
|
|
57
|
+
.max(4096)
|
|
58
|
+
// The curly quotes as escapes: bun's transpiler rewrites a literal one in a regex to its escape and tsgo keeps it,
|
|
59
|
+
// so spelled literally the pattern reads differently from source and from dist, and contract.lock.json can match
|
|
60
|
+
// only one of them.
|
|
61
|
+
.regex(/^(?:~|\/|[A-Za-z]:[\\/])[^"'`$;|&\n\r\u201C\u201D\u201E\u2018\u2019\u201A\u201B]*$/);
|
|
62
|
+
// A TCP port number, the one value a caller supplies that reaches a command line as a number rather than a string.
|
|
63
|
+
export const PortNumberSchema = z.number().int().min(1).max(65535);
|
|
64
|
+
export const DeviceCommandInputSchema = z.object({
|
|
65
|
+
id: z.string().min(1),
|
|
66
|
+
command: DeviceCommandSchema,
|
|
67
|
+
sandboxId: DeviceSandboxIdSchema.optional(),
|
|
68
|
+
// `sync-install` only: which half to enroll (a member's "sync" still comes back mirror, decided daemon-side) and,
|
|
69
|
+
// for file sync, the folder on that device. The pairing token is minted by the daemon; no caller ever carries one.
|
|
70
|
+
mode: z.enum(["sync", "mirror"]).optional(),
|
|
71
|
+
localDir: DeviceLocalDirSchema.optional(),
|
|
72
|
+
// The two per-port mirror switches only. A number rather than a string, so nothing a caller sends can widen the
|
|
73
|
+
// command line it lands in.
|
|
74
|
+
port: PortNumberSchema.optional(),
|
|
75
|
+
});
|
|
76
|
+
export type DeviceCommandInput = z.infer<typeof DeviceCommandInputSchema>;
|
|
77
|
+
// `ok` is the command's own exit status, not this route's: a refusal or non-zero exit is a real answer, not a thrown
|
|
78
|
+
// error. Only an unreachable machine throws. `output` is the command's own printed text.
|
|
79
|
+
export const DeviceCommandResultSchema = z.object({
|
|
80
|
+
ok: z.boolean(),
|
|
81
|
+
message: z.string(),
|
|
82
|
+
output: z.string().optional(),
|
|
83
|
+
// WHICH KIND OF `ok: false` THIS IS, because a caller that repeats itself needs to tell them apart: the device
|
|
84
|
+
// turning the command away (a switch of its own is off, the path is out of its reach) will turn it away again,
|
|
85
|
+
// whereas a command it accepted and then killed at its deadline says nothing except that this attempt was too slow.
|
|
86
|
+
refused: z.boolean(),
|
|
87
|
+
});
|
|
88
|
+
export type DeviceCommandResult = z.infer<typeof DeviceCommandResultSchema>;
|
|
89
|
+
|
|
@@ -33,6 +33,15 @@ export type DeviceConflict = z.infer<typeof DeviceConflictSchema>;
|
|
|
33
33
|
export const clearableOnDevice = (conflict: DeviceConflict): boolean =>
|
|
34
34
|
conflict.nature === "derived-leftover" && conflict.local === "untracked" && conflict.sandbox === "deleted";
|
|
35
35
|
|
|
36
|
+
// WHY A FOLDER'S FILE SYNC IS HELD STILL, AND BY WHOM (2026-10-20). The machine agent reads each off a fact it keeps
|
|
37
|
+
// on the pairing and makes Mutagen agree every pass (its sync/holds.ts), so a pause never outlives its reason and a
|
|
38
|
+
// resume never undoes somebody else's. `owner` is a person's Pause, the only one a person lifts; every other is the
|
|
39
|
+
// agent's own and lifts by itself once its reason ends. `transfer` (2026-10-21) is a bring-back, a restore or a delivery
|
|
40
|
+
// of landed work writing into the folder. Strings on the wire, so a reason a newer agent adds still reads as held to an
|
|
41
|
+
// older page.
|
|
42
|
+
export const DeviceSyncHoldSchema = z.enum(["owner", "gone", "container-missing", "container-trashed", "swap", "unreachable", "low-disk", "transfer"]);
|
|
43
|
+
export type DeviceSyncHold = z.infer<typeof DeviceSyncHoldSchema>;
|
|
44
|
+
|
|
36
45
|
// The drive reading that paused a pairing for want of room: when (epoch ms), and how much was free of how much.
|
|
37
46
|
export const DeviceLowDiskSchema = z.object({
|
|
38
47
|
since: z.number(),
|
package/src/schemas/devices.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// What one of the user's own machines is running.
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { PortNumberSchema } from "./device-commands.js";
|
|
3
4
|
import { DeviceBridgeHoldSchema, DeviceConflictSchema, DeviceLowDiskSchema } from "./device-folder.js";
|
|
4
|
-
import { parseHostConnection, type DeviceFacts, DeviceFactsSchema, MachineIdSchema, userDistrosOf, WslEnvironmentSchema } from "./hosts.js";
|
|
5
|
+
import { parseHostConnection, type DeviceFacts, DeviceFactsSchema, MachineIdentitySchema, MachineIdSchema, userDistrosOf, WslEnvironmentSchema } from "./hosts.js";
|
|
5
6
|
import { RollbackTargetSchema, UpdateOutcomeSchema } from "./updates.js";
|
|
6
7
|
import { DEV_VERSION } from "../state/versions.js";
|
|
7
8
|
// Desktop-sync report shape shared by the agent, daemon and browser, produced only by the agent's own `deviceReport`.
|
|
@@ -230,92 +231,7 @@ export type DeviceAgentFlow = z.infer<typeof DeviceAgentFlowSchema>;
|
|
|
230
231
|
// Adds the device id to the flow input.
|
|
231
232
|
export const DeviceAgentFlowInputSchema = DeviceAgentFlowSchema.extend({ id: z.string().min(1) });
|
|
232
233
|
export type DeviceAgentFlowInput = z.infer<typeof DeviceAgentFlowInputSchema>;
|
|
233
|
-
|
|
234
|
-
// command: the same socket carries `run_command`, which would grant a shell on the user's machine.
|
|
235
|
-
// `sync-unpair` runs the machine's own `sync uninstall --sandbox` (terminates both Mutagen sessions, drops the pairing,
|
|
236
|
-
// self-revokes enrollment); revoking from the sandbox side is a different route.
|
|
237
|
-
// `dev-restart`, `dev-rebuild` and `sync-install` are the three whose argv the daemon fills from what only it knows — the
|
|
238
|
-
// dev checkout on that machine, a freshly minted pairing token — which is also why none takes a path or a token from the
|
|
239
|
-
// caller. The two dev commands are also why a machine-side op is the wrong shape for them: this door carries a COMMAND
|
|
240
|
-
// STRING to a tool every released agent already has, so a sandbox can drive a machine whose agent predates the feature.
|
|
241
|
-
// `dev-rebuild-log` is the read side of `dev-rebuild`: the build is detached out there, so its log is the only place its
|
|
242
|
-
// progress exists, and polling this is what turns a fired-and-forgotten command into something with a progress bar.
|
|
243
|
-
// `sync-clean` removes the build output left in directories the sandbox has deleted, which is what stops those
|
|
244
|
-
// deletions from ever landing. It deletes only content the session already ignores, so it is the one command here whose
|
|
245
|
-
// worst outcome is a rebuild — which is why it is a button and not a turn.
|
|
246
|
-
// `mirror-ignore`/`mirror-unignore` are `mirror-off`'s per-port form, and the reason they exist: a port number that is
|
|
247
|
-
// permanently taken on one machine's localhost has no answer in an all-or-nothing switch, so the choice lives where the
|
|
248
|
-
// conflict does — one port, one device, durable — instead of muting every port the pairing serves.
|
|
249
|
-
export const DeviceCommandSchema = z.enum([
|
|
250
|
-
"mirror-off",
|
|
251
|
-
"mirror-on",
|
|
252
|
-
"mirror-ignore",
|
|
253
|
-
"mirror-unignore",
|
|
254
|
-
"sync-pause",
|
|
255
|
-
"sync-resume",
|
|
256
|
-
"sync-unpair",
|
|
257
|
-
"sync-clean",
|
|
258
|
-
"dev-restart",
|
|
259
|
-
"dev-rebuild",
|
|
260
|
-
"dev-rebuild-log",
|
|
261
|
-
"sync-install",
|
|
262
|
-
]);
|
|
263
|
-
export type DeviceCommand = z.infer<typeof DeviceCommandSchema>;
|
|
264
|
-
// The reversible sync switches: the subset a device's own row drives with a pair of buttons, as opposed to the two
|
|
265
|
-
// commands a card elsewhere issues once. Its own type so those button tables stay total without carrying entries for
|
|
266
|
-
// commands they can never send.
|
|
267
|
-
export const DeviceSyncSwitchSchema = DeviceCommandSchema.exclude(["dev-restart", "dev-rebuild", "dev-rebuild-log", "sync-install", "sync-clean"]);
|
|
268
|
-
export type DeviceSyncSwitch = z.infer<typeof DeviceSyncSwitchSchema>;
|
|
269
|
-
|
|
270
|
-
// Where a detached rebuild reports itself, and the reader of that report (dev-rebuild-log.ts).
|
|
271
|
-
export { DEV_REBUILD_EXIT_MARK, DEV_REBUILD_QUIET_MARK, type DevRebuildLog, devRebuildLogPath, readDevRebuildLog } from "./dev-rebuild-log.js";
|
|
272
|
-
|
|
273
|
-
// Machine's sandbox id (absent = every paired sandbox); must start alphanumeric, else it parses as a CLI flag.
|
|
274
|
-
export const DeviceSandboxIdSchema = z
|
|
275
|
-
.string()
|
|
276
|
-
.max(200)
|
|
277
|
-
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
|
|
278
|
-
// A folder on the DEVICE for `sync-install` to keep in step. The only caller-supplied string that reaches a command
|
|
279
|
-
// line, so its shape is the guard: `~`, an absolute POSIX path or a drive letter, and none of the characters that would
|
|
280
|
-
// end the argument it sits in. `~` is expanded by the daemon, which is why `$` is not allowed to arrive here. The line
|
|
281
|
-
// may be PowerShell, which reads the typographic quotes “ ” „ as double quotes and ‘ ’ ‚ ‛ as single quotes beside the
|
|
282
|
-
// ASCII ones, so those are refused too: the daemon quotes the folder, and this keeps a second guard where one quoter
|
|
283
|
-
// misses a character.
|
|
284
|
-
export const DeviceLocalDirSchema = z
|
|
285
|
-
.string()
|
|
286
|
-
.min(1)
|
|
287
|
-
.max(4096)
|
|
288
|
-
// The curly quotes as escapes: bun's transpiler rewrites a literal one in a regex to its escape and tsgo keeps it,
|
|
289
|
-
// so spelled literally the pattern reads differently from source and from dist, and contract.lock.json can match
|
|
290
|
-
// only one of them.
|
|
291
|
-
.regex(/^(?:~|\/|[A-Za-z]:[\\/])[^"'`$;|&\n\r\u201C\u201D\u201E\u2018\u2019\u201A\u201B]*$/);
|
|
292
|
-
// A TCP port number, the one value a caller supplies that reaches a command line as a number rather than a string.
|
|
293
|
-
export const PortNumberSchema = z.number().int().min(1).max(65535);
|
|
294
|
-
export const DeviceCommandInputSchema = z.object({
|
|
295
|
-
id: z.string().min(1),
|
|
296
|
-
command: DeviceCommandSchema,
|
|
297
|
-
sandboxId: DeviceSandboxIdSchema.optional(),
|
|
298
|
-
// `sync-install` only: which half to enroll (a member's "sync" still comes back mirror, decided daemon-side) and,
|
|
299
|
-
// for file sync, the folder on that device. The pairing token is minted by the daemon; no caller ever carries one.
|
|
300
|
-
mode: z.enum(["sync", "mirror"]).optional(),
|
|
301
|
-
localDir: DeviceLocalDirSchema.optional(),
|
|
302
|
-
// The two per-port mirror switches only. A number rather than a string, so nothing a caller sends can widen the
|
|
303
|
-
// command line it lands in.
|
|
304
|
-
port: PortNumberSchema.optional(),
|
|
305
|
-
});
|
|
306
|
-
export type DeviceCommandInput = z.infer<typeof DeviceCommandInputSchema>;
|
|
307
|
-
// `ok` is the command's own exit status, not this route's: a refusal or non-zero exit is a real answer, not a thrown
|
|
308
|
-
// error. Only an unreachable machine throws. `output` is the command's own printed text.
|
|
309
|
-
export const DeviceCommandResultSchema = z.object({
|
|
310
|
-
ok: z.boolean(),
|
|
311
|
-
message: z.string(),
|
|
312
|
-
output: z.string().optional(),
|
|
313
|
-
// WHICH KIND OF `ok: false` THIS IS, because a caller that repeats itself needs to tell them apart: the device
|
|
314
|
-
// turning the command away (a switch of its own is off, the path is out of its reach) will turn it away again,
|
|
315
|
-
// whereas a command it accepted and then killed at its deadline says nothing except that this attempt was too slow.
|
|
316
|
-
refused: z.boolean(),
|
|
317
|
-
});
|
|
318
|
-
export type DeviceCommandResult = z.infer<typeof DeviceCommandResultSchema>;
|
|
234
|
+
export * from "./device-commands.js";
|
|
319
235
|
|
|
320
236
|
// What a synced folder reports beyond Mutagen's word, its conflicts and what the device holds back (device-folder.ts).
|
|
321
237
|
export * from "./device-folder.js";
|
|
@@ -323,8 +239,7 @@ export * from "./device-folder.js";
|
|
|
323
239
|
// One paired sandbox as the local agent holds it; `localDir` is which folder on that device holds this sandbox's /work.
|
|
324
240
|
export const DevicePairingSchema = z.object({
|
|
325
241
|
sandboxId: z.string(),
|
|
326
|
-
// The account's name for the sandbox, stamped by the editor that reads the report (withSandboxNames)
|
|
327
|
-
// container's `name` is. A machine never sends one: all it knows is the folder, named once at setup.
|
|
242
|
+
// The account's name for the sandbox, stamped by the editor that reads the report (withSandboxNames); never a machine's.
|
|
328
243
|
name: z.string().optional(),
|
|
329
244
|
mode: z.enum(["sync", "mirror"]),
|
|
330
245
|
// Set only for mode "sync", and only for the sandbox being reported to.
|
|
@@ -332,12 +247,13 @@ export const DevicePairingSchema = z.object({
|
|
|
332
247
|
// Which sandbox folder `localDir` holds when it is not /work itself: a project pairing's `/work/<name>`, the
|
|
333
248
|
// owner's own folder, which carries no state backup and no git bridge. Absent means /work.
|
|
334
249
|
remoteDir: z.string().optional(),
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
projectsHost
|
|
338
|
-
//
|
|
339
|
-
// folder attached to the machine's own sandbox says "auto"; absent means Bring back by hand, as before.
|
|
250
|
+
// Whether landed work in this project folder is delivered to `localDir` by itself (`deliverProject`). A project
|
|
251
|
+
// sandbox's folder says "auto"; absent means Bring back by hand, as before. (2026-10-21) The folderless
|
|
252
|
+
// `projectsHost` pairing of the desktop app's one sandbox per computer went with that sandbox; an older agent's report
|
|
253
|
+
// that still carries it parses, the key dropped.
|
|
340
254
|
deliver: z.enum(["auto", "off"]).optional(),
|
|
255
|
+
// (2026-10-20) The slug this machine's ic knows the sandbox by: how the desktop app starts or forgets a folder's sandbox.
|
|
256
|
+
slug: z.string().optional(),
|
|
341
257
|
// Machine-owned switch (agent's own `sync mirror off`); absent reads as "on", not merely unknown.
|
|
342
258
|
mirroring: z.enum(["on", "off"]).optional(),
|
|
343
259
|
// Mutagen's own status word, kept verbatim rather than reduced to a traffic light.
|
|
@@ -346,7 +262,9 @@ export const DevicePairingSchema = z.object({
|
|
|
346
262
|
conflicts: z.number().int().nonnegative().optional(),
|
|
347
263
|
// Conflicted paths, capped at `CONFLICT_PATHS_MAX`; `conflicts` above is the true, uncapped count.
|
|
348
264
|
conflictedPaths: z.array(DeviceConflictSchema).optional(),
|
|
265
|
+
// Mutagen's word, and why the agent holds it still, owner first (DeviceSyncHoldSchema); agreeing by its next pass.
|
|
349
266
|
paused: z.boolean().optional(),
|
|
267
|
+
heldBy: z.array(z.string()).optional(),
|
|
350
268
|
// Second session's status (the state-dir backup mirror), reported separately: the two fail independently.
|
|
351
269
|
backupStatus: z.string().optional(),
|
|
352
270
|
// (2026-10-18) While the drive holding `localDir` is nearly full, the reading that paused its sync and git bridge; and the
|
|
@@ -488,12 +406,11 @@ export const SshHostKeySchema = z
|
|
|
488
406
|
|
|
489
407
|
// POST /system/authorized-key: a machine's agent enrolling its SSH key for desktop sync, redeeming a pairing token.
|
|
490
408
|
// Every field past `key` is optional: an older agent sends none of them and is still enrolled.
|
|
491
|
-
|
|
409
|
+
// The key, and the computer and environment enrolling it (the same identity a device redeem sends), so the
|
|
410
|
+
// enrollment joins that machine's card.
|
|
411
|
+
export const SyncEnrollmentRequestSchema = MachineIdentitySchema.extend({
|
|
492
412
|
// One authorized_keys line; its comment is the name the enrollment goes by (`DeviceSync.machine`).
|
|
493
413
|
key: z.string(),
|
|
494
|
-
// The computer and the environment on it that are enrolling, so the enrollment joins that machine's card.
|
|
495
|
-
machineId: MachineIdSchema.optional(),
|
|
496
|
-
environment: z.string().min(1).optional(),
|
|
497
414
|
});
|
|
498
415
|
export type SyncEnrollmentRequest = z.infer<typeof SyncEnrollmentRequestSchema>;
|
|
499
416
|
|
|
@@ -546,6 +463,10 @@ export const DeviceSchema = z.object({
|
|
|
546
463
|
// (a switch that is off, an `ic` too old to list). Absent when it listed, or when nothing answered at all. What a
|
|
547
464
|
// card falling back to a command says instead of "connect the computer": that computer IS connected (2026-10-07).
|
|
548
465
|
sandboxesUnread: z.string().optional(),
|
|
466
|
+
// Whether its card's "Manage sandboxes on this device" switch is on (HostSummary.manages). Not the same fact as
|
|
467
|
+
// `sandboxes`: "Run commands" alone lists the containers, and only this switch lets one be made, started or removed,
|
|
468
|
+
// so a screen offering such a press asks here first. Absent on a sync-only row and from a daemon older than it.
|
|
469
|
+
manages: z.boolean().optional(),
|
|
549
470
|
gap: DeviceGapSchema.optional(),
|
|
550
471
|
});
|
|
551
472
|
export type Device = z.infer<typeof DeviceSchema>;
|
|
@@ -659,14 +580,11 @@ const doorsRunningSandbox = (devices: readonly Device[], slug: string | undefine
|
|
|
659
580
|
? []
|
|
660
581
|
: devices.filter((device) => device.hostId !== undefined && device.online === true && (device.sandboxes ?? []).some((box) => box.slug === slug));
|
|
661
582
|
|
|
662
|
-
// The connected, online device whose docker reports a given sandbox slug: the machine that sandbox RUNS ON,
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
// a terminal command for a machine one click could have done it on.
|
|
668
|
-
// Answers with a door, and a container verb may take any door of the machine; a command written for a PATH may not
|
|
669
|
-
// (`hostHoldingPath`).
|
|
583
|
+
// The connected, online device whose docker reports a given sandbox slug: the machine that sandbox RUNS ON, not any
|
|
584
|
+
// machine merely paired with it, and `undefined` for a sync-only agent, which reports no containers. Read off the row's
|
|
585
|
+
// own container list, not the report's: a card granting sandbox management without "Run commands" refuses to describe
|
|
586
|
+
// itself yet runs every swap this answer leads to, so judging it on the report printed a command one click could do.
|
|
587
|
+
// Answers with a door, any of which a container verb may take; a command written for a PATH may not (`hostHoldingPath`).
|
|
670
588
|
export const hostRunningSandbox = (devices: readonly Device[], slug: string | undefined): string | undefined =>
|
|
671
589
|
doorsRunningSandbox(devices, slug)[0]?.hostId;
|
|
672
590
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// environment: the overlay Dockerfile extending the sandbox image
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
// The approved
|
|
4
|
-
// is derived, never stored:
|
|
5
|
-
//
|
|
6
|
-
// pending rebuild: approved present but hashes differ.
|
|
3
|
+
// The approved overlay is daemon-composed (pinned FROM + capability fragments + the owner-approved custom section) from its
|
|
4
|
+
// inputs on every read. Status is derived, never stored:
|
|
5
|
+
// pending rebuild: rebuildPending(approved.hash, appliedHash) (state/sandbox-build.ts); applied otherwise.
|
|
7
6
|
// proposed: proposal present with a hash different from custom's.
|
|
8
7
|
|
|
9
8
|
const environmentFileSchema = z.object({ content: z.string(), hash: z.string() });
|
|
@@ -45,10 +44,10 @@ export type RuntimeInstall = z.infer<typeof RuntimeInstallSchema>;
|
|
|
45
44
|
// (`environment propose`) clears its tool's entries.
|
|
46
45
|
export const SettledDraftSchema = z.object({ tool: z.string(), hash: z.string(), at: z.number() });
|
|
47
46
|
export type SettledDraft = z.infer<typeof SettledDraftSchema>;
|
|
48
|
-
// One overlay the daemon composed, by the hash a rebuild is stamped with, and the
|
|
49
|
-
//
|
|
50
|
-
// running container was actually built from, which is rarely the newest one composed:
|
|
51
|
-
// and daemon update composes again, and a rebuild builds whatever was newest
|
|
47
|
+
// One overlay the daemon composed, by the hash a rebuild is stamped with, and the sections it carried, each by the hash
|
|
48
|
+
// of its trimmed steps: capability fragments unnamed, custom-section blocks by name. Kept so "is this in the image" is
|
|
49
|
+
// answered against the overlay the running container was actually built from, which is rarely the newest one composed:
|
|
50
|
+
// every approval, capability change and daemon update composes again, and a rebuild builds whatever was newest then.
|
|
52
51
|
export const EnvironmentCompositionSchema = z.object({
|
|
53
52
|
hash: z.string(),
|
|
54
53
|
at: z.number(),
|
|
@@ -196,7 +195,7 @@ export type EnvironmentRemove = z.infer<typeof EnvironmentRemoveSchema>;
|
|
|
196
195
|
export const EnvironmentContentsSchema = z.object({ items: z.array(EnvironmentItemSchema) });
|
|
197
196
|
export type EnvironmentContents = z.infer<typeof EnvironmentContentsSchema>;
|
|
198
197
|
// A sandbox is four stores: /work, /history, the container, and the AI-provider credential root. A bundle carries only
|
|
199
|
-
// the first two (WORKSPACE_STATE_FILES
|
|
198
|
+
// the first two (WORKSPACE_STATE_FILES here, the daemon's HISTORY_STATE_FILES); it cannot rebuild the container or credentials, so
|
|
200
199
|
// importing one is a report, not a claim of equivalence. Only the outbound half is here; inbound is one of arrival.ts's
|
|
201
200
|
// four sources.
|
|
202
201
|
|