Skip to content

feat(protected-content): installed inactive e2e proof — three custody replicas + repair after loss (ELACITY-2298) - #57

Closed
irzhywau wants to merge 37 commits into
feat/protected-content-installed-provisioningfrom
feat/protected-content-installed-e2e-proof
Closed

irzhywau wants to merge 37 commits into
feat/protected-content-installed-provisioningfrom
feat/protected-content-installed-e2e-proof

Conversation

@irzhywau

@irzhywau irzhywau commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #52 (ELACITY-2297, installed provisioning). ELACITY-2298: run the full inactive protected-content path on an installed Runtime with three real custody replicas, prove repair after one replica is lost, and record hash-bound evidence. No cutover; the provisional drm/rights/key/decrypt path stays live throughout.

What this delivers

Three commits, each reviewable on its own.

a4faa2e2 feat(server,capsules) — product changes the live run required. Every item was found only by running the installed journey; the mock-provider tests masked them all.

  • carrier: seed the address book from the operator peer store; operator peers become replication candidates behind announced holders; a failed candidate no longer consumes a placement slot (a dead announced holder made a single node loss unrepairable); the availability receipt is stamped when the observation completes and the pre-buy/open verifier re-samples its clock after the refresh.
  • elastos run <provider> [--with …]: host native providers standalone on the runtime core planes, now including the chain plane trusting the provisioned client issuer — a custody committee member settles every release through its own chain rights evidence.
  • media-provider against real ffmpeg: 11-field _runtime_invocation envelope, 1-based DASH segment numbering, styp/sidx prefix stripping, -streaming 0; contracts crate accepts codec-configuration children (avcC, colr, esds) in avc1/mp4a sample entries.
  • chain-provider: envelope handling, describe_protected_content_market_source, detailed upstream_rpc_error, multi-source rights corroboration pinned to the lowest common finalized block, opt-in plain_http_rpc_hosts allowlist for containerised evidence sources.
  • protected-content runtime: read encrypted principal-root sources for media preparation; availability policy travels with the publish request; pre-buy availability is a fresh ensure; managed-account viewer release persists a pending approval and resumes the identical wallet request; coordinator accepts the wallet approval envelope; media parts are released in order.
  • creator tail: *_pending mint receipts are no longer hard failures; ERC-1155 operator approval is raised for the market gateway on the operative (resolved from the AssetCreated log), not the asset ledger.
  • transaction effects: completing a purchase after a Runtime restart projects the validated Chain outcome under the effect's persisted authority instead of the caller's fresh launch (the wallet refused the newer launch and the purchase stayed pending forever). The mock wallet now binds on the same six authority fields as the real wallet, and a dedicated gateway test fails without the fix.
  • diagnosability: every library_error carries detail (source site + cause chain); fail-closed sites log at warn; workflow checkpoints at debug; the provider bridge warns while a raw request is pending for more than 30 s.

