blun-king-cli 9.0.0 → 9.0.1

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.
Files changed (55) hide show
  1. package/LIESMICH.txt +1 -7
  2. package/README.md +4 -16
  3. package/bin/blun.js +248 -160
  4. package/bin/core-bootstrap.js +47 -0
  5. package/bin/king.js +277 -1
  6. package/bin/launcher-mode.js +2 -1
  7. package/bin/launcher-runtime.js +221 -0
  8. package/bin/plugin-bootstrap.js +0 -0
  9. package/bin/private-paths.js +0 -0
  10. package/bin/update-lease.js +399 -0
  11. package/bin/update-notice.js +1094 -0
  12. package/blun.mjs +4060 -6667
  13. package/package.json +3 -10
  14. package/skills/screenshot-lesen/SKILL.md +0 -1
  15. package/skills/web-lesen/SKILL.md +0 -1
  16. package/telegram-plugin/dist/bridge.mjs +1 -21
  17. package/mnemo/access_routes.js +0 -692
  18. package/mnemo/agent_governance.js +0 -4242
  19. package/mnemo/agent_mail.js +0 -901
  20. package/mnemo/bootstrap_auto.js +0 -137
  21. package/mnemo/brief_coordination.js +0 -226
  22. package/mnemo/code_read_tools.js +0 -375
  23. package/mnemo/context_preview_tools.js +0 -603
  24. package/mnemo/embeddings.js +0 -66
  25. package/mnemo/external_repo_ops.js +0 -575
  26. package/mnemo/facts/example-project-rules.json +0 -90
  27. package/mnemo/facts/example.json +0 -34
  28. package/mnemo/identity_schema.sql +0 -139
  29. package/mnemo/journal_schema.js +0 -561
  30. package/mnemo/loop_doctor_tools.js +0 -661
  31. package/mnemo/mail_secret_refs.js +0 -150
  32. package/mnemo/mcp.js +0 -9309
  33. package/mnemo/memory_consolidation.js +0 -1914
  34. package/mnemo/memory_health_tools.js +0 -165
  35. package/mnemo/package.json +0 -79
  36. package/mnemo/protected_scope_gate.js +0 -627
  37. package/mnemo/resource_access_control.js +0 -684
  38. package/mnemo/runtime_governance.js +0 -1256
  39. package/mnemo/runtime_turn_gate.js +0 -862
  40. package/mnemo/sandbox.js +0 -143
  41. package/mnemo/schema.sql +0 -389
  42. package/mnemo/shared_utils.js +0 -763
  43. package/mnemo/skills/agent-auto-resume/SKILL.md +0 -56
  44. package/mnemo/skills/agent_hand/SKILL.md +0 -43
  45. package/mnemo/skills/agent_hand/run.js +0 -63
  46. package/mnemo/skills/book_flight/SKILL.md +0 -34
  47. package/mnemo/skills/external_repo_review/SKILL.md +0 -43
  48. package/mnemo/skills/external_repo_review/run.js +0 -73
  49. package/mnemo/skills/pay_invoice/SKILL.md +0 -34
  50. package/mnemo/team_quality_ops.js +0 -944
  51. package/mnemo/timeline_report_tools.js +0 -810
  52. package/mnemo/write_gate_risk.js +0 -80
  53. package/mnemo/writer_health.js +0 -152
  54. package/skills/doku-ingestion/SKILL.md +0 -48
  55. package/skills/doku-ingestion/ingest_docs.py +0 -133
