Skip to content

covenant-said-bridge: register, lookup, verify (live mainnet)#109

Open
mizuki0x wants to merge 14 commits into
mainfrom
feat/said-bridge-v2
Open

covenant-said-bridge: register, lookup, verify (live mainnet)#109
mizuki0x wants to merge 14 commits into
mainfrom
feat/said-bridge-v2

Conversation

@mizuki0x

Copy link
Copy Markdown
Contributor

Summary

Bridge between Covenant and SAID Protocol (Solana Agent Identity). Lets a Covenant agent register, get verified, and resolve identity through SAID's program + REST API. Replaces the paused #86 — that branch was forked 158 commits behind main and the said-sdk API has narrowed since.

Live mainnet, our agent on SAID:

What ships

  • covenant-said-bridge (Rust): Config, paid-gate guards, REST client for /api/agents/:wallet + /xchain/{inbox,free-tier,message}, SQLite-backed anchor cursor, subprocess worker invoker.
  • @covenant/said-bridge (TypeScript): subprocess worker that drives said-sdk for the on-chain instructions. status, register-agent, get-verified work live against 5dpw6KEQ… on mainnet today. submit-anchor and validate-work return a clean BridgeUnsupportedError envelope because they aren't in the published SDK surface (see below).
  • covenantd wiring: said_bridge: Option<SaidBridge> field, 10 dispatch handlers, IPC Said* variants, boot wiring in main.rs.
  • covenant said … CLI verbs: status, register, verify, validate-work, lookup, anchor, inbox, free-tier, send, anchor-status.
  • landing/public/.well-known/said-agent.json: hosted at https://opencovenant.org/.well-known/said-agent.json. Already served via f432f538 to main so register_agent could resolve it.

Architecture

  • Two keys, never collapsed. Operator key (~/.config/solana/id.json) signs Covenant settlement. SAID owner key (COVENANT_SAID_KEYPAIR) signs SAID instructions. Live run used the existing Covenant agent identity (covenant-agent.jsonAdChc…).
  • Hybrid wiring. In-process Rust for REST + xchain reads; subprocess TS worker for on-chain instructions, mirroring the JSON envelope contract used by @covenant/sap-bridge.
  • Per-instruction paid gates. Five env flags, all default off. An operator can fund the verification round without unlocking register or future anchor.
  • SQLite-backed anchor cursor persists every claim before submit and the confirmation after, so a crash mid-submit doesn't lose position. (Cursor logic lives in cursor.rs and is fully covered; the on-chain submit path is the part waiting on the SAID SDK.)

Notes for the SAID team (cc @kaiclawd)

Two real findings worth flagging:

  1. said-sdk@0.3.4 AGENT_ACCOUNT_SIZE is stale. The on-chain Agent account for our PDA is 342 bytes; the SDK's lookup/lookupByPDA reject anything that isn't 263 bytes, so freshly-registered agents look like they don't exist via the SDK. We have a working manual parse — happy to PR the SDK fix once we know the new authoritative struct layout (we read disc[8] | owner[32] | authority[32] | uri_len[u32] | uri[..] | registeredAt[i64] | isVerified[u8] | verifiedAt[i64] + trailing space). Our getAgent against AdChc… returned null from the SDK while the on-chain PDA reads as isVerified = true.
  2. submitAnchor and validateWork aren't exposed by the SDK. We built the full bridge surface and gates anyway and gracefully error today — if you ship those instructions, the bridge picks them up the moment we bump the peer dep.

What we deliberately don't do

  • No on-chain identity merge with Covenant's existing settlement program (separate program cov9UDyp…, separate slash authority).
  • No sponsor pool surface — removed early on said-bridge for lack of a use case.
  • No A2A transport rewrite — inbox / send are explicit CLI verbs, routing through covenant-a2a is a follow-up.

