diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..2d1707b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Private security report + url: https://github.com/SetraTheXX/Codex-Engineering-Workflow-Pack/security/advisories/new + about: Report vulnerabilities privately. Do not disclose secrets or exploit details in a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..312b176 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,36 @@ +name: Feature request +description: Propose a focused improvement backed by a real workflow need. +title: "[feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Do not include secrets, private source, repository paths, or raw logs. Keep examples sanitized or redacted. Native Codex goal mode remains the simpler path for low-risk one-step work. + - type: textarea + id: task + attributes: + label: Target task class + description: Describe the bounded, risky, recoverable, or evidence-sensitive task where this helps. + validations: + required: true + - type: textarea + id: current + attributes: + label: Current workaround and limitation + validations: + required: true + - type: textarea + id: outcome + attributes: + label: Observable outcome + description: Explain the smallest public behavior that would solve the need. + validations: + required: true + - type: dropdown + id: native + attributes: + label: Would native goal mode be sufficient? + options: ["No", "Uncertain", "Yes"] + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/receipt-quality.yml b/.github/ISSUE_TEMPLATE/receipt-quality.yml new file mode 100644 index 0000000..f0976cf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/receipt-quality.yml @@ -0,0 +1,36 @@ +name: Receipt quality feedback +description: Report missing, misleading, or hard-to-understand evidence. +title: "[receipt]: " +labels: ["evidence"] +body: + - type: markdown + attributes: + value: | + Do not include secrets, credentials, source code, private paths, prompts, or raw logs. Include only a redacted export or a sanitized excerpt. + - type: dropdown + id: truth + attributes: + label: Affected truth label + description: Which observed / estimated / budgeted / unknown distinction was unclear? + options: [observed, estimated, budgeted, unknown, multiple, not-applicable] + validations: + required: true + - type: textarea + id: field + attributes: + label: Misleading or missing field + description: Name the receipt section and explain what a reader could misunderstand. + validations: + required: true + - type: textarea + id: excerpt + attributes: + label: Redacted receipt excerpt + description: Use `cewp workflow export` or `cewp pilot export`; remove anything still sensitive. + render: json + - type: textarea + id: expected + attributes: + label: Expected explanation + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/setup-failure.yml b/.github/ISSUE_TEMPLATE/setup-failure.yml new file mode 100644 index 0000000..31681e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/setup-failure.yml @@ -0,0 +1,38 @@ +name: Setup failure +description: Report an install, initialization, or diagnostics failure. +title: "[setup]: " +labels: ["setup"] +body: + - type: markdown + attributes: + value: | + Do not include secrets, credentials, private repository paths, source code, or raw logs. Redact local paths and sensitive values before submitting. + - type: input + id: version + attributes: + label: CEWP version + placeholder: 0.13.0-beta.0 + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: Provide OS, Node, Git, and Codex versions without account details. + validations: + required: true + - type: textarea + id: doctor + attributes: + label: Sanitized cewp doctor --json output + description: Remove absolute paths and sensitive values. + render: json + validations: + required: true + - type: textarea + id: failure + attributes: + label: Sanitized failure and attempted remediation + description: Include the public command, short redacted error, and whether the suggested remediation worked. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/workflow-failure.yml b/.github/ISSUE_TEMPLATE/workflow-failure.yml new file mode 100644 index 0000000..b47924e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/workflow-failure.yml @@ -0,0 +1,40 @@ +name: Workflow failure +description: Report a supervision, verification, recovery, review, or finalize failure. +title: "[workflow]: " +labels: ["workflow"] +body: + - type: markdown + attributes: + value: | + Do not include secrets, credentials, private repository paths, source code, or raw logs. Use a redacted receipt excerpt when evidence is useful. + - type: dropdown + id: stage + attributes: + label: Workflow stage + options: [plan, approval, execution, verification, recovery, review, receipt, finalize, cleanup] + validations: + required: true + - type: textarea + id: command + attributes: + label: Public command and sanitized error + description: Include only the bounded command and redacted error. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true + - type: textarea + id: recovery + attributes: + label: Recovery state + description: State whether prior evidence was retained and what next action CEWP reported. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d0f48d3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,77 @@ +# Contributing To CEWP + +CEWP is a Codex-first, local-first supervision and evidence project. Contributions +should improve the guarded Codex path, compatibility, recovery, verification, or +evidence quality. Provider expansion and automatic model routing remain paused +through 1.0. + +## Local Setup + +Use maintained Node.js 22 or newer and Git: + +```bash +node ./bin/cewp.js --help +node ./bin/cewp.js doctor +npm test +``` + +The runtime has no package dependencies. Do not introduce one unless it produces +a demonstrated contract-level benefit. + +## Development Loop + +Use one observable behavior at a time: + +```text +red test -> smallest green implementation -> refactor -> next behavior +``` + +Prefer public CLI or Core contracts over private call-count assertions. Start with +the nearest focused command, for example: + +```bash +npm run test:pilot-record +npm run test:phase13 +npm run check +git diff --check +``` + +Before claiming completion, run the commands required by the affected contract, +the full suite, package dry-run, and the relevant clean-install/platform gate. + +## Contribution Boundaries + +- CEWP Core remains authoritative for ownership, scope, policy, verification, + budgets, reviewer PASS, and finalization. +- Keep exactly one execution owner and one managed backend per checkpoint. +- Keep provider-specific identities outside provider-neutral workflow schemas. +- Do not add providers, desktop UI, terminal servers, private host protocols, + mandatory telemetry, automatic merge/push/publish/tag/release, or a global skip + verification switch. +- Treat unknown host usage and billing impact as unknown, never zero. +- Preserve beta read compatibility and add migration tests for format changes. + +## Privacy + +Do not commit `.cewp/pilots/`, `.cewp/`, `.cewp-private`, credentials, raw prompts, +private logs, or local repository paths. The private `phase-8-to-1.0` roadmap is +not a public package artifact. Before every maintainer commit, the tracking scan +must return no matches: + +```bash +git ls-files | rg "phase-8-to-1\.0|\.cewp-private" +``` + +Redacted exports reduce disclosure risk but do not prove arbitrary prose contains +no secret. Review every export before sharing it. + +## Issues And Pull Requests + +Choose a `good first issue` only when it has a bounded public behavior, explicit +files or scope, a focused test command, and no private pilot dependency. Useful +starter work includes documentation corrections, deterministic fixture gaps, +clear diagnostics, and narrowly scoped contract tests. + +Describe the user-visible outcome, test evidence, migration impact, and any gate +that remains open. A passing test is evidence for the behavior it covers, not a +claim that a whole roadmap phase or real-user pilot succeeded. diff --git a/README.md b/README.md index a3e5826..405deaf 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 @@ -108,10 +117,38 @@ Budget or host exhaustion produces a resumable pause, not a fake PASS. Partial f CEWP still ships ten reusable engineering skills and the earlier Coordinator Mode runtime for compatibility. The supervised path remains the managed `codex-exec` golden path. The workflow runtime adds source-bound compiler requests, approved task graphs, variable workers, result intake, recovery, revisions, and migrations without executing arbitrary prose or adding another backend. +## Local Pilot Evidence + +Phase 13 pilot records are optional, local, and ignored by Git. They distinguish +maintainer dogfood from genuinely independent participants and never add mandatory +telemetry: + +```bash +cewp pilot create --pilot-id dogfood-1 --participant maintainer-dogfood --participant-id maintainer-1 +cewp pilot status --json +cewp pilot export dogfood-1 --json +``` + +The infrastructure is implemented, but real external pilot evidence is still +absent. Fixtures and this maintainer's own runs do not satisfy external-user gates; +1.0 is therefore not complete. + ## Documentation - [Install Guide](docs/install.md) - [Supervised Workflow](docs/supervised-workflow.md) +- [Native Goal or CEWP?](docs/native-goal-or-cewp.md) +- [Stable Compatibility Contract](docs/stable-compatibility.md) +- [Stable Contract Reference](docs/contracts.md) +- [Migration Policy](docs/migration-policy.md) +- [Troubleshooting and Recovery](docs/recovery-guide.md) +- [Performance Budgets](docs/performance-budgets.md) +- [1.0 Security Review](docs/security-review-1.0.md) +- [Türkçe Manuel Kabul ve Deneme Rehberi](docs/manual-acceptance.md) +- [External Integration Boundary](docs/external-integration-boundary.md) +- [Evidence Receipts](docs/evidence-receipts.md) +- [Architecture](docs/architecture.md) +- [Contract Extension Example](docs/contract-extension-example.md) - [Workflow Runtime](docs/workflow-runtime.md) - [Known Limitations](docs/known-limitations.md) - [Pilot Kit](docs/pilot-kit.md) @@ -120,10 +157,12 @@ CEWP still ships ten reusable engineering skills and the earlier Coordinator Mod - [Coordinator Mode Compatibility](docs/coordinator-mode.md) - [Adapter Contract](docs/adapter-contract.md) - [Release Notes](docs/release-notes.md) +- [Contributing](CONTRIBUTING.md) +- [Security Policy](SECURITY.md) ## Status -CEWP is beta software. The workflow compiler emits a source-bound agent request rather than calling a model, and the graph runtime accepts only validated, explicitly approved definitions and evidence. External pilot gates are still required before a future stable release is declared complete. Review the plan, evidence, and receipt before integrating changes. +CEWP is beta software. Phase 14 stable-core contracts are available through `cewp compatibility --json`, but external pilot gates and exact clean release matrices are still required before 1.0 can be declared complete. Review the plan, evidence, and receipt before integrating changes. ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..47bc9cf --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,45 @@ +# Security Policy + +CEWP is beta software. Security fixes for supported local Core, CLI, Codex plugin, +MCP, hook, evidence, and packaging surfaces are accepted throughout the beta. + +## Private Reporting + +Report a suspected vulnerability through a private GitHub security advisory: + +https://github.com/SetraTheXX/Codex-Engineering-Workflow-Pack/security/advisories/new + +Do not disclose exploit details, secrets, credentials, private paths, source code, +or unredacted logs in a public issue. If the private advisory form is unavailable, +open a public issue that asks the maintainer for a private contact path without +including vulnerability details. + +Include the CEWP version, OS, Node/Git/Codex versions, affected public command, +expected boundary, and the smallest sanitized reproduction. Do not send account +data or authentication material. + +## In-Scope Boundaries + +Reports are especially useful for: + +- path containment, traversal, and symlink escapes; +- unsafe command or argument construction; +- MCP schema, confirmation, or Core-gate bypasses; +- hook trust, bundle drift, and false enforcement claims; +- receipt and pilot-export redaction failures; +- worktree ownership, cleanup, or double-dispatch conflicts; +- imported evidence being presented as preventive enforcement; +- provider output parsing that mutates state after malformed or partial output; +- package inclusion of private runtime or planning files. + +CEWP does not claim that local hashes are tamper-proof, that hooks intercept every +tool path, or that pattern redaction proves arbitrary prose has no secret. Reports +that demonstrate a mismatch between documented and actual trust boundaries are in +scope. + +## Response And Disclosure + +The maintainer will acknowledge a private report when available, reproduce it in +a safe local fixture, classify data-loss and guardrail impact, and coordinate a +fix and disclosure timeline with the reporter. Do not publish details before a +fix or explicit coordinated disclosure agreement. 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..b71fc84 100644 --- a/bin/cewp.js +++ b/bin/cewp.js @@ -19,6 +19,7 @@ const { } = require("../src/run/basic"); const { runFinalize } = require("../src/run/finalize"); const { runCollect } = require("../src/run/collect"); +const { runVerify } = require("../src/run/verify"); const { runDispatchPlan } = require("../src/run/dispatch/plan"); const { runDispatchCheck } = require("../src/run/dispatch/check"); const { runDispatchPrompts } = require("../src/run/dispatch/prompts"); @@ -34,6 +35,9 @@ 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 { runPilot } = require("../src/pilot/cli"); +const { runCompatibility } = require("../src/compatibility/contract"); const { printHuman: printSupervisedDemo, runSupervisedDemo } = require("../src/demo/supervised"); function runDemo(options) { @@ -76,6 +80,11 @@ async function runCommand(options) { return; } + if (options.subcommand === "verify") { + runVerify(options); + return; + } + if (options.subcommand === "prompts") { runPrompts(options); return; @@ -209,12 +218,27 @@ async function main() { return; } + if (args.command === "integration") { + runIntegration(args); + return; + } + + if (args.command === "pilot") { + runPilot(args); + return; + } + + if (args.command === "compatibility") { + runCompatibility(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", "pilot", "compatibility", "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/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..bb96db4 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,70 @@ +# CEWP Architecture + +CEWP supervises long-running Codex work without replacing the host's model loop, +terminal, task UI, or native goal system. CEWP Core is authoritative for every +policy and completion decision; conversation output, hooks, reports, and plugins +are projections of that state. + +## Layers + +```text +Host surface + -> Codex plugin (skills, optional hook, local MCP declaration) + -> CEWP Core (state, policy, budgets, verification, reviewer gates) + -> Execution bridge (managed codex-exec, supported native intake, audit-only) + -> Evidence and pilot (receipts, reports, comparisons, local pilot ledger) +``` + +### Host surface + +ChatGPT desktop Codex mode or Codex CLI provides the conversation and task +surface. CEWP does not attach to a private host session or patch native UI. + +### Codex plugin + +The plugin supplies focused skills, a local stdio MCP bridge, and an optional +review-required evidence hook. These surfaces call Core and cannot reopen a gate. + +### CEWP Core + +Core owns canonical run state, scope, policy, budgets, task transitions, +verification, reviewer PASS, evidence integrity, and explicit finalization. JSON +state and append-only events are runtime truth; Markdown is derived presentation. + +### Execution bridge + +Every run has one execution owner and one managed backend where a backend applies. +The supported managed golden path remains `managed` plus `codex-exec`. Native and +audit-only paths use documented bindings or explicit intake and never inherit +managed enforcement claims. + +### Evidence and pilot + +Workflow receipts, offline reports, comparisons, and redacted exports consume the +same canonical run evidence. Phase 13 pilot records live under `.cewp/pilots/` and +count only validated independent evidence. Fixtures and maintainer dogfood never +become external pilot proof. + +## Contract Boundary + +Provider-neutral workflow schemas describe tasks, policy, evidence, and ownership +semantics. Provider-specific identities stay outside the workflow schema in +integration bindings and observations. New host fields remain unknown until a +documented, version-tested boundary exposes them. + +CLI, MCP, plugin skills, hooks, reports, and future supported clients use shared +Core services. None may duplicate or weaken scope, budget, verification, reviewer, +or finalization logic. + +## Local State + +```text +.cewp/workflow-runs/ canonical workflow runtime and evidence +.cewp/supervised-runs/ compatibility supervised runtime +.cewp/pilots/ canonical private pilot records +.cewp/pilot-exports/ explicit redacted pilot projections +../.cewp-worktrees/ managed isolated task worktrees +``` + +Runtime state is ignored and excluded from the npm package. Public schemas, +documentation, fixtures, and migration code remain tracked and reviewable. diff --git a/docs/case-study-template.md b/docs/case-study-template.md new file mode 100644 index 0000000..18ff31e --- /dev/null +++ b/docs/case-study-template.md @@ -0,0 +1,64 @@ +# CEWP Pilot Case Study + +Use this template only for a genuinely independent pilot or label it clearly as +maintainer dogfood. Maintainer dogfood does not count as independent Phase 13 +evidence. Remove secrets, private paths, source code, prompts, and raw logs before +publication. + +## Task shape + +- Participant classification: +- Repository language and size bucket: +- Operating system and test stack: +- Plan input and risk level: +- Why CEWP was chosen instead of native goal mode alone: + +## Plan and checkpoints + +- Bounded goal and exclusions: +- Acceptance criteria defined before execution: +- Checkpoint structure and approved scopes: +- Assurance and test-authoring policy: + +## Elapsed time and CEWP overhead + +- Native setup and elapsed time, when comparable: +- CEWP setup and elapsed time: +- CEWP-controlled operations and local verification time: +- Was the overhead justified? Why? + +## Usage truth and estimate confidence + +- Observed categories and source: +- Estimated range, estimator version, sample basis, and confidence: +- Budgeted limits and protected allocations: +- Unknown fields and why they are unavailable: +- Interval coverage, error, and drift state when enough comparable samples exist: + +Unknown is never zero. Do not assign ChatGPT subscription work a per-run currency +amount without a supported billing mapping. + +## Interventions and recovery + +- Pause/revise/resume or failure/retry scenario: +- Evidence retained across recovery: +- Budget or host-limit behavior: +- Operator decision and next safe action: + +## Failures caught and reviewer findings + +- Verification or scope failure caught outside the model: +- Independent reviewer finding that changed the result: +- Guardrail bypasses discovered or remaining: + +## Redacted receipt excerpt + +Include only the minimum excerpt from an explicit redacted export. State the +redaction policy and keep the canonical local receipt separate. + +## Limitations + +- Unavailable evidence: +- Sources of bias or non-equivalence: +- What this case study does not prove: +- Whether CEWP showed no measurable advantage for this task: 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/contract-extension-example.md b/docs/contract-extension-example.md new file mode 100644 index 0000000..46316e8 --- /dev/null +++ b/docs/contract-extension-example.md @@ -0,0 +1,51 @@ +# Contract Extension Example + +Use this checklist when extending a CEWP JSON contract. The example is +provider-neutral and keeps host identities in an integration record. + +## Start With Observable Behavior + +Add one focused contract test through a public Core function or CLI command. The +test should fail for the missing behavior and state the truth boundary it proves. + +```json +{ + "schemaVersion": "example-evidence/v1", + "evidenceId": "example-1", + "availability": "unknown", + "value": null, + "reason": "the supported host boundary did not expose this field" +} +``` + +Do not add provider thread, account, model, or billing identifiers to a workflow +definition merely because one adapter exposes them. Store those references in a +versioned integration binding or observation. + +## Validate And Fail Closed + +- Use an explicit `schemaVersion` and bounded enums. +- Reject missing required fields, unsupported versions, unsafe paths, and + contradictory claims. +- Preserve unknown instead of supplying a structural zero or guessed value. +- Keep imported observations separate from preventive enforcement. +- Require the same Core gates from CLI and MCP callers. + +Malformed new data must fail closed. Historical data that can be read safely may +produce a partial projection with warnings; it must not be silently promoted. + +## Compatibility And Migration + +Define read compatibility before changing a writer. Preserve the prior schema in +fixtures, preview migrations, back up mutable state, and require explicit apply. +Never rewrite append-only historical evidence merely to make it look current. + +Document the stability level, deprecation path, and downgrade warning. Add the +focused contract test to an independently runnable package script and to the +appropriate phase suite. + +## Verification + +Run the focused test first, then syntax, full contracts, smoke, package dry-run, +privacy tracking scan, and platform gates proportional to the change. Explain +exactly which requirement each result proves. diff --git a/docs/contracts.md b/docs/contracts.md new file mode 100644 index 0000000..932db66 --- /dev/null +++ b/docs/contracts.md @@ -0,0 +1,12 @@ +# Stable Contract Reference + +The machine-readable index is `cewp compatibility --json`. Core owns workflow +definitions, run state, ownership, budgets, scope, verification, review, receipts, +events, and finalization. CLI, MCP, hooks, plugin conversation output, and reports are +projections over those services and cannot bypass their gates. + +Stable candidate schemas include `workflow-definition/v1`, `run-state/v2`, +`task-result/v1`, `event/v1`, and `evidence-receipt/v1`. Pilot schemas remain honest +evidence contracts but cannot make external observations exist. See +`migration-policy.md` for read compatibility and deprecation guarantees and +`external-integration-boundary.md` for MCP/host boundaries. diff --git a/docs/evidence-receipts.md b/docs/evidence-receipts.md new file mode 100644 index 0000000..2e36e9e --- /dev/null +++ b/docs/evidence-receipts.md @@ -0,0 +1,106 @@ +# Evidence Receipts + +`cewp workflow receipt ` writes `evidence-receipt.json` and `evidence-receipt.md` under the local +workflow run directory. `--json` wraps the same receipt and paths in `operator-json/v1` for external tools. +Receipt generation does not execute an agent, verification command, or control process. + +`evidence-receipt/v1` is a normalized read model over the approved workflow definition, canonical run +state, checkpoints, validated task results, interventions, events, independent reviews, integration control +receipt, and referenced evidence files. It includes: + +- goal and source-plan identity; +- workflow digest and revision history; +- operating modes, execution owner, and backend; +- tasks, attempts, changed files, scope verdicts, commands, and verification evidence; +- failure and recovery fields, interventions, review decision, and timestamps; +- observed usage where supported and explicit unknown values otherwise; +- the full approved and consumed budget envelope; +- git base/head identities for new runs; +- warnings and a sorted integrity inventory. + +The JSON model is deterministic for unchanged inputs when `generatedAt` is fixed. The CLI timestamp is the +documented variable. Historical runs that predate a field retain an explicit unknown. Runs that are not +finalized, have malformed event/evidence data, or are missing referenced evidence produce a partial receipt +with warnings rather than a complete claim. + +The Markdown view is generated from that same model and includes tasks, checkpoints, approved commands and +verification, revisions, interventions/recovery, budget and protected reserves, usage provenance/estimate, +control classifications, final review, timestamps, and warnings. It is intended to explain a run without +requiring raw logs; the JSON remains the machine-readable contract. + +## Integrity Boundary + +Integrity entries contain byte length and `sha256` for canonical run evidence, the approved definition, +and referenced evidence files. Source identities include workflow/source hashes when available plus git +base and receipt-time head commits. This is `tamper-evident-local-metadata`, not tamper-proof storage: an +attacker who can rewrite both evidence and metadata can replace both. Durable signing or remote attestation +is not implied. + +## Privacy Boundary + +Raw prompts, adapter output, transcripts, and raw log contents are excluded by default. The receipt may +contain repository-relative paths, changed filenames, approved commands, bounded failure/review summaries, +artifact names, goal/source metadata, host goal identity, revisions, events, timestamps, and reviewer text. +Inspect these fields before sharing. + +`cewp workflow export ` writes separate `.redacted.json`, `.redacted.md`, and `.redacted.html` +artifacts. It removes recognized credential assignments, authorization values, provider-token shapes, +private-key blocks, URL credentials, sensitive/absolute/traversal paths, and active-content markup. It never +overwrites or implicitly creates the canonical receipt. The export records `redaction-policy/v1`, its +replacement count/classes, and that canonical local evidence is still required for integrity verification. +Pattern redaction reduces accidental disclosure but is not a proof that arbitrary prose contains no secret; +inspect exported metadata before sending it outside the repository boundary. + +## Event Ledger And Run Health + +New workflow lifecycle records use `event/v1`, with a closed type-to-category vocabulary covering run, +revision, task, checkpoint, dispatch, intervention, verification, usage, estimates, budgets, warnings, +pauses, scope, review, cancellation, and finalization. Existing `workflow-event/v1` records are accepted as +read-only legacy input and normalized in receipts; CEWP does not rewrite historical ledgers implicitly. +Core workflow transitions emit distinct budget approval, checkpoint, dispatch, scope, verification, usage, +allocation consumption, threshold/warning, pause, cancellation, review, and finalization records where the +corresponding action occurs. + +`cewp run verify ` checks canonical state/definition consistency, event syntax and schemas, +required result/checkpoint artifacts, bound-worktree liveness, and every receipt integrity hash. It executes +no agent and no approved verification command. A failed check returns a nonzero exit code while `--json` +still emits the complete `run-verification/v1` diagnostic model. + +## Offline Operator Report + +`cewp workflow report ` writes `operator-report.json` and a standalone `operator-report.html` in +the workflow run directory. Both are derived from the same normalized receipt model. The HTML uses no +JavaScript, remote fonts, external stylesheets, network requests, server, or control process, so it can be +opened directly from disk on Windows or Linux. + +The report separates observed, estimated, budgeted, and unknown values; shows task progress, revisions, +checkpoint verification, interventions and recovery state, protected reserves, preventive versus observed +controls, and final review. Repository metadata is HTML-escaped, and raw prompts/logs remain excluded. + +Task receipts retain the failed checkpoint/classification, blocker, failure history, state history, and the +operator intervention/reason that reopened work. Budget-paused receipts remain partial and separately prove +absolute-ceiling and protected-allocation compliance; a pause is never rendered as completion. + +## Run Comparison + +`cewp workflow compare ` derives `run-comparison/v1` from two receipts. It +compares outcome, bounded duration, execution owner/backend, observed usage categories, estimates and API +cost when valid, attempts, interventions, failures, scope, commands, and verification evidence. Model time, +CEWP overhead, estimate accuracy, billing cost, or usage that was not observed remains `unknown` and is +excluded from numeric deltas. + +A native-owned workflow is labeled as a native-goal baseline only when a validated host binding includes a +native goal reference. Native ownership alone is insufficient, and unavailable native usage is never zero. + +## Usage And Estimate Truth + +Each task-result, review-result, and supported host usage record becomes a separate +`usage-observation/v1`. The receipt retains its normalized category, raw category name, observed/imported/ +unknown availability, source schema, authentication boundary, timestamp, scope, and effective model only +when known. Bounded raw host payloads are not copied into the receipt. Imported observations stay imported, +do not enter observed totals, and never imply billing impact. + +`usage-estimate/v1` records estimator version/method, grouping dimensions, local sample basis, calibration +snapshot, and drift state. With fewer than five comparable runs—or without known model/effort—it remains an +unknown range with unavailable confidence. CEWP does not promote a numeric estimate from fixtures or +non-comparable history. 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..82ff934 100644 --- a/docs/known-limitations.md +++ b/docs/known-limitations.md @@ -2,7 +2,7 @@ CEWP is beta software. These limits are product boundaries, not hidden roadmap promises. -- The supervised golden path supports one active checkpoint per run and manual linear continuation from a verified snapshot. A general multi-checkpoint graph, dependency scheduler, automatic plan compiler, and plan migration engine are not shipped. +- The managed supervised convenience path supports one active checkpoint at a time. The workflow runtime supports validated DAGs, scheduling, revisions, and migrations, but its compiler emits a source-bound agent request rather than invoking a model or making arbitrary prose executable. - Managed supervised execution uses exactly one owner/backend pair: `managed` with `codex-exec`. - CEWP does not attach to a ChatGPT desktop task's private thread or native goal lifecycle. - CEWP does not inject persistent desktop UI, a title-bar meter, or arbitrary notifications. Structured conversation and CLI output are the minimum warning surface. @@ -10,11 +10,13 @@ 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. - CEWP never automatically merges, pushes, publishes, tags, or creates a release. -- Beta schemas and command details can change with migration notes before 1.0. +- The stable-core candidate publishes explicit schema and migration contracts, but the package remains beta and 1.0 eligibility is blocked by real Phase 13 evidence and exact release matrices. +- Phase 13 pilot infrastructure validates local records and thresholds but does not supply real participants. Maintainer dogfood and deterministic fixtures remain excluded; the external-user, repository, repeat-use, case-study, feedback, and contribution gates are still open. Report setup failures, workflow failures, or misleading evidence through the repository issue tracker. Include `cewp doctor --json` output after removing local paths or sensitive values. diff --git a/docs/manual-acceptance.md b/docs/manual-acceptance.md new file mode 100644 index 0000000..d6467b9 --- /dev/null +++ b/docs/manual-acceptance.md @@ -0,0 +1,220 @@ +# CEWP Manuel Kabul ve Deneme Rehberi + +Bu rehber, CEWP'yi bakım sorumlusu veya pilot katılımcısı olarak güvenli biçimde +denemen içindir. Testleri yeni bir dalda, geçici bir klonda veya gözden çıkarılabilir +bir depoda yap. Komutları onaylamadan önce oku; prompt ve test dosyalarına gizli bilgi +koyma. + +Kendi yaptığın testler `maintainer-dogfood` olarak kaydedilmelidir. Bunlar değerli +teknik kanıttır fakat bağımsız Phase 13 kullanıcı doğrulaması sayılmaz. + +## Şimdi ne yapmalısın? + +1. Önce aşağıdaki **Kimlik bilgisi gerektirmeyen ön kontrol** bölümünü uygula. +2. Sonra deneme için küçük ve geçici bir Git deposu hazırla. +3. Bu depoda yalnız `README.md` dosyasını değiştiren tek bir denetimli checkpoint çalıştır. +4. Duraklatma, devam ettirme, doğrulama, inceleme ve makbuz adımlarını sırayla dene. +5. Sonucu `maintainer-dogfood` olarak kaydet; dış kullanıcı sonucu gibi gösterme. +6. Hata görürsen komutu, temizlenmiş hata mesajını, beklenen sonucu ve gerçek sonucu not et. + +İlk turda gerçek bir projeni kullanma. Önce geçici depoda akışın tamamını gör. + +## Kimlik bilgisi gerektirmeyen ön kontrol + +CEWP kaynak deposunda PowerShell aç ve kaynak komutunun yolunu tanımla: + +```powershell +$cewpRepo = "C:\Users\tunca\Desktop\Projeler Ve Planlar Güncel\CEWP Phase 8" +$cewp = Join-Path $cewpRepo "bin\cewp.js" +Set-Location $cewpRepo +node $cewp --help +node $cewp doctor --json +node $cewp compatibility --json +npm run test:clean-install +npm run test:plugin-lifecycle +``` + +Beklenen sonuç: + +- Yardım metni görüntülenir. +- Doctor sonucu anlaşılır ve uygulanabilir olur. +- Uyumluluk sonucu, gerçek pilot kanıtları olmadığı için `blocked-pilot-evidence` gösterir. +- Temiz kurulum, kimlik bilgisi gerektirmeyen demo ve kaldırma testi geçer. +- İzole plugin testi kurulum, devre dışı bırakma, yükseltme ve kaldırma adımlarını geçer. +- Testler yeni bir Codex kimlik doğrulama dosyası oluşturmaz. + +## Geçici deneme deposu hazırlama + +PowerShell'de CEWP deposunun dışında geçici bir klasör oluştur: + +```powershell +$deneme = Join-Path $env:TEMP "cewp-manuel-deneme" +New-Item -ItemType Directory -Force -Path $deneme | Out-Null +Set-Location $deneme +git init +git config user.email "cewp-deneme@example.local" +git config user.name "CEWP Manuel Deneme" +"# CEWP Manuel Deneme" | Set-Content README.md +git add README.md +git commit -m "Başlangıç" +``` + +Bu depo yalnız deneme içindir. İçinde gerçek kaynak kodu, token, parola veya özel belge +bulundurma. + +## Denetimli checkpoint + +Aşağıdaki yol gerçek yönetilen Codex yürütmesini kullanır. Hedefi küçük tut: + +```powershell +node $cewp supervise plan --goal "README.md dosyasına Manuel kabul notu başlıklı tek bir bölüm ekle" --scope README.md --verify "git diff --check" --stop "Not eklenmiş ve git diff --check geçmiş olmalı" --json +node $cewp supervise approve --yes --json +``` + +`` yerine ilk komutun döndürdüğü çalışma kimliğini yaz. +Bu aşamada henüz `execute` çalıştırma; önce aşağıdaki duraklatma ve revizyon +kontrolünü tamamla. + +Beklenen sonuç: + +- Öneri çalıştırmadan önce açıkça gösterilir. +- Yürütme için ayrıca onay gerekir. +- Yalnızca izin verilen `README.md` değişir. +- Codex host tarafında tamamlandı görünmesi tek başına CEWP doğrulama PASS'i oluşturmaz. +- Yanlış depo veya worktree görünürse yürütmeyi onaylama. + +## Duraklatma, revizyon ve devam ettirme + +Finalization öncesinde kontrollü duraklatmayı ve revizyonu dene: + +```powershell +node $cewp supervise pause --reason budget-safe --yes --json +node $cewp supervise status --json +node $cewp supervise resume --yes --json +node $cewp supervise revise --goal "Aynı sınırlı README notunu koru ve anlatımını netleştir" --json +node $cewp supervise approve --yes --json +``` + +Beklenen sonuç: + +- Duraklatma gerçeğe uygun ve devam ettirilebilir bir durum üretir. +- Önceden tamamlanan kanıt kaybolmaz. +- Devam ettirme, çalışmayı doğru önceki kapıya döndürür. +- Revizyon kapsamı sessizce değiştirmez; yeniden inceleme gerektirir. + +## Yürütme ve doğrulama + +Revize edilen planı yeniden onayladıktan sonra: + +```powershell +node $cewp supervise execute --yes --json +node $cewp supervise verify --json +``` + +Beklenen sonuç: + +- Yalnızca izin verilen `README.md` değişir. +- Kapsam dışı değişiklik güvenli biçimde reddedilir. +- Repository doğrulama komutu ayrı çalışır ve sonucu kaydedilir. +- Host tarafındaki tamamlanma tek başına doğrulama PASS'i değildir. + +## Bağımsız inceleme ve makbuz + +Yürütme ve doğrulama tamamlandıktan sonra: + +```powershell +node $cewp supervise review --yes --json +node $cewp supervise receipt --json +node $cewp supervise finalize --yes --json +``` + +Beklenen sonuç: + +- Doğrulama veya reviewer PASS yoksa finalize reddedilir. +- JSON ve Markdown makbuzu; owner/backend, kapsam, doğrulama, inceleme, kullanım + kaynağı, bilinmeyen host kullanımı ve bütünlük bilgisini gösterir. +- Ham prompt, token veya gizli dosya içeriği makbuza kopyalanmaz. + +## Sahiplik çakışması + +CEWP geliştirme deposunda: + +```powershell +Set-Location $cewpRepo +npm run test:ownership-gates +npm run test:integration-binding +``` + +Beklenen sonuç: + +- `managed` ve `native` sahiplik aynı görev worktree'sini hedefleyemez. +- Güvensiz iç içe dispatch güvenli biçimde reddedilir. +- Bırakılan veya terk edilen sahiplik açıkça kaydedilir. + +Bunu iki gerçek ajanı aynı worktree üzerinde başlatarak deneme. Deterministik testleri kullan. + +## Hata ve kurtarma + +CEWP geliştirme deposunda: + +```powershell +Set-Location $cewpRepo +npm run test:supervised-failure +npm run test:supervised-controls +npm run test:workflow-failure-matrix +npm run test:workflow-lifecycle +``` + +Beklenen sonuç: + +- Tekrarlanan aynı hata imzası sınırsız döngü başlatmaz. +- Operasyon bütçesi veya host limiti, devam ettirilebilir ve dürüst durum üretir. +- Reviewer için korunan bütçe başka işler tarafından tüketilmez. +- Kısmi kanıt kurtarma sırasında korunur. +- Hiçbir hata yolu sahte reviewer PASS üretmez. + +## Pilot kanıtı + +Kendi testini dürüstçe bakım sorumlusu denemesi olarak kaydet: + +```powershell +Set-Location $deneme +node $cewp pilot create --pilot-id maintainer-manual-1 --participant maintainer-dogfood --participant-id maintainer-1 --json +node $cewp pilot status --json +node $cewp pilot export maintainer-manual-1 --json +``` + +Beklenen sonuç: + +- Pilot durumu tamamlanmamış kalır. +- Bakım sorumlusu kaydı bağımsız kullanıcı sayımlarına dahil edilmez. +- Asıl kayıt ignored `.cewp/pilots/` altında kalır. +- Export ayrı ve redakte edilmiş bir projeksiyondur. + +Gelecekteki dış katılımcı kendi gerçek repository sonucunu onaylamalı ve kişisel bilgi +içermeyen bağımsız bir katılımcı kimliği kullanmalıdır. + +## Ne iddia edilmemeli? + +- Geçen fixture testi, gerçek dış kullanıcı veya case study değildir. +- Kendi testin bağımsız kullanıcı testi değildir. +- Native host tamamlanması CEWP doğrulaması veya reviewer PASS değildir. +- Eksik, eski veya bozuk host kullanım bilgisi sıfır değildir. +- Windows testi tek başına Linux release matrisini kanıtlamaz. +- Artifact hazırlamak publish, tag, push veya GitHub release yapmak değildir. +- Gerçek Phase 13 kapıları geçmeden `1.0.0` hazır veya tamamlandı denmemelidir. + +## Test sonunda kaydetmen gerekenler + +- CEWP sürümü +- İşletim sistemi +- Node, Git ve Codex sürümleri +- Çalıştırılan komut +- Beklenen davranış +- Gerçek davranış +- Duraklatma veya kurtarma sonucu +- Reviewer kararı +- Son makbuzun gizli bilgi içermeyen özeti + +Herkese açık rapora token, auth dosyası, ham özel prompt, kaynak kodu veya mutlak özel +repository yolu koyma. diff --git a/docs/migration-policy.md b/docs/migration-policy.md new file mode 100644 index 0000000..bb1f1e7 --- /dev/null +++ b/docs/migration-policy.md @@ -0,0 +1,17 @@ +# Migration And Deprecation Policy + +Stable writers emit the documented current schema. Readers reject unknown future +schemas with an actionable compatibility error. `supervised-run/v1` remains readable +through a read-only projection to `run-state/v2`; execution requires an explicit +previewed migration with digest confirmation and a backup. + +Stable 1.x commands and schemas receive a documented deprecation notice for at least +one minor release before removal, unless retaining them would create a P0/P1 security +or data-loss risk. Beta run formats listed in the compatibility contract remain +readable throughout 1.x where a safe projection exists. Migration never silently +rewrites canonical evidence. + +An upgrade must preserve supported records. A downgrade warning is mandatory when a +newer writer or schema cannot be safely read by the requested older version; CEWP +must stop instead of modifying that state. Uninstall removes package/plugin files but +does not silently delete `.cewp` evidence or worktrees. diff --git a/docs/native-goal-or-cewp.md b/docs/native-goal-or-cewp.md new file mode 100644 index 0000000..2cd971b --- /dev/null +++ b/docs/native-goal-or-cewp.md @@ -0,0 +1,11 @@ +# Native Goal Or CEWP? + +Use native Codex goal mode for a clear, low-risk, one-repository task where failure is +cheap and independent evidence is unnecessary. Use CEWP when bounded scope, +checkpoint approval, resumable recovery, deterministic verification, independent +review, or a tamper-evident receipt justifies the extra operations. + +In one sentence: CEWP adds local-first supervision, recovery, independent verification, +and reviewable evidence around long-running Codex work without pretending to control +private host state. A finalized `evidence-receipt/v1` proves that value; native host +completion alone does not. diff --git a/docs/performance-budgets.md b/docs/performance-budgets.md new file mode 100644 index 0000000..71173a3 --- /dev/null +++ b/docs/performance-budgets.md @@ -0,0 +1,21 @@ +# CEWP-Controlled Performance Budgets + +These budgets exclude model execution, network latency, repository test commands, and +host queue time. They are regression ceilings for CEWP-owned processing on a supported +local machine, not user-facing speed guarantees. + +| Operation | Candidate ceiling | +| --- | ---: | +| Doctor JSON | 2,000 ms | +| Workflow plan validation | 1,000 ms | +| Run status/progress rendering | 1,000 ms | +| Checkpoint verification bookkeeping, excluding the command | 1,000 ms | +| Local package install/uninstall fixture | 30,000 ms | +| Large-run receipt/inspection fixture | 3,000 ms | + +Release evidence records OS, Node, Git, package version, fixture size, repetitions, +median, upper quantile, and maximum. Model-operation counts, available usage +categories, estimate error, repair operations, verification runs, and log volume are +reported separately in receipts/pilot reports. Estimator confidence requires the +minimum sample and current drift checks; pricing snapshots are dated inputs and are +never inferred from unavailable host usage. diff --git a/docs/pilot-kit.md b/docs/pilot-kit.md index 803187b..dd4a07a 100644 --- a/docs/pilot-kit.md +++ b/docs/pilot-kit.md @@ -2,6 +2,39 @@ This kit collects real adoption evidence without mandatory telemetry. Do not mark a pilot complete unless a person outside the maintainer's normal environment performs the steps and confirms the result. +## Local Pilot Ledger + +Canonical pilot records stay under the ignored local runtime path +`.cewp/pilots/`. They are never packaged or committed. Create a privacy-safe +record, then add one validated observation at a time: + +```bash +cewp pilot create --pilot-id --participant --participant-id +cewp pilot record --from --yes +cewp pilot status --json +cewp pilot export [] --json +``` + +`cewp pilot status` exits nonzero until every Phase 13 gate has qualifying +evidence. `cewp pilot export` writes a separate redacted JSON/Markdown projection; +it does not change the canonical record, transmit data, or prove arbitrary prose +contains no secret. + +A `repository-attempt` observation must use two different privacy-safe identifiers: +`attempt.id` identifies that run, while `attempt.repositoryId` is a stable pseudonym +for the repository. Status counts distinct `repositoryId` values, so repeat attempts +against one repository cannot satisfy the ten-repository gate. Repeat-user evidence +is likewise counted once per privacy-safe participant id. Malformed or incompatible +local records remain visible as invalid warnings and keep status incomplete. + +Maintainer runs, fixtures, demos, and multiple machines used by one person remain +visible but excluded from independent counts. Real pilot completion requires ten +independent repositories, at least three independent participants, five full +reviewed runs, three repeat users without maintainer assistance, three comparable +native-goal studies, three recovery scenarios, budget and host-limit evidence, +three public case studies, onboarding remediation, and external contributor +evidence. The CLI infrastructure cannot manufacture any of those results. + ## Pilot Target Choose one bounded repository task that can be reviewed independently: diff --git a/docs/plans/2026-07-22-phase-13-pilot-evidence-design.md b/docs/plans/2026-07-22-phase-13-pilot-evidence-design.md new file mode 100644 index 0000000..731c6f9 --- /dev/null +++ b/docs/plans/2026-07-22-phase-13-pilot-evidence-design.md @@ -0,0 +1,151 @@ +# Phase 13 Pilot Evidence Design + +Status: approved 2026-07-22 + +## Purpose + +Phase 13 must make real pilot evidence reviewable without inventing external +participants or adding mandatory telemetry. CEWP will provide a local, +machine-validated pilot ledger and privacy-safe export path. Maintainer +dogfooding remains useful engineering evidence but never satisfies an +independent-user gate. + +## Chosen Approach + +Pilot records live under the ignored runtime root: + +```text +.cewp/pilots//record.json +``` + +The alternatives were tracked public records by default and a documentation-only +process. Public-by-default records create unnecessary privacy risk. A +documentation-only process cannot enforce counting rules or prove which Phase 13 +gates remain open. A hosted service was rejected because the product is +local-first and does not require telemetry or an account. + +Only an explicit redacted export may leave the local pilot root. Export never +modifies the canonical record and never treats pattern redaction as proof that +arbitrary prose is secret-free. + +## Contracts + +`pilot-record/v1` is the canonical local record. It contains: + +- a stable pilot id and timestamps; +- participant classification: `maintainer-dogfood` or `independent-external`; +- a privacy-safe participant id supplied by the operator; +- repository attempt metadata using language, size, operating-system, test-stack, + input, risk, and mode buckets rather than an absolute path; +- the bounded task and golden-path outcomes; +- optional CEWP workflow run and receipt-integrity references; +- review, repeat-use, recovery, budget-pause, and controlled-host-limit outcomes; +- native-goal comparison evidence where genuinely comparable; +- onboarding failures, remediation, contributor feedback, and case-study status; +- provenance and warnings for evidence that is missing, malformed, or imported. + +`pilot-status/v1` is a derived read model. It reports every Phase 13 gate +separately, its threshold, qualifying evidence ids, exclusions, and remaining +count. It cannot mutate records. + +`pilot-export/v1` is a separately written redacted projection. It excludes raw +prompts, source code, logs, credentials, absolute paths, authentication material, +and unbounded free-form participant data. + +## CLI Surface + +The public commands are: + +```text +cewp pilot create +cewp pilot record +cewp pilot status +cewp pilot export +``` + +`create` writes one validated local skeleton. `record` applies an explicit, +validated observation without silently changing participant classification. +`status` evaluates the complete roadmap gate set and exits nonzero while any +required gate is unmet or invalid. `export` writes a redacted report only after +validation and discloses its redaction policy and counts. + +The first vertical slice is `create` plus `status`: a maintainer record can be +created and inspected, but every independent-user count remains zero. Later +slices add observation intake, linked receipt validation, recovery/comparison +evidence, and export. + +## Counting And Truth Rules + +- Only `independent-external` participants count toward external-participant, + full-reviewed-run, repeat-user, independent-repository, contribution, and case + study gates where the roadmap requires independent evidence. +- Multiple machines, repositories, or records belonging to the maintainer do not + become independent participants. +- A repository attempt is counted once by its stable attempt id. Duplicate ids, + conflicting participant classifications, or invalid timestamps fail closed. +- A full reviewed run requires linked final evidence and reviewer PASS; a test or + manually checked box alone is insufficient. +- Native-goal comparisons require equivalent task-shape metadata. Unavailable + usage remains unknown and is excluded from numeric deltas. +- Recovery, budget exhaustion, and host-limit scenarios are distinct evidence + categories and cannot substitute for one another. +- No status can report Phase 13 complete unless every roadmap gate has qualifying + evidence and no unresolved guardrail bypass is recorded. + +## Data Flow And Security + +The CLI resolves the repository root, then reads or atomically writes only below +`.cewp/pilots`. Pilot ids and export targets are validated against traversal, +absolute paths, reserved names, and symlink escapes. Canonical records use +bounded enums and structured fields instead of arbitrary prose wherever +possible. + +Run references are read-only. When a local workflow receipt is linked, CEWP +checks its run identity and integrity metadata; it does not copy raw evidence +files into the pilot record. Missing or stale references remain explicit and do +not qualify a gate. + +Export reuses the established redaction boundary, writes repository-relative +artifacts, and requires an operator-selected destination outside the ignored +canonical pilot directory only when the destination is safe. No command sends +data over the network. + +## Public Pilot And Contributor Surface + +Tracked repository assets will include: + +- issue forms for setup failure, workflow failure, feature request, and receipt + quality; +- a public case-study template with limitations and usage-truth fields; +- updated pilot instructions for manual, privacy-safe adoption milestones; +- contribution guidance, a concise architecture map, contract-extension + examples, and a private security-reporting path; +- explicit language that ecosystem listing and 1.0 remain gated on real external + evidence. + +Issue forms and templates collect no secrets, raw logs, repository paths, or +mandatory telemetry. + +## Error Handling + +Malformed JSON, unsupported schema versions, unsafe paths, duplicate identities, +contradictory observations, invalid gate claims, or broken receipt links produce +stable actionable errors. Read-only status remains available where possible and +marks affected evidence invalid rather than discarding it or converting it to +zero. Mutations use atomic writes and preserve the last valid record on failure. + +## Testing + +Development follows vertical TDD through the public CLI and Core modules: + +1. maintainer creation and an honestly incomplete status; +2. independent participant and repository-attempt counting; +3. reviewed run, repeat use, comparison, recovery, budget, host-limit, onboarding, + contribution, guardrail, and estimate-calibration gates; +4. duplicate, malformed, stale, traversal, symlink, and contradictory evidence; +5. redacted export and adversarial secret/path content; +6. package, help, documentation, Windows, and Linux contract coverage. + +Fixtures may prove behavior but never satisfy live pilot gates. The final Phase 13 +technical report will therefore distinguish implemented infrastructure from +external evidence still awaiting real users. diff --git a/docs/plans/2026-07-22-phase-13-pilot-evidence-implementation.md b/docs/plans/2026-07-22-phase-13-pilot-evidence-implementation.md new file mode 100644 index 0000000..e284c74 --- /dev/null +++ b/docs/plans/2026-07-22-phase-13-pilot-evidence-implementation.md @@ -0,0 +1,186 @@ +# Phase 13 Pilot Evidence Implementation Plan + +Status: approved design implementation + +## Constraints + +- Keep canonical pilot data under ignored `.cewp/pilots` runtime state. +- Never count fixtures or `maintainer-dogfood` as independent evidence. +- Do not add telemetry, a hosted service, another provider, or automatic remote + actions. +- Use public CLI behavior for contract tests and Core functions for validation. +- Keep the Phase 13 release gate incomplete until genuine external evidence + proves every threshold. +- Run the private-file tracking scan before every commit. + +## Slice 1: Honest Local Pilot Creation And Status + +Files: + +- `tests/contracts/pilot-record.js` +- `src/pilot/record.js` +- `src/pilot/status.js` +- `src/pilot/cli.js` +- `src/cli/parse.js` +- `src/cli/usage.js` +- `bin/cewp.js` +- `package.json` + +TDD behavior: + +1. `cewp pilot create --participant maintainer-dogfood --participant-id + --json` writes `pilot-record/v1` below `.cewp/pilots`. +2. `cewp pilot status --json` returns `pilot-status/v1`, reports the maintainer + record separately, and leaves every independent-user threshold at zero. +3. Status exits nonzero while Phase 13 gates are unmet but still emits valid JSON. +4. Unsafe ids, missing Git repository roots, duplicate ids, and unsupported + participant classifications fail with actionable errors. + +Focused verification: `npm run test:pilot-record`. + +## Slice 2: Structured Observation Intake And Gate Evaluation + +Files: + +- `tests/contracts/pilot-gates.js` +- `src/pilot/record.js` +- `src/pilot/status.js` +- `src/pilot/cli.js` +- `src/cli/parse.js` +- `package.json` + +TDD behavior: + +1. `cewp pilot record --from --yes --json` + atomically appends a validated observation. +2. Attempts, full reviewed runs, repeat use, native comparisons, recovery, + operational-budget exhaustion, host limits, onboarding remediation, + contribution, guardrail, case-study, and calibration evidence are counted only + when their required structured facts exist. +3. Duplicate attempt ids, participant reclassification, contradictory outcomes, + invalid timestamps, and impossible PASS claims fail closed. +4. Every roadmap gate exposes threshold, qualifying evidence, excluded evidence, + status, and remaining count. + +Focused verification: `npm run test:pilot-gates`. + +## Slice 3: Workflow Receipt Linking + +Files: + +- `tests/contracts/pilot-receipt-link.js` +- `src/pilot/evidence.js` +- `src/pilot/record.js` +- `src/pilot/status.js` +- `package.json` + +TDD behavior: + +1. Repository attempts may link a local workflow run without copying raw evidence. +2. A full reviewed run qualifies only when the run identity, final state, receipt + inventory, required verification, and reviewer PASS agree. +3. Missing, stale, partial, malformed, or integrity-failing receipt links are + warnings and never qualify a gate. +4. Native-only evidence remains explicitly imported and unknown usage never + becomes zero. + +Focused verification: `npm run test:pilot-receipt-link`. + +## Slice 4: Redacted Pilot Export + +Files: + +- `tests/contracts/pilot-export.js` +- `src/pilot/export.js` +- `src/pilot/cli.js` +- `src/cli/parse.js` +- `package.json` + +TDD behavior: + +1. `cewp pilot export [] --json` writes a separate + `pilot-export/v1` JSON/Markdown projection. +2. Canonical records are not modified and export is never implicit. +3. Absolute paths, traversal, secrets, credentials, raw prompts/logs/source, and + active markup are excluded or redacted with class/count disclosure. +4. Unsafe output targets and symlink escapes fail closed. + +Focused verification: `npm run test:pilot-export`. + +## Slice 5: Public Pilot Feedback And Case-Study Surface + +Files: + +- `.github/ISSUE_TEMPLATE/setup-failure.yml` +- `.github/ISSUE_TEMPLATE/workflow-failure.yml` +- `.github/ISSUE_TEMPLATE/feature-request.yml` +- `.github/ISSUE_TEMPLATE/receipt-quality.yml` +- `.github/ISSUE_TEMPLATE/config.yml` +- `docs/case-study-template.md` +- `docs/pilot-kit.md` +- `tests/contracts/pilot-public-surface.js` +- `package.json` + +TDD behavior: + +1. Four issue forms request sanitized reproducible evidence without secrets or + mandatory telemetry. +2. The case-study template covers task shape, plan quality, checkpoints, overhead, + truth labels, estimates, interventions, failures, receipt excerpt, and limits. +3. Pilot documentation explains local records, redacted export, honest dogfood, + and every external gate. + +Focused verification: `npm run test:pilot-public-surface`. + +## Slice 6: Contributor And Security Surface + +Files: + +- `CONTRIBUTING.md` +- `SECURITY.md` +- `docs/architecture.md` +- `docs/contract-extension-example.md` +- `tests/contracts/contributor-surface.js` +- `README.md` +- `package.json` + +TDD behavior: + +1. Contributors have local setup, focused tests, architecture boundaries, + contract-extension examples, issue selection guidance, and privacy rules. +2. Security reports use a private path and public issues explicitly reject + secrets and unredacted vulnerabilities. +3. Architecture retains Core authority, one execution owner/backend, and + provider-neutral contracts. + +Focused verification: `npm run test:contributor-surface`. + +## Slice 7: Phase 13 Release Surface + +Files: + +- `tests/contracts/pilot-release.js` +- `docs/release-notes.md` +- `docs/known-limitations.md` +- `README.md` +- `package.json` +- `plugins/cewp/.codex-plugin/plugin.json` + +TDD behavior: + +1. Prepare `0.13.0-beta.0` locally without publish, tag, or release. +2. Package the reviewed public pilot/contributor files but never `.cewp/pilots`, + private roadmap files, or `.cewp-private`. +3. Release notes distinguish infrastructure readiness, maintainer dogfood, and + missing external evidence. +4. Ecosystem submission remains unperformed until real case studies and golden + path evidence exist. + +Focused verification: `npm run test:pilot-release` and package dry runs. + +## Batch And Commit Boundaries + +Implement and commit in reviewable slices: local ledger/status; observations and +receipt links; redacted export; public pilot/contributor surface; Phase 13 release +preparation. Run focused tests during each red-green-refactor loop, then the full +baseline and privacy scan at the phase checkpoint. diff --git a/docs/recovery-guide.md b/docs/recovery-guide.md new file mode 100644 index 0000000..8ffd795 --- /dev/null +++ b/docs/recovery-guide.md @@ -0,0 +1,23 @@ +# Troubleshooting And Recovery + +Preserve `.cewp` evidence and inspect status before changing state. Do not delete a +worktree or edit canonical JSON to force progress. + +| Condition | Safe response | +| --- | --- | +| Interrupted native goal | Import only supported evidence, keep usage unknown where absent, then resume from the CEWP gate. | +| Failed cancellation or detached host session | Treat ownership as active/unknown, inspect the process and worktree, then explicitly retry, rollback, or abandon. | +| Plan revision | Preview and approve a source-bound revision; completed evidence remains immutable. | +| Deleted worktree | Stop execution, recreate only through the owner-aware worktree path, and re-verify. | +| Ownership conflict or unsafe nested dispatch | Do not dispatch; release, rollback, or abandon the existing owner first. | +| Missing warning surface or outdated plugin | Use CLI JSON/Core status, verify versions, then reinstall the plugin in its documented host. | +| Corrupted events | Run `cewp run verify`; retain the corrupt artifact and repair from a verified backup or explicit migration. | +| Operational/repair budget exhausted | Add budget only with approval or pause; protected reviewer/finalization allocation is not borrowed. | +| Host limit depleted | Record `paused-host-limit`, wait for supported recovery, and never claim PASS. | +| Repeated failure signature | Keep the task blocked and choose revise, bounded retry, rollback, or abandon. | +| Invalid tests or pre-existing failures | Classify them explicitly; never weaken a non-waivable verification gate. | +| Codex execution unavailable | Use documented native intake/manual evidence or restore `codex-exec`; do not substitute another provider. | +| Partial review | Preserve the partial receipt, obtain independent reviewer PASS, then finalize. | + +Failed recovery should end in a truthful blocked or resumable state, not fabricated +completion. Back up local evidence before any manual filesystem repair. diff --git a/docs/release-notes.md b/docs/release-notes.md index 20e756a..e1c68f9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,7 +2,143 @@ ## Unreleased -No changes yet. +### Added + +- Added a maintainer-safe manual acceptance guide covering credential-free preflight, + one bounded real supervised checkpoint, pause/revise/resume, review and receipt, + ownership conflicts, recovery fixtures, and honest pilot classification. + +## 0.14.0-beta.0 + +### Summary + +Stable-core candidate contracts, recovery/security guidance, CEWP-owned performance +budgets, package lifecycle coverage, and local release-artifact preparation. This +version is prepared locally and is not published, tagged, pushed, or released. +It is deliberately not `1.0.0`: real Phase 13 evidence and exact clean release +matrices remain mandatory blockers. + +### Added + +- Added `cewp compatibility --json` with `stable-compatibility/v1` covering Node, + OS, Git, Codex, package/plugin, host claims, modes, owners, backend, and schemas. +- Added explicit downgrade detection plus migration/deprecation guarantees for + supported beta state and stable candidate schemas. +- Added the 1.0 security re-audit record, failure-recovery guide, contract index, + native-goal decision guide, and CEWP-controlled performance budgets. +- Added clean package install/upgrade/downgrade-warning/uninstall capability evidence. +- Added local validation and artifact preparation with a hashed manifest; remote + publication, tag, push, and GitHub release actions remain human-only and absent. + +### Validation truth + +- Windows local contracts can prove the candidate implementation, but do not replace + a clean Linux run of this exact source or the repository release matrix. +- No external participant, repository, repeat-use, case-study, feedback, or + contribution evidence was fabricated. Phase 13 and the 1.0 gate remain open. +- The security review states the local technical finding boundary; it cannot prove + that no undisclosed external vulnerability exists. + +## 0.13.0-beta.0 + +### Summary + +Local, privacy-safe pilot evidence infrastructure and public feedback/contributor +surfaces. This version is prepared locally and is not published, tagged, or +released. Real external-user evidence is absent, so the Phase 13 exit gate remains +open and 1.0 is not validated. Exact clean Linux validation of the current source +also remains required. + +### Added + +- Added canonical `pilot-record/v1` records under ignored `.cewp/pilots/` state, + with explicit `maintainer-dogfood` and `independent-external` classification. +- Added `cewp pilot create`, `record`, `status`, and `export` with structured + observations, complete Phase 13 threshold reporting, atomic writes, duplicate + evidence refusal, and nonzero incomplete status. +- Added distinct privacy-safe repository and attempt identities, participant-deduped + repeat-use counts, and fail-safe status warnings for malformed local records. +- Added workflow receipt linking so a full reviewed run qualifies only with a + finalized run, complete receipt, passing integrity verification, and independent + reviewer PASS. +- Added separate `pilot-export/v1` JSON/Markdown artifacts with adversarial + redaction, canonical-record preservation, and symlink containment. +- Added four privacy-aware GitHub issue forms, a case-study template, contributor + guidance, architecture map, contract-extension example, and private security + reporting path. + +### Validation truth + +- Deterministic fixtures prove the pilot contracts but do not count as pilot + participants, repository attempts, repeat users, reviewed runs, comparisons, or + case studies. +- Maintainer dogfood is retained and explicitly excluded from independent counts. +- Real external participant, repository, repeat-use, case-study, feedback, and + contribution evidence is absent; it must be collected later from actual users. +- Ecosystem listing was not submitted because the golden-path evidence and public + case studies do not yet exist. +- No publish, tag, release, push, PR, merge, telemetry, provider, desktop UI, or + terminal-server action was added. + +## 0.12.0-beta.0 + +### Summary + +Portable, deterministic workflow evidence and offline operator reporting. This version is prepared locally +and is not published, tagged, or released. Exact clean Linux validation remains required before the Phase 12 +technical gate can close; Windows validation and cross-platform-safe artifact contracts do not substitute +for that run. Numeric usage confidence also remains unavailable until real comparable local samples meet +the documented calibration and drift thresholds. + +### Added + +- Added deterministic `evidence-receipt/v1` JSON/Markdown generation for workflow runs with complete-versus-partial truth, task/checkpoint/review evidence, usage unknowns, budget compliance, git identities, and local SHA-256 integrity metadata. +- Added `cewp workflow receipt ` without executing agents or verification commands. Raw prompts, transcripts, adapter output, and raw log contents remain excluded by default. +- Added the closed `event/v1` lifecycle vocabulary with read-only normalization of historical `workflow-event/v1` records. +- Added read-only `cewp run verify` checks for workflow state, schemas, events, required artifacts, worktree liveness, and receipt integrity. +- Added portable `operator-report/v1` JSON and standalone offline HTML generation from the normalized receipt model. +- Added `run-comparison/v1` and `cewp workflow compare` with explicit unknowns and evidence-backed native-goal baseline labeling. +- Added adversarially tested `redaction-policy/v1` exports that preserve canonical local evidence and avoid absolute-path output. +- Added `usage-observation/v1` provenance/raw-category records and reproducible unknown `usage-estimate/v1` calibration metadata. +- Expanded lifecycle evidence and recovery receipts for failed checkpoints, budget pauses, host limits, cancellations, and audit-only controls. +- Expanded the Markdown receipt so complete and partial runs can be understood without opening raw logs. + +### Truth boundaries + +- `usage-observation/v1` keeps source schemas, authentication boundaries, raw category names, and observed/imported/unknown states distinct. +- `usage-estimate/v1` keeps its estimator, sample basis, calibration snapshot, and drift state; insufficient evidence produces no numeric range. +- `run-comparison/v1` compares only equivalent observed dimensions. In particular, unavailable native usage remains unknown rather than zero. +- Audit-only controls remain observed-not-enforced and never appear as preventive enforcement. +- API-equivalent currency cost remains unknown without a supported dated model/pricing mapping. +- No publish, tag, or release action was performed. + +## 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 diff --git a/docs/security-review-1.0.md b/docs/security-review-1.0.md new file mode 100644 index 0000000..ac84778 --- /dev/null +++ b/docs/security-review-1.0.md @@ -0,0 +1,20 @@ +# 1.0 Security Review + +Status: technical review complete for the candidate surface; 1.0 remains blocked by +Phase 13 and exact release matrices. The release gate is zero open P0/P1 security or +data-loss issues. This document does not assert the state of unreported external bugs. + +Reviewed boundaries: + +- path containment rejects traversal and validates export/worktree roots; +- command construction uses argv arrays and explicit execution ownership; +- hook trust is opt-in, version-bound, reviewable, and fail-safe on drift; +- MCP tools call Core services and preserve approval, verification, and reviewer gates; +- artifact redaction is explicit, separate from canonical evidence, and not proof of secrecy; +- symlink and junction behavior fails closed at writable/export boundaries; +- worktree ownership and cleanup cannot overlap native and managed mutation; +- imported evidence remains audit-only and cannot manufacture enforced control; +- provider output parsing treats malformed, stale, and unavailable usage as unknown. + +Residual risks are documented in `security-model.md` and `known-limitations.md`. +Release preparation performs no push, publication, tag, or GitHub release action. 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/stable-compatibility.md b/docs/stable-compatibility.md new file mode 100644 index 0000000..8be5e66 --- /dev/null +++ b/docs/stable-compatibility.md @@ -0,0 +1,15 @@ +# Stable Compatibility Contract + +`cewp compatibility --json` is the canonical `stable-compatibility/v1` projection. +The 1.x candidate supports Node 22, 24, and 26 on the repository Windows and Ubuntu +CI matrix, with Git 2.39 or newer. Codex CLI 0.137.0 is the locally probed baseline; +version or capability drift warns and falls back instead of claiming compatibility. + +The supported execution owners are `managed`, `native`, and `audit-only`. +`codex-exec` is the selected managed backend. App Server is experimental and not +graduated, and its fallback is `codex-exec`. Native goal completion is observation, +not CEWP verification or reviewer PASS. Missing host usage remains unknown. + +Package and plugin versions must match. Private desktop attachment, persistent native +panels, automatic model routing, and non-Codex provider expansion are not supported. +The command reports `blocked-pilot-evidence` until genuine Phase 13 gates 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..58e2062 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.14.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": [ @@ -23,12 +24,27 @@ "src", "templates/workflows", "README.md", + "CONTRIBUTING.md", + "SECURITY.md", "docs/install.md", "docs/supervised-workflow.md", "docs/workflow-runtime.md", "docs/known-limitations.md", "docs/pilot-kit.md", + "docs/case-study-template.md", "docs/codex-capability-matrix.md", + "docs/external-integration-boundary.md", + "docs/evidence-receipts.md", + "docs/architecture.md", + "docs/contract-extension-example.md", + "docs/stable-compatibility.md", + "docs/migration-policy.md", + "docs/security-review-1.0.md", + "docs/recovery-guide.md", + "docs/performance-budgets.md", + "docs/contracts.md", + "docs/native-goal-or-cewp.md", + "docs/manual-acceptance.md", "docs/adr/0001-codex-first-supervised-goals.md", "docs/adr/0002-execution-ownership.md", "docs/adr/0003-cost-assurance-and-safe-pauses.md", @@ -49,8 +65,11 @@ "node": ">=22" }, "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": "npm run test:contracts && npm run test:phase12 && npm run test:phase13 && npm run test:phase14 && npm run smoke", + "test:phase12": "npm run test:evidence-receipt && npm run test:event-run-verify && npm run test:operator-report && npm run test:run-comparison && npm run test:evidence-redaction", + "test:phase13": "npm run test:pilot-record && npm run test:pilot-gates && npm run test:pilot-receipt-link && npm run test:pilot-export && npm run test:pilot-public-surface && npm run test:contributor-surface && npm run test:pilot-release", + "test:phase14": "npm run test:stable-core && npm run test:performance-budget && npm run test:release-preparation && npm run test:package-lifecycle", + "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,17 +109,42 @@ "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:evidence-receipt": "node tests/contracts/evidence-receipt.js", + "test:event-run-verify": "node tests/contracts/event-run-verify.js", + "test:operator-report": "node tests/contracts/operator-report.js", + "test:run-comparison": "node tests/contracts/run-comparison.js", + "test:evidence-redaction": "node tests/contracts/evidence-redaction.js", "test:integration-observation": "node tests/contracts/integration-observation.js", "test:native-goal-events": "node tests/contracts/native-goal-events.js", + "test:pilot-record": "node tests/contracts/pilot-record.js", + "test:pilot-gates": "node tests/contracts/pilot-gates.js", + "test:pilot-receipt-link": "node tests/contracts/pilot-receipt-link.js", + "test:pilot-export": "node tests/contracts/pilot-export.js", + "test:pilot-public-surface": "node tests/contracts/pilot-public-surface.js", + "test:contributor-surface": "node tests/contracts/contributor-surface.js", + "test:pilot-release": "node tests/contracts/pilot-release.js", + "test:stable-core": "node tests/contracts/stable-core.js", + "test:release-preparation": "node tests/contracts/release-preparation.js", + "test:performance-budget": "node tests/contracts/performance-budget.js", + "test:package-lifecycle": "node tests/capabilities/package-lifecycle.js", + "release:plan": "node scripts/prepare-release.js --plan --json", + "release:prepare": "node scripts/prepare-release.js --yes", "demo:supervised": "node src/demo/supervised.js", "test:plugin-lifecycle": "node tests/capabilities/plugin-lifecycle.js", "test:clean-install": "node tests/capabilities/clean-install.js", "probe:codex-app-server": "node tests/capabilities/codex-app-server.js", "smoke": "node tests/harness/run-smoke.js", "check:cli": "node ./bin/cewp.js --help", - "check:syntax": "node --check ./bin/cewp.js && node --check ./src/demo/fake-codex.js && node --check ./src/demo/supervised.js && node --check ./src/integration/capabilities.js && node --check ./src/integration/binding.js && node --check ./src/integration/observation.js && node --check ./src/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:evidence": "node --check ./src/evidence/receipt.js && node --check ./src/evidence/events.js && node --check ./src/evidence/report.js && node --check ./src/evidence/compare.js && node --check ./src/evidence/redaction.js && node --check ./src/evidence/usage.js && node --check ./src/evidence/verify.js && node --check ./src/run/verify.js && node --check ./tests/contracts/evidence-receipt.js && node --check ./tests/contracts/event-run-verify.js && node --check ./tests/contracts/operator-report.js && node --check ./tests/contracts/run-comparison.js && node --check ./tests/contracts/evidence-redaction.js", + "check:pilot": "node --check ./src/pilot/record.js && node --check ./src/pilot/observation.js && node --check ./src/pilot/evidence.js && node --check ./src/pilot/status.js && node --check ./src/pilot/export.js && node --check ./src/pilot/cli.js && node --check ./tests/contracts/pilot-record.js && node --check ./tests/contracts/pilot-gates.js && node --check ./tests/contracts/pilot-receipt-link.js && node --check ./tests/contracts/pilot-export.js && node --check ./tests/contracts/pilot-public-surface.js && node --check ./tests/contracts/contributor-surface.js && node --check ./tests/contracts/pilot-release.js", + "check:phase14": "node --check ./src/compatibility/contract.js && node --check ./src/performance/budgets.js && node --check ./scripts/prepare-release.js && node --check ./tests/contracts/stable-core.js && node --check ./tests/contracts/performance-budget.js && node --check ./tests/contracts/release-preparation.js && node --check ./tests/capabilities/package-lifecycle.js", + "check": "npm run check:syntax && npm run check:workflow-syntax && npm run check:mcp && npm run check:evidence && npm run check:pilot && npm run check:phase14 && 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..f9a673a 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.14.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/scripts/prepare-release.js b/scripts/prepare-release.js new file mode 100644 index 0000000..b16c585 --- /dev/null +++ b/scripts/prepare-release.js @@ -0,0 +1,71 @@ +"use strict"; + +const childProcess = require("node:child_process"); +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const path = require("node:path"); + +const repoRoot = path.resolve(__dirname, ".."); +const packageJson = require(path.join(repoRoot, "package.json")); + +function buildPlan() { + return { + schemaVersion: "release-preparation/v1", + packageVersion: packageJson.version, + artifactRoot: `.cewp/release-prep/${packageJson.version}`, + validation: ["npm run check", "npm run pack:dry-run", "git diff --check"], + blockers: ["phase-13-pilot-gates", "exact-clean-linux-current-source"], + externalActions: ["npm-publication", "git-tag", "github-release", "remote-push"].map((id) => ({ + id, + automatic: false, + humanApprovalRequired: true, + })), + }; +} + +function run(command, args) { + const result = childProcess.spawnSync(command, args, { + cwd: repoRoot, + encoding: "utf8", + shell: false, + windowsHide: true, + stdio: "inherit", + }); + if (result.error || result.status !== 0) throw result.error || new Error(`${command} failed with ${result.status}.`); +} + +function prepare(plan) { + run(process.execPath, [process.env.npm_execpath, "run", "check"]); + run(process.execPath, [process.env.npm_execpath, "run", "pack:dry-run"]); + run("git", ["diff", "--check"]); + const outputRoot = path.join(repoRoot, ...plan.artifactRoot.split("/")); + fs.mkdirSync(outputRoot, { recursive: true }); + const packed = childProcess.spawnSync(process.execPath, [ + process.env.npm_execpath, "pack", "--json", "--pack-destination", outputRoot, + ], { cwd: repoRoot, encoding: "utf8", shell: false, windowsHide: true }); + if (packed.error || packed.status !== 0) throw packed.error || new Error(packed.stderr || "package artifact preparation failed"); + const entry = JSON.parse(packed.stdout)[0]; + const artifactPath = path.join(outputRoot, entry.filename); + const manifest = { + ...plan, + preparedAt: new Date().toISOString(), + artifact: { + file: entry.filename, + bytes: fs.statSync(artifactPath).size, + sha256: crypto.createHash("sha256").update(fs.readFileSync(artifactPath)).digest("hex"), + }, + releasePerformed: false, + }; + fs.writeFileSync(path.join(outputRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" }); + return manifest; +} + +const args = new Set(process.argv.slice(2)); +const plan = buildPlan(); +if (args.has("--plan")) { + console.log(args.has("--json") ? JSON.stringify(plan, null, 2) : plan.validation.join("\n")); +} else if (!args.has("--yes")) { + throw new Error("Release artifact preparation requires --yes; external publication, tag, push, and release remain separate human actions."); +} else { + console.log(JSON.stringify(prepare(plan), null, 2)); +} diff --git a/src/cli/parse.js b/src/cli/parse.js index 15b3c04..1124c9b 100644 --- a/src/cli/parse.js +++ b/src/cli/parse.js @@ -74,6 +74,14 @@ function parseArgs(argv) { workerId: undefined, templateName: undefined, compilerDigest: undefined, + operation: undefined, + taskClass: undefined, + model: undefined, + effort: undefined, + comparisonRunId: undefined, + pilotId: undefined, + participant: undefined, + participantId: undefined, }; if (argv[0] === "--help" || argv[0] === "-h") { @@ -93,17 +101,51 @@ function parseArgs(argv) { return args; } - const optionStart = ["run", "supervise", "workflow", "demo"].includes(args.command) ? 2 : 1; + const optionStart = ["run", "supervise", "workflow", "integration", "pilot", "demo"].includes(args.command) ? 2 : 1; for (let index = optionStart; index < argv.length; index += 1) { const arg = argv[index]; + if (args.command === "pilot" && args.subcommand === "record" && index === 2 && !arg.startsWith("--")) { + args.pilotId = arg; + continue; + } + + if (args.command === "pilot" && args.subcommand === "export" && index === 2 && !arg.startsWith("--")) { + args.pilotId = arg; + continue; + } + + if (args.command === "pilot" && arg === "--pilot-id") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error("--pilot-id requires an id."); + args.pilotId = value; + index += 1; + continue; + } + + if (args.command === "pilot" && arg === "--participant") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error("--participant requires a classification."); + args.participant = value; + index += 1; + continue; + } + + if (args.command === "pilot" && arg === "--participant-id") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error("--participant-id requires an id."); + args.participantId = value; + index += 1; + continue; + } + if (args.command === "run" && args.subcommand === "prompt" && index === 2) { args.role = arg; continue; } - if (args.command === "run" && ["status", "next", "resume"].includes(args.subcommand) && index === 2 && !arg.startsWith("--")) { + if (args.command === "run" && ["status", "next", "resume", "verify"].includes(args.subcommand) && index === 2 && !arg.startsWith("--")) { args.runId = arg; continue; } @@ -128,6 +170,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; @@ -138,7 +195,17 @@ function parseArgs(argv) { continue; } - if (args.command === "workflow" && ["status", "start", "result", "review", "finalize", "intervene", "revise", "apply-revision", "migrate"].includes(args.subcommand) && index === 2 && !arg.startsWith("--")) { + if (args.command === "workflow" && args.subcommand === "compare" && index === 2 && !arg.startsWith("--")) { + args.workflowRunId = arg; + continue; + } + + if (args.command === "workflow" && args.subcommand === "compare" && index === 3 && !arg.startsWith("--")) { + args.comparisonRunId = arg; + continue; + } + + if (args.command === "workflow" && ["status", "start", "result", "review", "finalize", "receipt", "report", "export", "intervene", "revise", "apply-revision", "migrate"].includes(args.subcommand) && index === 2 && !arg.startsWith("--")) { args.workflowRunId = arg; continue; } @@ -203,7 +270,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 +280,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 +509,7 @@ function parseArgs(argv) { continue; } - if (["doctor", "run", "supervise", "workflow", "demo"].includes(args.command) && arg === "--json") { + if (["doctor", "run", "supervise", "workflow", "integration", "pilot", "demo", "compatibility"].includes(args.command) && arg === "--json") { args.json = true; continue; } @@ -425,6 +524,17 @@ function parseArgs(argv) { continue; } + if (args.command === "pilot" && arg === "--yes") { + args.yes = true; + continue; + } + + + if (args.command === "integration" && arg === "--yes") { + args.yes = true; + continue; + } + if (args.command === "supervise" && arg === "--allow-test-authoring") { args.allowTestAuthoring = true; continue; @@ -478,7 +588,7 @@ function parseArgs(argv) { continue; } - if (["run", "supervise", "workflow"].includes(args.command) && arg === "--from") { + if (["run", "supervise", "workflow", "pilot"].includes(args.command) && arg === "--from") { const value = argv[index + 1]; if (!value || value.startsWith("--")) { throw new Error("--from requires a file path."); diff --git a/src/cli/usage.js b/src/cli/usage.js index b804c0c..2e51cb5 100644 --- a/src/cli/usage.js +++ b/src/cli/usage.js @@ -7,6 +7,11 @@ Usage: cewp init [--mode repo|global] [--target ] [--force] [--with-config] cewp list [--mode repo|global] [--target ] cewp doctor [--mode repo|global] [--target ] [--json] + cewp compatibility [--json] + cewp pilot create --pilot-id --participant --participant-id [--json] + cewp pilot record --from --yes [--json] + cewp pilot status [--json] + cewp pilot export [] [--json] cewp workflow validate [--json] cewp workflow compile (--goal | --from ) [--source-kind ] [--json] cewp workflow template [--json] @@ -17,6 +22,13 @@ Usage: cewp workflow result --task --result --yes [--json] cewp workflow review --result --yes [--json] cewp workflow finalize --yes [--json] + cewp workflow receipt [--json] + cewp workflow report [--json] + cewp workflow compare [--json] + cewp workflow export [--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 +43,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] @@ -52,6 +65,7 @@ Usage: cewp run status [run-id] [--json] cewp run next [run-id] [--json] cewp run resume [run-id] [--json] + cewp run verify [--json] cewp run prompts cewp run prompt cewp run worktrees plan diff --git a/src/compatibility/contract.js b/src/compatibility/contract.js new file mode 100644 index 0000000..4f9dbfc --- /dev/null +++ b/src/compatibility/contract.js @@ -0,0 +1,97 @@ +"use strict"; + +const STABLE_COMPATIBILITY_SCHEMA_VERSION = "stable-compatibility/v1"; + +function packageVersion() { + try { + return require("../../package.json").version; + } catch { + return "unknown"; + } +} + +function numericVersion(value) { + const match = String(value || "").match(/^(\d+)\.(\d+)\.(\d+)/); + if (!match) throw new Error(`Unsupported package version: ${value || "missing"}.`); + return match.slice(1).map(Number); +} + +function assessDowngradeCompatibility(readerVersion, stateWrittenByVersion) { + const reader = numericVersion(readerVersion); + const writer = numericVersion(stateWrittenByVersion); + const readerIsOlder = reader.some((part, index) => part !== writer[index] && part < writer[index] + && reader.slice(0, index).every((earlier, earlierIndex) => earlier === writer[earlierIndex])); + return { + schemaVersion: "downgrade-compatibility/v1", + compatible: !readerIsOlder, + readerVersion, + stateWrittenByVersion, + warning: readerIsOlder ? { + code: "package-downgrade-state-newer", + action: "Use the newer CEWP reader or a documented export; do not rewrite canonical state.", + } : null, + }; +} + +function buildCompatibilityContract() { + return { + schemaVersion: STABLE_COMPATIBILITY_SCHEMA_VERSION, + packageVersion: packageVersion(), + release: { + line: "1.x candidate", + status: "blocked-pilot-evidence", + reason: "Phase 13 requires genuine independent pilot evidence before 1.0.", + }, + runtime: { + node: { majors: [22, 24, 26], minimum: "22.0.0", evidence: "repository CI matrix" }, + operatingSystems: ["windows-latest", "ubuntu-latest"], + git: { minimum: "2.39.0", testedLocally: "2.49.0.windows.1" }, + codexCli: { tested: ["0.137.0"], compatibility: "capability-probed; drift warns and falls back" }, + }, + package: { + npm: "same version as packageVersion", + pluginManifest: "same version as packageVersion", + pluginInstallSurface: "Codex CLI and supported ChatGPT desktop Codex mode", + }, + execution: { + modes: ["supervised", "autonomous", "audit-only"], + owners: ["managed", "native", "audit-only"], + managedBackend: { id: "codex-exec", status: "stable-selected" }, + appServer: { status: "experimental-not-graduated", fallback: "codex-exec" }, + nativeGoal: { status: "versioned-observation-or-explicit-intake", completionAuthority: false }, + }, + hostClaims: { + nativeCompletionIsVerification: false, + unknownUsageBecomesZero: false, + privateDesktopAttachmentSupported: false, + persistentNativePanelSupported: false, + }, + schemas: { + workflowDefinition: "workflow-definition/v1", + runState: "run-state/v2", + legacySupervisedRun: "supervised-run/v1 (read-only projection and explicit migration)", + taskResult: "task-result/v1", + evidenceReceipt: "evidence-receipt/v1", + event: "event/v1", + pilotRecord: "pilot-record/v1", + pilotObservation: "pilot-observation/v1", + pilotStatus: "pilot-status/v1", + pilotExport: "pilot-export/v1", + mcpProtocol: "cewp-mcp/v1", + }, + }; +} + +function runCompatibility(options = {}) { + const contract = buildCompatibilityContract(); + if (options.json) console.log(JSON.stringify(contract, null, 2)); + else { + console.log("CEWP stable compatibility candidate"); + console.log(`Package: ${contract.packageVersion}`); + console.log(`1.0 eligibility: ${contract.release.status}`); + console.log(`Managed backend: ${contract.execution.managedBackend.id}`); + console.log(`Node: ${contract.runtime.node.majors.join(", ")}`); + } +} + +module.exports = { STABLE_COMPATIBILITY_SCHEMA_VERSION, assessDowngradeCompatibility, buildCompatibilityContract, runCompatibility }; diff --git a/src/evidence/compare.js b/src/evidence/compare.js new file mode 100644 index 0000000..fac2963 --- /dev/null +++ b/src/evidence/compare.js @@ -0,0 +1,128 @@ +"use strict"; + +const RUN_COMPARISON_SCHEMA_VERSION = "run-comparison/v1"; + +function observed(value, source = "cewp-run-state") { + return { label: "observed", value, source }; +} + +function unknown(reason) { + return { label: "unknown", value: null, reason }; +} + +function duration(receipt) { + const start = Date.parse(receipt.timestamps.createdAt); + const end = Date.parse(receipt.timestamps.finalizedAt); + if (!Number.isFinite(start) || !Number.isFinite(end)) return unknown("run is not finalized with a bounded duration"); + return observed(end - start, "cewp-run-timestamps"); +} + +function commandCount(receipt) { + return receipt.commands.reduce((total, task) => ( + total + + (task.verification.baseline ? 1 : 0) + + task.verification.targeted.length + + task.verification.full.length + ), 0); +} + +function failureSummary(receipt) { + const failures = receipt.tasks.flatMap((task) => task.failure ? [{ taskId: task.id, ...task.failure }] : []); + return { count: failures.length, entries: failures }; +} + +function scopeSummary(receipt) { + return { + passed: receipt.tasks.filter((task) => task.scopeVerdict.status === "passed").length, + unknown: receipt.tasks.filter((task) => task.scopeVerdict.status === "unknown").length, + changedFiles: [...new Set(receipt.tasks.flatMap((task) => task.changedFiles))].sort(), + }; +} + +function verificationSummary(receipt) { + const evidence = receipt.tasks.flatMap((task) => [ + ...(task.verification.baseline.evidence || []), + ...task.verification.targeted, + ...task.verification.full, + ]); + return { + commandCount: commandCount(receipt), + passedEvidence: evidence.filter((entry) => entry.status === "passed").length, + failedEvidence: evidence.filter((entry) => entry.status === "failed").length, + evidence, + }; +} + +function summarizeRun(receipt) { + const nativeGoal = receipt.integration && receipt.integration.nativeGoal; + return { + runId: receipt.runId, + execution: receipt.execution, + nativeGoalBaseline: Boolean(nativeGoal && nativeGoal.status === "known"), + nativeGoalEvidence: nativeGoal || unknown("no supported native goal binding"), + }; +} + +function delta(left, right) { + if (left.label !== "observed" || right.label !== "observed") return unknown("both comparison values must be observed"); + return observed(right.value - left.value, "derived-from-observed-values"); +} + +function compareEvidenceReceipts(left, right) { + if (!left || left.schemaVersion !== "evidence-receipt/v1" || !right || right.schemaVersion !== "evidence-receipt/v1") { + throw new Error("Run comparison requires two evidence-receipt/v1 values."); + } + const leftDuration = duration(left); + const rightDuration = duration(right); + const leftAttempts = observed(left.tasks.reduce((total, task) => total + task.attempts, 0)); + const rightAttempts = observed(right.tasks.reduce((total, task) => total + task.attempts, 0)); + const leftInterventions = observed(left.interventions.length); + const rightInterventions = observed(right.interventions.length); + const leftFailures = failureSummary(left); + const rightFailures = failureSummary(right); + const unavailable = []; + if (leftDuration.label === "unknown" || rightDuration.label === "unknown") unavailable.push("duration"); + unavailable.push("model-time", "estimate-accuracy", "cewp-overhead"); + for (const category of ["managedOperations", "capturedOutputBytes", "managedTokens", "hostInternal"]) { + if (left.usage[category].label === "unknown" || right.usage[category].label === "unknown") unavailable.push(`usage.${category}`); + } + if (left.cost.apiEquivalent.label === "unknown" || right.cost.apiEquivalent.label === "unknown") unavailable.push("api-equivalent-cost"); + return { + schemaVersion: RUN_COMPARISON_SCHEMA_VERSION, + generatedAt: left.generatedAt === right.generatedAt ? left.generatedAt : null, + runs: { left: summarizeRun(left), right: summarizeRun(right) }, + dimensions: { + outcome: { + left: { completeness: left.completeness.status, runStatus: left.completeness.runStatus, reviewerDecision: left.reviewer.decision }, + right: { completeness: right.completeness.status, runStatus: right.completeness.runStatus, reviewerDecision: right.reviewer.decision }, + }, + duration: { left: leftDuration, right: rightDuration, delta: delta(leftDuration, rightDuration) }, + modelTime: { left: unknown("effective model time is unavailable"), right: unknown("effective model time is unavailable") }, + usage: { + managedOperations: { left: left.usage.managedOperations, right: right.usage.managedOperations }, + capturedOutputBytes: { left: left.usage.capturedOutputBytes, right: right.usage.capturedOutputBytes }, + managedTokens: { left: left.usage.managedTokens, right: right.usage.managedTokens }, + hostInternal: { left: left.usage.hostInternal, right: right.usage.hostInternal }, + }, + estimateAccuracy: { left: unknown("no calibrated observed outcome mapping"), right: unknown("no calibrated observed outcome mapping") }, + apiEquivalentCost: { left: left.cost.apiEquivalent, right: right.cost.apiEquivalent }, + cewpOverhead: { left: unknown("CEWP overhead was not separately observed"), right: unknown("CEWP overhead was not separately observed") }, + attempts: { left: leftAttempts, right: rightAttempts, delta: delta(leftAttempts, rightAttempts) }, + interventions: { left: leftInterventions, right: rightInterventions, delta: delta(leftInterventions, rightInterventions) }, + failures: { left: leftFailures, right: rightFailures, delta: observed(rightFailures.count - leftFailures.count, "derived-from-run-state") }, + scope: { left: scopeSummary(left), right: scopeSummary(right) }, + commands: { left: left.commands, right: right.commands }, + verification: { left: verificationSummary(left), right: verificationSummary(right) }, + }, + equivalence: { + status: unavailable.length === 0 ? "complete" : "partial", + unavailable: [...new Set(unavailable)].sort(), + warning: unavailable.length > 0 ? "Unavailable values remain unknown and are excluded from numeric deltas." : null, + }, + }; +} + +module.exports = { + RUN_COMPARISON_SCHEMA_VERSION, + compareEvidenceReceipts, +}; diff --git a/src/evidence/events.js b/src/evidence/events.js new file mode 100644 index 0000000..cdc5307 --- /dev/null +++ b/src/evidence/events.js @@ -0,0 +1,133 @@ +"use strict"; + +const fs = require("node:fs"); + +const EVENT_SCHEMA_VERSION = "event/v1"; +const LEGACY_EVENT_SCHEMA_VERSIONS = new Set(["workflow-event/v1"]); +const EVENT_CATEGORIES = Object.freeze({ + "workflow-approved": "run", + "workflow-migrated": "plan-revision", + "workflow-revised": "plan-revision", + "task-started": "task", + "task-failed": "task", + "task-completed": "task", + "checkpoint-started": "checkpoint", + "checkpoint-completed": "checkpoint", + "dispatch-started": "dispatch", + "dispatch-completed": "dispatch", + "workflow-intervention": "operator-intervention", + "verification-completed": "verification", + "usage-observed": "usage-observation", + "estimate-revised": "estimate-revision", + "budget-approved": "budget-approval", + "allocation-consumed": "allocation-consumption", + "budget-threshold": "threshold", + "warning-presented": "warning-presentation", + "pause-budget-safe": "safe-pause", + "paused-budget-safe": "safe-pause", + "pause-budget-unverified": "unverified-pause", + "paused-budget-unverified": "unverified-pause", + "pause-host-limit": "host-limit", + "paused-host-limit": "host-limit", + "scope-evaluated": "scope", + "review-passed": "review", + "review-blocked": "review", + "checkpoint-review-passed": "review", + "checkpoint-review-blocked": "review", + "workflow-cancelled": "cancellation", + "workflow-finalized": "finalize", + "workflow-lifecycle": "run", + "hook-evidence-approved": "scope", +}); + +function isObject(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function normalizeLifecycleEvent(candidate, options = {}) { + if (!isObject(candidate)) throw new Error("Lifecycle event must be an object."); + const legacy = LEGACY_EVENT_SCHEMA_VERSIONS.has(candidate.schemaVersion); + if (candidate.schemaVersion !== EVENT_SCHEMA_VERSION && !(options.allowLegacy && legacy)) { + throw new Error(`Incompatible lifecycle event schema: ${candidate.schemaVersion || "missing"}.`); + } + if (typeof candidate.type !== "string" || !EVENT_CATEGORIES[candidate.type]) { + throw new Error(`Unknown lifecycle event type: ${candidate.type || "missing"}.`); + } + if (typeof candidate.runId !== "string" || candidate.runId.length === 0) { + throw new Error("Lifecycle event runId is required."); + } + if (options.runId && candidate.runId !== options.runId) { + throw new Error(`Lifecycle event runId mismatch: ${candidate.runId}.`); + } + if (typeof candidate.timestamp !== "string" || !Number.isFinite(Date.parse(candidate.timestamp))) { + throw new Error("Lifecycle event timestamp must be an ISO timestamp."); + } + const category = EVENT_CATEGORIES[candidate.type]; + if (!legacy && candidate.category !== category) { + throw new Error(`Lifecycle event ${candidate.type} requires category ${category}.`); + } + return { + ...candidate, + schemaVersion: EVENT_SCHEMA_VERSION, + timestamp: new Date(candidate.timestamp).toISOString(), + category, + }; +} + +function createLifecycleEvent(candidate) { + return normalizeLifecycleEvent({ + ...candidate, + schemaVersion: EVENT_SCHEMA_VERSION, + category: EVENT_CATEGORIES[candidate.type], + }); +} + +function parseLifecycleEvents(content, options = {}) { + const events = []; + const issues = []; + String(content).split(/\r?\n/).forEach((line, index) => { + if (!line) return; + let parsed; + try { + parsed = JSON.parse(line); + } catch { + issues.push({ code: "malformed-event", line: index + 1, message: "Lifecycle event is not valid JSON." }); + return; + } + try { + events.push(normalizeLifecycleEvent(parsed, { ...options, allowLegacy: true })); + } catch (error) { + issues.push({ + code: /schema/.test(error.message) ? "incompatible-event-schema" : "invalid-event", + line: index + 1, + message: error.message, + }); + } + }); + return { events, issues }; +} + +function readLifecycleEvents(filePath, options = {}) { + const parsed = parseLifecycleEvents(fs.readFileSync(filePath, "utf8"), options); + if (parsed.issues.length > 0) { + const first = parsed.issues[0]; + throw new Error(`${first.code} at line ${first.line}: ${first.message}`); + } + return parsed.events; +} + +function appendLifecycleEvent(runRoot, candidate) { + const event = createLifecycleEvent(candidate); + fs.appendFileSync(require("node:path").join(runRoot, "events.jsonl"), `${JSON.stringify(event)}\n`); + return event; +} + +module.exports = { + EVENT_CATEGORIES, + EVENT_SCHEMA_VERSION, + appendLifecycleEvent, + createLifecycleEvent, + normalizeLifecycleEvent, + parseLifecycleEvents, + readLifecycleEvents, +}; diff --git a/src/evidence/receipt.js b/src/evidence/receipt.js new file mode 100644 index 0000000..61e0afb --- /dev/null +++ b/src/evidence/receipt.js @@ -0,0 +1,502 @@ +"use strict"; + +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const path = require("node:path"); +const { getGitHeadCommit } = require("../lib/git"); +const { normalizeSlashPath } = require("../lib/paths"); +const { loadHostBinding, loadIntegrationControlReceipt } = require("../integration/binding"); +const { parseLifecycleEvents } = require("./events"); +const { buildUsageObservations, unknownUsageEstimate } = require("./usage"); +const { writeJsonAtomic } = require("../workflow/state"); + +const EVIDENCE_RECEIPT_SCHEMA_VERSION = "evidence-receipt/v1"; +const RECEIPT_BASENAMES = new Set(["evidence-receipt.json", "evidence-receipt.md"]); + +function lexicalCompare(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function isInside(parentPath, childPath) { + const relative = path.relative(path.resolve(parentPath), path.resolve(childPath)); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} + +function readJson(filePath, label) { + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")); + } catch (error) { + throw new Error(`Invalid ${label}: ${error.message}`); + } +} + +function listFiles(rootPath) { + if (!fs.existsSync(rootPath)) return []; + const files = []; + const visit = (current) => { + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const entryPath = path.join(current, entry.name); + if (entry.isDirectory()) visit(entryPath); + else if (entry.isFile()) files.push(entryPath); + } + }; + visit(rootPath); + return files.sort((left, right) => lexicalCompare(normalizeSlashPath(left), normalizeSlashPath(right))); +} + +function readJsonDirectory(found, rootPath, label, warnings) { + return listFiles(rootPath) + .filter((filePath) => filePath.endsWith(".json")) + .map((filePath) => { + try { + return { path: filePath, value: readJson(filePath, label) }; + } catch (error) { + warnings.push({ + code: "malformed-evidence-file", + path: normalizeSlashPath(path.relative(found.repoRoot, filePath)), + message: error.message, + }); + return null; + } + }) + .filter(Boolean); +} + +function readEvents(found, warnings) { + const eventPath = path.join(found.runRoot, "events.jsonl"); + if (!fs.existsSync(eventPath)) { + warnings.push({ code: "missing-events", message: "Workflow event ledger is missing." }); + return []; + } + const parsed = parseLifecycleEvents(fs.readFileSync(eventPath, "utf8"), { runId: found.run.runId }); + warnings.push(...parsed.issues); + return parsed.events; +} + +function truthAggregate(values, reason) { + const present = values.filter(Boolean); + if (present.length > 0 && present.every((entry) => entry.label === "observed")) { + return { + label: "observed", + value: present.reduce((total, entry) => total + entry.value, 0), + sources: [...new Set(present.map((entry) => entry.source).filter(Boolean))].sort(), + }; + } + return { label: "unknown", value: null, reason }; +} + +function safeEvidencePath(found, relativePath) { + if (typeof relativePath !== "string" || relativePath.length === 0) return null; + const resolved = path.resolve(found.repoRoot, relativePath); + return isInside(found.repoRoot, resolved) ? resolved : null; +} + +function referencedPaths(results, reviews) { + const paths = []; + for (const result of results) { + for (const entry of result.value.verification.baseline.evidence || []) paths.push(entry.evidencePath); + for (const entry of result.value.verification.targeted || []) paths.push(entry.evidencePath); + for (const entry of result.value.verification.full || []) paths.push(entry.evidencePath); + for (const artifact of result.value.artifacts || []) paths.push(artifact.path); + for (const evidencePath of (result.value.failure && result.value.failure.evidencePaths) || []) paths.push(evidencePath); + } + for (const review of reviews) { + for (const evidence of review.value.evidence || []) paths.push(evidence.path); + for (const finding of review.value.findings || []) { + for (const evidencePath of finding.evidencePaths || []) paths.push(evidencePath); + } + } + return [...new Set(paths)].sort(); +} + +function hashFile(found, filePath) { + const content = fs.readFileSync(filePath); + return { + path: normalizeSlashPath(path.relative(found.repoRoot, filePath)), + sha256: `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`, + bytes: content.length, + }; +} + +function integrityInventory(found, results, reviews, warnings) { + const canonical = listFiles(found.runRoot).filter((filePath) => { + if (RECEIPT_BASENAMES.has(path.basename(filePath))) return false; + const relative = normalizeSlashPath(path.relative(found.runRoot, filePath)); + return relative === "run.json" + || relative === "events.jsonl" + || relative.startsWith("checkpoints/") + || relative.startsWith("results/") + || relative.startsWith("reviews/") + || relative.startsWith("integration/"); + }); + const definitionPath = path.resolve(found.repoRoot, found.run.workflow.definitionPath); + if (isInside(found.repoRoot, definitionPath) && fs.existsSync(definitionPath)) canonical.push(definitionPath); + for (const relativePath of referencedPaths(results, reviews)) { + const filePath = safeEvidencePath(found, relativePath); + if (!filePath || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + warnings.push({ code: "missing-referenced-evidence", path: relativePath, message: "Referenced evidence file is unavailable." }); + continue; + } + canonical.push(filePath); + } + return [...new Set(canonical.map((entry) => path.resolve(entry)))] + .map((filePath) => hashFile(found, filePath)) + .sort((left, right) => lexicalCompare(left.path, right.path)); +} + +function taskReceipt(found, runtimeTask, resultsByTask) { + const definitionTask = found.definition.tasks.find((entry) => entry.id === runtimeTask.id); + const result = resultsByTask.get(runtimeTask.id) || null; + return { + id: runtimeTask.id, + title: definitionTask.title, + status: runtimeTask.status, + attempts: runtimeTask.attempts, + allowedFiles: definitionTask.allowedFiles, + forbiddenFiles: definitionTask.forbiddenFiles, + stoppingConditions: definitionTask.stoppingConditions, + changedFiles: result ? result.changedFiles : [], + scopeVerdict: result ? { status: "passed", basis: "validated-task-result" } : { status: "unknown", basis: "result-unavailable" }, + verification: result ? result.verification : { + baseline: { status: "unknown", evidence: [] }, + targeted: [], + full: [], + }, + failure: result ? result.failure : null, + artifacts: result ? result.artifacts : [], + resultId: runtimeTask.resultId, + recovery: { + blocker: runtimeTask.blocker, + failureHistory: runtimeTask.failureHistory || [], + stateHistory: runtimeTask.stateHistory || [], + interventions: (found.run.interventions || []).filter((entry) => entry.taskId === runtimeTask.id), + }, + }; +} + +function checkpointReceipt(entry) { + return { + checkpointId: entry.value.checkpointId, + taskId: entry.value.taskId, + attempt: entry.value.attempt, + status: entry.value.status, + startedAt: entry.value.startedAt, + completedAt: entry.value.completedAt, + failureClassification: entry.value.failureClassification, + interventionState: entry.value.interventionState, + reviewer: entry.value.reviewer, + result: entry.value.result, + verification: entry.value.verification, + }; +} + +function budgetReceipt(budget) { + const allocationChecks = Object.keys(budget.allocations).sort().map((name) => ({ + name, + approved: budget.allocations[name], + consumed: budget.consumed.allocations[name], + respected: budget.consumed.allocations[name] <= budget.allocations[name], + protected: budget.protectedAllocations.includes(name), + })); + const ceilings = { + modelOperations: budget.consumed.modelOperations <= budget.modelOperations, + targetedVerificationRuns: budget.consumed.targetedVerificationRuns <= budget.maxTargetedVerificationRuns, + fullVerificationRuns: budget.consumed.fullVerificationRuns <= budget.maxFullVerificationRuns, + capturedOutputBytes: budget.consumed.capturedOutputBytes <= budget.maxCapturedOutputBytes, + }; + const respected = Object.values(ceilings).every(Boolean) && allocationChecks.every((entry) => entry.respected); + return { + ...budget, + compliance: { + status: respected ? "passed" : "failed", + absoluteCeilingRespected: ceilings.modelOperations, + protectedAllocationsRespected: allocationChecks.filter((entry) => entry.protected).every((entry) => entry.respected), + ceilings, + allocations: allocationChecks, + }, + }; +} + +function buildEvidenceReceipt(found, options = {}) { + const generatedAt = options.generatedAt || new Date().toISOString(); + const warnings = []; + const results = readJsonDirectory(found, path.join(found.runRoot, "results"), "workflow result", warnings); + const reviews = readJsonDirectory(found, path.join(found.runRoot, "reviews"), "workflow review", warnings); + const checkpoints = readJsonDirectory(found, path.join(found.runRoot, "checkpoints"), "workflow checkpoint", warnings); + const events = readEvents(found, warnings); + const resultsByTask = new Map(results.map((entry) => [entry.value.taskId, entry.value])); + const reviewValues = reviews.map((entry) => entry.value); + const usageValues = [...results.map((entry) => entry.value), ...reviewValues].map((entry) => entry.usage); + const usageObservations = buildUsageObservations( + found, + results.map((entry) => entry.value), + reviewValues, + warnings, + ); + let complete = found.run.status === "finalized" + && found.run.tasks.every((task) => task.status === "completed" && task.verification && task.verification.status === "passed") + && (!found.run.reviewerPolicy.requiredForFinalize || found.run.reviewer.status === "passed"); + if (!complete) warnings.push({ code: "run-not-finalized", message: `Run status ${found.run.status} produces a partial receipt.` }); + const integrityFiles = integrityInventory(found, results, reviews, warnings); + if (warnings.some((warning) => ["missing-referenced-evidence", "malformed-evidence-file", "malformed-event", "incompatible-event-schema", "invalid-event", "missing-events", "malformed-usage-observation-ledger"].includes(warning.code))) { + complete = false; + } + const headCommit = getGitHeadCommit(found.repoRoot); + const baseCommit = found.run.git && found.run.git.baseCommit; + const hostBinding = loadHostBinding(found); + const warningDeliveries = events + .filter((entry) => entry.category === "warning-presentation") + .map((entry) => ({ + warning: entry.warning || "unknown", + surface: entry.surface || "unknown", + deliveredAt: entry.timestamp, + evidence: "event/v1", + })); + const knownCodexProvider = found.run.execution.backend === "codex-exec" + || Boolean(hostBinding && hostBinding.host.product === "codex"); + + return { + schemaVersion: EVIDENCE_RECEIPT_SCHEMA_VERSION, + generatedAt, + runId: found.run.runId, + goal: found.run.goal, + sourcePlan: found.run.approval.source, + workflow: found.run.workflow, + planRevisions: found.run.revisionHistory || [], + operatingModes: found.run.execution.allowedModes, + execution: { + owner: found.run.execution.owner, + backend: found.run.execution.backend, + }, + assurance: found.run.assurance, + completeness: { status: complete ? "complete" : "partial", runStatus: found.run.status }, + tasks: found.run.tasks.map((task) => taskReceipt(found, task, resultsByTask)), + checkpoints: checkpoints.map(checkpointReceipt), + interventions: found.run.interventions || [], + events, + providers: [{ + provider: knownCodexProvider + ? { status: "known", value: "codex" } + : { status: "unknown", value: null, reason: "selected execution boundary does not identify a provider" }, + effectiveModel: { status: "unknown", value: null, reason: "workflow result does not expose an effective model" }, + }], + commands: found.definition.tasks.map((task) => ({ taskId: task.id, verification: task.verification })), + reviewer: found.run.reviewer, + reviews: reviewValues, + reviewHistory: found.run.reviewHistory || [], + budget: budgetReceipt(found.run.budget), + usage: { + managedOperations: truthAggregate(usageValues.map((usage) => usage && usage.managedOperations), "no uniformly observed managed operation evidence"), + capturedOutputBytes: truthAggregate(usageValues.map((usage) => usage && usage.capturedOutputBytes), "no uniformly observed captured output evidence"), + managedTokens: truthAggregate(usageValues.map((usage) => usage && usage.managedTokens), "managed token totals are unavailable"), + hostInternal: truthAggregate(usageValues.map((usage) => usage && usage.hostInternal), "host-internal usage is unavailable"), + observations: usageObservations, + }, + estimate: unknownUsageEstimate(), + cost: { apiEquivalent: { label: "unknown", value: null, currency: null, pricingDate: null, model: null, reason: "no valid dated API pricing mapping" } }, + warningSurface: warningDeliveries.length > 0 + ? { status: "observed", deliveries: warningDeliveries } + : { status: "unknown", deliveries: [] }, + git: { + baseCommit: baseCommit + ? { status: "known", value: baseCommit } + : { status: "unknown", value: null, reason: "historical run predates source git identity" }, + headCommit: { status: "known", value: headCommit }, + }, + policy: { + approval: found.run.approval, + assurance: found.run.assurance, + controls: loadIntegrationControlReceipt(found), + }, + integration: { + nativeGoal: hostBinding && hostBinding.execution.owner === "native" && hostBinding.references.goalId + ? { + status: "known", + goalId: hostBinding.references.goalId, + surface: hostBinding.host.surface, + authenticationBoundary: hostBinding.provenance.authenticationBoundary, + } + : { status: "unknown", goalId: null, reason: "no supported native goal binding" }, + }, + integrity: { + algorithm: "sha256", + claim: "tamper-evident-local-metadata", + tamperProof: false, + files: integrityFiles, + sourceIdentities: { + workflowDigest: found.run.workflow.digest, + sourcePlanSha256: found.run.approval.source.sha256, + gitBaseCommit: baseCommit || null, + gitHeadCommit: headCommit, + }, + }, + timestamps: { + createdAt: found.run.createdAt, + updatedAt: found.run.updatedAt, + finalizedAt: found.run.finalization ? found.run.finalization.finalizedAt : null, + generatedAt, + }, + warnings, + }; +} + +function renderEvidenceReceiptMarkdown(receipt) { + const taskLines = receipt.tasks.flatMap((task) => { + const recovery = task.recovery || { failureHistory: [], interventions: [] }; + return [ + `- ${task.id}: ${task.status}; attempts ${task.attempts}; scope ${task.scopeVerdict.status}`, + ` - Allowed: ${task.allowedFiles.join(", ") || "none"}`, + ` - Changed: ${task.changedFiles.join(", ") || "none"}`, + ` - Artifacts: ${task.artifacts.map((entry) => `${entry.kind}:${entry.path}`).join(", ") || "none"}`, + ` - Failure: ${task.failure ? `${task.failure.classification}: ${task.failure.summary}` : "none"}`, + ` - Recovery: ${recovery.interventions.map((entry) => `${entry.event}: ${entry.reason}`).join("; ") || "none"}`, + ]; + }).join("\n"); + const checkpointLines = receipt.checkpoints.map((entry) => { + const baseline = entry.verification && entry.verification.baseline + ? entry.verification.baseline.status + : "unknown"; + return `- ${entry.checkpointId}: ${entry.status}; task ${entry.taskId}; attempt ${entry.attempt}; baseline ${baseline}; failure ${entry.failureClassification || "none"}`; + }).join("\n"); + const commandLines = receipt.commands.flatMap((entry) => [ + `- ${entry.taskId} baseline: ${entry.verification.baseline || "none"}`, + ...entry.verification.targeted.map((command) => ` - targeted: ${command}`), + ...entry.verification.full.map((command) => ` - full: ${command}`), + ]).join("\n"); + const revisionLines = receipt.planRevisions.map((entry) => ( + `- revision ${entry.revision}: ${entry.reason || entry.digest}; superseded ${entry.supersededAt || "unknown"}` + )).join("\n"); + const interventionLines = receipt.interventions.map((entry) => ( + `- ${entry.event}: ${entry.reason}; actor ${entry.actor}; ${entry.recordedAt}` + )).join("\n"); + const significantEventLines = receipt.events + .filter((entry) => ["threshold", "warning-presentation", "safe-pause", "unverified-pause", "host-limit", "cancellation"].includes(entry.category)) + .map((entry) => `- ${entry.timestamp}: ${entry.category}/${entry.type}; ${entry.reason || entry.warning || entry.threshold || "recorded"}`) + .join("\n"); + const allocationLines = receipt.budget.compliance.allocations.map((entry) => ( + `- ${entry.name}: ${entry.consumed}/${entry.approved}; protected ${entry.protected ? "yes" : "no"}; ${entry.respected ? "passed" : "failed"}` + )).join("\n"); + const observationLines = receipt.usage.observations.map((entry) => ( + `- ${entry.category}/${entry.rawCategory}: ${entry.availability}; source ${entry.source.id}; auth ${entry.source.authenticationBoundary}; model ${entry.effectiveModel.status}` + )).join("\n"); + const controlReceipt = receipt.policy.controls; + const controlLines = controlReceipt + ? controlReceipt.controls.map((entry) => `- ${entry.name}: ${entry.classification}; ${entry.effect}`).join("\n") + : "- none"; + const warningLines = receipt.warnings.length > 0 + ? receipt.warnings.map((warning) => `- ${warning.code}: ${warning.message}`).join("\n") + : "- none"; + return `# CEWP Evidence Receipt + +- Run: ${receipt.runId} +- Goal: ${receipt.goal} +- Status: ${receipt.completeness.runStatus} (${receipt.completeness.status}) +- Execution: ${receipt.execution.owner} / ${receipt.execution.backend || "none"} +- Operating modes: ${receipt.operatingModes.join(", ")} +- Provider: ${receipt.providers[0].provider.status === "known" ? receipt.providers[0].provider.value : "unknown"} +- Effective model: ${receipt.providers[0].effectiveModel.status === "known" ? receipt.providers[0].effectiveModel.value : "unknown"} +- Source: ${receipt.sourcePlan.kind}; ${receipt.sourcePlan.path || "direct goal"} +- Workflow: ${receipt.workflow.id} revision ${receipt.workflow.revision}; ${receipt.workflow.digest} +- Git base: ${receipt.git.baseCommit.status === "known" ? receipt.git.baseCommit.value : "unknown"} +- Git head: ${receipt.git.headCommit.value} +- Integrity: ${receipt.integrity.claim}; ${receipt.integrity.files.length} hashed files; not tamper-proof +${receipt.redaction && receipt.redaction.applied ? `- Redaction: applied (${receipt.redaction.replacements} replacements)\n` : ""} + +## Tasks + +${taskLines || "- none"} + +## Checkpoints + +${checkpointLines || "- none"} + +## Commands and verification + +${commandLines || "- none"} + +## Plan revisions + +${revisionLines || "- none"} + +## Interventions and lifecycle decisions + +${interventionLines || "- none"} +${significantEventLines ? `\n${significantEventLines}` : ""} + +## Budget + +- Compliance: ${receipt.budget.compliance.status} +- Absolute ceiling: ${receipt.budget.compliance.absoluteCeilingRespected ? "passed" : "failed"} +- Protected allocations: ${receipt.budget.compliance.protectedAllocationsRespected ? "passed" : "failed"} +- Pause reason: ${receipt.budget.pauseReason || "none"} + +${allocationLines || "- none"} + +## Usage and estimate + +- Managed operations: ${receipt.usage.managedOperations.label}${receipt.usage.managedOperations.value === null ? "" : ` (${receipt.usage.managedOperations.value})`} +- Captured output: ${receipt.usage.capturedOutputBytes.label}${receipt.usage.capturedOutputBytes.value === null ? "" : ` (${receipt.usage.capturedOutputBytes.value})`} +- Managed tokens: ${receipt.usage.managedTokens.label} +- Host-internal usage: ${receipt.usage.hostInternal.label} +- Estimate: ${receipt.estimate.label}; confidence ${receipt.estimate.confidence}; samples ${receipt.estimate.sampleBasis.count}; estimator ${receipt.estimate.estimator.version}; drift ${receipt.estimate.drift.state} +- API-equivalent cost: ${receipt.cost.apiEquivalent.label}${receipt.cost.apiEquivalent.model ? `; model ${receipt.cost.apiEquivalent.model}; pricing ${receipt.cost.apiEquivalent.pricingDate}` : ""} +- Warning surface: ${receipt.warningSurface.status}; deliveries ${receipt.warningSurface.deliveries.length} + +${observationLines || "- no usage observations"} + +## Controls + +- Execution owner: ${receipt.execution.owner} +- Preventive enforced: ${controlReceipt ? controlReceipt.summary.preventiveEnforced : 0} +- Imported observed: ${controlReceipt ? controlReceipt.summary.importedObserved : 0} + +${controlLines} + +## Final review + +- Status: ${receipt.reviewer.status} +- Decision: ${receipt.reviewer.decision || "none"} +- Review ID: ${receipt.reviewer.reviewId || "none"} +- Reviews recorded: ${receipt.reviews.length} + +## Timestamps + +- Created: ${receipt.timestamps.createdAt} +- Updated: ${receipt.timestamps.updatedAt} +- Finalized: ${receipt.timestamps.finalizedAt || "none"} +- Receipt generated: ${receipt.timestamps.generatedAt} + +## Warnings + +${warningLines} + +This receipt excludes raw prompts and raw log contents by default. +`; +} + +function writeTextAtomic(filePath, content) { + const temporaryPath = `${filePath}.${process.pid}.tmp`; + fs.writeFileSync(temporaryPath, content, { flag: "wx" }); + try { + fs.renameSync(temporaryPath, filePath); + } catch (error) { + fs.rmSync(temporaryPath, { force: true }); + throw error; + } +} + +function writeEvidenceReceipt(found, options = {}) { + const receipt = buildEvidenceReceipt(found, options); + const jsonPath = path.join(found.runRoot, "evidence-receipt.json"); + const markdownPath = path.join(found.runRoot, "evidence-receipt.md"); + writeJsonAtomic(jsonPath, receipt); + writeTextAtomic(markdownPath, renderEvidenceReceiptMarkdown(receipt)); + return { receipt, paths: { json: jsonPath, markdown: markdownPath } }; +} + +module.exports = { + EVIDENCE_RECEIPT_SCHEMA_VERSION, + buildEvidenceReceipt, + renderEvidenceReceiptMarkdown, + writeEvidenceReceipt, +}; diff --git a/src/evidence/redaction.js b/src/evidence/redaction.js new file mode 100644 index 0000000..ccbe8a9 --- /dev/null +++ b/src/evidence/redaction.js @@ -0,0 +1,138 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { normalizeSlashPath } = require("../lib/paths"); +const { writeJsonAtomic } = require("../workflow/state"); +const { buildEvidenceReceipt, renderEvidenceReceiptMarkdown } = require("./receipt"); +const { buildOperatorReport, renderOperatorReportHtml } = require("./report"); + +const REDACTION_SCHEMA_VERSION = "redaction-policy/v1"; +const SENSITIVE_KEY = /^(authorization|password|passwd|secret|client[_-]?secret|api[_-]?key|access[_-]?token|refresh[_-]?token|session[_-]?token|auth[_-]?token|cookie|private[_-]?key|credential)$/i; +const PATH_KEY = /(^|[A-Z_-])(paths?|files?|director(?:y|ies))$/i; +const SENSITIVE_PATH = /(^|[\\/])(?:\.env(?:\.[^\\/]*)?|id_rsa|id_ed25519|credentials?(?:\.[^\\/]*)?|[^\\/]+\.(?:pem|p12|pfx|key))$/i; +const ABSOLUTE_WINDOWS_PATH = /^[a-z]:[\\/]/i; +const ABSOLUTE_UNC_PATH = /^\\\\/; + +function redactString(input, key, state) { + if (SENSITIVE_KEY.test(key || "")) { + state.replacements += 1; + state.classes.add("sensitive-key"); + return "[REDACTED]"; + } + if (PATH_KEY.test(key || "") && ( + path.isAbsolute(input) + || ABSOLUTE_WINDOWS_PATH.test(input) + || ABSOLUTE_UNC_PATH.test(input) + || /^~[\\/]/.test(input) + || /^(?:file:|\$(?:\{)?HOME|%(?:USERPROFILE|HOME)%)/i.test(input) + || input.split(/[\\/]+/).includes("..") + || SENSITIVE_PATH.test(input) + )) { + state.replacements += 1; + state.classes.add("sensitive-path"); + return "[REDACTED_PATH]"; + } + let value = input; + const patterns = [ + { className: "active-content", pattern: /<\/?(?:script|style|iframe|object|embed|link|meta)\b[^>]*>/gi, replacement: "[REDACTED_MARKUP]" }, + { className: "private-key", pattern: /-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" }, + { className: "authorization", pattern: /\bBearer\s+[A-Za-z0-9._~+\/-]+=*/gi, replacement: "Bearer [REDACTED]" }, + { className: "url-credentials", pattern: /([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi, replacement: "$1[REDACTED]@" }, + { className: "provider-token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|npm_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,}|AKIA[A-Z0-9]{16}|xox[baprs]-[A-Za-z0-9-]{20,})\b/g, replacement: "[REDACTED_TOKEN]" }, + { className: "assigned-secret", pattern: /((?:--)?(?:password|passwd|token|secret|api[_-]?key)\s*[=:]\s*)[^\s,;]+/gi, replacement: "$1[REDACTED]" }, + { className: "embedded-path", pattern: /\b[A-Za-z]:[\\/](?:[^\s,;<>"']+[\\/]?)+|\/(?:home|Users|root|tmp|var\/tmp)\/[^\s,;<>"']+/g, replacement: "[REDACTED_PATH]" }, + ]; + for (const entry of patterns) { + let count = 0; + value = value.replace(entry.pattern, (...args) => { + count += 1; + return typeof entry.replacement === "function" ? entry.replacement(...args) : entry.replacement.replace("$1", args[1] || ""); + }); + if (count > 0) { + state.replacements += count; + state.classes.add(entry.className); + } + } + return value; +} + +function redactNode(value, key, state) { + if (typeof value === "string") return redactString(value, key, state); + if (Array.isArray(value)) return value.map((entry) => redactNode(entry, key, state)); + if (!value || typeof value !== "object") return value; + return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [ + entryKey, + redactNode(entryValue, entryKey, state), + ])); +} + +function redactEvidenceValue(value) { + const state = { replacements: 0, classes: new Set() }; + return { + value: redactNode(value, "", state), + replacements: state.replacements, + classes: [...state.classes].sort(), + }; +} + +function redactEvidenceReceipt(receipt) { + const redacted = redactEvidenceValue(receipt); + return { + ...redacted.value, + redaction: { + schemaVersion: REDACTION_SCHEMA_VERSION, + applied: true, + replacements: redacted.replacements, + classes: redacted.classes, + canonicalReceiptModified: false, + rawPromptsIncluded: false, + rawLogsIncluded: false, + }, + integrity: { + ...redacted.value.integrity, + exportRedaction: { + applied: true, + canonicalLocalReceiptRequiredForVerification: true, + }, + }, + }; +} + +function writeTextAtomic(filePath, content) { + const temporaryPath = `${filePath}.${process.pid}.tmp`; + fs.writeFileSync(temporaryPath, content, { flag: "wx" }); + try { + fs.renameSync(temporaryPath, filePath); + } catch (error) { + fs.rmSync(temporaryPath, { force: true }); + throw error; + } +} + +function exportRedactedEvidence(found, options = {}) { + const receipt = redactEvidenceReceipt(buildEvidenceReceipt(found, options)); + const report = { ...buildOperatorReport(receipt), redaction: receipt.redaction }; + const absolutePaths = { + receiptJson: path.join(found.runRoot, "evidence-receipt.redacted.json"), + receiptMarkdown: path.join(found.runRoot, "evidence-receipt.redacted.md"), + reportJson: path.join(found.runRoot, "operator-report.redacted.json"), + html: path.join(found.runRoot, "operator-report.redacted.html"), + }; + writeJsonAtomic(absolutePaths.receiptJson, receipt); + writeTextAtomic(absolutePaths.receiptMarkdown, renderEvidenceReceiptMarkdown(receipt)); + writeJsonAtomic(absolutePaths.reportJson, report); + writeTextAtomic(absolutePaths.html, renderOperatorReportHtml(report)); + const paths = Object.fromEntries(Object.entries(absolutePaths).map(([name, filePath]) => [ + name, + normalizeSlashPath(path.relative(found.repoRoot, filePath)), + ])); + return { receipt, report, paths }; +} + +module.exports = { + REDACTION_SCHEMA_VERSION, + exportRedactedEvidence, + redactEvidenceReceipt, + redactEvidenceValue, +}; diff --git a/src/evidence/report.js b/src/evidence/report.js new file mode 100644 index 0000000..b378bea --- /dev/null +++ b/src/evidence/report.js @@ -0,0 +1,168 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { writeJsonAtomic } = require("../workflow/state"); +const { buildEvidenceReceipt } = require("./receipt"); + +const OPERATOR_REPORT_SCHEMA_VERSION = "operator-report/v1"; +const ACTIVE_TASK_STATUSES = new Set(["running", "verifying", "review-pending"]); + +function remainingAllocation(entry) { + return Math.max(0, entry.approved - entry.consumed); +} + +function groupControls(receipt) { + const controls = receipt.policy.controls && Array.isArray(receipt.policy.controls.controls) + ? receipt.policy.controls.controls + : []; + return { + enforced: controls.filter((entry) => entry.classification === "preventive"), + checked: controls.filter((entry) => entry.classification === "post-execution"), + observed: controls.filter((entry) => entry.classification === "imported"), + unavailable: controls.filter((entry) => entry.classification === "unavailable"), + claims: receipt.policy.controls ? receipt.policy.controls.claims : { + observedEvidenceIsPreventiveEnforcement: false, + providerExecutionSuppliesEnforcement: false, + preventiveControlAuthority: "none", + guardrailAuthority: "cewp-core-outside-provider-execution", + }, + }; +} + +function buildOperatorReport(receipt) { + if (!receipt || receipt.schemaVersion !== "evidence-receipt/v1") { + throw new Error("Operator report requires evidence-receipt/v1."); + } + const allocationEntries = receipt.budget.compliance.allocations; + const completedTasks = receipt.tasks.filter((task) => task.status === "completed").length; + const activeTasks = receipt.tasks.filter((task) => ACTIVE_TASK_STATUSES.has(task.status)).length; + return { + schemaVersion: OPERATOR_REPORT_SCHEMA_VERSION, + generatedAt: receipt.generatedAt, + runId: receipt.runId, + goal: receipt.goal, + completeness: receipt.completeness, + execution: receipt.execution, + progress: { + runStatus: receipt.completeness.runStatus, + totalTasks: receipt.tasks.length, + completedTasks, + activeTasks, + remainingTasks: receipt.tasks.length - completedTasks, + }, + planRevisions: receipt.planRevisions, + tasks: receipt.tasks, + checkpoints: receipt.checkpoints, + interventions: receipt.interventions, + values: { + observed: receipt.usage, + estimated: receipt.estimate, + budgeted: receipt.budget, + apiEquivalentCost: receipt.cost.apiEquivalent, + }, + reserves: { + absoluteCeilingRespected: receipt.budget.compliance.absoluteCeilingRespected, + protectedAllocationsRespected: receipt.budget.compliance.protectedAllocationsRespected, + allocations: allocationEntries.map((entry) => ({ ...entry, remaining: remainingAllocation(entry) })), + }, + pauseAndRecovery: { + pauseReason: receipt.budget.pauseReason, + resumeStatus: receipt.budget.resumeStatus, + thresholdEvents: receipt.budget.thresholdEvents, + interventions: receipt.interventions, + failures: receipt.tasks.flatMap((task) => task.failure ? [{ taskId: task.id, ...task.failure }] : []), + }, + controls: groupControls(receipt), + finalReview: receipt.reviewer, + reviews: receipt.reviews, + warnings: receipt.warnings, + integrity: { + claim: receipt.integrity.claim, + tamperProof: receipt.integrity.tamperProof, + fileCount: receipt.integrity.files.length, + }, + }; +} + +function escapeHtml(value) { + return String(value === null || value === undefined ? "unknown" : value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function truthText(value) { + if (!value || value.label === "unknown") return "unknown"; + return `${value.label}: ${value.value}`; +} + +function list(items, render, empty = "None") { + return items.length > 0 + ? `
    ${items.map((item) => `
  • ${render(item)}
  • `).join("")}
