Skip to content

v0.9: Stage 1 Verify — runtime/verify + audit recorder + lifectl info/run wiring (#121)#128

Merged
LING71671 merged 3 commits into
masterfrom
devin/1777294723-121-stage1-verify
Apr 27, 2026
Merged

v0.9: Stage 1 Verify — runtime/verify + audit recorder + lifectl info/run wiring (#121)#128
LING71671 merged 3 commits into
masterfrom
devin/1777294723-121-stage1-verify

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented Apr 27, 2026

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, emits assembly_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.json re-validation + binding-spec §7 forbidden_uses namespace check; x- extensions accepted as advisory, unknown non-x- keys fail-close)
    • _time.py (§2.2: created_at not in future / expires_at not 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 in contents[])
    • _audit_chain.py (§2.4: v0.4 hash-chain prev_hash walk + canonical re-hash of every event + audit_event_ref resolves to a package_emitted event whose metadata.package_id matches)
    • _consent.py (§2.5: in-archive consent_evidence_ref readability + HTTP GET of withdrawal_endpoint with 4xx/5xx/network-failure rejection + status:"withdrawn" rejection)
    • _lifecycle.py (Part B §B.1 row 1: withdrawn and tainted states refused; absent lifecycle/lifecycle.json treated as active for pre-v0.8 packages, with a warning)
  • runtime/verify/__init__.py — public verify(life_path, *, audit, withdrawal_policy) -> VerifyResult entry point. Always returns a VerifyResult (with package_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-memory AuditRecorder used 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 bundled audit/events.jsonl. Records mount_attempted, withdrawal_poll, assembly_aborted{stage="verify"} events.
  • runtime/cli/lifectl.pyinfo and run wired:
    • 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 with Stage 2+ pending sub-issues #122-#126 to stderr; on FAIL exits 1.
    • Both subcommands accept --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:
    • Happy path with a real local http.server driving the §2.5 urllib.request.urlopen end-to-end
    • Negative fixtures constructed by mutating a freshly-built .life zip (_rebuild_zip_with): bad zip / missing descriptor / inventory hash mismatch / unlisted entry / audit chain break / expired package / lifecycle=withdrawn
    • Withdrawal endpoint returning HTTP 403, returning status:"withdrawn", or being unreachable (port-rebind trick)
    • assembly_aborted audit event emission
    • CLI surface: lifectl info exit 0 + PASS print, --json structured errors, lifectl run exit 2 with Stage 1 ✓
  • .github/workflows/validate.ymlruntime-scaffold job now runs both v0.9 sub-issue #1: runtime/ package scaffold + lifectl CLI 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, installs tools/requirements.txt so jsonschema is 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

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.)

  • CI runtime-scaffold job passes on both py3.11 and py3.12.
  • Smoke test on the bundled fixture:
    pip install -e .
    lifectl info examples/minimal-life-package/out/*.life --withdrawal-mock not-revoked
    
    should print verification: PASS and exit 0.
  • --json output is parsable (lifectl info <pkg> --withdrawal-mock revoked --json | jq).
  • lifectl run <pkg> --withdrawal-mock not-revoked prints Stage 1 Verify ✓ + the package metadata, then exits 2 with Stage 2+ pending sub-issues #122-#126 to stderr.

Notes

Link to Devin session: https://app.devin.ai/sessions/ff7322e18fd94887875daa2c1c75f87d
Requested by: @LING71671


Open in Devin Review

…/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>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

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>
devin-ai-integration[bot]

This comment was marked as resolved.

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>
@LING71671 LING71671 merged commit 2978c94 into master Apr 27, 2026
7 checks passed
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.

v0.9 sub-issue #2: Stage 1 Verify (zip / schema / time / identity / integrity / audit chain / consent / withdrawal pre-flight / lifecycle gate)

1 participant