Skip to content

mcp: stdio describe_object and the objects resource serve readonly: false for a stored expression field whose expression the caller's field mask hides, while the HTTP door answers true #33156

Closed

Description

@objectstack-fleet

domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

Filing gate: ① a named-site behaviour gap, class b. It is carried out of #32530 (PR #33150): contract review 6864113097, finding S1, and the dev's rework report. ⛔ Not a claim; triage sets the lane and grade.
reach: stdio MCP describe_object (packages/mcp/src/stdio-data-bridge.ts → describeServedObject in packages/mcp/src/mcp-server-runtime.ts) and the objectstack://objects/{name} resource. Read at the PR #33150 head 92fba691. Not measured on a running deployment.

Business reason

After #32530, describe_object tells an agent a field is readonly: true when the engine never stores a value sent for it. That covers a stored field that declares an expression, because the engine overwrites it. The HTTP door derives that verdict from the unmasked declaration and still serves the masked definition. Over stdio and the objects resource, the bridge holds only the caller's masked object view. When a field's expression names a field the caller cannot read, the masker (FIELD_REFERENCE_POSITIONS.expression, metadata-core object-schema-fls-references.ts) deletes the expression, and the field is served readonly: false. The agent writes it, and the engine silently replaces the value.

What is true after PR #33150

  • HTTP twin (packages/runtime/src/domains/mcp.ts): readonly comes from writeNeverSetsField(declared.fields[k]). The answer is built from the masked definition, which is pinned.
  • stdio twin and resource: readonly comes from the masked field. The JSDoc and the changeset say the expression case holds there only when the caller is shown the expression.
  • Fixing it needs the unmasked declaration at the stdio seam: a second metadata read (objectView.getObject) or a bridge member that hands the verdict over.

Expected

The stdio door and the objects resource answer readonly exactly as the HTTP door does for the same caller, without serving the hidden expression.

Dedupe: stdio describe_object readonly expression mask, writeNeverSetsField stdio → #32530 (PR #33150, the HTTP half); none open.

