Files
orca/src/shared/host-operation-ownership.ts
T
fde0fe35a7 fix(daemon): stop giving new terminals to a daemon whose AppImage files are gone (#28011)
* fix(daemon): stop giving new terminals to a daemon whose AppImage files are gone

On Linux AppImage, a terminal daemon can outlive the mount it runs from and
keep answering health checks until it SIGBUSes. Orca now reads the daemon's
/proc exe link and mount table at adoption: an idle severed daemon is
replaced; a busy one is kept for its sessions but quarantined, so fresh
terminals use the local provider and a healthy RPC never re-admits them.
Status and a verified restart are available over RPC and as
`orca terminal-daemon status|restart`.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(daemon): lift the severed quarantine once a new daemon owns the endpoint

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(daemon): a severed-daemon restart counts fallback terminals and never stops a replacement

The restart re-checks the authenticated endpoint owner before any teardown
and again at shutdown, and never falls back to a pid kill when targeted.
Status counts the local fallback terminals the restart also ends.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(daemon): merge duplicate import

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(runtime): classify the terminalDaemon RPC namespace as host-owned

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 08:50:25 -07:00

99 lines
3.2 KiB
TypeScript

/**
* Who decides where an operation runs.
* - `host`: the host that owns the resource (a workspace, terminal, session or account).
* - `credential`: forge APIs. A runtime-owned repo goes to its owner runtime; a local or SSH repo
* uses this desktop's credentials. Never chosen by focus.
* - `client`: an effect on this client (reveal, open in editor); never sent to a host.
* - `stream`: a subscription that lives on the host owning the subscribed resource.
* - `scope-picked`: row-less work whose host comes from an explicit picker, never from focus.
*/
export type HostOperationOwnership = 'host' | 'credential' | 'client' | 'stream' | 'scope-picked'
/** By runtime RPC namespace; client effects by their preload API name. */
export const HOST_OPERATION_OWNERSHIP = {
accounts: 'host',
agent: 'host',
agentHooks: 'host',
agentSession: 'host',
agentSessionAttachment: 'host',
agentTeams: 'host',
aiVault: 'host',
artifacts: 'host',
automation: 'host',
browser: 'host',
clipboard: 'host',
computer: 'host',
diagnostics: 'host',
emulator: 'host',
files: 'host',
folderWorkspace: 'host',
git: 'host',
github: 'credential',
gitlab: 'credential',
host: 'host',
hostedReview: 'credential',
jira: 'scope-picked',
layout: 'host',
linear: 'scope-picked',
managedServer: 'host',
markdown: 'host',
mobileRelay: 'host',
mobileWeb: 'host',
nativeChat: 'host',
network: 'host',
notifications: 'host',
orcad: 'host',
orchestration: 'host',
pairing: 'host',
plugins: 'host',
preflight: 'host',
project: 'host',
projectGroup: 'host',
projectHostSetup: 'host',
reference: 'host',
repo: 'host',
runtime: 'host',
session: 'host',
settings: 'scope-picked',
shell: 'client',
skills: 'host',
speech: 'host',
ssh: 'host',
stats: 'host',
status: 'host',
terminal: 'host',
terminalDaemon: 'host',
ui: 'host',
updater: 'host',
workspacePorts: 'host',
worktree: 'host'
} as const satisfies Record<string, HostOperationOwnership>
/** Methods whose owner differs from their namespace: row-less forge reads use the source picker. */
const HOST_OPERATION_METHOD_OWNERSHIP = {
'github.project.listAccessible': 'scope-picked',
'github.project.listViews': 'scope-picked',
'github.project.resolveRef': 'scope-picked',
'github.project.viewTable': 'scope-picked'
} as const satisfies Record<string, HostOperationOwnership>
const OWNERSHIP_BY_METHOD: ReadonlyMap<string, HostOperationOwnership> = new Map(
Object.entries(HOST_OPERATION_METHOD_OWNERSHIP)
)
const OWNERSHIP_BY_NAMESPACE: ReadonlyMap<string, HostOperationOwnership> = new Map(
Object.entries(HOST_OPERATION_OWNERSHIP)
)
const STREAM_METHOD = /^(un)?(subscribe|watch)/i
/** Null for a namespace nobody classified, so callers fail closed. */
export function getHostOperationOwnership(method: string): HostOperationOwnership | null {
const ownership =
OWNERSHIP_BY_METHOD.get(method) ??
OWNERSHIP_BY_NAMESPACE.get(method.slice(0, method.indexOf('.')))
if (!ownership || !method.includes('.')) {
return null
}
const leaf = method.slice(method.lastIndexOf('.') + 1)
return ownership !== 'client' && STREAM_METHOD.test(leaf) ? 'stream' : ownership
}