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
159 changes: 159 additions & 0 deletions docs/vrs/06-omp-driver/.experiments/2026-09-02-omp-18-1-2-admission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# omp 18.1.2 admission checks

## Question

The omp launch gate admits the `18.0` minor series. The coding-agent-runtime
profile now pins `18.1.2`; per OMP-R05, may the `18.1` series be admitted, or
did one of the delivery-critical surfaces move?

Date: 2026-09-02. Profile under test:
`/nix/store/w5i48iwnmy92izxkg4473im45bk1akbw-coding-agent-runtime-profile.json`.
Selecting `accountId == "omp/omp-default"` resolves `execution.binPath` to
`/nix/store/j6811vbwii56ajl18svasjj8gijaj80r-omp-18.1.2/bin/omp`.
`readlink -f` returns that same path and `--version` prints `omp/18.1.2`.
Linux x86_64. Every probe below names that store binary explicitly.

## Method

The five OMP-R05 checks from
[`2026-08-28-omp-18-0-9-admission.md`](./2026-08-28-omp-18-0-9-admission.md)
were repeated with throwaway TypeScript extensions. Print mode used
`-p --no-session --no-tools --no-lsp --no-skills --no-rules --no-extensions`
and redirected stdin from `/dev/null`; interactive checks ran in named PTYs.
The approval check forced `--approval-mode always-ask`. All probe output,
session state, the hook root, catalog, workspace, and PTY registry lived below
one newly-created `/tmp/st2-omp1812-admission.*` root.

The delivery check did not imitate the channel. The branch's built `st2` binary
installed its immutable hook set into the disposable hook root and launched the
candidate through `st2 driver omp-session`; the injected shipped
`omp-channel.ts` spawned that same `st2` as `driver omp-channel`. The scratch
catalog validated with zero errors and zero warnings before launch.

## Result

**1. Extension load — pass.** At extension initialization the first argument
reported:

```json
{"sendUserMessage":"function","sendMessage":"function","on":"function","piNamespace":true}
```

The three calls used by the shipped channel and the `.pi` internals namespace
therefore remain present.

**2. Lifecycle inventory and missing `agent_settled` — pass.** A print-mode
turn fired, in order:

```text
session_start, agent_start, turn_start,
message_start, message_end, message_start, message_end,
turn_end, agent_end, session_shutdown
```

The interactive turn fired the same order through `agent_end` (the TUI was
then deliberately kept alive). Registering `agent_settled` did not throw and no
such event fired in either run. A raw byte count over the candidate binary found
`agent_settled` **0** times. The same count found
`tool_approval_requested` **3** times and `tool_approval_resolved` **3** times.
OMP-R03's sampled settle remains necessary.

**3. Idle edge — pass.** `ctx.isIdle()` was `false` at `agent_end` in both
modes. With a 25 ms sampler, the interactive run's first sample at **+26 ms**
was `true`. In print mode the first runnable sample was `true` at **+186 ms**
(after `session_shutdown`, while the scheduled sampler was still live). The
load-bearing rule is unchanged: `agent_end` itself is not an idle proof; the
channel must wait for the first positive bounded poll.

**4. Approval correlation — pass.** An interactive bash tool call under
`--approval-mode always-ask` emitted this pair and the approved command wrote
`APPROVAL_OK`:

```text
tool_approval_requested
sessionId: 01a06337-8ca5-75ef-8aed-6d65ea68ad77
toolName: bash
toolCallId: call_RFtkTfped0aDxOJf6lTR4CRe|fc_0b1ce953da5da810016a986026e18887d283e902092fca9482
approvalMode: always-ask

tool_approval_resolved
sessionId: 01a06337-8ca5-75ef-8aed-6d65ea68ad77
toolName: bash
toolCallId: call_RFtkTfped0aDxOJf6lTR4CRe|fc_0b1ce953da5da810016a986026e18887d283e902092fca9482
approved: true
```

