Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
run: python tools/test_pipelines.py

runtime-scaffold:
name: v0.9 runtime scaffold (sub-issue #120)
name: v0.9 runtime scaffold + Stage 1 Verify (sub-issues #120, #121)
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -109,29 +109,33 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install runtime package (editable)
- name: Install runtime package (editable) + tooling deps
run: |
python -m pip install --upgrade pip
python -m pip install -r tools/requirements.txt
python -m pip install -e .

- name: Run runtime-scaffold test suite
run: python tools/test_runtime_scaffold.py

- name: Run runtime-verify test suite (Stage 1)
run: python tools/test_runtime_verify.py

- name: Confirm `lifectl` console script installed
run: |
lifectl version
# exit non-zero on info / run is expected (scaffold-only stubs)
# missing-path on info / run is expected to exit non-zero
set +e
lifectl info pretend.life
info_rc=$?
lifectl run pretend.life
run_rc=$?
set -e
if [ "$info_rc" -eq 0 ] || [ "$run_rc" -eq 0 ]; then
echo "ERROR: lifectl info/run unexpectedly succeeded in scaffold-only build"
echo "ERROR: lifectl info/run unexpectedly succeeded for missing path"
exit 1
fi
echo "scaffold stubs exit non-zero as expected (info=$info_rc, run=$run_rc)"
echo "missing-path stubs exit non-zero as expected (info=$info_rc, run=$run_rc)"

docs:
name: Lint docs (markdownlint + linkcheck)
Expand Down
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
reader at the right follow-up sub-issue (#121 / #121-#126).
- `pyproject.toml` at repo root — declares `dlrs-runtime` package
(`name = "dlrs-runtime"`, `version = "0.9.0.dev0"`, `requires-python
>= 3.10`, deps `jsonschema` + `pyyaml`) and exports the `lifectl`

Check failure on line 46 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Lint docs (markdownlint + linkcheck)

Spaces inside code span elements

CHANGELOG.md:46:40 MD038/no-space-in-code Spaces inside code span elements [Context: "`) and exports the `"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md038.md

Check failure on line 46 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Lint docs (markdownlint + linkcheck)

Spaces inside code span elements

CHANGELOG.md:46:10 MD038/no-space-in-code Spaces inside code span elements [Context: "`, deps `"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md038.md
console script via `[project.scripts]`. Setuptools is told to
package only `runtime*` so the existing `tools/` and `pipelines/`

Check failure on line 48 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Lint docs (markdownlint + linkcheck)

Spaces inside code span elements

CHANGELOG.md:48:3 MD038/no-space-in-code Spaces inside code span elements [Context: "package only `"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md038.md
trees stay out of the wheel.
- `runtime/audit/emitter.py` — `RuntimeAuditEmitter` stub class that
raises `NotImplementedError` referencing sub-issue #125 (the full
Expand Down Expand Up @@ -74,6 +74,48 @@
the gates run yet. Sub-issues #121–#126 reinstate each invariant as
they implement the corresponding Stage.

### Added (sub-issue #121 — Stage 1 Verify)

- `runtime/verify/` populated with the seven §2.1–§2.5 + lifecycle gate
sub-steps (`_structural`, `_schema`, `_time`, `_inventory`,
`_audit_chain`, `_consent`, `_lifecycle`) plus a public
`verify(life_path, *, audit, withdrawal_policy)` entry point in
`runtime/verify/__init__.py`. The function returns a structured
`VerifyResult` (with `package_id`, `lifecycle_state`, audit-chain
length, inventory entry count, errors, warnings) on every call —
ok or not — so the caller can present a structured rejection
reason to the user (D6=fail-close).
- `runtime/audit/recorder.py` — minimal in-memory `AuditRecorder` used
by Stages 1-4 until v0.9 sub-issue #125 ships the full v0.4 hash-chain
emitter that links the runtime's session log back to the bundled
`audit/events.jsonl` chain. Records `mount_attempted`,
`withdrawal_poll`, and `assembly_aborted{stage="verify"}` events.
- `runtime/cli/lifectl.py` — `lifectl info <pkg>` now prints a
structured Stage 1 report (human-readable by default, JSON via
`--json`) and exits 0 on PASS / 1 on FAIL. `lifectl run <pkg>`
executes Stage 1 and exits 1 on Stage 1 FAIL or 2 (with
`Stage 2+ pending sub-issues #122-#126` to stderr) on Stage 1 PASS.
Both subcommands accept `--withdrawal-mock {not-revoked|revoked|
unreachable|malformed}` (test-only; production runtimes MUST omit it
— spec mandates a real HTTP poll per §2.5).
- `tools/test_runtime_verify.py` — 15 sanity-test cases covering the
seven verify sub-steps plus three CLI surface assertions. Negative
fixtures are constructed by mutating a freshly-built `.life` zip
(`_rebuild_zip_with`) and the happy path drives a real local
`http.server` so the §2.5 `urllib.request.urlopen` path is exercised
end-to-end. The driver is wired into the existing
`runtime-scaffold` CI job as a new `Run runtime-verify test suite
(Stage 1)` step.
- `.github/workflows/validate.yml` — `runtime-scaffold` job renamed to
cover both #120 and #121, installs `tools/requirements.txt` (for
`jsonschema`) before running Stage 1 tests.

Hard-rule invariants now enforced for Stage 1: D6=fail-close (any
sub-step failure aborts Stage 1, emits `assembly_aborted`, and refuses
to proceed); the §2.5 withdrawal pre-flight rejects 4xx/5xx/network
failures; the lifecycle gate refuses `withdrawn` and `tainted`
packages.

## v0.8-asset-architecture (2026-04-26)

**Status**: Released. v0.8 closes the four asset-architecture gaps left
Expand Down Expand Up @@ -114,7 +156,7 @@
`compute.hosted_api_used: true` requires at least one entry in
`compute.hosted_api_providers[]`. [#101]
- `tools/test_genesis_schema.py` — 36 sanity-test cases (4 happy-path
+ 32 negative) wired into `tools/batch_validate.py`. [#101]

Check failure on line 159 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Lint docs (markdownlint + linkcheck)

Unordered list style

CHANGELOG.md:159:1 MD004/ul-style Unordered list style [Expected: dash; Actual: plus] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md004.md
- `docs/LIFE_LIFECYCLE_SPEC.md` — per-topic normative spec for Topic 2
(Asset Lifecycle). Defines four document shapes
(`package_lifecycle`, `asset_lifecycle`, `mutation_event`,
Expand Down Expand Up @@ -156,7 +198,7 @@
non-`x-` keys reject statically (decision D4=C fail-close at schema
layer). [#103]
- `tools/test_binding_schema.py` — 63 sanity-test cases (11 happy-path
+ 52 negative) wired into `tools/batch_validate.py`. The 63 includes

Check failure on line 201 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Lint docs (markdownlint + linkcheck)

Unordered list style

CHANGELOG.md:201:1 MD004/ul-style Unordered list style [Expected: dash; Actual: plus] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md004.md
three negatives for `providers_whitelist_ref` path-traversal (added
in #111 review fix-up) and eight more cases (6 negative + 2 happy)
for path-traversal rejection on `surface.ui_hints.avatar_image_ref`
Expand All @@ -183,7 +225,7 @@
`LIFE_TIER_SPEC.md` so future naming schemes can ship without a
spec major bump. [#104]
- `tools/test_tier_schema.py` — 81 sanity-test cases (26 happy-path
+ 55 negative) covering both ends of every score → level range,

Check failure on line 228 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Lint docs (markdownlint + linkcheck)

Unordered list style

CHANGELOG.md:228:1 MD004/ul-style Unordered list style [Expected: dash; Actual: plus] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md004.md
every score → level mismatch boundary, every required-field
removal, every dimension off-enum, and the auto-computation guard
on `computed_by`. Wired into `tools/batch_validate.py`. [#104]
Expand Down Expand Up @@ -250,7 +292,7 @@
[#104]: https://github.com/Digital-Life-Repository-Standard/DLRS/issues/104
[#105]: https://github.com/Digital-Life-Repository-Standard/DLRS/issues/105


Check failure on line 295 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Lint docs (markdownlint + linkcheck)

Multiple consecutive blank lines

CHANGELOG.md:295 MD012/no-multiple-blanks Multiple consecutive blank lines [Expected: 1; Actual: 2] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md012.md
## v0.7-vision-shift (2026-04-26)

**Status**: Released. Repositions DLRS's ULTIMATE from "Digital Life
Expand All @@ -258,7 +300,7 @@
to "**`.life` 可运行数字生命档案文件标准**" — a dual standard:

1. **`.life` archive file format** — the distribution unit, a packaged
+ signed subset of a DLRS v0.6 record.

Check failure on line 303 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Lint docs (markdownlint + linkcheck)

Unordered list style

CHANGELOG.md:303:1 MD004/ul-style Unordered list style [Expected: dash; Actual: plus] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md004.md
2. **`.life` runtime protocol** — how compatible runtimes load + execute
a `.life` to produce an *AI digital life instance*.

Expand All @@ -271,7 +313,7 @@
under milestone
[`.life Archive + Runtime Standard (v0.7-vision-shift)`](https://github.com/Digital-Life-Repository-Standard/DLRS/milestone/5).
All 8 sub-issues #80–#87 closed; PRs #88, #89, #91, #92, #93, #94,
#95, #97, #98 merged.

Check failure on line 316 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Lint docs (markdownlint + linkcheck)

No space after hash on atx style heading

CHANGELOG.md:316:1 MD018/no-missing-space-atx No space after hash on atx style heading [Context: "#95, #97, #98 merged."] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md018.md

This epic ships **specs + schema + example builder**. It does **not**
ship a working runtime — that is deferred to v0.8+.
Expand Down Expand Up @@ -585,7 +627,7 @@

### Closes

#28 (epic), #29, #30, #31, #32, #33, #34, #35, #36, #37, #38.

Check failure on line 630 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / Lint docs (markdownlint + linkcheck)

No space after hash on atx style heading

CHANGELOG.md:630:1 MD018/no-missing-space-atx No space after hash on atx style heading [Context: "#28 (epic), #29, #30, #31, #32..."] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md018.md

---

Expand Down
17 changes: 13 additions & 4 deletions runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@ runtime/
```
pip install -e . # from repo root
lifectl version # confirm install
lifectl run examples/minimal-life-package/out/*.life
lifectl info examples/minimal-life-package/out/*.life --withdrawal-mock not-revoked
lifectl run examples/minimal-life-package/out/*.life --withdrawal-mock not-revoked
```

Until v0.9 sub-issues #121-#126 land, `lifectl info` and `lifectl run` exit
with a "not yet implemented in this sub-issue" message — only `lifectl version`
is functional.
As of v0.9 sub-issue #121, Stage 1 Verify is wired:

- `lifectl info <pkg>` prints a structured §2.1–§2.5 + lifecycle report
(human-readable by default, JSON via `--json`) and exits **0** on PASS /
**1** on FAIL.
- `lifectl run <pkg>` runs Stage 1 then exits **2** with a "Stage 2+ pending
sub-issues #122-#126" message; full mount comes online sub-issue by
sub-issue.

`--withdrawal-mock` is **test-only**; production runtimes MUST omit it so
the §2.5 withdrawal endpoint is genuinely polled over HTTP.

## Why a separate Python package?

Expand Down
9 changes: 5 additions & 4 deletions runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
This package implements the protocol defined in
``docs/LIFE_RUNTIME_STANDARD.md`` (v0.7 §1-10 + v0.8 Part B 5-stage assembly).

Public surface today (v0.9 sub-issue #120 — scaffold only):
Public surface as of v0.9 sub-issue #121 (Stage 1 Verify wired):

- ``__version__`` — runtime package version (``0.9.0.dev0``).
- ``LIFE_RUNTIME_PROTOCOL_VERSION`` — declared life-runtime spec version.
- ``Runtime`` — placeholder class; concrete assembly stages land in sub-issues
#121-#126.
- ``Runtime`` — placeholder class; concrete Stages 2-5 land in sub-issues
#122-#125, end-to-end echo Provider in #126.
- ``runtime.verify.verify`` — Stage 1 Verify entry point.

The ``runtime.cli.lifectl`` module exposes the ``lifectl`` console script.
"""
Expand All @@ -29,7 +30,7 @@ def __init__(self) -> None:
def __repr__(self) -> str:
return (
f"Runtime(version={self.version!r}, "
f"protocol={self.protocol!r}, stages_implemented=[])"
f"protocol={self.protocol!r}, stages_implemented=['verify'])"
)


Expand Down
15 changes: 13 additions & 2 deletions runtime/audit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
"""Runtime-side audit emitter (v0.4 hash chain).
"""Runtime-side audit emitter.

Stub in v0.9 sub-issue #120; full implementation lands in #125 (Stage 5 Guard).
v0.9 sub-issue #121 lands the *recorder* surface used by Stages 1-4
(``AuditRecorder.emit(event_type, **fields)``). The full v0.4 hash-chain
implementation that links the runtime's session log back to the bundled
``audit/events.jsonl`` chain ships in v0.9 sub-issue #125 (Stage 5 Guard);
until then events are accumulated in memory and optionally written to a
JSONL file for inspection / test assertions.
"""

from __future__ import annotations

from runtime.audit.recorder import AuditRecorder, AuditEvent

__all__ = ["AuditRecorder", "AuditEvent"]
89 changes: 89 additions & 0 deletions runtime/audit/recorder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Lightweight in-memory audit recorder used until v0.9 sub-issue #125.

This is **not** the v0.4 hash-chain emitter — that lands in #125 and will
take over both responsibilities of recording AND of chaining
``prev_hash`` from the bundled ``audit/events.jsonl`` tip.

For Stage 1 Verify (sub-issue #121) the runtime needs a way to record
``mount_attempted``, ``withdrawal_poll``, and ``assembly_aborted`` events
deterministically so tests can assert on them. The recorder accumulates
events in order and optionally mirrors them to a JSONL file.
"""

from __future__ import annotations

import json
import os
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


def _utc_now_iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")


@dataclass(frozen=True)
class AuditEvent:
"""One recorded audit event.

``event_type`` matches the v0.7 / v0.8 vocabulary (e.g.
``mount_attempted``, ``withdrawal_poll``, ``assembly_aborted``,
``capability_bound``, ``lifecycle_transition_observed``, ``unmount``).
``occurred_at`` is the wall-clock timestamp at recording.
``fields`` carries per-event payload as a plain dict.
"""

event_type: str
occurred_at: str
fields: dict[str, Any]

def to_dict(self) -> dict[str, Any]:
return {
"event_type": self.event_type,
"occurred_at": self.occurred_at,
"fields": self.fields,
}


@dataclass
class AuditRecorder:
"""Append-only ordered list of audit events.

Pass ``mirror_path`` to also stream each event to a JSONL file on
disk (used by ``lifectl info`` to produce inspectable output). The
full hash-chained emitter from sub-issue #125 will subclass / replace
this object while keeping the same ``emit`` API.
"""

mirror_path: Path | None = None
events: list[AuditEvent] = field(default_factory=list)

def emit(self, event_type: str, **fields: Any) -> AuditEvent:
evt = AuditEvent(
event_type=event_type,
occurred_at=_utc_now_iso(),
fields=dict(fields),
)
self.events.append(evt)
if self.mirror_path is not None:
self.mirror_path.parent.mkdir(parents=True, exist_ok=True)
with self.mirror_path.open("a", encoding="utf-8") as fp:
fp.write(json.dumps(evt.to_dict(), sort_keys=True) + "\n")
return evt

def types(self) -> list[str]:
return [e.event_type for e in self.events]

def latest(self, event_type: str) -> AuditEvent | None:
for evt in reversed(self.events):
if evt.event_type == event_type:
return evt
return None


def default_mirror_path(package_id: str) -> Path:
"""Default per-mount audit log path: ``$XDG_DATA_HOME or ~/.local/share/dlrs/mounts/<pkg>/events.jsonl``."""
base = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
return Path(base) / "dlrs" / "mounts" / package_id / "events.jsonl"
Loading
Loading