agent-policy-kit 0.2.0 → 0.3.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 +8 -8
- package/CHANGELOG.md +41 -0
- package/README.md +16 -55
- package/bin/agent-policy.mjs +10 -10
- package/docs/assets/configurator.png +0 -0
- package/docs/assets/subagent-limits.svg +44 -0
- package/docs/compatibility.md +22 -6
- package/docs/configuration-page.md +10 -10
- package/docs/deployment.md +2 -0
- package/docs/development.md +10 -0
- package/docs/installation.md +3 -3
- package/docs/limits-evidence.md +24 -0
- package/docs/model-catalog.md +15 -15
- package/docs/policy.md +1 -1
- package/docs/releases.md +4 -4
- package/docs/use-cases.md +3 -3
- package/docs/verification.md +16 -0
- package/models/catalog.json +6 -6
- package/package.json +7 -3
- package/policies/balanced.json +2 -2
- package/policies/fast.json +2 -2
- package/policies/quality.json +2 -2
- package/schema/policy.schema.json +2 -2
- package/site/index.html +85 -46
- package/src/detect.mjs +61 -13
- package/src/explicit.mjs +4 -4
- package/src/inspect.mjs +12 -12
- package/src/install.mjs +5 -5
- package/src/native.mjs +8 -8
- package/src/output.mjs +5 -5
- package/src/policy.mjs +2 -2
- package/src/release.mjs +1 -1
- package/src/setup.mjs +1 -1
- package/src/share.mjs +1 -1
package/src/detect.mjs
CHANGED
|
@@ -4,17 +4,65 @@ 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 = ['
|
|
7
|
+
const CLIENTS = ['chatgpt','claude','kimi','gemini','grok'];
|
|
8
|
+
// Each tool's own executable keeps its native name: ChatGPT runs Codex, and Gemini runs Antigravity (agy).
|
|
9
|
+
const EXECUTABLES = {chatgpt:'codex',claude:'claude',kimi:'kimi',gemini:'agy',grok:'grok'};
|
|
8
10
|
const OUTPUT_LIMIT = 65536;
|
|
9
11
|
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
12
|
function failure(code) { return Object.assign(new Error(code),{code}); }
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const absolute = value => typeof value === 'string' && path.isAbsolute(value) && !value.includes('\0');
|
|
14
|
+
function options({env=process.env,hostHome=os.homedir(),timeoutMs=5000,platform=process.platform,systemApplications} = {}) {
|
|
15
|
+
// AGENT_POLICY_APPLICATIONS_DIR replaces /Applications, so hermetic runs never see the host's installed apps.
|
|
16
|
+
systemApplications ??= (typeof env?.AGENT_POLICY_APPLICATIONS_DIR === 'string' && env.AGENT_POLICY_APPLICATIONS_DIR) || '/Applications';
|
|
17
|
+
if (!env || typeof env !== 'object' || !absolute(hostHome) || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000 || typeof platform !== 'string' || !absolute(systemApplications)) throw failure('INVALID_DETECTION_OPTIONS');
|
|
18
|
+
return {env,hostHome:path.resolve(hostHome),timeoutMs,platform,systemApplications:path.resolve(systemApplications)};
|
|
14
19
|
}
|
|
15
|
-
|
|
20
|
+
// Desktop apps ship their own copy of a CLI and keep it off PATH. ChatGPT Desktop runs Codex from a
|
|
21
|
+
// fixed bundle path; Claude Desktop keeps one Claude Code per <version>/<build> directory.
|
|
22
|
+
const CHATGPT_CODEX = 'ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex';
|
|
23
|
+
const CLAUDE_DESKTOP = 'Library/Application Support/Claude/claude-code';
|
|
24
|
+
const DESKTOP_VERSION = /^(\d{1,4})\.(\d{1,4})\.(\d{1,4})$/;
|
|
25
|
+
const DESKTOP_BUILD = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
26
|
+
const DESKTOP_LIMIT = 8;
|
|
27
|
+
async function directories(dir) {
|
|
28
|
+
try { return (await fs.readdir(dir,{withFileTypes:true})).filter(entry=>entry.isDirectory()).map(entry=>entry.name); }
|
|
29
|
+
catch { return []; }
|
|
30
|
+
}
|
|
31
|
+
function newerFirst(a,b) {
|
|
32
|
+
const [x,y] = [a,b].map(name=>name.match(DESKTOP_VERSION).slice(1).map(Number));
|
|
33
|
+
return y[0]-x[0] || y[1]-x[1] || y[2]-x[2];
|
|
34
|
+
}
|
|
35
|
+
async function desktopCandidates(client,{hostHome,platform,systemApplications}) {
|
|
36
|
+
if (platform !== 'darwin') return [];
|
|
37
|
+
if (client === 'chatgpt') return [systemApplications,path.join(hostHome,'Applications')].map(dir=>path.join(dir,CHATGPT_CODEX));
|
|
38
|
+
if (client !== 'claude') return [];
|
|
39
|
+
const base = path.join(hostHome,CLAUDE_DESKTOP), files = [];
|
|
40
|
+
for (const version of (await directories(base)).filter(name=>DESKTOP_VERSION.test(name)).sort(newerFirst)) {
|
|
41
|
+
// Only builds that contain the CLI count toward the limit, so empty or partial folders cannot hide a working one.
|
|
42
|
+
// Within one version the most recently written CLI comes first; the name breaks ties.
|
|
43
|
+
const builds = [];
|
|
44
|
+
for (const build of (await directories(path.join(base,version))).filter(name=>DESKTOP_BUILD.test(name))) {
|
|
45
|
+
const file = path.join(base,version,build,'claude.app/Contents/MacOS/claude');
|
|
46
|
+
try { const stat = await fs.stat(file); if (stat.isFile()) builds.push({file,build,mtime:stat.mtimeMs}); } catch {}
|
|
47
|
+
}
|
|
48
|
+
builds.sort((a,b)=>b.mtime-a.mtime || (a.build < b.build ? -1 : a.build > b.build ? 1 : 0));
|
|
49
|
+
for (const {file} of builds) {
|
|
50
|
+
files.push(file);
|
|
51
|
+
// The limit bounds detection time: each candidate that exists is started once with --version.
|
|
52
|
+
if (files.length === DESKTOP_LIMIT) return files;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return files;
|
|
56
|
+
}
|
|
57
|
+
async function candidates(client,settings) {
|
|
58
|
+
const result = cliCandidates(client,settings);
|
|
59
|
+
for (const file of await desktopCandidates(client,settings)) result.push({file,origin:'desktop_app'});
|
|
60
|
+
return result.filter((candidate,index) => result.findIndex(c=>c.file === candidate.file) === index);
|
|
61
|
+
}
|
|
62
|
+
function cliCandidates(client,{env,hostHome}) {
|
|
16
63
|
const dirs = typeof env.PATH === 'string' ? env.PATH.split(path.delimiter) : [];
|
|
17
|
-
const
|
|
64
|
+
const executable = EXECUTABLES[client];
|
|
65
|
+
const names = process.platform === 'win32' ? [executable + '.exe',executable] : [executable];
|
|
18
66
|
const result = [];
|
|
19
67
|
for (const dir of dirs) {
|
|
20
68
|
// Never search the working directory through empty/relative PATH segments.
|
|
@@ -23,9 +71,9 @@ function candidates(client,{env,hostHome}) {
|
|
|
23
71
|
}
|
|
24
72
|
const conventional = client === 'kimi' ? ['.kimi-code/bin/kimi']
|
|
25
73
|
: client === 'claude' ? ['.local/bin/claude','.claude/local/claude']
|
|
26
|
-
: [`.local/bin/${
|
|
74
|
+
: [`.local/bin/${executable}`];
|
|
27
75
|
for (const relative of conventional) result.push({file:path.join(hostHome,relative),origin:'known_location'});
|
|
28
|
-
return result
|
|
76
|
+
return result;
|
|
29
77
|
}
|
|
30
78
|
async function inspect(file) {
|
|
31
79
|
try {
|
|
@@ -78,10 +126,10 @@ function probe(executable,arg,{env,timeoutMs}) {
|
|
|
78
126
|
}
|
|
79
127
|
function identity(client,output) {
|
|
80
128
|
const patterns = {
|
|
81
|
-
|
|
129
|
+
chatgpt:[new RegExp(String.raw`^\s*codex(?:-cli)?\s+v?${VERSION}(?:\s|$)`,'im')],
|
|
82
130
|
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
131
|
kimi:[new RegExp(String.raw`^\s*kimi(?:\s+code(?:\s+cli)?)?(?:,?\s+version)?\s+v?${VERSION}(?:\s|$)`,'im')],
|
|
84
|
-
|
|
132
|
+
gemini:[new RegExp(String.raw`^\s*(?:Antigravity|agy)\s+v?${VERSION}(?:\s|$)`,'im')],
|
|
85
133
|
grok:[new RegExp(String.raw`^\s*grok\s+v?${VERSION}(?:\s|$)`,'im')],
|
|
86
134
|
};
|
|
87
135
|
for (const pattern of patterns[client]) { const match = output.match(pattern); if (match && match[1].length <= 128) return match[1]; }
|
|
@@ -92,7 +140,7 @@ async function verify(client,executable,opts) {
|
|
|
92
140
|
if (version.status !== 'ok') return {status:version.status};
|
|
93
141
|
const branded = identity(client,version.output);
|
|
94
142
|
if (branded) return {status:'detected',version:branded};
|
|
95
|
-
if (client !== 'kimi' && client !== '
|
|
143
|
+
if (client !== 'kimi' && client !== 'gemini') return {status:'invalid'};
|
|
96
144
|
const number = version.output.match(new RegExp(String.raw`^\s*v?${VERSION}(?:\s|$)`));
|
|
97
145
|
if (!number || number[1].length > 128) return {status:'invalid'};
|
|
98
146
|
const help = await probe(executable,'--help',opts);
|
|
@@ -110,7 +158,7 @@ export async function resolveClient(client,opts = {}) {
|
|
|
110
158
|
const settings = options(opts);
|
|
111
159
|
let best = {client,status:'not_found',version:null,origin:null,executable:null};
|
|
112
160
|
const checked = new Set();
|
|
113
|
-
for (const candidate of candidates(client,settings)) {
|
|
161
|
+
for (const candidate of await candidates(client,settings)) {
|
|
114
162
|
const inspected = await inspect(candidate.file);
|
|
115
163
|
if (inspected.status === 'not_found') continue;
|
|
116
164
|
if (inspected.status === 'invalid') {
|
|
@@ -132,6 +180,6 @@ export function summarizeDetection(records) {
|
|
|
132
180
|
return records.filter(r=>CLIENTS.includes(r?.client)).map(({client,status,version,origin})=>{
|
|
133
181
|
status = ['detected','not_found','invalid','timeout'].includes(status) ? status : 'invalid';
|
|
134
182
|
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};
|
|
183
|
+
return {client,status,version:status === 'detected' ? version : null,origin:['path','known_location','desktop_app'].includes(origin) ? origin : null};
|
|
136
184
|
});
|
|
137
185
|
}
|
package/src/explicit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure command codec, also embedded in the offline configuration page.
|
|
2
|
-
export const TOOL_FLAGS=Object.freeze({
|
|
2
|
+
export const TOOL_FLAGS=Object.freeze({chatgpt:'chatgpt',claude:'claude',kimi:'kimi',gemini:'gemini',grok:'grok'});
|
|
3
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'];
|
|
@@ -31,7 +31,7 @@ export function parseExplicitOptions(options,roleIds){
|
|
|
31
31
|
const at=entry.indexOf('='),role=entry.slice(0,at).trim(),value=entry.slice(at+1).trim();
|
|
32
32
|
if(at<1||!roles.includes(role)||Object.hasOwn(choices,role))invalidExplicit();
|
|
33
33
|
let model=value,effort=null;
|
|
34
|
-
if(client==='
|
|
34
|
+
if(client==='gemini'){if(!['pro','flash','flash_lite','inherit'].includes(model))invalidExplicit();}
|
|
35
35
|
else if(client==='grok'){if(grokEffortSuffix(value))invalidExplicit();}
|
|
36
36
|
else{
|
|
37
37
|
const colon=value.lastIndexOf(':');if(colon<1)invalidExplicit();
|
|
@@ -68,9 +68,9 @@ export function explicitOptions(config,roleCatalog){
|
|
|
68
68
|
options[flag]=roles.map(role=>{
|
|
69
69
|
const selected=preferences.roles?.[role]??preferences[kinds.get(role)];
|
|
70
70
|
if(!selected||typeof selected.model!=='string'||!EXPLICIT_MODEL.test(selected.model)||(selected.effort!==null&&!EXPLICIT_EFFORTS.includes(selected.effort)))invalidExplicit();
|
|
71
|
-
if(['
|
|
71
|
+
if(['gemini','grok'].includes(client)&&selected.effort!==null)invalidExplicit();
|
|
72
72
|
if(client==='grok'&&grokEffortSuffix(selected.model))invalidExplicit();
|
|
73
|
-
return `${role}=${selected.model}${['
|
|
73
|
+
return `${role}=${selected.model}${['gemini','grok'].includes(client)?'':':'+(selected.effort??'native')}`;
|
|
74
74
|
}).join(',');
|
|
75
75
|
if(preferences.maxConcurrent!==undefined)options[flag+'-max-concurrent']=String(preferences.maxConcurrent);
|
|
76
76
|
if(!Array.isArray(preferences.avoidModels)||preferences.avoidModels.some(model=>typeof model!=='string'||!EXPLICIT_MODEL.test(model)))invalidExplicit();
|
package/src/inspect.mjs
CHANGED
|
@@ -5,8 +5,8 @@ import {parseToml} from './toml.mjs';
|
|
|
5
5
|
import {CLIENTS} from './policy.mjs';
|
|
6
6
|
import {inspectConfiguration} from './install.mjs';
|
|
7
7
|
|
|
8
|
-
const ROOTS={
|
|
9
|
-
const ENV_ROOTS={
|
|
8
|
+
const ROOTS={chatgpt:'.codex',claude:'.claude',kimi:'.kimi-code',gemini:'.gemini',grok:'.grok'};
|
|
9
|
+
const ENV_ROOTS={chatgpt:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'};
|
|
10
10
|
const MAX_FILE=1024*1024,MAX_TOTAL=4*MAX_FILE,MAX_ENTRIES=256;
|
|
11
11
|
const identifier=/^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
|
|
12
12
|
const namePattern=/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,79}$/;
|
|
@@ -88,7 +88,7 @@ function extractSettings(client,id,native,env,models){
|
|
|
88
88
|
if(subagents.max_concurrent!==undefined)client.concurrency.value=cap(subagents.max_concurrent);
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
|
-
if(id==='
|
|
91
|
+
if(id==='chatgpt'){
|
|
92
92
|
const agents=native.agents??{};if(!record(agents))throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
93
93
|
client.defaults.model=optionalString(agents.default_subagent_model);
|
|
94
94
|
client.defaults.effort=optionalString(agents.default_subagent_reasoning_effort);
|
|
@@ -132,14 +132,14 @@ async function rolesIn(directory,client,id,budget,models,depth=0){
|
|
|
132
132
|
try{
|
|
133
133
|
if(entry.isSymbolicLink())throw failure('UNSAFE_FILE');
|
|
134
134
|
if(entry.isDirectory()){
|
|
135
|
-
if(id==='
|
|
135
|
+
if(id==='chatgpt')continue;
|
|
136
136
|
if(depth>=4)throw failure('INSPECTION_LIMIT');
|
|
137
137
|
await rolesIn(file,client,id,budget,models,depth+1);continue;
|
|
138
138
|
}
|
|
139
|
-
if(!entry.name.endsWith(id==='
|
|
139
|
+
if(!entry.name.endsWith(id==='chatgpt'?'.toml':'.md'))continue;
|
|
140
140
|
const body=await readFile(file,budget);if(body===null)throw failure('CONFIG_CHANGED_DURING_READ');
|
|
141
|
-
const fields=id==='
|
|
142
|
-
const role={name:safeString(fields.name,namePattern),model:optionalString(fields.model),effort:optionalString(id==='
|
|
141
|
+
const fields=id==='chatgpt'?parseToml(body):frontmatter(body);
|
|
142
|
+
const role={name:safeString(fields.name,namePattern),model:optionalString(fields.model),effort:optionalString(id==='chatgpt'?fields.model_reasoning_effort:fields.effort)};
|
|
143
143
|
if(id==='kimi'){
|
|
144
144
|
// APK's Markdown role files contain explicit routing guidance, not a
|
|
145
145
|
// native model binding. Other prose is never interpreted as configuration.
|
|
@@ -154,7 +154,7 @@ async function rolesIn(directory,client,id,budget,models,depth=0){
|
|
|
154
154
|
role.model=models.get(role.name)??role.model;
|
|
155
155
|
if(fields.effort!==undefined){role.effort=null;warning(client,failure('UNSUPPORTED_ROLE_EFFORT'));}
|
|
156
156
|
}
|
|
157
|
-
if(id==='
|
|
157
|
+
if(id==='gemini'){
|
|
158
158
|
if(role.model!==null&&!['pro','flash','flash_lite','inherit'].includes(role.model))throw failure('UNSUPPORTED_NATIVE_SETTING');
|
|
159
159
|
role.enabled=fields.subagent??null;
|
|
160
160
|
if(fields.effort!==undefined){role.effort=null;warning(client,failure('UNSUPPORTED_ROLE_EFFORT'));}
|
|
@@ -179,19 +179,19 @@ export async function readConfiguration({client=null,home,env=process.env,explic
|
|
|
179
179
|
const report={schemaVersion:1,scope:'user',modelExecution:'not_checked',clients:{}};
|
|
180
180
|
for(const id of client?[client]:CLIENTS){
|
|
181
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:['
|
|
182
|
+
const c={status:'not_configured',defaults:{model:null,effort:null,enabled:null},concurrency:{value:null,kind:['chatgpt','claude','grok'].includes(id)?'native_setting':'guidance'},roles:[],pool:[],warnings:[],apk:{status:'not_checked'}};
|
|
183
183
|
report.clients[id]=c;const budget={bytes:0,entries:0},models=new Map();
|
|
184
184
|
try{
|
|
185
185
|
await safePath(root);
|
|
186
|
-
if(id!=='
|
|
186
|
+
if(id!=='gemini'){
|
|
187
187
|
try{
|
|
188
188
|
const text=await readFile(path.join(root,id==='claude'?'settings.json':'config.toml'),budget);
|
|
189
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
190
|
else if(id==='claude')extractSettings(c,id,{},env,models);
|
|
191
191
|
}catch(error){warning(c,error);}
|
|
192
192
|
}
|
|
193
|
-
await rolesIn(path.join(root,id==='
|
|
194
|
-
if(id==='kimi'||id==='
|
|
193
|
+
await rolesIn(path.join(root,id==='gemini'?'config/agents':'agents'),c,id,budget,models);
|
|
194
|
+
if(id==='kimi'||id==='gemini'){try{await readGuidance(root,c,budget);}catch(error){warning(c,error);}}
|
|
195
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
196
|
if(root===path.resolve(home,ROOTS[id])){
|
|
197
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;}}
|
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 = ['
|
|
13
|
+
const CLIENTS = ['chatgpt', 'claude', 'kimi', 'gemini', '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';
|
|
@@ -155,7 +155,7 @@ function selectedAliases(preferences,selectedRoles=ROLE_IDS) {
|
|
|
155
155
|
}
|
|
156
156
|
function tomlSettings(client,preferences,maxConcurrent,selectedRoles=ROLE_IDS) {
|
|
157
157
|
const worker=rolePreference(preferences,selectedRoles[0]);
|
|
158
|
-
if(client==='
|
|
158
|
+
if(client==='chatgpt')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
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
160
|
if(client!=='kimi')return [];
|
|
161
161
|
const selected=selectedAliases(preferences,selectedRoles),roles=effectiveRoles(preferences,selectedRoles);
|
|
@@ -185,7 +185,7 @@ function scalar(op,target,type) {
|
|
|
185
185
|
}
|
|
186
186
|
function validateTomlSnapshot(file,client,schemaVersion,selectedRoles=ROLE_IDS) {
|
|
187
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==='
|
|
188
|
+
if(client==='chatgpt') {
|
|
189
189
|
if(!(schemaVersion===2?[3]:[2,3]).includes(file.ops.length))fail('INVALID_RECEIPT');
|
|
190
190
|
const cap=scalar(file.ops[0],['agents','max_concurrent_threads_per_session'],'number');
|
|
191
191
|
if(cap<1||cap>32)fail('INVALID_RECEIPT');
|
|
@@ -389,8 +389,8 @@ export async function planInstall(options) {
|
|
|
389
389
|
const cap=subagents?.max_concurrent;
|
|
390
390
|
if(cap!==undefined&&(!Number.isSafeInteger(cap)||cap<1))fail('UNSUPPORTED_CONFIG');
|
|
391
391
|
}
|
|
392
|
-
if(plan.client==='
|
|
393
|
-
if(native.agents&&own(native.agents,'max_threads'))fail('
|
|
392
|
+
if(plan.client==='chatgpt') {
|
|
393
|
+
if(native.agents&&own(native.agents,'max_threads'))fail('CHATGPT_CAP_ALIAS_CONFLICT');
|
|
394
394
|
const cap=native.agents?.max_concurrent_threads_per_session;
|
|
395
395
|
if(cap!==undefined&&(!Number.isSafeInteger(cap)||cap<1))fail('UNSUPPORTED_CONFIG');
|
|
396
396
|
}
|
package/src/native.mjs
CHANGED
|
@@ -4,25 +4,25 @@ 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 = {
|
|
7
|
+
const directory = {chatgpt:'.codex',claude:'.claude',kimi:'.kimi-code',gemini:'.gemini',grok:'.grok'};
|
|
8
8
|
export function nativeHomeConflict(client,home,env) {
|
|
9
|
-
const variable={
|
|
9
|
+
const variable={chatgpt:'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
|
}
|
|
13
13
|
export function nativeLocations(client,home) {
|
|
14
14
|
const root=path.join(home,directory[client]);
|
|
15
15
|
return {
|
|
16
|
-
config:client==='
|
|
16
|
+
config:client==='gemini'?null:path.join(root,client==='claude'?'settings.json':'config.toml'),
|
|
17
17
|
instructions:path.join(root,client==='claude'?'CLAUDE.md':'AGENTS.md'),
|
|
18
|
-
roles:Object.keys(ROLES).map(role=>path.join(root,client==='
|
|
18
|
+
roles:Object.keys(ROLES).map(role=>path.join(root,client==='gemini'?'config/agents':'agents',`apk-${role}.${client==='chatgpt'?'toml':'md'}`))
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
export function nativeRoles(client,config,selectedRoles=ROLE_IDS) {
|
|
22
22
|
return selectedRoles.map(role=>{
|
|
23
23
|
const selected=rolePreference(config,role),name='apk-'+role;
|
|
24
24
|
const {description,prompt}=ROLE_DEFINITIONS.find(value=>value.id===role);
|
|
25
|
-
if(client==='
|
|
25
|
+
if(client==='chatgpt') 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
28
|
if(client==='grok') {
|
|
@@ -47,18 +47,18 @@ export function instructionBlock(client,config,maxConcurrent,selectedRoles=ROLE_
|
|
|
47
47
|
for(const role of selectedRoles) {const selected=rolePreference(config,role);text.push(`For apk-${role}, select pool alias ${selected.model} (effort ${selected.effort??'native default'}).`);}
|
|
48
48
|
text.push('Roles sharing an alias share its native effort default; an explicit effort pin for that alias also affects its native-default roles.');
|
|
49
49
|
text.push("Use the Agent tool's model selection when spawning; the role file alone does not select a model. The concurrency limit is guidance; no native Agent cap is configured.");
|
|
50
|
-
} else if(client==='
|
|
50
|
+
} else if(client==='gemini') {
|
|
51
51
|
for(const role of selectedRoles)text.push(`When delegating to apk-${role}, use model tier ${rolePreference(config,role).model}.`);
|
|
52
52
|
text.push('Per-role effort and a hard concurrency cap are not supported by the documented native agent settings.');
|
|
53
53
|
}
|
|
54
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.');
|
|
55
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.');
|
|
56
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.');
|
|
57
|
-
if(client==='
|
|
57
|
+
if(client==='chatgpt'||client==='claude')text.push('An omitted role effort uses the native client default; it does not pin a level.');
|
|
58
58
|
if(config.avoidModels.length)text.push(`Avoid these models when delegating: ${config.avoidModels.join(', ')}. This is routing guidance.`);
|
|
59
59
|
text.push(END);
|
|
60
60
|
return text.join('\n')+'\n';
|
|
61
61
|
}
|
|
62
62
|
export function nativeWarnings(client) {
|
|
63
|
-
return client==='kimi'?['kimi-concurrency-guidance-only','kimi-model-pool-routing-guidance']:client==='
|
|
63
|
+
return client==='kimi'?['kimi-concurrency-guidance-only','kimi-model-pool-routing-guidance']:client==='gemini'?['gemini-concurrency-guidance-only','gemini-per-role-effort-unsupported']:client==='grok'?['grok-per-role-effort-unsupported','grok-native-precedence-limits']:client==='claude'?['claude-native-precedence-limits']:['chatgpt-native-precedence-limits'];
|
|
64
64
|
}
|
package/src/output.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import {PRESETS} from './release.mjs';
|
|
2
|
-
const LABELS={
|
|
2
|
+
const LABELS={chatgpt:'ChatGPT',claude:'Claude',kimi:'Kimi',gemini:'Gemini',grok:'Grok'};
|
|
3
3
|
const label=client=>LABELS[client]??'Tool';
|
|
4
4
|
const code=value=>typeof value==='string'&&/^[A-Za-z][A-Za-z0-9_-]{0,127}$/.test(value)?value:'OPERATION_FAILED';
|
|
5
5
|
const PRESET_LABELS=Object.fromEntries(PRESETS.map(entry=>[entry.id,entry.label]));
|
|
6
6
|
const preset=value=>Object.hasOwn(PRESET_LABELS,value)?' · '+PRESET_LABELS[value]:'';
|
|
7
7
|
const NOTES={
|
|
8
|
-
'
|
|
8
|
+
'chatgpt-native-precedence-limits':'Project and explicit client settings can override these defaults.',
|
|
9
9
|
'claude-native-precedence-limits':'Project and explicit client settings can override these defaults.',
|
|
10
10
|
'kimi-concurrency-guidance-only':'Kimi: the concurrency limit is guidance.',
|
|
11
11
|
'kimi-model-pool-routing-guidance':'Kimi: the parent selects the model alias for each role.',
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'grok-per-role-effort-unsupported':'Grok
|
|
12
|
+
'gemini-concurrency-guidance-only':'Gemini: the concurrency limit is guidance.',
|
|
13
|
+
'gemini-per-role-effort-unsupported':'Gemini: native tiers do not support per-role effort.',
|
|
14
|
+
'grok-per-role-effort-unsupported':'Grok: native agent settings do not support per-role effort.',
|
|
15
15
|
'grok-native-precedence-limits':'Project and explicit client settings can override these defaults.',
|
|
16
16
|
'kimi-catalog-alias-resolved':'Kimi: an existing matching model alias was selected.',
|
|
17
17
|
};
|
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(['
|
|
4
|
+
export const CLIENTS = Object.freeze(['chatgpt', 'claude', 'kimi', 'gemini', '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}$/;
|
|
@@ -27,7 +27,7 @@ export function validatePolicy(value) {
|
|
|
27
27
|
for (const selected of [config.code,config.light,...Object.values(config.roles??{})]) {
|
|
28
28
|
object(selected,['model','effort']); model(selected.model);
|
|
29
29
|
if (client === 'kimi' && selected.model === 'primary') invalid();
|
|
30
|
-
if(client==='
|
|
30
|
+
if(client==='gemini'&&!['pro','flash','flash_lite','inherit'].includes(selected.model))invalid();
|
|
31
31
|
if(selected.effort!==null){
|
|
32
32
|
if(!NATIVE_EFFORTS[client].includes(selected.effort))invalid();
|
|
33
33
|
const known=catalog.clients[client].models.find(model=>model.id===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:'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({
|
|
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({chatgpt:'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')));
|
package/src/setup.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { nativeHomeConflict } from './native.mjs';
|
|
|
4
4
|
import { PACKAGE_VERSION } from './release.mjs';
|
|
5
5
|
import { CLIENTS } from './policy.mjs';
|
|
6
6
|
|
|
7
|
-
const ERRORS = new Set(['INVALID_PRESET','INVALID_RELEASE','LEGACY_RUNTIME_RECEIPT','INVALID_CLIENT','INVALID_POLICY','INVALID_JSON','INVALID_TOML','UNSUPPORTED_CONFIG','UNSUPPORTED_TOML_STRUCTURE','UNSUPPORTED_TOML_VALUE','
|
|
7
|
+
const ERRORS = new Set(['INVALID_PRESET','INVALID_RELEASE','LEGACY_RUNTIME_RECEIPT','INVALID_CLIENT','INVALID_POLICY','INVALID_JSON','INVALID_TOML','UNSUPPORTED_CONFIG','UNSUPPORTED_TOML_STRUCTURE','UNSUPPORTED_TOML_VALUE','CHATGPT_CAP_ALIAS_CONFLICT','OWNERSHIP_CONFLICT','INVALID_RECEIPT','OWNED_DRIFT','INSTALLATION_CONFLICT','POLICY_CONFLICT','UNSAFE_PATH','UNSAFE_FILE','STALE_PLAN','RECOVERY_REQUIRED','ROLLBACK_CONFLICT','INVALID_PLAN','PLATFORM_UNSUPPORTED','INSTALL_LOCKED','INSTALL_FAILED','EACCES','EPERM','ENOENT','ENOSPC','EIO','EROFS']);
|
|
8
8
|
const safeError = error => ERRORS.has(error?.code) ? error.code : 'SETUP_FAILED';
|
|
9
9
|
function actions(client,warnings) {
|
|
10
10
|
return warnings.map(code=>({client,code,automatable:false}));
|
package/src/share.mjs
CHANGED
|
@@ -55,7 +55,7 @@ function shareRelease(release){
|
|
|
55
55
|
}
|
|
56
56
|
function shareChoice(choice,id,metadata){
|
|
57
57
|
shareRecord(choice,['model','effort']);
|
|
58
|
-
if(typeof choice.model!=='string'||choice.model.length>128||(choice.effort!==null&&(['
|
|
58
|
+
if(typeof choice.model!=='string'||choice.model.length>128||(choice.effort!==null&&(['gemini','grok'].includes(id)||!metadata.catalog[id].efforts.includes(choice.effort))))invalidShare();
|
|
59
59
|
return {model:choice.model,effort:choice.effort};
|
|
60
60
|
}
|
|
61
61
|
function sharePayload(payload,release,metadata){
|