switchroom 0.15.7 → 0.15.9
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 +189 -7
- package/dist/auth-broker/index.js +18 -0
- package/dist/cli/notion-write-pretool.mjs +18 -0
- package/dist/cli/switchroom.js +204 -30
- 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 +170 -8
- 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
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 ?? {};
|
|
@@ -15417,6 +15435,93 @@ var init_cron_routing = __esm(() => {
|
|
|
15417
15435
|
OPUS_MODEL_RE = /opus/i;
|
|
15418
15436
|
});
|
|
15419
15437
|
|
|
15438
|
+
// src/scheduler/cron-cadence.ts
|
|
15439
|
+
function csvSmallestGap(field) {
|
|
15440
|
+
if (!field.includes(","))
|
|
15441
|
+
return null;
|
|
15442
|
+
const parts = field.split(",").map((s) => Number(s)).filter((n) => Number.isInteger(n) && n >= 0);
|
|
15443
|
+
if (parts.length < 2)
|
|
15444
|
+
return null;
|
|
15445
|
+
const sorted = [...parts].sort((a, b) => a - b);
|
|
15446
|
+
let smallest = Infinity;
|
|
15447
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
15448
|
+
const gap = sorted[i] - sorted[i - 1];
|
|
15449
|
+
if (gap > 0 && gap < smallest)
|
|
15450
|
+
smallest = gap;
|
|
15451
|
+
}
|
|
15452
|
+
return Number.isFinite(smallest) ? smallest : null;
|
|
15453
|
+
}
|
|
15454
|
+
function estimateCronGapMin(expr) {
|
|
15455
|
+
const fields = expr.trim().split(/\s+/);
|
|
15456
|
+
if (fields.length < 5)
|
|
15457
|
+
return Infinity;
|
|
15458
|
+
const [min, hour] = fields;
|
|
15459
|
+
if (min === "*")
|
|
15460
|
+
return 1;
|
|
15461
|
+
const minStep = min.match(/^\*\/(\d+)$/);
|
|
15462
|
+
if (minStep) {
|
|
15463
|
+
const n = Number(minStep[1]);
|
|
15464
|
+
return n > 0 ? n : Infinity;
|
|
15465
|
+
}
|
|
15466
|
+
const minCsv = csvSmallestGap(min);
|
|
15467
|
+
if (minCsv !== null)
|
|
15468
|
+
return minCsv;
|
|
15469
|
+
if (!/^\d+$/.test(min))
|
|
15470
|
+
return Infinity;
|
|
15471
|
+
if (hour === "*")
|
|
15472
|
+
return 60;
|
|
15473
|
+
const hourStep = hour.match(/^\*\/(\d+)$/);
|
|
15474
|
+
if (hourStep) {
|
|
15475
|
+
const n = Number(hourStep[1]);
|
|
15476
|
+
return n > 0 ? n * 60 : Infinity;
|
|
15477
|
+
}
|
|
15478
|
+
const hourCsv = csvSmallestGap(hour);
|
|
15479
|
+
if (hourCsv !== null)
|
|
15480
|
+
return hourCsv * 60;
|
|
15481
|
+
if (/^\d+$/.test(hour))
|
|
15482
|
+
return 1440;
|
|
15483
|
+
return Infinity;
|
|
15484
|
+
}
|
|
15485
|
+
|
|
15486
|
+
// src/scheduler/tier-selector.ts
|
|
15487
|
+
function recommendCronTier(input, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
15488
|
+
if (input.kind === "poll") {
|
|
15489
|
+
return { tier: "poll", source: "explicit", reason: "declared kind: poll (model-free check)" };
|
|
15490
|
+
}
|
|
15491
|
+
if (input.context === "fresh") {
|
|
15492
|
+
return { tier: "cheap", source: "explicit", reason: "declared context: fresh (cheap cron session)" };
|
|
15493
|
+
}
|
|
15494
|
+
if (input.context === "agent") {
|
|
15495
|
+
return { tier: "main", source: "explicit", reason: "declared context: agent (full live session)" };
|
|
15496
|
+
}
|
|
15497
|
+
if (input.model !== undefined) {
|
|
15498
|
+
return isKnownCheapModel(input.model) ? { tier: "cheap", source: "explicit", reason: `cheap model '${input.model}' \u2192 cheap cron session` } : { tier: "main", source: "explicit", reason: `model '${input.model}' is not a known-cheap id \u2192 full live session` };
|
|
15499
|
+
}
|
|
15500
|
+
if (input.smallestGapMin <= frequentGapMin) {
|
|
15501
|
+
return {
|
|
15502
|
+
tier: "cheap",
|
|
15503
|
+
source: "cadence-default",
|
|
15504
|
+
reason: `fires every ~${input.smallestGapMin}min (\u2264 ${frequentGapMin}min) \u2014 defaulting to a cheap ` + `session; set context: agent (or an Opus/custom model) if this needs the agent's full context`
|
|
15505
|
+
};
|
|
15506
|
+
}
|
|
15507
|
+
return {
|
|
15508
|
+
tier: "main",
|
|
15509
|
+
source: "cadence-default",
|
|
15510
|
+
reason: `fires every ~${input.smallestGapMin}min (> ${frequentGapMin}min) \u2014 defaulting to the agent's ` + `full session; set model: sonnet (or context: fresh) to run it cheaply`
|
|
15511
|
+
};
|
|
15512
|
+
}
|
|
15513
|
+
function applyDefaultTier(entry, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
15514
|
+
if (entry.kind === "poll" || entry.context !== undefined || entry.model !== undefined) {
|
|
15515
|
+
return entry;
|
|
15516
|
+
}
|
|
15517
|
+
const rec = recommendCronTier({ smallestGapMin: estimateCronGapMin(entry.cron) }, frequentGapMin);
|
|
15518
|
+
return rec.tier === "cheap" ? { ...entry, context: "fresh" } : entry;
|
|
15519
|
+
}
|
|
15520
|
+
var DEFAULT_FREQUENT_GAP_MIN = 60;
|
|
15521
|
+
var init_tier_selector = __esm(() => {
|
|
15522
|
+
init_cron_routing();
|
|
15523
|
+
});
|
|
15524
|
+
|
|
15420
15525
|
// src/config/timezone.ts
|
|
15421
15526
|
import { readFileSync as readFileSync3, readlinkSync } from "node:fs";
|
|
15422
15527
|
function defaultReadEtcTimezone() {
|
|
@@ -23244,7 +23349,7 @@ function describeAgents(config) {
|
|
|
23244
23349
|
const resolved = resolveAgentConfig(config.defaults, config.profiles, agent);
|
|
23245
23350
|
const profile = agent.extends ?? "default";
|
|
23246
23351
|
const uid = allocateAgentUid(name);
|
|
23247
|
-
const cronSession = scheduleNeedsCronSession((resolved.schedule ?? []).map((e) => ({ kind: e.kind, model: e.model, context: e.context })), { cheapCronEnabled: true });
|
|
23352
|
+
const cronSession = scheduleNeedsCronSession((resolved.schedule ?? []).map((e) => applyDefaultTier({ cron: e.cron, kind: e.kind, model: e.model, context: e.context })), { cheapCronEnabled: true });
|
|
23248
23353
|
const resources = resolveResourceDefaults(name, profile, resolved.resources, { cronSession });
|
|
23249
23354
|
const strippedCaps = readStrippedCaps(agent);
|
|
23250
23355
|
out.push({
|
|
@@ -23742,6 +23847,7 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
23742
23847
|
var AGENT_UID_MIN = 10001, AGENT_UID_MAX = 10999, RESOURCE_BY_PROFILE, CRON_SESSION_MEM_BUMP_MIB = 512, CRON_SESSION_PIDS_BUMP = 128, BIND_MOUNT_SOURCE_DENYLIST, BIND_MOUNT_TARGET_DENYLIST, BIND_MOUNT_EXACT_SOURCE_DENY, CONTAINER_CONFIG_PATH = "/state/config/switchroom.yaml";
|
|
23743
23848
|
var init_compose = __esm(() => {
|
|
23744
23849
|
init_cron_routing();
|
|
23850
|
+
init_tier_selector();
|
|
23745
23851
|
init_merge();
|
|
23746
23852
|
init_timezone();
|
|
23747
23853
|
init_peercred();
|
|
@@ -49409,6 +49515,10 @@ function dispatchTool(name, args) {
|
|
|
49409
49515
|
base.push("--name", a.name);
|
|
49410
49516
|
if (a.secrets && a.secrets.length > 0)
|
|
49411
49517
|
base.push("--secrets", a.secrets.join(","));
|
|
49518
|
+
if (a.model)
|
|
49519
|
+
base.push("--model", a.model);
|
|
49520
|
+
if (a.context)
|
|
49521
|
+
base.push("--context", a.context);
|
|
49412
49522
|
cliArgs = base;
|
|
49413
49523
|
parseMode = "json";
|
|
49414
49524
|
break;
|
|
@@ -49593,7 +49703,7 @@ var init_server3 = __esm(() => {
|
|
|
49593
49703
|
},
|
|
49594
49704
|
{
|
|
49595
49705
|
name: "schedule_add",
|
|
49596
|
-
description: "Append a cron schedule entry to
|
|
49706
|
+
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
49707
|
inputSchema: {
|
|
49598
49708
|
type: "object",
|
|
49599
49709
|
required: ["cron_expr", "prompt"],
|
|
@@ -49601,7 +49711,16 @@ var init_server3 = __esm(() => {
|
|
|
49601
49711
|
cron_expr: { type: "string" },
|
|
49602
49712
|
prompt: { type: "string", minLength: 1, maxLength: 4000 },
|
|
49603
49713
|
secrets: { type: "array", items: { type: "string" } },
|
|
49604
|
-
name: { type: "string", pattern: "^[a-z0-9-]{1,40}$" }
|
|
49714
|
+
name: { type: "string", pattern: "^[a-z0-9-]{1,40}$" },
|
|
49715
|
+
model: {
|
|
49716
|
+
type: "string",
|
|
49717
|
+
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."
|
|
49718
|
+
},
|
|
49719
|
+
context: {
|
|
49720
|
+
type: "string",
|
|
49721
|
+
enum: ["fresh", "agent"],
|
|
49722
|
+
description: "Tier hint: 'fresh' = minimal-context cheap session (Tier 1); 'agent' " + "= your full live session (Tier 2). Usually inferred from `model`."
|
|
49723
|
+
}
|
|
49605
49724
|
}
|
|
49606
49725
|
}
|
|
49607
49726
|
},
|
|
@@ -50204,8 +50323,8 @@ var {
|
|
|
50204
50323
|
} = import__.default;
|
|
50205
50324
|
|
|
50206
50325
|
// src/build-info.ts
|
|
50207
|
-
var VERSION = "0.15.
|
|
50208
|
-
var COMMIT_SHA = "
|
|
50326
|
+
var VERSION = "0.15.9";
|
|
50327
|
+
var COMMIT_SHA = "6ed776e2";
|
|
50209
50328
|
|
|
50210
50329
|
// src/cli/agent.ts
|
|
50211
50330
|
init_source();
|
|
@@ -50316,6 +50435,7 @@ var LEGACY_CRON_SCRIPT_BASENAME_RE = /^cron-(\d+)\.sh$/;
|
|
|
50316
50435
|
// src/agents/scaffold.ts
|
|
50317
50436
|
init_schema();
|
|
50318
50437
|
init_cron_routing();
|
|
50438
|
+
init_tier_selector();
|
|
50319
50439
|
init_merge();
|
|
50320
50440
|
init_timezone();
|
|
50321
50441
|
|
|
@@ -50409,6 +50529,13 @@ function stripSecretValues(value) {
|
|
|
50409
50529
|
function restartRequiredNote(agent) {
|
|
50410
50530
|
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
50531
|
}
|
|
50532
|
+
function scheduleRestartRequired() {
|
|
50533
|
+
return (process.env.SWITCHROOM_SCHEDULER_HOT_RELOAD ?? "") === "0";
|
|
50534
|
+
}
|
|
50535
|
+
function scheduleLiveNote(agent) {
|
|
50536
|
+
const hotReload = (process.env.SWITCHROOM_SCHEDULER_HOT_RELOAD ?? "") !== "0";
|
|
50537
|
+
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.`;
|
|
50538
|
+
}
|
|
50412
50539
|
function getAgentSlice(config, agent) {
|
|
50413
50540
|
const slice = config.agents?.[agent];
|
|
50414
50541
|
if (!slice) {
|
|
@@ -51451,11 +51578,7 @@ function renderFleetInvariants() {
|
|
|
51451
51578
|
`);
|
|
51452
51579
|
}
|
|
51453
51580
|
function buildCronSessionContext(agentConfig) {
|
|
51454
|
-
const entries = (agentConfig.schedule ?? []).map((e) => ({
|
|
51455
|
-
kind: e.kind,
|
|
51456
|
-
model: e.model,
|
|
51457
|
-
context: e.context
|
|
51458
|
-
}));
|
|
51581
|
+
const entries = (agentConfig.schedule ?? []).map((e) => applyDefaultTier({ cron: e.cron, kind: e.kind, model: e.model, context: e.context }));
|
|
51459
51582
|
return {
|
|
51460
51583
|
cronSessionEnabled: scheduleNeedsCronSession(entries, { cheapCronEnabled: true }),
|
|
51461
51584
|
cronModelQ: shellSingleQuote(DEFAULT_CRON_MODEL)
|
|
@@ -68118,6 +68241,7 @@ init_source();
|
|
|
68118
68241
|
// src/web/server.ts
|
|
68119
68242
|
init_merge();
|
|
68120
68243
|
init_loader();
|
|
68244
|
+
init_client();
|
|
68121
68245
|
init_lifecycle();
|
|
68122
68246
|
import {
|
|
68123
68247
|
readFileSync as readFileSync45,
|
|
@@ -74490,12 +74614,37 @@ function loadWebhookSecrets() {
|
|
|
74490
74614
|
return {};
|
|
74491
74615
|
}
|
|
74492
74616
|
}
|
|
74617
|
+
async function resolveWebhookSecretFromVault(agent, source, config) {
|
|
74618
|
+
const key = `webhook/${agent}/${source}`;
|
|
74619
|
+
try {
|
|
74620
|
+
const socket = resolveBrokerSocketPath({
|
|
74621
|
+
vaultBrokerSocket: config.vault?.broker?.socket ? resolvePath(config.vault.broker.socket) : undefined
|
|
74622
|
+
});
|
|
74623
|
+
const result = await getViaBrokerStructured(key, { socket });
|
|
74624
|
+
if (result.kind === "ok" && result.entry.kind === "string") {
|
|
74625
|
+
return result.entry.value;
|
|
74626
|
+
}
|
|
74627
|
+
if (result.kind === "denied" || result.kind === "unreachable") {
|
|
74628
|
+
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
|
|
74629
|
+
`);
|
|
74630
|
+
}
|
|
74631
|
+
} catch (err) {
|
|
74632
|
+
process.stderr.write(`webhook-ingest: vault resolve for ${key} threw: ${err.message}
|
|
74633
|
+
`);
|
|
74634
|
+
}
|
|
74635
|
+
return null;
|
|
74636
|
+
}
|
|
74493
74637
|
async function handleWebhookRoute(req, agent, source, config) {
|
|
74494
74638
|
const agentConfigRaw = config.agents[agent];
|
|
74495
74639
|
const agentConfig = agentConfigRaw ? resolveAgentConfig(config.defaults, config.profiles, agentConfigRaw) : undefined;
|
|
74496
74640
|
const allowedSources = agentConfig?.channels?.telegram?.webhook_sources ?? [];
|
|
74497
74641
|
const allSecrets = loadWebhookSecrets();
|
|
74498
|
-
const agentSecrets = allSecrets[agent] ?? {};
|
|
74642
|
+
const agentSecrets = { ...allSecrets[agent] ?? {} };
|
|
74643
|
+
if (!agentSecrets[source]) {
|
|
74644
|
+
const fromVault = await resolveWebhookSecretFromVault(agent, source, config);
|
|
74645
|
+
if (fromVault)
|
|
74646
|
+
agentSecrets[source] = fromVault;
|
|
74647
|
+
}
|
|
74499
74648
|
const requireEdge = agentConfig?.channels?.telegram?.webhook_require_edge === true;
|
|
74500
74649
|
const edgeSecret = requireEdge ? loadEdgeSecret() : null;
|
|
74501
74650
|
let bodyBuf;
|
|
@@ -82933,6 +83082,10 @@ function scheduleAdd(opts) {
|
|
|
82933
83082
|
entry.secrets = opts.secrets;
|
|
82934
83083
|
if (opts.name)
|
|
82935
83084
|
entry.name = opts.name;
|
|
83085
|
+
if (opts.model)
|
|
83086
|
+
entry.model = opts.model;
|
|
83087
|
+
if (opts.context)
|
|
83088
|
+
entry.context = opts.context;
|
|
82936
83089
|
const doc = {
|
|
82937
83090
|
schedule: [
|
|
82938
83091
|
Object.fromEntries(Object.entries(entry).filter(([k]) => k !== "name"))
|
|
@@ -83017,8 +83170,8 @@ function scheduleAdd(opts) {
|
|
|
83017
83170
|
path: path8,
|
|
83018
83171
|
cron_hash: hash2,
|
|
83019
83172
|
would_recreate: false,
|
|
83020
|
-
restart_required:
|
|
83021
|
-
restart_hint:
|
|
83173
|
+
restart_required: scheduleRestartRequired(),
|
|
83174
|
+
restart_hint: scheduleLiveNote(agent)
|
|
83022
83175
|
};
|
|
83023
83176
|
}
|
|
83024
83177
|
function scheduleAddOrStage(opts) {
|
|
@@ -83043,6 +83196,10 @@ function scheduleAddOrStage(opts) {
|
|
|
83043
83196
|
entry.secrets = opts.secrets;
|
|
83044
83197
|
if (opts.name)
|
|
83045
83198
|
entry.name = opts.name;
|
|
83199
|
+
if (opts.model)
|
|
83200
|
+
entry.model = opts.model;
|
|
83201
|
+
if (opts.context)
|
|
83202
|
+
entry.context = opts.context;
|
|
83046
83203
|
const doc = {
|
|
83047
83204
|
schedule: [
|
|
83048
83205
|
Object.fromEntries(Object.entries(entry).filter(([k]) => k !== "name"))
|
|
@@ -83142,13 +83299,17 @@ function scheduleRemove(opts) {
|
|
|
83142
83299
|
ok: true,
|
|
83143
83300
|
slug: match.slug,
|
|
83144
83301
|
path: match.path,
|
|
83145
|
-
restart_required:
|
|
83146
|
-
restart_hint:
|
|
83302
|
+
restart_required: scheduleRestartRequired(),
|
|
83303
|
+
restart_hint: scheduleLiveNote(agent)
|
|
83147
83304
|
};
|
|
83148
83305
|
}
|
|
83149
83306
|
function registerAgentConfigWriteCommands(program3) {
|
|
83150
83307
|
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) => {
|
|
83308
|
+
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) => {
|
|
83309
|
+
if (opts.context && opts.context !== "fresh" && opts.context !== "agent") {
|
|
83310
|
+
emitError("E_INVALID_PROMPT", "--context must be 'fresh' or 'agent'");
|
|
83311
|
+
process.exit(1);
|
|
83312
|
+
}
|
|
83152
83313
|
const secrets = opts.secrets ? opts.secrets.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
83153
83314
|
if (opts.name && !/^[a-z0-9-]{1,40}$/.test(opts.name)) {
|
|
83154
83315
|
emitError("E_INVALID_PROMPT", "name must match [a-z0-9-]{1,40}");
|
|
@@ -83166,7 +83327,9 @@ function registerAgentConfigWriteCommands(program3) {
|
|
|
83166
83327
|
cronExpr: opts.cron,
|
|
83167
83328
|
prompt: opts.prompt,
|
|
83168
83329
|
secrets,
|
|
83169
|
-
name: opts.name
|
|
83330
|
+
name: opts.name,
|
|
83331
|
+
model: opts.model,
|
|
83332
|
+
context: opts.context
|
|
83170
83333
|
});
|
|
83171
83334
|
} catch (err) {
|
|
83172
83335
|
process.stderr.write(`${err.message}
|
|
@@ -85050,7 +85213,11 @@ import { homedir as homedir48 } from "node:os";
|
|
|
85050
85213
|
import { join as join82 } from "node:path";
|
|
85051
85214
|
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
85052
85215
|
init_audit_reader();
|
|
85053
|
-
|
|
85216
|
+
function resolveHostdImageTag(explicitTag, release) {
|
|
85217
|
+
if (explicitTag)
|
|
85218
|
+
return explicitTag;
|
|
85219
|
+
return resolveImageTag(resolveRelease({ root: release }));
|
|
85220
|
+
}
|
|
85054
85221
|
var HOSTD_COMPOSE_PROJECT = "switchroom-hostd";
|
|
85055
85222
|
function renderHostdComposeFile(opts) {
|
|
85056
85223
|
const { hostHome, imageTag, operatorUid } = opts;
|
|
@@ -85202,9 +85369,10 @@ async function doInstall(opts, program3) {
|
|
|
85202
85369
|
const dir = hostdDir();
|
|
85203
85370
|
const composePath = hostdComposePath();
|
|
85204
85371
|
mkdirSync47(dir, { recursive: true });
|
|
85372
|
+
const imageTag = resolveHostdImageTag(opts.tag, cfg.release);
|
|
85205
85373
|
const yaml = renderHostdComposeFile({
|
|
85206
85374
|
hostHome: resolveHostdHostHome(),
|
|
85207
|
-
imageTag
|
|
85375
|
+
imageTag,
|
|
85208
85376
|
operatorUid: resolveOperatorUid()
|
|
85209
85377
|
});
|
|
85210
85378
|
if (opts.dryRun) {
|
|
@@ -85221,12 +85389,12 @@ async function doInstall(opts, program3) {
|
|
|
85221
85389
|
const adminAgents = Object.entries(cfg.agents ?? {}).filter(([, a]) => a?.admin === true).map(([name]) => name);
|
|
85222
85390
|
console.log(source_default.dim(` agents served (one socket each): ${allAgents.length === 0 ? "(none)" : allAgents.join(", ")}`));
|
|
85223
85391
|
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:${
|
|
85392
|
+
console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-hostd:${imageTag}\u2026`));
|
|
85225
85393
|
const pull = runDocker(["compose", "-p", HOSTD_COMPOSE_PROJECT, "-f", composePath, "pull"]);
|
|
85226
85394
|
if (!pull.ok) {
|
|
85227
85395
|
console.error(source_default.red(` pull failed:
|
|
85228
85396
|
${pull.stderr}`));
|
|
85229
|
-
console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-hostd:${
|
|
85397
|
+
console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-hostd:${imageTag}\` may not be published yet.
|
|
85230
85398
|
` + ` Check the docker-images workflow run and verify the tag at:
|
|
85231
85399
|
` + ` https://github.com/switchroom/switchroom/pkgs/container/switchroom-hostd`));
|
|
85232
85400
|
process.exit(1);
|
|
@@ -85311,7 +85479,7 @@ ${down.stderr}`));
|
|
|
85311
85479
|
}
|
|
85312
85480
|
function registerHostdCommand(program3) {
|
|
85313
85481
|
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)"
|
|
85482
|
+
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
85483
|
await doInstall(opts, program3);
|
|
85316
85484
|
}));
|
|
85317
85485
|
hostd.command("status").description("Show daemon state and bound sockets").action(() => doStatus());
|
|
@@ -85369,7 +85537,11 @@ import { existsSync as existsSync85, mkdirSync as mkdirSync48, writeFileSync as
|
|
|
85369
85537
|
import { homedir as homedir49 } from "node:os";
|
|
85370
85538
|
import { join as join83 } from "node:path";
|
|
85371
85539
|
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
85372
|
-
|
|
85540
|
+
function resolveWebImageTag(explicitTag, release) {
|
|
85541
|
+
if (explicitTag)
|
|
85542
|
+
return explicitTag;
|
|
85543
|
+
return resolveImageTag(resolveRelease({ root: release }));
|
|
85544
|
+
}
|
|
85373
85545
|
var WEB_COMPOSE_PROJECT = "switchroom-web";
|
|
85374
85546
|
function renderWebComposeFile(opts) {
|
|
85375
85547
|
const { hostHome, imageTag, operatorUid } = opts;
|
|
@@ -85470,7 +85642,7 @@ function runDocker2(args) {
|
|
|
85470
85642
|
stderr: r.stderr ?? ""
|
|
85471
85643
|
};
|
|
85472
85644
|
}
|
|
85473
|
-
async function doInstall2(opts) {
|
|
85645
|
+
async function doInstall2(opts, program3) {
|
|
85474
85646
|
const operatorUid = resolveOperatorUid();
|
|
85475
85647
|
if (operatorUid === undefined) {
|
|
85476
85648
|
console.error(source_default.red(`Could not resolve the operator uid (no SUDO_UID and getuid() is 0 or unavailable).
|
|
@@ -85481,9 +85653,11 @@ async function doInstall2(opts) {
|
|
|
85481
85653
|
const dir = webdDir();
|
|
85482
85654
|
const composePath = webdComposePath();
|
|
85483
85655
|
mkdirSync48(dir, { recursive: true });
|
|
85656
|
+
const cfg = getConfig(program3);
|
|
85657
|
+
const imageTag = resolveWebImageTag(opts.tag, cfg.release);
|
|
85484
85658
|
const yaml = renderWebComposeFile({
|
|
85485
85659
|
hostHome: homedir49(),
|
|
85486
|
-
imageTag
|
|
85660
|
+
imageTag,
|
|
85487
85661
|
operatorUid
|
|
85488
85662
|
});
|
|
85489
85663
|
if (opts.dryRun) {
|
|
@@ -85498,12 +85672,12 @@ async function doInstall2(opts) {
|
|
|
85498
85672
|
writeFileSync41(composePath, yaml, "utf8");
|
|
85499
85673
|
console.log(source_default.green(` \u2713 Wrote ${composePath}`));
|
|
85500
85674
|
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:${
|
|
85675
|
+
console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-web:${imageTag}\u2026`));
|
|
85502
85676
|
const pull = runDocker2(["compose", "-p", WEB_COMPOSE_PROJECT, "-f", composePath, "pull"]);
|
|
85503
85677
|
if (!pull.ok) {
|
|
85504
85678
|
console.error(source_default.red(` pull failed:
|
|
85505
85679
|
${pull.stderr}`));
|
|
85506
|
-
console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-web:${
|
|
85680
|
+
console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-web:${imageTag}\` may not be published yet.
|
|
85507
85681
|
` + ` Check the docker-images workflow run and verify the tag at:
|
|
85508
85682
|
` + ` https://github.com/switchroom/switchroom/pkgs/container/switchroom-web`));
|
|
85509
85683
|
process.exit(1);
|
|
@@ -85571,8 +85745,8 @@ ${down.stderr}`));
|
|
|
85571
85745
|
}
|
|
85572
85746
|
function registerWebdCommand(program3) {
|
|
85573
85747
|
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);
|
|
85748
|
+
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) => {
|
|
85749
|
+
await doInstall2(opts, program3);
|
|
85576
85750
|
}));
|
|
85577
85751
|
webd.command("status").description("Show web-service container state").action(() => doStatus2());
|
|
85578
85752
|
webd.command("uninstall").description("Stop the web container. Leaves the compose file in place for re-install.").action(() => doUninstall2());
|
|
@@ -13967,6 +13967,10 @@ var ReactionsSchema = exports_external.object({
|
|
|
13967
13967
|
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."),
|
|
13968
13968
|
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.")
|
|
13969
13969
|
}).optional();
|
|
13970
|
+
var ReactionDispatchSchema = exports_external.object({
|
|
13971
|
+
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."),
|
|
13972
|
+
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.")
|
|
13973
|
+
}).optional();
|
|
13970
13974
|
var ReleaseBlock = exports_external.object({
|
|
13971
13975
|
channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
|
|
13972
13976
|
pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
|
|
@@ -14002,6 +14006,7 @@ var profileFields = {
|
|
|
14002
14006
|
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
14003
14007
|
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)."),
|
|
14004
14008
|
reactions: ReactionsSchema,
|
|
14009
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
14005
14010
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
|
|
14006
14011
|
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."),
|
|
14007
14012
|
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."),
|
|
@@ -14071,6 +14076,7 @@ var AgentSchema = exports_external.object({
|
|
|
14071
14076
|
schedule: exports_external.array(ScheduleEntrySchema).default([]),
|
|
14072
14077
|
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(),
|
|
14073
14078
|
reactions: ReactionsSchema,
|
|
14079
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
14074
14080
|
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')"),
|
|
14075
14081
|
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."),
|
|
14076
14082
|
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."),
|
|
@@ -14746,6 +14752,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
14746
14752
|
}
|
|
14747
14753
|
merged.reactions = combined;
|
|
14748
14754
|
}
|
|
14755
|
+
const dReactionDispatch = defaults.reaction_dispatch;
|
|
14756
|
+
const mReactionDispatch = merged.reaction_dispatch;
|
|
14757
|
+
if (dReactionDispatch || mReactionDispatch) {
|
|
14758
|
+
const base = dReactionDispatch ?? {};
|
|
14759
|
+
const override = mReactionDispatch ?? {};
|
|
14760
|
+
const combined = { ...base };
|
|
14761
|
+
for (const [k, v] of Object.entries(override)) {
|
|
14762
|
+
if (v !== undefined)
|
|
14763
|
+
combined[k] = v;
|
|
14764
|
+
}
|
|
14765
|
+
merged.reaction_dispatch = combined;
|
|
14766
|
+
}
|
|
14749
14767
|
if (defaults.resources || merged.resources) {
|
|
14750
14768
|
const d = defaults.resources ?? {};
|
|
14751
14769
|
const a = merged.resources ?? {};
|
|
@@ -4293,6 +4293,18 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
4293
4293
|
}
|
|
4294
4294
|
merged.reactions = combined;
|
|
4295
4295
|
}
|
|
4296
|
+
const dReactionDispatch = defaults.reaction_dispatch;
|
|
4297
|
+
const mReactionDispatch = merged.reaction_dispatch;
|
|
4298
|
+
if (dReactionDispatch || mReactionDispatch) {
|
|
4299
|
+
const base = dReactionDispatch ?? {};
|
|
4300
|
+
const override = mReactionDispatch ?? {};
|
|
4301
|
+
const combined = { ...base };
|
|
4302
|
+
for (const [k, v] of Object.entries(override)) {
|
|
4303
|
+
if (v !== undefined)
|
|
4304
|
+
combined[k] = v;
|
|
4305
|
+
}
|
|
4306
|
+
merged.reaction_dispatch = combined;
|
|
4307
|
+
}
|
|
4296
4308
|
if (defaults.resources || merged.resources) {
|
|
4297
4309
|
const d = defaults.resources ?? {};
|
|
4298
4310
|
const a = merged.resources ?? {};
|
|
@@ -11277,7 +11289,7 @@ var init_dist = __esm(() => {
|
|
|
11277
11289
|
});
|
|
11278
11290
|
|
|
11279
11291
|
// src/config/schema.ts
|
|
11280
|
-
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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11292
|
+
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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11281
11293
|
var init_schema = __esm(() => {
|
|
11282
11294
|
init_zod();
|
|
11283
11295
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -11553,6 +11565,10 @@ var init_schema = __esm(() => {
|
|
|
11553
11565
|
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."),
|
|
11554
11566
|
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.")
|
|
11555
11567
|
}).optional();
|
|
11568
|
+
ReactionDispatchSchema = exports_external.object({
|
|
11569
|
+
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."),
|
|
11570
|
+
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.")
|
|
11571
|
+
}).optional();
|
|
11556
11572
|
ReleaseBlock = exports_external.object({
|
|
11557
11573
|
channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
|
|
11558
11574
|
pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
|
|
@@ -11588,6 +11604,7 @@ var init_schema = __esm(() => {
|
|
|
11588
11604
|
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
11589
11605
|
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)."),
|
|
11590
11606
|
reactions: ReactionsSchema,
|
|
11607
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
11591
11608
|
model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
|
|
11592
11609
|
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."),
|
|
11593
11610
|
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."),
|
|
@@ -11657,6 +11674,7 @@ var init_schema = __esm(() => {
|
|
|
11657
11674
|
schedule: exports_external.array(ScheduleEntrySchema).default([]),
|
|
11658
11675
|
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(),
|
|
11659
11676
|
reactions: ReactionsSchema,
|
|
11677
|
+
reaction_dispatch: ReactionDispatchSchema,
|
|
11660
11678
|
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')"),
|
|
11661
11679
|
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."),
|
|
11662
11680
|
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."),
|