feat(beltic): end-to-end Beltic VC integration — identity + agent authorization - #278
Open
alivia-beltic wants to merge 19 commits into
Open
feat(beltic): end-to-end Beltic VC integration — identity + agent authorization#278alivia-beltic wants to merge 19 commits into
alivia-beltic wants to merge 19 commits into
Conversation
…verifier
New transport-neutral Rust crate wrapping Beltic's Credentials API. Models
on houston-composio (third-party-integration reference). 35 inline tests,
all green; no Tauri / React / axum coupling.
- errors.rs — typed BelticError mirroring Beltic's nested `{ error: {code,
message} }` envelope; from_envelope() mapper; is_retryable() for backoff
- config.rs — Configuration::from_env(); jwks_url() / status_list_url()
derived from BELTIC_BASE_URL so local Beltic just works
- client.rs — reqwest wrapper with X-Api-Key, JSON in/out, parses the
actual Beltic error envelope
- issuer.rs — issue / revoke / get + client-side FinCEN guard:
delegated_by_subject_id required on any wallet-scoped agent permission
- webhook_verifier.rs — Stripe-pattern Beltic-Signature: sha256=<hex>
+ Beltic-Timestamp with 300s replay window; constant-time compare
- verifier/ — split into mod/jwks/status_list/policy (200-line rule):
- jwks: cache w/ Cache-Control max-age, ES256 P-256 signature verify
- status_list: W3C Status List 2021 gzip + base64url bit lookup
- policy: lte/lt/gte/gt/eq/neq/in operators on permission conditions
Stores Beltic-issued credentials per agent (agent_authorization) and at the workspace level (user identity). Files-first per knowledge-base/ files-first.md: append-only Vec<VerifiableCredential> in .houston/credentials/credentials.json (agent) or .houston/identity/identity.json (workspace), reactive via the file watcher + WS event invalidation in chunk 3. - types.rs — VerifiableCredential (Beltic subject_id, claims, signed JWT, delegated_by_subject_id, status_list_index), CredentialStatus enum (Active/Suspended/Revoked/Expired) - store.rs — agent-scoped list/save/active/find_by_credential_id/ update_status. save() rejects duplicates by credential_id so retried issuance jobs don't produce phantom rows. update_status() auto-stamps revoked_at when transitioning to Revoked. - identity.rs — workspace-scoped equivalent for the user's `user` credential. Same shape; different folder. 22 inline tests passing (9 mine + 13 pre-existing in adjacent modules).
…ential*
Wires houston-beltic into the engine: REST + WS exposure. Events route
to `agent:{agent_path}` so the existing per-agent subscription picks them
up alongside ActivityChanged/SkillsChanged/etc.
- houston-ui-events: 3 new HoustonEvent variants — CredentialIssued,
CredentialRevoked, CredentialSuspended (all carry agent_path +
credential_id)
- houston-engine-protocol::event_topic: routes the new variants to
`agent:{agent_path}` so existing UI subscriptions get them
- routes/credentials.rs:
GET /v1/agents/credentials?agent_path=... list local
POST /v1/agents/credentials?agent_path=... issue via Beltic
POST /v1/agents/credentials/:id/revoke?agent_path=… revoke via Beltic
POST /v1/agents/credentials/:id/verify?agent_path=… local JWT-VC verify
- routes/webhooks_beltic.rs:
POST /v1/webhooks/beltic receive + verify
HMAC + propagate
status change to
the matching agent
- routes/beltic_shared.rs: lazy OnceLock-backed BelticContext (Client +
Issuer + Verifier from env) and map_beltic() error translator. Shared
by both route files.
- houston-beltic::IssueRequest now `Deserialize` so axum can parse it
from JSON. WebhookVerifier gains associated constants
SIGNATURE_HEADER/TIMESTAMP_HEADER for ergonomic access in handlers.
255 workspace tests passing (houston-beltic 35 + houston-engine-core 220).
Mirrors the Rust DTOs on the TS side so the desktop app can talk to the new credentials routes. Wire format snake_case (Beltic's convention) — no transform layer. - ui/core/src/types.ts: HoustonEvent union gains CredentialIssued / CredentialRevoked / CredentialSuspended variants (all carry agent_path + credential_id, matching the Rust event payload) - ui/engine-client/src/types.ts: VerifiableCredential, CredentialStatus, IssueCredentialRequest, VerifyCredentialResult - ui/engine-client/src/client.ts: 4 new HoustonClient methods — listAgentCredentials / issueAgentCredential / revokeAgentCredential / verifyAgentCredential — wrapping the engine REST surface - app/src/hooks/queries/use-agent-credentials.ts: TanStack Query hooks (useAgentCredentials, useActiveAgentCredential, useIssueAgentCredential, useRevokeAgentCredential, useVerifyAgentCredential). Mutations surface failures via showErrorToast() with Report-bug action per CLAUDE.md no-silent-failures - app/src/hooks/use-agent-invalidation.ts: wire the 3 new events to invalidate the ["agent-credentials", agentPath] query key
Adds two new Settings sub-nav items per the v2 Figma design (which I
realised matches the actual 3-pane Settings layout the app already uses,
not a card-heavy standalone page).
- settings-view.tsx: 2 new SettingsSectionId variants ("identity",
"agents"); 2 new nav items (ShieldCheck + Users icons); 2 new render
branches for the section components
- sections/identity.tsx: empty-state pane with a "Verify identity" CTA.
Disabled for now — workspace-identity routes land in a follow-up
alongside the consent modal
- sections/agents.tsx: lists every agent in the current workspace via
useAgentStore, one row per agent
- sections/agents-row.tsx: pulls per-agent credential via the chunk-4
useActiveAgentCredential hook, shows status pill, short credential id,
delegated_by_subject_id chain, and a revoke action. Confirm prompt
before mutate; tone-colored status pill
- locales en/es/pt: settings.nav.{identity,agents} +
settings.identity.* + settings.agents.* keys. check-locales clean,
no em dashes per i18n rule
`pnpm tsc --noEmit` clean. Mission Control card variants land in a
follow-up; they need extending @houston-ai/board status enum and are a
chunkier change.
Matches the Figma "Authorize this agent" screen. Opens from the Authorize button on each agent row in Settings → Authorized agents. Closes on successful issuance; the WS event invalidator (chunk 4) refreshes the row's status pill. - authorize-agent-dialog.tsx: modal with spend-limit fields (daily + per-transaction), currency chips (USD/BRL/EUR/GBP), ISO-8601 idle timeout, 3-mode confirmation rules (always / threshold / never) with inline threshold input, declaration checkbox. Submits via the useIssueAgentCredential mutation from chunk 4. - agents-row.tsx: when an agent has no active credential, shows an Authorize button that launches the modal. When it has one, shows the Revoke button (same behaviour as before). - agents.tsx: passes the new agentId prop down. - locales en/es/pt: settings.agents.consent.* keys covering every string in the modal. check-locales clean. Known placeholders (called out in code comments) — chunk 8 follow-ups: - subject.id is `did:jwk:houston-<agentId>` for now; a real ES256 keypair (generated server-side, key stored encrypted) lands with the identity flow - claims.delegated_by_subject_id is `usr_houston_local` placeholder; pulls from the workspace identity credential once chunk 8 wires identity routes
Mission cards now render a "Verified by Beltic" tag when their owning agent has an active agent_authorization credential. Cheapest possible touchpoint: uses the existing KanbanItem.tags array (which the board already renders as gray pills below the card) — no library boundary crossing into @houston-ai/board. - use-verified-agent-paths.ts: cross-agent fan-out via TanStack useQueries. Returns the Set<string> of agent paths whose newest agent_authorization row is active. 30s staleTime since Mission Control isn't the credentials-focused surface (and the WS event invalidator keeps it honest anyway) - use-mission-control.ts: looks up each card's owning agent in the verified set and appends the tag when present A future enhancement is a Beltic-tinted tag tone — that'd take a `tagTone` prop on KanbanItem and is left for a follow-up so this chunk stays inside the app boundary.
End-to-end Beltic user-credential flow: route → hooks → modal → wired
Identity Settings pane.
- engine-server/src/routes/identity.rs:
GET /v1/identity current Beltic user credential or null
POST /v1/identity issue (build Beltic IssueRequest from
nationality, dob, id document fields and
persist via houston_engine_core::credentials::identity)
POST /v1/identity/revoke call Beltic revoke + update_status
Auto-promotes trust_level to "idv_verified" when an id_document_type
is set; validates that an ID type without a country is rejected
client-side (matches Beltic schema constraint).
- ui/engine-client: IssueIdentityRequest type + 3 HoustonClient methods
- app/src/hooks/queries/use-identity.ts: useIdentity / useIssueIdentity
/ useRevokeIdentity TanStack hooks. Mutations surface failures via
showErrorToast() with Report-bug button per CLAUDE.md.
- app/src/hooks/use-agent-invalidation.ts: CredentialIssued/Revoked/
Suspended events with agent_path prefixed "identity:" route to the
workspace-identity query key instead of the per-agent one.
- app/src/components/settings/sections/identity.tsx: real state.
Empty → shows verify CTA + opens the modal. Verified → renders
status pill, trust level, credential id, issued/expires, and the
Re-verify / Revoke buttons. Revoke uses window.confirm with the
cascading-warning copy.
- verify-identity-dialog.tsx: matches the Figma "Verify Your Identity"
fields (nationality, DOB, optional document type+country) with the
three Beltic declarations folded into one consent checkbox. Submits
via useIssueIdentity.
- locales en/es/pt: settings.identity.verify.* + new
identity.revokeConfirm key. No em dashes.
The placeholder user_id from chunk 6 (delegated_by_subject_id =
"usr_houston_local") will be replaced in a follow-up that wires the
real user subject_id from this identity credential. Both routes work
independently for now — agents authorize fine without an identity, and
identity issues fine without any agents.
…g Beltic key for dev
Two related changes that finalise the agent_authorization flow for the
dev demo.
1. authorize-agent-dialog reads useIdentity and uses the active identity
credential's subject_id as delegated_by_subject_id. If no identity is
active, the modal shows an amber warning pointing to Settings →
Identity → Verify, and the submit button is disabled. Previously the
modal sent a "usr_houston_local" placeholder, which is the kind of
thing Beltic's FinCEN AML check is meant to catch.
2. app/src-tauri/src/lib.rs now seeds BELTIC_API_KEY +
BELTIC_BASE_URL into the engine sidecar env at spawn time, but ONLY
in debug builds (cfg!(debug_assertions)). Release builds are stripped
by the compiler — no literal staging key ships to users. A parent-
shell `export BELTIC_API_KEY=…` still wins via a follow-up env
pass-through block. Key has read/write/revoke/verify scopes, no
delete; staging only.
locales en/es/pt: settings.agents.consent.identityRequired{Title,Body}
+ delegatingFrom. check-locales clean.
Houston UI sends a `did:jwk:houston-<agentId>` placeholder when issuing an agent_authorization credential. The engine route now detects that placeholder, mints a fresh ES256 (P-256) keypair, builds a canonical did:jwk, attaches the public JWK to the subject, and persists the private JWK at `.houston/agent_did/agent_did.json` (mode 0600 on Unix) before calling Beltic. Issuance success therefore always corresponds to a keypair Houston can actually use later for presentation flows (W3C VPs, AP2/UCP). Non-placeholder subjects and identity credentials are left untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…x verify dialog i18n keys
Chunk 5 created the section files but never registered them in
SettingsView, so the new sub-nav was never visible. Adds them at the
top of the Settings sidebar (BadgeCheck + ShieldCheck icons) so users
can actually reach the Beltic flows.
VerifyIdentityDialog was calling `t("verify.title")` etc., but the
locale JSON nests those keys under `identity.verify.*`, so every label
rendered as the raw key path. Repoint all keys to `identity.verify.*`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ment Replaces the placeholder dialog with a designed-system version and adds a drag-drop file picker so users can attach supporting documents (passport, driver's license, national ID, residence permit) when verifying their identity. Each document is hashed locally with SHA-256; only the hash + document type + filename leave the machine, encoded as opaque `sha256:<hex>:<doctype>:<urlencoded-filename>` entries in `evidence_refs[]`. Beltic stores those strings verbatim today; when the Beltic /v1/evidence endpoint ships (see beltichq/platform PR gethouston#179), the ref format will switch to `evidence:<id>` and the JWT-VC will gain a W3C evidence[] claim with digestSRI. App side: - VerifyIdentityDialog rewritten to use @houston-ai/core Input + Select, a Card-styled drop-zone with hover/drag affordances, and per-file rows showing filename, size, sha256 prefix, doc-type picker, remove. - Accepts PDF/JPG/PNG/WEBP/HEIC up to 10 MB. SubtleCrypto SHA-256 runs in the renderer; submit blocks while any file is still hashing. - Identity panel grows a "Verified with" row that parses `sha256:...:...:...` refs and renders document type + filename + hash prefix per attached document. - New i18n keys identity.verifiedWith, identity.noEvidence, identity.verify.{nationality,dob}Hint, identity.verify.evidence.* in en/es/pt. No em dashes per locale validator. Engine side: - IssueIdentityRequest gains an optional `evidence_refs: Vec<String>`, forwarded verbatim onto the Beltic IssueRequest. - engine-client TS types updated on both VerifiableCredential (response) and IssueIdentityRequest (request). Out of scope (next session, paired with Beltic PR gethouston#179 follow-up): - Engine route to mirror evidence bytes to local `.houston/identity/evidence/<sha256>.<ext>` for offline audit - Swap ref format from `sha256:...` to `evidence:<id>` once Beltic endpoint is live; render `evidence:<id>` entries by fetching metadata from Beltic instead of decoding the opaque string Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an engine route POST /v1/identity/evidence that takes raw bytes + sha256 + content_type query params and writes them to <home>/.houston/identity/evidence/<sha256>.<ext> (mode 0600 on Unix). The engine re-hashes the body and rejects on sha256 mismatch so a buggy renderer can't poison a "trusted" content address. Verify dialog now persists every attached document via this route BEFORE calling issueIdentity. If any persist fails (network, disk full, hash mismatch), the issuance is aborted and the user sees a toast — preferable to a credential whose evidence_refs name files that aren't on disk. This is the local-mirror half of the W3C evidence story. When Beltic's /v1/evidence endpoint ships (beltichq/platform PR gethouston#179 follow-up), the engine route will additionally upload these same locally-mirrored bytes server-side, no client changes needed. Added: - engine-core::credentials::evidence_store — content-addressed writer with sha256 + content-type → path validation. 8 inline tests cover round-trip, path-traversal guards (rejects `..` in sha256), 0600 permissions, content-type → extension mapping, overwrite semantics. - Engine route handler verifies sha256 server-side via sha2 crate. - HoustonClient.persistIdentityEvidence(bytes, {sha256, contentType}) client method using application/octet-stream upload. - Verify dialog holds File refs through hashing and POSTs bytes on submit. Issuance is aborted on any persist failure. - New i18n key identity.verify.evidence.persistFailed in en/es/pt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Reveal button per evidence row in the Identity panel. Clicking
it locates the locally-mirrored file by SHA-256 and opens Finder with
it selected.
The credential's evidence_refs[] carries the sha256 but not the
content_type the file was saved under, so the engine globs the
evidence dir for any `<sha256>.<anything>` match. Saved files include
the content-type-derived extension (.pdf, .jpg, etc.), so the glob
finds them regardless of the original filename.
Added:
- engine-core::credentials::evidence_store::locate_by_sha256(root, hex)
→ Option<PathBuf>. Validates the hex shape (rejects `..` and other
non-hex chars) before touching the filesystem so a crafted query
param can't traverse. 3 new tests bring the file to 11/11.
- Engine route GET /v1/identity/evidence?sha256=<hex> returning
{path: "/absolute/path"} or 404.
- HoustonClient.locateIdentityEvidence(sha256) → {path}.
- Identity panel evidence row grows a FolderOpen icon button that
resolves the path via the engine then calls osRevealPath (the
existing Tauri command for Finder reveal / Explorer /select).
- New i18n keys identity.{revealEvidence,revealEvidenceAria,
revealEvidenceFailed} in en/es/pt. No em dashes per locale validator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…:<id> refs End-to-end ref-format swap. When Beltic's /v1/evidence endpoint is reachable (beltichq/platform PR gethouston#179 deployed to staging), the engine now forwards attached identity-evidence bytes to Beltic alongside the local mirror. The Beltic-returned `ev_<uuid>` is used as `evidence:<id>` in the issued credential's evidence_refs[], which triggers the W3C `evidence[]` block embedding in the JWT-VC (PR gethouston#179 chunk 11d). Best-effort upstream: when the Beltic endpoint isn't reachable yet (staging hasn't deployed), the engine logs at WARN and returns the local-mirror path without `beltic_evidence_id`. The renderer falls back to the existing `sha256:<hex>:<doctype>:<filename>` opaque shape, which Beltic also stores verbatim. Re-running the same submit once /v1/evidence is live is a no-op — Beltic's sha256-dedupe path returns the existing resource on second upload. houston-beltic crate: - reqwest gains the `multipart` feature - Client.post_multipart<Res>(path, form) — single multipart helper, inlined here rather than as a generic since evidence is the only consumer. - Issuer.upload_evidence(bytes, content_type, document_type?, filename?) returns EvidenceResource. Both `document_type` and `filename` ride as multipart text fields per the Beltic schema (mirrors the curl example in the Mintlify guide). - EvidenceResource type mirrors Beltic's flat JSON shape (ADR-002). houston-engine-server: - POST /v1/identity/evidence query params gain optional `document_type` and `filename` so the Beltic upload carries the metadata. Response gains optional `beltic_evidence_id`. - After the local save succeeds, attempts the upstream upload via beltic_ctx().issuer.upload_evidence. Failures log at WARN and do NOT abort the route — the user's local copy is preserved either way. App side: - HoustonClient.persistIdentityEvidence gains `documentType` + `filename` args; return shape grows optional `beltic_evidence_id`. - VerifyIdentityDialog uses the new return value: when Beltic responded with an id, ref is `evidence:<id>`; otherwise falls back to `sha256:<hex>:...`. The fallback is transparent and survives a later re-attempt. - Identity panel `parseEvidenceRefs` recognizes both formats; `evidence:<id>` entries render with a truncated `evidence:` token instead of `sha256:`. Reveal-in-Finder button is hidden for evidence:<id> refs since the local-mirror key isn't recoverable from the ref alone (no sha256 inline). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Select component above the drop-zone showing which document
type the next dropped file will be tagged as (default: passport).
Users change the type between drops to upload different categories
in one session. The drop-zone prompt echoes the current selection
so it's clear what the next drop becomes.
Per-row dropdown stays as a quiet safety net — most users will
rely on the pre-select, but if they drop something and realize it
was tagged wrong, they can fix it inline without removing and
re-adding the file.
i18n: new keys identity.verify.evidence.{nextDocType, tagAs} in
en/es/pt. tagAs is a templated string ("Tagged as {{type}}. Change
above to upload a different type.") so the live preview reads
naturally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds knowledge-base/beltic-integration.md documenting Houston's side of the Beltic credentials integration, the wire-format fallback chain (evidence:<id> preferred → sha256:<hex>:... fallback), and exactly which Beltic PRs unblock which Houston features. The Houston-side code is unchanged — this is just the deploy-dependency documentation flagged as the last deferred item in the Beltic PR series. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Passive lockfile update — no Rust code changed this session. Captures the mime_guess 2.0.5 transitive that picked up during incidental cargo metadata resolution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Adds Beltic verifiable-credentials issuance to Houston end-to-end:
evidence_refs.delegated_by_subject_id), spending limits, and a freshly-minted ES256did:jwksubject + private keypair persisted at~/.houston/workspaces/<W>/<Agent>/.houston/agent_did/agent_did.json(mode 0600).Targets the Beltic staging API (
api.staging.beltic.com/v1); a dev-onlycfg!(debug_assertions)block inapp/src-tauri/src/lib.rsinjects the staging key.Pairs with beltichq/platform PR #179 which adds the evidence storage primitives (Prisma
Evidencemodel +EvidenceRepository+ S3 storage service +credentials:evidence:uploadpermission). When that PR's follow-up (Hono route + JWTevidence[]embedding) lands, Houston will swap the opaquesha256:...evidence refs forevidence:<id>refs in a one-line change.What's in this PR (commits, oldest → newest)
cad9f16houston-belticcrate: REST client, issuer (issue/revoke), local JWT-VC verifier (JWKS + Status List 2021), webhook HMAC verifiere75918bhouston-engine-corecredentials module: persist VCs in.houston/credentials/, identity helpers at workspace root45ae465HoustonEvent::Credential*for UI invalidation850d79c0875eb5@houston-ai/engine-clientTS types + methods + agent-credentials TanStack Query hooks0bf62e3SettingsView)1e58567052ef3e7e2e6ec4a14874delegated_by_subject_idto live identity credential; bake staging Beltic key for devedbfa52did:jwkfor agents on credential issuance; persist private JWK 06002769416616948cArchitecture highlights
.houston/and surfaced via TanStack Query keys invalidated byHoustonEvent::Credential*. A separate file watcher catches bypass writes so direct edits show up in the UI without refresh.houston-belticcrate. The engine-server thin-wraps it; the app side has no Beltic dependency, only the engine-client TS types.ui/@houston-ai/*. The consent + verify dialogs and identity sections live inapp/src/components/settings/per the project-rule that domain-specific UI stays inapp/.BelticError → CoreError → ApiError → errorMessage(err) → toast. Beta-stage policy: surface, don't swallow.Out of scope (next session)
~/.houston/workspaces/<W>/.houston/identity/evidence/<sha256>.<ext>for local offline auditsha256:...toevidence:<id>once beltichq/platform PR Provider config loading fails with type mismatch error #179 + its follow-up land in stagingTest plan
cargo test --workspace— all crates greencd app && pnpm tsc --noEmit— clean (relay typecheck errors are pre-existing, unrelated)cd app && pnpm check-locales— en/es/pt in sync, no em dashescargo build -p houston-engine-server— staged sidecar freshpnpm tauri dev— Verify identity flow round-trips againstapi.staging.beltic.com/v1, agent authorization mints a realdid:jwkand persists 0600 private JWKevidence_refs[]on the resulting credential row.houston/agent_did/agent_did.jsonexists with mode-rw-------and a valid P-256 keypair🤖 Generated with Claude Code