mindforge-cc 11.9.5 → 11.9.6

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,5 +1,5 @@
1
1
  {
2
- "version": "11.9.5",
2
+ "version": "11.9.6",
3
3
  "environment": "development",
4
4
  "governance": {
5
5
  "drift_threshold": 0.75,
package/CHANGELOG.md CHANGED
@@ -1,5 +1,138 @@
1
1
  # Changelog
2
2
 
3
+ ## [11.9.6] — 2026-09-20 — The docs stop overselling what the code discloses about itself
4
+
5
+ Patch release. No new features — this is the release-readiness pass before pointing real
6
+ users at the project for the first time, and it found the same pattern one more time:
7
+ code that honestly labels its own simulated/dormant parts, sitting under docs that hadn't
8
+ caught up. Every fix below was hand-verified against the live source or a real command
9
+ run, not carried over from a prior audit's prose.
10
+
11
+ ### Fixed
12
+
13
+ **Two real, reproducible bugs**
14
+
15
+ - **`/mindforge:learn` crashed on every skill that scored high enough to register.**
16
+ `bin/skills-builder/learn-cli.js` called `skill-registrar.js`'s `register()` with two
17
+ positional string arguments (`skillPath`, `'project'`); `register()` destructures a
18
+ single options object, so every call threw inside it, silently swallowed by the CLI's
19
+ generic catch. A validly-generated `SKILL.md` never reached `MANIFEST.md`, and the CLI
20
+ reported a bare `❌ Error`. Fixed the call site to pass `{ skillName, skillPath, tier,
21
+ qualityScore, sourceType, source }`, matching `register()`'s real signature.
22
+
23
+ - **`bin/browser/browser-daemon.js` printed its own bearer token to stdout** (captured
24
+ verbatim into the persistent `.planning/browser-daemon.log` by `daemon-manager.js`),
25
+ and gated only `/evaluate` — `/navigate`, `/click`, `/type`, and `/screenshot` had no
26
+ auth check at all. The idle-timeout path also called `process.exit(0)` directly,
27
+ bypassing the token-file cleanup in `shutdown()`. Fixed all three: the startup log now
28
+ prints only the token's file path, the auth check runs once before dispatch and covers
29
+ every route but `/status`, and the idle path calls the real `shutdown()`.
30
+
31
+ **A test with a blind spot for the exact bug it exists to catch**
32
+
33
+ - `tests/sdk-exports.test.js` guards against any tracked file instructing
34
+ `require('@mindforge/sdk')` — the unpublished, wrong package name (`sdk/package.json`
35
+ publishes as `mindforge-sdk`). Its scan used `git ls-files -z '*.md' '*.ts'`, which
36
+ cannot match `.js` files — so it did not, and could not, catch the live occurrence in
37
+ `examples/sdk-integration/index.js`. Fixed both: the import, and the pathspec (now also
38
+ `*.js`/`*.mjs`/`*.cjs`, with `tests/` added to the same "records, not instructions"
39
+ allowlist as `changelogs/`, since this file's own `WRONG` string literal would otherwise
40
+ self-flag).
41
+
42
+ **A dashboard that rendered success while showing nothing**
43
+
44
+ - Three more panels in `bin/dashboard/frontend/app.js` read response fields their real
45
+ backing functions in `metrics-aggregator.js` have never produced — the same bug class
46
+ already fixed once for the avg-quality tile, left unaddressed here. `refreshMemory()`
47
+ read `data.graph`/`data.count`; the real shape is `{ entries, total }`. `refreshTeam()`
48
+ called `.map()` on the whole response object; the real shape is `{ active, conflicts }`
49
+ with `email`/`last_seen`/`current_task` fields, not `user`/`action`/`timestamp`.
50
+ `drawCharts()` read `state.costs`/`state.quality`, fields `/api/metrics` never returns;
51
+ the real per-session series is `state.sessions[].cost_usd` /
52
+ `state.sessions[].quality_score`. All three failed silently behind an empty `catch`, so
53
+ the panels looked idle rather than broken. Fixed to read the real shapes.
54
+
55
+ **Two shipped documents that stated the opposite of the code**
56
+
57
+ - `SECURITY.md` claimed `AUDIT.jsonl` "auto-archives beyond 5000 lines with gzip
58
+ compression" — that rotation mechanism was removed in an earlier release specifically
59
+ because truncating the file broke the hash chain (it orphans `previous_hash` pointers to
60
+ archived entries). The log grows unbounded by design; the doc now says so.
61
+ - `docs/security/SECURITY.md` had drifted into a second, independent copy of this policy
62
+ with a `5.x.x`/`4.x.x`/`< 4.0.0` support table — years behind the real `11.x` line the
63
+ root `/SECURITY.md` (the canonical file) documents. Replaced with a pointer to the root
64
+ file so this can't re-drift.
65
+
66
+ **A stale Homebrew formula**
67
+
68
+ - `Formula/mindforge.rb` was pinned to `11.9.3` (url, sha256, and the version-assertion
69
+ test) — two releases behind, so `brew install mindforge` installed an old build with
70
+ none of 11.9.4/11.9.5's fixes. Re-pinned to the real published `11.9.5` tarball with its
71
+ actual sha256 (fetched and hashed directly, not carried over). This is the same drift
72
+ class the project has hit twice before (11.9.2 shipped with this file and `Dockerfile`
73
+ four releases behind); `Formula/mindforge.rb` will lag one release again until
74
+ `node scripts/sync-version.js --fetch-sha` runs after this version publishes — expected,
75
+ not a regression.
76
+
77
+ ### Changed — a documentation honesty pass
78
+
79
+ The following describe MindForge's own PQAS (post-quantum crypto), ZTAI (Zero-Trust
80
+ Agentic Identity), and "Pillar"-numbered subsystems as live, unconditional security
81
+ guarantees. They are not: `bin/governance/quantum-crypto.js` and
82
+ `bin/governance/ztai-manager.js` self-label these SIMULATED and gate them off the live
83
+ trust path by default (`SECURITY_TIER_3_SIMULATED = true`), and `SwarmController` /
84
+ `PersonaFactory` / `WaveExecutor` are role names in markdown specs with no backing file —
85
+ a distinction this repo's own `.claude/CLAUDE.md` already draws, just not everywhere yet.
86
+ Rewrote each to match the same "measured, not asserted" tone already used in
87
+ `docs/faq.md` and `docs/troubleshooting.md` and the root README's *What is actually
88
+ enforced* section:
89
+
90
+ `docs/usp-features.md`, `CODEBASE-MAP.md`, `docs/architecture/README.md`,
91
+ `docs/CAPABILITIES-MANIFEST.md`, `docs/governance-guide.md`,
92
+ `docs/MIND-FORGE-REFERENCE-V6.md`, `docs/INTELLIGENCE-MESH.md`, `docs/PERSONAS.md`,
93
+ `docs/security/threat-model.md`, `docs/security/penetration-test-results.md` (the latter
94
+ two now banner-marked as scoped to an earlier, materially smaller predecessor system, not
95
+ a current assessment — no new pentest content was fabricated to replace them).
96
+
97
+ **Six reference docs (`docs/registry/*.md`) were stuck at v11.3.1** (six releases behind)
98
+ with command/skill/persona/subagent counts off by 2–9x against the live filesystem, and
99
+ listed 14+ slash commands with no backing file (`/mindforge:quantum-verify`,
100
+ `/mindforge:hindsight`, `/mindforge:harvest`, `/mindforge:self-heal`,
101
+ `/mindforge:swarm-execution`, `/mindforge:identity`, and others) — each individually
102
+ re-verified against `.claude/commands/mindforge/` before being removed or reworded.
103
+ Updated all six to the real, live-verified counts (221 commands, 232+123 skills, 217
104
+ personas, 164 subagents) and pointed each at `docs/commands-reference.md` as the
105
+ canonical source, so staleness here is lower-stakes going forward.
106
+
107
+ Also corrected: `docs/plugin-installation.md` and `docs/reference(s)/commands.md` (stale
108
+ hand-typed counts), `docs/References/{decimal-phase-calculation,git-integration,
109
+ git-planning-commit}.md` (wrong CLI path — real tool is `.agent/bin/mindforge-tools.cjs`,
110
+ not `.agent/mindforge/bin/...`), `docs/References/model-profile-resolution.md` (dead
111
+ `@`-include path), and a fabricated `"Claude 4.5 Opus"` model-name literal in two docs
112
+ (no such string exists anywhere in `bin/`) replaced with the real "highest-capability
113
+ tier configured" language. Deleted two orphaned scratch files that were never real
114
+ documentation: `docs/testing-current-version.md` (a pre-release scratch file hardcoding
115
+ a personal machine path) and `docs/commands-skills/DISCOVERED_SKILLS.md` (stale output
116
+ from an unrelated external tool referencing a directory that doesn't exist in this repo).
117
+
118
+ ### Changed — discoverability
119
+
120
+ - Root `README.md`: npm-version/downloads/license/Node-version badges, an "at a glance"
121
+ capability summary with real counts, and jump links to the existing sections.
122
+ - `package.json`: `description` rewritten from marketing language ("Sovereign Agentic
123
+ Intelligence Framework... Production-Hardened... (v11)") to concrete, keyword-bearing
124
+ text; added `ai-agents`, `llm-tools`, `developer-tools`, `mcp-server` to `keywords`.
125
+
126
+ ### Verified
127
+
128
+ - `npm test`: 137 passed, 0 failed, 3 env-dependent skips (`browser.test.js`,
129
+ `browser-daemon-auth-live.test.js`, `sre-integration.test.js` — all three require a
130
+ Chromium daemon/display or git worktree support this sandbox does not have). Verified
131
+ clean through the real pre-commit hook, not just a standalone run.
132
+ - `node scripts/sync-version.js`: 27 channels synced; `Formula/mindforge.rb` correctly
133
+ deferred (tarball doesn't exist yet); `plugins/mindforge/.claude-plugin/plugin.json` and
134
+ `plugins/mindforge/mcp/dist/index.js` rebuilt.
135
+
3
136
  ## [11.9.5] — 2026-08-22 — The release path can no longer strand itself, and the SDK ships
4
137
 
5
138
  Patch release, and the shortest one in a while. It exists because 11.9.4 published two
package/MINDFORGE.md CHANGED
@@ -1,9 +1,9 @@
1
- # MINDFORGE.md — Parameter Registry (v11.9.5)
1
+ # MINDFORGE.md — Parameter Registry (v11.9.6)
2
2
 
3
3
  ## 1. IDENTITY & VERSIONING
4
4
 
5
5
  [NAME] = MindForge
6
- [VERSION] = 11.9.5
6
+ [VERSION] = 11.9.6
7
7
  [STABLE] = true
8
8
  [MODE] = "Platform Sovereign"
9
9
  [REQUIRED_CORE_VERSION] = 11.9.1
package/README.md CHANGED
@@ -1,29 +1,39 @@
1
1
  # MindForge
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/mindforge-cc.svg)](https://www.npmjs.com/package/mindforge-cc)
4
+ [![npm downloads](https://img.shields.io/npm/dm/mindforge-cc.svg)](https://www.npmjs.com/package/mindforge-cc)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![Node >=18](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](package.json)
7
+
3
8
  **An agentic intelligence framework for Claude Code** — orchestrates multi-agent workflows with governance, memory, and autonomous execution. Production-hardened with true parallelism, streaming SDK, and zero-trust security. Install once, get structured AI-driven development with built-in quality gates.
4
9
 
10
+ **At a glance:** 221 slash commands · 355 skills (232 auto-triggered + 123 explicit) · 218 personas · 164 installable subagents · 35 pre-built multi-agent dynamic workflows · a tamper-evident audit hash-chain · cost-aware routing across Anthropic/OpenAI/Gemini/Bedrock/Ollama · a local-first knowledge graph on zero-native-dependency SQLite (sql.js) · a live Express+SSE dashboard. Ships as an npm package, a Claude Code plugin, and an MCP server.
11
+
12
+ **Jump to:** [Latest release](#latest-release) · [What is actually enforced](#what-is-actually-enforced) · [Install](#install) · [Quick start](#quick-start-new-project) · [Documentation](#documentation) · [Core workflow](#core-workflow) · [Dynamic workflows](#dynamic-workflow-library)
13
+
5
14
  ---
6
15
 
7
16
  ## Latest release
8
17
 
9
- **v11.9.5** (2026-08-22) — The release path can no longer strand itself, and the SDK ships.
10
- 11.9.4 published two packages and then failed on the third; because that step sat *before* the
11
- release page and the `stable` dist-tag move, its failure skipped both. Fixed two ways: the steps
12
- that finish a release now run ahead of any additive package publish, and a new offline preflight
13
- gate refuses to reach a publish that the registry will reject. Verified against a worktree at tag
14
- `v11.9.4` — the exact tree npm rejected — the gate exits 1 and names the file.
15
-
16
- **`mindforge-sdk` publishes for the first time since 11.8.0, and for the first time with
17
- provenance.** Everything fixed in it across 11.8.1–11.9.4 had reached nobody, including a
18
- `WebSocketEventStream` reconnect whose unhandled rejection **terminates the caller's process**.
19
-
20
- The previous release, **v11.9.4**, is where the hook gates started actually registering: 11.9.3
21
- shipped the code and then declined to run it on essentially every project. Measured against the
22
- published tarballs — 11.9.3: **11 hook scripts installed, 0 registered**; 11.9.4: **8 registered,
23
- 3 deny-class verified blocking**. That **behaviour change under a patch bump** still applies — the
24
- installer writes `.claude/settings.json` where it previously declined, merging append-only and
25
- backing up first. See the BREAKING section in [CHANGELOG.md](./CHANGELOG.md), or
26
- [RELEASENOTES.md](./RELEASENOTES.md) for human-readable notes.
18
+ **v11.9.6** (2026-09-20) — The docs stop overselling what the code discloses about itself.
19
+ The release-readiness pass before pointing real, external users at the project: fixed a
20
+ crash in `/mindforge:learn` (wrong argument shape into `skill-registrar.js`), a token-leak
21
+ and inconsistent auth in the browser daemon, three dashboard panels that silently rendered
22
+ nothing, a stale Homebrew formula, and a long-running pattern of docs describing
23
+ PQAS/ZTAI/"Pillar"-numbered subsystems as live security guarantees when the code that
24
+ implements them already self-labels them simulated and off-by-default. No new features.
25
+ See [RELEASENOTES.md](./RELEASENOTES.md) for the human-readable summary, or
26
+ [CHANGELOG.md](./CHANGELOG.md) for the complete, file-by-file list.
27
+
28
+ The previous release, **v11.9.5**, fixed a release pipeline that could strand itself
29
+ mid-publish and shipped `mindforge-sdk` for the first time since 11.8.0, with provenance.
30
+ **v11.9.4**, before that, is where the hook gates started actually registering: 11.9.3
31
+ shipped the code and then declined to run it on essentially every project. Measured against
32
+ the published tarballs — 11.9.3: **11 hook scripts installed, 0 registered**; 11.9.4:
33
+ **8 registered, 3 deny-class verified blocking**. That **behaviour change under a patch
34
+ bump** still applies — the installer writes `.claude/settings.json` where it previously
35
+ declined, merging append-only and backing up first. See the BREAKING section in
36
+ [CHANGELOG.md](./CHANGELOG.md).
27
37
 
28
38
  ---
29
39
 
@@ -156,7 +166,7 @@ Full verification walkthrough: [docs/quick-verify.md](docs/quick-verify.md).
156
166
  - **Audit events:** [docs/References/audit-events.md](docs/References/audit-events.md)
157
167
  - **Upgrade guide:** [docs/upgrade.md](docs/upgrade.md)
158
168
  - **Workflow atlas:** [docs/workflow-atlas.md](docs/workflow-atlas.md)
159
- - **Security:** [docs/security/SECURITY.md](docs/security/SECURITY.md) (MindForge never stores credentials in files)
169
+ - **Security:** [SECURITY.md](SECURITY.md) (credentials are read from env vars and never committed to the repository)
160
170
  - **Threat model:** [docs/security/threat-model.md](docs/security/threat-model.md)
161
171
  - **Architecture:** [docs/architecture/README.md](docs/architecture/README.md)
162
172
  - **Contributing:** [docs/contributing/CONTRIBUTING.md](docs/contributing/CONTRIBUTING.md)
package/RELEASENOTES.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Release Notes
2
2
 
3
+ ## v11.9.6 — 2026-09-20 — The docs stop overselling what the code discloses about itself
4
+
5
+ ### Why this release exists
6
+
7
+ This is the readiness pass before pointing real, external users at the project for the
8
+ first time. No new features — it fixes two reproducible bugs, three dashboard panels that
9
+ silently rendered nothing, one stale security policy and one stale Homebrew formula, and a
10
+ long-running pattern where docs described PQAS/ZTAI/"Pillar"-numbered subsystems as live
11
+ security guarantees when the code that implements them (`bin/governance/quantum-crypto.js`,
12
+ `bin/governance/ztai-manager.js`) already self-labels them simulated and off-by-default.
13
+
14
+ ### The user-visible part
15
+
16
+ **`/mindforge:learn` works again.** It crashed on every skill that scored well enough to
17
+ auto-register — `skill-registrar.js`'s `register()` was called with the wrong argument
18
+ shape, so the CLI printed a bare `❌ Error` and nothing ever reached `MANIFEST.md`.
19
+
20
+ **The browser daemon no longer leaks its own auth token, and now actually checks it.**
21
+ `/navigate`, `/click`, `/type`, and `/screenshot` had zero authentication before this —
22
+ only `/evaluate` did. The startup log used to print the raw token to stdout (captured into
23
+ a plaintext, non-gitignored log file); now it only prints where the token file lives.
24
+
25
+ **Three dashboard panels (Memory, Team, and the cost/quality charts) were reading response
26
+ fields the backing API has never produced**, so they rendered as permanently empty with no
27
+ error. They now read the real shapes.
28
+
29
+ **If you use Homebrew:** `brew install mindforge` was pinned two releases behind
30
+ (`11.9.3`) and is now current. It will lag one release again after this one ships — that's
31
+ expected; the formula can't point at a tarball that doesn't exist yet.
32
+
33
+ ### The documentation part
34
+
35
+ Six reference docs (`docs/registry/*.md`) were frozen at v11.3.1 with command/skill/
36
+ persona counts off by 2–9x and 14+ slash commands listed that don't exist. `usp-features.md`,
37
+ `CODEBASE-MAP.md`, `docs/architecture/README.md`, and several other pages described
38
+ post-quantum crypto and Zero-Trust Agentic Identity as unconditional, shipped security
39
+ rather than the explicitly-simulated, opt-in-gated features they are in the actual code.
40
+ All rewritten to match the same measured tone the README's *What is actually enforced*
41
+ section and `docs/faq.md`/`docs/troubleshooting.md` already used. Two orphaned scratch
42
+ files that were never real documentation were removed.
43
+
44
+ See [CHANGELOG.md](./CHANGELOG.md) for the complete, file-by-file list.
45
+
3
46
  ## v11.9.5 — 2026-08-22 — The release path can no longer strand itself, and the SDK ships
4
47
 
5
48
  ### Why this release exists
package/SECURITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Security Policy
2
2
 
3
- > **Current version:** 11.9.5 | **npm audit:** 0 vulnerabilities across root, sdk, mcp-server
3
+ > **Current version:** 11.9.6 | **npm audit:** 0 vulnerabilities across root, sdk, mcp-server
4
4
 
5
5
  ## Supported Versions
6
6
 
@@ -70,7 +70,7 @@ We follow responsible disclosure practices. We will credit reporters in the rele
70
70
  hash of the previous entry. Not a Merkle tree: there is no hash tree and no inclusion proof, so
71
71
  "Merkle" was the wrong word for it. What it detects, and does not, is measured below.
72
72
  - **AuditWriter with buffered writes** — Atomic append operations prevent partial writes from corrupting the log.
73
- - **Log rotation with archival** — AUDIT.jsonl auto-archives beyond 5000 lines with gzip compression, preventing unbounded disk growth.
73
+ - **Unbounded audit log, by design** — A prior AUDIT.jsonl rotation/archival mechanism (5000-line threshold, gzip) was removed: truncating the file broke the hash chain by orphaning `previous_hash` pointers to archived entries. AUDIT.jsonl now grows without bound; chain-aware compaction is a tracked future improvement, not yet shipped.
74
74
  - **npm provenance** — Published packages include SLSA Build Level 2 attestation via `--provenance`, proving the package was built from the stated source commit in CI.
75
75
 
76
76
  ### Input Validation & Injection Prevention
@@ -5,37 +5,33 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const http = require('http');
9
- const crypto = require('crypto');
8
+ const http = require('http');
9
+ const crypto = require('crypto');
10
10
  const playwright = require('playwright-core');
11
- const fs = require('fs');
12
- const path = require('path');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { requiresAuth, isAuthValid } = require('./daemon-auth');
13
14
 
14
- const PORT = process.env.BROWSER_PORT || 7338;
15
- const HEADLESS = process.env.BROWSER_HEADLESS !== 'false';
16
- const TIMEOUT = (parseInt(process.env.BROWSER_IDLE_TIMEOUT_MINUTES) || 30) * 60 * 1000;
15
+ const PORT = process.env.BROWSER_PORT || 7338;
16
+ const HEADLESS = process.env.BROWSER_HEADLESS !== 'false';
17
+ const TIMEOUT =
18
+ (parseInt(process.env.BROWSER_IDLE_TIMEOUT_MINUTES) || 30) * 60 * 1000;
17
19
 
18
20
  // ── Bearer token authentication ──────────────────────────────────────────────
19
21
  const DAEMON_TOKEN = crypto.randomBytes(32).toString('hex');
20
- const DAEMON_TOKEN_FILE = path.join(process.cwd(), '.mindforge', '.browser-daemon-token');
22
+ const DAEMON_TOKEN_FILE = path.join(
23
+ process.cwd(),
24
+ '.mindforge',
25
+ '.browser-daemon-token',
26
+ );
21
27
 
22
28
  // Write token to file with restrictive permissions (owner-only read/write)
23
29
  fs.mkdirSync(path.dirname(DAEMON_TOKEN_FILE), { recursive: true });
24
30
  fs.writeFileSync(DAEMON_TOKEN_FILE, DAEMON_TOKEN, { mode: 0o600 });
25
31
 
26
- /**
27
- * Validate bearer token from Authorization header.
28
- * Returns true if valid, false otherwise.
29
- */
30
- function isAuthValid(req) {
31
- const authHeader = req.headers.authorization;
32
- if (!authHeader || !authHeader.startsWith('Bearer ')) return false;
33
- const provided = authHeader.slice(7);
34
- if (provided.length !== DAEMON_TOKEN.length) return false;
35
- return crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(DAEMON_TOKEN));
36
- }
37
-
38
- let browser, lastActionAt = Date.now(), isLaunching = false;
32
+ let browser,
33
+ lastActionAt = Date.now(),
34
+ isLaunching = false;
39
35
  const sessions = new Map(); // name -> { context, page }
40
36
 
41
37
  async function init() {
@@ -44,7 +40,7 @@ async function init() {
44
40
  try {
45
41
  browser = await playwright.chromium.launch({
46
42
  headless: HEADLESS,
47
- args: ['--no-sandbox', '--disable-setuid-sandbox', '--no-first-run']
43
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--no-first-run'],
48
44
  });
49
45
  setInterval(checkIdle, 60000);
50
46
  } finally {
@@ -55,14 +51,14 @@ async function init() {
55
51
  function checkIdle() {
56
52
  if (Date.now() - lastActionAt > TIMEOUT) {
57
53
  console.log('[daemon] Idle timeout reached. Shutting down.');
58
- process.exit(0);
54
+ shutdown();
59
55
  }
60
56
  }
61
57
 
62
58
  async function getOrCreateSession(name = 'default') {
63
59
  if (sessions.has(name)) return sessions.get(name);
64
60
  const context = await browser.newContext();
65
- const page = await context.newPage();
61
+ const page = await context.newPage();
66
62
  const s = { context, page };
67
63
  sessions.set(name, s);
68
64
  return s;
@@ -77,33 +73,64 @@ const server = http.createServer(async (req, res) => {
77
73
 
78
74
  // Only allow localhost
79
75
  const remote = req.socket.remoteAddress;
80
- if (remote !== '127.0.0.1' && remote !== '::1' && remote !== '::ffff:127.0.0.1') {
76
+ if (
77
+ remote !== '127.0.0.1' &&
78
+ remote !== '::1' &&
79
+ remote !== '::ffff:127.0.0.1'
80
+ ) {
81
81
  return send({ error: 'Forbidden: Localhost only' }, 403);
82
82
  }
83
83
 
84
84
  let body = '';
85
- req.on('data', chunk => body += chunk);
85
+ req.on('data', (chunk) => (body += chunk));
86
86
  req.on('end', async () => {
87
87
  try {
88
- const { url, session: sessionName, selector, text, script, type, expected_text, name } = body ? JSON.parse(body) : {};
88
+ if (
89
+ requiresAuth(req.url) &&
90
+ !isAuthValid(req.headers.authorization, DAEMON_TOKEN)
91
+ ) {
92
+ return send(
93
+ {
94
+ error:
95
+ 'Authentication required. Use the token written to the daemon token file at startup.',
96
+ },
97
+ 401,
98
+ );
99
+ }
100
+ const {
101
+ url,
102
+ session: sessionName,
103
+ selector,
104
+ text,
105
+ script,
106
+ type,
107
+ expected_text,
108
+ name,
109
+ } = body ? JSON.parse(body) : {};
89
110
  const { page, context } = await getOrCreateSession(sessionName);
90
111
 
91
112
  if (req.url === '/status' && req.method === 'GET') {
92
- return send({ alive: true, sessions: Array.from(sessions.keys()), uptime: process.uptime() });
113
+ return send({
114
+ alive: true,
115
+ sessions: Array.from(sessions.keys()),
116
+ uptime: process.uptime(),
117
+ });
93
118
  }
94
119
 
95
120
  if (req.url === '/navigate' && req.method === 'POST') {
96
121
  const start = Date.now();
97
122
  const r = await page.goto(url, { waitUntil: 'load', timeout: 30000 });
98
- return send({
99
- success: true,
100
- status_code: r ? r.status() : 200,
101
- load_time_ms: Date.now() - start
123
+ return send({
124
+ success: true,
125
+ status_code: r ? r.status() : 200,
126
+ load_time_ms: Date.now() - start,
102
127
  });
103
128
  }
104
129
 
105
130
  if (req.url === '/click' && req.method === 'POST') {
106
- const target = selector ? page.locator(selector) : page.getByText(text, { exact: false });
131
+ const target = selector
132
+ ? page.locator(selector)
133
+ : page.getByText(text, { exact: false });
107
134
  await target.first().click({ timeout: 5000 });
108
135
  return send({ success: true, element_found: true });
109
136
  }
@@ -119,9 +146,6 @@ const server = http.createServer(async (req, res) => {
119
146
  }
120
147
 
121
148
  if (req.url === '/evaluate' && req.method === 'POST') {
122
- if (!isAuthValid(req)) {
123
- return send({ error: 'Authentication required. Use the token printed at daemon startup.' }, 401);
124
- }
125
149
  const result = await page.evaluate(script);
126
150
  return send({ success: true, result });
127
151
  }
@@ -131,11 +155,20 @@ const server = http.createServer(async (req, res) => {
131
155
  const loc = page.locator(selector).first();
132
156
  const visible = await loc.isVisible();
133
157
  const actual = visible ? await loc.innerText() : '';
134
- const passed = visible && (!expected_text || actual.includes(expected_text));
158
+ const passed =
159
+ visible && (!expected_text || actual.includes(expected_text));
135
160
  return send({ passed, actual_text: actual });
136
161
  }
137
- if (type === 'url') return send({ passed: page.url().includes(expected_text), actual_url: page.url() });
138
- if (type === 'title') return send({ passed: (await page.title()).includes(expected_text), actual_title: await page.title() });
162
+ if (type === 'url')
163
+ return send({
164
+ passed: page.url().includes(expected_text),
165
+ actual_url: page.url(),
166
+ });
167
+ if (type === 'title')
168
+ return send({
169
+ passed: (await page.title()).includes(expected_text),
170
+ actual_title: await page.title(),
171
+ });
139
172
  if (type === 'no_console_errors') return send({ passed: true }); // simplified
140
173
  }
141
174
 
@@ -146,16 +179,19 @@ const server = http.createServer(async (req, res) => {
146
179
  });
147
180
  });
148
181
 
149
- init().then(() => {
150
- server.listen(PORT, '127.0.0.1', () => {
151
- console.log(`[BrowserDaemon] Listening on port ${PORT}`);
152
- console.log(`[BrowserDaemon] Auth token: ${DAEMON_TOKEN}`);
153
- console.log(`[BrowserDaemon] Token file: ${DAEMON_TOKEN_FILE}`);
182
+ init()
183
+ .then(() => {
184
+ server.listen(PORT, '127.0.0.1', () => {
185
+ console.log(`[BrowserDaemon] Listening on port ${PORT}`);
186
+ console.log(
187
+ `[BrowserDaemon] Auth token written to: ${DAEMON_TOKEN_FILE}`,
188
+ );
189
+ });
190
+ })
191
+ .catch((err) => {
192
+ console.error('[daemon] Initialization failed:', err);
193
+ process.exit(1);
154
194
  });
155
- }).catch(err => {
156
- console.error('[daemon] Initialization failed:', err);
157
- process.exit(1);
158
- });
159
195
 
160
196
  async function shutdown() {
161
197
  console.log('[daemon] Shutting down gracefully...');
@@ -0,0 +1,36 @@
1
+ /**
2
+ * MindForge v2 — Browser Daemon Auth
3
+ * Pure bearer-token validation extracted from browser-daemon.js so it can be
4
+ * unit-tested without booting Chromium. See ADR-024 / ADR-026.
5
+ */
6
+ 'use strict';
7
+
8
+ const crypto = require('crypto');
9
+
10
+ /**
11
+ * Endpoints that MUST present a valid bearer token. /status is intentionally
12
+ * excluded: it is the unauthenticated liveness probe daemon-manager.js#isRunning
13
+ * relies on, it leaks only session names + uptime, and it is already covered by
14
+ * ADR-024's localhost bind + remoteAddress check.
15
+ */
16
+ const AUTH_REQUIRED_PATHS = new Set([
17
+ '/navigate',
18
+ '/click',
19
+ '/type',
20
+ '/screenshot',
21
+ '/evaluate',
22
+ '/assert',
23
+ ]);
24
+
25
+ function requiresAuth(urlPath) {
26
+ return AUTH_REQUIRED_PATHS.has(urlPath);
27
+ }
28
+
29
+ function isAuthValid(authHeader, token) {
30
+ if (!authHeader || !authHeader.startsWith('Bearer ')) return false;
31
+ const provided = authHeader.slice(7);
32
+ if (provided.length !== token.length) return false;
33
+ return crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(token));
34
+ }
35
+
36
+ module.exports = { AUTH_REQUIRED_PATHS, requiresAuth, isAuthValid };