switchroom 0.14.91 → 0.14.92
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 +1030 -56
- package/dist/auth-broker/index.js +50 -3
- package/dist/cli/notion-write-pretool.mjs +50 -3
- package/dist/cli/switchroom.js +306 -21
- package/dist/host-control/main.js +50 -3
- package/dist/vault/approvals/kernel-server.js +51 -4
- package/dist/vault/broker/server.js +51 -4
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +77 -0
- package/profiles/_base/start.sh.hbs +13 -0
- package/telegram-plugin/dist/gateway/gateway.js +95 -15
- package/telegram-plugin/gateway/cron-session.ts +45 -0
- package/telegram-plugin/gateway/gateway.ts +52 -6
- package/telegram-plugin/tests/cron-session.test.ts +32 -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, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, 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, SwitchroomConfigSchema;
|
|
13523
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, 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;
|
|
13524
13524
|
var init_schema = __esm(() => {
|
|
13525
13525
|
init_zod();
|
|
13526
13526
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -13533,15 +13533,54 @@ var init_schema = __esm(() => {
|
|
|
13533
13533
|
target: exports_external.string().optional().describe("Container path the source mounts to. Must be absolute. Defaults to " + "the same path as `source` (matches switchroom's existing dual-mount " + "convention so absolute paths in scaffolded scripts Just Work)."),
|
|
13534
13534
|
mode: exports_external.enum(["ro", "rw"]).optional().describe("Read-only (default) or read-write. Use `rw` only when the agent " + "must mutate the host path (e.g. editing switchroom source). " + "Default: 'ro'.")
|
|
13535
13535
|
});
|
|
13536
|
+
HttpDiffPollSchema = exports_external.object({
|
|
13537
|
+
type: exports_external.literal("http-diff"),
|
|
13538
|
+
url: exports_external.string().url().describe("Poll target. Host MUST match the operator egress allowlist (\u00a76.1) \u2014 " + "loopback/private/link-local/non-https are rejected; the IP is " + "resolve-then-pinned against DNS-rebind. Not agent-writable without " + "operator commit."),
|
|
13539
|
+
method: exports_external.enum(["GET", "POST"]).default("GET"),
|
|
13540
|
+
headers: exports_external.record(exports_external.string()).optional(),
|
|
13541
|
+
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")).default([]).describe("Vault keys this poll may inject into request headers. Each is " + "HOST-PINNED (\u00a76.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved poll cannot exfil it elsewhere."),
|
|
13542
|
+
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
13543
|
+
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
13544
|
+
});
|
|
13545
|
+
TelegramReactionsPollSchema = exports_external.object({
|
|
13546
|
+
type: exports_external.literal("telegram-reactions"),
|
|
13547
|
+
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
13548
|
+
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\u200d\uD83D\uDCBB)."),
|
|
13549
|
+
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
13550
|
+
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
13551
|
+
});
|
|
13552
|
+
PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
13553
|
+
HttpDiffPollSchema,
|
|
13554
|
+
TelegramReactionsPollSchema
|
|
13555
|
+
]);
|
|
13536
13556
|
ScheduleEntrySchema = exports_external.object({
|
|
13537
13557
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
13538
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
|
|
13539
|
-
|
|
13558
|
+
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
13559
|
+
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. Honoured only when SWITCHROOM_CHEAP_CRON is on."),
|
|
13560
|
+
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
13561
|
+
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) \u2014 the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
13562
|
+
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' \u2192 a minimal-" + "context cheap cron session (Tier 1). 'agent' \u2192 the agent's live " + "session with full persona/memory (Tier 2). Unset \u2192 inferred from " + "`model` (cheap\u2192fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
13540
13563
|
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")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default \u2014 broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary \u2014 " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
13541
13564
|
topic: exports_external.union([
|
|
13542
13565
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
13543
13566
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
13544
13567
|
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load \u2014 typos surface immediately. " + "See docs/rfcs/supergroup-mode.md.")
|
|
13568
|
+
}).superRefine((entry, ctx) => {
|
|
13569
|
+
const kind = entry.kind ?? "prompt";
|
|
13570
|
+
if (kind === "poll" && !entry.poll) {
|
|
13571
|
+
ctx.addIssue({
|
|
13572
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13573
|
+
path: ["poll"],
|
|
13574
|
+
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
13575
|
+
});
|
|
13576
|
+
}
|
|
13577
|
+
if (kind === "prompt" && entry.poll) {
|
|
13578
|
+
ctx.addIssue({
|
|
13579
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13580
|
+
path: ["poll"],
|
|
13581
|
+
message: "`poll` is only valid when kind: poll."
|
|
13582
|
+
});
|
|
13583
|
+
}
|
|
13545
13584
|
});
|
|
13546
13585
|
AgentSoulSchema = exports_external.object({
|
|
13547
13586
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -13988,6 +14027,13 @@ var init_schema = __esm(() => {
|
|
|
13988
14027
|
config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit \u00a73). Default false \u2014 the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
|
|
13989
14028
|
config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit \u00a75). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
|
|
13990
14029
|
});
|
|
14030
|
+
CronEgressSchema = exports_external.object({
|
|
14031
|
+
allowed_hosts: exports_external.array(exports_external.string().min(1)).default([]).describe("Hosts a poll may reach (exact, https-only). loopback/private/IP-literal are always rejected."),
|
|
14032
|
+
secret_bindings: exports_external.record(exports_external.string(), exports_external.string().min(1)).default({}).describe("secretName \u2192 the single host it may be sent to. A poll carrying a secret to any other host is rejected.")
|
|
14033
|
+
});
|
|
14034
|
+
CronConfigSchema = exports_external.object({
|
|
14035
|
+
egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
|
|
14036
|
+
});
|
|
13991
14037
|
SwitchroomConfigSchema = exports_external.object({
|
|
13992
14038
|
switchroom: exports_external.object({
|
|
13993
14039
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -14036,7 +14082,8 @@ var init_schema = __esm(() => {
|
|
|
14036
14082
|
profiles: exports_external.record(exports_external.string(), ProfileSchema).optional().describe("Named profile definitions. Agents reference via `extends: <name>`. " + "Inline profiles declared here take priority over filesystem " + "profiles/<name>/ directories when both exist."),
|
|
14037
14083
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
14038
14084
|
message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
|
|
14039
|
-
}), AgentSchema).describe("Map of agent name to agent configuration")
|
|
14085
|
+
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
14086
|
+
cron: CronConfigSchema.optional().describe("Cheap-cron settings (docs/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (\u00a76.1). Required to enable any http-diff poll; not agent-writable.")
|
|
14040
14087
|
});
|
|
14041
14088
|
});
|
|
14042
14089
|
|
|
@@ -15311,6 +15358,58 @@ var init_helpers = __esm(() => {
|
|
|
15311
15358
|
init_loader();
|
|
15312
15359
|
});
|
|
15313
15360
|
|
|
15361
|
+
// src/scheduler/cron-routing.ts
|
|
15362
|
+
function isKnownCheapModel(model) {
|
|
15363
|
+
return model !== undefined && CHEAP_MODEL_RE.test(model);
|
|
15364
|
+
}
|
|
15365
|
+
function resolveCronModel(model) {
|
|
15366
|
+
return isKnownCheapModel(model) ? model : DEFAULT_CRON_MODEL;
|
|
15367
|
+
}
|
|
15368
|
+
function resolveCronRouting(input, opts) {
|
|
15369
|
+
if (!opts.cheapCronEnabled) {
|
|
15370
|
+
return { tier: "main", session: "main", customModelDowngrade: false };
|
|
15371
|
+
}
|
|
15372
|
+
const kind = input.kind ?? "prompt";
|
|
15373
|
+
let context;
|
|
15374
|
+
let customModelDowngrade = false;
|
|
15375
|
+
if (input.context) {
|
|
15376
|
+
context = input.context;
|
|
15377
|
+
} else if (isKnownCheapModel(input.model)) {
|
|
15378
|
+
context = "fresh";
|
|
15379
|
+
} else {
|
|
15380
|
+
context = "agent";
|
|
15381
|
+
customModelDowngrade = input.model !== undefined && !isKnownCheapModel(input.model) && !OPUS_MODEL_RE.test(input.model);
|
|
15382
|
+
}
|
|
15383
|
+
if (kind === "poll") {
|
|
15384
|
+
return { tier: "poll", session: null, customModelDowngrade };
|
|
15385
|
+
}
|
|
15386
|
+
if (context === "fresh") {
|
|
15387
|
+
return {
|
|
15388
|
+
tier: "cheap",
|
|
15389
|
+
session: "cron",
|
|
15390
|
+
cronModel: resolveCronModel(input.model),
|
|
15391
|
+
customModelDowngrade
|
|
15392
|
+
};
|
|
15393
|
+
}
|
|
15394
|
+
return { tier: "main", session: "main", customModelDowngrade };
|
|
15395
|
+
}
|
|
15396
|
+
function resolveEscalationRouting(input, opts) {
|
|
15397
|
+
return resolveCronRouting({ ...input, kind: "prompt" }, opts);
|
|
15398
|
+
}
|
|
15399
|
+
function scheduleNeedsCronSession(entries, opts) {
|
|
15400
|
+
if (!opts.cheapCronEnabled)
|
|
15401
|
+
return false;
|
|
15402
|
+
return entries.some((e) => {
|
|
15403
|
+
const r = e.kind === "poll" ? resolveEscalationRouting(e, opts) : resolveCronRouting(e, opts);
|
|
15404
|
+
return r.session === "cron";
|
|
15405
|
+
});
|
|
15406
|
+
}
|
|
15407
|
+
var DEFAULT_CRON_MODEL = "claude-sonnet-4-6", CHEAP_MODEL_RE, OPUS_MODEL_RE;
|
|
15408
|
+
var init_cron_routing = __esm(() => {
|
|
15409
|
+
CHEAP_MODEL_RE = /(^|[^a-z])(sonnet|haiku)([^a-z]|$)/i;
|
|
15410
|
+
OPUS_MODEL_RE = /opus/i;
|
|
15411
|
+
});
|
|
15412
|
+
|
|
15314
15413
|
// src/config/timezone.ts
|
|
15315
15414
|
import { readFileSync as readFileSync3, readlinkSync } from "node:fs";
|
|
15316
15415
|
function defaultReadEtcTimezone() {
|
|
@@ -23078,7 +23177,20 @@ var init_tmux = __esm(() => {
|
|
|
23078
23177
|
import { createHash as createHash3 } from "node:crypto";
|
|
23079
23178
|
import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync12 } from "node:fs";
|
|
23080
23179
|
import { join as join9 } from "node:path";
|
|
23081
|
-
function
|
|
23180
|
+
function parseMemToMib(s) {
|
|
23181
|
+
const m = /^(\d+(?:\.\d+)?)\s*([gmk]?)b?$/i.exec(s.trim());
|
|
23182
|
+
if (!m)
|
|
23183
|
+
return null;
|
|
23184
|
+
const n = parseFloat(m[1]);
|
|
23185
|
+
const unit = (m[2] || "m").toLowerCase();
|
|
23186
|
+
const mult = unit === "g" ? 1024 : unit === "k" ? 1 / 1024 : 1;
|
|
23187
|
+
return Math.round(n * mult);
|
|
23188
|
+
}
|
|
23189
|
+
function bumpMem(s, addMib) {
|
|
23190
|
+
const mib = parseMemToMib(s);
|
|
23191
|
+
return mib == null ? s : `${mib + addMib}m`;
|
|
23192
|
+
}
|
|
23193
|
+
function resolveResourceDefaults(agentName, profile, overrides, opts) {
|
|
23082
23194
|
let base;
|
|
23083
23195
|
if (agentName === "klanker") {
|
|
23084
23196
|
base = RESOURCE_BY_PROFILE.klanker;
|
|
@@ -23087,18 +23199,26 @@ function resolveResourceDefaults(agentName, profile, overrides) {
|
|
|
23087
23199
|
} else {
|
|
23088
23200
|
base = RESOURCE_BY_PROFILE.default;
|
|
23089
23201
|
}
|
|
23090
|
-
if (!overrides)
|
|
23091
|
-
return { ...base };
|
|
23092
23202
|
const merged = { ...base };
|
|
23093
|
-
if (overrides
|
|
23094
|
-
|
|
23095
|
-
|
|
23096
|
-
|
|
23097
|
-
|
|
23098
|
-
|
|
23203
|
+
if (overrides) {
|
|
23204
|
+
if (overrides.memory !== undefined)
|
|
23205
|
+
merged.memLimit = overrides.memory;
|
|
23206
|
+
if (overrides.cpus !== undefined)
|
|
23207
|
+
merged.cpus = overrides.cpus;
|
|
23208
|
+
if (overrides.memory_reservation !== undefined) {
|
|
23209
|
+
merged.memReservation = overrides.memory_reservation;
|
|
23210
|
+
}
|
|
23211
|
+
if (overrides.pids_limit !== undefined) {
|
|
23212
|
+
merged.pidsLimit = overrides.pids_limit;
|
|
23213
|
+
}
|
|
23099
23214
|
}
|
|
23100
|
-
if (
|
|
23101
|
-
|
|
23215
|
+
if (opts?.cronSession) {
|
|
23216
|
+
if (overrides?.memory === undefined) {
|
|
23217
|
+
merged.memLimit = bumpMem(merged.memLimit, CRON_SESSION_MEM_BUMP_MIB);
|
|
23218
|
+
}
|
|
23219
|
+
if (overrides?.pids_limit === undefined && merged.pidsLimit !== undefined) {
|
|
23220
|
+
merged.pidsLimit = merged.pidsLimit + CRON_SESSION_PIDS_BUMP;
|
|
23221
|
+
}
|
|
23102
23222
|
}
|
|
23103
23223
|
return merged;
|
|
23104
23224
|
}
|
|
@@ -23153,7 +23273,8 @@ function describeAgents(config) {
|
|
|
23153
23273
|
const resolved = resolveAgentConfig(config.defaults, config.profiles, agent);
|
|
23154
23274
|
const profile = agent.extends ?? "default";
|
|
23155
23275
|
const uid = allocateAgentUid(name);
|
|
23156
|
-
const
|
|
23276
|
+
const cronSession = scheduleNeedsCronSession((resolved.schedule ?? []).map((e) => ({ kind: e.kind, model: e.model, context: e.context })), { cheapCronEnabled: true });
|
|
23277
|
+
const resources = resolveResourceDefaults(name, profile, resolved.resources, { cronSession });
|
|
23157
23278
|
const strippedCaps = readStrippedCaps(agent);
|
|
23158
23279
|
out.push({
|
|
23159
23280
|
name,
|
|
@@ -23616,8 +23737,9 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
23616
23737
|
lines.push(` - ${homePrefix}/.switchroom/logs/${a.name}:${homePrefix}/.switchroom/logs/${a.name}`);
|
|
23617
23738
|
lines.push(``);
|
|
23618
23739
|
}
|
|
23619
|
-
var AGENT_UID_MIN = 10001, AGENT_UID_MAX = 10999, RESOURCE_BY_PROFILE, BIND_MOUNT_SOURCE_DENYLIST, BIND_MOUNT_TARGET_DENYLIST, BIND_MOUNT_EXACT_SOURCE_DENY;
|
|
23740
|
+
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;
|
|
23620
23741
|
var init_compose = __esm(() => {
|
|
23742
|
+
init_cron_routing();
|
|
23621
23743
|
init_merge();
|
|
23622
23744
|
init_timezone();
|
|
23623
23745
|
init_peercred();
|
|
@@ -49815,8 +49937,8 @@ var {
|
|
|
49815
49937
|
} = import__.default;
|
|
49816
49938
|
|
|
49817
49939
|
// src/build-info.ts
|
|
49818
|
-
var VERSION = "0.14.
|
|
49819
|
-
var COMMIT_SHA = "
|
|
49940
|
+
var VERSION = "0.14.92";
|
|
49941
|
+
var COMMIT_SHA = "cd0b9973";
|
|
49820
49942
|
|
|
49821
49943
|
// src/cli/agent.ts
|
|
49822
49944
|
init_source();
|
|
@@ -49926,6 +50048,7 @@ var LEGACY_CRON_SCRIPT_BASENAME_RE = /^cron-(\d+)\.sh$/;
|
|
|
49926
50048
|
|
|
49927
50049
|
// src/agents/scaffold.ts
|
|
49928
50050
|
init_schema();
|
|
50051
|
+
init_cron_routing();
|
|
49929
50052
|
init_merge();
|
|
49930
50053
|
init_timezone();
|
|
49931
50054
|
|
|
@@ -50808,6 +50931,33 @@ function renderFleetInvariants() {
|
|
|
50808
50931
|
].join(`
|
|
50809
50932
|
`);
|
|
50810
50933
|
}
|
|
50934
|
+
function buildCronSessionContext(agentConfig) {
|
|
50935
|
+
const entries = (agentConfig.schedule ?? []).map((e) => ({
|
|
50936
|
+
kind: e.kind,
|
|
50937
|
+
model: e.model,
|
|
50938
|
+
context: e.context
|
|
50939
|
+
}));
|
|
50940
|
+
return {
|
|
50941
|
+
cronSessionEnabled: scheduleNeedsCronSession(entries, { cheapCronEnabled: true }),
|
|
50942
|
+
cronModelQ: shellSingleQuote(DEFAULT_CRON_MODEL)
|
|
50943
|
+
};
|
|
50944
|
+
}
|
|
50945
|
+
function maybeWriteTrimmedCronMcp(agentDir, mcpServers, cronSessionEnabled) {
|
|
50946
|
+
if (!cronSessionEnabled)
|
|
50947
|
+
return null;
|
|
50948
|
+
const telegram = mcpServers["switchroom-telegram"];
|
|
50949
|
+
if (!telegram)
|
|
50950
|
+
return null;
|
|
50951
|
+
const cronDir = join8(agentDir, ".claude-cron");
|
|
50952
|
+
mkdirSync9(cronDir, { recursive: true });
|
|
50953
|
+
const path = join8(cronDir, ".mcp.json");
|
|
50954
|
+
const content = JSON.stringify({ mcpServers: { "switchroom-telegram": telegram } }, null, 2) + `
|
|
50955
|
+
`;
|
|
50956
|
+
if (!existsSync13(path) || readFileSync11(path, "utf-8") !== content) {
|
|
50957
|
+
writeFileSync5(path, content, { encoding: "utf-8", mode: 384 });
|
|
50958
|
+
}
|
|
50959
|
+
return path;
|
|
50960
|
+
}
|
|
50811
50961
|
function alignAgentUid(name, agentDir, uid, opts = {}) {
|
|
50812
50962
|
const writeOut = opts.writeOut ?? ((s) => process.stdout.write(s));
|
|
50813
50963
|
const logsDir = join8(homedir4(), ".switchroom", "logs", name);
|
|
@@ -51617,6 +51767,7 @@ function buildWorkspaceContext(args) {
|
|
|
51617
51767
|
switchroomConfigPathQ: switchroomConfigPath ? shellSingleQuote(resolve11(switchroomConfigPath)) : undefined,
|
|
51618
51768
|
hostHomeQ: process.env.HOME ? shellSingleQuote(process.env.HOME) : undefined,
|
|
51619
51769
|
modelQ: shellSingleQuote(agentConfig.model ?? SWITCHROOM_DEFAULT_MAIN_MODEL),
|
|
51770
|
+
...buildCronSessionContext(agentConfig),
|
|
51620
51771
|
thinkingEffort: agentConfig.thinking_effort ?? SWITCHROOM_DEFAULT_THINKING_EFFORT,
|
|
51621
51772
|
permissionMode: agentConfig.permission_mode,
|
|
51622
51773
|
fallbackModelQ: agentConfig.fallback_model ? shellSingleQuote(agentConfig.fallback_model) : undefined,
|
|
@@ -51856,6 +52007,12 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
51856
52007
|
if (existsSync13(join8(agentDir, "start.sh"))) {
|
|
51857
52008
|
chmodSync2(join8(agentDir, "start.sh"), 448);
|
|
51858
52009
|
}
|
|
52010
|
+
if (context.cronSessionEnabled) {
|
|
52011
|
+
writeIfChanged(join8(agentDir, "cron-session.sh"), () => renderTemplate(join8(basePath, "cron-session.sh.hbs"), context), created, skipped);
|
|
52012
|
+
if (existsSync13(join8(agentDir, "cron-session.sh"))) {
|
|
52013
|
+
chmodSync2(join8(agentDir, "cron-session.sh"), 448);
|
|
52014
|
+
}
|
|
52015
|
+
}
|
|
51859
52016
|
writeIfMissing(join8(agentDir, ".claude", "settings.json"), () => renderTemplate(join8(basePath, "settings.json.hbs"), context), created, skipped, 384);
|
|
51860
52017
|
if (switchroomConfig) {
|
|
51861
52018
|
const settingsPath = join8(agentDir, ".claude", "settings.json");
|
|
@@ -51980,6 +52137,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
51980
52137
|
}
|
|
51981
52138
|
writeIfChanged(mcpJsonPath, () => JSON.stringify({ mcpServers }, null, 2) + `
|
|
51982
52139
|
`, created, skipped, 384);
|
|
52140
|
+
maybeWriteTrimmedCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
|
|
51983
52141
|
mcpServerKeysToTrust = Object.keys(mcpServers);
|
|
51984
52142
|
ensureMcpServersTrusted(agentDir, mcpServerKeysToTrust);
|
|
51985
52143
|
}
|
|
@@ -52683,6 +52841,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
52683
52841
|
hindsightTopicFilterMode,
|
|
52684
52842
|
hostHomeQ: process.env.HOME ? shellSingleQuote(process.env.HOME) : undefined,
|
|
52685
52843
|
modelQ: shellSingleQuote(agentConfig.model ?? SWITCHROOM_DEFAULT_MAIN_MODEL),
|
|
52844
|
+
...buildCronSessionContext(agentConfig),
|
|
52686
52845
|
thinkingEffort: agentConfig.thinking_effort ?? SWITCHROOM_DEFAULT_THINKING_EFFORT,
|
|
52687
52846
|
permissionMode: agentConfig.permission_mode,
|
|
52688
52847
|
fallbackModelQ: agentConfig.fallback_model ? shellSingleQuote(agentConfig.fallback_model) : undefined,
|
|
@@ -52737,6 +52896,16 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
52737
52896
|
chmodSync2(startShPath, 493);
|
|
52738
52897
|
changes.push(startShPath);
|
|
52739
52898
|
}
|
|
52899
|
+
if (startShContext.cronSessionEnabled) {
|
|
52900
|
+
const cronShPath = join8(agentDir, "cron-session.sh");
|
|
52901
|
+
const beforeCronSh = existsSync13(cronShPath) ? readFileSync11(cronShPath, "utf-8") : "";
|
|
52902
|
+
const afterCronSh = renderTemplate(join8(basePath, "cron-session.sh.hbs"), startShContext);
|
|
52903
|
+
if (afterCronSh !== beforeCronSh) {
|
|
52904
|
+
writeFileSync5(cronShPath, afterCronSh, "utf-8");
|
|
52905
|
+
chmodSync2(cronShPath, 493);
|
|
52906
|
+
changes.push(cronShPath);
|
|
52907
|
+
}
|
|
52908
|
+
}
|
|
52740
52909
|
}
|
|
52741
52910
|
if (!options.preserveClaudeMd && !options.skipProfileTemplates) {
|
|
52742
52911
|
const profilePath = getProfilePath(agentConfig.extends ?? DEFAULT_PROFILE);
|
|
@@ -53024,6 +53193,9 @@ ${body}
|
|
|
53024
53193
|
writeFileSync5(mcpJsonPath, after, { encoding: "utf-8", mode: 384 });
|
|
53025
53194
|
changes.push(mcpJsonPath);
|
|
53026
53195
|
}
|
|
53196
|
+
const trimmedCronMcp = maybeWriteTrimmedCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
|
|
53197
|
+
if (trimmedCronMcp)
|
|
53198
|
+
changes.push(trimmedCronMcp);
|
|
53027
53199
|
ensureMcpServersTrusted(agentDir, Object.keys(mcpServers));
|
|
53028
53200
|
}
|
|
53029
53201
|
const reconcileWorkspaceDir = join8(agentDir, "workspace");
|
|
@@ -67170,7 +67342,11 @@ function collectScheduleEntries(config) {
|
|
|
67170
67342
|
cron: entry.cron,
|
|
67171
67343
|
prompt: entry.prompt,
|
|
67172
67344
|
promptKey: createHash9("sha256").update(entry.prompt).digest("hex").slice(0, 12),
|
|
67173
|
-
...entry.topic !== undefined ? { topic: entry.topic } : {}
|
|
67345
|
+
...entry.topic !== undefined ? { topic: entry.topic } : {},
|
|
67346
|
+
...entry.kind !== undefined ? { kind: entry.kind } : {},
|
|
67347
|
+
...entry.model !== undefined ? { model: entry.model } : {},
|
|
67348
|
+
...entry.context !== undefined ? { context: entry.context } : {},
|
|
67349
|
+
...entry.poll !== undefined ? { poll: entry.poll } : {}
|
|
67174
67350
|
});
|
|
67175
67351
|
}
|
|
67176
67352
|
}
|
|
@@ -81965,8 +82141,80 @@ function denyPendingScheduleEntry(opts) {
|
|
|
81965
82141
|
}
|
|
81966
82142
|
|
|
81967
82143
|
// src/cli/agent-config-write.ts
|
|
81968
|
-
init_protocol3();
|
|
81969
82144
|
import { existsSync as existsSync78, readFileSync as readFileSync65 } from "node:fs";
|
|
82145
|
+
import { execFileSync as execFileSync25 } from "node:child_process";
|
|
82146
|
+
|
|
82147
|
+
// src/scheduler/schedule-report.ts
|
|
82148
|
+
var TIER_WEIGHT = { poll: 0, cheap: 1, main: 5 };
|
|
82149
|
+
function summarizeScheduleReport(rows, opts = {}) {
|
|
82150
|
+
const s = {
|
|
82151
|
+
total: 0,
|
|
82152
|
+
pollFires: 0,
|
|
82153
|
+
cheapFires: 0,
|
|
82154
|
+
mainFires: 0,
|
|
82155
|
+
errors: 0,
|
|
82156
|
+
deferred: 0,
|
|
82157
|
+
costWeight: 0,
|
|
82158
|
+
byModel: {}
|
|
82159
|
+
};
|
|
82160
|
+
for (const r of rows) {
|
|
82161
|
+
if (opts.sinceMs !== undefined && (r.startedAt ?? 0) < opts.sinceMs)
|
|
82162
|
+
continue;
|
|
82163
|
+
s.total += 1;
|
|
82164
|
+
const tier = r.tier ?? "main";
|
|
82165
|
+
if (tier === "poll")
|
|
82166
|
+
s.pollFires += 1;
|
|
82167
|
+
else if (tier === "cheap")
|
|
82168
|
+
s.cheapFires += 1;
|
|
82169
|
+
else
|
|
82170
|
+
s.mainFires += 1;
|
|
82171
|
+
s.costWeight += TIER_WEIGHT[tier];
|
|
82172
|
+
if (r.exitCode === -2)
|
|
82173
|
+
s.deferred += 1;
|
|
82174
|
+
else if (r.exitCode < 0)
|
|
82175
|
+
s.errors += 1;
|
|
82176
|
+
if (r.modelUsed)
|
|
82177
|
+
s.byModel[r.modelUsed] = (s.byModel[r.modelUsed] ?? 0) + 1;
|
|
82178
|
+
}
|
|
82179
|
+
return s;
|
|
82180
|
+
}
|
|
82181
|
+
function parseScheduleJsonl(blob) {
|
|
82182
|
+
const rows = [];
|
|
82183
|
+
for (const line of blob.split(`
|
|
82184
|
+
`)) {
|
|
82185
|
+
const t = line.trim();
|
|
82186
|
+
if (!t)
|
|
82187
|
+
continue;
|
|
82188
|
+
try {
|
|
82189
|
+
const o = JSON.parse(t);
|
|
82190
|
+
if (typeof o.exitCode === "number")
|
|
82191
|
+
rows.push(o);
|
|
82192
|
+
} catch {}
|
|
82193
|
+
}
|
|
82194
|
+
return rows;
|
|
82195
|
+
}
|
|
82196
|
+
function formatScheduleReport(agent, s) {
|
|
82197
|
+
const modelFires = s.cheapFires + s.mainFires;
|
|
82198
|
+
const savedPct = s.total > 0 ? Math.round(s.pollFires / s.total * 100) : 0;
|
|
82199
|
+
const lines = [
|
|
82200
|
+
`cron report \u2014 ${agent}`,
|
|
82201
|
+
` total fires ${s.total}`,
|
|
82202
|
+
` Tier 0 poll (free) ${s.pollFires} (${savedPct}% model-free)`,
|
|
82203
|
+
` Tier 1 cheap ${s.cheapFires}`,
|
|
82204
|
+
` Tier 2 main ${s.mainFires}`,
|
|
82205
|
+
` model fires ${modelFires}`,
|
|
82206
|
+
` errors / deferred ${s.errors} / ${s.deferred}`,
|
|
82207
|
+
` cost-weight proxy ${s.costWeight} (poll\u00d70 + cheap\u00d71 + main\u00d75; relative, not $)`
|
|
82208
|
+
];
|
|
82209
|
+
const models = Object.entries(s.byModel);
|
|
82210
|
+
if (models.length)
|
|
82211
|
+
lines.push(` by model ${models.map(([m, n]) => `${m}=${n}`).join(" ")}`);
|
|
82212
|
+
return lines.join(`
|
|
82213
|
+
`);
|
|
82214
|
+
}
|
|
82215
|
+
|
|
82216
|
+
// src/cli/agent-config-write.ts
|
|
82217
|
+
init_protocol3();
|
|
81970
82218
|
var MAX_ENTRIES_PER_AGENT = 20;
|
|
81971
82219
|
var MIN_CRON_INTERVAL_MIN = 5;
|
|
81972
82220
|
function extractCronSmallestGapMin(expr) {
|
|
@@ -82441,6 +82689,43 @@ function registerAgentConfigWriteCommands(program3) {
|
|
|
82441
82689
|
`);
|
|
82442
82690
|
appendAudit(resolvedAgent, "schedule.remove", { ...opts, slug: r.slug }, 0);
|
|
82443
82691
|
});
|
|
82692
|
+
schedule.command("report [agent]").description("Cheap-cron cost breakdown from the agent's scheduler.jsonl (Tier 0/1/2 fire counts)").option("--jsonl <path>", "Read a local scheduler.jsonl instead of docker-exec into the container").option("--since <iso>", "Only count fires at/after this ISO timestamp").option("--json", "Emit the summary as JSON").action((agentArg, opts) => {
|
|
82693
|
+
const agent = agentArg ?? process.env.SWITCHROOM_AGENT_NAME;
|
|
82694
|
+
if (!agent) {
|
|
82695
|
+
process.stderr.write(`schedule report: pass an agent name (or set $SWITCHROOM_AGENT_NAME)
|
|
82696
|
+
`);
|
|
82697
|
+
process.exit(2);
|
|
82698
|
+
}
|
|
82699
|
+
let blob;
|
|
82700
|
+
if (opts.jsonl) {
|
|
82701
|
+
blob = existsSync78(opts.jsonl) ? readFileSync65(opts.jsonl, "utf-8") : "";
|
|
82702
|
+
} else {
|
|
82703
|
+
try {
|
|
82704
|
+
blob = execFileSync25("docker", ["exec", `switchroom-${agent}`, "cat", "/state/agent/scheduler.jsonl"], {
|
|
82705
|
+
encoding: "utf-8",
|
|
82706
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
82707
|
+
});
|
|
82708
|
+
} catch {
|
|
82709
|
+
process.stderr.write(`schedule report: could not read scheduler.jsonl from container switchroom-${agent} ` + `(is it running? try --jsonl <path>)
|
|
82710
|
+
`);
|
|
82711
|
+
process.exit(1);
|
|
82712
|
+
}
|
|
82713
|
+
}
|
|
82714
|
+
const sinceMs = opts.since ? Date.parse(opts.since) : undefined;
|
|
82715
|
+
if (opts.since && Number.isNaN(sinceMs)) {
|
|
82716
|
+
process.stderr.write(`schedule report: invalid --since '${opts.since}'
|
|
82717
|
+
`);
|
|
82718
|
+
process.exit(2);
|
|
82719
|
+
}
|
|
82720
|
+
const summary = summarizeScheduleReport(parseScheduleJsonl(blob), sinceMs ? { sinceMs } : {});
|
|
82721
|
+
if (opts.json) {
|
|
82722
|
+
process.stdout.write(JSON.stringify({ agent, ...summary }) + `
|
|
82723
|
+
`);
|
|
82724
|
+
} else {
|
|
82725
|
+
process.stdout.write(formatScheduleReport(agent, summary) + `
|
|
82726
|
+
`);
|
|
82727
|
+
}
|
|
82728
|
+
});
|
|
82444
82729
|
}
|
|
82445
82730
|
|
|
82446
82731
|
// src/cli/agent-config-skill-write.ts
|
|
@@ -13704,15 +13704,54 @@ var AgentBindMountSchema = exports_external.object({
|
|
|
13704
13704
|
target: exports_external.string().optional().describe("Container path the source mounts to. Must be absolute. Defaults to " + "the same path as `source` (matches switchroom's existing dual-mount " + "convention so absolute paths in scaffolded scripts Just Work)."),
|
|
13705
13705
|
mode: exports_external.enum(["ro", "rw"]).optional().describe("Read-only (default) or read-write. Use `rw` only when the agent " + "must mutate the host path (e.g. editing switchroom source). " + "Default: 'ro'.")
|
|
13706
13706
|
});
|
|
13707
|
+
var HttpDiffPollSchema = exports_external.object({
|
|
13708
|
+
type: exports_external.literal("http-diff"),
|
|
13709
|
+
url: exports_external.string().url().describe("Poll target. Host MUST match the operator egress allowlist (§6.1) — " + "loopback/private/link-local/non-https are rejected; the IP is " + "resolve-then-pinned against DNS-rebind. Not agent-writable without " + "operator commit."),
|
|
13710
|
+
method: exports_external.enum(["GET", "POST"]).default("GET"),
|
|
13711
|
+
headers: exports_external.record(exports_external.string()).optional(),
|
|
13712
|
+
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")).default([]).describe("Vault keys this poll may inject into request headers. Each is " + "HOST-PINNED (§6.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved poll cannot exfil it elsewhere."),
|
|
13713
|
+
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
13714
|
+
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
13715
|
+
});
|
|
13716
|
+
var TelegramReactionsPollSchema = exports_external.object({
|
|
13717
|
+
type: exports_external.literal("telegram-reactions"),
|
|
13718
|
+
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
13719
|
+
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\uD83D\uDCBB)."),
|
|
13720
|
+
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
13721
|
+
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
13722
|
+
});
|
|
13723
|
+
var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
13724
|
+
HttpDiffPollSchema,
|
|
13725
|
+
TelegramReactionsPollSchema
|
|
13726
|
+
]);
|
|
13707
13727
|
var ScheduleEntrySchema = exports_external.object({
|
|
13708
13728
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
13709
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
|
|
13710
|
-
|
|
13729
|
+
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
13730
|
+
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. Honoured only when SWITCHROOM_CHEAP_CRON is on."),
|
|
13731
|
+
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
13732
|
+
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) — the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
13733
|
+
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' → a minimal-" + "context cheap cron session (Tier 1). 'agent' → the agent's live " + "session with full persona/memory (Tier 2). Unset → inferred from " + "`model` (cheap→fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
13711
13734
|
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")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
13712
13735
|
topic: exports_external.union([
|
|
13713
13736
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
13714
13737
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
13715
13738
|
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See docs/rfcs/supergroup-mode.md.")
|
|
13739
|
+
}).superRefine((entry, ctx) => {
|
|
13740
|
+
const kind = entry.kind ?? "prompt";
|
|
13741
|
+
if (kind === "poll" && !entry.poll) {
|
|
13742
|
+
ctx.addIssue({
|
|
13743
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13744
|
+
path: ["poll"],
|
|
13745
|
+
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
13746
|
+
});
|
|
13747
|
+
}
|
|
13748
|
+
if (kind === "prompt" && entry.poll) {
|
|
13749
|
+
ctx.addIssue({
|
|
13750
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13751
|
+
path: ["poll"],
|
|
13752
|
+
message: "`poll` is only valid when kind: poll."
|
|
13753
|
+
});
|
|
13754
|
+
}
|
|
13716
13755
|
});
|
|
13717
13756
|
var AgentSoulSchema = exports_external.object({
|
|
13718
13757
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -14159,6 +14198,13 @@ var HostdConfigSchema = exports_external.object({
|
|
|
14159
14198
|
config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
|
|
14160
14199
|
config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
|
|
14161
14200
|
});
|
|
14201
|
+
var CronEgressSchema = exports_external.object({
|
|
14202
|
+
allowed_hosts: exports_external.array(exports_external.string().min(1)).default([]).describe("Hosts a poll may reach (exact, https-only). loopback/private/IP-literal are always rejected."),
|
|
14203
|
+
secret_bindings: exports_external.record(exports_external.string(), exports_external.string().min(1)).default({}).describe("secretName → the single host it may be sent to. A poll carrying a secret to any other host is rejected.")
|
|
14204
|
+
});
|
|
14205
|
+
var CronConfigSchema = exports_external.object({
|
|
14206
|
+
egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
|
|
14207
|
+
});
|
|
14162
14208
|
var SwitchroomConfigSchema = exports_external.object({
|
|
14163
14209
|
switchroom: exports_external.object({
|
|
14164
14210
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -14207,7 +14253,8 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
14207
14253
|
profiles: exports_external.record(exports_external.string(), ProfileSchema).optional().describe("Named profile definitions. Agents reference via `extends: <name>`. " + "Inline profiles declared here take priority over filesystem " + "profiles/<name>/ directories when both exist."),
|
|
14208
14254
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
14209
14255
|
message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
|
|
14210
|
-
}), AgentSchema).describe("Map of agent name to agent configuration")
|
|
14256
|
+
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
14257
|
+
cron: CronConfigSchema.optional().describe("Cheap-cron settings (docs/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (§6.1). Required to enable any http-diff poll; not agent-writable.")
|
|
14211
14258
|
});
|
|
14212
14259
|
|
|
14213
14260
|
// src/config/paths.ts
|