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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ jobs:
# crates/<contract>/Cargo.toml in a PR; on merge this tags + publishes
# it. The constructorless allow-list in publish-plan decides which of
# these actually publish on-chain (the rest just get a tag).
CONTRACTS="perch-interpreter perch-doc-compiler perch-ed25519-verifier perch-spending-limit perch-account perch-stateless-registry"
CONTRACTS="perch-interpreter perch-doc-compiler perch-ed25519-verifier perch-spending-limit perch-account perch-stateless-registry perch-recovery"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
releases='[]'
Expand Down Expand Up @@ -240,11 +240,16 @@ jobs:
paths_for() { case "$1" in
perch-interpreter) echo "perch-interpreter perch-program";;
perch-doc-compiler) echo "perch-doc-compiler perch-compile perch-ir perch-program";;
perch-account) echo "perch-account perch-smart-account perch-doc-compiler perch-compile perch-ir perch-program perch-registry-resolve perch-registry-resolve-macro";;
# perch-smart-account depends on perch-recovery (the RecoveryController
# client apply_doc cross-calls), so a perch-recovery change re-releases
# the account too — see AGENTS.md's version-pin sharp edge.
perch-account) echo "perch-account perch-smart-account perch-doc-compiler perch-compile perch-ir perch-program perch-registry-resolve perch-registry-resolve-macro perch-recovery";;
perch-recovery) echo "perch-recovery perch-doc-compiler perch-compile perch-ir perch-program";;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: added a sync pass to release-pr right after the per-contract bump loop. For every crate actually bumped this run, it now greps every crates/*/Cargo.toml — tracked contracts and dev-only consumers alike, not just the ones in that contract's own paths_for() scope — for a { version = "...", path = "..." } pin naming it, and rewrites that pin's version in place. Verified against a real Ubuntu/GNU-sed container (matching runs-on: ubuntu-latest) with a stubbed git cliff forcing a 0.x minor bump on two contracts — confirmed every dependent pin (perch-smart-account, perch-testkit, integration-tests, and perch-recovery's own pin on perch-doc-compiler) updates correctly. Also updated AGENTS.md's sharp-edge entry, which described the old manual grep-and-bump step this automates.

*) echo "$1";;
esac; }
CONTRACTS="perch-interpreter perch-doc-compiler perch-ed25519-verifier perch-spending-limit perch-account perch-stateless-registry"
CONTRACTS="perch-interpreter perch-doc-compiler perch-ed25519-verifier perch-spending-limit perch-account perch-stateless-registry perch-recovery"
summary=""
bumped=""
for c in $CONTRACTS; do
inc=()
for d in $(paths_for "$c"); do inc+=(--include-path "crates/$d/**"); done
Expand All @@ -262,7 +267,25 @@ jobs:
git cliff --config cliff.toml --tag-pattern "$c-v.*" "${inc[@]}" --bump -o "crates/$c/CHANGELOG.md" 2>/dev/null
summary="${summary}- \`$c\`: $cur -> $next_ver"$'\n'
echo "bump $c: $cur -> $next_ver"
bumped="${bumped}$c=$next_ver"$'\n'
done
# Sync every intra-workspace path-dependency pin to the version its
# target crate just moved to. Without this, a 0.x MINOR bump here
# (breaking, under Cargo's caret rules) leaves a dependent's pin
# stale and `cargo metadata` fails workspace-wide the next time
# anything touches that dependent — see AGENTS.md's version-pin
# sharp edge (this used to be a manual "grep and bump every match"
# step, which is exactly how that failure mode happened before).
if [ -n "$bumped" ]; then
while IFS='=' read -r name ver; do
[ -n "$name" ] || continue
for f in crates/*/Cargo.toml; do
grep -qE "^${name} = \{ version = \"[^\"]*\", path = " "$f" || continue
sed -i -E "s|^(${name} = \{ version = \")[^\"]*(\", path = )|\1${ver}\2|" "$f"
echo "synced pin: $f -> $name $ver"
done
done <<<"$bumped"
fi
# The npm packages ride the same convention: tag-pattern <pkg>-v*,
# scoped to packages/<pkg>. Bump via `npm version` so
# package-lock.json stays in sync with the manifest.
Expand Down Expand Up @@ -618,6 +641,13 @@ jobs:
# The constructorless set. Keep in step with the detect-releases
# CONTRACTS list. A contract not listed here is tagged but never
# published to the constructorless registry.
# perch-recovery is deliberately NOT in this allow-list yet: it is
# tagged/versioned (CONTRACTS above) so its intra-workspace pins stay
# tracked, but publishing a recovery controller on-chain is a
# separate, deliberate decision (see
# docs/recovery/pending-activity-policy.md) — add it here explicitly
# when that decision is made, not as a side effect of adding the
# crate.
ALLOW='["perch-interpreter","perch-doc-compiler","perch-ed25519-verifier"]'
if [ -n "$DISPATCH_TAG" ]; then
# Strict allow-list regex, not a prefix glob: the tag and the package
Expand Down
99 changes: 84 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,100 @@ This file is the project's committed home for project-intrinsic agent knowledge:

