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
|
@@ -10969,15 +10969,54 @@ var AgentBindMountSchema = exports_external.object({
|
|
|
10969
10969
|
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)."),
|
|
10970
10970
|
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'.")
|
|
10971
10971
|
});
|
|
10972
|
+
var HttpDiffPollSchema = exports_external.object({
|
|
10973
|
+
type: exports_external.literal("http-diff"),
|
|
10974
|
+
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."),
|
|
10975
|
+
method: exports_external.enum(["GET", "POST"]).default("GET"),
|
|
10976
|
+
headers: exports_external.record(exports_external.string()).optional(),
|
|
10977
|
+
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."),
|
|
10978
|
+
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
10979
|
+
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
10980
|
+
});
|
|
10981
|
+
var TelegramReactionsPollSchema = exports_external.object({
|
|
10982
|
+
type: exports_external.literal("telegram-reactions"),
|
|
10983
|
+
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)."),
|
|
10984
|
+
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\uD83D\uDCBB)."),
|
|
10985
|
+
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
10986
|
+
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
10987
|
+
});
|
|
10988
|
+
var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
10989
|
+
HttpDiffPollSchema,
|
|
10990
|
+
TelegramReactionsPollSchema
|
|
10991
|
+
]);
|
|
10972
10992
|
var ScheduleEntrySchema = exports_external.object({
|
|
10973
10993
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
10974
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
|
|
10975
|
-
|
|
10994
|
+
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
10995
|
+
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."),
|
|
10996
|
+
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
10997
|
+
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."),
|
|
10998
|
+
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."),
|
|
10976
10999
|
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."),
|
|
10977
11000
|
topic: exports_external.union([
|
|
10978
11001
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
10979
11002
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
10980
11003
|
]).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.")
|
|
11004
|
+
}).superRefine((entry, ctx) => {
|
|
11005
|
+
const kind = entry.kind ?? "prompt";
|
|
11006
|
+
if (kind === "poll" && !entry.poll) {
|
|
11007
|
+
ctx.addIssue({
|
|
11008
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11009
|
+
path: ["poll"],
|
|
11010
|
+
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11011
|
+
});
|
|
11012
|
+
}
|
|
11013
|
+
if (kind === "prompt" && entry.poll) {
|
|
11014
|
+
ctx.addIssue({
|
|
11015
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11016
|
+
path: ["poll"],
|
|
11017
|
+
message: "`poll` is only valid when kind: poll."
|
|
11018
|
+
});
|
|
11019
|
+
}
|
|
10981
11020
|
});
|
|
10982
11021
|
var AgentSoulSchema = exports_external.object({
|
|
10983
11022
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -11424,6 +11463,13 @@ var HostdConfigSchema = exports_external.object({
|
|
|
11424
11463
|
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."),
|
|
11425
11464
|
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.")
|
|
11426
11465
|
});
|
|
11466
|
+
var CronEgressSchema = exports_external.object({
|
|
11467
|
+
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."),
|
|
11468
|
+
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.")
|
|
11469
|
+
});
|
|
11470
|
+
var CronConfigSchema = exports_external.object({
|
|
11471
|
+
egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
|
|
11472
|
+
});
|
|
11427
11473
|
var SwitchroomConfigSchema = exports_external.object({
|
|
11428
11474
|
switchroom: exports_external.object({
|
|
11429
11475
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -11472,7 +11518,8 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11472
11518
|
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."),
|
|
11473
11519
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
11474
11520
|
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)"
|
|
11475
|
-
}), AgentSchema).describe("Map of agent name to agent configuration")
|
|
11521
|
+
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
11522
|
+
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.")
|
|
11476
11523
|
});
|
|
11477
11524
|
|
|
11478
11525
|
// src/config/paths.ts
|
|
@@ -11717,15 +11717,54 @@ var AgentBindMountSchema = exports_external.object({
|
|
|
11717
11717
|
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)."),
|
|
11718
11718
|
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'.")
|
|
11719
11719
|
});
|
|
11720
|
+
var HttpDiffPollSchema = exports_external.object({
|
|
11721
|
+
type: exports_external.literal("http-diff"),
|
|
11722
|
+
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."),
|
|
11723
|
+
method: exports_external.enum(["GET", "POST"]).default("GET"),
|
|
11724
|
+
headers: exports_external.record(exports_external.string()).optional(),
|
|
11725
|
+
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."),
|
|
11726
|
+
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
11727
|
+
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
11728
|
+
});
|
|
11729
|
+
var TelegramReactionsPollSchema = exports_external.object({
|
|
11730
|
+
type: exports_external.literal("telegram-reactions"),
|
|
11731
|
+
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)."),
|
|
11732
|
+
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\u200d\uD83D\uDCBB)."),
|
|
11733
|
+
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
11734
|
+
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
11735
|
+
});
|
|
11736
|
+
var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11737
|
+
HttpDiffPollSchema,
|
|
11738
|
+
TelegramReactionsPollSchema
|
|
11739
|
+
]);
|
|
11720
11740
|
var ScheduleEntrySchema = exports_external.object({
|
|
11721
11741
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
11722
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
|
|
11723
|
-
|
|
11742
|
+
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
11743
|
+
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."),
|
|
11744
|
+
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11745
|
+
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."),
|
|
11746
|
+
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."),
|
|
11724
11747
|
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."),
|
|
11725
11748
|
topic: exports_external.union([
|
|
11726
11749
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
11727
11750
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
11728
11751
|
]).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.")
|
|
11752
|
+
}).superRefine((entry, ctx) => {
|
|
11753
|
+
const kind = entry.kind ?? "prompt";
|
|
11754
|
+
if (kind === "poll" && !entry.poll) {
|
|
11755
|
+
ctx.addIssue({
|
|
11756
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11757
|
+
path: ["poll"],
|
|
11758
|
+
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11759
|
+
});
|
|
11760
|
+
}
|
|
11761
|
+
if (kind === "prompt" && entry.poll) {
|
|
11762
|
+
ctx.addIssue({
|
|
11763
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11764
|
+
path: ["poll"],
|
|
11765
|
+
message: "`poll` is only valid when kind: poll."
|
|
11766
|
+
});
|
|
11767
|
+
}
|
|
11729
11768
|
});
|
|
11730
11769
|
var AgentSoulSchema = exports_external.object({
|
|
11731
11770
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -12172,6 +12211,13 @@ var HostdConfigSchema = exports_external.object({
|
|
|
12172
12211
|
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."),
|
|
12173
12212
|
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.")
|
|
12174
12213
|
});
|
|
12214
|
+
var CronEgressSchema = exports_external.object({
|
|
12215
|
+
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."),
|
|
12216
|
+
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.")
|
|
12217
|
+
});
|
|
12218
|
+
var CronConfigSchema = exports_external.object({
|
|
12219
|
+
egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
|
|
12220
|
+
});
|
|
12175
12221
|
var SwitchroomConfigSchema = exports_external.object({
|
|
12176
12222
|
switchroom: exports_external.object({
|
|
12177
12223
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -12220,7 +12266,8 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
12220
12266
|
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."),
|
|
12221
12267
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
12222
12268
|
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)"
|
|
12223
|
-
}), AgentSchema).describe("Map of agent name to agent configuration")
|
|
12269
|
+
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
12270
|
+
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.")
|
|
12224
12271
|
});
|
|
12225
12272
|
|
|
12226
12273
|
// src/config/paths.ts
|