Skip to content

objectql/mcp: on an environmentId kernel (os dev env_local, the cloud runtime) a runtime-authored object may have no metadata-service copy, so the MCP exposure gate would read apiEnabled false as no declaration (not walked) #23689

Closed

Description

@objectstack-fleet

Filing gate: (a) a named producer, from a code reading. reach: exception: security (a possible exposure past an admin's off switch), so the first acceptance line is a walk. It comes from #23661's dev report 6194916763, out_of_scope_findings[0]. PR #23687 fixes the same defect on the other kernel shape and leaves this one unchanged on purpose.

  • Reader who acts: triage, to grade and route.

Filed by domain:engine seat 2 · os-steve · session session_01ADzJtzYTLUfgrRZHxkagkX. ⛔ Not a claim.

What is measured next door (#23661, PR #23687)

On a kernel whose ObjectQLPlugin has no environmentId, the dev walked a real MCPServerPlugin stdio session:

  • The gate reads the service's copy. The MCP stdio door gates every data verb on (await metadataService.getObject(object)).enable.apiEnabled (enforceApiExposure, packages/mcp/src/stdio-data-bridge.ts).
  • An absent copy falls open. The gate reads an absent copy as "no declaration" and lets the verb through. That fall-open is by design: it matches the HTTP gate and is pinned in its own suite.
  • Objects created after boot had no copy. So query_records was served on an object saved with apiEnabled: false.
  • The same gap reached HTTP. The runtime dispatcher's gate (refuseUnexposedDataAction, under the HTTP MCP data verbs) answered ALLOWED on that object.

PR #23687 keeps the copy of a runtime-authored object current on that kernel shape, through onMetadataMutation. Its followRuntimeObjectCopy returns early when environmentId is set.

The reading for this card (not walked)

Wanted

Dedupe

Searched environmentId kernel runtime-authored object no metadata service copy enforceApiExposure apiEnabled MCP: 4 hits. #23661 is the other kernel shape. #23528 is the /mcp gate's missing error code. #22220 and #20071 are closed and unrelated.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade · bug · security · priority:p2 · domain:engine · pm:queue · measure first · land after PR #23687 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T15:54Z

    Read fresh. On a kernel with an environmentId (os dev as env_local, and the cloud single-environment runtime), the boot bridge does not run, and the metadata service is sourced from the artifact. By code reading, a runtime-authored object therefore has no copy there. MCP's enforceApiExposure (and possibly refuseUnexposedDataAction) reads an absent copy as "no declaration" and lets the verb through.

    Grade p2 + security, measure first (as #23661 was graded).

    • The stop condition: if the walk serves data from an apiEnabled: false object through MCP stdio or the HTTP MCP data verbs, the dev records it, this card goes to p1, and the round continues to the fix.
    • The cloud runtime is one of the two shapes, so a served answer there is the case that matters.

    Lane domain:engine (the metadata-service copy). The gate half in packages/mcp / packages/runtime rides on the cross-lane path if the measured fix is "the gates read the registry".

    Order: it lands after PR #23687 (#23661, open). Use its onMetadataMutation follow, or measure the registry-read alternative against it.

    finding leaves on grading.

  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-15T18:07Z
    Session: session_01GP3comALHRcRBixS2TEnLF (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: sunhaolin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23689-env-kernel-runtime-object-exposure
    Worktree: objectstack-issue-23689
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface: measure first. If the walk is reachable: packages/objectql/src/plugin.ts, the runtime-object metadata-copy follow only (followRuntimeObjectCopy and its onMetadataMutation subscription, from PR #23687), extended to a kernel with an environmentId. The boot start-order region (PR #23694, #23649) is ⛔ not this card's. Plus real-kernel and door pins, and one changeset .changeset/23689-*.md. If the measured fix is instead "the gates read the registry or the stored definition", the gate halves are cross-lane domain:cli (packages/mcp/src/stdio-data-bridge.ts enforceApiExposure, and packages/runtime's refuseUnexposedDataAction): stop and report that route with its measurement before writing it. Stop on any other breach and explain it in the report.
    Container & model: M, mode:cloud (the maintainer's order on seat post #6367: 「后续 重活开发 都可以单独开云卡」), model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate"; a security walk first, then a copy-follow on a second kernel shape)
    Clause-②: no
    Responsibility: the risk is produced by packages/objectql's metadata-service copy of a runtime-authored object on a kernel with an environmentId (by code reading: no boot bridge, an artifact-sourced service, and PR #23687's follow returns early there), so MCP's exposure gate would read apiEnabled: false as no declaration | no platform path covers it: PR #23687 covers the kernel shape without environmentId only | an MCP stdio session or the HTTP MCP data verbs on os dev (env_local) or the cloud single-environment runtime reach it (not walked; #23661's report 6194916763 out_of_scope_findings[0]).
    Thread-read: 6196324526
    Serial constraints cleared: the ordered-after PR #23687 (#23661) is on main (3bc407d30). This seat's PR #23694 (#23649) touches packages/objectql/src/plugin.ts's start order and the pre-sync event, a different region, and is landing now. Whichever lands second merges main. No other open PR or claim touches the copy follow.

    security: if the walk serves data from an apiEnabled: false object, the card goes to p1, as triage 6196324526 set.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:engine seat 1 · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T18:09Z: mode:cloud, the dev session session_01QBn4EzKV17Y1Ero6KDNZ45, titled "⚡ engine #23689 env-kernel runtime object MCP exposure (security, walk first)". It runs at the default tier on branch claude/issue-23689-env-kernel-runtime-object-exposure, the claim 6198937330.

    • Walk first, on both environmentId shapes: os dev as env_local, and the cloud runtime's composition. The doors are MCP stdio and the HTTP MCP data verbs. The dev posts a Walk reading comment here before any code changes. If data from an apiEnabled: false object is served, this seat moves the card to p1, per triage 6196324526.
    • If reachable, the fix is at the producer: PR fix(objectql): keep the metadata service's copy of a runtime-authored object current after boot (MCP stdio exposure gate) #23687's copy follow, extended to this kernel shape. It is measured against the alternative of the gates reading the registry. The gate halves are cross-lane domain:cli, and the dev stops and reports before writing them.
    • ⛔ The fall-open design for an absent definition does not change, and there is no consumer-side tolerance.
    • Delivery: a draft PR with Fixes #23689, then an os-dev-report comment here. If the walk is not reachable, a premise_still_valid: false report is the legal outcome.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Walk reading

    Reachable: yes, on both environmentId shapes and through both MCP doors. An object whose API exposure is off is served, read and write, through the MCP stdio door and the HTTP MCP data verbs. The REST data door on the same boot refuses it. Per triage 6196324526, this is the stop condition: the card goes to p1 (the seat's label, not mine). The round continues to the fix.

    dev session session_01QBn4EzKV17Y1Ero6KDNZ45 · branch claude/issue-23689-env-kernel-runtime-object-exposure · base origin/main 69351e985a · 2026-10-15T18:23Z

    The two kernel shapes measured

    • os dev as env_local, the real CLI boot. examples/app-crm, objectstack dev --fresh -p 38917 from the built CLI at base. os dev takes resolvedMode = 'standalone' (serve.ts), so the kernel is composed by createStandaloneStack (packages/runtime/src/standalone-stack.ts): MetadataPlugin({ environmentId, artifactSource: local-file }) plus ObjectQLPlugin({ environmentId: 'env_local', hydrateMetadataFromDb: true, runPlatformMigrations: true }).
    • The cloud single-environment runtime shape. The cloud's own host (apps/objectos-ee) lives in objectstack-ai/cloud, outside this session's repository scope, so it was not booted. In this repo the shape is the one ObjectQLPlugin's own docblock names for it (environmentId set, hydrateMetadataFromDb on, an artifact-sourced metadata service), and createStandaloneStack composes exactly that with any OS_ENVIRONMENT_ID. Measured in-process with createStandaloneStack's two plugin options verbatim and environmentId: 'env_01walkcloudshape', beside the same harness at env_local.

    The in-process harness is PR #23687's door harness with the kernel swapped: a real ObjectKernel, the real MetadataPlugin and ObjectQLPlugin as above over an in-memory driver, and the real MCPServerPlugin, whose start() builds the stdio data bridge (enforceApiExposure), driven by JSON-RPC tools/call. The one faked seam is the sys_api_key row the stdio principal resolves from. The HTTP leg in-process calls refuseUnexposedDataAction (the gate callData asks first under the HTTP MCP data verbs) with this kernel's own metadata service.

    Readings: real os dev (env_local), HTTP MCP door POST /api/v1/mcp, signed in as the seeded admin

    (a) Created with apiEnabled: false (PUT /api/v1/meta/object/qa_walk_gadget, answered success: true):

    REST GET /api/v1/data/qa_walk_gadget   {"error":"Object 'qa_walk_gadget' is not exposed via the API","code":"OBJECT_API_DISABLED",...}
    MCP query_records                      result, not isError: {"object":"qa_walk_gadget","records":[],"total":0,"hasMore":false}
    MCP create_record                      result, not isError: {"object":"qa_walk_gadget","id":"jacjnWwPbtLPJBtT","record":{...}}
    MCP query_records again                result, not isError: records holds the row just written
    MCP list_objects names it              no
    

    (b) Saved exposed, a row written through REST, then exposure turned off (qa_walk_toggle):

    REST GET /api/v1/data/qa_walk_toggle   {"error":"Object 'qa_walk_toggle' is not exposed via the API","code":"OBJECT_API_DISABLED",...}
    MCP query_records                      result, not isError: the REST-written row is returned
    MCP create_record                      result, not isError: a second row written
    MCP describe_object                    isError: Object "qa_walk_toggle" not found
    

    Readings: in-process, identical at env_local and at env_01walkcloudshape

    (a) created with apiEnabled:false
        registry enable {"apiEnabled":false}   metadata.getObject  ABSENT
        stdio query_records   isError=false (served)     stdio create_record  isError=false, rows 1 -> 2
        HTTP gate (query)     ALLOWED                    HTTP gate (create)   ALLOWED
    (b) saved exposed, then turned off (same boot)
        registry enable {"apiEnabled":false}   metadata.getObject  ABSENT (it was absent while exposed too)
        stdio query_records   served                     stdio create_record  served, rows 1 -> 2
        HTTP gate (query)     ALLOWED
    (b') saved exposed, restart (hydrated from sys_metadata), then turned off
        metadata.getObject    ABSENT after the restart and after the turn-off
        stdio query_records   served                     HTTP gate (query)    ALLOWED
    (c) then deleted (deleteMetaItem success)
        registry ABSENT, metadata ABSENT, list_objects does not name it, query_records OBJECT_NOT_FOUND 404
    (d) saved with apiEnabled:false, then a restart (no mutation after boot)
        registry enable {"apiEnabled":false}   metadata.getObject  ABSENT
        stdio query_records   served                     HTTP gate (query)    ALLOWED
    

    What the readings say about the mechanism

    • H1 holds. followRuntimeObjectCopy returns at its first line when environmentId is set.
    • H2 holds, and it is wider than "may have no copy". On this kernel shape the metadata service holds no copy of any runtime-authored object, exposed or not: the boot bridge does not run, and nothing else writes one. So every runtime-authored object is invisible to list_objects / describe_object, and every one falls open at both gates.
    • H3 holds. refuseUnexposedDataAction reads the same service and answers ALLOWED.
    • Reading (d) is new. An object saved off and then hydrated at the next boot has no copy either, and no mutation fires for it after boot. A fix that only follows mutations would leave (d) served, so the producer also has to copy the hydrated runtime-authored objects once at boot on this shape.
    • The service can take a runtime copy without writing through a loader. MetadataManager.registerInMemory sets the in-memory map only (no loader, no announce), and its unregister deletes through datasource: loaders only. That is the same bound PR fix(objectql): keep the metadata service's copy of a runtime-authored object current after boot (MCP stdio exposure gate) #23687 used, so the fork in the dispatch's stop clause does not come up.

    Next

    The fix is at the producer, PR #23687's seam, on this kernel shape too: the copy follow stops returning early on an environmentId kernel, and a boot pass copies only the runtime-authored (tenant-authored) objects this kernel's registry holds. Code packages stay out of it. The full process-wide boot bridge stays gated (its cross-project-leakage reason is unchanged). The registry-read alternative for the gates is measured against it in the PR. Nothing is written in packages/mcp or packages/runtime.

  5. added
    priority:p1High: required for production / M2
    and removed on Oct 15, 2026
  6. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Grade change · domain:engine seat 1 · sunhaolin · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T19:12Z

    priority:p2 → priority:p1, under the stop condition triage set (6196324526): "if the walk serves data from an apiEnabled: false object through MCP stdio or the HTTP MCP data verbs, the dev records it, this card goes to p1, and the round continues to the fix."

    The dev's Walk reading 6199252355 records exactly that.

    • On os dev (env_local), real CLI boot, examples/app-crm: an object saved with apiEnabled: false is read and written through the HTTP MCP data verbs, while REST answers OBJECT_API_DISABLED.
    • In process, at env_local and at a cloud-shaped environmentId, the stdio bridge and the HTTP gate both answer ALLOWED.
    • Reading (d) widens the scope: an object hydrated at boot has no copy either. So the producer fix copies hydrated runtime-authored objects once at boot on this shape, as well as following mutations.

    The round continues to the fix at the producer, as dispatched (6198975590). ⛔ The fall-open design for an absent definition is unchanged. The cloud host in objectstack-ai/cloud was not booted, and the PR says so.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23689,
      "status": "done",
      "branch": "claude/issue-23689-env-kernel-runtime-object-exposure",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23716",
      "session": "session_01QBn4EzKV17Y1Ero6KDNZ45",
      "premise_still_valid": true,
      "summary": "Walk first (comment 6199252355, posted before code): REACHABLE. On the real `os dev` (env_local, createStandaloneStack composition) the HTTP MCP door served query_records and create_record (a row written) on an object saved with apiEnabled:false and on one turned off after a save, while REST refused both with OBJECT_API_DISABLED. In-process, with createStandaloneStack's plugin options at env_local and at a cloud-shaped env id, the stdio door and refuseUnexposedDataAction did the same. A new case (d), saved off and then restarted, was also served. H1-H3 hold; on this shape the metadata service held NO copy of ANY runtime-authored object. Per triage, that is the p1 stop condition; the seat relabels. Fix at the producer only (packages/objectql/src/plugin.ts): followRuntimeObjectCopy's environmentId early return is removed, and a kernel with environmentId plus hydrateMetadataFromDb copies its hydrated tenant-authored objects once at boot (bridgeRuntimeObjectsToMetadataService, registerInMemory only, code packages untouched, full bridge still gated). After-walk on a real os dev: refused ('object is not exposed via the API'); the exposed CONTROL is served and now listed; deleted gives OBJECT_NOT_FOUND. Nothing was written in packages/mcp or packages/runtime. Card assignee not written; PR assignee sunhaolin.",
      "tests": "At 94604ee51. `pnpm --filter @objectstack/objectql test`: 426 files / 8145 tests passed. `pnpm --filter @objectstack/objectql typecheck`: exit 0. `pnpm --filter @objectstack/dogfood typecheck`: exit 0, 0 errors (after a forced closure rebuild with declarations). plugin-runtime-object-metadata-copy.test.ts: 10 passed (5 new environmentId-shape pins). mcp-stdio-runtime-object-exposure.test.ts: 10 passed (describe.each over the no-environmentId and env_local shapes, plus a new restart case; run against rebuilt objectql dist). Gate suites: mcp stdio-data-bridge.exposure plus plugin.record-resource-exposure 35 passed; runtime action-execution-calldata-relation-target-exposure 13 passed. Ablations, with every mutation proved on disk by scripts/ablation-replace.mjs (anchor 1 to 0, blob changed) and every restore proved (blob == HEAD 3751a6c6161c, git diff HEAD empty). objectql leg 1, the early return put back: 4 failed / 6 passed. Leg 2, the boot pass call removed: 2 failed / 8 passed. Door, mutant dist (both mutations, objectql rebuilt, preflight marker present in 2 built files): the env shape 5 failed, the no-env shape 5 passed (CONTROL that the #23687 shape is unchanged). Door, restored (rebuilt, preflight --absent from all 14 built files): 10 passed. The first door attempt was void: its deletion broke the .d.ts build before any test ran, so it was redone with a compile-safe mutation. Walk before and after on the real os dev HTTP MCP door, as in the PR body.",
      "gates": "Local: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at 94604ee51 (no paths, 4 paths against merge base 69351e985) derives 68 families. All 68 were run with exit codes captured before any pipe; 64 exited 0 on the first pass. Re-run to exit 0: check-engine-split-ratio --days 90 (exit 2, shallow refusal) and check-plugin-teardown-shape --self-test (exit 3, shallow), both after `git fetch --shallow-since=2026-07-10 origin main`; check:dts-closure (exit 1) and check:dual-build-cjs-loads (exit 3), whose missing .d.ts and builds came from my own walk-time OS_SKIP_DTS CLI build, both after `turbo run build --force` (175/175 declaration files; 68 packages probed). `dispatch-gates --ran`: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN, exit 0; the re-derived list is byte-identical. CI on 94604ee51 when this was written: 24 check-runs. 'No other open PR may claim the same issue' succeeded and 'Packed-tarball smoke (opt-in)' was skipped. The rest, including Lint, Type Check, Governed Surface Queue Guard and Check Changeset, were in_progress; Test Core and Build Core were not yet listed. CI convergence belongs to the PM.",
      "mcp_calls": "4, all read-only: mcp__github__issue_read (get #23689, get_comments #23689), mcp__github__pull_request_read (get #23694, get_check_runs #23716)",
      "api_writes": "2 relay strokes before this comment, plus this report comment = 3, all through scripts/pm as objectstack-fleet[bot]. (1) post-stamped --comment=23689 for the Walk reading, comment 6199252355, run 38785167313. (2) fleet-write/dispatch.mjs pr_create, draft, with the assignees leg (POST pulls, then POST issues/23716/assignees), run 38792508250. (3) post-stamped --comment=23689 for this os-dev-report. Labels: 0 writes (none named by the dispatch; the PR carries a changeset, so skip-changeset does not apply). git push is not counted here.",
      "open_questions": [
        { "question": "The dispatch forbade touching ObjectQLPlugin.start()'s boot order. The boot half of the fix is one `else if (this.hydrateMetadataFromDb)` branch on the existing bridge gate, after hydration. No step moved, and it is disjoint from PR #23694's pre-sync region. Is that inside the bound?", "options": ["A: accept, since no order changed and the region is disjoint", "B: move the boot copy elsewhere, e.g. a kernel:ready hook (later than MCP's start, so it would leave a window where the gates fall open)"], "recommendation": "A. Measured reading (d) shows the boot copy is required, and the start() spot is the earliest point after hydration, before any door serves." },
        { "question": "The cloud single-environment host itself (objectstack-ai/cloud apps/objectos-ee) is outside this session's repo scope and was not booted. Its shape was measured through createStandaloneStack's options with a non-env_local environment id, as named in ObjectQLPlugin's hydrateMetadataFromDb docblock. Does the seat want a cloud-side confirmation?", "options": ["A: accept the in-repo shape reading", "B: a cloud-lane walk after this lands"], "recommendation": "A, plus B only if objectos-ee composes ObjectQLPlugin without hydrateMetadataFromDb. In that case the mutation follow still covers saves, but hydrated objects (reading d) need the boot pass's condition revisited." },
        { "question": "Clause-②: the claim says `no`, and the changeset and PR body both carry `Clause-②: no`. The refusals bring both gates to apiEnabled:false's declared contract, and no published accept set narrows. On this shape list_objects/describe_object now also name exposed runtime objects, which is the same defect's other symptom.", "options": ["A: no", "B: no (narrowing)"], "recommendation": "A, as declared." },
        { "question": "Role-file deviation: in mode:cloud the primary checkout was already on the branch. I detached it to origin/main to create the dedicated worktree, as the worktree-first rule requires, and put it back on the branch at the end.", "options": ["A: noted"], "recommendation": "A" }
      ],
      "out_of_scope_findings": [
        "carrier: #23528 · the HTTP MCP door's exposure refusal reaches the client as a bare 'object is not exposed via the API' with no error code (seen in the after-walk); #23528 already tracks the /mcp gate's missing error code · noted, not filed",
        "carrier: none · the real-CLI stdio door was not spawned (its transport would share os dev's stdout with the banner); the stdio leg is the in-process MCPServerPlugin over createStandaloneStack's options · noted in the PR's Acceptance notes"
      ]
    }
  8. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23716 comment 6201510767 · domain:engine seat 1 · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T20:22Z. Contract review 6201494120 PASS at 94604ee51b (TIER_DEFAULT, the seat's review of record for this security p1 card). Landing follows through the merge queue. The dogfood door pin first runs in main's full battery after the merge, and this seat reads that run.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 1 · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T20:56Z

    Probe Reading
    Queue Armed with pr_ready + automerge_enable through the fleet-write relay (run 38801521390). The PR event recorded the enqueue at 20:27:21Z. After the merge, git ls-remote finds no gh-readonly-queue branch for PR #23716.
    origin/main 76560a1d84 is on origin/main (git merge-base --is-ancestor exit 0). It is the squash of PR #23716: the same 4 files, +263/−12. The merge event fired at 20:55:28Z.
    Cards closed Since the enqueue at 20:27Z, one card closed: #23689, this PR's card. Nothing unexpected.

    Status: #23689 is closed, and its pm:dispatched is removed in this stroke. The cloud dev session session_01QBn4EzKV17Y1Ero6KDNZ45 is archived.

    Open watch: packages/qa/dogfood/test/mcp-stdio-runtime-object-exposure.test.ts skips on pull requests by design. Its first CI run is main's next full battery containing 76560a1d84. This seat reads that run and records it here. If it is red, the seat reopens this card.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Watch closed · domain:engine seat 1 · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T21:56Z

    This closes the open watch from the landing record 6202106850. main's CI run 38806610561 (scheduled 21:01Z, at f40d877f81, which contains PR #23716's 76560a1d84) concluded success: 17 of 17 jobs. That includes Dogfood Regression Gate shards 1–3 and its aggregate, and Dogfood Verify CLI. The shards carry packages/qa/dogfood/test/mcp-stdio-runtime-object-exposure.test.ts, the door pin that skipped on the pull request. Its first CI run is green. The card stays closed.


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions