agent-policy-kit 0.0.0-stage → 0.2.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.
@@ -0,0 +1,582 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { isDeepStrictEqual } from 'node:util';
5
+ import { constants } from 'node:fs';
6
+ import { validatePolicy, rolePreference } from './policy.mjs';
7
+ import {ROLE_IDS,LEGACY_ROLE_IDS,selectedRoleIds} from './roles.mjs';
8
+ import { PACKAGE_VERSION } from './release.mjs';
9
+ import catalog from '../models/catalog.json' with {type:'json'};
10
+ import { parseToml, setTomlValue, restoreTomlValue, removeTomlValue } from './toml.mjs';
11
+ import { nativeLocations, nativeRoles, instructionBlock, nativeWarnings, BEGIN, END } from './native.mjs';
12
+
13
+ const CLIENTS = ['codex', 'claude', 'kimi', 'agy', 'grok'];
14
+ const LIMIT = 1024 * 1024;
15
+ const digest = value => value === null ? null : createHash('sha256').update(value).digest('hex');
16
+ const json = value => JSON.stringify(value, null, 2) + '\n';
17
+ const equal = isDeepStrictEqual;
18
+ const own = (o, k) => Object.hasOwn(o, k);
19
+ const canonical = value => Array.isArray(value) ? value.map(canonical) : object(value) ? Object.fromEntries(Object.keys(value).sort().map(k => [k,canonical(value[k])])) : value;
20
+ function fail(code) { throw Object.assign(new Error(code), {code}); }
21
+ function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
22
+ function absolute(value) { if (typeof value !== 'string' || !path.isAbsolute(value) || value.includes('\0')) fail('UNSAFE_PATH'); return path.resolve(value); }
23
+ function within(base, target) { const relative = path.relative(base, target); return !relative.startsWith('..' + path.sep) && relative !== '..' && !path.isAbsolute(relative); }
24
+ function locations(client,home,stateDir) {
25
+ return {...nativeLocations(client,home),receipt:path.join(stateDir,'installations',client+'.json'),policy:path.join(stateDir,'policy.json')};
26
+ }
27
+ function nativeTargets(l,selectedRoles=ROLE_IDS) { return [...(l.config?[l.config]:[]),l.instructions,...l.roles.filter((_,i)=>selectedRoles.includes(ROLE_IDS[i]))]; }
28
+ async function statOrNull(file) { try { return await fs.lstat(file); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } }
29
+ async function safePath(file, base) {
30
+ if (!within(base, file)) fail('UNSAFE_PATH');
31
+ const root = await statOrNull(base);
32
+ if (root && (!root.isDirectory() || root.isSymbolicLink())) fail('UNSAFE_PATH');
33
+ let at = base;
34
+ for (const part of path.relative(base, file).split(path.sep).filter(Boolean)) {
35
+ at = path.join(at, part);
36
+ const s = await statOrNull(at);
37
+ if (s && (s.isSymbolicLink() || (at !== file && !s.isDirectory()) || (at === file && !s.isFile() && !s.isDirectory()))) fail('UNSAFE_PATH');
38
+ }
39
+ }
40
+ async function read(file, base) {
41
+ await safePath(file, base);
42
+ const s = await statOrNull(file);
43
+ if (!s) return {text:null,mode:0o600};
44
+ if (!s.isFile() || s.size > LIMIT) fail('UNSAFE_FILE');
45
+ const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
46
+ try {
47
+ const actual = await handle.stat();
48
+ if (!actual.isFile() || actual.ino !== s.ino || actual.dev !== s.dev || actual.size > LIMIT) fail('UNSAFE_FILE');
49
+ const text = await handle.readFile('utf8');
50
+ if (Buffer.byteLength(text) > LIMIT) fail('UNSAFE_FILE');
51
+ return {text,mode:s.mode & 0o777};
52
+ } finally { await handle.close(); }
53
+ }
54
+ async function readDependency(plan, file, base) {
55
+ const before = await read(file,base);
56
+ const snapshot = {path:file,base,beforeHash:digest(before.text),beforeMode:before.mode};
57
+ const earlier = plan.dependencies.find(d => d.path === file);
58
+ if (earlier && (earlier.beforeHash !== snapshot.beforeHash || earlier.beforeMode !== snapshot.beforeMode)) fail('STALE_PLAN');
59
+ if (!earlier) plan.dependencies.push(snapshot);
60
+ return before;
61
+ }
62
+ // JSON.parse validates grammar; this token pass additionally refuses duplicate object keys.
63
+ function parseJson(text, code = 'INVALID_JSON') {
64
+ try {
65
+ if (typeof text !== 'string' || Buffer.byteLength(text) > LIMIT) fail(code);
66
+ const result = JSON.parse(text);
67
+ const stack = [];
68
+ for (const token of text.matchAll(/"(?:[^"\\]|\\.)*"|[{}\[\],:]/g)) {
69
+ const t = token[0];
70
+ if (t === '{') stack.push({keys:new Set(),key:true});
71
+ else if (t === '[') stack.push(null);
72
+ else if (t === '}' || t === ']') stack.pop();
73
+ else if (t === ',') { if (stack.at(-1)) stack.at(-1).key = true; }
74
+ else if (t === ':') { if (stack.at(-1)) stack.at(-1).key = false; }
75
+ else if (stack.at(-1)?.key) {
76
+ const value = JSON.parse(t);
77
+ if (stack.at(-1).keys.has(value)) fail(code);
78
+ stack.at(-1).keys.add(value);
79
+ }
80
+ }
81
+ return result;
82
+ } catch { fail(code); }
83
+ }
84
+ function configJson(text) { const value = text === null ? {} : parseJson(text); if (!object(value)) fail('UNSUPPORTED_CONFIG'); return value; }
85
+ function ensureObject(parent,key) {
86
+ if(!own(parent,key))parent[key]={};
87
+ if(!object(parent[key]))fail('UNSUPPORTED_CONFIG');
88
+ return parent[key];
89
+ }
90
+ function jsonInstall(text,maxConcurrent,defaultModel) {
91
+ const value=configJson(text),ops=[],created=[];
92
+ if(!own(value,'env'))created.push(['env']);
93
+ const env=ensureObject(value,'env');
94
+ for(const [key,after] of [['CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS',String(maxConcurrent)],['CLAUDE_CODE_SUBAGENT_MODEL',defaultModel]]) {
95
+ if(own(env,key)&&typeof env[key]!=='string')fail('UNSUPPORTED_CONFIG');
96
+ ops.push({kind:'value',path:['env',key],existed:own(env,key),before:env[key]??null,after});env[key]=after;
97
+ }
98
+ return {text:json(value),ops,created};
99
+ }
100
+ function atPath(value, keys) {
101
+ for (const key of keys) { if (!object(value) || !own(value,key)) fail('OWNED_DRIFT'); value = value[key]; }
102
+ return value;
103
+ }
104
+ function undoJson(text, file) {
105
+ const value = configJson(text);
106
+ for (const op of file.ops.toReversed()) {
107
+ const parent = atPath(value,op.path.slice(0,-1)), key = op.path.at(-1);
108
+ if (!equal(parent[key],op.after)) fail('OWNED_DRIFT');
109
+ if (op.existed) parent[key] = op.before; else delete parent[key];
110
+ }
111
+ for (const keys of file.created.toReversed()) {
112
+ const parent = atPath(value,keys.slice(0,-1)), key = keys.at(-1);
113
+ if ((object(parent[key]) && Object.keys(parent[key]).length === 0) || (Array.isArray(parent[key]) && parent[key].length === 0)) delete parent[key];
114
+ }
115
+ return !file.existed && Object.keys(value).length === 0 ? null : json(value);
116
+ }
117
+ function allowedBases(plan) { return target => within(plan.stateDir,target) ? plan.stateDir : plan.home; }
118
+ async function recoveryRequired(stateDir) {
119
+ const pending = path.join(stateDir,'pending.json');
120
+ await safePath(pending,stateDir);
121
+ if (await statOrNull(pending)) fail('RECOVERY_REQUIRED');
122
+ const lock = await read(path.join(stateDir,'.install.lock'),stateDir);
123
+ if (lock.text !== null && /^[a-f0-9-]{36}$/.test(lock.text)) {
124
+ const index = path.join(stateDir,'backups',lock.text,'index.json');
125
+ await safePath(index,stateDir);
126
+ if (await statOrNull(index)) fail('RECOVERY_REQUIRED');
127
+ }
128
+ }
129
+ async function context({client,home,stateDir}) {
130
+ if (!CLIENTS.includes(client)) fail('INVALID_CLIENT');
131
+ home = absolute(home); stateDir = absolute(stateDir);
132
+ if (home === stateDir || within(stateDir,home)) fail('UNSAFE_PATH');
133
+ await safePath(home,path.parse(home).root);
134
+ await safePath(stateDir,path.parse(stateDir).root);
135
+ const homeStat = await statOrNull(home);
136
+ if (!homeStat?.isDirectory() || homeStat.isSymbolicLink()) fail('UNSAFE_PATH');
137
+ // Existing ancestors of the state root are checked before creating any private tree.
138
+ let anchor = within(home,stateDir) ? home : path.dirname(stateDir);
139
+ while (!(await statOrNull(anchor))) anchor = path.dirname(anchor);
140
+ await safePath(stateDir,anchor);
141
+ const result = {schemaVersion:1,client,home,stateDir,warnings:[],changes:[],dependencies:[]};
142
+ const l = locations(client,home,stateDir);
143
+ await recoveryRequired(stateDir);
144
+ const receipt = await readDependency(result,l.receipt,stateDir);
145
+ return {result,l,receipt};
146
+ }
147
+ function change(plan,target,before,after,mode=0o600) {
148
+ if (before.text !== after) plan.changes.push({path:target,before:before.text,after,beforeHash:digest(before.text),afterHash:digest(after),mode,beforeMode:before.mode});
149
+ }
150
+ function effectiveRoles(preferences,selectedRoles=ROLE_IDS) {return selectedRoles.map(role=>({role,...rolePreference(preferences,role)}));}
151
+ function selectedAliases(preferences,selectedRoles=ROLE_IDS) {
152
+ const aliases=new Map();
153
+ for(const selected of effectiveRoles(preferences,selectedRoles))if(!aliases.has(selected.model)||selected.effort!==null)aliases.set(selected.model,selected);
154
+ return [...aliases.values()];
155
+ }
156
+ function tomlSettings(client,preferences,maxConcurrent,selectedRoles=ROLE_IDS) {
157
+ const worker=rolePreference(preferences,selectedRoles[0]);
158
+ if(client==='codex')return [[['agents','max_concurrent_threads_per_session'],maxConcurrent],[['agents','default_subagent_model'],worker.model],...(worker.effort===null?[]:[[['agents','default_subagent_reasoning_effort'],worker.effort]])];
159
+ if(client==='grok')return [[['subagents','max_concurrent'],maxConcurrent],...selectedRoles.map(role=>[['subagents','models','apk-'+role],rolePreference(preferences,role).model]),[['subagents','models','general-purpose'],worker.model]];
160
+ if(client!=='kimi')return [];
161
+ const selected=selectedAliases(preferences,selectedRoles),roles=effectiveRoles(preferences,selectedRoles);
162
+ return [[['secondary_model','default_model'],worker.model],...selected.map(value=>[['secondary_model','models',value.model],'Use for '+roles.filter(role=>role.model===value.model).map(role=>'apk-'+role.role).join(', ')+'.']),...selected.filter(value=>value.effort!==null).map(value=>[['models',value.model,'overrides','default_effort'],value.effort])];
163
+ }
164
+ const HASH=/^[a-f0-9]{64}$/;
165
+ const validHash=value=>typeof value==='string'&&HASH.test(value);
166
+ const VERSION=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
167
+ const SOURCES=['release','custom','saved'];
168
+ const validPreset=value=>value===null||(typeof value==='string'&&/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value));
169
+ function version(value) {return typeof value==='string'&&value.length<=128&&VERSION.test(value);}
170
+ function fields(value,allowed) {if(!object(value)||Object.keys(value).some(key=>!allowed.includes(key)))fail('INVALID_RECEIPT');}
171
+ function literalValue(raw,type,nullable=false) {
172
+ if(raw===null&&nullable)return null;
173
+ if(typeof raw!=='string'||raw.length>8192)fail('INVALID_RECEIPT');
174
+ let value;
175
+ try {const parsed=parseToml(`value = ${raw}\n`);if(Object.keys(parsed).length!==1)fail('INVALID_RECEIPT');value=parsed.value;}catch {fail('INVALID_RECEIPT');}
176
+ if(typeof value!==type||(type==='number'&&!Number.isSafeInteger(value))||(type==='string'&&value.length>4096))fail('INVALID_RECEIPT');
177
+ return value;
178
+ }
179
+ function alias(value) {return typeof value==='string'&&/^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/.test(value);}
180
+ function scalar(op,target,type) {
181
+ fields(op,['kind','path','before','after','addedTable']);
182
+ if(op.kind!=='scalar'||!equal(op.path,target)||(op.addedTable&&!equal(op.addedTable,target.slice(0,-1))))fail('INVALID_RECEIPT');
183
+ literalValue(op.before,type,true);
184
+ return literalValue(op.after,type);
185
+ }
186
+ function validateTomlSnapshot(file,client,schemaVersion,selectedRoles=ROLE_IDS) {
187
+ if(!Array.isArray(file.ops)||!file.ops.every(op=>object(op)&&Array.isArray(op.path)&&op.path.every(key=>typeof key==='string')&&op.path.length<=4))fail('INVALID_RECEIPT');
188
+ if(client==='codex') {
189
+ if(!(schemaVersion===2?[3]:[2,3]).includes(file.ops.length))fail('INVALID_RECEIPT');
190
+ const cap=scalar(file.ops[0],['agents','max_concurrent_threads_per_session'],'number');
191
+ if(cap<1||cap>32)fail('INVALID_RECEIPT');
192
+ scalar(file.ops[1],['agents','default_subagent_model'],'string');if(file.ops.length===3)scalar(file.ops[2],['agents','default_subagent_reasoning_effort'],'string');
193
+ return;
194
+ }
195
+ if(client==='grok') {
196
+ if(schemaVersion!==4||file.ops.length!==selectedRoles.length+2)fail('INVALID_RECEIPT');
197
+ const cap=scalar(file.ops[0],['subagents','max_concurrent'],'number');
198
+ if(cap<1||cap>32)fail('INVALID_RECEIPT');
199
+ for(const[i,role]of selectedRoles.entries())scalar(file.ops[1+i],['subagents','models','apk-'+role],'string');
200
+ scalar(file.ops.at(-1),['subagents','models','general-purpose'],'string');
201
+ return;
202
+ }
203
+ if(client!=='kimi')fail('INVALID_RECEIPT');
204
+ if(schemaVersion===2) {
205
+ if(![4,6].includes(file.ops.length))fail('INVALID_RECEIPT');
206
+ const removed=file.ops[0];fields(removed,['kind','path','before']);
207
+ if(removed.kind!=='removed'||!equal(removed.path,['secondary_model','default_effort']))fail('INVALID_RECEIPT');
208
+ literalValue(removed.before,'string',true);
209
+ const defaultAlias=scalar(file.ops[1],['secondary_model','default_model'],'string');
210
+ const count=(file.ops.length-2)/2,aliases=[];
211
+ for(let i=0;i<count;i++) {
212
+ const op=file.ops[2+i],name=op.path?.[2];
213
+ if(!alias(name)||aliases.includes(name))fail('INVALID_RECEIPT');aliases.push(name);scalar(op,['secondary_model','models',name],'string');
214
+ }
215
+ if(!aliases.includes(defaultAlias))fail('INVALID_RECEIPT');
216
+ for(let i=0;i<count;i++)scalar(file.ops[2+count+i],['models',aliases[i],'overrides','default_effort'],'string');
217
+ return;
218
+ }
219
+ const maxAliases=schemaVersion===4?ROLE_IDS.length:LEGACY_ROLE_IDS.length;
220
+ if(file.ops.length<2||file.ops.length>2+2*maxAliases)fail('INVALID_RECEIPT');
221
+ let at=0;
222
+ if(file.ops[0].kind==='removed') {
223
+ const removed=file.ops[at++];fields(removed,['kind','path','before']);
224
+ if(!equal(removed.path,['secondary_model','default_effort']))fail('INVALID_RECEIPT');literalValue(removed.before,'string',true);
225
+ }
226
+ const defaultAlias=scalar(file.ops[at++],['secondary_model','default_model'],'string'),aliases=[];
227
+ while(at<file.ops.length&&equal(file.ops[at].path?.slice(0,2),['secondary_model','models'])) {
228
+ const op=file.ops[at++],name=op.path[2];
229
+ if(!alias(name)||aliases.includes(name)||aliases.length===maxAliases)fail('INVALID_RECEIPT');aliases.push(name);scalar(op,['secondary_model','models',name],'string');
230
+ }
231
+ if(!aliases.length||!aliases.includes(defaultAlias))fail('INVALID_RECEIPT');
232
+ const pinned=new Set();
233
+ while(at<file.ops.length) {
234
+ const op=file.ops[at++],name=op.path?.[1];
235
+ if(!aliases.includes(name)||pinned.has(name))fail('INVALID_RECEIPT');pinned.add(name);scalar(op,['models',name,'overrides','default_effort'],'string');
236
+ }
237
+ }
238
+ function validateJsonSnapshot(file) {
239
+ if(!Array.isArray(file.ops)||file.ops.length!==2||!Array.isArray(file.created)||file.created.length>1||!file.created.every(keys=>equal(keys,['env'])))fail('INVALID_RECEIPT');
240
+ for(const[i,key]of ['CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS','CLAUDE_CODE_SUBAGENT_MODEL'].entries()) {
241
+ const op=file.ops[i];fields(op,['kind','path','existed','before','after']);
242
+ if(op.kind!=='value'||!equal(op.path,['env',key])||typeof op.existed!=='boolean'||typeof op.after!=='string'||op.after.length>4096||(op.existed?typeof op.before!=='string'||op.before.length>4096:op.before!==null))fail('INVALID_RECEIPT');
243
+ if(i===0&&(!/^[1-9]\d?$/.test(op.after)||Number(op.after)>32))fail('INVALID_RECEIPT');
244
+ }
245
+ }
246
+ function validateReceipt(r,plan) {
247
+ if(object(r)&&(r.schemaVersion===1||own(r,'packageRoot')||own(r,'nodePath')||own(r,'mode')||(Array.isArray(r.files)&&r.files.some(file=>object(file)&&(['owned','hook'].includes(file.type)||(Array.isArray(file.ops)&&file.ops.some(op=>object(op)&&['hook','entry'].includes(op.kind))))))))fail('LEGACY_RUNTIME_RECEIPT');
248
+ fields(r,['schemaVersion','kind','client','home','stateDir','policyHash','warnings','files',...(r?.schemaVersion===2?['maxConcurrent','preferences']:['releaseVersion','source','preset']),...(r?.schemaVersion===4?['selectedRoles']:[])]);
249
+ if(r.schemaVersion===4&&(!Array.isArray(r.selectedRoles)||!r.selectedRoles.length||r.selectedRoles.length>ROLE_IDS.length||new Set(r.selectedRoles).size!==r.selectedRoles.length||r.selectedRoles.some(role=>!ROLE_IDS.includes(role))))fail('INVALID_RECEIPT');
250
+ const l=locations(plan.client,plan.home,plan.stateDir),allowed=nativeTargets(l,r.schemaVersion===4?r.selectedRoles:LEGACY_ROLE_IDS);
251
+ if(![2,3,4].includes(r.schemaVersion)||r.kind!=='native-config'||r.client!==plan.client||r.home!==plan.home||r.stateDir!==plan.stateDir||!validHash(r.policyHash)||!Array.isArray(r.warnings)||r.warnings.length>32||!r.warnings.every(w=>typeof w==='string'&&/^[a-z][a-z0-9-]{0,127}$/.test(w))||!Array.isArray(r.files)||r.files.length!==allowed.length||!r.files.every(file=>object(file)&&allowed.includes(file.path))||new Set(r.files.map(file=>file.path)).size!==allowed.length)fail('INVALID_RECEIPT');
252
+ if(r.schemaVersion>=3) {if(!version(r.releaseVersion)||!SOURCES.includes(r.source)||(own(r,'preset')&&!validPreset(r.preset)))fail('INVALID_RECEIPT');}
253
+ else if(!object(r.preferences)||!Number.isInteger(r.maxConcurrent)||r.maxConcurrent<1||r.maxConcurrent>32)fail('INVALID_RECEIPT');
254
+ for(const file of r.files) {
255
+ if(typeof file.existed!=='boolean')fail('INVALID_RECEIPT');
256
+ if(l.roles.includes(file.path)) {
257
+ fields(file,['path','type','existed','afterHash']);
258
+ if(file.type!=='role'||file.existed||!validHash(file.afterHash))fail('INVALID_RECEIPT');
259
+ } else if(file.path===l.instructions) {
260
+ fields(file,['path','type','existed','prefix','block']);
261
+ if(file.type!=='instructions'||!['','\n','\n\n'].includes(file.prefix)||typeof file.block!=='string'||file.block.length>65536||!file.block.startsWith(BEGIN+'\n')||!file.block.endsWith(END+'\n')||file.block.split(BEGIN).length!==2||file.block.split(END).length!==2)fail('INVALID_RECEIPT');
262
+ } else if(plan.client==='claude') {
263
+ fields(file,['path','type','existed','ops','created']);if(file.type!=='json')fail('INVALID_RECEIPT');validateJsonSnapshot(file);
264
+ } else {
265
+ fields(file,['path','type','existed','ops']);if(file.type!=='toml')fail('INVALID_RECEIPT');validateTomlSnapshot(file,plan.client,r.schemaVersion,r.schemaVersion===4?r.selectedRoles:LEGACY_ROLE_IDS);
266
+ }
267
+ }
268
+ return r;
269
+ }
270
+ function appliedRecord(r) {return r?{version:r.schemaVersion>=3?r.releaseVersion:null,source:r.schemaVersion>=3?r.source:'unknown',preset:r.schemaVersion>=3?r.preset??null:null}:{version:null,source:null,preset:null};}
271
+ function releaseMetadata(plan,completed=false) {
272
+ return {targetReleaseVersion:plan.targetReleaseVersion??null,targetSource:plan.targetSource??null,previousReleaseVersion:plan.previousReleaseVersion??null,previousSource:plan.previousSource??null,appliedReleaseVersion:completed?(plan.kind==='uninstall'?null:plan.targetReleaseVersion??null):plan.appliedReleaseVersion??null,appliedSource:completed?(plan.kind==='uninstall'?null:plan.targetSource??null):plan.appliedSource??null,targetPreset:plan.targetPreset??null,previousPreset:plan.previousPreset??null,appliedPreset:completed?(plan.kind==='uninstall'?null:plan.targetPreset??null):plan.appliedPreset??null};
273
+ }
274
+ function recordMetadata(plan,r) {
275
+ const previous=appliedRecord(r);
276
+ Object.assign(plan,{previousReleaseVersion:previous.version,previousSource:previous.source,appliedReleaseVersion:previous.version,appliedSource:previous.source,previousPreset:previous.preset,appliedPreset:previous.preset});
277
+ }
278
+ async function undoFiles(plan,r,writeChanges=true) {
279
+ const baselines=new Map();
280
+ for(const file of r.files) {
281
+ const before=await readDependency(plan,file.path,plan.home);if(before.text===null)fail('OWNED_DRIFT');let after;
282
+ if(file.type==='json')after=undoJson(before.text,file);
283
+ else if(file.type==='toml') {after=before.text;for(const op of file.ops.toReversed())after=restoreTomlValue(after,op);if(!file.existed&&!after.trim())after=null;}
284
+ else if(file.type==='instructions') {
285
+ const owned=file.prefix+file.block;
286
+ if(before.text.split(BEGIN).length!==2||before.text.split(END).length!==2||!before.text.includes(owned))fail('OWNED_DRIFT');
287
+ after=before.text.replace(owned,'');if(!file.existed&&!after)after=null;
288
+ } else {if(digest(before.text)!==file.afterHash)fail('OWNED_DRIFT');after=null;}
289
+ baselines.set(file.path,{text:after,mode:before.mode,actual:before});
290
+ if(writeChanges)change(plan,file.path,before,after,before.mode);
291
+ }
292
+ return baselines;
293
+ }
294
+ export async function inspectConfiguration(options) {
295
+ const {result:plan,receipt}=await context(options);
296
+ if(receipt.text===null)return {status:'not_configured',releaseVersion:null,source:null,preset:null,warnings:[]};
297
+ const record=validateReceipt(parseJson(receipt.text,'INVALID_RECEIPT'),plan);
298
+ await undoFiles(plan,record,false);await verifyDependencies(plan);
299
+ const applied=appliedRecord(record);
300
+ return {status:'verified',releaseVersion:applied.version,source:applied.source,preset:applied.preset,warnings:[...record.warnings]};
301
+ }
302
+ function prerequisite(plan,warning) {return {...plan,status:'prerequisite',warnings:[warning],changes:[]};}
303
+ function resolveKimiAliases(native,preferences,selectedRoles=ROLE_IDS) {
304
+ const roles={},pins=new Map();let changed=false;
305
+ for(const selected of effectiveRoles(preferences,selectedRoles)) {
306
+ let actual=selected.model;
307
+ if(!object(native.models)||!own(native.models,actual)) {
308
+ const known=catalog.clients.kimi.models.find(value=>value.id===actual&&typeof value.upstreamModelId==='string');
309
+ if(!known)return {prerequisite:'kimi-provisioned-models-required'};
310
+ const matches=Object.entries(object(native.models)?native.models:{}).filter(([,entry])=>object(entry)&&entry.model===known.upstreamModelId);
311
+ if(matches.length!==1)return {prerequisite:matches.length?'kimi-catalog-alias-ambiguous':'kimi-provisioned-models-required'};
312
+ actual=matches[0][0];changed=true;
313
+ }
314
+ if(!alias(actual)||actual==='primary')return {prerequisite:'kimi-provisioned-models-required'};
315
+ if(preferences.avoidModels.includes(actual))return {prerequisite:'kimi-resolved-model-avoided'};
316
+ if(selected.effort!==null) {
317
+ if(pins.has(actual)&&pins.get(actual)!==selected.effort)return {prerequisite:'kimi-resolved-effort-conflict'};
318
+ pins.set(actual,selected.effort);
319
+ }
320
+ roles[selected.role]={model:actual,effort:selected.effort};
321
+ }
322
+ return {preferences:changed?{...preferences,roles}:preferences,changed};
323
+ }
324
+ function kimiPrerequisite(native,preferences,selectedRoles=ROLE_IDS) {
325
+ const selected=selectedAliases(preferences,selectedRoles),pinned=selected.filter(value=>value.effort!==null);
326
+ for(const value of selected) {
327
+ const model=native.models?.[value.model];
328
+ if(!object(model)||typeof model.provider!=='string'||typeof model.model!=='string'||!Number.isSafeInteger(model.max_context_size)||model.max_context_size<1||!object(native.providers?.[model.provider]))return 'kimi-provisioned-models-required';
329
+ }
330
+ if(!pinned.length){
331
+ if(native.thinking?.enabled===false&&selected.some(value=>catalog.clients.kimi.models.some(model=>model.thinkingRequired&&model.upstreamModelId===native.models[value.model].model)))return 'kimi-thinking-disabled';
332
+ return null;
333
+ }
334
+ if(pinned.length<selected.length&&native.secondary_model?.default_effort!==undefined)return 'kimi-mixed-native-default-effort';
335
+ if(native.thinking?.enabled===false)return 'kimi-thinking-disabled';
336
+ for(const value of pinned) {
337
+ const model=native.models[value.model],effective={...model,...model.overrides};
338
+ if(!Array.isArray(effective.support_efforts)||!effective.support_efforts.includes(value.effort))return 'kimi-supported-effort-required';
339
+ const globalEffort=native.thinking?.effort;
340
+ const mainEffortPinned=['low','medium','high','xhigh','max'].includes(globalEffort)&&effective.support_efforts.includes(globalEffort);
341
+ if(value.model===native.default_model&&effective.default_effort!==value.effort&&!mainEffortPinned)return 'kimi-main-effort-conflict';
342
+ }
343
+ return null;
344
+ }
345
+ export async function planInstall(options) {
346
+ const {releaseVersion=PACKAGE_VERSION,replace=false,source='release',preset=null}=options;
347
+ if(!validPreset(preset))fail('INVALID_PRESET');
348
+ if(!version(releaseVersion)||typeof replace!=='boolean'||!SOURCES.includes(source))fail('INVALID_RELEASE');
349
+ const {result:plan,l,receipt}=await context(options);plan.kind='install';
350
+ plan.targetReleaseVersion=releaseVersion;plan.targetSource=source;plan.targetPreset=preset;
351
+ let record=null;
352
+ if(receipt.text!==null)record=validateReceipt(parseJson(receipt.text,'INVALID_RECEIPT'),plan);
353
+ recordMetadata(plan,record);
354
+ plan.policyPath=absolute(options.policyPath);
355
+ const input=await readDependency(plan,plan.policyPath,path.dirname(plan.policyPath));if(input.text===null)fail('INVALID_POLICY');
356
+ const policy=parseJson(input.text,'INVALID_POLICY');try {validatePolicy(policy);}catch {fail('INVALID_POLICY');}
357
+ if(!own(policy.clients,plan.client))fail('INVALID_POLICY');
358
+ let selected=policy.clients[plan.client],resolvedAliases=false;
359
+ const roleIds=selectedRoleIds(policy);
360
+ const maxConcurrent=selected.maxConcurrent??policy.maxConcurrent,policyHash=digest(json(canonical(policy)));
361
+ const policyBefore=await readDependency(plan,l.policy,plan.stateDir);
362
+ const baselines=record?await undoFiles(plan,record,false):new Map();
363
+ if(record&&!replace) {
364
+ if(record.policyHash!==policyHash)fail('INSTALLATION_CONFLICT');
365
+ if(policyBefore.text===null||!equal(parseJson(policyBefore.text,'OWNED_DRIFT'),policy))fail('OWNED_DRIFT');
366
+ return {...plan,status:'unchanged',warnings:[...record.warnings]};
367
+ }
368
+ if(!replace&&policyBefore.text!==null&&!equal(parseJson(policyBefore.text,'POLICY_CONFLICT'),policy))fail('POLICY_CONFLICT');
369
+ const baseline=async target=>baselines.get(target)??await readDependency(plan,target,plan.home);
370
+ const nativeChange=(target,before,after,mode=before.mode)=>change(plan,target,before.actual??before,after,mode);
371
+ const config=l.config?await baseline(l.config):null;
372
+ let native;
373
+ if(plan.client==='claude') {
374
+ native=configJson(config.text);
375
+ if(native.env!==undefined&&!object(native.env))fail('UNSUPPORTED_CONFIG');
376
+ if(native.env?.CLAUDE_CODE_SUBAGENT_MODEL_FORCE==='1')return prerequisite(plan,'claude-force-model-conflict');
377
+ } else if(config) {
378
+ native=parseToml(config.text??'');
379
+ if(plan.client==='kimi') {
380
+ const resolved=resolveKimiAliases(native,selected,roleIds);if(resolved.prerequisite)return prerequisite(plan,resolved.prerequisite);
381
+ selected=resolved.preferences;resolvedAliases=resolved.changed;
382
+ const required=kimiPrerequisite(native,selected,roleIds);if(required)return prerequisite(plan,required);
383
+ if(selectedAliases(selected,roleIds).some(value=>value.effort!==null)&&native.secondary_model?.default_effort!==undefined&&typeof native.secondary_model.default_effort!=='string')fail('UNSUPPORTED_CONFIG');
384
+ }
385
+ if(plan.client==='grok') {
386
+ const subagents=native.subagents;
387
+ if(subagents!==undefined&&!object(subagents))fail('UNSUPPORTED_CONFIG');
388
+ if(subagents?.models!==undefined&&!object(subagents.models))fail('UNSUPPORTED_CONFIG');
389
+ const cap=subagents?.max_concurrent;
390
+ if(cap!==undefined&&(!Number.isSafeInteger(cap)||cap<1))fail('UNSUPPORTED_CONFIG');
391
+ }
392
+ if(plan.client==='codex') {
393
+ if(native.agents&&own(native.agents,'max_threads'))fail('CODEX_CAP_ALIAS_CONFLICT');
394
+ const cap=native.agents?.max_concurrent_threads_per_session;
395
+ if(cap!==undefined&&(!Number.isSafeInteger(cap)||cap<1))fail('UNSUPPORTED_CONFIG');
396
+ }
397
+ }
398
+ const files=[];plan.warnings=[...nativeWarnings(plan.client),...(resolvedAliases?['kimi-catalog-alias-resolved']:[])];
399
+ if(plan.client==='claude') {
400
+ const edited=jsonInstall(config.text,maxConcurrent,rolePreference(selected,roleIds[0]).model);nativeChange(l.config,config,edited.text);
401
+ files.push({path:l.config,type:'json',existed:config.text!==null,ops:edited.ops,created:edited.created});
402
+ } else if(config) {
403
+ let text=config.text??'';const ops=[];
404
+ if(plan.client==='kimi'&&selectedAliases(selected,roleIds).some(value=>value.effort!==null)) {const edit=removeTomlValue(text,['secondary_model','default_effort']);text=edit.text;ops.push(edit.ownership);}
405
+ for(const[keys,value]of tomlSettings(plan.client,selected,maxConcurrent,roleIds)) {
406
+ const previous=keys.reduce((at,key)=>at?.[key],native);
407
+ if(previous!==undefined&&typeof previous!==typeof value)fail('UNSUPPORTED_CONFIG');
408
+ const edit=setTomlValue(text,keys,value);text=edit.text;ops.push(edit.ownership);
409
+ }
410
+ nativeChange(l.config,config,text);files.push({path:l.config,type:'toml',existed:config.text!==null,ops});
411
+ }
412
+ const roles=nativeRoles(plan.client,selected,roleIds),targets=roleIds.map(role=>l.roles[ROLE_IDS.indexOf(role)]);
413
+ for(const[i,target]of targets.entries()) {
414
+ const before=await baseline(target);if(before.text!==null)fail('OWNERSHIP_CONFLICT');
415
+ nativeChange(target,before,roles[i],0o600);files.push({path:target,type:'role',existed:false,afterHash:digest(roles[i])});
416
+ }
417
+ for(const target of l.roles.filter(target=>!targets.includes(target))) {const before=baselines.get(target);if(before)nativeChange(target,before,before.text);}
418
+ const before=await baseline(l.instructions),text=before.text??'';
419
+ if(text.includes(BEGIN)||text.includes(END))fail('OWNERSHIP_CONFLICT');
420
+ const prefix=!text||text.endsWith('\n\n')?'':text.endsWith('\n')?'\n':'\n\n',block=instructionBlock(plan.client,selected,maxConcurrent,roleIds);
421
+ nativeChange(l.instructions,before,text+prefix+block);
422
+ files.push({path:l.instructions,type:'instructions',existed:before.text!==null,prefix,block});
423
+ const r={schemaVersion:4,kind:'native-config',client:plan.client,home:plan.home,stateDir:plan.stateDir,releaseVersion,source,...(preset===null?{}:{preset}),selectedRoles:roleIds,policyHash,warnings:plan.warnings,files};
424
+ validateReceipt(r,plan);
425
+ let canonicalText=policyBefore.text;
426
+ if(canonicalText===null||replace) {
427
+ let matches=false;try {matches=canonicalText!==null&&equal(parseJson(canonicalText),policy);}catch {}
428
+ if(!matches)canonicalText=json(policy);
429
+ }
430
+ change(plan,l.policy,policyBefore,canonicalText,0o600);
431
+ change(plan,l.receipt,receipt,json(r),0o600);
432
+ return {...plan,status:'planned'};
433
+ }
434
+
435
+ export function summarizePlan(plan) {
436
+ const relative = file => within(plan.home,file) ? '~/' + path.relative(plan.home,file).split(path.sep).join('/') : '<state-dir>/' + path.relative(plan.stateDir,file).split(path.sep).join('/');
437
+ return {client:plan.client,kind:plan.kind,status:plan.status,files:plan.changes.map(c => ({path:relative(c.path),action:c.after === null ? 'remove' : c.before === null ? 'create' : 'update'})),warnings:[...plan.warnings],...releaseMetadata(plan)};
438
+ }
439
+
440
+ async function privateDir(dir,base) {
441
+ await safePath(dir,base);
442
+ await fs.mkdir(dir,{recursive:true,mode:0o700});
443
+ await fs.chmod(dir,0o700);
444
+ }
445
+ async function durableWrite(file,text,mode) {
446
+ const handle = await fs.open(file,'wx',mode);
447
+ try { await handle.chmod(mode); await handle.writeFile(text,'utf8'); await handle.sync(); } finally { await handle.close(); }
448
+ }
449
+ async function syncDir(dir) {
450
+ const handle = await fs.open(dir,constants.O_RDONLY);
451
+ try { await handle.sync(); } finally { await handle.close(); }
452
+ }
453
+ async function verifyDependencies(plan, committed = []) {
454
+ for (const dependency of plan.dependencies) {
455
+ const postimage = committed.find(c => c.path === dependency.path);
456
+ const current = await read(dependency.path,dependency.base);
457
+ const hash = postimage ? postimage.afterHash : dependency.beforeHash;
458
+ const mode = postimage ? postimage.mode : dependency.beforeMode;
459
+ if (digest(current.text) !== hash || (current.text !== null && current.mode !== mode)) fail('STALE_PLAN');
460
+ }
461
+ }
462
+ function validPlan(plan) {
463
+ if (!object(plan) || plan.schemaVersion !== 1 || !CLIENTS.includes(plan.client) || !['install','uninstall'].includes(plan.kind) || !Array.isArray(plan.changes) || !Array.isArray(plan.dependencies)) fail('INVALID_PLAN');
464
+ absolute(plan.home); absolute(plan.stateDir);
465
+ const l = locations(plan.client,plan.home,plan.stateDir), allowed = [...nativeTargets(l),l.policy,l.receipt];
466
+ if (plan.policyPath !== undefined) absolute(plan.policyPath);
467
+ if (new Set(plan.dependencies.map(d=>d.path)).size !== plan.dependencies.length) fail('INVALID_PLAN');
468
+ for (const d of plan.dependencies) {
469
+ const source = d.path === plan.policyPath;
470
+ if ((!allowed.includes(d.path) && !source) || (d.base !== allowedBases(plan)(d.path) && !(source && d.base === path.dirname(d.path))) || !(d.beforeHash === null || /^[a-f0-9]{64}$/.test(d.beforeHash)) || !Number.isInteger(d.beforeMode)) fail('INVALID_PLAN');
471
+ }
472
+ if (new Set(plan.changes.map(c=>c.path)).size !== plan.changes.length) fail('INVALID_PLAN');
473
+ for (const c of plan.changes) {
474
+ if (!allowed.includes(c.path) || !(c.before === null || typeof c.before === 'string') || !(c.after === null || typeof c.after === 'string') || digest(c.before) !== c.beforeHash || digest(c.after) !== c.afterHash || !Number.isInteger(c.mode) || (c.mode & ~0o777) || !Number.isInteger(c.beforeMode)) fail('INVALID_PLAN');
475
+ if (!plan.dependencies.some(d => d.path === c.path && d.beforeHash === c.beforeHash && d.beforeMode === c.beforeMode)) fail('INVALID_PLAN');
476
+ }
477
+ }
478
+ export async function applyInstall(plan) {
479
+ validPlan(plan);
480
+ if (plan.status === 'prerequisite') return {status:'prerequisite',client:plan.client,changedFiles:0,warnings:[...plan.warnings],...releaseMetadata(plan)};
481
+ await safePath(plan.home,path.parse(plan.home).root);
482
+ await safePath(plan.stateDir,path.parse(plan.stateDir).root);
483
+ if (!plan.changes.length) {
484
+ await recoveryRequired(plan.stateDir);
485
+ await verifyDependencies(plan);
486
+ return {status:'unchanged',client:plan.client,changedFiles:0,warnings:[...plan.warnings],...releaseMetadata(plan)};
487
+ }
488
+ if (process.platform === 'win32') fail('PLATFORM_UNSUPPORTED');
489
+ // Lock creation is the first mutation. All target preimages were collected in plan-only mode.
490
+ await privateDir(plan.stateDir,within(plan.home,plan.stateDir) ? plan.home : path.dirname(plan.stateDir));
491
+ const lock = path.join(plan.stateDir,'.install.lock'), id = randomUUID();
492
+ await safePath(lock,plan.stateDir);
493
+ let held;
494
+ try { held = await fs.open(lock,'wx',0o600); } catch (e) { if (e.code === 'EEXIST') fail('INSTALL_LOCKED'); throw e; }
495
+ const pending = path.join(plan.stateDir,'pending.json'), staged = [], committed = [];
496
+ let journalCreated = false, rollbackConflict = false, journalText = null, retainLock = false;
497
+ try {
498
+ await held.writeFile(id); await held.sync();
499
+ await safePath(pending,plan.stateDir);
500
+ if (await statOrNull(pending)) fail('RECOVERY_REQUIRED');
501
+ await verifyDependencies(plan);
502
+ const backupDir = path.join(plan.stateDir,'backups',id);
503
+ await privateDir(path.dirname(backupDir),plan.stateDir);
504
+ await privateDir(backupDir,plan.stateDir);
505
+ for (const [i,c] of plan.changes.entries()) {
506
+ if (c.before !== null) await durableWrite(path.join(backupDir,`${i}.before`),c.before,0o600);
507
+ await safePath(c.path,allowedBases(plan)(c.path));
508
+ await fs.mkdir(path.dirname(c.path),{recursive:true,mode:0o700});
509
+ if (c.after !== null) {
510
+ const stage = path.join(path.dirname(c.path),`.agent-policy-${id}-${i}.tmp`);
511
+ staged.push(stage);
512
+ await durableWrite(stage,c.after,c.mode);
513
+ }
514
+ }
515
+ const journal = {schemaVersion:1,id,client:plan.client,kind:plan.kind,backupDir,changes:plan.changes.map((c,i)=>({path:c.path,beforeHash:c.beforeHash,afterHash:c.afterHash,backup:c.before === null ? null : `${i}.before`,beforeMode:c.beforeMode}))};
516
+ journalText = json(journal);
517
+ await durableWrite(path.join(backupDir,'index.json'),journalText,0o600);
518
+ await syncDir(backupDir);
519
+ await syncDir(path.dirname(backupDir));
520
+ await durableWrite(pending,journalText,0o600); journalCreated = true;
521
+ await syncDir(plan.stateDir);
522
+ for (const [i,c] of plan.changes.entries()) {
523
+ await verifyDependencies(plan,committed);
524
+ if (c.after === null) await fs.unlink(c.path);
525
+ else await fs.rename(path.join(path.dirname(c.path),`.agent-policy-${id}-${i}.tmp`),c.path);
526
+ committed.push(c);
527
+ await syncDir(path.dirname(c.path));
528
+ }
529
+ await verifyDependencies(plan,committed);
530
+ await fs.unlink(pending);
531
+ await syncDir(plan.stateDir);
532
+ await verifyDependencies(plan,committed);
533
+ journalCreated = false;
534
+ return {status:'applied',client:plan.client,changedFiles:plan.changes.length,warnings:[...plan.warnings],...releaseMetadata(plan,true)};
535
+ } catch (error) {
536
+ if (journalCreated) retainLock = true;
537
+ // Final directory sync can fail after unlink. Restore the recovery record before
538
+ // any rollback that might conflict. The retained lock UUID also locates the
539
+ // durable backup index if storage refuses recreation of pending.json.
540
+ if (journalCreated && !(await statOrNull(pending))) {
541
+ try { await durableWrite(pending,journalText,0o600); await syncDir(plan.stateDir); }
542
+ catch { retainLock = true; fail('RECOVERY_REQUIRED'); }
543
+ }
544
+ for (const c of committed.toReversed()) {
545
+ try {
546
+ const current = await read(c.path,allowedBases(plan)(c.path));
547
+ if (digest(current.text) !== c.afterHash || (current.text !== null && current.mode !== c.mode)) { rollbackConflict = true; continue; }
548
+ if (c.before === null) await fs.unlink(c.path);
549
+ else {
550
+ const rollback = path.join(path.dirname(c.path),`.agent-policy-${id}-rollback.tmp`);
551
+ staged.push(rollback);
552
+ await durableWrite(rollback,c.before,c.beforeMode);
553
+ await fs.rename(rollback,c.path);
554
+ }
555
+ } catch { rollbackConflict = true; }
556
+ }
557
+ if (journalCreated && !rollbackConflict) { try { await fs.unlink(pending); } catch { rollbackConflict = true; } }
558
+ if (rollbackConflict) { retainLock = true; fail('ROLLBACK_CONFLICT'); }
559
+ retainLock = false;
560
+ if (typeof error.code === 'string' && ['STALE_PLAN','RECOVERY_REQUIRED','UNSAFE_PATH','UNSAFE_FILE'].includes(error.code)) throw error;
561
+ fail('INSTALL_FAILED');
562
+ } finally {
563
+ for (const stage of staged) { try { await fs.unlink(stage); } catch {} }
564
+ await held.close();
565
+ try { if (!retainLock && (await fs.readFile(lock,'utf8')) === id) await fs.unlink(lock); } catch {}
566
+ }
567
+ }
568
+
569
+ export async function uninstall(options) {
570
+ const {result:plan,l,receipt} = await context(options);
571
+ plan.kind = 'uninstall';
572
+ plan.targetReleaseVersion=null;plan.targetSource=null;plan.targetPreset=null;recordMetadata(plan,null);
573
+ if (receipt.text === null) { plan.status = 'unchanged'; return options.apply ? applyInstall(plan) : plan; }
574
+ const record = validateReceipt(parseJson(receipt.text,'INVALID_RECEIPT'),plan);
575
+ recordMetadata(plan,record);
576
+ plan.warnings = record.warnings ?? [];
577
+ await undoFiles(plan,record);
578
+ change(plan,l.receipt,receipt,null,0o600);
579
+ // Shared preferences and private transaction backups are retained for later configuration/removal.
580
+ plan.status = 'planned';
581
+ return options.apply ? applyInstall(plan) : plan;
582
+ }