@intentic/sandbox-contract 1.337.0 → 1.338.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 (263) hide show
  1. package/README.md +36 -0
  2. package/dist/chores/probes.d.ts.map +1 -1
  3. package/dist/chores/probes.js +4 -7
  4. package/dist/chores/probes.js.map +1 -1
  5. package/dist/contracts/agent.contract.d.ts +32 -23
  6. package/dist/contracts/agent.contract.d.ts.map +1 -1
  7. package/dist/contracts/agents.contract.d.ts +6016 -5994
  8. package/dist/contracts/agents.contract.d.ts.map +1 -1
  9. package/dist/contracts/agents.contract.js +3 -0
  10. package/dist/contracts/agents.contract.js.map +1 -1
  11. package/dist/contracts/approvals.contract.d.ts +8 -0
  12. package/dist/contracts/approvals.contract.d.ts.map +1 -1
  13. package/dist/contracts/automations.contract.d.ts +3 -3
  14. package/dist/contracts/capabilities.contract.d.ts +28 -0
  15. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  16. package/dist/contracts/capabilities.contract.js +11 -1
  17. package/dist/contracts/capabilities.contract.js.map +1 -1
  18. package/dist/contracts/device.contract.d.ts +225 -0
  19. package/dist/contracts/device.contract.d.ts.map +1 -1
  20. package/dist/contracts/device.contract.js +10 -0
  21. package/dist/contracts/device.contract.js.map +1 -1
  22. package/dist/contracts/extensions.contract.d.ts +16 -0
  23. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  24. package/dist/contracts/needs.contract.d.ts +22 -7
  25. package/dist/contracts/needs.contract.d.ts.map +1 -1
  26. package/dist/contracts/org.contract.d.ts +35 -0
  27. package/dist/contracts/org.contract.d.ts.map +1 -0
  28. package/dist/contracts/org.contract.js +14 -0
  29. package/dist/contracts/org.contract.js.map +1 -0
  30. package/dist/contracts/privacy.contract.d.ts +115 -0
  31. package/dist/contracts/privacy.contract.d.ts.map +1 -1
  32. package/dist/contracts/privacy.contract.js +11 -2
  33. package/dist/contracts/privacy.contract.js.map +1 -1
  34. package/dist/contracts/runner.contract.d.ts +11 -7
  35. package/dist/contracts/runner.contract.d.ts.map +1 -1
  36. package/dist/contracts/secrets.contract.d.ts +85 -0
  37. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  38. package/dist/contracts/secrets.contract.js +48 -1
  39. package/dist/contracts/secrets.contract.js.map +1 -1
  40. package/dist/contracts/sessions.contract.d.ts +4 -1
  41. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  42. package/dist/contracts/settings.contract.d.ts +11 -3
  43. package/dist/contracts/settings.contract.d.ts.map +1 -1
  44. package/dist/contracts/system.contract.d.ts +20 -2
  45. package/dist/contracts/system.contract.d.ts.map +1 -1
  46. package/dist/events/agent-events.d.ts +17 -4
  47. package/dist/events/agent-events.d.ts.map +1 -1
  48. package/dist/events/requests.d.ts +4 -0
  49. package/dist/events/requests.d.ts.map +1 -1
  50. package/dist/events/requests.js +1 -1
  51. package/dist/events/requests.js.map +1 -1
  52. package/dist/events/system-events.d.ts +4 -0
  53. package/dist/events/system-events.d.ts.map +1 -1
  54. package/dist/events/system-events.js +2 -0
  55. package/dist/events/system-events.js.map +1 -1
  56. package/dist/events/transcript.d.ts +27 -6
  57. package/dist/events/transcript.d.ts.map +1 -1
  58. package/dist/index.d.ts +7555 -7426
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +31 -257
  61. package/dist/index.js.map +1 -1
  62. package/dist/policy/identity-assertion.d.ts.map +1 -1
  63. package/dist/policy/identity-assertion.js +2 -8
  64. package/dist/policy/identity-assertion.js.map +1 -1
  65. package/dist/policy/keyring-seal.d.ts +48 -0
  66. package/dist/policy/keyring-seal.d.ts.map +1 -0
  67. package/dist/policy/keyring-seal.js +94 -0
  68. package/dist/policy/keyring-seal.js.map +1 -0
  69. package/dist/policy/message-limits.d.ts +2 -0
  70. package/dist/policy/message-limits.d.ts.map +1 -1
  71. package/dist/policy/message-limits.js +2 -0
  72. package/dist/policy/message-limits.js.map +1 -1
  73. package/dist/policy/org-locks.d.ts +58 -0
  74. package/dist/policy/org-locks.d.ts.map +1 -0
  75. package/dist/policy/org-locks.js +113 -0
  76. package/dist/policy/org-locks.js.map +1 -0
  77. package/dist/policy/reserved-servers.d.ts.map +1 -1
  78. package/dist/policy/reserved-servers.js +1 -1
  79. package/dist/policy/reserved-servers.js.map +1 -1
  80. package/dist/protocol/doors/webext-links.d.ts +3 -0
  81. package/dist/protocol/doors/webext-links.d.ts.map +1 -1
  82. package/dist/protocol/doors/webext-links.js +3 -0
  83. package/dist/protocol/doors/webext-links.js.map +1 -1
  84. package/dist/protocol/raw/raw-json-routes.d.ts +269 -0
  85. package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
  86. package/dist/protocol/raw/raw-json-routes.js +14 -1
  87. package/dist/protocol/raw/raw-json-routes.js.map +1 -1
  88. package/dist/protocol/raw/raw-routes.d.ts +70 -0
  89. package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
  90. package/dist/protocol/raw/raw-routes.js +18 -0
  91. package/dist/protocol/raw/raw-routes.js.map +1 -1
  92. package/dist/protocol/route-meta.d.ts.map +1 -1
  93. package/dist/protocol/route-meta.js.map +1 -1
  94. package/dist/protocol/routes.d.ts +4 -0
  95. package/dist/protocol/routes.d.ts.map +1 -1
  96. package/dist/protocol/routes.js +12 -0
  97. package/dist/protocol/routes.js.map +1 -1
  98. package/dist/schemas/agents.d.ts +1 -0
  99. package/dist/schemas/agents.d.ts.map +1 -1
  100. package/dist/schemas/agents.js +1 -0
  101. package/dist/schemas/agents.js.map +1 -1
  102. package/dist/schemas/approvals.d.ts +20 -0
  103. package/dist/schemas/approvals.d.ts.map +1 -1
  104. package/dist/schemas/approvals.js +10 -0
  105. package/dist/schemas/approvals.js.map +1 -1
  106. package/dist/schemas/capabilities.d.ts +26 -0
  107. package/dist/schemas/capabilities.d.ts.map +1 -1
  108. package/dist/schemas/capabilities.js +17 -0
  109. package/dist/schemas/capabilities.js.map +1 -1
  110. package/dist/schemas/data-protection.d.ts +5 -0
  111. package/dist/schemas/data-protection.d.ts.map +1 -1
  112. package/dist/schemas/data-protection.js +29 -0
  113. package/dist/schemas/data-protection.js.map +1 -1
  114. package/dist/schemas/devices.d.ts +11 -0
  115. package/dist/schemas/devices.d.ts.map +1 -1
  116. package/dist/schemas/devices.js +2 -1
  117. package/dist/schemas/devices.js.map +1 -1
  118. package/dist/schemas/extension-updates.d.ts +32 -0
  119. package/dist/schemas/extension-updates.d.ts.map +1 -1
  120. package/dist/schemas/git/git.d.ts +19 -0
  121. package/dist/schemas/git/git.d.ts.map +1 -1
  122. package/dist/schemas/git/git.js +12 -1
  123. package/dist/schemas/git/git.js.map +1 -1
  124. package/dist/schemas/hosts.d.ts +2 -0
  125. package/dist/schemas/hosts.d.ts.map +1 -1
  126. package/dist/schemas/hosts.js +11 -1
  127. package/dist/schemas/hosts.js.map +1 -1
  128. package/dist/schemas/keyring.d.ts +421 -0
  129. package/dist/schemas/keyring.d.ts.map +1 -0
  130. package/dist/schemas/keyring.js +176 -0
  131. package/dist/schemas/keyring.js.map +1 -0
  132. package/dist/schemas/links.d.ts +208 -0
  133. package/dist/schemas/links.d.ts.map +1 -0
  134. package/dist/schemas/links.js +116 -0
  135. package/dist/schemas/links.js.map +1 -0
  136. package/dist/schemas/maintenance.d.ts.map +1 -1
  137. package/dist/schemas/maintenance.js.map +1 -1
  138. package/dist/schemas/members.d.ts.map +1 -1
  139. package/dist/schemas/members.js +2 -0
  140. package/dist/schemas/members.js.map +1 -1
  141. package/dist/schemas/needs.d.ts +25 -8
  142. package/dist/schemas/needs.d.ts.map +1 -1
  143. package/dist/schemas/needs.js +15 -3
  144. package/dist/schemas/needs.js.map +1 -1
  145. package/dist/schemas/organisation.d.ts +106 -0
  146. package/dist/schemas/organisation.d.ts.map +1 -0
  147. package/dist/schemas/organisation.js +52 -0
  148. package/dist/schemas/organisation.js.map +1 -0
  149. package/dist/schemas/privacy.d.ts +232 -0
  150. package/dist/schemas/privacy.d.ts.map +1 -1
  151. package/dist/schemas/privacy.js +154 -4
  152. package/dist/schemas/privacy.js.map +1 -1
  153. package/dist/schemas/secrets.d.ts +151 -0
  154. package/dist/schemas/secrets.d.ts.map +1 -1
  155. package/dist/schemas/secrets.js +71 -8
  156. package/dist/schemas/secrets.js.map +1 -1
  157. package/dist/schemas/settings.d.ts +8 -0
  158. package/dist/schemas/settings.d.ts.map +1 -1
  159. package/dist/schemas/settings.js +4 -0
  160. package/dist/schemas/settings.js.map +1 -1
  161. package/dist/schemas/speech.d.ts +12 -0
  162. package/dist/schemas/speech.d.ts.map +1 -1
  163. package/dist/schemas/speech.js +23 -0
  164. package/dist/schemas/speech.js.map +1 -1
  165. package/dist/schemas/terminal.d.ts +15 -0
  166. package/dist/schemas/terminal.d.ts.map +1 -1
  167. package/dist/schemas/terminal.js +11 -2
  168. package/dist/schemas/terminal.js.map +1 -1
  169. package/dist/schemas/voice.d.ts +3 -0
  170. package/dist/schemas/voice.d.ts.map +1 -1
  171. package/dist/schemas/voice.js +7 -0
  172. package/dist/schemas/voice.js.map +1 -1
  173. package/dist/schemas/webext.d.ts +17 -0
  174. package/dist/schemas/webext.d.ts.map +1 -1
  175. package/dist/schemas/webext.js +7 -1
  176. package/dist/schemas/webext.js.map +1 -1
  177. package/dist/state/contract-lock.d.ts +3 -1
  178. package/dist/state/contract-lock.d.ts.map +1 -1
  179. package/dist/state/contract-lock.js +34 -9
  180. package/dist/state/contract-lock.js.map +1 -1
  181. package/dist/state/contract-modules.d.ts +4 -0
  182. package/dist/state/contract-modules.d.ts.map +1 -0
  183. package/dist/state/contract-modules.js +28 -0
  184. package/dist/state/contract-modules.js.map +1 -0
  185. package/dist/state/daemon-floor.d.ts +4 -0
  186. package/dist/state/daemon-floor.d.ts.map +1 -0
  187. package/dist/state/daemon-floor.js +8 -0
  188. package/dist/state/daemon-floor.js.map +1 -0
  189. package/dist/state/definition.d.ts +34 -0
  190. package/dist/state/definition.d.ts.map +1 -1
  191. package/dist/state/history-state.d.ts.map +1 -1
  192. package/dist/state/history-state.js +7 -0
  193. package/dist/state/history-state.js.map +1 -1
  194. package/dist/text/documents.d.ts +3 -1
  195. package/dist/text/documents.d.ts.map +1 -1
  196. package/dist/text/documents.js +30 -0
  197. package/dist/text/documents.js.map +1 -1
  198. package/dist/text/pages.d.ts.map +1 -1
  199. package/dist/text/pages.js +3 -7
  200. package/dist/text/pages.js.map +1 -1
  201. package/package.json +27 -5
  202. package/src/chores/probes.ts +4 -6
  203. package/src/contracts/agents.contract.ts +5 -0
  204. package/src/contracts/capabilities.contract.ts +14 -0
  205. package/src/contracts/device.contract.ts +30 -0
  206. package/src/contracts/org.contract.ts +19 -0
  207. package/src/contracts/privacy.contract.ts +13 -1
  208. package/src/contracts/secrets.contract.ts +64 -0
  209. package/src/events/requests.ts +3 -1
  210. package/src/events/system-events.ts +9 -0
  211. package/src/index.ts +41 -279
  212. package/src/policy/identity-assertion.ts +2 -8
  213. package/src/policy/keyring-seal.test.ts +93 -0
  214. package/src/policy/keyring-seal.ts +148 -0
  215. package/src/policy/message-limits.ts +3 -0
  216. package/src/policy/org-locks.ts +173 -0
  217. package/src/policy/reserved-servers.ts +3 -2
  218. package/src/policy/tests/org-locks.test.ts +92 -0
  219. package/src/policy/tests/reserved-servers.test.ts +3 -2
  220. package/src/protocol/doors/listener-protocol.ts +1 -1
  221. package/src/protocol/doors/webext-links.ts +13 -0
  222. package/src/protocol/ingress-contract.fixture.json +2 -0
  223. package/src/protocol/pairing-redeem.ts +1 -1
  224. package/src/protocol/peer-dial.test.ts +6 -6
  225. package/src/protocol/peer-mcp-server.test.ts +2 -1
  226. package/src/protocol/raw/raw-json-routes.ts +31 -1
  227. package/src/protocol/raw/raw-routes.ts +35 -1
  228. package/src/protocol/route-meta.ts +2 -1
  229. package/src/protocol/routes.test.ts +33 -1
  230. package/src/protocol/routes.ts +18 -0
  231. package/src/protocol/sse.ts +1 -1
  232. package/src/schemas/agent.ts +1 -1
  233. package/src/schemas/agents.ts +1 -0
  234. package/src/schemas/approvals.ts +14 -0
  235. package/src/schemas/capabilities.ts +25 -0
  236. package/src/schemas/data-protection.ts +47 -0
  237. package/src/schemas/devices.ts +8 -1
  238. package/src/schemas/git/git.ts +22 -2
  239. package/src/schemas/hosts.ts +14 -1
  240. package/src/schemas/keyring.ts +277 -0
  241. package/src/schemas/links.ts +180 -0
  242. package/src/schemas/maintenance.ts +1 -0
  243. package/src/schemas/members.ts +4 -0
  244. package/src/schemas/needs.ts +21 -5
  245. package/src/schemas/organisation.ts +110 -0
  246. package/src/schemas/privacy.test.ts +63 -0
  247. package/src/schemas/privacy.ts +213 -5
  248. package/src/schemas/secrets.ts +96 -8
  249. package/src/schemas/settings.ts +8 -0
  250. package/src/schemas/speech.ts +38 -1
  251. package/src/schemas/terminal.ts +13 -2
  252. package/src/schemas/voice.ts +11 -0
  253. package/src/schemas/webext.ts +16 -1
  254. package/src/state/contract-lock.test.ts +12 -12
  255. package/src/state/contract-lock.ts +43 -12
  256. package/src/state/contract-modules.ts +41 -0
  257. package/src/state/daemon-floor.test.ts +81 -0
  258. package/src/state/daemon-floor.ts +32 -0
  259. package/src/state/history-state.ts +14 -0
  260. package/src/state/workspace-state.ts +1 -1
  261. package/src/text/documents.test.ts +30 -1
  262. package/src/text/documents.ts +43 -1
  263. package/src/text/pages.ts +5 -7
