diff --git a/README.md b/README.md index a3e5826..43aa468 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,9 @@ Nothing in that flow merges, pushes, publishes, tags, or creates a release. ## Codex Plugin -The thin plugin contributes exactly three conversational skills. CEWP Core and the CLI remain authoritative. +The thin plugin contributes exactly three conversational skills, a local stdio MCP bridge, and an optional +review-required subagent evidence hook. Every mutating MCP operation delegates to the same CEWP Core used +by the CLI; plugin surfaces do not become execution owners or bypass gates. From a source checkout: @@ -77,7 +79,14 @@ codex plugin add cewp@cewp-local codex plugin list ``` -Then ask Codex to plan a supervised run, run the current checkpoint, or resume an existing run. The plugin does not gain direct access to the host's private thread, native goal lifecycle, billing data, or persistent UI. +Then ask Codex to plan a supervised run, run the current checkpoint, or resume an existing run. The plugin +can expose `cewp_create`, `cewp_inspect`, `cewp_approve`, `cewp_continue`, `cewp_retry`, `cewp_revise`, +`cewp_verify`, and `cewp_finalize` when the package-provided `cewp-mcp` command is on `PATH`. It does not +gain direct access to the host's private thread, native goal lifecycle, billing data, or persistent UI. + +For a workflow with a validated host binding, `cewp integration controls --json` shows +preventive, post-execution, imported-observation, and unavailable control classes without promoting +audit-only evidence into enforcement. ## What CEWP Records @@ -112,6 +121,7 @@ CEWP still ships ten reusable engineering skills and the earlier Coordinator Mod - [Install Guide](docs/install.md) - [Supervised Workflow](docs/supervised-workflow.md) +- [External Integration Boundary](docs/external-integration-boundary.md) - [Workflow Runtime](docs/workflow-runtime.md) - [Known Limitations](docs/known-limitations.md) - [Pilot Kit](docs/pilot-kit.md) diff --git a/bin/cewp-mcp.js b/bin/cewp-mcp.js new file mode 100644 index 0000000..a3b0ce8 --- /dev/null +++ b/bin/cewp-mcp.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +"use strict"; + +const { runStdio } = require("../src/mcp/server"); + +runStdio(); diff --git a/bin/cewp.js b/bin/cewp.js index a6b91b2..c5c2af2 100644 --- a/bin/cewp.js +++ b/bin/cewp.js @@ -34,6 +34,7 @@ const { init } = require("../src/skills/install"); const { list, doctor } = require("../src/skills/status"); const { runSupervise } = require("../src/supervise/cli"); const { runWorkflow } = require("../src/workflow/cli"); +const { runIntegration } = require("../src/integration/cli"); const { printHuman: printSupervisedDemo, runSupervisedDemo } = require("../src/demo/supervised"); function runDemo(options) { @@ -209,12 +210,17 @@ async function main() { return; } + if (args.command === "integration") { + runIntegration(args); + return; + } + if (args.command === "demo") { runDemo(args); return; } - if (!["init", "list", "doctor", "policy", "run", "supervise", "workflow", "demo"].includes(args.command)) { + if (!["init", "list", "doctor", "policy", "run", "supervise", "workflow", "integration", "demo"].includes(args.command)) { throw new Error(`Unsupported command: ${args.command}`); } } catch (error) { diff --git a/docs/adr/0002-execution-ownership.md b/docs/adr/0002-execution-ownership.md index 3218f0d..84e2bdb 100644 --- a/docs/adr/0002-execution-ownership.md +++ b/docs/adr/0002-execution-ownership.md @@ -42,6 +42,11 @@ Receipts label each control as one of: Native and audit-only runs must not inherit managed claims. A hook or conversation warning may project a decision but never becomes the ownership or enforcement source. +The runtime materializes these classifications as `integration-control-receipt/v1` and exposes the receipt +through `cewp integration controls --json`. Audit-only bindings with preventive entries or +controls assigned to multiple classes are invalid. Imported entries explicitly render as observed, not +enforced, and receipt inspection verifies the artifact still matches its validated host binding. + ## Consequences The runtime needs a deterministic ownership registry and conflict fixtures before the supervised golden path ships. Recovery can safely resume only after worktree, plan, policy, owner, backend, and process state are compatible. App Server may later replace `codex-exec` for a managed checkpoint only through a new capability and migration decision; it cannot run beside it for the same checkpoint. diff --git a/docs/adr/0005-codex-integration-backend.md b/docs/adr/0005-codex-integration-backend.md index b654a4c..dbf01a9 100644 --- a/docs/adr/0005-codex-integration-backend.md +++ b/docs/adr/0005-codex-integration-backend.md @@ -39,6 +39,7 @@ The Phase 11 review used the current Codex manual and a controlled local probe a - Existing `codex-exec` users keep a stable fallback and one backend per managed checkpoint. - Native goals remain useful without CEWP pretending to control or inspect a private host session. - External agent interfaces can use MCP and `operator-json/v1` without CEWP building a competing terminal or desktop UI. +- The shipped MCP transport is local stdio only, fixes repository scope to process `cwd`, and delegates all eight operations directly to CEWP Core. It is a control surface, not an execution owner or reviewer. - Capability or schema drift produces an explicit compatibility warning and returns to generated-goal or explicit intake. - App Server can be reconsidered later without changing CEWP's provider-neutral workflow and evidence schemas. diff --git a/docs/codex-capability-matrix.md b/docs/codex-capability-matrix.md index c6ec9f3..769833a 100644 --- a/docs/codex-capability-matrix.md +++ b/docs/codex-capability-matrix.md @@ -47,10 +47,12 @@ Schema presence does not prove that a plugin can attach to the desktop app's exi | Desktop notifications | host-specific | The host owns documented notification behavior and settings. CEWP has no arbitrary notification category. | | Hook `statusMessage` | supported | Official hook configuration exposes it as transient handler status. | | Hook `systemMessage` | supported | Official hook output exposes it as a UI or event-stream warning. | +| `SubagentStart`/`SubagentStop` evidence | supported, opt-in | The plugin records only documented parent session/turn, agent id/type, permission/model context, and bounded stop summary after separate CEWP approval and host `/hooks` trust. The documented input exposes no subagent thread id, so CEWP preserves it as `unknown`. | +| Hook trust and version drift | supported | `npm run test:integration-hook-evidence` binds the exact bundle, Codex version, CEWP runtime, hook contract, and workflow revision. Drift or malformed input emits a warning, appends no trusted evidence, and leaves Core gates unchanged. | | `PreToolUse` deny output | supported | The deterministic fixture emits the documented `permissionDecision: deny` shape and is covered by `npm run test:hook-output`. | | `PreToolUse` as complete enforcement | unavailable | Official docs exclude or limit richer shell and non-MCP paths. A real CLI 0.137.0 Windows probe executed the requested PowerShell command despite the Bash deny hook. Core policy remains authoritative. | | Hook-based instant turn cancellation | unknown | Stop semantics do not establish instantaneous cancellation of an in-flight model or external process. | -| Local MCP to CEWP Core | unknown | MCP is supported by the host. Phase 11 implements a small Core-backed tool surface while conversation and CLI fallbacks remain required. | +| Local MCP to CEWP Core | supported | `cewp-mcp` implements the documented local stdio JSON-RPC lifecycle and exactly eight Core-backed tools. `npm run test:integration-mcp` proves schema validation, current-directory repository scope, Core state transitions, confirmation gates, business errors, protocol errors, and explicit protocol-version drift fallback without credentials. | ## App Server Boundary @@ -102,7 +104,7 @@ Reasons: - App Server adds useful goal metadata and lifecycle methods, but remains a separately owned experimental process with version drift and unresolved authenticated usage/cancellation behavior. - The spike did not demonstrate enough recovery or accounting advantage to justify shipping two incomplete managed backends. -The native fallback is a bounded generated goal brief plus supported host goal tools or explicit result intake. `audit-only` remains available for evidence supplied by another owner. The local MCP bridge is the next supported integration surface. Hooks and Apps SDK UI remain optional projections; their absence never weakens CEWP Core. See [ADR 0005](adr/0005-codex-integration-backend.md). +The native fallback is a bounded generated goal brief plus supported host goal tools or explicit result intake. `audit-only` remains available for evidence supplied by another owner. The local MCP bridge is the supported headless integration surface. Hooks and Apps SDK UI remain optional projections; their absence never weakens CEWP Core. See [ADR 0005](adr/0005-codex-integration-backend.md) and the [external integration boundary](external-integration-boundary.md). ## Reproduction @@ -115,6 +117,8 @@ npm run probe:codex-app-server npm run test:plugin-lifecycle npm run test:hook-output npm run test:integration-capabilities +npm run test:integration-hook-evidence +npm run test:integration-mcp ``` The nested model probe is intentionally excluded from automated tests because it consumes account usage. Raw account values, credentials, thread ids, and machine-specific paths are not part of this document. diff --git a/docs/external-integration-boundary.md b/docs/external-integration-boundary.md new file mode 100644 index 0000000..e1645eb --- /dev/null +++ b/docs/external-integration-boundary.md @@ -0,0 +1,51 @@ +# External Integration Boundary + +CEWP is a local workflow, governance, verification, and evidence runtime. A third-party interface may +present CEWP state in its own agent UI, but it must not become the execution owner merely by calling a +CEWP control surface. It remains responsible for its UI, user consent, authentication, transport lifecycle, +and any agent process it starts. + +## Supported Headless Surfaces + +`operator-json/v1` is the stable envelope shape for CLI inspection and control results. External tools may +invoke documented `cewp ... --json` commands, retain the `command`, `generatedAt`, `data`, and `warnings` +fields, and render them without converting warnings or observations into PASS. In particular, +`cewp integration controls --json` keeps preventive, post-execution, imported, and +unavailable controls distinct. + +`cewp-mcp` is the structured local stdio bridge. Configure its current working directory as exactly one +intended repository. It exposes create, inspect, approve, continue, retry, revise, verify, and finalize; the +tools import the same CEWP Core services as the CLI. An MCP client may add its own confirmation UI, but +cannot bypass Core approval, ownership, policy, effort, scope, budget, verification, receipt, or reviewer +gates. The server opens no network listener and provides no host account, billing, or private-session data. +Unsupported MCP protocol versions return an explicit `mcp-protocol-version-drift` compatibility warning +and name `cewp-cli-operator-json` as the safe fallback. + +Hooks and conversation messages are optional projections. Hook evidence is separately trusted and +version-bound; a missing, disabled, stale, or malformed hook never changes Core enforcement. + +## Execution Ownership + +The external UI is not a fourth owner. Each run remains `managed`, `native`, or `audit-only`. A managed +checkpoint retains one backend and one CEWP-owned worktree. Native work remains host-owned. Audit-only +evidence may be imported or checked after execution but never presented as preventive enforcement. +Provider-specific host, goal, thread, turn, and subagent identifiers stay in integration sidecars rather +than provider-neutral workflow schemas. + +## Rich Codex Clients And App Server + +A client that needs rich Codex thread, turn, or goal lifecycle should integrate with the documented Codex +App Server and own that separate process, authentication boundary, thread identifiers, selected working +directory, interruption behavior, and cleanup. That client is separate from the CEWP plugin. Its process +does not attach to the ChatGPT desktop app's existing internal session, inherit private desktop credentials, +or turn App Server schema presence into plugin capability. + +CEWP has not graduated App Server as a managed backend. A request for that ungraduated backend falls back +to the selected `codex-exec` path, which already owns isolated dispatch, artifacts, verification, recovery, +and reviewer gates. External clients can still use CEWP MCP or operator JSON around their own UI without +changing this backend decision. + +CEWP will build no custom terminal-session protocol, terminal server, desktop shell, private Codex +protocol adapter, UI scraper, or undocumented desktop-session attachment. A richer client should compose +the supported Codex App Server and CEWP's headless surfaces rather than making CEWP a competing terminal +product. diff --git a/docs/install.md b/docs/install.md index ab7dd01..89c0a61 100644 --- a/docs/install.md +++ b/docs/install.md @@ -228,6 +228,16 @@ The harness uses temporary repos, exercises Coordinator Mode runtime helpers, an The supervised demo uses a deterministic fake Codex process in a temporary repository. It does not use credentials or start a real provider. +The npm package also installs the plugin-declared local MCP command: + +```bash +cewp-mcp +``` + +It is a stdio protocol process, not an interactive shell command or network server. Codex starts it from +the repository selected for the task. Third-party MCP clients may configure the same command with the +intended repository as `cwd`; the command must be available on `PATH`. + If Codex does not show installed skills, restart or reload Codex and confirm that each skill has: ```txt diff --git a/docs/known-limitations.md b/docs/known-limitations.md index d17d910..abd8be4 100644 --- a/docs/known-limitations.md +++ b/docs/known-limitations.md @@ -10,7 +10,8 @@ CEWP is beta software. These limits are product boundaries, not hidden roadmap p - ChatGPT subscription credit impact and host-internal retries or compaction remain `unknown` without a supported machine-readable contract. - Numeric usage estimates stay unavailable until enough comparable local runs exist. When available, they are ranges with confidence and sample basis, never point promises. - File-level test-authoring enforcement recognizes common test directories and filename conventions. It cannot prove whether production code contains test-like logic. -- The plugin contributes skills only. It does not provide MCP tools, hooks, an Apps SDK card, or an App Server client. +- The plugin contributes skills, a local stdio MCP bridge, and an optional review-required `SubagentStart`/`SubagentStop` evidence hook. The hook cannot expose a subagent thread id, does not read transcripts, and is never a Core enforcement boundary. MCP exposes only CEWP Core operations and does not attach to native host sessions. An Apps SDK card and App Server client are not shipped. +- Audit-only integration can validate imported evidence and record post-execution checks, but it cannot claim that CEWP prevented actions performed by the external owner. Its integration control receipt therefore permits no preventive entries. - Experimental OpenCode execution remains optional and outside the supervised golden path. Binary/version availability does not prove authentication or model readiness. - Manual is a non-executing handoff adapter. Claude, Gemini, Hermes, and other providers are not implemented. - Supervised worktree cleanup automation is not shipped; rollback is available for owned unverified work, and terminal evidence is retained for deliberate inspection/removal. diff --git a/docs/release-notes.md b/docs/release-notes.md index 20e756a..91ce82a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,34 @@ No changes yet. +## 0.11.0-beta.0 + +### Summary + +Codex-first native-goal supervision and integration bridge preparation. Phase 11 keeps managed, native, +and audit-only ownership separate, retains `codex-exec`, adds supported headless integration surfaces, +and preserves provider-neutral workflow state. This version is prepared locally and is not published, +tagged, or released; clean Linux validation remains required before the technical release gate can close. + +### Added + +- Enforced cross-mode worktree conflicts so native and managed ownership cannot target the same CEWP task worktree or active checkpoint. +- Added explicit implementation, repair, and reviewer task classes with operator-approved model/effort revisions and no automatic model routing. +- Added opt-in, exact-definition and version-bound `SubagentStart`/`SubagentStop` evidence. Hook absence, drift, malformed input, or host distrust leaves Core gates unchanged. +- Added a local stdio bridge with eight Core-backed MCP tools for create, inspect, approve, continue, retry, revise, verify, and finalize. MCP and CLI call the same services and preserve the same Core gates. +- Added MCP protocol-drift negotiation with a stable compatibility warning and CLI/operator-JSON fallback. +- Added structured host observations that keep observed, imported, stale, malformed, unavailable, and unknown truth states distinct without inventing billing impact. +- Added `integration-control-receipt/v1` and `cewp integration controls` so audit-only evidence cannot be presented as preventive enforcement. +- Added a packaged external-integration boundary for third-party MCP/operator JSON clients and rich Codex clients that own a separate App Server lifecycle. + +### Changed + +- App Server remains ungraduated because no material supported lifecycle, usage, or recovery advantage was proven. An explicit request retains the `codex-exec` fallback. +- Provider-specific host, goal, thread, turn, subagent, and worktree references stay outside provider-neutral workflow schemas. +- Host goal completion, hook completion, and imported evidence never count as CEWP verification or independent reviewer PASS. +- Independent external pilot evidence remains Phase 13 validation debt; fixtures, maintainer dogfood, and multiple machines used by one maintainer do not satisfy it. +- No provider, desktop UI, terminal server, merge, push, publish, tag, or release automation was added. + ## 0.10.0-beta.0 ### Summary diff --git a/docs/skill-plugin-compatibility.md b/docs/skill-plugin-compatibility.md index 1a06e95..a6f9ac6 100644 --- a/docs/skill-plugin-compatibility.md +++ b/docs/skill-plugin-compatibility.md @@ -1,6 +1,6 @@ # Skill And Plugin Compatibility -Observed: 2026-07-16 +Observed: 2026-07-22 CEWP's ten bundled skills use the current Codex skill shape: a required `SKILL.md` with `name` and `description`, plus optional `scripts/`, `references/`, `assets/`, and `agents/`. `agents/openai.yaml` is accepted as optional UI, invocation-policy, and dependency metadata. CEWP no longer treats these official components as forbidden. @@ -23,7 +23,7 @@ The Phase 9 plugin follows the official boundary: - `.codex-plugin/plugin.json` is the required manifest and the only file under `.codex-plugin/`. - `skills/`, `hooks/`, `.mcp.json`, `.app.json`, and `assets/` live at the plugin root and use `./`-prefixed contained paths. -- Installing or enabling a plugin does not trust its bundled hooks. CEWP hooks remain optional until the user reviews and trusts the current definition. +- Installing or enabling a plugin does not trust its bundled hooks. CEWP declares one contained subagent-evidence bundle, requires a run-bound operator approval, and still directs the user to `/hooks` to review and trust the exact current host definition. Bundle, Codex, CEWP runtime, hook-contract, or workflow-revision drift disables trusted evidence and falls back to Core plus conversation output. - A repo marketplace lives at `.agents/plugins/marketplace.json`; npm remains the source of the CEWP Core CLI/runtime. - MCP and Apps SDK components are optional projections. The plugin skeleton and golden path cannot depend on them until their versioned capability tests pass. diff --git a/docs/supervised-workflow.md b/docs/supervised-workflow.md index 10e57ad..c718924 100644 --- a/docs/supervised-workflow.md +++ b/docs/supervised-workflow.md @@ -56,6 +56,26 @@ Changing the goal, scope, checks, or stopping conditions creates a new plan revi ## 3. Execute And Verify +An optional Codex-specific sidecar can assign an explicit task class without changing the +provider-neutral workflow or run schemas: + +```bash +cewp supervise effort \ + --operation implementation \ + --task-class demanding-implementation \ + --model \ + --effort high \ + --yes +``` + +Supported operations are `implementation`, `repair`, and `reviewer`. Supported task classes are +`fast-exploration`, `demanding-implementation`, and `high-effort-independent-review`. A task class +never selects a model or reasoning effort automatically. Every initial selection or change requires +`--yes`, creates a revision with an operator-approval digest, and fails closed when the sidecar's +approved selection digest no longer matches. Model and effort remain `unknown` when omitted. Explicit selections become known +effective evidence only after supported structured turn-completion usage is received; an early CLI, +model, or host rejection leaves them unknown. + Managed execution is an advanced local action: ```bash @@ -136,6 +156,29 @@ Generated files include: Editing `progress.md` does not mutate `run.json`. Managed token categories are observed only from valid structured turn events. Host-internal usage remains unknown when the selected boundary does not expose it. +## Local MCP Bridge + +The npm package installs `cewp-mcp`, a local stdio MCP server. The Codex plugin declares it through +`plugins/cewp/.mcp.json`; it opens no network listener and uses the server process working directory as +the fixed repository root. The eight tools are `cewp_create`, `cewp_inspect`, `cewp_approve`, +`cewp_continue`, `cewp_retry`, `cewp_revise`, `cewp_verify`, and `cewp_finalize`. + +The MCP dispatcher imports the same CEWP Core functions used by `cewp supervise`; it does not invoke a +second workflow implementation. CLI `--yes` gates map to a required `confirm: true` argument for approve, +retry, and finalize. All state, execution ownership, policy, effort, scope, budget, verification, receipt, +and independent-review checks remain in Core. MCP host approval is an additional host safety boundary and +never substitutes for those checks. Business-rule failures return structured tool errors; malformed calls +and unknown tools return JSON-RPC protocol errors. + +During initialization, a supported MCP protocol version is echoed with `compatibility.compatible: true`. +An unsupported version negotiates the current supported version but also returns the stable +`mcp-protocol-version-drift` warning and `cewp-cli-operator-json` fallback; clients should disconnect or +use that fallback rather than assuming newer semantics. + +The plugin MCP entry expects the package-provided `cewp-mcp` binary to be on `PATH`. If a third-party MCP +client cannot discover the plugin manifest, configure a local stdio server with command `cewp-mcp` and set +its working directory to the intended repository. Do not point one server process at multiple repositories. + ## Cleanup `cewp demo supervised` removes its temporary repository and worktree automatically. diff --git a/docs/workflow-runtime.md b/docs/workflow-runtime.md index 1c07524..acaa7f9 100644 --- a/docs/workflow-runtime.md +++ b/docs/workflow-runtime.md @@ -36,6 +36,22 @@ A succeeded `task-result/v1` must include every approved baseline, targeted, and A failed `task-result/v1` is still evidence. CEWP validates its scope, approved verification commands, failure classification and signature, bounded output, and usage truth. It persists the result, accounts observed CEWP-controlled operations, and moves the checkpoint, task, and run to `blocked`. An identical canonical signature is derived as `repeated-failure`; a provider cannot self-declare it. Unknown host-internal work remains unknown and is not fabricated as observed usage. +## Integration Control Claims + +A validated host binding writes `integration/control-receipt.json` beside the provider-specific binding, +outside provider-neutral workflow state. Inspect it with: + +```bash +cewp integration controls --json +``` + +`integration-control-receipt/v1` classifies each named check as `preventive`, `postExecution`, `imported`, +or `unavailable`. One check cannot occupy multiple classes. Audit-only ownership cannot claim any preventive +control: imported evidence is rendered as `observed-not-enforced`, while a CEWP check performed after import +remains post-execution rather than preventive. The receipt is derived from and checked against the validated +host binding, so editing the artifact cannot promote an observation into enforcement. Guardrail authority +remains CEWP Core outside provider-controlled execution. + Recovery is explicit: retry, revise, reassign, a permitted pre-existing-failure waiver, rollback, cancel, or abandon. New regressions, repeated failures, scope gates, destructive-operation policy, and required reviewer PASS are non-waivable. A failed or unverified checkpoint never counts as completed. ## Budgets diff --git a/package.json b/package.json index 811b8db..c2fba85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@setrathex/codex-engineering-workflow-pack", - "version": "0.10.0-beta.0", + "version": "0.11.0-beta.0", "description": "Long-running Codex goals without blind runs: local-first supervision, verification, recovery, review, and evidence.", "license": "MIT", "repository": { @@ -13,6 +13,7 @@ }, "bin": { "cewp": "bin/cewp.js", + "cewp-mcp": "bin/cewp-mcp.js", "codex-engineering-workflow-pack": "bin/cewp.js" }, "files": [ @@ -29,6 +30,7 @@ "docs/known-limitations.md", "docs/pilot-kit.md", "docs/codex-capability-matrix.md", + "docs/external-integration-boundary.md", "docs/adr/0001-codex-first-supervised-goals.md", "docs/adr/0002-execution-ownership.md", "docs/adr/0003-cost-assurance-and-safe-pauses.md", @@ -50,7 +52,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 && npm run test:native-goal-events", + "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-effort-policy && npm run test:integration-hook-evidence && npm run test:integration-mcp && 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", @@ -90,6 +92,9 @@ "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-effort-policy": "node tests/contracts/integration-effort-policy.js", + "test:integration-hook-evidence": "node tests/contracts/integration-hook-evidence.js", + "test:integration-mcp": "node tests/contracts/integration-mcp.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", @@ -98,9 +103,10 @@ "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/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: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/cli.js && node --check ./src/integration/effort-policy.js && node --check ./src/integration/hook-evidence.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 ./plugins/cewp/hooks/capture-subagent.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-effort-policy.js && node --check ./tests/contracts/integration-hook-evidence.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", + "check:mcp": "node --check ./bin/cewp-mcp.js && node --check ./src/mcp/server.js && node --check ./src/mcp/tools.js && node --check ./tests/contracts/integration-mcp.js", + "check": "npm run check:syntax && npm run check:workflow-syntax && npm run check:mcp && npm test", "pack:dry-run": "npm pack --dry-run" }, "keywords": [ diff --git a/plugins/cewp/.codex-plugin/plugin.json b/plugins/cewp/.codex-plugin/plugin.json index b04e299..2c21b5c 100644 --- a/plugins/cewp/.codex-plugin/plugin.json +++ b/plugins/cewp/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cewp", - "version": "0.10.0-beta.0", + "version": "0.11.0-beta.0", "description": "Plan, run, recover, and review bounded engineering checkpoints through the local CEWP runtime.", "author": { "name": "SetraTheXX", @@ -16,6 +16,8 @@ "evidence" ], "skills": "./skills/", + "mcpServers": "./.mcp.json", + "hooks": "./hooks/hooks.json", "interface": { "displayName": "CEWP", "shortDescription": "Long-running goals without blind runs", diff --git a/plugins/cewp/.mcp.json b/plugins/cewp/.mcp.json new file mode 100644 index 0000000..7aa9e5b --- /dev/null +++ b/plugins/cewp/.mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "cewp": { + "command": "cewp-mcp", + "args": [], + "env": {} + } + } +} diff --git a/plugins/cewp/README.md b/plugins/cewp/README.md index 5fba658..d6fcf4a 100644 --- a/plugins/cewp/README.md +++ b/plugins/cewp/README.md @@ -8,7 +8,27 @@ It ships exactly three entry skills: - `run-supervised-checkpoint`: execute one controlled model operation and follow Core gates. - `resume-supervised-run`: inspect or recover canonical state without silently restarting work. -The plugin does not attach to the ChatGPT desktop app's private thread, automate native goals, inject persistent UI, expose hidden host usage, execute the optional OpenCode adapter, or add another provider. Phase 9 uses one selected pair: `managed` owner with the `codex-exec` backend. +The plugin does not attach to the ChatGPT desktop app's private thread, automate native goals, inject persistent UI, expose hidden host usage, execute the optional OpenCode adapter, or add another provider. The managed path uses one selected pair: `managed` owner with the `codex-exec` backend. + +## Local MCP Tools + +The plugin declares one local stdio server backed by the npm package's `cewp-mcp` command. It exposes +create, inspect, approve, continue, retry, revise, verify, and finalize as structured tools. The server fixes +repository scope to its working directory and imports the same CEWP Core services as the CLI. MCP host +consent never replaces Core approval, ownership, policy, effort, scope, budget, verification, receipt, or +independent-review gates. + +## Optional Subagent Evidence + +The plugin declares one `SubagentStart`/`SubagentStop` hook bundle. Installation or enablement does not trust it. For a selected workflow run, first inspect the bundle and activate the CEWP-side binding: + +```bash +cewp integration hooks approve --yes --json +``` + +Then open `/hooks` in Codex, review the exact current definition, and decide whether to trust it. `cewp integration hooks status --json` reports bundle, Codex, CEWP runtime, hook-contract, and workflow-revision drift. A changed or malformed source produces a warning and no trusted evidence. + +The hook records bounded parent session/turn references, the documented subagent id/type, and the stop summary in a provider-specific sidecar. The documented hook input does not expose a subagent thread id, so that field remains `unknown`. The handler never reads transcript files, never blocks or continues a subagent, and never opens a policy, verification, review, or finalization gate. Install from the CEWP source marketplace: @@ -17,4 +37,4 @@ codex plugin marketplace add /path/to/Codex-Engineering-Workflow-Pack codex plugin add cewp@cewp-local ``` -The npm package supplies the `cewp` runtime used by these skills. Run `cewp doctor --json` before the first supervised checkpoint. +The npm package supplies the `cewp` runtime used by the skills and optional evidence handler. Run `cewp doctor --json` before the first supervised checkpoint. diff --git a/plugins/cewp/hooks/capture-subagent.js b/plugins/cewp/hooks/capture-subagent.js new file mode 100644 index 0000000..7d0b9ed --- /dev/null +++ b/plugins/cewp/hooks/capture-subagent.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node +"use strict"; + +const childProcess = require("node:child_process"); + +process.stdin.setEncoding("utf8"); +let input = ""; +let oversized = false; +process.stdin.on("data", (chunk) => { + input += chunk; + if (Buffer.byteLength(input, "utf8") > 64 * 1024) oversized = true; +}); +process.stdin.on("end", () => { + try { + if (oversized) throw new Error("hook input exceeds 65536 bytes"); + JSON.parse(input || "{}"); + const command = process.env.CEWP_HOOK_CLI_COMMAND || (process.platform === "win32" ? "cewp.cmd" : "cewp"); + const prefixArgs = process.env.CEWP_HOOK_CLI_PREFIX_ARGS + ? JSON.parse(process.env.CEWP_HOOK_CLI_PREFIX_ARGS) + : []; + if (!Array.isArray(prefixArgs) || prefixArgs.some((value) => typeof value !== "string")) { + throw new Error("invalid CEWP hook CLI prefix configuration"); + } + const result = childProcess.spawnSync(command, [ + ...prefixArgs, + "integration", "hooks", "ingest", + ], { + cwd: process.cwd(), + input, + encoding: "utf8", + windowsHide: true, + timeout: 15000, + shell: process.platform === "win32" && !process.env.CEWP_HOOK_CLI_COMMAND, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + const reason = String(result.stderr || "CEWP hook ingestion failed") + .replace(/\s+/g, " ") + .trim() + .slice(0, 1000); + throw new Error(reason); + } + process.stdout.write("{}\n"); + } catch (error) { + process.stdout.write(`${JSON.stringify({ + systemMessage: `CEWP hook evidence unavailable: ${error.message} Core gates remain unchanged.`, + })}\n`); + } +}); diff --git a/plugins/cewp/hooks/hooks.json b/plugins/cewp/hooks/hooks.json new file mode 100644 index 0000000..ef858e0 --- /dev/null +++ b/plugins/cewp/hooks/hooks.json @@ -0,0 +1,31 @@ +{ + "description": "Optional CEWP subagent lifecycle evidence. CEWP Core gates remain authoritative.", + "hooks": { + "SubagentStart": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "node \"$PLUGIN_ROOT/hooks/capture-subagent.js\"", + "command_windows": "node \"%PLUGIN_ROOT%\\hooks\\capture-subagent.js\"", + "statusMessage": "Recording optional CEWP subagent evidence" + } + ] + } + ], + "SubagentStop": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "node \"$PLUGIN_ROOT/hooks/capture-subagent.js\"", + "command_windows": "node \"%PLUGIN_ROOT%\\hooks\\capture-subagent.js\"", + "statusMessage": "Recording optional CEWP subagent summary" + } + ] + } + ] + } +} diff --git a/plugins/cewp/skills/run-supervised-checkpoint/SKILL.md b/plugins/cewp/skills/run-supervised-checkpoint/SKILL.md index 717e208..4350cc0 100644 --- a/plugins/cewp/skills/run-supervised-checkpoint/SKILL.md +++ b/plugins/cewp/skills/run-supervised-checkpoint/SKILL.md @@ -11,9 +11,9 @@ Use canonical JSON from `cewp supervise`; generated Markdown is a view, not muta 2. Perform no more than one CEWP-controlled model operation per invocation. Local verification commands do not count as model operations, but their approved limits still apply. 3. Follow only the transition matching canonical state: - `proposed`: show the plan and run `cewp supervise approve --yes --json` only after explicit approval. Stop before dispatch. - - `approved/ready`: run `cewp supervise execute --yes --json` only after explicit execution approval. If dispatch reaches `verifying`, run `cewp supervise verify --json`; then stop at the verified, repair, paused, or blocked result. - - `needs-repair/repair-ready`: explain the failure signature and remaining repair allocation. Run `cewp supervise retry --yes --json` only when the user explicitly chooses retry, then run local verification and stop. - - `checkpoint-complete/verified`: offer final review or a safe pause for one manually bounded next checkpoint. For final review, record `cewp supervise continue --json`, run `cewp supervise review --yes --json`, and stop at the reviewer decision. For more work, hand off to the resume workflow without dispatching another model operation. + - `approved/ready`: if the operator explicitly requested a Codex task class, model, or effort, record it first with `cewp supervise effort --operation implementation --task-class [--model ] [--effort ] --yes --json`. Never infer model or effort from the class. Then run `cewp supervise execute --yes --json` only after explicit execution approval. If dispatch reaches `verifying`, run `cewp supervise verify --json`; then stop at the verified, repair, paused, or blocked result. + - `needs-repair/repair-ready`: explain the failure signature and remaining repair allocation. Record any explicitly requested repair-specific Codex selection through `cewp supervise effort ... --operation repair ... --yes --json`; do not reuse or change implementation settings implicitly. Run `cewp supervise retry --yes --json` only when the user explicitly chooses retry, then run local verification and stop. + - `checkpoint-complete/verified`: offer final review or a safe pause for one manually bounded next checkpoint. For final review, record any explicitly requested independent-review Codex selection through `cewp supervise effort ... --operation reviewer ... --yes --json`, record `cewp supervise continue --json`, run `cewp supervise review --yes --json`, and stop at the reviewer decision. For more work, hand off to the resume workflow without dispatching another model operation. - `review-passed`: run `cewp supervise receipt --json` to preview the receipt. Do not finalize in the same step unless the user already gave explicit finalize intent after seeing equivalent receipt facts. - `ready-to-finalize`: run `cewp supervise finalize --yes --json` only after explicit finalization approval. - any `paused-*` or `blocked`: do not dispatch. Present Core recovery actions and hand off to the resume workflow. diff --git a/src/cli/parse.js b/src/cli/parse.js index 15b3c04..730114d 100644 --- a/src/cli/parse.js +++ b/src/cli/parse.js @@ -74,6 +74,10 @@ function parseArgs(argv) { workerId: undefined, templateName: undefined, compilerDigest: undefined, + operation: undefined, + taskClass: undefined, + model: undefined, + effort: undefined, }; if (argv[0] === "--help" || argv[0] === "-h") { @@ -93,7 +97,7 @@ function parseArgs(argv) { return args; } - const optionStart = ["run", "supervise", "workflow", "demo"].includes(args.command) ? 2 : 1; + const optionStart = ["run", "supervise", "workflow", "integration", "demo"].includes(args.command) ? 2 : 1; for (let index = optionStart; index < argv.length; index += 1) { const arg = argv[index]; @@ -128,6 +132,21 @@ function parseArgs(argv) { continue; } + if (args.command === "integration" && args.subcommand === "hooks" && index === 2) { + args.action = arg; + continue; + } + + if (args.command === "integration" && args.subcommand === "hooks" && index === 3 && !arg.startsWith("--")) { + args.workflowRunId = arg; + continue; + } + + if (args.command === "integration" && args.subcommand === "controls" && index === 2 && !arg.startsWith("--")) { + args.workflowRunId = arg; + continue; + } + if (args.command === "workflow" && args.subcommand === "validate" && index === 2 && !arg.startsWith("--")) { args.definitionFile = arg; continue; @@ -203,7 +222,7 @@ function parseArgs(argv) { if ( args.command === "supervise" && [ - "approve", "status", "execute", "verify", "retry", "review", "receipt", "finalize", + "approve", "status", "execute", "verify", "retry", "review", "receipt", "finalize", "effort", "revise", "pause", "resume", "add-budget", "rollback", "cancel", "abandon", "block", "continue", "reassign", ].includes(args.subcommand) && index === 2 @@ -213,6 +232,38 @@ function parseArgs(argv) { continue; } + if (args.command === "supervise" && arg === "--operation") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error("--operation requires an effort-policy operation."); + args.operation = value; + index += 1; + continue; + } + + if (args.command === "supervise" && arg === "--task-class") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error("--task-class requires a Codex task class."); + args.taskClass = value; + index += 1; + continue; + } + + if (args.command === "supervise" && arg === "--model") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error("--model requires an explicit Codex model."); + args.model = value; + index += 1; + continue; + } + + if (args.command === "supervise" && arg === "--effort") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error("--effort requires a supported Codex reasoning effort."); + args.effort = value; + index += 1; + continue; + } + if ( ( args.command === "supervise" @@ -410,7 +461,7 @@ function parseArgs(argv) { continue; } - if (["doctor", "run", "supervise", "workflow", "demo"].includes(args.command) && arg === "--json") { + if (["doctor", "run", "supervise", "workflow", "integration", "demo"].includes(args.command) && arg === "--json") { args.json = true; continue; } @@ -425,6 +476,12 @@ function parseArgs(argv) { continue; } + + if (args.command === "integration" && arg === "--yes") { + args.yes = true; + continue; + } + if (args.command === "supervise" && arg === "--allow-test-authoring") { args.allowTestAuthoring = true; continue; diff --git a/src/cli/usage.js b/src/cli/usage.js index b804c0c..c4a5afa 100644 --- a/src/cli/usage.js +++ b/src/cli/usage.js @@ -17,6 +17,9 @@ Usage: cewp workflow result --task --result --yes [--json] cewp workflow review --result --yes [--json] cewp workflow finalize --yes [--json] + cewp integration hooks approve --yes [--json] + cewp integration hooks status [--json] + cewp integration controls [--json] cewp workflow revise --proposal [--from ] [--json] cewp workflow apply-revision --proposal --digest --yes [--json] cewp workflow migrate [--digest --yes] [--json] @@ -31,6 +34,7 @@ Usage: cewp supervise plan --proposal [--from ] [--source-kind ] [--json] cewp supervise approve [run-id] [--allow-test-authoring] --yes [--json] cewp supervise status [run-id] [--json] + cewp supervise effort [run-id] --operation --task-class [--model ] [--effort ] --yes [--json] cewp supervise execute [run-id] --yes [--timeout ] [--json] cewp supervise verify [run-id] [--timeout ] [--json] cewp supervise retry [run-id] --yes [--timeout ] [--json] diff --git a/src/integration/binding.js b/src/integration/binding.js index a9027b4..7cbc46f 100644 --- a/src/integration/binding.js +++ b/src/integration/binding.js @@ -7,10 +7,17 @@ const { selectManagedBackend, validateCodexCapabilitySnapshot, } = require("./capabilities"); +const { + OWNERSHIP_SCHEMA_VERSION, + findOwnershipConflict, + loadOwnershipRecords, + validateOwnershipRecord, +} = require("../run/ownership"); const { writeJsonAtomic } = require("../workflow/state"); const HOST_BINDING_SCHEMA_VERSION = "host-binding/v1"; const GENERATED_GOAL_BRIEF_SCHEMA_VERSION = "generated-goal-brief/v1"; +const INTEGRATION_CONTROL_RECEIPT_SCHEMA_VERSION = "integration-control-receipt/v1"; const HOST_SURFACES = Object.freeze([ "chatgpt-desktop", "codex-cli", @@ -203,6 +210,13 @@ function validateHostBinding(value, found) { const controls = Object.fromEntries( CONTROL_CLASSES.map((name) => [name, normalizeStringList(value.controls[name], `controls.${name}`)]), ); + const classifiedControls = CONTROL_CLASSES.flatMap((name) => controls[name]); + if (new Set(classifiedControls).size !== classifiedControls.length) { + throw new Error("Invalid host binding: a control cannot appear in more than one control class."); + } + if (execution.owner === "audit-only" && controls.preventive.length > 0) { + throw new Error("Invalid host binding: audit-only execution cannot claim preventive enforcement."); + } return { schemaVersion: HOST_BINDING_SCHEMA_VERSION, @@ -255,6 +269,91 @@ function bindingPath(found) { return path.join(found.runRoot, "integration", "host-binding.json"); } +function bindingOwnershipPath(found) { + return path.join(found.runRoot, "integration", "ownership.json"); +} + +function controlReceiptPath(found) { + return path.join(found.runRoot, "integration", "control-receipt.json"); +} + +function buildIntegrationControlReceipt(binding) { + const classifications = { + preventive: "preventive", + postExecution: "post-execution", + imported: "imported", + unavailable: "unavailable", + }; + const effects = { + preventive: "prevented-before-execution", + postExecution: "checked-after-execution", + imported: "observed-not-enforced", + unavailable: "unavailable", + }; + const controls = CONTROL_CLASSES.flatMap((classification) => ( + binding.controls[classification].map((name) => ({ + name, + classification: classifications[classification], + effect: effects[classification], + })) + )); + return { + schemaVersion: INTEGRATION_CONTROL_RECEIPT_SCHEMA_VERSION, + generatedAt: binding.provenance.recordedAt, + workflow: binding.workflow, + execution: binding.execution, + provenance: { + kind: binding.provenance.kind, + authenticationBoundary: binding.provenance.authenticationBoundary, + }, + controls, + summary: { + preventiveEnforced: binding.controls.preventive.length, + postExecutionChecked: binding.controls.postExecution.length, + importedObserved: binding.controls.imported.length, + unavailable: binding.controls.unavailable.length, + }, + claims: { + observedEvidenceIsPreventiveEnforcement: false, + providerExecutionSuppliesEnforcement: false, + preventiveControlAuthority: binding.controls.preventive.length > 0 ? "cewp-core" : "none", + guardrailAuthority: "cewp-core-outside-provider-execution", + }, + warnings: binding.execution.owner === "audit-only" + ? ["Audit-only evidence can be imported or checked after execution; it is not preventive enforcement."] + : [], + }; +} + +function claimBindingWorktree(found, binding) { + if (!binding.references.worktree) return null; + if (!binding.workflow.taskId || !binding.workflow.checkpointId) { + throw new Error("Host binding worktree ownership requires a task and active checkpoint identity."); + } + const filePath = bindingOwnershipPath(found); + const requested = validateOwnershipRecord({ + schemaVersion: OWNERSHIP_SCHEMA_VERSION, + runId: binding.workflow.runId, + taskId: binding.workflow.taskId, + checkpointId: binding.workflow.checkpointId, + owner: binding.execution.owner, + backend: binding.execution.backend, + status: "active", + createdAt: binding.provenance.recordedAt, + cleanupAuthority: binding.execution.owner === "managed" ? "cewp-core" : "host-owner", + worktree: binding.references.worktree, + }); + const existing = loadOwnershipRecords(found.repoRoot, { excludePath: filePath }); + const conflict = findOwnershipConflict(existing, requested, { repoRoot: found.repoRoot }); + if (conflict) { + throw new Error( + `Host binding execution ownership conflict with ${conflict.owner} run ${conflict.runId} task ${conflict.taskId}.`, + ); + } + writeJsonAtomic(filePath, requested); + return requested; +} + function createHostBinding(found, candidate, options = {}) { const binding = validateHostBinding(candidate, found); if (!options.capabilities) throw new Error("Host binding requires a versioned capability snapshot."); @@ -264,7 +363,9 @@ function createHostBinding(found, candidate, options = {}) { throw new Error(`Host binding already exists for workflow run ${found.run.runId}.`); } fs.mkdirSync(path.dirname(filePath), { recursive: true }); + claimBindingWorktree(found, binding); writeJsonAtomic(filePath, binding); + writeJsonAtomic(controlReceiptPath(found), buildIntegrationControlReceipt(binding)); return binding; } @@ -274,6 +375,20 @@ function loadHostBinding(found) { return validateHostBinding(JSON.parse(fs.readFileSync(filePath, "utf8")), found); } +function loadIntegrationControlReceipt(found) { + const filePath = controlReceiptPath(found); + if (!fs.existsSync(filePath)) return null; + const receipt = JSON.parse(fs.readFileSync(filePath, "utf8")); + if (receipt.schemaVersion !== INTEGRATION_CONTROL_RECEIPT_SCHEMA_VERSION) { + throw new Error(`Invalid integration control receipt: expected ${INTEGRATION_CONTROL_RECEIPT_SCHEMA_VERSION}.`); + } + const binding = loadHostBinding(found); + if (!binding || JSON.stringify(receipt) !== JSON.stringify(buildIntegrationControlReceipt(binding))) { + throw new Error("Invalid integration control receipt: receipt does not match the validated host binding."); + } + return receipt; +} + function createGeneratedGoalBrief(found, taskId) { if (found.run.execution.owner !== "native") { throw new Error("Generated native goal briefs require native execution ownership."); @@ -314,8 +429,11 @@ module.exports = { GENERATED_GOAL_BRIEF_SCHEMA_VERSION, HOST_BINDING_SCHEMA_VERSION, HOST_SURFACES, + INTEGRATION_CONTROL_RECEIPT_SCHEMA_VERSION, + buildIntegrationControlReceipt, createGeneratedGoalBrief, createHostBinding, + loadIntegrationControlReceipt, loadHostBinding, validateHostBinding, }; diff --git a/src/integration/cli.js b/src/integration/cli.js new file mode 100644 index 0000000..c209b77 --- /dev/null +++ b/src/integration/cli.js @@ -0,0 +1,85 @@ +"use strict"; + +const fs = require("node:fs"); +const { + approveCodexHookTrust, + inspectCodexHookTrust, + recordSubagentHookEvent, +} = require("./hook-evidence"); +const { loadIntegrationControlReceipt } = require("./binding"); +const { loadWorkflowRun } = require("../workflow/state"); + +function outputJson(command, data) { + console.log(JSON.stringify({ + schemaVersion: "operator-json/v1", + command, + generatedAt: new Date().toISOString(), + data, + warnings: [], + }, null, 2)); +} + +function runIntegration(options = {}) { + if (options.subcommand === "controls") { + if (!options.workflowRunId) throw new Error("integration controls requires a workflow run id."); + const found = loadWorkflowRun(process.cwd(), options.workflowRunId); + const result = loadIntegrationControlReceipt(found); + if (!result) throw new Error(`Integration control receipt not found for workflow run ${options.workflowRunId}.`); + if (options.json) outputJson("integration.controls", result); + else { + console.log("CEWP integration control receipt"); + console.log(`Run ID: ${result.workflow.runId}`); + console.log(`Owner: ${result.execution.owner}`); + console.log(`Preventive: ${result.summary.preventiveEnforced}`); + console.log(`Post-execution: ${result.summary.postExecutionChecked}`); + console.log(`Imported observations: ${result.summary.importedObserved}`); + console.log(`Unavailable: ${result.summary.unavailable}`); + } + return; + } + if (options.subcommand === "hooks" && options.action === "ingest") { + const raw = fs.readFileSync(0, "utf8"); + if (Buffer.byteLength(raw, "utf8") > 64 * 1024) { + throw new Error("Codex hook input exceeds 65536 bytes."); + } + const input = JSON.parse(raw || "{}"); + recordSubagentHookEvent({ input }); + console.log("{}"); + return; + } + + if (options.subcommand === "hooks" && options.action === "approve") { + if (!options.workflowRunId) throw new Error("integration hooks approve requires a workflow run id."); + const result = approveCodexHookTrust({ + repoRoot: process.cwd(), + runId: options.workflowRunId, + yes: options.yes, + }); + if (options.json) outputJson("integration.hooks.approve", result); + else { + console.log("CEWP Codex hook evidence approved"); + console.log(`Run ID: ${result.trust.runId}`); + console.log(`Bundle: ${result.trust.bundleDigest}`); + console.log("Next: open /hooks and review the current definition"); + } + return; + } + if (options.subcommand === "hooks" && options.action === "status") { + if (!options.workflowRunId) throw new Error("integration hooks status requires a workflow run id."); + const result = inspectCodexHookTrust({ + repoRoot: process.cwd(), + runId: options.workflowRunId, + }); + if (options.json) outputJson("integration.hooks.status", result); + else { + console.log("CEWP Codex hook evidence status"); + console.log(`Run ID: ${result.runId}`); + console.log(`Active: ${result.active ? "yes" : "no"}`); + result.warnings.forEach((warning) => console.log(`Warning: ${warning.code}: ${warning.message}`)); + } + return; + } + throw new Error(`Unsupported integration command: ${options.subcommand || "missing"}.`); +} + +module.exports = { runIntegration }; diff --git a/src/integration/effort-policy.js b/src/integration/effort-policy.js new file mode 100644 index 0000000..27b98d4 --- /dev/null +++ b/src/integration/effort-policy.js @@ -0,0 +1,265 @@ +"use strict"; + +const fs = require("node:fs"); +const crypto = require("node:crypto"); +const path = require("node:path"); +const { appendEvent, findSupervisedRun, getNextAction } = require("../supervise/state"); +const { writeJsonAtomic } = require("../workflow/state"); + +const CODEX_EFFORT_POLICY_SCHEMA_VERSION = "codex-effort-policy/v1"; +const CODEX_TASK_CLASSES = Object.freeze([ + "fast-exploration", + "demanding-implementation", + "high-effort-independent-review", +]); +const CODEX_EFFORT_OPERATIONS = Object.freeze(["implementation", "repair", "reviewer"]); +const CODEX_REASONING_EFFORTS = Object.freeze(["minimal", "low", "medium", "high", "xhigh"]); + +function requiredChoice(value, allowed, label) { + if (!allowed.includes(value)) { + throw new Error(`${label} must be one of: ${allowed.join(", ")}.`); + } + return value; +} + +function optionalSelection(value, label) { + if (value === undefined || value === null) return { status: "unknown", value: null }; + if (typeof value !== "string" || value.trim().length === 0 || value.length > 128 || /[\u0000-\u001f]/.test(value)) { + throw new Error(`${label} must be bounded non-empty text.`); + } + return { status: "explicit", value: value.trim() }; +} + +function effortPolicyPath(found) { + return path.join(found.runRoot, "integration", "codex-effort-policy.json"); +} + +function selectionDigest(operation, assignment) { + const content = JSON.stringify({ + operation, + workflow: assignment.workflow, + taskClass: assignment.taskClass, + model: assignment.requested.model, + effort: assignment.requested.effort, + }); + return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`; +} + +function assignmentSnapshot(assignment) { + if (!assignment) return null; + return { + workflow: assignment.workflow, + taskClass: assignment.taskClass, + requested: assignment.requested, + approval: assignment.approval, + }; +} + +function validateAssignment(operation, assignment, found, options = {}) { + if (!assignment || typeof assignment !== "object") { + throw new Error(`Invalid Codex effort assignment for ${operation}.`); + } + requiredChoice(assignment.taskClass, CODEX_TASK_CLASSES, `${operation}.taskClass`); + if ( + !assignment.workflow + || !Number.isInteger(assignment.workflow.planRevision) + || assignment.workflow.planRevision <= 0 + || typeof assignment.workflow.checkpointId !== "string" + || assignment.workflow.checkpointId.length === 0 + ) { + throw new Error(`Invalid Codex effort workflow binding for ${operation}.`); + } + const model = optionalSelection( + assignment.requested && assignment.requested.model && assignment.requested.model.value, + `${operation}.requested.model`, + ); + const effortValue = assignment.requested && assignment.requested.effort && assignment.requested.effort.value; + if (effortValue !== null && effortValue !== undefined) { + requiredChoice(effortValue, CODEX_REASONING_EFFORTS, `${operation}.requested.effort`); + } + const effort = optionalSelection(effortValue, `${operation}.requested.effort`); + const normalized = { + ...assignment, + requested: { model, effort }, + }; + if ( + !assignment.approval + || assignment.approval.kind !== "operator" + || assignment.approval.selectionDigest !== selectionDigest(operation, normalized) + ) { + throw new Error(`Codex effort assignment for ${operation} is not operator-approved or was modified.`); + } + if ( + options.allowStale !== true + && ( + assignment.workflow.planRevision !== found.run.planRevision + || assignment.workflow.checkpointId !== found.run.tasks[0].id + ) + ) { + throw new Error(`Codex effort assignment for ${operation} was not approved for the current plan revision and checkpoint.`); + } + return normalized; +} + +function loadCodexEffortPolicy(found, options = {}) { + const filePath = effortPolicyPath(found); + if (!fs.existsSync(filePath)) return null; + const policy = JSON.parse(fs.readFileSync(filePath, "utf8")); + if (policy.schemaVersion !== CODEX_EFFORT_POLICY_SCHEMA_VERSION || policy.runId !== found.runId) { + throw new Error(`Invalid Codex effort policy for run ${found.runId}.`); + } + if (policy.provider !== "codex" || policy.automaticModelRouting !== false) { + throw new Error("Invalid Codex effort policy provider or routing boundary."); + } + return { + ...policy, + assignments: Object.fromEntries(Object.entries(policy.assignments || {}).map(([operation, assignment]) => { + requiredChoice(operation, CODEX_EFFORT_OPERATIONS, "effort policy operation"); + return [operation, validateAssignment(operation, assignment, found, options)]; + })), + }; +} + +function approveCodexEffortPolicy(options = {}) { + if (!options.yes) { + throw new Error("Codex effort policy changes require explicit operator approval with --yes."); + } + const found = findSupervisedRun(options); + const operation = requiredChoice(options.operation, CODEX_EFFORT_OPERATIONS, "--operation"); + const taskClass = requiredChoice(options.taskClass, CODEX_TASK_CLASSES, "--task-class"); + if (options.effort !== undefined) { + requiredChoice(options.effort, CODEX_REASONING_EFFORTS, "--effort"); + } + const previous = loadCodexEffortPolicy(found, { allowStale: true }); + const revision = previous ? previous.revision + 1 : 1; + const approvedAt = new Date().toISOString(); + const assignment = { + workflow: { + planRevision: found.run.planRevision, + checkpointId: found.run.tasks[0].id, + }, + taskClass, + requested: { + model: optionalSelection(options.model, "--model"), + effort: optionalSelection(options.effort, "--effort"), + }, + approval: { + kind: "operator", + event: "codex-effort-policy-approved", + revision, + approvedAt, + selectionDigest: null, + }, + }; + assignment.approval.selectionDigest = selectionDigest(operation, assignment); + const previousAssignment = previous ? previous.assignments[operation] || null : null; + const policy = { + schemaVersion: CODEX_EFFORT_POLICY_SCHEMA_VERSION, + runId: found.runId, + provider: "codex", + automaticModelRouting: false, + revision, + updatedAt: approvedAt, + assignments: { + ...(previous ? previous.assignments : {}), + [operation]: assignment, + }, + history: [ + ...(previous ? previous.history : []), + { + revision, + operation, + taskClass, + approvedAt, + previousRevision: previous ? previous.revision : null, + previous: assignmentSnapshot(previousAssignment), + next: assignmentSnapshot(assignment), + }, + ], + }; + const filePath = effortPolicyPath(found); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + writeJsonAtomic(filePath, policy); + appendEvent(found.runRoot, { + schemaVersion: "supervised-event/v1-beta", + timestamp: approvedAt, + type: "codex-effort-policy-approved", + runId: found.runId, + planRevision: found.run.planRevision, + checkpointId: found.run.tasks[0].id, + actor: "operator", + operation, + revision, + selectionDigest: assignment.approval.selectionDigest, + }); + return { + run: found.run, + effortPolicy: policy, + nextAction: getNextAction(found.run), + }; +} + +function resolveCodexEffortForDispatch(found, operation) { + requiredChoice(operation, CODEX_EFFORT_OPERATIONS, "Codex dispatch operation"); + const policy = loadCodexEffortPolicy(found); + const assignment = policy && policy.assignments[operation]; + if (!assignment) { + return { + model: undefined, + effort: undefined, + evidence: { + taskClass: null, + policyRevision: policy ? policy.revision : null, + selectedModel: { status: "unknown", value: null }, + selectedEffort: { status: "unknown", value: null }, + effectiveModel: { status: "unknown", value: null, source: "not-explicitly-selected" }, + effectiveEffort: { status: "unknown", value: null, source: "not-explicitly-selected" }, + }, + }; + } + const model = assignment.requested.model.value || undefined; + const effort = assignment.requested.effort.value || undefined; + return { + model, + effort, + evidence: { + taskClass: assignment.taskClass, + policyRevision: policy.revision, + selectedModel: assignment.requested.model, + selectedEffort: assignment.requested.effort, + effectiveModel: { status: "unknown", value: null, source: model ? "awaiting-supported-turn-evidence" : "not-explicitly-selected" }, + effectiveEffort: { status: "unknown", value: null, source: effort ? "awaiting-supported-turn-evidence" : "not-explicitly-selected" }, + }, + }; +} + +function confirmCodexEffortEvidence(evidence, usage) { + const confirmed = usage && usage.label === "observed"; + const effective = (selected) => ( + confirmed && selected && selected.status === "explicit" + ? { status: "known", value: selected.value, source: "codex-exec-turn-completed-usage" } + : { + status: "unknown", + value: null, + source: selected && selected.status === "explicit" + ? "supported-turn-evidence-unavailable" + : "not-explicitly-selected", + } + ); + return { + ...evidence, + effectiveModel: effective(evidence.selectedModel), + effectiveEffort: effective(evidence.selectedEffort), + }; +} + +module.exports = { + CODEX_EFFORT_OPERATIONS, + CODEX_EFFORT_POLICY_SCHEMA_VERSION, + CODEX_REASONING_EFFORTS, + CODEX_TASK_CLASSES, + approveCodexEffortPolicy, + confirmCodexEffortEvidence, + loadCodexEffortPolicy, + resolveCodexEffortForDispatch, +}; diff --git a/src/integration/hook-evidence.js b/src/integration/hook-evidence.js new file mode 100644 index 0000000..c20dd76 --- /dev/null +++ b/src/integration/hook-evidence.js @@ -0,0 +1,402 @@ +"use strict"; + +const childProcess = require("node:child_process"); +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const path = require("node:path"); +const { loadWorkflowRun } = require("../workflow/state"); +const { writeJsonAtomic } = require("../workflow/state"); + +const CODEX_HOOK_TRUST_SCHEMA_VERSION = "codex-hook-trust/v1"; +const CODEX_HOOK_CONTRACT_VERSION = "codex-hooks-doc/2026-07-22"; +const HOOK_BUNDLE_FILES = Object.freeze([ + "hooks/hooks.json", + "hooks/capture-subagent.js", +]); +const SUBAGENT_HOOK_EVIDENCE_SCHEMA_VERSION = "subagent-hook-evidence/v1"; +const SUBAGENT_HOOK_EVENTS = Object.freeze({ + SubagentStart: "subagent-started", + SubagentStop: "subagent-stopped", +}); + +function sha256(value) { + return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`; +} + +function pluginRoot() { + if (process.env.PLUGIN_ROOT) return path.resolve(process.env.PLUGIN_ROOT); + return path.resolve(__dirname, "..", "..", "plugins", "cewp"); +} + +function inspectHookBundle(root = pluginRoot()) { + const files = HOOK_BUNDLE_FILES.map((relativePath) => { + const filePath = path.join(root, ...relativePath.split("/")); + if (!fs.existsSync(filePath)) throw new Error(`Codex hook bundle file is missing: ${relativePath}.`); + const content = fs.readFileSync(filePath); + return { path: relativePath, digest: sha256(content), content }; + }); + const bundle = Buffer.concat(files.flatMap((file) => [ + Buffer.from(`${file.path}\0`, "utf8"), + file.content, + Buffer.from("\0", "utf8"), + ])); + return { + digest: sha256(bundle), + files: files.map(({ path: relativePath, digest }) => ({ path: relativePath, digest })), + }; +} + +function detectCodexVersion(options = {}) { + if (process.env.CEWP_HOOK_CODEX_VERSION) return process.env.CEWP_HOOK_CODEX_VERSION.trim(); + const command = options.command || "codex"; + const result = childProcess.spawnSync(command, ["--version"], { + encoding: "utf8", + windowsHide: true, + timeout: 10000, + }); + if (result.status !== 0 || !String(result.stdout || "").trim()) { + throw new Error("Codex hook approval requires a successful local `codex --version` probe."); + } + return String(result.stdout).trim(); +} + +function detectCewpVersion() { + const packagePath = path.resolve(__dirname, "..", "..", "package.json"); + if (!fs.existsSync(packagePath)) { + throw new Error("Codex hook evidence requires the complete CEWP package, including package.json."); + } + const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); + if (typeof packageJson.version !== "string" || packageJson.version.trim().length === 0) { + throw new Error("Codex hook evidence could not determine the CEWP runtime version."); + } + return packageJson.version.trim(); +} + +function trustPath(found) { + return path.join(found.runRoot, "integration", "codex-hook-trust.json"); +} + +function activationPath(repoRoot) { + return path.join(repoRoot, ".cewp", "integration", "active-hook-trust.json"); +} + +function boundedText(value, label, maximum, optional = false) { + if (optional && (value === null || value === undefined)) return null; + if (typeof value !== "string" || value.trim().length === 0) { + throw new Error(`Invalid Codex subagent hook event: ${label} is required.`); + } + const text = value.trim(); + if (text.length > maximum || /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(text)) { + throw new Error(`Invalid Codex subagent hook event: ${label} is too long or contains control characters.`); + } + return text; +} + +function findActivatedRepoRoot(startPath) { + let current = path.resolve(startPath); + while (true) { + if (fs.existsSync(activationPath(current))) return current; + const parent = path.dirname(current); + if (parent === current) return null; + current = parent; + } +} + +function validateActiveTrust(repoRoot, currentCodexVersion, root = pluginRoot(), currentCewpVersion) { + const activePath = activationPath(repoRoot); + if (!fs.existsSync(activePath)) return null; + const active = JSON.parse(fs.readFileSync(activePath, "utf8")); + if (active.schemaVersion !== "codex-hook-activation/v1") { + throw new Error("Codex hook evidence activation is malformed; approve the hook bundle again."); + } + const found = loadWorkflowRun(repoRoot, active.runId); + const filePath = trustPath(found); + if (!fs.existsSync(filePath)) throw new Error("Codex hook trust receipt is missing; approve the hook bundle again."); + const trust = JSON.parse(fs.readFileSync(filePath, "utf8")); + if (trust.schemaVersion !== CODEX_HOOK_TRUST_SCHEMA_VERSION || trust.provider !== "codex") { + throw new Error("Codex hook trust receipt is malformed; approve the hook bundle again."); + } + const selection = { + runId: trust.runId, + workflowRevision: trust.workflowRevision, + workflowDigest: trust.workflowDigest, + cewpVersion: trust.cewpVersion, + codexVersion: trust.codexVersion, + hookContractVersion: trust.hookContractVersion, + bundleDigest: trust.bundleDigest, + }; + const approvalDigest = sha256(JSON.stringify(selection)); + if ( + !trust.approval + || trust.approval.kind !== "operator" + || trust.approval.digest !== approvalDigest + || active.trustDigest !== approvalDigest + ) { + throw new Error("Codex hook trust approval digest changed; review and approve the hook bundle again."); + } + const currentBundle = inspectHookBundle(root); + if (trust.bundleDigest !== currentBundle.digest) { + throw new Error("Codex hook definition drift detected; review and approve the current bundle again."); + } + if (trust.codexVersion !== currentCodexVersion) { + throw new Error(`Codex hook version drift detected: approved ${trust.codexVersion}, observed ${currentCodexVersion}.`); + } + const observedCewpVersion = currentCewpVersion || detectCewpVersion(); + if (trust.cewpVersion !== observedCewpVersion) { + throw new Error(`CEWP hook runtime drift detected: approved ${trust.cewpVersion}, observed ${observedCewpVersion}.`); + } + if (trust.hookContractVersion !== CODEX_HOOK_CONTRACT_VERSION) { + throw new Error("Codex hook contract drift detected; review and approve the current contract again."); + } + if ( + trust.runId !== found.run.runId + || trust.workflowRevision !== found.run.workflow.revision + || trust.workflowDigest !== found.run.workflow.digest + ) { + throw new Error("Codex hook trust is stale for the current workflow revision."); + } + return { active, found, trust }; +} + +function recordSubagentHookEvent(options = {}) { + const input = options.input; + if (!input || typeof input !== "object" || Array.isArray(input)) { + throw new Error("Invalid Codex subagent hook event: expected one JSON object."); + } + const eventCwd = boundedText(input.cwd, "cwd", 4096); + const repoRoot = findActivatedRepoRoot(options.repoRoot || eventCwd); + if (!repoRoot) return { recorded: false, reason: "not-activated" }; + const resolvedCwd = path.resolve(eventCwd); + const relativeCwd = path.relative(repoRoot, resolvedCwd); + if (relativeCwd.startsWith("..") || path.isAbsolute(relativeCwd)) { + throw new Error("Invalid Codex subagent hook event: cwd is outside the activated repository."); + } + const currentCodexVersion = options.codexVersion || detectCodexVersion(options); + const active = validateActiveTrust(repoRoot, currentCodexVersion, options.pluginRoot, options.cewpVersion); + if (!active) return { recorded: false, reason: "not-activated" }; + const hookEventName = boundedText(input.hook_event_name, "hook_event_name", 64); + const type = SUBAGENT_HOOK_EVENTS[hookEventName]; + if (!type) throw new Error(`Invalid Codex subagent hook event: unsupported event ${hookEventName}.`); + const agentId = boundedText(input.agent_id, "agent_id", 512); + const parentSessionId = boundedText(input.session_id, "session_id", 512); + const parentTurnId = boundedText(input.turn_id, "turn_id", 512); + const agentType = boundedText(input.agent_type, "agent_type", 128); + const model = boundedText(input.model, "model", 256); + const permissionMode = boundedText(input.permission_mode, "permission_mode", 64); + const summaryValue = hookEventName === "SubagentStop" + ? boundedText(input.last_assistant_message, "last_assistant_message", 4000, true) + : null; + const observedAt = (options.now || new Date()).toISOString(); + const evidence = { + schemaVersion: SUBAGENT_HOOK_EVIDENCE_SCHEMA_VERSION, + eventId: sha256(JSON.stringify({ + runId: active.found.run.runId, + hookEventName, + parentSessionId, + parentTurnId, + agentId, + observedAt, + })), + type, + observedAt, + workflow: { + runId: active.found.run.runId, + revision: active.found.run.workflow.revision, + digest: active.found.run.workflow.digest, + }, + source: { + path: "plugin-hook", + evidenceClass: "observed", + codexVersion: currentCodexVersion, + cewpVersion: active.trust.cewpVersion, + hookContractVersion: CODEX_HOOK_CONTRACT_VERSION, + bundleDigest: active.trust.bundleDigest, + trustApprovalDigest: active.trust.approval.digest, + }, + references: { + agentId, + agentType, + parentSessionId, + parentTurnId, + agentThreadId: { status: "unknown", value: null, reason: "not-exposed-by-documented-hook-input" }, + }, + context: { + model, + permissionMode, + workingDirectory: path.relative(repoRoot, resolvedCwd).replace(/\\/g, "/") || ".", + }, + summary: summaryValue + ? { status: "observed", value: summaryValue } + : { status: "unknown", value: null }, + claims: { + coreEnforcement: false, + opensCoreGates: false, + transcriptRead: false, + }, + }; + const ledgerPath = path.join(active.found.runRoot, "integration", "subagent-hook-evidence.jsonl"); + fs.mkdirSync(path.dirname(ledgerPath), { recursive: true }); + fs.appendFileSync(ledgerPath, `${JSON.stringify(evidence)}\n`); + return { recorded: true, evidence }; +} + +function approveCodexHookTrust(options = {}) { + if (!options.yes) { + throw new Error("Codex hook evidence requires explicit operator approval with --yes after reviewing the bundle and `/hooks`."); + } + const found = loadWorkflowRun(options.repoRoot || process.cwd(), options.runId); + const bundle = inspectHookBundle(options.pluginRoot); + const codexVersion = options.codexVersion || detectCodexVersion(options); + const approvedAt = (options.now || new Date()).toISOString(); + const selection = { + runId: found.run.runId, + workflowRevision: found.run.workflow.revision, + workflowDigest: found.run.workflow.digest, + cewpVersion: options.cewpVersion || detectCewpVersion(), + codexVersion, + hookContractVersion: CODEX_HOOK_CONTRACT_VERSION, + bundleDigest: bundle.digest, + }; + const trust = { + schemaVersion: CODEX_HOOK_TRUST_SCHEMA_VERSION, + provider: "codex", + ...selection, + bundleFiles: bundle.files, + approvedAt, + approval: { + kind: "operator", + digest: sha256(JSON.stringify(selection)), + }, + hostTrust: { + required: true, + status: "pending-review", + reviewCommand: "/hooks", + }, + provenance: { + source: "official-documentation", + url: "https://learn.chatgpt.com/docs/hooks", + }, + claims: { + coreEnforcement: false, + opensCoreGates: false, + transcriptRead: false, + }, + }; + const filePath = trustPath(found); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + writeJsonAtomic(filePath, trust); + const activePath = activationPath(found.repoRoot); + fs.mkdirSync(path.dirname(activePath), { recursive: true }); + writeJsonAtomic(activePath, { + schemaVersion: "codex-hook-activation/v1", + runId: found.run.runId, + trustDigest: trust.approval.digest, + activatedAt: approvedAt, + }); + fs.appendFileSync(path.join(found.runRoot, "events.jsonl"), `${JSON.stringify({ + schemaVersion: "workflow-event/v1", + timestamp: approvedAt, + type: "hook-evidence-approved", + runId: found.run.runId, + revision: found.run.workflow.revision, + actor: "operator", + approvalDigest: trust.approval.digest, + })}\n`); + return { + trust, + nextAction: { + kind: "host-hook-review", + command: "/hooks", + reason: "Codex separately reviews and trusts the exact current plugin hook definition.", + }, + }; +} + +function classifyCompatibilityWarning(error) { + const message = error && error.message ? error.message : String(error); + const code = /Codex hook version drift/.test(message) + ? "codex-version-drift" + : /definition drift/.test(message) + ? "hook-definition-drift" + : /runtime drift/.test(message) + ? "cewp-version-drift" + : /contract drift/.test(message) + ? "hook-contract-drift" + : /workflow revision|not active/.test(message) + ? "workflow-binding-drift" + : /approval digest|trust receipt/.test(message) + ? "hook-trust-change" + : "hook-evidence-unavailable"; + return { code, message }; +} + +function inspectCodexHookTrust(options = {}) { + const repoRoot = path.resolve(options.repoRoot || process.cwd()); + const found = loadWorkflowRun(repoRoot, options.runId); + const filePath = trustPath(found); + const claims = { + coreEnforcement: false, + opensCoreGates: false, + hostTrustInferred: false, + }; + if (!fs.existsSync(filePath)) { + return { + schemaVersion: "codex-hook-status/v1", + runId: found.run.runId, + compatible: false, + active: false, + trust: null, + warnings: [{ code: "hook-evidence-not-approved", message: "No approved hook evidence bundle exists for this run." }], + fallback: "core-and-conversation-only", + claims, + }; + } + let trust = null; + try { + try { + trust = JSON.parse(fs.readFileSync(filePath, "utf8")); + } catch (error) { + throw new Error(`Codex hook trust receipt is malformed: ${error.message}`); + } + const currentCodexVersion = options.codexVersion || detectCodexVersion(options); + const active = validateActiveTrust(repoRoot, currentCodexVersion, options.pluginRoot, options.cewpVersion); + if (!active || active.found.run.runId !== found.run.runId) { + throw new Error("Codex hook trust is not active for the requested workflow run."); + } + return { + schemaVersion: "codex-hook-status/v1", + runId: found.run.runId, + compatible: true, + active: true, + trust, + warnings: [], + fallback: null, + claims, + }; + } catch (error) { + return { + schemaVersion: "codex-hook-status/v1", + runId: found.run.runId, + compatible: false, + active: false, + trust, + warnings: [classifyCompatibilityWarning(error)], + fallback: "core-and-conversation-only", + claims, + }; + } +} + +module.exports = { + CODEX_HOOK_CONTRACT_VERSION, + CODEX_HOOK_TRUST_SCHEMA_VERSION, + SUBAGENT_HOOK_EVIDENCE_SCHEMA_VERSION, + approveCodexHookTrust, + detectCewpVersion, + detectCodexVersion, + findActivatedRepoRoot, + inspectCodexHookTrust, + inspectHookBundle, + recordSubagentHookEvent, + validateActiveTrust, +}; diff --git a/src/mcp/server.js b/src/mcp/server.js new file mode 100644 index 0000000..aeb9f2a --- /dev/null +++ b/src/mcp/server.js @@ -0,0 +1,115 @@ +"use strict"; + +const readline = require("node:readline"); +const { TOOLS, callTool } = require("./tools"); + +const PROTOCOL_VERSIONS = new Set(["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"]); + +function packageVersion() { + try { + return require("../../package.json").version; + } catch { + return "unknown"; + } +} + +function response(id, result) { + return { jsonrpc: "2.0", id, result }; +} + +function rpcError(id, code, message) { + return { jsonrpc: "2.0", id, error: { code, message } }; +} + +function toolResult(value) { + return { + content: [{ type: "text", text: JSON.stringify(value) }], + structuredContent: value, + isError: false, + }; +} + +function toolError(error) { + return { + content: [{ type: "text", text: error && error.message ? error.message : String(error) }], + isError: true, + }; +} + +function createMcpSession(options = {}) { + const repoRoot = options.repoRoot || process.cwd(); + let initialized = false; + + function handle(message) { + if (!message || message.jsonrpc !== "2.0" || typeof message.method !== "string") { + return rpcError(message && message.id !== undefined ? message.id : null, -32600, "Invalid JSON-RPC request."); + } + if (message.method === "notifications/initialized" || message.method.startsWith("notifications/")) { + return null; + } + if (message.method === "initialize") { + const requested = message.params && message.params.protocolVersion; + const compatible = PROTOCOL_VERSIONS.has(requested); + const protocolVersion = compatible ? requested : "2025-11-25"; + initialized = true; + return response(message.id, { + protocolVersion, + capabilities: { tools: { listChanged: false } }, + serverInfo: { + name: "cewp-local-core", + version: packageVersion(), + description: "Local CEWP Core tools with supervised approval and evidence gates.", + }, + instructions: "Inspect before mutating. Explicit confirmation does not bypass CEWP Core state, ownership, scope, policy, budget, verification, or reviewer gates.", + compatibility: compatible + ? { compatible: true, requestedProtocolVersion: requested, fallback: null, warning: null } + : { + compatible: false, + requestedProtocolVersion: requested || null, + fallback: "cewp-cli-operator-json", + warning: { + code: "mcp-protocol-version-drift", + message: `Requested MCP protocol ${requested || "unknown"} is unsupported; negotiated ${protocolVersion}. Disconnect or use the CEWP CLI operator JSON fallback.`, + }, + }, + }); + } + if (!initialized) return rpcError(message.id, -32002, "MCP session is not initialized."); + if (message.method === "ping") return response(message.id, {}); + if (message.method === "tools/list") return response(message.id, { tools: TOOLS }); + if (message.method === "tools/call") { + const params = message.params; + if (!params || typeof params.name !== "string") { + return rpcError(message.id, -32602, "tools/call requires a tool name."); + } + try { + return response(message.id, toolResult(callTool(params.name, params.arguments || {}, { repoRoot }))); + } catch (error) { + if (error && error.code === -32602) return rpcError(message.id, -32602, error.message); + return response(message.id, toolError(error)); + } + } + return rpcError(message.id, -32601, `Method not found: ${message.method}`); + } + + return { handle }; +} + +function runStdio(options = {}) { + const session = createMcpSession(options); + const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); + lines.on("line", (line) => { + if (!line.trim()) return; + let message; + try { + message = JSON.parse(line); + } catch { + process.stdout.write(`${JSON.stringify(rpcError(null, -32700, "Parse error."))}\n`); + return; + } + const result = session.handle(message); + if (result) process.stdout.write(`${JSON.stringify(result)}\n`); + }); +} + +module.exports = { createMcpSession, runStdio }; diff --git a/src/mcp/tools.js b/src/mcp/tools.js new file mode 100644 index 0000000..08e8032 --- /dev/null +++ b/src/mcp/tools.js @@ -0,0 +1,188 @@ +"use strict"; + +const { + approveSupervisedRun, + createProposedRun, + inspectSupervisedRun, +} = require("../supervise/state"); +const { retrySupervisedCheckpoint } = require("../supervise/execution"); +const { verifySupervisedCheckpoint } = require("../supervise/verification"); +const { finalizeSupervisedRun } = require("../supervise/receipt"); +const { runSupervisedControl } = require("../supervise/controls"); + +const stringArray = { type: "array", items: { type: "string" } }; +const runId = { type: "string", minLength: 1, description: "CEWP supervised run ID." }; +const confirm = { + type: "boolean", + description: "Explicit operator confirmation after reviewing the relevant CEWP evidence.", +}; + +function objectSchema(properties, required = []) { + return { type: "object", properties, required, additionalProperties: false }; +} + +const TOOLS = [ + { + name: "cewp_create", + title: "Create CEWP run", + description: "Create a proposed bounded supervised run in the current repository. Does not approve or execute it.", + inputSchema: objectSchema({ + goal: { type: "string", minLength: 1 }, + scopes: stringArray, + verificationCommands: stringArray, + fullVerificationCommands: stringArray, + stoppingConditions: stringArray, + assurance: { type: "string", enum: ["prototype", "standard", "critical"] }, + testAuthoring: { type: "string", enum: ["auto", "ask", "never"] }, + }, ["goal", "scopes", "verificationCommands", "stoppingConditions"]), + annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false }, + }, + { + name: "cewp_inspect", + title: "Inspect CEWP run", + description: "Inspect canonical state and the next allowed action, refreshing the derived progress file.", + inputSchema: objectSchema({ runId }, ["runId"]), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }, + { + name: "cewp_approve", + title: "Approve CEWP run", + description: "Approve the current proposed plan revision after explicit operator confirmation. Does not execute it.", + inputSchema: objectSchema({ runId, confirm, allowTestAuthoring: { type: "boolean" } }, ["runId", "confirm"]), + annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false }, + }, + { + name: "cewp_continue", + title: "Continue CEWP run", + description: "Record operator continuation only after the Core confirms a verified checkpoint.", + inputSchema: objectSchema({ runId }, ["runId"]), + annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false }, + }, + { + name: "cewp_retry", + title: "Retry CEWP checkpoint", + description: "Dispatch one bounded managed repair, subject to ownership, policy, effort, scope, and budget gates.", + inputSchema: objectSchema({ runId, confirm, timeoutSeconds: { type: "integer", minimum: 1 } }, ["runId", "confirm"]), + annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: false }, + }, + { + name: "cewp_revise", + title: "Revise CEWP plan", + description: "Revise an allowed unstarted or completed checkpoint and invalidate prior approval as required by Core.", + inputSchema: objectSchema({ + runId, + goal: { type: "string", minLength: 1 }, + scopes: stringArray, + verificationCommands: stringArray, + fullVerificationCommands: stringArray, + stoppingConditions: stringArray, + }, ["runId"]), + annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false }, + }, + { + name: "cewp_verify", + title: "Verify CEWP checkpoint", + description: "Run the approved verification schedule in the managed worktree under Core policy and budget gates.", + inputSchema: objectSchema({ runId, timeoutSeconds: { type: "integer", minimum: 1 } }, ["runId"]), + annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: false }, + }, + { + name: "cewp_finalize", + title: "Finalize CEWP run", + description: "Finalize only after receipt preview, verified evidence, current worktree gates, and independent reviewer PASS.", + inputSchema: objectSchema({ runId, confirm }, ["runId", "confirm"]), + annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: false }, + }, +]; + +const definitions = new Map(TOOLS.map((tool) => [tool.name, tool])); + +function validateArguments(name, args) { + const definition = definitions.get(name); + if (!definition) { + const error = new Error(`Unknown tool: ${name}`); + error.code = -32602; + throw error; + } + if (!args || typeof args !== "object" || Array.isArray(args)) { + const error = new Error(`Tool ${name} arguments must be an object.`); + error.code = -32602; + throw error; + } + const allowed = new Set(Object.keys(definition.inputSchema.properties)); + const unknown = Object.keys(args).filter((key) => !allowed.has(key)); + if (unknown.length > 0) { + const error = new Error(`Tool ${name} received unknown arguments: ${unknown.join(", ")}.`); + error.code = -32602; + throw error; + } + for (const required of definition.inputSchema.required || []) { + if (!(required in args)) { + const error = new Error(`Tool ${name} requires argument ${required}.`); + error.code = -32602; + throw error; + } + } + for (const [key, value] of Object.entries(args)) { + const schema = definition.inputSchema.properties[key]; + const valid = schema.type === "array" + ? Array.isArray(value) && value.every((entry) => typeof entry === "string") + : schema.type === "string" + ? typeof value === "string" && (!schema.minLength || value.length >= schema.minLength) + : schema.type === "boolean" + ? typeof value === "boolean" + : schema.type === "integer" + ? Number.isInteger(value) && (!schema.minimum || value >= schema.minimum) + : true; + if (!valid || (schema.enum && !schema.enum.includes(value))) { + const error = new Error(`Tool ${name} argument ${key} does not match its input schema.`); + error.code = -32602; + throw error; + } + } +} + +function requireConfirmation(name, args) { + if (args.confirm !== true) { + throw new Error(`${name} requires explicit confirmation after reviewing CEWP state and evidence.`); + } +} + +function coreOptions(repoRoot, args) { + return { + ...args, + repoRoot, + scopes: Array.isArray(args.scopes) ? args.scopes : [], + verificationCommands: Array.isArray(args.verificationCommands) ? args.verificationCommands : [], + fullVerificationCommands: Array.isArray(args.fullVerificationCommands) ? args.fullVerificationCommands : [], + stoppingConditions: Array.isArray(args.stoppingConditions) ? args.stoppingConditions : [], + }; +} + +function callTool(name, args, options = {}) { + validateArguments(name, args); + const repoRoot = options.repoRoot || process.cwd(); + const core = coreOptions(repoRoot, args); + if (name === "cewp_create") return createProposedRun(core); + if (name === "cewp_inspect") return inspectSupervisedRun(core); + if (name === "cewp_approve") { + requireConfirmation("approve", args); + return approveSupervisedRun({ ...core, yes: true }); + } + if (name === "cewp_continue") { + return runSupervisedControl({ ...core, subcommand: "continue" }); + } + if (name === "cewp_retry") { + requireConfirmation("retry", args); + return retrySupervisedCheckpoint({ ...core, yes: true }); + } + if (name === "cewp_revise") return runSupervisedControl({ ...core, subcommand: "revise" }); + if (name === "cewp_verify") return verifySupervisedCheckpoint(core); + if (name === "cewp_finalize") { + requireConfirmation("finalize", args); + return finalizeSupervisedRun({ ...core, yes: true }); + } + throw new Error(`Unknown tool: ${name}`); +} + +module.exports = { TOOLS, callTool }; diff --git a/src/run/adapters/codex-exec.js b/src/run/adapters/codex-exec.js index 2e91d81..b5d3ec8 100644 --- a/src/run/adapters/codex-exec.js +++ b/src/run/adapters/codex-exec.js @@ -198,14 +198,20 @@ function buildCodexExecInvocation({ outputLastMessagePath, sandbox = "workspace-write", structuredJson = false, + model, + effort, }) { const formatArgs = structuredJson ? ["--json"] : []; + const modelArgs = model ? ["--model", model] : []; + const effortArgs = effort ? ["--config", `model_reasoning_effort="${effort}"`] : []; return { command: command || "codex", args: [ ...prefixArgs, "exec", ...formatArgs, + ...modelArgs, + ...effortArgs, "--cd", worktreePath, "--sandbox", @@ -218,7 +224,7 @@ function buildCodexExecInvocation({ }; } -function runCodexExecAdapter({ worktreePath, promptPath, outputLastMessagePath, timeoutSeconds, sandbox = "workspace-write", structuredJson = false }) { +function runCodexExecAdapter({ worktreePath, promptPath, outputLastMessagePath, timeoutSeconds, sandbox = "workspace-write", structuredJson = false, model, effort }) { validateTimeoutSeconds(timeoutSeconds); const prompt = fs.readFileSync(promptPath, "utf8"); const invocation = buildCodexExecInvocation({ @@ -229,6 +235,8 @@ function runCodexExecAdapter({ worktreePath, promptPath, outputLastMessagePath, outputLastMessagePath, sandbox, structuredJson, + model, + effort, }); return childProcess.spawnSync(invocation.command, invocation.args, { diff --git a/src/run/ownership.js b/src/run/ownership.js index ebf7846..9bd88a2 100644 --- a/src/run/ownership.js +++ b/src/run/ownership.js @@ -1,5 +1,6 @@ "use strict"; +const fs = require("node:fs"); const path = require("node:path"); const { normalizeComparePath } = require("../lib/paths"); @@ -76,11 +77,49 @@ function findOwnershipConflict(records, requested, options = {}) { return undefined; } +function loadOwnershipRecords(repoRoot, options = {}) { + const recordsRoot = path.join(repoRoot, ".cewp"); + if (!fs.existsSync(recordsRoot)) return []; + const excludedPath = options.excludePath + ? normalizeComparePath(path.resolve(options.excludePath)) + : null; + const records = []; + const pending = [recordsRoot]; + + while (pending.length > 0) { + const directory = pending.pop(); + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory() && !entry.isSymbolicLink()) { + pending.push(entryPath); + continue; + } + if ( + !entry.isFile() + || entry.name !== "ownership.json" + || normalizeComparePath(path.resolve(entryPath)) === excludedPath + ) { + continue; + } + let record; + try { + record = JSON.parse(fs.readFileSync(entryPath, "utf8")); + } catch (error) { + throw new Error(`Invalid execution ownership registry entry ${entryPath}: ${error.message}`); + } + records.push(validateOwnershipRecord(record)); + } + } + + return records; +} + module.exports = { EXECUTION_OWNERS, MANAGED_BACKENDS, OWNERSHIP_SCHEMA_VERSION, findOwnershipConflict, + loadOwnershipRecords, normalizeWorktreePath, validateOwnershipRecord, }; diff --git a/src/supervise/cli.js b/src/supervise/cli.js index cea23e3..f8811f4 100644 --- a/src/supervise/cli.js +++ b/src/supervise/cli.js @@ -10,6 +10,7 @@ const { verifySupervisedCheckpoint } = require("./verification"); const { reviewSupervisedCheckpoint } = require("./review"); const { finalizeSupervisedRun, previewSupervisedReceipt } = require("./receipt"); const { runSupervisedControl } = require("./controls"); +const { approveCodexEffortPolicy } = require("../integration/effort-policy"); const CONTROL_COMMANDS = new Set([ "revise", "pause", "resume", "add-budget", "rollback", "cancel", "abandon", "block", "continue", "reassign", @@ -99,6 +100,19 @@ function runSupervise(options = {}) { return; } + if (options.subcommand === "effort") { + const result = approveCodexEffortPolicy({ + ...options, + repoRoot: process.cwd(), + }); + if (options.json) { + outputJson("supervise.effort", result); + } else { + printStatus("CEWP Codex effort policy approved", result); + } + return; + } + if (options.subcommand === "execute") { const result = executeSupervisedCheckpoint({ ...options, diff --git a/src/supervise/execution.js b/src/supervise/execution.js index 3381da7..0320ad1 100644 --- a/src/supervise/execution.js +++ b/src/supervise/execution.js @@ -23,8 +23,13 @@ const { } = require("../run/adapters/codex-exec"); const { OWNERSHIP_SCHEMA_VERSION, + loadOwnershipRecords, validateOwnershipRecord, } = require("../run/ownership"); +const { + confirmCodexEffortEvidence, + resolveCodexEffortForDispatch, +} = require("../integration/effort-policy"); const { appendEvent, findSupervisedRun, @@ -210,27 +215,7 @@ function createOwnedWorktree(found, startedAt) { if (continued) return continued; const paths = getWorktreePaths(found); - if (fs.existsSync(paths.worktreePath)) { - throw new Error(`Managed checkpoint worktree already exists: ${paths.worktreePath}`); - } - const branchProbe = getGitOutput(["show-ref", "--verify", "--quiet", `refs/heads/${paths.branch}`], found.repoRoot); - if (branchProbe.status === 0) { - throw new Error(`Managed checkpoint branch already exists: ${paths.branch}`); - } - - fs.mkdirSync(path.dirname(paths.worktreePath), { recursive: true }); - const created = getGitOutput([ - "worktree", - "add", - paths.worktreePath, - "-b", - paths.branch, - checkpointBaseCommit(found.run, found.run.tasks[0]), - ], found.repoRoot); - if (created.status !== 0) { - throw new Error(`Failed to create managed checkpoint worktree: ${(created.stderr || created.stdout || "").trim()}`); - } - + const ownershipPath = path.join(found.runRoot, "ownership.json"); const ownership = validateOwnershipRecord({ schemaVersion: OWNERSHIP_SCHEMA_VERSION, runId: found.runId, @@ -254,13 +239,34 @@ function createOwnedWorktree(found, startedAt) { app: false, notification: false, }, - ownershipRecords: [], + ownershipRecords: loadOwnershipRecords(found.repoRoot, { excludePath: ownershipPath }), requestedOwnership: ownership, }, { repoRoot: found.repoRoot }); if (!gate.allowed) { throw new Error(`Controlled operation blocked: ${gate.reason}`); } - writeJsonAtomic(path.join(found.runRoot, "ownership.json"), ownership); + if (fs.existsSync(paths.worktreePath)) { + throw new Error(`Managed checkpoint worktree already exists: ${paths.worktreePath}`); + } + const branchProbe = getGitOutput(["show-ref", "--verify", "--quiet", `refs/heads/${paths.branch}`], found.repoRoot); + if (branchProbe.status === 0) { + throw new Error(`Managed checkpoint branch already exists: ${paths.branch}`); + } + + fs.mkdirSync(path.dirname(paths.worktreePath), { recursive: true }); + const created = getGitOutput([ + "worktree", + "add", + paths.worktreePath, + "-b", + paths.branch, + checkpointBaseCommit(found.run, found.run.tasks[0]), + ], found.repoRoot); + if (created.status !== 0) { + throw new Error(`Failed to create managed checkpoint worktree: ${(created.stderr || created.stdout || "").trim()}`); + } + + writeJsonAtomic(ownershipPath, ownership); return { ...paths, ownership }; } @@ -301,6 +307,7 @@ function executeSupervisedCheckpoint(options = {}) { assertPolicyAllows(found.repoRoot, "runWorkers"); assertPolicyAllows(found.repoRoot, "runCommands"); enforceOperationBudget(found, "implementation"); + const codexSelection = resolveCodexEffortForDispatch(found, "implementation"); const startedAt = new Date().toISOString(); const owned = createOwnedWorktree(found, startedAt); @@ -330,6 +337,7 @@ function executeSupervisedCheckpoint(options = {}) { scope: { status: "pending", warnings: [] }, testAuthoring: { policy: found.run.assurance.testAuthoring, status: "pending", violations: [] }, usage: { label: "unknown", value: null }, + codex: codexSelection.evidence, }; let startedRun = { ...found.run, @@ -396,6 +404,8 @@ function executeSupervisedCheckpoint(options = {}) { timeoutSeconds: options.timeoutSeconds, sandbox: "workspace-write", structuredJson: true, + model: codexSelection.model, + effort: codexSelection.effort, }); const remainingOutput = Math.max( 0, @@ -439,6 +449,7 @@ function executeSupervisedCheckpoint(options = {}) { }, testAuthoring, usage, + codex: confirmCodexEffortEvidence(attempt.codex, usage), logs: { stdout: path.relative(found.runRoot, stdoutPath).replace(/\\/g, "/"), stderr: path.relative(found.runRoot, stderrPath).replace(/\\/g, "/"), @@ -549,6 +560,7 @@ function retrySupervisedCheckpoint(options = {}) { } assertPolicyAllows(found.repoRoot, "runWorkers"); enforceOperationBudget(found, "repair"); + const codexSelection = resolveCodexEffortForDispatch(found, "repair"); const repairCount = task.attempts.filter((attempt) => attempt.kind === "repair").length; if (repairCount >= found.run.budget.maxRepairsPerCheckpoint.value) { throw new Error("Checkpoint repair limit is exhausted; explicit budget revision is required."); @@ -592,6 +604,7 @@ function retrySupervisedCheckpoint(options = {}) { scope: { status: "pending", warnings: [] }, testAuthoring: { policy: found.run.assurance.testAuthoring, status: "pending", violations: [] }, usage: { label: "unknown", value: null }, + codex: codexSelection.evidence, }; let startedRun = { ...found.run, @@ -638,6 +651,8 @@ function retrySupervisedCheckpoint(options = {}) { timeoutSeconds: options.timeoutSeconds, sandbox: "workspace-write", structuredJson: true, + model: codexSelection.model, + effort: codexSelection.effort, }); const remainingOutput = Math.max( 0, @@ -679,6 +694,7 @@ function retrySupervisedCheckpoint(options = {}) { }, testAuthoring, usage, + codex: confirmCodexEffortEvidence(attempt.codex, usage), logs: { stdout: path.relative(found.runRoot, stdoutPath).replace(/\\/g, "/"), stderr: path.relative(found.runRoot, stderrPath).replace(/\\/g, "/"), diff --git a/src/supervise/review.js b/src/supervise/review.js index d502c7e..40dbedc 100644 --- a/src/supervise/review.js +++ b/src/supervise/review.js @@ -10,6 +10,10 @@ const { runCodexExecAdapter, } = require("../run/adapters/codex-exec"); const { validateOwnershipRecord } = require("../run/ownership"); +const { + confirmCodexEffortEvidence, + resolveCodexEffortForDispatch, +} = require("../integration/effort-policy"); const { applyThresholdObservation, enforceOperationBudget } = require("./budget"); const { mergeManagedUsage, @@ -80,6 +84,7 @@ function reviewSupervisedCheckpoint(options = {}) { } assertPolicyAllows(found.repoRoot, "runReviewer"); enforceOperationBudget(found, "reviewer"); + const codexSelection = resolveCodexEffortForDispatch(found, "reviewer"); const ownership = validateOwnershipRecord( readJsonFile(path.join(found.runRoot, "ownership.json"), "execution ownership"), ); @@ -114,6 +119,7 @@ function reviewSupervisedCheckpoint(options = {}) { independent: true, status: "executing", startedAt, + codex: codexSelection.evidence, }, }; startedRun.budget.consumed.modelOperations += 1; @@ -147,6 +153,8 @@ function reviewSupervisedCheckpoint(options = {}) { timeoutSeconds: options.timeoutSeconds, sandbox: "read-only", structuredJson: true, + model: codexSelection.model, + effort: codexSelection.effort, }); const remainingOutput = Math.max( 0, @@ -188,6 +196,7 @@ function reviewSupervisedCheckpoint(options = {}) { timedOut, reportPath: path.relative(found.runRoot, reportPath).replace(/\\/g, "/"), usage, + codex: confirmCodexEffortEvidence(startedRun.reviewer.codex, usage), reason: decision ? null : "Reviewer output did not contain a supported Decision line.", diff --git a/tests/contracts/integration-binding.js b/tests/contracts/integration-binding.js index 91da018..5b4d922 100644 --- a/tests/contracts/integration-binding.js +++ b/tests/contracts/integration-binding.js @@ -3,16 +3,19 @@ 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 { cleanupRepo, makeTempRepo, runNode } = require("../harness/lib/temp-repo"); const { supportedSnapshot } = require("./integration-capabilities"); const { validDefinition } = require("./workflow-definition"); const { approveWorkflow } = require("./workflow-scheduler"); const { createGeneratedGoalBrief, createHostBinding, + loadIntegrationControlReceipt, loadHostBinding, } = require("../../src/integration/binding"); -const { loadWorkflowRun } = require("../../src/workflow/state"); +const { loadWorkflowRun, startWorkflowTask } = require("../../src/workflow/state"); + +const cewpCli = path.join(__dirname, "..", "..", "bin", "cewp.js"); function assertThrows(action, expected, label) { let error; @@ -36,6 +39,17 @@ function nativeDefinition() { return definition; } +function auditDefinition() { + const definition = validDefinition(); + definition.workflowId = "audit-integration"; + definition.execution = { + owner: "audit-only", + backend: null, + allowedModes: ["audit-only"], + }; + return definition; +} + function explicitBinding(runId) { return { schemaVersion: "host-binding/v1", @@ -129,6 +143,128 @@ function main() { cleanupRepo(managedRepo); } + const auditRepo = makeTempRepo("cewp-integration-audit-binding-"); + try { + const audit = approveWorkflow(auditRepo, auditDefinition()); + const auditFound = loadWorkflowRun(auditRepo, audit.runId); + const auditBinding = explicitBinding(audit.runId); + auditBinding.execution = { owner: "audit-only", backend: null }; + auditBinding.host.surface = "external-client"; + auditBinding.mode = "audit-import"; + auditBinding.provenance.kind = "imported-audit"; + auditBinding.references.goalId = null; + auditBinding.references.threadId = "external-thread-1"; + auditBinding.controls = { + preventive: ["scope-policy"], + postExecution: ["receipt-schema"], + imported: ["external-scope-observation"], + unavailable: ["provider-tool-prevention"], + }; + assertThrows( + () => createHostBinding(auditFound, auditBinding, { capabilities: supportedSnapshot() }), + /audit-only.*preventive/i, + "audit-only binding cannot claim preventive enforcement", + ); + + auditBinding.controls.preventive = []; + createHostBinding(auditFound, auditBinding, { capabilities: supportedSnapshot() }); + const controlReceipt = loadIntegrationControlReceipt(auditFound); + assert(controlReceipt.schemaVersion === "integration-control-receipt/v1", "control receipt is versioned"); + assert(controlReceipt.execution.owner === "audit-only", "receipt retains audit-only ownership"); + assert(controlReceipt.summary.preventiveEnforced === 0, "audit-only receipt claims no preventive enforcement"); + assert(controlReceipt.summary.postExecutionChecked === 1, "post-execution checks stay distinct"); + assert( + controlReceipt.controls.find((entry) => entry.name === "receipt-schema").classification === "post-execution", + "public receipt uses the documented post-execution classification", + ); + assert(controlReceipt.summary.importedObserved === 1, "imported observations stay distinct"); + assert( + controlReceipt.controls.find((entry) => entry.name === "external-scope-observation").effect === "observed-not-enforced", + "imported audit evidence is labeled observed rather than enforced", + ); + assert( + controlReceipt.claims.providerExecutionSuppliesEnforcement === false, + "audit receipt never treats provider-controlled execution as the enforcement source", + ); + const shown = runNode(cewpCli, ["integration", "controls", audit.runId, "--json"], auditRepo); + assert(shown.status === 0, `control receipt is available through operator JSON: ${shown.stderr}`); + const shownReceipt = JSON.parse(shown.stdout); + assert(shownReceipt.command === "integration.controls", "operator JSON identifies control inspection"); + assert(shownReceipt.data.summary.importedObserved === 1, "operator JSON preserves observed audit evidence"); + + const duplicate = { ...auditBinding, controls: { + ...auditBinding.controls, + imported: ["receipt-schema"], + } }; + assertThrows( + () => createHostBinding(auditFound, duplicate, { capabilities: supportedSnapshot(), replace: true }), + /more than one control class/, + "one control cannot receive conflicting enforcement classifications", + ); + + const receiptPath = path.join(auditFound.runRoot, "integration", "control-receipt.json"); + const tampered = JSON.parse(fs.readFileSync(receiptPath, "utf8")); + tampered.controls.find((entry) => entry.classification === "imported").effect = "prevented-before-execution"; + fs.writeFileSync(receiptPath, `${JSON.stringify(tampered, null, 2)}\n`); + assertThrows( + () => loadIntegrationControlReceipt(auditFound), + /does not match the validated host binding/, + "edited receipt cannot promote observed audit evidence to enforcement", + ); + } finally { + cleanupRepo(auditRepo); + } + + const conflictRepo = makeTempRepo("cewp-integration-ownership-conflict-"); + try { + const native = approveWorkflow(conflictRepo, nativeDefinition()); + let nativeFound = loadWorkflowRun(conflictRepo, native.runId); + const started = startWorkflowTask(nativeFound, "implement-example", { + now: new Date("2026-07-18T12:01:00.000Z"), + }); + nativeFound = loadWorkflowRun(conflictRepo, native.runId); + const sharedWorktree = path.join(conflictRepo, "..", ".cewp-worktrees", "shared-task"); + const managedOwnershipPath = path.join( + conflictRepo, + ".cewp", + "supervised-runs", + "managed-conflict", + "ownership.json", + ); + fs.mkdirSync(path.dirname(managedOwnershipPath), { recursive: true }); + fs.writeFileSync(managedOwnershipPath, `${JSON.stringify({ + schemaVersion: "execution-ownership/v1", + runId: "managed-conflict", + taskId: "implement-example", + checkpointId: "implement-example", + owner: "managed", + backend: "codex-exec", + status: "active", + createdAt: "2026-07-18T12:00:00.000Z", + cleanupAuthority: "cewp-core", + worktree: { id: "shared-task", path: sharedWorktree }, + }, null, 2)}\n`); + + const conflictingBinding = explicitBinding(native.runId); + conflictingBinding.workflow = { + runId: native.runId, + taskId: "implement-example", + checkpointId: started.checkpoint.checkpointId, + }; + conflictingBinding.references.worktree = { id: "shared-task", path: sharedWorktree }; + assertThrows( + () => createHostBinding(nativeFound, conflictingBinding, { capabilities: supportedSnapshot() }), + /execution ownership conflict/, + "native host binding cannot claim an active managed task worktree", + ); + assert( + !fs.existsSync(path.join(nativeFound.runRoot, "integration", "host-binding.json")), + "conflicting native binding is not persisted", + ); + } finally { + cleanupRepo(conflictRepo); + } + const coreRun = JSON.parse(fs.readFileSync(found.runPath, "utf8")); assert(coreRun.host === undefined && coreRun.references === undefined, "provider ids stay outside core schema"); diff --git a/tests/contracts/integration-capabilities.js b/tests/contracts/integration-capabilities.js index 6679d4f..4eaa45c 100644 --- a/tests/contracts/integration-capabilities.js +++ b/tests/contracts/integration-capabilities.js @@ -131,6 +131,23 @@ function main() { assert(capabilityMatrix.includes("plugin install, disable, upgrade, and uninstall"), "plugin lifecycle evidence is current"); assert(!capabilityMatrix.includes("Phase 9 must test"), "capability matrix has no stale Phase 9 promise"); + const externalBoundary = fs.readFileSync( + path.join(repoRoot, "docs", "external-integration-boundary.md"), + "utf8", + ); + for (const required of [ + "operator-json/v1", + "cewp-mcp", + "current working directory", + "must not become the execution owner", + "Codex App Server", + "codex-exec", + "does not attach to the ChatGPT desktop app's existing internal session", + "no custom terminal-session protocol", + ]) { + assert(externalBoundary.includes(required), `external integration boundary documents ${required}`); + } + console.log("[PASS] Codex integration capability drift and backend decision stay truthful"); } diff --git a/tests/contracts/integration-effort-policy.js b/tests/contracts/integration-effort-policy.js new file mode 100644 index 0000000..b07f566 --- /dev/null +++ b/tests/contracts/integration-effort-policy.js @@ -0,0 +1,271 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { assert } = require("../harness/lib/assertions"); +const { createFakeCodexAdapter } = require("../harness/lib/fake-adapter"); +const { cleanupRepo, makeTempRepo, runNode } = require("../harness/lib/temp-repo"); + +const cewpCli = path.join(__dirname, "..", "..", "bin", "cewp.js"); + +function parseJson(result, label) { + assert(result.status === 0, `${label} failed: ${result.stderr}`); + return JSON.parse(result.stdout); +} + +function createApprovedRun(repoRoot) { + const planned = parseJson(runNode(cewpCli, [ + "supervise", "plan", + "--goal", "Inspect one bounded behavior", + "--scope", "README.md", + "--verify", "git diff --check", + "--stop", "The bounded behavior is inspected", + "--json", + ], repoRoot), "supervise plan"); + const runId = planned.data.run.runId; + parseJson(runNode(cewpCli, [ + "supervise", "approve", runId, "--yes", "--json", + ], repoRoot), "supervise approve"); + return runId; +} + +function createApprovedRepairRun(repoRoot) { + const verification = "node -e \"const fs=require('fs'); process.exit(fs.readFileSync('README.md','utf8').includes('Fake Codex')?1:0)\""; + const planned = parseJson(runNode(cewpCli, [ + "supervise", "plan", + "--goal", "Repair one bounded regression", + "--scope", "README.md", + "--verify", verification, + "--stop", "The bounded regression is repaired", + "--json", + ], repoRoot), "repair supervise plan"); + const runId = planned.data.run.runId; + parseJson(runNode(cewpCli, ["supervise", "approve", runId, "--yes", "--json"], repoRoot), "repair supervise approve"); + return runId; +} + +function runRepairEffortContract() { + const repoRoot = makeTempRepo("cewp-repair-effort-policy-"); + const fake = createFakeCodexAdapter(); + try { + const runId = createApprovedRepairRun(repoRoot); + assert(runNode(cewpCli, ["policy", "set", "full-authority"], repoRoot).status === 0, "repair fixture grants dispatch authority"); + parseJson(runNode(cewpCli, [ + "supervise", "execute", runId, "--yes", "--timeout", "20", "--json", + ], repoRoot, { env: fake.env }), "execute repair fixture"); + const failedVerification = runNode(cewpCli, [ + "supervise", "verify", runId, "--timeout", "20", "--json", + ], repoRoot); + assert(failedVerification.status === 1, "repair fixture reaches a failed verification gate"); + const failed = JSON.parse(failedVerification.stdout); + assert(failed.data.run.status === "needs-repair", "failed verification exposes bounded repair"); + + parseJson(runNode(cewpCli, [ + "supervise", "effort", runId, + "--operation", "repair", + "--task-class", "demanding-implementation", + "--model", "gpt-test-repair", + "--effort", "medium", + "--yes", "--json", + ], repoRoot), "approve repair effort policy"); + const retried = parseJson(runNode(cewpCli, [ + "supervise", "retry", runId, "--yes", "--timeout", "20", "--json", + ], repoRoot, { + env: { + ...fake.env, + CEWP_FAKE_CODEX_EXPECT_MODEL: "gpt-test-repair", + CEWP_FAKE_CODEX_EXPECT_EFFORT: "medium", + }, + }), "retry with approved effort policy"); + const repairAttempt = retried.data.run.tasks[0].attempts.at(-1); + assert(repairAttempt.kind === "repair", "repair attempt remains explicitly classified"); + assert(repairAttempt.codex.taskClass === "demanding-implementation", "repair evidence retains its task class"); + assert(repairAttempt.codex.effectiveModel.value === "gpt-test-repair", "repair evidence records the effective model"); + assert(repairAttempt.codex.effectiveEffort.value === "medium", "repair evidence records the effective effort"); + } finally { + fs.rmSync(fake.fakeRoot, { recursive: true, force: true }); + cleanupRepo(repoRoot); + } +} + +function runEffortTamperContract() { + const repoRoot = makeTempRepo("cewp-effort-policy-tamper-"); + const fake = createFakeCodexAdapter(); + try { + const runId = createApprovedRun(repoRoot); + parseJson(runNode(cewpCli, [ + "supervise", "effort", runId, + "--operation", "implementation", + "--task-class", "demanding-implementation", + "--model", "gpt-approved", + "--effort", "high", + "--yes", "--json", + ], repoRoot), "approve tamper fixture policy"); + const policyPath = path.join(repoRoot, ".cewp", "supervised-runs", runId, "integration", "codex-effort-policy.json"); + const policy = JSON.parse(fs.readFileSync(policyPath, "utf8")); + policy.assignments.implementation.requested.model.value = "gpt-unapproved-edit"; + fs.writeFileSync(policyPath, `${JSON.stringify(policy, null, 2)}\n`); + assert(runNode(cewpCli, ["policy", "set", "full-authority"], repoRoot).status === 0, "tamper fixture grants dispatch authority"); + const refused = runNode(cewpCli, [ + "supervise", "execute", runId, "--yes", "--timeout", "20", "--json", + ], repoRoot, { env: fake.env }); + assert(refused.status === 1, "modified effort sidecar cannot dispatch"); + assert(refused.stderr.includes("not operator-approved or was modified"), "tamper refusal explains the approval failure"); + } finally { + fs.rmSync(fake.fakeRoot, { recursive: true, force: true }); + cleanupRepo(repoRoot); + } +} + +function runStalePlanRevisionContract() { + const repoRoot = makeTempRepo("cewp-effort-policy-stale-revision-"); + const fake = createFakeCodexAdapter(); + try { + const runId = createApprovedRun(repoRoot); + parseJson(runNode(cewpCli, [ + "supervise", "effort", runId, + "--operation", "implementation", + "--task-class", "demanding-implementation", + "--model", "gpt-old-plan", + "--effort", "high", + "--yes", "--json", + ], repoRoot), "approve old-plan effort policy"); + const revised = parseJson(runNode(cewpCli, [ + "supervise", "revise", runId, + "--goal", "Inspect a revised bounded behavior", + "--json", + ], repoRoot), "revise effort-policy plan"); + assert(revised.data.run.planRevision === 2, "fixture creates a new plan revision"); + parseJson(runNode(cewpCli, ["supervise", "approve", runId, "--yes", "--json"], repoRoot), "approve revised plan"); + assert(runNode(cewpCli, ["policy", "set", "full-authority"], repoRoot).status === 0, "stale revision fixture grants dispatch authority"); + const refused = runNode(cewpCli, [ + "supervise", "execute", runId, "--yes", "--timeout", "20", "--json", + ], repoRoot, { env: fake.env }); + assert(refused.status === 1, "old-plan effort approval cannot dispatch a revised plan"); + assert(refused.stderr.includes("current plan revision"), "stale approval refusal names the plan-revision mismatch"); + } finally { + fs.rmSync(fake.fakeRoot, { recursive: true, force: true }); + cleanupRepo(repoRoot); + } +} + +function main() { + const repoRoot = makeTempRepo("cewp-effort-policy-"); + const fake = createFakeCodexAdapter(); + try { + const runId = createApprovedRun(repoRoot); + const unapproved = runNode(cewpCli, [ + "supervise", "effort", runId, + "--operation", "implementation", + "--task-class", "fast-exploration", + "--json", + ], repoRoot); + assert(unapproved.status === 1, "effort changes require explicit --yes approval"); + assert(unapproved.stderr.includes("explicit operator approval"), "approval refusal is actionable"); + const configured = parseJson(runNode(cewpCli, [ + "supervise", "effort", runId, + "--operation", "implementation", + "--task-class", "fast-exploration", + "--yes", "--json", + ], repoRoot), "supervise effort"); + + assert(configured.command === "supervise.effort", "effort command identifies its public operation"); + const policy = configured.data.effortPolicy; + assert(policy.schemaVersion === "codex-effort-policy/v1", "effort policy is versioned"); + assert(policy.provider === "codex", "provider identity stays in the integration sidecar"); + assert(policy.automaticModelRouting === false, "automatic model routing remains disabled"); + assert(policy.assignments.implementation.taskClass === "fast-exploration", "explicit task class is retained"); + assert(policy.assignments.implementation.requested.model.status === "unknown", "task class does not infer a model"); + assert(policy.assignments.implementation.requested.effort.status === "unknown", "task class does not infer effort"); + assert(policy.assignments.implementation.approval.kind === "operator", "operator approval is recorded"); + const canonicalRun = JSON.parse(fs.readFileSync( + path.join(repoRoot, ".cewp", "supervised-runs", runId, "run.json"), + "utf8", + )); + assert(canonicalRun.effortPolicy === undefined, "provider-specific effort policy stays outside canonical run state"); + + const explicit = parseJson(runNode(cewpCli, [ + "supervise", "effort", runId, + "--operation", "implementation", + "--task-class", "demanding-implementation", + "--model", "gpt-test-explicit", + "--effort", "high", + "--yes", "--json", + ], repoRoot), "approve explicit model and effort"); + assert(explicit.data.effortPolicy.revision === 2, "approved setting change creates a new revision"); + const change = explicit.data.effortPolicy.history.at(-1); + assert(change.previous.taskClass === "fast-exploration", "change history retains the previous task class"); + assert(change.previous.requested.model.status === "unknown", "change history retains the previous unknown model"); + assert(change.next.taskClass === "demanding-implementation", "change history retains the next task class"); + assert(change.next.requested.model.value === "gpt-test-explicit", "change history retains the approved next model"); + assert(change.next.requested.effort.value === "high", "change history retains the approved next effort"); + const approvalEvents = fs.readFileSync( + path.join(repoRoot, ".cewp", "supervised-runs", runId, "events.jsonl"), + "utf8", + ).trim().split("\n").map((line) => JSON.parse(line)) + .filter((event) => event.type === "codex-effort-policy-approved"); + assert(approvalEvents.length === 2, "each effort policy approval is retained in the run event log"); + assert(approvalEvents.at(-1).revision === 2, "the event log identifies the approved policy revision"); + assert( + approvalEvents.at(-1).selectionDigest === explicit.data.effortPolicy.assignments.implementation.approval.selectionDigest, + "the event log binds the operator approval to the selected policy digest", + ); + assert(runNode(cewpCli, ["policy", "set", "full-authority"], repoRoot).status === 0, "fixture grants dispatch authority"); + + const executed = parseJson(runNode(cewpCli, [ + "supervise", "execute", runId, "--yes", "--timeout", "20", "--json", + ], repoRoot, { + env: { + ...fake.env, + CEWP_FAKE_CODEX_EXPECT_MODEL: "gpt-test-explicit", + CEWP_FAKE_CODEX_EXPECT_EFFORT: "high", + }, + }), "execute with approved effort policy"); + const attempt = executed.data.run.tasks[0].attempts[0]; + assert(attempt.codex.taskClass === "demanding-implementation", "dispatch evidence retains the approved task class"); + assert(attempt.codex.effectiveModel.status === "known", "explicit dispatch model becomes known evidence"); + assert(attempt.codex.effectiveModel.value === "gpt-test-explicit", "effective model matches the approved override"); + assert(attempt.codex.effectiveEffort.status === "known", "explicit dispatch effort becomes known evidence"); + assert(attempt.codex.effectiveEffort.value === "high", "effective effort matches the approved override"); + + const verified = parseJson(runNode(cewpCli, [ + "supervise", "verify", runId, "--timeout", "20", "--json", + ], repoRoot), "verify explicit-effort checkpoint"); + assert(verified.data.run.status === "checkpoint-complete", "review setup retains the verification gate"); + parseJson(runNode(cewpCli, [ + "supervise", "effort", runId, + "--operation", "reviewer", + "--task-class", "high-effort-independent-review", + "--model", "gpt-test-reviewer", + "--effort", "xhigh", + "--yes", "--json", + ], repoRoot), "approve reviewer effort policy"); + const reviewed = parseJson(runNode(cewpCli, [ + "supervise", "review", runId, "--yes", "--timeout", "20", "--json", + ], repoRoot, { + env: { + ...fake.env, + CEWP_FAKE_CODEX_EXPECT_MODEL: "gpt-test-reviewer", + CEWP_FAKE_CODEX_EXPECT_EFFORT: "xhigh", + }, + }), "review with approved effort policy"); + assert(reviewed.data.run.reviewer.codex.taskClass === "high-effort-independent-review", "review evidence retains its task class"); + assert(reviewed.data.run.reviewer.codex.effectiveModel.value === "gpt-test-reviewer", "review evidence records the effective model"); + assert(reviewed.data.run.reviewer.codex.effectiveEffort.value === "xhigh", "review evidence records the effective effort"); + } finally { + fs.rmSync(fake.fakeRoot, { recursive: true, force: true }); + cleanupRepo(repoRoot); + } +} + +try { + main(); + runRepairEffortContract(); + runEffortTamperContract(); + runStalePlanRevisionContract(); + console.log("[PASS] Codex task classes never trigger automatic model routing"); +} catch (error) { + console.error("[FAIL] Codex effort policy contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/integration-hook-evidence.js b/tests/contracts/integration-hook-evidence.js new file mode 100644 index 0000000..3422e86 --- /dev/null +++ b/tests/contracts/integration-hook-evidence.js @@ -0,0 +1,192 @@ +"use strict"; + +const fs = require("node:fs"); +const childProcess = require("node:child_process"); +const path = require("node:path"); +const { assert } = require("../harness/lib/assertions"); +const { cleanupRepo, makeTempRepo, runNode } = require("../harness/lib/temp-repo"); +const { validDefinition } = require("./workflow-definition"); +const { approveWorkflow } = require("./workflow-scheduler"); +const { recordSubagentHookEvent } = require("../../src/integration/hook-evidence"); + +const cewpCli = path.join(__dirname, "..", "..", "bin", "cewp.js"); + +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 main() { + const repoRoot = makeTempRepo("cewp-hook-evidence-"); + try { + const run = approveWorkflow(repoRoot, validDefinition()); + const runPath = path.join(repoRoot, ".cewp", "workflow-runs", run.runId, "run.json"); + const runBefore = fs.readFileSync(runPath, "utf8"); + const refused = runNode(cewpCli, [ + "integration", "hooks", "approve", run.runId, "--json", + ], repoRoot, { + env: { ...process.env, CEWP_HOOK_CODEX_VERSION: "codex-cli 0.200.0" }, + }); + assert(refused.status === 1, "hook trust cannot be activated without explicit --yes approval"); + assert(refused.stderr.includes("explicit operator approval"), "approval refusal explains the trust boundary"); + const approved = runNode(cewpCli, [ + "integration", "hooks", "approve", run.runId, "--yes", "--json", + ], repoRoot, { + env: { ...process.env, CEWP_HOOK_CODEX_VERSION: "codex-cli 0.200.0" }, + }); + assert(approved.status === 0, `hook approval succeeds: ${approved.stderr}`); + const output = JSON.parse(approved.stdout); + assert(output.command === "integration.hooks.approve", "approval identifies the public command"); + assert(output.data.trust.schemaVersion === "codex-hook-trust/v1", "hook trust is versioned"); + assert(output.data.trust.cewpVersion === "0.11.0-beta.0", "approval binds the CEWP runtime version"); + assert(output.data.trust.codexVersion === "codex-cli 0.200.0", "approval binds the observed Codex version"); + assert(/^sha256:[a-f0-9]{64}$/.test(output.data.trust.bundleDigest), "approval binds the exact hook bundle"); + assert(output.data.nextAction.command === "/hooks", "approval still requires the host trust review"); + assert(fs.readFileSync(runPath, "utf8") === runBefore, "hook trust stays outside provider-neutral run state"); + + const inspected = runNode(cewpCli, [ + "integration", "hooks", "status", run.runId, "--json", + ], repoRoot, { + env: { ...process.env, CEWP_HOOK_CODEX_VERSION: "codex-cli 0.200.0" }, + }); + assert(inspected.status === 0, `hook status succeeds: ${inspected.stderr}`); + const status = JSON.parse(inspected.stdout); + assert(status.command === "integration.hooks.status", "status identifies the public inspection command"); + assert(status.data.compatible === true && status.data.active === true, "current approved hook evidence is active"); + assert(status.data.claims.coreEnforcement === false, "status never promotes hook evidence to Core enforcement"); + + const driftStatus = runNode(cewpCli, [ + "integration", "hooks", "status", run.runId, "--json", + ], repoRoot, { + env: { ...process.env, CEWP_HOOK_CODEX_VERSION: "codex-cli 0.201.0" }, + }); + assert(driftStatus.status === 0, `hook drift status remains inspectable: ${driftStatus.stderr}`); + const drift = JSON.parse(driftStatus.stdout).data; + assert(drift.compatible === false && drift.active === false, "version drift disables trusted hook evidence"); + assert(drift.warnings[0].code === "codex-version-drift", "version drift has a stable compatibility code"); + assert(drift.fallback === "core-and-conversation-only", "version drift names the safe fallback"); + + const hookPath = path.join(__dirname, "..", "..", "plugins", "cewp", "hooks", "capture-subagent.js"); + const runHook = (input) => childProcess.spawnSync(process.execPath, [hookPath], { + cwd: repoRoot, + input: JSON.stringify(input), + encoding: "utf8", + windowsHide: true, + env: { + ...process.env, + CEWP_HOOK_CODEX_VERSION: "codex-cli 0.200.0", + CEWP_HOOK_CLI_COMMAND: process.execPath, + CEWP_HOOK_CLI_PREFIX_ARGS: JSON.stringify([cewpCli]), + }, + }); + const common = { + session_id: "parent-session-1", + transcript_path: path.join(repoRoot, "does-not-exist.jsonl"), + cwd: repoRoot, + model: "gpt-test-host", + turn_id: "parent-turn-1", + agent_id: "agent-1", + agent_type: "explorer", + permission_mode: "default", + }; + const started = runHook({ ...common, hook_event_name: "SubagentStart" }); + assert(started.status === 0, `SubagentStart hook succeeds: ${started.stderr}`); + assert(JSON.stringify(JSON.parse(started.stdout)) === "{}", "evidence-only start does not steer the subagent"); + const stopped = runHook({ + ...common, + hook_event_name: "SubagentStop", + agent_transcript_path: path.join(repoRoot, "also-does-not-exist.jsonl"), + stop_hook_active: false, + last_assistant_message: "Inspected the bounded files and found no scope issue.", + }); + assert(stopped.status === 0, `SubagentStop hook succeeds: ${stopped.stderr}`); + assert(JSON.stringify(JSON.parse(stopped.stdout)) === "{}", "evidence-only stop does not continue or block the subagent"); + + const ledgerPath = path.join( + repoRoot, ".cewp", "workflow-runs", run.runId, "integration", "subagent-hook-evidence.jsonl", + ); + const evidence = fs.readFileSync(ledgerPath, "utf8").trim().split("\n").map((line) => JSON.parse(line)); + assert(evidence.length === 2, "start and stop lifecycle evidence are append-only"); + assert(evidence[0].type === "subagent-started" && evidence[1].type === "subagent-stopped", "supported lifecycle types are normalized"); + assert(evidence[1].references.agentId === "agent-1", "documented subagent id is preserved"); + assert(evidence[1].references.parentSessionId === "parent-session-1", "documented parent session is preserved"); + assert(evidence[1].references.agentThreadId.status === "unknown", "an unavailable subagent thread id is never invented"); + assert(evidence[1].summary.value.includes("no scope issue"), "bounded host summary is retained"); + assert(evidence[1].claims.coreEnforcement === false, "hook evidence never claims Core enforcement"); + + const ledgerBeforeDrift = fs.readFileSync(ledgerPath, "utf8"); + const versionDrift = childProcess.spawnSync(process.execPath, [hookPath], { + cwd: repoRoot, + input: JSON.stringify({ ...common, hook_event_name: "SubagentStart", agent_id: "agent-drift" }), + encoding: "utf8", + windowsHide: true, + env: { + ...process.env, + CEWP_HOOK_CODEX_VERSION: "codex-cli 0.201.0", + CEWP_HOOK_CLI_COMMAND: process.execPath, + CEWP_HOOK_CLI_PREFIX_ARGS: JSON.stringify([cewpCli]), + }, + }); + assert(versionDrift.status === 0, "version drift does not break the host lifecycle"); + assert(JSON.parse(versionDrift.stdout).systemMessage.includes("version drift"), "version drift is visible and actionable"); + assert(fs.readFileSync(ledgerPath, "utf8") === ledgerBeforeDrift, "version drift cannot append trusted evidence"); + + const malformed = runHook({ ...common, hook_event_name: "SubagentStart", agent_id: null }); + assert(malformed.status === 0, "malformed hook input fails without breaking the host lifecycle"); + assert(JSON.parse(malformed.stdout).systemMessage.includes("agent_id is required"), "malformed input explains the compatibility failure"); + assert(fs.readFileSync(ledgerPath, "utf8") === ledgerBeforeDrift, "malformed input cannot append evidence"); + + const changedPluginRoot = path.join(repoRoot, "changed-plugin"); + fs.mkdirSync(path.join(changedPluginRoot, "hooks"), { recursive: true }); + fs.copyFileSync( + path.join(__dirname, "..", "..", "plugins", "cewp", "hooks", "hooks.json"), + path.join(changedPluginRoot, "hooks", "hooks.json"), + ); + fs.copyFileSync(hookPath, path.join(changedPluginRoot, "hooks", "capture-subagent.js")); + fs.appendFileSync(path.join(changedPluginRoot, "hooks", "capture-subagent.js"), "\n// changed after review\n"); + assertThrows( + () => recordSubagentHookEvent({ + repoRoot, + input: { ...common, hook_event_name: "SubagentStart", agent_id: "agent-definition-drift" }, + codexVersion: "codex-cli 0.200.0", + pluginRoot: changedPluginRoot, + }), + /definition drift/, + "changed hook definitions require a fresh review", + ); + assert(fs.readFileSync(ledgerPath, "utf8") === ledgerBeforeDrift, "definition drift cannot append trusted evidence"); + assert(fs.readFileSync(runPath, "utf8") === runBefore, "hook failures and observations never mutate Core workflow state"); + + const trustPath = path.join( + repoRoot, ".cewp", "workflow-runs", run.runId, "integration", "codex-hook-trust.json", + ); + fs.writeFileSync(trustPath, "{ malformed trust receipt\n"); + const malformedTrustStatus = runNode(cewpCli, [ + "integration", "hooks", "status", run.runId, "--json", + ], repoRoot, { + env: { ...process.env, CEWP_HOOK_CODEX_VERSION: "codex-cli 0.200.0" }, + }); + assert(malformedTrustStatus.status === 0, "malformed trust remains inspectable through a fail-safe status"); + const malformedTrust = JSON.parse(malformedTrustStatus.stdout).data; + assert(malformedTrust.active === false && malformedTrust.compatible === false, "malformed trust disables evidence"); + assert(malformedTrust.warnings[0].code === "hook-trust-change", "malformed trust has a stable warning code"); + assert(malformedTrust.fallback === "core-and-conversation-only", "malformed trust preserves the safe fallback"); + } finally { + cleanupRepo(repoRoot); + } +} + +try { + main(); + console.log("[PASS] hook evidence is explicitly approved and version-bound"); +} catch (error) { + console.error("[FAIL] hook evidence integration contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/integration-mcp.js b/tests/contracts/integration-mcp.js new file mode 100644 index 0000000..2b962ab --- /dev/null +++ b/tests/contracts/integration-mcp.js @@ -0,0 +1,116 @@ +"use strict"; + +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); +const { assert } = require("../harness/lib/assertions"); +const { cleanupRepo, makeTempRepo } = require("../harness/lib/temp-repo"); + +const mcpBin = path.join(__dirname, "..", "..", "bin", "cewp-mcp.js"); + +function request(id, method, params) { + return JSON.stringify({ jsonrpc: "2.0", id, method, ...(params ? { params } : {}) }); +} + +function runMcp(repoRoot, messages) { + const result = spawnSync(process.execPath, [mcpBin], { + cwd: repoRoot, + encoding: "utf8", + input: `${messages.join("\n")}\n`, + }); + assert(result.status === 0, `MCP server exits cleanly: ${result.stderr}`); + return result.stdout.trim().split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)); +} + +function call(id, name, args = {}) { + return request(id, "tools/call", { name, arguments: args }); +} + +function runContract() { + const repoRoot = makeTempRepo("cewp-integration-mcp-"); + try { + const responses = runMcp(repoRoot, [ + request(1, "initialize", { + protocolVersion: "2025-11-25", + capabilities: {}, + clientInfo: { name: "cewp-contract", version: "1.0.0" }, + }), + JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }), + request(2, "tools/list", {}), + call(3, "cewp_create", { + goal: "Update the bounded file", + scopes: ["README.md"], + verificationCommands: ["git diff --check"], + stoppingConditions: ["The diff check passes"], + }), + ]); + + assert(responses.length === 3, "notifications do not receive responses"); + assert(responses[0].result.protocolVersion === "2025-11-25", "supported protocol is negotiated"); + assert(responses[0].result.compatibility.compatible === true, "matching MCP protocol is compatible"); + assert(responses[0].result.capabilities.tools, "server advertises tools capability"); + const names = responses[1].result.tools.map((tool) => tool.name); + assert(JSON.stringify(names) === JSON.stringify([ + "cewp_create", "cewp_inspect", "cewp_approve", "cewp_continue", + "cewp_retry", "cewp_revise", "cewp_verify", "cewp_finalize", + ]), "MCP exposes only the eight roadmap operations"); + const inspectTool = responses[1].result.tools.find((tool) => tool.name === "cewp_inspect"); + assert(inspectTool.annotations.readOnlyHint === false, "inspect truthfully declares its generated progress refresh"); + assert(responses[2].result.isError !== true, "create succeeds through MCP"); + const created = responses[2].result.structuredContent; + assert(created.run.status === "proposed", "create calls the supervised Core proposal service"); + assert(created.run.repo.root === repoRoot, "repository root is fixed to the MCP process cwd"); + + const runId = created.run.runId; + const gates = runMcp(repoRoot, [ + request(1, "initialize", { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: "cewp-contract", version: "1.0.0" }, + }), + call(2, "cewp_inspect", { runId }), + call(3, "cewp_approve", { runId, confirm: false }), + call(4, "cewp_approve", { runId, confirm: true }), + call(5, "cewp_continue", { runId }), + call(6, "cewp_retry", { runId, confirm: false }), + call(7, "cewp_revise", { runId, goal: "Revised goal" }), + call(8, "cewp_verify", { runId }), + call(9, "cewp_finalize", { runId, confirm: false }), + call(10, "not_a_cewp_tool", {}), + call(11, "cewp_inspect", { runId: 7 }), + ]); + + assert(gates[1].result.structuredContent.run.status === "proposed", "inspect uses Core state inspection"); + assert(gates[2].result.isError === true && gates[2].result.content[0].text.includes("explicit confirmation"), "approve requires MCP confirmation"); + assert(gates[3].result.structuredContent.run.status === "approved", "confirmed approve reaches Core approval"); + assert(gates[4].result.isError === true && gates[4].result.content[0].text.includes("verified checkpoint"), "continue preserves checkpoint gate"); + assert(gates[5].result.isError === true && gates[5].result.content[0].text.includes("explicit confirmation"), "retry requires MCP confirmation before Core dispatch"); + assert(gates[6].result.structuredContent.run.status === "proposed", "revise calls Core and invalidates approval"); + assert(gates[7].result.isError === true && gates[7].result.content[0].text.includes("cannot verify"), "verify preserves Core state gate"); + assert(gates[8].result.isError === true && gates[8].result.content[0].text.includes("explicit confirmation"), "finalize requires MCP confirmation"); + assert(gates[9].error.code === -32602 && gates[9].error.message.includes("Unknown tool"), "unknown tools are protocol errors"); + assert(gates[10].error.code === -32602 && gates[10].error.message.includes("input schema"), "malformed tool arguments are protocol errors"); + + const drift = runMcp(repoRoot, [ + request(1, "initialize", { + protocolVersion: "2099-01-01", + capabilities: {}, + clientInfo: { name: "future-client", version: "1.0.0" }, + }), + ])[0].result; + assert(drift.protocolVersion === "2025-11-25", "unsupported MCP version negotiates a supported version"); + assert(drift.compatibility.compatible === false, "MCP protocol drift is explicit"); + assert(drift.compatibility.warning.code === "mcp-protocol-version-drift", "MCP drift has a stable warning code"); + assert(drift.compatibility.fallback === "cewp-cli-operator-json", "MCP drift names the CLI fallback"); + } finally { + cleanupRepo(repoRoot); + } +} + +try { + runContract(); + console.log("[PASS] local MCP tools share supervised Core gates"); +} catch (error) { + console.error("[FAIL] local MCP integration contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/plugin-package.js b/tests/contracts/plugin-package.js index a2a4e1e..6520bdb 100644 --- a/tests/contracts/plugin-package.js +++ b/tests/contracts/plugin-package.js @@ -23,8 +23,24 @@ function runPluginPackageContract() { assert(manifest.version === packageJson.version, "plugin and npm versions stay aligned"); assert(manifest.skills === "./skills/", "plugin skill path is contained and relative"); assert(manifest.apps === undefined, "plugin does not claim an unbuilt app"); - assert(manifest.mcpServers === undefined, "plugin does not claim an unbuilt MCP server"); - assert(manifest.hooks === undefined, "plugin does not enable unreviewed hooks"); + assert(manifest.mcpServers === "./.mcp.json", "plugin declares one contained local MCP bundle"); + const mcpConfig = readJson(path.join(repoRoot, "plugins", "cewp", ".mcp.json")); + assert( + JSON.stringify(Object.keys(mcpConfig.mcpServers)) === JSON.stringify(["cewp"]), + "plugin MCP bundle declares only the CEWP local server", + ); + assert(mcpConfig.mcpServers.cewp.command === "cewp-mcp", "plugin MCP delegates to the installed CEWP Core binary"); + assert(packageJson.bin["cewp-mcp"] === "bin/cewp-mcp.js", "npm package exposes the declared MCP binary"); + assert(manifest.hooks === "./hooks/hooks.json", "plugin declares one contained reviewable hook bundle"); + const hookConfig = readJson(path.join(repoRoot, "plugins", "cewp", "hooks", "hooks.json")); + assert( + JSON.stringify(Object.keys(hookConfig.hooks).sort()) === JSON.stringify(["SubagentStart", "SubagentStop"]), + "plugin hooks are limited to subagent evidence events", + ); + assert( + fs.existsSync(path.join(repoRoot, "plugins", "cewp", "hooks", "capture-subagent.js")), + "declared hook handler exists", + ); assert( fs.existsSync(path.join(repoRoot, "plugins", "cewp", "assets", "cewp.svg")), "plugin asset exists", diff --git a/tests/contracts/supervised-execution.js b/tests/contracts/supervised-execution.js index cc8ad5b..bd7d198 100644 --- a/tests/contracts/supervised-execution.js +++ b/tests/contracts/supervised-execution.js @@ -48,6 +48,57 @@ function createApprovedRun(repoRoot) { return runId; } +function runNativeOwnershipConflictContract() { + const repoRoot = makeTempRepo("cewp-supervised-native-conflict-"); + const fake = createFakeCodexAdapter(); + try { + const runId = createApprovedRun(repoRoot); + assert(runNode(cewpCli, ["policy", "set", "full-authority"], repoRoot).status === 0, "fixture grants worker authority"); + const runPath = path.join(repoRoot, ".cewp", "supervised-runs", runId, "run.json"); + const run = JSON.parse(fs.readFileSync(runPath, "utf8")); + const taskId = run.tasks[0].id; + const targetWorktree = path.resolve( + repoRoot, + "..", + ".cewp-worktrees", + path.basename(repoRoot), + runId, + taskId, + ); + const nativeOwnershipPath = path.join( + repoRoot, + ".cewp", + "workflow-runs", + "native-owner", + "integration", + "ownership.json", + ); + fs.mkdirSync(path.dirname(nativeOwnershipPath), { recursive: true }); + fs.writeFileSync(nativeOwnershipPath, `${JSON.stringify({ + schemaVersion: "execution-ownership/v1", + runId: "native-owner", + taskId, + checkpointId: `${taskId}-attempt-0001`, + owner: "native", + backend: null, + status: "active", + createdAt: "2026-07-18T12:00:00.000Z", + cleanupAuthority: "host-owner", + worktree: { id: `${runId}:${taskId}`, path: targetWorktree }, + }, null, 2)}\n`); + + const result = runNode(cewpCli, [ + "supervise", "execute", runId, "--yes", "--timeout", "20", "--json", + ], repoRoot, { env: fake.env }); + assert(result.status === 1, "managed dispatch rejects a native-owned task worktree"); + assert(result.stderr.includes("execution-ownership-conflict"), "ownership refusal is actionable"); + assert(!fs.existsSync(targetWorktree), "conflicting managed worktree is not created"); + } finally { + fs.rmSync(fake.fakeRoot, { recursive: true, force: true }); + cleanupRepo(repoRoot); + } +} + function runSupervisedExecutionContract() { const repoRoot = makeTempRepo("cewp-supervised-exec-"); const fake = createFakeCodexAdapter(); @@ -209,6 +260,7 @@ function runSupervisedExecutionContract() { } try { + runNativeOwnershipConflictContract(); runSupervisedExecutionContract(); console.log("[PASS] supervised dispatch preserves ownership, scope, and usage truth"); } catch (error) { diff --git a/tests/contracts/workflow-release.js b/tests/contracts/workflow-release.js index 23e0064..839b8d1 100644 --- a/tests/contracts/workflow-release.js +++ b/tests/contracts/workflow-release.js @@ -13,36 +13,38 @@ const plugin = JSON.parse(fs.readFileSync( const releaseNotes = fs.readFileSync(path.join(repoRoot, "docs", "release-notes.md"), "utf8"); function runWorkflowReleaseContract() { - assert(packageJson.version === "0.10.0-beta.0", "Phase 10 package version is exact"); + assert(packageJson.version === "0.11.0-beta.0", "Phase 11 package version is exact"); assert(plugin.version === packageJson.version, "plugin version follows the package version"); const unreleasedIndex = releaseNotes.indexOf("## Unreleased"); - const releaseIndex = releaseNotes.indexOf("## 0.10.0-beta.0"); - const previousIndex = releaseNotes.indexOf("## 0.8.0-beta.0"); - assert(unreleasedIndex >= 0 && releaseIndex > unreleasedIndex, "fresh Unreleased precedes the Phase 10 release"); - assert(previousIndex > releaseIndex, "Phase 10 release precedes earlier release history"); + const releaseIndex = releaseNotes.indexOf("## 0.11.0-beta.0"); + const previousIndex = releaseNotes.indexOf("## 0.10.0-beta.0"); + assert(unreleasedIndex >= 0 && releaseIndex > unreleasedIndex, "fresh Unreleased precedes the Phase 11 release"); + assert(previousIndex > releaseIndex, "Phase 11 release precedes earlier release history"); const unreleased = releaseNotes.slice(unreleasedIndex, releaseIndex); assert(unreleased.includes("No changes yet."), "fresh Unreleased is explicitly empty"); for (const claim of [ - "supervised golden path", - "workflow-compiler-request/v1", - "workflow-definition/v1", - "run-state/v2", - "one-, two-, and four-worker", - "OpenCode remains experimental", - "independent external pilot evidence remains Phase 13 validation debt", - "No provider, desktop UI, terminal server, native-goal control, merge, push, publish, tag, or release automation", + "native and managed ownership", + "no automatic model routing", + "SubagentStart", + "eight Core-backed MCP tools", + "observed, imported, stale, malformed, unavailable, and unknown", + "audit-only", + "App Server remains ungraduated", + "`codex-exec` fallback", + "external pilot evidence remains Phase 13 validation debt", + "No provider, desktop UI, terminal server, merge, push, publish, tag, or release automation", ]) { - assert(releaseNotes.slice(releaseIndex, previousIndex).includes(claim), `Phase 10 notes include honest claim: ${claim}`); + assert(releaseNotes.slice(releaseIndex, previousIndex).includes(claim), `Phase 11 notes include honest claim: ${claim}`); } - assert(packageJson.files.includes("docs/workflow-runtime.md"), "Phase 10 runtime guide is in the package surface"); + assert(packageJson.files.includes("docs/external-integration-boundary.md"), "Phase 11 boundary guide is in the package surface"); } try { runWorkflowReleaseContract(); - console.log("[PASS] Phase 10 version and release surface are aligned"); + console.log("[PASS] Phase 11 version and release surface are aligned"); } catch (error) { - console.error("[FAIL] Phase 10 release surface contract"); + console.error("[FAIL] Phase 11 release surface contract"); console.error(error && error.stack ? error.stack : error); process.exitCode = 1; } diff --git a/tests/harness/README.md b/tests/harness/README.md index cc20b65..945ed1c 100644 --- a/tests/harness/README.md +++ b/tests/harness/README.md @@ -25,6 +25,7 @@ npm run test:skill-format npm run test:ownership-gates npm run test:fixtures npm run test:plugin-package +npm run test:integration-hook-evidence ``` `npm test` runs these focused contracts before the broader smoke lifecycle. diff --git a/tests/harness/lib/fake-adapter.js b/tests/harness/lib/fake-adapter.js index 53438f2..bc528ac 100644 --- a/tests/harness/lib/fake-adapter.js +++ b/tests/harness/lib/fake-adapter.js @@ -38,6 +38,17 @@ if (args[0] !== "exec") { process.exit(2); } +const expectedModel = process.env.CEWP_FAKE_CODEX_EXPECT_MODEL; +const expectedEffort = process.env.CEWP_FAKE_CODEX_EXPECT_EFFORT; +if (expectedModel && valueAfter("--model") !== expectedModel) { + console.error("fake codex did not receive the approved --model override"); + process.exit(3); +} +if (expectedEffort && !args.includes('model_reasoning_effort="' + expectedEffort + '"')) { + console.error("fake codex did not receive the approved reasoning-effort override"); + process.exit(3); +} + function valueAfter(flag) { const index = args.indexOf(flag); return index === -1 ? undefined : args[index + 1]; diff --git a/tests/harness/run-smoke.js b/tests/harness/run-smoke.js index c176348..c442adb 100644 --- a/tests/harness/run-smoke.js +++ b/tests/harness/run-smoke.js @@ -2572,7 +2572,7 @@ async function main() { const pack = run("npm", ["pack", "--dry-run"], { cwd: cewpRoot, timeout: 120000 }); const packOutput = `${pack.stdout}\n${pack.stderr}`; assertExit(pack, 0, "npm pack --dry-run"); - assert(packageJson.version === "0.10.0-beta.0", `unexpected package version: ${packageJson.version}`); + assert(packageJson.version === "0.11.0-beta.0", `unexpected package version: ${packageJson.version}`); assert(packOutput.includes("docs/adapter-contract.md"), "adapter contract doc should be packed"); assert(packOutput.includes("docs/supervised-workflow.md"), "supervised workflow doc should be packed"); assert(packOutput.includes("docs/known-limitations.md"), "known limitations should be packed");