switchroom 0.15.7 → 0.15.8
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 +102 -5
- package/dist/auth-broker/index.js +18 -0
- package/dist/cli/notion-write-pretool.mjs +18 -0
- package/dist/cli/switchroom.js +113 -24
- package/dist/host-control/main.js +18 -0
- package/dist/vault/approvals/kernel-server.js +19 -1
- package/dist/vault/broker/server.js +19 -1
- package/package.json +1 -1
- package/profiles/_shared/agent-self-service.md.hbs +24 -5
- package/telegram-plugin/dist/gateway/gateway.js +171 -9
- package/telegram-plugin/gateway/gateway.ts +136 -2
- package/telegram-plugin/gateway/reaction-dispatch.ts +174 -0
- package/telegram-plugin/tests/reaction-dispatch.test.ts +137 -0
|
@@ -11232,6 +11232,10 @@ var ReactionsSchema = exports_external.object({
|
|
|
11232
11232
|
per_hour_cap: exports_external.number().int().nonnegative().optional().describe("Max reaction-triggered synthetic turns per chat per rolling hour. " + "Refusals are stderr-logged but not surfaced to the agent. " + "Default 10. Set to 0 to disable triggering via the cap path."),
|
|
11233
11233
|
group_admin_only: exports_external.boolean().optional().describe("In groups/supergroups (negative chat_id), only trigger a synthetic " + "turn when the reacter is a chat admin (creator or administrator). " + "Failing the lookup is treated as non-admin (fail-closed). " + "DMs are never affected by this flag — the reacter IS the user. " + "Default true.")
|
|
11234
11234
|
}).optional();
|
|
11235
|
+
var ReactionDispatchSchema = exports_external.object({
|
|
11236
|
+
enabled: exports_external.boolean().optional().describe("Master switch for the reaction-dispatch path. Default false — " + "with no reaction_dispatch block, reactions are persisted (and may " + "feed the `reactions` feedback path) but are NEVER dispatched as " + "event-driven inbound turns."),
|
|
11237
|
+
emojis: exports_external.array(exports_external.string()).optional().describe('Emoji allowlist that triggers a `<channel event="reaction">` ' + "inbound turn when reacted to any message. Default [] (nothing " + "fires). Cascade mode: REPLACE (not union) — a layer's list " + "replaces lower layers entirely so an operator can narrow per-agent.")
|
|
11238
|
+
}).optional();
|
|
11235
11239
|
var ReleaseBlock = exports_external.object({
|
|
11236
11240
|
channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
|
|
11237
11241
|
pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
|
|
@@ -11267,6 +11271,7 @@ var profileFields = {
|
|
|
11267
11271
|
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
11268
11272
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
|
|
11269
11273
|
reactions: ReactionsSchema,
|
|
11274
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
11270
11275
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
|
|
11271
11276
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
11272
11277
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Omit to use Claude's default (acceptEdits for switchroom agents). " + "Warning: bypassPermissions and dontAsk skip all safety checks — use only in trusted sandboxes."),
|
|
@@ -11336,6 +11341,7 @@ var AgentSchema = exports_external.object({
|
|
|
11336
11341
|
schedule: exports_external.array(ScheduleEntrySchema).default([]),
|
|
11337
11342
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
|
|
11338
11343
|
reactions: ReactionsSchema,
|
|
11344
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
11339
11345
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
|
|
11340
11346
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
11341
11347
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Per-agent override wins over defaults.permission_mode. " + "Warning: bypassPermissions and dontAsk skip all safety checks — use only in trusted sandboxes."),
|
|
@@ -12001,6 +12007,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
12001
12007
|
}
|
|
12002
12008
|
merged.reactions = combined;
|
|
12003
12009
|
}
|
|
12010
|
+
const dReactionDispatch = defaults.reaction_dispatch;
|
|
12011
|
+
const mReactionDispatch = merged.reaction_dispatch;
|
|
12012
|
+
if (dReactionDispatch || mReactionDispatch) {
|
|
12013
|
+
const base = dReactionDispatch ?? {};
|
|
12014
|
+
const override = mReactionDispatch ?? {};
|
|
12015
|
+
const combined = { ...base };
|
|
12016
|
+
for (const [k, v] of Object.entries(override)) {
|
|
12017
|
+
if (v !== undefined)
|
|
12018
|
+
combined[k] = v;
|
|
12019
|
+
}
|
|
12020
|
+
merged.reaction_dispatch = combined;
|
|
12021
|
+
}
|
|
12004
12022
|
if (defaults.resources || merged.resources) {
|
|
12005
12023
|
const d = defaults.resources ?? {};
|
|
12006
12024
|
const a = merged.resources ?? {};
|
|
@@ -14364,6 +14382,43 @@ function registerAgentSchedule(opts) {
|
|
|
14364
14382
|
}
|
|
14365
14383
|
return tasks;
|
|
14366
14384
|
}
|
|
14385
|
+
function scheduleSignature(entries) {
|
|
14386
|
+
return JSON.stringify(entries);
|
|
14387
|
+
}
|
|
14388
|
+
function createScheduleReloader(opts) {
|
|
14389
|
+
let tasks = opts.initialTasks;
|
|
14390
|
+
let sig = scheduleSignature(opts.initialEntries);
|
|
14391
|
+
return {
|
|
14392
|
+
tick() {
|
|
14393
|
+
let next;
|
|
14394
|
+
try {
|
|
14395
|
+
next = opts.loadEntries();
|
|
14396
|
+
} catch (err) {
|
|
14397
|
+
opts.onError?.(err);
|
|
14398
|
+
return;
|
|
14399
|
+
}
|
|
14400
|
+
const nextSig = scheduleSignature(next);
|
|
14401
|
+
if (nextSig === sig)
|
|
14402
|
+
return;
|
|
14403
|
+
const before = tasks.length;
|
|
14404
|
+
for (const t of tasks)
|
|
14405
|
+
t.task.stop();
|
|
14406
|
+
tasks = opts.register(next);
|
|
14407
|
+
sig = nextSig;
|
|
14408
|
+
opts.log(`schedule reloaded: ${before} → ${tasks.length} task(s)`);
|
|
14409
|
+
},
|
|
14410
|
+
currentTasks() {
|
|
14411
|
+
return tasks;
|
|
14412
|
+
}
|
|
14413
|
+
};
|
|
14414
|
+
}
|
|
14415
|
+
function resolveReloadPollMs(env) {
|
|
14416
|
+
const raw = Number.parseInt(env.SWITCHROOM_SCHEDULER_RELOAD_POLL_MS ?? "", 10);
|
|
14417
|
+
return Number.isFinite(raw) && raw >= 1000 ? raw : 30000;
|
|
14418
|
+
}
|
|
14419
|
+
function isHotReloadEnabled(env) {
|
|
14420
|
+
return env.SWITCHROOM_SCHEDULER_HOT_RELOAD !== "0";
|
|
14421
|
+
}
|
|
14367
14422
|
function ipcDispatcher(client) {
|
|
14368
14423
|
return {
|
|
14369
14424
|
sendToAgent(agentName, inbound) {
|
|
@@ -14405,8 +14460,28 @@ async function main() {
|
|
|
14405
14460
|
if (entries.length === 0) {
|
|
14406
14461
|
process.stdout.write(`agent-scheduler: ${agentName} has no schedule entries — idling ` + `(re-checks on container restart)
|
|
14407
14462
|
`);
|
|
14408
|
-
|
|
14463
|
+
let idleTimer;
|
|
14464
|
+
if (isHotReloadEnabled(process.env)) {
|
|
14465
|
+
idleTimer = setInterval(() => {
|
|
14466
|
+
let appeared;
|
|
14467
|
+
try {
|
|
14468
|
+
appeared = collectScheduleEntries(loadConfig(configPath)).filter((e) => e.agent === agentName);
|
|
14469
|
+
} catch {
|
|
14470
|
+
return;
|
|
14471
|
+
}
|
|
14472
|
+
if (appeared.length > 0) {
|
|
14473
|
+
process.stdout.write(`agent-scheduler: ${agentName} schedule appeared (${appeared.length} ` + `entr${appeared.length === 1 ? "y" : "ies"}) — restarting to activate
|
|
14474
|
+
`);
|
|
14475
|
+
clearInterval(idleTimer);
|
|
14476
|
+
releaseLock(lockPath);
|
|
14477
|
+
process.exit(0);
|
|
14478
|
+
}
|
|
14479
|
+
}, resolveReloadPollMs(process.env));
|
|
14480
|
+
} else {
|
|
14481
|
+
idleTimer = setInterval(() => {}, 1 << 30);
|
|
14482
|
+
}
|
|
14409
14483
|
const cleanup = () => {
|
|
14484
|
+
clearInterval(idleTimer);
|
|
14410
14485
|
releaseLock(lockPath);
|
|
14411
14486
|
process.exit(0);
|
|
14412
14487
|
};
|
|
@@ -14551,8 +14626,8 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
|
|
|
14551
14626
|
process.stdout.write(`agent-scheduler: ${agentName} cheap-cron ENABLED` + (recovered > 0 ? ` (recovered ${recovered} pending escalation(s))` : "") + `
|
|
14552
14627
|
`);
|
|
14553
14628
|
}
|
|
14554
|
-
const
|
|
14555
|
-
entries,
|
|
14629
|
+
const registerForEntries = (es) => registerAgentSchedule({
|
|
14630
|
+
entries: es,
|
|
14556
14631
|
channel,
|
|
14557
14632
|
sink,
|
|
14558
14633
|
cronLib,
|
|
@@ -14560,10 +14635,28 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
|
|
|
14560
14635
|
...quotaGate ? { quotaGate } : {},
|
|
14561
14636
|
...cheapCron ? { cheapCron } : {}
|
|
14562
14637
|
});
|
|
14638
|
+
const tasks = registerForEntries(entries);
|
|
14563
14639
|
process.stdout.write(`agent-scheduler: ${agentName} registered ${tasks.length} task(s); ` + `chat=${channel.chatId} thread=${channel.threadId ?? "(none)"} ` + `socket=${socketPath} jsonl=${jsonlPath}
|
|
14564
14640
|
`);
|
|
14641
|
+
let reloader;
|
|
14642
|
+
let reloadTimer;
|
|
14643
|
+
if (isHotReloadEnabled(process.env)) {
|
|
14644
|
+
reloader = createScheduleReloader({
|
|
14645
|
+
loadEntries: () => collectScheduleEntries(loadConfig(configPath)).filter((e) => e.agent === agentName),
|
|
14646
|
+
register: registerForEntries,
|
|
14647
|
+
initialTasks: tasks,
|
|
14648
|
+
initialEntries: entries,
|
|
14649
|
+
log: (m) => process.stdout.write(`agent-scheduler: ${agentName} ${m}
|
|
14650
|
+
`),
|
|
14651
|
+
onError: (e) => process.stderr.write(`agent-scheduler: ${agentName} reload skipped (config error, keeping current schedule): ${e.message}
|
|
14652
|
+
`)
|
|
14653
|
+
});
|
|
14654
|
+
reloadTimer = setInterval(() => reloader.tick(), resolveReloadPollMs(process.env));
|
|
14655
|
+
}
|
|
14565
14656
|
const shutdown = () => {
|
|
14566
|
-
|
|
14657
|
+
if (reloadTimer)
|
|
14658
|
+
clearInterval(reloadTimer);
|
|
14659
|
+
for (const t of reloader ? reloader.currentTasks() : tasks)
|
|
14567
14660
|
t.task.stop();
|
|
14568
14661
|
sink.close();
|
|
14569
14662
|
ipcClient.close();
|
|
@@ -14581,10 +14674,14 @@ if (import.meta.url === `file://${process.argv[1]}` && /(?:^|[/\\])agent-schedul
|
|
|
14581
14674
|
});
|
|
14582
14675
|
}
|
|
14583
14676
|
export {
|
|
14677
|
+
scheduleSignature,
|
|
14678
|
+
resolveReloadPollMs,
|
|
14584
14679
|
resolveEntryThreadId,
|
|
14585
14680
|
resolveChannelTarget,
|
|
14586
14681
|
registerAgentSchedule,
|
|
14587
14682
|
recoverPendingEscalations,
|
|
14588
14683
|
main,
|
|
14589
|
-
|
|
14684
|
+
isHotReloadEnabled,
|
|
14685
|
+
ipcDispatcher,
|
|
14686
|
+
createScheduleReloader
|
|
14590
14687
|
};
|
|
@@ -11232,6 +11232,10 @@ var ReactionsSchema = exports_external.object({
|
|
|
11232
11232
|
per_hour_cap: exports_external.number().int().nonnegative().optional().describe("Max reaction-triggered synthetic turns per chat per rolling hour. " + "Refusals are stderr-logged but not surfaced to the agent. " + "Default 10. Set to 0 to disable triggering via the cap path."),
|
|
11233
11233
|
group_admin_only: exports_external.boolean().optional().describe("In groups/supergroups (negative chat_id), only trigger a synthetic " + "turn when the reacter is a chat admin (creator or administrator). " + "Failing the lookup is treated as non-admin (fail-closed). " + "DMs are never affected by this flag — the reacter IS the user. " + "Default true.")
|
|
11234
11234
|
}).optional();
|
|
11235
|
+
var ReactionDispatchSchema = exports_external.object({
|
|
11236
|
+
enabled: exports_external.boolean().optional().describe("Master switch for the reaction-dispatch path. Default false — " + "with no reaction_dispatch block, reactions are persisted (and may " + "feed the `reactions` feedback path) but are NEVER dispatched as " + "event-driven inbound turns."),
|
|
11237
|
+
emojis: exports_external.array(exports_external.string()).optional().describe('Emoji allowlist that triggers a `<channel event="reaction">` ' + "inbound turn when reacted to any message. Default [] (nothing " + "fires). Cascade mode: REPLACE (not union) — a layer's list " + "replaces lower layers entirely so an operator can narrow per-agent.")
|
|
11238
|
+
}).optional();
|
|
11235
11239
|
var ReleaseBlock = exports_external.object({
|
|
11236
11240
|
channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
|
|
11237
11241
|
pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
|
|
@@ -11267,6 +11271,7 @@ var profileFields = {
|
|
|
11267
11271
|
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
11268
11272
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
|
|
11269
11273
|
reactions: ReactionsSchema,
|
|
11274
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
11270
11275
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
|
|
11271
11276
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
11272
11277
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Omit to use Claude's default (acceptEdits for switchroom agents). " + "Warning: bypassPermissions and dontAsk skip all safety checks — use only in trusted sandboxes."),
|
|
@@ -11336,6 +11341,7 @@ var AgentSchema = exports_external.object({
|
|
|
11336
11341
|
schedule: exports_external.array(ScheduleEntrySchema).default([]),
|
|
11337
11342
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
|
|
11338
11343
|
reactions: ReactionsSchema,
|
|
11344
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
11339
11345
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
|
|
11340
11346
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
11341
11347
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Per-agent override wins over defaults.permission_mode. " + "Warning: bypassPermissions and dontAsk skip all safety checks — use only in trusted sandboxes."),
|
|
@@ -12001,6 +12007,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
12001
12007
|
}
|
|
12002
12008
|
merged.reactions = combined;
|
|
12003
12009
|
}
|
|
12010
|
+
const dReactionDispatch = defaults.reaction_dispatch;
|
|
12011
|
+
const mReactionDispatch = merged.reaction_dispatch;
|
|
12012
|
+
if (dReactionDispatch || mReactionDispatch) {
|
|
12013
|
+
const base = dReactionDispatch ?? {};
|
|
12014
|
+
const override = mReactionDispatch ?? {};
|
|
12015
|
+
const combined = { ...base };
|
|
12016
|
+
for (const [k, v] of Object.entries(override)) {
|
|
12017
|
+
if (v !== undefined)
|
|
12018
|
+
combined[k] = v;
|
|
12019
|
+
}
|
|
12020
|
+
merged.reaction_dispatch = combined;
|
|
12021
|
+
}
|
|
12004
12022
|
if (defaults.resources || merged.resources) {
|
|
12005
12023
|
const d = defaults.resources ?? {};
|
|
12006
12024
|
const a = merged.resources ?? {};
|
|
@@ -11980,6 +11980,10 @@ var ReactionsSchema = exports_external.object({
|
|
|
11980
11980
|
per_hour_cap: exports_external.number().int().nonnegative().optional().describe("Max reaction-triggered synthetic turns per chat per rolling hour. " + "Refusals are stderr-logged but not surfaced to the agent. " + "Default 10. Set to 0 to disable triggering via the cap path."),
|
|
11981
11981
|
group_admin_only: exports_external.boolean().optional().describe("In groups/supergroups (negative chat_id), only trigger a synthetic " + "turn when the reacter is a chat admin (creator or administrator). " + "Failing the lookup is treated as non-admin (fail-closed). " + "DMs are never affected by this flag \u2014 the reacter IS the user. " + "Default true.")
|
|
11982
11982
|
}).optional();
|
|
11983
|
+
var ReactionDispatchSchema = exports_external.object({
|
|
11984
|
+
enabled: exports_external.boolean().optional().describe("Master switch for the reaction-dispatch path. Default false \u2014 " + "with no reaction_dispatch block, reactions are persisted (and may " + "feed the `reactions` feedback path) but are NEVER dispatched as " + "event-driven inbound turns."),
|
|
11985
|
+
emojis: exports_external.array(exports_external.string()).optional().describe('Emoji allowlist that triggers a `<channel event="reaction">` ' + "inbound turn when reacted to any message. Default [] (nothing " + "fires). Cascade mode: REPLACE (not union) \u2014 a layer's list " + "replaces lower layers entirely so an operator can narrow per-agent.")
|
|
11986
|
+
}).optional();
|
|
11983
11987
|
var ReleaseBlock = exports_external.object({
|
|
11984
11988
|
channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
|
|
11985
11989
|
pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
|
|
@@ -12015,6 +12019,7 @@ var profileFields = {
|
|
|
12015
12019
|
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
12016
12020
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker \u2014 independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 \u2014 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
|
|
12017
12021
|
reactions: ReactionsSchema,
|
|
12022
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
12018
12023
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
|
|
12019
12024
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
12020
12025
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Omit to use Claude's default (acceptEdits for switchroom agents). " + "Warning: bypassPermissions and dontAsk skip all safety checks \u2014 use only in trusted sandboxes."),
|
|
@@ -12084,6 +12089,7 @@ var AgentSchema = exports_external.object({
|
|
|
12084
12089
|
schedule: exports_external.array(ScheduleEntrySchema).default([]),
|
|
12085
12090
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
|
|
12086
12091
|
reactions: ReactionsSchema,
|
|
12092
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
12087
12093
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
|
|
12088
12094
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
12089
12095
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Per-agent override wins over defaults.permission_mode. " + "Warning: bypassPermissions and dontAsk skip all safety checks \u2014 use only in trusted sandboxes."),
|
|
@@ -12751,6 +12757,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
12751
12757
|
}
|
|
12752
12758
|
merged.reactions = combined;
|
|
12753
12759
|
}
|
|
12760
|
+
const dReactionDispatch = defaults.reaction_dispatch;
|
|
12761
|
+
const mReactionDispatch = merged.reaction_dispatch;
|
|
12762
|
+
if (dReactionDispatch || mReactionDispatch) {
|
|
12763
|
+
const base = dReactionDispatch ?? {};
|
|
12764
|
+
const override = mReactionDispatch ?? {};
|
|
12765
|
+
const combined = { ...base };
|
|
12766
|
+
for (const [k, v] of Object.entries(override)) {
|
|
12767
|
+
if (v !== undefined)
|
|
12768
|
+
combined[k] = v;
|
|
12769
|
+
}
|
|
12770
|
+
merged.reaction_dispatch = combined;
|
|
12771
|
+
}
|
|
12754
12772
|
if (defaults.resources || merged.resources) {
|
|
12755
12773
|
const d = defaults.resources ?? {};
|
|
12756
12774
|
const a = merged.resources ?? {};
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -13520,7 +13520,7 @@ var init_zod = __esm(() => {
|
|
|
13520
13520
|
});
|
|
13521
13521
|
|
|
13522
13522
|
// src/config/schema.ts
|
|
13523
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
13523
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
13524
13524
|
var init_schema = __esm(() => {
|
|
13525
13525
|
init_zod();
|
|
13526
13526
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -13796,6 +13796,10 @@ var init_schema = __esm(() => {
|
|
|
13796
13796
|
per_hour_cap: exports_external.number().int().nonnegative().optional().describe("Max reaction-triggered synthetic turns per chat per rolling hour. " + "Refusals are stderr-logged but not surfaced to the agent. " + "Default 10. Set to 0 to disable triggering via the cap path."),
|
|
13797
13797
|
group_admin_only: exports_external.boolean().optional().describe("In groups/supergroups (negative chat_id), only trigger a synthetic " + "turn when the reacter is a chat admin (creator or administrator). " + "Failing the lookup is treated as non-admin (fail-closed). " + "DMs are never affected by this flag \u2014 the reacter IS the user. " + "Default true.")
|
|
13798
13798
|
}).optional();
|
|
13799
|
+
ReactionDispatchSchema = exports_external.object({
|
|
13800
|
+
enabled: exports_external.boolean().optional().describe("Master switch for the reaction-dispatch path. Default false \u2014 " + "with no reaction_dispatch block, reactions are persisted (and may " + "feed the `reactions` feedback path) but are NEVER dispatched as " + "event-driven inbound turns."),
|
|
13801
|
+
emojis: exports_external.array(exports_external.string()).optional().describe('Emoji allowlist that triggers a `<channel event="reaction">` ' + "inbound turn when reacted to any message. Default [] (nothing " + "fires). Cascade mode: REPLACE (not union) \u2014 a layer's list " + "replaces lower layers entirely so an operator can narrow per-agent.")
|
|
13802
|
+
}).optional();
|
|
13799
13803
|
ReleaseBlock = exports_external.object({
|
|
13800
13804
|
channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
|
|
13801
13805
|
pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
|
|
@@ -13831,6 +13835,7 @@ var init_schema = __esm(() => {
|
|
|
13831
13835
|
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
13832
13836
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker \u2014 independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 \u2014 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
|
|
13833
13837
|
reactions: ReactionsSchema,
|
|
13838
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
13834
13839
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
|
|
13835
13840
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
13836
13841
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Omit to use Claude's default (acceptEdits for switchroom agents). " + "Warning: bypassPermissions and dontAsk skip all safety checks \u2014 use only in trusted sandboxes."),
|
|
@@ -13900,6 +13905,7 @@ var init_schema = __esm(() => {
|
|
|
13900
13905
|
schedule: exports_external.array(ScheduleEntrySchema).default([]),
|
|
13901
13906
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
|
|
13902
13907
|
reactions: ReactionsSchema,
|
|
13908
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
13903
13909
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
|
|
13904
13910
|
thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
|
|
13905
13911
|
permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Per-agent override wins over defaults.permission_mode. " + "Warning: bypassPermissions and dontAsk skip all safety checks \u2014 use only in trusted sandboxes."),
|
|
@@ -14617,6 +14623,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
14617
14623
|
}
|
|
14618
14624
|
merged.reactions = combined;
|
|
14619
14625
|
}
|
|
14626
|
+
const dReactionDispatch = defaults.reaction_dispatch;
|
|
14627
|
+
const mReactionDispatch = merged.reaction_dispatch;
|
|
14628
|
+
if (dReactionDispatch || mReactionDispatch) {
|
|
14629
|
+
const base = dReactionDispatch ?? {};
|
|
14630
|
+
const override = mReactionDispatch ?? {};
|
|
14631
|
+
const combined = { ...base };
|
|
14632
|
+
for (const [k, v] of Object.entries(override)) {
|
|
14633
|
+
if (v !== undefined)
|
|
14634
|
+
combined[k] = v;
|
|
14635
|
+
}
|
|
14636
|
+
merged.reaction_dispatch = combined;
|
|
14637
|
+
}
|
|
14620
14638
|
if (defaults.resources || merged.resources) {
|
|
14621
14639
|
const d = defaults.resources ?? {};
|
|
14622
14640
|
const a = merged.resources ?? {};
|
|
@@ -49409,6 +49427,10 @@ function dispatchTool(name, args) {
|
|
|
49409
49427
|
base.push("--name", a.name);
|
|
49410
49428
|
if (a.secrets && a.secrets.length > 0)
|
|
49411
49429
|
base.push("--secrets", a.secrets.join(","));
|
|
49430
|
+
if (a.model)
|
|
49431
|
+
base.push("--model", a.model);
|
|
49432
|
+
if (a.context)
|
|
49433
|
+
base.push("--context", a.context);
|
|
49412
49434
|
cliArgs = base;
|
|
49413
49435
|
parseMode = "json";
|
|
49414
49436
|
break;
|
|
@@ -49593,7 +49615,7 @@ var init_server3 = __esm(() => {
|
|
|
49593
49615
|
},
|
|
49594
49616
|
{
|
|
49595
49617
|
name: "schedule_add",
|
|
49596
|
-
description: "Append a cron schedule entry to
|
|
49618
|
+
description: "Append a cron schedule entry to your overlay. Takes effect within ~30s \u2014 " + "the scheduler hot-reloads, no restart needed. Overlay entries with " + "non-empty `secrets:` are REJECTED (E_OVERLAY_SECRETS_REQUIRES_APPROVAL). " + "COST: by default each fire runs as a full turn in your live session " + "(your model, your whole context) \u2014 fine for work that needs your memory/" + "persona, but costly for routine checks. For a lighter recurring task, set " + '`model: "sonnet"` to run that fire in a cheap, minimal-context cron ' + "session instead (saves tokens; needs cheap-cron enabled by the operator). " + "For 'only do something when X changes' (e.g. a webpage, or a reaction), " + "ask the operator to set up a poll or reaction-dispatch instead of a " + "frequent prompt cron \u2014 far cheaper than polling with a full turn.",
|
|
49597
49619
|
inputSchema: {
|
|
49598
49620
|
type: "object",
|
|
49599
49621
|
required: ["cron_expr", "prompt"],
|
|
@@ -49601,7 +49623,16 @@ var init_server3 = __esm(() => {
|
|
|
49601
49623
|
cron_expr: { type: "string" },
|
|
49602
49624
|
prompt: { type: "string", minLength: 1, maxLength: 4000 },
|
|
49603
49625
|
secrets: { type: "array", items: { type: "string" } },
|
|
49604
|
-
name: { type: "string", pattern: "^[a-z0-9-]{1,40}$" }
|
|
49626
|
+
name: { type: "string", pattern: "^[a-z0-9-]{1,40}$" },
|
|
49627
|
+
model: {
|
|
49628
|
+
type: "string",
|
|
49629
|
+
description: "Optional cheap-cron tier hint. A known-cheap model ('sonnet'/'haiku') " + "routes this fire to a fresh, minimal-context cron session (Tier 1) " + "instead of your full live session (Tier 2) \u2014 cheaper per fire. Omit " + "for context-heavy work that needs your memory/persona. Inert unless " + "the operator has enabled cheap-cron."
|
|
49630
|
+
},
|
|
49631
|
+
context: {
|
|
49632
|
+
type: "string",
|
|
49633
|
+
enum: ["fresh", "agent"],
|
|
49634
|
+
description: "Tier hint: 'fresh' = minimal-context cheap session (Tier 1); 'agent' " + "= your full live session (Tier 2). Usually inferred from `model`."
|
|
49635
|
+
}
|
|
49605
49636
|
}
|
|
49606
49637
|
}
|
|
49607
49638
|
},
|
|
@@ -50204,8 +50235,8 @@ var {
|
|
|
50204
50235
|
} = import__.default;
|
|
50205
50236
|
|
|
50206
50237
|
// src/build-info.ts
|
|
50207
|
-
var VERSION = "0.15.
|
|
50208
|
-
var COMMIT_SHA = "
|
|
50238
|
+
var VERSION = "0.15.8";
|
|
50239
|
+
var COMMIT_SHA = "318cb85f";
|
|
50209
50240
|
|
|
50210
50241
|
// src/cli/agent.ts
|
|
50211
50242
|
init_source();
|
|
@@ -50409,6 +50440,13 @@ function stripSecretValues(value) {
|
|
|
50409
50440
|
function restartRequiredNote(agent) {
|
|
50410
50441
|
return `Not live yet \u2014 claude loads skills, MCP servers and scheduled ` + `tasks at process start. Run \`switchroom agent restart ${agent}\` ` + `for this to take effect.`;
|
|
50411
50442
|
}
|
|
50443
|
+
function scheduleRestartRequired() {
|
|
50444
|
+
return (process.env.SWITCHROOM_SCHEDULER_HOT_RELOAD ?? "") === "0";
|
|
50445
|
+
}
|
|
50446
|
+
function scheduleLiveNote(agent) {
|
|
50447
|
+
const hotReload = (process.env.SWITCHROOM_SCHEDULER_HOT_RELOAD ?? "") !== "0";
|
|
50448
|
+
return hotReload ? `Live within ~30s \u2014 the in-agent scheduler hot-reloads the overlay; no restart needed.` : `Not live yet \u2014 hot-reload is disabled (SWITCHROOM_SCHEDULER_HOT_RELOAD=0). ` + `Run \`switchroom agent restart ${agent}\` for this to take effect.`;
|
|
50449
|
+
}
|
|
50412
50450
|
function getAgentSlice(config, agent) {
|
|
50413
50451
|
const slice = config.agents?.[agent];
|
|
50414
50452
|
if (!slice) {
|
|
@@ -68118,6 +68156,7 @@ init_source();
|
|
|
68118
68156
|
// src/web/server.ts
|
|
68119
68157
|
init_merge();
|
|
68120
68158
|
init_loader();
|
|
68159
|
+
init_client();
|
|
68121
68160
|
init_lifecycle();
|
|
68122
68161
|
import {
|
|
68123
68162
|
readFileSync as readFileSync45,
|
|
@@ -74490,12 +74529,37 @@ function loadWebhookSecrets() {
|
|
|
74490
74529
|
return {};
|
|
74491
74530
|
}
|
|
74492
74531
|
}
|
|
74532
|
+
async function resolveWebhookSecretFromVault(agent, source, config) {
|
|
74533
|
+
const key = `webhook/${agent}/${source}`;
|
|
74534
|
+
try {
|
|
74535
|
+
const socket = resolveBrokerSocketPath({
|
|
74536
|
+
vaultBrokerSocket: config.vault?.broker?.socket ? resolvePath(config.vault.broker.socket) : undefined
|
|
74537
|
+
});
|
|
74538
|
+
const result = await getViaBrokerStructured(key, { socket });
|
|
74539
|
+
if (result.kind === "ok" && result.entry.kind === "string") {
|
|
74540
|
+
return result.entry.value;
|
|
74541
|
+
}
|
|
74542
|
+
if (result.kind === "denied" || result.kind === "unreachable") {
|
|
74543
|
+
process.stderr.write(`webhook-ingest: vault resolve for ${key} \u2192 ${result.kind}` + `${"code" in result && result.code ? ` (${result.code})` : ""}` + `; webhook will 401 until the secret is operator-readable
|
|
74544
|
+
`);
|
|
74545
|
+
}
|
|
74546
|
+
} catch (err) {
|
|
74547
|
+
process.stderr.write(`webhook-ingest: vault resolve for ${key} threw: ${err.message}
|
|
74548
|
+
`);
|
|
74549
|
+
}
|
|
74550
|
+
return null;
|
|
74551
|
+
}
|
|
74493
74552
|
async function handleWebhookRoute(req, agent, source, config) {
|
|
74494
74553
|
const agentConfigRaw = config.agents[agent];
|
|
74495
74554
|
const agentConfig = agentConfigRaw ? resolveAgentConfig(config.defaults, config.profiles, agentConfigRaw) : undefined;
|
|
74496
74555
|
const allowedSources = agentConfig?.channels?.telegram?.webhook_sources ?? [];
|
|
74497
74556
|
const allSecrets = loadWebhookSecrets();
|
|
74498
|
-
const agentSecrets = allSecrets[agent] ?? {};
|
|
74557
|
+
const agentSecrets = { ...allSecrets[agent] ?? {} };
|
|
74558
|
+
if (!agentSecrets[source]) {
|
|
74559
|
+
const fromVault = await resolveWebhookSecretFromVault(agent, source, config);
|
|
74560
|
+
if (fromVault)
|
|
74561
|
+
agentSecrets[source] = fromVault;
|
|
74562
|
+
}
|
|
74499
74563
|
const requireEdge = agentConfig?.channels?.telegram?.webhook_require_edge === true;
|
|
74500
74564
|
const edgeSecret = requireEdge ? loadEdgeSecret() : null;
|
|
74501
74565
|
let bodyBuf;
|
|
@@ -82933,6 +82997,10 @@ function scheduleAdd(opts) {
|
|
|
82933
82997
|
entry.secrets = opts.secrets;
|
|
82934
82998
|
if (opts.name)
|
|
82935
82999
|
entry.name = opts.name;
|
|
83000
|
+
if (opts.model)
|
|
83001
|
+
entry.model = opts.model;
|
|
83002
|
+
if (opts.context)
|
|
83003
|
+
entry.context = opts.context;
|
|
82936
83004
|
const doc = {
|
|
82937
83005
|
schedule: [
|
|
82938
83006
|
Object.fromEntries(Object.entries(entry).filter(([k]) => k !== "name"))
|
|
@@ -83017,8 +83085,8 @@ function scheduleAdd(opts) {
|
|
|
83017
83085
|
path: path8,
|
|
83018
83086
|
cron_hash: hash2,
|
|
83019
83087
|
would_recreate: false,
|
|
83020
|
-
restart_required:
|
|
83021
|
-
restart_hint:
|
|
83088
|
+
restart_required: scheduleRestartRequired(),
|
|
83089
|
+
restart_hint: scheduleLiveNote(agent)
|
|
83022
83090
|
};
|
|
83023
83091
|
}
|
|
83024
83092
|
function scheduleAddOrStage(opts) {
|
|
@@ -83043,6 +83111,10 @@ function scheduleAddOrStage(opts) {
|
|
|
83043
83111
|
entry.secrets = opts.secrets;
|
|
83044
83112
|
if (opts.name)
|
|
83045
83113
|
entry.name = opts.name;
|
|
83114
|
+
if (opts.model)
|
|
83115
|
+
entry.model = opts.model;
|
|
83116
|
+
if (opts.context)
|
|
83117
|
+
entry.context = opts.context;
|
|
83046
83118
|
const doc = {
|
|
83047
83119
|
schedule: [
|
|
83048
83120
|
Object.fromEntries(Object.entries(entry).filter(([k]) => k !== "name"))
|
|
@@ -83142,13 +83214,17 @@ function scheduleRemove(opts) {
|
|
|
83142
83214
|
ok: true,
|
|
83143
83215
|
slug: match.slug,
|
|
83144
83216
|
path: match.path,
|
|
83145
|
-
restart_required:
|
|
83146
|
-
restart_hint:
|
|
83217
|
+
restart_required: scheduleRestartRequired(),
|
|
83218
|
+
restart_hint: scheduleLiveNote(agent)
|
|
83147
83219
|
};
|
|
83148
83220
|
}
|
|
83149
83221
|
function registerAgentConfigWriteCommands(program3) {
|
|
83150
83222
|
const schedule = program3.command("schedule").description("Add / remove an agent's scheduled cron entries (overlay-backed)");
|
|
83151
|
-
schedule.command("add").description("Append a schedule entry to the agent's overlay dir").requiredOption("--cron <expr>", "Cron expression").requiredOption("--prompt <text>", "Prompt to fire at the scheduled time").option("--agent <name>", "Target agent (defaults to $SWITCHROOM_AGENT_NAME)").option("--secrets <list>", "Comma-separated vault keys (REJECTED for agent-authored overlays)").option("--name <slug>", "Optional human-readable name (a-z 0-9 -)").option("--stage-on-reject", "When a security gate trips (secrets/quota/min-interval), stage the entry under .pending/ for operator approval instead of rejecting with exit 9. Used by the MCP path; operator CLI defaults to off.").action(async (opts) => {
|
|
83223
|
+
schedule.command("add").description("Append a schedule entry to the agent's overlay dir").requiredOption("--cron <expr>", "Cron expression").requiredOption("--prompt <text>", "Prompt to fire at the scheduled time").option("--agent <name>", "Target agent (defaults to $SWITCHROOM_AGENT_NAME)").option("--secrets <list>", "Comma-separated vault keys (REJECTED for agent-authored overlays)").option("--name <slug>", "Optional human-readable name (a-z 0-9 -)").option("--model <id>", "Cheap-cron tier hint: a known-cheap model (sonnet/haiku) routes this fire to a fresh, minimal-context cron session (Tier 1) instead of the agent's full live session (Tier 2), cutting token cost. Inert unless SWITCHROOM_CHEAP_CRON is on.").option("--context <mode>", "Tier hint: 'fresh' (minimal-context cheap session) or 'agent' (full live session). Unset \u2192 inferred from --model.").option("--stage-on-reject", "When a security gate trips (secrets/quota/min-interval), stage the entry under .pending/ for operator approval instead of rejecting with exit 9. Used by the MCP path; operator CLI defaults to off.").action(async (opts) => {
|
|
83224
|
+
if (opts.context && opts.context !== "fresh" && opts.context !== "agent") {
|
|
83225
|
+
emitError("E_INVALID_PROMPT", "--context must be 'fresh' or 'agent'");
|
|
83226
|
+
process.exit(1);
|
|
83227
|
+
}
|
|
83152
83228
|
const secrets = opts.secrets ? opts.secrets.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
83153
83229
|
if (opts.name && !/^[a-z0-9-]{1,40}$/.test(opts.name)) {
|
|
83154
83230
|
emitError("E_INVALID_PROMPT", "name must match [a-z0-9-]{1,40}");
|
|
@@ -83166,7 +83242,9 @@ function registerAgentConfigWriteCommands(program3) {
|
|
|
83166
83242
|
cronExpr: opts.cron,
|
|
83167
83243
|
prompt: opts.prompt,
|
|
83168
83244
|
secrets,
|
|
83169
|
-
name: opts.name
|
|
83245
|
+
name: opts.name,
|
|
83246
|
+
model: opts.model,
|
|
83247
|
+
context: opts.context
|
|
83170
83248
|
});
|
|
83171
83249
|
} catch (err) {
|
|
83172
83250
|
process.stderr.write(`${err.message}
|
|
@@ -85050,7 +85128,11 @@ import { homedir as homedir48 } from "node:os";
|
|
|
85050
85128
|
import { join as join82 } from "node:path";
|
|
85051
85129
|
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
85052
85130
|
init_audit_reader();
|
|
85053
|
-
|
|
85131
|
+
function resolveHostdImageTag(explicitTag, release) {
|
|
85132
|
+
if (explicitTag)
|
|
85133
|
+
return explicitTag;
|
|
85134
|
+
return resolveImageTag(resolveRelease({ root: release }));
|
|
85135
|
+
}
|
|
85054
85136
|
var HOSTD_COMPOSE_PROJECT = "switchroom-hostd";
|
|
85055
85137
|
function renderHostdComposeFile(opts) {
|
|
85056
85138
|
const { hostHome, imageTag, operatorUid } = opts;
|
|
@@ -85202,9 +85284,10 @@ async function doInstall(opts, program3) {
|
|
|
85202
85284
|
const dir = hostdDir();
|
|
85203
85285
|
const composePath = hostdComposePath();
|
|
85204
85286
|
mkdirSync47(dir, { recursive: true });
|
|
85287
|
+
const imageTag = resolveHostdImageTag(opts.tag, cfg.release);
|
|
85205
85288
|
const yaml = renderHostdComposeFile({
|
|
85206
85289
|
hostHome: resolveHostdHostHome(),
|
|
85207
|
-
imageTag
|
|
85290
|
+
imageTag,
|
|
85208
85291
|
operatorUid: resolveOperatorUid()
|
|
85209
85292
|
});
|
|
85210
85293
|
if (opts.dryRun) {
|
|
@@ -85221,12 +85304,12 @@ async function doInstall(opts, program3) {
|
|
|
85221
85304
|
const adminAgents = Object.entries(cfg.agents ?? {}).filter(([, a]) => a?.admin === true).map(([name]) => name);
|
|
85222
85305
|
console.log(source_default.dim(` agents served (one socket each): ${allAgents.length === 0 ? "(none)" : allAgents.join(", ")}`));
|
|
85223
85306
|
console.log(source_default.dim(` admin agents (full config-edit verbs): ${adminAgents.length === 0 ? "(none)" : adminAgents.join(", ")}`));
|
|
85224
|
-
console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-hostd:${
|
|
85307
|
+
console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-hostd:${imageTag}\u2026`));
|
|
85225
85308
|
const pull = runDocker(["compose", "-p", HOSTD_COMPOSE_PROJECT, "-f", composePath, "pull"]);
|
|
85226
85309
|
if (!pull.ok) {
|
|
85227
85310
|
console.error(source_default.red(` pull failed:
|
|
85228
85311
|
${pull.stderr}`));
|
|
85229
|
-
console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-hostd:${
|
|
85312
|
+
console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-hostd:${imageTag}\` may not be published yet.
|
|
85230
85313
|
` + ` Check the docker-images workflow run and verify the tag at:
|
|
85231
85314
|
` + ` https://github.com/switchroom/switchroom/pkgs/container/switchroom-hostd`));
|
|
85232
85315
|
process.exit(1);
|
|
@@ -85311,7 +85394,7 @@ ${down.stderr}`));
|
|
|
85311
85394
|
}
|
|
85312
85395
|
function registerHostdCommand(program3) {
|
|
85313
85396
|
const hostd = program3.command("hostd").description("Manage switchroom-hostd, the host-control daemon for admin agents (RFC C)");
|
|
85314
|
-
hostd.command("install").description("Install or refresh the hostd container (writes ~/.switchroom/hostd/docker-compose.yml + docker compose up -d)").option("--tag <tag>", "Image tag (default: latest)"
|
|
85397
|
+
hostd.command("install").description("Install or refresh the hostd container (writes ~/.switchroom/hostd/docker-compose.yml + docker compose up -d)").option("--tag <tag>", "Image tag override (default: resolved from release.pin in switchroom.yaml, else latest)").option("--dry-run", "Print the compose file and the docker commands without writing or running anything").action(withConfigError(async (opts) => {
|
|
85315
85398
|
await doInstall(opts, program3);
|
|
85316
85399
|
}));
|
|
85317
85400
|
hostd.command("status").description("Show daemon state and bound sockets").action(() => doStatus());
|
|
@@ -85369,7 +85452,11 @@ import { existsSync as existsSync85, mkdirSync as mkdirSync48, writeFileSync as
|
|
|
85369
85452
|
import { homedir as homedir49 } from "node:os";
|
|
85370
85453
|
import { join as join83 } from "node:path";
|
|
85371
85454
|
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
85372
|
-
|
|
85455
|
+
function resolveWebImageTag(explicitTag, release) {
|
|
85456
|
+
if (explicitTag)
|
|
85457
|
+
return explicitTag;
|
|
85458
|
+
return resolveImageTag(resolveRelease({ root: release }));
|
|
85459
|
+
}
|
|
85373
85460
|
var WEB_COMPOSE_PROJECT = "switchroom-web";
|
|
85374
85461
|
function renderWebComposeFile(opts) {
|
|
85375
85462
|
const { hostHome, imageTag, operatorUid } = opts;
|
|
@@ -85470,7 +85557,7 @@ function runDocker2(args) {
|
|
|
85470
85557
|
stderr: r.stderr ?? ""
|
|
85471
85558
|
};
|
|
85472
85559
|
}
|
|
85473
|
-
async function doInstall2(opts) {
|
|
85560
|
+
async function doInstall2(opts, program3) {
|
|
85474
85561
|
const operatorUid = resolveOperatorUid();
|
|
85475
85562
|
if (operatorUid === undefined) {
|
|
85476
85563
|
console.error(source_default.red(`Could not resolve the operator uid (no SUDO_UID and getuid() is 0 or unavailable).
|
|
@@ -85481,9 +85568,11 @@ async function doInstall2(opts) {
|
|
|
85481
85568
|
const dir = webdDir();
|
|
85482
85569
|
const composePath = webdComposePath();
|
|
85483
85570
|
mkdirSync48(dir, { recursive: true });
|
|
85571
|
+
const cfg = getConfig(program3);
|
|
85572
|
+
const imageTag = resolveWebImageTag(opts.tag, cfg.release);
|
|
85484
85573
|
const yaml = renderWebComposeFile({
|
|
85485
85574
|
hostHome: homedir49(),
|
|
85486
|
-
imageTag
|
|
85575
|
+
imageTag,
|
|
85487
85576
|
operatorUid
|
|
85488
85577
|
});
|
|
85489
85578
|
if (opts.dryRun) {
|
|
@@ -85498,12 +85587,12 @@ async function doInstall2(opts) {
|
|
|
85498
85587
|
writeFileSync41(composePath, yaml, "utf8");
|
|
85499
85588
|
console.log(source_default.green(` \u2713 Wrote ${composePath}`));
|
|
85500
85589
|
console.log(source_default.dim(` running as uid ${operatorUid} (operator), network_mode: host`));
|
|
85501
|
-
console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-web:${
|
|
85590
|
+
console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-web:${imageTag}\u2026`));
|
|
85502
85591
|
const pull = runDocker2(["compose", "-p", WEB_COMPOSE_PROJECT, "-f", composePath, "pull"]);
|
|
85503
85592
|
if (!pull.ok) {
|
|
85504
85593
|
console.error(source_default.red(` pull failed:
|
|
85505
85594
|
${pull.stderr}`));
|
|
85506
|
-
console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-web:${
|
|
85595
|
+
console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-web:${imageTag}\` may not be published yet.
|
|
85507
85596
|
` + ` Check the docker-images workflow run and verify the tag at:
|
|
85508
85597
|
` + ` https://github.com/switchroom/switchroom/pkgs/container/switchroom-web`));
|
|
85509
85598
|
process.exit(1);
|
|
@@ -85571,8 +85660,8 @@ ${down.stderr}`));
|
|
|
85571
85660
|
}
|
|
85572
85661
|
function registerWebdCommand(program3) {
|
|
85573
85662
|
const webd = program3.command("webd").description("Manage switchroom-web, the dashboard + GitHub-webhook receiver container");
|
|
85574
|
-
webd.command("install").description("Install or refresh the web container (writes ~/.switchroom/web/docker-compose.yml + docker compose up -d)").option("--tag <tag>", "Image tag (default: latest)"
|
|
85575
|
-
await doInstall2(opts);
|
|
85663
|
+
webd.command("install").description("Install or refresh the web container (writes ~/.switchroom/web/docker-compose.yml + docker compose up -d)").option("--tag <tag>", "Image tag override (default: resolved from release.pin in switchroom.yaml, else latest)").option("--dry-run", "Print the compose file and the docker commands without writing or running anything").action(withConfigError(async (opts) => {
|
|
85664
|
+
await doInstall2(opts, program3);
|
|
85576
85665
|
}));
|
|
85577
85666
|
webd.command("status").description("Show web-service container state").action(() => doStatus2());
|
|
85578
85667
|
webd.command("uninstall").description("Stop the web container. Leaves the compose file in place for re-install.").action(() => doUninstall2());
|