` + : `

${escapeHtml(empty)}

`; +} + +function renderOperatorReportHtml(report) { + if (!report || report.schemaVersion !== OPERATOR_REPORT_SCHEMA_VERSION) { + throw new Error(`Operator HTML requires ${OPERATOR_REPORT_SCHEMA_VERSION}.`); + } + const allocationRows = report.reserves.allocations.map((entry) => `${escapeHtml(entry.name)}${entry.approved}${entry.consumed}${entry.remaining}${entry.protected ? "yes" : "no"}${entry.respected ? "passed" : "failed"}`).join(""); + const taskRows = report.tasks.map((task) => `${escapeHtml(task.id)}${escapeHtml(task.status)}${task.attempts}${escapeHtml(task.scopeVerdict.status)}${escapeHtml(task.changedFiles.join(", ") || "none")}`).join(""); + const controlItems = (label, entries) => `

${label}

${list(entries, (entry) => `${escapeHtml(entry.name)} (${escapeHtml(entry.effect)})`)}`; + return ` + + + + + +CEWP Operator Report - ${escapeHtml(report.runId)} + + + +

CEWP Operator Report

+

Run: ${escapeHtml(report.runId)}
Goal: ${escapeHtml(report.goal)}
Status: ${escapeHtml(report.progress.runStatus)} (${escapeHtml(report.completeness.status)})
Execution: ${escapeHtml(report.execution.owner)} / ${escapeHtml(report.execution.backend)}

