@lifeaitools/clauth 1.30.23 → 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 +41 -95
- 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">' +
|
|
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,37 +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
|
-
const originalLabel = btn ? btn.textContent : null;
|
|
1553
|
-
if (btn) { btn.disabled = true; btn.textContent = "..."; btn.style.opacity = "0.6"; }
|
|
1554
|
-
supervisorFeedback(action + " " + id + ": sending...", false);
|
|
1549
|
+
async function runSupervisorPlugin(id, action) {
|
|
1555
1550
|
try {
|
|
1556
|
-
const receipt = await supervisorJson("/v1/
|
|
1557
|
-
|
|
1558
|
-
const ok = result.ok !== false;
|
|
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));
|
|
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);
|
|
1562
1553
|
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
|
-
}
|
|
1554
|
+
} catch (err) { supervisorFeedback("Plugin " + action + " failed: " + (err.message || err), true); }
|
|
1568
1555
|
}
|
|
1569
1556
|
|
|
1570
|
-
async function
|
|
1571
|
-
const originalLabel = btn ? btn.textContent : null;
|
|
1572
|
-
if (btn) { btn.disabled = true; btn.textContent = "..."; btn.style.opacity = "0.6"; }
|
|
1557
|
+
async function runSupervisorSurface(id, action) {
|
|
1573
1558
|
try {
|
|
1574
|
-
const receipt = await supervisorJson("/v1/
|
|
1575
|
-
|
|
1576
|
-
|
|
1559
|
+
const receipt = await supervisorJson("/v1/surfaces/" + encodeURIComponent(id) + "/actions", { method: "POST", headers: writeHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ action }) });
|
|
1560
|
+
const result = receipt.resulting_state || {};
|
|
1561
|
+
supervisorFeedback("Surface " + action + " receipt " + (receipt.operationId || "recorded") + " · " + (result.state || "completed"), result.ok === false);
|
|
1577
1562
|
await loadSupervisorCockpit();
|
|
1578
|
-
} catch (err) {
|
|
1579
|
-
supervisorFeedback(pluginId + " " + (enable ? "enable" : "disable") + " failed: " + (err.message || err), true);
|
|
1580
|
-
if (btn) { setTimeout(() => { btn.disabled = false; btn.textContent = originalLabel; btn.style.opacity = ""; }, 2000); }
|
|
1581
|
-
}
|
|
1563
|
+
} catch (err) { supervisorFeedback("Surface " + action + " failed: " + (err.message || err), true); }
|
|
1582
1564
|
}
|
|
1583
1565
|
|
|
1584
1566
|
// ── Unlock ──────────────────────────────────
|
|
@@ -1659,60 +1641,24 @@ async function lockVault() {
|
|
|
1659
1641
|
showLockScreen(r.hard_locked || false);
|
|
1660
1642
|
}
|
|
1661
1643
|
|
|
1662
|
-
//
|
|
1644
|
+
// ── Unlock writes (re-establish write scope without locking) ──
|
|
1663
1645
|
// Needed when the daemon auto-unlocks via --pw/boot.key: the page never sees the
|
|
1664
|
-
// unlock screen, so it holds no write token. POST /auth mints one (
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
// suppresses repeated dialogs for a tab ("prevent this page from creating
|
|
1670
|
-
// additional dialogs") -- both cases fail with zero visible feedback.
|
|
1671
|
-
function unlockWrites() {
|
|
1672
|
-
openWriteUnlockModal();
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
function openWriteUnlockModal() {
|
|
1676
|
-
const overlay = document.getElementById("write-unlock-overlay");
|
|
1677
|
-
const input = document.getElementById("write-unlock-input");
|
|
1678
|
-
const err = document.getElementById("write-unlock-err");
|
|
1679
|
-
if (err) err.textContent = "";
|
|
1680
|
-
if (input) { input.value = ""; input.className = "lock-input"; }
|
|
1681
|
-
if (overlay) overlay.style.display = "flex";
|
|
1682
|
-
if (input) setTimeout(() => input.focus(), 50);
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
function closeWriteUnlockModal() {
|
|
1686
|
-
const overlay = document.getElementById("write-unlock-overlay");
|
|
1687
|
-
if (overlay) overlay.style.display = "none";
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
async function submitWriteUnlock() {
|
|
1691
|
-
const input = document.getElementById("write-unlock-input");
|
|
1692
|
-
const btn = document.getElementById("write-unlock-btn");
|
|
1693
|
-
const err = document.getElementById("write-unlock-err");
|
|
1694
|
-
const pw = input ? input.value : "";
|
|
1695
|
-
if (!pw) { if (err) err.textContent = "Password is required."; return; }
|
|
1696
|
-
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;
|
|
1697
1651
|
try {
|
|
1698
1652
|
const r = await fetch(BASE + "/auth", {
|
|
1699
1653
|
method: "POST",
|
|
1700
1654
|
headers: { "Content-Type": "application/json" },
|
|
1701
1655
|
body: JSON.stringify({ password: pw }),
|
|
1702
1656
|
}).then(r => r.json());
|
|
1703
|
-
if (r.error) {
|
|
1704
|
-
if (input) { input.className = "lock-input error"; setTimeout(() => input.className = "lock-input", 600); }
|
|
1705
|
-
if (err) err.textContent = "Invalid: " + (r.error || "Invalid password");
|
|
1706
|
-
return;
|
|
1707
|
-
}
|
|
1657
|
+
if (r.error) { alert("Unlock failed: " + r.error); return; }
|
|
1708
1658
|
writeToken = r.write_token || null;
|
|
1709
1659
|
refreshWriteLockUi();
|
|
1710
|
-
|
|
1711
|
-
} catch (e) {
|
|
1712
|
-
if (err) err.textContent = "Unlock error: " + (e.message || e);
|
|
1713
|
-
} finally {
|
|
1714
|
-
if (btn) { btn.disabled = false; btn.textContent = "Unlock Writes"; }
|
|
1715
|
-
}
|
|
1660
|
+
alert(writeToken ? "Writes unlocked for 10 minutes." : "Unlock did not return a write token.");
|
|
1661
|
+
} catch (e) { alert("Unlock error: " + (e.message || e)); }
|
|
1716
1662
|
}
|
|
1717
1663
|
|
|
1718
1664
|
// Reflect write-lock state on the button so it is obvious when a save will fail.
|