Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to ocserve. The wire contract targets the frozen opencode
1.19.5 release and the v2 surface the 2.2.5 client; anything that differs is a
named `D-*` row in the ledger at `PLAN.md` section 18.

## Unreleased

### Fixed
- Permission requests now serialize in the correct v1 or v2 shape at each API
boundary, even when both API families share the same pending request. V2
permission creation also rejects fields and values outside the frozen
request schema before storing them.

## 0.7.1 - 2026-11-10

### Added
Expand Down
2 changes: 2 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,8 @@ current anywhere.
| `D-WIN-WAL-NETWORK` | A data directory on a network filesystem is refused at boot rather than degraded. | accepted |
| `D-MACOS-TRIM` | `trim_heap()` is a no-op off glibc unless measurement justifies otherwise. | open |
| `D-TOOLS-TASK` | The `task` subagent tool was advertised by the frozen provider but not implemented. | closed (the tool runs on the shared executor with a depth guard) |
| `D-PERM-SOURCE-KEY` | The permission `source` object uses different keys per contract line: the v1 (1.19.5) SDK type `PermissionV2Source` is `{type,messageID,callID}` while v2 (2.2.5) `Permission.Source` is `{type,messageID,id}`. ocserve emits `id` on the v2 wire and `callID` on the v1 wire; the gate stores the call id under `callID` and each projector renames it. A body cannot satisfy both schemas because both are `additionalProperties:false`. | open |
| `D-PERM-EVENT-NAME` | Upstream 1.19.5 names the v2 permission event `permission.v2.asked`; 2.2.5 names it `permission.asked`. ocserve keeps the v1 name `permission.asked` on every stream and projects the v2 *payload* on `/api/event`, so a 1.19.5-era consumer that dispatches on `permission.v2.asked` will not see it. No shipped 1.19.5 client dispatches on that event: the TUI refreshes permissions over REST, and the web app's adapter maps `permission.v2.asked` only when the server sends it. The 1.19.5/2.2.5 web app also probes `/global/health` first and treats any `{healthy:true}` answer as a v1 server (`server-protocol.ts`), so against ocserve it subscribes to `/global/event` and consumes the v1 payload — the v2 permission frame is reachable only from a client that calls `/api/event` directly. | open |

