switchroom 0.20.10 → 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/dist/agent-scheduler/index.js +65 -2
- package/dist/auth-broker/index.js +185 -23
- 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 +3669 -2107
- 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/_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 +1405 -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/scripts/retain.py +306 -0
- 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
|
@@ -11666,8 +11666,10 @@ var AgentSchema = exports_external.object({
|
|
|
11666
11666
|
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')."),
|
|
11667
11667
|
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."),
|
|
11668
11668
|
auth: exports_external.object({
|
|
11669
|
-
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.")
|
|
11670
|
-
|
|
11669
|
+
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."),
|
|
11670
|
+
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)."),
|
|
11671
|
+
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.")
|
|
11672
|
+
}).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)."),
|
|
11671
11673
|
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."),
|
|
11672
11674
|
topic_name: exports_external.string().describe("Telegram forum topic display name"),
|
|
11673
11675
|
topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
|
|
@@ -11742,6 +11744,17 @@ var AgentSchema = exports_external.object({
|
|
|
11742
11744
|
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()
|
|
11743
11745
|
}).optional()
|
|
11744
11746
|
}).superRefine((agent, ctx) => {
|
|
11747
|
+
if (agent.auth && !agent.auth.override) {
|
|
11748
|
+
for (const flag of ["strict", "exclusive"]) {
|
|
11749
|
+
if (agent.auth[flag]) {
|
|
11750
|
+
ctx.addIssue({
|
|
11751
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11752
|
+
message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` — there is no pinned account for it to apply to.`,
|
|
11753
|
+
path: ["auth", flag]
|
|
11754
|
+
});
|
|
11755
|
+
}
|
|
11756
|
+
}
|
|
11757
|
+
}
|
|
11745
11758
|
if (agent.dm_only !== true)
|
|
11746
11759
|
return;
|
|
11747
11760
|
const tg = agent.channels?.telegram;
|
|
@@ -11854,6 +11867,13 @@ var UserSchema = exports_external.object({
|
|
|
11854
11867
|
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
11855
11868
|
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.")
|
|
11856
11869
|
});
|
|
11870
|
+
var ConfigRepoConfigSchema = exports_external.object({
|
|
11871
|
+
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."),
|
|
11872
|
+
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."),
|
|
11873
|
+
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."),
|
|
11874
|
+
remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
|
|
11875
|
+
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.")
|
|
11876
|
+
});
|
|
11857
11877
|
var SwitchroomConfigSchema = exports_external.object({
|
|
11858
11878
|
switchroom: exports_external.object({
|
|
11859
11879
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -11866,6 +11886,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11866
11886
|
memory: MemoryBackendConfigSchema.optional(),
|
|
11867
11887
|
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`."),
|
|
11868
11888
|
vault: VaultConfigSchema.optional(),
|
|
11889
|
+
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."),
|
|
11869
11890
|
auth: exports_external.object({
|
|
11870
11891
|
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>`."),
|
|
11871
11892
|
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."),
|
|
@@ -11933,6 +11954,48 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11933
11954
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11934
11955
|
checkServes(a.serves, ["agents", name, "serves"]);
|
|
11935
11956
|
}
|
|
11957
|
+
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11958
|
+
const agentAuth = a.auth;
|
|
11959
|
+
if (!agentAuth?.exclusive || !agentAuth.override)
|
|
11960
|
+
continue;
|
|
11961
|
+
const acct = agentAuth.override;
|
|
11962
|
+
if (cfg.auth?.active === acct) {
|
|
11963
|
+
ctx.addIssue({
|
|
11964
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11965
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
|
|
11966
|
+
path: ["auth", "active"]
|
|
11967
|
+
});
|
|
11968
|
+
}
|
|
11969
|
+
const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
|
|
11970
|
+
if (fbIdx !== -1) {
|
|
11971
|
+
ctx.addIssue({
|
|
11972
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11973
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
|
|
11974
|
+
path: ["auth", "fallback_order", fbIdx]
|
|
11975
|
+
});
|
|
11976
|
+
}
|
|
11977
|
+
for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
|
|
11978
|
+
if (other === name)
|
|
11979
|
+
continue;
|
|
11980
|
+
const otherOverride = oa.auth?.override;
|
|
11981
|
+
if (otherOverride === acct) {
|
|
11982
|
+
ctx.addIssue({
|
|
11983
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11984
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
|
|
11985
|
+
path: ["agents", other, "auth", "override"]
|
|
11986
|
+
});
|
|
11987
|
+
}
|
|
11988
|
+
}
|
|
11989
|
+
(cfg.auth?.consumers ?? []).forEach((c, i) => {
|
|
11990
|
+
if (c.account === acct) {
|
|
11991
|
+
ctx.addIssue({
|
|
11992
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11993
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
|
|
11994
|
+
path: ["auth", "consumers", i, "account"]
|
|
11995
|
+
});
|
|
11996
|
+
}
|
|
11997
|
+
});
|
|
11998
|
+
}
|
|
11936
11999
|
const microsoftAccounts = cfg.microsoft_accounts;
|
|
11937
12000
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11938
12001
|
const mw = a.microsoft_workspace;
|
|
@@ -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;
|
|
@@ -22183,11 +22246,30 @@ class AuthBroker {
|
|
|
22183
22246
|
return override;
|
|
22184
22247
|
return auth.active ?? null;
|
|
22185
22248
|
}
|
|
22249
|
+
exclusiveOwnerOf(account) {
|
|
22250
|
+
for (const [name, agent] of Object.entries(this.config.agents ?? {})) {
|
|
22251
|
+
if (agent.auth?.exclusive && agent.auth?.override === account)
|
|
22252
|
+
return name;
|
|
22253
|
+
}
|
|
22254
|
+
return null;
|
|
22255
|
+
}
|
|
22256
|
+
isServableBy(account, forAgent) {
|
|
22257
|
+
const owner = this.exclusiveOwnerOf(account);
|
|
22258
|
+
return owner === null || owner === forAgent;
|
|
22259
|
+
}
|
|
22186
22260
|
servingAccount(identity) {
|
|
22187
|
-
const account = this.callerAccount(identity);
|
|
22188
22261
|
if (identity.kind === "operator")
|
|
22189
|
-
return
|
|
22190
|
-
|
|
22262
|
+
return this.callerAccount(identity);
|
|
22263
|
+
if (identity.kind === "agent")
|
|
22264
|
+
return this.servedAccountForAgent(identity.name);
|
|
22265
|
+
return this.accountWithFailover(this.callerAccount(identity));
|
|
22266
|
+
}
|
|
22267
|
+
servedAccountForAgent(name) {
|
|
22268
|
+
const agent = (this.config.agents ?? {})[name];
|
|
22269
|
+
const override = agent?.auth?.override;
|
|
22270
|
+
if (override && agent?.auth?.strict)
|
|
22271
|
+
return override;
|
|
22272
|
+
return this.accountWithFailover(override ?? this.config.auth?.active ?? null, name);
|
|
22191
22273
|
}
|
|
22192
22274
|
isOverageAllowed(account) {
|
|
22193
22275
|
return (this.config.auth?.allow_overage_accounts ?? []).includes(account);
|
|
@@ -22305,6 +22387,8 @@ class AuthBroker {
|
|
|
22305
22387
|
continue;
|
|
22306
22388
|
if (this.isAccountPremiumWalled(cand))
|
|
22307
22389
|
continue;
|
|
22390
|
+
if (this.exclusiveOwnerOf(cand))
|
|
22391
|
+
continue;
|
|
22308
22392
|
if (!accountExists(cand, this.home))
|
|
22309
22393
|
continue;
|
|
22310
22394
|
if (!readAccountCredentials(cand, this.home))
|
|
@@ -22450,15 +22534,30 @@ class AuthBroker {
|
|
|
22450
22534
|
if (reselected && this.accountEligibilityOf(reselected) === "eligible")
|
|
22451
22535
|
return reselected;
|
|
22452
22536
|
for (const cand of ring) {
|
|
22453
|
-
if (cand && cand !== current && accountExists(cand, this.home) && this.accountEligibilityOf(cand) === "unknown") {
|
|
22537
|
+
if (cand && cand !== current && accountExists(cand, this.home) && !this.exclusiveOwnerOf(cand) && this.accountEligibilityOf(cand) === "unknown") {
|
|
22454
22538
|
return cand;
|
|
22455
22539
|
}
|
|
22456
22540
|
}
|
|
22457
22541
|
return null;
|
|
22458
22542
|
}
|
|
22459
|
-
accountWithFailover(account) {
|
|
22543
|
+
accountWithFailover(account, forAgent) {
|
|
22460
22544
|
if (!account)
|
|
22461
22545
|
return null;
|
|
22546
|
+
if (!this.isServableBy(account, forAgent)) {
|
|
22547
|
+
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22548
|
+
if (cand === account || this.isAccountExhausted(cand))
|
|
22549
|
+
continue;
|
|
22550
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22551
|
+
continue;
|
|
22552
|
+
if (readAccountCredentials(cand, this.home))
|
|
22553
|
+
return cand;
|
|
22554
|
+
}
|
|
22555
|
+
const active2 = this.config.auth?.active;
|
|
22556
|
+
if (active2 && active2 !== account && !this.isAccountExhausted(active2) && this.isServableBy(active2, forAgent) && readAccountCredentials(active2, this.home)) {
|
|
22557
|
+
return active2;
|
|
22558
|
+
}
|
|
22559
|
+
return null;
|
|
22560
|
+
}
|
|
22462
22561
|
const exhausted = this.isAccountExhausted(account);
|
|
22463
22562
|
if (!exhausted && !this.isAccountSoftAvoided(account))
|
|
22464
22563
|
return account;
|
|
@@ -22466,6 +22565,8 @@ class AuthBroker {
|
|
|
22466
22565
|
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22467
22566
|
if (cand === account || this.isAccountExhausted(cand))
|
|
22468
22567
|
continue;
|
|
22568
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22569
|
+
continue;
|
|
22469
22570
|
if (this.isAccountSoftAvoided(cand)) {
|
|
22470
22571
|
softAvoidExcludedAny = true;
|
|
22471
22572
|
continue;
|
|
@@ -22479,6 +22580,8 @@ class AuthBroker {
|
|
|
22479
22580
|
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22480
22581
|
if (cand === account || this.isAccountExhausted(cand))
|
|
22481
22582
|
continue;
|
|
22583
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22584
|
+
continue;
|
|
22482
22585
|
if (!readAccountCredentials(cand, this.home))
|
|
22483
22586
|
continue;
|
|
22484
22587
|
const score = this.softAvoidUtilScore(cand);
|
|
@@ -22493,12 +22596,14 @@ class AuthBroker {
|
|
|
22493
22596
|
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22494
22597
|
if (cand === account || this.isAccountExhausted(cand))
|
|
22495
22598
|
continue;
|
|
22599
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22600
|
+
continue;
|
|
22496
22601
|
if (readAccountCredentials(cand, this.home))
|
|
22497
22602
|
return cand;
|
|
22498
22603
|
}
|
|
22499
22604
|
}
|
|
22500
22605
|
const active = this.config.auth?.active;
|
|
22501
|
-
if (active && active !== account && !this.isAccountExhausted(active) && readAccountCredentials(active, this.home)) {
|
|
22606
|
+
if (active && active !== account && !this.isAccountExhausted(active) && this.isServableBy(active, forAgent) && readAccountCredentials(active, this.home)) {
|
|
22502
22607
|
return active;
|
|
22503
22608
|
}
|
|
22504
22609
|
return account;
|
|
@@ -22550,7 +22655,9 @@ class AuthBroker {
|
|
|
22550
22655
|
const agents = Object.entries(this.config.agents ?? {}).map(([name, agent]) => {
|
|
22551
22656
|
const override = agent.auth?.override ?? null;
|
|
22552
22657
|
const account = override ?? auth.active ?? "";
|
|
22553
|
-
|
|
22658
|
+
const strict = Boolean(override && agent.auth?.strict);
|
|
22659
|
+
const exclusive = Boolean(override && agent.auth?.exclusive);
|
|
22660
|
+
return { name, account, override, strict, exclusive };
|
|
22554
22661
|
});
|
|
22555
22662
|
const consumers = (auth.consumers ?? []).map((c) => ({
|
|
22556
22663
|
name: c.name,
|
|
@@ -22877,6 +22984,12 @@ class AuthBroker {
|
|
|
22877
22984
|
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
|
|
22878
22985
|
return;
|
|
22879
22986
|
}
|
|
22987
|
+
const exclusiveOwner = this.exclusiveOwnerOf(account);
|
|
22988
|
+
if (exclusiveOwner) {
|
|
22989
|
+
this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: false, error: "exclusive-account" });
|
|
22990
|
+
socket.write(encodeError(id, "FORBIDDEN", `account '${account}' is exclusive to agent '${exclusiveOwner}' ` + `(agents.${exclusiveOwner}.auth.exclusive) — it cannot be the fleet active. ` + `Clear the exclusive flag in switchroom.yaml first if you really mean this.`));
|
|
22991
|
+
return;
|
|
22992
|
+
}
|
|
22880
22993
|
const cfg = {
|
|
22881
22994
|
...this.config,
|
|
22882
22995
|
auth: { ...this.config.auth ?? {}, active: account }
|
|
@@ -22888,6 +23001,15 @@ class AuthBroker {
|
|
|
22888
23001
|
this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: true });
|
|
22889
23002
|
socket.write(encodeSuccess(id, { active: account, fanned }));
|
|
22890
23003
|
}
|
|
23004
|
+
callerStrictPinned(identity) {
|
|
23005
|
+
if (identity.kind !== "agent")
|
|
23006
|
+
return false;
|
|
23007
|
+
const a = (this.config.agents ?? {})[identity.name]?.auth;
|
|
23008
|
+
return Boolean(a?.override && a?.strict);
|
|
23009
|
+
}
|
|
23010
|
+
callerRolledTo(identity, rolledTo) {
|
|
23011
|
+
return this.callerStrictPinned(identity) ? null : rolledTo;
|
|
23012
|
+
}
|
|
22891
23013
|
async opMarkExhausted(socket, id, identity, until) {
|
|
22892
23014
|
const account = this.callerAccount(identity);
|
|
22893
23015
|
if (!account) {
|
|
@@ -22897,7 +23019,12 @@ class AuthBroker {
|
|
|
22897
23019
|
}
|
|
22898
23020
|
const { rolled, rolledTo } = await this.markExhaustedAndRoll(account, until, identity);
|
|
22899
23021
|
this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true });
|
|
22900
|
-
socket.write(encodeSuccess(id, {
|
|
23022
|
+
socket.write(encodeSuccess(id, {
|
|
23023
|
+
account,
|
|
23024
|
+
rolled,
|
|
23025
|
+
rolledTo: this.callerRolledTo(identity, rolledTo),
|
|
23026
|
+
caller_pinned_strict: this.callerStrictPinned(identity)
|
|
23027
|
+
}));
|
|
22901
23028
|
}
|
|
22902
23029
|
async opMarkThrottled(socket, id, identity, until, probeOnly = false) {
|
|
22903
23030
|
const account = this.callerAccount(identity);
|
|
@@ -22914,7 +23041,8 @@ class AuthBroker {
|
|
|
22914
23041
|
account,
|
|
22915
23042
|
throttled_until: this.quota[account]?.throttled_until ?? 0,
|
|
22916
23043
|
escalated: escalated2,
|
|
22917
|
-
rolledTo: escalated2 ? rolledTo2 : null
|
|
23044
|
+
rolledTo: escalated2 ? this.callerRolledTo(identity, rolledTo2) : null,
|
|
23045
|
+
caller_pinned_strict: this.callerStrictPinned(identity)
|
|
22918
23046
|
}));
|
|
22919
23047
|
return;
|
|
22920
23048
|
}
|
|
@@ -22957,7 +23085,8 @@ class AuthBroker {
|
|
|
22957
23085
|
account,
|
|
22958
23086
|
throttled_until: throttledUntil,
|
|
22959
23087
|
escalated,
|
|
22960
|
-
rolledTo: escalated ? rolledTo : null
|
|
23088
|
+
rolledTo: escalated ? this.callerRolledTo(identity, rolledTo) : null,
|
|
23089
|
+
caller_pinned_strict: this.callerStrictPinned(identity)
|
|
22961
23090
|
}));
|
|
22962
23091
|
}
|
|
22963
23092
|
async maybeEscalateThrottle(account, identity, now) {
|
|
@@ -23529,6 +23658,22 @@ class AuthBroker {
|
|
|
23529
23658
|
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
|
|
23530
23659
|
return;
|
|
23531
23660
|
}
|
|
23661
|
+
if (account !== null) {
|
|
23662
|
+
const exclusiveOwner = this.exclusiveOwnerOf(account);
|
|
23663
|
+
if (exclusiveOwner && exclusiveOwner !== agentName) {
|
|
23664
|
+
this.audit({ op: "set-override", identity, account, accountKind: "claude", ok: false, error: "exclusive-account" });
|
|
23665
|
+
socket.write(encodeError(id, "FORBIDDEN", `account '${account}' is exclusive to agent '${exclusiveOwner}' ` + `(agents.${exclusiveOwner}.auth.exclusive) — agent '${agentName}' cannot pin it.`));
|
|
23666
|
+
return;
|
|
23667
|
+
}
|
|
23668
|
+
}
|
|
23669
|
+
{
|
|
23670
|
+
const curAuth = (this.config.agents ?? {})[agentName]?.auth;
|
|
23671
|
+
if ((curAuth?.strict || curAuth?.exclusive) && account !== (curAuth?.override ?? null)) {
|
|
23672
|
+
this.audit({ op: "set-override", identity, account: account ?? undefined, accountKind: "claude", ok: false, error: "yaml-pinned" });
|
|
23673
|
+
socket.write(encodeError(id, "FORBIDDEN", `agent '${agentName}' has a strict/exclusive pin declared in switchroom.yaml — ` + `edit the yaml (agents.${agentName}.auth) and restart the broker to change it.`));
|
|
23674
|
+
return;
|
|
23675
|
+
}
|
|
23676
|
+
}
|
|
23532
23677
|
const agents = { ...this.config.agents ?? {} };
|
|
23533
23678
|
const cur = agents[agentName];
|
|
23534
23679
|
const auth = { ...cur.auth ?? {} };
|
|
@@ -23704,10 +23849,9 @@ class AuthBroker {
|
|
|
23704
23849
|
return out;
|
|
23705
23850
|
}
|
|
23706
23851
|
fanoutToAffectedAgents(label) {
|
|
23707
|
-
const auth = this.config.auth ?? {};
|
|
23708
23852
|
const fanned = [];
|
|
23709
|
-
for (const
|
|
23710
|
-
const effective = this.
|
|
23853
|
+
for (const name of Object.keys(this.config.agents ?? {})) {
|
|
23854
|
+
const effective = this.servedAccountForAgent(name);
|
|
23711
23855
|
if (effective === label) {
|
|
23712
23856
|
if (this.fanoutForAgent(name))
|
|
23713
23857
|
fanned.push(name);
|
|
@@ -23726,10 +23870,9 @@ class AuthBroker {
|
|
|
23726
23870
|
const isEffective = effective === label;
|
|
23727
23871
|
if (!isPinned && !isEffective)
|
|
23728
23872
|
continue;
|
|
23729
|
-
|
|
23730
|
-
if (toMirror == null)
|
|
23873
|
+
if (effective == null)
|
|
23731
23874
|
continue;
|
|
23732
|
-
if (this.mirrorAccountToConsumer(
|
|
23875
|
+
if (this.mirrorAccountToConsumer(effective, consumer)) {
|
|
23733
23876
|
fanned.push(consumer.name);
|
|
23734
23877
|
}
|
|
23735
23878
|
}
|
|
@@ -23830,6 +23973,10 @@ class AuthBroker {
|
|
|
23830
23973
|
const effective = agent.auth?.override ?? auth.active;
|
|
23831
23974
|
if (effective !== label)
|
|
23832
23975
|
continue;
|
|
23976
|
+
if (agent.auth?.override && agent.auth?.strict)
|
|
23977
|
+
continue;
|
|
23978
|
+
if (!this.isServableBy(next, name))
|
|
23979
|
+
continue;
|
|
23833
23980
|
if (this.mirrorAccountToAgent(next, name)) {
|
|
23834
23981
|
rolled.push(name);
|
|
23835
23982
|
}
|
|
@@ -23838,8 +23985,13 @@ class AuthBroker {
|
|
|
23838
23985
|
}
|
|
23839
23986
|
nextHealthyAccount(current, order) {
|
|
23840
23987
|
const start = order.indexOf(current);
|
|
23841
|
-
if (start === -1)
|
|
23842
|
-
|
|
23988
|
+
if (start === -1) {
|
|
23989
|
+
for (const cand of order) {
|
|
23990
|
+
if (cand && !this.exclusiveOwnerOf(cand))
|
|
23991
|
+
return cand;
|
|
23992
|
+
}
|
|
23993
|
+
return null;
|
|
23994
|
+
}
|
|
23843
23995
|
let bestSoftAvoided = null;
|
|
23844
23996
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
23845
23997
|
for (let i = 1;i <= order.length; i++) {
|
|
@@ -23848,6 +24000,8 @@ class AuthBroker {
|
|
|
23848
24000
|
continue;
|
|
23849
24001
|
if (this.isAccountExhausted(cand) || !accountExists(cand, this.home))
|
|
23850
24002
|
continue;
|
|
24003
|
+
if (this.exclusiveOwnerOf(cand))
|
|
24004
|
+
continue;
|
|
23851
24005
|
if (!this.isAccountSoftAvoided(cand))
|
|
23852
24006
|
return cand;
|
|
23853
24007
|
const score = this.softAvoidUtilScore(cand);
|
|
@@ -23859,11 +24013,10 @@ class AuthBroker {
|
|
|
23859
24013
|
return bestSoftAvoided;
|
|
23860
24014
|
}
|
|
23861
24015
|
fanoutForAgent(name) {
|
|
23862
|
-
const auth = this.config.auth ?? {};
|
|
23863
24016
|
const agent = (this.config.agents ?? {})[name];
|
|
23864
24017
|
if (!agent)
|
|
23865
24018
|
return false;
|
|
23866
|
-
const effective = this.
|
|
24019
|
+
const effective = this.servedAccountForAgent(name);
|
|
23867
24020
|
if (!effective)
|
|
23868
24021
|
return false;
|
|
23869
24022
|
return this.mirrorAccountToAgent(effective, name);
|
|
@@ -23992,6 +24145,15 @@ class AuthBroker {
|
|
|
23992
24145
|
`);
|
|
23993
24146
|
return;
|
|
23994
24147
|
}
|
|
24148
|
+
const exclusiveOwner = this.exclusiveOwnerOf(ov.active);
|
|
24149
|
+
if (exclusiveOwner) {
|
|
24150
|
+
process.stdout.write(`auth-broker: active-override dropped — account '${ov.active}' is now exclusive ` + `to agent '${exclusiveOwner}' (agents.${exclusiveOwner}.auth.exclusive); yaml wins
|
|
24151
|
+
`);
|
|
24152
|
+
try {
|
|
24153
|
+
unlinkSync(join7(this.stateDir, "active-override.json"));
|
|
24154
|
+
} catch {}
|
|
24155
|
+
return;
|
|
24156
|
+
}
|
|
23995
24157
|
if (ov.active === this.config.auth?.active)
|
|
23996
24158
|
return;
|
|
23997
24159
|
this.config = {
|
|
@@ -12427,8 +12427,10 @@ var AgentSchema = exports_external.object({
|
|
|
12427
12427
|
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')."),
|
|
12428
12428
|
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."),
|
|
12429
12429
|
auth: exports_external.object({
|
|
12430
|
-
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only \u2014 " + "this agent talks to the named account regardless of fleet active. See RFC H \u00a74.5.")
|
|
12431
|
-
|
|
12430
|
+
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only \u2014 " + "this agent talks to the named account regardless of fleet active. See RFC H \u00a74.5."),
|
|
12431
|
+
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 \u2014 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)."),
|
|
12432
|
+
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.")
|
|
12433
|
+
}).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)."),
|
|
12432
12434
|
dm_only: exports_external.boolean().optional().describe("Mark this agent as a DM-only bot \u2014 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 \u2014 set it " + "to a display label like 'DM' for /switchroom status output."),
|
|
12433
12435
|
topic_name: exports_external.string().describe("Telegram forum topic display name"),
|
|
12434
12436
|
topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB\ufe0f')"),
|
|
@@ -12503,6 +12505,17 @@ var AgentSchema = exports_external.object({
|
|
|
12503
12505
|
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()
|
|
12504
12506
|
}).optional()
|
|
12505
12507
|
}).superRefine((agent, ctx) => {
|
|
12508
|
+
if (agent.auth && !agent.auth.override) {
|
|
12509
|
+
for (const flag of ["strict", "exclusive"]) {
|
|
12510
|
+
if (agent.auth[flag]) {
|
|
12511
|
+
ctx.addIssue({
|
|
12512
|
+
code: exports_external.ZodIssueCode.custom,
|
|
12513
|
+
message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` \u2014 there is no pinned account for it to apply to.`,
|
|
12514
|
+
path: ["auth", flag]
|
|
12515
|
+
});
|
|
12516
|
+
}
|
|
12517
|
+
}
|
|
12518
|
+
}
|
|
12506
12519
|
if (agent.dm_only !== true)
|
|
12507
12520
|
return;
|
|
12508
12521
|
const tg = agent.channels?.telegram;
|
|
@@ -12615,6 +12628,13 @@ var UserSchema = exports_external.object({
|
|
|
12615
12628
|
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
12616
12629
|
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 \u2014 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 \u2014 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 \u2014 see docs/configuration.md.")
|
|
12617
12630
|
});
|
|
12631
|
+
var ConfigRepoConfigSchema = exports_external.object({
|
|
12632
|
+
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."),
|
|
12633
|
+
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."),
|
|
12634
|
+
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 \u2014 useful " + "for offline hosts or a review-before-push workflow."),
|
|
12635
|
+
remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
|
|
12636
|
+
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 \u2014 or when the API is unreachable \u2014 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.")
|
|
12637
|
+
});
|
|
12618
12638
|
var SwitchroomConfigSchema = exports_external.object({
|
|
12619
12639
|
switchroom: exports_external.object({
|
|
12620
12640
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -12627,6 +12647,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
12627
12647
|
memory: MemoryBackendConfigSchema.optional(),
|
|
12628
12648
|
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) \u2014 takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
|
|
12629
12649
|
vault: VaultConfigSchema.optional(),
|
|
12650
|
+
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."),
|
|
12630
12651
|
auth: exports_external.object({
|
|
12631
12652
|
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>`."),
|
|
12632
12653
|
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."),
|
|
@@ -12694,6 +12715,48 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
12694
12715
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
12695
12716
|
checkServes(a.serves, ["agents", name, "serves"]);
|
|
12696
12717
|
}
|
|
12718
|
+
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
12719
|
+
const agentAuth = a.auth;
|
|
12720
|
+
if (!agentAuth?.exclusive || !agentAuth.override)
|
|
12721
|
+
continue;
|
|
12722
|
+
const acct = agentAuth.override;
|
|
12723
|
+
if (cfg.auth?.active === acct) {
|
|
12724
|
+
ctx.addIssue({
|
|
12725
|
+
code: exports_external.ZodIssueCode.custom,
|
|
12726
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) \u2014 it cannot be the fleet \`auth.active\``,
|
|
12727
|
+
path: ["auth", "active"]
|
|
12728
|
+
});
|
|
12729
|
+
}
|
|
12730
|
+
const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
|
|
12731
|
+
if (fbIdx !== -1) {
|
|
12732
|
+
ctx.addIssue({
|
|
12733
|
+
code: exports_external.ZodIssueCode.custom,
|
|
12734
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) \u2014 it cannot appear in \`auth.fallback_order\``,
|
|
12735
|
+
path: ["auth", "fallback_order", fbIdx]
|
|
12736
|
+
});
|
|
12737
|
+
}
|
|
12738
|
+
for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
|
|
12739
|
+
if (other === name)
|
|
12740
|
+
continue;
|
|
12741
|
+
const otherOverride = oa.auth?.override;
|
|
12742
|
+
if (otherOverride === acct) {
|
|
12743
|
+
ctx.addIssue({
|
|
12744
|
+
code: exports_external.ZodIssueCode.custom,
|
|
12745
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) \u2014 agent '${other}' cannot pin it`,
|
|
12746
|
+
path: ["agents", other, "auth", "override"]
|
|
12747
|
+
});
|
|
12748
|
+
}
|
|
12749
|
+
}
|
|
12750
|
+
(cfg.auth?.consumers ?? []).forEach((c, i) => {
|
|
12751
|
+
if (c.account === acct) {
|
|
12752
|
+
ctx.addIssue({
|
|
12753
|
+
code: exports_external.ZodIssueCode.custom,
|
|
12754
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) \u2014 consumer '${c.name}' cannot pin it`,
|
|
12755
|
+
path: ["auth", "consumers", i, "account"]
|
|
12756
|
+
});
|
|
12757
|
+
}
|
|
12758
|
+
});
|
|
12759
|
+
}
|
|
12697
12760
|
const microsoftAccounts = cfg.microsoft_accounts;
|
|
12698
12761
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
12699
12762
|
const mw = a.microsoft_workspace;
|