Test plan

  • cargo build (workspace) clean
  • cargo test -p covenant-said-bridge — 49/49 (30 unit + 11 worker_roundtrip + 8 rest_roundtrip)
  • cargo test -p covenantd --test said_dispatch — 21/21
  • pnpm -F @covenant/said-bridge typecheck + pnpm -F @covenant/said-bridge build — clean, dist/worker.js ships
  • Live REST against api.saidprotocol.com: lookup, free-tier, inbox confirmed shape-compatible (a few benign trailing fields the bridge ignores via serde(default))
  • Live mainnet register_agent against 5dpw6KEQ… on our AdChc… identity — PDA created, finalized
  • Live mainnet verify_agent (0.01 SOL fee) — finalized, isVerified=1 on chain
  • Live submit_anchor — blocked on SDK surface
  • Live validate_work — blocked on SDK surface

mizuki0x added 5 commits June 30, 2026 21:42
- agent_card/xchain: validate base58 wallet/address + chain charset before
  interpolating into the REST path (closes a path-injection vector)
- cursor: confirm requires tx_sig IS NULL, no silent overwrite; next_index
  uses checked_add for u64 saturation; new test pins the double-confirm
- anchor: refuse to submit when a prior claim is still pending so a failed
  worker leaves the cursor recoverable instead of poisoned; tracing::error
  before bubbling a confirm failure after a successful on-chain submit so
  the operator has the tx_sig to reconcile; atomic single-write JSONL line
- worker: cap subprocess stdout 1 MiB / stderr 256 KiB; first parseable
  envelope wins (a trailing log line no longer shadows a real success);
  ok=false always routes to Upstream with a Worker name fallback, leaving
  BridgeError::Rest reserved for the reqwest layer
- rest: cap response body via content-length pre-check + post-read guard
- config: reject COVENANT_SAID_API_BASE_URL that is not http(s) at parse
- worker (TS): stdin read times out (default 10s, env-overridable); stdin
  capped at 1 MiB; register-agent payload validated (string, non-empty,
  http(s) URL, ≤200 chars); BigInt-safe JSON replacer
- index (TS): said-sdk calls race a 30s timeout; getVerified fetches the
  real slot via getTransaction instead of hardcoding 0
- keypair (TS): validate JSON array shape, length=64, byte range before
  fromSecretKey; wrap web3 errors with a Covenant-prefixed message
- covenantd: said_status has_signer now stat()s the keypair file path,
  not just whether the env var is set; said_anchor + said_anchor_status
  refuse to materialise the cursor on a disabled bridge; said_anchor_status
  gains the same not-wired guard every other verb has; verb-prefix on
  said_anchor-status corrected
- packages/said-bridge: add README with the worker calling convention,
  the full env-var inventory, the cluster default + how to flip it, and
  the daemon vs shell modes a cold reader needs to onboard
- worker (TS): BridgePayloadError for input-shape failures so the error
  envelope's 'name' carries class info instead of generic 'Error'
