@yemi33/minions 0.1.1157 → 0.1.1159

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/CHANGELOG.md CHANGED
@@ -1,5 +1,115 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1159 (2026-04-18)
4
+
5
+ ### Features
6
+ - Header gate — Origin allowlist, Content-Type enforcement, security response headers (#1336)
7
+ - Header gate — Origin allowlist, Content-Type enforcement, security response headers (#1327)
8
+ - escapeHtml helper, hotspot XSS migration, regression gate (#1323)
9
+ - Prototype pollution guard in readBody (SEC-13) (#1300)
10
+ - redact ADO tokens and JWTs from engine/log.json writes (#1297)
11
+ - SEC-02 — replace curl shell-out in ado.js with adoFetch (#1296)
12
+ - validate project name and path on POST /api/projects/add (SEC-04, SEC-05) (#1298)
13
+ - seed realActivityMap at spawn time, stamp pid in live-output (#1200)
14
+
15
+ ### Fixes
16
+ - ado merge-commit mismatch preserves prior buildStatus (closes #1233) (#1326)
17
+ - scheduler substitutes {{date}} in title and description (#1275)
18
+ - prevent archived plans from re-triggering via orphaned .backup restore
19
+ - pinned context optimistic save (closes #1295) (#1316)
20
+ - re-check throttle state per-PR iteration to avoid stale fixThrottled
21
+ - preserve buildErrorLog through transient states, persist poll time (#1273)
22
+ - auto-fetch PR title on link-pr (closes #1283) (#1299)
23
+ - scheduler double-fire within same cron minute (#1277)
24
+ - gate auto-fix dispatch on throttle state to prevent stale-data spurious fixes
25
+ - preserve buildErrorLog through transient build states (#1232) (#1274)
26
+ - annotate fast-exit empty-output failures with diagnostic hint (#1276)
27
+ - invalidate PRD cache on pr-links.json change + guard aggregate PRs (#1220) (#1272)
28
+ - pass --add-dir for minions + ~/.claude to agents (#1271)
29
+ - preserve VERDICT marker by tail-slicing agent output (#1234) (#1270)
30
+ - PRD info cache staleness and aggregate PR bleed-through (#1222)
31
+ - avoid no-op work item writes
32
+ - resilient claude binary resolution + surface spawn errors
33
+ - resolve native claude.exe from npm wrapper on Windows
34
+ - cap temp-agent creation at maxConcurrent per tick (#1219)
35
+ - reassign pending items from unspawned temp agents to idle named agents (#1204) (#1212)
36
+
37
+ ### Other
38
+ - refactor: hoist fixThrottled before PR loop and drop underscore prefix
39
+ - docs(skill): add substitute-scheduler-template-vars (#1278)
40
+ - Clarify PR poll labels
41
+ - Prevent modal opens on text selection
42
+ - refactor: clarify settings page section structure and PR polling dependencies
43
+ - refactor: extract _probeClaudePackage helper, use shared.log for spawn errors
44
+ - Make work item descriptions scrollable
45
+ - Use PAT for publish merges
46
+ - test(queries): add unit tests for invalidateDispatchCache/getInbox/getAgentCharter (#1214)
47
+ - test(shared): add unit tests for truncateTextBytes/tailTextBytes/execSilent/trackReviewMetric/parseCanonicalPrId (#1215)
48
+ - Fix publish workflow merge
49
+ - chore: raise default meeting round timeout
50
+ - Harden prompt context handling
51
+ - Harden loop watch conversion
52
+ - Add watches sidebar activity badge
53
+ - test(cli): add unit tests for handleCommand, start, stop, kill, spawn (#1191)
54
+ - chore: untrack pipeline files — local config only
55
+ - restore: recover daily-arch-improvement and weekly-dead-code-cleanup pipelines
56
+ - Harden CC stream resilience
57
+
58
+ ## 0.1.1158 (2026-04-18)
59
+
60
+ ### Features
61
+ - Header gate — Origin allowlist, Content-Type enforcement, security response headers (#1336)
62
+ - Header gate — Origin allowlist, Content-Type enforcement, security response headers (#1327)
63
+ - escapeHtml helper, hotspot XSS migration, regression gate (#1323)
64
+ - Prototype pollution guard in readBody (SEC-13) (#1300)
65
+ - redact ADO tokens and JWTs from engine/log.json writes (#1297)
66
+ - SEC-02 — replace curl shell-out in ado.js with adoFetch (#1296)
67
+ - validate project name and path on POST /api/projects/add (SEC-04, SEC-05) (#1298)
68
+ - seed realActivityMap at spawn time, stamp pid in live-output (#1200)
69
+
70
+ ### Fixes
71
+ - ado merge-commit mismatch preserves prior buildStatus (closes #1233) (#1326)
72
+ - scheduler substitutes {{date}} in title and description (#1275)
73
+ - prevent archived plans from re-triggering via orphaned .backup restore
74
+ - pinned context optimistic save (closes #1295) (#1316)
75
+ - re-check throttle state per-PR iteration to avoid stale fixThrottled
76
+ - preserve buildErrorLog through transient states, persist poll time (#1273)
77
+ - auto-fetch PR title on link-pr (closes #1283) (#1299)
78
+ - scheduler double-fire within same cron minute (#1277)
79
+ - gate auto-fix dispatch on throttle state to prevent stale-data spurious fixes
80
+ - preserve buildErrorLog through transient build states (#1232) (#1274)
81
+ - annotate fast-exit empty-output failures with diagnostic hint (#1276)
82
+ - invalidate PRD cache on pr-links.json change + guard aggregate PRs (#1220) (#1272)
83
+ - pass --add-dir for minions + ~/.claude to agents (#1271)
84
+ - preserve VERDICT marker by tail-slicing agent output (#1234) (#1270)
85
+ - PRD info cache staleness and aggregate PR bleed-through (#1222)
86
+ - avoid no-op work item writes
87
+ - resilient claude binary resolution + surface spawn errors
88
+ - resolve native claude.exe from npm wrapper on Windows
89
+ - cap temp-agent creation at maxConcurrent per tick (#1219)
90
+ - reassign pending items from unspawned temp agents to idle named agents (#1204) (#1212)
91
+
92
+ ### Other
93
+ - refactor: hoist fixThrottled before PR loop and drop underscore prefix
94
+ - docs(skill): add substitute-scheduler-template-vars (#1278)
95
+ - Clarify PR poll labels
96
+ - Prevent modal opens on text selection
97
+ - refactor: clarify settings page section structure and PR polling dependencies
98
+ - refactor: extract _probeClaudePackage helper, use shared.log for spawn errors
99
+ - Make work item descriptions scrollable
100
+ - Use PAT for publish merges
101
+ - test(queries): add unit tests for invalidateDispatchCache/getInbox/getAgentCharter (#1214)
102
+ - test(shared): add unit tests for truncateTextBytes/tailTextBytes/execSilent/trackReviewMetric/parseCanonicalPrId (#1215)
103
+ - Fix publish workflow merge
104
+ - chore: raise default meeting round timeout
105
+ - Harden prompt context handling
106
+ - Harden loop watch conversion
107
+ - Add watches sidebar activity badge
108
+ - test(cli): add unit tests for handleCommand, start, stop, kill, spawn (#1191)
109
+ - chore: untrack pipeline files — local config only
110
+ - restore: recover daily-arch-improvement and weekly-dead-code-cleanup pipelines
111
+ - Harden CC stream resilience
112
+
3
113
  ## 0.1.1157 (2026-04-18)
4
114
 
5
115
  ### Features
package/dashboard.js CHANGED
@@ -1323,7 +1323,10 @@ function checkRateLimit(key, maxPerMinute) {
1323
1323
 
1324
1324
  function jsonReply(res, code, data, req) {
1325
1325
  res.setHeader('Content-Type', 'application/json');
1326
- res.setHeader('Access-Control-Allow-Origin', '*');
1326
+ // Access-Control-Allow-Origin is set ONCE by the server dispatcher prelude:
1327
+ // `*` for GET/HEAD (read-only), never for mutating responses (Origin gate
1328
+ // already blocked cross-origin POSTs). Setting it here would reopen the
1329
+ // cross-origin write path.
1327
1330
  res.statusCode = code;
1328
1331
  const json = JSON.stringify(data);
1329
1332
  const ae = req && req.headers && req.headers['accept-encoding'] || '';
@@ -1443,17 +1446,95 @@ function restartEngine() {
1443
1446
 
1444
1447
  // -- Server --
1445
1448
 
1449
+ // Mutating HTTP methods that require Origin and Content-Type gating.
1450
+ // GET/HEAD/OPTIONS are treated as read-only/preflight and bypass these checks.
1451
+ const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
1452
+
1446
1453
  const server = http.createServer(async (req, res) => {
1447
- // CORS preflight
1454
+ // ── Security headers (applied to every response) ──────────────────────────
1455
+ // Baseline CSP + clickjacking/mime/referrer protections. The dashboard HTML
1456
+ // entry-point later overrides CSP for its inline <script>/<style> blocks;
1457
+ // all API (JSON, text, SSE) responses inherit the strict CSP from here.
1458
+ const _secHeaders = shared.buildSecurityHeaders();
1459
+ for (const [k, v] of Object.entries(_secHeaders)) {
1460
+ try { res.setHeader(k, v); } catch { /* headers may already be sent in rare error paths */ }
1461
+ }
1462
+
1463
+ // ── Origin gate (mutating + OPTIONS preflight) ────────────────────────────
1464
+ // Defense-in-depth against CSRF / DNS-rebinding: even though the dashboard
1465
+ // binds to 127.0.0.1, a malicious page can coerce a user's browser to POST
1466
+ // to localhost. We reject any mutating request whose Origin (or Referer, if
1467
+ // Origin is absent) is not in the local allowlist. When both headers are
1468
+ // absent (curl, CLI tooling, Node http.request without Origin) we allow the
1469
+ // request to preserve existing local automation.
1470
+ const _rawOrigin = req.headers['origin'];
1471
+ const _rawReferer = req.headers['referer'];
1472
+ const _isMutating = MUTATING_METHODS.has(req.method);
1473
+
1474
+ function _originGateReject(reason) {
1475
+ console.warn(`[origin-gate] reject ${req.method} ${req.url} origin=${_rawOrigin || _rawReferer || '(none)'} reason=${reason}`);
1476
+ res.statusCode = 403;
1477
+ res.setHeader('Content-Type', 'application/json');
1478
+ res.end(JSON.stringify({ error: 'Origin not allowed' }));
1479
+ }
1480
+
1481
+ // CORS preflight — echo validated origin; reject disallowed origins outright.
1448
1482
  if (req.method === 'OPTIONS') {
1449
- res.setHeader('Access-Control-Allow-Origin', '*');
1450
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
1483
+ if (_rawOrigin) {
1484
+ if (!shared.isAllowedOrigin(_rawOrigin)) { _originGateReject('preflight-origin'); return; }
1485
+ res.setHeader('Access-Control-Allow-Origin', _rawOrigin);
1486
+ res.setHeader('Vary', 'Origin');
1487
+ }
1488
+ // Note: when Origin is absent (non-browser preflight), no ACAO is echoed —
1489
+ // that's fine, only browsers care about ACAO and they always send Origin.
1490
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
1451
1491
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
1452
1492
  res.statusCode = 204;
1453
1493
  res.end();
1454
1494
  return;
1455
1495
  }
1456
1496
 
1497
+ // Mutating requests: enforce Origin allowlist (Origin first, Referer fallback).
1498
+ if (_isMutating) {
1499
+ if (_rawOrigin) {
1500
+ if (!shared.isAllowedOrigin(_rawOrigin)) { _originGateReject('origin'); return; }
1501
+ } else if (_rawReferer) {
1502
+ if (!shared.isAllowedOrigin(_rawReferer)) { _originGateReject('referer'); return; }
1503
+ }
1504
+ // Neither Origin nor Referer present → legacy tooling (curl, Node.js) is allowed.
1505
+
1506
+ // Content-Type enforcement: require application/json on mutating requests.
1507
+ // readBody() always expects JSON. Rejecting anything other than
1508
+ // application/json closes the entire CSRF "simple request" loophole
1509
+ // (text/plain, application/x-www-form-urlencoded, multipart/form-data are
1510
+ // all cross-origin-postable without a preflight). DELETE with an empty
1511
+ // body (Content-Length: 0) is exempt — it carries no payload to parse.
1512
+ const ct = String(req.headers['content-type'] || '').toLowerCase();
1513
+ const clen = parseInt(req.headers['content-length'] || '0', 10) || 0;
1514
+ const hasBody = clen > 0 || req.headers['transfer-encoding'];
1515
+ if (hasBody && !ct.startsWith('application/json')) {
1516
+ res.statusCode = 415;
1517
+ res.setHeader('Content-Type', 'application/json');
1518
+ res.end(JSON.stringify({ error: 'Content-Type must be application/json' }));
1519
+ return;
1520
+ }
1521
+ // Empty-body mutating request (e.g. DELETE /api/cc-sessions/:id) with
1522
+ // no Content-Type is allowed — no body, no CSRF-friendly payload.
1523
+ if (!hasBody && ct && !ct.startsWith('application/json')) {
1524
+ res.statusCode = 415;
1525
+ res.setHeader('Content-Type', 'application/json');
1526
+ res.end(JSON.stringify({ error: 'Content-Type must be application/json' }));
1527
+ return;
1528
+ }
1529
+ }
1530
+
1531
+ // GET: permit cross-origin reads for external monitoring tools (curl, uptime
1532
+ // checks). Mutating responses deliberately do NOT set ACAO — cross-origin
1533
+ // browsers cannot use them anyway (Origin check blocks that path).
1534
+ if (req.method === 'GET' || req.method === 'HEAD') {
1535
+ res.setHeader('Access-Control-Allow-Origin', '*');
1536
+ }
1537
+
1457
1538
  // ── Route Handler Functions ───────────────────────────────────────────────
1458
1539
 
1459
1540
  async function handlePlansTriggerVerify(req, res) {
@@ -2289,6 +2370,20 @@ const server = http.createServer(async (req, res) => {
2289
2370
  }
2290
2371
 
2291
2372
  async function handleAgentLiveStream(req, res, match) {
2373
+ // SSE Origin gate — must run BEFORE res.writeHead(200, ...) so we can
2374
+ // still return a 403 with a normal status line. Once we upgrade to SSE
2375
+ // the client has no way to distinguish a rejection from a dropped stream.
2376
+ // GETs normally skip the Origin check, but SSE streams are long-lived and
2377
+ // warrant the same cross-origin guard as mutating endpoints.
2378
+ const _origin = req.headers['origin'];
2379
+ if (_origin && !shared.isAllowedOrigin(_origin)) {
2380
+ console.warn(`[sse-origin-gate] reject GET ${req.url} origin=${_origin}`);
2381
+ res.statusCode = 403;
2382
+ res.setHeader('Content-Type', 'application/json');
2383
+ res.end(JSON.stringify({ error: 'Origin not allowed' }));
2384
+ return;
2385
+ }
2386
+
2292
2387
  const agentId = match[1];
2293
2388
  const agentDir = path.join(MINIONS_DIR, 'agents', agentId);
2294
2389
  const liveLogPath = path.join(agentDir, 'live-output.log');
@@ -4021,6 +4116,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4021
4116
  }
4022
4117
 
4023
4118
  async function handleCommandCenterStream(req, res) {
4119
+ // SSE Origin gate (belt-and-suspenders: the top-level dispatcher has
4120
+ // already rejected disallowed origins on POST, but validate again here
4121
+ // before res.writeHead(200, text/event-stream) so any future refactor
4122
+ // that moves the route can't accidentally bypass the check).
4123
+ const _origin = req.headers['origin'];
4124
+ if (_origin && !shared.isAllowedOrigin(_origin)) {
4125
+ console.warn(`[sse-origin-gate] reject POST /api/command-center/stream origin=${_origin}`);
4126
+ res.statusCode = 403;
4127
+ res.setHeader('Content-Type', 'application/json');
4128
+ res.end(JSON.stringify({ error: 'Origin not allowed' }));
4129
+ return;
4130
+ }
4024
4131
  if (checkRateLimit('command-center', 10)) { res.statusCode = 429; res.end('Rate limited'); return; }
4025
4132
  let tabId;
4026
4133
  let _ccStreamAbort = null;
@@ -5366,6 +5473,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5366
5473
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
5367
5474
  res.setHeader('ETag', HTML_ETAG);
5368
5475
  res.setHeader('Cache-Control', 'no-cache'); // revalidate each time, but use 304 if unchanged
5476
+ // The SPA ships as a single self-contained HTML: inline <script> block,
5477
+ // inline <style> block, and many inline onclick= handlers on page
5478
+ // fragments. Strict `script-src 'self'` would break every button. We
5479
+ // relax the default CSP ONLY for the dashboard HTML entry-point — the
5480
+ // strict CSP still applies to all /api/* responses (verified by tests).
5481
+ // data: in img-src permits the inline SVG favicon (<link rel="icon" href="data:...">).
5482
+ res.setHeader(
5483
+ 'Content-Security-Policy',
5484
+ "default-src 'self'; " +
5485
+ "script-src 'self' 'unsafe-inline'; " +
5486
+ "style-src 'self' 'unsafe-inline'; " +
5487
+ "img-src 'self' data:; " +
5488
+ "connect-src 'self'"
5489
+ );
5369
5490
  if (req.headers['if-none-match'] === HTML_ETAG) {
5370
5491
  res.statusCode = 304;
5371
5492
  res.end();
package/engine/shared.js CHANGED
@@ -1131,6 +1131,56 @@ function sanitizeBranch(name) {
1131
1131
  return String(name).replace(/[^a-zA-Z0-9._\-\/]/g, '-').slice(0, 200);
1132
1132
  }
1133
1133
 
1134
+ // ── HTTP Origin Allowlist & Security Headers ─────────────────────────────────
1135
+ // Pure helpers used by dashboard.js to gate mutating requests against an
1136
+ // explicit allowlist of local origins and to attach uniform security response
1137
+ // headers. Extracted here so they're unit-testable without the HTTP server.
1138
+
1139
+ // Allowed origin (scheme + host) — port-agnostic. Dashboard always binds to
1140
+ // 127.0.0.1:7331 locally; browser tabs may arrive as localhost, 127.0.0.1, or
1141
+ // IPv6 [::1] depending on how the user opened the page.
1142
+ // WHATWG URL keeps IPv6 brackets in `hostname`, so we compare against the
1143
+ // bracketed form `[::1]`. We also accept the bare form `::1` defensively.
1144
+ const _ALLOWED_ORIGIN_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
1145
+ const _ALLOWED_ORIGIN_SCHEMES = new Set(['http:']);
1146
+
1147
+ /**
1148
+ * Returns true if the origin-like header value (either an `Origin` header value
1149
+ * such as `http://localhost:7331` or a full `Referer` URL) belongs to the local
1150
+ * dashboard allowlist. Port-agnostic. Returns false for null/undefined/empty,
1151
+ * the literal string `'null'` (sandboxed iframes, data: URIs), malformed URLs,
1152
+ * non-http schemes, and any host not in the allowlist.
1153
+ * @param {string|null|undefined} origin
1154
+ * @returns {boolean}
1155
+ */
1156
+ function isAllowedOrigin(origin) {
1157
+ if (!origin || typeof origin !== 'string') return false;
1158
+ const trimmed = origin.trim();
1159
+ if (!trimmed || trimmed === 'null') return false;
1160
+ let parsed;
1161
+ try { parsed = new URL(trimmed); } catch { return false; }
1162
+ if (!parsed.hostname) return false;
1163
+ if (!_ALLOWED_ORIGIN_SCHEMES.has(parsed.protocol)) return false;
1164
+ return _ALLOWED_ORIGIN_HOSTS.has(parsed.hostname);
1165
+ }
1166
+
1167
+ /**
1168
+ * Returns the baseline set of security response headers to apply on every HTTP
1169
+ * response from the dashboard. Values match OWASP defaults for a same-origin
1170
+ * SPA served from 127.0.0.1. The HTML entry-point response intentionally
1171
+ * overrides CSP to allow its inline `<script>` / `<style>` blocks; API (JSON,
1172
+ * SSE) responses inherit the strict CSP returned here.
1173
+ * @returns {{[key:string]: string}}
1174
+ */
1175
+ function buildSecurityHeaders() {
1176
+ return {
1177
+ 'Content-Security-Policy': "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
1178
+ 'X-Frame-Options': 'DENY',
1179
+ 'X-Content-Type-Options': 'nosniff',
1180
+ 'Referrer-Policy': 'same-origin',
1181
+ };
1182
+ }
1183
+
1134
1184
  // ── Project Name / Path Validation (SEC-04 / SEC-05) ─────────────────────────
1135
1185
  // Enforced at API boundaries (e.g. POST /api/projects/add). Callers that skip
1136
1186
  // these validators leak caller-controlled strings into worktree paths, config
@@ -1809,6 +1859,8 @@ module.exports = {
1809
1859
  getAdoOrgBase,
1810
1860
  sanitizePath,
1811
1861
  sanitizeBranch,
1862
+ isAllowedOrigin,
1863
+ buildSecurityHeaders,
1812
1864
  hasDangerousKey,
1813
1865
  validateProjectName,
1814
1866
  validateProjectPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1157",
3
+ "version": "0.1.1159",
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"