@gaunt-sloth/core 2.0.0-alpha.2 → 2.0.0-alpha.4
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/.gsloth.code.md +10 -0
- package/README.md +3 -4
- package/dist/config/defaults.d.ts +84 -0
- package/dist/config/defaults.js +97 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/loader.d.ts +88 -0
- package/dist/config/loader.js +604 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +471 -0
- package/dist/config/schema.js +301 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/shell-policy.d.ts +212 -0
- package/dist/config/shell-policy.js +142 -0
- package/dist/config/shell-policy.js.map +1 -0
- package/dist/config/types.d.ts +453 -0
- package/dist/config/types.js +12 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config.d.ts +18 -647
- package/dist/config.js +15 -516
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +24 -1
- package/dist/core/GthAbstractAgent.js +86 -4
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +127 -1
- package/dist/core/GthAgentRunner.js +298 -4
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/shell/allowlist.d.ts +75 -0
- package/dist/core/shell/allowlist.js +187 -0
- package/dist/core/shell/allowlist.js.map +1 -0
- package/dist/core/shell/arity.d.ts +75 -0
- package/dist/core/shell/arity.js +313 -0
- package/dist/core/shell/arity.js.map +1 -0
- package/dist/core/shell/judge.d.ts +161 -0
- package/dist/core/shell/judge.js +261 -0
- package/dist/core/shell/judge.js.map +1 -0
- package/dist/core/shell/normalize.d.ts +27 -0
- package/dist/core/shell/normalize.js +53 -0
- package/dist/core/shell/normalize.js.map +1 -0
- package/dist/core/types.d.ts +75 -0
- package/dist/core/types.js.map +1 -1
- package/dist/providers/openrouter.js +2 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/utils/fileUtils.d.ts +4 -1
- package/dist/utils/fileUtils.js +19 -10
- package/dist/utils/fileUtils.js.map +1 -1
- package/dist/utils/systemUtils.d.ts +31 -0
- package/dist/utils/systemUtils.js +38 -0
- package/dist/utils/systemUtils.js.map +1 -1
- package/package.json +12 -8
- package/schema/gsloth-config.schema.json +1548 -0
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import { getEffectiveDevToolsConfig, getShellJudgeSettings, isShellAllowlistEnabled, isShellAllowlistPersisted, isShellJudgeEnabled, } from '#src/config.js';
|
|
1
2
|
import { GthLangChainAgent } from '#src/core/GthLangChainAgent.js';
|
|
3
|
+
import { AllowlistStore, PersistedAllowlist, matchesApproval, } from '#src/core/shell/allowlist.js';
|
|
4
|
+
import { classifyCommand } from '#src/core/shell/arity.js';
|
|
5
|
+
import { normalizeCommand } from '#src/core/shell/normalize.js';
|
|
6
|
+
import { judgeShellCommand, mapVerdictToAction, } from '#src/core/shell/judge.js';
|
|
7
|
+
import { env } from '#src/utils/systemUtils.js';
|
|
8
|
+
import { getGslothConfigWritePath } from '#src/utils/fileUtils.js';
|
|
9
|
+
import { SHELL_ALLOWLIST_FILE } from '#src/constants.js';
|
|
2
10
|
import { enhanceVertexUnauthorizedMessage } from '#src/utils/vertexaiUtils.js';
|
|
3
11
|
import { getNewRunnableConfig } from '#src/utils/llmUtils.js';
|
|
4
12
|
import { initDebugLogging, debugLog, debugLogError, debugLogObject, } from '#src/utils/debugUtils.js';
|
|
@@ -13,6 +21,38 @@ export class GthAgentRunner {
|
|
|
13
21
|
config = null;
|
|
14
22
|
runConfig = null;
|
|
15
23
|
agentFactory;
|
|
24
|
+
/**
|
|
25
|
+
* Consumer hook invoked when a run suspends on a tool-approval interrupt (e.g. the opt-in
|
|
26
|
+
* `run_shell_command` confirmation). Set via {@link setToolApprovalCallback}; when unset the
|
|
27
|
+
* runner REJECTS pending tool calls rather than hanging or auto-approving — the safe default
|
|
28
|
+
* for non-interactive entrypoints (a scripted `exec` run with no TTY to prompt on).
|
|
29
|
+
*/
|
|
30
|
+
toolApprovalCallback = null;
|
|
31
|
+
/** The command the runner was initialized for; selects which `devTools` config applies. */
|
|
32
|
+
command = undefined;
|
|
33
|
+
/**
|
|
34
|
+
* EXT-12 — runtime, session-scoped yolo flag toggled by the `/yolo` slash command. Distinct
|
|
35
|
+
* from the static `devTools.shellYolo` config flag (which omits the tool from `interruptOn` at
|
|
36
|
+
* agent-build time and cannot be changed mid-session). Because the tool stays gated (in
|
|
37
|
+
* `interruptOn`), this flag is consulted at the TOP of {@link decideToolApproval}: when ON, a
|
|
38
|
+
* gated `run_shell_command` is auto-approved WITHOUT prompting (yolo behaviour) for the rest of
|
|
39
|
+
* this runner's life. Never persisted; defaults OFF. It does NOT disable the hardline floor —
|
|
40
|
+
* catastrophic commands are still refused at exec time in `GthDevToolkit.executeCommand`.
|
|
41
|
+
*/
|
|
42
|
+
sessionYolo = false;
|
|
43
|
+
/**
|
|
44
|
+
* EXT-9 Tier-2 session allow-list — approved command prefixes that auto-approve for the
|
|
45
|
+
* life of THIS runner instance. Instance-scoped (not module-global) so concurrent
|
|
46
|
+
* sessions (ACP / AG-UI multi-session) cannot stomp each other's approvals.
|
|
47
|
+
*/
|
|
48
|
+
sessionAllowlist = new AllowlistStore();
|
|
49
|
+
/**
|
|
50
|
+
* EXT-9 Tier-2 persisted (`always`) allow-list, loaded lazily on first use from
|
|
51
|
+
* `.gsloth/.gsloth-settings/shell-allowlist.json`. Null until the shell tool is gated
|
|
52
|
+
* and the allow-list is enabled; null also when persistence is disabled by config.
|
|
53
|
+
*/
|
|
54
|
+
persistedAllowlist = null;
|
|
55
|
+
persistedAllowlistLoaded = false;
|
|
16
56
|
/**
|
|
17
57
|
* @param agentFactory Produces the {@link GthAgentInterface} the runner drives.
|
|
18
58
|
* Defaults to the lean {@link GthLangChainAgent} (core). `@gaunt-sloth/agent`
|
|
@@ -25,6 +65,28 @@ export class GthAgentRunner {
|
|
|
25
65
|
this.agentFactory =
|
|
26
66
|
agentFactory ?? ((status, agentResolvers) => new GthLangChainAgent(status, agentResolvers));
|
|
27
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Register the tool-approval handler the runner calls when a run suspends on a tool-approval
|
|
70
|
+
* interrupt (the interactive readline session wires a y/n prompt here). Pass `null` to clear.
|
|
71
|
+
* Without a handler the runner rejects pending tool calls (see {@link toolApprovalCallback}).
|
|
72
|
+
*/
|
|
73
|
+
setToolApprovalCallback(callback) {
|
|
74
|
+
this.toolApprovalCallback = callback;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* EXT-12 — flip the runtime, session-scoped yolo flag (the `/yolo` slash command). When ON,
|
|
78
|
+
* gated `run_shell_command` calls auto-approve without prompting for the rest of this session;
|
|
79
|
+
* the hardline floor still applies at exec time. Returns the NEW state so the caller can render
|
|
80
|
+
* a notice. Session-scoped only — nothing is written to config.
|
|
81
|
+
*/
|
|
82
|
+
toggleSessionYolo() {
|
|
83
|
+
this.sessionYolo = !this.sessionYolo;
|
|
84
|
+
return this.sessionYolo;
|
|
85
|
+
}
|
|
86
|
+
/** EXT-12 — current state of the runtime session-scoped yolo flag (see {@link toggleSessionYolo}). */
|
|
87
|
+
isSessionYolo() {
|
|
88
|
+
return this.sessionYolo;
|
|
89
|
+
}
|
|
28
90
|
/**
|
|
29
91
|
* Init is split into a separate method. This may create a number of connections,
|
|
30
92
|
* and we'd better have an instance by that moment, for the case things will go wrong,
|
|
@@ -32,6 +94,7 @@ export class GthAgentRunner {
|
|
|
32
94
|
*/
|
|
33
95
|
async init(command, configIn, checkpointSaver) {
|
|
34
96
|
this.config = configIn;
|
|
97
|
+
this.command = command;
|
|
35
98
|
// Initialize debug logging
|
|
36
99
|
initDebugLogging(configIn.debugLog ?? false);
|
|
37
100
|
debugLog(`Initializing GthAgentRunner with command: ${command || 'default'}`);
|
|
@@ -60,10 +123,11 @@ export class GthAgentRunner {
|
|
|
60
123
|
const stream = await this.agent.stream(messages, this.runConfig);
|
|
61
124
|
let result = '';
|
|
62
125
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
126
|
+
result = await this.drainTextStream(stream);
|
|
127
|
+
// A run may suspend on one or more tool-approval interrupts (run_shell_command).
|
|
128
|
+
// Resolve them in a loop: each resume can itself suspend again on the next gated
|
|
129
|
+
// tool call, so keep going until the graph completes with no pending interrupts.
|
|
130
|
+
result += await this.resolveToolInterrupts();
|
|
67
131
|
}
|
|
68
132
|
catch (streamError) {
|
|
69
133
|
// Handle streaming-specific errors
|
|
@@ -101,6 +165,189 @@ export class GthAgentRunner {
|
|
|
101
165
|
throw new Error(`Agent processing failed: ${enhancedMessage}`, error instanceof Error ? { cause: error } : undefined);
|
|
102
166
|
}
|
|
103
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Accumulate a text stream into a single string. Extracted so {@link processMessages} and
|
|
170
|
+
* the interrupt-resume loop ({@link resolveToolInterrupts}) drain streams identically.
|
|
171
|
+
*/
|
|
172
|
+
async drainTextStream(stream) {
|
|
173
|
+
let result = '';
|
|
174
|
+
for await (const chunk of stream) {
|
|
175
|
+
debugLogObject('Stream chunk', chunk);
|
|
176
|
+
result += chunk;
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* After a streamed run ends, resolve any tool-approval interrupts it suspended on. For each
|
|
182
|
+
* pending tool call the {@link toolApprovalCallback} is consulted (defaulting to REJECT when
|
|
183
|
+
* no handler is wired, so a non-interactive run never hangs or auto-approves); the collected
|
|
184
|
+
* decisions are then sent back via the agent's `streamResume` as a LangChain HITL resume
|
|
185
|
+
* (`{ decisions }`). Because a resumed run can suspend again on the next gated tool call, this
|
|
186
|
+
* loops until the graph completes with no pending interrupts. Returns the concatenated text
|
|
187
|
+
* streamed across all resume turns (empty when nothing was resumed).
|
|
188
|
+
*
|
|
189
|
+
* No-ops (returns '') when the agent does not support interrupts (`getPendingToolInterrupts`/
|
|
190
|
+
* `streamResume` absent), so the lean agent and non-HITL configs are unaffected.
|
|
191
|
+
*/
|
|
192
|
+
async resolveToolInterrupts() {
|
|
193
|
+
const agent = this.agent;
|
|
194
|
+
const runConfig = this.runConfig;
|
|
195
|
+
if (!agent || !runConfig)
|
|
196
|
+
return '';
|
|
197
|
+
if (!agent.getPendingToolInterrupts || !agent.streamResume)
|
|
198
|
+
return '';
|
|
199
|
+
let resumedText = '';
|
|
200
|
+
// Bound the loop defensively so a misbehaving graph that re-suspends forever cannot spin.
|
|
201
|
+
for (let guard = 0; guard < 100; guard++) {
|
|
202
|
+
const pending = await agent.getPendingToolInterrupts(runConfig);
|
|
203
|
+
if (pending.length === 0)
|
|
204
|
+
break;
|
|
205
|
+
const decisions = [];
|
|
206
|
+
for (const tool of pending) {
|
|
207
|
+
decisions.push(await this.decideToolApproval(tool));
|
|
208
|
+
}
|
|
209
|
+
const stream = await agent.streamResume({ decisions }, runConfig);
|
|
210
|
+
resumedText += await this.drainTextStream(stream);
|
|
211
|
+
}
|
|
212
|
+
return resumedText;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Decide a single pending tool call (EXT-9 Tier-2). For the opt-in `run_shell_command`,
|
|
216
|
+
* consult the scoped allow-list FIRST: if the command's classified prefix is already
|
|
217
|
+
* approved (session or persisted `always`) and survives the safe-bin anti-widening
|
|
218
|
+
* re-validation, auto-approve SILENTLY (no human prompt). Otherwise fall through to the
|
|
219
|
+
* human callback; when the human grants `session`/`always` scope, record the command's
|
|
220
|
+
* classified prefix into the matching store so future flag-variants stop re-prompting.
|
|
221
|
+
*
|
|
222
|
+
* When no human callback is wired (non-TTY exec run) and nothing is allow-listed, reject —
|
|
223
|
+
* never auto-approve. Non-shell tools (or any tool when the allow-list is disabled) skip the
|
|
224
|
+
* allow-list and go straight to the human callback / default-reject, preserving prior behaviour.
|
|
225
|
+
*
|
|
226
|
+
* Hardline catastrophic commands remain refused at exec time regardless of any approval here
|
|
227
|
+
* (defense in depth in `GthDevToolkit.executeCommand`), so an allow-listed `rm -rf /` still
|
|
228
|
+
* cannot run.
|
|
229
|
+
*/
|
|
230
|
+
async decideToolApproval(tool) {
|
|
231
|
+
const command = typeof tool.args?.command === 'string' ? tool.args.command : null;
|
|
232
|
+
const isShellCommand = tool.name === 'run_shell_command' && command !== null;
|
|
233
|
+
const allowlistApplies = isShellCommand && this.isShellAllowlistOn();
|
|
234
|
+
// EXT-12 — runtime session yolo (`/yolo`): when ON, auto-approve a gated shell command WITHOUT
|
|
235
|
+
// prompting, judging, or persisting. Scope `once` so nothing is written to the allow-list (the
|
|
236
|
+
// bypass is intentionally ephemeral and reversible). The hardline floor is NOT bypassed here —
|
|
237
|
+
// it is enforced at exec time in GthDevToolkit.executeCommand regardless of this decision, so a
|
|
238
|
+
// catastrophic command is still refused even under yolo.
|
|
239
|
+
if (isShellCommand && this.sessionYolo) {
|
|
240
|
+
return { type: 'approve', scope: 'once' };
|
|
241
|
+
}
|
|
242
|
+
// Auto-approve from the allow-list without prompting. The allow-list ALWAYS wins over the
|
|
243
|
+
// judge: a human-trusted prefix shouldn't pay for an LLM call on every variant.
|
|
244
|
+
if (allowlistApplies && this.isApprovedByAllowlist(command)) {
|
|
245
|
+
return { type: 'approve', scope: 'session' };
|
|
246
|
+
}
|
|
247
|
+
// EXT-10 — LLM-as-judge safety gate (default OFF). Runs BEFORE the human callback for a
|
|
248
|
+
// `run_shell_command` not already allow-listed: auto-approve clearly-safe (fatigue reducer),
|
|
249
|
+
// reject clearly-catastrophic (only when blockHigh), otherwise fall through to the human with
|
|
250
|
+
// the verdict attached. When disabled this is a no-op and behaviour is exactly EXT-9.
|
|
251
|
+
let safetyVerdict;
|
|
252
|
+
if (isShellCommand && command !== null && this.isShellJudgeOn()) {
|
|
253
|
+
const settings = getShellJudgeSettings(getEffectiveDevToolsConfig(this.config ?? undefined, this.command));
|
|
254
|
+
const verdict = await judgeShellCommand(command, this.config, {
|
|
255
|
+
home: env?.HOME,
|
|
256
|
+
});
|
|
257
|
+
const action = mapVerdictToAction(command, verdict, {
|
|
258
|
+
autoApproveLow: settings.autoApproveLow,
|
|
259
|
+
blockHigh: settings.blockHigh,
|
|
260
|
+
});
|
|
261
|
+
if (action === 'auto-approve') {
|
|
262
|
+
// Scope `once`: judge approvals are NEVER persisted to the allow-list.
|
|
263
|
+
return { type: 'approve', scope: 'once' };
|
|
264
|
+
}
|
|
265
|
+
if (action === 'reject') {
|
|
266
|
+
return {
|
|
267
|
+
type: 'reject',
|
|
268
|
+
message: `Safety judge blocked the command: ${verdict.reason}`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// Escalate: carry the verdict to the human approval surface.
|
|
272
|
+
safetyVerdict = verdict;
|
|
273
|
+
}
|
|
274
|
+
if (!this.toolApprovalCallback) {
|
|
275
|
+
// No interactive handler (e.g. non-TTY exec run): reject rather than auto-approve.
|
|
276
|
+
return {
|
|
277
|
+
type: 'reject',
|
|
278
|
+
message: 'Tool call rejected: no interactive approval handler available.',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// Surface the judge's verdict to the human prompt (if the judge escalated) without mutating
|
|
282
|
+
// the original interrupt object the caller holds.
|
|
283
|
+
const pending = safetyVerdict ? { ...tool, safetyVerdict } : tool;
|
|
284
|
+
const decision = await this.toolApprovalCallback(pending);
|
|
285
|
+
// Persist the human's scoped grant so future variants of the same operation skip the prompt.
|
|
286
|
+
if (decision.type === 'approve' && allowlistApplies && command) {
|
|
287
|
+
this.recordApproval(command, decision.scope ?? 'once');
|
|
288
|
+
}
|
|
289
|
+
return decision;
|
|
290
|
+
}
|
|
291
|
+
/** Whether the EXT-10 LLM-as-judge safety gate is enabled for the active command's config. */
|
|
292
|
+
isShellJudgeOn() {
|
|
293
|
+
if (!this.config)
|
|
294
|
+
return false;
|
|
295
|
+
const devTools = getEffectiveDevToolsConfig(this.config, this.command);
|
|
296
|
+
return isShellJudgeEnabled(devTools);
|
|
297
|
+
}
|
|
298
|
+
/** Whether the EXT-9 Tier-2 allow-list is enabled for the active command's devTools config. */
|
|
299
|
+
isShellAllowlistOn() {
|
|
300
|
+
const devTools = getEffectiveDevToolsConfig(this.config ?? undefined, this.command);
|
|
301
|
+
return isShellAllowlistEnabled(devTools);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Lazily load (once per instance) the persisted `always` allow-list, unless persistence is
|
|
305
|
+
* disabled by config. Returns null when persistence is off so `always` grants behave as
|
|
306
|
+
* `session` (in-memory only).
|
|
307
|
+
*/
|
|
308
|
+
getPersistedAllowlist() {
|
|
309
|
+
if (this.persistedAllowlistLoaded)
|
|
310
|
+
return this.persistedAllowlist;
|
|
311
|
+
this.persistedAllowlistLoaded = true;
|
|
312
|
+
const devTools = getEffectiveDevToolsConfig(this.config ?? undefined, this.command);
|
|
313
|
+
if (!isShellAllowlistPersisted(devTools)) {
|
|
314
|
+
this.persistedAllowlist = null;
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const filePath = getGslothConfigWritePath(SHELL_ALLOWLIST_FILE);
|
|
319
|
+
this.persistedAllowlist = new PersistedAllowlist(filePath);
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
// Path/IO failure → behave as no persisted store (still safe: just prompts more).
|
|
323
|
+
debugLogError('Loading persisted shell allow-list', e);
|
|
324
|
+
this.persistedAllowlist = null;
|
|
325
|
+
}
|
|
326
|
+
return this.persistedAllowlist;
|
|
327
|
+
}
|
|
328
|
+
/** Check the command against the session + persisted stores (with anti-widening re-validation). */
|
|
329
|
+
isApprovedByAllowlist(command) {
|
|
330
|
+
return matchesApproval(command, {
|
|
331
|
+
session: this.sessionAllowlist,
|
|
332
|
+
always: this.getPersistedAllowlist() ?? undefined,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Record a human-granted approval at the given scope. `once` persists nothing. `session`
|
|
337
|
+
* adds the classified prefix to the in-memory store. `always` additionally persists it (or
|
|
338
|
+
* falls back to session-only when persistence is disabled).
|
|
339
|
+
*/
|
|
340
|
+
recordApproval(command, scope) {
|
|
341
|
+
if (scope === 'once')
|
|
342
|
+
return;
|
|
343
|
+
const classification = classifyCommand(command, normalizeCommand);
|
|
344
|
+
if (!classification)
|
|
345
|
+
return; // unclassifiable (composition/redirection) → never remember.
|
|
346
|
+
this.sessionAllowlist.add(classification.prefix);
|
|
347
|
+
if (scope === 'always') {
|
|
348
|
+
this.getPersistedAllowlist()?.add(classification.prefix);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
104
351
|
/**
|
|
105
352
|
* Event-stream counterpart to {@link processMessages}: drives the agent's typed
|
|
106
353
|
* {@link AgentStreamEvent} path using the runner's own thread-bound `runConfig`, so a
|
|
@@ -111,6 +358,16 @@ export class GthAgentRunner {
|
|
|
111
358
|
* underlying `streamWithEvents` ends cleanly on abort or `interrupt()`. The string
|
|
112
359
|
* path's empty-stream retry/`invoke` fallback is intentionally NOT duplicated here — the
|
|
113
360
|
* TUI renders the live event stream directly; revisit if empty-stream retries are needed.
|
|
361
|
+
*
|
|
362
|
+
* Tool-approval round-trip (EXT-11): after the stream ends, a gated `run_shell_command`
|
|
363
|
+
* leaves the graph suspended on a `humanInTheLoopMiddleware` interrupt rather than
|
|
364
|
+
* completing. This is the event-stream counterpart to the readline path's
|
|
365
|
+
* {@link resolveToolInterrupts}: it drains any pending interrupts through
|
|
366
|
+
* {@link decideToolApproval} (allow-list → judge → bridged human prompt), resumes via
|
|
367
|
+
* `streamWithEventsResume({ decisions })`, and loops until the graph completes with no
|
|
368
|
+
* pending interrupts — so the executed command's output renders into the TUI. Without
|
|
369
|
+
* this the TUI silently finalized an empty turn (approval gate was dead code on the
|
|
370
|
+
* event-stream path).
|
|
114
371
|
*/
|
|
115
372
|
async *processMessagesWithEvents(messages, signal) {
|
|
116
373
|
if (!this.agent || !this.config || !this.runConfig) {
|
|
@@ -119,6 +376,43 @@ export class GthAgentRunner {
|
|
|
119
376
|
debugLog('Processing messages (event stream)...');
|
|
120
377
|
debugLogObject('Input Messages', messages);
|
|
121
378
|
yield* this.agent.streamWithEvents(messages, this.runConfig, signal);
|
|
379
|
+
yield* this.resolveToolInterruptsWithEvents(signal);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Event-stream counterpart to {@link resolveToolInterrupts}: after a streamed run ends,
|
|
383
|
+
* resolve any tool-approval interrupts it suspended on, yielding the resumed run's typed
|
|
384
|
+
* {@link AgentStreamEvent}s so the renderer (the Ink TUI) shows the executed command's
|
|
385
|
+
* output. Each pending tool call is consulted via {@link decideToolApproval} — the SAME
|
|
386
|
+
* three-layer gate the readline path uses (allow-list auto-approve → EXT-10 judge →
|
|
387
|
+
* bridged human callback, defaulting to REJECT when no handler is wired) — and the
|
|
388
|
+
* collected decisions are sent back via `streamWithEventsResume` as a LangChain HITL
|
|
389
|
+
* resume (`{ decisions }`). Because a resumed run can suspend again on the next gated
|
|
390
|
+
* tool call, this loops until the graph completes with no pending interrupts.
|
|
391
|
+
*
|
|
392
|
+
* No-ops (yields nothing) when the agent does not support interrupts
|
|
393
|
+
* (`getPendingToolInterrupts`/`streamWithEventsResume` absent), so the lean agent and
|
|
394
|
+
* non-HITL configs are unaffected. Aborts (`signal`) propagate through the resumed stream.
|
|
395
|
+
*/
|
|
396
|
+
async *resolveToolInterruptsWithEvents(signal) {
|
|
397
|
+
const agent = this.agent;
|
|
398
|
+
const runConfig = this.runConfig;
|
|
399
|
+
if (!agent || !runConfig)
|
|
400
|
+
return;
|
|
401
|
+
if (!agent.getPendingToolInterrupts || !agent.streamWithEventsResume)
|
|
402
|
+
return;
|
|
403
|
+
// Bound the loop defensively so a misbehaving graph that re-suspends forever cannot spin.
|
|
404
|
+
for (let guard = 0; guard < 100; guard++) {
|
|
405
|
+
if (signal?.aborted)
|
|
406
|
+
return;
|
|
407
|
+
const pending = await agent.getPendingToolInterrupts(runConfig);
|
|
408
|
+
if (pending.length === 0)
|
|
409
|
+
break;
|
|
410
|
+
const decisions = [];
|
|
411
|
+
for (const tool of pending) {
|
|
412
|
+
decisions.push(await this.decideToolApproval(tool));
|
|
413
|
+
}
|
|
414
|
+
yield* agent.streamWithEventsResume({ decisions }, runConfig, [], signal);
|
|
415
|
+
}
|
|
122
416
|
}
|
|
123
417
|
// noinspection JSUnusedGlobalSymbols
|
|
124
418
|
getAgent() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GthAgentRunner.js","sourceRoot":"","sources":["../../src/core/GthAgentRunner.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GthAgentRunner.js","sourceRoot":"","sources":["../../src/core/GthAgentRunner.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,0BAA0B,EAC1B,qBAAqB,EACrB,uBAAuB,EACvB,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AAcxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,eAAe,GAEhB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EACL,iBAAiB,EACjB,kBAAkB,GAEnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAC;AAChD,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,gCAAgC,EAAE,MAAM,6BAA6B,CAAC;AAE/E,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EACL,gBAAgB,EAChB,QAAQ,EACR,aAAa,EACb,cAAc,GACf,MAAM,0BAA0B,CAAC;AAElC;;;GAGG;AACH,MAAM,OAAO,cAAc;IACjB,YAAY,CAAuB;IACnC,SAAS,CAA6B;IACtC,KAAK,GAA6B,IAAI,CAAC;IACvC,MAAM,GAAqB,IAAI,CAAC;IAChC,SAAS,GAA0B,IAAI,CAAC;IACxC,YAAY,CAAkB;IACtC;;;;;OAKG;IACK,oBAAoB,GAAgC,IAAI,CAAC;IAEjE,2FAA2F;IACnF,OAAO,GAA2B,SAAS,CAAC;IAEpD;;;;;;;;OAQG;IACK,WAAW,GAAG,KAAK,CAAC;IAE5B;;;;OAIG;IACc,gBAAgB,GAAG,IAAI,cAAc,EAAE,CAAC;IAEzD;;;;OAIG;IACK,kBAAkB,GAA8B,IAAI,CAAC;IACrD,wBAAwB,GAAG,KAAK,CAAC;IAEzC;;;;;OAKG;IACH,YACE,YAAkC,EAClC,SAA0B,EAC1B,YAA8B;QAE9B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,YAAY;YACf,YAAY,IAAI,CAAC,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,CAAC,IAAI,iBAAiB,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IAChG,CAAC;IAED;;;;OAIG;IACI,uBAAuB,CAAC,QAAqC;QAClE,IAAI,CAAC,oBAAoB,GAAG,QAAQ,CAAC;IACvC,CAAC;IAED;;;;;OAKG;IACI,iBAAiB;QACtB,IAAI,CAAC,WAAW,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC;QACrC,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,sGAAsG;IAC/F,aAAa;QAClB,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAI,CACR,OAA+B,EAC/B,QAAmB,EACnB,eAAiD;QAEjD,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QAEvB,2BAA2B;QAC3B,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,IAAI,KAAK,CAAC,CAAC;QAC7C,QAAQ,CAAC,6CAA6C,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC;QAE9E,IAAI,CAAC,SAAS,GAAG,oBAAoB,EAAE,CAAC;QAExC,cAAc,CAAC,iBAAiB,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAElD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAElE,uBAAuB;QACvB,QAAQ,CAAC,uBAAuB,CAAC,CAAC;QAClC,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;QAE1D,QAAQ,CAAC,+BAA+B,CAAC,CAAC;IAC5C,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,eAAe,CAAC,QAAmB;QACvC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACrE,CAAC;QAED,QAAQ,CAAC,wBAAwB,CAAC,CAAC;QACnC,cAAc,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC;QAE3C,IAAI,CAAC;YACH,2DAA2D;YAC3D,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;gBAC7B,gBAAgB;gBAChB,QAAQ,CAAC,sBAAsB,CAAC,CAAC;gBACjC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;gBACjE,IAAI,MAAM,GAAG,EAAE,CAAC;gBAChB,IAAI,CAAC;oBACH,MAAM,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;oBAC5C,iFAAiF;oBACjF,iFAAiF;oBACjF,iFAAiF;oBACjF,MAAM,IAAI,MAAM,IAAI,CAAC,qBAAqB,EAAE,CAAC;gBAC/C,CAAC;gBAAC,OAAO,WAAW,EAAE,CAAC;oBACrB,mCAAmC;oBACnC,aAAa,CAAC,mBAAmB,EAAE,WAAW,CAAC,CAAC;oBAChD,MAAM,IAAI,KAAK,CACb,6BAA6B,WAAW,YAAY,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CACxG,CAAC;gBACJ,CAAC;gBACD,QAAQ,CAAC,4CAA4C,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;gBACtE,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC/B,QAAQ,CAAC,0EAA0E,CAAC,CAAC;oBACrF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;oBACnE,QAAQ,CAAC,wCAAwC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;oBACpE,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBACjC,MAAM,IAAI,KAAK,CACb,oGAAoG,CACrG,CAAC;oBACJ,CAAC;oBACD,OAAO,QAAQ,CAAC;gBAClB,CAAC;gBACD,OAAO,MAAM,CAAC;YAChB,CAAC;iBAAM,CAAC;gBACN,oBAAoB;gBACpB,QAAQ,CAAC,0BAA0B,CAAC,CAAC;gBACrC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;gBACjE,QAAQ,CAAC,+BAA+B,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;gBACzD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC/B,MAAM,IAAI,KAAK,CACb,+EAA+E,CAChF,CAAC;gBACJ,CAAC;gBACD,OAAO,MAAM,CAAC;YAChB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,iCAAiC;YACjC,aAAa,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;YACzC,MAAM,eAAe,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC/E,MAAM,eAAe,GAAG,gCAAgC,CAAC,eAAe,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAC5F,MAAM,IAAI,KAAK,CACb,4BAA4B,eAAe,EAAE,EAC7C,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CACtD,CAAC;QACJ,CAAC;IACH,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,eAAe,CAAC,MAA6B;QACzD,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACjC,cAAc,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;YACtC,MAAM,IAAI,KAAK,CAAC;QAClB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;;;;;;OAWG;IACK,KAAK,CAAC,qBAAqB;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QACjC,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,wBAAwB,IAAI,CAAC,KAAK,CAAC,YAAY;YAAE,OAAO,EAAE,CAAC;QAEtE,IAAI,WAAW,GAAG,EAAE,CAAC;QACrB,0FAA0F;QAC1F,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,wBAAwB,CAAC,SAAS,CAAC,CAAC;YAChE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,MAAM;YAEhC,MAAM,SAAS,GAA2B,EAAE,CAAC;YAC7C,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC3B,SAAS,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;YACtD,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,CAAC,CAAC;YAClE,WAAW,IAAI,MAAM,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,WAAW,CAAC;IACrB,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACK,KAAK,CAAC,kBAAkB,CAAC,IAA0B;QACzD,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,IAAI,EAAE,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAI,CAAC,IAAI,CAAC,OAAkB,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9F,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,KAAK,mBAAmB,IAAI,OAAO,KAAK,IAAI,CAAC;QAC7E,MAAM,gBAAgB,GAAG,cAAc,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAErE,+FAA+F;QAC/F,+FAA+F;QAC/F,+FAA+F;QAC/F,gGAAgG;QAChG,yDAAyD;QACzD,IAAI,cAAc,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACvC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAC5C,CAAC;QAED,0FAA0F;QAC1F,gFAAgF;QAChF,IAAI,gBAAgB,IAAI,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5D,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;QAC/C,CAAC;QAED,wFAAwF;QACxF,6FAA6F;QAC7F,8FAA8F;QAC9F,sFAAsF;QACtF,IAAI,aAA6C,CAAC;QAClD,IAAI,cAAc,IAAI,OAAO,KAAK,IAAI,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;YAChE,MAAM,QAAQ,GAAG,qBAAqB,CACpC,0BAA0B,CAAC,IAAI,CAAC,MAAM,IAAI,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,CACnE,CAAC;YACF,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,MAAmB,EAAE;gBACzE,IAAI,EAAE,GAAG,EAAE,IAAI;aAChB,CAAC,CAAC;YACH,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE;gBAClD,cAAc,EAAE,QAAQ,CAAC,cAAc;gBACvC,SAAS,EAAE,QAAQ,CAAC,SAAS;aAC9B,CAAC,CAAC;YACH,IAAI,MAAM,KAAK,cAAc,EAAE,CAAC;gBAC9B,uEAAuE;gBACvE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;YAC5C,CAAC;YACD,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACxB,OAAO;oBACL,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,qCAAqC,OAAO,CAAC,MAAM,EAAE;iBAC/D,CAAC;YACJ,CAAC;YACD,6DAA6D;YAC7D,aAAa,GAAG,OAAO,CAAC;QAC1B,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC/B,mFAAmF;YACnF,OAAO;gBACL,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,gEAAgE;aAC1E,CAAC;QACJ,CAAC;QAED,4FAA4F;QAC5F,kDAAkD;QAClD,MAAM,OAAO,GAAyB,aAAa,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACxF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;QAE1D,6FAA6F;QAC7F,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS,IAAI,gBAAgB,IAAI,OAAO,EAAE,CAAC;YAC/D,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,8FAA8F;IACtF,cAAc;QACpB,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC/B,MAAM,QAAQ,GAAG,0BAA0B,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACvE,OAAO,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,+FAA+F;IACvF,kBAAkB;QACxB,MAAM,QAAQ,GAAG,0BAA0B,CAAC,IAAI,CAAC,MAAM,IAAI,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACpF,OAAO,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAC3C,CAAC;IAED;;;;OAIG;IACK,qBAAqB;QAC3B,IAAI,IAAI,CAAC,wBAAwB;YAAE,OAAO,IAAI,CAAC,kBAAkB,CAAC;QAClE,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAC;QACrC,MAAM,QAAQ,GAAG,0BAA0B,CAAC,IAAI,CAAC,MAAM,IAAI,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACpF,IAAI,CAAC,yBAAyB,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzC,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;YAC/B,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,wBAAwB,CAAC,oBAAoB,CAAC,CAAC;YAChE,IAAI,CAAC,kBAAkB,GAAG,IAAI,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,kFAAkF;YAClF,aAAa,CAAC,oCAAoC,EAAE,CAAC,CAAC,CAAC;YACvD,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;QACjC,CAAC;QACD,OAAO,IAAI,CAAC,kBAAkB,CAAC;IACjC,CAAC;IAED,mGAAmG;IAC3F,qBAAqB,CAAC,OAAe;QAC3C,OAAO,eAAe,CAAC,OAAO,EAAE;YAC9B,OAAO,EAAE,IAAI,CAAC,gBAAgB;YAC9B,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,IAAI,SAAS;SAClD,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,cAAc,CAAC,OAAe,EAAE,KAAoB;QAC1D,IAAI,KAAK,KAAK,MAAM;YAAE,OAAO;QAC7B,MAAM,cAAc,GAAG,eAAe,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;QAClE,IAAI,CAAC,cAAc;YAAE,OAAO,CAAC,6DAA6D;QAC1F,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvB,IAAI,CAAC,qBAAqB,EAAE,EAAE,GAAG,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAK,CAAC,CAAC,yBAAyB,CAC9B,QAAmB,EACnB,MAAoB;QAEpB,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACrE,CAAC;QACD,QAAQ,CAAC,uCAAuC,CAAC,CAAC;QAClD,cAAc,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC;QAC3C,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACrE,KAAK,CAAC,CAAC,IAAI,CAAC,+BAA+B,CAAC,MAAM,CAAC,CAAC;IACtD,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACK,KAAK,CAAC,CAAC,+BAA+B,CAC5C,MAAoB;QAEpB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QACjC,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS;YAAE,OAAO;QACjC,IAAI,CAAC,KAAK,CAAC,wBAAwB,IAAI,CAAC,KAAK,CAAC,sBAAsB;YAAE,OAAO;QAE7E,0FAA0F;QAC1F,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC;YACzC,IAAI,MAAM,EAAE,OAAO;gBAAE,OAAO;YAC5B,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,wBAAwB,CAAC,SAAS,CAAC,CAAC;YAChE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,MAAM;YAEhC,MAAM,SAAS,GAA2B,EAAE,CAAC;YAC7C,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC3B,SAAS,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;YACtD,CAAC;YAED,KAAK,CAAC,CAAC,KAAK,CAAC,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;IAED,qCAAqC;IAC9B,QAAQ;QACb,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED;;;;;;;;OAQG;IACI,WAAW;QAChB,IAAI,CAAC,SAAS,GAAG,oBAAoB,EAAE,CAAC;QACxC,cAAc,CAAC,uBAAuB,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,OAAO;QACX,QAAQ,CAAC,+BAA+B,CAAC,CAAC;QAC1C,IAAI,IAAI,CAAC,KAAK,IAAI,SAAS,IAAI,IAAI,CAAC,KAAK,IAAI,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;YACtF,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,QAAQ,CAAC,iCAAiC,CAAC,CAAC;IAC9C,CAAC;CACF"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ToolApprovalScope } from '#src/core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Approval persistence scopes, widest-lived last. Alias of the canonical
|
|
4
|
+
* {@link ToolApprovalScope} so the allow-list engine and the decision vocabulary stay in sync.
|
|
5
|
+
*/
|
|
6
|
+
export type ApprovalScope = ToolApprovalScope;
|
|
7
|
+
/**
|
|
8
|
+
* Decide whether a candidate argv contains a flag that *widens* the approved operation.
|
|
9
|
+
*
|
|
10
|
+
* Rule implemented (documented for the coordinator):
|
|
11
|
+
* - Re-derive the meaningful prefix from the candidate's actual argv.
|
|
12
|
+
* - Inspect every token of the candidate that is a flag (starts with `-`).
|
|
13
|
+
* - If any flag token — normalized by stripping a trailing `=value` and lowercasing —
|
|
14
|
+
* is in {@link WIDENING_FLAGS}, the command is considered a widening of the approved
|
|
15
|
+
* operation and the match is REFUSED (returns true).
|
|
16
|
+
*
|
|
17
|
+
* This is purposely a deny-list of operation-changing flags rather than an allow-list of
|
|
18
|
+
* benign flags: benign flag variants (`-b`, `--oneline`, `-la`) are exactly what we WANT
|
|
19
|
+
* to keep auto-approving, while the handful of "run-an-arbitrary-program / redirect-the-
|
|
20
|
+
* transport" flags are what an injected approval must never silently enable.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hasWideningFlag(argv: string[]): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* A holder of approved prefixes with set semantics. Used for both the in-memory session
|
|
25
|
+
* store and the loaded persisted store. Pure data + membership; persistence is layered on
|
|
26
|
+
* top by {@link PersistedAllowlist}.
|
|
27
|
+
*/
|
|
28
|
+
export declare class AllowlistStore {
|
|
29
|
+
private readonly prefixes;
|
|
30
|
+
constructor(initial?: Iterable<string>);
|
|
31
|
+
has(prefix: string): boolean;
|
|
32
|
+
add(prefix: string): void;
|
|
33
|
+
list(): string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The persisted (`always`) allow-list, backed by a JSON file. The path is injected (the
|
|
37
|
+
* runner resolves it via fileUtils → `.gsloth/.gsloth-settings/shell-allowlist.json`) so
|
|
38
|
+
* tests can point it at a temp dir. Loads lazily/defensively: a missing or malformed file
|
|
39
|
+
* yields an empty store rather than throwing (fail-open on READ is safe — an empty
|
|
40
|
+
* allow-list just means "prompt"; it never auto-approves anything).
|
|
41
|
+
*/
|
|
42
|
+
export declare class PersistedAllowlist {
|
|
43
|
+
private readonly store;
|
|
44
|
+
private readonly filePath;
|
|
45
|
+
constructor(filePath: string);
|
|
46
|
+
private static load;
|
|
47
|
+
has(prefix: string): boolean;
|
|
48
|
+
list(): string[];
|
|
49
|
+
/** Add a prefix and persist the whole set to disk. */
|
|
50
|
+
add(prefix: string): void;
|
|
51
|
+
private persist;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read-only view over the stores consulted for an auto-approval check. The runner passes
|
|
55
|
+
* its per-instance session store and (optionally) the persisted store.
|
|
56
|
+
*/
|
|
57
|
+
export interface ApprovalStores {
|
|
58
|
+
session: Pick<AllowlistStore, 'has'>;
|
|
59
|
+
always?: Pick<PersistedAllowlist, 'has'>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Decide whether `command` is already approved by the given stores — the gate the runner
|
|
63
|
+
* consults BEFORE prompting the human.
|
|
64
|
+
*
|
|
65
|
+
* Returns true ONLY when ALL of the following hold:
|
|
66
|
+
* 1. {@link classifyCommand} returns a non-null classification (so composition /
|
|
67
|
+
* substitution / redirection commands can never match — anti-injection layer 1);
|
|
68
|
+
* 2. the classified prefix is present in the session OR persisted (`always`) store; AND
|
|
69
|
+
* 3. the safe-bin re-validation passes: the candidate's actual argv contains no
|
|
70
|
+
* operation-widening flag (anti-injection layer 2 — see {@link hasWideningFlag}).
|
|
71
|
+
*
|
|
72
|
+
* Anything else (unclassifiable, unknown prefix, or a widening flag) → false → the human
|
|
73
|
+
* is prompted. Fail-closed by construction.
|
|
74
|
+
*/
|
|
75
|
+
export declare function matchesApproval(command: string, stores: ApprovalStores): boolean;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/shell/allowlist
|
|
3
|
+
*
|
|
4
|
+
* EXT-9 Tier-2: the persisted + session-scoped allow-list engine for the opt-in
|
|
5
|
+
* `run_shell_command` tool. Once a human approves a command at `session` or `always`
|
|
6
|
+
* scope, future commands with the same classified prefix ({@link classifyCommand}) are
|
|
7
|
+
* auto-approved without re-prompting — the ergonomics improvement mature agents
|
|
8
|
+
* (opencode/openclaw/hermes) ship.
|
|
9
|
+
*
|
|
10
|
+
* SECURITY — a naive "remember the prefix" allow-list is an injection vector. Two layers
|
|
11
|
+
* guard it:
|
|
12
|
+
* 1. **Classification fail-closed** (arity.ts): any command with shell composition,
|
|
13
|
+
* substitution, or redirection returns `null` and can NEVER match, so
|
|
14
|
+
* `git checkout x; rm -rf /` does not ride an approved `git checkout *`.
|
|
15
|
+
* 2. **Safe-bin anti-widening re-validation** (here, after opencode/openclaw): a stored
|
|
16
|
+
* approval is matched ONLY if the candidate command's first non-flag operand region
|
|
17
|
+
* does not introduce a flag that *widens or redirects* the approved operation. The
|
|
18
|
+
* exact rule is documented on {@link matchesApproval} / {@link hasWideningFlag}.
|
|
19
|
+
*
|
|
20
|
+
* No module-global mutable state: the session store is a per-instance class so concurrent
|
|
21
|
+
* sessions (ACP / AG-UI multi-session) cannot stomp each other. The persisted (`always`)
|
|
22
|
+
* store is a small JSON file the runner loads once per instance.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { classifyCommand, tokenize } from '#src/core/shell/arity.js';
|
|
26
|
+
import { normalizeCommand } from '#src/core/shell/normalize.js';
|
|
27
|
+
const PERSISTED_VERSION = 1;
|
|
28
|
+
/**
|
|
29
|
+
* Flags that, if present in a candidate command but not implied by the approved prefix,
|
|
30
|
+
* could change the *operation* the human approved (point it at a different transport,
|
|
31
|
+
* exec a hook, follow an attacker-controlled URL). Approving `git clone` must not
|
|
32
|
+
* green-light `git clone --upload-pack=evil`. This list is intentionally conservative:
|
|
33
|
+
* it is the openclaw "safe-bin" idea reduced to a deny-set of operation-changing flags
|
|
34
|
+
* that commonly appear in shell-injection / supply-chain abuse.
|
|
35
|
+
*
|
|
36
|
+
* Matched against the flag token with any `=value` stripped, case-insensitively.
|
|
37
|
+
*/
|
|
38
|
+
const WIDENING_FLAGS = new Set([
|
|
39
|
+
// git: remote command / hook overrides.
|
|
40
|
+
'--upload-pack',
|
|
41
|
+
'--receive-pack',
|
|
42
|
+
'--exec',
|
|
43
|
+
'-u', // git clone -u <upload-pack>
|
|
44
|
+
'--config', // git -c is the short form; --config can inject core.sshCommand etc.
|
|
45
|
+
'-c', // git -c core.sshCommand='...' — arbitrary command execution
|
|
46
|
+
// generic "run this program" escape hatches across tools.
|
|
47
|
+
'--exec-path',
|
|
48
|
+
'-exec', // find -exec <cmd>
|
|
49
|
+
'--use-askpass',
|
|
50
|
+
'--ssh-command',
|
|
51
|
+
// package managers: lifecycle-script / arbitrary-script toggles.
|
|
52
|
+
'--unsafe-perm',
|
|
53
|
+
'--ignore-scripts=false',
|
|
54
|
+
'--allow-scripts',
|
|
55
|
+
// curl/wget style follow/exec (in case a bare binary is approved).
|
|
56
|
+
'-o', // write to arbitrary path
|
|
57
|
+
'--output',
|
|
58
|
+
'-T', // upload
|
|
59
|
+
'--upload-file',
|
|
60
|
+
]);
|
|
61
|
+
/**
|
|
62
|
+
* Decide whether a candidate argv contains a flag that *widens* the approved operation.
|
|
63
|
+
*
|
|
64
|
+
* Rule implemented (documented for the coordinator):
|
|
65
|
+
* - Re-derive the meaningful prefix from the candidate's actual argv.
|
|
66
|
+
* - Inspect every token of the candidate that is a flag (starts with `-`).
|
|
67
|
+
* - If any flag token — normalized by stripping a trailing `=value` and lowercasing —
|
|
68
|
+
* is in {@link WIDENING_FLAGS}, the command is considered a widening of the approved
|
|
69
|
+
* operation and the match is REFUSED (returns true).
|
|
70
|
+
*
|
|
71
|
+
* This is purposely a deny-list of operation-changing flags rather than an allow-list of
|
|
72
|
+
* benign flags: benign flag variants (`-b`, `--oneline`, `-la`) are exactly what we WANT
|
|
73
|
+
* to keep auto-approving, while the handful of "run-an-arbitrary-program / redirect-the-
|
|
74
|
+
* transport" flags are what an injected approval must never silently enable.
|
|
75
|
+
*/
|
|
76
|
+
export function hasWideningFlag(argv) {
|
|
77
|
+
for (const tok of argv) {
|
|
78
|
+
if (!tok.startsWith('-'))
|
|
79
|
+
continue;
|
|
80
|
+
const bare = tok.split('=', 1)[0].toLowerCase();
|
|
81
|
+
if (WIDENING_FLAGS.has(bare))
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A holder of approved prefixes with set semantics. Used for both the in-memory session
|
|
88
|
+
* store and the loaded persisted store. Pure data + membership; persistence is layered on
|
|
89
|
+
* top by {@link PersistedAllowlist}.
|
|
90
|
+
*/
|
|
91
|
+
export class AllowlistStore {
|
|
92
|
+
prefixes;
|
|
93
|
+
constructor(initial = []) {
|
|
94
|
+
this.prefixes = new Set(initial);
|
|
95
|
+
}
|
|
96
|
+
has(prefix) {
|
|
97
|
+
return this.prefixes.has(prefix);
|
|
98
|
+
}
|
|
99
|
+
add(prefix) {
|
|
100
|
+
this.prefixes.add(prefix);
|
|
101
|
+
}
|
|
102
|
+
list() {
|
|
103
|
+
return [...this.prefixes];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The persisted (`always`) allow-list, backed by a JSON file. The path is injected (the
|
|
108
|
+
* runner resolves it via fileUtils → `.gsloth/.gsloth-settings/shell-allowlist.json`) so
|
|
109
|
+
* tests can point it at a temp dir. Loads lazily/defensively: a missing or malformed file
|
|
110
|
+
* yields an empty store rather than throwing (fail-open on READ is safe — an empty
|
|
111
|
+
* allow-list just means "prompt"; it never auto-approves anything).
|
|
112
|
+
*/
|
|
113
|
+
export class PersistedAllowlist {
|
|
114
|
+
store;
|
|
115
|
+
filePath;
|
|
116
|
+
constructor(filePath) {
|
|
117
|
+
this.filePath = filePath;
|
|
118
|
+
this.store = new AllowlistStore(PersistedAllowlist.load(filePath));
|
|
119
|
+
}
|
|
120
|
+
static load(filePath) {
|
|
121
|
+
try {
|
|
122
|
+
if (!existsSync(filePath))
|
|
123
|
+
return [];
|
|
124
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
125
|
+
const parsed = JSON.parse(raw);
|
|
126
|
+
if (!parsed || !Array.isArray(parsed.prefixes))
|
|
127
|
+
return [];
|
|
128
|
+
return parsed.prefixes.filter((p) => typeof p === 'string');
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Corrupt/unreadable file → behave as empty (fail-closed on auto-approval).
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
has(prefix) {
|
|
136
|
+
return this.store.has(prefix);
|
|
137
|
+
}
|
|
138
|
+
list() {
|
|
139
|
+
return this.store.list();
|
|
140
|
+
}
|
|
141
|
+
/** Add a prefix and persist the whole set to disk. */
|
|
142
|
+
add(prefix) {
|
|
143
|
+
if (this.store.has(prefix))
|
|
144
|
+
return;
|
|
145
|
+
this.store.add(prefix);
|
|
146
|
+
this.persist();
|
|
147
|
+
}
|
|
148
|
+
persist() {
|
|
149
|
+
const file = {
|
|
150
|
+
version: PERSISTED_VERSION,
|
|
151
|
+
prefixes: this.store.list().sort(),
|
|
152
|
+
};
|
|
153
|
+
writeFileSync(this.filePath, JSON.stringify(file, null, 2) + '\n', 'utf8');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Decide whether `command` is already approved by the given stores — the gate the runner
|
|
158
|
+
* consults BEFORE prompting the human.
|
|
159
|
+
*
|
|
160
|
+
* Returns true ONLY when ALL of the following hold:
|
|
161
|
+
* 1. {@link classifyCommand} returns a non-null classification (so composition /
|
|
162
|
+
* substitution / redirection commands can never match — anti-injection layer 1);
|
|
163
|
+
* 2. the classified prefix is present in the session OR persisted (`always`) store; AND
|
|
164
|
+
* 3. the safe-bin re-validation passes: the candidate's actual argv contains no
|
|
165
|
+
* operation-widening flag (anti-injection layer 2 — see {@link hasWideningFlag}).
|
|
166
|
+
*
|
|
167
|
+
* Anything else (unclassifiable, unknown prefix, or a widening flag) → false → the human
|
|
168
|
+
* is prompted. Fail-closed by construction.
|
|
169
|
+
*/
|
|
170
|
+
export function matchesApproval(command, stores) {
|
|
171
|
+
const classification = classifyCommand(command, normalizeCommand);
|
|
172
|
+
if (!classification)
|
|
173
|
+
return false; // layer 1: composition/redirection/substitution.
|
|
174
|
+
const approved = stores.session.has(classification.prefix) ||
|
|
175
|
+
(stores.always?.has(classification.prefix) ?? false);
|
|
176
|
+
if (!approved)
|
|
177
|
+
return false;
|
|
178
|
+
// Layer 2: re-derive argv from the normalized command and refuse if an operation-
|
|
179
|
+
// widening flag is present that the human's prefix-level approval never implied.
|
|
180
|
+
const argv = tokenize(normalizeCommand(command));
|
|
181
|
+
if (!argv)
|
|
182
|
+
return false; // unbalanced quoting → fail-closed.
|
|
183
|
+
if (hasWideningFlag(argv))
|
|
184
|
+
return false;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=allowlist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowlist.js","sourceRoot":"","sources":["../../../src/core/shell/allowlist.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAgBhE,MAAM,iBAAiB,GAAG,CAAU,CAAC;AAErC;;;;;;;;;GASG;AACH,MAAM,cAAc,GAAwB,IAAI,GAAG,CAAC;IAClD,wCAAwC;IACxC,eAAe;IACf,gBAAgB;IAChB,QAAQ;IACR,IAAI,EAAE,6BAA6B;IACnC,UAAU,EAAE,qEAAqE;IACjF,IAAI,EAAE,6DAA6D;IACnE,0DAA0D;IAC1D,aAAa;IACb,OAAO,EAAE,mBAAmB;IAC5B,eAAe;IACf,eAAe;IACf,iEAAiE;IACjE,eAAe;IACf,wBAAwB;IACxB,iBAAiB;IACjB,mEAAmE;IACnE,IAAI,EAAE,0BAA0B;IAChC,UAAU;IACV,IAAI,EAAE,SAAS;IACf,eAAe;CAChB,CAAC,CAAC;AAEH;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,IAAc;IAC5C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACnC,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAChD,IAAI,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IAC5C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,cAAc;IACR,QAAQ,CAAc;IAEvC,YAAY,UAA4B,EAAE;QACxC,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED,GAAG,CAAC,MAAc;QAChB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IAED,GAAG,CAAC,MAAc;QAChB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,kBAAkB;IACZ,KAAK,CAAiB;IACtB,QAAQ,CAAS;IAElC,YAAY,QAAgB;QAC1B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,IAAI,cAAc,CAAC,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrE,CAAC;IAEO,MAAM,CAAC,IAAI,CAAC,QAAgB;QAClC,IAAI,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;gBAAE,OAAO,EAAE,CAAC;YACrC,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoC,CAAC;YAClE,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;gBAAE,OAAO,EAAE,CAAC;YAC1D,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;QAC3E,CAAC;QAAC,MAAM,CAAC;YACP,4EAA4E;YAC5E,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED,GAAG,CAAC,MAAc;QAChB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED,sDAAsD;IACtD,GAAG,CAAC,MAAc;QAChB,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,OAAO;QACnC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvB,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAEO,OAAO;QACb,MAAM,IAAI,GAA2B;YACnC,OAAO,EAAE,iBAAiB;YAC1B,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE;SACnC,CAAC;QACF,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7E,CAAC;CACF;AAWD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,MAAsB;IACrE,MAAM,cAAc,GAAG,eAAe,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;IAClE,IAAI,CAAC,cAAc;QAAE,OAAO,KAAK,CAAC,CAAC,iDAAiD;IAEpF,MAAM,QAAQ,GACZ,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,MAAM,CAAC;QACzC,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC;IACvD,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE5B,kFAAkF;IAClF,iFAAiF;IACjF,MAAM,IAAI,GAAG,QAAQ,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC;IACjD,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC,CAAC,oCAAoC;IAC7D,IAAI,eAAe,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAExC,OAAO,IAAI,CAAC;AACd,CAAC"}
|