2b13ac00 feat(deploy,scripts,ci) — the harness and proof machinery.

  • deploy/custody-host/: minimal multi-stage image (elastos, custody-provider, chain-provider, pinned checksum-verified kubo), zero-env restarts, DID-keyed descriptor handoff over /shared, three-node compose harness, up.sh sync-chain-config. Simulation only; not a production deployment (README states the boundary).
  • scripts/protected-content-installed-e2e-proof.sh: provision → preflight → wallet-setup → mint → availability → buy → open → drill-custody → drill-replica → negative → restart → cleanup → finalize, each phase writing to one hash-bound receipt; passkey step-up delegated through --step-up-hook; secret-redacted transcripts; fail-closed on every unexpected answer. Assertions follow the product contract observed live (2-of-3 serves with one member stopped or tampered; below quorum fails closed; content status is the last stored receipt; cross-principal reads are refused by the purchase gate).
  • scripts/custody-harness-ci-smoke.sh + CI job custody-harness-smoke (workflow_dispatch + PR paths filter): the ceremony + preflight rehearsal against a throwaway harness. Its first run on a Linux runner (this PR's first push) failed before any node exported its descriptor: a bind mount keeps the host directory's owner and mode on Linux, so the containers' unprivileged user could not write shared/ (Docker Desktop maps that ownership away, which is why no macOS run could see it), and the owner-only descriptor could not have been read back by the host either. up.sh now makes shared/ world-writable (sticky) before the nodes start, the entrypoint probes /shared writability up front and fails closed with the remedy, up.sh streams each ready node's descriptor out through compose exec cat into a host-owned owner-only copy (the ceremony insists on owner-only input), the descriptor poll dumps compose ps -a and each node's last log lines on timeout, and the smoke preserves the containers' logs in the uploaded evidence directory before teardown and tears the throwaway project down even when up.sh fails. The second Linux run then died on the file crossing the other way (cp: can't open '/shared/chain-provider.json': Permission denied, captured by that new artifact): the host wrote it owner-only; it is now written 0644 (network ids, contract addresses and RPC URLs, nothing secret) with an up-front readability check in the entrypoint, and the compose services map host.docker.internal to the engine's host-gateway, which only Docker Desktop resolves natively.
  • static audit: slim custody-host role = {custody, availability, ipfs, chain}.

745fc33e docs — docs/PROTECTED_CONTENT.md (provider-host run mode, simulated topology, operator flow, remaining work), scripts/README.md, state.md, TASKS.md gates 3/4/6/7 annotated to the verified boundary, none checked beyond evidence.

The branch also carries 89b25532 feat(hello), the portable Rust capsule example under examples/hello/, which this work was built on top of.

Live proof evidence

Run 2026-09-05..07 on the installed client (scripts/setup-source-home.sh receipt, gateway on localhost:61180), three custody-host containers, an Anvil fork of Base (two distinct-origin evidence RPC sources, finality advanced by a block ticker), headless recovery-ready profiled principals (creator, buyer, denial) with managed wallet accounts.

  • finalize reads overall_ok: true; every required phase present and ok: true (provision, preflight, wallet_setup, mint, availability, buy, open, drill_custody, drill_replica, negative, restart, cleanup; optional chain_config_real also ok).
  • Receipt, transcript and commands log under ~/Library/Application Support/elastos/receipts/protected-content-installed-e2e-proof* on the proving machine; the receipt records the host binary sha256 (e0124dcf…) and identical elastos / custody-provider sha256 on all three containers (21617dd6… / 6b3be62b…).
  • Proven live: three replicas with a live multi-peer proof at mint; on-chain mint and operator approval on the operative; buyAccess finalized on both evidence sources; managed-wallet viewer release, 2-of-3 release settled by the nodes' own chain evidence, ordered init + segment reads, close; one node stopped → repair-worker heals → open serves; tampered custody share excluded, served on the intact 2-of-3; below quorum fails closed; non-purchaser and cross-principal reads denied; stale replay rejected; SIGKILL between approval and confirmation resumes without a duplicate transaction; explicit close settles and the boot sweeper settles a lease abandoned by a mid-open kill.
  • The proof binaries predate two lint-only edits folded afterwards to pass clippy --all-targets -D warnings (an explicit too_many_arguments allow on RuntimePreparedRecipient::from_persisted_parts, boxing the signed arm of RuntimeReleaseWalletOutcome); state.md records this.

Verification

  • just test (elastos workspace + all 25 capsule workspaces): 3796 passed, 0 failed.
  • cargo clippy --workspace --all-targets -- -D warnings, cargo fmt --all --check, git diff --check: clean.
  • scripts/protected-content-installed-e2e-proof-smoke.py and the static-audit smoke pass.
  • scripts/custody-harness-ci-smoke.sh passes on a plain Linux Docker Engine (Ubuntu 24.04 VM, Docker Engine 29.1, compose v2, host uid 1000, branch cloned natively from a bundle): the same job steps as CI, run there because Docker Desktop on macOS maps bind-mount ownership away and cannot reproduce the runner's failures. Three nodes exported, descriptors pulled as host-owned owner-only copies, chain config read by the nodes, provision and preflight ok: true, clean teardown. host.docker.internal verified absent from a node's /etc/hosts without the compose mapping and present (host gateway) with it.
  • CI on this PR is green, including custody-harness-smoke on the Linux amd64 runner (run 34150293722): three distinct DID-keyed descriptors, composition generated and verified, three Carrier dial proofs, provision and preflight ok: true, clean teardown.
  • Not run for this PR: the UI/entropy/command-audit steps of just verify; nothing here touches those surfaces.

Not proven here (unchanged, see TASKS.md)

Distinct seed/third-node hardware and genuinely distinct operators/failure domains (gates 3/4); the real Base deployment (an Anvil fork was used); the Brave UIUX path of the journey (gate 8).

SashaMIT and others added 30 commits September 2, 2026 22:16
New Homes start with nothing on the desktop: apps are in the dock and the
Apps face, content is reached through Library. Marketplace joins the default
dock pins after Library so the way to get more is one click away.

Only affects the first-run layout seed; saved layouts are untouched.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ox and Chat Room

The Home unified-sidebar chrome paints the traffic lights over the first
52px of the capsule frame. Library, Marketplace, System, People and
Services already reserve that band; Documents, Inbox and Chat Room did
not, so the lights sat on top of the first sidebar control (Documents'
New button overlapped outright).

Documents follows the Library convention (--window-chrome-safe-top, minus
its own 0.75rem page inset). Inbox and Chat Room use a capsule-local 52px,
matching System and the entropy gate's rule that those two keep
capsule-local top padding only.

Co-authored-by: Cursor <cursoragent@cursor.com>
…he column height

Co-authored-by: Cursor <cursoragent@cursor.com>
…ge clock

Brings the URUX lock face visual onto the Home host: ELASTOS wordmark,
wallpaper as a full-bleed ground with a vignette, date and clock at
their intended size, compact avatar with the name beneath.

Two defects in the existing face go away with it: the generic
`.home-unlock p` rule outranked `.home-unlock-date/-time` so the clock
rendered at 13px grey, and the picker-flow entrance animation left a
transform on the face, making it the containing block for the fixed
ground and clipping the wallpaper to a centred column. Date/time rules
are now scoped under .home-unlock and the entrance animates the
brand/clock/account instead of the face.

No behaviour change: shell-auth.js, the explicit-click passkey flow, and
the person/person-name ordering are as before; no account directory.

Co-authored-by: Cursor <cursoragent@cursor.com>
Brings the URUX Documents look onto the 0.7.1 Documents: flat 220px
sidebar that owns the traffic-light safe-top, compact round New button
with a pill search beside it, flat main pane, segmented Write/Split/Read
control, uniform toolbar sizing. No cards inside a padded page.

Only the <style> block changes. Markup and script are the 0.7.1 ones
(26 Aug refine, 30 Aug save-or-discard and preview fixes), so behaviour
is unchanged; the five newer classes (toolbar-group, workspace-toggle,
workspace-tab, find-actions, find-highlight) are styled to match.

Tokens keep the canonical shared-sheet mappings the entropy gate
requires; the URUX literal palette values are not carried over.

Co-authored-by: Cursor <cursoragent@cursor.com>
The toolbar "…" menu already carries New ▸ Folder / Text Document,
Upload Here, Paste, Sort By, View, Refresh, Show Hidden and Properties,
and the toolbar already hides the Sort select and Refresh button in its
favour. The New folder and Upload buttons now follow the same rule, so
the toolbar is navigation, breadcrumbs, search, view toggle and the menu.

One line in syncToolbarChrome; the buttons and their handlers remain for
the picker/attach flows and the existing tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ive reorder

The idle pill is width:max-content, which no browser can interpolate, so the
declared width transition never ran: the dock jumped on every pin, unpin,
launch and Apps open. Width changes are now a pixel FLIP (lock, reflow, ease,
unlock), with transitions suppressed on the lock flush so the base transition
cannot start and be reversed at 0% into an instant jump.

The Apps face reveal is height-led (420ms ease-out, opacity only guards the
first frames) and the pill's width eases in step, so the one Shelf-face state
reads as the pill growing up rather than a fade over a snapped box.

Dragging over the dock reorders live: a dock-sized spacer holds the slot, the
other pins FLIP around it, a dragged pin parks, and the pill eases as the slot
opens and closes. Nothing commits until drop; the never-silent-unpin rule and
the drop rules are unchanged. The smoke's fake style gains the two CSSOM
methods (setProperty/removeProperty) the shell uses for inline custom props.

Co-authored-by: Cursor <cursoragent@cursor.com>
The face no longer widens to a 720px popup: it locks to the dock's own width
(min 320, set in px at open because the idle width is max-content) and uses
denser 72px tiles, so it grows up out of the dock with nothing either side of
the dock row. Motion timing moves to tokens — --shelf-face-ms 950ms,
--shelf-face-ease ease-in-out, --dock-width-ms 780ms — the pill glides rather
than pops. Gate bounds for the phone face are unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
…k row

The launcher content rode up with the pill's top edge during the reveal
(block launcher, popover shrinking to the growing max-height), so icons
appeared to leave the dock and slide upward. The launcher is now a bottom-
justified column, the popover keeps its full height, and the grid sinks to the
dock row with an auto margin — the rising edge uncovers stationary icons first
and the header last, and scrolling stays reachable when the grid overflows.

Shelf pins live in the dock row directly under the face, so they are no longer
duplicated in the grid; the open grid refreshes when pins change.

Co-authored-by: Cursor <cursoragent@cursor.com>
… close

Closing the face snapped the pill back to --taskbar-h while the launcher was
still collapsing, so the dock row was pushed below the pill and rode back up —
the "icons leave the dock and slide up" defect. The pill now sizes to content
in both faces (min-height 72, symmetric padding), so it shrinks with the
launcher and the row never moves.

The reveal follows the Shelf cadence: the pill grows up as empty glass (height
only, width locked) and the content fades in where it lives at 82% of the
stretch (header, then grid); closing lets the content leave first (140ms), then
collapses. Grid returns to the top; the grid/row gap and tile density live in
rules that do not flip with the face class, so nothing reflows on the first
close frame.

Co-authored-by: Cursor <cursoragent@cursor.com>
… pill

The first taskbar render on load eased the pill width from the empty pill,
so every icon slid in from outside it. Width easing now only runs between two
populated docks.

For pin and running-set changes, icons already on the dock start where they
were and glide to their new slot in step with the pill's edges (per-slot FLIP,
same duration and ease), so none is ever outside the pill; an icon new to the
dock settles into its slot once the pill has made room.

Co-authored-by: Cursor <cursoragent@cursor.com>
Every first-party provider capsule now declares `"icon": "icons"` and ships
its own icon-32/64/128/256 set, exactly like app, viewer and content
capsules do. The catalog read model already published declared icon routes
for any role; two things kept them from reaching the Store:

- the gateway only served declared icon variants for content/data
  capsules. Providers never serve a browser document, so extend the same
  declared-only allowlist to the provider role. Manifest, rootfs and the
  capsule root stay 404.
- source-home install materialized only `capsule.json` for provider
  contracts. Copy the declared icon set beside it (all four sizes required,
  no traversal).

Marketplace and Services render the icons through the existing
`iconVariants` read model; no name→icon map, no provider names in app
source.

Tests: declared provider icons servable and nothing else; all eleven
service manifests validate, declare `icons`, and ship the four rasters.

Co-authored-by: Cursor <cursoragent@cursor.com>
… model attribution

The Home Agent capsule needs two things from the Runtime and nothing else.

Workspace: GET/PUT /api/apps/home-agent/workspace behind the capsule's launch
token. The document is an opaque, bounded JSON value (size and depth capped),
revisioned with if_revision so a stale writer gets 409 and never overwrites,
written as a principal-root protected object (ciphertext at rest, declared
for recovery) and isolated per principal. Mirrors the Assistant workspace
route.

Model proxy: home-agent joins the capsules allowed to call the typed model
contract (offers_list, runs_create, runs_events, runs_cancel). Runs are
bound and audited to the calling capsule id taken from the launch token's
executable actor, instead of the previous hardcoded "assistant".

Tests: absent-get is read-only at revision 0; round trip and restart preserve
the exact document; wrong capsule is 403 and principals stay isolated; stale
and future revisions fail without mutation; malformed requests fail closed;
the object is declared for recovery and stored as ciphertext; a home-agent
run binds and audits to "home-agent".

Co-authored-by: Cursor <cursoragent@cursor.com>
…ent Space

Home GUI owns the motion and the place, nothing else. The Shelf pill grows
into a composer face (FLIP in pixels — CSS cannot interpolate max-content →
fixed width), the Home surface breathes down and the Agent Space rises as a
stage the pager knows about. The room itself is the home-agent capsule in an
opaque-sandboxed frame, launched like the Wallet rail through launchHomeTarget.

The frame is reached only by postMessage with the sender pinned to the frame
window:
  Home → capsule   home-agent:open | home-agent:shelf-handover {on} | home-agent:close
  capsule → Home   home-agent:ready | home-agent:shelf-metrics {width,height,radius}
                   home-agent:close | home-agent:open-viewer
Home never reads the frame's DOM; the capsule never reaches Home's.

Co-authored-by: Cursor <cursoragent@cursor.com>
…model contract

The URUX agent harness — sessions, projects, transcript with thinking blocks
and progress, markdown and math, edit / delete / regenerate, follow-up queue,
attachments, dictation — lifted as the home-agent capsule with its visual
fidelity intact, and with everything the Runtime does not back removed.

Inference is the typed model contract or nothing. offers_list decides what
the model menu and Settings › Models show; a turn is runs_create with
elastos.model.input.text/v1 {prompt}, polled by runs_events after_sequence,
stopped with runs_cancel. No offer, provider or model is named in source.
When no offer is advertised the harness says so; it never answers for the
model. The model chosen in the menu is the one the run uses and it persists.

State the Runtime backs lives in the capsule's workspace object (revisioned,
principal-root protected): sessions, projects, prompt, notes, context budget,
thinking visibility, chosen offer, live usage ledger. No browser storage.