- Add durable project-specific notes here as they are discovered through real work.

## Release pipeline sharp edge: dev-only crates aren't in a contract's version-bump scope
## Release pipeline: intra-workspace pins are auto-synced after each bump

`.github/workflows/release.yml`'s `release-pr` job (git-cliff) computes each
contract's next version from commits touching a fixed `paths_for()` path list
per contract (see the job for the current lists). Crates that are **only**
`[dev-dependencies]` consumers of a contract — `crates/integration-tests`,
`crates/perch-testkit` — are not in any contract's scope, so a commit that
only touches them never triggers a version bump there, and their
intra-workspace `path` dependency version pins (e.g. `perch-account = {
version = "0.1.1", path = "..." }`) can silently go stale when a contract
bumps a 0.x **minor** version (Cargo's caret rules treat 0.x minor bumps as
breaking). A stale pin fails `cargo metadata` for the **whole workspace**,
which only surfaces when `release.yml`'s `constructorless-build` job runs for
an unrelated contract — see PR #79 for a case where this silently blocked
`perch-doc-compiler-v0.2.0`'s publish for a day. When bumping any contract's
version, grep for its name across `crates/*/Cargo.toml` `{ version = "...",
path = "..." }` pins and bump every match, not just the crates in that
contract's `paths_for()` scope.
per contract (see the job for the current lists), then — after the bump
loop — greps **every** `crates/*/Cargo.toml` (not just the crates in scope
for a given contract, and not just other tracked contracts; dev-only
consumers like `crates/integration-tests`/`crates/perch-testkit` are
included) for a `{ version = "...", path = "..." }` pin naming a
just-bumped crate, and rewrites that pin's version to match. This exists
specifically because a stale pin fails `cargo metadata` for the **whole
workspace** the moment a contract bumps a 0.x **minor** version (Cargo's
caret rules treat 0.x minor bumps as breaking) — see PR #79 for a case
where a stale dev-only pin, fixed by hand at the time, silently blocked
`perch-doc-compiler-v0.2.0`'s publish for a day. If you're bumping a
version by hand outside this job (a one-off manual release), still grep for
the crate's name across `crates/*/Cargo.toml` and bump every matchthe
automation only runs inside `release-pr`.

