@intentic/sandbox-contract 1.343.0 → 1.344.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 (56) hide show
  1. package/README.md +4 -0
  2. package/dist/contracts/device.contract.d.ts +17 -0
  3. package/dist/contracts/device.contract.d.ts.map +1 -1
  4. package/dist/contracts/runner.contract.d.ts +3 -2
  5. package/dist/contracts/runner.contract.d.ts.map +1 -1
  6. package/dist/contracts/settings.contract.d.ts +4 -2
  7. package/dist/contracts/settings.contract.d.ts.map +1 -1
  8. package/dist/contracts/system.contract.d.ts +52 -5
  9. package/dist/contracts/system.contract.d.ts.map +1 -1
  10. package/dist/events/agent-events.d.ts +1 -0
  11. package/dist/events/agent-events.d.ts.map +1 -1
  12. package/dist/events/agent-events.js +1 -0
  13. package/dist/events/agent-events.js.map +1 -1
  14. package/dist/index.d.ts +56 -7
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/policy/turned-away.d.ts.map +1 -1
  17. package/dist/policy/turned-away.js +1 -0
  18. package/dist/policy/turned-away.js.map +1 -1
  19. package/dist/protocol/raw/raw-json-routes.d.ts +17 -0
  20. package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
  21. package/dist/schemas/data-protection.d.ts +4 -1
  22. package/dist/schemas/data-protection.d.ts.map +1 -1
  23. package/dist/schemas/data-protection.js +17 -1
  24. package/dist/schemas/data-protection.js.map +1 -1
  25. package/dist/schemas/device-folder.d.ts +61 -0
  26. package/dist/schemas/device-folder.d.ts.map +1 -0
  27. package/dist/schemas/device-folder.js +24 -0
  28. package/dist/schemas/device-folder.js.map +1 -0
  29. package/dist/schemas/devices.d.ts +108 -38
  30. package/dist/schemas/devices.d.ts.map +1 -1
  31. package/dist/schemas/devices.js +7 -10
  32. package/dist/schemas/devices.js.map +1 -1
  33. package/dist/schemas/metrics.d.ts +11 -0
  34. package/dist/schemas/metrics.d.ts.map +1 -1
  35. package/dist/schemas/metrics.js +17 -0
  36. package/dist/schemas/metrics.js.map +1 -1
  37. package/dist/schemas/settings-history.d.ts +1 -1
  38. package/dist/schemas/settings-history.d.ts.map +1 -1
  39. package/dist/schemas/settings-history.js +15 -0
  40. package/dist/schemas/settings-history.js.map +1 -1
  41. package/dist/schemas/settings.d.ts +4 -2
  42. package/dist/schemas/settings.d.ts.map +1 -1
  43. package/dist/schemas/settings.js +4 -8
  44. package/dist/schemas/settings.js.map +1 -1
  45. package/dist/state/definition.d.ts +8 -4
  46. package/dist/state/definition.d.ts.map +1 -1
  47. package/package.json +5 -5
  48. package/src/events/agent-events.ts +2 -0
  49. package/src/policy/turned-away.ts +1 -0
  50. package/src/schemas/data-protection.ts +35 -9
  51. package/src/schemas/device-folder.ts +59 -0
  52. package/src/schemas/devices.ts +13 -38
  53. package/src/schemas/metrics.ts +28 -3
  54. package/src/schemas/settings-history.ts +20 -0
  55. package/src/schemas/settings.test.ts +38 -0
  56. package/src/schemas/settings.ts +12 -12
@@ -1,5 +1,6 @@
1
1
  // What one of the user's own machines is running.
2
2
  import { z } from "zod";
3
+ import { DeviceBridgeHoldSchema, DeviceConflictSchema, DeviceLowDiskSchema } from "./device-folder.js";
3
4
  import { parseHostConnection, type DeviceFacts, DeviceFactsSchema, MachineIdSchema, userDistrosOf, WslEnvironmentSchema } from "./hosts.js";
4
5
  import { RollbackTargetSchema, UpdateOutcomeSchema } from "./updates.js";
5
6
  import { DEV_VERSION } from "../state/versions.js";
@@ -316,38 +317,8 @@ export const DeviceCommandResultSchema = z.object({
316
317
  });
317
318
  export type DeviceCommandResult = z.infer<typeof DeviceCommandResultSchema>;
318
319
 
