@yemi33/minions 0.1.1976 → 0.1.1978

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "id": "managed-spawn-env-allowlist",
4
+ "removedAt": "2026-05-18",
5
+ "reason": "ENGINE_DEFAULTS.managedSpawn.envKeyAllowlist + envKeyAllowlistPrefixes removed; replaced by envKeyDenyPatterns + envKeyDenyOverrides. The allowlist shape required an engine PR for every new framework/project env prefix (W-mpbpa09c000rd513 tried per-project allowlist extension; user steered away — 'make sure that we are not hardcoding any env variables or being so rigid about it'). The denylist shape matches the actual credential-leakage threat model and lets plain project vars like CONSTELLATION_SERVER, DATABASE_URL, REDIS_HOST work with zero engine config while still blocking credential-shaped keys (AWS_*, *_TOKEN, *_SECRET, etc.). Per-project tightening is supported via project.managedSpawnExtraDenyPatterns (additive only, no per-project override list).",
6
+ "removedLocations": [
7
+ "engine/shared.js ENGINE_DEFAULTS.managedSpawn.envKeyAllowlist (15 keys)",
8
+ "engine/shared.js ENGINE_DEFAULTS.managedSpawn.envKeyAllowlistPrefixes (8 prefixes)",
9
+ "engine/managed-spawn.js _envKeyAllowed (rewritten to deny+override+shape model)",
10
+ "engine/managed-spawn.js buildManagedSpawnHint (env-key guidance rewritten)",
11
+ "PR #2624 (closed, superseded — added per-project allowlist union; replaced here by per-project deny tightening)",
12
+ "test/unit/managed-spawn-validator.test.js 4e/4f/11a (rewritten for denylist semantics)"
13
+ ],
14
+ "notes": "Already removed in this PR; entry exists to track the breaking shape change in the deprecation log. Delete entry after 3 days per /cleanup-deprecated."
15
+ },
2
16
  {
3
17
  "id": "config-poll-key-migration",
4
18
  "location": "engine/queries.js:123-162",
@@ -36,7 +36,7 @@ The sidecar lives at `<MINIONS_DIR>/agents/<agentId>/managed-spawn.json` and is
36
36
  "cmd": "bun", // must be on engine.managedSpawn.executableAllowlist
37
37
  "args": ["run", "dev"], // ≤64 entries
38
38
  "cwd": "D:/repos/constellation", // must be a real git worktree (requireGitWorkdir: true)
39
- "env": { "VITE_HOST": "127.0.0.1" }, // ≤32 keys; allowlist + prefix-allowlist enforced
39
+ "env": { "CONSTELLATION_SERVER": "http://localhost:3000" }, // ≤32 keys; POSIX-shape + denylist enforced
40
40
  "ports": [3001], // 1024-65535; ≤20 per spec; advisory only (engine doesn't bind)
41
41
  "ttl_minutes": 240, // ≤1440 (24h hard cap); defaults to 240 (4h)
42
42
  "attrs": { // opaque per-spec metadata, ≤2048 bytes serialized
@@ -219,8 +219,8 @@ All knobs live under `engine.managedSpawn` in `engine/shared.js:1500` (`ENGINE_D
219
219
  | `promptContextMaxBytes` | `2048` | Auto-injected `## Live managed processes` block cap. |
220
220
  | `requireGitWorkdir` | `true` | Reject specs whose `cwd` isn't a git worktree. |
221
221
  | `executableAllowlist` | `[node, bun, npm, …]` | Single global. Applies to `spec.cmd` AND `command` healthcheck `cmd`. |
222
- | `envKeyAllowlist` | `[NODE_ENV, PORT, …]` | Exact-match env keys. |
223
- | `envKeyAllowlistPrefixes` | `[VITE_, NEXT_, ]` | Prefix-match env keys. |
222
+ | `envKeyDenyPatterns` | `[^AWS_, ^AZURE_, _SECRET, _TOKEN, _API_KEY, …]` | Regex source strings, matched case-insensitively. Keys matching ANY pattern are rejected unless exact-listed in `envKeyDenyOverrides`. Threat model: credential leakage, not env-key enumeration — plain project vars (`CONSTELLATION_SERVER`, `DATABASE_URL`, …) pass with no config. |
223
+ | `envKeyDenyOverrides` | `[AWS_REGION, AWS_DEFAULT_REGION, AZURE_REGION, GCP_REGION, AWS_PROFILE]` | Exact-match exemptions for known-safe keys that would otherwise be caught by a broad prefix pattern. Case-sensitive. |
224
224
 
225
225
  ## Failure modes
226
226
 
@@ -64,15 +64,78 @@ function _isOnAllowlist(name, allowlist) {
64
64
  return false;
65
65
  }
66
66
 
67
- function _envKeyAllowed(key, limits) {
68
- if (typeof key !== 'string' || key.length === 0) return false;
69
- const allowlist = Array.isArray(limits.envKeyAllowlist) ? limits.envKeyAllowlist : [];
70
- if (allowlist.indexOf(key) >= 0) return true;
71
- const prefixes = Array.isArray(limits.envKeyAllowlistPrefixes) ? limits.envKeyAllowlistPrefixes : [];
72
- for (const p of prefixes) {
73
- if (typeof p === 'string' && p.length > 0 && key.indexOf(p) === 0) return true;
67
+ // Env-key shape guard. Bash/POSIX-style identifier: leading letter or
68
+ // underscore, followed by letters / digits / underscores. Rejects keys with
69
+ // shell metachars ('FOO;BAR', 'FOO BAR', '$FOO') regardless of deny rules —
70
+ // argv smuggling protection, separate from the credential-shape threat
71
+ // model below.
72
+ const _ENV_KEY_SHAPE_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
73
+
74
+ // Compile envKeyDenyPatterns once per validator invocation and cache. The
75
+ // returned object also carries the override Set for fast exact-match lookup.
76
+ // All patterns are compiled case-insensitively so '_secret' / '_SECRET' are
77
+ // equivalent; overrides remain case-sensitive (exact match).
78
+ function _compileDenyRules(limits, projectExtraPatterns) {
79
+ const globalPatterns = Array.isArray(limits.envKeyDenyPatterns) ? limits.envKeyDenyPatterns : [];
80
+ const extra = Array.isArray(projectExtraPatterns) ? projectExtraPatterns : [];
81
+ const overridesArr = Array.isArray(limits.envKeyDenyOverrides) ? limits.envKeyDenyOverrides : [];
82
+ const sources = [];
83
+ const compiled = [];
84
+ for (const src of globalPatterns.concat(extra)) {
85
+ if (typeof src !== 'string' || src.length === 0) continue;
86
+ let re;
87
+ try { re = new RegExp(src, 'i'); }
88
+ catch (_e) { continue; }
89
+ sources.push(src);
90
+ compiled.push(re);
74
91
  }
75
- return false;
92
+ const overrides = new Set();
93
+ for (const k of overridesArr) {
94
+ if (typeof k === 'string' && k.length > 0) overrides.add(k);
95
+ }
96
+ return { sources, compiled, overrides };
97
+ }
98
+
99
+ // Check an env key against compiled deny rules. Returns
100
+ // { allowed: true } when the key passes, or
101
+ // { allowed: false, reason: 'env-key-denied (matched pattern X)' }
102
+ // when blocked. Does NOT enforce the shape guard — callers do that
103
+ // separately so shape-failures get a distinct reason.
104
+ function _envKeyAllowed(key, denyRules) {
105
+ if (typeof key !== 'string' || key.length === 0) {
106
+ return { allowed: false, reason: 'env-key-empty' };
107
+ }
108
+ if (denyRules.overrides.has(key)) return { allowed: true };
109
+ for (let i = 0; i < denyRules.compiled.length; i++) {
110
+ if (denyRules.compiled[i].test(key)) {
111
+ return { allowed: false, reason: 'env-key-denied (matched pattern ' + denyRules.sources[i] + ')' };
112
+ }
113
+ }
114
+ return { allowed: true };
115
+ }
116
+
117
+ // Find the configured project whose `localPath` contains the spec's cwd.
118
+ // Returns the most-specific match (longest matching localPath) so nested
119
+ // project layouts (e.g. /repos/parent and /repos/parent/sub) prefer the
120
+ // inner one. Returns null when no project matches or when inputs are
121
+ // missing. Pure — does not touch disk.
122
+ function _resolveProjectForCwd(cwd, projects) {
123
+ if (typeof cwd !== 'string' || cwd.length === 0) return null;
124
+ if (!Array.isArray(projects) || projects.length === 0) return null;
125
+ let best = null;
126
+ let bestLen = -1;
127
+ for (const p of projects) {
128
+ if (!p || typeof p !== 'object') continue;
129
+ const lp = typeof p.localPath === 'string' ? p.localPath : '';
130
+ if (!lp) continue;
131
+ if (!shared.isPathInsideOrEqual(cwd, lp)) continue;
132
+ const len = path.resolve(lp).length;
133
+ if (len > bestLen) {
134
+ best = p;
135
+ bestLen = len;
136
+ }
137
+ }
138
+ return best;
76
139
  }
77
140
 
78
141
  // Extract the first executable-like token from a shell-style healthcheck cmd
@@ -229,7 +292,16 @@ function _validateSpec(spec, index, limits, opts) {
229
292
  }
230
293
  }
231
294
 
232
- // env (optional)
295
+ // env (optional). Validation has three independent layers:
296
+ // 1. Shape guard: key matches /^[A-Za-z_][A-Za-z0-9_]*$/ (argv-smuggling
297
+ // protection). Reason: env-key-invalid-shape.
298
+ // 2. Denylist: key does NOT match any credential-shaped pattern unless
299
+ // explicitly listed in envKeyDenyOverrides. Per-project tightening
300
+ // (project.managedSpawnExtraDenyPatterns) can add MORE patterns
301
+ // when spec.cwd resolves under that project's localPath — projects
302
+ // can only tighten, never loosen (asymmetric on purpose; no
303
+ // per-project override list).
304
+ // 3. Value sanity: string, ≤1000 chars.
233
305
  const envRaw = spec.env == null ? {} : spec.env;
234
306
  if (typeof envRaw !== 'object' || Array.isArray(envRaw)) {
235
307
  return { ok: false, reason: 'env-not-object' };
@@ -239,10 +311,22 @@ function _validateSpec(spec, index, limits, opts) {
239
311
  if (envKeys.length > maxEnv) {
240
312
  return { ok: false, reason: 'env-too-many (>' + maxEnv + ')' };
241
313
  }
314
+ const projectForSpec = _resolveProjectForCwd(
315
+ typeof spec.cwd === 'string' ? spec.cwd : '',
316
+ opts && Array.isArray(opts.projects) ? opts.projects : null,
317
+ );
318
+ const projectExtraPatterns = projectForSpec && Array.isArray(projectForSpec.managedSpawnExtraDenyPatterns)
319
+ ? projectForSpec.managedSpawnExtraDenyPatterns
320
+ : null;
321
+ const denyRules = _compileDenyRules(limits, projectExtraPatterns);
242
322
  const env = {};
243
323
  for (const k of envKeys) {
244
- if (!_envKeyAllowed(k, limits)) {
245
- return { ok: false, reason: 'env-key-not-on-allowlist (' + k + ')' };
324
+ if (!_ENV_KEY_SHAPE_RE.test(k)) {
325
+ return { ok: false, reason: 'env-key-invalid-shape (' + k + ')' };
326
+ }
327
+ const decision = _envKeyAllowed(k, denyRules);
328
+ if (!decision.allowed) {
329
+ return { ok: false, reason: decision.reason };
246
330
  }
247
331
  const v = envRaw[k];
248
332
  if (typeof v !== 'string') return { ok: false, reason: 'env-value-not-string (' + k + ')' };
@@ -323,10 +407,25 @@ function validateManagedSpawnRecord(parsed, opts) {
323
407
  return { ok: false, reason: 'specs-too-many (>' + maxSpecs + ')' };
324
408
  }
325
409
 
410
+ // Resolve `projects` for per-project env-deny tightening. Callers (tests,
411
+ // future engine wiring) may pre-supply `opts.projects` for determinism;
412
+ // when undefined we lazy-load from MINIONS_DIR/config.json so the engine
413
+ // close-handler doesn't have to change signature. A read failure or a
414
+ // config without `projects` falls back to `[]` — unchanged behavior, the
415
+ // global deny patterns remain the only source.
416
+ let projects;
417
+ if (Array.isArray(opts.projects)) {
418
+ projects = opts.projects;
419
+ } else {
420
+ try { projects = shared.getProjects(); }
421
+ catch (_e) { projects = []; }
422
+ }
423
+ const specOpts = Object.assign({}, opts, { projects: projects });
424
+
326
425
  const seen = new Set();
327
426
  const out = [];
328
427
  for (let i = 0; i < parsed.specs.length; i++) {
329
- const v = _validateSpec(parsed.specs[i], i, limits, opts);
428
+ const v = _validateSpec(parsed.specs[i], i, limits, specOpts);
330
429
  if (!v.ok) {
331
430
  // Preserve workdir-rejection prefix at the top level so the engine
332
431
  // close-handler gate can key off it the same way it does for
@@ -489,7 +588,7 @@ function buildManagedSpawnHint(opts) {
489
588
  '- Specs per file: ≤ ' + maxSpecs,
490
589
  '- Name: kebab-case, ≤ 64 chars, unique within file',
491
590
  '- Executable (`cmd` and any `command` healthcheck cmd): on the engine\'s allowlist (node, bun, npm, npx, python, docker, adb, gradle, mvn, pwsh, …)',
492
- '- Env keys: on the engine\'s allowlist or matching a known prefix (e.g. `VITE_`, `NEXT_`, `REACT_APP_`, `npm_config_`)',
591
+ '- Env keys: any well-formed POSIX identifier (`/^[A-Za-z_][A-Za-z0-9_]*$/`) is accepted EXCEPT keys matching credential-shaped deny patterns (`*_SECRET`, `*_TOKEN`, `*_API_KEY`, `*_PASSWORD`, `*_PRIVATE_KEY`, `*_CREDENTIALS`, `*_AUTH`, `*_PAT`, `AWS_*`, `AZURE_*`, `GCP_*`, `GH_TOKEN`, `GITHUB_TOKEN`, `OPENAI_*`, `ANTHROPIC_*`, `COPILOT_*`, `DOCKER_AUTH*`, `NPM_TOKEN`). Region/profile names (`AWS_REGION`, `AWS_PROFILE`, …) are exempt. If your service genuinely needs a credential-looking var, rename it (e.g. `DATABASE_URL` not `DB_API_KEY`) or stash the value in `attrs` and have the child read it from there. **The engine forwards env values verbatim — the deny shape is a tripwire, not a scrubber. Do not put real credentials in env even if the key passes.** Projects may tighten by adding patterns to `config.projects[N].managedSpawnExtraDenyPatterns`; projects cannot loosen.',
493
592
  '- Ports: 1024–65535, ≤ 20 per spec',
494
593
  '- TTL: ≤ ' + maxTtl + ' minutes (hard cap), defaults to ' + defaultTtl + ' if omitted',
495
594
  '- `attrs` serialized: ≤ 2048 bytes (opaque blob the engine surfaces to downstream agents)',
package/engine/shared.js CHANGED
@@ -1526,24 +1526,36 @@ const ENGINE_DEFAULTS = {
1526
1526
  'curl', 'wget',
1527
1527
  'git',
1528
1528
  ],
1529
- // Env-key allowlist (exact match). Tight by default so a managed spec
1530
- // can't leak credentials (AWS_*, AZURE_*, GH_TOKEN, etc.). Anything not
1531
- // here must match one of the allowed prefixes below.
1532
- envKeyAllowlist: [
1533
- 'NODE_ENV', 'PORT', 'HOST', 'PATH',
1534
- 'DEBUG', 'LOG_LEVEL',
1535
- 'HOME', 'USERPROFILE', 'TMPDIR', 'TEMP', 'TMP',
1536
- 'LANG', 'LC_ALL',
1537
- 'JAVA_HOME', 'ANDROID_HOME', 'ANDROID_SDK_ROOT',
1529
+ // Env-key denylist (regex source strings, matched case-insensitively).
1530
+ // The threat model is credential leakage, not env-key enumeration —
1531
+ // any key NOT matching one of these patterns is accepted, so project
1532
+ // vars like CONSTELLATION_SERVER / DATABASE_URL / REDIS_HOST work with
1533
+ // zero engine config. Patterns are compiled lazily by the validator
1534
+ // and tested in order; the first match wins for the reject reason, so
1535
+ // the more specific prefix patterns are listed before the broad
1536
+ // suffix patterns (a key like AWS_SECRET_ACCESS_KEY should report the
1537
+ // narrower `^AWS_` cause, not the generic `_SECRET` cause). Extend
1538
+ // with caution; broad patterns belong here, ecosystem allow
1539
+ // exemptions belong in `envKeyDenyOverrides`.
1540
+ envKeyDenyPatterns: [
1541
+ // Prefix patterns first (more specific cause for vendor keys).
1542
+ '^AWS_', '^AZURE_', '^GCP_', '^GOOGLE_APPLICATION_CREDENTIALS',
1543
+ '^GH_TOKEN', '^GITHUB_TOKEN',
1544
+ '^OPENAI_', '^ANTHROPIC_', '^COPILOT_',
1545
+ '^DOCKER_AUTH', '^NPM_TOKEN',
1546
+ // Suffix / substring patterns (generic credential shapes).
1547
+ '_SECRET', '_TOKEN', '_API_KEY', '_APIKEY',
1548
+ '_PASSWORD', '_PASSWD',
1549
+ '_PRIVATE_KEY', '_PRIVKEY',
1550
+ '_CREDENTIALS', '_AUTH', '_PAT',
1538
1551
  ],
1539
- // Env-key prefix allowlist. Standard ecosystem prefixes that frontends
1540
- // and tooling depend on (Vite, Next.js, CRA, npm scripts). Extend with
1541
- // caution; broad prefixes (`AWS_`, `AZURE_`) belong on a deny-list, not
1542
- // an allow-list.
1543
- envKeyAllowlistPrefixes: [
1544
- 'VITE_', 'NEXT_', 'REACT_APP_', 'NUXT_', 'GATSBY_',
1545
- 'npm_config_', 'NPM_CONFIG_',
1546
- 'MINIONS_',
1552
+ // Exact-match exemption list for keys that would otherwise be caught
1553
+ // by a broad deny pattern (region/profile names aren't secrets).
1554
+ // Match is case-sensitive the override key must equal the spec key
1555
+ // exactly.
1556
+ envKeyDenyOverrides: [
1557
+ 'AWS_REGION', 'AWS_DEFAULT_REGION', 'AZURE_REGION', 'GCP_REGION',
1558
+ 'AWS_PROFILE',
1547
1559
  ],
1548
1560
  },
1549
1561
  // Backward-compat: keep `engine.claude.*` field family deprecation tracker. Listed here so preflight
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1976",
3
+ "version": "0.1.1978",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"