sealkeep 0.5.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 (180) hide show
  1. package/ARCHITECTURE.md +201 -0
  2. package/CHANGELOG.md +218 -0
  3. package/CONTROL_PLANE.md +86 -0
  4. package/LICENSE +34 -0
  5. package/README.md +249 -0
  6. package/THIRD_PARTY.md +22 -0
  7. package/THREAT_MODEL.md +107 -0
  8. package/dist/packages/vaultline-crypto/src/aead.d.ts +12 -0
  9. package/dist/packages/vaultline-crypto/src/aead.js +24 -0
  10. package/dist/packages/vaultline-crypto/src/chunk-access.d.ts +39 -0
  11. package/dist/packages/vaultline-crypto/src/chunk-access.js +93 -0
  12. package/dist/packages/vaultline-crypto/src/envelope.d.ts +71 -0
  13. package/dist/packages/vaultline-crypto/src/envelope.js +188 -0
  14. package/dist/packages/vaultline-crypto/src/format.d.ts +106 -0
  15. package/dist/packages/vaultline-crypto/src/format.js +43 -0
  16. package/dist/packages/vaultline-crypto/src/index.d.ts +5 -0
  17. package/dist/packages/vaultline-crypto/src/index.js +5 -0
  18. package/dist/packages/vaultline-crypto/src/recipients.d.ts +42 -0
  19. package/dist/packages/vaultline-crypto/src/recipients.js +129 -0
  20. package/dist/packages/vaultline-crypto/src/sha256-stream.d.ts +41 -0
  21. package/dist/packages/vaultline-crypto/src/sha256-stream.js +206 -0
  22. package/dist/packages/vaultline-crypto/src/stream.d.ts +139 -0
  23. package/dist/packages/vaultline-crypto/src/stream.js +477 -0
  24. package/dist/site/index.html +1542 -0
  25. package/dist/site.zip +0 -0
  26. package/dist/src/activity.d.ts +22 -0
  27. package/dist/src/activity.js +52 -0
  28. package/dist/src/adapters.d.ts +212 -0
  29. package/dist/src/adapters.js +533 -0
  30. package/dist/src/audit.d.ts +24 -0
  31. package/dist/src/audit.js +41 -0
  32. package/dist/src/autopilot.d.ts +77 -0
  33. package/dist/src/autopilot.js +148 -0
  34. package/dist/src/bip39-wordlist.d.ts +15 -0
  35. package/dist/src/bip39-wordlist.js +272 -0
  36. package/dist/src/branding.d.ts +31 -0
  37. package/dist/src/branding.js +31 -0
  38. package/dist/src/chunk-store.d.ts +142 -0
  39. package/dist/src/chunk-store.js +502 -0
  40. package/dist/src/cli.d.ts +2 -0
  41. package/dist/src/cli.js +2035 -0
  42. package/dist/src/cloud.d.ts +434 -0
  43. package/dist/src/cloud.js +851 -0
  44. package/dist/src/control-plane/auth.d.ts +62 -0
  45. package/dist/src/control-plane/auth.js +123 -0
  46. package/dist/src/control-plane/server.d.ts +31 -0
  47. package/dist/src/control-plane/server.js +263 -0
  48. package/dist/src/control-plane/store.d.ts +101 -0
  49. package/dist/src/control-plane/store.js +82 -0
  50. package/dist/src/control-plane-cli.d.ts +2 -0
  51. package/dist/src/control-plane-cli.js +37 -0
  52. package/dist/src/control-plane-server.d.ts +10 -0
  53. package/dist/src/control-plane-server.js +11 -0
  54. package/dist/src/control-plane.d.ts +78 -0
  55. package/dist/src/control-plane.js +61 -0
  56. package/dist/src/crypto.d.ts +56 -0
  57. package/dist/src/crypto.js +132 -0
  58. package/dist/src/daemon.d.ts +52 -0
  59. package/dist/src/daemon.js +142 -0
  60. package/dist/src/dashboard-cli.d.ts +2 -0
  61. package/dist/src/dashboard-cli.js +20 -0
  62. package/dist/src/disk.d.ts +110 -0
  63. package/dist/src/disk.js +169 -0
  64. package/dist/src/doctor.d.ts +11 -0
  65. package/dist/src/doctor.js +198 -0
  66. package/dist/src/enroll.d.ts +27 -0
  67. package/dist/src/enroll.js +136 -0
  68. package/dist/src/errors.d.ts +26 -0
  69. package/dist/src/errors.js +23 -0
  70. package/dist/src/heartbeat.d.ts +89 -0
  71. package/dist/src/heartbeat.js +120 -0
  72. package/dist/src/index-sync.d.ts +53 -0
  73. package/dist/src/index-sync.js +147 -0
  74. package/dist/src/leakscan.d.ts +48 -0
  75. package/dist/src/leakscan.js +222 -0
  76. package/dist/src/local-api.d.ts +132 -0
  77. package/dist/src/local-api.js +1757 -0
  78. package/dist/src/managed-chunks.d.ts +55 -0
  79. package/dist/src/managed-chunks.js +108 -0
  80. package/dist/src/mcp-install.d.ts +52 -0
  81. package/dist/src/mcp-install.js +140 -0
  82. package/dist/src/mcp.d.ts +1 -0
  83. package/dist/src/mcp.js +59 -0
  84. package/dist/src/migrate.d.ts +35 -0
  85. package/dist/src/migrate.js +88 -0
  86. package/dist/src/mnemonic.d.ts +60 -0
  87. package/dist/src/mnemonic.js +134 -0
  88. package/dist/src/net.d.ts +2 -0
  89. package/dist/src/net.js +16 -0
  90. package/dist/src/notify.d.ts +46 -0
  91. package/dist/src/notify.js +84 -0
  92. package/dist/src/offload.d.ts +117 -0
  93. package/dist/src/offload.js +331 -0
  94. package/dist/src/onboarding.d.ts +10 -0
  95. package/dist/src/onboarding.js +44 -0
  96. package/dist/src/packages.d.ts +126 -0
  97. package/dist/src/packages.js +114 -0
  98. package/dist/src/passkey.d.ts +26 -0
  99. package/dist/src/passkey.js +54 -0
  100. package/dist/src/password-lock.d.ts +19 -0
  101. package/dist/src/password-lock.js +156 -0
  102. package/dist/src/paths.d.ts +9 -0
  103. package/dist/src/paths.js +24 -0
  104. package/dist/src/providers/gcs.d.ts +133 -0
  105. package/dist/src/providers/gcs.js +235 -0
  106. package/dist/src/providers/gdrive.d.ts +156 -0
  107. package/dist/src/providers/gdrive.js +335 -0
  108. package/dist/src/providers/index.d.ts +45 -0
  109. package/dist/src/providers/index.js +74 -0
  110. package/dist/src/providers/s3.d.ts +174 -0
  111. package/dist/src/providers/s3.js +345 -0
  112. package/dist/src/providers/sigv4.d.ts +78 -0
  113. package/dist/src/providers/sigv4.js +112 -0
  114. package/dist/src/queue.d.ts +185 -0
  115. package/dist/src/queue.js +286 -0
  116. package/dist/src/recovery.d.ts +40 -0
  117. package/dist/src/recovery.js +132 -0
  118. package/dist/src/rehydrate.d.ts +43 -0
  119. package/dist/src/rehydrate.js +66 -0
  120. package/dist/src/restore.d.ts +34 -0
  121. package/dist/src/restore.js +80 -0
  122. package/dist/src/retention.d.ts +251 -0
  123. package/dist/src/retention.js +446 -0
  124. package/dist/src/rotate.d.ts +47 -0
  125. package/dist/src/rotate.js +95 -0
  126. package/dist/src/search.d.ts +147 -0
  127. package/dist/src/search.js +677 -0
  128. package/dist/src/secrets.d.ts +86 -0
  129. package/dist/src/secrets.js +220 -0
  130. package/dist/src/service.d.ts +73 -0
  131. package/dist/src/service.js +197 -0
  132. package/dist/src/share.d.ts +34 -0
  133. package/dist/src/share.js +68 -0
  134. package/dist/src/spool.d.ts +97 -0
  135. package/dist/src/spool.js +213 -0
  136. package/dist/src/start-tui.d.ts +17 -0
  137. package/dist/src/start-tui.js +113 -0
  138. package/dist/src/start.d.ts +75 -0
  139. package/dist/src/start.js +101 -0
  140. package/dist/src/storage-setup.d.ts +49 -0
  141. package/dist/src/storage-setup.js +222 -0
  142. package/dist/src/storage-targets.d.ts +40 -0
  143. package/dist/src/storage-targets.js +147 -0
  144. package/dist/src/stream-to-cloud.d.ts +76 -0
  145. package/dist/src/stream-to-cloud.js +820 -0
  146. package/dist/src/sync-rules.d.ts +85 -0
  147. package/dist/src/sync-rules.js +125 -0
  148. package/dist/src/trash.d.ts +15 -0
  149. package/dist/src/trash.js +63 -0
  150. package/dist/src/tui.d.ts +18 -0
  151. package/dist/src/tui.js +179 -0
  152. package/dist/src/types.d.ts +191 -0
  153. package/dist/src/types.js +3 -0
  154. package/dist/src/ui-server.d.ts +187 -0
  155. package/dist/src/ui-server.js +293 -0
  156. package/dist/src/ui.d.ts +41 -0
  157. package/dist/src/ui.js +102 -0
  158. package/dist/src/update.d.ts +30 -0
  159. package/dist/src/update.js +56 -0
  160. package/dist/src/upload.d.ts +46 -0
  161. package/dist/src/upload.js +80 -0
  162. package/dist/src/vault.d.ts +208 -0
  163. package/dist/src/vault.js +812 -0
  164. package/dist/src/watcher.d.ts +34 -0
  165. package/dist/src/watcher.js +121 -0
  166. package/dist/src/worker.d.ts +52 -0
  167. package/dist/src/worker.js +190 -0
  168. package/package.json +65 -0
  169. package/web/app.js +1372 -0
  170. package/web/index.html +476 -0
  171. package/web/rail.js +308 -0
  172. package/web/retention.html +17 -0
  173. package/web/rules-view.js +249 -0
  174. package/web/sessions-view.js +448 -0
  175. package/web/sessions.html +17 -0
  176. package/web/setup-api.js +181 -0
  177. package/web/setup-logic.js +394 -0
  178. package/web/setup.html +419 -0
  179. package/web/setup.js +697 -0
  180. package/web/style.css +990 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Seal-time secret detection. Deterministic, offline, dependency-free.
