@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.
- package/docs/deprecated.json +14 -0
- package/docs/managed-spawn.md +3 -3
- package/engine/managed-spawn.js +112 -13
- package/engine/shared.js +29 -17
- package/package.json +1 -1
package/docs/deprecated.json
CHANGED
|
@@ -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",
|
package/docs/managed-spawn.md
CHANGED
|
@@ -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": { "
|
|
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
|
-
| `
|
|
223
|
-
| `
|
|
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
|
|
package/engine/managed-spawn.js
CHANGED
|
@@ -64,15 +64,78 @@ function _isOnAllowlist(name, allowlist) {
|
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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 (!
|
|
245
|
-
return { ok: false, reason: 'env-key-
|
|
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,
|
|
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:
|
|
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
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1543
|
-
|
|
1544
|
-
'
|
|
1545
|
-
'
|
|
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.
|
|
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"
|