@intentic/sandbox-contract 1.316.0 → 1.317.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 (132) hide show
  1. package/dist/contracts/agent.contract.d.ts +561 -0
  2. package/dist/contracts/agent.contract.d.ts.map +1 -1
  3. package/dist/contracts/agents.contract.d.ts +402 -0
  4. package/dist/contracts/agents.contract.d.ts.map +1 -1
  5. package/dist/contracts/capabilities.contract.d.ts +16 -0
  6. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  7. package/dist/contracts/capabilities.contract.js +10 -1
  8. package/dist/contracts/capabilities.contract.js.map +1 -1
  9. package/dist/contracts/device.contract.d.ts +1 -0
  10. package/dist/contracts/device.contract.d.ts.map +1 -1
  11. package/dist/contracts/needs.contract.d.ts +532 -0
  12. package/dist/contracts/needs.contract.d.ts.map +1 -0
  13. package/dist/contracts/needs.contract.js +80 -0
  14. package/dist/contracts/needs.contract.js.map +1 -0
  15. package/dist/contracts/runner.contract.d.ts +294 -122
  16. package/dist/contracts/runner.contract.d.ts.map +1 -1
  17. package/dist/contracts/secrets.contract.d.ts +16 -0
  18. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  19. package/dist/contracts/secrets.contract.js +12 -2
  20. package/dist/contracts/secrets.contract.js.map +1 -1
  21. package/dist/contracts/sessions.contract.d.ts +79 -0
  22. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  23. package/dist/contracts/settings.contract.d.ts +2 -0
  24. package/dist/contracts/settings.contract.d.ts.map +1 -1
  25. package/dist/contracts/system.contract.d.ts +160 -0
  26. package/dist/contracts/system.contract.d.ts.map +1 -1
  27. package/dist/contracts/system.contract.js +11 -0
  28. package/dist/contracts/system.contract.js.map +1 -1
  29. package/dist/events/agent-events.d.ts +309 -0
  30. package/dist/events/agent-events.d.ts.map +1 -1
  31. package/dist/events/agent-events.js +2 -0
  32. package/dist/events/agent-events.js.map +1 -1
  33. package/dist/events/agent-words.d.ts.map +1 -1
  34. package/dist/events/agent-words.js +2 -1
  35. package/dist/events/agent-words.js.map +1 -1
  36. package/dist/events/need-wake.d.ts +17 -0
  37. package/dist/events/need-wake.d.ts.map +1 -0
  38. package/dist/events/need-wake.js +34 -0
  39. package/dist/events/need-wake.js.map +1 -0
  40. package/dist/events/system-events.d.ts +38 -0
  41. package/dist/events/system-events.d.ts.map +1 -1
  42. package/dist/events/transcript.d.ts +484 -0
  43. package/dist/events/transcript.d.ts.map +1 -1
  44. package/dist/events/transcript.js +9 -0
  45. package/dist/events/transcript.js.map +1 -1
  46. package/dist/index.d.ts +1826 -55
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +6 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/protocol/raw-routes.d.ts +0 -5
  51. package/dist/protocol/raw-routes.d.ts.map +1 -1
  52. package/dist/protocol/raw-routes.js +0 -1
  53. package/dist/protocol/raw-routes.js.map +1 -1
  54. package/dist/schemas/agents.d.ts +58 -0
  55. package/dist/schemas/agents.d.ts.map +1 -1
  56. package/dist/schemas/agents.js +9 -0
  57. package/dist/schemas/agents.js.map +1 -1
  58. package/dist/schemas/automations.d.ts +19 -0
  59. package/dist/schemas/automations.d.ts.map +1 -1
  60. package/dist/schemas/capabilities.d.ts +19 -0
  61. package/dist/schemas/capabilities.d.ts.map +1 -1
  62. package/dist/schemas/capabilities.js +16 -0
  63. package/dist/schemas/capabilities.js.map +1 -1
  64. package/dist/schemas/devices.d.ts +68 -0
  65. package/dist/schemas/devices.d.ts.map +1 -1
  66. package/dist/schemas/devices.js +7 -0
  67. package/dist/schemas/devices.js.map +1 -1
  68. package/dist/schemas/hosts.d.ts +2 -0
  69. package/dist/schemas/hosts.d.ts.map +1 -1
  70. package/dist/schemas/hosts.js +2 -1
  71. package/dist/schemas/hosts.js.map +1 -1
  72. package/dist/schemas/needs.d.ts +661 -0
  73. package/dist/schemas/needs.d.ts.map +1 -0
  74. package/dist/schemas/needs.js +196 -0
  75. package/dist/schemas/needs.js.map +1 -0
  76. package/dist/schemas/secrets.d.ts +18 -0
  77. package/dist/schemas/secrets.d.ts.map +1 -1
  78. package/dist/schemas/secrets.js +20 -0
  79. package/dist/schemas/secrets.js.map +1 -1
  80. package/dist/schemas/settings.d.ts +2 -0
  81. package/dist/schemas/settings.d.ts.map +1 -1
  82. package/dist/schemas/settings.js +4 -0
  83. package/dist/schemas/settings.js.map +1 -1
  84. package/dist/schemas/state-plan.d.ts +1 -0
  85. package/dist/schemas/state-plan.d.ts.map +1 -1
  86. package/dist/schemas/state-plan.js +3 -1
  87. package/dist/schemas/state-plan.js.map +1 -1
  88. package/dist/schemas/system.d.ts +20 -0
  89. package/dist/schemas/system.d.ts.map +1 -1
  90. package/dist/schemas/system.js +7 -0
  91. package/dist/schemas/system.js.map +1 -1
  92. package/dist/schemas/updates.d.ts +39 -0
  93. package/dist/schemas/updates.d.ts.map +1 -0
  94. package/dist/schemas/updates.js +31 -0
  95. package/dist/schemas/updates.js.map +1 -0
  96. package/dist/state/definition.d.ts +16 -0
  97. package/dist/state/definition.d.ts.map +1 -1
  98. package/dist/state/history-state.d.ts.map +1 -1
  99. package/dist/state/history-state.js +3 -0
  100. package/dist/state/history-state.js.map +1 -1
  101. package/dist/state/workspace-state.d.ts +4 -0
  102. package/dist/state/workspace-state.d.ts.map +1 -1
  103. package/dist/state/workspace-state.js +1 -0
  104. package/dist/state/workspace-state.js.map +1 -1
  105. package/dist/text/transcript-fold.d.ts.map +1 -1
  106. package/dist/text/transcript-fold.js +3 -0
  107. package/dist/text/transcript-fold.js.map +1 -1
  108. package/package.json +5 -5
  109. package/src/contracts/capabilities.contract.ts +14 -1
  110. package/src/contracts/needs.contract.ts +103 -0
  111. package/src/contracts/secrets.contract.ts +17 -3
  112. package/src/contracts/system.contract.ts +14 -0
  113. package/src/events/agent-events.ts +4 -0
  114. package/src/events/agent-words.ts +2 -1
  115. package/src/events/need-wake.test.ts +55 -0
  116. package/src/events/need-wake.ts +71 -0
  117. package/src/events/transcript.ts +16 -0
  118. package/src/index.ts +6 -0
  119. package/src/protocol/raw-routes.ts +1 -2
  120. package/src/schemas/agents.ts +14 -1
  121. package/src/schemas/capabilities.ts +28 -0
  122. package/src/schemas/devices.ts +17 -0
  123. package/src/schemas/hosts.ts +4 -1
  124. package/src/schemas/needs.ts +292 -0
  125. package/src/schemas/secrets.ts +23 -0
  126. package/src/schemas/settings.ts +8 -0
  127. package/src/schemas/state-plan.ts +8 -1
  128. package/src/schemas/system.ts +15 -0
  129. package/src/schemas/updates.ts +56 -0
  130. package/src/state/history-state.ts +5 -0
  131. package/src/state/workspace-state.ts +4 -0
  132. package/src/text/transcript-fold.ts +4 -0
