coding-artifact-evaluation-shell 0.0.0-stage → 0.1.0-eidolon.a1b37cbcd7c07

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.
Files changed (76) hide show
  1. package/AGENTS.md +15 -0
  2. package/README.md +56 -2
  3. package/SKILL.md +30 -0
  4. package/SOP.md +157 -0
  5. package/adapter/benchmark.ts +28 -0
  6. package/app/app.ts +15 -0
  7. package/app/host.ts +30 -0
  8. package/budget.ts +49 -0
  9. package/bun.lock +1045 -0
  10. package/calibrate.ts +81 -0
  11. package/calibration-history.ts +33 -0
  12. package/calibration-recheck.ts +74 -0
  13. package/calibration-report.ts +36 -0
  14. package/campaign.ts +85 -0
  15. package/capsule.ts +162 -0
  16. package/cli.ts +23 -0
  17. package/coding-scope.ts +13 -0
  18. package/contract.ts +116 -0
  19. package/counter-review.ts +74 -0
  20. package/coverage.ts +74 -0
  21. package/environment.ts +11 -0
  22. package/feedback.ts +31 -0
  23. package/history.ts +11 -0
  24. package/host.xnl +7 -0
  25. package/index.ts +6 -0
  26. package/logic.ts +135 -0
  27. package/manifest.xnl +10 -0
  28. package/native-references.ts +21 -0
  29. package/needs.ts +33 -0
  30. package/obligation-graph.ts +47 -0
  31. package/package.json +32 -4
  32. package/proposal.ts +94 -0
  33. package/protocol-error.ts +1 -0
  34. package/replay.ts +39 -0
  35. package/representation.ts +24 -0
  36. package/resources/counter-review.md +16 -0
  37. package/resources/evaluate.md +34 -0
  38. package/resources/iterate.md +20 -0
  39. package/resources/review.md +23 -0
  40. package/resources/submit.md +18 -0
  41. package/support/agent-runtime.ts +520 -0
  42. package/support/artifact-inventory.ts +20 -0
  43. package/support/artifact-links.ts +82 -0
  44. package/support/browser-channel.ts +209 -0
  45. package/support/browser-client.ts +33 -0
  46. package/support/browser-owner.ts +69 -0
  47. package/support/browser-worker.ts +61 -0
  48. package/support/campaign-ledger.ts +66 -0
  49. package/support/code-knowledge.ts +73 -0
  50. package/support/counter-review.ts +35 -0
  51. package/support/ego-tools.ts +19 -0
  52. package/support/evidence-catalog.ts +37 -0
  53. package/support/evidence-input.ts +70 -0
  54. package/support/evidence-object.ts +9 -0
  55. package/support/evidence-read.ts +18 -0
  56. package/support/execution-profile.ts +65 -0
  57. package/support/guidance.ts +16 -0
  58. package/support/history.ts +89 -0
  59. package/support/http-request.ts +21 -0
  60. package/support/isolation.ts +94 -0
  61. package/support/judge.ts +60 -0
  62. package/support/model-turn.ts +94 -0
  63. package/support/observation-agent.ts +279 -0
  64. package/support/observation-protocol.ts +33 -0
  65. package/support/observers.ts +102 -0
  66. package/support/process.ts +22 -0
  67. package/support/proposal-check.ts +82 -0
  68. package/support/retention.ts +93 -0
  69. package/support/runtime-binding.ts +39 -0
  70. package/support/runtime-preflight.ts +16 -0
  71. package/support/service-evidence.ts +23 -0
  72. package/support/service-group.ts +45 -0
  73. package/support/service.ts +84 -0
  74. package/support/source-identity.ts +22 -0
  75. package/support/target-group-view.ts +11 -0
  76. package/target-groups.ts +57 -0
