mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
Three fixes, all on paths this PR could not exercise locally. The managed hook command was stored as a bare path. jcode tokenizes that string shell-style before exec'ing it directly (parse_hook_command, crates/jcode-terminal-launch/src/lib.rs): unquoted whitespace splits, and every unquoted backslash is consumed as an escape. So on Windows `C:\Users\me\.orca\agent-hooks\jcode-hook.cmd` reached exec as `C:Usersme.orcaagent-hooksjcode-hook.cmd` and no hook fired at all, and a POSIX home with a space split into two arguments. Store the path single-quoted (verbatim, backslashes included), falling back to double quotes for a path containing a single quote. Existing bare entries are already repointed by the stale-key path, and getStatus accepts both forms so the repair is not reported as a user-owned hook. The quoting helper was previously dead code that only tests called; the three production sites now use it. isJcodeManagedCommand also normalizes separators, since a `/`-only needle never matched a Windows entry. Commit-message generation feeds a staged patch to `jcode run` as the prompt — attacker-influenced text — while jcode's default profile exposes shell, read, write, and MCP. Pass `--tool-profile none`, which resolves to an empty allowed-tool set in jcode's config (base_allowed_tools), matching the read-only posture claude (plan) and codex (read-only) already take. docs/reference/jcode-hook-events.md was never actually in this PR: the repo ignores docs/** and tracks reference docs by allow-list only, so the captured-payload evidence four source comments point at was silently dropped. Allow-list it. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>
325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
import type { TuiAgent } from './tui-agent'
|
|
import type {
|
|
CommitMessageAgentSpec,
|
|
CommitMessageModel,
|
|
ThinkingLevel
|
|
} from './commit-message-agent-spec'
|
|
|
|
type SecondaryAgentSpecDeps = {
|
|
BASIC_THINKING_LEVELS: ThinkingLevel[]
|
|
OPENAI_THINKING_LEVELS: ThinkingLevel[]
|
|
parseCursorModels: (stdout: string) => CommitMessageModel[]
|
|
parseAntigravityModels: (stdout: string) => CommitMessageModel[]
|
|
parseLineModels: (stdout: string) => CommitMessageModel[]
|
|
}
|
|
|
|
export function buildSecondaryCommitMessageAgentSpecs({
|
|
BASIC_THINKING_LEVELS,
|
|
OPENAI_THINKING_LEVELS,
|
|
parseCursorModels,
|
|
parseAntigravityModels,
|
|
parseLineModels
|
|
}: SecondaryAgentSpecDeps): Partial<Record<TuiAgent, CommitMessageAgentSpec>> {
|
|
return {
|
|
amp: {
|
|
id: 'amp',
|
|
label: 'Amp',
|
|
binary: 'amp',
|
|
promptDelivery: 'stdin',
|
|
buildArgs: ({ model, thinkingLevel }) => [
|
|
'--execute',
|
|
'--no-notifications',
|
|
'--no-ide',
|
|
'--no-jetbrains',
|
|
'--mode',
|
|
model,
|
|
...(thinkingLevel ? ['--effort', thinkingLevel] : [])
|
|
],
|
|
// Amp selects the model with `--mode`, not `--model`.
|
|
singletonOptions: [['--mode']],
|
|
modelSource: 'static',
|
|
models: [
|
|
{ id: 'smart', label: 'Smart' },
|
|
{ id: 'rush', label: 'Rush' },
|
|
{
|
|
id: 'large',
|
|
label: 'Large',
|
|
thinkingLevels: BASIC_THINKING_LEVELS,
|
|
defaultThinkingLevel: 'low'
|
|
},
|
|
{
|
|
id: 'deep',
|
|
label: 'Deep',
|
|
thinkingLevels: BASIC_THINKING_LEVELS,
|
|
defaultThinkingLevel: 'low'
|
|
}
|
|
],
|
|
defaultModelId: 'smart'
|
|
},
|
|
cursor: {
|
|
id: 'cursor',
|
|
label: 'Cursor',
|
|
binary: 'cursor-agent',
|
|
promptDelivery: 'argv',
|
|
buildArgs: ({ prompt, model }) => [
|
|
'--print',
|
|
'--mode',
|
|
'ask',
|
|
'--trust',
|
|
'--output-format',
|
|
'text',
|
|
'--model',
|
|
model,
|
|
prompt
|
|
],
|
|
modelSource: 'dynamic',
|
|
modelDiscovery: { binary: 'cursor-agent', args: ['--list-models'], parse: parseCursorModels },
|
|
models: [{ id: 'auto', label: 'Auto' }],
|
|
defaultModelId: 'auto'
|
|
},
|
|
kimi: {
|
|
id: 'kimi',
|
|
label: 'Kimi',
|
|
binary: 'kimi',
|
|
// Why: kimi-code accepts the generation prompt only via --prompt/-p (Claude's
|
|
// --print is rejected). Deliver on argv so --prompt receives the text (#11669).
|
|
promptDelivery: 'argv',
|
|
buildArgs: ({ prompt, model, thinkingLevel }) => [
|
|
'--prompt',
|
|
prompt,
|
|
'--quiet',
|
|
...(model && model !== 'default' ? ['--model', model] : []),
|
|
...(thinkingLevel === 'on'
|
|
? ['--thinking']
|
|
: thinkingLevel === 'off'
|
|
? ['--no-thinking']
|
|
: [])
|
|
],
|
|
modelSource: 'static',
|
|
models: [
|
|
{ id: 'default', label: 'Config default' },
|
|
{
|
|
// Why: Kimi resolves its managed model by provider/model; bare model
|
|
// names are rejected by the CLI with "LLM not set".
|
|
id: 'kimi-code/kimi-for-coding',
|
|
label: 'Kimi K2.6',
|
|
thinkingLevels: [
|
|
{ id: 'on', label: 'On' },
|
|
{ id: 'off', label: 'Off' }
|
|
],
|
|
defaultThinkingLevel: 'on'
|
|
}
|
|
],
|
|
defaultModelId: 'default'
|
|
},
|
|
muse: {
|
|
id: 'muse',
|
|
label: 'Muse',
|
|
binary: 'muse',
|
|
// Muse's `exec` subcommand accepts a positional prompt. Keep Source
|
|
// Control AI one-shot and workspace-read-only, matching the other text
|
|
// generators rather than launching the interactive TUI.
|
|
promptDelivery: 'argv',
|
|
buildArgs: ({ prompt, model, thinkingLevel }) => [
|
|
'exec',
|
|
'--no-session-log',
|
|
'--approval-mode',
|
|
'never',
|
|
'--disable-sandbox',
|
|
'--disable-shell',
|
|
'--disable-write',
|
|
'--disable-web-tools',
|
|
...(model && model !== 'default' ? ['--model', model] : []),
|
|
...(thinkingLevel ? ['--reasoning-effort', thinkingLevel] : []),
|
|
'--',
|
|
prompt
|
|
],
|
|
singletonOptions: [['--model'], ['--reasoning-effort']],
|
|
modelSource: 'static',
|
|
models: [{ id: 'default', label: 'Config default' }],
|
|
defaultModelId: 'default'
|
|
},
|
|
dsh: {
|
|
id: 'dsh',
|
|
label: 'DeepSeek Harness',
|
|
binary: 'dsh',
|
|
// Why: `dsh --profile headless` runs one fresh persisted session, prints the final
|
|
// answer and exits — the documented one-shot entry mode. The interactive `dsh-tui`
|
|
// profile is deliberately not used here; Source Control AI stays one-shot.
|
|
// Why stdin and not argv: the prompt carries the whole diff. On argv it would sit in
|
|
// the process table for every user on the box, and it would eventually hit the argv
|
|
// limit. `-` is DSH's explicit stdin marker; measured against 0.1.5-rc.1, omitting the
|
|
// positional entirely is rejected ("a task is required") even when stdin is a pipe.
|
|
promptDelivery: 'stdin',
|
|
buildArgs: () => ['--profile', 'headless', '-'],
|
|
// Why: the launcher owns `--profile`; a second one would boot a different profile.
|
|
singletonOptions: [['--profile']],
|
|
modelSource: 'static',
|
|
// Why: the headless app parses no `--model`. The model comes from the profile's
|
|
// `llm-deepseek` row, so the only honest choice here is the configured default.
|
|
models: [{ id: 'default', label: 'Config default' }],
|
|
defaultModelId: 'default'
|
|
},
|
|
copilot: {
|
|
id: 'copilot',
|
|
label: 'GitHub Copilot',
|
|
binary: 'copilot',
|
|
promptDelivery: 'argv',
|
|
buildArgs: ({ prompt, model, thinkingLevel }) => [
|
|
'--prompt',
|
|
prompt,
|
|
'--silent',
|
|
'--stream',
|
|
'off',
|
|
'--no-custom-instructions',
|
|
'--model',
|
|
model,
|
|
...(thinkingLevel ? ['--effort', thinkingLevel] : [])
|
|
],
|
|
modelSource: 'static',
|
|
// Why: Copilot CLI's picker is policy-filtered per account/org. Keep the
|
|
// full hosted CLI catalog here so users can select models enabled for them.
|
|
models: [
|
|
{ id: 'auto', label: 'Auto' },
|
|
{
|
|
id: 'claude-haiku-4.5',
|
|
label: 'Claude Haiku 4.5'
|
|
},
|
|
{
|
|
id: 'claude-sonnet-4.5',
|
|
label: 'Claude Sonnet 4.5'
|
|
},
|
|
{
|
|
id: 'claude-sonnet-4.6',
|
|
label: 'Claude Sonnet 4.6'
|
|
},
|
|
{
|
|
id: 'claude-opus-4.5',
|
|
label: 'Claude Opus 4.5'
|
|
},
|
|
{
|
|
id: 'claude-opus-4.6',
|
|
label: 'Claude Opus 4.6'
|
|
},
|
|
{
|
|
id: 'claude-opus-4.6-fast',
|
|
label: 'Claude Opus 4.6 Fast'
|
|
},
|
|
{
|
|
id: 'claude-opus-4.7',
|
|
label: 'Claude Opus 4.7'
|
|
},
|
|
{
|
|
id: 'gpt-4.1',
|
|
label: 'GPT-4.1'
|
|
},
|
|
{
|
|
id: 'gpt-5-mini',
|
|
label: 'GPT-5 Mini',
|
|
thinkingLevels: OPENAI_THINKING_LEVELS,
|
|
defaultThinkingLevel: 'low'
|
|
},
|
|
{
|
|
id: 'gpt-5.2',
|
|
label: 'GPT-5.2',
|
|
thinkingLevels: OPENAI_THINKING_LEVELS,
|
|
defaultThinkingLevel: 'low'
|
|
},
|
|
{
|
|
id: 'gpt-5.2-codex',
|
|
label: 'GPT-5.2 Codex',
|
|
thinkingLevels: OPENAI_THINKING_LEVELS,
|
|
defaultThinkingLevel: 'low'
|
|
},
|
|
{
|
|
id: 'gpt-5.3-codex',
|
|
label: 'GPT-5.3 Codex',
|
|
thinkingLevels: OPENAI_THINKING_LEVELS,
|
|
defaultThinkingLevel: 'low'
|
|
},
|
|
{
|
|
id: 'gpt-5.4',
|
|
label: 'GPT-5.4',
|
|
thinkingLevels: OPENAI_THINKING_LEVELS,
|
|
defaultThinkingLevel: 'low'
|
|
},
|
|
{
|
|
id: 'gpt-5.4-mini',
|
|
label: 'GPT-5.4 Mini',
|
|
thinkingLevels: OPENAI_THINKING_LEVELS,
|
|
defaultThinkingLevel: 'low'
|
|
},
|
|
{
|
|
id: 'gpt-5.5',
|
|
label: 'GPT-5.5',
|
|
thinkingLevels: OPENAI_THINKING_LEVELS,
|
|
defaultThinkingLevel: 'low'
|
|
}
|
|
],
|
|
defaultModelId: 'gpt-5.4'
|
|
},
|
|
antigravity: {
|
|
id: 'antigravity',
|
|
label: 'Antigravity',
|
|
binary: 'agy',
|
|
// agy's --print takes the prompt as its value (#19539, #14059). Deliver on argv
|
|
// using `--print=<value>` so a leading-dash prompt binds to the flag instead of
|
|
// being parsed as its own option, and --sandbox/--model stay separate options.
|
|
promptDelivery: 'argv',
|
|
buildArgs: ({ prompt, model, thinkingLevel }) => [
|
|
`--print=${prompt}`,
|
|
'--sandbox',
|
|
...(model && model !== 'default' ? ['--model', model] : []),
|
|
...(thinkingLevel ? ['--effort', thinkingLevel] : [])
|
|
],
|
|
singletonOptions: [['--model'], ['--effort']],
|
|
modelSource: 'dynamic',
|
|
modelDiscovery: { binary: 'agy', args: ['models'], parse: parseAntigravityModels },
|
|
models: [{ id: 'default', label: 'Config default' }],
|
|
defaultModelId: 'default'
|
|
},
|
|
jcode: {
|
|
id: 'jcode',
|
|
label: 'Jcode',
|
|
binary: 'jcode',
|
|
// Why: `jcode run` takes the message as a positional argv argument and has no
|
|
// stdin prompt mode, so Source Control AI prompts ride argv (fine for branch
|
|
// naming and small diffs, argv-capped on Windows).
|
|
promptDelivery: 'argv',
|
|
buildArgs: ({ prompt, model }) => [
|
|
// Why: these are jcode global options, so they must precede the subcommand;
|
|
// clap rejects them after `run`.
|
|
'--no-update',
|
|
'--quiet',
|
|
'--no-selfdev',
|
|
// Why: the prompt here IS a staged patch, i.e. attacker-influenced text, and
|
|
// jcode would otherwise expose shell/read/write/MCP to it. `none` resolves to
|
|
// an empty allowed-tool set in jcode's config (tools.rs `base_allowed_tools`),
|
|
// which drops `mcp` too since MCP is exposed as a tool. Matches the read-only
|
|
// posture the other generators already take (claude plan, codex read-only).
|
|
'--tool-profile',
|
|
'none',
|
|
...(model && model !== 'default' ? ['--model', model] : []),
|
|
'run',
|
|
'--json',
|
|
prompt
|
|
],
|
|
singletonOptions: [['--model']],
|
|
modelSource: 'dynamic',
|
|
// Why: `jcode model list` prints one bare model id per line, which is exactly
|
|
// what parseLineModels reads. Discovering beats a hardcoded list because
|
|
// jcode's catalog spans every provider the user has authenticated.
|
|
modelDiscovery: {
|
|
binary: 'jcode',
|
|
args: ['--no-update', '--quiet', 'model', 'list'],
|
|
parse: parseLineModels
|
|
},
|
|
// Why: `default` is not a jcode model id — it is the sentinel that omits
|
|
// --model so jcode uses the model from its own config.toml, rather than Orca
|
|
// pinning a provider the user may not be logged in to.
|
|
models: [{ id: 'default', label: 'Config default' }],
|
|
defaultModelId: 'default'
|
|
}
|
|
}
|
|
}
|