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;
|
|
@@ -19820,6 +19883,23 @@ function extractApiErrorMessage(bodyText) {
|
|
|
19820
19883
|
} catch {}
|
|
19821
19884
|
return bodyText.trim().slice(0, 500);
|
|
19822
19885
|
}
|
|
19886
|
+
function extractApiErrorCode(bodyText) {
|
|
19887
|
+
if (!bodyText || bodyText.trim().length === 0)
|
|
19888
|
+
return null;
|
|
19889
|
+
try {
|
|
19890
|
+
const parsed = JSON.parse(bodyText);
|
|
19891
|
+
const code = parsed?.error?.details?.error_code;
|
|
19892
|
+
if (typeof code === "string" && code.trim().length > 0)
|
|
19893
|
+
return code.trim();
|
|
19894
|
+
} catch {}
|
|
19895
|
+
return null;
|
|
19896
|
+
}
|
|
19897
|
+
var ENTITLEMENT_BLOCKED_ERROR_CODES = new Set([
|
|
19898
|
+
"oauth_not_allowed_for_organization"
|
|
19899
|
+
]);
|
|
19900
|
+
function isEntitlementBlockedErrorCode(code) {
|
|
19901
|
+
return code != null && ENTITLEMENT_BLOCKED_ERROR_CODES.has(code);
|
|
19902
|
+
}
|
|
19823
19903
|
function isEntitlementDisabledMessage(message) {
|
|
19824
19904
|
if (!message)
|
|
19825
19905
|
return false;
|
|
@@ -19877,7 +19957,8 @@ async function fetchQuota(opts) {
|
|
|
19877
19957
|
} catch {}
|
|
19878
19958
|
clearTimeout(timeout);
|
|
19879
19959
|
const apiErrorMessage = extractApiErrorMessage(bodyText);
|
|
19880
|
-
const
|
|
19960
|
+
const apiErrorCode = extractApiErrorCode(bodyText);
|
|
19961
|
+
const entitlement = resp.status === 403 && (isEntitlementBlockedErrorCode(apiErrorCode) || isEntitlementDisabledMessage(apiErrorMessage));
|
|
19881
19962
|
return {
|
|
19882
19963
|
ok: false,
|
|
19883
19964
|
reason: `HTTP ${resp.status} from Anthropic (${parsed.reason})`,
|
|
@@ -22165,11 +22246,30 @@ class AuthBroker {
|
|
|
22165
22246
|
return override;
|
|
22166
22247
|
return auth.active ?? null;
|
|
22167
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
|
+
}
|
|
22168
22260
|
servingAccount(identity) {
|
|
22169
|
-
const account = this.callerAccount(identity);
|
|
22170
22261
|
if (identity.kind === "operator")
|
|
22171
|
-
return
|
|
22172
|
-
|
|
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);
|
|
22173
22273
|
}
|
|
22174
22274
|
isOverageAllowed(account) {
|
|
22175
22275
|
return (this.config.auth?.allow_overage_accounts ?? []).includes(account);
|
|
@@ -22287,6 +22387,8 @@ class AuthBroker {
|
|
|
22287
22387
|
continue;
|
|
22288
22388
|
if (this.isAccountPremiumWalled(cand))
|
|
22289
22389
|
continue;
|
|
22390
|
+
if (this.exclusiveOwnerOf(cand))
|
|
22391
|
+
continue;
|
|
22290
22392
|
if (!accountExists(cand, this.home))
|
|
22291
22393
|
continue;
|
|
22292
22394
|
if (!readAccountCredentials(cand, this.home))
|
|
@@ -22432,15 +22534,30 @@ class AuthBroker {
|
|
|
22432
22534
|
if (reselected && this.accountEligibilityOf(reselected) === "eligible")
|
|
22433
22535
|
return reselected;
|
|
22434
22536
|
for (const cand of ring) {
|
|
22435
|
-
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") {
|
|
22436
22538
|
return cand;
|
|
22437
22539
|
}
|
|
22438
22540
|
}
|
|
22439
22541
|
return null;
|
|
22440
22542
|
}
|
|
22441
|
-
accountWithFailover(account) {
|
|
22543
|
+
accountWithFailover(account, forAgent) {
|
|
22442
22544
|
if (!account)
|
|
22443
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
|
+
}
|
|
22444
22561
|
const exhausted = this.isAccountExhausted(account);
|
|
22445
22562
|
if (!exhausted && !this.isAccountSoftAvoided(account))
|
|
22446
22563
|
return account;
|
|
@@ -22448,6 +22565,8 @@ class AuthBroker {
|
|
|
22448
22565
|
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22449
22566
|
if (cand === account || this.isAccountExhausted(cand))
|
|
22450
22567
|
continue;
|
|
22568
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22569
|
+
continue;
|
|
22451
22570
|
if (this.isAccountSoftAvoided(cand)) {
|
|
22452
22571
|
softAvoidExcludedAny = true;
|
|
22453
22572
|
continue;
|
|
@@ -22461,6 +22580,8 @@ class AuthBroker {
|
|
|
22461
22580
|
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22462
22581
|
if (cand === account || this.isAccountExhausted(cand))
|
|
22463
22582
|
continue;
|
|
22583
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22584
|
+
continue;
|
|
22464
22585
|
if (!readAccountCredentials(cand, this.home))
|
|
22465
22586
|
continue;
|
|
22466
22587
|
const score = this.softAvoidUtilScore(cand);
|
|
@@ -22475,12 +22596,14 @@ class AuthBroker {
|
|
|
22475
22596
|
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22476
22597
|
if (cand === account || this.isAccountExhausted(cand))
|
|
22477
22598
|
continue;
|
|
22599
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22600
|
+
continue;
|
|
22478
22601
|
if (readAccountCredentials(cand, this.home))
|
|
22479
22602
|
return cand;
|
|
22480
22603
|
}
|
|
22481
22604
|
}
|
|
22482
22605
|
const active = this.config.auth?.active;
|
|
22483
|
-
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)) {
|
|
22484
22607
|
return active;
|
|
22485
22608
|
}
|
|
22486
22609
|
return account;
|
|
@@ -22532,7 +22655,9 @@ class AuthBroker {
|
|
|
22532
22655
|
const agents = Object.entries(this.config.agents ?? {}).map(([name, agent]) => {
|
|
22533
22656
|
const override = agent.auth?.override ?? null;
|
|
22534
22657
|
const account = override ?? auth.active ?? "";
|
|
22535
|
-
|
|
22658
|
+
const strict = Boolean(override && agent.auth?.strict);
|
|
22659
|
+
const exclusive = Boolean(override && agent.auth?.exclusive);
|
|
22660
|
+
return { name, account, override, strict, exclusive };
|
|
22536
22661
|
});
|
|
22537
22662
|
const consumers = (auth.consumers ?? []).map((c) => ({
|
|
22538
22663
|
name: c.name,
|
|
@@ -22859,6 +22984,12 @@ class AuthBroker {
|
|
|
22859
22984
|
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
|
|
22860
22985
|
return;
|
|
22861
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
|
+
}
|
|
22862
22993
|
const cfg = {
|
|
22863
22994
|
...this.config,
|
|
22864
22995
|
auth: { ...this.config.auth ?? {}, active: account }
|
|
@@ -22870,6 +23001,15 @@ class AuthBroker {
|
|
|
22870
23001
|
this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: true });
|
|
22871
23002
|
socket.write(encodeSuccess(id, { active: account, fanned }));
|
|
22872
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
|
+
}
|
|
22873
23013
|
async opMarkExhausted(socket, id, identity, until) {
|
|
22874
23014
|
const account = this.callerAccount(identity);
|
|
22875
23015
|
if (!account) {
|
|
@@ -22879,7 +23019,12 @@ class AuthBroker {
|
|
|
22879
23019
|
}
|
|
22880
23020
|
const { rolled, rolledTo } = await this.markExhaustedAndRoll(account, until, identity);
|
|
22881
23021
|
this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true });
|
|
22882
|
-
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
|
+
}));
|
|
22883
23028
|
}
|
|
22884
23029
|
async opMarkThrottled(socket, id, identity, until, probeOnly = false) {
|
|
22885
23030
|
const account = this.callerAccount(identity);
|
|
@@ -22896,7 +23041,8 @@ class AuthBroker {
|
|
|
22896
23041
|
account,
|
|
22897
23042
|
throttled_until: this.quota[account]?.throttled_until ?? 0,
|
|
22898
23043
|
escalated: escalated2,
|
|
22899
|
-
rolledTo: escalated2 ? rolledTo2 : null
|
|
23044
|
+
rolledTo: escalated2 ? this.callerRolledTo(identity, rolledTo2) : null,
|
|
23045
|
+
caller_pinned_strict: this.callerStrictPinned(identity)
|
|
22900
23046
|
}));
|
|
22901
23047
|
return;
|
|
22902
23048
|
}
|
|
@@ -22939,7 +23085,8 @@ class AuthBroker {
|
|
|
22939
23085
|
account,
|
|
22940
23086
|
throttled_until: throttledUntil,
|
|
22941
23087
|
escalated,
|
|
22942
|
-
rolledTo: escalated ? rolledTo : null
|
|
23088
|
+
rolledTo: escalated ? this.callerRolledTo(identity, rolledTo) : null,
|
|
23089
|
+
caller_pinned_strict: this.callerStrictPinned(identity)
|
|
22943
23090
|
}));
|
|
22944
23091
|
}
|
|
22945
23092
|
async maybeEscalateThrottle(account, identity, now) {
|
|
@@ -23511,6 +23658,22 @@ class AuthBroker {
|
|
|
23511
23658
|
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
|
|
23512
23659
|
return;
|
|
23513
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
|
+
}
|
|
23514
23677
|
const agents = { ...this.config.agents ?? {} };
|
|
23515
23678
|
const cur = agents[agentName];
|
|
23516
23679
|
const auth = { ...cur.auth ?? {} };
|
|
@@ -23686,10 +23849,9 @@ class AuthBroker {
|
|
|
23686
23849
|
return out;
|
|
23687
23850
|
}
|
|
23688
23851
|
fanoutToAffectedAgents(label) {
|
|
23689
|
-
const auth = this.config.auth ?? {};
|
|
23690
23852
|
const fanned = [];
|
|
23691
|
-
for (const
|
|
23692
|
-
const effective = this.
|
|
23853
|
+
for (const name of Object.keys(this.config.agents ?? {})) {
|
|
23854
|
+
const effective = this.servedAccountForAgent(name);
|
|
23693
23855
|
if (effective === label) {
|
|
23694
23856
|
if (this.fanoutForAgent(name))
|
|
23695
23857
|
fanned.push(name);
|
|
@@ -23708,10 +23870,9 @@ class AuthBroker {
|
|
|
23708
23870
|
const isEffective = effective === label;
|
|
23709
23871
|
if (!isPinned && !isEffective)
|
|
23710
23872
|
continue;
|
|
23711
|
-
|
|
23712
|
-
if (toMirror == null)
|
|
23873
|
+
if (effective == null)
|
|
23713
23874
|
continue;
|
|
23714
|
-
if (this.mirrorAccountToConsumer(
|
|
23875
|
+
if (this.mirrorAccountToConsumer(effective, consumer)) {
|
|
23715
23876
|
fanned.push(consumer.name);
|
|
23716
23877
|
}
|
|
23717
23878
|
}
|
|
@@ -23812,6 +23973,10 @@ class AuthBroker {
|
|
|
23812
23973
|
const effective = agent.auth?.override ?? auth.active;
|
|
23813
23974
|
if (effective !== label)
|
|
23814
23975
|
continue;
|
|
23976
|
+
if (agent.auth?.override && agent.auth?.strict)
|
|
23977
|
+
continue;
|
|
23978
|
+
if (!this.isServableBy(next, name))
|
|
23979
|
+
continue;
|
|
23815
23980
|
if (this.mirrorAccountToAgent(next, name)) {
|
|
23816
23981
|
rolled.push(name);
|
|
23817
23982
|
}
|
|
@@ -23820,8 +23985,13 @@ class AuthBroker {
|
|
|
23820
23985
|
}
|
|
23821
23986
|
nextHealthyAccount(current, order) {
|
|
23822
23987
|
const start = order.indexOf(current);
|
|
23823
|
-
if (start === -1)
|
|
23824
|
-
|
|
23988
|
+
if (start === -1) {
|
|
23989
|
+
for (const cand of order) {
|
|
23990
|
+
if (cand && !this.exclusiveOwnerOf(cand))
|
|
23991
|
+
return cand;
|
|
23992
|
+
}
|
|
23993
|
+
return null;
|
|
23994
|
+
}
|
|
23825
23995
|
let bestSoftAvoided = null;
|
|
23826
23996
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
23827
23997
|
for (let i = 1;i <= order.length; i++) {
|
|
@@ -23830,6 +24000,8 @@ class AuthBroker {
|
|
|
23830
24000
|
continue;
|
|
23831
24001
|
if (this.isAccountExhausted(cand) || !accountExists(cand, this.home))
|
|
23832
24002
|
continue;
|
|
24003
|
+
if (this.exclusiveOwnerOf(cand))
|
|
24004
|
+
continue;
|
|
23833
24005
|
if (!this.isAccountSoftAvoided(cand))
|
|
23834
24006
|
return cand;
|
|
23835
24007
|
const score = this.softAvoidUtilScore(cand);
|
|
@@ -23841,11 +24013,10 @@ class AuthBroker {
|
|
|
23841
24013
|
return bestSoftAvoided;
|
|
23842
24014
|
}
|
|
23843
24015
|
fanoutForAgent(name) {
|
|
23844
|
-
const auth = this.config.auth ?? {};
|
|
23845
24016
|
const agent = (this.config.agents ?? {})[name];
|
|
23846
24017
|
if (!agent)
|
|
23847
24018
|
return false;
|
|
23848
|
-
const effective = this.
|
|
24019
|
+
const effective = this.servedAccountForAgent(name);
|
|
23849
24020
|
if (!effective)
|
|
23850
24021
|
return false;
|
|
23851
24022
|
return this.mirrorAccountToAgent(effective, name);
|
|
@@ -23974,6 +24145,15 @@ class AuthBroker {
|
|
|
23974
24145
|
`);
|
|
23975
24146
|
return;
|
|
23976
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
|
+
}
|
|
23977
24157
|
if (ov.active === this.config.auth?.active)
|
|
23978
24158
|
return;
|
|
23979
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;
|