@@ -0,0 +1,110 @@
1
+ import { z } from "zod";
2
+ import { PRESET_GROUPS, type PresetGroup, PresetGroupSchema } from "./compliance.js";
3
+
4
+ // An ORGANISATION'S POLICY as a sandbox holds it (docs/architecture/organisations.md). A sandbox that belongs to an
5
+ // organisation is handed this by the platform it is registered with; a personal sandbox never holds one, and nothing
6
+ // here applies to it. The daemon is the enforcing copy: a section the policy locks is refused at the daemon's own
7
+ // middleware for every caller, the owner and the agent included, and the editor only draws what the daemon says.
8
+
9
+ // The parts of a sandbox an organisation can take out of its members' hands. Each is a section of the editor's sandbox
10
+ // hub (sandboxNav.ts) except `updates`, which is the auto-update switch on Overview. Which daemon routes each one
11
+ // covers is policy/org-locks.ts, beside the refusal that reads it.
12
+ export const ORG_SECTIONS = [
13
+ "models",
14
+ "agent",
15
+ "environment",
16
+ "secrets",
17
+ "data-protection",
18
+ "extensions",
19
+ "access",
20
+ "areas",
21
+ "personas",
22
+ "updates",
23
+ ] as const;
24
+ export const OrgSectionSchema = z.enum(ORG_SECTIONS);
25
+ export type OrgSection = z.infer<typeof OrgSectionSchema>;
26
+
27
+ // `open`: as in a personal sandbox. `locked`: readable, every change refused. `hidden`: locked, and the editor does not
28
+ // show the section at all. Hidden is a fence on what people are shown, not on what the sandbox reads: the agent still
29
+ // uses the secrets of a hidden Secrets section, and a read route answers as it always did.
30
+ export const OrgSectionModeSchema = z.enum(["open", "locked", "hidden"]);
31
+ export type OrgSectionMode = z.infer<typeof OrgSectionModeSchema>;
32
+
33
+ // What an organisation admin edits. A section left out is open, so a policy written before a section existed opens it
34
+ // rather than guessing.
35
+ //
36
+ // `preset` PINS settings rather than locking a page: each named group of the public-office preset (compliance.ts
37
+ // PRESET_GROUPS; which settings a group holds is the daemon's compliance/preset.ts) is applied to what the sandbox READS,
38
+ // whatever its files say, and a change that would undo it is refused. Strings on the wire, not the enum: a group a newer
39
+ // platform names and this daemon does not know is skipped and reported (`unenforced`), never a reason to refuse the
40
+ // whole policy, which would stop every later edition reaching an older daemon. The platform validates the names an
41
+ // admin saves.
42
+ //
43
+ // `membersOnly` makes the organisation's member list the outer fence of every bound sandbox's roster: a person the
44
+ // organisation removes is taken off each sandbox's roster by its daemon at the next fetch, and a grant to someone who
45
+ // is not a member is refused. The sandbox's owner is not on a roster, so this never reaches them.
46
+ //
47
+ // Both are optional rather than defaulted, so every policy written before them (a stored row, a held file, a literal)
48
+ // still reads as the same type: absent is no pin and an open roster.
49
+ export const OrgPolicyRulesSchema = z.object({
50
+ sections: z
51
+ .partialRecord(OrgSectionSchema, OrgSectionModeSchema)
52
+ .default({})
53
+ .describe("How each section is held. A section left out is open."),
54
+ preset: z
55
+ .array(z.string().min(1).max(40))
56
+ .max(40)
57
+ .optional()
58
+ .describe(`Public-office preset groups every bound sandbox is held to (${PRESET_GROUPS.join(", ")}). Unknown names are skipped.`),
59
+ membersOnly: z
60
+ .boolean()
61
+ .optional()
62
+ .describe("Only the organisation's members may be on a bound sandbox's roster; removing someone from the organisation removes them everywhere."),
63
+ });
64
+ export type OrgPolicyRules = z.infer<typeof OrgPolicyRulesSchema>;
65
+
66
+ // The rules as one sandbox receives them: whose they are and which edition. `version` only grows on the platform; the
67
+ // daemon reports the edition it holds back, which is how the organisation's console tells applied from pending.
68
+ // `people` is computed by the platform, never edited: the organisation's members, each as the sha256 of
69
+ // orgPersonPreimage, sent only while `membersOnly` is on, so a daemon can test an address without being handed the
70
+ // staff list in clear. A policy with `membersOnly` and no `people` fences nothing: no list is not an empty list.
71
+ export const OrgPolicySchema = OrgPolicyRulesSchema.extend({
72
+ orgId: z.string().min(1).max(64),
73
+ orgName: z.string().min(1).max(120),
74
+ version: z.number().int().min(1),
75
+ people: z.array(z.string().regex(/^[0-9a-f]{64}$/u)).max(10_000).optional(),
76
+ });
77
+ export type OrgPolicy = z.infer<typeof OrgPolicySchema>;
78
+
79
+ // What the daemon answers the editor about it (GET /org/policy): unbound, or bound with the policy it enforces and
80
+ // when it last heard from the platform about it. `provisional` is a held file that could not be read: every section is
81
+ // held read-only until the platform answers, since a bound sandbox must not open up because its record broke.
82
+ // `unenforced` names preset groups the policy pins that this daemon does not know.
83
+ export const OrgPolicyStateSchema = z.discriminatedUnion("bound", [
84
+ z.object({ bound: z.literal(false) }),
85
+ z.object({
86
+ bound: z.literal(true),
87
+ policy: OrgPolicySchema,
88
+ receivedAt: z.string().describe("When this edition arrived (ISO)."),
89
+ provisional: z.boolean().optional(),
90
+ unenforced: z.array(z.string()).optional(),
91
+ }),
92
+ ]);
93
+ export type OrgPolicyState = z.infer<typeof OrgPolicyStateSchema>;
94
+
95
+ // How a policy holds one section; anything it does not name is open.
96
+ export const orgSectionMode = (policy: Pick<OrgPolicy, "sections"> | undefined, section: OrgSection): OrgSectionMode =>
97
+ policy?.sections[section] ?? "open";
98
+
99
+ // The preset groups a policy pins that this build knows, in the contract's order; none for a personal sandbox. A policy
100
+ // stored before pins existed has no `preset` at all.
101
+ export const orgPinnedGroups = (policy: Partial<Pick<OrgPolicy, "preset">> | undefined): PresetGroup[] =>
102
+ PRESET_GROUPS.filter((group) => policy?.preset?.includes(group) === true);
103
+
104
+ // The names a policy pins that this build does not know.
105
+ export const orgUnknownPins = (policy: Partial<Pick<OrgPolicy, "preset">> | undefined): string[] =>
106
+ (policy?.preset ?? []).filter((name) => !PresetGroupSchema.safeParse(name).success);
107
+
108
+ // What both sides hash (sha256, hex) to name one member of one organisation in `people`: salted with the organisation's
109
+ // id, so the same address in two organisations does not read as the same digest.
110
+ export const orgPersonPreimage = (orgId: string, email: string): string => `intentic-org-person:${orgId}:${email.trim().toLowerCase()}`;
@@ -0,0 +1,63 @@
1
+ import { DEFAULT_PRIVACY_SHIELD, PRIVACY_PATTERN_MAX, PrivacyShieldPolicySchema, patternProblem } from "./privacy.js";
2
+
3
+ // The owner's own patterns run on every string bound for an untrusted provider, in the daemon's one thread: one that
4
+ // cannot compile, matches empty text or nests a repeat inside a repeat is refused before it is saved.
5
+
6
+ describe(`patternProblem`, () => {
7
+ it(`takes the patterns an ID or a customer number is written with`, () => {
8
+ for (const pattern of [
9
+ String.raw`EMP-\d{6}`,
10
+ String.raw`\b[A-Z]{3}\d{6}\b`,
11
+ String.raw`Customer:\s*(?<value>\d+)`,
12
+ String.raw`(?:\d{3}-){2}\d{4}`,
13
+ String.raw`[A-Z]{2}\d{2}(?:[ -]?\d{4}){3}`,
14
+ String.raw`(?<=Ref\. )[A-Z0-9]+`,
15
+ String.raw`(ab|cd)+`,
16
+ String.raw`[(+*)]+x`,
17
+ ]) {
18
+ expect([pattern, patternProblem(pattern)]).toEqual([pattern, undefined]);
19
+ }
20
+ });
21
+
22
+ it(`refuses one that does not compile, matches empty text or is too long`, () => {
23
+ expect(patternProblem(`(unclosed`)).toBe(`invalid`);
24
+ expect(patternProblem(String.raw`\p{Nope}`)).toBe(`invalid`);
25
+ expect(patternProblem(String.raw`\d*`)).toBe(`empty`);
26
+ expect(patternProblem(`a|`)).toBe(`empty`);
27
+ expect(patternProblem(` `)).toBe(`empty`);
28
+ expect(patternProblem(`a`.repeat(PRIVACY_PATTERN_MAX + 1))).toBe(`tooLong`);
29
+ });
30
+
31
+ it(`refuses a variable repeat inside a variable repeat, the shape that backtracks for minutes`, () => {
32
+ expect(patternProblem(`(a+)+b`)).toBe(`nested`);
33
+ expect(patternProblem(String.raw`(\w+\s?)*!`)).toBe(`nested`);
34
+ expect(patternProblem(String.raw`(?:x(?:\d{2,})?)+y`)).toBe(`nested`);
35
+ expect(patternProblem(String.raw`(?<id>\d+-?){2,}z`)).toBe(`nested`);
36
+ // A fixed count is no variable repeat, inside or out.
37
+ expect(patternProblem(String.raw`(\d{3}-?){3}`)).toBeUndefined();
38
+ expect(patternProblem(String.raw`(\d{3}){2,}`)).toBeUndefined();
39
+ });
40
+ });
41
+
42
+ describe(`PrivacyShieldPolicySchema`, () => {
43
+ it(`reads a policy written before the owner's lists and rules as having none`, () => {
44
+ expect(PrivacyShieldPolicySchema.parse({ mode: `on` })).toMatchObject({ dictionary: { off: [], lists: [] }, rules: [] });
45
+ expect(DEFAULT_PRIVACY_SHIELD.rules).toEqual([]);
46
+ });
47
+
48
+ it(`fills a rule's defaults and keeps a stored pattern whatever a later build judges of it`, () => {
49
+ const parsed = PrivacyShieldPolicySchema.parse({
50
+ rules: [{ id: `r1`, title: `Nested`, class: `identity-document`, match: `pattern`, pattern: `(a+)+` }],
51
+ });
52
+ expect(parsed.rules[0]).toEqual({
53
+ id: `r1`,
54
+ title: `Nested`,
55
+ on: true,
56
+ class: `identity-document`,
57
+ match: `pattern`,
58
+ terms: [],
59
+ pattern: `(a+)+`,
60
+ matchCase: false,
61
+ });
62
+ });
63
+ });
@@ -44,6 +44,183 @@ export const PRIVACY_TRUSTED_MAX = 200;
44
44
  export const PRIVACY_ALLOW_MAX = 1000;
