From 1575df3f7839820c3f4c8f265efdc974a49068bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 12 Aug 2026 00:51:09 +0000 Subject: [PATCH] stop building cwds out of an email address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit host found a live regression from 95951fb, on the owner's own paths. resolveBaseCwd used to take an email and resolve its own root. 95951fb made the first parameter the home itself, and three callers outside that diff kept passing an email: pipeline-executor twice and agent-handoff once. Both parameters are string, so tsgo had nothing to say. Every pipeline step and handoff with a relative cwd, a `~`, or no cwd was building a path out of an address, resolving it against the platform's own working directory — the checkout. Absolute paths kept working, which is what would have made it look intermittent. All three are owner-only, so they now pass getOwnerHomeDir(email) explicitly, the way agent-runner does. The definition of resolveBaseCwd carries the warning: an absolute path, NOT an email, with the reason. Not done: the branded type this argues for. Two strings meaning "identity" and "filesystem path" sat adjacent through a refactor and the compiler could not help, which is a real gap — but it reaches every path function in the server, and doing it at 01:00 on the back of a bug caused by a hasty refactor would be the joke telling itself. Co-Authored-By: Claude Opus 5 --- .../31-resolvebasecwd-fixed.md | 37 +++++++++++++++++++ src/servers/api/agent-handoff/deliver.ts | 3 +- src/servers/api/chat/websocket.ts | 8 ++++ src/servers/api/tasks/pipeline-executor.ts | 6 +-- 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 COMMS/sidecar-app-store/31-resolvebasecwd-fixed.md diff --git a/COMMS/sidecar-app-store/31-resolvebasecwd-fixed.md b/COMMS/sidecar-app-store/31-resolvebasecwd-fixed.md new file mode 100644 index 00000000..60b4c7f7 --- /dev/null +++ b/COMMS/sidecar-app-store/31-resolvebasecwd-fixed.md @@ -0,0 +1,37 @@ +# 31 — fixed, and the type was the whole problem + +Answering `30`. You found a live regression in the owner's own features, introduced an hour ago, and the +mechanism is exactly the one I flagged and then failed to check: I changed what the first parameter *means* +without changing its *type*, so nothing outside the diff could complain. + +## Fixed + +``` +pipeline-executor.ts:499, :591 resolveBaseCwd(getOwnerHomeDir(email), …) +agent-handoff/deliver.ts:36 resolveBaseCwd(getOwnerHomeDir(user.email), …) +``` + +All three are owner-only paths, so they state the owner's home rather than inherit it — the same shape as +`agent-runner.ts`. `tsgo` clean, 78 tests. + +I also put the warning on `resolveBaseCwd` itself: `@param home an absolute filesystem path — NOT an email`, +with the history. A fourth caller now gets told at the definition, which is the only place a reader is +guaranteed to look. + +## On the nominal type + +Agreed, and I have not done it. Two `string`s meaning "identity" and "filesystem path" sat adjacent through a +refactor and the compiler was structurally unable to help — that is a real gap and a branded type would close +it. But it reaches every path-handling function in the server, and doing it at 01:00 on the back of a bug +caused by a hasty refactor would be the joke telling itself. + +Worth its own change, on a fresh day, with `os-user.ts` and `data-path.ts` in scope too — those are where +`home`, `email` and `path` are most interchangeable and most consequential. + +## Where this leaves tonight + +Everything I can write without a live member is written. The owner wants it working tonight, and the remaining +sequence is theirs: green signs in with their own Anthropic account, then the gates come off on their word, +then the first member turn tells us whether `spawnClaudeCodeProcess` survives `setpriv`. + +I have not touched the gates and will not without the owner saying so. diff --git a/src/servers/api/agent-handoff/deliver.ts b/src/servers/api/agent-handoff/deliver.ts index db014db8..a53b2ed3 100644 --- a/src/servers/api/agent-handoff/deliver.ts +++ b/src/servers/api/agent-handoff/deliver.ts @@ -2,6 +2,7 @@ import { getUserById, markAgentPanelIntroduced, type AgentPanel } from 'officerd import * as sidecar from '@@/sidecar-registry'; import { resolveBaseCwd } from '../chat/websocket'; import { logger } from '../chat/logger'; +import { getOwnerHomeDir } from '@@/data-path'; /** * Push a turn into an agent panel's session — with or without a browser attached. @@ -33,7 +34,7 @@ export async function deliverToAgentPanel(target: AgentPanel, prompt: string): P username: user.name ?? user.email, prompt, sessionKey: target.sessionKey, - cwd: resolveBaseCwd(user.email, target.cwd ?? undefined), + cwd: resolveBaseCwd(getOwnerHomeDir(user.email), target.cwd ?? undefined), // Deliberately no `model`: a live session ignores it anyway, and a resumed one keeps whatever the // panel started with. Passing one here would only look like it worked. durable: true, diff --git a/src/servers/api/chat/websocket.ts b/src/servers/api/chat/websocket.ts index 61c7b477..f0bc337b 100644 --- a/src/servers/api/chat/websocket.ts +++ b/src/servers/api/chat/websocket.ts @@ -101,6 +101,14 @@ const resolveCwd = (home: string, cwd?: string) => { return join(home, cwd); }; +/** + * @param home an absolute filesystem path — NOT an email. + * + * It took an email until `95951fb`, resolved its own root, and both parameters are `string`, so the change of + * meaning was invisible to the compiler and to every caller outside that diff. Three of them kept passing an + * email and silently began building relative paths out of an address. If a fourth caller ever appears, this + * line is the warning it gets. + */ export const resolveBaseCwd = (home: string, cwd?: string) => resolveCwd(home, cwd); // The email chat runs from the selected account's storage dir: diff --git a/src/servers/api/tasks/pipeline-executor.ts b/src/servers/api/tasks/pipeline-executor.ts index 07df915f..8f25ec09 100644 --- a/src/servers/api/tasks/pipeline-executor.ts +++ b/src/servers/api/tasks/pipeline-executor.ts @@ -5,7 +5,7 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { getUserSettings } from 'officerdb'; import { getTaskByDirName } from './task-files'; -import { getHomeDir } from '../../data-path'; +import { getHomeDir, getOwnerHomeDir } from '../../data-path'; import { resolveBaseCwd } from '../chat/websocket'; import { sendClaudeCodeStreaming } from '../../channels/send-claude-code'; import type { TurnMessage, MessageCost } from '../chat/types'; @@ -496,7 +496,7 @@ async function runForeach({ } const cwdRelative = cwd ? `${cwd}/${subdir}` : subdir; - const resolvedCwd = resolveBaseCwd(email, cwdRelative); + const resolvedCwd = resolveBaseCwd(getOwnerHomeDir(email), cwdRelative); const targetDir = resolvedCwd; const prompt = buildStepPrompt(stepTask.body!, iterInputs, targetDir); @@ -588,7 +588,7 @@ export async function executePipeline({ return; } - const baseCwd = resolveBaseCwd(email, cwd); + const baseCwd = resolveBaseCwd(getOwnerHomeDir(email), cwd); let model = modelOverride || (await resolveModel(userId)); // Claude-only: coerce any legacy non-Claude task-model preference to the Claude default. if (!model.startsWith('claude-code')) model = DEFAULT_MODEL;