319
- // What one side did to a conflicted path. The first three are Mutagen's own change kinds. `untracked` is its word for
320
- // content it SCANS but never carries — whatever sits under an ignore pattern, which for a workspace means build output
321
- // — and it is the whole difference between a conflict somebody must settle and one a device can clear by itself. Absent
322
- // means Mutagen did not say, not that a side is untouched.
323
- export const DeviceConflictChangeSchema = z.enum(["created", "modified", "deleted", "untracked"]);
324
- export type DeviceConflictChange = z.infer<typeof DeviceConflictChangeSchema>;
325
-
326
- // Which of two unlike things this conflict is. `both-edited` is the one the mode exists for: two real copies, one
327
- // choice, a person's. `derived-leftover` is not a disagreement at all — one side deleted a directory, the other still
328
- // holds build output inside it, and Mutagen will not delete a directory whose contents it never carried. Nothing at
329
- // stake, nobody's edit, and no judgement to make.
330
- export const DeviceConflictNatureSchema = z.enum(["derived-leftover", "both-edited"]);
331
- export type DeviceConflictNature = z.infer<typeof DeviceConflictNatureSchema>;
332
-
333
- export const DeviceConflictSchema = z.object({
334
- /** Relative to the synced folder (matches both `localDir` and /work); empty means the root itself. */
335
- path: z.string(),
336
- /** What happened on the device (Mutagen's alpha). */
337
- local: DeviceConflictChangeSchema.optional(),
338
- /** And in the sandbox's /work (its beta). */
339
- sandbox: DeviceConflictChangeSchema.optional(),
340
- // Absent from an agent older than the classification, which is why no reader may treat absence as `both-edited`:
341
- // `clearableOnDevice` below tests for the derived shape rather than against the other one.
342
- nature: DeviceConflictNatureSchema.optional(),
343
- });
344
- export type DeviceConflict = z.infer<typeof DeviceConflictSchema>;
345
-
346
- // Whether this one is the device's own to clear: build output on the device, inside a directory the sandbox deleted.
347
- // The rule lives here because two sides read it — the machine agent deciding what to remove, and the browser deciding
348
- // whether to offer a button instead of an agent — and they must never disagree about which conflicts need a person.
349
- export const clearableOnDevice = (conflict: DeviceConflict): boolean =>
350
- conflict.nature === "derived-leftover" && conflict.local === "untracked" && conflict.sandbox === "deleted";
320
+ // What a synced folder reports beyond Mutagen's word, its conflicts and what the device holds back (device-folder.ts).
321
+ export * from "./device-folder.js";
351
322
 
352
323
  // One paired sandbox as the local agent holds it; `localDir` is which folder on that device holds this sandbox's /work.
