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.
- package/.mindforge/config.json +1 -1
- package/CHANGELOG.md +133 -0
- package/MINDFORGE.md +2 -2
- package/README.md +29 -19
- package/RELEASENOTES.md +43 -0
- package/SECURITY.md +2 -2
- package/bin/browser/browser-daemon.js +84 -48
- package/bin/browser/daemon-auth.js +36 -0
- package/bin/browser/daemon-manager.js +53 -22
- package/bin/dashboard/frontend/app.js +206 -101
- package/bin/skills-builder/learn-cli.js +26 -14
- package/changelogs/v11.9.6.md +135 -0
- package/docs/References/commands.md +13 -8
- package/docs/References/decimal-phase-calculation.md +5 -5
- package/docs/References/git-integration.md +4 -4
- package/docs/References/git-planning-commit.md +2 -2
- package/docs/References/model-profile-resolution.md +1 -1
- package/docs/faq.md +5 -4
- package/docs/getting-started.md +3 -3
- package/docs/sdk-reference.md +3 -3
- package/docs/troubleshooting.md +1 -1
- package/docs/user-guide.md +4 -4
- package/examples/sdk-integration/index.js +1 -1
- package/package.json +6 -3
package/.mindforge/config.json
CHANGED
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.
|
|
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.
|
|
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
|
+
[](https://www.npmjs.com/package/mindforge-cc)
|
|
4
|
+
[](https://www.npmjs.com/package/mindforge-cc)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](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.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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:** [
|
|
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.
|
|
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
|
-
- **
|
|
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
|
|
9
|
-
const crypto
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
10
|
const playwright = require('playwright-core');
|
|
11
|
-
const fs
|
|
12
|
-
const path
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { requiresAuth, isAuthValid } = require('./daemon-auth');
|
|
13
14
|
|
|
14
|
-
const PORT
|
|
15
|
-
const HEADLESS
|
|
16
|
-
const TIMEOUT
|
|
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(
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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({
|
|
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
|
|
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 =
|
|
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')
|
|
138
|
-
|
|
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()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 };
|