Skip to content

CS-15: expose the five-rung runtime package ladder in the SDK and CLI - #392

Merged
maximelb merged 3 commits into
masterfrom
CS-15-public-ladder
Sep 22, 2026
Merged

maximelb merged 3 commits into
masterfrom
CS-15-public-ladder

Conversation

@maximelb

@maximelb maximelb commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

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.py gains the five rungs of plan 24 D6, mirroring findings.RuntimeStatuses() in order:

Constant Wire Means
RUNTIME_UNKNOWN "" no usable evidence — missing, stale, expired, unattributable or conflicting
RUNTIME_PRESENT present an agent is there, the telemetry cannot carry a claim
RUNTIME_NOT_OBSERVED not_observed a complete window saw the package never run
RUNTIME_LOADED loaded mapped into a running process
RUNTIME_EXECUTING executing is the running executable

Plus RUNTIME_REASONS (the 16-token closed vocabulary from the backend verdict contract) and RUNTIME_LEVELS.

decode_runtime_status is the one decoder: legacy dormant → 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 with recognized=False, so a caller can count malformed input instead of silently reading it as unknown.

is_runtime_negative exists 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) POSTs 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 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) mirrors backend.Aggregate / CheckResult.Headline, because the fold a caller would reach for is wrong in a specific way: the negative rung ranks BELOW present on 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 provides CheckResult.Status() rather than letting callers reduce the rows themselves.

One deliberate deviation from the Go original: it stamps no source on 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 existing finding subgroup pattern, with register_explain("cloudsec.finding.runtime-check", …) and a discovery.PROFILES entry so the verb is discoverable. The help text, the explain text and doc/cli/cloud-security.md 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.

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:

  • No fold over incomplete input can produce not_observed: a partial sensor set vetoes a unanimous negative (sensors_partial); one present sibling vetoes it; one unknown sibling vetoes it; malformed rows never read as a verdict.
  • But the negative is still reachable — a unanimous negative over a complete sensor set survives, so the rung is not decorative and a real regression cannot hide behind a fold that never fires.
  • dormant decodes 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.
  • An unrecognised token is unknown and reported as unrecognised; an empty token is unknown and reported as recognised (it is the wire spelling, not malformed input).
  • A sighting wins even with sensors_complete: false; executing outranks loaded; an all-unknown fold keeps a real reason from the closed vocabulary.
  • The client fold invents no producer.
  • The route is path-escaped and the response is passed through unchanged (identity-checked).
  • The command help affirms no safety claim while explicitly denying each misreading — checked as an affirmation, because the sentence that makes the copy honest is itself a denial and a flat substring ban on "not exploitable" would forbid exactly the fix. (That is how this test was written the first time; it failed, correctly.)

dormant left alone

Sensor 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_status is 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 pristine git archive <sha> extraction; findings are recorded as a PR comment.

No tags. No deploy. No flag enabled.

🤖 Generated with Claude Code

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>
lcbill
lcbill previously approved these changes Sep 22, 2026
…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>
lcbill
lcbill previously approved these changes Sep 22, 2026
@maximelb

maximelb commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Adversarial self-review (this repo does not use /lc-review)

Reviewed from a pristine git archive cc68bea extraction, against the merged the backend library contract and against the two backend halves that landed while this was in flight. It found one HIGH that made the whole client inert, and one guard of mine that guarded nothing. Both are fixed in a8b51b9.

HIGH — the client was written against the backend library's internal struct, not the public wire

The route's backend halves exist now: the API gateway CS-15-runtime-check-route and the backend graph service CS-15-runtime-package-check. The gateway forwards the actor's Data dict verbatim, so the real envelope is

{"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); the retry field is retry_after_seconds, an int of seconds, not a Go duration in nanoseconds.

So runtime_headline found zero rows and answered — 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. Worse, my tests cemented it: every fixture hand-built the struct-shaped payload, and the CLI command was only ever exercised with --help, so a pure composition bug was invisible.

Fix: runtime_headline is deleted 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 and runtime_packages() reads the flat rows. Re-deriving was also dishonestly described — the option help said "uses the aggregation the backend defines", which it did not. The reason the fold must not be rebuilt client-side now lives on the reader.

MEDIUM — decode_runtime_status rejected the unknown rung's actual spelling

the backend library merged WireRuntimeStatus, which renders the unknown rung as the literal "unknown". Every status the backend emits goes through it, so the old decoder classified 100% of legitimate unknowns as malformed input. Four prose surfaces asserted "the wire spelling is the EMPTY STRING". All corrected; both spellings now decode, as in DecodeRuntimeStatus.

MEDIUM — RUNTIME_REASONS advertised a closed set that the route does not honour

The route returns five reasons that are not backend.Reason members at all: feature_disabled, no_resource, no_packages, no_sensors, cache_unavailable. They explain the absence of a verdict and arrive with accepted: false. feature_disabled is the answer for most orgs today, because the feature is default-off — reporting that with the authority of a measured answer is the whole failure mode. They now live in a disjoint RUNTIME_UNAVAILABLE_REASONS, and RUNTIME_REASONS is documented as a reference list rather than a validator, since the backend owns it and may add to it.

MEDIUM — my honesty guard accepted the exact overclaim it existed to ban

_affirms_no_safety asserted "not" in clause. The copy satisfies that by accident: not_observed contains "not", as do nothing and cannot. It passed:

"not_observed proves the vulnerability is not exploitable and the finding is fixed."

A keyword check cannot decide whether prose affirms or denies — claiming otherwise is the same over-claim this package is about. 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 failure mode and no ban list catches it. test_the_guard_rejects_dishonest_copy pins all four samples that used to pass, and pins that the real copy still passes.

Also fixed

  • Raises: LcApiException named a class that does not exist in this SDK (it is the v1 name). Now PermissionDeniedError / NotFoundError / ApiError.
  • RUNTIME_LEVELS was dead documentation. It is now the validator behind level, so an unrecognised level reports as unknown instead of being echoed as a stronger claim.
  • The readers tracebacked on a non-dict runtime or a non-list packages.
  • The command is now actually executed by four tests against a mocked SDK, including one that fails if a projection reports unknown for an executing resource — the test that would have caught the HIGH.
  • The POST body is asserted empty (plan §9: every target derives from the finding id server-side).
  • The legacy token's absence is checked structurally against the module source, not just swept behaviourally — the earlier test claimed to do this and did not.
  • Every surface now says asking starts the measurement, so a cold first call is expected to be inconclusive.
  • CHANGELOG entry added.

Accepted as correct, for the record

The fold that is now deleted was byte-exact faithful to backend.Aggregate — differentially tested over all 1–3 row combinations × sensors_complete, 312/312 matching on status, reason and level. It was removed because the server states the answer, not because it was wrong. Path escaping, the POST verb, cloudsec.get, and the registration across register_explain / discovery.PROFILES / docs all checked out.

What I did not verify

No live call — the route is undeployed, so the envelope is proven against the committed backend and gateway code and their OpenAPI schema, not against a running datacenter. If either branch changes shape before merging, this client has to move with it; that is named as a gate on the package issue.

Full suite: 4516 passed, 6 skipped (all six pre-existing and platform-gated).

…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>
@maximelb
maximelb merged commit 58dc104 into master Sep 22, 2026
6 checks passed
@maximelb
maximelb deleted the CS-15-public-ladder branch September 22, 2026 20:50
@maximelb

maximelb commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

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 the API gateway CS-15-runtime-check-route + the backend graph service CS-15-runtime-package-check, both still unmerged — if either changes shape before it lands, this client moves with it. Recorded as a gate on the internal tracker.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants