sealkeep 0.9.0 → 0.11.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 (274) hide show
  1. package/ARCHITECTURE.md +163 -14
  2. package/CHANGELOG.md +223 -1
  3. package/CONTROL_PLANE.md +2 -2
  4. package/LICENSE +1 -1
  5. package/README.md +108 -22
  6. package/THIRD_PARTY.md +2 -2
  7. package/THREAT_MODEL.md +23 -4
  8. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
  9. package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
  10. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
  11. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
  12. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
  13. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
  14. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
  15. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
  16. package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
  17. package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
  18. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
  19. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
  20. package/dist/site/index.html +1808 -1904
  21. package/dist/site/llms.txt +67 -0
  22. package/dist/site/trust/architecture-data-flow.html +53 -0
  23. package/dist/site/trust/audit-roadmap.html +37 -0
  24. package/dist/site/trust/deployment-responsibility.html +11 -0
  25. package/dist/site/trust/dpa-sample.html +30 -0
  26. package/dist/site/trust/release-provenance.html +21 -0
  27. package/dist/site/trust/subprocessors.html +15 -0
  28. package/dist/site/trust/threat-model.html +34 -0
  29. package/dist/site/trust/toms.html +41 -0
  30. package/dist/site/trust-document.css +32 -0
  31. package/dist/site/trust.html +73 -0
  32. package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
  33. package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
  34. package/dist/site/visual/index.html +18 -0
  35. package/dist/site.zip +0 -0
  36. package/dist/src/activity.d.ts +9 -0
  37. package/dist/src/activity.js +90 -1
  38. package/dist/src/adapters.d.ts +175 -5
  39. package/dist/src/adapters.js +961 -73
  40. package/dist/src/agent-context.d.ts +135 -0
  41. package/dist/src/agent-context.js +1059 -0
  42. package/dist/src/archive-copies.d.ts +47 -0
  43. package/dist/src/archive-copies.js +179 -0
  44. package/dist/src/audit.d.ts +1 -1
  45. package/dist/src/audit.js +29 -4
  46. package/dist/src/autopilot.d.ts +52 -7
  47. package/dist/src/autopilot.js +143 -25
  48. package/dist/src/background-bandwidth.d.ts +46 -0
  49. package/dist/src/background-bandwidth.js +301 -0
  50. package/dist/src/background-cpu.d.ts +82 -0
  51. package/dist/src/background-cpu.js +212 -0
  52. package/dist/src/background-worker-error.d.ts +12 -0
  53. package/dist/src/background-worker-error.js +18 -0
  54. package/dist/src/branding.d.ts +24 -1
  55. package/dist/src/branding.js +31 -1
  56. package/dist/src/bridge.d.ts +233 -0
  57. package/dist/src/bridge.js +604 -0
  58. package/dist/src/byte-stream.d.ts +91 -0
  59. package/dist/src/byte-stream.js +385 -0
  60. package/dist/src/chunk-store.d.ts +41 -8
  61. package/dist/src/chunk-store.js +161 -65
  62. package/dist/src/cli.js +1698 -163
  63. package/dist/src/cloud.d.ts +841 -31
  64. package/dist/src/cloud.js +3196 -277
  65. package/dist/src/context-background.d.ts +37 -0
  66. package/dist/src/context-background.js +309 -0
  67. package/dist/src/context-drain-child.d.ts +1 -0
  68. package/dist/src/context-drain-child.js +98 -0
  69. package/dist/src/context-reader.d.ts +118 -0
  70. package/dist/src/context-reader.js +447 -0
  71. package/dist/src/control-plane/auth.d.ts +32 -4
  72. package/dist/src/control-plane/auth.js +85 -24
  73. package/dist/src/control-plane/server.js +19 -6
  74. package/dist/src/control-plane.d.ts +17 -1
  75. package/dist/src/control-plane.js +32 -6
  76. package/dist/src/crypto.d.ts +1 -1
  77. package/dist/src/crypto.js +5 -5
  78. package/dist/src/daemon-lease.d.ts +70 -0
  79. package/dist/src/daemon-lease.js +420 -0
  80. package/dist/src/daemon.d.ts +94 -1
  81. package/dist/src/daemon.js +1082 -105
  82. package/dist/src/darwin-service-policy.d.ts +41 -0
  83. package/dist/src/darwin-service-policy.js +60 -0
  84. package/dist/src/dashboard-cli.js +15 -15
  85. package/dist/src/device-authorization.d.ts +37 -0
  86. package/dist/src/device-authorization.js +199 -0
  87. package/dist/src/device-enrollment.d.ts +91 -0
  88. package/dist/src/device-enrollment.js +349 -0
  89. package/dist/src/disk.d.ts +17 -12
  90. package/dist/src/disk.js +43 -17
  91. package/dist/src/doctor.d.ts +35 -1
  92. package/dist/src/doctor.js +316 -41
  93. package/dist/src/durable-ticket-lock.d.ts +24 -0
  94. package/dist/src/durable-ticket-lock.js +232 -0
  95. package/dist/src/enroll.d.ts +1 -1
  96. package/dist/src/enroll.js +13 -7
  97. package/dist/src/env.d.ts +10 -1
  98. package/dist/src/env.js +11 -3
  99. package/dist/src/errors.d.ts +8 -8
  100. package/dist/src/errors.js +6 -6
  101. package/dist/src/flush.d.ts +12 -0
  102. package/dist/src/flush.js +37 -0
  103. package/dist/src/heartbeat.d.ts +86 -12
  104. package/dist/src/heartbeat.js +415 -29
  105. package/dist/src/index-background-watchdog.d.ts +1 -0
  106. package/dist/src/index-background-watchdog.js +94 -0
  107. package/dist/src/index-background-work.d.ts +21 -0
  108. package/dist/src/index-background-work.js +25 -0
  109. package/dist/src/index-background.d.ts +64 -0
  110. package/dist/src/index-background.js +394 -0
  111. package/dist/src/index-build-child.d.ts +1 -0
  112. package/dist/src/index-build-child.js +109 -0
  113. package/dist/src/index-manifest.d.ts +52 -0
  114. package/dist/src/index-manifest.js +444 -0
  115. package/dist/src/index-publication-proof.d.ts +84 -0
  116. package/dist/src/index-publication-proof.js +380 -0
  117. package/dist/src/index-publication-state.d.ts +149 -0
  118. package/dist/src/index-publication-state.js +696 -0
  119. package/dist/src/index-publication-verifier.d.ts +89 -0
  120. package/dist/src/index-publication-verifier.js +341 -0
  121. package/dist/src/index-publish.d.ts +62 -0
  122. package/dist/src/index-publish.js +540 -0
  123. package/dist/src/index-scratch-cleanup.d.ts +19 -0
  124. package/dist/src/index-scratch-cleanup.js +166 -0
  125. package/dist/src/index-segment-types.d.ts +132 -0
  126. package/dist/src/index-segment-types.js +21 -0
  127. package/dist/src/index-segments.d.ts +9 -0
  128. package/dist/src/index-segments.js +516 -0
  129. package/dist/src/index-store.d.ts +123 -0
  130. package/dist/src/index-store.js +495 -0
  131. package/dist/src/index-sync.d.ts +91 -15
  132. package/dist/src/index-sync.js +286 -53
  133. package/dist/src/index-upgrade-publication.d.ts +30 -0
  134. package/dist/src/index-upgrade-publication.js +179 -0
  135. package/dist/src/integration-manager.d.ts +32 -0
  136. package/dist/src/integration-manager.js +394 -0
  137. package/dist/src/leakscan.js +1 -1
  138. package/dist/src/local-api.d.ts +56 -15
  139. package/dist/src/local-api.js +4979 -541
  140. package/dist/src/machine-settings.d.ts +51 -0
  141. package/dist/src/machine-settings.js +166 -0
  142. package/dist/src/managed-chunks.d.ts +5 -2
  143. package/dist/src/managed-chunks.js +14 -14
  144. package/dist/src/mcp-install.d.ts +11 -9
  145. package/dist/src/mcp-install.js +73 -28
  146. package/dist/src/mcp-workspace.d.ts +18 -0
  147. package/dist/src/mcp-workspace.js +50 -0
  148. package/dist/src/mcp.js +294 -25
  149. package/dist/src/migrate.js +27 -21
  150. package/dist/src/notify.d.ts +1 -1
  151. package/dist/src/notify.js +5 -5
  152. package/dist/src/offload.d.ts +201 -14
  153. package/dist/src/offload.js +1848 -140
  154. package/dist/src/onboarding.d.ts +8 -1
  155. package/dist/src/onboarding.js +4 -4
  156. package/dist/src/packages.d.ts +2 -2
  157. package/dist/src/packages.js +10 -2
  158. package/dist/src/passkey.d.ts +0 -1
  159. package/dist/src/passkey.js +2 -7
  160. package/dist/src/password-lock.d.ts +2 -2
  161. package/dist/src/password-lock.js +6 -6
  162. package/dist/src/paths.d.ts +2 -0
  163. package/dist/src/paths.js +2 -0
  164. package/dist/src/presence.d.ts +86 -0
  165. package/dist/src/presence.js +240 -0
  166. package/dist/src/progress-deadline.d.ts +21 -0
  167. package/dist/src/progress-deadline.js +91 -0
  168. package/dist/src/project-repair.d.ts +55 -0
  169. package/dist/src/project-repair.js +131 -0
  170. package/dist/src/providers/gcs.d.ts +28 -7
  171. package/dist/src/providers/gcs.js +35 -24
  172. package/dist/src/providers/gdrive.d.ts +71 -8
  173. package/dist/src/providers/gdrive.js +223 -50
  174. package/dist/src/providers/index.d.ts +11 -3
  175. package/dist/src/providers/index.js +30 -10
  176. package/dist/src/providers/s3.d.ts +30 -8
  177. package/dist/src/providers/s3.js +41 -30
  178. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  179. package/dist/src/providers/safe-storage-fetch.js +72 -0
  180. package/dist/src/queue.d.ts +136 -19
  181. package/dist/src/queue.js +862 -96
  182. package/dist/src/reclaim-transaction.d.ts +156 -0
  183. package/dist/src/reclaim-transaction.js +1027 -0
  184. package/dist/src/recovery-codes.d.ts +32 -0
  185. package/dist/src/recovery-codes.js +338 -0
  186. package/dist/src/recovery.js +12 -9
  187. package/dist/src/rehydrate.d.ts +25 -22
  188. package/dist/src/rehydrate.js +319 -23
  189. package/dist/src/restore.d.ts +57 -4
  190. package/dist/src/restore.js +272 -36
  191. package/dist/src/resume-stub.d.ts +92 -0
  192. package/dist/src/resume-stub.js +417 -0
  193. package/dist/src/retention.d.ts +98 -7
  194. package/dist/src/retention.js +1066 -62
  195. package/dist/src/rotate.js +3 -3
  196. package/dist/src/search.d.ts +566 -8
  197. package/dist/src/search.js +5897 -290
  198. package/dist/src/secrets.d.ts +51 -7
  199. package/dist/src/secrets.js +316 -24
  200. package/dist/src/service.d.ts +49 -11
  201. package/dist/src/service.js +776 -35
  202. package/dist/src/share.js +3 -3
  203. package/dist/src/shared-spaces.d.ts +98 -0
  204. package/dist/src/shared-spaces.js +214 -0
  205. package/dist/src/source-reader.d.ts +73 -0
  206. package/dist/src/source-reader.js +715 -0
  207. package/dist/src/spool.d.ts +1 -1
  208. package/dist/src/spool.js +1 -1
  209. package/dist/src/start-tui.js +2 -1
  210. package/dist/src/start.js +2 -2
  211. package/dist/src/storage-endpoint.d.ts +21 -0
  212. package/dist/src/storage-endpoint.js +122 -0
  213. package/dist/src/storage-setup.js +12 -12
  214. package/dist/src/storage-targets.d.ts +109 -6
  215. package/dist/src/storage-targets.js +975 -67
  216. package/dist/src/stream-to-cloud.d.ts +5 -1
  217. package/dist/src/stream-to-cloud.js +34 -14
  218. package/dist/src/sync-rules.d.ts +31 -6
  219. package/dist/src/sync-rules.js +153 -14
  220. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  221. package/dist/src/team-backfill-scheduling.js +33 -0
  222. package/dist/src/team-backfill.d.ts +116 -0
  223. package/dist/src/team-backfill.js +1429 -0
  224. package/dist/src/team-index-cache.d.ts +16 -0
  225. package/dist/src/team-index-cache.js +152 -0
  226. package/dist/src/team-offboarding.d.ts +38 -0
  227. package/dist/src/team-offboarding.js +1043 -0
  228. package/dist/src/team-presence.d.ts +127 -0
  229. package/dist/src/team-presence.js +904 -0
  230. package/dist/src/team-publication-policy.d.ts +20 -0
  231. package/dist/src/team-publication-policy.js +140 -0
  232. package/dist/src/team-realtime.d.ts +68 -0
  233. package/dist/src/team-realtime.js +816 -0
  234. package/dist/src/team-source-facts-cache.d.ts +23 -0
  235. package/dist/src/team-source-facts-cache.js +255 -0
  236. package/dist/src/trash.d.ts +1 -1
  237. package/dist/src/trash.js +2 -2
  238. package/dist/src/tui.js +11 -12
  239. package/dist/src/types.d.ts +173 -7
  240. package/dist/src/types.js +20 -0
  241. package/dist/src/ui-server.d.ts +163 -35
  242. package/dist/src/ui-server.js +712 -72
  243. package/dist/src/ui.d.ts +1 -2
  244. package/dist/src/ui.js +1 -2
  245. package/dist/src/upload.d.ts +27 -0
  246. package/dist/src/upload.js +383 -43
  247. package/dist/src/vault.d.ts +226 -30
  248. package/dist/src/vault.js +1776 -192
  249. package/dist/src/watcher.d.ts +7 -1
  250. package/dist/src/watcher.js +198 -55
  251. package/dist/src/worker.d.ts +27 -3
  252. package/dist/src/worker.js +274 -55
  253. package/package.json +33 -12
  254. package/scripts/native-reboot-rehearsal.mjs +90 -0
  255. package/web/app.js +6032 -343
  256. package/web/bootstrap.js +17 -0
  257. package/web/index.html +255 -57
  258. package/web/rail.js +317 -40
  259. package/web/retention.html +2 -2
  260. package/web/rules-view.js +188 -16
  261. package/web/sessions-view.js +485 -62
  262. package/web/sessions.html +2 -2
  263. package/web/setup-api.js +152 -29
  264. package/web/setup-logic.js +68 -9
  265. package/web/setup.html +113 -44
  266. package/web/setup.js +604 -71
  267. package/web/style.css +513 -98
  268. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  269. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
  273. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
  274. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
