switchroom 0.15.11 → 0.15.12
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 +5 -0
- package/dist/auth-broker/index.js +5 -0
- package/dist/cli/notion-write-pretool.mjs +5 -0
- package/dist/cli/switchroom.js +290 -181
- package/dist/host-control/main.js +5 -0
- package/dist/vault/approvals/kernel-server.js +5 -0
- package/dist/vault/broker/server.js +5 -0
- package/package.json +1 -1
- package/telegram-plugin/bridge/bridge.ts +24 -0
- package/telegram-plugin/dist/bridge/bridge.js +23 -0
- package/telegram-plugin/dist/gateway/gateway.js +200 -6
- package/telegram-plugin/dist/server.js +23 -0
- package/telegram-plugin/gateway/gateway.ts +8 -0
- package/telegram-plugin/gateway/linear-activity.ts +160 -0
- package/telegram-plugin/gateway/model-command.ts +13 -5
- package/telegram-plugin/tests/gateway-request-secret.test.ts +1 -1
- package/telegram-plugin/tests/linear-agent-activity.test.ts +124 -0
- package/telegram-plugin/tests/model-command.test.ts +40 -0
|
@@ -13882,6 +13882,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
13882
13882
|
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
|
|
13883
13883
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
13884
13884
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
13885
|
+
linear_agent: exports_external.object({
|
|
13886
|
+
enabled: exports_external.boolean(),
|
|
13887
|
+
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.")
|
|
13889
|
+
}).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."),
|
|
13885
13890
|
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."),
|
|
13886
13891
|
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`."),
|
|
13887
13892
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
|
@@ -11480,6 +11480,11 @@ var init_schema = __esm(() => {
|
|
|
11480
11480
|
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
|
|
11481
11481
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
11482
11482
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11483
|
+
linear_agent: exports_external.object({
|
|
11484
|
+
enabled: exports_external.boolean(),
|
|
11485
|
+
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.")
|
|
11487
|
+
}).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."),
|
|
11483
11488
|
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."),
|
|
11484
11489
|
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`."),
|
|
11485
11490
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
|
@@ -11480,6 +11480,11 @@ var init_schema = __esm(() => {
|
|
|
11480
11480
|
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
|
|
11481
11481
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
11482
11482
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11483
|
+
linear_agent: exports_external.object({
|
|
11484
|
+
enabled: exports_external.boolean(),
|
|
11485
|
+
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.")
|
|
11487
|
+
}).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."),
|
|
11483
11488
|
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."),
|
|
11484
11489
|
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`."),
|
|
11485
11490
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
package/package.json
CHANGED
|
@@ -455,6 +455,30 @@ const TOOL_SCHEMAS = [
|
|
|
455
455
|
required: ['chat_id', 'key'],
|
|
456
456
|
},
|
|
457
457
|
},
|
|
458
|
+
{
|
|
459
|
+
name: 'linear_agent_activity',
|
|
460
|
+
description:
|
|
461
|
+
'Emit a structured Linear AgentActivity against an agent session (#2298). Use this ONLY inside a turn that was woken by a Linear agent session (the inbound carries meta.source="linear" and meta.agent_session_id) — pass that agent_session_id back here. Linear renders activities as status chips + a timeline on the issue, so the human sees acknowledge → work → result. Emit a `thought` within ~10s of being woken so the session does not look dead, then `message`(s) as you make progress, and finally exactly one terminal `complete` (work done) or `error` (you could not proceed). body is required for thought/message/error and optional for complete. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns an error instructing you to vault_request_access for `linear/<agent>/token`.',
|
|
462
|
+
inputSchema: {
|
|
463
|
+
type: 'object',
|
|
464
|
+
properties: {
|
|
465
|
+
agent_session_id: {
|
|
466
|
+
type: 'string',
|
|
467
|
+
description: 'The Linear AgentSession id — copy it verbatim from the woken turn\'s meta.agent_session_id.',
|
|
468
|
+
},
|
|
469
|
+
type: {
|
|
470
|
+
type: 'string',
|
|
471
|
+
enum: ['thought', 'message', 'complete', 'error'],
|
|
472
|
+
description: 'Activity kind. thought = visible reasoning ack (emit within ~10s); message = progress update; complete = terminal success; error = terminal failure.',
|
|
473
|
+
},
|
|
474
|
+
body: {
|
|
475
|
+
type: 'string',
|
|
476
|
+
description: 'Activity text (Markdown). Required for thought/message/error; optional for complete (a closing summary).',
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
required: ['agent_session_id', 'type'],
|
|
480
|
+
},
|
|
481
|
+
},
|
|
458
482
|
]
|
|
459
483
|
|
|
460
484
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }))
|
|
@@ -24934,6 +24934,29 @@ var TOOL_SCHEMAS = [
|
|
|
24934
24934
|
},
|
|
24935
24935
|
required: ["chat_id", "key"]
|
|
24936
24936
|
}
|
|
24937
|
+
},
|
|
24938
|
+
{
|
|
24939
|
+
name: "linear_agent_activity",
|
|
24940
|
+
description: 'Emit a structured Linear AgentActivity against an agent session (#2298). Use this ONLY inside a turn that was woken by a Linear agent session (the inbound carries meta.source="linear" and meta.agent_session_id) \u2014 pass that agent_session_id back here. Linear renders activities as status chips + a timeline on the issue, so the human sees acknowledge \u2192 work \u2192 result. Emit a `thought` within ~10s of being woken so the session does not look dead, then `message`(s) as you make progress, and finally exactly one terminal `complete` (work done) or `error` (you could not proceed). body is required for thought/message/error and optional for complete. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns an error instructing you to vault_request_access for `linear/<agent>/token`.',
|
|
24941
|
+
inputSchema: {
|
|
24942
|
+
type: "object",
|
|
24943
|
+
properties: {
|
|
24944
|
+
agent_session_id: {
|
|
24945
|
+
type: "string",
|
|
24946
|
+
description: "The Linear AgentSession id \u2014 copy it verbatim from the woken turn's meta.agent_session_id."
|
|
24947
|
+
},
|
|
24948
|
+
type: {
|
|
24949
|
+
type: "string",
|
|
24950
|
+
enum: ["thought", "message", "complete", "error"],
|
|
24951
|
+
description: "Activity kind. thought = visible reasoning ack (emit within ~10s); message = progress update; complete = terminal success; error = terminal failure."
|
|
24952
|
+
},
|
|
24953
|
+
body: {
|
|
24954
|
+
type: "string",
|
|
24955
|
+
description: "Activity text (Markdown). Required for thought/message/error; optional for complete (a closing summary)."
|
|
24956
|
+
}
|
|
24957
|
+
},
|
|
24958
|
+
required: ["agent_session_id", "type"]
|
|
24959
|
+
}
|
|
24937
24960
|
}
|
|
24938
24961
|
];
|
|
24939
24962
|
mcp.setRequestHandler(ListToolsRequestSchema2, async () => ({ tools: TOOL_SCHEMAS }));
|
|
@@ -23993,6 +23993,11 @@ var init_schema = __esm(() => {
|
|
|
23993
23993
|
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
|
|
23994
23994
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
23995
23995
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
23996
|
+
linear_agent: exports_external.object({
|
|
23997
|
+
enabled: exports_external.boolean(),
|
|
23998
|
+
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."),
|
|
23999
|
+
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational \u2014 used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
|
|
24000
|
+
}).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 \u2014 opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
|
|
23996
24001
|
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 \u2014 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."),
|
|
23997
24002
|
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 \u2014 set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
23998
24003
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
|
|
@@ -44920,7 +44925,7 @@ function labelTag(label) {
|
|
|
44920
44925
|
}
|
|
44921
44926
|
|
|
44922
44927
|
// gateway/model-command.ts
|
|
44923
|
-
var MODEL_ALIASES = ["opus", "sonnet", "haiku", "default"];
|
|
44928
|
+
var MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
|
|
44924
44929
|
var MODEL_ARG_RE = /^[A-Za-z0-9][A-Za-z0-9._\[\]-]{0,99}$/;
|
|
44925
44930
|
function isValidModelArg(arg) {
|
|
44926
44931
|
return MODEL_ARG_RE.test(arg);
|
|
@@ -47026,6 +47031,53 @@ function injectWebhookInbound(agent, prompt, ctx, deps = {}) {
|
|
|
47026
47031
|
`)).catch((err) => log(`webhook-dispatch: agent='${agent}' inject failed: ${String(err)}
|
|
47027
47032
|
`));
|
|
47028
47033
|
}
|
|
47034
|
+
function parseLinearAgentSession(payload) {
|
|
47035
|
+
const type = String(payload.type ?? "").toLowerCase();
|
|
47036
|
+
if (type !== "agentsessionevent")
|
|
47037
|
+
return null;
|
|
47038
|
+
const action = String(payload.action ?? "").toLowerCase();
|
|
47039
|
+
const session = payload.agentSession ?? payload.agent_session ?? {};
|
|
47040
|
+
const sessionId = String(session.id ?? payload.agentSessionId ?? payload.agent_session_id ?? "");
|
|
47041
|
+
if (!sessionId)
|
|
47042
|
+
return null;
|
|
47043
|
+
const explicitTrigger = String(session.trigger ?? payload.trigger ?? "").toLowerCase();
|
|
47044
|
+
const comment = session.comment;
|
|
47045
|
+
const trigger = explicitTrigger === "mention" || explicitTrigger === "delegation" ? explicitTrigger : comment ? "mention" : "delegation";
|
|
47046
|
+
const promptContext = typeof payload.promptContext === "string" ? payload.promptContext : typeof session.promptContext === "string" ? session.promptContext : undefined;
|
|
47047
|
+
const issue = session.issue ?? {};
|
|
47048
|
+
const issueId = String(issue.identifier ?? "");
|
|
47049
|
+
const issueTitle = String(issue.title ?? "");
|
|
47050
|
+
const commentBody = typeof comment?.body === "string" ? comment.body : "";
|
|
47051
|
+
const summaryParts = [];
|
|
47052
|
+
summaryParts.push(trigger === "mention" ? `You were @mentioned in Linear` : `An issue was delegated to you in Linear`);
|
|
47053
|
+
if (issueId || issueTitle) {
|
|
47054
|
+
summaryParts.push(`Issue ${issueId}${issueTitle ? `: ${issueTitle}` : ""}`.trim());
|
|
47055
|
+
}
|
|
47056
|
+
if (commentBody)
|
|
47057
|
+
summaryParts.push(commentBody);
|
|
47058
|
+
const summary = summaryParts.join(`
|
|
47059
|
+
`);
|
|
47060
|
+
return { sessionId, trigger, action, promptContext, summary };
|
|
47061
|
+
}
|
|
47062
|
+
function buildLinearInbound(session, ctx, now) {
|
|
47063
|
+
const content = session.promptContext && session.promptContext.trim().length > 0 ? session.promptContext : session.summary;
|
|
47064
|
+
return {
|
|
47065
|
+
type: "inbound",
|
|
47066
|
+
chatId: ctx.chatId,
|
|
47067
|
+
...ctx.threadId !== undefined ? { threadId: ctx.threadId } : {},
|
|
47068
|
+
messageId: now,
|
|
47069
|
+
user: "linear",
|
|
47070
|
+
userId: 0,
|
|
47071
|
+
ts: now,
|
|
47072
|
+
text: content,
|
|
47073
|
+
meta: {
|
|
47074
|
+
source: "linear",
|
|
47075
|
+
agent_session_id: session.sessionId,
|
|
47076
|
+
linear_trigger: session.trigger,
|
|
47077
|
+
linear_event: `agent_session.${session.action || "event"}`
|
|
47078
|
+
}
|
|
47079
|
+
};
|
|
47080
|
+
}
|
|
47029
47081
|
function evaluateDispatch(args, deps = {}) {
|
|
47030
47082
|
const log = deps.log ?? ((s) => process.stderr.write(s));
|
|
47031
47083
|
const now = (deps.now ?? Date.now)();
|
|
@@ -47137,6 +47189,38 @@ function recordWebhookEvent(rec, deps = {}) {
|
|
|
47137
47189
|
}
|
|
47138
47190
|
log(`webhook-gateway: agent='${agent}' source='${rec.source}' event='${rec.event_type}' recorded ts=${now}
|
|
47139
47191
|
`);
|
|
47192
|
+
if (rec.source === "linear") {
|
|
47193
|
+
try {
|
|
47194
|
+
const session = parseLinearAgentSession(rec.payload);
|
|
47195
|
+
if (session) {
|
|
47196
|
+
const config = (deps.loadConfig ?? loadConfig)();
|
|
47197
|
+
const rawAgent = config.agents?.[agent];
|
|
47198
|
+
const linearAgent = rawAgent ? resolveAgentConfig(config.defaults, config.profiles, rawAgent).channels?.telegram?.linear_agent : undefined;
|
|
47199
|
+
if (!linearAgent?.enabled) {
|
|
47200
|
+
log(`linear-agent: agent='${agent}' session='${session.sessionId}' ignored \u2014 linear_agent not enabled
|
|
47201
|
+
`);
|
|
47202
|
+
} else {
|
|
47203
|
+
const target = resolveChannelTarget(config, agent);
|
|
47204
|
+
if (!target) {
|
|
47205
|
+
log(`linear-agent: agent='${agent}' session='${session.sessionId}' skipped \u2014 no chat target (forum_chat_id / chat_id unset)
|
|
47206
|
+
`);
|
|
47207
|
+
} else {
|
|
47208
|
+
const inbound = buildLinearInbound(session, {
|
|
47209
|
+
chatId: target.chatId,
|
|
47210
|
+
...target.threadId !== undefined ? { threadId: target.threadId } : {}
|
|
47211
|
+
}, now);
|
|
47212
|
+
const ok = deps.inject ? deps.inject(agent, inbound) : false;
|
|
47213
|
+
log(`linear-agent: agent='${agent}' session='${session.sessionId}' trigger='${session.trigger}' ` + `${ok ? "injected" : "NOT injected (no inject sink)"}
|
|
47214
|
+
`);
|
|
47215
|
+
return { status: "ok", ts: now, dispatched: ok ? 1 : 0 };
|
|
47216
|
+
}
|
|
47217
|
+
}
|
|
47218
|
+
}
|
|
47219
|
+
} catch (err) {
|
|
47220
|
+
log(`linear-agent: agent='${agent}' session inject error (event recorded): ${err.message}
|
|
47221
|
+
`);
|
|
47222
|
+
}
|
|
47223
|
+
}
|
|
47140
47224
|
let dispatched = 0;
|
|
47141
47225
|
try {
|
|
47142
47226
|
const config = (deps.loadConfig ?? loadConfig)();
|
|
@@ -53793,10 +53877,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
53793
53877
|
}
|
|
53794
53878
|
|
|
53795
53879
|
// ../src/build-info.ts
|
|
53796
|
-
var VERSION = "0.15.
|
|
53797
|
-
var COMMIT_SHA = "
|
|
53798
|
-
var COMMIT_DATE = "2026-06-
|
|
53799
|
-
var LATEST_PR =
|
|
53880
|
+
var VERSION = "0.15.12";
|
|
53881
|
+
var COMMIT_SHA = "18b7b6e6";
|
|
53882
|
+
var COMMIT_DATE = "2026-06-13T04:55:14Z";
|
|
53883
|
+
var LATEST_PR = 2311;
|
|
53800
53884
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
53801
53885
|
|
|
53802
53886
|
// gateway/boot-version.ts
|
|
@@ -54087,6 +54171,110 @@ async function revokeGrantViaBroker(id, opts) {
|
|
|
54087
54171
|
return { kind: "error", msg: "unexpected broker response" };
|
|
54088
54172
|
}
|
|
54089
54173
|
|
|
54174
|
+
// gateway/linear-activity.ts
|
|
54175
|
+
init_client2();
|
|
54176
|
+
var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
|
|
54177
|
+
async function defaultResolveLinearToken(agent) {
|
|
54178
|
+
const key = `linear/${agent}/token`;
|
|
54179
|
+
const token = readVaultTokenFile(agent) ?? undefined;
|
|
54180
|
+
const result = await getViaBrokerStructured(key, token ? { token } : {});
|
|
54181
|
+
if (result.kind === "ok" && result.entry.kind === "string") {
|
|
54182
|
+
return { ok: true, token: result.entry.value };
|
|
54183
|
+
}
|
|
54184
|
+
if (result.kind === "unreachable")
|
|
54185
|
+
return { ok: false, reason: "unreachable" };
|
|
54186
|
+
if (result.kind === "not_found")
|
|
54187
|
+
return { ok: false, reason: "not_found" };
|
|
54188
|
+
if (result.kind === "denied")
|
|
54189
|
+
return { ok: false, reason: "denied" };
|
|
54190
|
+
return { ok: false, reason: "unknown" };
|
|
54191
|
+
}
|
|
54192
|
+
async function emitLinearAgentActivity(args, deps = {}) {
|
|
54193
|
+
const log = deps.log ?? ((s) => process.stderr.write(s));
|
|
54194
|
+
const sessionId = args.agent_session_id;
|
|
54195
|
+
if (!sessionId)
|
|
54196
|
+
throw new Error("linear_agent_activity: agent_session_id is required");
|
|
54197
|
+
const type = args.type;
|
|
54198
|
+
if (!type || !["thought", "message", "complete", "error"].includes(type)) {
|
|
54199
|
+
throw new Error("linear_agent_activity: type must be one of thought|message|complete|error");
|
|
54200
|
+
}
|
|
54201
|
+
const body = args.body;
|
|
54202
|
+
if (type !== "complete" && (body == null || body === "")) {
|
|
54203
|
+
throw new Error(`linear_agent_activity: body is required for type='${type}'`);
|
|
54204
|
+
}
|
|
54205
|
+
const agent = deps.agent ?? process.env.SWITCHROOM_AGENT_NAME ?? "-";
|
|
54206
|
+
const resolveToken = deps.resolveToken ?? defaultResolveLinearToken;
|
|
54207
|
+
const tokenResult = await resolveToken(agent);
|
|
54208
|
+
if (!tokenResult.ok) {
|
|
54209
|
+
if (tokenResult.reason === "denied" || tokenResult.reason === "not_found") {
|
|
54210
|
+
return {
|
|
54211
|
+
content: [
|
|
54212
|
+
{
|
|
54213
|
+
type: "text",
|
|
54214
|
+
text: `linear_agent_activity failed: no Linear token (vault ${tokenResult.reason}). ` + `Call vault_request_access for key 'linear/${agent}/token' (scope read), then retry.`
|
|
54215
|
+
}
|
|
54216
|
+
]
|
|
54217
|
+
};
|
|
54218
|
+
}
|
|
54219
|
+
return {
|
|
54220
|
+
content: [
|
|
54221
|
+
{
|
|
54222
|
+
type: "text",
|
|
54223
|
+
text: `linear_agent_activity failed: vault broker ${tokenResult.reason} resolving 'linear/${agent}/token'.`
|
|
54224
|
+
}
|
|
54225
|
+
]
|
|
54226
|
+
};
|
|
54227
|
+
}
|
|
54228
|
+
const content = { type };
|
|
54229
|
+
if (body != null && body !== "")
|
|
54230
|
+
content.body = body;
|
|
54231
|
+
const mutation = "mutation AgentActivityCreate($input: AgentActivityCreateInput!) { " + "agentActivityCreate(input: $input) { success agentActivity { id } } }";
|
|
54232
|
+
const variables = { input: { agentSessionId: sessionId, content } };
|
|
54233
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
54234
|
+
let resp;
|
|
54235
|
+
try {
|
|
54236
|
+
resp = await fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
|
|
54237
|
+
method: "POST",
|
|
54238
|
+
headers: {
|
|
54239
|
+
"Content-Type": "application/json",
|
|
54240
|
+
Authorization: tokenResult.token
|
|
54241
|
+
},
|
|
54242
|
+
body: JSON.stringify({ query: mutation, variables })
|
|
54243
|
+
});
|
|
54244
|
+
} catch (err) {
|
|
54245
|
+
return {
|
|
54246
|
+
content: [{ type: "text", text: `linear_agent_activity failed: request error: ${err.message}` }]
|
|
54247
|
+
};
|
|
54248
|
+
}
|
|
54249
|
+
if (!resp.ok) {
|
|
54250
|
+
const txt = await resp.text().catch(() => "");
|
|
54251
|
+
return {
|
|
54252
|
+
content: [
|
|
54253
|
+
{ type: "text", text: `linear_agent_activity failed: Linear API ${resp.status}${txt ? ` \u2014 ${txt.slice(0, 200)}` : ""}` }
|
|
54254
|
+
]
|
|
54255
|
+
};
|
|
54256
|
+
}
|
|
54257
|
+
let json;
|
|
54258
|
+
try {
|
|
54259
|
+
json = await resp.json();
|
|
54260
|
+
} catch {
|
|
54261
|
+
return { content: [{ type: "text", text: "linear_agent_activity failed: malformed Linear API response" }] };
|
|
54262
|
+
}
|
|
54263
|
+
if (json.errors && json.errors.length > 0) {
|
|
54264
|
+
return {
|
|
54265
|
+
content: [
|
|
54266
|
+
{ type: "text", text: `linear_agent_activity failed: ${json.errors.map((e) => e.message ?? "error").join("; ").slice(0, 300)}` }
|
|
54267
|
+
]
|
|
54268
|
+
};
|
|
54269
|
+
}
|
|
54270
|
+
if (json.data?.agentActivityCreate?.success === false) {
|
|
54271
|
+
return { content: [{ type: "text", text: "linear_agent_activity failed: Linear reported success=false" }] };
|
|
54272
|
+
}
|
|
54273
|
+
log(`telegram gateway: linear_agent_activity: emitted type=${type} session=${sessionId} agent=${agent}
|
|
54274
|
+
`);
|
|
54275
|
+
return { content: [{ type: "text", text: `Linear ${type} emitted on session ${sessionId}` }] };
|
|
54276
|
+
}
|
|
54277
|
+
|
|
54090
54278
|
// vault-approval-posture.ts
|
|
54091
54279
|
function resolveVaultApprovalPosture(broker) {
|
|
54092
54280
|
if (broker?.approvalAuth === "telegram-id") {
|
|
@@ -57294,7 +57482,8 @@ var ALLOWED_TOOLS = new Set([
|
|
|
57294
57482
|
"send_gif",
|
|
57295
57483
|
"vault_request_save",
|
|
57296
57484
|
"vault_request_access",
|
|
57297
|
-
"request_secret"
|
|
57485
|
+
"request_secret",
|
|
57486
|
+
"linear_agent_activity"
|
|
57298
57487
|
]);
|
|
57299
57488
|
async function executeToolCall(tool, args) {
|
|
57300
57489
|
if (!ALLOWED_TOOLS.has(tool)) {
|
|
@@ -57339,6 +57528,8 @@ async function executeToolCall(tool, args) {
|
|
|
57339
57528
|
return executeVaultRequestAccess(args);
|
|
57340
57529
|
case "request_secret":
|
|
57341
57530
|
return executeRequestSecret(args);
|
|
57531
|
+
case "linear_agent_activity":
|
|
57532
|
+
return executeLinearAgentActivity(args);
|
|
57342
57533
|
default:
|
|
57343
57534
|
throw new Error(`unknown tool: ${tool}`);
|
|
57344
57535
|
}
|
|
@@ -57369,6 +57560,9 @@ async function executeSendChecklist(args) {
|
|
|
57369
57560
|
`);
|
|
57370
57561
|
return { content: [{ type: "text", text: `checklist sent (id: ${sent.message_id})` }] };
|
|
57371
57562
|
}
|
|
57563
|
+
async function executeLinearAgentActivity(args) {
|
|
57564
|
+
return emitLinearAgentActivity(args);
|
|
57565
|
+
}
|
|
57372
57566
|
async function executeUpdateChecklist(args) {
|
|
57373
57567
|
const chat_id = args.chat_id;
|
|
57374
57568
|
if (!chat_id)
|
|
@@ -24631,6 +24631,29 @@ var init_bridge = __esm(async () => {
|
|
|
24631
24631
|
},
|
|
24632
24632
|
required: ["chat_id", "key"]
|
|
24633
24633
|
}
|
|
24634
|
+
},
|
|
24635
|
+
{
|
|
24636
|
+
name: "linear_agent_activity",
|
|
24637
|
+
description: 'Emit a structured Linear AgentActivity against an agent session (#2298). Use this ONLY inside a turn that was woken by a Linear agent session (the inbound carries meta.source="linear" and meta.agent_session_id) \u2014 pass that agent_session_id back here. Linear renders activities as status chips + a timeline on the issue, so the human sees acknowledge \u2192 work \u2192 result. Emit a `thought` within ~10s of being woken so the session does not look dead, then `message`(s) as you make progress, and finally exactly one terminal `complete` (work done) or `error` (you could not proceed). body is required for thought/message/error and optional for complete. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns an error instructing you to vault_request_access for `linear/<agent>/token`.',
|
|
24638
|
+
inputSchema: {
|
|
24639
|
+
type: "object",
|
|
24640
|
+
properties: {
|
|
24641
|
+
agent_session_id: {
|
|
24642
|
+
type: "string",
|
|
24643
|
+
description: "The Linear AgentSession id \u2014 copy it verbatim from the woken turn's meta.agent_session_id."
|
|
24644
|
+
},
|
|
24645
|
+
type: {
|
|
24646
|
+
type: "string",
|
|
24647
|
+
enum: ["thought", "message", "complete", "error"],
|
|
24648
|
+
description: "Activity kind. thought = visible reasoning ack (emit within ~10s); message = progress update; complete = terminal success; error = terminal failure."
|
|
24649
|
+
},
|
|
24650
|
+
body: {
|
|
24651
|
+
type: "string",
|
|
24652
|
+
description: "Activity text (Markdown). Required for thought/message/error; optional for complete (a closing summary)."
|
|
24653
|
+
}
|
|
24654
|
+
},
|
|
24655
|
+
required: ["agent_session_id", "type"]
|
|
24656
|
+
}
|
|
24634
24657
|
}
|
|
24635
24658
|
];
|
|
24636
24659
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
|
|
@@ -467,6 +467,7 @@ import {
|
|
|
467
467
|
listGrantsViaBroker,
|
|
468
468
|
revokeGrantViaBroker,
|
|
469
469
|
} from '../../src/vault/broker/client.js'
|
|
470
|
+
import { emitLinearAgentActivity } from './linear-activity.js'
|
|
470
471
|
import {
|
|
471
472
|
approvalRequest,
|
|
472
473
|
approvalConsume,
|
|
@@ -6718,6 +6719,7 @@ const ALLOWED_TOOLS = new Set([
|
|
|
6718
6719
|
'vault_request_save',
|
|
6719
6720
|
'vault_request_access',
|
|
6720
6721
|
'request_secret',
|
|
6722
|
+
'linear_agent_activity',
|
|
6721
6723
|
])
|
|
6722
6724
|
|
|
6723
6725
|
async function executeToolCall(tool: string, args: Record<string, unknown>): Promise<unknown> {
|
|
@@ -6763,6 +6765,8 @@ async function executeToolCall(tool: string, args: Record<string, unknown>): Pro
|
|
|
6763
6765
|
return executeVaultRequestAccess(args)
|
|
6764
6766
|
case 'request_secret':
|
|
6765
6767
|
return executeRequestSecret(args)
|
|
6768
|
+
case 'linear_agent_activity':
|
|
6769
|
+
return executeLinearAgentActivity(args)
|
|
6766
6770
|
default:
|
|
6767
6771
|
throw new Error(`unknown tool: ${tool}`)
|
|
6768
6772
|
}
|
|
@@ -6794,6 +6798,10 @@ async function executeSendChecklist(args: Record<string, unknown>): Promise<{ co
|
|
|
6794
6798
|
return { content: [{ type: 'text', text: `checklist sent (id: ${sent.message_id})` }] }
|
|
6795
6799
|
}
|
|
6796
6800
|
|
|
6801
|
+
async function executeLinearAgentActivity(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
6802
|
+
return emitLinearAgentActivity(args)
|
|
6803
|
+
}
|
|
6804
|
+
|
|
6797
6805
|
async function executeUpdateChecklist(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
6798
6806
|
const chat_id = args.chat_id as string
|
|
6799
6807
|
if (!chat_id) throw new Error('update_checklist: chat_id is required')
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear AgentActivity emission (#2298).
|
|
3
|
+
*
|
|
4
|
+
* The `linear_agent_activity` MCP tool lets an agent that was woken by a
|
|
5
|
+
* Linear agent session respond with structured activities (thought /
|
|
6
|
+
* message / complete / error) that Linear renders as status chips + a
|
|
7
|
+
* timeline on the issue. This module owns the pure logic — token
|
|
8
|
+
* resolution + the `agentActivityCreate` GraphQL POST — behind injectable
|
|
9
|
+
* deps so it is testable without a vault broker or the network. The
|
|
10
|
+
* gateway wires it into `executeToolCall`.
|
|
11
|
+
*
|
|
12
|
+
* The agent's Linear app token is resolved from the vault under
|
|
13
|
+
* `linear/<agent>/token` via the broker (never an inline literal, never an
|
|
14
|
+
* env file). On a vault denial the tool returns actionable text telling the
|
|
15
|
+
* agent to `vault_request_access` for that key rather than failing opaquely.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
getViaBrokerStructured,
|
|
20
|
+
readVaultTokenFile,
|
|
21
|
+
} from '../../src/vault/broker/client.js'
|
|
22
|
+
|
|
23
|
+
export const LINEAR_GRAPHQL_ENDPOINT = 'https://api.linear.app/graphql'
|
|
24
|
+
|
|
25
|
+
export type LinearTokenResult =
|
|
26
|
+
| { ok: true; token: string }
|
|
27
|
+
| { ok: false; reason: 'denied' | 'unreachable' | 'not_found' | 'unknown' }
|
|
28
|
+
|
|
29
|
+
export interface LinearActivityDeps {
|
|
30
|
+
/** Resolve the Linear app token for `agent` from the vault. */
|
|
31
|
+
resolveToken?: (agent: string) => Promise<LinearTokenResult>
|
|
32
|
+
/** Injectable fetch (tests). */
|
|
33
|
+
fetchImpl?: typeof fetch
|
|
34
|
+
/** Agent slug (defaults to SWITCHROOM_AGENT_NAME). */
|
|
35
|
+
agent?: string
|
|
36
|
+
/** Log sink — stderr in production. */
|
|
37
|
+
log?: (line: string) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ToolTextResult = { content: Array<{ type: string; text: string }> }
|
|
41
|
+
|
|
42
|
+
/** Default token resolver: vault broker get on `linear/<agent>/token`. */
|
|
43
|
+
export async function defaultResolveLinearToken(agent: string): Promise<LinearTokenResult> {
|
|
44
|
+
const key = `linear/${agent}/token`
|
|
45
|
+
const token = readVaultTokenFile(agent) ?? undefined
|
|
46
|
+
const result = await getViaBrokerStructured(key, token ? { token } : {})
|
|
47
|
+
if (result.kind === 'ok' && result.entry.kind === 'string') {
|
|
48
|
+
return { ok: true, token: result.entry.value }
|
|
49
|
+
}
|
|
50
|
+
if (result.kind === 'unreachable') return { ok: false, reason: 'unreachable' }
|
|
51
|
+
if (result.kind === 'not_found') return { ok: false, reason: 'not_found' }
|
|
52
|
+
if (result.kind === 'denied') return { ok: false, reason: 'denied' }
|
|
53
|
+
return { ok: false, reason: 'unknown' }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Emit a Linear AgentActivity. Validates args, resolves the token, POSTs
|
|
58
|
+
* the `agentActivityCreate` mutation, and returns an MCP text result. Never
|
|
59
|
+
* throws on a vault/network failure — returns actionable error text so the
|
|
60
|
+
* agent can recover (e.g. via vault_request_access).
|
|
61
|
+
*/
|
|
62
|
+
export async function emitLinearAgentActivity(
|
|
63
|
+
args: Record<string, unknown>,
|
|
64
|
+
deps: LinearActivityDeps = {},
|
|
65
|
+
): Promise<ToolTextResult> {
|
|
66
|
+
const log = deps.log ?? ((s) => process.stderr.write(s))
|
|
67
|
+
|
|
68
|
+
const sessionId = args.agent_session_id as string | undefined
|
|
69
|
+
if (!sessionId) throw new Error('linear_agent_activity: agent_session_id is required')
|
|
70
|
+
const type = args.type as string | undefined
|
|
71
|
+
if (!type || !['thought', 'message', 'complete', 'error'].includes(type)) {
|
|
72
|
+
throw new Error('linear_agent_activity: type must be one of thought|message|complete|error')
|
|
73
|
+
}
|
|
74
|
+
const body = args.body as string | undefined
|
|
75
|
+
if (type !== 'complete' && (body == null || body === '')) {
|
|
76
|
+
throw new Error(`linear_agent_activity: body is required for type='${type}'`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const agent = deps.agent ?? process.env.SWITCHROOM_AGENT_NAME ?? '-'
|
|
80
|
+
const resolveToken = deps.resolveToken ?? defaultResolveLinearToken
|
|
81
|
+
const tokenResult = await resolveToken(agent)
|
|
82
|
+
if (!tokenResult.ok) {
|
|
83
|
+
if (tokenResult.reason === 'denied' || tokenResult.reason === 'not_found') {
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text:
|
|
89
|
+
`linear_agent_activity failed: no Linear token (vault ${tokenResult.reason}). ` +
|
|
90
|
+
`Call vault_request_access for key 'linear/${agent}/token' (scope read), then retry.`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: 'text',
|
|
99
|
+
text: `linear_agent_activity failed: vault broker ${tokenResult.reason} resolving 'linear/${agent}/token'.`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// AgentActivity content discriminated by type. thought/message/error carry
|
|
106
|
+
// a body; complete is terminal with an optional summary body.
|
|
107
|
+
const content: Record<string, unknown> = { type }
|
|
108
|
+
if (body != null && body !== '') content.body = body
|
|
109
|
+
|
|
110
|
+
const mutation =
|
|
111
|
+
'mutation AgentActivityCreate($input: AgentActivityCreateInput!) { ' +
|
|
112
|
+
'agentActivityCreate(input: $input) { success agentActivity { id } } }'
|
|
113
|
+
const variables = { input: { agentSessionId: sessionId, content } }
|
|
114
|
+
|
|
115
|
+
const fetchImpl = deps.fetchImpl ?? fetch
|
|
116
|
+
let resp: Response
|
|
117
|
+
try {
|
|
118
|
+
resp = await fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: {
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
Authorization: tokenResult.token,
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({ query: mutation, variables }),
|
|
125
|
+
})
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: 'text', text: `linear_agent_activity failed: request error: ${(err as Error).message}` }],
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!resp.ok) {
|
|
133
|
+
const txt = await resp.text().catch(() => '')
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{ type: 'text', text: `linear_agent_activity failed: Linear API ${resp.status}${txt ? ` — ${txt.slice(0, 200)}` : ''}` },
|
|
137
|
+
],
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let json: { data?: { agentActivityCreate?: { success?: boolean } }; errors?: Array<{ message?: string }> }
|
|
142
|
+
try {
|
|
143
|
+
json = (await resp.json()) as typeof json
|
|
144
|
+
} catch {
|
|
145
|
+
return { content: [{ type: 'text', text: 'linear_agent_activity failed: malformed Linear API response' }] }
|
|
146
|
+
}
|
|
147
|
+
if (json.errors && json.errors.length > 0) {
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{ type: 'text', text: `linear_agent_activity failed: ${json.errors.map((e) => e.message ?? 'error').join('; ').slice(0, 300)}` },
|
|
151
|
+
],
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (json.data?.agentActivityCreate?.success === false) {
|
|
155
|
+
return { content: [{ type: 'text', text: 'linear_agent_activity failed: Linear reported success=false' }] }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
log(`telegram gateway: linear_agent_activity: emitted type=${type} session=${sessionId} agent=${agent}\n`)
|
|
159
|
+
return { content: [{ type: 'text', text: `Linear ${type} emitted on session ${sessionId}` }] }
|
|
160
|
+
}
|