@monoes/monomindcli 2.12.0 → 2.13.0
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/.claude/skills/mastermind/SKILL.md +1 -15
- package/README.md +1 -1
- package/dist/src/commands/doctor-monoes-checks.d.ts +1 -0
- package/dist/src/commands/doctor-monoes-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-monoes-checks.js +96 -34
- package/dist/src/commands/doctor-monoes-checks.js.map +1 -1
- package/dist/src/commands/doctor-project-checks.d.ts +19 -0
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +54 -0
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +11 -2
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +12 -0
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +9 -2
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/commands/ui.d.ts.map +1 -1
- package/dist/src/commands/ui.js +4 -0
- package/dist/src/commands/ui.js.map +1 -1
- package/dist/src/init/copy-assets.d.ts.map +1 -1
- package/dist/src/init/copy-assets.js +56 -18
- package/dist/src/init/copy-assets.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +9 -1
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/never-commit.d.ts +39 -0
- package/dist/src/init/never-commit.d.ts.map +1 -0
- package/dist/src/init/never-commit.js +15 -0
- package/dist/src/init/never-commit.js.map +1 -0
- package/dist/src/init/shared.d.ts +74 -1
- package/dist/src/init/shared.d.ts.map +1 -1
- package/dist/src/init/shared.js +166 -0
- package/dist/src/init/shared.js.map +1 -1
- package/dist/src/init/types.d.ts +5 -0
- package/dist/src/init/types.d.ts.map +1 -1
- package/dist/src/init/upgrade.d.ts.map +1 -1
- package/dist/src/init/upgrade.js +1 -0
- package/dist/src/init/upgrade.js.map +1 -1
- package/dist/src/init/write-kimicode.d.ts.map +1 -1
- package/dist/src/init/write-kimicode.js +13 -9
- package/dist/src/init/write-kimicode.js.map +1 -1
- package/dist/src/init/write-opencode.d.ts.map +1 -1
- package/dist/src/init/write-opencode.js +44 -2
- package/dist/src/init/write-opencode.js.map +1 -1
- package/dist/src/init/write-runtime-config.d.ts +2 -0
- package/dist/src/init/write-runtime-config.d.ts.map +1 -1
- package/dist/src/init/write-runtime-config.js +55 -38
- package/dist/src/init/write-runtime-config.js.map +1 -1
- package/dist/src/mastermind/manifest-data.d.ts.map +1 -1
- package/dist/src/mastermind/manifest-data.js +14 -0
- package/dist/src/mastermind/manifest-data.js.map +1 -1
- package/dist/src/mcp/monoes-mcp-entry.mjs +86 -0
- package/dist/src/mcp-tools/terminal-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/terminal-tools.js +52 -9
- package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts +84 -3
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +215 -17
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/platform-adapters/renderers/skills.d.ts +0 -6
- package/dist/src/platform-adapters/renderers/skills.d.ts.map +1 -1
- package/dist/src/platform-adapters/renderers/skills.js +12 -22
- package/dist/src/platform-adapters/renderers/skills.js.map +1 -1
- package/dist/src/ui/dashboard.html +7 -0
- package/dist/src/ui/orgs.html +7 -0
- package/dist/src/ui/routes-monoes.mjs +28 -3
- package/dist/src/ui/server.mjs +160 -14
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../../../../src/platform-adapters/renderers/skills.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAElD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EACL,2BAA2B,EAC3B,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,8BAA8B,CAAC;AAGtC,SAAS,wBAAwB,CAAC,OAAwB,EAAE,KAAmB;IAC7E,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;IACxD,OAAO,QAAQ,KAAK,SAAS,IAAI,OAAO,QAAQ,KAAK,QAAQ,CAAC;AAChE,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../../../../src/platform-adapters/renderers/skills.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAElD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EACL,2BAA2B,EAC3B,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,8BAA8B,CAAC;AAGtC,SAAS,wBAAwB,CAAC,OAAwB,EAAE,KAAmB;IAC7E,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;IACxD,OAAO,QAAQ,KAAK,SAAS,IAAI,OAAO,QAAQ,KAAK,QAAQ,CAAC;AAChE,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,iBAAiB,CAAC,OAAwB,EAAE,KAAmB;IAC7E,IAAI,OAAO,CAAC,YAAY,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,wBAAwB,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;QAC1F,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,wEAAwE;IACxE,yEAAyE;IACzE,2EAA2E;IAC3E,wEAAwE;IACxE,uEAAuE;IACvE,+DAA+D;IAC/D,uEAAuE;IACvE,wEAAwE;IACxE,2EAA2E;IAC3E,sEAAsE;IACtE,mCAAmC;IACnC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACjD,IAAI,EAAE,OAAgB;QACtB,WAAW,EAAE,OAAgB;QAC7B,OAAO,EAAE,kBAAkB,CAAC,KAAK,CAAC;QAClC,MAAM,EAAE,UAAU,OAAO,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE;QAC5C,YAAY,EAAE,GAAG,KAAK,CAAC,MAAM,WAAW;QACxC,KAAK;QACL,OAAO,EAAE,eAAwB;QACjC,MAAM,EAAE,IAAa;KACtB,CAAC,CAAC,CAAC;IACJ,MAAM,SAAS,GAAG,2BAA2B,EAAE,CAAC;IAChD,IAAI,CAAC,SAAS;QAAE,OAAO,QAAQ,CAAC;IAChC,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CACrD,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QACnC,IAAI,EAAE,OAAgB;QACtB,WAAW,EAAE,OAAgB;QAC7B,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QACvE,MAAM,EAAE,UAAU,OAAO,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,IAAI,SAAS,EAAE;QACzD,YAAY,EAAE,GAAG,KAAK,CAAC,MAAM,IAAI,SAAS,EAAE;QAC5C,KAAK;QACL,OAAO,EAAE,eAAwB;QACjC,MAAM,EAAE,IAAa;KACtB,CAAC,CAAC,CACJ,CAAC;IACF,OAAO,CAAC,GAAG,QAAQ,EAAE,GAAG,UAAU,CAAC,CAAC;AACtC,CAAC"}
|
|
@@ -2446,6 +2446,13 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
|
|
|
2446
2446
|
// automatically. EventSource can't carry custom headers at all, so SSE call
|
|
2447
2447
|
// sites must use window.mmEventSource(url) instead of `new EventSource(url)`
|
|
2448
2448
|
// directly — it appends the credential as a query param.
|
|
2449
|
+
//
|
|
2450
|
+
// i-073: the query param is accepted for GET/HEAD ONLY — a POST/PUT/DELETE
|
|
2451
|
+
// with only ?token= is a CORS-simple request (no preflight, reachable from a
|
|
2452
|
+
// plain <form>), so mutations MUST send the header. Any new call that
|
|
2453
|
+
// mutates has to go through fetch() (the wrapper above), never a hand-built
|
|
2454
|
+
// URL with the token appended — that would silently reopen the CSRF hole
|
|
2455
|
+
// this item closed.
|
|
2449
2456
|
(() => {
|
|
2450
2457
|
const AUTH_HEADER_NAME = 'x-monomind-' + 'token';
|
|
2451
2458
|
const AUTH_QUERY_PARAM = 'to' + 'ken';
|
package/dist/src/ui/orgs.html
CHANGED
|
@@ -1066,6 +1066,13 @@ html, body {
|
|
|
1066
1066
|
// orgs.html and dashboard.html are served as separate pages. EventSource
|
|
1067
1067
|
// can't carry custom headers, so SSE call sites must use
|
|
1068
1068
|
// window.mmEventSource(url) instead of `new EventSource(url)` directly.
|
|
1069
|
+
//
|
|
1070
|
+
// i-073: the query param is accepted for GET/HEAD ONLY — a POST/PUT/DELETE
|
|
1071
|
+
// with only ?token= is a CORS-simple request (no preflight, reachable from a
|
|
1072
|
+
// plain <form>), so mutations MUST send the header. Any new call that
|
|
1073
|
+
// mutates has to go through fetch() (the wrapper above), never a hand-built
|
|
1074
|
+
// URL with the token appended — that would silently reopen the CSRF hole
|
|
1075
|
+
// this item closed.
|
|
1069
1076
|
(() => {
|
|
1070
1077
|
const AUTH_HEADER_NAME = 'x-monomind-' + 'token';
|
|
1071
1078
|
const AUTH_QUERY_PARAM = 'to' + 'ken';
|
|
@@ -106,14 +106,33 @@ async function _registerClient(redirectUri) {
|
|
|
106
106
|
return data.client_id;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// #307: a registered client is bound to the ONE redirect_uri it was
|
|
110
|
+
// registered with, so the cached clientId is only reusable while the
|
|
111
|
+
// dashboard is still on that port. Caching the id alone meant a dashboard
|
|
112
|
+
// that fell back to another port (4242 busy -> 4243) sent the old client
|
|
113
|
+
// with a new redirect_uri, and monoes.me redirected the browser to the port
|
|
114
|
+
// the client was registered for — ERR_CONNECTION_REFUSED, connection never
|
|
115
|
+
// completes. Store the redirectUri alongside the id and re-register when
|
|
116
|
+
// they no longer agree.
|
|
109
117
|
async function _getOrRegisterClientId(monomindHome, redirectUri) {
|
|
110
118
|
const existing = readMonoesConnection(monomindHome);
|
|
111
|
-
if (existing?.clientId) return existing.clientId;
|
|
119
|
+
if (existing?.clientId && existing.redirectUri === redirectUri) return existing.clientId;
|
|
112
120
|
const clientId = await _registerClient(redirectUri);
|
|
113
|
-
_writeMonoesConnection(monomindHome, { ...(existing || {}), clientId });
|
|
121
|
+
_writeMonoesConnection(monomindHome, { ...(existing || {}), clientId, redirectUri });
|
|
114
122
|
return clientId;
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
/** Forgets the cached OAuth client (keeping any tokens) after monoes.me has
|
|
126
|
+
* rejected it — a client that the server no longer knows stays dead for
|
|
127
|
+
* every future attempt unless it is dropped, so the next connect must
|
|
128
|
+
* register a fresh one rather than retrying the same dead id. */
|
|
129
|
+
function _forgetMonoesClient(monomindHome) {
|
|
130
|
+
const existing = readMonoesConnection(monomindHome);
|
|
131
|
+
if (!existing) return;
|
|
132
|
+
const { clientId: _clientId, redirectUri: _redirectUri, ...rest } = existing;
|
|
133
|
+
_writeMonoesConnection(monomindHome, rest);
|
|
134
|
+
}
|
|
135
|
+
|
|
117
136
|
/** Merges (or removes) the `monoes` entry in the project's existing
|
|
118
137
|
* .mcp.json. Only touches that one key — never creates or rewrites the
|
|
119
138
|
* rest of the file, since .mcp.json is otherwise owned by `init`/the user.
|
|
@@ -248,7 +267,12 @@ export async function handleMonoesRoutes(req, res, url, corsOrigin, ctx) {
|
|
|
248
267
|
code_verifier: pending.codeVerifier,
|
|
249
268
|
}),
|
|
250
269
|
});
|
|
251
|
-
if (!tokenRes.ok)
|
|
270
|
+
if (!tokenRes.ok) {
|
|
271
|
+
// 400/401 here means the client itself was refused (unknown client,
|
|
272
|
+
// or a redirect_uri it isn't registered for) — see #307.
|
|
273
|
+
if (tokenRes.status === 400 || tokenRes.status === 401) _forgetMonoesClient(MONOMIND_HOME);
|
|
274
|
+
throw new Error(`token exchange failed: ${tokenRes.status}`);
|
|
275
|
+
}
|
|
252
276
|
const tokenData = await tokenRes.json();
|
|
253
277
|
|
|
254
278
|
const meRes = await fetch(`${MONOES_BASE_URL}/api/community/me`, {
|
|
@@ -258,6 +282,7 @@ export async function handleMonoesRoutes(req, res, url, corsOrigin, ctx) {
|
|
|
258
282
|
|
|
259
283
|
_writeMonoesConnection(MONOMIND_HOME, {
|
|
260
284
|
clientId: existing.clientId,
|
|
285
|
+
redirectUri: existing.redirectUri,
|
|
261
286
|
accessToken: /* value */ tokenData.access_token,
|
|
262
287
|
refreshToken: /* value */ tokenData.refresh_token || null,
|
|
263
288
|
expiresAt: Date.now() + (tokenData.expires_in || 3600) * 1000,
|
package/dist/src/ui/server.mjs
CHANGED
|
@@ -6,6 +6,10 @@ import os from 'node:os';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import zlib from 'node:zlib';
|
|
9
|
+
import {
|
|
10
|
+
detectDashboardTokenLeak,
|
|
11
|
+
formatDashboardTokenLeakWarning,
|
|
12
|
+
} from '../mcp/monoes-mcp-entry.mjs';
|
|
9
13
|
import {
|
|
10
14
|
collectAgents,
|
|
11
15
|
collectAll,
|
|
@@ -574,17 +578,34 @@ function _getGitMonomindDir(workDir) {
|
|
|
574
578
|
}
|
|
575
579
|
|
|
576
580
|
// Returns the monomind home directory for server-level data (capture, control.json, loops).
|
|
577
|
-
// Priority:
|
|
578
|
-
|
|
581
|
+
// Priority: an EXPLICIT project dir (--project-dir / CLAUDE_PROJECT_DIR) > MONOMIND_HOME env
|
|
582
|
+
// var > walk up from cwd finding .monomind/control.json, never past the enclosing git root >
|
|
583
|
+
// cwd fallback.
|
|
584
|
+
//
|
|
585
|
+
// #308: the walk used to run unconditionally, ignoring --project-dir, so it could resolve to
|
|
586
|
+
// any ancestor that happened to hold a control.json from an earlier dashboard run. Per-project
|
|
587
|
+
// state written under this home — the monoes.me connection file with its OAuth tokens,
|
|
588
|
+
// capture/, orgs/ — then landed in that ancestor instead of the project the dashboard is for.
|
|
589
|
+
//
|
|
590
|
+
// `projectDirExplicit` is what keeps the walk alive: both entry points always hand startServer
|
|
591
|
+
// a projectDir (`monomind ui` defaults the flag to cwd, the run-directly block below defaults
|
|
592
|
+
// CLAUDE_PROJECT_DIR to cwd), so short-circuiting on the dir alone would silently move the home
|
|
593
|
+
// of every dashboard started from a subdirectory of its project down into that subdirectory.
|
|
594
|
+
// Only a dir the caller actually named counts.
|
|
595
|
+
export function getMonomindHome(projectDir, projectDirExplicit) {
|
|
596
|
+
if (projectDirExplicit && projectDir) return path.resolve(projectDir);
|
|
579
597
|
if (process.env.MONOMIND_HOME) return path.resolve(process.env.MONOMIND_HOME);
|
|
580
598
|
let dir = process.cwd();
|
|
581
599
|
while (dir !== path.dirname(dir)) {
|
|
582
600
|
if (fs.existsSync(path.join(dir, '.monomind', 'control.json'))) return dir;
|
|
601
|
+
if (fs.existsSync(path.join(dir, '.git'))) break;
|
|
583
602
|
dir = path.dirname(dir);
|
|
584
603
|
}
|
|
585
604
|
return process.cwd();
|
|
586
605
|
}
|
|
587
|
-
|
|
606
|
+
// Re-resolved from the options at startServer() time (the projectDir only exists then); the
|
|
607
|
+
// call sites below all read it from inside startServer, after that assignment.
|
|
608
|
+
let MONOMIND_HOME = getMonomindHome();
|
|
588
609
|
|
|
589
610
|
// Resolve an org's project directory by searching across known projects.
|
|
590
611
|
// Returns the first project dir where {dir}/.monomind/orgs/{orgName}.json exists, or null.
|
|
@@ -878,6 +899,9 @@ function bindServer(server, port) {
|
|
|
878
899
|
* @param {object} [options]
|
|
879
900
|
* @param {number} [options.port=4242] - Preferred port. Tries up to port+10 on collision.
|
|
880
901
|
* @param {string} [options.projectDir] - Root of the project to collect data from.
|
|
902
|
+
* @param {boolean} [options.projectDirExplicit=false] - True when projectDir was named by the
|
|
903
|
+
* caller (--project-dir / CLAUDE_PROJECT_DIR) rather than defaulted to cwd; only then does it
|
|
904
|
+
* set the monomind home (see getMonomindHome, #308).
|
|
881
905
|
* @param {boolean} [options.openBrowser=true] - Whether to open the dashboard in the default browser.
|
|
882
906
|
* @returns {Promise<{port: number, url: string, server: http.Server}>}
|
|
883
907
|
*/
|
|
@@ -973,9 +997,24 @@ function _resolveSlugToPathUncached(slug, projDir) {
|
|
|
973
997
|
export async function startServer({
|
|
974
998
|
port = 4242,
|
|
975
999
|
projectDir,
|
|
1000
|
+
projectDirExplicit = false,
|
|
976
1001
|
openBrowser = true,
|
|
977
1002
|
allowedHosts,
|
|
978
1003
|
} = {}) {
|
|
1004
|
+
// #308: resolve the home now that the caller's project dir is in hand.
|
|
1005
|
+
MONOMIND_HOME = getMonomindHome(projectDir, projectDirExplicit);
|
|
1006
|
+
// i-052 commit 3: warn on every start, not only at `init` (executor.ts).
|
|
1007
|
+
// The dashboard is what WRITES dashboard-token — a user who never
|
|
1008
|
+
// re-runs `init` after the file got committed (e.g. before commits 1-2
|
|
1009
|
+
// shipped `.gitignore` coverage) would otherwise never see this warning
|
|
1010
|
+
// at all. Same "name the file, never the value" contract as the
|
|
1011
|
+
// monoes.me leak warning; best-effort (never blocks startup on a git
|
|
1012
|
+
// failure — detectDashboardTokenLeak already treats git-unavailable as
|
|
1013
|
+
// "not a leak signal", not an error).
|
|
1014
|
+
const dashboardTokenStartupWarning = formatDashboardTokenLeakWarning(
|
|
1015
|
+
await detectDashboardTokenLeak(projectDir || process.cwd()),
|
|
1016
|
+
);
|
|
1017
|
+
if (dashboardTokenStartupWarning) console.error(dashboardTokenStartupWarning);
|
|
979
1018
|
// Extra Host names accepted beyond loopback (see isAllowedHost above).
|
|
980
1019
|
const _allowedHosts = resolveAllowedHosts(allowedHosts);
|
|
981
1020
|
// ── Security: per-process auth credential for mutating (non-GET) requests ─
|
|
@@ -991,6 +1030,17 @@ export async function startServer({
|
|
|
991
1030
|
// silently degrading per-prompt Second Brain injection to keyword fallback.
|
|
992
1031
|
// So the write is deferred to bind time and gated: primary instances write
|
|
993
1032
|
// `dashboard-token`; secondaries write `dashboard-token-<port>` instead.
|
|
1033
|
+
// i-052 commit 4: the path THIS process actually wrote (primary
|
|
1034
|
+
// `dashboard-token` xor its own `dashboard-token-<port>` secondary) — set
|
|
1035
|
+
// once, at the bottom of writeDashboardToken(), and read by shutdown()
|
|
1036
|
+
// below for best-effort cleanup on a clean exit. `mode` is create-only
|
|
1037
|
+
// on `fs.writeFileSync` (POSIX `open(2)` ignores it when the file
|
|
1038
|
+
// exists) — measured on this machine: 2 of 11 token files were 644
|
|
1039
|
+
// despite every write site passing `{ mode: 0o600 }`, because a prior
|
|
1040
|
+
// run had already created them. `fs.chmodSync` after every write, here
|
|
1041
|
+
// and in propagateDashboardToken() below, enforces the mode on rewrite
|
|
1042
|
+
// too, not just on first creation.
|
|
1043
|
+
let _dashboardTokenFilePath = null;
|
|
994
1044
|
async function writeDashboardToken(actualPort) {
|
|
995
1045
|
try {
|
|
996
1046
|
actualPort = Number(actualPort);
|
|
@@ -1032,14 +1082,17 @@ export async function startServer({
|
|
|
1032
1082
|
} catch (_) {
|
|
1033
1083
|
/* no readable control.json — treat as primary */
|
|
1034
1084
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
{ mode: 0o600 },
|
|
1085
|
+
const tokenPath = path.join(
|
|
1086
|
+
authFileDir,
|
|
1087
|
+
primary ? 'dashboard-token' : `dashboard-token-${actualPort}`,
|
|
1039
1088
|
);
|
|
1089
|
+
fs.writeFileSync(tokenPath, dashboardAuthValue, { mode: 0o600 });
|
|
1090
|
+
fs.chmodSync(tokenPath, 0o600); // enforce on rewrite — {mode} above is create-only
|
|
1091
|
+
_dashboardTokenFilePath = tokenPath;
|
|
1040
1092
|
// Sweep stale secondary tokens (dead scratch/test instances) so they
|
|
1041
1093
|
// don't accumulate as orphaned credential files.
|
|
1042
|
-
//
|
|
1094
|
+
// Age-based sweep — the belt-and-braces backstop for a dirty death
|
|
1095
|
+
// (kill -9) that never reaches shutdown()'s cleanup below.
|
|
1043
1096
|
try {
|
|
1044
1097
|
const weekMs = 7 * 24 * 3600 * 1000;
|
|
1045
1098
|
for (const f of fs.readdirSync(authFileDir)) {
|
|
@@ -1050,9 +1103,45 @@ export async function startServer({
|
|
|
1050
1103
|
} catch (_) {}
|
|
1051
1104
|
} catch (_) {}
|
|
1052
1105
|
}
|
|
1106
|
+
// i-052 commit 4: this is the code path that actually planted
|
|
1107
|
+
// dashboard-token in the incident's other repos — a paired project may
|
|
1108
|
+
// never have run `init` (so it never got commits 1-2's `.gitignore`
|
|
1109
|
+
// coverage) yet still receives a live token here on every restart.
|
|
1110
|
+
// Ensures the SAME coverage `init` would have given it before writing
|
|
1111
|
+
// the token, content-guarded exactly like write-runtime-config.ts's
|
|
1112
|
+
// append (creates the file if absent; appends the one missing line if
|
|
1113
|
+
// present; no-ops if already covered) — duplicated locally rather than
|
|
1114
|
+
// imported because this file ships as plain ESM with no build step and
|
|
1115
|
+
// cannot import compiled TypeScript (see monoes-mcp-entry.mjs's own doc
|
|
1116
|
+
// comment for the identical constraint). Best-effort: a coverage-ensure
|
|
1117
|
+
// failure must never block token delivery, which is the more urgent
|
|
1118
|
+
// property (a stale token 401s every cross-project caller).
|
|
1119
|
+
function ensureDashboardTokenGitignoreCoverage(kpMonoDir) {
|
|
1120
|
+
try {
|
|
1121
|
+
const gitignorePath = path.join(kpMonoDir, '.gitignore');
|
|
1122
|
+
const reason =
|
|
1123
|
+
'live monomind dashboard credential (i-052) — grants cross-project file reads and agent execution; rewritten on every dashboard restart, so an already-tracked path re-commits a fresh live token on every restart';
|
|
1124
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
1125
|
+
fs.mkdirSync(kpMonoDir, { recursive: true });
|
|
1126
|
+
fs.writeFileSync(gitignorePath, `# ${reason}\ndashboard-token\n`);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
1130
|
+
const existingLines = new Set(existing.split('\n').map((l) => l.trim()));
|
|
1131
|
+
if (!existingLines.has('dashboard-token')) {
|
|
1132
|
+
fs.writeFileSync(gitignorePath, `${existing.trimEnd()}\n# ${reason}\ndashboard-token\n`);
|
|
1133
|
+
}
|
|
1134
|
+
} catch (_) {
|
|
1135
|
+
/* best effort — see doc comment above */
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1053
1139
|
// Propagate the fresh token to every known project whose control.json points
|
|
1054
1140
|
// at this server — otherwise each restart silently orphans cross-project
|
|
1055
|
-
// callers (their curls/CLI reads a stale token and 401s forever).
|
|
1141
|
+
// callers (their curls/CLI reads a stale token and 401s forever). The
|
|
1142
|
+
// population is `data/known-projects.json`'s own content, never a
|
|
1143
|
+
// directory glob (i-052 owner correction: the incident's affected-repo
|
|
1144
|
+
// count included projects outside any assumed directory layout).
|
|
1056
1145
|
// Called after bind so the match uses the ACTUAL bound port.
|
|
1057
1146
|
function propagateDashboardToken(actualPort) {
|
|
1058
1147
|
try {
|
|
@@ -1064,9 +1153,10 @@ export async function startServer({
|
|
|
1064
1153
|
const _kpCtl = JSON.parse(fs.readFileSync(path.join(_kpMono, 'control.json'), 'utf8'));
|
|
1065
1154
|
// Only pair projects that direct their traffic to this server's port.
|
|
1066
1155
|
if (_kpCtl && String(_kpCtl.url || '').includes(`:${actualPort}`)) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
});
|
|
1156
|
+
ensureDashboardTokenGitignoreCoverage(_kpMono);
|
|
1157
|
+
const _kpTokenPath = path.join(_kpMono, 'dashboard-token');
|
|
1158
|
+
fs.writeFileSync(_kpTokenPath, dashboardAuthValue, { mode: 0o600 });
|
|
1159
|
+
fs.chmodSync(_kpTokenPath, 0o600); // enforce on rewrite — {mode} above is create-only
|
|
1070
1160
|
}
|
|
1071
1161
|
} catch (_) {
|
|
1072
1162
|
/* project missing/unreadable — skip */
|
|
@@ -1771,7 +1861,16 @@ export async function startServer({
|
|
|
1771
1861
|
|
|
1772
1862
|
const _checkAuth = (req) => {
|
|
1773
1863
|
let _suppliedAuth = req.headers['x-monomind-token'] || '';
|
|
1774
|
-
|
|
1864
|
+
// i-073: the query-token fallback is for EventSource (which cannot send
|
|
1865
|
+
// headers) and other subresource GETs ONLY. It used to apply
|
|
1866
|
+
// unconditionally, which meant a mutation needed only the header OR the
|
|
1867
|
+
// query param — and the header is what forces a CORS preflight (a
|
|
1868
|
+
// mutation becomes a "simple request" without it). A plain
|
|
1869
|
+
// `<form method=POST action="...?token=LEAKED">` has no header to omit,
|
|
1870
|
+
// so accepting the query param on POST turned every mutation into a
|
|
1871
|
+
// CSRF target reachable with no preflight and no JS. Restoring the
|
|
1872
|
+
// unconditional fallback here reopens that hole — see i-073.
|
|
1873
|
+
if (!_suppliedAuth && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
1775
1874
|
try {
|
|
1776
1875
|
_suppliedAuth = new URL(req.url, 'http://localhost').searchParams.get('token') || '';
|
|
1777
1876
|
} catch (_) {}
|
|
@@ -1855,6 +1954,32 @@ export async function startServer({
|
|
|
1855
1954
|
_sendUnauthorized(res, corsOrigin);
|
|
1856
1955
|
return;
|
|
1857
1956
|
}
|
|
1957
|
+
// ── Security: Sec-Fetch-Site CSRF guard on mutations (i-073) ──────────
|
|
1958
|
+
// The token check alone cannot stop CSRF if the token ever leaks (a
|
|
1959
|
+
// browser cache, a proxy log, `ps` on a shared host). Reject when the
|
|
1960
|
+
// header is PRESENT and not `same-origin` — deliberately NOT "reject
|
|
1961
|
+
// cross-site": `Sec-Fetch-Site: same-site` covers a *different port on
|
|
1962
|
+
// the same host*, i.e. a page served by another local server (another
|
|
1963
|
+
// monomind dashboard among them — o-04 shows there can be eleven).
|
|
1964
|
+
// Rejecting anything that isn't `same-origin` closes that case too, at
|
|
1965
|
+
// no cost to the dashboard's own pages.
|
|
1966
|
+
// Header ABSENT must stay allowed, or every first-party non-browser
|
|
1967
|
+
// caller breaks: control-start.cjs, event-logger.cjs,
|
|
1968
|
+
// capture-handler.cjs, route-handler.cjs, forwarder.ts and the
|
|
1969
|
+
// `/mastermind:*` commands all POST with the token header and send no
|
|
1970
|
+
// Sec-Fetch-* at all. A browser too old to send Sec-Fetch-Site is
|
|
1971
|
+
// treated the same as a non-browser client — it still needs the token.
|
|
1972
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
1973
|
+
const _secFetchSite = req.headers['sec-fetch-site'];
|
|
1974
|
+
if (_secFetchSite && _secFetchSite !== 'same-origin') {
|
|
1975
|
+
res.writeHead(403, {
|
|
1976
|
+
'Content-Type': 'application/json',
|
|
1977
|
+
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
1978
|
+
});
|
|
1979
|
+
res.end(JSON.stringify({ error: 'Forbidden: cross-origin mutation' }));
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1858
1983
|
}
|
|
1859
1984
|
|
|
1860
1985
|
// ------------------------------------------------------------------ GET /
|
|
@@ -5911,6 +6036,21 @@ export async function startServer({
|
|
|
5911
6036
|
// Close all SSE connections
|
|
5912
6037
|
closeSseClients();
|
|
5913
6038
|
|
|
6039
|
+
// i-052 commit 4: best-effort cleanup of the credential THIS process
|
|
6040
|
+
// wrote (primary `dashboard-token`, or this process's own
|
|
6041
|
+
// `dashboard-token-<port>` secondary) — belt-and-braces only.
|
|
6042
|
+
// writeDashboardToken's age-based sweep is the actual backstop for a
|
|
6043
|
+
// dirty death: SIGKILL never reaches this handler at all, and a
|
|
6044
|
+
// failed unlink here (permissions, already-gone) must not block the
|
|
6045
|
+
// rest of shutdown.
|
|
6046
|
+
if (_dashboardTokenFilePath) {
|
|
6047
|
+
try {
|
|
6048
|
+
fs.unlinkSync(_dashboardTokenFilePath);
|
|
6049
|
+
} catch (_) {
|
|
6050
|
+
/* already gone, or unremovable — the age sweep is the backstop */
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
6053
|
+
|
|
5914
6054
|
// Drain in-flight JSONL appends before closing (prevents truncated writes on fast SIGTERM)
|
|
5915
6055
|
Promise.all([..._writeQueue.values()])
|
|
5916
6056
|
.catch(() => {})
|
|
@@ -5955,8 +6095,14 @@ export function getServerStatus() {
|
|
|
5955
6095
|
const _isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
5956
6096
|
if (_isMain) {
|
|
5957
6097
|
const _port = parseInt(process.argv[2] || process.env.CONTROL_PORT || '4242', 10);
|
|
6098
|
+
// CLAUDE_PROJECT_DIR names a project; the bare cwd fallback does not (#308).
|
|
5958
6099
|
const _dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
5959
|
-
startServer({
|
|
6100
|
+
startServer({
|
|
6101
|
+
port: _port,
|
|
6102
|
+
openBrowser: false,
|
|
6103
|
+
projectDir: _dir,
|
|
6104
|
+
projectDirExplicit: Boolean(process.env.CLAUDE_PROJECT_DIR),
|
|
6105
|
+
}).catch((err) => {
|
|
5960
6106
|
process.stderr.write(`[server] failed to start: ${err.message}\n`);
|
|
5961
6107
|
process.exit(1);
|
|
5962
6108
|
});
|