@@ -0,0 +1,394 @@
1
+ /** Marker understood by the daemon bootstrap and the standalone manager. */
2
+ export const INTEGRATION_MANAGER_GENERATION_ENV = "SEALKEEP_SERVICE_MANAGER_GENERATION";
3
+ export const INTEGRATION_MANAGER_GENERATION = "darwin-app-policy-v1";
4
+ /**
5
+ * The durable service manager's exact child command.
6
+ *
7
+ * A launchd job registered by an older Sealkeep release can retain its cached
8
+ * ProcessType=Background policy even after the plist is corrected. Starting
9
+ * the daemon through taskpolicy's application profile escapes that inherited
10
+ * job policy before Node evaluates the CLI; every helper the daemon later
11
+ * starts inherits the corrected policy too. Other platforms keep their byte-
12
+ * for-byte command because their native service definitions reload normally.
13
+ */
14
+ export function integrationManagerChildInvocation(receipt, target = process.platform) {
15
+ return target === "darwin" && receipt.kind === "launchd"
16
+ ? { executable: "/usr/sbin/taskpolicy", args: ["-a", receipt.executable, ...receipt.args] }
17
+ : { executable: receipt.executable, args: receipt.args };
18
+ }
19
+ const integrationManagerChildInvocationSource = integrationManagerChildInvocation.toString();
20
+ /**
21
+ * Source copied outside the npm package when the background service is
22
+ * installed. npm 7+ deliberately runs no uninstall lifecycle scripts, so a
23
+ * package cannot clean hooks or services from inside code npm just removed.
24
+ * This tiny disclosed supervisor survives long enough to notice that the
25
+ * package target stayed absent beyond an upgrade grace period, remove only the
26
+ * integration paths named in its receipt, write a tombstone, then remove
27
+ * itself. Vaults, encrypted archives, outboxes, and keychain items are kept.
28
+ */
29
+ export const INTEGRATION_MANAGER_SOURCE = String.raw `#!/usr/bin/env node
30
+ import { spawn, spawnSync } from 'node:child_process';
31
+ import { existsSync, statSync } from 'node:fs';
32
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
33
+ import { dirname, join } from 'node:path';
34
+ import { randomUUID } from 'node:crypto';
35
+
36
+ const receiptPath = process.argv[2];
37
+ if (!receiptPath) process.exit(64);
38
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
39
+ const readReceipt = async () => JSON.parse(await readFile(receiptPath, 'utf8'));
40
+ const atomicJson = async (path, value) => {
41
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
42
+ const temp = path + '.' + randomUUID() + '.tmp';
43
+ await writeFile(temp, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
44
+ await rename(temp, path);
45
+ };
46
+
47
+ // Keep this standalone manager's launch plan generated from the same function
48
+ // exported to the package tests. It cannot import package code: surviving a
49
+ // briefly absent npm target during upgrades is its reason for existing.
50
+ const childInvocationFor = (${integrationManagerChildInvocationSource});
51
+
52
+ const control = (file, args, options = {}) => spawnSync(file, args, {
53
+ timeout: 5000,
54
+ killSignal: 'SIGTERM',
55
+ ...options,
56
+ });
57
+
58
+ function ownedCommand(command) {
59
+ return typeof command === 'string' && /\bhook (enqueue|rehydrate|context|context-sync) --agent (codex|claude)\b/.test(command);
60
+ }
61
+
62
+ function prune(value) {
63
+ if (Array.isArray(value)) {
64
+ const kept = value.map(prune).filter((item) => item !== null);
65
+ return kept.length ? kept : null;
66
+ }
67
+ if (!value || typeof value !== 'object') return value;
68
+ if (ownedCommand(value.command)) return null;
69
+ const next = { ...value };
70
+ if (Object.prototype.hasOwnProperty.call(value, 'hooks')) {
71
+ const hooks = prune(value.hooks);
72
+ if (hooks === null) return null;
73
+ next.hooks = hooks;
74
+ }
75
+ return next;
76
+ }
77
+
78
+ async function cleanHook(path, stillCurrent) {
79
+ try {
80
+ const raw = await readFile(path, 'utf8');
81
+ const document = JSON.parse(raw);
82
+ if (!document || typeof document !== 'object' || Array.isArray(document)) return false;
83
+ const next = { ...document };
84
+ const hooks = {};
85
+ for (const [event, entries] of Object.entries(document.hooks ?? {})) {
86
+ const kept = prune(entries);
87
+ if (kept !== null) hooks[event] = kept;
88
+ }
89
+ next.hooks = hooks;
90
+ const before = JSON.stringify(document);
91
+ if (JSON.stringify(next) === before) return false;
92
+ if (!await stillCurrent()) return null;
93
+ const backup = path + '.sealkeep-uninstall-backup-' + new Date().toISOString().replace(/[:.]/g, '-');
94
+ await writeFile(backup, raw, { mode: 0o600 });
95
+ if (!await stillCurrent()) return null;
96
+ await atomicJson(path, next);
97
+ return true;
98
+ } catch { return false; }
99
+ }
100
+
101
+ function canonical(value) {
102
+ if (Array.isArray(value)) return value.map(canonical);
103
+ if (!value || typeof value !== 'object') return value;
104
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
105
+ }
106
+
107
+ function receiptGeneration(value) {
108
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
109
+ // missingSince is this manager's own monitor heartbeat, not an installation
110
+ // generation. Every install has a fresh installedAt and this full canonical
111
+ // fallback also keeps legacy receipts fail-closed across replacements.
112
+ const { missingSince: _missingSince, ...stable } = value;
113
+ return JSON.stringify(canonical(stable));
114
+ }
115
+
116
+ function property(output, name) {
117
+ const line = String(output ?? '').split(/\r?\n/).find((candidate) => candidate.startsWith(name + '='));
118
+ return line === undefined ? null : line.slice(name.length + 1);
119
+ }
120
+
121
+ /**
122
+ * A service/task name is user-global while receipts are vault-local. Before a
123
+ * surviving old manager edits current hooks or a unit, prove that the loaded
124
+ * registration is this exact process, unit, manager, and receipt. A stale
125
+ * manager must never dismantle a newer vault's registration merely because it
126
+ * inherited the same product label.
127
+ */
128
+ function ownsCurrentRegistration(receipt) {
129
+ if (receipt.kind === 'launchd') {
130
+ const domain = typeof process.getuid === 'function' ? 'gui/' + process.getuid() + '/' + receipt.label : null;
131
+ if (!domain) return false;
132
+ const loaded = control('launchctl', ['print', domain], { encoding: 'utf8' });
133
+ const loadedPid = Number(/^\s*pid\s*=\s*(\d+)\s*$/m.exec(loaded.stdout)?.[1]);
134
+ return loaded.status === 0
135
+ && loadedPid === process.pid
136
+ && process.argv[1] === receipt.managerPath
137
+ && process.argv[2] === receipt.receiptPath
138
+ && loaded.stdout.includes(receipt.managerPath)
139
+ && loaded.stdout.includes(receipt.receiptPath);
140
+ }
141
+ if (receipt.kind === 'systemd') {
142
+ const loaded = control('systemctl', [
143
+ '--user', 'show', receipt.label,
144
+ '--property=MainPID', '--property=FragmentPath', '--property=ExecStart',
145
+ ], { encoding: 'utf8' });
146
+ if (loaded.status !== 0) return false;
147
+ const mainPid = Number(property(loaded.stdout, 'MainPID'));
148
+ const fragmentPath = property(loaded.stdout, 'FragmentPath');
149
+ const execStart = property(loaded.stdout, 'ExecStart') ?? '';
150
+ // MainPID makes process.argv the kernel's exact argument vector for the
151
+ // current ExecStart, avoiding ambiguous substring parsing (and supporting
152
+ // paths systemctl renders with C escapes). FragmentPath independently
153
+ // proves which unit owns that process; a non-empty ExecStart proves the
154
+ // loaded registration exposes the property we asked systemd to verify.
155
+ return mainPid === process.pid
156
+ && fragmentPath === receipt.unitPath
157
+ && process.argv[1] === receipt.managerPath
158
+ && process.argv[2] === receipt.receiptPath
159
+ && execStart.length > 0;
160
+ }
161
+ // Task Scheduler is itself the durable registration. Keep its established
162
+ // cleanup behavior until it can expose the same process/unit identity proof.
163
+ return receipt.kind === 'schtasks';
164
+ }
165
+
166
+ async function cleanup(receipt) {
167
+ const expectedGeneration = receiptGeneration(receipt);
168
+ const stillCurrent = async () => {
169
+ if (!expectedGeneration || targetPresent()) return false;
170
+ const current = await readReceipt().catch(() => null);
171
+ return receiptGeneration(current) === expectedGeneration && !targetPresent();
172
+ };
173
+ // Resolve ownership before touching hooks. Hook commands are intentionally
174
+ // recognizable across package versions, which also means a stale receipt's
175
+ // broad matcher would otherwise delete a newer install's current entries.
176
+ if (!await stillCurrent() || !ownsCurrentRegistration(receipt) || !await stillCurrent()) return;
177
+
178
+ const removedHooks = [];
179
+ let ownedLingerUid = null;
180
+ for (const path of receipt.hookFiles ?? []) {
181
+ const result = await cleanHook(path, stillCurrent);
182
+ if (result === null) return;
183
+ if (result) removedHooks.push(path);
184
+ }
185
+ for (const path of receipt.ownedFragments ?? []) {
186
+ if (!await stillCurrent()) return;
187
+ await rm(path, { force: true }).catch(() => undefined);
188
+ }
189
+ for (const path of receipt.ownedRuntimeFiles ?? []) {
190
+ if (!await stillCurrent()) return;
191
+ await rm(path, { force: true }).catch(() => undefined);
192
+ }
193
+
194
+ if (receipt.kind === 'launchd') {
195
+ if (!await stillCurrent()) return;
196
+ await rm(receipt.unitPath, { force: true }).catch(() => undefined);
197
+ if (!await stillCurrent()) return;
198
+ control('launchctl', ['remove', receipt.label], { stdio: 'ignore' });
199
+ } else if (receipt.kind === 'systemd') {
200
+ if (!await stillCurrent()) return;
201
+ control('systemctl', ['--user', 'disable', receipt.label], { stdio: 'ignore' });
202
+ if (!await stillCurrent()) return;
203
+ await rm(receipt.unitPath, { force: true }).catch(() => undefined);
204
+ if (!await stillCurrent()) return;
205
+ control('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
206
+ // Linger is user-wide, not unit-local. Reverse it only when the owner-only
207
+ // receipt says Sealkeep proved it was off and enabled it, and only for the
208
+ // numeric account currently executing this manager. A forged/stale receipt
209
+ // can therefore never make a privileged cleanup target another user.
210
+ const currentUid = typeof process.getuid === 'function' ? process.getuid() : null;
211
+ const ownedLinger = receipt.linuxLinger?.enabledBySealkeep === true
212
+ && Number.isInteger(receipt.linuxLinger.uid)
213
+ && receipt.linuxLinger.uid === currentUid;
214
+ if (ownedLinger) ownedLingerUid = currentUid;
215
+ } else if (receipt.kind === 'schtasks') {
216
+ if (!await stillCurrent()) return;
217
+ control('schtasks', ['/delete', '/f', '/tn', receipt.label], { stdio: 'ignore' });
218
+ }
219
+
220
+ const tombstonePath = join(receipt.dataDir, 'integrations', 'uninstalled.json');
221
+ const tombstone = {
222
+ version: 1,
223
+ cleanedAt: new Date().toISOString(),
224
+ reason: 'package absent after upgrade grace period',
225
+ removedHooks,
226
+ removedService: receipt.unitPath,
227
+ removedLinger: false,
228
+ lingerCleanup: ownedLingerUid === null ? 'not-owned' : 'requested',
229
+ preserved: ['encrypted archives', 'unsynced outbox', 'recovery material in native keychain', 'remote objects'],
230
+ };
231
+ if (!await stillCurrent()) return;
232
+ await atomicJson(tombstonePath, tombstone);
233
+ if (!await stillCurrent()) return;
234
+ await rm(receipt.managerPath, { force: true }).catch(() => undefined);
235
+ if (!await stillCurrent()) return;
236
+ await rm(receiptPath, { force: true }).catch(() => undefined);
237
+ if (ownedLingerUid !== null) {
238
+ // Do this strictly last. On a logged-out headless host, disabling linger
239
+ // may stop the user manager — including this process — immediately. By
240
+ // then hooks, unit, receipt, manager, and the requested-cleanup tombstone
241
+ // are already durable. If logind leaves us alive, refine the receipt with
242
+ // the command's verified exit status.
243
+ const removedLinger = control('loginctl', ['disable-linger', String(ownedLingerUid)], { stdio: 'ignore' }).status === 0;
244
+ await atomicJson(tombstonePath, {
245
+ ...tombstone,
246
+ removedLinger,
247
+ lingerCleanup: removedLinger ? 'completed' : 'failed',
248
+ }).catch(() => undefined);
249
+ }
250
+ }
251
+
252
+ const receipt = await readReceipt();
253
+ const targetPresent = () => existsSync(receipt.packageTarget);
254
+ const targetIdentity = () => {
255
+ try {
256
+ const entry = statSync(receipt.packageTarget);
257
+ return [entry.dev, entry.ino, entry.size, entry.mtimeMs].join(':');
258
+ } catch { return null; }
259
+ };
260
+ if (!targetPresent()) {
261
+ await delay(Math.max(0, receipt.cleanupGraceMs ?? 300000));
262
+ if (!targetPresent()) await cleanup(receipt);
263
+ process.exit(0);
264
+ }
265
+
266
+ const childInvocation = childInvocationFor(receipt, process.platform);
267
+ const child = spawn(childInvocation.executable, childInvocation.args, {
268
+ env: {
269
+ ...process.env,
270
+ // Exact paths, not a generic boolean: service reconciliation may only
271
+ // defer a self-restart when it is running under this precise manager and
272
+ // ownership receipt.
273
+ SEALKEEP_SERVICE_MANAGER_PATH: receipt.managerPath,
274
+ SEALKEEP_SERVICE_RECEIPT_PATH: receipt.receiptPath,
275
+ ${JSON.stringify(INTEGRATION_MANAGER_GENERATION_ENV)}: ${JSON.stringify(INTEGRATION_MANAGER_GENERATION)},
276
+ },
277
+ stdio: 'inherit'
278
+ });
279
+ // Attach before the first await. A daemon that exits immediately (for example
280
+ // because an older generation still owns the vault) can otherwise disappear
281
+ // while this manager is publishing its startup heartbeat. EventEmitter does
282
+ // not replay the exit event, leaving a childless supervisor alive forever.
283
+ const childExit = new Promise((resolve) => child.once('exit', (code, signal) => resolve({ code, signal })));
284
+ let launchedTargetIdentity = targetIdentity();
285
+ let restartingForUpgrade = false;
286
+ let removingForUninstall = false;
287
+ let forceStopTimer = null;
288
+ const scheduleForcedStop = () => {
289
+ if (forceStopTimer) return;
290
+ // A daemon normally drains its current bounded unit before exiting. A bad
291
+ // provider or inherited old build must not strand an upgrade forever: after
292
+ // the grace period the supervisor ends only its own child, then the native
293
+ // service manager launches the new package. Tests may shorten the internal
294
+ // receipt value; installed receipts use the ten-second default.
295
+ const graceMs = Math.max(50, Math.min(10000, receipt.forceStopMs ?? 10000));
296
+ forceStopTimer = setTimeout(() => child.kill('SIGKILL'), graceMs);
297
+ forceStopTimer.unref?.();
298
+ };
299
+
300
+ // A freshly logged-in machine can spend a while loading the package and
301
+ // discovering a large agent-history tree before the daemon writes its first
302
+ // detailed heartbeat. The service manager already knows the child exists, so
303
+ // publish that narrow fact immediately. If the child exits, the normal
304
+ // liveness check sees that this pid is gone; once the daemon is ready it
305
+ // replaces this record with its real roots and progress.
306
+ const heartbeatPath = join(receipt.dataDir, 'daemon-state.json');
307
+ const previousHeartbeat = await readFile(heartbeatPath, 'utf8')
308
+ .then((raw) => JSON.parse(raw))
309
+ .catch(() => null);
310
+ const startedAt = new Date().toISOString();
311
+ await atomicJson(heartbeatPath, {
312
+ version: 1,
313
+ pid: child.pid ?? process.pid,
314
+ startedAt,
315
+ lastTickAt: startedAt,
316
+ intervalMs: 30000,
317
+ totals: previousHeartbeat?.totals ?? { archived: 0, indexed: 0, uploaded: 0, reclaimed: 0, failed: 0 },
318
+ last: { archived: 0, indexed: 0, uploaded: 0, reclaimed: 0, failed: 0, notes: ['starting automatic memory preservation'] },
319
+ paused: false,
320
+ watching: []
321
+ }).catch(() => undefined);
322
+
323
+ let missingSince = null;
324
+ const intervalMs = Math.max(250, Math.min(30000, receipt.monitorIntervalMs ?? 30000));
325
+ const monitor = setInterval(async () => {
326
+ if (targetPresent()) {
327
+ missingSince = null;
328
+ const currentIdentity = targetIdentity();
329
+ if (launchedTargetIdentity && currentIdentity && currentIdentity !== launchedTargetIdentity) {
330
+ // npm replaces the CLI while an existing Node process keeps the old
331
+ // JavaScript in memory. End this supervisor with a non-zero upgrade code
332
+ // so launchd/systemd starts it again and the next child loads the newly
333
+ // installed package. No person, login, or machine restart is required.
334
+ restartingForUpgrade = true;
335
+ clearInterval(monitor);
336
+ if (!child.kill('SIGTERM')) process.exit(75);
337
+ else scheduleForcedStop();
338
+ } else if (!launchedTargetIdentity && currentIdentity) {
339
+ launchedTargetIdentity = currentIdentity;
340
+ }
341
+ return;
342
+ }
343
+ if (missingSince === null) {
344
+ missingSince = Date.now();
345
+ await atomicJson(receiptPath, { ...receipt, missingSince: new Date(missingSince).toISOString() }).catch(() => undefined);
346
+ return;
347
+ }
348
+ if (Date.now() - missingSince < (receipt.cleanupGraceMs ?? 300000)) return;
349
+ removingForUninstall = true;
350
+ clearInterval(monitor);
351
+ if (child.exitCode === null && child.signalCode === null) {
352
+ child.kill('SIGTERM');
353
+ scheduleForcedStop();
354
+ }
355
+ // Ownership files are the evidence needed to stop this exact child. Never
356
+ // erase them while a SIGTERM-ignoring historical daemon can remain alive;
357
+ // the bounded timer escalates only this manager's child to SIGKILL.
358
+ await childExit;
359
+ if (forceStopTimer) clearTimeout(forceStopTimer);
360
+ // A slow upgrade can outlast the absence grace while the old child is still
361
+ // quiescing. Recheck at the destructive boundary: if the package returned,
362
+ // preserve its manager, receipt, hooks and native unit, then ask the service
363
+ // manager to launch the restored generation just like an in-place upgrade.
364
+ if (targetPresent()) process.exit(75);
365
+ await cleanup(receipt);
366
+ process.exit(0);
367
+ }, intervalMs);
368
+
369
+ // launchd/systemd stop the supervisor, not the daemon child directly. Without
370
+ // forwarding the signal the manager exits while Node leaves its child running
371
+ // as an orphan; the service manager then starts a second daemon beside it.
372
+ // Give the child enough time to close its queue/index writers cleanly, but do
373
+ // not let a stuck historical task make uninstall or upgrade hang forever.
374
+ let stoppingSignal = null;
375
+ for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => {
376
+ if (stoppingSignal) return;
377
+ stoppingSignal = signal;
378
+ clearInterval(monitor);
379
+ if (!child.kill(signal)) process.exit(0);
380
+ scheduleForcedStop();
381
+ });
382
+ void childExit.then(async ({ code }) => {
383
+ clearInterval(monitor);
384
+ if (forceStopTimer) clearTimeout(forceStopTimer);
385
+ if (removingForUninstall) return;
386
+ if (stoppingSignal) process.exit(0);
387
+ if (restartingForUpgrade) process.exit(75);
388
+ if (!targetPresent()) {
389
+ await delay(Math.max(0, receipt.cleanupGraceMs ?? 300000));
390
+ if (!targetPresent()) await cleanup(receipt);
391
+ }
392
+ process.exit(code ?? 1);
393
+ });
394
+ `;
@@ -120,7 +120,7 @@ const BENIGN_VALUE_SHAPES = [
120
120
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, // UUIDs: session ids, request ids, vault ids
121
121
  /^[0-9a-f]{32,}$/i, // hex digests (sha256 and friends): integrity data, not credentials
122
122
  /^AAAA/, // base64 of leading zero bytes: well-known placeholders and ssh key headers
123
- /^(?:vlt|vault)_/i // vaultline's own refs (vlt_…, vault_ref ids), which are public by design
123
+ /^(?:vlt|vault)_/i // Sealkeep's own refs (vlt_…, vault_ref ids), which are public by design
124
124
  ];
