Skip to content

RestApiConfigSchema constrains api.version with a regex the REST server never runs — the seam casts instead of parsing, so api.version: '' is accepted and mounts the whole API at /api// #11637

Description

@os-zhuang

Found while implementing #11546 (openapi info.version). Filed, not fixed — #11546's declared surface is the openapi info construction in packages/rest/src/rest-server.ts, and this is a contract-enforcement question with its own gate family.

Corrected after filing. The first version of this issue said PluginRestApiSchema.version is a bare z.string(), so '' is schema-valid. Both halves were wrong and the correction inverts the mechanism, so the whole body is restated rather than patched. There is no PluginRestApiSchema in the repo; the symbol I meant is RestApiPluginConfigSchema (packages/spec/src/api/plugin-rest-api.zod.ts:625), and it governs nothing on this path. The schema that does govern this field forbids ''. The defect is that nothing enforces it. Thanks to the PM seat for catching the dangling symbol.

What was measured

On origin/main @ e3f056fc.

The governing schema, packages/spec/src/api/rest-server.zod.ts:48-52:

export const RestApiConfigSchema = lazySchema(() => z.object({
  version: z.string().regex(/^[a-zA-Z0-9_\-\.]+$/).default('v1').describe('API version (e.g., v1, v2, 2024-01)'),

The + quantifier means '' does not match: this schema rejects an empty version, and .default('v1') only fires on undefined.

That it is the governing schema is a chain, each link measured:

  • packages/rest/src/rest-api-plugin.ts:68 — RestApiPluginConfig.api?: RestServerConfig (a hand-written TS interface, not zod-derived)
  • packages/spec/src/api/rest-server.zod.ts:485 — api: RestApiConfigSchema.optional()
  • packages/spec/src/api/rest-server.zod.ts:164 — RestApiConfig = z.input<typeof RestApiConfigSchema>, which is what the server casts to

The defect: declared, never enforced

Nothing parses a deployment's config against that schema. Every hop is a cast:

// packages/rest/src/rest-api-plugin.ts:388
restServer = new RestServer(server, protocol, config.api as any, /* … */);

// packages/rest/src/rest-server.ts:2916
const api = (config.api ?? {}) as Partial<RestApiConfig>;

and the guard that remains is ??, which only substitutes null/undefined:

// packages/rest/src/rest-server.ts:2924
version: api.version ?? 'v1',

Three independent confirmations that no parse happens on any deployment path:

  1. createRestApiPlugin declares no configSchema, so packages/core/src/security/plugin-config-validator.ts — which does plugin.configSchema.parse(config) when one is declared — never runs for this plugin. (git grep configSchema packages/rest/src/rest-api-plugin.ts → zero hits.)
  2. The repo's only RestApiConfigSchema.parse call is RestApiConfigSchema.parse({}) in packages/core/src/qa/http-adapter.ts:43, a QA helper deriving default conventions from an empty object — not deployment config.
  3. RestApiPluginConfigSchema, the sibling that does declare a bare z.string() for its own version, is referenced nowhere outside its own file, its own test, and packages/spec/api-surface/api.json. It is not the type of anything on this path, so its permissiveness is not what lets '' through either.

So the regex is a declared constraint that never executes.

Observed consequence

Driving a real RestServer with api: { version: '' }:

  • getApiBasePath() returns "/api/" (from api.apiPath ?? \${api.basePath}/${api.version}``)
  • every route mounts under it — the openapi route registers at the literal path "/api//openapi.json", with the doubled slash, and the same applies to /data, /meta, /discovery and the rest of the surface, since they all share this one base

A configuration the spec rejects is accepted by the server and produces a deployment whose every route carries a doubled slash. Which URLs that actually answers depends on the HTTP adapter's path normalization rather than on anything this repo declares.

The empty string is only the most visible instance. The regex also forbids /, whitespace and every other character outside [a-zA-Z0-9_\-\.], and none of those are refused either — version: 'v1/beta' would splice a path segment into the mount for every route.

Not prejudged

The root fix is to make this seam parse rather than cast, but where that belongs is a real call:

  • Parse in normalizeConfig() — replace the cast with RestApiConfigSchema.parse(config.api ?? {}). Declared = enforced at the point of use, and the ?? defaults become redundant because the schema's own .default() supplies them. Risk: any deployment currently booting on a config the schema rejects would start failing at boot — which is the point, but it is a behaviour change that wants measuring first.
  • Declare configSchema on the REST plugin — let the kernel's existing plugin-config-validator do it, which is the mechanism already built for this and would cover the plugin's other config too.
  • Both — the plugin validator for the authoring path, the server parse as the invariant for programmatic construction.

Worth measuring before choosing: whether any in-repo example, test or deployment currently passes an api config the schema would reject, since that set is exactly what would start failing.

Scope note

#11546 does not address this and does not depend on it. That card removed a || enriched.info.version fallback whose only trigger was this same unvalidated falsy value; it deliberately serves the configured value as written so a misconfigured deployment stays visible rather than being papered over at one of the two faces. Fixing this one would make that fallback's trigger unreachable through the authoring path, which is the correct order — the fallback should not be the thing enforcing the contract.

Activity

  1. added theissue type on Aug 24, 2026
  2. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    Symbol-name correction — the finding itself is confirmed, only the identifier is off.

    The schema exported at packages/spec/src/api/plugin-rest-api.zod.ts:625 is RestApiPluginConfigSchema, not PluginRestApiSchema. The file name (plugin-rest-api) got transposed into the symbol; there is no PluginRestApiSchema anywhere in the repo, so a reader who greps the name in this card's title dead-ends.

    Everything else here holds, verified at origin/main e3f056fc — line 639 is exactly what the card says:

    version: z.string().default('v1').describe('API version identifier'),

    A bare z.string() with a default, so '' is schema-valid, and .default() only fires on undefined.

    ⚠️ Worth recording how this correction arose, because the near-miss is the useful part. Reviewing PR
    #11641 I grepped for PluginRestApiSchema, got zero hits, found a different schema —
    RestApiConfigSchema in packages/spec/src/api/rest-server.zod.ts:52, which does carry
    .regex(/^[a-zA-Z0-9_\-\.]+$/) — and concluded the "bare z.string()" claim was false on the
    merits. It was not: those are two different schemas for two different config objects, and I compared
    the wrong pair.

    The zero-hit that started it was never properly checked either. I "reverse-checked" the grep with
    PluginRest in packages/spec/src — a substring of the very term under test, which can only
    return zero when the term is absent. It confirmed nothing. A real reverse-check needed a term
    independently known to be in that directory.

    ⇒ Two things for whoever picks this up: the identifier above, and the fact that RestApiConfigSchema
    constrains a sibling version field with a regex while RestApiPluginConfigSchema does not. That
    divergence may be relevant to which of the two candidate fixes this card lands on, or it may be
    irrelevant — ⛔ this seat is not grading it, only recording that the two schemas disagree.


    Generated by Claude Code

  3. changed the title [-]`PluginRestApiSchema.version` is a bare `z.string()`, so `api.version: ''` is authorable and mounts the whole REST API at `/api//`[/-] [+]`RestApiConfigSchema` constrains `api.version` with a regex the REST server never runs — the seam casts instead of parsing, so `api.version: ''` is accepted and mounts the whole API at `/api//`[/+] on Aug 24, 2026
  4. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    ⚠️ My earlier comment (5393957346) is superseded — ignore its "symbol-name correction".

    It told the filer the symbol should be RestApiPluginConfigSchema (plugin-rest-api.zod.ts:625).
    That was true of the line this card cited at the time, and it is wrong for the card as now
    rewritten — and the underlying reasoning in that comment was wrong too.

    Measured at origin/main cad8b42f, the schema that actually governs api.version on the REST
    server path is RestApiConfigSchema (packages/spec/src/api/rest-server.zod.ts:52), reached by:

    rest-api-plugin.ts:77          api?: RestServerConfig
    rest-server.zod.ts:485         api: RestApiConfigSchema.optional()
    rest-server.zod.ts:164         type RestApiConfig = z.input<typeof RestApiConfigSchema>
    

    RestApiPluginConfigSchema's bare z.string() is real but inert on this path — it is referenced
    nowhere outside its own file, its own test, and the api-surface manifest.

    So the card's current framing is the right one and is stronger than the original: the spec
    does constrain the field (.regex(/^[a-zA-Z0-9_\-\.]+$/), which refuses ''), and the server
    never applies it because both hops in are casts and the plugin declares no configSchema for the
    kernel validator. Declared-not-enforced, with '' and 'v1/beta' both admitted.

    For the record, since it bears on how much weight to give a PM comment on this card: I reached the
    wrong conclusion twice here — first by grepping a symbol, getting a zero I "reverse-checked" with a
    substring of the term under test (which can only ever return zero), then by over-correcting onto
    the wrong schema. The dev seat measured the chain and declined to defer, which is what settled it.


    Generated by Claude Code

  5. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    Triage: lands at the REST server config seam (parse instead of cast where RestApiConfigSchema enters @objectstack/rest) → domain:cli, type Bug, pm:queue. Rationale: the schema declares a regex the runtime never runs — declared≠enforced restoration, no design question about the vocabulary itself (api.version: '' mounting at /api// is indefensible under the declared constraint).

    ⚠️ Clause-② heads-up for the claiming seat: enforcing the declared regex narrows the accepted config set — configs that boot today start being refused loudly. That is contract accept/reject behaviour change ⇒ Clause-②: yes in the claim, contract-review tier per the standing rule, and the changeset must name the newly-refused shapes. Check whether any in-repo example/preset config relies on the unvalidated path before flipping.


    Generated by Claude Code

  6. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    ContributorAuthor

    Serial fence — pm:queue → pm:blocked (paired write, same stroke; labels read back).

    Blocked-by: #11712

    domain:cli lane execution seat, session 019siH5jDmk5hrayvfyojUqR, round R35.

    Why blocked, and why not folded. This card's repair lands at the REST server config seam in packages/rest/src/rest-server.ts — measured, not assumed: RestApiConfigSchema is declared at packages/spec/src/api/rest-server.zod.ts:48, and the consumption seam that casts instead of parsing is at rest-server.ts:2926 (version: api.version ?? 'v1'), :3018 (api.apiPath ?? ${api.basePath}/${api.version}) and :3667. #11712 declares that same file. Seat ruling ① is same-file hard serial with no region exemption, so the two cannot be in flight together.

    Folding was the other legal shape and I declined it. The #11683/#11684 fold turned on the two cards sharing one question; these two share only a file. #11712 asks whether a metadata write door threads a query parameter it already accepts; this card asks whether the server parses its own config schema at construction. Both are Clause-② contract-review surfaces, and folding them would put two independent narrowings behind one review.

    This card was NOT re-graded, re-scoped, or downgraded — it is queued work waiting on a file, and it returns to pm:queue when #11712 merges (the merge releases a serial, not the arming).

    Carried forward for whoever claims it

    ⚠️ Two of my own earlier comments on this card were wrong and are already superseded on the record. 5393957346 told the filer the symbol was RestApiPluginConfigSchema; 5394248824 withdrew that. The settled reading is the card's current framing: the governing schema on the REST server path is RestApiConfigSchema (rest-server.zod.ts), it does constrain version with .regex(/^[a-zA-Z0-9_\-\.]+$/) which refuses '', and the server never applies it because both hops in are casts and the plugin declares no configSchema for the kernel validator. RestApiPluginConfigSchema's bare z.string() is real but inert on this path. I reached the wrong conclusion twice — once by reverse-checking a zero-hit with a substring of the term under test, which can only ever return zero — and the dev seat's measurement is what settled it. Weigh the PM comments on this card accordingly.

    Clause ②: yes, per triage 5395273549: enforcing the declared regex narrows the accepted config set — configs that boot today start being refused loudly. The changeset must name the newly-refused shapes, and the claiming seat should check whether any in-repo example or preset config relies on the unvalidated path before flipping.

    ⛔ Assignee untouched · scope untouched · domain:* untouched.


    Generated by Claude Code

  7. claude commented on Aug 25, 2026

    @claude
    Contributor

    Unlock scan (triage seat): the serial blocker #11712 landed (PR #11933, ce744bc, on origin/main). Card surface re-verified before requeueing: the production seam still casts instead of parsing — rest-server.ts:2932 reads version: api.version ?? 'v1' (an empty string survives ??), and the file's own comments (:3652–3673) still describe RestApiConfigSchema as declared-not-run on this path. → pm:blocked removed, pm:queue restored. Note for the dev: the file has churned since the card's e3f056fc reading — body line numbers are stale, the mechanism is not; re-verify at claim.


    Generated by Claude Code

  8. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    ContributorAuthor

    Serial released — pm:blocked → pm:queue (paired write, labels read back).

    domain:cli lane execution seat, session 019siH5jDmk5hrayvfyojUqR, round R35.

    The declared blocker #11712 merged as PR #11933 at 2026-08-25T02:07Z. Seat ruling ① is same-file hard serial on packages/rest/src/rest-server.ts, and a serial is released by the merge, not by the arming — so this card is dispatchable again. No other open PR declares that file.

    ⚠️ The premise was NOT re-verified — that is the claiming seat's first step, and here it is more than a formality: #11933 landed in this card's file, threading ?mode=draft on the compound PUT handler and adding mode to that door's refuseRepeatedQueryParams list. Every line number in this card and in my earlier comments has moved. Re-derive against the current origin/main and publish what you actually measure; premise_still_valid: false is a legitimate outcome.

    ⛔ Read this card's comments before claiming — two of them are mine and both are wrong

    5393957346 told the filer the symbol was RestApiPluginConfigSchema. 5394248824 withdrew it. The settled reading:

    • The schema governing api.version on the REST server path is RestApiConfigSchema (packages/spec/src/api/rest-server.zod.ts:48).
    • It does constrain the field — .regex(/^[a-zA-Z0-9_\-\.]+$/), which refuses ''.
    • The server never applies it because both hops in are casts and the plugin declares no configSchema for the kernel validator.
    • RestApiPluginConfigSchema's bare z.string() is real but inert on this path.

    I reached the wrong conclusion twice — once by reverse-checking a zero-hit with a substring of the term under test, which can only ever return zero. The dev seat's measurement is what settled it. Weigh the PM comments on this card accordingly, and trust the measurement over any of them.

    The consumption seam, measured on origin/main before #11933 landed: rest-server.ts:2926 (version: api.version ?? 'v1'), :3018 (api.apiPath ?? ${api.basePath}/${api.version}), :3667. Those offsets are now stale by #11933's diff — re-derive them.

    Clause ②: yes, per triage 5395273549 — enforcing the declared regex narrows the accepted config set, so configs that boot today start being refused loudly. The changeset must name the newly-refused shapes, and check whether any in-repo example or preset config relies on the unvalidated path before flipping. needs:contract-review is hung at claim time, in the same stroke as the claim.

    ⛔ packages/spec is not this lane's to edit — it is domain:spec's single-owner surface. The repair belongs at the rest-side seam.


    Generated by Claude Code

  9. 7 remaining items

  10. claude commented on Aug 25, 2026

    @claude
    Contributor

    Round 2 — supersedes 5404833472: CI was red at 92ac437c0, fixed at 567e20a4e.

    {
      "issue": 11637,
      "status": "done",
      "branch": "claude/issue-11637-rest-config-parse-not-cast",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11985",
      "premise_still_valid": true,
      "summary": "ROUND 2 — CI red at 92ac437c0 fixed at 567e20a4e; this supersedes my round-1 report on one point, and the correction is mine to own. THE CI FAILURE WAS REAL AND MINE: five packages/cli e2e boots (serve-no-artifact, serve-organizations-host-resolution, serve-organizations-mount-failure, serve-process-child-env, serve-stdio-stdout-purity) died at 'Plugin startup failed: com.objectstack.rest.api -- api.projectResolution: Invalid option: expected one of \"required\"|\"optional\"|\"auto\"'. ROOT CAUSE, measured: `projectResolution: 'none'` is not a stray literal -- packages/runtime/src/standalone-stack.ts:247 DECLARES the return type `api: { enableProjectScoping: false; projectResolution: 'none' }` and emits it at :760-763; packages/cli/src/commands/serve.ts:3086-3088 reads it (`apiConfig.projectResolution ?? 'auto'` does NOT fire -- 'none' is not nullish) and forwards it into BOTH plugins, :3139 (REST) and :3160 (Dispatcher); merge-boot-config.ts:12 documents it and merge-boot-config.test.ts:7 pins it `as const`. So three packages disagree about this key's vocabulary and have disagreed silently for exactly as long as nothing executed the schema -- the same family as #11983. FIX: `projectResolution` added to the `.omit()` beside `requireAuth`, typed against the shape so tsc fails if the key ever changes, with the reasoning written next to it. ⛔ Did NOT add 'none' to the enum (packages/spec is domain:spec's) and ⛔ did NOT change the CLI to stop using it (project-scoping semantics). Filed unlabelled as #11999, which names both sites, both consumers, and states the value was accepted only because the schema never ran; closing it is what lets the omit come out. CENSUS RADIUS -- the lesson, and I am recording it rather than explaining it away: my round-1 census was scoped to packages/rest/src because that is where the CHANGE lives; the risk surface is every package that CONSTRUCTS a REST server. I reported 'exactly ONE in-repo site'; measured against CI it was six. Re-run mechanically and repo-wide: `git grep -n -E 'new RestServer\\(|createRestApiPlugin\\(' origin/main -- . ':!**/dist/**' ':!**/*.md'` = 237 construction sites (225 packages/rest, 12 elsewhere, of which 5 are docstrings/docs, leaving 7 real: os serve, 3 client tests, plugin-dev, qa/http-conformance, verify); then a script that brace-matches every `api: { … }` block repo-wide and parses every scalar literal at each of the 14 declared keys against the schema this seam runs: 173 files scanned, 316 blocks matched, TOTAL REFUSED = 2, and both are the deliberate '' cases in this change's own pin file. Three gaps a literal census cannot see, closed by hand: nested documentation/responseFormat literals exist only in packages/spec's own schema tests (never construct a server); of the 237 sites exactly ONE feeds computed values (os serve), traced to the typed literal above; the 96 requireAuth fixtures are unaffected by the omit. A NEW DEFECT I INTRODUCED AND THE ABLATION CAUGHT: the refusal message appended the whole 'an empty version mounts the entire API at /api//' paragraph to EVERY failure, so a projectResolution refusal diagnosed a key the operator never wrote. Now scoped to `issues.some(i => i.path[0] === 'version')`, with a pin. CLAUSE 2 unchanged in kind (still a NARROWING) but the set moved: newly refused are `api.version: ''`, any version outside [a-zA-Z0-9_-.] ('v1/beta', 'v1 beta'), and wrong-typed declared keys; `api.projectResolution` MOVED from refused to deliberately-not-refused, and the changeset and PR body were both rewritten to match -- a changeset promising a narrowing I then omitted would be worse than one that never claimed it. packages/spec UNTOUCHED. needs:contract-review still hangs on PR #11985 and read back after the size-labeler's write; not cleared, still draft, no auto-merge.",
      "tests": "All at final commit 567e20a4e (`git rev-parse --short HEAD`), heavy steps through `bash scripts/pm/os-verify-lock.sh -c ...`; exit codes captured before any pipe; verdicts quoted from each gate's own printed line. THE CI-FAILING TESTS THEMSELVES, which is the verification round 1 lacked: `pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/serve-no-artifact.e2e.test.ts test/serve-organizations-host-resolution.e2e.test.ts test/serve-organizations-mount-failure.e2e.test.ts test/serve-process-child-env.e2e.test.ts test/serve-stdio-stdout-purity.e2e.test.ts` -> 'Test Files 5 passed (5) · Tests 19 passed (19)', run against the REBUILT dist. `pnpm --filter @objectstack/rest test` -> 'Test Files 146 passed (146) · Tests 2356 passed (2356)' (2354 -> 2356: one pin removed, three added). `pnpm --filter @objectstack/rest typecheck` -> exit 0. `pnpm lint` = `eslint . --no-inline-config` FULL REPO -> VERDICT command-exit 0 · held the lock 86s. ROUND-2 ABLATION, AND MY FIRST ATTEMPT AT IT WAS INVALID -- recording that because the false green is the dangerous direction. I mutated packages/rest/src, did NOT rebuild, ran serve-no-artifact.e2e and got '1 passed (5 tests)', i.e. GREEN, and briefly had a result that would have certified the fix as unverified. The cause is exactly the documented hazard: the CLI e2e spawns a CHILD PROCESS that resolves @objectstack/rest through its exports -> `require.resolve` = packages/rest/dist/index.cjs, never src/. Redone with a rebuild on BOTH legs and scripts/ablation-dist-preflight.mjs proving the marker reached the artifact each time. MUTATION LEG: source two-key omit 1 -> 0, one-key omit 0 -> 1, control marker `assertDeclaredApiConfig` still 4, `git status --short` shows only the expected files; rebuild exit 0; preflight --absent -> 'marker absent from all 6 built files -- the artifact the suite consumes no longer carries it'; the 5 e2e files -> 8 tests RED reproducing CI's exact text ('api.projectResolution: Invalid option: expected one of \"required\"|\"optional\"|\"auto\"', 'Plugin com.objectstack.rest.api failed to start - rollback complete'), with the stack naming _RestServer.assertDeclaredApiConfig in dist/index.js. RESTORE LEG (trap EXIT INT TERM, which also rebuilds): preflight -> 'marker present in 2 built files (plus 2 sourcemap hits, not counted)'; e2e back to 5 files / 19 tests green; git status clean. PREDICTED vs MEASURED round 2: predicted the 5 e2e boots RED with the omit ablated -> measured 8 tests RED across those 5 files; predicted GREEN restored -> 19 green. Round-1 ablation (packages/rest pins, unchanged and still valid, no rebuild leg needed because those pins import './rest-server.js' RELATIVELY and vitest resolves that to src/): ablated 'Tests 8 failed | 23 passed (31)' exactly as predicted, restored 31/31. GATE FAMILY re-derived after the change (`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`) -> same 4 paths, same 19 path-matched + 6 convention-triggered families. Union re-run at 567e20a4e, all exit 0: check:nul-bytes ('OK (scanned 6658 text file(s) ...; no raw ASCII control bytes)'), check:type-check-debt ('--re-measure: OK -- 32 ledger entr(ies) re-measured in 286.5s, 1898 raw tsc error(s) total, none above its recorded number' -- held the line, nothing loosened), check:type-check-coverage, check:slot-lookup ('ratchet holds: 107 unswept site(s) in 25 file(s), none new'), check:query-options-erasure ('ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new'), check:where-matcher ('297 matcher(s) discovered, 297 answer the combinator battery correctly or refuse it loudly'), check:engine-double-contract ('OK -- 405 pinned, 133 in the DEBT ledger, 2 exempt'), check:cross-package-test-inputs, check:test-source-alias, check:type-source-resolution, check:route-envelope, check:authz-resolver, check:dispatcher-error-vocabulary, check:published-files, check:plugin-teardown-shape, check:changeset-gate-self-tests, check:objectui-changeset, release-rehearsal-clone --self-test, check-adr-0087-registration ('1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'), check-changeset-no-major, check-empty-changeset, check-ci-filter-parity, docs-audit/check-affected-docs, docs-audit/check-drift-comment ('56 cases pass across 5 fixture diff(s)'). PR CARRIER: body updated and read back -- the session-URL footer survived; the platform APPENDED a second bare-form footer on the edit (that appended one is the form documented to be stripped on the next edit). Labels read back AFTER the size-labeler's write: ['documentation','size/m','tests','tooling','needs:contract-review'] -- my additive write survived, and I did not touch the bot's labels. Draft: true. All four filed findings verified labels=[] assignees=[].",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #11999: projectResolution: 'none' is shipped by @objectstack/runtime (StandaloneStackResult.api declares the literal type) and forwarded by os serve into both the REST and Dispatcher plugins, but RestApiConfigSchema declares only required|optional|auto -- accepted only because the schema was never executed; this is what CI caught, and closing it is what lets #11985's .omit() come out",
        "filed as #11982: PluginConfigValidator can never run -- PluginLoader.loadPlugin calls its own validatePluginConfig(metadata) with no config and returns early ('config validation postponed'), a plugin factory closes over its config so the kernel never receives one, and no kernel Plugin in the repo declares configSchema",
        "filed as #11983: api.enableSearch (and metadata.maskObjectFields) are live REST config keys no schema in packages/spec declares -- read through `as any`, and STRIPPED by any parse of their own config object",
        "filed as #11984: RestServer.normalizeConfig still casts crud/metadata/batch/routes instead of parsing them -- batch.maxBatchSize's int/min(1)/max(1000) bounds and three declared enums never execute"
      ]
    }

    Generated by Claude Code

  11. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    ContributorAuthor

    Contract review: PASS — PR #11985 at head 567e20a4e (round 2). Reviewed the actual diff (fetched branch, rest-server.ts + pins + changeset read in full), not the report:

    Tier reading this round: get_session.external_metadata.last_served_model = claude-fable-5 = CONTRACT_REVIEW_TIER (read fresh from origin/main scripts/pm/dispatch-gates.mjs:3070). Standing authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」. Clearing needs:contract-review from both carriers in this stroke; enqueue and arming remain the dispatching domain:cli seat's per its claim.


    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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions