diff --git a/internal/agentruntime/charts.go b/internal/agentruntime/charts.go index 60d73549..0cb03414 100644 --- a/internal/agentruntime/charts.go +++ b/internal/agentruntime/charts.go @@ -5,12 +5,14 @@ package agentruntime // deployments. It MUST be updated as a single edit; bumping it here // updates every consumer in lockstep. // -// Chart 0.3.2 ships remote-signer image `v0.3.0`, which emits canonical -// Ethereum recovery-id signatures (`v=27/28`) from `/sign/.../message`, -// `/sign/.../typed-data`, and `/sign/.../hash`. Earlier images returned -// `v=0/1` (alloy y-parity), which was rejected by EIP-712 / ERC-3009 -// verifiers like USDC `transferWithAuthorization` and forced the buy.py -// caller to renormalize. +// Chart 0.3.3 ships remote-signer image `v0.4.0`, which honours +// `SIGNER__AUTH__TOKEN` (the bearer token the controller mints into the +// keystore Secret and injects via env). Canonical Ethereum recovery-id +// signatures (`v=27/28`) from `/sign/.../message`, `/sign/.../typed-data`, +// and `/sign/.../hash` have been the baseline since chart 0.3.2 / image +// v0.3.0 — earlier images returned `v=0/1` (alloy y-parity) and forced +// the buy.py caller to renormalize for EIP-712 / ERC-3009 verifiers like +// USDC `transferWithAuthorization`. // // renovate: datasource=helm depName=remote-signer registryUrl=https://obolnetwork.github.io/helm-charts/ -const RemoteSignerChartVersion = "0.3.2" +const RemoteSignerChartVersion = "0.3.3" diff --git a/internal/embed/embed_crd_test.go b/internal/embed/embed_crd_test.go index e8b45085..9964b65d 100644 --- a/internal/embed/embed_crd_test.go +++ b/internal/embed/embed_crd_test.go @@ -875,6 +875,8 @@ func TestServiceOfferControllerSecretRBAC_Scoped(t *testing.T) { readNames := map[string]bool{} deleteNames := map[string]bool{} + updateNames := map[string]bool{} + patchNames := map[string]bool{} var sawCreate bool for _, r := range rules { rm := r.(map[string]any) @@ -908,9 +910,25 @@ func TestServiceOfferControllerSecretRBAC_Scoped(t *testing.T) { if verbs["delete"] { deleteNames[n] = true } + if verbs["update"] { + updateNames[n] = true + } + if verbs["patch"] { + patchNames[n] = true + } } - if verbs["list"] || verbs["watch"] || verbs["update"] || verbs["patch"] { - t.Error("serviceoffer-controller scoped secrets rule must not grant list/watch/update/patch — Secrets are create-only in the reconciler and all reads are by name") + if verbs["list"] || verbs["watch"] { + t.Error("serviceoffer-controller scoped secrets rule must not grant list/watch — all reads are by name") + } + // update/patch is allowed only on remote-signer-keystore, which the + // reconciler updates via backfillSignerAuthToken to add the bearer + // token key to keystores minted before signer auth existed. + if verbs["update"] || verbs["patch"] { + for n := range names { + if n != "remote-signer-keystore" { + t.Errorf("serviceoffer-controller must not grant secrets:update/patch on %s — only remote-signer-keystore is mutated (auth-token backfill)", n) + } + } } if names["litellm-secrets"] && verbs["delete"] { t.Error("serviceoffer-controller must not grant secrets:delete on litellm-secrets; the code only reads LITELLM_MASTER_KEY") @@ -930,6 +948,16 @@ func TestServiceOfferControllerSecretRBAC_Scoped(t *testing.T) { t.Errorf("serviceoffer-controller must grant resourceName-scoped secrets:delete on %s for agent teardown", name) } } + // backfillSignerAuthToken (agent_wallet.go) calls Update on the keystore + // Secret to add the signer-auth bearer token to legacy keystores. Without + // update + patch the Agent stays in Provisioning and every downstream + // ServiceOffer condition blocks on WaitingForAgent. + if !updateNames["remote-signer-keystore"] { + t.Error("serviceoffer-controller must grant resourceName-scoped secrets:update on remote-signer-keystore for backfillSignerAuthToken") + } + if !patchNames["remote-signer-keystore"] { + t.Error("serviceoffer-controller must grant resourceName-scoped secrets:patch on remote-signer-keystore for backfillSignerAuthToken") + } if !sawCreate { t.Error("serviceoffer-controller must retain secrets:create for minting the per-agent API token + wallet keystore in dynamic namespaces") } diff --git a/internal/embed/infrastructure/base/templates/x402.yaml b/internal/embed/infrastructure/base/templates/x402.yaml index d3209cec..496d59c4 100644 --- a/internal/embed/infrastructure/base/templates/x402.yaml +++ b/internal/embed/infrastructure/base/templates/x402.yaml @@ -143,16 +143,19 @@ rules: # ServiceAccount + PVC are namespace-scoped child resources. # # Secrets: the reconciler touches exactly three, by name: - # - litellm-secrets get (read LITELLM_MASTER_KEY, ns llm) - # - hermes-api-server get/delete/create (mint the agent API token) - # - remote-signer-keystore get/delete/create (mint the agent wallet keystore) - # Secrets are create-only in the reconciler (isCreateOnlyKind): the API token - # is preserved, not rotated, and the keystore is minted once and never - # reshaped, so no `update`/`patch` verb is needed. `litellm-secrets` is - # get-only; delete is confined to the two per-agent Secret names that teardown - # owns. `create` is split into its own rule below: resourceNames cannot scope - # create, and agent namespaces are minted dynamically, so create stays - # namespace-wide. That is an integrity surface only. + # - litellm-secrets get (read LITELLM_MASTER_KEY, ns llm) + # - hermes-api-server get/delete/create (mint the agent API token) + # - remote-signer-keystore get/update/patch/delete/create (mint + backfill auth token) + # `hermes-api-server` is create-only: the token is preserved, not rotated. + # `remote-signer-keystore` needs `update`/`patch` for a one-shot backfill + # path (backfillSignerAuthToken in agent_wallet.go) that adds the + # signer-auth bearer-token key to keystores minted before signer auth + # existed. Idempotent: presence of the key (even empty) is a no-op, so + # operator-rotated tokens are never clobbered. `litellm-secrets` is + # get-only; delete is confined to the two per-agent Secret names that + # teardown owns. `create` is split into its own rule below: resourceNames + # cannot scope create, and agent namespaces are minted dynamically, so + # create stays namespace-wide. That is an integrity surface only. - apiGroups: [""] resources: ["serviceaccounts"] verbs: ["create"] @@ -173,8 +176,12 @@ rules: verbs: ["get"] - apiGroups: [""] resources: ["secrets"] - resourceNames: ["hermes-api-server", "remote-signer-keystore"] + resourceNames: ["hermes-api-server"] verbs: ["get", "delete"] + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["remote-signer-keystore"] + verbs: ["get", "update", "patch", "delete"] - apiGroups: [""] resources: ["secrets"] verbs: ["create"] diff --git a/internal/serviceoffercontroller/agent_wallet.go b/internal/serviceoffercontroller/agent_wallet.go index b0e61e36..08822417 100644 --- a/internal/serviceoffercontroller/agent_wallet.go +++ b/internal/serviceoffercontroller/agent_wallet.go @@ -15,11 +15,12 @@ import ( // Constants for the per-agent remote-signer side-stack. Image pinned by // digest is desirable but the chart still publishes by tag — keeping the // version synced with agentruntime.RemoteSignerChartVersion's notes -// (chart 0.3.2 → image v0.3.0, the canonical recovery-id behaviour). +// (chart 0.3.3 → image v0.4.0, the first image that honours +// SIGNER__AUTH__TOKEN). const ( remoteSignerName = "remote-signer" remoteSignerPort = 9000 - remoteSignerImage = "ghcr.io/obolnetwork/remote-signer:v0.3.0" + remoteSignerImage = "ghcr.io/obolnetwork/remote-signer:v0.4.0" // Image hard-codes /data/keystores as the default and reads its // config under the SIGNER__... env namespace; values picked to match // the master agent's working config in hermes-obol-agent. @@ -34,8 +35,8 @@ const ( // Bearer token for the signer's REST API. Injected into the signer // as SIGNER__AUTH__TOKEN and into Hermes as REMOTE_SIGNER_TOKEN — // defense-in-depth on top of the agent-isolation NetworkPolicy. - // Signer images < v0.4.0 ignore the env, so injection is a safe - // no-op until the image pin advances. + // Honoured by signer image v0.4.0+ (the version chart 0.3.3 ships); + // older images silently ignore the env. remoteSignerAuthTokenKey = "authToken" )