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.
- package/AGENT_INSTALL.md +79 -0
- package/CHANGELOG.md +101 -0
- package/LICENSE +21 -0
- package/README.md +69 -2
- package/bin/agent-policy.mjs +157 -0
- package/docs/architecture.md +17 -0
- package/docs/compatibility.md +23 -0
- package/docs/configuration-page.md +44 -0
- package/docs/deployment.md +31 -0
- package/docs/design/2026-11-01-grok-build-support.md +30 -0
- package/docs/development.md +21 -0
- package/docs/installation.md +23 -0
- package/docs/model-catalog.md +102 -0
- package/docs/model-updates.md +22 -0
- package/docs/npm-release.md +27 -0
- package/docs/policy.md +16 -0
- package/docs/privacy.md +9 -0
- package/docs/recovery.md +13 -0
- package/docs/releases.md +40 -0
- package/docs/use-cases.md +25 -0
- package/docs/verification.md +45 -0
- package/examples/kimi-pool.toml +17 -0
- package/models/README.md +7 -0
- package/models/catalog.json +743 -0
- package/package.json +63 -4
- package/policies/balanced.json +119 -0
- package/policies/fast.json +81 -0
- package/policies/quality.json +111 -0
- package/schema/policy.schema.json +459 -0
- package/site/index.html +1049 -0
- package/src/detect.mjs +137 -0
- package/src/explicit.mjs +89 -0
- package/src/inspect.mjs +203 -0
- package/src/install.mjs +582 -0
- package/src/native.mjs +64 -0
- package/src/output.mjs +108 -0
- package/src/policy.mjs +45 -0
- package/src/release.mjs +19 -0
- package/src/roles.mjs +13 -0
- package/src/setup.mjs +66 -0
- package/src/share.mjs +172 -0
- package/src/toml.mjs +96 -0
package/src/detect.mjs
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const CLIENTS = ['codex','claude','kimi','agy','grok'];
|
|
8
|
+
const OUTPUT_LIMIT = 65536;
|
|
9
|
+
const VERSION = String.raw`(\d{1,4}\.\d{1,4}\.\d{1,4}(?:-[A-Za-z0-9][A-Za-z0-9.-]{0,63})?(?:\+[A-Za-z0-9][A-Za-z0-9.-]{0,63})?)`;
|
|
10
|
+
function failure(code) { return Object.assign(new Error(code),{code}); }
|
|
11
|
+
function options({env=process.env,hostHome=os.homedir(),timeoutMs=5000} = {}) {
|
|
12
|
+
if (!env || typeof env !== 'object' || typeof hostHome !== 'string' || !path.isAbsolute(hostHome) || hostHome.includes('\0') || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000) throw failure('INVALID_DETECTION_OPTIONS');
|
|
13
|
+
return {env,hostHome:path.resolve(hostHome),timeoutMs};
|
|
14
|
+
}
|
|
15
|
+
function candidates(client,{env,hostHome}) {
|
|
16
|
+
const dirs = typeof env.PATH === 'string' ? env.PATH.split(path.delimiter) : [];
|
|
17
|
+
const names = process.platform === 'win32' ? [client + '.exe',client] : [client];
|
|
18
|
+
const result = [];
|
|
19
|
+
for (const dir of dirs) {
|
|
20
|
+
// Never search the working directory through empty/relative PATH segments.
|
|
21
|
+
if (!path.isAbsolute(dir) || dir.includes('\0')) continue;
|
|
22
|
+
for (const name of names) result.push({file:path.join(dir,name),origin:'path'});
|
|
23
|
+
}
|
|
24
|
+
const conventional = client === 'kimi' ? ['.kimi-code/bin/kimi']
|
|
25
|
+
: client === 'claude' ? ['.local/bin/claude','.claude/local/claude']
|
|
26
|
+
: [`.local/bin/${client}`];
|
|
27
|
+
for (const relative of conventional) result.push({file:path.join(hostHome,relative),origin:'known_location'});
|
|
28
|
+
return result.filter((candidate,index) => result.findIndex(c=>c.file === candidate.file) === index);
|
|
29
|
+
}
|
|
30
|
+
async function inspect(file) {
|
|
31
|
+
try {
|
|
32
|
+
const executable = await fs.realpath(file);
|
|
33
|
+
const stat = await fs.stat(executable);
|
|
34
|
+
if (!stat.isFile()) return {status:'invalid'};
|
|
35
|
+
await fs.access(executable,constants.X_OK);
|
|
36
|
+
return {status:'executable',executable};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return {status:error.code === 'ENOENT' || error.code === 'ENOTDIR' ? 'not_found' : 'invalid'};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function probe(executable,arg,{env,timeoutMs}) {
|
|
42
|
+
return new Promise(resolve => {
|
|
43
|
+
let child, timer, settled = false, size = 0;
|
|
44
|
+
const ownGroup = process.platform !== 'win32';
|
|
45
|
+
const buffers = [];
|
|
46
|
+
const killOwned = () => {
|
|
47
|
+
if (!child?.pid) return;
|
|
48
|
+
try { if (ownGroup) process.kill(-child.pid,'SIGKILL'); else child.kill('SIGKILL'); } catch {}
|
|
49
|
+
};
|
|
50
|
+
const alive = () => {
|
|
51
|
+
if (!child?.pid) return false;
|
|
52
|
+
if (!ownGroup) return child.exitCode === null && child.signalCode === null;
|
|
53
|
+
try { process.kill(-child.pid,0); return true; } catch (error) { return error.code !== 'ESRCH'; }
|
|
54
|
+
};
|
|
55
|
+
const finish = async (status) => {
|
|
56
|
+
if (settled) return;
|
|
57
|
+
settled = true;
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
killOwned();
|
|
60
|
+
child?.stdout?.destroy(); child?.stderr?.destroy();
|
|
61
|
+
for (let elapsed = 0; alive() && elapsed < 500; elapsed += 20) await new Promise(done=>setTimeout(done,20));
|
|
62
|
+
if (status === 'ok' && alive()) status = 'invalid';
|
|
63
|
+
resolve({status,output:status === 'ok' ? Buffer.concat(buffers).toString('utf8') : ''});
|
|
64
|
+
};
|
|
65
|
+
try { child = spawn(executable,[arg],{env,stdio:['ignore','pipe','pipe'],shell:false,windowsHide:true,detached:ownGroup}); }
|
|
66
|
+
catch { finish('invalid'); return; }
|
|
67
|
+
timer = setTimeout(()=>finish('timeout'),timeoutMs);
|
|
68
|
+
for (const stream of [child.stdout,child.stderr]) stream.on('data',chunk=>{
|
|
69
|
+
size += chunk.length;
|
|
70
|
+
if (size > OUTPUT_LIMIT) finish('invalid'); else buffers.push(chunk);
|
|
71
|
+
});
|
|
72
|
+
// A wrapper can exit successfully while descendants retain inherited pipes.
|
|
73
|
+
// Its dedicated POSIX group remains ours; stop it before waiting for close.
|
|
74
|
+
child.on('exit',killOwned);
|
|
75
|
+
child.on('error',()=>finish('invalid'));
|
|
76
|
+
child.on('close',code=>finish(code === 0 ? 'ok' : 'invalid'));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function identity(client,output) {
|
|
80
|
+
const patterns = {
|
|
81
|
+
codex:[new RegExp(String.raw`^\s*codex(?:-cli)?\s+v?${VERSION}(?:\s|$)`,'im')],
|
|
82
|
+
claude:[new RegExp(String.raw`^\s*v?${VERSION}\s+\(Claude Code\)(?:\s|$)`,'im'),new RegExp(String.raw`^\s*Claude Code\s+v?${VERSION}(?:\s|$)`,'im')],
|
|
83
|
+
kimi:[new RegExp(String.raw`^\s*kimi(?:\s+code(?:\s+cli)?)?(?:,?\s+version)?\s+v?${VERSION}(?:\s|$)`,'im')],
|
|
84
|
+
agy:[new RegExp(String.raw`^\s*(?:Antigravity|agy)\s+v?${VERSION}(?:\s|$)`,'im')],
|
|
85
|
+
grok:[new RegExp(String.raw`^\s*grok\s+v?${VERSION}(?:\s|$)`,'im')],
|
|
86
|
+
};
|
|
87
|
+
for (const pattern of patterns[client]) { const match = output.match(pattern); if (match && match[1].length <= 128) return match[1]; }
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
async function verify(client,executable,opts) {
|
|
91
|
+
const version = await probe(executable,'--version',opts);
|
|
92
|
+
if (version.status !== 'ok') return {status:version.status};
|
|
93
|
+
const branded = identity(client,version.output);
|
|
94
|
+
if (branded) return {status:'detected',version:branded};
|
|
95
|
+
if (client !== 'kimi' && client !== 'agy') return {status:'invalid'};
|
|
96
|
+
const number = version.output.match(new RegExp(String.raw`^\s*v?${VERSION}(?:\s|$)`));
|
|
97
|
+
if (!number || number[1].length > 128) return {status:'invalid'};
|
|
98
|
+
const help = await probe(executable,'--help',opts);
|
|
99
|
+
if (help.status !== 'ok') return {status:help.status};
|
|
100
|
+
const known = client === 'kimi' ? /\bKimi (?:Code(?: CLI)?|CLI)\b/i.test(help.output)
|
|
101
|
+
&& (/\busage:\s*kimi\b/i.test(help.output) || /moonshotai\.github\.io\/kimi-code\//i.test(help.output))
|
|
102
|
+
: /\bAntigravity\b/i.test(help.output) && /\busage:\s*agy\b/i.test(help.output)
|
|
103
|
+
|| /Usage of agy:/i.test(help.output) && /Available subcommands:/i.test(help.output)
|
|
104
|
+
&& ['--agent','--model','--effort'].every(flag=>help.output.includes(flag));
|
|
105
|
+
return known ? {status:'detected',version:number[1]} : {status:'invalid'};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function resolveClient(client,opts = {}) {
|
|
109
|
+
if (!CLIENTS.includes(client)) throw failure('INVALID_CLIENT');
|
|
110
|
+
const settings = options(opts);
|
|
111
|
+
let best = {client,status:'not_found',version:null,origin:null,executable:null};
|
|
112
|
+
const checked = new Set();
|
|
113
|
+
for (const candidate of candidates(client,settings)) {
|
|
114
|
+
const inspected = await inspect(candidate.file);
|
|
115
|
+
if (inspected.status === 'not_found') continue;
|
|
116
|
+
if (inspected.status === 'invalid') {
|
|
117
|
+
if (best.status === 'not_found') best = {...best,status:'invalid',origin:candidate.origin};
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (checked.has(inspected.executable)) continue;
|
|
121
|
+
checked.add(inspected.executable);
|
|
122
|
+
const result = await verify(client,inspected.executable,settings);
|
|
123
|
+
if (result.status === 'detected') return {client,status:'detected',version:result.version,origin:candidate.origin,executable:inspected.executable};
|
|
124
|
+
if (result.status === 'timeout' || best.status === 'not_found') best = {...best,status:result.status,origin:candidate.origin};
|
|
125
|
+
}
|
|
126
|
+
return best;
|
|
127
|
+
}
|
|
128
|
+
export async function detectClients(opts = {}) {
|
|
129
|
+
return Promise.all(CLIENTS.map(client=>resolveClient(client,opts)));
|
|
130
|
+
}
|
|
131
|
+
export function summarizeDetection(records) {
|
|
132
|
+
return records.filter(r=>CLIENTS.includes(r?.client)).map(({client,status,version,origin})=>{
|
|
133
|
+
status = ['detected','not_found','invalid','timeout'].includes(status) ? status : 'invalid';
|
|
134
|
+
if (status === 'detected' && (typeof version !== 'string' || version.length > 128 || !new RegExp(`^${VERSION}$`).test(version))) status = 'invalid';
|
|
135
|
+
return {client,status,version:status === 'detected' ? version : null,origin:['path','known_location'].includes(origin) ? origin : null};
|
|
136
|
+
});
|
|
137
|
+
}
|
package/src/explicit.mjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Pure command codec, also embedded in the offline configuration page.
|
|
2
|
+
export const TOOL_FLAGS=Object.freeze({codex:'codex',claude:'claude',kimi:'kimi',antigravity:'agy',grok:'grok'});
|
|
3
|
+
export const EXPLICIT_FIELDS=Object.freeze(['roles','max-concurrent',...Object.keys(TOOL_FLAGS),...Object.keys(TOOL_FLAGS).map(flag=>'avoid-'+flag),...Object.keys(TOOL_FLAGS).map(flag=>flag+'-max-concurrent')]);
|
|
4
|
+
const EXPLICIT_MODEL=/^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
|
|
5
|
+
const EXPLICIT_EFFORTS=['none','minimal','low','medium','high','xhigh','max'];
|
|
6
|
+
function invalidExplicit(){throw Object.assign(new Error('Invalid explicit configuration.'),{code:'INVALID_EXPLICIT_CONFIG'});}
|
|
7
|
+
function explicitList(value,limit){
|
|
8
|
+
if(typeof value!=='string'||!value.length)invalidExplicit();
|
|
9
|
+
const list=value.split(',').map(value=>value.trim());
|
|
10
|
+
if(!list.length||list.length>limit||list.some(value=>!value)||new Set(list).size!==list.length)invalidExplicit();
|
|
11
|
+
return list;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Grok has no per-role effort, so its value is the whole model id; a trailing ':<effort>' would be ambiguous and is refused.
|
|
15
|
+
const grokEffortSuffix=model=>{const colon=model.lastIndexOf(':');return colon>=0&&['native',...EXPLICIT_EFFORTS].includes(model.slice(colon+1));};
|
|
16
|
+
export function parseExplicitOptions(options,roleIds){
|
|
17
|
+
if(!options||typeof options!=='object'||Array.isArray(options))invalidExplicit();
|
|
18
|
+
const fields=Object.getOwnPropertyDescriptors(options);
|
|
19
|
+
if(Object.entries(fields).some(([key,value])=>!EXPLICIT_FIELDS.includes(key)||value.get||value.set||typeof value.value!=='string'))invalidExplicit();
|
|
20
|
+
options=Object.fromEntries(Object.entries(fields).map(([key,value])=>[key,value.value]));
|
|
21
|
+
if(Object.values(fields).reduce((size,value)=>size+value.value.length,0)>65536)invalidExplicit();
|
|
22
|
+
const requested=explicitList(options.roles,roleIds.length);
|
|
23
|
+
if(requested.some(role=>!roleIds.includes(role)))invalidExplicit();
|
|
24
|
+
const roles=roleIds.filter(role=>requested.includes(role)),maxConcurrent=Number(options['max-concurrent']);
|
|
25
|
+
if(!/^[1-9]\d*$/.test(options['max-concurrent'])||!Number.isInteger(maxConcurrent)||maxConcurrent>32)invalidExplicit();
|
|
26
|
+
for(const flag of Object.keys(TOOL_FLAGS))if((options['avoid-'+flag]!==undefined||options[flag+'-max-concurrent']!==undefined)&&options[flag]===undefined)invalidExplicit();
|
|
27
|
+
const clients={};
|
|
28
|
+
for(const flag of Object.keys(options).filter(flag=>Object.hasOwn(TOOL_FLAGS,flag))){
|
|
29
|
+
const client=TOOL_FLAGS[flag],choices={};
|
|
30
|
+
for(const entry of explicitList(options[flag],roles.length)){
|
|
31
|
+
const at=entry.indexOf('='),role=entry.slice(0,at).trim(),value=entry.slice(at+1).trim();
|
|
32
|
+
if(at<1||!roles.includes(role)||Object.hasOwn(choices,role))invalidExplicit();
|
|
33
|
+
let model=value,effort=null;
|
|
34
|
+
if(client==='agy'){if(!['pro','flash','flash_lite','inherit'].includes(model))invalidExplicit();}
|
|
35
|
+
else if(client==='grok'){if(grokEffortSuffix(value))invalidExplicit();}
|
|
36
|
+
else{
|
|
37
|
+
const colon=value.lastIndexOf(':');if(colon<1)invalidExplicit();
|
|
38
|
+
model=value.slice(0,colon);const level=value.slice(colon+1);
|
|
39
|
+
if(level!=='native'&&!EXPLICIT_EFFORTS.includes(level))invalidExplicit();effort=level==='native'?null:level;
|
|
40
|
+
}
|
|
41
|
+
if(!EXPLICIT_MODEL.test(model))invalidExplicit();choices[role]={model,effort};
|
|
42
|
+
}
|
|
43
|
+
if(Object.keys(choices).length!==roles.length)invalidExplicit();
|
|
44
|
+
const avoidModels=options['avoid-'+flag]===undefined?[]:explicitList(options['avoid-'+flag],32);
|
|
45
|
+
if(avoidModels.some(model=>!EXPLICIT_MODEL.test(model)))invalidExplicit();
|
|
46
|
+
const first=choices[roles[0]];
|
|
47
|
+
const limit=options[flag+'-max-concurrent'];
|
|
48
|
+
if(limit!==undefined&&(!/^[1-9]\d*$/.test(limit)||Number(limit)>32))invalidExplicit();
|
|
49
|
+
clients[client]={...(limit===undefined?{}:{maxConcurrent:Number(limit)}),code:{...first},light:{...first},roles:Object.fromEntries(roles.map(role=>[role,choices[role]])),avoidModels};
|
|
50
|
+
}
|
|
51
|
+
if(!Object.keys(clients).length)invalidExplicit();
|
|
52
|
+
return {schemaVersion:1,maxConcurrent,...(roles.length===roleIds.length?{}:{selectedRoles:roles}),clients};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function explicitOptions(config,roleCatalog){
|
|
56
|
+
const legacy={worker:'code',reviewer:'code',explorer:'light',routine:'light'};
|
|
57
|
+
if(!Array.isArray(roleCatalog)||!roleCatalog.length)invalidExplicit();
|
|
58
|
+
const definitions=roleCatalog.map(role=>typeof role==='string'?{id:role,kind:legacy[role]}:role);
|
|
59
|
+
if(definitions.some(role=>!role||typeof role.id!=='string'||!['code','light'].includes(role.kind))||new Set(definitions.map(role=>role.id)).size!==definitions.length)invalidExplicit();
|
|
60
|
+
const roleIds=definitions.map(role=>role.id),kinds=new Map(definitions.map(role=>[role.id,role.kind]));
|
|
61
|
+
if(!config||config.schemaVersion!==1||!config.clients||typeof config.clients!=='object'||Array.isArray(config.clients))invalidExplicit();
|
|
62
|
+
const selected=config.selectedRoles??roleIds;
|
|
63
|
+
if(!Array.isArray(selected)||!selected.length||new Set(selected).size!==selected.length||selected.some(role=>!roleIds.includes(role)))invalidExplicit();
|
|
64
|
+
const roles=roleIds.filter(role=>selected.includes(role));
|
|
65
|
+
const options={roles:roles.join(','),'max-concurrent':String(config.maxConcurrent)};
|
|
66
|
+
for(const [client,preferences]of Object.entries(config.clients)){
|
|
67
|
+
const flag=Object.keys(TOOL_FLAGS).find(flag=>TOOL_FLAGS[flag]===client);if(!flag||!preferences)invalidExplicit();
|
|
68
|
+
options[flag]=roles.map(role=>{
|
|
69
|
+
const selected=preferences.roles?.[role]??preferences[kinds.get(role)];
|
|
70
|
+
if(!selected||typeof selected.model!=='string'||!EXPLICIT_MODEL.test(selected.model)||(selected.effort!==null&&!EXPLICIT_EFFORTS.includes(selected.effort)))invalidExplicit();
|
|
71
|
+
if(['agy','grok'].includes(client)&&selected.effort!==null)invalidExplicit();
|
|
72
|
+
if(client==='grok'&&grokEffortSuffix(selected.model))invalidExplicit();
|
|
73
|
+
return `${role}=${selected.model}${['agy','grok'].includes(client)?'':':'+(selected.effort??'native')}`;
|
|
74
|
+
}).join(',');
|
|
75
|
+
if(preferences.maxConcurrent!==undefined)options[flag+'-max-concurrent']=String(preferences.maxConcurrent);
|
|
76
|
+
if(!Array.isArray(preferences.avoidModels)||preferences.avoidModels.some(model=>typeof model!=='string'||!EXPLICIT_MODEL.test(model)))invalidExplicit();
|
|
77
|
+
if(preferences.avoidModels.length)options['avoid-'+flag]=preferences.avoidModels.join(',');
|
|
78
|
+
}
|
|
79
|
+
parseExplicitOptions(options,roleIds);
|
|
80
|
+
return options;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatExplicitCommand(config,version,roleCatalog){
|
|
84
|
+
if(typeof version!=='string'||!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))throw new Error('Invalid release version.');
|
|
85
|
+
const options=explicitOptions(config,roleCatalog),quote=value=>"'"+value.replaceAll("'","'\"'\"'")+"'";
|
|
86
|
+
const lines=[`npx --yes --ignore-scripts --package agent-policy-kit@${version} agent-policy setup`];
|
|
87
|
+
for(const [name,value]of Object.entries(options))lines.push(`--${name} ${name==='max-concurrent'||name.endsWith('-max-concurrent')?value:quote(value)}`);
|
|
88
|
+
lines.push('--apply');return lines.join(' \\\n ');
|
|
89
|
+
}
|
package/src/inspect.mjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import {constants} from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {parseToml} from './toml.mjs';
|
|
5
|
+
import {CLIENTS} from './policy.mjs';
|
|
6
|
+
import {inspectConfiguration} from './install.mjs';
|
|
7
|
+
|
|
8
|
+
const ROOTS={codex:'.codex',claude:'.claude',kimi:'.kimi-code',agy:'.gemini',grok:'.grok'};
|
|
9
|
+
const ENV_ROOTS={codex:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'};
|
|
10
|
+
const MAX_FILE=1024*1024,MAX_TOTAL=4*MAX_FILE,MAX_ENTRIES=256;
|
|
11
|
+
const identifier=/^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
|
|
12
|
+
const namePattern=/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,79}$/;
|
|
13
|
+
const record=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
|
|
14
|
+
const failure=code=>Object.assign(new Error(code),{code});
|
|
15
|
+
function safeString(value,pattern=identifier){
|
|
16
|
+
if(typeof value!=='string'||!pattern.test(value)||/^(?:sk-|ghp_|github_pat_)/.test(value)||/\/(?:Users|home)\//.test(value))throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
async function safePath(target){
|
|
20
|
+
let current=path.parse(target).root;
|
|
21
|
+
for(const part of path.relative(current,target).split(path.sep).filter(Boolean)){
|
|
22
|
+
current=path.join(current,part);
|
|
23
|
+
try{if((await fs.lstat(current)).isSymbolicLink())throw failure('UNSAFE_FILE');}
|
|
24
|
+
catch(error){if(error.code==='ENOENT')return;throw error;}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readFile(file,budget){
|
|
28
|
+
await safePath(file);
|
|
29
|
+
let handle;
|
|
30
|
+
try{
|
|
31
|
+
handle=await fs.open(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
|
|
32
|
+
const before=await handle.stat();
|
|
33
|
+
if(!before.isFile())throw failure('UNSAFE_FILE');
|
|
34
|
+
if(before.size>MAX_FILE||budget.bytes+before.size>MAX_TOTAL)throw failure('INSPECTION_LIMIT');
|
|
35
|
+
const buffer=Buffer.alloc(MAX_FILE+1);let size=0;
|
|
36
|
+
while(size<buffer.length){const chunk=await handle.read(buffer,size,buffer.length-size,null);if(!chunk.bytesRead)break;size+=chunk.bytesRead;}
|
|
37
|
+
const after=await handle.stat();
|
|
38
|
+
budget.bytes+=size;
|
|
39
|
+
if(size>MAX_FILE||budget.bytes>MAX_TOTAL)throw failure('INSPECTION_LIMIT');
|
|
40
|
+
if(before.size!==after.size||before.mtimeMs!==after.mtimeMs||size!==after.size)throw failure('CONFIG_CHANGED_DURING_READ');
|
|
41
|
+
return buffer.subarray(0,size).toString('utf8');
|
|
42
|
+
}catch(error){if(error.code==='ENOENT')return null;throw error;}
|
|
43
|
+
finally{await handle?.close();}
|
|
44
|
+
}
|
|
45
|
+
function warning(client,error){
|
|
46
|
+
const allowed=['UNSAFE_FILE','INSPECTION_LIMIT','CONFIG_CHANGED_DURING_READ','UNSUPPORTED_NATIVE_SETTING','UNSUPPORTED_ROLE_FORMAT','UNSUPPORTED_ROLE_EFFORT','INVALID_TOML','INVALID_JSON','EACCES','EPERM','NATIVE_CAP_CONFLICT','EXTERNAL_ROLE_REFERENCES_NOT_READ'];
|
|
47
|
+
const code=allowed.includes(error?.code)?error.code:'NATIVE_READ_FAILED';
|
|
48
|
+
if(!client.warnings.includes(code))client.warnings.push(code);
|
|
49
|
+
client.status='attention';
|
|
50
|
+
}
|
|
51
|
+
function scalar(value){
|
|
52
|
+
const text=value.trim();
|
|
53
|
+
if(text.startsWith('"')){const match=text.match(/^("(?:[^"\\]|\\.)*")\s*(?:#.*)?$/);if(!match)throw failure('UNSUPPORTED_ROLE_FORMAT');try{return JSON.parse(match[1]);}catch{throw failure('UNSUPPORTED_ROLE_FORMAT');}}
|
|
54
|
+
if(text.startsWith("'")){const match=text.match(/^'((?:[^']|'')*)'\s*(?:#.*)?$/);if(!match)throw failure('UNSUPPORTED_ROLE_FORMAT');return match[1].replaceAll("''", "'");}
|
|
55
|
+
const match=text.match(/^([A-Za-z0-9][A-Za-z0-9._:@/\[\]-]*)(?:\s+#.*)?$/);
|
|
56
|
+
if(!match)throw failure('UNSUPPORTED_ROLE_FORMAT');
|
|
57
|
+
return match[1];
|
|
58
|
+
}
|
|
59
|
+
function frontmatter(text){
|
|
60
|
+
const match=text.replace(/^\uFEFF/,'').match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
61
|
+
if(!match)throw failure('UNSUPPORTED_ROLE_FORMAT');
|
|
62
|
+
const fields={};
|
|
63
|
+
for(const line of match[1].split(/\r?\n/)){
|
|
64
|
+
// YAML merges, quoted keys and flow mappings can supply model fields that
|
|
65
|
+
// this deliberately small scalar reader cannot resolve safely.
|
|
66
|
+
if(/^(?:<<\s*:|["'{\[])/.test(line))throw failure('UNSUPPORTED_ROLE_FORMAT');
|
|
67
|
+
const field=line.match(/^(name|model|effort|subagent)[ \t]*:\s*(.*)$/);
|
|
68
|
+
if(!field)continue;
|
|
69
|
+
if(Object.hasOwn(fields,field[1]))throw failure('UNSUPPORTED_ROLE_FORMAT');
|
|
70
|
+
if(field[1]==='subagent'){
|
|
71
|
+
const boolean=field[2].trim().match(/^(true|false)(?:\s+#.*)?$/);
|
|
72
|
+
if(!boolean)throw failure('UNSUPPORTED_ROLE_FORMAT');
|
|
73
|
+
fields.subagent=boolean[1]==='true';
|
|
74
|
+
}else fields[field[1]]=scalar(field[2]);
|
|
75
|
+
}
|
|
76
|
+
if(!fields.name)throw failure('UNSUPPORTED_ROLE_FORMAT');
|
|
77
|
+
return fields;
|
|
78
|
+
}
|
|
79
|
+
function cap(value){if(!Number.isSafeInteger(value)||value<1)throw failure('UNSUPPORTED_NATIVE_SETTING');return value;}
|
|
80
|
+
function optionalString(value,pattern){return value===undefined?null:safeString(value,pattern);}
|
|
81
|
+
function extractSettings(client,id,native,env,models){
|
|
82
|
+
if(!record(native))throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
83
|
+
if(id==='grok'){
|
|
84
|
+
const subagents=native.subagents??{};if(!record(subagents))throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
85
|
+
const configured=subagents.models??{};if(!record(configured)||Object.keys(configured).length>MAX_ENTRIES)throw failure(record(configured)?'INSPECTION_LIMIT':'UNSUPPORTED_NATIVE_SETTING');
|
|
86
|
+
for(const[name,model]of Object.entries(configured))models.set(safeString(name,namePattern),safeString(model));
|
|
87
|
+
client.defaults.model=models.get('general-purpose')??null;
|
|
88
|
+
if(subagents.max_concurrent!==undefined)client.concurrency.value=cap(subagents.max_concurrent);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if(id==='codex'){
|
|
92
|
+
const agents=native.agents??{};if(!record(agents))throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
93
|
+
client.defaults.model=optionalString(agents.default_subagent_model);
|
|
94
|
+
client.defaults.effort=optionalString(agents.default_subagent_reasoning_effort);
|
|
95
|
+
if(agents.enabled!==undefined){if(typeof agents.enabled!=='boolean')throw failure('UNSUPPORTED_NATIVE_SETTING');client.defaults.enabled=agents.enabled;}
|
|
96
|
+
const current=agents.max_concurrent_threads_per_session,legacy=agents.max_threads;
|
|
97
|
+
if(current!==undefined&&legacy!==undefined&¤t!==legacy)throw failure('NATIVE_CAP_CONFLICT');
|
|
98
|
+
if(current!==undefined||legacy!==undefined)client.concurrency.value=cap(current??legacy);
|
|
99
|
+
if(Object.values(agents).some(value=>record(value)&&value.config_file!==undefined))warning(client,failure('EXTERNAL_ROLE_REFERENCES_NOT_READ'));
|
|
100
|
+
}else if(id==='claude'){
|
|
101
|
+
const stored=native.env??{};if(!record(stored))throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
102
|
+
client.defaults.model=optionalString(stored.CLAUDE_CODE_SUBAGENT_MODEL);
|
|
103
|
+
const count=stored.CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS;
|
|
104
|
+
if(count!==undefined){if(typeof count!=='string'||!/^\d{1,9}$/.test(count))throw failure('UNSUPPORTED_NATIVE_SETTING');client.concurrency.value=cap(Number(count));}
|
|
105
|
+
client.overrides={};
|
|
106
|
+
for(const[key,field]of [['CLAUDE_CODE_SUBAGENT_MODEL','model'],['CLAUDE_CODE_EFFORT_LEVEL','effort']]){
|
|
107
|
+
const value=env[key]??(field==='effort'?stored[key]:undefined);
|
|
108
|
+
if(value!==undefined)client.overrides[field]=safeString(value);
|
|
109
|
+
}
|
|
110
|
+
if((env.CLAUDE_CODE_SUBAGENT_MODEL_FORCE??stored.CLAUDE_CODE_SUBAGENT_MODEL_FORCE)==='1')client.overrides.forceModel=true;
|
|
111
|
+
if(env.CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS!==undefined){if(!/^\d{1,9}$/.test(env.CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS))throw failure('UNSUPPORTED_NATIVE_SETTING');client.overrides.maxConcurrent=cap(Number(env.CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS));}
|
|
112
|
+
}else if(id==='kimi'){
|
|
113
|
+
const secondary=native.secondary_model??{};if(!record(secondary))throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
114
|
+
client.defaults.model=optionalString(secondary.default_model);
|
|
115
|
+
client.defaults.effort=optionalString(secondary.default_effort);
|
|
116
|
+
const pool=secondary.models??{};if(!record(pool)||Object.keys(pool).length>MAX_ENTRIES)throw failure('INSPECTION_LIMIT');
|
|
117
|
+
const aliases=new Set([...Object.keys(pool),...(client.defaults.model?[client.defaults.model]:[])]);
|
|
118
|
+
for(const alias of aliases){
|
|
119
|
+
const entry=native.models?.[alias];
|
|
120
|
+
client.pool.push({alias:safeString(alias),effort:optionalString(secondary.default_effort??entry?.overrides?.default_effort??entry?.default_effort),configured:record(entry)});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function rolesIn(directory,client,id,budget,models,depth=0){
|
|
125
|
+
await safePath(directory);
|
|
126
|
+
let directoryHandle;
|
|
127
|
+
try{directoryHandle=await fs.opendir(directory);}catch(error){if(error.code==='ENOENT')return;throw error;}
|
|
128
|
+
const entries=[];
|
|
129
|
+
for await(const entry of directoryHandle){if(++budget.entries>MAX_ENTRIES)throw failure('INSPECTION_LIMIT');entries.push(entry);}
|
|
130
|
+
for(const entry of entries.sort((a,b)=>a.name.localeCompare(b.name,'en'))){
|
|
131
|
+
const file=path.join(directory,entry.name);
|
|
132
|
+
try{
|
|
133
|
+
if(entry.isSymbolicLink())throw failure('UNSAFE_FILE');
|
|
134
|
+
if(entry.isDirectory()){
|
|
135
|
+
if(id==='codex')continue;
|
|
136
|
+
if(depth>=4)throw failure('INSPECTION_LIMIT');
|
|
137
|
+
await rolesIn(file,client,id,budget,models,depth+1);continue;
|
|
138
|
+
}
|
|
139
|
+
if(!entry.name.endsWith(id==='codex'?'.toml':'.md'))continue;
|
|
140
|
+
const body=await readFile(file,budget);if(body===null)throw failure('CONFIG_CHANGED_DURING_READ');
|
|
141
|
+
const fields=id==='codex'?parseToml(body):frontmatter(body);
|
|
142
|
+
const role={name:safeString(fields.name,namePattern),model:optionalString(fields.model),effort:optionalString(id==='codex'?fields.model_reasoning_effort:fields.effort)};
|
|
143
|
+
if(id==='kimi'){
|
|
144
|
+
// APK's Markdown role files contain explicit routing guidance, not a
|
|
145
|
+
// native model binding. Other prose is never interpreted as configuration.
|
|
146
|
+
role.model=null;role.effort=null;role.modelSelection='parent';
|
|
147
|
+
if(role.name.startsWith('apk-')){
|
|
148
|
+
const routing=body.match(/^The parent must select configured pool alias ([A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}) for this role; Markdown frontmatter does not select a model\. Requested effort: (native default|[A-Za-z0-9_-]+)\.$/m);
|
|
149
|
+
if(routing){role.model=safeString(routing[1]);role.effort=routing[2]==='native default'?null:safeString(routing[2]);role.modelSelection='guidance';}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if(id==='grok'){
|
|
153
|
+
// The native per-subagent model override lives in config.toml, not in the agent file.
|
|
154
|
+
role.model=models.get(role.name)??role.model;
|
|
155
|
+
if(fields.effort!==undefined){role.effort=null;warning(client,failure('UNSUPPORTED_ROLE_EFFORT'));}
|
|
156
|
+
}
|
|
157
|
+
if(id==='agy'){
|
|
158
|
+
if(role.model!==null&&!['pro','flash','flash_lite','inherit'].includes(role.model))throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
159
|
+
role.enabled=fields.subagent??null;
|
|
160
|
+
if(fields.effort!==undefined){role.effort=null;warning(client,failure('UNSUPPORTED_ROLE_EFFORT'));}
|
|
161
|
+
}
|
|
162
|
+
if(client.roles.some(r=>r.name===role.name))throw failure('UNSUPPORTED_ROLE_FORMAT');
|
|
163
|
+
client.roles.push(role);
|
|
164
|
+
}catch(error){warning(client,error);}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function readGuidance(root,client,budget){
|
|
168
|
+
const text=await readFile(path.join(root,'AGENTS.md'),budget);
|
|
169
|
+
if(text===null)return;
|
|
170
|
+
const blocks=[...text.matchAll(/<!-- BEGIN agent-policy-kit -->\r?\n([\s\S]*?)<!-- END agent-policy-kit -->/g)];
|
|
171
|
+
if(!blocks.length)return;
|
|
172
|
+
if(blocks.length!==1)throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
173
|
+
const counts=[...blocks[0][1].matchAll(/^When delegating, run at most (\d+) subagents concurrently in this session\./gm)];
|
|
174
|
+
if(counts.length!==1)throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
175
|
+
client.concurrency.value=cap(Number(counts[0][1]));
|
|
176
|
+
}
|
|
177
|
+
export async function readConfiguration({client=null,home,env=process.env,explicitHome=false,stateDir=path.join(home,'.agent-policy-kit')}={}){
|
|
178
|
+
if(!path.isAbsolute(home)||client!==null&&!CLIENTS.includes(client))throw failure('INVALID_CLIENT');
|
|
179
|
+
const report={schemaVersion:1,scope:'user',modelExecution:'not_checked',clients:{}};
|
|
180
|
+
for(const id of client?[client]:CLIENTS){
|
|
181
|
+
const root=path.resolve(!explicitHome&&env[ENV_ROOTS[id]]?env[ENV_ROOTS[id]]:path.join(home,ROOTS[id]));
|
|
182
|
+
const c={status:'not_configured',defaults:{model:null,effort:null,enabled:null},concurrency:{value:null,kind:['codex','claude','grok'].includes(id)?'native_setting':'guidance'},roles:[],pool:[],warnings:[],apk:{status:'not_checked'}};
|
|
183
|
+
report.clients[id]=c;const budget={bytes:0,entries:0},models=new Map();
|
|
184
|
+
try{
|
|
185
|
+
await safePath(root);
|
|
186
|
+
if(id!=='agy'){
|
|
187
|
+
try{
|
|
188
|
+
const text=await readFile(path.join(root,id==='claude'?'settings.json':'config.toml'),budget);
|
|
189
|
+
if(text!==null){let native;if(id==='claude'){try{native=JSON.parse(text);}catch{throw failure('INVALID_JSON');}}else native=parseToml(text);extractSettings(c,id,native,env,models);}
|
|
190
|
+
else if(id==='claude')extractSettings(c,id,{},env,models);
|
|
191
|
+
}catch(error){warning(c,error);}
|
|
192
|
+
}
|
|
193
|
+
await rolesIn(path.join(root,id==='agy'?'config/agents':'agents'),c,id,budget,models);
|
|
194
|
+
if(id==='kimi'||id==='agy'){try{await readGuidance(root,c,budget);}catch(error){warning(c,error);}}
|
|
195
|
+
if(c.status!=='attention'&&(c.roles.length||c.pool.length||c.defaults.model!==null||c.defaults.effort!==null||c.defaults.enabled!==null||c.concurrency.value!==null||Object.keys(c.overrides??{}).length))c.status='configured';
|
|
196
|
+
if(root===path.resolve(home,ROOTS[id])){
|
|
197
|
+
try{const installed=await inspectConfiguration({client:id,home,stateDir});c.apk.status=installed.status;if(installed.status==='verified'){c.apk.releaseVersion=installed.releaseVersion;c.apk.preset=installed.preset;}}
|
|
198
|
+
catch(error){c.apk.status=error.code==='OWNED_DRIFT'?'modified':'attention';warning(c,error);}
|
|
199
|
+
}
|
|
200
|
+
}catch(error){warning(c,error);}
|
|
201
|
+
}
|
|
202
|
+
return report;
|
|
203
|
+
}
|