@intentic/sandbox-contract 1.322.0 → 1.323.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 (143) hide show
  1. package/dist/contracts/agent.contract.d.ts +61 -6
  2. package/dist/contracts/agent.contract.d.ts.map +1 -1
  3. package/dist/contracts/agent.contract.js +13 -3
  4. package/dist/contracts/agent.contract.js.map +1 -1
  5. package/dist/contracts/agents.contract.d.ts +40 -0
  6. package/dist/contracts/agents.contract.d.ts.map +1 -1
  7. package/dist/contracts/loops.contract.d.ts +2 -0
  8. package/dist/contracts/loops.contract.d.ts.map +1 -1
  9. package/dist/contracts/needs.contract.d.ts +4 -0
  10. package/dist/contracts/needs.contract.d.ts.map +1 -1
  11. package/dist/contracts/privacy.contract.d.ts +187 -0
  12. package/dist/contracts/privacy.contract.d.ts.map +1 -0
  13. package/dist/contracts/privacy.contract.js +76 -0
  14. package/dist/contracts/privacy.contract.js.map +1 -0
  15. package/dist/contracts/runner.contract.d.ts +29 -18
  16. package/dist/contracts/runner.contract.d.ts.map +1 -1
  17. package/dist/contracts/sessions.contract.d.ts +3 -0
  18. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  19. package/dist/contracts/system.contract.d.ts +36 -32
  20. package/dist/contracts/system.contract.d.ts.map +1 -1
  21. package/dist/contracts/usage.contract.d.ts +1 -0
  22. package/dist/contracts/usage.contract.d.ts.map +1 -1
  23. package/dist/contracts/workflows.contract.d.ts +3 -0
  24. package/dist/contracts/workflows.contract.d.ts.map +1 -1
  25. package/dist/events/agent-events.d.ts +14 -0
  26. package/dist/events/agent-events.d.ts.map +1 -1
  27. package/dist/events/agent-events.js +2 -0
  28. package/dist/events/agent-events.js.map +1 -1
  29. package/dist/events/requests.d.ts +3 -0
  30. package/dist/events/requests.d.ts.map +1 -1
  31. package/dist/events/requests.js +4 -0
  32. package/dist/events/requests.js.map +1 -1
  33. package/dist/events/resume.d.ts +1 -0
  34. package/dist/events/resume.d.ts.map +1 -1
  35. package/dist/events/resume.js +7 -1
  36. package/dist/events/resume.js.map +1 -1
  37. package/dist/events/sandbox-notice.d.ts +2 -0
  38. package/dist/events/sandbox-notice.d.ts.map +1 -1
  39. package/dist/events/sandbox-notice.js +1 -1
  40. package/dist/events/sandbox-notice.js.map +1 -1
  41. package/dist/events/system-events.d.ts +4 -0
  42. package/dist/events/system-events.d.ts.map +1 -1
  43. package/dist/events/transcript.d.ts +22 -0
  44. package/dist/events/transcript.d.ts.map +1 -1
  45. package/dist/events/transcript.js +42 -14
  46. package/dist/events/transcript.js.map +1 -1
  47. package/dist/front/generated/browser-wire.d.ts +4 -0
  48. package/dist/front/generated/browser-wire.d.ts.map +1 -1
  49. package/dist/index.d.ts +338 -38
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +5 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/models/agent-runtimes.d.ts +1 -0
  54. package/dist/models/agent-runtimes.d.ts.map +1 -1
  55. package/dist/models/agent-runtimes.js +6 -0
  56. package/dist/models/agent-runtimes.js.map +1 -1
  57. package/dist/policy/request-status.d.ts.map +1 -1
  58. package/dist/policy/request-status.js +16 -7
  59. package/dist/policy/request-status.js.map +1 -1
  60. package/dist/policy/turned-away.d.ts.map +1 -1
  61. package/dist/policy/turned-away.js +1 -0
  62. package/dist/policy/turned-away.js.map +1 -1
  63. package/dist/protocol/raw-routes.d.ts +12 -0
  64. package/dist/protocol/raw-routes.d.ts.map +1 -1
  65. package/dist/protocol/raw-routes.js +3 -0
  66. package/dist/protocol/raw-routes.js.map +1 -1
  67. package/dist/schemas/agent.d.ts +97 -0
  68. package/dist/schemas/agent.d.ts.map +1 -1
  69. package/dist/schemas/agent.js +20 -2
  70. package/dist/schemas/agent.js.map +1 -1
  71. package/dist/schemas/agents.d.ts +6 -0
  72. package/dist/schemas/agents.d.ts.map +1 -1
  73. package/dist/schemas/agents.js +1 -1
  74. package/dist/schemas/agents.js.map +1 -1
  75. package/dist/schemas/automations.d.ts +2 -0
  76. package/dist/schemas/automations.d.ts.map +1 -1
  77. package/dist/schemas/git/git.d.ts.map +1 -1
  78. package/dist/schemas/git/git.js.map +1 -1
  79. package/dist/schemas/loops.d.ts +3 -0
  80. package/dist/schemas/loops.d.ts.map +1 -1
  81. package/dist/schemas/loops.js +1 -1
  82. package/dist/schemas/loops.js.map +1 -1
  83. package/dist/schemas/needs.d.ts +6 -0
  84. package/dist/schemas/needs.d.ts.map +1 -1
  85. package/dist/schemas/needs.js +13 -3
  86. package/dist/schemas/needs.js.map +1 -1
  87. package/dist/schemas/platform-link.d.ts +50 -0
  88. package/dist/schemas/platform-link.d.ts.map +1 -0
  89. package/dist/schemas/platform-link.js +19 -0
  90. package/dist/schemas/platform-link.js.map +1 -0
  91. package/dist/schemas/privacy.d.ts +277 -0
  92. package/dist/schemas/privacy.d.ts.map +1 -0
  93. package/dist/schemas/privacy.js +127 -0
  94. package/dist/schemas/privacy.js.map +1 -0
  95. package/dist/schemas/providers/plan-limits.d.ts +8 -0
  96. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  97. package/dist/schemas/providers/plan-limits.js +25 -4
  98. package/dist/schemas/providers/plan-limits.js.map +1 -1
  99. package/dist/schemas/providers/usage.d.ts +3 -0
  100. package/dist/schemas/providers/usage.d.ts.map +1 -1
  101. package/dist/schemas/providers/usage.js +4 -2
  102. package/dist/schemas/providers/usage.js.map +1 -1
  103. package/dist/schemas/push.d.ts +1 -0
  104. package/dist/schemas/push.d.ts.map +1 -1
  105. package/dist/schemas/push.js +4 -0
  106. package/dist/schemas/push.js.map +1 -1
  107. package/dist/schemas/workflows.d.ts +5 -0
  108. package/dist/schemas/workflows.d.ts.map +1 -1
  109. package/dist/state/workspace-state.d.ts +17 -1
  110. package/dist/state/workspace-state.d.ts.map +1 -1
  111. package/dist/state/workspace-state.js +20 -1
  112. package/dist/state/workspace-state.js.map +1 -1
  113. package/package.json +5 -5
  114. package/src/contracts/agent.contract.ts +16 -3
  115. package/src/contracts/privacy.contract.ts +95 -0
  116. package/src/events/agent-events.ts +5 -0
  117. package/src/events/requests.ts +6 -0
  118. package/src/events/resume.test.ts +13 -0
  119. package/src/events/resume.ts +11 -2
  120. package/src/events/sandbox-notice.ts +1 -1
  121. package/src/events/transcript.ts +79 -20
  122. package/src/front/generated/browser-wire.json +27 -0
  123. package/src/front/generated/browser-wire.ts +1 -1
  124. package/src/index.ts +5 -0
  125. package/src/models/agent-catalog.test.ts +1 -0
  126. package/src/models/agent-runtimes.ts +17 -0
  127. package/src/models/capability-ledger.test.ts +3 -0
  128. package/src/policy/request-status.ts +19 -9
  129. package/src/policy/turned-away.ts +2 -0
  130. package/src/protocol/raw-routes.ts +9 -0
  131. package/src/schemas/agent.ts +36 -4
  132. package/src/schemas/agents.ts +1 -1
  133. package/src/schemas/automations.ts +4 -4
  134. package/src/schemas/git/git.ts +1 -0
  135. package/src/schemas/loops.ts +2 -1
  136. package/src/schemas/needs.ts +18 -3
  137. package/src/schemas/platform-link.ts +42 -0
  138. package/src/schemas/privacy.ts +204 -0
  139. package/src/schemas/providers/plan-limits.ts +38 -5
  140. package/src/schemas/providers/usage.ts +10 -2
  141. package/src/schemas/push.ts +7 -0
  142. package/src/state/workspace-state.test.ts +8 -2
  143. package/src/state/workspace-state.ts +32 -2
