From 6d49053c505c375ad6bbb1ab7ecaf380c00ea3ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuncay=20=C3=96lmez?= Date: Tue, 21 Jul 2026 12:10:00 +0300 Subject: [PATCH 1/4] feat: persist native host identity bindings --- package.json | 5 +- src/integration/binding.js | 321 ++++++++++++++++++++ tests/contracts/integration-binding.js | 147 +++++++++ tests/contracts/integration-capabilities.js | 14 +- 4 files changed, 479 insertions(+), 8 deletions(-) create mode 100644 src/integration/binding.js create mode 100644 tests/contracts/integration-binding.js diff --git a/package.json b/package.json index 6349bf9..45bba3d 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, "scripts": { "test": "npm run test:contracts && npm run smoke", - "test:contracts": "npm run test:init-install && npm run test:adapter-profile && npm run test:doctor-json && npm run test:operator-json && npm run test:hook-output && npm run test:skill-format && npm run test:ownership-gates && npm run test:fixtures && npm run test:plugin-package && npm run test:supervised-intake && npm run test:supervised-proposal && npm run test:supervised-controls && npm run test:supervised-execution && npm run test:supervised-review && npm run test:supervised-failure && npm run test:supervised-linear-resume && npm run test:supervised-demo && npm run test:workflow-definition && npm run test:workflow-compiler && npm run test:workflow-proposal && npm run test:workflow-scheduler && npm run test:workflow-worker-matrix && npm run test:workflow-result && npm run test:workflow-failure-result && npm run test:workflow-state-machine && npm run test:workflow-interventions && npm run test:workflow-failure-matrix && npm run test:workflow-budget && npm run test:workflow-progress && npm run test:workflow-review && npm run test:workflow-checkpoint-review && npm run test:workflow-lifecycle && npm run test:workflow-revision && npm run test:workflow-migration && npm run test:workflow-templates && npm run test:workflow-docs && npm run test:workflow-release && npm run test:integration-capabilities", + "test:contracts": "npm run test:init-install && npm run test:adapter-profile && npm run test:doctor-json && npm run test:operator-json && npm run test:hook-output && npm run test:skill-format && npm run test:ownership-gates && npm run test:fixtures && npm run test:plugin-package && npm run test:supervised-intake && npm run test:supervised-proposal && npm run test:supervised-controls && npm run test:supervised-execution && npm run test:supervised-review && npm run test:supervised-failure && npm run test:supervised-linear-resume && npm run test:supervised-demo && npm run test:workflow-definition && npm run test:workflow-compiler && npm run test:workflow-proposal && npm run test:workflow-scheduler && npm run test:workflow-worker-matrix && npm run test:workflow-result && npm run test:workflow-failure-result && npm run test:workflow-state-machine && npm run test:workflow-interventions && npm run test:workflow-failure-matrix && npm run test:workflow-budget && npm run test:workflow-progress && npm run test:workflow-review && npm run test:workflow-checkpoint-review && npm run test:workflow-lifecycle && npm run test:workflow-revision && npm run test:workflow-migration && npm run test:workflow-templates && npm run test:workflow-docs && npm run test:workflow-release && npm run test:integration-capabilities && npm run test:integration-binding", "test:init-install": "node tests/contracts/init-install.js", "test:adapter-profile": "node tests/contracts/adapter-profile.js", "test:doctor-json": "node tests/contracts/doctor-json.js", @@ -89,13 +89,14 @@ "test:workflow-docs": "node tests/contracts/workflow-docs.js", "test:workflow-release": "node tests/contracts/workflow-release.js", "test:integration-capabilities": "node tests/contracts/integration-capabilities.js", + "test:integration-binding": "node tests/contracts/integration-binding.js", "demo:supervised": "node src/demo/supervised.js", "test:plugin-lifecycle": "node tests/capabilities/plugin-lifecycle.js", "test:clean-install": "node tests/capabilities/clean-install.js", "probe:codex-app-server": "node tests/capabilities/codex-app-server.js", "smoke": "node tests/harness/run-smoke.js", "check:cli": "node ./bin/cewp.js --help", - "check:syntax": "node --check ./bin/cewp.js && node --check ./src/demo/fake-codex.js && node --check ./src/demo/supervised.js && node --check ./src/integration/capabilities.js && node --check ./src/skills/install.js && node --check ./src/skills/format.js && node --check ./src/skills/doctor-report.js && node --check ./src/run/ownership.js && node --check ./src/run/control-gates.js && node --check ./src/supervise/budget.js && node --check ./src/supervise/cli.js && node --check ./src/supervise/commands.js && node --check ./src/supervise/controls.js && node --check ./src/supervise/execution.js && node --check ./src/supervise/profiles.js && node --check ./src/supervise/receipt.js && node --check ./src/supervise/review.js && node --check ./src/supervise/state.js && node --check ./src/supervise/test-authoring.js && node --check ./src/supervise/verification.js && node --check ./src/workflow/cli.js && node --check ./src/workflow/definition.js && node --check ./src/workflow/graph.js && node --check ./src/workflow/result.js && node --check ./src/workflow/scheduler.js && node --check ./src/workflow/source.js && node --check ./src/workflow/state.js && node --check ./src/workflow/transitions.js && node --check ./tests/contracts/init-install.js && node --check ./tests/contracts/adapter-profile.js && node --check ./tests/contracts/doctor-json.js && node --check ./tests/contracts/operator-json.js && node --check ./tests/contracts/hook-output.js && node --check ./tests/contracts/skill-format.js && node --check ./tests/contracts/ownership-gates.js && node --check ./tests/contracts/deterministic-fixtures.js && node --check ./tests/contracts/plugin-package.js && node --check ./tests/contracts/supervised-intake.js && node --check ./tests/contracts/supervised-proposal.js && node --check ./tests/contracts/supervised-controls.js && node --check ./tests/contracts/supervised-execution.js && node --check ./tests/contracts/supervised-review.js && node --check ./tests/contracts/supervised-failure.js && node --check ./tests/contracts/supervised-linear-resume.js && node --check ./tests/contracts/supervised-demo.js && node --check ./tests/contracts/integration-capabilities.js && node --check ./tests/contracts/workflow-definition.js && node --check ./tests/contracts/workflow-proposal.js && node --check ./tests/contracts/workflow-scheduler.js && node --check ./tests/contracts/workflow-result.js && node --check ./tests/contracts/workflow-state-machine.js && node --check ./tests/contracts/workflow-interventions.js && node --check ./tests/capabilities/clean-install.js && node --check ./tests/capabilities/plugin-lifecycle.js && node --check ./tests/capabilities/codex-app-server.js && node --check ./tests/capabilities/fixtures/deny-shell-hook.js", + "check:syntax": "node --check ./bin/cewp.js && node --check ./src/demo/fake-codex.js && node --check ./src/demo/supervised.js && node --check ./src/integration/capabilities.js && node --check ./src/integration/binding.js && node --check ./src/skills/install.js && node --check ./src/skills/format.js && node --check ./src/skills/doctor-report.js && node --check ./src/run/ownership.js && node --check ./src/run/control-gates.js && node --check ./src/supervise/budget.js && node --check ./src/supervise/cli.js && node --check ./src/supervise/commands.js && node --check ./src/supervise/controls.js && node --check ./src/supervise/execution.js && node --check ./src/supervise/profiles.js && node --check ./src/supervise/receipt.js && node --check ./src/supervise/review.js && node --check ./src/supervise/state.js && node --check ./src/supervise/test-authoring.js && node --check ./src/supervise/verification.js && node --check ./src/workflow/cli.js && node --check ./src/workflow/definition.js && node --check ./src/workflow/graph.js && node --check ./src/workflow/result.js && node --check ./src/workflow/scheduler.js && node --check ./src/workflow/source.js && node --check ./src/workflow/state.js && node --check ./src/workflow/transitions.js && node --check ./tests/contracts/init-install.js && node --check ./tests/contracts/adapter-profile.js && node --check ./tests/contracts/doctor-json.js && node --check ./tests/contracts/operator-json.js && node --check ./tests/contracts/hook-output.js && node --check ./tests/contracts/skill-format.js && node --check ./tests/contracts/ownership-gates.js && node --check ./tests/contracts/deterministic-fixtures.js && node --check ./tests/contracts/plugin-package.js && node --check ./tests/contracts/supervised-intake.js && node --check ./tests/contracts/supervised-proposal.js && node --check ./tests/contracts/supervised-controls.js && node --check ./tests/contracts/supervised-execution.js && node --check ./tests/contracts/supervised-review.js && node --check ./tests/contracts/supervised-failure.js && node --check ./tests/contracts/supervised-linear-resume.js && node --check ./tests/contracts/supervised-demo.js && node --check ./tests/contracts/integration-capabilities.js && node --check ./tests/contracts/integration-binding.js && node --check ./tests/contracts/workflow-definition.js && node --check ./tests/contracts/workflow-proposal.js && node --check ./tests/contracts/workflow-scheduler.js && node --check ./tests/contracts/workflow-result.js && node --check ./tests/contracts/workflow-state-machine.js && node --check ./tests/contracts/workflow-interventions.js && node --check ./tests/capabilities/clean-install.js && node --check ./tests/capabilities/plugin-lifecycle.js && node --check ./tests/capabilities/codex-app-server.js && node --check ./tests/capabilities/fixtures/deny-shell-hook.js", "check:workflow-syntax": "node --check ./src/workflow/budget.js && node --check ./src/workflow/compiler.js && node --check ./src/workflow/migration.js && node --check ./src/workflow/progress.js && node --check ./src/workflow/proposal.js && node --check ./src/workflow/review.js && node --check ./src/workflow/revision.js && node --check ./src/workflow/templates.js && node --check ./tests/contracts/workflow-budget.js && node --check ./tests/contracts/workflow-checkpoint-review.js && node --check ./tests/contracts/workflow-compiler.js && node --check ./tests/contracts/workflow-docs.js && node --check ./tests/contracts/workflow-failure-matrix.js && node --check ./tests/contracts/workflow-failure-result.js && node --check ./tests/contracts/workflow-migration.js && node --check ./tests/contracts/workflow-progress.js && node --check ./tests/contracts/workflow-release.js && node --check ./tests/contracts/workflow-review.js && node --check ./tests/contracts/workflow-lifecycle.js && node --check ./tests/contracts/workflow-revision.js && node --check ./tests/contracts/workflow-templates.js && node --check ./tests/contracts/workflow-worker-matrix.js", "check": "npm run check:syntax && npm run check:workflow-syntax && npm test", "pack:dry-run": "npm pack --dry-run" diff --git a/src/integration/binding.js b/src/integration/binding.js new file mode 100644 index 0000000..a9027b4 --- /dev/null +++ b/src/integration/binding.js @@ -0,0 +1,321 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { + CODEX_INTEGRATION_CAPABILITIES_SCHEMA_VERSION, + selectManagedBackend, + validateCodexCapabilitySnapshot, +} = require("./capabilities"); +const { writeJsonAtomic } = require("../workflow/state"); + +const HOST_BINDING_SCHEMA_VERSION = "host-binding/v1"; +const GENERATED_GOAL_BRIEF_SCHEMA_VERSION = "generated-goal-brief/v1"; +const HOST_SURFACES = Object.freeze([ + "chatgpt-desktop", + "codex-cli", + "ide", + "app-server", + "external-client", +]); +const BINDING_MODES = Object.freeze([ + "explicit-intake", + "attached", + "managed-backend", + "audit-import", +]); +const PROVENANCE_MODES = Object.freeze({ + "explicit-intake": "explicit-intake", + "plugin-observed": "attached", + "managed-backend": "managed-backend", + "imported-audit": "audit-import", +}); +const CONTROL_CLASSES = Object.freeze([ + "preventive", + "postExecution", + "imported", + "unavailable", +]); + +function isObject(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function requiredText(value, label, maximum = 1024) { + if (typeof value !== "string" || value.trim().length === 0) { + throw new Error(`Invalid host binding: ${label} is required.`); + } + const text = value.trim(); + if (text.length > maximum || /[\u0000-\u001f]/.test(text)) { + throw new Error(`Invalid host binding: ${label} is too long or contains control characters.`); + } + return text; +} + +function optionalText(value, label, maximum) { + return value === null ? null : requiredText(value, label, maximum); +} + +function normalizeTimestamp(value, label) { + const timestamp = requiredText(value, label); + if (!Number.isFinite(Date.parse(timestamp))) { + throw new Error(`Invalid host binding: ${label} must be an ISO timestamp.`); + } + return new Date(timestamp).toISOString(); +} + +function normalizeStringList(value, label) { + if (!Array.isArray(value)) throw new Error(`Invalid host binding: ${label} must be an array.`); + const entries = value.map((entry, index) => requiredText(entry, `${label}[${index}]`, 128)); + if (new Set(entries).size !== entries.length) { + throw new Error(`Invalid host binding: ${label} must not contain duplicates.`); + } + return entries; +} + +function normalizeExecution(value) { + if (!isObject(value)) throw new Error("Invalid host binding: execution is required."); + const owner = requiredText(value.owner, "execution.owner", 32); + if (!["managed", "native", "audit-only"].includes(owner)) { + throw new Error(`Invalid host binding: unsupported execution owner ${owner}.`); + } + const backend = value.backend === null ? null : requiredText(value.backend, "execution.backend", 64); + if (owner === "managed" && !["codex-exec", "app-server"].includes(backend)) { + throw new Error("Invalid host binding: managed execution requires codex-exec or app-server."); + } + if (owner !== "managed" && backend !== null) { + throw new Error(`Invalid host binding: ${owner} execution cannot claim a managed backend.`); + } + return { owner, backend }; +} + +function normalizeSubagents(value) { + if (!Array.isArray(value) || value.length > 32) { + throw new Error("Invalid host binding: references.subagents must contain at most 32 entries."); + } + const subagents = value.map((entry, index) => { + if (!isObject(entry)) throw new Error(`Invalid host binding: references.subagents[${index}] is invalid.`); + return { + id: requiredText(entry.id, `references.subagents[${index}].id`, 512), + threadId: optionalText(entry.threadId, `references.subagents[${index}].threadId`, 512), + summary: optionalText(entry.summary, `references.subagents[${index}].summary`, 4000), + }; + }); + if (new Set(subagents.map((entry) => entry.id)).size !== subagents.length) { + throw new Error("Invalid host binding: subagent ids must be unique."); + } + return subagents; +} + +function normalizeWorktree(value) { + if (value === null) return null; + if (!isObject(value)) throw new Error("Invalid host binding: references.worktree must be an object or null."); + return { + id: requiredText(value.id, "references.worktree.id", 512), + path: requiredText(value.path, "references.worktree.path", 4096), + }; +} + +function validateHostBinding(value, found) { + if (!isObject(value) || value.schemaVersion !== HOST_BINDING_SCHEMA_VERSION) { + throw new Error(`Invalid host binding: expected ${HOST_BINDING_SCHEMA_VERSION}.`); + } + if (!isObject(value.workflow)) throw new Error("Invalid host binding: workflow is required."); + const workflow = { + runId: requiredText(value.workflow.runId, "workflow.runId", 128), + taskId: optionalText(value.workflow.taskId, "workflow.taskId", 64), + checkpointId: optionalText(value.workflow.checkpointId, "workflow.checkpointId", 128), + }; + if (found) { + if (workflow.runId !== found.run.runId) { + throw new Error("Invalid host binding: workflow run id does not match the loaded run."); + } + const runtimeTask = workflow.taskId + ? found.run.tasks.find((task) => task.id === workflow.taskId) + : null; + if (workflow.taskId && !runtimeTask) { + throw new Error(`Invalid host binding: workflow task not found: ${workflow.taskId}.`); + } + if (workflow.checkpointId && (!runtimeTask || runtimeTask.activeCheckpointId !== workflow.checkpointId)) { + throw new Error("Invalid host binding: checkpoint id does not match the active workflow checkpoint."); + } + } + + const execution = normalizeExecution(value.execution); + if ( + found + && (execution.owner !== found.run.execution.owner || execution.backend !== found.run.execution.backend) + ) { + throw new Error("Invalid host binding: execution does not match workflow execution owner and backend."); + } + if (!isObject(value.host)) throw new Error("Invalid host binding: host is required."); + const surface = requiredText(value.host.surface, "host.surface", 64); + if (!HOST_SURFACES.includes(surface)) { + throw new Error(`Invalid host binding: unsupported host surface ${surface}.`); + } + const host = { + product: requiredText(value.host.product, "host.product", 64), + surface, + version: optionalText(value.host.version, "host.version", 256), + }; + + const mode = requiredText(value.mode, "mode", 32); + if (!BINDING_MODES.includes(mode)) throw new Error(`Invalid host binding: unsupported mode ${mode}.`); + if (!isObject(value.provenance)) throw new Error("Invalid host binding: provenance is required."); + const provenanceKind = requiredText(value.provenance.kind, "provenance.kind", 32); + if (PROVENANCE_MODES[provenanceKind] !== mode) { + throw new Error(`Invalid host binding: provenance ${provenanceKind} cannot claim mode ${mode}.`); + } + const provenance = { + kind: provenanceKind, + capabilitySchemaVersion: optionalText( + value.provenance.capabilitySchemaVersion, + "provenance.capabilitySchemaVersion", + 256, + ), + authenticationBoundary: requiredText( + value.provenance.authenticationBoundary, + "provenance.authenticationBoundary", + 256, + ), + recordedAt: normalizeTimestamp(value.provenance.recordedAt, "provenance.recordedAt"), + }; + + if (!isObject(value.references)) throw new Error("Invalid host binding: references are required."); + const references = { + goalId: optionalText(value.references.goalId, "references.goalId", 512), + threadId: optionalText(value.references.threadId, "references.threadId", 512), + turnId: optionalText(value.references.turnId, "references.turnId", 512), + subagents: normalizeSubagents(value.references.subagents), + worktree: normalizeWorktree(value.references.worktree), + }; + if ( + !references.goalId + && !references.threadId + && !references.turnId + && references.subagents.length === 0 + && !references.worktree + ) { + throw new Error("Invalid host binding: at least one host reference is required."); + } + + if (!isObject(value.controls)) throw new Error("Invalid host binding: controls are required."); + const controls = Object.fromEntries( + CONTROL_CLASSES.map((name) => [name, normalizeStringList(value.controls[name], `controls.${name}`)]), + ); + + return { + schemaVersion: HOST_BINDING_SCHEMA_VERSION, + workflow, + execution, + host, + mode, + provenance, + references, + controls, + }; +} + +function assertBindingCapability(binding, snapshot) { + const capabilities = validateCodexCapabilitySnapshot(snapshot); + if (binding.execution.owner === "managed" && binding.host.surface === "chatgpt-desktop") { + throw new Error("Invalid host binding: managed execution cannot bind to the ChatGPT desktop internal session."); + } + if (binding.provenance.kind === "plugin-observed") { + if ( + capabilities.surfaces.hostObservation.status !== "supported" + || capabilities.surfaces.hostObservation.pluginPathCapabilityTestPassed !== true + ) { + throw new Error( + "Invalid host binding: plugin path has not passed host observation capability tests.", + ); + } + if (binding.provenance.capabilitySchemaVersion !== capabilities.schemaVersion) { + throw new Error("Invalid host binding: plugin capability schema version does not match the probe."); + } + } + if (binding.provenance.kind === "explicit-intake" && binding.execution.owner !== "native") { + throw new Error("Invalid host binding: explicit native intake requires native execution ownership."); + } + if (binding.provenance.kind === "imported-audit" && binding.execution.owner !== "audit-only") { + throw new Error("Invalid host binding: imported audit evidence requires audit-only ownership."); + } + if (binding.provenance.kind === "managed-backend") { + if (binding.execution.owner !== "managed") { + throw new Error("Invalid host binding: managed backend provenance requires managed ownership."); + } + const selected = selectManagedBackend(capabilities, binding.execution.backend); + if (selected !== binding.execution.backend) { + throw new Error(`Invalid host binding: managed backend ${binding.execution.backend} is not graduated.`); + } + } +} + +function bindingPath(found) { + return path.join(found.runRoot, "integration", "host-binding.json"); +} + +function createHostBinding(found, candidate, options = {}) { + const binding = validateHostBinding(candidate, found); + if (!options.capabilities) throw new Error("Host binding requires a versioned capability snapshot."); + assertBindingCapability(binding, options.capabilities); + const filePath = bindingPath(found); + if (fs.existsSync(filePath) && options.replace !== true) { + throw new Error(`Host binding already exists for workflow run ${found.run.runId}.`); + } + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + writeJsonAtomic(filePath, binding); + return binding; +} + +function loadHostBinding(found) { + const filePath = bindingPath(found); + if (!fs.existsSync(filePath)) return null; + return validateHostBinding(JSON.parse(fs.readFileSync(filePath, "utf8")), found); +} + +function createGeneratedGoalBrief(found, taskId) { + if (found.run.execution.owner !== "native") { + throw new Error("Generated native goal briefs require native execution ownership."); + } + const task = taskId + ? found.run.tasks.find((entry) => entry.id === taskId) + : found.run.tasks.find((entry) => ["ready", "running", "verifying", "review-pending"].includes(entry.status)); + if (!task) throw new Error("No resumable native workflow task is available for a goal brief."); + const workflowTask = found.definition.tasks.find((entry) => entry.id === task.id); + const checkpointId = task.activeCheckpointId || null; + const checkpointText = checkpointId ? ` checkpoint ${checkpointId}` : " its next CEWP checkpoint"; + return { + schemaVersion: GENERATED_GOAL_BRIEF_SCHEMA_VERSION, + workflow: { + runId: found.run.runId, + taskId: task.id, + checkpointId, + workflowDigest: found.run.workflow.digest, + }, + objective: [ + `Continue CEWP workflow run ${found.run.runId}, task ${task.id},${checkpointText}.`, + `Stay inside these write scopes: ${workflowTask.allowedFiles.join(", ")}.`, + `Meet these observable stopping conditions: ${workflowTask.stoppingConditions.join("; ")}.`, + "Return explicit result and verification evidence to CEWP intake.", + "Do not mark the checkpoint complete or PASS without CEWP verification and reviewer gates.", + ].join(" "), + fallback: "explicit-intake", + claims: { + liveAttachment: false, + hostUsageObserved: false, + hostControlsEnforced: false, + }, + }; +} + +module.exports = { + BINDING_MODES, + GENERATED_GOAL_BRIEF_SCHEMA_VERSION, + HOST_BINDING_SCHEMA_VERSION, + HOST_SURFACES, + createGeneratedGoalBrief, + createHostBinding, + loadHostBinding, + validateHostBinding, +}; diff --git a/tests/contracts/integration-binding.js b/tests/contracts/integration-binding.js new file mode 100644 index 0000000..91da018 --- /dev/null +++ b/tests/contracts/integration-binding.js @@ -0,0 +1,147 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { assert } = require("../harness/lib/assertions"); +const { cleanupRepo, makeTempRepo } = require("../harness/lib/temp-repo"); +const { supportedSnapshot } = require("./integration-capabilities"); +const { validDefinition } = require("./workflow-definition"); +const { approveWorkflow } = require("./workflow-scheduler"); +const { + createGeneratedGoalBrief, + createHostBinding, + loadHostBinding, +} = require("../../src/integration/binding"); +const { loadWorkflowRun } = require("../../src/workflow/state"); + +function assertThrows(action, expected, label) { + let error; + try { + action(); + } catch (caught) { + error = caught; + } + assert(error, `${label}: expected an error`); + assert(expected.test(error.message), `${label}: unexpected error: ${error.message}`); +} + +function nativeDefinition() { + const definition = validDefinition(); + definition.workflowId = "native-integration"; + definition.execution = { + owner: "native", + backend: null, + allowedModes: ["supervised"], + }; + return definition; +} + +function explicitBinding(runId) { + return { + schemaVersion: "host-binding/v1", + workflow: { runId, taskId: null, checkpointId: null }, + execution: { owner: "native", backend: null }, + host: { product: "codex", surface: "chatgpt-desktop", version: null }, + mode: "explicit-intake", + provenance: { + kind: "explicit-intake", + capabilitySchemaVersion: null, + authenticationBoundary: "host-owned", + recordedAt: "2026-07-18T12:00:00.000Z", + }, + references: { + goalId: "goal-local-1", + threadId: "thread-local-1", + turnId: null, + subagents: [{ + id: "subagent-local-1", + threadId: "subthread-local-1", + summary: "Inspected the bounded native checkpoint.", + }], + worktree: null, + }, + controls: { + preventive: ["workflow-definition", "ownership-conflict"], + postExecution: [], + imported: ["goal-reference"], + unavailable: ["native-tool-enforcement", "host-usage"], + }, + }; +} + +function main() { + const repoRoot = makeTempRepo("cewp-integration-binding-"); + try { + const approved = approveWorkflow(repoRoot, nativeDefinition()); + const found = loadWorkflowRun(repoRoot, approved.runId); + const runBefore = fs.readFileSync(found.runPath, "utf8"); + + const brief = createGeneratedGoalBrief(found); + assert(brief.schemaVersion === "generated-goal-brief/v1", "native fallback has a versioned contract"); + assert(brief.workflow.runId === approved.runId, "goal brief carries CEWP run identity"); + assert(brief.fallback === "explicit-intake", "goal brief names the supported intake fallback"); + assert(brief.objective.includes(approved.runId), "goal objective tells the host which CEWP run to update"); + assert(!brief.claims.liveAttachment, "goal brief does not claim live attachment"); + + const created = createHostBinding(found, explicitBinding(approved.runId), { + capabilities: supportedSnapshot(), + }); + assert(created.mode === "explicit-intake", "explicit binding stays explicit intake"); + assert(created.references.goalId === "goal-local-1", "goal reference is preserved in sidecar"); + assert(created.references.subagents[0].threadId === "subthread-local-1", "subagent identity is preserved"); + assert(created.controls.unavailable.includes("host-usage"), "unavailable controls remain visible"); + assert(fs.readFileSync(found.runPath, "utf8") === runBefore, "host ids never mutate core workflow state"); + + const bindingPath = path.join(found.runRoot, "integration", "host-binding.json"); + assert(fs.existsSync(bindingPath), "host binding is persisted outside core run state"); + assert(loadHostBinding(found).workflow.runId === approved.runId, "persisted binding is readable"); + + const wrongOwner = explicitBinding(approved.runId); + wrongOwner.execution = { owner: "audit-only", backend: null }; + assertThrows( + () => createHostBinding(found, wrongOwner, { capabilities: supportedSnapshot() }), + /does not match workflow execution owner/, + "binding cannot change execution ownership", + ); + + const falseAttachment = explicitBinding(approved.runId); + falseAttachment.mode = "attached"; + falseAttachment.provenance.kind = "plugin-observed"; + falseAttachment.provenance.capabilitySchemaVersion = "codex-integration-capabilities/v1"; + assertThrows( + () => createHostBinding(found, falseAttachment, { capabilities: supportedSnapshot(), replace: true }), + /plugin path has not passed host observation capability tests/, + "plugin cannot claim a live native attachment without a capability proof", + ); + + const managedRepo = makeTempRepo("cewp-integration-managed-binding-"); + try { + const managed = approveWorkflow(managedRepo, validDefinition()); + const managedFound = loadWorkflowRun(managedRepo, managed.runId); + const unsafe = explicitBinding(managed.runId); + unsafe.execution = { owner: "managed", backend: "codex-exec" }; + assertThrows( + () => createHostBinding(managedFound, unsafe, { capabilities: supportedSnapshot() }), + /managed execution cannot bind to the ChatGPT desktop internal session/, + "managed execution never attaches to desktop internals", + ); + } finally { + cleanupRepo(managedRepo); + } + + const coreRun = JSON.parse(fs.readFileSync(found.runPath, "utf8")); + assert(coreRun.host === undefined && coreRun.references === undefined, "provider ids stay outside core schema"); + + console.log("[PASS] host identity sidecar preserves ownership and honest native fallback"); + } finally { + cleanupRepo(repoRoot); + } +} + +try { + main(); +} catch (error) { + console.error("[FAIL] host integration binding contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/integration-capabilities.js b/tests/contracts/integration-capabilities.js index 5ed1125..6679d4f 100644 --- a/tests/contracts/integration-capabilities.js +++ b/tests/contracts/integration-capabilities.js @@ -134,12 +134,14 @@ function main() { console.log("[PASS] Codex integration capability drift and backend decision stay truthful"); } -try { - main(); -} catch (error) { - console.error("[FAIL] Codex integration capability contract"); - console.error(error && error.stack ? error.stack : error); - process.exitCode = 1; +if (require.main === module) { + try { + main(); + } catch (error) { + console.error("[FAIL] Codex integration capability contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; + } } module.exports = { supportedSnapshot }; From 27cdc29a0690db1a27cb4c34449151ac05f555e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuncay=20=C3=96lmez?= Date: Tue, 21 Jul 2026 12:11:00 +0300 Subject: [PATCH 2/4] feat: record truthful host observations --- package.json | 5 +- src/integration/observation.js | 367 +++++++++++++++++++++ tests/contracts/integration-observation.js | 199 +++++++++++ 3 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 src/integration/observation.js create mode 100644 tests/contracts/integration-observation.js diff --git a/package.json b/package.json index 45bba3d..c48783b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, "scripts": { "test": "npm run test:contracts && npm run smoke", - "test:contracts": "npm run test:init-install && npm run test:adapter-profile && npm run test:doctor-json && npm run test:operator-json && npm run test:hook-output && npm run test:skill-format && npm run test:ownership-gates && npm run test:fixtures && npm run test:plugin-package && npm run test:supervised-intake && npm run test:supervised-proposal && npm run test:supervised-controls && npm run test:supervised-execution && npm run test:supervised-review && npm run test:supervised-failure && npm run test:supervised-linear-resume && npm run test:supervised-demo && npm run test:workflow-definition && npm run test:workflow-compiler && npm run test:workflow-proposal && npm run test:workflow-scheduler && npm run test:workflow-worker-matrix && npm run test:workflow-result && npm run test:workflow-failure-result && npm run test:workflow-state-machine && npm run test:workflow-interventions && npm run test:workflow-failure-matrix && npm run test:workflow-budget && npm run test:workflow-progress && npm run test:workflow-review && npm run test:workflow-checkpoint-review && npm run test:workflow-lifecycle && npm run test:workflow-revision && npm run test:workflow-migration && npm run test:workflow-templates && npm run test:workflow-docs && npm run test:workflow-release && npm run test:integration-capabilities && npm run test:integration-binding", + "test:contracts": "npm run test:init-install && npm run test:adapter-profile && npm run test:doctor-json && npm run test:operator-json && npm run test:hook-output && npm run test:skill-format && npm run test:ownership-gates && npm run test:fixtures && npm run test:plugin-package && npm run test:supervised-intake && npm run test:supervised-proposal && npm run test:supervised-controls && npm run test:supervised-execution && npm run test:supervised-review && npm run test:supervised-failure && npm run test:supervised-linear-resume && npm run test:supervised-demo && npm run test:workflow-definition && npm run test:workflow-compiler && npm run test:workflow-proposal && npm run test:workflow-scheduler && npm run test:workflow-worker-matrix && npm run test:workflow-result && npm run test:workflow-failure-result && npm run test:workflow-state-machine && npm run test:workflow-interventions && npm run test:workflow-failure-matrix && npm run test:workflow-budget && npm run test:workflow-progress && npm run test:workflow-review && npm run test:workflow-checkpoint-review && npm run test:workflow-lifecycle && npm run test:workflow-revision && npm run test:workflow-migration && npm run test:workflow-templates && npm run test:workflow-docs && npm run test:workflow-release && npm run test:integration-capabilities && npm run test:integration-binding && npm run test:integration-observation", "test:init-install": "node tests/contracts/init-install.js", "test:adapter-profile": "node tests/contracts/adapter-profile.js", "test:doctor-json": "node tests/contracts/doctor-json.js", @@ -90,13 +90,14 @@ "test:workflow-release": "node tests/contracts/workflow-release.js", "test:integration-capabilities": "node tests/contracts/integration-capabilities.js", "test:integration-binding": "node tests/contracts/integration-binding.js", + "test:integration-observation": "node tests/contracts/integration-observation.js", "demo:supervised": "node src/demo/supervised.js", "test:plugin-lifecycle": "node tests/capabilities/plugin-lifecycle.js", "test:clean-install": "node tests/capabilities/clean-install.js", "probe:codex-app-server": "node tests/capabilities/codex-app-server.js", "smoke": "node tests/harness/run-smoke.js", "check:cli": "node ./bin/cewp.js --help", - "check:syntax": "node --check ./bin/cewp.js && node --check ./src/demo/fake-codex.js && node --check ./src/demo/supervised.js && node --check ./src/integration/capabilities.js && node --check ./src/integration/binding.js && node --check ./src/skills/install.js && node --check ./src/skills/format.js && node --check ./src/skills/doctor-report.js && node --check ./src/run/ownership.js && node --check ./src/run/control-gates.js && node --check ./src/supervise/budget.js && node --check ./src/supervise/cli.js && node --check ./src/supervise/commands.js && node --check ./src/supervise/controls.js && node --check ./src/supervise/execution.js && node --check ./src/supervise/profiles.js && node --check ./src/supervise/receipt.js && node --check ./src/supervise/review.js && node --check ./src/supervise/state.js && node --check ./src/supervise/test-authoring.js && node --check ./src/supervise/verification.js && node --check ./src/workflow/cli.js && node --check ./src/workflow/definition.js && node --check ./src/workflow/graph.js && node --check ./src/workflow/result.js && node --check ./src/workflow/scheduler.js && node --check ./src/workflow/source.js && node --check ./src/workflow/state.js && node --check ./src/workflow/transitions.js && node --check ./tests/contracts/init-install.js && node --check ./tests/contracts/adapter-profile.js && node --check ./tests/contracts/doctor-json.js && node --check ./tests/contracts/operator-json.js && node --check ./tests/contracts/hook-output.js && node --check ./tests/contracts/skill-format.js && node --check ./tests/contracts/ownership-gates.js && node --check ./tests/contracts/deterministic-fixtures.js && node --check ./tests/contracts/plugin-package.js && node --check ./tests/contracts/supervised-intake.js && node --check ./tests/contracts/supervised-proposal.js && node --check ./tests/contracts/supervised-controls.js && node --check ./tests/contracts/supervised-execution.js && node --check ./tests/contracts/supervised-review.js && node --check ./tests/contracts/supervised-failure.js && node --check ./tests/contracts/supervised-linear-resume.js && node --check ./tests/contracts/supervised-demo.js && node --check ./tests/contracts/integration-capabilities.js && node --check ./tests/contracts/integration-binding.js && node --check ./tests/contracts/workflow-definition.js && node --check ./tests/contracts/workflow-proposal.js && node --check ./tests/contracts/workflow-scheduler.js && node --check ./tests/contracts/workflow-result.js && node --check ./tests/contracts/workflow-state-machine.js && node --check ./tests/contracts/workflow-interventions.js && node --check ./tests/capabilities/clean-install.js && node --check ./tests/capabilities/plugin-lifecycle.js && node --check ./tests/capabilities/codex-app-server.js && node --check ./tests/capabilities/fixtures/deny-shell-hook.js", + "check:syntax": "node --check ./bin/cewp.js && node --check ./src/demo/fake-codex.js && node --check ./src/demo/supervised.js && node --check ./src/integration/capabilities.js && node --check ./src/integration/binding.js && node --check ./src/integration/observation.js && node --check ./src/skills/install.js && node --check ./src/skills/format.js && node --check ./src/skills/doctor-report.js && node --check ./src/run/ownership.js && node --check ./src/run/control-gates.js && node --check ./src/supervise/budget.js && node --check ./src/supervise/cli.js && node --check ./src/supervise/commands.js && node --check ./src/supervise/controls.js && node --check ./src/supervise/execution.js && node --check ./src/supervise/profiles.js && node --check ./src/supervise/receipt.js && node --check ./src/supervise/review.js && node --check ./src/supervise/state.js && node --check ./src/supervise/test-authoring.js && node --check ./src/supervise/verification.js && node --check ./src/workflow/cli.js && node --check ./src/workflow/definition.js && node --check ./src/workflow/graph.js && node --check ./src/workflow/result.js && node --check ./src/workflow/scheduler.js && node --check ./src/workflow/source.js && node --check ./src/workflow/state.js && node --check ./src/workflow/transitions.js && node --check ./tests/contracts/init-install.js && node --check ./tests/contracts/adapter-profile.js && node --check ./tests/contracts/doctor-json.js && node --check ./tests/contracts/operator-json.js && node --check ./tests/contracts/hook-output.js && node --check ./tests/contracts/skill-format.js && node --check ./tests/contracts/ownership-gates.js && node --check ./tests/contracts/deterministic-fixtures.js && node --check ./tests/contracts/plugin-package.js && node --check ./tests/contracts/supervised-intake.js && node --check ./tests/contracts/supervised-proposal.js && node --check ./tests/contracts/supervised-controls.js && node --check ./tests/contracts/supervised-execution.js && node --check ./tests/contracts/supervised-review.js && node --check ./tests/contracts/supervised-failure.js && node --check ./tests/contracts/supervised-linear-resume.js && node --check ./tests/contracts/supervised-demo.js && node --check ./tests/contracts/integration-capabilities.js && node --check ./tests/contracts/integration-binding.js && node --check ./tests/contracts/integration-observation.js && node --check ./tests/contracts/workflow-definition.js && node --check ./tests/contracts/workflow-proposal.js && node --check ./tests/contracts/workflow-scheduler.js && node --check ./tests/contracts/workflow-result.js && node --check ./tests/contracts/workflow-state-machine.js && node --check ./tests/contracts/workflow-interventions.js && node --check ./tests/capabilities/clean-install.js && node --check ./tests/capabilities/plugin-lifecycle.js && node --check ./tests/capabilities/codex-app-server.js && node --check ./tests/capabilities/fixtures/deny-shell-hook.js", "check:workflow-syntax": "node --check ./src/workflow/budget.js && node --check ./src/workflow/compiler.js && node --check ./src/workflow/migration.js && node --check ./src/workflow/progress.js && node --check ./src/workflow/proposal.js && node --check ./src/workflow/review.js && node --check ./src/workflow/revision.js && node --check ./src/workflow/templates.js && node --check ./tests/contracts/workflow-budget.js && node --check ./tests/contracts/workflow-checkpoint-review.js && node --check ./tests/contracts/workflow-compiler.js && node --check ./tests/contracts/workflow-docs.js && node --check ./tests/contracts/workflow-failure-matrix.js && node --check ./tests/contracts/workflow-failure-result.js && node --check ./tests/contracts/workflow-migration.js && node --check ./tests/contracts/workflow-progress.js && node --check ./tests/contracts/workflow-release.js && node --check ./tests/contracts/workflow-review.js && node --check ./tests/contracts/workflow-lifecycle.js && node --check ./tests/contracts/workflow-revision.js && node --check ./tests/contracts/workflow-templates.js && node --check ./tests/contracts/workflow-worker-matrix.js", "check": "npm run check:syntax && npm run check:workflow-syntax && npm test", "pack:dry-run": "npm pack --dry-run" diff --git a/src/integration/observation.js b/src/integration/observation.js new file mode 100644 index 0000000..99e3f15 --- /dev/null +++ b/src/integration/observation.js @@ -0,0 +1,367 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { validateCodexCapabilitySnapshot } = require("./capabilities"); + +const HOST_OBSERVATION_SCHEMA_VERSION = "host-observation/v1"; +const SOURCE_PATHS = Object.freeze([ + "codex-exec", + "plugin", + "app-server", + "explicit-intake", + "audit-import", +]); +const OBSERVATION_CATEGORIES = Object.freeze([ + "thread-usage", + "rate-limit-window", + "credits", + "account-activity", + "goal-budget", + "goal-lifecycle", +]); +const AVAILABILITY = Object.freeze([ + "observed", + "imported", + "unknown", + "unavailable", + "stale", + "malformed", +]); +const ACCOUNT_CATEGORIES = new Set(["rate-limit-window", "credits", "account-activity"]); +const WORKFLOW_CATEGORIES = new Set(["thread-usage", "goal-budget", "goal-lifecycle"]); +const RAW_LIMIT_BYTES = 64 * 1024; + +function isObject(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function requiredText(value, label, maximum = 1024) { + if (typeof value !== "string" || value.trim().length === 0) { + throw new Error(`Invalid host observation: ${label} is required.`); + } + const text = value.trim(); + if (text.length > maximum || /[\u0000-\u001f]/.test(text)) { + throw new Error(`Invalid host observation: ${label} is too long or contains control characters.`); + } + return text; +} + +function optionalText(value, label, maximum = 1024) { + return value === null ? null : requiredText(value, label, maximum); +} + +function timestamp(value, label) { + const text = requiredText(value, label, 128); + if (!Number.isFinite(Date.parse(text))) { + throw new Error(`Invalid host observation: ${label} must be an ISO timestamp.`); + } + return new Date(text).toISOString(); +} + +function nonNegativeInteger(value, label) { + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error(`${label} must be a non-negative safe integer.`); + } + return value; +} + +function finiteNumber(value, label, minimum = 0, maximum = Number.MAX_SAFE_INTEGER) { + if (!Number.isFinite(value) || value < minimum || value > maximum) { + throw new Error(`${label} must be a number from ${minimum} to ${maximum}.`); + } + return value; +} + +function normalizeRaw(value) { + if (value === undefined) return null; + let json; + try { + json = JSON.stringify(value); + } catch { + throw new Error("Invalid host observation: raw data must be JSON serializable."); + } + if (json === undefined || Buffer.byteLength(json, "utf8") > RAW_LIMIT_BYTES) { + throw new Error(`Invalid host observation: raw data exceeds ${RAW_LIMIT_BYTES} bytes.`); + } + return JSON.parse(json); +} + +function normalizeSource(value) { + if (!isObject(value)) throw new Error("Invalid host observation: source is required."); + const sourcePath = requiredText(value.path, "source.path", 64); + if (!SOURCE_PATHS.includes(sourcePath)) { + throw new Error(`Invalid host observation: unsupported source path ${sourcePath}.`); + } + return { + path: sourcePath, + codexVersion: optionalText(value.codexVersion, "source.codexVersion", 256), + schemaVersion: requiredText(value.schemaVersion, "source.schemaVersion", 256), + authenticationBoundary: requiredText( + value.authenticationBoundary, + "source.authenticationBoundary", + 256, + ), + }; +} + +function normalizeScope(value, category) { + if (!isObject(value)) throw new Error("Invalid host observation: scope is required."); + const kind = requiredText(value.kind, "scope.kind", 32); + if (!["workflow-run", "account"].includes(kind)) { + throw new Error(`Invalid host observation: unsupported scope kind ${kind}.`); + } + const scope = { + kind, + runId: optionalText(value.runId, "scope.runId", 128), + taskId: optionalText(value.taskId, "scope.taskId", 64), + checkpointId: optionalText(value.checkpointId, "scope.checkpointId", 128), + }; + if (kind === "workflow-run") { + if (!scope.runId) throw new Error("Invalid host observation: workflow-run scope requires runId."); + if (!WORKFLOW_CATEGORIES.has(category)) { + throw new Error(`Invalid host observation: ${category} is account-scoped, not per-run usage.`); + } + } else { + if (scope.runId || scope.taskId || scope.checkpointId) { + throw new Error("Invalid host observation: account scope cannot claim workflow attribution."); + } + if (!ACCOUNT_CATEGORIES.has(category)) { + throw new Error(`Invalid host observation: ${category} requires workflow-run scope.`); + } + } + return scope; +} + +function normalizeThreadUsage(value) { + if (!isObject(value)) throw new Error("thread usage data must be an object."); + const data = { + inputTokens: nonNegativeInteger(value.inputTokens, "inputTokens"), + cachedInputTokens: nonNegativeInteger(value.cachedInputTokens, "cachedInputTokens"), + outputTokens: nonNegativeInteger(value.outputTokens, "outputTokens"), + reasoningOutputTokens: nonNegativeInteger(value.reasoningOutputTokens, "reasoningOutputTokens"), + }; + if (data.cachedInputTokens > data.inputTokens) { + throw new Error("cachedInputTokens cannot exceed inputTokens."); + } + return data; +} + +function normalizeRateLimit(value) { + if (!isObject(value)) throw new Error("rate-limit data must be an object."); + return { + window: requiredText(value.window, "window", 128), + usedPercent: finiteNumber(value.usedPercent, "usedPercent", 0, 100), + resetAt: timestamp(value.resetAt, "resetAt"), + }; +} + +function normalizeCredits(value) { + if (!isObject(value)) throw new Error("credit data must be an object."); + return { + remaining: finiteNumber(value.remaining, "remaining"), + unit: requiredText(value.unit, "unit", 64), + resetAt: value.resetAt === null ? null : timestamp(value.resetAt, "resetAt"), + }; +} + +function normalizeAccountActivity(value) { + if (!isObject(value)) throw new Error("account activity data must be an object."); + const windowStart = timestamp(value.windowStart, "windowStart"); + const windowEnd = timestamp(value.windowEnd, "windowEnd"); + if (Date.parse(windowEnd) < Date.parse(windowStart)) { + throw new Error("account activity windowEnd must not precede windowStart."); + } + return { + windowStart, + windowEnd, + amount: finiteNumber(value.amount, "amount"), + unit: requiredText(value.unit, "unit", 64), + }; +} + +function normalizeGoalBudget(value) { + if (!isObject(value)) throw new Error("goal budget data must be an object."); + return { + status: requiredText(value.status, "status", 64), + tokenBudget: value.tokenBudget === null ? null : nonNegativeInteger(value.tokenBudget, "tokenBudget"), + tokensUsed: value.tokensUsed === null ? null : nonNegativeInteger(value.tokensUsed, "tokensUsed"), + }; +} + +function normalizeGoalLifecycle(value) { + if (!isObject(value)) throw new Error("goal lifecycle data must be an object."); + return { + status: requiredText(value.status, "status", 64), + event: requiredText(value.event, "event", 128), + }; +} + +function normalizeData(category, value) { + if (category === "thread-usage") return normalizeThreadUsage(value); + if (category === "rate-limit-window") return normalizeRateLimit(value); + if (category === "credits") return normalizeCredits(value); + if (category === "account-activity") return normalizeAccountActivity(value); + if (category === "goal-budget") return normalizeGoalBudget(value); + return normalizeGoalLifecycle(value); +} + +function sourceCapability(source, category, capabilities) { + if (source.path === "codex-exec") { + return category === "thread-usage" && source.schemaVersion === "codex-exec-jsonl/v1" + ? { available: true } + : { available: false, reason: "codex-exec-source-category-not-supported" }; + } + if (["explicit-intake", "audit-import"].includes(source.path)) { + return { available: true, imported: true }; + } + if (!capabilities) return { available: false, reason: "versioned-capability-snapshot-missing" }; + const snapshot = validateCodexCapabilitySnapshot(capabilities); + if (source.path === "plugin") { + const surface = snapshot.surfaces.hostObservation; + return surface.status === "supported" && surface.pluginPathCapabilityTestPassed === true + ? { available: true } + : { available: false, reason: "plugin-path-capability-not-proved" }; + } + const appServer = snapshot.surfaces.appServer; + return appServer.status === "supported" + && snapshot.managedBackendDecision.appServerGraduated === true + && appServer.observationCapabilityTestPassed === true + ? { available: true } + : { available: false, reason: "app-server-observation-capability-not-proved" }; +} + +function normalizeHostObservation(value, options = {}) { + if (!isObject(value) || value.schemaVersion !== HOST_OBSERVATION_SCHEMA_VERSION) { + throw new Error(`Invalid host observation: expected ${HOST_OBSERVATION_SCHEMA_VERSION}.`); + } + const observationId = requiredText(value.observationId, "observationId", 128); + if (!/^[a-z0-9][a-z0-9-]{0,127}$/.test(observationId)) { + throw new Error("Invalid host observation: observationId must be a lowercase identifier."); + } + const observedAt = timestamp(value.observedAt, "observedAt"); + const source = normalizeSource(value.source); + const category = requiredText(value.category, "category", 64); + if (!OBSERVATION_CATEGORIES.includes(category)) { + throw new Error(`Invalid host observation: unsupported category ${category}.`); + } + const scope = normalizeScope(value.scope, category); + const rawCategory = requiredText(value.rawCategory, "rawCategory", 256); + const raw = normalizeRaw(value.raw); + let availability = requiredText(value.availability, "availability", 32); + if (!AVAILABILITY.includes(availability)) { + throw new Error(`Invalid host observation: unsupported availability ${availability}.`); + } + const capability = sourceCapability(source, category, options.capabilities); + let reason = value.reason === undefined || value.reason === null + ? null + : requiredText(value.reason, "reason", 512); + let data = null; + + if (["observed", "imported"].includes(availability)) { + if (!capability.available) { + availability = "unavailable"; + reason = capability.reason; + } else if (capability.imported && availability !== "imported") { + availability = "malformed"; + reason = "imported-source-cannot-claim-observed"; + } else if (!capability.imported && availability === "imported") { + availability = "malformed"; + reason = "managed-or-plugin-source-cannot-claim-imported"; + } else { + try { + data = normalizeData(category, value.data); + } catch (error) { + availability = "malformed"; + reason = error.message; + } + } + } else if (!reason) { + reason = `${availability}-host-observation`; + } + + const now = options.now instanceof Date ? options.now : new Date(); + const maxAgeMs = Number.isFinite(options.maxAgeMs) && options.maxAgeMs >= 0 + ? options.maxAgeMs + : null; + if ( + data + && maxAgeMs !== null + && Date.parse(observedAt) < now.getTime() - maxAgeMs + && ["rate-limit-window", "credits", "goal-budget"].includes(category) + ) { + availability = "stale"; + reason = "observation-exceeded-freshness-window"; + data = null; + } + + const evidenceClass = availability === "observed" + ? "observed" + : availability === "imported" ? "imported" : "unknown"; + if (!["observed", "imported"].includes(availability)) data = null; + + return { + schemaVersion: HOST_OBSERVATION_SCHEMA_VERSION, + observationId, + observedAt, + source, + scope, + category, + rawCategory, + availability, + evidenceClass, + reason, + data, + raw, + billingImpact: "unknown", + }; +} + +function ledgerPath(found) { + return path.join(found.runRoot, "integration", "host-observations.jsonl"); +} + +function readHostObservations(found) { + const filePath = ledgerPath(found); + if (!fs.existsSync(filePath)) return []; + return fs.readFileSync(filePath, "utf8") + .split(/\r?\n/) + .filter(Boolean) + .map((line, index) => { + let observation; + try { + observation = JSON.parse(line); + } catch { + throw new Error(`Invalid host observation ledger line ${index + 1}.`); + } + if (observation.schemaVersion !== HOST_OBSERVATION_SCHEMA_VERSION) { + throw new Error(`Invalid host observation ledger schema at line ${index + 1}.`); + } + return observation; + }); +} + +function recordHostObservation(found, candidate, options = {}) { + const observation = normalizeHostObservation(candidate, options); + if (observation.scope.kind === "workflow-run" && observation.scope.runId !== found.run.runId) { + throw new Error("Host observation run id does not match the loaded workflow run."); + } + const existing = readHostObservations(found); + if (existing.some((entry) => entry.observationId === observation.observationId)) { + throw new Error(`Host observation already exists: ${observation.observationId}.`); + } + const filePath = ledgerPath(found); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.appendFileSync(filePath, `${JSON.stringify(observation)}\n`); + return observation; +} + +module.exports = { + AVAILABILITY, + HOST_OBSERVATION_SCHEMA_VERSION, + OBSERVATION_CATEGORIES, + SOURCE_PATHS, + normalizeHostObservation, + readHostObservations, + recordHostObservation, +}; diff --git a/tests/contracts/integration-observation.js b/tests/contracts/integration-observation.js new file mode 100644 index 0000000..8d5ae46 --- /dev/null +++ b/tests/contracts/integration-observation.js @@ -0,0 +1,199 @@ +"use strict"; + +const fs = require("node:fs"); +const { assert } = require("../harness/lib/assertions"); +const { cleanupRepo, makeTempRepo } = require("../harness/lib/temp-repo"); +const { supportedSnapshot } = require("./integration-capabilities"); +const { validDefinition } = require("./workflow-definition"); +const { approveWorkflow } = require("./workflow-scheduler"); +const { + normalizeHostObservation, + readHostObservations, + recordHostObservation, +} = require("../../src/integration/observation"); +const { loadWorkflowRun } = require("../../src/workflow/state"); + +function assertThrows(action, expected, label) { + let error; + try { + action(); + } catch (caught) { + error = caught; + } + assert(error, `${label}: expected an error`); + assert(expected.test(error.message), `${label}: unexpected error: ${error.message}`); +} + +function baseObservation(runId, overrides = {}) { + return { + schemaVersion: "host-observation/v1", + observationId: "usage-0001", + observedAt: "2026-07-18T12:00:00.000Z", + source: { + path: "codex-exec", + codexVersion: "codex-cli 0.137.0", + schemaVersion: "codex-exec-jsonl/v1", + authenticationBoundary: "managed-child", + }, + scope: { + kind: "workflow-run", + runId, + taskId: "implement-change", + checkpointId: "implement-change-attempt-0001", + }, + category: "thread-usage", + rawCategory: "turn.completed.usage", + availability: "observed", + data: { + inputTokens: 24763, + cachedInputTokens: 24448, + outputTokens: 122, + reasoningOutputTokens: 0, + }, + raw: { + input_tokens: 24763, + cached_input_tokens: 24448, + output_tokens: 122, + reasoning_output_tokens: 0, + }, + ...overrides, + }; +} + +function main() { + const repoRoot = makeTempRepo("cewp-host-observation-"); + try { + const approved = approveWorkflow(repoRoot, validDefinition()); + const found = loadWorkflowRun(repoRoot, approved.runId); + const runBefore = fs.readFileSync(found.runPath, "utf8"); + const options = { + capabilities: supportedSnapshot(), + now: new Date("2026-07-18T12:01:00.000Z"), + maxAgeMs: 5 * 60 * 1000, + }; + + const observed = recordHostObservation(found, baseObservation(approved.runId), options); + assert(observed.availability === "observed", "codex-exec JSONL usage is observed"); + assert(observed.evidenceClass === "observed", "managed JSONL is observed evidence"); + assert(observed.data.cachedInputTokens === 24448, "token categories remain distinct"); + assert(observed.billingImpact === "unknown", "subscription or billing impact is not inferred"); + assert(fs.readFileSync(found.runPath, "utf8") === runBefore, "host usage never mutates workflow budget state"); + + const pluginClaim = baseObservation(approved.runId, { + observationId: "plugin-0001", + source: { + path: "plugin", + codexVersion: "codex-cli 0.137.0", + schemaVersion: "plugin-host-observation/v1", + authenticationBoundary: "host-owned", + }, + }); + const unavailable = normalizeHostObservation(pluginClaim, options); + assert(unavailable.availability === "unavailable", "unprobed plugin observation is unavailable"); + assert(unavailable.evidenceClass === "unknown", "unprobed plugin data is not observed evidence"); + assert(unavailable.data === null, "unavailable plugin data is not exposed"); + assert(unavailable.raw !== null, "unavailable input keeps bounded raw semantics for diagnosis"); + + const imported = normalizeHostObservation(baseObservation(approved.runId, { + observationId: "audit-0001", + source: { + path: "audit-import", + codexVersion: null, + schemaVersion: "external-receipt/v1", + authenticationBoundary: "external-owner", + }, + availability: "imported", + }), options); + assert(imported.availability === "imported", "audit evidence remains imported"); + assert(imported.evidenceClass === "imported", "audit evidence is not relabeled observed"); + + const stale = normalizeHostObservation(baseObservation(approved.runId, { + observationId: "rate-limit-old", + observedAt: "2026-07-18T11:00:00.000Z", + source: { + path: "audit-import", + codexVersion: null, + schemaVersion: "external-rate-limit/v1", + authenticationBoundary: "external-owner", + }, + scope: { kind: "account", runId: null, taskId: null, checkpointId: null }, + category: "rate-limit-window", + rawCategory: "account.rate_limit", + availability: "imported", + data: { window: "primary", usedPercent: 75, resetAt: "2026-07-18T13:00:00.000Z" }, + }), options); + assert(stale.availability === "stale", "old rate-limit data is explicitly stale"); + assert(stale.data === null, "stale limits are not presented as current data"); + + const malformed = normalizeHostObservation(baseObservation(approved.runId, { + observationId: "usage-malformed", + data: { inputTokens: -1, outputTokens: "many" }, + }), options); + assert(malformed.availability === "malformed", "invalid usage categories are retained as malformed"); + assert(malformed.data === null, "malformed usage is never exposed as observed totals"); + + const unknown = normalizeHostObservation(baseObservation(approved.runId, { + observationId: "usage-unknown", + availability: "unknown", + reason: "native-host-usage-not-exposed", + data: null, + }), options); + assert(unknown.availability === "unknown", "unavailable native usage remains unknown"); + assert(unknown.data === null && unknown.evidenceClass === "unknown", "unknown usage is never zero"); + + const accountActivity = normalizeHostObservation(baseObservation(approved.runId, { + observationId: "account-activity-0001", + source: { + path: "audit-import", + codexVersion: null, + schemaVersion: "external-account-activity/v1", + authenticationBoundary: "external-owner", + }, + scope: { kind: "account", runId: null, taskId: null, checkpointId: null }, + category: "account-activity", + rawCategory: "account.activity", + availability: "imported", + data: { + windowStart: "2026-07-18T00:00:00.000Z", + windowEnd: "2026-07-18T12:00:00.000Z", + amount: 12, + unit: "operations", + }, + }), options); + assert(accountActivity.scope.kind === "account", "account activity is not presented as per-run usage"); + assert(accountActivity.scope.runId === null, "account activity has no invented run attribution"); + + recordHostObservation(found, pluginClaim, options); + const ledger = readHostObservations(found); + assert(ledger.length === 2, "observation ledger retains observed and unavailable records"); + assert(ledger[1].availability === "unavailable", "persisted plugin claim remains unavailable"); + assertThrows( + () => recordHostObservation(found, baseObservation(approved.runId), options), + /already exists/, + "observation ids are immutable", + ); + + assertThrows( + () => normalizeHostObservation(baseObservation(approved.runId, { + observationId: "misattributed-account", + scope: { kind: "account", runId: approved.runId, taskId: null, checkpointId: null }, + category: "account-activity", + rawCategory: "account.activity", + }), options), + /account scope cannot claim workflow attribution/, + "account activity cannot be relabeled as per-run usage", + ); + + console.log("[PASS] structured host observations preserve observed, imported, stale, and unknown truth"); + } finally { + cleanupRepo(repoRoot); + } +} + +try { + main(); +} catch (error) { + console.error("[FAIL] structured host observation contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} From baee228ed420bbfc600918380f99e278e8632944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuncay=20=C3=96lmez?= Date: Tue, 21 Jul 2026 12:12:00 +0300 Subject: [PATCH 3/4] feat: preserve interrupted workflow recovery --- docs/workflow-runtime.md | 8 ++++++ src/workflow/migration.js | 1 + src/workflow/progress.js | 8 ++++++ src/workflow/state.js | 31 ++++++++++++++++++++--- src/workflow/transitions.js | 10 ++++++++ tests/contracts/workflow-lifecycle.js | 20 +++++++++++++++ tests/contracts/workflow-state-machine.js | 2 ++ 7 files changed, 77 insertions(+), 3 deletions(-) diff --git a/docs/workflow-runtime.md b/docs/workflow-runtime.md index 03f84f5..1c07524 100644 --- a/docs/workflow-runtime.md +++ b/docs/workflow-runtime.md @@ -180,6 +180,7 @@ Existing `supervised-run/v1` runs remain readable through a read-only `run-state - `active + pause-budget-safe -> paused-budget-safe` - `active + pause-budget-unverified -> paused-budget-unverified` - `active + pause-host-limit -> paused-host-limit` +- `active + interrupt -> interrupted` - `active + block -> blocked` - `active + tasks-completed -> review-pending` - `active + tasks-completed-no-review -> completed` @@ -202,6 +203,12 @@ Existing `supervised-run/v1` runs remain readable through a read-only `run-state - `paused-host-limit + rollback -> rolled-back` - `paused-host-limit + cancel -> cancelled` - `paused-host-limit + abandon -> abandoned` +- `interrupted + resume -> active` +- `interrupted + retry -> active` +- `interrupted + rollback -> rolled-back` +- `interrupted + cancel -> cancelled` +- `interrupted + timeout -> timed-out` +- `interrupted + abandon -> abandoned` - `blocked + retry -> active` - `blocked + revise -> active` - `blocked + reassign -> active` @@ -216,6 +223,7 @@ Existing `supervised-run/v1` runs remain readable through a read-only `run-state - `timed-out + abandon -> abandoned` - `review-pending + pause-budget-safe -> paused-budget-safe` - `review-pending + pause-host-limit -> paused-host-limit` +- `review-pending + interrupt -> interrupted` - `review-pending + reviewer-pass -> completed` - `review-pending + reviewer-block -> blocked` - `review-pending + cancel -> cancelled` diff --git a/src/workflow/migration.js b/src/workflow/migration.js index 3e11f51..a0bafd3 100644 --- a/src/workflow/migration.js +++ b/src/workflow/migration.js @@ -251,6 +251,7 @@ function projectLegacyRun(found, definition, sourceDigest) { checkpointReviews: [], revisionHistory: [], interventions: [], + interruption: null, warnings: [ ...(found.run.warnings || []), "Read-only compatibility projection; explicit backed-up migration is required before workflow execution.", diff --git a/src/workflow/progress.js b/src/workflow/progress.js index aa55618..d48c7bb 100644 --- a/src/workflow/progress.js +++ b/src/workflow/progress.js @@ -54,6 +54,13 @@ function nextActionFor(run, definition, schedule) { reason: "host availability must be restored explicitly", }; } + if (run.status === "interrupted") { + return { + kind: "host-resume", + command: `cewp workflow intervene ${run.runId} --event resume --reason --yes`, + reason: run.interruption ? run.interruption.reason : "workflow execution was interrupted", + }; + } const blocked = run.tasks.find((task) => task.status === "blocked"); if (blocked) { const classification = blocked.blocker && blocked.blocker.classification; @@ -181,6 +188,7 @@ function deriveProgressView(run, definition, schedule, options = {}) { }, reviewer: run.reviewer, compatibility: run.compatibility || null, + interruption: run.interruption || null, interventions: run.interventions, warnings: run.warnings || [], nextAction: nextActionFor(run, definition, schedule), diff --git a/src/workflow/state.js b/src/workflow/state.js index c8c7872..f545891 100644 --- a/src/workflow/state.js +++ b/src/workflow/state.js @@ -1141,6 +1141,7 @@ const RUN_INTERVENTION_EVENTS = new Set([ "pause-budget-safe", "pause-budget-unverified", "pause-host-limit", + "interrupt", "resume", ]); const RUN_LIFECYCLE_EVENTS = new Set(["continue", "cancel", "timeout", "rollback", "abandon"]); @@ -1154,11 +1155,30 @@ function workflowResumeStatus(budget, fallback) { return budget.resumeStatus; } +function interruptionResumeStatus(interruption, fallback) { + if (!interruption || interruption.resumeStatus === null || interruption.resumeStatus === undefined) { + return fallback; + } + if (!RESUMABLE_RUN_STATUSES.has(interruption.resumeStatus)) { + throw new Error(`Invalid workflow interruption resume status: ${interruption.resumeStatus}.`); + } + return interruption.resumeStatus; +} + function interveneWorkflowRun(found, options, timestamp, reason) { let budget = found.run.budget; + let interruption = found.run.interruption || null; const previousStatus = found.run.status; let nextStatus = transitionRun(previousStatus, options.event); - if (options.event === "add-budget") { + if (options.event === "interrupt") { + interruption = { + active: true, + observedAt: timestamp, + source: options.source || "operator", + reason, + resumeStatus: previousStatus, + }; + } else if (options.event === "add-budget") { if (!Number.isInteger(options.operations) || options.operations < 1) { throw new Error("add-budget requires --operations with a positive integer."); } @@ -1201,13 +1221,16 @@ function interveneWorkflowRun(found, options, timestamp, reason) { resumeStatus: previousStatus, }; } else if (options.event === "resume") { - nextStatus = workflowResumeStatus(budget, nextStatus); + nextStatus = previousStatus === "interrupted" + ? interruptionResumeStatus(interruption, nextStatus) + : workflowResumeStatus(budget, nextStatus); budget = { ...budget, hostLimit: previousStatus === "paused-host-limit" ? null : budget.hostLimit, pauseReason: null, resumeStatus: null, }; + if (previousStatus === "interrupted") interruption = null; } else { budget = { ...budget, @@ -1221,7 +1244,7 @@ function interveneWorkflowRun(found, options, timestamp, reason) { checkpointId: null, classification: null, reason, - actor: "operator", + actor: options.actor || "operator", recordedAt: timestamp, allocation: options.allocation || null, operations: options.operations || null, @@ -1231,6 +1254,7 @@ function interveneWorkflowRun(found, options, timestamp, reason) { status: nextStatus, updatedAt: timestamp, budget, + interruption, interventions: [...found.run.interventions, intervention], }; writeJsonAtomic(found.runPath, run); @@ -1528,6 +1552,7 @@ function createApprovedRun(options) { checkpointReviews: [], revisionHistory: [], interventions: [], + interruption: null, warnings: [], }; writeJsonAtomic(runPath, run); diff --git a/src/workflow/transitions.js b/src/workflow/transitions.js index b8090e9..b2cb86a 100644 --- a/src/workflow/transitions.js +++ b/src/workflow/transitions.js @@ -158,6 +158,7 @@ const RUN_TRANSITIONS = Object.freeze({ "pause-budget-safe": "paused-budget-safe", "pause-budget-unverified": "paused-budget-unverified", "pause-host-limit": "paused-host-limit", + interrupt: "interrupted", block: "blocked", "tasks-completed": "review-pending", "tasks-completed-no-review": "completed", @@ -187,6 +188,14 @@ const RUN_TRANSITIONS = Object.freeze({ cancel: "cancelled", abandon: "abandoned", }, + interrupted: { + resume: "active", + retry: "active", + rollback: "rolled-back", + cancel: "cancelled", + timeout: "timed-out", + abandon: "abandoned", + }, blocked: { retry: "active", revise: "active", @@ -206,6 +215,7 @@ const RUN_TRANSITIONS = Object.freeze({ "review-pending": { "pause-budget-safe": "paused-budget-safe", "pause-host-limit": "paused-host-limit", + interrupt: "interrupted", "reviewer-pass": "completed", "reviewer-block": "blocked", cancel: "cancelled", diff --git a/tests/contracts/workflow-lifecycle.js b/tests/contracts/workflow-lifecycle.js index a080ea0..309a2f4 100644 --- a/tests/contracts/workflow-lifecycle.js +++ b/tests/contracts/workflow-lifecycle.js @@ -142,6 +142,26 @@ function runWorkflowLifecycleContract() { assert(finalAbandon.status === 0, `rolled-back workflow can be abandoned: ${finalAbandon.stderr}`); assert(JSON.parse(finalAbandon.stdout).data.run.status === "abandoned", "rollback never becomes completion during abandon"); + const interruptedRun = approveWorkflow(repoRoot, validDefinition()); + assert(start(repoRoot, interruptedRun.runId).status === 0, "interrupted workflow starts a checkpoint"); + const interruptedResult = intervene(repoRoot, interruptedRun.runId, "interrupt", { + reason: "Host turn stopped before checkpoint evidence was returned", + }); + assert(interruptedResult.status === 0, `workflow records interruption: ${interruptedResult.stderr}`); + const interrupted = JSON.parse(interruptedResult.stdout).data; + assert(interrupted.run.status === "interrupted", "interruption has a distinct run state"); + assert(interrupted.run.tasks[0].status === "running", "interruption preserves the incomplete task state"); + assert(interrupted.run.interruption.resumeStatus === "active", "interruption remembers the deterministic resume state"); + assert(interrupted.progress.nextAction.kind === "host-resume", "interrupted progress exposes explicit resume"); + const resumedResult = intervene(repoRoot, interruptedRun.runId, "resume", { + reason: "Host is available and the same checkpoint can continue", + }); + assert(resumedResult.status === 0, `interrupted workflow resumes: ${resumedResult.stderr}`); + const resumed = JSON.parse(resumedResult.stdout).data.run; + assert(resumed.status === "active", "resume restores the prior active run state"); + assert(resumed.tasks[0].status === "running", "resume does not fabricate a fresh checkpoint or evidence"); + assert(resumed.interruption === null, "resume clears the active interruption marker"); + const eventsPath = path.join(repoRoot, ".cewp", "workflow-runs", repeatedRun.runId, "events.jsonl"); const events = fs.readFileSync(eventsPath, "utf8"); assert(events.includes("workflow-lifecycle"), "lifecycle decisions are append-only evidence"); diff --git a/tests/contracts/workflow-state-machine.js b/tests/contracts/workflow-state-machine.js index 520de55..7770b6a 100644 --- a/tests/contracts/workflow-state-machine.js +++ b/tests/contracts/workflow-state-machine.js @@ -50,6 +50,8 @@ function runWorkflowStateMachineContract() { run = transitionRun(run, "resume"); assert(run === "active", "paused run resumes through an explicit event"); assert(transitionRun("active", "pause-host-limit") === "paused-host-limit", "host limit has its own pause state"); + assert(transitionRun("active", "interrupt") === "interrupted", "interruption is distinct from failure and cancellation"); + assert(transitionRun("interrupted", "resume") === "active", "interrupted run has an explicit resume path"); assert(transitionRun("active", "timeout") === "timed-out", "run timeout is not generic failure"); assertWaivableClassification("pre-existing-failure"); From 1972ee152fe706462001118571f8a1c2be21bba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuncay=20=C3=96lmez?= Date: Tue, 21 Jul 2026 12:13:00 +0300 Subject: [PATCH 4/4] feat: map native goal lifecycle events --- package.json | 5 +- src/integration/native-goal.js | 347 ++++++++++++++++++++++++++ src/workflow/state.js | 4 +- tests/contracts/native-goal-events.js | 221 ++++++++++++++++ 4 files changed, 573 insertions(+), 4 deletions(-) create mode 100644 src/integration/native-goal.js create mode 100644 tests/contracts/native-goal-events.js diff --git a/package.json b/package.json index c48783b..811b8db 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, "scripts": { "test": "npm run test:contracts && npm run smoke", - "test:contracts": "npm run test:init-install && npm run test:adapter-profile && npm run test:doctor-json && npm run test:operator-json && npm run test:hook-output && npm run test:skill-format && npm run test:ownership-gates && npm run test:fixtures && npm run test:plugin-package && npm run test:supervised-intake && npm run test:supervised-proposal && npm run test:supervised-controls && npm run test:supervised-execution && npm run test:supervised-review && npm run test:supervised-failure && npm run test:supervised-linear-resume && npm run test:supervised-demo && npm run test:workflow-definition && npm run test:workflow-compiler && npm run test:workflow-proposal && npm run test:workflow-scheduler && npm run test:workflow-worker-matrix && npm run test:workflow-result && npm run test:workflow-failure-result && npm run test:workflow-state-machine && npm run test:workflow-interventions && npm run test:workflow-failure-matrix && npm run test:workflow-budget && npm run test:workflow-progress && npm run test:workflow-review && npm run test:workflow-checkpoint-review && npm run test:workflow-lifecycle && npm run test:workflow-revision && npm run test:workflow-migration && npm run test:workflow-templates && npm run test:workflow-docs && npm run test:workflow-release && npm run test:integration-capabilities && npm run test:integration-binding && npm run test:integration-observation", + "test:contracts": "npm run test:init-install && npm run test:adapter-profile && npm run test:doctor-json && npm run test:operator-json && npm run test:hook-output && npm run test:skill-format && npm run test:ownership-gates && npm run test:fixtures && npm run test:plugin-package && npm run test:supervised-intake && npm run test:supervised-proposal && npm run test:supervised-controls && npm run test:supervised-execution && npm run test:supervised-review && npm run test:supervised-failure && npm run test:supervised-linear-resume && npm run test:supervised-demo && npm run test:workflow-definition && npm run test:workflow-compiler && npm run test:workflow-proposal && npm run test:workflow-scheduler && npm run test:workflow-worker-matrix && npm run test:workflow-result && npm run test:workflow-failure-result && npm run test:workflow-state-machine && npm run test:workflow-interventions && npm run test:workflow-failure-matrix && npm run test:workflow-budget && npm run test:workflow-progress && npm run test:workflow-review && npm run test:workflow-checkpoint-review && npm run test:workflow-lifecycle && npm run test:workflow-revision && npm run test:workflow-migration && npm run test:workflow-templates && npm run test:workflow-docs && npm run test:workflow-release && npm run test:integration-capabilities && npm run test:integration-binding && npm run test:integration-observation && npm run test:native-goal-events", "test:init-install": "node tests/contracts/init-install.js", "test:adapter-profile": "node tests/contracts/adapter-profile.js", "test:doctor-json": "node tests/contracts/doctor-json.js", @@ -91,13 +91,14 @@ "test:integration-capabilities": "node tests/contracts/integration-capabilities.js", "test:integration-binding": "node tests/contracts/integration-binding.js", "test:integration-observation": "node tests/contracts/integration-observation.js", + "test:native-goal-events": "node tests/contracts/native-goal-events.js", "demo:supervised": "node src/demo/supervised.js", "test:plugin-lifecycle": "node tests/capabilities/plugin-lifecycle.js", "test:clean-install": "node tests/capabilities/clean-install.js", "probe:codex-app-server": "node tests/capabilities/codex-app-server.js", "smoke": "node tests/harness/run-smoke.js", "check:cli": "node ./bin/cewp.js --help", - "check:syntax": "node --check ./bin/cewp.js && node --check ./src/demo/fake-codex.js && node --check ./src/demo/supervised.js && node --check ./src/integration/capabilities.js && node --check ./src/integration/binding.js && node --check ./src/integration/observation.js && node --check ./src/skills/install.js && node --check ./src/skills/format.js && node --check ./src/skills/doctor-report.js && node --check ./src/run/ownership.js && node --check ./src/run/control-gates.js && node --check ./src/supervise/budget.js && node --check ./src/supervise/cli.js && node --check ./src/supervise/commands.js && node --check ./src/supervise/controls.js && node --check ./src/supervise/execution.js && node --check ./src/supervise/profiles.js && node --check ./src/supervise/receipt.js && node --check ./src/supervise/review.js && node --check ./src/supervise/state.js && node --check ./src/supervise/test-authoring.js && node --check ./src/supervise/verification.js && node --check ./src/workflow/cli.js && node --check ./src/workflow/definition.js && node --check ./src/workflow/graph.js && node --check ./src/workflow/result.js && node --check ./src/workflow/scheduler.js && node --check ./src/workflow/source.js && node --check ./src/workflow/state.js && node --check ./src/workflow/transitions.js && node --check ./tests/contracts/init-install.js && node --check ./tests/contracts/adapter-profile.js && node --check ./tests/contracts/doctor-json.js && node --check ./tests/contracts/operator-json.js && node --check ./tests/contracts/hook-output.js && node --check ./tests/contracts/skill-format.js && node --check ./tests/contracts/ownership-gates.js && node --check ./tests/contracts/deterministic-fixtures.js && node --check ./tests/contracts/plugin-package.js && node --check ./tests/contracts/supervised-intake.js && node --check ./tests/contracts/supervised-proposal.js && node --check ./tests/contracts/supervised-controls.js && node --check ./tests/contracts/supervised-execution.js && node --check ./tests/contracts/supervised-review.js && node --check ./tests/contracts/supervised-failure.js && node --check ./tests/contracts/supervised-linear-resume.js && node --check ./tests/contracts/supervised-demo.js && node --check ./tests/contracts/integration-capabilities.js && node --check ./tests/contracts/integration-binding.js && node --check ./tests/contracts/integration-observation.js && node --check ./tests/contracts/workflow-definition.js && node --check ./tests/contracts/workflow-proposal.js && node --check ./tests/contracts/workflow-scheduler.js && node --check ./tests/contracts/workflow-result.js && node --check ./tests/contracts/workflow-state-machine.js && node --check ./tests/contracts/workflow-interventions.js && node --check ./tests/capabilities/clean-install.js && node --check ./tests/capabilities/plugin-lifecycle.js && node --check ./tests/capabilities/codex-app-server.js && node --check ./tests/capabilities/fixtures/deny-shell-hook.js", + "check:syntax": "node --check ./bin/cewp.js && node --check ./src/demo/fake-codex.js && node --check ./src/demo/supervised.js && node --check ./src/integration/capabilities.js && node --check ./src/integration/binding.js && node --check ./src/integration/observation.js && node --check ./src/integration/native-goal.js && node --check ./src/skills/install.js && node --check ./src/skills/format.js && node --check ./src/skills/doctor-report.js && node --check ./src/run/ownership.js && node --check ./src/run/control-gates.js && node --check ./src/supervise/budget.js && node --check ./src/supervise/cli.js && node --check ./src/supervise/commands.js && node --check ./src/supervise/controls.js && node --check ./src/supervise/execution.js && node --check ./src/supervise/profiles.js && node --check ./src/supervise/receipt.js && node --check ./src/supervise/review.js && node --check ./src/supervise/state.js && node --check ./src/supervise/test-authoring.js && node --check ./src/supervise/verification.js && node --check ./src/workflow/cli.js && node --check ./src/workflow/definition.js && node --check ./src/workflow/graph.js && node --check ./src/workflow/result.js && node --check ./src/workflow/scheduler.js && node --check ./src/workflow/source.js && node --check ./src/workflow/state.js && node --check ./src/workflow/transitions.js && node --check ./tests/contracts/init-install.js && node --check ./tests/contracts/adapter-profile.js && node --check ./tests/contracts/doctor-json.js && node --check ./tests/contracts/operator-json.js && node --check ./tests/contracts/hook-output.js && node --check ./tests/contracts/skill-format.js && node --check ./tests/contracts/ownership-gates.js && node --check ./tests/contracts/deterministic-fixtures.js && node --check ./tests/contracts/plugin-package.js && node --check ./tests/contracts/supervised-intake.js && node --check ./tests/contracts/supervised-proposal.js && node --check ./tests/contracts/supervised-controls.js && node --check ./tests/contracts/supervised-execution.js && node --check ./tests/contracts/supervised-review.js && node --check ./tests/contracts/supervised-failure.js && node --check ./tests/contracts/supervised-linear-resume.js && node --check ./tests/contracts/supervised-demo.js && node --check ./tests/contracts/integration-capabilities.js && node --check ./tests/contracts/integration-binding.js && node --check ./tests/contracts/integration-observation.js && node --check ./tests/contracts/native-goal-events.js && node --check ./tests/contracts/workflow-definition.js && node --check ./tests/contracts/workflow-proposal.js && node --check ./tests/contracts/workflow-scheduler.js && node --check ./tests/contracts/workflow-result.js && node --check ./tests/contracts/workflow-state-machine.js && node --check ./tests/contracts/workflow-interventions.js && node --check ./tests/capabilities/clean-install.js && node --check ./tests/capabilities/plugin-lifecycle.js && node --check ./tests/capabilities/codex-app-server.js && node --check ./tests/capabilities/fixtures/deny-shell-hook.js", "check:workflow-syntax": "node --check ./src/workflow/budget.js && node --check ./src/workflow/compiler.js && node --check ./src/workflow/migration.js && node --check ./src/workflow/progress.js && node --check ./src/workflow/proposal.js && node --check ./src/workflow/review.js && node --check ./src/workflow/revision.js && node --check ./src/workflow/templates.js && node --check ./tests/contracts/workflow-budget.js && node --check ./tests/contracts/workflow-checkpoint-review.js && node --check ./tests/contracts/workflow-compiler.js && node --check ./tests/contracts/workflow-docs.js && node --check ./tests/contracts/workflow-failure-matrix.js && node --check ./tests/contracts/workflow-failure-result.js && node --check ./tests/contracts/workflow-migration.js && node --check ./tests/contracts/workflow-progress.js && node --check ./tests/contracts/workflow-release.js && node --check ./tests/contracts/workflow-review.js && node --check ./tests/contracts/workflow-lifecycle.js && node --check ./tests/contracts/workflow-revision.js && node --check ./tests/contracts/workflow-templates.js && node --check ./tests/contracts/workflow-worker-matrix.js", "check": "npm run check:syntax && npm run check:workflow-syntax && npm test", "pack:dry-run": "npm pack --dry-run" diff --git a/src/integration/native-goal.js b/src/integration/native-goal.js new file mode 100644 index 0000000..1a29247 --- /dev/null +++ b/src/integration/native-goal.js @@ -0,0 +1,347 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { validateCodexCapabilitySnapshot } = require("./capabilities"); +const { loadHostBinding } = require("./binding"); +const { interveneWorkflow, loadWorkflowRun } = require("../workflow/state"); + +const NATIVE_GOAL_EVENT_SCHEMA_VERSION = "native-goal-event/v1"; +const EVENT_TYPES = Object.freeze([ + "attached", + "started", + "checkpoint", + "revised", + "resumed", + "stopped", + "timed-out", + "status", + "partial-output", + "malformed", +]); +const SOURCE_PATHS = Object.freeze(["plugin", "app-server", "explicit-intake"]); +const RAW_LIMIT_BYTES = 64 * 1024; + +function isObject(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function requiredText(value, label, maximum = 1024) { + if (typeof value !== "string" || value.trim().length === 0) { + throw new Error(`Invalid native goal event: ${label} is required.`); + } + const text = value.trim(); + if (text.length > maximum || /[\u0000-\u001f]/.test(text)) { + throw new Error(`Invalid native goal event: ${label} is too long or contains control characters.`); + } + return text; +} + +function optionalText(value, label, maximum = 1024) { + return value === null ? null : requiredText(value, label, maximum); +} + +function timestamp(value, label) { + const text = requiredText(value, label, 128); + if (!Number.isFinite(Date.parse(text))) { + throw new Error(`Invalid native goal event: ${label} must be an ISO timestamp.`); + } + return new Date(text).toISOString(); +} + +function normalizeRaw(value) { + let json; + try { + json = JSON.stringify(value); + } catch { + throw new Error("Invalid native goal event: raw event must be JSON serializable."); + } + if (json === undefined || Buffer.byteLength(json, "utf8") > RAW_LIMIT_BYTES) { + throw new Error(`Invalid native goal event: raw event exceeds ${RAW_LIMIT_BYTES} bytes.`); + } + return JSON.parse(json); +} + +function normalizeSource(value) { + if (!isObject(value)) throw new Error("Invalid native goal event: source is required."); + const sourcePath = requiredText(value.path, "source.path", 64); + if (!SOURCE_PATHS.includes(sourcePath)) { + throw new Error(`Invalid native goal event: unsupported source path ${sourcePath}.`); + } + return { + path: sourcePath, + codexVersion: optionalText(value.codexVersion, "source.codexVersion", 256), + nativeGoalSchemaVersion: optionalText( + value.nativeGoalSchemaVersion, + "source.nativeGoalSchemaVersion", + 256, + ), + capabilitySchemaVersion: optionalText( + value.capabilitySchemaVersion, + "source.capabilitySchemaVersion", + 256, + ), + authenticationBoundary: requiredText( + value.authenticationBoundary, + "source.authenticationBoundary", + 256, + ), + }; +} + +function normalizeWorkflow(value) { + if (!isObject(value)) throw new Error("Invalid native goal event: workflow is required."); + return { + runId: requiredText(value.runId, "workflow.runId", 128), + taskId: optionalText(value.taskId, "workflow.taskId", 64), + checkpointId: optionalText(value.checkpointId, "workflow.checkpointId", 128), + }; +} + +function normalizePartialOutput(value, type) { + if (type !== "partial-output") { + if (value !== null) throw new Error("Invalid native goal event: partialOutput is valid only for partial-output."); + return null; + } + if (!isObject(value) || value.present !== true || value.complete !== false) { + throw new Error("Invalid native goal event: partial output must be present and incomplete."); + } + if (!Number.isSafeInteger(value.bytes) || value.bytes < 0) { + throw new Error("Invalid native goal event: partial output bytes must be a non-negative integer."); + } + return { present: true, bytes: value.bytes, complete: false }; +} + +function capabilityAssessment(source, options) { + if (source.path === "explicit-intake") { + return { available: true, compatibility: "explicit-intake", evidenceClass: "imported", snapshot: null }; + } + if (!options.capabilities) { + return { available: false, compatibility: "unavailable", evidenceClass: "unknown", snapshot: null }; + } + const snapshot = validateCodexCapabilitySnapshot(options.capabilities); + if (source.path === "plugin") { + const observed = snapshot.surfaces.hostObservation; + if (observed.status !== "supported" || observed.pluginPathCapabilityTestPassed !== true) { + return { available: false, compatibility: "unavailable", evidenceClass: "unknown", snapshot }; + } + } else if ( + snapshot.surfaces.appServer.status !== "supported" + || snapshot.managedBackendDecision.appServerGraduated !== true + ) { + return { available: false, compatibility: "unavailable", evidenceClass: "unknown", snapshot }; + } + const nativeGoal = snapshot.surfaces.nativeGoal; + const exact = source.capabilitySchemaVersion === snapshot.schemaVersion + && source.nativeGoalSchemaVersion === nativeGoal.schemaVersion; + return { + available: true, + compatibility: exact ? "compatible" : "drifted", + evidenceClass: "observed", + snapshot, + }; +} + +function statusMapping(status, capability) { + const knownLimit = status === "budgetLimited" || status === "usageLimited"; + const nativeGoal = capability.snapshot && capability.snapshot.surfaces.nativeGoal; + const versioned = capability.compatibility === "compatible" + && nativeGoal + && nativeGoal.statuses.includes(status); + if (knownLimit && !versioned) { + return { state: "paused-host-limit", limitKind: "unknown", versioned: false, success: false }; + } + if (!versioned) return { state: "unknown", limitKind: null, versioned: false, success: false }; + if (status === "active") return { state: "active", limitKind: null, versioned: true, success: false }; + if (status === "paused") return { state: "interrupted", limitKind: null, versioned: true, success: false }; + if (status === "blocked") return { state: "blocked", limitKind: null, versioned: true, success: false }; + if (status === "budgetLimited") { + return { state: "paused-host-limit", limitKind: "goal-budget", versioned: true, success: false }; + } + if (status === "usageLimited") { + return { state: "paused-host-limit", limitKind: "account-usage", versioned: true, success: false }; + } + if (status === "complete") { + return { state: "evidence-pending", limitKind: null, versioned: true, success: false }; + } + return { state: "unknown", limitKind: null, versioned: true, success: false }; +} + +function mappingFor(type, status, capability) { + if (type === "malformed") { + return { state: "unknown", limitKind: null, versioned: false, success: false }; + } + if (type === "partial-output") { + return { state: "interrupted", limitKind: null, versioned: false, success: false }; + } + if (type === "stopped") { + return { state: "cancelled", limitKind: null, versioned: false, success: false }; + } + if (type === "timed-out") { + return { state: "timed-out", limitKind: null, versioned: false, success: false }; + } + if (type === "checkpoint") { + return { state: "evidence-pending", limitKind: null, versioned: false, success: false }; + } + if (type === "revised") { + return { state: "revision-pending", limitKind: null, versioned: false, success: false }; + } + if (type === "attached") { + return { state: "attached", limitKind: null, versioned: false, success: false }; + } + return statusMapping(status, capability); +} + +function normalizeNativeGoalEvent(value, options = {}) { + if (!isObject(value) || value.schemaVersion !== NATIVE_GOAL_EVENT_SCHEMA_VERSION) { + throw new Error(`Invalid native goal event: expected ${NATIVE_GOAL_EVENT_SCHEMA_VERSION}.`); + } + const eventId = requiredText(value.eventId, "eventId", 128); + if (!/^[a-z0-9][a-z0-9-]{0,127}$/.test(eventId)) { + throw new Error("Invalid native goal event: eventId must be a lowercase identifier."); + } + const type = requiredText(value.type, "type", 64); + if (!EVENT_TYPES.includes(type)) throw new Error(`Invalid native goal event: unsupported type ${type}.`); + const source = normalizeSource(value.source); + const status = optionalText(value.status, "status", 64); + if (["started", "resumed", "status"].includes(type) && !status) { + throw new Error(`Invalid native goal event: ${type} requires status.`); + } + const capability = capabilityAssessment(source, options); + const mapping = capability.available + ? mappingFor(type, status, capability) + : { state: "unknown", limitKind: null, versioned: false, success: false }; + const availability = type === "malformed" + ? "malformed" + : capability.available ? (capability.evidenceClass === "imported" ? "imported" : "observed") : "unavailable"; + return { + schemaVersion: NATIVE_GOAL_EVENT_SCHEMA_VERSION, + eventId, + receivedAt: timestamp(value.receivedAt, "receivedAt"), + source, + workflow: normalizeWorkflow(value.workflow), + goalId: requiredText(value.goalId, "goalId", 512), + type, + status, + partialOutput: normalizePartialOutput(value.partialOutput, type), + availability, + evidenceClass: availability === "observed" ? "observed" : availability === "imported" ? "imported" : "unknown", + compatibility: capability.compatibility, + mapping, + raw: normalizeRaw(value.raw), + }; +} + +function eventLedgerPath(found) { + return path.join(found.runRoot, "integration", "native-goal-events.jsonl"); +} + +function readNativeGoalEvents(found) { + const filePath = eventLedgerPath(found); + if (!fs.existsSync(filePath)) return []; + return fs.readFileSync(filePath, "utf8") + .split(/\r?\n/) + .filter(Boolean) + .map((line, index) => { + let record; + try { + record = JSON.parse(line); + } catch { + throw new Error(`Invalid native goal event ledger line ${index + 1}.`); + } + if (!record.event || record.event.schemaVersion !== NATIVE_GOAL_EVENT_SCHEMA_VERSION) { + throw new Error(`Invalid native goal event ledger schema at line ${index + 1}.`); + } + return record; + }); +} + +function validateEventBinding(found, event) { + if (event.workflow.runId !== found.run.runId) { + throw new Error("Native goal event run id does not match the loaded workflow run."); + } + const task = event.workflow.taskId + ? found.run.tasks.find((entry) => entry.id === event.workflow.taskId) + : null; + if (event.workflow.taskId && !task) throw new Error(`Native goal event task not found: ${event.workflow.taskId}.`); + if (event.workflow.checkpointId && (!task || task.activeCheckpointId !== event.workflow.checkpointId)) { + throw new Error("Native goal event checkpoint does not match the active CEWP checkpoint."); + } + const binding = loadHostBinding(found); + if (!binding) throw new Error("Native goal event requires an existing host binding."); + if (binding.references.goalId !== event.goalId) { + throw new Error("Native goal event goal id does not match the host binding."); + } + if (event.source.path === "plugin" && binding.mode !== "attached") { + throw new Error("Plugin-observed native goal events require an attached binding."); + } + if (event.source.path === "explicit-intake" && binding.mode !== "explicit-intake") { + throw new Error("Explicit native goal events require an explicit-intake binding."); + } +} + +function applyEvent(found, event) { + const application = { + action: "none", + canonicalStateChanged: false, + successClaimed: false, + }; + if (!["observed", "imported"].includes(event.availability)) return { application, run: found.run }; + const current = found.run.status; + let interventionEvent = null; + if (event.mapping.state === "paused-host-limit" && ["active", "review-pending"].includes(current)) { + interventionEvent = "pause-host-limit"; + } else if (event.mapping.state === "interrupted" && ["active", "review-pending"].includes(current)) { + interventionEvent = "interrupt"; + } else if (event.mapping.state === "active" && ["paused-host-limit", "interrupted", "timed-out"].includes(current)) { + interventionEvent = "resume"; + } else if (event.mapping.state === "cancelled" && !["cancelled", "abandoned", "finalized"].includes(current)) { + interventionEvent = "cancel"; + } else if (event.mapping.state === "timed-out" && ["active", "interrupted"].includes(current)) { + interventionEvent = "timeout"; + } + + if (event.mapping.state === "revision-pending") application.action = "inspect-revision"; + if (event.mapping.state === "evidence-pending") application.action = "await-evidence"; + if (!interventionEvent) return { application, run: found.run }; + + const result = interveneWorkflow(found, { + event: interventionEvent, + reason: `Native goal ${event.type}${event.status ? ` (${event.status})` : ""}.`, + actor: event.evidenceClass === "observed" ? "host" : "operator-intake", + source: event.source.path, + now: new Date(event.receivedAt), + }); + return { + application: { + action: interventionEvent, + canonicalStateChanged: true, + successClaimed: false, + }, + run: result.run, + }; +} + +function recordNativeGoalEvent(found, candidate, options = {}) { + const current = loadWorkflowRun(found.repoRoot, found.run.runId); + const event = normalizeNativeGoalEvent(candidate, options); + validateEventBinding(current, event); + const existing = readNativeGoalEvents(current); + if (existing.some((entry) => entry.event.eventId === event.eventId)) { + throw new Error(`Native goal event already exists: ${event.eventId}.`); + } + const applied = applyEvent(current, event); + const record = { event, application: applied.application }; + const filePath = eventLedgerPath(current); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`); + return { ...record, run: applied.run }; +} + +module.exports = { + EVENT_TYPES, + NATIVE_GOAL_EVENT_SCHEMA_VERSION, + normalizeNativeGoalEvent, + readNativeGoalEvents, + recordNativeGoalEvent, +}; diff --git a/src/workflow/state.js b/src/workflow/state.js index f545891..3128d16 100644 --- a/src/workflow/state.js +++ b/src/workflow/state.js @@ -1214,7 +1214,7 @@ function interveneWorkflowRun(found, options, timestamp, reason) { hostLimit: { active: true, observedAt: timestamp, - source: "operator", + source: options.source || "operator", reason, }, pauseReason: "host-limit-active", @@ -1326,7 +1326,7 @@ function interveneWorkflowLifecycle(found, options, timestamp, reason) { checkpointId: null, classification: null, reason, - actor: "operator", + actor: options.actor || "operator", recordedAt: timestamp, }; const run = { diff --git a/tests/contracts/native-goal-events.js b/tests/contracts/native-goal-events.js new file mode 100644 index 0000000..cc6b591 --- /dev/null +++ b/tests/contracts/native-goal-events.js @@ -0,0 +1,221 @@ +"use strict"; + +const { assert } = require("../harness/lib/assertions"); +const { cleanupRepo, makeTempRepo } = require("../harness/lib/temp-repo"); +const { supportedSnapshot } = require("./integration-capabilities"); +const { validDefinition } = require("./workflow-definition"); +const { approveWorkflow } = require("./workflow-scheduler"); +const { createHostBinding } = require("../../src/integration/binding"); +const { + normalizeNativeGoalEvent, + readNativeGoalEvents, + recordNativeGoalEvent, +} = require("../../src/integration/native-goal"); +const { loadWorkflowRun, startWorkflowTask } = require("../../src/workflow/state"); + +function nativeDefinition(workflowId) { + const definition = validDefinition(); + definition.workflowId = workflowId; + definition.execution = { owner: "native", backend: null, allowedModes: ["supervised"] }; + return definition; +} + +function pluginSnapshot() { + const snapshot = supportedSnapshot(); + snapshot.surfaces.hostObservation = { + status: "supported", + source: "plugin", + pluginPathCapabilityTestPassed: true, + }; + return snapshot; +} + +function attach(repoRoot, runId, capabilities, goalId) { + const found = loadWorkflowRun(repoRoot, runId); + createHostBinding(found, { + schemaVersion: "host-binding/v1", + workflow: { runId, taskId: null, checkpointId: null }, + execution: { owner: "native", backend: null }, + host: { product: "codex", surface: "chatgpt-desktop", version: "0.137.0" }, + mode: "attached", + provenance: { + kind: "plugin-observed", + capabilitySchemaVersion: "codex-integration-capabilities/v1", + authenticationBoundary: "host-owned", + recordedAt: "2026-07-18T12:00:00.000Z", + }, + references: { goalId, threadId: "thread-native-1", turnId: null, subagents: [], worktree: null }, + controls: { + preventive: ["workflow-definition", "ownership-conflict"], + postExecution: [], + imported: [], + unavailable: ["private-desktop-session-control"], + }, + }, { capabilities }); +} + +function event(runId, type, status, sequence, overrides = {}) { + return { + schemaVersion: "native-goal-event/v1", + eventId: `native-event-${String(sequence).padStart(4, "0")}`, + receivedAt: `2026-07-18T12:${String(sequence).padStart(2, "0")}:00.000Z`, + source: { + path: "plugin", + codexVersion: "codex-cli 0.137.0", + nativeGoalSchemaVersion: "codex-app-server-schema/0.137.0", + capabilitySchemaVersion: "codex-integration-capabilities/v1", + authenticationBoundary: "host-owned", + }, + workflow: { + runId, + taskId: "implement-example", + checkpointId: "implement-example-attempt-0001", + }, + goalId: "goal-native-1", + type, + status, + partialOutput: null, + raw: { type, status, goalId: "goal-native-1" }, + ...overrides, + }; +} + +function activeNativeRun(repoRoot, workflowId, capabilities) { + const approved = approveWorkflow(repoRoot, nativeDefinition(workflowId)); + attach(repoRoot, approved.runId, capabilities, "goal-native-1"); + startWorkflowTask(loadWorkflowRun(repoRoot, approved.runId), "implement-example", { + now: new Date("2026-07-18T12:00:00.000Z"), + }); + return approved.runId; +} + +function main() { + const repoRoot = makeTempRepo("cewp-native-goal-events-"); + try { + const capabilities = pluginSnapshot(); + const runId = activeNativeRun(repoRoot, "native-event-matrix", capabilities); + + const started = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, runId), + event(runId, "started", "active", 1), + { capabilities }, + ); + assert(started.event.mapping.state === "active", "versioned start maps active"); + assert(started.application.action === "none", "already-active start does not duplicate checkpoint state"); + + const checkpoint = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, runId), + event(runId, "checkpoint", "active", 2), + { capabilities }, + ); + assert(checkpoint.event.mapping.state === "evidence-pending", "host checkpoint requires CEWP evidence intake"); + assert(checkpoint.application.successClaimed === false, "host checkpoint cannot claim CEWP success"); + + const revised = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, runId), + event(runId, "revised", "active", 3), + { capabilities }, + ); + assert(revised.application.action === "inspect-revision", "host revision remains an explicit CEWP revision decision"); + + const budgetLimited = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, runId), + event(runId, "status", "budgetLimited", 4), + { capabilities }, + ); + assert(budgetLimited.event.mapping.state === "paused-host-limit", "versioned goal budget maps host-limit pause"); + assert(budgetLimited.event.mapping.limitKind === "goal-budget", "goal budget stays distinct from account usage limits"); + assert(budgetLimited.run.status === "paused-host-limit", "host-limit mapping pauses canonical run state"); + assert(budgetLimited.run.tasks[0].status === "running", "host limit does not complete the checkpoint"); + + const resumed = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, runId), + event(runId, "resumed", "active", 5), + { capabilities }, + ); + assert(resumed.run.status === "active", "host reset/resume restores the canonical run"); + + const complete = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, runId), + event(runId, "status", "complete", 6), + { capabilities }, + ); + assert(complete.event.mapping.state === "evidence-pending", "native complete is not checkpoint success"); + assert(complete.run.status === "active", "native complete leaves CEWP gates open for evidence"); + + const partial = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, runId), + event(runId, "partial-output", null, 7, { + partialOutput: { present: true, bytes: 42, complete: false }, + }), + { capabilities }, + ); + assert(partial.event.mapping.state === "interrupted", "partial output maps interruption, not completion"); + assert(partial.run.status === "interrupted", "partial output preserves resumable canonical state"); + + const resumedPartial = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, runId), + event(runId, "resumed", "active", 8), + { capabilities }, + ); + assert(resumedPartial.run.status === "active", "partial output checkpoint resumes explicitly"); + + const stopped = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, runId), + event(runId, "stopped", null, 9), + { capabilities }, + ); + assert(stopped.run.status === "cancelled", "native stop maps explicit cancellation"); + assert(stopped.application.successClaimed === false, "stop never claims success"); + + const timeoutRunId = activeNativeRun(repoRoot, "native-timeout", capabilities); + const timedOut = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, timeoutRunId), + event(timeoutRunId, "timed-out", null, 10), + { capabilities }, + ); + assert(timedOut.run.status === "timed-out", "native timeout remains distinct from cancellation and failure"); + + const usageRunId = activeNativeRun(repoRoot, "native-usage-limit", capabilities); + const usageLimited = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, usageRunId), + event(usageRunId, "status", "usageLimited", 11), + { capabilities }, + ); + assert(usageLimited.event.mapping.limitKind === "account-usage", "usage limit stays distinct from goal budget"); + + const driftedEvent = event(runId, "status", "usageLimited", 12); + driftedEvent.source.nativeGoalSchemaVersion = "codex-app-server-schema/0.138.0"; + const drifted = normalizeNativeGoalEvent(driftedEvent, { capabilities }); + assert(drifted.mapping.state === "paused-host-limit", "drifted known limit uses generic host-limit fallback"); + assert(drifted.mapping.limitKind === "unknown", "schema drift does not infer exact limit semantics"); + assert(drifted.compatibility === "drifted", "schema drift is explicit"); + + const unavailable = normalizeNativeGoalEvent(event(runId, "status", "futureStatus", 13), { capabilities }); + assert(unavailable.mapping.state === "unknown", "unavailable future status remains unknown"); + + const malformed = recordNativeGoalEvent( + loadWorkflowRun(repoRoot, usageRunId), + event(usageRunId, "malformed", null, 14, { raw: { unexpected: true } }), + { capabilities }, + ); + assert(malformed.event.availability === "malformed", "malformed event is retained without state mutation"); + assert(malformed.application.action === "none", "malformed event cannot mutate canonical state"); + + const ledger = readNativeGoalEvents(loadWorkflowRun(repoRoot, runId)); + assert(ledger.length === 9, "main native lifecycle ledger is append-only"); + assert(ledger.every((entry) => entry.application.successClaimed === false), "native events never bypass CEWP success gates"); + + console.log("[PASS] native goal events map versioned limits and recovery without fabricating success"); + } finally { + cleanupRepo(repoRoot); + } +} + +try { + main(); +} catch (error) { + console.error("[FAIL] native goal event contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +}