@aragon-agent/cli 0.6.38 → 0.6.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2523 -2326
- package/README.md +3614 -3173
- package/dist/agent/controller.d.ts +142 -1
- package/dist/agent/controller.js +171 -3
- package/dist/agent/reducer.d.ts +41 -1
- package/dist/agent/reducer.js +25 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +5 -0
- package/dist/cli.js +85 -27
- package/dist/commands/builtins.js +13 -1
- package/dist/commands/keys-command.d.ts +23 -0
- package/dist/commands/keys-command.js +38 -0
- package/dist/commands/registry.d.ts +39 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -0
- package/dist/config/cli-commands.d.ts +23 -0
- package/dist/config/cli-commands.js +36 -0
- package/dist/config/env.js +17 -4
- package/dist/config/load.js +6 -1
- package/dist/config/schema.d.ts +43 -4
- package/dist/config/schema.js +31 -5
- package/dist/config/store.js +17 -2
- package/dist/diagnostics/doctor.d.ts +9 -0
- package/dist/diagnostics/doctor.js +34 -0
- package/dist/diagnostics/info.d.ts +71 -0
- package/dist/diagnostics/info.js +91 -5
- package/dist/exec/bridge/caller-tools.d.ts +96 -0
- package/dist/exec/bridge/caller-tools.js +213 -0
- package/dist/exec/bridge/decisions.d.ts +78 -0
- package/dist/exec/bridge/decisions.js +116 -0
- package/dist/exec/bridge/human-answers.d.ts +38 -0
- package/dist/exec/bridge/human-answers.js +164 -0
- package/dist/exec/bridge/limits.d.ts +68 -0
- package/dist/exec/bridge/limits.js +68 -0
- package/dist/exec/bridge/ndjson.d.ts +17 -0
- package/dist/exec/bridge/ndjson.js +83 -0
- package/dist/exec/bridge/pending.d.ts +70 -0
- package/dist/exec/bridge/pending.js +132 -0
- package/dist/exec/bridge/protocol.d.ts +346 -0
- package/dist/exec/bridge/protocol.js +256 -0
- package/dist/exec/bridge/server.d.ts +142 -0
- package/dist/exec/bridge/server.js +502 -0
- package/dist/exec/bridge/source.d.ts +58 -0
- package/dist/exec/bridge/source.js +145 -0
- package/dist/exec/bridge/writer.d.ts +26 -0
- package/dist/exec/bridge/writer.js +156 -0
- package/dist/exec/cli-commands.js +30 -2
- package/dist/exec/emitter.d.ts +16 -2
- package/dist/exec/emitter.js +26 -6
- package/dist/exec/events.d.ts +465 -10
- package/dist/exec/events.js +47 -1
- package/dist/exec/hooks/dispatch.d.ts +76 -0
- package/dist/exec/hooks/dispatch.js +169 -0
- package/dist/exec/hooks/index.d.ts +83 -0
- package/dist/exec/hooks/index.js +205 -0
- package/dist/exec/hooks/limits.d.ts +56 -0
- package/dist/exec/hooks/limits.js +49 -0
- package/dist/exec/hooks/registry.d.ts +49 -0
- package/dist/exec/hooks/registry.js +91 -0
- package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
- package/dist/exec/hooks/tool-wrapper.js +154 -0
- package/dist/exec/hooks/turn.d.ts +61 -0
- package/dist/exec/hooks/turn.js +59 -0
- package/dist/exec/hooks/validate.d.ts +85 -0
- package/dist/exec/hooks/validate.js +310 -0
- package/dist/exec/index.d.ts +130 -4
- package/dist/exec/index.js +501 -10
- package/dist/exec/options.d.ts +124 -2
- package/dist/exec/options.js +413 -6
- package/dist/exec/permission.d.ts +9 -1
- package/dist/exec/permission.js +7 -1
- package/dist/exec/request/limits.d.ts +51 -0
- package/dist/exec/request/limits.js +49 -0
- package/dist/exec/request/scope.d.ts +106 -0
- package/dist/exec/request/scope.js +149 -0
- package/dist/exec/result/limits.d.ts +74 -0
- package/dist/exec/result/limits.js +72 -0
- package/dist/exec/result/prompt.d.ts +28 -0
- package/dist/exec/result/prompt.js +74 -0
- package/dist/exec/result/schema.d.ts +99 -0
- package/dist/exec/result/schema.js +464 -0
- package/dist/exec/result/sink.d.ts +56 -0
- package/dist/exec/result/sink.js +50 -0
- package/dist/exec/result/tool.d.ts +37 -0
- package/dist/exec/result/tool.js +118 -0
- package/dist/exec/runner.d.ts +395 -1
- package/dist/exec/runner.js +860 -8
- package/dist/exec/threads/aggregate.d.ts +34 -0
- package/dist/exec/threads/aggregate.js +30 -0
- package/dist/exec/threads/capacity.d.ts +23 -0
- package/dist/exec/threads/capacity.js +38 -0
- package/dist/exec/threads/dispatch-source.d.ts +60 -0
- package/dist/exec/threads/dispatch-source.js +106 -0
- package/dist/exec/threads/execute-threads.d.ts +53 -0
- package/dist/exec/threads/execute-threads.js +234 -0
- package/dist/exec/threads/gate.d.ts +42 -0
- package/dist/exec/threads/gate.js +81 -0
- package/dist/exec/threads/identity.d.ts +36 -0
- package/dist/exec/threads/identity.js +67 -0
- package/dist/exec/threads/interrupt.d.ts +29 -0
- package/dist/exec/threads/interrupt.js +79 -0
- package/dist/exec/threads/limits.d.ts +58 -0
- package/dist/exec/threads/limits.js +35 -0
- package/dist/exec/threads/outcome.d.ts +47 -0
- package/dist/exec/threads/outcome.js +26 -0
- package/dist/exec/threads/registry.d.ts +81 -0
- package/dist/exec/threads/registry.js +427 -0
- package/dist/exec/threads/runtime.d.ts +107 -0
- package/dist/exec/threads/runtime.js +523 -0
- package/dist/exec/threads/thread-source.d.ts +20 -0
- package/dist/exec/threads/thread-source.js +134 -0
- package/dist/fast/limits.d.ts +54 -0
- package/dist/fast/limits.js +54 -0
- package/dist/fast/summary-call.d.ts +72 -0
- package/dist/fast/summary-call.js +133 -0
- package/dist/fast/summary-prompt.d.ts +106 -0
- package/dist/fast/summary-prompt.js +320 -0
- package/dist/fast/task-summary-types.d.ts +66 -0
- package/dist/fast/task-summary-types.js +17 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/limits.d.ts +7 -0
- package/dist/proc/limits.js +7 -0
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- package/dist/team/runtime.d.ts +9 -0
- package/dist/team/runtime.js +8 -0
- package/dist/team/subagent.d.ts +15 -0
- package/dist/team/subagent.js +45 -1
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +224 -11
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/interaction-hints.js +6 -0
- package/dist/ui/layout/OverlayFrame.d.ts +18 -0
- package/dist/ui/layout/OverlayFrame.js +19 -4
- package/dist/ui/layout/header-hints.d.ts +55 -62
- package/dist/ui/layout/header-hints.js +59 -166
- package/dist/ui/layout/header-left.d.ts +115 -0
- package/dist/ui/layout/header-left.js +206 -0
- package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
- package/dist/ui/layout/shortcut-panel-layout.js +321 -0
- package/dist/ui/layout/status-detail-layout.js +5 -1
- package/dist/ui/layout/task-summary-cell.d.ts +61 -0
- package/dist/ui/layout/task-summary-cell.js +65 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
- package/dist/ui/overlays/HelpOverlay.js +29 -2
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
package/dist/exec/options.d.ts
CHANGED
|
@@ -14,7 +14,20 @@
|
|
|
14
14
|
* half-written event would corrupt every consumer. The caller writes the message
|
|
15
15
|
* to stderr and exits.
|
|
16
16
|
*/
|
|
17
|
+
import { type HookTimeoutPolicy } from './hooks/limits.js';
|
|
17
18
|
import type { ExecInputFormat, ExecOutputFormat, ExecPermissionMode } from './events.js';
|
|
19
|
+
/**
|
|
20
|
+
* What `resolveExecOptions` needs from the resolved config.
|
|
21
|
+
*
|
|
22
|
+
* THE ONLY FACT IT NEEDS IS `confirmTools`, and the reason it is a parameter
|
|
23
|
+
* rather than a second `loadConfig()` call is that this module is PURE by
|
|
24
|
+
* contract (see the file header): it decides about flags, never about the world.
|
|
25
|
+
* The default is `false` so every existing caller - and every test that passes one
|
|
26
|
+
* argument - keeps the exact behaviour it had.
|
|
27
|
+
*/
|
|
28
|
+
export interface ExecOptionsContext {
|
|
29
|
+
confirmTools?: boolean;
|
|
30
|
+
}
|
|
18
31
|
/** Raw commander output. Every field is whatever the user typed, or absent. */
|
|
19
32
|
export interface RawExecOptions {
|
|
20
33
|
outputFormat?: string;
|
|
@@ -32,6 +45,36 @@ export interface RawExecOptions {
|
|
|
32
45
|
/** The hidden `--timeout` alias of `--max-duration` (P2-1 / D-16). */
|
|
33
46
|
timeout?: string;
|
|
34
47
|
permissionMode?: string;
|
|
48
|
+
/** `--decision-timeout <ms>`; bridge only. */
|
|
49
|
+
decisionTimeout?: string;
|
|
50
|
+
/** `--on-decision-timeout deny|error`; bridge only. */
|
|
51
|
+
onDecisionTimeout?: string;
|
|
52
|
+
/** `--caller-tool-timeout <ms>`; bridge only. */
|
|
53
|
+
callerToolTimeout?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Path to the caller's JSON Schema (headless-request-scope-and-typed-result
|
|
56
|
+
* section 4.1). Not readable from here: this module resolves flags, never the
|
|
57
|
+
* world, so `exec/index.ts` opens the file and reports `result_schema_unreadable`.
|
|
58
|
+
*/
|
|
59
|
+
resultSchema?: string;
|
|
60
|
+
/** `--result-repair-turns 0..2`. Needs `--result-schema`. */
|
|
61
|
+
resultRepairTurns?: string;
|
|
62
|
+
/** `--on-result-unsatisfied error|continue`. Needs `--result-schema`. */
|
|
63
|
+
onResultUnsatisfied?: string;
|
|
64
|
+
/** `--request-max-turns <n>`; bridge only. */
|
|
65
|
+
requestMaxTurns?: string;
|
|
66
|
+
/** `--request-max-duration <ms>`; bridge only. */
|
|
67
|
+
requestMaxDuration?: string;
|
|
68
|
+
/** `--threads`. Requires bridge input AND stream-json output. */
|
|
69
|
+
threads?: boolean;
|
|
70
|
+
/** `--max-open-threads <n>`; thread mode only. */
|
|
71
|
+
maxOpenThreads?: string;
|
|
72
|
+
/** `--thread-concurrency <n>`; thread mode only. */
|
|
73
|
+
threadConcurrency?: string;
|
|
74
|
+
/** `--hook-timeout <ms>`; bridge only. */
|
|
75
|
+
hookTimeout?: string;
|
|
76
|
+
/** `--on-hook-timeout deny|allow|error`; bridge only. */
|
|
77
|
+
onHookTimeout?: string;
|
|
35
78
|
allowTool?: string[];
|
|
36
79
|
denyTool?: string[];
|
|
37
80
|
appendSystemPrompt?: string;
|
|
@@ -53,8 +96,52 @@ export interface ExecOptions {
|
|
|
53
96
|
maxTurns?: number;
|
|
54
97
|
maxDurationMs?: number;
|
|
55
98
|
permissionMode: ExecPermissionMode;
|
|
99
|
+
/**
|
|
100
|
+
* The three bridge budgets, ALWAYS RESOLVED and only ever used under
|
|
101
|
+
* `--input-format bridge`. Defaulted here rather than left optional so no
|
|
102
|
+
* consumer has to repeat `?? 300000` and get a different number than the
|
|
103
|
+
* `BRIDGE_LIMITS` one.
|
|
104
|
+
*/
|
|
105
|
+
decisionTimeoutMs: number;
|
|
106
|
+
onDecisionTimeout: ExecDecisionTimeout;
|
|
107
|
+
callerToolTimeoutMs: number;
|
|
56
108
|
allowTools: string[];
|
|
57
109
|
denyTools: string[];
|
|
110
|
+
/**
|
|
111
|
+
* The structured-result contract for this run (headless-request-scope-and-typed-
|
|
112
|
+
* result section 4.1). All three are absent unless `--result-schema` was given,
|
|
113
|
+
* which is what keeps the three output faces byte-identical without it (I-C1).
|
|
114
|
+
*
|
|
115
|
+
* `resultRepairTurns` and `onResultUnsatisfied` are RESOLVED HERE rather than left
|
|
116
|
+
* optional, for the reason the bridge budgets are: a consumer must not repeat
|
|
117
|
+
* `?? 1` and get a different number than `RESULT_LIMITS` says.
|
|
118
|
+
*/
|
|
119
|
+
resultSchemaPath?: string;
|
|
120
|
+
resultRepairTurns?: number;
|
|
121
|
+
onResultUnsatisfied?: ExecResultUnsatisfiedMode;
|
|
122
|
+
/**
|
|
123
|
+
* The per-request budgets. Bridge only, and NEVER defaulted - "no ceiling" is the
|
|
124
|
+
* honest default for a long-running process, and a default ceiling would silently
|
|
125
|
+
* cut off a legitimate long request (section 9.7).
|
|
126
|
+
*/
|
|
127
|
+
requestMaxTurns?: number;
|
|
128
|
+
requestMaxDurationMs?: number;
|
|
129
|
+
/**
|
|
130
|
+
* Thread mode (headless-threads-and-lifecycle-hooks section 4.1).
|
|
131
|
+
*
|
|
132
|
+
* `threads` is a BOOLEAN because the mode has no tuning of its own: the two ceilings below
|
|
133
|
+
* are per-run numbers with defaults that come from `THREAD_LIMITS`, resolved here for the
|
|
134
|
+
* same reason the bridge budgets are - no consumer may restate a default.
|
|
135
|
+
*/
|
|
136
|
+
threads: boolean;
|
|
137
|
+
maxOpenThreads: number;
|
|
138
|
+
threadConcurrency: number;
|
|
139
|
+
/**
|
|
140
|
+
* `--hook-timeout`, ALWAYS RESOLVED and only consulted by a bridge run that registered a
|
|
141
|
+
* hook. Clamped to 30 s under `--on-hook-timeout allow` (section 2.4.8, review P2-1).
|
|
142
|
+
*/
|
|
143
|
+
hookTimeoutMs: number;
|
|
144
|
+
onHookTimeout: HookTimeoutPolicy;
|
|
58
145
|
appendSystemPrompt?: string;
|
|
59
146
|
appendSystemPromptFile?: string;
|
|
60
147
|
quiet: boolean;
|
|
@@ -64,7 +151,31 @@ export interface ExecOptions {
|
|
|
64
151
|
*/
|
|
65
152
|
warnings: string[];
|
|
66
153
|
}
|
|
67
|
-
export type ExecUsageCode = 'invalid_output_format' | 'invalid_input_format' | 'invalid_permission_mode' | 'invalid_max_turns' | 'invalid_max_duration' | 'invalid_session_id' | 'unknown_tool' | 'unsupported_in_text_mode' | 'input_format_requires_stream_json' | 'stream_only_flag' | 'session_flag_conflict' | 'print_not_supported'
|
|
154
|
+
export type ExecUsageCode = 'invalid_output_format' | 'invalid_input_format' | 'invalid_permission_mode' | 'invalid_max_turns' | 'invalid_max_duration' | 'invalid_session_id' | 'unknown_tool' | 'unsupported_in_text_mode' | 'input_format_requires_stream_json' | 'stream_only_flag' | 'session_flag_conflict' | 'print_not_supported' | 'bridge_flag_requires_bridge' | 'ask_confirm_conflict' | 'invalid_decision_timeout' | 'invalid_caller_tool_timeout' | 'invalid_on_decision_timeout'
|
|
155
|
+
/**
|
|
156
|
+
* The structured-result vocabulary (section 4.1). The first five are decided HERE,
|
|
157
|
+
* from flags alone. The last three come from `exec/result/schema.ts` preflight and
|
|
158
|
+
* are reported by `exec/index.ts`, which is the module allowed to touch the disk -
|
|
159
|
+
* one union so a caller switching on the code has one list to read.
|
|
160
|
+
*/
|
|
161
|
+
| 'result_flag_requires_schema' | 'invalid_result_repair_turns' | 'invalid_on_result_unsatisfied' | 'invalid_request_max_turns' | 'invalid_request_max_duration'
|
|
162
|
+
/**
|
|
163
|
+
* THE PREFLIGHT VOCABULARY (section 2.3.3 / 4.1), and it is eight members rather
|
|
164
|
+
* than three on purpose: `exec/result/schema.ts` decides these from the file's
|
|
165
|
+
* CONTENT, `exec/index.ts` decides `result_schema_unreadable` from the filesystem,
|
|
166
|
+
* and a caller switching on the code needs ONE list - including the five that only
|
|
167
|
+
* the parser can produce.
|
|
168
|
+
*/
|
|
169
|
+
| 'result_schema_unreadable' | 'result_schema_too_large' | 'result_schema_invalid_json' | 'result_schema_not_object' | 'result_schema_too_deep' | 'result_schema_unsupported_ref' | 'result_schema_wrapper_conflict' | 'result_schema_composite_root'
|
|
170
|
+
/**
|
|
171
|
+
* The thread and hook vocabulary (headless-threads-and-lifecycle-hooks section 4.1).
|
|
172
|
+
*
|
|
173
|
+
* SEVEN MEMBERS, and the last two are the ones a caller meets first: a thread flag without
|
|
174
|
+
* `--threads` and a hook flag without bridge are the same class of mistake as
|
|
175
|
+
* `--decision-timeout` without a bridge, and this package answers that class with a usage
|
|
176
|
+
* error rather than a silent no-op.
|
|
177
|
+
*/
|
|
178
|
+
| 'threads_require_bridge' | 'threads_require_stream_json' | 'threads_flag_conflict' | 'threads_flag_requires_threads' | 'invalid_thread_limit' | 'invalid_hook_timeout' | 'invalid_on_hook_timeout';
|
|
68
179
|
export type ExecOptionsResult = {
|
|
69
180
|
ok: true;
|
|
70
181
|
options: ExecOptions;
|
|
@@ -73,4 +184,15 @@ export type ExecOptionsResult = {
|
|
|
73
184
|
code: ExecUsageCode;
|
|
74
185
|
message: string;
|
|
75
186
|
};
|
|
76
|
-
|
|
187
|
+
/** `--on-decision-timeout`. There is deliberately no `allow` member. */
|
|
188
|
+
export type ExecDecisionTimeout = 'deny' | 'error';
|
|
189
|
+
/**
|
|
190
|
+
* `--on-result-unsatisfied error|continue` (section 2.3.7, rule 3).
|
|
191
|
+
*
|
|
192
|
+
* `error` IS THE DEFAULT BECAUSE A SINGLE-CALL EMBEDDER MUST NOT BE TOLD "SUCCESS"
|
|
193
|
+
* FOR AN ANSWER IT DID NOT GET, and it is also the ONLY value that ends the process.
|
|
194
|
+
* `continue` keeps the exit code at 0 but never makes `result.isError` false: the two
|
|
195
|
+
* signals a wrapper branches on must not be able to disagree.
|
|
196
|
+
*/
|
|
197
|
+
export type ExecResultUnsatisfiedMode = 'error' | 'continue';
|
|
198
|
+
export declare function resolveExecOptions(raw: RawExecOptions, context?: ExecOptionsContext): ExecOptionsResult;
|
package/dist/exec/options.js
CHANGED
|
@@ -16,9 +16,25 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { HOST_TOOL_NAMES } from '../tools/index.js';
|
|
18
18
|
import { isValidSessionId } from '../session/store.js';
|
|
19
|
+
import { BRIDGE_LIMITS } from './bridge/limits.js';
|
|
20
|
+
import { RESULT_LIMITS } from './result/limits.js';
|
|
21
|
+
import { REQUEST_LIMITS } from './request/limits.js';
|
|
22
|
+
import { THREAD_LIMITS } from './threads/limits.js';
|
|
23
|
+
import { HOOK_LIMITS } from './hooks/limits.js';
|
|
19
24
|
const OUTPUT_FORMATS = ['text', 'json', 'stream-json'];
|
|
20
|
-
const INPUT_FORMATS = ['text', 'stream-json'];
|
|
21
|
-
const PERMISSION_MODES = ['auto', 'plan', 'strict'];
|
|
25
|
+
const INPUT_FORMATS = ['text', 'stream-json', 'bridge'];
|
|
26
|
+
const PERMISSION_MODES = ['auto', 'plan', 'strict', 'ask'];
|
|
27
|
+
const DECISION_TIMEOUT_MODES = ['deny', 'error'];
|
|
28
|
+
const RESULT_UNSATISFIED_MODES = ['error', 'continue'];
|
|
29
|
+
/**
|
|
30
|
+
* `--on-hook-timeout deny|allow|error` (section 2.4.8).
|
|
31
|
+
*
|
|
32
|
+
* `deny` IS THE DEFAULT AND IT POINTS THE SAFE WAY: an unanswered hook is an unevaluatable
|
|
33
|
+
* policy, and letting the tool run would silently disable the redactor. `allow` exists
|
|
34
|
+
* because a purely observational hook that blocks a tool on timeout would be absurd - which
|
|
35
|
+
* is also why it is the one mode whose timeout is clamped.
|
|
36
|
+
*/
|
|
37
|
+
const ON_HOOK_TIMEOUT_MODES = ['deny', 'allow', 'error'];
|
|
22
38
|
/**
|
|
23
39
|
* Registered nowhere in a headless session, yet real names (P2-2).
|
|
24
40
|
*
|
|
@@ -47,6 +63,10 @@ const TEXT_MODE_REFUSED = [
|
|
|
47
63
|
{ flag: '--input-format stream-json', test: (o) => o.inputFormat === 'stream-json' },
|
|
48
64
|
{ flag: '--partial-messages', test: (o) => o.partialMessages === true },
|
|
49
65
|
{ flag: '--include-thinking', test: (o) => o.includeThinking === true },
|
|
66
|
+
// Text mode delegates to `runHeadless` VERBATIM, and `runHeadless` has never heard
|
|
67
|
+
// of `submit_result`: accepting this flag there would promise a contract nothing
|
|
68
|
+
// honours. Refused for the reason the four above are, not because it is new.
|
|
69
|
+
{ flag: '--result-schema', test: (o) => o.resultSchema !== undefined },
|
|
50
70
|
];
|
|
51
71
|
function usage(code, message) {
|
|
52
72
|
return { ok: false, code, message };
|
|
@@ -71,7 +91,7 @@ function parsePositiveInt(raw) {
|
|
|
71
91
|
const n = Number.parseInt(raw.trim(), 10);
|
|
72
92
|
return Number.isSafeInteger(n) ? n : null;
|
|
73
93
|
}
|
|
74
|
-
export function resolveExecOptions(raw) {
|
|
94
|
+
export function resolveExecOptions(raw, context = {}) {
|
|
75
95
|
if (raw.print) {
|
|
76
96
|
return usage('print_not_supported', '`aragon exec` is already the headless face; `-p/--print` is not accepted here. ' +
|
|
77
97
|
'Use `aragon exec --output-format text` for the same stdout as `aragon -p`.');
|
|
@@ -84,8 +104,12 @@ export function resolveExecOptions(raw) {
|
|
|
84
104
|
if (!INPUT_FORMATS.includes(inputFormat)) {
|
|
85
105
|
return usage('invalid_input_format', `Unknown --input-format "${inputFormat}". Choose: ${INPUT_FORMATS.join(', ')}.`);
|
|
86
106
|
}
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
const genericInputFormat = inputFormat === 'stream-json' || inputFormat === 'bridge';
|
|
108
|
+
if (genericInputFormat && outputFormat !== 'stream-json') {
|
|
109
|
+
// ONE RULE, NAMED PER SPELLING. `bridge` needs the per-event writer for `seq`
|
|
110
|
+
// and for the pre-`init` queue, which is the same structural reason
|
|
111
|
+
// `stream-json` needs it - so the message quotes the flag actually typed.
|
|
112
|
+
return usage('input_format_requires_stream_json', `--input-format ${inputFormat} requires --output-format stream-json.`);
|
|
89
113
|
}
|
|
90
114
|
const formatError = checkFormatCompatibility(raw, outputFormat);
|
|
91
115
|
if (formatError)
|
|
@@ -94,6 +118,70 @@ export function resolveExecOptions(raw) {
|
|
|
94
118
|
if (!PERMISSION_MODES.includes(permissionMode)) {
|
|
95
119
|
return usage('invalid_permission_mode', `Unknown --permission-mode "${permissionMode}". Choose: ${PERMISSION_MODES.join(', ')}.`);
|
|
96
120
|
}
|
|
121
|
+
const bridge = inputFormat === 'bridge';
|
|
122
|
+
const bridgeFlagError = checkBridgeFlags(raw, bridge);
|
|
123
|
+
if (bridgeFlagError)
|
|
124
|
+
return bridgeFlagError;
|
|
125
|
+
// THE STRUCTURED-RESULT FLAGS, resolved before anything else reads them. Two of the
|
|
126
|
+
// five are meaningful only WITH `--result-schema`, and "you passed --on-result-
|
|
127
|
+
// unsatisfied without a schema" is a usage error rather than a silent no-op: a
|
|
128
|
+
// caller that meant to protect its output must not get a run that quietly cannot.
|
|
129
|
+
const structured = resolveStructuredResult(raw);
|
|
130
|
+
if (!structured.ok)
|
|
131
|
+
return structured.error;
|
|
132
|
+
const requestBudgets = resolveRequestBudgets(raw);
|
|
133
|
+
if (!requestBudgets.ok)
|
|
134
|
+
return requestBudgets.error;
|
|
135
|
+
// THREADS AND HOOKS, IN THIS ORDER. Hook state is legal WITHOUT threads - "one
|
|
136
|
+
// conversation with one redactor" is the commonest embedding shape - so the two are
|
|
137
|
+
// resolved independently and neither implies the other (section 9.9).
|
|
138
|
+
const policy = resolveHookPolicy(raw);
|
|
139
|
+
if (!policy.ok)
|
|
140
|
+
return policy.error;
|
|
141
|
+
const threads = resolveThreads(raw, bridge, outputFormat);
|
|
142
|
+
if (!threads.ok)
|
|
143
|
+
return threads.error;
|
|
144
|
+
const hooks = resolveHooks(raw, policy.value);
|
|
145
|
+
if (!hooks.ok)
|
|
146
|
+
return hooks.error;
|
|
147
|
+
// THE FOURTH BASELINE NEEDS SOMEWHERE TO ASK. Without bridge there is no
|
|
148
|
+
// decision channel at all, so `ask` would silently degrade to `auto` - the
|
|
149
|
+
// worst of the three possible outcomes (run unasked, refuse everything, or say
|
|
150
|
+
// the invocation is wrong). `exec/options.ts:11-15` settles the choice.
|
|
151
|
+
if (permissionMode === 'ask' && !bridge) {
|
|
152
|
+
return usage('bridge_flag_requires_bridge', '--permission-mode ask requires --input-format bridge: there is nobody to answer.');
|
|
153
|
+
}
|
|
154
|
+
// TWO APPROVAL LAYERS WOULD ASK TWICE, which a user experiences as "no
|
|
155
|
+
// approval at all". They cover the same three mutating tools; `skill_create` is
|
|
156
|
+
// reachable only through the bridge gate, so nothing is left unguarded.
|
|
157
|
+
if (permissionMode === 'ask' && context.confirmTools === true) {
|
|
158
|
+
return usage('ask_confirm_conflict', '--permission-mode ask and --confirm both gate the same three tools. Pick one.');
|
|
159
|
+
}
|
|
160
|
+
const decisionTimeout = resolveMilliseconds({
|
|
161
|
+
raw: raw.decisionTimeout,
|
|
162
|
+
flag: '--decision-timeout',
|
|
163
|
+
min: 1_000,
|
|
164
|
+
max: BRIDGE_LIMITS.maxDecisionTimeoutMs,
|
|
165
|
+
fallback: BRIDGE_LIMITS.defaultDecisionTimeoutMs,
|
|
166
|
+
code: 'invalid_decision_timeout',
|
|
167
|
+
});
|
|
168
|
+
if (!decisionTimeout.ok)
|
|
169
|
+
return decisionTimeout.error;
|
|
170
|
+
const callerToolTimeout = resolveMilliseconds({
|
|
171
|
+
raw: raw.callerToolTimeout,
|
|
172
|
+
flag: '--caller-tool-timeout',
|
|
173
|
+
min: 1_000,
|
|
174
|
+
max: BRIDGE_LIMITS.maxCallerToolTimeoutMs,
|
|
175
|
+
fallback: BRIDGE_LIMITS.defaultCallerToolTimeoutMs,
|
|
176
|
+
code: 'invalid_caller_tool_timeout',
|
|
177
|
+
});
|
|
178
|
+
if (!callerToolTimeout.ok)
|
|
179
|
+
return callerToolTimeout.error;
|
|
180
|
+
const onDecisionTimeout = (raw.onDecisionTimeout ?? 'deny').trim();
|
|
181
|
+
if (!DECISION_TIMEOUT_MODES.includes(onDecisionTimeout)) {
|
|
182
|
+
return usage('invalid_on_decision_timeout', `Unknown --on-decision-timeout "${onDecisionTimeout}". ` +
|
|
183
|
+
`Choose: ${DECISION_TIMEOUT_MODES.join(', ')}.`);
|
|
184
|
+
}
|
|
97
185
|
const budgets = resolveBudgets(raw);
|
|
98
186
|
if (!budgets.ok)
|
|
99
187
|
return budgets.error;
|
|
@@ -106,11 +194,34 @@ export function resolveExecOptions(raw) {
|
|
|
106
194
|
if (toolError)
|
|
107
195
|
return toolError;
|
|
108
196
|
const warnings = [];
|
|
109
|
-
for (const name of allowTools) {
|
|
197
|
+
for (const name of bridge ? [] : allowTools) {
|
|
110
198
|
if (NEVER_REGISTERED_HEADLESS.includes(name)) {
|
|
111
199
|
warnings.push(`--allow-tool ${name}: that tool is never registered in a headless run, so allowing it has no effect.`);
|
|
112
200
|
}
|
|
113
201
|
}
|
|
202
|
+
// THE THREE INERT-OR-MISLEADING COMBINATIONS (section 4.1, W-1..W-3). All three are
|
|
203
|
+
// warnings and none changes the exit code: each one describes a run that may still
|
|
204
|
+
// do exactly what the caller wanted, and refusing it would be overreach.
|
|
205
|
+
if (structured.resultSchemaPath !== undefined) {
|
|
206
|
+
if (budgets.maxTurns === 1) {
|
|
207
|
+
// W-1. One turn CAN be enough (the model may submit on it), so this is not an
|
|
208
|
+
// error - but the number is almost certainly a mistake.
|
|
209
|
+
warnings.push('--max-turns 1 with --result-schema: a structured result needs at least 2 turns ' +
|
|
210
|
+
'(submitting is itself a turn).');
|
|
211
|
+
}
|
|
212
|
+
if (requestBudgets.requestMaxTurns === 1) {
|
|
213
|
+
// W-2. Names the exact shape of the resulting `request_end` (P1-9), so the
|
|
214
|
+
// "two fields saying two different things" outcome is announced up front
|
|
215
|
+
// rather than discovered in a log.
|
|
216
|
+
warnings.push('--request-max-turns 1 with --result-schema: this request will end with ' +
|
|
217
|
+
"stopReason 'max_turns' and error.code 'result_schema_unsatisfied'.");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (structured.resultSchemaPath === undefined && denyTools.includes('submit_result')) {
|
|
221
|
+
// W-3. `submit_result` is registered ONLY by `--result-schema`, so denying it here
|
|
222
|
+
// denies a name that will never exist. Same argument as NEVER_REGISTERED_HEADLESS.
|
|
223
|
+
warnings.push('--deny-tool submit_result: that tool is registered only under --result-schema, so this deny has no effect.');
|
|
224
|
+
}
|
|
114
225
|
return {
|
|
115
226
|
ok: true,
|
|
116
227
|
options: {
|
|
@@ -128,6 +239,27 @@ export function resolveExecOptions(raw) {
|
|
|
128
239
|
...(budgets.maxTurns !== undefined ? { maxTurns: budgets.maxTurns } : {}),
|
|
129
240
|
...(budgets.maxDurationMs !== undefined ? { maxDurationMs: budgets.maxDurationMs } : {}),
|
|
130
241
|
permissionMode: permissionMode,
|
|
242
|
+
decisionTimeoutMs: decisionTimeout.value,
|
|
243
|
+
onDecisionTimeout: onDecisionTimeout,
|
|
244
|
+
callerToolTimeoutMs: callerToolTimeout.value,
|
|
245
|
+
...(structured.resultSchemaPath !== undefined
|
|
246
|
+
? {
|
|
247
|
+
resultSchemaPath: structured.resultSchemaPath,
|
|
248
|
+
resultRepairTurns: structured.resultRepairTurns,
|
|
249
|
+
onResultUnsatisfied: structured.onResultUnsatisfied,
|
|
250
|
+
}
|
|
251
|
+
: {}),
|
|
252
|
+
...(requestBudgets.requestMaxTurns !== undefined
|
|
253
|
+
? { requestMaxTurns: requestBudgets.requestMaxTurns }
|
|
254
|
+
: {}),
|
|
255
|
+
...(requestBudgets.requestMaxDurationMs !== undefined
|
|
256
|
+
? { requestMaxDurationMs: requestBudgets.requestMaxDurationMs }
|
|
257
|
+
: {}),
|
|
258
|
+
threads: threads.threads,
|
|
259
|
+
maxOpenThreads: threads.maxOpenThreads,
|
|
260
|
+
threadConcurrency: threads.threadConcurrency,
|
|
261
|
+
hookTimeoutMs: hooks.hookTimeoutMs,
|
|
262
|
+
onHookTimeout: policy.value,
|
|
131
263
|
allowTools,
|
|
132
264
|
denyTools,
|
|
133
265
|
...(raw.appendSystemPrompt ? { appendSystemPrompt: raw.appendSystemPrompt } : {}),
|
|
@@ -189,6 +321,281 @@ function resolveBudgets(raw) {
|
|
|
189
321
|
...(maxDurationMs !== undefined ? { maxDurationMs } : {}),
|
|
190
322
|
};
|
|
191
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* A bridge-only flag without `--input-format bridge` is a USAGE ERROR.
|
|
326
|
+
*
|
|
327
|
+
* NOT A WARNING, and not a silent no-op: these three flags tune a channel that
|
|
328
|
+
* does not exist in any other mode, so accepting them would promise a ceiling
|
|
329
|
+
* that is never consulted. This project's rule for that case is the same one
|
|
330
|
+
* `checkFormatCompatibility` applies to `--partial-messages` in `json` mode.
|
|
331
|
+
*/
|
|
332
|
+
function checkBridgeFlags(raw, bridge) {
|
|
333
|
+
if (bridge)
|
|
334
|
+
return null;
|
|
335
|
+
const present = [
|
|
336
|
+
raw.decisionTimeout !== undefined ? '--decision-timeout' : null,
|
|
337
|
+
raw.onDecisionTimeout !== undefined ? '--on-decision-timeout' : null,
|
|
338
|
+
raw.callerToolTimeout !== undefined ? '--caller-tool-timeout' : null,
|
|
339
|
+
// The two REQUEST-LEVEL budgets join the same rule for the same reason: they tune
|
|
340
|
+
// a per-request channel that does not exist outside bridge mode, and accepting
|
|
341
|
+
// them elsewhere would promise a ceiling nothing consults.
|
|
342
|
+
raw.requestMaxTurns !== undefined ? '--request-max-turns' : null,
|
|
343
|
+
raw.requestMaxDuration !== undefined ? '--request-max-duration' : null,
|
|
344
|
+
// The two HOOK flags join it too: `hook_register` is a bridge frame, so a timeout for a
|
|
345
|
+
// hook that can never be registered is the same inert promise.
|
|
346
|
+
raw.hookTimeout !== undefined ? '--hook-timeout' : null,
|
|
347
|
+
raw.onHookTimeout !== undefined ? '--on-hook-timeout' : null,
|
|
348
|
+
].filter((flag) => flag !== null);
|
|
349
|
+
if (present.length === 0)
|
|
350
|
+
return null;
|
|
351
|
+
const subject = present.join(' and ');
|
|
352
|
+
const verb = present.length === 1 ? 'requires' : 'require';
|
|
353
|
+
return usage('bridge_flag_requires_bridge', `${subject} ${verb} --input-format bridge.`);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Resolve `--result-schema`, `--result-repair-turns` and `--on-result-unsatisfied`.
|
|
357
|
+
*
|
|
358
|
+
* THE FILE IS NOT OPENED HERE. Its contents are the world, and this module is the
|
|
359
|
+
* flag parser: `exec/index.ts` reads it and reports `result_schema_unreadable`, which
|
|
360
|
+
* is the same split `--prompt-file` already has.
|
|
361
|
+
*/
|
|
362
|
+
function resolveStructuredResult(raw) {
|
|
363
|
+
const hasSchema = raw.resultSchema !== undefined;
|
|
364
|
+
if (!hasSchema) {
|
|
365
|
+
const orphan = [
|
|
366
|
+
raw.resultRepairTurns !== undefined ? '--result-repair-turns' : null,
|
|
367
|
+
raw.onResultUnsatisfied !== undefined ? '--on-result-unsatisfied' : null,
|
|
368
|
+
].filter((flag) => flag !== null);
|
|
369
|
+
if (orphan.length > 0) {
|
|
370
|
+
// A USAGE ERROR, NOT A WARNING: these two exist only to shape an answer that
|
|
371
|
+
// will never be requested, so "the run ignores you" would be the whole outcome.
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
error: usage('result_flag_requires_schema', `${orphan.join(' and ')} ${orphan.length === 1 ? 'requires' : 'require'} --result-schema.`),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
ok: true,
|
|
379
|
+
resultRepairTurns: RESULT_LIMITS.defaultRepairTurns,
|
|
380
|
+
onResultUnsatisfied: 'error',
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
let repairTurns = RESULT_LIMITS.defaultRepairTurns;
|
|
384
|
+
if (raw.resultRepairTurns !== undefined) {
|
|
385
|
+
const parsed = parsePositiveInt(raw.resultRepairTurns);
|
|
386
|
+
if (parsed === null || parsed > RESULT_LIMITS.maxRepairTurns) {
|
|
387
|
+
return {
|
|
388
|
+
ok: false,
|
|
389
|
+
error: usage('invalid_result_repair_turns', `--result-repair-turns must be an integer between 0 and ${RESULT_LIMITS.maxRepairTurns}.`),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
repairTurns = parsed;
|
|
393
|
+
}
|
|
394
|
+
const mode = (raw.onResultUnsatisfied ?? 'error').trim();
|
|
395
|
+
if (!RESULT_UNSATISFIED_MODES.includes(mode)) {
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
error: usage('invalid_on_result_unsatisfied', `Unknown --on-result-unsatisfied "${mode}". Choose: ${RESULT_UNSATISFIED_MODES.join(', ')}.`),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
ok: true,
|
|
403
|
+
resultSchemaPath: raw.resultSchema,
|
|
404
|
+
resultRepairTurns: repairTurns,
|
|
405
|
+
onResultUnsatisfied: mode,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/** The two per-request ceilings (section 4.1). Bridge-only, checked one level up. */
|
|
409
|
+
function resolveRequestBudgets(raw) {
|
|
410
|
+
let requestMaxTurns;
|
|
411
|
+
if (raw.requestMaxTurns !== undefined) {
|
|
412
|
+
const parsed = parsePositiveInt(raw.requestMaxTurns);
|
|
413
|
+
if (parsed === null || parsed < 1 || parsed > REQUEST_LIMITS.maxRequestTurns) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
error: usage('invalid_request_max_turns', `--request-max-turns must be an integer between 1 and ${REQUEST_LIMITS.maxRequestTurns}.`),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
requestMaxTurns = parsed;
|
|
420
|
+
}
|
|
421
|
+
let requestMaxDurationMs;
|
|
422
|
+
if (raw.requestMaxDuration !== undefined) {
|
|
423
|
+
const parsed = parsePositiveInt(raw.requestMaxDuration);
|
|
424
|
+
if (parsed === null ||
|
|
425
|
+
parsed < REQUEST_LIMITS.minRequestTimeoutMs ||
|
|
426
|
+
parsed > REQUEST_LIMITS.maxRequestTimeoutMs) {
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
error: usage('invalid_request_max_duration', `--request-max-duration must be an integer between ` +
|
|
430
|
+
`${REQUEST_LIMITS.minRequestTimeoutMs} and ${REQUEST_LIMITS.maxRequestTimeoutMs} ` +
|
|
431
|
+
'(milliseconds).'),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
requestMaxDurationMs = parsed;
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
ok: true,
|
|
438
|
+
...(requestMaxTurns !== undefined ? { requestMaxTurns } : {}),
|
|
439
|
+
...(requestMaxDurationMs !== undefined ? { requestMaxDurationMs } : {}),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Parse a bounded millisecond flag, or fall back to the protocol default.
|
|
444
|
+
*
|
|
445
|
+
* BOTH ENDS ARE CHECKED (review P2-5). A lower bound alone would let
|
|
446
|
+
* `--decision-timeout 999999999` through, and I-6 ("every ceiling has a number")
|
|
447
|
+
* would then be false for the one ceiling a caller can set - the number would be
|
|
448
|
+
* a literal in a flag parser rather than a member of `BRIDGE_LIMITS`.
|
|
449
|
+
*/
|
|
450
|
+
function resolveMilliseconds(options) {
|
|
451
|
+
if (options.raw === undefined)
|
|
452
|
+
return { ok: true, value: options.fallback };
|
|
453
|
+
const parsed = parsePositiveInt(options.raw);
|
|
454
|
+
if (parsed === null || parsed < options.min || parsed > options.max) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
error: usage(options.code, `${options.flag} must be an integer between ${options.min} and ${options.max} ` +
|
|
458
|
+
'(milliseconds).'),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return { ok: true, value: parsed };
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* `--threads` and its two ceilings (section 4.1).
|
|
465
|
+
*
|
|
466
|
+
* THREE CROSS-CHECKS, and each one names BOTH offenders: `--threads` without bridge has no
|
|
467
|
+
* frame channel, without `stream-json` has no per-event writer (so no `seq` and no
|
|
468
|
+
* pre-`init` queue), and with `--max-turns` would apply a process-wide turn budget to a
|
|
469
|
+
* conversation it cannot describe - the honest advice there is `--request-max-turns`.
|
|
470
|
+
*/
|
|
471
|
+
function resolveThreads(raw, bridge, outputFormat) {
|
|
472
|
+
const maxOpenThreads = resolveThreadLimit({
|
|
473
|
+
raw: raw.maxOpenThreads,
|
|
474
|
+
flag: '--max-open-threads',
|
|
475
|
+
fallback: THREAD_LIMITS.defaultMaxOpenThreads,
|
|
476
|
+
min: 1,
|
|
477
|
+
max: THREAD_LIMITS.maxOpenThreads,
|
|
478
|
+
});
|
|
479
|
+
if (!maxOpenThreads.ok)
|
|
480
|
+
return { ok: false, error: maxOpenThreads.error };
|
|
481
|
+
const threadConcurrency = resolveThreadLimit({
|
|
482
|
+
raw: raw.threadConcurrency,
|
|
483
|
+
flag: '--thread-concurrency',
|
|
484
|
+
fallback: THREAD_LIMITS.defaultConcurrency,
|
|
485
|
+
min: 1,
|
|
486
|
+
max: THREAD_LIMITS.maxOpenThreads,
|
|
487
|
+
});
|
|
488
|
+
if (!threadConcurrency.ok)
|
|
489
|
+
return { ok: false, error: threadConcurrency.error };
|
|
490
|
+
if (threadConcurrency.value > maxOpenThreads.value) {
|
|
491
|
+
return {
|
|
492
|
+
ok: false,
|
|
493
|
+
error: usage('invalid_thread_limit', `--thread-concurrency (${threadConcurrency.value}) cannot exceed --max-open-threads ` +
|
|
494
|
+
`(${maxOpenThreads.value}): there would be more slots than threads.`),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const ceilingGiven = raw.maxOpenThreads !== undefined || raw.threadConcurrency !== undefined;
|
|
498
|
+
if (ceilingGiven && raw.threads !== true) {
|
|
499
|
+
// A CEILING FOR A MODE THAT IS OFF IS THE SAME INERT PROMISE as a bridge flag without a
|
|
500
|
+
// bridge, and this package answers that class with a usage error rather than a no-op.
|
|
501
|
+
return {
|
|
502
|
+
ok: false,
|
|
503
|
+
error: usage('threads_flag_requires_threads', `${raw.maxOpenThreads !== undefined ? '--max-open-threads' : '--thread-concurrency'} ` +
|
|
504
|
+
'only applies with --threads.'),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (raw.threads !== true) {
|
|
508
|
+
return {
|
|
509
|
+
ok: true,
|
|
510
|
+
threads: false,
|
|
511
|
+
maxOpenThreads: maxOpenThreads.value,
|
|
512
|
+
threadConcurrency: threadConcurrency.value,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (!bridge) {
|
|
516
|
+
return {
|
|
517
|
+
ok: false,
|
|
518
|
+
error: usage('threads_require_bridge', '--threads requires --input-format bridge: a thread is opened, prompted and closed ' +
|
|
519
|
+
'through the frame channel, which no other input format has.'),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
if (outputFormat !== 'stream-json') {
|
|
523
|
+
return {
|
|
524
|
+
ok: false,
|
|
525
|
+
error: usage('threads_require_stream_json', '--threads requires --output-format stream-json: thread events and their `threadId` ' +
|
|
526
|
+
'need the per-event writer.'),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
if (raw.maxTurns !== undefined) {
|
|
530
|
+
return {
|
|
531
|
+
ok: false,
|
|
532
|
+
error: usage('threads_flag_conflict', '--max-turns cannot be combined with --threads: one process-wide turn budget has no ' +
|
|
533
|
+
'honest meaning across N conversations. Use --request-max-turns instead.'),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
ok: true,
|
|
538
|
+
threads: true,
|
|
539
|
+
maxOpenThreads: maxOpenThreads.value,
|
|
540
|
+
threadConcurrency: threadConcurrency.value,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function resolveThreadLimit(options) {
|
|
544
|
+
if (options.raw === undefined)
|
|
545
|
+
return { ok: true, value: options.fallback };
|
|
546
|
+
const parsed = parsePositiveInt(options.raw);
|
|
547
|
+
if (parsed === null || parsed < options.min || parsed > options.max) {
|
|
548
|
+
return {
|
|
549
|
+
ok: false,
|
|
550
|
+
error: usage('invalid_thread_limit', `${options.flag} must be an integer between ${options.min} and ${options.max}.`),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return { ok: true, value: parsed };
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* `--on-hook-timeout`, alone in its own resolver.
|
|
557
|
+
*
|
|
558
|
+
* A HOOK POLICY IS NOT A THREAD CEILING, and the first version of this file parsed it inside
|
|
559
|
+
* `resolveThreads` because the mode has to be known before the timeout is clamped. That put a
|
|
560
|
+
* hook rule in a thread function and made `--on-hook-timeout` unreadable to anyone looking for
|
|
561
|
+
* it under "hooks". The dependency is the other way round: the policy is resolved first and
|
|
562
|
+
* handed to `resolveHooks`.
|
|
563
|
+
*/
|
|
564
|
+
function resolveHookPolicy(raw) {
|
|
565
|
+
const mode = (raw.onHookTimeout ?? 'deny').trim();
|
|
566
|
+
if (!ON_HOOK_TIMEOUT_MODES.includes(mode)) {
|
|
567
|
+
return {
|
|
568
|
+
ok: false,
|
|
569
|
+
error: usage('invalid_on_hook_timeout', `Unknown --on-hook-timeout "${mode}". Choose: ${ON_HOOK_TIMEOUT_MODES.join(', ')}.`),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
return { ok: true, value: mode };
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* `--hook-timeout` (section 2.4.8).
|
|
576
|
+
*
|
|
577
|
+
* THE `allow` CLAMP IS A RULE, NOT A CONVENIENCE (review P2-1): a tool can be asked twice
|
|
578
|
+
* (`tool_pre` + `tool_post`) and `ToolExecutor`'s own budget is 120 s, so two round trips
|
|
579
|
+
* that may each consume the full 60 s could starve a legitimate tool call. Under `deny` and
|
|
580
|
+
* `error` the ceiling stays at 60 s, because those stop the tool at the deadline instead of
|
|
581
|
+
* waiting longer.
|
|
582
|
+
*/
|
|
583
|
+
function resolveHooks(raw, proposed) {
|
|
584
|
+
const max = proposed === 'allow' ? HOOK_LIMITS.allowModeMaxTimeoutMs : HOOK_LIMITS.maxTimeoutMs;
|
|
585
|
+
if (raw.hookTimeout === undefined) {
|
|
586
|
+
const fallback = Math.min(HOOK_LIMITS.defaultTimeoutMs, max);
|
|
587
|
+
return { ok: true, hookTimeoutMs: fallback };
|
|
588
|
+
}
|
|
589
|
+
const parsed = parsePositiveInt(raw.hookTimeout);
|
|
590
|
+
if (parsed === null || parsed < HOOK_LIMITS.minTimeoutMs || parsed > max) {
|
|
591
|
+
return {
|
|
592
|
+
ok: false,
|
|
593
|
+
error: usage('invalid_hook_timeout', `--hook-timeout must be an integer between ${HOOK_LIMITS.minTimeoutMs} and ${max}` +
|
|
594
|
+
(proposed === 'allow' ? ' under --on-hook-timeout allow.' : '.')),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return { ok: true, hookTimeoutMs: parsed };
|
|
598
|
+
}
|
|
192
599
|
function checkSessionFlags(raw) {
|
|
193
600
|
if (raw.sessionId !== undefined) {
|
|
194
601
|
const id = raw.sessionId.trim();
|
|
@@ -25,7 +25,15 @@
|
|
|
25
25
|
* plan mode the point is that the model KNOWS the write exists and is being
|
|
26
26
|
* deferred; for a deny list the point is the opposite (D-4).
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* `ask` is the fourth baseline and it CHANGES NOTHING HERE (headless-host-bridge
|
|
30
|
+
* section 2.3.2). It filters no tool - an `ask` run registers exactly what an `auto` run
|
|
31
|
+
* registers - because what it changes is the CALL: `withToolApproval` makes the
|
|
32
|
+
* mutating tools wait for the caller first. Keeping the filter identical is what
|
|
33
|
+
* makes `--permission-mode ask --deny-tool bash` mean "not registered, not asked"
|
|
34
|
+
* rather than "asked and then filtered".
|
|
35
|
+
*/
|
|
36
|
+
export type PermissionMode = 'auto' | 'plan' | 'strict' | 'ask';
|
|
29
37
|
export interface ToolPermission {
|
|
30
38
|
mode: PermissionMode;
|
|
31
39
|
allow: ReadonlySet<string>;
|
package/dist/exec/permission.js
CHANGED
|
@@ -82,7 +82,13 @@ export function resolveToolAllowList(registered, mode, allow, deny) {
|
|
|
82
82
|
* alias of `-p --plan`.
|
|
83
83
|
*/
|
|
84
84
|
export function resolvePermission(input) {
|
|
85
|
-
|
|
85
|
+
// `ask` ALWAYS PRODUCES AN OBJECT, even with both lists empty, for the reason
|
|
86
|
+
// `strict` does: an empty allow list under `ask` is a posture ("ask me about
|
|
87
|
+
// everything in `CALLER_GATED_TOOLS`"), not the absence of one. It still filters
|
|
88
|
+
// nothing - `decide()` returns `true` for every name under `ask` apart from the
|
|
89
|
+
// deny list - so the array it hands back is the array `auto` would have built.
|
|
90
|
+
const alwaysPresent = input.mode === 'strict' || input.mode === 'ask';
|
|
91
|
+
if (!alwaysPresent && input.allow.length === 0 && input.deny.length === 0) {
|
|
86
92
|
return undefined;
|
|
87
93
|
}
|
|
88
94
|
const allow = new Set(input.allow);
|