switchroom 0.20.10 → 0.20.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +68 -3
- package/dist/auth-broker/index.js +211 -46
- package/dist/cli/notion-write-pretool.mjs +68 -3
- 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 +5063 -2961
- package/dist/host-control/main.js +93 -27
- package/dist/vault/approvals/kernel-server.js +92 -26
- package/dist/vault/broker/server.js +92 -26
- package/examples/personal-google-workspace-mcp/compose.yaml +1 -1
- 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/skills/switchroom-architecture/telegram.md +0 -1
- package/skills/switchroom-cli/SKILL.md +0 -1
- package/telegram-plugin/README.md +2 -11
- package/telegram-plugin/auto-fallback-fleet.ts +37 -2
- package/telegram-plugin/bridge/bridge.ts +0 -12
- package/telegram-plugin/chat-lock.ts +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +0 -12
- package/telegram-plugin/dist/gateway/gateway.js +1454 -976
- package/telegram-plugin/dist/server.js +0 -12
- 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 +109 -149
- package/telegram-plugin/gateway/ipc-protocol.ts +43 -0
- package/telegram-plugin/gateway/ipc-server.ts +28 -0
- package/telegram-plugin/gateway/liveness-wiring.ts +6 -1
- 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 +52 -35
- package/telegram-plugin/gateway/status-pin-store.ts +10 -9
- package/telegram-plugin/gateway/stream-render.ts +4 -4
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +15 -4
- package/telegram-plugin/gateway/turn-record-status.ts +32 -1
- package/telegram-plugin/hooks/hooks.json +13 -12
- package/telegram-plugin/hooks/narration-classify.mjs +1 -2
- package/telegram-plugin/hooks/silent-end-scan.mjs +1 -1
- package/telegram-plugin/slot-banner-driver.ts +42 -5
- package/telegram-plugin/status-pin.ts +2 -5
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +24 -0
- package/telegram-plugin/tests/backstop-exactly-once.test.ts +8 -2
- package/telegram-plugin/tests/framework-fallback-duration-guard.test.ts +125 -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/pin-message-tool-retired.test.ts +64 -0
- package/telegram-plugin/tests/privacy-reset-call-sites.test.ts +120 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +38 -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/tests/worker-activity-feed.test.ts +40 -1
- package/telegram-plugin/throttle-tier.ts +12 -3
- package/telegram-plugin/turn-flush-safety.ts +97 -0
- package/telegram-plugin/worker-activity-feed.ts +1 -1
- package/vendor/hindsight-memory/scripts/recall.py +140 -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_recall_latency_instrumentation.py +277 -0
- package/vendor/hindsight-memory/scripts/tests/test_self_improve_correction_tag.py +167 -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();
|
|
@@ -11444,7 +11444,7 @@ var init_schema = __esm(() => {
|
|
|
11444
11444
|
reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
|
|
11445
11445
|
consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
|
|
11446
11446
|
}).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
|
|
11447
|
-
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS — switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS — only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS — the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS — the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY — the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE — the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT — upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT — the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S — the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES — the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS — the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
|
|
11447
|
+
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS — switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS — only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS — the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS — the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY — the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE — the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT — upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT — the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S — the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES — the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS — the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan); and " + "HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE — the Postgres " + "text-search regconfig the native BM25 arms and the search_vector index " + "write run to_tsvector/to_tsquery against; unset means upstream's " + "always-safe `english`, set `hindsight_english` to use switchroom's " + "junk-stopping config (dict + stopword file provisioned durably by the " + "image entrypoint, recall degrading to semantic-only if it is ever " + "missing); plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
|
|
11448
11448
|
});
|
|
11449
11449
|
MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
11450
11450
|
microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
|
|
@@ -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,15 @@ 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
|
+
interval_minutes: exports_external.number().int().min(5).max(59).default(30).describe("Cadence (minutes) of the scheduled `config-repo sync` tick installed " + "by `switchroom config-repo --install-cron`. Rendered as a cron " + "`*/N * * * *` minute field, so it must be 5..59 (the 5-min floor " + "matches the rest of the fleet's cron floor). Default 30 (Ken's " + "approved cadence). Changing it takes effect on the next " + "`--install-cron` / `switchroom update` reconcile."),
|
|
11902
|
+
include_vault_backup: exports_external.enum(["off", "daily", "every_tick"]).default("daily").describe("Whether the scheduled tick also runs `switchroom vault backup` " + "(an encrypted vault/memory snapshot into <path>/vault-backups/) " + "before the sync. `off`: sync only. `daily`: an extra once-per-day " + "cron leg runs `vault backup && config-repo sync` (the 30-min legs " + "stay sync-only). `every_tick`: every tick runs the backup first. " + "Default `daily` — the first automated vault backup on this host.")
|
|
11903
|
+
});
|
|
11882
11904
|
SwitchroomConfigSchema = exports_external.object({
|
|
11883
11905
|
switchroom: exports_external.object({
|
|
11884
11906
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -11891,6 +11913,7 @@ var init_schema = __esm(() => {
|
|
|
11891
11913
|
memory: MemoryBackendConfigSchema.optional(),
|
|
11892
11914
|
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
11915
|
vault: VaultConfigSchema.optional(),
|
|
11916
|
+
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
11917
|
auth: exports_external.object({
|
|
11895
11918
|
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
11919
|
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 +11981,48 @@ var init_schema = __esm(() => {
|
|
|
11958
11981
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11959
11982
|
checkServes(a.serves, ["agents", name, "serves"]);
|
|
11960
11983
|
}
|
|
11984
|
+
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11985
|
+
const agentAuth = a.auth;
|
|
11986
|
+
if (!agentAuth?.exclusive || !agentAuth.override)
|
|
11987
|
+
continue;
|
|
11988
|
+
const acct = agentAuth.override;
|
|
11989
|
+
if (cfg.auth?.active === acct) {
|
|
11990
|
+
ctx.addIssue({
|
|
11991
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11992
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
|
|
11993
|
+
path: ["auth", "active"]
|
|
11994
|
+
});
|
|
11995
|
+
}
|
|
11996
|
+
const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
|
|
11997
|
+
if (fbIdx !== -1) {
|
|
11998
|
+
ctx.addIssue({
|
|
11999
|
+
code: exports_external.ZodIssueCode.custom,
|
|
12000
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
|
|
12001
|
+
path: ["auth", "fallback_order", fbIdx]
|
|
12002
|
+
});
|
|
12003
|
+
}
|
|
12004
|
+
for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
|
|
12005
|
+
if (other === name)
|
|
12006
|
+
continue;
|
|
12007
|
+
const otherOverride = oa.auth?.override;
|
|
12008
|
+
if (otherOverride === acct) {
|
|
12009
|
+
ctx.addIssue({
|
|
12010
|
+
code: exports_external.ZodIssueCode.custom,
|
|
12011
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
|
|
12012
|
+
path: ["agents", other, "auth", "override"]
|
|
12013
|
+
});
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
(cfg.auth?.consumers ?? []).forEach((c, i) => {
|
|
12017
|
+
if (c.account === acct) {
|
|
12018
|
+
ctx.addIssue({
|
|
12019
|
+
code: exports_external.ZodIssueCode.custom,
|
|
12020
|
+
message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
|
|
12021
|
+
path: ["auth", "consumers", i, "account"]
|
|
12022
|
+
});
|
|
12023
|
+
}
|
|
12024
|
+
});
|
|
12025
|
+
}
|
|
11961
12026
|
const microsoftAccounts = cfg.microsoft_accounts;
|
|
11962
12027
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11963
12028
|
const mw = a.microsoft_workspace;
|
|
@@ -18957,6 +19022,27 @@ function allocateAgentUid(name) {
|
|
|
18957
19022
|
return AGENT_UID_MIN + u32 % range;
|
|
18958
19023
|
}
|
|
18959
19024
|
|
|
19025
|
+
// src/setup/hindsight-recall-passthrough.ts
|
|
19026
|
+
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
|
19027
|
+
var HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT = "Relevant memories from past conversations (prioritize recent when " + "conflicting). Only use memories that are directly useful to continue " + "this conversation; ignore the rest:";
|
|
19028
|
+
var RECALL_PASSTHROUGH_DEFAULTS = Object.freeze({
|
|
19029
|
+
budget: "low",
|
|
19030
|
+
maxTokens: 1024,
|
|
19031
|
+
preferObservations: true,
|
|
19032
|
+
contextTurns: 2,
|
|
19033
|
+
roles: ["user", "assistant"],
|
|
19034
|
+
promptPreamble: HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT,
|
|
19035
|
+
tags: [],
|
|
19036
|
+
tagsMatch: "any",
|
|
19037
|
+
tagGroups: {},
|
|
19038
|
+
tagWeights: HINDSIGHT_RECALL_TAG_WEIGHT_SEED,
|
|
19039
|
+
additionalBankFilters: {},
|
|
19040
|
+
transcriptFallback: true,
|
|
19041
|
+
transcriptTailBytes: 262144,
|
|
19042
|
+
maxQueryChars: 800,
|
|
19043
|
+
parallel: true
|
|
19044
|
+
});
|
|
19045
|
+
|
|
18960
19046
|
// src/setup/hindsight-recall-tunables.ts
|
|
18961
19047
|
var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
|
|
18962
19048
|
var MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
|
|
@@ -19232,7 +19318,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
|
|
|
19232
19318
|
"HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
|
|
19233
19319
|
"HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S",
|
|
19234
19320
|
"HINDSIGHT_API_TEMPORAL_LANGUAGES",
|
|
19235
|
-
"HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS"
|
|
19321
|
+
"HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS",
|
|
19322
|
+
"HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE"
|
|
19236
19323
|
]);
|
|
19237
19324
|
var HINDSIGHT_WORKER_SLOT_TYPES = [
|
|
19238
19325
|
"consolidation",
|
|
@@ -19677,27 +19764,6 @@ init_vault();
|
|
|
19677
19764
|
// src/setup/onboarding.ts
|
|
19678
19765
|
init_paths();
|
|
19679
19766
|
|
|
19680
|
-
// src/setup/hindsight-recall-passthrough.ts
|
|
19681
|
-
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
|
19682
|
-
var HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT = "Relevant memories from past conversations (prioritize recent when " + "conflicting). Only use memories that are directly useful to continue " + "this conversation; ignore the rest:";
|
|
19683
|
-
var RECALL_PASSTHROUGH_DEFAULTS = Object.freeze({
|
|
19684
|
-
budget: "low",
|
|
19685
|
-
maxTokens: 1024,
|
|
19686
|
-
preferObservations: true,
|
|
19687
|
-
contextTurns: 2,
|
|
19688
|
-
roles: ["user", "assistant"],
|
|
19689
|
-
promptPreamble: HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT,
|
|
19690
|
-
tags: [],
|
|
19691
|
-
tagsMatch: "any",
|
|
19692
|
-
tagGroups: {},
|
|
19693
|
-
tagWeights: HINDSIGHT_RECALL_TAG_WEIGHT_SEED,
|
|
19694
|
-
additionalBankFilters: {},
|
|
19695
|
-
transcriptFallback: true,
|
|
19696
|
-
transcriptTailBytes: 262144,
|
|
19697
|
-
maxQueryChars: 800,
|
|
19698
|
-
parallel: true
|
|
19699
|
-
});
|
|
19700
|
-
|
|
19701
19767
|
// src/repos/bare-clone.ts
|
|
19702
19768
|
init_paths();
|
|
19703
19769
|
|
|
@@ -22183,11 +22249,30 @@ class AuthBroker {
|
|
|
22183
22249
|
return override;
|
|
22184
22250
|
return auth.active ?? null;
|
|
22185
22251
|
}
|
|
22252
|
+
exclusiveOwnerOf(account) {
|
|
22253
|
+
for (const [name, agent] of Object.entries(this.config.agents ?? {})) {
|
|
22254
|
+
if (agent.auth?.exclusive && agent.auth?.override === account)
|
|
22255
|
+
return name;
|
|
22256
|
+
}
|
|
22257
|
+
return null;
|
|
22258
|
+
}
|
|
22259
|
+
isServableBy(account, forAgent) {
|
|
22260
|
+
const owner = this.exclusiveOwnerOf(account);
|
|
22261
|
+
return owner === null || owner === forAgent;
|
|
22262
|
+
}
|
|
22186
22263
|
servingAccount(identity) {
|
|
22187
|
-
const account = this.callerAccount(identity);
|
|
22188
22264
|
if (identity.kind === "operator")
|
|
22189
|
-
return
|
|
22190
|
-
|
|
22265
|
+
return this.callerAccount(identity);
|
|
22266
|
+
if (identity.kind === "agent")
|
|
22267
|
+
return this.servedAccountForAgent(identity.name);
|
|
22268
|
+
return this.accountWithFailover(this.callerAccount(identity));
|
|
22269
|
+
}
|
|
22270
|
+
servedAccountForAgent(name) {
|
|
22271
|
+
const agent = (this.config.agents ?? {})[name];
|
|
22272
|
+
const override = agent?.auth?.override;
|
|
22273
|
+
if (override && agent?.auth?.strict)
|
|
22274
|
+
return override;
|
|
22275
|
+
return this.accountWithFailover(override ?? this.config.auth?.active ?? null, name);
|
|
22191
22276
|
}
|
|
22192
22277
|
isOverageAllowed(account) {
|
|
22193
22278
|
return (this.config.auth?.allow_overage_accounts ?? []).includes(account);
|
|
@@ -22305,6 +22390,8 @@ class AuthBroker {
|
|
|
22305
22390
|
continue;
|
|
22306
22391
|
if (this.isAccountPremiumWalled(cand))
|
|
22307
22392
|
continue;
|
|
22393
|
+
if (this.exclusiveOwnerOf(cand))
|
|
22394
|
+
continue;
|
|
22308
22395
|
if (!accountExists(cand, this.home))
|
|
22309
22396
|
continue;
|
|
22310
22397
|
if (!readAccountCredentials(cand, this.home))
|
|
@@ -22450,15 +22537,30 @@ class AuthBroker {
|
|
|
22450
22537
|
if (reselected && this.accountEligibilityOf(reselected) === "eligible")
|
|
22451
22538
|
return reselected;
|
|
22452
22539
|
for (const cand of ring) {
|
|
22453
|
-
if (cand && cand !== current && accountExists(cand, this.home) && this.accountEligibilityOf(cand) === "unknown") {
|
|
22540
|
+
if (cand && cand !== current && accountExists(cand, this.home) && !this.exclusiveOwnerOf(cand) && this.accountEligibilityOf(cand) === "unknown") {
|
|
22454
22541
|
return cand;
|
|
22455
22542
|
}
|
|
22456
22543
|
}
|
|
22457
22544
|
return null;
|
|
22458
22545
|
}
|
|
22459
|
-
accountWithFailover(account) {
|
|
22546
|
+
accountWithFailover(account, forAgent) {
|
|
22460
22547
|
if (!account)
|
|
22461
22548
|
return null;
|
|
22549
|
+
if (!this.isServableBy(account, forAgent)) {
|
|
22550
|
+
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22551
|
+
if (cand === account || this.isAccountExhausted(cand))
|
|
22552
|
+
continue;
|
|
22553
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22554
|
+
continue;
|
|
22555
|
+
if (readAccountCredentials(cand, this.home))
|
|
22556
|
+
return cand;
|
|
22557
|
+
}
|
|
22558
|
+
const active2 = this.config.auth?.active;
|
|
22559
|
+
if (active2 && active2 !== account && !this.isAccountExhausted(active2) && this.isServableBy(active2, forAgent) && readAccountCredentials(active2, this.home)) {
|
|
22560
|
+
return active2;
|
|
22561
|
+
}
|
|
22562
|
+
return null;
|
|
22563
|
+
}
|
|
22462
22564
|
const exhausted = this.isAccountExhausted(account);
|
|
22463
22565
|
if (!exhausted && !this.isAccountSoftAvoided(account))
|
|
22464
22566
|
return account;
|
|
@@ -22466,6 +22568,8 @@ class AuthBroker {
|
|
|
22466
22568
|
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22467
22569
|
if (cand === account || this.isAccountExhausted(cand))
|
|
22468
22570
|
continue;
|
|
22571
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22572
|
+
continue;
|
|
22469
22573
|
if (this.isAccountSoftAvoided(cand)) {
|
|
22470
22574
|
softAvoidExcludedAny = true;
|
|
22471
22575
|
continue;
|
|
@@ -22479,6 +22583,8 @@ class AuthBroker {
|
|
|
22479
22583
|
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22480
22584
|
if (cand === account || this.isAccountExhausted(cand))
|
|
22481
22585
|
continue;
|
|
22586
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22587
|
+
continue;
|
|
22482
22588
|
if (!readAccountCredentials(cand, this.home))
|
|
22483
22589
|
continue;
|
|
22484
22590
|
const score = this.softAvoidUtilScore(cand);
|
|
@@ -22493,12 +22599,14 @@ class AuthBroker {
|
|
|
22493
22599
|
for (const cand of this.config.auth?.fallback_order ?? []) {
|
|
22494
22600
|
if (cand === account || this.isAccountExhausted(cand))
|
|
22495
22601
|
continue;
|
|
22602
|
+
if (!this.isServableBy(cand, forAgent))
|
|
22603
|
+
continue;
|
|
22496
22604
|
if (readAccountCredentials(cand, this.home))
|
|
22497
22605
|
return cand;
|
|
22498
22606
|
}
|
|
22499
22607
|
}
|
|
22500
22608
|
const active = this.config.auth?.active;
|
|
22501
|
-
if (active && active !== account && !this.isAccountExhausted(active) && readAccountCredentials(active, this.home)) {
|
|
22609
|
+
if (active && active !== account && !this.isAccountExhausted(active) && this.isServableBy(active, forAgent) && readAccountCredentials(active, this.home)) {
|
|
22502
22610
|
return active;
|
|
22503
22611
|
}
|
|
22504
22612
|
return account;
|
|
@@ -22550,7 +22658,9 @@ class AuthBroker {
|
|
|
22550
22658
|
const agents = Object.entries(this.config.agents ?? {}).map(([name, agent]) => {
|
|
22551
22659
|
const override = agent.auth?.override ?? null;
|
|
22552
22660
|
const account = override ?? auth.active ?? "";
|
|
22553
|
-
|
|
22661
|
+
const strict = Boolean(override && agent.auth?.strict);
|
|
22662
|
+
const exclusive = Boolean(override && agent.auth?.exclusive);
|
|
22663
|
+
return { name, account, override, strict, exclusive };
|
|
22554
22664
|
});
|
|
22555
22665
|
const consumers = (auth.consumers ?? []).map((c) => ({
|
|
22556
22666
|
name: c.name,
|
|
@@ -22877,6 +22987,12 @@ class AuthBroker {
|
|
|
22877
22987
|
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
|
|
22878
22988
|
return;
|
|
22879
22989
|
}
|
|
22990
|
+
const exclusiveOwner = this.exclusiveOwnerOf(account);
|
|
22991
|
+
if (exclusiveOwner) {
|
|
22992
|
+
this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: false, error: "exclusive-account" });
|
|
22993
|
+
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.`));
|
|
22994
|
+
return;
|
|
22995
|
+
}
|
|
22880
22996
|
const cfg = {
|
|
22881
22997
|
...this.config,
|
|
22882
22998
|
auth: { ...this.config.auth ?? {}, active: account }
|
|
@@ -22888,6 +23004,15 @@ class AuthBroker {
|
|
|
22888
23004
|
this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: true });
|
|
22889
23005
|
socket.write(encodeSuccess(id, { active: account, fanned }));
|
|
22890
23006
|
}
|
|
23007
|
+
callerStrictPinned(identity) {
|
|
23008
|
+
if (identity.kind !== "agent")
|
|
23009
|
+
return false;
|
|
23010
|
+
const a = (this.config.agents ?? {})[identity.name]?.auth;
|
|
23011
|
+
return Boolean(a?.override && a?.strict);
|
|
23012
|
+
}
|
|
23013
|
+
callerRolledTo(identity, rolledTo) {
|
|
23014
|
+
return this.callerStrictPinned(identity) ? null : rolledTo;
|
|
23015
|
+
}
|
|
22891
23016
|
async opMarkExhausted(socket, id, identity, until) {
|
|
22892
23017
|
const account = this.callerAccount(identity);
|
|
22893
23018
|
if (!account) {
|
|
@@ -22897,7 +23022,12 @@ class AuthBroker {
|
|
|
22897
23022
|
}
|
|
22898
23023
|
const { rolled, rolledTo } = await this.markExhaustedAndRoll(account, until, identity);
|
|
22899
23024
|
this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true });
|
|
22900
|
-
socket.write(encodeSuccess(id, {
|
|
23025
|
+
socket.write(encodeSuccess(id, {
|
|
23026
|
+
account,
|
|
23027
|
+
rolled,
|
|
23028
|
+
rolledTo: this.callerRolledTo(identity, rolledTo),
|
|
23029
|
+
caller_pinned_strict: this.callerStrictPinned(identity)
|
|
23030
|
+
}));
|
|
22901
23031
|
}
|
|
22902
23032
|
async opMarkThrottled(socket, id, identity, until, probeOnly = false) {
|
|
22903
23033
|
const account = this.callerAccount(identity);
|
|
@@ -22914,7 +23044,8 @@ class AuthBroker {
|
|
|
22914
23044
|
account,
|
|
22915
23045
|
throttled_until: this.quota[account]?.throttled_until ?? 0,
|
|
22916
23046
|
escalated: escalated2,
|
|
22917
|
-
rolledTo: escalated2 ? rolledTo2 : null
|
|
23047
|
+
rolledTo: escalated2 ? this.callerRolledTo(identity, rolledTo2) : null,
|
|
23048
|
+
caller_pinned_strict: this.callerStrictPinned(identity)
|
|
22918
23049
|
}));
|
|
22919
23050
|
return;
|
|
22920
23051
|
}
|
|
@@ -22957,7 +23088,8 @@ class AuthBroker {
|
|
|
22957
23088
|
account,
|
|
22958
23089
|
throttled_until: throttledUntil,
|
|
22959
23090
|
escalated,
|
|
22960
|
-
rolledTo: escalated ? rolledTo : null
|
|
23091
|
+
rolledTo: escalated ? this.callerRolledTo(identity, rolledTo) : null,
|
|
23092
|
+
caller_pinned_strict: this.callerStrictPinned(identity)
|
|
22961
23093
|
}));
|
|
22962
23094
|
}
|
|
22963
23095
|
async maybeEscalateThrottle(account, identity, now) {
|
|
@@ -23529,6 +23661,22 @@ class AuthBroker {
|
|
|
23529
23661
|
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
|
|
23530
23662
|
return;
|
|
23531
23663
|
}
|
|
23664
|
+
if (account !== null) {
|
|
23665
|
+
const exclusiveOwner = this.exclusiveOwnerOf(account);
|
|
23666
|
+
if (exclusiveOwner && exclusiveOwner !== agentName) {
|
|
23667
|
+
this.audit({ op: "set-override", identity, account, accountKind: "claude", ok: false, error: "exclusive-account" });
|
|
23668
|
+
socket.write(encodeError(id, "FORBIDDEN", `account '${account}' is exclusive to agent '${exclusiveOwner}' ` + `(agents.${exclusiveOwner}.auth.exclusive) — agent '${agentName}' cannot pin it.`));
|
|
23669
|
+
return;
|
|
23670
|
+
}
|
|
23671
|
+
}
|
|
23672
|
+
{
|
|
23673
|
+
const curAuth = (this.config.agents ?? {})[agentName]?.auth;
|
|
23674
|
+
if ((curAuth?.strict || curAuth?.exclusive) && account !== (curAuth?.override ?? null)) {
|
|
23675
|
+
this.audit({ op: "set-override", identity, account: account ?? undefined, accountKind: "claude", ok: false, error: "yaml-pinned" });
|
|
23676
|
+
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.`));
|
|
23677
|
+
return;
|
|
23678
|
+
}
|
|
23679
|
+
}
|
|
23532
23680
|
const agents = { ...this.config.agents ?? {} };
|
|
23533
23681
|
const cur = agents[agentName];
|
|
23534
23682
|
const auth = { ...cur.auth ?? {} };
|
|
@@ -23704,10 +23852,9 @@ class AuthBroker {
|
|
|
23704
23852
|
return out;
|
|
23705
23853
|
}
|
|
23706
23854
|
fanoutToAffectedAgents(label) {
|
|
23707
|
-
const auth = this.config.auth ?? {};
|
|
23708
23855
|
const fanned = [];
|
|
23709
|
-
for (const
|
|
23710
|
-
const effective = this.
|
|
23856
|
+
for (const name of Object.keys(this.config.agents ?? {})) {
|
|
23857
|
+
const effective = this.servedAccountForAgent(name);
|
|
23711
23858
|
if (effective === label) {
|
|
23712
23859
|
if (this.fanoutForAgent(name))
|
|
23713
23860
|
fanned.push(name);
|
|
@@ -23726,10 +23873,9 @@ class AuthBroker {
|
|
|
23726
23873
|
const isEffective = effective === label;
|
|
23727
23874
|
if (!isPinned && !isEffective)
|
|
23728
23875
|
continue;
|
|
23729
|
-
|
|
23730
|
-
if (toMirror == null)
|
|
23876
|
+
if (effective == null)
|
|
23731
23877
|
continue;
|
|
23732
|
-
if (this.mirrorAccountToConsumer(
|
|
23878
|
+
if (this.mirrorAccountToConsumer(effective, consumer)) {
|
|
23733
23879
|
fanned.push(consumer.name);
|
|
23734
23880
|
}
|
|
23735
23881
|
}
|
|
@@ -23830,6 +23976,10 @@ class AuthBroker {
|
|
|
23830
23976
|
const effective = agent.auth?.override ?? auth.active;
|
|
23831
23977
|
if (effective !== label)
|
|
23832
23978
|
continue;
|
|
23979
|
+
if (agent.auth?.override && agent.auth?.strict)
|
|
23980
|
+
continue;
|
|
23981
|
+
if (!this.isServableBy(next, name))
|
|
23982
|
+
continue;
|
|
23833
23983
|
if (this.mirrorAccountToAgent(next, name)) {
|
|
23834
23984
|
rolled.push(name);
|
|
23835
23985
|
}
|
|
@@ -23838,8 +23988,13 @@ class AuthBroker {
|
|
|
23838
23988
|
}
|
|
23839
23989
|
nextHealthyAccount(current, order) {
|
|
23840
23990
|
const start = order.indexOf(current);
|
|
23841
|
-
if (start === -1)
|
|
23842
|
-
|
|
23991
|
+
if (start === -1) {
|
|
23992
|
+
for (const cand of order) {
|
|
23993
|
+
if (cand && !this.exclusiveOwnerOf(cand))
|
|
23994
|
+
return cand;
|
|
23995
|
+
}
|
|
23996
|
+
return null;
|
|
23997
|
+
}
|
|
23843
23998
|
let bestSoftAvoided = null;
|
|
23844
23999
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
23845
24000
|
for (let i = 1;i <= order.length; i++) {
|
|
@@ -23848,6 +24003,8 @@ class AuthBroker {
|
|
|
23848
24003
|
continue;
|
|
23849
24004
|
if (this.isAccountExhausted(cand) || !accountExists(cand, this.home))
|
|
23850
24005
|
continue;
|
|
24006
|
+
if (this.exclusiveOwnerOf(cand))
|
|
24007
|
+
continue;
|
|
23851
24008
|
if (!this.isAccountSoftAvoided(cand))
|
|
23852
24009
|
return cand;
|
|
23853
24010
|
const score = this.softAvoidUtilScore(cand);
|
|
@@ -23859,11 +24016,10 @@ class AuthBroker {
|
|
|
23859
24016
|
return bestSoftAvoided;
|
|
23860
24017
|
}
|
|
23861
24018
|
fanoutForAgent(name) {
|
|
23862
|
-
const auth = this.config.auth ?? {};
|
|
23863
24019
|
const agent = (this.config.agents ?? {})[name];
|
|
23864
24020
|
if (!agent)
|
|
23865
24021
|
return false;
|
|
23866
|
-
const effective = this.
|
|
24022
|
+
const effective = this.servedAccountForAgent(name);
|
|
23867
24023
|
if (!effective)
|
|
23868
24024
|
return false;
|
|
23869
24025
|
return this.mirrorAccountToAgent(effective, name);
|
|
@@ -23992,6 +24148,15 @@ class AuthBroker {
|
|
|
23992
24148
|
`);
|
|
23993
24149
|
return;
|
|
23994
24150
|
}
|
|
24151
|
+
const exclusiveOwner = this.exclusiveOwnerOf(ov.active);
|
|
24152
|
+
if (exclusiveOwner) {
|
|
24153
|
+
process.stdout.write(`auth-broker: active-override dropped — account '${ov.active}' is now exclusive ` + `to agent '${exclusiveOwner}' (agents.${exclusiveOwner}.auth.exclusive); yaml wins
|
|
24154
|
+
`);
|
|
24155
|
+
try {
|
|
24156
|
+
unlinkSync(join7(this.stateDir, "active-override.json"));
|
|
24157
|
+
} catch {}
|
|
24158
|
+
return;
|
|
24159
|
+
}
|
|
23995
24160
|
if (ov.active === this.config.auth?.active)
|
|
23996
24161
|
return;
|
|
23997
24162
|
this.config = {
|