mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* feat(agents): store each agent's permission itself instead of typing flags into Arguments Terminals and new chats read one stored choice per agent (Use the agent's own settings, Ask for approval, Accept edits, Approve for me, Full access). Orca adds the flag at launch; Arguments hold only the user's own text. A one-time move lifts Orca's flags out of saved Arguments. Older clients still see and write the flag as part of Arguments; the host translates. * fix(agents): say the all-agents switch resets agents set separately; keep primitives unstyled * test(agents): the one-time move returns every launch-profile field * Fix permission carrier ownership and surface-specific defaults * Preserve uncertain permission operands and name the option inventory * Check env entries without indexing typed agent maps * Combine Chat permission capability imports * Use folder execution-host platform for permission environment * Preserve launch map replacement and acknowledge permission saves * Keep permission feedback fallbacks statically discoverable * Provide preload platform to Agents settings render tests * Preserve Gemini approval arguments under Full access * fix(settings): retain complete environment edits through save outcomes * fix(settings): narrow local failure scope and preserve host messages * fix(settings): retire argument drafts after acknowledged saves * fix(settings): keep Goose environment editable after reset * fix(settings): show resolved arguments for legacy hosts * Preserve pending environment resets and bound launch settings work * Use agent permission choices for new chats * Let Claude resolve chat permissions from its own settings * Keep optional settings fallback in repository metadata reads * Consolidate permission report imports * Update permission choice contracts for own settings * Read repository visibility defaults through the raw settings fixtures * Match existing translated Agents settings labels * Update readback and legacy host test expectations * Align Chat behavior tests with host permission capability * Fix permission compatibility and host-owned descriptions * Prepare whole Environment edits and retire unchanged drafts * Handle null explicitly in permission report decoder * Keep Environment settings transitions in an acyclic module * Update raw settings reader test fixtures * Place test fixture safety notes on assertion starts * test: pin permission settings work and recovery contracts * test: type permission settings work fixtures * test: cover migrated permission inputs and captured CLI grammar * Publish permission reports and reuse host capability state * Combine mobile option state React imports * Honor mobile status feed capability for permission reports * test: preserve typed permissions in WSL Claude restarts * Correct permission report child and fixture type contracts * Delegate older-host chats to explicitly selected own settings * Avoid rebuilding live ACP option catalogs * test(chat): place cross-boundary permission contracts in integration project * Preserve existing chat capability gates and complete report test fixtures * fix(chat): preserve newer Claude startup permission reports * test(chat): clarify startup report fixture provenance * Show reported permission modes in chat menus * Keep reported permissions available after chat mode picks * Remove obsolete guard from concrete permission selection * Report the installed approval policy for ACP chat menus * test: keep accepted mobile mutation fixture on current fence * test: cover installed ACP approval report for own chats * test: align capability scaling contract with concrete permission menus * Ship required permission help in English runtime catalog * Adapt permission persistence test to shared menu
243 lines
10 KiB
TypeScript
243 lines
10 KiB
TypeScript
// Compose shared admission, cwd, environment, visuals and child sealing; agent parts add only agent-specific inputs and never set cwd/sealed env.
|
|
|
|
import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types'
|
|
import type { AgentChatPermissionChoice } from '../../shared/agent-chat-permission-mode'
|
|
import type { AgentSessionRecord } from '../../shared/agent-session-record'
|
|
import { isNativeLocalLocation } from '../../shared/execution-host'
|
|
import type { StructuredAgentDefinition } from '../native-chat/agent-session-wire/structured-agent-definition'
|
|
import { agentDrivesSession } from '../native-chat/agent-session-wire/structured-agent-session-provider-support'
|
|
import type { StructuredAgentCommandSettings } from '../native-chat/structured-agent-command-resolution'
|
|
import {
|
|
createNativeChatVisualsDelivery,
|
|
type NativeChatVisualsLaunch,
|
|
type PrepareNativeChatVisuals
|
|
} from '../native-chat/native-chat-visuals-delivery'
|
|
import { resolveAgentSessionLaunchDirectory } from './agent-session-launch-directory'
|
|
import type { AgentSessionRecordStore } from './agent-session-record-store'
|
|
import type { StructuredAgentAdapterContext } from './structured-agent-runtime-registrations'
|
|
import { sealStructuredSessionChild } from './structured-session-child-env'
|
|
|
|
/** Where a launch's shared inputs come from. */
|
|
export type StructuredAgentLaunchSources = {
|
|
store: Pick<AgentSessionRecordStore, 'getRecord' | 'pinLaunchDirectory'>
|
|
resolveWorkspacePath: (workspaceId: string) => Promise<string>
|
|
resolveBaseEnvironment: () => Promise<Record<string, string>>
|
|
/** The base environment with the user's overlay for `agent` laid over it. */
|
|
resolveAgentEnvironment: (agent: string) => Promise<Record<string, string>>
|
|
/** The user's per-agent environment overlay from settings. */
|
|
resolveAgentLaunchEnv?: (agent: string) => Record<string, string>
|
|
resolveCommandSettings?: () => StructuredAgentCommandSettings
|
|
prepareVisuals?: PrepareNativeChatVisuals
|
|
/** Rereads the base environment after a lookup that started at `askedAt` found no program in
|
|
* it; absent never retries. */
|
|
refreshAfterMissingProgram?: (askedAt: number) => Promise<void>
|
|
}
|
|
|
|
/** Resolve each input lazily once in agent-defined order; refusals before directory reads leave floating records unpinned. */
|
|
export type StructuredAgentLaunchBasis = {
|
|
/** Exists, belongs to this agent, runs on this host, and pins what this agent drives. */
|
|
record: AgentSessionRecord
|
|
launchDirectory(): Promise<string>
|
|
baseEnvironment(): Promise<Record<string, string>>
|
|
/** The user's overlay for this agent; empty when none. */
|
|
launchEnv(): Record<string, string>
|
|
/** The base environment with the user's overlay laid over it. */
|
|
environment(): Promise<Record<string, string>>
|
|
/** Null when the chat gets no visuals. */
|
|
visuals(): Promise<NativeChatVisualsLaunch | null>
|
|
commandSettings(): StructuredAgentCommandSettings
|
|
/** After a missing program, refresh both environment reads together; absent means no retry. */
|
|
refreshAfterMissingProgram?: (askedAt: number) => Promise<void>
|
|
/** The permission mode the host decided this start runs with; absent asks. */
|
|
permissionMode?: AgentChatPermissionChoice
|
|
}
|
|
|
|
export type StructuredLaunchRequest = {
|
|
identity: AgentSessionJournalIdentity
|
|
/** The reservation's token, which the seal hands the child. */
|
|
spawnToken: string
|
|
permissionMode?: AgentChatPermissionChoice
|
|
}
|
|
|
|
/** What the composition sets on a launch; an agent's part never does. */
|
|
type ComposedLaunchFields = 'cwd' | 'env' | 'envToDelete' | 'commandEnv'
|
|
|
|
export type StructuredAgentLaunchPart<L> = {
|
|
launch: Omit<L, ComposedLaunchFields>
|
|
/** The agent's environment, before the seal names the child. */
|
|
env: Record<string, string>
|
|
/** Inherited keys this agent's child must not keep; required, so an agent states even none. */
|
|
inheritedEnvToDelete: readonly string[]
|
|
/** See `sealStructuredSessionChild`. */
|
|
cliRuntimeCommand?: string
|
|
}
|
|
|
|
/** An agent's own part of a launch; it sees the session, never the spawn token. */
|
|
export type StructuredAgentLaunchPartResolver<L> = (
|
|
basis: StructuredAgentLaunchBasis,
|
|
identity: AgentSessionJournalIdentity
|
|
) => Promise<StructuredAgentLaunchPart<L>>
|
|
|
|
export type ComposedStructuredLaunch<L> = Omit<L, ComposedLaunchFields> & {
|
|
cwd: string
|
|
env: Record<string, string>
|
|
envToDelete: string[]
|
|
/** See `SealedStructuredSessionChild`; absent from a read, which seals no child. */
|
|
commandEnv?: Record<string, string>
|
|
}
|
|
|
|
/** The child to spawn, sealed with the reservation's token. */
|
|
export type StructuredAgentLaunchResolver<L> = (
|
|
request: StructuredLaunchRequest
|
|
) => Promise<ComposedStructuredLaunch<L>>
|
|
|
|
/** The same launch for a read that starts no child: the agent's own env, unsealed. */
|
|
export type StructuredAgentLaunchReader<L> = (
|
|
identity: AgentSessionJournalIdentity
|
|
) => Promise<ComposedStructuredLaunch<L>>
|
|
|
|
function once<T>(read: () => T): () => T {
|
|
let result: { value: T } | null = null
|
|
return () => (result ??= { value: read() }).value
|
|
}
|
|
|
|
function admittedRecord(
|
|
definition: StructuredAgentDefinition,
|
|
store: StructuredAgentLaunchSources['store'],
|
|
sessionId: string
|
|
): AgentSessionRecord {
|
|
const record = store.getRecord(sessionId)
|
|
if (!record) {
|
|
throw new Error(`no durable agent-session record for ${sessionId}`)
|
|
}
|
|
if (record.provider !== definition.agent) {
|
|
throw new Error(`session ${sessionId} is a ${record.provider} session`)
|
|
}
|
|
const { location } = record
|
|
// A session pinned elsewhere belongs to that host; local launch would create a second provider-session writer.
|
|
if (!isNativeLocalLocation(location)) {
|
|
throw new Error(
|
|
`${definition.agent} structured sessions run on the local host, not ${location.executionHostId}`
|
|
)
|
|
}
|
|
const agents = { definition: (agent: string) => (agent === definition.agent ? definition : null) }
|
|
if (!agentDrivesSession(agents, record)) {
|
|
const pin = definition.accountHomeVariable ?? definition.accountLocatorKind
|
|
throw new Error(
|
|
`session ${sessionId}'s account home or handles are not ${definition.agent}'s: its sessions pin ${pin} and hold ${definition.handleTransport} handles`
|
|
)
|
|
}
|
|
return record
|
|
}
|
|
|
|
async function composedPart<L>(
|
|
definition: StructuredAgentDefinition,
|
|
sources: StructuredAgentLaunchSources,
|
|
part: StructuredAgentLaunchPartResolver<L>,
|
|
identity: AgentSessionJournalIdentity,
|
|
permissionMode?: AgentChatPermissionChoice
|
|
): Promise<StructuredAgentLaunchPart<L> & { cwd: string }> {
|
|
const record = admittedRecord(definition, sources.store, identity.sessionId)
|
|
const launchEnv = once(() => sources.resolveAgentLaunchEnv?.(definition.agent) ?? {})
|
|
const readEnvironments = () => ({
|
|
base: once(sources.resolveBaseEnvironment),
|
|
agent: once(() => sources.resolveAgentEnvironment(definition.agent))
|
|
})
|
|
let environments = readEnvironments()
|
|
const refresh = sources.refreshAfterMissingProgram
|
|
const basis: StructuredAgentLaunchBasis = {
|
|
record,
|
|
launchDirectory: once(() => resolveAgentSessionLaunchDirectory(sources, record)),
|
|
baseEnvironment: () => environments.base(),
|
|
launchEnv,
|
|
environment: () => environments.agent(),
|
|
visuals: once(async () => (await sources.prepareVisuals?.(record.sessionId)) ?? null),
|
|
commandSettings: once(() => sources.resolveCommandSettings?.() ?? {}),
|
|
...(refresh
|
|
? {
|
|
refreshAfterMissingProgram: async (askedAt) => {
|
|
await refresh(askedAt)
|
|
environments = readEnvironments()
|
|
}
|
|
}
|
|
: {}),
|
|
...(permissionMode ? { permissionMode } : {})
|
|
}
|
|
const composed = await part(basis, identity)
|
|
return { ...composed, cwd: await basis.launchDirectory() }
|
|
}
|
|
|
|
export function composeStructuredLaunch<L>(
|
|
definition: StructuredAgentDefinition,
|
|
sources: StructuredAgentLaunchSources,
|
|
part: StructuredAgentLaunchPartResolver<L>
|
|
): StructuredAgentLaunchResolver<L> {
|
|
return async ({ identity, spawnToken, permissionMode }) => {
|
|
const { launch, cwd, env, inheritedEnvToDelete, cliRuntimeCommand } = await composedPart(
|
|
definition,
|
|
sources,
|
|
part,
|
|
identity,
|
|
permissionMode
|
|
)
|
|
const child = sealStructuredSessionChild({
|
|
sessionId: identity.sessionId,
|
|
spawnToken,
|
|
env,
|
|
inheritedEnvToDelete,
|
|
...(cliRuntimeCommand ? { cliRuntimeCommand } : {})
|
|
})
|
|
return {
|
|
...launch,
|
|
cwd,
|
|
env: child.env,
|
|
envToDelete: child.envToDelete,
|
|
commandEnv: child.commandEnv
|
|
}
|
|
}
|
|
}
|
|
|
|
export function composeStructuredLaunchRead<L>(
|
|
definition: StructuredAgentDefinition,
|
|
sources: StructuredAgentLaunchSources,
|
|
part: StructuredAgentLaunchPartResolver<L>
|
|
): StructuredAgentLaunchReader<L> {
|
|
return async (identity) => {
|
|
const { launch, cwd, env, inheritedEnvToDelete } = await composedPart(
|
|
definition,
|
|
sources,
|
|
part,
|
|
identity
|
|
)
|
|
return { ...launch, cwd, env, envToDelete: [...inheritedEnvToDelete] }
|
|
}
|
|
}
|
|
|
|
/** The shared launch inputs of the runtime an agent's adapter is built in. */
|
|
export function structuredAgentLaunchSources(
|
|
context: Pick<StructuredAgentAdapterContext, 'deps' | 'store' | 'environment'>
|
|
): StructuredAgentLaunchSources {
|
|
const { deps } = context
|
|
return {
|
|
store: context.store,
|
|
resolveWorkspacePath: deps.resolveWorkspacePath,
|
|
resolveBaseEnvironment: context.environment.resolveBaseEnvironment,
|
|
resolveAgentEnvironment: context.environment.resolveAgentEnvironment,
|
|
refreshAfterMissingProgram: context.environment.refreshAfterMissingProgram,
|
|
...(deps.resolveAgentLaunchEnv ? { resolveAgentLaunchEnv: deps.resolveAgentLaunchEnv } : {}),
|
|
...(deps.resolveAgentCommandSettings
|
|
? { resolveCommandSettings: deps.resolveAgentCommandSettings }
|
|
: {}),
|
|
// Wired by the real hosts only, so a test runtime never loads the bundled skill.
|
|
...(deps.nativeChatVisuals
|
|
? {
|
|
prepareVisuals: createNativeChatVisualsDelivery({
|
|
stateDirectory: deps.stateDirectory,
|
|
logger: deps.logger,
|
|
isEnabled: deps.nativeChatVisuals.isEnabled
|
|
})
|
|
}
|
|
: {})
|
|
}
|
|
}
|