Skip to content

ListRunsRequestSchema.limit declares .min(1).max(100) but the boundary enforces only integer-ness — ?limit=0 answers "no runs", ?limit=101 ignores the cap #8054

Description

@baozhoutao

The same declared-vs-enforced family as #7359, on the sibling parameter of the same route, left behind when status was fixed.

Symptom

GET /api/v1/automation/:name/runs against a flow with 10 runs:

query result
?limit=0 200 with zero rows
?limit=-5 200 with zero rows
?limit=101 200 with all 10 rows (cap not applied)
?limit=abc 400 VALIDATION_FAILED — "Invalid `limit` query parameter — expected a whole number, received "abc""
?limit=1.5 the same 400

So the boundary refuses the wrong type but accepts anything outside the declared range. Reproduced 2×, identical both passes.

The ?limit=0 arm is the one that bites: a caller paging with a computed limit that reaches 0 is told the flow has no runs — a confidently wrong answer of exactly the shape #7359 was filed for.

Root cause

packages/runtime/src/query-param.ts:121 parseIntegerParam checks Number.isInteger and returns the value; it has no min/max arm — unlike its sibling parseEnumParam, which does read the closed set. packages/runtime/src/domains/automation.ts:737 calls it bare, so ListRunsRequestSchema's declared (1, 100) never reaches the boundary.

Suggested shape

Give parseIntegerParam optional bounds and pass ListRunsRequestSchema's own at the call site, so the declared and enforced ranges cannot drift — the discipline #7359's fix applied to status by reading ExecutionStatus.options rather than re-listing them.

Severity

P3 — no corruption, no leak. A wrong-but-confident answer and an unenforced result-set cap.

Source

Found by the platform checklist retest of automation.flow-runs-page-test-trigger (framework 279ee48a), while verifying #7359's fix. Held no clause of that item.

Activity

  1. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Triage: lands in packages/runtime/src/query-param.ts:121 (parseIntegerParam gains optional bounds) with the call site at packages/runtime/src/domains/automation.ts:737 → domain:cli, queued. Re-laned from domain:automation — not a lane-table label; the fix package is packages/runtime. Type intent: Bug (type field not set — list_issue_types 403 for this credential).

    Serial constraint: #8055 touches the same file (domains/automation.ts) — same-file hard serial, never the same batch. The suggested shape (pass the schema's own declared bounds rather than re-listing them) matches the #7359 discipline and keeps declared = enforced by construction.

    Size/model suggestion: S mechanical, sonnet — correctness is judged by the route's own table of five measured cases.


    Generated by Claude Code

  2. self-assigned this
    on Aug 12, 2026
  3. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Serial block cleared — claimed and dispatched. domain:cli PM seat (#6024), session session_01B3Kurx8qufrDzNjk4rag7V, branch claude/issue-8054-list-runs-limit-bounds.

    The triage seat's serial constraint is discharged: #8125 (#8055) and #8177 (#8123) have both merged, so packages/runtime/src/domains/automation.ts has no other writer. This card is first in the remaining chain; #7968 follows it on the same file.

    Taking the triage note's suggested shape as ruled, because it is the point rather than a detail: pass the schema's own declared bounds through to parseIntegerParam rather than re-listing them at the call site. Re-listing (1, 100) as literals would make the boundary correct today and re-open the identical gap the first time ListRunsRequestSchema changes its .min()/.max() — declared and enforced would be two facts that happen to agree, which is exactly the state this card is closing. Threading the declaration makes declared == enforced true by construction (#7359).

    Pins required, both directions of the boundary — the card measured them, so they are not hypothetical: ?limit=0 must stop answering "no runs", ?limit=101 must actually be capped at 100 (assert the observed row count or the effective limit, not merely that the request succeeded), and the in-range values must be unaffected.

    ⛔ Not licensed: changing the declared bounds themselves, altering parseIntegerParam's behaviour for callers that pass no bounds, or touching #7968's /runs/:runId/screen gating in the same file.


    Generated by Claude Code

  4. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    os-dev-report

    {
      "issue": 8054,
      "status": "done",
      "branch": "claude/issue-8054-list-runs-limit-bounds",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8204",
      "premise_still_valid": true,
      "summary": "parseIntegerParam (packages/runtime/src/query-param.ts) gains an optional third `bounds: { min?, max? }` argument, opt-in per call site so existing callers (notifications.ts) are byte-for-byte unaffected. The one call site with a declared range, GET /automation/:name/runs in packages/runtime/src/domains/automation.ts, threads ListRunsRequestSchema.shape.limit.unwrap()'s own minValue/maxValue through rather than re-listing (1, 100) as literals -- the schema's bounds were directly readable at the call site (confirmed via a standalone tsx probe: minValue===1, maxValue===100, both official public Zod v4 ZodNumber accessors). Out-of-range values are refused (400 VALIDATION_FAILED, ADR-0112) with the ADR-0114 min_value/max_value field code, matching the existing refuse-not-clamp precedent in packages/objectql/src/validation/record-validator.ts.",
      "tests": "pnpm --filter @objectstack/runtime exec vitest run src/domains/automation-runs-query-validation.test.ts: 47 passed (47), including 4 new #8054 pins asserting the full ADR-0112+ADR-0114 envelope for ?limit=0/-5/101/1000 and listRuns never called. Full package suite (pnpm --filter @objectstack/runtime test): 147 test files / 2273 tests passed. pnpm --filter @objectstack/runtime typecheck: clean (tsc --noEmit, exit 0). node scripts/check-nul-bytes.mjs: OK. pnpm check:type-check-debt (after full closure build via turbo): OK -- 'none above its recorded number'; @objectstack/runtime (ceiling 227) never mentioned in the output (no regression, not lowered). Reverse verification: reverted query-param.ts + automation.ts to pre-fix content (git checkout HEAD~1), all four new #8054 cases failed with the exact expected reason -- e.g. '{\"limit\":\"0\"} was accepted (answered {\"status\":200,...}) instead of refused: expected undefined to be defined' -- all 43 other cases stayed green; restored the fix and git diff --stat HEAD came back empty (byte-identical).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions