total-recall-brain 3.40.0 → 3.40.2

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 (187) hide show
  1. package/README.md +152 -41
  2. package/bin/total-recall.mjs +16 -76
  3. package/docs/reference/cli-reference.md +10 -10
  4. package/extension/background.js +6 -2
  5. package/extension/config.js +8 -0
  6. package/extension/lib/brain-client.js +29 -4
  7. package/extension/options/options.html +1 -0
  8. package/extension/sidepanel/sidepanel.html +1 -0
  9. package/frontend/dist/assets/{index-CvQRdT2j.css → index-AGUes-VG.css} +1 -1
  10. package/frontend/dist/assets/index-Bt5iVPZj.js +5618 -0
  11. package/frontend/dist/index.html +2 -2
  12. package/frontend/src/api/keys.ts +9 -2
  13. package/frontend/src/api/plugins.ts +4 -0
  14. package/frontend/src/components/plugins/card/CardEngine.tsx +2 -1
  15. package/frontend/src/components/plugins/card/card.css +1 -1
  16. package/frontend/src/pages/IntegrationsPage.tsx +315 -8
  17. package/frontend/src/pages/PluginsPage.tsx +532 -87
  18. package/package.json +6 -5
  19. package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
  20. package/scaffold/.agent/skills/meta-harness/references/optimized/section-09.md +1 -1
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +10 -10
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -6
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +0 -1
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/src/browser-session.mjs +309 -4
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +44 -34
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/src/google-oauth.mjs +6 -4
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +1 -1
  28. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-rotation-recipes.mjs +118 -4
  29. package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/auth.mjs +2 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +3 -3
  31. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +24 -6
  33. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +164 -123
  34. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +65 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +76 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +19 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +290 -0
  38. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +446 -0
  39. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +152 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +71 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +36 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +35 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +175 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +18 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/code-quality/LICENSE +21 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/code-quality/README.md +84 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/code-quality/cli.mjs +79 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/check.mjs +6 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/report.mjs +6 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +27 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/snapshot.mjs +163 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +757 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/config.schema.json +86 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/detect.mjs +89 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +145 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +210 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/state.mjs +20 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +2869 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +28 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +186 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +18 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +272 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/code-quality-report.mjs +129 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/LICENSE +21 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/README.md +73 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/cli.mjs +375 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/echo-tool.mjs +23 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/group.mjs +262 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/project-id.mjs +74 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +2869 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +31 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +221 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/schemas/cli_command.schema.json +107 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/skills/composable-cli/SKILL.md +63 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/command.mjs +313 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compile.mjs +19 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compose.mjs +144 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/contract.mjs +110 -0
  79. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/library.mjs +23 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/registry.mjs +192 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/resolve.mjs +221 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/risk.mjs +126 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/ssss-documents.mjs +29 -0
  84. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +18 -0
  85. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +273 -0
  86. package/scaffold/.agent/skills/total-recall/plugins/embeddings/LICENSE +21 -0
  87. package/scaffold/.agent/skills/total-recall/plugins/embeddings/README.md +20 -0
  88. package/scaffold/.agent/skills/total-recall/plugins/embeddings/cli.mjs +310 -0
  89. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +2826 -0
  90. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +11 -0
  91. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +62 -0
  92. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +297 -0
  93. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/LICENSE +21 -0
  94. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +43 -0
  95. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/cli.mjs +258 -0
  96. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4005 -0
  97. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +20 -0
  98. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +125 -0
  99. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +18 -0
  100. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +278 -0
  101. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +9 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +6 -0
  103. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4721 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -21
  105. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +14 -1
  106. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +10 -0
  107. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/headscale-client.mjs +5 -1
  108. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +4 -13
  109. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-access-verify.mjs +22 -0
  110. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-files.mjs +18 -0
  111. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +2 -0
  112. package/scaffold/.agent/skills/total-recall/plugins/mesh/test-support/memory-root.mjs +14 -0
  113. package/scaffold/.agent/skills/total-recall/plugins/openwiki/LICENSE +21 -0
  114. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +35 -0
  115. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +124 -0
  116. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4097 -0
  117. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +12 -0
  118. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +165 -0
  119. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/ingest.mjs +127 -0
  120. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +18 -0
  121. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +279 -0
  122. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +539 -0
  123. package/scaffold/.agent/skills/total-recall/plugins/security/LICENSE +21 -0
  124. package/scaffold/.agent/skills/total-recall/plugins/security/README.md +67 -0
  125. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +932 -0
  126. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3256 -0
  127. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +25 -0
  128. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +297 -0
  129. package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +4 -0
  130. package/scaffold/.agent/skills/total-recall/plugins/security/skills/security/SKILL.md +37 -0
  131. package/scaffold/.agent/skills/total-recall/plugins/security/src/android.mjs +21 -0
  132. package/scaffold/.agent/skills/total-recall/plugins/security/src/devices.mjs +94 -0
  133. package/scaffold/.agent/skills/total-recall/plugins/security/src/machines.mjs +164 -0
  134. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +41 -0
  135. package/scaffold/.agent/skills/total-recall/plugins/security/src/windows.mjs +42 -0
  136. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +18 -0
  137. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +95 -0
  138. package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +98 -0
  139. package/scaffold/.agent/skills/total-recall/references/auth.md +1 -1
  140. package/src/cli/backup.mjs +71 -3
  141. package/src/cli/brief.mjs +33 -9
  142. package/src/cli/command.mjs +2 -7
  143. package/src/cli/config.mjs +1 -0
  144. package/src/cli/connect.mjs +5 -0
  145. package/src/cli/context.mjs +2 -1
  146. package/src/cli/daemon.mjs +36 -1
  147. package/src/cli/doctor.mjs +6 -0
  148. package/src/cli/harness.mjs +3 -0
  149. package/src/cli/help.mjs +26 -0
  150. package/src/cli/init.mjs +4 -4
  151. package/src/cli/memory-entry.mjs +1 -0
  152. package/src/cli/memory-init.mjs +2 -0
  153. package/src/cli/plugin/index.mjs +11 -0
  154. package/src/cli/plugin/install.mjs +2 -1
  155. package/src/cli/rebuild.mjs +3 -1
  156. package/src/cli/startup.mjs +2 -1
  157. package/src/cli/update.mjs +19 -0
  158. package/src/core/automatic-brain-updates.mjs +67 -0
  159. package/src/core/brain-updates.mjs +21 -2
  160. package/src/core/cli-commands.mjs +71 -0
  161. package/src/core/cli-maintenance.mjs +45 -0
  162. package/src/core/cli-quick-reference.mjs +20 -0
  163. package/src/core/cli-semantic-index.mjs +722 -0
  164. package/src/core/context-compiler.mjs +27 -1
  165. package/src/core/crons.mjs +2 -13
  166. package/src/core/daemon-control.mjs +6 -4
  167. package/src/core/daemon-loop.mjs +12 -8
  168. package/src/core/default-plugins.mjs +5 -0
  169. package/src/core/global-brain-updates.mjs +39 -0
  170. package/src/core/mesh-brain-updates.mjs +30 -0
  171. package/src/core/meta-harness.mjs +108 -0
  172. package/src/core/plugin-dependencies.mjs +2 -1
  173. package/src/core/plugin-loader.mjs +2 -1
  174. package/src/core/plugin-runner.mjs +12 -4
  175. package/src/core/plugin-setup.mjs +20 -0
  176. package/src/core/plugin-store.mjs +22 -4
  177. package/src/core/plugin-tasks.mjs +1 -1
  178. package/src/core/project-brain.mjs +2 -0
  179. package/src/core/project-status.mjs +64 -0
  180. package/src/core/required-plugins.mjs +46 -0
  181. package/src/core/scaffold-plugins.mjs +17 -3
  182. package/src/core/source-brain-updates.mjs +47 -0
  183. package/src/core/startup-health.mjs +5 -3
  184. package/src/core/surface.mjs +44 -10
  185. package/src/server/routes/extension.mjs +53 -8
  186. package/templates/onboarding-interview.md +1 -1
  187. package/frontend/dist/assets/index-SOVUlZqm.js +0 -5596
