Skip to content

Commit bb17ca3

Browse files
authored
Optional knob to waive npm release-age soak for the zero-dependency commy-mcp bundle (#29)
## Problem An operator who runs npm's [`min-release-age`](https://docs.npmjs.com/cli/using-npm/config#min-release-age) supply-chain soak can't launch a freshly-published `@codeforbreakfast/commy-mcp` release. npm compiles `min-release-age` into a global `before=` publish-date cutoff over the whole install tree, with **no per-package or per-scope exclude**, so every commy-mcp version younger than the window is filtered → `ENOVERSIONS: No versions available` → the MCP server never launches. `min-release-age` exists to hold back a freshly-published release so a compromised one can be caught before you auto-pull it — it guards you against the **publisher's own not-yet-vetted code**. Waiving it is therefore a deliberate trust decision about the publisher, not a free pass. ## Change Expose an optional, **off-by-default** operator knob, `COMMY_NPM_MIN_RELEASE_AGE`, threaded into the launcher env as `npm_config_min_release_age` (scoped to commy's own `npx` invocation). Set it to `0` to trust the commy publisher and run a fresh release immediately; leave it unset to keep your own npm setting fully in force. The published package is a **zero-dependency bundle** — `scripts/assemble-npm-package.ts` inlines every dependency into a single `server.js`, and the generated manifest has no `dependencies` field. So the waiver is cleanly scoped to exactly this one publisher: no transitive dependency tree rides along with the decision. The safety property — verified against npm 11.12.1 — is that an unset knob never weakens the operator's own configuration: an unset/empty env var leaves a consumer's `.npmrc min-release-age` untouched (npm ignores an empty env var rather than overriding the file), and only an explicit `0` waives the soak. The launcher test pins this by asserting the userConfig is optional with no default. ## Surface - `clients/claude-code/.claude-plugin/plugin.json` — optional `COMMY_NPM_MIN_RELEASE_AGE` userConfig declaration - `clients/claude-code/.mcp.json` — thread it as `npm_config_min_release_age` - `clients/claude-code/launcher.test.ts` — pin the threaded knob and its off-by-default contract - `docs/self-hosting.md` — document the knob and when an operator needs it
1 parent 9b9b783 commit bb17ca3

4 files changed

Lines changed: 49 additions & 2 deletions

File tree

‎clients/claude-code/.claude-plugin/plugin.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@
3838
"title": "Boot-time catch-up window (seconds)",
3939
"description": "How far back to fetch recent messages across the boot-time subscribe set on a persistent-bot restart (comms-3wl). Default 14400 (4 hours). Applies to channel: and thread: narrows only; the mentions: narrow uses a separate cursor-bounded mechanism. Set to 0 to disable boot-time channel/thread catch-up.",
4040
"required": false
41+
},
42+
"COMMY_NPM_MIN_RELEASE_AGE": {
43+
"type": "string",
44+
"title": "npm release-age soak override for this package",
45+
"description": "Operator override for npm's min-release-age supply-chain soak, scoped to commy's own `npx @codeforbreakfast/commy-mcp` launch (threaded as npm_config_min_release_age). min-release-age soaks a freshly-published release so a compromised one can be caught before you auto-pull it — it protects you from the publisher's own not-yet-vetted code, which the soak would otherwise block with ENOVERSIONS until it ages in. Setting this to 0 waives that soak for commy: a deliberate decision to trust the commy publisher and run a new release immediately (the publisher's own posture). Because commy ships as a single zero-dependency bundle, the waiver is scoped to exactly this one publisher — no transitive dependency tree rides along. Leave UNSET to keep your own npm min-release-age fully in force; an unset value never weakens it.",
46+
"required": false
4147
}
4248
}
4349
}

‎clients/claude-code/.mcp.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"ZULIP_MINTER_EMAIL": "${user_config.ZULIP_MINTER_EMAIL}",
1010
"ZULIP_MINTER_API_KEY": "${user_config.ZULIP_MINTER_API_KEY}",
1111
"COMMY_SUBSCRIBE": "${user_config.COMMY_SUBSCRIBE}",
12-
"COMMY_CATCHUP_WINDOW_SECONDS": "${user_config.COMMY_CATCHUP_WINDOW_SECONDS}"
12+
"COMMY_CATCHUP_WINDOW_SECONDS": "${user_config.COMMY_CATCHUP_WINDOW_SECONDS}",
13+
"npm_config_min_release_age": "${user_config.COMMY_NPM_MIN_RELEASE_AGE}"
1314
}
1415
}
1516
}

‎clients/claude-code/launcher.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from 'bun:test'
22

3+
import pluginManifest from './.claude-plugin/plugin.json'
34
import mcpConfig from './.mcp.json'
45