@@ -51,6 +51,11 @@ export interface AgentCapabilities {
51
51
  readonly rulebook: "hooks" | "approval" | "refuse-only" | "none";
52
52
  // Whether a credential is masked; "none" is structural, only the Claude Code loop can rewrite a model's read.
53
53
  readonly secrets: "masked" | "none";
54
+ // Whether the privacy shield can stand between this loop and its provider: `gateway` when the daemon names the base
55
+ // URL the loop sends its model requests to (so the gateway reads and masks every byte of them), `none` when the loop
56
+ // talks to its provider on a wire or a configuration the daemon does not own. A `none` loop on an untrusted provider
57
+ // is refused while the shield is on.
58
+ readonly privacy: "gateway" | "none";
54
59
  }
55
60
 
56
61
  // The Claude Code Agent SDK loop, the ceiling every other runtime is measured against: the only one that owns the whole
@@ -75,6 +80,8 @@ export const CLAUDE_CODE: AgentCapabilities = {
75
80
  // The only runtime with its own pre-execution hook, so the only one where a hold can park instead of refusing.
76
81
  rulebook: "hooks",
77
82
  secrets: "masked",
83
+ // Every request goes to ANTHROPIC_BASE_URL, which the daemon sets per turn.
84
+ privacy: "gateway",
78
85
  };
79
86
 
80
87
  // Codex app-server: item-level events, the turn's remote MCP servers over http, and four seams (steer, a question
@@ -100,6 +107,8 @@ export const CODEX: AgentCapabilities = {
100
107
  // Asked only when the owner has written command rules; an unconfigured workspace pays nothing for it.
101
108
  rulebook: "approval",
102
109
  secrets: "none",
110
+ // Its model provider's base_url is the daemon's, written into the per-thread config.
111
+ privacy: "gateway",
103
112
  };
104
113
 
105
114
  // OpenCode (the Grok runtime): its own agentic loop and tools, with the turn's remote MCP servers mounted on the shared
@@ -126,6 +135,8 @@ export const OPENCODE: AgentCapabilities = {
126
135
  // hold parked on a card while the turn's inactivity watchdog is held.
127
136
  rulebook: "approval",
128
137
  secrets: "none",
138
+ // The shared server's provider baseURLs are written by the daemon at spawn.
139
+ privacy: "gateway",
129
140
  };
130
141
 
131
142
  // The same OpenCode loop, serving Gemini instead of xAI, identical abilities. The Claude Code loop announces itself in
@@ -157,6 +168,8 @@ export const ACP: AgentCapabilities = {
157
168
  // In the protocol floor, so every agent has it; which calls it asks about is entirely its own choice.
158
169
  rulebook: "approval",
159
170
  secrets: "none",
171
+ // The agent picks its own provider and endpoint from its own login and environment.
172
+ privacy: "none",
160
173
  };
161
174
 
