@awak-app/simy-cli 0.7.46 → 0.7.47

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.7.46",
3
+ "version": "0.7.47",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,7 @@
43
43
  "check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
44
44
  "check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
45
45
  "check:sqm": "node --check ./src/sqm/provider-plugin-repair.js && node --check ./src/sqm/provider-plugin-status.js && node --check ./src/sqm/advisory-guidance.js && node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
46
- "check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/claude-delivery-fault-live-e2e.mjs && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js && node --check ./scripts/codex-plugin-runtime-e2e.js",
46
+ "check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/claude-stream-acceptance.mjs && node --check ./scripts/claude-delivery-fault-live-e2e.mjs && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js && node --check ./scripts/codex-plugin-runtime-e2e.js",
47
47
  "check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/connection-diagnostics.js && node --check ./src/launch-authorization.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/login-shell-path.js && node --check ./src/hook-runtime.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/delivery-hooks/recovery.js && node --check ./src/delivery-hooks/supervisor-owner.js && node --check ./src/delivery-hooks/supervisor-watchdog.js && node --check ./src/delivery-hooks/supervisor-service.js && node --check ./scripts/supervisor-watchdog-live-e2e.js && node --check ./src/codex-plugins/simy/hooks/recovery-incident.mjs && node --check ./src/session-supervision/automatic-continuation.js && node --check ./src/session-supervision/stop-notifications.js",
48
48
  "check:package": "node ./scripts/check-package-contents.js",
49
49
  "build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
@@ -14,7 +14,7 @@ import {
14
14
  } from "./durable-local-task-steps-contract.js";
15
15
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
16
16
 
17
- export const CLI_VERSION = "0.7.46";
17
+ export const CLI_VERSION = "0.7.47";
18
18
  export const CLI_API_CONTRACT_VERSION = 10;
19
19
 
20
20
  export function narrativeImageCapabilityMetadata() {
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "simy_native": {
4
+ "command": "./native/launch",
5
+ "args": ["./native/server.mjs"],
6
+ "cwd": ".",
7
+ "env_vars": ["CODEX_APP_TOOLS_PIPE_PATH", "CODEX_MCP_NODE_PATH", "CODEX_BROWSER_USE_NODE_PATH", "CODEX_ELECTRON_RESOURCES_PATH", "CODEX_CLI_PATH", "SIMY_HOME", "HOME", "PATH"],
8
+ "startup_timeout_sec": 10
9
+ }
10
+ }
11
+ }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import {guidancePending} from '../native/guidance-intent.mjs';
2
3
  import { decideLoop, compactWorkHistory } from './loop-kernel.mjs';
3
4
  import { completionContractIdentity } from './purpose-completion-contract.mjs';
4
5
  import { captureDispatchProvenance } from './update-dispatch-identity.mjs';
@@ -1674,10 +1675,10 @@ export function externalReviewIsCurrent(managed,review,now) {
1674
1675
  const refs=[...(review.criteria||[]).filter(row=>row.passed),...(review.rules||[]).filter(row=>row.status==='satisfied'),...(review.workUnits||[])].flatMap(row=>row.evidenceRefs||[]).filter(link=>/^[a-f0-9]{64}$/.test(link.ref));
1675
1676
  // A flexible semantic verdict refers to its actual inspection, including
1676
1677
  // external observations even when no authored citation was supplied.
1677
- if (review.semanticInspection?.source === 'controller_actual_read_set') refs.push(...(review.semanticInspection.observationRefs || []).map(ref => ({ ref })));
1678
+ if (['controller_actual_read_set', 'controller_conversation_answer'].includes(review.semanticInspection?.source)) refs.push(...(review.semanticInspection.observationRefs || []).map(ref => ({ ref })));
1678
1679
  const ledger=managed.purpose.evidence;
1679
1680
  const observations=[...(ledger?.observations||[]),...(ledger?.reviewObservationActionId===(review.decisionId||managed.decision.id) ? ledger.reviewObservations||[] : [])];
1680
- const inspected = new Set(review.semanticInspection?.source === 'controller_actual_read_set' ? review.semanticInspection.observationRefs || [] : []);
1681
+ const inspected = new Set(['controller_actual_read_set', 'controller_conversation_answer'].includes(review.semanticInspection?.source) ? review.semanticInspection.observationRefs || [] : []);
1681
1682
  return refs.filter(link => inspected.has(link.ref) || observations.some(item => item.id === link.ref))
1682
1683
  .every(link=>observations.some(item=>item.id===link.ref && item.status==='observed' && currentPurposeObservation(item,now,review.observationWindow)));
1683
1684
 
@@ -1799,7 +1800,7 @@ export async function settlePurposeVerification({ root, provider, session, expec
1799
1800
  } else if (pending.length && pending.every(c => c.check.type === 'manual')) {
1800
1801
  run.state = 'verification_pending'; run.reason = 'Only explicitly human acceptance remains.'; state.phase = 'waiting';
1801
1802
  run.checkpoint = { status: 'completed', summary: review.summary }; managed.autoRecovery = false;
1802
- } else review = { ...review, passed: false, needsHuman: false, findings: ['Local acceptance changed during review; investigate and recheck.'] };
1803
+ } else if (review.passed || review.needsHuman) review = { ...review, passed: false, needsHuman: false, findings: ['Local acceptance changed during review; investigate and recheck.'] };
1803
1804
  }