45
45
  export const PRIVACY_CONVERSATIONS_MAX = 200;
46
46
 
47
+ // The owner's own additions to what the shield finds. The policy travels whole on every write, so each is kept to what a
48
+ // person types or pastes; a dataset of thousands of values is taught to the vault instead (privacy learn).
49
+ export const PRIVACY_OWN_LISTS_MAX = 20;
50
+ export const PRIVACY_OWN_LIST_WORDS_MAX = 2000;
51
+ export const PRIVACY_WORD_MAX = 60;
52
+ export const PRIVACY_RULES_MAX = 50;
53
+ export const PRIVACY_RULE_TERMS_MAX = 1000;
54
+ export const PRIVACY_TERM_MAX = 200;
55
+ export const PRIVACY_PATTERN_MAX = 500;
56
+ export const PRIVACY_TITLE_MAX = 80;
57
+
58
+ // What a word on a name list says about a name: a first name or a surname (a name when the two stand together), a word
59
+ // that is a name only beside other evidence, a title after which a capitalized word is a person, or never part of one.
60
+ export const PrivacyNameListKindSchema = z.enum(["first-name", "surname", "ambiguous", "title", "never"]);
61
+ export type PrivacyNameListKind = z.infer<typeof PrivacyNameListKindSchema>;
62
+
63
+ // inflected: every Polish grammatical form of a listed word ("Kasia", "Kasi", "Kasię"); as-written: only as listed.
64
+ export const PrivacyNameMatchingSchema = z.enum(["inflected", "as-written"]);
65
+ export type PrivacyNameMatching = z.infer<typeof PrivacyNameMatchingSchema>;
66
+
67
+ // Ids the editor mints for the owner's lists and rules: stable across renames, so a write never confuses two of them.
68
+ const OwnIdSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,39}$/u);
69
+
70
+ export const PrivacyOwnNameListSchema = z.object({
71
+ id: OwnIdSchema,
72
+ title: z.string().min(1).max(PRIVACY_TITLE_MAX),
73
+ kind: PrivacyNameListKindSchema,
74
+ matching: PrivacyNameMatchingSchema.default("as-written"),
75
+ words: z
76
+ .array(z.string().min(1).max(PRIVACY_WORD_MAX))
77
+ .max(PRIVACY_OWN_LIST_WORDS_MAX)
78
+ .default([])
79
+ .describe("One word each, in any case: the list is read as the built-in lists are."),
80
+ on: z.boolean().default(true),
81
+ });
82
+ export type PrivacyOwnNameList = z.infer<typeof PrivacyOwnNameListSchema>;
83
+
84
+ export const PrivacyDictionaryPolicySchema = z.object({
85
+ off: z.array(z.string().min(1).max(64)).max(64).default([]).describe("Built-in name lists switched off, by id (privacy.dictionary names them)."),
86
+ lists: z
87
+ .array(PrivacyOwnNameListSchema)
88
+ .max(PRIVACY_OWN_LISTS_MAX)
89
+ .default([])
90
+ .describe("The owner's own name lists, read beside the built-in ones."),
91
+ });
92
+ export type PrivacyDictionaryPolicy = z.infer<typeof PrivacyDictionaryPolicySchema>;
93
+
94
+ // One of the owner's own rules: what it matches is masked wherever it stands, with no context asked for, as the kind it
95
+ // names. terms: each word or phrase as a whole word, in any case unless matchCase. pattern: a regular expression (with the
96
+ // `u` flag, `i` unless matchCase); a group named `value` masks only that part of a match.
97
+ export const PrivacyRuleMatchSchema = z.enum(["terms", "pattern"]);
98
+ export type PrivacyRuleMatch = z.infer<typeof PrivacyRuleMatchSchema>;
99
+
100
+ export const PrivacyRuleSchema = z.object({
101
+ id: OwnIdSchema,
102
+ title: z.string().min(1).max(PRIVACY_TITLE_MAX),
103
+ on: z.boolean().default(true),
104
+ class: PersonalDataClassSchema.describe("The kind what it matches is masked as; switching that kind off stops the rule too."),
105
+ match: PrivacyRuleMatchSchema,
106
+ terms: z.array(z.string().min(1).max(PRIVACY_TERM_MAX)).max(PRIVACY_RULE_TERMS_MAX).default([]),
107
+ // Only its size is held here: whether a pattern is safe to run is judged when it is saved (patternProblem), so a
108
+ // stored one never turns unreadable because a later build judges more strictly.
109
+ pattern: z.string().max(PRIVACY_PATTERN_MAX).default(""),
110
+ matchCase: z.boolean().default(false),
111
+ });
112
+ export type PrivacyRule = z.infer<typeof PrivacyRuleSchema>;
113
+
114
+ // Why a pattern cannot be saved. Owner-written patterns run on every string bound for an untrusted provider, in the
115
+ // daemon's one thread, so beside not compiling: one that matches empty text masks nothing and only costs, and a repeat
116
+ // inside a repeat (`(a+)+`, `(\w+\s?)*`) can take minutes on one line that almost matches.
117
+ export type PrivacyPatternProblem = "invalid" | "tooLong" | "empty" | "nested";
118
+
119
+ // The length of a quantifier at `at`, and whether it repeats a variable number of times (`*`, `+`, `{2,}`, `{2,5}`); `?`
120
+ // and `{3}` do not. A lazy `?` after it is part of it.
121
+ const quantifierAt = (source: string, at: number): { readonly length: number; readonly varies: boolean } | undefined => {
122
+ const char = source[at];
123
+ let length: number;
124
+ let varies: boolean;
125
+ if (char === "*" || char === "+" || char === "?") {
126
+ length = 1;
127
+ varies = char !== "?";
128
+ } else if (char === "{") {
129
+ const braces = /^\{(\d+)(?:(,)(\d*))?\}/u.exec(source.slice(at));
130
+ if (braces === null) {
131
+ return undefined;
132
+ }
133
+ const [whole, min = "0", comma, max] = braces;
134
+ length = whole.length;
135
+ varies = comma !== undefined && (max === undefined || max === "" || Number(max) > Number(min));
136
+ } else {
137
+ return undefined;
138
+ }
139
+ return { length: source[at + length] === "?" ? length + 1 : length, varies };
140
+ };
141
+
142
+ // One step of the walk below: where the next one starts, and what this one was.
143
+ interface PatternStep {
144
+ readonly next: number;
145
+ readonly kind: "open" | "close" | "repeat" | "other";
146
+ }
147
+
148
+ // Past a character class, escapes inside it included.
149
+ const classEnd = (source: string, at: number): number => {
150
+ let end = at + 1;
151
+ while (end < source.length && source[end] !== "]") {
152
+ end += source[end] === "\\" ? 2 : 1;
153
+ }
154
+ return end + 1;
155
+ };
156
+
157
+ // Past a group's opening and its own prefix (`?:`, `?=`, `?<name>`), which is no quantifier.
158
+ const groupStart = (source: string, at: number): number => {
159
+ if (source[at + 1] !== "?") {
160
+ return at + 1;
161
+ }
162
+ const name = /^\?<(?![=!])[^>]*>/u.exec(source.slice(at + 1));
163
+ return at + 1 + (name === null ? 2 : name[0].length);
164
+ };
165
+
166
+ const stepAt = (source: string, at: number): PatternStep => {
167
+ const char = source[at];
168
+ if (char === "\\") {
169
+ return { next: at + 2, kind: "other" };
170
+ }
171
+ if (char === "[") {
172
+ return { next: classEnd(source, at), kind: "other" };
173
+ }
174
+ if (char === "(") {
175
+ return { next: groupStart(source, at), kind: "open" };
176
+ }
177
+ if (char === ")") {
178
+ return { next: at + 1, kind: "close" };
179
+ }
180
+ const quantifier = quantifierAt(source, at);
181
+ return quantifier === undefined
182
+ ? { next: at + 1, kind: "other" }
183
+ : { next: at + quantifier.length, kind: quantifier.varies ? "repeat" : "other" };
184
+ };
185
+
186
+ // Whether a group that repeats a variable number of times holds such a repeat itself: a star height above one.
187
+ const nestedRepeat = (source: string): boolean => {
188
+ // Per open group, whether something inside it repeats.
189
+ const open: boolean[] = [];
190
+ // Whether the step before was the close of a group that held a repeat.
191
+ let closedRepeating = false;
192
+ for (let at = 0; at < source.length;) {
193
+ const step = stepAt(source, at);
194
+ at = step.next;
195
+ if (step.kind === "open") {
196
+ open.push(false);
197
+ } else if (step.kind === "repeat") {
198
+ if (closedRepeating) {
199
+ return true;
200
+ }
201
+ open.fill(true);
202
+ }
203
+ closedRepeating = step.kind === "close" && (open.pop() ?? false);
204
+ }
205
+ return false;
206
+ };
207
+
208
+ export const patternProblem = (source: string): PrivacyPatternProblem | undefined => {
209
+ if (source.length > PRIVACY_PATTERN_MAX) {
210
+ return "tooLong";
211
+ }
212
+ let compiled: RegExp;
213
+ try {
214
+ compiled = new RegExp(source, "u");
215
+ } catch {
216
+ return "invalid";
217
+ }
218
+ if (source.trim() === "" || compiled.test("")) {
219
+ return "empty";
220
+ }
221
+ return nestedRepeat(source) ? "nested" : undefined;
222
+ };
223
+
47
224
  // One conversation the owner let an untrusted provider read as it is, granted from that conversation's own composer:
