@lifeaitools/clauth 1.30.22 → 1.30.24
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/.clauth-skill/SKILL.md +111 -111
- package/README.md +25 -0
- package/cli/api.classify.test.js +75 -75
- package/cli/assets/codevelop/launcher-active.cmd.template +20 -20
- package/cli/assets/codevelop/launcher-static.cmd.template +7 -7
- package/cli/assets/codevelop/windows-terminal.profiles.json +48 -48
- package/cli/assets/watchdog.ps1 +42 -42
- package/cli/commands/agent-cron.js +396 -396
- package/cli/commands/agent-pool.js +1962 -1962
- package/cli/commands/codevelop.js +1190 -1190
- package/cli/commands/doctor.js +302 -302
- package/cli/commands/install.js +10 -10
- package/cli/commands/invite.js +175 -175
- package/cli/commands/join.js +179 -179
- package/cli/commands/npm.js +182 -182
- package/cli/commands/scrub.js +327 -327
- package/cli/commands/scrub.test.js +115 -115
- package/cli/commands/serve.js +43 -86
- package/cli/commands/watchdog.js +209 -209
- package/cli/conf-path.js +21 -21
- package/cli/enrollment-script.js +82 -82
- package/cli/fingerprint.js +143 -143
- package/cli/index.js +1053 -1053
- package/cli/lib/fs-git.js +282 -282
- package/cli/recovery.js +101 -101
- package/cli/studio-debug.js +1095 -1095
- package/cli/supervisor-registry.js +594 -589
- package/cli/supervisor-registry.test.js +397 -397
- package/cli/supervisor-ui.test.js +5 -83
- package/cli/watchdog-registry.js +209 -209
- package/cli/watchdog-registry.test.js +89 -89
- package/install.ps1 +21 -21
- package/package.json +2 -2
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bin/bootstrap-win.exe +0 -0
- package/supabase/migrations/001_clauth_schema.sql +12 -12
- package/supabase/migrations/003_clauth_config.sql +13 -13
- package/supabase/migrations/003_machine_enrollments.sql +39 -39
- package/cli/served-script-syntax.test.mjs +0 -54
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
// node --test cli/commands/scrub.test.js
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
PATTERNS,
|
|
10
|
-
findTranscripts,
|
|
11
|
-
scrubFile,
|
|
12
|
-
isSecretLike,
|
|
13
|
-
loadExtraPatterns,
|
|
14
|
-
sessionTargets,
|
|
15
|
-
} from "./scrub.js";
|
|
16
|
-
|
|
17
|
-
const GH_TOKEN = "ghp_0123456789abcdefABCDEF0123456789abcd"; // ghp_ + 36 chars
|
|
18
|
-
const VAULT_VALUE = "SuperSecretVaultValue_abc123XYZ"; // secret-like literal
|
|
19
|
-
const CUSTOM_SECRET = "MYCORP-TOKEN-998877"; // only an editable pattern catches it
|
|
20
|
-
|
|
21
|
-
function seedProjects() {
|
|
22
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-scrub-"));
|
|
23
|
-
const sess = path.join(root, "proj", "session-uuid");
|
|
24
|
-
const sidecarDir = path.join(sess, "tool-results");
|
|
25
|
-
fs.mkdirSync(sidecarDir, { recursive: true });
|
|
26
|
-
|
|
27
|
-
const jsonl = path.join(root, "proj", "session-uuid.jsonl");
|
|
28
|
-
const txt = path.join(sidecarDir, "toolu_abc.txt");
|
|
29
|
-
const body = `token=${GH_TOKEN} value=${VAULT_VALUE} custom=${CUSTOM_SECRET}\n`;
|
|
30
|
-
fs.writeFileSync(jsonl, `{"x":"${body.trim()}"}\n`, "utf-8");
|
|
31
|
-
fs.writeFileSync(txt, body, "utf-8"); // the sidecar that the old scrubber skipped
|
|
32
|
-
return { root, jsonl, txt };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
test("findTranscripts discovers .jsonl AND tool-results/*.txt sidecars", () => {
|
|
36
|
-
const { root, jsonl, txt } = seedProjects();
|
|
37
|
-
const found = findTranscripts(root);
|
|
38
|
-
assert.ok(found.includes(jsonl), "should find the .jsonl transcript");
|
|
39
|
-
assert.ok(found.includes(txt), "should find the .txt sidecar (the previously-skipped leak path)");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("scrubFile redacts the github token in BOTH the jsonl and the sidecar", () => {
|
|
43
|
-
const { jsonl, txt } = seedProjects();
|
|
44
|
-
for (const f of [jsonl, txt]) {
|
|
45
|
-
const n = scrubFile(f, { force: true, patterns: PATTERNS, literals: [] });
|
|
46
|
-
assert.ok(n >= 1, `expected >=1 redaction in ${path.basename(f)}`);
|
|
47
|
-
const after = fs.readFileSync(f, "utf-8");
|
|
48
|
-
assert.ok(!after.includes(GH_TOKEN), "github token must be gone");
|
|
49
|
-
assert.ok(after.includes("[GITHUB_TOKEN_REDACTED]"), "redaction marker present");
|
|
50
|
-
assert.ok(after.includes("[CLAUTH-SCRUBBED]"), "file stamped as scrubbed");
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("vault-value (literal) redaction removes an arbitrary secret regardless of format", () => {
|
|
55
|
-
const { txt } = seedProjects();
|
|
56
|
-
const n = scrubFile(txt, { force: true, patterns: [], literals: [{ name: "test-svc", value: VAULT_VALUE }] });
|
|
57
|
-
assert.equal(n, 1, "exactly one literal occurrence redacted");
|
|
58
|
-
const after = fs.readFileSync(txt, "utf-8");
|
|
59
|
-
assert.ok(!after.includes(VAULT_VALUE), "vault value must be gone");
|
|
60
|
-
assert.ok(after.includes("[CLAUTH:test-svc_REDACTED]"), "labelled vault redaction present");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("editable patterns file catches a custom secret with no clauth release", () => {
|
|
64
|
-
const cfg = path.join(os.tmpdir(), `clauth-scrub-patterns-${Date.now()}.json`);
|
|
65
|
-
fs.writeFileSync(cfg, JSON.stringify([{ pattern: "MYCORP-TOKEN-\\d+", replacement: "[MYCORP_REDACTED]" }]), "utf-8");
|
|
66
|
-
const extra = loadExtraPatterns(cfg);
|
|
67
|
-
assert.equal(extra.length, 1, "one extra pattern loaded");
|
|
68
|
-
|
|
69
|
-
const { txt } = seedProjects();
|
|
70
|
-
const n = scrubFile(txt, { force: true, patterns: extra, literals: [] });
|
|
71
|
-
assert.ok(n >= 1, "custom pattern matched");
|
|
72
|
-
const after = fs.readFileSync(txt, "utf-8");
|
|
73
|
-
assert.ok(!after.includes(CUSTOM_SECRET), "custom secret gone");
|
|
74
|
-
assert.ok(after.includes("[MYCORP_REDACTED]"), "custom replacement applied");
|
|
75
|
-
fs.rmSync(cfg, { force: true });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("isSecretLike: accepts real tokens, rejects short/url/whitespace values", () => {
|
|
79
|
-
assert.ok(isSecretLike(GH_TOKEN), "long token is secret-like");
|
|
80
|
-
assert.ok(isSecretLike(VAULT_VALUE), "31-char no-space value is secret-like");
|
|
81
|
-
assert.ok(!isSecretLike("short"), "short value rejected");
|
|
82
|
-
assert.ok(!isSecretLike("https://research.regendevcorp.com/mcp"), "plain URL rejected");
|
|
83
|
-
assert.ok(!isSecretLike("has spaces in it value"), "whitespace value rejected");
|
|
84
|
-
assert.ok(!isSecretLike(""), "empty rejected");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("sessionTargets returns ONLY the ending session's transcript + its sidecars", () => {
|
|
88
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-sess-"));
|
|
89
|
-
const proj = path.join(root, "C--proj");
|
|
90
|
-
const sid = "11112222-3333-4444-5555-666677778888";
|
|
91
|
-
fs.mkdirSync(path.join(proj, sid, "tool-results"), { recursive: true });
|
|
92
|
-
const transcript = path.join(proj, `${sid}.jsonl`);
|
|
93
|
-
const sidecar = path.join(proj, sid, "tool-results", "toolu_x.txt");
|
|
94
|
-
const otherSession = path.join(proj, "99990000-aaaa-bbbb-cccc-ddddeeeeffff.jsonl");
|
|
95
|
-
fs.writeFileSync(transcript, "{}\n");
|
|
96
|
-
fs.writeFileSync(sidecar, "tool output\n");
|
|
97
|
-
fs.writeFileSync(otherSession, "{}\n"); // a DIFFERENT session — must NOT be included
|
|
98
|
-
|
|
99
|
-
const targets = sessionTargets({ transcript_path: transcript, session_id: sid });
|
|
100
|
-
assert.ok(targets.includes(transcript), "includes the session transcript");
|
|
101
|
-
assert.ok(targets.includes(sidecar), "includes the session's sidecar");
|
|
102
|
-
assert.ok(!targets.includes(otherSession), "does NOT include other sessions (session-only)");
|
|
103
|
-
assert.equal(targets.length, 2, "exactly the 2 session files");
|
|
104
|
-
|
|
105
|
-
assert.deepEqual(sessionTargets(null), [], "no hook input → no targets (caller falls back)");
|
|
106
|
-
assert.deepEqual(sessionTargets({ transcript_path: path.join(root, "nope.jsonl") }), [], "missing file → none");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("loadExtraPatterns tolerates a missing/malformed file", () => {
|
|
110
|
-
assert.deepEqual(loadExtraPatterns(path.join(os.tmpdir(), "does-not-exist-xyz.json")), []);
|
|
111
|
-
const bad = path.join(os.tmpdir(), `clauth-bad-${Date.now()}.json`);
|
|
112
|
-
fs.writeFileSync(bad, "{ not json", "utf-8");
|
|
113
|
-
assert.deepEqual(loadExtraPatterns(bad), [], "malformed json → empty, never throws");
|
|
114
|
-
fs.rmSync(bad, { force: true });
|
|
115
|
-
});
|
|
1
|
+
// node --test cli/commands/scrub.test.js
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
PATTERNS,
|
|
10
|
+
findTranscripts,
|
|
11
|
+
scrubFile,
|
|
12
|
+
isSecretLike,
|
|
13
|
+
loadExtraPatterns,
|
|
14
|
+
sessionTargets,
|
|
15
|
+
} from "./scrub.js";
|
|
16
|
+
|
|
17
|
+
const GH_TOKEN = "ghp_0123456789abcdefABCDEF0123456789abcd"; // ghp_ + 36 chars
|
|
18
|
+
const VAULT_VALUE = "SuperSecretVaultValue_abc123XYZ"; // secret-like literal
|
|
19
|
+
const CUSTOM_SECRET = "MYCORP-TOKEN-998877"; // only an editable pattern catches it
|
|
20
|
+
|
|
21
|
+
function seedProjects() {
|
|
22
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-scrub-"));
|
|
23
|
+
const sess = path.join(root, "proj", "session-uuid");
|
|
24
|
+
const sidecarDir = path.join(sess, "tool-results");
|
|
25
|
+
fs.mkdirSync(sidecarDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
const jsonl = path.join(root, "proj", "session-uuid.jsonl");
|
|
28
|
+
const txt = path.join(sidecarDir, "toolu_abc.txt");
|
|
29
|
+
const body = `token=${GH_TOKEN} value=${VAULT_VALUE} custom=${CUSTOM_SECRET}\n`;
|
|
30
|
+
fs.writeFileSync(jsonl, `{"x":"${body.trim()}"}\n`, "utf-8");
|
|
31
|
+
fs.writeFileSync(txt, body, "utf-8"); // the sidecar that the old scrubber skipped
|
|
32
|
+
return { root, jsonl, txt };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("findTranscripts discovers .jsonl AND tool-results/*.txt sidecars", () => {
|
|
36
|
+
const { root, jsonl, txt } = seedProjects();
|
|
37
|
+
const found = findTranscripts(root);
|
|
38
|
+
assert.ok(found.includes(jsonl), "should find the .jsonl transcript");
|
|
39
|
+
assert.ok(found.includes(txt), "should find the .txt sidecar (the previously-skipped leak path)");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("scrubFile redacts the github token in BOTH the jsonl and the sidecar", () => {
|
|
43
|
+
const { jsonl, txt } = seedProjects();
|
|
44
|
+
for (const f of [jsonl, txt]) {
|
|
45
|
+
const n = scrubFile(f, { force: true, patterns: PATTERNS, literals: [] });
|
|
46
|
+
assert.ok(n >= 1, `expected >=1 redaction in ${path.basename(f)}`);
|
|
47
|
+
const after = fs.readFileSync(f, "utf-8");
|
|
48
|
+
assert.ok(!after.includes(GH_TOKEN), "github token must be gone");
|
|
49
|
+
assert.ok(after.includes("[GITHUB_TOKEN_REDACTED]"), "redaction marker present");
|
|
50
|
+
assert.ok(after.includes("[CLAUTH-SCRUBBED]"), "file stamped as scrubbed");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("vault-value (literal) redaction removes an arbitrary secret regardless of format", () => {
|
|
55
|
+
const { txt } = seedProjects();
|
|
56
|
+
const n = scrubFile(txt, { force: true, patterns: [], literals: [{ name: "test-svc", value: VAULT_VALUE }] });
|
|
57
|
+
assert.equal(n, 1, "exactly one literal occurrence redacted");
|
|
58
|
+
const after = fs.readFileSync(txt, "utf-8");
|
|
59
|
+
assert.ok(!after.includes(VAULT_VALUE), "vault value must be gone");
|
|
60
|
+
assert.ok(after.includes("[CLAUTH:test-svc_REDACTED]"), "labelled vault redaction present");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("editable patterns file catches a custom secret with no clauth release", () => {
|
|
64
|
+
const cfg = path.join(os.tmpdir(), `clauth-scrub-patterns-${Date.now()}.json`);
|
|
65
|
+
fs.writeFileSync(cfg, JSON.stringify([{ pattern: "MYCORP-TOKEN-\\d+", replacement: "[MYCORP_REDACTED]" }]), "utf-8");
|
|
66
|
+
const extra = loadExtraPatterns(cfg);
|
|
67
|
+
assert.equal(extra.length, 1, "one extra pattern loaded");
|
|
68
|
+
|
|
69
|
+
const { txt } = seedProjects();
|
|
70
|
+
const n = scrubFile(txt, { force: true, patterns: extra, literals: [] });
|
|
71
|
+
assert.ok(n >= 1, "custom pattern matched");
|
|
72
|
+
const after = fs.readFileSync(txt, "utf-8");
|
|
73
|
+
assert.ok(!after.includes(CUSTOM_SECRET), "custom secret gone");
|
|
74
|
+
assert.ok(after.includes("[MYCORP_REDACTED]"), "custom replacement applied");
|
|
75
|
+
fs.rmSync(cfg, { force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("isSecretLike: accepts real tokens, rejects short/url/whitespace values", () => {
|
|
79
|
+
assert.ok(isSecretLike(GH_TOKEN), "long token is secret-like");
|
|
80
|
+
assert.ok(isSecretLike(VAULT_VALUE), "31-char no-space value is secret-like");
|
|
81
|
+
assert.ok(!isSecretLike("short"), "short value rejected");
|
|
82
|
+
assert.ok(!isSecretLike("https://research.regendevcorp.com/mcp"), "plain URL rejected");
|
|
83
|
+
assert.ok(!isSecretLike("has spaces in it value"), "whitespace value rejected");
|
|
84
|
+
assert.ok(!isSecretLike(""), "empty rejected");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("sessionTargets returns ONLY the ending session's transcript + its sidecars", () => {
|
|
88
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-sess-"));
|
|
89
|
+
const proj = path.join(root, "C--proj");
|
|
90
|
+
const sid = "11112222-3333-4444-5555-666677778888";
|
|
91
|
+
fs.mkdirSync(path.join(proj, sid, "tool-results"), { recursive: true });
|
|
92
|
+
const transcript = path.join(proj, `${sid}.jsonl`);
|
|
93
|
+
const sidecar = path.join(proj, sid, "tool-results", "toolu_x.txt");
|
|
94
|
+
const otherSession = path.join(proj, "99990000-aaaa-bbbb-cccc-ddddeeeeffff.jsonl");
|
|
95
|
+
fs.writeFileSync(transcript, "{}\n");
|
|
96
|
+
fs.writeFileSync(sidecar, "tool output\n");
|
|
97
|
+
fs.writeFileSync(otherSession, "{}\n"); // a DIFFERENT session — must NOT be included
|
|
98
|
+
|
|
99
|
+
const targets = sessionTargets({ transcript_path: transcript, session_id: sid });
|
|
100
|
+
assert.ok(targets.includes(transcript), "includes the session transcript");
|
|
101
|
+
assert.ok(targets.includes(sidecar), "includes the session's sidecar");
|
|
102
|
+
assert.ok(!targets.includes(otherSession), "does NOT include other sessions (session-only)");
|
|
103
|
+
assert.equal(targets.length, 2, "exactly the 2 session files");
|
|
104
|
+
|
|
105
|
+
assert.deepEqual(sessionTargets(null), [], "no hook input → no targets (caller falls back)");
|
|
106
|
+
assert.deepEqual(sessionTargets({ transcript_path: path.join(root, "nope.jsonl") }), [], "missing file → none");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("loadExtraPatterns tolerates a missing/malformed file", () => {
|
|
110
|
+
assert.deepEqual(loadExtraPatterns(path.join(os.tmpdir(), "does-not-exist-xyz.json")), []);
|
|
111
|
+
const bad = path.join(os.tmpdir(), `clauth-bad-${Date.now()}.json`);
|
|
112
|
+
fs.writeFileSync(bad, "{ not json", "utf-8");
|
|
113
|
+
assert.deepEqual(loadExtraPatterns(bad), [], "malformed json → empty, never throws");
|
|
114
|
+
fs.rmSync(bad, { force: true });
|
|
115
|
+
});
|
package/cli/commands/serve.js
CHANGED
|
@@ -486,7 +486,7 @@ const LOG_FILE = path.join(os.tmpdir(), "clauth-serve.log");
|
|
|
486
486
|
const LIVE_PORT = 52437;
|
|
487
487
|
const STAGED_PORT = 52438;
|
|
488
488
|
const WRITE_TOKEN_BYTES = 32;
|
|
489
|
-
const WRITE_TOKEN_TTL_MS =
|
|
489
|
+
const WRITE_TOKEN_TTL_MS = 10 * 60 * 1000;
|
|
490
490
|
|
|
491
491
|
function makeWriteToken() {
|
|
492
492
|
return {
|
|
@@ -677,9 +677,6 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
677
677
|
.btn-unlock:hover{background:#2563eb}
|
|
678
678
|
.btn-unlock:disabled{background:#1e3a5f;color:#4a6fa5;cursor:not-allowed}
|
|
679
679
|
.lock-err{color:#f87171;font-size:.82rem;margin-top:.75rem;min-height:1.2em}
|
|
680
|
-
#write-unlock-overlay{display:none;position:fixed;inset:0;background:rgba(2,6,23,.72);z-index:9999;align-items:center;justify-content:center;padding:2rem}
|
|
681
|
-
.write-unlock-cancel{width:100%;background:transparent;color:#94a3b8;border:1px solid #334155;border-radius:8px;padding:8px;font-size:.85rem;cursor:pointer;margin-top:.6rem;transition:border-color .15s}
|
|
682
|
-
.write-unlock-cancel:hover{border-color:#64748b;color:#cbd5e1}
|
|
683
680
|
/* ── Main view ── */
|
|
684
681
|
#main-view{display:none;padding:2rem}
|
|
685
682
|
.header{display:flex;align-items:center;gap:10px;margin-bottom:1.5rem;flex-wrap:wrap}
|
|
@@ -933,7 +930,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
933
930
|
.supervisor-name{font-size:.82rem;color:#e2e8f0;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
934
931
|
.supervisor-row-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}
|
|
935
932
|
.supervisor-log{font-family:'Courier New',monospace;font-size:.7rem;color:#94a3b8;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow:auto}
|
|
936
|
-
.supervisor-feedback{margin-top:8px;padding:9px
|
|
933
|
+
.supervisor-feedback{margin-top:8px;padding:7px 9px;border-radius:6px;font-family:'Courier New',monospace;font-size:.72rem;color:#86efac;background:rgba(34,197,94,.08);border:1px solid rgba(74,222,128,.22);display:none}
|
|
937
934
|
.supervisor-feedback.bad{color:#fecaca;background:rgba(248,113,113,.08);border-color:rgba(248,113,113,.25)}
|
|
938
935
|
</style>
|
|
939
936
|
</head>
|
|
@@ -954,24 +951,6 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
954
951
|
</div>
|
|
955
952
|
</div>
|
|
956
953
|
|
|
957
|
-
<!-- Write-unlock modal (replaces window.prompt(), which silently no-ops in
|
|
958
|
-
embedded/webview browser contexts and after a browser suppresses repeated
|
|
959
|
-
native dialogs) -->
|
|
960
|
-
<div id="write-unlock-overlay">
|
|
961
|
-
<div class="lock-card">
|
|
962
|
-
<div class="lock-icon">🔒</div>
|
|
963
|
-
<div class="lock-title">Enable writes</div>
|
|
964
|
-
<div class="lock-sub">Enter your vault password to enable saving changes (30-minute write session)</div>
|
|
965
|
-
<form onsubmit="submitWriteUnlock();return false;" autocomplete="on">
|
|
966
|
-
<input type="text" name="username" value="clauth" autocomplete="username" style="display:none">
|
|
967
|
-
<input class="lock-input" id="write-unlock-input" type="password" placeholder="••••••••••••" autocomplete="current-password">
|
|
968
|
-
<button class="btn-unlock" id="write-unlock-btn" type="submit">Unlock Writes</button>
|
|
969
|
-
</form>
|
|
970
|
-
<div class="lock-err" id="write-unlock-err"></div>
|
|
971
|
-
<button type="button" class="write-unlock-cancel" onclick="closeWriteUnlockModal()">Cancel</button>
|
|
972
|
-
</div>
|
|
973
|
-
</div>
|
|
974
|
-
|
|
975
954
|
<!-- ── Main view (shown after unlock) ──────── -->
|
|
976
955
|
<div id="main-view">
|
|
977
956
|
<div id="upgrade-banner" style="display:none" class="upgrade-banner">
|
|
@@ -1187,6 +1166,12 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1187
1166
|
<div id="supervisor-status" class="supervisor-sub">Loading supervisor state…</div>
|
|
1188
1167
|
<div id="supervisor-feedback" class="supervisor-feedback" role="status"></div>
|
|
1189
1168
|
<div class="supervisor-grid" style="margin-top:10px">
|
|
1169
|
+
<div class="supervisor-card">
|
|
1170
|
+
<h4>Plugins</h4>
|
|
1171
|
+
<div class="supervisor-kpi" id="supervisor-plugin-count">—</div>
|
|
1172
|
+
<div class="supervisor-meta" id="supervisor-plugin-meta">awaiting data</div>
|
|
1173
|
+
<div class="supervisor-list" id="supervisor-plugins" style="margin-top:8px"></div>
|
|
1174
|
+
</div>
|
|
1190
1175
|
<div class="supervisor-card">
|
|
1191
1176
|
<h4>Surfaces</h4>
|
|
1192
1177
|
<div class="supervisor-kpi" id="supervisor-surface-count">—</div>
|
|
@@ -1509,16 +1494,21 @@ async function loadSupervisorCockpit() {
|
|
|
1509
1494
|
const status = document.getElementById("supervisor-status");
|
|
1510
1495
|
if (status) status.textContent = "Loading supervisor state…";
|
|
1511
1496
|
try {
|
|
1512
|
-
const [health, surfaces, logs] = await Promise.all([
|
|
1497
|
+
const [health, plugins, surfaces, logs] = await Promise.all([
|
|
1513
1498
|
supervisorJson("/health"),
|
|
1499
|
+
supervisorJson("/v1/plugins"),
|
|
1514
1500
|
supervisorJson("/v1/surfaces"),
|
|
1515
1501
|
supervisorJson("/v1/logs?limit=40"),
|
|
1516
1502
|
]);
|
|
1503
|
+
const pluginRows = plugins.plugins || [];
|
|
1517
1504
|
const surfaceRows = surfaces.surfaces || [];
|
|
1505
|
+
document.getElementById("supervisor-plugin-count").textContent = String(pluginRows.length);
|
|
1518
1506
|
document.getElementById("supervisor-surface-count").textContent = String(surfaceRows.length);
|
|
1519
1507
|
document.getElementById("supervisor-operation-count").textContent = String((logs.operations || []).length);
|
|
1508
|
+
document.getElementById("supervisor-plugin-meta").textContent = "current " + (pluginRows.filter(p => p.state === "current").length) + " · awaiting " + (pluginRows.filter(p => p.state === "awaiting_enable").length);
|
|
1520
1509
|
document.getElementById("supervisor-surface-meta").textContent = "pm2 home: " + (health.pm2_home || "—");
|
|
1521
1510
|
document.getElementById("supervisor-log-path").textContent = logs.log_path || "events.jsonl";
|
|
1511
|
+
document.getElementById("supervisor-plugins").innerHTML = pluginRows.length ? pluginRows.map(renderSupervisorPlugin).join("") : '<div class="supervisor-sub">No plugins discovered.</div>';
|
|
1522
1512
|
document.getElementById("supervisor-surfaces").innerHTML = surfaceRows.length ? surfaceRows.map(renderSupervisorSurface).join("") : '<div class="supervisor-sub">No surfaces declared.</div>';
|
|
1523
1513
|
document.getElementById("supervisor-events").textContent = (logs.events || []).slice(-20).reverse().map(e => {
|
|
1524
1514
|
const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
|
|
@@ -1531,13 +1521,21 @@ async function loadSupervisorCockpit() {
|
|
|
1531
1521
|
}
|
|
1532
1522
|
}
|
|
1533
1523
|
|
|
1524
|
+
function renderSupervisorPlugin(plugin) {
|
|
1525
|
+
const stateKind = plugin.state === "current" ? "ok" : (plugin.state === "manifest_invalid" || plugin.state === "missing" ? "bad" : "warn");
|
|
1526
|
+
return '<div class="supervisor-row"><div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(plugin.id) + '</span>' + supervisorBadge(plugin.state || "unknown", stateKind) + '</div>' +
|
|
1527
|
+
'<div class="supervisor-meta">' + htmlEscape(plugin.publisher || "unknown") + ' · ' + htmlEscape(plugin.version || "—") + ' · ' + htmlEscape(plugin.source || "—") + '</div>' +
|
|
1528
|
+
'<div class="supervisor-meta">' + htmlEscape((plugin.manifest_hash || "").slice(0, 12)) + '</div>' +
|
|
1529
|
+
'<div class="supervisor-row-actions"><button class="supervisor-action" onclick="runSupervisorPlugin(' + jsArg(plugin.id) + ',' + jsArg(plugin.enabled ? "disable" : "enable") + ')">' + (plugin.enabled ? "Disable" : "Enable") + '</button><button class="supervisor-action" onclick="runSupervisorPlugin(' + jsArg(plugin.id) + ',' + jsArg("test") + ')">Test</button><button class="supervisor-action" onclick="runSupervisorPlugin(' + jsArg(plugin.id) + ',' + jsArg("promote") + ')">Promote</button></div></div>';
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1534
1532
|
function renderSupervisorSurface(surface) {
|
|
1535
1533
|
const ownerKind = surface.lifecycle_owner === "clauth" ? "ok" : (surface.lifecycle_owner === "plugin" ? "warn" : "");
|
|
1536
1534
|
const stateKind = surface.state === "current" || surface.status === "healthy" ? "ok" : (surface.state === "unavailable" ? "bad" : "warn");
|
|
1537
1535
|
return '<div class="supervisor-row"><div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(surface.plugin_id + ":" + surface.id) + '</span>' + supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + supervisorBadge(surface.state || surface.status || "unknown", stateKind) + '</div>' +
|
|
1538
1536
|
'<div class="supervisor-meta">' + htmlEscape(surface.destination || "—") + ' · port ' + htmlEscape(surface.port || "—") + '</div>' +
|
|
1539
1537
|
'<div class="supervisor-meta">' + htmlEscape(surface.health || surface.health_url || "no health") + '</div>' +
|
|
1540
|
-
'<div class="supervisor-row-actions">' + ["start","stop","restart","reconcile","test","promote","rollback"].map(a => '<button class="supervisor-action" data-supervisor-action="' + a + '" onclick="runSupervisorSurface(' + jsArg(surface.
|
|
1538
|
+
'<div class="supervisor-row-actions">' + ["start","stop","restart","reconcile","test","promote","rollback"].map(a => '<button class="supervisor-action" data-supervisor-action="' + a + '" onclick="runSupervisorSurface(' + jsArg(surface.id) + ',' + jsArg(a) + ')">' + a + '</button>').join("") + '</div></div>';
|
|
1541
1539
|
}
|
|
1542
1540
|
|
|
1543
1541
|
async function rescanSupervisorPlugins() {
|
|
@@ -1548,23 +1546,21 @@ async function rescanSupervisorPlugins() {
|
|
|
1548
1546
|
} catch (err) { supervisorFeedback("Plugin rescan failed: " + (err.message || err), true); }
|
|
1549
1547
|
}
|
|
1550
1548
|
|
|
1551
|
-
async function
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1549
|
+
async function runSupervisorPlugin(id, action) {
|
|
1550
|
+
try {
|
|
1551
|
+
const receipt = await supervisorJson("/v1/plugins/" + encodeURIComponent(id) + "/" + encodeURIComponent(action), { method: "POST", headers: writeHeaders() });
|
|
1552
|
+
supervisorFeedback("Plugin " + action + " receipt " + (receipt.operationId || "recorded") + " · " + ((receipt.resulting_state && receipt.resulting_state.state) || "completed"), false);
|
|
1553
|
+
await loadSupervisorCockpit();
|
|
1554
|
+
} catch (err) { supervisorFeedback("Plugin " + action + " failed: " + (err.message || err), true); }
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
async function runSupervisorSurface(id, action) {
|
|
1555
1558
|
try {
|
|
1556
1559
|
const receipt = await supervisorJson("/v1/surfaces/" + encodeURIComponent(id) + "/actions", { method: "POST", headers: writeHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ action }) });
|
|
1557
1560
|
const result = receipt.resulting_state || {};
|
|
1558
|
-
|
|
1559
|
-
if (btn) { btn.textContent = ok ? "done" : "failed"; btn.style.background = ok ? "#14532d" : "#7f1d1d"; }
|
|
1560
|
-
supervisorFeedback(id + " " + action + " " + (ok ? "succeeded" : "failed") + " " + " -> " + " " + (result.state || result.reason || "completed"), !ok);
|
|
1561
|
-
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
1561
|
+
supervisorFeedback("Surface " + action + " receipt " + (receipt.operationId || "recorded") + " · " + (result.state || "completed"), result.ok === false);
|
|
1562
1562
|
await loadSupervisorCockpit();
|
|
1563
|
-
} catch (err) {
|
|
1564
|
-
if (btn) { btn.textContent = "failed"; btn.style.background = "#7f1d1d"; }
|
|
1565
|
-
supervisorFeedback(id + " " + action + " failed: " + (err.message || err), true);
|
|
1566
|
-
if (btn) { setTimeout(() => { btn.disabled = false; btn.textContent = originalLabel; btn.style.opacity = ""; btn.style.background = ""; }, 2500); }
|
|
1567
|
-
}
|
|
1563
|
+
} catch (err) { supervisorFeedback("Surface " + action + " failed: " + (err.message || err), true); }
|
|
1568
1564
|
}
|
|
1569
1565
|
|
|
1570
1566
|
// ── Unlock ──────────────────────────────────
|
|
@@ -1645,63 +1641,24 @@ async function lockVault() {
|
|
|
1645
1641
|
showLockScreen(r.hard_locked || false);
|
|
1646
1642
|
}
|
|
1647
1643
|
|
|
1648
|
-
//
|
|
1644
|
+
// ── Unlock writes (re-establish write scope without locking) ──
|
|
1649
1645
|
// Needed when the daemon auto-unlocks via --pw/boot.key: the page never sees the
|
|
1650
|
-
// unlock screen, so it holds no write token. POST /auth mints one (
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
// suppresses repeated dialogs for a tab ("prevent this page from creating
|
|
1656
|
-
// additional dialogs") -- both cases fail with zero visible feedback.
|
|
1657
|
-
function unlockWrites() {
|
|
1658
|
-
if (writeToken) {
|
|
1659
|
-
if (!confirm("Writes are already unlocked this session. Re-unlock?")) return;
|
|
1660
|
-
}
|
|
1661
|
-
openWriteUnlockModal();
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
function openWriteUnlockModal() {
|
|
1665
|
-
const overlay = document.getElementById("write-unlock-overlay");
|
|
1666
|
-
const input = document.getElementById("write-unlock-input");
|
|
1667
|
-
const err = document.getElementById("write-unlock-err");
|
|
1668
|
-
if (err) err.textContent = "";
|
|
1669
|
-
if (input) { input.value = ""; input.className = "lock-input"; }
|
|
1670
|
-
if (overlay) overlay.style.display = "flex";
|
|
1671
|
-
if (input) setTimeout(() => input.focus(), 50);
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
function closeWriteUnlockModal() {
|
|
1675
|
-
const overlay = document.getElementById("write-unlock-overlay");
|
|
1676
|
-
if (overlay) overlay.style.display = "none";
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
async function submitWriteUnlock() {
|
|
1680
|
-
const input = document.getElementById("write-unlock-input");
|
|
1681
|
-
const btn = document.getElementById("write-unlock-btn");
|
|
1682
|
-
const err = document.getElementById("write-unlock-err");
|
|
1683
|
-
const pw = input ? input.value : "";
|
|
1684
|
-
if (!pw) { if (err) err.textContent = "Password is required."; return; }
|
|
1685
|
-
if (btn) { btn.disabled = true; btn.textContent = "Verifying..."; }
|
|
1646
|
+
// unlock screen, so it holds no write token. POST /auth mints one (10-min TTL).
|
|
1647
|
+
async function unlockWrites() {
|
|
1648
|
+
if (writeToken && !confirm("Writes are already unlocked this session. Re-unlock?")) return;
|
|
1649
|
+
const pw = prompt("Enter your vault password to enable saving changes (10-minute write session):");
|
|
1650
|
+
if (!pw) return;
|
|
1686
1651
|
try {
|
|
1687
1652
|
const r = await fetch(BASE + "/auth", {
|
|
1688
1653
|
method: "POST",
|
|
1689
1654
|
headers: { "Content-Type": "application/json" },
|
|
1690
1655
|
body: JSON.stringify({ password: pw }),
|
|
1691
1656
|
}).then(r => r.json());
|
|
1692
|
-
if (r.error) {
|
|
1693
|
-
if (input) { input.className = "lock-input error"; setTimeout(() => input.className = "lock-input", 600); }
|
|
1694
|
-
if (err) err.textContent = "Invalid: " + (r.error || "Invalid password");
|
|
1695
|
-
return;
|
|
1696
|
-
}
|
|
1657
|
+
if (r.error) { alert("Unlock failed: " + r.error); return; }
|
|
1697
1658
|
writeToken = r.write_token || null;
|
|
1698
1659
|
refreshWriteLockUi();
|
|
1699
|
-
|
|
1700
|
-
} catch (e) {
|
|
1701
|
-
if (err) err.textContent = "Unlock error: " + (e.message || e);
|
|
1702
|
-
} finally {
|
|
1703
|
-
if (btn) { btn.disabled = false; btn.textContent = "Unlock Writes"; }
|
|
1704
|
-
}
|
|
1660
|
+
alert(writeToken ? "Writes unlocked for 10 minutes." : "Unlock did not return a write token.");
|
|
1661
|
+
} catch (e) { alert("Unlock error: " + (e.message || e)); }
|
|
1705
1662
|
}
|
|
1706
1663
|
|
|
1707
1664
|
// Reflect write-lock state on the button so it is obvious when a save will fail.
|