Both `sessionId` and the complete `toolCallId` match across the pair, preserving
the OMP-R02 blocked-on-human correlation edge.

**5. Shipped-driver live delivery — pass.** The built branch binary installed
hook set
`sha256-2d4f8a83808b43fd1291a3d21567185fdf1a75a7357098970fba2fac6acc329e`
and launched `lab.omp1812` through `st2 driver omp-session` with PTY runtime
`omp1812-demo-run`. In the clean measured incarnation, the wrapper and channel
published:

```json
{"schema":"st2.harness-state.v1","agent":"lab.omp1812","harness":"omp",
"state":"idle","blockedOn":"none","inputBuffer":"unknown","ask":"none",
"ptySession":"omp1812-demo-run","incarnation":"60587-1788371709709-0",
"seq":2,"transitions":9}
```

Presence was `available`. A real
`st2 message send lab.omp1812 --as lab.omp1812` created
`1788371738141-gaf07f.md`. The shipped channel delivered its body into the idle
TUI without screen injection: harness state moved to `active`, transitions
**10**; the model ran the requested bash command and produced the exact artifact
content `CLEAN_SHIPPED_DRIVER_DELIVERY_OK`; it replied
`CLEAN_DRIVER_DELIVERY_DONE`; and state returned to `idle`, transitions **11**,
under the same incarnation and sequence. This covers the delivery path that the
minor gate protects.

## Focused verification

```text
$ nix develop -c cargo test --lib omp_session::tests
running 14 tests
...
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 657 filtered out

$ nix develop -c cargo build --bin st2
Finished `dev` profile [unoptimized + debuginfo] target(s) in 38.17s
```

The build emitted pre-existing warnings outside this change; neither command
reported an error. The disposable driver PTY was killed and removed, then the
entire probe root (catalog, hook set, PTY registry, workspace, extensions,
logs, and artifacts) was deleted.

## Conclusion

All five delivery-critical checks pass on omp 18.1.2. The extension API,
lifecycle vocabulary, still-missing `agent_settled`, sampled idle edge,
approval correlation, and shipped driver delivery loop remain valid. Admitting
the `18.1` minor series alongside `18.0` is justified under decision 0007 and
OMP-R05.

The gate remains per minor. This capture does not assert that harness-context
`tokens` arithmetic was remeasured on 18.1.2; the exact-build fixture for that
separate HC-R13/HC-T03 question remains unchanged.

## Residual risk

- The approval deny path remains unmeasured, as in the 18.0 captures (OMP-T01,
DQ-OMP-1).
- This is one Linux x86_64 capture using the configured `openai-codex`
GPT-5.6-Sol model and the approval success path; it is not a provider matrix.
- Minor-series admission intentionally lets later `18.1.x` patches launch
without another capture. A new minor remains refused.
- The interactive TUI advertised `18.1.4` during the probe. Update-banner
suppression remains the existing DQ-OMP-5 concern and did not affect delivery.

## VRS Impact

Extends `SUPPORTED_OMP_MINORS` in `src/omp_session.rs` from `[(18, 0)]` to
`[(18, 0), (18, 1)]`, pins the three admission capture builds in the focused
gate test, and keeps neighbouring minors, majors, ambiguous banners, and
pre-releases fail-closed. OMP-R02, OMP-R03, OMP-R05, and OMP-A01 remain
satisfied; no open question is closed by this capture.
69 changes: 37 additions & 32 deletions src/omp_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::process::ExitStatus;
use anyhow::{Context as _, Result};

use crate::provider_session::{
PROVIDER_POLL, ProviderOutcome, STOP, install_signal_handler, run_provider_observed,
install_signal_handler, run_provider_observed, ProviderOutcome, PROVIDER_POLL, STOP,
};
use crate::{harness_state, harness_version, hooks, message, status};

Expand All @@ -44,15 +44,17 @@ pub const CHANNEL_SEQ: &str = "ST2_OMP_CHANNEL_SEQ";
/// harmless either way.
const OFFLINE_DEFAULTS: [(&str, &str); 2] = [("PI_OFFLINE", "1"), ("PI_SKIP_VERSION_CHECK", "1")];

