@staix/agent-mem 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +27 -1
  3. package/package.json +1 -1
  4. package/src/cli.ts +34 -4
  5. package/src/commands-table.ts +3 -0
  6. package/src/index.ts +6 -0
  7. package/src/memory/commands.ts +3 -2
  8. package/src/memory/public-prune-activation.ts +4 -4
  9. package/src/memory/shard-sync.ts +1 -2
  10. package/src/memory/sync-cadence.ts +2 -1
  11. package/src/memory/sync-placement-abandon.ts +142 -0
  12. package/src/memory/sync-placement-activation-locks.ts +28 -0
  13. package/src/memory/sync-placement-activation-path.ts +2 -0
  14. package/src/memory/sync-placement-activation-proof.ts +68 -0
  15. package/src/memory/sync-placement-activation-storage.ts +167 -0
  16. package/src/memory/sync-placement-activation.ts +185 -0
  17. package/src/memory/sync-placement-capture.ts +104 -23
  18. package/src/memory/sync-placement-effective-writers.ts +84 -0
  19. package/src/memory/sync-placement-guard.ts +12 -6
  20. package/src/memory/sync-placement-inspection.ts +5 -1
  21. package/src/memory/sync-placement-journal-storage.ts +51 -7
  22. package/src/memory/sync-placement-journal.ts +11 -4
  23. package/src/memory/sync-placement-prerequisites.ts +50 -0
  24. package/src/memory/sync-placement-transport.ts +17 -6
  25. package/src/memory/sync-placement-writer-history.ts +278 -0
  26. package/src/memory/sync-placement-writer-renewal.ts +179 -0
  27. package/src/memory/sync-placement-writer-revision-format.ts +122 -0
  28. package/src/memory/sync-placement-writer-roots.ts +167 -0
  29. package/src/memory/sync-placement-writers.ts +135 -0
  30. package/src/memory/sync.ts +1 -2
  31. package/src/setup.ts +101 -14
  32. package/src/sync-placement-writer-context.ts +136 -0
@@ -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
+ }
@@ -18,6 +18,10 @@ import { validatePruneRow, type PruneSourceRow, type PruneRow } from "./prune.ts
18
18
  import { shardKeyHash, SHARD_SCHEMA } from "./shard.ts";
19
19
  import { SyncPlacementJournalError, placementKeyId, type PlacementKey } from "./sync-placement-journal-format.ts";
20
20
  import type { Entry } from "./store.ts";
21
+ import { assertPlacementRuntime } from "./sync-placement-guard.ts";
22
+ import { activationSourceMarkers, assertPlacementActivationOwner, type PlacementActivationOwner, type PlacementActivationCaptureProof } from "./sync-placement-activation-proof.ts";
23
+ import { readEffectiveSyncPlacementWriters, writerBoundPlacementContextHash, writerInventoryEvidence, type EffectiveSyncPlacementWriters } from "./sync-placement-effective-writers.ts";
24
+ import { SharedWriterRevisionError } from "./sync-placement-writer-revision-format.ts";
21
25
 
22
26
  /** Host authentication/compatibility facts are re-read at every effect boundary, never inferred from a session marker. */
