sealkeep 0.9.0 → 0.11.1
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 +252 -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.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +53 -13
- 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 +332 -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 +5940 -297
- 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/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
package/dist/src/doctor.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { detectSetupAgents } from "./adapters.js";
|
|
5
|
+
import { isSealkeepError } from "./errors.js";
|
|
6
6
|
import { localApiTokenPath } from "./paths.js";
|
|
7
7
|
import { ArchiveQueue } from "./queue.js";
|
|
8
8
|
import { signerEnabled } from "./control-plane.js";
|
|
9
|
+
import { liveness, readHeartbeat } from "./heartbeat.js";
|
|
9
10
|
import { quotaMessage, quotaState } from "./packages.js";
|
|
10
11
|
import { loadProviderCredentials, resolveRecoveryPhrase } from "./secrets.js";
|
|
11
|
-
import { ENVELOPE_VERSION, SUITES } from "../packages/
|
|
12
|
+
import { ENVELOPE_VERSION, SUITES } from "../packages/sealkeep-crypto/src/index.js";
|
|
12
13
|
import { readConfig } from "./vault.js";
|
|
13
14
|
import { envVar } from "./env.js";
|
|
15
|
+
import { providerLabel } from "./branding.js";
|
|
16
|
+
import { readUntouchedCodexResumeStub } from "./resume-stub.js";
|
|
14
17
|
async function writable(directory) {
|
|
15
|
-
const probe = join(directory, `.
|
|
18
|
+
const probe = join(directory, `.sealkeep-write-probe-${randomUUID()}`);
|
|
16
19
|
try {
|
|
17
20
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
18
21
|
await writeFile(probe, "");
|
|
@@ -25,6 +28,164 @@ async function writable(directory) {
|
|
|
25
28
|
await rm(probe, { force: true });
|
|
26
29
|
}
|
|
27
30
|
}
|
|
31
|
+
function archiveMetadataId(raw) {
|
|
32
|
+
try {
|
|
33
|
+
const record = JSON.parse(raw);
|
|
34
|
+
return record && typeof record === "object" && typeof record.id === "string" && record.id
|
|
35
|
+
? record.id
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function mapIntegrityMetadata(names, concurrency, work) {
|
|
43
|
+
const output = new Array(names.length);
|
|
44
|
+
let cursor = 0;
|
|
45
|
+
const workers = Array.from({ length: Math.min(Math.max(1, concurrency), names.length) }, async () => {
|
|
46
|
+
for (;;) {
|
|
47
|
+
const index = cursor++;
|
|
48
|
+
if (index >= names.length)
|
|
49
|
+
return;
|
|
50
|
+
output[index] = await work(names[index]);
|
|
51
|
+
if ((index + 1) % 16 === 0)
|
|
52
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
await Promise.all(workers);
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
58
|
+
function sameNames(left, right) {
|
|
59
|
+
return left.length === right.length && left.every((name, index) => name === right[index]);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read one sidecar without turning descriptor pressure into a corruption report.
|
|
63
|
+
* A malformed document is called invalid only when the exact bytes remain
|
|
64
|
+
* malformed across a second read. I/O failures and changing bytes stay in the
|
|
65
|
+
* transient bucket; Health remains red, but does not tell the user the archive
|
|
66
|
+
* metadata itself is corrupt.
|
|
67
|
+
*/
|
|
68
|
+
async function readIntegrityMetadata(directory, name, read, readAttempts) {
|
|
69
|
+
const path = join(directory, name);
|
|
70
|
+
let raw = null;
|
|
71
|
+
for (let attempt = 0; attempt < readAttempts; attempt += 1) {
|
|
72
|
+
try {
|
|
73
|
+
raw = await read(path);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
if (attempt + 1 < readAttempts)
|
|
78
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (raw === null)
|
|
82
|
+
return { kind: "transient", name };
|
|
83
|
+
const id = archiveMetadataId(raw);
|
|
84
|
+
if (id)
|
|
85
|
+
return { kind: "valid", name, id };
|
|
86
|
+
// Atomic record updates should never expose partial JSON. Confirm stable bad
|
|
87
|
+
// bytes anyway, so a manual repair racing this read is not branded damage.
|
|
88
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
89
|
+
let confirmation;
|
|
90
|
+
try {
|
|
91
|
+
confirmation = await read(path);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return { kind: "transient", name };
|
|
95
|
+
}
|
|
96
|
+
const confirmedId = archiveMetadataId(confirmation);
|
|
97
|
+
if (confirmedId)
|
|
98
|
+
return { kind: "valid", name, id: confirmedId };
|
|
99
|
+
return confirmation === raw ? { kind: "invalid", name } : { kind: "transient", name };
|
|
100
|
+
}
|
|
101
|
+
export async function archiveIntegritySummary(directory, options = {}) {
|
|
102
|
+
const listDirectory = options.listDirectory ?? readdir;
|
|
103
|
+
const readMetadata = options.readMetadata ?? ((path) => readFile(path, "utf8"));
|
|
104
|
+
const concurrency = Math.min(64, Math.max(1, Math.trunc(options.concurrency ?? 24) || 1));
|
|
105
|
+
const readAttempts = Math.min(5, Math.max(1, Math.trunc(options.readAttempts ?? 2) || 1));
|
|
106
|
+
const snapshotAttempts = Math.min(5, Math.max(1, Math.trunc(options.snapshotAttempts ?? 3) || 1));
|
|
107
|
+
let names = [];
|
|
108
|
+
let reads = [];
|
|
109
|
+
let scanIncomplete = false;
|
|
110
|
+
for (let attempt = 0; attempt < snapshotAttempts; attempt += 1) {
|
|
111
|
+
let before;
|
|
112
|
+
try {
|
|
113
|
+
before = (await listDirectory(directory)).filter((name) => name.endsWith(".json") || name.endsWith(".skarchive")).sort();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
unreadableMetadataCount: 0,
|
|
119
|
+
invalidMetadataCount: 0,
|
|
120
|
+
transientMetadataReadErrorCount: 0,
|
|
121
|
+
detachedArchiveCount: 0,
|
|
122
|
+
scanIncomplete: true,
|
|
123
|
+
detail: "Sealkeep cannot list archive metadata. Nothing was changed.",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const metadata = before.filter((name) => name.endsWith(".json"));
|
|
127
|
+
reads = await mapIntegrityMetadata(metadata, concurrency, (name) => readIntegrityMetadata(directory, name, readMetadata, readAttempts));
|
|
128
|
+
let after;
|
|
129
|
+
try {
|
|
130
|
+
after = (await listDirectory(directory)).filter((name) => name.endsWith(".json") || name.endsWith(".skarchive")).sort();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
names = before;
|
|
134
|
+
scanIncomplete = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
names = after;
|
|
138
|
+
if (sameNames(before, after)) {
|
|
139
|
+
scanIncomplete = false;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
scanIncomplete = true;
|
|
143
|
+
if (attempt + 1 < snapshotAttempts)
|
|
144
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
145
|
+
}
|
|
146
|
+
const validIds = new Set(reads.filter((item) => item.kind === "valid").map((item) => item.id));
|
|
147
|
+
const invalid = reads.filter((item) => item.kind === "invalid").map((item) => item.name);
|
|
148
|
+
const transient = reads.filter((item) => item.kind === "transient").map((item) => item.name);
|
|
149
|
+
const transientStems = new Set(transient.map((name) => name.slice(0, -".json".length)));
|
|
150
|
+
// A sidecar that could not be read may still be perfectly valid. Do not turn
|
|
151
|
+
// the same temporary I/O failure into a second, invented detached-file error.
|
|
152
|
+
const detached = scanIncomplete ? [] : names
|
|
153
|
+
.filter((name) => name.endsWith(".skarchive"))
|
|
154
|
+
.map((name) => name.slice(0, -".skarchive".length))
|
|
155
|
+
.filter((id) => !validIds.has(id) && !transientStems.has(id));
|
|
156
|
+
const unreadableMetadataCount = invalid.length + transient.length;
|
|
157
|
+
if (unreadableMetadataCount === 0 && detached.length === 0 && !scanIncomplete) {
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
unreadableMetadataCount: 0,
|
|
161
|
+
invalidMetadataCount: 0,
|
|
162
|
+
transientMetadataReadErrorCount: 0,
|
|
163
|
+
detachedArchiveCount: 0,
|
|
164
|
+
scanIncomplete: false,
|
|
165
|
+
detail: `${validIds.size} archive metadata record${validIds.size === 1 ? " is" : "s are"} readable; no sealed files are detached from their metadata.`
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const parts = [
|
|
169
|
+
invalid.length > 0 ? `${invalid.length} archive metadata file${invalid.length === 1 ? " is" : "s are"} invalid` : null,
|
|
170
|
+
transient.length > 0 ? `${transient.length} archive metadata file${transient.length === 1 ? " could" : "s could"} not be checked after retrying` : null,
|
|
171
|
+
detached.length > 0 ? `${detached.length} sealed file${detached.length === 1 ? " has" : "s have"} no usable metadata` : null,
|
|
172
|
+
scanIncomplete ? "the archive folder changed throughout the check, so the snapshot is incomplete" : null,
|
|
173
|
+
].filter((part) => Boolean(part));
|
|
174
|
+
const first = (invalid[0] ?? transient[0] ?? (detached[0] ? `${detached[0]}.skarchive` : null));
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
unreadableMetadataCount,
|
|
178
|
+
invalidMetadataCount: invalid.length,
|
|
179
|
+
transientMetadataReadErrorCount: transient.length,
|
|
180
|
+
detachedArchiveCount: detached.length,
|
|
181
|
+
scanIncomplete,
|
|
182
|
+
detail: `${parts.join("; ")}. Sealkeep left every file untouched. Invalid metadata and detached sealed files need repair; a temporary read or changing-folder result should be checked again before treating it as damage.${first ? ` First affected file: ${first}` : ""}`
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function archiveIntegrityCheck(directory) {
|
|
186
|
+
const summary = await archiveIntegritySummary(directory);
|
|
187
|
+
return { name: "archive-integrity", status: summary.ok ? "pass" : "fail", detail: summary.detail };
|
|
188
|
+
}
|
|
28
189
|
/**
|
|
29
190
|
* A read-mostly environment report. It writes only a probe file it removes, and
|
|
30
191
|
* it never reads transcript contents, prints a recovery phrase, or shows the API token.
|
|
@@ -59,7 +220,7 @@ function quotaCheck(managed) {
|
|
|
59
220
|
*/
|
|
60
221
|
async function sealedCopyCheck(dataDir, signedIn) {
|
|
61
222
|
if (!signedIn)
|
|
62
|
-
return { name: "sealed-key-copy", status: "warn", detail: "
|
|
223
|
+
return { name: "sealed-key-copy", status: "warn", detail: "Could not verify the account connection or its key backup. Review your Recovery Kit; an existing backup may still be stored." };
|
|
63
224
|
try {
|
|
64
225
|
const { hasSealedCopy } = await import("./cloud.js");
|
|
65
226
|
return await hasSealedCopy(dataDir)
|
|
@@ -70,19 +231,21 @@ async function sealedCopyCheck(dataDir, signedIn) {
|
|
|
70
231
|
return { name: "sealed-key-copy", status: "warn", detail: `Could not check for a sealed copy: ${error instanceof Error ? error.message.split("\n")[0] : "unknown"}` };
|
|
71
232
|
}
|
|
72
233
|
}
|
|
73
|
-
export async function runDoctor(dataDir, env = process.env) {
|
|
234
|
+
export async function runDoctor(dataDir, env = process.env, home = env.HOME ?? env.USERPROFILE ?? env.home ?? ".", options = {}) {
|
|
74
235
|
const checks = [];
|
|
236
|
+
let managed = null;
|
|
75
237
|
const config = await readConfig(dataDir).catch((error) => {
|
|
76
238
|
// This renders in the Health tab, where a raw "config_unsupported_version:"
|
|
77
239
|
// slug or a bare "unreadable" reads, to someone told there is no reset, as
|
|
78
240
|
// "your vault is gone." Say what is and is not affected, and never a code.
|
|
79
|
-
const detail =
|
|
241
|
+
const detail = isSealkeepError(error) && error.code === "vault_not_initialized"
|
|
80
242
|
? "No vault has been set up on this machine yet. Open Sealkeep's setup page to create one."
|
|
81
243
|
: "Sealkeep can't read this vault's settings file. Your sealed archives are separate files and are not affected — don't create a new vault; restore this machine's vault folder from a backup, or contact support.";
|
|
82
244
|
checks.push({ name: "config", status: "fail", detail });
|
|
83
245
|
return null;
|
|
84
246
|
});
|
|
85
247
|
if (config) {
|
|
248
|
+
managed = await managedAccount(dataDir);
|
|
86
249
|
checks.push({ name: "config", status: "pass", detail: `Vault ${config.vaultId} (config v${config.version})` });
|
|
87
250
|
// A standing warning, like the crypto row: nothing about a healthy plain
|
|
88
251
|
// vault makes this go away, because the exposure is the choice itself.
|
|
@@ -92,59 +255,165 @@ export async function runDoctor(dataDir, env = process.env) {
|
|
|
92
255
|
checks.push(await writable(config.storage.root)
|
|
93
256
|
? { name: "archive-store", status: "pass", detail: `Writable: ${config.storage.root}` }
|
|
94
257
|
: { name: "archive-store", status: "fail", detail: `Not writable: ${config.storage.root}` });
|
|
258
|
+
checks.push(await archiveIntegrityCheck(config.storage.root));
|
|
95
259
|
// A machine on the managed tier has no remoteStorage of its own — the
|
|
96
260
|
// bucket is ours. Reporting "archives stay on this device" to someone who
|
|
97
261
|
// has been pushing to managed storage all week is worse than saying
|
|
98
262
|
// nothing: it is a health check telling them a comforting falsehood.
|
|
99
263
|
if (config.remoteStorage) {
|
|
100
|
-
checks.push({ name: "remote-storage", status: "pass", detail: `Target ${config.remoteStorage.provider}
|
|
264
|
+
checks.push({ name: "remote-storage", status: "pass", detail: `Target ${providerLabel(config.remoteStorage.provider)} · ${config.remoteStorage.bucket}/${config.remoteStorage.prefix}` });
|
|
101
265
|
}
|
|
102
266
|
else {
|
|
103
|
-
const managed = await managedAccount(dataDir);
|
|
104
267
|
checks.push(managed
|
|
105
268
|
// The cloud token lives in this machine's keychain, not in the vault,
|
|
106
269
|
// so being signed in says archives from here *can* go to managed
|
|
107
270
|
// storage — not that any have. Say the weaker, true thing.
|
|
108
271
|
? { name: "remote-storage", status: "pass", detail: `Signed in to managed storage as ${managed.email} (${managed.plan})` }
|
|
109
|
-
:
|
|
272
|
+
: config.storageTargets?.length
|
|
273
|
+
? { name: "remote-storage", status: "warn", detail: `Configured storage: ${config.storageTargets.map((target) => providerLabel(target.provider)).join(", ")}. The account connection could not be verified by this check. Review storage connections for current availability.` }
|
|
274
|
+
: { name: "remote-storage", status: "warn", detail: "No remote target configured. Add a storage destination to send new archives off this machine." });
|
|
110
275
|
checks.push(await sealedCopyCheck(dataDir, managed !== null));
|
|
111
276
|
// A zero quota is a plan with no managed entitlement at all — nothing to warn about.
|
|
112
277
|
if (managed && managed.quotaBytes > 0)
|
|
113
278
|
checks.push(quotaCheck(managed));
|
|
114
279
|
}
|
|
115
280
|
}
|
|
281
|
+
// The native service can deliberately stay alive while waiting for a phrase:
|
|
282
|
+
// its supervised UI is the recovery path that lets Settings store one and
|
|
283
|
+
// wake the worker. Keep this context separate from the heartbeat because the
|
|
284
|
+
// locked process has not started a daemon tick yet, and its startup heartbeat
|
|
285
|
+
// only proves that the child exists.
|
|
286
|
+
const phraseAvailable = config
|
|
287
|
+
? await resolveRecoveryPhrase(dataDir, config.vaultId, undefined, env).then((phrase) => Boolean(phrase)).catch(() => false)
|
|
288
|
+
: Boolean(envVar("RECOVERY_PHRASE", env));
|
|
289
|
+
const waitingForPhrase = config !== null && options.nativeServicePresent === true && !phraseAvailable;
|
|
116
290
|
const stats = await new ArchiveQueue(dataDir).stats();
|
|
117
291
|
checks.push(stats.failed > 0
|
|
118
292
|
? { name: "queue", status: "warn", detail: `${stats.failed} session${stats.failed === 1 ? "" : "s"} could not be archived. See the Queue tab for why, and retry from there.` }
|
|
119
293
|
: { name: "queue", status: "pass", detail: `${stats.ready} ready, ${stats.leased} in flight, ${stats.done} archived` });
|
|
120
|
-
const agents = await
|
|
121
|
-
const
|
|
294
|
+
const agents = await detectSetupAgents(home, dataDir);
|
|
295
|
+
const supported = agents.filter((agent) => agent.detected && agent.archivable);
|
|
296
|
+
const detected = supported.map((agent) => agent.agent);
|
|
122
297
|
checks.push(detected.length > 0
|
|
123
298
|
? { name: "agents", status: "pass", detail: `Detected: ${detected.join(", ")}` }
|
|
124
299
|
: { name: "agents", status: "warn", detail: "No supported agent directory found on this machine" });
|
|
300
|
+
// A command in hooks.json is not proof that Codex will dispatch it. Codex
|
|
301
|
+
// keeps enabled/trust state separately in config.toml and deliberately lets
|
|
302
|
+
// the user disable a reviewed hook. Report that state without changing it;
|
|
303
|
+
// the filesystem watcher remains the independent preservation path.
|
|
304
|
+
if (supported.length > 0) {
|
|
305
|
+
const inactive = supported.filter((agent) => agent.hooksInstalled && !agent.hooksActive);
|
|
306
|
+
const missing = supported.filter((agent) => !agent.hooksInstalled);
|
|
307
|
+
// Routed elsewhere is worse than missing: it passes every presence check
|
|
308
|
+
// while recall and hook capture silently do nothing for this vault.
|
|
309
|
+
const aimedElsewhere = supported.filter((agent) => agent.hooksAimedElsewhere);
|
|
310
|
+
if (aimedElsewhere.length > 0) {
|
|
311
|
+
checks.push({
|
|
312
|
+
name: "agent-memory-hooks", status: "fail",
|
|
313
|
+
detail: aimedElsewhere.map((agent) => `${agent.label}'s memory hooks route to ${agent.hooksDataDir ?? "another data directory"}, not this vault (${dataDir}): automatic recall and hook capture are silently off here. Run \`sealkeep autopilot\` to re-target them.`).join(" ")
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
else if (inactive.length === 0 && missing.length === 0) {
|
|
317
|
+
checks.push({
|
|
318
|
+
name: "agent-memory-hooks", status: "pass",
|
|
319
|
+
detail: `${supported.map((agent) => agent.label).join(" and ")} lifecycle hooks are configured and active.`
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
const details = [
|
|
324
|
+
...inactive.map((agent) => agent.note ?? `${agent.label}'s configured lifecycle hooks are not active.`),
|
|
325
|
+
...missing.map((agent) => `${agent.label}'s complete Sealkeep lifecycle is not configured.`),
|
|
326
|
+
"The background watcher remains the independent archive path while Automatic archiving is running."
|
|
327
|
+
];
|
|
328
|
+
checks.push({ name: "agent-memory-hooks", status: "warn", detail: details.join(" ") });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Hooks fail open, so a recall lane whose passes keep dying is invisible at
|
|
332
|
+
// the agent: memory is simply absent. The service's heartbeat is the one
|
|
333
|
+
// place that records it.
|
|
334
|
+
const recallBeat = await readHeartbeat(dataDir);
|
|
335
|
+
const recallLane = recallBeat?.context;
|
|
336
|
+
const recallService = liveness(recallBeat);
|
|
337
|
+
const { readHookUnlockFailure } = await import("./agent-context.js");
|
|
338
|
+
const unlockFailure = await readHookUnlockFailure(dataDir);
|
|
339
|
+
// A stopped service is the quietest failure of all. The hooks still run and
|
|
340
|
+
// still fail open, so every session simply gets no memory and no reason —
|
|
341
|
+
// exactly what happened on a rehearsal machine whose daemon had died: the
|
|
342
|
+
// requests queued and nobody drained them. Only worth saying when the hooks
|
|
343
|
+
// are installed, because a vault archived by hand has nothing to prepare.
|
|
344
|
+
// Only a service that RAN and stopped: a vault that never started one is
|
|
345
|
+
// being used by hand, which this product supports and says so elsewhere.
|
|
346
|
+
const hooksExpectRecall = supported.some((agent) => agent.hooksInstalled && !agent.hooksAimedElsewhere);
|
|
347
|
+
if (hooksExpectRecall && recallService.state === "stopped") {
|
|
348
|
+
checks.push({
|
|
349
|
+
name: "automatic-recall", status: "fail",
|
|
350
|
+
detail: `Your agents' memory hooks are installed, but the service that prepares memory is not running (${recallService.detail.replace(/^Not running\. /, "").replace(/^It /, "it ")}). Until it runs, every session starts with no memory and no error. Start it from Settings › Automatic archiving, or run \`sealkeep daemon\`.`,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
else if (unlockFailure && Date.now() - Date.parse(unlockFailure.at) < 24 * 60 * 60_000) {
|
|
354
|
+
checks.push({
|
|
355
|
+
name: "automatic-recall", status: "fail",
|
|
356
|
+
detail: `The ${unlockFailure.agent === "codex" ? "Codex" : "Claude Code"} memory hook could not unlock this vault at ${unlockFailure.at.slice(0, 16).replace("T", " ")}: automatic recall and hook capture were silently off for that session, while archiving may have kept working. The hook ran with SEALKEEP_SECRET_BACKEND ${unlockFailure.backend ? `= ${unlockFailure.backend}` : "unset"}; the hooks and the background service must find the recovery phrase in the same secret backend (Settings › Unlocking, or \`sealkeep autopilot\`).`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else if (recallLane?.state === "attention" && recallLane.failures >= 3) {
|
|
360
|
+
checks.push({
|
|
361
|
+
name: "automatic-recall", status: "warn",
|
|
362
|
+
detail: `Automatic recall passes keep failing (${recallLane.failures} in a row, last: ${recallLane.lastError ?? "unknown"}${recallLane.lastSuccessAt ? `, last success ${recallLane.lastSuccessAt.slice(0, 16).replace("T", " ")}` : ""}). Memory hooks still run, but recall arrives late or not at all until this clears; the background service retries every half minute.`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
else if (recallLane?.lastSuccessAt) {
|
|
366
|
+
checks.push({
|
|
367
|
+
name: "automatic-recall", status: "pass",
|
|
368
|
+
detail: `Automatic recall last prepared memory at ${recallLane.lastSuccessAt.slice(0, 16).replace("T", " ")}.`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
125
371
|
// The failure Paul hit in the wild: an agent's own catalog still points at
|
|
126
372
|
// a session file that is gone — reclaimed by us, or deleted by anything —
|
|
127
373
|
// and "resume" breaks with no hint that the bytes are one command away.
|
|
128
374
|
// This check counts archived sessions whose source file is missing and
|
|
129
375
|
// hands over the exact command that puts each back where resume expects it.
|
|
376
|
+
// A reclaimed Codex transcript can deliberately leave a tiny valid rollout
|
|
377
|
+
// at that path. Do not call that pointer the original source: name what it
|
|
378
|
+
// is and give the overwrite mode required while it occupies the destination.
|
|
130
379
|
try {
|
|
131
380
|
const { stat } = await import("node:fs/promises");
|
|
132
381
|
const { listArchives } = await import("./vault.js");
|
|
133
382
|
const records = (await listArchives(dataDir)).filter((record) => record.version === 2);
|
|
134
383
|
const missing = [];
|
|
384
|
+
const pointers = [];
|
|
135
385
|
for (const record of records) {
|
|
136
386
|
const there = await stat(record.source.path).then((entry) => entry.isFile()).catch(() => false);
|
|
137
|
-
|
|
138
|
-
|
|
387
|
+
const filename = record.source.path.split("/").pop() ?? record.id;
|
|
388
|
+
if (!there) {
|
|
389
|
+
missing.push({ id: record.id, filename });
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (record.reclaimed?.resumeStub?.version === 1) {
|
|
393
|
+
const pointer = await readUntouchedCodexResumeStub(record.source.path);
|
|
394
|
+
if (pointer
|
|
395
|
+
&& pointer.archiveId === record.id
|
|
396
|
+
&& pointer.sessionId === record.reclaimed.resumeStub.sessionId
|
|
397
|
+
&& pointer.bytes === record.reclaimed.resumeStub.bytes
|
|
398
|
+
&& pointer.sha256 === record.reclaimed.resumeStub.sha256) {
|
|
399
|
+
pointers.push({ id: record.id, filename, sessionId: pointer.sessionId });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
139
402
|
}
|
|
140
403
|
if (missing.length > 0) {
|
|
141
404
|
checks.push({
|
|
142
405
|
name: "resumable-history", status: "pass",
|
|
143
|
-
detail: `${missing.length} archived session${missing.length === 1 ? " file is" : " files are"} no longer on disk (reclaimed or removed).
|
|
406
|
+
detail: `${missing.length} archived session${missing.length === 1 ? " file is" : " files are"} no longer on disk (reclaimed or removed). Restore the exact archive to its native path before resuming. First: ${missing[0].filename} — \`sealkeep recover ${missing[0].id} --native\``
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
else if (pointers.length > 0) {
|
|
410
|
+
checks.push({
|
|
411
|
+
name: "resumable-history", status: "pass",
|
|
412
|
+
detail: `${pointers.length} reclaimed Codex session${pointers.length === 1 ? " keeps" : "s keep"} a tiny native resume pointer, so \`codex resume ${pointers[0].sessionId}\` remains discoverable without downloading the full transcript. Use its Sealkeep MCP search instruction for relevant memory. To restore every byte explicitly while preserving the pointer as a backup, run \`sealkeep recover ${pointers[0].id} --native --overwrite backup\`, then resume again. First: ${pointers[0].filename}`
|
|
144
413
|
});
|
|
145
414
|
}
|
|
146
415
|
else if (records.length > 0) {
|
|
147
|
-
checks.push({ name: "resumable-history", status: "pass", detail: "Every archived session
|
|
416
|
+
checks.push({ name: "resumable-history", status: "pass", detail: "Every archived session remains discoverable at its native path; agent resume needs nothing from Sealkeep" });
|
|
148
417
|
}
|
|
149
418
|
}
|
|
150
419
|
catch { /* an unreadable archive store already failed its own check above */ }
|
|
@@ -159,12 +428,31 @@ export async function runDoctor(dataDir, env = process.env) {
|
|
|
159
428
|
// nothing archived yet — nothing to say
|
|
160
429
|
}
|
|
161
430
|
else if (coverage.missing.length === 0) {
|
|
162
|
-
|
|
431
|
+
// "All N archives" means all N THIS MACHINE sealed. Saying so keeps the
|
|
432
|
+
// sentence true on a team, where the account holds more than this
|
|
433
|
+
// machine has ever seen.
|
|
434
|
+
checks.push({ name: "search-index", status: "pass", detail: `All ${coverage.total} archives sealed on this machine are in the content search index${coverage.builtAt ? ` (updated ${coverage.builtAt.slice(0, 10)})` : ""}.` });
|
|
163
435
|
}
|
|
164
436
|
else {
|
|
437
|
+
const daemonBeat = await readHeartbeat(dataDir);
|
|
438
|
+
const service = liveness(daemonBeat);
|
|
439
|
+
const indexing = service.state === "running" ? daemonBeat?.indexing : undefined;
|
|
440
|
+
let automation;
|
|
441
|
+
if (waitingForPhrase) {
|
|
442
|
+
automation = "Automatic catch-up is waiting for this machine's recovery phrase; it will resume after you store it in Settings › Unlocking.";
|
|
443
|
+
}
|
|
444
|
+
else if (indexing?.state === "building") {
|
|
445
|
+
automation = `Automatic catch-up is running in the background (attempt ${indexing.attempts}).`;
|
|
446
|
+
}
|
|
447
|
+
else if (indexing?.state === "retry-wait") {
|
|
448
|
+
automation = `Automatic catch-up is waiting to retry${indexing.nextAttemptAt ? ` after ${indexing.nextAttemptAt}` : ""}${indexing.lastError ? ` (${indexing.lastError})` : ""}.`;
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
automation = "Automatic catch-up is not currently active; repair or restart automatic archiving in Settings.";
|
|
452
|
+
}
|
|
165
453
|
checks.push({
|
|
166
|
-
name: "search-index", status: "
|
|
167
|
-
detail: `${coverage.indexed} of ${coverage.total} archives are searchable; ${coverage.missing.length} not yet indexed
|
|
454
|
+
name: "search-index", status: "warn",
|
|
455
|
+
detail: `${coverage.indexed} of ${coverage.total} archives sealed on this machine are searchable; ${coverage.missing.length} not yet indexed. ${automation}`
|
|
168
456
|
});
|
|
169
457
|
}
|
|
170
458
|
}
|
|
@@ -179,20 +467,13 @@ export async function runDoctor(dataDir, env = process.env) {
|
|
|
179
467
|
const pinned = [...new Set(targets.flatMap((target) => target.projects ?? []))];
|
|
180
468
|
if (pinned.length > 0) {
|
|
181
469
|
const { listArchives } = await import("./vault.js");
|
|
182
|
-
const {
|
|
470
|
+
const { projectOfRecord } = await import("./adapters.js");
|
|
183
471
|
const records = await listArchives(dataDir).catch(() => []);
|
|
184
472
|
const known = new Set();
|
|
185
473
|
for (const record of records) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
: record.source.agent === "codex"
|
|
190
|
-
? (await codexProjectFromRollout(record.source.path, record.createdAt)).project
|
|
191
|
-
: null;
|
|
192
|
-
if (name)
|
|
193
|
-
known.add(name);
|
|
194
|
-
}
|
|
195
|
-
catch { /* a project we cannot read is not evidence of a bad pin */ }
|
|
474
|
+
const name = await projectOfRecord(record).catch(() => null);
|
|
475
|
+
if (name)
|
|
476
|
+
known.add(name);
|
|
196
477
|
}
|
|
197
478
|
const unmatched = pinned.filter((name) => !known.has(name));
|
|
198
479
|
if (unmatched.length === 0) {
|
|
@@ -212,25 +493,35 @@ export async function runDoctor(dataDir, env = process.env) {
|
|
|
212
493
|
// so a machine set up correctly through the window — phrase in the keystore,
|
|
213
494
|
// sealing away happily — was told on its Health tab that its phrase was
|
|
214
495
|
// missing and named an environment variable as the cure.
|
|
215
|
-
const phraseAvailable = config
|
|
216
|
-
? await resolveRecoveryPhrase(dataDir, config.vaultId, undefined, env).then((phrase) => Boolean(phrase)).catch(() => false)
|
|
217
|
-
: Boolean(envVar("RECOVERY_PHRASE", env));
|
|
218
496
|
checks.push(phraseAvailable
|
|
219
497
|
? { name: "recovery-phrase", status: "pass", detail: "This machine can unlock the vault on its own, so archiving runs unattended." }
|
|
220
498
|
: { name: "recovery-phrase", status: "warn", detail: "This machine doesn't hold your recovery phrase, so it can't archive on its own. Add it under Settings › Unlocking to let archiving run unattended. Your vault and archives are unaffected." });
|
|
221
499
|
const tokenMode = await stat(localApiTokenPath(dataDir)).then((info) => info.mode & 0o777).catch(() => null);
|
|
222
500
|
checks.push(tokenMode === null
|
|
223
|
-
? { name: "local-api-token", status: "warn", detail: "No local API token yet; it
|
|
501
|
+
? { name: "local-api-token", status: "warn", detail: "No local API token yet; Autopilot creates it with the local UI, as do `sealkeep ui` and `sealkeep api`" }
|
|
224
502
|
: tokenMode === 0o600
|
|
225
503
|
? { name: "local-api-token", status: "pass", detail: "Local API token is owner-only" }
|
|
226
504
|
: { name: "local-api-token", status: "fail", detail: `Local API token mode is ${tokenMode.toString(8)}; it must be 600` });
|
|
227
|
-
|
|
505
|
+
// The doctor normally runs inside the local dashboard process, while the
|
|
506
|
+
// uploader runs inside a separately installed launchd/systemd service. Its
|
|
507
|
+
// environment is intentionally not inherited by the dashboard. Inspecting
|
|
508
|
+
// this process's signer flag therefore produced the exact opposite of the
|
|
509
|
+
// truth on healthy machines. The daemon publishes its effective capability
|
|
510
|
+
// in the heartbeat; the local flag remains a compatibility fallback for a
|
|
511
|
+
// foreground `sealkeep doctor` invocation.
|
|
512
|
+
const daemonBeat = await readHeartbeat(dataDir);
|
|
513
|
+
const daemonLive = liveness(daemonBeat);
|
|
514
|
+
const daemonUploads = daemonLive.state !== "stopped" && daemonLive.state !== "never-started"
|
|
515
|
+
&& daemonBeat?.capabilities?.uploads === true;
|
|
516
|
+
const signerOn = daemonUploads || signerEnabled(env);
|
|
228
517
|
const credentials = config ? await loadProviderCredentials(dataDir, config.vaultId).then(() => true).catch(() => false) : false;
|
|
229
|
-
checks.push(
|
|
230
|
-
? { name: "signer", status: "warn", detail: "
|
|
231
|
-
:
|
|
232
|
-
? { name: "signer", status: "
|
|
233
|
-
:
|
|
518
|
+
checks.push(waitingForPhrase
|
|
519
|
+
? { name: "signer", status: "warn", detail: "Automatic uploads are waiting for this machine's recovery phrase; they will resume after you store it in Settings › Unlocking." }
|
|
520
|
+
: !signerOn
|
|
521
|
+
? { name: "signer", status: "warn", detail: `${daemonLive.state === "late" ? "The background service has stopped checking in on time." : "Automatic uploads are not running on this machine."} Anything already shown as verified in your storage is safely there. Repair automatic uploads in Settings.` }
|
|
522
|
+
: managed || credentials
|
|
523
|
+
? { name: "signer", status: "pass", detail: daemonUploads ? "The background service reports automatic uploads enabled." : "Uploads are enabled in this process and storage credentials are available." }
|
|
524
|
+
: { name: "signer", status: "warn", detail: "Uploads are on, but this machine has no usable sign-in for its configured storage. Reconnect that destination in Settings." });
|
|
234
525
|
// Deliberately a standing warning: the envelope is unaudited, and no local check can change that.
|
|
235
526
|
checks.push({ name: "crypto", status: "warn", detail: `Archive envelope v${ENVELOPE_VERSION} (${SUITES[0]}); implemented against published primitives and not independently audited` });
|
|
236
527
|
return { ok: checks.every((check) => check.status !== "fail"), checks };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type DurableTicketLock = {
|
|
2
|
+
readonly queuePath: string;
|
|
3
|
+
readonly claimPath: string;
|
|
4
|
+
readonly ticket: string;
|
|
5
|
+
readonly token: string;
|
|
6
|
+
assertOwned(): Promise<void>;
|
|
7
|
+
release(): Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export type DurableTicketLockOptions = {
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
pollMs?: number;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
isProcessAlive?: (pid: number) => Promise<boolean>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Cross-process FIFO lock without stale-owner unlink.
|
|
17
|
+
*
|
|
18
|
+
* Every contender atomically allocates a monotonically increasing immutable
|
|
19
|
+
* ticket. Released and dead-owner tickets stay as tiny tombstones, so no later
|
|
20
|
+
* process can recreate an older pathname or race a compare-then-unlink stale
|
|
21
|
+
* takeover. A dead process is skipped by later tickets; a malformed earlier
|
|
22
|
+
* ticket fails closed until an operator repairs it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function acquireDurableTicketLock(requestedQueuePath: string, resource: string, options?: DurableTicketLockOptions): Promise<DurableTicketLock>;
|