shadok-ai 0.15.56 → 0.15.58
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/README.md +6 -1
- package/dist/secret-audit.d.ts +80 -0
- package/dist/secret-audit.js +156 -0
- package/dist/secret-audit.js.map +1 -0
- package/dist/server.js +99 -28
- package/dist/server.js.map +1 -1
- package/dist/telegram.js +19 -1
- package/dist/telegram.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +107 -2
package/README.md
CHANGED
|
@@ -906,7 +906,12 @@ mode a neutral near-black.
|
|
|
906
906
|
Light/dark **mode** is an orthogonal, **per-browser** choice in the same menu
|
|
907
907
|
(default dark; stored in `localStorage`, applied before first paint) and needs no
|
|
908
908
|
endpoint — the accent is the instance's shared identity, the mode is personal),
|
|
909
|
-
`/profiles`, `/secrets`, `/
|
|
909
|
+
`/profiles`, `/secrets`, `/secrets/audit` (GET, **admin only** — the secret
|
|
910
|
+
audit log: who created/updated/deleted a secret, which profile it was attached
|
|
911
|
+
to, and which agent **received** it at spawn or in a cron guard, with the person
|
|
912
|
+
or door that caused it; names only, never a value; filters `secret`, `action`,
|
|
913
|
+
`actor`, `agent`, `since`, `limit`. Also in the vault panel → *Audit log*, and
|
|
914
|
+
🕘 on each secret), `/telegram`, `/defaults`, `/first-agent` (GET —
|
|
910
915
|
`{pending, reason}`: whether this instance's lead agent is being started
|
|
911
916
|
right now), `/version`, `/autoupdate`,
|
|
912
917
|
`/update-channel` (POST `{channel}` — `alpha` or `beta`), `/permission-mode`, `/tweak/prepare` (POST — clone/refresh shadok-ai's own source,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type SecretAuditAction = "created" | "updated" | "deleted" | "attached" | "detached" | "injected" | "cron-check";
|
|
2
|
+
export declare const SECRET_AUDIT_ACTIONS: SecretAuditAction[];
|
|
3
|
+
/** Through which door the action came. */
|
|
4
|
+
export type SecretAuditVia = "web" | "agent" | "telegram" | "cron" | "peer" | "api" | "system";
|
|
5
|
+
export interface SecretAuditRow {
|
|
6
|
+
at: number;
|
|
7
|
+
action: SecretAuditAction;
|
|
8
|
+
/** Vault NAMES concerned — never values. */
|
|
9
|
+
secrets: string[];
|
|
10
|
+
/** Who did it: an account name, `agent <id8>`, a Telegram sender, `cron <id8>`… */
|
|
11
|
+
actor: string;
|
|
12
|
+
via: SecretAuditVia;
|
|
13
|
+
/** The agent concerned (the one that received it, or the one that acted). */
|
|
14
|
+
agent?: string;
|
|
15
|
+
agentName?: string;
|
|
16
|
+
profile?: string;
|
|
17
|
+
/** Launch dir of the instance that wrote the row. */
|
|
18
|
+
instance: string;
|
|
19
|
+
detail?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Rotate past this size, keeping exactly one previous file. */
|
|
22
|
+
export declare const AUDIT_ROTATE_BYTES: number;
|
|
23
|
+
/** Build a row, or null when there is nothing to record (no secret named). An
|
|
24
|
+
* agent spawned with no secret received none, and a row saying so is noise. */
|
|
25
|
+
export declare function auditRow(r: Omit<SecretAuditRow, "at" | "secrets"> & {
|
|
26
|
+
secrets: (string | undefined | null)[];
|
|
27
|
+
}, now: number): SecretAuditRow | null;
|
|
28
|
+
/**
|
|
29
|
+
* Who started (or restarted) an agent, from what the WebSocket connection
|
|
30
|
+
* knows. The ORDER is the point: the most specific principal wins. A peer
|
|
31
|
+
* names a remote instance; a parent names the agent that delegated (pilotctl
|
|
32
|
+
* connects as the bootstrap admin, so the account alone would credit a person
|
|
33
|
+
* who did nothing); a cron or Telegram origin names a door with no account of
|
|
34
|
+
* its own. Only a plain web client is credited to its signed-in account.
|
|
35
|
+
*/
|
|
36
|
+
export declare function spawnActor(c: {
|
|
37
|
+
account?: string | null;
|
|
38
|
+
origin?: string | null;
|
|
39
|
+
peer?: string | null;
|
|
40
|
+
parent?: string | null;
|
|
41
|
+
from?: string | null;
|
|
42
|
+
}): {
|
|
43
|
+
actor: string;
|
|
44
|
+
via: SecretAuditVia;
|
|
45
|
+
};
|
|
46
|
+
/** Who made an HTTP call: an agent by its session key beats the account it
|
|
47
|
+
* authenticates as (which is the bootstrap admin for every agent). */
|
|
48
|
+
export declare function httpActor(c: {
|
|
49
|
+
agentId?: string | null;
|
|
50
|
+
account?: string | null;
|
|
51
|
+
browser: boolean;
|
|
52
|
+
}): {
|
|
53
|
+
actor: string;
|
|
54
|
+
via: SecretAuditVia;
|
|
55
|
+
agent?: string;
|
|
56
|
+
};
|
|
57
|
+
/** Names a profile write adds and removes. */
|
|
58
|
+
export declare function profileSecretDiff(before: string[] | undefined, after: string[] | undefined): {
|
|
59
|
+
added: string[];
|
|
60
|
+
removed: string[];
|
|
61
|
+
};
|
|
62
|
+
/** Parse JSONL, skipping any line that is not a row (a torn write, a hand edit). */
|
|
63
|
+
export declare function parseAudit(text: string): SecretAuditRow[];
|
|
64
|
+
export interface AuditQuery {
|
|
65
|
+
secret?: string;
|
|
66
|
+
actor?: string;
|
|
67
|
+
agent?: string;
|
|
68
|
+
action?: string;
|
|
69
|
+
since?: number;
|
|
70
|
+
limit?: number;
|
|
71
|
+
}
|
|
72
|
+
/** Newest first, narrowed. `secret` is exact (it is a name); `actor`/`agent`
|
|
73
|
+
* are substrings, since a person searches by a fragment of a name or an id. */
|
|
74
|
+
export declare function filterAudit(rows: SecretAuditRow[], q: AuditQuery): SecretAuditRow[];
|
|
75
|
+
/** Append one row. Never throws: losing an audit line must not fail the
|
|
76
|
+
* action it describes — but it says so, since a silent gap is the failure an
|
|
77
|
+
* audit exists to rule out. */
|
|
78
|
+
export declare function recordSecretAudit(r: Parameters<typeof auditRow>[0], file?: string): SecretAuditRow | null;
|
|
79
|
+
/** Every row we still hold (the rotated file first, so order is preserved). */
|
|
80
|
+
export declare function readSecretAudit(file?: string): SecretAuditRow[];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The secret AUDIT log: who wrote, attached, and above all USED which vault
|
|
3
|
+
* secret — "which agent, started by which person, received which credential".
|
|
4
|
+
*
|
|
5
|
+
* Names only, never a value: the log is read by people who must not learn the
|
|
6
|
+
* credentials from it, and a value written here would be a second copy of the
|
|
7
|
+
* vault with none of its care.
|
|
8
|
+
*
|
|
9
|
+
* Global, like the vault it describes (`~/.shadok-ai/secret-audit.jsonl`, 600),
|
|
10
|
+
* with the launch dir on every row: several instances share one vault, and an
|
|
11
|
+
* audit split per instance would hide the one that touched it. Append-only
|
|
12
|
+
* JSONL — one `appendFileSync` per row, so two instances writing at once never
|
|
13
|
+
* rewrite each other's lines the way a read-modify-write JSON file would.
|
|
14
|
+
* Bounded by rotation (one `.1` kept), not by pruning rows in place: an audit
|
|
15
|
+
* trail that edits its own past is not one.
|
|
16
|
+
*
|
|
17
|
+
* "Use" means INJECTION, which is the only moment shadok hands a value to a
|
|
18
|
+
* process: an agent's spawn (env of `claude`) and a cron guard's run (env of
|
|
19
|
+
* `sh -c`). What the process does with the value afterwards is outside what
|
|
20
|
+
* shadok can see — the log says who RECEIVED a secret, and says so.
|
|
21
|
+
*
|
|
22
|
+
* Pure cores (`auditRow`, `spawnActor`, `httpActor`, `profileSecretDiff`,
|
|
23
|
+
* `parseAudit`, `filterAudit`) are tested; the file I/O is the thin part.
|
|
24
|
+
*/
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import os from "node:os";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
export const SECRET_AUDIT_ACTIONS = [
|
|
29
|
+
"created", "updated", "deleted", "attached", "detached", "injected", "cron-check",
|
|
30
|
+
];
|
|
31
|
+
const FILE = path.join(os.homedir(), ".shadok-ai", "secret-audit.jsonl");
|
|
32
|
+
/** Rotate past this size, keeping exactly one previous file. */
|
|
33
|
+
export const AUDIT_ROTATE_BYTES = 8 * 1024 * 1024;
|
|
34
|
+
// ── Pure core ────────────────────────────────────────────────────────────
|
|
35
|
+
/** Build a row, or null when there is nothing to record (no secret named). An
|
|
36
|
+
* agent spawned with no secret received none, and a row saying so is noise. */
|
|
37
|
+
export function auditRow(r, now) {
|
|
38
|
+
const secrets = [...new Set(r.secrets.filter((s) => typeof s === "string" && !!s.trim()).map((s) => s.trim()))].sort();
|
|
39
|
+
if (!secrets.length)
|
|
40
|
+
return null;
|
|
41
|
+
const row = { at: now, action: r.action, secrets, actor: r.actor || "unknown", via: r.via, instance: r.instance };
|
|
42
|
+
if (r.agent)
|
|
43
|
+
row.agent = r.agent;
|
|
44
|
+
if (r.agentName)
|
|
45
|
+
row.agentName = r.agentName;
|
|
46
|
+
if (r.profile)
|
|
47
|
+
row.profile = r.profile;
|
|
48
|
+
if (r.detail)
|
|
49
|
+
row.detail = r.detail;
|
|
50
|
+
return row;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Who started (or restarted) an agent, from what the WebSocket connection
|
|
54
|
+
* knows. The ORDER is the point: the most specific principal wins. A peer
|
|
55
|
+
* names a remote instance; a parent names the agent that delegated (pilotctl
|
|
56
|
+
* connects as the bootstrap admin, so the account alone would credit a person
|
|
57
|
+
* who did nothing); a cron or Telegram origin names a door with no account of
|
|
58
|
+
* its own. Only a plain web client is credited to its signed-in account.
|
|
59
|
+
*/
|
|
60
|
+
export function spawnActor(c) {
|
|
61
|
+
if (c.peer)
|
|
62
|
+
return { actor: `peer ${c.peer}`, via: "peer" };
|
|
63
|
+
if (c.parent)
|
|
64
|
+
return { actor: `agent ${c.parent.slice(0, 8)}`, via: "agent" };
|
|
65
|
+
if (c.origin === "cron")
|
|
66
|
+
return { actor: "cron", via: "cron" };
|
|
67
|
+
if (c.origin === "telegram")
|
|
68
|
+
return { actor: c.from ? `telegram ${c.from}` : "telegram", via: "telegram" };
|
|
69
|
+
if (c.origin === "web")
|
|
70
|
+
return { actor: c.account || "unknown", via: "web" };
|
|
71
|
+
return { actor: c.account || c.origin || "unknown", via: c.origin ? "api" : "system" };
|
|
72
|
+
}
|
|
73
|
+
/** Who made an HTTP call: an agent by its session key beats the account it
|
|
74
|
+
* authenticates as (which is the bootstrap admin for every agent). */
|
|
75
|
+
export function httpActor(c) {
|
|
76
|
+
if (c.agentId)
|
|
77
|
+
return { actor: `agent ${c.agentId.slice(0, 8)}`, via: "agent", agent: c.agentId };
|
|
78
|
+
return { actor: c.account || "unknown", via: c.browser ? "web" : "api" };
|
|
79
|
+
}
|
|
80
|
+
/** Names a profile write adds and removes. */
|
|
81
|
+
export function profileSecretDiff(before, after) {
|
|
82
|
+
const b = new Set(before ?? []);
|
|
83
|
+
const a = new Set(after ?? []);
|
|
84
|
+
return { added: [...a].filter((n) => !b.has(n)).sort(), removed: [...b].filter((n) => !a.has(n)).sort() };
|
|
85
|
+
}
|
|
86
|
+
/** Parse JSONL, skipping any line that is not a row (a torn write, a hand edit). */
|
|
87
|
+
export function parseAudit(text) {
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const line of text.split("\n")) {
|
|
90
|
+
if (!line.trim())
|
|
91
|
+
continue;
|
|
92
|
+
try {
|
|
93
|
+
const r = JSON.parse(line);
|
|
94
|
+
if (r && typeof r.at === "number" && typeof r.action === "string" && Array.isArray(r.secrets))
|
|
95
|
+
out.push(r);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* torn line */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** Newest first, narrowed. `secret` is exact (it is a name); `actor`/`agent`
|
|
104
|
+
* are substrings, since a person searches by a fragment of a name or an id. */
|
|
105
|
+
export function filterAudit(rows, q) {
|
|
106
|
+
const actor = q.actor?.toLowerCase();
|
|
107
|
+
const agent = q.agent?.toLowerCase();
|
|
108
|
+
const out = rows.filter((r) => (!q.secret || r.secrets.includes(q.secret)) &&
|
|
109
|
+
(!q.action || r.action === q.action) &&
|
|
110
|
+
(!q.since || r.at >= q.since) &&
|
|
111
|
+
(!actor || r.actor.toLowerCase().includes(actor)) &&
|
|
112
|
+
(!agent || (r.agent ?? "").toLowerCase().includes(agent) || (r.agentName ?? "").toLowerCase().includes(agent)));
|
|
113
|
+
out.sort((a, b) => b.at - a.at);
|
|
114
|
+
return q.limit && q.limit > 0 ? out.slice(0, q.limit) : out;
|
|
115
|
+
}
|
|
116
|
+
// ── Storage ──────────────────────────────────────────────────────────────
|
|
117
|
+
/** Append one row. Never throws: losing an audit line must not fail the
|
|
118
|
+
* action it describes — but it says so, since a silent gap is the failure an
|
|
119
|
+
* audit exists to rule out. */
|
|
120
|
+
export function recordSecretAudit(r, file = FILE) {
|
|
121
|
+
const row = auditRow(r, Date.now());
|
|
122
|
+
if (!row)
|
|
123
|
+
return null;
|
|
124
|
+
try {
|
|
125
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
126
|
+
try {
|
|
127
|
+
if (fs.statSync(file).size > AUDIT_ROTATE_BYTES)
|
|
128
|
+
fs.renameSync(file, file + ".1");
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* no file yet */
|
|
132
|
+
}
|
|
133
|
+
fs.appendFileSync(file, JSON.stringify(row) + "\n", { mode: 0o600 });
|
|
134
|
+
fs.chmodSync(file, 0o600);
|
|
135
|
+
console.log(`secret-audit: ${row.action} [${row.secrets.join(",")}] by ${row.actor}${row.agent ? ` → ${row.agent.slice(0, 8)}` : ""}`);
|
|
136
|
+
return row;
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
console.log(`secret-audit: could not record ${row.action} [${row.secrets.join(",")}] — ${err.message}`);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Every row we still hold (the rotated file first, so order is preserved). */
|
|
144
|
+
export function readSecretAudit(file = FILE) {
|
|
145
|
+
let text = "";
|
|
146
|
+
for (const f of [file + ".1", file]) {
|
|
147
|
+
try {
|
|
148
|
+
text += fs.readFileSync(f, "utf8") + "\n";
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* absent */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return parseAudit(text);
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=secret-audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-audit.js","sourceRoot":"","sources":["../src/secret-audit.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
CHANGED
|
@@ -51,6 +51,7 @@ import { rowKind } from "../context/ledger-skill/ledger-core.mjs";
|
|
|
51
51
|
import { linkRefusal, markAgentPrompt, notificationText, } from "./kinship.js";
|
|
52
52
|
import { loadConfig, saveConfig, telegramConfig, applyTelegramPatch, titleForCwd, setTitleForCwd, themeForCwd, setThemeForCwd, twoFactorForCwd, setTwoFactorForCwd, backendForCwd, setBackendForCwd, } from "./config.js";
|
|
53
53
|
import { secretsFor, secretNames, setSecret, deleteSecret, secretWriteVerdict, recordOrigin, originProfile, } from "./secrets.js";
|
|
54
|
+
import { recordSecretAudit, readSecretAudit, filterAudit, spawnActor, httpActor, profileSecretDiff, } from "./secret-audit.js";
|
|
54
55
|
import { getProfile, profileArgs, permissionModeArgs, isPermissionMode, isEffortLevel, envVarsNote, seedDefaultProfiles, migrateToTracking, seedTweakProfile, loadProfiles, upsertProfile, removeProfile, promptEditVerdict, secretAttachVerdict, READONLY_DENY, TWEAK_PROFILE_NAME, promptOrigin, effectiveProfile, shippedProfile, } from "./profiles.js";
|
|
55
56
|
import { BOOTSTRAP_ADMIN, loadAccounts, verifyPassword, sessionSecret, signSession, readSessionFull, sessionAlive, saveAccounts, userWriteVerdict, newInvite, inviteVerdict, hashPassword, promptAuthor, signSessionKey, readSessionKey, } from "./accounts.js";
|
|
56
57
|
import { admins2fa, authenticationOptions, enrollCodeVerdict, isInternalClient, keepsTwoAdminsVerdict, loginNext, newEnrollCode, publicPasskey, putChallenge, readPending, registrationOptions, resetVerdict, signPending, takeChallenge, twoFactorEnableVerdict, verifyAuthentication, verifyRegistration, PENDING_TTL_MS, } from "./twofactor.js";
|
|
@@ -280,7 +281,18 @@ function runCronCheck(c, target) {
|
|
|
280
281
|
return Promise.resolve({ kind: "quiet" }); // no guard → the agent always runs
|
|
281
282
|
return new Promise((resolve) => {
|
|
282
283
|
const profile = target.profile ? getProfile(target.profile) : undefined;
|
|
283
|
-
const
|
|
284
|
+
const secretEnv = secretsFor(profile?.secrets);
|
|
285
|
+
const env = { ...process.env, ...secretEnv };
|
|
286
|
+
recordSecretAudit({
|
|
287
|
+
action: "cron-check",
|
|
288
|
+
secrets: Object.keys(secretEnv),
|
|
289
|
+
actor: `cron ${c.id.slice(0, 8)}`,
|
|
290
|
+
via: "cron",
|
|
291
|
+
agent: c.sessionId,
|
|
292
|
+
agentName: loadChannels().find((ch) => ch.sessionId === c.sessionId)?.name,
|
|
293
|
+
...(target.profile ? { profile: target.profile } : {}),
|
|
294
|
+
instance: process.cwd(),
|
|
295
|
+
});
|
|
284
296
|
execFile("sh", ["-c", c.check], { cwd: target.cwd, env, timeout: 2 * 60_000, maxBuffer: 1 << 20 }, (err, stdout, stderr) => {
|
|
285
297
|
const out = (stdout || "").trim();
|
|
286
298
|
if (out)
|
|
@@ -957,6 +969,21 @@ function callerOf(req) {
|
|
|
957
969
|
const profile = sessions.get(id)?.profile ?? loadChannels().find((c) => c.sessionId === id)?.profile ?? null;
|
|
958
970
|
return { id, profile };
|
|
959
971
|
}
|
|
972
|
+
/** Who an HTTP call to the vault/profile routes is, for the secret audit log. */
|
|
973
|
+
function auditActorOf(req) {
|
|
974
|
+
return httpActor({
|
|
975
|
+
agentId: callerOf(req).id,
|
|
976
|
+
account: currentAccount(req)?.name ?? null,
|
|
977
|
+
browser: requestFromBrowser(req),
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
/** One audit row per direction of a profile's secret list change. */
|
|
981
|
+
function auditProfileSecrets(req, profile, diff, detail) {
|
|
982
|
+
const who = auditActorOf(req);
|
|
983
|
+
const base = { ...who, profile, instance: process.cwd(), ...(detail ? { detail } : {}) };
|
|
984
|
+
recordSecretAudit({ action: "attached", secrets: diff.added, ...base });
|
|
985
|
+
recordSecretAudit({ action: "detached", secrets: diff.removed, ...base });
|
|
986
|
+
}
|
|
960
987
|
/** A browser on our own origin — the only caller allowed to change guardrails. */
|
|
961
988
|
function requestFromBrowser(req) {
|
|
962
989
|
return browserOrigin(req.headers.origin, req.headers.host, EXTRA_ORIGINS);
|
|
@@ -1725,6 +1752,7 @@ app.put("/secrets", (req, res) => {
|
|
|
1725
1752
|
if (verdict === "refused")
|
|
1726
1753
|
return res.status(409).json({ error: "exists", name: key });
|
|
1727
1754
|
setSecret(key, value);
|
|
1755
|
+
recordSecretAudit({ action: verdict, secrets: [key], ...auditActorOf(req), instance: process.cwd() });
|
|
1728
1756
|
// Remember WHO created it, so the agent can later attach it to its own
|
|
1729
1757
|
// profile (`PUT /profiles/secret`). Only on a creation: recording an
|
|
1730
1758
|
// overwrite too would let an agent clobber a human's secret to become its
|
|
@@ -1736,10 +1764,33 @@ app.put("/secrets", (req, res) => {
|
|
|
1736
1764
|
}
|
|
1737
1765
|
res.json({ names: secretNames(), result: verdict });
|
|
1738
1766
|
});
|
|
1767
|
+
// Who wrote, attached and RECEIVED which secret (names only). Admin only: it
|
|
1768
|
+
// maps every credential to the agents and people that held it. See
|
|
1769
|
+
// src/secret-audit.ts.
|
|
1770
|
+
app.get("/secrets/audit", (req, res) => {
|
|
1771
|
+
if (currentAccount(req)?.role !== "admin")
|
|
1772
|
+
return res.status(403).json({ error: "admins only" });
|
|
1773
|
+
const q = req.query;
|
|
1774
|
+
const str = (v) => (typeof v === "string" && v.trim() ? v.trim() : undefined);
|
|
1775
|
+
const limit = Math.min(Math.max(Number(q.limit) || 500, 1), 5000);
|
|
1776
|
+
const since = Number(q.since) || undefined;
|
|
1777
|
+
res.json({
|
|
1778
|
+
rows: filterAudit(readSecretAudit(), {
|
|
1779
|
+
secret: str(q.secret),
|
|
1780
|
+
actor: str(q.actor),
|
|
1781
|
+
agent: str(q.agent),
|
|
1782
|
+
action: str(q.action),
|
|
1783
|
+
since,
|
|
1784
|
+
limit,
|
|
1785
|
+
}),
|
|
1786
|
+
});
|
|
1787
|
+
});
|
|
1739
1788
|
app.delete("/secrets", (req, res) => {
|
|
1740
1789
|
const name = String(req.query.name ?? req.body?.name ?? "").trim();
|
|
1741
|
-
if (name)
|
|
1790
|
+
if (name && secretNames().includes(name)) {
|
|
1742
1791
|
deleteSecret(name);
|
|
1792
|
+
recordSecretAudit({ action: "deleted", secrets: [name], ...auditActorOf(req), instance: process.cwd() });
|
|
1793
|
+
}
|
|
1743
1794
|
res.json({ names: secretNames() });
|
|
1744
1795
|
});
|
|
1745
1796
|
// Agent profiles (global): role + permission guardrails + referenced secrets,
|
|
@@ -2461,7 +2512,9 @@ app.put("/profiles", (req, res) => {
|
|
|
2461
2512
|
};
|
|
2462
2513
|
if (b.effort !== undefined && b.effort !== "" && !isEffortLevel(b.effort))
|
|
2463
2514
|
return res.status(400).json({ error: `unknown effort level: ${String(b.effort)}` });
|
|
2515
|
+
const secretDiff = profileSecretDiff(getProfile(name)?.secrets, p.secrets);
|
|
2464
2516
|
upsertProfile(p);
|
|
2517
|
+
auditProfileSecrets(req, name, secretDiff);
|
|
2465
2518
|
res.json(loadProfiles().map((x) => ({ ...x, origin: promptOrigin(x, shippedProfile(x.name)) })));
|
|
2466
2519
|
});
|
|
2467
2520
|
/**
|
|
@@ -2538,6 +2591,8 @@ app.put("/profiles/secret", (req, res) => {
|
|
|
2538
2591
|
const secrets = [...new Set([...(existing.secrets ?? []), name])];
|
|
2539
2592
|
upsertProfile({ ...existing, secrets });
|
|
2540
2593
|
console.log(`profile-secret: ${caller} += ${name}`);
|
|
2594
|
+
if (!(existing.secrets ?? []).includes(name))
|
|
2595
|
+
recordSecretAudit({ action: "attached", secrets: [name], ...auditActorOf(req), profile: caller, instance: process.cwd() });
|
|
2541
2596
|
res.json({
|
|
2542
2597
|
ok: true,
|
|
2543
2598
|
profile: caller,
|
|
@@ -2564,14 +2619,17 @@ app.post("/reload", (req, res) => {
|
|
|
2564
2619
|
// new process is ready so it carries on. Scoped to the SELF-reload only — not
|
|
2565
2620
|
// the GUI reload, restart-all, or an auto-update respawn (those touch many/all
|
|
2566
2621
|
// agents at once, and waking every idle one would be a token storm).
|
|
2567
|
-
void restartSession(s)
|
|
2622
|
+
void restartSession(s, { actor: `agent ${id.slice(0, 8)}`, via: "agent" })
|
|
2568
2623
|
.then(() => continueAfterReload(s.id))
|
|
2569
2624
|
.catch(() => { });
|
|
2570
2625
|
});
|
|
2571
2626
|
app.delete("/profiles", (req, res) => {
|
|
2572
2627
|
const name = String(req.query.name ?? req.body?.name ?? "").trim();
|
|
2573
|
-
if (name)
|
|
2628
|
+
if (name) {
|
|
2629
|
+
const before = getProfile(name)?.secrets;
|
|
2574
2630
|
removeProfile(name);
|
|
2631
|
+
auditProfileSecrets(req, name, profileSecretDiff(before, []), "profile deleted");
|
|
2632
|
+
}
|
|
2575
2633
|
res.json(loadProfiles());
|
|
2576
2634
|
});
|
|
2577
2635
|
/** Build the /telegram state view (token is never included). */
|
|
@@ -2773,14 +2831,14 @@ app.post("/autoupdate", (req, res) => {
|
|
|
2773
2831
|
* await the loop: a caller (the toggle, the button) must not hang for minutes
|
|
2774
2832
|
* while a big fleet cycles.
|
|
2775
2833
|
*/
|
|
2776
|
-
function restartAllSessions(ids) {
|
|
2834
|
+
function restartAllSessions(ids, by = SYSTEM_SPAWN) {
|
|
2777
2835
|
// A subset (e.g. only the agents flagged as needing a Claude Code reload) when
|
|
2778
2836
|
// `ids` is given, all live sessions otherwise. Same one-at-a-time discipline.
|
|
2779
2837
|
const wanted = ids && ids.length ? new Set(ids) : null;
|
|
2780
2838
|
const live = [...sessions.values()].filter((s) => !wanted || wanted.has(s.id));
|
|
2781
2839
|
void (async () => {
|
|
2782
2840
|
for (const s of live)
|
|
2783
|
-
await restartSession(s).catch(() => { });
|
|
2841
|
+
await restartSession(s, by).catch(() => { });
|
|
2784
2842
|
})();
|
|
2785
2843
|
return live.length;
|
|
2786
2844
|
}
|
|
@@ -2793,7 +2851,7 @@ app.post("/restart-all", (req, res) => {
|
|
|
2793
2851
|
const ids = Array.isArray(req.body?.sessions)
|
|
2794
2852
|
? req.body.sessions.filter((x) => typeof x === "string" && !!x)
|
|
2795
2853
|
: undefined;
|
|
2796
|
-
const restarted = restartAllSessions(ids);
|
|
2854
|
+
const restarted = restartAllSessions(ids, auditActorOf(req));
|
|
2797
2855
|
console.log(`restart-all: respawning ${restarted} agent(s)${ids ? " (filtered)" : ""}, one at a time`);
|
|
2798
2856
|
res.json({ restarted });
|
|
2799
2857
|
});
|
|
@@ -3030,7 +3088,7 @@ app.post("/ledger", (req, res) => {
|
|
|
3030
3088
|
cfg.ledgerEnabled = ledgerEnabled;
|
|
3031
3089
|
saveConfig(cfg);
|
|
3032
3090
|
broadcastAll(versionMessage());
|
|
3033
|
-
const restarted = changed ? restartAllSessions() : 0;
|
|
3091
|
+
const restarted = changed ? restartAllSessions(undefined, auditActorOf(req)) : 0;
|
|
3034
3092
|
res.json({ ledgerEnabled, restarted });
|
|
3035
3093
|
});
|
|
3036
3094
|
/**
|
|
@@ -3384,7 +3442,7 @@ function broadcast(s, msg, except) {
|
|
|
3384
3442
|
* preserved (resume reads the transcript); every attached client keeps its ref.
|
|
3385
3443
|
* Shared by the `restart` message and by `set-profile` with `restart: true`.
|
|
3386
3444
|
*/
|
|
3387
|
-
async function restartSession(s) {
|
|
3445
|
+
async function restartSession(s, by = SYSTEM_SPAWN) {
|
|
3388
3446
|
s.restarting = true;
|
|
3389
3447
|
// The raw pipe is bound to the pilot we're about to replace — close it;
|
|
3390
3448
|
// clients re-attach on the next "ready" if the terminal is open.
|
|
@@ -3445,6 +3503,7 @@ async function restartSession(s) {
|
|
|
3445
3503
|
// A restart spawns a brand-new process, so its anchor is current: re-record
|
|
3446
3504
|
// it (clearing `legacy`), which is what makes the reload indicator go dark.
|
|
3447
3505
|
recordSpawnFingerprint(s);
|
|
3506
|
+
recordSecretInjection(s, by, "restart");
|
|
3448
3507
|
s.lastStaleSig = undefined;
|
|
3449
3508
|
// `lastTurnMs`, like the connect-path `ready`: the transient "working" above
|
|
3450
3509
|
// started the client's turn timer, and this "ready" is what stops it — without
|
|
@@ -3492,19 +3551,27 @@ function profileMessage(s) {
|
|
|
3492
3551
|
function broadcastProfile(s) {
|
|
3493
3552
|
broadcast(s, profileMessage(s));
|
|
3494
3553
|
}
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3554
|
+
const SYSTEM_SPAWN = { actor: "system", via: "system" };
|
|
3555
|
+
/**
|
|
3556
|
+
* Audit which vault secrets a FRESH process just received. An adopted tmux
|
|
3557
|
+
* pane got its env when it was first spawned (already logged then), so a
|
|
3558
|
+
* reattach records nothing — it hands no value to anyone.
|
|
3559
|
+
*/
|
|
3560
|
+
function recordSecretInjection(s, by, detail) {
|
|
3561
|
+
if (s.pilot instanceof TmuxPilot && s.pilot.attached)
|
|
3562
|
+
return;
|
|
3563
|
+
const profile = s.profile ? getProfile(s.profile) : undefined;
|
|
3564
|
+
recordSecretAudit({
|
|
3565
|
+
action: "injected",
|
|
3566
|
+
secrets: Object.keys(secretsFor(profile?.secrets)),
|
|
3567
|
+
...by,
|
|
3568
|
+
agent: s.id,
|
|
3569
|
+
agentName: loadChannels().find((c) => c.sessionId === s.id)?.name,
|
|
3570
|
+
...(s.profile ? { profile: s.profile } : {}),
|
|
3571
|
+
instance: process.cwd(),
|
|
3572
|
+
detail,
|
|
3573
|
+
});
|
|
3574
|
+
}
|
|
3508
3575
|
function recordSpawnFingerprint(s) {
|
|
3509
3576
|
const adopted = s.pilot instanceof TmuxPilot && s.pilot.attached;
|
|
3510
3577
|
if (adopted) {
|
|
@@ -3690,7 +3757,7 @@ function ensureClaudeOnce(revalidate = true) {
|
|
|
3690
3757
|
claudeReady = null; });
|
|
3691
3758
|
return p;
|
|
3692
3759
|
}
|
|
3693
|
-
async function createSession(id, cwd, args, worktree = null, profile = null, model = null, effort = null) {
|
|
3760
|
+
async function createSession(id, cwd, args, worktree = null, profile = null, model = null, effort = null, by = SYSTEM_SPAWN) {
|
|
3694
3761
|
const pilot = makePilot(id, cwd, args, profile, model, effort);
|
|
3695
3762
|
// Resumed sessions start with what the transcript already consumed.
|
|
3696
3763
|
const seededUsage = scanUsage(sessionFilePath(cwd, id));
|
|
@@ -3739,6 +3806,7 @@ async function createSession(id, cwd, args, worktree = null, profile = null, mod
|
|
|
3739
3806
|
// tell whether it drifted. No-op-safe for an adopted pane (keeps the old
|
|
3740
3807
|
// anchor); seeds `legacy` the first time a pre-feature agent is adopted.
|
|
3741
3808
|
recordSpawnFingerprint(s);
|
|
3809
|
+
recordSecretInjection(s, by, args.includes("--resume") ? "resume" : "spawn");
|
|
3742
3810
|
return s;
|
|
3743
3811
|
}
|
|
3744
3812
|
/**
|
|
@@ -4383,6 +4451,9 @@ wss.on("connection", (ws, req) => {
|
|
|
4383
4451
|
// profile's `deny` (measured), so pilotctl and the agents, which connect with
|
|
4384
4452
|
// no Origin at all (invariant 11), must not be able to reach it.
|
|
4385
4453
|
const fromBrowser = requestFromBrowser(req);
|
|
4454
|
+
// Who a respawn asked from this socket is credited to in the secret audit
|
|
4455
|
+
// log. A function, not a value: `origin` is only known once `start` arrives.
|
|
4456
|
+
const wsBy = () => spawnActor({ account: me?.name, origin, peer: callerPeer });
|
|
4386
4457
|
let session = null;
|
|
4387
4458
|
// Where does this client come from? Declared at `start` (web, cron, telegram,
|
|
4388
4459
|
// cli…), it travels with the prompt echo: other clients must be able to SAY
|
|
@@ -4663,7 +4734,7 @@ wss.on("connection", (ws, req) => {
|
|
|
4663
4734
|
const initialName = !resumed && typeof msg.name === "string" && msg.name.trim()
|
|
4664
4735
|
? msg.name.trim().slice(0, 80)
|
|
4665
4736
|
: undefined;
|
|
4666
|
-
session = await createSession(id, effectiveCwd, args, worktree, profile, model, effort);
|
|
4737
|
+
session = await createSession(id, effectiveCwd, args, worktree, profile, model, effort, spawnActor({ account: me?.name, origin: msg.origin, peer: callerPeer, parent: parentAtStart }));
|
|
4667
4738
|
session.clients.add(ws);
|
|
4668
4739
|
if (resumed) {
|
|
4669
4740
|
const turns = historyWithOffers(loadHistory(effectiveCwd, id), id);
|
|
@@ -5086,7 +5157,7 @@ wss.on("connection", (ws, req) => {
|
|
|
5086
5157
|
case "restart": {
|
|
5087
5158
|
if (!session)
|
|
5088
5159
|
return fail("no session started");
|
|
5089
|
-
await restartSession(session);
|
|
5160
|
+
await restartSession(session, wsBy());
|
|
5090
5161
|
break;
|
|
5091
5162
|
}
|
|
5092
5163
|
case "set-profile": {
|
|
@@ -5105,7 +5176,7 @@ wss.on("connection", (ws, req) => {
|
|
|
5105
5176
|
upsertChannel({ sessionId: s.id, profile: name });
|
|
5106
5177
|
broadcastProfile(s); // even without a restart: the gap shows everywhere
|
|
5107
5178
|
if (msg.restart)
|
|
5108
|
-
await restartSession(s);
|
|
5179
|
+
await restartSession(s, wsBy());
|
|
5109
5180
|
break;
|
|
5110
5181
|
}
|
|
5111
5182
|
case "set-model": {
|
|
@@ -5120,7 +5191,7 @@ wss.on("connection", (ws, req) => {
|
|
|
5120
5191
|
const model = msg.model?.trim() || null;
|
|
5121
5192
|
s.model = model ?? undefined;
|
|
5122
5193
|
upsertChannel({ sessionId: s.id, model });
|
|
5123
|
-
await restartSession(s); // respawns with the new --model (s.model)
|
|
5194
|
+
await restartSession(s, wsBy()); // respawns with the new --model (s.model)
|
|
5124
5195
|
broadcastProfile(s); // running == desired now
|
|
5125
5196
|
break;
|
|
5126
5197
|
}
|
|
@@ -5140,7 +5211,7 @@ wss.on("connection", (ws, req) => {
|
|
|
5140
5211
|
return fail(`unknown effort level: ${wanted}`);
|
|
5141
5212
|
s.effort = wanted ?? undefined;
|
|
5142
5213
|
upsertChannel({ sessionId: s.id, effort: wanted });
|
|
5143
|
-
await restartSession(s); // respawns with the new --effort
|
|
5214
|
+
await restartSession(s, wsBy()); // respawns with the new --effort
|
|
5144
5215
|
broadcastProfile(s); // running == desired now
|
|
5145
5216
|
break;
|
|
5146
5217
|
}
|