Repository navigation
spike: policy showcase — lossless doc inspector, doc-only builder with update diff, dApp session-key request - #202
Closed
willemneal wants to merge 14 commits into
Closed
willemneal wants to merge 14 commits into
willemneal wants to merge 14 commits into
Conversation
|
Example dApp preview deployed! https://example-pr-202.mysoroban.pages.dev The |
|
Preview deployed! Account URLs use numeric preview suffixes, for example |
willemneal
force-pushed
the
fm/nido-pr168-showcase-n2
branch
from
September 10, 2026 15:29
9cfc0ac to
c9f03ab
Compare
willemneal
force-pushed
the
fm/nido-pr168-showcase-n2
branch
from
September 10, 2026 17:07
9902ed6 to
1725553
Compare
Adds a Policy page (/account/policy) that visualizes every context rule on a smart account and a builder for adding new rules. Inspector: fetches all context rules (fetchAllChainRules) and renders each as a card — scope (any contract / one contract / contract creation), signers (passkeys vs delegated keys), attached policy conditions (labeled from the registry where known), and expiry classified against the current ledger. Each rule gets a plain-language sentence of what it permits, and rule 0 is flagged as the account's primary authority. Builder: composes a new context rule (scope, one or more passkey/delegated signers, optional spending-limit policy, optional expiry), validates it, lowers it to the smart-account add_context_rule arguments, and submits through the account's existing passkey signing path (signAndSubmit). No silent signing — the on-chain write always goes through the user's passkey. The display model (policyView) and draft validation/lowering (policyDraft) are pure and unit-tested (34 tests); the Security page stays the curated front door for recovery/session keys, this is the general view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011iRUGB6K1XCEsdHoxEzsbP
The policy page now runs the SDK's three-tier readPolicy: - tier a: an applied document recovered losslessly (get_applied_doc storage view first, DocApplied event history as fallback) and verified against the stored doc_hash renders as the document itself — the doc's own rule names, signer ids, and function lists, with a doc_hash badge and a provenance badge (stored on chain / from event history). - tier b: same view plus the SDK's drift findings, rendered prominently. - tier c: the existing raw rule cards, unchanged. The builder gains a doc mode (default) with the canned v1 template — a scoped session key (delegated signer, one contract's named functions, expiry) plus an optional cumulative spending cap — with a live canonical JSON + doc_hash preview. Uncapped docs apply through the account's one-transaction apply_doc surface; capped docs (and accounts without the surface) install per-rule via the SDK's client-side lowering, where the cap rides the stock spending-limit policy. The raw add_context_rule form stays as the second mode. New pure libs (docView, docDraft, docPolicyFetch helpers) carry the display models, validation, and route choice; all vitest-covered. @stellar-registry/perch-interpreter joins the frontend deps for the typed get_program read (stellar-sdk pinned by the root override).
The status-message demo dApp gains a second delegation option: request a SCOPED SESSION KEY as a perch policy document. Its existing local G keypair becomes a delegated signer restricted to the note function, and the click-through the showcase demonstrates is: dApp requests → wallet builds the doc → /sign/ applies it → the wallet's Policy page shows the lossless document. New wallet page /security/delegate-doc/ (doc sibling of /security/ delegate/): parses the request (pure lib lib/policy/docRequest), offers duration + optional spending-cap controls, previews the document's canonical JSON and doc_hash — the exact identity the account stores on chain — and hands off to /sign/ with a new apply-policy-doc descriptor. The expiry ledger is anchored once at page load so the previewed hash is byte-identical to what gets signed. /sign/ renders the descriptor as a review card built from the document itself (rule name, key, functions, cap, expiry, doc_hash) with the canonical JSON behind a toggle; buildOperation routes it through buildApplyDocTx (uncapped, apply_doc surface) or a single-rule per-rule install, refusing multi-rule docs on the per-rule route offline. Anti-redirect-abuse validation and the ?delegation=ok|cancelled return contract are carried over from the passkey delegate flow unchanged.
…puts Rebased onto 856e258 (on-chain canonical doc JSON + get_applied_doc): pass the storage-read doc as storedDocJson and an event-recovered doc as eventDocJson, matching the SDK's view-first input surface instead of funnelling both through the event field.
Same conventions as account-ui.spec.ts: built-HTML anchors, no-fatal-JS boot checks, and client-side validation that fires before any network call — doc-mode template validation, the mode toggle, delegate-doc request rejection (missing origin) and offline doc-JSON preview.
Captain ruling: every policy write goes through apply_doc — the per-rule add_context_rule lowering is gone from the builder, the delegate-doc flow, and the /sign/ descriptor (route field dropped), and the builder's raw-rule mode is removed with it. chooseApplyRoute/docHasCap die. apply_doc REPLACES the applied document, so both write surfaces are now update-aware: they read the account's applied doc (get_applied_doc, event fallback), UPSERT the requested session rule into it (upsertSessionRule: same-key signer reuse, id-collision allocation, orphan pruning, cross-network refusal), and preview the merged document. A new pure diff (diffPolicyDocs) classifies rules added / removed / modified — with field-level change lines on modified rules, including rekeyed-signer surfacing — and renderDocDiffHtml shows it on the builder, the delegate-doc page, and /sign/ (prevDocJson carried in the descriptor) before the user confirms. First-time applies render as all-new. Fail-closed guards: the delegate-doc flow refuses accounts without the doc surface and refuses to build an update over an unrecoverable applied doc; fetchDocSurface/fetchAppliedDocJson now swallow constructor throws (an invalid account id reads as no-surface instead of escaping). Note: capped documents now rely on the reworked apply_doc gaining cap support on the base branch (PR 201's in-flight update); until that lands the SDK's buildApplyDocTx still refuses caps at build time.
…, two-tier read Rebased onto 464f6b0 (apply_doc is the sole policy write path): - First applies now upsert into an owner-admin BASELINE (ownerAdminBaseline: the account's live primary passkey with a policy-free self-admin rule) instead of submitting a standalone session doc — apply_doc replaces EVERY rule including the constructor default, and the contract's DocAdminLockout refuses documents without that admin shape. upsertSessionRule now requires a non-null base; the builder and delegate-doc page read the passkey off rule 0 (fetchDefaultRuleAuthInfo) when nothing is applied yet, and fail closed when neither an applied doc nor the passkey can be read. - readPolicy is two-tier now (no mutators → no drift by construction): drop the docRuleIds input, the drift renderings, and the drift model fields; the doc view badge is always Verified · lossless. - Capped documents ride the single apply_doc tx (the contract lowers caps onto its pinned spending-limit policy) — no client-side special case remains. - e2e: the offline builder expectation flips to the fail-closed message; the first-apply preview happy path moves to pure unit coverage (renderDocDiffHtml + upsert-into-baseline tests).
Captain feature: the builder gains an Admin keys tab alongside the session-key template. Adding enrolls another ADMIN signer — a brand-new passkey created via the WebAuthn ceremony on submit (verifier resolved from the account's own rule) or a pasted external/delegated key — as a policy-free cap-free self-admin rule, the same shape the contract's anti-brick check requires; each admin key gets its OWN rule (all principals are N-of-N, so sharing one would force co-signing). Removal falls out of the same machinery: pick an enrolled admin key, review the removal diff, apply — refusing the LAST admin key up front with a human-readable reason (DocAdminLockout would reject the document anyway). Both paths are doc updates through the shared apply plumbing: compose against the loaded baseline, show the added/removed signer + rule in the what-changes diff like any other doc change, one apply_doc, passkey ceremony via signAndSubmit. The add form refuses duplicate rule names (never a silent rule replacement) and keys that already hold admin authority. Pure lib: isAdminRule/adminRules/nextAdminRuleName, validateAdminKeyDraft, addAdminKey, removeAdminRule in docDraft, with the signer-merge/prune/revalidate helpers factored out of upsertSessionRule and shared. 10 new vitest cases; new @fast e2e spec for the admin tab (mount, defaults, offline fail-closed).
Captain feedback from live testing of the showcase: - Navigation: the Security page gains an 'Account policy' nav card (existing navrow conventions) linking to /account/policy/, and the delegate-doc preview links to the Policy page where the applied document will be visible. The dApp's return banner already linked. - Readable previews: the builder (both tabs) and the delegate-doc page now render the 'document after this update' as compact rule cards from the inspector's own display model — signer chips with the doc's ids, one mini-card per rule with the permission sentence and function/cap/expiry facts — with the canonical JSON folded behind a 'Raw document JSON' toggle instead of dumped raw. The what-changes diff panel is unchanged and stays legible alongside. renderDocPreviewHtml is a pure string builder in PolicyInspector, unit-tested; styles shared across the policy and delegate-doc pages.
Captain's live test failed with Error(Contract, #19): the dApp's 'Delegate this dApp' button routes through /security/delegate/, which still emitted add_context_rule — the doc-only gate refused it exactly as designed. This closes that escaped path and sweeps the rest: - /security/delegate/ is now a policy-document update, same discipline as delegate-doc: baseline load (applied doc, or owner-admin baseline on first apply; fail-closed otherwise), the dApp's session passkey declared as an external signer against the account's own verifier, a 'session-key' rule upserted (re-delegation REPLACES it — the diff shows it as modified), what-changes diff + readable doc preview + doc_hash, and an apply-policy-doc handoff. The query-param contract and anti-redirect-abuse validation are unchanged, so startDelegation keeps working. The spending-limit control moved here from /sign/ (doc grants are confirm-only there); caps ride the doc (compiler 0.2.1 base). - Both delegate pages now build their /sign/ operation through ONE shared helper (buildSessionGrantOperation in docRequest) — the regression tests pin that it emits apply-policy-doc, carries prevDocJson on updates, and replaces same-named rules. - operationBuilders: the add-context-rule branch now THROWS doc-only guidance offline (like remove-context-rule) so a stale stashed request fails with a message instead of #19 on-chain; dead SmartAccountClient plumbing dropped. Regression tests cover both refusals. - Emitter sweep: the only remaining add_context_rule builder is zkRecovery's recovery COMPLETION (the contract's sanctioned gated window); multisig-recovery paths already throw doc-only guidance in the SDK. No other legacy mutator emitters in frontend or sdk. - Rebased onto 1979dca (caps re-enabled on perch compiler 0.2.1 + realigned testnet wasm). - New @fast specs for the converted delegate page (param rejection, fail-closed baseline); passkey-signer draft coverage in docDraft.
willemneal
force-pushed
the
fm/nido-pr168-showcase-n2
branch
from
September 11, 2026 20:28
1725553 to
1e90ce3
Compare
…ption C)
Ruling on the admin composition (needs-decision admin-any-principals):
keep ONE policy-free self-admin rule per admin — each independent full
authority, which already IS 'any admin may act' — no contract change,
no interpreter-gated admin rule (doc v1 has no 'any' principal type;
threshold m=1 always lowers interpreter-attached, which the anti-brick
check refuses by design). What changes is naming and presentation:
- Naming: the founder keeps signer id 'owner' (rule 'admin'); added
admins get signer ids admin-2, admin-3, … mirroring their default
rule names — no more bare 'admin' signer colliding with the rule name.
nextAdminRuleName picks the slot free as BOTH rule name and signer id.
- Single-list UI: the policy page's doc view folds every admin rule
into ONE 'Admin keys' card ('Any of these N keys can act … each holds
independent full authority'), non-admin rules keep their per-rule
cards; DocRuleView carries isAdmin. The builder's admin tab copy says
the same; its list, add/remove flows, and the diff preview already
present the set as one list.
Tests updated for the new ids plus new coverage: isAdmin classification
and the consolidated card (one data-doc-admins card, both keys listed,
no per-rule admin cards).
'It could be confusing for new users that there are both.' The founder's signer id is now 'admin' (rule name 'admin'); added admins stay admin-2, admin-3, …. The 'owner' id disappears everywhere: - adminBaseline (né ownerAdminBaseline) declares the founder as 'admin' in the first-apply document; add/remove flows and UI copy follow (the doc-head card no longer says 'owner' either). - Legacy applied docs that still declare 'owner': renameLegacyOwner is a guarded pure migration (only when no distinct 'admin' signer exists, so two different keys can never merge) applied inside rebuildDoc — so the rename rides the user's NEXT doc update through any compose path, and the diff preview renders it (signer 'owner' removed / 'admin' added, referencing rules modified). - Display layer: summarizeDoc migrates ids before rendering, so an un-updated legacy doc already shows 'admin' on the policy page and in previews — the two names never appear at once. Tests: fixtures moved to the new id; new coverage for the composed rename (visible in the diff) and the distinct-admin guard; composed signerId return values map through the rename.
… labeled Captain live repro: a NEW account's /account/policy showed no document (nothing applied yet → decompiled tier → doc section hidden). Now a doc-surface account with no applied document renders the SYNTHESIZED baseline — the founder admin rule over the default rule's live passkey, built by the same adminBaseline the builder and both delegate flows compose against on first apply (fetchUnappliedBaseline, shared) — as the current effective policy, with the full rule-card + admin-list + raw-JSON-toggle presentation. Clearly labeled: 'Not yet applied' badge, lead copy saying the first policy edit applies exactly this document, and 'Document hash (once applied)' for the would-be identity. The first real edit applies it and the label drops (readPolicy then reads doc-verified as usual). readDocPolicy exposes the unapplied state (surface supported, no stored hash); page and flows provably share one baseline. New fixture test: the new-account doc view renders labeled, with the admins card and raw toggle, and an applied render never carries the label.
Closed
7 tasks done
Contributor
Author
|
Superseded by #207, which merges this branch's work together with 200/201/202/204/205/206 into one reconciled, non-draft PR off main. This branch stays on origin for history. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this demonstrates
The policy showcase: the lossless policy view the doc layer buys, end to end, clickable — under the doc-only ruling (every policy write is one
apply_doc; there is no per-rule route).The click-through: the status-message demo dApp requests a scoped session key → the wallet composes a policy-document update and shows exactly what changes plus a readable document preview (rule cards; raw canonical JSON behind a toggle) +
doc_hash→/sign/applies it (oneapply_doctx) → the wallet's Policy page renders the document itself — the author's rule names, signer ids, and function lists, verified byte-for-byte against the hash the account stores on chain.1. Perch-powered policy inspector (
/account/policy)Incorporates #168's inspector (cherry-picked) and layers the SDK's two-tier
readPolicyon top:doc-verified: the applied document, read losslessly from the on-chain copy (get_applied_doc;DocAppliedevent history as in-tier fallback), rendered with the doc's own names everywhere — rule names, signer ids, function chips, cap, expiry — plus adoc_hashbadge, a provenance badge (Stored on chain / From event history), and a raw-JSON toggle (pretty-printed for reading; Copy gives the exact canonical bytes whose sha256 is the stored hash). Under doc-only there is no drift tier by construction.decompiled: feat(frontend): policy inspector + builder for a smart account #168's raw rule cards, unchanged. The raw cards also stay below the doc view in tier a (they carry rule ids / on-chain context).2. Doc-only builder with the canned template and a document-update diff
The builder is the v1 template — scoped session key (delegated signer, one contract's named functions, expiry) plus an optional cumulative spending cap (caps now ride the single
apply_doctx; the contract lowers them onto its pinned stock spending-limit policy). #168's rawadd_context_ruleform is gone with the mutators.apply_docreplaces the whole document, so every submit is an update:upsertSessionRule: same-key signer reuse, id-collision allocationsession-2…, orphaned-declaration pruning, cross-network refusal).ownerAdminBaseline— the account's live primary passkey with a policy-free self-admin rule — because the contract'sDocAdminLockoutanti-brick check refuses documents without one (and the constructor's default rule dies on first apply).diffPolicyDocs+renderDocDiffHtml) shows rules added / removed / modified — name-level with field-level change lines on modified rules (scope, signers, quorum, functions, expiry, cap, arg conditions, rekeyed signer ids) — before the user confirms. First applies render as all-new; identical docs as a no-op.3. Admin keys — add / remove
The builder's second tab enrolls another admin key: a brand-new passkey (WebAuthn ceremony on submit, verifier resolved from the account's own rule) or a pasted external/delegated key, added as a policy-free self-admin rule — the same shape the anti-brick check requires, one rule per key. The admin model is any-may-act (captain's ruling): each admin rule is independent full authority, so the UI presents the set as one list — a consolidated Admin keys card on the policy page, the single list in the builder — with naming
admin(founder) /admin-2,admin-3, … (signer ids mirror rule names; the earlierownerid is gone — legacy docs are renamed on their next update, the rename shows in the diff, and the display layer already rendersownerasadminso both never appear at once). One rule per key rather than one merged any-of rule because doc v1 expresses "any" asthreshold m=1, which always lowers interpreter-attached — exactly what theDocAdminLockoutanti-brick check refuses (the admin path must never depend on an interpreter refusal). Removal uses the same machinery: pick an enrolled admin key, review the removal diff, apply — the last admin key is refused up front with a human-readable reason. Duplicate rule names and keys that already hold admin authority are refused at validation.4. dApp example (status-message page)
“Request a scoped session key (policy doc)” next to the existing passkey delegate button. The dApp's local G keypair becomes the delegated signer, scoped to
udpate_message, 24h duration. The wallet page/security/delegate-doc/(same anti-redirect-abuse guard as/security/delegate/, pure param-parsing lib) composes the doc update, shows the What changes diff + canonical JSON +doc_hash, and hands off to/sign/via theapply-policy-docdescriptor — which carriesprevDocJsonso/sign/shows the same diff at the confirm ceremony. On return the dApp links straight to the policy page.Deltas / shortcuts (spike-honest)
/sign/doc grants are confirm-only — cap/duration edits happen on the delegate-doc page before the handoff./security/delegate/) is now doc-only too: the dApp's session passkey becomes an external signer on asession-keydoc rule (re-delegation replaces it), with the same baseline/diff/preview discipline — both delegate pages build their/sign/operation through one sharedbuildSessionGrantOperationhelper, and theadd-context-rule/remove-context-ruledescriptors throw doc-only guidance offline (regression-tested). The only survivingadd_context_rulebuilder is zk-recovery's completion vehicle (the contract's sanctioned gated window). The spending-limit edit moved from/sign/onto the delegate page. The testnet-lane e2e specs for the old flow (tests/e2e/testnet/*) still describe the pre-doc behavior and need a pass once the stack settles./account/policy/; the delegate-doc preview and the dApp's return banner link there too.doc_hashis byte-identical to what gets signed.@stellar-registry/perch-interpreteradded to frontend deps for the typedget_programread (rootoverrideskeeps it on the workspace's single stellar-sdk — the Deliverable 4 (Tranche 2): gas abstraction via OZ Relayer + session-key scope UI #72 hazard).Tests
docView,docDraft,docRequest,docDiff— diff classification, field-level lines, rekeyed signers, upsert/baseline/pruning, first-apply rendering, admin add/remove/last-admin refusal — plusdocPolicyFetchhelpers andoperationBuildersguards)@fast:tests/e2e/ui/policy-page.spec.ts(boot, template validation, admin-keys tab, fail-closed baselines, delegate + delegate-doc rejection) — chromium green locally; firefox/webkit not installed in this environmentastro check: 0 errors; frontend build green