mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
* fix(artifacts): gate agent artifact publishing behind an off-by-default capability Public artifact sharing was reachable by any agent through `orca artifacts share`: the Artifacts settings toggle only controlled sidebar visibility, and nothing in the main process checked a capability before minting a public URL. Add `artifactSharingEnabled` (default off) and enforce it in ArtifactCloudService.share/update — before auth, network, or the share-record write — so the CLI, relay-forwarded remote CLI, and IPC paths are all denied. The denial carries a stable `artifact_sharing_disabled` code plus next steps through the RPC error allowlist, so the CLI prints actionable guidance. list, unshare, and delete stay ungated: turning publishing off must not strand already-published links. The capability is absent from the `settings.update` RPC schema, so an agent cannot grant it to itself — only the desktop UI can. Co-authored-by: Orca <help@stably.ai> * fix(artifacts): gate agent artifact publishing behind an off-by-default Publishing is blocked until enabled in Settings → Artifacts. CLI preflights the capability before reading files to avoid unnecessary uploads. RPC surface rejects capability grants so callers cannot self-grant. UI shows opt-in workflow and recovery path when publishing is off. Web clients mirror the host's setting read-only. --------- Co-authored-by: Orca <help@stably.ai>
195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
// Why: every RPC response needs the same runtimeId envelope, and the
|
|
// runtime/browser error allowlists define the contract the CLI relies on to
|
|
// format human-facing messages. Centralizing this mapping keeps the allowlist
|
|
// auditable in one place instead of spread across per-method branches.
|
|
import type { RpcEnvelopeMeta, RpcFailure, RpcSuccess } from './core'
|
|
import { computerUseErrorRecoveryData } from '../../../shared/computer-use-error-recovery'
|
|
import { COMPUTER_ERROR_CODES } from '../../../shared/runtime-types'
|
|
import { LINEAR_ERROR_CODES } from '../../../shared/linear-agent-access'
|
|
import { AGENT_SESSION_RPC_ERROR_CODES } from '../../../shared/agent-session-host-authority'
|
|
import { ARTIFACT_SHARING_DISABLED_CODE } from '../../../shared/artifact-sharing-gate'
|
|
|
|
export function successResponse(id: string, meta: RpcEnvelopeMeta, result: unknown): RpcSuccess {
|
|
return {
|
|
id,
|
|
ok: true,
|
|
result,
|
|
_meta: meta
|
|
}
|
|
}
|
|
|
|
export function errorResponse(
|
|
id: string,
|
|
meta: RpcEnvelopeMeta,
|
|
code: string,
|
|
message: string,
|
|
data?: unknown
|
|
): RpcFailure {
|
|
return {
|
|
id,
|
|
ok: false,
|
|
error: data === undefined ? { code, message } : { code, message, data },
|
|
_meta: meta
|
|
}
|
|
}
|
|
|
|
// Why: the OrcaRuntimeService throws plain Error objects whose `message` is
|
|
// actually a stable error code. This allowlist is the contract the CLI relies
|
|
// on — expanding or renaming entries without updating the CLI would silently
|
|
// change user-visible error codes.
|
|
const RUNTIME_PASSTHROUGH_CODES: ReadonlySet<string> = new Set([
|
|
'runtime_unavailable',
|
|
'selector_not_found',
|
|
'selector_ambiguous',
|
|
'terminal_handle_stale',
|
|
'terminal_not_writable',
|
|
'terminal_exited',
|
|
'terminal_gone',
|
|
'terminal_tab_close_timeout',
|
|
'terminal_tab_not_found',
|
|
'terminal_tab_pinned',
|
|
'no_active_terminal',
|
|
'repo_not_found',
|
|
'timeout',
|
|
'invalid_limit',
|
|
'remote_update_manual_required',
|
|
'remote_update_not_available',
|
|
'remote_update_not_downloaded',
|
|
...AGENT_SESSION_RPC_ERROR_CODES
|
|
])
|
|
|
|
const COMPUTER_PASSTHROUGH_CODES: ReadonlySet<string> = new Set(Object.values(COMPUTER_ERROR_CODES))
|
|
const LINEAR_PASSTHROUGH_CODES: ReadonlySet<string> = new Set(LINEAR_ERROR_CODES)
|
|
const STRUCTURED_RUNTIME_PASSTHROUGH_CODES: ReadonlySet<string> = new Set([
|
|
'worktree_id_requires_full_path',
|
|
'run_not_found',
|
|
'run_required',
|
|
'stable_pane_required',
|
|
'consumer_fenced',
|
|
'task_not_found',
|
|
'task_not_startable',
|
|
'dispatch_not_found',
|
|
'dispatch_run_mismatch',
|
|
'dispatch_inactive',
|
|
'worker_identity_changed',
|
|
'cursor_invalid',
|
|
'cursor_dispatch_mismatch',
|
|
'source_changed',
|
|
'transcript_required',
|
|
'server_required',
|
|
'worktree_not_found_on_server',
|
|
'resource_server_mismatch',
|
|
'peer_changed',
|
|
'remote_runtime_unavailable',
|
|
'runtime_timeout',
|
|
'invalid_runtime_response',
|
|
'capability_unsupported',
|
|
'relay_quota_exceeded',
|
|
'dispatch_capability_invalid',
|
|
'agent_unconfigured',
|
|
'terminal_worktree_mismatch',
|
|
'request_mismatch',
|
|
'mutation_ledger_full',
|
|
'legacy_read_only',
|
|
'orchestration_migration_required',
|
|
'operation_unknown',
|
|
'question_not_found',
|
|
'answer_conflict',
|
|
'stale_delivery',
|
|
'waiter_exists',
|
|
'invalid_argument',
|
|
ARTIFACT_SHARING_DISABLED_CODE
|
|
])
|
|
|
|
export function mapRuntimeError(id: string, meta: RpcEnvelopeMeta, error: unknown): RpcFailure {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
if (
|
|
error instanceof Error &&
|
|
'code' in error &&
|
|
typeof (error as { code: unknown }).code === 'string' &&
|
|
COMPUTER_PASSTHROUGH_CODES.has((error as { code: string }).code)
|
|
) {
|
|
const code = (error as { code: string }).code
|
|
return errorResponse(id, meta, code, message, computerErrorData(code, message))
|
|
}
|
|
if (
|
|
error instanceof Error &&
|
|
'code' in error &&
|
|
typeof (error as { code: unknown }).code === 'string' &&
|
|
(error as { code: string }).code.startsWith('LINEAGE_')
|
|
) {
|
|
return errorResponse(
|
|
id,
|
|
meta,
|
|
(error as { code: string }).code,
|
|
message,
|
|
(error as { data?: unknown }).data
|
|
)
|
|
}
|
|
if (
|
|
error instanceof Error &&
|
|
'code' in error &&
|
|
typeof (error as { code: unknown }).code === 'string' &&
|
|
LINEAR_PASSTHROUGH_CODES.has((error as { code: string }).code)
|
|
) {
|
|
return errorResponse(
|
|
id,
|
|
meta,
|
|
(error as { code: string }).code,
|
|
message,
|
|
(error as { data?: unknown }).data
|
|
)
|
|
}
|
|
if (
|
|
error instanceof Error &&
|
|
'code' in error &&
|
|
typeof (error as { code: unknown }).code === 'string' &&
|
|
STRUCTURED_RUNTIME_PASSTHROUGH_CODES.has((error as { code: string }).code)
|
|
) {
|
|
return errorResponse(
|
|
id,
|
|
meta,
|
|
(error as { code: string }).code,
|
|
message,
|
|
(error as { data?: unknown }).data
|
|
)
|
|
}
|
|
if (RUNTIME_PASSTHROUGH_CODES.has(message)) {
|
|
return errorResponse(id, meta, message, message)
|
|
}
|
|
if (message === 'invalid_terminal_send') {
|
|
return errorResponse(id, meta, 'invalid_argument', 'Missing terminal send payload')
|
|
}
|
|
return errorResponse(id, meta, 'runtime_error', message)
|
|
}
|
|
|
|
export const computerErrorData = computerUseErrorRecoveryData
|
|
|
|
// Why: browser errors carry a structured .code property (BrowserError from
|
|
// cdp-bridge.ts) that maps directly to agent-facing error codes. We forward
|
|
// that code rather than falling back to the runtime allowlist, because the
|
|
// browser surface area uses its own code namespace (browser_no_tab, etc.).
|
|
export function mapBrowserError(id: string, meta: RpcEnvelopeMeta, error: unknown): RpcFailure {
|
|
if (
|
|
error instanceof Error &&
|
|
'code' in error &&
|
|
typeof (error as { code: unknown }).code === 'string'
|
|
) {
|
|
return errorResponse(id, meta, (error as { code: string }).code, error.message)
|
|
}
|
|
return mapRuntimeError(id, meta, error)
|
|
}
|
|
|
|
// Why: same as browser — emulator errors (EmulatorError) carry .code (emulator_no_active etc.)
|
|
// so we forward the structured code instead of generic runtime_error.
|
|
export function mapEmulatorError(id: string, meta: RpcEnvelopeMeta, error: unknown): RpcFailure {
|
|
if (
|
|
error instanceof Error &&
|
|
'code' in error &&
|
|
typeof (error as { code: unknown }).code === 'string'
|
|
) {
|
|
return errorResponse(id, meta, (error as { code: string }).code, error.message)
|
|
}
|
|
return mapRuntimeError(id, meta, error)
|
|
}
|