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
11 changes: 8 additions & 3 deletions .envrc.local.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# Local environment overrides (copy to .envrc.local)
# This file is for local development customizations and is gitignored

# Recommended when your stack lives in ~/.config/obol but you build obol from this repo:
# export OBOL_CONFIG_DIR="${HOME}/.config/obol"
# export OBOL_BIN_DIR="${PWD}/.workspace/bin"
# PATH_add .workspace/bin
#
# Without OBOL_CONFIG_DIR, OBOL_DEVELOPMENT=true uses .workspace/config and may
# create a second k3d cluster (separate stack ID from your real install).

# Example: Override development mode
# export OBOL_DEVELOPMENT=false

# Example: Add workspace bin to PATH
# PATH_add .workspace/bin

# Example: Override config directories
# export OBOL_CONFIG_DIR=/custom/config/path
# export OBOL_BIN_DIR=/custom/bin/path
Expand Down
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Integration tests use `//go:build integration`; skip when prerequisites missing.
| Public (tunnel) | none | `/services/<name>/*` | x402 ForwardAuth -> upstream |
| Public (tunnel) | none | `/.well-known/agent-registration.json` | ERC-8004 httpd |
| Public (tunnel) | none | `/skill.md` | service catalog |
| Public (tunnel) | none | `/api/services.json` | service catalog JSON feed |
| Public (tunnel) | none | `/api/services.json` | service catalog JSON feed (`displayName`, `tagline`, `logoUrl`, `services[]`) |
| Public (tunnel) | tunnel hostname only | `/` | storefront landing page (Next.js) |

