Add autonomous agents page under contract accounts - #2811
Conversation
Covers bounding a long-running agent's authority with two composable restriction layers (callee-side instruction-set design, key-side smart account policy) and three runtime pitfalls that don't show up until the agent is actually operating: simulation not verifying auth, stale cached authority in a long-running loop, and where LLM discretion should stop versus hard-coded limits. Sits alongside the existing Advanced contract account patterns page, which covers the design-time guardrail primitives this page composes for a specifically autonomous, unattended caller. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds guidance for safely bounding autonomous agents using contract restrictions and smart-account policies.
Changes:
- Documents layered authority restrictions.
- Covers simulation, revocation, and model-safety pitfalls.
- Links to relevant implementations and specifications.
Recommendation: NEEDS-CHANGES — Correct the overstated security and revocation claims, and use relative links for internal pages.
Suppressed comments (1)
docs/build/guides/contract-accounts/autonomous-agents.mdx:40
- This is an in-repository docs target and should be relative so it resolves in local and versioned previews.
- [OpenZeppelin Smart Accounts](https://developers.stellar.org/docs/tools/openzeppelin-contracts) — context rules, signers, and policies, audited by OpenZeppelin's security team, with formal verification by Certora in progress.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The composed-layers paragraph implied the key-wrapping (smart account) layer was operating alongside the callee-side restriction. It isn't yet: the deployed contract's context_rules are confirmed correct by reading them on-chain, but nothing has authorized a real signed transaction through it. Only the callee-side restriction (DeFindex's Invest/Unwind) has a track record of real signed calls. Correcting before that distinction gets cited as settled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
docs/build/guides/contract-accounts/autonomous-agents.mdx:17
CallContract(vault)scopes the credential to an address, not a function, and DeFindex also exposes withdrawal and administrative entry points. The final sentence therefore overstates what the two described layers guarantee: the bound comes from assigning this account only theRebalanceManagerrole. State that dependency explicitly rather than claiming the whole contract can only return funds to itself.
Neither layer is sufficient alone for an unattended agent. Restrict only the callee, and a leaked key can still authorize anything the callee's own logic happens to allow — a swap at bad slippage, say, if the callee supports swaps at all. Restrict only the key, and you're trusting that the callee's logic never grows a footgun later. Composed, an agent's key can only reach one contract, and that contract can't move funds anywhere but back into itself.
docs/build/guides/contract-accounts/autonomous-agents.mdx:42
- Links from
docs/**to another page in this repository must be relative; the hard-coded published-site URL can also break preview/versioned builds. Point this link at the local MDX page.
- [OpenZeppelin Smart Accounts](https://developers.stellar.org/docs/tools/openzeppelin-contracts) — context rules, signers, and policies, audited by OpenZeppelin's security team, with formal verification by Certora in progress.
docs/build/guides/contract-accounts/autonomous-agents.mdx:29
- Revocation does not become ineffective when the process caches an earlier permission read: the contract evaluates its current on-chain role and authorization state for every submitted call, so a revoked agent's later transaction fails. Re-reading each cycle is still useful to stop constructing or submitting doomed transactions, but it is not what enforces revocation. Please correct this security behavior.
A human signer revokes access once, and the session ends there. A long-running agent process keeps looping regardless of what changed underneath it, so if it caches "am I still authorized" from the start of the process, a revocation made ten minutes into a multi-day run does nothing until the process restarts. Read the current role or permission state from chain at the top of every cycle, not once at boot.
Confirmed empirically (needsNonInvokerSigningBy(), raw simulation auth entries, both authMode values) that a Signer::Delegated's own signing requirement never surfaces via the SDK's standard discovery flow — full detail and repro in OpenZeppelin/stellar-contracts#863. Framed with the same precision as that issue: what's empirically confirmed vs. what's inferred about recording mode's internal behavior, not independently verified against the host implementation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
docs/build/guides/contract-accounts/autonomous-agents.mdx:17
- This overstates the callee-side guarantee. DeFindex investments send assets into strategy contracts, and swaps transfer input to a pair; the verified property is that proceeds cannot be directed to an arbitrary recipient, not that funds never move anywhere except back into the vault. Narrow the conclusion to the actual destination restriction.
Neither layer is sufficient alone for an unattended agent. Restrict only the callee, and a leaked key can still authorize anything the callee's own logic happens to allow — a swap at bad slippage, say, if the callee supports swaps at all. Restrict only the key, and you're trusting that the callee's logic never grows a footgun later. Composed, an agent's key can only reach one contract, and that contract can't move funds anywhere but back into itself.
docs/build/guides/contract-accounts/autonomous-agents.mdx:25
- This conflates two different operations:
enforceis asimulateTransactionauthorization mode, while an on-chain submission has no “enforcement mode.” The internal link also needs to be relative. Distinguish enforcement-mode simulation from actual transaction submission so readers can implement the deny-path test correctly.
[Recording-mode simulation](https://developers.stellar.org/docs/learn/fundamentals/contract-development/contract-interactions/transaction-simulation#recording-mode) records every `require_auth` call as successful, and "never emulates authorization failures... failing authorization is always an 'exceptional' situation." That's the right behavior for building a transaction to sign, and the wrong tool for testing that a restriction actually holds. A policy that looks correctly restrictive under simulation can still pass simulation for an action it should deny, because simulation was never checking the signature in the first place. To test the deny path, submit the transaction in enforcement mode and confirm it actually fails on-chain — don't infer it from a clean simulation.
docs/build/guides/contract-accounts/autonomous-agents.mdx:46
- Docs links to pages in this repository must be relative. Point this item at the local MDX source rather than the published-site URL.
- [OpenZeppelin Smart Accounts](https://developers.stellar.org/docs/tools/openzeppelin-contracts) — context rules, signers, and policies, audited by OpenZeppelin's security team, with formal verification by Certora in progress.
…al ones Verified each of Copilot's six review points against real source before responding, per the standing discipline: - DeFindex rebalance(): Copilot was right. The router's own source shows a swap's input leg transfers to the trading pool mid-execution, not only ever back to the vault. Narrowed the claim. - Nirium Invest/Unwind evidence: Copilot was wrong (or working from a stale source) — there are two of each, not one Invest and zero Unwind. Verified b7bf6d70... (Unwind) directly against Horizon before citing it; both hashes now linked explicitly instead of an unbacked claim. - Hardcoded published-site URLs (3 instances): fixed to relative links, verified with the repo's own scripts/check-relative-links.sh. - Caching/revocation: Copilot was right — __check_auth re-reads current rules from chain every check, so an on-chain revocation is effective regardless of what a process cached. Reframed: the real cost of caching is wasted cycles and delayed self-awareness, not a security bypass. - "Enforcement mode": fixed to name the real authMode values (enforce/ record/record_allow_nonroot) and be precise that a real submission, not a special mode, is what actually enforces auth. - AuthMode count/API usage: Copilot's core technical point (three modes, not two) is correct per the real CHANGELOG, even though the specific file path it cited doesn't exist in this repo. Checking this surfaced a real methodological gap in the linked issue's own multi-mode claim (OpenZeppelin/stellar-contracts#863) — softened this page's citation to what's independently confirmed, added a real on-chain settlement of the delegated-signer case as stronger evidence. pnpm exec prettier -c and scripts/check-relative-links.sh --range both pass clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
docs/build/guides/contract-accounts/autonomous-agents.mdx:25
- A deny path does not require an on-chain submission. This repository's Transaction Simulation page says enforcing mode executes
__check_authand is effectively equivalent to on-chain execution, while the signing guide explicitly recommends enforcing-mode simulation to catch authorization errors without paying fees. Instruct readers to provide the attempted transaction's auth entries/signatures and confirm that an enforcing-mode simulation fails; submission is optional.
[Recording-mode simulation](../../../learn/fundamentals/contract-development/contract-interactions/transaction-simulation.mdx#recording-mode) records every `require_auth` call as successful, and "never emulates authorization failures... failing authorization is always an 'exceptional' situation." That's the right behavior for building a transaction to sign, and the wrong tool for testing that a restriction actually holds. A policy that looks correctly restrictive under simulation can still pass simulation for an action it should deny, because simulation was never checking the signature in the first place. `simulateTransaction` takes an `authMode` of `enforce`, `record`, or `record_allow_nonroot` — to test the deny path, actually submit the transaction (a real submission always enforces authorization, `authMode` or not) and confirm it fails on-chain. Don't infer a deny path from a clean recording-mode simulation.
docs/build/guides/contract-accounts/autonomous-agents.mdx:33
- The chain cannot reject an attempt to sign: signing happens locally with the agent's private key and does not consult
__check_auth. Revocation is enforced when the transaction is simulated in enforcing mode or submitted, so saying the next transaction the agent “tries to sign” is rejected misstates the security boundary.
An on-chain revocation is effective the moment it lands, regardless of what any process has cached: `__check_auth` re-reads the account's current rules from chain on every check, so the very next transaction the agent tries to sign is rejected whether or not its own process noticed anything changed. What caching actually costs you is time and signal, not security. A long-running agent that only reads "am I still authorized" once at boot keeps looping anyway, spending cycles building and submitting transactions that fail on-chain one after another, instead of noticing the moment its authority changed and stopping or alerting. Read the current role or permission state from chain at the top of every cycle — not to keep the revocation itself effective, but so the agent finds out it's been cut off immediately, not after a string of failed submissions.
Three real findings, all fixed, all verified against this repo's own
transaction-simulation.mdx and signing-soroban-invocations.mdx before
writing the fix, not assumed from the review comment alone:
- The 'composed guarantee' summary sentence still claimed a leaked key
plus a restricted callee 'can't move funds anywhere but back into
itself', the broader claim already narrowed earlier in the same page
(DeFindex's swap-input leg does transfer out). Aligned both sentences
to the same, narrower guarantee.
- 'To test the deny path, actually submit the transaction' overstated
what's needed. Enforcing-mode simulation already validates signatures
and executes __check_auth for real ('basically equivalent to running
the transaction on-chain', per transaction-simulation.mdx's own
description) at no cost, per signing-soroban-invocations.mdx's own
'Failed simulations cost nothing; failed submissions cost real fees.'
Submission still works, but isn't the free, recommended check.
- 'The very next transaction the agent tries to sign is rejected'
conflated signing (a local, offline step that never consults
__check_auth) with the actual enforcement points (enforcing-mode
simulation or a real submission). Corrected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Two more real points from this review, bundled as "suppressed comments" without their own inline thread, both verified against this repo's own docs before fixing, both addressed in
Thanks for catching both — accurate on all three points this round. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
docs/build/guides/contract-accounts/autonomous-agents.mdx:13
- DeFindex is not an example of withdrawal being absent from the contract interface:
vault/src/interface.rsexplicitly exposeswithdraw, plus several administrative entry points. The narrow property established bymodels.rsapplies torebalance()for a least-privileged RebalanceManager, not to the vault as a whole. Please scope this explanation to that entry point so readers do not treat the full callee interface as destination-less.
**Restrict the callee.** The safest scope is a capability the contract you're calling never implements in the first place. A vault that only exposes `Invest(strategy, amount)` and `Unwind(strategy, amount)`, with no destination argument anywhere in the interface, cannot be told to pay out to an arbitrary address no matter what the caller's key can sign. Withdrawal isn't denied by a check; it's absent from the interface. [DeFindex's vault](https://github.com/defindex-io/stellar-contracts/blob/main/vault/src/models.rs) is a real example: its `rebalance()` instruction set is `Unwind`, `Invest`, `SwapExactIn`, and `SwapExactOut`, and none of the four take a destination argument. That's a narrower guarantee than "funds only ever move back into the vault," though — a swap's input leg does transfer out to the trading pool mid-execution ([verified directly against the router's own source](https://github.com/defindex-io/stellar-contracts/blob/main/vault/src/router.rs)), and the pool address itself is resolved on-chain from the token pair, not supplied by the caller. What the caller genuinely cannot do is redirect a payout or a swap's output to an address of their choosing.
…hole vault; make the role-assignment dependency explicit Two real points from the latest review, both verified against DeFindex's actual source (interface.rs, access.rs, lib.rs), not just its docs: - interface.rs genuinely exposes withdraw() and Manager-gated admin functions (set_manager, set_fee_receiver, set_emergency_manager, upgrade, distribute_fees, rescue) -- the vault as a whole isn't destination-less, only rebalance()'s own instruction set is. Scoped the claim accordingly. withdraw() itself turned out to be self-service (from.require_auth(), pays from itself) -- not an attack surface, so it's called out as such rather than lumped in with the admin functions. - CallContract(vault) restricts which contract a smart account can reach, not which function on it -- access.rs shows set_manager()/upgrade() are gated by require_role(Manager), a completely separate on-chain fact from the smart account's own context rules. Made explicit that the composed guarantee additionally depends on this address holding only the vault's RebalanceManager role, verifiable via get_rebalance_manager()/get_manager()/get_emergency_manager()/get_fee_receiver(). -f content=
|
Both real, verified against DeFindex's actual source rather than assumed from the earlier text, fixed in
Small note on the commit itself: pushed via the Contents API and a stray |
What
Adds a new page,
docs/build/guides/contract-accounts/autonomous-agents.mdx, sitting alongside the existing Advanced contract account patterns page. No existing files touched.Why
Advanced contract account patterns covers the individual guardrail primitives (spend limits, allow lists, policy signers, time rules, session keys, external policy contracts) — installed once, typically for a human session. An autonomous agent is a different caller: the same signing key stays live indefinitely and decides on its own when to act, so the restriction has to hold for as long as the agent keeps running, not just at setup.
This page covers two composable ways to bound an agent's authority (restricting the callee's own instruction set vs. restricting the caller's key via a smart account policy, and why you generally want both), plus three pitfalls that specifically only surface once the agent is actually operating rather than when the policy is installed:
require_authcalls as successful and never emulates authorization failures — testing a deny path requires actually submitting in enforcement mode, not reading a clean simulation.Closes with a short note on what a technically sound restriction does and doesn't resolve (the legal categorization question is separate from the technical one).
Notes
rebalance()instruction set is quoted from DeFindex's current public repo (verified directly, not from memory — an instruction set some notes elsewhere describe as having dropped swap support, which is not what the current code shows); the simulation behavior is quoted verbatim from this site's own Transaction Simulation page; the Certora line matches this site's own OpenZeppelin Contracts page wording exactly ("is being completed", not "completed" — no Certora report exists in OpenZeppelin's ownaudits/folder yet).sidebar_position: 45, between Advanced contract account patterns (40) and Contract account examples (50).pnpm check:mdxpasses clean against the real.prettierrc.js.Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com