|
| 1 | +# Interface Contract: `allez oneshot` CLI surface |
| 2 | + |
| 3 | +This feature exposes a **CLI subcommand contract**: exit codes, JSON/ |
| 4 | +human stdout/stderr shapes, and the `tracing` observability schema |
| 5 | +extension — not a new Rust library API (that's GEN-23/GEN-24's contracts, |
| 6 | +consumed here unmodified). This is what an automated caller (spec.md's |
| 7 | +Operating Context: an AI agent, not a human at a terminal) can rely on. |
| 8 | + |
| 9 | +## Invocation shape (unchanged from GEN-22's existing scaffold) |
| 10 | + |
| 11 | +```text |
| 12 | +allez oneshot [PACKAGES]... -- <COMMAND> [ARGS...] |
| 13 | +``` |
| 14 | + |
| 15 | +- `PACKAGES`: zero or more package-name tokens before `--`. Zero is valid |
| 16 | + (FR-001's Acceptance Scenario 2) — resolves to the configured default/ |
| 17 | + override package set, not an error. |
| 18 | +- `-- <COMMAND> [ARGS...]`: required (FR-011); rejected as a usage error |
| 19 | + (exit `2`, category `missing_pass_through_command`) if `--` is absent, |
| 20 | + or present with nothing after it — enforced at the dispatch layer by the |
| 21 | + existing `cli::validate_pass_through`, unchanged by this ticket. |
| 22 | + |
| 23 | +## Exit codes (FR-006) |
| 24 | + |
| 25 | +| Code | Meaning | Caller-facing category (stderr) | FR-012 observability category | |
| 26 | +|---|---|---|---| |
| 27 | +| `0` | Pass-through command started and exited `0`. | N/A | N/A | |
| 28 | +| `1..=255` (pass-through's own code) | Pass-through command started and exited normally with that code. **Authoritative once the command starts** — even if it happens to equal one of the values below (FR-006's own explicit caveat). | None — FR-013: no envelope at all once started. | None (a normal exit carries no `failure_category`). | |
| 29 | +| `1` | Environment creation failed (FR-010), activation of an already-created environment failed, **or** registering a signal listener failed (this plan's `activation_failed`/`signal_setup_failed`; see `research.md`). | One of `unresolvable_package`, `integrity_verification_failed`, `unwritable_location`, `no_channels_configured`, `activation_failed`, or `signal_setup_failed`. `teardown_failed` never appears here — it appears only as `cleanup_category` in the dual-failure case below, alongside one of the other four `EphemeralEnvError` categories. | Same category, mirrored. | |
| 30 | +| `2` | Usage error (FR-011) — no `--`, or `--` with nothing after it. | `missing_pass_through_command`. | N/A — this case never reaches `oneshot::run` at all; covered instead by `main.rs`'s own pre-existing, schema-versioned usage-rejection record (see Observability contract below). | |
| 31 | +| `126` | Pass-through program found but could not be executed (FR-008). | `pass_through_not_executable`. | Same category, mirrored. | |
| 32 | +| `127` | Pass-through program's name could not be found (FR-008). | `pass_through_not_found`. | Same category, mirrored. | |
| 33 | +| `128 + N` | Pass-through program terminated by signal `N`, on a platform where that concept exists (FR-007). | **None** — the command already started, so FR-013 forbids any caller-facing message or category for this outcome; `pass_through_terminated_by_signal` exists *only* as an FR-012 observability category, never on stderr. | `pass_through_terminated_by_signal`. | |
| 34 | + |
| 35 | +Every other subcommand's own exit-code surface is unaffected (FR-006). |
| 36 | + |
| 37 | +## stdout/stderr contract |
| 38 | + |
| 39 | +**Before the pass-through program starts** (usage error, environment- |
| 40 | +creation failure, or pass-through-not-found/not-executable/activation |
| 41 | +failure): identical to every other existing `allez` subcommand's failure |
| 42 | +convention — `output::render_error` (or the new, additive `output:: |
| 43 | +render_ephemeral_creation_failure` for the dual-failure case below) on |
| 44 | +stderr; stdout is empty. JSON is the default; `--human` selects the |
| 45 | +human-readable form. Shape (JSON default): |
| 46 | + |
| 47 | +```json |
| 48 | +{"schema_version": "0.1.0-unstable", "category": "unresolvable_package", "message": "could not resolve package `nonexistent-pkg-xyz`"} |
| 49 | +``` |
| 50 | + |
| 51 | +**FR-010 dual-failure case** (creation failed *and* its own rollback also |
| 52 | +failed) — additive, optional fields, so this remains backward-compatible |
| 53 | +with every existing single-failure JSON consumer that only reads |
| 54 | +`category`/`message`: |
| 55 | + |
| 56 | +```json |
| 57 | +{ |
| 58 | + "schema_version": "0.1.0-unstable", |
| 59 | + "category": "unwritable_location", |
| 60 | + "message": "ephemeral environment location is unwritable", |
| 61 | + "cleanup_category": "teardown_failed", |
| 62 | + "cleanup_message": "ephemeral environment teardown failed" |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +`--human` mode renders both via one line, reusing `CreationFailure`'s own |
| 67 | +existing `Display` impl (GEN-24) verbatim — it already produces `"{error} |
| 68 | +(cleanup also failed: {cleanup})"` — so no separate human-mode dual-field |
| 69 | +logic is needed; only the JSON path needs the two new optional fields. |
| 70 | + |
| 71 | +**Once the pass-through program has successfully started** (FR-013): |
| 72 | +`allez` writes no further caller-facing result payload of its own to |
| 73 | +stdout or stderr, ever again, for this invocation. The pass-through |
| 74 | +program's own stdout/stderr — streamed live, each stream kept separate |
| 75 | +(FR-004) — *is* the entire visible output; `allez`'s own final exit code |
| 76 | +is the entire machine-actionable signal. This is the one documented |
| 77 | +exception to Constitution III's dual-format convention (spec.md FR-013 |
| 78 | +itself says so) — there is no JSON/human rendering to reconcile because |
| 79 | +there is no separate `allez`-authored result payload at all for this |
| 80 | +outcome. The `RUST_LOG`-gated `tracing` channel (Observability contract, |
| 81 | +below) is not this result payload: it is silent unless the caller |
| 82 | +explicitly opts in via `RUST_LOG`, and even then carries only the fixed, |
| 83 | +schema-versioned `OneshotOutcomeEvent` fields — never a second copy of, |
| 84 | +or a substitute for, the pass-through program's own output or exit code. |
| 85 | + |
| 86 | +## Observability contract (FR-012, additive extension) |
| 87 | + |
| 88 | +The "rejected as a usage error, never attempted" case (no `--`, or `--` |
| 89 | +with nothing after it) is already covered by `main.rs`'s existing |
| 90 | +`exit_on_invalid_pass_through`/`tracing::warn!(operation, category, ...)` |
| 91 | +call site — that rejection happens before `oneshot::run` (and therefore |
| 92 | +before any `EnvironmentId` exists to correlate a new event by) is ever |
| 93 | +reached, so it needs no new event type. This ticket adds one small, |
| 94 | +additive field to that pre-existing call, `schema_version` (the same |
| 95 | +value `OneshotOutcomeEvent` below uses), so the record satisfies FR-012's |
| 96 | +"carrying its own documented schema version" clause explicitly; no |
| 97 | +correlation identifier is added, since a rejected invocation never |
| 98 | +produces more than this one record, and FR-012's correlation requirement |
| 99 | +exists to tie multiple records together. |
| 100 | + |
| 101 | +For every invocation that passes usage validation, exactly one `tracing` |
| 102 | +event (in addition to whatever `ephemeral::mod.rs`'s own `create`/ |
| 103 | +`install`/`teardown` events already emit — unchanged by this ticket): |
| 104 | + |
| 105 | +1. Immediately after `create_ephemeral_environment` resolves — carries |
| 106 | + `pass_through_started: false` if it failed (with `failure_category`/ |
| 107 | + `message`, plus `cleanup_category`/`cleanup_message` for the dual- |
| 108 | + failure case). Emitted only on failure; a success is not separately |
| 109 | + recorded here, since the terminal event below already reports |
| 110 | + `pass_through_started: true` for that case, and this ticket emits |
| 111 | + exactly one event per invocation that reaches `oneshot::run`, never |
| 112 | + two. |
| 113 | +2. Immediately after the pass-through program's own outcome is known |
| 114 | + (started-and-exited, started-and-signaled, or could-not-start) — |
| 115 | + `pass_through_started: true` unless the failure is `NotFound`/ |
| 116 | + `NotExecutable`/`ActivationFailed`/`SignalSetupFailed`, all four of |
| 117 | + which are pre-start. |
| 118 | + |
| 119 | +Exactly one of the two above ever fires for a given invocation — never |
| 120 | +both, and never during the pass-through program's own execution. Both are |
| 121 | +emitted strictly outside the FR-004 streaming window — never while the |
| 122 | +pass-through program is running — per `OneshotOutcomeEvent` |
| 123 | +(`data-model.md`). `RUST_LOG` gates visibility identically to every other |
| 124 | +`tracing::*!` call site already in this codebase (`observability.rs`, |
| 125 | +unchanged): silent unless explicitly set, matching the existing "never |
| 126 | +interleave with the single-JSON-object stderr error contract" guarantee |
| 127 | +`tests/cli_scaffold.rs`'s `t063`/`t063a`/`t063b` already lock in for other |
| 128 | +subcommands. |
| 129 | + |
| 130 | +## Non-goals of this contract |
| 131 | + |
| 132 | +- No change to `create`/`list`/`remove`'s own exit-code/JSON surface. |
| 133 | +- No change to `run`/`sandbox`'s pass-through behavior — this ticket wires |
| 134 | + only `oneshot`; `pass_through.rs`'s functions are written as plain |
| 135 | + `pub(crate)` building blocks, but wiring `run`/`sandbox` to them is |
| 136 | + explicitly out of this ticket's scope, and no future consumer is |
| 137 | + assumed. |
| 138 | +- No environment-removal/teardown surface of any kind (FR-009) — GEN-24 |
| 139 | + exposes no environment-removal API at all for this ticket to call. |
| 140 | +- No new CLI flag — `oneshot`'s argument shape (`PackagesAndCommandArgs`) |
| 141 | + is unchanged from GEN-22's existing scaffold. `ALLEZ_CONDARC_PATH` (see |
| 142 | + `research.md` § Test strategy) is a test-only internal environment |
| 143 | + variable, gated behind the non-default `test-config-override` Cargo |
| 144 | + feature — a release build of `allez` has no code path that reads it at |
| 145 | + all, and it is not part of this contract. |
0 commit comments