@@ -1,692 +0,0 @@
1
- "use strict";
2
-
3
- const { cleanScope, parseMaybeJson, boolFlag } = require("./shared_utils");
4
-
5
- const DEFAULT_SCOPE = cleanScope(process.env.MNEMO_DEFAULT_SCOPE || "default");
6
- const DEFAULT_AGENT = process.env.MNEMO_DEFAULT_AGENT || process.env.MNEMO_AGENT || "agent";
7
-
8
- const ROUTE_COLUMNS = [
9
- ["route_kind", "TEXT NOT NULL DEFAULT 'direct'"],
10
- ["direct_allowed", "INTEGER NOT NULL DEFAULT 1"],
11
- ["jump_host", "TEXT"],
12
- ["jump_user", "TEXT"],
13
- ["jump_secret_ref", "TEXT"],
14
- ["proxy_command", "TEXT"],
15
- ["canonical_command", "TEXT"],
16
- ["route_steps_json", "TEXT"],
17
- ["preflight_required", "INTEGER NOT NULL DEFAULT 1"],
18
- ["last_route_check_at", "TEXT"],
19
- ];
20
-
21
- function scopeName(scope) {
22
- return cleanScope(scope || DEFAULT_SCOPE);
23
- }
24
-
25
- function isoNow() {
26
- return new Date().toISOString();
27
- }
28
-
29
- function normalizeRouteKind(value) {
30
- const raw = String(value || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
31
- if (!raw) return "direct";
32
- if (["ssh_jump", "jump_host", "jumphost", "bastion", "bastion_host"].includes(raw)) return "jump";
33
- if (["proxycommand", "proxy_command", "ssh_proxy"].includes(raw)) return "proxy";
34
- if (["direct", "jump", "proxy", "vpn", "tunnel", "manual", "unknown"].includes(raw)) return raw;
35
- return raw;
36
- }
37
-
38
- function normalizeAccessKind(value) {
39
- return String(value || "other").trim().toLowerCase() || "other";
40
- }
41
-
42
- function cleanText(value) {
43
- if (value === undefined || value === null) return null;
44
- const text = String(value).trim();
45
- return text || null;
46
- }
47
-
48
- function parseAllowedAgents(value) {
49
- if (!value) return [];
50
- if (Array.isArray(value)) return value.map((v) => String(v || "").trim()).filter(Boolean);
51
- const parsed = parseMaybeJson(value, null);
52
- if (Array.isArray(parsed)) return parsed.map((v) => String(v || "").trim()).filter(Boolean);
53
- return String(value).split(",").map((v) => v.trim()).filter(Boolean);
54
- }
55
-
56
- function parseRouteSteps(value) {
57
- if (!value) return [];
58
- if (Array.isArray(value)) return value.map((v) => String(v || "").trim()).filter(Boolean);
59
- const parsed = parseMaybeJson(value, null);
60
- if (Array.isArray(parsed)) return parsed.map((v) => String(v || "").trim()).filter(Boolean);
61
- return String(value).split(/\r?\n/).map((v) => v.trim()).filter(Boolean);
62
- }
63
-
64
- function stringifyAllowedAgents(value) {
65
- const agents = parseAllowedAgents(value);
66
- return agents.length ? JSON.stringify(agents) : null;
67
- }
68
-
69
- function stringifyRouteSteps(value) {
70
- const steps = parseRouteSteps(value);
71
- return steps.length ? JSON.stringify(steps) : null;
72
- }
73
-
74
- function sshTarget(route) {
75
- const entrypoint = cleanText(route.entrypoint);
76
- if (!entrypoint) return "";
77
- if (/^ssh\s+/i.test(entrypoint) || /^https?:\/\//i.test(entrypoint) || entrypoint.includes("@")) return entrypoint;
78
- const account = cleanText(route.account_hint);
79
- return account ? `${account}@${entrypoint}` : entrypoint;
80
- }
81
-
82
- function jumpTarget(route) {
83
- const host = cleanText(route.jump_host);
84
- if (!host) return "";
85
- if (host.includes("@")) return host;
86
- const user = cleanText(route.jump_user);
87
- return user ? `${user}@${host}` : host;
88
- }
89
-
90
- function buildCanonicalCommand(route) {
91
- if (cleanText(route.canonical_command)) return cleanText(route.canonical_command);
92
- const kind = normalizeAccessKind(route.access_kind);
93
- const routeKind = normalizeRouteKind(route.route_kind);
94
- const entrypoint = cleanText(route.entrypoint);
95
- const target = sshTarget(route);
96
-
97
- if (["ssh", "server", "shell"].includes(kind)) {
98
- if (/^ssh\s+/i.test(target)) return target;
99
- if (routeKind === "jump" && target && jumpTarget(route)) return `ssh -J ${jumpTarget(route)} ${target}`;
100
- if (routeKind === "proxy" && target && cleanText(route.proxy_command)) return `ssh -o ProxyCommand='${cleanText(route.proxy_command)}' ${target}`;
101
- if (routeKind === "tunnel" && target && cleanText(route.proxy_command)) return cleanText(route.proxy_command);
102
- if (routeKind === "direct" && target) return `ssh ${target}`;
103
- }
104
-
105
- if (routeKind === "proxy" && cleanText(route.proxy_command)) return cleanText(route.proxy_command);
106
- if (entrypoint) return entrypoint;
107
- return "";
108
- }
109
-
110
- function buildRouteSteps(route) {
111
- const explicit = parseRouteSteps(route.route_steps || route.route_steps_json);
112
- if (explicit.length) return explicit;
113
-
114
- const steps = ["Resolve this Mnemo access route before attempting the connection."];
115
- const routeKind = normalizeRouteKind(route.route_kind);
116
- if (!route.direct_allowed && routeKind !== "direct") {
117
- steps.push("Direct access is not allowed for this route.");
118
- }
119
- if (routeKind === "jump") {
120
- steps.push(`Use jump host: ${jumpTarget(route) || route.jump_host || "configured jump host"}.`);
121
- } else if (routeKind === "proxy") {
122
- steps.push("Use the configured proxy command.");
123
- } else if (routeKind === "vpn" || routeKind === "tunnel") {
124
- steps.push(`Use the configured ${routeKind} path before touching the entrypoint.`);
125
- }
126
- const command = buildCanonicalCommand(route);
127
- if (command) steps.push(`Canonical command: ${command}`);
128
- if (route.secret_ref) steps.push(`Secret reference only: ${route.secret_ref}`);
129
- if (route.jump_secret_ref) steps.push(`Jump secret reference only: ${route.jump_secret_ref}`);
130
- return steps;
131
- }
132
-
133
- function routeFromRow(row) {
134
- if (!row) return null;
135
- const routeKind = normalizeRouteKind(row.route_kind || "direct");
136
- const directAllowed = boolFlag(row.direct_allowed, routeKind === "direct");
137
- const route = {
138
- id: row.id,
139
- access_id: row.id,
140
- scope: row.scope || DEFAULT_SCOPE,
141
- project: row.project || null,
142
- system_name: row.system_name,
143
- access_kind: row.access_kind,
144
- entrypoint: row.entrypoint || "",
145
- account_hint: row.account_hint || null,
146
- secret_ref: row.secret_ref || null,
147
- allowed_agents: parseAllowedAgents(row.allowed_agents),
148
- status: row.status || "active",
149
- route_kind: routeKind,
150
- direct_allowed: directAllowed,
151
- jump_host: row.jump_host || null,
152
- jump_user: row.jump_user || null,
153
- jump_secret_ref: row.jump_secret_ref || null,
154
- proxy_command: row.proxy_command || null,
155
- canonical_command: row.canonical_command || null,
156
- route_steps: parseRouteSteps(row.route_steps_json),
157
- preflight_required: boolFlag(row.preflight_required, true),
158
- last_route_check_at: row.last_route_check_at || null,
159
- last_verified_at: row.last_verified_at || null,
160
- verification_method: row.verification_method || null,
161
- notes: row.notes || null,
162
- updated_by: row.updated_by || null,
163
- updated_at: row.updated_at || null,
164
- created_at: row.created_at || null,
165
- };
166
- route.canonical_command = buildCanonicalCommand(route);
167
- route.route_steps = buildRouteSteps(route);
168
- return route;
169
- }
170
-
171
- function ensureAccessRouteSchema(db) {
172
- db.exec(`
173
- CREATE TABLE IF NOT EXISTS access_inventory (
174
- id INTEGER PRIMARY KEY AUTOINCREMENT,
175
- scope TEXT NOT NULL DEFAULT 'default',
176
- project TEXT,
177
- system_name TEXT NOT NULL,
178
- access_kind TEXT NOT NULL,
179
- entrypoint TEXT,
180
- account_hint TEXT,
181
- secret_ref TEXT,
182
- allowed_agents TEXT,
183
- status TEXT NOT NULL DEFAULT 'active',
184
- route_kind TEXT NOT NULL DEFAULT 'direct',
185
- direct_allowed INTEGER NOT NULL DEFAULT 1,
186
- jump_host TEXT,
187
- jump_user TEXT,
188
- jump_secret_ref TEXT,
189
- proxy_command TEXT,
190
- canonical_command TEXT,
191
- route_steps_json TEXT,
192
- preflight_required INTEGER NOT NULL DEFAULT 1,
193
- last_route_check_at TEXT,
194
- last_verified_at TEXT,
195
- verification_method TEXT,
196
- notes TEXT,
197
- updated_by TEXT,
198
- updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
199
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
200
- UNIQUE(scope, system_name, access_kind, entrypoint)
201
- );
202
- CREATE INDEX IF NOT EXISTS idx_access_project ON access_inventory(project, status);
203
- CREATE INDEX IF NOT EXISTS idx_access_system ON access_inventory(system_name, access_kind);
204
- CREATE INDEX IF NOT EXISTS idx_access_status ON access_inventory(status, updated_at DESC);
205
-
206
- CREATE TABLE IF NOT EXISTS access_event (
207
- id INTEGER PRIMARY KEY AUTOINCREMENT,
208
- access_id INTEGER REFERENCES access_inventory(id) ON DELETE SET NULL,
209
- event_kind TEXT NOT NULL,
210
- actor TEXT,
211
- status TEXT,
212
- notes TEXT,
213
- meta_json TEXT,
214
- occurred_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
215
- );
216
- CREATE INDEX IF NOT EXISTS idx_access_event_access ON access_event(access_id, occurred_at DESC);
217
- CREATE INDEX IF NOT EXISTS idx_access_event_actor ON access_event(actor, occurred_at DESC);
218
- `);
219
-
220
- const cols = new Set(db.prepare("PRAGMA table_info(access_inventory)").all().map((c) => c.name));
221
- for (const [name, ddl] of ROUTE_COLUMNS) {
222
- if (!cols.has(name)) db.exec(`ALTER TABLE access_inventory ADD COLUMN ${name} ${ddl}`);
223
- }
224
- db.exec("CREATE INDEX IF NOT EXISTS idx_access_route_kind ON access_inventory(route_kind, direct_allowed, status)");
225
- }
226
-
227
- function allowedForAgent(route, agentName) {
228
- if (!route.allowed_agents || route.allowed_agents.length === 0) return true;
229
- const agent = String(agentName || DEFAULT_AGENT).trim().toLowerCase();
230
- return route.allowed_agents.some((name) => String(name || "").trim().toLowerCase() === agent);
231
- }
232
-
233
- function logAccessEvent(db, routeId, eventKind, actor, status, notes, meta) {
234
- try {
235
- db.prepare("INSERT INTO access_event (access_id, event_kind, actor, status, notes, meta_json) VALUES (?,?,?,?,?,?)")
236
- .run(routeId || null, eventKind, actor || DEFAULT_AGENT, status || null, notes || null, meta ? JSON.stringify(meta) : null);
237
- } catch {}
238
- }
239
-
240
- function upsertAccessRoute(db, input = {}) {
241
- ensureAccessRouteSchema(db);
242
- if (!input.system_name || !input.access_kind) return { ok: false, error: "system_name + access_kind required" };
243
-
244
- const scope = scopeName(input.scope);
245
- const entrypoint = cleanText(input.entrypoint) || "";
246
- const actor = input.updated_by || input.agent_name || DEFAULT_AGENT;
247
- const routeKindProvided = input.route_kind !== undefined || !!(input.meta && input.meta.route_kind !== undefined);
248
- const directAllowedProvided = input.direct_allowed !== undefined || !!(input.meta && input.meta.direct_allowed !== undefined);
249
- const routeKind = normalizeRouteKind(input.route_kind || (input.meta && input.meta.route_kind) || "direct");
250
- const directAllowed = input.direct_allowed === undefined && input.meta && input.meta.direct_allowed !== undefined
251
- ? boolFlag(input.meta.direct_allowed, routeKind === "direct")
252
- : boolFlag(input.direct_allowed, routeKind === "direct");
253
- const allowed = stringifyAllowedAgents(input.allowed_agents);
254
- const routeStepsProvided = input.route_steps !== undefined || input.route_steps_json !== undefined;
255
- const routeSteps = stringifyRouteSteps(input.route_steps || input.route_steps_json);
256
-
257
- const existing = db.prepare(
258
- "SELECT id FROM access_inventory WHERE scope=? AND system_name=? AND access_kind=? AND COALESCE(entrypoint,'')=?"
259
- ).get(scope, input.system_name, input.access_kind, entrypoint);
260
-
261
- let id;
262
- if (existing) {
263
- id = existing.id;
264
- db.prepare(
265
- "UPDATE access_inventory SET project=?, entrypoint=?, account_hint=?, secret_ref=?, allowed_agents=?, status=?, " +
266
- "route_kind=COALESCE(?, route_kind), direct_allowed=COALESCE(?, direct_allowed), jump_host=COALESCE(?, jump_host), jump_user=COALESCE(?, jump_user), jump_secret_ref=COALESCE(?, jump_secret_ref), proxy_command=COALESCE(?, proxy_command), canonical_command=COALESCE(?, canonical_command), route_steps_json=COALESCE(?, route_steps_json), preflight_required=COALESCE(?, preflight_required), " +
267
- "last_verified_at=COALESCE(?, last_verified_at), last_route_check_at=COALESCE(?, last_route_check_at), verification_method=COALESCE(?, verification_method), notes=COALESCE(?, notes), updated_by=?, updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id=?"
268
- ).run(
269
- input.project || null,
270
- entrypoint,
271
- input.account_hint || null,
272
- input.secret_ref || null,
273
- allowed,
274
- input.status || "active",
275
- routeKindProvided ? routeKind : null,
276
- directAllowedProvided ? (directAllowed ? 1 : 0) : null,
277
- input.jump_host || null,
278
- input.jump_user || null,
279
- input.jump_secret_ref || null,
280
- input.proxy_command || null,
281
- input.canonical_command || null,
282
- routeStepsProvided ? routeSteps : null,
283
- input.preflight_required !== undefined ? (boolFlag(input.preflight_required, true) ? 1 : 0) : null,
284
- input.last_verified_at || null,
285
- input.last_route_check_at || null,
286
- input.verification_method || null,
287
- input.notes || null,
288
- actor,
289
- id
290
- );
291
- } else {
292
- const info = db.prepare(
293
- "INSERT INTO access_inventory (scope, project, system_name, access_kind, entrypoint, account_hint, secret_ref, allowed_agents, status, route_kind, direct_allowed, jump_host, jump_user, jump_secret_ref, proxy_command, canonical_command, route_steps_json, preflight_required, last_verified_at, last_route_check_at, verification_method, notes, updated_by) " +
294
- "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
295
- ).run(
296
- scope,
297
- input.project || null,
298
- input.system_name,
299
- input.access_kind,
300
- entrypoint,
301
- input.account_hint || null,
302
- input.secret_ref || null,
303
- allowed,
304
- input.status || "active",
305
- routeKind,
306
- directAllowed ? 1 : 0,
307
- input.jump_host || null,
308
- input.jump_user || null,
309
- input.jump_secret_ref || null,
310
- input.proxy_command || null,
311
- input.canonical_command || null,
312
- routeSteps,
313
- boolFlag(input.preflight_required, true) ? 1 : 0,
314
- input.last_verified_at || null,
315
- input.last_route_check_at || null,
316
- input.verification_method || null,
317
- input.notes || null,
318
- actor
319
- );
320
- id = info.lastInsertRowid;
321
- }
322
-
323
- logAccessEvent(db, id, existing ? "updated" : "created", actor, input.status || "active", input.notes || null, {
324
- route_kind: routeKind,
325
- direct_allowed: directAllowed,
326
- secret_ref: input.secret_ref || null,
327
- jump_secret_ref: input.jump_secret_ref || null,
328
- meta: input.meta || null,
329
- });
330
-
331
- const row = db.prepare("SELECT * FROM access_inventory WHERE id=?").get(id);
332
- return {
333
- ok: true,
334
- id,
335
- status: existing ? "updated" : "created",
336
- secret_stored: false,
337
- secret_ref: input.secret_ref || null,
338
- route: routeFromRow(row),
339
- };
340
- }
341
-
342
- function buildWhere(input = {}) {
343
- const where = [];
344
- const params = [];
345
- if (input.scope) { where.push("LOWER(COALESCE(scope,''))=?"); params.push(scopeName(input.scope)); }
346
- if (input.project) { where.push("project=?"); params.push(input.project); }
347
- if (input.system_name) {
348
- where.push("(system_name LIKE ? OR entrypoint LIKE ?)");
349
- params.push(`%${input.system_name}%`, `%${input.system_name}%`);
350
- }
351
- if (input.access_kind) { where.push("access_kind=?"); params.push(input.access_kind); }
352
- if (input.entrypoint) { where.push("entrypoint LIKE ?"); params.push(`%${input.entrypoint}%`); }
353
- if (input.route_kind) { where.push("route_kind=?"); params.push(normalizeRouteKind(input.route_kind)); }
354
- if (input.direct_allowed !== undefined) { where.push("direct_allowed=?"); params.push(boolFlag(input.direct_allowed, false) ? 1 : 0); }
355
- if (input.status) { where.push("status=?"); params.push(input.status); }
356
- else if (!input.include_inactive) { where.push("status IN ('active','verified','ok','fresh','observed')"); }
357
- return { where, params };
358
- }
359
-
360
- function listAccessRoutes(db, input = {}) {
361
- ensureAccessRouteSchema(db);
362
- let rows = queryAccessRows(db, input);
363
- let usedScopeFallback = false;
364
- const hasSpecificFilter = !!(input.project || input.system_name || input.access_kind || input.entrypoint || input.route_kind);
365
- if (!rows.length && input.scope && hasSpecificFilter) {
366
- rows = queryAccessRows(db, Object.assign({}, input, { scope: null }));
367
- usedScopeFallback = rows.length > 0;
368
- }
369
- return {
370
- count: rows.length,
371
- access: rows.map(routeFromRow),
372
- scope_fallback: usedScopeFallback,
373
- };
374
- }
375
-
376
- function queryAccessRows(db, input = {}) {
377
- const { where, params } = buildWhere(input);
378
- params.push(Math.min(input.limit || 50, 300));
379
- return db.prepare(
380
- "SELECT * FROM access_inventory" +
381
- (where.length ? " WHERE " + where.join(" AND ") : "") +
382
- " ORDER BY COALESCE(last_verified_at, updated_at) DESC LIMIT ?"
383
- ).all(...params);
384
- }
385
-
386
- function routeScore(route, input = {}) {
387
- let score = 0;
388
- if (input.system_name && String(route.system_name).toLowerCase() === String(input.system_name).toLowerCase()) score += 40;
389
- if (input.access_kind && String(route.access_kind).toLowerCase() === String(input.access_kind).toLowerCase()) score += 20;
390
- if (input.entrypoint && String(route.entrypoint || "").toLowerCase() === String(input.entrypoint).toLowerCase()) score += 20;
391
- if (route.status === "active") score += 15;
392
- if (route.last_verified_at) score += 10;
393
- if (route.scope === scopeName(input.scope)) score += 5;
394
- return score;
395
- }
396
-
397
- function selectCandidateRoutes(db, input = {}) {
398
- const where = [];
399
- const params = [];
400
- if (input.scope) { where.push("LOWER(COALESCE(scope,''))=?"); params.push(scopeName(input.scope)); }
401
- if (input.project) { where.push("project=?"); params.push(input.project); }
402
- if (input.system_name) {
403
- where.push("(LOWER(system_name)=LOWER(?) OR system_name LIKE ? OR entrypoint LIKE ?)");
404
- params.push(input.system_name, `%${input.system_name}%`, `%${input.system_name}%`);
405
- }
406
- if (input.access_kind) { where.push("access_kind=?"); params.push(input.access_kind); }
407
- if (input.entrypoint) { where.push("(entrypoint=? OR entrypoint LIKE ?)"); params.push(input.entrypoint, `%${input.entrypoint}%`); }
408
- if (!input.include_inactive) { where.push("status IN ('active','verified','ok')"); }
409
- params.push(Math.min(input.limit || 20, 100));
410
- const rows = db.prepare(
411
- "SELECT * FROM access_inventory" +
412
- (where.length ? " WHERE " + where.join(" AND ") : "") +
413
- " ORDER BY COALESCE(last_verified_at, updated_at) DESC LIMIT ?"
414
- ).all(...params).map(routeFromRow);
415
- return rows.sort((a, b) => routeScore(b, input) - routeScore(a, input));
416
- }
417
-
418
- function intendedLooksDirect(input = {}, route) {
419
- const rawKind = input.intended_route_kind || input.route_kind || "";
420
- if (rawKind && normalizeRouteKind(rawKind) === "direct") return true;
421
- const command = String(input.intended_command || "").trim();
422
- if (!command) return false;
423
- if (/(^|\s)-J(\s|=|$)/.test(command) || /\bProxyCommand\b/i.test(command) || /\bproxy\b/i.test(command)) return false;
424
- if (route.route_kind !== "direct" && /\bssh\b/i.test(command)) return true;
425
- const intendedEntry = String(input.intended_entrypoint || input.entrypoint || "").trim();
426
- if (intendedEntry && route.entrypoint && intendedEntry === route.entrypoint && route.route_kind !== "direct") return true;
427
- return false;
428
- }
429
-
430
- function resolveAccessRoute(db, input = {}) {
431
- ensureAccessRouteSchema(db);
432
- if (!input.system_name && !input.project && !input.entrypoint) {
433
- return {
434
- ok: false,
435
- status: "block",
436
- error: "system_name_or_project_or_entrypoint_required",
437
- message: "Call mem_access_route_resolve with at least system_name, project, or entrypoint before attempting access.",
438
- };
439
- }
440
-
441
- let candidates = selectCandidateRoutes(db, input);
442
- let usedScopeFallback = false;
443
- if (!candidates.length && input.scope) {
444
- candidates = selectCandidateRoutes(db, Object.assign({}, input, { scope: null }));
445
- usedScopeFallback = candidates.length > 0;
446
- }
447
- if (!candidates.length) {
448
- return {
449
- ok: false,
450
- status: "block",
451
- error: "access_route_missing",
452
- message: "No canonical access route is stored for this target. Do not improvise direct access; add or verify the route first with mem_access_upsert.",
453
- query: {
454
- scope: input.scope || null,
455
- project: input.project || null,
456
- system_name: input.system_name || null,
457
- access_kind: input.access_kind || null,
458
- entrypoint: input.entrypoint || input.intended_entrypoint || null,
459
- },
460
- next_step: "Use mem_access_upsert with route_kind, direct_allowed, jump/proxy fields, secret_ref labels, and verification evidence.",
461
- };
462
- }
463
-
464
- const route = candidates[0];
465
- const agent = input.agent_name || input.actor || DEFAULT_AGENT;
466
- if (!allowedForAgent(route, agent)) {
467
- return {
468
- ok: false,
469
- status: "block",
470
- error: "agent_not_allowed_for_access_route",
471
- message: `${agent} is not listed in allowed_agents for this access route.`,
472
- route,
473
- scope_fallback: usedScopeFallback,
474
- allowed_agents: route.allowed_agents,
475
- };
476
- }
477
-
478
- if (!route.direct_allowed && intendedLooksDirect(input, route)) {
479
- return {
480
- ok: false,
481
- status: "block",
482
- error: "direct_access_blocked_use_canonical_route",
483
- message: "This target is reachable only through the canonical stored route. Do not try direct access first.",
484
- route,
485
- scope_fallback: usedScopeFallback,
486
- must_use: {
487
- route_kind: route.route_kind,
488
- direct_allowed: route.direct_allowed,
489
- canonical_command: route.canonical_command,
490
- route_steps: route.route_steps,
491
- },
492
- };
493
- }
494
-
495
- return {
496
- ok: true,
497
- status: "ok",
498
- route,
499
- candidates: candidates.slice(0, 5),
500
- scope_fallback: usedScopeFallback,
501
- must_use: {
502
- route_kind: route.route_kind,
503
- direct_allowed: route.direct_allowed,
504
- canonical_command: route.canonical_command,
505
- route_steps: route.route_steps,
506
- },
507
- };
508
- }
509
-
510
- function preflightAccessRoute(db, input = {}) {
511
- const result = resolveAccessRoute(db, input);
512
- const routeId = result.route && result.route.id ? result.route.id : null;
513
- const actor = input.agent_name || input.actor || DEFAULT_AGENT;
514
- const status = result.ok ? "allowed" : "blocked";
515
- const notes = result.ok
516
- ? `preflight allowed for ${result.route.system_name} (${result.route.route_kind})`
517
- : `preflight blocked: ${result.error || "unknown"}`;
518
- logAccessEvent(db, routeId, result.ok ? "preflight_allowed" : "preflight_blocked", actor, status, notes, {
519
- system_name: input.system_name || null,
520
- access_kind: input.access_kind || null,
521
- intended_route_kind: input.intended_route_kind || null,
522
- intended_entrypoint: input.intended_entrypoint || input.entrypoint || null,
523
- intended_command: input.intended_command || null,
524
- result_error: result.error || null,
525
- });
526
- if (routeId) {
527
- try {
528
- db.prepare("UPDATE access_inventory SET last_route_check_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id=?").run(routeId);
529
- } catch {}
530
- }
531
- return Object.assign({}, result, { preflight_logged: true, preflight_at: isoNow() });
532
- }
533
-
534
- function accessGuide(db, input = {}) {
535
- const listed = listAccessRoutes(db, Object.assign({}, input, { limit: input.limit || 100 }));
536
- const grouped = new Map();
537
- for (const route of listed.access) {
538
- const key = `${route.project || "_"}::${route.system_name}`;
539
- if (!grouped.has(key)) {
540
- grouped.set(key, {
541
- project: route.project || null,
542
- system_name: route.system_name,
543
- status: route.status,
544
- last_verified_at: route.last_verified_at || null,
545
- notes: route.notes || null,
546
- routes: [],
547
- });
548
- }
549
- grouped.get(key).routes.push(route);
550
- }
551
- const systems = Array.from(grouped.values());
552
- const projectNames = Array.from(new Set(systems.map((row) => row.project).filter(Boolean)));
553
- const registry = {};
554
- try {
555
- if (input.project) {
556
- const row = db.prepare("SELECT name, domain, repo, server, pm2_processes, nginx_files, admin_url, auth_system, live_status, live_url, staging_url, updated_at, updated_by FROM project_registry WHERE name=?").get(input.project);
557
- if (row) registry[input.project] = normalizeRegistryRow(row);
558
- } else if (projectNames.length) {
559
- const placeholders = projectNames.map(() => "?").join(",");
560
- const rows = db.prepare("SELECT name, domain, repo, server, pm2_processes, nginx_files, admin_url, auth_system, live_status, live_url, staging_url, updated_at, updated_by FROM project_registry WHERE name IN (" + placeholders + ")").all(...projectNames);
561
- for (const row of rows) registry[row.name] = normalizeRegistryRow(row);
562
- }
563
- } catch {}
564
- const lines = ["# Access Guide"];
565
- if (input.project) lines.push(`Project: ${input.project}`);
566
- if (input.system_name) lines.push(`System search: ${input.system_name}`);
567
- lines.push("");
568
- if (input.project && registry[input.project]) {
569
- const reg = registry[input.project];
570
- lines.push("## Project Registry");
571
- if (reg.domain) lines.push(`- Domain: ${reg.domain}`);
572
- if (reg.live_url) lines.push(`- Live URL: ${reg.live_url}`);
573
- if (reg.staging_url) lines.push(`- Staging URL: ${reg.staging_url}`);
574
- if (reg.repo) lines.push(`- Repo: ${reg.repo}`);
575
- if (reg.server) lines.push(`- Server: ${reg.server}`);
576
- if (reg.admin_url) lines.push(`- Admin URL: ${reg.admin_url}`);
577
- if (reg.auth_system) lines.push(`- Auth system: ${reg.auth_system}`);
578
- if (Array.isArray(reg.pm2_processes) && reg.pm2_processes.length) lines.push(`- PM2: ${reg.pm2_processes.join(", ")}`);
579
- if (Array.isArray(reg.nginx_files) && reg.nginx_files.length) lines.push(`- Nginx: ${reg.nginx_files.join(", ")}`);
580
- lines.push("");
581
- }
582
- for (const system of systems) {
583
- lines.push(`## ${system.system_name}`);
584
- if (system.project) lines.push(`- Project: ${system.project}`);
585
- if (system.status) lines.push(`- Status: ${system.status}`);
586
- if (system.last_verified_at) lines.push(`- Last verified: ${system.last_verified_at}`);
587
- if (system.notes) lines.push(`- Notes: ${system.notes}`);
588
- for (const route of system.routes) {
589
- const parts = [
590
- route.access_kind,
591
- `route=${route.route_kind}`,
592
- route.direct_allowed ? "direct_allowed=yes" : "direct_allowed=no",
593
- route.entrypoint ? `entrypoint=${route.entrypoint}` : null,
594
- route.jump_host ? `jump=${jumpTarget(route)}` : null,
595
- route.secret_ref ? `secret_ref=${route.secret_ref}` : null,
596
- route.canonical_command ? `canonical=${route.canonical_command}` : null,
597
- route.allowed_agents && route.allowed_agents.length ? `agents=${route.allowed_agents.join(",")}` : null,
598
- ].filter(Boolean);
599
- lines.push(`- ${parts.join(" | ")}`);
600
- }
601
- lines.push("");
602
- }
603
- if (!systems.length) lines.push("_No access routes found. Add them with mem_access_upsert before attempting access._");
604
- return { count: listed.count, systems, registry, guide_markdown: lines.join("\n") };
605
- }
606
-
607
- function normalizeRegistryRow(row) {
608
- const out = Object.assign({}, row);
609
- for (const key of ["pm2_processes", "nginx_files"]) {
610
- try { out[key] = out[key] ? JSON.parse(out[key]) : []; } catch { out[key] = []; }
611
- }
612
- return out;
613
- }
614
-
615
- const routeInputProperties = {
616
- scope: { type: "string" },
617
- project: { type: "string" },
618
- system_name: { type: "string" },
619
- access_kind: { type: "string" },
620
- entrypoint: { type: "string" },
621
- intended_entrypoint: { type: "string" },
622
- intended_command: { type: "string" },
623
- intended_route_kind: { type: "string" },
624
- route_kind: { type: "string", description: "direct | jump | proxy | vpn | tunnel | manual" },
625
- direct_allowed: { type: "boolean" },
626
- account_hint: { type: "string" },
627
- secret_ref: { type: "string" },
628
- allowed_agents: { oneOf: [{ type: "array", items: { type: "string" } }, { type: "string" }] },
629
- jump_host: { type: "string" },
630
- jump_user: { type: "string" },
631
- jump_secret_ref: { type: "string" },
632
- proxy_command: { type: "string" },
633
- canonical_command: { type: "string" },
634
- route_steps: { oneOf: [{ type: "array", items: { type: "string" } }, { type: "string" }] },
635
- preflight_required: { type: "boolean" },
636
- status: { type: "string" },
637
- last_verified_at: { type: "string" },
638
- last_route_check_at: { type: "string" },
639
- verification_method: { type: "string" },
640
- notes: { type: "string" },
641
- updated_by: { type: "string" },
642
- agent_name: { type: "string" },
643
- actor: { type: "string" },
644
- limit: { type: "integer" },
645
- include_inactive: { type: "boolean" },
646
- meta: { type: "object" },
647
- };
648
-
649
- const ACCESS_ROUTE_TOOL_DEFS = {
650
- mem_access_upsert: {
651
- description: "Create/update the canonical access route for a server/admin/repo/API/DB. Store route_kind, jump/proxy details, direct_allowed, canonical_command, and secret_ref labels only; never raw secrets.",
652
- inputSchema: { type: "object", properties: routeInputProperties, required: ["system_name", "access_kind"] },
653
- },
654
- mem_access_list: {
655
- description: "List canonical access routes including jump/proxy/direct policy. Returns secret references, never raw secrets.",
656
- inputSchema: { type: "object", properties: routeInputProperties },
657
- },
658
- mem_access_guide: {
659
- description: "Render the fixed access guide. Agents must read this or preflight before touching servers, repos, APIs, dashboards, databases, or providers.",
660
- inputSchema: { type: "object", properties: routeInputProperties },
661
- },
662
- mem_access_route_resolve: {
663
- description: "Resolve the canonical route before access. Blocks missing routes, unauthorized agents, and direct attempts when direct_allowed=false.",
664
- inputSchema: { type: "object", properties: routeInputProperties },
665
- },
666
- mem_access_preflight: {
667
- description: "Mandatory preflight before SSH/API/DB/admin/provider access. Logs allowed/blocked evidence and returns the canonical command/steps.",
668
- inputSchema: { type: "object", properties: routeInputProperties },
669
- },
670
- };
671
-
672
- function handleAccessRouteTool(db, name, args = {}) {
673
- if (name === "mem_access_upsert") return { handled: true, result: upsertAccessRoute(db, args) };
674
- if (name === "mem_access_list") return { handled: true, result: listAccessRoutes(db, args) };
675
- if (name === "mem_access_guide") return { handled: true, result: accessGuide(db, args) };
676
- if (name === "mem_access_route_resolve") return { handled: true, result: resolveAccessRoute(db, args) };
677
- if (name === "mem_access_preflight") return { handled: true, result: preflightAccessRoute(db, args) };
678
- return { handled: false };
679
- }
680
-
681
- module.exports = {
682
- ACCESS_ROUTE_TOOL_DEFS,
683
- ensureAccessRouteSchema,
684
- handleAccessRouteTool,
685
- upsertAccessRoute,
686
- listAccessRoutes,
687
- resolveAccessRoute,
688
- preflightAccessRoute,
689
- routeFromRow,
690
- buildCanonicalCommand,
691
- normalizeRouteKind,
692
- };