CS-15: expose the five-rung runtime package ladder in the SDK and CLI - #392
Conversation
Neither the SDK nor the CLI could answer "did that vulnerable code actually
run?". This adds the read surface for it, and — more importantly — adds it with
the honesty constraints the contract carries, so the first caller cannot read a
telemetry gap as a clean bill of health.
`CloudSec.check_finding_runtime(finding_id)` POSTs the documented route
`POST /cloudsec/{oid}/findings/{finding_id}/runtime-check` (plan 24 §6,
`cloudsec.get`, informational). That gateway route is a SEPARATE SLICE and is
not deployed; the method and its docs say so, and the call fails like any
unknown route rather than degrading to an answer from nothing. The response is
returned verbatim — nothing is reinterpreted here, so an unsettled answer
cannot be quietly rendered settled.
The ladder mirrors go-cloudsec `findings/runtime.go` exactly: `RUNTIME_UNKNOWN`
(the empty string on the wire), `RUNTIME_PRESENT`, `RUNTIME_NOT_OBSERVED`,
`RUNTIME_LOADED`, `RUNTIME_EXECUTING`, plus the closed `RUNTIME_REASONS`
vocabulary from `runtimeevidence/verdict.go`. `decode_runtime_status` is the
one decoder: it folds the legacy `dormant` spelling to `not_observed` (plan
§14) and cannot emit it, an empty/absent token stays unknown, and a token this
build does not recognise becomes unknown with `recognized=False` so a caller
can count malformed input instead of reading it as a verdict.
`runtime_headline` mirrors `runtimeevidence.Aggregate`, because the naive fold
is wrong in a specific and dangerous way: the negative rung ranks BELOW
`present` on purpose, so one package with an incomplete window — or a partial
sensor enumeration — vetoes a whole-resource negative, and a maximum over the
rungs silently loses that veto. Unlike the Go original it stamps no `source` on
a locally-folded unknown: claiming the producer token for a client-side
reduction would invent provenance.
`limacharlie cloudsec finding runtime-check <id>` follows the existing
`finding` subgroup, with `--headline` for the safe fold. Its help, its `explain`
text and the CLI reference all state plainly that `not_observed` is not a
safety claim, that nothing here proves anything about exploitability, and that
a telemetry lapse yields `present` or unknown with a reason — never a negative.
Tests pin the two properties the ladder exists for rather than restating its
shape: no fold over incomplete input can produce `not_observed` (partial sensor
set, one incomplete sibling, one unknown sibling, malformed rows), a unanimous
negative over a complete set still survives so the rung is not decorative, and
`dormant` can never come back out of any path. Also: the route is path-escaped,
the response is passed through unchanged, and the command's help affirms no
safety claim while explicitly denying each misreading.
This repo does not use /lc-review; the diff was reviewed adversarially from a
pristine `git archive` extraction and the findings recorded on the PR.
Contract: refractionPOINT/go-cloudsec#413
Package: maximelb/claude-config#150
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t guarded nothing
Adversarial self-review (pristine `git archive` extraction) found the client
written against go-cloudsec's INTERNAL struct rather than the public wire. The
route's two backend halves landed while this was in flight — lc_api-go
`CS-15-runtime-check-route` and legion_graph `CS-15-runtime-package-check` — and
they emit something else entirely:
{"accepted": bool,
"runtime": null | {resource_urn, status, reason, level, source, sensors,
sensors_complete, complete, checked_at,
retry_after_seconds?, packages: [FLAT rows]}}
Three mismatches at once: everything is nested under `runtime`, the per-package
rows are FLAT (no `verdict` sub-object), and the retry field is
`retry_after_seconds` (int seconds), not a Go duration in nanoseconds. The old
`runtime_headline` therefore found zero rows and ANSWERED — reporting
`unknown / sensors_partial` for a resource whose package was `executing`. That is
precisely the "quietly answers from nothing" failure its own docstring claimed to
prevent, and the tests cemented it because every fixture hand-built the
struct-shaped payload.
`runtime_headline` is gone, and nothing replaces the fold. The backend already
computes `CheckResult.Headline()` and publishes it at the top of the `runtime`
object, so `runtime_verdict()` READS it. Re-deriving was both dishonestly
described ("uses the aggregation the backend defines" — it did not) and a
permanent drift surface. `runtime_packages()` reads the flat rows. The reason the
fold must not be rebuilt client-side is now documented where it belongs, on the
reader.
`decode_runtime_status` now accepts `"unknown"`. go-cloudsec merged
`WireRuntimeStatus`, which renders the unknown rung as that literal token because
an empty string in a JSON enum reads as a missing field rather than as an answer.
Every status on the wire goes through it, so the old decoder classified 100% of
legitimate unknowns as malformed input. Four prose surfaces said "the wire
spelling is the empty string"; all corrected.
Also from the review:
- `RUNTIME_UNAVAILABLE_REASONS` is new and deliberately disjoint from
`RUNTIME_REASONS`: `feature_disabled`, `no_resource`, `no_packages`,
`no_sensors`, `cache_unavailable` explain the ABSENCE of a verdict, not a
verdict, and arrive with `accepted: false`. The feature is DEFAULT-OFF, so
`feature_disabled` is the answer for most orgs today and reporting it with the
authority of a measured answer would be the whole failure mode. `RUNTIME_REASONS`
is now documented as a reference list rather than a closed validator, since the
backend owns it and may add to it.
- Asking STARTS the measurement (the route publishes relevance, which is why it is
a POST), so a cold first call is expected to be inconclusive. Every surface now
says so and names `retry_after_seconds`.
- `Raises:` named `LcApiException`, which does not exist in this SDK. Replaced with
the real `PermissionDeniedError` / `NotFoundError` / `ApiError`.
- `RUNTIME_LEVELS` was dead documentation; it is now the validator behind the
`level` field, so an unrecognised level is reported as `unknown` rather than
echoed as a stronger claim than it is.
- Readers no longer traceback on a non-dict `runtime` or a non-list `packages`.
The honesty guard was the worst of it, and it was mine. `_affirms_no_safety`
asserted `"not" in clause`, which the copy satisfies by accident — `not_observed`
contains "not", as do `nothing` and `cannot`. It accepted "not_observed proves the
vulnerability is not exploitable and the finding is fixed." A keyword check cannot
decide whether prose affirms or denies, so it no longer pretends to: it bans only
constructions with no honest use here, requires a negation BEFORE the verb in any
proof claim about exploitability, and the callers separately assert the required
denials are PRESENT — because silence is the other way this copy goes wrong and no
ban list catches that. `test_the_guard_rejects_dishonest_copy` pins all four
samples that used to pass.
Finally, the command is now actually EXECUTED by tests (it was only ever invoked
with --help, which is why a CLI-composition bug shipped invisible): four tests run
it against a mocked SDK, including one that fails if a projection reports `unknown`
for an `executing` resource. The POST body is asserted empty — plan §9 requires
every target to be derived from the finding id server-side — and the legacy token's
absence is now checked structurally against the module source, not just swept
behaviourally.
Full suite: 4516 passed, 6 skipped (all six pre-existing and platform-gated).
Contract: refractionPOINT/go-cloudsec#413
Package: maximelb/claude-config#150
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adversarial self-review (this repo does not use
|
…tly again `stale_confirmation` landed on go-cloudsec master while this was in flight (the CS-15-confirmation branch merged): the producer has not confirmed a window recently enough, so the stored evidence may be behind what it saw — or the producer may be gone. It was missing from RUNTIME_REASONS, which would have left a live reason with no explanation anywhere in the CLI or the docs. The list is a reference rather than a validator — an unrecognised reason is still rendered, never dropped — but a missing entry is a gap in what we can EXPLAIN, which is the whole point of a closed vocabulary. So it is now asserted against the merged contract rather than trusted to stay in step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Merged as 58dc104 (CI green on the exact head e69e461: unit tests on Python 3.10–3.14 plus the Cloud Build). Contract now bound to the gateway envelope from Inert as merged: the route is not deployed and the runtime-evidence feature it needs is default-off, so the new verb fails like any unknown route rather than answering from nothing. Nothing tagged, nothing released. |
Package CS-15, public surface. Contract authority: the backend contract (the backend status contract, the backend verdict contract, the backend design note).
Neither the SDK nor the CLI could answer "did that vulnerable code actually run?". This adds the read surface — and, more to the point, adds it with the honesty constraints the contract carries, so the first caller cannot read a telemetry gap as a clean bill of health.
The ladder
limacharlie/sdk/cloudsec.pygains the five rungs of plan 24 D6, mirroringfindings.RuntimeStatuses()in order:RUNTIME_UNKNOWN""RUNTIME_PRESENTpresentRUNTIME_NOT_OBSERVEDnot_observedRUNTIME_LOADEDloadedRUNTIME_EXECUTINGexecutingPlus
RUNTIME_REASONS(the 16-token closed vocabulary from the backend verdict contract) andRUNTIME_LEVELS.decode_runtime_statusis the one decoder: legacydormant→not_observed(plan §14) and it cannot emit the legacy token; an empty or absent token stays unknown; a token this build does not recognise becomes unknown withrecognized=False, so a caller can count malformed input instead of silently reading it as unknown.is_runtime_negativeexists so callers ask the question one way — "not loaded and not executing" and "watched a complete window and never saw it run" are different statements and only the second has been earned.The transport
CloudSec.check_finding_runtime(finding_id)POSTsPOST /cloudsec/{oid}/findings/{finding_id}/runtime-check(plan 24 §6,cloudsec.get, informational).That gateway route is a separate slice and is not deployed. The docstring, the CLI help and the CLI reference all say so, and the call fails the way any unknown route does rather than degrading to an answer from nothing. The response is returned verbatim — no field is reinterpreted here, so an unsettled answer cannot be quietly rendered settled and a duration cannot become a guess. The finding id is path-escaped.
The fold
runtime_headline(result)mirrorsbackend.Aggregate/CheckResult.Headline, because the fold a caller would reach for is wrong in a specific way: the negative rung ranks BELOWpresenton purpose, so one package with an incomplete window — or a sensor set the caller could not fully enumerate — vetoes a whole-resource negative. A maximum over the rungs silently loses that veto, which is exactly why the backend library providesCheckResult.Status()rather than letting callers reduce the rows themselves.One deliberate deviation from the Go original: it stamps no
sourceon a locally-folded unknown.backend.Unknown()does, because there it is the producer; claiming the producer token for a reduction performed client-side would invent provenance.The CLI
limacharlie cloudsec finding runtime-check <finding_id> [--headline], following the existingfindingsubgroup pattern, withregister_explain("cloudsec.finding.runtime-check", …)and adiscovery.PROFILESentry so the verb is discoverable. The help text, the explain text anddoc/cli/cloud-security.mdall state plainly thatnot_observedis not a safety claim, that nothing here proves anything about exploitability, and that a telemetry lapse yieldspresentor unknown with a reason — never a negative.What the tests prove
tests/unit/test_sdk_runtime_package_ladder.py(34 tests) pins the two properties the ladder exists for, not its shape:not_observed: a partial sensor set vetoes a unanimous negative (sensors_partial); onepresentsibling vetoes it; oneunknownsibling vetoes it; malformed rows never read as a verdict.dormantdecodes from any casing/whitespace and cannot come back out of any path, checked structurally over the whole token space plus a full fold, not by inspecting one caller.sensors_complete: false;executingoutranksloaded; an all-unknown fold keeps a real reason from the closed vocabulary.dormantleft aloneSensor sleep mode in
limacharlie/commands/sensor.py(is_sleep, "dormant mode") is a different concept and is untouched. So is every CIEM identity-dormancy reference.decode_runtime_statusis the only thing in this diff that knows the word.Verification
pytest tests/unit/ tests/microbenchmarks/— 4501 passed, 6 skipped (all six skips pre-existing and platform-gated: Windows-only, macOS-only, orjson-unavailable, two pre-existing missing docstrings). No test added or changed is skipped.Review
This repo does not use
/lc-review(the bot never posts a review body here), so per the standing decision the diff got an adversarial self-review from a pristinegit archive <sha>extraction; findings are recorded as a PR comment.No tags. No deploy. No flag enabled.
🤖 Generated with Claude Code