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.
- package/ARCHITECTURE.md +163 -14
- package/CHANGELOG.md +223 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1698 -163
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +316 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.js +27 -21
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5897 -290
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +226 -30
- package/dist/src/vault.js +1776 -192
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { link, lstat, open, realpath, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants } from "node:fs";
|
|
4
|
+
import { openForFlush } from "./flush.js";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { validateNoFollowPath } from "./reclaim-transaction.js";
|
|
8
|
+
/**
|
|
9
|
+
* A stable, model-visible marker. It deliberately lives in an ordinary Codex
|
|
10
|
+
* message rather than in a made-up rollout record type: old and new Codex
|
|
11
|
+
* builds may ignore fields they do not know, but the conversation loader has
|
|
12
|
+
* always understood user/assistant messages.
|
|
13
|
+
*/
|
|
14
|
+
export const CODEX_RESUME_STUB_MARKER = "sealkeep://codex-resume-stub/v1";
|
|
15
|
+
const MAX_STUB_BYTES = 1024 * 1024;
|
|
16
|
+
const MAX_META_BYTES = 1024 * 1024;
|
|
17
|
+
const PREPARED_STUB_BASENAME = /^\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.sealkeep-resume-stub\.partial$/i;
|
|
18
|
+
const POINTER_OPEN_FLAGS = fsConstants.O_RDONLY
|
|
19
|
+
| (typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0)
|
|
20
|
+
| (typeof fsConstants.O_NONBLOCK === "number" ? fsConstants.O_NONBLOCK : 0);
|
|
21
|
+
function samePointerSnapshot(left, right) {
|
|
22
|
+
return right.isFile() && left.dev === right.dev && left.ino === right.ino && left.size === right.size
|
|
23
|
+
&& left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
24
|
+
}
|
|
25
|
+
/** A growing rollout must not turn a <=1 MiB stat into an unbounded readFile. */
|
|
26
|
+
async function readStablePointer(handle, path, before) {
|
|
27
|
+
if (!before.isFile() || before.size < 0n || before.size > BigInt(MAX_STUB_BYTES)) {
|
|
28
|
+
throw new Error(`Codex resume pointer candidate exceeds the regular-file read bound and was retained: ${path}`);
|
|
29
|
+
}
|
|
30
|
+
// One extra byte detects growth even on filesystems with coarse timestamps.
|
|
31
|
+
// The allocation and total bytes read remain bounded regardless of appends.
|
|
32
|
+
const buffer = Buffer.alloc(Number(before.size) + 1);
|
|
33
|
+
let offset = 0;
|
|
34
|
+
while (offset < buffer.length) {
|
|
35
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
36
|
+
if (!bytesRead)
|
|
37
|
+
break;
|
|
38
|
+
offset += bytesRead;
|
|
39
|
+
}
|
|
40
|
+
const after = await handle.stat({ bigint: true });
|
|
41
|
+
const named = await lstat(path, { bigint: true });
|
|
42
|
+
if (offset !== Number(before.size) || !samePointerSnapshot(before, after) || !samePointerSnapshot(before, named)) {
|
|
43
|
+
throw new Error(`Codex resume pointer candidate changed while it was validated and was retained: ${path}`);
|
|
44
|
+
}
|
|
45
|
+
return buffer.subarray(0, offset);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Only real, non-symlinked native Codex rollout locations participate.
|
|
49
|
+
*
|
|
50
|
+
* Lexical containment is insufficient here: a symlinked `sessions` directory
|
|
51
|
+
* can make a path spell `~/.codex/sessions/...` while naming an unrelated file
|
|
52
|
+
* elsewhere. Check every user-controlled path component with lstat, then check
|
|
53
|
+
* canonical containment as a second independent condition.
|
|
54
|
+
*/
|
|
55
|
+
export async function isNativeCodexSessionPath(path, home = homedir()) {
|
|
56
|
+
return validateNativeCodexSessionPath(path, home, "file").then(() => true, () => false);
|
|
57
|
+
}
|
|
58
|
+
/** Canonical no-follow validation shared by reclaim and crash reconciliation. */
|
|
59
|
+
export async function validateNativeCodexSessionPath(path, home = homedir(), leaf = "file") {
|
|
60
|
+
const lexicalHome = resolve(home);
|
|
61
|
+
const suffix = relative(lexicalHome, resolve(path));
|
|
62
|
+
if (suffix === "" || suffix.startsWith(`..${sep}`) || suffix === ".." || isAbsolute(suffix)) {
|
|
63
|
+
throw new Error(`Codex session path is outside the selected home: ${path}`);
|
|
64
|
+
}
|
|
65
|
+
const canonicalHome = await realpath(lexicalHome).catch(() => null);
|
|
66
|
+
if (!canonicalHome)
|
|
67
|
+
throw new Error(`Codex home is unavailable: ${lexicalHome}`);
|
|
68
|
+
const candidate = join(canonicalHome, suffix);
|
|
69
|
+
await validateNoFollowPath(candidate, [join(canonicalHome, ".codex", "sessions"), join(canonicalHome, ".codex", "archived_sessions")], { leaf });
|
|
70
|
+
return resolve(path);
|
|
71
|
+
}
|
|
72
|
+
async function firstLine(path) {
|
|
73
|
+
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
74
|
+
const handle = await open(path, fsConstants.O_RDONLY | noFollow);
|
|
75
|
+
try {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
let offset = 0;
|
|
78
|
+
while (offset <= MAX_META_BYTES) {
|
|
79
|
+
const chunk = Buffer.alloc(Math.min(64 * 1024, MAX_META_BYTES + 1 - offset));
|
|
80
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, offset);
|
|
81
|
+
if (bytesRead === 0)
|
|
82
|
+
break;
|
|
83
|
+
const part = chunk.subarray(0, bytesRead);
|
|
84
|
+
const newline = part.indexOf(0x0a);
|
|
85
|
+
if (newline !== -1) {
|
|
86
|
+
chunks.push(part.subarray(0, newline));
|
|
87
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
88
|
+
}
|
|
89
|
+
chunks.push(part);
|
|
90
|
+
offset += bytesRead;
|
|
91
|
+
}
|
|
92
|
+
if (offset > MAX_META_BYTES)
|
|
93
|
+
throw new Error(`Codex session metadata exceeds ${MAX_META_BYTES} bytes`);
|
|
94
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await handle.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
101
|
+
const UUID_IN_NAME = /(?:^|[^0-9a-f])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?=$|[^0-9a-f])/ig;
|
|
102
|
+
/** The native filename, not a parent directory or a longer hex token, owns the session id. */
|
|
103
|
+
export function codexSessionIdMatchesFilename(path, sessionId) {
|
|
104
|
+
// Index rows are searchable across operating systems, so recognize both
|
|
105
|
+
// separator styles even when this process did not create the path.
|
|
106
|
+
const filename = path.split(/[\\/]/).at(-1) ?? basename(path);
|
|
107
|
+
return [...filename.matchAll(UUID_IN_NAME)]
|
|
108
|
+
.some((match) => match[1]?.toLowerCase() === sessionId.toLowerCase());
|
|
109
|
+
}
|
|
110
|
+
function parseSessionMeta(line, path, options = {}) {
|
|
111
|
+
let value;
|
|
112
|
+
try {
|
|
113
|
+
value = JSON.parse(line);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error(`Codex session ${path} has unreadable session metadata; keeping the original on disk`);
|
|
117
|
+
}
|
|
118
|
+
const payloadId = value?.payload?.id;
|
|
119
|
+
const payloadSessionId = value?.payload?.session_id;
|
|
120
|
+
const source = value?.payload?.source;
|
|
121
|
+
const subagentThreadSpawn = source && typeof source === "object" && !Array.isArray(source)
|
|
122
|
+
&& source.subagent
|
|
123
|
+
&& typeof source.subagent === "object"
|
|
124
|
+
&& !Array.isArray(source.subagent)
|
|
125
|
+
&& (source.subagent.thread_spawn)
|
|
126
|
+
&& typeof (source.subagent.thread_spawn) === "object"
|
|
127
|
+
&& !Array.isArray(source.subagent.thread_spawn)
|
|
128
|
+
? source.subagent.thread_spawn
|
|
129
|
+
: null;
|
|
130
|
+
if (subagentThreadSpawn) {
|
|
131
|
+
// Direct resume is unproven for every structurally recognized child—not
|
|
132
|
+
// just today's dual-id shape. Installed Codex v0.152 rejects a real v2
|
|
133
|
+
// child until its parent is loaded, so variants with one/equal IDs must
|
|
134
|
+
// remain full transcripts until a parent-first protocol is proven.
|
|
135
|
+
const parentThreadId = subagentThreadSpawn.parent_thread_id;
|
|
136
|
+
const recognized = typeof payloadId === "string" && UUID.test(payloadId)
|
|
137
|
+
&& typeof payloadSessionId === "string" && UUID.test(payloadSessionId)
|
|
138
|
+
&& typeof parentThreadId === "string" && UUID.test(parentThreadId)
|
|
139
|
+
&& parentThreadId.toLowerCase() === payloadSessionId.toLowerCase();
|
|
140
|
+
if (!options.allowRecognizedSubagent || !recognized) {
|
|
141
|
+
throw new Error(`Codex subagent session ${path} cannot be independently resumed by the installed Codex; keeping the original on disk`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (typeof payloadId === "string" && typeof payloadSessionId === "string"
|
|
145
|
+
&& payloadId.toLowerCase() !== payloadSessionId.toLowerCase()
|
|
146
|
+
&& !subagentThreadSpawn) {
|
|
147
|
+
throw new Error(`Codex session ${path} has conflicting session ids; keeping the original on disk`);
|
|
148
|
+
}
|
|
149
|
+
const id = payloadId ?? payloadSessionId;
|
|
150
|
+
const cwd = value?.payload?.cwd;
|
|
151
|
+
const rawTimestamp = value.timestamp ?? value?.payload?.timestamp;
|
|
152
|
+
if (value?.type !== "session_meta" || typeof id !== "string" || !UUID.test(id)
|
|
153
|
+
|| typeof cwd !== "string" || !cwd.trim()
|
|
154
|
+
|| typeof rawTimestamp !== "string" || !Number.isFinite(Date.parse(rawTimestamp))) {
|
|
155
|
+
throw new Error(`Codex session ${path} has unsupported session metadata; keeping the original on disk`);
|
|
156
|
+
}
|
|
157
|
+
// The catalog and the rollout must agree. A pointer under the wrong UUID
|
|
158
|
+
// would make `codex resume <id>` open another session, which is worse than
|
|
159
|
+
// declining to reclaim a file.
|
|
160
|
+
if (!codexSessionIdMatchesFilename(path, id)) {
|
|
161
|
+
throw new Error(`Codex session id ${id} does not match an exact UUID token in its rollout filename; keeping the original on disk`);
|
|
162
|
+
}
|
|
163
|
+
const ordinal = typeof value.ordinal === "number" && Number.isSafeInteger(value.ordinal) && value.ordinal >= 0
|
|
164
|
+
? value.ordinal
|
|
165
|
+
: undefined;
|
|
166
|
+
const relatedProcessSessionIds = [id];
|
|
167
|
+
if (typeof payloadSessionId === "string" && UUID.test(payloadSessionId)
|
|
168
|
+
&& !relatedProcessSessionIds.some((candidate) => candidate.toLowerCase() === payloadSessionId.toLowerCase())) {
|
|
169
|
+
relatedProcessSessionIds.push(payloadSessionId);
|
|
170
|
+
}
|
|
171
|
+
return { sessionId: id, relatedProcessSessionIds, timestamp: rawTimestamp, cwd, ...(ordinal === undefined ? {} : { ordinal }) };
|
|
172
|
+
}
|
|
173
|
+
/** Validates that replacing this exact native rollout with a pointer is supported. */
|
|
174
|
+
export async function assertCodexResumePointerSupported(path, home = homedir()) {
|
|
175
|
+
const canonical = await validateNativeCodexSessionPath(path, home, "file");
|
|
176
|
+
await assertCodexResumePointerMetadataSupported(canonical, canonical);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Re-validates a staged original against its logical native filename.
|
|
180
|
+
* Crash reconciliation uses this after upgrades: staging lives inside the
|
|
181
|
+
* vault, but the rollout UUID contract still belongs to the original path.
|
|
182
|
+
*/
|
|
183
|
+
export async function assertCodexResumePointerMetadataSupported(contentsPath, logicalNativePath) {
|
|
184
|
+
parseSessionMeta(await firstLine(resolve(contentsPath)), resolve(logicalNativePath));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A reclaim journal may outlive the binary that wrote it. Do not let such a
|
|
188
|
+
* journal turn an arbitrary neighbouring file into a replacement temp: only
|
|
189
|
+
* the private basename generated by prepareCodexResumeStub is eligible.
|
|
190
|
+
*/
|
|
191
|
+
export function assertCodexResumePointerTempPath(tempPath) {
|
|
192
|
+
if (!PREPARED_STUB_BASENAME.test(basename(resolve(tempPath)))) {
|
|
193
|
+
throw new Error(`Codex resume pointer temp has an unsupported name and was retained: ${tempPath}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function parseUntouchedPointerContents(raw, logicalNativePath, options = {}) {
|
|
197
|
+
const lines = raw.endsWith("\n") ? raw.slice(0, -1).split("\n") : raw.split("\n");
|
|
198
|
+
if (lines.length !== 3)
|
|
199
|
+
return null;
|
|
200
|
+
try {
|
|
201
|
+
const meta = parseSessionMeta(lines[0], resolve(logicalNativePath), options);
|
|
202
|
+
const message = JSON.parse(lines[1]);
|
|
203
|
+
const content = Array.isArray(message?.payload?.content) ? message.payload.content : [];
|
|
204
|
+
const text = content.length === 1 && content[0] && typeof content[0] === "object"
|
|
205
|
+
? content[0].text
|
|
206
|
+
: null;
|
|
207
|
+
if (message.type !== "response_item" || message.payload?.type !== "message" || message.payload.role !== "user"
|
|
208
|
+
|| typeof text !== "string" || !text.startsWith(`${CODEX_RESUME_STUB_MARKER}\n`))
|
|
209
|
+
return null;
|
|
210
|
+
const match = / as archive ([0-9a-f-]{36}), then reclaimed/.exec(text);
|
|
211
|
+
if (!match)
|
|
212
|
+
return null;
|
|
213
|
+
const final = JSON.parse(lines[2]);
|
|
214
|
+
if (final.type !== "response_item" || final.payload?.type !== "message" || final.payload.role !== "assistant")
|
|
215
|
+
return null;
|
|
216
|
+
return { version: 1, archiveId: match[1], sessionId: meta.sessionId };
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Return true only when an existing pathname is the exact journaled pointer
|
|
224
|
+
* and its structured contents bind it to this native session and archive.
|
|
225
|
+
* A missing or fingerprint-mismatched pathname returns false because generic
|
|
226
|
+
* reconciliation already retains it. A matching-but-invalid pathname throws:
|
|
227
|
+
* deleting or publishing it based only on a journal-supplied hash would let a
|
|
228
|
+
* corrupt journal target an unrelated neighbouring file.
|
|
229
|
+
*/
|
|
230
|
+
export async function validateCodexResumePointerBinding(contentsPath, logicalNativePath, archiveId, expected) {
|
|
231
|
+
let handle;
|
|
232
|
+
try {
|
|
233
|
+
handle = await open(resolve(contentsPath), POINTER_OPEN_FLAGS);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (error.code === "ENOENT")
|
|
237
|
+
return false;
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const before = await handle.stat({ bigint: true });
|
|
242
|
+
if (!before.isFile())
|
|
243
|
+
throw new Error(`Codex resume pointer candidate is not a regular file and was retained: ${contentsPath}`);
|
|
244
|
+
if (before.size !== BigInt(expected.bytes))
|
|
245
|
+
return false;
|
|
246
|
+
if (before.size > BigInt(MAX_STUB_BYTES)) {
|
|
247
|
+
throw new Error(`Codex resume pointer candidate exceeds ${MAX_STUB_BYTES} bytes and was retained: ${contentsPath}`);
|
|
248
|
+
}
|
|
249
|
+
const rawBuffer = await readStablePointer(handle, contentsPath, before);
|
|
250
|
+
const sha256 = createHash("sha256").update(rawBuffer).digest("hex");
|
|
251
|
+
if (sha256 !== expected.sha256)
|
|
252
|
+
return false;
|
|
253
|
+
// Binding is not an eligibility decision. An older release could have
|
|
254
|
+
// generated a structurally valid child pointer before direct child resume
|
|
255
|
+
// was disproven. Recognize only that narrow, internally consistent shape
|
|
256
|
+
// here so reconciliation may safely remove the pointer while restoring the
|
|
257
|
+
// full child rollout; normal pointer creation/reading still rejects it.
|
|
258
|
+
const pointer = parseUntouchedPointerContents(rawBuffer.toString("utf8"), logicalNativePath, {
|
|
259
|
+
allowRecognizedSubagent: true,
|
|
260
|
+
});
|
|
261
|
+
if (!pointer || pointer.archiveId.toLowerCase() !== archiveId.toLowerCase()) {
|
|
262
|
+
throw new Error(`Codex resume pointer candidate is not bound to archive ${archiveId} and was retained: ${contentsPath}`);
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
await handle.close();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** Own and validated parent IDs that may identify a live Codex process. */
|
|
271
|
+
export async function codexSessionProcessIds(path) {
|
|
272
|
+
return parseSessionMeta(await firstLine(path), resolve(path)).relatedProcessSessionIds;
|
|
273
|
+
}
|
|
274
|
+
/** IDs plus project cwd used when a live Codex process has no UUID in argv. */
|
|
275
|
+
export async function codexSessionProcessIdentity(path) {
|
|
276
|
+
const meta = parseSessionMeta(await firstLine(path), resolve(path));
|
|
277
|
+
return { ids: meta.relatedProcessSessionIds, cwd: meta.cwd };
|
|
278
|
+
}
|
|
279
|
+
function inputMessage(timestamp, text, ordinal) {
|
|
280
|
+
return {
|
|
281
|
+
...(ordinal === undefined ? {} : { ordinal }),
|
|
282
|
+
timestamp,
|
|
283
|
+
type: "response_item",
|
|
284
|
+
payload: { type: "message", role: "user", content: [{ type: "input_text", text }] },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function outputMessage(timestamp, text, ordinal) {
|
|
288
|
+
return {
|
|
289
|
+
...(ordinal === undefined ? {} : { ordinal }),
|
|
290
|
+
timestamp,
|
|
291
|
+
type: "response_item",
|
|
292
|
+
payload: { type: "message", role: "assistant", phase: "final_answer", content: [{ type: "output_text", text }] },
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Builds the replacement beside the source, while the original is still
|
|
297
|
+
* present and fully verified. The caller transaction-stages the original first
|
|
298
|
+
* and then publishes this prepared file into its native path without
|
|
299
|
+
* overwriting a path the agent recreated in the meantime.
|
|
300
|
+
*
|
|
301
|
+
* The original session-meta line is kept byte-for-byte. It is the format
|
|
302
|
+
* contract of the installed Codex build and carries the stable id, cwd, git
|
|
303
|
+
* project, source kind and original timestamp. Even a large modern metadata
|
|
304
|
+
* line is tiny beside a multi-GB rollout and avoids guessing at vendor fields.
|
|
305
|
+
*/
|
|
306
|
+
export async function prepareCodexResumeStub(sourcePath, archiveId, options = {}) {
|
|
307
|
+
const path = resolve(sourcePath);
|
|
308
|
+
if (!(await isNativeCodexSessionPath(path, options.home)))
|
|
309
|
+
return null;
|
|
310
|
+
const sourceStat = await stat(path);
|
|
311
|
+
if (!sourceStat.isFile())
|
|
312
|
+
throw new Error(`Codex session ${path} is not a regular file; keeping it on disk`);
|
|
313
|
+
const metaLine = await firstLine(path);
|
|
314
|
+
const meta = parseSessionMeta(metaLine, path);
|
|
315
|
+
const project = options.project?.trim();
|
|
316
|
+
const scope = project ? ` for project ${JSON.stringify(project)}` : "";
|
|
317
|
+
const pointer = [
|
|
318
|
+
CODEX_RESUME_STUB_MARKER,
|
|
319
|
+
`Sealkeep preserved a searchable snapshot of Codex session ${meta.sessionId}${scope} as archive ${archiveId}, then reclaimed only the bulky local source. Other snapshots of this session may have different archive ids.`,
|
|
320
|
+
`Use the local MCP tool \`sealkeep_search\` with \`{"query":"<current question>","agent":"codex","sessionId":"${meta.sessionId}"}\` to retrieve relevant knowledge across every preserved snapshot of this Codex session.`,
|
|
321
|
+
"For a matching result, pass its context object unchanged to `sealkeep_read_context` to read the cited source passage, actual user prompt when recorded, and neighboring messages. Follow its continuation with the same source if needed; a search snippet is not the full earlier conversation.",
|
|
322
|
+
`To restore this exact archived snapshot, run \`sealkeep recover ${archiveId} --native --overwrite backup\`, then run \`codex resume ${meta.sessionId}\` again.`,
|
|
323
|
+
"Do not claim that earlier context was recovered until the search or restore returns it.",
|
|
324
|
+
].join("\n");
|
|
325
|
+
const display = [
|
|
326
|
+
"This session's preserved history is searchable in Sealkeep.",
|
|
327
|
+
"This lightweight resume intentionally keeps archived snapshots off disk. Use Sealkeep MCP search now, or run the exact restore command above and resume the same session again when you need every byte from that snapshot.",
|
|
328
|
+
].join(" ");
|
|
329
|
+
const contents = `${metaLine}\n${JSON.stringify(inputMessage(meta.timestamp, pointer, meta.ordinal === undefined ? undefined : meta.ordinal + 1))}\n${JSON.stringify(outputMessage(meta.timestamp, display, meta.ordinal === undefined ? undefined : meta.ordinal + 2))}\n`;
|
|
330
|
+
if (Buffer.byteLength(contents) > MAX_STUB_BYTES) {
|
|
331
|
+
throw new Error(`Codex resume pointer for ${path} exceeds ${MAX_STUB_BYTES} bytes; keeping the original on disk`);
|
|
332
|
+
}
|
|
333
|
+
const tempPath = join(dirname(path), `.${randomUUID()}.sealkeep-resume-stub.partial`);
|
|
334
|
+
try {
|
|
335
|
+
await writeFile(tempPath, contents, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
336
|
+
await utimes(tempPath, sourceStat.atime, sourceStat.mtime);
|
|
337
|
+
if (options.syncPreparedFile) {
|
|
338
|
+
await options.syncPreparedFile(tempPath);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
342
|
+
const { handle } = await openForFlush(tempPath, noFollow);
|
|
343
|
+
try {
|
|
344
|
+
const info = await handle.stat();
|
|
345
|
+
if (!info.isFile())
|
|
346
|
+
throw new Error(`Codex resume pointer temp is not a regular file: ${tempPath}`);
|
|
347
|
+
await handle.sync();
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
await handle.close();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
await rm(tempPath, { force: true }).catch(() => undefined);
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
version: 1,
|
|
360
|
+
archiveId,
|
|
361
|
+
sessionId: meta.sessionId,
|
|
362
|
+
path,
|
|
363
|
+
bytes: Buffer.byteLength(contents),
|
|
364
|
+
sha256: createHash("sha256").update(contents).digest("hex"),
|
|
365
|
+
tempPath,
|
|
366
|
+
atime: sourceStat.atime,
|
|
367
|
+
mtime: sourceStat.mtime,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/** Installs an already-prepared pointer without clobbering a recreated path. */
|
|
371
|
+
export async function installCodexResumeStub(stub) {
|
|
372
|
+
// `rename(temp, path)` overwrites on POSIX. Codex can recreate its rollout
|
|
373
|
+
// after the staging move but before this call; a hard-link publication is the
|
|
374
|
+
// same-filesystem atomic create-if-absent primitive and returns EEXIST rather
|
|
375
|
+
// than discarding those newly written bytes. The extra temp link is harmless
|
|
376
|
+
// after a crash and is removed best-effort once the native name is durable.
|
|
377
|
+
await link(stub.tempPath, stub.path);
|
|
378
|
+
await rm(stub.tempPath, { force: true }).catch(() => undefined);
|
|
379
|
+
}
|
|
380
|
+
export async function discardCodexResumeStub(stub) {
|
|
381
|
+
if (stub)
|
|
382
|
+
await rm(stub.tempPath, { force: true }).catch(() => undefined);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Recognises only the complete, untouched pointer format. Once Codex appends
|
|
386
|
+
* a new turn there are more than the three expected lines, so the worker
|
|
387
|
+
* archives that new work normally instead of permanently excluding the file.
|
|
388
|
+
*/
|
|
389
|
+
export async function readUntouchedCodexResumeStub(path) {
|
|
390
|
+
let handle;
|
|
391
|
+
try {
|
|
392
|
+
// lstat before open also avoids opening a FIFO or other special file.
|
|
393
|
+
const named = await lstat(path, { bigint: true });
|
|
394
|
+
if (!named.isFile() || named.size <= 0n || named.size > BigInt(MAX_STUB_BYTES))
|
|
395
|
+
return null;
|
|
396
|
+
// Nonblocking is ignored for regular files, but prevents a FIFO swapped
|
|
397
|
+
// into the pathname after lstat from hanging the background worker.
|
|
398
|
+
handle = await open(path, POINTER_OPEN_FLAGS);
|
|
399
|
+
const before = await handle.stat({ bigint: true });
|
|
400
|
+
if (!samePointerSnapshot(named, before))
|
|
401
|
+
return null;
|
|
402
|
+
const raw = await readStablePointer(handle, path, before);
|
|
403
|
+
const pointer = parseUntouchedPointerContents(raw.toString("utf8"), path);
|
|
404
|
+
return pointer ? {
|
|
405
|
+
...pointer,
|
|
406
|
+
path: resolve(path),
|
|
407
|
+
bytes: raw.length,
|
|
408
|
+
sha256: createHash("sha256").update(raw).digest("hex"),
|
|
409
|
+
} : null;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
await handle?.close();
|
|
416
|
+
}
|
|
417
|
+
}
|
package/dist/src/retention.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type ActivityOptions } from "./activity.js";
|
|
2
|
-
import {
|
|
3
|
-
import { type
|
|
2
|
+
import { recordAudit } from "./audit.js";
|
|
3
|
+
import { moveToTrash, type TrashResult } from "./trash.js";
|
|
4
|
+
import { type ArchiveRecord, type ArchiveRecordV2, type RemoteObject, type VaultConfig } from "./types.js";
|
|
5
|
+
import { mutateArchiveRecord } from "./vault.js";
|
|
6
|
+
import { installCodexResumeStub } from "./resume-stub.js";
|
|
7
|
+
import { type VerifiedManagedAccountIndexPublication } from "./index-publication-verifier.js";
|
|
4
8
|
export type RetentionPolicy = "local-only" | "sync-only" | "archive-and-reclaim" | "manual-approval";
|
|
5
9
|
export type RetentionSettings = {
|
|
6
10
|
policy: RetentionPolicy;
|
|
@@ -44,6 +48,8 @@ export type Candidate = {
|
|
|
44
48
|
agent: string;
|
|
45
49
|
path: string;
|
|
46
50
|
bytes: number;
|
|
51
|
+
/** Filesystem blocks attributed to the source inode at preview time. */
|
|
52
|
+
allocatedBytes: number;
|
|
47
53
|
createdAt: string;
|
|
48
54
|
eligible: boolean;
|
|
49
55
|
/** Every reason this source is still protected. Empty exactly when eligible. */
|
|
@@ -60,11 +66,68 @@ export type RetentionOptions = {
|
|
|
60
66
|
now?: number;
|
|
61
67
|
activity?: ActivityOptions;
|
|
62
68
|
settings?: Partial<RetentionSettings>;
|
|
69
|
+
/** Set by the unattended daemon; foreground/manual actions leave it unset. */
|
|
70
|
+
backgroundCpuTarget?: number;
|
|
71
|
+
/** Aggregate daemon transfer ceiling for full remote identity proof. */
|
|
72
|
+
backgroundRemoteBytesPerSecond?: number;
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
/** Selected home for native Codex/Claude path and resume-format validation. */
|
|
75
|
+
home?: string;
|
|
76
|
+
/** Explicit unlock for foreground use; background work normally uses the OS keystore. */
|
|
77
|
+
phrase?: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Narrow operation seams for deterministic failure testing of the destructive
|
|
81
|
+
* boundary. Production callers leave this absent and get the real atomic
|
|
82
|
+
* filesystem, record-lock, and append-only audit operations.
|
|
83
|
+
*/
|
|
84
|
+
export type ReclaimOperations = {
|
|
85
|
+
moveToTrash?: typeof moveToTrash;
|
|
86
|
+
installResumeStub?: typeof installCodexResumeStub;
|
|
87
|
+
mutateRecord?: typeof mutateArchiveRecord;
|
|
88
|
+
audit?: typeof recordAudit;
|
|
89
|
+
/** Test/embedding seam; production asks the recorded provider immediately before purge. */
|
|
90
|
+
checkRemoteCopy?: (dataDir: string, archiveId: string, copyId: string) => Promise<{
|
|
91
|
+
ok: boolean;
|
|
92
|
+
detail: string;
|
|
93
|
+
}>;
|
|
94
|
+
/**
|
|
95
|
+
* Stream seam that keeps the production byte-count/SHA-256 proof intact.
|
|
96
|
+
* Unlike checkRemoteCopy, returning a stream here does not assert success.
|
|
97
|
+
*/
|
|
98
|
+
fetchRemoteCopy?: (dataDir: string, record: ArchiveRecordV2, copy: RemoteObject, signal?: AbortSignal) => Promise<AsyncIterable<Buffer>>;
|
|
99
|
+
/** Deterministic seam for cooperative hashing; daemon uses the 30% lane. */
|
|
100
|
+
cooperate?: () => Promise<void>;
|
|
101
|
+
/** Exact managed-index proof seam. Production always uses the real verifier. */
|
|
102
|
+
verifyIndexPublication?: (dataDir: string, chain: readonly ArchiveRecord[], requestedHead: ArchiveRecord, phrase: string, signal?: AbortSignal,
|
|
103
|
+
/** Reclaimed snapshots belonging to the same exact native Codex session. */
|
|
104
|
+
searchableHeads?: readonly ArchiveRecord[]) => Promise<VerifiedManagedAccountIndexPublication>;
|
|
105
|
+
/** Cheap exact-head recheck seam used immediately before unlink. */
|
|
106
|
+
recheckIndexPublication?: (dataDir: string, verification: VerifiedManagedAccountIndexPublication, signal?: AbortSignal) => Promise<void>;
|
|
63
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* A live permission check for source deletion. Returning anything except
|
|
110
|
+
* literal `true`, or failing to read the current policy, keeps the original.
|
|
111
|
+
* The daemon supplies its machine-local gate; foreground entry points derive a
|
|
112
|
+
* command-scoped gate so crash reconciliation cannot borrow authority from an
|
|
113
|
+
* unrelated command or from a preview.
|
|
114
|
+
*/
|
|
115
|
+
export type ReclaimAuthorization = (archiveId: string) => Promise<boolean>;
|
|
64
116
|
export declare function retentionSettings(config: VaultConfig, overrides?: Partial<RetentionSettings>): RetentionSettings;
|
|
65
117
|
export declare function readApprovals(dataDir: string): Promise<string[]>;
|
|
66
118
|
/** Manual-approval policy: an operator names the archives whose sources may be reclaimed. */
|
|
67
119
|
export declare function approveReclamation(dataDir: string, archiveIds: string[]): Promise<string[]>;
|
|
120
|
+
/**
|
|
121
|
+
* The standing "never take this one" list, honoured by EVERY reclaimer.
|
|
122
|
+
*
|
|
123
|
+
* `<dataDir>/never-reclaim` (one id or path fragment per line, `#` comments)
|
|
124
|
+
* was read by exactly one of the four deleters — the squeeze command — so a
|
|
125
|
+
* pinned session survived a squeeze and was then reclaimed by the daemon's
|
|
126
|
+
* ordinary retention tick, with no user in the loop. A pin only one deleter
|
|
127
|
+
* honours is a hole; it lives here now so every caller inherits it.
|
|
128
|
+
*/
|
|
129
|
+
export declare function neverReclaimMarks(dataDir: string): Promise<string[]>;
|
|
130
|
+
export declare function isPinned(path: string, marks: string[]): boolean;
|
|
68
131
|
/**
|
|
69
132
|
* Decides, per archive, whether its local source may be reclaimed.
|
|
70
133
|
*
|
|
@@ -86,26 +149,44 @@ export type ReclaimOutcome = {
|
|
|
86
149
|
id: string;
|
|
87
150
|
path: string;
|
|
88
151
|
bytes: number;
|
|
152
|
+
/** Legacy recoverable-trash destination. Empty after permanent purge. */
|
|
89
153
|
movedTo: string;
|
|
90
|
-
strategy: TrashResult["strategy"];
|
|
154
|
+
strategy: TrashResult["strategy"] | "purged";
|
|
155
|
+
/** Allocated blocks released from the source inode, net of its pointer. */
|
|
156
|
+
freedBytes: number;
|
|
157
|
+
/** Present only once the durable staged original has been permanently purged. */
|
|
158
|
+
purgedAt?: string;
|
|
159
|
+
/** A tiny Codex-native pointer now occupies `path`, so `codex resume <id>` remains discoverable. */
|
|
160
|
+
resumeStub?: {
|
|
161
|
+
sessionId: string;
|
|
162
|
+
bytes: number;
|
|
163
|
+
};
|
|
91
164
|
};
|
|
165
|
+
/** Resolve kill -9 windows before starting another native source reclaim. */
|
|
166
|
+
export declare function reconcileCodexReclaims(dataDir: string, storageRoot: string, home?: string, remoteCheck?: ReclaimOperations["checkRemoteCopy"], cooperate?: () => Promise<void>, activity?: ActivityOptions, signal?: AbortSignal, remoteBytesPerSecond?: number, remoteFetch?: ReclaimOperations["fetchRemoteCopy"], authorizeReclaim?: ReclaimAuthorization, phrase?: string, indexOperations?: Pick<ReclaimOperations, "verifyIndexPublication" | "recheckIndexPublication">): Promise<void>;
|
|
92
167
|
/**
|
|
93
168
|
* Reclaims eligible sources. Dry-run is the default: without `confirm`, nothing on
|
|
94
|
-
* disk changes.
|
|
169
|
+
* disk changes. Confirmed, verified sources are transaction-staged and then
|
|
170
|
+
* permanently purged; Codex receives a tiny same-id resume pointer.
|
|
95
171
|
*/
|
|
96
172
|
export declare function applyRetention(dataDir: string, options?: RetentionOptions & {
|
|
97
173
|
confirm?: boolean;
|
|
98
174
|
home?: string;
|
|
99
175
|
strategy?: TrashResult["strategy"];
|
|
176
|
+
operations?: ReclaimOperations;
|
|
177
|
+
/** Set only by the installed daemon; checked repeatedly during a long pass. */
|
|
178
|
+
authorizeReclaim?: ReclaimAuthorization;
|
|
100
179
|
}): Promise<{
|
|
101
180
|
mode: "dry-run";
|
|
102
181
|
policy: RetentionPolicy;
|
|
103
182
|
wouldReclaim: number;
|
|
104
183
|
wouldFreeBytes: number;
|
|
184
|
+
wouldMoveBytes: number;
|
|
105
185
|
candidates: Candidate[];
|
|
106
186
|
reclaimed?: undefined;
|
|
107
187
|
failed?: undefined;
|
|
108
188
|
freedBytes?: undefined;
|
|
189
|
+
movedBytes?: undefined;
|
|
109
190
|
protected?: undefined;
|
|
110
191
|
} | {
|
|
111
192
|
mode: "apply";
|
|
@@ -116,9 +197,11 @@ export declare function applyRetention(dataDir: string, options?: RetentionOptio
|
|
|
116
197
|
error: string;
|
|
117
198
|
}[];
|
|
118
199
|
freedBytes: number;
|
|
200
|
+
movedBytes: number;
|
|
119
201
|
protected: number;
|
|
120
202
|
wouldReclaim?: undefined;
|
|
121
203
|
wouldFreeBytes?: undefined;
|
|
204
|
+
wouldMoveBytes?: undefined;
|
|
122
205
|
candidates?: undefined;
|
|
123
206
|
}>;
|
|
124
207
|
/**
|
|
@@ -132,13 +215,16 @@ export declare function applyRetention(dataDir: string, options?: RetentionOptio
|
|
|
132
215
|
*
|
|
133
216
|
* - the archive must have a verified remote copy (and a delta head's whole
|
|
134
217
|
* chain must be verified — one unverified base and the cloud cannot
|
|
135
|
-
* reproduce the file this is about to
|
|
218
|
+
* reproduce the file this is about to reclaim);
|
|
219
|
+
* - this exact archive must be covered by the search index, so durable memory
|
|
220
|
+
* is also memory a fresh agent can actually find;
|
|
136
221
|
* - the source must be byte-for-byte the file that was archived — a session
|
|
137
222
|
* that grew since has content in no archive at all;
|
|
138
223
|
* - nothing may have the file open right now.
|
|
139
224
|
*
|
|
140
|
-
* Policy, age and grace are deliberately not consulted.
|
|
141
|
-
*
|
|
225
|
+
* Policy, age and grace are deliberately not consulted. The explicit click is
|
|
226
|
+
* the irreversible-delete approval; all durability/search/activity gates still
|
|
227
|
+
* run immediately before the transaction commits.
|
|
142
228
|
*/
|
|
143
229
|
export declare function reclaimOne(dataDir: string, archiveId: string, options?: {
|
|
144
230
|
confirm?: boolean;
|
|
@@ -146,6 +232,9 @@ export declare function reclaimOne(dataDir: string, archiveId: string, options?:
|
|
|
146
232
|
strategy?: TrashResult["strategy"];
|
|
147
233
|
now?: number;
|
|
148
234
|
activity?: ActivityOptions;
|
|
235
|
+
operations?: ReclaimOperations;
|
|
236
|
+
phrase?: string;
|
|
237
|
+
signal?: AbortSignal;
|
|
149
238
|
}): Promise<ReclaimOutcome>;
|
|
150
239
|
/**
|
|
151
240
|
* `reclaimOne` over a selection. Each row passes or is kept with its reason —
|
|
@@ -158,6 +247,7 @@ export declare function reclaimBatch(dataDir: string, ids: readonly string[], op
|
|
|
158
247
|
strategy?: TrashResult["strategy"];
|
|
159
248
|
now?: number;
|
|
160
249
|
activity?: ActivityOptions;
|
|
250
|
+
operations?: ReclaimOperations;
|
|
161
251
|
}): Promise<{
|
|
162
252
|
reclaimed: ReclaimOutcome[];
|
|
163
253
|
kept: {
|
|
@@ -165,6 +255,7 @@ export declare function reclaimBatch(dataDir: string, ids: readonly string[], op
|
|
|
165
255
|
reason: string;
|
|
166
256
|
}[];
|
|
167
257
|
freedBytes: number;
|
|
258
|
+
movedBytes: number;
|
|
168
259
|
}>;
|
|
169
260
|
export declare function setRetentionPolicy(dataDir: string, settings: Partial<RetentionSettings>): Promise<VaultConfig>;
|
|
170
261
|
export type RedundantArchive = {
|