Skip to content

fix(openclaw): launch-fix — rebuild agent-shield on @palveron/sdk + tiered fail policy#1

Open
disruptivetrends wants to merge 13 commits into
masterfrom
fix/openclaw-launch-sdk-contract
Open

fix(openclaw): launch-fix — rebuild agent-shield on @palveron/sdk + tiered fail policy#1
disruptivetrends wants to merge 13 commits into
masterfrom
fix/openclaw-launch-sdk-contract

Conversation

@disruptivetrends

Copy link
Copy Markdown
Contributor

Fixes the launch blockers proven in ANALYSE_AGENT_SHIELD_LAUNCH_READINESS.md. Read-only analysis → this build.

Root cause (B1)

agent-shield hand-built the /verify body, drifting from the gateway contract (gateway requires top-level prompt, reads tool_name from context, agent_id from metadata). Every verify failed server-side and the blanket fail-open swallowed it into a silent ALLOW — the agent ran ungoverned. Fix: one contract, one source — delegate verify/health/listPolicies to @palveron/sdk (also brings retries + circuit breaker). Verified the SDK's real wire format against sdk-typescript@1.1.0 before building on it.

What changed

Block Fix
B1 src/client.mjs is a thin facade over @palveron/sdk; new contract test asserts the on-the-wire body (prompt top-level, context.tool_name, metadata.agent_id) through the real SDK
B2 init preflight uses sdk.health() → real GET /health (was /api/v1/health, which 404s in prod and killed init at step 1)
B3 new src/fail-policy.mjs: fail-LOUD on contract/auth (never ALLOW); transport tiered by risk (HIGH → fail-closed BLOCK, MEDIUM/LOW → fail-open ALLOW); AGENT_SHIELD_FAIL_CLOSED override; MCP catch returns ERROR, never silent ALLOW
B4 README/SKILL: honest advisory wording; hard in-path before_tool_call hook called out as roadmap
B5 correct MCP invocation everywhere: npx -y -p @palveron/agent-shield agent-shield-mcp
B6 removed dead BYOM LLM-key forwarding; BYOM is dashboard/server-side
B7 publishConfig.access: "public"
B8 added MIT LICENSE; README license aligned to MIT
B10 init reports the real activated+created count (no hardcoded "8")

Also: dropped the now-redundant client circuit-breaker (SDK owns resilience), removed agent-shield.zip, added .gitignore + CI (node 18/20/22).

Tests

node --test13 passing (contract + tiered fail policy, incl. "validation/auth → throws, never ALLOW" and "HIGH-risk outage → BLOCK").

⚠️ Decisions for @arndt (NOT decided here — please confirm)

  • E1 — Version: stays 0.1.0 (honest pre-release while enforcement is advisory). Go 1.0.0 only after the in-path hook + verified live E2E. No code change; confirm?
  • E3 — Default fail behavior: HIGH-risk now fails closed during a Palveron outage — a deliberate behavior change vs. the old "always fail open" README. Steerable via AGENT_SHIELD_FAIL_CLOSED. Confirm this is the intended default?
  • E4 — Two MCP products: added a "Which package do I need?" section (agent-shield = OpenClaw/zero-config; @palveron/mcp-server = Cursor/Claude-Code). No consolidation now. OK?

Note on base

Supersedes the local-only WIP commit cfbde5a ("honor body.decision on 4xx") — the SDK handles that contract natively, so this PR is based on origin/master (3d8ab9e).

Live E2E (you run it — needs a TEST key)

Acceptance #2/#3 require a live smoke against gateway.palveron.com with a pv_test_ project key (never the prod core key). Script committed at scripts/smoke-live.mjs (refuses any key not starting with pv_test_):

# PowerShell
$env:PALVERON_API_KEY="pv_test_xxx"
node scripts/smoke-live.mjs

It checks: health, benign→ALLOW, rm -rf /→BLOCK, fake AKIA…→BLOCK (soft), outage HIGH→fail-closed / MEDIUM→fail-open, invalid key→fail-loud throw, and init (real count + idempotency + status). Please paste the output here.

🤖 Generated with Claude Code

disruptivetrends and others added 3 commits June 16, 2026 11:15
…il policy

Resolves the launch blockers from ANALYSE_AGENT_SHIELD_LAUNCH_READINESS.md.

Root cause (B1): agent-shield hand-built the /verify body, drifting from the
gateway contract (gateway requires top-level `prompt`, reads tool_name from
context and agent_id from metadata). The mismatch made every verify fail
server-side and the blanket fail-open swallowed it into a silent ALLOW —
the agent ran ungoverned. Fix: delegate verify/health/listPolicies to
@palveron/sdk, the single source of truth for the wire contract (+ retries +
circuit breaker). One contract, one implementation, no drift.

- B1: src/client.mjs is now a thin facade over @palveron/sdk; new contract
  test asserts the on-the-wire body (prompt top-level, context.tool_name,
  metadata.agent_id) through the real SDK.
- B2: init preflight uses sdk.health() → real GET /health (was /api/v1/health,
  which 404s in prod and killed init at step 1).
