Skip to content

fix(api): name the rejected plugin id and secret binding destination in 400s - #1431

Merged
freeqaz merged 4 commits into
mainfrom
findings-sweep-1-api
Oct 5, 2026
Merged

freeqaz merged 4 commits into
mainfrom
findings-sweep-1-api

Conversation

@freeqaz

@freeqaz freeqaz commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Two 400 INVALID_REQUEST responses said what was wrong but not where:

  • Plugin not offered by the selected Driver (fix(plugins): name the selected driver when a plugin id is not offered #1404): A plugin selection names a plugin that the selected Plugin Driver (codex-plugin) does not offer…. With several selections the caller has to guess which one was rejected, for example after an Installation switches its Plugin Driver.
  • Reserved Secret binding destination: A secret binding uses a reserved or invalid environment destination. This names neither the binding key nor the rule (reserved prefix, reserved name, or invalid format). A Configuration with ten bindings gave no hint which one to change.

Change

Plugin ID. The runtime translator's plugin-ID errors carry the rejected selection key. PluginPolicyValidationError("unknownPlugin", driverId, pluginId) appends the key to the existing message after the rule (A plugin selection names a plugin that the selected Plugin Driver (occ-plugin) does not offer: codex-plugin:linear@openai-curated-remote. Check each plugin ID…), so the 256-character cap cuts the advice before the rule, and exposes it as pluginId. HTTP adds a details entry { path: "/plugins/<id>", code: "INVALID_VALUE" }. This is the JSON-pointer mechanism #1409 added for /cursor, with ~ and / escaped. The key is the caller's own selection key, which the API contract limits to [A-Za-z0-9._~:@-]{1,253}.

Secret binding destination. normalizeSecretBindings (contracts) now reports which rule a destination broke, and the destination key as error properties:

Rule Message
reserved prefix A secret binding destination uses the reserved prefix OPENCLAW_*: OPENCLAW_TOKEN.
reserved name A secret binding destination is a reserved process or platform variable name: PATH.
invalid format A secret binding destination is not a valid environment variable name: it must match ^[A-Za-z_][A-Za-z0-9_]*$ and have at most 253 characters. (the key is not echoed)

The rule comes before the key, so the 256-character message cap never cuts the rule. normalizeRequestSecretBindings passes the key into SecretBindingValidationError.destination. HTTP turns that into a detail: /secretBindings/<key> for Configuration create and update, and /configuration/secretBindings/<key> for Agent provisioning. Reserved destinations get INVALID_VALUE at the key. A malformed key is never echoed: its INVALID_FORMAT detail points at the binding map itself. A detail path over the contract's 512-character limit also points at the map.

  • Only the destination key, which the caller submitted, appears in the message or details. No Secret value, Secret ID or Namespace is added. Other SecretBindingValidationErrors, such as cross-Namespace references, keep their message and have no details.
  • Worker (revisionSecretBindings) and Kubernetes Compute both catch normalizeSecretBindings failures and replace the message with their own static result, so their behavior is unchanged. Preset validation keeps its own static message.
  • The rule is in the message and the key is in the detail path. The ErrorDetail schema (path, code) is unchanged, so no OpenAPI change.

Docs: docs/reference/agent-plugins.md (failure list), docs/flows/configuration-driver.md (binding admission), and a changelog line in docs/flows/agent-provisioning.md.

Verification

Review (opus): APPROVE WITH NITS. The third commit (cf4228d68) addresses the message order, malformed-key echo and test-gap nits. The delta re-review found two stale flow-doc lines, fixed in acb334cd5.

  • node --test tests/conformance/http-errors.test.mjs tests/conformance/plugin-driver.test.mjs: 104/104 pass. tests/conformance/presets.test.mjs passes too. A new http-errors case drives normalizeRequestSecretBindings through requestFailure for a reserved prefix, a reserved name (provisioning path) and a malformed key (INVALID_FORMAT at the map), and checks that the Secret ID is never in the failure.
  • node --test tests/integration/secret-api.test.mjs tests/integration/occ-api.test.mjs: 75/75 pass.
  • Regression proof: with this PR's apps/ and packages/ changes reverted, the extended tests fail. Seven tests fail:
    • HTTP error mapping: a plugin selection the selected Plugin Driver does not offer
    • HTTP error mapping: a reserved Secret binding destination
    • Plugin Drivers name themselves when a selection names a plugin they do not offer
    • Agent plugin maps reject structural errors… (the PATCH with an OCC and a Codex selection gets the ID and /plugins/<id> detail)
    • Configuration and Agent writes reject invalid Secret bindings as invalid requests
    • Agent provisioning API validates inline configuration with existing Secret references
    • Harness source admission rejects foreign references… (nine reserved destinations: exact message, /secretBindings/<key> detail, and no Secret ID in the body)
  • Full tests/conformance and tests/integration run: 2915 pass, 423 skipped, 18 fail. The 18 are the Podman/dev-up, CLI release, and repository-credential container and Git suites. The same files fail on a main tree on this host (16/81 in those files), so they are environment failures, not this change.
  • tsc --build clean; eslint --max-warnings 0 and prettier --check on touched files clean.

Open PRs touching the same files

I ran gh pr diff --name-only across all 94 open PRs, then compared git merge-tree against each overlapping PR for this head and for origin/main, by file and by hunk:

PR Shared files Conflicts with main Conflicts with this PR
#378 drivers/plugin/index.ts, occ/src/errors.ts 21 files (2 hunks in plugin/index.ts) same files, same hunks
#489 drivers/plugin/index.ts, agent-plugins.md, plugin-driver.test.mjs 18 files (1 hunk each in plugin/index.ts, agent-plugins.md) same
#508 runtime-translator.ts, plugin-driver.test.mjs 4 files same 4 files
#299 http/errors.ts, occ/src/errors.ts 20 files (1 hunk in http/errors.ts) same
#307 secret-bindings.ts, occ/src/errors.ts 53 files (1 hunk each in secret-bindings.ts, occ/src/errors.ts) same
#913 http/errors.ts, occ/src/errors.ts 1 file same 1 file
#914 agent-provisioning.md, occ-api.test.mjs none none
#1370 configuration-driver.md none none
#115, #144, #252 occ-api.test.mjs (#115 also plugin-driver.test.mjs) 26 / 29 / 58 files (1 / 1 / 2 hunks in occ-api.test.mjs) same
#848 occ-api.test.mjs 5 files same 5 files
#906, #1400 none 1 / 0 files same

This PR adds no new conflicted files or hunks. An earlier revision conflicted with #914 and #1370 in flow docs; the second commit moves those notes out of their hunks.

Risks

  • Error text changes: clients that matched the old static secret binding message (A secret binding uses a reserved or invalid environment destination.) no longer match. Nothing in the repository (console, occ, docs) matches it. The unknown-plugin message keeps its full original prefix, A plugin selection names a plugin that the selected Plugin Driver (<id>) does not offer.
  • Messages now echo a caller-submitted destination key (only when well-formed) or plugin ID. Both are restricted to plain identifier characters, and HTTP still caps messages at 256 characters and replaces control characters.
  • The console's error-detail filter (console/api-client.mjs) drops paths containing :, @ or ., so it ignores the plugin detail. The message still names the ID, and the console is unchanged.
  • On deploy or a PATCH that omits plugins, the rejected selections come from the stored Agent. In that case /plugins/<id> points at the Agent's plugins field rather than the request body.

…in 400s

A plugin selection the selected Plugin Driver does not offer named the
Driver but not the selection, so with several selections the caller had
to guess. The error now carries the rejected selection key: the message
names it and HTTP adds a /plugins/<id> INVALID_VALUE detail, the same
JSON-pointer mechanism the log route uses for /cursor.

A Secret binding with a reserved destination returned a static 400 that
named neither the key nor the rule. normalizeSecretBindings now says which
rule broke (reserved prefix, reserved name, or invalid format) and names a
well-formed destination; request admission adds a /secretBindings/<key>
(or /configuration/secretBindings/<key> for provisioning) detail. No
Secret value or ID is echoed. Worker and Compute discard this message,
so their behaviour is unchanged.
Drop the provisioning paragraph edit (#914 rewrites that line) and the
Configuration changelog entry (#1370 adds one at the same spot); the
provisioning changelog entry still records the detail pointer.
@freeqaz
freeqaz requested a review from a team as a code owner October 5, 2026 23:24
@clawsweeper

clawsweeper Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Oct 5, 2026
@clawsweeper

clawsweeper Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Codex review: needs changes before merge. Reviewed October 5, 2026, 7:42 PM ET / 23:42 UTC (Revision 2).

ClawSweeper review

What this changes

The PR makes API rejection messages identify unsupported plugin selections and reserved Secret binding destinations, adds field pointers, and extends tests and documentation.

Merge readiness

⛔ Needs changes before merge - 2 items remain

The diagnostics remain useful and are absent from current main. No blocking runtime or security defect was found; one small documentation correction remains.

Priority: P3
Reviewed head: cf4228d68bdf95a9ba2dcaf286292d53fd51ce13

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) A focused implementation with meaningful API coverage and no blocking runtime defect; the final safety behavior needs a small documentation correction.
Proof confidence 🌊 off-meta tidepool Not applicable: The supplied MEMBER classification exempts this PR from ordinary external-contributor proof. Reported target-native API tests cover the changed validation and HTTP mapping; unrelated creation-retry media does not demonstrate these diagnostics. No authority or stored-data contract changes.
Patch quality 🐚 platinum hermit (4/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Not applicable Not applicable: The supplied MEMBER classification exempts this PR from ordinary external-contributor proof. Reported target-native API tests cover the changed validation and HTTP mapping; unrelated creation-retry media does not demonstrate these diagnostics. No authority or stored-data contract changes.
Evidence reviewed 7 items Introduced patch: Read all 13 introduced file diffs using the pinned merge-base and original head. The patch changes diagnostics, error metadata, tests, and documentation; it introduces no dependency, deployment, persistence, or authorization change.
Current main still needs the diagnostics: Current main maps both validation errors to a message-only 400. The merged predecessor #1404 identifies the selected Driver but does not implement this PR's rejected-ID detail. The merged #1409 concerns log-query diagnostics, a distinct behavior.
Safe destination diagnostics: Destination validation preserves the allowed-name grammar and reserved-name rejection. Malformed destinations receive INVALID_FORMAT at the binding map without echoing the key; well-formed reserved names receive INVALID_VALUE at their escaped pointer. Secret values and references are not added to these errors.
Findings 1 actionable finding [P3] Document malformed destinations as map-level failures
Security None None.

How this fits together

OpenClaw Control Plane validates Agent plugin selections and Configuration Secret bindings before accepting resource changes. Its HTTP error mapper turns rejected input into bounded messages and field pointers for callers.

flowchart TB
A[Agent or Configuration request] --> B[API admission]
B --> C[Plugin selection validation]
B --> D[Secret destination validation]
C --> E[HTTP error mapping]
D --> E
E --> F[400 response with rule and field pointer]
Loading

Before merge

  • Document malformed destinations as map-level failures (P3) - The final change deliberately avoids echoing malformed destination keys and returns INVALID_FORMAT at /secretBindings, but this paragraph promises a named key and /secretBindings/&lt;key> for every destination failure. Distinguish well-formed reserved destinations from malformed destinations, and correct the matching new entry in docs/flows/agent-provisioning.md. This mismatch follows the latest malformed-key safety change.
  • Complete next step (P2) - Correct the Configuration and provisioning flow descriptions to distinguish reserved destination keys from malformed keys.

Findings

  • [P3] Document malformed destinations as map-level failures — docs/flows/configuration-driver.md:129-131
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production and test delta production +121/-17; tests +148/-23 Production growth carries diagnostic metadata through existing owners; tests extend API and Driver coverage without adding a competing implementation.

Technical review

Best possible solution:

Keep the bounded field-specific diagnostics and document the distinction between reserved-name pointers and malformed-name map pointers.

Do we have a high-confidence way to reproduce the issue?

Yes, source establishes the diagnostic gap: submit a plugin from the other Driver or a reserved Secret destination through the existing API routes. Current main returns no pointer identifying the rejected selection or destination; this review did not execute those requests.

Is this the best way to solve the issue?

Yes, extending the existing validation errors and HTTP detail mechanism is a focused solution. The flow documentation needs to reflect the final malformed-key safety behavior.

Full review comments:

  • [P3] Document malformed destinations as map-level failures — docs/flows/configuration-driver.md:129-131
    The final change deliberately avoids echoing malformed destination keys and returns INVALID_FORMAT at /secretBindings, but this paragraph promises a named key and /secretBindings/&lt;key> for every destination failure. Distinguish well-formed reserved destinations from malformed destinations, and correct the matching new entry in docs/flows/agent-provisioning.md. This mismatch follows the latest malformed-key safety change.
    Confidence: 0.99

Overall correctness: patch is correct
Overall confidence: 0.94

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 0d9bb168a060.

Labels

Label changes:

No label changes.

Label justifications:

  • P3: This improves existing rejection diagnostics without changing admission decisions or resource lifecycle behavior.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🌊 off-meta tidepool and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Not applicable: The supplied MEMBER classification exempts this PR from ordinary external-contributor proof. Reported target-native API tests cover the changed validation and HTTP mapping; unrelated creation-retry media does not demonstrate these diagnostics. No authority or stored-data contract changes.

Evidence

Acceptance criteria:

  • [P1] git diff --check.
  • [P1] pnpm format:check.
  • [P1] pnpm docs:check-length.
  • [P1] pnpm docs:check.

What I checked:

  • Introduced patch: Read all 13 introduced file diffs using the pinned merge-base and original head. The patch changes diagnostics, error metadata, tests, and documentation; it introduces no dependency, deployment, persistence, or authorization change. (cf4228d68bdf)
  • Current main still needs the diagnostics: Current main maps both validation errors to a message-only 400. The merged predecessor fix(plugins): name the selected driver when a plugin id is not offered #1404 identifies the selected Driver but does not implement this PR's rejected-ID detail. The merged fix(logs): say why a log download with a cursor is rejected #1409 concerns log-query diagnostics, a distinct behavior. (apps/controller/src/http/errors.ts:653, 0d9bb168a060)
  • Safe destination diagnostics: Destination validation preserves the allowed-name grammar and reserved-name rejection. Malformed destinations receive INVALID_FORMAT at the binding map without echoing the key; well-formed reserved names receive INVALID_VALUE at their escaped pointer. Secret values and references are not added to these errors. (packages/occ/src/agent-provisioning.ts:79, cf4228d68bdf)
  • Target-native integration coverage: The changed API cases exercise the actual controller app, OCC admission, and HTTP mapping for plugin PATCH, Configuration create/update, and inline Agent provisioning. Unchanged-state checks and Secret-ID exclusion assertions protect consequential behavior. Test Drivers and in-memory storage do not prove installed runtime behavior. The body reports 75 passing integration cases and a failing-before regression comparison; no tests were executed during this read-only review. (tests/integration/occ-api.test.mjs:3203, cf4228d68bdf)
  • Documentation disagrees with final malformed-key handling: The Configuration flow promises that every destination failure names its key and points at that key. The provisioning flow's new changelog makes the same claim. The final implementation deliberately omits malformed keys and points at the map. (docs/flows/configuration-driver.md:130, cf4228d68bdf)
  • Feature-history routing: Main-path logs identify freeqaz's earlier plugin diagnostics and Secret-binding admission work, and stevenlee-oai's provider-credential work touching the binding grammar. GitHub verified freeqaz as author of the merged plugin predecessor and commit 223dd6d. Historical blame and some parent-tree comparisons could not complete because required promisor objects were unavailable; no source-line introduction attribution is claimed. (packages/occ/src/agent-provisioning.ts, 223dd6dc2288)

Likely related people:

  • freeqaz: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • stevenlee-oai: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Correct both flow descriptions so malformed keys are omitted and INVALID_FORMAT points at the binding map.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (1 earlier review cycle)
  • reviewed 2026-10-05T23:29:20.367Z sha 1b7add5 :: needs maintainer review before merge. :: none

…d keys (review)

The plugin message named the ID before the rule, so a long Codex ID let
the 256-character cap cut "does not offer". Keep the original
"A plugin selection names a plugin that the selected Plugin Driver (X)
does not offer" prefix and append the ID after it.

A malformed Secret binding destination was echoed raw into the detail
path, control characters included. Point INVALID_FORMAT details at the
binding map instead. A conformance test drives normalizeRequestSecretBindings
through requestFailure for reserved prefix, reserved name and invalid
format, and checks the Secret ID never appears.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant