diff --git a/src/core/orchestrator.ts b/src/core/orchestrator.ts index d6071d572..06d7faa98 100644 --- a/src/core/orchestrator.ts +++ b/src/core/orchestrator.ts @@ -41,6 +41,7 @@ import { configuredConnectorProviders, connectorStatusIsStale, refreshConnectorStatus, + CONNECTOR_STATUS_ACCOUNT_TYPES, } from "../credentials/connector-status.ts"; import { renderComputerBlock, renderResidentLoginsBlock, renderConnectedAppsBlock } from "./environment-facts.ts"; import { PROVIDERS } from "../connectors/oauth.ts"; @@ -1069,11 +1070,16 @@ export function createOrchestrator(deps: OrchestratorDeps): Orchestrator { } if (!strictReadOnly && deps.connectorTokens && conversation.kind === "dm") { for (const host of CONNECTOR_HOSTS) { - const token = - (await deps.connectorTokens.connectorAccessToken(host, actor.id, "personal")) ?? - (await deps.connectorTokens.connectorAccessToken(host, actor.id)) ?? - (await deps.connectorTokens.connectorAccessToken(host, actor.id, "company")); + const slots = new Map(); + for (const accountType of CONNECTOR_STATUS_ACCOUNT_TYPES) { + const slotToken = await deps.connectorTokens.connectorAccessToken(host, actor.id, accountType); + if (slotToken) slots.set(accountType ?? "default", slotToken); + } + const token = slots.get("personal") ?? slots.get("default") ?? slots.get("company"); if (token) connectorEnv[envKey(host)] = token; + if (slots.size > 1) { + for (const [slot, slotToken] of slots) connectorEnv[`${envKey(host)}__${slot.toUpperCase()}`] = slotToken; + } } } perf.credsMs += Date.now() - credsStart; diff --git a/test/connector-invariants.test.ts b/test/connector-invariants.test.ts index f0e3f4c24..31fe7787f 100644 --- a/test/connector-invariants.test.ts +++ b/test/connector-invariants.test.ts @@ -225,3 +225,30 @@ test("a read-only wake never reaches the sandbox (execute stripped), so no exec "a read-only wake spins no sandbox exec", ); }); + +test("every connected account slot reaches the exec env, not just the winning one", async () => { + const built: BuiltApp = buildApp(testConfig({ dataDir: mkdtempSync(join(tmpdir(), "slots-")) })); + const host = "gmail.googleapis.com"; + await built.connectorTokens.setConnectorToken(host, "U1", { accessToken: "u1-default" }); + await built.connectorTokens.setConnectorToken(host, "U1", { accessToken: "u1-company" }, "company"); + + fakeSprites.reset(); + const dm = await built.app.turn(turn("dm", "!run true")); + assert.equal(dm.status, "ok"); + assert.ok(execScriptsMention(`export ${envKey(host)}=`), "the primary slot keeps the unsuffixed name"); + assert.ok(execScriptsMention("u1-default"), "the default slot stays the primary token"); + assert.ok(execScriptsMention(`${envKey(host)}__COMPANY=`), "the second slot gets its own variable"); + assert.ok(execScriptsMention("u1-company"), "the second slot's token value is present"); +}); + +test("a single connected slot injects only the unsuffixed variable", async () => { + const built: BuiltApp = buildApp(testConfig({ dataDir: mkdtempSync(join(tmpdir(), "one-slot-")) })); + const host = "gmail.googleapis.com"; + await built.connectorTokens.setConnectorToken(host, "U1", { accessToken: "u1-only" }); + + fakeSprites.reset(); + const dm = await built.app.turn(turn("dm", "!run true")); + assert.equal(dm.status, "ok"); + assert.ok(execScriptsMention(`export ${envKey(host)}=`)); + assert.ok(!execScriptsMention(`${envKey(host)}__`), "no per-slot variables when there is nothing to disambiguate"); +});