/// The omp MINORS verified against the admission checks in `docs/vrs/06-omp-driver/spec.md`
/// (18.0 measured twice: at 18.0.3 on 2026-08-25 and again at 18.0.9 on 2026-08-28).
/// The omp MINORS verified against the admission checks in `docs/vrs/06-omp-driver/spec.md`.
///
/// 18.0 was measured twice: at 18.0.3 on 2026-08-25 and again at 18.0.9 on 2026-08-28.
/// 18.1 was measured at 18.1.2 on 2026-09-02.
///
/// Admission is per minor, per decision 0007 ("hard version gate on the minor, 18.x initially")
/// and OMP-R05 ("a later minor stays rejected"). Any patch inside an admitted minor launches
/// without new evidence: omp releases near-daily, so gating patches blocked the fleet on changes
/// the capture already covered — 18.0.10 shipped within hours of 18.0.9 being admitted. A new
/// MINOR still costs the five OMP-R05 probes.
const SUPPORTED_OMP_MINORS: [(u32, u32); 1] = [(18, 0)];
const SUPPORTED_OMP_MINORS: [(u32, u32); 2] = [(18, 0), (18, 1)];

/// The omp builds the harness-context producer's arithmetic was measured against (HC-R13, HC-T03).
///
Expand Down Expand Up @@ -317,14 +319,15 @@ mod tests {
/// `.experiments/` capture that justifies it.
#[test]
fn admitted_minors_are_exactly_the_measured_set() {
assert_eq!(SUPPORTED_OMP_MINORS, [(18, 0)]);
assert_eq!(SUPPORTED_OMP_MINORS, [(18, 0), (18, 1)]);
}

/// The two versions actually measured must still launch — widening to the minor must not
/// drop the evidence the minor was admitted on.
/// Every exact build that admitted a minor must still launch. Keeping the literals here makes
/// each OMP-R05 capture a deliberate part of the gate rather than inferring evidence from the
/// admitted series.
#[test]
fn version_gate_admits_every_measured_version() {
for version in MEASURED_CONTEXT_VERSIONS {
fn version_gate_admits_every_admission_capture() {
for version in ["18.0.3", "18.0.9", "18.1.2"] {
let fake = FakeExecutable::new(&format!(
"#!/bin/sh\nprintf 'omp v{version}\\n{version}\\n'\n"
));
Expand Down Expand Up @@ -352,36 +355,35 @@ mod tests {
}
}

/// A pre-release must not be admitted as its base release, even though its base minor is
/// A pre-release must not be admitted as its base release, even when its base minor is
/// admitted: it is not the build any capture measured.
#[test]
fn version_gate_refuses_a_prerelease_inside_an_admitted_minor() {
for version in ["18.0.9-rc1", "18.0.9+meta"] {
let fake =
FakeExecutable::new(&format!("#!/bin/sh\nprintf '{version}\\n'\n"));
for version in ["18.0.9-rc1", "18.0.9+meta", "18.1.2-rc1", "18.1.2+meta"] {
let fake = FakeExecutable::new(&format!("#!/bin/sh\nprintf '{version}\\n'\n"));
assert!(
verify_supported_version(fake.path().to_str().unwrap()).is_err(),
"{version} must not be admitted as 18.0.9"
"{version} must not be admitted as its base release"
);
}
}

/// A banner mentioning some other version must not bind the gate to it. Reported in review of
/// #370: `runtime 18.0.0 omp/18.1.0` would otherwise admit on the unrelated `18.0.0` and then
/// launch an unverified 18.1 provider. The provider's own label decides; an unlabelled banner
/// #370: `runtime 18.0.0 omp/18.2.0` would otherwise admit on the unrelated `18.0.0` and then
/// launch an unverified 18.2 provider. The provider's own label decides; an unlabelled banner
/// carrying two different releases fails closed.
#[test]
fn a_stray_version_in_the_banner_cannot_admit_an_unverified_provider() {
let fake = FakeExecutable::new(
"#!/bin/sh\nprintf 'runtime 18.0.0 omp/18.1.0\\n'\n",
);
let fake = FakeExecutable::new("#!/bin/sh\nprintf 'runtime 18.0.0 omp/18.2.0\\n'\n");
let error = verify_supported_version(fake.path().to_str().unwrap())
.expect_err("the omp-labelled 18.1.0 must decide, not the stray 18.0.0")
.expect_err("the omp-labelled 18.2.0 must decide, not the stray 18.0.0")
.to_string();
assert!(error.contains("18.1.0"), "must name the provider's own release: {error}");
assert!(
error.contains("18.2.0"),
"must name the provider's own release: {error}"
);

let ambiguous =
FakeExecutable::new("#!/bin/sh\nprintf 'runtime 18.0.0 18.1.0\\n'\n");
let ambiguous = FakeExecutable::new("#!/bin/sh\nprintf 'runtime 18.0.0 18.2.0\\n'\n");
assert!(
verify_supported_version(ambiguous.path().to_str().unwrap()).is_err(),
"an unlabelled banner with two different releases must fail closed"
Expand All @@ -406,13 +408,12 @@ mod tests {
);
}

/// Minors are compared as numbers. `18.10` must not pass on the strength of admitted `18.0`,
/// Minors are compared as numbers. `18.10` must not pass on the strength of admitted `18.1`,
/// which is how a minor gate would decay into "accept anything that starts with 18".
#[test]
fn version_gate_refuses_a_neighbouring_minor_that_shares_a_prefix() {
for version in ["18.10.0", "18.1.0"] {
let fake =
FakeExecutable::new(&format!("#!/bin/sh\nprintf '{version}\\n'\n"));
for version in ["18.10.0", "18.2.0"] {
let fake = FakeExecutable::new(&format!("#!/bin/sh\nprintf '{version}\\n'\n"));
let error = verify_supported_version(fake.path().to_str().unwrap())
.expect_err(version)
.to_string();
Expand All @@ -425,14 +426,18 @@ mod tests {
/// allowlist; passes once the gate keys on MAJOR.MINOR.
#[test]
fn a_patch_inside_an_admitted_minor_is_accepted_without_new_evidence() {
let fake = FakeExecutable::new("#!/bin/sh\nprintf 'omp v18.0.11\\n18.0.11\\n'\n");
verify_supported_version(fake.path().to_str().unwrap())
.expect("18.0.11 is inside admitted minor 18.0 and must launch");
for version in ["18.0.11", "18.1.99"] {
let fake = FakeExecutable::new(&format!(
"#!/bin/sh\nprintf 'omp v{version}\\n{version}\\n'\n"
));
verify_supported_version(fake.path().to_str().unwrap())
.unwrap_or_else(|error| panic!("{version} is inside an admitted minor: {error}"));
}
}

#[test]
fn version_gate_refuses_an_unverified_minor() {
let fake = FakeExecutable::new("#!/bin/sh\nprintf '18.1.0\\n'\n");
let fake = FakeExecutable::new("#!/bin/sh\nprintf '18.2.0\\n'\n");
let error = verify_supported_version(fake.path().to_str().unwrap()).unwrap_err();
assert!(error.to_string().contains("unverified"), "{error}");
}
Expand Down Expand Up @@ -464,7 +469,7 @@ mod tests {
let mut paths = Vec::with_capacity(ROUNDS);
barrier.wait();
for _ in 0..ROUNDS {
let fake = FakeExecutable::new("#!/bin/sh\nprintf '18.1.0\\n'\n");
let fake = FakeExecutable::new("#!/bin/sh\nprintf '18.2.0\\n'\n");
paths.push(fake.path().to_path_buf());
let error =
verify_supported_version(fake.path().to_str().unwrap()).unwrap_err();
Expand Down
Loading