Removed because nothing behind them was real on this Runtime: the mock
provider and its seeded replies, tool grants and the Inbox grant cards
(library.read / web.search / wallet.sign routes do not exist here), the
approval-mode chip, Chat/Build segment, Workbench rail (outputs, plan, diff,
browser, terminal), Studio, Usage, model catalog with Get / Mine / hardware
estimate, Settings › Overview / Machine / Tools / Runtime / Permissions,
temperature and reasoning-effort controls (the typed input has no sampling
knobs), the preview banner and every "preview"/"mock" line of copy.

Also fixes session menu state that was undeclared after the capsule split
(opening a chat's "…" menu threw a ReferenceError).

Co-authored-by: Cursor <cursoragent@cursor.com>
…and the seam

node scripts/home-agent-shell-smoke.mjs, wired into `just verify`. Asserts in
source that no offer is named, no browser storage or provider ping is used,
no removed module or absent route is imported or called, no mock reply path
exists, the workspace client reads before it writes and reloads on 409, the
gateway route is principal-root protected and capsule-bound, index.html
carries no theatre surface or preview copy, Settings offers only what the
Runtime backs, and Home is reached by message only. Exercises the pure
model-contract module (offer eligibility, transcript prompt, run body,
after_sequence page reduction and its failure modes) directly.

Co-authored-by: Cursor <cursoragent@cursor.com>
irzhywau added a commit that referenced this pull request Sep 7, 2026
…iver, CI smoke

Simulation harness and proof machinery for the installed inactive e2e
proof (three custody replicas + repair-after-loss), all fail-closed and
evidence-receipted:

- static audit: slim `custody-host` role ({custody, availability, ipfs}
  parity + state-root/permission checks; no chain/composition demand) for
  standalone dKMS containers; full-home `custody-node` role untouched.
- deploy/custody-host: minimal 4-binary multi-stage image (rust builder,
  stripped binaries, busybox:glibc final, TARGETARCH-portable, image-as-
  source-of-truth resync at boot), POSIX-sh entrypoint (first-boot-only
  env provisioning, DID-keyed /shared descriptor exports, zero-env
  restarts), three-node docker-compose harness with hand-built Carrier
  tickets and resurrect-on-restart semantics. Simulation only — not a
  production deployment (see its README).
- kubo ships in the image too: ipfs-provider's replica plane hard-errors
  without it, and this repo has no Cargo path to a separate Go project,
  so the builder stage fetches a pinned, checksum-verified official
  release tarball (per-TARGETARCH, fail-closed on mismatch) and the
  runtime stage places it at a fixed path outside the state volume with
  `ELASTOS_IPFS_KUBO_PATH` baked in, keeping the zero-env restart
  contract intact.
- proof driver: provisioning, composition ceremony, Carrier dial-proof
  preflight, and the full journey/drill/negative/finalize phase set with
  trap-based harness restoration, secret-redacted transcripts, pinned
  fail-closed denial constants, and a hash-bound evidence receipt;
  bash-3.2 compatible with a smoke suite exercising extracted driver
  function bodies under stock /bin/bash. Hardened by the live run against
  a real Home: capsule HTTP calls send the opaque `Origin: null` the
  capsule browser plane requires; the mint publish fails fast on any
  non-pending-approval error instead of polling to timeout; the mint
  object write no longer forces `application/octet-stream` (which routed
  the source into encrypted-blob storage and fed ciphertext to media
  preparation); file data and HTTP bodies travel via stdin / `--data @file`
  rather than argv, so large sources do not trip ARG_MAX.
- proof driver, onboarding gate: the Runtime needs each principal's profile
  DID (mint reads the creator's, open reads the buyer's) and a profile can
  only exist on a recovery-ready root; both are one-time Home UI steps that
  only the passkey holder can perform (recovery export needs a passkey
  step-up). wallet-setup now reads each principal's People summary and
  fails closed with the exact System -> Recovery / People -> Profile actions
  instead of letting the mint fail later with the opaque "viewer release
  approval is unavailable".
- proof driver, mint body: copies/price are sent as canonical hex
  quantities like the Library app does (`decimalIntegerToHexQuantity`);
  the decimal strings the driver used to send are rejected by the mint
  intent as an invalid selection.
- proof driver, wallet approvals: approving a managed-wallet request
  (mint, buy, restart) requires a fresh passkey step-up bound to
  `wallet.approve` + `{request_id, reason}` from the same Wallet launch;
  only the passkey holder can produce it. New `--step-up-hook <cmd>`
  (`$ELASTOS_STEP_UP_HOOK`) delegates to `<cmd> <principal> <operation>
  <app-token>` with the canonical request on stdin; without it the driver
  fails closed naming the Wallet UI action.
- proof driver, settlement: the approval store outlives the mint journal,
  so the driver now approves only a pending request created after its own
  publish/buy began (newest wins), and after approval re-issues the
  idempotent call until the Wallet/Chain settlement clears (bounded)
  instead of a single second shot.
- proof driver, buy: like mint, only the pending-settlement answer enqueues
  a wallet approval; any other buy error now fails fast instead of polling
  approvals for the full timeout.
- proof driver, follow-up approvals: a pending settlement can raise further
  managed-wallet requests on the way (the creator tail adds the operative's
  operator approval for the market gateway after the mint lands), so the
  settle loop now approves any new pending request for the account raised
  since the call began, through the same step-up hook.
- proof driver, open: like buy, a managed buyer's viewer release raises a
  pending Wallet approval; the open phase now launches the buyer's System
  and Wallet apps, approves the request through the step-up hook and
  re-issues the identical open until the release settles.
- custody-host, chain plane: the slim role is {custody, availability, ipfs,
  chain} -- the image builds and ships chain-provider (components.json
  checksum + profile), the entrypoint syncs /shared/chain-provider.json into
  the private data root on every boot (fail closed with the remedy when
  absent) and hosts it with `--with chain-provider`; `up.sh` derives that
  file from the client's protected-content config with host-loopback RPC
  URLs rewritten to host.docker.internal (new `sync-chain-config`
  subcommand; `up` does it itself, falling back to the proof driver's
  placeholder network with a loud NOTE when the client has no chain config
  yet, which is the CI ceremony's order). Static audit: custody-host demands
  chain_config (never custody_composition); smoke updated accordingly.
- proof driver, open helper + drills: every buyer-side open (open, both
  drills, the negative cases, restart) goes through one helper that
  approves the managed-wallet viewer release request and re-issues the
  identical open, so each phase asserts the settled answer. drill-custody
  now asserts the product contract observed live: with ONE committee
  member stopped the 2-of-3 composition still serves the viewer (attempt
  1), and serves again on 3-of-3 after the node restarts with its DID and
  state intact (attempt 2); the earlier "attempt 1 fails closed" guess was
  never confirmed live and contradicts k-of-n -- below-quorum fail-closed
  stays with the negative phase (two nodes stopped). The first fresh
  ensure right after the loss is order-dependent (bounded candidate
  attempts can land on the dead node, and replication at mint is bounded
  to two node candidates, so the stopped node may have held one of the
  only two node replicas), so attempt 1 admits an availability fail-closed
  answer, repairs onto the surviving nodes with the node still stopped
  (`content repair-worker --force`, healthy status required) and the
  viewer must then serve on the 2-of-3 committee. read_viewer reads the
  init segment before segment 0 (media parts are released in order).
- proof driver, drill-replica: `elastos content status` echoes the latest
  STORED availability receipt (never a live probe), so demanding
  replicas<3 right after stopping a node asserted something the product
  never claims. The drill now captures that cached reading for the receipt
  and records the degraded buy/open: both re-`ensure` the CID live, and
  their outcome right after a single loss is order-dependent (the first
  ensure can spend a bounded candidate attempt on the dead node and come up
  short; the next, with it deprioritized, reaches local + two live replicas)
  -- only ok, the pending-settlement answer (the denial principal's wallet
  request is never approved) or the availability fail-closed message is
  admissible. The
  stable contract stays asserted: repair-worker heals, status reads
  healthy, open serves.
- proof driver, negative: the cross-principal read is refused before the
  session gate (read_viewer loads the caller's purchase first, so a
  non-purchasing principal is denied "before purchase", confirmed live);
  the assertion admits that message alongside the session one.
- proof driver, negative tamper: the target is the node's newest custody
  share (kubo names blocks by multihash, never by CID, so the CID path
  search matched nothing on the image), and the assertion follows k-of-n:
  a corrupt share makes that node answer BackendUnavailable and the
  release settles on the two intact nodes, so "served on 2-of-3" and the
  release fail-closed message are the admissible answers.
- proof driver, restart: the post-restart buy re-issue settles the
  purchase (bounded settle loop with fresh post-restart System/Wallet
  tokens) instead of demanding an immediate ok -- the approval survives
  the kill, but the exact Chain settlement it interrupted still has to
  finalize on both evidence sources (observed live).
- CI: `custody-harness-smoke` job (workflow_dispatch + PR paths filter,
  layer-cached) running the throwaway-project ceremony + preflight smoke.
- custody-host on a Linux host (first real amd64 CI run, PR #57): the
  containers run as uid 10001 and `shared/` is bind-mounted, and a bind
  mount keeps the host directory's owner and mode -- Docker Desktop on
  macOS maps ownership so the nodes could always write, a Linux runner
  does not, so the descriptor export failed on every boot and `up.sh`
  reported 0/3 descriptors with no container output to explain it. `up.sh
  up` now makes `shared/` world-writable with the sticky bit before the
  containers start; the entrypoint probes /shared for writability up front
  and fails closed with that remedy; the descriptor stays owner-only (the
  composition ceremony refuses anything else), so once a node is ready
  `up.sh` streams the descriptor out through an exec (`compose exec cat`) and
  replaces the node's uid-10001 copy with one this host user owns at 0600,
  readable on either host; the descriptor poll dumps `compose ps -a` and
  each node's last log lines before failing, and the CI smoke
  preserves the containers' state and full logs in the evidence directory
  BEFORE teardown so the uploaded artifact carries the cause. The smoke
  also marks its project as up BEFORE calling up.sh and reclaims this
  run's descriptors from the before-snapshot when up.sh fails after the
  nodes exported them: previously a failure inside up.sh skipped the
  teardown and left the containers and their descriptors behind, and the
  next run failed on "descriptor DIDs are not distinct".
@irzhywau
irzhywau force-pushed the feat/protected-content-installed-e2e-proof branch from 2f544c0 to aa5805c Compare September 7, 2026 16:01
irzhywau added a commit that referenced this pull request Sep 7, 2026
…iver, CI smoke

Simulation harness and proof machinery for the installed inactive e2e
proof (three custody replicas + repair-after-loss), all fail-closed and
evidence-receipted:

- static audit: slim `custody-host` role ({custody, availability, ipfs}
  parity + state-root/permission checks; no chain/composition demand) for
  standalone dKMS containers; full-home `custody-node` role untouched.
- deploy/custody-host: minimal 4-binary multi-stage image (rust builder,
  stripped binaries, busybox:glibc final, TARGETARCH-portable, image-as-
  source-of-truth resync at boot), POSIX-sh entrypoint (first-boot-only
  env provisioning, DID-keyed /shared descriptor exports, zero-env
  restarts), three-node docker-compose harness with hand-built Carrier
  tickets and resurrect-on-restart semantics. Simulation only — not a
  production deployment (see its README).
- kubo ships in the image too: ipfs-provider's replica plane hard-errors
  without it, and this repo has no Cargo path to a separate Go project,
  so the builder stage fetches a pinned, checksum-verified official
  release tarball (per-TARGETARCH, fail-closed on mismatch) and the
  runtime stage places it at a fixed path outside the state volume with
  `ELASTOS_IPFS_KUBO_PATH` baked in, keeping the zero-env restart
  contract intact.
- proof driver: provisioning, composition ceremony, Carrier dial-proof
  preflight, and the full journey/drill/negative/finalize phase set with
  trap-based harness restoration, secret-redacted transcripts, pinned
  fail-closed denial constants, and a hash-bound evidence receipt;
  bash-3.2 compatible with a smoke suite exercising extracted driver
  function bodies under stock /bin/bash. Hardened by the live run against
  a real Home: capsule HTTP calls send the opaque `Origin: null` the
  capsule browser plane requires; the mint publish fails fast on any
  non-pending-approval error instead of polling to timeout; the mint
  object write no longer forces `application/octet-stream` (which routed
  the source into encrypted-blob storage and fed ciphertext to media
  preparation); file data and HTTP bodies travel via stdin / `--data @file`
  rather than argv, so large sources do not trip ARG_MAX.
- proof driver, onboarding gate: the Runtime needs each principal's profile
  DID (mint reads the creator's, open reads the buyer's) and a profile can
  only exist on a recovery-ready root; both are one-time Home UI steps that
  only the passkey holder can perform (recovery export needs a passkey
  step-up). wallet-setup now reads each principal's People summary and
  fails closed with the exact System -> Recovery / People -> Profile actions
  instead of letting the mint fail later with the opaque "viewer release
  approval is unavailable".
- proof driver, mint body: copies/price are sent as canonical hex
  quantities like the Library app does (`decimalIntegerToHexQuantity`);
  the decimal strings the driver used to send are rejected by the mint
  intent as an invalid selection.
- proof driver, wallet approvals: approving a managed-wallet request
  (mint, buy, restart) requires a fresh passkey step-up bound to
  `wallet.approve` + `{request_id, reason}` from the same Wallet launch;
  only the passkey holder can produce it. New `--step-up-hook <cmd>`
  (`$ELASTOS_STEP_UP_HOOK`) delegates to `<cmd> <principal> <operation>
  <app-token>` with the canonical request on stdin; without it the driver
  fails closed naming the Wallet UI action.
- proof driver, settlement: the approval store outlives the mint journal,
  so the driver now approves only a pending request created after its own
  publish/buy began (newest wins), and after approval re-issues the
  idempotent call until the Wallet/Chain settlement clears (bounded)
  instead of a single second shot.
- proof driver, buy: like mint, only the pending-settlement answer enqueues
  a wallet approval; any other buy error now fails fast instead of polling
  approvals for the full timeout.
- proof driver, follow-up approvals: a pending settlement can raise further
  managed-wallet requests on the way (the creator tail adds the operative's
  operator approval for the market gateway after the mint lands), so the
  settle loop now approves any new pending request for the account raised
  since the call began, through the same step-up hook.
- proof driver, open: like buy, a managed buyer's viewer release raises a
  pending Wallet approval; the open phase now launches the buyer's System
  and Wallet apps, approves the request through the step-up hook and
  re-issues the identical open until the release settles.
- custody-host, chain plane: the slim role is {custody, availability, ipfs,
  chain} -- the image builds and ships chain-provider (components.json
  checksum + profile), the entrypoint syncs /shared/chain-provider.json into
  the private data root on every boot (fail closed with the remedy when
  absent) and hosts it with `--with chain-provider`; `up.sh` derives that
  file from the client's protected-content config with host-loopback RPC
  URLs rewritten to host.docker.internal (new `sync-chain-config`
  subcommand; `up` does it itself, falling back to the proof driver's
  placeholder network with a loud NOTE when the client has no chain config
  yet, which is the CI ceremony's order). Static audit: custody-host demands
  chain_config (never custody_composition); smoke updated accordingly.
- proof driver, open helper + drills: every buyer-side open (open, both
  drills, the negative cases, restart) goes through one helper that
  approves the managed-wallet viewer release request and re-issues the
  identical open, so each phase asserts the settled answer. drill-custody
  now asserts the product contract observed live: with ONE committee
  member stopped the 2-of-3 composition still serves the viewer (attempt
  1), and serves again on 3-of-3 after the node restarts with its DID and
  state intact (attempt 2); the earlier "attempt 1 fails closed" guess was
  never confirmed live and contradicts k-of-n -- below-quorum fail-closed
  stays with the negative phase (two nodes stopped). The first fresh
  ensure right after the loss is order-dependent (bounded candidate
  attempts can land on the dead node, and replication at mint is bounded
  to two node candidates, so the stopped node may have held one of the
  only two node replicas), so attempt 1 admits an availability fail-closed
  answer, repairs onto the surviving nodes with the node still stopped
  (`content repair-worker --force`, healthy status required) and the
  viewer must then serve on the 2-of-3 committee. read_viewer reads the
  init segment before segment 0 (media parts are released in order).
- proof driver, drill-replica: `elastos content status` echoes the latest
  STORED availability receipt (never a live probe), so demanding
  replicas<3 right after stopping a node asserted something the product
  never claims. The drill now captures that cached reading for the receipt
  and records the degraded buy/open: both re-`ensure` the CID live, and
  their outcome right after a single loss is order-dependent (the first
  ensure can spend a bounded candidate attempt on the dead node and come up
  short; the next, with it deprioritized, reaches local + two live replicas)
  -- only ok, the pending-settlement answer (the denial principal's wallet
  request is never approved) or the availability fail-closed message is
  admissible. The
  stable contract stays asserted: repair-worker heals, status reads
  healthy, open serves.
- proof driver, negative: the cross-principal read is refused before the
  session gate (read_viewer loads the caller's purchase first, so a
  non-purchasing principal is denied "before purchase", confirmed live);
  the assertion admits that message alongside the session one.
- proof driver, negative tamper: the target is the node's newest custody
  share (kubo names blocks by multihash, never by CID, so the CID path
  search matched nothing on the image), and the assertion follows k-of-n:
  a corrupt share makes that node answer BackendUnavailable and the
  release settles on the two intact nodes, so "served on 2-of-3" and the
  release fail-closed message are the admissible answers.
- proof driver, restart: the post-restart buy re-issue settles the
  purchase (bounded settle loop with fresh post-restart System/Wallet
  tokens) instead of demanding an immediate ok -- the approval survives
  the kill, but the exact Chain settlement it interrupted still has to
  finalize on both evidence sources (observed live).
- CI: `custody-harness-smoke` job (workflow_dispatch + PR paths filter,
  layer-cached) running the throwaway-project ceremony + preflight smoke.
- custody-host on a Linux host (first real amd64 CI run, PR #57): the
  containers run as uid 10001 and `shared/` is bind-mounted, and a bind
  mount keeps the host directory's owner and mode -- Docker Desktop on
  macOS maps ownership so the nodes could always write, a Linux runner
  does not, so the descriptor export failed on every boot and `up.sh`
  reported 0/3 descriptors with no container output to explain it. `up.sh
  up` now makes `shared/` world-writable with the sticky bit before the
  containers start; the entrypoint probes /shared for writability up front
  and fails closed with that remedy; the descriptor stays owner-only (the
  composition ceremony refuses anything else), so once a node is ready
  `up.sh` streams the descriptor out through an exec (`compose exec cat`) and
  replaces the node's uid-10001 copy with one this host user owns at 0600,
  readable on either host; the same rule the other way: `shared/
  chain-provider.json` (network ids, contract addresses, RPC URLs) is
  written 0644 so the nodes' user can read it, and the entrypoint checks
  that readability up front with a remedy (the second Linux run died on
  cp's bare "Permission denied", captured by the new evidence upload);
  the compose services map `host.docker.internal` to the engine's
  `host-gateway`, which only Docker Desktop resolves natively (the rewritten
  RPC URLs point there, and a Linux engine would fail every RPC dial);
  the descriptor poll dumps `compose ps -a` and
  each node's last log lines before failing, and the CI smoke
  preserves the containers' state and full logs in the evidence directory
  BEFORE teardown so the uploaded artifact carries the cause. The smoke
  also marks its project as up BEFORE calling up.sh and reclaims this
  run's descriptors from the before-snapshot when up.sh fails after the
  nodes exported them: previously a failure inside up.sh skipped the
  teardown and left the containers and their descriptors behind, and the
  next run failed on "descriptor DIDs are not distinct".
@irzhywau
irzhywau force-pushed the feat/protected-content-installed-e2e-proof branch 2 times, most recently from 745fc33 to f9b0ad0 Compare September 7, 2026 18:38
CI lint on PR #58 failed under Rust 1.91 clippy with -D warnings:
extracted_bundle_cache_stale_reason guarded with
`if extract_path.is_none() { return None; }`, which the
question_mark lint rewrites as `extract_path.as_ref()?;`.
Same behaviour, lint-clean.
0.7.1 integration: protected-content provisioning, Home first-run seed, Home Agent on the typed model contract
@irzhywau
irzhywau deleted the branch feat/protected-content-installed-provisioning September 8, 2026 13:55
@irzhywau irzhywau closed this Sep 8, 2026
@irzhywau irzhywau reopened this Sep 8, 2026
This commit introduces a new example capsule named `hello`, demonstrating a portable Rust component for the ElastOS Runtime. The following files were added:

- `.gitignore`: Excludes build artifacts and target directory.
- `build.sh`: Script to build the capsule.
- `capsule.json`: Manifest defining the capsule's properties.
- `Cargo.toml` and `Cargo.lock`: Configuration for Rust dependencies.
- `README.md`: Documentation on how to build and run the capsule.
- `verify.sh`: Script for verifying the capsule's integrity and compliance.
- `src/lib.rs`: Contains the core logic of the capsule.

This example serves as a standalone demonstration of how to create and run a Rust-based capsule within the ElastOS environment.
…fmpeg media provider

Product changes backing the installed custody topology and the live
runtime-custody mint path:

- carrier: seed the Carrier address book from the operator peer store at
  startup (debug per peer, warn-and-skip on malformed entries), so
  operator-added peers are dialable without runtime intervention.
- run: `elastos run <provider> [--with <provider>]... [--carrier-addr]`
  hosts native providers standalone on the runtime core planes
  (Carrier + registry + custody/content planes extracted additively from
  the full profile; gateway/serve behavior untouched), with a readiness
  receipt at <data_dir>/run/provider-host.ready.json.
- protected-content: characterization test pinning release resume
  semantics under single node loss (ExactResume skips dead nodes,
  persisted decisions survive; threshold stays data).
- protected-content-config: contract-address validation accepts EIP-55
  mixed-case input and stores canonical lowercase (downstream comparisons
  were already case-insensitive; the validator was stricter than its
  consumers).
- media-provider: defects that only surface against the real Runtime and
  real ffmpeg, all masked by the mock-provider tests. (1) The Runtime
  attaches an 11-field `_runtime_invocation` envelope (including the
  `abi` transfer contract) but the provider still required 10, rejecting
  every real `prepare` as `invalid_request`; validate against the exact
  local JSON transfer ABI, mirroring protected-content-decrypt-provider.
  (2) ffmpeg's DASH muxer has no `start_number` option and numbers media
  segments from 1 while the Runtime contract is 0-based; drop the ignored
  flag and shift a contiguous 1-based run to 0-based (gapped or offset
  runs stay rejected). (3) The DASH muxer prefixes every segment with
  `styp` + `sidx`, and `-streaming 1` fragments per frame, while the
  Runtime contract is exactly one `moof` + `mdat` per segment; switch to
  `-streaming 0` and strip the optional prefix boxes (the `sidx` byte
  ranges go stale after protection anyway), rejecting multi-fragment or
  trailing-box segments. Process tests now send the real envelope and
  cover 1-based normalization, prefix stripping, gapped/offset/multi-
  fragment rejection, and wrong/missing ABI; the success-path test budget
  is raised because a bare `sh` mock has been observed to take seconds
  under the parallel suite on a loaded host.
- protected-content runtime: media preparation copied the creator's source
  object raw from disk, but once recovery is activated the principal root
  is encrypted and every object is an `elastos.principal-root.object/v1`
  envelope, so ffprobe received ciphertext and every mint from an onboarded
  (recovery-ready) creator failed. Read the source once through
  `read_principal_root_object` (decrypts a protected root, passes an
  unprotected one through), key the preparation record on the plaintext
  digest, and stage those bytes for the provider; test added for a
  protected root, fixtures now use object URIs under the principal's root.
- carrier: the content availability plane only picked replication
  candidates from signed announcements on the per-CID gossip topic, so a
  freshly published CID (which nobody has announced yet) never reached the
  custody nodes' content-replica receive plane and every mint failed
  closed with 1 local replica / no live multi-peer proof. Peers registered
  through `elastos node peer add` are now offered as candidates behind any
  announced holder (base score 40 vs 50, same reputation adjustment,
  self/duplicates skipped); an unreachable peer still fails its invocation
  and is recorded as a replication error and reputation failure. Test
  added: ensure replicates to an operator peer with zero announcements.
- content/protected-content runtime: the mint verified the signed
  availability receipt against `protected-content-replication/v1` but the
  publish request never named a policy, so the content plane recorded its
  `network_default` and every otherwise-healthy receipt (3 replicas, live
  multi-peer proof) failed the binding check. `ContentPublishRequirements`
  now carries an optional availability policy that travels with the
  publish request as `availability_policy`; the mint sets it from the
  requirement. The freshness check sampled `now` before the publish, so a
  replication round that legitimately takes tens of seconds put the
  receipt's `checked_at` past the 5 s future-skew allowance and every
  healthy receipt was rejected as "outside its freshness window"; the
  verifier now samples the clock after the receipt is fetched. The
  swallowed verifier error is logged at warn with the mint id so an
  operator can see why availability failed closed.
- gateway creator tail: the 40 fail-closed sites behind "creator mint is
  unavailable" now log the underlying error and source line at warn, the
  same way the availability verifier does, so a live failure is locatable.
  A chain-provider `*_pending` answer while resolving the mint receipt or
  the verified listing (receipt not yet finalized on enough evidence
  sources) is the Chain half of "pending exact Wallet or Chain settlement"
  and now surfaces as that pending state for callers to re-poll, instead
  of failing closed the moment the transaction lands.
  The buyer side gets the same treatment: the fail-closed sites behind
  "purchase is denied before buy" log the cause and source line at warn.
- protected-content runtime, pre-buy/open verification: "fresh"
  availability re-read the receipt stored at mint time and applied the
  60 s freshness window, so every purchase made more than a minute after
  the mint was denied. The verifier now asks the content plane to `ensure`
  the CID again under the listing's exact policy/object/publisher bindings
  (local pin + Carrier replica proofs, new signed receipt, nothing
  republished) and verifies that receipt; the pre-buy test asserts the
  ensure and its bindings.
- creator tail, operator approval: the mint transaction never grants the
  market gateway ERC-1155 operator rights on the ledger, so every purchase
  of a runtime-minted listing reverted with
  `ERC1155MissingApprovalForAll(gateway, creator)` and surfaced as
  "purchase is unavailable". The copies do not live on the asset ledger:
  the gateway resolves an operative (per-asset ERC-1155) from
  `(ledger, token_id)` and `buyAccess` transfers from there, so an approval
  on the ledger changes nothing (confirmed with a `cast call --trace` on
  the fork). After the mint settles the creator tail now resolves the
  finalized mint receipt first (token id + operative), asks the
  chain-provider for the market gateway (new
  `describe_protected_content_market_source` op), checks
  `isApprovedForAll(creator, gateway)` on that operative, and when it is
  missing raises `setApprovalForAll(gateway, true)` on the operative as a
  second exact wallet transaction effect (journaled as the creator state's
  `operator_approval`, same approval/settlement/pending semantics as the
  mint) before the listing is verified. The mock chain answers the probe
  only for the operative, and the test pins the approval's `to`. Purchase
  fail-closed sites log their cause at warn like the creator tail does.
- diagnosability: the stable `library_error` messages ("purchase is
  unavailable", "creator mint is unavailable", ...) stay the wire contract,
  but the error envelope now carries a `detail` field with the anyhow
  cause chain (source `file:line` of the fail-closed site plus the
  underlying provider error, joined by " <- ", capped at 1 KiB), built by
  `provider_error_from`; the fail-closed macros, the availability/policy
  errors and the ~35 viewer release sites ("viewer release approval is
  unavailable", previously `map_err(|_| ...)` dropping every cause)
  attach that chain instead of flattening to a string.
  chain-provider `evm_rpc` reports the JSON-RPC error code, message and
  revert `data` (truncated) as `upstream_rpc_error` instead of a bare
  "EVM RPC request rejected", so an estimate-gas revert is readable from
  the Runtime side. Debug tracing added at the workflow checkpoints:
  publish (media protected, rights policy resolved, custody provisioned,
  availability verified), creator tail (chain plan resolved, wallet effect
  ensured, operator approval probed, pending bails) and buy (fresh
  availability verified). Unit tests cover the stable-message/detail
  contract.
- open, managed accounts (viewer release approval): the open path was
  one-shot and expected the Wallet to return the rights signature inline,
  but a managed buyer account answers `RequestProtectedContentRightsSignature`
  with a pending approval (step-up bound) and only replays its stored
  result for the byte-identical request once approved. Every real open
  therefore failed closed as "viewer release approval is unavailable",
  leaving an orphaned pending approval per attempt (three found in the
  live wallet store). The runtime now models the pending release: the
  first pending answer persists the exact wallet request plus the
  prepared recipient's public identity on the open-pending viewer record
  (the decrypt provider keeps the key under the session handle;
  `RuntimePreparedRecipient::from_persisted_parts` restores the public
  side) and answers the new stable
  "Runtime custody viewer release is pending exact Wallet approval"; a
  re-issued open for the same runtime session resumes that record and
  replays the identical request instead of re-preparing. The coordinator's
  `validate_wallet_rights_signature` (and the Runtime-side decoder) now
  accept the Wallet's approval envelope (`signed_result` under
  `requires_approval: false`) as well as the bare result; the two
  contracts had only ever been tested against their own mocks. Tests:
  managed wallet mock (pending -> exact replay pending -> foreign
  recipient refused -> approved replay signed), envelope decoder.
- provider host, chain plane: a custody committee member settles every
  release through its own chain rights evidence (`evaluate` -> prepare ->
  `chain.protected_content_rights_evidence` -> settle), so a standalone host
  serving `custody` without `chain` fails every release closed
  (`ProviderEffectUncertain`, every peer answering "chain provider
  protected_content_rights_evidence invocation failed"). `elastos run`
  gains `ProviderPlane::Chain` (`--with chain-provider`): prerequisite is a
  valid `protected-content/chain-provider.json` naming a
  protected_content_network (fail closed with the remedy), registration
  is the extracted `server_infra::register_chain_provider_plane` (spawn,
  apply the protected config, register `elastos://chain`), which unlike the
  full Home profile refuses to start without the configuration. On a host
  with provisioned custody state the plane verifies signed Runtime
  operations against the state root's trusted Runtime issuer (the client
  that signs them; new `custody_provider::load_trusted_runtime_issuer`
  reads only that file), never the host's own device key -- the first
  live attempt had every node answer "signed Runtime operation is
  invalid" for exactly that reason. The open path's release fall-through
  arms now carry the coordinator outcome in the error detail instead of
  discarding it, and the node-side JSON provider invocation helper keeps
  the provider's error code and message (it is the only trace of why a
  committee member refused, and it travels back over Carrier).
- chain-provider, plain-http allowlist: the admission rule for RPC URLs
  stays https-or-loopback by default, but a network may now name bare
  hostnames in `plain_http_rpc_hosts` that it is allowed to reach over
  plain http (exact, case-insensitive host match, any port; entries are
  validated as bare hostnames so an entry can never widen the rule beyond
  the host it names). This is what lets a custody node inside a container
  settle releases against the client's local fork through the host gateway
  (`host.docker.internal`) without the simulation punching a hole in the
  product default. Unit tests cover the rule and the entry validation.
- chain-provider, rights-evidence corroboration: the multi-source check
  demanded that every evidence source report the identical finalized head,
  which no two independent providers guarantee (they lag each other by a
  block) and which a local fork with a block ticker fails on back-to-back
  calls -- live, a committee member refused a release with
  "conflicting_rights_observations" purely on timing. When heads differ the
  provider now pins every source to the lowest finalized block (finality is
  monotone, so it is final everywhere), requires that block to be canonical
  on each source under the pinned hash, and re-evaluates the rights call at
  exactly that block; corroboration compares the same finalized state.
  Heads that already agree take the unchanged single pass. Test: sources
  one block apart corroborate at the lower block.
- carrier, replica placement after a node loss: `ensure` took exactly
  `desired - local` remote candidates from the ordered pool (announced
  holders first, operator-store peers behind them), so a dead announced
  holder kept occupying one of the two placement slots and the
  never-attempted third peer was unreachable on every pass -- three forced
  repair passes with one node stopped left the receipt local-only (live,
  2026-09-06). The placement count is now the number of proven remote
  replicas the ensure seeks; a failed candidate no longer consumes a slot,
  the ordered pool is walked until the placements are proven, and the
  abuse guard is the dial cap (MAX_CARRIER_REPLICATION_CANDIDATES);
  `attempt_limit` in the receipt keeps meaning the placement target.
  Test: a dead announced holder ahead of a live operator peer, shortfall
  one, proves the replica on the peer (attempted 2, failed 1).
- carrier, receipt freshness after a node loss: the availability
  observation stamped `checked_at` when the ensure STARTED, so an ensure
  that walks a dead candidate's connect timeouts and still proves the
  replicas on the survivors returned a good receipt already outside the
  60 s freshness window, and the viewer failed closed on availability
  right after a successful repair (live, 2026-09-06). The observation is
  now stamped when its last proof completes, and the pre-buy/open verifier
  compares it against a clock re-sampled after the refresh (the caller's
  earlier sample made a completed receipt read as "in the future"); the
  mint publish path already sampled after the fetch.
- provider bridge, pending raw requests: `send_raw` has no timeout (media
  preparation and custody rounds legitimately run long) and holds the
  bridge's I/O lock until the provider answers, so a provider that never
  answers wedged every later call to it with nothing in the log. The bridge
  now names each raw request (`op`, lock wait) at debug, logs its settlement
  and elapsed time, and warns every 30 s while a request is still pending
  -- no behaviour change, only the missing trace.
- purchase settlement diagnosability: the buy stage folded "approval not
  completed", "no Chain receipt yet" and "completion pending (projection or
  audit)" into one silent pending answer and discarded the completion
  error; it now names each at debug (effect id, hash, receipt/pending/
  error/already-confirmed), a non-pending completion failure carries its
  status and message in `detail`, and the transaction effect reconcile
  traces the state it reconciles from and warns with the exact validation
  error when a Chain result is rejected.
- transaction effects, completion across a Runtime restart: the validated
  Chain outcome was projected to the Wallet under the CALLER's authority,
  but the Wallet binds an approval to the exact launch that raised it
  (principal, session, proof binding, grant, actor, launch); after a
  restart the completing caller holds a fresh launch, the Wallet answered
  "validated Chain outcome authority does not match the approval", and the
  purchase stayed "pending exact Wallet or Chain settlement" forever with
  the transaction mined and finalized (the restart drill's exact case,
  live 2026-09-07). The projection now runs under the effect's persisted
  authority binding (`RuntimeWalletAuthority::from_persisted_transaction_authority`);
  the mock wallet now binds approvals and outcome attachment on the same
  six authority fields as the real wallet, and a dedicated gateway test
  (purchase approved under one launch, completed by a re-issued launch)
  fails without the fix; the two browser fixtures that seeded completed
  approvals with placeholder bindings now derive the real one.
- protected-content-provider-contracts: the clear fMP4 validator rejected
  any child box inside the `avc1`/`mp4a` sample entry, but a real entry
  must carry its codec configuration (`avcC` is mandatory for AVC; ffmpeg
  also emits `colr`; AAC needs `esds`), so no real init segment could ever
  be protected. Accept codec-configuration children and reject only an
  entry that already carries `sinf`; the protect rewrite keeps them and
  appends `sinf` after them, and the clear rewrite restores the original
  bytes exactly (round-trip test added).
- chain-provider: the Runtime provider plane attaches a `_runtime_invocation`
  envelope to every forwarded request, but the chain provider's typed
  request enum rejects unknown fields and it had no envelope handling, so
  every Runtime-plane chain call (rights policy, creator mint, purchase
  access, rights evidence) failed with `unknown field _runtime_invocation`
  and the Runtime surfaced it as "rights policy is unavailable". Decode
  frames through a validating step that checks the envelope for this hop
  (target, op, local JSON transport and exact transfer ABI; source is not
  pinned because Home capsules also call the provider), strips it, and
  refuses a predeclared `_runtime_transfer`; envelope-less frames still
  decode for tooling and tests.
- model-provider: stop the artifact-redirect status test flaking under
  parallel `test-capsules` load by queuing benign "running" follow-ups so
  an extra 25ms poll is absorbed. Test-only.
…iver, CI smoke

Simulation harness and proof machinery for the installed inactive e2e
proof (three custody replicas + repair-after-loss), all fail-closed and
evidence-receipted:

- static audit: slim `custody-host` role ({custody, availability, ipfs}
  parity + state-root/permission checks; no chain/composition demand) for
  standalone dKMS containers; full-home `custody-node` role untouched.
- deploy/custody-host: minimal 4-binary multi-stage image (rust builder,
  stripped binaries, busybox:glibc final, TARGETARCH-portable, image-as-
  source-of-truth resync at boot), POSIX-sh entrypoint (first-boot-only
  env provisioning, DID-keyed /shared descriptor exports, zero-env
  restarts), three-node docker-compose harness with hand-built Carrier
  tickets and resurrect-on-restart semantics. Simulation only — not a
  production deployment (see its README).
- kubo ships in the image too: ipfs-provider's replica plane hard-errors
  without it, and this repo has no Cargo path to a separate Go project,
  so the builder stage fetches a pinned, checksum-verified official
  release tarball (per-TARGETARCH, fail-closed on mismatch) and the
  runtime stage places it at a fixed path outside the state volume with
  `ELASTOS_IPFS_KUBO_PATH` baked in, keeping the zero-env restart
  contract intact.
- proof driver: provisioning, composition ceremony, Carrier dial-proof
  preflight, and the full journey/drill/negative/finalize phase set with
  trap-based harness restoration, secret-redacted transcripts, pinned
  fail-closed denial constants, and a hash-bound evidence receipt;
  bash-3.2 compatible with a smoke suite exercising extracted driver
  function bodies under stock /bin/bash. Hardened by the live run against
  a real Home: capsule HTTP calls send the opaque `Origin: null` the
  capsule browser plane requires; the mint publish fails fast on any
  non-pending-approval error instead of polling to timeout; the mint
  object write no longer forces `application/octet-stream` (which routed
  the source into encrypted-blob storage and fed ciphertext to media
  preparation); file data and HTTP bodies travel via stdin / `--data @file`
  rather than argv, so large sources do not trip ARG_MAX.
- proof driver, onboarding gate: the Runtime needs each principal's profile
  DID (mint reads the creator's, open reads the buyer's) and a profile can
  only exist on a recovery-ready root; both are one-time Home UI steps that
  only the passkey holder can perform (recovery export needs a passkey
  step-up). wallet-setup now reads each principal's People summary and
  fails closed with the exact System -> Recovery / People -> Profile actions
  instead of letting the mint fail later with the opaque "viewer release
  approval is unavailable".
- proof driver, mint body: copies/price are sent as canonical hex
  quantities like the Library app does (`decimalIntegerToHexQuantity`);
  the decimal strings the driver used to send are rejected by the mint
  intent as an invalid selection.
- proof driver, wallet approvals: approving a managed-wallet request
  (mint, buy, restart) requires a fresh passkey step-up bound to
  `wallet.approve` + `{request_id, reason}` from the same Wallet launch;
  only the passkey holder can produce it. New `--step-up-hook <cmd>`
  (`$ELASTOS_STEP_UP_HOOK`) delegates to `<cmd> <principal> <operation>
  <app-token>` with the canonical request on stdin; without it the driver
  fails closed naming the Wallet UI action.
- proof driver, settlement: the approval store outlives the mint journal,
  so the driver now approves only a pending request created after its own
  publish/buy began (newest wins), and after approval re-issues the
  idempotent call until the Wallet/Chain settlement clears (bounded)
  instead of a single second shot.
- proof driver, buy: like mint, only the pending-settlement answer enqueues
  a wallet approval; any other buy error now fails fast instead of polling
  approvals for the full timeout.
- proof driver, follow-up approvals: a pending settlement can raise further
  managed-wallet requests on the way (the creator tail adds the operative's
  operator approval for the market gateway after the mint lands), so the
  settle loop now approves any new pending request for the account raised
  since the call began, through the same step-up hook.
- proof driver, open: like buy, a managed buyer's viewer release raises a
  pending Wallet approval; the open phase now launches the buyer's System
  and Wallet apps, approves the request through the step-up hook and
  re-issues the identical open until the release settles.
- custody-host, chain plane: the slim role is {custody, availability, ipfs,
  chain} -- the image builds and ships chain-provider (components.json
  checksum + profile), the entrypoint syncs /shared/chain-provider.json into
  the private data root on every boot (fail closed with the remedy when
  absent) and hosts it with `--with chain-provider`; `up.sh` derives that
  file from the client's protected-content config with host-loopback RPC
  URLs rewritten to host.docker.internal (new `sync-chain-config`
  subcommand; `up` does it itself, falling back to the proof driver's
  placeholder network with a loud NOTE when the client has no chain config
  yet, which is the CI ceremony's order). Static audit: custody-host demands
  chain_config (never custody_composition); smoke updated accordingly.
- proof driver, open helper + drills: every buyer-side open (open, both
  drills, the negative cases, restart) goes through one helper that
  approves the managed-wallet viewer release request and re-issues the
  identical open, so each phase asserts the settled answer. drill-custody
  now asserts the product contract observed live: with ONE committee
  member stopped the 2-of-3 composition still serves the viewer (attempt
  1), and serves again on 3-of-3 after the node restarts with its DID and
  state intact (attempt 2); the earlier "attempt 1 fails closed" guess was
  never confirmed live and contradicts k-of-n -- below-quorum fail-closed
  stays with the negative phase (two nodes stopped). The first fresh
  ensure right after the loss is order-dependent (bounded candidate
  attempts can land on the dead node, and replication at mint is bounded
  to two node candidates, so the stopped node may have held one of the
  only two node replicas), so attempt 1 admits an availability fail-closed
  answer, repairs onto the surviving nodes with the node still stopped
  (`content repair-worker --force`, healthy status required) and the
  viewer must then serve on the 2-of-3 committee. read_viewer reads the
  init segment before segment 0 (media parts are released in order).
- proof driver, drill-replica: `elastos content status` echoes the latest
  STORED availability receipt (never a live probe), so demanding
  replicas<3 right after stopping a node asserted something the product
  never claims. The drill now captures that cached reading for the receipt
  and records the degraded buy/open: both re-`ensure` the CID live, and
  their outcome right after a single loss is order-dependent (the first
  ensure can spend a bounded candidate attempt on the dead node and come up
  short; the next, with it deprioritized, reaches local + two live replicas)
  -- only ok, the pending-settlement answer (the denial principal's wallet
  request is never approved) or the availability fail-closed message is
  admissible. The
  stable contract stays asserted: repair-worker heals, status reads
  healthy, open serves.
- proof driver, negative: the cross-principal read is refused before the
  session gate (read_viewer loads the caller's purchase first, so a
  non-purchasing principal is denied "before purchase", confirmed live);
  the assertion admits that message alongside the session one.
- proof driver, negative tamper: the target is the node's newest custody
  share (kubo names blocks by multihash, never by CID, so the CID path
  search matched nothing on the image), and the assertion follows k-of-n:
  a corrupt share makes that node answer BackendUnavailable and the
  release settles on the two intact nodes, so "served on 2-of-3" and the
  release fail-closed message are the admissible answers.
- proof driver, restart: the post-restart buy re-issue settles the
  purchase (bounded settle loop with fresh post-restart System/Wallet
  tokens) instead of demanding an immediate ok -- the approval survives
  the kill, but the exact Chain settlement it interrupted still has to
  finalize on both evidence sources (observed live).
- CI: `custody-harness-smoke` job (workflow_dispatch + PR paths filter,
  layer-cached) running the throwaway-project ceremony + preflight smoke.
- custody-host on a Linux host (first real amd64 CI run, PR #57): the
  containers run as uid 10001 and `shared/` is bind-mounted, and a bind
  mount keeps the host directory's owner and mode -- Docker Desktop on
  macOS maps ownership so the nodes could always write, a Linux runner
  does not, so the descriptor export failed on every boot and `up.sh`
  reported 0/3 descriptors with no container output to explain it. `up.sh
  up` now makes `shared/` world-writable with the sticky bit before the
  containers start; the entrypoint probes /shared for writability up front
  and fails closed with that remedy; the descriptor stays owner-only (the
  composition ceremony refuses anything else), so once a node is ready
  `up.sh` streams the descriptor out through an exec (`compose exec cat`) and
  replaces the node's uid-10001 copy with one this host user owns at 0600,
  readable on either host; the same rule the other way: `shared/
  chain-provider.json` (network ids, contract addresses, RPC URLs) is
  written 0644 so the nodes' user can read it, and the entrypoint checks
  that readability up front with a remedy (the second Linux run died on
  cp's bare "Permission denied", captured by the new evidence upload);
  the compose services map `host.docker.internal` to the engine's
  `host-gateway`, which only Docker Desktop resolves natively (the rewritten
  RPC URLs point there, and a Linux engine would fail every RPC dial);
  the descriptor poll dumps `compose ps -a` and
  each node's last log lines before failing, and the CI smoke
  preserves the containers' state and full logs in the evidence directory
  BEFORE teardown so the uploaded artifact carries the cause. The smoke
  also marks its project as up BEFORE calling up.sh and reclaims this
  run's descriptors from the before-snapshot when up.sh fails after the
  nodes exported them: previously a failure inside up.sh skipped the
  teardown and left the containers and their descriptors behind, and the
  next run failed on "descriptor DIDs are not distinct".
… state

- docs/PROTECTED_CONTENT.md: standalone provider-host run mode, the
  container-simulated three-node topology and operator flow; remaining
  work updated with what the live headless proof now covers and what
  still needs distinct hardware, the real Base deployment, or Brave.
- scripts/README.md: proof driver, its smoke, the CI harness smoke, and
  the deploy/custody-host pointer.
- state.md: proof-path ledger entry; contract truth records the verified
  harness surface (resurrection, dial proofs, ceremony, preflight, local
  CI smoke) and the live installed journey verified 2026-09-05..07 on the
  installed client against an Anvil fork of Base with three custody-host
  containers (finalize overall_ok: true, every required phase ok), the
  product contracts the driver now asserts, the chain plane every custody
  member needs, and what remains unverified (distinct hardware/operators,
  real Base, Brave UIUX, amd64 CI run).
- TASKS.md: gates 3/4/6/7 annotated to the verified boundary, none
  checked beyond evidence; gate 6 replica/repair proof and gate 7 headless
  journey recorded as proven on the simulation-only harness.
@irzhywau
irzhywau force-pushed the feat/protected-content-installed-e2e-proof branch from f9b0ad0 to 617796a Compare September 8, 2026 14:09
@irzhywau

irzhywau commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Superseded. This PR is pinned to feat/protected-content-installed-provisioning, which merged in #52, and GitHub refuses to retarget it because it is part of a stack. The branch has been rebased onto upstream/0.7.1-dev (f9b0ad05 -> 617796a9, same four commits) and a replacement PR opens against the correct base. No review comments existed here, so nothing is lost.

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.

3 participants