@intentic/sandbox-contract 1.334.0 → 1.335.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 (238) hide show
  1. package/README.md +8 -3
  2. package/dist/contracts/accounts.contract.d.ts +11 -11
  3. package/dist/contracts/activity.contract.d.ts +1 -0
  4. package/dist/contracts/activity.contract.d.ts.map +1 -1
  5. package/dist/contracts/activity.contract.js +1 -1
  6. package/dist/contracts/activity.contract.js.map +1 -1
  7. package/dist/contracts/agent.contract.d.ts +102 -18
  8. package/dist/contracts/agent.contract.d.ts.map +1 -1
  9. package/dist/contracts/agents.contract.d.ts +163 -0
  10. package/dist/contracts/agents.contract.d.ts.map +1 -1
  11. package/dist/contracts/agents.contract.js +10 -1
  12. package/dist/contracts/agents.contract.js.map +1 -1
  13. package/dist/contracts/audit.contract.d.ts +84 -0
  14. package/dist/contracts/audit.contract.d.ts.map +1 -0
  15. package/dist/contracts/audit.contract.js +43 -0
  16. package/dist/contracts/audit.contract.js.map +1 -0
  17. package/dist/contracts/automations.contract.d.ts +15 -0
  18. package/dist/contracts/automations.contract.d.ts.map +1 -1
  19. package/dist/contracts/ci.contract.d.ts +10 -0
  20. package/dist/contracts/ci.contract.d.ts.map +1 -1
  21. package/dist/contracts/compliance.contract.d.ts +163 -0
  22. package/dist/contracts/compliance.contract.d.ts.map +1 -0
  23. package/dist/contracts/compliance.contract.js +41 -0
  24. package/dist/contracts/compliance.contract.js.map +1 -0
  25. package/dist/contracts/device.contract.d.ts +6 -0
  26. package/dist/contracts/device.contract.d.ts.map +1 -1
  27. package/dist/contracts/extensions.contract.d.ts +2 -0
  28. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  29. package/dist/contracts/loops.contract.d.ts +15 -0
  30. package/dist/contracts/loops.contract.d.ts.map +1 -1
  31. package/dist/contracts/needs.contract.d.ts +49 -0
  32. package/dist/contracts/needs.contract.d.ts.map +1 -1
  33. package/dist/contracts/offload.contract.d.ts +5 -0
  34. package/dist/contracts/offload.contract.d.ts.map +1 -1
  35. package/dist/contracts/personas.contract.d.ts +10 -0
  36. package/dist/contracts/personas.contract.d.ts.map +1 -1
  37. package/dist/contracts/providers.contract.d.ts +2 -0
  38. package/dist/contracts/providers.contract.d.ts.map +1 -1
  39. package/dist/contracts/providers.contract.js +4 -0
  40. package/dist/contracts/providers.contract.js.map +1 -1
  41. package/dist/contracts/runner.contract.d.ts +42 -10
  42. package/dist/contracts/runner.contract.d.ts.map +1 -1
  43. package/dist/contracts/sessions.contract.d.ts +17 -0
  44. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  45. package/dist/contracts/settings.contract.d.ts +84 -0
  46. package/dist/contracts/settings.contract.d.ts.map +1 -1
  47. package/dist/contracts/settings.contract.js +7 -0
  48. package/dist/contracts/settings.contract.js.map +1 -1
  49. package/dist/contracts/system.contract.d.ts +16 -2
  50. package/dist/contracts/system.contract.d.ts.map +1 -1
  51. package/dist/contracts/workflows.contract.d.ts +30 -0
  52. package/dist/contracts/workflows.contract.d.ts.map +1 -1
  53. package/dist/contracts/workspace.contract.d.ts +3 -0
  54. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  55. package/dist/events/agent-events.d.ts +74 -0
  56. package/dist/events/agent-events.d.ts.map +1 -1
  57. package/dist/events/agent-events.js +1 -0
  58. package/dist/events/agent-events.js.map +1 -1
  59. package/dist/events/requests.d.ts +45 -0
  60. package/dist/events/requests.d.ts.map +1 -1
  61. package/dist/events/system-events.d.ts +12 -0
  62. package/dist/events/system-events.d.ts.map +1 -1
  63. package/dist/events/transcript.d.ts +117 -0
  64. package/dist/events/transcript.d.ts.map +1 -1
  65. package/dist/index.d.ts +828 -73
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +11 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/models/agent-catalog.d.ts +18 -3
  70. package/dist/models/agent-catalog.d.ts.map +1 -1
  71. package/dist/models/agent-catalog.js +61 -12
  72. package/dist/models/agent-catalog.js.map +1 -1
  73. package/dist/models/agent-runtimes.d.ts +5 -2
  74. package/dist/models/agent-runtimes.d.ts.map +1 -1
  75. package/dist/models/agent-runtimes.js +73 -40
  76. package/dist/models/agent-runtimes.js.map +1 -1
  77. package/dist/models/model-setups.d.ts +5 -0
  78. package/dist/models/model-setups.d.ts.map +1 -1
  79. package/dist/models/provider-specs.d.ts +153 -32
  80. package/dist/models/provider-specs.d.ts.map +1 -1
  81. package/dist/models/provider-specs.js +97 -11
  82. package/dist/models/provider-specs.js.map +1 -1
  83. package/dist/policy/reserved-servers.d.ts.map +1 -1
  84. package/dist/policy/reserved-servers.js +1 -0
  85. package/dist/policy/reserved-servers.js.map +1 -1
  86. package/dist/policy/sandbox-power.d.ts +7 -2
  87. package/dist/policy/sandbox-power.d.ts.map +1 -1
  88. package/dist/policy/sandbox-power.js +34 -1
  89. package/dist/policy/sandbox-power.js.map +1 -1
  90. package/dist/policy/turned-away.d.ts.map +1 -1
  91. package/dist/policy/turned-away.js +1 -0
  92. package/dist/policy/turned-away.js.map +1 -1
  93. package/dist/protocol/doors/listener-protocol.d.ts +43 -1
  94. package/dist/protocol/doors/listener-protocol.d.ts.map +1 -1
  95. package/dist/protocol/doors/listener-protocol.js +10 -0
  96. package/dist/protocol/doors/listener-protocol.js.map +1 -1
  97. package/dist/protocol/raw/raw-json-routes.d.ts +92 -0
  98. package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
  99. package/dist/protocol/raw/raw-json-routes.js +2 -1
  100. package/dist/protocol/raw/raw-json-routes.js.map +1 -1
  101. package/dist/protocol/raw/raw-routes.d.ts +13 -0
  102. package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
  103. package/dist/protocol/raw/raw-routes.js +3 -0
  104. package/dist/protocol/raw/raw-routes.js.map +1 -1
  105. package/dist/schemas/activity.d.ts +2 -0
  106. package/dist/schemas/activity.d.ts.map +1 -1
  107. package/dist/schemas/agent.d.ts +34 -0
  108. package/dist/schemas/agent.d.ts.map +1 -1
  109. package/dist/schemas/agent.js +7 -6
  110. package/dist/schemas/agent.js.map +1 -1
  111. package/dist/schemas/agents-list.d.ts +7 -0
  112. package/dist/schemas/agents-list.d.ts.map +1 -1
  113. package/dist/schemas/agents.d.ts +18 -0
  114. package/dist/schemas/agents.d.ts.map +1 -1
  115. package/dist/schemas/ai-literacy.d.ts +54 -0
  116. package/dist/schemas/ai-literacy.d.ts.map +1 -0
  117. package/dist/schemas/ai-literacy.js +15 -0
  118. package/dist/schemas/ai-literacy.js.map +1 -0
  119. package/dist/schemas/audit.d.ts +166 -0
  120. package/dist/schemas/audit.d.ts.map +1 -0
  121. package/dist/schemas/audit.js +100 -0
  122. package/dist/schemas/audit.js.map +1 -0
  123. package/dist/schemas/automations.d.ts +27 -0
  124. package/dist/schemas/automations.d.ts.map +1 -1
  125. package/dist/schemas/automations.js +11 -0
  126. package/dist/schemas/automations.js.map +1 -1
  127. package/dist/schemas/ci.d.ts +10 -0
  128. package/dist/schemas/ci.d.ts.map +1 -1
  129. package/dist/schemas/compliance.d.ts +423 -0
  130. package/dist/schemas/compliance.d.ts.map +1 -0
  131. package/dist/schemas/compliance.js +135 -0
  132. package/dist/schemas/compliance.js.map +1 -0
  133. package/dist/schemas/data-protection.d.ts +28 -0
  134. package/dist/schemas/data-protection.d.ts.map +1 -0
  135. package/dist/schemas/data-protection.js +49 -0
  136. package/dist/schemas/data-protection.js.map +1 -0
  137. package/dist/schemas/devices.d.ts +12 -0
  138. package/dist/schemas/devices.d.ts.map +1 -1
  139. package/dist/schemas/engines.d.ts +22 -1
  140. package/dist/schemas/engines.d.ts.map +1 -1
  141. package/dist/schemas/engines.js +1 -1
  142. package/dist/schemas/engines.js.map +1 -1
  143. package/dist/schemas/extension-updates.d.ts +4 -0
  144. package/dist/schemas/extension-updates.d.ts.map +1 -1
  145. package/dist/schemas/git/git.d.ts +33 -0
  146. package/dist/schemas/git/git.d.ts.map +1 -1
  147. package/dist/schemas/git/git.js +29 -0
  148. package/dist/schemas/git/git.js.map +1 -1
  149. package/dist/schemas/hosts.d.ts +36 -0
  150. package/dist/schemas/hosts.d.ts.map +1 -1
  151. package/dist/schemas/hosts.js +2 -0
  152. package/dist/schemas/hosts.js.map +1 -1
  153. package/dist/schemas/intentic-os.d.ts +117 -0
  154. package/dist/schemas/intentic-os.d.ts.map +1 -0
  155. package/dist/schemas/intentic-os.js +66 -0
  156. package/dist/schemas/intentic-os.js.map +1 -0
  157. package/dist/schemas/loops.d.ts +15 -0
  158. package/dist/schemas/loops.d.ts.map +1 -1
  159. package/dist/schemas/members.d.ts +90 -0
  160. package/dist/schemas/members.d.ts.map +1 -1
  161. package/dist/schemas/members.js +73 -3
  162. package/dist/schemas/members.js.map +1 -1
  163. package/dist/schemas/needs.d.ts +56 -0
  164. package/dist/schemas/needs.d.ts.map +1 -1
  165. package/dist/schemas/offload.d.ts +5 -0
  166. package/dist/schemas/offload.d.ts.map +1 -1
  167. package/dist/schemas/personas.d.ts +10 -0
  168. package/dist/schemas/personas.d.ts.map +1 -1
  169. package/dist/schemas/providers/plan-limits.d.ts +20 -0
  170. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  171. package/dist/schemas/providers/provider-oauth.js +1 -1
  172. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  173. package/dist/schemas/providers/usage.d.ts +1 -0
  174. package/dist/schemas/providers/usage.d.ts.map +1 -1
  175. package/dist/schemas/providers/usage.js +4 -0
  176. package/dist/schemas/providers/usage.js.map +1 -1
  177. package/dist/schemas/settings.d.ts +48 -0
  178. package/dist/schemas/settings.d.ts.map +1 -1
  179. package/dist/schemas/settings.js +14 -0
  180. package/dist/schemas/settings.js.map +1 -1
  181. package/dist/schemas/system.d.ts +8 -0
  182. package/dist/schemas/system.d.ts.map +1 -1
  183. package/dist/schemas/system.js +2 -0
  184. package/dist/schemas/system.js.map +1 -1
  185. package/dist/schemas/workflows.d.ts +50 -0
  186. package/dist/schemas/workflows.d.ts.map +1 -1
  187. package/dist/schemas/workspace/workspace-tree.d.ts +4 -0
  188. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  189. package/dist/schemas/workspace/workspace-tree.js +16 -1
  190. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  191. package/dist/state/definition.d.ts +80 -4
  192. package/dist/state/definition.d.ts.map +1 -1
  193. package/dist/state/history-state.d.ts.map +1 -1
  194. package/dist/state/history-state.js +2 -0
  195. package/dist/state/history-state.js.map +1 -1
  196. package/package.json +5 -5
  197. package/src/contracts/activity.contract.ts +1 -1
  198. package/src/contracts/agents.contract.ts +13 -1
  199. package/src/contracts/audit.contract.ts +48 -0
  200. package/src/contracts/compliance.contract.ts +57 -0
  201. package/src/contracts/providers.contract.ts +9 -0
  202. package/src/contracts/settings.contract.ts +7 -0
  203. package/src/events/agent-events.ts +4 -0
  204. package/src/index.ts +11 -0
  205. package/src/models/agent-catalog.test.ts +317 -53
  206. package/src/models/agent-catalog.ts +115 -22
  207. package/src/models/agent-runtimes.ts +273 -75
  208. package/src/models/capability-ledger.test.ts +12 -1
  209. package/src/models/provider-specs.test.ts +42 -7
  210. package/src/models/provider-specs.ts +160 -20
  211. package/src/policy/reserved-servers.ts +2 -1
  212. package/src/policy/sandbox-power.ts +67 -8
  213. package/src/policy/tests/sandbox-power.test.ts +59 -1
  214. package/src/policy/turned-away.ts +2 -0
  215. package/src/protocol/doors/listener-protocol.ts +74 -6
  216. package/src/protocol/raw/raw-json-routes.ts +2 -1
  217. package/src/protocol/raw/raw-routes.ts +7 -0
  218. package/src/schemas/activity.ts +2 -2
  219. package/src/schemas/agent.ts +22 -9
  220. package/src/schemas/ai-literacy.ts +17 -0
  221. package/src/schemas/audit.ts +131 -0
  222. package/src/schemas/automations.ts +13 -0
  223. package/src/schemas/compliance.ts +195 -0
  224. package/src/schemas/daemon-health.test.ts +2 -0
  225. package/src/schemas/data-protection.ts +99 -0
  226. package/src/schemas/engines.ts +1 -1
  227. package/src/schemas/git/git.ts +46 -0
  228. package/src/schemas/hosts.ts +5 -0
  229. package/src/schemas/intentic-os.test.ts +86 -0
  230. package/src/schemas/intentic-os.ts +130 -0
  231. package/src/schemas/members.test.ts +47 -0
  232. package/src/schemas/members.ts +141 -6
  233. package/src/schemas/providers/provider-oauth.ts +1 -1
  234. package/src/schemas/providers/usage.ts +6 -0
  235. package/src/schemas/settings.ts +32 -0
  236. package/src/schemas/system.ts +2 -0
  237. package/src/schemas/workspace/workspace-tree.ts +26 -1
  238. package/src/state/history-state.ts +7 -0
