@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +27 -1
- package/package.json +1 -1
- package/src/cli.ts +34 -4
- package/src/commands-table.ts +3 -0
- package/src/index.ts +6 -0
- package/src/memory/commands.ts +3 -2
- package/src/memory/public-prune-activation.ts +4 -4
- package/src/memory/shard-sync.ts +1 -2
- 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 +12 -6
- 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.ts +101 -14
- package/src/sync-placement-writer-context.ts +136 -0
|
@@ -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.ts
CHANGED
|
@@ -10,11 +10,11 @@ import { JCODE_CAPTURE_EVENTS, putJcodeHooks } from "./setup-jcode.ts";
|
|
|
10
10
|
const jcodeHookCommand = (bun: string, entry: string) => `${quote(bun)} --smol ${quote(entry)} hook jcode`;
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
-
import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, opendirSync, readFileSync, readSync, renameSync, rmdirSync, rmSync, writeFileSync, type BigIntStats } from "node:fs";
|
|
14
14
|
import { homedir, tmpdir } from "node:os";
|
|
15
15
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { isDeepStrictEqual } from "node:util";
|
|
17
|
+
import { isDeepStrictEqual, types } from "node:util";
|
|
18
18
|
import { HOOK_EVENTS } from "./hook-contract.ts";
|
|
19
19
|
import { memoryHome } from "./memory/home.ts";
|
|
20
20
|
import { label, PLAIN, singleLine, type Style } from "./memory/style.ts";
|
|
@@ -209,20 +209,16 @@ function assertOpencodeLayout(c: ReturnType<typeof context>, mode: SetupPlan["mo
|
|
|
209
209
|
}
|
|
210
210
|
function intent(c: ReturnType<typeof context>, target: SetupTarget): string { return digest([1, target, c.paths[target], c.bun, c.entry, ...(["codex", "kimi", "qwen"].includes(target) ? ["capture-json-v1"] : []), ...(target === "jcode" ? ["jcode-hooks-v1"] : []), ...(["claude", "codex", "kimi", "pi", "qwen", "jcode"].includes(target) ? ["root-hooks-v2"] : []), ...(target === "agy" ? [c.workspace!.root] : [])]); }
|
|
211
211
|
function journal(c: ReturnType<typeof context>, target: SetupTarget): string { return join(c.state, target === "agy" ? `agy-${digest(c.workspace!.root)}.json` : `${target}.json`); }
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
const value = record(JSON.parse(Buffer.from(raw.bytes, "base64").toString("utf8")));
|
|
212
|
+
function decodeReceipt(value: Record<string, unknown>, target: SetupTarget, count: number, strict = false): Receipt {
|
|
213
|
+
if (strict && Object.keys(value).some(key => !["version", "target", "intent", "phase", "changes", "createdDirectory"].includes(key))) throw Error("setup receipt invalid");
|
|
215
214
|
if (value.version !== 1 && value.version !== 2) throw Error("setup receipt version is unsupported; run setup or undo with the release that wrote it");
|
|
216
|
-
if (value.target !== target || typeof value.intent !== "string" || !/^[a-f0-9]{64}$/.test(value.intent) ||
|
|
215
|
+
if (value.target !== target || typeof value.intent !== "string" || !/^[a-f0-9]{64}$/.test(value.intent) || strict && value.intent.length !== 64 || !["prepared", "applied"].includes(String(value.phase)) || !Array.isArray(value.changes) || value.changes.length !== count && !(target === "jcode" && value.changes.length === 1)) throw Error("setup receipt does not match this installation");
|
|
217
216
|
for (let i = 0; i < value.changes.length; i++) {
|
|
218
|
-
const change = record(value.changes[i]);
|
|
219
|
-
if (target
|
|
220
|
-
&& resolveSetupPaths({ ...c.env, CLAUDE_CONFIG_DIR: dirname(change.path) }, c.home).redirects.some(row => row.from === change.path && row.to === currentPath))
|
|
221
|
-
throw Error("setup receipt was written from an Orca Claude profile; rerun setup from that Orca profile");
|
|
222
|
-
if (change.path !== currentPath && !legacyRedirect || change.key !== `${target}:${i}`) throw Error("setup receipt target mismatch");
|
|
217
|
+
const change = record(value.changes[i]);
|
|
218
|
+
if (change.key !== `${target}:${i}` || strict && (Object.keys(change).some(key => !["key", "path", "before", "after", "start"].includes(key)) || typeof change.path !== "string" || !isAbsolute(change.path) || change.path.length > 4096 || change.path.includes("\0"))) throw Error("setup receipt target mismatch");
|
|
223
219
|
for (const key of ["before", "after", ...(change.start !== undefined ? ["start"] : [])]) {
|
|
224
220
|
const v = record(change[key]);
|
|
225
|
-
if (!Number.isInteger(v.mode) || Number(v.mode) < 0 || Number(v.mode) > 0o777 || v.bytes !== null && (typeof v.bytes !== "string" || v.bytes.length > 2 * MAX || Buffer.from(v.bytes, "base64").toString("base64") !== v.bytes)) throw Error("setup receipt invalid");
|
|
221
|
+
if (strict && Object.keys(v).sort().join() !== "bytes,mode" || !Number.isInteger(v.mode) || Number(v.mode) < 0 || Number(v.mode) > 0o777 || v.bytes !== null && (typeof v.bytes !== "string" || v.bytes.length > 2 * MAX || strict && (v.bytes.length > Math.ceil(MAX / 3) * 4 || v.bytes.length / 4 * 3 - (v.bytes.endsWith("==") ? 2 : v.bytes.endsWith("=") ? 1 : 0) > MAX) || Buffer.from(v.bytes, "base64").toString("base64") !== v.bytes)) throw Error("setup receipt invalid");
|
|
226
222
|
}
|
|
227
223
|
}
|
|
228
224
|
if (value.createdDirectory !== undefined) {
|
|
@@ -231,6 +227,19 @@ function receipt(c: ReturnType<typeof context>, target: SetupTarget, requireRunt
|
|
|
231
227
|
}
|
|
232
228
|
return value as unknown as Receipt;
|
|
233
229
|
}
|
|
230
|
+
function receipt(c: ReturnType<typeof context>, target: SetupTarget, requireRuntime = false): Receipt | undefined {
|
|
231
|
+
const raw = read(journal(c, target), JOURNAL_MAX); if (raw.bytes === null) return;
|
|
232
|
+
const value = decodeReceipt(record(JSON.parse(Buffer.from(raw.bytes, "base64").toString("utf8"))), target, c.paths[target].length);
|
|
233
|
+
if (requireRuntime && value.intent !== intent(c, target)) throw Error("setup receipt does not match this installation");
|
|
234
|
+
for (let i = 0; i < value.changes.length; i++) {
|
|
235
|
+
const change = value.changes[i]!, currentPath = c.paths[target][i], legacyRedirect = c.redirects.some(row => row.target === target && row.from === change.path && row.to === currentPath);
|
|
236
|
+
if (target === "claude" && i === 0 && typeof change.path === "string" && change.path !== currentPath && !legacyRedirect
|
|
237
|
+
&& resolveSetupPaths({ ...c.env, CLAUDE_CONFIG_DIR: dirname(change.path) }, c.home).redirects.some(row => row.from === change.path && row.to === currentPath))
|
|
238
|
+
throw Error("setup receipt was written from an Orca Claude profile; rerun setup from that Orca profile");
|
|
239
|
+
if (change.path !== currentPath && !legacyRedirect) throw Error("setup receipt target mismatch");
|
|
240
|
+
}
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
234
243
|
function plannedHooks(c: ReturnType<typeof context>, target: SetupTarget): PlannedHook[] {
|
|
235
244
|
if (target === "jcode") return JCODE_CAPTURE_EVENTS.map(event => ({ event, command: jcodeHookCommand(c.bun, c.entry), timeout: 1 }));
|
|
236
245
|
if (target === "opencode" || target === "gemini" || target === "agy") return [];
|
|
@@ -407,9 +416,14 @@ function previousRuntimes(target: SetupTarget, images: readonly Image[], bun: st
|
|
|
407
416
|
return [...found.values()];
|
|
408
417
|
}
|
|
409
418
|
/** The runtime of a generated `<bun> --smol <entry> mcp` server record, if the record is exactly that shape. */
|
|
410
|
-
function launcher(value: unknown): { bun: string; entry: string } | undefined {
|
|
419
|
+
function launcher(value: unknown, target?: SetupTarget): { bun: string; entry: string } | undefined {
|
|
411
420
|
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
412
|
-
const server = value as Record<string, unknown
|
|
421
|
+
const server = value as Record<string, unknown>;
|
|
422
|
+
if (target === "opencode") {
|
|
423
|
+
if (server.type !== "local" || !Array.isArray(server.command) || server.command.length !== 4) return;
|
|
424
|
+
return launcher({ command: server.command[0], args: server.command.slice(1) });
|
|
425
|
+
}
|
|
426
|
+
const args = server.args;
|
|
413
427
|
return typeof server.command === "string" && isAbsolute(server.command) && Array.isArray(args) && args.length === 3
|
|
414
428
|
&& args[0] === "--smol" && typeof args[1] === "string" && isAbsolute(args[1]) && args[2] === "mcp" ? { bun: server.command, entry: args[1] } : undefined;
|
|
415
429
|
}
|
|
@@ -720,3 +734,76 @@ export async function runSetup(target: string, mode: SetupPlan["mode"], options:
|
|
|
720
734
|
const result = mode === "apply" ? applySetup(plan, options) : undoSetup(plan, options);
|
|
721
735
|
return options.json ? JSON.stringify(result, null, 2) : setupResultText(result, s);
|
|
722
736
|
}
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
/** Fixed safe category only; receipt text, native paths and external error messages are never diagnostics. */
|
|
740
|
+
export class SetupWriterRootsError extends Error {
|
|
741
|
+
readonly code = "setup_writer_roots_unavailable";
|
|
742
|
+
constructor(readonly category: "unsafe" | "changed" | "bound" | "unsupported" | "pending" | "unavailable") {
|
|
743
|
+
super(`Setup writer inventory ${category}; have the person inspect setup receipts and rerun setup before activation.`);
|
|
744
|
+
this.name = "SetupWriterRootsError";
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/** Current managed MCP AFTER images only. Native configuration paths and historical BEFORE/START images are never opened. */
|
|
748
|
+
export function readSetupWriterRoots(home: string): string[] {
|
|
749
|
+
function fail(category: SetupWriterRootsError["category"]): never { throw new SetupWriterRootsError(category); }
|
|
750
|
+
try {
|
|
751
|
+
const owned = (stat: BigIntStats) => { if (stat.isSymbolicLink() || stat.uid !== BigInt(process.getuid?.() ?? -1) || (stat.mode & 0o077n) !== 0n) fail("unsafe"); };
|
|
752
|
+
const stamp = (stat: BigIntStats) => JSON.stringify([stat.dev, stat.ino, stat.uid, stat.mode, stat.nlink, stat.size, stat.mtimeNs, stat.ctimeNs].map(String));
|
|
753
|
+
const base = lstatSync(home, { bigint: true, throwIfNoEntry: false }); if (!base) return []; owned(base); if (!base.isDirectory()) fail("unsafe");
|
|
754
|
+
const directory = join(home, "setup"), initial = lstatSync(directory, { bigint: true, throwIfNoEntry: false });
|
|
755
|
+
if (!initial) return [];
|
|
756
|
+
owned(initial); if (!initial.isDirectory()) fail("unsafe");
|
|
757
|
+
// ponytail:256 setup namespace entries and doctor's existing32MiB aggregate receipt-byte ceiling.
|
|
758
|
+
// A reviewed metadata-only setup inventory is required before increasing either ceiling.
|
|
759
|
+
const files: { name: string; stat: BigIntStats }[] = [], namespace: string[] = [], iterator = opendirSync(directory);
|
|
760
|
+
let entries = 0, totalBytes = 0;
|
|
761
|
+
try { for (;;) {
|
|
762
|
+
const entry = iterator.readSync(); if (!entry) break;
|
|
763
|
+
namespace.push(entry.name);
|
|
764
|
+
if (++entries > 256) fail("bound"); if (entry.name === "setup.lock") fail("pending");
|
|
765
|
+
if (!entry.name.endsWith(".json")) continue; // Retained .json.stale-* and unregistered temps are historical, never current authority.
|
|
766
|
+
const stat = lstatSync(join(directory, entry.name), { bigint: true }); owned(stat); if (!stat.isFile() || stat.nlink !== 1n) fail("unsafe");
|
|
767
|
+
if (stat.size > BigInt(JOURNAL_MAX) || totalBytes + Number(stat.size) > 32 * 1024 * 1024) fail("bound");
|
|
768
|
+
totalBytes += Number(stat.size); files.push({ name: entry.name, stat });
|
|
769
|
+
} } finally { iterator.closeSync(); }
|
|
770
|
+
const roots = new Set<string>(), launchers: { entry: string; root: string }[] = [];
|
|
771
|
+
for (const file of files.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
772
|
+
const target = (file.name.startsWith("agy-") && /^agy-[a-f0-9]{64}\.json$/.test(file.name) ? "agy" : file.name.slice(0, -5)) as SetupTarget;
|
|
773
|
+
if (!SETUP_TARGETS.includes(target) || target === "agy" && !/^agy-[a-f0-9]{64}\.json$/.test(file.name)) fail("unsupported");
|
|
774
|
+
const path = join(directory, file.name), fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
775
|
+
let text: string;
|
|
776
|
+
try {
|
|
777
|
+
const opened = fstatSync(fd, { bigint: true }); owned(opened); if (!opened.isFile() || opened.nlink !== 1n || stamp(opened) !== stamp(file.stat)) fail("changed");
|
|
778
|
+
const data = Buffer.alloc(Number(opened.size) + 1); let count = 0;
|
|
779
|
+
while (count < data.length) { const n = readSync(fd, data, count, data.length - count, count); if (!n) break; count += n; }
|
|
780
|
+
if (count !== Number(opened.size) || stamp(fstatSync(fd, { bigint: true })) !== stamp(opened) || stamp(lstatSync(path, { bigint: true })) !== stamp(opened)) fail("changed");
|
|
781
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(0, count));
|
|
782
|
+
} finally { closeSync(fd); }
|
|
783
|
+
const counts: Record<SetupTarget, number> = { claude: 2, codex: 2, kimi: 2, pi: 2, qwen: 1, jcode: 2, opencode: 1, gemini: 1, agy: 1 };
|
|
784
|
+
let value: Receipt;
|
|
785
|
+
try { value = decodeReceipt(record(JSON.parse(text)), target, counts[target], true); } catch { return fail("unsupported"); }
|
|
786
|
+
if (value.phase !== "applied") fail("pending");
|
|
787
|
+
const runtime = launcher(mcpRecord(target, value.changes[0]!.after), target);
|
|
788
|
+
if (!runtime || runtime.bun.includes("\0") || runtime.entry.length > 4096 || runtime.entry.includes("\0") || !["/bin/amem.js", "/src/cli.ts"].some(suffix => runtime.entry.endsWith(suffix))
|
|
789
|
+
|| target === "opencode" && [runtime.bun, runtime.entry].some(value => /\{(?:env|file):/.test(value))) fail("unsupported");
|
|
790
|
+
const root = realPath(dirname(dirname(runtime.entry))); if (!lstatSync(root).isDirectory()) fail("unavailable");
|
|
791
|
+
roots.add(root); launchers.push({ entry: runtime.entry, root });
|
|
792
|
+
}
|
|
793
|
+
const finalHome = lstatSync(home, { bigint: true }); owned(finalHome);
|
|
794
|
+
if (!finalHome.isDirectory() || stamp(lstatSync(directory, { bigint: true })) !== stamp(initial) || finalHome.dev !== base.dev || finalHome.ino !== base.ino) fail("changed");
|
|
795
|
+
const finalNames: string[] = [], finalDirectory = opendirSync(directory);
|
|
796
|
+
try { for (;;) { const entry = finalDirectory.readSync(); if (!entry) break; if (finalNames.length >= namespace.length) fail("changed"); finalNames.push(entry.name); } }
|
|
797
|
+
finally { finalDirectory.closeSync(); }
|
|
798
|
+
if (JSON.stringify(finalNames.sort()) !== JSON.stringify(namespace.sort())) fail("changed");
|
|
799
|
+
for (const file of files) if (stamp(lstatSync(join(directory, file.name), { bigint: true })) !== stamp(file.stat)) fail("changed");
|
|
800
|
+
for (const runtime of launchers) if (realPath(dirname(dirname(runtime.entry))) !== runtime.root) fail("changed");
|
|
801
|
+
return [...roots].sort();
|
|
802
|
+
} catch (error) {
|
|
803
|
+
if (error && typeof error === "object" && !types.isProxy(error) && error instanceof SetupWriterRootsError) {
|
|
804
|
+
const category = Object.getOwnPropertyDescriptor(error, "category");
|
|
805
|
+
if (category && "value" in category && ["unsafe", "changed", "bound", "unsupported", "pending", "unavailable"].includes(category.value)) throw new SetupWriterRootsError(category.value);
|
|
806
|
+
}
|
|
807
|
+
throw new SetupWriterRootsError("unavailable");
|
|
808
|
+
}
|
|
809
|
+
}
|