Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7f092a5
fix: x402 verifier TLS and facilitator compatibility
bussyjd Apr 8, 2026
db38000
feat: switch default facilitator to Obol-operated x402.gcp.obol.tech
bussyjd Apr 8, 2026
e470d33
feat: LiteLLM zero-downtime config and hot-reload
bussyjd Apr 8, 2026
bccc02d
fix: allow obolup.sh to install openclaw via Docker when Node.js is m…
bussyjd Apr 8, 2026
4f4d03c
feat: PurchaseRequest CRD and controller for buy-side x402 (#329)
bussyjd Apr 8, 2026
0ba1a3b
feat: buy.py creates PurchaseRequest CR instead of direct ConfigMap w…
bussyjd Apr 8, 2026
f7da011
fix: eRPC Host routing + private-key-file priority for sell register
bussyjd Apr 8, 2026
7d55060
fix: anchored sed patterns for Bob's port remapping
bussyjd Apr 8, 2026
fecb609
fix: add polling wait for pod readiness in flow-11
bussyjd Apr 8, 2026
9ad6480
fix: port check uses LISTEN state only (ignore FIN_WAIT)
bussyjd Apr 8, 2026
341a6f5
fix: macOS grep/kubectl compat in flow-11
bussyjd Apr 8, 2026
2c8a2b8
feat: flow-11 uses PurchaseRequest CR path for buy verification
bussyjd Apr 8, 2026
7c7f929
fix: consolidate all flow-11 fixes (polling, ports, sed, LISTEN)
bussyjd Apr 8, 2026
650764c
fix: widen agent response validation + provide model name in buy prompt
bussyjd Apr 8, 2026
86e3668
feat: auto-fund Bob's remote-signer wallet in flow-11 (shortcut for #…
bussyjd Apr 8, 2026
06b2f5c
fix: buy.py handles 409 Conflict with resourceVersion on PurchaseRequ…
bussyjd Apr 8, 2026
752dad1
fix: controller signer key format + flow-11 robustness
bussyjd Apr 8, 2026
abd4a0c
feat: embed pre-signed auths in PurchaseRequest spec (no cross-NS sec…
bussyjd Apr 8, 2026
7efd658
fix: wallet address extraction + discovery validation keywords
bussyjd Apr 8, 2026
6eae339
fix: add explicit LiteLLM model entry for paid routes with colons + s…
bussyjd Apr 8, 2026
cb520b2
fix: use kubectl replace for CA bundle to avoid annotation size limit
bussyjd Apr 8, 2026
37920e6
fix: restart x402-verifier after CA bundle population
bussyjd Apr 8, 2026
ed87f02
feat: LiteLLM API model management + buyer sidecar reload
bussyjd Apr 9, 2026
1d8c8ed
fix: bump LiteLLM fork image to sha-778111d
bussyjd Apr 9, 2026
b15beaa
security: fix shell injection in kubectl exec + document gotcha
bussyjd Apr 9, 2026
d1d83f5
fix: bump LiteLLM fork image to sha-c16b156 (multiplatform)
bussyjd Apr 9, 2026
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
1 change: 1 addition & 0 deletions .agents/skills/obol-stack-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,4 @@ go test -tags integration -v -run TestIntegration_Tunnel_SellDiscoverBuySidecar_
- **ConfigMap propagation**: File watcher takes 60-120s. Force restart verifier for immediate effect.
- **Projected ConfigMap refresh**: the LiteLLM pod can take ~60s to reflect updated buyer ConfigMaps in the sidecar.
- **eRPC balance lag**: `buy.py balance` uses `eth_call` through eRPC, and the default unfinalized cache TTL is 10s. After a paid request, poll until the reported balance catches up with the on-chain delta.
- **kubectl exec shell quoting**: NEVER use `sh -c` with `fmt.Sprintf` to embed JSON or secrets in shell commands passed via `kubectl exec`. JSON body or auth tokens containing single quotes will break the shell. Instead, pass args directly: `kubectl exec ... -- wget -qO- --post-data=<json> --header=Authorization:\ Bearer\ <key> <url>`. Each argument goes as a separate argv element, bypassing shell interpretation entirely.
22 changes: 13 additions & 9 deletions cmd/obol/sell.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Examples:
&cli.StringFlag{
Name: "facilitator",
Usage: "x402 facilitator URL",
Value: "https://facilitator.x402.rs",
Value: x402verifier.DefaultFacilitatorURL,
},
&cli.StringFlag{
Name: "listen",
Expand Down Expand Up @@ -1375,15 +1375,19 @@ Examples:
}
agentURI := endpoint + "/.well-known/agent-registration.json"

// Determine signing method: remote-signer (preferred) or private key file (fallback).
// Determine signing method: private key file (if explicitly provided)
// or remote-signer (default when OpenClaw agent is deployed).
useRemoteSigner := false
var signerNS string

if _, err := openclaw.ResolveWalletAddress(cfg); err == nil {
ns, nsErr := openclaw.ResolveInstanceNamespace(cfg)
if nsErr == nil {
useRemoteSigner = true
signerNS = ns
// If --private-key-file is explicitly provided, honour user intent.
if !cmd.IsSet("private-key-file") {
if _, err := openclaw.ResolveWalletAddress(cfg); err == nil {
ns, nsErr := openclaw.ResolveInstanceNamespace(cfg)
if nsErr == nil {
useRemoteSigner = true
signerNS = ns
}
}
}

Expand Down Expand Up @@ -1494,7 +1498,7 @@ func registerDirectViaSigner(ctx context.Context, cfg *config.Config, u *ui.UI,
u.Printf(" Wallet: %s", addr.Hex())

// Connect to eRPC for this network.
client, err := erc8004.NewClientForNetwork(ctx, "http://localhost/rpc", net)
client, err := erc8004.NewClientForNetwork(ctx, "http://obol.stack/rpc", net)
if err != nil {
return fmt.Errorf("connect to %s via eRPC: %w", net.Name, err)
}
Expand Down Expand Up @@ -1529,7 +1533,7 @@ func registerDirectWithKey(ctx context.Context, u *ui.UI, net erc8004.NetworkCon
return fmt.Errorf("invalid private key: %w", err)
}

client, err := erc8004.NewClientForNetwork(ctx, "http://localhost/rpc", net)
client, err := erc8004.NewClientForNetwork(ctx, "http://obol.stack/rpc", net)
if err != nil {
return fmt.Errorf("connect to %s via eRPC: %w", net.Name, err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/obol/sell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func TestSellInference_Flags(t *testing.T) {
assertStringDefault(t, flags, "chain", "base-sepolia")
assertStringDefault(t, flags, "listen", ":8402")
assertStringDefault(t, flags, "upstream", "http://localhost:11434")
assertStringDefault(t, flags, "facilitator", "https://facilitator.x402.rs")
assertStringDefault(t, flags, "facilitator", "https://x402.gcp.obol.tech")
assertStringDefault(t, flags, "vm-image", "ollama/ollama:latest")
assertIntDefault(t, flags, "vm-cpus", 4)
assertIntDefault(t, flags, "vm-memory", 8192)
Expand Down
74 changes: 50 additions & 24 deletions cmd/x402-buyer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ import (

func main() {
var (
configPath = flag.String("config", "/config/config.json", "path to upstream config JSON")
authsPath = flag.String("auths", "/config/auths.json", "path to pre-signed auths JSON")
configDir = flag.String("config-dir", "", "directory of per-upstream config files (SSA mode)")
authsDir = flag.String("auths-dir", "", "directory of per-upstream auth files (SSA mode)")
configPath = flag.String("config", "/config/config.json", "single config JSON file (legacy)")
authsPath = flag.String("auths", "/config/auths.json", "single auths JSON file (legacy)")
statePath = flag.String("state", "/state/consumed.json", "path to persisted consumed-auth state")
listen = flag.String("listen", ":8402", "listen address")
reloadInterval = flag.Duration("reload-interval", 5*time.Second, "config/auth reload interval")
Expand All @@ -42,14 +44,9 @@ func main() {
log.Fatalf("load state: %v", err)
}

cfg, err := buyer.LoadConfig(*configPath)
cfg, auths, err := loadConfigAndAuths(*configDir, *authsDir, *configPath, *authsPath)
if err != nil {
log.Fatalf("load config: %v", err)
}

auths, err := buyer.LoadAuths(*authsPath)
if err != nil {
log.Fatalf("load auths: %v", err)
log.Fatalf("load config/auths: %v", err)
}

proxy, err := buyer.NewProxy(cfg, auths, state)
Expand Down Expand Up @@ -89,26 +86,25 @@ func main() {
defer ticker.Stop()

go func() {
reload := func(reason string) {
newCfg, newAuths, err := loadConfigAndAuths(*configDir, *authsDir, *configPath, *authsPath)
if err != nil {
log.Printf("reload (%s): %v", reason, err)
return
}
if err := proxy.Reload(newCfg, newAuths); err != nil {
log.Printf("reload proxy (%s): %v", reason, err)
}
}

for {
select {
case <-ctx.Done():
return
case <-ticker.C:
cfg, err := buyer.LoadConfig(*configPath)
if err != nil {
log.Printf("reload config: %v", err)
continue
}

auths, err := buyer.LoadAuths(*authsPath)
if err != nil {
log.Printf("reload auths: %v", err)
continue
}

if err := proxy.Reload(cfg, auths); err != nil {
log.Printf("reload proxy: %v", err)
}
reload("ticker")
case <-proxy.ReloadCh():
reload("admin")
}
}
}()
Expand All @@ -124,3 +120,33 @@ func main() {
fmt.Fprintf(os.Stderr, "shutdown: %v\n", err)
}
}

// loadConfigAndAuths loads config and auths from either directory mode (SSA,
// one file per upstream) or single-file mode (legacy, all upstreams in one JSON).
func loadConfigAndAuths(configDir, authsDir, configPath, authsPath string) (*buyer.Config, buyer.AuthsFile, error) {
var (
cfg *buyer.Config
auths buyer.AuthsFile
err error
)

if configDir != "" {
cfg, err = buyer.LoadConfigDir(configDir)
} else {
cfg, err = buyer.LoadConfig(configPath)
}
if err != nil {
return nil, nil, fmt.Errorf("config: %w", err)
}

if authsDir != "" {
auths, err = buyer.LoadAuthsDir(authsDir)
} else {
auths, err = buyer.LoadAuths(authsPath)
}
if err != nil {
return nil, nil, fmt.Errorf("auths: %w", err)
}

return cfg, auths, nil
}
Loading
Loading