@intentic/sandbox-contract 1.315.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 (214) hide show
  1. package/README.md +5 -5
  2. package/dist/contracts/agent.contract.d.ts +562 -0
  3. package/dist/contracts/agent.contract.d.ts.map +1 -1
  4. package/dist/contracts/agents.contract.d.ts +402 -0
  5. package/dist/contracts/agents.contract.d.ts.map +1 -1
  6. package/dist/contracts/agents.contract.js +3 -3
  7. package/dist/contracts/agents.contract.js.map +1 -1
  8. package/dist/contracts/capabilities.contract.d.ts +16 -0
  9. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  10. package/dist/contracts/capabilities.contract.js +10 -1
  11. package/dist/contracts/capabilities.contract.js.map +1 -1
  12. package/dist/contracts/ci.contract.d.ts +22 -0
  13. package/dist/contracts/ci.contract.d.ts.map +1 -1
  14. package/dist/contracts/device.contract.d.ts +1 -0
  15. package/dist/contracts/device.contract.d.ts.map +1 -1
  16. package/dist/contracts/needs.contract.d.ts +532 -0
  17. package/dist/contracts/needs.contract.d.ts.map +1 -0
  18. package/dist/contracts/needs.contract.js +80 -0
  19. package/dist/contracts/needs.contract.js.map +1 -0
  20. package/dist/contracts/runner.contract.d.ts +294 -122
  21. package/dist/contracts/runner.contract.d.ts.map +1 -1
  22. package/dist/contracts/secrets.contract.d.ts +16 -0
  23. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  24. package/dist/contracts/secrets.contract.js +12 -2
  25. package/dist/contracts/secrets.contract.js.map +1 -1
  26. package/dist/contracts/sessions.contract.d.ts +79 -0
  27. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  28. package/dist/contracts/settings.contract.d.ts +2 -3
  29. package/dist/contracts/settings.contract.d.ts.map +1 -1
  30. package/dist/contracts/system.contract.d.ts +163 -0
  31. package/dist/contracts/system.contract.d.ts.map +1 -1
  32. package/dist/contracts/system.contract.js +11 -0
  33. package/dist/contracts/system.contract.js.map +1 -1
  34. package/dist/contracts/workspace.contract.d.ts +15 -137
  35. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  36. package/dist/contracts/workspace.contract.js +25 -15
  37. package/dist/contracts/workspace.contract.js.map +1 -1
  38. package/dist/events/agent-events.d.ts +309 -0
  39. package/dist/events/agent-events.d.ts.map +1 -1
  40. package/dist/events/agent-events.js +2 -0
  41. package/dist/events/agent-events.js.map +1 -1
  42. package/dist/events/agent-words.d.ts.map +1 -1
  43. package/dist/events/agent-words.js +2 -1
  44. package/dist/events/agent-words.js.map +1 -1
  45. package/dist/events/land-breakage.d.ts +0 -2
  46. package/dist/events/land-breakage.d.ts.map +1 -1
  47. package/dist/events/land-breakage.js +0 -2
  48. package/dist/events/land-breakage.js.map +1 -1
  49. package/dist/events/need-wake.d.ts +17 -0
  50. package/dist/events/need-wake.d.ts.map +1 -0
  51. package/dist/events/need-wake.js +34 -0
  52. package/dist/events/need-wake.js.map +1 -0
  53. package/dist/events/system-events.d.ts +40 -0
  54. package/dist/events/system-events.d.ts.map +1 -1
  55. package/dist/events/transcript.d.ts +484 -0
  56. package/dist/events/transcript.d.ts.map +1 -1
  57. package/dist/events/transcript.js +9 -0
  58. package/dist/events/transcript.js.map +1 -1
  59. package/dist/index.d.ts +1956 -284
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +8 -2
  62. package/dist/index.js.map +1 -1
  63. package/dist/models/agent-runtimes.d.ts.map +1 -1
  64. package/dist/models/agent-runtimes.js +1 -1
  65. package/dist/models/agent-runtimes.js.map +1 -1
  66. package/dist/models/plan-pools.d.ts +2 -1
  67. package/dist/models/plan-pools.d.ts.map +1 -1
  68. package/dist/models/plan-pools.js +8 -4
  69. package/dist/models/plan-pools.js.map +1 -1
  70. package/dist/protocol/raw-routes.d.ts +9 -5
  71. package/dist/protocol/raw-routes.d.ts.map +1 -1
  72. package/dist/protocol/raw-routes.js +2 -1
  73. package/dist/protocol/raw-routes.js.map +1 -1
  74. package/dist/protocol/request-id.d.ts +0 -1
  75. package/dist/protocol/request-id.d.ts.map +1 -1
  76. package/dist/protocol/request-id.js +0 -1
  77. package/dist/protocol/request-id.js.map +1 -1
  78. package/dist/schemas/agent.js +1 -1
  79. package/dist/schemas/agent.js.map +1 -1
  80. package/dist/schemas/agents.d.ts +58 -1
  81. package/dist/schemas/agents.d.ts.map +1 -1
  82. package/dist/schemas/agents.js +11 -2
  83. package/dist/schemas/agents.js.map +1 -1
  84. package/dist/schemas/automations.d.ts +19 -0
  85. package/dist/schemas/automations.d.ts.map +1 -1
  86. package/dist/schemas/capabilities.d.ts +19 -0
  87. package/dist/schemas/capabilities.d.ts.map +1 -1
  88. package/dist/schemas/capabilities.js +16 -0
  89. package/dist/schemas/capabilities.js.map +1 -1
  90. package/dist/schemas/ci.d.ts +45 -0
  91. package/dist/schemas/ci.d.ts.map +1 -1
  92. package/dist/schemas/ci.js +18 -0
  93. package/dist/schemas/ci.js.map +1 -1
  94. package/dist/schemas/devices.d.ts +68 -1
  95. package/dist/schemas/devices.d.ts.map +1 -1
  96. package/dist/schemas/devices.js +10 -4
  97. package/dist/schemas/devices.js.map +1 -1
  98. package/dist/schemas/hosts.d.ts +2 -0
  99. package/dist/schemas/hosts.d.ts.map +1 -1
  100. package/dist/schemas/hosts.js +2 -1
  101. package/dist/schemas/hosts.js.map +1 -1
  102. package/dist/schemas/needs.d.ts +661 -0
  103. package/dist/schemas/needs.d.ts.map +1 -0
  104. package/dist/schemas/needs.js +196 -0
  105. package/dist/schemas/needs.js.map +1 -0
  106. package/dist/schemas/personas.js +2 -2
  107. package/dist/schemas/personas.js.map +1 -1
  108. package/dist/schemas/providers/plan-limits.d.ts +2 -0
  109. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  110. package/dist/schemas/providers/plan-limits.js +4 -0
  111. package/dist/schemas/providers/plan-limits.js.map +1 -1
  112. package/dist/schemas/secrets.d.ts +18 -0
  113. package/dist/schemas/secrets.d.ts.map +1 -1
  114. package/dist/schemas/secrets.js +20 -0
  115. package/dist/schemas/secrets.js.map +1 -1
  116. package/dist/schemas/settings-history.d.ts +1 -1
  117. package/dist/schemas/settings-history.d.ts.map +1 -1
  118. package/dist/schemas/settings-history.js +2 -1
  119. package/dist/schemas/settings-history.js.map +1 -1
  120. package/dist/schemas/settings.d.ts +2 -4
  121. package/dist/schemas/settings.d.ts.map +1 -1
  122. package/dist/schemas/settings.js +10 -13
  123. package/dist/schemas/settings.js.map +1 -1
  124. package/dist/schemas/state-plan.d.ts +1 -0
  125. package/dist/schemas/state-plan.d.ts.map +1 -1
  126. package/dist/schemas/state-plan.js +3 -1
  127. package/dist/schemas/state-plan.js.map +1 -1
  128. package/dist/schemas/system.d.ts +20 -0
  129. package/dist/schemas/system.d.ts.map +1 -1
  130. package/dist/schemas/system.js +7 -0
  131. package/dist/schemas/system.js.map +1 -1
  132. package/dist/schemas/updates.d.ts +39 -0
  133. package/dist/schemas/updates.d.ts.map +1 -0
  134. package/dist/schemas/updates.js +31 -0
  135. package/dist/schemas/updates.js.map +1 -0
  136. package/dist/schemas/workspace/push-checks.d.ts +222 -0
  137. package/dist/schemas/workspace/push-checks.d.ts.map +1 -0
  138. package/dist/schemas/workspace/push-checks.js +97 -0
  139. package/dist/schemas/workspace/push-checks.js.map +1 -0
  140. package/dist/schemas/workspace/workspace-tree.d.ts +9 -0
  141. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  142. package/dist/schemas/workspace/workspace-tree.js +12 -0
  143. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  144. package/dist/state/definition.d.ts +16 -4
  145. package/dist/state/definition.d.ts.map +1 -1
  146. package/dist/state/history-state.d.ts.map +1 -1
  147. package/dist/state/history-state.js +3 -0
  148. package/dist/state/history-state.js.map +1 -1
  149. package/dist/state/runtime-state.d.ts +2 -2
  150. package/dist/state/runtime-state.d.ts.map +1 -1
  151. package/dist/state/runtime-state.js +1 -1
  152. package/dist/state/runtime-state.js.map +1 -1
  153. package/dist/state/workspace-state.d.ts +5 -11
  154. package/dist/state/workspace-state.d.ts.map +1 -1
  155. package/dist/state/workspace-state.js +2 -13
  156. package/dist/state/workspace-state.js.map +1 -1
  157. package/dist/text/transcript-fold.d.ts.map +1 -1
  158. package/dist/text/transcript-fold.js +3 -0
  159. package/dist/text/transcript-fold.js.map +1 -1
  160. package/package.json +5 -5
  161. package/src/contracts/agents.contract.ts +3 -3
  162. package/src/contracts/capabilities.contract.ts +14 -1
  163. package/src/contracts/needs.contract.ts +103 -0
  164. package/src/contracts/secrets.contract.ts +17 -3
  165. package/src/contracts/system.contract.ts +14 -0
  166. package/src/contracts/workspace.contract.ts +35 -21
  167. package/src/events/agent-events.ts +4 -0
  168. package/src/events/agent-words.ts +2 -1
  169. package/src/events/errands.test.ts +4 -4
  170. package/src/events/land-breakage.ts +5 -9
  171. package/src/events/need-wake.test.ts +55 -0
  172. package/src/events/need-wake.ts +71 -0
  173. package/src/events/transcript.ts +16 -0
  174. package/src/events/verify-nudge.ts +3 -3
  175. package/src/index.ts +8 -2
  176. package/src/models/agent-runtimes.ts +5 -4
  177. package/src/models/plan-pools.test.ts +15 -0
  178. package/src/models/plan-pools.ts +20 -4
  179. package/src/protocol/ingress-contract.fixture.json +2 -0
  180. package/src/protocol/raw-routes.ts +5 -3
  181. package/src/protocol/request-id.ts +0 -3
  182. package/src/schemas/agent.ts +1 -1
  183. package/src/schemas/agents.ts +19 -8
  184. package/src/schemas/automations.ts +2 -2
  185. package/src/schemas/capabilities.ts +28 -0
  186. package/src/schemas/ci.ts +25 -0
  187. package/src/schemas/devices.ts +25 -12
  188. package/src/schemas/hosts.ts +4 -1
  189. package/src/schemas/needs.ts +292 -0
  190. package/src/schemas/personas.ts +2 -2
  191. package/src/schemas/providers/plan-limits.ts +8 -0
  192. package/src/schemas/secrets.ts +23 -0
  193. package/src/schemas/settings-history.ts +3 -1
  194. package/src/schemas/settings.test.ts +4 -3
  195. package/src/schemas/settings.ts +25 -31
  196. package/src/schemas/speaker.ts +4 -4
  197. package/src/schemas/state-plan.ts +8 -1
  198. package/src/schemas/system.ts +15 -0
  199. package/src/schemas/updates.ts +56 -0
  200. package/src/schemas/workspace/{mainline.test.ts → push-checks.test.ts} +5 -5
  201. package/src/schemas/workspace/push-checks.ts +155 -0
  202. package/src/schemas/workspace/workspace-tree.ts +15 -0
  203. package/src/state/fix-stance.test.ts +2 -1
  204. package/src/state/history-state.ts +5 -0
  205. package/src/state/landed-commit.ts +1 -1
  206. package/src/state/red-streak.ts +4 -4
  207. package/src/state/runtime-state.ts +2 -3
  208. package/src/state/workspace-state.ts +5 -13
  209. package/src/text/transcript-fold.ts +4 -0
  210. package/dist/schemas/workspace/mainline.d.ts +0 -513
  211. package/dist/schemas/workspace/mainline.d.ts.map +0 -1
  212. package/dist/schemas/workspace/mainline.js +0 -182
  213. package/dist/schemas/workspace/mainline.js.map +0 -1
  214. package/src/schemas/workspace/mainline.ts +0 -268