1804
1805
  state.review={...review,decisionId:managed.decision.id};
1805
1806
  if (!review.passed && !review.needsHuman && openFindings.some(item => item.observation)) {
@@ -2461,6 +2462,7 @@ function recordManagedOutcomeProgress(run, now, assessmentAvailable = true) {
2461
2462
  // Manual/legacy runs have no marker and retain their existing controls.
2462
2463
  export const ownerRenewalPending = record => ['awaiting_human', 'prepared', 'gate_committed'].includes(record?.run?.managed?.ownerRenewal?.status);
2463
2464
  export function automaticContinuationAllowed(record) {
2465
+ if (guidancePending(record)) return false;
2464
2466
  if (ownerRenewalPending(record) || record?.run?.managed?.ownerRenewal?.needsReadOnlyReassessment === true) return false;
2465
2467
  if (record?.providerArchived === true || record?.run?.managed?.purpose && record.autoContinue === false) return false;
2466
2468
  const epoch = record?.run?.managed?.autoContinueEpoch;
@@ -0,0 +1,45 @@
1
+ import {spawn} from 'node:child_process';
2
+ import {createInterface} from 'node:readline';
3
+ const fail=code=>Object.assign(new Error(code),{code});
4
+ export async function openAppTools({record,signal,node,server,pipe}) {
5
+ const p=spawn(node,[server],{env:{...process.env,CODEX_APP_TOOLS_PIPE_PATH:pipe},stdio:['pipe','pipe','ignore']});
6
+ let sequence=0,closed=false;const pending=new Map();const lines=createInterface({input:p.stdout});
7
+ const close=()=>{if(closed)return;closed=true;signal?.removeEventListener('abort',close);for(const q of pending.values())q.reject(fail(q.write?'native_submission_unknown':'native_tools_closed'));pending.clear();lines.close();p.stdin.end();p.kill('SIGTERM');};
8
+ p.on('error',close);p.on('exit',close);p.stdin.on('error',close);signal?.addEventListener('abort',close,{once:true});
9
+ lines.on('line',line=>{let d;try{d=JSON.parse(line);}catch{close();return;}const q=pending.get(d.id);if(!q)return;pending.delete(d.id);d.error?q.reject(fail(q.write?'native_submission_unknown':'native_tools_request_failed')):q.resolve(d.result);});
10
+ const rpc=(method,params,write=false)=>new Promise((resolve,reject)=>{
11
+ if(closed||signal?.aborted){reject(fail('native_tools_closed'));return;}const id=++sequence;
12
+ const timer=setTimeout(()=>{pending.delete(id);reject(fail(write?'native_submission_unknown':'native_tools_timeout'));},15000);
13
+ pending.set(id,{write,resolve:r=>{clearTimeout(timer);resolve(r)},reject:e=>{clearTimeout(timer);reject(e)}});
14
+ p.stdin.write(JSON.stringify({jsonrpc:'2.0',id,method,params})+'\n');
15
+ });
16
+ try{
17
+ await rpc('initialize',{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'simy-original-continuation',version:'1.0.0'}});
18
+ p.stdin.write(JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})+'\n');
19
+ const list=await rpc('tools/list',{});const names=new Set(list.tools.map(t=>t.name));
20
+ if(!names.has('read_thread')||!names.has('send_message_to_thread'))throw fail('native_tools_unsupported');
21
+ const call=async(name,args,write=false)=>{
22
+ const result=await rpc('tools/call',{name,arguments:args,_meta:{'openai/threadId':record.session}},write);
23
+ if(result.isError)throw fail(write?'native_submission_unknown':result.content?.some(c=>c.type==='text'&&c.text.includes('exceeds the size limit'))?'native_tools_response_too_large':'native_tools_request_failed');
24
+ const text=result.content?.find(c=>c.type==='text')?.text;if(!text)throw fail(write?'native_submission_unknown':'native_tools_invalid_response');
25
+ return JSON.parse(text);
26
+ };
27
+ const read=async(cursor,includeOutputs=false)=>{
28
+ const r=await call('read_thread',{threadId:record.session,hostId:'local',turnLimit:1,includeOutputs,maxOutputCharsPerItem:16000,...(cursor?{cursor}:{})});
29
+ if(r.thread?.id!==record.session||r.thread?.hostId!=='local'||r.thread?.cwd!==record.cwd)throw fail('native_tools_owner_mismatch');return r;
30
+ };
31
+ return {transport:'codex_app_tools',close,
32
+ async request(method,params){
33
+ if(params.threadId!==record.session)throw fail('native_tools_target_mismatch');
34
+ const r=await read(params.cursor,method==='thread/turns/list');
35
+ if(method==='thread/read')return {thread:{...r.thread,status:{type:r.thread.status?.type==='active'?'active':r.thread.status?.type==='idle'?'idle':'unknown'}}};
36
+ if(method==='thread/turns/list')return {data:r.turns,nextCursor:r.page?.nextCursor};
37
+ throw fail('native_method_unsupported');
38
+ },
39
+ async submitOriginal({text,beforeSubmit}){
40
+ const r=await read();if(r.thread.status?.type!=='idle')throw fail('native_thread_busy');
41
+ await beforeSubmit();signal?.throwIfAborted();
42
+ return call('send_message_to_thread',{threadId:record.session,hostId:'local',prompt:text},true);
43
+ }};
44
+ }catch(e){close();throw e;}
45
+ }
@@ -0,0 +1,156 @@
1
+ import {assertGuidanceIntent, guidancePending} from './guidance-intent.mjs';
2
+ import {createServer, createConnection} from 'node:net';
3
+ import {mkdir, stat, readFile, writeFile, rename, chmod, rm, readdir} from 'node:fs/promises';
4
+ import {join} from 'node:path';
5
+ import {randomUUID, createHash} from 'node:crypto';
6
+ import {readRecord, updateRecord, automaticContinuationAllowed, hasPendingUserStop, managedBudgetReason} from '../hooks/delivery-loop.mjs';
7
+ import {openAppTools} from './app-tools.mjs';
8
+
9
+ const fail = code => Object.assign(new Error(code), {code});
10
+ export const nativeIntentId = r => createHash('sha256').update(JSON.stringify([r.run.id, r.autoContinueEpoch, r.run.controlRevision, r.run.revision, r.run.managed.decision?.id])).digest('hex');
11
+ const directory = root => join(root, 'native-app-bridge');
12
+ // This is an app-launched transport, never a supervisor or a new task owner.
13
+ // Every write requires an already persisted, exact original-task send intent.
14
+ export async function startNativeBridge({root, node, server, pipe, openClient = openAppTools}) {
15
+ const pipeStat = await stat(pipe);
16
+ if (!pipeStat.isSocket() || pipeStat.uid !== process.getuid()) throw fail('native_tools_owner_mismatch');
17
+ const dir = directory(root), id = randomUUID(), token = randomUUID();
18
+ await mkdir(dir, {recursive: true, mode: 0o700});
19
+ const socket = join(dir, id + '.sock'), peers = new Set();
20
+ const listener = createServer(peer => {
21
+ peers.add(peer); peer.setEncoding('utf8'); peer.setTimeout(60000, () => peer.destroy());
22
+ let buffer = '', busy = false, authorizationReply = null;
23
+ peer.on('error', () => {}); peer.on('close', () => peers.delete(peer));
24
+ peer.on('data', chunk => {
25
+ buffer += chunk;
26
+ if (buffer.length > 1024 * 1024) {peer.destroy(); return;}
27
+ const end = buffer.indexOf('\n'); if (end < 0) return;
28
+ if (buffer.indexOf('\n', end + 1) !== -1) {peer.destroy(); return;}
29
+ const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
30
+ if (busy) {if (authorizationReply) authorizationReply(line); else peer.destroy(); return;}
31
+ busy = true;
32
+ void (async () => {
33
+ let client;
34
+ try {
35
+ const q = JSON.parse(line);
36
+ if (q.token !== token || !['thread/read', 'thread/turns/list', 'submit', 'submit_guidance'].includes(q.method)) throw fail('native_tools_bridge_denied');
37
+ // Old hosts cannot answer the final controller challenge. Fail closed
38
+ // rather than silently accepting their earlier authorization.
39
+ if (q.method === 'submit_guidance' && q.guidanceAuthorization !== 1) throw fail('native_guidance_unavailable');
40
+ const authorizeGuidance = () => new Promise((resolve, reject) => {
41
+ const challenge = randomUUID();
42
+ const finish = error => {
43
+ clearTimeout(timer); authorizationReply = null; peer.removeListener('close', lost);
44
+ error ? reject(error) : resolve();
45
+ };
46
+ const lost = () => finish(fail('native_guidance_changed'));
47
+ const timer = setTimeout(lost, 15000);
48
+ peer.once('close', lost);
49
+ authorizationReply = line => {
50
+ try {
51
+ const ack = JSON.parse(line);
52
+ if (ack.token !== token || ack.method !== 'authorize_guidance' || ack.challenge !== challenge ||
53
+ ack.guidanceId !== q.guidanceId || ack.authorized !== true) throw fail('native_guidance_changed');
54
+ finish();
55
+ } catch {finish(fail('native_guidance_changed'));}
56
+ };
57
+ if (peer.destroyed) {lost(); return;}
58
+ peer.write(JSON.stringify({kind: 'guidance_authorization_required', guidanceId: q.guidanceId, challenge}) + '\n');
59
+ });
60
+ const r = await readRecord(join(root, 'delivery-hooks'), 'codex', q.session);
61
+ if (!r?.run?.managed || r.run.id !== q.runId || r.run.managed.taskId !== q.taskId || r.autoContinueEpoch !== q.epoch || !(q.guidanceId ? guidancePending(r) && r.run.managed.userGuidance.id === q.guidanceId : automaticContinuationAllowed(r))) throw fail('native_tools_attachment_changed');
62
+ client = await openClient({record: r, node, server, pipe});
63
+ const result = q.method === 'submit_guidance' ? await client.submitOriginal({text: q.text, beforeSubmit: async () => {
64
+ await updateRecord(join(root, 'delivery-hooks'), 'codex', q.session, async fresh => {
65
+ const intent = assertGuidanceIntent(fresh, q.guidanceId, q.text);
66
+ if (intent.phase !== 'submitting' || intent.bridgeAttemptedAt || hasPendingUserStop(join(root, 'delivery-hooks'), fresh) || managedBudgetReason(fresh.run)) throw fail('native_guidance_changed');
67
+ // Hold the exact intent lock while asking its original controller
68
+ // to recheck account/org/provider authority at the provider boundary.
69
+ await authorizeGuidance();
70
+ assertGuidanceIntent(fresh, q.guidanceId, q.text);
71
+ if (peer.destroyed || hasPendingUserStop(join(root, 'delivery-hooks'), fresh) || managedBudgetReason(fresh.run)) throw fail('native_guidance_changed');
72
+ intent.bridgeAttemptedAt = Date.now();
73
+ return fresh;
74
+ });
75
+ }}) : q.method === 'submit' ? await client.submitOriginal({text: q.text, beforeSubmit: async () => {
76
+ const fresh = await readRecord(join(root, 'delivery-hooks'), 'codex', q.session), m = fresh?.run?.managed, h = m?.nativeHandoff;
77
+ if (fresh?.run?.id !== q.runId || m?.taskId !== q.taskId || fresh.autoContinueEpoch !== q.epoch || !automaticContinuationAllowed(fresh) || hasPendingUserStop(join(root, 'delivery-hooks'), fresh) || fresh.run.stopReason || fresh.run.state !== 'recovery_required' || m.pendingApproval || m.resumeLease || m.liveWorkspaceLease || m.outcomeAudit?.lease || m.providerWarning || m.usageBlocked || m.maxTokens || m.understandingQuestion || m.purpose?.nativeBoundary || managedBudgetReason(fresh.run) || ['verifying', 'understanding', 'needs_clarification', 'action_association'].includes(m.phase) || m.executionUncertain || m.nativeWait || m.userQuestion || m.authenticationWait || m.dependencyWait || h?.id !== q.handoffId || h?.id !== nativeIntentId(fresh) || h.phase !== 'submitting' || h.text !== q.text) throw fail('native_tools_intent_changed');
78
+ }}) : await client.request(q.method, {threadId: q.session, cursor: q.cursor});
79
+ peer.end(JSON.stringify({ok: true, result, guidanceAuthorization: 1}) + '\n');
80
+ } catch (error) {peer.end(JSON.stringify({ok: false, code: error.code || 'native_tools_request_failed'}) + '\n');}
81
+ finally {client?.close();}
82
+ })();
83
+ });
84
+ });
85
+ await new Promise((resolve, reject) => {listener.once('error', reject); listener.listen(socket, resolve);});
86
+ await chmod(socket, 0o600);
87
+ const target = join(dir, id + '.json'), temp = target + '.tmp';
88
+ await writeFile(temp, JSON.stringify({schema: 1, id, pid: process.pid, socket, token, startedAt: Date.now()}), {mode: 0o600});
89
+ await rename(temp, target);
90
+ return {id, async close() {
91
+ for (const peer of peers) peer.destroy();
92
+ await new Promise(resolve => listener.close(resolve));
93
+ await rm(socket, {force: true});
94
+ try {if (JSON.parse(await readFile(target, 'utf8')).id === id) await rm(target);} catch (e) {if (e.code !== 'ENOENT') throw e;}
95
+ }};
96
+ }
97
+
98
+ export async function openNativeBridge(options) {
99
+ let names;
100
+ try {names = (await readdir(directory(options.root))).filter(n => /^[0-9a-f-]{36}\.json$/.test(n)).sort();}
101
+ catch (e) {if (e.code === 'ENOENT') return null; throw e;}
102
+ if (!names.length) return null;
103
+ for (const name of names) {
104
+ let client;
105
+ try {
106
+ const a = JSON.parse(await readFile(join(directory(options.root), name), 'utf8'));
107
+ client = await openBridgeInstance(options, a);
108
+ await client.request('thread/read', {threadId: options.record.session});
109
+ return client;
110
+ } catch (e) {client?.close(); if (options.signal?.aborted) throw e;}
111
+ }
112
+ throw fail('native_tools_bridge_unavailable');
113
+ }
114
+ async function openBridgeInstance({root, record, signal, guidanceId: optionsGuidanceId}, a) {
115
+ if (a.schema !== 1 || typeof a.token !== 'string' || a.socket !== join(directory(root), a.id + '.sock')) throw fail('native_tools_bridge_invalid');
116
+ const s = await stat(a.socket).catch(() => {throw fail('native_tools_bridge_unavailable');});
117
+ if (!s.isSocket() || s.uid !== process.getuid() || (s.mode & 0o077)) throw fail('native_tools_owner_mismatch');
118
+ const peers = new Set(); let closed = false, guidanceAuthorization = false;
119
+ const call = (method, extra = {}, authorizeGuidance) => new Promise((resolve, reject) => {
120
+ if (closed || signal?.aborted) {reject(fail('native_tools_closed')); return;}
121
+ const peer = createConnection(a.socket); peers.add(peer); let buffer = '', sent = false, settled = false, challenged = false;
122
+ const finish = (error, result) => {if (settled) return; settled = true; clearTimeout(timer); peers.delete(peer); peer.destroy(); error ? reject(error) : resolve(result);};
123
+ const lost = () => finish(fail(sent && ['submit', 'submit_guidance'].includes(method) ? 'native_submission_unknown' : 'native_tools_bridge_unavailable'));
124
+ const timer = setTimeout(lost, 45000);
125
+ peer.setEncoding('utf8'); peer.on('error', lost); peer.on('close', lost);
126
+ peer.on('connect', () => {sent = true; peer.write(JSON.stringify({token: a.token, session: record.session, runId: record.run.id, taskId: record.run.managed.taskId, epoch: record.autoContinueEpoch, method, ...(method === 'submit_guidance' ? {guidanceAuthorization: 1} : {}), ...(optionsGuidanceId ? {guidanceId: optionsGuidanceId} : {}), ...extra}) + '\n');});
127
+ peer.on('data', chunk => {
128
+ buffer += chunk; if (buffer.length > 16 * 1024 * 1024) {lost(); return;}
129
+ const end = buffer.indexOf('\n'); if (end < 0) return;
130
+ const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
131
+ try {
132
+ const r = JSON.parse(line);
133
+ if (r.kind === 'guidance_authorization_required') {
134
+ if (method !== 'submit_guidance' || challenged || !authorizeGuidance || r.guidanceId !== optionsGuidanceId ||
135
+ typeof r.challenge !== 'string' || !/^[0-9a-f-]{36}$/i.test(r.challenge)) {lost(); return;}
136
+ challenged = true;
137
+ const acknowledge = authorized => {
138
+ if (!settled && !peer.destroyed) peer.write(JSON.stringify({token: a.token, method: 'authorize_guidance',
139
+ guidanceId: optionsGuidanceId, challenge: r.challenge, authorized}) + '\n');
140
+ };
141
+ Promise.resolve().then(authorizeGuidance).then(() => {signal?.throwIfAborted(); acknowledge(true);}, () => acknowledge(false)).catch(() => acknowledge(false));
142
+ return;
143
+ }
144
+ if (r.guidanceAuthorization === 1) guidanceAuthorization = true;
145
+ finish(r.ok ? null : fail(r.code), r.result);
146
+ } catch {lost();}
147
+ });
148
+ });
149
+ const close = () => {closed = true; signal?.removeEventListener('abort', close); for (const peer of peers) peer.destroy();};
150
+ signal?.addEventListener('abort', close, {once: true});
151
+ return {transport: 'codex_app_tools', close, get guidanceAuthorization() {return guidanceAuthorization;},
152
+ request(method, params) {if (params.threadId !== record.session) throw fail('native_tools_target_mismatch'); return call(method, {cursor: params.cursor});},
153
+ async submitGuidance({text, beforeSubmit}) {if (!optionsGuidanceId || !guidanceAuthorization) throw fail('native_guidance_unavailable'); await beforeSubmit(); signal?.throwIfAborted(); return call('submit_guidance', {text}, beforeSubmit);},
154
+ async submitOriginal({text, beforeSubmit}) {await beforeSubmit(); signal?.throwIfAborted(); const fresh = await readRecord(join(root, 'delivery-hooks'), 'codex', record.session); return call('submit', {text, handoffId: fresh?.run?.managed?.nativeHandoff?.id});},
155
+ };
156
+ }
@@ -0,0 +1,30 @@
1
+ import {createHash} from 'node:crypto';
2
+
3
+ export const guidancePending = record => ['prepared', 'submitting', 'submitted', 'unknown'].includes(record?.run?.managed?.userGuidance?.phase);
4
+ export const guidanceDigest = text => createHash('sha256').update(text).digest('hex');
5
+ export function guidanceTarget(record) {
6
+ return {provider: record?.provider, session: record?.session, cwd: record?.cwd,
7
+ runId: record?.run?.id, taskId: record?.run?.managed?.taskId, epoch: record?.autoContinueEpoch,
8
+ revision: (record?.run?.revision || 0) + (record?.run?.controlRevision || 0)};
9
+ }
10
+ export function guidanceAllowed(record, {pendingId} = {}) {
11
+ const m = record?.run?.managed;
12
+ return record?.provider === 'codex' && record.autoContinue === true && !!record.autoContinueEpoch &&
13
+ !record.providerArchived && !record.parentSession && record.run?.state === 'recovery_required' && !record.run.stopReason &&
14
+ m?.purpose?.enabled === true && m.resumeChannel === 'codex_app_server' && (!m.autoContinueEpoch || m.autoContinueEpoch === record.autoContinueEpoch) &&
15
+ !m.pendingApproval && !m.userQuestion && !m.understandingQuestion && !m.nativeWait && !m.dependencyWait && !m.authenticationWait &&
16
+ !m.resumeLease && !m.liveWorkspaceLease && !m.outcomeAudit?.lease && !m.executionUncertain && !m.providerWarning &&
17
+ !m.usageBlocked && !m.maxTokens && !m.purpose?.nativeBoundary &&
18
+ !['verifying', 'understanding', 'needs_clarification', 'action_association'].includes(m.phase) &&
19
+ (!m.nativeHandoff || ['finished', 'cancelled'].includes(m.nativeHandoff.phase)) &&
20
+ (!guidancePending(record) || pendingId === m.userGuidance.id);
21
+ }
22
+ export function assertGuidanceIntent(record, id, text) {
23
+ const intent = record?.run?.managed?.userGuidance;
24
+ if (!intent || intent.id !== id || !guidanceAllowed(record, {pendingId: id}) ||
25
+ JSON.stringify(intent.target) !== JSON.stringify(guidanceTarget(record)) ||
26
+ intent.text !== text || intent.digest !== guidanceDigest(text)) {
27
+ throw Object.assign(new Error('The original guidance target or send intent changed.'), {code: 'native_guidance_changed'});
28
+ }
29
+ return intent;
30
+ }
@@ -0,0 +1,36 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ if [ "$#" -lt 1 ]; then
5
+ printf '%s\n' 'Codex app tools could not start: missing MCP server path.' >&2
6
+ exit 64
7
+ fi
8
+
9
+ PATH="${PATH:-/usr/bin:/bin}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
10
+ export PATH
11
+
12
+ cache_root=${XDG_CACHE_HOME:-${HOME:-}/.cache}
13
+ codex_resources=
14
+ case "${CODEX_CLI_PATH:-}" in
15
+ */*) codex_resources=${CODEX_CLI_PATH%/*} ;;
16
+ esac
17
+ for candidate in \
18
+ "${CODEX_MCP_NODE_PATH:-}" \
19
+ "${CODEX_BROWSER_USE_NODE_PATH:-}" \
20
+ "${CODEX_ELECTRON_RESOURCES_PATH:-}/cua_node/bin/node" \
21
+ "$codex_resources/cua_node/bin/node" \
22
+ "$cache_root/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node"
23
+ do
24
+ if [ -n "$candidate" ] && [ -x "$candidate" ]; then
25
+ exec "$candidate" "$@"
26
+ fi
27
+ done
28
+
29
+ if node_path=$(command -v node 2>/dev/null); then
30
+ case "$node_path" in
31
+ /*) exec "$node_path" "$@" ;;
32
+ esac
33
+ fi
34
+
35
+ printf '%s\n' 'Codex app tools could not find a Node runtime. Reinstall or update Codex.' >&2
36
+ exit 127
@@ -0,0 +1,26 @@
1
+ import {createInterface} from 'node:readline';
2
+ import {homedir} from 'node:os';
3
+ import {join, dirname} from 'node:path';
4
+ import {startNativeBridge} from './bridge.mjs';
5
+
6
+ // Codex launches this ordinary stdio MCP server with its own Node runtime and
7
+ // fresh app-tools pipe. No saved pipe, daemon launch or app modification needed.
8
+ let bridge, reason = 'native_tools_attachment_missing';
9
+ try {
10
+ const resources = process.env.CODEX_ELECTRON_RESOURCES_PATH || dirname(dirname(dirname(process.execPath)));
11
+ bridge = await startNativeBridge({root: process.env.SIMY_HOME || join(homedir(), '.simy'), node: process.execPath,
12
+ server: join(resources, 'plugins/openai-bundled/plugins/codex-app-tools/server.mjs'), pipe: process.env.CODEX_APP_TOOLS_PIPE_PATH});
13
+ } catch (e) {reason = e.code || reason;}
14
+ const lines = createInterface({input: process.stdin});
15
+ for await (const line of lines) {
16
+ let q; try {q = JSON.parse(line);} catch {continue;}
17
+ if (q.id === undefined) continue;
18
+ let result;
19
+ if (q.method === 'initialize') result = {protocolVersion: q.params.protocolVersion, capabilities: {tools: {}}, serverInfo: {name: 'simy-native-continuation', version: '1.0.0'}};
20
+ else if (q.method === 'tools/list') result = {tools: [{name: 'native_connection_status', description: 'Read SIMY native Codex transport availability. Connection availability is not execution or completion proof.', inputSchema: {type: 'object', properties: {}, additionalProperties: false}, annotations: {readOnlyHint: true}}]};
21
+ else if (q.method === 'tools/call' && q.params?.name === 'native_connection_status') result = {content: [{type: 'text', text: JSON.stringify({connected: !!bridge, reason: bridge ? 'app_launched_transport' : reason})}]};
22
+ else if (q.method === 'ping') result = {};
23
+ else {process.stdout.write(JSON.stringify({jsonrpc: '2.0', id: q.id, error: {code: -32601, message: 'Method not found'}}) + '\n'); continue;}
24
+ process.stdout.write(JSON.stringify({jsonrpc: '2.0', id: q.id, result}) + '\n');
25
+ }
26
+ await bridge?.close();
@@ -0,0 +1,9 @@
1
+ // Exact tool output, original sender, complete text and XML escaping are all
2
+ // required. A quoted message or worker assertion is never a send receipt.
3
+ export function matchesNativeDelegation(turn, session, text) {
4
+ const escaped = text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
5
+ const expected = `<codex_delegation>\n <source_thread_id>${session}</source_thread_id>\n <input>${escaped}</input>\n</codex_delegation>`;
6
+ return turn.items?.some(item => item.type === 'functionCallOutput' && item.namespace === 'codex_app' &&
7
+ item.name === 'send_message_to_thread' && !item.output?.truncated &&
8
+ (typeof item.output === 'string' ? item.output : item.output?.text) === expected) === true;
9
+ }
@@ -1,3 +1,4 @@
1
+ import {assertNativeObservationScope} from '../session-supervision/native-turn-observer.js';
1
2
  import { captureDispatchProvenance } from '../update-detachment/provenance.js';
2
3
  import { captureDispatchOwner, assertTaskAdmission, dispatchWithFence, queueDispatchReceipt, confirmDispatch, flushDispatchIntake, rootFromDelivery, observeDispatchEvent } from '../update-detachment/dispatch-gate.js';
3
4
  import { bindCompletionArtifacts } from '../session-supervision/completion-artifacts.js';
@@ -29,7 +30,7 @@ import { createTaskPool } from '../codex-plugins/simy/hooks/loop-kernel.mjs';
29
30
  const MAX_OUTPUT = 64 * 1024;
30
31
  const SAFE_PROVIDER_CODES = new Set(['authentication_required', 'identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
31
32
  'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval', 'provider_question', 'usage_unavailable',
32
- 'state_busy', 'dispatch_detached', 'owner_fenced', 'task_admission_blocked', 'task_admission_identity_required',
33
+ 'state_busy', 'review_context_changed', 'review_history_changed', 'cancelled', 'dispatch_detached', 'owner_fenced', 'task_admission_blocked', 'task_admission_identity_required',
33
34
  'task_identity_conflict', 'attempt_already_recorded', 'identity_unavailable']);
34
35
  function safeProviderCode(code) { return SAFE_PROVIDER_CODES.has(code) ? code : 'provider_error'; }
35
36
  function providerFailureReason(code) {
@@ -39,6 +40,7 @@ function providerFailureReason(code) {
39
40
  if (code === 'checkpoint' || code === 'checkpoint_rejected') return 'Codex returned an invalid managed checkpoint; inspect its plan before resuming.';
40
41
  if (code === 'turn_failed') return 'The Codex turn did not complete; inspect the original conversation.';
41
42
  if (code === 'state_busy') return 'The local dispatch state is busy; reconcile its lock before another attempt.';
43
+ if (code === 'review_context_changed' || code === 'review_history_changed') return 'The reviewed conversation or captured results changed. Preserve the prior verdict and inspect the latest settled result before another independent review.';
42
44
  if (['dispatch_detached', 'owner_fenced', 'task_admission_blocked', 'task_admission_identity_required', 'task_identity_conflict', 'identity_unavailable'].includes(code)) return `Local dispatch admission requires reconciliation (${code}); preserve the existing task and provider authority.`;
43
45
  return 'Codex recovery process failed; inspect its checkpoint and original session before the next bounded recovery.';
44
46
  }
@@ -416,8 +418,14 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
416
418
  });
417
419
  },
418
420
  onEvidence: async evidence => {
419
- await persistReceipt(current => {
421
+ await persistReceipt(async current => {
420
422
  if (current?.run?.managed?.resumeLease?.id !== leaseId || current.run.managed.decision?.id !== evidence.actionId) throw new Error('Evidence capture lease changed.');
423
+ if ((evidence.observations || []).some(item => item.kind === 'codex_native_history')) {
424
+ await authorizeClaim();
425
+ assertNativeObservationScope(evidence, current, root);
426
+ for (const item of evidence.observations.filter(item => item.kind === 'codex_native_history'))
427
+ await requireProviderLink({root, provider: current.provider, expectedEpoch: item.providerEpoch});
428
+ }
421
429
  if(evidence.workerContext)current.run.managed.executionAttempt.workerContext=structuredClone(evidence.workerContext);
422
430
  if (searchEnabled(current.run.managed)) retainControllerEvidence(current.run.managed, evidence, currentTime());
423
431
  return current;