switchroom 0.14.90 → 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 +147 -15
- package/telegram-plugin/gateway/cron-session.ts +45 -0
- package/telegram-plugin/gateway/gateway.ts +116 -7
- package/telegram-plugin/gateway/subagent-status-surface.test.ts +118 -0
- package/telegram-plugin/gateway/subagent-status-surface.ts +69 -0
- package/telegram-plugin/tests/cron-session.test.ts +32 -0
|
@@ -11277,7 +11277,7 @@ var init_dist = __esm(() => {
|
|
|
11277
11277
|
});
|
|
11278
11278
|
|
|
11279
11279
|
// src/config/schema.ts
|
|
11280
|
-
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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
|
|
11280
|
+
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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11281
11281
|
var init_schema = __esm(() => {
|
|
11282
11282
|
init_zod();
|
|
11283
11283
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -11290,15 +11290,54 @@ var init_schema = __esm(() => {
|
|
|
11290
11290
|
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)."),
|
|
11291
11291
|
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'.")
|
|
11292
11292
|
});
|
|
11293
|
+
HttpDiffPollSchema = exports_external.object({
|
|
11294
|
+
type: exports_external.literal("http-diff"),
|
|
11295
|
+
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."),
|
|
11296
|
+
method: exports_external.enum(["GET", "POST"]).default("GET"),
|
|
11297
|
+
headers: exports_external.record(exports_external.string()).optional(),
|
|
11298
|
+
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."),
|
|
11299
|
+
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
11300
|
+
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
11301
|
+
});
|
|
11302
|
+
TelegramReactionsPollSchema = exports_external.object({
|
|
11303
|
+
type: exports_external.literal("telegram-reactions"),
|
|
11304
|
+
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)."),
|
|
11305
|
+
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\uD83D\uDCBB)."),
|
|
11306
|
+
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
11307
|
+
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
11308
|
+
});
|
|
11309
|
+
PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11310
|
+
HttpDiffPollSchema,
|
|
11311
|
+
TelegramReactionsPollSchema
|
|
11312
|
+
]);
|
|
11293
11313
|
ScheduleEntrySchema = exports_external.object({
|
|
11294
11314
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
11295
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
|
|
11296
|
-
|
|
11315
|
+
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
11316
|
+
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."),
|
|
11317
|
+
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11318
|
+
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."),
|
|
11319
|
+
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."),
|
|
11297
11320
|
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."),
|
|
11298
11321
|
topic: exports_external.union([
|
|
11299
11322
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
11300
11323
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
11301
11324
|
]).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.")
|
|
11325
|
+
}).superRefine((entry, ctx) => {
|
|
11326
|
+
const kind = entry.kind ?? "prompt";
|
|
11327
|
+
if (kind === "poll" && !entry.poll) {
|
|
11328
|
+
ctx.addIssue({
|
|
11329
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11330
|
+
path: ["poll"],
|
|
11331
|
+
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11332
|
+
});
|
|
11333
|
+
}
|
|
11334
|
+
if (kind === "prompt" && entry.poll) {
|
|
11335
|
+
ctx.addIssue({
|
|
11336
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11337
|
+
path: ["poll"],
|
|
11338
|
+
message: "`poll` is only valid when kind: poll."
|
|
11339
|
+
});
|
|
11340
|
+
}
|
|
11302
11341
|
});
|
|
11303
11342
|
AgentSoulSchema = exports_external.object({
|
|
11304
11343
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -11745,6 +11784,13 @@ var init_schema = __esm(() => {
|
|
|
11745
11784
|
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."),
|
|
11746
11785
|
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.")
|
|
11747
11786
|
});
|
|
11787
|
+
CronEgressSchema = exports_external.object({
|
|
11788
|
+
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."),
|
|
11789
|
+
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.")
|
|
11790
|
+
});
|
|
11791
|
+
CronConfigSchema = exports_external.object({
|
|
11792
|
+
egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
|
|
11793
|
+
});
|
|
11748
11794
|
SwitchroomConfigSchema = exports_external.object({
|
|
11749
11795
|
switchroom: exports_external.object({
|
|
11750
11796
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -11793,7 +11839,8 @@ var init_schema = __esm(() => {
|
|
|
11793
11839
|
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."),
|
|
11794
11840
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
11795
11841
|
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)"
|
|
11796
|
-
}), AgentSchema).describe("Map of agent name to agent configuration")
|
|
11842
|
+
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
11843
|
+
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.")
|
|
11797
11844
|
});
|
|
11798
11845
|
});
|
|
11799
11846
|
|
|
@@ -11277,7 +11277,7 @@ var init_zod = __esm(() => {
|
|
|
11277
11277
|
});
|
|
11278
11278
|
|
|
11279
11279
|
// src/config/schema.ts
|
|
11280
|
-
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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
|
|
11280
|
+
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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11281
11281
|
var init_schema = __esm(() => {
|
|
11282
11282
|
init_zod();
|
|
11283
11283
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -11290,15 +11290,54 @@ var init_schema = __esm(() => {
|
|
|
11290
11290
|
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)."),
|
|
11291
11291
|
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'.")
|
|
11292
11292
|
});
|
|
11293
|
+
HttpDiffPollSchema = exports_external.object({
|
|
11294
|
+
type: exports_external.literal("http-diff"),
|
|
11295
|
+
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."),
|
|
11296
|
+
method: exports_external.enum(["GET", "POST"]).default("GET"),
|
|
11297
|
+
headers: exports_external.record(exports_external.string()).optional(),
|
|
11298
|
+
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."),
|
|
11299
|
+
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
11300
|
+
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
11301
|
+
});
|
|
11302
|
+
TelegramReactionsPollSchema = exports_external.object({
|
|
11303
|
+
type: exports_external.literal("telegram-reactions"),
|
|
11304
|
+
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)."),
|
|
11305
|
+
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\uD83D\uDCBB)."),
|
|
11306
|
+
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
11307
|
+
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
11308
|
+
});
|
|
11309
|
+
PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11310
|
+
HttpDiffPollSchema,
|
|
11311
|
+
TelegramReactionsPollSchema
|
|
11312
|
+
]);
|
|
11293
11313
|
ScheduleEntrySchema = exports_external.object({
|
|
11294
11314
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
11295
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
|
|
11296
|
-
|
|
11315
|
+
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
11316
|
+
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."),
|
|
11317
|
+
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11318
|
+
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."),
|
|
11319
|
+
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."),
|
|
11297
11320
|
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."),
|
|
11298
11321
|
topic: exports_external.union([
|
|
11299
11322
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
11300
11323
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
11301
11324
|
]).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.")
|
|
11325
|
+
}).superRefine((entry, ctx) => {
|
|
11326
|
+
const kind = entry.kind ?? "prompt";
|
|
11327
|
+
if (kind === "poll" && !entry.poll) {
|
|
11328
|
+
ctx.addIssue({
|
|
11329
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11330
|
+
path: ["poll"],
|
|
11331
|
+
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11332
|
+
});
|
|
11333
|
+
}
|
|
11334
|
+
if (kind === "prompt" && entry.poll) {
|
|
11335
|
+
ctx.addIssue({
|
|
11336
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11337
|
+
path: ["poll"],
|
|
11338
|
+
message: "`poll` is only valid when kind: poll."
|
|
11339
|
+
});
|
|
11340
|
+
}
|
|
11302
11341
|
});
|
|
11303
11342
|
AgentSoulSchema = exports_external.object({
|
|
11304
11343
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -11745,6 +11784,13 @@ var init_schema = __esm(() => {
|
|
|
11745
11784
|
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."),
|
|
11746
11785
|
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.")
|
|
11747
11786
|
});
|
|
11787
|
+
CronEgressSchema = exports_external.object({
|
|
11788
|
+
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."),
|
|
11789
|
+
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.")
|
|
11790
|
+
});
|
|
11791
|
+
CronConfigSchema = exports_external.object({
|
|
11792
|
+
egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
|
|
11793
|
+
});
|
|
11748
11794
|
SwitchroomConfigSchema = exports_external.object({
|
|
11749
11795
|
switchroom: exports_external.object({
|
|
11750
11796
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -11793,7 +11839,8 @@ var init_schema = __esm(() => {
|
|
|
11793
11839
|
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."),
|
|
11794
11840
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
11795
11841
|
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)"
|
|
11796
|
-
}), AgentSchema).describe("Map of agent name to agent configuration")
|
|
11842
|
+
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
11843
|
+
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.")
|
|
11797
11844
|
});
|
|
11798
11845
|
});
|
|
11799
11846
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tier-1 cheap cron SESSION launcher — docs/rfcs/cheap-cron-sessions.md §2.2.
|
|
3
|
+
#
|
|
4
|
+
# A SECOND interactive `claude` (no -p — compliance pillar 3) in the agent
|
|
5
|
+
# container, dedicated to cheap cron fires. It registers to the SAME gateway
|
|
6
|
+
# as a DISTINCT bridge identity `<name>-cron` (start.sh's gateway sidecar
|
|
7
|
+
# routes meta.session=cron fires here and gates all status surfaces off this
|
|
8
|
+
# identity — see telegram-plugin/gateway/cron-session.ts). Minimal context:
|
|
9
|
+
# its own CLAUDE_CONFIG_DIR (separate transcript) + a TRIMMED .mcp.json (only
|
|
10
|
+
# switchroom-telegram) → it pays a fraction of the main session's schema +
|
|
11
|
+
# cache-read tax, at the cheap cron model.
|
|
12
|
+
#
|
|
13
|
+
# Forked as a supervised sidecar by start.sh ONLY when {{name}} actually has a
|
|
14
|
+
# Tier-1 (context: fresh) cron entry AND SWITCHROOM_CHEAP_CRON is on — so the
|
|
15
|
+
# whole feature is inert for the rest of the fleet (the start.sh fork block
|
|
16
|
+
# renders empty). This script is a no-op artifact on disk until then.
|
|
17
|
+
set -u
|
|
18
|
+
|
|
19
|
+
# Runtime kill-switch. The fork is baked into start.sh whenever {{name}} has a
|
|
20
|
+
# context:fresh cron entry (a config property), but the session only actually
|
|
21
|
+
# runs when SWITCHROOM_CHEAP_CRON is on at runtime. Exit 78 (EX_CONFIG) so the
|
|
22
|
+
# supervisor does NOT respawn-loop when the feature is off — a clean idle.
|
|
23
|
+
case "${SWITCHROOM_CHEAP_CRON:-}" in
|
|
24
|
+
1 | true | on | ON) : ;;
|
|
25
|
+
*)
|
|
26
|
+
echo "cron-session: SWITCHROOM_CHEAP_CRON off — not starting (exit 78, no respawn)" >&2
|
|
27
|
+
exit 78
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
31
|
+
CRON_NAME="{{name}}-cron"
|
|
32
|
+
CRON_CONFIG_DIR="{{agentDir}}/.claude-cron"
|
|
33
|
+
MAIN_CONFIG_DIR="{{agentDir}}/.claude"
|
|
34
|
+
# TRIMMED MCP config (scaffold writes .claude-cron/.mcp.json with ONLY
|
|
35
|
+
# switchroom-telegram) → the cron session pays a fraction of the main session's
|
|
36
|
+
# ~31k-token MCP schema tax. The switchroom-telegram BRIDGE reads
|
|
37
|
+
# SWITCHROOM_AGENT_NAME from the process env (exported below), NOT its .mcp.json
|
|
38
|
+
# env block, so it registers as the cron identity. Fall back to the full
|
|
39
|
+
# .mcp.json if the trimmed file is missing (older scaffold) so it still boots.
|
|
40
|
+
CRON_MCP_CONFIG="{{agentDir}}/.claude-cron/.mcp.json"
|
|
41
|
+
[ -f "$CRON_MCP_CONFIG" ] || CRON_MCP_CONFIG="{{agentDir}}/.mcp.json"
|
|
42
|
+
CRON_SOCKET="switchroom-${CRON_NAME}"
|
|
43
|
+
|
|
44
|
+
mkdir -p "$CRON_CONFIG_DIR"
|
|
45
|
+
|
|
46
|
+
# Share the broker-managed OAuth creds (same subscription) without sharing the
|
|
47
|
+
# main session's transcript/settings. The broker is the sole writer of
|
|
48
|
+
# .credentials.json into the MAIN config dir; symlink it so the cron session
|
|
49
|
+
# authenticates with the same account and re-reads on refresh. Transcript
|
|
50
|
+
# (projects/) stays separate → minimal, /clear-friendly context.
|
|
51
|
+
if [ -e "$MAIN_CONFIG_DIR/.credentials.json" ]; then
|
|
52
|
+
ln -sfn "$MAIN_CONFIG_DIR/.credentials.json" "$CRON_CONFIG_DIR/.credentials.json"
|
|
53
|
+
fi
|
|
54
|
+
if [ -e "$MAIN_CONFIG_DIR/settings.json" ]; then
|
|
55
|
+
ln -sfn "$MAIN_CONFIG_DIR/settings.json" "$CRON_CONFIG_DIR/settings.json"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Distinct identity + config dir for the bridge that runs inside claude.
|
|
59
|
+
export SWITCHROOM_AGENT_NAME="$CRON_NAME"
|
|
60
|
+
export CLAUDE_CONFIG_DIR="$CRON_CONFIG_DIR"
|
|
61
|
+
|
|
62
|
+
CRON_APPEND_PROMPT="You are the cheap background cron worker for {{name}}. You handle scheduled tasks only. Do the task, reply once with the result, and keep it brief. You do not have {{name}}'s full memory or persona — if a task needs them, say so rather than guessing."
|
|
63
|
+
|
|
64
|
+
# Launch the cron claude in its OWN tmux session/socket so it never contends
|
|
65
|
+
# with the main session's pane. Interactive (no -p). --strict-mcp-config pins
|
|
66
|
+
# it to the trimmed config (switchroom-telegram only). Fresh each boot (no
|
|
67
|
+
# --continue) — minimal context by construction.
|
|
68
|
+
exec tmux -L "$CRON_SOCKET" \
|
|
69
|
+
new-session -A -s "$CRON_NAME" -x 400 -y 50 \
|
|
70
|
+
claude \
|
|
71
|
+
--dangerously-load-development-channels server:switchroom-telegram \
|
|
72
|
+
--plugin-dir "{{securityPluginDir}}" \
|
|
73
|
+
--mcp-config "$CRON_MCP_CONFIG" \
|
|
74
|
+
--strict-mcp-config \
|
|
75
|
+
--model {{{cronModelQ}}} \
|
|
76
|
+
--append-system-prompt "$CRON_APPEND_PROMPT"{{#if dangerousMode}} \
|
|
77
|
+
--dangerously-skip-permissions{{/if}}
|
|
@@ -163,6 +163,19 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
163
163
|
bun /opt/switchroom/agent-scheduler/index.js &
|
|
164
164
|
fi
|
|
165
165
|
|
|
166
|
+
{{#if cronSessionEnabled}}
|
|
167
|
+
# 4) cheap cron SESSION (Tier 1, docs/rfcs/cheap-cron-sessions.md §2.2).
|
|
168
|
+
# A SECOND interactive claude (no -p) dedicated to context:fresh cron
|
|
169
|
+
# fires, registering to the gateway as the cron-suffixed bridge. This
|
|
170
|
+
# block is rendered ONLY for an agent that has a Tier-1 cron entry; for
|
|
171
|
+
# every other agent the cronSessionEnabled guard is false so the whole
|
|
172
|
+
# block is absent and their start.sh is byte-identical to before.
|
|
173
|
+
if command -v claude >/dev/null 2>&1 && [ -f "$(dirname "$0")/cron-session.sh" ]; then
|
|
174
|
+
_switchroom_supervise cron-session /var/log/switchroom/cron-session.log \
|
|
175
|
+
bash "$(dirname "$0")/cron-session.sh" &
|
|
176
|
+
fi
|
|
177
|
+
{{/if}}
|
|
178
|
+
|
|
166
179
|
export SWITCHROOM_DOCKER_TMUX_INNER=1
|
|
167
180
|
exec tmux -L "switchroom-{{name}}" \
|
|
168
181
|
new-session -A -s "{{name}}" -x 400 -y 50 \
|
|
@@ -23781,7 +23781,7 @@ var init_dist = __esm(() => {
|
|
|
23781
23781
|
});
|
|
23782
23782
|
|
|
23783
23783
|
// ../src/config/schema.ts
|
|
23784
|
-
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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
|
|
23784
|
+
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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
23785
23785
|
var init_schema = __esm(() => {
|
|
23786
23786
|
init_zod();
|
|
23787
23787
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -23794,15 +23794,54 @@ var init_schema = __esm(() => {
|
|
|
23794
23794
|
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)."),
|
|
23795
23795
|
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'.")
|
|
23796
23796
|
});
|
|
23797
|
+
HttpDiffPollSchema = exports_external.object({
|
|
23798
|
+
type: exports_external.literal("http-diff"),
|
|
23799
|
+
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."),
|
|
23800
|
+
method: exports_external.enum(["GET", "POST"]).default("GET"),
|
|
23801
|
+
headers: exports_external.record(exports_external.string()).optional(),
|
|
23802
|
+
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."),
|
|
23803
|
+
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
23804
|
+
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
23805
|
+
});
|
|
23806
|
+
TelegramReactionsPollSchema = exports_external.object({
|
|
23807
|
+
type: exports_external.literal("telegram-reactions"),
|
|
23808
|
+
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)."),
|
|
23809
|
+
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\u200d\uD83D\uDCBB)."),
|
|
23810
|
+
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
23811
|
+
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
23812
|
+
});
|
|
23813
|
+
PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
23814
|
+
HttpDiffPollSchema,
|
|
23815
|
+
TelegramReactionsPollSchema
|
|
23816
|
+
]);
|
|
23797
23817
|
ScheduleEntrySchema = exports_external.object({
|
|
23798
23818
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
23799
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
|
|
23800
|
-
|
|
23819
|
+
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
23820
|
+
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."),
|
|
23821
|
+
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
23822
|
+
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."),
|
|
23823
|
+
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."),
|
|
23801
23824
|
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."),
|
|
23802
23825
|
topic: exports_external.union([
|
|
23803
23826
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
23804
23827
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
23805
23828
|
]).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.")
|
|
23829
|
+
}).superRefine((entry, ctx) => {
|
|
23830
|
+
const kind = entry.kind ?? "prompt";
|
|
23831
|
+
if (kind === "poll" && !entry.poll) {
|
|
23832
|
+
ctx.addIssue({
|
|
23833
|
+
code: exports_external.ZodIssueCode.custom,
|
|
23834
|
+
path: ["poll"],
|
|
23835
|
+
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
23836
|
+
});
|
|
23837
|
+
}
|
|
23838
|
+
if (kind === "prompt" && entry.poll) {
|
|
23839
|
+
ctx.addIssue({
|
|
23840
|
+
code: exports_external.ZodIssueCode.custom,
|
|
23841
|
+
path: ["poll"],
|
|
23842
|
+
message: "`poll` is only valid when kind: poll."
|
|
23843
|
+
});
|
|
23844
|
+
}
|
|
23806
23845
|
});
|
|
23807
23846
|
AgentSoulSchema = exports_external.object({
|
|
23808
23847
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -24249,6 +24288,13 @@ var init_schema = __esm(() => {
|
|
|
24249
24288
|
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."),
|
|
24250
24289
|
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.")
|
|
24251
24290
|
});
|
|
24291
|
+
CronEgressSchema = exports_external.object({
|
|
24292
|
+
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."),
|
|
24293
|
+
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.")
|
|
24294
|
+
});
|
|
24295
|
+
CronConfigSchema = exports_external.object({
|
|
24296
|
+
egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
|
|
24297
|
+
});
|
|
24252
24298
|
SwitchroomConfigSchema = exports_external.object({
|
|
24253
24299
|
switchroom: exports_external.object({
|
|
24254
24300
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -24297,7 +24343,8 @@ var init_schema = __esm(() => {
|
|
|
24297
24343
|
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."),
|
|
24298
24344
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
24299
24345
|
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)"
|
|
24300
|
-
}), AgentSchema).describe("Map of agent name to agent configuration")
|
|
24346
|
+
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
24347
|
+
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.")
|
|
24301
24348
|
});
|
|
24302
24349
|
});
|
|
24303
24350
|
|
|
@@ -47522,6 +47569,18 @@ function createPendingInboundBuffer(opts = {}) {
|
|
|
47522
47569
|
};
|
|
47523
47570
|
}
|
|
47524
47571
|
|
|
47572
|
+
// gateway/cron-session.ts
|
|
47573
|
+
var CRON_IDENTITY_SUFFIX = "-cron";
|
|
47574
|
+
function cronIdentity(agent) {
|
|
47575
|
+
return `${agent}${CRON_IDENTITY_SUFFIX}`;
|
|
47576
|
+
}
|
|
47577
|
+
function isCronIdentity(name) {
|
|
47578
|
+
return typeof name === "string" && name.endsWith(CRON_IDENTITY_SUFFIX);
|
|
47579
|
+
}
|
|
47580
|
+
function resolveInjectTarget(agentName3, meta) {
|
|
47581
|
+
return meta?.session === "cron" ? cronIdentity(agentName3) : agentName3;
|
|
47582
|
+
}
|
|
47583
|
+
|
|
47525
47584
|
// gateway/obligation-ledger.ts
|
|
47526
47585
|
class ObligationLedger {
|
|
47527
47586
|
maxRepresents;
|
|
@@ -52900,11 +52959,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
52900
52959
|
}
|
|
52901
52960
|
|
|
52902
52961
|
// ../src/build-info.ts
|
|
52903
|
-
var VERSION = "0.14.
|
|
52904
|
-
var COMMIT_SHA = "
|
|
52905
|
-
var COMMIT_DATE = "2026-06-
|
|
52906
|
-
var LATEST_PR =
|
|
52907
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
52962
|
+
var VERSION = "0.14.92";
|
|
52963
|
+
var COMMIT_SHA = "cd0b9973";
|
|
52964
|
+
var COMMIT_DATE = "2026-06-10T06:33:22+10:00";
|
|
52965
|
+
var LATEST_PR = null;
|
|
52966
|
+
var COMMITS_AHEAD_OF_TAG = 6;
|
|
52908
52967
|
|
|
52909
52968
|
// gateway/boot-version.ts
|
|
52910
52969
|
function formatRelativeAgo(iso) {
|
|
@@ -53545,6 +53604,21 @@ function resolveWorkerFeedDispatch(sub, watcherDescription) {
|
|
|
53545
53604
|
};
|
|
53546
53605
|
}
|
|
53547
53606
|
|
|
53607
|
+
// gateway/subagent-status-surface.ts
|
|
53608
|
+
function resolveSubagentStatusSurface(input) {
|
|
53609
|
+
if (!input.isBackground) {
|
|
53610
|
+
if (input.liveTurnPresent)
|
|
53611
|
+
return "nest";
|
|
53612
|
+
if (!input.orphanStatusEnabled)
|
|
53613
|
+
return "skip";
|
|
53614
|
+
return input.workerFeedEnabled ? "worker-feed" : "skip";
|
|
53615
|
+
}
|
|
53616
|
+
return input.workerFeedEnabled ? "worker-feed" : "legacy-relay";
|
|
53617
|
+
}
|
|
53618
|
+
function isOrphanSubagentStatusEnabled(envVal) {
|
|
53619
|
+
return envVal !== "0";
|
|
53620
|
+
}
|
|
53621
|
+
|
|
53548
53622
|
// gateway/resolve-calling-subagent.ts
|
|
53549
53623
|
function resolveCallingSubagent(opts) {
|
|
53550
53624
|
if (opts.db == null)
|
|
@@ -55703,6 +55777,16 @@ var ipcServer = createIpcServer({
|
|
|
55703
55777
|
onClientRegistered(client3) {
|
|
55704
55778
|
process.stderr.write(`telegram gateway: bridge registered \u2014 agent=${client3.agentName}
|
|
55705
55779
|
`);
|
|
55780
|
+
if (isCronIdentity(client3.agentName)) {
|
|
55781
|
+
client3.send({ type: "status", status: "agent_connected" });
|
|
55782
|
+
const pending2 = pendingInboundBuffer.drain(client3.agentName ?? "");
|
|
55783
|
+
for (const m of pending2) {
|
|
55784
|
+
try {
|
|
55785
|
+
client3.send(m);
|
|
55786
|
+
} catch {}
|
|
55787
|
+
}
|
|
55788
|
+
return;
|
|
55789
|
+
}
|
|
55706
55790
|
const bridgeUpEffects = client3.agentName != null ? shadowEmit({ kind: "bridgeUp", at: Date.now() }) : [];
|
|
55707
55791
|
client3.send({ type: "status", status: "agent_connected" });
|
|
55708
55792
|
if (client3.agentName != null) {
|
|
@@ -55825,6 +55909,11 @@ var ipcServer = createIpcServer({
|
|
|
55825
55909
|
}
|
|
55826
55910
|
},
|
|
55827
55911
|
onClientDisconnected(client3) {
|
|
55912
|
+
if (isCronIdentity(client3.agentName)) {
|
|
55913
|
+
process.stderr.write(`telegram gateway: cron-session bridge disconnected \u2014 agent=${client3.agentName}
|
|
55914
|
+
`);
|
|
55915
|
+
return;
|
|
55916
|
+
}
|
|
55828
55917
|
if (client3.agentName != null) {
|
|
55829
55918
|
process.stderr.write(`telegram gateway: bridge disconnected \u2014 agent=${client3.agentName}
|
|
55830
55919
|
`);
|
|
@@ -55872,7 +55961,9 @@ var ipcServer = createIpcServer({
|
|
|
55872
55961
|
};
|
|
55873
55962
|
}
|
|
55874
55963
|
},
|
|
55875
|
-
onSessionEvent(
|
|
55964
|
+
onSessionEvent(client3, msg) {
|
|
55965
|
+
if (isCronIdentity(client3.agentName))
|
|
55966
|
+
return;
|
|
55876
55967
|
if (msg.activeFile)
|
|
55877
55968
|
lastSessionActiveFile = msg.activeFile;
|
|
55878
55969
|
const ev = msg.event;
|
|
@@ -55972,7 +56063,9 @@ var ipcServer = createIpcServer({
|
|
|
55972
56063
|
firstSeenAt: new Date
|
|
55973
56064
|
});
|
|
55974
56065
|
},
|
|
55975
|
-
onPtyPartial(
|
|
56066
|
+
onPtyPartial(client3, msg) {
|
|
56067
|
+
if (isCronIdentity(client3.agentName))
|
|
56068
|
+
return;
|
|
55976
56069
|
handlePtyPartial(msg.text);
|
|
55977
56070
|
},
|
|
55978
56071
|
async onRequestDriveApproval(client3, msg) {
|
|
@@ -56197,13 +56290,15 @@ var ipcServer = createIpcServer({
|
|
|
56197
56290
|
onInjectInbound(_client, msg) {
|
|
56198
56291
|
const promptKey = typeof msg.inbound.meta?.prompt_key === "string" ? msg.inbound.meta.prompt_key : "unknown";
|
|
56199
56292
|
const source = typeof msg.inbound.meta?.source === "string" ? msg.inbound.meta.source : "unknown";
|
|
56200
|
-
const
|
|
56201
|
-
|
|
56293
|
+
const target = resolveInjectTarget(msg.agentName, msg.inbound.meta);
|
|
56294
|
+
const toCron = target !== msg.agentName;
|
|
56295
|
+
const delivered = ipcServer.sendToAgent(target, msg.inbound);
|
|
56296
|
+
if (delivered && !toCron)
|
|
56202
56297
|
markClaudeBusyForInbound(msg.inbound);
|
|
56203
|
-
process.stderr.write(`telegram gateway: inject_inbound agent=${msg.agentName} source=${source} prompt_key=${promptKey} delivered=${delivered}
|
|
56298
|
+
process.stderr.write(`telegram gateway: inject_inbound agent=${msg.agentName} target=${target} source=${source} prompt_key=${promptKey} delivered=${delivered}
|
|
56204
56299
|
`);
|
|
56205
56300
|
if (!delivered) {
|
|
56206
|
-
pendingInboundBuffer.push(
|
|
56301
|
+
pendingInboundBuffer.push(target, msg.inbound);
|
|
56207
56302
|
}
|
|
56208
56303
|
},
|
|
56209
56304
|
onQuotaWallDetected(_client, msg) {
|
|
@@ -64569,6 +64664,7 @@ var didOneTimeSetup = false;
|
|
|
64569
64664
|
if (watcherAgentDir != null) {
|
|
64570
64665
|
const workerFeedEnabled = isWorkerActivityFeedEnabled(process.env.SWITCHROOM_WORKER_ACTIVITY_FEED);
|
|
64571
64666
|
const foregroundNestingEnabled = process.env.SWITCHROOM_FOREGROUND_SUBAGENT_NESTING !== "0";
|
|
64667
|
+
const orphanStatusEnabled = isOrphanSubagentStatusEnabled(process.env.SWITCHROOM_ORPHAN_SUBAGENT_STATUS);
|
|
64572
64668
|
const workerActivityFeed = createWorkerActivityFeed({
|
|
64573
64669
|
bot: {
|
|
64574
64670
|
sendMessage: async (cid, text, sendOpts) => {
|
|
@@ -64650,6 +64746,22 @@ var didOneTimeSetup = false;
|
|
|
64650
64746
|
}
|
|
64651
64747
|
}
|
|
64652
64748
|
}
|
|
64749
|
+
return;
|
|
64750
|
+
}
|
|
64751
|
+
if (resolveSubagentStatusSurface({
|
|
64752
|
+
isBackground: false,
|
|
64753
|
+
liveTurnPresent: false,
|
|
64754
|
+
workerFeedEnabled,
|
|
64755
|
+
orphanStatusEnabled
|
|
64756
|
+
}) === "worker-feed") {
|
|
64757
|
+
workerActivityFeed.finish(agentId, {
|
|
64758
|
+
description: dispatch.feedDescription,
|
|
64759
|
+
lastTool: null,
|
|
64760
|
+
toolCount,
|
|
64761
|
+
latestSummary: resultText,
|
|
64762
|
+
elapsedMs: durationMs,
|
|
64763
|
+
state: outcome === "failed" ? "failed" : "done"
|
|
64764
|
+
});
|
|
64653
64765
|
}
|
|
64654
64766
|
return;
|
|
64655
64767
|
}
|
|
@@ -64716,6 +64828,26 @@ var didOneTimeSetup = false;
|
|
|
64716
64828
|
}
|
|
64717
64829
|
const isBackground = dispatch.isBackground;
|
|
64718
64830
|
if (!isBackground) {
|
|
64831
|
+
const surface = resolveSubagentStatusSurface({
|
|
64832
|
+
isBackground: false,
|
|
64833
|
+
liveTurnPresent: currentTurn != null,
|
|
64834
|
+
workerFeedEnabled,
|
|
64835
|
+
orphanStatusEnabled
|
|
64836
|
+
});
|
|
64837
|
+
if (surface === "worker-feed") {
|
|
64838
|
+
const origin = resolveSubagentOriginChat(agentId);
|
|
64839
|
+
workerActivityFeed.update(agentId, origin?.chatId || fleetChatId || (loadAccess().allowFrom[0] ?? ""), {
|
|
64840
|
+
description: dispatch.feedDescription,
|
|
64841
|
+
lastTool,
|
|
64842
|
+
toolCount,
|
|
64843
|
+
latestSummary,
|
|
64844
|
+
elapsedMs,
|
|
64845
|
+
state: "running"
|
|
64846
|
+
}, origin?.threadId);
|
|
64847
|
+
return;
|
|
64848
|
+
}
|
|
64849
|
+
if (surface !== "nest")
|
|
64850
|
+
return;
|
|
64719
64851
|
const turn = currentTurn;
|
|
64720
64852
|
if (turn == null)
|
|
64721
64853
|
return;
|