switchroom 0.15.12 → 0.15.14
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 +324 -14
- package/dist/auth-broker/index.js +61 -4
- package/dist/cli/notion-write-pretool.mjs +61 -4
- package/dist/cli/switchroom.js +402 -113
- package/dist/host-control/main.js +61 -4
- package/dist/vault/approvals/kernel-server.js +62 -5
- package/dist/vault/broker/server.js +62 -5
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +30 -13
- package/profiles/_shared/agent-self-service.md.hbs +37 -0
- package/telegram-plugin/bridge/bridge.ts +38 -1
- package/telegram-plugin/dist/bridge/bridge.js +31 -1
- package/telegram-plugin/dist/gateway/gateway.js +536 -53
- package/telegram-plugin/dist/server.js +31 -1
- package/telegram-plugin/gateway/gateway.ts +169 -6
- package/telegram-plugin/gateway/ipc-protocol.ts +31 -1
- package/telegram-plugin/gateway/ipc-server.ts +29 -0
- package/telegram-plugin/gateway/linear-activity.ts +145 -0
- package/telegram-plugin/runtime-metrics.ts +14 -0
- package/telegram-plugin/scoped-approval.ts +253 -0
- package/telegram-plugin/tests/bridge-liveness-override.test.ts +21 -0
- package/telegram-plugin/tests/ipc-server-validate-send-outbound.test.ts +54 -0
- package/telegram-plugin/tests/linear-agent-activity.test.ts +1 -1
- package/telegram-plugin/tests/linear-create-issue.test.ts +211 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +13 -0
- package/telegram-plugin/tests/runtime-metrics.test.ts +9 -0
- package/telegram-plugin/tests/scoped-approval.test.ts +254 -0
- package/telegram-plugin/tests/send-outbound-wiring.test.ts +63 -0
|
@@ -13724,11 +13724,29 @@ var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
|
13724
13724
|
HttpDiffPollSchema,
|
|
13725
13725
|
TelegramReactionsPollSchema
|
|
13726
13726
|
]);
|
|
13727
|
+
var TelegramMessageActionSchema = exports_external.object({
|
|
13728
|
+
type: exports_external.literal("telegram-message"),
|
|
13729
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable — fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output — the text is fully determined by config."),
|
|
13730
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
13731
|
+
});
|
|
13732
|
+
var WebhookActionSchema = exports_external.object({
|
|
13733
|
+
type: exports_external.literal("webhook"),
|
|
13734
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (§6.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
13735
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
13736
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
13737
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
13738
|
+
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 webhook may inject into headers/body via {{name}}. 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 action cannot exfil it elsewhere.")
|
|
13739
|
+
});
|
|
13740
|
+
var ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
13741
|
+
TelegramMessageActionSchema,
|
|
13742
|
+
WebhookActionSchema
|
|
13743
|
+
]);
|
|
13727
13744
|
var ScheduleEntrySchema = exports_external.object({
|
|
13728
13745
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
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.
|
|
13746
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
13747
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).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. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
13731
13748
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
13749
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
13732
13750
|
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
13751
|
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."),
|
|
13734
13752
|
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."),
|
|
@@ -13745,13 +13763,41 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
13745
13763
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
13746
13764
|
});
|
|
13747
13765
|
}
|
|
13748
|
-
if (kind
|
|
13766
|
+
if (kind !== "poll" && entry.poll) {
|
|
13749
13767
|
ctx.addIssue({
|
|
13750
13768
|
code: exports_external.ZodIssueCode.custom,
|
|
13751
13769
|
path: ["poll"],
|
|
13752
13770
|
message: "`poll` is only valid when kind: poll."
|
|
13753
13771
|
});
|
|
13754
13772
|
}
|
|
13773
|
+
if (kind === "action" && !entry.action) {
|
|
13774
|
+
ctx.addIssue({
|
|
13775
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13776
|
+
path: ["action"],
|
|
13777
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
13778
|
+
});
|
|
13779
|
+
}
|
|
13780
|
+
if (kind !== "action" && entry.action) {
|
|
13781
|
+
ctx.addIssue({
|
|
13782
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13783
|
+
path: ["action"],
|
|
13784
|
+
message: "`action` is only valid when kind: action."
|
|
13785
|
+
});
|
|
13786
|
+
}
|
|
13787
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
13788
|
+
ctx.addIssue({
|
|
13789
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13790
|
+
path: ["prompt"],
|
|
13791
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
13792
|
+
});
|
|
13793
|
+
}
|
|
13794
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
13795
|
+
ctx.addIssue({
|
|
13796
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13797
|
+
path: ["prompt"],
|
|
13798
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
13799
|
+
});
|
|
13800
|
+
}
|
|
13755
13801
|
});
|
|
13756
13802
|
var AgentSoulSchema = exports_external.object({
|
|
13757
13803
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -13885,7 +13931,8 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
13885
13931
|
linear_agent: exports_external.object({
|
|
13886
13932
|
enabled: exports_external.boolean(),
|
|
13887
13933
|
token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
|
|
13888
|
-
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
|
|
13934
|
+
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace."),
|
|
13935
|
+
default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
|
|
13889
13936
|
}).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
|
|
13890
13937
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
13891
13938
|
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
@@ -14769,6 +14816,16 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
14769
14816
|
}
|
|
14770
14817
|
merged.reaction_dispatch = combined;
|
|
14771
14818
|
}
|
|
14819
|
+
const linearEnabled = merged.channels?.telegram?.linear_agent?.enabled === true;
|
|
14820
|
+
if (linearEnabled) {
|
|
14821
|
+
const rd = merged.reaction_dispatch;
|
|
14822
|
+
if (!rd || rd.emojis === undefined) {
|
|
14823
|
+
merged.reaction_dispatch = {
|
|
14824
|
+
enabled: rd?.enabled ?? true,
|
|
14825
|
+
emojis: ["\uD83D\uDC68\uD83D\uDCBB", "\uD83D\uDCCC"]
|
|
14826
|
+
};
|
|
14827
|
+
}
|
|
14828
|
+
}
|
|
14772
14829
|
if (defaults.resources || merged.resources) {
|
|
14773
14830
|
const d = defaults.resources ?? {};
|
|
14774
14831
|
const a = merged.resources ?? {};
|
|
@@ -4305,6 +4305,16 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
4305
4305
|
}
|
|
4306
4306
|
merged.reaction_dispatch = combined;
|
|
4307
4307
|
}
|
|
4308
|
+
const linearEnabled = merged.channels?.telegram?.linear_agent?.enabled === true;
|
|
4309
|
+
if (linearEnabled) {
|
|
4310
|
+
const rd = merged.reaction_dispatch;
|
|
4311
|
+
if (!rd || rd.emojis === undefined) {
|
|
4312
|
+
merged.reaction_dispatch = {
|
|
4313
|
+
enabled: rd?.enabled ?? true,
|
|
4314
|
+
emojis: ["\uD83D\uDC68\uD83D\uDCBB", "\uD83D\uDCCC"]
|
|
4315
|
+
};
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4308
4318
|
if (defaults.resources || merged.resources) {
|
|
4309
4319
|
const d = defaults.resources ?? {};
|
|
4310
4320
|
const a = merged.resources ?? {};
|
|
@@ -11289,7 +11299,7 @@ var init_dist = __esm(() => {
|
|
|
11289
11299
|
});
|
|
11290
11300
|
|
|
11291
11301
|
// src/config/schema.ts
|
|
11292
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11302
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11293
11303
|
var init_schema = __esm(() => {
|
|
11294
11304
|
init_zod();
|
|
11295
11305
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -11322,11 +11332,29 @@ var init_schema = __esm(() => {
|
|
|
11322
11332
|
HttpDiffPollSchema,
|
|
11323
11333
|
TelegramReactionsPollSchema
|
|
11324
11334
|
]);
|
|
11335
|
+
TelegramMessageActionSchema = exports_external.object({
|
|
11336
|
+
type: exports_external.literal("telegram-message"),
|
|
11337
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable — fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output — the text is fully determined by config."),
|
|
11338
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
11339
|
+
});
|
|
11340
|
+
WebhookActionSchema = exports_external.object({
|
|
11341
|
+
type: exports_external.literal("webhook"),
|
|
11342
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (§6.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
11343
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
11344
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
11345
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
11346
|
+
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 webhook may inject into headers/body via {{name}}. 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 action cannot exfil it elsewhere.")
|
|
11347
|
+
});
|
|
11348
|
+
ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11349
|
+
TelegramMessageActionSchema,
|
|
11350
|
+
WebhookActionSchema
|
|
11351
|
+
]);
|
|
11325
11352
|
ScheduleEntrySchema = exports_external.object({
|
|
11326
11353
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
11327
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
11328
|
-
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.
|
|
11354
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
11355
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).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. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
11329
11356
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11357
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
11330
11358
|
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."),
|
|
11331
11359
|
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."),
|
|
11332
11360
|
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."),
|
|
@@ -11343,13 +11371,41 @@ var init_schema = __esm(() => {
|
|
|
11343
11371
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11344
11372
|
});
|
|
11345
11373
|
}
|
|
11346
|
-
if (kind
|
|
11374
|
+
if (kind !== "poll" && entry.poll) {
|
|
11347
11375
|
ctx.addIssue({
|
|
11348
11376
|
code: exports_external.ZodIssueCode.custom,
|
|
11349
11377
|
path: ["poll"],
|
|
11350
11378
|
message: "`poll` is only valid when kind: poll."
|
|
11351
11379
|
});
|
|
11352
11380
|
}
|
|
11381
|
+
if (kind === "action" && !entry.action) {
|
|
11382
|
+
ctx.addIssue({
|
|
11383
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11384
|
+
path: ["action"],
|
|
11385
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
11386
|
+
});
|
|
11387
|
+
}
|
|
11388
|
+
if (kind !== "action" && entry.action) {
|
|
11389
|
+
ctx.addIssue({
|
|
11390
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11391
|
+
path: ["action"],
|
|
11392
|
+
message: "`action` is only valid when kind: action."
|
|
11393
|
+
});
|
|
11394
|
+
}
|
|
11395
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
11396
|
+
ctx.addIssue({
|
|
11397
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11398
|
+
path: ["prompt"],
|
|
11399
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
11400
|
+
});
|
|
11401
|
+
}
|
|
11402
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
11403
|
+
ctx.addIssue({
|
|
11404
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11405
|
+
path: ["prompt"],
|
|
11406
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
11407
|
+
});
|
|
11408
|
+
}
|
|
11353
11409
|
});
|
|
11354
11410
|
AgentSoulSchema = exports_external.object({
|
|
11355
11411
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -11483,7 +11539,8 @@ var init_schema = __esm(() => {
|
|
|
11483
11539
|
linear_agent: exports_external.object({
|
|
11484
11540
|
enabled: exports_external.boolean(),
|
|
11485
11541
|
token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
|
|
11486
|
-
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
|
|
11542
|
+
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace."),
|
|
11543
|
+
default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
|
|
11487
11544
|
}).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
|
|
11488
11545
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
11489
11546
|
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
@@ -339,6 +339,16 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
339
339
|
}
|
|
340
340
|
merged.reaction_dispatch = combined;
|
|
341
341
|
}
|
|
342
|
+
const linearEnabled = merged.channels?.telegram?.linear_agent?.enabled === true;
|
|
343
|
+
if (linearEnabled) {
|
|
344
|
+
const rd = merged.reaction_dispatch;
|
|
345
|
+
if (!rd || rd.emojis === undefined) {
|
|
346
|
+
merged.reaction_dispatch = {
|
|
347
|
+
enabled: rd?.enabled ?? true,
|
|
348
|
+
emojis: ["\uD83D\uDC68\uD83D\uDCBB", "\uD83D\uDCCC"]
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
342
352
|
if (defaults.resources || merged.resources) {
|
|
343
353
|
const d = defaults.resources ?? {};
|
|
344
354
|
const a = merged.resources ?? {};
|
|
@@ -11289,7 +11299,7 @@ var init_zod = __esm(() => {
|
|
|
11289
11299
|
});
|
|
11290
11300
|
|
|
11291
11301
|
// src/config/schema.ts
|
|
11292
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11302
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11293
11303
|
var init_schema = __esm(() => {
|
|
11294
11304
|
init_zod();
|
|
11295
11305
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -11322,11 +11332,29 @@ var init_schema = __esm(() => {
|
|
|
11322
11332
|
HttpDiffPollSchema,
|
|
11323
11333
|
TelegramReactionsPollSchema
|
|
11324
11334
|
]);
|
|
11335
|
+
TelegramMessageActionSchema = exports_external.object({
|
|
11336
|
+
type: exports_external.literal("telegram-message"),
|
|
11337
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable — fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output — the text is fully determined by config."),
|
|
11338
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
11339
|
+
});
|
|
11340
|
+
WebhookActionSchema = exports_external.object({
|
|
11341
|
+
type: exports_external.literal("webhook"),
|
|
11342
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (§6.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
11343
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
11344
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
11345
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
11346
|
+
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 webhook may inject into headers/body via {{name}}. 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 action cannot exfil it elsewhere.")
|
|
11347
|
+
});
|
|
11348
|
+
ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11349
|
+
TelegramMessageActionSchema,
|
|
11350
|
+
WebhookActionSchema
|
|
11351
|
+
]);
|
|
11325
11352
|
ScheduleEntrySchema = exports_external.object({
|
|
11326
11353
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
11327
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
11328
|
-
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.
|
|
11354
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
11355
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).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. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
11329
11356
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11357
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
11330
11358
|
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."),
|
|
11331
11359
|
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."),
|
|
11332
11360
|
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."),
|
|
@@ -11343,13 +11371,41 @@ var init_schema = __esm(() => {
|
|
|
11343
11371
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11344
11372
|
});
|
|
11345
11373
|
}
|
|
11346
|
-
if (kind
|
|
11374
|
+
if (kind !== "poll" && entry.poll) {
|
|
11347
11375
|
ctx.addIssue({
|
|
11348
11376
|
code: exports_external.ZodIssueCode.custom,
|
|
11349
11377
|
path: ["poll"],
|
|
11350
11378
|
message: "`poll` is only valid when kind: poll."
|
|
11351
11379
|
});
|
|
11352
11380
|
}
|
|
11381
|
+
if (kind === "action" && !entry.action) {
|
|
11382
|
+
ctx.addIssue({
|
|
11383
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11384
|
+
path: ["action"],
|
|
11385
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
11386
|
+
});
|
|
11387
|
+
}
|
|
11388
|
+
if (kind !== "action" && entry.action) {
|
|
11389
|
+
ctx.addIssue({
|
|
11390
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11391
|
+
path: ["action"],
|
|
11392
|
+
message: "`action` is only valid when kind: action."
|
|
11393
|
+
});
|
|
11394
|
+
}
|
|
11395
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
11396
|
+
ctx.addIssue({
|
|
11397
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11398
|
+
path: ["prompt"],
|
|
11399
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
11400
|
+
});
|
|
11401
|
+
}
|
|
11402
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
11403
|
+
ctx.addIssue({
|
|
11404
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11405
|
+
path: ["prompt"],
|
|
11406
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
11407
|
+
});
|
|
11408
|
+
}
|
|
11353
11409
|
});
|
|
11354
11410
|
AgentSoulSchema = exports_external.object({
|
|
11355
11411
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -11483,7 +11539,8 @@ var init_schema = __esm(() => {
|
|
|
11483
11539
|
linear_agent: exports_external.object({
|
|
11484
11540
|
enabled: exports_external.boolean(),
|
|
11485
11541
|
token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
|
|
11486
|
-
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
|
|
11542
|
+
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace."),
|
|
11543
|
+
default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
|
|
11487
11544
|
}).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
|
|
11488
11545
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
11489
11546
|
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
package/package.json
CHANGED
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
# container, dedicated to cheap cron fires. It registers to the SAME gateway
|
|
6
6
|
# as a DISTINCT bridge identity `<name>-cron` (start.sh's gateway sidecar
|
|
7
7
|
# routes meta.session=cron fires here and gates all status surfaces off this
|
|
8
|
-
# identity — see telegram-plugin/gateway/cron-session.ts).
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# identity — see telegram-plugin/gateway/cron-session.ts).
|
|
9
|
+
#
|
|
10
|
+
# Tier-1 (#2307) is "a fresh conversation that STILL has the agent's memory +
|
|
11
|
+
# tools, on a cheaper model." It loads the agent's FULL .mcp.json (its own
|
|
12
|
+
# .claude-cron/.mcp.json — same hindsight bank baked from the BASE name, same
|
|
13
|
+
# tools) with ENABLE_TOOL_SEARCH so the schema set DEFERS (low context cost).
|
|
14
|
+
# The saving is dropping the accumulated main-session conversation context +
|
|
15
|
+
# the cheaper model — NOT lobotomising the session. Its CLAUDE_CONFIG_DIR is
|
|
16
|
+
# separate (own transcript) so each fire starts fresh and /clear-friendly.
|
|
12
17
|
#
|
|
13
18
|
# Forked as a supervised sidecar by start.sh ONLY when {{name}} actually has a
|
|
14
19
|
# Tier-1 (context: fresh) cron entry AND SWITCHROOM_CHEAP_CRON is on — so the
|
|
@@ -35,12 +40,13 @@ esac
|
|
|
35
40
|
CRON_NAME="{{name}}-cron"
|
|
36
41
|
CRON_CONFIG_DIR="{{agentDir}}/.claude-cron"
|
|
37
42
|
MAIN_CONFIG_DIR="{{agentDir}}/.claude"
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# .mcp.json if the
|
|
43
|
+
# Cron .mcp.json (scaffold writes .claude-cron/.mcp.json with the agent's FULL
|
|
44
|
+
# filtered MCP set — same hindsight bank, same tools — but the switchroom-
|
|
45
|
+
# telegram entry carries a DISTINCT SWITCHROOM_BRIDGE_ALIVE_PATH so its liveness
|
|
46
|
+
# file can't mask the main bridge). The bridge reads SWITCHROOM_AGENT_NAME from
|
|
47
|
+
# the process env (exported below), NOT its .mcp.json env block, so it registers
|
|
48
|
+
# as the cron identity. Fall back to the main .mcp.json if the cron file is
|
|
49
|
+
# missing (older scaffold) so it still boots.
|
|
44
50
|
CRON_MCP_CONFIG="{{agentDir}}/.claude-cron/.mcp.json"
|
|
45
51
|
[ -f "$CRON_MCP_CONFIG" ] || CRON_MCP_CONFIG="{{agentDir}}/.mcp.json"
|
|
46
52
|
CRON_SOCKET="switchroom-${CRON_NAME}"
|
|
@@ -63,12 +69,23 @@ fi
|
|
|
63
69
|
export SWITCHROOM_AGENT_NAME="$CRON_NAME"
|
|
64
70
|
export CLAUDE_CONFIG_DIR="$CRON_CONFIG_DIR"
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
# Defer the (now full) MCP schema set via tool-search so the cron session keeps
|
|
73
|
+
# its low-context cost — the heavy servers load on demand; switchroom-telegram
|
|
74
|
+
# + hindsight stay alwaysLoad. Normally inherited from start.sh's env; set here
|
|
75
|
+
# as a belt-and-braces default, honouring the operator kill-switch + any value
|
|
76
|
+
# already in the env.
|
|
77
|
+
if [ "${SWITCHROOM_DISABLE_TOOL_SEARCH:-}" != "1" ]; then
|
|
78
|
+
export ENABLE_TOOL_SEARCH="${ENABLE_TOOL_SEARCH:-auto}"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
CRON_APPEND_PROMPT="You are the cheap background cron worker for {{name}}. You run scheduled tasks in a fresh, low-context conversation on a cheaper model — but you SHARE {{name}}'s memory (hindsight) and tools, so use them: recall what you need, look things up, reply with real substance. Do the scheduled task, reply once with the result, and keep it tight."
|
|
67
82
|
|
|
68
83
|
# Launch the cron claude in its OWN tmux session/socket so it never contends
|
|
69
84
|
# with the main session's pane. Interactive (no -p). --strict-mcp-config pins
|
|
70
|
-
# it to the
|
|
71
|
-
# --continue) —
|
|
85
|
+
# it to the cron .mcp.json (full set, tool-search-deferred). Fresh each boot
|
|
86
|
+
# (no --continue) — low context by construction. cd into the workspace so
|
|
87
|
+
# claude's project key matches the pre-seeded trust state (.claude-cron).
|
|
88
|
+
cd "{{agentDir}}" || exit 1
|
|
72
89
|
exec tmux -L "$CRON_SOCKET" \
|
|
73
90
|
new-session -A -s "$CRON_NAME" -x 400 -y 50 \
|
|
74
91
|
claude \
|
|
@@ -123,6 +123,43 @@ After a successful `schedule_add`, confirm to the user with:
|
|
|
123
123
|
After a failed write (any `E_*` code from the rails above), surface the
|
|
124
124
|
specific error verbatim, explain which rail tripped, and offer the
|
|
125
125
|
closest legal alternative.
|
|
126
|
+
{{#if linearAgentEnabled}}
|
|
127
|
+
|
|
128
|
+
## Capture to Linear (👨💻 / 📌 reaction → new issue)
|
|
129
|
+
|
|
130
|
+
You're connected to Linear as a first-class app actor. The operator has the
|
|
131
|
+
**capture reactions** wired: when they react to a Telegram message with **👨💻**
|
|
132
|
+
(laptop) or **📌** (pushpin), you're woken with a turn whose inbound is shaped
|
|
133
|
+
`<channel … event="reaction" emoji="👨💻" message_id="…" chat_id="…">[the
|
|
134
|
+
reacted message text]</channel>`. That reaction means: **"turn this into a
|
|
135
|
+
Linear issue."**
|
|
136
|
+
|
|
137
|
+
When you get such a reaction turn:
|
|
138
|
+
|
|
139
|
+
1. **Read the reacted message + nearby context.** The `[reacted text]` is the
|
|
140
|
+
seed. Pull the surrounding thread (`get_recent_messages`) when the ask needs
|
|
141
|
+
it — the issue should stand on its own without the operator re-explaining.
|
|
142
|
+
2. **Call `linear_create_issue`** with:
|
|
143
|
+
- `title` — a crisp, imperative one-liner ("Fix duplicate Brevo webhook
|
|
144
|
+
retries"), not a restatement of the chat.
|
|
145
|
+
- `body` — the ask, the context you gathered, and any acceptance criteria
|
|
146
|
+
you can reasonably infer. Markdown.
|
|
147
|
+
- `dedup_key` — set it to `"<chat_id>:<message_id>"` from the reaction event.
|
|
148
|
+
This makes a re-react of the same message idempotent (it returns the
|
|
149
|
+
existing issue instead of filing a duplicate).
|
|
150
|
+
- `team_id` — omit it. A single-team workspace auto-resolves; you'll only be
|
|
151
|
+
asked for one if the workspace has multiple teams (then tell the operator
|
|
152
|
+
to set a default via `switchroom linear-agent set-team`).
|
|
153
|
+
3. **Reply in plain text, not an emoji.** On success the tool returns
|
|
154
|
+
`Filed: <title> → <url>` — reply that link so the operator can click
|
|
155
|
+
through ("📋 Filed: <url>"). On failure (vault denial, multi-team, API
|
|
156
|
+
error) the tool returns actionable text — relay it plainly ("Couldn't file
|
|
157
|
+
it — …") and, if it's a vault denial, follow the `vault_request_access`
|
|
158
|
+
instruction it gives you, then retry.
|
|
159
|
+
|
|
160
|
+
Don't acknowledge with only a reaction or a bare "done" — the operator wants
|
|
161
|
+
the link (or the honest reason it didn't file).
|
|
162
|
+
{{/if}}
|
|
126
163
|
|
|
127
164
|
### Don't lie about scheduling
|
|
128
165
|
|
|
@@ -479,6 +479,37 @@ const TOOL_SCHEMAS = [
|
|
|
479
479
|
required: ['agent_session_id', 'type'],
|
|
480
480
|
},
|
|
481
481
|
},
|
|
482
|
+
{
|
|
483
|
+
name: 'linear_create_issue',
|
|
484
|
+
description:
|
|
485
|
+
'File a new Linear issue from a Telegram message the operator flagged for capture (#2312). Use this when a turn was triggered by a capture reaction (the inbound carries event="reaction" with a capture emoji like 👨💻 or 📌) — turn the reacted message + any relevant thread context into a well-formed issue. Write a crisp imperative title and a body that captures the ask, the context, and any acceptance criteria you can infer; the agent files it AS its own Linear app actor. Team is auto-resolved when the workspace has a single team; if there are multiple it returns text asking for an explicit team_id. Pass dedup_key (e.g. the chat_id:message_id of the reacted message) so a re-react of the same message does not file a duplicate. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns text instructing you to vault_request_access for `linear/<agent>/token`. Returns "Filed: <title> → <url>" on success — reply that link to the operator in plain text.',
|
|
486
|
+
inputSchema: {
|
|
487
|
+
type: 'object',
|
|
488
|
+
properties: {
|
|
489
|
+
title: {
|
|
490
|
+
type: 'string',
|
|
491
|
+
description: 'Issue title — a crisp, imperative one-liner (e.g. "Fix duplicate webhook retries on Brevo sync").',
|
|
492
|
+
},
|
|
493
|
+
body: {
|
|
494
|
+
type: 'string',
|
|
495
|
+
description: 'Issue description (Markdown). Capture the ask, relevant context from the message/thread, and any acceptance criteria you can infer.',
|
|
496
|
+
},
|
|
497
|
+
team_id: {
|
|
498
|
+
type: 'string',
|
|
499
|
+
description: 'Optional Linear team id. Omit to auto-resolve when the workspace has a single team; required only when the workspace has multiple teams.',
|
|
500
|
+
},
|
|
501
|
+
dedup_key: {
|
|
502
|
+
type: 'string',
|
|
503
|
+
description: 'Optional idempotency key (use the reacted message identity, e.g. "<chat_id>:<message_id>"). A prior capture with the same key short-circuits to "Already filed: <url>".',
|
|
504
|
+
},
|
|
505
|
+
priority: {
|
|
506
|
+
type: 'number',
|
|
507
|
+
description: 'Optional Linear priority (0 none, 1 urgent, 2 high, 3 normal, 4 low).',
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
required: ['title', 'body'],
|
|
511
|
+
},
|
|
512
|
+
},
|
|
482
513
|
]
|
|
483
514
|
|
|
484
515
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }))
|
|
@@ -796,7 +827,13 @@ async function main(): Promise<void> {
|
|
|
796
827
|
onPermission,
|
|
797
828
|
onStatus,
|
|
798
829
|
log: (msg) => process.stderr.write(`telegram bridge: ipc: ${msg}\n`),
|
|
799
|
-
|
|
830
|
+
// #2307 Tier-1: the cron-session bridge shares the agent's STATE_DIR
|
|
831
|
+
// (access.json / history / gateway.sock) but writes its liveness file to a
|
|
832
|
+
// DISTINCT path (SWITCHROOM_BRIDGE_ALIVE_PATH, set in the cron .mcp.json) so
|
|
833
|
+
// a live <agent>-cron bridge can't mask a dead MAIN bridge in the
|
|
834
|
+
// dashboard/doctor liveness probe (RISK #2). Unset ⟹ the main bridge's
|
|
835
|
+
// canonical STATE_DIR/.bridge-alive, exactly as before.
|
|
836
|
+
livenessFilePath: process.env.SWITCHROOM_BRIDGE_ALIVE_PATH ?? join(STATE_DIR, ".bridge-alive"),
|
|
800
837
|
})
|
|
801
838
|
if (ipc.isConnected()) {
|
|
802
839
|
process.stderr.write(`telegram bridge: connected to gateway at ${SOCKET_PATH}\n`)
|
|
@@ -24957,6 +24957,36 @@ var TOOL_SCHEMAS = [
|
|
|
24957
24957
|
},
|
|
24958
24958
|
required: ["agent_session_id", "type"]
|
|
24959
24959
|
}
|
|
24960
|
+
},
|
|
24961
|
+
{
|
|
24962
|
+
name: "linear_create_issue",
|
|
24963
|
+
description: 'File a new Linear issue from a Telegram message the operator flagged for capture (#2312). Use this when a turn was triggered by a capture reaction (the inbound carries event="reaction" with a capture emoji like \uD83D\uDC68\u200D\uD83D\uDCBB or \uD83D\uDCCC) \u2014 turn the reacted message + any relevant thread context into a well-formed issue. Write a crisp imperative title and a body that captures the ask, the context, and any acceptance criteria you can infer; the agent files it AS its own Linear app actor. Team is auto-resolved when the workspace has a single team; if there are multiple it returns text asking for an explicit team_id. Pass dedup_key (e.g. the chat_id:message_id of the reacted message) so a re-react of the same message does not file a duplicate. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns text instructing you to vault_request_access for `linear/<agent>/token`. Returns "Filed: <title> \u2192 <url>" on success \u2014 reply that link to the operator in plain text.',
|
|
24964
|
+
inputSchema: {
|
|
24965
|
+
type: "object",
|
|
24966
|
+
properties: {
|
|
24967
|
+
title: {
|
|
24968
|
+
type: "string",
|
|
24969
|
+
description: 'Issue title \u2014 a crisp, imperative one-liner (e.g. "Fix duplicate webhook retries on Brevo sync").'
|
|
24970
|
+
},
|
|
24971
|
+
body: {
|
|
24972
|
+
type: "string",
|
|
24973
|
+
description: "Issue description (Markdown). Capture the ask, relevant context from the message/thread, and any acceptance criteria you can infer."
|
|
24974
|
+
},
|
|
24975
|
+
team_id: {
|
|
24976
|
+
type: "string",
|
|
24977
|
+
description: "Optional Linear team id. Omit to auto-resolve when the workspace has a single team; required only when the workspace has multiple teams."
|
|
24978
|
+
},
|
|
24979
|
+
dedup_key: {
|
|
24980
|
+
type: "string",
|
|
24981
|
+
description: 'Optional idempotency key (use the reacted message identity, e.g. "<chat_id>:<message_id>"). A prior capture with the same key short-circuits to "Already filed: <url>".'
|
|
24982
|
+
},
|
|
24983
|
+
priority: {
|
|
24984
|
+
type: "number",
|
|
24985
|
+
description: "Optional Linear priority (0 none, 1 urgent, 2 high, 3 normal, 4 low)."
|
|
24986
|
+
}
|
|
24987
|
+
},
|
|
24988
|
+
required: ["title", "body"]
|
|
24989
|
+
}
|
|
24960
24990
|
}
|
|
24961
24991
|
];
|
|
24962
24992
|
mcp.setRequestHandler(ListToolsRequestSchema2, async () => ({ tools: TOOL_SCHEMAS }));
|
|
@@ -25176,7 +25206,7 @@ async function main() {
|
|
|
25176
25206
|
onStatus,
|
|
25177
25207
|
log: (msg) => process.stderr.write(`telegram bridge: ipc: ${msg}
|
|
25178
25208
|
`),
|
|
25179
|
-
livenessFilePath: join4(STATE_DIR, ".bridge-alive")
|
|
25209
|
+
livenessFilePath: process.env.SWITCHROOM_BRIDGE_ALIVE_PATH ?? join4(STATE_DIR, ".bridge-alive")
|
|
25180
25210
|
});
|
|
25181
25211
|
if (ipc.isConnected()) {
|
|
25182
25212
|
process.stderr.write(`telegram bridge: connected to gateway at ${SOCKET_PATH}
|