@@ -0,0 +1,164 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { auditWindows } from './windows.mjs';
7
+ import { auditAndroid } from './android.mjs';
8
+ import { normalizePlatform } from './devices.mjs';
9
+
10
+ export function runCheck(command, args) {
11
+ const result = spawnSync(command, args, { encoding: 'utf8', timeout: 8000, maxBuffer: 262144, stdio: ['ignore', 'pipe', 'pipe'] });
12
+ // Diagnostics deliberately exclude stderr: system tools can include sensitive configuration.
13
+ return { ok: !result.error && result.status === 0, output: !result.error && result.status === 0 ? result.stdout : '', reason: result.error?.code || (result.status === 0 ? null : 'unavailable-or-insufficient-privilege') };
14
+ }
15
+
16
+ export function parseListeners(output, platform, protocol = 'tcp') {
17
+ const endpoints = new Map();
18
+ for (const line of output.split('\n')) {
19
+ const endpoint = platform === 'darwin' ? (line.startsWith('n') ? line.slice(1).replace(/ \(LISTEN\)$/, '') : '') : line.trim().split(/\s+/)[3];
20
+ if (!endpoint) continue;
21
+ const match = endpoint.match(/^(.*):(\d+)$/);
22
+ if (!match) continue;
23
+ const address = match[1].replace(/^\[|\]$/g, '');
24
+ const port = Number(match[2]);
25
+ if (port < 1 || port > 65535) continue;
26
+ const scope = address === '*' || address === '0.0.0.0' || address === '::' ? 'all-interfaces' : address === '::1' || /^127\./.test(address) ? 'loopback' : 'interface';
27
+ endpoints.set(`${address}:${port}`, { address, port, scope, protocol });
28
+ }
29
+ return [...endpoints.values()];
30
+ }
31
+
32
+ function sshPermissions(home) {
33
+ const directory = path.join(home, '.ssh');
34
+ if (!fs.existsSync(directory)) return { state: 'not-configured', files: [] };
35
+ const root = fs.lstatSync(directory);
36
+ if (root.isSymbolicLink() || !root.isDirectory()) return { state: 'unavailable', reason: 'SSH directory must be a regular directory', files: [] };
37
+ const files = [{ path: directory, expected: 0o700, mode: root.mode & 0o777, directory: true }];
38
+ for (const entry of fs.readdirSync(directory)) {
39
+ const file = path.join(directory, entry);
40
+ const stat = fs.lstatSync(file);
41
+ if (stat.isSymbolicLink()) continue;
42
+ // Never read private key material; only inspect metadata for recognized SSH files.
43
+ if (stat.isFile() && (/^id_[\w-]+$/.test(entry) || ['config', 'authorized_keys'].includes(entry))) {
44
+ files.push({ path: file, mode: stat.mode & 0o777, expected: 0o600, directory: false });
45
+ }
46
+ }
47
+ return { state: 'observed', files };
48
+ }
49
+
50
+ export async function auditMachine({ platform = process.platform, hostname = os.hostname(), home = os.homedir(), execute = runCheck } = {}) {
51
+ if (platform === 'win32') return auditWindows({ execute, hostname });
52
+ if (platform === 'android') return auditAndroid({ execute, hostname });
53
+ const checks = [];
54
+ const add = (id, result, classify, remediation) => {
55
+ const state = result.ok ? classify(result.output) : 'unavailable';
56
+ checks.push({ id, state, severity: state === 'fail' ? 'HIGH' : state === 'review' ? 'MEDIUM' : null, remediation: state === 'pass' ? null : remediation, ...(result.ok ? {} : { reason: result.reason || 'check-unavailable' }) });
57
+ };
58
+ if (!['darwin', 'linux'].includes(platform)) return { schema: 'security-machine/v1', hostname, platform, timestamp: new Date().toISOString(), status: 'incomplete', checks: [], listeners: [], coverage: { complete: false, reason: 'unsupported-platform' } };
59
+ if (platform === 'darwin') {
60
+ add('firewall', execute('/usr/libexec/ApplicationFirewall/socketfilterfw', ['--getglobalstate']), text => /enabled/i.test(text) ? 'pass' : /disabled/i.test(text) ? 'fail' : 'unavailable', 'Enable the application firewall in System Settings → Network → Firewall; review application exceptions and verify again.');
61
+ add('disk-encryption', execute('/usr/bin/fdesetup', ['status']), text => /FileVault is On/.test(text) ? 'pass' : /FileVault is Off/.test(text) ? 'fail' : 'unavailable', 'Enable FileVault in Privacy & Security after verifying recovery-key custody and a current backup.');
62
+ add('gatekeeper', execute('/usr/sbin/spctl', ['--status']), text => /assessments enabled/.test(text) ? 'pass' : /assessments disabled/.test(text) ? 'fail' : 'unavailable', 'Enable application assessment; investigate why it was disabled before accepting unsigned software.');
63
+ add('system-integrity', execute('/usr/bin/csrutil', ['status']), text => /status: enabled/.test(text) ? 'pass' : /status: disabled/.test(text) ? 'fail' : 'unavailable', 'Restore System Integrity Protection through macOS Recovery with physical access and a verified backup.');
64
+ add('automatic-updates', execute('/usr/sbin/softwareupdate', ['--schedule']), text => /turned on/.test(text) ? 'pass' : /turned off/.test(text) ? 'review' : 'unavailable', 'Enable automatic security updates and verify the installed OS is supported.');
65
+ } else {
66
+ add('firewall', execute('ufw', ['status']), text => /Status: active/.test(text) ? 'pass' : /Status: inactive/.test(text) ? 'fail' : 'unavailable', 'Inspect the active firewall backend with administrator access. Preserve verified SSH/mesh access, review allow rules and default inbound policy, then verify from a second connection. UFW unavailable does not establish absence of nftables/firewalld protection.');
67
+ const root = execute('findmnt', ['-n', '-o', 'SOURCE', '/']);
68
+ const source = root.output?.trim();
69
+ const disk = root.ok && source?.startsWith('/dev/') ? execute('lsblk', ['-s', '-n', '-o', 'TYPE', source]) : { ok: false, reason: 'root-storage-encryption-not-observable' };
70
+ add('disk-encryption', disk, text => /(^|\n)crypt(\n|$)/.test(text.trim()) ? 'pass' : 'review', 'Verify root-volume encryption and provider storage policy; this check cannot prove encryption of every volume or remote storage.');
71
+ add('automatic-updates', execute('systemctl', ['is-enabled', 'unattended-upgrades.service']), text => /^(enabled|static)\s*$/.test(text) ? 'review' : 'unavailable', 'Inspect your distribution security-update timers and last successful run. A configured service alone does not prove patches are installed.');
72
+ }
73
+ const ssh = execute('/usr/sbin/sshd', ['-T']);
74
+ const settings = Object.fromEntries(ssh.ok ? ssh.output.split('\n').map(line => line.trim().split(/\s+/, 2)).filter(parts => parts.length === 2) : []);
75
+ for (const [setting, expected] of [['permitrootlogin', 'no'], ['passwordauthentication', 'no'], ['permitemptypasswords', 'no'], ['pubkeyauthentication', 'yes']]) {
76
+ add(`ssh-${setting}`, { ok: ssh.ok && Object.hasOwn(settings, setting), output: settings[setting] || '', reason: ssh.reason || 'effective-setting-unavailable' }, value => value === expected ? 'pass' : 'fail', `Review effective SSH ${setting}=${expected}, including Match blocks. Validate sshd -t and a second key-authenticated session before any reload; retain rollback access.`);
77
+ }
78
+ const listening = platform === 'darwin' ? execute('/usr/sbin/lsof', ['-nP', '-iTCP', '-sTCP:LISTEN', '-Fn']) : execute('ss', ['-H', '-lnt']);
79
+ const listeners = listening.ok ? parseListeners(listening.output, platform) : [];
80
+ add('listening-ports', listening, () => listeners.some(item => item.scope !== 'loopback') ? 'review' : 'pass', 'Review every non-loopback listener. Bind private services to loopback or the mesh interface and verify firewall rules plus an authorized external vantage. Bind scope alone does not prove Internet exposure.');
81
+ const udp = platform === 'darwin' ? execute('/usr/sbin/lsof', ['-nP', '-iUDP', '-Fn']) : execute('ss', ['-H', '-lnu']);
82
+ const udpListeners = udp.ok ? parseListeners(udp.output, platform, 'udp') : [];
83
+ add('udp-bindings', udp, () => udpListeners.some(item => item.scope !== 'loopback') ? 'review' : 'pass', 'Review UDP bindings and firewall rules, including discovery and DNS services. A local binding alone does not establish remote reachability.');
84
+ listeners.push(...udpListeners);
85
+ let permissions;
86
+ try { permissions = sshPermissions(home); } catch { permissions = { state: 'unavailable', files: [], reason: 'SSH metadata inspection failed' }; }
87
+ checks.push({ id: 'ssh-file-permissions', state: permissions.state === 'observed' ? permissions.files.some(file => (file.mode & ~file.expected) !== 0) ? 'fail' : 'pass' : permissions.state === 'not-configured' ? 'not-applicable' : 'unavailable', severity: permissions.files.some(file => (file.mode & ~file.expected) !== 0) ? 'HIGH' : null, remediation: 'Use security machine --apply-permissions to restrict recognized local SSH files; symbolic links are never modified.' });
88
+ const complete = checks.every(check => check.state !== 'unavailable');
89
+ return { schema: 'security-machine/v1', hostname, platform, timestamp: new Date().toISOString(), status: checks.some(check => check.state === 'fail') ? 'at-risk' : !complete ? 'incomplete' : checks.some(check => check.state === 'review') ? 'review-required' : 'passed', coverage: { complete, observed: checks.filter(check => !['unavailable', 'not-applicable'].includes(check.state)).length, total: checks.length, limitations: ['Scope is the current OS runtime; a Linux container does not certify its Chromebook, Windows or other host', 'TCP/UDP bindings visible to current user only', 'No external reachability or patch-currency certification', 'SSH Match blocks require connection-specific review'] }, checks, listeners, permissions };
90
+ }
91
+
92
+ export function hardenSshPermissions({ home = os.homedir() } = {}) {
93
+ const inspection = sshPermissions(home);
94
+ if (inspection.state !== 'observed') throw new Error('SSH directory cannot be safely hardened');
95
+ const actions = [];
96
+ for (const file of inspection.files) {
97
+ if ((file.mode & ~file.expected) === 0) continue;
98
+ let fd;
99
+ try {
100
+ // Lock to the inspected inode; refuse symlink/ownership changes instead of following them.
101
+ const parent = fs.lstatSync(path.join(home, '.ssh'));
102
+ if (parent.isSymbolicLink() || !parent.isDirectory()) throw new Error('SSH directory changed');
103
+ fd = fs.openSync(file.path, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
104
+ const before = fs.fstatSync(fd);
105
+ const current = fs.lstatSync(file.path);
106
+ if (before.ino !== current.ino || before.dev !== current.dev || before.uid !== process.getuid()) throw new Error('Ownership or identity changed');
107
+ fs.fchmodSync(fd, file.expected);
108
+ if ((fs.fstatSync(fd).mode & 0o777) !== file.expected) throw new Error('Verification failed');
109
+ actions.push({ path: file.path, before: file.mode.toString(8), after: file.expected.toString(8), status: 'verified' });
110
+ } catch { actions.push({ path: file.path, status: 'failed', reason: 'Unsafe path, ownership, or permission verification failure' }); }
111
+ finally { if (fd !== undefined) fs.closeSync(fd); }
112
+ }
113
+ return { status: actions.some(action => action.status === 'failed') ? 'failed' : 'verified', changed: actions.filter(action => action.status === 'verified').length, actions };
114
+ }
115
+
116
+ export async function loadOwner(id, name) {
117
+ const host = process.env.TR_PACKAGE_ROOT;
118
+ const runtime = host ? await import(pathToFileURL(path.join(host, 'src/core/plugin-runtime.mjs')).href) : await import('total-recall-brain/src/core/plugin-runtime.mjs');
119
+ return runtime.loadPluginModule(id, name);
120
+ }
121
+
122
+ export async function auditFleet({ mesh, target, auditLocal = auditMachine } = {}) {
123
+ mesh ||= await loadOwner('mesh', 'mesh');
124
+ if (!mesh.isMeshAvailable()) throw new Error('Mesh runtime is unavailable; fleet coverage cannot be established');
125
+ const nodes = mesh.getMeshPeers({ includeSelf: true });
126
+ const selected = target ? nodes.filter(node => node.hostname === target || node.ip === target) : nodes;
127
+ if (!selected.length) throw new Error('No matching discovered mesh machines');
128
+ const reports = [];
129
+ // Sequential bounded execution avoids exhausting every peer at once.
130
+ for (const node of selected) {
131
+ if (!node.online) { reports.push({ hostname: node.hostname, state: 'unavailable', reason: 'mesh-peer-offline' }); continue; }
132
+ let devicePlatform;
133
+ try { devicePlatform = normalizePlatform(String(node.os || '').toLowerCase()); } catch { /* native machine path */ }
134
+ if (!node.self && ['ios', 'ipados', 'android', 'chromeos', 'chromeos-flex'].includes(devicePlatform)) {
135
+ reports.push({ hostname: node.hostname, state: 'unavailable', reason: 'No authenticated native posture collector is connected for this device', assessment: { platform: devicePlatform, command: 'security device', evidence: 'manual-review-required' } }); continue;
136
+ }
137
+ try {
138
+ let report;
139
+ let transportIdentity = node.hostname;
140
+ if (node.self) report = await auditLocal();
141
+ else {
142
+ const response = await mesh.execMeshCommand(node.hostname, 'total-recall security machine --json', { timeoutMs: 120000 });
143
+ // A collector exit 1 is an actual risk/incomplete report, not a transport success.
144
+ if (![0, 1].includes(response.exitCode)) throw new Error('remote-collector-failed');
145
+ transportIdentity = response.hostname;
146
+ report = JSON.parse(response.stdout);
147
+ }
148
+ const age = Date.now() - Date.parse(report.timestamp);
149
+ // Mesh authenticates the target; OS hostname may differ from its registered mesh name.
150
+ if (transportIdentity !== node.hostname || report.schema !== 'security-machine/v1' || typeof report.hostname !== 'string' || !report.hostname || !Array.isArray(report.checks) || !['passed', 'at-risk', 'incomplete', 'review-required'].includes(report.status) || typeof report.coverage?.complete !== 'boolean' || !Number.isFinite(age) || age < -5000 || age > 180000) throw new Error('remote-report-identity-schema-or-freshness-invalid');
151
+ const states = new Set(['pass', 'fail', 'review', 'unavailable', 'not-applicable']);
152
+ if (report.checks.some(check => typeof check.id !== 'string' || !states.has(check.state))) throw new Error('invalid-check-state');
153
+ if (report.checks.length) {
154
+ const complete = report.checks.every(check => check.state !== 'unavailable');
155
+ const observed = report.checks.filter(check => !['unavailable', 'not-applicable'].includes(check.state)).length;
156
+ const expectedStatus = report.checks.some(check => check.state === 'fail') ? 'at-risk' : !complete ? 'incomplete' : report.checks.some(check => check.state === 'review') ? 'review-required' : 'passed';
157
+ if (report.coverage.complete !== complete || report.coverage.total !== report.checks.length || report.coverage.observed !== observed || report.status !== expectedStatus) throw new Error('inconsistent-report-claims');
158
+ } else if (report.status !== 'incomplete' || report.coverage.complete) throw new Error('empty-report-cannot-pass');
159
+ reports.push({ hostname: node.hostname, state: 'observed', report });
160
+ } catch { reports.push({ hostname: node.hostname, state: 'unavailable', reason: 'Remote collector unavailable, invalid, or insufficiently authorized' }); }
161
+ }
162
+ const complete = reports.every(item => item.state === 'observed' && item.report.coverage.complete);
163
+ return { schema: 'security-fleet/v1', timestamp: new Date().toISOString(), status: reports.some(item => item.report?.status === 'at-risk') ? 'at-risk' : !complete ? 'incomplete' : reports.some(item => item.report.status !== 'passed') ? 'review-required' : 'passed', coverage: { complete, discovered: nodes.length, selected: selected.length, observed: reports.filter(item => item.state === 'observed').length }, machines: reports };
164
+ }
@@ -0,0 +1,41 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ // Findings contain locations and rule identifiers only, never matched source or values.
5
+ export function scanSource(targetPath) {
6
+ const root = fs.realpathSync(path.resolve(targetPath));
7
+ const findings = [];
8
+ const skipped = new Set(['node_modules', '.git', 'dist', 'coverage', 'memory-vault', '.cache', '.venv', '__pycache__']);
9
+ let count = 0;
10
+ function visit(file) {
11
+ const stat = fs.lstatSync(file);
12
+ if (stat.isSymbolicLink()) {
13
+ findings.push({ file: path.relative(root, file), line: 0, rule: 'uninspected-symlink', severity: 'HIGH', detail: 'Linked source was not inspected; coverage incomplete' });
14
+ return;
15
+ }
16
+ if (stat.isDirectory()) {
17
+ for (const name of fs.readdirSync(file)) if (!skipped.has(name)) visit(path.join(file, name));
18
+ return;
19
+ }
20
+ if (!stat.isFile() || !/\.(?:[cm]?js|tsx?|py|sh|bash|ps1|json|env|ya?ml)$|(?:^|\/)\.env(?:\.|$)/.test(file)) return;
21
+ if (++count > 20000 || stat.size > 2097152) throw new Error('Scan coverage limit exceeded; narrow the target');
22
+ const testFile = /(?:^|\/)(?:test|tests|fixtures)(?:\/|$)|\.(?:test|spec)\./.test(file);
23
+ fs.readFileSync(file, 'utf8').split('\n').forEach((line, index) => {
24
+ const rules = [];
25
+ if (/^\s*(?:\/\/|\*|\/\*)/.test(line)) return;
26
+ if (!testFile && /(?:process\.env|\benv)\s*\[\s*(?:key|name|secretRef|TOKEN_KEY|API_KEY_ENV)\s*\]/.test(line)) rules.push(['dynamic-credential-source-review', 'HIGH']);
27
+ if (!testFile && /(?:fallback_(?:geo|rev|place|route)|tr-tickets-default-secret)/.test(line)) rules.push(['synthetic-provider-or-signing-default', 'HIGH']);
28
+ if (/\beval\s*\(|new\s+Function\s*\(/.test(line)) rules.push(['code-evaluation', 'CRITICAL']);
29
+ if (/(?:api[_-]?key|secret|password|token)\s*[:=]\s*['"][\w+/=-]{16,}['"]/i.test(line)) rules.push(['embedded-credential', 'HIGH']);
30
+ if (/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/.test(line)) rules.push(['private-key-material', 'CRITICAL']);
31
+ if (!testFile && /process\.env(?:\.[A-Z_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)\b|\[['"][A-Z_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)['"]\])/.test(line)) rules.push(['ambient-credential-bypass-review', 'HIGH']);
32
+ if (!testFile && /os\.(?:getenv\(\s*|environ(?:\.get\(\s*|\[\s*))['"][A-Z_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)['"]/.test(line)) rules.push(['python-ambient-credential-review', 'HIGH']);
33
+ if (!testFile && /\.(?:sh|bash|ps1)$/.test(file) && /\$(?:env:|\{)?[A-Z_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)\b/.test(line)) rules.push(['shell-ambient-credential-review', 'HIGH']);
34
+ if (!testFile && /(?:from\s*|import\s*\()\s*['"][^'"]*total-recall[^'"]*\/(?:secrets-store|secrets-gateway|crypto)\.mjs/.test(line)) rules.push(['core-secret-ownership', 'HIGH']);
35
+ if (!testFile && /(?:secret|passphrase|password).*(?:\|\||\?\?).*['"][^'"]+['"]/i.test(line)) rules.push(['credential-default-review', 'HIGH']);
36
+ for (const [rule, severity] of rules) findings.push({ file: path.relative(root, file) || path.basename(file), line: index + 1, rule, severity, detail: 'Review owning source; matched content withheld' });
37
+ });
38
+ }
39
+ visit(root);
40
+ return findings;
41
+ }
@@ -0,0 +1,42 @@
1
+ // Fixed native queries select posture only, never BitLocker recovery material.
2
+ const queries = {
3
+ firewall: "Get-NetFirewallProfile -PolicyStore ActiveStore | Select-Object Name,@{Name='Enabled';Expression={$_.Enabled.ToString()}},DefaultInboundAction",
4
+ antivirus: 'Get-MpComputerStatus | Select-Object AntivirusEnabled,RealTimeProtectionEnabled,AMRunningMode,AntivirusSignatureAge',
5
+ encryption: 'Get-BitLockerVolume -MountPoint $env:SystemDrive | Select-Object VolumeStatus,ProtectionStatus,EncryptionPercentage',
6
+ 'secure-boot': 'Confirm-SecureBootUEFI',
7
+ tcp: 'Get-NetTCPConnection -State Listen | Select-Object LocalAddress,LocalPort',
8
+ udp: 'Get-NetUDPEndpoint | Select-Object LocalAddress,LocalPort',
9
+ };
10
+ export function auditWindows({ execute, hostname }) {
11
+ const checks = [], listeners = [];
12
+ for (const [id, query] of Object.entries(queries)) {
13
+ const result = execute('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `$ErrorActionPreference='Stop'; @(${query}) | ConvertTo-Json -Compress -Depth 3`]);
14
+ let values, state = 'unavailable';
15
+ try { if (result.ok) { values = JSON.parse(result.output); if (!Array.isArray(values)) values = [values]; } } catch { /* malformed output is unavailable */ }
16
+ if (values?.length) {
17
+ if (id === 'firewall' && ['Domain', 'Private', 'Public'].every(name => values.filter(v => v?.Name === name).length === 1) && values.length === 3 && values.every(v => [true, false, 'True', 'False'].includes(v.Enabled))) state = values.every(v => v.Enabled === true || v.Enabled === 'True') ? 'review' : 'fail';
18
+ if (id === 'antivirus' && values.length === 1 && typeof values[0]?.AntivirusEnabled === 'boolean' && typeof values[0]?.RealTimeProtectionEnabled === 'boolean') state = values[0].AntivirusEnabled && values[0].RealTimeProtectionEnabled && values[0].AMRunningMode === 'Normal' && Number.isInteger(values[0].AntivirusSignatureAge) && values[0].AntivirusSignatureAge <= 1 && values[0].AntivirusSignatureAge >= 0 ? 'pass' : 'review';
19
+ if (id === 'encryption' && values.length === 1) {
20
+ const v = values[0];
21
+ if (v && [0, 1, 'On', 'Off'].includes(v.ProtectionStatus) && Number.isFinite(v.EncryptionPercentage) && v.EncryptionPercentage >= 0 && v.EncryptionPercentage <= 100) state = [1, 'On'].includes(v.ProtectionStatus) && v.EncryptionPercentage === 100 ? 'pass' : 'fail';
22
+ }
23
+ if (id === 'secure-boot' && values.length === 1 && typeof values[0] === 'boolean') state = values[0] ? 'pass' : 'fail';
24
+ }
25
+ if ((id === 'tcp' || id === 'udp') && values && values.every(v => v && typeof v.LocalAddress === 'string' && /^[0-9a-fA-F:.%]+$/.test(v.LocalAddress) && Number.isInteger(v.LocalPort) && v.LocalPort > 0 && v.LocalPort <= 65535)) {
26
+ const endpoints = values.map(v => ({ address: v.LocalAddress, port: v.LocalPort, protocol: id, scope: ['0.0.0.0', '::'].includes(v.LocalAddress) ? 'all-interfaces' : v.LocalAddress === '::1' || /^127\./.test(v.LocalAddress) ? 'loopback' : 'interface' }));
27
+ listeners.push(...endpoints); state = endpoints.some(v => v.scope !== 'loopback') ? 'review' : 'pass';
28
+ }
29
+ const guidance = {
30
+ firewall: 'Review all three active firewall profiles and inbound allow rules. Enabled profiles alone do not certify blocked exposure. Preserve a verified management path before changes.',
31
+ antivirus: 'Review Windows Security active providers, real-time protection and signature updates. Passive or unavailable built-in protection requires checking the actual installed provider.',
32
+ encryption: 'Review system-volume Device encryption / BitLocker protection and recovery custody. This query does not inspect other disks or collect recovery keys.',
33
+ 'secure-boot': 'Review UEFI Secure Boot and hardware requirements. Unsupported firmware or insufficient privilege is unverified, not disabled. Keep recovery access before firmware changes.',
34
+ tcp: 'Review non-loopback TCP listeners and firewall rules; local binds do not prove remote reachability.',
35
+ udp: 'Review UDP bindings and firewall rules; verify reachability from an authorized external vantage.',
36
+ };
37
+ checks.push({ id, state, evidence: state === 'unavailable' ? 'none' : 'native-command', severity: state === 'fail' ? 'HIGH' : null, remediation: state === 'pass' ? null : guidance[id], ...(state === 'unavailable' ? { reason: 'native-query-unavailable-or-invalid; edition, permission or tooling may restrict access' } : {}) });
38
+ }
39
+ checks.push({ id: 'patch-currency', state: 'unavailable', evidence: 'none', remediation: 'Review Windows Update and support entitlement for the installed release. Service presence cannot certify patch currency.' });
40
+ const complete = checks.every(v => v.state !== 'unavailable');
41
+ return { schema: 'security-machine/v1', hostname, platform: 'win32', timestamp: new Date().toISOString(), status: checks.some(v => v.state === 'fail') ? 'at-risk' : !complete ? 'incomplete' : 'review-required', coverage: { complete, observed: checks.filter(v => v.state !== 'unavailable').length, total: checks.length, limitations: ['Current machine only; no administrative changes', 'Patch currency and alternate antivirus providers require review', 'System volume only; no recovery keys collected', 'Listener visibility does not certify Internet exposure'] }, checks, listeners };
42
+ }
@@ -0,0 +1,18 @@
1
+ # DESIGN.md — Tin Foil
2
+
3
+ ```yaml
4
+ tokens:
5
+ brand-name: "Tin Foil"
6
+ wordmark: "[TIN//FOIL]"
7
+ color-primary: "#f59e0b"
8
+ color-background: "#0a0a0c"
9
+ color-accent: "#e2e8f0"
10
+ badge: "Hardening"
11
+ font-mono: "'JetBrains Mono', 'Fira Code', monospace"
12
+ border-radius: "8px"
13
+ ```
14
+
15
+ ## Brand & Typographic Wordmark
16
+ - **Name**: Tin Foil
17
+ - **Wordmark Logo**: `[TIN//FOIL]` (custom typographic badge, no generic icons)
18
+ - **Palette**: Primary `#f59e0b`, Surface `#0a0a0c`, Accent `#e2e8f0`.
@@ -0,0 +1,95 @@
1
+ export class SecuritySecurityCard extends HTMLElement {
2
+ connectedCallback() {
3
+ if (this.shadowRoot) return;
4
+ const root = this.attachShadow({ mode: 'open' });
5
+ root.innerHTML = `<style>
6
+ :host{display:block;color:#edf3fa;background:#16222e;font:14px system-ui;border-radius:12px;padding:18px}header{display:flex;align-items:center;gap:12px}svg{width:32px;height:38px;color:#73d6bf}h2{margin:0;font-size:20px}p{color:#bbcad6;line-height:1.5}nav{display:flex;gap:8px;flex-wrap:wrap}button,input,select{min-height:44px;border:1px solid #71928e;border-radius:6px;padding:8px 12px;background:#213644;color:inherit}button{cursor:pointer}button:disabled{opacity:.5}input{min-width:0;flex:1}#state{margin:14px 0;color:#73d6bf}table{width:100%;border-collapse:collapse}td,th{padding:10px 6px;text-align:left;border-bottom:1px solid #3a4e5c;vertical-align:top}td:last-child{line-height:1.5}pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:360px;overflow:auto}.scroll{overflow:auto}
7
+ </style><header><svg viewBox="0 0 32 38" fill="none" aria-hidden="true"><path d="M16 2 29 8v11c0 8-7 14-13 17C10 33 3 27 3 19V8Z" stroke="currentColor" stroke-width="2"/><path d="m9 19 5 5 10-12" stroke="currentColor" stroke-width="2"/></svg><h2>Security Assessments</h2></header>
8
+ <p>Inspect this machine or your mesh. Review firewall, SSH, disk protection, and listening services with concrete remediation.</p>
9
+ <nav><button data-action="machine">This machine</button><button data-action="fleet">Mesh fleet</button><button data-action="plugins">Plugin audit</button><button data-action="harden">Hardening plan</button></nav>
10
+ <nav style="margin-top:10px"><input id="target" aria-label="Machine name" placeholder="Optional mesh machine name"><button id="selected">Audit selected</button></nav>
11
+ <nav style="margin-top:10px"><input id="website" type="url" aria-label="Authorized HTTPS website" placeholder="https://your-site.example"><button id="probe">Check website</button></nav>
12
+ <nav style="margin-top:10px"><input id="source" aria-label="Source directory" placeholder="Source directory"><button id="scan">Scan source</button><button id="permissions">Repair local SSH permissions</button></nav>
13
+ <nav style="margin-top:10px"><select id="platform" aria-label="Device platform"><option value="ios">iPhone</option><option value="ipados">iPad</option><option value="android">Android</option><option value="chromeos">Chromebook</option><option value="chromeos-flex">ChromeOS Flex</option><option value="windows">Windows</option></select><input id="device" aria-label="Device label" placeholder="Device label (no passwords)"><button id="review-device">Review device</button></nav>
14
+ <section id="device-review" hidden><p>Manual device review — answers are your observations, not an automatic scan. Unknown checks remain unverified. Never enter passwords or recovery keys.</p><div id="device-checks"></div><button id="evaluate-device">Evaluate review</button></section>
15
+ <nav style="margin-top:10px"><input id="android-serial" aria-label="Already authorized Android serial" placeholder="Already authorized Android serial"><button id="android-check">Read Android properties</button></nav>
16
+ <div id="state" role="status">Not audited</div><div class="scroll"><table hidden><thead><tr><th>Check / machine</th><th>State</th><th>Remediation / coverage</th></tr></thead><tbody></tbody></table></div><pre id="details"></pre>`;
17
+ root.querySelectorAll('[data-action]').forEach(button => button.addEventListener('click', () => this.execute(button.dataset.action)));
18
+ root.querySelector('#selected').addEventListener('click', () => {
19
+ const target = root.querySelector('#target').value.trim();
20
+ if (!target) { root.querySelector('#state').textContent = 'Enter a discovered machine name'; return; }
21
+ this.execute('fleet', ['--target', target]);
22
+ });
23
+ root.querySelector('#probe').addEventListener('click', () => {
24
+ const website = root.querySelector('#website').value.trim();
25
+ try {
26
+ const url = new URL(website);
27
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) throw new Error();
28
+ } catch { root.querySelector('#state').textContent = 'Enter an authorized HTTPS URL without credentials or query'; return; }
29
+ this.execute('probe', ['--url', website]);
30
+ });
31
+ root.querySelector('#scan').addEventListener('click', () => {
32
+ const source = root.querySelector('#source').value.trim();
33
+ if (!source) { root.querySelector('#state').textContent = 'Enter a source directory'; return; }
34
+ this.execute('scan', [source]);
35
+ });
36
+ root.querySelector('#permissions').addEventListener('click', () => this.execute('machine', ['--apply-permissions']));
37
+ const resetReview = () => { root.querySelector('#device-review').hidden = true; root.querySelector('#device-checks').replaceChildren(); };
38
+ root.querySelector('#platform').addEventListener('change', resetReview);
39
+ root.querySelector('#device').addEventListener('input', resetReview);
40
+ const identity = () => ['--platform', root.querySelector('#platform').value, '--device', root.querySelector('#device').value.trim()];
41
+ root.querySelector('#review-device').addEventListener('click', () => this.execute('device', identity()));
42
+ root.querySelector('#evaluate-device').addEventListener('click', () => {
43
+ const observations = {};
44
+ root.querySelectorAll('#device-checks select').forEach(select => { if (select.dataset.observedAt) observations[select.dataset.check] = { status: select.value, observedAt: select.dataset.observedAt }; });
45
+ this.execute('device', [...identity(), '--answers-json', JSON.stringify(observations)]);
46
+ });
47
+ root.querySelector('#android-check').addEventListener('click', () => this.execute('android', ['--serial', root.querySelector('#android-serial').value.trim()]));
48
+ }
49
+ async execute(action, args = []) {
50
+ const root = this.shadowRoot;
51
+ const state = root.querySelector('#state');
52
+ const details = root.querySelector('#details');
53
+ root.querySelectorAll('button,input,select').forEach(button => { button.disabled = true; });
54
+ state.textContent = 'Checking…';
55
+ details.textContent = '';
56
+ root.querySelector('tbody').replaceChildren();
57
+ root.querySelector('table').hidden = true;
58
+ try {
59
+ if (!this.host?.run) throw new Error('Security runtime is unavailable');
60
+ let response;
61
+ try { response = await this.host.run(action, [...args, '--json']); }
62
+ catch (error) {
63
+ try { response = JSON.parse(error.message); } catch { throw error; }
64
+ }
65
+ const report = typeof response === 'string' ? JSON.parse(response) : typeof response?.output === 'string' ? JSON.parse(response.output) : response;
66
+ if (!report || typeof report !== 'object') throw new Error('No audit report returned');
67
+ if (report.status === 'failed') throw new Error(report.error);
68
+ if (report.schema === 'security-device/v1' && !args.includes('--answers-json')) {
69
+ const form = root.querySelector('#device-checks'); form.replaceChildren();
70
+ for (const check of report.checks) {
71
+ const label = document.createElement('label'); label.style.display = 'block';
72
+ const title = document.createElement('p'); title.textContent = `${check.title}: ${check.remediation}`;
73
+ const select = document.createElement('select'); select.dataset.check = check.id; select.setAttribute('aria-label', check.title);
74
+ for (const [value, text] of [['unknown', 'Not checked'], ['secure', 'Reviewed — protection confirmed'], ['needs-attention', 'Needs attention']]) { const option = document.createElement('option'); option.value = value; option.textContent = text; select.append(option); }
75
+ select.addEventListener('change', () => { select.dataset.observedAt = new Date().toISOString(); });
76
+ label.append(title, select); form.append(label);
77
+ }
78
+ root.querySelector('#device-review').hidden = false;
79
+ }
80
+ state.textContent = `${report.status || (report.total_findings ? 'Findings require review' : 'Audit returned')} · ${report.timestamp || 'No observation timestamp supplied'}`;
81
+ const rows = report.checks?.map(check => [check.id, check.state, check.remediation || check.reason || 'Observed']) || report.machines?.map(machine => [machine.hostname, machine.report?.status || machine.state, machine.reason || `${machine.report.coverage.observed}/${machine.report.coverage.total} checks observed`]) || [];
82
+ if (rows.length) {
83
+ root.querySelector('table').hidden = false;
84
+ for (const row of rows) {
85
+ const tr = document.createElement('tr');
86
+ row.forEach(value => { const td = document.createElement('td'); td.textContent = value; tr.append(td); });
87
+ root.querySelector('tbody').append(tr);
88
+ }
89
+ }
90
+ details.textContent = JSON.stringify(report, null, 2);
91
+ } catch (error) { state.textContent = 'Audit unavailable'; details.textContent = error.message; }
92
+ finally { root.querySelectorAll('button,input,select').forEach(button => { button.disabled = false; }); }
93
+ }
94
+ }
95
+ if (!customElements.get('tr-security-card')) customElements.define('tr-security-card', SecuritySecurityCard);
@@ -0,0 +1,98 @@
1
+ export class SecuritySecurityCard extends HTMLElement {
2
+ connectedCallback() {
3
+ if (this.shadowRoot) return;
4
+ const root = this.attachShadow({ mode: 'open' });
5
+ root.innerHTML = `<style>
6
+ :host{display:block;color:#edf3fa;background:#16222e;font:14px system-ui;border-radius:12px;padding:18px}header{display:flex;align-items:center;gap:12px}svg{width:32px;height:38px;color:#73d6bf}h2{margin:0;font-size:20px}p{color:#bbcad6;line-height:1.5}nav{display:flex;gap:8px;flex-wrap:wrap}button,input,select{min-height:44px;border:1px solid #71928e;border-radius:6px;padding:8px 12px;background:#213644;color:inherit}button{cursor:pointer}button:disabled{opacity:.5}input{min-width:0;flex:1}#state{margin:14px 0;color:#73d6bf}table{width:100%;border-collapse:collapse}td,th{padding:10px 6px;text-align:left;border-bottom:1px solid #3a4e5c;vertical-align:top}td:last-child{line-height:1.5}pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:360px;overflow:auto}.scroll{overflow:auto}
7
+ </style><header><svg viewBox="0 0 32 38" fill="none" aria-hidden="true"><path d="M16 2 29 8v11c0 8-7 14-13 17C10 33 3 27 3 19V8Z" stroke="currentColor" stroke-width="2"/><path d="m9 19 5 5 10-12" stroke="currentColor" stroke-width="2"/></svg><h2>Security Assessments</h2></header>
8
+ <p>Inspect this machine or your mesh. Review firewall, SSH, disk protection, and listening services with concrete remediation.</p>
9
+ <nav><button data-action="machine">This machine</button><button data-action="fleet">Mesh fleet</button><button data-action="plugins">Plugin audit</button><button data-action="harden">Hardening plan</button></nav>
10
+ <nav style="margin-top:10px"><input id="target" aria-label="Machine name" placeholder="Optional mesh machine name"><button id="selected">Audit selected</button></nav>
11
+ <nav style="margin-top:10px"><input id="website" type="url" aria-label="Authorized HTTPS website" placeholder="https://your-site.example"><button id="probe">Check website</button></nav>
12
+ <nav style="margin-top:10px"><input id="source" aria-label="Source directory" placeholder="Source directory"><button id="scan">Scan source</button><button id="permissions">Repair local SSH permissions</button></nav>
13
+ <nav style="margin-top:10px"><select id="platform" aria-label="Device platform"><option value="ios">iPhone</option><option value="ipados">iPad</option><option value="android">Android</option><option value="chromeos">Chromebook</option><option value="chromeos-flex">ChromeOS Flex</option><option value="windows">Windows</option></select><input id="device" aria-label="Device label" placeholder="Device label (no passwords)"><button id="review-device">Review device</button></nav>
14
+ <section id="device-review" hidden><p>Manual device review — answers are your observations, not an automatic scan. Unknown checks remain unverified. Never enter passwords or recovery keys.</p><div id="device-checks"></div><button id="evaluate-device">Evaluate review</button></section>
15
+ <nav style="margin-top:10px"><input id="android-serial" aria-label="Already authorized Android serial" placeholder="Already authorized Android serial"><button id="android-check">Read Android properties</button></nav>
16
+ <div id="state" role="status">Not audited</div><div class="scroll"><table hidden><thead><tr><th>Check / machine</th><th>State</th><th>Remediation / coverage</th></tr></thead><tbody></tbody></table></div><pre id="details"></pre>`;
17
+ root.querySelectorAll('[data-action]').forEach(button => button.addEventListener('click', () => this.execute(button.dataset.action)));
18
+ root.querySelector('#selected').addEventListener('click', () => {
19
+ const target = root.querySelector('#target').value.trim();
20
+ if (!target) { root.querySelector('#state').textContent = 'Enter a discovered machine name'; return; }
21
+ this.execute('fleet', ['--target', target]);
22
+ });
23
+ root.querySelector('#probe').addEventListener('click', () => {
24
+ const website = root.querySelector('#website').value.trim();
25
+ try {
26
+ const url = new URL(website);
27
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) throw new Error();
28
+ } catch { root.querySelector('#state').textContent = 'Enter an authorized HTTPS URL without credentials or query'; return; }
29
+ this.execute('probe', ['--url', website]);
30
+ });
31
+ root.querySelector('#scan').addEventListener('click', () => {
32
+ const source = root.querySelector('#source').value.trim();
33
+ if (!source) { root.querySelector('#state').textContent = 'Enter a source directory'; return; }
34
+ this.execute('scan', [source]);
35
+ });
36
+ root.querySelector('#permissions').addEventListener('click', () => this.execute('machine', ['--apply-permissions']));
37
+ const resetReview = () => { root.querySelector('#device-review').hidden = true; root.querySelector('#device-checks').replaceChildren(); };
38
+ root.querySelector('#platform').addEventListener('change', resetReview);
39
+ root.querySelector('#device').addEventListener('input', resetReview);
40
+ const identity = () => ['--platform', root.querySelector('#platform').value, '--device', root.querySelector('#device').value.trim()];
41
+ root.querySelector('#review-device').addEventListener('click', () => this.execute('device', identity()));
42
+ root.querySelector('#evaluate-device').addEventListener('click', () => {
43
+ const observations = {};
44
+ root.querySelectorAll('#device-checks select').forEach(select => { if (select.dataset.observedAt) observations[select.dataset.check] = { status: select.value, observedAt: select.dataset.observedAt }; });
45
+ this.execute('device', [...identity(), '--answers-json', JSON.stringify(observations)]);
46
+ });
47
+ root.querySelector('#android-check').addEventListener('click', () => this.execute('android', ['--serial', root.querySelector('#android-serial').value.trim()]));
48
+ }
49
+ async execute(action, args = []) {
50
+ const root = this.shadowRoot;
51
+ const state = root.querySelector('#state');
52
+ const details = root.querySelector('#details');
53
+ root.querySelectorAll('button,input,select').forEach(button => { button.disabled = true; });
54
+ state.textContent = 'Checking…';
55
+ details.textContent = '';
56
+ root.querySelector('tbody').replaceChildren();
57
+ root.querySelector('table').hidden = true;
58
+ try {
59
+ if (!this.host?.run) throw new Error('Security runtime is unavailable');
60
+ let response;
61
+ try { response = await this.host.run(action, [...args, '--json']); }
62
+ catch (error) {
63
+ try { response = JSON.parse(error.message); } catch { throw error; }
64
+ }
65
+ const report = typeof response === 'string' ? JSON.parse(response) : typeof response?.output === 'string' ? JSON.parse(response.output) : response;
66
+ if (!report || typeof report !== 'object') throw new Error('No audit report returned');
67
+ if (report.status === 'failed') throw new Error(report.error);
68
+ if (report.schema === 'security-device/v1' && !args.includes('--answers-json')) {
69
+ const form = root.querySelector('#device-checks'); form.replaceChildren();
70
+ for (const check of report.checks) {
71
+ const label = document.createElement('label'); label.style.display = 'block';
72
+ const title = document.createElement('p'); title.textContent = `${check.title}: ${check.remediation}`;
73
+ const select = document.createElement('select'); select.dataset.check = check.id; select.setAttribute('aria-label', check.title);
74
+ for (const [value, text] of [['unknown', 'Not checked'], ['secure', 'Reviewed — protection confirmed'], ['needs-attention', 'Needs attention']]) { const option = document.createElement('option'); option.value = value; option.textContent = text; select.append(option); }
75
+ select.addEventListener('change', () => { select.dataset.observedAt = new Date().toISOString(); });
76
+ label.append(title, select); form.append(label);
77
+ }
78
+ root.querySelector('#device-review').hidden = false;
79
+ }
80
+ state.textContent = `${report.status || (report.total_findings ? 'Findings require review' : 'Audit returned')} · ${report.timestamp || 'No observation timestamp supplied'}`;
81
+ const rows = report.checks?.map(check => [check.id, check.state, check.remediation || check.reason || 'Observed']) || report.machines?.map(machine => [machine.hostname, machine.report?.status || machine.state, machine.reason || `${machine.report.coverage.observed}/${machine.report.coverage.total} checks observed`]) || [];
82
+ if (rows.length) {
83
+ root.querySelector('table').hidden = false;
84
+ for (const row of rows) {
85
+ const tr = document.createElement('tr');
86
+ row.forEach(value => { const td = document.createElement('td'); td.textContent = value; tr.append(td); });
87
+ root.querySelector('tbody').append(tr);
88
+ }
89
+ }
90
+ details.textContent = JSON.stringify(report, null, 2);
91
+ } catch (error) { state.textContent = 'Audit unavailable'; details.textContent = error.message; }
92
+ finally { root.querySelectorAll('button,input,select').forEach(button => { button.disabled = false; }); }
93
+ }
94
+ }
95
+ if (!customElements.get('tr-security-card')) customElements.define('tr-security-card', SecuritySecurityCard);
96
+
97
+ class SecurityPanel extends SecuritySecurityCard {}
98
+ if (!customElements.get('security-panel')) customElements.define('security-panel', SecurityPanel);
@@ -2,7 +2,7 @@
2
2
 
3
3
  Auth is the credential owner and is shipped in Total Recall scaffold. Its source repository owns storage, encryption, provider catalog/account probes, rotation, rekey, exports, sync, CLI, API and UI. The generic runtime loader requests manifest-declared exports; core must never retain a second secret implementation. Missing or invalid auth fails. Secret operations must never fall through to a core handler or silently switch stores.
4
4
 
5
- Discover `total-recall auth --help` and `total-recall auth secret --help`. The compatibility command name `total-recall secret` routes to auth only; it is not another store. Use `auth secret set KEY --stdin` for values, never argv, chat, logs or vault content. `auth secret list` and `catalog` expose metadata. Avoid `get` in a tool transcript; consumers resolve through the auth runtime in-process. Auth API lives under `/api/plugins/auth/secrets`; any retained legacy HTTP route is implemented by auth, never core.
5
+ Discover `total-recall auth --help` and `total-recall auth secret --help`. The auth namespace is the only credential command path. Use `auth secret set KEY --stdin` for values, never argv, chat, logs or vault content. `auth secret list` and `catalog` expose metadata. Avoid `get` in a tool transcript; consumers resolve through the auth runtime in-process. Auth API lives under `/api/plugins/auth/secrets`; any retained legacy HTTP route is implemented by auth, never core.
6
6
 
7
7
  Select the store explicitly with the supported brain/store configuration. Existing carrier paths and encrypted data are preserved; no migration may overwrite credentials. No implicit merging of project/global stores or environment credential fallback. Auth alone manages its unlock carriers. Failed unlock/decryption must propagate; never report the store empty after failure. Rekey changes the master carrier, not provider keys: read the current rekey CLI and runbook, update every selected carrier, verify fresh-shell reads and retired-password rejection without exposing either password.
8
8
 
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { spawnSync } from 'node:child_process';
20
+ import crypto from 'node:crypto';
20
21
  import fs from 'node:fs';
21
22
  import os from 'node:os';
22
23
  import path from 'node:path';
@@ -64,8 +65,22 @@ function parseArgs(args) {
64
65
  return opts;
65
66
  }
66
67
 
68
+ /**
69
+ * The backup repo's git directory lives OUTSIDE the project tree, under
70
+ * ~/.agent/brain-backups. A `.git` inside `<project>/.agent/skills/total-recall`
71
+ * makes the project repo see an embedded repository: `git add -A` records it as a
72
+ * gitlink (mode 160000) with no .gitmodules, which cannot be cloned back.
73
+ */
74
+ export function brainGitDir(skillDir) {
75
+ const abs = path.resolve(skillDir);
76
+ const owner = path.basename(path.resolve(abs, '..', '..', '..')) || 'brain';
77
+ const tag = crypto.createHash('sha1').update(abs).digest('hex').slice(0, 8);
78
+ return path.join(os.homedir(), '.agent', 'brain-backups', `${owner}-${tag}.git`);
79
+ }
80
+
67
81
  function git(cwd, ...gitArgs) {
68
- const result = spawnSync('git', gitArgs, { cwd, stdio: 'pipe' });
82
+ const env = { ...process.env, GIT_DIR: brainGitDir(cwd), GIT_WORK_TREE: path.resolve(cwd) };
83
+ const result = spawnSync('git', gitArgs, { cwd, env, stdio: 'pipe' });
69
84
  return {
70
85
  ok: result.status === 0,
71
86
  stdout: result.stdout?.toString().trim() || '',
@@ -73,6 +88,56 @@ function git(cwd, ...gitArgs) {
73
88
  };
74
89
  }
75
90
 
91
+ /**
92
+ * Move a legacy in-tree `<brain>/.git` to the external git dir, and keep the
93
+ * enclosing project repo from absorbing the brain directory. Idempotent.
94
+ */
95
+ export function adoptExternalGitDir(skillDir) {
96
+ const inTree = path.join(skillDir, '.git');
97
+ const external = brainGitDir(skillDir);
98
+ let moved = false;
99
+ if (fs.existsSync(inTree) && fs.statSync(inTree).isDirectory() && !fs.existsSync(external)) {
100
+ fs.mkdirSync(path.dirname(external), { recursive: true });
101
+ try {
102
+ fs.renameSync(inTree, external);
103
+ } catch {
104
+ fs.cpSync(inTree, external, { recursive: true });
105
+ fs.rmSync(inTree, { recursive: true, force: true });
106
+ }
107
+ spawnSync('git', ['--git-dir', external, 'config', 'core.worktree', path.resolve(skillDir)], { stdio: 'pipe' });
108
+ moved = true;
109
+ }
110
+ if (fs.existsSync(external) && !fs.existsSync(inTree)) excludeFromParentRepo(skillDir);
111
+ return moved;
112
+ }
113
+
114
+ /** Add the brain dir to the enclosing repo's local exclude unless that repo tracks it on purpose. */
115
+ function excludeFromParentRepo(skillDir) {
116
+ try {
117
+ const top = spawnSync('git', ['-C', skillDir, 'rev-parse', '--show-toplevel'], { stdio: 'pipe' });
118
+ if (top.status !== 0) return;
119
+ const root = top.stdout.toString().trim();
120
+ const rel = path.relative(fs.realpathSync(root), fs.realpathSync(skillDir)).split(path.sep).join('/');
121
+ if (!rel || rel.startsWith('..')) return;
122
+ const tracked = spawnSync('git', ['-C', root, 'ls-files', '-s', '--', rel], { stdio: 'pipe' }).stdout.toString().trim();
123
+ if (/^160000 /.test(tracked) && !tracked.includes('\n')) {
124
+ // A stale gitlink (embedded repo with no .gitmodules) cannot be cloned back: unstage it.
125
+ spawnSync('git', ['-C', root, 'rm', '--cached', '-f', '-q', '--', rel], { stdio: 'pipe' });
126
+ } else if (tracked) {
127
+ return; // the project repo versions its brain files itself
128
+ }
129
+ const excludePath = path.join(root, '.git', 'info', 'exclude');
130
+ try {
131
+ const entry = `/${rel}/`;
132
+ const current = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, 'utf8') : '';
133
+ if (!current.split('\n').includes(entry)) {
134
+ fs.mkdirSync(path.dirname(excludePath), { recursive: true });
135
+ fs.appendFileSync(excludePath, `${current && !current.endsWith('\n') ? '\n' : ''}${entry}\n`);
136
+ }
137
+ } catch { /* parent exclude is best effort */ }
138
+ } catch { /* best effort: never fail a backup over the parent's exclude file */ }
139
+ }
140
+
76
141
  function defaultOutput(encrypt) {
77
142
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
78
143
  const suffix = encrypt ? '.tar.gz.gpg' : '.tar.gz';
@@ -169,8 +234,10 @@ async function pushGitBackup(remote, layer = 'auto') {
169
234
  // rule are corrected on their next backup.
170
235
  ensureBrainGitignore(SKILL_DIR);
171
236
 
172
- // Initialise the skill folder as a git repo if needed
173
- const gitDir = path.join(SKILL_DIR, '.git');
237
+ // Initialise the skill folder as a git repo if needed. The git dir lives outside
238
+ // the project tree; a legacy in-tree .git is moved there first.
239
+ adoptExternalGitDir(SKILL_DIR);
240
+ const gitDir = brainGitDir(SKILL_DIR);
174
241
  if (!fs.existsSync(gitDir)) {
175
242
  console.error(` 🔧 Initialising git repo in ${SKILL_DIR}`);
176
243
  const init = git(SKILL_DIR, 'init', '-b', 'main');
@@ -178,6 +245,7 @@ async function pushGitBackup(remote, layer = 'auto') {
178
245
  git(SKILL_DIR, 'init');
179
246
  git(SKILL_DIR, 'checkout', '-b', 'main');
180
247
  }
248
+ excludeFromParentRepo(SKILL_DIR);
181
249
  }
182
250
 
183
251
  // Resolve the remote: if the argument is an existing remote name, use its URL.