deadbyte-mcp 0.13.0 → 0.14.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/.gitattributes +7 -0
- package/AGENTS.md +25 -0
- package/CONTEXT.md +3 -3
- package/MANIFEST.SHA256 +66 -40
- package/R42-PARENT.json +19 -0
- package/README.txt +2 -2
- package/bin/appcontainer-stage.obj +0 -0
- package/bin/bootstrap-advapi32.obj +0 -0
- package/bin/bootstrap-exitcode.obj +0 -0
- package/bin/bootstrap-kernel32.obj +0 -0
- package/bin/child-control-probe.obj +0 -0
- package/bin/child-control-stage.obj +0 -0
- package/bin/contained-reverse-worker.obj +0 -0
- package/bin/contained-transform-worker.obj +0 -0
- package/bin/containment-probe.obj +0 -0
- package/bin/deadbyte-contain.obj +0 -0
- package/bin/deadbyte-exec.obj +0 -0
- package/bin/deadbyte-process-host.obj +0 -0
- package/bin/deadbyte-tunnel-host.obj +0 -0
- package/controller/README.TXT +1 -1
- package/controller/deadbyte-controller.ps1 +1 -1
- package/controller/deadbyte-desktop-policy.json +5 -5
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/ARCHITECTURE.md +1 -1
- package/harness/r42-agent-evals.json +48 -0
- package/harness/task-contract.schema.json +33 -0
- package/package.json +9 -7
- package/proof/CONTAINMENT-BUILD.txt +6 -6
- package/scripts/agent-eval-r42.mjs +46 -0
- package/scripts/deferred-slot-operation-r42.mjs +126 -0
- package/scripts/final-closure-r42.mjs +116 -0
- package/scripts/final-closure-verify-r42.mjs +223 -0
- package/scripts/gate-windows-r42.ps1 +54 -0
- package/scripts/release-parity-r42.mjs +235 -0
- package/scripts/release-parity-tests-r42.ps1 +6 -0
- package/scripts/verify-r42-parent.mjs +101 -0
- package/scripts/windows-gate-evidence-r42.mjs +64 -0
- package/src/agent-eval-r42.mjs +73 -0
- package/src/agent-eval-runner-r42.mjs +138 -0
- package/src/agent-governance-r42.mjs +142 -0
- package/src/autonomous-context-engine.mjs +2 -0
- package/src/autonomous-loop-state.mjs +10 -0
- package/src/autonomous-mcp-tools.mjs +21 -6
- package/src/autonomous-output-contracts.mjs +18 -2
- package/src/autonomous-planner-contracts.mjs +3 -0
- package/src/autonomous-planner.mjs +46 -6
- package/src/autonomous-policy.mjs +10 -2
- package/src/autonomous-runtime.mjs +116 -16
- package/src/autonomous-supervisor.mjs +1 -1
- package/src/evidence-publisher-r42.mjs +76 -0
- package/src/mcp-server.mjs +1 -1
- package/src/provider-circuit.mjs +4 -4
- package/src/version.mjs +1 -1
- package/test/agent-eval-r42.test.mjs +39 -0
- package/test/agent-eval-runner-r42.test.mjs +61 -0
- package/test/agent-governance-r42.test.mjs +77 -0
- package/test/autonomous-r34-e2e.test.mjs +2 -1
- package/test/cli-entrypoint.test.mjs +1 -1
- package/test/core.test.mjs +1 -1
- package/test/evidence-publisher-r42.test.mjs +53 -0
- package/test/human-clarification-r42.test.mjs +53 -0
- package/test/provider-responses-r42.test.mjs +90 -0
- package/test/r33-closeout-regression.test.mjs +27 -12
- package/test/r33-finalization.test.mjs +3 -3
- package/test/r42-finalization.test.mjs +38 -0
- package/test/release-version.test.mjs +13 -13
- package/test/task-contract-r42.test.mjs +41 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify as cryptoVerify } from 'node:crypto';
|
|
2
|
+
import { lstat, readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { canonicalJson } from '../src/canonical-json.mjs';
|
|
7
|
+
|
|
8
|
+
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
9
|
+
const stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
|
|
10
|
+
const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
const sha1=bytes=>createHash('sha1').update(bytes).digest('hex');
|
|
12
|
+
const insist=(ok,message)=>{if(!ok)throw new Error(message);};
|
|
13
|
+
const CLOSURE_DOMAIN=Buffer.from('DEADBYTE-R41-FINAL-CLOSURE-V1\0','utf8');
|
|
14
|
+
|
|
15
|
+
function parseManifest(bytes,label){
|
|
16
|
+
const entries=new Map();
|
|
17
|
+
for(const [index,line] of bytes.toString('utf8').split(/\r?\n/).entries()){
|
|
18
|
+
if(!line)continue;
|
|
19
|
+
const match=/^([0-9a-f]{64}) ([^\0]+)$/.exec(line);insist(match,`${label} line ${index+1} malformed`);
|
|
20
|
+
const rel=match[2];
|
|
21
|
+
insist(!path.posix.isAbsolute(rel)&&!rel.split('/').includes('..')&&!entries.has(rel),`unsafe/duplicate ${label} path ${rel}`);
|
|
22
|
+
entries.set(rel,match[1]);
|
|
23
|
+
}
|
|
24
|
+
return entries;
|
|
25
|
+
}
|
|
26
|
+
async function walk(dir,rel=''){
|
|
27
|
+
const out=[];
|
|
28
|
+
for(const name of (await readdir(path.join(dir,...(rel?rel.split('/'):[])))).sort((a,b)=>a.localeCompare(b,'en'))){
|
|
29
|
+
const child=rel?`${rel}/${name}`:name;
|
|
30
|
+
const full=path.join(dir,...child.split('/'));
|
|
31
|
+
const st=await lstat(full);
|
|
32
|
+
insist(!st.isSymbolicLink(),`parent tree symlink forbidden: ${child}`);
|
|
33
|
+
if(st.isDirectory())out.push(...await walk(dir,child));
|
|
34
|
+
else{insist(st.isFile(),`parent tree non-file forbidden: ${child}`);out.push(child);}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
async function verifyTree(dir,manifestName,expectedManifest,{exact}){
|
|
39
|
+
const bytes=await readFile(path.join(dir,manifestName));
|
|
40
|
+
insist(sha256(bytes)===expectedManifest,`${manifestName} digest mismatch: ${dir}`);
|
|
41
|
+
const entries=parseManifest(bytes,manifestName);
|
|
42
|
+
if(exact){
|
|
43
|
+
const actual=(await walk(dir)).filter(rel=>rel!==manifestName);
|
|
44
|
+
insist(actual.length===entries.size&&actual.every(rel=>entries.has(rel)),`${manifestName} exact tree mismatch: ${dir}`);
|
|
45
|
+
}
|
|
46
|
+
for(const [rel,digest] of entries){
|
|
47
|
+
insist(sha256(await readFile(path.join(dir,...rel.split('/'))))===digest,`${manifestName} file hash mismatch: ${dir} :: ${rel}`);
|
|
48
|
+
}
|
|
49
|
+
return entries.size;
|
|
50
|
+
}
|
|
51
|
+
function verifySignedClosure(envelope,record,publicPem){
|
|
52
|
+
insist(envelope?.body?.schema==='deadbyte.r41-final-closure.v1','R41 parent closure schema mismatch');
|
|
53
|
+
insist(envelope.body.version==='0.13.0','R41 parent closure version mismatch');
|
|
54
|
+
const pointer=envelope.body.active_pointer;
|
|
55
|
+
insist(pointer?.release_id===record.parent_release_id&&pointer?.manifest_sha256===record.parent_manifest_sha256,'R41 parent closure release identity mismatch');
|
|
56
|
+
insist(pointer?.generation_manifest_sha256===record.parent_generation_manifest_sha256,'R41 parent closure generation identity mismatch');
|
|
57
|
+
const bodyBytes=Buffer.from(canonicalJson(envelope.body),'utf8');
|
|
58
|
+
insist(sha256(bodyBytes)===envelope.body_sha256,'R41 parent closure body hash mismatch');
|
|
59
|
+
const {closure_sha256,...unsigned}=envelope;
|
|
60
|
+
insist(sha256(Buffer.from(canonicalJson(unsigned),'utf8'))===closure_sha256,'R41 parent closure envelope hash mismatch');
|
|
61
|
+
const key=createPublicKey(publicPem);insist(key.asymmetricKeyType==='ed25519','R41 parent closure key must be Ed25519');
|
|
62
|
+
const der=Buffer.from(key.export({type:'spki',format:'der'}));
|
|
63
|
+
const keyId=sha256(der);
|
|
64
|
+
insist(envelope.attestation?.algorithm==='ed25519'&&envelope.attestation?.domain==='DEADBYTE-R41-FINAL-CLOSURE-V1','R41 parent closure attestation contract mismatch');
|
|
65
|
+
insist(envelope.attestation.key_id===keyId,'R41 parent closure signer mismatch');
|
|
66
|
+
insist(cryptoVerify(null,Buffer.concat([CLOSURE_DOMAIN,bodyBytes]),key,Buffer.from(envelope.attestation.signature_base64,'base64')),'R41 parent closure signature invalid');
|
|
67
|
+
return pointer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function verifyR42Parent(file=path.join(root,'R42-PARENT.json')){
|
|
71
|
+
const record=JSON.parse(await readFile(path.resolve(file),'utf8'));
|
|
72
|
+
insist(record.schema==='deadbyte.r42-parent.v1','R42 parent schema mismatch');
|
|
73
|
+
insist(record.parent_version==='0.13.0'&&record.parent_release_id==='v0.13.0-3255be10de4daf08','R42 parent identity mismatch');
|
|
74
|
+
insist(record.parent_manifest_sha256==='3255be10de4daf0837d67d1ab1913b785b8b8215bc7405823c2ff46968c4b98f','R42 parent manifest mismatch');
|
|
75
|
+
insist(record.parent_generation_manifest_sha256==='143d2d98b675b4ed68cce2bfb1cda76ec22633035b49746e445140fee58b25cd','R42 parent generation mismatch');
|
|
76
|
+
const [sourceFiles,releaseFiles]=await Promise.all([
|
|
77
|
+
verifyTree(path.resolve(record.parent_source_root),'MANIFEST.SHA256',record.parent_manifest_sha256,{exact:false}),
|
|
78
|
+
verifyTree(path.resolve(record.parent_release_root),'MANIFEST.SHA256',record.parent_manifest_sha256,{exact:true})
|
|
79
|
+
]);
|
|
80
|
+
insist(sourceFiles===record.parent_manifest_file_count&&releaseFiles===record.parent_manifest_file_count,'R42 parent file count mismatch');
|
|
81
|
+
|
|
82
|
+
const closureBytes=await readFile(path.resolve(record.parent_final_closure_path));
|
|
83
|
+
insist(sha256(closureBytes)===record.parent_final_closure_sha256,'R41 parent final closure raw hash mismatch');
|
|
84
|
+
const closure=JSON.parse(closureBytes.toString('utf8').replace(/^\uFEFF/,''));
|
|
85
|
+
const pointer=verifySignedClosure(closure,record,await readFile(path.join(stateRoot,'trust','ed25519-public.pem')));
|
|
86
|
+
const generationFiles=await verifyTree(path.resolve(pointer.generation_root),'GENERATION.SHA256',record.parent_generation_manifest_sha256,{exact:true});
|
|
87
|
+
|
|
88
|
+
const tarball=await readFile(path.resolve(record.parent_public_tarball_path));
|
|
89
|
+
insist(sha256(tarball)===record.parent_public_tarball_sha256,'R41 parent public tarball SHA-256 mismatch');
|
|
90
|
+
insist(sha1(tarball)===record.parent_public_tarball_sha1,'R41 parent public tarball SHA-1 mismatch');
|
|
91
|
+
|
|
92
|
+
return {status:'passed',parent_release_id:record.parent_release_id,parent_manifest_sha256:record.parent_manifest_sha256,
|
|
93
|
+
parent_generation_manifest_sha256:record.parent_generation_manifest_sha256,source_files:sourceFiles,release_files:releaseFiles,
|
|
94
|
+
generation_files:generationFiles,parent_final_closure_sha256:record.parent_final_closure_sha256,
|
|
95
|
+
parent_public_tarball_sha256:record.parent_public_tarball_sha256,parent_public_tarball_sha1:record.parent_public_tarball_sha1};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
|
|
99
|
+
verifyR42Parent(process.argv[2]).then(value=>console.log(JSON.stringify(value,null,2)))
|
|
100
|
+
.catch(error=>{console.error(error instanceof Error?error.stack:String(error));process.exitCode=1;});
|
|
101
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { canonicalJson } from '../src/canonical-json.mjs';
|
|
7
|
+
import { readAndVerifyR42EvalEvidence } from '../src/agent-eval-runner-r42.mjs';
|
|
8
|
+
|
|
9
|
+
const here=path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const root=path.resolve(here,'..');
|
|
11
|
+
const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
12
|
+
const marker='DEADBYTE V0.14.0 / R42 WINDOWS BATCH GATE: PASS';
|
|
13
|
+
function insist(ok,message){if(!ok) throw new Error(message);}
|
|
14
|
+
|
|
15
|
+
export async function writeWindowsGateEvidence({
|
|
16
|
+
candidateRoot=root,
|
|
17
|
+
stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp')),
|
|
18
|
+
clock=()=>new Date()
|
|
19
|
+
}={}){
|
|
20
|
+
const candidate=path.resolve(candidateRoot);
|
|
21
|
+
const manifestBytes=await readFile(path.join(candidate,'MANIFEST.SHA256'));
|
|
22
|
+
const manifestSha256=sha256(manifestBytes);
|
|
23
|
+
const pkg=JSON.parse(await readFile(path.join(candidate,'package.json'),'utf8'));
|
|
24
|
+
insist(pkg.version==='0.14.0',`Windows gate evidence requires V0.14.0, found ${pkg.version}`);
|
|
25
|
+
|
|
26
|
+
const suite=JSON.parse(await readFile(path.join(candidate,'harness','r42-agent-evals.json'),'utf8'));
|
|
27
|
+
const suiteSha256=sha256(Buffer.from(canonicalJson(suite),'utf8'));
|
|
28
|
+
const evalId=`r42-${manifestSha256.slice(0,16)}-${suiteSha256.slice(0,16)}`;
|
|
29
|
+
const evalPath=path.join(path.resolve(stateRoot),'evidence','release','r42-agent-evals','agent_eval',`${evalId}.json`);
|
|
30
|
+
const evalVerified=await readAndVerifyR42EvalEvidence(evalPath,{subjectManifestSha256:manifestSha256,suiteSha256});
|
|
31
|
+
|
|
32
|
+
const completed=clock();
|
|
33
|
+
insist(completed instanceof Date&&!Number.isNaN(completed.getTime()),'Windows gate evidence clock invalid');
|
|
34
|
+
const evidence={
|
|
35
|
+
schema:'deadbyte.r42-windows-gate.v1',status:'passed',exit_code:0,version:'0.14.0',
|
|
36
|
+
marker,completed_at_utc:completed.toISOString(),manifest_sha256:manifestSha256,
|
|
37
|
+
model_eval:{path:evalPath,evidence_sha256:evalVerified.evidence_sha256,payload_sha256:evalVerified.payload_sha256,
|
|
38
|
+
suite_sha256:suiteSha256,case_count:evalVerified.payload.case_count}
|
|
39
|
+
};
|
|
40
|
+
const bytes=Buffer.from(`${JSON.stringify(evidence,null,2)}\n`,'utf8');
|
|
41
|
+
const dir=path.join(path.resolve(stateRoot),'evidence','release');
|
|
42
|
+
await mkdir(dir,{recursive:true});
|
|
43
|
+
const file=path.join(dir,`r42-windows-gate-${manifestSha256.slice(0,16)}.json`);
|
|
44
|
+
try{
|
|
45
|
+
const handle=await open(file,'wx',0o600);
|
|
46
|
+
try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}
|
|
47
|
+
return {status:'written',path:file,sha256:sha256(bytes),manifest_sha256:manifestSha256,model_eval:evidence.model_eval};
|
|
48
|
+
}catch(error){
|
|
49
|
+
if(error?.code!=='EEXIST') throw error;
|
|
50
|
+
const existingBytes=await readFile(file);
|
|
51
|
+
const existing=JSON.parse(existingBytes.toString('utf8').replace(/^\uFEFF/,''));
|
|
52
|
+
insist(existing.schema===evidence.schema&&existing.status==='passed'&&existing.exit_code===0,'existing R42 Windows gate evidence invalid');
|
|
53
|
+
insist(existing.version==='0.14.0'&&existing.marker===marker&&existing.manifest_sha256===manifestSha256,'existing R42 Windows gate identity mismatch');
|
|
54
|
+
insist(existing.model_eval?.evidence_sha256===evidence.model_eval.evidence_sha256&&existing.model_eval?.suite_sha256===suiteSha256,'existing R42 Windows gate model-eval mismatch');
|
|
55
|
+
return {status:'existing',path:file,sha256:sha256(existingBytes),manifest_sha256:manifestSha256,model_eval:existing.model_eval};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const invoked=process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url);
|
|
60
|
+
if(invoked){
|
|
61
|
+
writeWindowsGateEvidence()
|
|
62
|
+
.then(result=>console.log(JSON.stringify(result)))
|
|
63
|
+
.catch(error=>{console.error(error instanceof Error?error.stack:String(error));process.exitCode=1;});
|
|
64
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
3
|
+
|
|
4
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
5
|
+
const ID=/^[A-Za-z0-9._-]{1,96}$/;
|
|
6
|
+
const SEVERITY=new Set(['info','low','medium','high','critical']);
|
|
7
|
+
const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
|
|
8
|
+
function object(value,label){if(!value||typeof value!=='object'||Array.isArray(value))throw new Error(`${label} invalid`);return value;}
|
|
9
|
+
function keys(value,allowed,label){const extra=Object.keys(value).filter(key=>!allowed.includes(key));if(extra.length)throw new Error(`${label} unknown field '${extra[0]}'`);}
|
|
10
|
+
function id(value,label){if(typeof value!=='string'||!ID.test(value))throw new Error(`${label} invalid`);return value;}
|
|
11
|
+
function text(value,label,max=8192){if(typeof value!=='string'||value.length<1||value.length>max||value.includes('\0'))throw new Error(`${label} invalid`);return value;}
|
|
12
|
+
function textList(value,label,maxItems=32,maxLength=1024){if(!Array.isArray(value)||value.length>maxItems||value.some(item=>typeof item!=='string'||item.length<1||item.length>maxLength))throw new Error(`${label} invalid`);return [...value];}
|
|
13
|
+
|
|
14
|
+
function normalizeCase(raw){
|
|
15
|
+
const value=object(raw,'agent eval case');keys(value,['schema','case_id','task','allowed_context','hidden_oracle','acceptance'],'agent eval case');
|
|
16
|
+
if(value.schema!=='deadbyte.agent-eval-case.v1')throw new Error('agent eval case schema invalid');
|
|
17
|
+
const core={schema:value.schema,case_id:id(value.case_id,'eval case_id'),task:text(value.task,'eval task'),
|
|
18
|
+
allowed_context:structuredClone(object(value.allowed_context,'eval allowed_context')),
|
|
19
|
+
hidden_oracle:structuredClone(object(value.hidden_oracle,'eval hidden_oracle')),
|
|
20
|
+
acceptance:textList(value.acceptance,'eval acceptance')};
|
|
21
|
+
return Object.freeze({...core,case_sha256:sha(core)});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeJudgeResult(raw){
|
|
25
|
+
const value=object(raw,'judge result');keys(value,['schema','verdict','advisory','findings','evidence_refs'],'judge result');
|
|
26
|
+
if(value.schema!==undefined&&value.schema!=='deadbyte.agent-eval-judge-result.v1')throw new Error('judge result schema invalid');
|
|
27
|
+
if(!['pass','fail'].includes(value.verdict))throw new Error('judge verdict invalid');
|
|
28
|
+
if(value.advisory!==undefined&&value.advisory!==true)throw new Error('judge result must remain advisory');
|
|
29
|
+
if(!Array.isArray(value.findings)||value.findings.length>64)throw new Error('judge findings invalid');
|
|
30
|
+
const findings=value.findings.map(finding=>{
|
|
31
|
+
object(finding,'judge finding');keys(finding,['code','severity','evidence'],'judge finding');
|
|
32
|
+
if(!SEVERITY.has(finding.severity))throw new Error('judge finding severity invalid');
|
|
33
|
+
return {code:id(finding.code,'judge finding code'),severity:finding.severity,evidence:text(finding.evidence,'judge finding evidence',2048)};
|
|
34
|
+
});
|
|
35
|
+
if(!Array.isArray(value.evidence_refs)||value.evidence_refs.length<1||value.evidence_refs.length>64||value.evidence_refs.some(ref=>!HEX64.test(ref)))throw new Error('judge evidence refs invalid');
|
|
36
|
+
return Object.freeze({schema:'deadbyte.agent-eval-judge-result.v1',verdict:value.verdict,advisory:true,findings,evidence_refs:[...value.evidence_refs]});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runIsolatedEvaluation({caseSpec,generator,judge}){
|
|
40
|
+
const normalized=normalizeCase(caseSpec);
|
|
41
|
+
if(typeof generator!=='function'||typeof judge!=='function')throw new Error('eval generator and judge required');
|
|
42
|
+
const generatorInput=Object.freeze({schema:'deadbyte.agent-eval-generator-input.v1',case_id:normalized.case_id,
|
|
43
|
+
task:normalized.task,allowed_context:structuredClone(normalized.allowed_context),acceptance:[...normalized.acceptance]});
|
|
44
|
+
const generated=object(await generator(generatorInput),'generator result');
|
|
45
|
+
keys(generated,['artifact','trace'],'generator result');
|
|
46
|
+
const artifact=structuredClone(object(generated.artifact,'generator artifact'));
|
|
47
|
+
const candidateSha=sha(artifact);
|
|
48
|
+
const judgeInput=Object.freeze({schema:'deadbyte.agent-eval-judge-input.v1',case_id:normalized.case_id,
|
|
49
|
+
task:normalized.task,acceptance:[...normalized.acceptance],candidate:artifact,candidate_sha256:candidateSha,
|
|
50
|
+
hidden_oracle:structuredClone(normalized.hidden_oracle)});
|
|
51
|
+
const judged=normalizeJudgeResult(await judge(judgeInput));
|
|
52
|
+
const core={schema:'deadbyte.agent-eval-result.v1',case_id:normalized.case_id,case_sha256:normalized.case_sha256,
|
|
53
|
+
candidate_sha256:candidateSha,verdict:judged.verdict,advisory:true,findings:judged.findings,evidence_refs:judged.evidence_refs};
|
|
54
|
+
return Object.freeze({...core,result_sha256:sha(core)});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildFreshCriticContext(raw){
|
|
58
|
+
const value=object(raw,'fresh critic context');keys(value,['case_id','artifact','evidence','criteria'],'fresh critic context');
|
|
59
|
+
id(value.case_id,'critic case_id');object(value.artifact,'critic artifact');
|
|
60
|
+
if(!Array.isArray(value.evidence)||value.evidence.length>128)throw new Error('critic evidence invalid');
|
|
61
|
+
return Object.freeze({schema:'deadbyte.fresh-critic-context.v1',case_id:value.case_id,
|
|
62
|
+
artifact:structuredClone(value.artifact),evidence:structuredClone(value.evidence),
|
|
63
|
+
criteria:textList(value.criteria,'critic criteria',64,1024)});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function triageJudgeResult(raw,{critical_action,noncritical_action}){
|
|
67
|
+
const judge=normalizeJudgeResult(raw);
|
|
68
|
+
if(judge.advisory!==true)throw new Error('judge result must remain advisory');
|
|
69
|
+
if(!['pause','repair','continue'].includes(critical_action)||!['pause','repair','continue'].includes(noncritical_action))throw new Error('judge triage action invalid');
|
|
70
|
+
const critical=judge.findings.some(finding=>finding.severity==='critical'||finding.severity==='high');
|
|
71
|
+
return Object.freeze({action:judge.verdict==='pass'?'continue':(critical?critical_action:noncritical_action),
|
|
72
|
+
reason_codes:judge.findings.map(finding=>finding.code),judge_advisory:true});
|
|
73
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
5
|
+
import { runIsolatedEvaluation } from './agent-eval-r42.mjs';
|
|
6
|
+
import { publishEvidenceAtomic } from './evidence-publisher-r42.mjs';
|
|
7
|
+
|
|
8
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
9
|
+
const ID=/^[A-Za-z0-9._-]{1,96}$/;
|
|
10
|
+
const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
|
|
11
|
+
const shaText=value=>createHash('sha256').update(Buffer.from(String(value),'utf8')).digest('hex');
|
|
12
|
+
|
|
13
|
+
function object(value,label){if(!value||typeof value!=='object'||Array.isArray(value))throw new Error(`${label} invalid`);return value;}
|
|
14
|
+
function keys(value,allowed,label){const extra=Object.keys(value).filter(key=>!allowed.includes(key));if(extra.length)throw new Error(`${label} unknown field '${extra[0]}'`);}
|
|
15
|
+
function parseJson(raw,label){
|
|
16
|
+
if(typeof raw!=='string'||raw.length<2||raw.length>1024*1024)throw new Error(`${label} response invalid`);
|
|
17
|
+
let value;try{value=JSON.parse(raw);}catch{throw new Error(`${label} response must be strict JSON`);}
|
|
18
|
+
return object(value,label);
|
|
19
|
+
}
|
|
20
|
+
function providerMeta(raw){
|
|
21
|
+
const value=object(raw,'model response');
|
|
22
|
+
const meta={
|
|
23
|
+
backend:typeof value.backend==='string'?value.backend:'injected',
|
|
24
|
+
model:typeof value.model==='string'?value.model.slice(0,128):'injected',
|
|
25
|
+
response_id:typeof value.response_id==='string'?value.response_id.slice(0,160):null,
|
|
26
|
+
raw_response_sha256:HEX64.test(value.raw_response_sha256??'')?value.raw_response_sha256:shaText(value.raw_text??''),
|
|
27
|
+
usage:value.usage&&Number.isInteger(value.usage.input_tokens)&&Number.isInteger(value.usage.output_tokens)&&Number.isInteger(value.usage.total_tokens)
|
|
28
|
+
?{input_tokens:value.usage.input_tokens,output_tokens:value.usage.output_tokens,total_tokens:value.usage.total_tokens}:null
|
|
29
|
+
};
|
|
30
|
+
return Object.freeze(meta);
|
|
31
|
+
}
|
|
32
|
+
function normalizeSuite(raw){
|
|
33
|
+
const value=object(raw,'eval suite');keys(value,['schema','suite_id','cases'],'eval suite');
|
|
34
|
+
if(value.schema!=='deadbyte.agent-eval-suite.v1')throw new Error('eval suite schema invalid');
|
|
35
|
+
if(typeof value.suite_id!=='string'||!ID.test(value.suite_id))throw new Error('eval suite_id invalid');
|
|
36
|
+
if(!Array.isArray(value.cases)||value.cases.length<1||value.cases.length>32)throw new Error('eval suite cases invalid');
|
|
37
|
+
const ids=new Set();
|
|
38
|
+
for(const item of value.cases){
|
|
39
|
+
object(item,'eval suite case');
|
|
40
|
+
if(item.schema!=='deadbyte.agent-eval-case.v1'||typeof item.case_id!=='string'||!ID.test(item.case_id)||ids.has(item.case_id))throw new Error('eval suite case identity invalid');
|
|
41
|
+
ids.add(item.case_id);
|
|
42
|
+
}
|
|
43
|
+
const copy=structuredClone(value);
|
|
44
|
+
return Object.freeze({...copy,suite_sha256:sha(copy)});
|
|
45
|
+
}
|
|
46
|
+
function generatorPrompt(input){
|
|
47
|
+
return [
|
|
48
|
+
'You are the implementation-side evaluator for DEADBYTE R42.',
|
|
49
|
+
'Return exactly one JSON object with shape {"artifact":{...}} and no Markdown.',
|
|
50
|
+
'Do not invent authority, credentials, or evidence. Prefer bounded, typed, fail-closed behavior.',
|
|
51
|
+
`INPUT=${canonicalJson(input)}`
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
function judgePrompt(input){
|
|
55
|
+
return [
|
|
56
|
+
'You are an independent advisory judge for DEADBYTE R42.',
|
|
57
|
+
'Return exactly one JSON object with schema deadbyte.agent-eval-judge-result.v1.',
|
|
58
|
+
'Shape: {"schema":"deadbyte.agent-eval-judge-result.v1","verdict":"pass|fail","advisory":true,"findings":[{"code":"ID","severity":"info|low|medium|high|critical","evidence":"text"}],"evidence_refs":["64hex"]}.',
|
|
59
|
+
'Judge only the supplied candidate against acceptance and hidden oracle. Do not assume implementer reasoning.',
|
|
60
|
+
'Use candidate_sha256 as at least one evidence_refs entry.',
|
|
61
|
+
`INPUT=${canonicalJson(input)}`
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function verifyR42EvalPayload(payload,{subjectManifestSha256=null,suiteSha256=null}={}){
|
|
66
|
+
const value=object(payload,'R42 eval payload');
|
|
67
|
+
keys(value,['schema','status','subject_manifest_sha256','suite_id','suite_sha256','case_count','cases','completed_at_utc'],'R42 eval payload');
|
|
68
|
+
if(value.schema!=='deadbyte.r42-model-eval.v1'||value.status!=='passed')throw new Error('R42 eval payload is not passed');
|
|
69
|
+
if(!HEX64.test(value.subject_manifest_sha256??''))throw new Error('R42 eval manifest invalid');
|
|
70
|
+
if(subjectManifestSha256!==null&&value.subject_manifest_sha256!==subjectManifestSha256)throw new Error('R42 eval manifest mismatch');
|
|
71
|
+
if(!HEX64.test(value.suite_sha256??''))throw new Error('R42 eval suite hash invalid');
|
|
72
|
+
if(suiteSha256!==null&&value.suite_sha256!==suiteSha256)throw new Error('R42 eval suite hash mismatch');
|
|
73
|
+
if(typeof value.suite_id!=='string'||!ID.test(value.suite_id))throw new Error('R42 eval suite id invalid');
|
|
74
|
+
if(!Number.isInteger(value.case_count)||value.case_count<1||!Array.isArray(value.cases)||value.cases.length!==value.case_count)throw new Error('R42 eval cases invalid');
|
|
75
|
+
const seen=new Set();
|
|
76
|
+
for(const item of value.cases){
|
|
77
|
+
object(item,'R42 eval case result');keys(item,['case_id','result','generator','judge'],'R42 eval case result');
|
|
78
|
+
if(typeof item.case_id!=='string'||!ID.test(item.case_id)||seen.has(item.case_id))throw new Error('R42 eval case result identity invalid');
|
|
79
|
+
seen.add(item.case_id);
|
|
80
|
+
if(item.result?.schema!=='deadbyte.agent-eval-result.v1'||item.result?.verdict!=='pass'||item.result?.advisory!==true||!HEX64.test(item.result?.result_sha256??''))throw new Error(`R42 eval case did not pass: ${item.case_id}`);
|
|
81
|
+
for(const role of ['generator','judge']){
|
|
82
|
+
if(!HEX64.test(item[role]?.raw_response_sha256??''))throw new Error(`R42 eval ${role} provenance invalid`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if(typeof value.completed_at_utc!=='string'||!Number.isFinite(Date.parse(value.completed_at_utc)))throw new Error('R42 eval completion time invalid');
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function runAgentEvalSuiteR42({suite,subjectManifestSha256,requestModel,evidenceRoot=null,clock=()=>new Date()}){
|
|
90
|
+
const normalized=normalizeSuite(suite);
|
|
91
|
+
if(!HEX64.test(subjectManifestSha256??''))throw new Error('R42 eval subject manifest invalid');
|
|
92
|
+
if(typeof requestModel!=='function')throw new Error('R42 eval requestModel required');
|
|
93
|
+
const rows=[];
|
|
94
|
+
for(const caseSpec of normalized.cases){
|
|
95
|
+
let generatorMeta=null,judgeMeta=null;
|
|
96
|
+
const result=await runIsolatedEvaluation({
|
|
97
|
+
caseSpec,
|
|
98
|
+
generator:async input=>{
|
|
99
|
+
const response=object(await requestModel({role:'generator',case_id:caseSpec.case_id,prompt:generatorPrompt(input)}),'generator model response');
|
|
100
|
+
generatorMeta=providerMeta(response);
|
|
101
|
+
const parsed=parseJson(response.raw_text,'generator');
|
|
102
|
+
keys(parsed,['artifact'],'generator response');
|
|
103
|
+
return {artifact:object(parsed.artifact,'generator artifact')};
|
|
104
|
+
},
|
|
105
|
+
judge:async input=>{
|
|
106
|
+
const response=object(await requestModel({role:'judge',case_id:caseSpec.case_id,prompt:judgePrompt(input)}),'judge model response');
|
|
107
|
+
judgeMeta=providerMeta(response);
|
|
108
|
+
return parseJson(response.raw_text,'judge');
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
rows.push({case_id:caseSpec.case_id,result,generator:generatorMeta,judge:judgeMeta});
|
|
112
|
+
}
|
|
113
|
+
if(rows.some(row=>row.result.verdict!=='pass'))throw new Error('R42 model-backed eval suite failed');
|
|
114
|
+
const completed=clock();if(!(completed instanceof Date)||Number.isNaN(completed.getTime()))throw new Error('R42 eval clock invalid');
|
|
115
|
+
const payload={
|
|
116
|
+
schema:'deadbyte.r42-model-eval.v1',status:'passed',subject_manifest_sha256:subjectManifestSha256,
|
|
117
|
+
suite_id:normalized.suite_id,suite_sha256:normalized.suite_sha256,case_count:rows.length,cases:rows,
|
|
118
|
+
completed_at_utc:completed.toISOString()
|
|
119
|
+
};
|
|
120
|
+
verifyR42EvalPayload(payload,{subjectManifestSha256,suiteSha256:normalized.suite_sha256});
|
|
121
|
+
let publication=null;
|
|
122
|
+
if(evidenceRoot!==null){
|
|
123
|
+
if(typeof evidenceRoot!=='string'||!path.isAbsolute(evidenceRoot))throw new Error('R42 eval evidence root must be absolute');
|
|
124
|
+
const evidenceId=`r42-${subjectManifestSha256.slice(0,16)}-${normalized.suite_sha256.slice(0,16)}`;
|
|
125
|
+
publication=await publishEvidenceAtomic({root:evidenceRoot,kind:'agent_eval',evidenceId,payload});
|
|
126
|
+
}
|
|
127
|
+
return Object.freeze({payload,publication});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function readAndVerifyR42EvalEvidence(file,{subjectManifestSha256,suiteSha256}={}){
|
|
131
|
+
const envelope=JSON.parse(await readFile(path.resolve(file),'utf8'));
|
|
132
|
+
if(envelope?.schema!=='deadbyte.evidence-publication.v1'||envelope?.kind!=='agent_eval')throw new Error('R42 eval evidence envelope invalid');
|
|
133
|
+
if(!HEX64.test(envelope.payload_sha256??'')||sha(envelope.payload)!==envelope.payload_sha256)throw new Error('R42 eval payload hash mismatch');
|
|
134
|
+
const {evidence_sha256,...body}=envelope;
|
|
135
|
+
if(!HEX64.test(evidence_sha256??'')||sha(body)!==evidence_sha256)throw new Error('R42 eval evidence hash mismatch');
|
|
136
|
+
verifyR42EvalPayload(envelope.payload,{subjectManifestSha256,suiteSha256});
|
|
137
|
+
return Object.freeze({status:'passed',evidence_sha256,payload_sha256:envelope.payload_sha256,payload:envelope.payload});
|
|
138
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
3
|
+
|
|
4
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
5
|
+
const ID=/^[A-Za-z0-9._-]{1,64}$/;
|
|
6
|
+
const RIGOR=Object.freeze({lightweight:0,structured:1,agentic:2});
|
|
7
|
+
const RISK_MINIMUM=Object.freeze({low:'lightweight',medium:'structured',high:'agentic'});
|
|
8
|
+
const EVIDENCE=Object.freeze({assumption:1,source:2,traced_path:3,automated:4,runtime:5});
|
|
9
|
+
|
|
10
|
+
const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
|
|
11
|
+
function object(value,label){if(!value||typeof value!=='object'||Array.isArray(value)) throw new Error(`${label} invalid`);return value;}
|
|
12
|
+
function keys(value,allowed,label){const extra=Object.keys(value).filter(key=>!allowed.includes(key));if(extra.length) throw new Error(`${label} unknown field '${extra[0]}'`);}
|
|
13
|
+
function id(value,label){if(typeof value!=='string'||!ID.test(value)) throw new Error(`${label} invalid`);return value;}
|
|
14
|
+
function hex(value,label){if(typeof value!=='string'||!HEX64.test(value)) throw new Error(`${label} invalid`);return value;}
|
|
15
|
+
function text(value,label,max=2048){if(typeof value!=='string'||value.length<1||value.length>max||value.includes('\0')) throw new Error(`${label} invalid`);return value;}
|
|
16
|
+
function idList(value,label,max=128){if(!Array.isArray(value)||value.length>max||value.some(item=>typeof item!=='string'||!ID.test(item))||new Set(value).size!==value.length) throw new Error(`${label} invalid`);return [...value];}
|
|
17
|
+
|
|
18
|
+
export function bindApprovalSubject(raw){
|
|
19
|
+
const value=object(raw,'approval subject');
|
|
20
|
+
keys(value,['goal_id','reviewed_event_seq','reviewed_event_sha256','journal_head_sha256','scope','subject'],'approval subject');
|
|
21
|
+
hex(value.goal_id,'approval goal_id');
|
|
22
|
+
if(!Number.isInteger(value.reviewed_event_seq)||value.reviewed_event_seq<1) throw new Error('approval reviewed_event_seq invalid');
|
|
23
|
+
hex(value.reviewed_event_sha256,'approval reviewed_event_sha256');
|
|
24
|
+
hex(value.journal_head_sha256,'approval journal_head_sha256');
|
|
25
|
+
id(value.scope,'approval scope'); object(value.subject,'approval canonical subject');
|
|
26
|
+
const normalized=structuredClone(value);
|
|
27
|
+
return Object.freeze({...normalized,approval_subject_sha256:sha({schema:'deadbyte.approval-subject.v1',...normalized})});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateBoundApproval({approval,subject,now=new Date()}){
|
|
31
|
+
const {approval_subject_sha256:claimedSubjectSha,...subjectBody}=object(subject,'approval subject');
|
|
32
|
+
const bound=bindApprovalSubject(subjectBody);
|
|
33
|
+
if(claimedSubjectSha!==undefined&&claimedSubjectSha!==bound.approval_subject_sha256) throw new Error('approval subject hash mismatch');
|
|
34
|
+
const value=object(approval,'bound approval');
|
|
35
|
+
keys(value,['schema','approval_subject_sha256','reviewed_event_seq','reviewed_event_sha256','journal_head_sha256','scope','issued_at_utc','expires_at_utc','provenance'],'bound approval');
|
|
36
|
+
if(value.schema!=='deadbyte.bound-approval.v1') throw new Error('bound approval schema invalid');
|
|
37
|
+
if(value.approval_subject_sha256!==bound.approval_subject_sha256) throw new Error('bound approval subject mismatch');
|
|
38
|
+
if(value.reviewed_event_seq!==bound.reviewed_event_seq||value.reviewed_event_sha256!==bound.reviewed_event_sha256) throw new Error('bound approval reviewed event mismatch');
|
|
39
|
+
if(value.journal_head_sha256!==bound.journal_head_sha256) throw new Error('bound approval journal head mismatch');
|
|
40
|
+
if(value.scope!==bound.scope) throw new Error('bound approval scope mismatch');
|
|
41
|
+
if(value.provenance!=='operator_mcp') throw new Error('bound approval provenance invalid');
|
|
42
|
+
const issued=Date.parse(value.issued_at_utc),expires=Date.parse(value.expires_at_utc),observed=now.getTime();
|
|
43
|
+
if(!Number.isFinite(issued)||!Number.isFinite(expires)||expires<=issued||expires-issued>3_600_000) throw new Error('bound approval lifetime invalid');
|
|
44
|
+
if(observed<issued||observed>=expires) throw new Error('bound approval expired or not yet valid');
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createTaskDag(raw){
|
|
49
|
+
const value=object(raw,'task DAG'); keys(value,['run_id','goal_id','tasks'],'task DAG');
|
|
50
|
+
id(value.run_id,'task DAG run_id'); hex(value.goal_id,'task DAG goal_id');
|
|
51
|
+
if(!Array.isArray(value.tasks)||value.tasks.length<1||value.tasks.length>256) throw new Error('task DAG tasks invalid');
|
|
52
|
+
const tasks=value.tasks.map(item=>{
|
|
53
|
+
object(item,'task DAG task');keys(item,['task_id','depends_on','input_artifacts','output_artifacts','state'],'task DAG task');
|
|
54
|
+
return {task_id:id(item.task_id,'task_id'),depends_on:idList(item.depends_on,'depends_on'),
|
|
55
|
+
input_artifacts:idList(item.input_artifacts,'input_artifacts'),output_artifacts:idList(item.output_artifacts,'output_artifacts'),
|
|
56
|
+
state:item.state??'PENDING'};
|
|
57
|
+
});
|
|
58
|
+
const ids=new Set(tasks.map(task=>task.task_id)); if(ids.size!==tasks.length) throw new Error('task DAG duplicate task_id');
|
|
59
|
+
for(const task of tasks) if(task.depends_on.some(dep=>!ids.has(dep))) throw new Error('task DAG dependency missing');
|
|
60
|
+
if(tasks.some(task=>!['PENDING','RUNNING','COMPLETE','FAILED','STALE'].includes(task.state))) throw new Error('task DAG state invalid');
|
|
61
|
+
const visiting=new Set(),visited=new Set(),byId=new Map(tasks.map(task=>[task.task_id,task]));
|
|
62
|
+
const visit=taskId=>{if(visiting.has(taskId)) throw new Error('task DAG cycle detected');if(visited.has(taskId))return;visiting.add(taskId);for(const dep of byId.get(taskId).depends_on)visit(dep);visiting.delete(taskId);visited.add(taskId);};
|
|
63
|
+
for(const task of tasks) visit(task.task_id);
|
|
64
|
+
const core={schema:'deadbyte.task-dag.v1',run_id:value.run_id,goal_id:value.goal_id,tasks};
|
|
65
|
+
return Object.freeze({...core,dag_sha256:sha(core)});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function invalidateTaskDag(dag,{changed_artifact_ids}){
|
|
69
|
+
const changed=new Set(idList(changed_artifact_ids,'changed_artifact_ids'));
|
|
70
|
+
const stale=new Set(dag.tasks.filter(task=>task.input_artifacts.some(artifact=>changed.has(artifact))).map(task=>task.task_id));
|
|
71
|
+
let grew=true;while(grew){grew=false;for(const task of dag.tasks){if(!stale.has(task.task_id)&&task.depends_on.some(dep=>stale.has(dep))){stale.add(task.task_id);grew=true;}}}
|
|
72
|
+
return createTaskDag({run_id:dag.run_id,goal_id:dag.goal_id,tasks:dag.tasks.map(task=>({...task,state:stale.has(task.task_id)?'STALE':task.state}))});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function taskDagFromPlan({run_id,goal_id,plan}){
|
|
76
|
+
object(plan,'task DAG plan');
|
|
77
|
+
if(!Array.isArray(plan.steps)||plan.steps.length<1) throw new Error('task DAG plan steps invalid');
|
|
78
|
+
const tasks=plan.steps.map((step,index)=>{
|
|
79
|
+
const previous=index===0?null:plan.steps[index-1].step_id;
|
|
80
|
+
return {task_id:step.step_id,depends_on:previous?[previous]:[],
|
|
81
|
+
input_artifacts:previous?[`${previous}.result`]:[],output_artifacts:[`${step.step_id}.result`],
|
|
82
|
+
state:step.status==='completed'?'COMPLETE':step.status==='active'?'RUNNING':'PENDING'};
|
|
83
|
+
});
|
|
84
|
+
return createTaskDag({run_id,goal_id,tasks});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createEvidenceClaim(raw){
|
|
88
|
+
const value=object(raw,'evidence claim');keys(value,['claim_id','statement','level','refs'],'evidence claim');
|
|
89
|
+
id(value.claim_id,'claim_id');text(value.statement,'claim statement',2048);
|
|
90
|
+
if(!Object.hasOwn(EVIDENCE,value.level)) throw new Error('evidence level invalid');
|
|
91
|
+
if(!Array.isArray(value.refs)||value.refs.length>64||value.refs.some(ref=>!HEX64.test(ref))) throw new Error('evidence reference invalid');
|
|
92
|
+
if(value.level!=='assumption'&&value.refs.length<1) throw new Error('evidence reference required');
|
|
93
|
+
const core={schema:'deadbyte.evidence-claim.v1',claim_id:value.claim_id,statement:value.statement,level:value.level,refs:[...value.refs]};
|
|
94
|
+
return Object.freeze({...core,claim_sha256:sha(core)});
|
|
95
|
+
}
|
|
96
|
+
export function evidenceSatisfies(claim,minimum){if(!Object.hasOwn(EVIDENCE,minimum)) throw new Error('minimum evidence level invalid');return EVIDENCE[claim.level]>=EVIDENCE[minimum];}
|
|
97
|
+
|
|
98
|
+
export function deriveEffectiveRigor({project_default,risk_level,explicit_override=null}){
|
|
99
|
+
if(!Object.hasOwn(RIGOR,project_default)) throw new Error('project default rigor invalid');
|
|
100
|
+
if(!Object.hasOwn(RISK_MINIMUM,risk_level)) throw new Error('risk level invalid');
|
|
101
|
+
if(explicit_override!==null&&!Object.hasOwn(RIGOR,explicit_override)) throw new Error('explicit rigor override invalid');
|
|
102
|
+
const risk_minimum=RISK_MINIMUM[risk_level];
|
|
103
|
+
const values=[project_default,risk_minimum,...(explicit_override?[explicit_override]:[])];
|
|
104
|
+
const effective_rigor=values.reduce((best,item)=>RIGOR[item]>RIGOR[best]?item:best,'lightweight');
|
|
105
|
+
return Object.freeze({project_default,risk_minimum,explicit_override,effective_rigor});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createDecisionRecord(raw){
|
|
109
|
+
const value=object(raw,'decision record');keys(value,['decision_id','decision','reason','evidence_refs','invalidates_if'],'decision record');
|
|
110
|
+
id(value.decision_id,'decision_id');text(value.decision,'decision',2048);text(value.reason,'decision reason',2048);
|
|
111
|
+
if(!Array.isArray(value.evidence_refs)||value.evidence_refs.length<1||value.evidence_refs.length>64||value.evidence_refs.some(ref=>!HEX64.test(ref))) throw new Error('decision evidence refs invalid');
|
|
112
|
+
if(!Array.isArray(value.invalidates_if)||value.invalidates_if.length>32) throw new Error('decision invalidation rules invalid');
|
|
113
|
+
const rules=value.invalidates_if.map(rule=>{
|
|
114
|
+
object(rule,'decision invalidation rule');
|
|
115
|
+
if(rule.kind==='artifact_sha_changed'){
|
|
116
|
+
keys(rule,['kind','artifact_id','expected_sha256'],'artifact invalidation rule');
|
|
117
|
+
return {kind:rule.kind,artifact_id:id(rule.artifact_id,'artifact_id'),expected_sha256:hex(rule.expected_sha256,'expected artifact sha256')};
|
|
118
|
+
}
|
|
119
|
+
if(rule.kind==='policy_sha_changed'){
|
|
120
|
+
keys(rule,['kind','expected_sha256'],'policy invalidation rule');
|
|
121
|
+
return {kind:rule.kind,expected_sha256:hex(rule.expected_sha256,'expected policy sha256')};
|
|
122
|
+
}
|
|
123
|
+
throw new Error('decision invalidation rule kind invalid');
|
|
124
|
+
});
|
|
125
|
+
const core={schema:'deadbyte.decision-record.v1',decision_id:value.decision_id,decision:value.decision,reason:value.reason,
|
|
126
|
+
evidence_refs:[...value.evidence_refs],invalidates_if:rules};
|
|
127
|
+
return Object.freeze({...core,decision_sha256:sha(core)});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function evaluateDecisionFreshness(record,observed){
|
|
131
|
+
object(record,'decision record');object(observed,'decision observed state');
|
|
132
|
+
const invalidated_by=[];
|
|
133
|
+
for(const rule of record.invalidates_if){
|
|
134
|
+
if(rule.kind==='artifact_sha_changed'){
|
|
135
|
+
const actual=observed.artifact_sha256?.[rule.artifact_id]??null;
|
|
136
|
+
if(actual!==rule.expected_sha256) invalidated_by.push({...rule,observed_sha256:actual});
|
|
137
|
+
}else if(rule.kind==='policy_sha_changed'&&observed.policy_sha256!==rule.expected_sha256){
|
|
138
|
+
invalidated_by.push({...rule,observed_sha256:observed.policy_sha256??null});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return Object.freeze({decision_id:record.decision_id,state:invalidated_by.length?'STALE':'CURRENT',invalidated_by});
|
|
142
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canonicalJson } from './canonical-json.mjs';
|
|
3
3
|
import { provenanceFor, dedupeEvidence } from './context-provenance.mjs';
|
|
4
|
+
import { taskDagFromPlan } from './agent-governance-r42.mjs';
|
|
4
5
|
|
|
5
6
|
const SECRET_KEY = /(?:^|[_-])(token|password|secret|authorization|cookie|credential|credentials|api[_-]?key|private[_-]?key|recovery)(?:$|[_-])/i;
|
|
6
7
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
@@ -252,6 +253,7 @@ export function buildLoopContext({ goal, policy, loopState = {}, tree = [], cont
|
|
|
252
253
|
desktop_memory:desktopMemory===null?null:structuredClone(desktopMemory),
|
|
253
254
|
long_term_memory:longTermMemory===null?null:structuredClone(longTermMemory),
|
|
254
255
|
capabilities:capabilities===null?null:structuredClone(capabilities),
|
|
256
|
+
task_dag:loopState.plan&&typeof goal?.goal_id==='string' ? taskDagFromPlan({run_id:goal.goal_id,goal_id:goal.goal_id,plan:loopState.plan}) : null,
|
|
255
257
|
profiles
|
|
256
258
|
};
|
|
257
259
|
return compactLoopContext(state,policy.limits.max_context_bytes);
|
|
@@ -146,6 +146,7 @@ export function deriveLoopState(goal,events) {
|
|
|
146
146
|
replan_count:0,
|
|
147
147
|
pending_action:null,
|
|
148
148
|
pending_approval:false,
|
|
149
|
+
clarification:null,
|
|
149
150
|
progress:null,
|
|
150
151
|
critic:null,
|
|
151
152
|
repair:null,
|
|
@@ -223,6 +224,10 @@ export function deriveLoopState(goal,events) {
|
|
|
223
224
|
state.phase='WAITING';
|
|
224
225
|
state.wait_reason=requireText(payload.reason,'loop wait reason');
|
|
225
226
|
state.pending_approval=payload.requires_approval===true;
|
|
227
|
+
state.clarification=payload.reason==='human_input_required' ? {
|
|
228
|
+
question:requireText(payload.question,'loop clarification question',1000),
|
|
229
|
+
choices:Array.isArray(payload.choices) ? payload.choices.map((choice,index)=>requireText(choice,`loop clarification choice ${index+1}`,256)) : []
|
|
230
|
+
} : null;
|
|
226
231
|
break;
|
|
227
232
|
case 'waiting_resumed': {
|
|
228
233
|
if (state.phase !== 'WAITING') throw new Error('loop waiting_resumed outside WAITING phase');
|
|
@@ -231,6 +236,7 @@ export function deriveLoopState(goal,events) {
|
|
|
231
236
|
state.phase=resumed;
|
|
232
237
|
state.wait_reason=null;
|
|
233
238
|
state.pending_approval=false;
|
|
239
|
+
state.clarification=null;
|
|
234
240
|
break;
|
|
235
241
|
}
|
|
236
242
|
case 'planner_requested':
|
|
@@ -255,24 +261,28 @@ export function deriveLoopState(goal,events) {
|
|
|
255
261
|
state.phase='PLAN';
|
|
256
262
|
state.wait_reason=null;
|
|
257
263
|
state.pending_approval=false;
|
|
264
|
+
state.clarification=null;
|
|
258
265
|
}
|
|
259
266
|
break;
|
|
260
267
|
case 'goal_cancelled':
|
|
261
268
|
state.phase='FAILED';
|
|
262
269
|
state.wait_reason=typeof payload.reason==='string'&&payload.reason.length>0?payload.reason:'cancelled';
|
|
263
270
|
state.pending_approval=false;
|
|
271
|
+
state.clarification=null;
|
|
264
272
|
state.pending_action=null;
|
|
265
273
|
break;
|
|
266
274
|
case 'goal_failed':
|
|
267
275
|
state.phase='FAILED';
|
|
268
276
|
state.wait_reason=typeof payload.reason==='string'&&payload.reason.length>0?payload.reason:'failed';
|
|
269
277
|
state.pending_approval=false;
|
|
278
|
+
state.clarification=null;
|
|
270
279
|
state.pending_action=null;
|
|
271
280
|
break;
|
|
272
281
|
case 'release_completed':
|
|
273
282
|
state.phase='SUCCEEDED';
|
|
274
283
|
state.wait_reason=null;
|
|
275
284
|
state.pending_approval=false;
|
|
285
|
+
state.clarification=null;
|
|
276
286
|
state.pending_action=null;
|
|
277
287
|
break;
|
|
278
288
|
default:
|