56
/**
@@ -56,5 +57,34 @@ test('the realm credentials and subscription env are still threaded through', ()
5657
'ZULIP_MINTER_API_KEY',
5758
'COMMY_SUBSCRIBE',
5859
'COMMY_CATCHUP_WINDOW_SECONDS',
60+
'npm_config_min_release_age',
5961
])
6062
})
63+
64+
/**
65+
* npm's `min-release-age` soaks a freshly-published release so a compromised
66+
* one can be caught before a consumer auto-pulls it — it guards the publisher's
67+
* own not-yet-vetted code. Waiving it is therefore a trust decision about the
68+
* publisher, which the publisher itself can safely make (no detection window is
69+
* needed for code you authored and released). The launcher exposes an optional
70+
* knob for that: COMMY_NPM_MIN_RELEASE_AGE=0 waives the soak for commy;
71+
* everyone else leaves it unset. commy ships as a ZERO-dependency bundle
72+
* (scripts/assemble-npm-package.ts inlines every dep into one server.js), so the
73+
* waiver is scoped to exactly this one publisher — no transitive tree rides
74+
* along — and the consumer otherwise can't install a fresh release until it ages
75+
* past their window (ENOVERSIONS).
76+
*
77+
* The safety property — proven against npm 11.12.1 — is that an UNSET knob must
78+
* not weaken the consumer's own setting: an empty/absent env var leaves the
79+
* consumer's `.npmrc min-release-age` untouched (npm ignores an empty env var
80+
* rather than overriding the file), and only an explicit `0` waives the soak.
81+
* That hinges on the userConfig being OPTIONAL with no default, so an
82+
* unconfigured consumer threads nothing.
83+
*/
84+
test('the npm release-age knob is threaded from an optional userConfig, off by default', () => {
85+
expect(commyServer.env.npm_config_min_release_age).toBe(
86+
`\${user_config.COMMY_NPM_MIN_RELEASE_AGE}`,
87+
)
88+
expect(pluginManifest.userConfig.COMMY_NPM_MIN_RELEASE_AGE.required).toBe(false)
89+
expect(pluginManifest.userConfig.COMMY_NPM_MIN_RELEASE_AGE).not.toHaveProperty('default')
90+
})

‎docs/self-hosting.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ no-op-ish values when unset):
3030
| `COMMY_PROJECT` | no | Project slug used for channel naming and the concierge's project subscriptions. When unset it is derived per-session from the calling cwd (git remote / git root). |
3131
| `COMMY_SUBSCRIBE` | no | Comma-separated auto-subscribe tokens applied at boot: `channel:<name>`, `thread:<channel>/<thread>`, `new-topics:<channel>`, `mentions`. Blank means no auto-subscription. |
3232
| `COMMY_CATCHUP_WINDOW_SECONDS` | no | How far back to fetch recent messages across the boot-time subscribe set on a persistent restart. Default `14400` (4 hours); `0` disables. |
33+
| `COMMY_NPM_MIN_RELEASE_AGE` | no | Operator override for npm's `min-release-age` supply-chain soak, scoped to commy's own `npx` launch. Set to `0` to run a freshly-published release that your global soak would otherwise block with `ENOVERSIONS`. Leave **unset** to fully respect your own npm setting. See [Running outside Claude Code](#running-outside-claude-code). |
3334

3435
The live-test suite additionally needs a channel to exercise against:
3536

@@ -58,7 +59,16 @@ npx -y @codeforbreakfast/commy-mcp
5859
`server.js` with every dependency inlined — and runs it under node; there is no
5960
install step and nothing to stage. (The package carries the `@codeforbreakfast`
6061
scope because `@commy` is taken on npm; the substrate is otherwise `commy`
61-
throughout.) Working from a source checkout instead, the dev toolchain runs the
62+
throughout.) If you run npm's `min-release-age` supply-chain soak — which holds
63+
back a freshly-published release so a compromised one can be caught before you
64+
auto-pull it — a commy release younger than your window fails to resolve
65+
(`ENOVERSIONS`) until it ages in. That soak guards you against the publisher's
66+
own not-yet-vetted code, so waiving it is a decision to trust the commy
67+
publisher and run a new release immediately: set `COMMY_NPM_MIN_RELEASE_AGE=0`
68+
(which the launcher threads as `npm_config_min_release_age`). Because the bundle
69+
has **no dependencies**, that waiver is scoped to exactly this one package — no
70+
transitive dependency tree rides along — and an unset value never weakens your
71+
own setting. Working from a source checkout instead, the dev toolchain runs the
6272
TypeScript entry point directly under [Bun][bun] with `bun packages/mcp/server.ts`
6373
— bun is the development runtime, never a consumer prerequisite. `nix` is **not**
6474
a runtime dependency either; the flake's dev shell stays supported for those who

0 commit comments

Comments
 (0)