@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,122 @@
|
|
|
1
|
+
/** Pure same-epoch writer metadata codecs. Valid data and seals are neither person authority nor fresh install proof. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { types } from "node:util";
|
|
5
|
+
import { canonicalJSON, hashJSON } from "./generation.ts";
|
|
6
|
+
import { syncPlacementWriterId, type MeasuredSyncPlacementWriterRoot } from "./sync-placement-writers.ts";
|
|
7
|
+
import { PLACEMENT_FORMAT } from "./sync-placement-format.ts";
|
|
8
|
+
import type { SyncPlacementWriterAttestation } from "./sync-placement-plan.ts";
|
|
9
|
+
|
|
10
|
+
export const WRITER_REVISION_MAX_BYTES=512*1024;
|
|
11
|
+
export const WRITER_COMMIT_MAX_BYTES=64*1024;
|
|
12
|
+
export const WRITER_RENEWAL_INTENT_MAX_BYTES=1024*1024;
|
|
13
|
+
export const WRITER_REVISION_LIMIT=64;
|
|
14
|
+
export const WRITER_COMMIT_LOG="writer-revisions.jsonl";
|
|
15
|
+
export const WRITER_RENEWAL_INTENT_FILE="sync-placement-writer-renewal.json";
|
|
16
|
+
export const WRITER_REVISION_SCHEMA="agent-mem.sync-placement-writer-revision/v1";
|
|
17
|
+
export const WRITER_COMMIT_SCHEMA="agent-mem.sync-placement-writer-commit/v1";
|
|
18
|
+
export const WRITER_RENEWAL_INTENT_SCHEMA="agent-mem.sync-placement-writer-renewal/v1";
|
|
19
|
+
|
|
20
|
+
export type SharedWriterRevisionErrorCode="invalid"|"bound"|"changed"|"pending"|"staging"|"cap";
|
|
21
|
+
const messages:Record<SharedWriterRevisionErrorCode,string>={
|
|
22
|
+
invalid:"Writer renewal metadata is invalid or unsupported; have the person inspect the retained same-epoch inventory and rerun amem migrate --sync-placement --dry-run.",
|
|
23
|
+
bound:"Writer renewal metadata exceeds its supported bound; retain the existing inventory and request a separately reviewed archival/checkpoint before retrying.",
|
|
24
|
+
changed:"Writer renewal bindings changed; retain the existing inventory, inspect the same-epoch evidence and review a fresh dry-run before applying.",
|
|
25
|
+
pending:"Writer renewal is pending; preserve its exact intent and have the person run amem migrate --sync-placement --resume.",
|
|
26
|
+
staging:"Writer renewal has orphan staging without a registered final intent; have the person inspect it and run amem migrate --sync-placement --abandon, then amem migrate --sync-placement --dry-run before another apply.",
|
|
27
|
+
cap:"Writer revision capacity is exhausted (remaining 0); retain this epoch and request a separately reviewed archival/checkpoint before another renewal.",
|
|
28
|
+
};
|
|
29
|
+
// ponytail: at most64 immutable writer revisions; only a separately reviewed archival/checkpoint can extend this history.
|
|
30
|
+
export class SharedWriterRevisionError extends Error {
|
|
31
|
+
readonly remaining?:number;
|
|
32
|
+
constructor(readonly code:SharedWriterRevisionErrorCode){
|
|
33
|
+
const safe=typeof code==="string"&&Object.hasOwn(messages,code)?code:"invalid";
|
|
34
|
+
super(messages[safe]);this.name="SharedWriterRevisionError";this.code=safe;if(safe==="cap")this.remaining=0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function fail(code:SharedWriterRevisionErrorCode="invalid"):never {throw new SharedWriterRevisionError(code);}
|
|
38
|
+
function safe<T>(run:()=>T):T {try{return run();}catch(error){
|
|
39
|
+
if(error&&typeof error==="object"&&!types.isProxy(error)&&error instanceof SharedWriterRevisionError){
|
|
40
|
+
const code=Object.getOwnPropertyDescriptor(error,"code");if(code&&"value"in code&&typeof code.value==="string"&&Object.hasOwn(messages,code.value))fail(code.value as SharedWriterRevisionErrorCode);
|
|
41
|
+
}return fail();
|
|
42
|
+
}}
|
|
43
|
+
function object(input:unknown,keys:readonly string[]):Record<string,unknown>{
|
|
44
|
+
if(!input||typeof input!=="object"||types.isProxy(input)||Array.isArray(input)||![Object.prototype,null].includes(Object.getPrototypeOf(input))||Object.getOwnPropertySymbols(input).length)fail();
|
|
45
|
+
if(Object.getOwnPropertyNames(input).length!==keys.length)fail();const out:Record<string,unknown>=Object.create(null);
|
|
46
|
+
for(const key of keys){const field=Object.getOwnPropertyDescriptor(input,key);if(!field||!("value"in field)||!field.enumerable)fail();out[key]=field.value;}return out;
|
|
47
|
+
}
|
|
48
|
+
function array(input:unknown,maximum:number):unknown[]{
|
|
49
|
+
if(!input||typeof input!=="object"||types.isProxy(input)||!Array.isArray(input)||Object.getPrototypeOf(input)!==Array.prototype||Object.getOwnPropertySymbols(input).length)fail();
|
|
50
|
+
const length=Object.getOwnPropertyDescriptor(input,"length")?.value;
|
|
51
|
+
if(!Number.isSafeInteger(length)||length<1||length>maximum)fail("bound");
|
|
52
|
+
if(Object.getOwnPropertyNames(input).length!==length+1)fail();const out:unknown[]=[];
|
|
53
|
+
for(let i=0;i<length;i++){const field=Object.getOwnPropertyDescriptor(input,String(i));if(!field||!("value"in field)||!field.enumerable)fail();out.push(field.value);}return out;
|
|
54
|
+
}
|
|
55
|
+
function hex(input:unknown,width=64):string {if(typeof input!=="string"||input.length!==width||!/^[a-f0-9]+$/.test(input))fail();return input;}
|
|
56
|
+
function integer(input:unknown,minimum:number,maximum:number):number {if(typeof input!=="number"||!Number.isSafeInteger(input)||input<minimum||input>maximum)fail();return input;}
|
|
57
|
+
function sequence(input:unknown):number {if(typeof input==="number"&&Number.isSafeInteger(input)&&input>WRITER_REVISION_LIMIT)fail("cap");return integer(input,1,WRITER_REVISION_LIMIT);}
|
|
58
|
+
function path(input:unknown):string {if(typeof input!=="string"||!isAbsolute(input)||resolve(input)!==input||input.includes("\0"))fail();if(Buffer.byteLength(input)>4096)fail("bound");return input;}
|
|
59
|
+
function version(input:unknown):string {
|
|
60
|
+
if(typeof input!=="string"||input.length>64)fail();const match=/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.exec(input);
|
|
61
|
+
if(!match||match[0]!==input)fail();const parts=match.slice(1).map(Number);
|
|
62
|
+
if(!parts.every(Number.isSafeInteger)||parts[0]===0&&parts[1]!<22)fail();return input;
|
|
63
|
+
}
|
|
64
|
+
function bounded(value:unknown,maximum:number):void {if(Buffer.byteLength(canonicalJSON(value)+"\n")>maximum)fail("bound");}
|
|
65
|
+
function roots(input:unknown):MeasuredSyncPlacementWriterRoot[]{
|
|
66
|
+
const out:MeasuredSyncPlacementWriterRoot[]=[],ids=new Set<string>();let previous="";
|
|
67
|
+
for(const entry of array(input,256)){
|
|
68
|
+
const value=object(entry,["root","writerId","version","pin"]),root=path(value.root),writerId=hex(value.writerId);
|
|
69
|
+
if(root<=previous||ids.has(writerId)||writerId!==syncPlacementWriterId(root))fail();
|
|
70
|
+
out.push({root,writerId,version:version(value.version),pin:hex(value.pin)});ids.add(writerId);previous=root;
|
|
71
|
+
}return out;
|
|
72
|
+
}
|
|
73
|
+
export function writerRevisionFile(value:number):string {return `writer-revision-${String(sequence(value)).padStart(5,"0")}.json`;}
|
|
74
|
+
export function writerHistoryFileLimit(name:string):number|undefined {
|
|
75
|
+
if(name===WRITER_COMMIT_LOG)return WRITER_COMMIT_MAX_BYTES;
|
|
76
|
+
if(typeof name!=="string")return;const match=/^writer-revision-([0-9]{5})\.json$/.exec(name);
|
|
77
|
+
if(!match||match[0]!==name)return;const n=Number(match[1]);return n>=1&&n<=WRITER_REVISION_LIMIT?WRITER_REVISION_MAX_BYTES:undefined;
|
|
78
|
+
}
|
|
79
|
+
export function writerInventoryGenesis(epochHash:string,activationHash:string):string {
|
|
80
|
+
return hashJSON(["agent-mem.sync-placement-writer-genesis/v1",hex(epochHash),hex(activationHash)]);
|
|
81
|
+
}
|
|
82
|
+
export interface WriterRevisionBody {
|
|
83
|
+
schema:typeof WRITER_REVISION_SCHEMA;epochHash:string;generation:string;homeHash:string;activationHash:string;initialLocatorHash:string;
|
|
84
|
+
sequence:number;previousHash:string;journal:{sequence:number;headHash:string};roots:MeasuredSyncPlacementWriterRoot[];requestId:string;
|
|
85
|
+
}
|
|
86
|
+
export interface WriterRevision extends WriterRevisionBody {seal:string}
|
|
87
|
+
export interface WriterCommitBody {schema:typeof WRITER_COMMIT_SCHEMA;epochHash:string;sequence:number;previousHash:string;revisionHash:string}
|
|
88
|
+
export interface WriterCommit extends WriterCommitBody {seal:string}
|
|
89
|
+
export interface WriterRenewalIntentBody {schema:typeof WRITER_RENEWAL_INTENT_SCHEMA;home:string;revision:WriterRevision;commit:WriterCommit;previousLogBytes:number;previousLogHash:string}
|
|
90
|
+
export interface WriterRenewalIntent extends WriterRenewalIntentBody {seal:string}
|
|
91
|
+
function revision(input:unknown,sealed:boolean):WriterRevision {
|
|
92
|
+
const v=object(input,["schema","epochHash","generation","homeHash","activationHash","initialLocatorHash","sequence","previousHash","journal","roots","requestId",...(sealed?["seal"]:[])]);
|
|
93
|
+
if(v.schema!==WRITER_REVISION_SCHEMA)fail();const journal=object(v.journal,["sequence","headHash"]);
|
|
94
|
+
const body:WriterRevisionBody={schema:WRITER_REVISION_SCHEMA,epochHash:hex(v.epochHash),generation:hex(v.generation,32),homeHash:hex(v.homeHash),activationHash:hex(v.activationHash),
|
|
95
|
+
initialLocatorHash:hex(v.initialLocatorHash),sequence:sequence(v.sequence),previousHash:hex(v.previousHash),journal:{sequence:integer(journal.sequence,0,20000),headHash:hex(journal.headHash)},roots:roots(v.roots),requestId:hex(v.requestId)};
|
|
96
|
+
if(body.sequence===1&&body.previousHash!==writerInventoryGenesis(body.epochHash,body.activationHash))fail();
|
|
97
|
+
const value={...body,seal:hashJSON(body)};if(sealed&&hex(v.seal)!==value.seal)fail();bounded(value,WRITER_REVISION_MAX_BYTES);return value;
|
|
98
|
+
}
|
|
99
|
+
export function createWriterRevision(body:WriterRevisionBody):WriterRevision {return safe(()=>revision(body,false));}
|
|
100
|
+
export function validateWriterRevision(value:unknown):WriterRevision {return safe(()=>revision(value,true));}
|
|
101
|
+
export function writerRevisionWriters(input:WriterRevision):SyncPlacementWriterAttestation[]{
|
|
102
|
+
return validateWriterRevision(input).roots.map<SyncPlacementWriterAttestation>(({writerId,version,pin})=>({writerId,version,pin,publicationFormat:PLACEMENT_FORMAT,compatible:true as const})).sort((a,b)=>a.writerId.localeCompare(b.writerId));
|
|
103
|
+
}
|
|
104
|
+
function commit(input:unknown,sealed:boolean):WriterCommit {
|
|
105
|
+
const v=object(input,["schema","epochHash","sequence","previousHash","revisionHash",...(sealed?["seal"]:[])]);if(v.schema!==WRITER_COMMIT_SCHEMA)fail();
|
|
106
|
+
const body:WriterCommitBody={schema:WRITER_COMMIT_SCHEMA,epochHash:hex(v.epochHash),sequence:sequence(v.sequence),previousHash:hex(v.previousHash),revisionHash:hex(v.revisionHash)};
|
|
107
|
+
const value={...body,seal:hashJSON(body)};if(sealed&&hex(v.seal)!==value.seal)fail();bounded(value,WRITER_COMMIT_MAX_BYTES);return value;
|
|
108
|
+
}
|
|
109
|
+
export function createWriterCommit(input:WriterRevision):WriterCommit {
|
|
110
|
+
const rev=validateWriterRevision(input);return safe(()=>commit({schema:WRITER_COMMIT_SCHEMA,epochHash:rev.epochHash,sequence:rev.sequence,previousHash:rev.previousHash,revisionHash:rev.seal},false));
|
|
111
|
+
}
|
|
112
|
+
export function validateWriterCommit(value:unknown):WriterCommit {return safe(()=>commit(value,true));}
|
|
113
|
+
function intent(input:unknown,sealed:boolean):WriterRenewalIntent {
|
|
114
|
+
const v=object(input,["schema","home","revision","commit","previousLogBytes","previousLogHash",...(sealed?["seal"]:[])]);if(v.schema!==WRITER_RENEWAL_INTENT_SCHEMA)fail();
|
|
115
|
+
const rev=validateWriterRevision(v.revision),ack=validateWriterCommit(v.commit),previousLogBytes=integer(v.previousLogBytes,0,WRITER_COMMIT_MAX_BYTES),previousLogHash=hex(v.previousLogHash);
|
|
116
|
+
if(ack.epochHash!==rev.epochHash||ack.sequence!==rev.sequence||ack.previousHash!==rev.previousHash||ack.revisionHash!==rev.seal)fail();
|
|
117
|
+
if(rev.sequence===1&&(previousLogBytes!==0||previousLogHash!==createHash("sha256").update("").digest("hex")))fail();
|
|
118
|
+
const body:WriterRenewalIntentBody={schema:WRITER_RENEWAL_INTENT_SCHEMA,home:path(v.home),revision:rev,commit:ack,previousLogBytes,previousLogHash};
|
|
119
|
+
const value={...body,seal:hashJSON(body)};if(sealed&&hex(v.seal)!==value.seal)fail();bounded(value,WRITER_RENEWAL_INTENT_MAX_BYTES);return value;
|
|
120
|
+
}
|
|
121
|
+
export function createWriterRenewalIntent(body:WriterRenewalIntentBody):WriterRenewalIntent {return safe(()=>intent(body,false));}
|
|
122
|
+
export function validateWriterRenewalIntent(value:unknown):WriterRenewalIntent {return safe(()=>intent(value,true));}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Immutable initial install locator only. Returned paths are never fresh compatibility or quiescence evidence. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, 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 { memoryPolicySnapshot } from "./policy.ts";
|
|
8
|
+
import { realPath } from "./platform.ts";
|
|
9
|
+
import { assertMutationHandle, type MutationHandle } from "./mutation.ts";
|
|
10
|
+
import { readPlacementRuntimeMarkers } from "./sync-placement-guard.ts";
|
|
11
|
+
import { readActivePlacementBinding } from "./sync-placement-active.ts";
|
|
12
|
+
import { readPlacementJournal } from "./sync-placement-journal-storage.ts";
|
|
13
|
+
import { writeImmutableActivationMetadata } from "./public-prune-activation.ts";
|
|
14
|
+
import { measureSyncPlacementWriters, SyncPlacementWriterError, type MeasuredSyncPlacementWriterRoot } from "./sync-placement-writers.ts";
|
|
15
|
+
import { PLACEMENT_FORMAT } from "./sync-placement-format.ts";
|
|
16
|
+
import type { SyncPlacementWriterAttestation } from "./sync-placement-plan.ts";
|
|
17
|
+
|
|
18
|
+
export const SYNC_PLACEMENT_WRITER_ROOTS_FILE="sync-placement-writer-roots.json";
|
|
19
|
+
export const SYNC_PLACEMENT_WRITER_ROOTS_SCHEMA="agent-mem.sync-placement-writer-roots/v1";
|
|
20
|
+
interface LocatorBody {
|
|
21
|
+
schema:typeof SYNC_PLACEMENT_WRITER_ROOTS_SCHEMA;homeHash:string;epochHash:string;activationHash:string;
|
|
22
|
+
roots:{root:string;writerId:string}[];
|
|
23
|
+
}
|
|
24
|
+
export interface InitialSyncPlacementWriterLocator extends LocatorBody {seal:string}
|
|
25
|
+
type Locator=InitialSyncPlacementWriterLocator;
|
|
26
|
+
const MAX_BYTES=256*1024,MAX_ROOTS=256,MAX_PATH=4096;
|
|
27
|
+
const hash=(text:string)=>createHash("sha256").update(text).digest("hex");
|
|
28
|
+
const hex=(value:unknown):value is string=>typeof value==="string"&&value.length===64&&/^[a-f0-9]+$/.test(value);
|
|
29
|
+
const stamp=(s:BigIntStats)=>[s.dev,s.ino,s.mode,s.uid,s.nlink,s.size,s.mtimeNs,s.ctimeNs].map(String).join(":");
|
|
30
|
+
function fail():never {throw new SyncPlacementWriterError("unavailable");}
|
|
31
|
+
function protectedCall<T>(work:()=>T):T {try{return work();}catch(error){
|
|
32
|
+
if(error&&typeof error==="object"&&!types.isProxy(error)&&error instanceof SyncPlacementWriterError){const code=Object.getOwnPropertyDescriptor(error,"code");if(code&&"value"in code&&code.value==="bound")throw new SyncPlacementWriterError("bound");}
|
|
33
|
+
return fail();
|
|
34
|
+
}}
|
|
35
|
+
function directory(home:string):BigIntStats {
|
|
36
|
+
const s=lstatSync(home,{bigint:true});
|
|
37
|
+
if(!s.isDirectory()||s.isSymbolicLink()||s.uid!==BigInt(process.getuid?.()??-1)||(s.mode&0o777n)!==0o700n)fail();return s;
|
|
38
|
+
}
|
|
39
|
+
function fileState(file:string):BigIntStats {
|
|
40
|
+
const s=lstatSync(file,{bigint:true});
|
|
41
|
+
if(!s.isFile()||s.isSymbolicLink()||s.uid!==BigInt(process.getuid?.()??-1)||(s.mode&0o777n)!==0o600n
|
|
42
|
+
||s.nlink!==1n&&s.nlink!==2n||s.size<1n||s.size>BigInt(MAX_BYTES))fail();return s;
|
|
43
|
+
}
|
|
44
|
+
function object(input:unknown,keys:readonly string[]):Record<string,unknown> {
|
|
45
|
+
if(!input||typeof input!=="object"||types.isProxy(input)||Array.isArray(input)||![Object.prototype,null].includes(Object.getPrototypeOf(input))||Object.getOwnPropertySymbols(input).length)fail();
|
|
46
|
+
const fields=Object.getOwnPropertyDescriptors(input);if(Object.keys(fields).sort().join()!==[...keys].sort().join())fail();
|
|
47
|
+
const out:Record<string,unknown>=Object.create(null);
|
|
48
|
+
for(const key of keys){const field=fields[key];if(!field||!("value"in field)||!field.enumerable)fail();out[key]=field.value;}return out;
|
|
49
|
+
}
|
|
50
|
+
function array(input:unknown):unknown[] {
|
|
51
|
+
if(!input||typeof input!=="object"||types.isProxy(input)||!Array.isArray(input)||Object.getPrototypeOf(input)!==Array.prototype||Object.getOwnPropertySymbols(input).length)fail();
|
|
52
|
+
const fields=Object.getOwnPropertyDescriptors(input),length=Object.getOwnPropertyDescriptor(input,"length")?.value;
|
|
53
|
+
if(!Number.isSafeInteger(length)||length<1||length>MAX_ROOTS||Object.keys(fields).length!==length+1)fail();
|
|
54
|
+
const out:unknown[]=[];for(let i=0;i<length;i++){const field=fields[String(i)];if(!field||!("value"in field)||!field.enumerable)fail();out.push(field.value);}return out;
|
|
55
|
+
}
|
|
56
|
+
function validate(input:unknown):Locator {
|
|
57
|
+
const value=object(input,["schema","homeHash","epochHash","activationHash","roots","seal"]);
|
|
58
|
+
if(value.schema!==SYNC_PLACEMENT_WRITER_ROOTS_SCHEMA||!hex(value.homeHash)||!hex(value.epochHash)||!hex(value.activationHash)||!hex(value.seal)
|
|
59
|
+
)fail();
|
|
60
|
+
const roots:{root:string;writerId:string}[]=[],ids=new Set<string>();let prior="";
|
|
61
|
+
for(const entry of array(value.roots)){
|
|
62
|
+
const item=object(entry,["root","writerId"]);
|
|
63
|
+
if(typeof item.root!=="string"||!isAbsolute(item.root)||resolve(item.root)!==item.root||Buffer.byteLength(item.root)>MAX_PATH||item.root.includes("\0")
|
|
64
|
+
||item.root<=prior||!hex(item.writerId)||ids.has(item.writerId))fail();
|
|
65
|
+
roots.push({root:item.root,writerId:item.writerId});prior=item.root;ids.add(item.writerId);
|
|
66
|
+
}
|
|
67
|
+
const body:LocatorBody={schema:SYNC_PLACEMENT_WRITER_ROOTS_SCHEMA,homeHash:value.homeHash,epochHash:value.epochHash,activationHash:value.activationHash,roots};
|
|
68
|
+
if(hashJSON(body)!==value.seal)fail();
|
|
69
|
+
if(Buffer.byteLength(canonicalJSON(value)+"\n")>MAX_BYTES)throw new SyncPlacementWriterError("bound");
|
|
70
|
+
return{...body,seal:value.seal};
|
|
71
|
+
}
|
|
72
|
+
function idSet(writers:readonly SyncPlacementWriterAttestation[]):string[] {
|
|
73
|
+
const copy=memoryPolicySnapshot(writers);if(!Array.isArray(copy)||copy.length<1||copy.length>MAX_ROOTS)fail();
|
|
74
|
+
const ids=copy.map(writer=>{if(!writer||typeof writer!=="object"||!hex(writer.writerId))fail();return writer.writerId;}).sort();
|
|
75
|
+
if(new Set(ids).size!==ids.length)fail();return ids;
|
|
76
|
+
}
|
|
77
|
+
function bind(value:Locator,expected:{homeHash:string;epochHash:string;activationHash:string},writers:readonly SyncPlacementWriterAttestation[]):void {
|
|
78
|
+
if(value.homeHash!==expected.homeHash||value.epochHash!==expected.epochHash||value.activationHash!==expected.activationHash
|
|
79
|
+
||canonicalJSON(value.roots.map(root=>root.writerId).sort())!==canonicalJSON(idSet(writers)))fail();
|
|
80
|
+
}
|
|
81
|
+
function readOwned(file:string):{text:string;check():void} {
|
|
82
|
+
const before=fileState(file),fd=openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
|
|
83
|
+
try{
|
|
84
|
+
if(stamp(fstatSync(fd,{bigint:true}))!==stamp(before))fail();
|
|
85
|
+
const data=Buffer.alloc(Number(before.size)+1);let at=0;
|
|
86
|
+
while(at<data.length){const count=readSync(fd,data,at,data.length-at,at);if(!count)break;at+=count;}
|
|
87
|
+
if(at!==Number(before.size)||stamp(fstatSync(fd,{bigint:true}))!==stamp(before)||stamp(fileState(file))!==stamp(before))fail();
|
|
88
|
+
const text=new TextDecoder("utf-8",{fatal:true}).decode(data.subarray(0,at)),companion=`${file}.pending-${hash(text)}`;
|
|
89
|
+
const paired=lstatSync(companion,{bigint:true,throwIfNoEntry:false});
|
|
90
|
+
if(before.nlink===2n){const peer=fileState(companion);if(peer.dev!==before.dev||peer.ino!==before.ino||stamp(peer)!==stamp(before))fail();}
|
|
91
|
+
else if(paired)fail();
|
|
92
|
+
return{text,check(){if(stamp(fileState(file))!==stamp(before))fail();
|
|
93
|
+
const now=lstatSync(companion,{bigint:true,throwIfNoEntry:false});
|
|
94
|
+
if(paired? !now||stamp(fileState(companion))!==stamp(paired):!!now)fail();}};
|
|
95
|
+
}finally{closeSync(fd);}
|
|
96
|
+
}
|
|
97
|
+
export interface SyncPlacementWriterRootsInput {epochHash:string;homeHash:string;activationHash:string;roots:readonly MeasuredSyncPlacementWriterRoot[]}
|
|
98
|
+
export interface SyncPlacementWriterRootsBinding {homeHash:string;epochHash:string;activationHash:string;writers:readonly SyncPlacementWriterAttestation[]}
|
|
99
|
+
function copiedInput(input:SyncPlacementWriterRootsInput) {
|
|
100
|
+
const copied=object(input,["epochHash","homeHash","activationHash","roots"]);
|
|
101
|
+
if(!hex(copied.epochHash)||!hex(copied.homeHash)||!hex(copied.activationHash))fail();
|
|
102
|
+
const roots=array(copied.roots).map(value=>{
|
|
103
|
+
const root=object(value,["root","writerId","version","pin"]);
|
|
104
|
+
if(typeof root.root!=="string"||typeof root.version!=="string"||!hex(root.writerId)||!hex(root.pin))fail();
|
|
105
|
+
return{root:root.root,writerId:root.writerId,version:root.version,pin:root.pin};
|
|
106
|
+
}).sort((a,b)=>a.root<b.root?-1:a.root>b.root?1:0);
|
|
107
|
+
return{homeHash:copied.homeHash,epochHash:copied.epochHash,activationHash:copied.activationHash,roots};
|
|
108
|
+
}
|
|
109
|
+
/** Pure initial-locator data plan. It proves framing/binding/bounds only, never fresh install bytes or permission. */
|
|
110
|
+
export function planSyncPlacementWriterRoots(input:SyncPlacementWriterRootsInput,binding:SyncPlacementWriterRootsBinding):Locator {
|
|
111
|
+
return protectedCall(()=>{
|
|
112
|
+
const copied=copiedInput(input),expected=object(binding,["homeHash","epochHash","activationHash","writers"]);
|
|
113
|
+
if(!hex(expected.homeHash)||!hex(expected.epochHash)||!hex(expected.activationHash))fail();
|
|
114
|
+
const writers=memoryPolicySnapshot(expected.writers) as readonly SyncPlacementWriterAttestation[]|undefined;if(!writers)fail();idSet(writers);
|
|
115
|
+
const proposed=copied.roots.map(({writerId,version,pin})=>({writerId,version,pin,publicationFormat:PLACEMENT_FORMAT,compatible:true})).sort((a,b)=>a.writerId.localeCompare(b.writerId));
|
|
116
|
+
if(canonicalJSON(proposed)!==canonicalJSON([...writers].sort((a,b)=>a.writerId.localeCompare(b.writerId))))fail();
|
|
117
|
+
const body:LocatorBody={schema:SYNC_PLACEMENT_WRITER_ROOTS_SCHEMA,homeHash:copied.homeHash,epochHash:copied.epochHash,activationHash:copied.activationHash,
|
|
118
|
+
roots:copied.roots.map(({root,writerId})=>({root,writerId}))};
|
|
119
|
+
const value=validate({...body,seal:hashJSON(body)});
|
|
120
|
+
bind(value,{homeHash:expected.homeHash,epochHash:expected.epochHash,activationHash:expected.activationHash},writers);return value;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export interface SyncPlacementWriterRootsWriteOptions {mutation:MutationHandle;assertCurrent():void}
|
|
124
|
+
/** Called only under activation's live fences. Initial bytes are immutable; renewal belongs to its own reviewed protocol. */
|
|
125
|
+
export function writeSyncPlacementWriterRoots(homeInput:string,input:SyncPlacementWriterRootsInput,expectedWriters:readonly SyncPlacementWriterAttestation[],options:SyncPlacementWriterRootsWriteOptions):void {
|
|
126
|
+
protectedCall(()=>{
|
|
127
|
+
const home=realPath(homeInput),before=directory(home);assertMutationHandle(options.mutation,home);options.assertCurrent();
|
|
128
|
+
const copied=copiedInput(input),roots=copied.roots;
|
|
129
|
+
const measured=measureSyncPlacementWriters(roots.map(root=>root.root));
|
|
130
|
+
if(canonicalJSON(measured.roots)!==canonicalJSON(roots.sort((a,b)=>a.root<b.root?-1:a.root>b.root?1:0)))fail();
|
|
131
|
+
const journal=readPlacementJournal(home,copied.epochHash),expected=memoryPolicySnapshot(expectedWriters);
|
|
132
|
+
if(!expected||!Array.isArray(expected))fail();idSet(expected);
|
|
133
|
+
const sorted=(writers:readonly SyncPlacementWriterAttestation[])=>[...writers].sort((a,b)=>a.writerId.localeCompare(b.writerId));
|
|
134
|
+
if(canonicalJSON(sorted(measured.writers))!==canonicalJSON(sorted(expected))||canonicalJSON(sorted(measured.writers))!==canonicalJSON(sorted(journal.activation.writers)))fail();
|
|
135
|
+
const value=planSyncPlacementWriterRoots({...copied,roots:measured.roots},{homeHash:journal.epoch.homeHash,epochHash:hashJSON(journal.epoch),activationHash:journal.activation.seal,writers:journal.activation.writers});
|
|
136
|
+
if(canonicalJSON(idSet(expected))!==canonicalJSON(idSet(journal.activation.writers)))fail();
|
|
137
|
+
assertMutationHandle(options.mutation,home);options.assertCurrent();
|
|
138
|
+
const file=join(home,SYNC_PLACEMENT_WRITER_ROOTS_FILE),text=canonicalJSON(value)+"\n";
|
|
139
|
+
const existing=lstatSync(file,{throwIfNoEntry:false});if(existing){const read=readOwned(file);if(read.text!==text)fail();read.check();}
|
|
140
|
+
// The shared writer reads a crash-left orphan before its own content check. Bound that read here,
|
|
141
|
+
// after the final caller proof; only its exact deterministic staging name can be resumed.
|
|
142
|
+
const staging=lstatSync(`${file}.pending-${hash(text)}`,{bigint:true,throwIfNoEntry:false});
|
|
143
|
+
if(staging){
|
|
144
|
+
if(!staging.isFile()||staging.isSymbolicLink()||staging.uid!==BigInt(process.getuid?.()??-1)||(staging.mode&0o777n)!==0o600n||staging.size>BigInt(MAX_BYTES))fail();
|
|
145
|
+
if(existing){const final=fileState(file);if(final.nlink!==2n||staging.nlink!==2n||stamp(staging)!==stamp(final))fail();}
|
|
146
|
+
else if(staging.nlink!==1n)fail();
|
|
147
|
+
}
|
|
148
|
+
writeImmutableActivationMetadata(file,text);
|
|
149
|
+
assertMutationHandle(options.mutation,home);options.assertCurrent();
|
|
150
|
+
const after=directory(home);if(after.dev!==before.dev||after.ino!==before.ino)fail();
|
|
151
|
+
const read=readOwned(file);if(read.text!==text)fail();read.check();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** Returns only initial locator paths; the caller must independently remeasure their actual bytes at each attestation. */
|
|
155
|
+
export function readInitialSyncPlacementWriterLocator(homeInput:string):InitialSyncPlacementWriterLocator {
|
|
156
|
+
return protectedCall(()=>{
|
|
157
|
+
const home=realPath(homeInput),before=directory(home);
|
|
158
|
+
const active=()=>{const markers=readPlacementRuntimeMarkers(home),value=readActivePlacementBinding(home,markers.profile,markers.shards);if(!value)fail();return value;};
|
|
159
|
+
const initial=active(),read=readOwned(join(home,SYNC_PLACEMENT_WRITER_ROOTS_FILE)),value=validate(JSON.parse(read.text));
|
|
160
|
+
if(canonicalJSON(value)+"\n"!==read.text)fail();
|
|
161
|
+
bind(value,{homeHash:initial.epoch.homeHash,epochHash:initial.profile.placementHead,activationHash:initial.activation.seal},initial.activation.writers);
|
|
162
|
+
const final=active();if(canonicalJSON(final)!==canonicalJSON(initial))fail();
|
|
163
|
+
const after=directory(home);if(after.dev!==before.dev||after.ino!==before.ino)fail();read.check();
|
|
164
|
+
return value;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
export function readSyncPlacementWriterRoots(homeInput:string):string[]{return readInitialSyncPlacementWriterLocator(homeInput).roots.map(root=>root.root);}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Read-only install measurement. Pins are exact-byte facts, not person authority or host quiescence. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, opendirSync, readSync, type BigIntStats } from "node:fs";
|
|
4
|
+
import { isAbsolute, join } from "node:path";
|
|
5
|
+
import { types } from "node:util";
|
|
6
|
+
import { hashJSON } from "./generation.ts";
|
|
7
|
+
import { realPath } from "./platform.ts";
|
|
8
|
+
import { PLACEMENT_FORMAT } from "./sync-placement-format.ts";
|
|
9
|
+
import type { SyncPlacementWriterAttestation } from "./sync-placement-plan.ts";
|
|
10
|
+
|
|
11
|
+
export interface MeasuredSyncPlacementWriterRoot { root: string; writerId: string; version: string; pin: string }
|
|
12
|
+
export interface SyncPlacementWriterMeasurement { roots: MeasuredSyncPlacementWriterRoot[]; writers: SyncPlacementWriterAttestation[] }
|
|
13
|
+
export type SyncPlacementWriterErrorCode = "invalid" | "unavailable" | "unsupported" | "bound" | "changed";
|
|
14
|
+
const causes: Record<SyncPlacementWriterErrorCode,string> = {
|
|
15
|
+
invalid:"invalid or duplicate writer-root inventory",unavailable:"unreadable or unsafe writer-root runtime files",
|
|
16
|
+
unsupported:"unsupported writer-root package identity or release",bound:"writer-root inventory exceeds its supported bound",changed:"writer-root changed during measurement",
|
|
17
|
+
};
|
|
18
|
+
export type SyncPlacementWriterFailureDetail = "below-minimum" | "wrong-package" | "writable-runtime" | "unreadable" | "invalid-version" | "invalid-inventory" | "unsafe-runtime" | "missing-runtime" | "bound" | "changed";
|
|
19
|
+
const details:Record<SyncPlacementWriterFailureDetail,string>={
|
|
20
|
+
"below-minimum":"release is below 0.21.0", "wrong-package":"package is not @staix/agent-mem", "writable-runtime":"runtime code is group- or world-writable",
|
|
21
|
+
unreadable:"runtime files are unreadable or missing", "invalid-version":"package version is not a supported plain release", "invalid-inventory":"root inventory is invalid or duplicated",
|
|
22
|
+
"unsafe-runtime":"runtime files have unsafe types or symlinks", "missing-runtime":"required bin/amem.js or src/index.ts is missing",bound:"inventory exceeds a supported bound",changed:"install changed during measurement",
|
|
23
|
+
};
|
|
24
|
+
export class SyncPlacementWriterError extends Error {
|
|
25
|
+
constructor(readonly code: SyncPlacementWriterErrorCode, readonly rootIndex?: number, readonly detail:SyncPlacementWriterFailureDetail=code==="unsupported"?"invalid-version":code==="invalid"?"invalid-inventory":code==="bound"?"bound":code==="changed"?"changed":"unreadable") {
|
|
26
|
+
super(`Sync placement ${causes[code]} (${details[detail]}); upgrade or repair the amem install and its host exact pin, then rerun amem migrate --sync-placement --dry-run. For a managed writer, rerun amem setup <target> --apply after upgrading.`);
|
|
27
|
+
this.name="SyncPlacementWriterError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Closed own data only: external error messages, accessors and proxies are never diagnostic authority. */
|
|
31
|
+
export function extractSyncPlacementWriterFailure(error:unknown):{code:SyncPlacementWriterErrorCode;detail:SyncPlacementWriterFailureDetail;rootIndex?:number}|undefined {
|
|
32
|
+
try{
|
|
33
|
+
if(!error||typeof error!=="object"||types.isProxy(error)||!(error instanceof SyncPlacementWriterError))return;
|
|
34
|
+
const fields=Object.getOwnPropertyDescriptors(error),code=fields.code?.value,detail=fields.detail?.value,index=fields.rootIndex;
|
|
35
|
+
if(typeof code!=="string"||!Object.hasOwn(causes,code)||typeof detail!=="string"||!Object.hasOwn(details,detail)
|
|
36
|
+
||index&&!("value"in index))return;
|
|
37
|
+
const rootIndex=index?.value;if(rootIndex!==undefined&&(!Number.isSafeInteger(rootIndex)||rootIndex<0||rootIndex>=256))return;
|
|
38
|
+
return{code:code as SyncPlacementWriterErrorCode,detail:detail as SyncPlacementWriterFailureDetail,...(rootIndex===undefined?{}:{rootIndex})};
|
|
39
|
+
}catch{return;}
|
|
40
|
+
}
|
|
41
|
+
const MAX_ROOTS=256,MAX_FILES=4096,MAX_BYTES=64*1024*1024,MAX_PACKAGE=64*1024,MAX_PATH=4096,MAX_NODES=8192;
|
|
42
|
+
// ponytail: namespace traversal also caps 8192 entries and depth64; larger installs need a reviewed chunked inventory.
|
|
43
|
+
const RUNTIME=["src","bin","hooks",".claude-plugin"] as const;
|
|
44
|
+
const stamp=(s:BigIntStats)=>[s.dev,s.ino,s.mode,s.uid,s.gid,s.nlink,s.size,s.mtimeNs,s.ctimeNs].map(String).join(":");
|
|
45
|
+
const digest=(bytes:Uint8Array)=>createHash("sha256").update(bytes).digest("hex");
|
|
46
|
+
|
|
47
|
+
/** Internal frozen-locator identity; callers validate canonical paths before treating it as a writer key. */
|
|
48
|
+
export function syncPlacementWriterId(root:string):string {return hashJSON(["agent-mem.sync-placement-writer/v1",root]);}
|
|
49
|
+
|
|
50
|
+
export function measureSyncPlacementWriters(input:readonly string[]):SyncPlacementWriterMeasurement {
|
|
51
|
+
let index:number|undefined;
|
|
52
|
+
function fail(code:SyncPlacementWriterErrorCode,detail?:SyncPlacementWriterFailureDetail):never {throw new SyncPlacementWriterError(code,index,detail);}
|
|
53
|
+
if(!input||typeof input!=="object"||types.isProxy(input)||!Array.isArray(input)||Object.getPrototypeOf(input)!==Array.prototype
|
|
54
|
+
||input.length<1||Object.getOwnPropertySymbols(input).length)fail("invalid");
|
|
55
|
+
if(input.length>MAX_ROOTS)fail("bound");
|
|
56
|
+
const fields=Object.getOwnPropertyDescriptors(input);
|
|
57
|
+
if(Object.keys(fields).length!==input.length+1)fail("invalid");
|
|
58
|
+
const requested:string[]=[],seen=new Set<string>(),checks:(()=>void)[]=[],namespaces:(()=>void)[]=[],measured:MeasuredSyncPlacementWriterRoot[]=[];
|
|
59
|
+
let files=0,bytes=0,nodes=0;
|
|
60
|
+
const rememberCheck=(check:()=>void)=>{const rootIndex=index;checks.push(()=>{index=rootIndex;check();});};
|
|
61
|
+
try {
|
|
62
|
+
const node=(file:string,directory:boolean):BigIntStats=>{
|
|
63
|
+
const s=lstatSync(file,{bigint:true});
|
|
64
|
+
// Public installs may be readable by everyone and share Bun's hardlinked cache. Writeable code is unsafe.
|
|
65
|
+
if(s.isSymbolicLink()||(directory?!s.isDirectory():!s.isFile()))fail("unavailable","unsafe-runtime");
|
|
66
|
+
if((s.mode&0o022n)!==0n)fail("unavailable","writable-runtime");
|
|
67
|
+
return s;
|
|
68
|
+
};
|
|
69
|
+
for(index=0;index<input.length;index++){
|
|
70
|
+
const field=fields[String(index)];if(!field||!("value"in field)||!field.enumerable)fail("invalid");
|
|
71
|
+
const value:unknown=field.value;
|
|
72
|
+
if(typeof value!=="string"||!isAbsolute(value)||Buffer.byteLength(value)>MAX_PATH||value.includes("\0"))fail("invalid");
|
|
73
|
+
requested.push(value);
|
|
74
|
+
}
|
|
75
|
+
for(index=0;index<requested.length;index++){
|
|
76
|
+
const requestedRoot=requested[index]!,root=realPath(requestedRoot);
|
|
77
|
+
if(Buffer.byteLength(root)>MAX_PATH||seen.has(root))fail("invalid");seen.add(root);
|
|
78
|
+
const rootStat=node(root,true),inventory:[string,string][]=[];
|
|
79
|
+
rememberCheck(()=>{if(realPath(requestedRoot)!==root||stamp(node(root,true))!==stamp(rootStat))fail("changed");});
|
|
80
|
+
const remember=(file:string,before:BigIntStats,directory:boolean)=>rememberCheck(()=>{if(stamp(node(file,directory))!==stamp(before))fail("changed");});
|
|
81
|
+
const path=(relative:string)=>{if(Buffer.byteLength(relative)>MAX_PATH||Buffer.byteLength(join(root,relative))>MAX_PATH)fail("bound");return join(root,relative);};
|
|
82
|
+
const read=(relative:string,maximum:number):Buffer=>{
|
|
83
|
+
const file=path(relative),before=node(file,false);
|
|
84
|
+
if(++files>MAX_FILES||before.size>BigInt(maximum)||before.size>BigInt(MAX_BYTES-bytes))fail("bound");
|
|
85
|
+
const fd=openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
|
|
86
|
+
try{
|
|
87
|
+
if(stamp(fstatSync(fd,{bigint:true}))!==stamp(before))fail("changed");
|
|
88
|
+
const data=Buffer.alloc(Number(before.size));let at=0;
|
|
89
|
+
while(at<data.length){const count=readSync(fd,data,at,data.length-at,at);if(count<=0)fail("changed");at+=count;}
|
|
90
|
+
if(stamp(fstatSync(fd,{bigint:true}))!==stamp(before)||stamp(node(file,false))!==stamp(before))fail("changed");
|
|
91
|
+
bytes+=data.length;inventory.push([relative,digest(data)]);remember(file,before,false);return data;
|
|
92
|
+
}finally{closeSync(fd);}
|
|
93
|
+
};
|
|
94
|
+
const visit=(relative:string,depth=0)=>{
|
|
95
|
+
if(depth>64)fail("bound");
|
|
96
|
+
const file=path(relative),before=node(file,true),names:string[]=[];remember(file,before,true);
|
|
97
|
+
const dir=opendirSync(file);
|
|
98
|
+
try{for(;;){const entry=dir.readSync();if(!entry)break;if(++nodes>MAX_NODES)fail("bound");names.push(entry.name);}}finally{dir.closeSync();}
|
|
99
|
+
if(stamp(node(file,true))!==stamp(before))fail("changed");
|
|
100
|
+
const rootIndex=index;
|
|
101
|
+
namespaces.push(()=>{
|
|
102
|
+
index=rootIndex;const current:string[]=[],reader=opendirSync(file);
|
|
103
|
+
try{for(;;){const entry=reader.readSync();if(!entry)break;if(current.length>=names.length)fail("changed");current.push(entry.name);}}finally{reader.closeSync();}
|
|
104
|
+
if(JSON.stringify(current.sort())!==JSON.stringify(names))fail("changed");
|
|
105
|
+
});
|
|
106
|
+
for(const name of names.sort()){
|
|
107
|
+
const child=`${relative}/${name}`,s=lstatSync(path(child),{bigint:true});
|
|
108
|
+
if(s.isSymbolicLink())fail("unavailable","unsafe-runtime");
|
|
109
|
+
if(s.isDirectory())visit(child,depth+1);else read(child,MAX_BYTES);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const packageBytes=read("package.json",MAX_PACKAGE);let pkg:unknown;
|
|
113
|
+
try{pkg=JSON.parse(new TextDecoder("utf-8",{fatal:true}).decode(packageBytes));}catch(error){if(error&&typeof error==="object"&&!types.isProxy(error)&&error instanceof SyncPlacementWriterError)throw error;fail("unsupported");}
|
|
114
|
+
const version=pkg&&typeof pkg==="object"&&!Array.isArray(pkg)?Object.getOwnPropertyDescriptor(pkg,"version")?.value:undefined;
|
|
115
|
+
const name=pkg&&typeof pkg==="object"?Object.getOwnPropertyDescriptor(pkg,"name")?.value:undefined;
|
|
116
|
+
const match=typeof version==="string"&&version.length<=64?/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.exec(version):null;
|
|
117
|
+
if(name!=="@staix/agent-mem")fail("unsupported","wrong-package");
|
|
118
|
+
if(!match||match[0]!==version)fail("unsupported","invalid-version");
|
|
119
|
+
const parts=match.slice(1).map(Number);if(!parts.every(Number.isSafeInteger))fail("unsupported","invalid-version");
|
|
120
|
+
if(parts[0]===0&&parts[1]!<21)fail("unsupported","below-minimum");
|
|
121
|
+
for(const relative of RUNTIME){
|
|
122
|
+
const file=path(relative),s=lstatSync(file,{bigint:true,throwIfNoEntry:false});
|
|
123
|
+
if(s)visit(relative);else rememberCheck(()=>{if(lstatSync(file,{throwIfNoEntry:false}))fail("changed");});
|
|
124
|
+
}
|
|
125
|
+
if(!inventory.some(([file])=>file==="bin/amem.js")||!inventory.some(([file])=>file==="src/index.ts"))fail("unsupported","missing-runtime");
|
|
126
|
+
inventory.sort(([a],[b])=>a<b?-1:a>b?1:0);
|
|
127
|
+
measured.push({root,writerId:syncPlacementWriterId(root),version:version as string,pin:hashJSON(inventory)});
|
|
128
|
+
}
|
|
129
|
+
for(const check of checks)check();
|
|
130
|
+
for(const check of namespaces)check();
|
|
131
|
+
measured.sort((a,b)=>a.root<b.root?-1:a.root>b.root?1:0);
|
|
132
|
+
const writers=measured.map<SyncPlacementWriterAttestation>(({writerId,version,pin})=>({writerId,version,pin,publicationFormat:PLACEMENT_FORMAT,compatible:true})).sort((a,b)=>a.writerId.localeCompare(b.writerId));
|
|
133
|
+
return{roots:measured,writers};
|
|
134
|
+
}catch(error){if(error&&typeof error==="object"&&!types.isProxy(error)&&error instanceof SyncPlacementWriterError)throw error;return fail("unavailable");}
|
|
135
|
+
}
|
package/src/memory/sync.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { syncPlacementMemory } from "./sync-placement-transport.ts";
|
|
2
1
|
import type { PlacementCapturePort } from "./sync-placement-capture.ts";
|
|
3
2
|
import { placementRuntimeRefusal } from "./sync-placement-guard.ts";
|
|
4
3
|
import { assertFormatFloor } from "./formats.ts";
|
|
@@ -238,7 +237,7 @@ export async function syncMemory(store: MemoryStore, opts: SyncOptions = {}): Pr
|
|
|
238
237
|
assertFormatFloor(store.profile.home, "sync");
|
|
239
238
|
const placementRefusal = placementRuntimeRefusal(store.profile.home, store.profile);
|
|
240
239
|
if (placementRefusal) return { ok: false, pushed: false, entries: 0, error: placementRefusal };
|
|
241
|
-
if (store.profile.publicationFormat === "agent-mem.sync-placement/v1") return syncPlacementMemory(store, opts);
|
|
240
|
+
if (store.profile.publicationFormat === "agent-mem.sync-placement/v1") return (await import("./sync-placement-transport.ts")).syncPlacementMemory(store, opts);
|
|
242
241
|
if (store.profile.sharded) {
|
|
243
242
|
try {
|
|
244
243
|
// Refresh only through the existing proved monotone profile rebind before pinning this attempt.
|
package/src/setup-toml.ts
CHANGED
|
@@ -37,7 +37,7 @@ function inline(value: unknown): string {
|
|
|
37
37
|
// Bun emits scalars and many arrays as one assignment. Reuse that literal when it is safe in this scope.
|
|
38
38
|
// Objects/AOTs may become headers; those need the inline form below to avoid changing subsequent table scope.
|
|
39
39
|
let rendered = "";
|
|
40
|
-
try { rendered = Bun.TOML.stringify({ setup_value: value }); } catch { /* The validated inline encoder covers values the built-in writer cannot emit. */ }
|
|
40
|
+
try { rendered = Bun.TOML.stringify({ setup_value: value }) ?? ""; } catch { /* The validated inline encoder covers values the built-in writer cannot emit. */ }
|
|
41
41
|
const literal = /^setup_value\s*=\s*([^\r\n]*)\r?\n?$/.exec(rendered)?.[1];
|
|
42
42
|
if (literal !== undefined && isDeepStrictEqual(shape(parse(rendered).setup_value), shape(value))) return literal;
|
|
43
43
|
const encode = (part: unknown): string => {
|
|
@@ -255,7 +255,8 @@ function arrayEdit(before: readonly unknown[], after: readonly unknown[]): { kee
|
|
|
255
255
|
function appendedTables(path: readonly string[], values: readonly unknown[], eol: string): string {
|
|
256
256
|
if (!values.every(plain)) return error();
|
|
257
257
|
const document: Record<string, unknown> = {}; edit(document, path, [...values]);
|
|
258
|
-
let source: string; try { source = Bun.TOML.stringify(document); } catch { return error(); }
|
|
258
|
+
let source: string | undefined; try { source = Bun.TOML.stringify(document); } catch { return error(); }
|
|
259
|
+
if (source === undefined) return error();
|
|
259
260
|
const sourceUnits = statements(source), rows = sourceUnits.filter(unit => prefix(path, unit.path));
|
|
260
261
|
if (rows.filter(unit => unit.header && unit.array && unit.path.length === path.length).length !== values.length) return error();
|
|
261
262
|
return rows.map(unit => source.slice(unit.start, unit.end)).join("").replace(/\r?\n/g, eol);
|