Historical rows kept for the tokens' own history, all closed: `D-GENERATE-EPHEMERAL`,
`D-REVERT-NOSNAP`, `D-VCS-NOAPPLY`, `D-HOOK-*` items are in §16.9 and
Expand Down
1 change: 1 addition & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ event types), the two antagonism ledgers (every finding = at least one test).
| a built-in command is advertised but not invocable (2026-11-05, reported by a real client: `/goal <objective>` failed with "Command failed"). The built-in entry is advertised with `template: ""` because the v1 `Command` schema requires the field, and the dispatcher tested `template.is_none()` — true only in the test fixture's EMPTY command list, never in production, so the call fell through to `expand_command("")`, sent the model an EMPTY prompt and 500'd; the v2 path had a sibling bug (`command disappeared`) | the predicate is "missing OR empty" and the v2 dispatch matches the built-in by name before its template lookup; guard rule 69 fails on `template.is_none()` as a dispatch test | `tests/goal_e2e.rs` fixtures are built with the PRODUCTION payload shape (`template: ""` advertised) and the regression test asserts the response is `SessionV1.WithParts`; planted negative control: restore `is_none()` → 5 red; live proof on the release binary + stub provider: HTTP 200 in 24 ms, `info.role=assistant`, 3 parts, goal armed |
| model-driven reviewer text treated as instructions (2026-11-05 security review H1: the continuation template interpolated `${feedback}` RAW — only the objective was escaped — so a hostile repository could steer the judge into emitting `{"verdict":"continue","reason":"<instructions>"}`, which was persisted as a user-role message and replayed with tools enabled, surviving compaction) | `continuation()` escapes and wraps the verdict in `<untrusted_review>`; reviewer prose is capped (`MAX_FEEDBACK`); `judge_prompt` escapes shell commands too | `reviewer_feedback_is_escaped_and_wrapped`, `reviewer_feedback_is_bounded`, `judge_escapes_shell_commands` (a `</untrusted_review>` in the reason must not close the block) |
| an autonomous loop that keeps spending after the user stopped it (2026-11-05 reviews M1/M2/M3: `/abort`, v2 `interrupt` and v2 session delete aborted only `prompt_tasks`; the goal driver is a bare task, so the turn finished, the reviewer said continue, and another turn ran; `/abort` also left the row ACTIVE so the next idle hook restarted the loop) | v1 abort, v2 interrupt and v2 delete all `cancel_driver` + pause the goal with "Stopped by the user" (upstream pauses on a user interrupt); drivers re-check status AND objective before every turn; a `Drop` guard removes the registry entry on every exit path; a failed context/lock start PAUSES instead of leaving ACTIVE-with-no-driver | `abort_pauses_the_goal_and_stops_the_loop` (deterministic: the provider holds the kickoff open on a gate so the abort lands mid-turn; asserts status paused, rounds frozen, zero further reviewer calls) with a proven negative control (remove the pause → red) |
| bundled v2 permission UI crashed on `request.resources.length` because pending asks created by v1 tool calls and v2 permission routes crossed the shared gate without boundary projection; malformed v2 fields could also create requests outside the frozen schema | v1 and v2 permission projectors map gate values into each frozen request shape, filter string arrays, and only project a tool source; v2 create validates required string arrays plus optional metadata/source and rejects unknown keys (including `message`) before persistence; the shared always-grant/cascade logic accepts `always/patterns` and `save/resources`; `/api/event` and v2 reads use v2, while `/permission` uses v1 | `v2_surface.rs::v2_permission_requests_project_v1_shape_for_reads`, `permission_create_evaluates_ruleset` (checks both read shapes, then replies always and verifies grant/cascade), `permission_create_rejects_malformed_request_fields_before_persisting`, `event_envelope_unit::v2_permission_event_projects_the_permission_request_shape`, `event::tests::permission_request_projects_v2_gate_values_into_the_v1_schema`, `event::tests::v2_permission_projection_drops_malformed_fields_and_source_types`; negative controls bypass projectors, pass malformed entries/source types, accept an unknown create key, or read only v1 gate fields |
| goal state writes swallowed (2026-11-05 SRE review: `set_goal` results discarded with `let _ =` in the boot scan, the round increment and `settle`; a persistently failing round write silently disables the 50-round cap, and a failed pause leaves ACTIVE-with-no-driver) | every goal write propagates or logs+counts (`ocserve_goal_total{event="persist_failed"}`); boot-scan failure skips the row loudly | `goal_state` suite; the store's single-writer discipline unchanged (all writes go through `Writer`) |
| an autonomous loop the operator cannot see or stop (2026-11-05 SRE review: no reviewer budget, no cap on concurrent drivers, reviewer spend invisible, failures not surfaced to clients) | `OCSERVE_GOAL_REVIEW_SECS` (default 300 s) bounds each reviewer call so a chatty provider cannot hold the wake gate; `MAX_CONCURRENT_DRIVERS` (=4) bounds loops process-wide and PAUSES the ones that cannot start; reviewer tokens land in `ocserve_transient_tokens_total{owner="session"}` via `RunOpts::attribution_session`; a failed goal turn emits the same durable `session.error` shape `run_prompt` does | `review_budget` + env contract row (regenerated `docs/ENVIRONMENT.md`); the abort e2e asserts frozen rounds; `ocserve doctor` prints the switch and active goals |
## 3. Test levels (ISTQB) mapped to ocserve
Expand Down
2 changes: 2 additions & 0 deletions TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ no-inflation correct), PASS cost (−58.9% weighted) + score (median 1.0 both)
| K-FREEZE-1195 | the v1 freeze must be adopted through the pipeline, not by hand, and every artifact that embeds the tag must move with it | Moved 1.18.31 -> 1.19.5 with `bump.sh v1`. Contract: 188 operations over the same 162 paths (byte-diff: 2 schema descriptions, 1 additive `reasoning_options` property, `x-codeSamples` added to every operation). Prompt/tool text for everything the server sends is byte-identical (`tool/shell/shell.txt` unchanged, `bashCommandSection` unchanged); `plan-enter.txt` was DELETED upstream and is removed here. ARITY rules identical. Two live capture timings were also fixed: `acquire-freeze` printed success while `cp` failed on the root-owned vendored path (step 2 then booted the OLD binary), and `extract-prompts` had no remote fallback. | `capdiff` GREEN with the provider-header oracle enabled: it found the two real deltas (composite UA runtime segment `bun/1.3.14 -> 1.4.4`; `x-opencode-session-id` now sent to EVERY provider, not just `opencode*`), both fixed; replay 26/0; pair 18 compared / 0 divergent, 52 schema checks / 0 violations; `render_check` (prompt parity) green; `global_health` golden now `1.19.5`; test `composite_ua_matches_the_captured_artifact` pins the UA to a captured file, so a hand-written literal cannot drift again | green |
| K-MUTANTS | mutation baseline + gate (Phase 2) | `snapshot::tests::non_git_directory_is_inactive` made hermetic (fixture plants a bogus `.git` boundary — the test falsely passed only when TMPDIR sat outside any repo; the mutants harness sets TMPDIR inside the repo, aborting the baseline). **Full-scope baseline (i9, 2330 mutants, 34 min): 939 caught / 1221 missed / 24 timeout / 146 unviable = 43.5 % score** — `bench/mutants/BASELINE.md` + `baseline-outcomes.json`; zero env-gated skips verified (1 intentional ignore). `scripts/check-mutants.py` gates missed count against ceiling 1221 (ratchet-down-only; selftest red/green proven); `scripts/merge-mutants.py` combines `--shard` files with a duplicate guard (selftest red/green proven). Method corrected on evidence: `additional_cargo_test_args=["--lib"]` removed (drops store integration binaries → 933 false survivors, and lands `--lib` on the test binary). Acceleration: `.cargo/mutants.toml` profile=mutants/debug=none + 45s timeout floor; runs on the i9 (`-j8`, mold, sccache, tmpfs) | green |

