@aragon-agent/cli 0.6.37 → 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.
Files changed (162) hide show
  1. package/CHANGELOG.md +2523 -2326
  2. package/README.md +3614 -3173
  3. package/dist/agent/controller.d.ts +142 -1
  4. package/dist/agent/controller.js +171 -3
  5. package/dist/agent/reducer.d.ts +41 -1
  6. package/dist/agent/reducer.js +25 -0
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +5 -0
  9. package/dist/cli.js +85 -27
  10. package/dist/commands/builtins.js +13 -1
  11. package/dist/commands/keys-command.d.ts +23 -0
  12. package/dist/commands/keys-command.js +38 -0
  13. package/dist/commands/perf.d.ts +7 -1
  14. package/dist/commands/perf.js +2 -2
  15. package/dist/commands/registry.d.ts +39 -0
  16. package/dist/commands/task-command.d.ts +39 -0
  17. package/dist/commands/task-command.js +102 -0
  18. package/dist/config/cli-commands.d.ts +23 -0
  19. package/dist/config/cli-commands.js +36 -0
  20. package/dist/config/env.js +17 -4
  21. package/dist/config/load.js +6 -1
  22. package/dist/config/schema.d.ts +43 -4
  23. package/dist/config/schema.js +31 -5
  24. package/dist/config/store.js +17 -2
  25. package/dist/diagnostics/doctor.d.ts +9 -0
  26. package/dist/diagnostics/doctor.js +34 -0
  27. package/dist/diagnostics/info.d.ts +71 -0
  28. package/dist/diagnostics/info.js +91 -5
  29. package/dist/exec/bridge/caller-tools.d.ts +96 -0
  30. package/dist/exec/bridge/caller-tools.js +213 -0
  31. package/dist/exec/bridge/decisions.d.ts +78 -0
  32. package/dist/exec/bridge/decisions.js +116 -0
  33. package/dist/exec/bridge/human-answers.d.ts +38 -0
  34. package/dist/exec/bridge/human-answers.js +164 -0
  35. package/dist/exec/bridge/limits.d.ts +68 -0
  36. package/dist/exec/bridge/limits.js +68 -0
  37. package/dist/exec/bridge/ndjson.d.ts +17 -0
  38. package/dist/exec/bridge/ndjson.js +83 -0
  39. package/dist/exec/bridge/pending.d.ts +70 -0
  40. package/dist/exec/bridge/pending.js +132 -0
  41. package/dist/exec/bridge/protocol.d.ts +346 -0
  42. package/dist/exec/bridge/protocol.js +256 -0
  43. package/dist/exec/bridge/server.d.ts +142 -0
  44. package/dist/exec/bridge/server.js +502 -0
  45. package/dist/exec/bridge/source.d.ts +58 -0
  46. package/dist/exec/bridge/source.js +145 -0
  47. package/dist/exec/bridge/writer.d.ts +26 -0
  48. package/dist/exec/bridge/writer.js +156 -0
  49. package/dist/exec/cli-commands.js +30 -2
  50. package/dist/exec/emitter.d.ts +16 -2
  51. package/dist/exec/emitter.js +26 -6
  52. package/dist/exec/events.d.ts +465 -10
  53. package/dist/exec/events.js +47 -1
  54. package/dist/exec/hooks/dispatch.d.ts +76 -0
  55. package/dist/exec/hooks/dispatch.js +169 -0
  56. package/dist/exec/hooks/index.d.ts +83 -0
  57. package/dist/exec/hooks/index.js +205 -0
  58. package/dist/exec/hooks/limits.d.ts +56 -0
  59. package/dist/exec/hooks/limits.js +49 -0
  60. package/dist/exec/hooks/registry.d.ts +49 -0
  61. package/dist/exec/hooks/registry.js +91 -0
  62. package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
  63. package/dist/exec/hooks/tool-wrapper.js +154 -0
  64. package/dist/exec/hooks/turn.d.ts +61 -0
  65. package/dist/exec/hooks/turn.js +59 -0
  66. package/dist/exec/hooks/validate.d.ts +85 -0
  67. package/dist/exec/hooks/validate.js +310 -0
  68. package/dist/exec/index.d.ts +130 -4
  69. package/dist/exec/index.js +501 -10
  70. package/dist/exec/options.d.ts +124 -2
  71. package/dist/exec/options.js +413 -6
  72. package/dist/exec/permission.d.ts +9 -1
  73. package/dist/exec/permission.js +7 -1
  74. package/dist/exec/request/limits.d.ts +51 -0
  75. package/dist/exec/request/limits.js +49 -0
  76. package/dist/exec/request/scope.d.ts +106 -0
  77. package/dist/exec/request/scope.js +149 -0
  78. package/dist/exec/result/limits.d.ts +74 -0
  79. package/dist/exec/result/limits.js +72 -0
  80. package/dist/exec/result/prompt.d.ts +28 -0
  81. package/dist/exec/result/prompt.js +74 -0
  82. package/dist/exec/result/schema.d.ts +99 -0
  83. package/dist/exec/result/schema.js +464 -0
  84. package/dist/exec/result/sink.d.ts +56 -0
  85. package/dist/exec/result/sink.js +50 -0
  86. package/dist/exec/result/tool.d.ts +37 -0
  87. package/dist/exec/result/tool.js +118 -0
  88. package/dist/exec/runner.d.ts +395 -1
  89. package/dist/exec/runner.js +860 -8
  90. package/dist/exec/threads/aggregate.d.ts +34 -0
  91. package/dist/exec/threads/aggregate.js +30 -0
  92. package/dist/exec/threads/capacity.d.ts +23 -0
  93. package/dist/exec/threads/capacity.js +38 -0
  94. package/dist/exec/threads/dispatch-source.d.ts +60 -0
  95. package/dist/exec/threads/dispatch-source.js +106 -0
  96. package/dist/exec/threads/execute-threads.d.ts +53 -0
  97. package/dist/exec/threads/execute-threads.js +234 -0
  98. package/dist/exec/threads/gate.d.ts +42 -0
  99. package/dist/exec/threads/gate.js +81 -0
  100. package/dist/exec/threads/identity.d.ts +36 -0
  101. package/dist/exec/threads/identity.js +67 -0
  102. package/dist/exec/threads/interrupt.d.ts +29 -0
  103. package/dist/exec/threads/interrupt.js +79 -0
  104. package/dist/exec/threads/limits.d.ts +58 -0
  105. package/dist/exec/threads/limits.js +35 -0
  106. package/dist/exec/threads/outcome.d.ts +47 -0
  107. package/dist/exec/threads/outcome.js +26 -0
  108. package/dist/exec/threads/registry.d.ts +81 -0
  109. package/dist/exec/threads/registry.js +427 -0
  110. package/dist/exec/threads/runtime.d.ts +107 -0
  111. package/dist/exec/threads/runtime.js +523 -0
  112. package/dist/exec/threads/thread-source.d.ts +20 -0
  113. package/dist/exec/threads/thread-source.js +134 -0
  114. package/dist/fast/limits.d.ts +54 -0
  115. package/dist/fast/limits.js +54 -0
  116. package/dist/fast/summary-call.d.ts +72 -0
  117. package/dist/fast/summary-call.js +133 -0
  118. package/dist/fast/summary-prompt.d.ts +106 -0
  119. package/dist/fast/summary-prompt.js +320 -0
  120. package/dist/fast/task-summary-types.d.ts +66 -0
  121. package/dist/fast/task-summary-types.js +17 -0
  122. package/dist/logging/logger.d.ts +1 -1
  123. package/dist/proc/limits.d.ts +7 -0
  124. package/dist/proc/limits.js +7 -0
  125. package/dist/proc/output-streams.d.ts +50 -0
  126. package/dist/proc/output-streams.js +54 -0
  127. package/dist/team/runtime.d.ts +9 -0
  128. package/dist/team/runtime.js +8 -0
  129. package/dist/team/subagent.d.ts +15 -0
  130. package/dist/team/subagent.js +45 -1
  131. package/dist/tools/bash-tool.d.ts +10 -0
  132. package/dist/tools/bash-tool.js +71 -6
  133. package/dist/tools/index.d.ts +93 -1
  134. package/dist/tools/index.js +112 -3
  135. package/dist/tools/tool-gate.d.ts +100 -0
  136. package/dist/tools/tool-gate.js +87 -0
  137. package/dist/ui/App.js +224 -11
  138. package/dist/ui/Header.d.ts +28 -16
  139. package/dist/ui/Header.js +21 -19
  140. package/dist/ui/interaction-hints.js +6 -0
  141. package/dist/ui/layout/OverlayFrame.d.ts +18 -0
  142. package/dist/ui/layout/OverlayFrame.js +19 -4
  143. package/dist/ui/layout/header-hints.d.ts +55 -62
  144. package/dist/ui/layout/header-hints.js +59 -166
  145. package/dist/ui/layout/header-left.d.ts +115 -0
  146. package/dist/ui/layout/header-left.js +206 -0
  147. package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
  148. package/dist/ui/layout/shortcut-panel-layout.js +321 -0
  149. package/dist/ui/layout/status-detail-layout.js +5 -1
  150. package/dist/ui/layout/task-summary-cell.d.ts +61 -0
  151. package/dist/ui/layout/task-summary-cell.js +65 -0
  152. package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
  153. package/dist/ui/overlays/HelpOverlay.js +29 -2
  154. package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
  155. package/dist/ui/overlays/ShortcutPanel.js +106 -0
  156. package/dist/ui/scrollback/canvas.d.ts +3 -2
  157. package/dist/ui/scrollback/canvas.js +3 -2
  158. package/dist/ui/scrollback/scrollback-writer.d.ts +13 -1
  159. package/dist/ui/scrollback/scrollback-writer.js +109 -10
  160. package/dist/ui/shortcuts.d.ts +81 -0
  161. package/dist/ui/shortcuts.js +221 -0
  162. package/package.json +2 -2
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Validating untrusted hook frames (headless-threads-and-lifecycle-hooks section 2.4.3).
3
+ *
4
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
5
+ *
6
+ * PURE, AND THAT IS THE POINT. `hook_register` and `hook_answer` arrive from a pipe, so
7
+ * every field is `unknown` until something says otherwise. This module is that
8
+ * something, and it is the ONLY reader (I-H7): a second validator somewhere else would
9
+ * mean an answer that is legal in one path and illegal in another, which is worse than
10
+ * having no validator at all.
11
+ *
12
+ * EVERY REFUSAL IS A STABLE CODE, NEVER A SILENT CORRECTION. The two exceptions are
13
+ * deliberate and both are documented where they happen: an out-of-range `timeoutMs` is
14
+ * clamped (with a warning on the ack) because the caller asked for a bounded round trip
15
+ * and got a smaller one, and a missing `tools` defaults to `['*']` because "every tool"
16
+ * is the only sensible reading of an absent filter.
17
+ */
18
+ import { HOOK_LIMITS, HOOK_POINTS, isHookPoint, } from './limits.js';
19
+ const CODE_UNKNOWN_POINT = 'unknown_hook_point';
20
+ const CODE_UNKNOWN_TOOL = 'unknown_tool';
21
+ const CODE_INVALID = 'hook_invalid_answer';
22
+ /**
23
+ * Whole-frame validation: ALL specs are accepted or the frame is refused.
24
+ *
25
+ * PARTIAL SUCCESS IS UNIMPOSSIBLE TO DEBUG. A caller that sent six hooks and got
26
+ * `accepted:true` with two of them silently dropped has no way to tell which two, and
27
+ * the failure it causes later ("the redactor did not run for read_file") looks exactly
28
+ * like a bug in the caller's own matcher.
29
+ */
30
+ export function validateHookSpecs(raw, options) {
31
+ const specs = [];
32
+ const warnings = [];
33
+ for (let index = 0; index < raw.length; index += 1) {
34
+ const entry = raw[index];
35
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
36
+ return {
37
+ ok: false,
38
+ code: CODE_INVALID,
39
+ message: `hooks[${index}] must be an object with a "point".`,
40
+ };
41
+ }
42
+ const record = entry;
43
+ if (!isHookPoint(record.point)) {
44
+ return {
45
+ ok: false,
46
+ code: CODE_UNKNOWN_POINT,
47
+ message: `hooks[${index}].point must be one of ${HOOK_POINTS.join(', ')}; ` +
48
+ `got ${JSON.stringify(record.point)}.`,
49
+ };
50
+ }
51
+ const tools = normaliseTools(record.tools, index);
52
+ if ('code' in tools)
53
+ return { ok: false, code: tools.code, message: tools.message };
54
+ for (const tool of tools.tools) {
55
+ if (tool === '*')
56
+ continue;
57
+ if (options.knownTools.has(tool))
58
+ continue;
59
+ return {
60
+ ok: false,
61
+ code: CODE_UNKNOWN_TOOL,
62
+ message: `hooks[${index}].tools names "${tool}", which is not a tool this run can call. ` +
63
+ 'Use "*" to match every tool.',
64
+ };
65
+ }
66
+ const timeout = normaliseTimeout(record.timeoutMs, index, warnings);
67
+ specs.push({
68
+ point: record.point,
69
+ ...(typeof record.name === 'string' && record.name.trim().length > 0
70
+ ? { name: record.name.trim() }
71
+ : {}),
72
+ tools: tools.tools,
73
+ timeoutMs: timeout,
74
+ });
75
+ }
76
+ return { ok: true, specs, warnings };
77
+ }
78
+ function normaliseTools(value, index) {
79
+ if (value === undefined)
80
+ return { tools: ['*'] };
81
+ if (!Array.isArray(value)) {
82
+ return { code: CODE_INVALID, message: `hooks[${index}].tools must be an array of names.` };
83
+ }
84
+ const names = [];
85
+ for (const item of value) {
86
+ if (typeof item !== 'string' || item.trim().length === 0) {
87
+ return { code: CODE_INVALID, message: `hooks[${index}].tools must hold non-empty strings.` };
88
+ }
89
+ const name = item.trim();
90
+ if (!names.includes(name))
91
+ names.push(name);
92
+ }
93
+ return { tools: names.length > 0 ? names : ['*'] };
94
+ }
95
+ /**
96
+ * CLAMP, DO NOT REFUSE, and say so.
97
+ *
98
+ * A timeout outside the window describes a round trip the caller wants bounded, so the
99
+ * nearest legal bound is what they asked for; refusing the whole registration would
100
+ * leave the run with no hook at all over a number that only shapes a wait. The warning
101
+ * on the ack is what keeps the clamping honest.
102
+ */
103
+ function normaliseTimeout(value, index, warnings) {
104
+ if (value === undefined)
105
+ return HOOK_LIMITS.defaultTimeoutMs;
106
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
107
+ warnings.push(`hooks[${index}].timeoutMs was not a number; using ${HOOK_LIMITS.defaultTimeoutMs}.`);
108
+ return HOOK_LIMITS.defaultTimeoutMs;
109
+ }
110
+ const ms = Math.round(value);
111
+ if (ms < HOOK_LIMITS.minTimeoutMs) {
112
+ warnings.push(`hooks[${index}].timeoutMs raised to ${HOOK_LIMITS.minTimeoutMs}.`);
113
+ return HOOK_LIMITS.minTimeoutMs;
114
+ }
115
+ if (ms > HOOK_LIMITS.maxTimeoutMs) {
116
+ warnings.push(`hooks[${index}].timeoutMs lowered to ${HOOK_LIMITS.maxTimeoutMs}.`);
117
+ return HOOK_LIMITS.maxTimeoutMs;
118
+ }
119
+ return ms;
120
+ }
121
+ /**
122
+ * THE FOUR POINTS' LEGAL ACTIONS, ENFORCED HERE AND NOWHERE ELSE.
123
+ *
124
+ * A single `validateHookAnswer(point, raw)` rather than four validators, because the
125
+ * frame is one shape and the point is a property of the request the CLI made - so the
126
+ * caller never has to restate which point it is answering.
127
+ */
128
+ export function validateHookAnswer(point, raw) {
129
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
130
+ return { ok: false, code: CODE_INVALID, message: 'A hook answer must be an object.' };
131
+ }
132
+ const record = raw;
133
+ const action = typeof record.action === 'string' ? record.action : '';
134
+ switch (point) {
135
+ case 'prompt_pre':
136
+ return validatePromptPre(action, record);
137
+ case 'turn_start':
138
+ return validateTurnStart(action, record);
139
+ case 'tool_pre':
140
+ return validateToolPre(action, record);
141
+ case 'tool_post':
142
+ return validateToolPost(action, record);
143
+ default:
144
+ return { ok: false, code: CODE_INVALID, message: `Unknown hook point "${String(point)}".` };
145
+ }
146
+ }
147
+ function denyAnswer(record) {
148
+ const reason = clipReason(record.reason);
149
+ const message = clipReason(record.message);
150
+ return {
151
+ ok: true,
152
+ answer: {
153
+ action: 'deny',
154
+ ...(reason ? { reason } : {}),
155
+ ...(message ? { message } : {}),
156
+ },
157
+ };
158
+ }
159
+ function validatePromptPre(action, record) {
160
+ if (action === 'allow')
161
+ return { ok: true, answer: { action: 'allow' } };
162
+ if (action === 'deny')
163
+ return denyAnswer(record);
164
+ if (action !== 'rewrite')
165
+ return wrongAction('prompt_pre', 'allow, rewrite, deny', action);
166
+ const text = typeof record.text === 'string' ? record.text : '';
167
+ // AN EMPTY REWRITE IS INVALID, NOT AN EMPTY PROMPT: Core rejects an empty user message
168
+ // with a provider 400, which is the hardest class of failure to attribute after the
169
+ // fact (section 2.4.7).
170
+ if (text.trim().length === 0) {
171
+ return { ok: false, code: CODE_INVALID, message: 'A prompt_pre rewrite needs non-empty text.' };
172
+ }
173
+ if (text.length > HOOK_LIMITS.maxInjectChars) {
174
+ return {
175
+ ok: false,
176
+ code: CODE_INVALID,
177
+ message: `A prompt_pre rewrite may carry at most ${HOOK_LIMITS.maxInjectChars} characters.`,
178
+ };
179
+ }
180
+ return { ok: true, answer: { action: 'rewrite', text } };
181
+ }
182
+ function validateTurnStart(action, record) {
183
+ if (action === 'allow')
184
+ return { ok: true, answer: { action: 'allow' } };
185
+ if (action !== 'inject')
186
+ return wrongAction('turn_start', 'allow, inject', action);
187
+ const text = typeof record.text === 'string' ? record.text.trim() : '';
188
+ if (text.length === 0) {
189
+ return { ok: false, code: CODE_INVALID, message: 'A turn_start inject needs non-empty text.' };
190
+ }
191
+ if (text.length > HOOK_LIMITS.maxInjectChars) {
192
+ return {
193
+ ok: false,
194
+ code: CODE_INVALID,
195
+ message: `A turn_start inject may carry at most ${HOOK_LIMITS.maxInjectChars} characters.`,
196
+ };
197
+ }
198
+ return { ok: true, answer: { action: 'inject', text } };
199
+ }
200
+ function validateToolPre(action, record) {
201
+ if (action === 'allow')
202
+ return { ok: true, answer: { action: 'allow' } };
203
+ if (action === 'deny')
204
+ return denyAnswer(record);
205
+ if (action !== 'rewrite')
206
+ return wrongAction('tool_pre', 'allow, rewrite, deny', action);
207
+ const input = record.input;
208
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
209
+ return {
210
+ ok: false,
211
+ code: CODE_INVALID,
212
+ message: 'A tool_pre rewrite needs a JSON object "input".',
213
+ };
214
+ }
215
+ // THE TOOL'S OWN SCHEMA IS NOT CHECKED HERE. `wrapWithToolHooks` re-validates against
216
+ // `tool.parameters` at the point of use, because that is the only place the tool object
217
+ // exists - and an edit that passes this check and fails that one must be reported as
218
+ // `invalid`, which the wrapper does (I-H3).
219
+ return { ok: true, answer: { action: 'rewrite', input: input } };
220
+ }
221
+ function validateToolPost(action, record) {
222
+ if (action === 'keep')
223
+ return { ok: true, answer: { action: 'keep' } };
224
+ if (action === 'deny')
225
+ return denyAnswer(record);
226
+ if (action !== 'replace')
227
+ return wrongAction('tool_post', 'keep, replace, deny', action);
228
+ if (record.content === undefined) {
229
+ return { ok: false, code: CODE_INVALID, message: 'A tool_post replace needs "content".' };
230
+ }
231
+ // A PART LIST IS HISTORY, SO ITS SHAPE IS PART OF THE ANSWER'S VALIDITY.
232
+ //
233
+ // `content` becomes the `tool_result` the model reads, and it is persisted with the
234
+ // session: a part without a `type` produces a provider 400 on the NEXT request, and a
235
+ // resumed session keeps producing it. Attributing that to the model - which is what an
236
+ // unchecked answer does - is the one hole I-H7 cannot afford, so the shape is checked
237
+ // here, once, with the same `hook_invalid_answer` code as every other illegal answer.
238
+ if (Array.isArray(record.content)) {
239
+ for (const part of record.content) {
240
+ if (!part || typeof part !== 'object' || Array.isArray(part)) {
241
+ return {
242
+ ok: false,
243
+ code: CODE_INVALID,
244
+ message: 'A tool_post replace content array must hold content-block objects.',
245
+ };
246
+ }
247
+ const type = part.type;
248
+ if (typeof type !== 'string' || type.trim().length === 0) {
249
+ return {
250
+ ok: false,
251
+ code: CODE_INVALID,
252
+ message: 'Every content block in a tool_post replace needs a non-empty "type".',
253
+ };
254
+ }
255
+ }
256
+ }
257
+ const chars = contentChars(record.content);
258
+ if (chars > HOOK_LIMITS.maxContentChars) {
259
+ return {
260
+ ok: false,
261
+ code: CODE_INVALID,
262
+ message: `A tool_post replace may carry at most ${HOOK_LIMITS.maxContentChars} characters.`,
263
+ };
264
+ }
265
+ return {
266
+ ok: true,
267
+ answer: {
268
+ action: 'replace',
269
+ content: record.content,
270
+ ...(record.isError === true ? { isError: true } : {}),
271
+ },
272
+ };
273
+ }
274
+ function wrongAction(point, allowed, action) {
275
+ return {
276
+ ok: false,
277
+ code: CODE_INVALID,
278
+ message: `${point} accepts ${allowed}; got ${JSON.stringify(action)}.`,
279
+ };
280
+ }
281
+ /** Total characters in a content value, whether it is a string or a part list. */
282
+ function contentChars(content) {
283
+ if (typeof content === 'string')
284
+ return content.length;
285
+ if (Array.isArray(content)) {
286
+ let total = 0;
287
+ for (const part of content) {
288
+ const text = part?.text;
289
+ if (part && typeof part === 'object' && typeof text === 'string') {
290
+ total += (part.text).length;
291
+ }
292
+ else {
293
+ total += JSON.stringify(part ?? null).length;
294
+ }
295
+ }
296
+ return total;
297
+ }
298
+ return JSON.stringify(content ?? null).length;
299
+ }
300
+ /** Bound the one caller-supplied string that becomes MODEL-VISIBLE (I-H2's sibling). */
301
+ export function clipReason(value) {
302
+ if (typeof value !== 'string')
303
+ return undefined;
304
+ const trimmed = value.trim();
305
+ if (trimmed.length === 0)
306
+ return undefined;
307
+ return trimmed.length <= HOOK_LIMITS.maxReasonChars
308
+ ? trimmed
309
+ : trimmed.slice(0, HOOK_LIMITS.maxReasonChars);
310
+ }
@@ -20,6 +20,75 @@ import type { AgentController } from '../agent/controller.js';
20
20
  import { type CliFlags } from '../config/load.js';