162
175
  // Pi driven over its RPC mode: above the ACP floor and below the Claude Code ceiling. Real mid-turn steering, an effort
@@ -181,6 +194,8 @@ export const PI: AgentCapabilities = {
181
194
  // The one runtime with no seam at all: bash runs in-process, and no rule the owner writes can apply.
182
195
  rulebook: "none",
183
196
  secrets: "none",
197
+ // Pi calls its provider from its own login; the daemon names no endpoint.
198
+ privacy: "none",
184
199
  };
185
200
 
186
201
  // Cursor's own runtime, driven through `@cursor/sdk` from this daemon's process; an embedding surface, so most seams
@@ -219,4 +234,6 @@ export const CURSOR: AgentCapabilities = {
219
234
  rulebook: "hooks",
220
235
  // none, structurally: its hooks only allow/deny or are discarded, nothing here to substitute a reference into.
221
236
  secrets: "none",
237
+ // Cursor's own RPC to its own servers, which then reach the model: redirectable, not readable.
238
+ privacy: "none",
222
239
  };
@@ -34,6 +34,9 @@ const LEDGER: Record<keyof AgentCapabilities, Backing> = {
34
34
  rulebook: "enforced",
35
35
  // agent/run/turn/cache-keepwarm.ts files nothing to keep warm for a turn whose runtime lacks it.
36
36
  warm: "enforced",
37
+ // agent/run/decide/turn-decision.ts refuses a "none" runtime on an untrusted provider while the privacy shield is
38
+ // on; "gateway" runtimes get the daemon's gateway as their base URL (privacy/gateway).
39
+ privacy: "enforced",
37
40
 
38
41
  // Descriptive: true of the runtime but nothing consults these; wiring one up moves it to enforced.
39
42
  questions: "descriptive",
@@ -1,4 +1,4 @@
1
- import { REQUEST_FIELDS, type RequestField, type TranscriptRequests, type TranscriptRow } from "../events/transcript.js";
1
+ import { type PermissionStatus, REQUEST_FIELDS, type RequestField, type TranscriptRequests, type TranscriptRow } from "../events/transcript.js";
2
2
  import type { AgentReply } from "../schemas/providers/plan-limits.js";
3
3
  import { startedChildCard } from "../events/child-run.js";
4
4
 
@@ -7,6 +7,21 @@ import { startedChildCard } from "../events/child-run.js";
7
7
  // of the wrong kind cannot settle a card of another, since requestId is what matches them.
8
8
  type Cards = { -readonly [K in RequestField]?: TranscriptRow[K] };
9
9
 
10
+ // A permission card's ending, from the reply that released it: each kind of yes keeps its own word.
11
+ const permissionStatusOf = (reply: AgentReply | undefined): PermissionStatus => {
12
+ if (reply?.kind !== "permission") {
13
+ return "cancelled";
14
+ }
15
+ switch (reply.decision) {
16
+ case "deny":
17
+ return "denied";
18
+ case "once":
19
+ return "allowed";
20
+ default:
21
+ return reply.decision;
22
+ }
23
+ };
24
+
10
25
  export const settledRequests = (cards: TranscriptRequests, reply: AgentReply | undefined): TranscriptRequests => {
11
26
  const out: Cards = {};
12
27
  const { plan, question, permission, browserHelp, terminalHelp, capabilityOffer, paymentOffer, credentialOffer } = cards;
@@ -15,23 +30,18 @@ export const settledRequests = (cards: TranscriptRequests, reply: AgentReply | u
15
30
  }
16
31
  if (question !== undefined) {
17
32
  const answers = reply?.kind === "question" ? reply.answers : undefined;
33
+ const attachments = reply?.kind === "question" ? reply.attachments : undefined;
18
34
  out.question = {
19
35
  ...question,
20
36
  status: reply?.kind === "question" && reply.cancelled !== true ? "answered" : "cancelled",
21
37
  ...(answers === undefined ? {} : { answers }),
38
+ ...(attachments === undefined ? {} : { attachments }),
22
39
  };
23
40
  }
24
41
  if (permission !== undefined) {
25
42
  out.permission = {
26
43
  ...startedChildCard(permission, reply),
27
- status:
28
- reply?.kind !== "permission"
29
- ? "cancelled"
30
- : reply.decision === "deny"
31
- ? "denied"
32
- : reply.decision === "always"
33
- ? "always"
34
- : "allowed",
44
+ status: permissionStatusOf(reply),
35
45
  };
36
46
  }
37
47
  if (browserHelp !== undefined) {
@@ -11,6 +11,8 @@ const TURNED_AWAY: ReadonlySet<string> = new Set([
11
11
  "unknown-command",
12
12
  "context-window-too-small",
13
13
  "model-helper-only",
14
+ // The privacy shield refused an untrusted provider on a runtime it cannot stand in front of.
15
+ "privacy-unshielded",
14
16
  "sandbox-memory-low",
15
17
  "trial-unavailable",
16
18
  "trial-model-unavailable",
@@ -8,6 +8,9 @@ import { runnerTranslatorPath } from "./runner-protocol.js";
8
8
  export const RAW_ROUTES = {
9
9
  // The "is a daemon there" probe every flow uses; it names the sandbox and says whether it is still converging.
10
10
  "GET /health": { auth: "door", beforeBoot: true },
11
+ // A person opening this sandbox's own address in a browser: sent to the app's /open on it, the address being what
12
+ // they may still hold when the platform has lost the rest. Anything else gets the same 401 as before it existed.
13
+ "GET /": { auth: "door", beforeBoot: true },
11
14
  "GET /diff/raw": { lane: "bulk" },
12
15
  // Arming the dictation model is a read any tier makes; dictating is writing a message, the collaborator's grant.
13
16
  "GET /speech/status": { guest: true },
@@ -59,6 +62,8 @@ export const RAW_ROUTES = {
59
62
  "POST /members": { control: "never" },
60
63
  "DELETE /members": { control: "never" },
61
64
  "DELETE /members/self": { floor: "viewer", guest: true, control: "never" },
65
+ // The owner's Reconnect: register with the platform now, having it adopt the sandbox first when it holds no record.
66
+ "POST /platform/relink": { control: "never" },
62
67
  "GET /environment": {},
63
68
  "GET /environment/contents": {},
64
69
  "POST /environment/approve": {},
@@ -153,6 +158,10 @@ export const RAW_ROUTES = {
153
158
  "POST /system/runners/credentials": { auth: "door", control: "never" },
154
159
  "POST /system/runners/credentials/refresh": { auth: "door", control: "never" },
155
160
  [`ALL ${runnerTranslatorPath}/*` as const]: { auth: "door", control: "never" },
161
+ // The privacy shield's gateway: every shielded runtime's model requests, on the signed session its base URL carries
162
+ // (privacy/gateway/session-token.ts), which names the provider and the only upstream it may forward to. Held open
163
+ // while a model streams its answer.
164
+ "ALL /privacy/gateway/{session}/*": { auth: "door", control: "never", stream: true },
156
165
  "POST /system/control/tokens": { control: "never" },
157
166
  "GET /system/control/tokens": { control: "never" },
158
167
  "DELETE /system/control/tokens/{id}": { control: "never" },
@@ -304,15 +304,28 @@ const AgentTurnFieldsSchema = z.object({
304
304
  editorContext: EditorContextSchema.optional().describe(
305
305
  'What the user has open in their editor, folded into the prompt so that pointing words like "this" resolve.',
306
306
  ),
307
- // The composer's scheduled send: a person who knows the account is spent books the message for its reopen instead of
308
- // sending it into a refusal. Held in the conversation's queue, never started early; the queue's `until` is this.
307
+ // The composer's scheduled send: a time the person chose, or a spent account's reopen they would rather wait for than
308
+ // send into a refusal. Held in the conversation's queue, never started early; the queue's `until` is this.
309
309
  sendAt: z
310
310
  .number()
311
311
  .int()
312
312
  .positive()
313
313
  .optional()
314
314
  .describe(
315
- "Hold this message until then (epoch milliseconds) instead of starting a turn now, for an allowance you know is spent. It waits in the conversation's queue, where it can be sent early, reworded or removed, and goes by itself at that instant. Ignored when already past, or when a turn is running.",
315
+ "Hold this message until then (epoch milliseconds) instead of starting a turn now: a time you chose, or the reopen of an allowance you know is spent. It waits in the conversation's queue, where it can be sent early, reworded, rescheduled or removed, and goes by itself at that instant, even for a conversation this message opens or one whose turn is running now. Ignored when already past; at most a month ahead.",
316
+ ),
317
+ // The other scheduled send: work that builds on another agent's, held until that agent's work is in the workspace.
318
+ // Held in the queue like `sendAt`; the queue's `after` is this. Never both: a hold waits on exactly one thing.
319
+ sendAfter: ConversationIdSchema.optional().describe(
320
+ "Hold this message until the conversation named here has finished and all of its work has landed in the workspace, instead of starting a turn now: for work that builds on another agent's. It waits in this conversation's queue, where it can be sent early, reworded, rescheduled or removed. Sent at once when that conversation has nothing running and nothing left to land.",
321
+ ),
322
+ // Decided like `isolated`: the opening turn's answer, latched on the entry it opens. A maintainer's alone, since the
323
+ // standing answer it sets is what `agents.autoLand` sets, and that route is a maintainer's.
324
+ conversationAutoLand: z
325
+ .boolean()
326
+ .optional()
327
+ .describe(
328
+ "Whether this conversation's finished work merges into the workspace by itself from now on: its own answer to the sandbox-wide setting, the one `agents.autoLand` changes later. Read only from the message that opens the conversation, and only from a maintainer. Unlike `autoLand`, it holds for every later turn.",
316
329
  ),
317
330
  });
318
331
  export const AgentTurnSchema = AgentTurnFieldsSchema
@@ -335,8 +348,24 @@ export const AgentTurnSchema = AgentTurnFieldsSchema
335
348
  // "then" needs a checkout of its own; rolling back the shared tree under everyone else is what it must never mean.
336
349
  .refine((turn) => turn.forkOf?.files !== "then" || turn.isolated === true, {
337
350
  message: 'forkOf.files "then" requires isolated',
351
+ })
352
+ .refine((turn) => turn.sendAt === undefined || turn.sendAfter === undefined, {
353
+ message: "sendAt and sendAfter are two different holds: name one",
354
+ })
355
+ .refine((turn) => turn.sendAfter === undefined || turn.sendAfter !== turn.conversationId, {
356
+ message: "a conversation cannot wait for its own work to land",
338
357
  });
339
358
  export type AgentTurn = z.infer<typeof AgentTurnSchema>;
359
+ // What `agent.run` takes: a turn, plus the one field only a request carries and no stored turn keeps. A daemon from before
360
+ // `continues` reads past it and takes `prompt` as the words, which is why a client still sends some.
361
+ export const AgentRunSchema = AgentTurnSchema.safeExtend({
362
+ continues: z
363
+ .literal(true)
364
+ .optional()
365
+ .describe(
366
+ "Carry the conversation on from where its last turn stopped instead of saying anything: nothing of yours is added to the conversation, and `prompt` is ignored. A turn the sandbox still holds runs again as it was; otherwise the agent is told to continue in its own session. Refused while a turn is running.",
367
+ ),
368
+ });
340
369
  // What a turn naming no provider or loop runs on, stated once. Applied where a request comes in (the daemon's turn port),
341
370
  // so nothing downstream defaults them again: the engine takes a turn with both named (withRuntimeDefaults).
342
371
  /** A turn with its provider and loop named, as the engine takes it past the door. */
@@ -491,12 +520,15 @@ export const ConversationQueueSchema = z.object({
491
520
  items: z.array(QueuedMessageSchema).describe("What waits, in the order it goes out."),
492
521
  revision: z.number().int().nonnegative().describe("Moves with every change to the queue, so of two copies the higher is the newer."),
493
522
  paused: QueuePauseSchema.optional().describe(
494
- "Why nothing goes out by itself: somebody stopped the turn, the turn these messages started was refused before it ran, or they were scheduled for later. Resuming lets them go, and so does sending another message unless they are scheduled.",
523
+ "Why nothing goes out by itself: somebody stopped the turn, the turn these messages started was refused before it ran, or they were scheduled for a time or for after another conversation's work lands. Resuming lets them go, and so does sending another message.",
495
524
  ),
496
525
  until: z
497
526
  .number()
498
527
  .optional()
499
528
  .describe("When scheduled messages go out by themselves, in milliseconds. Only on a queue paused as `scheduled`."),
529
+ after: ConversationIdSchema.optional().describe(
530
+ "The conversation whose finished work must land before scheduled messages go out by themselves. Only on a queue paused as `scheduled`, in place of `until`.",
531
+ ),
500
532
  });
501
533
  export type ConversationQueue = z.infer<typeof ConversationQueueSchema>;
502
534
  // What resuming a queue did: started a turn with what waited, when nothing else ran.
@@ -357,7 +357,7 @@ export const AgentSummarySchema = z.object({
357
357
  // Root repo's short base sha; per-repo bases stay daemon-internal.
358
358
  base: z.string().optional().describe("The commit its private copy started from, shortened."),
359
359
  costUsd: z.number().optional().describe("What it has cost so far, in dollars. A subagent's spend is its own and is not folded in here."),
360
- inputTokens: z.number().optional().describe("Tokens sent."),
360
+ inputTokens: z.number().optional().describe("Uncached input tokens, excluding cache reads and cache writes."),
361
361
  outputTokens: z.number().optional().describe("Tokens received."),
362
362
  contextTokens: z.number().optional().describe("How much of the window the conversation currently fills."),
363
363
  contextWindow: z.number().optional().describe("How large that window is."),
@@ -9,10 +9,10 @@ import { ZoneSchema } from "../time/zone.js";
9
9
  // (non-zero exit skips the wake), then runs one turn with the prompt. The manifest is user config; run history is
10
10
  // daemon-recorded.
11
11
 
12
- // schedule: fires on its cron
13
- // once: fires a single time at `at`, then retires itself (the daemon switches it off as it fires). Unlike a cron, a
14
- // fire missed while the sandbox was down still lands, late, on the next poll: a recurring wake that skips a beat has
15
- // another one coming, a one-time wake has nothing behind it
12
+ // schedule: fires on its cron. Moments missed while the sandbox was down fire once, late, as the daemon starts, however
13
+ // many there were: a recurring wake is owed its work, not a backlog
14
+ // once: fires a single time at `at`, then retires itself (the daemon switches it off as it fires). A fire missed while
15
+ // the sandbox was down still lands, late, on the next poll
16
16
  // event: fires when an external system POSTs /automations/{id}/fire; its auth token lives outside the manifest
17
17
  // (.intentic/secrets/doors.json), never in this versioned, widely-readable file
18
18
  // listener: fires from a realtime source's own connection (an extension's gateway), no cron, no token, never reachable
@@ -379,6 +379,7 @@ export type WorkspaceModules = z.infer<typeof WorkspaceModulesSchema>;
379
379
  // One file an agent touched that still differs from main, plus whether main's working tree already holds it:
380
380
  // in main's history: accepted, no longer a difference — no row (see `absorbed`).
381
381
  // in main's tree, uncommitted: `landed: true`.
382
+ // taken by the last land and edited in main since (committed or not): `landed: true`, since no land has anything to apply.
382
383
  // neither (never landed, or landed then discarded): `landed: false` — what "Land now" applies to.
383
384
  // A landed row stays until committed; that is the only act that retires it.
384
385
  export const AgentChangeSchema = GitChangeSchema.extend({
@@ -186,7 +186,8 @@ export type LoopIteration = z.infer<typeof LoopIterationSchema>;
186
186
  // overspent: maxSpendUsd reached.
187
187
  // stopped: the user pressed Stop.
188
188
  // error: the loop itself failed, not a turn inside it (see `LoopIteration.outcome`).
189
- export const LoopStateSchema = z.enum(["running", "done", "exhausted", "stalled", "overspent", "stopped", "error"]);
189
+ // unpriced: a spend ceiling was set but the runtime does not report cost, so the ceiling cannot be enforced.
190
+ export const LoopStateSchema = z.enum(["running", "done", "exhausted", "stalled", "overspent", "stopped", "error", "unpriced"]);
190
191
  export type LoopState = z.infer<typeof LoopStateSchema>;
191
192
  export const LoopRecordSchema = LoopSchema.extend({
192
193
  state: LoopStateSchema.describe(
@@ -262,6 +262,17 @@ export const ConversationGrantSchema = z.object({
262
262
  .boolean()
263
263
  .default(false)
264
264
  .describe("Whether its own dependency installs run without asking, where the owner's setting would otherwise ask first."),
265
+ // Answered on a host guard's card, "Allow <secret> anywhere in this conversation", read live by the host guard.
266
+ secrets: z
267
+ .array(z.string())
268
+ .default([])
269
+ .describe("Secrets whose host guard it may send past without asking, by registry name."),
270
+ // Answered on any permission card's Allow menu, read live by the card registry: every later request a person could
271
+ // have allowed once settles as allowed. A request that always asks still asks.
272
+ everything: z
273
+ .boolean()
274
+ .default(false)
275
+ .describe("Whether every request an allow-once could settle is allowed without asking, until somebody takes it back."),
265
276
  updatedAt: z.number().describe("When it last changed, in milliseconds."),
266
277
  by: z.string().optional().describe("Who last allowed something here."),
267
278
  });
@@ -277,6 +288,8 @@ export const StandingGrantsSchema = z.object({
277
288
  folders: z.array(z.string()).describe("Workspace folders its file tools may touch beyond its fence."),
278
289
  shelves: z.array(GrantShelfSchema).describe("Shelves of tools opened for it."),
279
290
  installs: z.boolean().default(false).describe("Whether its own dependency installs run without asking."),
291
+ secrets: z.array(z.string()).default([]).describe("Secrets it may send past their host guard without asking."),
292
+ everything: z.boolean().default(false).describe("Whether every request an allow-once could settle is allowed without asking."),
280
293
  by: z.string().optional().describe("Who last allowed one of those."),
281
294
  updatedAt: z.number().optional().describe("When one of those last changed, in milliseconds."),
282
295
  releases: z
@@ -290,9 +303,11 @@ export type StandingGrants = z.infer<typeof StandingGrantsSchema>;
290
303
  export const GrantRevokeSchema = z.object({
291
304
  conversationId: z.string(),
292
305
  kind: z
293
- .enum(["capability", "folder", "shelf", "release", "install"])
294
- .describe("Which kind of yes: a grant past the persona or area, a credential's release, or letting its installs run unasked."),
295
- what: z.string().describe("The capability id, folder, shelf or released credential it named; empty for installs."),
306
+ .enum(["capability", "folder", "shelf", "release", "install", "secret", "everything"])
307
+ .describe(
308
+ "Which kind of yes: a grant past the persona or area, a credential's release, letting its installs run unasked, a secret sent past its host guard, or allowing everything.",
309
+ ),
310
+ what: z.string().describe("The capability id, folder, shelf, released credential or secret it named; empty for installs and everything."),
296
311
  });
297
312
  export type GrantRevoke = z.infer<typeof GrantRevokeSchema>;
298
313
 
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+
3
+ // THE DAEMON'S LINK WITH THE PLATFORM, as /health reports it (`announce`) and as the owner's Reconnect renews it (POST
4
+ // /platform/relink). The one link nothing outside the container can probe: whether this daemon's registration was
5
+ // taken, and, when it was refused, whether the platform has no record of the sandbox (`unknown`, which a platform that
6
+ // forgot it says too) or deleted it (`deleted`). `ic sandbox doctor`, the setup wizard and the editor's recovery screen
7
+ // read it.
8
+ export const AnnounceStateSchema = z.object({
9
+ // off: nothing to register with; pending: the first attempt is in flight; registered: the platform took it;
10
+ // rejected: it answered no; unreachable: it could not be reached.
11
+ state: z.enum(["off", "pending", "registered", "rejected", "unreachable"]),
12
+ // Why, for the failing states, already in the owner's terms.
13
+ detail: z.string().optional(),
14
+ // On a failing state: false only once the platform said the sandbox was deleted; every other failure is retried.
15
+ retrying: z.boolean().optional(),
16
+ // Which no: `unknown` the platform has no record of this sandbox, and its owner's Reconnect can have it adopted;
17
+ // `deleted` it holds a deletion record, which ends the retrying.
18
+ reason: z.enum(["unknown", "deleted"]).optional(),
19
+ // The identity of the database that took the registration (the platform's GET /api/identity), when it said.
20
+ identity: z.string().optional(),
21
+ // When this state was last confirmed, ms since epoch.
22
+ at: z.number().optional(),
23
+ });
24
+ export type AnnounceState = z.infer<typeof AnnounceStateSchema>;
25
+
26
+ // The owner's Reconnect: registers now, and with a ticket (the platform's adoption ticket, carried opaque), first asks a
27
+ // platform that has no record of the sandbox to make one (its POST /sandbox/adopt). `name` and `image` are what the
28
+ // owner's browser remembers of it, since only the platform held them.
29
+ export const RelinkRequestSchema = z.object({
30
+ ticket: z.string().min(1).optional(),
31
+ name: z.string().max(60).optional(),
32
+ image: z.string().max(150_000).optional(),
33
+ });
34
+ export type RelinkRequest = z.infer<typeof RelinkRequestSchema>;
35
+
36
+ export const RelinkAnswerSchema = z.object({
37
+ announce: AnnounceStateSchema,
38
+ // What the platform said to the adoption, when one was asked for and needed: its HTTP status (0 when it could not
39
+ // be reached) and its reason, in the owner's terms.
40
+ adoption: z.object({ status: z.number(), detail: z.string() }).optional(),
41
+ });
42
+ export type RelinkAnswer = z.infer<typeof RelinkAnswerSchema>;
@@ -0,0 +1,204 @@
1
+ // The privacy shield: personal data (names, national ids, document numbers, accounts…) kept away from model providers
2
+ // the owner does not trust. The daemon puts a gateway between every runtime it can point at one and the provider, which
3
+ // replaces what it finds with stable tokens on the way out and puts the real values back on the way in, so the agent
4
+ // works on real data while an untrusted provider only ever reads tokens. Off unless the owner turns it on.
5
+
6
+ import { z } from "zod";
7
+
8
+ // What the shield looks for, one switch each. `known` values (a dataset the owner taught it) carry the class they were
9
+ // taught under, so there is no class of their own.
10
+ export const PERSONAL_DATA_CLASSES = [
11
+ "person-name",
12
+ "national-id",
13
+ "tax-id",
14
+ "identity-document",
15
+ "bank-account",
16
+ "payment-card",
17
+ "email",
18
+ "phone",
19
+ "address",
20
+ ] as const;
21
+ export const PersonalDataClassSchema = z.enum(PERSONAL_DATA_CLASSES);
22
+ export type PersonalDataClass = z.infer<typeof PersonalDataClassSchema>;
23
+
24
+ // off: nothing is routed or read. watch: every shieldable turn goes through the gateway, which reads and records what it
25
+ // would have masked and sends everything unchanged. on: untrusted providers get tokens, and a turn that cannot be
26
+ // shielded (a runtime the gateway cannot sit in front of) is refused unless its provider is trusted.
27
+ export const PrivacyShieldModeSchema = z.enum(["off", "watch", "on"]);
28
+ export type PrivacyShieldMode = z.infer<typeof PrivacyShieldModeSchema>;
29
+
30
+ // What an image bound for an untrusted provider becomes. mask: it is sent, with every stretch of its text the shield
31
+ // reads on this machine (OCR) and finds to be personal data painted over with that value's token; one that cannot be
32
+ // read is held back with a note, never sent unchecked. allow: sent as it is. (Before 2026-10, `withhold` sent a note
33
+ // in place of every image and `read` sent its masked text instead of the picture; both read as `mask` now.)
34
+ export const PrivacyImagesSchema = z.enum(["mask", "allow"]);
35
+ export type PrivacyImages = z.infer<typeof PrivacyImagesSchema>;
36
+
37
+ // How names are found. dictionary: the first-name and surname lists, with titles and inflection. model: those plus a
38
+ // local named-entity model, where one is installed; without it, the dictionary alone.
39
+ export const PrivacyNamesSchema = z.enum(["dictionary", "model"]);
40
+ export type PrivacyNames = z.infer<typeof PrivacyNamesSchema>;
41
+
42
+ export const PRIVACY_TRUSTED_MAX = 200;
43
+ export const PRIVACY_ALLOW_MAX = 1000;
44
+ export const PRIVACY_CONVERSATIONS_MAX = 200;
45
+
46
+ // One conversation the owner let an untrusted provider read as it is, granted from that conversation's own composer:
47
+ // narrower than the trusted list, and the way on for a provider whose runtime the gateway cannot sit in front of
48
+ // (Cursor's own wire, an ACP agent, Pi), which the shield otherwise refuses outright.
49
+ export const PrivacyConversationTrustSchema = z.object({
50
+ conversationId: z.string().min(1).max(200),
51
+ provider: z.string().min(1).max(200).describe("Provider id, as the trusted list names it."),
52
+ });
53
+ export type PrivacyConversationTrust = z.infer<typeof PrivacyConversationTrustSchema>;
54
+
55
+ export const PrivacyShieldPolicySchema = z.object({
56
+ mode: PrivacyShieldModeSchema.default("off").describe("Whether the shield is off, only watching, or masking."),
57
+ trusted: z
58
+ .array(z.string().min(1).max(200))
59
+ .max(PRIVACY_TRUSTED_MAX)
60
+ .default([])
61
+ .describe(
62
+ "Providers that may read personal data as it is, by provider id (`claude`, `codex`, `endpoint/<id>`). A local model is always trusted.",
63
+ ),
64
+ classes: z
65
+ .array(PersonalDataClassSchema)
66
+ .default([...PERSONAL_DATA_CLASSES])
67
+ .describe("Which kinds of personal data are looked for."),
68
+ images: PrivacyImagesSchema.default("mask").describe("What an image bound for an untrusted provider becomes."),
69
+ names: PrivacyNamesSchema.default("dictionary").describe("How names are found."),
70
+ allow: z
71
+ .array(z.string().min(1).max(200))
72
+ .max(PRIVACY_ALLOW_MAX)
73
+ .default([])
74
+ .describe("Values never masked: your own company, a public figure, a word the detector keeps mistaking for a name."),
75
+ conversations: z
76
+ .array(PrivacyConversationTrustSchema)
77
+ .max(PRIVACY_CONVERSATIONS_MAX)
78
+ .default([])
79
+ .describe("Providers that may read one conversation's personal data as it is, each granted from that conversation; oldest first."),
80
+ });
81
+ export type PrivacyShieldPolicy = z.infer<typeof PrivacyShieldPolicySchema>;
82
+
83
+ // Whether the owner let this provider read this conversation as it is. A turn outside any conversation (a helper job)
84
+ // never is.
85
+ export const trustedInConversation = (
86
+ policy: Pick<PrivacyShieldPolicy, "conversations">,
87
+ provider: string,
88
+ conversationId: string | undefined,
89
+ ): boolean => conversationId !== undefined && policy.conversations.some((entry) => entry.conversationId === conversationId && entry.provider === provider);
90
+
91
+ // The policy with that grant made or taken back. A full list drops its oldest grant to make room, since the newest is
92
+ // the one somebody is waiting on.
93
+ export const withConversationTrust = (policy: PrivacyShieldPolicy, conversationId: string, provider: string, on: boolean): PrivacyShieldPolicy => {
94
+ const others = policy.conversations.filter((entry) => !(entry.conversationId === conversationId && entry.provider === provider));
95
+ return { ...policy, conversations: on ? [...others, { conversationId, provider }].slice(-PRIVACY_CONVERSATIONS_MAX) : others };
96
+ };
97
+
98
+ // The policy a sandbox that never wrote one runs on.
99
+ export const DEFAULT_PRIVACY_SHIELD: PrivacyShieldPolicy = PrivacyShieldPolicySchema.parse({});
100
+
101
+ // How a provider stands against the gateway, for the trusted list: whether its traffic can be shielded at all, and
102
+ // whether it never leaves this machine.
103
+ export const PrivacyProviderSchema = z.object({
104
+ id: z.string().describe("Provider id, as the trusted list names it."),
105
+ label: z.string(),
106
+ shieldable: z.boolean().describe("Its runtime can be put behind the gateway; one that cannot is refused while the shield is on, unless trusted."),
107
+ local: z.boolean().describe("It runs on this machine, so it is trusted whatever the list says."),
108
+ });
109
+ export type PrivacyProvider = z.infer<typeof PrivacyProviderSchema>;
110
+
111
+ export const PrivacyShieldStatusSchema = z.object({
112
+ policy: PrivacyShieldPolicySchema,
113
+ known: z.number().int().describe("Values taught from your datasets, matched exactly wherever they appear."),
114
+ tokens: z.number().int().describe("Values the shield has given a token so far."),
115
+ readers: z.object({
116
+ ocr: z.boolean().describe("The local text reader (PaddleOCR) that finds personal data in images is installed."),
117
+ model: z.boolean().describe("A local named-entity model for names is installed."),
118
+ }),
119
+ providers: z.array(PrivacyProviderSchema),
120
+ });
121
+ export type PrivacyShieldStatus = z.infer<typeof PrivacyShieldStatusSchema>;
122
+
123
+ // One request the gateway handled, as the log keeps it: counts and kinds, never a value.
124
+ export const PrivacyLedgerActionSchema = z.enum(["masked", "watched", "passed", "refused"]);
125
+ export type PrivacyLedgerAction = z.infer<typeof PrivacyLedgerActionSchema>;
126
+
127
+ export const PrivacyLedgerEntrySchema = z.object({
128
+ at: z.string().describe("When, as an ISO timestamp."),
129
+ conversationId: z.string().optional(),
130
+ provider: z.string(),
131
+ trusted: z.boolean(),
132
+ action: PrivacyLedgerActionSchema,
133
+ counts: z.partialRecord(PersonalDataClassSchema, z.number().int()).describe("How many of each kind were found in what this request added."),
134
+ images: z.number().int().describe("Images the shield changed: personal data painted over, or held back when they could not be read."),
135
+ documents: z.number().int().describe("Documents replaced by their masked text."),
136
+ protocol: z.string().describe("Which wire format the request spoke."),
137
+ detail: z.string().optional().describe("Why it was refused, when it was."),
138
+ });
139
+ export type PrivacyLedgerEntry = z.infer<typeof PrivacyLedgerEntrySchema>;
140
+
141
+ // A dataset taught to the shield: each value is matched exactly from then on, in every form it is written.
142
+ export const PRIVACY_KNOWN_BATCH_MAX = 50_000;
143
+
144
+ export const PrivacyKnownValueSchema = z.object({
145
+ value: z.string().min(2).max(500),
146
+ class: PersonalDataClassSchema,
147
+ });
148
+ export type PrivacyKnownValue = z.infer<typeof PrivacyKnownValueSchema>;
149
+
150
+ export const PrivacyKnownSourceSchema = z.object({
151
+ source: z.string().describe("Where the values came from, as whoever taught them named it."),
152
+ count: z.number().int(),
153
+ at: z.string().describe("When they were last taught."),
154
+ });
155
+ export type PrivacyKnownSource = z.infer<typeof PrivacyKnownSourceSchema>;
156
+
157
+ // The name lists the shield finds names by when no model is asked for (and alongside one when it is): what each list
158
+ // holds, how many, and where it came from.
159
+ export const PrivacyNameListSchema = z.object({
160
+ id: z.string().describe("Stable id of the list."),
161
+ kind: z.enum(["first-name", "surname", "ambiguous", "title", "never"]).describe("What a word on it says about a name."),
162
+ languages: z.array(z.enum(["pl", "en"])).describe("The languages its words come from."),
163
+ count: z.number().int().describe("How many words it holds."),
164
+ matching: z
165
+ .enum(["inflected", "as-written"])
166
+ .describe("inflected: matched in every grammatical form of a listed word; as-written: matched only exactly as listed."),
167
+ source: z.string().describe("Where the words come from: the register or dataset, or that they were written by hand."),
168
+ url: z.string().optional().describe("The source's page, where it has one."),
169
+ license: z.string().optional(),
170
+ });
171
+ export type PrivacyNameList = z.infer<typeof PrivacyNameListSchema>;
172
+
173
+ // What the lists say of one word.
174
+ export const PrivacyNameWordSchema = z.object({
175
+ word: z.string(),
176
+ firstName: z.boolean().describe("A listed first name, in this form or as an inflection of one."),
177
+ surname: z.boolean().describe("A listed surname, in this form or as an inflection of one."),
178
+ surnameForm: z.boolean().describe("Shaped like a Polish surname (-ski, -cki, -wicz…), listed or not."),
179
+ ambiguous: z.boolean().describe("Also an ordinary word, so found only beside other evidence (a surname, a title)."),
180
+ never: z.boolean().describe("Never taken as part of a name (a title, an institution, a function word)."),
181
+ });
182
+ export type PrivacyNameWord = z.infer<typeof PrivacyNameWordSchema>;
183
+
184
+ // What the dictionary makes of a word or a full name, for checking whether it would be found.
185
+ export const PrivacyNameLookupSchema = z.object({
186
+ text: z.string().describe("The query as a name is written: each word capitalized."),
187
+ found: z.boolean().describe("Whether the dictionary alone masks it as a name, written so on its own."),
188
+ words: z.array(PrivacyNameWordSchema),
189
+ });
190
+ export type PrivacyNameLookup = z.infer<typeof PrivacyNameLookupSchema>;
191
+
192
+ export const PRIVACY_DICTIONARY_SAMPLE_MAX = 200;
193
+
194
+ export const PrivacyDictionarySchema = z.object({
195
+ lists: z.array(PrivacyNameListSchema),
196
+ totals: z
197
+ .object({ firstNames: z.number().int(), surnames: z.number().int() })
198
+ .describe("Distinct words across the first-name lists, and across the surname lists."),
199
+ // Words of the lists that start with a one-word query, for browsing; empty without one.
200
+ matches: z.array(z.object({ word: z.string(), lists: z.array(z.string()).describe("Ids of the lists holding it.") })),
201
+ // The query read as a name, when there is one.
202
+ lookup: PrivacyNameLookupSchema.optional(),
203
+ });
204
+ export type PrivacyDictionary = z.infer<typeof PrivacyDictionarySchema>;