coding-artifact-evaluation-shell 0.0.0-stage → 0.1.0-eidolon.a1a6ca75ec65e
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/AGENTS.md +15 -0
- package/README.md +56 -2
- package/SKILL.md +30 -0
- package/SOP.md +157 -0
- package/adapter/benchmark.ts +28 -0
- package/app/app.ts +15 -0
- package/app/host.ts +30 -0
- package/budget.ts +49 -0
- package/bun.lock +1045 -0
- package/calibrate.ts +81 -0
- package/calibration-history.ts +33 -0
- package/calibration-recheck.ts +74 -0
- package/calibration-report.ts +36 -0
- package/campaign.ts +85 -0
- package/capsule.ts +162 -0
- package/cli.ts +23 -0
- package/coding-scope.ts +13 -0
- package/contract.ts +116 -0
- package/counter-review.ts +74 -0
- package/coverage.ts +74 -0
- package/environment.ts +11 -0
- package/feedback.ts +31 -0
- package/history.ts +11 -0
- package/host.xnl +7 -0
- package/index.ts +6 -0
- package/logic.ts +135 -0
- package/manifest.xnl +10 -0
- package/native-references.ts +21 -0
- package/needs.ts +33 -0
- package/obligation-graph.ts +47 -0
- package/package.json +32 -4
- package/proposal.ts +94 -0
- package/protocol-error.ts +1 -0
- package/replay.ts +39 -0
- package/representation.ts +24 -0
- package/resources/counter-review.md +16 -0
- package/resources/evaluate.md +34 -0
- package/resources/iterate.md +20 -0
- package/resources/review.md +25 -0
- package/resources/submit.md +18 -0
- package/support/agent-runtime.ts +520 -0
- package/support/artifact-inventory.ts +20 -0
- package/support/artifact-links.ts +82 -0
- package/support/browser-channel.ts +209 -0
- package/support/browser-client.ts +33 -0
- package/support/browser-owner.ts +69 -0
- package/support/browser-worker.ts +61 -0
- package/support/campaign-ledger.ts +66 -0
- package/support/code-knowledge.ts +73 -0
- package/support/counter-review.ts +35 -0
- package/support/ego-tools.ts +19 -0
- package/support/evidence-catalog.ts +37 -0
- package/support/evidence-input.ts +70 -0
- package/support/evidence-object.ts +9 -0
- package/support/evidence-read.ts +18 -0
- package/support/execution-profile.ts +65 -0
- package/support/guidance.ts +16 -0
- package/support/history.ts +89 -0
- package/support/http-request.ts +21 -0
- package/support/isolation.ts +94 -0
- package/support/judge.ts +60 -0
- package/support/model-turn.ts +94 -0
- package/support/observation-agent.ts +279 -0
- package/support/observation-protocol.ts +33 -0
- package/support/observers.ts +102 -0
- package/support/process.ts +22 -0
- package/support/proposal-check.ts +82 -0
- package/support/retention.ts +93 -0
- package/support/runtime-binding.ts +39 -0
- package/support/runtime-preflight.ts +16 -0
- package/support/service-evidence.ts +23 -0
- package/support/service-group.ts +45 -0
- package/support/service.ts +84 -0
- package/support/source-identity.ts +22 -0
- package/support/target-group-view.ts +11 -0
- package/target-groups.ts +57 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import {copyArtifact} from './artifact-links';
|
|
4
|
+
import {digest,fingerprint} from './isolation';
|
|
5
|
+
|
|
6
|
+
interface RetainedFile {path:string;sha256:string;bytes:number;link?:string}
|
|
7
|
+
export interface RetainedEvaluation {
|
|
8
|
+
version:1;
|
|
9
|
+
closed:boolean;
|
|
10
|
+
artifactDigest:string;
|
|
11
|
+
originalRunRoot:string;
|
|
12
|
+
originalArtifactRoot:string;
|
|
13
|
+
files:RetainedFile[];
|
|
14
|
+
limitations:string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function absoluteDirectory(directory:string):void {
|
|
18
|
+
if(!path.isAbsolute(directory)||path.resolve(directory)!==directory)throw Error('Retention requires an absolute path without traversal');
|
|
19
|
+
let parent=directory;
|
|
20
|
+
while(!fs.existsSync(parent))parent=path.dirname(parent);
|
|
21
|
+
if(fs.realpathSync(parent)!==parent)throw Error('Retention symlink ancestor rejected');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function assertRetentionDestination(destination:string,excludedRoots:string[]=[]):void {
|
|
25
|
+
absoluteDirectory(destination);
|
|
26
|
+
if(fs.existsSync(destination))throw Error('Never overwrite retained evidence');
|
|
27
|
+
if(excludedRoots.some(root=>destination===root||destination.startsWith(root+path.sep)||root.startsWith(destination+path.sep)))throw Error('Retention must be disjoint from execution and artifact');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function inventory(root:string,relative=''):RetainedFile[] {
|
|
31
|
+
return fs.readdirSync(path.join(root,relative),{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name)).flatMap(entry=>{
|
|
32
|
+
const name=path.join(relative,entry.name),file=path.join(root,name);
|
|
33
|
+
if(entry.isDirectory())return inventory(root,name);
|
|
34
|
+
if(entry.isSymbolicLink()){
|
|
35
|
+
const link=fs.readlinkSync(file),resolved=fs.realpathSync(file);
|
|
36
|
+
if(!resolved.startsWith(root+path.sep)||!fs.statSync(resolved).isFile())throw Error('Retained link escapes archive');
|
|
37
|
+
return [{path:name,sha256:digest(link),bytes:Buffer.byteLength(link),link}];
|
|
38
|
+
}
|
|
39
|
+
if(!entry.isFile())throw Error('Unsupported retained object');
|
|
40
|
+
const bytes=fs.readFileSync(file);
|
|
41
|
+
return [{path:name,sha256:digest(bytes),bytes:bytes.length}];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Immutable data-only closure. Never copy HOME, auth, bin, live channels or service capabilities. */
|
|
46
|
+
export function retainEvaluation(input:{runRoot:string;artifact:string;artifactDigest:string;destination:string}):RetainedEvaluation {
|
|
47
|
+
const {runRoot,artifact,artifactDigest,destination}=input;
|
|
48
|
+
assertRetentionDestination(destination,[runRoot,artifact]);
|
|
49
|
+
if(fingerprint(artifact)!==artifactDigest)throw Error('Artifact changed before retention');
|
|
50
|
+
fs.mkdirSync(destination,{mode:0o700});
|
|
51
|
+
copyArtifact({source:artifact,destination:path.join(destination,'artifact')});
|
|
52
|
+
const copy=(source:string,relative:string)=>{
|
|
53
|
+
if(!fs.existsSync(source))return;
|
|
54
|
+
const stat=fs.lstatSync(source);
|
|
55
|
+
if(stat.isSymbolicLink())throw Error('Runtime evidence symlink rejected');
|
|
56
|
+
const target=path.join(destination,relative);
|
|
57
|
+
if(stat.isDirectory()){
|
|
58
|
+
fs.mkdirSync(target,{recursive:true,mode:0o700});
|
|
59
|
+
for(const name of fs.readdirSync(source))copy(path.join(source,name),path.join(relative,name));
|
|
60
|
+
}else if(stat.isFile()){
|
|
61
|
+
fs.mkdirSync(path.dirname(target),{recursive:true,mode:0o700});
|
|
62
|
+
fs.writeFileSync(target,fs.readFileSync(source),{flag:'wx',mode:0o600});
|
|
63
|
+
}else throw Error('Unsupported runtime evidence');
|
|
64
|
+
};
|
|
65
|
+
for(const name of ['input','logs','target-input.json','final-input.json','coding-scope.json','groups.json','decision.json','report.json','ledger.jsonl','reservations.jsonl','invocations.jsonl','execution-profile.json','execution-preflight.json','runtime-preflight.json','judge-runtime-preflight.json','counter-review.json'])copy(path.join(runRoot,name),path.join('evaluation',name));
|
|
66
|
+
// Exact final model outputs are data; other scratch may contain runtime capabilities.
|
|
67
|
+
const invocationFile=path.join(runRoot,'invocations.jsonl');
|
|
68
|
+
if(fs.existsSync(invocationFile))for(const line of fs.readFileSync(invocationFile,'utf8').trim().split('\n')){
|
|
69
|
+
if(!line)continue;
|
|
70
|
+
const {id}=JSON.parse(line) as {id:string};
|
|
71
|
+
if(!/^[a-zA-Z0-9-]+$/.test(id))throw Error('Invalid retained invocation identity');
|
|
72
|
+
copy(path.join(runRoot,'home','tmp','turn-'+id,id+'.json'),path.join('model-output',id+'.json'));
|
|
73
|
+
}
|
|
74
|
+
if(fingerprint(artifact)!==artifactDigest)throw Error('Artifact changed during retention');
|
|
75
|
+
const value:RetainedEvaluation={version:1,closed:fs.existsSync(path.join(runRoot,'decision.json'))&&fs.existsSync(path.join(runRoot,'report.json')),artifactDigest,originalRunRoot:runRoot,originalArtifactRoot:artifact,files:inventory(destination),limitations:['Data-only immutable evidence; not an executable checkpoint','Hashes establish byte consistency, not semantic correctness or a signature','Original target bytes are in target-input/final-input; absolute original paths are historical locators','Partial closures preserve failures and cannot certify completion']};
|
|
76
|
+
fs.writeFileSync(path.join(destination,'archive.json'),JSON.stringify(value,null,2)+'\n',{flag:'wx',mode:0o600});
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Read-only verification works after every original temporary directory is gone. */
|
|
81
|
+
export function verifyRetainedEvaluation(root:string):RetainedEvaluation {
|
|
82
|
+
absoluteDirectory(root);
|
|
83
|
+
const manifest=path.join(root,'archive.json');
|
|
84
|
+
if(fs.lstatSync(manifest).isSymbolicLink())throw Error('Archive manifest symlink rejected');
|
|
85
|
+
const value=JSON.parse(fs.readFileSync(manifest,'utf8')) as RetainedEvaluation;
|
|
86
|
+
if(value.version!==1||!Array.isArray(value.files)||typeof value.closed!=='boolean')throw Error('Invalid retained evidence manifest');
|
|
87
|
+
for(const entry of value.files){
|
|
88
|
+
if(!entry.path||path.isAbsolute(entry.path)||entry.path.split(/[\\/]/).some(part=>!part||part==='.'||part==='..')||entry.path==='archive.json')throw Error('Unsafe retained path');
|
|
89
|
+
}
|
|
90
|
+
const actual=inventory(root).filter(entry=>entry.path!=='archive.json');
|
|
91
|
+
if(JSON.stringify(actual)!==JSON.stringify(value.files))throw Error('Retained evidence missing, changed or extended');
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import type {RuntimeSpec} from '../contract';
|
|
5
|
+
import type {AgentRun} from './agent-runtime';
|
|
6
|
+
|
|
7
|
+
export function environmentName(name:string):void {
|
|
8
|
+
if(!/^[_A-Z][_A-Z0-9]*$/.test(name)||['HOME','PATH','CODEX_HOME','TMPDIR','NODE_OPTIONS','BUN_OPTIONS','PYTHONSTARTUP','BASH_ENV','ENV','SHELLOPTS','GIT_CONFIG','GIT_CONFIG_GLOBAL','GIT_CONFIG_SYSTEM'].includes(name)||/^(DYLD_|LD_)/.test(name))throw Error('Runtime system overrides forbidden');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Trusted deployment data only: explicit aliases, dependencies and literal environment. */
|
|
12
|
+
export function bindRuntime(run:AgentRun,spec:RuntimeSpec|undefined):AgentRun {
|
|
13
|
+
if(!spec)return run;
|
|
14
|
+
const readPaths=(spec.readPaths??[]).map(file=>{
|
|
15
|
+
if(!path.isAbsolute(file))throw Error('Runtime dependency roots must be absolute');
|
|
16
|
+
const real=fs.realpathSync(file);
|
|
17
|
+
if(!fs.statSync(real).isDirectory())throw Error('Runtime dependency root must be a directory');
|
|
18
|
+
if(real===run.workspace||real.startsWith(run.workspace+'/'))return real;
|
|
19
|
+
// Dependencies may live in a separate owned temporary installation, not personal
|
|
20
|
+
// data, owner records or arbitrary filesystem roots. Original artifacts stay readonly.
|
|
21
|
+
if(!/^\/private\/tmp\/[^/]+\/.+/.test(real)||real===run.root||real.startsWith(run.root+'/')||run.root.startsWith(real+'/'))throw Error('Runtime dependency root exceeds readonly temporary capability');
|
|
22
|
+
return real;
|
|
23
|
+
});
|
|
24
|
+
const environment={...spec.environment};
|
|
25
|
+
for(const [name,value] of Object.entries(environment)){environmentName(name);if(typeof value!=='string'||value.includes('\0'))throw Error('Literal runtime environment required');}
|
|
26
|
+
const executables:Record<string,string>={};
|
|
27
|
+
const home=os.homedir(),allowed=[path.join(home,'.bun/bin'),path.join(home,'.local/share/uv/python'),'/usr','/bin','/opt','/System','/Library'];
|
|
28
|
+
for(const [name,file] of Object.entries(spec.executables??{})){
|
|
29
|
+
if(!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name)||['codex','ego-browser'].includes(name)||!path.isAbsolute(file))throw Error('Explicit non-agent tool alias required');
|
|
30
|
+
const real=fs.realpathSync(file);
|
|
31
|
+
if(!allowed.some(root=>real.startsWith(root+'/'))&&!readPaths.some(root=>real.startsWith(root+'/')))throw Error('Executable is outside declared host tools or readonly dependency roots');
|
|
32
|
+
fs.accessSync(real,fs.constants.X_OK);executables[name]=real;
|
|
33
|
+
}
|
|
34
|
+
const bin=path.join(run.root,'bin','runtime');fs.mkdirSync(bin,{mode:0o700});
|
|
35
|
+
for(const [name,file] of Object.entries(executables))fs.symlinkSync(file,path.join(bin,name));
|
|
36
|
+
const bindings={executables,readPaths,environment};
|
|
37
|
+
fs.writeFileSync(path.join(run.root,'runtime-bindings.json'),JSON.stringify(bindings,null,2),{flag:'wx'});
|
|
38
|
+
return {...run,env:{...run.env,...environment,PATH:`${bin}:${run.env.PATH}`},runtimeReadPaths:[bin,...readPaths,...Object.values(executables)],runtimeBindings:bindings};
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import type {AgentRun,AgentRuntime} from './agent-runtime';
|
|
4
|
+
import {boundary,digest} from './isolation';
|
|
5
|
+
import {executeNative} from './process';
|
|
6
|
+
/** No model/auth/implementation: exercise the actual runtime through the exact boundary. */
|
|
7
|
+
export async function preflightJudgeRuntime(run:AgentRun,agent:AgentRuntime,timeoutMs=10000,name='runtime-preflight') {
|
|
8
|
+
if(!/^[a-z][a-z0-9-]{0,80}$/.test(name))throw Error('Invalid runtime preflight name');
|
|
9
|
+
const log=path.join(run.root,'logs',`${name}.log`);
|
|
10
|
+
const result=await executeNative({interrupted:false},{argv:boundary(run,agent,'judge',[agent.executable(run),'--version']),cwd:run.controlWorkspace??run.workspace,env:run.env,log,timeoutMs});
|
|
11
|
+
const content=fs.readFileSync(log,'utf8');
|
|
12
|
+
const receipt={...result,executable:agent.executable(run),outputDigest:digest(content),output:content,modelInvoked:false};
|
|
13
|
+
fs.writeFileSync(path.join(run.root,`${name}.json`),JSON.stringify(receipt,null,2)+'\n',{flag:'wx'});
|
|
14
|
+
if(result.code!==0||!content.includes('codex-cli'))throw Error(`Judge runtime preflight failed before model use: ${content}`);
|
|
15
|
+
return receipt;
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import {randomUUID} from 'node:crypto';
|
|
3
|
+
import type {Observation,OwnedServiceCommand,ProcessProbe,ServiceLaunchReceipt} from '../contract';
|
|
4
|
+
import {digest} from './isolation';
|
|
5
|
+
|
|
6
|
+
/** Project an actual native launch into the same immutable fact catalog. */
|
|
7
|
+
export function serviceObservation(input:{runId:string;artifactDigest:string},serviceId:string,receipt:ServiceLaunchReceipt):Observation {
|
|
8
|
+
const content=JSON.stringify({serviceId,...receipt});
|
|
9
|
+
return {id:randomUUID(),...input,phase:'observation',channel:'process',purpose:'delivery',method:'native owned service launch/readiness',conditions:'Declared deployment argv/cwd, native fresh port and initially empty leased state; only explicit port/data/dependency environment projected',content,contentDigest:digest(content),bytes:Buffer.byteLength(content),elapsedMs:receipt.elapsedMs,status:'completed',argv:receipt.argv,limitations:['Availability and supplied launch conditions only, not proof of application business correctness','Fresh directory and supplied environment do not prove the application exclusively stores state there','Inherited environment and credentials are deliberately not projected']};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Finite process channel cannot duplicate a declared long-lived lease. */
|
|
13
|
+
export function assertTerminatingProcess(probe:ProcessProbe,services:OwnedServiceCommand[],executables:Record<string,string>):void {
|
|
14
|
+
const executable=(value:string)=>executables[value]??value;
|
|
15
|
+
const repeated=services.find(service=>{
|
|
16
|
+
if(path.normalize(probe.cwd??'.')!==path.normalize(service.cwd??'.'))return false;
|
|
17
|
+
if(probe.argv.length!==service.argv.length)return false;
|
|
18
|
+
return probe.argv.every((value,index)=>index===0
|
|
19
|
+
? executable(value)===executable(service.argv[index]!)
|
|
20
|
+
: value===service.argv[index]);
|
|
21
|
+
});
|
|
22
|
+
if(repeated)throw Error(`Process channel requires a terminating command; service ${repeated.serviceId} is already leased. Read its native launch receipt or use the explicitly granted owned restart capability`);
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {ServiceGraph,ServiceNode} from '../contract';
|
|
2
|
+
import type {AgentRun,AgentRuntime} from './agent-runtime';
|
|
3
|
+
import {startService,serviceDirectory} from './service';
|
|
4
|
+
import {environmentName} from './runtime-binding';
|
|
5
|
+
|
|
6
|
+
/** Pure dependency plan. IDs denote deployment nodes, never application scenarios. */
|
|
7
|
+
export function serviceOrder(graph:ServiceGraph):ServiceNode[] {
|
|
8
|
+
if(!graph||!Array.isArray(graph.nodes)||!graph.nodes.length||graph.nodes.length>8)throw Error('Service graph needs 1..8 declared nodes');
|
|
9
|
+
const nodes=new Map<string,ServiceNode>();
|
|
10
|
+
for(const node of graph.nodes){if(!/^[a-zA-Z0-9][\w-]*$/.test(node.id)||nodes.has(node.id))throw Error('Unique service IDs required');nodes.set(node.id,node);}
|
|
11
|
+
if(!nodes.has(graph.entry))throw Error('Unknown entry service');
|
|
12
|
+
const visited=new Set<string>(),pending=new Set<string>(),order:ServiceNode[]=[];
|
|
13
|
+
function visit(id:string):void {
|
|
14
|
+
if(visited.has(id))return;
|
|
15
|
+
const node=nodes.get(id);if(!node)throw Error('Unknown linked service');
|
|
16
|
+
if(pending.has(id))throw Error('Cyclic service dependency');pending.add(id);
|
|
17
|
+
for(const [name,target] of Object.entries(node.links??{})){environmentName(name);visit(target);}
|
|
18
|
+
pending.delete(id);visited.add(id);order.push(node);
|
|
19
|
+
}
|
|
20
|
+
for(const id of nodes.keys())visit(id);
|
|
21
|
+
return order;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Exact owned origins, dependency-only network and reverse lifecycle cleanup. */
|
|
25
|
+
export async function startServices(runtime:{run:AgentRun;agent:AgentRuntime;deadlineAt:number},graph:ServiceGraph) {
|
|
26
|
+
const order=serviceOrder(graph);
|
|
27
|
+
for(const node of order)serviceDirectory(runtime.run.workspace,node.cwd);
|
|
28
|
+
const leases=new Map<string,Awaited<ReturnType<typeof startService>>>();
|
|
29
|
+
let closing:Promise<void>|undefined;
|
|
30
|
+
const close=()=>closing??=(async()=>{
|
|
31
|
+
const errors:unknown[]=[];
|
|
32
|
+
for(const lease of [...leases.values()].reverse())try{await lease.close();}catch(error){errors.push(error);}
|
|
33
|
+
if(errors.length)throw new AggregateError(errors,'Service graph cleanup failed');
|
|
34
|
+
})();
|
|
35
|
+
try{
|
|
36
|
+
for(const node of order){
|
|
37
|
+
const environment=Object.fromEntries(Object.entries(node.links??{}).map(([name,id])=>[name,leases.get(id)!.origin]));
|
|
38
|
+
const outboundPorts=Object.values(node.links??{}).map(id=>Number(new URL(leases.get(id)!.origin).port));
|
|
39
|
+
leases.set(node.id,await startService(runtime,node,{environment,outboundPorts}));
|
|
40
|
+
}
|
|
41
|
+
return {origin:leases.get(graph.entry)!.origin,origins:Object.fromEntries([...leases].map(([id,lease])=>[id,lease.origin])),receipts:[...leases].map(([serviceId,lease])=>({serviceId,receipt:lease.receipt})),close,restart:async(id:string,deadlineAt?:number)=>{
|
|
42
|
+
if(closing)throw Error('Service graph is closed');const lease=leases.get(id);if(!lease)throw Error('Unknown owned service');return lease.restart(deadlineAt);
|
|
43
|
+
}};
|
|
44
|
+
}catch(error){await close();throw error;}
|
|
45
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import {spawn} from 'node:child_process';
|
|
4
|
+
import {randomUUID} from 'node:crypto';
|
|
5
|
+
import type {ServiceSpec,ServiceLaunchReceipt} from '../contract';
|
|
6
|
+
import type {AgentRun,AgentRuntime} from './agent-runtime';
|
|
7
|
+
import {boundary} from './isolation';
|
|
8
|
+
import {environmentName} from './runtime-binding';
|
|
9
|
+
|
|
10
|
+
export function serviceDirectory(workspace:string,relative='.') :string {
|
|
11
|
+
const root=fs.realpathSync(workspace),file=path.resolve(root,relative);
|
|
12
|
+
if(file!==root&&!file.startsWith(root+'/'))throw Error('Service cwd escapes artifact');
|
|
13
|
+
const real=fs.realpathSync(file);if(real!==file||!fs.statSync(real).isDirectory())throw Error('Service cwd must be a real artifact directory');
|
|
14
|
+
return file;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function originPath(origin:string,relative:string):string {
|
|
18
|
+
if(typeof relative!=='string'||!relative.startsWith('/')||relative.startsWith('//')||relative.includes('\\')||/[\r\n]/.test(relative))throw Error('Literal origin-relative path required');
|
|
19
|
+
const url=new URL(relative,origin);
|
|
20
|
+
if(url.origin!==origin||url.pathname!==relative.split('?')[0])throw Error('Path escapes leased origin or is normalized');
|
|
21
|
+
return url.href;
|
|
22
|
+
}
|
|
23
|
+
/** One native service/data lease. No application-specific setup, assertions or commands. */
|
|
24
|
+
export async function startService(runtime:{run:AgentRun;agent:AgentRuntime;deadlineAt:number},spec:ServiceSpec,binding:{environment:Record<string,string>;outboundPorts:number[]}={environment:{},outboundPorts:[]}) {
|
|
25
|
+
if(!Array.isArray(spec.argv)||!spec.argv.length||spec.argv.some(s=>typeof s!=='string'||!s.trim()))throw Error('Service requires explicit argv');
|
|
26
|
+
const envNames=[spec.portEnv,...spec.dataEnv,...Object.keys(spec.dataFiles??{}),...Object.keys(binding.environment)];
|
|
27
|
+
if(new Set(envNames).size!==envNames.length)throw Error('Declare distinct port/data/link env names');
|
|
28
|
+
for(const name of envNames)environmentName(name);
|
|
29
|
+
for(const name of Object.values(spec.dataFiles??{}))if(!name||path.isAbsolute(name)||name.split(/[\\/]/).some(part=>part==='..'||part==='.')||name.includes('\0'))throw Error('Data file must be a safe lease-relative path');
|
|
30
|
+
if(spec.readyTimeoutMs!==undefined&&(!Number.isSafeInteger(spec.readyTimeoutMs)||spec.readyTimeoutMs<1||spec.readyTimeoutMs>30000))throw Error('Readiness bound must be 1..30000ms');
|
|
31
|
+
const {run,agent}=runtime,leaseId=randomUUID(),data=path.join(run.home,'tmp',`service-${leaseId}`);
|
|
32
|
+
const allocation=Bun.serve({hostname:'127.0.0.1',port:0,fetch:()=>new Response('allocation')});
|
|
33
|
+
const port=allocation.port!;allocation.stop(true);
|
|
34
|
+
const origin=`http://127.0.0.1:${port}`,ready=originPath(origin,spec.readyPath);
|
|
35
|
+
fs.mkdirSync(data,{mode:0o700});
|
|
36
|
+
const environment={...binding.environment,[spec.portEnv]:String(port),...Object.fromEntries(spec.dataEnv.map(n=>[n,data])),...Object.fromEntries(Object.entries(spec.dataFiles??{}).map(([name,file])=>[name,path.join(data,file)]))};
|
|
37
|
+
const env={...run.env,...environment},cwd=serviceDirectory(run.execution?.root??run.workspace,spec.cwd);
|
|
38
|
+
const argv=boundary(run,agent,'service',spec.argv,[],port,[],binding.outboundPorts);
|
|
39
|
+
const lifecycle=path.join(run.root,'logs',`service-${leaseId}-lifecycle.jsonl`);
|
|
40
|
+
const record=(event:object)=>fs.appendFileSync(lifecycle,JSON.stringify({leaseId,origin,data,at:Date.now(),...event})+'\n');
|
|
41
|
+
let child:ReturnType<typeof spawn>|undefined,failure:unknown,closing:Promise<void>|undefined,generation=0;
|
|
42
|
+
const stop=()=>{try{if(child?.pid)process.kill(-child.pid,'SIGKILL');}catch{/* own group already closed */}};
|
|
43
|
+
process.once('SIGTERM',stop);process.once('SIGINT',stop);
|
|
44
|
+
const stopGeneration=async()=>{
|
|
45
|
+
const current=child;stop();
|
|
46
|
+
if(current?.exitCode===null&¤t.signalCode===null)await Promise.race([new Promise<void>(resolve=>current.once('close',()=>resolve())),Bun.sleep(2000)]);
|
|
47
|
+
let reachable=false;try{await fetch(origin,{signal:AbortSignal.timeout(500)});reachable=true;}catch{/* no listener */}
|
|
48
|
+
record({kind:'generation-stopped',generation,pid:current?.pid,reachable});
|
|
49
|
+
if(reachable)throw Error('Owned service endpoint remained reachable after close');
|
|
50
|
+
};
|
|
51
|
+
const close=()=>closing??=(async()=>{
|
|
52
|
+
process.off('SIGTERM',stop);process.off('SIGINT',stop);await stopGeneration();
|
|
53
|
+
fs.writeFileSync(path.join(run.root,`service-${leaseId}-closed.json`),JSON.stringify({leaseId,pid:child?.pid,origin,reachable:false,generations:generation+1}));
|
|
54
|
+
})();
|
|
55
|
+
const launch=async(phaseDeadlineAt=runtime.deadlineAt)=>{
|
|
56
|
+
const started=Date.now();
|
|
57
|
+
if(Date.now()>=Math.min(runtime.deadlineAt,phaseDeadlineAt))throw Error('Service window expired before launch');
|
|
58
|
+
failure=undefined;
|
|
59
|
+
const log=path.join(run.root,'logs',`service-${leaseId}${generation?'-generation-'+generation:''}.log`),fd=fs.openSync(log,'wx',0o600);
|
|
60
|
+
try{child=spawn(argv[0]!,argv.slice(1),{cwd,env,detached:true,stdio:['ignore',fd,fd]});}finally{fs.closeSync(fd);}
|
|
61
|
+
child.on('error',e=>{failure=e;});
|
|
62
|
+
const deadline=Math.min(runtime.deadlineAt,phaseDeadlineAt,Date.now()+(spec.readyTimeoutMs??15000));
|
|
63
|
+
while(Date.now()<deadline){
|
|
64
|
+
if(failure||child.exitCode!==null||child.signalCode!==null)throw Error(`Service exited before readiness: ${String(failure??fs.readFileSync(log,'utf8'))}`);
|
|
65
|
+
try{
|
|
66
|
+
// Any actual HTTP response proves readiness, not business correctness.
|
|
67
|
+
const response=await fetch(ready,{redirect:'manual',signal:AbortSignal.timeout(Math.min(1000,Math.max(1,deadline-Date.now())))});
|
|
68
|
+
await response.body?.cancel();
|
|
69
|
+
const receipt:ServiceLaunchReceipt={leaseId,pid:child.pid,origin,data,argv:[...spec.argv],cwd,generation,environment,state:{directory:data,initiallyEmpty:true},readiness:{path:spec.readyPath,status:response.status},semanticPass:false,elapsedMs:Date.now()-started};
|
|
70
|
+
record({kind:'generation-ready',...receipt});
|
|
71
|
+
if(!generation)fs.writeFileSync(path.join(run.root,`service-${leaseId}.json`),JSON.stringify(receipt));
|
|
72
|
+
return receipt;
|
|
73
|
+
}catch{/* wait only until the declared startup deadline */}
|
|
74
|
+
await Bun.sleep(Math.min(200,Math.max(0,deadline-Date.now())));
|
|
75
|
+
}
|
|
76
|
+
throw Error('Native service readiness deadline exhausted');
|
|
77
|
+
};
|
|
78
|
+
const restart=async(phaseDeadlineAt=runtime.deadlineAt)=>{
|
|
79
|
+
if(closing||Date.now()>=Math.min(runtime.deadlineAt,phaseDeadlineAt))throw Error('Closed or expired service lease cannot restart');
|
|
80
|
+
const previousPid=child?.pid;await stopGeneration();generation++;
|
|
81
|
+
try{return {...await launch(phaseDeadlineAt),previousPid,statePreserved:true};}catch(error){await close();throw error;}
|
|
82
|
+
};
|
|
83
|
+
try{const receipt=await launch();return {leaseId,origin,data,receipt,close,restart};}catch(error){await close();throw error;}
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import {digest} from './isolation';
|
|
4
|
+
|
|
5
|
+
/** Evaluator source identity, distinct from the strict delivered-artifact fingerprint.
|
|
6
|
+
* Installed dependency/cache trees are not source; the independent lock is source.
|
|
7
|
+
*/
|
|
8
|
+
export function evaluatorSourceDigest(root: string): string {
|
|
9
|
+
const excluded = new Set(['node_modules', '.git', 'dist', 'coverage', '.tmp']);
|
|
10
|
+
function walk(directory: string): string[] {
|
|
11
|
+
const entries = fs.readdirSync(directory, {withFileTypes: true})
|
|
12
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
13
|
+
return entries.flatMap(entry => {
|
|
14
|
+
if (entry.isDirectory() && excluded.has(entry.name)) return [];
|
|
15
|
+
const file = path.join(directory, entry.name);
|
|
16
|
+
if (entry.isSymbolicLink()) throw Error(`Evaluator source contains a symlink: ${file}`);
|
|
17
|
+
if (entry.isDirectory()) return walk(file);
|
|
18
|
+
return [`${path.relative(root, file)}:${digest(fs.readFileSync(file))}`];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return digest(walk(root).join('\n'));
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {readFileSync} from 'node:fs';
|
|
2
|
+
import {isAbsolute} from 'node:path';
|
|
3
|
+
import {projectTargetGroups,type TargetGroup} from '../target-groups';
|
|
4
|
+
|
|
5
|
+
/** Explicit readonly files only; no history discovery or new model/effect. */
|
|
6
|
+
export function readTargetGroupView(inputPath:string,decisionPath:string,groups:TargetGroup[]) {
|
|
7
|
+
if(!isAbsolute(inputPath)||!isAbsolute(decisionPath))throw Error('Absolute native input and decision paths required');
|
|
8
|
+
const input=JSON.parse(readFileSync(inputPath,'utf8'));
|
|
9
|
+
const decision=JSON.parse(readFileSync(decisionPath,'utf8'));
|
|
10
|
+
return projectTargetGroups(input,decision,groups,{policyVersion:decision.policyVersion,includeCommitments:input.policy?.includeCommitments??false});
|
|
11
|
+
}
|
package/target-groups.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type {Decision,EvaluationConfig,EvaluationInput,Judgment} from './contract';
|
|
2
|
+
import {admit,defaultConfig} from './logic';
|
|
3
|
+
import {obligationGraph} from './obligation-graph';
|
|
4
|
+
import {canonicalIdentity} from './representation';
|
|
5
|
+
|
|
6
|
+
/** Caller-owned presentation scope. Labels never add requirements or evidence. */
|
|
7
|
+
export interface TargetGroup {id:string;label:string;rootIds:string[]}
|
|
8
|
+
|
|
9
|
+
/** Project an admitted graph, not a second judge. Never infer groups from names,
|
|
10
|
+
* source wording, domains, historical scores or model completion assertions.
|
|
11
|
+
*/
|
|
12
|
+
export function projectTargetGroups(input:EvaluationInput,decision:Decision,groups:TargetGroup[],config:EvaluationConfig=defaultConfig) {
|
|
13
|
+
if(decision.runId!==input.runId||decision.artifactDigest!==input.artifactDigest||decision.policyVersion!==config.policyVersion)throw Error('Group view requires the same native evaluation identity and policy');
|
|
14
|
+
if(canonicalIdentity(input.observations)!==canonicalIdentity(decision.observations))throw Error('Group view requires the original decision observations, not later or altered facts');
|
|
15
|
+
if(!Array.isArray(groups)||groups.length>64)throw Error('At most 64 explicit target groups');
|
|
16
|
+
const ids=new Set<string>();
|
|
17
|
+
for(const g of groups){
|
|
18
|
+
if(!g||typeof g.id!=='string'||!g.id.trim()||ids.has(g.id)||typeof g.label!=='string'||!g.label.trim()||!Array.isArray(g.rootIds)||!g.rootIds.length||g.rootIds.some(id=>typeof id!=='string'||!id.trim())||new Set(g.rootIds).size!==g.rootIds.length)throw Error('Groups need unique IDs, labels and distinct nonempty root IDs');
|
|
19
|
+
ids.add(g.id);
|
|
20
|
+
}
|
|
21
|
+
const proposal=decision.proposal;
|
|
22
|
+
const diagnostics=proposal?admit(input,proposal,config):[];
|
|
23
|
+
const eligible=decision.execution==='completed'&&decision.diagnostics.length===0&&proposal!==null&&diagnostics.length===0&&decision.judgment===proposal.judgment;
|
|
24
|
+
// Rejected/unfinished drafts cannot yield a partial success view.
|
|
25
|
+
const graph=eligible?obligationGraph(proposal!):undefined;
|
|
26
|
+
const roots=graph?.roots??[];
|
|
27
|
+
const rootIds=new Set(roots.map(o=>o.id));
|
|
28
|
+
const obligations=new Map(proposal?.obligations.map(o=>[o.id,o]));
|
|
29
|
+
if(graph)for(const g of groups)for(const id of g.rootIds)if(!rootIds.has(id))throw Error('Group references an unknown or non-root obligation: '+id);
|
|
30
|
+
const selected=new Set(groups.flatMap(g=>g.rootIds));
|
|
31
|
+
function sourcesFor(id:string,seen=new Set<string>()):Set<string> {
|
|
32
|
+
if(seen.has(id))return new Set();seen.add(id);
|
|
33
|
+
const o=obligations.get(id)!;
|
|
34
|
+
return new Set([o.sourceId,...o.children.flatMap(child=>[...sourcesFor(child,seen)])]);
|
|
35
|
+
}
|
|
36
|
+
const results=groups.map(g=>{
|
|
37
|
+
let judgment:Judgment='indeterminate';
|
|
38
|
+
let reason='No complete admitted decision is available; drafts cannot be mined for partial success.';
|
|
39
|
+
let sourceIds:string[]=[];
|
|
40
|
+
if(graph){
|
|
41
|
+
const outcomes=g.rootIds.map(id=>graph.compare(id)).filter(o=>o.judgment!=='not-applicable');
|
|
42
|
+
sourceIds=[...new Set(g.rootIds.flatMap(id=>[...sourcesFor(id)]))];
|
|
43
|
+
const reviewed=sourceIds.every(id=>proposal!.sourceReviews.some(r=>r.sourceId===id&&r.complete&&!r.omissions.length&&!r.ambiguities.length));
|
|
44
|
+
if(outcomes.some(o=>o.judgment==='fail')){judgment='fail';reason='An admitted selected required root is contradicted.';}
|
|
45
|
+
else if(outcomes.length&&outcomes.every(o=>o.judgment==='pass')&&reviewed){judgment='pass';reason='All selected applicable roots are supported; related source reviews contain no recorded gaps.';}
|
|
46
|
+
else reason=outcomes.length?'A selected root or its source review remains unresolved.':'No selected applicable root was evaluated; not-applicable is not PASS.';
|
|
47
|
+
}
|
|
48
|
+
return {...g,rootIds:[...g.rootIds],sourceIds,judgment,reason};
|
|
49
|
+
});
|
|
50
|
+
return {runId:decision.runId,artifactDigest:decision.artifactDigest,
|
|
51
|
+
overall:{execution:decision.execution,judgment:decision.judgment,failureClass:decision.failureClass},
|
|
52
|
+
scope:'caller-selected-root-obligations' as const,groups:results,
|
|
53
|
+
ungroupedRootIds:roots.filter(o=>!selected.has(o.id)).map(o=>o.id),
|
|
54
|
+
sharedRootIds:roots.filter(o=>groups.filter(g=>g.rootIds.includes(o.id)).length>1).map(o=>o.id),
|
|
55
|
+
semanticAuthority:false as const,
|
|
56
|
+
limitations:['A group label does not certify unlisted requirements or a whole domain.','This is a view of the original fallible judgment, not a fresh assessment or authorization to change the artifact.','Overall judgment and exit semantics remain unchanged; overlapping groups must not be averaged.']};
|
|
57
|
+
}
|