Skip to content

mcp: validate_expression answers ok:true for an empty or blank expression that the metadata save gate refuses as needing a non-blank source #27411

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

QA-source: #27409 · ai.mcp-validate-expression · observed beside the clauses

Found by an off-clause probe (empty-string boundary) in the 18.3.0 release-candidate QA run #27409, at framework c4c515ad, on the stock showcase.

Reproduction

  1. Boot the showcase: objectstack dev --seed-admin -p PORT -d file:/tmp/run/data.db. Sign in as admin and mint an API key with POST /api/v1/keys.
  2. Over MCP HTTP, POST /api/v1/mcp with tools/call validate_expression and arguments {"objectName":"showcase_task","expression":"","site":"formula"}. Repeat with "expression":" ".
  3. As admin, PUT /api/v1/meta/object/qa_probe_formula with a formula field whose expression is "".

Expected vs actual

  • Expected: validate_expression reports an error for a blank expression, the same verdict the build and save gate gives. The tool exists so that an agent can check an expression before saving it.
  • Actual:
    • Step 2 answers {ok: true, errors: [], inferredType: "unknown"} for both inputs.
    • Step 3 answers 422 INVALID_METADATA: "An expression in an evaluated slot needs a non-blank source".
  • So an agent that validates first and then saves is told the expression is fine, and the save then refuses it.

Mechanism

  • packages/mcp/src/mcp-http-tools.ts: the tool's input schema declares expression: z.string() with no blank check (around line 806). The handler passes the value straight to validateExpression(role, expression, hint) (around line 833).
  • validateExpression from @objectstack/formula returns ok for an empty source. Measured directly on the built package: validateExpression('value', '', hint) and validateExpression('value', ' ', hint) both answer {"ok":true,"errors":[],"warnings":[]}, while 'record.' answers invalid-cel.
  • The blank-source refusal lives only in the spec envelope check used at save and build time. The MCP tool never consults it.