@@ -0,0 +1,99 @@
1
+ // data-protection: what a sandbox needs to run under a data protection officer's rules (GDPR/RODO, Poland's KRI, the
2
+ // AI Act): how long what it keeps is kept, what its audit trail records and for how long, whether the product's own
3
+ // analytics may watch it, and the notice its members read about working with an AI. Every default here is what the
4
+ // sandbox did before these settings existed. A public office switches them on with the public-office preset on the
5
+ // data protection page, never through a changed default: most sandboxes belong to developers and individuals, and a
6
+ // stricter default would take away something they rely on.
7
+ import { z } from "zod";
8
+
9
+ export const RETENTION_DAYS_MAX = 3650;
10
+ const retentionDays = () => z.number().int().min(0).max(RETENTION_DAYS_MAX).default(0);
11
+
12
+ // One field per kind of copy the sandbox keeps of what people and agents wrote. Zero keeps that kind forever, which is
13
+ // what every sandbox did before this block existed. A positive value DELETES (it does not archive) once the copy is
14
+ // older than that many days, so a person's data asked to be forgotten is gone from every place it sat.
15
+ export const RetentionSettingsSchema = z.object({
16
+ transcriptDays: retentionDays().describe(
17
+ "How many days a conversation's transcript is kept after its last message. After that the transcript and its backups are deleted for good, not archived. Zero keeps them forever.",
18
+ ),
19
+ restorePointDays: retentionDays().describe(
20
+ "How many days the workspace's restore points are kept. These are the snapshots taken every minute while agents work, so files deleted from the workspace stay recoverable until then. Zero keeps every restore point.",
21
+ ),
22
+ privacyVaultDays: retentionDays().describe(
23
+ "How many days the privacy shield remembers a personal value after it last saw it. After that the value and its token are deleted, and the value is matched as new if it comes back. Zero remembers them forever.",
24
+ ),
25
+ cacheDays: retentionDays().describe(
26
+ "How many days converted copies of documents are kept: text read out of PDFs and office files, and the results of reading scans and images. Zero keeps them until the cache needs the room.",
27
+ ),
28
+ rawOutputDays: retentionDays().describe(
29
+ "How many days the full, untrimmed output of commands is kept beside the trimmed version the agent read. Zero keeps it until the space is needed.",
30
+ ),
31
+ });
32
+ export type RetentionSettings = z.infer<typeof RetentionSettingsSchema>;
33
+
34
+ export const AUDIT_RETENTION_DAYS_MIN = 30;
35
+ export const AUDIT_RETENTION_DAYS_DEFAULT = 365;
36
+ // Poland's KRI regulation (Dz.U. 2024 poz. 773, §20) keeps a public system's logs for two years; the preset uses this.
37
+ export const AUDIT_RETENTION_DAYS_PUBLIC_OFFICE = 730;
38
+
39
+ // The audit trail: who changed a policy, a member, a secret's gate or a setting, and (when asked) who read what. It is
40
+ // separate from the activity log, which records the messages team-chat bots send and receive. It is hash-chained, so a
41
+ // changed or removed record shows, and it is written by the daemon alone, outside the agent's workspace.
42
+ export const AuditSettingsSchema = z.object({
43
+ retentionDays: z
44
+ .number()
45
+ .int()
46
+ .min(AUDIT_RETENTION_DAYS_MIN)
47
+ .max(RETENTION_DAYS_MAX)
48
+ .default(AUDIT_RETENTION_DAYS_DEFAULT)
49
+ .describe(
50
+ "How many days audit records are kept before the oldest are deleted. Public bodies in Poland keep system logs for two years (730 days).",
51
+ ),
52
+ recordReads: z
53
+ .boolean()
54
+ .default(false)
55
+ .describe(
56
+ "Also record who opened which conversation, transcript, file or log, not only who changed something. Off by default because it writes a record on every page someone opens.",
57
+ ),
58
+ forwardUrl: z
59
+ .union([z.literal(""), z.url({ protocol: /^https$/ })])
60
+ .default("")
61
+ .describe(
62
+ "An HTTPS address each audit record is also sent to as it is written, such as your organisation's log collector (SIEM). A copy kept outside the sandbox is the one nobody inside it can change. Empty sends nothing.",
63
+ ),
64
+ forwardSecret: z
65
+ .string()
66
+ .max(200)
67
+ .default("")
68
+ .describe(
69
+ "The name of a stored secret sent as a bearer token with each forwarded record. Empty sends none.",
70
+ ),
71
+ });
72
+ export type AuditSettings = z.infer<typeof AuditSettingsSchema>;
73
+
74
+ export const AI_NOTICE_TEXT_MAX = 4000;
75
+
76
+ // The AI Act's literacy duty (Art. 4) falls on whoever puts staff in front of an AI system. This is the card that tells
77
+ // them, once, what they are working with. Off by default: a developer does not need to be told, and the preset turns
78
+ // it on.
79
+ export const AiNoticeSchema = z.object({
80
+ show: z
81
+ .boolean()
82
+ .default(false)
83
+ .describe(
84
+ "Show members a short notice the first time they open this sandbox: that they are working with an AI, that it makes mistakes, what to check before acting on its answers, and what not to paste into it. They confirm it once, and the confirmation is recorded.",
85
+ ),
86
+ text: z
87
+ .string()
88
+ .max(AI_NOTICE_TEXT_MAX)
89
+ .default("")
90
+ .describe(
91
+ "Your own wording for that notice, such as your office's rules for using AI. Empty shows the built-in text in the reader's language.",
92
+ ),
93
+ });
94
+ export type AiNotice = z.infer<typeof AiNoticeSchema>;
95
+
96
+ // Where the key that encrypts the sandbox's stores at rest (secrets, the credential vault, the privacy shield's vault)
97
+ // is read from: a file path in the daemon's environment, normally a secret mounted from outside the sandbox's volumes.
98
+ // Unset, those stores stay plain files readable only by their owner, as they always were.
99
+ export const AT_REST_KEY_FILE_ENV = "INTENTIC_AT_REST_KEY_FILE";
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  // An engine is the installed program a runtime (agent-catalog.ts) spawns or imports — the `claude` binary, `codex`
5
5
  // wrapper, `@cursor/sdk`, `opencode`, `cli-proxy-api` — not the loop itself; one engine can back several runtimes.
