mindforge-cc 11.9.5 → 11.9.7

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.7",
3
3
  "environment": "development",
4
4
  "governance": {
5
5
  "drift_threshold": 0.75,
package/CHANGELOG.md CHANGED
@@ -1,5 +1,172 @@
1
1
  # Changelog
2
2
 
3
+ ## [11.9.7] — 2026-09-20 — The install banner stops contradicting itself
4
+
5
+ Patch release. Found by actually running the documented install command
6
+ (`npx mindforge-cc@latest --claude --local`) in a clean project instead of stopping at
7
+ `--version` — the check that should have caught the v11.9.6 regressions below before they
8
+ shipped.
9
+
10
+ ### Fixed
11
+
12
+ **Two real bugs in the install banner itself, both live in every v11.9.6 install**
13
+
14
+ - **Version self-contradiction.** The top banner (`bin/wizard/theme.js`'s
15
+ `printBrandManifest()`) hardcoded `SOVEREIGN INTELLIGENCE v8.1.1`, while the install
16
+ activation line two screens later (`bin/installer-core.js`) prints `v8.2.0` for the same
17
+ subsystem — one command, two version numbers. Standardized on `v8.2.0`.
18
+ - **False "Enabled" claim.** The same top banner said `PQAS & Proactive Homing Enabled`
19
+ unconditionally, directly contradicting the accurate disclosure a few lines later in the
20
+ same run: "available in simulated/experimental mode (inactive by default)". Changed to
21
+ `PQAS (simulated) & Proactive Homing`. `bin/engine/learning-manager.js` carried the
22
+ identical string; fixed there too.
23
+ - **Dead docs link.** The banner's `Docs: https://docs.mindforge.cc` fails TLS handshake —
24
+ DNS resolves but no site was ever deployed there. Repointed to the real, verified-live
25
+ `https://github.com/sairam0424/MindForge#documentation`.
26
+
27
+ **Doc regression from the v11.9.6 pass, self-inflicted**
28
+
29
+ - The persona-count reconciliation in v11.9.6 used a naive
30
+ `find .mindforge/personas -type f | wc -l` (218), which silently swept in
31
+ `swarm-templates.json` and `overrides/README.md` — neither is a persona. The installer's
32
+ own `collectManifestStats()` counts top-level `.md` files only: **216**, matching the
33
+ preserved 2026-08-16 codebase-index snapshot. Reverted `README.md`,
34
+ `docs/CAPABILITIES-MANIFEST.md`, `docs/user-guide.md`, `docs/PERSONAS.md`,
35
+ `docs/registry/PERSONAS.md`, and `docs/registry/README.md`.
36
+
37
+ ## [11.9.6] — 2026-09-20 — The docs stop overselling what the code discloses about itself
38
+
39
+ Patch release. No new features — this is the release-readiness pass before pointing real
40
+ users at the project for the first time, and it found the same pattern one more time:
41
+ code that honestly labels its own simulated/dormant parts, sitting under docs that hadn't
42
+ caught up. Every fix below was hand-verified against the live source or a real command
43
+ run, not carried over from a prior audit's prose.
44
+
45
+ ### Fixed
46
+
47
+ **Two real, reproducible bugs**
48
+
49
+ - **`/mindforge:learn` crashed on every skill that scored high enough to register.**
50
+ `bin/skills-builder/learn-cli.js` called `skill-registrar.js`'s `register()` with two
51
+ positional string arguments (`skillPath`, `'project'`); `register()` destructures a
52
+ single options object, so every call threw inside it, silently swallowed by the CLI's
53
+ generic catch. A validly-generated `SKILL.md` never reached `MANIFEST.md`, and the CLI
54
+ reported a bare `❌ Error`. Fixed the call site to pass `{ skillName, skillPath, tier,
55
+ qualityScore, sourceType, source }`, matching `register()`'s real signature.
56
+
57
+ - **`bin/browser/browser-daemon.js` printed its own bearer token to stdout** (captured
58
+ verbatim into the persistent `.planning/browser-daemon.log` by `daemon-manager.js`),
59
+ and gated only `/evaluate` — `/navigate`, `/click`, `/type`, and `/screenshot` had no
60
+ auth check at all. The idle-timeout path also called `process.exit(0)` directly,
61
+ bypassing the token-file cleanup in `shutdown()`. Fixed all three: the startup log now
62
+ prints only the token's file path, the auth check runs once before dispatch and covers
63
+ every route but `/status`, and the idle path calls the real `shutdown()`.
64
+
65
+ **A test with a blind spot for the exact bug it exists to catch**
66
+
67
+ - `tests/sdk-exports.test.js` guards against any tracked file instructing
68
+ `require('@mindforge/sdk')` — the unpublished, wrong package name (`sdk/package.json`
69
+ publishes as `mindforge-sdk`). Its scan used `git ls-files -z '*.md' '*.ts'`, which
70
+ cannot match `.js` files — so it did not, and could not, catch the live occurrence in
71
+ `examples/sdk-integration/index.js`. Fixed both: the import, and the pathspec (now also
72
+ `*.js`/`*.mjs`/`*.cjs`, with `tests/` added to the same "records, not instructions"
73
+ allowlist as `changelogs/`, since this file's own `WRONG` string literal would otherwise
74
+ self-flag).
75
+
76
+ **A dashboard that rendered success while showing nothing**
77
+
78
+ - Three more panels in `bin/dashboard/frontend/app.js` read response fields their real
79
+ backing functions in `metrics-aggregator.js` have never produced — the same bug class
80
+ already fixed once for the avg-quality tile, left unaddressed here. `refreshMemory()`
81
+ read `data.graph`/`data.count`; the real shape is `{ entries, total }`. `refreshTeam()`
82
+ called `.map()` on the whole response object; the real shape is `{ active, conflicts }`
83
+ with `email`/`last_seen`/`current_task` fields, not `user`/`action`/`timestamp`.
84
+ `drawCharts()` read `state.costs`/`state.quality`, fields `/api/metrics` never returns;
85
+ the real per-session series is `state.sessions[].cost_usd` /
86
+ `state.sessions[].quality_score`. All three failed silently behind an empty `catch`, so
87
+ the panels looked idle rather than broken. Fixed to read the real shapes.
88
+
89
+ **Two shipped documents that stated the opposite of the code**
90
+
91
+ - `SECURITY.md` claimed `AUDIT.jsonl` "auto-archives beyond 5000 lines with gzip
92
+ compression" — that rotation mechanism was removed in an earlier release specifically
93
+ because truncating the file broke the hash chain (it orphans `previous_hash` pointers to
94
+ archived entries). The log grows unbounded by design; the doc now says so.
95
+ - `docs/security/SECURITY.md` had drifted into a second, independent copy of this policy
96
+ with a `5.x.x`/`4.x.x`/`< 4.0.0` support table — years behind the real `11.x` line the
97
+ root `/SECURITY.md` (the canonical file) documents. Replaced with a pointer to the root
98
+ file so this can't re-drift.
99
+
100
+ **A stale Homebrew formula**
101
+
102
+ - `Formula/mindforge.rb` was pinned to `11.9.3` (url, sha256, and the version-assertion
103
+ test) — two releases behind, so `brew install mindforge` installed an old build with
104
+ none of 11.9.4/11.9.5's fixes. Re-pinned to the real published `11.9.5` tarball with its
105
+ actual sha256 (fetched and hashed directly, not carried over). This is the same drift
106
+ class the project has hit twice before (11.9.2 shipped with this file and `Dockerfile`
107
+ four releases behind); `Formula/mindforge.rb` will lag one release again until
108
+ `node scripts/sync-version.js --fetch-sha` runs after this version publishes — expected,
109
+ not a regression.
110
+
111
+ ### Changed — a documentation honesty pass
112
+
113
+ The following describe MindForge's own PQAS (post-quantum crypto), ZTAI (Zero-Trust
114
+ Agentic Identity), and "Pillar"-numbered subsystems as live, unconditional security
115
+ guarantees. They are not: `bin/governance/quantum-crypto.js` and
116
+ `bin/governance/ztai-manager.js` self-label these SIMULATED and gate them off the live
117
+ trust path by default (`SECURITY_TIER_3_SIMULATED = true`), and `SwarmController` /
118
+ `PersonaFactory` / `WaveExecutor` are role names in markdown specs with no backing file —
119
+ a distinction this repo's own `.claude/CLAUDE.md` already draws, just not everywhere yet.
120
+ Rewrote each to match the same "measured, not asserted" tone already used in
121
+ `docs/faq.md` and `docs/troubleshooting.md` and the root README's *What is actually
122
+ enforced* section:
123
+
124
+ `docs/usp-features.md`, `CODEBASE-MAP.md`, `docs/architecture/README.md`,
125
+ `docs/CAPABILITIES-MANIFEST.md`, `docs/governance-guide.md`,
126
+ `docs/MIND-FORGE-REFERENCE-V6.md`, `docs/INTELLIGENCE-MESH.md`, `docs/PERSONAS.md`,
127
+ `docs/security/threat-model.md`, `docs/security/penetration-test-results.md` (the latter
128
+ two now banner-marked as scoped to an earlier, materially smaller predecessor system, not
129
+ a current assessment — no new pentest content was fabricated to replace them).
130
+
131
+ **Six reference docs (`docs/registry/*.md`) were stuck at v11.3.1** (six releases behind)
132
+ with command/skill/persona/subagent counts off by 2–9x against the live filesystem, and
133
+ listed 14+ slash commands with no backing file (`/mindforge:quantum-verify`,
134
+ `/mindforge:hindsight`, `/mindforge:harvest`, `/mindforge:self-heal`,
135
+ `/mindforge:swarm-execution`, `/mindforge:identity`, and others) — each individually
136
+ re-verified against `.claude/commands/mindforge/` before being removed or reworded.
137
+ Updated all six to the real, live-verified counts (221 commands, 232+123 skills, 217
138
+ personas, 164 subagents) and pointed each at `docs/commands-reference.md` as the
139
+ canonical source, so staleness here is lower-stakes going forward.
140
+
141
+ Also corrected: `docs/plugin-installation.md` and `docs/reference(s)/commands.md` (stale
142
+ hand-typed counts), `docs/References/{decimal-phase-calculation,git-integration,
143
+ git-planning-commit}.md` (wrong CLI path — real tool is `.agent/bin/mindforge-tools.cjs`,
144
+ not `.agent/mindforge/bin/...`), `docs/References/model-profile-resolution.md` (dead
145
+ `@`-include path), and a fabricated `"Claude 4.5 Opus"` model-name literal in two docs
146
+ (no such string exists anywhere in `bin/`) replaced with the real "highest-capability
147
+ tier configured" language. Deleted two orphaned scratch files that were never real
148
+ documentation: `docs/testing-current-version.md` (a pre-release scratch file hardcoding
149
+ a personal machine path) and `docs/commands-skills/DISCOVERED_SKILLS.md` (stale output
150
+ from an unrelated external tool referencing a directory that doesn't exist in this repo).
151
+
152
+ ### Changed — discoverability
153
+
154
+ - Root `README.md`: npm-version/downloads/license/Node-version badges, an "at a glance"
155
+ capability summary with real counts, and jump links to the existing sections.
156
+ - `package.json`: `description` rewritten from marketing language ("Sovereign Agentic
157
+ Intelligence Framework... Production-Hardened... (v11)") to concrete, keyword-bearing
158
+ text; added `ai-agents`, `llm-tools`, `developer-tools`, `mcp-server` to `keywords`.
159
+
160
+ ### Verified
161
+
162
+ - `npm test`: 137 passed, 0 failed, 3 env-dependent skips (`browser.test.js`,
163
+ `browser-daemon-auth-live.test.js`, `sre-integration.test.js` — all three require a
164
+ Chromium daemon/display or git worktree support this sandbox does not have). Verified
165
+ clean through the real pre-commit hook, not just a standalone run.
166
+ - `node scripts/sync-version.js`: 27 channels synced; `Formula/mindforge.rb` correctly
167
+ deferred (tarball doesn't exist yet); `plugins/mindforge/.claude-plugin/plugin.json` and
168
+ `plugins/mindforge/mcp/dist/index.js` rebuilt.
169
+
3
170
  ## [11.9.5] — 2026-08-22 — The release path can no longer strand itself, and the SDK ships
4
171
 
5
172
  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.7)
2
2
 
3
3
  ## 1. IDENTITY & VERSIONING
4
4
 
5
5
  [NAME] = MindForge
6
- [VERSION] = 11.9.5
6
+ [VERSION] = 11.9.7
7
7
  [STABLE] = true
8
8
  [MODE] = "Platform Sovereign"
9
9
  [REQUIRED_CORE_VERSION] = 11.9.1
package/README.md CHANGED
@@ -1,29 +1,43 @@
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) · 216 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.7** (2026-09-20) — The install banner stops contradicting itself. Found by
19
+ actually running v11.9.6's own documented install command in a clean project instead of
20
+ stopping at `--version`: the top banner claimed `SOVEREIGN INTELLIGENCE v8.1.1` while the
21
+ install activation line two screens later said `v8.2.0` for the same subsystem, and claimed
22
+ `PQAS ... Enabled` right before accurately disclosing it's simulated and off by default a
23
+ few lines down. Both fixed, plus a dead `docs.mindforge.cc` link and a persona-count doc
24
+ regression (218 → back to the correct 216) introduced by v11.9.6's own honesty pass. No
25
+ new features. See [RELEASENOTES.md](./RELEASENOTES.md) or [CHANGELOG.md](./CHANGELOG.md).
26
+
27
+ The previous release, **v11.9.6**, was the release-readiness pass before pointing real,
28
+ external users at the project for the first time: fixed a crash in `/mindforge:learn`, a
29
+ token-leak in the browser daemon, three dashboard panels that silently rendered nothing, a
30
+ stale Homebrew formula, and docs describing PQAS/ZTAI/"Pillar"-numbered subsystems as live
31
+ guarantees when the code already self-labels them simulated. **v11.9.5** fixed a release
32
+ pipeline that could strand itself mid-publish and shipped `mindforge-sdk` for the first
33
+ time since 11.8.0, with provenance. **v11.9.4**, before that, is where the hook gates
34
+ started actually registering: 11.9.3
35
+ shipped the code and then declined to run it on essentially every project. Measured against
36
+ the published tarballs — 11.9.3: **11 hook scripts installed, 0 registered**; 11.9.4:
37
+ **8 registered, 3 deny-class verified blocking**. That **behaviour change under a patch
38
+ bump** still applies — the installer writes `.claude/settings.json` where it previously
39
+ declined, merging append-only and backing up first. See the BREAKING section in
40
+ [CHANGELOG.md](./CHANGELOG.md).
27
41
 
28
42
  ---
29
43
 
@@ -156,7 +170,7 @@ Full verification walkthrough: [docs/quick-verify.md](docs/quick-verify.md).
156
170
  - **Audit events:** [docs/References/audit-events.md](docs/References/audit-events.md)
157
171
  - **Upgrade guide:** [docs/upgrade.md](docs/upgrade.md)
158
172
  - **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)
