Files
orca/src/shared/artifact-sharing-gate.test.ts
JinjingandOrca 3ec48a74d5 Gate artifact publishing behind off-by-default capability (#13368)
* 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>
2026-08-09 13:19:08 -07:00

38 lines
1.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { getDefaultSettings } from './constants'
import {
ARTIFACT_SHARING_DISABLED_CODE,
ArtifactSharingDisabledError,
assertArtifactSharingAllowed,
isArtifactSharingEnabled
} from './artifact-sharing-gate'
describe('artifact sharing capability gate', () => {
it('denies by default and for profiles written before the setting existed', () => {
expect(isArtifactSharingEnabled(getDefaultSettings('/tmp'))).toBe(false)
expect(isArtifactSharingEnabled({})).toBe(false)
expect(isArtifactSharingEnabled(null)).toBe(false)
expect(isArtifactSharingEnabled(undefined)).toBe(false)
})
it('requires an exact true, so a truthy value on disk cannot open the gate', () => {
expect(isArtifactSharingEnabled({ artifactSharingEnabled: true })).toBe(true)
expect(isArtifactSharingEnabled({ artifactSharingEnabled: 'yes' as never })).toBe(false)
expect(isArtifactSharingEnabled({ artifactSharingEnabled: 1 as never })).toBe(false)
})
it('throws a coded, actionable error when the capability is withheld', () => {
expect(() => assertArtifactSharingAllowed(() => false)).toThrow(ArtifactSharingDisabledError)
try {
assertArtifactSharingAllowed(() => false)
expect.unreachable('gate must throw')
} catch (error) {
expect(error).toMatchObject({
code: ARTIFACT_SHARING_DISABLED_CODE,
data: { nextSteps: expect.arrayContaining([expect.stringContaining('Settings')]) }
})
}
expect(() => assertArtifactSharingAllowed(() => true)).not.toThrow()
})
})