21
21
  import { type RawExecOptions } from './options.js';
22
22
  import { type ToolPermission } from './permission.js';
23
+ import type { AgentTool } from '@aragon-agent/core';
24
+ import type { ApprovalGate } from '../skills/service.js';
25
+ import type { HumanInputGate } from '../tools/human-input.js';
26
+ import type { ToolGate } from '../tools/tool-gate.js';
27
+ import type { ToolHookRunner } from './hooks/tool-wrapper.js';
28
+ /**
29
+ * Everything `aragon exec` may hand the controller factory.
30
+ *
31
+ * THE THREE GATES ARE OPTIONAL AND ABSENT IN EVERY MODE BUT BRIDGE, which is the
32
+ * rule that keeps the tool array identical by object identity for `-p`, the TUI
33
+ * and every subagent (I-1). `cli.tsx` decides nothing here; it spreads what it is
34
+ * given.
35
+ */
36
+ export interface ExecControllerDeps {
37
+ permission?: ToolPermission;
38
+ appendSystemPrompt?: string;
39
+ /** The per-call approval gate; bridge only. */
40
+ toolGate?: ToolGate;
41
+ /** `skill_install` approval; bridge only. */
42
+ approval?: ApprovalGate;
43
+ /** `ask_user` / `submit_plan`; bridge only. */
44
+ humanInput?: HumanInputGate;
45
+ /** Added to each gated tool's executor timeout; bridge only. */
46
+ decisionBudgetMs?: number;
47
+ /** The gate each subagent gets, keyed by label; bridge only (section 2.3.7). */
48
+ toolGateForChild?: (label: string) => ToolGate;
49
+ /** Caller-served tools registered at construction; bridge only. */
50
+ callerTools?: AgentTool[];
51
+ /**
52
+ * `submit_result`, or absent (headless-request-scope-and-typed-result section 2.6
53
+ * row 11). Decided by `--result-schema` alone, NOT by bridge mode: a `json` run can
54
+ * carry a schema too, and there is no second request to scope it to.
55
+ */
56
+ resultTool?: AgentTool;
57
+ /** The `<structured_result>` block that tells the model the contract exists. */
58
+ structuredResultBlock?: string;
59
+ /**
60
+ * The caller's lifecycle hooks (headless-threads-and-lifecycle-hooks section 2.4.9).
61
+ *
62
+ * BRIDGE ONLY, and always present there even before `hook_register` arrives: the tool
63
+ * array is built once, at controller construction, while hooks are registered later.
64
+ */
65
+ hookRunner?: ToolHookRunner;
66
+ /**
67
+ * Set by the thread runtime, and read by `makeExecController` alone
68
+ * (section 2.3.4): non-`undefined` means "this controller is one of N".
69
+ */
70
+ threadId?: string;
71
+ /**
72
+ * WHETHER THIS CONSTRUCTION OWNS THE PROCESS-LEVEL LOG MOUNTS (review P1-8).
73
+ *
74
+ * `threadId` is the WRONG signal for that decision, because the default thread has an id
75
+ * too (`main`) - a guard on "is there a thread id" therefore skipped the mount for every
76
+ * thread, leaving a thread-mode run with no `run_exec` line, no `onFailure` handler (a
77
+ * failing log write becomes silent) and no untrusted-skill warning. Exactly ONE caller
78
+ * sets this: the runtime that opens the default thread. `undefined` means "single-run
79
+ * invocation", which owns them by definition.
80
+ */
81
+ processLevelMounts?: boolean;
82
+ /**
83
+ * Receives the two disposers `makeExecController` got back from
84
+ * `attachAgentEvents` / `attachTeamEvents`.
85
+ *
86
+ * A CALLBACK RATHER THAN A RETURN VALUE because the factory's signature is a port
87
+ * (`cli.tsx` implements it) and this round must not change that signature. Interactive
88
+ * callers pass nothing, which leaves their behaviour byte-identical.
89
+ */
90
+ onAttach?: (disposers: Array<() => void>) => void;
91
+ }
23
92
  /**
24
93
  * How `runExec` obtains a controller.
25
94
  *
@@ -29,10 +98,34 @@ import { type ToolPermission } from './permission.js';
29
98
  * untrusted-skill notices. Everything this module needs and nothing it should
30
99
  * decide for itself.
31
100
  */