Activity

  1. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: first touch → domain:server · p2 · pm:queue · 2026-11-13T22:48Z

    ⛔ Not a claim.

    • Row: stdio MCP describe_object and the objects resource tell an agent readonly: false for a stored expression field whose expression the caller's mask hides, while HTTP answers true. The agent's write is accepted and then overwritten. That is a wrong answer (p2).
    • Reach discount (−1 → p3): it needs the stdio door or the resource, plus a field mask that hides the expression, so it is a non-default arrangement.
    • MCP focus raise (+1 → p2): the agent gets a wrong answer at the MCP door, which is the false-green class in rule 8.
    • Fix lead: derive readonly from the unmasked declaration in describeServedObject, as the HTTP twin does (writeNeverSetsField(declared.fields[k])).
  2. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 13, 2026
  3. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY

    Claim: PM loop round 1 · epic seat epic:#32897#1 (registration 6867120489) · 2026-11-13T23:12Z
    Session: session_01L9qibvhQvy8SbR3j9f7kcb (card thread; seat session_01L9qibzoApSyVPuia3NCrBY)
    Account: os-tesla
    Branch: claude/project-thread-3d24w1 (the card thread's harness-pinned branch; the thread can push only this name)
    Worktree: objectstack-issue-33156
    Domain: domain:server
    Seat: epic:#32897#1
    File surface: packages/mcp/src/stdio-data-bridge.ts, packages/mcp/src/mcp-server-runtime.ts, packages/mcp/src/mcp-http-tools.ts (the get_record branch only), packages/runtime/src/domains/mcp.ts (bridge member only, if needed), their tests, root .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: platform code in objectstack (describeServedObject in packages/mcp/src/mcp-server-runtime.ts reads the masked field); the platform path that already covers it is the HTTP twin's writeNeverSetsField(declared.fields[k]) (packages/runtime/src/domains/mcp.ts, PR #33150); reached by a stdio-transport host with a mask that hides an expression — not seen on a deployment
    Thread-read: 6866865047
    Premise: holds after PR #33150 merges (the #33156 half starts then; read at main c04aa295)
    Serial constraints cleared: PR #33150 (#32530, domain:server#7) on mcp-server-runtime.ts / mcp-http-tools.ts: this card's half starts after it merges; PR #33159 (#32942) files excluded

    Order: /mnt/project-files/epic-32897-1/r2026-11-13/dispatch-33156-33030.md under that folder's FRAME.md. Contract review: this seat, at CONTRACT_REVIEW_TIER.


    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 Nov 13, 2026
  5. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#32897#1 · os-tesla · session_01L9qibvhQvy8SbR3j9f7kcb
    os-dev-report

    {
      "issue": [
        33156,
        33030
      ],
      "status": "done",
      "branch": "claude/project-thread-3d24w1",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/33175",
      "head": "59ccee50",
      "session": "session_01L9qibvhQvy8SbR3j9f7kcb",
      "premise_still_valid": true,
      "summary": "#33030: stdio get_record's null branch now throws recordNotFoundError into its own catch, so the stdio door renders the HTTP door's coded RECORD_NOT_FOUND / 404 (dataDoorErrorResult) for a missing id and a row-filter-hidden id alike; update_record / delete_record were already coded and are pinned. #33156 (after PR #33150 merged, 28a2f0f5 merged once): callerObjectView registers a package-internal declared read (setDeclaredObjectRead, keyed weakly by the view) answering the served object and its declaration from one read; describeServedObject(def, declared) judges readonly on the declared field like the HTTP twin, and builds the answer from the served object only. Both stdio describe_object and the objects resource read through readObjectWithDeclaration; plugin.ts unchanged.",
      "tests": "Measured first: #33030 pins at 47f6c939 3 failed | 5 passed (stdio text 'Record \"ghost\" not found in \"task\"', uncoded); #33156 pins at 28a2f0f5 2 failed | 32 passed (marked_up readonly false on tool and resource). At head: not-found 8/8, object-schema-mask 34/34, runtime mcp-object-schema-mask 73/73; pnpm --filter @objectstack/mcp test 64 files / 1148 tests passed (at c1b63360; 59ccee50 only reshapes a test double); mcp + runtime typecheck exit 0. Ablations via ablation-replace.mjs, each restored blob == HEAD: get_record bare sentence -> 3 red; served-field readonly -> 2 red; setDeclaredObjectRead removed -> 2 red; describe from the declaration -> 9 red (expression/hidden_cost not-served pin and case-table masking pins). Source-imported suites, no dist leg.",
      "gates": "dispatch-gates --commands at 59ccee50: 66 families, each with stdin closed, exit captured before any pipe; --ran: 66 accounted, 65 run, 1 NOT-MEASURED (check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, needs repo-wide dist; CI runs it). plugin-teardown-shape --self-test exit 3 in the shallow clone, 0 after fetching 621a4876. engine-double-contract exit 1 on a second double, fixed by reusing the file's one double, exit 0. Test-reading gates re-run at 59ccee50, exit 0. check:type-check-debt 0, pnpm lint 0. PR check-runs: in_progress at report time.",
      "line_budget": "8 files, +424 -25 (two changesets, 3 mcp sources, 3 test files)",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "deviations": [
        "get_record null branch throws recordNotFoundError into the catch (rendered by dataDoorErrorResult, the HTTP throw's renderer) instead of calling errorResultWithCode directly as the order words it; same bytes today, stays equal if the /data door adds a member to this code.",
        "#33156 parity pin across doors is one literal pinned on each side (mcp stdio tests + a runtime PARITY case on the real-layer harness): neither package depends on the other.",
        "Runtime full test suite not run locally: its only change is one test file (73/73); declared to CI.",
        "Report posted on both cards (one PR closes both)."
      ],
      "files_changed": [
        ".changeset/33030-stdio-get-record-coded-not-found.md",
        ".changeset/33156-stdio-describe-readonly-declared.md",
        "packages/mcp/src/mcp-http-tools.ts",
        "packages/mcp/src/mcp-server-runtime.ts",
        "packages/mcp/src/stdio-data-bridge.ts",
        "packages/mcp/src/stdio-data-bridge.not-found.test.ts",
        "packages/mcp/src/stdio-data-bridge.object-schema-mask.test.ts",
        "packages/runtime/src/domains/mcp-object-schema-mask.test.ts"
      ],
      "mcp_calls": "0",
      "api_writes": "2 relay writes so far — pr_create (fleet-write run 43531716436, PR #33175) and this os-dev-report comment (per card); 0 direct REST writes",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: b · reach: stdio resources/read objectstack://objects/task/records/ghost over the long-lived server at 59ccee50 answers {\"error\":\"Record \\\"ghost\\\" not found in \\\"task\\\"\"} with no code/status, while the two sibling object resources answer { error, code, status } and the same handler's catch adds code/status for a thrown coded refusal (mcp-server-runtime.ts bridgeResources record_by_id) · dedupe: record_by_id resource not found code, objectstack records resource RECORD_NOT_FOUND, stdio record resource uncoded miss",
        "carrier: seat · noted, not filed: runtime/src/domains/mcp.ts writeNeverSetsField JSDoc last sentence ('only the masked object to ask') and the unreleased .changeset/32530 sentence on the stdio expression case are now stale; outside this PR's surface"
      ]
    }
  6. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY

    Landed · #33156 · 2026-11-14T01:14Z — PR #33175 merged as f45c0125 (one PR for #33156 and #33030); Fixes #33156 closed this card. pm:dispatched off in this stroke.

    Delivered: stdio get_record answers a missing or hidden id with the coded RECORD_NOT_FOUND / 404, and stdio describe_object and the objects resource judge readonly on the declared field, as the HTTP door does; the hidden expression is still not served. The pending note .changeset/32530 was corrected in the same PR (seat record 6868774819). Left: the stdio record resource's uncoded miss is #33178.


    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

domain: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