23
27
  export interface PlacementCapturePort {
@@ -35,10 +39,22 @@ export interface PlacementCapture {
35
39
  /** Diagnostic inventory only; these rows never enter the ordinary transport/capture row map implicitly. */
36
40
  specialRows: Map<string, PlacementSpecialSourceRow>;
37
41
  sourceRowsByRealm: Map<string, readonly PruneSourceRow[]>;
42
+ writerInventory?:EffectiveSyncPlacementWriters;
38
43
  assertCurrent(): void;
39
44
  /** Under live writer fences, inode/ctime/size checks protect the already-hashed prefixes across callbacks without reparsing rows. */
40
45
  assertUnchanged(): void;
41
46
  }
47
+ export function placementCaptureContextHash(capture:Pick<PlacementCapture,"context"|"writerInventory">):string{return writerBoundPlacementContextHash(capture.context,capture.writerInventory);}
48
+ export interface PlacementActivationMarkerSnapshot {
49
+ profileText: string; shardsText: string; profileStamp: [string,string,string,string,string]; shardsStamp: [string,string,string,string,string];
50
+ }
51
+ /** Source facts only: no writer attestation, quiescence claim or effect authority. */
52
+ export interface PlacementSourcePreview {
53
+ home: string; base: Omit<SyncPlacementActivationContext, "writers" | "legacySyncQuiesced">;
54
+ activeHead?: string; markerHash: string; markers: PlacementActivationMarkerSnapshot; directories: string[]; prefixes: PlacementPublicationSource[];
55
+ rows: Map<string, PlacementSourceRow>; specialRows: Map<string, PlacementSpecialSourceRow>;
56
+ sourceRowsByRealm: Map<string, readonly PruneSourceRow[]>; assertSourcesUnchanged(): void;
57
+ }
42
58
  function fail(code: "changed" | "invalid" | "bound" = "changed"): never { throw new SyncPlacementJournalError(code); }
43
59
  const digest = (bytes: Uint8Array | string) => createHash("sha256").update(bytes).digest("hex");
44
60
  const stamp = (s: BigIntStats) => [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs].map(String);
@@ -50,7 +66,7 @@ function ownedDirectory(dir: string): BigIntStats {
50
66
  }
51
67
  /** ponytail: capture is capped at 256 metadata objects and 8 MiB/20000 source rows across all realms.
52
68
  * Larger homes need a reviewed chunked activation proof; no input is silently omitted at this ceiling. */
53
- function capture(homeInput: string, port: PlacementCapturePort, selected?: readonly PlacementRealm[], pending: readonly PlacementPendingSource[] = []): Omit<PlacementCapture, "assertCurrent"> {
69
+ function sourcePreview(homeInput: string, selected?: readonly PlacementRealm[], pending: readonly PlacementPendingSource[] = [], policyVersion?: string, ownerProof?: PlacementActivationCaptureProof, sourceOwner?:PlacementActivationOwner): PlacementSourcePreview {
54
70
  let selection: PlacementRealm[] | undefined;
55
71
  if (selected !== undefined) {
56
72
  if (!Array.isArray(selected) || types.isProxy(selected) || selected.length > 256) fail("bound");
@@ -58,11 +74,8 @@ function capture(homeInput: string, port: PlacementCapturePort, selected?: reado
58
74
  selection = copied.map(realm => validatePlacementKey({ realm, id: "0".repeat(32), rowHash: "0".repeat(64) }).realm);
59
75
  if (!selection.length || new Set(selection.map(realm => canonicalJSON(realm))).size !== selection.length) fail();
60
76
  }
61
- // All host callouts precede filesystem capture: a callback cannot alter input after its final proof.
62
- const attestation = memoryPolicySnapshot(port.attest()), policyVersion = port.policyVersion?.();
63
- if (!attestation) fail();
64
- if (port.policyVersion && typeof policyVersion !== "string") fail();
65
77
  const home = realPath(homeInput), root = ownedDirectory(home);
78
+ if(sourceOwner)assertPlacementActivationOwner(sourceOwner,home);
66
79
  let metadataObjects = 0, metadataBytes = 0;
67
80
  const facts: unknown[] = [], sourceChecks: (() => void)[] = [];
68
81
  const pendingByPath = new Map(pending.map(item => [canonicalJSON(item.realm)+":"+item.pathHash,item]));
@@ -90,7 +103,17 @@ function capture(homeInput: string, port: PlacementCapturePort, selected?: reado
90
103
  const input = bytes(file, optional); if (!input) return undefined;
91
104
  return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input));
92
105
  };
93
- const rawProfile = json(join(home, "profile.json")), rawShards = json(join(home, "shards.json"));
106
+ const markerStart = facts.length;
107
+ const profileBytes = bytes(join(home, "profile.json"))!, shardsBytes = bytes(join(home, "shards.json"))!;
108
+ const actualMarkers: PlacementActivationMarkerSnapshot = { profileText: profileBytes.toString("utf8"), shardsText: shardsBytes.toString("utf8"),
109
+ profileStamp: [...(facts[markerStart] as [string,PlacementActivationMarkerSnapshot["profileStamp"],string])[1]], shardsStamp: [...(facts[markerStart+1] as [string,PlacementActivationMarkerSnapshot["shardsStamp"],string])[1]] };
110
+ const markers = ownerProof ? activationSourceMarkers(ownerProof,home,actualMarkers) : actualMarkers;
111
+ if (ownerProof) {
112
+ facts[markerStart] = [join(home,"profile.json"),markers.profileStamp,digest(Buffer.from(markers.profileText))];
113
+ facts[markerStart+1] = [join(home,"shards.json"),markers.shardsStamp,digest(Buffer.from(markers.shardsText))];
114
+ }
115
+ const decodeMarker = (text: string): unknown => JSON.parse(new TextDecoder("utf-8",{fatal:true}).decode(Buffer.from(text)));
116
+ const rawProfile = decodeMarker(markers.profileText) as { schema?: string }, rawShards = decodeMarker(markers.shardsText);
94
117
  let activeHead: string | undefined;
95
118
  let base: ReturnType<typeof validatePlacementBaseV2>;
96
119
  if (rawProfile?.schema === PLACEMENT_PROFILE_SCHEMA) {
@@ -109,7 +132,11 @@ function capture(homeInput: string, port: PlacementCapturePort, selected?: reado
109
132
  const machine = json(join(home, "machine.json"));
110
133
  if (machine?.id !== base.shards.machine.id) fail();
111
134
  // An in-flight migration/prune is never accepted as a stable placement source.
112
- for (const name of ["profile.lock", PUBLIC_PRUNE_INTENT]) if (lstatSync(join(home, name), { throwIfNoEntry: false })) fail();
135
+ for (const name of ["profile.lock", PUBLIC_PRUNE_INTENT]) {
136
+ if (name === "profile.lock" && ownerProof) continue; // The module-private proof validated this exact live owner and sealed intent.
137
+ if(name==="profile.lock"&&sourceOwner){assertPlacementActivationOwner(sourceOwner,home);continue;}
138
+ if (lstatSync(join(home,name),{throwIfNoEntry:false})) fail();
139
+ }
113
140
  const reader = new SegmentReader(16 * 1024 * 1024, { maxSourceBytes: 8 * 1024 * 1024, maxRows: 20000 });
114
141
  const rows = new Map<string, PlacementSourceRow>(), specialRows = new Map<string, PlacementSpecialSourceRow>(), sourceRowsByRealm = new Map<string, readonly PruneSourceRow[]>();
115
142
  const directories = [home], prefixes: PlacementPublicationSource[] = [];
@@ -242,29 +269,68 @@ function capture(homeInput: string, port: PlacementCapturePort, selected?: reado
242
269
  if (selection && realms.length !== selection.length) fail();
243
270
  const defaults = memoryPolicySnapshot({ names: DENY_NAMES, segments: DENY_SEGMENTS }); if (!defaults) fail();
244
271
  const configuration = loadAmemConfig(home).config;
245
- const context: SyncPlacementActivationContext = { ...base, homeHash: hashJSON([home, String(root.dev), String(root.ino), String(root.uid)]), sourceHash: hashJSON(facts),
246
- aliasesHash: digest(aliases ?? Buffer.alloc(0)), policyHash: hashJSON({ config: configuration, defaults, policyVersion }), realms,
247
- writers: attestation.writers, legacySyncQuiesced: attestation.legacySyncQuiesced };
248
- // Reuse the closed stage-1 context validator, including writer/version/realm bounds.
272
+ const contextBase: PlacementSourcePreview["base"] = { ...base,
273
+ homeHash: hashJSON([home, String(root.dev), String(root.ino), String(root.uid)]), sourceHash: hashJSON(facts),
274
+ aliasesHash: digest(aliases ?? Buffer.alloc(0)), policyHash: hashJSON({ config: configuration, defaults, policyVersion }), realms };
275
+ // ponytail: passive checks rely on native inode/size/ctime identity for the hashed prefixes.
276
+ // Immutable descriptor-bound source blocks are the upgrade path for hostile same-user raw writers.
277
+ const assertSourcesUnchanged = () => {
278
+ try {
279
+ const currentDefaults = memoryPolicySnapshot({ names: DENY_NAMES, segments: DENY_SEGMENTS });
280
+ if (!currentDefaults || hashJSON({ config: configuration, defaults: currentDefaults, policyVersion }) !== contextBase.policyHash) fail();
281
+ const now = ownedDirectory(home); if (now.dev !== root.dev || now.ino !== root.ino) fail();
282
+ for (const check of sourceChecks) check();
283
+ if (ownerProof) activationSourceMarkers(ownerProof,home,actualMarkers);
284
+ if(sourceOwner)assertPlacementActivationOwner(sourceOwner,home);
285
+ } catch { fail(); }
286
+ };
287
+ return { home, base: contextBase, assertSourcesUnchanged, ...(activeHead ? { activeHead } : {}), markerHash, markers,
288
+ directories: directories.sort(), prefixes, rows, specialRows, sourceRowsByRealm };
289
+ }
290
+ /** Read-only preview shares the source collector, without fabricating a quiescence attestation. */
291
+ export function previewSyncPlacementSources(home: string): PlacementSourcePreview {
292
+ return previewSources(home);
293
+ }
294
+ /** Internal renewal inventory under the exact live home/profile/mutation owner; no marker virtualization. */
295
+ export function previewOwnedSyncPlacementSources(home:string,owner:PlacementActivationOwner):PlacementSourcePreview {
296
+ assertPlacementActivationOwner(owner,home);return previewSources(home,owner);
297
+ }
298
+ function previewSources(home:string,owner?:PlacementActivationOwner):PlacementSourcePreview {
299
+ try { assertPlacementRuntime(home); const preview = sourcePreview(home,undefined,[],undefined,undefined,owner); preview.assertSourcesUnchanged(); return preview; }
300
+ catch (error) {
301
+ if (error && typeof error === "object" && !types.isProxy(error)) {
302
+ if (error instanceof SyncPlacementJournalError) throw error;
303
+ if (error instanceof SegmentReadBudgetError) return fail("bound");
304
+ }
305
+ return fail();
306
+ }
307
+ }
308
+ function capture(home: string, port: PlacementCapturePort, selected?: readonly PlacementRealm[], pending: readonly PlacementPendingSource[] = [], ownerProof?: PlacementActivationCaptureProof): Omit<PlacementCapture, "assertCurrent"> {
309
+ // All host callouts precede filesystem capture, including the final passive proof.
310
+ const attestation = memoryPolicySnapshot(port.attest()), policyVersion = port.policyVersion?.();
311
+ if (!attestation || port.policyVersion && typeof policyVersion !== "string") fail();
312
+ const source = sourcePreview(home, selected, pending, policyVersion, ownerProof);
313
+ const context: SyncPlacementActivationContext = { ...source.base, writers: attestation.writers, legacySyncQuiesced: attestation.legacySyncQuiesced };
249
314
  const validated = planSyncPlacementActivation(context, "0".repeat(64));
250
315
  context.writers = validated.writers; context.realms = validated.realms;
251
- // ponytail: fast rechecks rely on held mutation fences plus native inode/size/ctime identity for the already-hashed prefixes.
252
- // A hostile same-user raw writer is outside that contract; descriptor-bound immutable source blocks are the upgrade path.
316
+ const writerInventory=source.activeHead&&writerInventoryEvidence(source.home,source.activeHead)?readEffectiveSyncPlacementWriters(source.home):undefined;
317
+ if(writerInventory&&canonicalJSON(context.writers)!==canonicalJSON(writerInventory.writers))fail();
253
318
  const assertUnchanged = () => {
254
319
  try {
255
- // Host callbacks run first; the final filesystem proof never invokes caller code afterwards.
256
- const currentAttestation=memoryPolicySnapshot(port.attest()), currentVersion=port.policyVersion?.();
257
- if(!currentAttestation) fail();
258
- const checked=planSyncPlacementActivation({...context,writers:currentAttestation.writers,legacySyncQuiesced:currentAttestation.legacySyncQuiesced},"0".repeat(64));
259
- const currentDefaults=memoryPolicySnapshot({names:DENY_NAMES,segments:DENY_SEGMENTS});
260
- if(!currentDefaults)fail();
261
- if(canonicalJSON(checked.writers)!==canonicalJSON(context.writers)||hashJSON({config:configuration,defaults:currentDefaults,policyVersion:currentVersion})!==context.policyHash) fail();
262
- const now=ownedDirectory(home); if(now.dev!==root.dev||now.ino!==root.ino) fail();
263
- for(const check of sourceChecks)check();
320
+ const currentAttestation = memoryPolicySnapshot(port.attest()), currentVersion = port.policyVersion?.();
321
+ if (!currentAttestation) fail();
322
+ const checked = planSyncPlacementActivation({ ...context, writers: currentAttestation.writers, legacySyncQuiesced: currentAttestation.legacySyncQuiesced }, "0".repeat(64));
323
+ if (canonicalJSON(checked.writers) !== canonicalJSON(context.writers) || currentVersion !== policyVersion) fail();
324
+ writerInventory?.assertCurrent();
325
+ if(source.activeHead&&!writerInventory&&writerInventoryEvidence(source.home,source.activeHead))fail();
326
+ source.assertSourcesUnchanged();
264
327
  } catch { fail(); }
265
328
  };
266
- return { home, context, assertUnchanged, ...(activeHead ? { activeHead } : {}), markerHash, fingerprint: hashJSON({ context, markerHash }), directories: directories.sort(), prefixes, rows, specialRows, sourceRowsByRealm };
329
+ return { home: source.home, context, assertUnchanged, ...(source.activeHead ? { activeHead: source.activeHead } : {}),
330
+ ...(writerInventory?{writerInventory}:{}),markerHash: source.markerHash, fingerprint: hashJSON({ context, markerHash: source.markerHash,...(writerInventory?.revisionSequence?{writerRevisionHash:writerInventory.revisionHash}:{}) }), directories: source.directories,
331
+ prefixes: source.prefixes, rows: source.rows, specialRows: source.specialRows, sourceRowsByRealm: source.sourceRowsByRealm };
267
332
  }
333
+
268
334
  export function captureSyncPlacementContext(home: string, port: PlacementCapturePort, selected?: readonly PlacementRealm[], pendingInput: readonly PlacementPendingSource[] = []): PlacementCapture {
269
335
  try {
270
336
  const selection = selected === undefined ? undefined : memoryPolicySnapshot(selected);
@@ -282,6 +348,21 @@ export function captureSyncPlacementContext(home: string, port: PlacementCapture
282
348
  });
283
349
  const before = capture(home, port, selection, pending);
284
350
  return { ...before, assertCurrent: () => { try { if (capture(home, port, selection, pending).fingerprint !== before.fingerprint) fail(); } catch { fail(); } } };
351
+ } catch (error) {
352
+ if (error && typeof error === "object" && !types.isProxy(error)) {
353
+ if(error instanceof SharedWriterRevisionError){const code=Object.getOwnPropertyDescriptor(error,"code")?.value;if(code==="staging"||code==="pending")throw new SharedWriterRevisionError(code);}
354
+ if (error instanceof SyncPlacementJournalError) throw error;
355
+ if (error instanceof SegmentReadBudgetError) return fail("bound");
356
+ }
357
+ return fail();
358
+ }
359
+ }
360
+
361
+ /** Internal initial-activation capture only. Tokens require the actual profile owner, live home mutation and exact durable intent. */
362
+ export function captureSyncPlacementActivationContext(home: string, port: PlacementCapturePort, proof: PlacementActivationCaptureProof): PlacementCapture {
363
+ try {
364
+ const before = capture(home,port,undefined,[],proof);
365
+ return { ...before, assertCurrent: () => { if (capture(home,port,undefined,[],proof).fingerprint !== before.fingerprint) fail(); } };
285
366
  } catch (error) {
286
367
  if (error && typeof error === "object" && !types.isProxy(error)) {
287
368
  if (error instanceof SyncPlacementJournalError) throw error;