32
- export type ExecControllerFactory = (flags: CliFlags, deps: {
33
- permission?: ToolPermission;
34
- appendSystemPrompt?: string;
35
- }) => AgentController;
101
+ /**
102
+ * How `runExec` obtains a controller.
103
+ *
104
+ * `cli.tsx` supplies `(flags, deps) => makeController(flags, { interactive:
105
+ * false, ...deps }).controller`, already wired for headless: the deny-all
106
+ * approval gate, the deny-all human-input gate, the log attachments and the
107
+ * untrusted-skill notices. Everything this module needs and nothing it should
108
+ * decide for itself.
109
+ */
110
+ /**
111
+ * How `runExec` obtains a controller.
112
+ *
113
+ * `cli.tsx` supplies `(flags, deps) => makeController(flags, { interactive:
114
+ * false, ...deps }).controller`, already wired for headless: the deny-all
115
+ * approval gate, the deny-all human-input gate, the log attachments and the
116
+ * untrusted-skill notices. Everything this module needs and nothing it should
117
+ * decide for itself.
118
+ */
119
+ /**
120
+ * How `runExec` obtains a controller.
121
+ *
122
+ * `cli.tsx` supplies `(flags, deps) => makeController(flags, { interactive:
123
+ * false, ...deps }).controller`, already wired for headless: the deny-all
124
+ * approval gate, the deny-all human-input gate, the log attachments and the
125
+ * untrusted-skill notices. Everything this module needs and nothing it should
126
+ * decide for itself.
127
+ */
128
+ export type ExecControllerFactory = (flags: CliFlags, deps: ExecControllerDeps) => AgentController;
36
129
  export interface RunExecContext {
37
130
  version: string;
38
131
  makeController: ExecControllerFactory;
@@ -54,3 +147,36 @@ export interface RunExecContext {
54
147
  * schema has begun - so that one DOES emit a `result` with `exitCode: 2`.
55
148
  */
56
149
  export declare function runExec(flags: CliFlags, raw: RawExecOptions, promptArg: string | undefined, ctx: RunExecContext): Promise<number>;
150
+ /**
151
+ * What thread mode needs, and why it is not an `ExecPromptSource`
152
+ * (headless-threads-and-lifecycle-hooks section 4.3, review P1-3).
153
+ *
154
+ * A PRE-BUILT SOURCE HAS ALREADY ATTACHED ITS STDIN LISTENERS. Handing one to a thread run
155
+ * would put two splitters on the same bytes and the frames after the first newline would
156
+ * land in whichever was registered first, so `execute()` refuses the combination and this
157
+ * type carries the STREAM instead.
158
+ */
159
+ export interface ThreadsInput {
160
+ /** The raw stdin. The thread dispatcher is its only parser. */
161
+ stdin: NodeJS.ReadableStream;
162
+ }
163
+ /**
164
+ * Persist one thread's session, for thread mode.
165
+ *
166
+ * A THIN WRAPPER OVER `persist()`, and deliberately not a second implementation: the file
167
+ * shape, the cumulative counters and the "a failed write is not a failed run" rule are one
168
+ * set of decisions. Threads never resume (section 1.4), so there is no previous metadata to
169
+ * accumulate onto; `--resume` stays a process-level, default-thread feature.
170
+ */
171
+ export declare function persistThreadSession(input: {
172
+ sessionId: string;
173
+ controller: AgentController;
174
+ turns: number;
175
+ usage: {
176
+ inputTokens: number;
177
+ outputTokens: number;
178
+ };
179
+ at: number;
180
+ cli: string;
181
+ cwd: string;
182
+ }): void;