HF15: agent access — delegate listed operations to an agent account - #167
Merged
Merged
Conversation
…id 105 Account-level delegation of an explicit operation list: `agent` may broadcast the listed operations on behalf of `account`, signing with its own active key, until `expiration` (epoch = perpetual, empty list = revoke). No roles, no masks: a mask would silently widen the grant when a new operation is appended to the chain. Wiring: - op struct appended at the END of the operation static_variant (index 105), so no existing op-id moves; - operation_wire_name() / is_broadcastable_operation_wire_name() in operations.cpp: name lookup derived from the same type names the wire format uses, with virtual operations excluded (they are never broadcast, so a delegation to one is dead); - validate() refuses what looks fine but is not: unknown names, virtual names, legacy aliases (would duplicate a canonical name), malformed names, self-agent, set_agent_permission itself (an agent must not re-delegate) and the proposal wrappers (a proposal carries arbitrary operations whose authorities are collected at execution time, so delegating proposal_create would bypass the explicit list). Tests: tests/pm/agent_access_test.cpp (6 cases: op-id anchors on both sides of the append, wire-name classification, and the validate() accept/refuse sets). Proven to be able to fail: with the never-delegable assert disabled, exactly the 4 escalation checks go red.
… delegation names Step 2 of HF15 agent access: the consensus state behind set_agent_permission. Object `agent_permission_object` holds one row per (principal, agent) pair: the canonical `,`-joined list of operation names and an expiration (epoch = perpetual). Unique index on (principal, agent) so a re-grant replaces rather than accumulates, and a non-unique index on the agent — the wipe rules need both directions. Two adversarial findings drove the deny-list change (protocol side, same branch): - `account_update` is an account takeover: an op WITHOUT the master field is satisfied by the ACTIVE authority and may carry a new `active` authority, so an agent granted account_update for "metadata edits" can rotate the principal's active key to its own and own the account. - master-only operations (recover_account, change_recovery_account, set_account_price, set_subaccount_price, target_account_sale) are unreachable through a delegation — the hook never substitutes master — so granting one would be a permission that can never succeed. The deny-list is now a single source of truth in protocol (`never_delegable_operation_names`), checked both at grant time and, in step 3, in the authority hook. The evaluator re-checks the list and the operation names on every grant: the hook trusts these rows, so a bad row would be live escalation rather than cosmetics. Tests: 7 cases / 33 checks green (op-id anchors, wire names, accept/refuse sets). Verified: graphene_protocol + graphene_chain build clean (rc=0) with the new object type in the enum.
A principal's active authority is answered with its agent's active authority only when a live delegation row covers every authority-requiring operation of the transaction, the principal's own signatures do not already satisfy it, and the transaction asks for no master or regular authority. Signature recovery is deferred until a row exists, so ordinary transactions pay nothing extra. Chain-level tests (consensus_sim, BUILD_TESTNET): granted op, principal still signs, full-coverage rule (mutation-checked), master-mixed tx, missing/expired/deny-listed/unknown rows, no leak through nested account_auths.
Export the index and import it with an explicit setter for the shared_string operations list.
Rows go on both sides (principal and agent) in the same step as a master change (account_update, recover_account), an active change, a direct sale (buy_account) and an auction close. Regular-only updates keep the row: agents sign with active. Tests: principal active rotation and agent master rotation wipe the row (mutation-checked), regular-only change keeps it.
The auction-close path is left untested for now: bid extensions are measured by wall clock, which the virtual-clock harness cannot reach (fixed separately in fix-auction-wallclock).
… on grant The authority hook walks all of a principal's rows for each transaction the principal did not sign itself, and a rejected transaction pays no bandwidth, so the row count must be bounded. Every grant now removes the principal's expired rows before counting, so a dead row lives at most until the next grant and the total stays <= 16. Test: cap refuses the 17th agent, a re-grant is an overwrite, an expired row is swept and its slot reused (cap mutation-checked).
Possible now that #166 (auction extension from head block time) is in pm.
An agent is no longer a separate account but a record on the principal: agent_name (unique per principal) + agent_key + operation list + expiration. A transaction signed by the agent key passes on behalf of the principal when every operation is in the list. - one key per agent; reissue by name replaces the key; key reuse across names rejected - all agents wiped on master/active change, recovery, direct sale and auction close; regular-only change keeps them - snapshot imports agent_name/agent_key - 15 chain tests rewritten; mutations (coverage, wipe, key uniqueness) are caught
Returns a principal's agents (name, key, operation list, expiration) ordered by name, with expired rows flagged. At most CHAIN_AGENT_MAX_PER_ACCOUNT rows per principal, so no paging.
Agents carry an optional set of addons, e.g. ["vizhub"]: scopes external services read to accept the agent key for their own actions. The node stores them and never interprets them; they grant nothing on chain. At most 10, each shorter than 64 bytes, no ','. An addon-only agent is valid; revoke = both lists empty. Stored in the object, snapshot and get_agent_permissions.
…object The addons import landed in import_transactions (same expiration line) and used variant::contains, which does not exist; the Docker build failed on it.
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
A principal account can register agents: named keys that may broadcast a listed set of operations on the principal's behalf. An agent is not an account — it is a record on the principal:
agent_name(unique per principal,[a-z0-9_-]) +agent_key(public key) + operation list + expiration. A transaction signed by the agent key passes as the principal's when every authority-requiring operation is in the list. The principal's own keys are never shared.New operation
set_agent_permission(op-id 105):account,agent_name,agent_key,operations(wire names; empty = revoke),expiration(epoch = perpetual; past = revoke),addons(off-chain scopes such asvizhub, at most 10, each shorter than 64 bytes, not interpreted by the node and granting nothing on chain; an addon-only agent is valid; revoke = both lists empty). The key is required on grant, not on revoke. Requires the principal's active authority; rejected before HF15.Rules
set_agent_permission,proposal_create/update/delete,account_update,recover_account,change_recovery_account,set_account_price,set_subaccount_price,target_account_sale.database::_validate_transaction): only after HF15; the transaction needs no master/regular authority; the principal's own signatures do not already satisfy it; a live row covers every authority-requiring operation; the agent key must be among the signatures. No nested reach throughaccount_auths.CHAIN_AGENT_MAX_PER_ACCOUNT); a grant sweeps the principal's expired rows first.recover_account, direct sale, auction close. Regular-only changes keep them.agent_name/agent_key) is exported and imported (new object type → testnet redeploy from snapshot).Read API
get_agent_permissions(account)indatabase_api: the principal's agents with name, key, operations, expiration, addons and anexpiredflag.Tests
tests/pm/agent_access_test.cpp.tests/consensus_sim/scenarios/test_agent_access.cpp(16 cases, incl. addon-only agent grants nothing on chain, BUILD_TESTNET) — agent key carries the op, principal still signs, foreign key rejected, full coverage, master-mixed tx, invalid rows and revoke, nested authorities, key uniqueness and rotation, wipes on active/master change, kept on regular change, wipes on direct sale / auction close / recovery, cap + expired sweep.🤖 Generated with Claude Code