Skip to content

Commit 2deb6fa

Browse files
bussyjdOisinKynebussyjd
authored
release: v0.10.0-rc12 (consolidate #593/#594/#595/#597 + review hardening) (#600)
* Work on openapi spec for services * Improve 402 html page * Update storefronts * feat(buy-x402): add --set-default so the agent self-adopts paid/<model> as primary After a persistent inference buy publishes paid/<remote-model> in LiteLLM, the agent adopts it as its own primary chat model in-pod via native 'hermes config set model.default' (atomic write, per-request re-read, no restart, no host CLI, no new RBAC). Includes a LiteLLM /v1/models existence guard, an auto-refill safety warning, and a PyYAML fallback writer. Validated by a design+adversarial workflow and a live CLI smoke against a running obol-agent: buy --set-default flips config.yaml model.default to paid/AEON-7/... and the next agent chat settled via the x402-buyer pool (spent 0->1) with no restart; rollback verified. * Have agent stream responses to keep the tunnel alive * security(x402): SRI-pin the Scalar bundle on the public /api page The /api OpenAPI reference is served over the public tunnel and pulls the @scalar/api-reference bundle from jsdelivr. The integrity hash was left empty in phase 1, so the browser executed whatever the CDN returned, unverified. Populate scalarBundleSRI with the sha384 of the pinned 1.34.0 bundle so a tampered CDN response is blocked. Comment updated to stress the hash must be re-derived in lockstep with every scalarBundleVersion bump. * fix(buy-x402): run --set-default existence guard before auto-refill warning The 'paid/<model> not selectable in LiteLLM' guard ran *after* the no-auto-refill WARNING. A model that LiteLLM would refuse still printed a scary 'every chat turn fails when the pool empties' warning describing a primary-model failure mode that cannot occur when the default was never switched. Reorder so we refuse first and only warn when we are actually about to adopt the model. * security(x402): sanitize ServiceOffer-sourced tokens in 402 copy-paste commands spec.model.name and metadata.name flow from the ServiceOffer CR into copy-pasteable 'obol buy inference ...' commands rendered on the public 402 page. A hostile or fat-fingered offer could smuggle shell metacharacters into a command a reader might paste. Add sanitizeDisplayToken at the render boundary: CR-sourced tokens must match the model-id/k8s-name charset (^[A-Za-z0-9._:/-]+$) or collapse to the existing safe placeholder. Real ids like qwen3.5:9b and anthropic/claude-3-5-sonnet-latest pass through unchanged. * docs(claude): compress and correct CLAUDE.md; fold in #597 streaming/sell-agent facts Terse rewrite of project CLAUDE.md (42725 -> 41797 bytes) corrected against the live codebase. Preserves all invariants, the 14 pitfalls, and flag warnings; adds #597's stream:true / statusRecorder.Flush guidance and agent-backed-offer (port 8642) facts so the compressed doc loses nothing rc12 ships. --------- Co-authored-by: Oisín Kyne <oisin@obol.tech> Co-authored-by: bussyjd <jd@obol.tech> Co-authored-by: bussyjd <bussyjd@users.noreply.github.com>
1 parent 8fb1553 commit 2deb6fa

27 files changed

Lines changed: 3204 additions & 266 deletions

CLAUDE.md

Lines changed: 194 additions & 154 deletions
Large diffs are not rendered by default.

cmd/obol/sell.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,9 @@ Examples:
195195
Usage: "Agent name for ERC-8004 registration (defaults to the offer name)",
196196
},
197197
&cli.StringFlag{
198-
Name: "register-description",
199-
Usage: "Agent description for ERC-8004 registration",
198+
Name: "description",
199+
Aliases: []string{"register-description"},
200+
Usage: "Human-readable description of the service. Surfaced on the 402 payment page, in the storefront catalog, and (when registration is enabled) on the ERC-8004 registration document.",
200201
},
201202
&cli.StringFlag{
202203
Name: "register-image",
@@ -296,6 +297,18 @@ Examples:
296297
if modelFlag == "" {
297298
return fmt.Errorf("--model is required (or run interactively to auto-detect)")
298299
}
300+
// LiteLLM's `paid/*` wildcard route doesn't match model names
301+
// containing `/` — buyers signing against this seller would see
302+
// requests fall through to the buyer sidecar with a 404 (see
303+
// CLAUDE.md and the obol-agent's recent buy report). Reject
304+
// up-front and suggest a `--` separator that survives the route.
305+
if strings.Contains(modelFlag, "/") {
306+
return fmt.Errorf(
307+
"--model %q contains '/', which breaks LiteLLM's `paid/*` wildcard on the buyer side; "+
308+
"use `--` (or another non-slash separator) — e.g. `%s` instead of `%s`",
309+
modelFlag, strings.ReplaceAll(modelFlag, "/", "--"), modelFlag,
310+
)
311+
}
299312

300313
teeType := cmd.String("tee")
301314
modelHash := cmd.String("model-hash")
@@ -345,7 +358,7 @@ Examples:
345358
persistedRegistration, _, regErr := buildSellRegistrationConfig(name, sellRegistrationInput{
346359
NoRegister: cmd.Bool("no-register"),
347360
Name: cmd.String("register-name"),
348-
Description: cmd.String("register-description"),
361+
Description: cmd.String("description"),
349362
Image: cmd.String("register-image"),
350363
Skills: cmd.StringSlice("register-skills"),
351364
Domains: cmd.StringSlice("register-domains"),
@@ -601,8 +614,9 @@ Examples:
601614
Usage: "Agent name for ERC-8004 registration",
602615
},
603616
&cli.StringFlag{
604-
Name: "register-description",
605-
Usage: "Agent description for ERC-8004 registration",
617+
Name: "description",
618+
Aliases: []string{"register-description"},
619+
Usage: "Human-readable description of the service. Surfaced on the 402 payment page, in the storefront catalog, and (when registration is enabled) on the ERC-8004 registration document.",
606620
},
607621
&cli.StringFlag{
608622
Name: "register-image",
@@ -799,7 +813,7 @@ Examples:
799813
NoRegister: cmd.Bool("no-register"),
800814
Register: cmd.Bool("register"),
801815
Name: cmd.String("register-name"),
802-
Description: cmd.String("register-description"),
816+
Description: cmd.String("description"),
803817
Image: cmd.String("register-image"),
804818
Skills: cmd.StringSlice("register-skills"),
805819
Domains: cmd.StringSlice("register-domains"),
@@ -1362,7 +1376,7 @@ var demoTypes = map[string]demoSpec{
13621376
"quant": {
13631377
Type: "quant",
13641378
Price: "10",
1365-
Description: "Agent-backed chain analyst (Agent CRD + ServiceOffer of type=agent)",
1379+
Description: "A simple example agent that can analyse Ethereum and Base for you",
13661380
NeedsERPC: true,
13671381
DefaultChain: "ethereum",
13681382
DefaultToken: "OBOL",
@@ -3861,7 +3875,7 @@ func buildResumeGatewayArgs(d *inference.Deployment) []string {
38613875
args = append(args, "--register-name", v)
38623876
}
38633877
if v, _ := d.Registration["description"].(string); v != "" {
3864-
args = append(args, "--register-description", v)
3878+
args = append(args, "--description", v)
38653879
}
38663880
if v, _ := d.Registration["image"].(string); v != "" {
38673881
args = append(args, "--register-image", v)

cmd/obol/sell_agent.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ Examples:
7575
Usage: "Agent name for ERC-8004 registration (defaults to the offer name)",
7676
},
7777
&cli.StringFlag{
78-
Name: "register-description",
79-
Usage: "Agent description for ERC-8004 registration (defaults to the agent's objective)",
78+
Name: "description",
79+
Aliases: []string{"register-description"},
80+
Usage: "Human-readable description of the service. Surfaced on the 402 payment page, in the storefront catalog, and (when registration is enabled) on the ERC-8004 registration document. Defaults to the agent's objective.",
8081
},
8182
},
8283
Action: func(ctx context.Context, cmd *cli.Command) error {
@@ -173,7 +174,7 @@ Examples:
173174
if regName == "" {
174175
regName = name
175176
}
176-
regDesc := strings.TrimSpace(cmd.String("register-description"))
177+
regDesc := strings.TrimSpace(cmd.String("description"))
177178
if regDesc == "" {
178179
regDesc = agent.Objective
179180
}
@@ -418,6 +419,16 @@ func runAgentBackedDemo(
418419
"metadata": map[string]any{
419420
"name": name,
420421
"namespace": offerNs,
422+
// Agent-backed demos can't live in the legacy "demo"
423+
// namespace today (the controller's confused-deputy guard at
424+
// agent_resolver.go forces spec.agent.ref.namespace ==
425+
// offer.namespace), so the catalog renderer can't infer
426+
// "demo" from offer.namespace alone. The obol.org/demo
427+
// label is the explicit signal — keep it set here so quant
428+
// and friends show up under "Demo services" on the
429+
// storefront. Drop this once we relax the cross-namespace
430+
// guard (see plans/openapi-402-followups.md).
431+
"labels": map[string]any{"obol.org/demo": "true"},
421432
},
422433
"spec": specMap,
423434
}

cmd/obol/sell_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,7 +1396,7 @@ func TestBuildResumeGatewayArgs(t *testing.T) {
13961396
"--per-mtok", "23",
13971397
"--facilitator", "https://x402.gcp.obol.tech",
13981398
"--register-name", "Qwen3.6-27B AEON Ultimate",
1399-
"--register-description", "Uncensored Qwen3.6-27B abliteration",
1399+
"--description", "Uncensored Qwen3.6-27B abliteration",
14001400
"--register-skills", "llm/inference",
14011401
"--register-skills", "llm/uncensored",
14021402
"--register-domains", "inference.v1337.org",
@@ -1419,7 +1419,7 @@ func TestBuildResumeGatewayArgs(t *testing.T) {
14191419
},
14201420
wantNoSub: []string{
14211421
"--register-name",
1422-
"--register-description",
1422+
"--description",
14231423
},
14241424
},
14251425
{

internal/embed/skills/buy-x402/SKILL.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Purchase access to remote x402-gated services. There are two flows, picked by us
1010

1111
- **`pay <url>`** — single-shot. Probe the URL, sign **one** payment authorization, attach `X-PAYMENT`, send the request, return the response. Stateless. Use for `type:http` services and any one-off purchase. Max loss = price of one request.
1212
- **`buy <name>`** — pre-payment budget. Pre-sign **N** authorizations, declare them in a `PurchaseRequest` CR, let the `x402-buyer` sidecar spend them transparently as the agent calls the model through LiteLLM at `paid/<remote-model>`. Use for long-running paid inference. Max loss = N × price; runtime path holds zero signer access.
13+
- **`buy <name> --model <id> --set-default`** — same as `buy` above, then adopt `paid/<remote-model>` as the agent's **own primary model**, in-pod, by itself: an atomic `hermes config set model.default` that Hermes re-reads per request (effective next chat turn, **no restart**, no host-side `obol model prefer`/`obol model sync`). Refuses if the model isn't selectable in LiteLLM. Pair with `--auto-refill` so the primary model doesn't brick when the pre-signed pool empties.
1314

1415
Both flows auto-detect the token + transfer method from the seller's 402 response. Currently supported: **USDC via EIP-3009** (Base Sepolia, Base Mainnet, Ethereum Mainnet) and **OBOL via Permit2** (Ethereum Mainnet).
1516

@@ -71,6 +72,18 @@ This is one tx, ~46k gas, valid forever (unless the user later revokes). EIP-300
7172
storefront publishes machine-readable metadata at
7273
`<base>/api/services.json` with full asset, EIP-712 signing domain,
7374
transfer method, and atomic-unit price for every offered service.
75+
- **`pay` timeout defaults to ~100 s.** This is the Cloudflare free-tier
76+
tunnel cap — longer requests get killed by the edge before our client
77+
ever sees a response. Reasoning models, long generations, or large
78+
batches need `--timeout <seconds>` set explicitly, and the seller's own
79+
upstream/edge limit still applies.
80+
- **Avoid `/` in remote model identifiers.** LiteLLM's `paid/*` wildcard
81+
route only matches a single segment; a remote `vendor/model` would
82+
resolve to `paid/vendor/model` and miss the wildcard, so the request
83+
falls through to the buyer sidecar and 404s. Sellers should use a non-
84+
slash separator (e.g. `vendor--model`); buyers signing against a
85+
legacy slashed name need the controller to insert an explicit LiteLLM
86+
entry for the alias (`addLiteLLMModelEntry`).
7487

7588
## When to Use
7689

@@ -150,6 +163,7 @@ python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py maint
150163
| `probe <url> [--model <id>] [--type http\|inference] [--method GET\|POST]` | Send request without payment, parse 402 response for pricing |
151164
| `pay <url> [--type http\|inference] [--method GET\|POST] [--data <body>]` | Single-shot paid request: sign 1 auth, attach X-PAYMENT, send |
152165
| `buy <name> --endpoint <url> --model <id> [--budget N] [--count N]` | Pre-sign auths, create/update `PurchaseRequest`, expose `paid/<model>` |
166+
| `buy <name> --endpoint <url> --model <id> --set-default [--auto-refill]` | As above, then set `paid/<model>` as the agent's own primary model in-pod (no restart, no host CLI) |
153167
| `process <name> \| --all` | Reconcile `autoRefill` policies against live `x402-buyer` status |
154168
| `list` | List purchased providers + remaining auth counts |
155169
| `status <name>` | Check sidecar pod status + remaining auths |

0 commit comments

Comments
 (0)