sealkeep 0.8.1 → 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 +244 -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 +1746 -166
- 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.d.ts +1 -0
- package/dist/src/migrate.js +33 -5
- 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 +235 -29
- package/dist/src/vault.js +1783 -190
- 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,47 @@
|
|
|
1
|
+
import type { ArchiveRecordV2, RemoteObject } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* One physical object has one id, independent of which routing rule currently
|
|
4
|
+
* names it. The version marker makes a future identity scheme explicit rather
|
|
5
|
+
* than silently changing ids already used by an API or operation journal.
|
|
6
|
+
*/
|
|
7
|
+
export declare function archiveCopyId(copy: Pick<RemoteObject, "provider" | "bucket" | "objectKey" | "destinationFingerprint">): string;
|
|
8
|
+
/** Returns a copy with the deterministic id old records did not persist. */
|
|
9
|
+
export declare function normalizeRemoteCopy(copy: RemoteObject): RemoteObject;
|
|
10
|
+
/** Stable-order dedupe: the first occurrence keeps its position. */
|
|
11
|
+
export declare function dedupeArchiveCopies(copies: readonly RemoteObject[]): RemoteObject[];
|
|
12
|
+
/** Primary first, followed by unique secondary copies. Never mutates a record. */
|
|
13
|
+
export declare function archiveCopies(record: Pick<ArchiveRecordV2, "remote" | "copies">): RemoteObject[];
|
|
14
|
+
/** Rebuilds the compatibility shape: one primary plus optional secondaries. */
|
|
15
|
+
export declare function normalizeArchiveCopies(record: ArchiveRecordV2): ArchiveRecordV2;
|
|
16
|
+
/** True when any primary or secondary copy has earned a verification marker. */
|
|
17
|
+
export declare function hasVerifiedCopy(record: Pick<ArchiveRecordV2, "remote" | "copies">): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Non-secret identity used only to prove that separately stored delta links
|
|
20
|
+
* can be fetched from one destination. A mutable target label is secondary to
|
|
21
|
+
* the immutable destination fingerprint. Legacy AWS S3/GCS/B2 records can use
|
|
22
|
+
* their bucket; ambiguous R2/Drive records fail closed unless they retained a
|
|
23
|
+
* target id. A single self-contained archive does not need this join key.
|
|
24
|
+
*/
|
|
25
|
+
export declare function verifiedCopyDestinationKey(copy: RemoteObject): string | null;
|
|
26
|
+
/** Destination keys at which this exact archive object has a verified copy. */
|
|
27
|
+
export declare function verifiedCopyDestinations(record: Pick<ArchiveRecordV2, "remote" | "copies">): Set<string>;
|
|
28
|
+
/**
|
|
29
|
+
* True only when one remote destination contains every record needed by a
|
|
30
|
+
* session. Verified links split between Drive and Cloud are not a restorable
|
|
31
|
+
* off-machine chain.
|
|
32
|
+
*/
|
|
33
|
+
export declare function hasCompleteVerifiedCopyChain(records: readonly ArchiveRecordV2[]): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Idempotently adds or refreshes a copy. The first copy becomes primary;
|
|
36
|
+
* `makePrimary` explicitly promotes an existing or newly added destination.
|
|
37
|
+
*/
|
|
38
|
+
export declare function appendArchiveCopy(record: ArchiveRecordV2, incoming: RemoteObject, options?: {
|
|
39
|
+
makePrimary?: boolean;
|
|
40
|
+
}): ArchiveRecordV2;
|
|
41
|
+
/** Promotes one known copy; an unknown id is refused rather than silently succeeding. */
|
|
42
|
+
export declare function promoteArchiveCopy(record: ArchiveRecordV2, copyId: string): ArchiveRecordV2;
|
|
43
|
+
/**
|
|
44
|
+
* Idempotently removes a copy. Removing the primary promotes the first surviving
|
|
45
|
+
* secondary, preserving the legacy `remote` contract.
|
|
46
|
+
*/
|
|
47
|
+
export declare function removeArchiveCopy(record: ArchiveRecordV2, copyId: string): ArchiveRecordV2;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* One physical object has one id, independent of which routing rule currently
|
|
4
|
+
* names it. The version marker makes a future identity scheme explicit rather
|
|
5
|
+
* than silently changing ids already used by an API or operation journal.
|
|
6
|
+
*/
|
|
7
|
+
export function archiveCopyId(copy) {
|
|
8
|
+
const location = copy.destinationFingerprint
|
|
9
|
+
? ["v2", copy.destinationFingerprint, copy.provider.trim().toLowerCase(), copy.bucket, copy.objectKey].join("\0")
|
|
10
|
+
: ["v1", copy.provider.trim().toLowerCase(), copy.bucket, copy.objectKey].join("\0");
|
|
11
|
+
return `copy_${createHash("sha256").update(location).digest("hex")}`;
|
|
12
|
+
}
|
|
13
|
+
/** Returns a copy with the deterministic id old records did not persist. */
|
|
14
|
+
export function normalizeRemoteCopy(copy) {
|
|
15
|
+
return { ...copy, copyId: archiveCopyId(copy) };
|
|
16
|
+
}
|
|
17
|
+
function verifiedTime(copy) {
|
|
18
|
+
const time = Date.parse(copy.verifiedAt);
|
|
19
|
+
return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Merge two observations of the same object. The newest verification owns the
|
|
23
|
+
* byte/checksum/layout facts; a target id is routing metadata and is retained
|
|
24
|
+
* when either observation has it.
|
|
25
|
+
*/
|
|
26
|
+
function mergeSameCopy(current, incoming) {
|
|
27
|
+
const newer = verifiedTime(incoming) >= verifiedTime(current) ? incoming : current;
|
|
28
|
+
const older = newer === incoming ? current : incoming;
|
|
29
|
+
const targetId = newer.targetId ?? older.targetId;
|
|
30
|
+
const merged = {
|
|
31
|
+
...older,
|
|
32
|
+
...newer,
|
|
33
|
+
copyId: archiveCopyId(newer),
|
|
34
|
+
...(targetId ? { targetId } : {}),
|
|
35
|
+
};
|
|
36
|
+
// Missing layout means the current object is the legacy single-object form.
|
|
37
|
+
// A stale chunk observation must not turn it back into a folder.
|
|
38
|
+
if (newer.layout === undefined)
|
|
39
|
+
delete merged.layout;
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
/** Stable-order dedupe: the first occurrence keeps its position. */
|
|
43
|
+
export function dedupeArchiveCopies(copies) {
|
|
44
|
+
const result = [];
|
|
45
|
+
const positions = new Map();
|
|
46
|
+
for (const raw of copies) {
|
|
47
|
+
const copy = normalizeRemoteCopy(raw);
|
|
48
|
+
const id = copy.copyId;
|
|
49
|
+
const existing = positions.get(id);
|
|
50
|
+
if (existing === undefined) {
|
|
51
|
+
positions.set(id, result.length);
|
|
52
|
+
result.push(copy);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
result[existing] = mergeSameCopy(result[existing], copy);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
/** Primary first, followed by unique secondary copies. Never mutates a record. */
|
|
61
|
+
export function archiveCopies(record) {
|
|
62
|
+
return dedupeArchiveCopies([...(record.remote ? [record.remote] : []), ...(record.copies ?? [])]);
|
|
63
|
+
}
|
|
64
|
+
/** Rebuilds the compatibility shape: one primary plus optional secondaries. */
|
|
65
|
+
export function normalizeArchiveCopies(record) {
|
|
66
|
+
const all = archiveCopies(record);
|
|
67
|
+
const { remote: _remote, copies: _copies, ...unchanged } = record;
|
|
68
|
+
if (all.length === 0)
|
|
69
|
+
return unchanged;
|
|
70
|
+
const [remote, ...copies] = all;
|
|
71
|
+
return {
|
|
72
|
+
...unchanged,
|
|
73
|
+
remote,
|
|
74
|
+
...(copies.length > 0 ? { copies } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** True when any primary or secondary copy has earned a verification marker. */
|
|
78
|
+
export function hasVerifiedCopy(record) {
|
|
79
|
+
return archiveCopies(record).some((copy) => Number.isFinite(Date.parse(copy.verifiedAt)));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Non-secret identity used only to prove that separately stored delta links
|
|
83
|
+
* can be fetched from one destination. A mutable target label is secondary to
|
|
84
|
+
* the immutable destination fingerprint. Legacy AWS S3/GCS/B2 records can use
|
|
85
|
+
* their bucket; ambiguous R2/Drive records fail closed unless they retained a
|
|
86
|
+
* target id. A single self-contained archive does not need this join key.
|
|
87
|
+
*/
|
|
88
|
+
export function verifiedCopyDestinationKey(copy) {
|
|
89
|
+
if (!Number.isFinite(Date.parse(copy.verifiedAt)) || typeof copy.provider !== "string")
|
|
90
|
+
return null;
|
|
91
|
+
const provider = copy.provider.trim().toLowerCase();
|
|
92
|
+
if (!provider)
|
|
93
|
+
return null;
|
|
94
|
+
if (copy.destinationFingerprint?.trim())
|
|
95
|
+
return `${provider}\0fingerprint\0${copy.destinationFingerprint.trim()}`;
|
|
96
|
+
if (provider === "vaultline")
|
|
97
|
+
return `${provider}\0managed\0${copy.bucket || "account"}`;
|
|
98
|
+
if (copy.targetId?.trim())
|
|
99
|
+
return `${provider}\0target\0${copy.targetId.trim()}\0${copy.bucket || ""}`;
|
|
100
|
+
if (copy.bucket && provider !== "r2" && provider !== "gdrive")
|
|
101
|
+
return `${provider}\0legacy-bucket\0${copy.bucket}`;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/** Destination keys at which this exact archive object has a verified copy. */
|
|
105
|
+
export function verifiedCopyDestinations(record) {
|
|
106
|
+
return new Set(archiveCopies(record).map(verifiedCopyDestinationKey).filter((key) => Boolean(key)));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* True only when one remote destination contains every record needed by a
|
|
110
|
+
* session. Verified links split between Drive and Cloud are not a restorable
|
|
111
|
+
* off-machine chain.
|
|
112
|
+
*/
|
|
113
|
+
export function hasCompleteVerifiedCopyChain(records) {
|
|
114
|
+
if (records.length === 0)
|
|
115
|
+
return false;
|
|
116
|
+
if (records.length === 1)
|
|
117
|
+
return hasVerifiedCopy(records[0]);
|
|
118
|
+
let common = null;
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
const destinations = verifiedCopyDestinations(record);
|
|
121
|
+
if (common === null)
|
|
122
|
+
common = destinations;
|
|
123
|
+
else {
|
|
124
|
+
const previous = common;
|
|
125
|
+
common = new Set([...previous].filter((destination) => destinations.has(destination)));
|
|
126
|
+
}
|
|
127
|
+
if (common.size === 0)
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return (common?.size ?? 0) > 0;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Idempotently adds or refreshes a copy. The first copy becomes primary;
|
|
134
|
+
* `makePrimary` explicitly promotes an existing or newly added destination.
|
|
135
|
+
*/
|
|
136
|
+
export function appendArchiveCopy(record, incoming, options = {}) {
|
|
137
|
+
const all = archiveCopies(record);
|
|
138
|
+
const copy = normalizeRemoteCopy(incoming);
|
|
139
|
+
const existing = all.findIndex((candidate) => candidate.copyId === copy.copyId);
|
|
140
|
+
if (existing >= 0)
|
|
141
|
+
all[existing] = mergeSameCopy(all[existing], copy);
|
|
142
|
+
else
|
|
143
|
+
all.push(copy);
|
|
144
|
+
if (options.makePrimary && all.length > 1) {
|
|
145
|
+
const selected = all.findIndex((candidate) => candidate.copyId === copy.copyId);
|
|
146
|
+
if (selected > 0)
|
|
147
|
+
all.unshift(...all.splice(selected, 1));
|
|
148
|
+
}
|
|
149
|
+
return recordWithCopies(record, all);
|
|
150
|
+
}
|
|
151
|
+
/** Promotes one known copy; an unknown id is refused rather than silently succeeding. */
|
|
152
|
+
export function promoteArchiveCopy(record, copyId) {
|
|
153
|
+
const all = archiveCopies(record);
|
|
154
|
+
const selected = all.findIndex((copy) => copy.copyId === copyId);
|
|
155
|
+
if (selected < 0)
|
|
156
|
+
throw new RangeError(`Archive ${record.id} has no remote copy ${copyId}`);
|
|
157
|
+
if (selected > 0)
|
|
158
|
+
all.unshift(...all.splice(selected, 1));
|
|
159
|
+
return recordWithCopies(record, all);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Idempotently removes a copy. Removing the primary promotes the first surviving
|
|
163
|
+
* secondary, preserving the legacy `remote` contract.
|
|
164
|
+
*/
|
|
165
|
+
export function removeArchiveCopy(record, copyId) {
|
|
166
|
+
return recordWithCopies(record, archiveCopies(record).filter((copy) => copy.copyId !== copyId));
|
|
167
|
+
}
|
|
168
|
+
function recordWithCopies(record, copies) {
|
|
169
|
+
const normal = dedupeArchiveCopies(copies);
|
|
170
|
+
const { remote: _remote, copies: _copies, ...unchanged } = record;
|
|
171
|
+
if (normal.length === 0)
|
|
172
|
+
return unchanged;
|
|
173
|
+
const [remote, ...secondary] = normal;
|
|
174
|
+
return {
|
|
175
|
+
...unchanged,
|
|
176
|
+
remote,
|
|
177
|
+
...(secondary.length > 0 ? { copies: secondary } : {}),
|
|
178
|
+
};
|
|
179
|
+
}
|
package/dist/src/audit.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AuditAction = "archive.create" | "archive.restore" | "archive.migrate" | "archive.rewrap" | "archive.prune" | "archive.offload" | "archive.delete" | "phrase.rotate" | "password.change" | "archive.share" | "source.reclaim" | "recipient.add" | "recipient.remove" | "upload.verify" | "remote.check" | "retention.approve";
|
|
1
|
+
export type AuditAction = "archive.create" | "index.tokens" | "index.merge" | "archive.restore" | "archive.restore_local" | "archive.sealed_copy_download" | "archive.migrate" | "archive.rewrap" | "archive.prune" | "archive.offload" | "archive.delete" | "archive.copy_delete" | "phrase.rotate" | "password.change" | "archive.share" | "source.reclaim" | "recipient.add" | "recipient.remove" | "upload.verify" | "remote.check" | "retention.approve" | "storage.gdrive_connect" | "storage.target_connect" | "storage.targets_update" | "phrase.reveal" | "recovery.codes_created" | "bridge.share" | "project.sharing" | "project.draft";
|
|
2
2
|
export type LocalAuditEvent = {
|
|
3
3
|
id: string;
|
|
4
4
|
at: string;
|
package/dist/src/audit.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, open, readFile, rename, stat } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
const MAX_BYTES = 5 * 1024 * 1024;
|
|
5
5
|
export const auditPath = (dataDir) => join(dataDir, "audit.jsonl");
|
|
6
|
+
async function syncAuditDirectory(dataDir) {
|
|
7
|
+
if (process.platform === "win32")
|
|
8
|
+
return;
|
|
9
|
+
const directory = await open(dataDir, "r");
|
|
10
|
+
try {
|
|
11
|
+
await directory.sync();
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
await directory.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
6
17
|
/**
|
|
7
18
|
* Append-only local audit log.
|
|
8
19
|
*
|
|
@@ -14,10 +25,24 @@ export async function recordAudit(dataDir, action, outcome, detail) {
|
|
|
14
25
|
const event = { id: randomUUID(), at: new Date().toISOString(), action, outcome, detail };
|
|
15
26
|
const path = auditPath(dataDir);
|
|
16
27
|
await mkdir(dataDir, { recursive: true, mode: 0o700 });
|
|
17
|
-
const
|
|
18
|
-
if (size > MAX_BYTES)
|
|
28
|
+
const current = await stat(path).catch(() => null);
|
|
29
|
+
if ((current?.size ?? 0) > MAX_BYTES) {
|
|
19
30
|
await rename(path, `${path}.1`).catch(() => undefined);
|
|
20
|
-
|
|
31
|
+
await syncAuditDirectory(dataDir);
|
|
32
|
+
}
|
|
33
|
+
// A destructive decision is not "committed" merely because bytes reached a
|
|
34
|
+
// userspace stream. fsync the append before returning so reclaim can safely
|
|
35
|
+
// place its durable purge-authorized marker afterwards.
|
|
36
|
+
const handle = await open(path, "a", 0o600);
|
|
37
|
+
try {
|
|
38
|
+
await handle.writeFile(`${JSON.stringify(event)}\n`, "utf8");
|
|
39
|
+
await handle.sync();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await handle.close();
|
|
43
|
+
}
|
|
44
|
+
if (!current || current.size > MAX_BYTES)
|
|
45
|
+
await syncAuditDirectory(dataDir);
|
|
21
46
|
return event;
|
|
22
47
|
}
|
|
23
48
|
export async function readAudit(dataDir, options = {}) {
|
package/dist/src/autopilot.d.ts
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import { ArchiveQueue } from "./queue.js";
|
|
2
|
-
import { type RetentionSettings } from "./retention.js";
|
|
2
|
+
import { type RetentionPolicy, type RetentionSettings } from "./retention.js";
|
|
3
3
|
import { type BackendName } from "./secrets.js";
|
|
4
4
|
import { serviceStatus, type ServiceOptions, type ServiceOutcome } from "./service.js";
|
|
5
5
|
import { type Liveness } from "./heartbeat.js";
|
|
6
|
+
import type { McpExec } from "./mcp-install.js";
|
|
7
|
+
import { type UiRuntimeStatus } from "./ui-server.js";
|
|
6
8
|
export type AutopilotOptions = {
|
|
7
9
|
phrase?: string;
|
|
8
10
|
/** Keep the phrase in this machine's keystore so the service can run unattended. */
|
|
9
11
|
remember?: boolean;
|
|
10
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Let the service reclaim disk once every safety gate passes. Omitted means
|
|
14
|
+
* "keep this machine's saved choice"; a machine with no saved choice starts
|
|
15
|
+
* false. This distinction is what makes a package upgrade idempotent without
|
|
16
|
+
* turning deletion on for a fresh install.
|
|
17
|
+
*/
|
|
11
18
|
reclaim?: boolean;
|
|
12
19
|
olderThanDays?: number;
|
|
13
20
|
graceDays?: number;
|
|
14
21
|
home?: string;
|
|
15
22
|
platform?: NodeJS.Platform;
|
|
16
23
|
exec?: ServiceOptions["exec"];
|
|
24
|
+
/** Separate seam for agent-owned MCP installers; falls back to `exec` in tests. */
|
|
25
|
+
mcpExec?: McpExec;
|
|
17
26
|
cliPath?: string;
|
|
18
27
|
/** Pin the keystore. Omitted, the strongest backend this machine offers is used. */
|
|
19
28
|
backend?: BackendName;
|
|
@@ -30,6 +39,8 @@ export type AutopilotResult = {
|
|
|
30
39
|
retention: RetentionSettings;
|
|
31
40
|
service: ServiceOutcome;
|
|
32
41
|
agents: string[];
|
|
42
|
+
/** Agents whose complete automatic memory lifecycle is installed (or would be in a dry run). */
|
|
43
|
+
hooks: string[];
|
|
33
44
|
found: {
|
|
34
45
|
sessions: number;
|
|
35
46
|
bytes: number;
|
|
@@ -39,14 +50,35 @@ export type AutopilotResult = {
|
|
|
39
50
|
hasRemoteTarget: boolean;
|
|
40
51
|
notes: string[];
|
|
41
52
|
};
|
|
53
|
+
export type RetentionMachineSafety = {
|
|
54
|
+
/** Whether this call changed the machine's standing deletion permission. */
|
|
55
|
+
reclaimPermissionRevoked: boolean;
|
|
56
|
+
/** Whether an Autopilot service definition belongs to this vault. */
|
|
57
|
+
serviceInstalled: boolean;
|
|
58
|
+
/** Whether that installed definition was rewritten with the safe invocation. */
|
|
59
|
+
serviceReconciled: boolean;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Keep a non-reclaim retention policy and the machine that executes it in sync.
|
|
63
|
+
*
|
|
64
|
+
* Retention timing lives in the vault config, while unattended deletion is a
|
|
65
|
+
* separate, machine-local permission and an installed service also remembers
|
|
66
|
+
* its invocation. Changing only the policy is safe today, but leaves enough
|
|
67
|
+
* stale state for a later policy edit or older service generation to resume
|
|
68
|
+
* deletion without a new machine opt-in. Revoke that permission and rewrite an
|
|
69
|
+
* already-installed service receipt, preserving the schedule and every other
|
|
70
|
+
* local setting. A reclaim-capable policy deliberately grants nothing here.
|
|
71
|
+
*/
|
|
72
|
+
export declare function reconcileMachineRetentionPolicy(dataDir: string, policy: RetentionPolicy, options?: Pick<AutopilotOptions, "home" | "platform" | "exec" | "cliPath">): Promise<RetentionMachineSafety>;
|
|
42
73
|
/**
|
|
43
74
|
* Turns Sealkeep into something that runs itself.
|
|
44
75
|
*
|
|
45
|
-
* One call sets up the vault, teaches this machine to unlock it,
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
76
|
+
* One call sets up the vault, teaches this machine to unlock it, sweeps up the
|
|
77
|
+
* sessions already on disk, and installs a login service. Preservation, indexing,
|
|
78
|
+
* and uploads are automatic. Reclaim is a separate standing permission: it is off
|
|
79
|
+
* on a fresh machine and is enabled only by an explicit option or an already-saved
|
|
80
|
+
* machine choice. Even then, the ordinary remote, search, identity, age, grace and
|
|
81
|
+
* liveness gates remain mandatory.
|
|
50
82
|
*/
|
|
51
83
|
export declare function enableAutopilot(dataDir: string, options?: AutopilotOptions): Promise<AutopilotResult>;
|
|
52
84
|
export type AutopilotStatus = {
|
|
@@ -55,9 +87,13 @@ export type AutopilotStatus = {
|
|
|
55
87
|
live: Liveness;
|
|
56
88
|
service: Awaited<ReturnType<typeof serviceStatus>>;
|
|
57
89
|
phraseAvailable: boolean;
|
|
90
|
+
/** Effective per-machine permission followed by the installed service. */
|
|
91
|
+
reclaimEnabled: boolean;
|
|
58
92
|
retention: RetentionSettings;
|
|
59
93
|
hasRemoteTarget: boolean;
|
|
60
94
|
queue: Awaited<ReturnType<ArchiveQueue["stats"]>>;
|
|
95
|
+
/** Live listener truth, including a recorded fallback when 7477 was busy. */
|
|
96
|
+
ui: UiRuntimeStatus;
|
|
61
97
|
};
|
|
62
98
|
export declare function autopilotStatus(dataDir: string, options?: AutopilotOptions): Promise<AutopilotStatus>;
|
|
63
99
|
/**
|
|
@@ -65,6 +101,13 @@ export declare function autopilotStatus(dataDir: string, options?: AutopilotOpti
|
|
|
65
101
|
* automation off is not a way to lose data.
|
|
66
102
|
*/
|
|
67
103
|
export declare function disableAutopilot(dataDir: string, options?: AutopilotOptions): Promise<{
|
|
104
|
+
service: ServiceOutcome;
|
|
105
|
+
forgotPhrase: boolean;
|
|
106
|
+
retention: RetentionSettings;
|
|
107
|
+
archivesKept: boolean;
|
|
108
|
+
hooksRemoved: string[];
|
|
109
|
+
hookOutcomes: never[];
|
|
110
|
+
} | {
|
|
68
111
|
service: ServiceOutcome;
|
|
69
112
|
forgotPhrase: boolean;
|
|
70
113
|
retention: {
|
|
@@ -74,4 +117,6 @@ export declare function disableAutopilot(dataDir: string, options?: AutopilotOpt
|
|
|
74
117
|
deleteAfterDays?: number | null;
|
|
75
118
|
};
|
|
76
119
|
archivesKept: boolean;
|
|
120
|
+
hooksRemoved: import("./adapters.js").AgentId[];
|
|
121
|
+
hookOutcomes: import("./adapters.js").HookRemoveOutcome[];
|
|
77
122
|
}>;
|
package/dist/src/autopilot.js
CHANGED
|
@@ -1,29 +1,81 @@
|
|
|
1
1
|
import { resolveSyncRules, applySyncRules, describeSyncRules } from "./sync-rules.js";
|
|
2
|
-
import { detectAgents, findTranscripts } from "./adapters.js";
|
|
2
|
+
import { detectAgents, findTranscripts, installAgentHooks, removeOwnedAgentHooks } from "./adapters.js";
|
|
3
3
|
import { fail } from "./errors.js";
|
|
4
4
|
import { setupVault } from "./onboarding.js";
|
|
5
5
|
import { ArchiveQueue } from "./queue.js";
|
|
6
6
|
import { retentionSettings, setRetentionPolicy } from "./retention.js";
|
|
7
7
|
import { forgetRecoveryPhrase, rememberRecoveryPhrase, resolveRecoveryPhrase } from "./secrets.js";
|
|
8
|
-
import { daemonInvocation, installService, serviceStatus, servicePlan, serviceUnitEnvironment, uninstallService } from "./service.js";
|
|
8
|
+
import { daemonInvocation, installService, reconcileInstalledService, serviceStatus, servicePlan, serviceUnitEnvironment, uninstallService } from "./service.js";
|
|
9
9
|
import { readConfig } from "./vault.js";
|
|
10
10
|
import { liveness, readHeartbeat } from "./heartbeat.js";
|
|
11
|
+
import { uiRuntimeStatus } from "./ui-server.js";
|
|
12
|
+
import { readLocalSettings, writeLocalSettings } from "./machine-settings.js";
|
|
11
13
|
const DEFAULTS = { olderThanDays: 30, graceDays: 7 };
|
|
14
|
+
const policyCanReclaim = (policy) => policy === "archive-and-reclaim" || policy === "manual-approval";
|
|
15
|
+
/**
|
|
16
|
+
* Keep a non-reclaim retention policy and the machine that executes it in sync.
|
|
17
|
+
*
|
|
18
|
+
* Retention timing lives in the vault config, while unattended deletion is a
|
|
19
|
+
* separate, machine-local permission and an installed service also remembers
|
|
20
|
+
* its invocation. Changing only the policy is safe today, but leaves enough
|
|
21
|
+
* stale state for a later policy edit or older service generation to resume
|
|
22
|
+
* deletion without a new machine opt-in. Revoke that permission and rewrite an
|
|
23
|
+
* already-installed service receipt, preserving the schedule and every other
|
|
24
|
+
* local setting. A reclaim-capable policy deliberately grants nothing here.
|
|
25
|
+
*/
|
|
26
|
+
export async function reconcileMachineRetentionPolicy(dataDir, policy, options = {}) {
|
|
27
|
+
const local = await readLocalSettings(dataDir);
|
|
28
|
+
if (policyCanReclaim(policy)) {
|
|
29
|
+
return { reclaimPermissionRevoked: false, serviceInstalled: false, serviceReconciled: false };
|
|
30
|
+
}
|
|
31
|
+
const reclaimPermissionRevoked = local.reclaimEnabled;
|
|
32
|
+
if (reclaimPermissionRevoked) {
|
|
33
|
+
await writeLocalSettings(dataDir, { ...local, reclaimEnabled: false });
|
|
34
|
+
}
|
|
35
|
+
const invocation = daemonInvocation(dataDir, {
|
|
36
|
+
reclaim: false,
|
|
37
|
+
cliPath: options.cliPath,
|
|
38
|
+
intervalSeconds: local.schedule.intervalMinutes * 60,
|
|
39
|
+
});
|
|
40
|
+
const serviceOptions = {
|
|
41
|
+
dataDir,
|
|
42
|
+
executable: invocation.executable,
|
|
43
|
+
args: invocation.args,
|
|
44
|
+
home: options.home,
|
|
45
|
+
platform: options.platform,
|
|
46
|
+
exec: options.exec,
|
|
47
|
+
environment: serviceUnitEnvironment(),
|
|
48
|
+
};
|
|
49
|
+
const installed = await serviceStatus(serviceOptions);
|
|
50
|
+
if (!installed.installed) {
|
|
51
|
+
return { reclaimPermissionRevoked, serviceInstalled: false, serviceReconciled: false };
|
|
52
|
+
}
|
|
53
|
+
const serviceReconciled = await reconcileInstalledService(serviceOptions);
|
|
54
|
+
if (!serviceReconciled) {
|
|
55
|
+
fail("internal", "Automatic reclaim is off, but the installed Sealkeep service could not be updated. Run `sealkeep autopilot` to repair it before turning reclaim on again.");
|
|
56
|
+
}
|
|
57
|
+
return { reclaimPermissionRevoked, serviceInstalled: true, serviceReconciled };
|
|
58
|
+
}
|
|
12
59
|
/**
|
|
13
60
|
* Turns Sealkeep into something that runs itself.
|
|
14
61
|
*
|
|
15
|
-
* One call sets up the vault, teaches this machine to unlock it,
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
62
|
+
* One call sets up the vault, teaches this machine to unlock it, sweeps up the
|
|
63
|
+
* sessions already on disk, and installs a login service. Preservation, indexing,
|
|
64
|
+
* and uploads are automatic. Reclaim is a separate standing permission: it is off
|
|
65
|
+
* on a fresh machine and is enabled only by an explicit option or an already-saved
|
|
66
|
+
* machine choice. Even then, the ordinary remote, search, identity, age, grace and
|
|
67
|
+
* liveness gates remain mandatory.
|
|
20
68
|
*/
|
|
21
69
|
export async function enableAutopilot(dataDir, options = {}) {
|
|
22
70
|
const notes = [];
|
|
23
71
|
const remember = options.remember !== false;
|
|
24
|
-
const
|
|
25
|
-
|
|
72
|
+
const setup = await setupVault(dataDir, options.phrase, {
|
|
73
|
+
home: options.home,
|
|
74
|
+
mcpExec: options.mcpExec ?? options.exec
|
|
75
|
+
});
|
|
26
76
|
const config = await readConfig(dataDir);
|
|
77
|
+
const localSettings = await readLocalSettings(dataDir);
|
|
78
|
+
const reclaim = options.reclaim ?? localSettings.reclaimEnabled;
|
|
27
79
|
const phrase = setup.recoveryPhrase ?? (await resolveRecoveryPhrase(dataDir, config.vaultId, options.phrase, process.env, options.backend));
|
|
28
80
|
if (!phrase) {
|
|
29
81
|
fail("recovery_phrase_missing", "This vault already exists and its phrase is not on this machine. Re-run with --recovery-phrase so the service can unlock it.", { dataDir });
|
|
@@ -40,13 +92,45 @@ export async function enableAutopilot(dataDir, options = {}) {
|
|
|
40
92
|
else if (!remember) {
|
|
41
93
|
notes.push("The phrase was not stored. Put SEALKEEP_RECOVERY_PHRASE in the service environment yourself, or sessions will queue up unsealed.");
|
|
42
94
|
}
|
|
43
|
-
const
|
|
95
|
+
const currentRetention = retentionSettings(config);
|
|
96
|
+
const currentPolicyCanReclaim = policyCanReclaim(currentRetention.policy);
|
|
97
|
+
// Keep an existing policy (including manual approval) across an upgrade. A
|
|
98
|
+
// fresh vault has no saved policy, so its useful non-destructive mode is
|
|
99
|
+
// sync-only: archive/index/upload automatically while originals remain.
|
|
100
|
+
const policy = reclaim
|
|
101
|
+
? currentPolicyCanReclaim ? currentRetention.policy : "archive-and-reclaim"
|
|
102
|
+
: config.retention?.policy ?? "sync-only";
|
|
103
|
+
const desired = {
|
|
104
|
+
policy,
|
|
105
|
+
olderThanDays: options.olderThanDays ?? config.retention?.olderThanDays ?? DEFAULTS.olderThanDays,
|
|
106
|
+
graceDays: options.graceDays ?? config.retention?.graceDays ?? DEFAULTS.graceDays,
|
|
107
|
+
};
|
|
44
108
|
// A vault written before archive lifecycle existed has no deleteAfterDays; absent means never.
|
|
45
109
|
const stored = options.dryRun ? undefined : (await setRetentionPolicy(dataDir, desired)).retention;
|
|
46
110
|
const retention = retentionSettings(config, { ...desired, ...stored });
|
|
111
|
+
// The native service reads this setting on every pass. Persist the effective
|
|
112
|
+
// choice even when it is the fresh false default, so its behavior, generated
|
|
113
|
+
// unit, status JSON, and the Settings page all describe the same permission.
|
|
114
|
+
if (!options.dryRun) {
|
|
115
|
+
await writeLocalSettings(dataDir, { ...localSettings, reclaimEnabled: reclaim });
|
|
116
|
+
}
|
|
47
117
|
const installs = await detectAgents(options.home);
|
|
48
118
|
const detected = installs.filter((install) => install.detected);
|
|
49
|
-
const found = (await Promise.all(detected.map((install) => findTranscripts(install.agent, options.home
|
|
119
|
+
const found = (await Promise.all(detected.map((install) => findTranscripts(install.agent, options.home)))).flat();
|
|
120
|
+
// `autopilot` is the explicit one-time request for unattended behavior. A
|
|
121
|
+
// fresh CLI setup that installed only the watcher service used to preserve
|
|
122
|
+
// sessions but never inject their memory into the agent. Install both halves
|
|
123
|
+
// here; an existing hook is reconciled idempotently and custom hooks survive.
|
|
124
|
+
const hookOutcomes = options.dryRun
|
|
125
|
+
? []
|
|
126
|
+
: await installAgentHooks(detected.map((install) => install.agent), { dataDir, home: options.home });
|
|
127
|
+
const hooks = options.dryRun
|
|
128
|
+
? detected.map((install) => install.agent)
|
|
129
|
+
: hookOutcomes.filter((outcome) => outcome.installed).map((outcome) => outcome.agent);
|
|
130
|
+
for (const outcome of hookOutcomes) {
|
|
131
|
+
if (!outcome.installed && outcome.reason)
|
|
132
|
+
notes.push(`${outcome.agent} hooks were not installed: ${outcome.reason}`);
|
|
133
|
+
}
|
|
50
134
|
// What leaves this machine is decided here, before anything is read or sealed.
|
|
51
135
|
const { rules, source } = await resolveSyncRules(dataDir);
|
|
52
136
|
const { included, heldBack } = applySyncRules(found, rules);
|
|
@@ -68,10 +152,9 @@ export async function enableAutopilot(dataDir, options = {}) {
|
|
|
68
152
|
// telling it to run `storage configure` sends a paying customer to set up
|
|
69
153
|
// storage they are already paying us to run.
|
|
70
154
|
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// environment rather than inheriting the shell that installed it.
|
|
155
|
+
// This remains useful setup/status truth, but it does not decide whether the
|
|
156
|
+
// service is upload-capable. A target connected tomorrow must be picked up by
|
|
157
|
+
// the already-running daemon without reinstalling or restarting it.
|
|
75
158
|
let managedEmail = null;
|
|
76
159
|
if (!config.remoteStorage) {
|
|
77
160
|
try {
|
|
@@ -81,15 +164,17 @@ export async function enableAutopilot(dataDir, options = {}) {
|
|
|
81
164
|
catch { /* not signed in */ }
|
|
82
165
|
}
|
|
83
166
|
const hasRemoteTarget = Boolean(config.remoteStorage) || managedEmail !== null;
|
|
84
|
-
const invocation = daemonInvocation(dataDir, {
|
|
167
|
+
const invocation = daemonInvocation(dataDir, {
|
|
168
|
+
reclaim,
|
|
169
|
+
cliPath: options.cliPath,
|
|
170
|
+
intervalSeconds: localSettings.schedule.intervalMinutes * 60,
|
|
171
|
+
});
|
|
85
172
|
const serviceOptions = {
|
|
86
173
|
dataDir, executable: invocation.executable, args: invocation.args,
|
|
87
174
|
home: options.home, platform: options.platform, exec: options.exec,
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
// a byte — the one thing the user installed it to do.
|
|
92
|
-
environment: serviceUnitEnvironment(hasRemoteTarget ? { SEALKEEP_ENABLE_SIGNER: "1" } : {})
|
|
175
|
+
// Automatic service ownership is the upload opt-in. Targets and credentials
|
|
176
|
+
// still fail closed, but connecting them later requires no service rewrite.
|
|
177
|
+
environment: serviceUnitEnvironment()
|
|
93
178
|
};
|
|
94
179
|
const service = options.dryRun
|
|
95
180
|
? { kind: servicePlan(serviceOptions).kind, installed: false, path: servicePlan(serviceOptions).path, ranCommands: [], note: "dry run: nothing was installed" }
|
|
@@ -106,6 +191,7 @@ export async function enableAutopilot(dataDir, options = {}) {
|
|
|
106
191
|
retention,
|
|
107
192
|
service,
|
|
108
193
|
agents: detected.map((install) => install.agent),
|
|
194
|
+
hooks,
|
|
109
195
|
found: { sessions: found.length, bytes: found.reduce((total, candidate) => total + candidate.bytes, 0) },
|
|
110
196
|
queued,
|
|
111
197
|
reclaimEnabled: reclaim,
|
|
@@ -115,16 +201,32 @@ export async function enableAutopilot(dataDir, options = {}) {
|
|
|
115
201
|
}
|
|
116
202
|
export async function autopilotStatus(dataDir, options = {}) {
|
|
117
203
|
const config = await readConfig(dataDir);
|
|
204
|
+
let hasRemoteTarget = Boolean(config.remoteStorage) || Boolean(config.storageTargets?.length);
|
|
205
|
+
if (!hasRemoteTarget) {
|
|
206
|
+
try {
|
|
207
|
+
const { accountStatus } = await import("./cloud.js");
|
|
208
|
+
hasRemoteTarget = Boolean((await accountStatus(dataDir))?.email);
|
|
209
|
+
}
|
|
210
|
+
catch { /* not signed in to managed storage */ }
|
|
211
|
+
}
|
|
118
212
|
const invocation = daemonInvocation(dataDir, { cliPath: options.cliPath });
|
|
119
213
|
const service = await serviceStatus({ dataDir, executable: invocation.executable, args: invocation.args, home: options.home, platform: options.platform, exec: options.exec });
|
|
214
|
+
const [phraseAvailable, queue, ui, localSettings] = await Promise.all([
|
|
215
|
+
resolveRecoveryPhrase(dataDir, config.vaultId, undefined, process.env, options.backend).then((phrase) => phrase !== null),
|
|
216
|
+
new ArchiveQueue(dataDir).stats(),
|
|
217
|
+
uiRuntimeStatus(dataDir),
|
|
218
|
+
readLocalSettings(dataDir),
|
|
219
|
+
]);
|
|
120
220
|
return {
|
|
121
221
|
running: service.present,
|
|
122
222
|
live: liveness(await readHeartbeat(dataDir)),
|
|
123
223
|
service,
|
|
124
|
-
phraseAvailable
|
|
224
|
+
phraseAvailable,
|
|
225
|
+
reclaimEnabled: localSettings.reclaimEnabled,
|
|
125
226
|
retention: retentionSettings(config),
|
|
126
|
-
hasRemoteTarget
|
|
127
|
-
queue
|
|
227
|
+
hasRemoteTarget,
|
|
228
|
+
queue,
|
|
229
|
+
ui,
|
|
128
230
|
};
|
|
129
231
|
}
|
|
130
232
|
/**
|
|
@@ -135,6 +237,19 @@ export async function disableAutopilot(dataDir, options = {}) {
|
|
|
135
237
|
const config = await readConfig(dataDir);
|
|
136
238
|
const invocation = daemonInvocation(dataDir, { cliPath: options.cliPath });
|
|
137
239
|
const service = await uninstallService({ dataDir, executable: invocation.executable, args: invocation.args, home: options.home, platform: options.platform, exec: options.exec });
|
|
240
|
+
if (service.installed) {
|
|
241
|
+
// A failed native stop is not "off". Keep every dependency the still-live
|
|
242
|
+
// daemon needs, including its remembered phrase and hooks, so a retry is
|
|
243
|
+
// safe and the status remains actionable.
|
|
244
|
+
return {
|
|
245
|
+
service,
|
|
246
|
+
forgotPhrase: false,
|
|
247
|
+
retention: retentionSettings(config),
|
|
248
|
+
archivesKept: true,
|
|
249
|
+
hooksRemoved: [],
|
|
250
|
+
hookOutcomes: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
138
253
|
let forgot = false;
|
|
139
254
|
if (options.remember !== false) {
|
|
140
255
|
try {
|
|
@@ -144,5 +259,8 @@ export async function disableAutopilot(dataDir, options = {}) {
|
|
|
144
259
|
catch { /* nothing stored */ }
|
|
145
260
|
}
|
|
146
261
|
const retention = (await setRetentionPolicy(dataDir, { policy: "local-only" })).retention;
|
|
147
|
-
|
|
262
|
+
const localSettings = await readLocalSettings(dataDir);
|
|
263
|
+
await writeLocalSettings(dataDir, { ...localSettings, reclaimEnabled: false });
|
|
264
|
+
const hookOutcomes = await removeOwnedAgentHooks({ home: options.home });
|
|
265
|
+
return { service, forgotPhrase: forgot, retention, archivesKept: true, hooksRemoved: hookOutcomes.filter((outcome) => outcome.removed).map((outcome) => outcome.agent), hookOutcomes };
|
|
148
266
|
}
|