Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions internal/agentruntime/charts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
32 changes: 30 additions & 2 deletions internal/embed/embed_crd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -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")
}
Expand Down
29 changes: 18 additions & 11 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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"]
Expand Down
9 changes: 5 additions & 4 deletions internal/serviceoffercontroller/agent_wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
)

Expand Down
Loading