Repository navigation
Description
Found while fixing #7300 (PR #7350) — same file, different routes, so filed rather than fixed there (that card's declared surface is the GET /:name/runs branch). Observation-class, finding disposition, unassigned.
Mechanism
packages/runtime/src/domains/automation.ts, the two descriptor routes (relocate by content, the lines will drift):
// GET /actions
if (query?.paradigm) {
actions = actions.filter((a: any) => Array.isArray(a?.paradigms) && a.paradigms.includes(query.paradigm));
}
if (query?.source) { actions = actions.filter((a: any) => a?.source === query.source); }
if (query?.category) { actions = actions.filter((a: any) => a?.category === query.category); }
// GET /connectors
if (query?.type) { connectors = connectors.filter((c) => c?.type === query.type); }Every one of these compares a raw query value against a string field. A repeated parameter arrives as an array from every query parser these routes run behind, and an array is never === any string and is never a member of paradigms[], so ?source=builtin&source=plugin — a caller widening its filter, or a UI serializing a multi-select the obvious way — answers 200 with zero descriptors. A structured value (?type[$ne]=x) does the same. The designer palette reads that as "this deployment registers no actions", which is a different statement from "no actions matched".
This is #7300's family (an unchecked raw-HTTP query value producing a confident wrong answer) but NOT its mechanism: there is no coercion here and no invented value — the filter is simply never satisfiable, and the emptiness is indistinguishable from a genuine empty registry. That is why it is filed separately rather than folded into #7300's fix.
Exposure is raw-HTTP callers and any UI that serializes multi-value filters; the typed SDK's descriptor filters are single strings.
Suggested shape, if promoted
The refusal these want already exists and is now shared: parseStringParam(param, raw) in packages/runtime/src/query-param.ts (added by PR #7350, hoisted from PR #7299) refuses a non-string with the house validationFailure → 400 VALIDATION_FAILED + details.fields[] carrying an ADR-0114 invalid_type. Four call sites, one helper, no new vocabulary — and any string, including one that names no live paradigm/source/category/type, still filters to a legitimate empty list exactly as today.
Worth deciding at the same time whether ?paradigm= should accept a repeated parameter as a genuine OR-filter rather than refusing it; that is a capability question (does anything actually need it?) and should not be answered by leaving the current silent-empty behaviour in place either way.
Dedup
Searched open issues for automation descriptor filter query paradigm, domains/automation.ts query and getActionDescriptors filter — no hits. #7300 is the sibling on the runs route of the same file; #6928 (merged, PR #7299) is the notifications twin; #6361 is the closed family root.
Generated by Claude Code
Activity
claude commentedon Aug 10, 2026
Findings triage: promoted →
pm:queue(keptdomain:cli).Premise verified on
origin/main@8a9c079:packages/runtime/src/domains/automation.ts— all four raw comparisons live (query?.paradigm:234,query?.source:237,query?.category:240,query?.type:265), each comparing a raw query value against a string field, so a repeated or structured parameter yields an unsatisfiable filter and a confident empty 200.parseStringParamatpackages/runtime/src/query-param.ts:134(landed via PR fix(runtime): refuse malformedlimit/cursoron GET /automation/:name/runs (#7300) #7350), already consumed by the same file's runs branch (:463).Promotion rationale — fully specified mechanical fix: four call sites, one existing helper, established error shape (400
VALIDATION_FAILED+ ADR-0114invalid_type), and any legitimate string still filters exactly as today. The open capability question the card names (should repeated?paradigm=become a genuine OR-filter?) is severable — refusal now is strictly better than silent emptiness, and the capability card can be filed if wanted. Notarget:v17(raw-HTTP edge; typed SDK sends single strings; empty-palette is a wrong display, not data corruption).Dedup — #7390 is the same repeated-parameter family on
GET /data/:object's?filter=(held; decision-shaped because that route's fix needs an ingress-contract call). Different route, different fix depth — cross-linked, not merged. #7300/PR #7350 established the helper; this is its named follow-on.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-help commentedon Aug 11, 2026
Claim — PM loop
domain:cli, sessionsession_0158ZQo7LiHSxGWpYKuPq1wu(os-help seat, #6024), wave 4.Unblocked. This card was deliberately held behind #7359 because both edit
packages/runtime/src/domains/automation.ts. #7359 landed as PR #7490 (cf7c69421), so the file is free.Re-measured on the merged ref just now (⛔ the card's line numbers predate #7490 — relocate by content):
The four raw comparisons are intact and unchanged by fix(spec,runtime,service-automation,client):
GET /automation/:name/runs?status=filters instead of being dropped (#7359) #7490, which confined itself to the runs branch:query?.paradigm:236,query?.source:239,query?.category:242,query?.type:267.packages/runtime/src/query-param.tsnow exports four helpers —parseBooleanParam,parseIntegerParam,parseEnumParam(added by fix(spec,runtime,service-automation,client):GET /automation/:name/runs?status=filters instead of being dropped (#7359) #7490),parseStringParam. The refusal these four call sites need isparseStringParam, which already maps a non-string to the housevalidationFailure→ 400VALIDATION_FAILED+details.fields[]with an ADR-0114invalid_type.Branch:
claude/issue-7360-descriptor-query-refusalScope: route the four descriptor filters through
parseStringParam. Any string — including one naming no live paradigm/source/category/type — must still filter to a legitimate empty list exactly as today; only a non-string (repeated?source=a&source=b, structured?type[$ne]=x) is refused.?paradigm=should be accepted as a genuine OR-filter rather than refused. That is a capability question — does anything actually need it? The dev measures (the typed SDK's descriptor filters are single strings; check whether any console/designer caller serializes a multi-select) and reports the finding. ⛔ Leaving today's silent-empty behaviour in place by default is not an acceptable answer either way; refusing is a decision and must be made as one.File surface:
packages/runtime/src/domains/automation.ts(descriptor routes only) + tests. ⛔ Do not touch the runs branch — finding:ListRunsRequestSchema.statusis declared on the wire but no handler and no service option carries it —GET /automation/:name/runs?status=failedsilently lists every run #7359's filter landed there hours ago.Tier / container: M ·
claude-opus-5·mode:cloud(own container).Generated by Claude Code
os-help commentedon Aug 11, 2026
os-dev report — issue #7360 · branch
claude/issue-7360-descriptor-query-refusal· draft PR #7499 · commit68968fbDisposition
Implemented as dispatched. Four call sites routed through the shared
parseStringParam; the runs branch untouched.What landed
packages/runtime/src/domains/automation.ts— relocated by content, as instructed. The card's re-measured line numbers held:query?.paradigm:236,query?.source:239,query?.category:242,query?.type:267, all four intact and unchanged by #7490. Each now reads a parsed value; a non-string is refused in the house shape (400VALIDATION_FAILED+details.fields[]with ADR-0114invalid_type). No new vocabulary, no new helper —parseStringParamas specified.parseEnumParam(new from #7490) was read and deliberately not used; see the capability answer below.Preserved verbatim: the falsy gate each filter always had. Absent and empty spellings still mean "no filter"; every string — including one naming no live paradigm/source/category/type — still filters to a legitimate empty list exactly as today.
One judgement call beyond the letter of the dispatch, flagged rather than assumed: the parse runs ahead of each route's service-capability probe, so the same request gets the same answer whichever automation service a deployment mounts. Inside the probe, a 400 would appear only where
getActionDescriptorshappens to be implemented — a caller still could not tell a rejected filter from an empty registry, which is the exact confusion this card is about. A well-formed filter against an unimplemented method still returns the declared empty-but-valid registry, unchanged. Pinned both ways.The open question, answered on measurement
Refuse. Do not accept a repeated
?paradigm=as an OR-filter. Not a default-by-omission — a decision, and the evidence points away from the capability rather than merely being silent about it:client.automation.listActions—packages/client/src/index.ts:3043URLSearchParams.setper key — structurally cannot emit a repeated parameterclient.automation.listConnectors—:3058?type=${encodeURIComponent(...)}packages/app-shell/src/views/metadata-admin/previews/useFlowNodePalette.ts:93/automation/actionswith no query string at all; narrows byparadigms.includes('flow')client-side (:61)inspectors/FlowReferenceField.tsx:378,415/automation/connectors, unfilteredplugin-rest-api.zod.ts:1339requestSchema, no multi-value languageobjectstackrepo?paradigm=anywhere: a single-string assertion inpackages/client/src/meta-automation-descriptors.test.ts:70(objectui was cloned and grepped for this — it is the real consumer the card's "designer palette" names, and it is not in this container by default.)
The sharpest reading: the one place a multi-paradigm filter would be natural — the palette — does not use the server-side filter at all. It pulls the whole registry and filters in the browser. So OR-widening would be inventing a wire capability nothing asks for, on the route where a wrong empty answer is hardest to notice. Widening a refusal later is a compatible change; refusing first is the safe floor. No follow-up capability card filed — a card saying "consider OR-filtering" with no caller behind it is backlog weight, and this comment is the record if one ever appears.
Tests
New file
packages/runtime/src/domains/automation-descriptor-query-validation.test.ts, 37 cases, sitting beside the runs branch's own file. Every refusal asserts the envelope (code and status), never a baretoThrow()— unfixed, these inputs do not throw at all, they answer 200 with an empty list, so a throw-only assertion would pin the absence of a throw rather than the defect.Mutation table — mutate, see red, revert
paradigmback to the raw value?paradigm=refusals + the capped-message pinsourceback to raw?source=refusals, the literal?source=builtin&source=pluginpin,/actionsdeployment-independencecategoryback to raw?category=refusals + "refuses before the registry is read at all"typeback to raw?type=refusals +/connectorsdeployment-independence/actionsdeployment-independence pin, alone!== undefinedparseStringParam→ closed-setparseEnumParamon?source?source=filter inverted (!==)M7 is the one worth naming: it is the plausible wrong fix (gate the filters to the live set), and it fails exactly the rows that say a caller typing
marketplaceis asking a well-formed question with a genuinely empty answer.Gates
pnpm lint(repo-wide) — cleantsc --noEmit(@objectstack/runtime) — clean@objectstack/runtime— 1976/1976 pass, 122 files@objectstack/clientdescriptor tests — 6/6 pass.changeset/automation-descriptor-query-refusal.md(@objectstack/runtimepatch). Nocontent/docs/releases/**edits. Worktree-isolated, nogit stash.For the PM
Nothing blocked, nothing deferred. Ending the turn here — not waiting on CI, not arming auto-merge.
Generated by Claude Code
os-help commentedon Aug 11, 2026
ACCEPT — PR #7499, head
68968fb0d.Reviewed against the dispatch, self-proved on the diff and the report.
packages/runtime/src/domains/automation.ts(descriptor routes only) + a new 37-case test file + changeset. The runs branch is untouched — verified by diff, not by assertion, so finding:ListRunsRequestSchema.statusis declared on the wire but no handler and no service option carries it —GET /automation/:name/runs?status=failedsilently lists every run #7359's?status=filter that landed an hour earlier in this same file is undisturbed.paradigm/source/category/type) go through the sharedparseStringParam; no hand-rolled comparison, no new vocabulary — the refusal is the housevalidationFailure→ 400VALIDATION_FAILED(ADR-0112) withdetails.fields[]carrying ADR-0114'sinvalid_type.The card's open question was answered by measurement, which is the part I most wanted and the part easiest to fudge. Should a repeated
?paradigm=be read as a genuine OR-filter? Measured across both sides of the wire: the typed SDK builds both filter sets withURLSearchParams.set/ a single?type=so it cannot emit one; and in objectui — the actual consumer — the designer palette fetches/automation/actionswith no query string and narrows byparadigms.includes('flow')client-side (useFlowNodePalette.ts), while the connector pickers fetch unfiltered (FlowReferenceField.tsx). No caller anywhere sends a repeated descriptor filter. Refusing is therefore the safe floor, widening later is a compatible change, and accepting would have invented a wire capability nothing asks for on the exact route where a wrong empty answer is hardest to notice. Reasoned to a decision rather than left as status quo — which is what the dispatch required.A second judgement call I did not specify and agree with: the parse runs ahead of each route's service-capability probe. A malformed query is malformed whichever automation service a deployment mounts, and a 400 that appeared only where
getActionDescriptorshappens to be implemented would be a contract varying by deployment — leaving the caller unable to distinguish a rejected filter from an empty registry, which is precisely this card's confusion. A well-formed filter against an unimplemented method still answers the declared empty-but-valid registry.Assertions pin the envelope, not a throw — deliberately, because unfixed these inputs do not throw at all, they answer 200 with an empty list, so a throw-only assertion would have pinned the absence of a throw rather than the defect. Six mutations, each proved red then reverted, including the two that check the reasoning rather than the code: the parse moved back inside the capability branch (catches only the deployment-independence pin) and
parseStringParamswapped for a closed-setparseEnumParam(catches the two "a string naming nothing live is still a 200 empty list" rows). The#5519anonymous-baseline ordering is pinned too.success, TypeScript Type Checksuccess, Check Changesetsuccess, all Test Core and Dogfood shardssuccess— 25 checks, zero failures.Ready flipped, auto-merge armed; queue membership being verified by branch before any flip (per the conditional prescription — the empty-field receipt is not evidence of a failed arming).
Generated by Claude Code
fix(spec,runtime,service-automation,client): `GET /automation/:name/r…
fix(runtime): refuse a non-string descriptor filter instead of servin…