- worker (TS): explicit 'expected a JSON payload on stdin' when a verb
  that needs a body is invoked with no stdin (previously the missing
  body surfaced as 'metadataUri must be a non-empty string' which sent
  cold users hunting for an argv format that doesn't exist)
- worker (TS): hasSigner now stat()s the keypair file instead of just
  checking env presence — a typo'd path no longer claims a usable signer
- config: accept COVENANT_SAID_ALLOW_PAID_VALIDATE_WORK alongside the
  legacy ..._VALIDATE so the gate name matches the verb (validate-work)
  on both Rust and TS sides
- zero em dashes in scope (was 9 across 3 test files); parentheticals or
  periods where the em dash narrated
- lib.rs module header drops the false 'off-chain register' claim that
  was deleted in c71db2c of the original said-bridge spike
- cursor pub fn docs trimmed from 2-3 narrating lines to single-line
  contract statements
- said_dispatch module header collapsed to 4 lines; one verbose stub-fn
  comment replaced with the single non-obvious why (whitespace-split env)
- rest_roundtrip module header + serve_once doc trimmed similarly
- worker error fallback reads 'worker error (no message)' instead of
  the polite-AI 'worker reported an error'
- said paths handler error says '$COVENANT_HOME is unset' instead of
  hedging with 'cannot resolve'
Matches the docs/integrations/krexa.md format. Covers the env
contract, the read-only vs paid-gated surface, the anchor cursor
invariants, and the said-sdk dependency posture (stale account-size
constant + missing submitAnchor/validateWork) so a reviewer has a
single place to read instead of grepping the diff.
@mizuki0x

Copy link
Copy Markdown
Contributor Author

Reviewer guide

Addressing the diff-size and review-surface notes: this PR is large because it touches a crate, a TS worker, IPC, the daemon, and the CLI in one cut. To make the review tractable, here is a path through the diff that mirrors how the bridge composes at runtime.

Start at the new docs. docs/integrations/said.md (now in this branch) has the env contract, the trust boundary, the anchor cursor invariants, and the explicit dependency posture on said-sdk. Most context questions a reviewer would have are answered there.

Then read in dependency order:

  1. agent-os/crates/covenant-said-bridge/src/config.rs — every env var. Defaults off everywhere.
  2. agent-os/crates/covenant-said-bridge/src/path.rs — base58 wallet + chain validation at the REST trust boundary. The two callers are agent_card.rs and xchain.rs.
  3. agent-os/crates/covenant-said-bridge/src/cursor.rs — SQLite anchor cursor. Two invariants worth pinning: confirm rejects double-write via WHERE tx_sig IS NULL, and anchor() refuses to proceed when a prior claim is still pending. Both pinned by cursor_tests + anchor::tests::anchor_live_worker_failure_leaves_claim_pending.
  4. agent-os/crates/covenant-said-bridge/src/worker.rs — subprocess transport. stdout cap 1 MiB, stderr cap 256 KiB, first parseable envelope wins so a trailing log line cannot shadow a real success. Any ok:false routes to Upstream, reserving BridgeError::Rest for the reqwest layer.
  5. agent-os/crates/covenant-said-bridge/src/rest.rs — REST client, 15s timeout default, response body cap with a Content-Length pre-check.
  6. packages/said-bridge/src/index.ts — said-sdk adapter. Wraps the SAID class for registerAgent + verifyAgent; submitAnchor and validateWork throw BridgeUnsupportedError until the SDK exposes them.
  7. packages/said-bridge/src/worker.ts — JSON envelope contract on stdin/stdout, 10s stdin timeout, 1 MiB stdin cap, per-command payload validation.
  8. agent-os/crates/covenantd/src/lib.rs — search for said_ to find the 10 dispatch handlers. Each handler is mirrored by a test in said_dispatch.rs.
  9. agent-os/crates/covenantd/tests/said_dispatch.rs — what the dispatch surface is contractually held to. 22 tests.

On the said-sdk dependency risk (acknowledged, but worth being explicit): the bridge surfaces the 4 paid verbs even though the SDK only ships 2 of them today. submit_anchor and validate_work ship with the full bridge wiring, the operator gates, the typed IPC variants, the cursor logic, and the worker plumbing in place. The only thing they cannot do is actually broadcast the on-chain instruction. They report BridgeUnsupportedError cleanly. The moment said-sdk publishes those methods, the bridge picks them up on a peer-dep bump. So the SDK risk is bounded to "two verbs are inert" rather than "two verbs are missing infrastructure."

The two findings going back to the SAID team (the stale AGENT_ACCOUNT_SIZE constant and the missing methods) are documented in the integration doc's Dependency posture section.

Test totals:

  • 35 unit + 8 REST roundtrip + 12 worker roundtrip in the crate.
  • 22 covenantd dispatch tests.
  • Workspace cargo build and cargo fmt --check both clean.
  • TS worker pnpm typecheck + pnpm build clean.

Happy to split this if the reviewer would rather see the crate alone first, then the daemon wiring as a follow-up. Left as a single PR because the dispatch tests in covenantd/tests/said_dispatch.rs exercise the wiring end-to-end and would land orphaned otherwise.

The harden round-3 reflow formatted the crate but missed the `covenant said`
CLI arm in crates/covenant/src/main.rs and the covenantd boot/dispatch wiring,
so cargo fmt --check flagged those hunks on CI. Reflow them with stable rustfmt
(1.8.0); scoped to the said-authored lines only. No behaviour change; the 22
said_dispatch tests still pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant