-
Notifications
You must be signed in to change notification settings - Fork 19
fix(mcpl): scrub host env from stdio MCPL children #175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| - **Operators of stdio MCPL servers:** children no longer inherit the whole | ||
| host environment. They get a small allowlist (`PATH`, `HOME`, locale/`LC_*`, | ||
| `TMPDIR`, TLS roots, …; see `CHILD_ENV_ALLOWLIST`) plus the server's own | ||
| `env`, so one server's credentials (provider API keys, other bots' tokens) | ||
| are no longer readable by another. A server that relied on an inherited | ||
| variable must declare it in its `env` (recipes can `${VAR}`-substitute), or | ||
| set `inheritEnv: true` to restore full inheritance. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /** | ||
| * Stdio MCPL children must not inherit the host's secrets. They get a small | ||
| * operating allowlist (PATH, HOME, LC_*, ...) plus their declared `env`; | ||
| * `inheritEnv: true` restores full inheritance. | ||
| */ | ||
| import { test } from 'node:test'; | ||
| import assert from 'node:assert/strict'; | ||
|
|
||
| import { buildChildEnv, StdioTransport } from '../src/mcpl/transport.js'; | ||
|
|
||
| const HOST_ENV = { | ||
| PATH: '/usr/bin:/bin', | ||
| HOME: '/home/resident', | ||
| LANG: 'en_US.UTF-8', | ||
| LC_CTYPE: 'UTF-8', | ||
| TMPDIR: '/tmp/x', | ||
| ANTHROPIC_AUTH_TOKEN: 'host-anthropic-secret', | ||
| DISCORD_TOKEN: 'host-discord-secret', | ||
| OPENAI_API_KEY: 'host-openai-secret', | ||
| }; | ||
|
|
||
| test('drops host secrets, keeps operating vars and LC_*', () => { | ||
| const env = buildChildEnv({}, HOST_ENV); | ||
| assert.deepEqual(env, { | ||
| PATH: '/usr/bin:/bin', | ||
| HOME: '/home/resident', | ||
| LANG: 'en_US.UTF-8', | ||
| LC_CTYPE: 'UTF-8', | ||
| TMPDIR: '/tmp/x', | ||
| }); | ||
| }); | ||
|
|
||
| test('declared env is passed through and wins over allowlisted host vars', () => { | ||
| const env = buildChildEnv( | ||
| { env: { DISCORD_TOKEN: 'declared-token', PATH: '/opt/bin' } }, | ||
| HOST_ENV, | ||
| ); | ||
| assert.equal(env.DISCORD_TOKEN, 'declared-token'); | ||
| assert.equal(env.PATH, '/opt/bin'); | ||
| assert.equal(env.HOME, '/home/resident'); | ||
| assert.equal(env.ANTHROPIC_AUTH_TOKEN, undefined); | ||
| assert.equal(env.OPENAI_API_KEY, undefined); | ||
| }); | ||
|
|
||
| test('inheritEnv: true passes the whole host env, declared env on top', () => { | ||
| const env = buildChildEnv({ inheritEnv: true, env: { EXTRA: '1' } }, HOST_ENV); | ||
| assert.equal(env.ANTHROPIC_AUTH_TOKEN, 'host-anthropic-secret'); | ||
| assert.equal(env.EXTRA, '1'); | ||
| }); | ||
|
|
||
| test('a real spawned stdio child sees only allowlist + declared env', async () => { | ||
| const secretKey = 'MCPL_ENV_TEST_HOST_SECRET'; | ||
| process.env[secretKey] = 'must-not-leak'; | ||
| try { | ||
| const transport = StdioTransport.spawn({ | ||
| id: 'env-probe', | ||
| command: process.execPath, | ||
| args: ['-e', 'console.log(JSON.stringify(process.env))'], | ||
| env: { DECLARED_VAR: 'declared-value' }, | ||
| }); | ||
| const line = await new Promise<string>((resolve, reject) => { | ||
| transport.once('line', resolve); | ||
| transport.once('error', reject); | ||
| }); | ||
| await transport.close(); | ||
|
Comment on lines
+61
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the spawned probe exits without printing a line, the transport emits Prompt To Fix With AIThis is a comment left during a code review.
Path: test/mcpl-child-env.test.ts
Line: 61-65
Comment:
**Probe exit leaves test waiting**
If the spawned probe exits without printing a line, the transport emits `close`, but this promise listens only for `line` and `error`. The test cannot report the exit as a useful failure or reach `transport.close()`; a child that stays running without output can stall the suite. Handle `close` and ensure cleanup on every outcome.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| const childEnv = JSON.parse(line) as Record<string, string>; | ||
| assert.equal(childEnv[secretKey], undefined); | ||
| assert.equal(childEnv.DECLARED_VAR, 'declared-value'); | ||
| if (process.env.PATH) assert.equal(childEnv.PATH, process.env.PATH); | ||
| if (process.env.HOME) assert.equal(childEnv.HOME, process.env.HOME); | ||
| } finally { | ||
| delete process.env[secretKey]; | ||
| } | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a Windows host enumerates variables as
Path,SystemRoot, orComSpec, this exact-case check drops them because the allowlist contains only uppercase spellings. Every default stdio child then starts without variables it previously inherited, so a server that launches tools by name or needsSystemRootcan fail. Match these names case-insensitively on Windows while keeping their original spelling.Prompt To Fix With AI