@awak-app/simy-cli 0.6.26 → 0.6.27
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 +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/acceptance-policy.mjs +20 -3
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +2 -1
- package/src/delivery-hooks/command.js +18 -7
- package/src/delivery-hooks/consistency-monitor.js +1 -1
- package/src/delivery-hooks/runtime-watchdog.js +6 -2
- package/src/desktop-process-lock.js +1 -1
- package/src/session-supervision/autonomous-upgrade.js +62 -4
- package/src/session-supervision/codex-app-server.js +20 -7
- package/src/session-supervision/index.js +4 -0
- package/src/session-supervision/purpose-recovery.js +2 -2
package/package.json
CHANGED
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.6.
|
|
16
|
+
export const CLI_VERSION = "0.6.27";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function narrativeImageCapabilityMetadata() {
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const acceptanceIdentity = criterion => createHash('sha256').update(JSON.stringify(criterion)).digest('hex');
|
|
4
|
+
// A description cannot establish consent to replace a frozen verifier in any
|
|
5
|
+
// language. Only a user's delegation naming this exact criterion can do so.
|
|
6
|
+
export function legacyIndependentCriterion(criterion, delegatedIds = []) {
|
|
7
|
+
return criterion.check?.type === 'manual' && delegatedIds.includes(criterion.id) && Boolean(criterion.description?.trim());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const policyContractIdentity = managed => createHash('sha256').update(JSON.stringify([managed.taskId, managed.enrollment || null, managed.acceptanceCriteria, managed.approvalScopes, managed.grants || null, managed.autoContinueEpoch || null])).digest('hex');
|
|
11
|
+
export const autonomousPolicyAuthorized = managed => managed?.policyAuthorization?.source === 'explicit_user_runtime_policy' && managed.policyAuthorization.revokedAt === undefined &&
|
|
12
|
+
managed.policyAuthorization.taskId === managed.taskId && managed.policyAuthorization.contractDigest === policyContractIdentity(managed);
|
|
13
|
+
|
|
3
14
|
export function effectiveAcceptanceCriteria(managed) {
|
|
4
|
-
return managed?.acceptanceCriteria || []
|
|
15
|
+
return (managed?.acceptanceCriteria || []).map(criterion => {
|
|
16
|
+
const route = managed.acceptanceRoutes?.[criterion.id];
|
|
17
|
+
const eligible = autonomousPolicyAuthorized(managed) && managed.autonomyPolicy === 'autonomous' && managed.purpose?.version === 2 &&
|
|
18
|
+
managed.purpose.decisionPolicy === 'llm-first-hc102' && route?.source === 'explicit_user_runtime_policy' &&
|
|
19
|
+
route.authorizationId === managed.policyAuthorization.id && route.criterionDigest === acceptanceIdentity(criterion) && legacyIndependentCriterion(criterion, managed.policyAuthorization.independentCriterionIds);
|
|
20
|
+
return eligible ? { ...criterion, check: { type: 'semantic' }, originalCheck: criterion.check } : criterion;
|
|
21
|
+
});
|
|
5
22
|
}
|
|
@@ -1183,7 +1183,7 @@ export async function verifyAutomaticCriteria(record, now) {
|
|
|
1183
1183
|
evidence_refs: [`host-check:${managed.taskId}:${criterion.id}:${sha256 || 'failed'}`] };
|
|
1184
1184
|
};
|
|
1185
1185
|
const failed = (criterion, detail) => { delete managed.verified[criterion.id]; observe(criterion, false, detail); };
|
|
1186
|
-
for (const criterion of managed
|
|
1186
|
+
for (const criterion of effectiveAcceptanceCriteria(managed)) {
|
|
1187
1187
|
if (criterion.check.type === 'manual') {
|
|
1188
1188
|
if (managed.verified[criterion.id]?.source !== 'user') delete managed.verified[criterion.id];
|
|
1189
1189
|
continue;
|
|
@@ -1780,6 +1780,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
1780
1780
|
}
|
|
1781
1781
|
else if (action === 'set_policy') {
|
|
1782
1782
|
if (!run.managed?.autonomyPolicy || !run.managed.plan || TERMINAL.has(run.state) || !AUTONOMY_POLICIES.has(autonomyPolicy)) throw new Error('Only a nonterminal assessed managed task can change autonomy policy.');
|
|
1783
|
+
if (run.managed.policyAuthorization) run.managed.policyAuthorization = { ...run.managed.policyAuthorization, revokedAt: now, revokedBy: 'user_set_policy' };
|
|
1783
1784
|
run.managed.autonomyPolicy = autonomyPolicy;
|
|
1784
1785
|
const decision = recordDecision(run.managed, run.managed.plan, now);
|
|
1785
1786
|
if (decision.outcome === 'wait') { run.state = 'blocked'; run.reason = decision.reason; escalate(run, decision.reason); }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { adoptAutonomousPolicy } from '../session-supervision/autonomous-upgrade.js';
|
|
1
2
|
import { deliveryEvidence } from './evidence.js';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { startDelivery, checkpointDelivery, controlDelivery, listRecords, rootDirectory, verifyDelivery, readRecord } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
@@ -25,10 +26,24 @@ async function readCheckpointInput(input) {
|
|
|
25
26
|
return Buffer.concat(chunks).toString('utf8');
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function parseDeliveryOptions(args, allowed) {
|
|
30
|
+
const options = {};
|
|
31
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
32
|
+
const key = args[i]?.slice(2);
|
|
33
|
+
if (!args[i]?.startsWith('--') || !allowed.has(key) || args[i + 1] === undefined || Object.hasOwn(options, key)) throw new Error('Invalid delivery option. Use simy delivery --help.');
|
|
34
|
+
options[key] = args[i + 1];
|
|
35
|
+
}
|
|
36
|
+
return options;
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = process.cwd(), input = process.stdin, output = console.log } = {}) {
|
|
29
40
|
const [action, ...args] = argv;
|
|
30
41
|
if (action === 'runtime') {
|
|
31
|
-
if (args
|
|
42
|
+
if (args[0] === 'policy-adopt') {
|
|
43
|
+
const options = parseDeliveryOptions(args.slice(1), new Set(['task-id', 'criterion-id']));
|
|
44
|
+
output(JSON.stringify(await adoptAutonomousPolicy({ root, taskId: options['task-id'], criterionId: options['criterion-id'] }), null, 2)); return 0;
|
|
45
|
+
}
|
|
46
|
+
if (args.length !== 1 || !['install', 'status', 'workflows', 'desktop-install', 'desktop-activate', 'watch', 'watchdog-install', 'legacy-hooks', 'policy-adopt', 'doctor'].includes(args[0])) throw new Error('Use simy delivery runtime install|status|workflows|desktop-install|doctor.');
|
|
32
47
|
if (args[0] === 'legacy-hooks') { output(JSON.stringify(await routeLegacyHooks({ root: dirname(root) }), null, 2)); return 0; }
|
|
33
48
|
if (args[0] === 'watchdog-install') { output(JSON.stringify(await installRuntimeWatchdog({ root: dirname(root) }), null, 2)); return 0; }
|
|
34
49
|
if (args[0] === 'desktop-activate') { output(JSON.stringify(await activateLocalDesktop({ root: dirname(root) }), null, 2)); return 0; }
|
|
@@ -63,6 +78,7 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
63
78
|
if (!action || action === '--help') {
|
|
64
79
|
output('simy delivery runtime install|status|workflows — install an immutable local candidate, inspect its version/digest, or refresh existing plugin sources/cache while preserving enabled state and hook trust. Never enables a task.');
|
|
65
80
|
output('simy delivery runtime doctor|desktop-install|desktop-activate|watch — audit live host/state/monitor alignment, or prepare a separate local desktop using the verified fixed runtime. Does not start or resume tasks.');
|
|
81
|
+
output('simy delivery runtime policy-adopt [--task-id ID [--criterion-id ID]] — explicitly delegate decisions within existing permissions. Manual acceptance stays manual unless the user names the exact task and criterion to delegate its verifier.');
|
|
66
82
|
output('simy delivery supervise [--once] — service already opted-in tasks with this runtime; refuses an active desktop host.');
|
|
67
83
|
output('simy delivery evidence --provider codex|claude --session ID — read-only execution and acceptance receipts.');
|
|
68
84
|
output('simy delivery activity [--watch] [--json] [--provider codex|claude] [--session ID] — local observations every 60 seconds; not productive time.');
|
|
@@ -72,13 +88,8 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
72
88
|
output('simy delivery start --provider codex|claude --session ID --objective TEXT --next-step TEXT [--managed-file task.json] [--execution-cwd PATH] [--max-failures 3]\nsimy delivery checkpoint --provider codex|claude --session ID --file checkpoint.json|-\nsimy delivery status\nsimy delivery stop|pause|resume --provider codex|claude --session ID [--task-id ID]\nsimy delivery replan --provider PROVIDER --session ID --task-id ID\nsimy delivery approve|reject --provider PROVIDER --session ID --task-id ID --approval-id ID\nsimy delivery verify --provider PROVIDER --session ID --task-id ID --criterion-id ID\nsimy delivery set_policy --provider PROVIDER --session ID --task-id ID --autonomy-policy strict|balanced|autonomous\nEnable and review SIMY hooks in your provider first. Standard start tracks checkpoints and reports done without certifying acceptance. Managed start freezes the original objective, optional user-selected deliveryLevel, and acceptance specification. Desktop Continue infers the original conversation goal first. Continue adopts an eligible ordinary loop with the same goal and failure budget; its first managed action reconciles prior work read-only. --execution-cwd explicitly binds the task directory while provider identity retains its original folder. Resume preserves original task identity, history and approvals.');
|
|
73
89
|
return 0;
|
|
74
90
|
}
|
|
75
|
-
const options = {};
|
|
76
91
|
const allowed = new Set(['provider', 'session', 'objective', 'next-step', 'max-failures', 'max-no-progress-minutes', 'continuation-policy', 'file', 'managed-file', 'execution-cwd', 'task-id', 'approval-id', 'criterion-id', 'autonomy-policy', 'wait-id', 'answer', 'target-session']);
|
|
77
|
-
|
|
78
|
-
const key = args[i]?.replace(/^--/, '');
|
|
79
|
-
if (!args[i]?.startsWith('--') || !allowed.has(key) || args[i + 1] === undefined || Object.hasOwn(options, key)) throw new Error('Invalid delivery option. Use simy delivery --help.');
|
|
80
|
-
options[key] = args[i + 1];
|
|
81
|
-
}
|
|
92
|
+
const options = parseDeliveryOptions(args, allowed);
|
|
82
93
|
const base = { root, cwd, provider: options.provider, session: options.session };
|
|
83
94
|
let result;
|
|
84
95
|
if (action === 'evidence') {
|
|
@@ -122,5 +122,5 @@ export async function runConsistencyMonitor({ root, now = Date.now(), records, r
|
|
|
122
122
|
|
|
123
123
|
export async function consistencyMonitorStatus(root, now = Date.now()) {
|
|
124
124
|
const status = await read(join(root, 'consistency', 'monitor.json'));
|
|
125
|
-
return status ? { ...status, health: status.lastFailure ? 'failed' : status.lastAttemptAt > (status.lastSuccessAt || 0) ? 'interrupted' : now - status.lastSuccessAt > 3 * 60_000 ? 'stale' : 'healthy' } : { health: 'not_started' };
|
|
125
|
+
return status ? { ...status, health: status.lastFailure ? 'failed' : status.lastAttemptAt > (status.lastSuccessAt || 0) ? now - (status.lastHeartbeatAt || status.lastAttemptAt) < 90_000 ? 'auditing' : 'interrupted' : now - (status.lastHeartbeatAt || status.lastSuccessAt) > 3 * 60_000 ? 'stale' : 'healthy' } : { health: 'not_started' };
|
|
126
126
|
}
|
|
@@ -53,7 +53,8 @@ export async function tickRuntimeWatchdog({ root, now = Date.now(), run = exec,
|
|
|
53
53
|
host: null, monitor: null, error: null, action: 'observe' };
|
|
54
54
|
try {
|
|
55
55
|
state.host = await observeHost({ root });
|
|
56
|
-
|
|
56
|
+
const monitor = await observeMonitor(join(root, 'delivery-hooks'), now);
|
|
57
|
+
state.monitor = Object.fromEntries(['health', 'ownerPid', 'lastAttemptAt', 'lastSuccessAt', 'lastHeartbeatAt', 'lastFailure', 'metrics'].map(key => [key, monitor[key] ?? null]));
|
|
57
58
|
const active = await read(join(root, 'desktop-runtime/active.json'));
|
|
58
59
|
if (active?.suspendedBy === 'user_quit') state.action = 'user_quit_preserved';
|
|
59
60
|
if (active && !active.suspendedBy && state.host.state !== 'matched' && now - (previous.lastLaunchAt || 0) >= 60_000) {
|
|
@@ -96,6 +97,9 @@ export async function installRuntimeWatchdog({ root, home = homedir(), run = exe
|
|
|
96
97
|
await run('/usr/bin/plutil', ['-lint', plist]);
|
|
97
98
|
const domain = `gui/${process.getuid()}`;
|
|
98
99
|
try { await run('/bin/launchctl', ['bootout', `${domain}/${label}`]); } catch { /* First install has no job. */ }
|
|
99
|
-
|
|
100
|
+
for (let attempt = 0; ; attempt++) {
|
|
101
|
+
try { await run('/bin/launchctl', ['bootstrap', domain, plist]); break; }
|
|
102
|
+
catch (error) { if (attempt >= 2) throw error; await delay(250 * (attempt + 1)); }
|
|
103
|
+
}
|
|
100
104
|
return { label, plist, scope: 'activated_local_desktop_only' };
|
|
101
105
|
}
|
|
@@ -172,7 +172,7 @@ export function parseLegacyAgentArguments(args) {
|
|
|
172
172
|
const previous = agentArgs[index - 1];
|
|
173
173
|
return previous !== "--host" && previous !== "--port";
|
|
174
174
|
});
|
|
175
|
-
if (subcommand === "sqm" || subcommand === "update") return null;
|
|
175
|
+
if (subcommand === "sqm" || subcommand === "update" || subcommand === "delivery" && agentArgs[agentArgs.indexOf("delivery") + 1] !== "supervise" || agentArgs.includes("--version") || agentArgs.includes("-v")) return null;
|
|
176
176
|
let webOrigin = null;
|
|
177
177
|
const hostIndex = argv.indexOf("--host", entryIndex + 1);
|
|
178
178
|
const inlineHost = argv.slice(entryIndex + 1).find((value) => value.startsWith("--host="));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { automaticContinuationAllowed, queuePurposeDiagnosis, queuePurposeVerification } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { automaticContinuationAllowed, listRecords, updateRecord, queuePurposeDiagnosis, queuePurposeVerification } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
|
+
import { autonomousPolicyAuthorized, policyContractIdentity, acceptanceIdentity, legacyIndependentCriterion, effectiveAcceptanceCriteria } from '../codex-plugins/simy/hooks/acceptance-policy.mjs';
|
|
3
4
|
import { purposeFailureMode } from '../codex-plugins/simy/hooks/purpose-search.mjs';
|
|
4
5
|
|
|
5
6
|
export const AUTONOMOUS_STATE_CONTRACT = 2;
|
|
@@ -21,6 +22,12 @@ export function upgradeEligible(record) {
|
|
|
21
22
|
!(record.run.state === 'paused' && pauseOrigin(record.run) === 'user');
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
export function policyAdoptionPending(record) {
|
|
26
|
+
const m = record?.run?.managed;
|
|
27
|
+
return autonomousPolicyAuthorized(m) && (m.autonomyPolicy !== 'autonomous' || m.acceptanceCriteria.some(c => !m.verified[c.id] && legacyIndependentCriterion(c, m.policyAuthorization.independentCriterionIds) &&
|
|
28
|
+
(m.acceptanceRoutes?.[c.id]?.source !== 'explicit_user_runtime_policy' || m.acceptanceRoutes[c.id].authorizationId !== m.policyAuthorization.id || m.acceptanceRoutes[c.id].criterionDigest !== acceptanceIdentity(c))));
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
// Controller-only, idempotent migration. Frozen criteria and grants remain
|
|
25
32
|
// byte-identical, including each criterion's original verifier.
|
|
26
33
|
export function upgradeAutonomousRecord(record, { rules, now, metadata } = {}) {
|
|
@@ -28,28 +35,79 @@ export function upgradeAutonomousRecord(record, { rules, now, metadata } = {}) {
|
|
|
28
35
|
const run = record.run, m = run.managed, previousState = run.state;
|
|
29
36
|
if (metadata?.session !== record.session || metadata.cwd !== record.cwd || !metadata.model ||
|
|
30
37
|
metadata.model !== (m.resumeModel || record.admission.model)) return { changed: false, reason: 'original_channel_unverified' };
|
|
38
|
+
if (metadata.isRunning !== false) return { changed: false, reason: 'original_not_idle' };
|
|
31
39
|
if (!m.approvalScopes?.length) return { changed: false, reason: 'authorized_read_scope_missing' };
|
|
32
40
|
const before = digest({ purpose: m.purpose, autonomyPolicy: m.autonomyPolicy, routes: m.acceptanceRoutes, channel: m.resumeChannel, contractVersion: m.controllerContractVersion });
|
|
33
41
|
const original = digest({ objective: run.objective, criteria: m.acceptanceCriteria, level: m.deliveryLevel, grants: m.grants });
|
|
34
42
|
m.purpose = { ...m.purpose, enabled: true, version: 2, decisionPolicy: 'llm-first-hc102',
|
|
35
43
|
failureBudgetMode: purposeFailureMode(record), ruleDigest: rules.digest, ruleIds: rules.rules.map(rule => rule.id),
|
|
36
44
|
recoveries: m.purpose?.recoveries || [] };
|
|
45
|
+
if (autonomousPolicyAuthorized(m)) {
|
|
46
|
+
m.autonomyPolicy = 'autonomous'; m.acceptanceRoutes ||= {};
|
|
47
|
+
for (const criterion of m.acceptanceCriteria) if (!m.verified[criterion.id] && legacyIndependentCriterion(criterion, m.policyAuthorization.independentCriterionIds)) {
|
|
48
|
+
const expected = acceptanceIdentity(criterion), route = m.acceptanceRoutes[criterion.id];
|
|
49
|
+
if (route?.criterionDigest !== expected || route.authorizationId !== m.policyAuthorization.id || route.source !== 'explicit_user_runtime_policy') m.acceptanceRoutes[criterion.id] = {
|
|
50
|
+
source: 'explicit_user_runtime_policy', authorizationId: m.policyAuthorization.id, mode: 'independent', criterionDigest: expected, at: now };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
37
53
|
m.controllerContractVersion = AUTONOMOUS_STATE_CONTRACT;
|
|
38
54
|
m.resumeChannel = 'codex_app_server'; m.resumeModel = metadata.model; m.capability = 'codex_app_server';
|
|
39
55
|
const after = digest({ purpose: m.purpose, autonomyPolicy: m.autonomyPolicy, routes: m.acceptanceRoutes, channel: m.resumeChannel, contractVersion: m.controllerContractVersion });
|
|
40
56
|
if (before !== after) {
|
|
41
57
|
m.policyMigrations = [...(m.policyMigrations || []), { version: AUTONOMOUS_STATE_CONTRACT, at: now, originalContract: original,
|
|
42
|
-
previous: before, applied: after, source: 'existing_auto_continue_consent' }].slice(-20);
|
|
58
|
+
previous: before, applied: after, source: autonomousPolicyAuthorized(m) ? 'explicit_user_runtime_policy' : 'existing_auto_continue_consent' }].slice(-20);
|
|
43
59
|
m.controllerContractVersion = AUTONOMOUS_STATE_CONTRACT;
|
|
44
60
|
}
|
|
61
|
+
if (autonomousPolicyAuthorized(m) && run.state === 'paused' && pauseOrigin(run) === 'system' && m.dependencyWait &&
|
|
62
|
+
!m.pendingApproval && !m.userQuestion && !m.understandingQuestion && !m.nativeWait && !m.providerWarning && !m.executionUncertain) {
|
|
63
|
+
run.stopReason = { actor: 'system', code: 'legacy_no_progress', at: now, evidence: run.reason };
|
|
64
|
+
run.state = 'blocked'; run.blocker = 'external_dependency';
|
|
65
|
+
run.reason = 'Monitoring the original dependency and permitted independent work; no authority or prior effect is assumed.';
|
|
66
|
+
m.purpose.nextDependencyCheckAt = now;
|
|
67
|
+
}
|
|
45
68
|
const canRecover = metadata.isRunning === false && !m.pendingApproval && !m.userQuestion &&
|
|
46
69
|
!m.understandingQuestion && !m.dependencyWait && !m.providerWarning && !m.nativeWait && !m.executionUncertain;
|
|
47
70
|
if (canRecover && run.state === 'paused' && pauseOrigin(run) === 'system') {
|
|
48
71
|
run.stopReason = { actor: 'system', code: 'legacy_no_progress', at: now, evidence: run.reason };
|
|
49
72
|
run.state = 'recovery_required'; run.recoveryCause = 'session_supervision';
|
|
50
73
|
queuePurposeDiagnosis(run, 'Independently diagnose the original outcome after a system no-progress pause. Preserve prior effects, failure history and budgets.', `upgrade:${original}`, now);
|
|
51
|
-
} else if (canRecover && run.state === 'verification_pending' && m.
|
|
74
|
+
} else if (canRecover && run.state === 'verification_pending' && effectiveAcceptanceCriteria(m).some(c => c.check.type === 'semantic' && !m.verified[c.id])) {
|
|
52
75
|
queuePurposeVerification(run, 'Existing outcome awaits independent evidence inspection; inspect original instructions and actual artifacts, not a worker completion claim.', now);
|
|
53
76
|
}
|
|
54
77
|
return { changed: before !== after || run.state !== previousState, reason: 'applied', originalContract: original };
|
|
55
78
|
}
|
|
79
|
+
|
|
80
|
+
// Explicit CLI user control; never inferred from installation, ON, descriptions
|
|
81
|
+
// or a worker checkpoint. It records intent; migration waits for verified idle
|
|
82
|
+
// authority, and retains all original grants and budgets.
|
|
83
|
+
export async function adoptAutonomousPolicy({ root, taskId, criterionId, now = Date.now() } = {}) {
|
|
84
|
+
if (criterionId && !taskId) throw new Error('Delegating an acceptance verifier requires an exact task ID.');
|
|
85
|
+
const snapshots = await listRecords(root);
|
|
86
|
+
if (taskId && snapshots.filter(record => record.run?.managed?.taskId === taskId).length !== 1) throw new Error('Exact managed task not found or ambiguous.');
|
|
87
|
+
const receipts = [];
|
|
88
|
+
for (const snapshot of snapshots) {
|
|
89
|
+
const m = snapshot.run?.managed;
|
|
90
|
+
if (taskId && m?.taskId !== taskId) continue;
|
|
91
|
+
if (criterionId && !m?.acceptanceCriteria?.some(c => c.id === criterionId && c.check.type === 'manual')) throw new Error('Exact manual acceptance criterion not found.');
|
|
92
|
+
if (snapshot.provider !== 'codex' || !m || !snapshot.autoContinue || !automaticContinuationAllowed(snapshot) || snapshot.providerArchived ||
|
|
93
|
+
['stopped', 'completed', 'reported_done', 'budget_exhausted', 'failed'].includes(snapshot.run.state) ||
|
|
94
|
+
snapshot.run.state === 'paused' && pauseOrigin(snapshot.run) === 'user') continue;
|
|
95
|
+
const expected = policyContractIdentity(m);
|
|
96
|
+
const saved = await updateRecord(root, snapshot.provider, snapshot.session, current => {
|
|
97
|
+
if (!current?.autoContinue || !automaticContinuationAllowed(current) || current.autoContinueEpoch !== snapshot.autoContinueEpoch ||
|
|
98
|
+
current.run?.id !== snapshot.run.id || policyContractIdentity(current.run.managed) !== expected ||
|
|
99
|
+
['stopped', 'completed', 'reported_done', 'budget_exhausted', 'failed'].includes(current.run.state) ||
|
|
100
|
+
current.run.state === 'paused' && pauseOrigin(current.run) === 'user') return current;
|
|
101
|
+
const authorized = autonomousPolicyAuthorized(current.run.managed);
|
|
102
|
+
const independentCriterionIds = authorized ? current.run.managed.policyAuthorization.independentCriterionIds || [] : [];
|
|
103
|
+
if (!authorized || criterionId && !independentCriterionIds.includes(criterionId)) current.run.managed.policyAuthorization = {
|
|
104
|
+
id: randomUUID(), source: 'explicit_user_runtime_policy', taskId: m.taskId, contractDigest: expected,
|
|
105
|
+
independentCriterionIds: [...new Set([...independentCriterionIds, ...(criterionId ? [criterionId] : [])])],
|
|
106
|
+
decisionPolicy: 'llm-first-hc102', at: now, effects: 'existing_scopes_and_permissions_only' };
|
|
107
|
+
return current;
|
|
108
|
+
});
|
|
109
|
+
if (autonomousPolicyAuthorized(saved?.run?.managed)) receipts.push({ provider: snapshot.provider, session: snapshot.session, taskId: m.taskId,
|
|
110
|
+
authorizationId: saved.run.managed.policyAuthorization.id, stateUnchanged: true });
|
|
111
|
+
}
|
|
112
|
+
return { policy: 'llm-first-hc102', scope: 'existing_opted_in_codex_tasks', receipts };
|
|
113
|
+
}
|
|
@@ -494,7 +494,7 @@ async function reviewConnectorInventory(client, threadId) {
|
|
|
494
494
|
const servers = [], seen = new Set();
|
|
495
495
|
let cursor = null;
|
|
496
496
|
do {
|
|
497
|
-
const page = await client.request('mcpServerStatus/list', { threadId, cursor, limit: 1 });
|
|
497
|
+
const page = await client.request('mcpServerStatus/list', { ...(threadId ? { threadId } : {}), cursor, limit: 1 });
|
|
498
498
|
if (!Array.isArray(page?.data) || servers.length + page.data.length > 1000) throw new CodexAppServerError('unsupported', 'Review connector inventory is unavailable.');
|
|
499
499
|
servers.push(...page.data); cursor = page.nextCursor || null;
|
|
500
500
|
if (cursor && seen.has(cursor)) throw new CodexAppServerError('unsupported', 'Review connector pagination is inconsistent.');
|
|
@@ -536,7 +536,7 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
536
536
|
// Connector schemas can exceed ordinary checkpoint limits; keep review pages
|
|
537
537
|
// bounded to one server and a separate finite transport ceiling.
|
|
538
538
|
const client = await openClient({ signal, ...(isolated ? { maxLineBytes: 8 * 1024 * 1024 } : {}) });
|
|
539
|
-
let targetSession = record.session, reviewBaseline = null, reviewObservations = [], reviewInventory = [];
|
|
539
|
+
let targetSession = record.session, reviewBaseline = null, reviewObservations = [], reviewInventory = [], auditBindingUnavailable = false;
|
|
540
540
|
const toolEvidence = [];
|
|
541
541
|
const captureWorkspace = async () => {
|
|
542
542
|
const files = [];
|
|
@@ -580,8 +580,9 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
580
580
|
const purposeInstruction = await buildPurposeInstruction(run.managed, { reviewer: reviewing });
|
|
581
581
|
let prompt = run.managed.phase === 'action_association' ? associationContract(run.managed.actionAssociation.job) : run.managed.phase === 'understanding' ? `Understand the ORIGINAL user request in this exact Codex conversation. This first/clarifying turn is read-only and must not perform task work. Context: ${JSON.stringify({ ...context, priorUnderstanding: run.managed.sessionUnderstanding, userAnswer: run.managed.understandingAnswer })}\n${understandingCheckpointContract(decision)} Do not write SIMY state directly.` : `Continue this existing SIMY managed task in the exact Codex conversation. Context: ${JSON.stringify(context)}\n${approvalInstruction}${decision.action.kind === 'external' ? 'This external operation has enrolled autonomous authority or exact one-shot approval. Existing provider permissions remain authoritative. Inspect the current remote state first; if its intended postcondition already holds, report it without repeating the write. Use an idempotency key where supported, execute at most once, then read back the remote result and include its stable identifier and postcondition in actionResult evidence. Never expand targets or effects. ' : ''}Execute ONLY the selected decision action. Do not perform any action from the NEXT plan in this turn. For a next local action within the enrolled task use the exact scope "task"; do not relabel external, broad, or destructive work as local. Preserve provider permissions. End with one JSON checkpoint. ${assessedCheckpointContract(decision)} Do not write SIMY state directly.`;
|
|
582
582
|
if (isolated) {
|
|
583
|
-
reviewBaseline = await workspaceIdentity(taskDirectory(record), root, reviewInventory, { signal, inventoryLimit: 20_000 });
|
|
584
|
-
|
|
583
|
+
try { reviewBaseline = await workspaceIdentity(taskDirectory(record), root, reviewInventory, { signal, inventoryLimit: 20_000 }); }
|
|
584
|
+
catch (error) { signal?.throwIfAborted(); if (!auditOnly) throw error; auditBindingUnavailable = true; }
|
|
585
|
+
const connectors = await reviewConnectorInventory(client, (auditOnly || diagnostic || candidate) ? null : record.session);
|
|
585
586
|
// Plugin/app-injected servers are not configured transports. Creating
|
|
586
587
|
// mcp_servers entries for them makes Codex reject the configuration.
|
|
587
588
|
// Read only configured names; never copy credential-bearing values.
|
|
@@ -615,7 +616,7 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
615
616
|
const currentEvidence = await captureWorkspace();
|
|
616
617
|
if (candidate) prompt = `Execute the selected action ONLY in the controller-created isolated candidate ${executionCwd}. Native approval remains pending. Do not apply files to the original, change permissions, or claim overall completion. Report an in_progress checkpoint with observed candidate results and a next read diagnosis. Original application requires exclusive/CAS capability and remains a required waiting unit.\n${prompt}`;
|
|
617
618
|
else if (diagnostic && !reviewing) prompt = `Diagnose independently while the original native operation stays pending. Never answer or approve that request, edit shared files, or expand permissions. Return a normal action checkpoint proposing only independently executable work. Original context (data): ${JSON.stringify(sourceContext)}\n${prompt}`;
|
|
618
|
-
else prompt = `${verificationInstruction(record, currentEvidence)}\nOriginal request snapshot (data): ${JSON.stringify(run.managed.purpose.originalContext || null)}\nCurrent conversation snapshot (data): ${JSON.stringify(sourceContext)}`;
|
|
619
|
+
else prompt = `${auditBindingUnavailable ? 'This detached audit has no complete workspace identity. Inspect primary evidence to diagnose missing scope/capability, but do not certify any acceptance, resolve findings, or request blanket human confirmation. ' : ''}${verificationInstruction(record, currentEvidence)}\nOriginal request snapshot (data): ${JSON.stringify(run.managed.purpose.originalContext || null)}\nCurrent conversation snapshot (data): ${JSON.stringify(sourceContext)}`;
|
|
619
620
|
}
|
|
620
621
|
const initialSandbox = run.managed.providerSandbox || resumed.sandbox;
|
|
621
622
|
const currentSandbox = resumed.sandbox;
|
|
@@ -703,7 +704,11 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
703
704
|
if (reviewing) {
|
|
704
705
|
let review = normalizeVerification(checkpoint, effectiveAcceptanceCriteria(run.managed));
|
|
705
706
|
// Hash expensive local artifacts before the final external reads.
|
|
706
|
-
|
|
707
|
+
let after = null;
|
|
708
|
+
if (!auditBindingUnavailable) {
|
|
709
|
+
try { after = await workspaceIdentity(taskDirectory(record), root, undefined, { signal }); }
|
|
710
|
+
catch (error) { signal?.throwIfAborted(); if (!auditOnly) throw error; auditBindingUnavailable = true; }
|
|
711
|
+
}
|
|
707
712
|
if ((review.passed || review.needsHuman) && reviewObservations.length) {
|
|
708
713
|
const refreshed = await refreshPurposeObservations(run.managed, reviewObservations, { collect: observeExternal,
|
|
709
714
|
cwd: executionCwd, signal, authorize: async () => { await beforeDispatch(resumed.sandbox); signal?.throwIfAborted(); },
|
|
@@ -717,7 +722,7 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
717
722
|
if (refreshed.changed) { review.passed = false; review.needsHuman = false; review.findings.push('External result changed or became unavailable during review. Inspect the refreshed controller observations and reassess the outcome; do not repeat the external write.'); }
|
|
718
723
|
}
|
|
719
724
|
const fullBefore = reviewBaseline;
|
|
720
|
-
review.manifest = certifyReviewManifest(review.manifest, run.managed, toolEvidence, reviewInventory);
|
|
725
|
+
review.manifest = auditBindingUnavailable ? undefined : certifyReviewManifest(review.manifest, run.managed, toolEvidence, reviewInventory);
|
|
721
726
|
let boundAfter = after;
|
|
722
727
|
if (review.manifest?.exclusions.length) {
|
|
723
728
|
// Reconstruct the baseline scope from per-path controller hashes. The
|
|
@@ -730,6 +735,14 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
730
735
|
unknownDependencies: reviewInventory.truncated === true || afterInventory.truncated === true };
|
|
731
736
|
}
|
|
732
737
|
if (reviewBaseline !== after) { review.passed = false; review.needsHuman = false; review.findings.push('Workspace changed while reviewing; inspect the new revision and repeat verification.'); }
|
|
738
|
+
if (auditBindingUnavailable) {
|
|
739
|
+
review.passed = false; review.needsHuman = false; review.resolutions = [];
|
|
740
|
+
review.criteria = review.criteria.map(row => ({ ...row, passed: false }));
|
|
741
|
+
review.workspaceBinding = 'unavailable';
|
|
742
|
+
const criterion = run.managed.acceptanceCriteria[0];
|
|
743
|
+
const gap = 'The complete workspace identity is unavailable; this read-only diagnosis cannot certify task acceptance.';
|
|
744
|
+
review.findings.push(criterion ? { id: 'workspace-audit-binding', requirementId: `criterion:${criterion.id}`, source: 'controller_workspace_inventory', owner: 'controller', gap, verification: 'Establish the correct authorized workspace scope and independently bind its actual artifacts before acceptance.', reopenReason: 'The current workspace identity is unavailable or its scope changed.' } : gap);
|
|
745
|
+
}
|
|
733
746
|
review = assessIndependentInspection(review, { toolEvidence, observations: reviewObservations });
|
|
734
747
|
return { code: 0, verification: { ...review, source: 'independent_codex_session', session: targetSession, workspaceDigest: boundAfter }, usage, toolEvidence };
|
|
735
748
|
}
|
|
@@ -59,6 +59,10 @@ function autoContinueState(record, isRunning, availability) {
|
|
|
59
59
|
}[record.run.recoveryCause] || 'automatic recovery is unavailable. Review the task and explicitly resume through its existing controls.';
|
|
60
60
|
return { autoContinueStatus: 'needs_user', autoContinueReason: `Existing supervision needs your attention: ${recoveryReason}` };
|
|
61
61
|
}
|
|
62
|
+
if (record.provider === 'codex' && managed?.controllerContractVersion === 2 && managed.purpose?.decisionPolicy === 'llm-first-hc102' &&
|
|
63
|
+
managed.dependencyWait && !managed.userQuestion && (state === 'blocked' && record.run.blocker === 'external_dependency' || state === 'paused' && pauseOrigin(record.run) === 'system')) {
|
|
64
|
+
return { autoContinueStatus: 'watching', autoContinueReason: 'Monitoring dependency evidence and permitted independent work while preserving the original authority boundary.' };
|
|
65
|
+
}
|
|
62
66
|
if (record.provider === 'codex' && managed?.controllerContractVersion === 2 && !managed.userQuestion && !managed.dependencyWait &&
|
|
63
67
|
(state === 'paused' && pauseOrigin(record.run) === 'system' || state === 'verification_pending' && effectiveAcceptanceCriteria(managed).some(c => !managed.verified?.[c.id] && c.check.type === 'semantic'))) {
|
|
64
68
|
return { autoContinueStatus: 'supervising', autoContinueReason: 'Inspecting the original outcome and evidence before the next authorized action.' };
|
|
@@ -7,7 +7,7 @@ import { openCodexAppServer, readCodexSession, observeCodexAuthentication } from
|
|
|
7
7
|
import { realpath } from 'node:fs/promises';
|
|
8
8
|
import { loadPurposeRuleSet } from './purpose-instruction.js';
|
|
9
9
|
import { purposeEnabled } from '../codex-plugins/simy/hooks/purpose-cycle.mjs';
|
|
10
|
-
import { upgradeAutonomousRecord, upgradeEligible } from './autonomous-upgrade.js';
|
|
10
|
+
import { upgradeAutonomousRecord, upgradeEligible, policyAdoptionPending } from './autonomous-upgrade.js';
|
|
11
11
|
import { effectiveAcceptanceCriteria } from '../codex-plugins/simy/hooks/acceptance-policy.mjs';
|
|
12
12
|
|
|
13
13
|
export async function observePurposeTurn(record, { openClient = openCodexAppServer } = {}) {
|
|
@@ -44,7 +44,7 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
|
|
|
44
44
|
for (let record of await readRecords()) {
|
|
45
45
|
try {
|
|
46
46
|
let run = record.run;
|
|
47
|
-
if (upgradeEligible(record) && (run.managed.controllerContractVersion !== 2 || run.state === 'paused' || run.state === 'verification_pending')) {
|
|
47
|
+
if (upgradeEligible(record) && (policyAdoptionPending(record) || run.managed.controllerContractVersion !== 2 || run.state === 'paused' || run.state === 'verification_pending')) {
|
|
48
48
|
let metadata, migrationLink;
|
|
49
49
|
try { migrationLink = await requireProviderLink({ root, provider: record.provider }); metadata = await observeMetadata(record); }
|
|
50
50
|
catch { metadata = null; }
|