+

Progress

Tasks: ${report.progress.completedTasks}/${report.progress.totalTasks} complete
Active: ${report.progress.activeTasks}
Remaining: ${report.progress.remainingTasks}
${taskRows}
TaskStatusAttemptsScopeChanged files
+

Plan revisions

${list(report.planRevisions, (entry) => `Revision ${escapeHtml(entry.revision)}: ${escapeHtml(entry.reason || entry.digest)}`)}
+

Checkpoint evidence

${list(report.checkpoints, (entry) => `${escapeHtml(entry.checkpointId)}: ${escapeHtml(entry.status)}; task ${escapeHtml(entry.taskId)}; baseline ${escapeHtml(entry.verification && entry.verification.baseline && entry.verification.baseline.status)}`)}
+

Interventions and recovery

${list(report.interventions, (entry) => `${escapeHtml(entry.event)}: ${escapeHtml(entry.reason)}`)}

Pause: ${escapeHtml(report.pauseAndRecovery.pauseReason)}; resume state: ${escapeHtml(report.pauseAndRecovery.resumeStatus)}

+

Observed, estimated, budgeted, and unknown

Observed managed operations: ${escapeHtml(truthText(report.values.observed.managedOperations))}
Observed host usage: ${escapeHtml(truthText(report.values.observed.hostInternal))}
Estimate: ${escapeHtml(report.values.estimated.label)} (${escapeHtml(report.values.estimated.confidence)})
API-equivalent cost: ${escapeHtml(report.values.apiEquivalentCost.label)}
+

