Merge remote-tracking branch 'private/cloud/integration' into cloud/multiplayer-gaps

This commit is contained in:
Neil
2026-11-10 21:30:03 -08:00
17 changed files with 223 additions and 137 deletions
+6
View File
@@ -132,6 +132,12 @@ export type Env = {
/** Operator routes (/v1/internal/*): kill switch, drain, bindings. Secret, >= 32 chars; unset = no routes. */
OPERATOR_TOKEN?: string
/**
* Auth's bearer for POST /v1/internal/org-members/removed only (PropelAuth offboarding and
* Orca-side removals). Secret, >= 32 chars; lets auth push removals without holding the
* operator token. The operator token is still accepted on that route.
*/
MEMBER_REMOVED_TOKEN?: string
/** `true` refuses every create/resume/rollover (static kill switch; the D1 flag is the runtime one). */
SANDBOX_CREATES_DISABLED?: string
/** Warm pool size (pool.ts); unset or 0 = off. */
+6 -1
View File
@@ -1150,7 +1150,12 @@ export async function handleRequest(request: Request, env: Env, deps: Deps = {})
return await internalInstallUrl(env, deps, url)
}
if (url.pathname.startsWith('/v1/internal/')) {
if (!(await isOperator(request, env.OPERATOR_TOKEN))) {
// Auth's member-removed push holds a token good for that one route, not the operator token.
const memberRemovedOnly =
url.pathname === '/v1/internal/org-members/removed' &&
request.method === 'POST' &&
(await isOperator(request, env.MEMBER_REMOVED_TOKEN))
if (!memberRemovedOnly && !(await isOperator(request, env.OPERATOR_TOKEN))) {
return fail('not_found', 404)
}
const life = await lifecycleDeps(env, deps)
@@ -180,3 +180,18 @@ describe('pokes', () => {
expect((await t.req(solo, 'GET', '/v1/team/pokes')).body).toEqual({ enabled: false, pokes: [] })
})
})
describe('member-removed token', () => {
it('authorizes only POST /v1/internal/org-members/removed, never other operator routes', async () => {
const MR = 'm'.repeat(40)
const body = { accountId: 'org:acme', userId: 'bob' }
const h = await createHarness({ OPERATOR_TOKEN: 'o'.repeat(40), MEMBER_REMOVED_TOKEN: MR })
expect((await h.call('POST', '/v1/internal/org-members/removed', { bearer: MR, body })).status).toBe(200)
expect((await h.call('GET', '/v1/internal/status', { bearer: MR })).status).toBe(404)
expect((await h.call('POST', '/v1/internal/members/removed', { bearer: MR, body })).status).toBe(404)
// The operator token still works on the route; a short token is never accepted.
expect((await h.call('POST', '/v1/internal/org-members/removed', { bearer: 'o'.repeat(40), body })).status).toBe(200)
const weak = await createHarness({ OPERATOR_TOKEN: 'o'.repeat(40), MEMBER_REMOVED_TOKEN: 'short' })
expect((await weak.call('POST', '/v1/internal/org-members/removed', { bearer: 'short', body })).status).toBe(404)
})
})
@@ -10,7 +10,6 @@ import { useTranslate, type Translate } from '@/i18n/use-translate'
import { useAppStore } from '@/store/index'
import { OrcaProfileSignOutConfirmDialog } from '../../../orca-profiles/OrcaProfileSignOutConfirmDialog'
import { OrcaSignInButton, OrcaSignInWaitNotice } from '../../../OrcaSignInProgress'
import { OrcaCloudSettingsLinks } from '../../orca-cloud/OrcaCloudSettingsLinks'
import { useOrcaCloudApiAvailable } from '@/hooks/useOrcaCloudApiAvailability'
function accountStatusCopy(
@@ -68,7 +67,6 @@ export function OrcaAccountSettingsPane(): React.JSX.Element {
const t = useTranslate()
const authStatus = useAppStore((state) => state.orcaProfileAuthStatus)
const signOut = useAppStore((state) => state.signOutCurrentOrcaProfile)
const openSettingsTarget = useAppStore((state) => state.openSettingsTarget)
const cloudAvailable = useOrcaCloudApiAvailable()
const [signOutOpen, setSignOutOpen] = useState(false)
const [signingOut, setSigningOut] = useState(false)
@@ -133,51 +131,54 @@ export function OrcaAccountSettingsPane(): React.JSX.Element {
{connected ? null : <OrcaSignInWaitNotice className="basis-full" />}
</div>
{connected && cloudAvailable ? (
<OrcaCloudSettingsLinks onOpen={(pane) => openSettingsTarget({ pane, repoId: null })} />
) : null}
<div className="space-y-4 border-t border-border/60 pt-5">
<SettingsGroupHeader
className="px-0"
title={t(
'auto.components.settings.orcaAccount.benefitsTitle',
'Included with your account'
)}
/>
<div className="space-y-5">
<div className="grid gap-5 md:grid-cols-2 md:gap-0 md:divide-x md:divide-border/60">
{/* JA-16: with Orca Cloud the page is identity + Sign out only; plan, team and agents have
their own entries in the Orca Cloud group right beside it. */}
{cloudAvailable ? null : (
<div className="space-y-4 border-t border-border/60 pt-5">
<SettingsGroupHeader
className="px-0"
title={t(
'auto.components.settings.orcaAccount.benefitsTitle',
'Included with your account'
)}
/>
<div className="space-y-5">
<div className="grid gap-5 md:grid-cols-2 md:gap-0 md:divide-x md:divide-border/60">
<AccountBenefit
icon={Files}
className="md:pr-6"
title={t(
'auto.components.settings.orcaAccount.artifactsTitle',
'Artifact sharing'
)}
description={t(
'auto.components.settings.orcaAccount.artifactsDescription',
'Publish HTML and Markdown files, then manage every shared link from Orca.'
)}
/>
<AccountBenefit
icon={Smartphone}
className="md:pl-6"
title={t('auto.components.settings.orcaAccount.relayTitle', 'Orca Relay')}
description={t(
'auto.components.settings.orcaAccount.relayDescription',
'Connect Orca Mobile to this desktop across cellular or any Wi-Fi.'
)}
/>
</div>
{/* Put the third benefit below at full width so it cannot squeeze the existing pair. */}
<AccountBenefit
icon={Files}
className="md:pr-6"
title={t('auto.components.settings.orcaAccount.artifactsTitle', 'Artifact sharing')}
icon={BookOpen}
className="border-t border-border/60 pt-5"
title={t('auto.components.settings.orcaAccount.skillsTitle', 'Skill sharing')}
description={t(
'auto.components.settings.orcaAccount.artifactsDescription',
'Publish HTML and Markdown files, then manage every shared link from Orca.'
)}
/>
<AccountBenefit
icon={Smartphone}
className="md:pl-6"
title={t('auto.components.settings.orcaAccount.relayTitle', 'Orca Relay')}
description={t(
'auto.components.settings.orcaAccount.relayDescription',
'Connect Orca Mobile to this desktop across cellular or any Wi-Fi.'
'auto.components.settings.orcaAccount.skillsDescription',
'Share one skill or a whole set behind an unlisted link, and install them on any machine you use.'
)}
/>
</div>
{/* Put the third benefit below at full width so it cannot squeeze the existing pair. */}
<AccountBenefit
icon={BookOpen}
className="border-t border-border/60 pt-5"
title={t('auto.components.settings.orcaAccount.skillsTitle', 'Skill sharing')}
description={t(
'auto.components.settings.orcaAccount.skillsDescription',
'Share one skill or a whole set behind an unlisted link, and install them on any machine you use.'
)}
/>
</div>
</div>
)}
</div>
<OrcaProfileSignOutConfirmDialog
@@ -21,14 +21,13 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Switch } from '@/components/ui/switch'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import type {
CloudApiProject,
Routine,
RoutineRun
} from '../../../../../shared/cloud-api/cloud-api-types'
import { PersonSentence } from './PersonName'
import { RoutineRunHistory, type LoadRoutineRuns } from './RoutineRunHistory'
import {
describeRoutineTrigger,
@@ -62,7 +61,8 @@ export function RoutineRow({
onLatestRun,
onOpenWorkspace,
fixedNow,
viewerUserId
viewerUserId,
memberNames
}: {
routine: Routine
projects: readonly CloudApiProject[]
@@ -79,6 +79,7 @@ export function RoutineRow({
onOpenWorkspace?: (workspaceId: string) => void
fixedNow?: number
viewerUserId: string | null
memberNames?: ReadonlyMap<string, string>
}): React.JSX.Element {
const t = useTranslate()
const locale = useIntlLocale()
@@ -164,14 +165,9 @@ export function RoutineRow({
{viewerUserId && routine.createdBy !== viewerUserId ? (
<span className="text-muted-foreground">
{' · '}
<PersonSentence
sentence={t(
'auto.components.settings.cloudRoutines.createdByPerson',
'by <person></person>'
)}
name={routine.createdByName}
id={routine.createdBy}
/>
{t('auto.components.settings.cloudRoutines.createdByName', 'by {{name}}', {
name: creatorName(routine, memberNames, t)
})}
</span>
) : null}
</p>
@@ -321,3 +317,16 @@ export function RoutineRow({
</li>
)
}
/** Display name only, never a raw id: the server's name, then the team roster, then "Former member". */
export function creatorName(
routine: Pick<Routine, 'createdBy' | 'createdByName'>,
memberNames: ReadonlyMap<string, string> | undefined,
t: Translate
): string {
return (
routine.createdByName?.trim() ||
memberNames?.get(routine.createdBy)?.trim() ||
t('auto.components.settings.cloudRoutines.formerMember', 'Former member')
)
}
@@ -16,6 +16,7 @@ import { localTimeZone } from './cloud-api-format'
import { toCloudLoadState, type CloudApiFailure, type CloudLoadState } from './cloud-load-state'
import { useOrcaCloudApiAvailability } from '@/hooks/useOrcaCloudApiAvailability'
import { useRequestGeneration } from './use-request-generation'
import { useOrgRoster } from '../../billing/team/use-org-roster'
const RUNS_PAGE_SIZE = 20
@@ -48,6 +49,20 @@ export function CloudRoutinesPane(): React.JSX.Element | null {
const [installations, setInstallations] = useState<CloudApiInstallation[] | null>(null)
const [now, setNow] = useState(() => Date.now())
const timezone = useMemo(() => localTimeZone(), [])
// Creators the server can't name come from the team roster, the same directory Home uses.
const orgId = useAppStore((s) => s.orcaProfileAuthStatus?.cloud?.activeOrgId ?? null)
const roster = useOrgRoster(orgId)
const memberNames = useMemo(() => {
const names = new Map<string, string>()
if (roster.result?.status === 'ok') {
for (const m of roster.result.roster.members) {
if (m.userId && (m.displayName || m.email)) {
names.set(m.userId, m.displayName || m.email)
}
}
}
return names
}, [roster.result])
const word = useCallback(
(failure: CloudApiFailure) => cloudApiErrorCopy(failure.error, t, failure),
[t]
@@ -126,6 +141,7 @@ export function CloudRoutinesPane(): React.JSX.Element | null {
onRefresh={() => void load()}
refreshing={refreshing}
viewerUserId={viewerUserId}
memberNames={memberNames}
onOpenWorkspace={(sandboxId) => void openRunWorkspace(sandboxId)}
onLatestRun={(routine, run) => {
// Keep the row's pill in step with the newest run its open history just fetched.
@@ -48,6 +48,8 @@ export type CloudRoutinesViewProps = {
fixtureNow?: number
/** Routines someone else created name their creator; null while unknown. */
viewerUserId?: string | null
/** Team roster names by user id (the same directory Home uses), for creators the server can't name. */
memberNames?: ReadonlyMap<string, string>
/** A refresh is running while the current list stays on screen. */
refreshing?: boolean
/** Fixtures: start with the editor, history or delete confirmation open. */
@@ -183,6 +185,7 @@ export function CloudRoutinesView(props: CloudRoutinesViewProps): React.JSX.Elem
onOpenWorkspace={props.onOpenWorkspace}
fixedNow={props.fixtureNow}
viewerUserId={props.viewerUserId ?? null}
memberNames={props.memberNames}
onRunNow={async () => {
const failure = await props.onRunNow(routine)
if (!failure) {
@@ -1,5 +1,7 @@
import { describe, expect, it } from 'vitest'
import { CLOUD_API_SCOPES } from '../../../../../shared/cloud-api/cloud-api-types'
import { creatorName } from './CloudRoutineRow'
import { translate } from '@/i18n/i18n'
import {
cloudApiErrorCopy,
describeRoutineRunError,
@@ -137,3 +139,21 @@ describe('run failure tone', () => {
])
})
})
describe('routine creator', () => {
it('shows a display name only, never a raw id', () => {
const roster = new Map([['u_lin', 'Lin Chen']])
expect(
creatorName({ createdBy: 'u_grace', createdByName: 'Grace Hopper' }, roster, translate)
).toBe('Grace Hopper')
expect(creatorName({ createdBy: 'u_lin', createdByName: null }, roster, translate)).toBe(
'Lin Chen'
)
expect(creatorName({ createdBy: 'u_7f3k', createdByName: ' ' }, roster, translate)).toBe(
'Former member'
)
expect(creatorName({ createdBy: 'u_7f3k', createdByName: null }, undefined, translate)).toBe(
'Former member'
)
})
})
@@ -9,7 +9,6 @@ import { FIXTURE_NAMES, fixtureCloudSecretsApi } from '../cloud-secrets/cloud-se
import { CloudMcpSyncPanel } from '../runtime-environments/cloud-mcp/CloudMcpSyncPanel'
import { SettingsSection } from '../section-layout/SettingsSection'
import { OrcaCloudMcpServersView, type OrcaCloudMcpWorkspace } from './OrcaCloudMcpServersPane'
import { OrcaCloudSettingsLinks } from './OrcaCloudSettingsLinks'
import { OrcaCloudSignedOutNotice } from './OrcaCloudSettingsPanes'
const noop = (): void => {}
@@ -197,16 +196,6 @@ export const sessionExpired = defineFixture({
)
})
/** Settings › Orca account, signed in: the Orca Cloud panes are one click away. */
export const accountLinks = defineFixture({
width: 720,
render: () => (
<div className="bg-background p-6">
<OrcaCloudSettingsLinks onOpen={noop} />
</div>
)
})
/** Settings › Agent API keys header: desktop keys are not for Orca Cloud; link to Cloud secrets. */
export const agentApiKeysHeader = defineFixture({
width: 784,
@@ -1,54 +0,0 @@
import { Boxes, ChevronRight, CreditCard, LockKeyhole, Plug, Users } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { SettingsGroupHeader } from '@/components/settings/section-layout/SettingsGroup'
import { useTranslate } from '@/i18n/use-translate'
import type { SettingsNavTarget } from '@/lib/settings-navigation-types'
type OrcaCloudLink = { pane: SettingsNavTarget; icon: LucideIcon; label: string }
/** Settings › Orca account: the Orca Cloud panes live in their own group; this points at them. */
export function OrcaCloudSettingsLinks({
onOpen
}: {
onOpen: (pane: SettingsNavTarget) => void
}): React.JSX.Element {
const t = useTranslate()
const links: OrcaCloudLink[] = [
{
pane: 'cloud-billing',
icon: CreditCard,
label: t('auto.components.billing.title', 'Plan & billing')
},
{ pane: 'cloud-team', icon: Users, label: t('auto.components.billing.team.title', 'Team') },
{ pane: 'cloud-secrets', icon: LockKeyhole, label: t('cloudSecrets.title', 'Cloud secrets') },
{
pane: 'cloud-environments',
icon: Boxes,
label: t('settings.orcaCloud.environments.title', 'Cloud environments')
},
{ pane: 'cloud-mcp', icon: Plug, label: t('cloudMcpSync.title', 'MCP servers') }
]
return (
<nav
className="space-y-3 border-t border-border/60 pt-5"
aria-label={t('settings.groups.orcaCloud', 'Orca Cloud')}
>
<SettingsGroupHeader className="px-0" title={t('settings.groups.orcaCloud', 'Orca Cloud')} />
<ul className="divide-y divide-border/60 overflow-hidden rounded-lg border border-border bg-card shadow-xs">
{links.map(({ pane, icon: Icon, label }) => (
<li key={pane}>
<button
type="button"
onClick={() => onOpen(pane)}
className="flex w-full items-center gap-3 px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring"
>
<Icon aria-hidden className="size-4 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate">{label}</span>
<ChevronRight aria-hidden className="size-4 text-muted-foreground" />
</button>
</li>
))}
</ul>
</nav>
)
}
@@ -24,10 +24,7 @@ export function renderOrcaAccountSettingsSection(
title={t('auto.components.settings.orcaAccount.title', 'Orca account')}
description={
model.orcaCloudApiAvailable
? t(
'settings.orcaAccount.descriptionWithCloud',
'Sign-in, plan, team and Orca Cloud settings.'
)
? t('settings.orcaAccount.descriptionIdentity', 'The Orca account you’re signed in with.')
: t(
'auto.components.settings.orcaAccount.description',
'Share work instantly and reach your desktop from Orca Mobile wherever you are.'
@@ -170,8 +170,8 @@ export function buildSetupSettingsSections(
title: t('auto.components.settings.orcaAccount.title', 'Orca account'),
description: isOrcaCloudApiAvailable
? t(
'settings.orcaAccount.descriptionWithCloud',
'Sign-in, plan, team and Orca Cloud settings.'
'settings.orcaAccount.descriptionIdentity',
'The Orca account you’re signed in with.'
)
: t(
'auto.components.settings.orcaAccount.description',
@@ -1,7 +1,12 @@
import { Bot } from 'lucide-react'
import type { SettingsNavSection } from '@/lib/settings-navigation-types'
import { describe, expect, it } from 'vitest'
import { buildSetupSettingsSections } from './settings-navigation-capability-sections'
import type { SettingsNavigationBuildOptions } from './settings-navigation-build-options'
import { buildOrcaCloudSettingsSections } from './settings-navigation-orca-cloud-sections'
import {
buildOrcaCloudSettingsSections,
gatherOrcaCloudSettingsGroup
} from './settings-navigation-orca-cloud-sections'
function options(patch: Partial<SettingsNavigationBuildOptions>): SettingsNavigationBuildOptions {
return {
@@ -45,7 +50,7 @@ describe('Orca Cloud settings group', () => {
const setup = buildSetupSettingsSections(options({}))
expect(setup.some((section) => section.id.startsWith('cloud-'))).toBe(false)
expect(setup.find((section) => section.id === 'orca-account')?.description).toBe(
'Sign-in, plan, team and Orca Cloud settings.'
'The Orca account you’re signed in with.'
)
const local = buildSetupSettingsSections(options({ isOrcaCloudApiAvailable: false }))
expect(local.find((section) => section.id === 'orca-account')?.description).not.toMatch(
@@ -53,3 +58,44 @@ describe('Orca Cloud settings group', () => {
)
})
})
describe('one Orca Cloud group (JA-16)', () => {
const nav = (id: string, group: SettingsNavSection['group']): SettingsNavSection => ({
id,
title: id,
description: '',
icon: Bot,
searchEntries: [],
group
})
const all = [
nav('agents', 'capabilities'),
nav('accounts', 'capabilities'),
nav('orca-account', 'setup'),
nav('general', 'setup'),
nav('cloud-billing', 'orca-cloud'),
nav('cloud-team', 'orca-cloud'),
nav('automations', 'workflows')
]
it('leads the Orca Cloud group with the Orca account; Agent accounts stays in AI capabilities', () => {
const out = gatherOrcaCloudSettingsGroup(all, options({}))
expect(out.map((section) => `${section.group}:${section.id}`)).toEqual([
'capabilities:agents',
'capabilities:accounts',
'setup:general',
'orca-cloud:orca-account',
'orca-cloud:cloud-billing',
'orca-cloud:cloud-team',
'workflows:automations'
])
})
it('leaves the upstream grouping alone without Orca Cloud', () => {
expect(gatherOrcaCloudSettingsGroup(all, options({ isOrcaCloudApiAvailable: false }))).toEqual(
all
)
expect(gatherOrcaCloudSettingsGroup(all, options({ isWebClient: true }))).toEqual(all)
})
})
@@ -104,3 +104,26 @@ export function buildOrcaCloudSettingsSections(
}
]
}
/**
* JA-16: in Orca Cloud builds the Orca account leads the one Orca Cloud group instead of sitting in
* Set up. Agent accounts stays in AI capabilities: it covers this computer and hosts as much as
* Orca Cloud, and its Orca Cloud part is deep-linked from every cloud entry point.
*/
export function gatherOrcaCloudSettingsGroup(
sections: readonly SettingsNavSection[],
{ isWebClient, isOrcaCloudApiAvailable }: SettingsNavigationBuildOptions
): SettingsNavSection[] {
if (isWebClient || !isOrcaCloudApiAvailable) {
return [...sections]
}
const leadIds = ['orca-account']
const leads = leadIds
.map((id) => sections.find((section) => section.id === id))
.filter((section): section is SettingsNavSection => section !== undefined)
.map((section) => ({ ...section, group: 'orca-cloud' as const }))
const rest = sections.filter((section) => !leadIds.includes(section.id))
const firstCloud = rest.findIndex((section) => section.group === 'orca-cloud')
const at = firstCloud === -1 ? rest.length : firstCloud
return [...rest.slice(0, at), ...leads, ...rest.slice(at)]
}
@@ -31,7 +31,10 @@ import {
buildSetupSettingsSections
} from './settings-navigation-capability-sections'
import type { SettingsNavigationBuildOptions } from './settings-navigation-build-options'
import { buildOrcaCloudSettingsSections } from './settings-navigation-orca-cloud-sections'
import {
buildOrcaCloudSettingsSections,
gatherOrcaCloudSettingsGroup
} from './settings-navigation-orca-cloud-sections'
import { buildInterfaceSettingsSections } from './settings-navigation-interface-sections'
import { buildRemoteSettingsSections } from './settings-navigation-remote-sections'
import { buildWorkflowSettingsSections } from './settings-navigation-workflow-sections'
@@ -110,14 +113,17 @@ export function buildSettingsNavigationMetadata(
}
// Match SETTINGS_NAV_GROUPS order so Settings and Cmd+J keep siblings together.
return [
...buildCapabilitySettingsSections(options, t),
...buildSetupSettingsSections(options, t),
...buildOrcaCloudSettingsSections(options, t),
...buildWorkflowSettingsSections(options, terminalPaneSearchEntries, t),
...buildInterfaceSettingsSections(options, t),
...buildRemoteSettingsSections(options, runtimeEnvironmentsSearchEntry, reposById, t)
]
return gatherOrcaCloudSettingsGroup(
[
...buildCapabilitySettingsSections(options, t),
...buildSetupSettingsSections(options, t),
...buildOrcaCloudSettingsSections(options, t),
...buildWorkflowSettingsSections(options, terminalPaneSearchEntries, t),
...buildInterfaceSettingsSections(options, t),
...buildRemoteSettingsSections(options, runtimeEnvironmentsSearchEntry, reposById, t)
],
options
)
}
export function useSettingsNavigationMetadata(): SettingsNavSection[] {
+3
View File
@@ -2478,6 +2478,9 @@
"sendingNotConfigured": "sending diagnostics is not configured for this build"
}
},
"groups": {
"orcaCloud": "Orca Cloud"
},
"mcp": {
"serverCount_one": "{{count}} server",
"serverCount_other": "{{count}} servers"
+3 -2
View File
@@ -1397,7 +1397,7 @@
"orcaCloud": "Orca Cloud"
},
"orcaAccount": {
"descriptionWithCloud": "Sign-in, plan, team and Orca Cloud settings."
"descriptionIdentity": "The Orca account you’re signed in with."
},
"agentAccounts": {
"searchTitle": "Agent accounts",
@@ -14603,7 +14603,8 @@
},
"cloudRoutines": {
"pausedSr": "(paused)",
"createdByPerson": "by <person></person>",
"createdByName": "by {{name}}",
"formerMember": "Former member",
"lastRun": "Last run:",
"neverRun": "Never run",
"nextRun": "Next run {{time}}",