125
125
  function scanLine(line, lineNumber) {
126
126
  const findings = [];
@@ -2,8 +2,12 @@ import { type Server } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { type AgentId } from "./adapters.js";
4
4
  import { ArchiveQueue } from "./queue.js";
5
- import { localApiTokenPath } from "./paths.js";
5
+ import { drainQueue } from "./worker.js";
6
+ import { localApiTokenPath, localUiProofKeyPath } from "./paths.js";
7
+ type GdriveConnector = typeof cloud.connectGdriveViaCloud;
6
8
  import { type ServiceOptions } from "./service.js";
9
+ import * as cloud from "./cloud.js";
10
+ import type { McpExec } from "./mcp-install.js";
7
11
  declare const setupStorageSchema: z.ZodObject<{
8
12
  provider: z.ZodEnum<{
9
13
  s3: "s3";
@@ -36,13 +40,15 @@ declare const setupStorageSchema: z.ZodObject<{
36
40
  * the B2 key is a native one rather than S3-compatible. All of that is knowable
37
41
  * in a second while the person is still looking at the form.
38
42
  *
39
- * It writes a small object, reads it back, and compares the bytes, because
40
- * anything less proves less: a successful PUT says nothing about whether GET is
41
- * permitted, and this product's whole promise is that the bytes come back.
43
+ * It writes a small object, reads it back, compares the bytes, deletes it, and
44
+ * confirms it is gone. Anything less proves less: a successful PUT says
45
+ * nothing about whether GET is permitted, while a target that cannot delete
46
+ * cannot support move/cleanup workflows safely.
42
47
  *
43
- * Nothing is stored. The credential is used to build a client, held for the
44
- * round trip, and dropped — the caller has not committed to this target yet,
45
- * and a test that saved a broken configuration would be worse than no test.
48
+ * No probe object or credential is retained. The credential is used to build a
49
+ * client, held for the round trip, and dropped — the caller has not committed
50
+ * to this target yet, and a test that saved a broken configuration would be
51
+ * worse than no test.
46
52
  */
47
53
  export declare function testStorageTarget(input: z.infer<typeof setupStorageSchema>): Promise<{
48
54
  ok: boolean;
@@ -56,16 +62,27 @@ export type SessionRow = {
56
62
  modifiedAt: string;
57
63
  /** True when an archive already covers this file at exactly this length. */
58
64
  archived: boolean;
59
- /** True when that archive also has a verified copy in storage — off this machine, not just sealed on it. */
65
+ /** True when one exact archive and every delta-chain link it needs have verified copies off this machine. */
60
66
  stored: boolean;
61
- /** True while a queued or in-flight job exists for this file: it is being sealed and must not be selected again. */
67
+ /**
68
+ * Exact V2 archive head whose complete chain is present on this machine and
69
+ * can therefore enter normal storage routing. Null unless the live session
70
+ * is archived-but-not-stored; the dashboard must never infer this from
71
+ * `archived` alone.
72
+ */
73
+ sendableArchiveId: string | null;
74
+ /** True while a queued or in-flight job exists for this file. It may be waiting; it must not be selected again. */
62
75
  queued: boolean;
63
- /** Why a queued job is waiting instead of running — today, the disk guard's message. Null when it is genuinely being worked. A held row saying "sealing…" was a lie the queue knew better than. */
76
+ /** Why a queued job is waiting instead of running. Null when it is genuinely being worked. A held row saying "sealing…" would be a lie the queue knows better than. */
64
77
  held: string | null;
78
+ /** What is holding a ready job. Policy pauses do not need a three-second browser retry loop; disk/retry holds still do. */
79
+ heldKind: "policy" | "space" | "retry" | null;
65
80
  /** Whole percentages of the source read so far while sealing, else null. The number a person actually watches on a 1.6 GB row. */
66
81
  sealingPct: number | null;
67
82
  /** The project this session ran in, when its metadata says — the Sessions page filters and labels on it. Null when nothing says. */
68
83
  project: string | null;
84
+ /** Stable identity used for team mapping; the display label above is never an access key. */
85
+ projectKey: string | null;
69
86
  /** The working directory behind `project`, best-effort decoded; a tooltip, never a filter. */
70
87
  projectPath: string | null;
71
88
  };
@@ -83,13 +100,15 @@ export type SessionRow = {
83
100
  * done" is as useful as "this one is not", and hiding them would make the list
84
101
  * shrink as you work and lose your place.
85
102
  */
86
- export declare function listSessions(dataDir: string, queue?: ArchiveQueue): Promise<{
103
+ export declare function listSessions(dataDir: string, queue?: ArchiveQueue, home?: string): Promise<{
87
104
  sessions: SessionRow[];
88
105
  totalBytes: number;
89
106
  unarchivedBytes: number;
90
107
  }>;
91
- export declare function ensureDraining(dataDir: string, queue: ArchiveQueue): Promise<void>;
92
- export declare function archiveSelected(dataDir: string, paths: string[], queue: ArchiveQueue): Promise<{
108
+ export declare function ensureDraining(dataDir: string, queue: ArchiveQueue, operations?: {
109
+ drainQueue?: typeof drainQueue;
110
+ }): Promise<void>;
111
+ export declare function archiveSelected(dataDir: string, paths: string[], queue: ArchiveQueue, home?: string): Promise<{
93
112
  requested: number;
94
113
  queued: number;
95
114
  bytes: number;
@@ -101,23 +120,34 @@ export declare function archiveSelected(dataDir: string, paths: string[], queue:
101
120
  error?: string;
102
121
  }[];
103
122
  }>;
104
- export { localApiTokenPath };
123
+ export { localApiTokenPath, localUiProofKeyPath };
105
124
  /** Mints the loopback token once and keeps it owner-readable. It is never logged or returned by an endpoint. */
106
125
  export declare function localApiToken(dataDir: string): Promise<string>;
126
+ /** Keyed listener identity proof; it grants no access to any /v1 data. */
127
+ export declare function localUiProofKey(dataDir: string): Promise<string>;
128
+ export declare const LOCAL_UI_PROOF_VERSION = 1;
129
+ export declare const localUiProof: (secret: string, nonce: string, port: number, instanceId: string) => string;
130
+ /** Owner-authenticated request for a short-lived browser bootstrap ticket. */
131
+ export declare const localUiBootstrapProof: (secret: string, nonce: string, port: number, instanceId: string) => string;
107
132
  /** One row of GET /v1/cloud/copies: a copy held off this machine, wherever it is held. */
108
133
  export type StoredCopy = {
109
134
  ref: string;
135
+ archiveId?: string;
136
+ copyId?: string;
137
+ provider?: string;
138
+ targetId?: string | null;
110
139
  bytes: number;
111
140
  addedAt: string | null;
112
141
  state: string;
113
142
  /** True when this machine no longer holds the archive's bytes, so this stored copy is the archive. */
114
143
  lastCopy: boolean;
115
144
  };
116
- /** Test seam: the setup endpoints act on the real machine, so tests need a home and a service manager of their own. */
145
+ /** Test seams: setup acts on the real machine, so tests need a home and command runners of their own. */
117
146
  export type SetupEnvironment = {
118
147
  home?: string;
119
148
  platform?: NodeJS.Platform;
120
149
  exec?: ServiceOptions["exec"];
150
+ mcpExec?: McpExec;
121
151
  };
122
152
  /**
123
153
  * The loopback API shared by the CLI, dashboard, and MCP surfaces. It is the only
@@ -129,4 +159,15 @@ export type SetupEnvironment = {
129
159
  export declare function createLocalApiServer(dataDir: string, token: string, options?: {
130
160
  queue?: ArchiveQueue;
131
161
  setup?: SetupEnvironment;
162
+ gdriveConnector?: GdriveConnector;
163
+ listenerInstanceId?: string;
164
+ /** True hands post-unlock draining to the supervised daemon that is waiting to start. */
165
+ onRecoveryPhraseAvailable?: () => boolean;
166
+ /** Deterministic transfer seam; production dynamically loads uploadArchive. */
167
+ uploadArchive?: typeof import("./upload.js")["uploadArchive"];
168
+ /** Maximum silence between real transfer byte/checkpoint progress. */
169
+ uploadIdleMs?: number;
170
+ /** Short seams keep browser handoff expiry tests fast; production uses the defaults. */
171
+ bootstrapRequestTtlMs?: number;
172
+ bootstrapTicketTtlMs?: number;
132
173
  }): Server;