Once a contract's tag (`<contract>-v<version>`) exists, `detect-releases`
will never retry that version even if the build later succeeds after a
fix — it only tags versions with no existing tag. Recovering a
build-that-never-published requires a fresh version bump (see PR #79), not a
rerun once the underlying commit's tree is fixed.

Adding a brand-new deployable contract crate needs entries in **four**
places in `release.yml`, not just workspace membership: both `CONTRACTS=`
lists (`detect-releases` and `release-pr` — kept as duplicated literals, not
one shared value), a `paths_for()` case for its own intra-workspace scope,
and — only once the contract is actually meant to auto-publish on-chain when
tagged — the `publish-plan` job's `ALLOW` list and `DISPATCH_TAG` regex.
Deliberately leaving a new contract out of `ALLOW` (while still tagging it
via `CONTRACTS`/`paths_for()`) is the correct way to ship its source and keep
its version-bump/pin tracking correct without triggering an on-chain publish
before that's a deliberate decision — see `perch-recovery`'s introduction
(PR that added `docs/recovery/`) for a worked example of this split.

## Infra contracts are resolved from a build-time-pinned wasm hash, not a runtime address

`perch-smart-account`'s `infra` module (`perch_registry_resolve::registry_contract!`)
derives the doc-compiler/interpreter/spending-limit addresses as
`deployer(stateless_registry_id, sha256(pinned_wasm_bytes))`, where the wasm
bytes are fetched into the build tree (`scripts/fetch-infra-wasm.sh`) and
baked into the *consuming* contract's own wasm at compile time — there is no
constructor argument or storage slot naming these addresses, and no way to
repoint them post-deploy. Two consequences that are easy to miss:

- **Any change to the doc/wire schema the doc-compiler accepts or emits
(`perch-ir`, `CompiledDoc`/`CompiledRule`) requires a new `perch-doc-compiler`
build, which is a new address — no existing deployed `PerchAccount` will
ever call it, no matter what's published later.** A schema change is never
a live upgrade for deployed accounts; it's a new option only newly-built
accounts can use. See `docs/recovery/migration.md` for the worked-out
consequence (an account built before a schema change can never adopt it in
place — only a new account can).
- A new deployable that other in-repo crates need to *cross-call the client
of* without linking its full logic (parser, storage, `Policy` impl, ...)
should split like `perch-doc-compiler` does: a `contract` feature
(default-on) gating the actual `#[contract]` struct/impl, plus an
always-available hand-written `#[contractclient]` trait outside that gate
for the entry points consumers actually call. `perch-recovery` follows this
split (`RecoveryControllerClient`, ungated, vs. the full `PerchRecovery`
contract, gated) so `perch-smart-account` links no controller storage or
`Policy`-lifecycle code into account wasm.

## Soroban auth mocking never invokes a custom account's `__check_auth`

`env.mock_all_auths()`/`mock_all_auths_allowing_non_root_auth()` puts the
host in recording-auth mode, which — confirmed by reading
`soroban-env-host`'s own source (`auth.rs`, `require_auth_recording`: "we
don't call `__check_auth` in this flow") — **skips invoking a custom
account's `__check_auth` entirely**, for every address including the account
under test. `perch_testkit::Bootstrap`'s `World` uses this mode, so any test
built on it (the whole `apply_doc*.rs`/`cap_matrix.rs` suite) proves
*compiler-level* validation and rule-installation shape, never OZ's
context-rule *selection*/`Policy::enforce` mechanics — those calls succeed
regardless of which rule, if any, would actually have authorized them.
To test real rule-selection/policy-enforcement behavior (as `matrix.rs` does,
and `crates/integration-tests/tests/recovery.rs` following its pattern for
the recovery controller), call
`stellar_accounts::smart_account::do_check_auth` directly with a hand-built
`AuthPayload`/`Context`, wrapped in `env.as_contract(&account, || {...})` —
this bypasses host-level auth entirely rather than depending on it, so it
works the same with or without mocking. Relatedly: OZ's
`remove_context_rule` calls a policy's `uninstall` via `try_uninstall` and
discards the result even if it panics — a policy can never rely on
`uninstall` as a security gate (see `docs/recovery/controller-governance.md`
for how `perch-recovery` handles this). When chaining several
`do_check_auth` calls that are expected to panic (via
`catch_unwind`) inside one test, keep it to one or two per `Env` — this
host's test call-stack bookkeeping was observed not to reliably survive a
longer chain of recovered panics in one `Env`; prefer a fresh `Env` (a new
`setup()`) per scenario instead of accumulating them.

## Maintaining this file

Keep this file for knowledge useful to almost every future agent session in this project.
Expand Down
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ members = [
"crates/perch-registry-resolve-macro",
"crates/perch-registry-resolve",
"crates/perch-testkit",
"crates/perch-recovery",
]

[workspace.package]
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ crates/
perch-smart-account/ the doc-only account trait: apply_doc (the sole write path) on OZ
perch-account/ deployable shell of perch-smart-account (6 exported functions, ~28 KB)
perch-ed25519-verifier/ deployable ed25519 verifier for External signers
perch-recovery/ deployable account-recovery controller (guardian/ZK/combined modes),
an OZ Policy attached via apply_doc's `recovery` document field —
see docs/recovery/
perch-deploy/ deploy/CI bin: signs smart-account auth entries (apply_doc, publish)
perch-conformance/ eval-semantics conformance vectors: hand-authored (program,
invocation) → verdict cases + compile→eval differential + wasm-leg suites
Expand All @@ -60,6 +63,8 @@ komet/ Komet (K-framework) symbolic property tests — an indepen
scripts/ bootstrap-testnet.sh — one-time registry + account bootstrap
docs/slides/ the perch story as an HTML deck (served via GitHub Pages)
docs/verification/ the layered verification plan (PLAN.md) + enforceability theory (THEORY.md)
docs/recovery/ opt-in account recovery: schema, controller governance, migration,
and the open release-blocking pending-activity gate
docs/testnet-deployment.md verified live-state map of the canonical testnet deployment
testdata/ golden vectors shared by the Rust and TS suites (frozen)
testdata/eval/ eval-semantics vectors shared by Rust, the Lean model, and the wasm leg
Expand Down
1 change: 1 addition & 0 deletions crates/integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ perch-ed25519-verifier = { version = "0.1.1", path = "../perch-ed25519-verifier"
perch-spending-limit = { version = "0.1.1", path = "../perch-spending-limit" }
perch-testkit = { version = "0.1.1", path = "../perch-testkit" }
perch-registry-resolve = { version = "0.1.1", path = "../perch-registry-resolve" }
perch-recovery = { version = "0.1.0", path = "../perch-recovery" }
stellar-accounts = { workspace = true }
soroban-sdk = { workspace = true, features = ["testutils"] }
ed25519-dalek = "2"
Expand Down
Loading
Loading