**NEVER remove hostname restrictions from frontend or eRPC HTTPRoutes** — exposing the frontend/RPC to the public internet is a critical security flaw.
Expand Down Expand Up @@ -99,7 +99,7 @@ obol
- `agent auth` (alias `token`): `--runtime [hermes|openclaw|all]`, `--regenerate`; positional `[instance-name]` defaults to stack-managed agent. Replaces legacy `hermes token`.
- `agent new` (alias `onboard`): CRD-declared sub-agent via `--model`, `--skills`, `--objective`, `--create-wallet`. Without positional name, falls back to legacy host-rendered Hermes/OpenClaw onboard.
- `network install` has dynamic subcommands (one per supported chain; `--help` to list). `network sync [<network>/<id>]` with `--all`.
- `sell info <name>` prints purchase instructions (URL, model, buy.py command).
- `sell info set|show|reset` configures seller branding in `/api/services.json` (`displayName`, `tagline`, `logoUrl`). Writes `x402/obol-storefront-profile`; controller merges into the catalog envelope. Example: `obol sell info set --display-name "Acme Labs" --tagline "Paid APIs." --logo-url "https://…"`. **Dev note**: branding changes need both a dev-built `obol` CLI (`go build -o .workspace/bin/obol ./cmd/obol`) and a current `serviceoffer-controller` image — if `sell info set` times out with `timed out waiting for controller to publish /api/services.json`, the profile ConfigMap was likely applied but the running controller is still publishing the legacy bare-array catalog. Rebuild + roll the controller before retrying: `OBOL_FORCE_REBUILD_LOCAL_DEV_IMAGES=serviceoffer-controller OBOL_DEVELOPMENT=true obol stack up`, or `docker build -f Dockerfile.serviceoffer-controller -t ghcr.io/obolnetwork/serviceoffer-controller:latest . && k3d image import … -c <cluster> && kubectl rollout restart deploy/serviceoffer-controller -n x402`.
- `sell mcp [name]` runs a foreground x402-paid MCP server: forwards buyer JSON args to a backend HTTP service, injecting the seller's own API key (buyer never sees it). Payment rides MCP `_meta` (`internal/x402mcp`).
- `sell resume` replays every persisted sell offer (inference incl. detached host-gateway relaunch; http/agent/demo-agent via the manifest ledger at `$OBOL_CONFIG_DIR/sell-http/`) — run after a host reboot; `obol stack up` runs the same path. `--install-boot-unit` adds a systemd user unit (Linux). `sell mcp` is foreground-only, no offer, not resumed.
- `tunnel setup [<token>]`: the one permanent-URL command. Connector-token based (dashboard-managed) — no host binary, no account-wide API key. Accepts the bare connector token, the `--token` flag, a positional arg, or the whole `cloudflared tunnel run --token …` line (prefix stripped via `extractConnectorToken`). Reuses the remote runtime (`ProvisionWithToken` → `TUNNEL_TOKEN` secret, chart `management_mode=remote`); DNS/ingress are configured by the user in the Cloudflare dashboard (route Public Hostname → `http://traefik.traefik.svc.cluster.local:80`), not via API. The API-token provisioning path was removed (no more `tunnel provision`, no setup `--api-token/--account-id/--zone-id/--register-domain`). `--management local` (alias hidden `tunnel login`) is the browser fallback (needs `cloudflared`). `tunnel status` reads connector health from cloudflared's in-cluster `/ready`+`/metrics` (port 2000, no token) plus a public HTTP probe; concise by default, `--verbose` for replicas/pods, `--no-probe` to stay offline. Domain management lives under `obol domain` (`list`, `search`, `check`, `register`) — an optional CLI wrapper around Cloudflare Registrar; still uses a scoped Cloudflare **API token** (Account → Domain perm, via `--api-token`/`CLOUDFLARE_API_TOKEN`; on a TTY it walks you through token creation and prompts). `--api-token` deliberately has NO `-t` alias to avoid colliding with `tunnel setup -t` (connector token — a different credential). `register` is billable (needs a payment method on the CF account); on success it prints the `obol tunnel setup --hostname …` handoff.
Expand Down Expand Up @@ -407,6 +407,7 @@ A registry digest pin instead of `:latest` on the verifier means your dev rewrit
16. **Clusters created on <= v0.10.0-rc12 keep hostPath-typed PVs** — kubelet ignores `fsGroup` there, and v0.10.0's non-root pods (UID 1000, no chown inits) cannot read the legacy 10000-owned data. Symptom after ANY chart re-render (`agent sync`, model sync, tests that sync): `Init:CrashLoopBackOff` with `mkdir /data/.hermes: Permission denied`. Supported path: recreate the cluster (`obol stack export` -> recreate -> `import`); full steps in the v0.10.0 release notes (Breaking changes). Non-destructive workaround: `docker exec <k3d-node> chown -R 1000:1000 /data/<ns>/hermes-data` then delete the pod.
17. **EIP-7702-contaminated test accounts on a Base Sepolia fork** — standard anvil/hardhat accounts #1–#9 (the `test test ... junk` mnemonic) carry EIP-7702 delegation code (`0xef0100…`) from real-chain 7702 experiments on Base Sepolia. Base-Sepolia USDC is FiatTokenV2_2, which verifies EIP-3009 via `SignatureChecker.isValidSignatureNow` — any `from` with code routes to EIP-1271 `isValidSignature` and ignores a perfectly valid ECDSA signature, reverting `FiatTokenV2: invalid signature` (surfacing as facilitator 503 / `unexpected_error`). The buyer/`from` MUST be a freshly generated EOA (account #0 happens to be clean; payTo with code is fine — only the signer is checked). This is why flow-08 funds the agent's generated wallet, and why `flow-17-sell-mcp.sh` generates fresh buyer + seller keys and preflights `cast code "$BUYER_ADDR" == 0x`.
18. **x402 SDK signs `validAfter = now` with no past buffer** — the `x402-foundation/x402/go` client sets EIP-3009 `validAfter` to wall-clock now. An anvil fork's `block.timestamp` is pinned to the forked block and lags real time the longer the fork has been up, so verify/settle revert `FiatTokenV2: authorization is not yet valid`. In a normal release-smoke run flow-17 follows flow-10 immediately so the gap is tiny; flow-17 still defends with `cast rpc evm_setNextBlockTimestamp $((now+30)) && evm_mine` right before the paid call. (obol's own buy.py uses a past buffer and isn't affected.)
19. **`obol sell info set` times out but profile CM updated** — the dev CLI wrote `x402/obol-storefront-profile` but the in-cluster `serviceoffer-controller` is still on an image that publishes `/api/services.json` as a bare `services[]` array (no `displayName` envelope). Symptom: `configmap/obol-storefront-profile configured` then `timed out waiting for controller to publish /api/services.json`; `kubectl get cm -n x402 obol-skill-md -o jsonpath='{.data.services\.json}'` starts with `[` not `{`. Fix: rebuild + import + restart `serviceoffer-controller` (see `sell info` bullet above). `obol stack up` with a warm cache (`built == 0`) does not pick up controller source changes unless `OBOL_FORCE_REBUILD_LOCAL_DEV_IMAGES=serviceoffer-controller`.

