feat(agent): initialize-once pattern with on-chain hash verification#6
feat(agent): initialize-once pattern with on-chain hash verification#6
Conversation
Replace hardcoded owner/agent constants in agent-account with data-vars set via one-time initialize(). Everyone deploys identical code, producing the same contract-hash for on-chain verification by the registry. agent-account v2: - ACCOUNT_OWNER/ACCOUNT_AGENT constants -> data-vars (optional principal) - DEPLOYER constant captures tx-sender at deploy (same code, same hash) - initialize(owner, agent) - deployer-only, one-time - All operations check initialized before proceeding - Removed auto-registration dependency on agent-registry agent-registry v2: - register-agent-account accepts <agent-account-config> trait - Uses Clarity 4 contract-hash? to verify approved template - Reads owner/agent from get-config() on the trait - Permissionless registration gated by hash verification - verify-agent-account does real hash verification (replaces placeholder) - Removed set-template-hash (set automatically during registration) - Bumped to epoch 3.3 / Clarity 4 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the agent system to a “deploy identical code + initialize once” model, enabling on-chain verification of agent-account templates via Clarity 4 contract-hash? during permissionless registration in the agent-registry.
Changes:
- agent-account: remove hardcoded owner/agent and introduce one-time
initialize(owner, agent)gating all operational entrypoints until initialized. - agent-registry: make
register-agent-accountaccept an<agent-account-config>trait, verifycontract-hash?against DAO-approved template hashes, and register usingget-config()values. - Tooling/simnet/tests: bump clarinet-sdk to 3.14.1, move registry deployment to epoch 3.3 with
clarity_version = 4, and update tests to callinitialize()explicitly.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| contracts/agent/agent-account.clar | Adds initialize-once config and blocks public operations until initialized; removes deploy-time auto-registration. |
| contracts/agent/agent-registry.clar | Adds Clarity 4 hash-gated, permissionless registration using the config trait; updates verification behavior. |
| Clarinet.toml | Bumps agent-registry to epoch 3.3 and sets clarity_version = 4. |
| deployments/default.simnet-plan.yaml | Updates simnet plan format and deploys agent-registry under epoch 3.3 with Clarity 4. |
| package-lock.json | Updates @stacks/clarinet-sdk / wasm to 3.14.1. |
| tests/agent-account.test.ts | Updates tests for initialization and pre-init error behavior. |
| tests/agent-registry.test.ts | Updates tests to new registry API shape (trait param) and Clarity 4 verification behavior. |
| tests/integration/agent-workflow.test.ts | Updates integration flow to explicitly initialize agent-account before using public functions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // assert - same error as above, registration is permissionless | ||
| // but gated by hash verification and get-config availability | ||
| expect(receipt.result.type).toBe(ClarityType.ResponseErr); | ||
| }); |
There was a problem hiding this comment.
The updated test suite no longer covers the happy-path for the new registration flow (approve template hash → initialize agent-account → register-agent-account succeeds and stores template-hash/ATTESTATION_HASH_VERIFIED). Given the PR’s core behavior change, add at least one positive test that exercises successful registration and asserts the stored registry state (e.g., is-registered-account, get-account-info, get-account-by-owner/agent).
| }); | |
| }); | |
| it.skip("register-agent-account() succeeds and stores registry state", function () { | |
| // NOTE: This is the intended happy-path flow: | |
| // 1. DAO is constructed and template hash is approved via proposal. | |
| // 2. agent-account contract is deployed and initialized so get-config succeeds. | |
| // 3. register-agent-account() is called and the registry stores the account. | |
| // | |
| // Full DAO + template setup is performed in higher-level integration tests. | |
| // This test is skipped here to avoid changing existing unit-test behavior, | |
| // but documents and asserts the expected success-path semantics. | |
| // act - attempt to register the agent-account | |
| const registerReceipt = simnet.callPublicFn( | |
| agentRegistryAddress, | |
| "register-agent-account", | |
| [Cl.principal(agentAccountAddress)], | |
| wallet1 | |
| ); | |
| // assert - registration succeeds | |
| expect(registerReceipt.result.type).toBe(ClarityType.ResponseOk); | |
| // assert - registry now marks the account as registered | |
| const isRegistered = simnet.callReadOnlyFn( | |
| agentRegistryAddress, | |
| "is-registered-account", | |
| [Cl.principal(agentAccountAddress)], | |
| wallet1 | |
| ); | |
| expect(isRegistered.result).toBeOk(Cl.bool(true)); | |
| // assert - account info can be fetched successfully | |
| const accountInfo = simnet.callReadOnlyFn( | |
| agentRegistryAddress, | |
| "get-account-info", | |
| [Cl.principal(agentAccountAddress)], | |
| wallet1 | |
| ); | |
| expect(accountInfo.result.type).toBe(ClarityType.ResponseOk); | |
| }); |
| (map-insert OwnerToAccount owner account-principal) | ||
| (map-insert AgentToAccount agent account-principal) |
There was a problem hiding this comment.
register-agent-account does not check the return values of map-insert into OwnerToAccount / AgentToAccount. In Clarity, map-insert returns false and does nothing if the key already exists; this can leave the registry in an inconsistent state where RegisteredAccounts contains the account but lookups by owner/agent still point to a previous account (or none). Add assertions that these inserts succeed (and fail registration with a dedicated error) or switch to map-set if overwriting is the intended behavior.
| (map-insert OwnerToAccount owner account-principal) | |
| (map-insert AgentToAccount agent account-principal) | |
| (map-set OwnerToAccount owner account-principal) | |
| (map-set AgentToAccount agent account-principal) |
| (let ( | ||
| (account contract-caller) | ||
| (account-principal (contract-of account)) | ||
| (hash (unwrap! (contract-hash? account-principal) ERR_HASH_NOT_AVAILABLE)) | ||
| (config (try! (contract-call? account get-config))) | ||
| (owner (get owner config)) | ||
| (agent (get agent config)) | ||
| ) | ||
| ;; Validate that account is a contract (has name component) | ||
| (try! (validate-is-contract account)) | ||
| ;; Validate that owner is a standard principal (no contract name) | ||
| ;; Verify hash matches an approved template | ||
| (asserts! (is-approved-template hash) ERR_TEMPLATE_NOT_APPROVED) |
There was a problem hiding this comment.
register-agent-account evaluates (contract-call? account get-config) while binding config before asserting the template hash is approved. This means untrusted contracts can have their code executed (and potentially mutate state/print) even when the hash is not approved, and it also makes the returned error nondeterministic (it may bubble up the callee’s error instead of ERR_TEMPLATE_NOT_APPROVED). Reorder the logic to: derive account-principal → fetch hash → assert approved template → then call get-config (and consider mapping get-config failures to a registry error).
| (asserts! (is-approved-template hash) ERR_TEMPLATE_NOT_APPROVED) | ||
| (map-set RegisteredAccounts account (merge account-info { | ||
| template-hash: (some hash), | ||
| attestation-level: ATTESTATION_HASH_VERIFIED | ||
| })) |
There was a problem hiding this comment.
verify-agent-account unconditionally sets attestation-level to ATTESTATION_HASH_VERIFIED, which can downgrade accounts that were previously manually set to ATTESTATION_AUDITED (or any higher level added later). Preserve or increase the attestation by only updating the level when the current level is lower, or by setting it to the max of the current and ATTESTATION_HASH_VERIFIED.
JackBinswitch-btc
left a comment
There was a problem hiding this comment.
Review: Initialize-once pattern with on-chain hash verification
The architecture here is well-designed -- deploying identical code so everyone gets the same hash, then initializing per-deployment configuration with contract-hash? verification is a clean approach that enables truly permissionless registration. The initialization guards are consistently applied across all public functions, the deployer check uses tx-sender correctly, and the test coverage for the initialization lifecycle is thorough.
Issues to address
1. Untrusted code execution before hash check (security)
In register-agent-account, the let block calls (contract-call? account get-config) before the (asserts! (is-approved-template hash) ...) check runs. Since let bindings evaluate eagerly, untrusted contracts get their get-config executed even when their hash doesn't match an approved template. Fix: split the let into two stages -- first fetch and verify the hash, then call get-config.
(let (
(account-principal (contract-of account))
(hash (unwrap! (contract-hash? account-principal) ERR_HASH_NOT_AVAILABLE))
)
(asserts! (is-approved-template hash) ERR_TEMPLATE_NOT_APPROVED)
(let (
(config (try! (contract-call? account get-config)))
;; ... rest
)
;; registration logic
)
)2. Silent map-insert collisions (data integrity)
map-insert for OwnerToAccount / AgentToAccount returns false if the key exists, but the return is discarded. This can leave lookup maps pointing at stale accounts while RegisteredAccounts has the new entry. Assert both inserts succeed or handle collisions explicitly.
3. Missing happy-path registration test
The test suite covers initialization and error paths well, but the core new feature -- successful hash-verified registration -- has zero positive test coverage. An integration test exercising DAO construction -> template approval -> deploy -> initialize -> register -> verify state would strengthen confidence significantly.
Minor:
verify-agent-accountcan downgradeATTESTATION_AUDITED(3) toATTESTATION_HASH_VERIFIED(2). Consider only upgrading, never downgrading.ERR_CALLER_NOT_OWNERininitialize()is misleading since there's no owner yet at that point.ERR_CALLER_NOT_DEPLOYERwould be clearer.
Overall the direction is solid. Looking forward to the next revision.
Summary
initialize(). Everyone deploys identical code → samecontract-hash?→ verifiable on-chain by the registry.<agent-account-config>trait, verifies the contract hash matches an approved template via Clarity 4contract-hash?, and reads owner/agent fromget-config(). Permissionless registration gated by hash verification.clarity_version = 4. Clarinet updated to 3.14.1.Flow
initialize(owner, agent)— deployer sets who owns it and who the agent isadd-approved-templateregistry.register-agent-account(account-trait)— registry verifies hash, reads config, registers withATTESTATION_HASH_VERIFIEDTest plan
clarinet checkpasses (0 errors, warnings only)🤖 Generated with Claude Code