@@ -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(
@@ -1,7 +1,7 @@
1
1
  // The follow-up the daemon USED to send when a turn ended with work it could not confirm. Nothing sends it any more:
2
- // checks run after work lands, never inside a conversation (schemas/workspace/mainline.ts). It stays on the wire because
3
- // records already hold it, and the chat has to go on recognising it: a prompt nobody typed must not reach a reader as
4
- // their own words.
2
+ // nothing checks inside a conversation, CI checks what the owner pushes (schemas/workspace/push-checks.ts). It stays on
3
+ // the wire because records already hold it, and the chat has to go on recognising it: a prompt nobody typed must not
4
+ // reach a reader as their own words.
5
5
 
6
6
  // Anchored on by the reader, so it must stay unique and stable across releases — a reworded opening un-recognises every
7
7
  // nudge already in a record, and they read as the user's own typing again.
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";
@@ -83,7 +84,7 @@ export { intenticContract } from "./contracts/intentic.contract.js";
83
84
  export { inventoryContract } from "./contracts/inventory.contract.js";
84
85
  export { issuesContract } from "./contracts/issues.contract.js";
85
86
  export { logsContract } from "./contracts/logs.contract.js";
86
- export { REQUEST_ID_EVIDENCE_ROUTE, REQUEST_ID_HEADER } from "./protocol/request-id.js";
87
+ export { REQUEST_ID_HEADER } from "./protocol/request-id.js";
87
88
  export * from "./protocol/edge-verdict.js";
88
89
  export { loopsContract } from "./contracts/loops.contract.js";
89
90
  export { panelsContract } from "./contracts/panels.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";
@@ -234,7 +239,7 @@ export * from "./schemas/webext.js";
234
239
  export * from "./schemas/workflows.js";
235
240
  export * from "./schemas/workspace/workspace-repos.js";
236
241
  export * from "./schemas/workspace/workspace-search.js";
237
- export * from "./schemas/workspace/mainline.js";
242
+ export * from "./schemas/workspace/push-checks.js";
238
243
  export * from "./schemas/workspace/workspace-setup.js";
239
244
  export * from "./schemas/workspace/workspace-tree.js";
240
245
  export * from "./state/arrival.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,
@@ -181,7 +181,7 @@ export const PI: AgentCapabilities = {
181
181
  secrets: "none",
182
182
  };
183
183
 
184
- // Cursor's own runtime, driven through `@cursor/sdk` in this daemon's process; an embedding surface, so most seams
184
+ // Cursor's own runtime, driven through `@cursor/sdk` from this daemon's process; an embedding surface, so most seams
185
185
  // other runtimes lack are function arguments here. The harness axis doesn't apply: the SDK is the only door.
186
186
  export const CURSOR: AgentCapabilities = {
187
187
  runtime: "cursor",
@@ -199,11 +199,12 @@ export const CURSOR: AgentCapabilities = {
199
199
  // Cursor publishes effort as model parameters, not one scale; true here just means it's forwardable at all.
200
200
  effort: true,
201
201
  fastMode: false,
202
- // cwd: Cursor's loop runs inside the daemon's own process, whose /work must stay the shared checkout.
203
- isolation: "cwd",
202
+ // The SDK agent of an anchored turn runs in a runtime process born in the turn's mount namespace (the daemon keeps
203
+ // the loop, the tools and the hook gate), so its shell and its edits see the worktree at /work.
204
+ isolation: "namespace",
204
205
  // Cursor's commands are files on disk the SDK loads but doesn't publish back, so there's no list to offer.
205
206
  commands: false,
206
- // The SDK runs its shell in-process; there is no tmux session for the terminal panel to attach to.
207
+ // The SDK runs its shell itself, as a plain child; there is no tmux session for the terminal panel to attach to.
207
208
  terminals: false,
208
209
  // The SDK throws typed errors instead of dissolving a refusal into prose, so the adapter files coded frames.
209
210
  recovery: true,
@@ -209,6 +209,21 @@ test("a refusal stands until a later reading contradicts it, and never past its
209
209
  expect(refusalVerdict(refusal({ kind: "limit", account: "a" }), [])).toBe("standing");
210
210
  });
211
211
 
212
+ // Codex's usage endpoint kept reading room while every turn was refused "try again at 3:38 PM": the provider's own
213
+ // instant is what ends the refusal, where it is known which account it refused.
214
+ test("a spent allowance with the provider's own reset stands until that instant, whatever a later reading says", () => {
215
+ const room = at(2_000, window({ kind: "five_hour", utilization: 0 }));
216
+ const until = { kind: "limit", resetsAt: 10 } as const;
217
+ expect(refusalVerdict(refusal({ ...until, account: "a" }), [{ account: "a", usage: room }], 9_999)).toBe("standing");
218
+ expect(refusalVerdict(refusal({ ...until, account: "a" }), [{ account: "a", usage: room }], 10_000)).toBe("answered");
219
+ // A routed refusal names no account, but with one account there is no doubt whose it was.
220
+ expect(refusalVerdict(refusal(until), [{ account: "a", usage: room }], 9_999)).toBe("standing");
221
+ // Over several, the translator benches the one it refused and routes on, so another's room still answers it.
222
+ expect(refusalVerdict(refusal(until), [{ account: "a", usage: room }, { account: "b", usage: room }], 9_999)).toBe("answered");
223
+ // Spent until the instant the provider named, not the pool's own reset.
224
+ expect(serviceStates([{ account: "a", usage: room }], refusal({ ...until, account: "a" }), undefined, 9_999).get("a")).toEqual({ kind: "spent", reopensAt: 10 });
225
+ });
226
+
212
227
  test("a provider's accounts are judged together: a standing refusal lands only on the account it names", () => {
213
228
  const room = at(500, window({ kind: "five_hour", utilization: 40 }));
214
229
  const states = serviceStates([{ account: "a", usage: room }, { account: "b", usage: room }], refusal({ kind: "entitlement", account: "a" }));
@@ -147,6 +147,9 @@ export interface ServiceFacts {
147
147
  readonly seatRefusal?: string | undefined;
148
148
  readonly cooling?: { readonly until?: number | undefined; readonly reason?: string | undefined; readonly verify?: string | undefined } | undefined;
149
149
  readonly usage?: AccountUsage | undefined;
150
+ // How the account is named to a person (its label, a routed credential's address); a surface that quotes a figure
151
+ // says whose it is.
152
+ readonly label?: string | undefined;
150
153
  }
151
154
 
152
155
  // A spent allowance is over once a reading with room lands after it; a refused credential once any reading does (the
@@ -168,11 +171,22 @@ const answers = (refusal: ProviderRefusal, facts: ServiceFacts): boolean => {
168
171
  * the account it named is disconnected, `answered` once that account (or, for a refusal naming none, any account) has a
169
172
  * later reading that contradicts it, else `standing`. An empty list is one not loaded yet, so the refusal stands.
170
173
  */
171
- export const refusalVerdict = (refusal: ProviderRefusal, accounts: readonly ServiceFacts[]): "standing" | "answered" | "gone" => {
174
+ export const refusalVerdict = (
175
+ refusal: ProviderRefusal,
176
+ accounts: readonly ServiceFacts[],
177
+ now: number = Date.now(),
178
+ ): "standing" | "answered" | "gone" => {
172
179
  const named = accounts.filter((facts) => facts.account === refusal.account);
173
180
  if (refusal.account !== undefined && named.length === 0) {
174
181
  return accounts.length === 0 ? "standing" : "gone";
175
182
  }
183
+ // Where the provider named when to try again, and it is known which account it refused (it named one, or there is
184
+ // only one), its own words outrank a usage endpoint that kept showing room while it refused: over at that instant,
185
+ // and not before. A nameless refusal over several accounts is still answered by any of them reading room, since the
186
+ // translator benches the one it refused and routes on.
187
+ if (refusal.kind === "limit" && refusal.resetsAt !== undefined && (refusal.account !== undefined || accounts.length <= 1)) {
188
+ return refusal.resetsAt * 1000 <= now ? "answered" : "standing";
189
+ }
176
190
  return (refusal.account === undefined ? accounts : named).some((facts) => answers(refusal, facts)) ? "answered" : "standing";
177
191
  };
178
192
 
@@ -215,10 +229,12 @@ const refusedPool = (usage: AccountUsage | undefined, refusal: ProviderRefusal,
215
229
  if (usage === undefined || binding === undefined) {
216
230
  // No pool to pin: the refusal itself is the evidence, unless it was about a different model than this question.
217
231
  const sameModel = refusal.model === undefined || model === undefined || refusal.model === model.id;
218
- return sameModel ? { kind: "spent" } : headroomState(usage, model);
232
+ return sameModel ? spent(refusal.resetsAt) : headroomState(usage, model);
219
233
  }
220
234
  const pinned = usage.windows.map((window) => (window === binding ? { ...window, utilization: Math.max(window.utilization, SPENT_UTILIZATION) } : window));
221
- return headroomState({ ...usage, windows: pinned }, model);
235
+ const state = headroomState({ ...usage, windows: pinned }, model);
236
+ // The provider's own "try again at" is when it reopens, over whatever the polled pool last said.
237
+ return state.kind === "spent" && refusal.resetsAt !== undefined ? spent(refusal.resetsAt) : state;
222
238
  };
223
239
 
224
240
  /**
@@ -262,7 +278,7 @@ export const serviceStates = (
262
278
  model?: ModelRef,
263
279
  now: number = Date.now(),
264
280
  ): ReadonlyMap<string, AccountState> => {
265
- const standing = refusal !== undefined && refusalVerdict(refusal, accounts) === "standing" ? refusal : undefined;
281
+ const standing = refusal !== undefined && refusalVerdict(refusal, accounts, now) === "standing" ? refusal : undefined;
266
282
  return new Map(
267
283
  accounts.map((facts) => [
268
284
  facts.account,
@@ -48,6 +48,7 @@
48
48
  "GET /diff/raw",
49
49
  "GET /workspace/raw",
50
50
  "GET /workspace/media",
51
+ "GET /workspace/download",
51
52
  "POST /workspace/upload",
52
53
  "POST /workspace/upload-archive",
53
54
  "GET /system/sync/ssh",
@@ -59,6 +60,7 @@
59
60
  ],
60
61
  "lanes": [
61
62
  { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/workspace/media?path=a.mp4", "lane": "bulk" },
63
+ { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/workspace/download?ticket=t", "lane": "bulk" },
62
64
  { "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/workspace/upload?path=a", "lane": "bulk" },
63
65
  { "host": "sandbox-abcdef012345.sbx.example.test:443", "method": "GET", "path": "/system/runners/git/app/info/refs?service=git-upload-pack", "lane": "bulk" },
64
66
  { "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/system/runners/git/app/git-receive-pack", "lane": "bulk" },
@@ -18,6 +18,8 @@ export const RAW_ROUTES = {
18
18
  "GET /workspace/thumb": { guest: true },
19
19
  // Fetched by a <video>/<audio> tag, which sends no header: its scoped ticket is checked in the handler.
20
20
  "GET /workspace/media": { auth: "door", guest: true, lane: "bulk" },
21
+ // Navigated to for a ZIP of a selection, so no header either: the ticket it carries names the selection.
22
+ "GET /workspace/download": { auth: "door", guest: true, lane: "bulk" },
21
23
  // An attachment is part of the message it rides with; any other target edits the shared tree.
22
24
  "POST /workspace/upload": { floor: "writer", attachmentFloor: "collaborator", lane: "bulk" },
23
25
  "POST /workspace/upload-diff": {},
@@ -81,9 +83,8 @@ export const RAW_ROUTES = {
81
83
  "GET /extensions/{id}/bundle": { lane: "bulk" },
82
84
  // An extension backend's own namespace, proxied verbatim.
83
85
  "ALL /x/*": {},
84
- // 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).
85
87
  "GET /capabilities/connectable": { floor: "maintainer", agent: true, control: "never" },
86
- "POST /capabilities/ask": { floor: "maintainer", agent: true, control: "never" },
87
88
  // The `sandboxes` CLI; every create parks on a card in the owner's chat first.
88
89
  "GET /sandboxes": { agent: true },
89
90
  "POST /sandboxes": { agent: true },
@@ -91,12 +92,13 @@ export const RAW_ROUTES = {
91
92
  "GET /wallet/status": { agent: true, control: "never" },
92
93
  "POST /wallet/fetch": { agent: true, control: "never" },
93
94
  "GET /wallet/history": { agent: true, control: "never" },
94
- // The `agents` CLI's children: start, follow up, answer (never a consent card), list.
95
+ // The `agents` CLI's children: start, follow up, answer (never a consent card), cancel, list.
95
96
  "POST /children/spawn": { agent: true, control: "never" },
96
97
  "GET /children/providers": { agent: true, control: "never" },
97
98
  "POST /children/wait": { agent: true, control: "never" },
98
99
  "POST /children/send": { agent: true, control: "never" },
99
100
  "POST /children/answer": { agent: true, control: "never" },
101
+ "POST /children/cancel": { agent: true, control: "never" },
100
102
  "GET /children": { agent: true, control: "never" },
101
103
  // The fleet reads, and the one write: words in front of another conversation, which a person can do by typing.
102
104
  "GET /fleet": { agent: true },
@@ -1,5 +1,2 @@
1
1
  // Correlates a browser call to the daemon request that served it; a token only, never a security boundary.
2
2
  export const REQUEST_ID_HEADER = "x-intentic-request-id";
3
-
4
- // Confirms daemon CORS accepts this header before sending; no evidence here means don't send, unlike supportsRoute.
5
- export const REQUEST_ID_EVIDENCE_ROUTE = "logs.report";
@@ -131,7 +131,7 @@ export type ForkedFrom = z.infer<typeof ForkedFromSchema>;
131
131
  const AgentTurnFieldsSchema = z.object({
132
132
  prompt: z.string().describe("What to say to the agent. May be empty if you are only attaching files."),
133
133
  errand: TurnErrandSchema.optional().describe(
134
- "What the words are for, when the app or the sandbox composed them rather than a person typing them: a land conflict to resolve, a red main line to fix. Shown as the sandbox's words, not yours. Leave it out for your own words.",
134
+ "What the words are for, when the app or the sandbox composed them rather than a person typing them: a land conflict to resolve, a red CI run to fix. Shown as the sandbox's words, not yours. Leave it out for your own words.",
135
135
  ),
136
136
  // Minted by the sender so a send whose answer was lost can be sent again without being delivered twice.
137
137
  messageId: z
@@ -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.
@@ -88,11 +94,11 @@ export const UnfinishedWorkSchema = z.object({
88
94
  check: z
89
95
  .string()
90
96
  .optional()
91
- .describe("The end-of-turn check that was still failing when the turn ended, by name. No longer written: checks run after work lands."),
97
+ .describe("The end-of-turn check that was still failing when the turn ended, by name. No longer written: nothing checks inside a turn."),
92
98
  });
93
99
  export type UnfinishedWork = z.infer<typeof UnfinishedWorkSchema>;
94
100
  // What the last turn showed of its own work, read off its tool calls and never asked of the model: the record a card
95
- // badges instead of sending the turn back to prove anything. Checks run after the work lands (mainline.ts).
101
+ // badges instead of sending the turn back to prove anything. CI checks what the owner pushes (push-checks.ts).
96
102
  export const TurnProofSchema = z.object({
97
103
  at: z.number().describe("When the turn that left this ended, in milliseconds."),
98
104
  verification: z
@@ -137,7 +143,7 @@ export const LandedMessageSchema = z.object({
137
143
  .optional()
138
144
  .describe("Why a test this change weakened is meant to be weaker, in the conversation's own words. Nearly always absent."),
139
145
  // Present only when the conversation declared an exception a check should grant this change (`Allow: <check> — <reason>`);
140
- // the check after the land and the push read it off the commit.
146
+ // the push reads it off the commit.
141
147
  allows: z
142
148
  .array(z.string())
143
149
  .optional()
@@ -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(
@@ -574,10 +587,8 @@ export const AgentSummarySchema = z.object({
574
587
  ),
575
588
  });
576
589
  export type AgentSummary = z.infer<typeof AgentSummarySchema>;
577
- // Whether a conversation runs again by itself: the daemon's own reading, or, from a daemon older than it, whether a watch
578
- // is armed, which was all that one could say.
579
- export const awaitsWake = (agent: { readonly awaitingWake?: boolean | undefined; readonly watches?: readonly unknown[] | undefined }): boolean =>
580
- agent.awaitingWake ?? (agent.watches?.length ?? 0) > 0;
590
+ // Whether a conversation runs again by itself: the daemon's own reading, never guessed from its armed watches.
591
+ export const awaitsWake = (agent: { readonly awaitingWake?: boolean | undefined }): boolean => agent.awaitingWake === true;
581
592
  // One armed watch as a card carries it; derived from AgentSummarySchema.watches so the two shapes can't drift apart.
582
593
  export type AgentWatch = NonNullable<AgentSummary["watches"]>[number];
583
594
  // One background job as a card carries it.
@@ -28,8 +28,8 @@ import { ZoneSchema } from "../time/zone.js";
28
28
  // reads these.
29
29
  // turn.settled: fires once per isolated turn regardless of outcome, while the diff is still on screen
30
30
  // agent.landed: fires only once work reaches the main tree, including a later manual Land
31
- // deps.broken/deps.fixed: edges from the dependency verifier around a landed change, not standing states, so a chore
32
- // wakes on the transition, never on a tree that stays red
31
+ // deps.broken/deps.fixed: retired with the check that ran after every land (2026-09-27). Nothing emits them any more;
32
+ // they stay so an automation written for them still reads, and it never fires. Main's red CI is the `ci` listener's.
33
33
  export const WorkspaceEventKindSchema = z.enum(["turn.settled", "agent.landed", "deps.broken", "deps.fixed"]);
34
34
  export type WorkspaceEventKind = z.infer<typeof WorkspaceEventKindSchema>;
35
35
  // Payload of a workspace-triggered wake, delivered as JSON in $AUTOMATION_PAYLOAD and appended to the prompt.
@@ -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>;
package/src/schemas/ci.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // ci: pipeline runs on the workspace repos' github/gitlab remotes
2
2
  import { z } from "zod";
3
3
  import { AgentRunPickSchema } from "./agent.js";
4
+ import { RedDecisionSchema } from "./workspace/push-checks.js";
4
5
  // The daemon maps each workspace repo to its CI project via a connected github/gitlab capability, registers a webhook
5
6
  // to fire `ci` listener automations instantly, and serves the Pipelines rail from a webhook-freshened, poll-backfilled
6
7
  // cache. `host` names which provider API serves a repo; the listener provider is always `ci`, since a trigger narrows
@@ -39,6 +40,9 @@ export const PipelineRunSchema = z.object({
39
40
  .describe(
40
41
  "What set it off, in the forge's own word rather than flattened into a shared vocabulary, because the forge's word is the precise one.",
41
42
  ),
43
+ // github's workflow name: several workflows run on one push there, and one passing says nothing of another. Absent on
44
+ // gitlab, where a commit's pipeline is the project's one.
45
+ workflow: z.string().optional().describe("Which workflow it is a run of, where a push starts several. Absent where a commit has one pipeline."),
42
46
  branch: z.string().describe("Which branch."),
43
47
  sha: z.string().describe("Which commit."),
44
48
  status: PipelineStatusSchema.describe(
@@ -108,10 +112,31 @@ export type CiRepo = z.infer<typeof CiRepoSchema>;
108
112
  // Fallback poll interval when a repo's webhook couldn't register; lives here since both the poller and the automation
109
113
  // editor need it to tell the owner what a `hookWarning` actually costs (minutes' delay, not the feature).
110
114
  export const CI_POLL_INTERVAL_MS = 2 * 60_000;
115
+ // Main's CI while it is red, as the daemon's one fix agent on it sees it (ci/main-fixer.ts): from the first job that
116
+ // failed until a later run of every workflow that failed on it passes, whatever else failed along the way.
117
+ export const CiMainRedSchema = z.object({
118
+ repo: z.string().describe("Which workspace repository."),
119
+ branch: z.string().describe("Which main-line branch."),
120
+ since: z.number().describe("When its first job failed, in milliseconds."),
121
+ runId: z.number().describe("The newest run that failed on it."),
122
+ jobs: z.array(z.string()).describe("The jobs failing on it now, by name: the newest red run's failures, and any that failed since."),
123
+ fixer: z
124
+ .string()
125
+ .optional()
126
+ .describe("The one conversation working on it, which every failure goes to until the branch is green. Absent while nobody is on it."),
127
+ decision: RedDecisionSchema.optional().describe(
128
+ "The latest thing decided about it: `fix-up` while the fix agent has it, `spent` once it waits for you (its turns are used up, or it stopped without a fix), `reported` when repairs are off.",
129
+ ),
130
+ });
131
+ export type CiMainRed = z.infer<typeof CiMainRedSchema>;
111
132
  export const CiRunsResponseSchema = z.object({
112
133
  repos: z.array(CiRepoSchema).describe("Which workspace repositories are wired to a forge, and how each one's notifications are set up."),
113
134
  // Newest first, across all mapped repos.
114
135
  runs: z.array(PipelineRunSchema).describe("Runs across all of them, newest first."),
136
+ reds: z
137
+ .array(CiMainRedSchema)
138
+ .optional()
139
+ .describe("Every main-line branch red right now, with the fix agent on it. Absent from a daemon that keeps none."),
115
140
  });
116
141
  export type CiRunsResponse = z.infer<typeof CiRunsResponseSchema>;
117
142
  // Re-resolves repo → project + token on every call, so a stale card can't act on a mapping the workspace no longer has.