Skip to content

dweb: installed dwapp code is mutable with no re-verification — add an explicit fork state + grant rotation #316

Description

@NotASithLord

Problem

A dwapp's dweb.version_id is a provenance claim, not an integrity binding. Three concrete consequences, all verified in source:

  1. Writes don't check provenance. app_write_file / app_update / edit_file route to appClient.writeFile / update (background/app-client.js), which resolve an appId and write straight into OPFS peerd-apps/<appId>/ with no check of record.dweb, record.source, or the manifest hash. The agent (or the user via the app tab's Edit toggle) can silently rewrite code installed from a peer.
  2. Nothing re-verifies at run time. engine-tabs/app-tab/app-tab.js composes whatever is in OPFS; there is no comparison against dweb.version_id.
  3. Grants survive local mutation. The bridge grant key is derived from the stored dweb.hash (peerd-distributed/apps/bridge.js, appKey), which editing doesn't update — so a locally modified dwapp keeps the room grants the user approved for the original publisher's bytes.

Additionally, the update path (background/routes/dweb.js app-update, deliberately replace-not-merge) will silently clobber local modifications with no conflict detection.

Design intent: fork state, not immutability

Local mutability is the product thesis — "your agent modifies your software" — so the fix is not Sandstorm/Cloudflare-OS-style immutable packages. It's making the fork honest:

  • Fork transition. On first write to an app whose record carries dweb with a non-null hash: mark the record forked (e.g. dweb.forked: true), preserving lineage (base publisher, dwapp_id, version_id) for display as "modified from <publisher>". Registry change lands in peerd-engine/app-registry.js (note: dweb is currently excluded from applyPatch by design — the fork flag needs a sanctioned mutation path, not a loosening of that rule).
  • Grant rotation on fork. The bridge grant key becomes fork-scoped (e.g. recompute from current content, or fork:<appId>), so the next join re-consents under the new bytes. This closes the "approved bytes ≠ running bytes" gap without blocking the edit itself.
  • UI surface. "Locally modified" badge in the app tab; shown in the library/catalog if relevant.
  • Update conflict handling. dweb/base/update-app on a forked app must not silently overwrite — require an explicit confirm ("discard local changes and update to the publisher's vN?") or decline. Old-version seed data is already retained (offscreen/dweb-base.js keeps prior bundles as "the substrate for a future revert/changelog") — this is the first consumer.
  • Reshare already does the right thing — republishing a modified app goes out under your publisher DID and slug (dwapp_id = H(publisher‖slug), peerd-distributed/apps/meta.js), so attribution can't be spoofed. Optional follow-up: carry base-lineage in the reshared card.
  • Optional follow-up: verify-on-launch. For non-forked dweb apps, recompute the bundle hash at app-tab load and compare with version_id — catches tampering via any code path that didn't take the sanctioned fork transition. Needs a perf look (bundles are ≤50MB; hash lazily/cache by OPFS mtime).

Tests

  • Bun: registry fork-transition semantics; grant-key derivation before/after fork.
  • In-browser/e2e: editing a dweb-installed app forces re-consent on the next room join; update-on-forked-app prompts instead of clobbering.

Prior art

Cloudflare OS keeps app code as an immutable content-addressed snapshot (Blueprints) and puts all mutability in per-instance data; Sandstorm likewise (spk packages vs. grain storage). peerd deliberately inverts this — which is exactly why the fork boundary has to be explicit rather than implicit.

Related: #235 (dwapp actors epic — actors driving dwapps raises the stakes on knowing which bytes are running).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions