total-recall-brain 3.41.1 → 3.41.3

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 (105) hide show
  1. package/bin/total-recall.mjs +9 -3
  2. package/frontend/dist/assets/{index-BG9NkVUy.css → index-BIQmqDKP.css} +1 -1
  3. package/frontend/dist/assets/{index-BtuiZqwP.js → index-DFenQQEQ.js} +74 -79
  4. package/frontend/dist/index.html +2 -2
  5. package/frontend/dist/sw.js +8 -5
  6. package/frontend/src/api/auth.ts +0 -3
  7. package/frontend/src/components/plugins/PluginPanel.tsx +7 -3
  8. package/frontend/src/components/plugins/card/CardEngine.tsx +1 -1
  9. package/frontend/src/pages/LoginPage.tsx +1 -41
  10. package/frontend/src/pages/PluginsPage.tsx +14 -7
  11. package/package.json +2 -2
  12. package/scaffold/.agent/skills/plugin/scripts/record.mjs +6 -3
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +10 -0
  14. package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +197 -31
  15. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +1456 -12
  16. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +4 -4
  17. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +8 -1
  18. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +210 -27
  19. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +10 -1
  20. package/scaffold/.agent/skills/total-recall/plugins/auth/src/rotation-capability.mjs +15 -0
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +78 -6
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-merge.mjs +131 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +111 -2
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +26 -5
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +56 -8
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +1068 -204
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/panel.js +4 -2
  28. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +32 -0
  29. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +436 -274
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +16 -2
  33. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +461 -242
  34. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  35. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  36. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -7
  37. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +17 -2
  38. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +426 -300
  39. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  40. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +32 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +350 -283
  42. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
  43. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +31 -19
  44. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/mesh/{src/core/headscale-server.mjs → capability/control-server.mjs} +5 -2
  46. package/scaffold/.agent/skills/total-recall/plugins/mesh/capability/headscale-api.mjs +192 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/mesh/capability/index.mjs +46 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +9 -1
  49. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -22
  50. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +6 -3
  51. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +242 -8
  52. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +14 -9
  53. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/server.mjs +1 -1
  54. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/credentials.mjs +4 -2
  55. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/headscale-client.mjs +34 -181
  56. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-auth.mjs +2 -2
  57. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/presence.mjs +5 -5
  58. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/screen-view.mjs +78 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/DESIGN.md +211 -12
  60. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +323 -388
  61. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +12 -1
  62. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +7 -4
  63. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
  64. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
  65. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +34 -1
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/bootstrap.mjs +99 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/README.md +10 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/architecture.md +5 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/ide.md +10 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/memory.md +11 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/secrets.md +7 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/skills.md +7 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +16 -2
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +380 -298
  75. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -1
  76. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +6 -6
  77. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  78. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  79. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +17 -3
  80. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +38 -15
  81. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +60 -34
  82. package/scaffold/.agent/skills/total-recall/references/auth.md +2 -0
  83. package/src/cli/command.mjs +23 -18
  84. package/src/cli/daemon.mjs +23 -1
  85. package/src/cli/plugin/copies.mjs +8 -5
  86. package/src/cli/plugin/index.mjs +25 -2
  87. package/src/cli/rebuild.mjs +22 -0
  88. package/src/cli/reset-password.mjs +3 -2
  89. package/src/cli/skill.mjs +8 -2
  90. package/src/cli/status.mjs +25 -7
  91. package/src/core/automatic-brain-updates.mjs +9 -3
  92. package/src/core/brain-updates.mjs +63 -5
  93. package/src/core/global-brain-updates.mjs +21 -1
  94. package/src/core/mesh-brain-updates.mjs +5 -2
  95. package/src/core/meta-harness.mjs +16 -41
  96. package/src/core/plugin-runner.mjs +6 -1
  97. package/src/core/plugin-store.mjs +67 -1
  98. package/src/core/plugin-updates.mjs +1 -1
  99. package/src/core/portable-skills-maintenance.mjs +19 -0
  100. package/src/core/required-plugins.mjs +7 -0
  101. package/src/core/skills-registry.mjs +33 -3
  102. package/src/server/auth.mjs +41 -1
  103. package/src/server/memory-app.mjs +2 -1
  104. package/src/server/routes/auth.mjs +25 -1
  105. package/src/server/routes/plugins.mjs +17 -4
@@ -9,7 +9,7 @@
9
9
  "version": "1.1.2",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
- "total-recall-brain": "3.40.7",
12
+ "total-recall-brain": "3.41.2",
13
13
  "vitest": "^4.1.10"
14
14
  }
15
15
  },
@@ -3560,9 +3560,9 @@
3560
3560
  }
3561
3561
  },
3562
3562
  "node_modules/total-recall-brain": {
3563
- "version": "3.40.7",
3564
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.40.7.tgz",
3565
- "integrity": "sha512-G+sEWXVucI5Zs8uV1ie8P+mV/6cncWX76ES72T5dnPB3sFAwlcG9ous2SYTUaYUOETGB2vHTSo3cV4XWkHQ0Ww==",
3563
+ "version": "3.41.2",
3564
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.41.2.tgz",
3565
+ "integrity": "sha512-bwJFAFAyvk9ysf+5zhBzMxrKus1Hj1AygUWdBNt+vQbM2sfSEf6701sa30KwfGyU8+cMuN6YNT5Mn63mFZIi/w==",
3566
3566
  "dev": true,
3567
3567
  "license": "MIT",
3568
3568
  "workspaces": [
@@ -410,14 +410,26 @@ export class GitSentinelCard extends HTMLElement {
410
410
  --face-ok: #047857;
411
411
  --face-warn: #b45309;
412
412
  --face-danger: #b91c1c;
413
+ --face-text-default: #111827;
414
+ --face-muted-default: #6b7280;
415
+ --face-line-default: #e5e7eb;
416
+ --face-surface-default: #f9fafb;
417
+ --face-accent-default: #b45309;
418
+ --face-dark-ok: #34d399;
419
+ --face-dark-warn: #fbbf24;
420
+ --face-dark-danger: #f87171;
421
+ --face-dark-text: #e8eaf0;
422
+ --face-dark-muted: #a2a8b8;
423
+ --face-dark-line: #2a3040;
424
+ --face-dark-surface: #171a23;
413
425
  --face-radius: 6px;
414
426
  --face-motion: 140ms ease-out;
415
427
  --face-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
416
- --face-text: var(--text-primary, #111827);
417
- --face-muted: var(--text-secondary, #6b7280);
418
- --face-line: var(--border, #e5e7eb);
419
- --face-surface: var(--bg-secondary, #f9fafb);
420
- --face-accent: var(--card-color, #b45309);
428
+ --face-text: var(--text-primary, var(--face-text-default));
429
+ --face-muted: var(--text-secondary, var(--face-muted-default));
430
+ --face-line: var(--border, var(--face-line-default));
431
+ --face-surface: var(--bg-secondary, var(--face-surface-default));
432
+ --face-accent: var(--card-color, var(--face-accent-default));
421
433
  font-family: 'Space Grotesk', var(--font-sans, system-ui, sans-serif);
422
434
  color: var(--face-text);
423
435
  }
@@ -425,25 +437,25 @@ export class GitSentinelCard extends HTMLElement {
425
437
  verdict line, so they are retuned whenever the host is dark. */
426
438
  @media (prefers-color-scheme: dark) {
427
439
  :host {
428
- --face-ok: #34d399;
429
- --face-warn: #fbbf24;
430
- --face-danger: #f87171;
431
- --face-text: var(--text-primary, #e8eaf0);
432
- --face-muted: var(--text-secondary, #a2a8b8);
433
- --face-line: var(--border, #2a3040);
434
- --face-surface: var(--bg-secondary, #171a23);
440
+ --face-ok: var(--face-dark-ok);
441
+ --face-warn: var(--face-dark-warn);
442
+ --face-danger: var(--face-dark-danger);
443
+ --face-text: var(--text-primary, var(--face-dark-text));
444
+ --face-muted: var(--text-secondary, var(--face-dark-muted));
445
+ --face-line: var(--border, var(--face-dark-line));
446
+ --face-surface: var(--bg-secondary, var(--face-dark-surface));
435
447
  }
436
448
  }
437
449
  :host-context([data-theme="dark"]),
438
450
  :host-context(.theme-dark),
439
451
  :host-context(.dark) {
440
- --face-ok: #34d399;
441
- --face-warn: #fbbf24;
442
- --face-danger: #f87171;
443
- --face-text: var(--text-primary, #e8eaf0);
444
- --face-muted: var(--text-secondary, #a2a8b8);
445
- --face-line: var(--border, #2a3040);
446
- --face-surface: var(--bg-secondary, #171a23);
452
+ --face-ok: var(--face-dark-ok);
453
+ --face-warn: var(--face-dark-warn);
454
+ --face-danger: var(--face-dark-danger);
455
+ --face-text: var(--text-primary, var(--face-dark-text));
456
+ --face-muted: var(--text-secondary, var(--face-dark-muted));
457
+ --face-line: var(--border, var(--face-dark-line));
458
+ --face-surface: var(--bg-secondary, var(--face-dark-surface));
447
459
  }
448
460
  * { box-sizing: border-box; }
449
461
  .top { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
@@ -45,3 +45,13 @@ This copies the plugin's source, skills, tests and docs into `bricks/mesh` and r
45
45
  **Use it directly with Total Recall (for your own machines and workflow).** Install it with `total-recall plugin install https://github.com/gregiteen/tr-plugin-mesh.git` and run `total-recall mesh`. Manage its credentials with `total-recall auth secret` and its skills with `total-recall skill`.
46
46
 
47
47
  Native-only state: new installations use validated SSSS records and the shared auth owner. Legacy imports and compatibility fallback formats are not supported.
48
+
49
+ ## Mesh screens and integration declarations
50
+
51
+ The card defines Machines, Flows, Placement, Boundary and Join. Machines separates control membership, path checks, recorded login and actual execution. Hardware cells retain their measurement dates; failed or untested runtime probes remain unknown. Placement filters confirmed capabilities and includes the integration matrix declared in `plugin.json.mesh`.
52
+
53
+ `total-recall mesh view <machines|flows|placement|boundary|join> --json` reads fixed owner commands. Each source carries its command, attempt time and either data or an explicit error. `mesh.provides` references declared operations/runtime modules; `mesh.integrations` identifies each owner and the meaning of its evidence; `mesh.endpoints` identifies served peer routes. These are declarations, not primitive grants or proof every provider action works. Read-only views never perform enrollment, policy changes, grant edits, deployments or secret transfers.
54
+
55
+ The dashboard supplies frozen `host.operations`. Read-only cards show CLI handoffs for login administration and re-enrollment. A writable host may expose those declared actions; re-enrollment then requires target-specific confirmation. Enrollment keys never enter the card.
56
+
57
+ Auth's `host-secrets-store` runtime supplies the single host-store selector for control credentials and mesh-sync authentication. Project `AGENT_DIR`/`TR_BRAIN` cannot divert these reads. Native SSSS audit contention is serialized by the generic host; errors still fail closed. The corresponding source runtime must be installed; missing modules never select a fallback implementation.
@@ -117,9 +117,12 @@ export async function provisionHeadscale(target, { exec = sshExec(target), fetch
117
117
  await must(exec('headscale configtest'), 'headscale configtest');
118
118
 
119
119
  log('start');
120
- await must(exec('systemctl enable --now headscale'), 'Starting Headscale');
120
+ // The package starts Headscale at install with the example config (loopback :8080);
121
+ // `enable --now` leaves that process running, so restart to load the written config.
122
+ await must(exec('systemctl enable headscale && systemctl restart headscale'), 'Starting Headscale');
121
123
  let healthy = false;
122
- for (let i = 0; i < 20 && !healthy; i++) {
124
+ // The first start also obtains the Let's Encrypt certificate, so allow it about a minute.
125
+ for (let i = 0; i < 40 && !healthy; i++) {
123
126
  healthy = (await exec(`curl -fsS https://${target.domain}/health`)).code === 0;
124
127
  if (!healthy) await wait(1500);
125
128
  }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Headscale control-server API, host-free.
3
+ *
4
+ * The caller supplies the control-server URL, the API key and the HTTP
5
+ * transport, so this module reads no credential store and no environment. The
6
+ * Total Recall host resolves the key from its auth plugin and passes it in;
7
+ * a vendored copy passes its own.
8
+ */
9
+
10
+ const DEFAULT_TIMEOUT_MS = 10_000;
11
+
12
+ /**
13
+ * Reject non-HTTPS control servers (loopback excepted for local development).
14
+ * Bearer tokens must never travel in cleartext over a network hop.
15
+ */
16
+ export function assertSecureControlUrl(url) {
17
+ const parsed = new URL(url);
18
+ const isLoopback = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
19
+ if (parsed.protocol !== 'https:' && !(isLoopback && parsed.protocol === 'http:')) {
20
+ throw new Error('Headscale URL must use HTTPS (HTTP is allowed only for loopback development)');
21
+ }
22
+ return parsed;
23
+ }
24
+
25
+ /** Strip a trailing slash so path joining stays predictable. */
26
+ export function normalizeControlUrl(url) {
27
+ const value = String(url || '').trim();
28
+ return value.endsWith('/') ? value.slice(0, -1) : value;
29
+ }
30
+
31
+ /** A fetch with a deadline, used when the caller supplies no transport. */
32
+ function timedFetch(fetchImpl) {
33
+ return async (url, options = {}) => {
34
+ const controller = new AbortController();
35
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs || DEFAULT_TIMEOUT_MS);
36
+ try {
37
+ return await fetchImpl(url, { ...options, signal: controller.signal });
38
+ } finally {
39
+ clearTimeout(timer);
40
+ }
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Bind a client to one control server.
46
+ * @param {{ url: string, token: string, fetch?: (url: string, options: object) => Promise<Response> }} options
47
+ * `fetch` receives `timeoutMs` in its options and is responsible for honouring it.
48
+ */
49
+ export function createHeadscaleApi({ url, token, fetch: transport } = {}) {
50
+ const base = normalizeControlUrl(url);
51
+ if (!base) throw new Error('A Headscale control server URL is required');
52
+ assertSecureControlUrl(base);
53
+ if (typeof token !== 'string' || !token) throw new Error('A Headscale API key is required');
54
+ const send = transport || timedFetch(globalThis.fetch);
55
+
56
+ /** Authenticated JSON request; a non-2xx answer throws with `status` and a bounded `detail`. */
57
+ async function request(path, options = {}) {
58
+ const cleanPath = path.startsWith('/') ? path : `/${path}`;
59
+ const res = await send(`${base}${cleanPath}`, {
60
+ ...options,
61
+ headers: {
62
+ ...options.headers,
63
+ Authorization: `Bearer ${token}`,
64
+ Accept: 'application/json',
65
+ 'Content-Type': 'application/json',
66
+ },
67
+ timeoutMs: options.timeoutMs || DEFAULT_TIMEOUT_MS,
68
+ });
69
+ if (!res.ok) {
70
+ const errorText = await res.text().catch(() => '');
71
+ const error = new Error(`Headscale API error (${res.status} ${res.statusText})`);
72
+ error.status = res.status;
73
+ error.detail = errorText.slice(0, 500);
74
+ throw error;
75
+ }
76
+ const contentType = res.headers.get('content-type') || '';
77
+ if (contentType.includes('application/json')) return res.json();
78
+ throw new Error('Control server returned a non-JSON response');
79
+ }
80
+
81
+ /**
82
+ * Resolve a control-server user to both of its identifying forms.
83
+ * User-facing names resolve to the current API numeric ID.
84
+ */
85
+ async function resolveUser(requested) {
86
+ const data = await request('/api/v1/user');
87
+ const users = data?.users || [];
88
+ if (requested) {
89
+ const match = users.find(
90
+ (u) => String(u.id) === String(requested) || u.name === requested || (u.email && u.email === requested),
91
+ );
92
+ if (match) return { id: String(match.id), name: match.name };
93
+ // Unknown to us — pass it through and let the control server decide.
94
+ return { id: String(requested), name: String(requested) };
95
+ }
96
+ if (!users.length) return null;
97
+ return { id: String(users[0].id), name: users[0].name };
98
+ }
99
+
100
+ /** Mint using the current numeric-user API without older-version retries. */
101
+ async function createPreAuthKey(options = {}) {
102
+ const ttlMinutes = Number.isFinite(options.ttlMinutes) ? options.ttlMinutes : 10;
103
+ const expiration = options.expiration || new Date(Date.now() + ttlMinutes * 60_000).toISOString();
104
+ const ref = options.userRef || await resolveUser(options.user);
105
+ if (!ref?.id || !/^[0-9]+$/.test(String(ref.id))) throw new Error('Select a registered numeric control-server user');
106
+ const user = String(ref.id);
107
+ const data = await request('/api/v1/preauthkey', {
108
+ method: 'POST', body: JSON.stringify({ user, reusable: options.reusable === true,
109
+ ephemeral: options.ephemeral === true, expiration, aclTags: options.aclTags || [] }),
110
+ });
111
+ const key = data?.preAuthKey?.key;
112
+ if (typeof key !== 'string' || !key) throw new Error('Headscale did not return a pre-auth key');
113
+ return { key, expiration, user, raw: data };
114
+ }
115
+
116
+ /**
117
+ * Read the control server's ACL policy.
118
+ *
119
+ * Requires the server to run with `policy.mode: database` — in `file` mode the
120
+ * policy lives on the control server's disk and the API cannot manage it.
121
+ *
122
+ * Headscale returns a 500 (not a 404) when database mode is on but the policy
123
+ * table is still empty, so an unset policy is normalised to `policy: null`
124
+ * rather than thrown. That distinction matters: "no policy yet" is the expected
125
+ * first-run state, whereas a real transport or auth failure must still surface.
126
+ */
127
+ async function getPolicy() {
128
+ try {
129
+ const data = await request('/api/v1/policy');
130
+ return { policy: data?.policy ?? null, updatedAt: data?.updatedAt ?? null, configured: Boolean(data?.policy), raw: data };
131
+ } catch (err) {
132
+ const detail = String(err.detail || '');
133
+ const emptyPolicyTable = err.status === 404 || (err.status === 500 && /policy|not found|no such/i.test(detail));
134
+ if (emptyPolicyTable) return { policy: null, updatedAt: null, configured: false, unset: true, raw: null };
135
+ if (err.status === 400 && /database/i.test(detail)) {
136
+ const modeError = new Error('Headscale is running in file policy mode; set `policy.mode: database` to manage the policy over the API');
137
+ modeError.status = err.status;
138
+ modeError.detail = err.detail;
139
+ modeError.code = 'POLICY_MODE_FILE';
140
+ throw modeError;
141
+ }
142
+ throw err;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Replace the control server's ACL policy.
148
+ *
149
+ * `policy` may be an object (serialised here) or an already-formatted
150
+ * HuJSON/JSON string. Headscale validates server-side and rejects the write if
151
+ * the policy's own `tests` block fails, so a bad policy never lands.
152
+ */
153
+ async function setPolicy(policy) {
154
+ if (!policy) throw new Error('A policy is required');
155
+ const serialised = typeof policy === 'string' ? policy : JSON.stringify(policy, null, 2);
156
+ const data = await request('/api/v1/policy', { method: 'PUT', body: JSON.stringify({ policy: serialised }) });
157
+ return { policy: data?.policy ?? serialised, updatedAt: data?.updatedAt ?? null, raw: data };
158
+ }
159
+
160
+ return { url: base, request, resolveUser, createPreAuthKey, getPolicy, setPolicy };
161
+ }
162
+
163
+ /**
164
+ * Build a policy that lets every mesh member reach every other member, and use
165
+ * Tailscale SSH between them as any non-root user.
166
+ *
167
+ * This is deliberately the simple single-operator shape: one person's own
168
+ * machines, all equally trusted. It is NOT appropriate for a multi-tenant
169
+ * tailnet — headscale membership is the trust boundary, so anything admitted to
170
+ * the mesh inherits this access. Narrow `src`/`dst` before adding a node you do
171
+ * not control.
172
+ *
173
+ * root is excluded via `autogroup:nonroot`: SSH-ing straight to root over the
174
+ * mesh removes the audit trail that makes per-user access worth having.
175
+ */
176
+ export function buildMeshSshPolicy({ allowRoot = false } = {}) {
177
+ return {
178
+ acls: [{ action: 'accept', src: ['*'], dst: ['*:*'] }],
179
+ ssh: [
180
+ {
181
+ action: 'accept',
182
+ // autogroup:member, not autogroup:self. 'self' limits a member to SSH on their own
183
+ // devices, so no node on the mesh could reach any other one and every cross-machine
184
+ // operation fell back to key auth that had never been set up — which reads as the peer
185
+ // being unreachable. The point of this policy is that members reach each other.
186
+ src: ['autogroup:member'],
187
+ dst: ['autogroup:member'],
188
+ users: allowRoot ? ['autogroup:nonroot', 'root'] : ['autogroup:nonroot'],
189
+ },
190
+ ],
191
+ };
192
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Mesh control-server capability: the developer's own Headscale, host-free.
3
+ *
4
+ * A project that vendors this brick provisions, updates and administers its
5
+ * own control server without Total Recall. The caller supplies the target host,
6
+ * and may inject the command runner (`exec`), the release lookups and the HTTP
7
+ * transport; the defaults use only Node built-ins (`ssh` through
8
+ * child_process, global `fetch`). Credentials are never read from a store or
9
+ * the environment here: provisioning returns the new API key to the caller,
10
+ * and the API client takes the key as an argument.
11
+ */
12
+ import {
13
+ meshTargetOf,
14
+ sshExec,
15
+ provisionHeadscale,
16
+ updateHeadscale,
17
+ installConsole,
18
+ updateConsole,
19
+ } from './control-server.mjs';
20
+
21
+ export { meshTargetOf, sshExec, shellQuote, patchConfig, parseChecksum, consoleConfig } from './control-server.mjs';
22
+ export { createHeadscaleApi, buildMeshSshPolicy, assertSecureControlUrl, normalizeControlUrl } from './headscale-api.mjs';
23
+
24
+ /**
25
+ * Bind the control-server lifecycle to one host.
26
+ * @param {{ target: { name?: string, host: string, user: string, port?: number, domain: string },
27
+ * exec?: Function, fetchJson?: Function, fetchText?: Function, checkDns?: Function,
28
+ * log?: Function, wait?: Function }} options
29
+ */
30
+ export function createControlServer({ target, exec, ...deps } = {}) {
31
+ const resolved = meshTargetOf({ name: target?.name, ssh: { host: target?.host, user: target?.user, port: target?.port }, domain: target?.domain });
32
+ if (!resolved) throw new Error('A control-server target needs a valid SSH host, SSH user and a public domain pointing at that host');
33
+ const run = exec || sshExec(resolved);
34
+ const shared = Object.fromEntries(Object.entries(deps).filter(([, v]) => v !== undefined));
35
+ return {
36
+ target: resolved,
37
+ /** Install Headscale on a fresh Debian 12+/Ubuntu 22.04+ host; returns the control URL, API key and a 1-hour pre-auth key. */
38
+ provision: (opts = {}) => provisionHeadscale(resolved, { ...shared, ...opts, exec: run }),
39
+ /** Stage, trial, switch and roll back to the latest release; `checkOnly` reports without changing anything. */
40
+ update: (opts = {}) => updateHeadscale(resolved, { ...shared, ...opts, exec: run }),
41
+ /** Install the optional Headplane console, bound to loopback. */
42
+ installConsole: (opts = {}) => installConsole(resolved, { ...shared, ...opts, exec: run }),
43
+ /** Move the console to the latest release, restoring the previous image on failure. */
44
+ updateConsole: (opts = {}) => updateConsole(resolved, { ...shared, ...opts, exec: run }),
45
+ };
46
+ }
@@ -18,7 +18,15 @@ export async function run(argv = []) {
18
18
  const at=args.indexOf('--mode');const mode=at<0?0o600:Number.parseInt(args[at+1],8);
19
19
  console.log(JSON.stringify(await putMeshFile({node:args[2],source:args[3],destination:args[4],mode})));return;
20
20
  }
21
- if (sub === 'status' || sub === 'view' || sub === 'probe') {
21
+ if (sub === 'view') {
22
+ const { readMeshView } = await import('./src/core/screen-view.mjs');
23
+ if (args.slice(1).some(arg => arg.startsWith('-') && arg !== '--json')) throw new Error('view accepts a screen and --json only');
24
+ const names = args.slice(1).filter(arg => arg !== '--json');
25
+ if (names.length > 1) throw new Error('view accepts one screen');
26
+ console.log(JSON.stringify(await readMeshView(names[0])));
27
+ return;
28
+ }
29
+ if (sub === 'status' || sub === 'probe') {
22
30
  const result = await meshStatus();
23
31
  console.log(JSON.stringify(result));
24
32
  if (sub === 'probe' && !result.control.verified) process.exitCode = 1;
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "tr-plugin-mesh",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-mesh",
9
- "version": "0.2.2",
9
+ "version": "0.2.4",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "express": "^5.2.1"
13
13
  },
14
14
  "devDependencies": {
15
15
  "jsdom": "^29.1.1",
16
- "supertest": "^7.2.2",
17
- "total-recall-brain": "3.40.1",
18
- "vitest": "^4.1.11"
16
+ "supertest": "^7.3.1",
17
+ "total-recall-brain": "3.41.2",
18
+ "vitest": "^4.1.12"
19
19
  }
20
20
  },
21
21
  "node_modules/@asamuzakjp/css-color": {
@@ -1259,18 +1259,18 @@
1259
1259
  }
1260
1260
  },
1261
1261
  "node_modules/body-parser": {
1262
- "version": "2.3.0",
1263
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
1264
- "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
1262
+ "version": "2.4.0",
1263
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz",
1264
+ "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==",
1265
1265
  "license": "MIT",
1266
1266
  "dependencies": {
1267
1267
  "bytes": "^3.1.2",
1268
- "content-type": "^2.0.0",
1268
+ "content-type": "^2.1.0",
1269
1269
  "debug": "^4.4.3",
1270
1270
  "http-errors": "^2.0.1",
1271
- "iconv-lite": "^0.7.2",
1271
+ "iconv-lite": "^0.7.3",
1272
1272
  "on-finished": "^2.4.1",
1273
- "qs": "^6.15.2",
1273
+ "qs": "^6.16.1",
1274
1274
  "raw-body": "^3.0.2",
1275
1275
  "type-is": "^2.1.0"
1276
1276
  },
@@ -4187,22 +4187,22 @@
4187
4187
  }
4188
4188
  },
4189
4189
  "node_modules/tldts": {
4190
- "version": "7.4.18",
4191
- "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.18.tgz",
4192
- "integrity": "sha512-somxA8xKAUzlAxl0TX/jMSYIbRNFIVGnI96pVGJPeMc5BsHmfVXn6mOJT6U++ELnl04eGfzpfcEuZe8WiASkXg==",
4190
+ "version": "7.4.19",
4191
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.19.tgz",
4192
+ "integrity": "sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==",
4193
4193
  "dev": true,
4194
4194
  "license": "MIT",
4195
4195
  "dependencies": {
4196
- "tldts-core": "^7.4.18"
4196
+ "tldts-core": "^7.4.19"
4197
4197
  },
4198
4198
  "bin": {
4199
4199
  "tldts": "bin/cli.js"
4200
4200
  }
4201
4201
  },
4202
4202
  "node_modules/tldts-core": {
4203
- "version": "7.4.18",
4204
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.18.tgz",
4205
- "integrity": "sha512-KI2PKvCmvQI/xD6ypPqkU0i7oxNXR/O58RLRaHR4svsgVUQ2JE/XC8BtMbG6PSehD27I8sZJRcC1ASnCy6lp3g==",
4203
+ "version": "7.4.19",
4204
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.19.tgz",
4205
+ "integrity": "sha512-goNXV6Ho9TIOfpJZanTd6CKG5DyYgYAL+k+22T0SLh8l6+KHIfBUPFuePuSZv05UttIaaLmI9tb7TmGIdK5AkQ==",
4206
4206
  "dev": true,
4207
4207
  "license": "MIT"
4208
4208
  },
@@ -4216,9 +4216,9 @@
4216
4216
  }
4217
4217
  },
4218
4218
  "node_modules/total-recall-brain": {
4219
- "version": "3.40.1",
4220
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.40.1.tgz",
4221
- "integrity": "sha512-tZMnDHXsMT1sa698oF26Np/G4k+Cn4ZXpTdo+BTNOvUMYEhxWR+7Tl+upHLMhcEEPuEM0EiAObL53a9JELzGag==",
4219
+ "version": "3.41.2",
4220
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.41.2.tgz",
4221
+ "integrity": "sha512-bwJFAFAyvk9ysf+5zhBzMxrKus1Hj1AygUWdBNt+vQbM2sfSEf6701sa30KwfGyU8+cMuN6YNT5Mn63mFZIi/w==",
4222
4222
  "dev": true,
4223
4223
  "license": "MIT",
4224
4224
  "workspaces": [
@@ -4247,7 +4247,8 @@
4247
4247
  "bin": {
4248
4248
  "antigravity": "bin/antigravity.mjs",
4249
4249
  "total-recall": "bin/total-recall.mjs",
4250
- "total-recall-memory": "bin/total-recall-memory.mjs"
4250
+ "total-recall-memory": "bin/total-recall-memory.mjs",
4251
+ "vibebrain": "bin/total-recall.mjs"
4251
4252
  },
4252
4253
  "optionalDependencies": {
4253
4254
  "playwright": "^1.60.0"
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "tr-plugin-mesh",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "Headscale and Tailscale SSH control-server mesh administration and node discovery",
6
6
  "scripts": {
7
- "test": "node --test test/*.test.mjs"
7
+ "test": "node --test test/*.test.mjs && vitest run"
8
8
  },
9
9
  "license": "MIT",
10
10
  "dependencies": {
@@ -16,6 +16,9 @@
16
16
  "README.md",
17
17
  "LICENSE",
18
18
  "ui/",
19
- "src/"
19
+ "capability/",
20
+ "src/",
21
+ "!src/**/*.spec.mjs",
22
+ "!src/**/*.test.mjs"
20
23
  ]
21
24
  }