6
6
 
7
- export const ENGINE_IDS = ["claude", "codex", "cursor", "devin", "opencode", "translator"] as const;
7
+ export const ENGINE_IDS = ["claude", "codex", "cursor", "devin", "grok-build", "kimi", "muse", "opencode", "translator"] as const;
8
8
  export type EngineId = (typeof ENGINE_IDS)[number];
9
9
  export const EngineIdSchema = z.enum(ENGINE_IDS);
10
10
 
@@ -275,6 +275,21 @@ export type ScratchPath = z.infer<typeof ScratchPathSchema>;
275
275
  // Whether a scratch entry covers `path`: a directory entry everything under it, a file entry only itself.
276
276
  export const isScratch = (path: string, scratch: readonly Pick<ScratchPath, "path">[]): boolean =>
277
277
  scratch.some((entry) => (entry.path.endsWith("/") ? path.startsWith(entry.path) : path === entry.path));
278
+ // Apart from scratch, which is left out for its shape: these are left out because the repository says so, and the
279
+ // owner usually wants them most (a render in `out/`, a build in `dist/`).
280
+ export const IgnoredOutputKindSchema = z
281
+ .enum(["new", "changed"])
282
+ .describe("Whether your tree has nothing at this path yet, or holds a different file there that copying would replace.");
283
+ export type IgnoredOutputKind = z.infer<typeof IgnoredOutputKindSchema>;
284
+ export const IgnoredOutputSchema = z.object({
285
+ path: z.string().describe("Relative to its repository. Always one file, never a directory."),
286
+ kind: IgnoredOutputKindSchema,
287
+ bytes: z.number().describe("Its size in bytes."),
288
+ });
289
+ export type IgnoredOutput = z.infer<typeof IgnoredOutputSchema>;
290
+ // How many ignored outputs one repository lists by name; past it they are only counted, since a turn that ran a build
291
+ // can leave tens of thousands.
292
+ export const IGNORED_OUTPUTS_LISTED = 200;
278
293
  export const RepoChangesSchema = z.object({
279
294
  // The {repo} param the per-repo git routes accept: "root" or a repo id (its root-relative dir).
280
295
  repo: z.string(),
@@ -501,6 +516,25 @@ export const AgentChangesSchema = z.object({
501
516
  .describe(
502
517
  "Untracked files the conversation left in its copy that look like scratch: logs, probe scripts, dumps, a checkout of its own. They are not in the list above and no merge carries them. They stay in its copy until they are included or deleted, and go with the copy when it is archived or retired. Absent when there are none.",
503
518
  ),
519
+ // Read off the live copy like scratch, and judged against the main tree as it stands now, so a file already copied
520
+ // over drops out.
521
+ ignored: z
522
+ .array(
523
+ z.object({
524
+ repo: z.string().describe("Which repository."),
525
+ paths: z.array(IgnoredOutputSchema).describe(`What it wrote there, sorted by path, at most ${IGNORED_OUTPUTS_LISTED} of them.`),
526
+ more: z
527
+ .number()
528
+ .optional()
529
+ .describe(
530
+ "How many more such files it wrote there past the listed ones. A floor, not an exact count, when the copy holds more files than are looked at. Absent when every one is listed.",
531
+ ),
532
+ }),
533
+ )
534
+ .optional()
535
+ .describe(
536
+ "Files the conversation wrote in its copy that the repository's ignore rules exclude, such as a render in `out/` or a build in `dist/`, which your tree lacks or holds differently. No merge carries them and they never enter git; copying one puts it at the same path in your tree. Dependencies (`node_modules`), hidden files and folders, and logs, dumps and caches are never listed. Absent when there are none.",
537
+ ),
504
538
  });
505
539
  export type AgentChanges = z.infer<typeof AgentChangesSchema>;
506
540
 
@@ -522,6 +556,18 @@ export const AgentScratchSchema = z.object({
522
556
  });
523
557
  export type AgentScratch = z.infer<typeof AgentScratchSchema>;
524
558
 
559
+ // Names ignored outputs as `AgentChanges.ignored` lists them, one file each; a path that is not one now is refused.
560
+ export const AgentIgnoredSchema = z.object({
561
+ id: z.string().min(1).describe("Which conversation."),
562
+ repo: z.string().min(1).describe("Which repository of its composition."),
563
+ paths: z
564
+ .array(z.string().min(1))
565
+ .min(1)
566
+ .max(MAX_ACTION_PATHS)
567
+ .describe("Paths exactly as the review lists them under ignored, each a file relative to the repository."),
568
+ });
569
+ export type AgentIgnored = z.infer<typeof AgentIgnoredSchema>;
570
+
525
571
  // Own read (a `git log` per repo), not folded into the review, whose rows are differences against main and these are
526
572
  // not. A commit CARRIES the work without authoring it: a path is attributed to the newest commit that left it there.
527
573
  export const AgentHistoryCommitSchema = z.object({
@@ -1,5 +1,6 @@
1
1
  // hosts: the user's own connected devices (the `host` capability's live half)
2
2
  import { z } from "zod";
3
+ import { IntenticOsFactsSchema } from "./intentic-os.js";
3
4
  // Manifest says which machines are intended; this says which actually hold a socket now. Nothing persists across a
4
5
  // daemon restart except the enrollment itself, so a closed laptop reads offline within a heartbeat.
5
6
 
@@ -83,6 +84,10 @@ export const DeviceFactsSchema = z.object({
83
84
  // a daemon bug for an afternoon. Override values are the agent's own redaction (secret-looking ones by length).
84
85
  ic: z.object({ path: z.string(), version: z.string(), modified: z.string().optional() }).optional(),
85
86
  icOverrides: z.array(z.string()).optional(),
87
+ // (2026-10-13) Present only on Intentic OS (`/usr/lib/intentic-os/release.json`): which edition, version and channel
88
+ // the machine boots, so the daemon tells the agent it may be this box's steward and the os_* tools are there. `os`
89
+ // above stays the human name. Absent everywhere else, and from an agent older than the field.
90
+ intenticOs: IntenticOsFactsSchema.optional(),
86
91
  });
87
92
  export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
88
93
 
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { packageRoot } from "@intentic/constants/node";
4
+ import { DeviceFactsSchema } from "./hosts.js";
5
+ import {
6
+ icOsChannelArgs,
7
+ icOsRebootArgs,
8
+ icOsRollbackArgs,
9
+ icOsStatusArgs,
10
+ icOsUpdateArgs,
11
+ type IntenticOsChannel,
12
+ intenticOsFactsFrom,
13
+ IntenticOsReleaseSchema,
14
+ IntenticOsStatusSchema,
15
+ IntenticOsUpdateCheckSchema,
16
+ isIntenticOs,
17
+ } from "./intentic-os.js";
18
+
19
+ // What `ic os` answers, as examples: golden/intentic-os.json holds a release.json, the shapes `ic os status --json`
20
+ // gives (a quiet box, one with an update staged and a failed boot, one whose bootc said little) and both answers of
21
+ // `ic os update --check --json`. This is the TS half: what ic gives is what the contract reads, to the field.
22
+
23
+ // SAFETY: the golden is this package's own file; the assertions below fail on any field it lacks.
24
+ const GOLDEN = JSON.parse(readFileSync(join(packageRoot(import.meta.url), "golden", "intentic-os.json"), "utf8")) as {
25
+ readonly release: unknown;
26
+ readonly status: readonly Readonly<Record<string, unknown>>[];
27
+ readonly updateCheck: readonly Readonly<Record<string, unknown>>[];
28
+ };
29
+
30
+ test("every status and update check in the golden reads back field for field", () => {
31
+ expect(GOLDEN.status.length).toBe(3);
32
+ for (const status of GOLDEN.status) {
33
+ const read: unknown = IntenticOsStatusSchema.parse(status);
34
+ expect(read).toEqual(status);
35
+ }
36
+ for (const check of GOLDEN.updateCheck) {
37
+ const read: unknown = IntenticOsUpdateCheckSchema.parse(check);
38
+ expect(read).toEqual(check);
39
+ }
40
+ });
41
+
42
+ test("a status from a newer ic keeps what this side reads, and only a wrong verdict, isolation or OS is no status", () => {
43
+ const [quiet] = GOLDEN.status;
44
+ const read: unknown = IntenticOsStatusSchema.parse({ ...quiet, pinned: true });
45
+ expect(read).toEqual(quiet);
46
+ expect(IntenticOsStatusSchema.safeParse({ ...quiet, health: { verdict: "amber", failed: [] } }).success).toBe(false);
47
+ expect(IntenticOsStatusSchema.safeParse({ ...quiet, isolation: "gvisor" }).success).toBe(false);
48
+ expect(IntenticOsStatusSchema.safeParse({ ...quiet, os: "fedora" }).success).toBe(false);
49
+ });
50
+
51
+ test("the facts a device reports are release.json's edition, version and channel, under the OS's own name", () => {
52
+ const release = IntenticOsReleaseSchema.parse(GOLDEN.release);
53
+ const facts = intenticOsFactsFrom(release);
54
+ expect(facts).toEqual({ name: "intentic-os", edition: "server", version: "2026.10.13.0", channel: "stable" });
55
+ expect(isIntenticOs({ intenticOs: facts })).toBe(true);
56
+ expect(isIntenticOs({})).toBe(false);
57
+ expect(isIntenticOs(undefined)).toBe(false);
58
+ expect(isIntenticOs({ intenticOs: { name: "Intentic OS" } })).toBe(false);
59
+ // An optional fact: a device on any other OS says nothing, and an older daemon's read of a newer one is unharmed.
60
+ const device = { os: "Intentic OS 2026.10.13.0 (kernel 6.11)", arch: "x64", shell: "/bin/bash", home: "/var/home/intentic", roots: [] };
61
+ expect(DeviceFactsSchema.parse({ ...device, intenticOs: facts }).intenticOs).toEqual(facts);
62
+ expect(DeviceFactsSchema.parse(device).intenticOs).toBeUndefined();
63
+ });
64
+
65
+ test("a release.json without its edition, version or channel is not one", () => {
66
+ expect(IntenticOsReleaseSchema.safeParse({ name: "Intentic OS", edition: "server", version: "2026.10.13.0" }).success).toBe(false);
67
+ });
68
+
69
+ test("the argv is ic's own verbs, the one spelling every caller uses", () => {
70
+ expect(icOsStatusArgs()).toEqual(["os", "status", "--json"]);
71
+ expect(icOsUpdateArgs("check")).toEqual(["os", "update", "--check", "--json"]);
72
+ expect(icOsUpdateArgs("stage")).toEqual(["os", "update"]);
73
+ expect(icOsUpdateArgs("apply")).toEqual(["os", "update", "--apply"]);
74
+ expect(icOsRollbackArgs(false)).toEqual(["os", "rollback"]);
75
+ expect(icOsRollbackArgs(true)).toEqual(["os", "rollback", "--apply"]);
76
+ expect(icOsRebootArgs()).toEqual(["os", "reboot"]);
77
+ expect(icOsChannelArgs("beta")).toEqual(["os", "channel", "beta"]);
78
+ expect(icOsChannelArgs("stable")).toEqual(["os", "channel", "stable"]);
79
+ });
80
+
81
+ test("a channel ic does not have never reaches it, even past the type", () => {
82
+ for (const channel of ["--apply", "nightly"]) {
83
+ // SAFETY: deliberately not a channel: what a caller that skipped its own schema would pass.
84
+ expect(() => icOsChannelArgs(channel as IntenticOsChannel), channel).toThrow();
85
+ }
86
+ });
@@ -0,0 +1,130 @@
1
+ import { z } from "zod";
2
+
3
+ // INTENTIC OS (2026-10-13): the immutable Linux that boots straight into Intentic (_os/README.md). Everything that changes
4
+ // the OS goes through `ic os`, and everything that calls it reads ic's JSON rather than bootc's. This is that JSON as the
5
+ // TypeScript side reads it, and the argv every caller spells it with: the machine agent's os_* tools run it, and a page
6
+ // that cannot reach the machine can print it for a person to type. golden/intentic-os.json holds an example of each
7
+ // answer, for ic's Rust to produce field for field.
8
+
9
+ /** What `ic os status` says the OS is called, and what the machine agent reports in its facts. */
10
+ export const INTENTIC_OS_NAME = "intentic-os";
11
+
12
+ /** Written by the image build; a machine without it is not Intentic OS, and `ic os` refuses there. */
13
+ export const INTENTIC_OS_RELEASE_PATH = "/usr/lib/intentic-os/release.json";
14
+
15
+ /** The exit code `ic os` refuses with on a machine that is not Intentic OS. */
16
+ export const IC_OS_NOT_INTENTIC_OS_EXIT = 3;
17
+
18
+ // `/usr/lib/intentic-os/release.json`, as the image build writes it. `name` is the human one ("Intentic OS").
19
+ // Read loosely: a field a newer image adds is ignored, and only the four the facts carry are required.
20
+ export const IntenticOsReleaseSchema = z.object({
21
+ name: z.string(),
22
+ edition: z.string().min(1),
23
+ version: z.string().min(1),
24
+ channel: z.string().min(1),
25
+ repository: z.string().optional(),
26
+ });
27
+ export type IntenticOsRelease = z.infer<typeof IntenticOsReleaseSchema>;
28
+
29
+ // What a device on Intentic OS reports in its facts (DeviceFacts.intenticOs), read off release.json at connect. Strings
30
+ // on the wire, like every fact, so an edition or channel a newer image names never fails an older daemon's read.
31
+ export const IntenticOsFactsSchema = z.object({
32
+ // Always INTENTIC_OS_NAME from a current agent; compared, never trusted to be a literal.
33
+ name: z.string(),
34
+ edition: z.string(),
35
+ version: z.string(),
36
+ channel: z.string(),
37
+ });
38
+ export type IntenticOsFacts = z.infer<typeof IntenticOsFactsSchema>;
39
+
40
+ export const intenticOsFactsFrom = (release: IntenticOsRelease): IntenticOsFacts => ({
41
+ name: INTENTIC_OS_NAME,
42
+ edition: release.edition,
43
+ version: release.version,
44
+ channel: release.channel,
45
+ });
46
+
47
+ /** Whether a device's facts say it runs Intentic OS. */
48
+ export const isIntenticOs = (facts: { readonly intenticOs?: { readonly name: string } | undefined } | undefined): boolean =>
49
+ facts?.intenticOs?.name === INTENTIC_OS_NAME;
50
+
51
+ // A channel is the image tag. `ic os channel` takes only these two; `status` reads the booted image's tag and says
52
+ // whatever it is, so the status field stays a string.
53
+ export const IntenticOsChannelSchema = z.enum(["stable", "beta"]);
54
+ export type IntenticOsChannel = z.infer<typeof IntenticOsChannelSchema>;
55
+
56
+ // One bootc deployment (`status.booted|staged|rollback`), each field `null` where bootc does not say.
57
+ export const IntenticOsDeploymentSchema = z.object({
58
+ image: z.string().nullable(),
59
+ version: z.string().nullable(),
60
+ digest: z.string().nullable(),
61
+ // ISO 8601, as bootc gives it.
62
+ timestamp: z.string().nullable(),
63
+ });
64
+ export type IntenticOsDeployment = z.infer<typeof IntenticOsDeploymentSchema>;
65
+
66
+ // The boot's health as `ic os health` last wrote it (`/run/intentic-os/health.json`): `unknown` before it ran, and
67
+ // `failed` names each failed check.
68
+ export const IntenticOsHealthVerdictSchema = z.enum(["green", "red", "unknown"]);
69
+ export const IntenticOsHealthSchema = z.object({ verdict: IntenticOsHealthVerdictSchema, failed: z.array(z.string()) });
70
+ export type IntenticOsHealth = z.infer<typeof IntenticOsHealthSchema>;
71
+
72
+ // How sandbox containers run (`/etc/intentic/os.json`): `container` is runc, `vm` a Kata microVM for every sandbox, and
73
+ // `auto` a microVM only for a sandbox started privileged.
74
+ export const IntenticOsIsolationSchema = z.enum(["auto", "vm", "container"]);
75
+ export type IntenticOsIsolation = z.infer<typeof IntenticOsIsolationSchema>;
76
+
77
+ // `ic os status --json`: one line. A key a newer ic adds is dropped, not refused.
78
+ export const IntenticOsStatusSchema = z.object({
79
+ os: z.literal(INTENTIC_OS_NAME),
80
+ edition: z.string(),
81
+ version: z.string(),
82
+ channel: z.string(),
83
+ image: z.string(),
84
+ // Nullable for a bootc that would not answer; `staged` and `rollback` are null when there is none.
85
+ booted: IntenticOsDeploymentSchema.nullable(),
86
+ staged: IntenticOsDeploymentSchema.nullable(),
87
+ rollback: IntenticOsDeploymentSchema.nullable(),
88
+ rollbackQueued: z.boolean(),
89
+ // `staged != null || rollbackQueued`: the next boot changes what runs.
90
+ rebootRequired: z.boolean(),
91
+ health: IntenticOsHealthSchema,
92
+ isolation: IntenticOsIsolationSchema,
93
+ // Whether dockerd has the `kata` runtime.
94
+ vmRuntime: z.boolean(),
95
+ });
96
+ export type IntenticOsStatus = z.infer<typeof IntenticOsStatusSchema>;
97
+
98
+ // `ic os update --check --json`: whether the registry has a newer image, and which. `version` and `digest` are null
99
+ // when there is none.
100
+ export const IntenticOsUpdateCheckSchema = z.object({
101
+ available: z.boolean(),
102
+ version: z.string().nullable(),
103
+ digest: z.string().nullable(),
104
+ });
105
+ export type IntenticOsUpdateCheck = z.infer<typeof IntenticOsUpdateCheckSchema>;
106
+
107
+ // What an update does: `check` asks and changes nothing, `stage` downloads and stages for the next boot and restarts
108
+ // nothing, `apply` stages and then reboots the machine into it.
109
+ export const IntenticOsUpdateOpSchema = z.enum(["check", "stage", "apply"]);
110
+ export type IntenticOsUpdateOp = z.infer<typeof IntenticOsUpdateOpSchema>;
111
+
112
+ // THE ARGV, the one place any caller spells `ic os`. The verbs that reboot (`update --apply`, `rollback --apply`,
113
+ // `reboot`) first leave every running sandbox the daemon's resume ask and record what runs, as every restart ic makes
114
+ // does: none of that is the caller's to do.
115
+ export const icOsStatusArgs = (): string[] => ["os", "status", "--json"];
116
+
117
+ export const icOsUpdateArgs = (op: IntenticOsUpdateOp): string[] => {
118
+ if (op === "check") {
119
+ return ["os", "update", "--check", "--json"];
120
+ }
121
+ return op === "apply" ? ["os", "update", "--apply"] : ["os", "update"];
122
+ };
123
+
124
+ export const icOsRollbackArgs = (apply: boolean): string[] => (apply ? ["os", "rollback", "--apply"] : ["os", "rollback"]);
125
+
126
+ export const icOsRebootArgs = (): string[] => ["os", "reboot"];
127
+
128
+ // Checked here too, not only by a caller's schema: a channel is a positional, and one beginning with a hyphen would
129
+ // reach ic as a flag.
130
+ export const icOsChannelArgs = (channel: IntenticOsChannel): string[] => ["os", "channel", IntenticOsChannelSchema.parse(channel)];
@@ -0,0 +1,47 @@
1
+ import { accessRefusal, memberAccess, tierOf } from "./members.js";
2
+
3
+ // A person's access is three answers, and the tier is what they add up to; a row from before the answers reads as its
4
+ // role meant, so nobody's access moves when the daemon learns the new fields.
5
+
6
+ describe(`memberAccess`, () => {
7
+ it(`reads a row written before agents and edit as its role meant`, () => {
8
+ expect(memberAccess({ role: `guest`, areas: [`support`] })).toEqual({ manage: false, agents: `chat`, see: [`support`], edit: `none` });
9
+ expect(memberAccess({ role: `viewer` })).toEqual({ manage: false, agents: `off`, see: undefined, edit: `none` });
10
+ expect(memberAccess({ role: `collaborator`, areas: [`web`] })).toEqual({ manage: false, agents: `run`, see: [`web`], edit: `none` });
11
+ expect(memberAccess({ role: `writer`, areas: [`web`] })).toEqual({ manage: false, agents: `run`, see: [`web`], edit: [`web`] });
12
+ expect(memberAccess({ role: `maintainer`, areas: [`web`] })).toEqual({ manage: true, agents: `run`, see: undefined, edit: `all` });
13
+ });
14
+
15
+ it(`takes what a row says when it says it, and never lets a chat edit`, () => {
16
+ expect(memberAccess({ role: `writer`, agents: `off`, edit: [`web`] })).toEqual({ manage: false, agents: `off`, see: undefined, edit: [`web`] });
17
+ expect(memberAccess({ role: `guest`, agents: `chat`, areas: [`support`], edit: `all` }).edit).toBe(`none`);
18
+ });
19
+ });
20
+
21
+ describe(`tierOf`, () => {
22
+ it(`names the tier the answers add up to`, () => {
23
+ expect(tierOf({ manage: true, agents: `run`, see: undefined, edit: `all` })).toBe(`maintainer`);
24
+ expect(tierOf({ manage: false, agents: `chat`, see: [`support`], edit: `none` })).toBe(`guest`);
25
+ expect(tierOf({ manage: false, agents: `off`, see: undefined, edit: [`web`] })).toBe(`writer`);
26
+ expect(tierOf({ manage: false, agents: `run`, see: undefined, edit: `none` })).toBe(`collaborator`);
27
+ expect(tierOf({ manage: false, agents: `off`, see: [`web`], edit: `none` })).toBe(`viewer`);
28
+ });
29
+ });
30
+
31
+ describe(`accessRefusal`, () => {
32
+ const access = (over: Partial<Parameters<typeof tierOf>[0]>) => ({ manage: false, agents: `run` as const, see: undefined, edit: `none` as const, ...over });
33
+
34
+ it(`asks a chat for the areas whose assistants it talks to`, () => {
35
+ expect(accessRefusal(access({ agents: `chat` }))).toContain(`at least one area`);
36
+ expect(accessRefusal(access({ agents: `chat`, see: [`support`] }))).toBeUndefined();
37
+ });
38
+
39
+ // Editing reaches only where they can look: everything needs the whole workspace, and each area named must be seen.
40
+ it(`keeps editing inside what they see`, () => {
41
+ expect(accessRefusal(access({ see: [`web`], edit: `all` }))).toContain(`whole workspace`);
42
+ expect(accessRefusal(access({ edit: `all` }))).toBeUndefined();
43
+ expect(accessRefusal(access({ see: [`web`], edit: [`web`, `finance`] }))).toContain(`finance`);
44
+ expect(accessRefusal(access({ edit: [`web`] }))).toBeUndefined();
45
+ expect(accessRefusal(access({ edit: [] }))).toContain(`at least one area`);
46
+ });
47
+ });
@@ -1,17 +1,134 @@
1
1
  // members: the daemon's shared-access roster (GET/POST/DELETE /members), who besides the owner reaches this sandbox
2
2
  import { z } from "zod";
3
- import { GrantedRoleSchema } from "./shared.js";
3
+ import type { CapabilityKind } from "./capabilities.js";
4
+ import { type GrantedRole, GrantedRoleSchema, MemberRoleSchema } from "./shared.js";
5
+
6
+ // WHAT A PERSON'S CONVERSATIONS MAY SPEND: the connected capabilities (by id) and the stored secrets (by name) an agent
7
+ // working for them is handed. Enforced by the daemon, not told to the model: a capability outside it is never mounted
8
+ // for their turns, its variables and skill are left out, and every exit that spends a credential (a `{{secret:…}}`
9
+ // reference, the credential gateway, an ssh signature, an account's password) refuses it with no card that could lift
10
+ // it (secrets/credential-reach.ts). A conversation reaches only what every person who has written in it reaches, so
11
+ // a person can never borrow more by replying in somebody else's.
12
+ export const CredentialReachSchema = z.object({
13
+ capabilities: z.array(z.string().min(1)).max(500).describe("Connected capability ids their conversations may use."),
14
+ secrets: z.array(z.string().min(1)).max(500).describe("Stored secret names (not a capability's own) their conversations may use."),
15
+ });
16
+ export type CredentialReach = z.infer<typeof CredentialReachSchema>;
17
+
18
+ // The capability kinds a reach can hold back, because each one reaches a turn only through something the daemon hands
19
+ // it per turn (a mount, a variable, a signature, a gateway request). The rest are the sandbox's own machinery with no
20
+ // value to withhold (Docker, a VPN once dialled, a mounted share, a local model, the wallet's owner-approved cards), so a
21
+ // reach neither lists nor removes them: a switch for one would be a line on a screen.
22
+ export const REACHABLE_CREDENTIAL_KINDS: readonly CapabilityKind[] = [
23
+ "cli",
24
+ "mcp",
25
+ "browser",
26
+ "identity",
27
+ "ssh",
28
+ "device",
29
+ "phone",
30
+ "webext",
31
+ "plugin",
32
+ "extension",
33
+ ];
34
+
35
+ // WHAT ONE PERSON MAY DO, as three independent answers rather than one rung of a ladder: whether their conversations
36
+ // run agents (`agents`), where they may look (`areas`, the fence their agents run inside too) and where they may change
37
+ // files by hand (`edit`), and whether they manage the sandbox (the maintainer tier, which implies every other answer).
38
+ // The `role` a row carries is the tier those answers make (`tierOf`), kept because the platform's invite record, the
39
+ // presence list and every route's floor read a tier; a row written before the answers existed reads as its role meant
40
+ // (`memberAccess`).
41
+ // `chat` is the guest's way of working: it talks to the assistants that work in its areas and is shown nothing else of
42
+ // the sandbox. `off` watches; `run` drives agents, whose work lands only when somebody who manages the sandbox lands it.
43
+ export const AgentAccessSchema = z.enum(["off", "chat", "run"]);
44
+ export type AgentAccess = z.infer<typeof AgentAccessSchema>;
45
+
46
+ // Where a person may change files by hand: every folder they may see, or the areas named (each one they may see too).
47
+ export const EditScopeSchema = z.union([z.literal("all"), z.array(z.string().min(1)).max(20)]);
48
+ export type EditScope = z.infer<typeof EditScopeSchema>;
49
+
50
+ /** A person's access resolved from their row: `see` undefined is the whole workspace, `edit` `none` changes nothing. */
51
+ export interface MemberAccess {
52
+ readonly manage: boolean;
53
+ readonly agents: AgentAccess;
54
+ readonly see: readonly string[] | undefined;
55
+ readonly edit: "none" | "all" | readonly string[];
56
+ }
57
+
58
+ // The fields of a row or a grant that decide a person's access.
59
+ interface AccessFields {
60
+ readonly role: GrantedRole;
61
+ readonly areas?: readonly string[] | undefined;
62
+ readonly agents?: AgentAccess | undefined;
63
+ readonly edit?: EditScope | undefined;
64
+ }
65
+
66
+ /** A row's access: what it says, and for a row written before `agents` and `edit` existed, what its role meant then. */
67
+ export const memberAccess = (row: AccessFields): MemberAccess => {
68
+ if (row.role === "maintainer") {
69
+ return { manage: true, agents: "run", see: undefined, edit: "all" };
70
+ }
71
+ const agents = row.agents ?? (row.role === "guest" ? "chat" : row.role === "viewer" ? "off" : "run");
72
+ // A writer from before `edit` changes files where it sees them, which its areas always named.
73
+ const edit = row.edit ?? (row.role === "writer" && row.areas !== undefined ? [...row.areas] : "none");
74
+ return { manage: false, agents, see: row.areas, edit: agents === "chat" ? "none" : edit };
75
+ };
76
+
77
+ /** The tier an access makes: what every route's floor, the platform's record and the presence list read. */
78
+ export const tierOf = (access: MemberAccess): GrantedRole => {
79
+ if (access.manage) {
80
+ return "maintainer";
81
+ }
82
+ if (access.agents === "chat") {
83
+ return "guest";
84
+ }
85
+ if (access.edit !== "none") {
86
+ return "writer";
87
+ }
88
+ return access.agents === "run" ? "collaborator" : "viewer";
89
+ };
90
+
91
+ /** Why an access cannot be granted on its face, before any area or assistant is looked up; undefined when it can. */
92
+ export const accessRefusal = (access: MemberAccess): string | undefined => {
93
+ if (access.manage) {
94
+ return undefined;
95
+ }
96
+ if (access.agents === "chat" && (access.see?.length ?? 0) === 0) {
97
+ return "chatting with assistants needs at least one area: the assistants that work there are the ones they talk to";
98
+ }
99
+ if (access.edit === "all") {
100
+ return access.see === undefined ? undefined : "editing everything they see needs the whole workspace; name the areas to edit instead";
101
+ }
102
+ if (access.edit === "none") {
103
+ return undefined;
104
+ }
105
+ if (access.edit.length === 0) {
106
+ return "editing needs at least one area to edit in";
107
+ }
108
+ const seen = access.see;
109
+ const unseen = seen === undefined ? [] : access.edit.filter((area) => !seen.includes(area));
110
+ return unseen.length === 0 ? undefined : `they can only edit where they can see: ${unseen.join(", ")} is not among their areas`;
111
+ };
4
112
 
5
113
  // One person on the roster as the daemon answers it. `areas` fences what of the workspace they reach, and with it which
6
114
  // assistants they may talk to: absent is the whole workspace, empty is nothing.
7
115
  export const MemberSchema = z.object({
8
116
  email: z.string().describe("Who, lowercased."),
9
- role: GrantedRoleSchema.describe("The tier they were granted."),
10
- areas: z.array(z.string()).optional().describe("The areas they are fenced to. Absent means the whole workspace."),
117
+ role: GrantedRoleSchema.describe("The tier their access makes: maintainer when they manage the sandbox, else what `agents` and `edit` add up to."),
118
+ areas: z.array(z.string()).optional().describe("The areas they may see, and their agents work in. Absent means the whole workspace."),
119
+ agents: AgentAccessSchema.optional().describe(
120
+ "Whether their conversations run agents: off (they watch), chat (only with the assistants in their areas, nothing else of the sandbox), or run. Absent means what their role meant.",
121
+ ),
122
+ edit: EditScopeSchema.optional().describe(
123
+ "Where they may change files by hand: everything they see, or these areas. Absent means nowhere, or for a writer its areas.",
124
+ ),
11
125
  emailSignIn: z
12
126
  .boolean()
13
127
  .optional()
14
128
  .describe("Whether they may sign in with an email code the platform confirmed, rather than only with Google or a passkey. Absent means no."),
129
+ credentials: CredentialReachSchema.optional().describe(
130
+ "The capabilities and secrets their conversations may spend. Absent means every one, including any connected later.",
131
+ ),
15
132
  });
16
133
  export type Member = z.infer<typeof MemberSchema>;
17
134
 
@@ -24,18 +141,36 @@ export type MembersList = z.infer<typeof MembersListSchema>;
24
141
  export const MembersRosterSchema = MembersListSchema.extend({ owner: z.string().optional() });
25
142
  export type MembersRoster = z.infer<typeof MembersRosterSchema>;
26
143
 
27
- // POST /members: a grant is a role decision, so the role is required. Areas are optional, their absence being the whole
28
- // workspace; a writer and a guest are refused without them, since for those two tiers the fence is the tier.
144
+ // POST /members: the role is required, the tier the grant makes. Areas are optional, their absence being the whole
145
+ // workspace; chatting with assistants needs them, since the assistants working there are the whole of that access.
29
146
  // `emailSignIn` lets the platform's identity assertion open this sandbox for them (policy/identity-assertion.ts): true
30
147
  // allows it, false takes it back, absent keeps what the row already says, so a re-grade from an editor that predates
31
- // the field never drops it.
148
+ // the field never drops it. `credentials` the same way: `all` lifts a reach, a list sets one, absent keeps the row's,
149
+ // so a re-grade that never mentions it can never widen what a person's conversations may spend. A maintainer is refused
150
+ // a list: that tier reads every credential.
151
+ // `agents` and `edit` decide the rest of the access; absent, the role's own meaning stands, so an editor that predates
152
+ // them still grants what it always did. The role sent must be the tier the access makes (`tierOf`): two answers to one
153
+ // question are refused rather than one of them guessed away.
32
154
  export const MemberGrantSchema = z.object({
33
155
  email: z.string(),
34
156
  role: GrantedRoleSchema,
35
157
  areas: z.array(z.string().min(1)).max(20).optional(),
158
+ agents: AgentAccessSchema.optional(),
159
+ edit: EditScopeSchema.optional(),
36
160
  emailSignIn: z.boolean().optional(),
161
+ credentials: z.union([z.literal("all"), CredentialReachSchema]).optional(),
37
162
  });
38
163
  export type MemberGrant = z.infer<typeof MemberGrantSchema>;
39
164
 
165
+ // GET /members/self: what the signed-in person may do here, for an editor that offers only what will be taken. The
166
+ // owner reads as managing everything.
167
+ export const MemberSelfSchema = z.object({
168
+ role: MemberRoleSchema.describe("Their tier on this sandbox."),
169
+ agents: AgentAccessSchema.describe("Whether their conversations run agents."),
170
+ areas: z.array(z.string()).optional().describe("The areas they may see. Absent means the whole workspace."),
171
+ edit: z.union([z.literal("none"), EditScopeSchema]).describe("Where they may change files by hand."),
172
+ });
173
+ export type MemberSelf = z.infer<typeof MemberSelfSchema>;
174
+
40
175
  // DELETE /members: whose access to take back.
41
176
  export const MemberEmailSchema = z.object({ email: z.string() });
@@ -20,7 +20,7 @@ export const OauthAccountSchema = z.object({
20
20
  .string()
21
21
  .optional()
22
22
  .describe(
23
- "Which of the provider's estates it signs in to, for a provider selling more than one (Z.ai's international or mainland plan). With the email and organisation, it is who the account is: a sign-in matching all three lands on this account again, same id.",
23
+ "Which of the provider's sign-ins it came from: the estate, for a provider selling more than one (Z.ai's international or mainland plan), or the loop's own door, for a loop that signs in with a program of its own (Grok Build's CLI beside Grok's OpenCode sign-in). With the email and organisation, it is who the account is: a sign-in matching all three lands on this account again, same id.",
24
24
  ),
25
25
  scope: z.string().optional().describe("What the credential is permitted to do, in the provider's terms."),
26
26
  connectedAt: z.number().describe("When it was connected, in milliseconds."),