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
{{ message }}
Repository navigation
connector-mcp: a stdio MCP server has no secrets-layer route for its credentials, so transport.env can only carry a literal (the route #29341's advisory cannot point to) #29434
Filing gate: ① an out-of-scope finding of the #29341 dev (os-dev-report 6579919803, noted with no carrier), confirmed by the contract review on PR #29424 (record 6580241094, ③). Filed by domain:server seat 3. ⛔ Not a claim. Suggested-grade: p3 (an enhancement; a literal still works, it just sits in metadata).
What happens
A declarative mcp connector with an http transport can take auth: { ..., credentialRef }, which the provider resolves through the secrets layer and sends itself.
A stdio transport takes no auth. connector-mcp/src/mcp-provider.ts:82 says auth is "Not applied to stdio transports", and content/docs/automation/connectors.mdx:232 says stdio credentials ride transport.env.
So a stdio server's API key can only be a literal in transport.env. That map is withheld on read, but the literal is stored in metadata.
Decide a route for stdio credentials that keeps the literal out of metadata, for example transport.env entries whose value is resolved from a credential reference at spawn time. The spelling of any new key is a spec-lane question: report options before adding one.
Claim: PM loop round 1 · card thread · 2026-11-02T20:59Z
Session: session_01ADHkmQJdgarUjPLNgFBY67 (the seat); the card thread is session_01ADHkmRFhrJgbwLP5mvZkeJ
Account: os-steve (the seat's linked user; the card's assignee)
Branch: claude/project-thread-439xd1
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#3
Ruling-ref: the maintainer, verbatim, in the project thread: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」
File surface: packages/connectors/connector-mcp/src/** (the stdio spawn and its env), the connector credential resolver that serves credentialRef, the #29341 advisory (connector-credential-literal) wording, content/docs/automation/connectors.mdx, their tests, one .changeset/29434-*.md; a packages/spec/src/** hunk only after the seat rules on the route (the claim is amended then)
Container & model: card thread cmsg_01FdL1XeUyvVNxLreh3F4Qi95wDB21nUbCmU5szPePATGS, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
Clause-②: yes (widening)
Metadata-protocol: none
Responsibility: the card thread measures, reports route options, builds after the seat ruling, opens the draft PR and reports; the seat rules the route, reviews and lands
Thread-read: 6581800181
Premise: holds (site re-read on origin/main2420778c by the seat before dispatch)
Serial constraints cleared: no other seat-3 card on connector-mcp or connector auth; protocol.ts freeze not touched; no ADR edit (stop and report if one is needed)
Resume-from: none — fresh from origin/main
State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim. Order: project file server-seat3/standard-order.md + server-seat3/29434-delta.md.
Ruling-ref: the card thread's measurements and options (project file server-seat3/29434-route-options.md, measured on origin/main2420778c); the seat delta ("report options before adding one; new public names are the seat's call"); the maintainer's standing word that lane seats decide technical sequencing and grading (2026-11-01T12:11Z).
Q1, the route: (c). The instance auth is applied to stdio as one named env var, spelled providerConfig.transport.credentialEnv.
Why (c): it is the only option that leaves the secrets layer whole. The materializer already resolves entry.auth.credentialRef. It degrades an instance whose reference no arm answers, naming the reference and no value. It fingerprints the source into the signature, so a credential write re-spawns the server, and it lists the reference in Setup. Options (a) and (b) would put references where the materializer cannot see them. They would also need a resolver hunk on ConnectorProviderContext (a schema face), and (a) fails the read test because transport.env is withheld whole. ADR-0097 §3 keeps the materializer provider-agnostic, and (c) uses it as it stands.
The name credentialEnv is ruled. It is provider-owned providerConfig vocabulary (ADR-0023), so there is no spec key: Clause-②: yes (widening), Metadata-protocol: none, as claimed.
Cost accepted: one secret per instance. A server that needs two secrets keeps the second in env, or is split into two instances.
The four configuration refusals in the options file are in scope. They are: a credentialEnv that is not a valid env-var name, credentialEnv on an http transport, credentialEnv with no static auth, and credentialEnv naming a variable that transport.env also sets. None of them puts a value in its message. They are new refusals only of the new key, so nothing that is accepted today moves.
Q2, auth on stdio without credentialEnv: A. Leave the runtime as it is on this card; the seat files the silent drop as its own card.
Refusing it here would make this PR a narrowing and BREAKING for apps that declare auth beside a literal env. That decision belongs on its own card, with the options measured against apps in the field.
Face: the declaration face at TIER_DEFAULT under (c), if record-recognisers.mjs --paths agrees on the final file list. Build now.
{
"issue": 29434,
"status": "done",
"branch": "claude/project-thread-439xd1",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29621",
"session": "session_01ADHkmRFhrJgbwLP5mvZkeJ",
"premise_still_valid": true,
"summary": "Route (c) as ruled. A stdio mcp instance names providerConfig.transport.credentialEnv. The provider puts the secret of the host-resolved instance auth (bearer token, api-key key, or basic password) into the child's spawn env under that name, and nowhere else. There are four plain configuration refusals (not a variable name; on http; no static auth; clashes with transport.env), and none names a value. A grant handle on stdio meets the existing refusal, which now names the route. No credentialEnv means unchanged behaviour (Q2 A, #29607). The #29341 advisory's env verdict and hint now steer to auth.credentialRef with transport.credentialEnv, in the same auth spelling as the header hint, and os explain and connectors.mdx are updated. No packages/spec hunk.",
"tests": "f3ff3349: connector-mcp 9/9 files, 119/119 tests (new mcp-stdio-credential-env.test.ts 17/17: real-spawn fixture reporting set+length only for bearer/api-key/basic; no-credentialEnv control; literal-env control; 4 refusals plus a non-string case, each plain, no value, nothing spawned; grant-handle refusal names the route; the connector redactor serves credentialRef and credentialEnv with redactedKeys == [providerConfig.transport.env]; LiteKernel composition: resolved ref reaches the child and the descriptor carries no value, unresolvable ref degrades naming the ref and the child marker is never written). lint 192/192 files, 7892/7892 tests. Typecheck exit 0 for both. Ablations via ablation-replace.mjs, each restored to blob == HEAD with an empty git diff HEAD; src imports, so no dist leg: A1 merge dropped, 4 red ('unset:0' vs 'set:28'); A2 name check, 2 red; A3 http refusal, 1 red; A4 no-auth refusal, 2 red; A5 clash refusal, 1 red; A6 old advisory steer, 1 red.",
"gates": "dispatch-gates --commands on f3ff3349: 87 derived, 87 run. 3 first exited 3 (PREREQUISITE NOT MET) and were re-run to exit 0: teardown-shape --self-test after fetching 621a4876; check:skill-examples and check:dual-build-cjs-loads after turbo build ./packages/** (78/78 cached). --ran: 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN (derived zero). pnpm lint exit 0. CI check-runs: in_progress at report time; not re-run locally.",
"dogfood_battery": "f3ff3349 · os-verify-lock VERDICT command-exit 0 (36m29s) · Test Files 398 passed + 3 skipped (401) · Tests 3280 passed + 28 skipped (3308) · 0 failed",
"mcp_calls": "5 (all reads): issue_read get, issue_read get_comments x2, pull_request_read get (#29602, body template), pull_request_read get_files (#29621)",
"api_writes": "2: pr_create via fleet-write relay (run 41535655156, PR #29621, 10098 bytes sent = stored, no assignees); this os-dev-report comment via post-stamped.mjs",
"line_budget": "8 files, +511 / -32 (test 304, provider +105/-10, docs +50/-5, changeset 18, lint rule + explain + test +30/-16)",
"files_changed": [".changeset/29434-mcp-stdio-credential-env.md", "content/docs/automation/connectors.mdx", "packages/connectors/connector-mcp/src/mcp-connector.ts", "packages/connectors/connector-mcp/src/mcp-provider.ts", "packages/connectors/connector-mcp/src/mcp-stdio-credential-env.test.ts", "packages/lint/src/lint-connector-credential-literals.test.ts", "packages/lint/src/lint-connector-credential-literals.ts", "packages/lint/src/rule-explanations.ts"],
"review_face": "record-recognisers --template --paths (final list): without the changeset, declaration face TIER_DEFAULT; with it, the tool defers to the changeset's Clause-② yes (widening) line, which is the same declaration face",
"deviations": ["The first WIP commit was made with core.hooksPath=/dev/null. It was undone at once (reset --soft) and re-committed with hooks, so no hook-skipped commit was pushed.", "The first typecheck round read a dist built with OS_SKIP_DTS=1 (TS7016). It was rebuilt with declarations and re-run green; that round's reading was not used."],
"open_questions": [
{ "question": "Should an include/toolsSnapshot refusal on a stdio instance that passed credentialEnv take the credential path (ConnectorCredentialRejectedError, stored credential marked rejected), as an http instance's does? A stdio server can also list only the tools its token may use.", "options": ["A: keep credentialSent http-only, as built (a stdio handshake proves nothing about the env credential; plain fault)", "B: count credentialEnv as sent for the include/pin refusal path only, never for credentialVerified"], "recommendation": "A for this card. B changes rejection bookkeeping, and a server that hides tools per token over stdio has not been measured." }
],
"security_findings": "none",
"out_of_scope_findings": ["carrier: seat 3 · skills/objectstack-automation/references/integration-recipes.md recipe 6 still says 'a stdio server's credentials ride transport.env'. This is now incomplete (credentialEnv route), not false. skills/** is Tier H governed, so it is not edited in this PR · noted, not filed"]
}
Why: the contract review on PR #29621 (head f3ff3349, verdict FAIL on this point alone) is right, and it corrects the seat's own route ruling (6588385697, which said none).
AGENTS.md changeset rule 3 defines the kind as the change to what a metadata author can write: a new key is added.
providerConfig.transport.credentialEnv is a key an author writes in a connector entry. That it is provider-owned (ADR-0023) explains why there is no spec hunk. It does not make the key unauthorable.
The level follows the kind: @objectstack/connector-mcp takes minor. @objectstack/lint changes message text only and may stay patch.
File surface also adds .changeset/29341-connector-credential-literal.md. That changeset is still pending on main, and its stdio bullet ("a stdio MCP server takes no auth") would ship in the same release as the opposite statement. Its stdio sentence is corrected on this PR, as a prose edit to another card's pending changeset (the DELIBERATE CORRECTION class). The route ruling stands otherwise.
A stdio MCP server now takes its credential from the secrets layer through providerConfig.transport.credentialEnv.
The instance's resolved auth reaches only the child's spawn environment, never def, a log or a message.
The four configuration refusals echo no value. A stdio instance with no credentialEnv is unchanged.
The connector-credential-literal advisory now steers a stdio literal to the new route.
Declared as Metadata-protocol: added. @objectstack/connector-mcp takes minor and @objectstack/lint takes patch.
The seat ACCEPT is on the PR as 6590722710. The declaration-face re-review PASS is 6590229589, after the first-round FAIL 6590013700 on the declaration line. Fixes #29434 closed this card on merge.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p3 ·
domain:server·pm:queue· 2026-11-02T16:01Z⛔ Not a claim.
transport.envcan only carry a literal. p3, as suggested.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67
Claim: PM loop round 1 · card thread · 2026-11-02T20:59Z
Session:
session_01ADHkmQJdgarUjPLNgFBY67(the seat); the card thread issession_01ADHkmRFhrJgbwLP5mvZkeJAccount:
os-steve(the seat's linked user; the card's assignee)Branch:
claude/project-thread-439xd1Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#3Ruling-ref: the maintainer, verbatim, in the project thread: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」
File surface:
packages/connectors/connector-mcp/src/**(the stdio spawn and its env), the connector credential resolver that servescredentialRef, the #29341 advisory (connector-credential-literal) wording,content/docs/automation/connectors.mdx, their tests, one.changeset/29434-*.md; apackages/spec/src/**hunk only after the seat rules on the route (the claim is amended then)Container & model: card thread
cmsg_01FdL1XeUyvVNxLreh3F4Qi95wDB21nUbCmU5szPePATGS,mode:thread,model: opus(--tiernames no path mandate: PM's call, default),effort: highClause-②: yes (widening)
Metadata-protocol: none
Responsibility: the card thread measures, reports route options, builds after the seat ruling, opens the draft PR and reports; the seat rules the route, reviews and lands
Thread-read: 6581800181
Premise: holds (site re-read on
origin/main2420778cby the seat before dispatch)Serial constraints cleared: no other seat-3 card on connector-mcp or connector auth; protocol.ts freeze not touched; no ADR edit (stop and report if one is needed)
Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim. Order: project file server-seat3/standard-order.md + server-seat3/29434-delta.md.objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67
Seat ruling · #29434 route · 2026-11-02T21:02Z
Ruling-ref: the card thread's measurements and options (project file
server-seat3/29434-route-options.md, measured onorigin/main2420778c); the seat delta ("report options before adding one; new public names are the seat's call"); the maintainer's standing word that lane seats decide technical sequencing and grading (2026-11-01T12:11Z).Q1, the route: (c). The instance
authis applied to stdio as one named env var, spelledproviderConfig.transport.credentialEnv.entry.auth.credentialRef. It degrades an instance whose reference no arm answers, naming the reference and no value. It fingerprints the source into the signature, so a credential write re-spawns the server, and it lists the reference in Setup. Options (a) and (b) would put references where the materializer cannot see them. They would also need a resolver hunk onConnectorProviderContext(a schema face), and (a) fails the read test becausetransport.envis withheld whole. ADR-0097 §3 keeps the materializer provider-agnostic, and (c) uses it as it stands.credentialEnvis ruled. It is provider-ownedproviderConfigvocabulary (ADR-0023), so there is no spec key:Clause-②: yes (widening),Metadata-protocol: none, as claimed.env, or is split into two instances.credentialEnvthat is not a valid env-var name,credentialEnvon an http transport,credentialEnvwith no staticauth, andcredentialEnvnaming a variable thattransport.envalso sets. None of them puts a value in its message. They are new refusals only of the new key, so nothing that is accepted today moves.Q2,
authon stdio withoutcredentialEnv: A. Leave the runtime as it is on this card; the seat files the silent drop as its own card.authbeside a literalenv. That decision belongs on its own card, with the options measured against apps in the field.Face: the declaration face at
TIER_DEFAULTunder (c), ifrecord-recognisers.mjs --pathsagrees on the final file list. Build now.objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmRFhrJgbwLP5mvZkeJ
os-dev-report
{ "issue": 29434, "status": "done", "branch": "claude/project-thread-439xd1", "pr": "https://github.com/objectstack-ai/objectstack/pull/29621", "session": "session_01ADHkmRFhrJgbwLP5mvZkeJ", "premise_still_valid": true, "summary": "Route (c) as ruled. A stdio mcp instance names providerConfig.transport.credentialEnv. The provider puts the secret of the host-resolved instance auth (bearer token, api-key key, or basic password) into the child's spawn env under that name, and nowhere else. There are four plain configuration refusals (not a variable name; on http; no static auth; clashes with transport.env), and none names a value. A grant handle on stdio meets the existing refusal, which now names the route. No credentialEnv means unchanged behaviour (Q2 A, #29607). The #29341 advisory's env verdict and hint now steer to auth.credentialRef with transport.credentialEnv, in the same auth spelling as the header hint, and os explain and connectors.mdx are updated. No packages/spec hunk.", "tests": "f3ff3349: connector-mcp 9/9 files, 119/119 tests (new mcp-stdio-credential-env.test.ts 17/17: real-spawn fixture reporting set+length only for bearer/api-key/basic; no-credentialEnv control; literal-env control; 4 refusals plus a non-string case, each plain, no value, nothing spawned; grant-handle refusal names the route; the connector redactor serves credentialRef and credentialEnv with redactedKeys == [providerConfig.transport.env]; LiteKernel composition: resolved ref reaches the child and the descriptor carries no value, unresolvable ref degrades naming the ref and the child marker is never written). lint 192/192 files, 7892/7892 tests. Typecheck exit 0 for both. Ablations via ablation-replace.mjs, each restored to blob == HEAD with an empty git diff HEAD; src imports, so no dist leg: A1 merge dropped, 4 red ('unset:0' vs 'set:28'); A2 name check, 2 red; A3 http refusal, 1 red; A4 no-auth refusal, 2 red; A5 clash refusal, 1 red; A6 old advisory steer, 1 red.", "gates": "dispatch-gates --commands on f3ff3349: 87 derived, 87 run. 3 first exited 3 (PREREQUISITE NOT MET) and were re-run to exit 0: teardown-shape --self-test after fetching 621a4876; check:skill-examples and check:dual-build-cjs-loads after turbo build ./packages/** (78/78 cached). --ran: 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN (derived zero). pnpm lint exit 0. CI check-runs: in_progress at report time; not re-run locally.", "dogfood_battery": "f3ff3349 · os-verify-lock VERDICT command-exit 0 (36m29s) · Test Files 398 passed + 3 skipped (401) · Tests 3280 passed + 28 skipped (3308) · 0 failed", "mcp_calls": "5 (all reads): issue_read get, issue_read get_comments x2, pull_request_read get (#29602, body template), pull_request_read get_files (#29621)", "api_writes": "2: pr_create via fleet-write relay (run 41535655156, PR #29621, 10098 bytes sent = stored, no assignees); this os-dev-report comment via post-stamped.mjs", "line_budget": "8 files, +511 / -32 (test 304, provider +105/-10, docs +50/-5, changeset 18, lint rule + explain + test +30/-16)", "files_changed": [".changeset/29434-mcp-stdio-credential-env.md", "content/docs/automation/connectors.mdx", "packages/connectors/connector-mcp/src/mcp-connector.ts", "packages/connectors/connector-mcp/src/mcp-provider.ts", "packages/connectors/connector-mcp/src/mcp-stdio-credential-env.test.ts", "packages/lint/src/lint-connector-credential-literals.test.ts", "packages/lint/src/lint-connector-credential-literals.ts", "packages/lint/src/rule-explanations.ts"], "review_face": "record-recognisers --template --paths (final list): without the changeset, declaration face TIER_DEFAULT; with it, the tool defers to the changeset's Clause-② yes (widening) line, which is the same declaration face", "deviations": ["The first WIP commit was made with core.hooksPath=/dev/null. It was undone at once (reset --soft) and re-committed with hooks, so no hook-skipped commit was pushed.", "The first typecheck round read a dist built with OS_SKIP_DTS=1 (TS7016). It was rebuilt with declarations and re-run green; that round's reading was not used."], "open_questions": [ { "question": "Should an include/toolsSnapshot refusal on a stdio instance that passed credentialEnv take the credential path (ConnectorCredentialRejectedError, stored credential marked rejected), as an http instance's does? A stdio server can also list only the tools its token may use.", "options": ["A: keep credentialSent http-only, as built (a stdio handshake proves nothing about the env credential; plain fault)", "B: count credentialEnv as sent for the include/pin refusal path only, never for credentialVerified"], "recommendation": "A for this card. B changes rejection bookkeeping, and a server that hides tools per token over stdio has not been measured." } ], "security_findings": "none", "out_of_scope_findings": ["carrier: seat 3 · skills/objectstack-automation/references/integration-recipes.md recipe 6 still says 'a stdio server's credentials ride transport.env'. This is now incomplete (credentialEnv route), not false. skills/** is Tier H governed, so it is not edited in this PR · noted, not filed"] }Stamped 2026-11-02T22:16Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67
Claim amendment · #29434 · 2026-11-02T22:24Z
Branch:
claude/project-thread-439xd1(unchanged)Clause-②: yes (widening)
Metadata-protocol: added
Why: the contract review on PR #29621 (head
f3ff3349, verdict FAIL on this point alone) is right, and it corrects the seat's own route ruling (6588385697, which saidnone).added.providerConfig.transport.credentialEnvis a key an author writes in a connector entry. That it is provider-owned (ADR-0023) explains why there is no spec hunk. It does not make the key unauthorable.providerConfig.includetoconnector-openapiwith no spec hunk and declaredadded.@objectstack/connector-mcptakesminor.@objectstack/lintchanges message text only and may staypatch.File surface also adds
.changeset/29341-connector-credential-literal.md. That changeset is still pending onmain, and its stdio bullet ("a stdio MCP server takes noauth") would ship in the same release as the opposite statement. Its stdio sentence is corrected on this PR, as a prose edit to another card's pending changeset (the DELIBERATE CORRECTION class). The route ruling stands otherwise.objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67
Landing record · #29434 · PR #29621
domain:serverseat 3 ·session_01ADHkmQJdgarUjPLNgFBY67· 2026-11-02T23:26Zgit ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-29621-*'git merge-base --is-ancestor dd03ee06 origin/maingit grep -n "credentialEnv" origin/main -- packages/connectors/connector-mcp/src/mcp-provider.tsdd03ee06.providerConfig.transport.credentialEnv.authreaches only the child's spawn environment, neverdef, a log or a message.credentialEnvis unchanged.connector-credential-literaladvisory now steers a stdio literal to the new route.Metadata-protocol: added.@objectstack/connector-mcptakesminorand@objectstack/linttakespatch.6590722710. The declaration-face re-review PASS is6590229589, after the first-round FAIL6590013700on the declaration line.Fixes #29434closed this card on merge.transport.env; a static credentialRef now reaches stdio throughtransport.credentialEnv(#29434) #29625 (governed path).credentialEnv) is connector-mcp: an mcp instance with a stdio transport accepts a staticauth, resolves its credentialRef, and then drops the value silently, so the declared credential reaches nothing #29607.pm:dispatchedand the assignee removed in the same round.