agent-policy-kit 0.1.0 → 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 +9 -5
- package/CHANGELOG.md +40 -0
- package/README.md +21 -4
- package/bin/agent-policy.mjs +25 -10
- package/docs/architecture.md +1 -1
- package/docs/compatibility.md +2 -0
- package/docs/configuration-page.md +12 -6
- package/docs/deployment.md +31 -0
- package/docs/design/2026-11-01-grok-build-support.md +30 -0
- package/docs/development.md +1 -1
- package/docs/installation.md +9 -1
- package/docs/model-catalog.md +44 -36
- package/docs/npm-release.md +4 -2
- package/docs/policy.md +3 -3
- package/docs/releases.md +4 -4
- package/docs/verification.md +23 -2
- package/models/catalog.json +44 -0
- package/package.json +7 -3
- package/policies/balanced.json +23 -0
- package/policies/fast.json +12 -1
- package/policies/quality.json +28 -7
- package/schema/policy.schema.json +98 -0
- package/site/index.html +237 -84
- package/src/detect.mjs +2 -1
- package/src/explicit.mjs +14 -7
- package/src/inspect.mjs +203 -0
- package/src/install.mjs +20 -4
- package/src/native.mjs +8 -4
- package/src/output.mjs +108 -0
- package/src/policy.mjs +3 -2
- package/src/release.mjs +1 -1
- package/src/share.mjs +47 -16
package/src/detect.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
|
|
7
|
-
const CLIENTS = ['codex','claude','kimi','agy'];
|
|
7
|
+
const CLIENTS = ['codex','claude','kimi','agy','grok'];
|
|
8
8
|
const OUTPUT_LIMIT = 65536;
|
|
9
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
10
|
function failure(code) { return Object.assign(new Error(code),{code}); }
|
|
@@ -82,6 +82,7 @@ function identity(client,output) {
|
|
|
82
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
83
|
kimi:[new RegExp(String.raw`^\s*kimi(?:\s+code(?:\s+cli)?)?(?:,?\s+version)?\s+v?${VERSION}(?:\s|$)`,'im')],
|
|
84
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')],
|
|
85
86
|
};
|
|
86
87
|
for (const pattern of patterns[client]) { const match = output.match(pattern); if (match && match[1].length <= 128) return match[1]; }
|
|
87
88
|
return null;
|
package/src/explicit.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
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'});
|
|
3
|
-
export const EXPLICIT_FIELDS=Object.freeze(['roles','max-concurrent',...Object.keys(TOOL_FLAGS),...Object.keys(TOOL_FLAGS).map(flag=>'avoid-'+flag)]);
|
|
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
4
|
const EXPLICIT_MODEL=/^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
|
|
5
5
|
const EXPLICIT_EFFORTS=['none','minimal','low','medium','high','xhigh','max'];
|
|
6
6
|
function invalidExplicit(){throw Object.assign(new Error('Invalid explicit configuration.'),{code:'INVALID_EXPLICIT_CONFIG'});}
|
|
@@ -11,6 +11,8 @@ function explicitList(value,limit){
|
|
|
11
11
|
return list;
|
|
12
12
|
}
|
|
13
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));};
|
|
14
16
|
export function parseExplicitOptions(options,roleIds){
|
|
15
17
|
if(!options||typeof options!=='object'||Array.isArray(options))invalidExplicit();
|
|
16
18
|
const fields=Object.getOwnPropertyDescriptors(options);
|
|
@@ -21,7 +23,7 @@ export function parseExplicitOptions(options,roleIds){
|
|
|
21
23
|
if(requested.some(role=>!roleIds.includes(role)))invalidExplicit();
|
|
22
24
|
const roles=roleIds.filter(role=>requested.includes(role)),maxConcurrent=Number(options['max-concurrent']);
|
|
23
25
|
if(!/^[1-9]\d*$/.test(options['max-concurrent'])||!Number.isInteger(maxConcurrent)||maxConcurrent>32)invalidExplicit();
|
|
24
|
-
for(const flag of Object.keys(TOOL_FLAGS))if(options['avoid-'+flag]!==undefined&&options[flag]===undefined)invalidExplicit();
|
|
26
|
+
for(const flag of Object.keys(TOOL_FLAGS))if((options['avoid-'+flag]!==undefined||options[flag+'-max-concurrent']!==undefined)&&options[flag]===undefined)invalidExplicit();
|
|
25
27
|
const clients={};
|
|
26
28
|
for(const flag of Object.keys(options).filter(flag=>Object.hasOwn(TOOL_FLAGS,flag))){
|
|
27
29
|
const client=TOOL_FLAGS[flag],choices={};
|
|
@@ -30,6 +32,7 @@ export function parseExplicitOptions(options,roleIds){
|
|
|
30
32
|
if(at<1||!roles.includes(role)||Object.hasOwn(choices,role))invalidExplicit();
|
|
31
33
|
let model=value,effort=null;
|
|
32
34
|
if(client==='agy'){if(!['pro','flash','flash_lite','inherit'].includes(model))invalidExplicit();}
|
|
35
|
+
else if(client==='grok'){if(grokEffortSuffix(value))invalidExplicit();}
|
|
33
36
|
else{
|
|
34
37
|
const colon=value.lastIndexOf(':');if(colon<1)invalidExplicit();
|
|
35
38
|
model=value.slice(0,colon);const level=value.slice(colon+1);
|
|
@@ -41,7 +44,9 @@ export function parseExplicitOptions(options,roleIds){
|
|
|
41
44
|
const avoidModels=options['avoid-'+flag]===undefined?[]:explicitList(options['avoid-'+flag],32);
|
|
42
45
|
if(avoidModels.some(model=>!EXPLICIT_MODEL.test(model)))invalidExplicit();
|
|
43
46
|
const first=choices[roles[0]];
|
|
44
|
-
|
|
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};
|
|
45
50
|
}
|
|
46
51
|
if(!Object.keys(clients).length)invalidExplicit();
|
|
47
52
|
return {schemaVersion:1,maxConcurrent,...(roles.length===roleIds.length?{}:{selectedRoles:roles}),clients};
|
|
@@ -63,9 +68,11 @@ export function explicitOptions(config,roleCatalog){
|
|
|
63
68
|
options[flag]=roles.map(role=>{
|
|
64
69
|
const selected=preferences.roles?.[role]??preferences[kinds.get(role)];
|
|
65
70
|
if(!selected||typeof selected.model!=='string'||!EXPLICIT_MODEL.test(selected.model)||(selected.effort!==null&&!EXPLICIT_EFFORTS.includes(selected.effort)))invalidExplicit();
|
|
66
|
-
if(
|
|
67
|
-
|
|
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')}`;
|
|
68
74
|
}).join(',');
|
|
75
|
+
if(preferences.maxConcurrent!==undefined)options[flag+'-max-concurrent']=String(preferences.maxConcurrent);
|
|
69
76
|
if(!Array.isArray(preferences.avoidModels)||preferences.avoidModels.some(model=>typeof model!=='string'||!EXPLICIT_MODEL.test(model)))invalidExplicit();
|
|
70
77
|
if(preferences.avoidModels.length)options['avoid-'+flag]=preferences.avoidModels.join(',');
|
|
71
78
|
}
|
|
@@ -77,6 +84,6 @@ export function formatExplicitCommand(config,version,roleCatalog){
|
|
|
77
84
|
if(typeof version!=='string'||!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))throw new Error('Invalid release version.');
|
|
78
85
|
const options=explicitOptions(config,roleCatalog),quote=value=>"'"+value.replaceAll("'","'\"'\"'")+"'";
|
|
79
86
|
const lines=[`npx --yes --ignore-scripts --package agent-policy-kit@${version} agent-policy setup`];
|
|
80
|
-
for(const [name,value]of Object.entries(options))lines.push(`--${name} ${name==='max-concurrent'?value:quote(value)}`);
|
|
87
|
+
for(const [name,value]of Object.entries(options))lines.push(`--${name} ${name==='max-concurrent'||name.endsWith('-max-concurrent')?value:quote(value)}`);
|
|
81
88
|
lines.push('--apply');return lines.join(' \\\n ');
|
|
82
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
|
+
}
|
package/src/install.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import catalog from '../models/catalog.json' with {type:'json'};
|
|
|
10
10
|
import { parseToml, setTomlValue, restoreTomlValue, removeTomlValue } from './toml.mjs';
|
|
11
11
|
import { nativeLocations, nativeRoles, instructionBlock, nativeWarnings, BEGIN, END } from './native.mjs';
|
|
12
12
|
|
|
13
|
-
const CLIENTS = ['codex', 'claude', 'kimi', 'agy'];
|
|
13
|
+
const CLIENTS = ['codex', 'claude', 'kimi', 'agy', 'grok'];
|
|
14
14
|
const LIMIT = 1024 * 1024;
|
|
15
15
|
const digest = value => value === null ? null : createHash('sha256').update(value).digest('hex');
|
|
16
16
|
const json = value => JSON.stringify(value, null, 2) + '\n';
|
|
@@ -156,6 +156,7 @@ function selectedAliases(preferences,selectedRoles=ROLE_IDS) {
|
|
|
156
156
|
function tomlSettings(client,preferences,maxConcurrent,selectedRoles=ROLE_IDS) {
|
|
157
157
|
const worker=rolePreference(preferences,selectedRoles[0]);
|
|
158
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]];
|
|
159
160
|
if(client!=='kimi')return [];
|
|
160
161
|
const selected=selectedAliases(preferences,selectedRoles),roles=effectiveRoles(preferences,selectedRoles);
|
|
161
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])];
|
|
@@ -182,7 +183,7 @@ function scalar(op,target,type) {
|
|
|
182
183
|
literalValue(op.before,type,true);
|
|
183
184
|
return literalValue(op.after,type);
|
|
184
185
|
}
|
|
185
|
-
function validateTomlSnapshot(file,client,schemaVersion) {
|
|
186
|
+
function validateTomlSnapshot(file,client,schemaVersion,selectedRoles=ROLE_IDS) {
|
|
186
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');
|
|
187
188
|
if(client==='codex') {
|
|
188
189
|
if(!(schemaVersion===2?[3]:[2,3]).includes(file.ops.length))fail('INVALID_RECEIPT');
|
|
@@ -191,6 +192,14 @@ function validateTomlSnapshot(file,client,schemaVersion) {
|
|
|
191
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');
|
|
192
193
|
return;
|
|
193
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
|
+
}
|
|
194
203
|
if(client!=='kimi')fail('INVALID_RECEIPT');
|
|
195
204
|
if(schemaVersion===2) {
|
|
196
205
|
if(![4,6].includes(file.ops.length))fail('INVALID_RECEIPT');
|
|
@@ -253,7 +262,7 @@ function validateReceipt(r,plan) {
|
|
|
253
262
|
} else if(plan.client==='claude') {
|
|
254
263
|
fields(file,['path','type','existed','ops','created']);if(file.type!=='json')fail('INVALID_RECEIPT');validateJsonSnapshot(file);
|
|
255
264
|
} else {
|
|
256
|
-
fields(file,['path','type','existed','ops']);if(file.type!=='toml')fail('INVALID_RECEIPT');validateTomlSnapshot(file,plan.client,r.schemaVersion);
|
|
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);
|
|
257
266
|
}
|
|
258
267
|
}
|
|
259
268
|
return r;
|
|
@@ -348,7 +357,7 @@ export async function planInstall(options) {
|
|
|
348
357
|
if(!own(policy.clients,plan.client))fail('INVALID_POLICY');
|
|
349
358
|
let selected=policy.clients[plan.client],resolvedAliases=false;
|
|
350
359
|
const roleIds=selectedRoleIds(policy);
|
|
351
|
-
const maxConcurrent=policy.maxConcurrent,policyHash=digest(json(canonical(policy)));
|
|
360
|
+
const maxConcurrent=selected.maxConcurrent??policy.maxConcurrent,policyHash=digest(json(canonical(policy)));
|
|
352
361
|
const policyBefore=await readDependency(plan,l.policy,plan.stateDir);
|
|
353
362
|
const baselines=record?await undoFiles(plan,record,false):new Map();
|
|
354
363
|
if(record&&!replace) {
|
|
@@ -373,6 +382,13 @@ export async function planInstall(options) {
|
|
|
373
382
|
const required=kimiPrerequisite(native,selected,roleIds);if(required)return prerequisite(plan,required);
|
|
374
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');
|
|
375
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
|
+
}
|
|
376
392
|
if(plan.client==='codex') {
|
|
377
393
|
if(native.agents&&own(native.agents,'max_threads'))fail('CODEX_CAP_ALIAS_CONFLICT');
|
|
378
394
|
const cap=native.agents?.max_concurrent_threads_per_session;
|
package/src/native.mjs
CHANGED
|
@@ -4,9 +4,9 @@ import {ROLE_DEFINITIONS,ROLE_IDS} from './roles.mjs';
|
|
|
4
4
|
|
|
5
5
|
export const BEGIN = '<!-- BEGIN agent-policy-kit -->';
|
|
6
6
|
export const END = '<!-- END agent-policy-kit -->';
|
|
7
|
-
const directory = {codex:'.codex',claude:'.claude',kimi:'.kimi-code',agy:'.gemini'};
|
|
7
|
+
const directory = {codex:'.codex',claude:'.claude',kimi:'.kimi-code',agy:'.gemini',grok:'.grok'};
|
|
8
8
|
export function nativeHomeConflict(client,home,env) {
|
|
9
|
-
const variable={codex:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME'}[client];
|
|
9
|
+
const variable={codex:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'}[client];
|
|
10
10
|
const active=variable&&env[variable];
|
|
11
11
|
return typeof active==='string'&&active.length>0&&path.resolve(active)!==path.resolve(home,directory[client]);
|
|
12
12
|
}
|
|
@@ -24,7 +24,10 @@ export function nativeRoles(client,config,selectedRoles=ROLE_IDS) {
|
|
|
24
24
|
const {description,prompt}=ROLE_DEFINITIONS.find(value=>value.id===role);
|
|
25
25
|
if(client==='codex') return Object.entries({name,description,model:selected.model,...(selected.effort===null?{}:{model_reasoning_effort:selected.effort}),developer_instructions:prompt}).map(([k,v])=>`${k} = ${JSON.stringify(v)}\n`).join('');
|
|
26
26
|
const fields=[`name: ${name}`,`description: ${JSON.stringify(description)}`];
|
|
27
|
-
|
|
27
|
+
// Grok selects the model from config.toml [subagents.models]; read-only roles use the bundled plan permission mode.
|
|
28
|
+
if(client==='grok') {
|
|
29
|
+
if(['reviewer','explorer'].includes(role))fields.push('permission_mode: plan');
|
|
30
|
+
} else if(client==='kimi') {
|
|
28
31
|
if(['reviewer','explorer'].includes(role))fields.push('tools: [Read, Grep, Glob]');
|
|
29
32
|
} else {
|
|
30
33
|
fields.push(`model: ${JSON.stringify(selected.model)}`);
|
|
@@ -48,6 +51,7 @@ export function instructionBlock(client,config,maxConcurrent,selectedRoles=ROLE_
|
|
|
48
51
|
for(const role of selectedRoles)text.push(`When delegating to apk-${role}, use model tier ${rolePreference(config,role).model}.`);
|
|
49
52
|
text.push('Per-role effort and a hard concurrency cap are not supported by the documented native agent settings.');
|
|
50
53
|
}
|
|
54
|
+
else if(client==='grok')text.push('Use the named role definitions for model selection. Per-role effort is not supported by the documented native agent settings; subagents use the main session effort. Explicit spawn settings and higher-priority instructions can supersede user defaults.');
|
|
51
55
|
else if(client==='claude')text.push('Use the named role definitions for model and effort selection. Session, project, managed settings, environment overrides and some spawn paths can supersede these user defaults; the native cap applies to ordinary Agent-tool spawns with documented exceptions.');
|
|
52
56
|
else text.push('Use the named role definitions for model and effort selection. Explicit spawn settings and higher-priority instructions can supersede user defaults.');
|
|
53
57
|
if(client==='codex'||client==='claude')text.push('An omitted role effort uses the native client default; it does not pin a level.');
|
|
@@ -56,5 +60,5 @@ export function instructionBlock(client,config,maxConcurrent,selectedRoles=ROLE_
|
|
|
56
60
|
return text.join('\n')+'\n';
|
|
57
61
|
}
|
|
58
62
|
export function nativeWarnings(client) {
|
|
59
|
-
return client==='kimi'?['kimi-concurrency-guidance-only','kimi-model-pool-routing-guidance']:client==='agy'?['agy-concurrency-guidance-only','agy-per-role-effort-unsupported']:client==='claude'?['claude-native-precedence-limits']:['codex-native-precedence-limits'];
|
|
63
|
+
return client==='kimi'?['kimi-concurrency-guidance-only','kimi-model-pool-routing-guidance']:client==='agy'?['agy-concurrency-guidance-only','agy-per-role-effort-unsupported']:client==='grok'?['grok-per-role-effort-unsupported','grok-native-precedence-limits']:client==='claude'?['claude-native-precedence-limits']:['codex-native-precedence-limits'];
|
|
60
64
|
}
|
package/src/output.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {PRESETS} from './release.mjs';
|
|
2
|
+
const LABELS={codex:'Codex',claude:'Claude Code',kimi:'Kimi',agy:'Antigravity',grok:'Grok Build'};
|
|
3
|
+
const label=client=>LABELS[client]??'Tool';
|
|
4
|
+
const code=value=>typeof value==='string'&&/^[A-Za-z][A-Za-z0-9_-]{0,127}$/.test(value)?value:'OPERATION_FAILED';
|
|
5
|
+
const PRESET_LABELS=Object.fromEntries(PRESETS.map(entry=>[entry.id,entry.label]));
|
|
6
|
+
const preset=value=>Object.hasOwn(PRESET_LABELS,value)?' · '+PRESET_LABELS[value]:'';
|
|
7
|
+
const NOTES={
|
|
8
|
+
'codex-native-precedence-limits':'Project and explicit client settings can override these defaults.',
|
|
9
|
+
'claude-native-precedence-limits':'Project and explicit client settings can override these defaults.',
|
|
10
|
+
'kimi-concurrency-guidance-only':'Kimi: the concurrency limit is guidance.',
|
|
11
|
+
'kimi-model-pool-routing-guidance':'Kimi: the parent selects the model alias for each role.',
|
|
12
|
+
'agy-concurrency-guidance-only':'Antigravity: the concurrency limit is guidance.',
|
|
13
|
+
'agy-per-role-effort-unsupported':'Antigravity: native tiers do not support per-role effort.',
|
|
14
|
+
'grok-per-role-effort-unsupported':'Grok Build: native agent settings do not support per-role effort.',
|
|
15
|
+
'grok-native-precedence-limits':'Project and explicit client settings can override these defaults.',
|
|
16
|
+
'kimi-catalog-alias-resolved':'Kimi: an existing matching model alias was selected.',
|
|
17
|
+
};
|
|
18
|
+
const REASONS={
|
|
19
|
+
OWNED_DRIFT:'Local edits prevent an update',
|
|
20
|
+
'kimi-provisioned-models-required':'Required model aliases are missing',
|
|
21
|
+
'native-home-override-unsupported':'The active client profile uses a nonstandard location',
|
|
22
|
+
'client-not-selected':'This tool is not selected in the configuration',
|
|
23
|
+
'claude-force-model-conflict':'A forced model conflicts with the selected roles',
|
|
24
|
+
'kimi-supported-effort-required':'A selected alias does not declare the required effort support',
|
|
25
|
+
'kimi-main-effort-conflict':'The requested effort would change the main model defaults',
|
|
26
|
+
'kimi-thinking-disabled':'Thinking is disabled for a model that requires it',
|
|
27
|
+
'kimi-mixed-native-default-effort':'A shared effort override conflicts with native-default roles',
|
|
28
|
+
'kimi-catalog-alias-ambiguous':'More than one existing model alias matches',
|
|
29
|
+
'kimi-resolved-model-avoided':'The matching alias is on the avoid list',
|
|
30
|
+
'kimi-resolved-effort-conflict':'Roles sharing an alias request conflicting effort levels',
|
|
31
|
+
INSTALLATION_CONFLICT:'Installed preferences differ from this request',
|
|
32
|
+
POLICY_CONFLICT:'Saved preferences differ from this request',
|
|
33
|
+
RECOVERY_REQUIRED:'An interrupted transaction requires recovery',
|
|
34
|
+
INSTALL_LOCKED:'Another configuration operation holds the lock',
|
|
35
|
+
};
|
|
36
|
+
export const useJsonOutput=({isTTY,json=false}={})=>json||!isTTY;
|
|
37
|
+
function reason(value){const id=code(value);return (REASONS[id]??'Configuration could not be completed')+` (${id})`;}
|
|
38
|
+
function row(client,text,mark='✓'){return `${mark} ${label(client).padEnd(14)} ${text}`;}
|
|
39
|
+
function setup(result,{apply}){
|
|
40
|
+
const results=result.results??[],failed=results.some(r=>['error','prerequisite','invalid','timeout'].includes(r.status));
|
|
41
|
+
const changed=results.filter(r=>r.status==='configured');
|
|
42
|
+
const heading=failed?'Setup incomplete':result.status==='no_clients'?'No supported selected tools found':!apply?'Preview · no files changed':changed.length?'Configuration applied':'Already up to date';
|
|
43
|
+
const lines=[heading,''],missing=[],notes=new Set();
|
|
44
|
+
for(const r of results){
|
|
45
|
+
if(r.status==='not_selected')continue;
|
|
46
|
+
if(r.status==='not_found'){missing.push(label(r.client));continue;}
|
|
47
|
+
const warnings=r.warnings??r.plan?.warnings??[];
|
|
48
|
+
let text,mark='✓';
|
|
49
|
+
if(r.status==='configured')text='Configuration applied'+preset(r.appliedPreset);
|
|
50
|
+
else if(r.status==='unchanged'||r.status==='planned'&&r.plan?.files?.length===0)text='Already up to date'+preset(r.appliedPreset);
|
|
51
|
+
else if(r.status==='planned'){text='Would configure'+preset(r.targetPreset);mark='·';}
|
|
52
|
+
else if(r.status==='invalid'){text='Client executable identity could not be verified';mark='!';}
|
|
53
|
+
else if(r.status==='timeout'){text='Client detection timed out';mark='!';}
|
|
54
|
+
else {text=reason(r.error??warnings[0]);mark='!';}
|
|
55
|
+
lines.push(row(r.client,text,mark));
|
|
56
|
+
if(r.status==='prerequisite'&&r.appliedReleaseVersion&&/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(r.appliedReleaseVersion))lines.push(` Existing configuration retained · ${r.appliedReleaseVersion}${preset(r.appliedPreset)}`);
|
|
57
|
+
if(['configured','unchanged','planned'].includes(r.status))for(const warning of warnings)notes.add(NOTES[warning]??`Client note: ${code(warning)}`);
|
|
58
|
+
}
|
|
59
|
+
if(missing.length)lines.push(`Skipped: ${missing.join(', ')} — not installed.`);
|
|
60
|
+
if(changed.length)lines.push('',`Open a new session in ${changed.map(r=>label(r.client)).join(', ')} to load the changes.`);
|
|
61
|
+
if(results.some(r=>r.error==='OWNED_DRIFT'))lines.push('Review local edits before retrying; APK will not overwrite them.');
|
|
62
|
+
if(results.some(r=>(r.warnings??r.plan?.warnings??[]).includes('kimi-provisioned-models-required')))lines.push('Configure the selected model aliases in Kimi, then run the same command again.');
|
|
63
|
+
if(notes.size)lines.push('',...notes);
|
|
64
|
+
lines.push('Native configuration only; model execution was not checked.');
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
function uninstall(result,{apply}){
|
|
68
|
+
const results=result.results??(result.client?[result]:[]);
|
|
69
|
+
const lines=[!apply?'Preview · no files changed':result.status==='partial'?'Removal incomplete':result.status==='not_installed'?'Nothing to remove':'Configuration removal',''];
|
|
70
|
+
for(const r of results){const bad=r.status==='error';lines.push(row(r.client,bad?reason(r.error):r.status==='unchanged'||r.files?.length===0?'Nothing to remove':apply?'Configuration removed':'Would remove',bad?'!':apply?'✓':'·'));}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
const setting=value=>typeof value==='string'&&/^[A-Za-z0-9][A-Za-z0-9 _.:@/\[\]-]{0,127}$/.test(value)?value:'native default';
|
|
74
|
+
function show(result){
|
|
75
|
+
const lines=['User configuration · subagents',''];
|
|
76
|
+
for(const[id,c]of Object.entries(result.clients??{})){
|
|
77
|
+
lines.push(`${label(id)}${c.status==='attention'?' · incomplete read':c.status==='not_configured'?' · no explicit subagent settings':''}`);
|
|
78
|
+
if(c.status!=='not_configured'){
|
|
79
|
+
if(c.defaults.enabled===false)lines.push(' Subagents explicitly disabled.');
|
|
80
|
+
lines.push(` Default model: ${setting(c.defaults.model)} · effort: ${setting(c.defaults.effort)}`);
|
|
81
|
+
lines.push(` Max concurrent: ${Number.isSafeInteger(c.concurrency.value)?c.concurrency.value:'not explicitly set'}${c.concurrency.kind==='guidance'?' (guidance)':' (native setting)'}`);
|
|
82
|
+
for(const role of c.roles)lines.push(` ${setting(role.name).padEnd(22)} ${setting(role.model)} · ${setting(role.effort)}${role.modelSelection==='guidance'?' (routing guidance)':role.modelSelection==='parent'?' (parent selects model)':''}${role.enabled===false?' (delegation disabled)':''}`);
|
|
83
|
+
for(const entry of c.pool)lines.push(` Pool: ${setting(entry.alias)} · ${setting(entry.effort)}${entry.configured?'':' · alias not provisioned'}`);
|
|
84
|
+
for(const[field,value]of Object.entries(c.overrides??{}))lines.push(` Override ${field}: ${typeof value==='boolean'?String(value):typeof value==='number'?value:setting(value)}`);
|
|
85
|
+
}
|
|
86
|
+
if(c.apk.status==='modified')lines.push(' APK-managed files have local edits; the values above come from current files.');
|
|
87
|
+
for(const warning of c.warnings)lines.push(` ! ${code(warning)}`);
|
|
88
|
+
lines.push('');
|
|
89
|
+
}
|
|
90
|
+
lines.push('User-level files only; project, managed and session settings may override them.','Native configuration only; model execution was not checked.');
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
export function formatResult(command,result,{version='',apply=false}={}){
|
|
94
|
+
let lines;
|
|
95
|
+
if(result.error)lines=[`Operation failed (${code(result.error)})`];
|
|
96
|
+
else if(command==='setup'||command==='install')lines=setup(result,{apply});
|
|
97
|
+
else if(command==='uninstall')lines=uninstall(result,{apply});
|
|
98
|
+
else if(command==='show')lines=show(result);
|
|
99
|
+
else if(command==='init')lines=['Preferences created.'];
|
|
100
|
+
else if(command==='detect')lines=result.map(r=>row(r.client,r.status==='detected'?`Detected${typeof r.version==='string'&&/^[0-9A-Za-z.+-]{1,128}$/.test(r.version)?' · '+r.version:''}`:r.status==='not_found'?'Not installed':r.status==='timeout'?'Detection timed out':'Identity unverified',r.status==='detected'?'✓':r.status==='not_found'?'·':'!'));
|
|
101
|
+
else if(command==='doctor'){
|
|
102
|
+
lines=['Configuration check',''];
|
|
103
|
+
for(const[client,r]of Object.entries(result.clients??{})){const c=r.configuration;lines.push(row(client,c.status==='verified'?`Verified${preset(c.preset)}`:c.status==='not_configured'?'Not configured':reason(c.reason),c.status==='attention'?'!':c.status==='verified'?'✓':'·'));}
|
|
104
|
+
lines.push('Native configuration only; model execution was not checked.');
|
|
105
|
+
}else if(command==='release')lines=[`Configuration release ${/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(result.version)?result.version:''}${preset(result.preset)}`,'','Use setup --release to preview; add --apply to configure.'];
|
|
106
|
+
else lines=['Operation completed.'];
|
|
107
|
+
return [`Agent Policy Kit${/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)?' · '+version:''}`,'',...lines].join('\n')+'\n';
|
|
108
|
+
}
|
package/src/policy.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import catalog from '../models/catalog.json' with {type:'json'};
|
|
2
2
|
import {ROLES,ROLE_IDS,selectedRoleIds} from './roles.mjs';
|
|
3
3
|
export {ROLES} from './roles.mjs';
|
|
4
|
-
export const CLIENTS = Object.freeze(['codex', 'claude', 'kimi', 'agy']);
|
|
4
|
+
export const CLIENTS = Object.freeze(['codex', 'claude', 'kimi', 'agy', 'grok']);
|
|
5
5
|
export const rolePreference=(config,role)=>config.roles?.[role]??config[ROLES[role]];
|
|
6
6
|
export const NATIVE_EFFORTS=Object.freeze(Object.fromEntries(CLIENTS.map(client=>[client,Object.freeze([...catalog.clients[client].efforts])])));
|
|
7
7
|
const identifier = /^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
|
|
@@ -21,7 +21,8 @@ export function validatePolicy(value) {
|
|
|
21
21
|
object(value.clients,CLIENTS);
|
|
22
22
|
if (!Object.keys(value.clients).length) invalid();
|
|
23
23
|
for (const [client,config] of Object.entries(value.clients)) {
|
|
24
|
-
object(config,['code','light','roles','avoidModels']);
|
|
24
|
+
object(config,['code','light','roles','avoidModels','maxConcurrent']);
|
|
25
|
+
if(config.maxConcurrent!==undefined&&(!Number.isInteger(config.maxConcurrent)||config.maxConcurrent<1||config.maxConcurrent>32))invalid();
|
|
25
26
|
if(config.roles!==undefined)object(config.roles,Object.keys(ROLES));
|
|
26
27
|
for (const selected of [config.code,config.light,...Object.values(config.roles??{})]) {
|
|
27
28
|
object(selected,['model','effort']); model(selected.model);
|
package/src/release.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {validatePolicy} from './policy.mjs';
|
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_VERSION=JSON.parse(fs.readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
|
|
6
6
|
const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(key=>[key,canonical(value[key])])):value;
|
|
7
|
-
export const PRESETS=Object.freeze([{id:'fast',label:'
|
|
7
|
+
export const PRESETS=Object.freeze([{id:'fast',label:'Light',description:'Light models for every role. Limits last longest.'},{id:'balanced',label:'Standard',description:'Stronger models for code, review, tests and docs. Fits most plans.'},{id:'quality',label:'Top-tier',description:'High-end models for most roles. Uses limits fastest.',topModels:Object.freeze({codex:'gpt-6-astra',claude:'claude-fable-5-1',kimi:'kimi-code/k3.1-pro'})}].map(Object.freeze));
|
|
8
8
|
export function presetPreferences(id='balanced'){
|
|
9
9
|
if(!PRESETS.some(preset=>preset.id===id))throw Object.assign(new Error('Unknown preset.'),{code:'UNKNOWN_PRESET'});
|
|
10
10
|
return validatePolicy(JSON.parse(fs.readFileSync(new URL(`../policies/${id}.json`,import.meta.url),'utf8')));
|