Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f77eb81
fix(network): stop reth liveness probe from killing consistency recovery
bussyjd Jul 14, 2026
d419620
fix(x402): default 402 challenge resource.url to https on public hosts
bussyjd Jul 14, 2026
440c59f
chore(deps): update dependency kubernetes-sigs/gateway-api to v1.6.1
github-actions[bot] Jul 16, 2026
a04c05d
fix(ui): honor OBOL_NONINTERACTIVE on a real TTY
bussyjd Jul 18, 2026
f5c880b
fix(agentcrd): strip server-managed metadata before ResumeAll re-apply
bussyjd Jul 18, 2026
c70634a
fix(serviceoffercontroller): align well-known/x402 resource path with…
bussyjd Jul 18, 2026
58c7db2
fix(stack): don't treat own cluster's ports as conflicts under --force
bussyjd Jul 18, 2026
8427f10
fix(tunnel): make hostname add idempotent for already-bound hosts
bussyjd Jul 18, 2026
8908241
chore(deps): update cloudflare/cloudflared docker tag to v2026.7.2
github-actions[bot] Jul 20, 2026
c3afc6e
chore(deps): update dependency @scalar/api-reference to v1.62.9
github-actions[bot] Jul 20, 2026
c632485
chore(deps): update ethereum el/cl client updates
github-actions[bot] Jul 20, 2026
947c843
chore(deps): update obolup.sh dependency updates
github-actions[bot] Jul 20, 2026
aa74352
feat(x402): auth-capture unlock gate + fee revenue metrics
bussyjd Jul 20, 2026
5d9a849
Merge remote-tracking branch 'origin/fix/reth-liveness-recovery' into…
bussyjd Jul 20, 2026
7a3964e
Merge remote-tracking branch 'origin/renovate/kubernetes-sigs-gateway…
bussyjd Jul 20, 2026
bb1f94f
Merge remote-tracking branch 'origin/fix/noninteractive-prompt-guard'…
bussyjd Jul 20, 2026
5115f9f
Merge remote-tracking branch 'origin/fix/agent-resume-strip-metadata'…
bussyjd Jul 20, 2026
bb0d1e3
Merge remote-tracking branch 'origin/fix/discovery-x402-resource-path…
bussyjd Jul 20, 2026
36d918b
Merge remote-tracking branch 'origin/fix/init-force-own-cluster-ports…
bussyjd Jul 20, 2026
252f9b9
Merge remote-tracking branch 'origin/fix/tunnel-hostname-idempotent' …
bussyjd Jul 20, 2026
53b45f1
Merge remote-tracking branch 'origin/renovate/cloudflared-updates' in…
bussyjd Jul 20, 2026
2b34310
Merge remote-tracking branch 'origin/renovate/scalar-api-reference-up…
bussyjd Jul 20, 2026
0dd03af
Merge remote-tracking branch 'origin/renovate/ethereum-clients' into …
bussyjd Jul 20, 2026
316d664
Merge remote-tracking branch 'origin/renovate/obolup.sh-dependency-up…
bussyjd Jul 20, 2026
ca07281
Merge remote-tracking branch 'origin/feat/authcapture-unlock' into in…
bussyjd Jul 20, 2026
3fe7ea1
Merge remote-tracking branch 'origin/fix/x402-challenge-url-scheme' i…
bussyjd Jul 20, 2026
0e157e2
fix(x402): drop the model from agent 402 copy + pay-agent
bussyjd Jun 26, 2026
35ee8f0
fix(x402): stop surfacing agentModel in the 402 extra
bussyjd Jun 26, 2026
8fafdbb
fix(x402): keep the internal model out of the bazaar example for agen…
bussyjd Jun 26, 2026
2cddee0
fix(serviceoffer-controller): drop the internal model from /skill.md …
bussyjd Jun 26, 2026
fc5080e
fix(serviceoffer-controller): drop internal model from /api/services.…
bussyjd Jun 27, 2026
84ab5f3
fix(serviceoffer-controller): serve catalog as UTF-8 to stop em-dash …
bussyjd Jun 26, 2026
f420d78
fix(x402): surface on-chain settle tx hash on unlock settle failure
bussyjd Jul 20, 2026
1eb291e
Merge remote-tracking branch 'origin/feat/authcapture-unlock' into in…
bussyjd Jul 20, 2026
ab28470
Merge branch 'main' into integration/v0.14.0-rc1
bussyjd Jul 20, 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
6 changes: 3 additions & 3 deletions internal/agentcrd/agent_contract_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import (
// (agentcrd.HostNoBundledSkillsMarkerPath), so Hermes' installer/sync
// skips seeding its ~80 bundled skills;
// (2) the rendered hermes-config ConfigMap in the agent's namespace carries
// the capped knobs: lifetime_seconds: 90, max_turns: 30,
// reasoning_effort: low, and disabled_toolsets {memory, web};
// the capped knobs: lifetime_seconds: 180, max_turns: 30,
// reasoning_effort: low, and disabled_toolsets {memory, web, code_execution};
// (3) a BEHAVIORAL signal that bundled skills were actually skipped — see
// assertBundledSkillsSkippedInPod for why we assert pod filesystem state
// rather than grep a log line.
Expand Down Expand Up @@ -233,7 +233,7 @@ func getHermesConfigYAML(t *testing.T, cfg *config.Config, ns string) string {
func assertHermesConfigCaps(t *testing.T, cfgYAML string) {
t.Helper()
for _, want := range []string{
"lifetime_seconds: 90",
"lifetime_seconds: 180",
"max_turns: 30",
"reasoning_effort: low",
"disabled_toolsets:",
Expand Down
11 changes: 7 additions & 4 deletions internal/buyprompts/buyprompts.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ func modelOr(in Input, placeholder string) string {
}

func agentBlock(in Input) Block {
modelFlag := modelOr(in, "<model-id>")
// Deliberately no model anywhere: an Obol Agent runs its own pinned
// model, skills, and memory — the buyer never picks one, pay-agent takes
// no --model, and the agent ignores the chat-completions `model` field,
// so the wire example omits it too.
return Block{
CallShape: CallShape{
Method: "POST",
Expand All @@ -171,8 +174,8 @@ func agentBlock(in Input) Block {
"Use the buy-x402 skill's `pay-agent` command to buy one round of work from this "+
"Obol Agent — it has its own skills, tools, and memory, not just a model. Edit the "+
"message, then run:\n\n"+
"pay-agent %s --model %q --message %q",
in.URL, modelFlag, task(in),
"pay-agent %s --message %q",
in.URL, task(in),
),
PromptGenericLLM: fmt.Sprintf(
"Help me call the Obol Agent at %s — it's an autonomous agent (tools + skills + memory), "+
Expand All @@ -187,7 +190,7 @@ func agentBlock(in Input) Block {
in.URL, task(in),
),
},
Example: ChatExample(in.URL, in.Model, in.TaskExample),
Example: ChatExample(in.URL, "", in.TaskExample),
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/embed/infrastructure/cloudflared/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ transport:

image:
repository: cloudflare/cloudflared
tag: "2026.7.1@sha256:188bb03589a32affed3cf4d0590565ffe67b78866e6b5582574afab2b705bafe"
tag: "2026.7.2@sha256:4f6655284ab3d252b7f28fedb19fe6c8fc82ee5b1295c20ac74d475e5398a52d"

metrics:
address: "0.0.0.0:2000"
Expand Down
2 changes: 1 addition & 1 deletion internal/embed/infrastructure/helmfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ repositories:
# Single source of truth: change this to switch networks
values:
- network: mainnet
- gatewayApiVersion: v1.6.0
- gatewayApiVersion: v1.6.1
# Default the cloudflared release to enabled. `obol stack up` overrides via
# `--state-values-set cloudflared.enabled=false` when it detects a running
# quick tunnel that should be preserved across the sync.
Expand Down
22 changes: 18 additions & 4 deletions internal/embed/networks/ethereum/helmfile.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,34 @@ releases:
image:
{{- if eq .Values.executionClient "reth" }}
# renovate: datasource=github-releases depName=paradigmxyz/reth
tag: v2.3.0
tag: v2.4.1
{{- else if eq .Values.executionClient "geth" }}
# renovate: datasource=github-releases depName=ethereum/go-ethereum
tag: v1.17.4
{{- else if eq .Values.executionClient "nethermind" }}
# renovate: datasource=github-releases depName=NethermindEth/nethermind
tag: "1.39.0"
tag: "1.39.1"
{{- else if eq .Values.executionClient "besu" }}
# renovate: datasource=github-releases depName=hyperledger/besu
tag: "26.7.0"
{{- else if eq .Values.executionClient "erigon" }}
# renovate: datasource=github-releases depName=erigontech/erigon
tag: v3.5.1
tag: v3.5.2
{{- end }}
{{- if eq .Values.executionClient "reth" }}
# reth keeps p2p (30303) closed while running post-crash consistency
# recovery, which can exceed any bounded probe window on a large
# datadir. The chart's default tcp liveness probe then SIGKILLs it
# mid-recovery, forcing a longer recovery on the next start — a
# permanent crash loop. A genuinely dead process exits the container
# on its own, so make the probe effectively never fire.
livenessProbe:
tcpSocket:
port: p2p-tcp
initialDelaySeconds: 60
periodSeconds: 120
failureThreshold: 10000
{{- end }}
persistence:
enabled: true
size: {{ if (index .Values "executionStorageSize" | default "") }}{{ index .Values "executionStorageSize" }}{{ else }}{{ if eq .Values.network "mainnet" }}{{ if eq .Values.mode "archive" }}4500Gi{{ else }}500Gi{{ end }}{{ else }}{{ if eq .Values.mode "archive" }}300Gi{{ else }}100Gi{{ end }}{{ end }}{{ end }}
Expand All @@ -120,7 +134,7 @@ releases:
tag: v8.2.0
{{- else if eq .Values.consensusClient "prysm" }}
# renovate: datasource=github-releases depName=OffchainLabs/prysm
tag: v7.1.6
tag: v7.1.7
{{- else if eq .Values.consensusClient "teku" }}
# renovate: datasource=github-releases depName=Consensys/teku
tag: "26.7.1"
Expand Down
6 changes: 3 additions & 3 deletions internal/embed/skills/buy-x402/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py go <u
`go` never creates persistent state; for a pre-authorized inference pool it points you at `buy`. The expert flows underneath, picked by usage shape:

- **`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. Settlement normally lands only after the request succeeds — but a facilitator can submit the settle tx on-chain and *then* fail the request. When that happens the failure report prints `⚠️ SETTLEMENT MAY HAVE COMPLETED ON-CHAIN` with the tx hash: verify with `balance --chain <X>` before retrying (mechanism: docs/observability.md, "Verify settlement against the chain"). Applies to `pay-agent` too.
- **`pay-agent <url> --model <id>`** — single-shot paid **streaming** agent call. Same payment shape as `pay` (one auth, X-PAYMENT, max-loss = price), but POSTs to `<url>/v1/chat/completions` with `stream: true` and forwards every SSE event verbatim to stdout as it arrives. Use this for `type:agent` ServiceOffers when the calling agent wants to consume the response *itself* (memory, tool-call traces, partial results) instead of routing it through LiteLLM as a paid alias. Default HTTP read timeout is **1 hour** — agent calls can legitimately run for many minutes; override with `--timeout <seconds>`.
- **`pay-agent <url>`** — single-shot paid **streaming** agent call. Same payment shape as `pay` (one auth, X-PAYMENT, max-loss = price), but POSTs to `<url>/v1/chat/completions` with `stream: true` and forwards every SSE event verbatim to stdout as it arrives. No `--model`: a `type:agent` offer runs its own model (the request `model` field is ignored), so you only send a prompt. Use this for `type:agent` ServiceOffers when the calling agent wants to consume the response *itself* (memory, tool-call traces, partial results) instead of routing it through LiteLLM as a paid alias. Default HTTP read timeout is **1 hour** — agent calls can legitimately run for many minutes; override with `--timeout <seconds>`.
- **`buy <name>`** — pre-authorize a budget. Sign **N** authorizations up front (the buyer pays nothing yet), declare them in a `PurchaseRequest` CR, let the `x402-buyer` sidecar redeem them transparently as the agent calls the model through LiteLLM at `paid/<remote-model>`. Use for long-running paid inference. Max loss = N × price (only as vouchers are spent); runtime path holds zero signer access.
- **`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-authorized budget runs out.

Expand Down Expand Up @@ -170,7 +170,7 @@ python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py pay h
# One-shot paid STREAMING agent call (SSE events flushed to stdout as they arrive)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py pay-agent \
https://seller.example.com/services/demo-quant \
--model qwen3.5:9b --message 'summarize the latest research on staking'
--message 'summarize the latest research on staking'

# Pay-agent with a full OpenAI-compatible body (stream:true is forced on)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py pay-agent \
Expand Down Expand Up @@ -224,7 +224,7 @@ python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py maint
| `go <url> [--message <text>] [--data <json>] [--method GET\|POST]` | Probe, detect offer type (agent / chat inference / http), dispatch to the right flow |
| `probe <url> [--model <id>] [--type http\|inference\|agent] [--method GET\|POST]` | Send request without payment, parse 402 response for pricing |
| `pay <url> [--type http\|inference] [--method GET\|POST] [--data <body>]` | Single-shot paid request: sign 1 auth, attach X-PAYMENT, send |
| `pay-agent <url> --model <id> [--message <text> \| --data <json>] [--timeout <s>]` | Single-shot paid streaming agent call: SSE events flush to stdout as they arrive (default timeout 1h) |
| `pay-agent <url> [--message <text> \| --data <json>] [--timeout <s>]` | Single-shot paid streaming agent call (no `--model` — the agent runs its own): SSE events flush to stdout as they arrive (default timeout 1h) |
| `buy <name> --endpoint <url> --model <id> [--budget <amount>] [--count N]` | Pre-sign auths, create/update `PurchaseRequest`, expose `paid/<model>`. `--budget` takes atomic units or token units (`'1.5 USDC'`) |
| `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) |
| `process <name> \| --all` | Reconcile `autoRefill` policies against live `x402-buyer` status |
Expand Down
29 changes: 14 additions & 15 deletions internal/embed/skills/buy-x402/scripts/buy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2647,7 +2647,7 @@ def cmd_pay(url, method="GET", data=None, kind="http", network=None, timeout=Non
sys.exit(1)


def cmd_pay_agent(url, messages=None, model_id=None, network=None, timeout=None, body=None, token=None, payment_option=None):
def cmd_pay_agent(url, messages=None, network=None, timeout=None, body=None, token=None, payment_option=None):
"""Single-shot paid streaming agent call: probe -> sign one auth -> SSE-stream.

Sibling of `cmd_pay` for `type=agent` ServiceOffers. Differences from
Expand All @@ -2667,9 +2667,10 @@ def cmd_pay_agent(url, messages=None, model_id=None, network=None, timeout=None,
alias.

`body` is an optional JSON-encoded request body. When omitted, `messages`
+ `model_id` are required and a `{model, messages, stream:true}` body is
synthesized. When provided, the body is parsed and `"stream": true` is
forced onto whatever the caller passed.
is required and a `{messages, stream:true}` body is synthesized — NO `model`
field: a type=agent offer runs its own model and ignores any `model` sent.
When provided, the body is parsed and `"stream": true` is forced onto
whatever the caller passed.
"""
if timeout is None or float(timeout) <= 0:
timeout = 3600.0
Expand All @@ -2689,27 +2690,24 @@ def cmd_pay_agent(url, messages=None, model_id=None, network=None, timeout=None,
# Force streaming on. cmd_pay handles non-streaming; cmd_pay_agent
# exists precisely to stream.
parsed_body["stream"] = True
if model_id and not parsed_body.get("model"):
parsed_body["model"] = model_id
else:
if not messages:
print(
"Error: --message (or --data <json>) is required for `pay-agent`.\n"
"Example: pay-agent <url> --model qwen3.5:9b --message 'summarize the docs'",
"Example: pay-agent <url> --message 'summarize the docs'",
file=sys.stderr,
)
sys.exit(1)
if not model_id:
print("Error: --model is required when using --message.", file=sys.stderr)
sys.exit(1)
# type=agent ServiceOffers run their own model — there is nothing to
# select and the agent ignores any `model` field — so pay-agent sends
# only the prompt.
parsed_body = {
"model": model_id,
"messages": [{"role": "user", "content": messages}],
"stream": True,
}

print(f"Probing {url} ...")
pricing = _probe_endpoint(url, model_id=model_id or "test", kind="inference")
pricing = _probe_endpoint(url, model_id="probe", kind="inference")
if not pricing:
print("Failed to get x402 pricing.", file=sys.stderr)
sys.exit(1)
Expand Down Expand Up @@ -3095,7 +3093,9 @@ def usage():
print(" pay <url> [--type http|inference] [--method GET|POST] [--data '<body>'] [--timeout <seconds>]")
print(" [--token <SYMBOL>] [--network <name>] [--payment-option <N>]")
print(" Single-shot paid request (sign 1 auth, attach X-PAYMENT)")
print(" pay-agent <url> --model <id> [--message '<text>' | --data '<json>'] [--timeout <seconds>]")
print(" Multi-currency offers: pick which asset/price to pay with")
print(" --token/--network/--payment-option (probe to see options)")
print(" pay-agent <url> [--message '<text>' | --data '<json>'] [--timeout <seconds>]")
print(" [--token <SYMBOL>] [--network <name>] [--payment-option <N>]")
print(" Single-shot paid streaming agent call (POST /v1/chat/completions,")
print(" stream: true). Each SSE event flushes to stdout. Default timeout 1h.")
Expand Down Expand Up @@ -3190,7 +3190,7 @@ def usage():
positional, opts = parse_flags(rest)
if not positional:
print(
"Usage: pay-agent <url> --model <id> [--message '<text>' | --data '<json>'] "
"Usage: pay-agent <url> [--message '<text>' | --data '<json>'] "
"[--network <name>] [--timeout <seconds>]",
file=sys.stderr,
)
Expand All @@ -3207,7 +3207,6 @@ def usage():
cmd_pay_agent(
positional[0],
messages=opts.get("message"),
model_id=opts.get("model"),
network=opts.get("network"),
timeout=timeout,
body=opts.get("data"),
Expand Down
1 change: 1 addition & 0 deletions internal/monetizeapi/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ type ServiceOfferPayment struct {
// x402 payment scheme.
// +kubebuilder:default="exact"
// +kubebuilder:validation:Enum=exact
// TODO(auth-capture): regenerate CRD manifests + wire per-offer unlock config
Scheme string `json:"scheme,omitempty"`
// Chain identifier for payments (human-friendly). Reconciler resolves
// to CAIP-2 format (e.g., "base-sepolia" → "eip155:84532").
Expand Down
38 changes: 22 additions & 16 deletions internal/serviceoffercontroller/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,12 @@ func buildStaticSiteConfigMap(content, servicesJSON, openAPIJSON, apiDocsHTML st
"services.json": servicesJSON,
"openapi.json": openAPIJSON,
"api.html": apiDocsHTML,
"httpd.conf": ".md:text/markdown\n.json:application/json\n.html:text/html\n.js:text/javascript\n",
// charset=utf-8 on the text types so UTF-8 content (em dashes in the
// catalog, accented operator descriptions, …) renders correctly
// instead of mojibake — busybox httpd otherwise sends a bare text/*
// type and clients fall back to Latin-1/CP1252. JSON is always UTF-8
// by spec (RFC 8259), so it carries no charset param.
"httpd.conf": ".md:text/markdown; charset=utf-8\n.json:application/json\n.html:text/html; charset=utf-8\n.js:text/javascript; charset=utf-8\n",
}
for _, f := range bundles {
data[f.Key] = f.Content
Expand Down Expand Up @@ -1334,7 +1339,7 @@ func buildSkillMarkdown(offers []*monetizeapi.ServiceOffer, baseURL string, expl
lines = append(lines, "| Service | Type | Model | Pay with | Status | Endpoint |")
lines = append(lines, "|---------|------|-------|----------|--------|----------|")
for _, offer := range ready {
modelName := offer.Spec.Model.Name
modelName := catalogModelName(offer)
if modelName == "" {
modelName = "—"
}
Expand All @@ -1359,7 +1364,7 @@ func buildSkillMarkdown(offers []*monetizeapi.ServiceOffer, baseURL string, expl
}
lines = append(lines, "", "## Service Details", "")
for _, offer := range ready {
modelName := offer.Spec.Model.Name
modelName := catalogModelName(offer)
endpoint := baseURL + offer.EffectivePath()
if origin := offer.EffectiveOrigin(); origin != "" {
endpoint = origin
Expand Down Expand Up @@ -1449,22 +1454,18 @@ func skillMarkdownHowToPay(baseURL string) []string {
}
}

// catalogModelName resolves the model id a buyer should put in a paid
// chat-completions body. type=agent offers leave spec.model empty by design
// (the model lives on the linked Agent), so fall back to the controller's
// resolved view. Shared by /api/services.json and the /skill.md worked
// examples so both surfaces advertise the same id.
// catalogModelName returns the model id to surface in catalog surfaces
// (/api/services.json, /skill.md worked examples, buyprompts), or "" to omit
// it. Agent offers run their own model and ignore the request `model` field,
// so their id is an internal detail and is never surfaced — mirrors the 402
// page / extra / bazaar model-strip in internal/x402 (and it goes stale the
// moment the operator swaps the backing model). Inference (and other
// model-bearing) offers keep their id, since there the buyer selects it.
func catalogModelName(offer *monetizeapi.ServiceOffer) string {
if offer == nil {
if offer == nil || offer.IsAgent() {
return ""
}
if offer.Spec.Model.Name != "" {
return offer.Spec.Model.Name
}
if offer.Status.AgentResolution != nil {
return offer.Status.AgentResolution.Model
}
return ""
return offer.Spec.Model.Name
}

// skillMarkdownTryIt renders the per-offer "Try it" subsection: one curl that
Expand Down Expand Up @@ -1638,6 +1639,11 @@ func buildServiceCatalogJSON(offers []*monetizeapi.ServiceOffer, baseURL string,
if desc == "" {
desc = fmt.Sprintf("x402 payment-gated %s service", fallbackOfferType(offer))
}
// Agent offers run their own model and ignore the request `model`
// field, so the id is an internal detail and is never surfaced in
// the catalog — mirrors skill.md, the 402 page / extra, and the
// bazaar example. Inference (and other model-bearing) offers keep
// their id, since there the buyer selects it.
modelName := catalogModelName(offer)

drainEndsAt := ""
Expand Down
5 changes: 5 additions & 0 deletions internal/serviceoffercontroller/render_builders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ func TestBuildStaticSiteConfigMap(t *testing.T) {
if conf, _ := data["httpd.conf"].(string); !strings.Contains(conf, ".md:text/markdown") || !strings.Contains(conf, ".json:application/json") || !strings.Contains(conf, ".html:text/html") {
t.Errorf("httpd.conf missing required mime mappings: %q", conf)
}
// Text types must declare charset=utf-8 or UTF-8 content (em dashes,
// accented descriptions) renders as Latin-1 mojibake.
if conf, _ := data["httpd.conf"].(string); !strings.Contains(conf, ".md:text/markdown; charset=utf-8") || !strings.Contains(conf, ".html:text/html; charset=utf-8") {
t.Errorf("httpd.conf text types missing charset=utf-8: %q", conf)
}
// Managed-by label so the controller owns cleanup on uninstall.
lbls, _ := cm.Object["metadata"].(map[string]any)["labels"].(map[string]any)
if lbls["obol.org/managed-by"] != "serviceoffer-controller" {
Expand Down
Loading