173
+ - **Security:** [SECURITY.md](SECURITY.md) (credentials are read from env vars and never committed to the repository)
160
174
  - **Threat model:** [docs/security/threat-model.md](docs/security/threat-model.md)
161
175
  - **Architecture:** [docs/architecture/README.md](docs/architecture/README.md)
162
176
  - **Contributing:** [docs/contributing/CONTRIBUTING.md](docs/contributing/CONTRIBUTING.md)
package/RELEASENOTES.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # Release Notes
2
2
 
3
+ ## v11.9.7 — 2026-09-20 — The install banner stops contradicting itself
4
+
5
+ ### Why this release exists
6
+
7
+ Running v11.9.6's own documented install command in a clean project (instead of stopping
8
+ at `--version`) surfaced two real bugs in the install banner itself, plus a persona-count
9
+ regression introduced by v11.9.6's own doc-honesty pass.
10
+
11
+ ### The user-visible part
12
+
13
+ **The install banner no longer contradicts itself.** It used to print `SOVEREIGN
14
+ INTELLIGENCE v8.1.1` at the top and `v8.2.0` for the same subsystem two screens later, and
15
+ claim `PQAS ... Enabled` right before accurately disclosing it's simulated and off by
16
+ default. Both fixed. The banner's `Docs:` link no longer points at a domain with no site
17
+ deployed on it.
18
+
19
+ **Docs say 216 personas again, not 218.** v11.9.6's own persona-count fix used a count that
20
+ accidentally included two non-persona files; reverted to the verified real number.
21
+
22
+ ## v11.9.6 — 2026-09-20 — The docs stop overselling what the code discloses about itself
23
+
24
+ ### Why this release exists
25
+
26
+ This is the readiness pass before pointing real, external users at the project for the
27
+ first time. No new features — it fixes two reproducible bugs, three dashboard panels that
28
+ silently rendered nothing, one stale security policy and one stale Homebrew formula, and a
29
+ long-running pattern where docs described PQAS/ZTAI/"Pillar"-numbered subsystems as live
30
+ security guarantees when the code that implements them (`bin/governance/quantum-crypto.js`,
31
+ `bin/governance/ztai-manager.js`) already self-labels them simulated and off-by-default.
32
+
33
+ ### The user-visible part
34
+
35
+ **`/mindforge:learn` works again.** It crashed on every skill that scored well enough to
36
+ auto-register — `skill-registrar.js`'s `register()` was called with the wrong argument
37
+ shape, so the CLI printed a bare `❌ Error` and nothing ever reached `MANIFEST.md`.
38
+
39
+ **The browser daemon no longer leaks its own auth token, and now actually checks it.**
40
+ `/navigate`, `/click`, `/type`, and `/screenshot` had zero authentication before this —
41
+ only `/evaluate` did. The startup log used to print the raw token to stdout (captured into
42
+ a plaintext, non-gitignored log file); now it only prints where the token file lives.
43
+
44
+ **Three dashboard panels (Memory, Team, and the cost/quality charts) were reading response
45
+ fields the backing API has never produced**, so they rendered as permanently empty with no
46
+ error. They now read the real shapes.
47
+
48
+ **If you use Homebrew:** `brew install mindforge` was pinned two releases behind
49
+ (`11.9.3`) and is now current. It will lag one release again after this one ships — that's
50
+ expected; the formula can't point at a tarball that doesn't exist yet.
51
+
52
+ ### The documentation part
53
+
54
+ Six reference docs (`docs/registry/*.md`) were frozen at v11.3.1 with command/skill/
55
+ persona counts off by 2–9x and 14+ slash commands listed that don't exist. `usp-features.md`,
56
+ `CODEBASE-MAP.md`, `docs/architecture/README.md`, and several other pages described
57
+ post-quantum crypto and Zero-Trust Agentic Identity as unconditional, shipped security
58
+ rather than the explicitly-simulated, opt-in-gated features they are in the actual code.
59
+ All rewritten to match the same measured tone the README's *What is actually enforced*
60
+ section and `docs/faq.md`/`docs/troubleshooting.md` already used. Two orphaned scratch
61
+ files that were never real documentation were removed.
62
+
63
+ See [CHANGELOG.md](./CHANGELOG.md) for the complete, file-by-file list.
64
+
3
65
  ## v11.9.5 — 2026-08-22 — The release path can no longer strand itself, and the SDK ships
4
66
 
5
67
  ### 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.7 | **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 };