switchroom 0.15.36 → 0.15.38
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 +10 -9
- package/dist/auth-broker/index.js +9 -9
- package/dist/cli/autoaccept-poll.js +13 -7
- package/dist/cli/notion-write-pretool.mjs +9 -9
- package/dist/cli/switchroom.js +480 -217
- package/dist/cli/ui/index.html +87 -17
- package/dist/host-control/main.js +10 -10
- package/dist/vault/approvals/kernel-server.js +9 -9
- package/dist/vault/broker/server.js +9 -9
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +1 -1
- package/profiles/_base/start.sh.hbs +1 -1
- package/profiles/_shared/agent-self-service.md.hbs +25 -0
- package/skills/switchroom-manage/SKILL.md +1 -1
- package/skills/switchroom-runtime/SKILL.md +1 -1
- package/telegram-plugin/answer-stream.ts +1 -1
- package/telegram-plugin/bridge/bridge.ts +50 -1
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/bridge/tool-filter.ts +77 -0
- package/telegram-plugin/chat-lock.ts +1 -1
- package/telegram-plugin/credits-watch.ts +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +60 -3
- package/telegram-plugin/dist/gateway/gateway.js +753 -207
- package/telegram-plugin/dist/server.js +64 -4
- package/telegram-plugin/gateway/auto-classify-mid-turn.ts +1 -1
- package/telegram-plugin/gateway/boot-card.ts +5 -1
- package/telegram-plugin/gateway/boot-probes.ts +62 -0
- package/telegram-plugin/gateway/cron-session.ts +1 -1
- package/telegram-plugin/gateway/gateway.ts +254 -15
- package/telegram-plugin/gateway/grant-restart.ts +1 -1
- package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +1 -1
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +1 -1
- package/telegram-plugin/gateway/inbound-delivery-machine.ts +1 -1
- package/telegram-plugin/gateway/interrupt-defer.ts +1 -1
- package/telegram-plugin/gateway/ipc-protocol.ts +12 -0
- package/telegram-plugin/gateway/linear-activity.ts +56 -0
- package/telegram-plugin/gateway/linear-auth-watch.ts +102 -0
- package/telegram-plugin/gateway/linear-setup.ts +196 -0
- package/telegram-plugin/gateway/permission-card-origin.ts +62 -0
- package/telegram-plugin/gateway/permission-timeout.ts +70 -0
- package/telegram-plugin/gateway/prefix-warmup.ts +1 -1
- package/telegram-plugin/gateway/webhook-ingest-server.test.ts +1 -1
- package/telegram-plugin/gateway/webhook-ingest-server.ts +1 -1
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +1 -1
- package/telegram-plugin/interrupt-marker.ts +1 -1
- package/telegram-plugin/over-ping-safety-net.ts +1 -1
- package/telegram-plugin/scoped-approval.ts +1 -1
- package/telegram-plugin/secret-detect/vault-error.ts +1 -1
- package/telegram-plugin/silence-poke.ts +2 -2
- package/telegram-plugin/silent-reply-anchor.ts +1 -1
- package/telegram-plugin/slot-banner-driver.ts +1 -1
- package/telegram-plugin/startup-reset.ts +1 -1
- package/telegram-plugin/tests/boot-probes-connections.test.ts +66 -0
- package/telegram-plugin/tests/gateway-startup-reset.test.ts +1 -1
- package/telegram-plugin/tests/inbound-delivery-machine.test.ts +1 -1
- package/telegram-plugin/tests/linear-agent-activity.test.ts +77 -0
- package/telegram-plugin/tests/linear-agent-setup.test.ts +132 -0
- package/telegram-plugin/tests/linear-auth-watch.test.ts +79 -0
- package/telegram-plugin/tests/linear-create-issue.test.ts +3 -1
- package/telegram-plugin/tests/permission-card-origin.test.ts +97 -0
- package/telegram-plugin/tests/permission-card-routing.test.ts +23 -0
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +76 -0
- package/telegram-plugin/tests/permission-timeout.test.ts +87 -0
- package/telegram-plugin/tests/scoped-approval.test.ts +1 -1
- package/telegram-plugin/tests/silence-poke.test.ts +1 -1
- package/telegram-plugin/tests/tool-filter.test.ts +87 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +1 -1
- package/telegram-plugin/turn-flush-safety.ts +1 -1
- package/telegram-plugin/uat/assertions.ts +1 -1
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-fast-ack-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-fast-trivial-dm.test.ts +2 -2
- package/telegram-plugin/uat/scenarios/jtbd-forwarded-burst-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-memory-survives-restart-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-reflective-status-reaction-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-wake-audit-content-dm.test.ts +1 -1
package/dist/cli/ui/index.html
CHANGED
|
@@ -474,7 +474,14 @@
|
|
|
474
474
|
}
|
|
475
475
|
|
|
476
476
|
@media (min-width: 701px) {
|
|
477
|
-
|
|
477
|
+
/* The Accounts tab shows a wide-screen TABLE and a mobile card grid,
|
|
478
|
+
hiding the grid on desktop. Scope that hide to #accounts: the
|
|
479
|
+
Connections tab reuses .accounts-grid for its OAuth/Notion cards but
|
|
480
|
+
has NO table fallback, so an unscoped rule blanked every connection
|
|
481
|
+
card on any screen wider than 700px (the cards rendered but were
|
|
482
|
+
display:none, while the empty-state text — not in a grid — still
|
|
483
|
+
showed). */
|
|
484
|
+
#accounts .accounts-grid { display: none; }
|
|
478
485
|
}
|
|
479
486
|
|
|
480
487
|
@media (max-width: 600px) {
|
|
@@ -1029,20 +1036,21 @@
|
|
|
1029
1036
|
.then(r => r.ok ? r.json().then(d => ({ ok: true, data: d })) : { ok: false, data: fallback })
|
|
1030
1037
|
.catch(() => ({ ok: false, data: fallback }));
|
|
1031
1038
|
try {
|
|
1032
|
-
const [g, ms, n, ag] = await Promise.all([
|
|
1039
|
+
const [g, ms, n, li, ag] = await Promise.all([
|
|
1033
1040
|
safe(fetch(`${API}/api/google-accounts`, { headers: authHeaders() }), []),
|
|
1034
1041
|
safe(fetch(`${API}/api/microsoft-accounts`, { headers: authHeaders() }), []),
|
|
1035
1042
|
safe(fetch(`${API}/api/notion-workspace`, { headers: authHeaders() }), { configured: false, databases: [] }),
|
|
1043
|
+
safe(fetch(`${API}/api/linear-agents`, { headers: authHeaders() }), { configured: false, agents: [] }),
|
|
1036
1044
|
safe(fetch(`${API}/api/agents`, { headers: authHeaders() }), []),
|
|
1037
1045
|
]);
|
|
1038
|
-
// Notion legitimately
|
|
1039
|
-
// providers + the agent list are the ones whose failure must not
|
|
1040
|
-
// as "empty".
|
|
1046
|
+
// Notion + Linear legitimately report unconfigured — not a failure. The
|
|
1047
|
+
// OAuth providers + the agent list are the ones whose failure must not
|
|
1048
|
+
// read as "empty".
|
|
1041
1049
|
const fetchFailed = !g.ok || !ms.ok || !ag.ok;
|
|
1042
1050
|
renderConnections({
|
|
1043
|
-
google: g.data, microsoft: ms.data, notion: n.data,
|
|
1051
|
+
google: g.data, microsoft: ms.data, notion: n.data, linear: li.data,
|
|
1044
1052
|
agentNames: (ag.data || []).map(a => a.name).sort(),
|
|
1045
|
-
googleFailed: !g.ok, microsoftFailed: !ms.ok, fetchFailed,
|
|
1053
|
+
googleFailed: !g.ok, microsoftFailed: !ms.ok, linearFailed: !li.ok, fetchFailed,
|
|
1046
1054
|
});
|
|
1047
1055
|
clearError();
|
|
1048
1056
|
// Self-heal a transient blip without a manual re-click. Bounded
|
|
@@ -1240,12 +1248,33 @@
|
|
|
1240
1248
|
}
|
|
1241
1249
|
}
|
|
1242
1250
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1251
|
+
// Canonical tab list — kept in sync with the nav buttons above and with
|
|
1252
|
+
// SPA_TAB_ROUTES in src/web/server.ts (the server serves the SPA shell for
|
|
1253
|
+
// these paths so a reload/deep-link doesn't 404).
|
|
1254
|
+
const TAB_NAMES = ['summary', 'agents', 'accounts', 'system', 'memory', 'connections', 'schedule', 'approvals'];
|
|
1255
|
+
|
|
1256
|
+
// Derive the active tab from the URL path (`/connections` → 'connections',
|
|
1257
|
+
// '/' → 'summary'). Unknown paths fall back to summary.
|
|
1258
|
+
function tabFromPath() {
|
|
1259
|
+
const name = location.pathname.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
1260
|
+
return TAB_NAMES.includes(name) ? name : 'summary';
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// opts.push === false suppresses the history push — used when the switch is
|
|
1264
|
+
// itself driven by navigation (initial load, back/forward) so we don't
|
|
1265
|
+
// double-stack history entries.
|
|
1266
|
+
function switchTab(tab, opts) {
|
|
1267
|
+
if (!TAB_NAMES.includes(tab)) tab = 'summary';
|
|
1268
|
+
for (const t of TAB_NAMES) {
|
|
1246
1269
|
document.getElementById(`tab-${t}`).classList.toggle('active', tab === t);
|
|
1247
1270
|
document.getElementById(t).style.display = tab === t ? '' : 'none';
|
|
1248
1271
|
}
|
|
1272
|
+
// Mirror the tab into the URL path so reload/deep-link/back-forward all
|
|
1273
|
+
// land here. Summary canonicalises to '/'.
|
|
1274
|
+
if (!opts || opts.push !== false) {
|
|
1275
|
+
const path = tab === 'summary' ? '/' : '/' + tab;
|
|
1276
|
+
if (location.pathname !== path) history.pushState({ tab }, '', path);
|
|
1277
|
+
}
|
|
1249
1278
|
if (tab === 'summary') fetchSummary();
|
|
1250
1279
|
if (tab === 'accounts') fetchAccounts();
|
|
1251
1280
|
if (tab === 'system') fetchSystemHealth();
|
|
@@ -1255,6 +1284,11 @@
|
|
|
1255
1284
|
if (tab === 'approvals') fetchApprovals();
|
|
1256
1285
|
}
|
|
1257
1286
|
|
|
1287
|
+
// Back/forward: restore the tab from the history entry (or the path).
|
|
1288
|
+
window.addEventListener('popstate', (e) => {
|
|
1289
|
+
switchTab((e.state && e.state.tab) || tabFromPath(), { push: false });
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1258
1292
|
// Fleet overview — ONE round-trip to the server-side aggregate
|
|
1259
1293
|
// (/api/summary), which pulls each part through the same per-tab
|
|
1260
1294
|
// cache so a Summary open warms the tabs and they can never disagree.
|
|
@@ -2117,6 +2151,7 @@
|
|
|
2117
2151
|
const google = data.google || [];
|
|
2118
2152
|
const microsoft = data.microsoft || [];
|
|
2119
2153
|
const notion = data.notion || { configured: false, databases: [] };
|
|
2154
|
+
const linear = data.linear || { configured: false, agents: [] };
|
|
2120
2155
|
const agentNames = data.agentNames || [];
|
|
2121
2156
|
|
|
2122
2157
|
// When a provider's fetch FAILED (not genuinely empty), its empty-state
|
|
@@ -2180,6 +2215,37 @@
|
|
|
2180
2215
|
${fullAccessBanner}${notionBody}
|
|
2181
2216
|
</div>`;
|
|
2182
2217
|
|
|
2218
|
+
// Linear — first-class OAuth app actors (config-only; the web tier never
|
|
2219
|
+
// reads the per-agent vault, so no token/expiry is shown — and never the
|
|
2220
|
+
// secret). One card per agent with linear_agent.enabled.
|
|
2221
|
+
const linearCards = (linear.agents || []).map(a => {
|
|
2222
|
+
const meta = [];
|
|
2223
|
+
if (a.workspaceId) meta.push(`<div class="meta-item"><label>Workspace </label><span>${escapeHtml(a.workspaceId)}</span></div>`);
|
|
2224
|
+
if (a.defaultTeamId) meta.push(`<div class="meta-item"><label>Default team </label><span>${escapeHtml(a.defaultTeamId)}</span></div>`);
|
|
2225
|
+
if (a.tokenVaultKey) meta.push(`<div class="meta-item"><label>Token </label><span><code>${escapeHtml(a.tokenVaultKey)}</code></span></div>`);
|
|
2226
|
+
return `
|
|
2227
|
+
<div class="account-card">
|
|
2228
|
+
<div class="account-card-header">
|
|
2229
|
+
<div class="account-label">${escapeHtml(a.agent)}</div>
|
|
2230
|
+
<span style="margin-left:auto" class="usage-pill primary">OAuth agent</span>
|
|
2231
|
+
</div>
|
|
2232
|
+
<div class="card-meta" style="padding:0">${meta.join('') || _dimC('—')}</div>
|
|
2233
|
+
</div>`;
|
|
2234
|
+
}).join('');
|
|
2235
|
+
const linearNote = (linear.agents && linear.agents.length)
|
|
2236
|
+
? `<div style="margin:0 0 .6rem;font-size:.85rem;color:var(--text-dim)">Connected as Linear <b>OAuth app actors</b> — each appears in the workspace as its own actor (not a personal API token for MCP).</div>`
|
|
2237
|
+
: '';
|
|
2238
|
+
const linearBody = linearCards
|
|
2239
|
+
? `<div class="accounts-grid">${linearCards}</div>`
|
|
2240
|
+
: `<div class="loading" style="padding:.8rem">${data.linearFailed
|
|
2241
|
+
? "Couldn't load Linear agents — the data source was unreachable (retrying)."
|
|
2242
|
+
: 'No Linear agents connected. Run <code>switchroom linear-agent setup --agent <name></code> to connect one as a Linear OAuth app actor.'}</div>`;
|
|
2243
|
+
const linearSection = `
|
|
2244
|
+
<div style="margin-bottom:1.5rem">
|
|
2245
|
+
<h3 style="margin:0 0 .6rem;font-size:.95rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em">Linear</h3>
|
|
2246
|
+
${linearNote}${linearBody}
|
|
2247
|
+
</div>`;
|
|
2248
|
+
|
|
2183
2249
|
// Tab-level degraded banner: when every OAuth account (Google +
|
|
2184
2250
|
// Microsoft) is config-only (no live broker slot) and there's at least
|
|
2185
2251
|
// one, the broker data is unavailable — surface that ABOVE the sections
|
|
@@ -2197,7 +2263,7 @@
|
|
|
2197
2263
|
? renderProblem(problemFor('connections-unreachable', {}))
|
|
2198
2264
|
: '';
|
|
2199
2265
|
|
|
2200
|
-
container.innerHTML = unreachableBanner + degradedBanner + googleSection + microsoftSection + notionSection;
|
|
2266
|
+
container.innerHTML = unreachableBanner + degradedBanner + googleSection + microsoftSection + notionSection + linearSection;
|
|
2201
2267
|
}
|
|
2202
2268
|
|
|
2203
2269
|
function renderSchedule(data) {
|
|
@@ -2635,12 +2701,16 @@
|
|
|
2635
2701
|
}
|
|
2636
2702
|
}
|
|
2637
2703
|
|
|
2638
|
-
// Init.
|
|
2639
|
-
//
|
|
2640
|
-
//
|
|
2641
|
-
//
|
|
2642
|
-
//
|
|
2643
|
-
|
|
2704
|
+
// Init. The initial tab comes from the URL path (so a reload/deep-link of
|
|
2705
|
+
// `/connections` opens Connections, not Summary); '/' → Summary. We stamp
|
|
2706
|
+
// the history entry so the first back/forward works, then switchTab
|
|
2707
|
+
// (push:false) shows the tab and fetches just its data. fetchAgents still
|
|
2708
|
+
// runs unconditionally (keeps the 10s fleet poll warm + the log WS depends
|
|
2709
|
+
// on it). Per-tab data is fetched on switch only — deliberately NOT on the
|
|
2710
|
+
// 10s interval (it fans out to system-health etc.; on-demand is enough).
|
|
2711
|
+
const initialTab = tabFromPath();
|
|
2712
|
+
history.replaceState({ tab: initialTab }, '', location.pathname);
|
|
2713
|
+
switchTab(initialTab, { push: false });
|
|
2644
2714
|
fetchAgents();
|
|
2645
2715
|
connectWebSocket();
|
|
2646
2716
|
// Fleet poll — gated on tab visibility. A phone with the dashboard
|
|
@@ -13736,7 +13736,7 @@ var ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
|
13736
13736
|
var ScheduleEntrySchema = exports_external.object({
|
|
13737
13737
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
13738
13738
|
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)."),
|
|
13739
|
-
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (
|
|
13739
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (reference/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 tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
|
|
13740
13740
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
13741
13741
|
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
13742
13742
|
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."),
|
|
@@ -13745,7 +13745,7 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
13745
13745
|
topic: exports_external.union([
|
|
13746
13746
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
13747
13747
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
13748
|
-
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See
|
|
13748
|
+
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See reference/rfcs/supergroup-mode.md.")
|
|
13749
13749
|
}).superRefine((entry, ctx) => {
|
|
13750
13750
|
const kind = entry.kind ?? "prompt";
|
|
13751
13751
|
if (kind === "poll" && !entry.poll) {
|
|
@@ -13918,15 +13918,15 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
13918
13918
|
webhook_rate_limit: exports_external.object({
|
|
13919
13919
|
rpm: exports_external.number().int().positive()
|
|
13920
13920
|
}).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."),
|
|
13921
|
-
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
|
|
13922
|
-
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
|
|
13921
|
+
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 reference/rfcs/webhook-via-gateway-socket.md."),
|
|
13922
|
+
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 reference/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
13923
13923
|
linear_agent: exports_external.object({
|
|
13924
13924
|
enabled: exports_external.boolean(),
|
|
13925
13925
|
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."),
|
|
13926
13926
|
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."),
|
|
13927
13927
|
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>`.")
|
|
13928
13928
|
}).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."),
|
|
13929
|
-
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
|
|
13929
|
+
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 reference/rfcs/supergroup-mode.md."),
|
|
13930
13930
|
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`."),
|
|
13931
13931
|
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.")
|
|
13932
13932
|
}).optional().superRefine((tg, ctx) => {
|
|
@@ -14152,7 +14152,7 @@ var AgentSchema = exports_external.object({
|
|
|
14152
14152
|
drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
|
|
14153
14153
|
google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides — currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent — they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
|
|
14154
14154
|
microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override — pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
|
|
14155
|
-
notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC
|
|
14155
|
+
notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write — names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
|
|
14156
14156
|
repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
|
|
14157
14157
|
url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
|
|
14158
14158
|
branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
|
|
@@ -14287,9 +14287,9 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
14287
14287
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
14288
14288
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
14289
14289
|
microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
|
|
14290
|
-
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC
|
|
14290
|
+
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
|
|
14291
14291
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
14292
|
-
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (
|
|
14292
|
+
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
14293
14293
|
hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
|
|
14294
14294
|
web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
|
|
14295
14295
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
@@ -14311,7 +14311,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
14311
14311
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
14312
14312
|
message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
|
|
14313
14313
|
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
14314
|
-
cron: CronConfigSchema.optional().describe("Cheap-cron settings (
|
|
14314
|
+
cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (§6.1). Required to enable any http-diff poll; not agent-writable.")
|
|
14315
14315
|
});
|
|
14316
14316
|
|
|
14317
14317
|
// src/config/paths.ts
|
|
@@ -21680,7 +21680,7 @@ class HostdServer {
|
|
|
21680
21680
|
async handleAgentExec(req, started) {
|
|
21681
21681
|
const argv0 = req.args.argv[0];
|
|
21682
21682
|
if (!isAllowlistedReadOnlyArgv(argv0)) {
|
|
21683
|
-
return deniedResponse(req.request_id, `agent_exec: "${argv0}" is not on the read-only allowlist. ` + `Allowed: ${READONLY_EXEC_ALLOWLIST.join(", ")}. ` + `Writes inside peer containers require the host_os.exec ` + `approval-kernel scope, which is not yet wired — see ` + `
|
|
21683
|
+
return deniedResponse(req.request_id, `agent_exec: "${argv0}" is not on the read-only allowlist. ` + `Allowed: ${READONLY_EXEC_ALLOWLIST.join(", ")}. ` + `Writes inside peer containers require the host_os.exec ` + `approval-kernel scope, which is not yet wired — see ` + `reference/rfcs/approval-kernel.md §6 (deferred follow-up).`, Date.now() - started);
|
|
21684
21684
|
}
|
|
21685
21685
|
if (!req.args.argv.every(isSafeExecArgvElement)) {
|
|
21686
21686
|
return deniedResponse(req.request_id, `agent_exec: an argv element contains a control character ` + `(C0 / DEL) or exceeds ${MAX_EXEC_ARGV_ELEMENT_BYTES} bytes, ` + `which is not permitted (#1401 / #1400).`, Date.now() - started);
|
|
@@ -11344,7 +11344,7 @@ var init_schema = __esm(() => {
|
|
|
11344
11344
|
ScheduleEntrySchema = exports_external.object({
|
|
11345
11345
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
11346
11346
|
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)."),
|
|
11347
|
-
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (
|
|
11347
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (reference/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 tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
|
|
11348
11348
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11349
11349
|
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
11350
11350
|
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."),
|
|
@@ -11353,7 +11353,7 @@ var init_schema = __esm(() => {
|
|
|
11353
11353
|
topic: exports_external.union([
|
|
11354
11354
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
11355
11355
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
11356
|
-
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See
|
|
11356
|
+
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See reference/rfcs/supergroup-mode.md.")
|
|
11357
11357
|
}).superRefine((entry, ctx) => {
|
|
11358
11358
|
const kind = entry.kind ?? "prompt";
|
|
11359
11359
|
if (kind === "poll" && !entry.poll) {
|
|
@@ -11526,15 +11526,15 @@ var init_schema = __esm(() => {
|
|
|
11526
11526
|
webhook_rate_limit: exports_external.object({
|
|
11527
11527
|
rpm: exports_external.number().int().positive()
|
|
11528
11528
|
}).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."),
|
|
11529
|
-
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
|
|
11530
|
-
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
|
|
11529
|
+
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 reference/rfcs/webhook-via-gateway-socket.md."),
|
|
11530
|
+
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 reference/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11531
11531
|
linear_agent: exports_external.object({
|
|
11532
11532
|
enabled: exports_external.boolean(),
|
|
11533
11533
|
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."),
|
|
11534
11534
|
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."),
|
|
11535
11535
|
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>`.")
|
|
11536
11536
|
}).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."),
|
|
11537
|
-
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
|
|
11537
|
+
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 reference/rfcs/supergroup-mode.md."),
|
|
11538
11538
|
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`."),
|
|
11539
11539
|
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.")
|
|
11540
11540
|
}).optional().superRefine((tg, ctx) => {
|
|
@@ -11760,7 +11760,7 @@ var init_schema = __esm(() => {
|
|
|
11760
11760
|
drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
|
|
11761
11761
|
google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides — currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent — they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
|
|
11762
11762
|
microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override — pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
|
|
11763
|
-
notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC
|
|
11763
|
+
notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write — names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
|
|
11764
11764
|
repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
|
|
11765
11765
|
url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
|
|
11766
11766
|
branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
|
|
@@ -11895,9 +11895,9 @@ var init_schema = __esm(() => {
|
|
|
11895
11895
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
11896
11896
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
11897
11897
|
microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
|
|
11898
|
-
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC
|
|
11898
|
+
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
|
|
11899
11899
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
11900
|
-
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (
|
|
11900
|
+
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
11901
11901
|
hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
|
|
11902
11902
|
web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
|
|
11903
11903
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
@@ -11919,7 +11919,7 @@ var init_schema = __esm(() => {
|
|
|
11919
11919
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
11920
11920
|
message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
|
|
11921
11921
|
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
11922
|
-
cron: CronConfigSchema.optional().describe("Cheap-cron settings (
|
|
11922
|
+
cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (§6.1). Required to enable any http-diff poll; not agent-writable.")
|
|
11923
11923
|
});
|
|
11924
11924
|
});
|
|
11925
11925
|
|
|
@@ -11344,7 +11344,7 @@ var init_schema = __esm(() => {
|
|
|
11344
11344
|
ScheduleEntrySchema = exports_external.object({
|
|
11345
11345
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
11346
11346
|
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)."),
|
|
11347
|
-
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (
|
|
11347
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (reference/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 tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
|
|
11348
11348
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11349
11349
|
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
11350
11350
|
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."),
|
|
@@ -11353,7 +11353,7 @@ var init_schema = __esm(() => {
|
|
|
11353
11353
|
topic: exports_external.union([
|
|
11354
11354
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
11355
11355
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
11356
|
-
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See
|
|
11356
|
+
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See reference/rfcs/supergroup-mode.md.")
|
|
11357
11357
|
}).superRefine((entry, ctx) => {
|
|
11358
11358
|
const kind = entry.kind ?? "prompt";
|
|
11359
11359
|
if (kind === "poll" && !entry.poll) {
|
|
@@ -11526,15 +11526,15 @@ var init_schema = __esm(() => {
|
|
|
11526
11526
|
webhook_rate_limit: exports_external.object({
|
|
11527
11527
|
rpm: exports_external.number().int().positive()
|
|
11528
11528
|
}).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."),
|
|
11529
|
-
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
|
|
11530
|
-
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
|
|
11529
|
+
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 reference/rfcs/webhook-via-gateway-socket.md."),
|
|
11530
|
+
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 reference/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11531
11531
|
linear_agent: exports_external.object({
|
|
11532
11532
|
enabled: exports_external.boolean(),
|
|
11533
11533
|
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."),
|
|
11534
11534
|
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."),
|
|
11535
11535
|
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>`.")
|
|
11536
11536
|
}).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."),
|
|
11537
|
-
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
|
|
11537
|
+
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 reference/rfcs/supergroup-mode.md."),
|
|
11538
11538
|
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`."),
|
|
11539
11539
|
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.")
|
|
11540
11540
|
}).optional().superRefine((tg, ctx) => {
|
|
@@ -11760,7 +11760,7 @@ var init_schema = __esm(() => {
|
|
|
11760
11760
|
drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
|
|
11761
11761
|
google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides — currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent — they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
|
|
11762
11762
|
microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override — pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
|
|
11763
|
-
notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC
|
|
11763
|
+
notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write — names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
|
|
11764
11764
|
repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
|
|
11765
11765
|
url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
|
|
11766
11766
|
branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
|
|
@@ -11895,9 +11895,9 @@ var init_schema = __esm(() => {
|
|
|
11895
11895
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
11896
11896
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
11897
11897
|
microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
|
|
11898
|
-
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC
|
|
11898
|
+
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
|
|
11899
11899
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
11900
|
-
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (
|
|
11900
|
+
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
11901
11901
|
hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
|
|
11902
11902
|
web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
|
|
11903
11903
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
@@ -11919,7 +11919,7 @@ var init_schema = __esm(() => {
|
|
|
11919
11919
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
11920
11920
|
message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
|
|
11921
11921
|
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
11922
|
-
cron: CronConfigSchema.optional().describe("Cheap-cron settings (
|
|
11922
|
+
cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (§6.1). Required to enable any http-diff poll; not agent-writable.")
|
|
11923
11923
|
});
|
|
11924
11924
|
});
|
|
11925
11925
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Tier-1 cheap cron SESSION launcher —
|
|
2
|
+
# Tier-1 cheap cron SESSION launcher — reference/rfcs/cheap-cron-sessions.md §2.2.
|
|
3
3
|
#
|
|
4
4
|
# A SECOND interactive `claude` (no -p — compliance pillar 3) in the agent
|
|
5
5
|
# container, dedicated to cheap cron fires. It registers to the SAME gateway
|
|
@@ -164,7 +164,7 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
164
164
|
fi
|
|
165
165
|
|
|
166
166
|
{{#if cronSessionEnabled}}
|
|
167
|
-
# 4) cheap cron SESSION (Tier 1,
|
|
167
|
+
# 4) cheap cron SESSION (Tier 1, reference/rfcs/cheap-cron-sessions.md §2.2).
|
|
168
168
|
# A SECOND interactive claude (no -p) dedicated to context:fresh cron
|
|
169
169
|
# fires, registering to the gateway as the cron-suffixed bridge. This
|
|
170
170
|
# block is rendered ONLY for an agent that has a Tier-1 cron entry; for
|
|
@@ -168,6 +168,31 @@ When you get such a reaction turn:
|
|
|
168
168
|
|
|
169
169
|
Don't acknowledge with only a reaction or a bare "done" — the operator wants
|
|
170
170
|
the link (or the honest reason it didn't file).
|
|
171
|
+
|
|
172
|
+
### If your Linear auth breaks (a 401 / "can't reach Linear")
|
|
173
|
+
|
|
174
|
+
Linear `actor=app` access tokens expire (~24h) and renew from a stored
|
|
175
|
+
**refresh bundle** (`linear/<you>/oauth`). If that bundle is missing or its
|
|
176
|
+
refresh token was revoked, your Linear calls 401 and the operator gets a
|
|
177
|
+
"🔑 Linear auth needs you" alert. You can re-authorize yourself — it's an
|
|
178
|
+
operator-approved, in-container flow (no host shell needed):
|
|
179
|
+
|
|
180
|
+
1. Ask the operator for the Linear **OAuth app client_id + redirect_uri** (and
|
|
181
|
+
they'll have the client_secret ready for step 3).
|
|
182
|
+
2. Call **`linear_agent_setup`** with `action: "authorize_url"`, the
|
|
183
|
+
`client_id`, and `redirect_uri`. Relay the returned URL — the operator opens
|
|
184
|
+
it, consents, and copies the `code=` value from the redirect.
|
|
185
|
+
3. Call **`linear_agent_setup`** with `action: "complete"` + `client_id`,
|
|
186
|
+
`client_secret`, `redirect_uri`, and that `code`. It exchanges the code and
|
|
187
|
+
stores the access token + refresh bundle via the vault broker.
|
|
188
|
+
- If it returns **vault_request_access** instructions, the keys need a
|
|
189
|
+
write-grant — make those calls, the operator approves, then re-run
|
|
190
|
+
`complete` (re-open the authorize URL first if the code went stale).
|
|
191
|
+
- If it returns **config_propose_edit** guidance (durability/ACL), propose
|
|
192
|
+
that edit so the change survives restarts and auto-refresh keeps working.
|
|
193
|
+
|
|
194
|
+
The client_secret and code are used only for the exchange — never store them in
|
|
195
|
+
config or paste them into a normal message; pass them straight to the tool.
|
|
171
196
|
{{/if}}
|
|
172
197
|
|
|
173
198
|
### Don't lie about scheduling
|
|
@@ -38,7 +38,7 @@ When the user says "add a new agent", "add an agent to my switchroom setup", or
|
|
|
38
38
|
|
|
39
39
|
### Anthropic accounts (one OAuth, many agents)
|
|
40
40
|
|
|
41
|
-
The auth model treats the Anthropic account as the unit of authentication: one OAuth flow per account, then every agent in the fleet inherits the fleet-wide active account. The `switchroom-auth-broker` daemon owns the refresh loop and is the sole writer of every `credentials.json`. See `docs/auth.md` for the operator guide and `reference/share-auth-across-the-fleet.md` for the design.
|
|
41
|
+
The auth model treats the Anthropic account as the unit of authentication: one OAuth flow per account, then every agent in the fleet inherits the fleet-wide active account. The `switchroom-auth-broker` daemon owns the refresh loop and is the sole writer of every `credentials.json`. See `docs/auth.md` for the operator guide and `reference/jobs/share-auth-across-the-fleet.md` for the design.
|
|
42
42
|
|
|
43
43
|
**Bootstrap flow when the user wants to share one Pro/Max subscription across agents:**
|
|
44
44
|
|
|
@@ -145,7 +145,7 @@ Doubled `!!` (typo / emphasis) reaches you verbatim. Empty `!` gets a "Send your
|
|
|
145
145
|
|
|
146
146
|
## "status?" / "still there?" — UX-failure signal
|
|
147
147
|
|
|
148
|
-
**Trigger:** the user sends a short, low-content message asking whether you're alive — "status?", "still there?", "any update?", "you working?". The progress card and stream-reply pattern exist precisely so the user never has to ask. When you see one of those messages, treat it as a defect signal: something about the in-flight turn made the user feel uncertain. The product expectation (per `reference/know-what-my-agent-is-doing.md`) is that this rate trends to zero.
|
|
148
|
+
**Trigger:** the user sends a short, low-content message asking whether you're alive — "status?", "still there?", "any update?", "you working?". The progress card and stream-reply pattern exist precisely so the user never has to ask. When you see one of those messages, treat it as a defect signal: something about the in-flight turn made the user feel uncertain. The product expectation (per `reference/jobs/know-what-my-agent-is-doing.md`) is that this rate trends to zero.
|
|
149
149
|
|
|
150
150
|
Your response should:
|
|
151
151
|
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
* Answer-lane incremental streaming for long Telegram replies.
|
|
12
12
|
*
|
|
13
13
|
* This module implements the "narrative" liveness layer described in
|
|
14
|
-
* `reference/know-what-my-agent-is-doing.md`:
|
|
14
|
+
* `reference/jobs/know-what-my-agent-is-doing.md`:
|
|
15
15
|
*
|
|
16
16
|
* ambient → 👀 ack reaction
|
|
17
17
|
* structured → progress card (existing, via stream-reply-handler.ts lane:'progress')
|