3
+ *
4
+ * Agent transcripts are where pasted credentials go to be forgotten: an AWS
5
+ * key dropped into a prompt in March is still in the archive in November, and
6
+ * a sealed archive keeps it faithfully forever. Detection has to happen here,
7
+ * on this machine, at seal time — E2EE means nothing downstream (bucket,
8
+ * control plane, org dashboard) can ever look at the plaintext, so a scanner
9
+ * that lives anywhere else would need the very access the product promises
10
+ * away.
11
+ *
12
+ * This module is detection only, in the gitleaks/trufflehog family: fixed
13
+ * patterns plus a Shannon-entropy fallback. No AI, no network, no config —
14
+ * the same bytes always produce the same findings, which is what lets the
15
+ * exit code gate a script.
16
+ *
17
+ * Phase 2 — integration points, deliberately not implemented here:
18
+ *
19
+ * (a) Worker seal path: worker.ts calls scanFileStreaming(job.sourcePath)
20
+ * just before sealing and stores the findings — kind, severity, line,
21
+ * masked preview — as metadata beside the archive record, so "which
22
+ * archives hold live credentials?" is answerable without opening any
23
+ * of them.
24
+ * (b) Org rollup: the control plane receives kind/severity/counts only —
25
+ * never content, never lines, never previews. Because only this machine
26
+ * ever holds plaintext, an org can learn *that* a credential was sealed
27
+ * and *what sort*, but the credential itself cannot travel.
28
+ */
29
+ import { createReadStream } from "node:fs";
30
+ import { readdir, stat } from "node:fs/promises";
31
+ import { join } from "node:path";
32
+ import { createInterface } from "node:readline";
33
+ import { fail } from "./errors.js";
34
+ /**
35
+ * First four characters, then only the length. A leak report that repeats the
36
+ * leak is a second leak: findings travel further than the transcript did —
37
+ * terminals, CI logs, screenshots, phase-2 metadata — so the preview carries
38
+ * just enough to locate the match and never enough to use it.
39
+ */
40
+ export function maskSecret(secret) {
41
+ return `${secret.slice(0, 4)}…(${secret.length} chars)`;
42
+ }
43
+ /** Character-frequency Shannon entropy, in bits per character. */
44
+ export function shannonEntropy(value) {
45
+ const chars = [...value];
46
+ if (chars.length === 0)
47
+ return 0;
48
+ const counts = new Map();
49
+ for (const char of chars)
50
+ counts.set(char, (counts.get(char) ?? 0) + 1);
51
+ let entropy = 0;
52
+ for (const count of counts.values()) {
53
+ const p = count / chars.length;
54
+ entropy -= p * Math.log2(p);
55
+ }
56
+ return entropy;
57
+ }
58
+ /** A JWT is three base64url segments, but so is a lot of noise. The header must decode to JSON naming an alg. */
59
+ function plausibleJwtHeader(token) {
60
+ try {
61
+ const header = JSON.parse(Buffer.from(token.slice(0, token.indexOf(".")), "base64url").toString("utf8"));
62
+ return typeof header === "object" && header !== null && typeof header.alg === "string";
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ /**
69
+ * Pattern detectors: shapes only a real credential takes. Every one is
70
+ * anchored on both sides (a 17th character disqualifies an AWS key id rather
71
+ * than truncating it) because precision is the whole game — false positives
72
+ * are how people come to disable scanners, and a disabled scanner catches
73
+ * nothing. A hit here is near-certain, so these report "high".
74
+ *
75
+ * Transcripts are jsonl, so quotes often arrive escaped (`key=\"value\"`);
76
+ * the assignment-context patterns accept the backslash.
77
+ */
78
+ const PATTERN_DETECTORS = [
79
+ // AKIA (long-lived) / ASIA (temporary) + exactly 16 more upper-alnum.
80
+ { kind: "aws-access-key-id", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/dg },
81
+ // The 40-char base64-ish value alone is far too common a shape, so it only
82
+ // counts inside an assignment whose name says "secret".
83
+ {
84
+ kind: "aws-secret-access-key",
85
+ pattern: /[A-Za-z0-9_.-]*secret[A-Za-z0-9_.-]*(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+=])/dgi,
86
+ group: 1
87
+ },
88
+ { kind: "github-token", pattern: /\b(?:(?:ghp|gho|ghs)_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,255})\b/dg },
89
+ { kind: "google-api-key", pattern: /(?<![0-9A-Za-z_-])AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/dg },
90
+ { kind: "slack-token", pattern: /\bxox[bpars]-[A-Za-z0-9][A-Za-z0-9-]{9,}(?![A-Za-z0-9-])/dg },
91
+ // The armor header is proof enough; the body may be split across lines, but
92
+ // no innocent text starts a line of itself this way.
93
+ { kind: "private-key", pattern: /-----BEGIN (?:(?:RSA|EC|OPENSSH|PGP|ENCRYPTED) )?PRIVATE KEY(?: BLOCK)?-----/dg },
94
+ {
95
+ kind: "jwt",
96
+ pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9_.-])/dg,
97
+ validate: plausibleJwtHeader
98
+ }
99
+ ];
100
+ /** Tests derive their vectors from these rather than restating the numbers. */
101
+ export const GENERIC_MIN_LENGTH = 24;
102
+ export const GENERIC_ENTROPY_THRESHOLD = 4.2;
103
+ /**
104
+ * The generic fallback: something named like a credential, assigned a value
105
+ * long and dense enough that prose and identifiers cannot reach it. This is
106
+ * where scanners lose their users, so it is gated three ways — assignment
107
+ * context, length + entropy, and an allowlist of known-benign shapes — and
108
+ * even then it reports only "medium": pattern hits are certainties, entropy
109
+ * hits are suspicions, and the exit code (phase 1) and seal metadata
110
+ * (phase 2) treat them differently.
111
+ */
112
+ const GENERIC_ASSIGNMENT = new RegExp(String.raw `[A-Za-z0-9_.-]*(?:key|token|secret|password|passwd|pwd)[A-Za-z0-9_.-]*(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?([A-Za-z0-9+/=_-]{${GENERIC_MIN_LENGTH},})(?![A-Za-z0-9+/=_-])`, "dgi");
113
+ /**
114
+ * Benign shapes that clear the length bar and would otherwise page someone.
115
+ * Pure hex (and UUIDs) cannot actually exceed 4.2 bits/char, but relying on
116
+ * that emergent arithmetic would make the next threshold change a regression;
117
+ * the allowlist states the intent.
118
+ */
119
+ const BENIGN_VALUE_SHAPES = [
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
+ /^[0-9a-f]{32,}$/i, // hex digests (sha256 and friends): integrity data, not credentials
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
124
+ ];
125
+ function scanLine(line, lineNumber) {
126
+ const findings = [];
127
+ const claimed = [];
128
+ for (const detector of PATTERN_DETECTORS) {
129
+ for (const match of line.matchAll(detector.pattern)) {
130
+ const span = match.indices?.[detector.group ?? 0];
131
+ if (!span)
132
+ continue;
133
+ const secret = line.slice(span[0], span[1]);
134
+ if (detector.validate && !detector.validate(secret))
135
+ continue;
136
+ claimed.push(span);
137
+ findings.push({ kind: detector.kind, line: lineNumber, column: span[0] + 1, preview: maskSecret(secret), severity: "high" });
138
+ }
139
+ }
140
+ for (const match of line.matchAll(GENERIC_ASSIGNMENT)) {
141
+ const span = match.indices?.[1];
142
+ if (!span)
143
+ continue;
144
+ // A secret a precise detector already named is one finding, not two.
145
+ if (claimed.some(([start, end]) => span[0] < end && start < span[1]))
146
+ continue;
147
+ const value = line.slice(span[0], span[1]);
148
+ if (BENIGN_VALUE_SHAPES.some((shape) => shape.test(value)))
149
+ continue;
150
+ if (shannonEntropy(value) <= GENERIC_ENTROPY_THRESHOLD)
151
+ continue;
152
+ findings.push({ kind: "high-entropy-value", line: lineNumber, column: span[0] + 1, preview: maskSecret(value), severity: "medium" });
153
+ }
154
+ return findings.sort((a, b) => a.column - b.column || a.kind.localeCompare(b.kind));
155
+ }
156
+ export function scanText(text) {
157
+ return text.split(/\r?\n/).flatMap((line, index) => scanLine(line, index + 1));
158
+ }
159
+ /**
160
+ * Line-by-line over a read stream: memory is bounded by the longest single
161
+ * line, never by the file, so a multi-GB jsonl scans in the same footprint as
162
+ * a small one. An unreadable input rejects — a scanner that swallows a read
163
+ * error would be reporting "clean" about bytes it never saw.
164
+ */
165
+ export async function scanFileStreaming(path, onFinding) {
166
+ const input = createReadStream(path, { encoding: "utf8" });
167
+ const lines = createInterface({ input, crlfDelay: Infinity });
168
+ const findings = [];
169
+ let lineNumber = 0;
170
+ try {
171
+ for await (const line of lines) {
172
+ lineNumber += 1;
173
+ for (const finding of scanLine(line, lineNumber)) {
174
+ findings.push(finding);
175
+ onFinding?.(finding);
176
+ }
177
+ }
178
+ }
179
+ finally {
180
+ lines.close();
181
+ input.close();
182
+ }
183
+ return findings;
184
+ }
185
+ /**
186
+ * Every *.jsonl under a directory, depth-first, sorted at each level so a
187
+ * report is comparable run to run. Symlinks are left alone: transcript trees
188
+ * do not need them and following them invites cycles.
189
+ */
190
+ async function jsonlFiles(dir) {
191
+ const entries = await readdir(dir, { withFileTypes: true });
192
+ const found = [];
193
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
194
+ const path = join(dir, entry.name);
195
+ if (entry.isDirectory())
196
+ found.push(...(await jsonlFiles(path)));
197
+ else if (entry.isFile() && entry.name.endsWith(".jsonl"))
198
+ found.push(path);
199
+ }
200
+ return found;
201
+ }
202
+ /**
203
+ * A file is scanned as pointed at, whatever its name; a directory means every
204
+ * *.jsonl beneath it. Only high findings set the exit code — failing a script
205
+ * over a "medium" suspicion is exactly the cry-wolf behaviour the severity
206
+ * split exists to avoid.
207
+ */
208
+ export async function scanPath(target, onFinding) {
209
+ const info = await stat(target).catch(() => fail("source_unreadable", `Cannot read ${target}`));
210
+ const files = info.isDirectory() ? await jsonlFiles(target) : [target];
211
+ const findings = [];
212
+ for (const file of files) {
213
+ await scanFileStreaming(file, (finding) => {
214
+ const placed = { ...finding, file };
215
+ findings.push(placed);
216
+ onFinding?.(placed);
217
+ });
218
+ }
219
+ const high = findings.filter((finding) => finding.severity === "high").length;
220
+ const medium = findings.length - high;
221
+ return { files: files.length, findings, high, medium, exitCode: high > 0 ? 1 : 0 };
222
+ }
@@ -0,0 +1,132 @@
1
+ import { type Server } from "node:http";
2
+ import { z } from "zod";
3
+ import { type AgentId } from "./adapters.js";
4
+ import { ArchiveQueue } from "./queue.js";
5
+ import { localApiTokenPath } from "./paths.js";
6
+ import { type ServiceOptions } from "./service.js";
7
+ declare const setupStorageSchema: z.ZodObject<{
8
+ provider: z.ZodEnum<{
9
+ s3: "s3";
10
+ r2: "r2";
11
+ b2: "b2";
12
+ gcs: "gcs";
13
+ gdrive: "gdrive";
14
+ }>;
15
+ bucket: z.ZodString;
16
+ prefix: z.ZodDefault<z.ZodString>;
17
+ region: z.ZodOptional<z.ZodString>;
18
+ endpoint: z.ZodOptional<z.ZodString>;
19
+ credentials: z.ZodUnion<readonly [z.ZodObject<{
20
+ accessKeyId: z.ZodString;
21
+ secretAccessKey: z.ZodString;
22
+ sessionToken: z.ZodOptional<z.ZodString>;
23
+ }, z.core.$strict>, z.ZodObject<{
24
+ clientEmail: z.ZodString;
25
+ privateKey: z.ZodString;
26
+ }, z.core.$strict>]>;
27
+ confirm: z.ZodDefault<z.ZodBoolean>;
28
+ }, z.core.$strict>;
29
+ /**
30
+ * Proves a storage target works, before anyone trusts it with an archive.
31
+ *
32
+ * Credentials are the step of setup most likely to be wrong, and every way of
33
+ * being wrong used to surface at the same moment: hours later, in a background
34
+ * upload, as a failure nobody was watching. The bucket name has a typo, the key
35
+ * lacks PutObject, the region is the console's display name rather than its id,
36
+ * the B2 key is a native one rather than S3-compatible. All of that is knowable
37
+ * in a second while the person is still looking at the form.
38
+ *
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.
42
+ *
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.
46
+ */
47
+ export declare function testStorageTarget(input: z.infer<typeof setupStorageSchema>): Promise<{
48
+ ok: boolean;
49
+ detail: string;
50
+ probeKey?: string;
51
+ }>;
52
+ export type SessionRow = {
53
+ agent: AgentId;
54
+ path: string;
55
+ bytes: number;
56
+ modifiedAt: string;
57
+ /** True when an archive already covers this file at exactly this length. */
58
+ archived: boolean;
59
+ /** True when that archive also has a verified copy in storage — off this machine, not just sealed on it. */
60
+ 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. */
62
+ 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. */
64
+ held: string | null;
65
+ /** 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
+ sealingPct: number | null;
67
+ /** The project this session ran in, when its metadata says — the Sessions page filters and labels on it. Null when nothing says. */
68
+ project: string | null;
69
+ /** The working directory behind `project`, best-effort decoded; a tooltip, never a filter. */
70
+ projectPath: string | null;
71
+ };
72
+ /**
73
+ * Every session on this machine, with the two facts a person needs to choose:
74
+ * how big it is, and when they last touched it.
75
+ *
76
+ * This exists because archiving everything automatically is the wrong default
77
+ * for the situation people are actually in — a nearly full disk, a handful of
78
+ * enormous transcripts, and no appetite for a background process deciding which
79
+ * ones to work through. Choosing is cheap when you can see sizes and dates;
80
+ * guessing is not.
81
+ *
82
+ * Already-archived sessions are marked rather than hidden, because "this one is
83
+ * done" is as useful as "this one is not", and hiding them would make the list
84
+ * shrink as you work and lose your place.
85
+ */
86
+ export declare function listSessions(dataDir: string, queue?: ArchiveQueue): Promise<{
87
+ sessions: SessionRow[];
88
+ totalBytes: number;
89
+ unarchivedBytes: number;
90
+ }>;
91
+ export declare function ensureDraining(dataDir: string, queue: ArchiveQueue): Promise<void>;
92
+ export declare function archiveSelected(dataDir: string, paths: string[], queue: ArchiveQueue): Promise<{
93
+ requested: number;
94
+ queued: number;
95
+ bytes: number;
96
+ background: boolean;
97
+ results: {
98
+ path: string;
99
+ ok: boolean;
100
+ bytes?: number;
101
+ error?: string;
102
+ }[];
103
+ }>;
104
+ export { localApiTokenPath };
105
+ /** Mints the loopback token once and keeps it owner-readable. It is never logged or returned by an endpoint. */
106
+ export declare function localApiToken(dataDir: string): Promise<string>;
107
+ /** One row of GET /v1/cloud/copies: a copy held off this machine, wherever it is held. */
108
+ export type StoredCopy = {
109
+ ref: string;
110
+ bytes: number;
111
+ addedAt: string | null;
112
+ state: string;
113
+ /** True when this machine no longer holds the archive's bytes, so this stored copy is the archive. */
114
+ lastCopy: boolean;
115
+ };
116
+ /** Test seam: the setup endpoints act on the real machine, so tests need a home and a service manager of their own. */
117
+ export type SetupEnvironment = {
118
+ home?: string;
119
+ platform?: NodeJS.Platform;
120
+ exec?: ServiceOptions["exec"];
121
+ };
122
+ /**
123
+ * The loopback API shared by the CLI, dashboard, and MCP surfaces. It is the only
124
+ * place local surfaces read state from, so business logic is never duplicated.
125
+ *
126
+ * It deliberately cannot decrypt: no endpoint accepts a recovery phrase, and
127
+ * archive listings omit wrapped key material.
128
+ */
129
+ export declare function createLocalApiServer(dataDir: string, token: string, options?: {
130
+ queue?: ArchiveQueue;
131
+ setup?: SetupEnvironment;
132
+ }): Server;