switchroom 0.18.28 → 0.18.30
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 +15 -2
- package/dist/agent-scheduler/index.js +111 -7
- package/dist/auth-broker/index.js +154 -73
- package/dist/cli/autoaccept-poll.js +8 -3
- package/dist/cli/drive-write-pretool.mjs +8 -3
- package/dist/cli/ms-365-write-pretool.mjs +158 -11
- package/dist/cli/notion-write-pretool.mjs +103 -4
- package/dist/cli/switchroom.js +2712 -2219
- package/dist/host-control/main.js +110 -70
- package/dist/vault/approvals/kernel-server.js +116 -70
- package/dist/vault/broker/server.js +314 -202
- package/package.json +3 -3
- package/profiles/_base/start.sh.hbs +105 -34
- package/telegram-plugin/dist/bridge/bridge.js +71 -47
- package/telegram-plugin/dist/gateway/gateway.js +1128 -666
- package/telegram-plugin/dist/server.js +89 -64
- package/telegram-plugin/gateway/backstop-delivery.ts +272 -0
- package/telegram-plugin/gateway/forward-origin.ts +9 -1
- package/telegram-plugin/gateway/gateway.ts +656 -388
- package/telegram-plugin/gateway/model-command.ts +331 -602
- package/telegram-plugin/gateway/session-model-file.ts +40 -0
- package/telegram-plugin/gateway/turn-record-status.ts +45 -0
- package/telegram-plugin/gateway/unhandled-message.ts +177 -0
- package/telegram-plugin/history.ts +153 -23
- package/telegram-plugin/llm-error-present.ts +24 -0
- package/telegram-plugin/model-unavailable.ts +55 -0
- package/telegram-plugin/operator-events.ts +113 -0
- package/telegram-plugin/pending-user-notice.ts +88 -0
- package/telegram-plugin/shared/local-time.ts +99 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +250 -0
- package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +103 -0
- package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +264 -0
- package/telegram-plugin/tests/forward-origin.test.ts +30 -3
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +111 -60
- package/telegram-plugin/tests/history.test.ts +88 -0
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +278 -0
- package/telegram-plugin/tests/local-time.test.ts +135 -0
- package/telegram-plugin/tests/model-command.test.ts +427 -1512
- package/telegram-plugin/tests/session-model-file.test.ts +23 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +34 -0
- package/telegram-plugin/tier-downgrade.ts +4 -3
- package/telegram-plugin/turn-flush-safety.ts +25 -1
- package/vendor/hindsight-memory/scripts/backfill_transcripts.py +399 -2
- package/vendor/hindsight-memory/scripts/lib/client.py +47 -0
- package/vendor/hindsight-memory/scripts/lib/content.py +93 -7
- package/vendor/hindsight-memory/scripts/lib/turnlog.py +450 -0
- package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +467 -0
- package/vendor/hindsight-memory/tests/test_content.py +63 -7
package/bin/handoff-briefing.sh
CHANGED
|
@@ -135,8 +135,14 @@ $RECALL_TEXT"
|
|
|
135
135
|
fi
|
|
136
136
|
|
|
137
137
|
# ── Source 3: Today's daily memory ─────────────────────────────────────────────
|
|
138
|
+
# Resolve "today" in the agent's LOCAL time — NOT the process default (UTC on
|
|
139
|
+
# most hosts/CI). TODAY keys the daily-memory lookup (memory/${TODAY}.md); using
|
|
140
|
+
# UTC here would look up the wrong day's file during the window where the local
|
|
141
|
+
# date is ahead of/behind UTC, silently dropping today's memory. Same
|
|
142
|
+
# SWITCHROOM_TIMEZONE → TZ → UTC cascade the restart-timestamp render below uses.
|
|
143
|
+
_TZ_VAL="${SWITCHROOM_TIMEZONE:-${TZ:-UTC}}"
|
|
138
144
|
DAILY_SECTION=""
|
|
139
|
-
TODAY=$(date +%Y-%m-%d 2>/dev/null || true)
|
|
145
|
+
TODAY=$(TZ="$_TZ_VAL" date +%Y-%m-%d 2>/dev/null || date +%Y-%m-%d 2>/dev/null || true)
|
|
140
146
|
if [ -n "$TODAY" ] && [ -n "$WORKSPACE_DIR" ]; then
|
|
141
147
|
DAILY_FILE="$WORKSPACE_DIR/memory/${TODAY}.md"
|
|
142
148
|
if [ -f "$DAILY_FILE" ] && [ -s "$DAILY_FILE" ]; then
|
|
@@ -150,7 +156,14 @@ $DAILY_CONTENT"
|
|
|
150
156
|
fi
|
|
151
157
|
|
|
152
158
|
# ── Assemble briefing ───────────────────────────────────────────────────────────
|
|
153
|
-
|
|
159
|
+
# Restart timestamp — model-facing: it lands in the resume-turn system prompt
|
|
160
|
+
# via --append-system-prompt ("You just restarted at …"). Render the agent's
|
|
161
|
+
# LOCAL am/pm wall clock, NOT UTC, so the restart turn never sees a competing
|
|
162
|
+
# UTC "now" (the whole point of the deterministic-local-time work). Same
|
|
163
|
+
# SWITCHROOM_TIMEZONE → TZ → UTC cascade and `%A %Y-%m-%d %I:%M %p %Z` am/pm
|
|
164
|
+
# format the UserPromptSubmit local-time hook (bin/timezone-hook.sh) uses.
|
|
165
|
+
# (_TZ_VAL is computed once above, in the daily-memory section.)
|
|
166
|
+
TIMESTAMP=$(TZ="$_TZ_VAL" date '+%A %Y-%m-%d %I:%M %p %Z' 2>/dev/null || date '+%A %Y-%m-%d %I:%M %p %Z')
|
|
154
167
|
|
|
155
168
|
# Determine restart reason if available
|
|
156
169
|
RESTART_REASON="unknown"
|
|
@@ -11354,11 +11354,52 @@ var AgentGoogleWorkspaceConfigSchema = exports_external.object({
|
|
|
11354
11354
|
approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card."),
|
|
11355
11355
|
tier: GoogleWorkspaceTierSchema.optional().describe("Per-agent tier override (RFC G Phase 1). When set, replaces the " + "top-level google_workspace.tier for this agent. Common case: most " + "agents on `core`, one specialist on `extended` for Slides access.")
|
|
11356
11356
|
}).optional();
|
|
11357
|
+
var MicrosoftAccountEmailSchema = exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11358
|
+
message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
|
|
11359
|
+
}).transform((v) => v.trim().toLowerCase());
|
|
11360
|
+
var MicrosoftToolTokenSchema = exports_external.string().min(1).regex(/^[a-z0-9-]+$/, {
|
|
11361
|
+
message: "microsoft_workspace tools[] tokens are forwarded to softeria's " + "--enabled-tools regex; each must be lowercase kebab-case " + "([a-z0-9-]+, e.g. 'mail', 'calendar', 'send-mail') so an unescaped " + "regex metacharacter can't crash the launcher"
|
|
11362
|
+
});
|
|
11363
|
+
var MicrosoftAccountBindingSchema = exports_external.object({
|
|
11364
|
+
account: MicrosoftAccountEmailSchema.describe("The Microsoft account this binding uses. Must be a key in top-level " + "`microsoft_accounts:` with this agent in its `enabled_for[]`."),
|
|
11365
|
+
tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist → softeria `--enabled-tools <regex>` " + "(tokens joined with `|`). Omitted = all tools exposed for this account."),
|
|
11366
|
+
org_mode: exports_external.boolean().optional().describe("Per-binding org_mode override (RFC #1873 §6.4).")
|
|
11367
|
+
});
|
|
11357
11368
|
var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
11358
|
-
account:
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11369
|
+
account: MicrosoftAccountEmailSchema.optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized). Mutually exclusive with `accounts` (plural)."),
|
|
11370
|
+
tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist for the SINGULAR `account` form → " + "softeria `--enabled-tools <regex>`. Omitted = all tools. Only " + "valid with the singular `account`; using it together with the " + "plural `accounts` (which carries per-binding `tools`) is an error."),
|
|
11371
|
+
org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 §6.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false)."),
|
|
11372
|
+
accounts: exports_external.array(MicrosoftAccountBindingSchema).min(1).optional().describe("Plural multi-account form: bind MULTIPLE Microsoft accounts to " + "this agent, each with its own tool scope. Mutually exclusive with " + "the singular `account`. Each account gets its own `ms-365-<slug>` " + "MCP server.")
|
|
11373
|
+
}).superRefine((v, ctx) => {
|
|
11374
|
+
const hasSingular = v.account !== undefined;
|
|
11375
|
+
const hasPlural = v.accounts !== undefined;
|
|
11376
|
+
if (hasSingular && hasPlural) {
|
|
11377
|
+
ctx.addIssue({
|
|
11378
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11379
|
+
message: "microsoft_workspace: use EITHER `account` (singular) OR " + "`accounts` (plural array), not both",
|
|
11380
|
+
path: ["accounts"]
|
|
11381
|
+
});
|
|
11382
|
+
}
|
|
11383
|
+
if (hasPlural && v.tools !== undefined) {
|
|
11384
|
+
ctx.addIssue({
|
|
11385
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11386
|
+
message: "microsoft_workspace: block-level `tools` applies to the singular " + "`account` only; with `accounts` put `tools` inside each binding",
|
|
11387
|
+
path: ["tools"]
|
|
11388
|
+
});
|
|
11389
|
+
}
|
|
11390
|
+
if (hasPlural) {
|
|
11391
|
+
const seen = new Set;
|
|
11392
|
+
for (const b of v.accounts) {
|
|
11393
|
+
if (seen.has(b.account)) {
|
|
11394
|
+
ctx.addIssue({
|
|
11395
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11396
|
+
message: `microsoft_workspace: duplicate account '${b.account}' in accounts[]`,
|
|
11397
|
+
path: ["accounts"]
|
|
11398
|
+
});
|
|
11399
|
+
}
|
|
11400
|
+
seen.add(b.account);
|
|
11401
|
+
}
|
|
11402
|
+
}
|
|
11362
11403
|
}).optional();
|
|
11363
11404
|
var AgentNotionWorkspaceConfigSchema = exports_external.object({
|
|
11364
11405
|
databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
|
|
@@ -11733,6 +11774,26 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11733
11774
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11734
11775
|
checkServes(a.serves, ["agents", name, "serves"]);
|
|
11735
11776
|
}
|
|
11777
|
+
const microsoftAccounts = cfg.microsoft_accounts;
|
|
11778
|
+
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
11779
|
+
const mw = a.microsoft_workspace;
|
|
11780
|
+
const accounts = mw?.accounts;
|
|
11781
|
+
if (!accounts)
|
|
11782
|
+
continue;
|
|
11783
|
+
accounts.forEach((b, i) => {
|
|
11784
|
+
const acct = b?.account?.trim().toLowerCase();
|
|
11785
|
+
if (!acct)
|
|
11786
|
+
return;
|
|
11787
|
+
const enabledFor = microsoftAccounts?.[acct]?.enabled_for ?? [];
|
|
11788
|
+
if (!enabledFor.includes(name)) {
|
|
11789
|
+
ctx.addIssue({
|
|
11790
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11791
|
+
message: `agent '${name}' binds Microsoft account '${acct}' but is not in ` + `microsoft_accounts['${acct}'].enabled_for[] — operator must run ` + `\`switchroom auth microsoft enable ${acct} ${name}\``,
|
|
11792
|
+
path: ["agents", name, "microsoft_workspace", "accounts", i, "account"]
|
|
11793
|
+
});
|
|
11794
|
+
}
|
|
11795
|
+
});
|
|
11796
|
+
}
|
|
11736
11797
|
});
|
|
11737
11798
|
|
|
11738
11799
|
// src/config/paths.ts
|
|
@@ -12326,6 +12387,22 @@ function validateNotionWorkspaceConfig(config) {
|
|
|
12326
12387
|
return issues;
|
|
12327
12388
|
}
|
|
12328
12389
|
|
|
12390
|
+
// src/config/timezone.ts
|
|
12391
|
+
function isResolvableTimezone(zone) {
|
|
12392
|
+
try {
|
|
12393
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
12394
|
+
return true;
|
|
12395
|
+
} catch {
|
|
12396
|
+
return false;
|
|
12397
|
+
}
|
|
12398
|
+
}
|
|
12399
|
+
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
12400
|
+
"UTC",
|
|
12401
|
+
"Etc/UTC",
|
|
12402
|
+
"Etc/Universal",
|
|
12403
|
+
"Universal"
|
|
12404
|
+
]);
|
|
12405
|
+
|
|
12329
12406
|
// src/config/loader.ts
|
|
12330
12407
|
class ConfigError extends Error {
|
|
12331
12408
|
details;
|
|
@@ -12449,8 +12526,30 @@ function loadConfig(configPath) {
|
|
|
12449
12526
|
if (notionIssues.length > 0) {
|
|
12450
12527
|
throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
|
|
12451
12528
|
}
|
|
12529
|
+
validateAllTimezones(config, filePath);
|
|
12452
12530
|
return config;
|
|
12453
12531
|
}
|
|
12532
|
+
function validateAllTimezones(config, filePath) {
|
|
12533
|
+
const issues = [];
|
|
12534
|
+
const check = (zone, where) => {
|
|
12535
|
+
if (zone == null)
|
|
12536
|
+
return;
|
|
12537
|
+
if (!isResolvableTimezone(zone)) {
|
|
12538
|
+
issues.push(` ${where}: "${zone}" is not a resolvable IANA timezone ` + `(shape is valid but no such zone exists — check for a typo, ` + `e.g. "Australia/Melbourne", "America/New_York", "UTC").`);
|
|
12539
|
+
}
|
|
12540
|
+
};
|
|
12541
|
+
check(config.switchroom?.timezone, "switchroom.timezone");
|
|
12542
|
+
check(config.defaults?.timezone, "defaults.timezone");
|
|
12543
|
+
for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
|
|
12544
|
+
check(profile?.timezone, `profiles.${profileName}.timezone`);
|
|
12545
|
+
}
|
|
12546
|
+
for (const [agentName, agentRaw] of Object.entries(config.agents)) {
|
|
12547
|
+
check(agentRaw?.timezone, `agents.${agentName}.timezone`);
|
|
12548
|
+
}
|
|
12549
|
+
if (issues.length > 0) {
|
|
12550
|
+
throw new ConfigError(`Invalid timezone configuration in ${filePath}`, issues);
|
|
12551
|
+
}
|
|
12552
|
+
}
|
|
12454
12553
|
function validateAllCronTopicAliases(config, filePath) {
|
|
12455
12554
|
const issues = [];
|
|
12456
12555
|
for (const [agentName, agentRaw] of Object.entries(config.agents)) {
|
|
@@ -13691,7 +13790,8 @@ var GetCredentialsRequestSchema = exports_external.object({
|
|
|
13691
13790
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
13692
13791
|
op: exports_external.literal("get-credentials"),
|
|
13693
13792
|
id: exports_external.string().min(1),
|
|
13694
|
-
provider: ProviderNameSchema.optional()
|
|
13793
|
+
provider: ProviderNameSchema.optional(),
|
|
13794
|
+
account: exports_external.string().min(1).optional()
|
|
13695
13795
|
});
|
|
13696
13796
|
var ListStateRequestSchema = exports_external.object({
|
|
13697
13797
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
@@ -14033,13 +14133,17 @@ class AuthBrokerClient {
|
|
|
14033
14133
|
sock.destroy();
|
|
14034
14134
|
}
|
|
14035
14135
|
}
|
|
14036
|
-
async getCredentials(provider) {
|
|
14136
|
+
async getCredentials(provider, account) {
|
|
14037
14137
|
const base = {
|
|
14038
14138
|
v: PROTOCOL_VERSION,
|
|
14039
14139
|
id: randomUUID(),
|
|
14040
14140
|
op: "get-credentials"
|
|
14041
14141
|
};
|
|
14042
|
-
|
|
14142
|
+
let req = base;
|
|
14143
|
+
if (provider !== undefined)
|
|
14144
|
+
req = { ...req, provider };
|
|
14145
|
+
if (account !== undefined)
|
|
14146
|
+
req = { ...req, account };
|
|
14043
14147
|
const data = await this.send(req);
|
|
14044
14148
|
return data;
|
|
14045
14149
|
}
|
|
@@ -16983,11 +16983,52 @@ var AgentGoogleWorkspaceConfigSchema = exports_external.object({
|
|
|
16983
16983
|
approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card."),
|
|
16984
16984
|
tier: GoogleWorkspaceTierSchema.optional().describe("Per-agent tier override (RFC G Phase 1). When set, replaces the " + "top-level google_workspace.tier for this agent. Common case: most " + "agents on `core`, one specialist on `extended` for Slides access.")
|
|
16985
16985
|
}).optional();
|
|
16986
|
+
var MicrosoftAccountEmailSchema = exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
16987
|
+
message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
|
|
16988
|
+
}).transform((v) => v.trim().toLowerCase());
|
|
16989
|
+
var MicrosoftToolTokenSchema = exports_external.string().min(1).regex(/^[a-z0-9-]+$/, {
|
|
16990
|
+
message: "microsoft_workspace tools[] tokens are forwarded to softeria's " + "--enabled-tools regex; each must be lowercase kebab-case " + "([a-z0-9-]+, e.g. 'mail', 'calendar', 'send-mail') so an unescaped " + "regex metacharacter can't crash the launcher"
|
|
16991
|
+
});
|
|
16992
|
+
var MicrosoftAccountBindingSchema = exports_external.object({
|
|
16993
|
+
account: MicrosoftAccountEmailSchema.describe("The Microsoft account this binding uses. Must be a key in top-level " + "`microsoft_accounts:` with this agent in its `enabled_for[]`."),
|
|
16994
|
+
tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist → softeria `--enabled-tools <regex>` " + "(tokens joined with `|`). Omitted = all tools exposed for this account."),
|
|
16995
|
+
org_mode: exports_external.boolean().optional().describe("Per-binding org_mode override (RFC #1873 §6.4).")
|
|
16996
|
+
});
|
|
16986
16997
|
var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
16987
|
-
account:
|
|
16988
|
-
|
|
16989
|
-
|
|
16990
|
-
|
|
16998
|
+
account: MicrosoftAccountEmailSchema.optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized). Mutually exclusive with `accounts` (plural)."),
|
|
16999
|
+
tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist for the SINGULAR `account` form → " + "softeria `--enabled-tools <regex>`. Omitted = all tools. Only " + "valid with the singular `account`; using it together with the " + "plural `accounts` (which carries per-binding `tools`) is an error."),
|
|
17000
|
+
org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 §6.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false)."),
|
|
17001
|
+
accounts: exports_external.array(MicrosoftAccountBindingSchema).min(1).optional().describe("Plural multi-account form: bind MULTIPLE Microsoft accounts to " + "this agent, each with its own tool scope. Mutually exclusive with " + "the singular `account`. Each account gets its own `ms-365-<slug>` " + "MCP server.")
|
|
17002
|
+
}).superRefine((v, ctx) => {
|
|
17003
|
+
const hasSingular = v.account !== undefined;
|
|
17004
|
+
const hasPlural = v.accounts !== undefined;
|
|
17005
|
+
if (hasSingular && hasPlural) {
|
|
17006
|
+
ctx.addIssue({
|
|
17007
|
+
code: exports_external.ZodIssueCode.custom,
|
|
17008
|
+
message: "microsoft_workspace: use EITHER `account` (singular) OR " + "`accounts` (plural array), not both",
|
|
17009
|
+
path: ["accounts"]
|
|
17010
|
+
});
|
|
17011
|
+
}
|
|
17012
|
+
if (hasPlural && v.tools !== undefined) {
|
|
17013
|
+
ctx.addIssue({
|
|
17014
|
+
code: exports_external.ZodIssueCode.custom,
|
|
17015
|
+
message: "microsoft_workspace: block-level `tools` applies to the singular " + "`account` only; with `accounts` put `tools` inside each binding",
|
|
17016
|
+
path: ["tools"]
|
|
17017
|
+
});
|
|
17018
|
+
}
|
|
17019
|
+
if (hasPlural) {
|
|
17020
|
+
const seen = new Set;
|
|
17021
|
+
for (const b of v.accounts) {
|
|
17022
|
+
if (seen.has(b.account)) {
|
|
17023
|
+
ctx.addIssue({
|
|
17024
|
+
code: exports_external.ZodIssueCode.custom,
|
|
17025
|
+
message: `microsoft_workspace: duplicate account '${b.account}' in accounts[]`,
|
|
17026
|
+
path: ["accounts"]
|
|
17027
|
+
});
|
|
17028
|
+
}
|
|
17029
|
+
seen.add(b.account);
|
|
17030
|
+
}
|
|
17031
|
+
}
|
|
16991
17032
|
}).optional();
|
|
16992
17033
|
var AgentNotionWorkspaceConfigSchema = exports_external.object({
|
|
16993
17034
|
databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
|
|
@@ -17362,6 +17403,26 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
17362
17403
|
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
17363
17404
|
checkServes(a.serves, ["agents", name, "serves"]);
|
|
17364
17405
|
}
|
|
17406
|
+
const microsoftAccounts = cfg.microsoft_accounts;
|
|
17407
|
+
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
17408
|
+
const mw = a.microsoft_workspace;
|
|
17409
|
+
const accounts = mw?.accounts;
|
|
17410
|
+
if (!accounts)
|
|
17411
|
+
continue;
|
|
17412
|
+
accounts.forEach((b, i) => {
|
|
17413
|
+
const acct = b?.account?.trim().toLowerCase();
|
|
17414
|
+
if (!acct)
|
|
17415
|
+
return;
|
|
17416
|
+
const enabledFor = microsoftAccounts?.[acct]?.enabled_for ?? [];
|
|
17417
|
+
if (!enabledFor.includes(name)) {
|
|
17418
|
+
ctx.addIssue({
|
|
17419
|
+
code: exports_external.ZodIssueCode.custom,
|
|
17420
|
+
message: `agent '${name}' binds Microsoft account '${acct}' but is not in ` + `microsoft_accounts['${acct}'].enabled_for[] — operator must run ` + `\`switchroom auth microsoft enable ${acct} ${name}\``,
|
|
17421
|
+
path: ["agents", name, "microsoft_workspace", "accounts", i, "account"]
|
|
17422
|
+
});
|
|
17423
|
+
}
|
|
17424
|
+
});
|
|
17425
|
+
}
|
|
17365
17426
|
});
|
|
17366
17427
|
|
|
17367
17428
|
// src/config/paths.ts
|
|
@@ -17956,6 +18017,22 @@ function validateNotionWorkspaceConfig(config) {
|
|
|
17956
18017
|
return issues;
|
|
17957
18018
|
}
|
|
17958
18019
|
|
|
18020
|
+
// src/config/timezone.ts
|
|
18021
|
+
function isResolvableTimezone(zone) {
|
|
18022
|
+
try {
|
|
18023
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
18024
|
+
return true;
|
|
18025
|
+
} catch {
|
|
18026
|
+
return false;
|
|
18027
|
+
}
|
|
18028
|
+
}
|
|
18029
|
+
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
18030
|
+
"UTC",
|
|
18031
|
+
"Etc/UTC",
|
|
18032
|
+
"Etc/Universal",
|
|
18033
|
+
"Universal"
|
|
18034
|
+
]);
|
|
18035
|
+
|
|
17959
18036
|
// src/config/loader.ts
|
|
17960
18037
|
class ConfigError extends Error {
|
|
17961
18038
|
details;
|
|
@@ -18079,8 +18156,30 @@ function loadConfig(configPath) {
|
|
|
18079
18156
|
if (notionIssues.length > 0) {
|
|
18080
18157
|
throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
|
|
18081
18158
|
}
|
|
18159
|
+
validateAllTimezones(config, filePath);
|
|
18082
18160
|
return config;
|
|
18083
18161
|
}
|
|
18162
|
+
function validateAllTimezones(config, filePath) {
|
|
18163
|
+
const issues = [];
|
|
18164
|
+
const check = (zone, where) => {
|
|
18165
|
+
if (zone == null)
|
|
18166
|
+
return;
|
|
18167
|
+
if (!isResolvableTimezone(zone)) {
|
|
18168
|
+
issues.push(` ${where}: "${zone}" is not a resolvable IANA timezone ` + `(shape is valid but no such zone exists — check for a typo, ` + `e.g. "Australia/Melbourne", "America/New_York", "UTC").`);
|
|
18169
|
+
}
|
|
18170
|
+
};
|
|
18171
|
+
check(config.switchroom?.timezone, "switchroom.timezone");
|
|
18172
|
+
check(config.defaults?.timezone, "defaults.timezone");
|
|
18173
|
+
for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
|
|
18174
|
+
check(profile?.timezone, `profiles.${profileName}.timezone`);
|
|
18175
|
+
}
|
|
18176
|
+
for (const [agentName, agentRaw] of Object.entries(config.agents)) {
|
|
18177
|
+
check(agentRaw?.timezone, `agents.${agentName}.timezone`);
|
|
18178
|
+
}
|
|
18179
|
+
if (issues.length > 0) {
|
|
18180
|
+
throw new ConfigError(`Invalid timezone configuration in ${filePath}`, issues);
|
|
18181
|
+
}
|
|
18182
|
+
}
|
|
18084
18183
|
function validateAllCronTopicAliases(config, filePath) {
|
|
18085
18184
|
const issues = [];
|
|
18086
18185
|
for (const [agentName, agentRaw] of Object.entries(config.agents)) {
|
|
@@ -18179,14 +18278,6 @@ function allocateAgentUid(name) {
|
|
|
18179
18278
|
return AGENT_UID_MIN + u32 % range;
|
|
18180
18279
|
}
|
|
18181
18280
|
|
|
18182
|
-
// src/config/timezone.ts
|
|
18183
|
-
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
18184
|
-
"UTC",
|
|
18185
|
-
"Etc/UTC",
|
|
18186
|
-
"Etc/Universal",
|
|
18187
|
-
"Universal"
|
|
18188
|
-
]);
|
|
18189
|
-
|
|
18190
18281
|
// src/cli/agent-config.ts
|
|
18191
18282
|
import { join } from "node:path";
|
|
18192
18283
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -18221,63 +18312,6 @@ for (const name of SHARED_FRAGMENTS) {
|
|
|
18221
18312
|
}
|
|
18222
18313
|
}
|
|
18223
18314
|
|
|
18224
|
-
// src/agents/pane-lock.ts
|
|
18225
|
-
var tails = new Map;
|
|
18226
|
-
|
|
18227
|
-
// src/agents/inject.ts
|
|
18228
|
-
var INJECT_COMMANDS = new Map([
|
|
18229
|
-
["/cost", { description: "Show session cost", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18230
|
-
["/status", { description: "Show session status", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18231
|
-
["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18232
|
-
["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18233
|
-
["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18234
|
-
["/help", { description: "Show help / command discovery", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18235
|
-
["/context", { description: "Show context window usage", expectsOutput: true, argsAllowed: false }],
|
|
18236
|
-
["/release-notes", { description: "Show release-notes version list", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18237
|
-
["/model", { description: "Open model picker", expectsOutput: true, argsAllowed: true }],
|
|
18238
|
-
[
|
|
18239
|
-
"/clear",
|
|
18240
|
-
{
|
|
18241
|
-
description: "Clear session screen",
|
|
18242
|
-
expectsOutput: false,
|
|
18243
|
-
silentNote: "context cleared — fresh slate",
|
|
18244
|
-
argsAllowed: false
|
|
18245
|
-
}
|
|
18246
|
-
],
|
|
18247
|
-
[
|
|
18248
|
-
"/compact",
|
|
18249
|
-
{
|
|
18250
|
-
description: "Compact conversation history",
|
|
18251
|
-
expectsOutput: false,
|
|
18252
|
-
silentNote: "compaction runs silently",
|
|
18253
|
-
argsAllowed: false
|
|
18254
|
-
}
|
|
18255
|
-
]
|
|
18256
|
-
]);
|
|
18257
|
-
var INJECT_ALLOWLIST = new Set(INJECT_COMMANDS.keys());
|
|
18258
|
-
var INJECT_BLOCKED = new Map([
|
|
18259
|
-
["/login", { reason: "would mutate auth state" }],
|
|
18260
|
-
["/logout", { reason: "would terminate the agent's auth session" }],
|
|
18261
|
-
["/exit", { reason: "would kill the agent process" }],
|
|
18262
|
-
["/quit", { reason: "would kill the agent process" }],
|
|
18263
|
-
["/upgrade", { reason: "mutates the Claude Code installation" }],
|
|
18264
|
-
["/init", { reason: "generates/overwrites CLAUDE.md and runs a model turn" }],
|
|
18265
|
-
["/mcp", { reason: "opens an interactive MCP server management dialog" }],
|
|
18266
|
-
["/permissions", { reason: "opens an interactive permissions editor that mutates tool policy" }],
|
|
18267
|
-
["/install-github-app", { reason: "runs a network/OAuth install flow" }],
|
|
18268
|
-
["/add-dir", { reason: "mutates the session's working-directory set" }],
|
|
18269
|
-
["/terminal-setup", { reason: "mutates terminal keybinding configuration" }],
|
|
18270
|
-
["/privacy-settings", { reason: "opens an interactive privacy-settings dialog" }],
|
|
18271
|
-
["/bug", { reason: "submits a bug report over the network" }],
|
|
18272
|
-
[
|
|
18273
|
-
"/effort",
|
|
18274
|
-
{
|
|
18275
|
-
reason: "leaves a blocking confirmation modal open and wedges the pane; use the /effort command (it drives the modal), not raw inject"
|
|
18276
|
-
}
|
|
18277
|
-
]
|
|
18278
|
-
]);
|
|
18279
|
-
var INJECT_BLOCKLIST = new Set(INJECT_BLOCKED.keys());
|
|
18280
|
-
|
|
18281
18315
|
// src/setup/hindsight.ts
|
|
18282
18316
|
var HINDSIGHT_DEFAULT_API_PORT = 18888;
|
|
18283
18317
|
var HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
|
|
@@ -18305,6 +18339,29 @@ var PROFILE_MEMORY_DEFAULTS = {
|
|
|
18305
18339
|
}
|
|
18306
18340
|
};
|
|
18307
18341
|
|
|
18342
|
+
// src/config/microsoft-workspace-acl.ts
|
|
18343
|
+
function normalizeMicrosoftBindings(mw) {
|
|
18344
|
+
if (!mw)
|
|
18345
|
+
return [];
|
|
18346
|
+
if (mw.accounts !== undefined) {
|
|
18347
|
+
return mw.accounts.filter((b) => b && typeof b.account === "string" && b.account.length > 0).map((b) => ({
|
|
18348
|
+
account: b.account.trim().toLowerCase(),
|
|
18349
|
+
tools: b.tools,
|
|
18350
|
+
org_mode: b.org_mode
|
|
18351
|
+
}));
|
|
18352
|
+
}
|
|
18353
|
+
if (typeof mw.account === "string" && mw.account.length > 0) {
|
|
18354
|
+
return [
|
|
18355
|
+
{
|
|
18356
|
+
account: mw.account.trim().toLowerCase(),
|
|
18357
|
+
tools: mw.tools,
|
|
18358
|
+
org_mode: mw.org_mode
|
|
18359
|
+
}
|
|
18360
|
+
];
|
|
18361
|
+
}
|
|
18362
|
+
return [];
|
|
18363
|
+
}
|
|
18364
|
+
|
|
18308
18365
|
// src/agents/reconcile-default-skills.ts
|
|
18309
18366
|
var warnedMissingPool = new Set;
|
|
18310
18367
|
|
|
@@ -18662,6 +18719,12 @@ var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
|
|
|
18662
18719
|
var DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin";
|
|
18663
18720
|
var DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
|
|
18664
18721
|
|
|
18722
|
+
// src/vault/grants-db-path.ts
|
|
18723
|
+
var GRANTS_DB_DIRNAME = "vault-broker";
|
|
18724
|
+
var GRANTS_DB_FILENAME = "vault-grants.db";
|
|
18725
|
+
var GRANTS_DB_CONTAINER_DIR = `/root/.switchroom/${GRANTS_DB_DIRNAME}`;
|
|
18726
|
+
var GRANTS_DB_CONTAINER_PATH = `${GRANTS_DB_CONTAINER_DIR}/${GRANTS_DB_FILENAME}`;
|
|
18727
|
+
|
|
18665
18728
|
// src/agents/compose.ts
|
|
18666
18729
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
18667
18730
|
|
|
@@ -19966,7 +20029,8 @@ var GetCredentialsRequestSchema = exports_external.object({
|
|
|
19966
20029
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
19967
20030
|
op: exports_external.literal("get-credentials"),
|
|
19968
20031
|
id: exports_external.string().min(1),
|
|
19969
|
-
provider: ProviderNameSchema.optional()
|
|
20032
|
+
provider: ProviderNameSchema.optional(),
|
|
20033
|
+
account: exports_external.string().min(1).optional()
|
|
19970
20034
|
});
|
|
19971
20035
|
var ListStateRequestSchema = exports_external.object({
|
|
19972
20036
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
@@ -20656,7 +20720,7 @@ class AuthBroker {
|
|
|
20656
20720
|
break;
|
|
20657
20721
|
}
|
|
20658
20722
|
if (provider === "microsoft") {
|
|
20659
|
-
await this.opMicrosoftGetCredentials(socket, reqId, identity);
|
|
20723
|
+
await this.opMicrosoftGetCredentials(socket, reqId, identity, req.account);
|
|
20660
20724
|
break;
|
|
20661
20725
|
}
|
|
20662
20726
|
socket.write(encodeError(reqId, "INTERNAL", `unhandled provider '${provider}' in get-credentials dispatch`));
|
|
@@ -21755,14 +21819,31 @@ class AuthBroker {
|
|
|
21755
21819
|
this.audit({ op: "rm-account", identity, account: label, accountKind: "google", ok: true });
|
|
21756
21820
|
socket.write(encodeSuccess(id, { label }));
|
|
21757
21821
|
}
|
|
21758
|
-
async opMicrosoftGetCredentials(socket, id, identity) {
|
|
21822
|
+
async opMicrosoftGetCredentials(socket, id, identity, requestedAccount) {
|
|
21759
21823
|
if (identity.kind !== "agent") {
|
|
21760
21824
|
socket.write(encodeError(id, "INVALID_ARGS", `Microsoft get-credentials is per-agent only (caller kind '${identity.kind}' not supported); use the agent's per-agent socket bind`));
|
|
21761
21825
|
return;
|
|
21762
21826
|
}
|
|
21763
21827
|
const agentName = identity.name;
|
|
21764
21828
|
const agent = (this.config.agents ?? {})[agentName];
|
|
21765
|
-
const
|
|
21829
|
+
const bindings = normalizeMicrosoftBindings(agent?.microsoft_workspace);
|
|
21830
|
+
let account;
|
|
21831
|
+
if (requestedAccount !== undefined) {
|
|
21832
|
+
const wanted = requestedAccount.trim().toLowerCase();
|
|
21833
|
+
const bound = bindings.find((b) => b.account === wanted);
|
|
21834
|
+
if (!bound) {
|
|
21835
|
+
this.audit({ op: "get-credentials", identity, account: wanted, accountKind: "microsoft", ok: false, error: "account-not-bound" });
|
|
21836
|
+
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `agent '${agentName}' is not bound to Microsoft account '${wanted}' in switchroom.yaml microsoft_workspace`));
|
|
21837
|
+
return;
|
|
21838
|
+
}
|
|
21839
|
+
account = bound.account;
|
|
21840
|
+
} else if (bindings.length > 1) {
|
|
21841
|
+
this.audit({ op: "get-credentials", identity, accountKind: "microsoft", ok: false, error: "account-ambiguous" });
|
|
21842
|
+
socket.write(encodeError(id, "INVALID_ARGS", `agent '${agentName}' is bound to ${bindings.length} Microsoft accounts (${bindings.map((b) => b.account).join(", ")}); an account must be specified — call getCredentials("microsoft", <account>)`));
|
|
21843
|
+
return;
|
|
21844
|
+
} else {
|
|
21845
|
+
account = bindings[0]?.account;
|
|
21846
|
+
}
|
|
21766
21847
|
if (!account) {
|
|
21767
21848
|
this.audit({ op: "get-credentials", identity, accountKind: "microsoft", ok: false, error: "no-microsoft-account-configured" });
|
|
21768
21849
|
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `agent '${agentName}' has no microsoft_workspace.account configured in switchroom.yaml`));
|
|
@@ -4721,7 +4721,8 @@ var GetCredentialsRequestSchema = exports_external.object({
|
|
|
4721
4721
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
4722
4722
|
op: exports_external.literal("get-credentials"),
|
|
4723
4723
|
id: exports_external.string().min(1),
|
|
4724
|
-
provider: ProviderNameSchema.optional()
|
|
4724
|
+
provider: ProviderNameSchema.optional(),
|
|
4725
|
+
account: exports_external.string().min(1).optional()
|
|
4725
4726
|
});
|
|
4726
4727
|
var ListStateRequestSchema = exports_external.object({
|
|
4727
4728
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
@@ -5063,13 +5064,17 @@ class AuthBrokerClient {
|
|
|
5063
5064
|
sock.destroy();
|
|
5064
5065
|
}
|
|
5065
5066
|
}
|
|
5066
|
-
async getCredentials(provider) {
|
|
5067
|
+
async getCredentials(provider, account) {
|
|
5067
5068
|
const base = {
|
|
5068
5069
|
v: PROTOCOL_VERSION,
|
|
5069
5070
|
id: randomUUID2(),
|
|
5070
5071
|
op: "get-credentials"
|
|
5071
5072
|
};
|
|
5072
|
-
|
|
5073
|
+
let req = base;
|
|
5074
|
+
if (provider !== undefined)
|
|
5075
|
+
req = { ...req, provider };
|
|
5076
|
+
if (account !== undefined)
|
|
5077
|
+
req = { ...req, account };
|
|
5073
5078
|
const data = await this.send(req);
|
|
5074
5079
|
return data;
|
|
5075
5080
|
}
|
|
@@ -4009,7 +4009,8 @@ var init_protocol = __esm(() => {
|
|
|
4009
4009
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
4010
4010
|
op: exports_external.literal("get-credentials"),
|
|
4011
4011
|
id: exports_external.string().min(1),
|
|
4012
|
-
provider: ProviderNameSchema.optional()
|
|
4012
|
+
provider: ProviderNameSchema.optional(),
|
|
4013
|
+
account: exports_external.string().min(1).optional()
|
|
4013
4014
|
});
|
|
4014
4015
|
ListStateRequestSchema = exports_external.object({
|
|
4015
4016
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
@@ -4324,13 +4325,17 @@ class AuthBrokerClient {
|
|
|
4324
4325
|
sock.destroy();
|
|
4325
4326
|
}
|
|
4326
4327
|
}
|
|
4327
|
-
async getCredentials(provider) {
|
|
4328
|
+
async getCredentials(provider, account) {
|
|
4328
4329
|
const base = {
|
|
4329
4330
|
v: PROTOCOL_VERSION,
|
|
4330
4331
|
id: randomUUID(),
|
|
4331
4332
|
op: "get-credentials"
|
|
4332
4333
|
};
|
|
4333
|
-
|
|
4334
|
+
let req = base;
|
|
4335
|
+
if (provider !== undefined)
|
|
4336
|
+
req = { ...req, provider };
|
|
4337
|
+
if (account !== undefined)
|
|
4338
|
+
req = { ...req, account };
|
|
4334
4339
|
const data = await this.send(req);
|
|
4335
4340
|
return data;
|
|
4336
4341
|
}
|