From b00fa5d8da86349b07aeae41ae957d082f3ab1bc Mon Sep 17 00:00:00 2001 From: bussyjd Date: Thu, 16 Jul 2026 20:37:20 +0400 Subject: [PATCH] chore: remove internal audit tooling/docs from the production repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/testing/proactive-bug-finding.md, .claude/workflows/obol-audit.js, and hack/canary/run-canary.sh were internal bug-finding methodology and agent tooling merged via #770. They do not belong in the shipped product repo (and the doc's findings tables risked disclosing open issues on public main). The methodology and results are retained privately. The invariant regression test (name_injectivity_test.go) stays — it guards a real fix. Supersedes the redaction in #770; addresses blocker #1 of the #756 review. Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk --- .claude/workflows/obol-audit.js | 99 ---------------- docs/testing/proactive-bug-finding.md | 156 -------------------------- hack/canary/run-canary.sh | 117 ------------------- 3 files changed, 372 deletions(-) delete mode 100644 .claude/workflows/obol-audit.js delete mode 100644 docs/testing/proactive-bug-finding.md delete mode 100755 hack/canary/run-canary.sh diff --git a/.claude/workflows/obol-audit.js b/.claude/workflows/obol-audit.js deleted file mode 100644 index eee3ebe0..00000000 --- a/.claude/workflows/obol-audit.js +++ /dev/null @@ -1,99 +0,0 @@ -// obol-audit — hostile-operator bug sweep (layer 4 of docs/testing/proactive-bug-finding.md). -// -// Fan out N operator-lens finders over the current tree, adversarially verify -// each candidate with two independent refuters, return the ranked survivors. -// Re-run after any large change. Pass args to tell it what is already fixed so -// it does not re-report known issues: -// Workflow({ name: "obol-audit", args: { repo: "/abs/path", known: ["short description of a fixed bug", ...] } }) -// -// Defaults: repo = the obol-stack checkout, known = [] (report everything). - -export const meta = { - name: 'obol-audit', - description: 'Hostile-operator audit: lens finders + adversarial refuters → ranked confirmed bugs', - phases: [ - { title: 'Hunt', detail: 'one finder per operator lens' }, - { title: 'Verify', detail: 'two adversarial refuters per candidate' }, - ], -} - -const REPO = (args && args.repo) || '/Users/bussyjd/Development/OBOL-WORKBENCH/obol-stack' -const KNOWN = (args && args.known) || [] - -const knownBlock = KNOWN.length - ? `KNOWN ISSUES — re-reporting any of these is a FAILURE, exclude them:\n${KNOWN.map((k, i) => `(${i + 1}) ${k}`).join('\n')}` - : 'No known-issue exclusions supplied — report every real bug you find.' - -const COMMON = `You are a hostile-but-realistic node operator auditing the obol-stack Go repo at ${REPO}. READ-ONLY: no file edits, no commits, no kubectl, no deploys. You may grep, read, run go test, and write throwaway /tmp programs to confirm behavior. - -${knownBlock} - -Find NEW, REAL bugs of your assigned lens — behavior a reasonable operator hits that produces wrong results, broken routes, lost/misleading state, leaked internal data, free rides, or misleading output. For each: trace the ACTUAL code path end to end (function + file:line — no speculation), give the concrete operator scenario that triggers it, and confirm it is not a known issue and not already guarded by a test. Quality over quantity: top findings only (max 3), ranked by confidence x severity. Zero findings is fine — do NOT pad with style nits or untriggerable theoretical races.` - -// The invariants these lenses probe map to docs/testing/proactive-bug-finding.md. -const LENSES = [ - { key: 'state-staleness', prompt: 'Lens: SPEC-CHANGE STALENESS (invariant 1). When an operator mutates a spec field (hostname, payTo, price, network, type, upstream) or deletes+recreates an offer, which derived status / published document / on-chain artifact fails to reset or re-derive? Read the reconcile paths and ask of each derived artifact: what resets this when its input changes?' }, - { key: 'name-injectivity', prompt: 'Lens: GENERATED NAME COLLISIONS (invariant 2). Enumerate every generated resource name (middlewares, routes, services, configmaps, grants, tunnel/hermes resources) and check each is injective over (namespace, name, purpose), including truncation collisions and delimiter ambiguity (can two distinct (ns,name) pairs join to the same string?).' }, - { key: 'status-truth', prompt: 'Lens: STATUS LIES (invariant 3). Find conditions/statuses set on apply-success rather than observed reality, or stuck after the cause resolves. Check every readiness signal: PaymentConfigured, Registered, agent/purchase readiness, wallet address, tunnel status, sell status output, dashboard sources.' }, - { key: 'public-surface-leak', prompt: 'Lens: INTERNAL DATA IN PUBLIC SURFACES (invariant 4). Enumerate every publicly-served document (openapi.json, .well-known/x402, agent-registration.json, skill.md, catalog, storefront, 402 body, chat page, async job-status/result endpoints) and trace each field back to its source: does anything pull internal CR fields, cluster-internal hostnames/IPs, raw errors, secrets, or model endpoints without a public-intent gate?' }, - { key: 'url-construction', prompt: 'Lens: URL/HOST/PATH CONSTRUCTION (invariant 5). Audit every URL build for double slashes, missing/hardcoded scheme, port handling, trailing-slash sensitivity, host-header trust, path traversal via offer names, http fallbacks for non-local hosts.' }, - { key: 'payment-verification', prompt: 'Lens: PAYMENT CORRECTNESS (invariant 6). Audit the x402 settle/verify flow for money-losing or free-ride bugs: request reaching upstream without settle; replay of a payment across offers/paths/methods/prices with the same tuple; concurrent duplicate submissions; amount/asset/network confusion; nonce single-use scope; price-change-in-flight; a cheap-route payment passing on an expensive route.' }, - { key: 'tx-sequencing', prompt: 'Lens: ON-CHAIN CLIENT CORRECTNESS. Audit the erc8004 client/signer and CLI registration flow: missing receipt.Status checks (tx sent but reverted → success assumed), gas estimation on stale state, chain-id confusion, recovery paths trusting ambiguous historical matches without re-verifying current ownership, partial multi-network failure leaving inconsistent CRD state.' }, - { key: 'cli-validation', prompt: 'Lens: CLI INPUT TRAPS. Audit cmd/obol for flags whose help mismatches behavior, silent normalization that surprises (TrimRight/ToLower on case-sensitive addresses/hostnames), values passed unvalidated into k8s names / YAML / URLs / on-chain amounts (injection, panics, zero/negative prices), and defaults that differ between sibling subcommands.' }, -] - -const FINDINGS_SCHEMA = { - type: 'object', - required: ['findings'], - properties: { - findings: { - type: 'array', - items: { - type: 'object', - required: ['title', 'file', 'lines', 'severity', 'scenario', 'codePath', 'whyNotKnown'], - properties: { - title: { type: 'string' }, - file: { type: 'string' }, - lines: { type: 'string' }, - severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] }, - scenario: { type: 'string' }, - codePath: { type: 'string' }, - whyNotKnown: { type: 'string' }, - }, - }, - }, - }, -} - -const VERDICT_SCHEMA = { - type: 'object', - required: ['refuted', 'notes'], - properties: { - refuted: { type: 'boolean' }, - notes: { type: 'string' }, - }, -} - -phase('Hunt') -const found = await parallel(LENSES.map((l) => () => - agent(`${COMMON}\n\n${l.prompt}`, { label: `hunt:${l.key}`, phase: 'Hunt', model: 'sonnet', schema: FINDINGS_SCHEMA }) -)) -const candidates = found.filter(Boolean).flatMap((r, i) => r.findings.map((f) => ({ ...f, lens: LENSES[i].key }))) -log(`${candidates.length} candidate findings across ${LENSES.length} lenses`) - -phase('Verify') -const verified = await parallel(candidates.map((c) => () => - parallel([0, 1].map((n) => () => - agent(`${COMMON}\n\nYou are adversarial refuter #${n + 1}. A finder claims this bug. Try HARD to refute it by reading the actual code: is the cited path real, is the scenario operator-triggerable, is it already guarded/fixed/known, does the claimed consequence follow? Default to refuted=true if uncertain or if it is a style/theoretical issue.\n\nFinding (lens ${c.lens}):\n${JSON.stringify(c, null, 2)}`, - { label: `refute:${c.lens}`, phase: 'Verify', model: 'sonnet', schema: VERDICT_SCHEMA }) - )).then((votes) => { - const live = votes.filter(Boolean) - return { ...c, survives: live.length >= 1 && live.every((v) => !v.refuted), refuterNotes: live.map((v) => v.notes) } - }) -)) - -const sev = { critical: 0, high: 1, medium: 2, low: 3 } -const confirmed = verified.filter(Boolean).filter((f) => f.survives).sort((a, b) => (sev[a.severity] ?? 9) - (sev[b.severity] ?? 9)) -const rejected = verified.filter(Boolean).filter((f) => !f.survives).map((f) => ({ title: f.title, lens: f.lens })) -log(`${confirmed.length} confirmed, ${rejected.length} rejected`) -return { confirmed, rejected } diff --git a/docs/testing/proactive-bug-finding.md b/docs/testing/proactive-bug-finding.md deleted file mode 100644 index c875a129..00000000 --- a/docs/testing/proactive-bug-finding.md +++ /dev/null @@ -1,156 +0,0 @@ -# Finding operator bugs before operators do - -The Canary402 field report (10 issues) had one thing in common: every bug was -found by a real operator doing a **reasonable-but-unanticipated sequence on a -live deployment** — switch chain after registering, configure the tunnel -before selling, combine two documented flags, pass a path where an origin was -expected. None were "the code is wrong on the happy path." Example-based unit -tests never exercise those sequences, which is why humans found them first. - -The approach that finds them without waiting for a human is **an automated -synthetic operator driving generated operation sequences, checked against -invariants instead of hand-written expectations.** A normal test asserts -"after X, status is Y"; an invariant asserts something that must hold after -*any* sequence. Once the invariants exist, anything can generate the traffic. - -This document describes the four layers, cheapest first, and records what the -first run found. - -## The invariants (oracles) - -These are the properties that, if they ever break, mean a bug — regardless of -how the system got there: - -1. **Derived state is a pure function of current spec.** After any spec - mutation, every status field / published document / on-chain artifact - reflects the *current* spec, never a stale prior value. (The worst field - bug — cross-chain AgentID reuse — was this invariant broken for one field.) -2. **Generated names are injective over identity.** Any resource in a shared - namespace has a name that is unique per owning offer's `(namespace, name)`. -3. **Status conditions reflect observed reality, not apply-success.** `Ready`, - `RoutePublished`, `Registered`, tunnel "active", etc. are True only when a - real probe confirms the dataplane/on-chain fact — and flip back False when - the fact stops holding. -4. **Public documents contain only public-intent fields.** No internal CR - field (agent objective, tool addresses, cluster-internal hostnames, model - endpoints, raw Go errors) reaches openapi.json / agent-registration.json / - skill.md / catalog / 402 body without an explicit public gate. -5. **Every URL emitted is fetchable as written** (scheme, host, port, path). -6. **No request reaches a paid upstream without a corresponding settle**, and - no payment authorized for one (route, price, network) passes on another. - -## The four layers - -| Layer | What | Cost | Catches | -|---|---|---|---| -| 1. Invariant oracles | The properties above, written once as reusable checks | one-time | shared by all layers below | -| 2. Stateful model tests | Generated op-sequences run against the reconciler with the fake client; assert invariants after each step | CI, seconds | state-machine / staleness / name / status bugs | -| 3. Canary cluster | Nightly ephemeral k3s; the **real CLI** driven through a scenario + flag-combination matrix; invariants checked as black-box probes | nightly, minutes | everything mocks can't reach: Traefik route-age ties, router rejection, header stripping, nonce lag, real 402/settle | -| 4. Agent audit | Periodic hostile-operator LLM sweep over the code, each finding adversarially verified, output = a ranked triage list | weekly, on-demand | judgment-layer bugs with no mechanical oracle: misleading messages, unclear partial-failure reporting, docs-vs-behavior drift | - -Layer 2 is the highest yield per unit cost and belongs in CI. Layer 3 is the -only layer that reproduces the six field bugs that are invisible without real -infrastructure. Layer 4 is the only layer that finds "this output would -mislead an operator," which has no assertion. - -### Running each layer - -- **Layer 2** — `go test ./internal/serviceoffercontroller/...`. The first - invariant oracle lives in `name_injectivity_test.go` (property test over - `sharedNamespaceNameBuilders`); add a builder there and any future - shared-namespace child is covered. -- **Layer 3** — `hack/canary/run-canary.sh` (ephemeral k3s + scenario matrix + - black-box invariant probes). Runnable skeleton; wire into nightly CI. -- **Layer 4** — `Workflow({ name: "obol-audit" })` from Claude Code (the - `.claude/workflows/obol-audit.js` fan-out: N hostile-operator lenses → 2 - adversarial refuters each → ranked survivors). Re-run after any large - change; feed the `KNOWN` list the already-fixed issues so it doesn't - re-report them. - -## First run — 2026-07-16 - -Layer 4 was run against the integration branch with all six Canary402 fixes -merged in (so it hunted for *new* bugs beyond the field report). 8 lenses × 2 -adversarial refuters. **11 findings survived** verification; 3 were rejected by -a refuter (correctly — one was subsumed by the known cross-chain issue, one was -unreachable via the ForwardAuth path, one was harmless). The layer-1 -name-injectivity oracle independently found the same #1 grant bug the agent -sweep did — two methods, one bug, high confidence. - -| Sev | Finding | Location | Lens | Status | -|-----|---------|----------|------|--------| -| CRITICAL | `--price`/`--per-request` never validated → `decimalToAtomic` mis-prices ($0 on `0,01`) or panics on every request (`abc`/`""`/`$0.01`) | `internal/x402/chains.go` | cli-validation | **fixing** | -| HIGH | ReferenceGrant name `ns-name` dash-join not injective — `(foo-bar, baz)` vs `(foo, bar-baz)` collide in shared x402 ns (HTTP 500) | `internal/serviceoffercontroller/render.go` | name-injectivity | **fixed** (PR #767) | -| HIGH | PurchaseRequest `Ready` never re-checked once True — sidecar outage after configure is invisible | `internal/serviceoffercontroller/purchase.go` | status-truth | open | -| HIGH | Agent `Ready` + `status.WalletAddress` go True before the remote-signer holding the key is up | `internal/serviceoffercontroller/agent.go` | status-truth | open | -| HIGH | Raw Go network errors (internal cluster DNS/IP) leak through the public unauthenticated job-status endpoint | `internal/jobbroker/server.go` | public-surface-leak | open | -| HIGH | No payment dedup — concurrent duplicate `X-PAYMENT` all reach the paid upstream, only one settles (free ride) | `internal/x402/forwardauth.go` | payment-verification | open | -| HIGH | `SetMetadata`/`SetAgentURI` never check `receipt.Status` — a mined-but-reverted tx is reported as success | `internal/erc8004/client.go` | tx-sequencing | open | -| HIGH | Registration recovery by owner+URI trusts a historical Registered event without re-verifying current ownership | `cmd/obol/sell.go` | tx-sequencing | open | -| MEDIUM | `status.AgentResolution` survives a `spec.type` flip away from agent → stale model advertised in public catalog forever | `internal/serviceoffercontroller/agent_resolver.go` | state-staleness | open | -| MEDIUM | `obol tunnel status` reports "active" even when the public-reachability probe just failed | `internal/tunnel/tunnel.go` | status-truth | open | -| MEDIUM | SIWX JSON 401 challenge hardcodes `https://` (ignores `isLocalHost`), unlike every other URL builder in the package | `internal/x402/authgate.go` | url-construction | open | - -Four of the eleven are the **status-truth** invariant (#3) broken in a new -place — the same class as the field report's "offer Ready while Traefik -rejected the router." That clustering is the signal to prioritize invariant #3 -as a layer-2 sweep: gate every readiness condition on an observed probe, the -way `RoutePublished` now is. - -## Why this beats more unit tests - -Unit tests encode what the author expected. These bugs live precisely where the -author's expectation was wrong, so an assertion written from the same mental -model can't catch them. Invariants encode what must be true for *any* input, and -the three generators (sequence fuzzer, canary CLI matrix, hostile-operator -agent) explore the input space the author didn't think to. - -## Second run — 2026-07-16, whole product surface - -The first run covered the serviceoffer/x402/sell core. This run pointed layer 4 -at the **14 subsystems it had not touched** — wallet/key handling, backup -export/import, stack lifecycle, self-update, chain networking, buyer flow, -hermes runtime, openclaw import, model/inference serving, storefront serving, -secrets/config, infra shell-out, and CRD validation — each finder given the -full known-issue list so it would not re-report. 14 lenses × 2 adversarial -refuters (68 agents). **21 findings survived** verification (excluding two -TEE findings tracked separately); 4 rejected by a refuter. - -The subsystems the first pass never reached held the most severe bugs — three -criticals, none in the already-hardened core: - -| Sev | Finding | Location | Subsystem | Status | -|-----|---------|----------|-----------|--------| -| CRITICAL | `obol stack init --force --backend ` destroys the live cluster with zero confirmation, and do | `internal/stack/stack.go` | stack-lifecycle | open | -| CRITICAL | Unescaped openclaw.json fields interpolated into Helm values YAML → arbitrary value injection (i | `internal/openclaw/openclaw.go` | openclaw-import | **fixing** | -| CRITICAL | obol sell inference: default (cluster-available) deployment binds the payment-gated port to ALL | `cmd/obol/sell.go` | model-serving | open | -| HIGH | Secret material (keystore password, wallet metadata) keeps a pre-existing file's permissions on | `internal/openclaw/wallet.go` | wallet-key-security | open | -| HIGH | Import silently clobbers preserved DataDir with no --force gate or confirmation | `internal/stackbackup/import.go` | backup-export-import | open | -| HIGH | `obol stack up` silently reverts operator hand-edits to the local defaults tree (e.g. eRPC value | `internal/defaults/defaults.go` | stack-lifecycle | open | -| HIGH | k3s backend Down()/Destroy() sends `sudo kill -TERM`/`sudo kill -9` to a stale PID with no proce | `internal/stack/backend_k3s.go` | stack-lifecycle | open | -| HIGH | kubectl/helm/k3d/helmfile/k9s (and Ollama's third-party installer) are downloaded and installed | `obolup.sh` | self-update-integrity | open | -| HIGH | One-shot buyer flows (pay / pay-agent / go) sign the seller's quoted price verbatim with no ceil | `internal/embed/skills/buy-x402/scripts/buy.py` | buy-flow | open | -| HIGH | Hermes-dashboard messaging gateway defaults to GATEWAY_ALLOW_ALL_USERS=true with no override tha | `internal/hermes/hermes.go` | hermes-agent-runtime | open | -| HIGH | x402 inference gateway's unauthenticated catch-all route lets clients hit the upstream's native | `internal/inference/gateway.go` | model-serving | open | -| HIGH | Stored XSS via breakout in the JSON-LD structured-data block on every storefront page | `web/public-storefront/src/app/layout.tsx` | storefront-serving | open | -| HIGH | obol stack import trusts archive-declared file mode when extracting secrets into cfg.ConfigDir/c | `internal/stackbackup/tar.go` | secrets-config-defaults | open | -| HIGH | Unsanitized agent instance ID injects arbitrary lines into /etc/hosts (root-owned) via sudo tee | `internal/dns/resolver.go` | infra-plumbing-injection | **fixing** | -| HIGH | AgentWallet.Create has no immutability/reset guard — toggling it off strands a live signing key | `internal/monetizeapi/types.go` | crd-validation-consistency | open | -| MEDIUM | readArchiveManifest (Import's first action) has no decompression-bomb guard — CPU-exhaustion DoS | `internal/stackbackup/tar.go` | backup-export-import | open | -| MEDIUM | verify_release_checksum fails open (silently skips verification) instead of failing closed, unde | `obolup.sh` | self-update-integrity | **fixing** | -| MEDIUM | Seller-controlled catalog `endpoint` field is used verbatim as an outbound request target with n | `cmd/obol/buy.go` | buy-flow | open | -| MEDIUM | Unsanitized --id flows into filesystem paths and a recursive-delete target, enabling directory t | `internal/hermes/hermes.go` | hermes-agent-runtime | open | -| MEDIUM | Unvalidated `agents.defaults.workspace` path + symlink-following recursive copy lets an imported | `internal/openclaw/import.go` | openclaw-import | open | -| MEDIUM | ServiceOfferPriceTable's decimal price fields have no CRD pattern — a malformed value written ou | `internal/monetizeapi/types.go` | crd-validation-consistency | open | - -Fixing this round: the openclaw→Helm-YAML injection (arbitrary Helm values incl. -container image → RCE via `helmfile sync`), the agent `--id` → /etc/hosts -injection (which also closes the sibling `--id` path-traversal), the -`stack init --force` unconfirmed-destroy, and the self-update checksum -fail-open. The rest are a ranked worklist. Two clusters stand out: **destructive -ops without the confirmation gate** that Down/Purge already have (stack init, -backup import clobber, k3s kill-by-stale-PID), and **untrusted input reaching a -privileged sink** (openclaw.json → YAML/helm, agent id → /etc/hosts & fs paths, -backup tar → file modes & paths, catalog endpoint → outbound request). The -`obol stack up` hand-edit reversion is the exact risk your ops runbook already -works around by hand. diff --git a/hack/canary/run-canary.sh b/hack/canary/run-canary.sh deleted file mode 100755 index 44399b77..00000000 --- a/hack/canary/run-canary.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash -# run-canary.sh — layer 3 of docs/testing/proactive-bug-finding.md. -# -# A synthetic operator on a throwaway cluster: spin ephemeral k3s, install the -# stack, drive the REAL `obol` CLI through a scenario + flag-combination matrix, -# then check the invariants as BLACK-BOX probes (HTTP requests, on-chain reads, -# `obol sell status`) — not against hand-written expectations. This is the only -# layer that reproduces the field bugs that need real Traefik / cloudflared / -# chain behavior (route-age ties, router rejection, header stripping, nonce -# lag, real 402/settle). -# -# It is intentionally fail-LOUD: every invariant probe that trips prints -# CANARY-FAIL with the scenario, and the script exits non-zero so nightly CI -# turns red. Wire it into a nightly job; do NOT point it at a production cluster. -# -# Status: runnable skeleton. The scenario matrix and invariant probes are real; -# the cluster bring-up and the CLI/endpoint specifics marked TODO must be filled -# in against your environment (k3d vs kind, tunnel test-mode, funded test wallet -# for the on-chain scenarios). -set -euo pipefail - -OBOL="${OBOL:-obol}" -FAILURES=0 -fail() { echo "CANARY-FAIL: $*" >&2; FAILURES=$((FAILURES + 1)); } -step() { echo "== $*"; } - -# --- 1. ephemeral cluster --------------------------------------------------- -setup_cluster() { - step "creating ephemeral k3s" - # TODO: k3d cluster create obol-canary --wait (or kind); then `obol stack up` - # on the throwaway kubeconfig. NEVER run against a real cluster. - : "${KUBECONFIG:?set KUBECONFIG to the throwaway cluster before running}" -} -teardown_cluster() { step "destroying ephemeral k3s"; : ; } # TODO: k3d cluster delete obol-canary -trap teardown_cluster EXIT - -# --- 2. invariant probes (black-box) ---------------------------------------- -# Each probe returns non-zero (via fail) when an invariant is broken. They read -# only externally-observable state, so they catch bugs regardless of internals. - -# invariant 3 (status truth): an offer reported Ready must actually serve. -probe_ready_means_serves() { # $1=offer $2=ns $3=public-url - local ready; ready=$("$OBOL" sell status "$1" -n "$2" -o json 2>/dev/null | jq -r '.status.conditions[]?|select(.type=="Ready").status' || echo "") - if [ "$ready" = "True" ]; then - local code; code=$(curl -s -o /dev/null -w '%{http_code}' "$3" || echo 000) - # A Ready offer's public URL must not 404 to the storefront / hang. - [ "$code" = "402" ] || [ "$code" = "200" ] || fail "Ready=True but $3 returned HTTP $code (offer $2/$1)" - fi -} - -# invariant 5 (url truth): every URL in the 402 challenge is fetchable as written. -probe_402_urls_fetchable() { # $1=public-url - local body; body=$(curl -s -H 'Accept: application/json' "$1" || echo '{}') - echo "$body" | jq -r '.. | .resource? // .url? // empty' 2>/dev/null | while read -r u; do - [ -z "$u" ] && continue - case "$u" in - http://*local*|https://*) : ;; # ok - http://*) fail "402 challenge advertises non-https URL behind tunnel: $u" ;; - esac - curl -s -o /dev/null --max-time 5 "$u" || fail "402 challenge URL not fetchable: $u" - done -} - -# invariant 4 (no leak): public docs must not contain internal markers. -probe_no_internal_leak() { # $1=public-url (expects a sentinel planted in the Agent objective) - for doc in /api/services.json /openapi.json /.well-known/agent-registration.json /skill.md; do - curl -s "${1}${doc}" 2>/dev/null | grep -qiE 'CANARY_SECRET|cluster\.local|svc:8|internal-only' \ - && fail "internal marker leaked into public ${doc}" - done || true -} - -# invariant 2 (name injectivity): two offers whose (ns,name) dash-collide must both work. -probe_grant_no_collision() { - # (ns=foo-bar, name=baz) and (ns=foo, name=bar-baz): both Ready AND both serve. - # TODO: create both offers, then probe_ready_means_serves each; a collision - # manifests as one of them 500-ing while both report Ready. - : -} - -# --- 3. scenario matrix (the sequences humans hit) -------------------------- -# Each scenario is a sequence of REAL CLI operations followed by invariant -# probes. Add a row here whenever a field bug teaches a new sequence. -run_scenarios() { - step "scenario: switch payment network after registering" - # TODO: obol sell http svc --network base-sepolia --price 0.01 --pay-to $W ... - # register; then `obol sell update svc --network base`; then assert - # status.agentId is chain-scoped (not the sepolia id) and the published - # agent-registration.json has no stale entry. (field issue 1) - - step "scenario: configure tunnel before binding hostname to an offer" - # TODO: create storefront/tunnel first; then `obol sell http svc --hostname H`; - # then probe_ready_means_serves — a stale catch-all storefront route - # manifests as 404. (field issue 5) - - step "scenario: combine --max-in-flight and --rps" - # TODO: obol sell http svc --max-in-flight 10 --rps 5 ...; probe that the route - # actually serves (not silently rejected by Traefik). (field issue 3) - - step "scenario: malformed price (EU comma)" - # TODO: obol sell http svc --price 0,01 ... MUST be rejected by the CLI, not - # accepted and mis-priced/panicking. (2026-07-16 audit critical) - - step "scenario: pass a path to --origin/--endpoint" - # TODO: obol sell register --origin https://host/some/path MUST be rejected. - # (field issue 7) - - step "scenario: dash-colliding offer names across namespaces" - probe_grant_no_collision # (2026-07-16 audit high) -} - -main() { - setup_cluster - run_scenarios - if [ "$FAILURES" -gt 0 ]; then echo "CANARY: $FAILURES invariant(s) broken"; exit 1; fi - echo "CANARY: all invariants held" -} -main "$@"