48
225
  // narrower than the trusted list, and the way on for a provider whose runtime the gateway cannot sit in front of
49
226
  // (Cursor's own wire, an ACP agent, Pi), which the shield otherwise refuses outright.
@@ -78,6 +255,14 @@ export const PrivacyShieldPolicySchema = z.object({
78
255
  .max(PRIVACY_CONVERSATIONS_MAX)
79
256
  .default([])
80
257
  .describe("Providers that may read one conversation's personal data as it is, each granted from that conversation; oldest first."),
258
+ dictionary: PrivacyDictionaryPolicySchema.default({ off: [], lists: [] }).describe(
259
+ "The name dictionary as the owner adjusted it: built-in lists switched off, and lists of their own.",
260
+ ),
261
+ rules: z
262
+ .array(PrivacyRuleSchema)
263
+ .max(PRIVACY_RULES_MAX)
264
+ .default([])
265
+ .describe("The owner's own rules: words, phrases and patterns masked wherever they stand."),
81
266
  });
82
267
  export type PrivacyShieldPolicy = z.infer<typeof PrivacyShieldPolicySchema>;
83
268
 
@@ -204,18 +389,21 @@ export const PrivacyKnownSourceSchema = z.object({
204
389
  export type PrivacyKnownSource = z.infer<typeof PrivacyKnownSourceSchema>;
205
390
 
206
391
  // The name lists the shield finds names by when no model is asked for (and alongside one when it is): what each list
207
- // holds, how many, and where it came from.
392
+ // holds, how many, and where it came from; the owner's own lists among them.
208
393
  export const PrivacyNameListSchema = z.object({
209
394
  id: z.string().describe("Stable id of the list."),
210
- kind: z.enum(["first-name", "surname", "ambiguous", "title", "never"]).describe("What a word on it says about a name."),
395
+ kind: PrivacyNameListKindSchema.describe("What a word on it says about a name."),
211
396
  languages: z.array(z.enum(["pl", "en", "multi"])).describe("The languages its words come from; multi: many languages at once."),
212
397
  count: z.number().int().describe("How many words it holds."),
213
- matching: z
214
- .enum(["inflected", "as-written"])
215
- .describe("inflected: matched in every grammatical form of a listed word; as-written: matched only exactly as listed."),
398
+ matching: PrivacyNameMatchingSchema.describe(
399
+ "inflected: matched in every grammatical form of a listed word; as-written: matched only exactly as listed.",
400
+ ),
216
401
  source: z.string().describe("Where the words come from: the register or dataset, or that they were written by hand."),
217
402
  url: z.string().optional().describe("The source's page, where it has one."),
218
403
  license: z.string().optional(),
404
+ on: z.boolean().describe("Read by the shield; the owner can switch any list off."),
405
+ own: z.boolean().optional().describe("One of the owner's own lists (policy.dictionary.lists), titled by them."),
406
+ title: z.string().optional().describe("An own list's title."),
219
407
  });
220
408
  export type PrivacyNameList = z.infer<typeof PrivacyNameListSchema>;
221
409
 
@@ -255,3 +443,23 @@ export const PrivacyDictionarySchema = z.object({
255
443
  lookup: PrivacyNameLookupSchema.optional(),
256
444
  });
257
445
  export type PrivacyDictionary = z.infer<typeof PrivacyDictionarySchema>;
446
+
447
+ // What the shield would mask in a text the owner tries it on, with the policy as saved: where each find sits, its kind,
448
+ // and what found it. Nothing is given a token and nothing is kept, so trying a value teaches the vault nothing.
449
+ export const PRIVACY_PREVIEW_MAX = 4000;
450
+
451
+ export const PrivacyPreviewSpanSchema = z.object({
452
+ start: z.number().int(),
453
+ end: z.number().int().describe("Exclusive."),
454
+ class: PersonalDataClassSchema,
455
+ by: z
456
+ .enum(["detector", "rule", "learned", "model"])
457
+ .describe(
458
+ "detector: the built-in detectors and name lists, the owner's lists included; rule: one of the owner's rules; learned: a value the vault knows; model: the local name model.",
459
+ ),
460
+ rule: z.string().optional().describe("The id of the owner's rule that found it."),
461
+ });
462
+ export type PrivacyPreviewSpan = z.infer<typeof PrivacyPreviewSpanSchema>;
463
+
464
+ export const PrivacyPreviewSchema = z.object({ spans: z.array(PrivacyPreviewSpanSchema) });
465
+ export type PrivacyPreview = z.infer<typeof PrivacyPreviewSchema>;
@@ -1,6 +1,7 @@
1
1
  // secrets: user-supplied env-var secrets the daemon writes to desired-state/.env
2
2
  import { BrokerRuleSchema } from "@intentic/extension-manifest";
3
3
  import { z } from "zod";
4
+ import { KEYRING_PROGRAMS_MAX, KeyringPackageSchema, KeyringProgramSchema, KeyringShareModeSchema, KeyringSourceSchema } from "./keyring.js";
4
5
  import { SECRET_HOST_MAX, SECRET_HOST_PATTERN_RE, SECRET_HOSTS_MAX } from "../policy/secret-hosts.js";
5
6
  // Straight to the sandbox daemon, never through the platform; `apply` reloads .env with no restart. `list` returns keys
6
7
  // only; `reveal` is the one owner-only exception that returns a value.
@@ -61,9 +62,9 @@ export const CredentialGateKindSchema = z
61
62
  export type CredentialGateKind = z.infer<typeof CredentialGateKindSchema>;
62
63
 
63
64
  export const CredentialLaneSchema = z
64
- .enum(["shell", "code", "browser", "session", "otp", "gateway", "ssh"])
65
+ .enum(["shell", "code", "browser", "session", "otp", "gateway", "ssh", "device", "desktop"])
65
66
  .describe(
66
- "What the credential was about to be used for: a shell command, a script, typing into a page, mounting a connected account, one one-time code, a request the credential gateway attaches it to, or a signature the sandbox's ssh agent makes with a held key.",
67
+ "What the credential was about to be used for: a shell command, a script, typing into a page, mounting a connected account, one one-time code, a request the credential gateway attaches it to, a signature the sandbox's ssh agent makes with a held key, typing into a window or phone app on one of the owner's computers, or typing into a window on the sandbox's own desktop.",
67
68
  );
68
69
  export type CredentialLane = z.infer<typeof CredentialLaneSchema>;
69
70
 
@@ -121,9 +122,9 @@ export const SecretHostSchema = z
121
122
  );
122
123
 
123
124
  export const SecretHostSourceSchema = z
124
- .enum(["owner", "connector"])
125
+ .enum(["owner", "connector", "keyring"])
125
126
  .describe(
126
- "Who set it: the owner, or the connector the credential belongs to, whose guard is on with its own service's hosts until the owner changes it.",
127
+ "Who set it: the owner, the connector the credential belongs to (whose guard is on with its own service's hosts until the owner changes it), or the keyring on the computer that holds the secret, whose list is changed there.",
127
128
  );
128
129
  export type SecretHostSource = z.infer<typeof SecretHostSourceSchema>;
129
130
 
@@ -208,12 +209,84 @@ export const CredentialPolicySetSchema = z.object({
208
209
  });
209
210
  export type CredentialPolicySet = z.infer<typeof CredentialPolicySetSchema>;
210
211
 
212
+ // The program guard: the host guard's counterpart for typing into windows. A sandbox-held secret typed into a window on
213
+ // one of the owner's computers goes unasked only into a program on its list and, unless `anyField`, only into a field the
214
+ // program marks as a password field; anywhere else it waits for a person. A keyring entry carries its own list, kept on
215
+ // that computer (schemas/keyring.ts), which the sandbox can show and never widen.
216
+ export const SecretProgramsSchema = z.object({
217
+ programs: z.array(KeyringProgramSchema).max(KEYRING_PROGRAMS_MAX).describe("Programs, by executable name or whole path, it may be typed into unasked."),
218
+ packages: z.array(KeyringPackageSchema).max(KEYRING_PROGRAMS_MAX).describe("Android apps, by package name, it may be typed into unasked."),
219
+ anyField: z.boolean().describe("Whether it may go into a field not marked as a password field without asking."),
220
+ source: z.enum(["owner", "keyring"]).describe("Who keeps the list: the owner, here, or the keyring on the computer that holds the secret."),
221
+ });
222
+ export type SecretPrograms = z.infer<typeof SecretProgramsSchema>;
223
+
224
+ export const SecretProgramGuardSchema = SecretProgramsSchema.omit({ source: true }).extend({
225
+ subject: z.string().min(1).describe("Which secret, by the name its reference carries."),
226
+ });
227
+ export type SecretProgramGuard = z.infer<typeof SecretProgramGuardSchema>;
228
+
229
+ export const SecretProgramGuardsSchema = z.object({
230
+ guards: z.array(SecretProgramGuardSchema).describe("Every sandbox-held secret whose program list has been set. Names and programs only, never a value."),
231
+ });
232
+
233
+ export const SecretProgramGuardSetSchema = SecretProgramGuardSchema.extend({
234
+ conversationId: z.string().optional().describe("Which conversation to ask the owner in, when the change needs them. The CLI fills this from the running turn."),
235
+ });
236
+
237
+ export const SecretProgramGuardSetResultSchema = SecretProgramsSchema.omit({ source: true }).extend({
238
+ approvedBy: z.string().optional().describe("Who approved the change, when it needed the owner's click. Absent when nobody had to."),
239
+ });
240
+
241
+ // The owner's Type into…: one secret typed into whatever has focus on one of their computers, after a countdown that
242
+ // leaves time to put the cursor there. Through that computer's keyring, which still asks when its entry's mode says to.
243
+ export const SecretTypeIntoSchema = z.object({
244
+ key: secretKey.describe("Which secret, by name."),
245
+ device: z.string().min(1).describe("Which computer, by its device card (`rog`, or `rog::wsl:archlinux` for a distro's own keyring)."),
246
+ countdownSeconds: z.number().int().min(0).max(10).default(3).describe("How long to wait before typing, for the cursor to be put in place."),
247
+ });
248
+ export const SecretTypeIntoResultSchema = z.object({
249
+ program: z.string().describe("What it was typed into."),
250
+ field: z.enum(["password", "text"]).describe("Whether the focused field was a password field."),
251
+ asked: z.boolean().describe("Whether the person at the computer was asked."),
252
+ });
253
+
254
+ // The keyrings on the owner's computers this sandbox can reach: where a secret can be kept or typed, and which two can be
255
+ // paired to keep one set of entries.
256
+ export const SandboxKeyringSchema = z.object({
257
+ machine: z.string().describe("The computer, by its device card."),
258
+ online: z.boolean().describe("Whether it answers now."),
259
+ locked: z.boolean().optional().describe("Whether its keyring is locked, as last heard. Absent while offline."),
260
+ version: z.string().optional().describe("The keyring's release, as last heard."),
261
+ pendingInbox: z.number().int().nonnegative().optional().describe("Values sent to be kept there that wait for its next unlock."),
262
+ shared: z.number().int().nonnegative().describe("How many of its entries are shared with this sandbox."),
263
+ sync: z
264
+ .object({
265
+ fingerprint: z.string().describe("Its sync key's fingerprint, to compare with what the other computer shows when pairing."),
266
+ peers: z.array(z.string()).describe("The computers it is paired with, by device card."),
267
+ })
268
+ .optional()
269
+ .describe("Its pairing with the owner's other keyrings. Absent while offline or before it was heard."),
270
+ });
271
+ export type SandboxKeyring = z.infer<typeof SandboxKeyringSchema>;
272
+ export const SandboxKeyringsSchema = z.object({ keyrings: z.array(SandboxKeyringSchema) });
273
+
274
+ export const SandboxKeyringPairSchema = z.object({
275
+ a: z.string().min(1).describe("One computer, by its device card."),
276
+ b: z.string().min(1).describe("The other."),
277
+ });
278
+ export const SandboxKeyringPairResultSchema = z.object({
279
+ fingerprints: z.object({ a: z.string(), b: z.string() }).describe("Each keyring's sync-key fingerprint, as both prompts showed them."),
280
+ });
281
+
211
282
  export const SecretInventoryEntrySchema = z.object({
212
283
  // Env-var key for env|generated; `<provider>:<accountId>` for provider; capability instance id otherwise.
213
284
  key: z.string().describe("What identifies it. Unique across the whole inventory, so several accounts of one provider each get their own entry."),
214
285
  kind: z
215
- .enum(["env", "generated", "capability", "provider"])
216
- .describe("Where it came from: you set it, the sandbox generated it, a connection needs it, or it is a model account's credential."),
286
+ .enum(["env", "generated", "capability", "provider", "device"])
287
+ .describe(
288
+ "Where it came from: you set it, the sandbox generated it, a connection needs it, it is a model account's credential, or it lives in the keyring on one of your computers and this sandbox holds only a pointer to it.",
289
+ ),
217
290
  // e.g. "<ProviderName> · <accountLabel>" for provider entries; absent on env/generated.
218
291
  label: z.string().optional().describe("A friendlier name, for entries that have one."),
219
292
  status: z.enum(["missing", "set", "connected"]).describe("Whether it exists and, for a connection, whether it is working."),
@@ -237,9 +310,9 @@ export const SecretInventoryEntrySchema = z.object({
237
310
  .object({
238
311
  at: z.number().describe("When, in milliseconds."),
239
312
  lane: z
240
- .enum(["shell", "code", "browser", "gateway", "ssh"])
313
+ .enum(["shell", "code", "browser", "gateway", "ssh", "device", "desktop"])
241
314
  .describe(
242
- "How it was used: a command, a script, typed into a page, attached by the credential gateway to a request the agent sent, or a key the sandbox's ssh agent signed with for the agent.",
315
+ "How it was used: a command, a script, typed into a page, attached by the credential gateway to a request the agent sent, a key the sandbox's ssh agent signed with for the agent, typed into a window or app on one of the owner's computers, or typed into a window on the sandbox's desktop.",
243
316
  ),
244
317
  detail: z
245
318
  .string()
@@ -272,6 +345,21 @@ export const SecretInventoryEntrySchema = z.object({
272
345
  })
273
346
  .optional()
274
347
  .describe("Its host guard. Absent when none was ever set and no connector sets one: the guard is off."),
348
+ // Joined from the program guards: which windows and phone apps a use may type it into unasked.
349
+ programs: SecretProgramsSchema.optional().describe(
350
+ "Which programs on the owner's computers, and which phone apps, it may be typed into without asking. Absent when none was set: typing it into any window asks first.",
351
+ ),
352
+ // A device row only: the keyring entry the pointer names. Everything here is the keyring's to change, on that computer.
353
+ device: z
354
+ .object({
355
+ machine: z.string().describe("Which computer's keyring holds it, by its device card."),
356
+ mode: KeyringShareModeSchema,
357
+ online: z.boolean().describe("Whether that computer answers now. Offline, an `each-use` or `while-here` secret cannot be used until it does."),
358
+ locked: z.boolean().optional().describe("Whether the keyring there is locked, as last heard. Absent while the computer is offline."),
359
+ source: KeyringSourceSchema,
360
+ })
361
+ .optional()
362
+ .describe("Where a secret held on one of your computers lives and how it is shared. Present on `device` rows only."),
275
363
  });
276
364
  export type SecretInventoryEntry = z.infer<typeof SecretInventoryEntrySchema>;
277
365
  export const SecretInventorySchema = z.object({
@@ -298,6 +298,14 @@ export const SandboxSettingsSchema = z.object({
298
298
  .describe(
299
299
  "Let the agent answer with a page drawn right in the chat: a chart, a table, a diagram, a mock-up, or a small form you answer it on. Off, the agent is not offered the tools at all, so it spends nothing on them.",
300
300
  ),
301
+ // The daemon's shadow keeper (derived/shadow-keeper.ts) and the ripgrep config agents' shells get with it. On by
302
+ // default: it renders without asking any model (OCR excepted, on this machine), one low-priority process at a time.
303
+ documentSearch: z
304
+ .boolean()
305
+ .default(true)
306
+ .describe(
307
+ "Keep the text of every document in the workspace (pdf, Word, OpenDocument, spreadsheets, presentations, epub, and scans where OCR is installed) ready to search. In the background, one low-priority process at a time, each document is rendered to text when it appears or changes, so `rg` and iq find what documents say under their own paths, cited by page. Off, nothing renders until someone opens a document, and searches skip them.",
308
+ ),
301
309
  hashlineEdits: z
302
310
  .boolean()
303
311
  .default(false)
@@ -74,7 +74,9 @@ export type SpeechTranscript = z.infer<typeof SpeechTranscriptSchema>;
74
74
  // streams each one as it is spoken. A text frame `begin` opens phrase `id`; binary frames that follow are its audio,
75
75
  // 16 kHz mono s16le; `end` closes it, keeping its first `keep` samples (the trailing pause trimmed off); `drop`
76
76
  // abandons it. The daemon answers with `partial` text while the phrase is still being spoken, one `final` per ended
77
- // phrase, in order, and `status` whenever the model's readiness moves. The language is the socket's `lang` query.
77
+ // phrase, in order, and `status` whenever the model's readiness moves. The language is the socket's `lang` query;
78
+ // `guesses=0` asks for finals alone, which the wake listener does: nobody reads a running guess at a room's talk, and
79
+ // each one is a decode.
78
80
  export const SpeechClientMessageSchema = z.discriminatedUnion("type", [
79
81
  z.object({ type: z.literal("begin"), id: z.number() }),
80
82
  z.object({ type: z.literal("end"), id: z.number(), keep: z.number() }),
@@ -98,3 +100,38 @@ export type SpeechServerMessage = z.infer<typeof SpeechServerMessageSchema>;
98
100
  // The longest phrase either door takes: a minute at 16 kHz. The composer's segmenter cuts a monologue at the same
99
101
  // length, so nothing it sends is ever refused.
100
102
  export const MAX_SPEECH_SAMPLES = 60 * 16_000;
103
+
104
+ // THE WAKE WORD: what a person says to the editor across the room to be heard without touching it ("Leon, fix the
105
+ // build"). The editor listens for it on a device the person switched it on for, and hears it with this sandbox's own
106
+ // model, so it is a word the model writes down: letters (any script), with a space, hyphen or apostrophe between them.
107
+ export const WAKE_WORD_DEFAULT = "Leon";
108
+ export const WakeWordSchema = z
109
+ .string()
110
+ .trim()
111
+ .regex(/^[\p{L}\p{M}](?:[\p{L}\p{M}' -]{0,38}[\p{L}\p{M}])?$/u)
112
+ .refine((word) => [...word].length >= 2, { message: "a wake word has at least two letters" });
113
+
114
+ // POST /speech/wake: one device's claim on a wake it just heard. Every device listening for the same person hears the
115
+ // same "Leon", and each would open its own session; the first to claim within a few seconds is granted it and the rest
116
+ // stand down. A device keeps its own claims, so its next wake a moment later is never refused by its last one, and a
117
+ // device in the middle of an exchange holds it longer, so a follow-up the other devices also hear stays with it.
118
+ export const WAKE_HOLD_MAX_MS = 10 * 60_000;
119
+ export const SpeechWakeClaimSchema = z.object({
120
+ device: z
121
+ .string()
122
+ .regex(/^[A-Za-z0-9_-]{8,64}$/u)
123
+ .describe("A random id this browser or app minted for itself once, so its own claims are told from another device's."),
124
+ holdMs: z
125
+ .number()
126
+ .int()
127
+ .min(0)
128
+ .max(WAKE_HOLD_MAX_MS)
129
+ .optional()
130
+ .describe("Keep the person's other devices off for this long rather than the few seconds a lone wake holds: an exchange this device is in the middle of."),
131
+ });
132
+ export type SpeechWakeClaim = z.infer<typeof SpeechWakeClaimSchema>;
133
+
134
+ export const SpeechWakeGrantSchema = z.object({
135
+ granted: z.boolean().describe("This device acts on the wake; false when another device of the same person claimed it first."),
136
+ });
137
+ export type SpeechWakeGrant = z.infer<typeof SpeechWakeGrantSchema>;