@@ -0,0 +1,209 @@
1
+ /** A single owner for an acceptance Page. No application-specific test logic. */
2
+ export interface Dialog { type: string; message: string; defaultPrompt?: string; url?: string }
3
+ export interface BrowserRequest { id: string; op: string; args: unknown[] }
4
+ export interface BrowserResponse {
5
+ id: string; sequence: number; status: 'ok' | 'dialog-open' | 'error';
6
+ result?: { value?: unknown; snapshot?: string; observationId?: string; url?: string; dialog?: Dialog | null };
7
+ error?: string;
8
+ failure?: {kind: 'invalid-request' | 'precondition-rejected' | 'predicate-unmet' | 'execution-uncertain' | 'control-lost' | 'provider-error'; recoverable: boolean; mayHaveLateEffects: boolean};
9
+ }
10
+ export interface BrowserPort {
11
+ invoke(method: string, args: unknown[]): Promise<unknown>;
12
+ events(): Promise<unknown[]>;
13
+ control(): Promise<boolean>;
14
+ url(): Promise<string>;
15
+ }
16
+ export interface ChannelRuntime {
17
+ page: BrowserPort; origin: string; timeoutMs: number;
18
+ /** AI-selected actions require native snapshot refs; caller-declared probes remain compatible. */
19
+ groundedTargets?:boolean;
20
+ pause(ms: number): Promise<void>;
21
+ armDeadline(ms: number, callback: () => void): () => void;
22
+ }
23
+ export interface ChannelState {
24
+ sequence: number; dialog: Dialog | null; stopped: string | null;
25
+ queue: Promise<void>; requests: Map<string, { digest: string; response: Promise<BrowserResponse> }>;
26
+ snapshotRefs:Set<string>;
27
+ }
28
+ export const channelState = (): ChannelState => ({ sequence: 0, dialog: null, stopped: null, queue: Promise.resolve(), requests: new Map(),snapshotRefs:new Set() });
29
+ const arities: Record<string, [number, number]> = {
30
+ goto: [1,1], snapshot: [0,1], click: [1,2], fill: [2,2], selectOption: [2,2], press: [2,2],
31
+ acceptDialog: [0,1], dismissDialog: [0,0], info: [0,0], waitForSelector: [1,2],
32
+ };
33
+ const selectorOperations = new Set(['click','fill','selectOption','press','waitForSelector']);
34
+ export const BROWSER_SELECTOR_GUIDANCE = 'Snapshot annotations are display text, not selector syntax. A current [ref=N] annotation must be addressed as "@N" or "ref=N", never "[ref=N]"; do not copy enclosing brackets or a combined ref/loc annotation. Alternatively use a current loc=css:, loc=role:, loc=href:, text=, xpath= or raw CSS selector. Quote CSS attribute values. Derive targets from the current snapshot; stale references require a new snapshot. Invalid requests dispatch no input: inspect the error, explicitly correct the selector and submit a NEW request ID, never automatically replay a mutation.';
35
+ export const GROUNDED_BROWSER_GUIDANCE='For this AI collector, click/fill/selectOption/press accept ONLY @N or ref=N from the latest native snapshot; guessed CSS/text targets are refused BEFORE dispatch. Start with snapshot. Navigation waits for DOMContentLoaded, not all passive resources or business readiness. Successful mutations return a fresh snapshot with the receipt in the same native response (unless a dialog blocks it); read that complete object before choosing next targets. A stale-ref refusal requires a new snapshot/new request ID. waitForSelector may use a goal-derived read-only predicate; unknown action timeouts still fence, never replay.';
36
+ const targetMutations=new Set(['click','fill','selectOption','press']);
37
+ function retainSnapshot(state:ChannelState,snapshot:string):void {
38
+ state.snapshotRefs=new Set([...snapshot.matchAll(/\[ref=(\d+)(?:\s*,[^\]]*)?\]/g)].map(m=>m[1]!));
39
+ }
40
+ function validateSelector(selector:string):void {
41
+ // Display annotations are not SDK selectors. Refuse before any provider IO,
42
+ // rather than guessing a target or trusting a generic SyntaxError afterward.
43
+ if (/^(?:(?:loc=)?css[:=])?\[\s*ref\s*=\s*\d+(?:\s*,\s*loc=[\s\S]*)?\s*\]$/.test(selector)) {
44
+ throw new TypeError('Invalid selector: snapshot display notation is not a selector. Use the current @N or ref=N without brackets, or a valid loc= selector. No browser input was dispatched; correct with a NEW request ID.');
45
+ }
46
+ }
47
+ function validSelection(value: unknown): boolean {
48
+ if (typeof value === 'string') return true;
49
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
50
+ const option = value as Record<string, unknown>;
51
+ const keys = Object.keys(option);
52
+ return keys.length > 0 && keys.every(key =>
53
+ key === 'index' ? Number.isSafeInteger(option[key]) && Number(option[key]) >= 0
54
+ : ['value', 'label'].includes(key) && typeof option[key] === 'string');
55
+ }
56
+ const quotedOption = String.raw`"(?:\\.|[^"\\\n])*"`;
57
+ const availableOption = String.raw`\d+: value=${quotedOption}, label=${quotedOption}`;
58
+ const missingSingleOption = new RegExp(String.raw`^ElementResolutionError: page\.selectOption timed out after \d+ms: page\.selectOption failed: option ${quotedOption} was not found; available options: ${availableOption}(?:; ${availableOption})*$`);
59
+ /** Known native resolution refusal, not a timeout or a generic exception class.
60
+ * Match the complete provider diagnostic and the requested selector. An explicit
61
+ * late-effect report/after-dispatch stage still takes precedence in classification.
62
+ */
63
+ function ambiguousClick(diagnostic: string, args?: readonly unknown[]): boolean {
64
+ const match = /^ElementResolutionError: Locator (.+) matched ([1-9]\d*) elements \((\d+) visible, (\d+) hidden\)\. Candidates: (.+)\. Use a current snapshot ref or a more specific role, text, or CSS selector\.$/.exec(diagnostic);
65
+ if (!match || match[1] !== args?.[0]) return false;
66
+ const count = Number(match[2]);
67
+ return count >= 2 && Number(match[3]) + Number(match[4]) === count && match[5]!.startsWith('1. ');
68
+ }
69
+ export function validateRequest(value: unknown, origin: string): BrowserRequest {
70
+ const v = value as BrowserRequest;
71
+ if (!v || typeof v.id !== 'string' || !/^[\w-]{1,100}$/.test(v.id) || !Object.hasOwn(arities,v.op) || !Array.isArray(v.args)) throw Error('Invalid browser command');
72
+ const [min,max] = arities[v.op]!;
73
+ if(v.args.length < min || v.args.length > max) throw Error('Invalid browser arguments');
74
+ if (['goto','click','fill','selectOption','press','waitForSelector'].includes(v.op) && typeof v.args[0] !== 'string') throw Error('Selector/URL must be a string');
75
+ if (selectorOperations.has(v.op)) validateSelector(v.args[0] as string);
76
+ if (['fill','press'].includes(v.op) && typeof v.args[1] !== 'string') throw Error('Text must be a string');
77
+ if (v.op === 'selectOption') {
78
+ const selection = v.args[1];
79
+ if (selection !== null && !(Array.isArray(selection) ? selection.every(validSelection) : validSelection(selection))) throw new TypeError('Selection must be a value/label string, option descriptor, array or null');
80
+ }
81
+ if(v.op === 'acceptDialog' && v.args.length && typeof v.args[0] !== 'string') throw Error('Prompt text must be a string');
82
+ let optionsIndex: number | null = null;
83
+ if (v.op === 'snapshot') optionsIndex = 0;
84
+ if (v.op === 'click' || v.op === 'waitForSelector') optionsIndex = 1;
85
+ if (optionsIndex !== null && v.args.length > optionsIndex) {
86
+ const options = v.args[optionsIndex];
87
+ if (options === null || typeof options !== 'object' || Array.isArray(options)) throw new TypeError(`${v.op} options must be an object`);
88
+ }
89
+ if(v.op === 'goto' && new URL(v.args[0] as string).origin !== origin) throw Error('Navigation outside leased origin');
90
+ if(v.op === 'snapshot') v.args = [{ scope: 'full_page', ...(v.args[0] as object ?? {}) }];
91
+ if(v.op === 'waitForSelector') {
92
+ const options = {...(v.args[1] as {timeout?:number} ?? {})};
93
+ if(options.timeout !== undefined && (!Number.isFinite(options.timeout) || options.timeout < 50 || options.timeout > 8000)) throw new TypeError('waitForSelector timeout must be 50..8000ms');
94
+ v.args[1] = {...options,timeout:options.timeout ?? 8000};
95
+ }
96
+ return v;
97
+ }
98
+ function applyEvents(state: ChannelState, events: unknown[]) {
99
+ for(const e of events as {method?:string;params?:Dialog}[]) {
100
+ if(e.method === 'Page.javascriptDialogClosed') state.dialog = null;
101
+ if(e.method === 'Page.javascriptDialogOpening' && e.params) state.dialog = e.params;
102
+ }
103
+ }
104
+ export function classifyBrowserFailure(error: unknown, op: string, stage = 'invoke', args?: readonly unknown[]): NonNullable<BrowserResponse['failure']> {
105
+ const failure = error as {name?: string; timedOut?: boolean; mayHaveLateEffects?: boolean; dispatchPhase?: string; kind?: string} | null;
106
+ const result = (kind: NonNullable<BrowserResponse['failure']>['kind'], recoverable: boolean, mayHaveLateEffects = false) => ({kind,recoverable,mayHaveLateEffects});
107
+ if (stage === 'control') return result('control-lost',false);
108
+ if (failure?.mayHaveLateEffects === true) return result('execution-uncertain',false,true);
109
+ if (stage === 'validation') return result('invalid-request',true);
110
+ if (stage === 'after-dispatch') return result('execution-uncertain',false,true);
111
+ if (failure?.dispatchPhase === 'pre-dispatch') return result('precondition-rejected',true);
112
+ if (op === 'waitForSelector' && failure?.kind === 'predicate-unmet') return result('predicate-unmet',true);
113
+ const diagnostic = String(error);
114
+ // Only the provider's exact read-only predicate diagnostic is recoverable.
115
+ if (op === 'waitForSelector' && /^Error: page\.waitForSelector timed out after \d+ms(?:$|:)/.test(diagnostic)) return result('predicate-unmet',true);
116
+ if (failure?.timedOut === true || /timeout/i.test(failure?.name ?? '')) return result('execution-uncertain',false,true);
117
+ // Exception class alone is not a dispatch fact. Only the known, complete SDK
118
+ // options rejection is safe; its usage word `timeout?` is not a deadline.
119
+ if (op === 'click' && diagnostic === 'TypeError: page.click options must be an object. Expected: await page.click(selector, { timeout?, label? })') return result('invalid-request',true);
120
+ // Real neutral-page conformance proves ambiguity is rejected before input.
121
+ // Unknown diagnostics (even ElementResolutionError) remain fail-closed.
122
+ if (op === 'click' && ambiguousClick(diagnostic,args)) return result('precondition-rejected',true);
123
+ // Ego's click timeout also bounds waiting for an element to become usable.
124
+ // This exact disabled-element rejection precedes native input dispatch.
125
+ if (/^ElementResolutionError: page\.click timed out after \d+ms: (?:page\.click failed:|Selector .+ matched \d+ elements, but none can receive input;) element is disabled$/.test(diagnostic)) return result('precondition-rejected',true);
126
+ // An exact native hit-test refusal precedes input dispatch. Do not generalize
127
+ // this to provider deadlines, appended diagnostics or late-effect reports.
128
+ const intercepted = /^ElementResolutionError: page\.(click|fill) timed out after \d+ms: (?:page\.\1 failed:|Selector [^\n]+ matched \d+ elements, but none can receive input;) <[^>\n]+> intercepts pointer events$/.exec(diagnostic);
129
+ if (intercepted?.[1] === op) return result('precondition-rejected',true);
130
+ // Calibrated only for one string selection. Array/descriptor failures may
131
+ // involve partial effects and are not generalized from this native refusal.
132
+ if (op === 'selectOption' && typeof args?.[1] === 'string' && missingSingleOption.test(diagnostic)) return result('precondition-rejected',true);
133
+ if (/^ElementResolutionError: Stale ref: @\d+; take a new snapshot$/.test(diagnostic)) return result('precondition-rejected',true);
134
+ // Confirmed by live calibration: a non-editable target is rejected before
135
+ // input dispatch. Keep this diagnostic exact; unrelated fill failures fence.
136
+ if (op === 'fill' && /^Error: page\.fill failed: element is not an input, textarea, or contenteditable element$/.test(diagnostic)) return result('precondition-rejected',true);
137
+ if (/\btimeout\b|\btimed out\b/i.test(diagnostic)) return result('execution-uncertain',false,true);
138
+ // Unknown mutation errors cannot establish that input was never dispatched.
139
+ if (['click','fill','selectOption','press','acceptDialog','dismissDialog','goto'].includes(op)) return result('execution-uncertain',false,true);
140
+ return result('provider-error',true);
141
+ }
142
+ async function perform(runtime: ChannelRuntime, state: ChannelState, request: BrowserRequest): Promise<BrowserResponse> {
143
+ const response: BrowserResponse = { id: request.id, sequence: ++state.sequence, status: 'ok' };
144
+ if(state.stopped) return {...response,status:'error',error:state.stopped,failure:{kind:'execution-uncertain',recoverable:false,mayHaveLateEffects:true}};
145
+ let stage = 'invoke';
146
+ try {
147
+ if(!await runtime.page.control()) { stage='control';state.stopped='User or inactive browser control'; throw Error(state.stopped); }
148
+ applyEvents(state,await runtime.page.events());
149
+ const dialogAction = ['acceptDialog','dismissDialog'].includes(request.op);
150
+ if(state.dialog && !dialogAction) return {...response,status:'dialog-open',result:{dialog:state.dialog}};
151
+ stage = 'validation';
152
+ if(runtime.groundedTargets&&targetMutations.has(request.op)){
153
+ const match=/^(?:@|ref=)(\d+)$/.exec(request.args[0] as string);
154
+ if(!match||!state.snapshotRefs.has(match[1]!))throw Error('Unobserved browser target; use a ref from the latest native snapshot. No browser input was dispatched; request a snapshot and use a NEW request ID.');
155
+ }
156
+ if(dialogAction && state.dialog?.type === 'prompt' && request.op === 'acceptDialog' && request.args.length === 0) throw Error('Prompt requires explicit text; default is not automatically preserved');
157
+ if(dialogAction && !state.dialog) throw Error('No observed dialog to answer');
158
+ stage = 'invoke';
159
+ const value = await runtime.page.invoke(request.op, request.args);
160
+ stage = 'after-dispatch';
161
+ if(dialogAction) state.dialog=null;
162
+ const receipt = value as {dialog?:Dialog} | null;
163
+ if(receipt?.dialog) state.dialog=receipt.dialog;
164
+ applyEvents(state,await runtime.page.events());
165
+ // The same live SDK listener captures follow-on prompt events before replying.
166
+ // No default answers or generated-application operations are supplied here.
167
+ if(dialogAction) {
168
+ for(let i=0;i<4 && !state.dialog;i++) { await runtime.pause(50); applyEvents(state,await runtime.page.events()); }
169
+ }
170
+ response.result = request.op === 'snapshot' ? {snapshot:String(value),observationId:request.id,dialog:state.dialog} : {value,dialog:state.dialog};
171
+ if(runtime.groundedTargets){
172
+ if(request.op==='snapshot')retainSnapshot(state,String(value));
173
+ else if(targetMutations.has(request.op)||['goto','acceptDialog','dismissDialog'].includes(request.op)){
174
+ state.snapshotRefs.clear();
175
+ if(!state.dialog){
176
+ const snapshot=String(await runtime.page.invoke('snapshot',[{scope:'full_page'}]));
177
+ response.result.snapshot=snapshot;retainSnapshot(state,snapshot);
178
+ }
179
+ }
180
+ }
181
+ if(!state.dialog) {
182
+ response.result.url=await runtime.page.url();
183
+ if(new URL(response.result.url).origin !== runtime.origin) {state.stopped='Page left leased origin';throw Error(state.stopped);}
184
+ }
185
+ if(state.dialog?.url && new URL(state.dialog.url).origin !== runtime.origin) { state.stopped='Dialog outside leased origin'; throw Error(state.stopped); }
186
+ return response;
187
+ } catch(error) {
188
+ const diagnostic=String(error);
189
+ const failure = classifyBrowserFailure(error,request.op,stage,request.args);
190
+ if(!failure.recoverable) state.stopped ??= `Native browser failure; outcome uncertain, session fenced: ${diagnostic}`;
191
+ return {...response,status:'error',error:state.stopped??diagnostic,failure};
192
+ }
193
+ }
194
+ /** Requests serialize even when agents yield shell commands and call concurrently. */
195
+ export function dispatchBrowser(runtime: ChannelRuntime, state: ChannelState, raw: unknown): Promise<BrowserResponse> {
196
+ let request: BrowserRequest;
197
+ try { request=validateRequest(raw,runtime.origin); } catch(error) { return Promise.resolve({id:'invalid',sequence:state.sequence,status:'error',error:String(error),failure:classifyBrowserFailure(error,'unknown','validation')}); }
198
+ const digest=JSON.stringify(request); const previous=state.requests.get(request.id);
199
+ if(previous) return previous.digest===digest ? previous.response : Promise.resolve({id:request.id,sequence:state.sequence,status:'error',error:'Request ID reused with different command'});
200
+ const response=state.queue.then(async()=> {
201
+ let cancelDeadline:()=>void;
202
+ const timeout=new Promise<BrowserResponse>(resolve=> {cancelDeadline=runtime.armDeadline(runtime.timeoutMs,()=> {
203
+ state.stopped='Native browser timeout; outcome uncertain, session fenced, no action replay';
204
+ resolve({id:request.id,sequence:state.sequence,status:'error',error:state.stopped,failure:{kind:'execution-uncertain',recoverable:false,mayHaveLateEffects:true}});
205
+ });});
206
+ const result=await Promise.race([perform(runtime,state,request),timeout]); cancelDeadline!(); return result;
207
+ });
208
+ state.requests.set(request.id,{digest,response}); state.queue=response.then(()=>{}); return response;
209
+ }
@@ -0,0 +1,33 @@
1
+ /** One transport command, not an application test. Args are authored by the AI. */
2
+ import { readFileSync } from 'node:fs';
3
+ import { randomUUID } from 'node:crypto';
4
+ import {readContent} from './evidence-object';
5
+ export {readContent} from './evidence-object';
6
+
7
+ /** Same authenticated transport for CLI and serial scratch scripts. No policy/retry. */
8
+ export async function observe(configPath:string,request:Record<string,unknown>):Promise<any>{
9
+ if(!request||typeof request!=='object'||Array.isArray(request))throw Error('Native request must be an object');
10
+ const config=JSON.parse(readFileSync(configPath,'utf8'));
11
+ const timeout=config.deadlineAt?Math.max(1,Math.min(190000,config.deadlineAt-Date.now())):35000;
12
+ const response=await fetch(config.endpoint+'/command',{method:'POST',headers:{authorization:`Bearer ${config.token}`,'content-type':'application/json'},body:JSON.stringify({...request,id:request.id??randomUUID()}),signal:AbortSignal.timeout(timeout)});
13
+ const value=await response.json() as Record<string,any>;
14
+ if(!value||!['ok','error'].includes(value.status))throw Error('Native transport returned an invalid response');
15
+ if(!response.ok&&value.status!=='error')throw Error(`Native transport refused: HTTP ${response.status}`);
16
+ // Small action results are useful immediately. Keep complete file/large
17
+ // objects at their absolute entries; never truncate, summarize or dispatch
18
+ // another request to obtain the same fact. Both entrances use this projection.
19
+ const observation=value.observation;
20
+ if(observation&&observation.channel!=='file'&&Number.isSafeInteger(observation.bytes)&&observation.bytes>=0&&observation.bytes<=16384){
21
+ return {...value,observation:{...observation,content:readContent(observation)}};
22
+ }
23
+ return value;
24
+ }
25
+
26
+ if(import.meta.main){
27
+ const [configPath,raw]=process.argv.slice(2);
28
+ if(!configPath||!raw)throw Error('Usage: bun browser-client.ts <connection.json> <JSON request>');
29
+ // Source quotations are data: @file avoids shell interpolation of quotes/backticks.
30
+ const request=JSON.parse(raw.startsWith('@')?readFileSync(raw.slice(1),'utf8'):raw);
31
+ const value=await observe(configPath,request);console.log(JSON.stringify(value,null,2));
32
+ process.exitCode=value.status==='error'?1:0;
33
+ }
@@ -0,0 +1,69 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { spawn } from 'node:child_process';
5
+ import type { AgentRun } from './agent-runtime';
6
+ import { resolveEgo } from './ego-tools';
7
+
8
+ export async function closeBrowserWorker(endpoint:string,adminToken:string):Promise<void>{
9
+ const response=await fetch(endpoint+'/close',{method:'POST',headers:{authorization:`Bearer ${adminToken}`},signal:AbortSignal.timeout(2000)});
10
+ if(!response.ok)throw Error(`Native browser cleanup failed: HTTP ${response.status}; ${await response.text()}`);
11
+ }
12
+
13
+ /** Harness Effect lifecycle. It owns the log; agents cannot write native evidence. */
14
+ export async function startBrowserChannel(run: AgentRun, origin: string, space: number, name: string, boundary: { sandbox(argv: string[]): string[]; deadlineAt?:number;groundedTargets?:boolean }) {
15
+ if (!Number.isSafeInteger(space) || space<=0 || !/^[a-zA-Z0-9-]+$/.test(name)) throw Error('Invalid browser ownership input');
16
+ const writeJson=(file:string,value:unknown)=>fs.writeFileSync(file,JSON.stringify(value)+'\n');
17
+ const worker=path.join(run.root,'bin/browser-worker.mjs');
18
+ const build=await Bun.build({entrypoints:[path.join(import.meta.dir,'browser-worker.ts')],target:'node',format:'esm'});
19
+ if(!build.success) throw Error(`Browser worker build failed: ${build.logs}`);
20
+ fs.writeFileSync(worker,await build.outputs[0]!.text());
21
+ const client=path.join(run.root,'bin/browser-client.ts');fs.copyFileSync(path.join(import.meta.dir,'browser-client.ts'),client);
22
+ const log=path.join(run.root,`logs/${name}-browser-channel.jsonl`),token=randomUUID(),adminToken=randomUUID();
23
+ fs.closeSync(fs.openSync(log,'wx',0o600));
24
+ const stderr=path.join(run.root,`logs/${name}-browser-process.log`),fd=fs.openSync(stderr,'wx',0o600);
25
+ const privateConfig=path.join(run.root,'bin',`browser-private-${name}.json`);
26
+ writeJson(privateConfig,{space,origin,token,adminToken,log,groundedTargets:boundary.groundedTargets??false});fs.chmodSync(privateConfig,0o600);
27
+ // Owner capabilities never appear in process argv, which is visible to tools.
28
+ const script=`const fs=await import("node:fs"); const config=JSON.parse(fs.readFileSync(${JSON.stringify(privateConfig)},"utf8")); const {startBrowserWorker}=await import(${JSON.stringify(worker)}); await startBrowserWorker(taskSpace,config);`;
29
+ const argv=boundary.sandbox([resolveEgo().executable,'nodejs','-e',script]);
30
+ // Only the harness worker may append this one native log. Reviewer sandbox
31
+ // keeps its deny-write rule; the model cannot forge channel evidence.
32
+ argv[2] += `(allow file-write* (literal ${JSON.stringify(log)}))`;
33
+ const child=spawn(argv[0]!,argv.slice(1),{cwd:run.workspace,env:run.env,detached:true,stdio:['ignore',fd,fd]});fs.closeSync(fd);
34
+ let spawnError:unknown;child.on('error',e=>{spawnError=e;});
35
+ const stop=()=> {try{process.kill(-child.pid!,'SIGKILL');}catch{/* terminal */}};
36
+ process.once('SIGTERM',stop);process.once('SIGINT',stop);
37
+ let endpoint:string|undefined;let connection:string|undefined;let heartbeat:ReturnType<typeof setInterval>|undefined;
38
+ let closing:Promise<void>|undefined;
39
+ const close=()=> closing??=(async()=> {
40
+ if(heartbeat)clearInterval(heartbeat);process.off('SIGTERM',stop);process.off('SIGINT',stop);
41
+ let closeError:unknown;
42
+ if(endpoint) {
43
+ try {await closeBrowserWorker(endpoint,adminToken);}catch(error){closeError=error;}
44
+ await Bun.sleep(100);
45
+ }
46
+ stop();
47
+ fs.rmSync(privateConfig,{force:true});
48
+ if(connection)fs.rmSync(connection,{force:true});
49
+ if(closeError)throw closeError;
50
+ })();
51
+ try {
52
+ const deadline=Math.min(Date.now()+15000,boundary.deadlineAt??Infinity);
53
+ while(Date.now()<deadline) {
54
+ for(const line of fs.readFileSync(log,'utf8').split('\n')) {try{endpoint=JSON.parse(line).channelReady??endpoint;}catch{/* native diagnostic */}}
55
+ if(endpoint)break;
56
+ if(spawnError || child.exitCode!==null)throw Error(`Browser worker exited: ${spawnError??fs.readFileSync(stderr,'utf8')}`);
57
+ await Bun.sleep(50);
58
+ }
59
+ if(!endpoint)throw Error(`Browser worker not ready: ${fs.readFileSync(stderr,'utf8')}`);
60
+ heartbeat=setInterval(()=>{void fetch(endpoint!+'/heartbeat',{method:'POST',headers:{authorization:`Bearer ${adminToken}`},signal:AbortSignal.timeout(2000)}).catch(()=>{});},15000);
61
+ connection=path.join(run.home,'tmp',`${name}-browser-connection.json`);writeJson(connection,{endpoint,token});
62
+ return {client,connection,log,close,async command(op:string,args:unknown[]=[],id=randomUUID(),phaseDeadlineAt=Infinity) {
63
+ const remaining=Math.min(boundary.deadlineAt??Infinity,phaseDeadlineAt)-Date.now();
64
+ if(remaining<=0)throw Error('Browser evaluation deadline exhausted; no dispatch');
65
+ const response=await fetch(endpoint+'/command',{method:'POST',headers:{authorization:`Bearer ${token}`},body:JSON.stringify({id,op,args}),signal:AbortSignal.timeout(Math.min(35000,Math.ceil(remaining)))});
66
+ return response.json() as Promise<import('./browser-channel').BrowserResponse>;
67
+ }};
68
+ }catch(error){await close();throw error;}
69
+ }
@@ -0,0 +1,61 @@
1
+ import * as http from 'node:http';
2
+ import { appendFileSync } from 'node:fs';
3
+ import { channelState, dispatchBrowser, type BrowserPort } from './browser-channel';
4
+
5
+ /** Lifecycle cancellation only; never answers a prompt or produces score evidence. */
6
+ export async function cancelOwnedDialog(page:Record<string,any>,control:()=>Promise<boolean>,origin:string):Promise<unknown>{
7
+ if(!await control())return {status:'control-lost',dismissed:false};
8
+ const info=await page.info(),dialog=info?.dialog;
9
+ if(!dialog)return {status:'no-dialog',dismissed:false};
10
+ if(!['alert','confirm','prompt','beforeunload'].includes(dialog.type)||typeof dialog.url!=='string'||new URL(dialog.url).origin!==origin)return {status:'outside-lease',dismissed:false};
11
+ if(!await control())return {status:'control-lost',dismissed:false};
12
+ await page.dismissDialog();
13
+ const after=await page.info();
14
+ if(after?.dialog)throw Error('Owned dialog cancellation did not quiesce; no default answer or repeated dismissal');
15
+ return {status:'cancelled',dismissed:true,dialogType:dialog.type,origin};
16
+ }
17
+
18
+ export async function startBrowserWorker(
19
+ connect: (space: number) => Promise<{ ownership: string; page(label: string): Record<string, any> }>,
20
+ config: {space:number;origin:string;token:string;adminToken:string;log:string;groundedTargets?:boolean},
21
+ ) {
22
+ const task=await connect(config.space); if(task.ownership!=='agent') throw Error('User or inactive browser control');
23
+ const page=task.page('p1');
24
+ const port: BrowserPort={
25
+ // Navigation establishes a committed DOM, not every passive resource's load.
26
+ // Keep the SDK deadline inside the owner fence so native diagnostics survive.
27
+ // A fresh snapshot still supplies actual state; never infer business readiness.
28
+ invoke: (method,args)=>method==='goto'
29
+ ?page.goto(args[0],{waitUntil:'domcontentloaded',timeout:10000})
30
+ :page[method](...args), events:()=>page.events(),
31
+ control: async()=> (await connect(config.space)).ownership === 'agent',
32
+ url:()=>page.url(),
33
+ };
34
+ const runtime={page:port,origin:config.origin,groundedTargets:config.groundedTargets,timeoutMs:15000,pause:(ms:number)=>new Promise<void>(r=>setTimeout(r,ms)),armDeadline:(ms:number,callback:()=>void)=>{const timer=setTimeout(callback,ms);return ()=>clearTimeout(timer);}};
35
+ const state=channelState();
36
+ let resolveClosed:()=>void;const closed=new Promise<void>(resolve=>{resolveClosed=resolve;});
37
+ let heartbeat=Date.now();
38
+ const shutdown=()=> {state.stopped='Browser channel closed by owner';clearInterval(watchdog);server.close(()=>resolveClosed());server.closeAllConnections();};
39
+ const watchdog=setInterval(()=>{if(Date.now()-heartbeat>90000)shutdown();},5000);
40
+ const server=http.createServer(async(req,res)=> {
41
+ if(req.method==='POST' && req.headers.authorization===`Bearer ${config.adminToken}` && ['/close','/heartbeat'].includes(req.url??'')) {
42
+ heartbeat=Date.now();
43
+ if(req.url==='/close'){
44
+ try{const cleanup=await cancelOwnedDialog(page,port.control,config.origin);appendFileSync(config.log,JSON.stringify({kind:'owner-dialog-cleanup',space:config.space,cleanup,semanticAuthority:false})+'\n');res.writeHead(200);res.end(JSON.stringify({cleanup}));}
45
+ catch(error){appendFileSync(config.log,JSON.stringify({kind:'owner-dialog-cleanup-failed',error:String(error),semanticAuthority:false})+'\n');res.writeHead(500);res.end(JSON.stringify({error:String(error)}));}
46
+ setImmediate(shutdown);return;
47
+ }
48
+ res.writeHead(200);res.end('{}');return;
49
+ }
50
+ if(req.method!=='POST' || req.url!=='/command' || req.headers.authorization!==`Bearer ${config.token}`) {res.writeHead(403);res.end();return;}
51
+ let body='';
52
+ try {
53
+ for await(const chunk of req) {body+=chunk; if(body.length>65536) throw Error('Request too large');}
54
+ const request=JSON.parse(body); const response=await dispatchBrowser(runtime,state,request);
55
+ appendFileSync(config.log,JSON.stringify({channel:'browser-channel-v1',space:config.space,origin:config.origin,request,response})+'\n');
56
+ res.writeHead(200,{'content-type':'application/json'});res.end(JSON.stringify(response));
57
+ }catch(error){res.writeHead(400);res.end(JSON.stringify({status:'error',error:String(error)}));}
58
+ });
59
+ server.listen(0,'127.0.0.1',()=>appendFileSync(config.log,JSON.stringify({channelReady:`http://127.0.0.1:${(server.address() as {port:number}).port}`,workerPid:process.pid})+'\n'));
60
+ await closed;
61
+ }
@@ -0,0 +1,66 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import type {Budget} from '../budget';
4
+ import {validateBudget} from '../budget';
5
+ import type {Invocation} from '../contract';
6
+
7
+ interface Reservation {
8
+ id:string; phase:Invocation['phase']; counter:number;
9
+ startedAt:number; deadlineMs:number; maxInvocations:number;
10
+ }
11
+ function readLines<T>(root:string,file:string):T[] {
12
+ const location=path.join(root,file);
13
+ if(!fs.existsSync(location))return [];
14
+ return fs.readFileSync(location,'utf8').split('\n').filter(Boolean).map(line=>JSON.parse(line) as T);
15
+ }
16
+
17
+ /** A reservation survives a controller crash. Unknown usage never refunds a call. */
18
+ export function campaignCalls(budget:Budget,roots:string[]):Invocation[] {
19
+ validateBudget(budget);
20
+ const calls:Invocation[]=[],seen=new Set<string>();
21
+ for(const root of roots) {
22
+ const reservations=readLines<Reservation>(root,'reservations.jsonl');
23
+ const finished=readLines<Invocation>(root,'invocations.jsonl');
24
+ const completed=new Map(finished.map(call=>[call.id,call]));
25
+ if(completed.size!==finished.length)throw Error('Duplicate native invocation');
26
+ for(const reservation of reservations) {
27
+ if(!reservation.id||!['preparation','judgment','representation-repair'].includes(reservation.phase)||reservation.startedAt!==budget.startedAt||reservation.deadlineMs!==budget.deadlineMs||reservation.maxInvocations!==budget.maxInvocations||reservation.counter!==calls.length+1||seen.has(reservation.id))throw Error('Native reservation identity/budget drift');
28
+ seen.add(reservation.id);
29
+ const call=completed.get(reservation.id);
30
+ if(call&&call.phase!==reservation.phase)throw Error('Invocation/reservation phase drift');
31
+ calls.push(call??{id:reservation.id,phase:reservation.phase,elapsedMs:0,usage:null,completed:false,model:'unknown',effort:null});
32
+ completed.delete(reservation.id);
33
+ }
34
+ // Earlier runs recorded final invocations but not reservations. Keep them,
35
+ // without inventing a reservation or recovering an unrecorded early launch.
36
+ if(reservations.length&&completed.size)throw Error('Invocation without native reservation');
37
+ for(const call of completed.values()) {
38
+ if(!call.id||seen.has(call.id))throw Error('Duplicate native invocation');
39
+ seen.add(call.id);calls.push(call);
40
+ }
41
+ }
42
+ if(calls.length<budget.invocations||calls.length>budget.maxInvocations)throw Error('Campaign budget differs from native ledger');
43
+ return calls;
44
+ }
45
+
46
+ export function recoverCampaignBudget(budget:Budget,roots:string[]):Invocation[] {
47
+ const calls=campaignCalls(budget,roots);
48
+ budget.invocations=calls.length; // Forward only, including incomplete reservations.
49
+ return calls;
50
+ }
51
+
52
+ export function assertCampaignRoot(output:string):void {
53
+ if(!path.isAbsolute(output)||path.resolve(output)!==output||!/^\/(private\/)?tmp\/depa-codument-eval-[^/]+$/.test(output))throw Error('New owned temporary campaign root required');
54
+ if(fs.existsSync(output)&&fs.lstatSync(output).isSymbolicLink())throw Error('Campaign root cannot be a symlink');
55
+ const parent=fs.realpathSync(path.dirname(output));
56
+ if(!['/private/tmp','/tmp'].includes(parent))throw Error('Campaign root escapes temporary ownership');
57
+ }
58
+
59
+ export function assertRunRoots(output:string,roots:string[]):void {
60
+ const canonical=fs.realpathSync(output);
61
+ if(new Set(roots).size!==roots.length)throw Error('Duplicate campaign run root');
62
+ for(const root of roots) {
63
+ if(path.dirname(path.resolve(root))!==path.resolve(output))throw Error('Run root escapes campaign');
64
+ if(fs.existsSync(root)&&(fs.lstatSync(root).isSymbolicLink()||path.dirname(fs.realpathSync(root))!==canonical))throw Error('Run root symlink escapes campaign');
65
+ }
66
+ }
@@ -0,0 +1,73 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import {digest} from './isolation';
4
+ import type {Manifest} from '../contract';
5
+
6
+ /** Caller-selected, optional navigation. No tool process, graph DB or new judge authority. */
7
+ export interface CodeEvidenceSelection {path:string;sourceRoot:string}
8
+ interface ByteIdentity {path:string;sha256:string;bytes:number}
9
+ export function sourceByteInventory(root:string):ByteIdentity[] {
10
+ const result:ByteIdentity[]=[];let bytes=0;
11
+ function visit(dir:string):void {
12
+ if(fs.lstatSync(dir).isSymbolicLink())throw Error('Code evidence cannot follow links');
13
+ for(const name of fs.readdirSync(dir)){
14
+ const file=path.join(dir,name),stat=fs.lstatSync(file);
15
+ if(stat.isSymbolicLink())throw Error('Code evidence cannot follow links');
16
+ if(stat.isDirectory()){visit(file);continue;}
17
+ if(!stat.isFile())throw Error('Code evidence requires regular files');
18
+ const relative=path.relative(root,file).split(path.sep).join('/');
19
+ if(/[\0\r\n\\]/.test(relative))throw Error('Unsupported snapshot path');
20
+ bytes+=stat.size;if(result.length>=10000||bytes>128*1024*1024)throw Error('Code snapshot exceeds inventory bound');
21
+ result.push({path:relative,sha256:digest(fs.readFileSync(file)),bytes:stat.size});
22
+ }
23
+ }
24
+ visit(root);return result.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
25
+ }
26
+ export function sourceByteDigest(files:ByteIdentity[]):string {
27
+ return digest(files.map(f=>`${f.path}\0${f.sha256}\0${f.bytes}\n`).join(''));
28
+ }
29
+ function owned(root:string,relative:string):string {
30
+ if(!relative||path.isAbsolute(relative)||relative.split(/[\\/]/).some(p=>p==='..'||p==='.'||p===''))throw Error('Code evidence path escapes artifact');
31
+ let current=root;
32
+ for(const part of relative.split('/')){
33
+ current=path.join(current,part);
34
+ if(fs.lstatSync(current).isSymbolicLink())throw Error('Code evidence cannot follow links');
35
+ }
36
+ return current;
37
+ }
38
+
39
+ /** Called after native artifact freezing; stale envelopes fail before model invocation.
40
+ * The ordinary file observer reads this envelope plus bounded candidate source files.
41
+ * Unsupported/unreadable sources remain in inventory; no observation is invented. */
42
+ export function attachCodeKnowledgeEvidence(manifest:Manifest,selection:CodeEvidenceSelection):Manifest {
43
+ const root=fs.realpathSync(manifest.artifact),source=owned(root,selection.sourceRoot),file=owned(root,selection.path);
44
+ if(file===source||file.startsWith(source+path.sep))throw Error('Evidence must be outside its source snapshot');
45
+ if(fs.statSync(file).size>1048576)throw Error('Code evidence exceeds file observation bound');
46
+ const evidence=JSON.parse(fs.readFileSync(file,'utf8'));
47
+ if(evidence.format!=='depa-code-navigation-v1')throw Error('Unknown code evidence format');
48
+ const inventory=sourceByteInventory(source),identity=sourceByteDigest(inventory);
49
+ if(evidence.snapshotDigest!==identity||JSON.stringify(evidence.inventory)!==JSON.stringify(inventory))throw Error('Stale or different code evidence snapshot');
50
+ if(!Array.isArray(evidence.coverage)||evidence.coverage.length!==inventory.length)throw Error('Incomplete code evidence coverage');
51
+ const seen=new Set<string>(),fileIds=new Map<string,string>();
52
+ for(const row of evidence.coverage){
53
+ const original=inventory.find(f=>f.path===row.path);
54
+ if(!original||seen.has(row.path)||original.sha256!==row.sha256||original.bytes!==row.bytes||!['document','text-or-regex-fallback','semantic-candidate','over-size-or-excluded','skipped','unsupported-or-excluded'].includes(row.status)||!Array.isArray(row.resolvers))throw Error('Invalid code evidence coverage');
55
+ seen.add(row.path);if(row.fileId)fileIds.set(row.fileId,row.path);
56
+ }
57
+ if(!Array.isArray(evidence.symbols)||!Array.isArray(evidence.edges)||evidence.symbols.length>200||evidence.edges.length>400||!evidence.parser||!evidence.settings||!Array.isArray(evidence.limitations)||!evidence.limitations.length)throw Error('Incomplete bounded code evidence');
58
+ for(const kind of ['symbols','edges'] as const){
59
+ const omitted=kind==='symbols'?'omittedSymbols':'omittedEdges';
60
+ if(!Number.isSafeInteger(evidence.totals?.[kind])||evidence.totals[kind]<evidence[kind].length||evidence.totals[omitted]!==evidence.totals[kind]-evidence[kind].length)throw Error('Invalid code evidence truncation');
61
+ }
62
+ for(const row of [...evidence.symbols,...evidence.edges])if(row.fileId&&!fileIds.has(row.fileId))throw Error('Code location outside frozen inventory');
63
+ const probes=[...(manifest.files??[])];
64
+ const add=(relative:string,conditions:string)=>{if(!probes.some(p=>p.path===relative))probes.push({path:relative,conditions});};
65
+ add(selection.path,'Optional static code navigation from byte-bound frozen source. Inspect coverage, parser/resolvers, unresolved calls and truncation. Missing hits prove no absence; paths prove no execution or past event. Back-read source before using a candidate; original requirements and judge authority are unchanged.');
66
+ for(const relative of new Set<string>(evidence.symbols.map((s:{fileId:string})=>fileIds.get(s.fileId)).filter(Boolean))){
67
+ const content=fs.readFileSync(path.join(source,relative));
68
+ try{if(content.length>1048576)continue;new TextDecoder('utf-8',{fatal:true}).decode(content);}
69
+ catch{continue;}
70
+ add(selection.sourceRoot+'/'+relative,'Actual frozen source read to check optional navigation candidates. Static inspection establishes neither runtime execution nor completeness of unsupported files.');
71
+ }
72
+ return {...manifest,files:probes};
73
+ }
@@ -0,0 +1,35 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import type {Decision,EvaluationConfig,EvaluationInput,Invocation} from '../contract';
4
+ import {counterCandidateView,eligibleCandidate,resolveCounterReview,unavailableCounterReview,type CounterReviewResult} from '../counter-review';
5
+ import type {Budget} from '../budget';
6
+ import {JudgeProtocolError} from '../proposal';
7
+ import {modelTurn} from './model-turn';
8
+ import type {AgentRun,AgentRuntime} from './agent-runtime';
9
+ import type {ProcessState} from './process';
10
+ import {JUDGE_GUIDANCE,SERVICE_FACT_GUIDANCE} from './judge';
11
+ import {PROPOSAL_GUIDANCE,COUNTER_REVIEW_GUIDANCE} from './guidance';
12
+ import {NEED_GUIDANCE} from '../needs';
13
+
14
+ export {candidateWindow} from '../budget';
15
+
16
+ export function counterGuidance(replacementGuide:string):string {
17
+ return [JUDGE_GUIDANCE.replace('Interpret native facts, not prior verdicts or execution inferred from code.','Interpret native facts. Only the explicitly supplied candidate is a fallible claim to challenge, never authority/evidence. No other assessment may be read. Do not infer execution from code.'),SERVICE_FACT_GUIDANCE,
18
+ COUNTER_REVIEW_GUIDANCE.replace('{{replacementGuide}}',JSON.stringify(replacementGuide))].join('\n');
19
+ }
20
+
21
+ /** Explicit opt-in effect. Exactly one fresh review, using the original budget. */
22
+ export async function counterReview(runtime:{run:AgentRun;budget:Budget;state:ProcessState;agent?:AgentRuntime},input:EvaluationInput,candidate:Decision,config:EvaluationConfig):Promise<CounterReviewResult> {
23
+ if(!eligibleCandidate(input,candidate,config))return {candidate,review:null,decision:candidate,resolution:'not-required'};
24
+ if(runtime.budget.invocations>=runtime.budget.maxInvocations||Date.now()>=runtime.budget.startedAt+runtime.budget.deadlineMs)return unavailableCounterReview(candidate,candidate.invocations,'No original budget remains for required experimental counter-review','budget');
25
+ const invocations:Invocation[]=[];
26
+ try{
27
+ const guide=path.join(runtime.run.root,'input','counter-replacement-guide.md');
28
+ fs.writeFileSync(guide,[PROPOSAL_GUIDANCE,...(input.needs?.length?[NEED_GUIDANCE]:[])].join('\n'),{flag:'wx'});
29
+ const payload={...input,policy:config,candidate:counterCandidateView(input,candidate,config)};
30
+ const value=await modelTurn({...runtime,invocations},{evaluation:input,payload,guidance:counterGuidance(guide),phase:'counter-review'});
31
+ return resolveCounterReview(input,candidate,value,[...candidate.invocations,...invocations],config);
32
+ }catch(error){
33
+ return unavailableCounterReview(candidate,[...candidate.invocations,...invocations],String(error),error instanceof JudgeProtocolError?'protocol':'infrastructure');
34
+ }
35
+ }
@@ -0,0 +1,19 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+
5
+ export const EGO_SKILL = path.join(os.homedir(), '.agents/skills/ego-browser');
6
+ export function resolveEgo(): { executable: string; installRoot: string } {
7
+ const executable = process.env.E2E_EGO_BINARY ?? path.join(os.homedir(), '.local/bin/ego-browser');
8
+ const real = fs.realpathSync(executable);
9
+ return { executable, installRoot: path.dirname(path.dirname(real)) };
10
+ }
11
+ export function egoToolPaths(): { read: string[]; write: string[] } {
12
+ return { read: [resolveEgo().installRoot, path.join(os.homedir(), '.local/share/ego'), EGO_SKILL], write: [] };
13
+ }
14
+ /** Supplied by the owner; never create/claim a replacement space on failure. */
15
+ export function egoSpace(): number {
16
+ const id = Number(process.env.E2E_EGO_SPACE_ID);
17
+ if (!Number.isSafeInteger(id) || id <= 0) throw new Error('E2E_EGO_SPACE_ID must identify the owner-created Ego TaskSpace');
18
+ return id;
19
+ }
@@ -0,0 +1,37 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import {readContent} from './evidence-object';
4
+
5
+ /** Native immutable index transport; metadata is never a semantic observation. */
6
+ export function evidenceCatalog(indexFile:string) {
7
+ if(!path.isAbsolute(indexFile))throw Error('Absolute current index required');
8
+ const index=JSON.parse(fs.readFileSync(indexFile,'utf8'));
9
+ if(!Array.isArray(index.observations))throw Error('Native observation catalog required');
10
+ const seen=new Set<string>();
11
+ const observations=index.observations.map((o:any)=>{
12
+ if(!/^obs:[1-9]\d*$/.test(o.observationRef)||seen.has(o.observationRef))throw Error('Unknown or ambiguous observation reference');
13
+ if(!path.isAbsolute(o.contentPath)||!Number.isSafeInteger(o.bytes)||o.bytes<0||!/^[a-f0-9]{64}$/.test(o.contentDigest))throw Error('Invalid complete object metadata');
14
+ seen.add(o.observationRef);
15
+ // Strip bodies even if a malformed caller duplicated them in the index.
16
+ const {content:_content,...metadata}=o;
17
+ return metadata;
18
+ });
19
+ return {indexPath:indexFile,metadataOnly:true,observations,
20
+ usage:'Choose current observationRef values and read complete objects. Empty refs lists metadata only; metadata does not prove any requirement.'};
21
+ }
22
+
23
+ /** Mechanical selection only; no action, score, summary, range or invented ref. */
24
+ export function readIndexedEvidence(indexFile:string,refs:string[]) {
25
+ if(!Array.isArray(refs)||!refs.length||refs.length>32||new Set(refs).size!==refs.length||refs.some(ref=>!/^obs:[1-9]\d*$/.test(ref)))throw Error('1..32 distinct obs:N references required');
26
+ const index=evidenceCatalog(indexFile);
27
+ return refs.map(ref=>{
28
+ const observation=index.observations.find((o:any)=>o.observationRef===ref);
29
+ if(!observation)throw Error('Unknown observation reference: '+ref);
30
+ return {...observation,content:readContent(observation)};
31
+ });
32
+ }
33
+
34
+ /** One discovery/read entrance shared by Halfcode and isolated native turns. */
35
+ export function inspectEvidence(indexFile:string,refs:string[]=[]) {
36
+ return refs.length?readIndexedEvidence(indexFile,refs):evidenceCatalog(indexFile);
37
+ }