opencode is owner-only until it carries an identity
a turn on the opencode harness ran as the owner, in the owner's home, whoever asked. handleOpenCodeChat resolves its cwd against getOwnerHomeDir(email), which discards the email it is given, and the sidecar runs one shared `opencode serve` as the service user — sendOpenCodeStreaming accepts userId/email/username and forwards none of them. it carried a comment calling itself owner-only; nothing enforced it. reachable by any account with the `chat` grant, which every role holds by default (DEFAULT_ROLE_CAPABILITIES), and isClaudeModel is a startsWith, so a typo'd model string landed there too. the model is client-supplied and never checked against the catalogue. the same gap on the read side: opencode's session store has no per-user scoping at all, so loadOpenCodeSession/delete/rename take an id and no identity, and the list and live routes returned other people's conversations. so: ChatIdentity carries isOwner as its own fact (not inferred from osUser === null, which holds only while resolveHomeDir refuses a member without one), and every opencode door in chat.ts checks it — list, load, live, delete, rename — plus a refusal on the execution path in handleChat. /chat/models hides opencode from non-owners as a courtesy; the socket refuses regardless. a stopgap, not a design. the fix is to thread identity through the opencode sidecar the way spawnClaudeAsMember does, and TODO.md has been saying so. not fixed here, and worth knowing: a member's session list is still empty and /chat/pwds still 500s, because readdirSync on their ~/.claude/projects is EACCES — claude creates it at mode 700, which zeroes the ACL mask. visible in officer-error.log right now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import type { TurnMessage } from '../chat/types';
|
||||
import { sendClaudeCodeStreaming } from '../../channels/send-claude-code';
|
||||
import { renameClaudeSession } from '../chat/claude-sessions';
|
||||
import { renameClaudeSession, type ChatIdentity } from '../chat/claude-sessions';
|
||||
import { getAgentRunsDir, getOwnerHomeDir } from '../../data-path';
|
||||
import { getAgentByDirName, DEFAULT_AGENT_MODEL, type AgentRecord } from './agent-files';
|
||||
import { logger } from '../chat/logger';
|
||||
@@ -87,7 +87,7 @@ export function buildAgentPrompt(agent: AgentRecord, inputs: Record<string, unkn
|
||||
* which is fine: while a run is live you find it as the newest entry in the agent's project group.
|
||||
*/
|
||||
function titleRun(
|
||||
who: { email: string; home: string; osUser: string | null },
|
||||
who: ChatIdentity,
|
||||
cwd: string,
|
||||
claudeSessionId: string,
|
||||
agentName: string,
|
||||
@@ -172,10 +172,11 @@ export async function startAgentRun(params: StartAgentRunParams): Promise<StartA
|
||||
if (msg.claudeSessionId) {
|
||||
run.claudeSessionId = msg.claudeSessionId;
|
||||
titleRun(
|
||||
// `osUser: null` tracks `homeDir` above: it is `getOwnerHomeDir`, which discards the email it is
|
||||
// given, so an agent run is always the owner's — its transcript is theirs and readable directly.
|
||||
// `osUser: null` and `isOwner: true` both track `homeDir` above: it is `getOwnerHomeDir`, which
|
||||
// discards the email it is given, so an agent run is always the owner's — its transcript is theirs
|
||||
// and readable directly. `agents` is an `execution` capability, so no other account reaches this.
|
||||
// If agent runs ever reach members, this and line 134 have to move together.
|
||||
{ email: params.user.email, home: homeDir, osUser: null },
|
||||
{ email: params.user.email, home: homeDir, osUser: null, isOwner: true },
|
||||
cwd,
|
||||
msg.claudeSessionId,
|
||||
agent.name || agent.dirName,
|
||||
|
||||
Reference in New Issue
Block a user