mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
* Source control actions start chats and terminals on their saved model A "start an agent" action's model lived only in free-text CLI arguments, which a native chat never reads, so AI Fix opened a chat on the default model. Each action now saves a structured model and effort that both routes apply: chat create options, or the agent's own flags in a terminal (winning over the same flag in CLI arguments). Desktop, dialog, fix-checks and mobile launches carry it; Settings rows get Model/Effort selects and the CLI arguments field says it is terminal-only. Tracked in STA-11102. * Name the model in the repository override note * Type the dialog's model hand-off; keep Agent in English in Japanese copy * fix: preserve action chat model choices through launch * Preserve action selection ownership across chat launches * Keep terminal host contract fixtures aligned with preflight * fix: wait for action Settings agent inventory * fix: consolidate direct agent routing imports * fix: use standard action option labels * fix: align action launch contracts with shared types * fix: sync action option localization metadata * test: provide action Settings inventory in mounted fixtures * Keep action options row height stable while loading * Let each Source Control action choose its chat permission mode An action's saved chat choice may now carry a permission mode, with or without a model; "Same as new chats" saves nothing. A model is replaced only when the action names one, so a permission-only choice keeps the chat's remembered model and options. * fix(native-chat): leave older hosts their own permission mode and keep action options readable - An action's saved permission mode is sent only to hosts that advertise agentSession.create.options-mode.v1. Older hosts get no mode, in create options or a later setOption, so the chat starts on that host's new-chat default, as a phone launch to such a host already does. Hosts from 1.4.225 to 1.4.227 refused a mode their tables lacked (OpenCode, Grok, OMP), leaving the action unable to start. - Settings action rows give Model and Permissions full rows in the options column so saved values are readable; the loading placeholder reserves the same rows. - Remove two unused locale keys; fix two stale comments. - Tests: the older-host create carries no permission mode; a permission mode picked alone keeps the remembered model on an acquired create. * fix(settings): reserve one options row while loading an action with nothing saved Terminal and permissions-only rows no longer shrink when loading ends; a chat agent with a model list may grow by its Model row once known. * fix(native-chat): keep the create options mode a checked literal type * fix(settings): share verified action option fields * fix(settings): keep action selectors on standard typography * Keep the launch files under their length limits after the main merge The structured route imports its result types where they are defined, and the two older-host action-launch tests share one setup helper. * One action settings section for chat and terminal launches A Source Control action's Model, model options and Permissions now apply whether it opens a chat or a terminal; terminals receive them as the agent's own flags, and a repository's own CLI arguments keep the flags it sets. Extra CLI arguments stay terminal-only. * fix(source-control-ai): one terminal projection for saved action choices Ask in a terminal now states its permission instead of leaving it to the user's own config: Claude and Grok pass --permission-mode default, Codex passes -a on-request -s read-only -c approvals_reviewer="user" (the interactive CLI has no untrusted policy, so a read-only sandbox is what asks before edits). Agents whose CLI has no flag for a mode show the existing "Applies to chats only" hint. The strict launch-preferences projection now requires the launch agent at every caller, composes model ids through the resolver's own step, carries a mode only where the agent's terminal has a flag for it, and the local string wire keeps booleans as 'true'/'false', so Cursor Thinking/Fast reach the terminal and an OpenCode permission-only choice is not refused. The argument parser knows flag arity, so removing a boolean permission flag keeps other options' values, following positionals and '--'. Pre-admission and executor share one terminal admission check on the options the terminal will receive. Customize with Save launches the recipe as it will resolve once saved; legacy partial repository overrides are edited as the complete recipe they resolve to. Empty Extra CLI arguments start collapsed. * fix(source-control-ai): an edited repository action saves complete Every Settings edit of a repository action (agent, model, permission, prompt or arguments) now starts from the complete recipe the action resolves to, whether its stored override is owned or a legacy partial. The resolved agent selection is kept (never an agent only detected to run a default), an explicit null args clear stays null, and empty args stay empty instead of being dropped. Clearing the last preference therefore means the action agent's own defaults rather than re-inheriting Global. The action text draft is lossless: unset or cleared prompt and args stay so until the user types, and the inputs show them as empty, so saving a prompt no longer writes empty args that suppress the agent's default flags. Untouched sibling actions are not rewritten by the edit. * fix(settings): style the Extra CLI arguments toggle through Button's own variant and size CI's changed-code quality check refuses restyle classes on Button and CollapsibleContent. Use the ghost variant at size xs with a margin, and put the content spacing on a plain inner wrapper. * fix: keep saved action and restart launch recipes consistent * fix: describe inherited arguments and chat-only permissions accurately * test: check repository action settings fixture type * test: map saved action options into terminal compiler inputs * fix: retain saved action controls during agent detection * fix(i18n): retire obsolete Portuguese action labels * test: align folder launch fallback mock with outcome export * fix: carry resolved connector launch intent and mode together * test: pin shared action launch branches to recorded agent output * test: preserve saved source control choices through detection and migration * test: align launch fixtures with integrated result contract * test: retain recipe and restart contracts after main integration * Unify Source Control action permission settings * Keep native Ask copy with its owning change * Preserve action drafts and old-host permission intent * Consume rejected Source Control action saves * Follow the moved Settings persistence fixture * Keep permission integration contracts in the cross-version test project * Include the action permission inheritance label in English catalogs * Align launch contracts with shared permission recipes * Keep Source Control action controls within their Settings columns * Use primitive variants for compact action settings controls * Show action arguments only for terminal defaults and widen recipe rows * Apply terminal-only action arguments visibility to repository overrides * Align action permission control with model controls * Use shared settings label for action permissions * Hide action model controls until argument migration ships
304 lines
11 KiB
TypeScript
304 lines
11 KiB
TypeScript
import { agentPermissionHostPlatform, permissionEnvNameMatches } from './agent-permission-env'
|
||
import type { SleepingAgentLaunchConfig } from './agent-session-resume'
|
||
import type { GlobalSettings } from './global-settings-types'
|
||
import { isTuiAgent } from './tui-agent-config'
|
||
import { YOLO_TUI_AGENT_ARGS, YOLO_TUI_AGENT_ENV } from './tui-agent-permissions'
|
||
import {
|
||
agentPermissionArgs,
|
||
readLaunchPermissionChoice,
|
||
type AgentPermissionChoice,
|
||
agentPermissionEnv,
|
||
hasTypedAgentPermission,
|
||
PERMISSION_CHOICE_AGENTS,
|
||
resolveAgentPermission,
|
||
sanitizeAgentPermissionUpdates
|
||
} from './tui-agent-permission-choice'
|
||
import { agentArgsSetPermission } from './tui-agent-permission-lift'
|
||
import {
|
||
resolveStartupShell,
|
||
tokenizeStartupCommand,
|
||
type AgentStartupShell
|
||
} from './tui-agent-startup-shell'
|
||
import type { TuiAgent } from './tui-agent'
|
||
import { resolveLocalWindowsAgentStartupShell } from './windows-terminal-shell'
|
||
|
||
const UNSUPPORTED_TUI_AGENT_ARGS: Partial<Record<TuiAgent, readonly string[]>> = {
|
||
opencode: ['--dangerously-skip-permissions'],
|
||
kilo: ['--dangerously-skip-permissions']
|
||
}
|
||
|
||
/** What a launch reads: the user's Arguments and env plus the stored permission choice. */
|
||
export type AgentLaunchProfileSettings = Partial<
|
||
Pick<
|
||
GlobalSettings,
|
||
'agentDefaultArgs' | 'agentDefaultEnv' | 'agentPermissionDefault' | 'agentPermissionOverrides'
|
||
>
|
||
>
|
||
|
||
/** Launch intent overrides only Orca's injected permission, preserving the user's args and env. */
|
||
export function agentLaunchPermissionSettings<T extends AgentLaunchProfileSettings>(
|
||
agent: TuiAgent,
|
||
settings: T,
|
||
sessionOptions: Readonly<Record<string, unknown>> | undefined
|
||
) {
|
||
const choice = readLaunchPermissionChoice(sessionOptions)
|
||
return choice
|
||
? {
|
||
...settings,
|
||
agentPermissionDefault: settings?.agentPermissionDefault ?? 'own',
|
||
agentPermissionOverrides: { ...settings?.agentPermissionOverrides, [agent]: choice }
|
||
}
|
||
: settings
|
||
}
|
||
|
||
// What an untouched agent ran with before permissions were stored as a choice; legacy text only.
|
||
export const DEFAULT_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> &
|
||
Readonly<Record<string, string | undefined>> = YOLO_TUI_AGENT_ARGS
|
||
|
||
export const DEFAULT_TUI_AGENT_ENV: Partial<Record<string, Record<string, string>>> =
|
||
YOLO_TUI_AGENT_ENV
|
||
|
||
function argPattern(arg: string): RegExp {
|
||
return new RegExp(`(^|\\s)${arg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=\\s|$)`, 'g')
|
||
}
|
||
|
||
export function hasUnsupportedTuiAgentArgs(agent: TuiAgent, value: unknown): boolean {
|
||
if (typeof value !== 'string') {
|
||
return false
|
||
}
|
||
return (UNSUPPORTED_TUI_AGENT_ARGS[agent] ?? []).some((arg) => argPattern(arg).test(value))
|
||
}
|
||
|
||
/** Read legacy startup argv so quoted prompt text and operands after -- cannot masquerade as permission flags. */
|
||
export function tuiAgentArgsBypassPermissions(
|
||
agent: TuiAgent,
|
||
value: string | null | undefined,
|
||
shell: AgentStartupShell
|
||
): boolean {
|
||
const bypassArg = YOLO_TUI_AGENT_ARGS[agent]
|
||
if (typeof value !== 'string' || bypassArg === undefined) {
|
||
return false
|
||
}
|
||
const tokenized = tokenizeStartupCommand(value, shell)
|
||
const bypass = tokenizeStartupCommand(bypassArg, shell)
|
||
if (!tokenized.ok || !bypass.ok || bypass.tokens.length === 0) {
|
||
return false
|
||
}
|
||
const terminator = tokenized.tokens.indexOf('--')
|
||
const options = terminator === -1 ? tokenized.tokens : tokenized.tokens.slice(0, terminator)
|
||
// A token sequence: several agents' bypass is a flag and its value (`--permission-mode X`).
|
||
return options.some((_, start) =>
|
||
bypass.tokens.every((token, offset) => options[start + offset] === token)
|
||
)
|
||
}
|
||
|
||
function sanitizeTuiAgentLaunchArgs(agent: TuiAgent, args: string): string {
|
||
const unsupportedArgs = UNSUPPORTED_TUI_AGENT_ARGS[agent]
|
||
if (!unsupportedArgs) {
|
||
return args.trim()
|
||
}
|
||
// Some interactive agent commands removed, relocated or never supported these skip-permission flags.
|
||
return unsupportedArgs.reduce((next, arg) => next.replace(argPattern(arg), ' '), args).trim()
|
||
}
|
||
|
||
export function normalizeTuiAgentArgsRecord(value: unknown): Partial<Record<TuiAgent, string>> {
|
||
const normalized: Partial<Record<TuiAgent, string>> = {}
|
||
if (!value || typeof value !== 'object') {
|
||
return normalized
|
||
}
|
||
for (const [agent, args] of Object.entries(value)) {
|
||
if (!isTuiAgent(agent) || typeof args !== 'string') {
|
||
continue
|
||
}
|
||
normalized[agent] = sanitizeTuiAgentLaunchArgs(agent, args)
|
||
}
|
||
return normalized
|
||
}
|
||
|
||
export function normalizeTuiAgentEnvRecord(
|
||
value: unknown
|
||
): Partial<Record<TuiAgent, Record<string, string>>> {
|
||
const normalized: Partial<Record<TuiAgent, Record<string, string>>> = {}
|
||
if (!value || typeof value !== 'object') {
|
||
return normalized
|
||
}
|
||
for (const [agent, env] of Object.entries(value)) {
|
||
if (!isTuiAgent(agent) || !env || typeof env !== 'object') {
|
||
continue
|
||
}
|
||
const entries: [string, string][] = []
|
||
for (const [name, raw] of Object.entries(env)) {
|
||
const key = name.trim()
|
||
if (!key || typeof raw !== 'string') {
|
||
continue
|
||
}
|
||
entries.push([key, raw])
|
||
}
|
||
normalized[agent] = Object.fromEntries(entries)
|
||
}
|
||
return normalized
|
||
}
|
||
|
||
/** Normalizes a settings write's launch profile in place, wherever the write lands. */
|
||
export function sanitizeAgentLaunchProfileUpdates(
|
||
target: Partial<GlobalSettings>,
|
||
updates: Partial<GlobalSettings>
|
||
): void {
|
||
if ('agentDefaultArgs' in updates) {
|
||
target.agentDefaultArgs = normalizeTuiAgentArgsRecord(updates.agentDefaultArgs)
|
||
target.agentYoloDefaultsMigrated = true
|
||
}
|
||
if ('agentDefaultEnv' in updates) {
|
||
target.agentDefaultEnv = normalizeTuiAgentEnvRecord(updates.agentDefaultEnv)
|
||
target.agentYoloDefaultsMigrated = true
|
||
}
|
||
sanitizeAgentPermissionUpdates(target, updates)
|
||
}
|
||
|
||
export function getTuiAgentDefaultArgs(agent: string): string {
|
||
return DEFAULT_TUI_AGENT_ARGS[agent] ?? ''
|
||
}
|
||
|
||
export function getTuiAgentDefaultEnv(agent: string): Record<string, string> {
|
||
return { ...DEFAULT_TUI_AGENT_ENV[agent] }
|
||
}
|
||
|
||
function ownArgs(agent: string, settings: AgentLaunchProfileSettings | null | undefined) {
|
||
const configured = settings?.agentDefaultArgs
|
||
const value: unknown = configured && Object.getOwnPropertyDescriptor(configured, agent)?.value
|
||
return typeof value === 'string' ? value : undefined
|
||
}
|
||
|
||
/** Shared injection rule: CLI-owned permission text wins over an Orca choice. */
|
||
export function agentPermissionArgsForLaunch(
|
||
agent: TuiAgent,
|
||
choice: AgentPermissionChoice,
|
||
text: string
|
||
): string | null {
|
||
return choice === 'own'
|
||
? ''
|
||
: agentArgsSetPermission(agent, text)
|
||
? null
|
||
: agentPermissionArgs(agent, choice)
|
||
}
|
||
|
||
/** Compose stored permission flags with user arguments, letting explicit user permission text win. Older host text already includes its flags. */
|
||
export function resolveTuiAgentLaunchArgs(
|
||
agent: string,
|
||
settings: AgentLaunchProfileSettings | null | undefined,
|
||
permissionChoice?: AgentPermissionChoice
|
||
): string {
|
||
const own = ownArgs(agent, settings)
|
||
if (
|
||
(!hasTypedAgentPermission(settings) && permissionChoice === undefined) ||
|
||
!isTuiAgent(agent)
|
||
) {
|
||
return own ?? getTuiAgentDefaultArgs(agent)
|
||
}
|
||
const user = own?.trim() ?? ''
|
||
const flag = agentPermissionArgsForLaunch(
|
||
agent,
|
||
permissionChoice ?? resolveAgentPermission(agent, settings).choice,
|
||
user
|
||
)
|
||
if (!flag) {
|
||
return user
|
||
}
|
||
return user ? `${flag} ${user}` : flag
|
||
}
|
||
|
||
/** Legacy missing Arguments use Orca’s bypass default; Manual stores an explicit empty value that overrides it. */
|
||
export function resolvedTuiAgentArgsBypassPermissions(
|
||
agent: TuiAgent,
|
||
settings:
|
||
| (AgentLaunchProfileSettings & Partial<Pick<GlobalSettings, 'terminalWindowsShell'>>)
|
||
| null
|
||
| undefined,
|
||
platform: NodeJS.Platform
|
||
): boolean {
|
||
const shell = resolveStartupShell(
|
||
platform,
|
||
resolveLocalWindowsAgentStartupShell({
|
||
platform,
|
||
isRemote: false,
|
||
terminalWindowsShell: settings?.terminalWindowsShell
|
||
})
|
||
)
|
||
return tuiAgentArgsBypassPermissions(agent, resolveTuiAgentLaunchArgs(agent, settings), shell)
|
||
}
|
||
|
||
/** The env a terminal launch of this agent gets; the user's own entries win over Orca's. */
|
||
export function resolveTuiAgentLaunchEnv(
|
||
agent: string,
|
||
settings: AgentLaunchProfileSettings | null | undefined,
|
||
platform: NodeJS.Platform | null | undefined = agentPermissionHostPlatform(),
|
||
visitBaseEntry?: (name: string, value: string) => true
|
||
): Record<string, string> {
|
||
const configured = settings?.agentDefaultEnv
|
||
const value: unknown = configured && Object.getOwnPropertyDescriptor(configured, agent)?.value
|
||
const own =
|
||
value && typeof value === 'object'
|
||
? Object.entries(value).filter(
|
||
(entry): entry is [string, string] =>
|
||
typeof entry[1] === 'string' && (visitBaseEntry?.(entry[0], entry[1]) ?? true)
|
||
)
|
||
: undefined
|
||
if (!hasTypedAgentPermission(settings) || !isTuiAgent(agent)) {
|
||
const entries = own ?? Object.entries(getTuiAgentDefaultEnv(agent))
|
||
if (!own) {
|
||
entries.forEach(([name, value]) => visitBaseEntry?.(name, value))
|
||
}
|
||
return Object.fromEntries(entries)
|
||
}
|
||
const added = Object.entries(
|
||
agentPermissionEnv(agent, resolveAgentPermission(agent, settings).choice)
|
||
).filter(([name]) => !own?.some(([key]) => permissionEnvNameMatches(key, name, platform)))
|
||
return Object.fromEntries(added.length ? [...added, ...(own ?? [])] : (own ?? []))
|
||
}
|
||
|
||
/** Publish composed args/env for older clients that infer permission from text; explicit keys prevent fallback to their built-in defaults. */
|
||
export function composeAgentLaunchProfile(
|
||
settings: AgentLaunchProfileSettings,
|
||
platform: NodeJS.Platform | null | undefined = agentPermissionHostPlatform()
|
||
): Required<Pick<AgentLaunchProfileSettings, 'agentDefaultArgs' | 'agentDefaultEnv'>> {
|
||
if (!hasTypedAgentPermission(settings)) {
|
||
return {
|
||
agentDefaultArgs: { ...settings.agentDefaultArgs },
|
||
agentDefaultEnv: { ...settings.agentDefaultEnv }
|
||
}
|
||
}
|
||
const argAgents = new Set([
|
||
...Object.keys(settings.agentDefaultArgs ?? {}),
|
||
...PERMISSION_CHOICE_AGENTS
|
||
])
|
||
const envAgents = new Set([
|
||
...Object.keys(settings.agentDefaultEnv ?? {}),
|
||
...PERMISSION_CHOICE_AGENTS.filter(
|
||
(agent) => Object.keys(agentPermissionEnv(agent, 'full-access')).length > 0
|
||
)
|
||
])
|
||
return {
|
||
agentDefaultArgs: Object.fromEntries(
|
||
[...argAgents].map((agent) => [agent, resolveTuiAgentLaunchArgs(agent, settings)])
|
||
),
|
||
agentDefaultEnv: Object.fromEntries(
|
||
[...envAgents].map((agent) => [agent, resolveTuiAgentLaunchEnv(agent, settings, platform)])
|
||
)
|
||
}
|
||
}
|
||
|
||
/** Resume only the recorded recipe; defaults would grant bypass to unrecorded manual launches that never used it. */
|
||
export function resolveResumeLaunchRecipe(
|
||
launchConfig: SleepingAgentLaunchConfig | undefined
|
||
): SleepingAgentLaunchConfig {
|
||
if (!launchConfig) {
|
||
return { agentArgs: '', agentEnv: {} }
|
||
}
|
||
return {
|
||
agentArgs: launchConfig.agentArgs,
|
||
agentEnv: launchConfig.agentEnv,
|
||
...(launchConfig.agentCommand ? { agentCommand: launchConfig.agentCommand } : {}),
|
||
...(launchConfig.ompResumeFilePath ? { ompResumeFilePath: launchConfig.ompResumeFilePath } : {})
|
||
}
|
||
}
|