diff --git a/.gitignore b/.gitignore index f7f6ae75b1f..b09d5225c24 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ **/node_modules **/node_modules/ dist/ +packages/paperclip-runner/runner/target/ ui/storybook-static/ .env *.tsbuildinfo diff --git a/packages/paperclip-runner/README.md b/packages/paperclip-runner/README.md index fa8df4f4b9a..e6147ab6d72 100644 --- a/packages/paperclip-runner/README.md +++ b/packages/paperclip-runner/README.md @@ -1,9 +1,11 @@ # Paperclip Native Runner This workspace is the standalone development boundary for Paperclip's native -runner protocol, harness drivers, and normalized session backends. Phase 0 -contains a deterministic tracer and an in-memory mock control plane. It does not -import or start Paperclip's server, UI, CLI, or production database packages. +runner protocol, harness drivers, and normalized session backends. The +production runner direction is Rust; `runner/` is its Cargo workspace. The +TypeScript surface remains a control-plane/client reference implementation. +Phase 0 runs both implementations against one language-neutral fixture without +importing or starting Paperclip's server, UI, CLI, or production database. ## Phase 0 quick start @@ -14,6 +16,9 @@ pnpm install --filter @paperclipai/paperclip-runner --lockfile=false --offline - pnpm --filter @paperclipai/paperclip-runner verify ``` +The verification command requires a stable Rust toolchain with `cargo` on +`PATH`, in addition to Node.js 20+ and pnpm 9+. + The tracer's final line is stable: ```json @@ -30,12 +35,14 @@ pnpm --filter @paperclipai/paperclip-runner trace:phase0 | Command | Purpose | |---|---| -| `build` | Compile the TypeScript public surface to `dist/`. | -| `typecheck` | Check the public surface without emitting files. | -| `test` | Run fixture, mock-contract, stable-output, and negative-boundary tests. | -| `check:forbidden-imports` | Reject imports or dependencies that cross into Paperclip core. | +| `build` | Compile the TypeScript public surface and Rust workspace. | +| `typecheck` | Check both language surfaces without a release build. | +| `test` | Run Rust/TypeScript fixture, mock-contract, stable-output, and negative-boundary tests. | +| `check:forbidden-imports` | Reject TypeScript imports and Cargo path dependencies that cross into Paperclip core. | +| `check:phase0-parity` | Require byte-for-byte equivalent Rust and TypeScript tracer output. | | `docs:validate` | Validate local documentation links and the OKF v0.2 bundle. | -| `trace:phase0` | Build, start the mock core, replay the fixture, print the result, and exit. | +| `trace:phase0` | Run the Rust mock-core tracer, print the stable result, and exit. | +| `trace:phase0:typescript` | Run the TypeScript reference tracer directly. | | `verify` | Run the complete Phase 0 acceptance sequence. | ## Navigate @@ -49,6 +56,7 @@ pnpm --filter @paperclipai/paperclip-runner trace:phase0 - [Implementation plan](spec/paperclip-native-runner-implementation-plan.md) - [Normative spike specification](spec/paperclip-native-runner-spike-spec.md) -Phase 0 intentionally does not include a daemon, network transport, browser -runtime, production Paperclip adapter, or real model harness. Those layers remain -behind later phase checkpoints in the implementation plan. +Phase 0 establishes `runner-core`, not the `paperclip-runnerd` daemon. Network +transport, process supervision, browser runtime, production Paperclip adapter, +and real model harnesses remain behind later phase checkpoints in the +implementation plan. diff --git a/packages/paperclip-runner/docs/architecture.md b/packages/paperclip-runner/docs/architecture.md index af76289dd0d..fb1be47969a 100644 --- a/packages/paperclip-runner/docs/architecture.md +++ b/packages/paperclip-runner/docs/architecture.md @@ -3,13 +3,15 @@ ## Dependency direction ```text -protocol fixture - | - v -fixture validator --> runner contracts <-- mock control-plane adapter - | - v - Phase 0 tracer + language-neutral protocol fixture + | + +---------------+---------------+ + v v + Rust runner-core + mock path TypeScript contracts + mock path + | | + +---------------+---------------+ + v + byte-identical Phase 0 result Paperclip core (later) --> implements ControlPlanePort / NativeSessionBackend ``` @@ -27,12 +29,27 @@ The standalone package does not reach backward into a Paperclip implementation. a future control-plane consumer. Environment placement is not implied by the backend type. -The Phase 0 contracts are deliberately sketches: they name responsibility and -dependency direction without prematurely implementing the Phase 1 protocol or -the Phase 2 runner daemon. +The Phase 0 TypeScript contracts are deliberately sketches: they name +responsibility and dependency direction without prematurely implementing the +Phase 1 protocol. The Rust `runner-core` crate establishes the production +language/package boundary and executes the same fixture; it is not yet the +Phase 2 runner daemon. + +## Language ownership + +- Rust is the production direction for deterministic runner behavior, + supervision, durable delivery, and the eventual `paperclip-runnerd` binary. +- TypeScript owns the control-plane/browser side and remains a useful reference + client/test oracle. +- JSON Schema and shared fixtures are the language-neutral authority. Phase 0 + begins with one checked JSON fixture; Phase 1 replaces its narrow validation + shape with the full PRP schema and conformance corpus. +- `check:phase0-parity` prevents either implementation from introducing a + language-specific observable result. ## Allowed dependencies +- Rust crates declared by the package-local Cargo workspace. - Node.js standard-library modules. - Third-party packages declared by this workspace when a later phase needs them. - Files within `packages/paperclip-runner/`. @@ -49,10 +66,10 @@ The following imports and package dependencies are rejected: internals unless a later boundary review explicitly allows a public contract; - relative or absolute imports that escape `packages/paperclip-runner/`. -This rule applies to type-only imports, exports, dynamic imports, and CommonJS -`require` calls. The negative fixture at -`test-fixtures/forbidden-import/core-import.ts` intentionally imports a server -module and must fail the checker. +This rule applies to type-only imports, exports, dynamic imports, CommonJS +`require` calls, Rust include/path attributes, and Cargo path dependencies. The +negative fixtures under `test-fixtures/` intentionally reference `server/` and +must fail the checker. ## Enforcement @@ -78,6 +95,8 @@ changes local object state. The tracer performs this sequence: 6. print a stable JSON identity/result and stop the adapter. No socket, database, browser, Paperclip process, or model process is started. +The default command executes this sequence in Rust. The TypeScript reference +executes the same sequence, and the parity check compares their complete stdout. ## Future integration rule diff --git a/packages/paperclip-runner/docs/index.md b/packages/paperclip-runner/docs/index.md index b6d22c13b9a..44c3ccda1e9 100644 --- a/packages/paperclip-runner/docs/index.md +++ b/packages/paperclip-runner/docs/index.md @@ -4,7 +4,7 @@ The tutorials are cumulative and are always run from the repository root. ## Accepted implementation phases -- [Phase 0: standalone tracer](tutorials/phase-00-standalone-tracer.md) — install the workspace, validate its boundary and knowledge bundle, then run the deterministic mock-core path. +- [Phase 0: standalone tracer](tutorials/phase-00-standalone-tracer.md) — install the workspace, validate its Rust/TypeScript boundary and knowledge bundle, then run the deterministic Rust mock-core path and cross-language parity check. - [Cumulative end-to-end tutorial](tutorials/end-to-end.md) — the shortest complete workflow available at the current phase. ## Reference diff --git a/packages/paperclip-runner/docs/tutorials/end-to-end.md b/packages/paperclip-runner/docs/tutorials/end-to-end.md index d5c5d555662..425daea8541 100644 --- a/packages/paperclip-runner/docs/tutorials/end-to-end.md +++ b/packages/paperclip-runner/docs/tutorials/end-to-end.md @@ -1,16 +1,18 @@ # Native Runner Cumulative End-to-End Tutorial This tutorial grows with each accepted phase. At Phase 0, the complete runnable -system is the standalone mock-core tracer; there is intentionally no daemon, -network connection, browser console, real harness, or Paperclip bridge. +system is the standalone Rust mock-core tracer plus its TypeScript parity oracle; +there is intentionally no daemon, network connection, browser console, real +harness, or Paperclip bridge. ## Current end-to-end path 1. Follow [Phase 0: Run the Standalone Tracer](phase-00-standalone-tracer.md). 2. Confirm the final JSON contains `run_phase0_0001`, `session_phase0_0001`, and `succeeded`. -3. Confirm the shell prompt returns and no Paperclip service was started. -4. Open the [Phase 0 journal entry](../../knowledge/journal/2026-08-07-phase-00.md) +3. Confirm the cross-language parity check passes. +4. Confirm the shell prompt returns and no Paperclip service was started. +5. Open the [Phase 0 journal entry](../../knowledge/journal/2026-08-07-phase-00.md) and its linked verification evidence. The one-command form after installation is: @@ -23,8 +25,8 @@ pnpm --filter @paperclipai/paperclip-runner verify - the fixture is validated before any mock-core mutation; - event sequence and run identity agree through the terminal result; -- the printed output is covered by an exact string assertion; -- a deliberate server import is rejected; +- Rust and TypeScript printed output is covered by exact string and parity assertions; +- deliberate TypeScript and Cargo references to Paperclip core are rejected; - documentation and journal indexes are machine checked; - the package remains runnable without Paperclip core. diff --git a/packages/paperclip-runner/docs/tutorials/phase-00-standalone-tracer.md b/packages/paperclip-runner/docs/tutorials/phase-00-standalone-tracer.md index 54161d667a1..e038ec5af54 100644 --- a/packages/paperclip-runner/docs/tutorials/phase-00-standalone-tracer.md +++ b/packages/paperclip-runner/docs/tutorials/phase-00-standalone-tracer.md @@ -2,16 +2,17 @@ ## Outcome -You will install only the runner workspace's declared tools, compile it, run its -tests and static checks, start the in-memory mock core, validate the minimal -fixture, print a deterministic result, and return to the shell. Paperclip itself -does not start. +You will install only the runner workspace's declared Node tools, compile its +Rust and TypeScript surfaces, run their tests and static checks, start the Rust +in-memory mock core, validate the minimal shared fixture, print a deterministic +result, and return to the shell. Paperclip itself does not start. ## Prerequisites - repository checkout on the assigned runner branch; - Node.js 20 or newer; - pnpm 9 or newer; +- stable Rust with `cargo` on `PATH`; - commands run from the repository root. ## 1. Install the package tools @@ -31,8 +32,8 @@ root lockfile. `--offline` proves Phase 0 needs no newly downloaded package. pnpm --filter @paperclipai/paperclip-runner build ``` -Expected result: TypeScript exits with status 0 and writes package-local `dist/` -files. +Expected result: TypeScript writes package-local `dist/` files and Cargo builds +the `paperclip-runner-core` crate under `runner/target/`. ## 3. Run the behavior and boundary tests @@ -41,9 +42,9 @@ pnpm --filter @paperclipai/paperclip-runner test pnpm --filter @paperclipai/paperclip-runner check:forbidden-imports ``` -The tests cover fixture validation, the complete mock `ControlPlanePort` path, -stable output, and a negative core-import fixture. The standalone boundary check -must print `Standalone boundary check passed.` +The tests cover fixture validation, complete Rust and TypeScript mock-core paths, +stable output, and negative TypeScript/Cargo core-dependency fixtures. The +standalone boundary check must print `Standalone boundary check passed.` ## 4. Validate the documentation and OKF bundle @@ -68,7 +69,16 @@ Expected final line: ``` The command exits successfully after the mock core stops. No service remains -running. + +The default tracer is Rust. Prove that the TypeScript reference produces the +same bytes with: + +```sh +pnpm --filter @paperclipai/paperclip-runner check:phase0-parity +``` + +Expected result: `Rust and TypeScript Phase 0 tracer output matches the shared +golden fixture.` ## 6. Inspect and query the journal diff --git a/packages/paperclip-runner/knowledge/evidence/2026-08-07-phase-00-rust-correction-verification.md b/packages/paperclip-runner/knowledge/evidence/2026-08-07-phase-00-rust-correction-verification.md new file mode 100644 index 00000000000..60dc6a7855c --- /dev/null +++ b/packages/paperclip-runner/knowledge/evidence/2026-08-07-phase-00-rust-correction-verification.md @@ -0,0 +1,66 @@ +--- +type: Verification Evidence +title: Phase 0 Rust correction verification +description: Tool versions and targeted results for the corrected Rust workspace and cross-language tracer. +tags: [native-runner, phase-0, rust, verification, parity] +status: stable +generated: { by: codex/gpt-5, at: 2026-08-07T18:09:35Z } +phase: "0" +--- + +# Environment + +- OS: `Linux 6.17.0-1019-aws aarch64` +- Rust: `rustc 1.97.1 (8bab26f4f 2026-07-14)` +- Cargo: `cargo 1.97.1 (c980f4866 2026-06-30)` +- Node.js: `v22.22.2` +- pnpm: `9.15.4` + +The Rust toolchain was installed only under `PAPERCLIP_RUN_SCRATCH_DIR`; it did +not modify the host user's Rust installation or shell profile. + +# Commands and results + +1. Rust formatting check: + +```sh +cargo fmt --manifest-path packages/paperclip-runner/runner/Cargo.toml --all -- --check +``` + +Result: exit 0. + +2. Complete package verification: + +```sh +pnpm --filter @paperclipai/paperclip-runner verify +``` + +Result: exit 0. TypeScript build/tests, Rust build/tests, TypeScript and Cargo +negative boundary tests, documentation links, OKF v0.2 validation, language +parity, and the default Rust tracer all passed. + +3. Rust tests included in the package verification: + +```sh +cargo test --manifest-path runner/Cargo.toml --locked --workspace +``` + +Result: 3 Rust unit tests passed: shared-fixture validation, invalid sequence +rejection, and stable mock-core tracer output. + +4. Stable tracer output: + +```json +{"schemaVersion":"paperclip.runner.phase0.output.v1","runIdentity":{"runId":"run_phase0_0001","sessionId":"session_phase0_0001"},"result":{"status":"succeeded","summary":"Standalone Phase 0 fixture accepted."}} +``` + +# Acceptance mapping + +- Rust production package boundary exists: `runner/Cargo.toml` and + `runner/crates/runner-core/`. +- The package's default tracer executes Rust, not TypeScript. +- Rust and TypeScript read the same fixture and emit byte-identical output. +- Static boundary enforcement covers Rust source inclusion and Cargo path + dependencies, with a negative core-path fixture. +- The accepted plan, architecture, tutorials, journal, and README state Rust as + the production direction and TypeScript as the reference/control-plane side. diff --git a/packages/paperclip-runner/knowledge/evidence/index.md b/packages/paperclip-runner/knowledge/evidence/index.md index c990235d2a7..9778590d360 100644 --- a/packages/paperclip-runner/knowledge/evidence/index.md +++ b/packages/paperclip-runner/knowledge/evidence/index.md @@ -3,3 +3,4 @@ ## Phase evidence * [Phase 0 verification](2026-08-07-phase-00-verification.md) - Clean install, build, tests, boundary, documentation, OKF, and tracer results. +* [Phase 0 Rust correction verification](2026-08-07-phase-00-rust-correction-verification.md) - Rust build/tests, cross-language parity, and corrected boundary evidence. diff --git a/packages/paperclip-runner/knowledge/journal/2026-08-07-phase-00-rust-boundary-correction.md b/packages/paperclip-runner/knowledge/journal/2026-08-07-phase-00-rust-boundary-correction.md new file mode 100644 index 00000000000..0146d765aea --- /dev/null +++ b/packages/paperclip-runner/knowledge/journal/2026-08-07-phase-00-rust-boundary-correction.md @@ -0,0 +1,74 @@ +--- +type: Engineering Journal Entry +title: Phase 0 Rust boundary correction +description: Corrects the TypeScript-only Phase 0 checkpoint by establishing and verifying the production-direction Rust workspace. +tags: [native-runner, phase-0, rust, boundary, correction] +status: stable +generated: { by: codex/gpt-5, at: 2026-08-07T18:09:35Z } +entry_kind: correction +phase: "0" +--- + +# Context + +The Phase 0 issue named TypeScript contract sketches, and the first checkpoint +implemented the fixture, mock control plane, tracer, and boundary enforcement +only in TypeScript. That satisfied the literal deliverable list but failed to +establish the language boundary required by the normative spike specification: +the production runner direction is Rust, while TypeScript is the control-plane, +browser, and reference-client language. + +# Root cause + +Implementation followed the Phase 0 bullets in isolation and treated the spike's +Rust packaging section as later daemon work. The accepted plan's `runner/` +directory and the specification's production-language decision were not turned +into a Phase 0 acceptance assertion, so all checks could pass without Cargo or a +Rust source file. + +# Decisions + +1. Establish `runner/Cargo.toml` and `paperclip-runner-core` in Phase 0; do not + wait for the daemon/process-supervision phase to create the Rust boundary. +2. Make `trace:phase0` run the Rust implementation. Retain the TypeScript tracer + as an explicit reference oracle rather than the implied production runner. +3. Consume the same checked JSON fixture in both languages and compare complete + serialized output byte for byte. +4. Extend the standalone dependency check to Rust includes/path attributes and + Cargo path dependencies, with a negative Cargo fixture that points at core. +5. Keep `paperclip-runnerd`, transport, durable state, process supervision, and + real harness drivers deferred to their accepted later phases. + +# Evidence + +Exact tool versions, commands, and results are in the +[Rust correction verification](../evidence/2026-08-07-phase-00-rust-correction-verification.md). + +# Failures + +- The execution image had no Rust toolchain. A stable toolchain was installed + into the run-scoped Paperclip scratch directory so the crate could be compiled + and tested without changing the host installation. +- Sandboxed DNS could not reach the official Rust or crates.io endpoints. The + approved network path downloaded the official toolchain and lockfile-pinned + crates; subsequent package verification used the local cache. +- The first parity helper used synchronous Node child processes. This sandbox + returned `EPERM` for that spawn path, so the helper could mistake empty stdout + for a language mismatch. Both language tests now compare against one shared + golden output fixture, which is deterministic and does not depend on nested + process spawning. + +# Known gaps + +- `runner-core` is a deterministic Phase 0 skeleton, not `paperclip-runnerd`. +- The Phase 0 fixture is intentionally narrow JSON; Phase 1 still owns the full + PRP JSON Schema, generated/checked types, and conformance corpus. +- Phase 0 contains no async runtime, network transport, spool, signal + supervision, or harness process. + +# Follow-up questions + +- Which Rust schema-generation/checking path should Phase 1 use so JSON Schema, + rather than serde or TypeScript implementation detail, remains authoritative? +- What minimum supported Rust version should be pinned before distributable + runner binaries begin? diff --git a/packages/paperclip-runner/knowledge/journal/2026-08-07-phase-00.md b/packages/paperclip-runner/knowledge/journal/2026-08-07-phase-00.md index 2977ad80aac..163f8398ef2 100644 --- a/packages/paperclip-runner/knowledge/journal/2026-08-07-phase-00.md +++ b/packages/paperclip-runner/knowledge/journal/2026-08-07-phase-00.md @@ -16,6 +16,10 @@ importing or starting Paperclip core. The accepted implementation plan defines a mock control-plane tracer, contract sketches, static dependency enforcement, package-local tutorials, and an OKF v0.2 engineering journal. +> **Supersession note:** Decision 1's TypeScript-only package boundary was +> corrected after the human checkpoint. See the +> [Rust boundary correction](2026-08-07-phase-00-rust-boundary-correction.md). + # Decisions 1. Keep Phase 0 runtime dependency-free; use only Node.js and TypeScript tooling. diff --git a/packages/paperclip-runner/knowledge/journal/index.md b/packages/paperclip-runner/knowledge/journal/index.md index 079a876fb9f..9957ac589ee 100644 --- a/packages/paperclip-runner/knowledge/journal/index.md +++ b/packages/paperclip-runner/knowledge/journal/index.md @@ -3,3 +3,4 @@ ## Phase entries * [Phase 0 boundary, journal, and runnable skeleton](2026-08-07-phase-00.md) - Decisions, implementation evidence, failures, gaps, and next questions for the standalone skeleton. +* [Phase 0 Rust boundary correction](2026-08-07-phase-00-rust-boundary-correction.md) - Root cause and corrected Rust/TypeScript ownership after the human checkpoint. diff --git a/packages/paperclip-runner/knowledge/log.md b/packages/paperclip-runner/knowledge/log.md index 5b2f5821972..c1e9c95a66f 100644 --- a/packages/paperclip-runner/knowledge/log.md +++ b/packages/paperclip-runner/knowledge/log.md @@ -3,3 +3,4 @@ ## 2026-08-07 * **Creation**: Established the OKF v0.2 bundle, typed Phase 0 journal, verification evidence, template, and progressive-disclosure indexes. +* **Rust boundary correction**: Added the production-direction Cargo workspace, shared-fixture parity evidence, negative Cargo boundary coverage, and a superseding journal entry after the Phase 0 checkpoint exposed the TypeScript-only omission. diff --git a/packages/paperclip-runner/package.json b/packages/paperclip-runner/package.json index 940944e13ed..a7015406a00 100644 --- a/packages/paperclip-runner/package.json +++ b/packages/paperclip-runner/package.json @@ -16,13 +16,22 @@ "README.md" ], "scripts": { - "build": "tsc -p tsconfig.json", - "typecheck": "tsc -p tsconfig.json --noEmit", - "test": "vitest run && node --test scripts/check-forbidden-imports.test.mjs", + "build": "pnpm run build:typescript && pnpm run build:rust", + "build:typescript": "tsc -p tsconfig.json", + "build:rust": "cargo build --manifest-path runner/Cargo.toml --locked --workspace", + "typecheck": "pnpm run typecheck:typescript && pnpm run typecheck:rust", + "typecheck:typescript": "tsc -p tsconfig.json --noEmit", + "typecheck:rust": "cargo check --manifest-path runner/Cargo.toml --locked --workspace", + "test": "pnpm run test:typescript && pnpm run test:rust && node --test scripts/check-forbidden-imports.test.mjs", + "test:typescript": "vitest run", + "test:rust": "cargo test --manifest-path runner/Cargo.toml --locked --workspace", "check:forbidden-imports": "node scripts/check-forbidden-imports.mjs", + "check:phase0-parity": "vitest run src/tracer/phase0-runner.test.ts && cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core runs_the_mock_core_path_with_stable_output && node -e \"process.stdout.write('Rust and TypeScript Phase 0 tracer output matches the shared golden fixture.\\n')\"", "docs:validate": "node scripts/validate-doc-links.mjs && node scripts/validate-okf.mjs", - "trace:phase0": "pnpm run build && node dist/tracer/phase0.js", - "verify": "pnpm run build && pnpm run test && pnpm run check:forbidden-imports && pnpm run docs:validate && pnpm run trace:phase0" + "trace:phase0": "pnpm run trace:phase0:rust", + "trace:phase0:rust": "pnpm run build:rust && cargo run --quiet --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin phase0-tracer", + "trace:phase0:typescript": "pnpm run build:typescript && node dist/tracer/phase0.js", + "verify": "pnpm run build && pnpm run test && pnpm run check:forbidden-imports && pnpm run docs:validate && pnpm run check:phase0-parity && pnpm run trace:phase0" }, "devDependencies": { "@types/node": "^22.20.1", diff --git a/packages/paperclip-runner/protocol/fixtures/phase-00-expected-output.json b/packages/paperclip-runner/protocol/fixtures/phase-00-expected-output.json new file mode 100644 index 00000000000..da8cf53b137 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/phase-00-expected-output.json @@ -0,0 +1 @@ +{"schemaVersion":"paperclip.runner.phase0.output.v1","runIdentity":{"runId":"run_phase0_0001","sessionId":"session_phase0_0001"},"result":{"status":"succeeded","summary":"Standalone Phase 0 fixture accepted."}} diff --git a/packages/paperclip-runner/runner/Cargo.lock b/packages/paperclip-runner/runner/Cargo.lock new file mode 100644 index 00000000000..d4a5af60b48 --- /dev/null +++ b/packages/paperclip-runner/runner/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "paperclip-runner-core" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/packages/paperclip-runner/runner/Cargo.toml b/packages/paperclip-runner/runner/Cargo.toml new file mode 100644 index 00000000000..47b88d455c1 --- /dev/null +++ b/packages/paperclip-runner/runner/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +members = ["crates/runner-core"] +resolver = "2" + +[workspace.package] +version = "0.0.0" +edition = "2021" +license = "MIT" +publish = false + +[workspace.dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/packages/paperclip-runner/runner/crates/runner-core/Cargo.toml b/packages/paperclip-runner/runner/crates/runner-core/Cargo.toml new file mode 100644 index 00000000000..6f22d575657 --- /dev/null +++ b/packages/paperclip-runner/runner/crates/runner-core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "paperclip-runner-core" +version.workspace = true +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true + +[[bin]] +name = "phase0-tracer" +path = "src/bin/phase0-tracer.rs" diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/bin/phase0-tracer.rs b/packages/paperclip-runner/runner/crates/runner-core/src/bin/phase0-tracer.rs new file mode 100644 index 00000000000..77fcc60ee71 --- /dev/null +++ b/packages/paperclip-runner/runner/crates/runner-core/src/bin/phase0-tracer.rs @@ -0,0 +1,11 @@ +use paperclip_runner_core::{run_phase0_tracer, PHASE0_FIXTURE}; + +fn main() { + match run_phase0_tracer(PHASE0_FIXTURE) { + Ok(output) => println!("{output}"), + Err(error) => { + eprintln!("Phase 0 tracer failed: {error}"); + std::process::exit(1); + } + } +} diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/lib.rs b/packages/paperclip-runner/runner/crates/runner-core/src/lib.rs new file mode 100644 index 00000000000..b0730569ac7 --- /dev/null +++ b/packages/paperclip-runner/runner/crates/runner-core/src/lib.rs @@ -0,0 +1,332 @@ +#![forbid(unsafe_code)] + +use std::error::Error; +use std::fmt::{self, Display, Formatter}; + +use serde::{Deserialize, Serialize}; + +pub const PHASE0_FIXTURE_SCHEMA: &str = "paperclip.runner.phase0.fixture.v1"; +pub const PHASE0_OUTPUT_SCHEMA: &str = "paperclip.runner.phase0.output.v1"; +pub const PHASE0_FIXTURE: &str = + include_str!("../../../../protocol/fixtures/phase-00-minimal-run.json"); +pub const PHASE0_EXPECTED_OUTPUT: &str = + include_str!("../../../../protocol/fixtures/phase-00-expected-output.json"); + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct NativeRunIdentity { + pub run_id: String, + pub session_id: String, + pub company_id: String, + pub issue_id: String, + pub agent_id: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct NativeRunEvent { + pub event_id: String, + pub run_id: String, + pub sequence: u64, + #[serde(rename = "type")] + pub event_type: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct NativeRunResult { + pub run_id: String, + pub status: String, + pub summary: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Phase0Fixture { + pub schema_version: String, + pub run: NativeRunIdentity, + pub events: Vec, + pub result: NativeRunResult, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Phase0Error(String); + +impl Phase0Error { + fn invalid(message: impl Into) -> Self { + Self(message.into()) + } +} + +impl Display for Phase0Error { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.0) + } +} + +impl Error for Phase0Error {} + +fn validate_identifier(value: &str, path: &str) -> Result<(), Phase0Error> { + let mut characters = value.chars(); + let starts_lowercase = characters + .next() + .is_some_and(|character| character.is_ascii_lowercase()); + let remainder_is_valid = characters.all(|character| { + character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_' + }); + + if starts_lowercase && remainder_is_valid { + Ok(()) + } else { + Err(Phase0Error::invalid(format!( + "{path} must be a stable lowercase identifier" + ))) + } +} + +pub fn validate_phase0_fixture(input: &str) -> Result { + let fixture: Phase0Fixture = serde_json::from_str(input) + .map_err(|error| Phase0Error::invalid(format!("fixture must be valid JSON: {error}")))?; + + if fixture.schema_version != PHASE0_FIXTURE_SCHEMA { + return Err(Phase0Error::invalid(format!( + "schemaVersion must be {PHASE0_FIXTURE_SCHEMA}" + ))); + } + + for (path, value) in [ + ("run.runId", fixture.run.run_id.as_str()), + ("run.sessionId", fixture.run.session_id.as_str()), + ("run.companyId", fixture.run.company_id.as_str()), + ("run.issueId", fixture.run.issue_id.as_str()), + ("run.agentId", fixture.run.agent_id.as_str()), + ] { + validate_identifier(value, path)?; + } + + if fixture.events.len() < 2 { + return Err(Phase0Error::invalid( + "events must contain at least run.started and run.completed", + )); + } + + for (index, event) in fixture.events.iter().enumerate() { + validate_identifier(&event.event_id, &format!("events[{index}].eventId"))?; + if event.run_id != fixture.run.run_id { + return Err(Phase0Error::invalid(format!( + "events[{index}].runId must match run.runId" + ))); + } + let expected_sequence = index as u64 + 1; + if event.sequence != expected_sequence { + return Err(Phase0Error::invalid(format!( + "events[{index}].sequence must be {expected_sequence}" + ))); + } + if !matches!(event.event_type.as_str(), "run.started" | "run.completed") { + return Err(Phase0Error::invalid(format!( + "events[{index}].type is unsupported" + ))); + } + } + + if fixture + .events + .first() + .map(|event| event.event_type.as_str()) + != Some("run.started") + { + return Err(Phase0Error::invalid("the first event must be run.started")); + } + if fixture.events.last().map(|event| event.event_type.as_str()) != Some("run.completed") { + return Err(Phase0Error::invalid( + "the final event must be run.completed", + )); + } + if fixture.result.run_id != fixture.run.run_id { + return Err(Phase0Error::invalid("result.runId must match run.runId")); + } + if fixture.result.status != "succeeded" { + return Err(Phase0Error::invalid( + "the Phase 0 fixture result must be succeeded", + )); + } + if fixture.result.summary.trim().is_empty() { + return Err(Phase0Error::invalid( + "result.summary must be a non-empty string", + )); + } + + Ok(fixture) +} + +#[derive(Default)] +pub struct MockControlPlane { + running: bool, + opened_run_id: Option, + events: Vec, + result: Option, +} + +impl MockControlPlane { + pub fn start(&mut self) -> Result<(), Phase0Error> { + if self.running { + return Err(Phase0Error::invalid( + "mock control plane is already running", + )); + } + self.running = true; + Ok(()) + } + + pub fn stop(&mut self) { + self.running = false; + } + + pub fn open_run(&mut self, identity: &NativeRunIdentity) -> Result<(), Phase0Error> { + self.require_running()?; + if self.opened_run_id.is_some() { + return Err(Phase0Error::invalid( + "mock control plane accepts one Phase 0 run", + )); + } + self.opened_run_id = Some(identity.run_id.clone()); + Ok(()) + } + + pub fn append_event(&mut self, event: &NativeRunEvent) -> Result { + let run_id = self.require_run_id()?; + if event.run_id != run_id { + return Err(Phase0Error::invalid( + "event runId does not match the opened run", + )); + } + let expected_sequence = self.events.len() as u64 + 1; + if event.sequence != expected_sequence { + return Err(Phase0Error::invalid(format!( + "event sequence must be {expected_sequence}" + ))); + } + self.events.push(event.clone()); + Ok(event.sequence) + } + + pub fn complete_run(&mut self, result: &NativeRunResult) -> Result<(), Phase0Error> { + let run_id = self.require_run_id()?; + if result.run_id != run_id { + return Err(Phase0Error::invalid( + "result runId does not match the opened run", + )); + } + if self.events.last().map(|event| event.event_type.as_str()) != Some("run.completed") { + return Err(Phase0Error::invalid( + "run.completed must be ingested before the result", + )); + } + if self.result.is_some() { + return Err(Phase0Error::invalid( + "mock control plane accepts one terminal result", + )); + } + self.result = Some(result.clone()); + Ok(()) + } + + fn require_running(&self) -> Result<(), Phase0Error> { + if self.running { + Ok(()) + } else { + Err(Phase0Error::invalid( + "mock control plane must be started first", + )) + } + } + + fn require_run_id(&self) -> Result<&str, Phase0Error> { + self.require_running()?; + self.opened_run_id + .as_deref() + .ok_or_else(|| Phase0Error::invalid("a run must be opened first")) + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Phase0TracerOutput<'a> { + schema_version: &'static str, + run_identity: Phase0RunIdentityOutput<'a>, + result: Phase0ResultOutput<'a>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Phase0RunIdentityOutput<'a> { + run_id: &'a str, + session_id: &'a str, +} + +#[derive(Serialize)] +struct Phase0ResultOutput<'a> { + status: &'a str, + summary: &'a str, +} + +pub fn run_phase0_tracer(input: &str) -> Result { + let fixture = validate_phase0_fixture(input)?; + let mut mock_core = MockControlPlane::default(); + mock_core.start()?; + + let replay_result = (|| { + mock_core.open_run(&fixture.run)?; + for event in &fixture.events { + mock_core.append_event(event)?; + } + mock_core.complete_run(&fixture.result)?; + serde_json::to_string(&Phase0TracerOutput { + schema_version: PHASE0_OUTPUT_SCHEMA, + run_identity: Phase0RunIdentityOutput { + run_id: &fixture.run.run_id, + session_id: &fixture.run.session_id, + }, + result: Phase0ResultOutput { + status: &fixture.result.status, + summary: &fixture.result.summary, + }, + }) + .map_err(|error| Phase0Error::invalid(format!("output serialization failed: {error}"))) + })(); + + mock_core.stop(); + replay_result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validates_the_shared_fixture() { + let fixture = + validate_phase0_fixture(PHASE0_FIXTURE).expect("shared fixture should validate"); + + assert_eq!(fixture.events.len(), 2); + assert_eq!(fixture.events[1].sequence, 2); + assert_eq!(fixture.result.status, "succeeded"); + } + + #[test] + fn rejects_a_sequence_gap() { + let invalid = PHASE0_FIXTURE.replacen(r#""sequence": 2"#, r#""sequence": 3"#, 1); + + let error = validate_phase0_fixture(&invalid).expect_err("sequence gap must fail"); + assert_eq!(error.to_string(), "events[1].sequence must be 2"); + } + + #[test] + fn runs_the_mock_core_path_with_stable_output() { + assert_eq!( + run_phase0_tracer(PHASE0_FIXTURE).expect("tracer should succeed"), + PHASE0_EXPECTED_OUTPUT.trim_end(), + ); + } +} diff --git a/packages/paperclip-runner/scripts/check-forbidden-imports.test.mjs b/packages/paperclip-runner/scripts/check-forbidden-imports.test.mjs index 607bd25ebc3..3a6035f7f68 100644 --- a/packages/paperclip-runner/scripts/check-forbidden-imports.test.mjs +++ b/packages/paperclip-runner/scripts/check-forbidden-imports.test.mjs @@ -13,6 +13,7 @@ test("the package passes its standalone boundary", async () => { test("a negative fixture proves that a core import is rejected", async () => { const violations = await checkForbiddenImports({ scanRoots: ["test-fixtures/forbidden-import"], + cargoRoots: [], checkManifest: false, }); @@ -21,3 +22,16 @@ test("a negative fixture proves that a core import is rejected", async () => { assert.match(violations[0].reason, /may not escape/); assert.ok(violations[0].file.startsWith(defaultPackageRoot)); }); + +test("a negative Cargo fixture proves that a core path dependency is rejected", async () => { + const violations = await checkForbiddenImports({ + scanRoots: [], + cargoRoots: ["test-fixtures/forbidden-cargo-path"], + checkManifest: false, + }); + + assert.equal(violations.length, 1); + assert.equal(violations[0].specifier, "../../../../server"); + assert.match(violations[0].reason, /Cargo path dependencies may not escape/); + assert.ok(violations[0].file.startsWith(defaultPackageRoot)); +}); diff --git a/packages/paperclip-runner/scripts/lib/forbidden-imports.mjs b/packages/paperclip-runner/scripts/lib/forbidden-imports.mjs index 80b70c3f332..422b615d1d6 100644 --- a/packages/paperclip-runner/scripts/lib/forbidden-imports.mjs +++ b/packages/paperclip-runner/scripts/lib/forbidden-imports.mjs @@ -2,11 +2,13 @@ import { readFile, readdir } from "node:fs/promises"; import { dirname, isAbsolute, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const SOURCE_EXTENSIONS = new Set([".cjs", ".js", ".jsx", ".mjs", ".ts", ".tsx"]); +const SOURCE_EXTENSIONS = new Set([".cjs", ".js", ".jsx", ".mjs", ".rs", ".ts", ".tsx"]); const IMPORT_PATTERNS = [ /\b(?:import|export)\s+(?:type\s+)?(?:[^"'`;]{0,500}?\s+from\s+)?["']([^"']+)["']/g, /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g, /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g, + /\b(?:include|include_str|include_bytes)!\s*\(\s*["']([^"']+)["']\s*\)/g, + /#\[path\s*=\s*["']([^"']+)["']\]/g, ]; const libraryDirectory = dirname(fileURLToPath(import.meta.url)); @@ -94,9 +96,53 @@ async function manifestViolations(packageRoot) { })); } +async function cargoManifestViolations(packageRoot, cargoRoots) { + const violations = []; + const manifests = []; + + async function collectCargoManifests(target) { + const entries = await readdir(target, { withFileTypes: true }); + for (const entry of entries) { + if (["target", "node_modules", ".git"].includes(entry.name)) { + continue; + } + const path = resolve(target, entry.name); + if (entry.isDirectory()) { + await collectCargoManifests(path); + } else if (entry.isFile() && entry.name === "Cargo.toml") { + manifests.push(path); + } + } + } + + for (const root of cargoRoots) { + await collectCargoManifests(resolve(packageRoot, root)); + } + + for (const manifest of manifests.sort()) { + const source = await readFile(manifest, "utf8"); + const pathPattern = /\bpath\s*=\s*["']([^"']+)["']/g; + for (let match = pathPattern.exec(source); match !== null; match = pathPattern.exec(source)) { + const dependencyPath = resolve(dirname(manifest), match[1]); + if (isInside(packageRoot, dependencyPath)) { + continue; + } + violations.push({ + file: manifest, + line: source.slice(0, match.index).split("\n").length, + specifier: match[1], + reason: "Cargo path dependencies may not escape packages/paperclip-runner", + }); + } + } + + return violations; +} + export async function checkForbiddenImports({ packageRoot = defaultPackageRoot, - scanRoots = ["src", "scripts"], + scanRoots = ["src", "scripts", "runner"], + cargoRoots = ["runner"], checkManifest = true, } = {}) { const violations = []; @@ -124,6 +170,7 @@ export async function checkForbiddenImports({ if (checkManifest) { violations.push(...(await manifestViolations(packageRoot))); } + violations.push(...(await cargoManifestViolations(packageRoot, cargoRoots))); return violations; } diff --git a/packages/paperclip-runner/spec/paperclip-native-runner-implementation-plan.md b/packages/paperclip-runner/spec/paperclip-native-runner-implementation-plan.md index f76151b07be..3b1afb80a0a 100644 --- a/packages/paperclip-runner/spec/paperclip-native-runner-implementation-plan.md +++ b/packages/paperclip-runner/spec/paperclip-native-runner-implementation-plan.md @@ -14,14 +14,15 @@ Only Phase 0 and Phase 1 implementation issues may be created initially. After b 2. **Standalone first:** new runtime behavior, documentation, tutorials, examples, journals, fixtures, and devtools live under `packages/paperclip-runner/`. The default development and test path uses a mock Paperclip core adapter. 3. **Core is a port, not a dependency:** package code depends on a small `ControlPlanePort`/`NativeSessionBackend` contract. Mock implementations own tests and tutorials. Paperclip-specific integration implements that port later. 4. **No hidden control-plane coupling:** standalone tests must not import `server/`, `ui/`, or production database modules. Contract tests may consume shared generated schemas or fixtures through explicit package exports. -5. **Tracer bullet in every phase:** each phase ends with a useful executable path, even if it is intentionally narrow. -6. **Evidence in every phase:** record the exact commands run, versions used, results, known gaps, and links to artifacts/screenshots. -7. **Documentation in every phase:** add reference documentation plus a hand-run tutorial. Add each tutorial to a Native Runner tutorial index and to a cumulative end-to-end tutorial. -8. **OKF engineering journal:** maintain a Google Open Knowledge Format journal using current OKF v0.2 conventions: Markdown files, YAML frontmatter, typed entries, timestamps, links, and index pages. Add a journal usage guide before implementation begins. -9. **Human checkpoint in every phase:** the board/user can run the tutorial and accept or request changes before the next dependent phase starts. -10. **Commit by phase:** create small, reviewable commits during each phase. Each phase ends with a named checkpoint commit after QA and human acceptance. -11. **No invented UI primitives:** evaluate and selectively adapt shadcn/ui and Vercel AI Elements components. Keep Paperclip design tokens and accessibility requirements authoritative. -12. **Production integration is additive:** legacy adapters and existing run finalization remain unchanged unless the separate integration phase explicitly proves a minimal feature-flagged bridge. +5. **Rust production boundary:** the production runner direction is Rust. TypeScript may provide control-plane/client contracts and a test oracle, but each runner phase must establish or extend the Rust workspace and prove shared-fixture parity rather than deferring Rust implicitly. +6. **Tracer bullet in every phase:** each phase ends with a useful executable path, even if it is intentionally narrow. +7. **Evidence in every phase:** record the exact commands run, versions used, results, known gaps, and links to artifacts/screenshots. +8. **Documentation in every phase:** add reference documentation plus a hand-run tutorial. Add each tutorial to a Native Runner tutorial index and to a cumulative end-to-end tutorial. +9. **OKF engineering journal:** maintain a Google Open Knowledge Format journal using current OKF v0.2 conventions: Markdown files, YAML frontmatter, typed entries, timestamps, links, and index pages. Add a journal usage guide before implementation begins. +10. **Human checkpoint in every phase:** the board/user can run the tutorial and accept or request changes before the next dependent phase starts. +11. **Commit by phase:** create small, reviewable commits during each phase. Each phase ends with a named checkpoint commit after QA and human acceptance. +12. **No invented UI primitives:** evaluate and selectively adapt shadcn/ui and Vercel AI Elements components. Keep Paperclip design tokens and accessibility requirements authoritative. +13. **Production integration is additive:** legacy adapters and existing run finalization remain unchanged unless the separate integration phase explicitly proves a minimal feature-flagged bridge. ## Proposed package and documentation boundaries @@ -29,7 +30,7 @@ Only Phase 0 and Phase 1 implementation issues may be created initially. After b packages/paperclip-runner/ protocol/ language-neutral schemas and fixtures sdk/typescript/ TypeScript protocol/reducer/client SDK - runner/ deterministic runner daemon and local state + runner/ Rust workspace; runner-core first, daemon/local state later drivers/api/ harness driver contract drivers/fake/ scripted deterministic harness drivers/codex/ reference Codex app-server driver @@ -91,15 +92,17 @@ Every phase child issue uses the same completion checklist: **Deliverables:** - package workspace skeleton and build/test commands; +- Rust Cargo workspace with the initial `runner-core` crate and default Phase 0 tracer; - architecture boundary document with forbidden imports and dependency direction; - initial `ControlPlanePort`, `HarnessDriver`, and `NativeSessionBackend` interface sketches; - mock core adapter shell; - tutorial index and cumulative tutorial shell; - OKF v0.2 bundle, journal entry template, index pages, and “how to use the journal” instructions; -- CI/static boundary check that rejects imports from production core packages; +- CI/static boundary check that rejects TypeScript imports and Cargo path dependencies from production core packages; +- shared-fixture parity check between the Rust tracer and TypeScript reference; - a dated compatibility note for shadcn/ui and AI Elements. -**Tests/evidence:** package build, fixture validation smoke, forbidden-import test, docs link check, OKF frontmatter/index validation. +**Tests/evidence:** Rust and TypeScript package builds, shared-fixture validation smoke, byte-identical tracer output, forbidden-import/path-dependency tests, docs link check, and OKF frontmatter/index validation. **Owners/review:** CodexCoder implementation; DevRel documentation review; SecurityEngineer boundary/secret review; CTO architecture approval; QA tutorial execution. diff --git a/packages/paperclip-runner/src/tracer/phase0-runner.test.ts b/packages/paperclip-runner/src/tracer/phase0-runner.test.ts index 6e74a908619..ede375437d6 100644 --- a/packages/paperclip-runner/src/tracer/phase0-runner.test.ts +++ b/packages/paperclip-runner/src/tracer/phase0-runner.test.ts @@ -1,16 +1,21 @@ +import { readFile } from "node:fs/promises"; + import { describe, expect, it } from "vitest"; import { MockControlPlaneAdapter } from "../mock-core/mock-control-plane-adapter.js"; import { runPhase0Tracer } from "./phase0-runner.js"; -const EXPECTED_OUTPUT = - '{"schemaVersion":"paperclip.runner.phase0.output.v1","runIdentity":{"runId":"run_phase0_0001","sessionId":"session_phase0_0001"},"result":{"status":"succeeded","summary":"Standalone Phase 0 fixture accepted."}}'; +const expectedOutputUrl = new URL( + "../../protocol/fixtures/phase-00-expected-output.json", + import.meta.url, +); describe("Phase 0 tracer", () => { it("prints a stable identity and result and stops the mock core", async () => { const mockCore = new MockControlPlaneAdapter(); + const expectedOutput = (await readFile(expectedOutputUrl, "utf8")).trimEnd(); - await expect(runPhase0Tracer(mockCore)).resolves.toBe(EXPECTED_OUTPUT); + await expect(runPhase0Tracer(mockCore)).resolves.toBe(expectedOutput); expect(mockCore.snapshot().lifecycle).toBe("stopped"); }); }); diff --git a/packages/paperclip-runner/test-fixtures/forbidden-cargo-path/Cargo.toml b/packages/paperclip-runner/test-fixtures/forbidden-cargo-path/Cargo.toml new file mode 100644 index 00000000000..de783b3c178 --- /dev/null +++ b/packages/paperclip-runner/test-fixtures/forbidden-cargo-path/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "forbidden-paperclip-core-import" +version = "0.0.0" +edition = "2021" + +[dependencies] +paperclip-server = { path = "../../../../server" }