@@ -0,0 +1,103 @@
1
+ import { procedure } from "../protocol/route-meta.js";
2
+ import { OkSchema } from "../schemas/shared.js";
3
+ import {
4
+ GrantRevokeSchema,
5
+ NeedAnswerInputSchema,
6
+ NeedIdParamSchema,
7
+ NeedRaisedSchema,
8
+ NeedRaiseSchema,
9
+ NeedSchema,
10
+ NeedSecretInputSchema,
11
+ NeedsListSchema,
12
+ NeedsQuerySchema,
13
+ StandingGrantsSchema,
14
+ } from "../schemas/needs.js";
15
+
16
+ // Needs (docs/architecture/needs.md): the agent's CLIs raise and withdraw them on the agent token; a person reads and
17
+ // answers them from the chat, the board and the Needs you inbox. Answering is the operating tier's, like the capability
18
+ // and secret routes an answer ends up writing through.
19
+
20
+ const agentDoor = procedure.meta({ agent: true, control: "never" });
21
+ const answerRoute = procedure.meta({ floor: "maintainer", control: "never" });
22
+
23
+ export const needsContract = {
24
+ ask: agentDoor
25
+ .route({
26
+ method: "POST",
27
+ path: "/needs/ask",
28
+ summary: "Ask a person for something the task needs",
29
+ description:
30
+ "Raises a need in the conversation the calling shell belongs to and holds the call up to `wait` seconds for an answer. Answers `met` when it is usable now (or already was), `open` when it is still waiting, and `refused` when nothing was raised or a person declined. An open need's answer reaches the conversation by itself.",
31
+ })
32
+ // Held open while the person decides, by design.
33
+ .meta({ stream: true })
34
+ .input(NeedRaiseSchema)
35
+ .output(NeedRaisedSchema),
36
+ mine: agentDoor
37
+ .route({
38
+ method: "GET",
39
+ path: "/needs/mine",
40
+ summary: "This conversation's needs",
41
+ description: "Every need the calling shell's conversation raised, newest first, open or answered.",
42
+ })
43
+ .output(NeedsListSchema),
44
+ withdraw: agentDoor
45
+ .route({
46
+ method: "POST",
47
+ path: "/needs/{id}/withdraw",
48
+ summary: "Withdraw a need",
49
+ description: "Closes one of this conversation's open needs because the task no longer needs it. Its card says so.",
50
+ })
51
+ .input(NeedIdParamSchema)
52
+ .output(NeedSchema),
53
+ list: procedure
54
+ .route({
55
+ method: "GET",
56
+ path: "/needs",
57
+ summary: "What agents are waiting on people for",
58
+ description: "Needs across the sandbox, or one conversation's, newest first. Never a secret's value.",
59
+ })
60
+ .input(NeedsQuerySchema)
61
+ .output(NeedsListSchema),
62
+ answer: answerRoute
63
+ .route({
64
+ method: "POST",
65
+ path: "/needs/{id}/answer",
66
+ summary: "Answer a need",
67
+ description:
68
+ "Declines it, or says yes the way its card offered: accept a connection being set up, apply a change, grant for this conversation or the persona, release a gated credential, approve an environment proposal. A release is refused from anyone the gate does not name.",
69
+ })
70
+ .input(NeedAnswerInputSchema)
71
+ .output(NeedSchema),
72
+ provideSecret: answerRoute
73
+ .route({
74
+ method: "POST",
75
+ path: "/needs/{id}/secret",
76
+ summary: "Give a secret a need asked for",
77
+ description:
78
+ "Stores the value under the name the need asked for and meets it. The value goes to the sandbox's secret store and nowhere else: not the answer, not the transcript, not a log.",
79
+ })
80
+ // Puts a credential in motion: withheld from the panel token.
81
+ .meta({ panel: false })
82
+ .input(NeedSecretInputSchema)
83
+ .output(NeedSchema),
84
+ grants: answerRoute
85
+ .route({
86
+ method: "GET",
87
+ path: "/needs/grants",
88
+ summary: "The yeses still standing",
89
+ description:
90
+ "What people allowed conversations beyond their persona or area, and the gated credentials released to them, by conversation. Names only, never a value.",
91
+ })
92
+ .output(StandingGrantsSchema),
93
+ revokeGrant: answerRoute
94
+ .route({
95
+ method: "POST",
96
+ path: "/needs/grants/revoke",
97
+ summary: "Take a yes back",
98
+ description:
99
+ "Takes back one grant or one release. The conversation's next turn runs without it; a turn already running keeps what it mounted.",
100
+ })
101
+ .input(GrantRevokeSchema)
102
+ .output(OkSchema),
103
+ };
@@ -7,6 +7,8 @@ import {
7
7
  CredentialRequestSchema,
8
8
  SecretInventorySchema,
9
9
  SecretKeyParamSchema,
10
+ SecretGeneratedSchema,
11
+ SecretGenerateSchema,
10
12
  SecretKeysSchema,
11
13
  SecretRevealSchema,
12
14
  SecretSetSchema,
@@ -16,8 +18,8 @@ import { OkSchema } from "../schemas/shared.js";
16
18
  // Credentials are the operating tier's, reads included: maintainer is the highest revokable grant, and no token's.
17
19
  const secretRoute = procedure.meta({ floor: "maintainer", control: "never" });
18
20
 
19
- // User-supplied secrets, in the gitignored desired-state/.env; `apply` reloads them, no restart. `inventory` aggregates
20
- // every store (never values) and always answers; the rest refuse until desired-state is scaffolded. `reveal` alone
21
+ // User-supplied secrets, in the gitignored desired-state/.env once DevOps is active and the sandbox's own store before
22
+ // it; `apply` reloads them, no restart. `inventory` aggregates every store (never values) and always answers. `reveal` alone
21
23
  // returns a value, owner-only, POST so the key avoids the URL.
22
24
  export const secretsContract = {
23
25
  set: secretRoute
@@ -26,10 +28,22 @@ export const secretsContract = {
26
28
  path: "/secrets",
27
29
  summary: "Store a secret",
28
30
  description:
29
- "Writes one name and value into the sandbox's own store, where running processes pick it up without a restart. Refused until the sandbox has somewhere to keep them.",
31
+ "Writes one name and value where the agent's references resolve it, without a restart: desired-state/.env once DevOps is active, the sandbox's own secret store before that.",
30
32
  })
31
33
  .input(SecretSetSchema)
32
34
  .output(OkSchema),
35
+ generate: secretRoute
36
+ .route({
37
+ method: "POST",
38
+ path: "/secrets/generate",
39
+ summary: "Make and store a random secret",
40
+ description:
41
+ "Makes a random value and stores it under a new name, where `set` would have put it, for a secret nobody has to find or paste (a session key, a signing secret, a password the task sets up itself). Answers the name and its length, never the value. Refused for a name something here already holds.",
42
+ })
43
+ // The `secrets generate` CLI, on the agent token: it creates a value, and hands none back.
44
+ .meta({ agent: true })
45
+ .input(SecretGenerateSchema)
46
+ .output(SecretGeneratedSchema),
33
47
  list: secretRoute
34
48
  .route({
35
49
  method: "GET",
@@ -15,6 +15,7 @@ import { PresenceReportSchema } from "../schemas/logs.js";
15
15
  import { SandboxMetricsSchema, StorageCleanInputSchema, StorageCleanResultSchema, StorageReportSchema } from "../schemas/metrics.js";
16
16
  import { OkSchema } from "../schemas/shared.js";
17
17
  import { DaemonSessionSchema, InfoSchema, ManifestProblemsSchema, ManifestRepairSchema } from "../schemas/system.js";
18
+ import { SkipUpdateInputSchema } from "../schemas/updates.js";
18
19
  import {
19
20
  BrowserNameParamSchema,
20
21
  BrowsersListSchema,
@@ -44,6 +45,19 @@ export const systemContract = {
44
45
  })
45
46
  .meta({ guest: true })
46
47
  .output(InfoSchema),
48
+ // The owner's "not this one" for the update card; /info's `skippedVersion` reads it back.
49
+ skipUpdate: systemRoute
50
+ .route({
51
+ method: "POST",
52
+ path: "/system/update/skip",
53
+ summary: "Stop offering one release",
54
+ description:
55
+ "Stops offering the named release as an update, typically one this sandbox already tried and went back from. A newer release is offered as usual. Null offers the newest release again.",
56
+ })
57
+ // Which version the sandbox runs is the operator's decision, like the update itself.
58
+ .meta({ floor: "maintainer" })
59
+ .input(SkipUpdateInputSchema)
60
+ .output(OkSchema),
47
61
  // Own route, not a field on /info: it goes stale on a manifest changing on disk, not on identity changing.
48
62
  manifestProblems: systemRoute
49
63
  .route({
@@ -5,6 +5,7 @@ import { LandConflictSchema } from "../schemas/agents.js";
5
5
  import { ContextTrimSchema } from "../schemas/context-trim.js";
6
6
  import { RateLimitInfoSchema } from "../schemas/providers/claude-gate.js";
7
7
  import { FastModeStateSchema } from "../schemas/providers/fast-mode.js";
8
+ import { NeedSchema } from "../schemas/needs.js";
8
9
  import { PromptCacheOpeningSchema, PromptFingerprintSchema } from "../schemas/keep-warm.js";
9
10
  import { AgentReplySchema, UsageWindowSchema } from "../schemas/providers/plan-limits.js";
10
11
  import { SubagentKindSchema, SubagentStatusSchema, SubagentVerificationSchema } from "../schemas/terminal.js";
@@ -206,6 +207,9 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
206
207
  // Who released it: `released` names the approver's verified address, `refused` means someone said no. Nothing is
207
208
  // pushed for a card nobody answered; `resolved` already says that.
208
209
  CredentialReceiptSchema.extend({ kind: z.literal("credential_receipt"), requestId: z.string() }),
210
+ // Something the agent asked a person for (docs/architecture/needs.md). Parks nothing: the turn carries on, the need
211
+ // outlives it in the needs store, and its card draws the store's live state by `need.id`.
212
+ z.object({ kind: z.literal("need"), need: NeedSchema }),
209
213
  // The named card is released and the turn resumes; emitted the moment its waiter settles, since nothing else on
210
214
  // this stream marks a park's end. `reply` is what a rebuilt transcript freezes the card with.
211
215
  z.object({ kind: z.literal("resolved"), requestId: z.string(), reply: AgentReplySchema.optional() }),
@@ -1,5 +1,6 @@
1
1
  import type { SubagentVerification } from "../schemas/terminal.js";
2
2
  import type { TranscriptAgentWords, TranscriptRow } from "./transcript.js";
3
+ import { needWakeRow } from "./need-wake.js";
3
4
  import { watchWakeRow } from "./watch-wake.js";
4
5
 
5
6
  // Composer and parser of another agent's prompt (a peer's message, a child's report) are one piece of knowledge.
@@ -124,4 +125,4 @@ export const agentWordsRow = (prompt: string): TranscriptRow | undefined => {
124
125
  };
125
126
 
126
127
  /** The row for a prompt nobody at the composer typed; every transcript reader asks this one function. */
127
- export const unspokenPromptRow = (prompt: string): TranscriptRow | undefined => watchWakeRow(prompt) ?? agentWordsRow(prompt);
128
+ export const unspokenPromptRow = (prompt: string): TranscriptRow | undefined => watchWakeRow(prompt) ?? needWakeRow(prompt) ?? agentWordsRow(prompt);
@@ -0,0 +1,55 @@
1
+ import { unspokenPromptRow } from "./agent-words.js";
2
+ import { needRowText, needWakeOf, needWakePrompt, needWakeRow, type NeedWakeFields, type NeedWakeOutcome } from "./need-wake.js";
3
+
4
+ const fields = (over: Partial<NeedWakeFields> = {}): NeedWakeFields => ({
5
+ outcome: "met",
6
+ id: "need-7k2q",
7
+ title: "Connect GitHub",
8
+ why: "to open the pull request for the fix",
9
+ result: 'GitHub is connected as "github".',
10
+ use: ["In this turn, prefix a command with GH_TOKEN={{secret:github/token}}."],
11
+ ...over,
12
+ });
13
+
14
+ const OUTCOMES: NeedWakeOutcome[] = ["met", "declined"];
15
+
16
+ describe("need wake", () => {
17
+ // The composer and the parser are the same piece of knowledge; this holds them together when either is reworded.
18
+ it.each(OUTCOMES)("round-trips a %s wake back to its fields", (outcome) => {
19
+ const prompt = needWakePrompt(fields({ outcome }));
20
+ expect(needWakeOf(prompt)).toEqual({ outcome, title: "Connect GitHub", id: "need-7k2q", sent: prompt });
21
+ });
22
+
23
+ it("tells a met need to carry on and a declined one not to wait", () => {
24
+ expect(needWakePrompt(fields()).split("\n").at(-1)).toBe("Continue the task that needed it.");
25
+ expect(needWakePrompt(fields({ outcome: "declined", use: [] }))).not.toContain("Continue the task");
26
+ });
27
+
28
+ it("leaves the agent's reason out when it gave none, rather than printing an empty label", () => {
29
+ expect(needWakePrompt(fields({ why: undefined }))).not.toContain("You asked because:");
30
+ expect(needWakePrompt(fields())).toContain("You asked because: to open the pull request for the fix");
31
+ });
32
+
33
+ it("is not a wake once its labelled lines are gone", () => {
34
+ const prompt = needWakePrompt(fields());
35
+ expect(needWakeOf(prompt.replace("Need id: need-7k2q\n", ""))).toBeUndefined();
36
+ expect(needWakeOf(prompt.replace("Need: Connect GitHub\n", ""))).toBeUndefined();
37
+ expect(needWakeOf("please connect github")).toBeUndefined();
38
+ });
39
+
40
+ it("becomes a notice row that says what happened, and every transcript reader finds it", () => {
41
+ const prompt = needWakePrompt(fields());
42
+ const row = needWakeRow(prompt);
43
+ expect(row).toEqual({
44
+ role: "notice",
45
+ text: "Connect GitHub: done, and the agent carries on.",
46
+ needWake: { outcome: "met", title: "Connect GitHub", id: "need-7k2q", sent: prompt },
47
+ });
48
+ expect(unspokenPromptRow(prompt)).toEqual(row);
49
+ expect(needWakeRow(needWakePrompt(fields({ outcome: "declined" })))?.text).toBe("Connect GitHub: declined, and the agent was told.");
50
+ });
51
+
52
+ it("words a raised need's row for a reader that draws no card", () => {
53
+ expect(needRowText({ title: "The OPENAI_API_KEY secret" })).toBe("Needs a person: The OPENAI_API_KEY secret");
54
+ });
55
+ });
@@ -0,0 +1,71 @@
1
+ import type { TranscriptNeedWake, TranscriptRow } from "./transcript.js";
2
+
3
+ // What a need says when its answer reaches a conversation, and how that reads to a person. Delivered as an ordinary
4
+ // turn prompt (a steer into a live turn, or a turn of its own), so composing it and recognising it stay one piece of
5
+ // knowledge, exactly as a condition watch's wake does (watch-wake.ts): the daemon's record, a steered live turn and a
6
+ // provider's session store all turn this prompt back into the same row.
7
+
8
+ export type NeedWakeOutcome = TranscriptNeedWake["outcome"];
9
+
10
+ // Per-outcome opening sentence, which is also what the parser anchors on: keep each unique and stable across releases,
11
+ // or a reworded opening un-recognises wakes already in a record and they read as the user's own words.
12
+ const OPENINGS: Record<NeedWakeOutcome, string> = {
13
+ met: "Need met: a person gave you what you asked for.",
14
+ declined: "Need declined: a person said no to what you asked for. Carry on without it, and say plainly what it would have enabled.",
15
+ };
16
+
17
+ // Labels the prompt writes and the parser reads back. `Need` and `Need id` are load-bearing on both sides.
18
+ const NEED = "Need: ";
19
+ const NEED_ID = "Need id: ";
20
+ const ASKED = "You asked because: ";
21
+
22
+ export interface NeedWakeFields {
23
+ readonly outcome: NeedWakeOutcome;
24
+ readonly id: string;
25
+ readonly title: string;
26
+ readonly why: string | undefined;
27
+ // The daemon's sentence on how it ended ("Connected as "github".", "Stored under OPENAI_API_KEY.").
28
+ readonly result: string;
29
+ // What the agent can do with it now, and what arrives on its next turn; empty for a decline.
30
+ readonly use: readonly string[];
31
+ }
32
+
33
+ export const needWakePrompt = (fields: NeedWakeFields): string =>
34
+ [
35
+ OPENINGS[fields.outcome],
36
+ `${NEED}${fields.title}`,
37
+ `${NEED_ID}${fields.id}`,
38
+ ...(fields.why === undefined || fields.why === "" ? [] : [`${ASKED}${fields.why}`]),
39
+ fields.result,
40
+ ...fields.use,
41
+ ...(fields.outcome === "met" ? ["Continue the task that needed it."] : []),
42
+ ].join("\n");
43
+
44
+ const headline = (outcome: NeedWakeOutcome, title: string): string =>
45
+ outcome === "met" ? `${title}: done, and the agent carries on.` : `${title}: declined, and the agent was told.`;
46
+
47
+ const valueOn = (lines: readonly string[], label: string): string | undefined => lines.find((line) => line.startsWith(label))?.slice(label.length);
48
+
49
+ // The words a raised need's row carries for a reader that draws no card (search, an export, an older client); the card
50
+ // itself is drawn from the need.
51
+ export const needRowText = (need: { readonly title: string }): string => `Needs a person: ${need.title}`;
52
+
53
+ // Which need wake a stored prompt is, if any; undefined for every prompt that isn't one, so any reader can ask without
54
+ // checking first. One that opens as a wake but lost its labelled lines is not one.
55
+ export const needWakeOf = (prompt: string): TranscriptNeedWake | undefined => {
56
+ const outcome = (Object.keys(OPENINGS) as NeedWakeOutcome[]).find((key) => prompt.startsWith(OPENINGS[key]));
57
+ if (outcome === undefined) {
58
+ return undefined;
59
+ }
60
+ const lines = prompt.split("\n");
61
+ const title = valueOn(lines, NEED);
62
+ const id = valueOn(lines, NEED_ID);
63
+ return title === undefined || id === undefined ? undefined : { outcome, title, id, sent: prompt };
64
+ };
65
+
66
+ // The row a wake becomes: a notice, since a need being answered happened to the conversation rather than being said by
67
+ // either side. Undefined when the prompt is not a need wake at all.
68
+ export const needWakeRow = (prompt: string): TranscriptRow | undefined => {
69
+ const wake = needWakeOf(prompt);
70
+ return wake === undefined ? undefined : { role: "notice", text: headline(wake.outcome, wake.title), needWake: wake };
71
+ };
@@ -3,6 +3,7 @@ import { AgentHarnessSchema, AgentProviderSchema } from "../schemas/agent.js";
3
3
  import { ShareDetailSchema } from "../schemas/share.js";
4
4
  import { TurnErrandSchema, TurnSpeakerSchema } from "../schemas/speaker.js";
5
5
  import { SubagentKindSchema, SubagentStatusSchema, SubagentVerificationSchema } from "../schemas/terminal.js";
6
+ import { NeedSchema } from "../schemas/needs.js";
6
7
  import { RetryLadderSchema } from "../schemas/turn-break.js";
7
8
  import type { ToolCallContent, ToolCallLocation, ToolCallStatus, ToolKind} from "./requests.js";
8
9
  import { browserHelpRequest, capabilityOfferRequest, CapabilityOutcomeSchema, credentialOfferRequest, CredentialReceiptSchema, paymentOfferRequest, PaymentReceiptSchema, PermissionAskSchema, permissionRequest, planRequest, questionRequest, terminalHelpRequest, TodoItemSchema, ToolCallContentSchema, ToolCallLocationSchema, ToolCallStatusSchema, ToolKindSchema } from "./requests.js";
@@ -155,6 +156,15 @@ export const TranscriptWatchWakeSchema = z.object({
155
156
  });
156
157
  export type TranscriptWatchWake = z.infer<typeof TranscriptWatchWakeSchema>;
157
158
 
159
+ // A need's answer that reached this conversation as a prompt (need-wake.ts); the row keeps the prompt and names the need.
160
+ export const TranscriptNeedWakeSchema = z.object({
161
+ outcome: z.enum(["met", "declined"]).describe("How the need ended: a person gave it, or said no."),
162
+ title: z.string().describe("The need, as its card leads with it."),
163
+ id: z.string().describe("The need's handle, which its live card is keyed by."),
164
+ sent: z.string().describe("The whole prompt the model received, disclosed under the row."),
165
+ });
166
+ export type TranscriptNeedWake = z.infer<typeof TranscriptNeedWakeSchema>;
167
+
158
168
  // Another agent's words that reached this conversation as a prompt; the row keeps the prompt and names the sender.
159
169
  export const TranscriptAgentWordsSchema = z.object({
160
170
  kind: z.enum(["peer", "child"]).describe("Who sent it: another conversation in the workspace, or a subagent this one started."),
@@ -299,6 +309,12 @@ export const TranscriptRowSchema = z.object({
299
309
  capabilityOffer: TranscriptCapabilityOfferSchema.optional().describe("The capability setup this row asked for, the decision, and the outcome."),
300
310
  paymentOffer: TranscriptPaymentOfferSchema.optional().describe("The payment this row asked for, the decision, and the receipt."),
301
311
  watchWake: TranscriptWatchWakeSchema.optional().describe("The condition watch that woke this conversation, and the prompt it was woken with."),
312
+ // A need is not a parked card: it holds no turn open, outlives the one that raised it, and its live state is the
313
+ // needs store's, keyed by `need.id`. What rides here is the need as it was raised, for a reader with no store.
314
+ need: NeedSchema.optional().describe(
315
+ "Something the agent asked a person for, as it was when raised. Its live state (answered, met) is read by its id, since it outlives the turn.",
316
+ ),
317
+ needWake: TranscriptNeedWakeSchema.optional().describe("The answered need that reached this conversation, and the prompt it came as."),
302
318
  agentWords: TranscriptAgentWordsSchema.optional().describe("Another agent's words that reached this conversation, whose they are, and the prompt they came as."),
303
319
  backgroundJob: TranscriptBackgroundJobSchema.optional().describe("The background job this row marks the start of."),
304
320
  credentialOffer: TranscriptCredentialOfferSchema.optional().describe(
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ import { netdiskContract } from "./contracts/netdisk.contract.js";
32
32
  import { providersContract } from "./contracts/providers.contract.js";
33
33
  import { pushContract } from "./contracts/push.contract.js";
34
34
  import { safetyContract } from "./contracts/safety.contract.js";
35
+ import { needsContract } from "./contracts/needs.contract.js";
35
36
  import { secretsContract } from "./contracts/secrets.contract.js";
36
37
  import { sessionsContract } from "./contracts/sessions.contract.js";
37
38
  import { settingsContract } from "./contracts/settings.contract.js";
@@ -92,6 +93,7 @@ export { publicContract } from "./contracts/public.contract.js";
92
93
  export { providersContract, type RunnableProviders, RunnableProvidersSchema } from "./contracts/providers.contract.js";
93
94
  export { pushContract } from "./contracts/push.contract.js";
94
95
  export { safetyContract } from "./contracts/safety.contract.js";
96
+ export { needsContract } from "./contracts/needs.contract.js";
95
97
  export { secretsContract } from "./contracts/secrets.contract.js";
96
98
  export { sessionsContract } from "./contracts/sessions.contract.js";
97
99
  export { settingsContract } from "./contracts/settings.contract.js";
@@ -115,6 +117,7 @@ export * from "./events/land-breakage.js";
115
117
  export * from "./events/land-conflict.js";
116
118
  export * from "./events/verify-nudge.js";
117
119
  export * from "./events/errands.js";
120
+ export * from "./events/need-wake.js";
118
121
  export * from "./events/watch-wake.js";
119
122
  export * from "./policy/request-status.js";
120
123
  export * from "./events/child-run.js";
@@ -214,6 +217,7 @@ export * from "./schemas/providers/provider-subscriptions.js";
214
217
  export * from "./schemas/public.js";
215
218
  export * from "./schemas/push.js";
216
219
  export * from "./schemas/git/remote-refs.js";
220
+ export * from "./schemas/needs.js";
217
221
  export * from "./schemas/secrets.js";
218
222
  export * from "./schemas/netdisk.js";
219
223
  export * from "./schemas/sessions.js";
@@ -224,6 +228,7 @@ export * from "./schemas/shared.js";
224
228
  export * from "./schemas/system-prompt.js";
225
229
  export * from "./schemas/system.js";
226
230
  export * from "./schemas/state-plan.js";
231
+ export * from "./schemas/updates.js";
227
232
  export * from "./schemas/terminal.js";
228
233
  export * from "./schemas/turn-break.js";
229
234
  export * from "./schemas/keep-warm.js";
@@ -285,6 +290,7 @@ export const sandboxContract = {
285
290
  public: publicContract,
286
291
  providers: providersContract,
287
292
  push: pushContract,
293
+ needs: needsContract,
288
294
  secrets: secretsContract,
289
295
  system: systemContract,
290
296
  translator: translatorContract,
@@ -83,9 +83,8 @@ export const RAW_ROUTES = {
83
83
  "GET /extensions/{id}/bundle": { lane: "bulk" },
84
84
  // An extension backend's own namespace, proxied verbatim.
85
85
  "ALL /x/*": {},
86
- // The `capabilities` CLI: discovery by name, and the ask that parks on an owner-decided card.
86
+ // The `capabilities` CLI's discovery by name; its ask is the needs door (contracts/needs.contract.ts).
87
87
  "GET /capabilities/connectable": { floor: "maintainer", agent: true, control: "never" },
88
- "POST /capabilities/ask": { floor: "maintainer", agent: true, control: "never" },
89
88
  // The `sandboxes` CLI; every create parks on a card in the owner's chat first.
90
89
  "GET /sandboxes": { agent: true },
91
90
  "POST /sandboxes": { agent: true },
@@ -4,6 +4,7 @@ import { AgentHarnessSchema, AgentOriginSchema, AgentProviderSchema, Conversatio
4
4
  import { LoopStateSchema } from "./loops.js";
5
5
  import { LimitPolicySchema, RetryPolicySchema, TurnBreakPolicySchema, TurnBreakSchema } from "./turn-break.js";
6
6
  import { KeepWarmSchema } from "./keep-warm.js";
7
+ import { AgentNeedSchema } from "./needs.js";
7
8
  import { EMOJI_MAX_LENGTH, isSingleEmoji } from "../text/emoji.js";
8
9
  // A fleet agent is any conversation with a registry entry, keyed by conversationId. Isolated ones own a git worktree
9
10
  // (branch agent/<id>); workspace conversations have none, but both share one status/activity/cost lifecycle.
@@ -56,7 +57,7 @@ export const AgentAttentionSchema = z.object({
56
57
  plan: z.boolean().describe("It has proposed a plan and is waiting for a yes."),
57
58
  question: z.boolean().describe("It has asked you something."),
58
59
  permission: z.boolean().describe("It wants to use a tool it needs permission for."),
59
- // A missing capability (capabilities/offers/capability-offer.ts); lets the lane say "setup needed" rather than a generic
60
+ // A missing capability on a card from before needs (docs/architecture/needs.md); lets the lane say "setup needed" rather than a generic
60
61
  // pause.
61
62
  capability: z.boolean().describe("It needs something connected that is not connected yet."),
62
63
  // A gated credential parked on a named person's click; the one pause the board's reader may not be able to clear
@@ -65,6 +66,11 @@ export const AgentAttentionSchema = z.object({
65
66
  .boolean()
66
67
  .describe("It is waiting for a named person to release a credential. The one pause that may not be yours to clear, whatever your role."),
67
68
  conflict: z.boolean().describe("Its work cannot be merged without somebody resolving a clash."),
69
+ // A need outlives its turn (docs/architecture/needs.md), so this can stand on a card that is not running at all.
70
+ need: z
71
+ .boolean()
72
+ .optional()
73
+ .describe("It asked a person for something it still needs: a connection, a secret, wider reach, a tool. Absent from a daemon older than needs."),
68
74
  });
69
75
  export type AgentAttention = z.infer<typeof AgentAttentionSchema>;
70
76
  // What the last turn left open, measured at the moment it ended, unlike AgentAttentionSchema's live parked waits.
@@ -527,6 +533,13 @@ export const AgentSummarySchema = z.object({
527
533
  .describe(
528
534
  "Outside conditions this conversation is parked on, each of which will wake it. Absent means none, which is nearly every conversation: an armed watch is why a finished-looking agent starts working by itself, and why a hosted machine will not go idle.",
529
535
  ),
536
+ // Kept in the needs store, so unlike `jobs` this survives a restart and stands while the conversation is idle.
537
+ needs: z
538
+ .array(AgentNeedSchema)
539
+ .optional()
540
+ .describe(
541
+ "What it is waiting on people for and has not got yet, oldest first. Absent means nothing: an open need is why an idle-looking agent still needs you.",
542
+ ),
530
543
  // In memory only: a daemon restart leaves transcript rows naming jobs this list no longer carries.
531
544
  jobs: z
532
545
  .array(
@@ -104,6 +104,15 @@ export const SshConfigSchema = z.discriminatedUnion("auth", [
104
104
  user: z.string().min(1).describe("Which user to connect as."),
105
105
  privateKey: z.string().min(1).describe("The private key, whole. Stored with tight permissions and never echoed back."),
106
106
  }),
107
+ // The sandbox made this key itself (capabilities.sshKey), so its private half never crossed the wire; stored and
108
+ // written exactly like a pasted one.
109
+ z.object({
110
+ auth: z.literal("generated").describe("Sign in with a key the sandbox generated. Its private half never left the sandbox."),
111
+ host: z.string().min(1).describe("The machine's address."),
112
+ port: z.coerce.number().default(22).describe("Which port it listens on."),
113
+ user: z.string().min(1).describe("Which user to connect as."),
114
+ privateKey: z.string().min(1).describe("The private half of the generated key. Stored with tight permissions and never echoed back."),
115
+ }),
107
116
  z.object({
108
117
  auth: z.literal("password").describe("Sign in with a password."),
109
118
  host: z.string().min(1).describe("The machine's address."),
@@ -112,6 +121,14 @@ export const SshConfigSchema = z.discriminatedUnion("auth", [
112
121
  password: z.string().min(1).describe("The password. Stored, never echoed back."),
113
122
  }),
114
123
  ]);
124
+ // What a form sends where a generated key's private half goes: the token capabilities.sshKey answered with, which the
125
+ // add swaps for the key the daemon kept. VAULTED's sibling (policy/capability-secrets.ts) for a value not stored yet; a
126
+ // marker with nothing held behind it is refused, never written.
127
+ const STASHED_PREFIX = "__intentic_stashed__:";
128
+ export const stashedMarker = (token: string): string => `${STASHED_PREFIX}${token}`;
129
+ // The token a marker names; undefined for anything else (a real key, VAULTED, a blank).
130
+ export const stashedToken = (value: unknown): string | undefined =>
131
+ typeof value === "string" && value.startsWith(STASHED_PREFIX) && value.length > STASHED_PREFIX.length ? value.slice(STASHED_PREFIX.length) : undefined;
115
132
  // What is optional about the in-sandbox Docker Engine: `gpu` is an IMAGE option (rides the Dockerfile overlay, needs a
116
133
  // rebuild), everything else is an ENGINE option (rewrites daemon.json, restarts dockerd, no rebuild but stops running
117
134
  // containers). Flat strings, not nested booleans, to match the manifest's own two-state convention.
@@ -460,3 +477,14 @@ export const CapabilityProbeSchema = z.object({
460
477
  who: z.string().optional().describe("Who the service said the credential belongs to, when it said."),
461
478
  });
462
479
  export type CapabilityProbe = z.infer<typeof CapabilityProbeSchema>;
480
+ // POST /capabilities/ssh-key: a key pair made inside the sandbox for a connection being set up. The public half is the
481
+ // whole answer; the private half stays in the daemon until an add sends `stashedMarker(token)` where it goes.
482
+ export const SshKeySchema = z.object({
483
+ publicKey: z.string().describe("The public half, as the one line a server's authorized_keys holds: `ssh-ed25519 AAAA… intentic-<sandbox>`."),
484
+ token: z
485
+ .string()
486
+ .describe(
487
+ "Stands for the private half, which never leaves the sandbox. Sent wrapped as a marker in the private key's place, it installs that key once, and it lapses after thirty minutes.",
488
+ ),
489
+ });
490
+ export type SshKey = z.infer<typeof SshKeySchema>;
@@ -1,6 +1,7 @@
1
1
  // What one of the user's own machines is running.
2
2
  import { z } from "zod";
3
3
  import { parseHostConnection, type DeviceFacts, DeviceFactsSchema, MachineIdSchema, userDistrosOf, WslEnvironmentSchema } from "./hosts.js";
4
+ import { RollbackTargetSchema, UpdateOutcomeSchema } from "./updates.js";
4
5
  import { DEV_VERSION } from "../state/versions.js";
5
6
  // Desktop-sync report shape shared by the agent, daemon and browser, produced only by the agent's own `deviceReport`.
6
7
  // The agent never reports `sandboxes`; the docker half is filled in by whoever reads the report, scoped to the reader's
@@ -76,6 +77,19 @@ export const DeviceSandboxSchema = z.object({
76
77
  resources: SandboxResourcesSchema.optional(),
77
78
  // The update `ic sandbox prepare` built and left waiting for this sandbox; absent when nothing is staged.
78
79
  staged: z.object({ image: z.string(), version: z.string().optional(), channel: z.string().optional() }).optional(),
80
+ // Everything below is read off an `ic` new enough to know it; an older ic's listing simply lacks it.
81
+ // What the running image says it is.
82
+ version: z.string().optional(),
83
+ // An interrupted swap left the old container set aside with no replacement: the sandbox is down, and `start` (or
84
+ // any swap) puts it back. `running` is false while this is true.
85
+ parked: z.boolean().optional(),
86
+ // A swap has just happened and the previous version is still parked and ready: until this moment the host watches
87
+ // the new one and goes back by itself if it keeps crashing, never becomes ready, or loses its tunnel.
88
+ probationUntil: z.number().optional(),
89
+ // What the host last did about this sandbox's version (the same record the sandbox reads as `lastUpdate`).
90
+ lastUpdate: UpdateOutcomeSchema.optional(),
91
+ // Newest first: what `rollback` would return to, then the older versions kept on this machine for `rollback --to`.
92
+ rollbackTargets: z.array(RollbackTargetSchema).optional(),
79
93
  });
80
94
  export type DeviceSandbox = z.infer<typeof DeviceSandboxSchema>;
81
95
  // One operation on one sandbox, streamed as lines ending in a `result` or `error` frame. `prepare` builds the pending
@@ -118,6 +132,9 @@ export const DeviceSandboxFlowSchema = z.strictObject({
118
132
  slug: z.string().min(1),
119
133
  // Approved overlay's sha256, required only by `rebuild`; only content matching it is ever built.
120
134
  hash: z.string().optional(),
135
+ // `rollback` only: a version or pinned image from the sandbox's `rollbackTargets` to go back to instead of the
136
+ // previous one. Sent only to an agent that advertises `rollback-to`: an older one refuses the field.
137
+ to: z.string().min(1).optional(),
121
138
  // `set-shape` only, both required by it: the whole shape, and when it takes effect.
122
139
  // Strict too, unlike the same shape read off a report (where a newer ic's extra field must not fail an older
123
140
  // reader): an order carrying a field this agent does not know is refused rather than carried out without it.
@@ -68,10 +68,13 @@ export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
68
68
  // - `set-shape`: the `set-shape`/`forget-shape` ops, and `start`/`restart` applying a saved shape, all through an `ic`
69
69
  // whose `ic sandbox shape` takes the contract's own shape (`--set`); advertised only when the agent's `ic` does.
70
70
  // Both features ride that verb, so an agent advertises both or neither.
71
- export const DeviceFeatureSchema = z.enum(["reshape-later", "set-shape"]);
71
+ // - `rollback-to`: the `rollback` op's `to`, going back to an older version kept on the machine rather than the
72
+ // previous one; advertised only when the agent's `ic sandbox rollback` takes `--to`.
73
+ export const DeviceFeatureSchema = z.enum(["reshape-later", "set-shape", "rollback-to"]);
72
74
  export type DeviceFeature = z.infer<typeof DeviceFeatureSchema>;
73
75
  export const DEVICE_FEATURE_RESHAPE_LATER: DeviceFeature = "reshape-later";
74
76
  export const DEVICE_FEATURE_SET_SHAPE: DeviceFeature = "set-shape";
77
+ export const DEVICE_FEATURE_ROLLBACK_TO: DeviceFeature = "rollback-to";
75
78
  // The features an agent advertised that this build knows; an unknown one is a newer agent's and means nothing here.
76
79
  export const deviceFeatures = (facts: Pick<DeviceFacts, "features"> | undefined): DeviceFeature[] =>
77
80
  (facts?.features ?? []).flatMap((feature) => {