You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(mcp)!: stdio transport requires an API-key principal — fail-closed, no unscoped bridge (ADR-0101, #3246)
Closes the platform's last identity-less execution surface: the long-lived MCP
stdio transport, which previously bridged the raw metadata service + data engine
with no ExecutionContext (record_by_id read via dataEngine.findOne, bypassing
RLS/FLS/tenant).
- plugin.ts start(): when stdio auto-start is requested, resolve
OS_MCP_STDIO_API_KEY through the SAME @objectstack/core chain as HTTP/REST
(resolveStdioExecutionContext → resolveAuthzContext → resolveApiKeyPrincipal),
build the caller ExecutionContext, and thread it (re-resolved per call) into a
principal-bound record reader. FAIL-CLOSED: no key / no objectql / an
unknown|revoked|expired key throws and refuses to start stdio. No `system`
bypass — full authority is a minted admin/service key.
- mcp-server-runtime.ts bridgeResources: record_by_id now takes a principal-bound
getRecord reader (ql.find(obj,{where:{id},context})); registered only when a
reader is supplied — no unscoped fallback. Dropped the raw IDataEngine bridge.
- authz matrix: mcp-stdio-authority experimental → enforced; probe/bite retargeted
from bridgeResources(unscoped-stdio) → stdio-principal-bound.
- ADR-0101 Proposed → Accepted; env-var docs add OS_MCP_STDIO_API_KEY.
Tests: mcp 83/83 (4 new fail-closed cases: no key / no objectql / unresolvable
key refuse to start; disabled stdio needs no key); dogfood authz-conformance 9/9.
BREAKING (stdio auto-start only): OS_MCP_STDIO_ENABLED/autoStart now requires
OS_MCP_STDIO_API_KEY; keyless stdio no longer starts. HTTP surface unaffected.
Refs #3246, #3167, ADR-0101, ADR-0096.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115eg8dAaCfWaDYYAm3ma36
Copy file name to clipboardExpand all lines: content/docs/deployment/environment-variables.mdx
+5-4Lines changed: 5 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -226,15 +226,16 @@ The **HTTP surface** and the long-lived **stdio transport** are separate switche
226
226
see the note below):
227
227
228
228
```bash
229
-
os start # HTTP MCP served at /api/v1/mcp by default
230
-
OS_MCP_SERVER_ENABLED=false os start # opt out of the HTTP MCP surface
231
-
OS_MCP_STDIO_ENABLED=true os start # additionally auto-start the local stdio transport
229
+
os start # HTTP MCP served at /api/v1/mcp by default
230
+
OS_MCP_SERVER_ENABLED=false os start # opt out of the HTTP MCP surface
231
+
OS_MCP_STDIO_ENABLED=true OS_MCP_STDIO_API_KEY=osk_... os start #start the local stdio transport as that key's identity
232
232
```
233
233
234
234
| Variable | Type | Default | Description |
235
235
|:---|:---|:---|:---|
236
236
|`OS_MCP_SERVER_ENABLED`| boolean |`true`| The MCP **HTTP** surface (`/api/v1/mcp`) is a core capability and defaults **on**. Set `false` to disable it (endpoint 404s, the Connect-an-Agent page disappears). |
237
-
|`OS_MCP_STDIO_ENABLED`| boolean |`false`| Auto-start the long-lived **stdio** transport at boot. Opt-in and **stricter** than the HTTP surface: stdio bridges the raw services with no per-request principal, so it is unscoped — safe only as a single-operator local tool. Leave off unless a local client spawns the process directly. |
237
+
|`OS_MCP_STDIO_ENABLED`| boolean |`false`| Auto-start the long-lived **stdio** transport at boot. Opt-in and **stricter** than the HTTP surface. **Requires `OS_MCP_STDIO_API_KEY`** — stdio runs as that key's identity with RLS/FLS/tenant applied; if the key is missing or invalid, boot **fails closed** (stdio refuses to start). See [ADR-0101]. |
238
+
|`OS_MCP_STDIO_API_KEY`| string | — | The `osk_...` API key the **stdio** transport runs as. Resolved through the same verify chain as the HTTP/REST surfaces, so reads are scoped to that identity's permissions. Mint one from **Setup → Connect an Agent** (or `POST /api/v1/keys`). For full authority, mint a key on a platform-admin or dedicated **service** identity — there is deliberately no `system`/unscoped bypass. |
238
239
|`OS_MCP_SERVER_NAME`| string |`objectstack`| Server name advertised to MCP clients. |
239
240
|`OS_MCP_SERVER_TRANSPORT`| enum |`stdio`|`stdio`\|`http`. Use `http` (Streamable HTTP) for a remote client; `stdio` for a local one. |
Copy file name to clipboardExpand all lines: docs/adr/0101-mcp-stdio-principal-admission.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# ADR-0101: MCP stdio Principal Admission — env-supplied API-key identity, fail-closed, no system bypass
2
2
3
-
**Status**: Proposed (2026-07-19)
3
+
**Status**: Accepted (2026-07-19) — v1 landed with this acceptance (#3246): key→EC threading, fail-closed start, no `system` bypass; `mcp-stdio-authority` graduated `experimental` → `enforced`
4
4
**Deciders**: ObjectStack Protocol Architects
5
5
**Builds on**: [ADR-0096](./0096-execution-surface-identity-admission.md) (execution-surface identity admission — this ADR closes its last unadmitted transport), [ADR-0024](./0024-mcp-connectors.md) §4 (trust model), [ADR-0036](./0036-app-as-rest-api-and-mcp-server.md) (the app *as* an MCP server — the surface being admitted), [ADR-0099](./0099-posture-adjudicated-tiering-and-external-rung.md) (posture rides the `ExecutionContext` this ADR threads)
6
6
**Composes with**: framework#3055 (consume-side declarative stdio **spawn** policy — the sibling trust decision: #3055 gates *who may start* a local MCP process from metadata; this ADR gates *as whom* our own stdio server reads data), [ADR-0097](./0097-declarative-connector-instances.md)
// ── stdio auto-start decision (opt-in, its OWN switch) ──
126
146
// Deliberately stricter than the HTTP-surface default (`isMcpServerEnabled`,
127
-
// default-on): start() attaches a long-lived transport — for stdio that
128
-
// means claiming the process's stdin/stdout AND bridging the RAW services
129
-
// with no per-request principal (unscoped — see the mcp-stdio-authority
130
-
// conformance row) — so it stays opt-in via a SEPARATE switch
147
+
// default-on): start() attaches a long-lived transport claiming the
148
+
// process's stdin/stdout, so it stays opt-in via a SEPARATE switch
131
149
// (`OS_MCP_STDIO_ENABLED` / the `autoStart` option), never the HTTP var.
132
150
// The HTTP surface does not depend on this: the runtime dispatcher serves
133
151
// `/api/v1/mcp` per-request regardless.
@@ -138,6 +156,69 @@ export class MCPServerPlugin implements Plugin {
138
156
'[MCP] Starting the stdio transport via OS_MCP_SERVER_ENABLED=true is DEPRECATED — that var now only gates the default-on HTTP surface. Use OS_MCP_STDIO_ENABLED=true (or the plugin `autoStart` option) for the long-lived stdio transport.',
139
157
);
140
158
}
159
+
160
+
// ── Principal-bound record reader for the stdio transport (ADR-0101) ──
161
+
// The long-lived stdio server reads ROW data only under an env-supplied
162
+
// API-key identity, resolved through the same @objectstack/core chain as the
0 commit comments