353
324
  export const DevicePairingSchema = z.object({
@@ -378,6 +349,10 @@ export const DevicePairingSchema = z.object({
378
349
  paused: z.boolean().optional(),
379
350
  // Second session's status (the state-dir backup mirror), reported separately: the two fail independently.
380
351
  backupStatus: z.string().optional(),
352
+ // (2026-10-18) While the drive holding `localDir` is nearly full, the reading that paused its sync and git bridge; and the
353
+ // repos whose git history stopped following for a while (device-folder.ts). Each absent while it does not hold.
354
+ lowDisk: DeviceLowDiskSchema.optional(),
355
+ bridgeHolds: z.array(DeviceBridgeHoldSchema).optional(),
381
356
  });
382
357
  export type DevicePairing = z.infer<typeof DevicePairingSchema>;
383
358
  // One sandbox port and what became of it on this machine's localhost; carries a state rather than just live forwards,
@@ -574,14 +549,14 @@ export const DeviceSchema = z.object({
574
549
  gap: DeviceGapSchema.optional(),
575
550
  });
576
551
  export type Device = z.infer<typeof DeviceSchema>;
577
- // (2026-10-15) A device enrollment no capability card holds: a key into this sandbox for which no board draws a machine,
578
- // since a machine is drawn from its card. Listed so the owner can withdraw it (`DELETE /system/hosts/{id}`, `id` being the
579
- // connection key); never withdrawn by the daemon itself, since the manifest the cards live in is a workspace file a land
580
- // can rewrite for a moment, and withdrawing on that would unpair real devices.
552
+ // Device keys no machine is drawn for, each list absent when empty: enrollments no card holds, which only the owner withdraws (a land can rewrite
553
+ // the card manifest for a moment), and connections live at once that report one machine id, kept rather than deduped (hosts/host-dedupe.ts).
581
554
  export const CardlessHostSchema = z.object({ id: z.string(), card: z.string(), environment: z.string() });
582
555
  export type CardlessHost = z.infer<typeof CardlessHostSchema>;
583
- // `cardlessHosts` is absent when there are none, and from a daemon older than the field.
584
- export const DevicesListSchema = z.object({ devices: z.array(DeviceSchema), cardlessHosts: z.array(CardlessHostSchema).optional() });
556
+ export const SharedIdentitySchema = z.object({ machineId: z.string(), connections: z.array(CardlessHostSchema) });
557
+ export type SharedIdentity = z.infer<typeof SharedIdentitySchema>;
558
+ const deviceKeyNotes = { cardlessHosts: z.array(CardlessHostSchema).optional(), sharedIdentities: z.array(SharedIdentitySchema).optional() };
559
+ export const DevicesListSchema = z.object({ devices: z.array(DeviceSchema), ...deviceKeyNotes });
585
560
  export type DevicesList = z.infer<typeof DevicesListSchema>;
586
561
 
587
562
  // The physical computer a device is an environment of. Windows and every WSL distro on it are one machine with one
@@ -97,7 +97,9 @@ export const SandboxUsageSchema = z.object({
97
97
  countedFileCacheBytes: z
98
98
  .number()
99
99
  .optional()
100
- .describe("File cache counted in resident usage (memory.stat file less inactive_file), in bytes. Absent unless both counters are valid."),
100
+ .describe(
101
+ "File cache counted in resident usage (memory.stat file less inactive_file), in bytes. Absent unless both counters are valid.",
102
+ ),
101
103
  kernelBytes: z
102
104
  .number()
103
105
  .optional()
@@ -163,7 +165,9 @@ export const SandboxMetricsSchema = z.object({
163
165
  .optional()
164
166
  .describe("How long the CPU figures were measured over, in milliseconds. Absent on a first reading, and then so is every CPU figure."),
165
167
  sandbox: SandboxUsageSchema.describe("The sandbox as a whole, including the daemon."),
166
- daemon: DaemonUsageSchema.describe("The daemon's own usage, included in the sandbox total and process count but excluded from roles and session sums."),
168
+ daemon: DaemonUsageSchema.describe(
169
+ "The daemon's own usage, included in the sandbox total and process count but excluded from roles and session sums.",
170
+ ),
167
171
  sessions: z
168
172
  .record(z.string(), SessionMetricsSchema)
169
173
  .describe(
@@ -273,8 +277,29 @@ export const StorageScanSchema = z.object({
273
277
  export type StorageScan = z.infer<typeof StorageScanSchema>;
274
278
 
275
279
  export const StorageReportSchema = z.object({
276
- scan: StorageScanSchema.optional().describe("The last scan that finished, kept across restarts; its `finishedAt` says how old it is. Absent until one has."),
280
+ scan: StorageScanSchema.optional().describe(
281
+ "The last scan that finished, kept across restarts; its `finishedAt` says how old it is. Absent until one has.",
282
+ ),
277
283
  scanning: z.boolean().describe("Whether a scan is running now."),
284
+ historyDisk: z
285
+ .object({
286
+ freeBytes: z.number().describe("Space available to the sandbox on the history volume, measured now."),
287
+ totalBytes: z.number().describe("The history volume's total size."),
288
+ floorBytes: z.number().describe("Space kept free before a new checkout: 2% of this volume clamped to 5–20 GiB."),
289
+ low: z.boolean().describe("Whether available history space is below the checkout floor."),
290
+ })
291
+ .optional()
292
+ .describe("The filesystem holding conversation checkouts, independent of the workspace volume. Absent if unreadable."),
293
+ repositoryCheckouts: z
294
+ .array(
295
+ z.object({
296
+ repo: z.string().describe("The live repository's workspace-relative id, or root for the workspace repository."),
297
+ head: z.string().describe("The commit whose tracked blobs were measured."),
298
+ bytes: z.number().describe("The sum of blob sizes at this HEAD: the estimated bytes for one full checkout."),
299
+ }),
300
+ )
301
+ .optional()
302
+ .describe("Live repositories' checkout costs, cached per repository and commit. Repositories without a readable HEAD are omitted."),
278
303
  });
279
304
  export type StorageReport = z.infer<typeof StorageReportSchema>;
280
305
 
@@ -3,6 +3,7 @@ import { at, drop, dropAll, fold, isJsonObject, type JsonObject, mapValue, retyp
3
3
  import { type ModelRole, ModelRoleSchema } from "../models/model-roles.js";
4
4
  import { setupsFromRoleLists } from "../models/model-setups.js";
5
5
  import { ModelPinSchema, type ModelPin } from "./agent.js";
6
+ import { RESTORE_POINT_KEEP_DAYS_DEFAULT } from "./data-protection.js";
6
7
  import type { KeepWarmSettings, RuleSchema } from "./settings.js";
7
8
 
8
9
  // The conversions the settings shape has had, oldest first: what brings any earlier release's settings to
@@ -101,6 +102,21 @@ const roleListsIntoSetups = transform("folds modelRoles and modelDefault into se
101
102
  return { ...rest, modelSetups, roleSetups };
102
103
  });
103
104
 
105
+ const restorePointsKept = fold("carries restorePointDays into restorePointKeepDays, a stored zero taking the 30-day default", {
106
+ from: ["restorePointDays"],
107
+ into: "restorePointKeepDays",
108
+ applies: (retention) => Object.hasOwn(retention, "restorePointDays"),
109
+ convert: ({ restorePointDays }, retention) => {
110
+ // A file that already names the new key (a definition written for both) keeps it.
111
+ const kept = FlatNumber.safeParse(retention["restorePointKeepDays"]);
112
+ if (kept.success) {
113
+ return kept.data;
114
+ }
115
+ const chosen = FlatNumber.safeParse(restorePointDays);
116
+ return chosen.success && chosen.data > 0 ? chosen.data : RESTORE_POINT_KEEP_DAYS_DEFAULT;
117
+ },
118
+ });
119
+
104
120
  export const SETTINGS_HISTORY = [
105
121
  // Every setting a release since 2026-08-10 had and this one does not (read off the contract lock's history):
106
122
  // retired, so passthrough stops carrying them, a definition naming one still applies, and no later setting may
@@ -154,4 +170,8 @@ export const SETTINGS_HISTORY = [
154
170
  ...dropAll(["toolResultClearing", "toolResultClearingHoldout"]),
155
171
  at("modelRoles", refactorsFromChores),
156
172
  roleListsIntoSetups,
173
+ // Restore points kept 30 days by default (2026-10-18). `restorePointDays` held zero both where the owner chose to keep
174
+ // every point and where the settings page had saved the old default, which no reading could tell apart: the key is
175
+ // retired, a positive choice carries over, and a stored zero takes the new default.
176
+ at("retention", restorePointsKept),
157
177
  ] as const;
@@ -207,3 +207,41 @@ describe(`per-job model lists`, () => {
207
207
  expect(convertDocument(SETTINGS_HISTORY, `object`, { modelSetups: [{ id: `low`, pins: [haiku] }], roleSetups: {} }, true).changes).toEqual([]);
208
208
  });
209
209
  });
210
+
211
+ test("disk retention defaults bound checkouts and restore points while preserving explicit zero", () => {
212
+ const defaults = SandboxSettingsSchema.parse({});
213
+ expect(defaults.agentRetentionDays).toBe(3);
214
+ expect(defaults.heldCheckoutDays).toBe(14);
215
+ expect(defaults.retention.restorePointKeepDays).toBe(30);
216
+ const existing = SandboxSettingsSchema.parse({ heldCheckoutDays: 0, retention: { restorePointKeepDays: 0 } });
217
+ expect(existing.heldCheckoutDays).toBe(0);
218
+ expect(existing.retention.restorePointKeepDays).toBe(0);
219
+ });
220
+
221
+ describe(`restore points' retention, renamed so a stored zero is the owner's own (2026-10-18)`, () => {
222
+ const read = (retention: Record<string, unknown>) => {
223
+ const converted = convertDocument(SETTINGS_HISTORY, `object`, { retention: { transcriptDays: 7, ...retention } }, true);
224
+ return { retention: SandboxSettingsSchema.parse(converted.value).retention, changes: converted.changes };
225
+ };
226
+
227
+ test(`an earlier release's stored zero, which the settings page wrote for the old default, takes the 30-day default`, () => {
228
+ const { retention, changes } = read({ restorePointDays: 0 });
229
+ expect(retention.restorePointKeepDays).toBe(30);
230
+ expect(retention.transcriptDays).toBe(7);
231
+ expect(changes.map((change) => change.at)).toEqual([`retention`]);
232
+ });
233
+
234
+ test(`a positive choice carries over`, () => {
235
+ expect(read({ restorePointDays: 90 }).retention.restorePointKeepDays).toBe(90);
236
+ });
237
+
238
+ test(`a zero stored under the new key is kept, and read again changes nothing`, () => {
239
+ const { retention, changes } = read({ restorePointKeepDays: 0 });
240
+ expect(retention.restorePointKeepDays).toBe(0);
241
+ expect(changes).toEqual([]);
242
+ });
243
+
244
+ test(`a file naming both keeps the new one`, () => {
245
+ expect(read({ restorePointDays: 0, restorePointKeepDays: 5 }).retention.restorePointKeepDays).toBe(5);
246
+ });
247
+ });
@@ -13,7 +13,13 @@ import { SessionsPlacementSchema } from "./offload.js";
13
13
  import { LimitPolicySchema, RetryPolicySchema, RoomPolicySchema } from "./turn-break.js";
14
14
  import { ZoneSchema } from "../time/zone.js";
15
15
  import { VoiceSettingsSchema } from "./voice.js";
16
- import { AiNoticeSchema, AuditSettingsSchema, RetentionSettingsSchema } from "./data-protection.js";
16
+ import {
17
+ AiNoticeSchema,
18
+ AuditSettingsSchema,
19
+ RetentionSettingsSchema,
20
+ HeldCheckoutDaysSchema,
21
+ AgentRetentionDaysSchema,
22
+ } from "./data-protection.js";
17
23
  // Which prompt base the agent runs before this turn composes anything on top: Intentic's own (default), Claude Code's
18
24
  // preset, or the owner's text. Declared out here since both the daemon and the browser branch on it.
19
25
  // Kept in the daemon's auth root, never in SandboxSettings: an agent cannot edit its execution boundary.
@@ -398,14 +404,8 @@ export const SandboxSettingsSchema = z.object({
398
404
  .describe(
399
405
  "Which repositories keep a changelog, and so get a user-facing note written alongside each merge. A list rather than a switch, and empty by default, because the commit writer's standing rule is to copy the house style rather than impose one, and a repository that has never written such a note gives it nothing to copy.",
400
406
  ),
401
- agentRetentionDays: z
402
- .number()
403
- .min(0)
404
- .max(365)
405
- .default(3)
406
- .describe(
407
- "How many days a finished conversation stays on the board before being put away. Zero means never. The one setting here that defaults on, because each card left behind is a real working copy on disk, not just a row.",
408
- ),
407
+ agentRetentionDays: AgentRetentionDaysSchema,
408
+ heldCheckoutDays: HeldCheckoutDaysSchema,
409
409
  // One answer per ending, never a set of switches over the same event: the chat's own question and these rows are
410
410
  // the same question at two scopes. Every ending defaults to `wait`, because a re-run spends the reader's allowance
411
411
  // on a turn they sent once.
@@ -543,10 +543,10 @@ export const SandboxSettingsSchema = z.object({
543
543
  voice: VoiceSettingsSchema.prefault({}).describe(
544
544
  "How the agent's replies are spoken aloud: whether a hands-free conversation is answered out loud, which voice speaks each language, and a speech provider of your own if you connected one.",
545
545
  ),
546
- // What a data protection officer asks about (schemas/data-protection.ts). Every default is the behaviour from before
547
- // these existed; the public-office preset on the data protection page is what turns them on, never a default.
546
+ // What a data protection officer asks about (schemas/data-protection.ts). Restore points default to thirty days;
547
+ // the other policies preserve the earlier behaviour until the owner changes them or applies the public-office preset.
548
548
  retention: RetentionSettingsSchema.prefault({}).describe(
549
- "How long this sandbox keeps each kind of copy it makes of what people and agents wrote: transcripts, restore points, the privacy shield's memory, converted documents and raw command output. Each defaults to forever.",
549
+ "How long this sandbox keeps each kind of copy it makes of what people and agents wrote: transcripts, restore points, the privacy shield's memory, converted documents and raw command output. Restore points default to 30 days; the other copies default to forever.",
550
550
  ),
551
551
  audit: AuditSettingsSchema.prefault({}).describe(
552
552
  "The audit trail: who changed a policy, a member, a gate or a setting, and optionally who read what. How long it is kept, and where a copy is sent.",