- B3: new src/fail-policy.mjs — fail-LOUD on contract/auth errors (never
  ALLOW); transport failures tiered by risk (HIGH → fail-closed BLOCK,
  MEDIUM/LOW → fail-open ALLOW); AGENT_SHIELD_FAIL_CLOSED override. MCP catch
  returns ERROR, never silent ALLOW.
- B4: README/SKILL — honest advisory wording (no "blocks before they execute"
  guarantee); hard in-path hook called out as roadmap.
- B5: correct MCP invocation everywhere (npx -y -p @palveron/agent-shield
  agent-shield-mcp) — agent-shield-mcp is a bin, not a standalone package.
- B6: removed dead BYOM LLM-key forwarding; BYOM is configured server-side
  (dashboard). README/SKILL/help corrected.
- B7: package.json publishConfig.access=public.
- B8: added MIT LICENSE; README license aligned to MIT.
- B10: init reports the real activated+created policy count (no hardcoded "8").
- Dropped the now-redundant client circuit-breaker (SDK owns resilience).
- Removed agent-shield.zip artifact; added .gitignore and CI (node 18/20/22).

Note: supersedes the local-only WIP commit cfbde5a ("honor body.decision on
4xx") — the SDK handles that contract natively.

Tests: 13 passing (node --test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scripts/smoke-live.mjs drives a real gateway: health preflight, benign→ALLOW,
rm -rf→BLOCK, fake AKIA→BLOCK (soft), simulated outage HIGH→fail-closed /
MEDIUM→fail-open, invalid key→fail-loud (no silent ALLOW), and init E2E
(real count + idempotency + status). Refuses any key not starting with
pv_test_ so it can never touch the production project. Not shipped (excluded
from package.json "files").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coordinated with the gateway fix (palveron/gateway) that removes the
schema-invalid SYSTEM-policy slug activation (setup returned 500 because
`UPDATE "Policy" ... WHERE slug = $2` hit a non-existent column) and now
returns `policies_active` — the real number of ACTIVE shield/system policies,
stable across idempotent re-runs.

- init prefers result.policies_active; falls back to the legacy
  activated+created sum for older gateways.
- smoke-live.mjs: assert setup succeeds with active>0 (no 500) and that an
  idempotent re-run reports the SAME active count (created drops to ~0).
  Dropped the incorrect hardcoded "== 8" expectation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@disruptivetrends

Copy link
Copy Markdown
Contributor Author

Update — B10 resolved (the seeding caveat above is now fixed, not just flagged).

Investigating the "8 rules" seeding statically turned up something worse than a count gap: POST /api/v1/setup/openclaw-shield was returning HTTP 500 on every call. Its activate_system_policies() ran UPDATE "Policy" … WHERE slug = $2, but the Policy table has no slug column (only Organization.slug exists) → Postgres undefined_column (42703). So init could never complete the setup step.

Fixed in palveron/gateway#19: remove the dead, schema-invalid slug activation; return a truthful policies_active count (stable on idempotent re-runs). Rust gates all green (fmt/check/clippy -D warnings/test: 540 passed).

This PR's latest commit (a5255b9) coordinates: init now reads policies_active, with a fallback to the legacy sum so rollout order is flexible.

Rollout: merge gateway#19 (or confirm it's deployed) before relying on the live init/smoke; the client falls back gracefully if the gateway isn't updated yet.

…,F5)

Follow-up pre-publish hardening on top of the SDK migration.

- F1 (security): a governance_check missing tool_name/input no longer returns a
  silent ALLOW — it returns decision ERROR (missing_required_field). That was
  the one reachable bypass; without tool_name the risk level isn't even knowable.
- F4 (correctness): remove the dead LOW-risk skip branch in mcp-server — every
  check now goes through verify (evaluated AND traced; a local skip would ALLOW
  without a gateway call, making the action invisible in the dashboard). Removed
  the now-trivial `shouldVerify` export; classifyRisk doc made truthful (no LOW
  tier; unknown → MEDIUM; risk only drives the outage fail policy). SKILL.md +
  README risk model aligned (HIGH→verify/fail-closed, MEDIUM incl. unknown→
  verify/fail-open, no skip).
- F5 (hardening): an unrecognized gateway verdict is no longer silently mapped
  to ALLOW. verify tiers it: HIGH → BLOCK (unknown_decision_failclosed,
  _anomaly), else ALLOW + _anomaly + console.warn. Added KNOWN_SDK_DECISIONS.
- F2 (user value): updateOpenClawConfig now finds ~/.openclaw/openclaw.json (the
  real OpenClaw global default) first, so the zero-config wiring actually fires.
  Extracted to src/openclaw-config.mjs with injectable home/cwd for testing;
  preserves existing mcpServers entries.
- Quick wins: bin header comment no longer says "8 rules"; cmdTest no longer
  defaults a missing decision to ALLOW (shows ERROR).

Tests: node --test → 22 passing (+9: F1/F2/F4/F5). New files: test/hardening,
test/mcp-server (stdio + mock gateway), test/openclaw-config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@disruptivetrends

Copy link
Copy Markdown
Contributor Author

Follow-up hardening (F1–F5)

Pre-publish hardening pass (commit b53da94). Every behavior below has a test; node --test22 passing (+9).

  • F1 (Security)governance_check with a missing tool_name/input no longer returns a silent ALLOW; it returns decision: ERROR (missing_required_field). That was the one reachable governance bypass; without tool_name the risk level isn't even knowable. (test: stdio MCP server → ERROR for both missing-field cases.)
  • F4 (Correctness) — removed the dead LOW-risk skip branch in mcp-server.mjs; every check goes through verify (evaluated and traced — a local skip would ALLOW without a gateway call, making the action invisible in the dashboard). Removed the now-trivial shouldVerify export; classifyRisk docs made truthful (no LOW tier). SKILL.md + README + docs risk model aligned. (tests: classifyRisk never LOW; shouldVerify undefined; a "low-risk" tool provably hits /verify.)
  • F5 (Hardening) — an unrecognized gateway verdict is no longer silently mapped to ALLOW. verify tiers it: HIGH → BLOCK (unknown_decision_failclosed, _anomaly), else ALLOW + _anomaly + console.warn. (test: injected SDK returning 'WAT' → BLOCK on exec, ALLOW+anomaly on read_file.)
  • F2 (User value)updateOpenClawConfig now finds ~/.openclaw/openclaw.json (the real OpenClaw global default) first, so the zero-config wiring actually fires. Extracted to src/openclaw-config.mjs with injectable home/cwd; preserves existing mcpServers. (test: temp home dir; foreign entry preserved; correct -p invocation.)
  • Quick wins — bin header comment no longer says "8 rules"; cmdTest no longer defaults a missing decision to ALLOW (shows ERROR).

⚠️ Decision for @arndt (F4 — please confirm, can overrule)

The documented LOW "trace only, no check" tier was never implemented (classifyRisk never returned LOW; the skip branch was dead code). I aligned the docs to the code: everything ≥ MEDIUM is verified + traced, no skip tier; removed the dead code. The cheap low-risk trace-only ingest optimization (visibility without a full verify) is noted as Phase 1.5, not built now.
Alternative (if you prefer): instruct the agent to literally check everything — stronger visibility promise, higher quota usage. Default is the alignment above.

disruptivetrends and others added 9 commits June 16, 2026 17:23
…override

- #3 (rm -rf): the catalog provisions destructive_action as REQUIRE_APPROVAL,
  so a governed destructive command surfaces as APPROVAL, not BLOCK. Accept
  BLOCK OR APPROVAL as success (Goal A-2 acceptance #4).
- Guard: allow a pv_live_ key only with SMOKE_I_UNDERSTAND=throwaway-project
  (dedicated throwaway project, never core) + a visible warning; pv_test_ runs
  as before. Local test helper, not shipped (excluded from package.json files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…waway project + .env.smoke.example

The smoke guard required a pv_test_ key by default and hid a non-test run behind
a self-invented SMOKE_I_UNDERSTAND="throwaway-project" sentinel. No pv_test_ key
can be minted, so that default path was dead and the override was the only way to
run — a hidden crutch. There is no test/sandbox key today; isolation comes from a
dedicated disposable project, not a key prefix.

- Drop the pv_test_/SMOKE_I_UNDERSTAND heuristic. Require PALVERON_API_KEY +
  PALVERON_SMOKE_PROJECT (naming the throwaway project is the conscious
  confirmation that you are pointing at a disposable target).
- Add committed .env.smoke.example + gitignore .env.smoke; run via
  `node --env-file=.env.smoke scripts/smoke-live.mjs` (no hidden env magic).
- Document the live-smoke flow in the README.
- Fix pv_test_ → pv_live_ across the bad-key fixture, test fixtures and the
  client.mjs apiKey doc comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… @palveron/sdk ^1.2.0

Consume the published @palveron/sdk@1.2.0 cause-preservation fix: a TLS-trust
transport failure (HTTPS-inspecting AV/firewall whose root CA Node doesn't
trust) now surfaces an honest, actionable BLOCK reason (gateway_tls_untrusted)
plus a remediation hint (--use-system-ca / NODE_EXTRA_CA_CERTS) instead of the
opaque gateway_unavailable_failclosed — while keeping fail-closed on HIGH risk.
Genuine connect/DNS/timeout failures keep the generic reason (no mis-reframe).

- error-cause.mjs: dependency-free causeChain + classifyTransportFailure (TLS
  trust codes), usable on both the diagnostic and live governance paths.
- client.mjs: inspect the (now-preserved) SDK cause chain; tiered fail policy.
- fail-policy.mjs: optional reason/hint override on the fail-closed branch.
- mcp-server.mjs: surface the hint in the governance_check response.
- README: AV/firewall TLS-inspection troubleshooting (never disable verification).
- package.json: bump @palveron/sdk to ^1.2.0; strip stray UTF-8 BOM + repair
  mojibaked em-dash in description.
- tests: tls-interception suite (45/45 green) against the published SDK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…acy error shapes (own read, SDK dep unpublished)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant