Skip to content

v0.9 sub-issue #4: Stage 3 Assemble (graded sandbox: built_in + user_installed subprocess + capability_bound audit) #123

@devin-ai-integration

Description

@devin-ai-integration

v0.9 Sub-issue #4 — Stage 3 Assemble

Part of v0.9 epic.

Implements v0.8 Part B Stage 3 (Assemble): instantiate each resolved
Provider in its declared sandbox class with hard_constraints injected,
emit capability_bound audit events, and produce an "assembled mount" that
Stage 4 (Run) can drive.

Spec ref

  • docs/LIFE_RUNTIME_STANDARD.md §2.6–§2.8 (asset handling, personality
    load, bind runtime obligations)
  • docs/LIFE_RUNTIME_STANDARD.md Part B §B.4 (sandbox classes)
  • docs/LIFE_RUNTIME_STANDARD.md Part B §B.4.1 (bundled_in_life refusal)
  • docs/LIFE_RUNTIME_STANDARD.md Part B §B.7 (capability_bound audit)
  • docs/LIFE_BINDING_SPEC.md §6 (hard_constraints, fail-close on missing)
  • docs/LIFE_BINDING_SPEC.md §8 (surface.ui_hints.disclosure_label)

Sandbox classes

Class Implementation
built_in Provider runs in the same Python process as lifectl. Direct method calls on the LifeCapabilityProvider instance. No subprocess.
user_installed Provider runs in a separate OS subprocess. IPC over stdin/stdout JSON-RPC 2.0 (one request, one response, line-delimited). The subprocess is launched as a Python child running a runtime.assemble.subprocess_host shim that imports the user-installed Provider package, hands stdin lines to invoke(), writes responses to stdout. SIGTERM on teardown; SIGKILL after 5s. The shim mode satisfies the §B.4 "separate OS process with IPC" minimum boundary. Stricter sandboxing (firejail / nsjail / seccomp / wasm) is RECOMMENDED future work, NOT required at v0.9.
bundled_in_life REJECTED at Stage 3 even if it slipped past Stage 2. Defence-in-depth per §B.4.1.

Assemble flow per capability

Given a ProviderRef from Stage 2:

  1. Read the binding/runtime_binding.json::capability_binding[capability]
    entry: asset_paths[], params{}, hard_constraints{}.
  2. Resolve asset_paths[] to absolute paths inside the mounted zip.
    For pointer-mode .life, resolve the pointer target with offline-first
    semantics (read from local cache; warn but DO NOT auto-fetch).
  3. Inject hard_constraints keys into the Provider's initialize() call.
    Per binding spec §6, missing-constraint = fail-close.
  4. If sandbox_class == built_in: import + instantiate the Provider in-
    process; call initialize(asset_paths, params, hard_constraints).
  5. If sandbox_class == user_installed: spawn the subprocess host, hand
    it the Provider import path + the same args; await initialize_ack.
  6. If sandbox_class == bundled_in_life: refuse fail-close.
  7. After successful initialize, register the Provider's invoke
    handle in the runtime's capability table.
  8. Inject surface.ui_hints.disclosure_label (binding spec §8) into
    the runtime's user-surface controller (will be consumed by Stage 4 Run).
  9. Emit capability_bound{capability, provider_name, provider_version, sandbox_class}.

After all capabilities bound: emit mount_succeeded{package_id, capabilities_bound[]}.
This single event marks the runtime as "live" — Stage 5 watchers may
begin polling immediately after this event.

Module layout

runtime/assemble/
├── __init__.py            # exports assemble(verify_result, resolve_result) -> AssembleResult
├── _hard_constraints.py   # inject + fail-close-on-missing
├── _disclosure.py         # ui_hints injection
├── builtin_host.py        # in-process Provider instantiation
├── subprocess_host.py     # user_installed subprocess + JSON-RPC shim
└── audit.py               # capability_bound + mount_succeeded

AssembleResult dataclass:

@dataclass
class AssembleResult:
    capability_table: dict[str, BoundCapability]   # capability → live invoke handle
    disclosure_label: str
    forbidden_uses: dict                            # passthrough to Stage 4
    hosted_api_preference: dict                     # passthrough to Stage 4
    sandbox_subprocesses: list[Popen]               # for Stage 5 teardown

Audit events emitted

  • capability_bound{capability, provider_name, provider_version, sandbox_class} — once per capability.
  • mount_succeeded{package_id, capabilities_bound[]} — once at end of Stage 3.
  • assembly_aborted{stage: "assemble", reason} — fail-close on hard_constraint
    miss / sandbox spawn error / bundled refusal / initialize exception.

CLI surface

lifectl run <pkg.life> after this PR: runs Stages 1–3; on PASS prints
the bound capability table + disclosure label. Stages 4–5 still TODO.

Stage 1 Verify   ✓
Stage 2 Resolve  ✓  3 caps resolved
Stage 3 Assemble ✓  3 caps bound (1 built_in, 2 user_installed subprocess)
                    disclosure_label="(AI digital life instance of …)"
Stage 4+ pending sub-issues 5-7

Tests

tools/test_runtime_assemble.py:

  1. Built-in echo path: registry has only an in-process echo Provider
    for text_chat → assemble succeeds, capability_table populated,
    capability_bound + mount_succeeded emitted.
  2. Hard-constraint miss: binding declares
    hard_constraints.max_response_length: 500; Provider's
    initialize() does not accept it → fail-close.
  3. Sandbox subprocess happy path: dummy user_installed Provider
    shipped under tests/fixtures/dummy_provider/ exposed via JSON-RPC
    shim; assemble spawns subprocess + initialize succeeds.
  4. Sandbox subprocess crash: Provider raises in initialize → fail-close
    assembly_aborted{stage: "assemble"} emitted; subprocess reaped (no zombie).
  5. bundled_in_life sneak path: registry returns a Provider with
    sandbox_class == "bundled_in_life" (bypassing Stage 2 filter via test
    harness) → Stage 3 still rejects.
  6. Disclosure label injection: assemble result includes the binding's
    surface.ui_hints.disclosure_label verbatim.

Acceptance

  • In-process built_in host implemented
  • Subprocess host with JSON-RPC IPC implemented
  • Hard-constraint injection + fail-close
  • All 6 test cases pass
  • No subprocess zombies after teardown
  • CI runtime-assemble job green

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions