switchroom 0.15.1 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +1 -0
- package/dist/auth-broker/index.js +7 -2
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +338 -14
- package/dist/cli/ui/index.html +67 -1
- package/dist/host-control/main.js +5 -1
- package/dist/vault/approvals/kernel-server.js +1 -0
- package/dist/vault/broker/server.js +2 -1
- package/package.json +1 -1
- package/profiles/default/CLAUDE.md.hbs +18 -0
- package/telegram-plugin/dist/gateway/gateway.js +127 -6
- package/telegram-plugin/gateway/gateway.ts +33 -4
- package/telegram-plugin/gateway/model-command.ts +182 -0
- package/telegram-plugin/tests/model-command.test.ts +205 -0
- package/telegram-plugin/welcome-text.ts +7 -1
|
@@ -11348,6 +11348,7 @@ var AgentSchema = exports_external.object({
|
|
|
11348
11348
|
dangerous_mode: exports_external.boolean().optional().describe("If true, include --dangerously-skip-permissions in start.sh"),
|
|
11349
11349
|
network_isolation: NetworkIsolationSchema,
|
|
11350
11350
|
admin: exports_external.boolean().optional().describe("If true, the agent's Telegram gateway intercepts admin slash commands " + "(/agents, /logs, /restart, /delete, /update, /auth, /reconcile, etc.) " + "locally before forwarding to Claude. Commands are handled silently — " + "Claude never sees them. Requires the agent to use the switchroom-telegram " + "plugin. When false or absent, all messages pass through to Claude unchanged."),
|
|
11351
|
+
root: exports_external.boolean().optional().describe("If true, this is a ROOT-tier debugging agent: a root-privileged " + "container (runs as uid 0, mounts /var/run/docker.sock, the whole " + "~/.switchroom tree, and the host root filesystem at /host) so you " + "can DM it to debug the whole fleet — read any agent's logs, " + "docker exec into peers, edit host files — instead of SSHing into " + "the host as root. Implies admin: true (all admin slash commands). " + "Standing root power, audited via the agent's own session transcript " + "and shell history; there is no per-action approval tap. Per-agent " + "only (never set at defaults/profile layers). Grant to exactly one " + "trusted operator-private agent — it ingests other agents' output, " + "which is attacker-influenced text. See docs/root-agent.md."),
|
|
11351
11352
|
settings_raw: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Escape hatch: raw object deep-merged into the generated " + "settings.json as the final step. Use for Claude Code settings " + "keys switchroom doesn't wrap directly (e.g. effort, apiKeyHelper). " + "Power-user-only — prefer the typed fields when they exist."),
|
|
11352
11353
|
claude_md_raw: exports_external.string().optional().describe("Escape hatch: markdown text appended verbatim to CLAUDE.md on " + "initial scaffold. Not re-applied on reconcile (CLAUDE.md is " + "user-protected). Use for one-off persona tuning that isn't " + "worth a template."),
|
|
11353
11354
|
cli_args: exports_external.array(exports_external.string()).optional().describe("Escape hatch: extra arguments appended to the `exec claude` " + "invocation in start.sh. Use for Claude Code CLI flags switchroom " + "doesn't expose directly (e.g. --effort high, " + "--exclude-dynamic-system-prompt-sections)."),
|
|
@@ -11348,6 +11348,7 @@ var AgentSchema = exports_external.object({
|
|
|
11348
11348
|
dangerous_mode: exports_external.boolean().optional().describe("If true, include --dangerously-skip-permissions in start.sh"),
|
|
11349
11349
|
network_isolation: NetworkIsolationSchema,
|
|
11350
11350
|
admin: exports_external.boolean().optional().describe("If true, the agent's Telegram gateway intercepts admin slash commands " + "(/agents, /logs, /restart, /delete, /update, /auth, /reconcile, etc.) " + "locally before forwarding to Claude. Commands are handled silently — " + "Claude never sees them. Requires the agent to use the switchroom-telegram " + "plugin. When false or absent, all messages pass through to Claude unchanged."),
|
|
11351
|
+
root: exports_external.boolean().optional().describe("If true, this is a ROOT-tier debugging agent: a root-privileged " + "container (runs as uid 0, mounts /var/run/docker.sock, the whole " + "~/.switchroom tree, and the host root filesystem at /host) so you " + "can DM it to debug the whole fleet — read any agent's logs, " + "docker exec into peers, edit host files — instead of SSHing into " + "the host as root. Implies admin: true (all admin slash commands). " + "Standing root power, audited via the agent's own session transcript " + "and shell history; there is no per-action approval tap. Per-agent " + "only (never set at defaults/profile layers). Grant to exactly one " + "trusted operator-private agent — it ingests other agents' output, " + "which is attacker-influenced text. See docs/root-agent.md."),
|
|
11351
11352
|
settings_raw: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Escape hatch: raw object deep-merged into the generated " + "settings.json as the final step. Use for Claude Code settings " + "keys switchroom doesn't wrap directly (e.g. effort, apiKeyHelper). " + "Power-user-only — prefer the typed fields when they exist."),
|
|
11352
11353
|
claude_md_raw: exports_external.string().optional().describe("Escape hatch: markdown text appended verbatim to CLAUDE.md on " + "initial scaffold. Not re-applied on reconcile (CLAUDE.md is " + "user-protected). Use for one-off persona tuning that isn't " + "worth a template."),
|
|
11353
11354
|
cli_args: exports_external.array(exports_external.string()).optional().describe("Escape hatch: extra arguments appended to the `exec claude` " + "invocation in start.sh. Use for Claude Code CLI flags switchroom " + "doesn't expose directly (e.g. --effort high, " + "--exclude-dynamic-system-prompt-sections)."),
|
|
@@ -13558,7 +13559,10 @@ function enrichMirrorContent(sourceJson) {
|
|
|
13558
13559
|
function configToShape(cfg) {
|
|
13559
13560
|
const auth = cfg.auth ?? {};
|
|
13560
13561
|
const agentsMap = cfg.agents ?? {};
|
|
13561
|
-
const adminAgents = Object.entries(agentsMap).filter(([, a]) =>
|
|
13562
|
+
const adminAgents = Object.entries(agentsMap).filter(([, a]) => {
|
|
13563
|
+
const cfg2 = a;
|
|
13564
|
+
return cfg2.admin === true || cfg2.root === true;
|
|
13565
|
+
}).map(([name]) => name);
|
|
13562
13566
|
return {
|
|
13563
13567
|
agents: Object.keys(agentsMap),
|
|
13564
13568
|
consumers: (auth.consumers ?? []).map((c) => c.name),
|
|
@@ -13774,7 +13778,8 @@ class AuthBroker {
|
|
|
13774
13778
|
}
|
|
13775
13779
|
const sockPath = this.agentSocketPath(agentName);
|
|
13776
13780
|
const uid = allocateAgentUid(agentName);
|
|
13777
|
-
const
|
|
13781
|
+
const agentCfg = this.config.agents?.[agentName];
|
|
13782
|
+
const adminFlag = agentCfg?.admin === true || agentCfg?.root === true;
|
|
13778
13783
|
await this.bindListener(sockPath, uid, 432, {
|
|
13779
13784
|
kind: "agent",
|
|
13780
13785
|
name: agentName,
|
|
@@ -12096,6 +12096,7 @@ var AgentSchema = exports_external.object({
|
|
|
12096
12096
|
dangerous_mode: exports_external.boolean().optional().describe("If true, include --dangerously-skip-permissions in start.sh"),
|
|
12097
12097
|
network_isolation: NetworkIsolationSchema,
|
|
12098
12098
|
admin: exports_external.boolean().optional().describe("If true, the agent's Telegram gateway intercepts admin slash commands " + "(/agents, /logs, /restart, /delete, /update, /auth, /reconcile, etc.) " + "locally before forwarding to Claude. Commands are handled silently \u2014 " + "Claude never sees them. Requires the agent to use the switchroom-telegram " + "plugin. When false or absent, all messages pass through to Claude unchanged."),
|
|
12099
|
+
root: exports_external.boolean().optional().describe("If true, this is a ROOT-tier debugging agent: a root-privileged " + "container (runs as uid 0, mounts /var/run/docker.sock, the whole " + "~/.switchroom tree, and the host root filesystem at /host) so you " + "can DM it to debug the whole fleet \u2014 read any agent's logs, " + "docker exec into peers, edit host files \u2014 instead of SSHing into " + "the host as root. Implies admin: true (all admin slash commands). " + "Standing root power, audited via the agent's own session transcript " + "and shell history; there is no per-action approval tap. Per-agent " + "only (never set at defaults/profile layers). Grant to exactly one " + "trusted operator-private agent \u2014 it ingests other agents' output, " + "which is attacker-influenced text. See docs/root-agent.md."),
|
|
12099
12100
|
settings_raw: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Escape hatch: raw object deep-merged into the generated " + "settings.json as the final step. Use for Claude Code settings " + "keys switchroom doesn't wrap directly (e.g. effort, apiKeyHelper). " + "Power-user-only \u2014 prefer the typed fields when they exist."),
|
|
12100
12101
|
claude_md_raw: exports_external.string().optional().describe("Escape hatch: markdown text appended verbatim to CLAUDE.md on " + "initial scaffold. Not re-applied on reconcile (CLAUDE.md is " + "user-protected). Use for one-off persona tuning that isn't " + "worth a template."),
|
|
12101
12102
|
cli_args: exports_external.array(exports_external.string()).optional().describe("Escape hatch: extra arguments appended to the `exec claude` " + "invocation in start.sh. Use for Claude Code CLI flags switchroom " + "doesn't expose directly (e.g. --effort high, " + "--exclude-dynamic-system-prompt-sections)."),
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -13912,6 +13912,7 @@ var init_schema = __esm(() => {
|
|
|
13912
13912
|
dangerous_mode: exports_external.boolean().optional().describe("If true, include --dangerously-skip-permissions in start.sh"),
|
|
13913
13913
|
network_isolation: NetworkIsolationSchema,
|
|
13914
13914
|
admin: exports_external.boolean().optional().describe("If true, the agent's Telegram gateway intercepts admin slash commands " + "(/agents, /logs, /restart, /delete, /update, /auth, /reconcile, etc.) " + "locally before forwarding to Claude. Commands are handled silently \u2014 " + "Claude never sees them. Requires the agent to use the switchroom-telegram " + "plugin. When false or absent, all messages pass through to Claude unchanged."),
|
|
13915
|
+
root: exports_external.boolean().optional().describe("If true, this is a ROOT-tier debugging agent: a root-privileged " + "container (runs as uid 0, mounts /var/run/docker.sock, the whole " + "~/.switchroom tree, and the host root filesystem at /host) so you " + "can DM it to debug the whole fleet \u2014 read any agent's logs, " + "docker exec into peers, edit host files \u2014 instead of SSHing into " + "the host as root. Implies admin: true (all admin slash commands). " + "Standing root power, audited via the agent's own session transcript " + "and shell history; there is no per-action approval tap. Per-agent " + "only (never set at defaults/profile layers). Grant to exactly one " + "trusted operator-private agent \u2014 it ingests other agents' output, " + "which is attacker-influenced text. See docs/root-agent.md."),
|
|
13915
13916
|
settings_raw: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Escape hatch: raw object deep-merged into the generated " + "settings.json as the final step. Use for Claude Code settings " + "keys switchroom doesn't wrap directly (e.g. effort, apiKeyHelper). " + "Power-user-only \u2014 prefer the typed fields when they exist."),
|
|
13916
13917
|
claude_md_raw: exports_external.string().optional().describe("Escape hatch: markdown text appended verbatim to CLAUDE.md on " + "initial scaffold. Not re-applied on reconcile (CLAUDE.md is " + "user-protected). Use for one-off persona tuning that isn't " + "worth a template."),
|
|
13917
13918
|
cli_args: exports_external.array(exports_external.string()).optional().describe("Escape hatch: extra arguments appended to the `exec claude` " + "invocation in start.sh. Use for Claude Code CLI flags switchroom " + "doesn't expose directly (e.g. --effort high, " + "--exclude-dynamic-system-prompt-sections)."),
|
|
@@ -23247,7 +23248,8 @@ function describeAgents(config) {
|
|
|
23247
23248
|
resources,
|
|
23248
23249
|
strippedCaps,
|
|
23249
23250
|
networkIsolation: resolved.network_isolation === "strict" ? "strict" : "host",
|
|
23250
|
-
admin: agent.admin === true,
|
|
23251
|
+
admin: agent.admin === true || agent.root === true,
|
|
23252
|
+
root: agent.root === true,
|
|
23251
23253
|
bindMounts: agent.bind_mounts ? [...agent.bind_mounts] : [],
|
|
23252
23254
|
userEnv: { ...resolved.env ?? {} },
|
|
23253
23255
|
timezone: resolveTimezone(config, resolved)
|
|
@@ -23559,7 +23561,11 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
23559
23561
|
lines.push(` tty: true`);
|
|
23560
23562
|
lines.push(` stdin_open: true`);
|
|
23561
23563
|
lines.push(` stop_grace_period: 45s`);
|
|
23562
|
-
|
|
23564
|
+
if (a.root) {
|
|
23565
|
+
lines.push(` user: "0:0"`);
|
|
23566
|
+
} else {
|
|
23567
|
+
lines.push(` user: "${a.uid}:${a.uid}"`);
|
|
23568
|
+
}
|
|
23563
23569
|
lines.push(` mem_limit: ${a.resources.memLimit}`);
|
|
23564
23570
|
if (a.resources.memReservation !== undefined) {
|
|
23565
23571
|
lines.push(` mem_reservation: ${a.resources.memReservation}`);
|
|
@@ -23568,11 +23574,13 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
23568
23574
|
lines.push(` pids_limit: ${a.resources.pidsLimit}`);
|
|
23569
23575
|
}
|
|
23570
23576
|
lines.push(` cpus: ${a.resources.cpus.toFixed(1)}`);
|
|
23571
|
-
|
|
23572
|
-
|
|
23573
|
-
|
|
23574
|
-
|
|
23575
|
-
|
|
23577
|
+
if (!a.root) {
|
|
23578
|
+
lines.push(` security_opt:`);
|
|
23579
|
+
lines.push(` - "no-new-privileges:true"`);
|
|
23580
|
+
lines.push(` cap_drop:`);
|
|
23581
|
+
lines.push(` - "ALL"`);
|
|
23582
|
+
lines.push(` read_only: true`);
|
|
23583
|
+
}
|
|
23576
23584
|
lines.push(` tmpfs:`);
|
|
23577
23585
|
lines.push(` - /tmp:size=1g,mode=1777`);
|
|
23578
23586
|
lines.push(` depends_on:`);
|
|
@@ -23615,6 +23623,9 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
23615
23623
|
if (switchroomConfigPath) {
|
|
23616
23624
|
env2.SWITCHROOM_CONFIG = "/state/config/switchroom.yaml";
|
|
23617
23625
|
}
|
|
23626
|
+
if (a.root === true) {
|
|
23627
|
+
env2.SWITCHROOM_AGENT_ROOT = "true";
|
|
23628
|
+
}
|
|
23618
23629
|
if (a.admin === true) {
|
|
23619
23630
|
env2.SWITCHROOM_AGENT_ADMIN = "true";
|
|
23620
23631
|
}
|
|
@@ -23632,6 +23643,11 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
23632
23643
|
lines.push(` - broker-${a.name}-sock:/run/switchroom/broker`);
|
|
23633
23644
|
lines.push(` - kernel-${a.name}-sock:/run/switchroom/kernel`);
|
|
23634
23645
|
lines.push(` - auth-broker-${a.name}-sock:/run/switchroom/auth-broker`);
|
|
23646
|
+
if (a.root === true) {
|
|
23647
|
+
lines.push(` - /var/run/docker.sock:/var/run/docker.sock:rw`);
|
|
23648
|
+
lines.push(` - ${homePrefix}/.switchroom:/host-home/.switchroom:rw`);
|
|
23649
|
+
lines.push(` - /:/host:rw`);
|
|
23650
|
+
}
|
|
23635
23651
|
if (a.admin === true) {
|
|
23636
23652
|
if (existsSync14(`${hostHomeForChecks}/.switchroom/vault-audit.log`)) {
|
|
23637
23653
|
lines.push(` - ${homePrefix}/.switchroom/vault-audit.log:/state/agent/home/.switchroom/vault-audit.log:ro`);
|
|
@@ -28704,6 +28720,118 @@ var init_protocol3 = __esm(() => {
|
|
|
28704
28720
|
ResponseSchema3 = exports_external.object(ResponseEnvelope);
|
|
28705
28721
|
});
|
|
28706
28722
|
|
|
28723
|
+
// src/memory/bank-health.ts
|
|
28724
|
+
function hindsightRestBase(mcpUrl) {
|
|
28725
|
+
return mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
|
|
28726
|
+
}
|
|
28727
|
+
async function getJson(url, opts) {
|
|
28728
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
28729
|
+
const timeoutMs = opts?.timeoutMs ?? 15000;
|
|
28730
|
+
const controller = new AbortController;
|
|
28731
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
28732
|
+
try {
|
|
28733
|
+
const resp = await fetchImpl(url, { signal: controller.signal });
|
|
28734
|
+
clearTimeout(timeout);
|
|
28735
|
+
if (!resp.ok)
|
|
28736
|
+
return { ok: false, reason: `HTTP ${resp.status}` };
|
|
28737
|
+
return { ok: true, data: await resp.json() };
|
|
28738
|
+
} catch (err) {
|
|
28739
|
+
clearTimeout(timeout);
|
|
28740
|
+
if (err.name === "AbortError")
|
|
28741
|
+
return { ok: false, reason: "Timeout" };
|
|
28742
|
+
return { ok: false, reason: String(err.message ?? err) };
|
|
28743
|
+
}
|
|
28744
|
+
}
|
|
28745
|
+
async function inspectBankHealth(mcpUrl, bankId, opts) {
|
|
28746
|
+
const base = hindsightRestBase(mcpUrl);
|
|
28747
|
+
const bank = encodeURIComponent(bankId);
|
|
28748
|
+
const empty = {
|
|
28749
|
+
bankId,
|
|
28750
|
+
ok: false,
|
|
28751
|
+
totalDocuments: 0,
|
|
28752
|
+
totalFacts: 0,
|
|
28753
|
+
pendingOperations: 0,
|
|
28754
|
+
newestDocumentAt: null,
|
|
28755
|
+
unextractedDocuments: [],
|
|
28756
|
+
mentalModels: []
|
|
28757
|
+
};
|
|
28758
|
+
const stats = await getJson(`${base}/v1/default/banks/${bank}/stats`, opts);
|
|
28759
|
+
if (!stats.ok)
|
|
28760
|
+
return { ...empty, reason: stats.reason };
|
|
28761
|
+
const docs = await getJson(`${base}/v1/default/banks/${bank}/documents?limit=${DOCUMENTS_PAGE_LIMIT}`, opts);
|
|
28762
|
+
if (!docs.ok)
|
|
28763
|
+
return { ...empty, reason: docs.reason };
|
|
28764
|
+
const models = await getJson(`${base}/v1/default/banks/${bank}/mental-models`, opts);
|
|
28765
|
+
if (!models.ok)
|
|
28766
|
+
return { ...empty, reason: models.reason };
|
|
28767
|
+
const docItems = docs.data.items ?? [];
|
|
28768
|
+
let newestDocumentAt = null;
|
|
28769
|
+
const unextracted = [];
|
|
28770
|
+
for (const d of docItems) {
|
|
28771
|
+
const createdAt = d.created_at ?? "";
|
|
28772
|
+
if (createdAt && (!newestDocumentAt || createdAt > newestDocumentAt)) {
|
|
28773
|
+
newestDocumentAt = createdAt;
|
|
28774
|
+
}
|
|
28775
|
+
if ((d.memory_unit_count ?? 0) === 0 && d.id) {
|
|
28776
|
+
unextracted.push({
|
|
28777
|
+
id: d.id,
|
|
28778
|
+
createdAt,
|
|
28779
|
+
textLength: d.text_length ?? 0,
|
|
28780
|
+
memoryUnitCount: 0
|
|
28781
|
+
});
|
|
28782
|
+
}
|
|
28783
|
+
}
|
|
28784
|
+
unextracted.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
28785
|
+
return {
|
|
28786
|
+
bankId,
|
|
28787
|
+
ok: true,
|
|
28788
|
+
totalDocuments: stats.data.total_documents ?? docItems.length,
|
|
28789
|
+
totalFacts: stats.data.total_nodes ?? 0,
|
|
28790
|
+
pendingOperations: stats.data.pending_operations ?? 0,
|
|
28791
|
+
newestDocumentAt,
|
|
28792
|
+
unextractedDocuments: unextracted,
|
|
28793
|
+
mentalModels: (models.data.items ?? []).filter((m) => typeof m?.id === "string" && typeof m?.name === "string").map((m) => ({
|
|
28794
|
+
id: m.id,
|
|
28795
|
+
name: m.name,
|
|
28796
|
+
lastRefreshedAt: m.last_refreshed_at ?? null,
|
|
28797
|
+
createdAt: m.created_at ?? null,
|
|
28798
|
+
contentLength: (m.content ?? "").length,
|
|
28799
|
+
contentHead: (m.content ?? "").slice(0, 200)
|
|
28800
|
+
}))
|
|
28801
|
+
};
|
|
28802
|
+
}
|
|
28803
|
+
function ageDays(iso, now = new Date) {
|
|
28804
|
+
if (!iso)
|
|
28805
|
+
return null;
|
|
28806
|
+
const t = Date.parse(iso);
|
|
28807
|
+
if (Number.isNaN(t))
|
|
28808
|
+
return null;
|
|
28809
|
+
return Math.max(0, (now.getTime() - t) / 86400000);
|
|
28810
|
+
}
|
|
28811
|
+
function corruptedMentalModels(models) {
|
|
28812
|
+
const failurePhrase = /out of (extra )?usage|hit your (usage |session )?limit|resets \d|quota exceeded|rate.?limit/i;
|
|
28813
|
+
return models.filter((m) => {
|
|
28814
|
+
if (m.contentLength === 0)
|
|
28815
|
+
return m.lastRefreshedAt !== null;
|
|
28816
|
+
return m.contentLength < 300 && failurePhrase.test(m.contentHead);
|
|
28817
|
+
});
|
|
28818
|
+
}
|
|
28819
|
+
function staleMentalModels(models, staleDays = 7, now = new Date) {
|
|
28820
|
+
return models.filter((m) => {
|
|
28821
|
+
const age = ageDays(m.lastRefreshedAt ?? m.createdAt, now);
|
|
28822
|
+
return age !== null && age > staleDays;
|
|
28823
|
+
});
|
|
28824
|
+
}
|
|
28825
|
+
function recentUnextracted(docs, withinDays = 30, now = new Date, minTextLength = 1000) {
|
|
28826
|
+
return docs.filter((d) => {
|
|
28827
|
+
if (d.textLength < minTextLength)
|
|
28828
|
+
return false;
|
|
28829
|
+
const age = ageDays(d.createdAt, now);
|
|
28830
|
+
return age !== null && age <= withinDays;
|
|
28831
|
+
});
|
|
28832
|
+
}
|
|
28833
|
+
var DOCUMENTS_PAGE_LIMIT = 500;
|
|
28834
|
+
|
|
28707
28835
|
// src/host-control/audit-reader.ts
|
|
28708
28836
|
import { homedir as homedir20 } from "node:os";
|
|
28709
28837
|
import { join as join38 } from "node:path";
|
|
@@ -31694,6 +31822,7 @@ __export(exports_doctor, {
|
|
|
31694
31822
|
checkHindsightConsumer: () => checkHindsightConsumer,
|
|
31695
31823
|
checkDepsCacheWritable: () => checkDepsCacheWritable,
|
|
31696
31824
|
checkConfig: () => checkConfig,
|
|
31825
|
+
checkBankIngestHealth: () => checkBankIngestHealth,
|
|
31697
31826
|
checkAgents: () => checkAgents,
|
|
31698
31827
|
MFF_VAULT_KEY: () => MFF_VAULT_KEY
|
|
31699
31828
|
});
|
|
@@ -32253,6 +32382,82 @@ function probeAuthBrokerSocket(consumerName) {
|
|
|
32253
32382
|
return "unreachable";
|
|
32254
32383
|
return "missing";
|
|
32255
32384
|
}
|
|
32385
|
+
async function checkBankIngestHealth(config, url, opts) {
|
|
32386
|
+
const results = [];
|
|
32387
|
+
const now = opts?.now ?? new Date;
|
|
32388
|
+
const banks = new Map;
|
|
32389
|
+
for (const [agentName, agentConfig] of Object.entries(config.agents)) {
|
|
32390
|
+
const bankId = agentConfig.memory?.collection ?? agentName;
|
|
32391
|
+
banks.set(bankId, [...banks.get(bankId) ?? [], agentName]);
|
|
32392
|
+
}
|
|
32393
|
+
const inspected = await Promise.all([...banks].map(async ([bankId, agents]) => [bankId, agents, await inspectBankHealth(url, bankId, { fetchImpl: opts?.fetchImpl })]));
|
|
32394
|
+
for (const [bankId, agents, h] of inspected) {
|
|
32395
|
+
const label = `bank ${bankId}` + (agents[0] !== bankId ? ` (${agents.join(", ")})` : "");
|
|
32396
|
+
if (!h.ok) {
|
|
32397
|
+
results.push({
|
|
32398
|
+
name: label,
|
|
32399
|
+
status: "warn",
|
|
32400
|
+
detail: `inspection failed: ${h.reason}`
|
|
32401
|
+
});
|
|
32402
|
+
continue;
|
|
32403
|
+
}
|
|
32404
|
+
if (h.totalDocuments === 0) {
|
|
32405
|
+
results.push({
|
|
32406
|
+
name: label,
|
|
32407
|
+
status: "warn",
|
|
32408
|
+
detail: "bank is empty (no documents retained yet)"
|
|
32409
|
+
});
|
|
32410
|
+
continue;
|
|
32411
|
+
}
|
|
32412
|
+
const gaps = recentUnextracted(h.unextractedDocuments, 30, now);
|
|
32413
|
+
const stale = staleMentalModels(h.mentalModels, 7, now);
|
|
32414
|
+
const corrupted = corruptedMentalModels(h.mentalModels);
|
|
32415
|
+
const newestAge = ageDays(h.newestDocumentAt, now);
|
|
32416
|
+
const summary = `${h.totalDocuments} docs \u00b7 ${h.totalFacts} facts \u00b7 ` + `newest ${h.newestDocumentAt?.slice(0, 10) ?? "?"} \u00b7 ` + `${h.mentalModels.length} mental models`;
|
|
32417
|
+
if (corrupted.length > 0) {
|
|
32418
|
+
results.push({
|
|
32419
|
+
name: label,
|
|
32420
|
+
status: "fail",
|
|
32421
|
+
detail: `${corrupted.length} mental model(s) hold a persisted LLM-failure message instead of real content (${corrupted.map((m) => m.name).join(", ")}) \u2014 that garbage is injected into every agent turn`,
|
|
32422
|
+
fix: "A refresh ran while the LLM was quota-walled and the error string was stored as content. " + "Once quota recovers: POST /v1/default/banks/<bank>/mental-models/<id>/refresh and verify the content regenerated."
|
|
32423
|
+
});
|
|
32424
|
+
continue;
|
|
32425
|
+
}
|
|
32426
|
+
if (gaps.length > 0) {
|
|
32427
|
+
const oldest = gaps[0];
|
|
32428
|
+
results.push({
|
|
32429
|
+
name: label,
|
|
32430
|
+
status: "fail",
|
|
32431
|
+
detail: `${gaps.length} document(s) in the last 30d retained with ZERO extracted facts (oldest ${oldest.createdAt.slice(0, 10)}) \u2014 conversations from those turns are invisible to recall`,
|
|
32432
|
+
fix: "Extraction silently failed (check hindsight logs for quota/429/shm). Re-extract each: " + `curl -X POST '${hindsightDocReprocessUrl(url, bankId, oldest.id)}' (repeat per doc id)`
|
|
32433
|
+
});
|
|
32434
|
+
continue;
|
|
32435
|
+
}
|
|
32436
|
+
if (h.pendingOperations > 0 && newestAge !== null && newestAge > 1) {
|
|
32437
|
+
results.push({
|
|
32438
|
+
name: label,
|
|
32439
|
+
status: "warn",
|
|
32440
|
+
detail: `${h.pendingOperations} pending operation(s) with no new documents for ${Math.round(newestAge)}d \u2014 pipeline may be stuck`
|
|
32441
|
+
});
|
|
32442
|
+
continue;
|
|
32443
|
+
}
|
|
32444
|
+
if (stale.length > 0) {
|
|
32445
|
+
results.push({
|
|
32446
|
+
name: label,
|
|
32447
|
+
status: "warn",
|
|
32448
|
+
detail: `${summary} \u00b7 ${stale.length} stale (>7d): ${stale.map((m) => m.name).join(", ")}`,
|
|
32449
|
+
fix: "POST /v1/default/banks/<bank>/mental-models/<id>/refresh to regenerate"
|
|
32450
|
+
});
|
|
32451
|
+
continue;
|
|
32452
|
+
}
|
|
32453
|
+
results.push({ name: label, status: "ok", detail: summary });
|
|
32454
|
+
}
|
|
32455
|
+
return results;
|
|
32456
|
+
}
|
|
32457
|
+
function hindsightDocReprocessUrl(mcpUrl, bankId, docId) {
|
|
32458
|
+
const base = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
|
|
32459
|
+
return `${base}/v1/default/banks/${encodeURIComponent(bankId)}/documents/${encodeURIComponent(docId)}/reprocess`;
|
|
32460
|
+
}
|
|
32256
32461
|
async function checkHindsight(config) {
|
|
32257
32462
|
if (!isHindsightEnabled(config)) {
|
|
32258
32463
|
return [];
|
|
@@ -32303,6 +32508,7 @@ async function checkHindsight(config) {
|
|
|
32303
32508
|
}
|
|
32304
32509
|
results.push(checkHindsightConsumer(config));
|
|
32305
32510
|
results.push(...checkHindsightContainerHealth());
|
|
32511
|
+
results.push(...await checkBankIngestHealth(config, url));
|
|
32306
32512
|
for (const [agentName, agentConfig] of Object.entries(config.agents)) {
|
|
32307
32513
|
const bankId = agentConfig.memory?.collection ?? agentName;
|
|
32308
32514
|
const hasBankMission = !!agentConfig.memory?.bank_mission;
|
|
@@ -49958,8 +50164,8 @@ var {
|
|
|
49958
50164
|
} = import__.default;
|
|
49959
50165
|
|
|
49960
50166
|
// src/build-info.ts
|
|
49961
|
-
var VERSION = "0.15.
|
|
49962
|
-
var COMMIT_SHA = "
|
|
50167
|
+
var VERSION = "0.15.2";
|
|
50168
|
+
var COMMIT_SHA = "95461524";
|
|
49963
50169
|
|
|
49964
50170
|
// src/cli/agent.ts
|
|
49965
50171
|
init_source();
|
|
@@ -51772,7 +51978,8 @@ function buildWorkspaceContext(args) {
|
|
|
51772
51978
|
botToken: resolvedBotToken ?? rawBotToken,
|
|
51773
51979
|
forumChatId: telegramConfig.forum_chat_id,
|
|
51774
51980
|
dangerousMode: agentConfig.dangerous_mode === true,
|
|
51775
|
-
admin: agentConfig.admin === true,
|
|
51981
|
+
admin: agentConfig.admin === true || agentConfig.root === true,
|
|
51982
|
+
root: agentConfig.root === true,
|
|
51776
51983
|
useSwitchroomPlugin: usesSwitchroomTelegramPlugin(agentConfig),
|
|
51777
51984
|
useHotReloadStable: agentConfig.channels?.telegram?.hotReloadStable === true,
|
|
51778
51985
|
telegramEnabledFlag: agentConfig.channels?.telegram?.enabled === false ? "false" : "true",
|
|
@@ -52124,7 +52331,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
52124
52331
|
alwaysLoad: true
|
|
52125
52332
|
}
|
|
52126
52333
|
};
|
|
52127
|
-
if (agentConfig.admin === true) {
|
|
52334
|
+
if (agentConfig.admin === true || agentConfig.root === true) {
|
|
52128
52335
|
mcpServers["hostd"] = {
|
|
52129
52336
|
command: switchroomCliPath,
|
|
52130
52337
|
args: ["mcp", "hostd"],
|
|
@@ -52945,7 +53152,8 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
52945
53152
|
model: agentConfig.model,
|
|
52946
53153
|
schedule: agentConfig.schedule,
|
|
52947
53154
|
useSwitchroomPlugin: usesSwitchroomTelegramPlugin(agentConfig),
|
|
52948
|
-
admin: agentConfig.admin === true
|
|
53155
|
+
admin: agentConfig.admin === true || agentConfig.root === true,
|
|
53156
|
+
root: agentConfig.root === true
|
|
52949
53157
|
};
|
|
52950
53158
|
let rendered = renderTemplate(claudeMdSrc, claudeContext);
|
|
52951
53159
|
const vaultProtocol = renderVaultProtocolFragment(claudeContext);
|
|
@@ -53174,7 +53382,7 @@ ${body}
|
|
|
53174
53382
|
alwaysLoad: true
|
|
53175
53383
|
}
|
|
53176
53384
|
};
|
|
53177
|
-
if (agentConfig.admin === true) {
|
|
53385
|
+
if (agentConfig.admin === true || agentConfig.root === true) {
|
|
53178
53386
|
mcpServers["hostd"] = {
|
|
53179
53387
|
command: switchroomCliPath,
|
|
53180
53388
|
args: ["mcp", "hostd"],
|
|
@@ -56220,10 +56428,12 @@ function registerAgentCommand(program3) {
|
|
|
56220
56428
|
const agentConfig = config.agents[name];
|
|
56221
56429
|
const status = statuses[name];
|
|
56222
56430
|
const sched = schedulerStates[name];
|
|
56431
|
+
const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
|
|
56223
56432
|
return {
|
|
56224
56433
|
name,
|
|
56225
56434
|
status: status?.active ?? "unknown",
|
|
56226
56435
|
uptime: formatUptime2(status?.uptime ?? null),
|
|
56436
|
+
model: resolved.model ?? SWITCHROOM_DEFAULT_MAIN_MODEL,
|
|
56227
56437
|
extends: agentConfig.extends ?? "default",
|
|
56228
56438
|
topic_name: agentConfig.topic_name,
|
|
56229
56439
|
topic_emoji: agentConfig.topic_emoji,
|
|
@@ -63747,7 +63957,7 @@ class VaultBroker {
|
|
|
63747
63957
|
const isGrantMgmtOp = req.op === "mint_grant" || req.op === "list_grants" || req.op === "revoke_grant";
|
|
63748
63958
|
let mintPassphraseAttested = false;
|
|
63749
63959
|
if (isGrantMgmtOp) {
|
|
63750
|
-
const isAdminAgent = agentName !== null && this.config?.agents?.[agentName]?.admin === true;
|
|
63960
|
+
const isAdminAgent = agentName !== null && (this.config?.agents?.[agentName]?.admin === true || this.config?.agents?.[agentName]?.root === true);
|
|
63751
63961
|
if ((req.op === "mint_grant" || req.op === "list_grants") && req.passphrase !== undefined && req.passphrase !== "") {
|
|
63752
63962
|
if (req.attest_via_posture === true) {
|
|
63753
63963
|
writeAudit({
|
|
@@ -73178,6 +73388,66 @@ async function handleGetApprovals() {
|
|
|
73178
73388
|
const sorted = [...decisions].sort((a, b) => b.granted_at - a.granted_at);
|
|
73179
73389
|
return { reachable: true, decisions: sorted };
|
|
73180
73390
|
}
|
|
73391
|
+
async function handleGetMemoryHealth(config, opts) {
|
|
73392
|
+
const url = config.memory?.config?.url ?? "http://127.0.0.1:18888/mcp/";
|
|
73393
|
+
const now = opts?.now ?? new Date;
|
|
73394
|
+
let reachable = false;
|
|
73395
|
+
try {
|
|
73396
|
+
reachable = (await probeHindsight(url, { fetchImpl: opts?.fetchImpl })).ok;
|
|
73397
|
+
} catch {
|
|
73398
|
+
reachable = false;
|
|
73399
|
+
}
|
|
73400
|
+
if (!reachable)
|
|
73401
|
+
return { reachable, url, banks: [] };
|
|
73402
|
+
const banks = new Map;
|
|
73403
|
+
for (const agentName of Object.keys(config.agents)) {
|
|
73404
|
+
const bank = getCollectionForAgent(agentName, config);
|
|
73405
|
+
banks.set(bank, [...banks.get(bank) ?? [], agentName]);
|
|
73406
|
+
}
|
|
73407
|
+
const rows = await Promise.all([...banks].map(async ([bank, agents]) => {
|
|
73408
|
+
const h = await inspectBankHealth(url, bank, { fetchImpl: opts?.fetchImpl });
|
|
73409
|
+
const gaps = recentUnextracted(h.unextractedDocuments, 30, now);
|
|
73410
|
+
const stale = staleMentalModels(h.mentalModels, 7, now);
|
|
73411
|
+
const corrupted = corruptedMentalModels(h.mentalModels);
|
|
73412
|
+
let status = "ok";
|
|
73413
|
+
let statusDetail = "facts flowing";
|
|
73414
|
+
if (!h.ok) {
|
|
73415
|
+
status = "warn";
|
|
73416
|
+
statusDetail = `inspection failed: ${h.reason ?? "unknown"}`;
|
|
73417
|
+
} else if (corrupted.length > 0) {
|
|
73418
|
+
status = "fail";
|
|
73419
|
+
statusDetail = `${corrupted.length} mental model(s) corrupted by an LLM failure message \u2014 injected into every turn until refreshed`;
|
|
73420
|
+
} else if (gaps.length > 0) {
|
|
73421
|
+
status = "fail";
|
|
73422
|
+
statusDetail = `${gaps.length} recent conversation(s) stored with zero extracted facts \u2014 invisible to recall`;
|
|
73423
|
+
} else if (stale.length > 0) {
|
|
73424
|
+
status = "warn";
|
|
73425
|
+
statusDetail = `${stale.length} mental model(s) not refreshed in >7d`;
|
|
73426
|
+
} else if (h.totalDocuments === 0) {
|
|
73427
|
+
status = "warn";
|
|
73428
|
+
statusDetail = "bank is empty";
|
|
73429
|
+
}
|
|
73430
|
+
return {
|
|
73431
|
+
bank,
|
|
73432
|
+
agents,
|
|
73433
|
+
ok: h.ok,
|
|
73434
|
+
reason: h.reason,
|
|
73435
|
+
totalDocuments: h.totalDocuments,
|
|
73436
|
+
totalFacts: h.totalFacts,
|
|
73437
|
+
pendingOperations: h.pendingOperations,
|
|
73438
|
+
newestDocumentAt: h.newestDocumentAt,
|
|
73439
|
+
recentUnextractedCount: gaps.length,
|
|
73440
|
+
oldestUnextractedAt: gaps[0]?.createdAt ?? null,
|
|
73441
|
+
mentalModels: h.mentalModels,
|
|
73442
|
+
staleMentalModelCount: stale.length,
|
|
73443
|
+
corruptedMentalModelNames: corrupted.map((m) => m.name),
|
|
73444
|
+
status,
|
|
73445
|
+
statusDetail
|
|
73446
|
+
};
|
|
73447
|
+
}));
|
|
73448
|
+
rows.sort((a, b) => a.bank.localeCompare(b.bank));
|
|
73449
|
+
return { reachable, url, banks: rows };
|
|
73450
|
+
}
|
|
73181
73451
|
|
|
73182
73452
|
// src/web/webhook-handler.ts
|
|
73183
73453
|
import { appendFileSync as appendFileSync3, existsSync as existsSync48, mkdirSync as mkdirSync27, readFileSync as readFileSync43, writeFileSync as writeFileSync25 } from "fs";
|
|
@@ -73820,6 +74090,9 @@ function parseRoute(pathname, method) {
|
|
|
73820
74090
|
if (method === "GET" && pathname === "/api/system-health") {
|
|
73821
74091
|
return { handler: "getSystemHealth", params: {} };
|
|
73822
74092
|
}
|
|
74093
|
+
if (method === "GET" && pathname === "/api/memory-health") {
|
|
74094
|
+
return { handler: "getMemoryHealth", params: {} };
|
|
74095
|
+
}
|
|
73823
74096
|
if (method === "GET" && pathname === "/api/google-accounts") {
|
|
73824
74097
|
return { handler: "getGoogleAccounts", params: {} };
|
|
73825
74098
|
}
|
|
@@ -73981,6 +74254,8 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
|
|
|
73981
74254
|
}
|
|
73982
74255
|
case "getSystemHealth":
|
|
73983
74256
|
return (async () => jsonResponse(await handleGetSystemHealth(config)))();
|
|
74257
|
+
case "getMemoryHealth":
|
|
74258
|
+
return (async () => jsonResponse(await handleGetMemoryHealth(freshConfig())))();
|
|
73984
74259
|
case "getGoogleAccounts":
|
|
73985
74260
|
return (async () => jsonResponse(await handleGetGoogleAccounts(freshConfig())))();
|
|
73986
74261
|
case "getMicrosoftAccounts":
|
|
@@ -79590,7 +79865,14 @@ async function ensureSwitchroomFolder2(deps, agentName) {
|
|
|
79590
79865
|
const top = await ensureFolder2(deps, "Switchroom", "root");
|
|
79591
79866
|
return ensureFolder2(deps, agentName, top.id);
|
|
79592
79867
|
}
|
|
79868
|
+
var GDRIVE_MULTIPART_MAX_BYTES = 5 * 1024 * 1024;
|
|
79593
79869
|
async function uploadFile2(deps, parentId, filename, bytes, mimeType = "application/octet-stream") {
|
|
79870
|
+
if (bytes.byteLength <= GDRIVE_MULTIPART_MAX_BYTES) {
|
|
79871
|
+
return uploadMultipart(deps, parentId, filename, bytes, mimeType);
|
|
79872
|
+
}
|
|
79873
|
+
return uploadResumable(deps, parentId, filename, bytes, mimeType);
|
|
79874
|
+
}
|
|
79875
|
+
async function uploadMultipart(deps, parentId, filename, bytes, mimeType = "application/octet-stream") {
|
|
79594
79876
|
const f = deps.fetchImpl ?? fetch;
|
|
79595
79877
|
const boundary = "switchroom-deliver-boundary";
|
|
79596
79878
|
const metadata = JSON.stringify({ name: filename, parents: [parentId] });
|
|
@@ -79622,6 +79904,48 @@ Content-Type: ${mimeType}\r
|
|
|
79622
79904
|
}
|
|
79623
79905
|
return await resp.json();
|
|
79624
79906
|
}
|
|
79907
|
+
async function uploadResumable(deps, parentId, filename, bytes, mimeType = "application/octet-stream") {
|
|
79908
|
+
const f = deps.fetchImpl ?? fetch;
|
|
79909
|
+
const init = await f(`${UPLOAD}/files?uploadType=resumable&fields=id,name,webViewLink`, {
|
|
79910
|
+
method: "POST",
|
|
79911
|
+
headers: {
|
|
79912
|
+
Authorization: `Bearer ${deps.accessToken}`,
|
|
79913
|
+
"Content-Type": "application/json; charset=UTF-8",
|
|
79914
|
+
"X-Upload-Content-Type": mimeType
|
|
79915
|
+
},
|
|
79916
|
+
body: JSON.stringify({ name: filename, parents: [parentId] })
|
|
79917
|
+
});
|
|
79918
|
+
if (!init.ok) {
|
|
79919
|
+
throw new Error(`Drive resumable init failed: HTTP ${init.status} \u2014 ${await readBody2(init)}`);
|
|
79920
|
+
}
|
|
79921
|
+
const sessionUrl = init.headers.get("location") ?? init.headers.get("Location");
|
|
79922
|
+
if (!sessionUrl) {
|
|
79923
|
+
throw new Error("Drive resumable init returned no session URL (Location header)");
|
|
79924
|
+
}
|
|
79925
|
+
const CHUNK = 8 * 1024 * 1024;
|
|
79926
|
+
const total = bytes.byteLength;
|
|
79927
|
+
let lastFile = null;
|
|
79928
|
+
for (let start = 0;start < total; start += CHUNK) {
|
|
79929
|
+
const end = Math.min(start + CHUNK, total);
|
|
79930
|
+
const chunk2 = bytes.subarray(start, end);
|
|
79931
|
+
const put = await f(sessionUrl, {
|
|
79932
|
+
method: "PUT",
|
|
79933
|
+
headers: {
|
|
79934
|
+
"Content-Length": String(chunk2.byteLength),
|
|
79935
|
+
"Content-Range": `bytes ${start}-${end - 1}/${total}`
|
|
79936
|
+
},
|
|
79937
|
+
body: chunk2
|
|
79938
|
+
});
|
|
79939
|
+
if (put.status === 200 || put.status === 201) {
|
|
79940
|
+
lastFile = await put.json();
|
|
79941
|
+
} else if (put.status !== 308) {
|
|
79942
|
+
throw new Error(`Drive resumable chunk failed: HTTP ${put.status} \u2014 ${await readBody2(put)}`);
|
|
79943
|
+
}
|
|
79944
|
+
}
|
|
79945
|
+
if (!lastFile)
|
|
79946
|
+
throw new Error("Drive resumable upload completed without a final file resource");
|
|
79947
|
+
return lastFile;
|
|
79948
|
+
}
|
|
79625
79949
|
async function createShareLink2(deps, file, scopes = ["anyone"]) {
|
|
79626
79950
|
const f = deps.fetchImpl ?? fetch;
|
|
79627
79951
|
for (const type of scopes) {
|