total-recall-brain 3.41.2 → 3.41.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/total-recall.mjs +19 -4
- package/frontend/dist/assets/{index-BG9NkVUy.css → index-BIQmqDKP.css} +1 -1
- package/frontend/dist/assets/{index-BtuiZqwP.js → index-CRzjQLu7.js} +7 -7
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +8 -5
- package/frontend/src/api/auth.ts +0 -3
- package/frontend/src/components/plugins/PluginPanel.tsx +7 -3
- package/frontend/src/components/plugins/card/CardEngine.tsx +1 -1
- package/frontend/src/pages/LoginPage.tsx +1 -41
- package/frontend/src/pages/PluginsPage.tsx +14 -7
- package/package.json +7 -5
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +197 -31
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +8 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +48 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/mesh-secrets.mjs +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/secrets.mjs +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-carriers.mjs +76 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-prune.mjs +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +53 -17
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +56 -8
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +1068 -204
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/panel.js +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +436 -274
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +12 -12
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +16 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +461 -242
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +12 -12
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -7
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +17 -2
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +426 -300
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +23 -8
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +350 -283
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +23 -8
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +31 -19
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -10
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/grants.mjs +7 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +61 -38
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +57 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +220 -36
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/server/routes/mesh.mjs +7 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +65 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +534 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +139 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +2849 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +376 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adapters/dsh.mjs +446 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +398 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +152 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +82 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +90 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +415 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/tracker.mjs +133 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +233 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +845 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/BRICK.md +64 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/index.mjs +250 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +344 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +2917 -140
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +223 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +355 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +375 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +37 -10
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +986 -280
- package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +25 -10
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +38 -15
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +60 -34
- package/src/cli/command.mjs +23 -18
- package/src/cli/config.mjs +3 -0
- package/src/cli/connect.mjs +12 -116
- package/src/cli/daemon.mjs +23 -1
- package/src/cli/harness.mjs +0 -1
- package/src/cli/plugin/copies.mjs +8 -5
- package/src/cli/plugin/create.mjs +56 -3
- package/src/cli/plugin/index.mjs +40 -5
- package/src/cli/plugin/monetization.mjs +232 -12
- package/src/cli/preflight.mjs +161 -0
- package/src/cli/rebuild.mjs +22 -0
- package/src/cli/reset-password.mjs +3 -2
- package/src/cli/skill.mjs +8 -2
- package/src/cli/status.mjs +25 -7
- package/src/core/automatic-brain-updates.mjs +9 -3
- package/src/core/brain-updates.mjs +63 -5
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/context-compiler.mjs +8 -1
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/global-brain-updates.mjs +21 -1
- package/src/core/mesh-brain-updates.mjs +5 -2
- package/src/core/meta-harness.mjs +32 -79
- package/src/core/plugin-dependencies.mjs +6 -1
- package/src/core/plugin-runner.mjs +6 -1
- package/src/core/plugin-store.mjs +67 -1
- package/src/core/plugin-updates.mjs +1 -1
- package/src/core/portable-skills-maintenance.mjs +19 -0
- package/src/core/preflight-gate.mjs +77 -0
- package/src/core/preflight-receipt.mjs +123 -0
- package/src/core/required-plugins.mjs +7 -0
- package/src/core/runtime.mjs +0 -3
- package/src/core/skills-registry.mjs +33 -3
- package/src/core/surface.mjs +38 -1
- package/src/server/auth.mjs +41 -1
- package/src/server/memory-app.mjs +2 -1
- package/src/server/routes/auth.mjs +25 -1
- package/src/server/routes/plugins.mjs +17 -4
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the secrets-store master password lives on this host.
|
|
3
|
+
*
|
|
4
|
+
* The sanctioned carriers are owner-only: the macOS Keychain, the launchd
|
|
5
|
+
* agent plist and `~/.agent/tr.env`. An `export TR_SECRETS_PASSWORD=…` in a
|
|
6
|
+
* shell profile or in /etc/environment is ambient: every process started from
|
|
7
|
+
* a shell (or, for /etc/environment, every login) inherits it, and a process
|
|
8
|
+
* manager such as pm2 then captures it into each app it starts. This module
|
|
9
|
+
* finds those exports and removes them once a sanctioned carrier holds the
|
|
10
|
+
* same password. It reports files and line numbers, never values.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { secretsPassword } from './secrets-gateway.mjs';
|
|
16
|
+
|
|
17
|
+
const EXPORT = /^\s*(?:export\s+)?(?:TR_SECRETS_PASSWORD|TR_MASTER_PASSWORD)\s*=/;
|
|
18
|
+
const PROFILES = ['.bashrc', '.bash_profile', '.profile', '.zshrc', '.zprofile', '.zshenv'];
|
|
19
|
+
|
|
20
|
+
/** Shell and system files that can make the password ambient. */
|
|
21
|
+
export function ambientCandidateFiles({ home = os.homedir(), etc = '/etc' } = {}) {
|
|
22
|
+
const files = PROFILES.map((name) => path.join(home, name));
|
|
23
|
+
files.push(path.join(etc, 'environment'), path.join(etc, 'profile'), path.join(etc, 'zshenv'));
|
|
24
|
+
try {
|
|
25
|
+
for (const name of fs.readdirSync(path.join(etc, 'profile.d'))) {
|
|
26
|
+
if (name.endsWith('.sh')) files.push(path.join(etc, 'profile.d', name));
|
|
27
|
+
}
|
|
28
|
+
} catch { /* no profile.d */ }
|
|
29
|
+
return files;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Lines that export the master password, by file. */
|
|
33
|
+
export function findAmbientPasswordExports(files = ambientCandidateFiles()) {
|
|
34
|
+
const found = [];
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
let text;
|
|
37
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
38
|
+
const lines = text.split('\n').map((line, index) => (EXPORT.test(line) ? index + 1 : 0)).filter(Boolean);
|
|
39
|
+
if (lines.length) found.push({ file, lines });
|
|
40
|
+
}
|
|
41
|
+
return found;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The password a sanctioned carrier returns with the environment ignored. */
|
|
45
|
+
export function carrierPassword(env = process.env, resolve = secretsPassword) {
|
|
46
|
+
const quiet = { ...env };
|
|
47
|
+
delete quiet.TR_SECRETS_PASSWORD;
|
|
48
|
+
delete quiet.TR_MASTER_PASSWORD;
|
|
49
|
+
return resolve({ env: quiet }) || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Remove ambient exports, only when a sanctioned carrier already returns the
|
|
54
|
+
* password this process was given; otherwise the store would be locked out.
|
|
55
|
+
*
|
|
56
|
+
* @param {{ files?: string[], env?: object, resolve?: Function }} [opts]
|
|
57
|
+
*/
|
|
58
|
+
export function removeAmbientPasswordExports({ files = ambientCandidateFiles(), env = process.env, resolve = secretsPassword } = {}) {
|
|
59
|
+
const exports = findAmbientPasswordExports(files);
|
|
60
|
+
if (!exports.length) return { removed: [], carrier: 'not-checked' };
|
|
61
|
+
const current = env.TR_SECRETS_PASSWORD || env.TR_MASTER_PASSWORD;
|
|
62
|
+
const carried = carrierPassword(env, resolve);
|
|
63
|
+
if (!carried) throw new Error('No owner-only carrier (Keychain, launchd agent or ~/.agent/tr.env) holds the master password; set one with `auth secret rekey --env-file|--keychain` before removing ambient exports');
|
|
64
|
+
if (current && carried !== current) throw new Error('The owner-only carrier holds a different password than this environment; reconcile them before removing ambient exports');
|
|
65
|
+
const removed = [];
|
|
66
|
+
for (const { file, lines } of exports) {
|
|
67
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
68
|
+
const kept = text.split('\n').filter((line) => !EXPORT.test(line)).join('\n');
|
|
69
|
+
const mode = fs.statSync(file).mode & 0o777;
|
|
70
|
+
const tmp = `${file}.tr-tmp-${process.pid}`;
|
|
71
|
+
fs.writeFileSync(tmp, kept, { mode });
|
|
72
|
+
fs.renameSync(tmp, file);
|
|
73
|
+
removed.push({ file, lines });
|
|
74
|
+
}
|
|
75
|
+
return { removed, carrier: 'verified' };
|
|
76
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `auth secret prune`: a read-only report of secrets worth removing or consolidating.
|
|
3
|
+
*
|
|
4
|
+
* A key is a candidate when it is bound to no repository, its provider tracking reports an
|
|
5
|
+
* error (usually a dead key), or it shares a value with another key. Nothing is deleted;
|
|
6
|
+
* the report says why each key is listed so the owner decides.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @param {Array<{key:string, repos?:string[], tracking_status?:string|null, tracking_exempt?:boolean, fingerprint_full?:string|null, set?:boolean}>} rows */
|
|
10
|
+
export function pruneCandidates(rows) {
|
|
11
|
+
const byFingerprint = new Map();
|
|
12
|
+
for (const row of rows) {
|
|
13
|
+
if (!row.set || !row.fingerprint_full) continue;
|
|
14
|
+
if (!byFingerprint.has(row.fingerprint_full)) byFingerprint.set(row.fingerprint_full, []);
|
|
15
|
+
byFingerprint.get(row.fingerprint_full).push(row.key);
|
|
16
|
+
}
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
const reasons = [];
|
|
20
|
+
if (!(row.repos || []).length) reasons.push('unbound');
|
|
21
|
+
if (row.tracking_status === 'error' && !row.tracking_exempt) reasons.push('error');
|
|
22
|
+
const twins = (byFingerprint.get(row.fingerprint_full) || []).filter((key) => key !== row.key);
|
|
23
|
+
if (twins.length) reasons.push(`same value as ${twins.join(', ')}`);
|
|
24
|
+
if (reasons.length) out.push({ key: row.key, reasons, provider: row.provider || null });
|
|
25
|
+
}
|
|
26
|
+
return out.sort((a, b) => b.reasons.length - a.reasons.length || a.key.localeCompare(b.key));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function pruneSummary(rows, candidates) {
|
|
30
|
+
return {
|
|
31
|
+
total: rows.length,
|
|
32
|
+
candidates: candidates.length,
|
|
33
|
+
unbound: candidates.filter((c) => c.reasons.includes('unbound')).length,
|
|
34
|
+
errored: candidates.filter((c) => c.reasons.includes('error')).length,
|
|
35
|
+
duplicated: candidates.filter((c) => c.reasons.some((r) => r.startsWith('same value'))).length,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -4,20 +4,22 @@ import { getLeaderInfo, isLeader } from './leader-election.mjs';
|
|
|
4
4
|
import { logger } from './logger.mjs';
|
|
5
5
|
import { brainDir } from './config.mjs';
|
|
6
6
|
import { resolveSecretsPath, loadSecrets, saveSecrets, ensureMeta, appendAudit, META_KEY } from './secrets-store.mjs';
|
|
7
|
+
import { projectGithubLogin } from './login-projections.mjs';
|
|
7
8
|
|
|
8
9
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
10
|
+
const FETCH_CONCURRENCY = 8;
|
|
9
11
|
|
|
10
12
|
function serverPort() {
|
|
11
13
|
const value = Number(process.env.TR_SERVER_PORT || process.env.PORT || 3000);
|
|
12
14
|
return Number.isInteger(value) && value > 0 && value <= 65535 ? value : 3000;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
function meshUrl(ip, route) {
|
|
17
|
+
function meshUrl(ip, route, port = serverPort()) {
|
|
16
18
|
const octets = String(ip || '').split('.').map(Number);
|
|
17
19
|
if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255) || octets[0] !== 100 || octets[1] < 64 || octets[1] > 127) {
|
|
18
20
|
throw new Error('Leader IP is outside the Tailscale CGNAT range');
|
|
19
21
|
}
|
|
20
|
-
return `http://${ip}:${
|
|
22
|
+
return `http://${ip}:${port}${route}`;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function getSecretsChecksum() {
|
|
@@ -39,23 +41,42 @@ export function secretsSyncMode(env = process.env) {
|
|
|
39
41
|
* anything. The node's store is never replaced wholesale and the node keeps
|
|
40
42
|
* its own master password.
|
|
41
43
|
*/
|
|
42
|
-
export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl = fetch } = {}) {
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl = fetch, port } = {}) {
|
|
45
|
+
// The leader's port, never this node's: the droplet serves on 3900 while devices
|
|
46
|
+
// default to 3000, and assuming our own port made every sync fail.
|
|
47
|
+
const base = meshUrl(leaderIp, '/api/secrets/mesh', port || serverPort());
|
|
48
|
+
let listResponse;
|
|
49
|
+
try {
|
|
50
|
+
listResponse = await fetchImpl(base, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// "fetch failed" alone named neither the address nor the reason.
|
|
53
|
+
throw new Error(`No Total Recall server answered at ${base}: ${err.cause?.code || err.message}`);
|
|
54
|
+
}
|
|
45
55
|
if (!listResponse.ok) throw new Error(`Leader returned ${listResponse.status}`);
|
|
46
56
|
const { keys } = await listResponse.json();
|
|
47
57
|
if (!Array.isArray(keys)) throw new Error('Invalid grant list');
|
|
48
58
|
|
|
59
|
+
// Each key costs the leader a store decryption plus a mesh round trip (2-3 s from a laptop),
|
|
60
|
+
// so a few at a time keeps a full sync under a minute instead of several.
|
|
49
61
|
const fetched = {};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
let next = 0;
|
|
63
|
+
const worker = async () => {
|
|
64
|
+
while (next < keys.length) {
|
|
65
|
+
const key = keys[next++];
|
|
66
|
+
const response = await fetchImpl(`${base}/${encodeURIComponent(key)}`, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
67
|
+
if (!response.ok) throw new Error(`Leader returned ${response.status} for ${key}`);
|
|
68
|
+
fetched[key] = (await response.json()).value;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
await Promise.all(Array.from({ length: Math.min(FETCH_CONCURRENCY, keys.length) }, worker));
|
|
55
72
|
|
|
56
73
|
const secrets = ensureMeta(await loadSecrets(dir));
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
// Only keys this sync created may be removed when their grant is revoked. A key the node
|
|
75
|
+
// already held before it was ever granted is the node's own: revoking the grant must not
|
|
76
|
+
// delete it (on the node that owns the full store, that would wipe its secrets).
|
|
77
|
+
const previouslySynced = Array.isArray(secrets[META_KEY].mesh_synced) ? secrets[META_KEY].mesh_synced : [];
|
|
78
|
+
const created = Object.keys(fetched).filter((key) => !(key in secrets));
|
|
79
|
+
const removed = previouslySynced.filter((key) => !(key in fetched) && key in secrets);
|
|
59
80
|
for (const key of removed) {
|
|
60
81
|
delete secrets[key];
|
|
61
82
|
delete secrets[META_KEY].keys?.[key];
|
|
@@ -66,16 +87,31 @@ export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl =
|
|
|
66
87
|
secrets[key] = value;
|
|
67
88
|
}
|
|
68
89
|
secrets[META_KEY].mesh_granted = Object.keys(fetched);
|
|
90
|
+
secrets[META_KEY].mesh_synced = [...new Set([...previouslySynced.filter((key) => key in fetched), ...created])];
|
|
69
91
|
await saveSecrets(dir, secrets);
|
|
70
92
|
appendAudit(dir, { action: 'mesh_grant_sync', key: `(${keys.length} granted, ${removed.length} revoked)`, actor: 'mesh-sync' });
|
|
71
93
|
return { granted: keys.length, updated, removed: removed.length };
|
|
72
94
|
}
|
|
73
95
|
|
|
96
|
+
/** Sign in the tools that use synced secrets (today: gh from github_token). */
|
|
97
|
+
export async function projectLogins({ dir = brainDir, run } = {}) {
|
|
98
|
+
const secrets = await loadSecrets(dir);
|
|
99
|
+
const github = await projectGithubLogin({ token: secrets.github_token || secrets.GITHUB_TOKEN, run });
|
|
100
|
+
if (github.status === 'logged-in') appendAudit(dir, { action: 'login_projection', key: 'github_token -> gh', actor: 'mesh-sync' });
|
|
101
|
+
if (github.status === 'failed') logger.error('secrets-sync', 'gh login from github_token failed', { error: github.error });
|
|
102
|
+
return { github };
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
export async function syncLoop() {
|
|
75
106
|
secretsSyncMode();
|
|
76
|
-
if (await isLeader())
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
if (!(await isLeader())) {
|
|
108
|
+
const leaderInfo = await getLeaderInfo();
|
|
109
|
+
if (leaderInfo?.ip) {
|
|
110
|
+
try { await syncGrantedSecrets(leaderInfo.ip, { port: leaderInfo.port }); }
|
|
111
|
+
catch (err) { logger.error('secrets-sync', 'Failed to sync granted secrets', { error: err.message }); }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Even when the leader is unreachable, the last synced token should still be signed in.
|
|
115
|
+
try { await projectLogins(); }
|
|
116
|
+
catch (err) { logger.error('secrets-sync', 'Login projection failed', { error: err.message }); }
|
|
81
117
|
}
|
|
@@ -3,17 +3,65 @@
|
|
|
3
3
|
```yaml
|
|
4
4
|
tokens:
|
|
5
5
|
brand-name: "Auth & Secrets"
|
|
6
|
-
wordmark: "
|
|
6
|
+
wordmark: "AUTH & SECRETS"
|
|
7
7
|
color-primary: "#10b981"
|
|
8
8
|
color-background: "#064e3b"
|
|
9
9
|
color-accent: "#34d399"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
color-ok: "#059669"
|
|
11
|
+
color-warn: "#d97706"
|
|
12
|
+
color-danger: "#dc2626"
|
|
13
|
+
font-body: "Outfit, system-ui, sans-serif"
|
|
14
|
+
font-mono: "'JetBrains Mono', ui-monospace, monospace"
|
|
13
15
|
border-radius: "8px"
|
|
16
|
+
density: "compact"
|
|
17
|
+
motion: "120ms ease-out"
|
|
14
18
|
```
|
|
15
19
|
|
|
16
|
-
##
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
## Who this card is for
|
|
21
|
+
|
|
22
|
+
The operator and autonomous agents managing access control, cryptographic secrets, and identities across the mesh.
|
|
23
|
+
|
|
24
|
+
The operator opens this card during active administration: verifying biometric passkeys, checking Google OAuth token freshness, auditing 90-day secret rotation schedules, or inspecting active session integrity.
|
|
25
|
+
|
|
26
|
+
## The four tasks, in the order the card presents them
|
|
27
|
+
|
|
28
|
+
| Screen | Task Frequency | Operational Rationale |
|
|
29
|
+
| :--- | :--- | :--- |
|
|
30
|
+
| **Passkeys** | Daily / Routine | Biometric hardware authentication via WebAuthn FIDO2. The operator checks registered authenticators and manages credential devices. |
|
|
31
|
+
| **Google OAuth** | Frequent | Identity bridge accounts. The operator monitors token expiration, executes manual refreshes, and reviews account scopes. |
|
|
32
|
+
| **Secrets** | Continuous | Cryptographic credential vault. The operator audits key freshness, tracks rotation deadlines, and checks provider bindings. |
|
|
33
|
+
| **Audit & Sessions** | Incident / Review | Security baseline verification. The operator inspects live access sessions and reviews policy findings. |
|
|
34
|
+
|
|
35
|
+
Screen ordering follows task frequency and operational urgency rather than arbitrary command taxonomy.
|
|
36
|
+
|
|
37
|
+
## The core tasks
|
|
38
|
+
|
|
39
|
+
1. **Verify security posture instantly** — confirm that the secrets store is sealed, zero rotation schedules are overdue, and the security audit baseline passes.
|
|
40
|
+
2. **Manage biometric hardware passkeys** — inspect registered platform authenticators (Face ID, Touch ID, Windows Hello) and security keys (YubiKey), register new authenticators, and safely revoke decommissioned hardware.
|
|
41
|
+
3. **Supervise OAuth tokens** — verify Google OAuth connection status, monitor token lifetime, refresh expiring credentials, and safely disconnect accounts.
|
|
42
|
+
4. **Audit secret hygiene** — review key expiration dates, inspect multi-repo bindings, verify remote deployment targets, and trigger rotation workflows without exposing secret values to the DOM.
|
|
43
|
+
|
|
44
|
+
## What must be visible without a single click
|
|
45
|
+
|
|
46
|
+
- **Security Verdict Line:** A clear verdict ("Secure" or the count of active warnings) and timestamp of the latest verification.
|
|
47
|
+
- **Passkey Count:** Total active hardware authenticators registered on the system.
|
|
48
|
+
- **OAuth Status:** Connected accounts and token health status.
|
|
49
|
+
- **Secrets Store Telemetry:** Total stored keys and count of overdue rotations.
|
|
50
|
+
- **Baseline Audit State:** System security verification status (Passed or Flagged).
|
|
51
|
+
|
|
52
|
+
## Why the layout is this layout
|
|
53
|
+
|
|
54
|
+
The card face provides a wide, responsive workspace rather than a narrow column.
|
|
55
|
+
|
|
56
|
+
- **Security Verdict at the Top:** The operator requires an immediate assessment. A prominent verdict line answers whether the authentication system is healthy before secondary details appear.
|
|
57
|
+
- **Horizontal Task Navigation:** Four dedicated domain screens (Passkeys, Google OAuth, Secrets, Audit & Sessions) organize operations by user intent rather than raw CLI syntax.
|
|
58
|
+
- **Zero Raw Secrets in DOM:** Secret values are never rendered, logged, or accepted via input fields. The interface exposes only key names, providers, fingerprints, creation timestamps, and rotation schedules.
|
|
59
|
+
- **Safe Revocation Guards:** Destructive actions such as credential revocation require explicit confirmation and display prominent warning styling. Deleting the final authentication method is blocked to prevent accidental administrative lockout.
|
|
60
|
+
- **Real Telemetry Only:** All figures render em dashes (`—`) until returned by the host. Zero mock responses, zero seeded numbers, and zero reassuring default strings appear in error branches.
|
|
61
|
+
|
|
62
|
+
## Responsive presentation and accessibility
|
|
63
|
+
|
|
64
|
+
- **Breakpoints:** Verified across mobile (360 CSS px), tablet (768 CSS px), and desktop (1280 CSS px) viewports.
|
|
65
|
+
- **Host Theme Adaptation:** Injected dark-dashboard variables (`--text-primary`, `--text-secondary`, `--bg-primary`, `--bg-secondary`, `--border`, `--accent`) govern surfaces and typography.
|
|
66
|
+
- **Touch Targets:** All interactive controls declare a minimum height of 44 px.
|
|
67
|
+
- **Reduced Motion:** All transitions and indicators respect `prefers-reduced-motion` settings.
|