For a fuller debug catalog with symptom->fix mapping, see `.agents/skills/obol-stack-dev/references/release-smoke-debugging.md`.

Expand All @@ -426,7 +427,8 @@ The Cloudflare tunnel exposes the cluster to the public internet. Only x402-gate
- `/services/*` — x402 payment-gated, safe by design
- `/.well-known/agent-registration.json` — ERC-8004 discovery
- `/skill.md` — machine-readable service catalog
- `/` on tunnel hostname — static storefront landing page (busybox httpd)
- `/api/services.json` — service catalog envelope (`displayName`, `tagline`, `logoUrl`, `services[]`)
- `/` on tunnel hostname — public storefront landing page (Next.js)

## Dependencies

Expand Down
87 changes: 0 additions & 87 deletions cmd/obol/sell.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bufio"
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
Expand All @@ -24,7 +23,6 @@ import (

"github.com/ObolNetwork/obol-stack/internal/agentcrd"
"github.com/ObolNetwork/obol-stack/internal/config"
"github.com/ObolNetwork/obol-stack/internal/enclave"
"github.com/ObolNetwork/obol-stack/internal/erc8004"
"github.com/ObolNetwork/obol-stack/internal/hermes"
"github.com/ObolNetwork/obol-stack/internal/images"
Expand Down Expand Up @@ -3559,91 +3557,6 @@ func (pf *signerPortForwarder) Stop() {
}
}

// sellInfoCommand returns info about a local inference gateway deployment.
// Kept for the enclave pubkey functionality.
func sellInfoCommand(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "info",
Usage: "Show inference gateway deployment details and encryption key",
ArgsUsage: "<name>",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "json",
Aliases: []string{"j"},
Usage: "Output as JSON",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
u := getUI(cmd)
name := cmd.Args().First()
if name == "" {
return fmt.Errorf("usage: obol sell info <name>")
}

store := inference.NewStore(cfg.ConfigDir)
d, err := store.Get(name)
if err != nil {
return err
}

var k enclave.Key
var keyErr error
if d.TEEType != "" {
k, keyErr = tee.NewKey(d.EnclaveTag, d.ModelHash)
} else {
k, keyErr = enclave.NewKey(d.EnclaveTag)
}

if u.IsJSON() || cmd.Bool("json") {
out := map[string]any{
"name": d.Name,
"enclave_tag": d.EnclaveTag,
"listen_addr": d.ListenAddr,
"upstream_url": d.UpstreamURL,
"wallet_address": d.WalletAddress,
"price_per_request": d.PricePerRequest,
"price_per_mtok": d.PricePerMTok,
"approx_tokens_per_request": d.ApproxTokensPerRequest,
"chain": d.Chain,
"facilitator_url": d.FacilitatorURL,
"created_at": d.CreatedAt,
"updated_at": d.UpdatedAt,
"algorithm": "ECIES-P256-HKDF-SHA256-AES256GCM",
}
if keyErr == nil {
out["pubkey"] = hex.EncodeToString(k.PublicKeyBytes())
out["persistent"] = k.Persistent()
} else {
out["pubkey_error"] = keyErr.Error()
}
return u.JSON(out)
}

u.Printf("Name: %s", d.Name)
u.Printf("Enclave tag: %s", d.EnclaveTag)
u.Printf("Algorithm: ECIES-P256-HKDF-SHA256-AES256GCM")
if keyErr == nil {
u.Printf("Pubkey: %s", hex.EncodeToString(k.PublicKeyBytes()))
u.Printf("Persistent: %v", k.Persistent())
} else {
u.Printf("Pubkey: (unavailable: %v)", keyErr)
}
u.Blank()
u.Printf("Listen: %s", d.ListenAddr)
u.Printf("Upstream: %s", d.UpstreamURL)
u.Printf("Wallet: %s", d.WalletAddress)
u.Printf("Price: %s", formatInferencePriceSummary(d, ""))
u.Printf("Chain: %s", d.Chain)
u.Printf("Facilitator: %s", d.FacilitatorURL)
u.Printf("Created: %s", d.CreatedAt)
if d.UpdatedAt != "" {
u.Printf("Updated: %s", d.UpdatedAt)
}
return nil
},
}
}

// ---------------------------------------------------------------------------
// kubectl helpers
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading