@staix/agent-mem 0.21.0 → 0.22.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/.claude-plugin/plugin.json +1 -1
- package/README.md +27 -1
- package/package.json +5 -2
- package/src/cli-agent-reads.ts +3 -1
- package/src/cli.ts +36 -4
- package/src/commands-table.ts +3 -0
- package/src/index.ts +6 -0
- package/src/memory/commands.ts +3 -2
- package/src/memory/generation.ts +6 -3
- package/src/memory/profile-lock-diagnostic.ts +69 -0
- package/src/memory/public-prune-activation.ts +4 -4
- package/src/memory/reader-lease.ts +3 -2
- package/src/memory/shard-sync.ts +1 -2
- package/src/memory/shard.ts +4 -0
- package/src/memory/sync-cadence.ts +2 -1
- package/src/memory/sync-placement-abandon.ts +142 -0
- package/src/memory/sync-placement-activation-locks.ts +28 -0
- package/src/memory/sync-placement-activation-path.ts +2 -0
- package/src/memory/sync-placement-activation-proof.ts +68 -0
- package/src/memory/sync-placement-activation-storage.ts +167 -0
- package/src/memory/sync-placement-activation.ts +185 -0
- package/src/memory/sync-placement-capture.ts +104 -23
- package/src/memory/sync-placement-effective-writers.ts +84 -0
- package/src/memory/sync-placement-guard.ts +18 -8
- package/src/memory/sync-placement-inspection.ts +5 -1
- package/src/memory/sync-placement-journal-storage.ts +51 -7
- package/src/memory/sync-placement-journal.ts +11 -4
- package/src/memory/sync-placement-prerequisites.ts +50 -0
- package/src/memory/sync-placement-transport.ts +17 -6
- package/src/memory/sync-placement-writer-history.ts +278 -0
- package/src/memory/sync-placement-writer-renewal.ts +179 -0
- package/src/memory/sync-placement-writer-revision-format.ts +122 -0
- package/src/memory/sync-placement-writer-roots.ts +167 -0
- package/src/memory/sync-placement-writers.ts +135 -0
- package/src/memory/sync.ts +1 -2
- package/src/setup-toml.ts +3 -2
- package/src/setup.ts +101 -14
- package/src/sync-placement-writer-context.ts +136 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Shared person-migration fences. No serialized value grants ownership. */
|
|
2
|
+
import { lstatSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { withProfileLock } from "./generation.ts";
|
|
5
|
+
import { acquireMutation, type MutationHandle } from "./mutation.ts";
|
|
6
|
+
import { generationSyncLock } from "./generation-sync.ts";
|
|
7
|
+
import { assertNoLiveReaders } from "./reader-lease.ts";
|
|
8
|
+
import { createPlacementActivationOwner } from "./sync-placement-activation-proof.ts";
|
|
9
|
+
export class PlacementInventoryFenceError extends Error{
|
|
10
|
+
constructor(readonly causeClass:"readers"|"busy"){super(`Sync placement migration is ${causeClass}; close cooperating readers/writers, then run amem migrate --sync-placement --dry-run before retrying.`);this.name="PlacementInventoryFenceError";}
|
|
11
|
+
}
|
|
12
|
+
function fail(cause:"readers"|"busy"):never{throw new PlacementInventoryFenceError(cause);}
|
|
13
|
+
export async function withPlacementActivationFences<T>(home:string,directories:readonly string[],work:(mutation:MutationHandle,owner:ReturnType<typeof createPlacementActivationOwner>)=>Promise<T>):Promise<T> {
|
|
14
|
+
return withProfileLock(home,async()=>{
|
|
15
|
+
const releases:(()=>void)[]=[];
|
|
16
|
+
try {
|
|
17
|
+
const transports=["shard-sync","shard-sync-placement"].map(name=>join(home,name)).filter(dir=>lstatSync(dir,{throwIfNoEntry:false})!==undefined);
|
|
18
|
+
const dirs=[...new Set([home,...directories,...transports])].sort();let mutation:MutationHandle|undefined;
|
|
19
|
+
for(const dir of dirs){
|
|
20
|
+
try{assertNoLiveReaders(dir,{includeShards:true});}catch{fail("readers");}
|
|
21
|
+
const handle=acquireMutation(dir);if(!handle)fail("busy");releases.push(handle);if(dir===home)mutation=handle;
|
|
22
|
+
}
|
|
23
|
+
if(!mutation)fail("busy");
|
|
24
|
+
for(const dir of [home,...transports]){const release=generationSyncLock(dir);if(!release)fail("busy");releases.push(release);}
|
|
25
|
+
const owner=createPlacementActivationOwner(home,mutation);return await work(mutation,owner);
|
|
26
|
+
}finally{for(const release of releases.reverse())release();}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Internal activation-owner proof. No serialized object or boolean grants lock bypass authority. */
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, type BigIntStats } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { canonicalJSON, hashJSON } from "./generation.ts";
|
|
5
|
+
import { assertMutationHandle, type MutationHandle } from "./mutation.ts";
|
|
6
|
+
import { processSignature, realPath } from "./platform.ts";
|
|
7
|
+
import type { PlacementActivationMarkerSnapshot } from "./sync-placement-capture.ts";
|
|
8
|
+
import { readPlacementActivationIntent, validatePlacementActivationIntent, placementActivationFinalMarkers, type PlacementActivationIntent } from "./sync-placement-activation-storage.ts";
|
|
9
|
+
|
|
10
|
+
declare const ownerBrand: unique symbol, captureBrand: unique symbol;
|
|
11
|
+
export interface PlacementActivationOwner { readonly [ownerBrand]: true }
|
|
12
|
+
export interface PlacementActivationCaptureProof { readonly [captureBrand]: true }
|
|
13
|
+
interface Owner { home: string; mutation: MutationHandle; root: BigIntStats; lock: string; lockStamp: string }
|
|
14
|
+
const owners = new WeakMap<PlacementActivationOwner, Owner>();
|
|
15
|
+
const captures = new WeakMap<PlacementActivationCaptureProof, { owner: PlacementActivationOwner; intent: PlacementActivationIntent }>();
|
|
16
|
+
const stamp = (s: BigIntStats) => [s.dev,s.ino,s.size,s.mtimeNs,s.ctimeNs].map(String).join(":");
|
|
17
|
+
function fail(): never { throw Error("Sync placement activation ownership or retained evidence changed; close cooperating writers and run amem migrate --sync-placement --resume after person inspection."); }
|
|
18
|
+
function lockBytes(home: string): { text: string; stamp: string } {
|
|
19
|
+
const file = join(home,"profile.lock"), before = lstatSync(file,{bigint:true});
|
|
20
|
+
if (!before.isFile() || before.isSymbolicLink() || before.uid !== BigInt(process.getuid?.() ?? -1) || (before.mode & 0o077n) !== 0n || before.nlink !== 1n || before.size < 1n || before.size > 4096n) fail();
|
|
21
|
+
const fd = openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
|
|
22
|
+
try {
|
|
23
|
+
const opened = fstatSync(fd,{bigint:true}); if (stamp(opened) !== stamp(before)) fail();
|
|
24
|
+
const out = Buffer.alloc(Number(opened.size)); let at = 0;
|
|
25
|
+
while (at < out.length) { const n = readSync(fd,out,at,out.length-at,at); if (!n) fail(); at += n; }
|
|
26
|
+
if (stamp(fstatSync(fd,{bigint:true})) !== stamp(opened) || stamp(lstatSync(file,{bigint:true})) !== stamp(opened)) fail();
|
|
27
|
+
return { text: new TextDecoder("utf-8",{fatal:true}).decode(out), stamp: stamp(opened) };
|
|
28
|
+
} finally { closeSync(fd); }
|
|
29
|
+
}
|
|
30
|
+
export function createPlacementActivationOwner(homeInput: string, mutation: MutationHandle): PlacementActivationOwner {
|
|
31
|
+
try {
|
|
32
|
+
const home = realPath(homeInput); assertMutationHandle(mutation,home);
|
|
33
|
+
const root = lstatSync(home,{bigint:true}), lock = lockBytes(home), value = JSON.parse(lock.text) as Record<string,unknown>;
|
|
34
|
+
if (!root.isDirectory() || root.isSymbolicLink() || root.uid !== BigInt(process.getuid?.() ?? -1) || (root.mode & 0o077n) !== 0n
|
|
35
|
+
|| Object.keys(value).sort().join() !== "pid,signature,token" || value.pid !== process.pid || value.signature !== processSignature(process.pid)
|
|
36
|
+
|| typeof value.signature !== "string" || !value.signature || typeof value.token !== "string" || !/^[a-f0-9]{32}$/.test(value.token)) fail();
|
|
37
|
+
const token = Object.freeze({}) as PlacementActivationOwner;
|
|
38
|
+
owners.set(token,{home,mutation,root,lock:lock.text,lockStamp:lock.stamp}); return token;
|
|
39
|
+
} catch { return fail(); }
|
|
40
|
+
}
|
|
41
|
+
export function assertPlacementActivationOwner(token: PlacementActivationOwner, home?: string): void {
|
|
42
|
+
try {
|
|
43
|
+
const owner = owners.get(token); if (!owner) fail(); assertMutationHandle(owner.mutation,owner.home);
|
|
44
|
+
if(home!==undefined&&realPath(home)!==owner.home)fail();
|
|
45
|
+
const root = lstatSync(owner.home,{bigint:true}), lock = lockBytes(owner.home);
|
|
46
|
+
if (!root.isDirectory() || root.isSymbolicLink() || root.dev !== owner.root.dev || root.ino !== owner.root.ino || root.uid !== owner.root.uid || (root.mode & 0o077n) !== 0n
|
|
47
|
+
|| lock.text !== owner.lock || lock.stamp !== owner.lockStamp) fail();
|
|
48
|
+
assertMutationHandle(owner.mutation,owner.home);
|
|
49
|
+
} catch { fail(); }
|
|
50
|
+
}
|
|
51
|
+
export function createPlacementActivationCaptureProof(owner: PlacementActivationOwner, input: PlacementActivationIntent): PlacementActivationCaptureProof {
|
|
52
|
+
assertPlacementActivationOwner(owner);
|
|
53
|
+
const state = owners.get(owner)!;
|
|
54
|
+
const intent = validatePlacementActivationIntent(input), actual = readPlacementActivationIntent(state.home);
|
|
55
|
+
if (!intent || !actual || canonicalJSON(intent) !== canonicalJSON(actual)
|
|
56
|
+
|| actual.home !== state.home || actual.epoch.homeHash !== hashJSON([state.home,String(state.root.dev),String(state.root.ino),String(state.root.uid)])) fail();
|
|
57
|
+
const token = Object.freeze({}) as PlacementActivationCaptureProof; captures.set(token,{owner,intent:actual}); return token;
|
|
58
|
+
}
|
|
59
|
+
export function activationSourceMarkers(token: PlacementActivationCaptureProof, home: string, markers: { profileText: string; shardsText: string; profileStamp: readonly string[]; shardsStamp: readonly string[] }): PlacementActivationMarkerSnapshot {
|
|
60
|
+
const scope = captures.get(token); if (!scope) fail(); assertPlacementActivationOwner(scope.owner);
|
|
61
|
+
const state = owners.get(scope.owner)!, actual = readPlacementActivationIntent(home), intent = scope.intent;
|
|
62
|
+
if (home !== state.home || !actual || canonicalJSON(actual) !== canonicalJSON(intent)) fail();
|
|
63
|
+
const { profileText, shardsText } = markers;
|
|
64
|
+
const final = placementActivationFinalMarkers(intent), oldProfile = profileText === intent.previousProfile, oldShards = shardsText === intent.previousShards;
|
|
65
|
+
if ((!oldProfile && profileText !== final.profile) || (!oldShards && shardsText !== final.shards) || !oldProfile && shardsText !== final.shards) fail();
|
|
66
|
+
if (oldProfile && canonicalJSON(markers.profileStamp) !== canonicalJSON(intent.profileStamp) || oldShards && canonicalJSON(markers.shardsStamp) !== canonicalJSON(intent.shardsStamp)) fail();
|
|
67
|
+
return { profileText:intent.previousProfile,shardsText:intent.previousShards,profileStamp:[...intent.profileStamp],shardsStamp:[...intent.shardsStamp] };
|
|
68
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Initial activation metadata only. Caller owns person authority, profile lock, writer inventory and source fences. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, readSync, unlinkSync, type BigIntStats } from "node:fs";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { types } from "node:util";
|
|
6
|
+
import { canonicalJSON, hashJSON } from "./generation.ts";
|
|
7
|
+
import { realPath } from "./platform.ts";
|
|
8
|
+
import { assertMutationHandle, type MutationHandle } from "./mutation.ts";
|
|
9
|
+
import { validatePlacementBaseV2, validatePlacementShards, validatePlacementMarkerTransition, PLACEMENT_FORMAT, PLACEMENT_SHARDS_SCHEMA } from "./sync-placement-format.ts";
|
|
10
|
+
import { validatePlacementPlan, type SyncPlacementActivationPlan } from "./sync-placement-plan.ts";
|
|
11
|
+
import { validatePlacementEpoch, placementEpochHash, SyncPlacementJournalError, type PlacementEpoch, type SyncPlacementJournalErrorCode } from "./sync-placement-journal-format.ts";
|
|
12
|
+
import { writeImmutableActivationMetadata } from "./public-prune-activation.ts";
|
|
13
|
+
import { PLACEMENT_ACTIVATION_INTENT } from "./sync-placement-activation-path.ts";
|
|
14
|
+
|
|
15
|
+
export const PLACEMENT_ACTIVATION_INTENT_SCHEMA = "agent-mem.sync-placement-activation/v1";
|
|
16
|
+
export const PLACEMENT_ACTIVATION_INTENT_FILE = PLACEMENT_ACTIVATION_INTENT;
|
|
17
|
+
const MAX_BYTES = 1024 * 1024, ROOTS_MAX = 256;
|
|
18
|
+
export type PlacementActivationMarkerStamp = [string, string, string, string, string];
|
|
19
|
+
export interface PlacementActivationIntent {
|
|
20
|
+
schema: typeof PLACEMENT_ACTIVATION_INTENT_SCHEMA; home: string; epoch: PlacementEpoch; plan: SyncPlacementActivationPlan; roots: string[];
|
|
21
|
+
previousProfile: string; previousShards: string; profileStamp: PlacementActivationMarkerStamp; shardsStamp: PlacementActivationMarkerStamp; floorHash: string;
|
|
22
|
+
inputHashes: {sourceHash:string;aliasesHash:string;policyHash:string}; seal: string;
|
|
23
|
+
}
|
|
24
|
+
export type PlacementActivationIntentBody = Omit<PlacementActivationIntent, "seal">;
|
|
25
|
+
export interface PlacementActivationStorageProof { mutation: MutationHandle; assertCurrent(): void }
|
|
26
|
+
function fail(code: SyncPlacementJournalErrorCode = "corrupt"): never { throw new SyncPlacementJournalError(code); }
|
|
27
|
+
const bytes = (value: unknown) => Buffer.from(canonicalJSON(value) + "\n");
|
|
28
|
+
const digest = (value: Uint8Array) => createHash("sha256").update(value).digest("hex");
|
|
29
|
+
const hex = (value: unknown, length = 64): value is string => typeof value === "string" && value.length === length && /^[a-f0-9]+$/.test(value);
|
|
30
|
+
function object(value: unknown, keys: readonly string[]): Record<string, unknown> {
|
|
31
|
+
if (!value || typeof value !== "object" || types.isProxy(value) || ![null, Object.prototype].includes(Object.getPrototypeOf(value)) || Object.getOwnPropertySymbols(value).length) fail("invalid");
|
|
32
|
+
const fields = Object.getOwnPropertyDescriptors(value); if (Object.keys(fields).length !== keys.length) fail("invalid");
|
|
33
|
+
const copied: Record<string, unknown> = Object.create(null);
|
|
34
|
+
for (const key of keys) { const field = fields[key]; if (!field || !("value" in field) || !field.enumerable) fail("invalid"); copied[key] = field.value; }
|
|
35
|
+
return copied;
|
|
36
|
+
}
|
|
37
|
+
function array(value: unknown, maximum: number): unknown[] {
|
|
38
|
+
if (!value || typeof value !== "object" || types.isProxy(value) || !Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || Object.getOwnPropertySymbols(value).length) fail("invalid");
|
|
39
|
+
const length = Object.getOwnPropertyDescriptor(value, "length")?.value;
|
|
40
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > maximum) fail("bound");
|
|
41
|
+
if (Object.getOwnPropertyNames(value).length !== length + 1) fail("invalid");
|
|
42
|
+
const output: unknown[] = [];
|
|
43
|
+
for (let i = 0; i < length; i++) { const field = Object.getOwnPropertyDescriptor(value, String(i)); if (!field || !("value" in field) || !field.enumerable) fail("invalid"); output.push(field.value); }
|
|
44
|
+
return output;
|
|
45
|
+
}
|
|
46
|
+
function path(value: unknown): string {
|
|
47
|
+
if (typeof value !== "string" || !isAbsolute(value) || Buffer.byteLength(value) > 4096 || value.includes("\0") || resolve(value) !== value) fail("invalid"); return value;
|
|
48
|
+
}
|
|
49
|
+
function markerStamp(value: unknown, text: string): PlacementActivationMarkerStamp {
|
|
50
|
+
const fields = array(value, 5);
|
|
51
|
+
if (fields.length !== 5 || fields.some((value, index) => typeof value !== "string" || value.length > 32 || !/^-?(?:0|[1-9][0-9]*)$/.test(value) || /[\r\n]/.test(value) || index < 3 && value.startsWith("-"))) fail("invalid");
|
|
52
|
+
if (fields[2] !== String(Buffer.byteLength(text))) fail("invalid"); return fields as PlacementActivationMarkerStamp;
|
|
53
|
+
}
|
|
54
|
+
function baseMarkers(profile: unknown, shards: unknown) {
|
|
55
|
+
if (typeof profile !== "string" || typeof shards !== "string" || Buffer.byteLength(profile) > 262144 || Buffer.byteLength(shards) > 262144) fail("bound");
|
|
56
|
+
// Preserve original UTF-8 text/stamps, including whitespace/BOM; only the containing intent and final v3 pair
|
|
57
|
+
// are canonical. Match the existing fatal UTF-8 marker decoder's BOM behavior without rewriting the source.
|
|
58
|
+
const decode = (text: string) => JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(text)));
|
|
59
|
+
return validatePlacementBaseV2(decode(profile), decode(shards));
|
|
60
|
+
}
|
|
61
|
+
function validate(value: unknown, sealed: boolean): PlacementActivationIntent {
|
|
62
|
+
const v = object(value, ["schema", "home", "epoch", "plan", "roots", "previousProfile", "previousShards", "profileStamp", "shardsStamp", "floorHash", "inputHashes", ...(sealed ? ["seal"] : [])]);
|
|
63
|
+
if (v.schema !== PLACEMENT_ACTIVATION_INTENT_SCHEMA || !hex(v.floorHash) || sealed && !hex(v.seal)) fail("invalid");
|
|
64
|
+
const home = path(v.home), epoch = validatePlacementEpoch(v.epoch), plan = validatePlacementPlan(v.plan), base = baseMarkers(v.previousProfile, v.previousShards);
|
|
65
|
+
const inputs=object(v.inputHashes,["sourceHash","aliasesHash","policyHash"]);
|
|
66
|
+
if(!hex(inputs.sourceHash)||!hex(inputs.aliasesHash)||!hex(inputs.policyHash))fail("invalid");
|
|
67
|
+
const inputHashes={sourceHash:inputs.sourceHash,aliasesHash:inputs.aliasesHash,policyHash:inputs.policyHash};
|
|
68
|
+
if(hashJSON({...base,homeHash:epoch.homeHash,...inputHashes,realms:plan.realms,writers:plan.writers,legacySyncQuiesced:true})!==plan.contextHash)fail("invalid");
|
|
69
|
+
const roots = array(v.roots, ROOTS_MAX).map(path); if (new Set(roots).size !== roots.length || canonicalJSON([...roots].sort()) !== canonicalJSON(roots)) fail("invalid");
|
|
70
|
+
if (epoch.contextHash !== plan.contextHash || epoch.generation !== plan.generation || base.profile.generation !== epoch.generation || placementEpochHash(epoch) !== plan.targetProfile.placementHead
|
|
71
|
+
|| hashJSON(base.profile) !== plan.previousProfileHash || hashJSON(base.shards) !== plan.previousShardsHash) fail("invalid");
|
|
72
|
+
const final = validatePlacementShards({ ...base.shards, schema: PLACEMENT_SHARDS_SCHEMA, selectorRevision: hashJSON(plan.targetProfile), publicationFormat: PLACEMENT_FORMAT, placementHead: plan.targetProfile.placementHead }, plan.targetProfile);
|
|
73
|
+
validatePlacementMarkerTransition(base.profile, base.shards, plan.targetProfile, final); if (hashJSON(final) !== plan.targetShardsHash) fail("invalid");
|
|
74
|
+
const body: PlacementActivationIntentBody = { schema: PLACEMENT_ACTIVATION_INTENT_SCHEMA, home, epoch, plan, roots, previousProfile: v.previousProfile as string, previousShards: v.previousShards as string,
|
|
75
|
+
profileStamp: markerStamp(v.profileStamp, v.previousProfile as string), shardsStamp: markerStamp(v.shardsStamp, v.previousShards as string), floorHash: v.floorHash,inputHashes };
|
|
76
|
+
const intent = { ...body, seal: hashJSON(body) }; if (sealed && intent.seal !== v.seal) fail("corrupt"); if (bytes(intent).length > MAX_BYTES) fail("bound"); return intent;
|
|
77
|
+
}
|
|
78
|
+
function safe<T>(work: () => T): T {
|
|
79
|
+
try { return work(); }
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error && typeof error === "object" && !types.isProxy(error) && error instanceof SyncPlacementJournalError) {
|
|
82
|
+
const field = Object.getOwnPropertyDescriptor(error, "code");
|
|
83
|
+
if (field && "value" in field && ["invalid", "corrupt", "bound", "conflict", "pending", "changed", "authorization", "policy", "local", "busy"].includes(field.value)) fail(field.value);
|
|
84
|
+
}
|
|
85
|
+
return fail();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function sealPlacementActivationIntent(body: PlacementActivationIntentBody): PlacementActivationIntent { return safe(() => validate(body, false)); }
|
|
89
|
+
/** Detached data validation uses intent bounds, never policy-payload string limits. */
|
|
90
|
+
export function validatePlacementActivationIntent(value: unknown): PlacementActivationIntent { return safe(() => validate(value, true)); }
|
|
91
|
+
export function placementActivationFinalMarkers(value: PlacementActivationIntent): { profile: string; shards: string } {
|
|
92
|
+
return safe(() => {
|
|
93
|
+
const intent = validate(value, true), old = baseMarkers(intent.previousProfile, intent.previousShards);
|
|
94
|
+
const shards = validatePlacementShards({ ...old.shards, schema: PLACEMENT_SHARDS_SCHEMA, selectorRevision: hashJSON(intent.plan.targetProfile), publicationFormat: PLACEMENT_FORMAT, placementHead: intent.plan.targetProfile.placementHead }, intent.plan.targetProfile);
|
|
95
|
+
return { profile: canonicalJSON(intent.plan.targetProfile) + "\n", shards: canonicalJSON(shards) + "\n" };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function owned(stat: BigIntStats, directory = false, links: readonly bigint[] = [1n]): void {
|
|
99
|
+
if (stat.isSymbolicLink() || stat.uid !== BigInt(process.getuid?.() ?? -1) || (directory ? !stat.isDirectory() || (stat.mode & 0o077n) !== 0n : !stat.isFile() || (stat.mode & 0o777n) !== 0o600n || !links.includes(stat.nlink))) fail();
|
|
100
|
+
}
|
|
101
|
+
function same(a: BigIntStats, b: BigIntStats): boolean { return a.dev === b.dev && a.ino === b.ino && a.uid === b.uid && a.mode === b.mode && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs; }
|
|
102
|
+
function root(home: string): { home: string; stat: BigIntStats; file: string } {
|
|
103
|
+
const stat = lstatSync(home, { bigint: true }); owned(stat, true); const canonical = realPath(home), current = lstatSync(canonical, { bigint: true }); owned(current, true);
|
|
104
|
+
if (stat.dev !== current.dev || stat.ino !== current.ino) fail("changed"); return { home: canonical, stat, file: join(canonical, PLACEMENT_ACTIVATION_INTENT_FILE) };
|
|
105
|
+
}
|
|
106
|
+
function bindHome(intent: PlacementActivationIntent, current: ReturnType<typeof root>): void {
|
|
107
|
+
if (intent.home !== current.home || intent.epoch.homeHash !== hashJSON([current.home, String(current.stat.dev), String(current.stat.ino), String(current.stat.uid)])) fail("changed");
|
|
108
|
+
}
|
|
109
|
+
interface Read { intent: PlacementActivationIntent; text: string; stat: BigIntStats }
|
|
110
|
+
function read(current: ReturnType<typeof root>): Read | undefined {
|
|
111
|
+
const before = lstatSync(current.file, { bigint: true, throwIfNoEntry: false }); if (!before) return undefined;
|
|
112
|
+
owned(before, false, [1n, 2n]); if (before.size < 1n || before.size > BigInt(MAX_BYTES)) fail("bound");
|
|
113
|
+
const fd = openSync(current.file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
114
|
+
try {
|
|
115
|
+
const opened = fstatSync(fd, { bigint: true }); owned(opened, false, [1n, 2n]); if (!same(before, opened)) fail("changed");
|
|
116
|
+
const data = Buffer.alloc(Number(opened.size) + 1); let at = 0;
|
|
117
|
+
while (at < data.length) { const n = readSync(fd, data, at, data.length - at, at); if (!n) break; at += n; }
|
|
118
|
+
if (at !== Number(opened.size) || !same(opened, fstatSync(fd, { bigint: true })) || !same(opened, lstatSync(current.file, { bigint: true }))) fail("changed");
|
|
119
|
+
const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(data.subarray(0, at)), intent = validate(JSON.parse(text), true);
|
|
120
|
+
if (canonicalJSON(intent) + "\n" !== text) fail(); bindHome(intent, current);
|
|
121
|
+
if (opened.nlink === 2n) {
|
|
122
|
+
const companion = lstatSync(current.file + ".pending-" + digest(Buffer.from(text)), { bigint: true }); owned(companion, false, [2n]);
|
|
123
|
+
if (!same(opened, companion) || !same(opened, lstatSync(current.file, { bigint: true }))) fail("changed");
|
|
124
|
+
}
|
|
125
|
+
const finalRoot = root(current.home); if (finalRoot.stat.dev !== current.stat.dev || finalRoot.stat.ino !== current.stat.ino) fail("changed");
|
|
126
|
+
return { intent, text, stat: opened };
|
|
127
|
+
} finally { closeSync(fd); }
|
|
128
|
+
}
|
|
129
|
+
export function readPlacementActivationIntent(home: string): PlacementActivationIntent | undefined { return safe(() => read(root(home))?.intent); }
|
|
130
|
+
function proof(current: ReturnType<typeof root>, options: PlacementActivationStorageProof): void {
|
|
131
|
+
try { assertMutationHandle(options.mutation, current.home); } catch { fail("busy"); }
|
|
132
|
+
try { options.assertCurrent(); } catch { fail("changed"); }
|
|
133
|
+
try { assertMutationHandle(options.mutation, current.home); } catch { fail("busy"); }
|
|
134
|
+
const now = root(current.home); if (now.stat.dev !== current.stat.dev || now.stat.ino !== current.stat.ino) fail("changed");
|
|
135
|
+
}
|
|
136
|
+
function syncHome(current: ReturnType<typeof root>, options: PlacementActivationStorageProof): void {
|
|
137
|
+
proof(current, options); const fd = openSync(current.home, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
138
|
+
try { const stat = fstatSync(fd, { bigint: true }); owned(stat, true); if (stat.dev !== current.stat.dev || stat.ino !== current.stat.ino) fail("changed"); proof(current, options); fsyncSync(fd); }
|
|
139
|
+
finally { closeSync(fd); } proof(current, options);
|
|
140
|
+
}
|
|
141
|
+
export function writePlacementActivationIntent(home: string, value: PlacementActivationIntent, options: PlacementActivationStorageProof): void {
|
|
142
|
+
safe(() => {
|
|
143
|
+
const current = root(home), intent = validate(value, true); bindHome(intent, current); const expected = canonicalJSON(intent) + "\n";
|
|
144
|
+
proof(current, options); const prior = read(current); if (prior && prior.text !== expected) fail("conflict");
|
|
145
|
+
proof(current, options);
|
|
146
|
+
// The shared helper intentionally keeps its old algorithm; bound its unregistered staging read here first.
|
|
147
|
+
const temp = lstatSync(current.file + ".pending-" + digest(Buffer.from(expected)), { bigint: true, throwIfNoEntry: false });
|
|
148
|
+
if (temp) { owned(temp, false, prior?.stat.nlink === 2n ? [2n] : [1n]); if (temp.size > BigInt(MAX_BYTES)) fail("bound"); }
|
|
149
|
+
writeImmutableActivationMetadata(current.file, expected); proof(current, options);
|
|
150
|
+
const installed = read(current); if (!installed || installed.stat.nlink !== 1n || installed.text !== expected) fail("changed");
|
|
151
|
+
const fd = openSync(current.file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
152
|
+
try { if (!same(installed.stat, fstatSync(fd, { bigint: true }))) fail("changed"); proof(current, options); if (!same(installed.stat, lstatSync(current.file, { bigint: true }))) fail("changed"); fsyncSync(fd); }
|
|
153
|
+
finally { closeSync(fd); } syncHome(current, options); proof(current, options);
|
|
154
|
+
if (read(current)?.text !== expected) fail("changed");
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
export function clearPlacementActivationIntent(home: string, value: PlacementActivationIntent, options: PlacementActivationStorageProof): void {
|
|
158
|
+
safe(() => {
|
|
159
|
+
const current = root(home), intent = validate(value, true); bindHome(intent, current); proof(current, options);
|
|
160
|
+
const prior = read(current);
|
|
161
|
+
if (prior) {
|
|
162
|
+
if (prior.text !== canonicalJSON(intent) + "\n") fail("conflict"); if (prior.stat.nlink !== 1n) fail("pending");
|
|
163
|
+
proof(current, options); if (!same(prior.stat, lstatSync(current.file, { bigint: true }))) fail("changed"); unlinkSync(current.file);
|
|
164
|
+
}
|
|
165
|
+
syncHome(current, options); proof(current, options); if (lstatSync(current.file, { throwIfNoEntry: false })) fail("changed");
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/** Person-only home-wide initial activation. No automatic migration, renewal or transport. */
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { types } from "node:util";
|
|
6
|
+
import { canonicalJSON, hashJSON, writeProfileJSON, fsyncDirectory } from "./generation.ts";
|
|
7
|
+
import { assertMutationHandle, type MutationHandle } from "./mutation.ts";
|
|
8
|
+
import { withPlacementActivationFences, PlacementInventoryFenceError } from "./sync-placement-activation-locks.ts";
|
|
9
|
+
import { realPath } from "./platform.ts";
|
|
10
|
+
import { planSyncPlacementActivation, validateSyncPlacementActivation, SyncPlacementPlanError } from "./sync-placement-plan.ts";
|
|
11
|
+
import { PLACEMENT_EPOCH_SCHEMA, placementEpochHash, SyncPlacementJournalError, type PlacementEpoch } from "./sync-placement-journal-format.ts";
|
|
12
|
+
import { preparePlacementEpoch, ensurePlacementFence } from "./sync-placement-journal-storage.ts";
|
|
13
|
+
import { readPlacementRuntimeMarkers } from "./sync-placement-guard.ts";
|
|
14
|
+
import { readActivePlacementBinding } from "./sync-placement-active.ts";
|
|
15
|
+
import { PLACEMENT_FORMAT } from "./sync-placement-format.ts";
|
|
16
|
+
import { captureSyncPlacementActivationContext, previewSyncPlacementSources, type PlacementSourcePreview } from "./sync-placement-capture.ts";
|
|
17
|
+
import { createPlacementActivationOwner, assertPlacementActivationOwner, createPlacementActivationCaptureProof } from "./sync-placement-activation-proof.ts";
|
|
18
|
+
import { sealPlacementActivationIntent, readPlacementActivationIntent, writePlacementActivationIntent, clearPlacementActivationIntent, placementActivationFinalMarkers, PLACEMENT_ACTIVATION_INTENT_SCHEMA, type PlacementActivationIntent } from "./sync-placement-activation-storage.ts";
|
|
19
|
+
import { writeSyncPlacementWriterRoots, readSyncPlacementWriterRoots, planSyncPlacementWriterRoots, SYNC_PLACEMENT_WRITER_ROOTS_FILE } from "./sync-placement-writer-roots.ts";
|
|
20
|
+
import { SyncPlacementWriterError } from "./sync-placement-writers.ts";
|
|
21
|
+
import { requirePlacementActivationPrerequisites, requirePlacementHomeFloor } from "./sync-placement-prerequisites.ts";
|
|
22
|
+
import { outboundPolicyEvaluator, type SyncPolicyCategory } from "./sync-screen.ts";
|
|
23
|
+
import { initialPlacementWriterContext, PlacementWriterContextError, type PlacementWriterContext } from "../sync-placement-writer-context.ts";
|
|
24
|
+
import { abandonPlacementActivation, PlacementActivationAbandonError } from "./sync-placement-abandon.ts";
|
|
25
|
+
import { renewSyncPlacementWriters } from "./sync-placement-writer-renewal.ts";
|
|
26
|
+
import type { WriterHistoryPhase } from "./sync-placement-writer-history.ts";
|
|
27
|
+
|
|
28
|
+
export type PlacementActivationStage = "after-intent"|"after-epoch"|"after-root-map"|"after-shards"|"after-profile"|"after-fence"|"after-remove";
|
|
29
|
+
export interface PlacementActivationOptions {
|
|
30
|
+
memoryDir: string; mode: "dry-run"|"apply"|"resume"|"abandon"; writerRoots: readonly string[]; ownRoot: string;
|
|
31
|
+
authorize(binding: Readonly<{seal:string;generation:string;homeHash:string}>): boolean;
|
|
32
|
+
checkpoint?(stage: PlacementActivationStage): void|Promise<void>;
|
|
33
|
+
/** Internal renewal fault seam; the CLI never supplies it. */
|
|
34
|
+
renewalCheckpoint?(stage:WriterHistoryPhase):void;
|
|
35
|
+
/** Internal fault-test observer, never supplied by CLI options. */
|
|
36
|
+
testOnlyFailure?(error: unknown): void;
|
|
37
|
+
}
|
|
38
|
+
export class PlacementActivationError extends Error {
|
|
39
|
+
constructor(readonly causeClass: "readers"|"busy"|"changed"|"metadata"|"authorization"|"pending"|"bound") {
|
|
40
|
+
super(`Sync placement activation failed (${causeClass}); close cooperating writers and run amem migrate --sync-placement --dry-run before retrying apply. If an activation intent is retained, use --resume, or --abandon only while both markers are still the original v2 bytes. Once a v3 marker exists, resume is forward-only; never restore v2 markers.`);
|
|
41
|
+
this.name="PlacementActivationError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class PlacementActivationInputError extends Error {
|
|
45
|
+
constructor(readonly inputClass:"policy"|"source"|"aliases"|"writer root"|"receipt"|"floor"){
|
|
46
|
+
super(`Sync placement activation frozen ${inputClass} input changed or is unavailable; restore that input and run amem migrate --sync-placement --resume. If both markers still contain their original v2 bytes, the person may run amem migrate --sync-placement --abandon, then review a fresh --dry-run and --apply. Once any v3 marker exists, only forward resume is supported.`);
|
|
47
|
+
this.name="PlacementActivationInputError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function writerInputFailure(error:unknown):never {
|
|
51
|
+
const category=error&&typeof error==="object"&&!types.isProxy(error)&&error instanceof PlacementWriterContextError?Object.getOwnPropertyDescriptor(error,"category")?.value:undefined;
|
|
52
|
+
throw new PlacementActivationInputError(category==="setup receipt"?"receipt":"writer root");
|
|
53
|
+
}
|
|
54
|
+
function fail(cause: PlacementActivationError["causeClass"]): never { throw new PlacementActivationError(cause); }
|
|
55
|
+
function rethrowActivationFailure(error:unknown):never {
|
|
56
|
+
if(error&&typeof error==="object"&&!types.isProxy(error)){
|
|
57
|
+
if(error instanceof PlacementInventoryFenceError)fail(error.causeClass);
|
|
58
|
+
if(error instanceof PlacementWriterContextError||error instanceof PlacementActivationError||error instanceof PlacementActivationInputError||error instanceof PlacementActivationAbandonError)throw error;
|
|
59
|
+
if(error instanceof SyncPlacementJournalError||error instanceof SyncPlacementPlanError||error instanceof SyncPlacementWriterError){
|
|
60
|
+
const code=Object.getOwnPropertyDescriptor(error,"code");
|
|
61
|
+
if(code&&"value" in code){if(code.value==="bound")fail("bound");if(code.value==="busy")fail("busy");if(code.value==="changed")fail("changed");if(code.value==="authorization")fail("authorization");}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
fail("metadata");
|
|
65
|
+
}
|
|
66
|
+
function counts(source: PlacementSourcePreview) {
|
|
67
|
+
const evaluate = outboundPolicyEvaluator(source.home), categories: Partial<Record<SyncPolicyCategory,number>> = {};
|
|
68
|
+
let eligibleOriginals=0,declined=0,intrinsicLocal=0;
|
|
69
|
+
for (const row of [...source.rows.values(),...source.specialRows.values()]) {
|
|
70
|
+
if (row.intrinsicLocal || row.sourcePlacement!=="synced") { intrinsicLocal++; continue; }
|
|
71
|
+
const verdict=evaluate(row.row as unknown as Record<string,unknown>);
|
|
72
|
+
if (verdict.ok) eligibleOriginals++; else { declined++;categories[verdict.category]=(categories[verdict.category]??0)+1; }
|
|
73
|
+
}
|
|
74
|
+
source.assertSourcesUnchanged();
|
|
75
|
+
return {selected:source.rows.size+source.specialRows.size,policyPermittedOriginals:eligibleOriginals,declined,intrinsicLocal,categories,dependencyDecisions:"deferred-to-sync" as const};
|
|
76
|
+
}
|
|
77
|
+
function committed(home:string,intent:Pick<PlacementActivationIntent,"epoch"|"plan">) {
|
|
78
|
+
const markers=readPlacementRuntimeMarkers(home);
|
|
79
|
+
if ((markers.profile as {schema?:string})?.schema!=="agent-mem.profile/v3"||(markers.shards as {schema?:string})?.schema!=="agent-mem.shards/v3") return false;
|
|
80
|
+
const binding=readActivePlacementBinding(home,markers.profile,markers.shards);
|
|
81
|
+
return binding?.profile.placementHead===placementEpochHash(intent.epoch)&&binding.activation.seal===intent.plan.seal;
|
|
82
|
+
}
|
|
83
|
+
function syncFile(file:string) {
|
|
84
|
+
const before=lstatSync(file,{bigint:true});
|
|
85
|
+
if (!before.isFile()||before.isSymbolicLink()||before.nlink!==1n||before.uid!==BigInt(process.getuid?.()??-1)||(before.mode&0o077n)!==0n) fail("metadata");
|
|
86
|
+
const fd=openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
|
|
87
|
+
try { const actual=fstatSync(fd,{bigint:true}); if(actual.dev!==before.dev||actual.ino!==before.ino||actual.size!==before.size)fail("changed");fsyncSync(fd); }
|
|
88
|
+
finally{closeSync(fd);}fsyncDirectory(join(file,".."));
|
|
89
|
+
}
|
|
90
|
+
function authorize(options:PlacementActivationOptions,intent:Pick<PlacementActivationIntent,"epoch"|"plan">) {
|
|
91
|
+
let okay=false;try{okay=options.authorize(Object.freeze({seal:intent.plan.seal,generation:intent.epoch.generation,homeHash:intent.epoch.homeHash}))===true;}catch{fail("authorization");}
|
|
92
|
+
if(!okay)fail("authorization");
|
|
93
|
+
}
|
|
94
|
+
async function finishCommittedActivation(home:string,binding:Pick<PlacementActivationIntent,"epoch"|"plan">,options:PlacementActivationOptions,
|
|
95
|
+
mutation:MutationHandle,owner:ReturnType<typeof createPlacementActivationOwner>,intent?:PlacementActivationIntent,report?:ReturnType<typeof counts>) {
|
|
96
|
+
const epochHash=placementEpochHash(binding.epoch);
|
|
97
|
+
const check=()=>{assertPlacementActivationOwner(owner);requirePlacementHomeFloor(home);if(!committed(home,binding))fail("changed");readSyncPlacementWriterRoots(home);assertMutationHandle(mutation,home);};
|
|
98
|
+
authorize(options,binding);check();
|
|
99
|
+
for(const name of ["profile.json","shards.json",SYNC_PLACEMENT_WRITER_ROOTS_FILE,join("sync-placement","epochs",epochHash,"epoch.json"),join("sync-placement","epochs",epochHash,"activation.json")]){check();syncFile(join(home,name));}
|
|
100
|
+
ensurePlacementFence(home,epochHash,{mutation,assertCurrent:check});check();
|
|
101
|
+
if(intent)clearPlacementActivationIntent(home,intent,{mutation,assertCurrent:check});else fsyncDirectory(home);
|
|
102
|
+
await options.checkpoint?.("after-remove");check();
|
|
103
|
+
return{mode:options.mode,active:true,forwardOnly:true,epochHash,generation:binding.epoch.generation,writers:binding.plan.writers.length,source:report,resumed:options.mode==="resume",alreadyCommitted:!intent,writerProof:"not-claimed-during-committed-cleanup" as const};
|
|
104
|
+
}
|
|
105
|
+
export async function activateSyncPlacement(options:PlacementActivationOptions) {
|
|
106
|
+
if(!["dry-run","apply","resume","abandon"].includes(options.mode)||typeof options.authorize!=="function")fail("authorization");
|
|
107
|
+
let home:string;
|
|
108
|
+
try{home=realPath(options.memoryDir);}catch{throw Error("Sync placement memory home is unavailable; select an existing private home and run amem migrate --sync-placement --dry-run.");}
|
|
109
|
+
const pending=readPlacementActivationIntent(home);
|
|
110
|
+
if(!pending&&requirePlacementActivationPrerequisites(home).active){
|
|
111
|
+
const renewed=await renewSyncPlacementWriters(options);if(renewed)return renewed;
|
|
112
|
+
}
|
|
113
|
+
if(options.mode==="abandon"){
|
|
114
|
+
if(!pending)throw Error("No initial sync placement intent is retained; use amem migrate --sync-placement --dry-run to inspect the home. Active v3 placement cannot be abandoned or downgraded.");
|
|
115
|
+
const dirs=pending.plan.realms.map(realm=>realm.scope==="user"?join(home,"user"):join(home,"projects",realm.keyHash.slice(0,16)));
|
|
116
|
+
return withPlacementActivationFences(home,dirs,async(mutation,owner)=>{
|
|
117
|
+
authorize(options,pending);
|
|
118
|
+
abandonPlacementActivation(home,pending,{mutation,assertCurrent:()=>assertPlacementActivationOwner(owner)});
|
|
119
|
+
return{mode:options.mode,active:false,abandoned:true,forwardOnly:true,nextStep:"amem migrate --sync-placement --dry-run"};
|
|
120
|
+
}).catch(error=>rethrowActivationFailure(error));
|
|
121
|
+
}
|
|
122
|
+
const initialBase=pending?undefined:requirePlacementActivationPrerequisites(home);
|
|
123
|
+
let initialFloorHash:string;
|
|
124
|
+
try{initialFloorHash=pending?requirePlacementHomeFloor(home):initialBase!.floorHash;}catch(error){if(pending)throw new PlacementActivationInputError("floor");throw error;}
|
|
125
|
+
if(options.mode==="resume"&&!pending){
|
|
126
|
+
if(!initialBase?.active)throw Error("No retained sync placement activation exists; run amem migrate --sync-placement --dry-run to review an initial activation.");
|
|
127
|
+
const active=initialBase.active,dirs=active.activation.realms.map(realm=>realm.scope==="user"?join(home,"user"):join(home,"projects",realm.keyHash.slice(0,16)));
|
|
128
|
+
return withPlacementActivationFences(home,dirs,(mutation,owner)=>finishCommittedActivation(home,{epoch:active.epoch,plan:active.activation},options,mutation,owner)).catch(error=>rethrowActivationFailure(error));
|
|
129
|
+
}
|
|
130
|
+
if(pending&&options.mode!=="resume")throw Error("Sync placement activation is retained; run amem migrate --sync-placement --resume, or person --abandon while both markers retain the original v2 bytes, before reviewing a new --dry-run.");
|
|
131
|
+
const source=pending?undefined:previewSyncPlacementSources(home);
|
|
132
|
+
const writer=pending?undefined:initialPlacementWriterContext(home,options.writerRoots,options.ownRoot);
|
|
133
|
+
const report=source?counts(source):undefined;
|
|
134
|
+
if(options.mode==="dry-run")return{mode:options.mode,active:false,forwardOnly:true,quiescence:"deferred-to-apply",realms:source!.base.realms.length,writers:writer!.measurement.roots,source:report};
|
|
135
|
+
if(!pending&&!options.writerRoots.length)throw Error("Initial sync placement apply requires an explicit complete --writer-root inventory; run amem migrate --sync-placement --dry-run, then apply the reviewed roots.");
|
|
136
|
+
const directories=pending?pending.plan.realms.map(realm=>realm.scope==="user"?join(home,"user"):join(home,"projects",realm.keyHash.slice(0,16))):source!.directories;
|
|
137
|
+
try{return await withPlacementActivationFences(home,directories,async(mutation,owner)=>{
|
|
138
|
+
let intent=pending;
|
|
139
|
+
const initialCheck=()=>{assertPlacementActivationOwner(owner);source!.assertSourcesUnchanged();writer!.attest();if(requirePlacementHomeFloor(home)!==initialFloorHash)fail("changed");};
|
|
140
|
+
if(!intent){
|
|
141
|
+
initialCheck();const facts=writer!.attest(),context={...source!.base,writers:facts.writers,legacySyncQuiesced:true as const};
|
|
142
|
+
const normalized=planSyncPlacementActivation(context,"0".repeat(64));context.writers=normalized.writers;context.realms=normalized.realms;
|
|
143
|
+
const epoch:PlacementEpoch={schema:PLACEMENT_EPOCH_SCHEMA,publicationFormat:PLACEMENT_FORMAT,generation:(context.profile as {generation:string}).generation,homeHash:context.homeHash,contextHash:hashJSON(context),nonce:randomBytes(16).toString("hex")};
|
|
144
|
+
const plan=planSyncPlacementActivation(context,placementEpochHash(epoch));
|
|
145
|
+
intent=sealPlacementActivationIntent({schema:PLACEMENT_ACTIVATION_INTENT_SCHEMA,home,epoch,plan,roots:writer!.measurement.roots.map(row=>row.root).sort(),previousProfile:source!.markers.profileText,previousShards:source!.markers.shardsText,profileStamp:source!.markers.profileStamp,shardsStamp:source!.markers.shardsStamp,floorHash:initialFloorHash,inputHashes:{sourceHash:context.sourceHash,aliasesHash:context.aliasesHash,policyHash:context.policyHash}});
|
|
146
|
+
// All future initial-locator framing/binding limits must fit before retaining an exclusive activation intent.
|
|
147
|
+
const epochHash=placementEpochHash(epoch);
|
|
148
|
+
planSyncPlacementWriterRoots({epochHash,homeHash:epoch.homeHash,activationHash:plan.seal,roots:writer!.measurement.roots},
|
|
149
|
+
{epochHash,homeHash:epoch.homeHash,activationHash:plan.seal,writers:plan.writers});
|
|
150
|
+
validateSyncPlacementActivation(plan,{current:()=>{initialCheck();return context;},authorize:()=>{authorize(options,intent!);return true;}});
|
|
151
|
+
writePlacementActivationIntent(home,intent,{mutation,assertCurrent:initialCheck});
|
|
152
|
+
}
|
|
153
|
+
const fixed=intent,epochHash=placementEpochHash(fixed.epoch);
|
|
154
|
+
const metadataCheck=()=>{assertPlacementActivationOwner(owner);requirePlacementHomeFloor(home);if(!committed(home,fixed))fail("changed");readSyncPlacementWriterRoots(home);assertMutationHandle(mutation,home);};
|
|
155
|
+
if(committed(home,fixed))return finishCommittedActivation(home,fixed,options,mutation,owner,fixed,report);
|
|
156
|
+
let current:PlacementWriterContext;
|
|
157
|
+
try{current=writer??initialPlacementWriterContext(home,fixed.roots,options.ownRoot);}catch(error){writerInputFailure(error);}
|
|
158
|
+
if(canonicalJSON(current.measurement.writers)!==canonicalJSON(fixed.plan.writers))throw new PlacementActivationInputError("writer root");
|
|
159
|
+
const proof=createPlacementActivationCaptureProof(owner,fixed);
|
|
160
|
+
const check=()=>{
|
|
161
|
+
assertPlacementActivationOwner(owner);
|
|
162
|
+
try{if(requirePlacementHomeFloor(home)!==fixed.floorHash)throw Error();}catch{throw new PlacementActivationInputError("floor");}
|
|
163
|
+
try{current.attest();}catch(error){writerInputFailure(error);}
|
|
164
|
+
let captured:ReturnType<typeof captureSyncPlacementActivationContext>;
|
|
165
|
+
try{captured=captureSyncPlacementActivationContext(home,{attest:current.attest},proof);}catch(error){if(current.lastRefusal)writerInputFailure(current.lastRefusal);throw new PlacementActivationInputError("source");}
|
|
166
|
+
if(captured.context.policyHash!==fixed.inputHashes.policyHash)throw new PlacementActivationInputError("policy");
|
|
167
|
+
if(captured.context.aliasesHash!==fixed.inputHashes.aliasesHash)throw new PlacementActivationInputError("aliases");
|
|
168
|
+
if(captured.context.sourceHash!==fixed.inputHashes.sourceHash||hashJSON(captured.context)!==fixed.plan.contextHash)throw new PlacementActivationInputError("source");
|
|
169
|
+
try{captured.assertUnchanged();}catch(error){if(current.lastRefusal)writerInputFailure(current.lastRefusal);throw new PlacementActivationInputError("source");}
|
|
170
|
+
assertPlacementActivationOwner(owner);
|
|
171
|
+
};
|
|
172
|
+
authorize(options,fixed);check();writePlacementActivationIntent(home,fixed,{mutation,assertCurrent:check});await options.checkpoint?.("after-intent");check();
|
|
173
|
+
preparePlacementEpoch(home,fixed.epoch,fixed.plan,{mutation,assertCurrent:check});await options.checkpoint?.("after-epoch");check();
|
|
174
|
+
writeSyncPlacementWriterRoots(home,{epochHash,homeHash:fixed.epoch.homeHash,activationHash:fixed.plan.seal,roots:current.measurement.roots},fixed.plan.writers,{mutation,assertCurrent:check});await options.checkpoint?.("after-root-map");check();
|
|
175
|
+
const final=placementActivationFinalMarkers(fixed),markers=readPlacementRuntimeMarkers(home);
|
|
176
|
+
if(canonicalJSON(markers.shards)+"\n"!==final.shards)writeProfileJSON(join(home,"shards.json"),JSON.parse(final.shards));else syncFile(join(home,"shards.json"));
|
|
177
|
+
await options.checkpoint?.("after-shards");check();
|
|
178
|
+
if(canonicalJSON(readPlacementRuntimeMarkers(home).profile)+"\n"!==final.profile)writeProfileJSON(join(home,"profile.json"),JSON.parse(final.profile));else syncFile(join(home,"profile.json"));
|
|
179
|
+
await options.checkpoint?.("after-profile");check();
|
|
180
|
+
ensurePlacementFence(home,epochHash,{mutation,assertCurrent:check});await options.checkpoint?.("after-fence");metadataCheck();return finishCommittedActivation(home,fixed,options,mutation,owner,fixed,report);
|
|
181
|
+
});}catch(error){
|
|
182
|
+
try{options.testOnlyFailure?.(error);}catch{/* observers never affect production outcomes */}
|
|
183
|
+
return rethrowActivationFailure(error);
|
|
184
|
+
}
|
|
185
|
+
}
|