switchroom 0.20.9 → 0.20.11
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/bin/handoff-briefing.sh +57 -5
- package/bin/working-state-reload-hook.sh +262 -0
- package/dist/agent-scheduler/index.js +65 -2
- package/dist/auth-broker/index.js +204 -24
- package/dist/cli/notion-write-pretool.mjs +65 -2
- package/dist/cli/self-improve-apply-guard-pretool.mjs +357 -92
- package/dist/cli/self-improve-stop.mjs +889 -7
- package/dist/cli/skill-validate-pretool.mjs +82 -3
- package/dist/cli/switchroom.js +3699 -2110
- package/dist/host-control/main.js +67 -4
- package/dist/vault/approvals/kernel-server.js +66 -3
- package/dist/vault/broker/server.js +66 -3
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +49 -0
- package/profiles/_shared/agent-self-service.md.hbs +15 -22
- package/profiles/_shared/delegation-golden-rule.md.hbs +1 -1
- package/profiles/_shared/dev-protocol.md.hbs +1 -1
- package/profiles/_shared/execution-discipline.md.hbs +4 -4
- package/profiles/_shared/vault-protocol.md.hbs +2 -18
- package/profiles/default/CLAUDE.md.hbs +3 -5
- package/telegram-plugin/auto-fallback-fleet.ts +37 -2
- package/telegram-plugin/dist/gateway/gateway.js +1414 -918
- package/telegram-plugin/fallback-card-collapse.ts +1 -0
- package/telegram-plugin/gateway/auth-command.ts +11 -1
- package/telegram-plugin/gateway/callback-query-handlers.ts +100 -0
- package/telegram-plugin/gateway/eval-case-proposal-card.ts +86 -0
- package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.test.ts +74 -0
- package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.ts +71 -0
- package/telegram-plugin/gateway/gateway.ts +85 -90
- package/telegram-plugin/gateway/ipc-protocol.ts +43 -0
- package/telegram-plugin/gateway/ipc-server.ts +28 -0
- package/telegram-plugin/gateway/narrative-lane.ts +33 -2
- package/telegram-plugin/gateway/privacy-reset.test.ts +216 -0
- package/telegram-plugin/gateway/privacy-reset.ts +87 -0
- package/telegram-plugin/gateway/privacy-state.test.ts +165 -0
- package/telegram-plugin/gateway/privacy-state.ts +206 -0
- package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +176 -0
- package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +24 -14
- package/telegram-plugin/gateway/stale-pin-sweep.test.ts +123 -26
- package/telegram-plugin/gateway/stale-pin-sweep.ts +48 -32
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +15 -4
- package/telegram-plugin/slot-banner-driver.ts +42 -5
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +24 -0
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +2 -0
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +97 -0
- package/telegram-plugin/tests/privacy-reset-call-sites.test.ts +120 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +25 -0
- package/telegram-plugin/tests/throttle-tier.test.ts +16 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +67 -0
- package/telegram-plugin/throttle-tier.ts +12 -3
- package/telegram-plugin/turn-flush-safety.ts +97 -0
- package/vendor/hindsight-memory/CHANGELOG.md +31 -0
- package/vendor/hindsight-memory/hooks/hooks.json +2 -1
- package/vendor/hindsight-memory/scripts/retain.py +306 -0
- package/vendor/hindsight-memory/scripts/session_start.py +35 -8
- package/vendor/hindsight-memory/scripts/subagent_retain.py +29 -1
- package/vendor/hindsight-memory/scripts/tests/test_private_mode.py +415 -0
- package/vendor/hindsight-memory/scripts/tests/test_self_improve_correction_tag.py +167 -0
- package/vendor/hindsight-memory/scripts/tests/test_session_start_durability.py +107 -0
|
@@ -10994,7 +10994,7 @@ var init_observation_scopes = __esm(() => {
|
|
|
10994
10994
|
});
|
|
10995
10995
|
|
|
10996
10996
|
// src/config/schema.ts
|
|
10997
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
10997
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
|
|
10998
10998
|
var init_schema = __esm(() => {
|
|
10999
10999
|
init_zod();
|
|
11000
11000
|
init_observation_scopes();
|
|
@@ -11691,8 +11691,10 @@ var init_schema = __esm(() => {
|
|
|
11691
11691
|
bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
|
|
11692
11692
|
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
|
|
11693
11693
|
auth: exports_external.object({
|
|
11694
|
-
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
|
|
11695
|
-
|
|
11694
|
+
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5."),
|
|
11695
|
+
strict: exports_external.boolean().optional().describe("Requires `override`. When true, the pin is a hard binding, not a " + "routing preference: the broker NEVER serves this agent from any " + "other account — no failover to `auth.fallback_order` while the " + "pinned account is quota-walled or exhausted. The agent rides out " + "the wall on its own account (surfacing the normal 429/quota " + "cards) instead of silently borrowing fleet quota. Use for " + "accounts that must never cross a billing/compliance boundary " + "(e.g. an employer-provided subscription)."),
|
|
11696
|
+
exclusive: exports_external.boolean().optional().describe("Requires `override`. When true, the pinned account belongs to " + "THIS agent alone: the broker refuses to serve it to any other " + "agent or consumer, refuses `auth use <label>` (set-active) onto " + "it, and refuses pinning another agent to it. Config that routes " + "others to the account (`auth.active`, `auth.fallback_order`, " + "another agent's `override`, a consumer pin) is rejected at load. " + "Usually paired with `strict: true` for full two-way isolation.")
|
|
11697
|
+
}).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases, " + "with optional `strict:` (never borrow another account) and " + "`exclusive:` (no one else may use the pinned account) hardening. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
|
|
11696
11698
|
dm_only: exports_external.boolean().optional().describe("Mark this agent as a DM-only bot — has its own bot_token and lives " + "exclusively in a private chat with the operator. Suppresses " + "scaffolding's default behavior of inheriting the global " + "telegram.forum_chat_id into the agent's access.json `groups` entry " + "(the forum chat the bot isn't a member of, which would otherwise " + "trigger a 'boot-probe-failed: 400 chat not found' warning every " + "restart). topic_name is still schema-required but unused — set it " + "to a display label like 'DM' for /switchroom status output."),
|
|
11697
11699
|
topic_name: exports_external.string().describe("Telegram forum topic display name"),
|
|
11698
11700
|
topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
|
|
@@ -11767,6 +11769,17 @@ var init_schema = __esm(() => {
|
|
|
11767
11769
|
tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional()
|
|
11768
11770
|
}).optional()
|
|
11769
11771
|
}).superRefine((agent, ctx) => {
|
|
11772
|
+
if (agent.auth && !agent.auth.override) {
|
|
11773
|
+
for (const flag of ["strict", "exclusive"]) {
|
|
11774
|
+
if (agent.auth[flag]) {
|
|
11775
|
+
ctx.addIssue({
|
|
11776
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11777
|
+
message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` — there is no pinned account for it to apply to.`,
|
|
11778
|
+
path: ["auth", flag]
|
|
11779
|
+
});
|
|
11780
|
+
}
|
|
11781
|
+
}
|
|
11782
|
+
}
|
|
11770
11783
|
if (agent.dm_only !== true)
|
|
11771
11784
|
return;
|
|
11772
11785
|
const tg = agent.channels?.telegram;
|
|
@@ -11879,6 +11892,13 @@ var init_schema = __esm(() => {
|
|
|
11879
11892
|
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
11880
11893
|
person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
|
|
11881
11894
|
});
|
|
11895
|
+
ConfigRepoConfigSchema = exports_external.object({
|
|
11896
|
+
enabled: exports_external.boolean().default(false).describe("Master switch for config-repo change control. Default false (opt-in). " + "When false the `config-repo sync` verb still runs on demand but the " + "doctor check and (later) the scheduled tick treat the feature as off."),
|
|
11897
|
+
path: exports_external.string().regex(/^[a-zA-Z0-9~._\-/]+$/, "config_repo.path must not contain shell-special characters ($, `, \", ', \\, etc.)").default("~/.switchroom-config").describe("Filesystem path to the operator's private config repo. Must be a git " + "repo (else `switchroom doctor` FAILs). Tilde-expanded at read time."),
|
|
11898
|
+
push: exports_external.boolean().default(true).describe("When true, `config-repo sync` pushes after committing (subject to the " + "`require_private` gate). When false, it commits locally only — useful " + "for offline hosts or a review-before-push workflow."),
|
|
11899
|
+
remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
|
|
11900
|
+
require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote — or when the API is unreachable — the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true.")
|
|
11901
|
+
});
|
|
11882
11902
|
SwitchroomConfigSchema = exports_external.object({
|
|
11883
11903
|
switchroom: exports_external.object({
|
|
11884
11904
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -11891,6 +11911,7 @@ var init_schema = __esm(() => {
|
|
|
11891
11911
|
memory: MemoryBackendConfigSchema.optional(),
|
|
11892
11912
|
hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) — takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
|
|
11893
11913
|
vault: VaultConfigSchema.optional(),
|
|
11914
|
+
config_repo: ConfigRepoConfigSchema.optional().describe("Change control for the operator's private ~/.switchroom-config git repo " + "(backup + audit trail of live host config, agent workspace state, and " + "mirrored personal skills). Consumed by `switchroom config-repo sync` and " + "the `config repo` doctor check. Optional and OFF by default."),
|
|
11894
11915
|
auth: exports_external.object({
|
|
11895
11916
|
active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
|
|
11896
11917
|
fallback_order: exports_external.array(exports_external.string().min(1)).optional().describe("Ordered list of account labels for `switchroom auth rotate` to cycle " + "through when the active account hits a quota event. First entry is " + "normally the same as `auth.active`. When unset, `rotate` is a no-op."),
|
|
@@ -11958,6 +11979,48 @@ var init_schema = __esm(() => {
|
|
|
11958
11979
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11959
11980
|
checkServes(a.serves, ["agents", name, "serves"]);
|
|
11960
11981
|
}
|
|
11982
|
+
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11983
|
+
const agentAuth = a.auth;
|
|
11984
|
+
if (!agentAuth?.exclusive || !agentAuth.override)
|
|
11985
|
+
continue;
|
|
11986
|
+
const acct = agentAuth.override;
|
|
11987
|
+
if (cfg.auth?.active === acct) {
|
|
11988
|
+
ctx.addIssue({
|
|
11989
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11990
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
|
|
11991
|
+
path: ["auth", "active"]
|
|
11992
|
+
});
|
|
11993
|
+
}
|
|
11994
|
+
const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
|
|
11995
|
+
if (fbIdx !== -1) {
|
|
11996
|
+
ctx.addIssue({
|
|
11997
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11998
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
|
|
11999
|
+
path: ["auth", "fallback_order", fbIdx]
|
|
12000
|
+
});
|
|
12001
|
+
}
|
|
12002
|
+
for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
|
|
12003
|
+
if (other === name)
|
|
12004
|
+
continue;
|
|
12005
|
+
const otherOverride = oa.auth?.override;
|
|
12006
|
+
if (otherOverride === acct) {
|
|
12007
|
+
ctx.addIssue({
|
|
12008
|
+
code: exports_external.ZodIssueCode.custom,
|
|
12009
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
|
|
12010
|
+
path: ["agents", other, "auth", "override"]
|
|
12011
|
+
});
|
|
12012
|
+
}
|
|
12013
|
+
}
|
|
12014
|
+
(cfg.auth?.consumers ?? []).forEach((c, i) => {
|
|
12015
|
+
if (c.account === acct) {
|
|
12016
|
+
ctx.addIssue({
|
|
12017
|
+
code: exports_external.ZodIssueCode.custom,
|
|
12018
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
|
|
12019
|
+
path: ["auth", "consumers", i, "account"]
|
|
12020
|
+
});
|
|
12021
|
+
}
|
|
12022
|
+
});
|
|
12023
|
+
}
|
|
11961
12024
|
const microsoftAccounts = cfg.microsoft_accounts;
|
|
11962
12025
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11963
12026
|
const mw = a.microsoft_workspace;
|
|
@@ -21493,7 +21556,7 @@ function allocateAgentUid(name) {
|
|
|
21493
21556
|
}
|
|
21494
21557
|
|
|
21495
21558
|
// src/build-info.ts
|
|
21496
|
-
var VERSION = "0.20.
|
|
21559
|
+
var VERSION = "0.20.11";
|
|
21497
21560
|
|
|
21498
21561
|
// src/setup/hindsight-recall-tunables.ts
|
|
21499
21562
|
var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
|
|
@@ -4434,7 +4434,7 @@ var init_observation_scopes = __esm(() => {
|
|
|
4434
4434
|
});
|
|
4435
4435
|
|
|
4436
4436
|
// src/config/schema.ts
|
|
4437
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
4437
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
|
|
4438
4438
|
var init_schema = __esm(() => {
|
|
4439
4439
|
init_zod();
|
|
4440
4440
|
init_observation_scopes();
|
|
@@ -5131,8 +5131,10 @@ var init_schema = __esm(() => {
|
|
|
5131
5131
|
bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
|
|
5132
5132
|
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
|
|
5133
5133
|
auth: exports_external.object({
|
|
5134
|
-
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
|
|
5135
|
-
|
|
5134
|
+
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5."),
|
|
5135
|
+
strict: exports_external.boolean().optional().describe("Requires `override`. When true, the pin is a hard binding, not a " + "routing preference: the broker NEVER serves this agent from any " + "other account — no failover to `auth.fallback_order` while the " + "pinned account is quota-walled or exhausted. The agent rides out " + "the wall on its own account (surfacing the normal 429/quota " + "cards) instead of silently borrowing fleet quota. Use for " + "accounts that must never cross a billing/compliance boundary " + "(e.g. an employer-provided subscription)."),
|
|
5136
|
+
exclusive: exports_external.boolean().optional().describe("Requires `override`. When true, the pinned account belongs to " + "THIS agent alone: the broker refuses to serve it to any other " + "agent or consumer, refuses `auth use <label>` (set-active) onto " + "it, and refuses pinning another agent to it. Config that routes " + "others to the account (`auth.active`, `auth.fallback_order`, " + "another agent's `override`, a consumer pin) is rejected at load. " + "Usually paired with `strict: true` for full two-way isolation.")
|
|
5137
|
+
}).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases, " + "with optional `strict:` (never borrow another account) and " + "`exclusive:` (no one else may use the pinned account) hardening. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
|
|
5136
5138
|
dm_only: exports_external.boolean().optional().describe("Mark this agent as a DM-only bot — has its own bot_token and lives " + "exclusively in a private chat with the operator. Suppresses " + "scaffolding's default behavior of inheriting the global " + "telegram.forum_chat_id into the agent's access.json `groups` entry " + "(the forum chat the bot isn't a member of, which would otherwise " + "trigger a 'boot-probe-failed: 400 chat not found' warning every " + "restart). topic_name is still schema-required but unused — set it " + "to a display label like 'DM' for /switchroom status output."),
|
|
5137
5139
|
topic_name: exports_external.string().describe("Telegram forum topic display name"),
|
|
5138
5140
|
topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
|
|
@@ -5207,6 +5209,17 @@ var init_schema = __esm(() => {
|
|
|
5207
5209
|
tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional()
|
|
5208
5210
|
}).optional()
|
|
5209
5211
|
}).superRefine((agent, ctx) => {
|
|
5212
|
+
if (agent.auth && !agent.auth.override) {
|
|
5213
|
+
for (const flag of ["strict", "exclusive"]) {
|
|
5214
|
+
if (agent.auth[flag]) {
|
|
5215
|
+
ctx.addIssue({
|
|
5216
|
+
code: exports_external.ZodIssueCode.custom,
|
|
5217
|
+
message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` — there is no pinned account for it to apply to.`,
|
|
5218
|
+
path: ["auth", flag]
|
|
5219
|
+
});
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5210
5223
|
if (agent.dm_only !== true)
|
|
5211
5224
|
return;
|
|
5212
5225
|
const tg = agent.channels?.telegram;
|
|
@@ -5319,6 +5332,13 @@ var init_schema = __esm(() => {
|
|
|
5319
5332
|
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
5320
5333
|
person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
|
|
5321
5334
|
});
|
|
5335
|
+
ConfigRepoConfigSchema = exports_external.object({
|
|
5336
|
+
enabled: exports_external.boolean().default(false).describe("Master switch for config-repo change control. Default false (opt-in). " + "When false the `config-repo sync` verb still runs on demand but the " + "doctor check and (later) the scheduled tick treat the feature as off."),
|
|
5337
|
+
path: exports_external.string().regex(/^[a-zA-Z0-9~._\-/]+$/, "config_repo.path must not contain shell-special characters ($, `, \", ', \\, etc.)").default("~/.switchroom-config").describe("Filesystem path to the operator's private config repo. Must be a git " + "repo (else `switchroom doctor` FAILs). Tilde-expanded at read time."),
|
|
5338
|
+
push: exports_external.boolean().default(true).describe("When true, `config-repo sync` pushes after committing (subject to the " + "`require_private` gate). When false, it commits locally only — useful " + "for offline hosts or a review-before-push workflow."),
|
|
5339
|
+
remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
|
|
5340
|
+
require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote — or when the API is unreachable — the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true.")
|
|
5341
|
+
});
|
|
5322
5342
|
SwitchroomConfigSchema = exports_external.object({
|
|
5323
5343
|
switchroom: exports_external.object({
|
|
5324
5344
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -5331,6 +5351,7 @@ var init_schema = __esm(() => {
|
|
|
5331
5351
|
memory: MemoryBackendConfigSchema.optional(),
|
|
5332
5352
|
hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) — takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
|
|
5333
5353
|
vault: VaultConfigSchema.optional(),
|
|
5354
|
+
config_repo: ConfigRepoConfigSchema.optional().describe("Change control for the operator's private ~/.switchroom-config git repo " + "(backup + audit trail of live host config, agent workspace state, and " + "mirrored personal skills). Consumed by `switchroom config-repo sync` and " + "the `config repo` doctor check. Optional and OFF by default."),
|
|
5334
5355
|
auth: exports_external.object({
|
|
5335
5356
|
active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
|
|
5336
5357
|
fallback_order: exports_external.array(exports_external.string().min(1)).optional().describe("Ordered list of account labels for `switchroom auth rotate` to cycle " + "through when the active account hits a quota event. First entry is " + "normally the same as `auth.active`. When unset, `rotate` is a no-op."),
|
|
@@ -5398,6 +5419,48 @@ var init_schema = __esm(() => {
|
|
|
5398
5419
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
5399
5420
|
checkServes(a.serves, ["agents", name, "serves"]);
|
|
5400
5421
|
}
|
|
5422
|
+
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
5423
|
+
const agentAuth = a.auth;
|
|
5424
|
+
if (!agentAuth?.exclusive || !agentAuth.override)
|
|
5425
|
+
continue;
|
|
5426
|
+
const acct = agentAuth.override;
|
|
5427
|
+
if (cfg.auth?.active === acct) {
|
|
5428
|
+
ctx.addIssue({
|
|
5429
|
+
code: exports_external.ZodIssueCode.custom,
|
|
5430
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
|
|
5431
|
+
path: ["auth", "active"]
|
|
5432
|
+
});
|
|
5433
|
+
}
|
|
5434
|
+
const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
|
|
5435
|
+
if (fbIdx !== -1) {
|
|
5436
|
+
ctx.addIssue({
|
|
5437
|
+
code: exports_external.ZodIssueCode.custom,
|
|
5438
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
|
|
5439
|
+
path: ["auth", "fallback_order", fbIdx]
|
|
5440
|
+
});
|
|
5441
|
+
}
|
|
5442
|
+
for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
|
|
5443
|
+
if (other === name)
|
|
5444
|
+
continue;
|
|
5445
|
+
const otherOverride = oa.auth?.override;
|
|
5446
|
+
if (otherOverride === acct) {
|
|
5447
|
+
ctx.addIssue({
|
|
5448
|
+
code: exports_external.ZodIssueCode.custom,
|
|
5449
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
|
|
5450
|
+
path: ["agents", other, "auth", "override"]
|
|
5451
|
+
});
|
|
5452
|
+
}
|
|
5453
|
+
}
|
|
5454
|
+
(cfg.auth?.consumers ?? []).forEach((c, i) => {
|
|
5455
|
+
if (c.account === acct) {
|
|
5456
|
+
ctx.addIssue({
|
|
5457
|
+
code: exports_external.ZodIssueCode.custom,
|
|
5458
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
|
|
5459
|
+
path: ["auth", "consumers", i, "account"]
|
|
5460
|
+
});
|
|
5461
|
+
}
|
|
5462
|
+
});
|
|
5463
|
+
}
|
|
5401
5464
|
const microsoftAccounts = cfg.microsoft_accounts;
|
|
5402
5465
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
5403
5466
|
const mw = a.microsoft_workspace;
|
|
@@ -4030,7 +4030,7 @@ var init_observation_scopes = __esm(() => {
|
|
|
4030
4030
|
});
|
|
4031
4031
|
|
|
4032
4032
|
// src/config/schema.ts
|
|
4033
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
4033
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
|
|
4034
4034
|
var init_schema = __esm(() => {
|
|
4035
4035
|
init_zod();
|
|
4036
4036
|
init_observation_scopes();
|
|
@@ -4727,8 +4727,10 @@ var init_schema = __esm(() => {
|
|
|
4727
4727
|
bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
|
|
4728
4728
|
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
|
|
4729
4729
|
auth: exports_external.object({
|
|
4730
|
-
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
|
|
4731
|
-
|
|
4730
|
+
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5."),
|
|
4731
|
+
strict: exports_external.boolean().optional().describe("Requires `override`. When true, the pin is a hard binding, not a " + "routing preference: the broker NEVER serves this agent from any " + "other account — no failover to `auth.fallback_order` while the " + "pinned account is quota-walled or exhausted. The agent rides out " + "the wall on its own account (surfacing the normal 429/quota " + "cards) instead of silently borrowing fleet quota. Use for " + "accounts that must never cross a billing/compliance boundary " + "(e.g. an employer-provided subscription)."),
|
|
4732
|
+
exclusive: exports_external.boolean().optional().describe("Requires `override`. When true, the pinned account belongs to " + "THIS agent alone: the broker refuses to serve it to any other " + "agent or consumer, refuses `auth use <label>` (set-active) onto " + "it, and refuses pinning another agent to it. Config that routes " + "others to the account (`auth.active`, `auth.fallback_order`, " + "another agent's `override`, a consumer pin) is rejected at load. " + "Usually paired with `strict: true` for full two-way isolation.")
|
|
4733
|
+
}).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases, " + "with optional `strict:` (never borrow another account) and " + "`exclusive:` (no one else may use the pinned account) hardening. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
|
|
4732
4734
|
dm_only: exports_external.boolean().optional().describe("Mark this agent as a DM-only bot — has its own bot_token and lives " + "exclusively in a private chat with the operator. Suppresses " + "scaffolding's default behavior of inheriting the global " + "telegram.forum_chat_id into the agent's access.json `groups` entry " + "(the forum chat the bot isn't a member of, which would otherwise " + "trigger a 'boot-probe-failed: 400 chat not found' warning every " + "restart). topic_name is still schema-required but unused — set it " + "to a display label like 'DM' for /switchroom status output."),
|
|
4733
4735
|
topic_name: exports_external.string().describe("Telegram forum topic display name"),
|
|
4734
4736
|
topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
|
|
@@ -4803,6 +4805,17 @@ var init_schema = __esm(() => {
|
|
|
4803
4805
|
tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional()
|
|
4804
4806
|
}).optional()
|
|
4805
4807
|
}).superRefine((agent, ctx) => {
|
|
4808
|
+
if (agent.auth && !agent.auth.override) {
|
|
4809
|
+
for (const flag of ["strict", "exclusive"]) {
|
|
4810
|
+
if (agent.auth[flag]) {
|
|
4811
|
+
ctx.addIssue({
|
|
4812
|
+
code: exports_external.ZodIssueCode.custom,
|
|
4813
|
+
message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` — there is no pinned account for it to apply to.`,
|
|
4814
|
+
path: ["auth", flag]
|
|
4815
|
+
});
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4806
4819
|
if (agent.dm_only !== true)
|
|
4807
4820
|
return;
|
|
4808
4821
|
const tg = agent.channels?.telegram;
|
|
@@ -4915,6 +4928,13 @@ var init_schema = __esm(() => {
|
|
|
4915
4928
|
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
4916
4929
|
person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
|
|
4917
4930
|
});
|
|
4931
|
+
ConfigRepoConfigSchema = exports_external.object({
|
|
4932
|
+
enabled: exports_external.boolean().default(false).describe("Master switch for config-repo change control. Default false (opt-in). " + "When false the `config-repo sync` verb still runs on demand but the " + "doctor check and (later) the scheduled tick treat the feature as off."),
|
|
4933
|
+
path: exports_external.string().regex(/^[a-zA-Z0-9~._\-/]+$/, "config_repo.path must not contain shell-special characters ($, `, \", ', \\, etc.)").default("~/.switchroom-config").describe("Filesystem path to the operator's private config repo. Must be a git " + "repo (else `switchroom doctor` FAILs). Tilde-expanded at read time."),
|
|
4934
|
+
push: exports_external.boolean().default(true).describe("When true, `config-repo sync` pushes after committing (subject to the " + "`require_private` gate). When false, it commits locally only — useful " + "for offline hosts or a review-before-push workflow."),
|
|
4935
|
+
remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
|
|
4936
|
+
require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote — or when the API is unreachable — the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true.")
|
|
4937
|
+
});
|
|
4918
4938
|
SwitchroomConfigSchema = exports_external.object({
|
|
4919
4939
|
switchroom: exports_external.object({
|
|
4920
4940
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -4927,6 +4947,7 @@ var init_schema = __esm(() => {
|
|
|
4927
4947
|
memory: MemoryBackendConfigSchema.optional(),
|
|
4928
4948
|
hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) — takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
|
|
4929
4949
|
vault: VaultConfigSchema.optional(),
|
|
4950
|
+
config_repo: ConfigRepoConfigSchema.optional().describe("Change control for the operator's private ~/.switchroom-config git repo " + "(backup + audit trail of live host config, agent workspace state, and " + "mirrored personal skills). Consumed by `switchroom config-repo sync` and " + "the `config repo` doctor check. Optional and OFF by default."),
|
|
4930
4951
|
auth: exports_external.object({
|
|
4931
4952
|
active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
|
|
4932
4953
|
fallback_order: exports_external.array(exports_external.string().min(1)).optional().describe("Ordered list of account labels for `switchroom auth rotate` to cycle " + "through when the active account hits a quota event. First entry is " + "normally the same as `auth.active`. When unset, `rotate` is a no-op."),
|
|
@@ -4994,6 +5015,48 @@ var init_schema = __esm(() => {
|
|
|
4994
5015
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
4995
5016
|
checkServes(a.serves, ["agents", name, "serves"]);
|
|
4996
5017
|
}
|
|
5018
|
+
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
5019
|
+
const agentAuth = a.auth;
|
|
5020
|
+
if (!agentAuth?.exclusive || !agentAuth.override)
|
|
5021
|
+
continue;
|
|
5022
|
+
const acct = agentAuth.override;
|
|
5023
|
+
if (cfg.auth?.active === acct) {
|
|
5024
|
+
ctx.addIssue({
|
|
5025
|
+
code: exports_external.ZodIssueCode.custom,
|
|
5026
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
|
|
5027
|
+
path: ["auth", "active"]
|
|
5028
|
+
});
|
|
5029
|
+
}
|
|
5030
|
+
const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
|
|
5031
|
+
if (fbIdx !== -1) {
|
|
5032
|
+
ctx.addIssue({
|
|
5033
|
+
code: exports_external.ZodIssueCode.custom,
|
|
5034
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
|
|
5035
|
+
path: ["auth", "fallback_order", fbIdx]
|
|
5036
|
+
});
|
|
5037
|
+
}
|
|
5038
|
+
for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
|
|
5039
|
+
if (other === name)
|
|
5040
|
+
continue;
|
|
5041
|
+
const otherOverride = oa.auth?.override;
|
|
5042
|
+
if (otherOverride === acct) {
|
|
5043
|
+
ctx.addIssue({
|
|
5044
|
+
code: exports_external.ZodIssueCode.custom,
|
|
5045
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
|
|
5046
|
+
path: ["agents", other, "auth", "override"]
|
|
5047
|
+
});
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
(cfg.auth?.consumers ?? []).forEach((c, i) => {
|
|
5051
|
+
if (c.account === acct) {
|
|
5052
|
+
ctx.addIssue({
|
|
5053
|
+
code: exports_external.ZodIssueCode.custom,
|
|
5054
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
|
|
5055
|
+
path: ["auth", "consumers", i, "account"]
|
|
5056
|
+
});
|
|
5057
|
+
}
|
|
5058
|
+
});
|
|
5059
|
+
}
|
|
4997
5060
|
const microsoftAccounts = cfg.microsoft_accounts;
|
|
4998
5061
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
4999
5062
|
const mw = a.microsoft_workspace;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.20.
|
|
4
|
+
"version": "0.20.11",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -1692,6 +1692,55 @@ if ! ls -d "$sr_cb_dir"/chromium-*/ >/dev/null 2>&1; then
|
|
|
1692
1692
|
fi
|
|
1693
1693
|
unset sr_cb_dir
|
|
1694
1694
|
|
|
1695
|
+
# Playwright browsers cache seeding. The agent image bakes the fleet-default
|
|
1696
|
+
# Playwright version's chromium at /opt/playwright/browsers (immutable:
|
|
1697
|
+
# root-owned inside a `read_only: true` root fs), but PLAYWRIGHT_BROWSERS_PATH
|
|
1698
|
+
# points at the persistent per-agent HOME cache (Dockerfile.agent ENV) so a
|
|
1699
|
+
# project pinning a DIFFERENT Playwright version can `playwright install` its
|
|
1700
|
+
# matching browser revision — revision-keyed dirs coexist side by side, and
|
|
1701
|
+
# the install survives container recreate via the HOME bind mount. Symlink
|
|
1702
|
+
# each baked revision into the cache so the fleet default (@playwright/mcp,
|
|
1703
|
+
# webapp-testing skill) still resolves with ZERO download; a real
|
|
1704
|
+
# agent-installed dir with the same name always wins over the link. Dangling
|
|
1705
|
+
# links into the bake (stale after an image bump changed the default
|
|
1706
|
+
# revision) are swept first. Dot-entries (.links) are untouched by the glob —
|
|
1707
|
+
# they must stay real + writable so project installs can register there.
|
|
1708
|
+
#
|
|
1709
|
+
# Zero-download depends on TWO things, not just this seeding. (1) Playwright
|
|
1710
|
+
# decides "already installed / skip download" purely by the INSTALLATION_COMPLETE
|
|
1711
|
+
# marker inside each revision dir (playwright-core browserFetcher.ts) — which
|
|
1712
|
+
# resolves fine through these symlinks, so the seed alone is enough at launch.
|
|
1713
|
+
# (2) BUT `playwright install` first GC-sweeps the browsers dir, deleting any
|
|
1714
|
+
# revision not referenced by a `.links/<sha1>` entry. The baked packages
|
|
1715
|
+
# registered their reference in /opt/.../.links at build time, not here, so a
|
|
1716
|
+
# skewed-version project install would otherwise GC these seed links and force a
|
|
1717
|
+
# re-download. Dockerfile.agent sets PLAYWRIGHT_SKIP_BROWSER_GC=1 in the runtime
|
|
1718
|
+
# env to disable that sweep — keep the two in lockstep. (`.links` is still
|
|
1719
|
+
# written by every install; we just never run the removal pass.)
|
|
1720
|
+
if [ -d /opt/playwright/browsers ]; then
|
|
1721
|
+
sr_pw_cache="${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}"
|
|
1722
|
+
mkdir -p "$sr_pw_cache" 2>/dev/null || true
|
|
1723
|
+
if [ -d "$sr_pw_cache" ] && [ -w "$sr_pw_cache" ]; then
|
|
1724
|
+
for sr_pw_link in "$sr_pw_cache"/*; do
|
|
1725
|
+
if [ -L "$sr_pw_link" ] && [ ! -e "$sr_pw_link" ]; then
|
|
1726
|
+
case "$(readlink "$sr_pw_link")" in
|
|
1727
|
+
/opt/playwright/browsers/*) rm -f "$sr_pw_link" ;;
|
|
1728
|
+
esac
|
|
1729
|
+
fi
|
|
1730
|
+
done
|
|
1731
|
+
for sr_pw_rev in /opt/playwright/browsers/*/; do
|
|
1732
|
+
[ -d "$sr_pw_rev" ] || continue
|
|
1733
|
+
sr_pw_name="$(basename "$sr_pw_rev")"
|
|
1734
|
+
if [ ! -e "$sr_pw_cache/$sr_pw_name" ]; then
|
|
1735
|
+
ln -s "${sr_pw_rev%/}" "$sr_pw_cache/$sr_pw_name" 2>/dev/null || true
|
|
1736
|
+
fi
|
|
1737
|
+
done
|
|
1738
|
+
else
|
|
1739
|
+
echo "WARNING (playwright cache): $sr_pw_cache missing or not writable — baked browsers not linked; playwright-based skills may attempt a fresh ~150MB download" >&2
|
|
1740
|
+
fi
|
|
1741
|
+
unset sr_pw_cache sr_pw_rev sr_pw_name sr_pw_link
|
|
1742
|
+
fi
|
|
1743
|
+
|
|
1695
1744
|
# LiteLLM routing (opt-in, #litellm). When SWITCHROOM_LITELLM is set (compose
|
|
1696
1745
|
# env, gated on litellm.enabled && keyConfirmed), route the unmodified `claude`
|
|
1697
1746
|
# CLI through the operator's LiteLLM proxy at ANTHROPIC_BASE_URL: fetch the
|
|
@@ -48,16 +48,12 @@ hot-reloads, no restart needed.
|
|
|
48
48
|
crons. `context: "agent"` pins a fire here when it genuinely needs your
|
|
49
49
|
accumulated context (this always wins).
|
|
50
50
|
- **Model-free tiers — you must REQUEST these from the operator** (they need a
|
|
51
|
-
config commit
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- **`kind: poll`** for "only act when X changes" — a model-free check where
|
|
58
|
-
only a *change* wakes you. Don't burn a frequent prompt cron on polling.
|
|
59
|
-
- **`reaction_dispatch`** for reaction-triggered work — an emoji reaction
|
|
60
|
-
wakes you instantly, zero polling.
|
|
51
|
+
config commit; you can only self-author plain prompt crons plus the
|
|
52
|
+
`model`/`context` hints): **`kind: action`** posts a FIXED text at zero
|
|
53
|
+
tokens (renders full GFM — set `parse_mode: text` for verbatim);
|
|
54
|
+
**`kind: poll`** is a model-free check where only a *change* wakes you —
|
|
55
|
+
don't burn a frequent prompt cron on polling; **`reaction_dispatch`** wakes
|
|
56
|
+
you instantly on an emoji reaction, zero polling.
|
|
61
57
|
|
|
62
58
|
Cron-fired sessions render rich markdown, so scheduled/digest output should use
|
|
63
59
|
**bold** headers and `code` for identifiers, just like a live reply.
|
|
@@ -87,11 +83,10 @@ fork is durable and auto-mirrors to `~/.switchroom-config/` when present.
|
|
|
87
83
|
|
|
88
84
|
### Confirm honestly
|
|
89
85
|
|
|
90
|
-
After a successful `schedule_add`, tell the user the human-readable schedule
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
tripped, and offer the closest legal alternative.
|
|
86
|
+
After a successful `schedule_add`, tell the user the human-readable schedule,
|
|
87
|
+
the cron expression you actually wrote, and the `name` slug (so they can
|
|
88
|
+
remove it later). After a failed write, surface the error verbatim, name
|
|
89
|
+
which rail tripped, and offer the closest legal alternative.
|
|
95
90
|
{{#if linearAgentEnabled}}
|
|
96
91
|
|
|
97
92
|
## Capture to Linear (👨💻 / 📌 reaction → new issue)
|
|
@@ -157,10 +152,8 @@ config or paste them into a normal message; pass them straight to the tool.
|
|
|
157
152
|
|
|
158
153
|
### Don't lie about scheduling
|
|
159
154
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
works. Don't claim "I've set a one-time reminder" and then leave a
|
|
166
|
-
recurring entry running silently.
|
|
155
|
+
Cron can't encode a one-shot — every entry recurs. For "at 5pm tomorrow,
|
|
156
|
+
remind me": either (a) schedule it recurring and offer to remove it after
|
|
157
|
+
the first fire, or (b) say one-shot isn't supported and ask what cadence
|
|
158
|
+
works. Never claim "one-time reminder" while leaving a recurring entry
|
|
159
|
+
running silently.
|
|
@@ -4,6 +4,6 @@ Execution belongs in sub-agents. **When in doubt, delegate.** A task is executio
|
|
|
4
4
|
|
|
5
5
|
Acting in-turn and delegating are the same move: for an execution-class task the in-turn act IS dispatching the worker, so dispatch and acknowledge — that IS the fast response, not a slower one. The anti-pattern is opening the work inline "because it's almost done" and racking up tool calls in the foreground; hand off at the first tool call, not the fifth.
|
|
6
6
|
|
|
7
|
-
If the user amends in-flight delegated work mid-turn, steer the running worker now (`SendMessage` by worker name
|
|
7
|
+
If the user amends in-flight delegated work mid-turn, steer the running worker now (`SendMessage` by worker name or agent id) rather than holding it for handback — and say in your reply which you did ("folded your update into the running worker" / "queued as a separate task"); never classify silently, never inferred. Unsure? Queue it and say so — queue is the default. If the worker is effectively done and the steer lands too late, say so and apply the update yourself in the parent.
|
|
8
8
|
|
|
9
9
|
If no sub-agents are configured, do the work yourself.
|
|
@@ -9,6 +9,6 @@ This governs HOW delegated work is done, and is **not a license to do it inline*
|
|
|
9
9
|
- **Design report before implementation, when the change is large.** Architecturally significant, cross-cutting, or ambiguous-approach work earns an evidence-grounded design report and a red-team pass before code, and ships as focused single-concern PRs.
|
|
10
10
|
- **Run `git fetch origin` first, then branch off `origin/main`.** Never branch off the working copy as-is: you do not start in the repo you are coding in, and the checkout on disk may be many commits stale. Scoped tests + lint locally; CI is the full-suite authority.
|
|
11
11
|
- **Adversarial review of the diff.** Read every change as an adversary would: what breaks, what's untested, what's inconsistent.
|
|
12
|
-
- **Blockers and majors block the merge; lows
|
|
12
|
+
- **Blockers and majors block the merge; prefer fixing lows before merge too** — ship reliable code with a slight delay over filing tech debt that never gets addressed. Fix a low now unless it's genuinely out of scope or disproportionate to the change; only then file it as a tracked follow-up (filing is mandatory, and a filed low is a commitment to fix it, not a way to forget it). Fix what blocks, then merge on CI green. Do not count review rounds and do not run a mandatory re-review pass: verifying your own fix is part of making it, not a separate step. When that fix-vs-defer call is a genuine judgment call, don't decide it silently — surface it to the user as choice / user-impact / recommendation: name the options (fix now vs defer), their user-outcome impact, and which you recommend.
|
|
13
13
|
- **Durable fixes over hack patches. Deterministic mechanisms over model-dependent behaviour** — if code can enforce a guarantee, don't leave it to prompt discipline. **Tests assert outcomes, not just code paths:** one that wouldn't fail on the bug it guards is not a test.
|
|
14
14
|
- **Communicate as you go.** Consolidated messages — one substantive update beats five fragments; never go dark; no foreground watch over 30 seconds (background it); max 15 parallel sub-agents.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
## Grounding — check before you assert
|
|
2
2
|
|
|
3
|
-
Your memory and prior context are leads, not facts. Anything you state as true —
|
|
3
|
+
Your memory and prior context are leads, not facts. Anything you state as true — code, config, a system's state, a number, a status, or what you can and can't do — must come from something you actually checked this turn.
|
|
4
4
|
|
|
5
|
-
- **Read the ground truth live.**
|
|
6
|
-
- **
|
|
7
|
-
- **Don't assert a limit you haven't tested.** Before saying "no access" / "operator-only" / "not editable"
|
|
5
|
+
- **Read the ground truth live.** Files, git status, config, versions, running services, schedules, permissions — read them with a tool, don't recall them.
|
|
6
|
+
- **Training memory is a lead, not a source — prefer finding over guessing.** A library's API, a version, a CLI flag, whether a repo exists — but equally a real-world fact: who a person is, what a company does, a product, a current event: validate against the real thing (the web, docs, the repo) before you assert it. Never answer "I don't recognize X" from model recall when X is one search away.
|
|
7
|
+
- **Don't assert a limit you haven't tested.** Before saying "no access" / "operator-only" / "not editable", verify it live. A fabricated boundary is as wrong as a fabricated fact.
|
|
8
8
|
- **A weak or empty result isn't a conclusion.** Vary the query, path, command, or source before deciding something doesn't exist.
|
|
9
9
|
- **Final answers carry evidence** — tool output, a file you read, a check you ran, or a named blocker; not "it should work."
|
|
10
10
|
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Switchroom-managed: vault protocol.
|
|
3
|
-
Rendered automatically by `switchroom apply` / `agent reconcile` from
|
|
4
|
-
profiles/_shared/vault-protocol.md.hbs. Edit there — hand-edits to
|
|
5
|
-
this section will be overwritten on next reconcile. To add agent-
|
|
6
|
-
specific guidance, edit CLAUDE.md below the "Yours (preserved across
|
|
7
|
-
apply)" marker line — that section is preserved verbatim across every
|
|
8
|
-
apply.
|
|
9
|
-
-->
|
|
1
|
+
<!-- Switchroom-managed: rendered from profiles/_shared/vault-protocol.md.hbs — edit there; hand-edits here are overwritten on reconcile. -->
|
|
10
2
|
|
|
11
3
|
## Vault & secrets
|
|
12
4
|
|
|
@@ -65,15 +57,7 @@ there's an operator in the loop:
|
|
|
65
57
|
|
|
66
58
|
### When a sub-agent or tool reports a service/API/credential as "inaccessible"
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Don't escalate to the operator to paste credentials or open a dashboard. Instead:
|
|
71
|
-
|
|
72
|
-
1. Infer the likely key name (`<service>/<key>` convention — e.g. `coolify/api-token`, `github/token`, `<repo>/DATABASE_URL`).
|
|
73
|
-
2. Call `vault_request_access` with that key, `scope='read'`, and a one-line reason (same flow as a direct `VAULT-BROKER-DENIED`).
|
|
74
|
-
3. End your turn cleanly and resume when the grant is approved.
|
|
75
|
-
|
|
76
|
-
Only escalate to the operator if the grant is explicitly denied, or if you cannot infer a plausible key name.
|
|
60
|
+
That's the broker wall surfacing as prose — treat it as `VAULT-BROKER-DENIED`. Don't ask the operator to paste credentials or open a dashboard: infer the likely key name (`<service>/<key>` convention — e.g. `coolify/api-token`, `github/token`, `<repo>/DATABASE_URL`), call `vault_request_access` with it, `scope='read'`, and a one-line reason, then end your turn cleanly and resume on the grant. Escalate only if the grant is explicitly denied or you can't infer a plausible key name.
|
|
77
61
|
|
|
78
62
|
### Hint: the deny stderr tells you the exact recovery
|
|
79
63
|
|