v0.9: Stage 1 Verify — runtime/verify + audit recorder + lifectl info/run wiring (#121)#128
Merged
Merged
Conversation
…/run wiring (#121) Implements the seven sub-steps of LIFE_RUNTIME_STANDARD §2.1-§2.5 plus v0.8 Part B §B.1 row 1 (lifecycle gate / withdrawal pre-flight / audit-chain integrity). Stage gating is fail-close (D6): any sub-step failure aborts Stage 1, emits assembly_aborted{stage='verify', reason}, and refuses to mount. - runtime/verify/{_structural,_schema,_time,_inventory,_audit_chain, _consent,_lifecycle}.py — one module per sub-step - runtime/verify/__init__.py — public verify(life_path, *, audit, withdrawal_policy) entry point - runtime/verify/result.py — VerifyResult / VerifyError dataclasses (always returned, even on FAIL, so callers can present structured rejection reasons) - runtime/audit/recorder.py — minimal in-memory AuditRecorder used by Stages 1-4 until #125 replaces it with the v0.4 hash-chain emitter - runtime/cli/lifectl.py — info subcommand prints structured Stage 1 report (text/JSON), exits 0/1; run subcommand runs Stage 1 then exits 1 on FAIL or 2 on PASS (Stage 2+ pending #122-#126); --withdrawal-mock {not-revoked|revoked|unreachable|malformed} for test-only short-circuit (production runtimes MUST omit it) - tools/test_runtime_verify.py — 15 sanity-test cases (real http.server for the §2.5 happy path, in-place zip mutation for negatives) - .github/workflows/validate.yml — runtime-scaffold job renamed to cover both #120 and #121, installs tools/requirements.txt, runs tools/test_runtime_verify.py - CHANGELOG.md — sub-issue #121 entry under the v0.9 section - runtime/README.md — Quickstart updated for the new info/run surface Closes #121. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
The bundled examples/minimal-life-package/out/*.life is gitignored (it's a build artifact), so CI runners check out the repo without it. Mirror the negative tests' approach and call _build_life() inside a TemporaryDirectory for the three CLI surface tests. Closes the CI failure on PR #128 (3 of 15 tests failing with 'missing bundled fixture' / 'life_path does not exist'). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Three edge cases where invalid input crashed verify() instead of returning a structured VerifyResult. All three are spec-permitted inputs that the descriptor schema does not currently reject (it relies on annotation-only `format: date-time` / `format: uri`), so the runtime must defend against them itself. - _audit_chain.py: wrap the audit/events.jsonl decode in try/except UnicodeDecodeError; emit audit_log_not_utf8. - _time.py: reject naive (no-tz) datetimes in _parse_iso. Naive datetimes violated RFC 3339 anyway; letting them through caused TypeError on offset-aware comparisons. - _consent.py: wrap the urllib.request.Request constructor in try/except ValueError; emit endpoint_malformed_url for schemeless withdrawal_endpoints. Adds three regression tests in tools/test_runtime_verify.py covering each crash path; total runtime-verify tests now 18 / 18 green. Reported via Devin Review on PR #128. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Second PR of the v0.9 epic (#119). Implements the seven sub-steps of
docs/LIFE_RUNTIME_STANDARD.md§2.1–§2.5 + v0.8 Part B §B.1 row 1 (lifecycle gate / withdrawal pre-flight / audit-chain integrity). Stage gating is fail-close (D6): any sub-step failure aborts Stage 1, emitsassembly_aborted{stage="verify", reason}, and refuses to mount.Closes #121.
What landed
runtime/verify/modules — one per sub-step:_structural.py(§2.1: zip safe-open + path-traversal rejection + descriptor parse)_schema.py(§2.1 cont.:life-package.schema.jsonre-validation + binding-spec §7forbidden_usesnamespace check;x-extensions accepted as advisory, unknown non-x-keys fail-close)_time.py(§2.2:created_atnot in future /expires_atnot past /expires_at > created_at, with 30s clock-skew tolerance)_inventory.py(§2.3: per-entry sha256 + size match, plus rejection of any zip entry not incontents[])_audit_chain.py(§2.4: v0.4 hash-chainprev_hashwalk + canonical re-hash of every event +audit_event_refresolves to apackage_emittedevent whosemetadata.package_idmatches)_consent.py(§2.5: in-archiveconsent_evidence_refreadability + HTTP GET ofwithdrawal_endpointwith 4xx/5xx/network-failure rejection +status:"withdrawn"rejection)_lifecycle.py(Part B §B.1 row 1:withdrawnandtaintedstates refused; absentlifecycle/lifecycle.jsontreated asactivefor pre-v0.8 packages, with a warning)runtime/verify/__init__.py— publicverify(life_path, *, audit, withdrawal_policy) -> VerifyResultentry point. Always returns aVerifyResult(withpackage_id,lifecycle_state,audit_chain_length,inventory_entries_verified,errors[],warnings[]) so the caller can present a structured rejection reason — no opaque "Failed to load".runtime/audit/recorder.py— minimal in-memoryAuditRecorderused by Stages 1–4 until v0.9 sub-issue v0.9 sub-issue #6: Stage 5 Guard (withdrawal / lifecycle / expiry watchers + clean teardown + audit emitter) #125 ships the full v0.4 hash-chain emitter that links the runtime's session log back to the bundledaudit/events.jsonl. Recordsmount_attempted,withdrawal_poll,assembly_aborted{stage="verify"}events.runtime/cli/lifectl.py—infoandrunwired:lifectl info <pkg>prints a structured §2.1–§2.5 + lifecycle report (human-readable by default, JSON via--json); exit 0 on PASS / 1 on FAIL.lifectl run <pkg>runs Stage 1; on PASS exits 2 withStage 2+ pending sub-issues #122-#126to stderr; on FAIL exits 1.--withdrawal-mock {not-revoked|revoked|unreachable|malformed}for test-only short-circuit of the §2.5 HTTP poll. Production runtimes MUST omit this flag — spec mandates a real HTTP GET per §2.5.tools/test_runtime_verify.py— 15 sanity-test cases:http.serverdriving the §2.5urllib.request.urlopenend-to-end.lifezip (_rebuild_zip_with): bad zip / missing descriptor / inventory hash mismatch / unlisted entry / audit chain break / expired package / lifecycle=withdrawnstatus:"withdrawn", or being unreachable (port-rebind trick)assembly_abortedaudit event emissionlifectl infoexit 0 + PASS print,--jsonstructured errors,lifectl runexit 2 with Stage 1 ✓.github/workflows/validate.yml—runtime-scaffoldjob now runs both v0.9 sub-issue #1:runtime/package scaffold +lifectlCLI entrypoint #120 + v0.9 sub-issue #2: Stage 1 Verify (zip / schema / time / identity / integrity / audit chain / consent / withdrawal pre-flight / lifecycle gate) #121 test drivers, installstools/requirements.txtsojsonschemais available.CHANGELOG.md+runtime/README.md— sub-issue v0.9 sub-issue #2: Stage 1 Verify (zip / schema / time / identity / integrity / audit chain / consent / withdrawal pre-flight / lifecycle gate) #121 entry; Quickstart refreshed for the new info/run surface.Hard-rule invariants enforced for Stage 1
assembly_aborted{stage="verify", reason}, and the runtime refuses to proceed (no degraded mount).status:"withdrawn"all reject the mount.withdrawnandtaintedpackages refused;frozen(memorial) flagged for read-only enforcement at sub-issue v0.9 sub-issue #6: Stage 5 Guard (withdrawal / lifecycle / expiry watchers + clean teardown + audit emitter) #125 (Stage 5 Guard).Review & Testing Checklist for Human
(Risk: yellow — first stage with real assembly logic, but every sub-step is covered by both Python unit tests and a CLI subprocess test. CI is the primary gate.)
runtime-scaffoldjob passes on both py3.11 and py3.12.verification: PASSand exit 0.--jsonoutput is parsable (lifectl info <pkg> --withdrawal-mock revoked --json | jq).lifectl run <pkg> --withdrawal-mock not-revokedprintsStage 1 Verify ✓+ the package metadata, then exits 2 withStage 2+ pending sub-issues #122-#126to stderr.Notes
lifectl runalways stops with the "Stage 2+ pending" message after Stage 1 PASS. Stages 2–5 land in v0.9 sub-issue #3: Stage 2 Resolve (Provider Registry + LifeCapabilityProvider + tier-aware resolution + bundled refusal) #122–v0.9 sub-issue #6: Stage 5 Guard (withdrawal / lifecycle / expiry watchers + clean teardown + audit emitter) #125; the e2e echo Provider in v0.9 sub-issue #7: Built-in echo Provider + end-to-end conformance harness + Quickstart docs #126.--withdrawal-mockflag is test-only and should be removed (or moved behind a hidden debug entry point) before v1.0. Until then it is the only way for CI / sandboxes without external network access to exercise the §2.5 path deterministically.Link to Devin session: https://app.devin.ai/sessions/ff7322e18fd94887875daa2c1c75f87d
Requested by: @LING71671