| K-V2-PERM-SHAPE | pending permission requests use each frozen grammar on reads and events, regardless of which API family created them, while shared always-grant behavior accepts both grammars | The permission gate is shared: v1 tool calls store `permission`/`patterns`/`always`/`tool`, while v2 creates store `action`/`resources`/`save`/`source`. V2 create validates string-array members plus metadata/source shapes, and rejects unknown fields such as `message` because the frozen request schema has `additionalProperties: false`. Bidirectional projectors whitelist fields, filter string arrays, and emit `source` only for the frozen `tool` variant. The gate's always-reply and cascade logic reads either vocabulary so v2 asks grant and resolve matching pending requests without leaking fields across wire shapes. | `v2_surface.rs::v2_permission_requests_project_v1_shape_for_reads` covers global/session/detail and isolation; `v2_surface.rs::v2_created_ask_is_readable_in_both_grammars` drives the REAL POST /api/session/{id}/permission create path and asserts the same stored ask reads back with `source.id` on the v2 route and `tool.callID` on the v1 route; `permission_create_evaluates_ruleset` creates v2 asks, checks both list shapes, replies `always`, and verifies the session grant plus matching-request cascade; `permission_create_rejects_malformed_request_fields_before_persisting` covers malformed resources/save entries, optional metadata/source fields, and rejects the non-schema `message` field; `event_envelope_unit::v2_permission_event_projects_the_permission_request_shape` checks v2 event projection and unchanged v1 event; `event::tests::permission_request_projects_v2_gate_values_into_the_v1_schema` pins reverse projection; `event::tests::v2_permission_projection_drops_malformed_fields_and_source_types` covers malformed shared values. Negative controls: reading only v1 `always`/`patterns` makes the v2 grant and cascade assertions red; bypass either projector, allow bad nested payload fields, pass a non-tool source type, or accept an unknown permission-create key breaks the shape assertions. | green |

| K-CI-PIN | CI must fail for repository changes, not for a Rust release rolling onto the stable channel | On 2026-11-13 every workflow on `main` and on all fork PRs failed within two minutes with zero tests run. rustc 1.100.0 had landed on the runner and introduced cargo manifest lints (`unused_dependencies`, `unused_workspace_dependencies`, `redundant_homepage`); `CARGO_BUILD_WARNINGS=deny` turned them fatal. Reproduced locally under 1.100.0 with the same env (exit 101) and green under 1.99.0, proving the trigger was the compiler, not the code. Fixed by pinning `rust-toolchain.toml` to 1.100.0 (a floating channel makes `main` hostage to a release) and removing the declarations the lint proved unused (workspace `hyper`, `metrics`, `metrics-exporter-prometheus`, `thiserror`; per-crate `thiserror`, `rustc-hash`, `metrics`) plus the duplicated `homepage`. Also replaced the `current_exe()` binary walk in three process tests with `CARGO_BIN_EXE_ocserve`, which is what CI and remote builds actually need; the walk only worked when the test exe sat beside the bin. | `rust-toolchain.toml` pin; `cargo +1.100.0 clippy --workspace --all-targets -- -D warnings` under `CARGO_BUILD_WARNINGS=deny` clean; 907 tests pass on the build box; guards 110/110, matrix, docs green | green |
## Test design techniques coverage (TESTING §4)

Expand Down
Loading
Loading