Protected reserves

Absolute ceiling: ${report.reserves.absoluteCeilingRespected ? "passed" : "failed"}; protected allocations: ${report.reserves.protectedAllocationsRespected ? "passed" : "failed"}

${allocationRows}
AllocationApprovedConsumedRemainingProtectedVerdict
+

Controls

${controlItems("Preventively enforced", report.controls.enforced)}${controlItems("Post-execution checked", report.controls.checked)}${controlItems("Observed, not enforced", report.controls.observed)}${controlItems("Unavailable", report.controls.unavailable)}
+

Final review

Status: ${escapeHtml(report.finalReview.status)}; decision: ${escapeHtml(report.finalReview.decision)}

+

Warnings

${list(report.warnings, (entry) => `${escapeHtml(entry.code)}: ${escapeHtml(entry.message)}`)}
+

Generated ${escapeHtml(report.generatedAt)}. Integrity: ${escapeHtml(report.integrity.claim)}; not tamper-proof. Redaction: ${report.redaction && report.redaction.applied ? `applied (${report.redaction.replacements} replacements)` : "not applied"}. This offline report excludes raw prompts and logs.

+ + +`; +} + +function writeTextAtomic(filePath, content) { + const temporaryPath = `${filePath}.${process.pid}.tmp`; + fs.writeFileSync(temporaryPath, content, { flag: "wx" }); + try { + fs.renameSync(temporaryPath, filePath); + } catch (error) { + fs.rmSync(temporaryPath, { force: true }); + throw error; + } +} + +function writeOperatorReport(found, options = {}) { + const receipt = buildEvidenceReceipt(found, options); + const report = buildOperatorReport(receipt); + const jsonPath = path.join(found.runRoot, "operator-report.json"); + const htmlPath = path.join(found.runRoot, "operator-report.html"); + writeJsonAtomic(jsonPath, report); + writeTextAtomic(htmlPath, renderOperatorReportHtml(report)); + return { report, paths: { json: jsonPath, html: htmlPath } }; +} + +module.exports = { + OPERATOR_REPORT_SCHEMA_VERSION, + buildOperatorReport, + renderOperatorReportHtml, + writeOperatorReport, +}; diff --git a/src/evidence/usage.js b/src/evidence/usage.js new file mode 100644 index 0000000..050aa52 --- /dev/null +++ b/src/evidence/usage.js @@ -0,0 +1,139 @@ +"use strict"; + +const { readHostObservations } = require("../integration/observation"); + +const USAGE_OBSERVATION_SCHEMA_VERSION = "usage-observation/v1"; +const USAGE_ESTIMATE_SCHEMA_VERSION = "usage-estimate/v1"; +const RESULT_CATEGORIES = Object.freeze([ + "managedOperations", + "capturedOutputBytes", + "managedTokens", + "hostInternal", +]); + +function sourceBoundary(source) { + if (typeof source !== "string") return "unknown"; + if (source.startsWith("codex-exec")) return "managed-child-process"; + if (source.startsWith("cewp-")) return "cewp-core-local"; + return "unknown"; +} + +function resultObservations(kind, values) { + return values.flatMap((value) => RESULT_CATEGORIES.map((category) => { + const truth = value.usage[category]; + const sourceId = truth.source || `${kind}-contract`; + return { + schemaVersion: USAGE_OBSERVATION_SCHEMA_VERSION, + observationId: `${kind}:${kind === "task-result" ? value.resultId : value.reviewId}:${category}`, + observedAt: value.completedAt, + scope: { + runId: value.runId, + taskId: value.taskId || (value.scope && value.scope.taskId) || null, + checkpointId: value.checkpointId || (value.scope && value.scope.checkpointId) || null, + }, + category, + rawCategory: `usage.${category}`, + availability: truth.label === "observed" ? "observed" : "unknown", + evidenceClass: truth.label === "observed" ? "observed" : "unknown", + value: truth.value, + reason: truth.reason || null, + source: { + kind, + id: sourceId, + schemaVersion: value.schemaVersion, + authenticationBoundary: sourceBoundary(truth.source), + }, + effectiveModel: { status: "unknown", value: null, reason: "result contract does not expose effective model" }, + rawValue: truth, + }; + })); +} + +function hostObservations(found, warnings) { + let entries; + try { + entries = readHostObservations(found); + } catch (error) { + warnings.push({ code: "malformed-usage-observation-ledger", message: error.message }); + return []; + } + return entries.map((entry) => ({ + schemaVersion: USAGE_OBSERVATION_SCHEMA_VERSION, + observationId: `host:${entry.observationId}`, + observedAt: entry.observedAt, + scope: { + runId: entry.scope.runId, + taskId: entry.scope.taskId, + checkpointId: entry.scope.checkpointId, + }, + category: entry.category, + rawCategory: entry.rawCategory, + availability: entry.availability, + evidenceClass: entry.evidenceClass, + value: entry.data, + reason: entry.reason, + source: { + kind: "host-observation", + id: entry.source.path, + schemaVersion: entry.source.schemaVersion, + codexVersion: entry.source.codexVersion, + authenticationBoundary: entry.source.authenticationBoundary, + }, + effectiveModel: { status: "unknown", value: null, reason: "host observation does not expose effective model" }, + rawValue: null, + billingImpact: entry.billingImpact, + })); +} + +function buildUsageObservations(found, results, reviews, warnings = []) { + return [ + ...resultObservations("task-result", results), + ...resultObservations("review-result", reviews), + ...hostObservations(found, warnings), + ].sort((left, right) => left.observationId < right.observationId ? -1 : left.observationId > right.observationId ? 1 : 0); +} + +function unknownUsageEstimate(reason = "At least five comparable local runs with known model and effort are required.") { + return { + schemaVersion: USAGE_ESTIMATE_SCHEMA_VERSION, + label: "unknown", + range: null, + confidence: "unavailable", + estimator: { + version: "local-history/v1", + method: "comparable-run-interval", + minimumSampleCount: 5, + groupingDimensions: [ + "taskClass", "effectiveModel", "effectiveEffort", "assurance", "repositorySizeBucket", + "checkpointCount", "workerReviewerShape", "repairs", "verificationSchedule", + ], + }, + sampleBasis: { + count: 0, + comparableRunIds: [], + localOnly: true, + promptsStored: false, + sourceStored: false, + rawLogsStored: false, + }, + calibrationSnapshot: { + id: null, + createdAt: null, + intervalCoverage: null, + absoluteError: null, + }, + drift: { + state: "unknown", + checkedAt: null, + changedDimensions: [], + }, + reason, + }; +} + +module.exports = { + USAGE_ESTIMATE_SCHEMA_VERSION, + USAGE_OBSERVATION_SCHEMA_VERSION, + buildUsageObservations, + unknownUsageEstimate, +}; diff --git a/src/evidence/verify.js b/src/evidence/verify.js new file mode 100644 index 0000000..5c0ff8c --- /dev/null +++ b/src/evidence/verify.js @@ -0,0 +1,191 @@ +"use strict"; + +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const path = require("node:path"); +const { normalizeSlashPath } = require("../lib/paths"); +const { loadWorkflowRun } = require("../workflow/state"); +const { parseLifecycleEvents } = require("./events"); +const { buildEvidenceReceipt } = require("./receipt"); + +const RUN_VERIFICATION_SCHEMA_VERSION = "run-verification/v1"; + +function sha256(filePath) { + return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`; +} + +function isInside(parentPath, childPath) { + const relative = path.relative(path.resolve(parentPath), path.resolve(childPath)); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} + +function check(id, issues, startIndex, skipped = false) { + const ownIssues = issues.slice(startIndex); + return { + id, + status: skipped ? "not-applicable" : ownIssues.some((entry) => entry.severity === "error") ? "failed" : "passed", + issueCount: ownIssues.length, + }; +} + +function issue(issues, code, message, details = {}) { + issues.push({ severity: "error", code, message, ...details }); +} + +function readJsonRecords(rootPath, issues, code) { + if (!fs.existsSync(rootPath)) return []; + return fs.readdirSync(rootPath, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith(".json")) + .map((entry) => { + const filePath = path.join(rootPath, entry.name); + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")); + } catch (error) { + issue(issues, code, `Artifact is not valid JSON: ${normalizeSlashPath(path.relative(rootPath, filePath))}: ${error.message}`); + return null; + } + }) + .filter(Boolean); +} + +function verifyReceipt(found, issues) { + const receiptPath = path.join(found.runRoot, "evidence-receipt.json"); + if (!fs.existsSync(receiptPath)) { + if (found.run.status === "finalized") { + issue(issues, "missing-evidence-receipt", "Finalized workflow run has no evidence receipt."); + } + return false; + } + let receipt; + try { + receipt = JSON.parse(fs.readFileSync(receiptPath, "utf8")); + } catch (error) { + issue(issues, "malformed-evidence-receipt", `Evidence receipt is not valid JSON: ${error.message}`); + return true; + } + if (receipt.schemaVersion !== "evidence-receipt/v1" || receipt.runId !== found.run.runId) { + issue(issues, "incompatible-evidence-receipt", "Evidence receipt schema or run identity is incompatible."); + return true; + } + const files = receipt.integrity && receipt.integrity.files; + if (!Array.isArray(files)) { + issue(issues, "malformed-evidence-receipt", "Evidence receipt integrity inventory is missing."); + return true; + } + try { + const rebuilt = buildEvidenceReceipt(found, { generatedAt: receipt.generatedAt }); + if (JSON.stringify(rebuilt.integrity) !== JSON.stringify(receipt.integrity)) { + issue(issues, "receipt-integrity-inventory-mismatch", "Receipt integrity inventory does not match current canonical evidence."); + } + } catch (error) { + issue(issues, "receipt-integrity-rebuild-failed", `Could not rebuild canonical receipt integrity: ${error.message}`); + } + for (const entry of files) { + const filePath = typeof entry.path === "string" ? path.resolve(found.repoRoot, entry.path) : ""; + if (!filePath || !isInside(found.repoRoot, filePath) || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + issue(issues, "receipt-integrity-missing", `Receipt evidence is unavailable: ${entry.path || "missing path"}.`, { path: entry.path || null }); + continue; + } + const actual = sha256(filePath); + const bytes = fs.statSync(filePath).size; + if (actual !== entry.sha256 || bytes !== entry.bytes) { + issue(issues, "receipt-integrity-mismatch", `Receipt evidence changed: ${entry.path}.`, { + path: entry.path, + expectedSha256: entry.sha256, + actualSha256: actual, + }); + } + } + return true; +} + +function verifyBoundWorktree(found, issues) { + const bindingPath = path.join(found.runRoot, "integration", "host-binding.json"); + if (!fs.existsSync(bindingPath)) return false; + let binding; + try { + binding = JSON.parse(fs.readFileSync(bindingPath, "utf8")); + } catch (error) { + issue(issues, "malformed-host-binding", `Host binding is not valid JSON: ${error.message}`); + return true; + } + const worktree = binding.references && binding.references.worktree; + if (worktree && typeof worktree.path === "string" && !fs.existsSync(path.resolve(found.repoRoot, worktree.path))) { + issue(issues, "stale-worktree", `Bound worktree is unavailable: ${normalizeSlashPath(worktree.path)}.`, { + worktreeId: worktree.id || null, + }); + } + return true; +} + +function verifyWorkflowRun(repoRoot, runId) { + const issues = []; + const checks = []; + let found; + let start = issues.length; + try { + found = loadWorkflowRun(repoRoot, runId); + } catch (error) { + issue(issues, "state-inconsistent", error.message); + } + checks.push(check("state-consistency", issues, start)); + if (!found) { + return { + schemaVersion: RUN_VERIFICATION_SCHEMA_VERSION, + runId, + status: "failed", + checks, + issues, + execution: { agentsExecuted: false, verificationCommandsExecuted: false }, + }; + } + + start = issues.length; + const eventsPath = path.join(found.runRoot, "events.jsonl"); + if (!fs.existsSync(eventsPath)) { + issue(issues, "missing-events", "Workflow event ledger is missing."); + } else { + const parsed = parseLifecycleEvents(fs.readFileSync(eventsPath, "utf8"), { runId }); + for (const entry of parsed.issues) issue(issues, entry.code, entry.message, { line: entry.line }); + } + checks.push(check("event-ledger", issues, start)); + + start = issues.length; + for (const task of found.run.tasks) { + if (task.resultId) { + const results = readJsonRecords(path.join(found.runRoot, "results", task.id), issues, "malformed-task-result"); + if (!results.some((entry) => entry.resultId === task.resultId)) { + issue(issues, "missing-task-result", `Task ${task.id} references result ${task.resultId}, but that result is missing.`, { taskId: task.id }); + } + } + if (task.activeCheckpointId) { + const checkpoints = readJsonRecords(path.join(found.runRoot, "checkpoints", task.id), issues, "malformed-checkpoint"); + if (!checkpoints.some((entry) => entry.checkpointId === task.activeCheckpointId)) { + issue(issues, "missing-checkpoint", `Task ${task.id} active checkpoint ${task.activeCheckpointId} is missing.`, { taskId: task.id }); + } + } + } + checks.push(check("required-artifacts", issues, start)); + + start = issues.length; + const hadBinding = verifyBoundWorktree(found, issues); + checks.push(check("worktree-liveness", issues, start, !hadBinding)); + + start = issues.length; + const hadReceipt = verifyReceipt(found, issues); + checks.push(check("receipt-integrity", issues, start, !hadReceipt)); + + return { + schemaVersion: RUN_VERIFICATION_SCHEMA_VERSION, + runId, + status: issues.some((entry) => entry.severity === "error") ? "failed" : "passed", + checks, + issues, + execution: { agentsExecuted: false, verificationCommandsExecuted: false }, + }; +} + +module.exports = { + RUN_VERIFICATION_SCHEMA_VERSION, + verifyWorkflowRun, +}; 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..0ca75c2 --- /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 { appendLifecycleEvent } = require("../evidence/events"); +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, + }); + appendLifecycleEvent(found.runRoot, { + timestamp: approvedAt, + type: "hook-evidence-approved", + runId: found.run.runId, + revision: found.run.workflow.revision, + actor: "operator", + approvalDigest: trust.approval.digest, + }); + 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/integration/observation.js b/src/integration/observation.js index 99e3f15..40b7fbf 100644 --- a/src/integration/observation.js +++ b/src/integration/observation.js @@ -2,6 +2,7 @@ const fs = require("node:fs"); const path = require("node:path"); +const { appendLifecycleEvent } = require("../evidence/events"); const { validateCodexCapabilitySnapshot } = require("./capabilities"); const HOST_OBSERVATION_SCHEMA_VERSION = "host-observation/v1"; @@ -353,6 +354,16 @@ function recordHostObservation(found, candidate, options = {}) { const filePath = ledgerPath(found); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.appendFileSync(filePath, `${JSON.stringify(observation)}\n`); + appendLifecycleEvent(found.runRoot, { + timestamp: observation.observedAt, + type: "usage-observed", + runId: found.run.runId, + observationId: observation.observationId, + usageCategory: observation.category, + availability: observation.availability, + evidenceClass: observation.evidenceClass, + actor: observation.source.path, + }); return observation; } diff --git a/src/lib/git.js b/src/lib/git.js index 006c74f..4743cb0 100644 --- a/src/lib/git.js +++ b/src/lib/git.js @@ -21,14 +21,24 @@ function getGitOutput(args, cwd) { return result; } -function isRepoDirty(repoRoot) { - const result = getGitOutput(["status", "--porcelain"], repoRoot); +function isUntrackedCewpRuntimeEntry(line) { + if (!line.startsWith("?? ")) return false; + const repoPath = line.slice(3).replace(/^"|"$/g, "").replace(/\\/g, "/"); + return repoPath === ".cewp" || repoPath.startsWith(".cewp/"); +} + +function isRepoDirty(repoRoot, options = {}) { + const result = getGitOutput(["status", "--porcelain=v1", "--untracked-files=all"], repoRoot); if (result.status !== 0) { throw new Error(`Failed to inspect git status: ${(result.stderr || result.stdout || "").trim()}`); } - return result.stdout.trim().length > 0; + const entries = result.stdout.split(/\r?\n/).filter(Boolean); + const relevant = options.ignoreUntrackedCewpRuntime === true + ? entries.filter((line) => !isUntrackedCewpRuntimeEntry(line)) + : entries; + return relevant.length > 0; } function branchExists(repoRoot, branch) { 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/performance/budgets.js b/src/performance/budgets.js new file mode 100644 index 0000000..c49fbaa --- /dev/null +++ b/src/performance/budgets.js @@ -0,0 +1,52 @@ +"use strict"; + +const PERFORMANCE_BUDGET_SCHEMA_VERSION = "performance-budget/v1"; + +function buildPerformanceBudgets() { + return { + schemaVersion: PERFORMANCE_BUDGET_SCHEMA_VERSION, + scope: "cewp-controlled-overhead-only", + exclusions: [ + "model-execution", + "network-latency", + "host-queue-time", + "repository-verification-command", + ], + operations: { + "doctor-json": { ceilingMs: 2000 }, + "workflow-validation": { ceilingMs: 1000 }, + "status-progress": { ceilingMs: 1000 }, + "checkpoint-bookkeeping": { ceilingMs: 1000 }, + "package-lifecycle": { ceilingMs: 30000 }, + "large-run-inspection": { ceilingMs: 3000 }, + }, + reporting: { + requiredStatistics: ["repetitions", "medianMs", "upperQuantileMs", "maximumMs"], + requiredEnvironment: ["os", "node", "git", "packageVersion", "fixtureShape"], + modelAndUsageReportedSeparately: true, + pricingSnapshotRequiresDate: true, + unknownHostUsageRemainsUnknown: true, + }, + }; +} + +function evaluatePerformanceMeasurement(policy, measurement) { + const budget = policy.operations[measurement.operation]; + if (!budget) throw new Error(`Unknown performance operation: ${measurement.operation || "missing"}.`); + if (!Number.isFinite(measurement.durationMs) || measurement.durationMs < 0 + || !Number.isFinite(measurement.excludedDurationMs) || measurement.excludedDurationMs < 0 + || measurement.excludedDurationMs > measurement.durationMs) { + return { status: "invalid-measurement", releaseBlocking: true }; + } + const ownedDurationMs = measurement.durationMs - measurement.excludedDurationMs; + const withinBudget = ownedDurationMs <= budget.ceilingMs; + return { + status: withinBudget ? "within-budget" : "over-budget", + releaseBlocking: !withinBudget, + operation: measurement.operation, + ownedDurationMs, + ceilingMs: budget.ceilingMs, + }; +} + +module.exports = { PERFORMANCE_BUDGET_SCHEMA_VERSION, buildPerformanceBudgets, evaluatePerformanceMeasurement }; diff --git a/src/pilot/cli.js b/src/pilot/cli.js new file mode 100644 index 0000000..97ad647 --- /dev/null +++ b/src/pilot/cli.js @@ -0,0 +1,79 @@ +"use strict"; + +const { createPilotRecord } = require("./record"); +const { derivePilotStatus } = require("./status"); +const { recordPilotObservation } = require("./observation"); +const { exportPilotEvidence } = require("./export"); + +function outputJson(command, data) { + console.log(JSON.stringify({ + schemaVersion: "operator-json/v1", + command, + generatedAt: new Date().toISOString(), + data, + warnings: [], + }, null, 2)); +} + +function runPilot(options = {}) { + if (options.subcommand === "export") { + const result = exportPilotEvidence(process.cwd(), options.pilotId); + if (options.json) outputJson("pilot.export", result); + else { + console.log("CEWP redacted pilot export written"); + console.log(`Scope: ${result.export.scope.pilotId || "all"}`); + console.log(`JSON: ${result.paths.json}`); + console.log(`Markdown: ${result.paths.markdown}`); + } + return; + } + if (options.subcommand === "record") { + if (!options.yes) throw new Error("pilot record requires --yes confirmation."); + const result = recordPilotObservation({ + repoRoot: process.cwd(), + pilotId: options.pilotId, + fromFile: options.fromFile, + }); + if (options.json) outputJson("pilot.record", result); + else { + console.log("CEWP pilot observation recorded"); + console.log(`Pilot ID: ${result.record.pilotId}`); + console.log(`Observation: ${result.observation.id} (${result.observation.type})`); + console.log(`Independent evidence eligible: ${result.observation.qualification.eligible ? "yes" : "no"}`); + } + return; + } + if (options.subcommand === "status") { + const status = derivePilotStatus(process.cwd()); + if (options.json) outputJson("pilot.status", status); + else { + console.log("CEWP Phase 13 pilot status"); + console.log(`Complete: ${status.complete ? "yes" : "no"}`); + console.log(`Independent participants: ${status.participants.independentExternal}/3`); + for (const gate of status.gates) { + console.log(`${gate.id}: ${gate.observed}/${gate.threshold} (${gate.status})`); + } + } + if (!status.complete) process.exitCode = 1; + return; + } + if (options.subcommand !== "create") { + throw new Error(`Unsupported pilot command: ${options.subcommand || "missing"}.`); + } + const record = createPilotRecord({ + repoRoot: process.cwd(), + pilotId: options.pilotId, + participant: options.participant, + participantId: options.participantId, + }); + if (options.json) outputJson("pilot.create", record); + else { + console.log("CEWP local pilot record created"); + console.log(`Pilot ID: ${record.pilotId}`); + console.log(`Participant: ${record.participant.classification}`); + } +} + +module.exports = { + runPilot, +}; diff --git a/src/pilot/evidence.js b/src/pilot/evidence.js new file mode 100644 index 0000000..86b7c05 --- /dev/null +++ b/src/pilot/evidence.js @@ -0,0 +1,84 @@ +"use strict"; + +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const path = require("node:path"); +const { verifyWorkflowRun } = require("../evidence/verify"); +const { loadWorkflowRun } = require("../workflow/state"); + +function sha256(filePath) { + return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`; +} + +function inspectReviewedRunEvidence(repoRoot, workflowRunId) { + let found; + try { + found = loadWorkflowRun(repoRoot, workflowRunId); + } catch (error) { + return { + status: "excluded", + reason: `workflow run is unavailable: ${error.message}`, + workflowRunId, + verification: { schemaVersion: "run-verification/v1", status: "failed", issues: [{ code: "run-unavailable", message: error.message }] }, + receipt: null, + rawEvidenceCopied: false, + }; + } + const verification = verifyWorkflowRun(repoRoot, workflowRunId); + const receiptPath = path.join(found.runRoot, "evidence-receipt.json"); + let receipt = null; + if (fs.existsSync(receiptPath)) { + try { + const value = JSON.parse(fs.readFileSync(receiptPath, "utf8")); + receipt = { + schemaVersion: value.schemaVersion || null, + generatedAt: value.generatedAt || null, + completeness: value.completeness?.status || "unknown", + integrityClaim: value.integrity?.claim || null, + sha256: sha256(receiptPath), + }; + } catch (error) { + receipt = { schemaVersion: null, generatedAt: null, completeness: "malformed", integrityClaim: null, sha256: sha256(receiptPath) }; + } + } + const reviewsRoot = path.join(found.runRoot, "reviews"); + const reviews = fs.existsSync(reviewsRoot) + ? fs.readdirSync(reviewsRoot, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith(".json")) + .map((entry) => { + try { + return JSON.parse(fs.readFileSync(path.join(reviewsRoot, entry.name), "utf8")); + } catch (error) { + return null; + } + }) + .filter(Boolean) + : []; + const independentPass = reviews.some((review) => review.independent === true && review.decision === "PASS"); + let reason = null; + if (found.run.status !== "finalized") reason = `workflow run is not finalized (status ${found.run.status})`; + else if (!receipt) reason = "workflow run has no persisted evidence receipt"; + else if (receipt.schemaVersion !== "evidence-receipt/v1" || receipt.completeness !== "complete") reason = "workflow receipt is incomplete or incompatible"; + else if (verification.status !== "passed") reason = "workflow run verification failed"; + else if (found.run.reviewer?.decision !== "PASS" || !independentPass) reason = "workflow run lacks an independent reviewer PASS"; + return { + status: reason ? "excluded" : "qualified", + reason, + workflowRunId, + verification: { + schemaVersion: verification.schemaVersion, + status: verification.status, + issues: verification.issues.map((issue) => ({ code: issue.code, message: issue.message })), + }, + receipt, + reviewer: { + decision: found.run.reviewer?.decision || null, + independentPass, + }, + rawEvidenceCopied: false, + }; +} + +module.exports = { + inspectReviewedRunEvidence, +}; diff --git a/src/pilot/export.js b/src/pilot/export.js new file mode 100644 index 0000000..b3749eb --- /dev/null +++ b/src/pilot/export.js @@ -0,0 +1,145 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { redactEvidenceValue, REDACTION_SCHEMA_VERSION } = require("../evidence/redaction"); +const { ensureDir } = require("../lib/fs"); +const { normalizeSlashPath } = require("../lib/paths"); +const { writeJsonAtomic } = require("../workflow/state"); +const { validatePilotId } = require("./record"); +const { derivePilotStatus, loadPilotRecords } = require("./status"); + +const PILOT_EXPORT_SCHEMA_VERSION = "pilot-export/v1"; + +function isInside(parentPath, childPath) { + const relative = path.relative(path.resolve(parentPath), path.resolve(childPath)); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} + +function prepareContainedExportRoot(repoRoot, exportRoot) { + const resolvedRepo = path.resolve(repoRoot); + const resolvedExport = path.resolve(exportRoot); + if (!isInside(resolvedRepo, resolvedExport)) throw new Error("Pilot export root must stay inside the repository."); + const relativeParts = path.relative(resolvedRepo, resolvedExport).split(path.sep).filter(Boolean); + let cursor = resolvedRepo; + for (const part of relativeParts) { + cursor = path.join(cursor, part); + if (!fs.existsSync(cursor)) break; + if (fs.lstatSync(cursor).isSymbolicLink()) { + throw new Error(`Pilot export refuses a symbolic link in its output path: ${normalizeSlashPath(path.relative(resolvedRepo, cursor))}.`); + } + } + ensureDir(resolvedExport); + const realRepo = fs.realpathSync(resolvedRepo); + const realExport = fs.realpathSync(resolvedExport); + if (!isInside(realRepo, realExport)) throw new Error("Pilot export root resolves outside the repository."); +} + +function writeTextAtomic(filePath, value) { + ensureDir(path.dirname(filePath)); + const temporaryPath = `${filePath}.${process.pid}.tmp`; + fs.writeFileSync(temporaryPath, value, { flag: "wx" }); + try { + fs.renameSync(temporaryPath, filePath); + } catch (error) { + fs.rmSync(temporaryPath, { force: true }); + throw error; + } +} + +function renderPilotExportMarkdown(pilotExport) { + const lines = [ + "# CEWP Pilot Export", + "", + `Generated: ${pilotExport.generatedAt}`, + `Scope: ${pilotExport.scope.pilotId || "all local pilot records"}`, + `Phase 13 complete: ${pilotExport.status.complete ? "yes" : "no"}`, + "", + "## Privacy", + "", + `Redaction policy: ${pilotExport.redaction.schemaVersion}`, + `Replacements: ${pilotExport.redaction.replacements}`, + `Classes: ${pilotExport.redaction.classes.join(", ") || "none"}`, + "Raw prompts: excluded", + "Raw logs: excluded", + "Source code: excluded", + "", + "## Gates", + "", + "| Gate | Observed | Required | Status |", + "| --- | ---: | ---: | --- |", + ...pilotExport.status.gates.map((gate) => `| ${gate.id} | ${gate.observed} | ${gate.threshold} | ${gate.status} |`), + "", + "## Records", + "", + ]; + for (const record of pilotExport.records) { + lines.push(`### ${record.pilotId}`); + lines.push(""); + lines.push(`Participant classification: ${record.participant.classification}`); + lines.push(`Observation types: ${(record.observations || []).map((entry) => entry.type).join(", ") || "none"}`); + lines.push(""); + } + return `${lines.join("\n").trimEnd()}\n`; +} + +function buildPilotExport(repoRoot, pilotId, options = {}) { + const selectedId = pilotId ? validatePilotId(pilotId) : null; + const records = loadPilotRecords(repoRoot); + const selected = selectedId ? records.filter((record) => record.pilotId === selectedId) : records; + if (selectedId && selected.length === 0) throw new Error(`Pilot record not found: ${selectedId}.`); + const base = { + schemaVersion: PILOT_EXPORT_SCHEMA_VERSION, + generatedAt: (options.now || new Date()).toISOString(), + scope: { pilotId: selectedId, recordCount: selected.length }, + records: selected, + status: derivePilotStatus(repoRoot), + privacy: { + localCanonicalRootIncluded: false, + rawPromptsIncluded: false, + rawLogsIncluded: false, + sourceCodeIncluded: false, + authenticationMaterialIncluded: false, + patternRedactionIsProofOfNoSecrets: false, + }, + }; + const redacted = redactEvidenceValue(base); + return { + ...redacted.value, + redaction: { + schemaVersion: REDACTION_SCHEMA_VERSION, + applied: true, + replacements: redacted.replacements, + classes: redacted.classes, + canonicalRecordsModified: false, + }, + }; +} + +function exportPilotEvidence(repoRoot, pilotId, options = {}) { + const pilotExport = buildPilotExport(repoRoot, pilotId, options); + const exportId = pilotExport.scope.pilotId || "phase-13"; + const exportRoot = path.join(path.resolve(repoRoot), ".cewp", "pilot-exports", exportId); + const absolutePaths = { + json: path.join(exportRoot, "pilot-export.json"), + markdown: path.join(exportRoot, "pilot-export.md"), + }; + prepareContainedExportRoot(repoRoot, exportRoot); + writeJsonAtomic(absolutePaths.json, pilotExport); + writeTextAtomic(absolutePaths.markdown, renderPilotExportMarkdown(pilotExport)); + return { + export: pilotExport, + paths: Object.fromEntries(Object.entries(absolutePaths).map(([name, filePath]) => [ + name, + normalizeSlashPath(path.relative(path.resolve(repoRoot), filePath)), + ])), + }; +} + +module.exports = { + PILOT_EXPORT_SCHEMA_VERSION, + buildPilotExport, + exportPilotEvidence, + prepareContainedExportRoot, + renderPilotExportMarkdown, +}; diff --git a/src/pilot/observation.js b/src/pilot/observation.js new file mode 100644 index 0000000..91a2607 --- /dev/null +++ b/src/pilot/observation.js @@ -0,0 +1,398 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { readRepoJson } = require("../workflow/source"); +const { writeJsonAtomic } = require("../workflow/state"); +const { PILOT_RECORD_SCHEMA_VERSION, validatePilotId } = require("./record"); +const { inspectReviewedRunEvidence } = require("./evidence"); + +const PILOT_OBSERVATION_SCHEMA_VERSION = "pilot-observation/v1"; +const SAFE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/; +const OBSERVATION_TYPES = new Set([ + "repository-attempt", + "golden-path-complete", + "bounded-external-task", + "repeat-user", + "native-goal-comparison", + "measurable-benefit", + "recovery", + "operational-budget-exhaustion", + "controlled-host-limit", + "onboarding-remediation", + "external-contribution", + "public-case-study", + "estimate-calibration-report", + "guardrail-audit-pass", + "full-reviewed-run", +]); +const ATTEMPT_ENUMS = Object.freeze({ + sizeBucket: new Set(["small", "medium", "large", "unknown"]), + operatingSystem: new Set(["windows", "linux", "macos", "other"]), + inputKind: new Set(["direct-goal", "issue", "prd", "plan", "progress"]), + riskLevel: new Set(["low", "medium", "high"]), + mode: new Set(["supervised", "autonomous", "audit-only"]), +}); + +function requireSafeId(value, label) { + const normalized = String(value || "").trim(); + if (!SAFE_ID.test(normalized)) throw new Error(`${label} requires a privacy-safe lowercase id.`); + return normalized; +} + +function requireLabel(value, label) { + const normalized = String(value || "").trim().toLowerCase(); + if (!/^[a-z0-9][a-z0-9.+#-]{0,31}$/.test(normalized)) { + throw new Error(`${label} requires a bounded privacy-safe label.`); + } + return normalized; +} + +function requireEnum(value, name) { + if (!ATTEMPT_ENUMS[name].has(value)) { + throw new Error(`pilot observation attempt.${name} is unsupported.`); + } + return value; +} + +function normalizeObservedAt(value) { + if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(value) || Number.isNaN(Date.parse(value))) { + throw new Error("pilot observation observedAt requires an ISO-8601 timestamp."); + } + return new Date(value).toISOString(); +} + +function validateRepositoryAttempt(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("repository-attempt observation requires attempt metadata."); + } + return { + id: requireSafeId(value.id, "pilot observation attempt.id"), + repositoryId: requireSafeId(value.repositoryId, "pilot observation attempt.repositoryId"), + language: requireLabel(value.language, "pilot observation attempt.language"), + sizeBucket: requireEnum(value.sizeBucket, "sizeBucket"), + operatingSystem: requireEnum(value.operatingSystem, "operatingSystem"), + testStack: requireLabel(value.testStack, "pilot observation attempt.testStack"), + inputKind: requireEnum(value.inputKind, "inputKind"), + riskLevel: requireEnum(value.riskLevel, "riskLevel"), + mode: requireEnum(value.mode, "mode"), + }; +} + +function validateGoldenPath(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("golden-path-complete observation requires completion metadata."); + } + if (value.supervised !== true || value.completed !== true || value.participantConfirmed !== true) { + throw new Error("golden-path-complete requires supervised, completed, and participantConfirmed to be true."); + } + if (!Number.isFinite(value.firstApprovalMinutes) || value.firstApprovalMinutes < 0 || value.firstApprovalMinutes > 300) { + throw new Error("golden-path-complete requires bounded firstApprovalMinutes."); + } + return { + supervised: true, + completed: true, + participantConfirmed: true, + firstApprovalMinutes: value.firstApprovalMinutes, + }; +} + +function validateBoundedExternalTask(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("bounded-external-task observation requires task metadata."); + } + if (value.bounded !== true || value.realRepository !== true || value.acceptanceDefinedBeforeExecution !== true) { + throw new Error("bounded-external-task requires bounded, realRepository, and acceptanceDefinedBeforeExecution to be true."); + } + return { + bounded: true, + realRepository: true, + acceptanceDefinedBeforeExecution: true, + }; +} + +function requireObject(value, label) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${label} requires structured metadata.`); + } + return value; +} + +function requireTrue(value, label) { + if (value !== true) throw new Error(`${label} must be true.`); + return true; +} + +function requireFalse(value, label) { + if (value !== false) throw new Error(`${label} must be false.`); + return false; +} + +function validateRepeatUse(value) { + const input = requireObject(value, "repeat-user"); + if (!Number.isInteger(input.runOrdinal) || input.runOrdinal < 2) { + throw new Error("repeat-user runOrdinal must be at least 2."); + } + return { + runOrdinal: input.runOrdinal, + withoutMaintainerAssistance: requireTrue(input.withoutMaintainerAssistance, "repeat-user withoutMaintainerAssistance"), + }; +} + +function validateComparison(value) { + const input = requireObject(value, "native-goal-comparison"); + const truthLabels = new Set(["observed", "unknown"]); + if (!truthLabels.has(input.nativeUsage) || !truthLabels.has(input.cewpUsage)) { + throw new Error("native-goal-comparison usage labels must be observed or unknown."); + } + return { + id: requireSafeId(input.id, "native-goal-comparison id"), + equivalentTaskShape: requireTrue(input.equivalentTaskShape, "native-goal-comparison equivalentTaskShape"), + setupOverheadMeasured: requireTrue(input.setupOverheadMeasured, "native-goal-comparison setupOverheadMeasured"), + nativeUsage: input.nativeUsage, + cewpUsage: input.cewpUsage, + }; +} + +function validateBenefit(value) { + const input = requireObject(value, "measurable-benefit"); + const dimensions = new Set(["recovery", "policy-enforcement", "independent-findings", "evidence-quality"]); + if (!dimensions.has(input.dimension)) throw new Error("measurable-benefit dimension is unsupported."); + return { + dimension: input.dimension, + measured: requireTrue(input.measured, "measurable-benefit measured"), + setupOverheadConsidered: requireTrue(input.setupOverheadConsidered, "measurable-benefit setupOverheadConsidered"), + }; +} + +function validateRecovery(value) { + const input = requireObject(value, "recovery"); + if (!["pause-revise-resume", "failure-retry"].includes(input.scenario)) { + throw new Error("recovery scenario is unsupported."); + } + return { + id: requireSafeId(input.id, "recovery id"), + scenario: input.scenario, + recoveredWithoutRestart: requireTrue(input.recoveredWithoutRestart, "recovery recoveredWithoutRestart"), + priorEvidenceRetained: requireTrue(input.priorEvidenceRetained, "recovery priorEvidenceRetained"), + }; +} + +function validatePause(value, expectedType) { + const input = requireObject(value, expectedType); + const allowedStates = expectedType === "controlled-host-limit" + ? ["paused-host-limit"] + : ["paused-budget-safe", "paused-budget-unverified"]; + if (!allowedStates.includes(input.state)) throw new Error(`${expectedType} pause state is unsupported.`); + return { + state: input.state, + absoluteCeilingRespected: requireTrue(input.absoluteCeilingRespected, `${expectedType} absoluteCeilingRespected`), + protectedReviewerAllocationRespected: requireTrue(input.protectedReviewerAllocationRespected, `${expectedType} protectedReviewerAllocationRespected`), + passClaimed: requireFalse(input.passClaimed, `${expectedType} passClaimed`), + }; +} + +function validateOnboardingFailure(value) { + const input = requireObject(value, "onboarding-remediation"); + if (!Number.isInteger(input.rank) || input.rank < 1 || input.rank > 5) { + throw new Error("onboarding-remediation rank must be from 1 to 5."); + } + if (!["fixed", "explicit-remediation"].includes(input.remediationStatus)) { + throw new Error("onboarding-remediation status is unsupported."); + } + return { + rank: input.rank, + code: requireSafeId(input.code, "onboarding-remediation code"), + remediationStatus: input.remediationStatus, + }; +} + +function validateContribution(value) { + const input = requireObject(value, "external-contribution"); + if (!["external-contribution", "substantive-issue"].includes(input.kind)) { + throw new Error("external-contribution kind is unsupported."); + } + return { + kind: input.kind, + externallyAuthored: requireTrue(input.externallyAuthored, "external-contribution externallyAuthored"), + }; +} + +function validateCaseStudy(value) { + const input = requireObject(value, "public-case-study"); + return { + id: requireSafeId(input.id, "public-case-study id"), + published: requireTrue(input.published, "public-case-study published"), + receiptExcerptIncluded: requireTrue(input.receiptExcerptIncluded, "public-case-study receiptExcerptIncluded"), + limitationsIncluded: requireTrue(input.limitationsIncluded, "public-case-study limitationsIncluded"), + usageTruthIncluded: requireTrue(input.usageTruthIncluded, "public-case-study usageTruthIncluded"), + }; +} + +function validateCalibration(value) { + const input = requireObject(value, "estimate-calibration-report"); + const confidencePromoted = input.confidencePromoted === true; + const minimumSampleMet = input.minimumSampleMet === true; + const driftCurrent = input.driftCurrent === true; + if (confidencePromoted && (!minimumSampleMet || !driftCurrent)) { + throw new Error("estimate confidence cannot be promoted without minimum samples and current drift evidence."); + } + return { + intervalCoverageReported: requireTrue(input.intervalCoverageReported, "estimate-calibration intervalCoverageReported"), + errorReported: requireTrue(input.errorReported, "estimate-calibration errorReported"), + taskClassBreakdown: requireTrue(input.taskClassBreakdown, "estimate-calibration taskClassBreakdown"), + confidencePromoted, + minimumSampleMet, + driftCurrent, + }; +} + +function validateGuardrailAudit(value) { + const input = requireObject(value, "guardrail-audit-pass"); + if (input.unresolvedBypasses !== 0) { + throw new Error("guardrail-audit-pass requires zero unresolved bypasses."); + } + return { + completed: requireTrue(input.completed, "guardrail-audit completed"), + unresolvedBypasses: 0, + }; +} + +function validateReviewedRun(value) { + const input = requireObject(value, "full-reviewed-run"); + return { + workflowRunId: requireSafeId(input.workflowRunId, "full-reviewed-run workflowRunId"), + }; +} + +function normalizeObservationData(value) { + switch (value.type) { + case "repository-attempt": return { attempt: validateRepositoryAttempt(value.attempt) }; + case "golden-path-complete": return { completion: validateGoldenPath(value.completion) }; + case "bounded-external-task": return { task: validateBoundedExternalTask(value.task) }; + case "repeat-user": return { repeatUse: validateRepeatUse(value.repeatUse) }; + case "native-goal-comparison": return { comparison: validateComparison(value.comparison) }; + case "measurable-benefit": return { benefit: validateBenefit(value.benefit) }; + case "recovery": return { recovery: validateRecovery(value.recovery) }; + case "operational-budget-exhaustion": return { pause: validatePause(value.pause, value.type) }; + case "controlled-host-limit": return { pause: validatePause(value.pause, value.type) }; + case "onboarding-remediation": return { failure: validateOnboardingFailure(value.failure) }; + case "external-contribution": return { contribution: validateContribution(value.contribution) }; + case "public-case-study": return { caseStudy: validateCaseStudy(value.caseStudy) }; + case "estimate-calibration-report": return { calibration: validateCalibration(value.calibration) }; + case "guardrail-audit-pass": return { audit: validateGuardrailAudit(value.audit) }; + case "full-reviewed-run": return { run: validateReviewedRun(value.run) }; + default: throw new Error(`Unsupported pilot observation type: ${value.type || "missing"}.`); + } +} + +function validatePilotObservation(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("pilot observation must be a JSON object."); + } + if (value.schemaVersion !== PILOT_OBSERVATION_SCHEMA_VERSION) { + throw new Error(`Unsupported pilot observation schema: ${value.schemaVersion || "missing"}.`); + } + if (!OBSERVATION_TYPES.has(value.type)) { + throw new Error(`Unsupported pilot observation type: ${value.type || "missing"}.`); + } + return { + schemaVersion: PILOT_OBSERVATION_SCHEMA_VERSION, + id: requireSafeId(value.observationId, "pilot observation observationId"), + type: value.type, + observedAt: normalizeObservedAt(value.observedAt), + data: normalizeObservationData(value), + }; +} + +function loadPilotRecord(repoRoot, pilotId) { + const id = validatePilotId(pilotId); + const recordPath = path.join(path.resolve(repoRoot), ".cewp", "pilots", id, "record.json"); + if (!fs.existsSync(recordPath)) throw new Error(`Pilot record not found: ${id}.`); + const record = JSON.parse(fs.readFileSync(recordPath, "utf8")); + if (record.schemaVersion !== PILOT_RECORD_SCHEMA_VERSION || record.pilotId !== id) { + throw new Error(`Pilot record ${id} is malformed or incompatible.`); + } + return { record, recordPath }; +} + +function observationEvidenceIdentity(observation) { + if (observation.type === "repository-attempt") return `repository-attempt:${observation.data.attempt.repositoryId}`; + if (observation.type === "native-goal-comparison") return `native-goal-comparison:${observation.data.comparison.id}`; + if (observation.type === "recovery") return `recovery:${observation.data.recovery.id}`; + if (observation.type === "public-case-study") return `public-case-study:${observation.data.caseStudy.id}`; + if (observation.type === "onboarding-remediation") return `onboarding-remediation:${observation.data.failure.code}`; + if (observation.type === "full-reviewed-run") return `full-reviewed-run:${observation.data.run.workflowRunId}`; + return `${observation.type}:${observation.id}`; +} + +function existingObservationIdentities(repoRoot) { + const pilotsRoot = path.join(path.resolve(repoRoot), ".cewp", "pilots"); + const identities = new Set(); + if (!fs.existsSync(pilotsRoot)) return identities; + for (const entry of fs.readdirSync(pilotsRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const recordPath = path.join(pilotsRoot, entry.name, "record.json"); + if (!fs.existsSync(recordPath)) continue; + const record = JSON.parse(fs.readFileSync(recordPath, "utf8")); + for (const observation of record.observations || []) { + identities.add(`observation:${observation.id}`); + identities.add(`evidence:${observationEvidenceIdentity(observation)}`); + } + } + return identities; +} + +function recordPilotObservation(options = {}) { + const repoRoot = path.resolve(options.repoRoot || process.cwd()); + const source = readRepoJson(repoRoot, options.fromFile, "pilot observation --from"); + const observation = validatePilotObservation(source.value); + const found = loadPilotRecord(repoRoot, options.pilotId); + const identities = existingObservationIdentities(repoRoot); + const duplicateIdentity = identities.has(`observation:${observation.id}`) + ? `observation:${observation.id}` + : identities.has(`evidence:${observationEvidenceIdentity(observation)}`) + ? observationEvidenceIdentity(observation) + : null; + if (duplicateIdentity) { + throw new Error(`Pilot evidence identity already exists: ${duplicateIdentity}.`); + } + const independentParticipant = found.record.participant.classification === "independent-external"; + const evidence = observation.type === "full-reviewed-run" + ? inspectReviewedRunEvidence(repoRoot, observation.data.run.workflowRunId) + : null; + const eligible = independentParticipant && (!evidence || evidence.status === "qualified"); + const stored = { + ...observation, + recordedAt: (options.now || new Date()).toISOString(), + source: { sha256: source.sha256, pathRecorded: false }, + evidence, + qualification: { + eligible, + classification: eligible + ? "independent-evidence" + : independentParticipant ? "excluded-evidence" : "maintainer-dogfood", + reason: eligible + ? "validated structured observation from an independent-external pilot record" + : independentParticipant + ? evidence.reason + : "maintainer dogfood never counts as independent Phase 13 evidence", + }, + }; + const record = { + ...found.record, + observations: [...(found.record.observations || []), stored], + updatedAt: stored.recordedAt, + }; + writeJsonAtomic(found.recordPath, record); + return { record, observation: stored }; +} + +module.exports = { + OBSERVATION_TYPES, + PILOT_OBSERVATION_SCHEMA_VERSION, + loadPilotRecord, + observationEvidenceIdentity, + recordPilotObservation, + validatePilotObservation, +}; diff --git a/src/pilot/record.js b/src/pilot/record.js new file mode 100644 index 0000000..4a04559 --- /dev/null +++ b/src/pilot/record.js @@ -0,0 +1,76 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { getGitHeadCommit } = require("../lib/git"); +const { writeJsonAtomic } = require("../workflow/state"); + +const PILOT_RECORD_SCHEMA_VERSION = "pilot-record/v1"; +const PARTICIPANT_CLASSIFICATIONS = new Set([ + "maintainer-dogfood", + "independent-external", +]); +const SAFE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/; + +function validatePilotId(value) { + const pilotId = String(value || "").trim(); + if (!SAFE_ID.test(pilotId)) { + throw new Error("--pilot-id requires 1-64 lowercase letters, numbers, or hyphens."); + } + return pilotId; +} + +function validateParticipantId(value) { + const participantId = String(value || "").trim(); + if (!SAFE_ID.test(participantId)) { + throw new Error("--participant-id requires a privacy-safe lowercase id."); + } + return participantId; +} + +function validateParticipantClassification(value) { + const classification = String(value || "").trim(); + if (!PARTICIPANT_CLASSIFICATIONS.has(classification)) { + throw new Error("--participant requires maintainer-dogfood or independent-external."); + } + return classification; +} + +function createPilotRecord(options = {}) { + const repoRoot = path.resolve(options.repoRoot || process.cwd()); + const pilotId = validatePilotId(options.pilotId); + const participantId = validateParticipantId(options.participantId); + const classification = validateParticipantClassification(options.participant); + const recordPath = path.join(repoRoot, ".cewp", "pilots", pilotId, "record.json"); + if (fs.existsSync(recordPath)) { + throw new Error(`Pilot record already exists: ${pilotId}.`); + } + const timestamp = (options.now || new Date()).toISOString(); + const record = { + schemaVersion: PILOT_RECORD_SCHEMA_VERSION, + pilotId, + participant: { + classification, + id: participantId, + independentEvidenceEligible: classification === "independent-external", + }, + repository: { + gitBaseCommit: getGitHeadCommit(repoRoot), + pathRecorded: false, + }, + observations: [], + createdAt: timestamp, + updatedAt: timestamp, + }; + writeJsonAtomic(recordPath, record); + return record; +} + +module.exports = { + PARTICIPANT_CLASSIFICATIONS, + PILOT_RECORD_SCHEMA_VERSION, + createPilotRecord, + validateParticipantClassification, + validateParticipantId, + validatePilotId, +}; diff --git a/src/pilot/status.js b/src/pilot/status.js new file mode 100644 index 0000000..b171c9b --- /dev/null +++ b/src/pilot/status.js @@ -0,0 +1,139 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { PILOT_RECORD_SCHEMA_VERSION } = require("./record"); + +const PILOT_STATUS_SCHEMA_VERSION = "pilot-status/v1"; +const COUNT_GATES = Object.freeze([ + { id: "independent-repository-attempts", threshold: 10, observationType: "repository-attempt" }, + { id: "independent-external-participants", threshold: 3, source: "participants" }, + { id: "real-bounded-external-repository-task", threshold: 1, observationType: "bounded-external-task" }, + { id: "full-reviewed-runs", threshold: 5, observationType: "full-reviewed-run" }, + { id: "repeat-users-without-maintainer-assistance", threshold: 3, observationType: "repeat-user" }, + { id: "comparable-native-goal-runs", threshold: 3, observationType: "native-goal-comparison" }, + { id: "measurable-cewp-benefit", threshold: 1, observationType: "measurable-benefit" }, + { id: "recovered-pause-or-failure-scenarios", threshold: 3, observationType: "recovery" }, + { id: "operational-budget-exhaustion", threshold: 1, observationType: "operational-budget-exhaustion" }, + { id: "controlled-host-limit", threshold: 1, observationType: "controlled-host-limit" }, + { id: "top-onboarding-failures-remediated", threshold: 5, observationType: "onboarding-remediation" }, + { id: "external-contribution-or-substantive-issue", threshold: 1, observationType: "external-contribution" }, + { id: "public-case-studies", threshold: 3, observationType: "public-case-study" }, + { id: "usage-estimate-calibration-reported", threshold: 1, observationType: "estimate-calibration-report" }, + { id: "guardrail-audit-with-no-unresolved-bypass", threshold: 1, observationType: "guardrail-audit-pass" }, +]); + +function inspectPilotRecords(repoRoot) { + const pilotsRoot = path.join(path.resolve(repoRoot), ".cewp", "pilots"); + if (!fs.existsSync(pilotsRoot)) return { valid: [], invalid: [] }; + const result = { valid: [], invalid: [] }; + for (const entry of fs.readdirSync(pilotsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .sort((left, right) => left.name.localeCompare(right.name))) { + const filePath = path.join(pilotsRoot, entry.name, "record.json"); + if (!fs.existsSync(filePath)) { + result.invalid.push({ pilotId: entry.name, reason: "record-missing" }); + continue; + } + try { + const record = JSON.parse(fs.readFileSync(filePath, "utf8")); + if (record.schemaVersion !== PILOT_RECORD_SCHEMA_VERSION || record.pilotId !== entry.name) { + result.invalid.push({ pilotId: entry.name, reason: "schema-or-identity-incompatible" }); + continue; + } + result.valid.push(record); + } catch { + result.invalid.push({ pilotId: entry.name, reason: "json-malformed" }); + } + } + return result; +} + +function loadPilotRecords(repoRoot) { + const inspected = inspectPilotRecords(repoRoot); + if (inspected.invalid.length > 0) { + throw new Error(`Pilot records contain ${inspected.invalid.length} invalid local record(s); inspect pilot status before export.`); + } + return inspected.valid; +} + +function derivePilotStatus(repoRoot) { + const inspected = inspectPilotRecords(repoRoot); + const records = inspected.valid; + const externalRecords = records.filter((record) => record.participant.classification === "independent-external"); + const externalParticipantIds = new Set(externalRecords + .filter((record) => (record.observations || []).some((observation) => ( + observation.type === "golden-path-complete" && observation.qualification?.eligible === true + ))) + .map((record) => record.participant.id)); + const evidenceByType = new Map(); + for (const record of externalRecords) { + for (const observation of record.observations || []) { + if (!observation || observation.qualification?.eligible !== true || typeof observation.type !== "string") continue; + if (!evidenceByType.has(observation.type)) evidenceByType.set(observation.type, []); + evidenceByType.get(observation.type).push({ + participantId: record.participant.id, + pilotId: record.pilotId, + observation, + }); + } + } + const gates = COUNT_GATES.map((definition) => { + let qualifyingEvidence; + if (definition.source === "participants") { + qualifyingEvidence = [...externalParticipantIds].sort(); + } else { + const evidence = evidenceByType.get(definition.observationType) || []; + const unique = new Map(); + for (const item of evidence) { + const identity = definition.observationType === "repository-attempt" + ? item.observation.data.attempt.repositoryId + : definition.observationType === "repeat-user" + ? item.participantId + : `${item.pilotId}:${item.observation.id || item.observation.type}`; + if (!unique.has(identity)) unique.set(identity, identity); + } + qualifyingEvidence = [...unique.values()].sort(); + } + const observed = qualifyingEvidence.length; + return { + id: definition.id, + threshold: definition.threshold, + observed, + remaining: Math.max(0, definition.threshold - observed), + status: observed >= definition.threshold ? "met" : "unmet", + qualifyingEvidence, + }; + }); + return { + schemaVersion: PILOT_STATUS_SCHEMA_VERSION, + complete: inspected.invalid.length === 0 && gates.every((gate) => gate.status === "met"), + records: { total: records.length + inspected.invalid.length, valid: records.length, invalid: inspected.invalid.length }, + participants: { + maintainerDogfood: records.filter((record) => record.participant.classification === "maintainer-dogfood").length, + independentExternal: externalParticipantIds.size, + }, + gates, + exclusions: records + .filter((record) => record.participant.classification !== "independent-external") + .map((record) => ({ + pilotId: record.pilotId, + classification: record.participant.classification, + reason: "maintainer dogfood never counts as independent Phase 13 evidence", + })), + warnings: [ + ...inspected.invalid.map((record) => ({ code: "pilot-record-invalid", pilotId: record.pilotId, reason: record.reason })), + ...gates + .filter((gate) => gate.status === "unmet") + .map((gate) => ({ code: "pilot-gate-unmet", gate: gate.id, remaining: gate.remaining })), + ], + }; +} + +module.exports = { + COUNT_GATES, + PILOT_STATUS_SCHEMA_VERSION, + derivePilotStatus, + inspectPilotRecords, + loadPilotRecords, +}; diff --git a/src/run/adapters/codex-exec.js b/src/run/adapters/codex-exec.js index 2e91d81..1c662f2 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, { @@ -248,6 +256,70 @@ function didAdapterTimeOut(execResult) { return Boolean(execResult.error && execResult.error.code === "ETIMEDOUT"); } +function unwrapCodexErrorMessage(value) { + if (typeof value !== "string" || value.trim().length === 0) return null; + const message = value.trim(); + try { + const parsed = JSON.parse(message); + if (parsed && parsed.error && typeof parsed.error.message === "string") { + return parsed.error.message.trim(); + } + } catch { + // A plain-text host message is already usable evidence. + } + return message; +} + +function getStructuredCodexFailureMessages(stdout) { + const messages = []; + for (const line of String(stdout || "").split(/\r?\n/).map((value) => value.trim()).filter(Boolean)) { + let event; + try { + event = JSON.parse(line); + } catch { + continue; + } + if (event.type === "turn.failed" && event.error) { + const message = unwrapCodexErrorMessage(event.error.message); + if (message) messages.push(message); + } else if (event.type === "error") { + const message = unwrapCodexErrorMessage(event.message); + if (message) messages.push(message); + } + } + return [...new Set(messages)]; +} + +function classifyCodexExecFailure(stdout) { + const messages = getStructuredCodexFailureMessages(stdout); + if (messages.length === 0) return null; + + const incompatibleModel = messages.find((message) => /requires a newer version of Codex/i.test(message)); + if (incompatibleModel) { + return { + code: "codex-cli-model-incompatible", + reasons: [incompatibleModel], + actions: ["rollback", "abandon", "recreate-after-remediation"], + remediation: [ + "Upgrade the Codex app or CLI through its supported installer.", + "Alternatively, explicitly approve a model supported by the installed Codex version.", + "Then roll back or abandon this blocked run and create a new supervised run.", + ], + automaticChanges: false, + }; + } + + return { + code: "codex-exec-host-failure", + reasons: messages, + actions: ["inspect-logs", "rollback", "abandon"], + remediation: [ + "Inspect the captured codex-exec stdout and stderr logs, remediate the reported host failure, and create a new supervised run.", + ], + automaticChanges: false, + }; +} + function normalizeAdapterResult({ role, status, @@ -307,5 +379,6 @@ module.exports = { runCodexExecAdapter, getAdapterExitCode, didAdapterTimeOut, + classifyCodexExecFailure, normalizeAdapterResult, }; 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/run/verify.js b/src/run/verify.js new file mode 100644 index 0000000..4b4ec55 --- /dev/null +++ b/src/run/verify.js @@ -0,0 +1,21 @@ +"use strict"; + +const { verifyWorkflowRun } = require("../evidence/verify"); + +function runVerify(options = {}) { + if (!options.runId) throw new Error("run verify requires a workflow run id."); + const report = verifyWorkflowRun(process.cwd(), options.runId); + if (options.json) { + console.log(JSON.stringify({ schemaVersion: "operator-json/v1", command: "run.verify", data: report }, null, 2)); + } else { + console.log("CEWP run verification"); + console.log(`Run ID: ${report.runId}`); + console.log(`Status: ${report.status}`); + for (const entry of report.checks) console.log(`${entry.id}: ${entry.status}`); + for (const entry of report.issues) console.log(`[${entry.code}] ${entry.message}`); + } + if (report.status !== "passed") process.exitCode = 1; + return report; +} + +module.exports = { runVerify }; 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..827c8f9 100644 --- a/src/supervise/execution.js +++ b/src/supervise/execution.js @@ -20,11 +20,17 @@ const { runCodexExecAdapter, getAdapterExitCode, didAdapterTimeOut, + classifyCodexExecFailure, } = require("../run/adapters/codex-exec"); const { OWNERSHIP_SCHEMA_VERSION, + loadOwnershipRecords, validateOwnershipRecord, } = require("../run/ownership"); +const { + confirmCodexEffortEvidence, + resolveCodexEffortForDispatch, +} = require("../integration/effort-policy"); const { appendEvent, findSupervisedRun, @@ -198,7 +204,7 @@ function reuseOwnedWorktree(found, startedAt) { } function createOwnedWorktree(found, startedAt) { - if (isRepoDirty(found.repoRoot)) { + if (isRepoDirty(found.repoRoot, { ignoreUntrackedCewpRuntime: true })) { throw new Error("Cannot dispatch a supervised checkpoint while the source repository is dirty."); } const currentHead = getGitHeadCommit(found.repoRoot); @@ -210,27 +216,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 +240,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 +308,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 +338,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 +405,8 @@ function executeSupervisedCheckpoint(options = {}) { timeoutSeconds: options.timeoutSeconds, sandbox: "workspace-write", structuredJson: true, + model: codexSelection.model, + effort: codexSelection.effort, }); const remainingOutput = Math.max( 0, @@ -420,6 +431,9 @@ function executeSupervisedCheckpoint(options = {}) { } const testAuthoring = getTestAuthoringVerdict(found.run, changes.changedFiles); const lastMessagePresent = fs.existsSync(lastMessagePath); + const adapterFailure = exitCode !== 0 + ? classifyCodexExecFailure(execResult.stdout) + : null; const succeeded = exitCode === 0 && !timedOut && scopeWarnings.length === 0 @@ -439,6 +453,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, "/"), @@ -450,11 +465,15 @@ function executeSupervisedCheckpoint(options = {}) { }, }; const blockerReasons = []; - if (exitCode !== 0) blockerReasons.push(`codex-exec exited with code ${exitCode}`); + if (adapterFailure) { + blockerReasons.push(...adapterFailure.reasons); + } else if (exitCode !== 0) { + blockerReasons.push(`codex-exec exited with code ${exitCode}`); + } if (timedOut) blockerReasons.push(`codex-exec timed out after ${options.timeoutSeconds}s`); blockerReasons.push(...scopeWarnings); blockerReasons.push(...testAuthoring.violations); - if (!lastMessagePresent) blockerReasons.push("codex-exec last message is missing"); + if (!lastMessagePresent && !adapterFailure) blockerReasons.push("codex-exec last message is missing"); const completedRun = { ...startedRun, status: succeeded ? "verifying" : "blocked", @@ -468,9 +487,15 @@ function executeSupervisedCheckpoint(options = {}) { status: succeeded ? "awaiting-verification" : "blocked", attempts: [completedAttempt], blocker: succeeded ? null : { - code: "dispatch-or-scope-failure", + code: adapterFailure ? adapterFailure.code : "dispatch-or-scope-failure", reasons: blockerReasons, - actions: ["retry", "revise", "rollback", "abandon"], + actions: adapterFailure + ? adapterFailure.actions + : ["retry", "revise", "rollback", "abandon"], + ...(adapterFailure ? { + remediation: adapterFailure.remediation, + automaticChanges: adapterFailure.automaticChanges, + } : {}), }, }], }; @@ -549,6 +574,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 +618,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 +665,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 +708,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/src/supervise/state.js b/src/supervise/state.js index 68cea0a..8f643fc 100644 --- a/src/supervise/state.js +++ b/src/supervise/state.js @@ -299,7 +299,7 @@ function renderProgress(run) { ? task.evidence.map(formatProgressEvidence).join("\n") : "- none"; const blockerDetail = blocker - ? `${blocker.code}; reasons: ${(blocker.reasons || []).join(" | ") || "none"}; actions: ${(blocker.actions || []).join(", ") || "none"}` + ? `${blocker.code}; reasons: ${(blocker.reasons || []).join(" | ") || "none"}; actions: ${(blocker.actions || []).join(", ") || "none"}; remediation: ${(blocker.remediation || []).join(" | ") || "none"}` : "none"; const pauseDetail = run.pause && run.status.startsWith("paused-") ? `${run.pause.reason}; actions: ${(run.pause.actions || []).join(", ") || "none"}` diff --git a/src/workflow/cli.js b/src/workflow/cli.js index 7abe7ba..442b9aa 100644 --- a/src/workflow/cli.js +++ b/src/workflow/cli.js @@ -30,6 +30,10 @@ const { } = require("./state"); const { deriveSchedule } = require("./scheduler"); const { listWorkflowTemplates, loadWorkflowTemplate } = require("./templates"); +const { buildEvidenceReceipt, writeEvidenceReceipt } = require("../evidence/receipt"); +const { writeOperatorReport } = require("../evidence/report"); +const { compareEvidenceReceipts } = require("../evidence/compare"); +const { exportRedactedEvidence } = require("../evidence/redaction"); function outputJson(command, data) { console.log(JSON.stringify({ @@ -61,6 +65,61 @@ function resolveProposalSource(options) { } function runWorkflow(options = {}) { + if (options.subcommand === "export") { + if (!options.workflowRunId) throw new Error("workflow export requires a run id."); + const result = exportRedactedEvidence(loadWorkflowRun(process.cwd(), options.workflowRunId)); + if (options.json) outputJson("workflow.export", result); + else { + console.log("CEWP redacted evidence export written"); + console.log(`Run ID: ${result.receipt.runId}`); + for (const [name, filePath] of Object.entries(result.paths)) console.log(`${name}: ${filePath}`); + } + return; + } + if (options.subcommand === "compare") { + if (!options.workflowRunId || !options.comparisonRunId) throw new Error("workflow compare requires two run ids."); + const generatedAt = new Date().toISOString(); + const left = buildEvidenceReceipt(loadWorkflowRun(process.cwd(), options.workflowRunId), { generatedAt }); + const right = buildEvidenceReceipt(loadWorkflowRun(process.cwd(), options.comparisonRunId), { generatedAt }); + const comparison = compareEvidenceReceipts(left, right); + if (options.json) outputJson("workflow.compare", comparison); + else { + console.log("CEWP workflow comparison"); + console.log(`Left: ${comparison.runs.left.runId} (${comparison.runs.left.execution.owner})`); + console.log(`Right: ${comparison.runs.right.runId} (${comparison.runs.right.execution.owner})`); + console.log(`Equivalence: ${comparison.equivalence.status}`); + console.log(`Unavailable: ${comparison.equivalence.unavailable.join(", ") || "none"}`); + } + return; + } + if (options.subcommand === "report") { + if (!options.workflowRunId) throw new Error("workflow report requires a run id."); + const found = loadWorkflowRun(process.cwd(), options.workflowRunId); + const result = writeOperatorReport(found); + if (options.json) outputJson("workflow.report", result); + else { + console.log("CEWP offline operator report written"); + console.log(`Run ID: ${result.report.runId}`); + console.log(`Status: ${result.report.completeness.status}`); + console.log(`JSON: ${result.paths.json}`); + console.log(`HTML: ${result.paths.html}`); + } + return; + } + if (options.subcommand === "receipt") { + if (!options.workflowRunId) throw new Error("workflow receipt requires a run id."); + const found = loadWorkflowRun(process.cwd(), options.workflowRunId); + const result = writeEvidenceReceipt(found); + if (options.json) outputJson("workflow.receipt", result); + else { + console.log("CEWP evidence receipt written"); + console.log(`Run ID: ${result.receipt.runId}`); + console.log(`Completeness: ${result.receipt.completeness.status}`); + console.log(`JSON: ${result.paths.json}`); + console.log(`Markdown: ${result.paths.markdown}`); + } + return; + } if (options.subcommand === "compile") { const request = createWorkflowCompilerRequest({ repoRoot: process.cwd(), diff --git a/src/workflow/migration.js b/src/workflow/migration.js index a0bafd3..ca10852 100644 --- a/src/workflow/migration.js +++ b/src/workflow/migration.js @@ -5,6 +5,7 @@ const fs = require("node:fs"); const path = require("node:path"); const { ensureDir } = require("../lib/fs"); const { normalizeSlashPath } = require("../lib/paths"); +const { appendLifecycleEvent } = require("../evidence/events"); const { findSupervisedRun } = require("../supervise/state"); const { digestWorkflowDefinition, @@ -366,8 +367,7 @@ function applyLegacyMigration(repoRoot, runId, options = {}) { }; writeJsonAtomic(runPath, run); const progress = writeWorkflowProgress(runRoot, run, preview.definition, { now: new Date(timestamp) }); - fs.appendFileSync(path.join(runRoot, "events.jsonl"), `${JSON.stringify({ - schemaVersion: "workflow-event/v1", + appendLifecycleEvent(runRoot, { timestamp, type: "workflow-migrated", runId: run.runId, @@ -378,7 +378,7 @@ function applyLegacyMigration(repoRoot, runId, options = {}) { migrationDigest: preview.migrationDigest, backupPath: normalizeSlashPath(path.relative(preview.found.repoRoot, backupPath)), actor: "operator", - })}\n`); + }); const record = { schemaVersion: "workflow-migration/v1", projectionVersion: MIGRATION_PROJECTION_VERSION, diff --git a/src/workflow/state.js b/src/workflow/state.js index 3128d16..ebca6f9 100644 --- a/src/workflow/state.js +++ b/src/workflow/state.js @@ -3,6 +3,8 @@ const fs = require("node:fs"); const path = require("node:path"); const { ensureDir } = require("../lib/fs"); +const { getGitHeadCommit } = require("../lib/git"); +const { appendLifecycleEvent } = require("../evidence/events"); const { normalizeSlashPath } = require("../lib/paths"); const { digestWorkflowDefinition, validateWorkflowDefinition } = require("./definition"); const { readRepoJson } = require("./source"); @@ -319,7 +321,7 @@ function loadWorkflowRun(repoRoot, runId) { } function appendWorkflowEvent(runRoot, event) { - fs.appendFileSync(path.join(runRoot, "events.jsonl"), `${JSON.stringify(event)}\n`); + appendLifecycleEvent(runRoot, event); } function pauseForWorkflowBudget(found, allocation, decision, timestamp) { @@ -347,6 +349,23 @@ function pauseForWorkflowBudget(found, allocation, decision, timestamp) { }; writeJsonAtomic(found.runPath, run); writeWorkflowProgress(found.runRoot, run, found.definition, { now: new Date(timestamp) }); + if (decision.warning) { + appendWorkflowEvent(found.runRoot, { + timestamp, + type: "budget-threshold", + runId: run.runId, + threshold: decision.warning, + percent: decision.percent, + allocation, + }); + appendWorkflowEvent(found.runRoot, { + timestamp, + type: "warning-presented", + runId: run.runId, + warning: decision.warning, + surface: "cewp-core-state", + }); + } appendWorkflowEvent(found.runRoot, { schemaVersion: "workflow-event/v1", timestamp, @@ -415,10 +434,9 @@ function startWorkflowTask(found, taskId, options = {}) { throw new Error(`Controlled workflow operation paused: ${budgetDecision.pauseStatus} (${budgetDecision.reason}).`); } let runBeforeStart = found.run; - if ( - budgetDecision.warning - && !found.run.budget.thresholdEvents.some((event) => event.threshold === budgetDecision.warning) - ) { + const newBudgetWarning = budgetDecision.warning + && !found.run.budget.thresholdEvents.some((event) => event.threshold === budgetDecision.warning); + if (newBudgetWarning) { runBeforeStart = { ...found.run, budget: { @@ -495,6 +513,41 @@ function startWorkflowTask(found, taskId, options = {}) { }; writeJsonAtomic(found.runPath, run); const progress = writeWorkflowProgress(found.runRoot, run, found.definition, { now }); + if (newBudgetWarning) { + appendWorkflowEvent(found.runRoot, { + timestamp, + type: "budget-threshold", + runId: run.runId, + threshold: budgetDecision.warning, + percent: budgetDecision.percent, + allocation, + }); + appendWorkflowEvent(found.runRoot, { + timestamp, + type: "warning-presented", + runId: run.runId, + warning: budgetDecision.warning, + surface: "cewp-core-state", + }); + } + appendWorkflowEvent(found.runRoot, { + timestamp, + type: "checkpoint-started", + runId: run.runId, + taskId, + checkpointId, + attempt, + }); + appendWorkflowEvent(found.runRoot, { + timestamp, + type: "dispatch-started", + runId: run.runId, + taskId, + checkpointId, + workerId, + executionOwner: run.execution.owner, + backend: run.execution.backend, + }); appendWorkflowEvent(found.runRoot, { schemaVersion: "workflow-event/v1", timestamp, @@ -584,6 +637,50 @@ function recordWorkflowFailureResult(found, runtimeTask, checkpoint, checkpointP writeJsonAtomic(found.runPath, run); const now = new Date(result.completedAt); const progress = writeWorkflowProgress(found.runRoot, run, found.definition, { now }); + appendWorkflowEvent(found.runRoot, { + timestamp: result.completedAt, + type: "scope-evaluated", + runId: run.runId, + taskId: runtimeTask.id, + checkpointId: checkpoint.checkpointId, + resultId: result.resultId, + status: "passed", + }); + appendWorkflowEvent(found.runRoot, { + timestamp: result.completedAt, + type: "verification-completed", + runId: run.runId, + taskId: runtimeTask.id, + checkpointId: checkpoint.checkpointId, + resultId: result.resultId, + status: "failed", + }); + appendWorkflowEvent(found.runRoot, { + timestamp: result.completedAt, + type: "usage-observed", + runId: run.runId, + taskId: runtimeTask.id, + checkpointId: checkpoint.checkpointId, + resultId: result.resultId, + usageCategories: Object.fromEntries(Object.entries(result.usage).map(([name, value]) => [name, value.label])), + }); + appendWorkflowEvent(found.runRoot, { + timestamp: result.completedAt, + type: "allocation-consumed", + runId: run.runId, + taskId: runtimeTask.id, + checkpointId: checkpoint.checkpointId, + allocation: checkpoint.budget.activeAllocation, + consumed: budget.consumed.allocations[checkpoint.budget.activeAllocation], + }); + appendWorkflowEvent(found.runRoot, { + timestamp: result.completedAt, + type: "checkpoint-completed", + runId: run.runId, + taskId: runtimeTask.id, + checkpointId: checkpoint.checkpointId, + status: blockedCheckpoint.status, + }); appendWorkflowEvent(found.runRoot, { schemaVersion: "workflow-event/v1", timestamp: result.completedAt, @@ -794,6 +891,50 @@ function recordWorkflowResult(found, taskId, candidate) { }; writeJsonAtomic(found.runPath, run); const progress = writeWorkflowProgress(found.runRoot, run, found.definition, { now: new Date(result.completedAt) }); + appendWorkflowEvent(found.runRoot, { + timestamp: result.completedAt, + type: "scope-evaluated", + runId: run.runId, + taskId, + checkpointId: checkpoint.checkpointId, + resultId: result.resultId, + status: "passed", + }); + appendWorkflowEvent(found.runRoot, { + timestamp: result.completedAt, + type: "verification-completed", + runId: run.runId, + taskId, + checkpointId: checkpoint.checkpointId, + resultId: result.resultId, + status: "passed", + }); + appendWorkflowEvent(found.runRoot, { + timestamp: result.completedAt, + type: "usage-observed", + runId: run.runId, + taskId, + checkpointId: checkpoint.checkpointId, + resultId: result.resultId, + usageCategories: Object.fromEntries(Object.entries(result.usage).map(([name, value]) => [name, value.label])), + }); + appendWorkflowEvent(found.runRoot, { + timestamp: result.completedAt, + type: "allocation-consumed", + runId: run.runId, + taskId, + checkpointId: checkpoint.checkpointId, + allocation: checkpoint.budget.activeAllocation, + consumed: budget.consumed.allocations[checkpoint.budget.activeAllocation], + }); + appendWorkflowEvent(found.runRoot, { + timestamp: result.completedAt, + type: "checkpoint-completed", + runId: run.runId, + taskId, + checkpointId: checkpoint.checkpointId, + status: completedCheckpoint.status, + }); appendWorkflowEvent(found.runRoot, { schemaVersion: "workflow-event/v1", timestamp: result.completedAt, @@ -1266,6 +1407,25 @@ function interveneWorkflowRun(found, options, timestamp, reason) { runId: run.runId, ...intervention, }); + if (options.event === "add-budget") { + appendWorkflowEvent(found.runRoot, { + timestamp, + type: "budget-approved", + runId: run.runId, + actor: intervention.actor, + allocation: options.allocation, + operations: options.operations, + revision: budget.revisions.length, + }); + } else if (options.event === "pause-host-limit") { + appendWorkflowEvent(found.runRoot, { + timestamp, + type: "paused-host-limit", + runId: run.runId, + reason, + actor: intervention.actor, + }); + } return { run, checkpoint: null, @@ -1345,6 +1505,15 @@ function interveneWorkflowLifecycle(found, options, timestamp, reason) { runId: run.runId, ...intervention, }); + if (options.event === "cancel") { + appendWorkflowEvent(found.runRoot, { + timestamp, + type: "workflow-cancelled", + runId: run.runId, + reason, + actor: intervention.actor, + }); + } return { run, checkpoints, @@ -1519,6 +1688,9 @@ function createApprovedRun(options) { status: "approved", createdAt: timestamp, updatedAt: timestamp, + git: { + baseCommit: getGitHeadCommit(repoRoot), + }, goal: options.definition.goal, execution: options.definition.execution, assurance: options.definition.assurance, @@ -1556,7 +1728,8 @@ function createApprovedRun(options) { warnings: [], }; writeJsonAtomic(runPath, run); - fs.writeFileSync(path.join(runRoot, "events.jsonl"), `${JSON.stringify({ + fs.writeFileSync(path.join(runRoot, "events.jsonl"), "", { flag: "wx" }); + appendWorkflowEvent(runRoot, { schemaVersion: "workflow-event/v1", timestamp, type: "workflow-approved", @@ -1566,7 +1739,16 @@ function createApprovedRun(options) { digest, approvalDigest, actor: "operator", - })}\n`, { flag: "wx" }); + }); + appendWorkflowEvent(runRoot, { + timestamp, + type: "budget-approved", + runId, + actor: "operator", + budgetSchemaVersion: run.budget.schemaVersion, + modelOperations: run.budget.modelOperations, + protectedAllocations: run.budget.protectedAllocations, + }); writeWorkflowProgress(runRoot, run, options.definition, { now }); return { run, diff --git a/tests/capabilities/package-lifecycle.js b/tests/capabilities/package-lifecycle.js new file mode 100644 index 0000000..b78d2da --- /dev/null +++ b/tests/capabilities/package-lifecycle.js @@ -0,0 +1,64 @@ +"use strict"; + +const childProcess = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { assert } = require("../harness/lib/assertions"); +const { assessDowngradeCompatibility } = require("../../src/compatibility/contract"); + +const repoRoot = path.resolve(__dirname, "..", ".."); +const packageJson = require(path.join(repoRoot, "package.json")); +const npmCli = process.env.npm_execpath; + +function runNpm(args, cwd) { + const result = childProcess.spawnSync(process.execPath, [npmCli, ...args], { + cwd, + encoding: "utf8", + shell: false, + windowsHide: true, + timeout: 120000, + }); + if (result.error || result.status !== 0) throw result.error || new Error(result.stderr || `npm failed with ${result.status}`); + return result.stdout; +} + +function runContract() { + assert(npmCli && fs.existsSync(npmCli), "package lifecycle requires npm_execpath"); + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "cewp-package-lifecycle-")); + try { + const legacyRoot = path.join(tempRoot, "legacy"); + const packRoot = path.join(tempRoot, "packs"); + const prefix = path.join(tempRoot, "prefix"); + fs.mkdirSync(legacyRoot, { recursive: true }); + fs.mkdirSync(packRoot, { recursive: true }); + fs.writeFileSync(path.join(legacyRoot, "package.json"), `${JSON.stringify({ + name: packageJson.name, + version: "0.13.0-beta.0", + files: ["marker.txt"], + }, null, 2)}\n`); + fs.writeFileSync(path.join(legacyRoot, "marker.txt"), "legacy fixture\n"); + const legacyPack = JSON.parse(runNpm(["pack", "--json", "--pack-destination", packRoot], legacyRoot))[0]; + const currentPack = JSON.parse(runNpm(["pack", "--json", "--pack-destination", packRoot], repoRoot))[0]; + runNpm(["install", "--prefix", prefix, "--ignore-scripts", "--no-audit", "--no-fund", path.join(packRoot, legacyPack.filename)], tempRoot); + const installedPackage = path.join(prefix, "node_modules", "@setrathex", "codex-engineering-workflow-pack", "package.json"); + assert(JSON.parse(fs.readFileSync(installedPackage, "utf8")).version === "0.13.0-beta.0", "legacy fixture installs cleanly"); + runNpm(["install", "--prefix", prefix, "--ignore-scripts", "--no-audit", "--no-fund", path.join(packRoot, currentPack.filename)], tempRoot); + assert(JSON.parse(fs.readFileSync(installedPackage, "utf8")).version === packageJson.version, "current package upgrades the legacy fixture"); + const downgrade = assessDowngradeCompatibility("0.13.0-beta.0", packageJson.version); + assert(downgrade.compatible === false && downgrade.warning, "downgrade receives a non-mutating warning"); + runNpm(["uninstall", "--prefix", prefix, "--ignore-scripts", "--no-audit", "--no-fund", packageJson.name], tempRoot); + assert(!fs.existsSync(path.dirname(installedPackage)), "uninstall removes the upgraded package"); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } +} + +try { + runContract(); + console.log(`[PASS] package install, upgrade, downgrade warning, and uninstall (${packageJson.version})`); +} catch (error) { + console.error("[FAIL] package lifecycle capability"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/contributor-surface.js b/tests/contracts/contributor-surface.js new file mode 100644 index 0000000..1fad337 --- /dev/null +++ b/tests/contracts/contributor-surface.js @@ -0,0 +1,66 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { assert } = require("../harness/lib/assertions"); + +const repoRoot = path.join(__dirname, "..", ".."); + +function read(relativePath) { + return fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); +} + +function runContract() { + const contributing = read("CONTRIBUTING.md"); + for (const requirement of [ + "npm run test:pilot-record", + "npm run check", + "git diff --check", + "good first issue", + "phase-8-to-1.0", + ".cewp-private", + "provider expansion", + ]) { + assert(contributing.toLowerCase().includes(requirement.toLowerCase()), `contribution guide includes ${requirement}`); + } + assert(/do not commit.*\.cewp\/pilots/is.test(contributing), "contribution guide keeps pilot records private"); + assert(/red.*green.*refactor/is.test(contributing), "contribution guide documents the TDD loop"); + + const security = read("SECURITY.md"); + assert(security.includes("security/advisories/new"), "security reports use a private advisory path"); + assert(/do not.*public issue/is.test(security), "security guide refuses public vulnerability details"); + for (const surface of ["path containment", "MCP", "hook trust", "redaction", "worktree ownership", "imported evidence"]) { + assert(security.toLowerCase().includes(surface.toLowerCase()), `security guide names ${surface}`); + } + + const architecture = read("docs/architecture.md"); + assert(/CEWP Core.*authoritative/is.test(architecture), "architecture keeps Core authoritative"); + assert(/one execution owner.*one managed backend/is.test(architecture), "architecture keeps ownership singular"); + for (const layer of ["Host surface", "Codex plugin", "CEWP Core", "Execution bridge", "Evidence and pilot"] ) { + assert(architecture.includes(layer), `architecture maps ${layer}`); + } + assert(/provider-specific identities.*outside.*workflow schema/is.test(architecture), "architecture keeps provider ids outside neutral schemas"); + + const extension = read("docs/contract-extension-example.md"); + for (const requirement of ["schemaVersion", "provider-neutral", "fail closed", "read compatibility", "focused contract test"]) { + assert(extension.includes(requirement), `contract extension guide includes ${requirement}`); + } + + const readme = read("README.md"); + for (const link of ["CONTRIBUTING.md", "SECURITY.md", "docs/architecture.md", "docs/contract-extension-example.md"]) { + assert(readme.includes(link), `README links ${link}`); + } + const packageJson = JSON.parse(read("package.json")); + for (const publicFile of ["CONTRIBUTING.md", "SECURITY.md", "docs/architecture.md", "docs/contract-extension-example.md"]) { + assert(packageJson.files.includes(publicFile), `package includes reviewed public file ${publicFile}`); + } +} + +try { + runContract(); + console.log("[PASS] contributor, architecture, contract, and security surfaces"); +} catch (error) { + console.error("[FAIL] contributor surface contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/event-run-verify.js b/tests/contracts/event-run-verify.js new file mode 100644 index 0000000..6cdbeb9 --- /dev/null +++ b/tests/contracts/event-run-verify.js @@ -0,0 +1,115 @@ +"use strict"; + +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const path = require("node:path"); +const { assert } = require("../harness/lib/assertions"); +const { cleanupRepo, makeTempRepo, runNode, writeFile } = require("../harness/lib/temp-repo"); +const { validDefinition } = require("./workflow-definition"); +const { approveWorkflow } = require("./workflow-scheduler"); +const { loadWorkflowRun, startWorkflowTask } = require("../../src/workflow/state"); +const { writeEvidenceReceipt } = require("../../src/evidence/receipt"); +const { + EVENT_SCHEMA_VERSION, + EVENT_CATEGORIES, + normalizeLifecycleEvent, + readLifecycleEvents, +} = require("../../src/evidence/events"); +const { verifyWorkflowRun } = require("../../src/evidence/verify"); + +const cewpCli = path.join(__dirname, "..", "..", "bin", "cewp.js"); + +function digest(content) { + return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`; +} + +function runContract() { + const repoRoot = makeTempRepo("cewp-event-run-verify-"); + try { + const approved = approveWorkflow(repoRoot, validDefinition()); + const categories = new Set(Object.values(EVENT_CATEGORIES)); + for (const required of [ + "run", "plan-revision", "task", "checkpoint", "dispatch", "operator-intervention", "verification", + "usage-observation", "estimate-revision", "budget-approval", "allocation-consumption", "threshold", + "warning-presentation", "safe-pause", "unverified-pause", "host-limit", "scope", "review", + "cancellation", "finalize", + ]) assert(categories.has(required), `event vocabulary includes ${required}`); + let found = loadWorkflowRun(repoRoot, approved.runId); + const eventsPath = path.join(found.runRoot, "events.jsonl"); + const events = readLifecycleEvents(eventsPath, { runId: approved.runId }); + assert(events.length === 2, "approved workflow writes run and budget approval events"); + assert(events[0].schemaVersion === EVENT_SCHEMA_VERSION, "new lifecycle events use event/v1"); + assert(events[0].category === "run", "workflow approval is categorized as a run event"); + assert(events[1].category === "budget-approval", "approved envelope is a distinct budget event"); + + const legacy = normalizeLifecycleEvent({ + schemaVersion: "workflow-event/v1", + timestamp: "2026-07-22T10:00:00.000Z", + type: "task-started", + runId: approved.runId, + taskId: "implement-example", + }, { allowLegacy: true }); + assert(legacy.schemaVersion === EVENT_SCHEMA_VERSION && legacy.category === "task", "legacy workflow events normalize without rewriting history"); + + const startedAt = new Date(new Date(found.run.createdAt).getTime() + 1000); + startWorkflowTask(found, "implement-example", { now: startedAt }); + found = loadWorkflowRun(repoRoot, approved.runId); + const startedEvents = readLifecycleEvents(eventsPath, { runId: approved.runId }); + for (const category of ["checkpoint", "dispatch", "task"]) { + assert(startedEvents.some((entry) => entry.category === category), `task start emits ${category} lifecycle evidence`); + } + writeEvidenceReceipt(found, { generatedAt: "2026-07-22T10:01:00.000Z" }); + const healthy = verifyWorkflowRun(repoRoot, approved.runId); + assert(healthy.schemaVersion === "run-verification/v1", "run verification is versioned"); + assert(healthy.status === "passed", "consistent run with intact partial receipt passes health verification"); + assert(healthy.execution.agentsExecuted === false && healthy.execution.verificationCommandsExecuted === false, "run verify remains read-only"); + assert(healthy.checks.some((entry) => entry.id === "receipt-integrity" && entry.status === "passed"), "receipt integrity is recomputed"); + + const cli = runNode(cewpCli, ["run", "verify", approved.runId, "--json"], repoRoot); + assert(cli.status === 0, `run verify CLI succeeds for healthy evidence: ${cli.stderr}`); + const cliOutput = JSON.parse(cli.stdout); + assert(cliOutput.schemaVersion === "operator-json/v1" && cliOutput.command === "run.verify", "run verify uses operator JSON"); + + const receiptPath = path.join(found.runRoot, "evidence-receipt.json"); + const receipt = JSON.parse(fs.readFileSync(receiptPath, "utf8")); + receipt.integrity.files[0].sha256 = digest("tampered"); + fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`); + fs.appendFileSync(eventsPath, "{malformed\n"); + fs.appendFileSync(eventsPath, `${JSON.stringify({ + schemaVersion: "event/v999", + category: "task", + timestamp: new Date(startedAt.getTime() + 1000).toISOString(), + type: "task-started", + runId: approved.runId, + })}\n`); + const checkpointDir = path.join(found.runRoot, "checkpoints", "implement-example"); + fs.rmSync(path.join(checkpointDir, fs.readdirSync(checkpointDir)[0])); + writeFile(path.join(found.runRoot, "integration", "host-binding.json"), JSON.stringify({ + schemaVersion: "host-binding/v1", + references: { worktree: { id: "gone", path: path.join(repoRoot, ".cewp-worktrees", "gone") } }, + })); + const unhealthy = verifyWorkflowRun(repoRoot, approved.runId); + assert(unhealthy.status === "failed", "health verification fails closed on evidence defects"); + assert(unhealthy.issues.some((entry) => entry.code === "malformed-event"), "malformed events are reported"); + assert(unhealthy.issues.some((entry) => entry.code === "incompatible-event-schema"), "incompatible event schemas are reported"); + assert(unhealthy.issues.some((entry) => entry.code === "receipt-integrity-mismatch"), "receipt tampering is reported"); + assert(unhealthy.issues.some((entry) => entry.code === "missing-checkpoint"), "missing active checkpoints are reported"); + assert(unhealthy.issues.some((entry) => entry.code === "stale-worktree"), "missing bound worktrees are reported"); + + const failedCli = runNode(cewpCli, ["run", "verify", approved.runId, "--json"], repoRoot); + assert(failedCli.status !== 0, "run verify exits nonzero when health verification fails"); + const failedOutput = JSON.parse(failedCli.stdout); + assert(failedOutput.data.status === "failed", "failed operator JSON remains inspectable"); + } finally { + cleanupRepo(repoRoot); + } +} + +try { + runContract(); + console.log("[PASS] event vocabulary and read-only run verification"); +} catch (error) { + console.error("[FAIL] event vocabulary and run verification contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/evidence-receipt.js b/tests/contracts/evidence-receipt.js new file mode 100644 index 0000000..87294f9 --- /dev/null +++ b/tests/contracts/evidence-receipt.js @@ -0,0 +1,210 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { assert } = require("../harness/lib/assertions"); +const { cleanupRepo, makeTempRepo, runNode, writeFile } = require("../harness/lib/temp-repo"); +const { validDefinition } = require("./workflow-definition"); +const { successfulResult } = require("./workflow-result"); +const { approveWorkflow } = require("./workflow-scheduler"); +const { + finalizeWorkflowRun, + loadWorkflowRun, + recordWorkflowResult, + recordWorkflowReview, + startWorkflowTask, +} = require("../../src/workflow/state"); +const { + buildEvidenceReceipt, + renderEvidenceReceiptMarkdown, + writeEvidenceReceipt, +} = require("../../src/evidence/receipt"); +const { recordHostObservation } = require("../../src/integration/observation"); +const { supportedSnapshot } = require("./integration-capabilities"); + +const cewpCli = path.join(__dirname, "..", "..", "bin", "cewp.js"); + +function singleTaskDefinition() { + const definition = validDefinition(); + definition.workflowId = "evidence-receipt"; + definition.tasks = [definition.tasks[0]]; + definition.budget.maxTargetedVerificationRuns = 6; + definition.budget.maxFullVerificationRuns = 1; + return definition; +} + +function reviewerCandidate(run) { + return { + schemaVersion: "review-result/v1", + reviewId: `${run.runId}-final-review`, + runId: run.runId, + workflowDigest: run.workflow.digest, + scope: { kind: "workflow", taskId: null, checkpointId: null }, + completedAt: "2026-07-18T12:05:00.000Z", + independent: true, + decision: "PASS", + summary: "Independent evidence receipt review passed.", + findings: [], + evidence: [{ kind: "review-report", path: "evidence/review.md" }], + usage: { + managedOperations: { label: "observed", value: 1, source: "codex-exec-jsonl" }, + capturedOutputBytes: { label: "observed", value: 128, source: "cewp-bounded-output" }, + managedTokens: { label: "unknown", value: null, reason: "fixture omits token totals" }, + hostInternal: { label: "unknown", value: null, reason: "host usage is unavailable" }, + }, + }; +} + +function completeRun(repoRoot) { + const approved = approveWorkflow(repoRoot, singleTaskDefinition()); + let found = loadWorkflowRun(repoRoot, approved.runId); + const started = startWorkflowTask(found, "implement-example", { + now: new Date("2026-07-18T12:00:00.000Z"), + }); + writeFile(path.join(repoRoot, "evidence", "baseline.json"), "{\"status\":\"passed\"}\n"); + writeFile(path.join(repoRoot, "evidence", "targeted.json"), "{\"status\":\"passed\"}\n"); + writeFile(path.join(repoRoot, "evidence", "change.diff"), "diff --git a/src/example.js b/src/example.js\n"); + writeFile(path.join(repoRoot, "evidence", "review.md"), "# Independent review\n\nPASS\n"); + const result = successfulResult(approved, started.checkpoint, [{ + command: "node --test tests/example.test.js", + status: "passed", + evidencePath: "evidence/targeted.json", + }]); + found = loadWorkflowRun(repoRoot, approved.runId); + recordWorkflowResult(found, "implement-example", result); + found = loadWorkflowRun(repoRoot, approved.runId); + recordWorkflowReview(found, reviewerCandidate(approved)); + found = loadWorkflowRun(repoRoot, approved.runId); + finalizeWorkflowRun(found, { now: new Date("2026-07-18T12:06:00.000Z") }); + return loadWorkflowRun(repoRoot, approved.runId); +} + +function runContract() { + const repoRoot = makeTempRepo("cewp-evidence-receipt-"); + try { + const found = completeRun(repoRoot); + recordHostObservation(found, { + schemaVersion: "host-observation/v1", + observationId: "receipt-usage-0001", + observedAt: "2026-07-18T12:06:30.000Z", + source: { + path: "codex-exec", + codexVersion: "codex-cli 0.137.0", + schemaVersion: "codex-exec-jsonl/v1", + authenticationBoundary: "managed-child", + }, + scope: { kind: "workflow-run", runId: found.run.runId, taskId: null, checkpointId: null }, + category: "thread-usage", + rawCategory: "turn.completed.usage", + availability: "observed", + data: { inputTokens: 100, cachedInputTokens: 40, outputTokens: 20, reasoningOutputTokens: 5 }, + raw: { input_tokens: 100, cached_input_tokens: 40, output_tokens: 20, reasoning_output_tokens: 5 }, + }, { capabilities: supportedSnapshot() }); + recordHostObservation(found, { + schemaVersion: "host-observation/v1", + observationId: "receipt-usage-imported-0001", + observedAt: "2026-07-18T12:06:31.000Z", + source: { + path: "audit-import", + codexVersion: null, + schemaVersion: "external-receipt/v1", + authenticationBoundary: "external-owner", + }, + scope: { kind: "workflow-run", runId: found.run.runId, taskId: null, checkpointId: null }, + category: "thread-usage", + rawCategory: "external.usage", + availability: "imported", + data: { inputTokens: 50, cachedInputTokens: 0, outputTokens: 10, reasoningOutputTokens: 0 }, + raw: { imported_total: 60 }, + }, { capabilities: supportedSnapshot() }); + writeFile(path.join(found.runRoot, "adapter-output", "prompt.md"), "TOP_SECRET_PROMPT\n"); + const options = { generatedAt: "2026-07-18T12:07:00.000Z" }; + const first = buildEvidenceReceipt(found, options); + const second = buildEvidenceReceipt(found, options); + assert(first.schemaVersion === "evidence-receipt/v1", "evidence receipt is versioned"); + assert(JSON.stringify(first) === JSON.stringify(second), "fixed-timestamp receipt generation is deterministic"); + assert(first.completeness.status === "complete", "finalized reviewed run produces a complete receipt"); + assert(first.goal === found.run.goal, "receipt retains the approved goal"); + assert(first.sourcePlan.kind === "direct-goal", "receipt retains source-plan identity"); + assert(first.execution.owner === "managed" && first.execution.backend === "codex-exec", "receipt retains owner and backend"); + assert(first.tasks.length === 1 && first.checkpoints.length === 1, "receipt explains task and checkpoint structure"); + assert(first.tasks[0].changedFiles.includes("src/example.js"), "receipt records changed files"); + assert(first.tasks[0].verification.targeted[0].status === "passed", "receipt records verification evidence"); + for (const category of ["scope", "verification", "usage-observation", "allocation-consumption", "checkpoint"]) { + assert(first.events.some((entry) => entry.category === category), `completed task emits ${category} lifecycle evidence`); + } + assert(first.reviewer.decision === "PASS", "receipt retains independent reviewer PASS"); + assert(first.usage.managedOperations.label === "observed", "receipt aggregates observed managed operations"); + assert(first.usage.hostInternal.label === "unknown", "unavailable host usage remains unknown"); + assert(first.usage.observations.every((entry) => entry.schemaVersion === "usage-observation/v1"), "usage observations are independently versioned"); + const hostUsage = first.usage.observations.find((entry) => entry.rawCategory === "turn.completed.usage"); + assert(hostUsage.source.authenticationBoundary === "managed-child", "usage observation preserves authentication boundary"); + assert(hostUsage.value.cachedInputTokens === 40 && hostUsage.value.reasoningOutputTokens === 5, "raw usage categories remain distinct"); + assert(hostUsage.rawValue === null, "receipt excludes raw host payload while preserving normalized categories"); + const importedUsage = first.usage.observations.find((entry) => entry.rawCategory === "external.usage"); + assert(importedUsage.availability === "imported" && importedUsage.evidenceClass === "imported", "imported usage is never relabeled observed"); + assert(importedUsage.billingImpact === "unknown", "host usage never implies a billing impact"); + assert(first.events.some((entry) => entry.type === "usage-observed" && entry.category === "usage-observation"), "usage recording emits the versioned lifecycle category"); + assert(first.estimate.estimator.version === "local-history/v1" && first.estimate.sampleBasis.count === 0, "unknown estimate remains reproducible from its empty sample basis"); + assert(first.estimate.calibrationSnapshot.intervalCoverage === null && first.estimate.drift.state === "unknown", "estimate calibration and drift stay explicit"); + assert(first.budget.compliance.status === "passed", "receipt proves the approved budget ceilings were respected"); + assert(first.budget.compliance.protectedAllocationsRespected === true, "receipt proves protected allocations were not overspent"); + assert(first.cost.apiEquivalent.label === "unknown", "currency cost is not invented"); + assert(first.integrity.claim === "tamper-evident-local-metadata", "integrity claim is explicitly local and tamper-evident"); + assert(first.integrity.files.length > 3, "receipt hashes canonical evidence files"); + assert(first.integrity.files.every((entry) => /^sha256:[a-f0-9]{64}$/.test(entry.sha256)), "evidence file hashes are stable sha256 values"); + assert(first.integrity.files.map((entry) => entry.path).join("\n") === first.integrity.files.map((entry) => entry.path).sort().join("\n"), "integrity inventory is sorted"); + assert(first.git.baseCommit.status === "known" && first.git.headCommit.status === "known", "receipt records source git identities"); + + const markdown = renderEvidenceReceiptMarkdown(first); + assert(markdown.includes("# CEWP Evidence Receipt"), "Markdown receipt is human-readable"); + assert(markdown.includes("Host-internal usage: unknown"), "Markdown keeps explicit unknowns"); + for (const section of ["## Tasks", "## Checkpoints", "## Commands and verification", "## Budget", "## Usage and estimate", "## Controls", "## Final review", "## Timestamps"]) { + assert(markdown.includes(section), `Markdown receipt includes ${section}`); + } + assert(markdown.includes("implement-example-attempt-0001"), "Markdown identifies checkpoint evidence"); + assert(markdown.includes("node --test tests/example.test.js"), "Markdown identifies approved verification commands"); + assert(markdown.includes("Protected allocations: passed"), "Markdown proves protected reserve compliance"); + assert(markdown.includes("Decision: PASS"), "Markdown exposes independent final review"); + assert(markdown.includes("Provider: codex") && markdown.includes("Effective model: unknown"), "Markdown distinguishes known provider from unknown effective model"); + assert(!markdown.includes("TOP_SECRET_PROMPT"), "Markdown does not include prompt or raw log content"); + const written = writeEvidenceReceipt(found, options); + assert(fs.existsSync(written.paths.json) && fs.existsSync(written.paths.markdown), "JSON and Markdown receipts are written locally"); + assert(JSON.stringify(written.receipt) === JSON.stringify(first), "written receipt uses the same normalized model"); + const cli = runNode(cewpCli, ["workflow", "receipt", found.run.runId, "--json"], repoRoot); + assert(cli.status === 0, `workflow receipt CLI succeeds: ${cli.stderr}`); + const cliOutput = JSON.parse(cli.stdout); + assert(cliOutput.command === "workflow.receipt", "receipt output uses operator JSON"); + assert(cliOutput.data.receipt.schemaVersion === "evidence-receipt/v1", "CLI writes the normalized receipt contract"); + + fs.rmSync(path.join(repoRoot, "evidence", "change.diff")); + const missingEvidence = buildEvidenceReceipt(found, options); + assert(missingEvidence.completeness.status === "partial", "missing referenced evidence closes receipt completeness"); + assert(missingEvidence.warnings.some((warning) => warning.code === "missing-referenced-evidence"), "missing evidence has an actionable warning"); + fs.appendFileSync(path.join(found.runRoot, "integration", "host-observations.jsonl"), "{malformed\n"); + const malformedUsage = buildEvidenceReceipt(found, options); + assert(malformedUsage.completeness.status === "partial", "malformed usage ledger cannot produce a complete receipt"); + assert(malformedUsage.warnings.some((warning) => warning.code === "malformed-usage-observation-ledger"), "malformed usage ledger has an explicit warning"); + + const partialRepo = makeTempRepo("cewp-evidence-receipt-partial-"); + try { + const partialRun = approveWorkflow(partialRepo, singleTaskDefinition()); + const partial = buildEvidenceReceipt(loadWorkflowRun(partialRepo, partialRun.runId), options); + assert(partial.completeness.status === "partial", "incomplete historical run produces a partial receipt"); + assert(partial.warnings.some((warning) => warning.code === "run-not-finalized"), "partial receipt explains why it is incomplete"); + } finally { + cleanupRepo(partialRepo); + } + } finally { + cleanupRepo(repoRoot); + } +} + +try { + runContract(); + console.log("[PASS] normalized evidence receipt is deterministic and tamper-evident"); +} catch (error) { + console.error("[FAIL] evidence receipt contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/evidence-redaction.js b/tests/contracts/evidence-redaction.js new file mode 100644 index 0000000..dc82abc --- /dev/null +++ b/tests/contracts/evidence-redaction.js @@ -0,0 +1,77 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { assert } = require("../harness/lib/assertions"); +const { cleanupRepo, makeTempRepo, runNode, writeFile } = require("../harness/lib/temp-repo"); +const { validDefinition } = require("./workflow-definition"); +const { approveWorkflow } = require("./workflow-scheduler"); +const { loadWorkflowRun } = require("../../src/workflow/state"); +const { exportRedactedEvidence, redactEvidenceValue } = require("../../src/evidence/redaction"); + +const cewpCli = path.join(__dirname, "..", "..", "bin", "cewp.js"); +const SECRET = "ghp_abcdefghijklmnopqrstuvwxyz1234567890"; + +function runContract() { + const synthetic = redactEvidenceValue({ + authorization: "Bearer top-secret-bearer", + password: "hunter2", + clientSecret: "client-secret-value", + command: `tool --token=${SECRET} --count=2`, + url: "https://alice:private@example.invalid/repo", + privateBlock: "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----", + absoluteWindowsPath: "C:\\Users\\Alice\\private\\notes.txt", + traversalPath: "../../etc/passwd", + dotenvPath: "config/.env", + changedFiles: ["src/safe.js", "config/.env"], + note: "See C:\\Users\\Alice\\private\\notes.txt before export", + managedTokens: { label: "observed", value: 42 }, + }); + const serializedSynthetic = JSON.stringify(synthetic.value); + for (const secret of ["top-secret-bearer", "hunter2", "client-secret-value", SECRET, "alice:private", "BEGIN PRIVATE KEY", "Users\\\\Alice", "../../etc/passwd", "config/.env"]) { + assert(!serializedSynthetic.includes(secret), `redaction removes adversarial value ${secret}`); + } + assert(synthetic.value.managedTokens.value === 42, "non-secret usage token counts are preserved"); + assert(synthetic.value.changedFiles[0] === "src/safe.js" && synthetic.value.changedFiles[1] === "[REDACTED_PATH]", "path arrays preserve safe paths and redact sensitive paths"); + assert(synthetic.replacements >= 7, "redaction reports replacement count"); + + const repoRoot = makeTempRepo("cewp-evidence-redaction-"); + try { + const definition = validDefinition(); + definition.workflowId = "redacted-export"; + definition.goal = `Fix using token=${SECRET}`; + definition.tasks[0].verification.targeted[0] = `node test.js --password=hunter2`; + const approved = approveWorkflow(repoRoot, definition); + const found = loadWorkflowRun(repoRoot, approved.runId); + writeFile(path.join(found.runRoot, "adapter-output", "prompt.md"), "RAW_PROMPT_DO_NOT_EXPORT\n"); + const exported = exportRedactedEvidence(found, { generatedAt: "2026-07-22T14:00:00.000Z" }); + assert(exported.receipt.schemaVersion === "evidence-receipt/v1", "redacted export preserves receipt contract"); + assert(exported.receipt.redaction.schemaVersion === "redaction-policy/v1" && exported.receipt.redaction.applied, "redaction is explicit and versioned"); + assert(exported.report.schemaVersion === "operator-report/v1", "redacted export preserves report contract"); + assert(Object.values(exported.paths).every((entry) => !path.isAbsolute(entry) && !entry.includes("..")), "export returns repository-relative paths"); + const contents = Object.values(exported.paths).map((relative) => fs.readFileSync(path.join(repoRoot, relative), "utf8")).join("\n"); + for (const secret of [SECRET, "hunter2", "RAW_PROMPT_DO_NOT_EXPORT", ""]) { + assert(!contents.includes(secret), `export excludes ${secret}`); + } + assert(!/https?:\/\//.test(fs.readFileSync(path.join(repoRoot, exported.paths.html), "utf8")), "redacted HTML remains offline"); + assert(contents.includes("Redaction") && contents.includes("redaction-policy/v1"), "export artifacts disclose that redaction was applied"); + assert(!fs.existsSync(path.join(found.runRoot, "evidence-receipt.json")), "export does not overwrite or create the canonical receipt"); + + const cli = runNode(cewpCli, ["workflow", "export", approved.runId, "--json"], repoRoot); + assert(cli.status === 0, `workflow export CLI succeeds: ${cli.stderr}`); + const output = JSON.parse(cli.stdout); + assert(output.command === "workflow.export" && output.data.receipt.redaction.applied, "CLI emits only the redacted export model"); + assert(!cli.stdout.includes(SECRET) && !cli.stdout.includes(repoRoot), "CLI JSON does not leak secrets or absolute repository paths"); + } finally { + cleanupRepo(repoRoot); + } +} + +try { + runContract(); + console.log("[PASS] adversarial evidence redaction and safe export"); +} catch (error) { + console.error("[FAIL] evidence redaction contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/integration-binding.js b/tests/contracts/integration-binding.js index 91da018..0e55c08 100644 --- a/tests/contracts/integration-binding.js +++ b/tests/contracts/integration-binding.js @@ -3,16 +3,20 @@ 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 { buildEvidenceReceipt } = require("../../src/evidence/receipt"); + +const cewpCli = path.join(__dirname, "..", "..", "bin", "cewp.js"); function assertThrows(action, expected, label) { let error; @@ -36,6 +40,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 +144,132 @@ 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 auditEvidence = buildEvidenceReceipt(auditFound, { generatedAt: "2026-07-22T15:20:00.000Z" }); + const importedControl = auditEvidence.policy.controls.controls.find((entry) => entry.classification === "imported"); + assert(importedControl.effect === "observed-not-enforced", "audit-only evidence receipt preserves observed-not-enforced effect"); + assert(auditEvidence.policy.controls.summary.preventiveEnforced === 0, "audit-only evidence receipt never reports preventive enforcement"); + 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..08a815c --- /dev/null +++ b/tests/contracts/integration-hook-evidence.js @@ -0,0 +1,193 @@ +"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 packageVersion = require("../../package.json").version; + +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 === packageVersion, "approval binds the current 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/operator-report.js b/tests/contracts/operator-report.js new file mode 100644 index 0000000..d7b9a97 --- /dev/null +++ b/tests/contracts/operator-report.js @@ -0,0 +1,73 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { assert } = require("../harness/lib/assertions"); +const { cleanupRepo, makeTempRepo, runNode, writeFile } = require("../harness/lib/temp-repo"); +const { validDefinition } = require("./workflow-definition"); +const { approveWorkflow } = require("./workflow-scheduler"); +const { loadWorkflowRun, startWorkflowTask } = require("../../src/workflow/state"); +const { buildEvidenceReceipt } = require("../../src/evidence/receipt"); +const { + buildOperatorReport, + renderOperatorReportHtml, + writeOperatorReport, +} = require("../../src/evidence/report"); + +const cewpCli = path.join(__dirname, "..", "..", "bin", "cewp.js"); + +function runContract() { + const repoRoot = makeTempRepo("cewp-operator-report-"); + try { + const definition = validDefinition(); + definition.goal = "Audit safely"; + const approved = approveWorkflow(repoRoot, definition); + let found = loadWorkflowRun(repoRoot, approved.runId); + startWorkflowTask(found, "implement-example", { + now: new Date(new Date(found.run.createdAt).getTime() + 1000), + }); + found = loadWorkflowRun(repoRoot, approved.runId); + writeFile(path.join(found.runRoot, "adapter-output", "prompt.md"), "REPORT_SECRET_PROMPT\n"); + const generatedAt = "2026-07-22T12:00:00.000Z"; + const receipt = buildEvidenceReceipt(found, { generatedAt }); + const first = buildOperatorReport(receipt); + const second = buildOperatorReport(receipt); + assert(first.schemaVersion === "operator-report/v1", "operator report is versioned"); + assert(JSON.stringify(first) === JSON.stringify(second), "operator report model is deterministic for one receipt"); + assert(first.progress.totalTasks === 2 && first.progress.activeTasks === 1, "report derives task progress"); + assert(Array.isArray(first.planRevisions) && Array.isArray(first.checkpoints), "report exposes revision and checkpoint evidence"); + assert(first.checkpoints[0].verification.baseline.status === "pending", "checkpoint verification evidence is preserved in the report model"); + assert(first.values.observed.managedOperations.label === "unknown", "report preserves observed-versus-unknown truth"); + assert(first.values.estimated.schemaVersion === "usage-estimate/v1", "report exposes estimates separately"); + assert(first.values.budgeted.compliance.status === "passed", "report exposes budget compliance"); + assert(first.reserves.allocations.some((entry) => entry.protected), "report identifies protected reserves"); + assert(first.controls.enforced.length === 0 && first.controls.observed.length === 0, "absent controls are not invented"); + assert(first.finalReview.status === "pending", "report presents final-review state without claiming PASS"); + + const html = renderOperatorReportHtml(first); + assert(html.includes("") && html.includes("Content-Security-Policy"), "report is standalone HTML with an offline policy"); + assert(!html.includes(""), "goal content is HTML escaped"); + assert(html.includes("<script>alert('unsafe')</script>"), "escaped goal remains understandable"); + assert(!/https?:\/\//.test(html), "report has no network dependency"); + assert(!html.includes("REPORT_SECRET_PROMPT"), "report excludes raw prompt content"); + assert(html.includes("Protected reserves") && html.includes("Final review"), "offline report includes critical operator sections"); + + const written = writeOperatorReport(found, { generatedAt }); + assert(fs.existsSync(written.paths.json) && fs.existsSync(written.paths.html), "operator JSON and HTML are written locally"); + const cli = runNode(cewpCli, ["workflow", "report", approved.runId, "--json"], repoRoot); + assert(cli.status === 0, `workflow report CLI succeeds: ${cli.stderr}`); + const output = JSON.parse(cli.stdout); + assert(output.command === "workflow.report" && output.data.report.schemaVersion === "operator-report/v1", "CLI returns the shared report model"); + } finally { + cleanupRepo(repoRoot); + } +} + +try { + runContract(); + console.log("[PASS] static offline operator report"); +} catch (error) { + console.error("[FAIL] operator report contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/performance-budget.js b/tests/contracts/performance-budget.js new file mode 100644 index 0000000..f9d1ece --- /dev/null +++ b/tests/contracts/performance-budget.js @@ -0,0 +1,29 @@ +"use strict"; + +const { assert } = require("../harness/lib/assertions"); +const { PERFORMANCE_BUDGET_SCHEMA_VERSION, buildPerformanceBudgets, evaluatePerformanceMeasurement } = require("../../src/performance/budgets"); + +function runContract() { + const policy = buildPerformanceBudgets(); + assert(policy.schemaVersion === PERFORMANCE_BUDGET_SCHEMA_VERSION, "performance budget is versioned"); + assert(policy.exclusions.includes("model-execution"), "model execution is excluded from CEWP speed guarantees"); + assert(policy.exclusions.includes("repository-verification-command"), "repository commands are excluded from CEWP bookkeeping budgets"); + for (const id of ["doctor-json", "workflow-validation", "status-progress", "checkpoint-bookkeeping", "package-lifecycle", "large-run-inspection"]) { + assert(Number.isFinite(policy.operations[id].ceilingMs), `${id} has a finite CEWP-owned ceiling`); + } + const passing = evaluatePerformanceMeasurement(policy, { operation: "doctor-json", durationMs: 125, excludedDurationMs: 0 }); + assert(passing.status === "within-budget", "bounded measurement passes"); + const failing = evaluatePerformanceMeasurement(policy, { operation: "doctor-json", durationMs: 2500, excludedDurationMs: 0 }); + assert(failing.status === "over-budget" && failing.releaseBlocking === true, "CEWP overhead regression is release-blocking"); + const invalid = evaluatePerformanceMeasurement(policy, { operation: "checkpoint-bookkeeping", durationMs: 100, excludedDurationMs: 150 }); + assert(invalid.status === "invalid-measurement", "excluded model or command time cannot exceed the sample"); +} + +try { + runContract(); + console.log("[PASS] CEWP-owned performance budgets exclude model execution"); +} catch (error) { + console.error("[FAIL] performance budget contract"); + console.error(error && error.stack ? error.stack : error); + process.exitCode = 1; +} diff --git a/tests/contracts/pilot-export.js b/tests/contracts/pilot-export.js new file mode 100644 index 0000000..6ed85b2 --- /dev/null +++ b/tests/contracts/pilot-export.js @@ -0,0 +1,81 @@ +"use strict"; + +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { assert } = require("../harness/lib/assertions"); +const { cleanupRepo, makeTempRepo, readJson, runNode, writeJson } = require("../harness/lib/temp-repo"); + +const cewpCli = path.join(__dirname, "..", "..", "bin", "cewp.js"); +const SECRET = "sk-abcdefghijklmnopqrstuvwxyz123456"; + +function runContract() { + const repoRoot = makeTempRepo("cewp-pilot-export-"); + const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), "cewp-pilot-export-outside-")); + try { + const created = runNode(cewpCli, [ + "pilot", "create", + "--pilot-id", "dogfood-1", + "--participant", "maintainer-dogfood", + "--participant-id", "maintainer-1", + "--json", + ], repoRoot); + assert(created.status === 0, `pilot fixture is created: ${created.stderr}`); + const canonicalPath = path.join(repoRoot, ".cewp", "pilots", "dogfood-1", "record.json"); + const canonical = readJson(canonicalPath); + canonical.adversarialFixture = { + apiKey: SECRET, + repositoryPath: "C:\\Users\\pilot\\private-repo", + note: ` Bearer ${SECRET}`, + }; + writeJson(canonicalPath, canonical); + const canonicalBefore = fs.readFileSync(canonicalPath, "utf8"); + + const exported = runNode(cewpCli, ["pilot", "export", "dogfood-1", "--json"], repoRoot); + assert(exported.status === 0, `pilot export succeeds: ${exported.stderr}`); + const output = JSON.parse(exported.stdout); + assert(output.command === "pilot.export", "pilot export has a stable command envelope"); + assert(output.data.export.schemaVersion === "pilot-export/v1", "pilot export is versioned"); + assert(output.data.export.records.length === 1 && output.data.export.records[0].pilotId === "dogfood-1", "selected export contains only the requested pilot record"); + assert(output.data.export.redaction.applied === true && output.data.export.redaction.replacements >= 3, "export discloses applied redaction and replacement count"); + assert(output.data.export.redaction.canonicalRecordsModified === false, "redaction never claims to modify canonical records"); + assert(output.data.export.privacy.rawPromptsIncluded === false && output.data.export.privacy.rawLogsIncluded === false, "raw prompts and logs are excluded"); + + const jsonPath = path.join(repoRoot, output.data.paths.json); + const markdownPath = path.join(repoRoot, output.data.paths.markdown); + assert(!path.isAbsolute(output.data.paths.json) && !output.data.paths.json.includes(".."), "export returns repository-relative contained paths"); + assert(fs.existsSync(jsonPath) && fs.existsSync(markdownPath), "separate JSON and Markdown exports are written"); + const exportedContents = `${fs.readFileSync(jsonPath, "utf8")}\n${fs.readFileSync(markdownPath, "utf8")}`; + for (const sensitive of [SECRET, "C:\\Users\\pilot\\private-repo", "