Suggested acceptance

  • validate_expression with an empty or whitespace-only expression answers ok:false with an error whose wording matches the save gate's.
  • A non-blank control expression still validates as before.
  • The case is pinned in packages/mcp/src/mcp-validate-expression.test.ts.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 错的必须被响亮拒绝并给处方 | none | P⑤

    Triage: p2 · domain:server · area:ai · 2026-10-29T06:01Z

    ⛔ Not a claim.

    • Read fresh. The MCP tool validate_expression answers ok: true for an empty or blank expression, which the metadata save gate refuses. The tool exists so an agent checks an expression before saving, so its answer disagreeing with the gate is the defect (「MCP 面暴露的就是应用真能做的」). p2.
    • ⛔ The tool calls the same check the save gate runs. It does not get a second blank-string rule.
    • It is extracted from the 18.3.0 RC run, so it is listed in wave anchor [sweep] v18.3.0 release test sweep #27377 §5 by the runner, and it is retested on the next wave.
    • bug, pm:queue.

    Generated by Claude Code

  2. added
    area:aiAI-native — agent / tool / skill metadata, and the MCP surface an agent drives
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Oct 29, 2026
  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    Path: 错的必须被响亮拒绝并给处方 | none | P⑤

    Triage: lands in packages/mcp/src/mcp-http-tools.ts (domain:server); rationale: this concurs with the first touch at 06:01Z (comment 6489262759). Labels stay as they are (bug, priority:p2, pm:queue, area:ai, domain:server). This note adds the parts of the first touch that the duties require and that were missing. Read at origin/main ded67bfb, 2026-10-29T09:55Z.

    Size/model suggestion: S, a one-file fix plus a pin in mcp-validate-expression.test.ts.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 29, 2026
  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    Claim: PM loop round 1
    Session: session_017xRBcScm27Q3R5mkYLPWdB
    Account: sunhaolin
    Branch: claude/issue-27411-mcp-validate-blank
    Worktree: objectstack-issue-27411
    Domain: domain:server
    Seat: domain:devx#1
    File surface: packages/mcp/src/mcp-http-tools.ts (the validate_expression tool only), its tests under packages/mcp/src, one @objectstack/mcp changeset (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default (dispatch-gates --tier at c4c515a: "no path-derived mandate"), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the MCP tool code (packages/mcp) answers ok:true for a blank expression | the save/build gate already refuses blank sources (EVALUATED_EXPRESSION_SOURCE_REQUIRED in packages/spec/src/shared/expression.zod.ts), so the fix reuses that one rule | reached by any MCP client with a key; validate-then-save agents use it today
    Thread-read: 6494043224
    Premise: holds (triage first touch re-verified the mechanism on origin/main ded67bfb; the dev re-measures before editing)
    Serial constraints cleared: none — the 10 open domain:server + pm:dispatched claims were read; none names packages/mcp

    Maintainer direct dispatch (cross-lane claim authority): the maintainer, in session session_017xRBcScm27Q3R5mkYLPWdB on 2026-10-29, verbatim: 「你应该使用 pm-dispatch 技能处理 issue 和 pr」. This devx-seat session claims these domain:server cards on that instruction; the card stays domain:server.
    Clause-② note: this narrows what the tool reports as valid (a blank expression stops reading ok) toward the already-published save-gate rule, so it is no, and it owes one contract-review-tier review before the queue.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB
    os-dev-report

    {
      "issue": 27411,
      "status": "done",
      "branch": "claude/issue-27411-mcp-validate-blank",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27534",
      "session": "session_017xRBcScm27Q3R5mkYLPWdB (mode:subagent, the PM's session; identity = the branch)",
      "premise_still_valid": true,
      "summary": "validate_expression now parses the expression with the spec input schema the save/build gate uses for the slot (EvaluatedExpressionInputSchema for formula/validation/flow_condition, TemplateExpressionInputSchema for template) before running validateExpression; a refusal answers ok:false with the schema's own issue (code invalid_union, message EVALUATED_EXPRESSION_SOURCE_REQUIRED or TYPED_EXPRESSION_SOURCE_REQUIRED.template), no warnings, no inferredType, inScope kept. Non-blank expressions pass both schemas, so their answers are unchanged. Premise re-measured on origin/main ded67bfb with built packages: validateExpression answers ok:true for '' and '   '; both PM mechanism assumptions held (no spec change needed). One PM instruction not followed: the PR was opened WITHOUT assignee sunhaolin, because the relay's pr_create row (scripts/pm/fleet-write/ops.mjs) says a dev's create carries no assignees; the seat decides (see open_questions).",
      "tests": "At head bb5eb6dd: pnpm --filter @objectstack/mcp exec vitest run --maxWorkers=2 src/mcp-validate-expression.test.ts -> Test Files 1 passed (1), Tests 22 passed (22), 14 of them new. pnpm --filter @objectstack/mcp test -> Test Files 45 passed (45), Tests 581 passed (581); pnpm --filter @objectstack/mcp typecheck -> tsc clean + check:test-typecheck OK (6 files / 53 errors / 8 pinned signatures, unchanged); lock VERDICT command-exit 0. Ablation, one run on the committed fix, through scripts/ablation-replace.mjs: anchor 'const slotParse = slot.safeParse(expression);' 1->0, blob 0015f6b5->1609783f -> 13 failed | 9 passed (22) (the 12 blank cases + default-site case red, both controls green), the predicted direction; restored blob == HEAD 0015f6b5, git diff HEAD empty. The test imports the tool from src through a relative path, so no dist rebuild was in the path.",
      "gates": "Local, at bb5eb6dd: node scripts/pm/dispatch-gates.mjs --commands derived 59 families from a 3-path change set vs merge base ded67bfb5; all 59 ran and each exit was captured before any pipe: 56 exited 0 on the first run; 3 exited 3 (PREREQUISITE) and exited 0 on a re-run once the prerequisite existed: check-plugin-teardown-shape --self-test (after fetching its pinned fixture commit 621a4876 on this shallow clone: 48 cases pass), check:lean-entry-closure (after building @objectstack/objectql: admitted set held exactly, 15 packages), check:dual-build-cjs-loads (after check:type-check-debt built every package: 112 require entry points across 69 packages load). dispatch-gates --ran -> exit 0, 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN. check:nul-bytes exit 0. CI on the PR head bb5eb6dd at report time: Check Changeset, Lint jobs, Type Check jobs, Governed Surface Queue Guard, PR-body guards all in_progress; Type Check workspace queued; 18.x upgrade harness, Packed-tarball smoke, Schedule gate skipped. NOT MEASURED: none of the CI families has concluded yet; reason: the report is due when local verification finishes.",
      "dogfood_battery": "n/a, no ! in the PR title and Clause-②: no with no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "3 writes: git push of the branch (not REST; empty-branch probe, then the fix commit); 1 relay stroke pr_create = POST /repos/objectstack-ai/objectstack/dispatches -> relay POST /repos/objectstack-ai/objectstack/pulls (#27534, no assignee or labels legs); 1 relay comment = this os-dev-report on #27411 (POST /repos/objectstack-ai/objectstack/issues/27411/comments). Also one git fetch of commit 621a4876 into the shared object store (a read).",
      "files_changed": [
        "packages/mcp/src/mcp-http-tools.ts",
        "packages/mcp/src/mcp-validate-expression.test.ts",
        ".changeset/27411-mcp-validate-blank-expression.md"
      ],
      "line_budget": "108 changed lines (+100 / -8) over 3 files, under the 3000-line threshold; no skills/** touched",
      "deviations": [
        "The PR has no assignee: the dispatch said assignee sunhaolin, and the relay row comment says a dev's create carries no assignees. I followed the relay row. The seat can add the assignee.",
        "The error code is the spec schema's zod issue code (invalid_union), not a @objectstack/formula kebab code. The formula refusal vocabulary is closed, outside the file surface, and allows one code per message template.",
        "The commit trailer uses the model-free pair AGENTS.md requires, not the harness's model-named Co-Authored-By line."
      ],
      "open_questions": [
        {
          "question": "Should PR #27534 carry assignee sunhaolin, as the dispatch said?",
          "options": [
            "A: leave it without an assignee, as the relay pr_create row comment says for a dev's create",
            "B: the seat adds sunhaolin with one relay assign stroke"
          ],
          "recommendation": "A. The relay row records the maintainer's ruling that a draft open summons nobody. If the seat reads os-dev's 'PR assignee = card's' as the rule, it applies B."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none, noted, not filed. The template site uses the general TemplateExpressionInputSchema sentence. The notify node title and message slots refuse a blank template with their own sentences that name the key, and the tool has no key to pick between them. This is noted in the PR Acceptance notes."
      ]
    }
  7. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    ACCEPT → PR #27534 comment 6495423331 (head bb5eb6dd, reviewed 2026-10-29T11:29Z). Landing after CI on that head is green.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    Handover: PR #27534 → PM session session_017xRBcVy4VXmkQjn2MGeaWk (review and landing)


    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

Labels

area:aiAI-native — agent / tool / skill metadata, and the MCP surface an agent drivesbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions