agent-policy-kit 0.0.0-stage → 0.1.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 +75 -0
- package/CHANGELOG.md +61 -0
- package/LICENSE +21 -0
- package/README.md +52 -2
- package/bin/agent-policy.mjs +142 -0
- package/docs/architecture.md +17 -0
- package/docs/compatibility.md +21 -0
- package/docs/configuration-page.md +38 -0
- package/docs/development.md +21 -0
- package/docs/installation.md +15 -0
- package/docs/model-catalog.md +94 -0
- package/docs/model-updates.md +22 -0
- package/docs/npm-release.md +25 -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 +24 -0
- package/examples/kimi-pool.toml +17 -0
- package/models/README.md +7 -0
- package/models/catalog.json +699 -0
- package/package.json +59 -4
- package/policies/balanced.json +96 -0
- package/policies/fast.json +70 -0
- package/policies/quality.json +90 -0
- package/schema/policy.schema.json +361 -0
- package/site/index.html +896 -0
- package/src/detect.mjs +136 -0
- package/src/explicit.mjs +82 -0
- package/src/install.mjs +566 -0
- package/src/native.mjs +60 -0
- package/src/policy.mjs +44 -0
- package/src/release.mjs +19 -0
- package/src/roles.mjs +13 -0
- package/src/setup.mjs +66 -0
- package/src/share.mjs +141 -0
- package/src/toml.mjs +96 -0
package/src/policy.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import catalog from '../models/catalog.json' with {type:'json'};
|
|
2
|
+
import {ROLES,ROLE_IDS,selectedRoleIds} from './roles.mjs';
|
|
3
|
+
export {ROLES} from './roles.mjs';
|
|
4
|
+
export const CLIENTS = Object.freeze(['codex', 'claude', 'kimi', 'agy']);
|
|
5
|
+
export const rolePreference=(config,role)=>config.roles?.[role]??config[ROLES[role]];
|
|
6
|
+
export const NATIVE_EFFORTS=Object.freeze(Object.fromEntries(CLIENTS.map(client=>[client,Object.freeze([...catalog.clients[client].efforts])])));
|
|
7
|
+
const identifier = /^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
|
|
8
|
+
function invalid() { throw Object.assign(new Error('Invalid shared preferences.'), {code:'INVALID_POLICY'}); }
|
|
9
|
+
function object(value, fields) {
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || ![Object.prototype,null].includes(Object.getPrototypeOf(value))) invalid();
|
|
11
|
+
for (const [key,descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
12
|
+
if (!fields.includes(key) || descriptor.get || descriptor.set) invalid();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function model(value) { if (typeof value !== 'string' || !identifier.test(value)) invalid(); }
|
|
16
|
+
function freeze(value) { for (const child of Object.values(value)) if (child && typeof child === 'object') freeze(child); return Object.freeze(value); }
|
|
17
|
+
export function validatePolicy(value) {
|
|
18
|
+
object(value,['schemaVersion','maxConcurrent','clients','selectedRoles']);
|
|
19
|
+
if (value.schemaVersion !== 1 || !Number.isInteger(value.maxConcurrent) || value.maxConcurrent < 1 || value.maxConcurrent > 32) invalid();
|
|
20
|
+
if(value.selectedRoles!==undefined&&(!Array.isArray(value.selectedRoles)||!value.selectedRoles.length||value.selectedRoles.length>ROLE_IDS.length||new Set(value.selectedRoles).size!==value.selectedRoles.length||value.selectedRoles.some(role=>!ROLE_IDS.includes(role))))invalid();
|
|
21
|
+
object(value.clients,CLIENTS);
|
|
22
|
+
if (!Object.keys(value.clients).length) invalid();
|
|
23
|
+
for (const [client,config] of Object.entries(value.clients)) {
|
|
24
|
+
object(config,['code','light','roles','avoidModels']);
|
|
25
|
+
if(config.roles!==undefined)object(config.roles,Object.keys(ROLES));
|
|
26
|
+
for (const selected of [config.code,config.light,...Object.values(config.roles??{})]) {
|
|
27
|
+
object(selected,['model','effort']); model(selected.model);
|
|
28
|
+
if (client === 'kimi' && selected.model === 'primary') invalid();
|
|
29
|
+
if(client==='agy'&&!['pro','flash','flash_lite','inherit'].includes(selected.model))invalid();
|
|
30
|
+
if(selected.effort!==null){
|
|
31
|
+
if(!NATIVE_EFFORTS[client].includes(selected.effort))invalid();
|
|
32
|
+
const known=catalog.clients[client].models.find(model=>model.id===selected.model);
|
|
33
|
+
if(known&&!known.efforts.includes(selected.effort))invalid();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!Array.isArray(config.avoidModels) || config.avoidModels.length > 32 || new Set(config.avoidModels).size !== config.avoidModels.length) invalid();
|
|
37
|
+
config.avoidModels.forEach(model);
|
|
38
|
+
const effective=selectedRoleIds(value).map(role=>rolePreference(config,role));
|
|
39
|
+
if (effective.some(selected=>config.avoidModels.includes(selected.model))) invalid();
|
|
40
|
+
const pinned=effective.filter(value=>value.effort!==null);
|
|
41
|
+
if (client==='kimi'&&new Set(pinned.map(v=>v.model+'\0'+v.effort)).size!==new Set(pinned.map(v=>v.model)).size) invalid();
|
|
42
|
+
}
|
|
43
|
+
return freeze(JSON.parse(JSON.stringify(value)));
|
|
44
|
+
}
|
package/src/release.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {validatePolicy} from './policy.mjs';
|
|
4
|
+
|
|
5
|
+
export const PACKAGE_VERSION=JSON.parse(fs.readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
|
|
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:'Fast & economical',description:'For small, well-defined tasks. Lighter models, with extra reasoning for review and tests.'},{id:'balanced',label:'Balanced',description:'Stronger coding and verification, lighter exploration and mechanical work.'},{id:'quality',label:'Quality first',description:'For demanding implementation and verification. Prioritizes capability over response time and usage.'}].map(Object.freeze));
|
|
8
|
+
export function presetPreferences(id='balanced'){
|
|
9
|
+
if(!PRESETS.some(preset=>preset.id===id))throw Object.assign(new Error('Unknown preset.'),{code:'UNKNOWN_PRESET'});
|
|
10
|
+
return validatePolicy(JSON.parse(fs.readFileSync(new URL(`../policies/${id}.json`,import.meta.url),'utf8')));
|
|
11
|
+
}
|
|
12
|
+
export function modelCatalog(){return JSON.parse(fs.readFileSync(new URL('../models/catalog.json',import.meta.url),'utf8'));}
|
|
13
|
+
|
|
14
|
+
export function bundledReleaseInfo(preset='balanced'){
|
|
15
|
+
const preferences=presetPreferences(preset);
|
|
16
|
+
return {schemaVersion:1,version:PACKAGE_VERSION,preset,presets:PRESETS,catalogReviewedAt:modelCatalog().reviewedAt,
|
|
17
|
+
preferencesHash:createHash('sha256').update(JSON.stringify(canonical(preferences))).digest('hex'),
|
|
18
|
+
notes:fs.readFileSync(new URL('../CHANGELOG.md',import.meta.url),'utf8')};
|
|
19
|
+
}
|
package/src/roles.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const noChildren=' Do not create child agents; return any need for further delegation to the parent.';
|
|
2
|
+
export const ROLE_DEFINITIONS=Object.freeze([
|
|
3
|
+
{id:'worker',name:'Coder',kind:'code',assignment:'scoped implementation and fixes',description:'Implements scoped changes, preserves peers\' edits, and verifies the result.',prompt:'Implement the assigned change or fix within the assigned files. You share the workspace with other agents: preserve their edits. Verify the result and report changes, checks, and remaining limitations.'},
|
|
4
|
+
{id:'reviewer',name:'Reviewer',kind:'code',assignment:'read-only correctness, security and regression review',description:'Reviews correctness, security and regressions without edits; reports actionable evidence.',prompt:'Review the assigned change without editing files. Find actionable correctness, security, and regression issues. Report each finding with file and line evidence and explain its impact; distinguish confirmed issues from uncertainty.'},
|
|
5
|
+
{id:'explorer',name:'Code explorer',kind:'light',assignment:'read-only code and documentation discovery',description:'Maps code and documentation, cites sources, and separates facts from uncertainty.',prompt:'Explore the assigned code and documentation without editing files. Return a concise map or factual answer with source paths or citations. Distinguish verified facts from assumptions and unknowns.'},
|
|
6
|
+
{id:'tester',name:'Test writer',kind:'code',assignment:'test creation and targeted execution',description:'Writes behavior and edge-case tests, reproduces issues, and runs targeted checks.',prompt:'Own the assigned tests and targeted execution. Reproduce the issue or establish a baseline, then test observable behavior and edge cases. Never weaken checks to make them pass. Do not edit application code unless explicitly assigned. Preserve peers\' edits and report results and limitations.'},
|
|
7
|
+
{id:'writer',name:'Docs writer',kind:'light',assignment:'assigned documentation, examples and release notes',description:'Updates assigned docs and examples, verifies claims, and keeps terminology consistent.',prompt:'Update only the assigned documentation, examples, or release notes. Verify claims against code or cited sources and use consistent terminology. Do not edit application code. Preserve peers\' edits and report changes and unknowns.'},
|
|
8
|
+
{id:'routine',name:'Task runner',kind:'light',assignment:'explicit commands and mechanical edits',description:'Runs prescribed commands and mechanical edits; returns ambiguous decisions to the parent.',prompt:'Execute only the prescribed commands or clearly specified mechanical edits. Preserve other agents\' work and report the outcome. Do not make ambiguous design or behavior decisions; return those decisions to the parent.'}
|
|
9
|
+
].map(role=>Object.freeze({...role,prompt:role.prompt+noChildren})));
|
|
10
|
+
export const ROLES=Object.freeze(Object.fromEntries(ROLE_DEFINITIONS.map(role=>[role.id,role.kind])));
|
|
11
|
+
export const ROLE_IDS=Object.freeze(Object.keys(ROLES));
|
|
12
|
+
export const LEGACY_ROLE_IDS=Object.freeze(['worker','reviewer','explorer','routine']);
|
|
13
|
+
export const selectedRoleIds=policy=>ROLE_IDS.filter(role=>policy.selectedRoles===undefined||policy.selectedRoles.includes(role));
|
package/src/setup.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { detectClients, resolveClient, summarizeDetection } from './detect.mjs';
|
|
2
|
+
import { planInstall, applyInstall, summarizePlan, inspectConfiguration } from './install.mjs';
|
|
3
|
+
import { nativeHomeConflict } from './native.mjs';
|
|
4
|
+
import { PACKAGE_VERSION } from './release.mjs';
|
|
5
|
+
import { CLIENTS } from './policy.mjs';
|
|
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','CODEX_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
|
+
const safeError = error => ERRORS.has(error?.code) ? error.code : 'SETUP_FAILED';
|
|
9
|
+
function actions(client,warnings) {
|
|
10
|
+
return warnings.map(code=>({client,code,automatable:false}));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RELEASE_FIELDS=['targetReleaseVersion','targetSource','previousReleaseVersion','previousSource','appliedReleaseVersion','appliedSource','targetPreset','previousPreset','appliedPreset'];
|
|
14
|
+
function releaseMetadata(value) {return Object.fromEntries(RELEASE_FIELDS.map(key=>[key,value[key]??null]));}
|
|
15
|
+
|
|
16
|
+
export async function setupDetected({home,stateDir,policyPath,apply=false,client=null,env=process.env,hostHome,explicitHome=false,releaseVersion,replace=false,source='release',enabledClients=CLIENTS,preset=null} = {}) {
|
|
17
|
+
if(!Array.isArray(enabledClients)||enabledClients.some(value=>!CLIENTS.includes(value))||new Set(enabledClients).size!==enabledClients.length)throw Object.assign(new Error('INVALID_CLIENT'),{code:'INVALID_CLIENT'});
|
|
18
|
+
const selectionConflict=client!==null&&!enabledClients.includes(client);
|
|
19
|
+
const records = client === null ? await detectClients({env,hostHome}) : [await resolveClient(client,{env,hostHome})];
|
|
20
|
+
const detected = summarizeDetection(records), results = [], requiredActions = [];
|
|
21
|
+
const available = records.filter(r=>r.status === 'detected'&&enabledClients.includes(r.client));
|
|
22
|
+
for (const record of records) {
|
|
23
|
+
if(!enabledClients.includes(record.client)) {
|
|
24
|
+
if(selectionConflict) {const warnings=['client-not-selected'];results.push({client:record.client,status:'prerequisite',warnings});requiredActions.push(...actions(record.client,warnings));}
|
|
25
|
+
else results.push({client:record.client,status:'not_selected'});
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
let retained;
|
|
29
|
+
if(record.status!=='detected'||(!explicitHome&&nativeHomeConflict(record.client,home,env))) {
|
|
30
|
+
try {
|
|
31
|
+
const applied=await inspectConfiguration({client:record.client,home,stateDir});
|
|
32
|
+
retained={targetReleaseVersion:releaseVersion??PACKAGE_VERSION,targetSource:source,previousReleaseVersion:applied.releaseVersion,previousSource:applied.source,appliedReleaseVersion:applied.releaseVersion,appliedSource:applied.source,targetPreset:preset,previousPreset:applied.preset,appliedPreset:applied.preset};
|
|
33
|
+
} catch(error) {
|
|
34
|
+
const code=safeError(error);results.push({client:record.client,status:'error',error:code});requiredActions.push({client:record.client,code,automatable:false});continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (record.status !== 'detected') {
|
|
38
|
+
results.push({client:record.client,status:record.status,...retained});
|
|
39
|
+
if (record.status === 'invalid' || record.status === 'timeout') requiredActions.push({client:record.client,code:record.status === 'timeout' ? 'native_client_detection_timeout' : 'native_client_identity_unverified',automatable:true});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if(!explicitHome&&nativeHomeConflict(record.client,home,env)) {
|
|
43
|
+
const warnings=['native-home-override-unsupported'];
|
|
44
|
+
results.push({client:record.client,status:'prerequisite',warnings,...retained});
|
|
45
|
+
requiredActions.push(...actions(record.client,warnings));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
// Planning and applying are sequential per client: the preceding transaction
|
|
50
|
+
// can create shared policy/receipts that the next plan must capture afresh.
|
|
51
|
+
const plan = await planInstall({client:record.client,home,stateDir,policyPath,releaseVersion,replace,source,preset});
|
|
52
|
+
requiredActions.push(...actions(record.client,plan.warnings));
|
|
53
|
+
if (plan.status === 'prerequisite') { results.push({client:record.client,status:'prerequisite',plan:summarizePlan(plan),...releaseMetadata(plan)}); continue; }
|
|
54
|
+
if (apply) {
|
|
55
|
+
const result = await applyInstall(plan);
|
|
56
|
+
results.push({client:record.client,status:result.status === 'unchanged' ? 'unchanged' : 'configured',changedFiles:result.changedFiles,warnings:[...result.warnings],...releaseMetadata(result)});
|
|
57
|
+
} else results.push({client:record.client,status:'planned',plan:summarizePlan(plan),...releaseMetadata(plan)});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const code = safeError(error);
|
|
60
|
+
results.push({client:record.client,status:'error',error:code});
|
|
61
|
+
requiredActions.push({client:record.client,code,automatable:false});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const failed = results.some(r=>['error','prerequisite','invalid','timeout'].includes(r.status));
|
|
65
|
+
return {schemaVersion:1,status:selectionConflict ? 'partial' : available.length === 0 ? 'no_clients' : failed ? 'partial' : apply ? 'configured' : 'planned',detected,results,requiredActions,exitCode:available.length === 0 || failed ? 3 : 0};
|
|
66
|
+
}
|
package/src/share.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Pure URL state codec, also embedded in the offline configuration page.
|
|
2
|
+
const SHARE_BYTES=65536,SHARE_TOKEN=87384;
|
|
3
|
+
const SHARE_VERSION=/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
4
|
+
const SHARE_UNSAFE=new Set(['__proto__','constructor','prototype']);
|
|
5
|
+
function invalidShare(){throw Object.assign(new Error('Invalid shared configuration link.'),{code:'INVALID_SHARE_LINK'});}
|
|
6
|
+
|
|
7
|
+
// Inspect descriptors before reading any user-supplied value. UI-only cached data is
|
|
8
|
+
// allowed, but accessors, exotic prototypes, sparse arrays and hidden fields are not.
|
|
9
|
+
function shareData(value,depth=0,budget={left:100000}){
|
|
10
|
+
if(--budget.left<0||depth>32)invalidShare();
|
|
11
|
+
if(value===null||value===undefined||typeof value==='string'||typeof value==='boolean')return;
|
|
12
|
+
if(typeof value==='number'){if(!Number.isFinite(value))invalidShare();return;}
|
|
13
|
+
if(typeof value!=='object')invalidShare();
|
|
14
|
+
const array=Array.isArray(value),prototype=Object.getPrototypeOf(value);
|
|
15
|
+
if(array?prototype!==Array.prototype:prototype!==Object.prototype&&prototype!==null)invalidShare();
|
|
16
|
+
const fields=Object.getOwnPropertyDescriptors(value),keys=Reflect.ownKeys(fields);
|
|
17
|
+
if(array&&(keys.length!==value.length+1||value.length>100000))invalidShare();
|
|
18
|
+
for(const key of keys){
|
|
19
|
+
if(typeof key!=='string'||SHARE_UNSAFE.has(key))invalidShare();
|
|
20
|
+
const field=fields[key];
|
|
21
|
+
if(!Object.hasOwn(field,'value')||(!field.enumerable&&!(array&&key==='length')))invalidShare();
|
|
22
|
+
if(array&&key!=='length'&&!/^(0|[1-9]\d*)$/.test(key))invalidShare();
|
|
23
|
+
if(key!=='length'||!array)shareData(field.value,depth+1,budget);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function shareRecord(value,keys){
|
|
27
|
+
if(!value||typeof value!=='object'||Array.isArray(value))invalidShare();
|
|
28
|
+
if(keys&&(Object.keys(value).length!==keys.length||keys.some(key=>!Object.hasOwn(value,key))))invalidShare();
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function shareList(value,known){
|
|
32
|
+
if(!Array.isArray(value)||value.length>known.length||new Set(value).size!==value.length||value.some(id=>typeof id!=='string'||!known.includes(id)))invalidShare();
|
|
33
|
+
return [...value];
|
|
34
|
+
}
|
|
35
|
+
function shareRelease(release){
|
|
36
|
+
shareData(release);shareRecord(release);
|
|
37
|
+
if(typeof release.version!=='string'||!SHARE_VERSION.test(release.version))invalidShare();
|
|
38
|
+
const preferences=shareRecord(release.preferences),clients=Object.keys(shareRecord(preferences.clients));
|
|
39
|
+
const catalog=shareRecord(shareRecord(release.catalog).clients);
|
|
40
|
+
if(!clients.length||clients.some(id=>!Object.hasOwn(catalog,id)||!Array.isArray(catalog[id].efforts)||catalog[id].efforts.some(level=>typeof level!=='string')))invalidShare();
|
|
41
|
+
if(!Array.isArray(release.roles)||!release.roles.length||!Array.isArray(release.presets)||!release.presets.length)invalidShare();
|
|
42
|
+
const roles=new Map(),presets=new Map();
|
|
43
|
+
for(const role of release.roles){
|
|
44
|
+
shareRecord(role);if(typeof role.id!=='string'||!role.id||SHARE_UNSAFE.has(role.id)||roles.has(role.id)||!['code','light'].includes(role.kind))invalidShare();
|
|
45
|
+
roles.set(role.id,role);
|
|
46
|
+
}
|
|
47
|
+
for(const preset of release.presets){
|
|
48
|
+
shareRecord(preset);if(typeof preset.id!=='string'||!preset.id||preset.id==='custom'||SHARE_UNSAFE.has(preset.id)||presets.has(preset.id))invalidShare();
|
|
49
|
+
shareRecord(shareRecord(preset.preferences).clients);presets.set(preset.id,preset);
|
|
50
|
+
}
|
|
51
|
+
if(!presets.has(release.preset))invalidShare();
|
|
52
|
+
return {clients,roles,presets,catalog};
|
|
53
|
+
}
|
|
54
|
+
function shareChoice(choice,id,metadata){
|
|
55
|
+
shareRecord(choice,['model','effort']);
|
|
56
|
+
if(typeof choice.model!=='string'||choice.model.length>128||(choice.effort!==null&&(id==='agy'||!metadata.catalog[id].efforts.includes(choice.effort))))invalidShare();
|
|
57
|
+
return {model:choice.model,effort:choice.effort};
|
|
58
|
+
}
|
|
59
|
+
function sharePayload(payload,release,metadata){
|
|
60
|
+
shareRecord(payload,['v','release','roles','tools','scope','maxConcurrent']);
|
|
61
|
+
if(payload.v!==1||typeof payload.release!=='string'||!SHARE_VERSION.test(payload.release))invalidShare();
|
|
62
|
+
if(payload.release!==release.version)throw Object.assign(new Error('This link uses a different release.'),{code:'SHARE_RELEASE_MISMATCH'});
|
|
63
|
+
if(!Number.isInteger(payload.maxConcurrent)||payload.maxConcurrent<1||payload.maxConcurrent>32)invalidShare();
|
|
64
|
+
const roles=shareList(payload.roles,[...metadata.roles.keys()]);
|
|
65
|
+
if(!Array.isArray(payload.tools)||payload.tools.length>metadata.clients.length)invalidShare();
|
|
66
|
+
const selected=[];
|
|
67
|
+
const tools=payload.tools.map(tool=>{
|
|
68
|
+
shareRecord(tool,['id','mode','base','choices']);
|
|
69
|
+
const {id,mode,base}=tool;
|
|
70
|
+
if(!metadata.clients.includes(id)||selected.includes(id)||!metadata.presets.has(base)||(mode!==null&&mode!=='custom'&&!metadata.presets.has(mode)))invalidShare();
|
|
71
|
+
selected.push(id);shareRecord(tool.choices,roles);
|
|
72
|
+
const choices=Object.fromEntries(roles.map(role=>[role,shareChoice(tool.choices[role],id,metadata)]));
|
|
73
|
+
if(mode!==null&&mode!=='custom'){
|
|
74
|
+
if(mode!==base)invalidShare();
|
|
75
|
+
const clients=metadata.presets.get(mode).preferences.clients;
|
|
76
|
+
if(!Object.hasOwn(clients,id))invalidShare();
|
|
77
|
+
const preferences=shareRecord(clients[id]);
|
|
78
|
+
for(const role of roles){
|
|
79
|
+
const expected=preferences.roles&&Object.hasOwn(preferences.roles,role)?preferences.roles[role]:preferences[metadata.roles.get(role).kind];
|
|
80
|
+
const choice=shareChoice(expected,id,metadata);
|
|
81
|
+
if(choice.model!==choices[role].model||choice.effort!==choices[role].effort)invalidShare();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {id,mode,base,choices};
|
|
85
|
+
});
|
|
86
|
+
const scope=payload.scope;
|
|
87
|
+
if(scope!==null&&(typeof scope!=='string'||(scope==='all'?tools.length<2:!selected.includes(scope))))invalidShare();
|
|
88
|
+
return {v:1,release:payload.release,roles,tools,scope,maxConcurrent:payload.maxConcurrent};
|
|
89
|
+
}
|
|
90
|
+
function shareBase64(bytes){return btoa(Array.from(bytes,byte=>String.fromCharCode(byte)).join('')).replaceAll('+','-').replaceAll('/','_').replace(/=+$/,'');}
|
|
91
|
+
function shareJson(text){
|
|
92
|
+
const payload=JSON.parse(text),stack=[];
|
|
93
|
+
// JSON.parse validates syntax; this small token pass preserves duplicate-key
|
|
94
|
+
// evidence that JSON.parse would otherwise discard, including escaped names.
|
|
95
|
+
const tokens=text.match(/"(?:[^"\\]|\\.)*"|[{}\[\]:,]/g)??[];
|
|
96
|
+
for(let at=0;at<tokens.length;at++){
|
|
97
|
+
const token=tokens[at];
|
|
98
|
+
if(token==='{')stack.push(new Set());
|
|
99
|
+
else if(token==='[')stack.push(null);
|
|
100
|
+
else if(token==='}'||token===']')stack.pop();
|
|
101
|
+
else if(token[0]==='"'&&tokens[at+1]===':'){
|
|
102
|
+
const key=JSON.parse(token),keys=stack.at(-1);
|
|
103
|
+
if(!keys||keys.has(key)||SHARE_UNSAFE.has(key))invalidShare();keys.add(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return payload;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function encodeFormState(snapshot,release){
|
|
110
|
+
try{
|
|
111
|
+
shareData(snapshot);shareRecord(snapshot);
|
|
112
|
+
const metadata=shareRelease(release),state=shareRecord(snapshot.state),profiles=shareRecord(state.profiles),jobs=shareRecord(state.jobs);
|
|
113
|
+
const roles=shareList(snapshot.helpers,[...metadata.roles.keys()]),selected=shareList(snapshot.pickedTools,metadata.clients);
|
|
114
|
+
const tools=selected.map(id=>{
|
|
115
|
+
if(!Object.hasOwn(profiles,id)||!Object.hasOwn(jobs,id))invalidShare();
|
|
116
|
+
const profile=shareRecord(profiles[id]),choices=shareRecord(jobs[id]);
|
|
117
|
+
if(typeof profile.custom!=='boolean'||(profile.modeSelected!==undefined&&typeof profile.modeSelected!=='boolean'))invalidShare();
|
|
118
|
+
return {id,mode:profile.modeSelected===false?null:profile.custom?'custom':profile.presetId,base:profile.presetId,choices:Object.fromEntries(roles.map(role=>{
|
|
119
|
+
if(!Object.hasOwn(choices,role))invalidShare();return [role,shareChoice(choices[role],id,metadata)];
|
|
120
|
+
}))};
|
|
121
|
+
});
|
|
122
|
+
const payload=sharePayload({v:1,release:release.version,roles,tools,scope:snapshot.selectedScope,maxConcurrent:state.maxConcurrent},release,metadata);
|
|
123
|
+
if(!roles.length&&!tools.length)return '';
|
|
124
|
+
const bytes=new TextEncoder().encode(JSON.stringify(payload));if(bytes.length>SHARE_BYTES)invalidShare();
|
|
125
|
+
const token=shareBase64(bytes);if(token.length>SHARE_TOKEN)invalidShare();return '#setup='+token;
|
|
126
|
+
}catch(error){if(error?.code==='INVALID_SHARE_LINK'||error?.code==='SHARE_RELEASE_MISMATCH')throw error;invalidShare();}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function decodeFormState(fragment,release){
|
|
130
|
+
try{
|
|
131
|
+
if(typeof fragment!=='string')invalidShare();
|
|
132
|
+
if(!fragment.startsWith('#setup='))return null;
|
|
133
|
+
const token=fragment.slice(7);
|
|
134
|
+
if(!token.length||token.length>SHARE_TOKEN||!/^[A-Za-z0-9_-]+$/.test(token)||token.length%4===1)invalidShare();
|
|
135
|
+
const binary=atob(token.replaceAll('-','+').replaceAll('_','/'));
|
|
136
|
+
if(binary.length>SHARE_BYTES)invalidShare();
|
|
137
|
+
const bytes=Uint8Array.from(binary,char=>char.charCodeAt(0));if(shareBase64(bytes)!==token)invalidShare();
|
|
138
|
+
const payload=shareJson(new TextDecoder('utf-8',{fatal:true}).decode(bytes));shareData(payload);
|
|
139
|
+
return sharePayload(payload,release,shareRelease(release));
|
|
140
|
+
}catch(error){if(error?.code==='INVALID_SHARE_LINK'||error?.code==='SHARE_RELEASE_MISMATCH')throw error;invalidShare();}
|
|
141
|
+
}
|
package/src/toml.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { parseForESLint, getStaticTOMLValue } from 'toml-eslint-parser';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
|
|
4
|
+
function fail(code) { throw Object.assign(new Error(code), {code}); }
|
|
5
|
+
function ast(text) {
|
|
6
|
+
if (typeof text !== 'string' || Buffer.byteLength(text) > 1024 * 1024) fail('INVALID_TOML');
|
|
7
|
+
try { return parseForESLint(text, {tomlVersion:'1.0'}).ast; }
|
|
8
|
+
catch { fail('INVALID_TOML'); }
|
|
9
|
+
}
|
|
10
|
+
export function parseToml(text) { return getStaticTOMLValue(ast(text)); }
|
|
11
|
+
const keys = key => key.keys.map(k => k.type === 'TOMLBare' ? k.name : k.value);
|
|
12
|
+
const same = (a, b) => isDeepStrictEqual(a, b);
|
|
13
|
+
const literal = value => {
|
|
14
|
+
if (!['string', 'number', 'boolean'].includes(typeof value) || (typeof value === 'number' && !Number.isSafeInteger(value))) fail('UNSUPPORTED_TOML_VALUE');
|
|
15
|
+
return JSON.stringify(value);
|
|
16
|
+
};
|
|
17
|
+
const keyText = key => /^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key);
|
|
18
|
+
function locate(tree, target) {
|
|
19
|
+
for (const node of tree.body[0].body) {
|
|
20
|
+
const table = node.type === 'TOMLTable' ? node : null;
|
|
21
|
+
for (const kv of table ? table.body : [node]) {
|
|
22
|
+
if (kv.type !== 'TOMLKeyValue') continue;
|
|
23
|
+
const full = [...(table?.resolvedKey ?? []), ...keys(kv.key)];
|
|
24
|
+
if (same(full, target)) return kv;
|
|
25
|
+
if (full.length < target.length && same(full, target.slice(0, full.length))) fail('UNSUPPORTED_TOML_STRUCTURE');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function replace(text, start, end, value) { return text.slice(0, start) + value + text.slice(end); }
|
|
31
|
+
function newline(text) { return text.includes('\r\n') ? '\r\n' : '\n'; }
|
|
32
|
+
function endLine(text, position) { const i = text.indexOf('\n', position); return i === -1 ? text.length : i + 1; }
|
|
33
|
+
function removeStatement(text, node) {
|
|
34
|
+
const startLine = text.lastIndexOf('\n', node.range[0] - 1) + 1;
|
|
35
|
+
const start = /^\s*$/.test(text.slice(startLine, node.range[0])) ? startLine : node.range[0];
|
|
36
|
+
const end = endLine(text, node.range[1]);
|
|
37
|
+
const tail = text.slice(node.range[1], end);
|
|
38
|
+
return replace(text, start, /^\s*$/.test(tail) ? end : node.range[1], '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function setTomlValue(text, target, value) {
|
|
42
|
+
const tree = ast(text);
|
|
43
|
+
const found = locate(tree, target);
|
|
44
|
+
const after = literal(value);
|
|
45
|
+
const ownership = {kind:'scalar',path:target,before:found ? text.slice(...found.value.range) : null,after};
|
|
46
|
+
if (found) return {text:replace(text, ...found.value.range, after),ownership};
|
|
47
|
+
const tablePath = target.slice(0, -1);
|
|
48
|
+
const tables = tree.body[0].body.filter(n => n.type === 'TOMLTable');
|
|
49
|
+
// Arrays of tables and inline/dotted parent definitions cannot be extended safely here.
|
|
50
|
+
if (tables.some(n => n.kind === 'array' && same(n.resolvedKey.slice(0, tablePath.length), tablePath))) fail('UNSUPPORTED_TOML_STRUCTURE');
|
|
51
|
+
const table = tables.find(n => n.kind === 'standard' && same(n.resolvedKey, tablePath));
|
|
52
|
+
const nl = newline(text);
|
|
53
|
+
const entry = `${keyText(target.at(-1))} = ${after}${nl}`;
|
|
54
|
+
if (table) {
|
|
55
|
+
const at = endLine(text, table.key.range[1]);
|
|
56
|
+
const separator = at === text.length && !text.endsWith('\n') ? nl : '';
|
|
57
|
+
text = replace(text, at, at, separator + entry);
|
|
58
|
+
} else if (tablePath.length === 0) {
|
|
59
|
+
text = entry + text;
|
|
60
|
+
} else {
|
|
61
|
+
const prefix = text.length && !text.endsWith('\n') ? nl : '';
|
|
62
|
+
text += `${prefix}${nl}[${tablePath.map(keyText).join('.')}]${nl}${entry}`;
|
|
63
|
+
ownership.addedTable = tablePath;
|
|
64
|
+
}
|
|
65
|
+
ast(text);
|
|
66
|
+
return {text,ownership};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function restoreTomlValue(text, ownership) {
|
|
70
|
+
const tree = ast(text);
|
|
71
|
+
const found = locate(tree, ownership.path);
|
|
72
|
+
if (ownership.kind === 'removed') {
|
|
73
|
+
if (found) fail('OWNED_DRIFT');
|
|
74
|
+
if (ownership.before === null) return text;
|
|
75
|
+
const before = parseToml(`value = ${ownership.before}\n`).value;
|
|
76
|
+
return setTomlValue(text,ownership.path,before).text;
|
|
77
|
+
}
|
|
78
|
+
if (!found || text.slice(...found.value.range) !== ownership.after) fail('OWNED_DRIFT');
|
|
79
|
+
if (ownership.before !== null) return replace(text, ...found.value.range, ownership.before);
|
|
80
|
+
text = removeStatement(text, found);
|
|
81
|
+
if (ownership.addedTable) {
|
|
82
|
+
const table = ast(text).body[0].body.find(n => n.type === 'TOMLTable' && n.kind === 'standard' && same(n.resolvedKey, ownership.addedTable));
|
|
83
|
+
if (table && table.body.length === 0) text = removeStatement(text, table);
|
|
84
|
+
}
|
|
85
|
+
ast(text);
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function removeTomlValue(text,target) {
|
|
90
|
+
const tree=ast(text),found=locate(tree,target);
|
|
91
|
+
if(!found)return {text,ownership:{kind:'removed',path:target,before:null}};
|
|
92
|
+
const ownership={kind:'removed',path:target,before:text.slice(...found.value.range)};
|
|
93
|
+
const result=removeStatement(text,found);
|
|
94
|
+
ast(result);
|
|
95
|
+
return {text:result,ownership};
|
|
96
|
+
}
|