@gaunt-sloth/core 2.0.0-alpha.24 → 2.0.0-alpha.26
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/README.md +1 -1
- package/dist/config/colour.d.ts +38 -0
- package/dist/config/colour.js +36 -0
- package/dist/config/colour.js.map +1 -0
- package/dist/config/defaults.d.ts +1 -0
- package/dist/config/defaults.js +1 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/loader.d.ts +13 -6
- package/dist/config/loader.js +171 -28
- package/dist/config/loader.js.map +1 -1
- package/dist/config/mouse.d.ts +50 -0
- package/dist/config/mouse.js +44 -0
- package/dist/config/mouse.js.map +1 -0
- package/dist/config/schema.d.ts +2372 -114
- package/dist/config/schema.js +845 -16
- package/dist/config/schema.js.map +1 -1
- package/dist/config/shell-policy.d.ts +466 -116
- package/dist/config/shell-policy.js +273 -62
- package/dist/config/shell-policy.js.map +1 -1
- package/dist/config/tool-descriptions.d.ts +165 -0
- package/dist/config/tool-descriptions.js +229 -0
- package/dist/config/tool-descriptions.js.map +1 -0
- package/dist/config/types.d.ts +36 -1
- package/dist/config/types.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +59 -1
- package/dist/core/GthAbstractAgent.js +72 -0
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +341 -58
- package/dist/core/GthAgentRunner.js +817 -149
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.d.ts +16 -0
- package/dist/core/GthLangChainAgent.js +72 -2
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/approvals/annotations.d.ts +122 -0
- package/dist/core/approvals/annotations.js +137 -0
- package/dist/core/approvals/annotations.js.map +1 -0
- package/dist/core/approvals/grants.d.ts +216 -0
- package/dist/core/approvals/grants.js +469 -0
- package/dist/core/approvals/grants.js.map +1 -0
- package/dist/core/approvals/matcher.d.ts +202 -0
- package/dist/core/approvals/matcher.js +267 -0
- package/dist/core/approvals/matcher.js.map +1 -0
- package/dist/core/approvals/mcpSubjects.d.ts +40 -0
- package/dist/core/approvals/mcpSubjects.js +99 -0
- package/dist/core/approvals/mcpSubjects.js.map +1 -0
- package/dist/core/approvals/toolAnnotationSources.d.ts +105 -0
- package/dist/core/approvals/toolAnnotationSources.js +277 -0
- package/dist/core/approvals/toolAnnotationSources.js.map +1 -0
- package/dist/core/approvals/toolHost.d.ts +46 -0
- package/dist/core/approvals/toolHost.js +108 -0
- package/dist/core/approvals/toolHost.js.map +1 -0
- package/dist/core/launchBanner.d.ts +120 -0
- package/dist/core/launchBanner.js +399 -0
- package/dist/core/launchBanner.js.map +1 -0
- package/dist/core/plainToolIndication.js +13 -7
- package/dist/core/plainToolIndication.js.map +1 -1
- package/dist/core/shell/approvalStop.d.ts +75 -0
- package/dist/core/shell/approvalStop.js +108 -0
- package/dist/core/shell/approvalStop.js.map +1 -0
- package/dist/core/shell/arity.d.ts +6 -0
- package/dist/core/shell/arity.js +20 -6
- package/dist/core/shell/arity.js.map +1 -1
- package/dist/core/shell/denylist.d.ts +11 -0
- package/dist/core/shell/denylist.js +37 -0
- package/dist/core/shell/denylist.js.map +1 -0
- package/dist/core/shell/normalize.d.ts +41 -1
- package/dist/core/shell/normalize.js +58 -4
- package/dist/core/shell/normalize.js.map +1 -1
- package/dist/core/shell/openWorld.d.ts +138 -0
- package/dist/core/shell/openWorld.js +523 -0
- package/dist/core/shell/openWorld.js.map +1 -0
- package/dist/core/shell/rater.d.ts +560 -0
- package/dist/core/shell/rater.js +895 -0
- package/dist/core/shell/rater.js.map +1 -0
- package/dist/core/shell/raterModel.d.ts +41 -0
- package/dist/core/shell/raterModel.js +51 -0
- package/dist/core/shell/raterModel.js.map +1 -0
- package/dist/core/shell/rejection.d.ts +66 -0
- package/dist/core/shell/rejection.js +38 -0
- package/dist/core/shell/rejection.js.map +1 -0
- package/dist/core/types.d.ts +71 -9
- package/dist/core/types.js.map +1 -1
- package/dist/runtime/askStructured.d.ts +8 -7
- package/dist/runtime/askStructured.js +8 -7
- package/dist/runtime/askStructured.js.map +1 -1
- package/dist/runtime/conversation.js +89 -76
- package/dist/runtime/conversation.js.map +1 -1
- package/dist/runtime/singleShot.js +71 -60
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/utils/ProgressIndicator.d.ts +21 -0
- package/dist/utils/ProgressIndicator.js +30 -3
- package/dist/utils/ProgressIndicator.js.map +1 -1
- package/dist/utils/consoleUtils.d.ts +30 -0
- package/dist/utils/consoleUtils.js +27 -2
- package/dist/utils/consoleUtils.js.map +1 -1
- package/dist/utils/systemUtils.d.ts +17 -0
- package/dist/utils/systemUtils.js +17 -0
- package/dist/utils/systemUtils.js.map +1 -1
- package/package.json +5 -2
- package/schema/gsloth-config.schema.json +1216 -417
- package/dist/core/shell/allowlist.d.ts +0 -75
- package/dist/core/shell/allowlist.js +0 -187
- package/dist/core/shell/allowlist.js.map +0 -1
- package/dist/core/shell/judge.d.ts +0 -161
- package/dist/core/shell/judge.js +0 -261
- package/dist/core/shell/judge.js.map +0 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/shell/approvalStop
|
|
3
|
+
*
|
|
4
|
+
* CFG-27 — the two ways the approvals gate **ends a run** rather than answering a tool call.
|
|
5
|
+
* Both are thrown from `GthAgentRunner.decideToolApproval`, both carry the command and the reason
|
|
6
|
+
* a person needs to see, and both are re-thrown UNCHANGED by `processMessages` (which otherwise
|
|
7
|
+
* wraps failures as `Agent processing failed: …`) so the explanation reaches the user intact.
|
|
8
|
+
*
|
|
9
|
+
* They are errors rather than {@link ../types.js ToolApprovalDecision}s on purpose. A decision is
|
|
10
|
+
* something the model observes as a `ToolMessage` and can respond to; these two are precisely the
|
|
11
|
+
* cases where the spec says the model gets no move at all:
|
|
12
|
+
*
|
|
13
|
+
* - **{@link AttackHaltError}** (§4.2) — an `attack` outcome *halts the run*. "A halt ends the
|
|
14
|
+
* agent loop. It is not a rejection the model can respond to, and no rung except `bypass` can
|
|
15
|
+
* turn it into anything else."
|
|
16
|
+
* - **{@link NonInteractiveEscalationError}** (§6.2) — where no human can answer, *every*
|
|
17
|
+
* escalation is an immediate non-zero exit carrying the command, the rating and its reason.
|
|
18
|
+
* There is no prompt, no waiting, and never a timeout into approval. Teams that need specific
|
|
19
|
+
* commands to run unattended declare them in `approvals.allow` (§3), which is consulted before
|
|
20
|
+
* the rater and therefore never escalates.
|
|
21
|
+
*
|
|
22
|
+
* Exit code: neither class sets one. The single-shot runtime (`runSingleShot`) already reports a
|
|
23
|
+
* thrown run as `ok: false`, and each command entry point turns that into `setExitCode(1)` — so
|
|
24
|
+
* "immediate non-zero exit carrying the explanation" is what a caller already gets, without a new
|
|
25
|
+
* exit path that could diverge from the existing one.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Base class for the two run-ending approvals outcomes, so a caller that wants to present them as
|
|
29
|
+
* an ending rather than a crash can catch both with one `instanceof`. Nothing does that today —
|
|
30
|
+
* every surface currently shows the message as-is, which is already the whole explanation — and
|
|
31
|
+
* [[TUI-C26]] is the node that will catch it to render the §6.1 banner.
|
|
32
|
+
*/
|
|
33
|
+
export declare abstract class ApprovalStopError extends Error {
|
|
34
|
+
/** The command that ended the run. */
|
|
35
|
+
readonly command: string;
|
|
36
|
+
protected constructor(message: string, command: string);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* §4.2 — an `attack` outcome: the command's own **structure** evidenced compromise (§4.1.1 —
|
|
40
|
+
* credential targeting, privilege escalation, persistence, deception, obfuscation). Ends the agent
|
|
41
|
+
* loop; the model is told nothing and offered nothing.
|
|
42
|
+
*
|
|
43
|
+
* [[TUI-C26]] will present this interactively as the §6.1 red banner — any key stops, typing
|
|
44
|
+
* `run anyway` runs this one command — BEFORE the throw; until then the run simply ends with this
|
|
45
|
+
* message, which is what a non-interactive session gets either way (§6.2).
|
|
46
|
+
*
|
|
47
|
+
* The recovery this message names is deliberately the **allow-list**, not `bypass`. §4.2 makes
|
|
48
|
+
* `approvals.allow` the supported way to run such a command unattended (it is consulted before the
|
|
49
|
+
* rater, so it never reaches a halt at all); `bypass` also works and is far blunter — it turns off
|
|
50
|
+
* the rater, the escalation and the halt together, for every command, for the whole run. It is a
|
|
51
|
+
* last resort, not the answer, and the wording says so in that order.
|
|
52
|
+
*/
|
|
53
|
+
export declare class AttackHaltError extends ApprovalStopError {
|
|
54
|
+
/** The rater's explanation of what the command's structure showed. */
|
|
55
|
+
readonly reason: string;
|
|
56
|
+
constructor(command: string, reason: string);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* §6.2 — an escalation with nobody to ask (CI, one-shot runs, servers). Fails the build loudly,
|
|
60
|
+
* with everything a person needs in order to see why.
|
|
61
|
+
*/
|
|
62
|
+
export declare class NonInteractiveEscalationError extends ApprovalStopError {
|
|
63
|
+
/** The rater's outcome, when a rating existed (the unrated rungs have none). */
|
|
64
|
+
readonly outcome: string | undefined;
|
|
65
|
+
/** The rater's explanation, when a rating existed. */
|
|
66
|
+
readonly reason: string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* EXT-71 §3.2 — the declared `approvals.escalate` entry that sent this call to a human, when one
|
|
69
|
+
* did. It changes the recovery the message names: pointing someone at `approvals.allow` when they
|
|
70
|
+
* themselves wrote an escalate entry sends them to a list that cannot win, since a match on
|
|
71
|
+
* `escalate` outranks a match on `allow`.
|
|
72
|
+
*/
|
|
73
|
+
readonly escalatedBy: string | undefined;
|
|
74
|
+
constructor(command: string, outcome?: string, reason?: string, escalatedBy?: string);
|
|
75
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/shell/approvalStop
|
|
3
|
+
*
|
|
4
|
+
* CFG-27 — the two ways the approvals gate **ends a run** rather than answering a tool call.
|
|
5
|
+
* Both are thrown from `GthAgentRunner.decideToolApproval`, both carry the command and the reason
|
|
6
|
+
* a person needs to see, and both are re-thrown UNCHANGED by `processMessages` (which otherwise
|
|
7
|
+
* wraps failures as `Agent processing failed: …`) so the explanation reaches the user intact.
|
|
8
|
+
*
|
|
9
|
+
* They are errors rather than {@link ../types.js ToolApprovalDecision}s on purpose. A decision is
|
|
10
|
+
* something the model observes as a `ToolMessage` and can respond to; these two are precisely the
|
|
11
|
+
* cases where the spec says the model gets no move at all:
|
|
12
|
+
*
|
|
13
|
+
* - **{@link AttackHaltError}** (§4.2) — an `attack` outcome *halts the run*. "A halt ends the
|
|
14
|
+
* agent loop. It is not a rejection the model can respond to, and no rung except `bypass` can
|
|
15
|
+
* turn it into anything else."
|
|
16
|
+
* - **{@link NonInteractiveEscalationError}** (§6.2) — where no human can answer, *every*
|
|
17
|
+
* escalation is an immediate non-zero exit carrying the command, the rating and its reason.
|
|
18
|
+
* There is no prompt, no waiting, and never a timeout into approval. Teams that need specific
|
|
19
|
+
* commands to run unattended declare them in `approvals.allow` (§3), which is consulted before
|
|
20
|
+
* the rater and therefore never escalates.
|
|
21
|
+
*
|
|
22
|
+
* Exit code: neither class sets one. The single-shot runtime (`runSingleShot`) already reports a
|
|
23
|
+
* thrown run as `ok: false`, and each command entry point turns that into `setExitCode(1)` — so
|
|
24
|
+
* "immediate non-zero exit carrying the explanation" is what a caller already gets, without a new
|
|
25
|
+
* exit path that could diverge from the existing one.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Base class for the two run-ending approvals outcomes, so a caller that wants to present them as
|
|
29
|
+
* an ending rather than a crash can catch both with one `instanceof`. Nothing does that today —
|
|
30
|
+
* every surface currently shows the message as-is, which is already the whole explanation — and
|
|
31
|
+
* [[TUI-C26]] is the node that will catch it to render the §6.1 banner.
|
|
32
|
+
*/
|
|
33
|
+
export class ApprovalStopError extends Error {
|
|
34
|
+
/** The command that ended the run. */
|
|
35
|
+
command;
|
|
36
|
+
constructor(message, command) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.command = command;
|
|
39
|
+
// Restore the prototype chain across the ES5 `extends Error` downlevel, so `instanceof` works.
|
|
40
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
41
|
+
this.name = new.target.name;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* §4.2 — an `attack` outcome: the command's own **structure** evidenced compromise (§4.1.1 —
|
|
46
|
+
* credential targeting, privilege escalation, persistence, deception, obfuscation). Ends the agent
|
|
47
|
+
* loop; the model is told nothing and offered nothing.
|
|
48
|
+
*
|
|
49
|
+
* [[TUI-C26]] will present this interactively as the §6.1 red banner — any key stops, typing
|
|
50
|
+
* `run anyway` runs this one command — BEFORE the throw; until then the run simply ends with this
|
|
51
|
+
* message, which is what a non-interactive session gets either way (§6.2).
|
|
52
|
+
*
|
|
53
|
+
* The recovery this message names is deliberately the **allow-list**, not `bypass`. §4.2 makes
|
|
54
|
+
* `approvals.allow` the supported way to run such a command unattended (it is consulted before the
|
|
55
|
+
* rater, so it never reaches a halt at all); `bypass` also works and is far blunter — it turns off
|
|
56
|
+
* the rater, the escalation and the halt together, for every command, for the whole run. It is a
|
|
57
|
+
* last resort, not the answer, and the wording says so in that order.
|
|
58
|
+
*/
|
|
59
|
+
export class AttackHaltError extends ApprovalStopError {
|
|
60
|
+
/** The rater's explanation of what the command's structure showed. */
|
|
61
|
+
reason;
|
|
62
|
+
constructor(command, reason) {
|
|
63
|
+
super(`Run halted: the auto-rater rated this command as an attack, which ends the run.\n` +
|
|
64
|
+
` Command: ${command}\n` +
|
|
65
|
+
` Reason: ${reason}\n` +
|
|
66
|
+
`This is not negotiable. If this command is legitimate and you need it to run, declare ` +
|
|
67
|
+
`it in approvals.allow — that list is consulted before the auto-rater, so it never ` +
|
|
68
|
+
`reaches a halt. Dropping to approvals "bypass" also works, but it turns off the rater, ` +
|
|
69
|
+
`the prompts and the halt for every command in the run.`, command);
|
|
70
|
+
this.reason = reason;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* §6.2 — an escalation with nobody to ask (CI, one-shot runs, servers). Fails the build loudly,
|
|
75
|
+
* with everything a person needs in order to see why.
|
|
76
|
+
*/
|
|
77
|
+
export class NonInteractiveEscalationError extends ApprovalStopError {
|
|
78
|
+
/** The rater's outcome, when a rating existed (the unrated rungs have none). */
|
|
79
|
+
outcome;
|
|
80
|
+
/** The rater's explanation, when a rating existed. */
|
|
81
|
+
reason;
|
|
82
|
+
/**
|
|
83
|
+
* EXT-71 §3.2 — the declared `approvals.escalate` entry that sent this call to a human, when one
|
|
84
|
+
* did. It changes the recovery the message names: pointing someone at `approvals.allow` when they
|
|
85
|
+
* themselves wrote an escalate entry sends them to a list that cannot win, since a match on
|
|
86
|
+
* `escalate` outranks a match on `allow`.
|
|
87
|
+
*/
|
|
88
|
+
escalatedBy;
|
|
89
|
+
constructor(command, outcome, reason, escalatedBy) {
|
|
90
|
+
super(`Approval required, but this session has no one to ask.\n` +
|
|
91
|
+
` Command: ${command}\n` +
|
|
92
|
+
(outcome ? ` Rating: ${outcome}\n` : '') +
|
|
93
|
+
(reason ? ` Reason: ${reason}\n` : '') +
|
|
94
|
+
(escalatedBy
|
|
95
|
+
? ` Matched approvals.escalate: ${escalatedBy}\n` +
|
|
96
|
+
`An escalate entry always asks a human, whatever the rung would have done, so no ` +
|
|
97
|
+
`entry in approvals.allow can answer it. Remove the escalate entry if this command ` +
|
|
98
|
+
`should run unattended.`
|
|
99
|
+
: `Declare the commands this run is allowed to execute in approvals.allow — write each ` +
|
|
100
|
+
`one as an explicit entry, for example { "type": "shell", "matcher": "exact", ` +
|
|
101
|
+
`"pattern": "npm test" }. That list is consulted before the auto-rater and never ` +
|
|
102
|
+
`escalates.`), command);
|
|
103
|
+
this.outcome = outcome;
|
|
104
|
+
this.reason = reason;
|
|
105
|
+
this.escalatedBy = escalatedBy;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=approvalStop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvalStop.js","sourceRoot":"","sources":["../../../src/core/shell/approvalStop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;;GAKG;AACH,MAAM,OAAgB,iBAAkB,SAAQ,KAAK;IACnD,sCAAsC;IAC7B,OAAO,CAAS;IAEzB,YAAsB,OAAe,EAAE,OAAe;QACpD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,+FAA+F;QAC/F,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC;QAClD,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,eAAgB,SAAQ,iBAAiB;IACpD,sEAAsE;IAC7D,MAAM,CAAS;IAExB,YAAY,OAAe,EAAE,MAAc;QACzC,KAAK,CACH,mFAAmF;YACjF,cAAc,OAAO,IAAI;YACzB,aAAa,MAAM,IAAI;YACvB,wFAAwF;YACxF,oFAAoF;YACpF,yFAAyF;YACzF,wDAAwD,EAC1D,OAAO,CACR,CAAC;QACF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,6BAA8B,SAAQ,iBAAiB;IAClE,gFAAgF;IACvE,OAAO,CAAqB;IACrC,sDAAsD;IAC7C,MAAM,CAAqB;IACpC;;;;;OAKG;IACM,WAAW,CAAqB;IAEzC,YAAY,OAAe,EAAE,OAAgB,EAAE,MAAe,EAAE,WAAoB;QAClF,KAAK,CACH,0DAA0D;YACxD,cAAc,OAAO,IAAI;YACzB,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,MAAM,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACvC,CAAC,WAAW;gBACV,CAAC,CAAC,iCAAiC,WAAW,IAAI;oBAChD,kFAAkF;oBAClF,oFAAoF;oBACpF,wBAAwB;gBAC1B,CAAC,CAAC,sFAAsF;oBACtF,+EAA+E;oBAC/E,kFAAkF;oBAClF,YAAY,CAAC,EACnB,OAAO,CACR,CAAC;QACF,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;CACF"}
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* redirections. Such commands NEVER auto-match an allow-list entry — they always go to
|
|
19
19
|
* fresh human approval. This is what stops `git checkout x; rm -rf /` from matching an
|
|
20
20
|
* approved `git checkout *`.
|
|
21
|
+
*
|
|
22
|
+
* EXT-55: "newlines" in that list was aspirational until this node. `normalizeCommand` folded
|
|
23
|
+
* a line break to a SPACE, so the check below never saw one and `ls -la\nrm -rf /` classified
|
|
24
|
+
* as the single command `ls`. The separator set now lives in ONE place
|
|
25
|
+
* ({@link import('./normalize.js').COMMAND_SEPARATOR_CLASS}) and a line break survives
|
|
26
|
+
* normalization, so a multi-line command is categorically ambiguous — exactly like `;`.
|
|
21
27
|
*/
|
|
22
28
|
/**
|
|
23
29
|
* Result of classifying a command for allow-list matching.
|
package/dist/core/shell/arity.js
CHANGED
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
* redirections. Such commands NEVER auto-match an allow-list entry — they always go to
|
|
19
19
|
* fresh human approval. This is what stops `git checkout x; rm -rf /` from matching an
|
|
20
20
|
* approved `git checkout *`.
|
|
21
|
+
*
|
|
22
|
+
* EXT-55: "newlines" in that list was aspirational until this node. `normalizeCommand` folded
|
|
23
|
+
* a line break to a SPACE, so the check below never saw one and `ls -la\nrm -rf /` classified
|
|
24
|
+
* as the single command `ls`. The separator set now lives in ONE place
|
|
25
|
+
* ({@link import('./normalize.js').COMMAND_SEPARATOR_CLASS}) and a line break survives
|
|
26
|
+
* normalization, so a multi-line command is categorically ambiguous — exactly like `;`.
|
|
21
27
|
*/
|
|
28
|
+
import { COMMAND_SEPARATOR_RE, LINE_BREAK_RE } from '#src/core/shell/normalize.js';
|
|
22
29
|
/**
|
|
23
30
|
* Arity table: command-prefix string → number of leading tokens (binary + subcommands,
|
|
24
31
|
* flags excluded) that define the "human-understandable command". Longest matching
|
|
@@ -181,11 +188,9 @@ const ARITY = {
|
|
|
181
188
|
* Note: `&&`/`||`/`|` are covered by the bare `&`/`|` character scan; listed conceptually.
|
|
182
189
|
*/
|
|
183
190
|
function hasUnsafeComposition(normalized) {
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// Shell control / separator operators and background.
|
|
188
|
-
if (/[;|&]/.test(normalized))
|
|
191
|
+
// Shell control / separator operators, background, AND line breaks — one shared definition of
|
|
192
|
+
// "a new command begins here" (EXT-55), so this can never drift from the hardline floor's.
|
|
193
|
+
if (COMMAND_SEPARATOR_RE.test(normalized))
|
|
189
194
|
return true;
|
|
190
195
|
// Command substitution: $(...) or `...`.
|
|
191
196
|
if (/\$\(/.test(normalized))
|
|
@@ -235,7 +240,9 @@ export function tokenize(command) {
|
|
|
235
240
|
inToken = true;
|
|
236
241
|
continue;
|
|
237
242
|
}
|
|
238
|
-
|
|
243
|
+
// EXT-55: line breaks are whitespace here too, so a token can never be glued across a line
|
|
244
|
+
// (`ls\n-o` must tokenize as `ls` + `-o`, or a caller inspecting the argv would miss the flag).
|
|
245
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
239
246
|
if (inToken) {
|
|
240
247
|
tokens.push(current);
|
|
241
248
|
current = '';
|
|
@@ -294,6 +301,13 @@ export function meaningfulPrefixTokens(argv) {
|
|
|
294
301
|
* @param normalize Normalizer to apply for the detection form (inject normalizeCommand).
|
|
295
302
|
*/
|
|
296
303
|
export function classifyCommand(command, normalize) {
|
|
304
|
+
// EXT-55 — the boundary question is answered HERE, not delegated to the injected normalizer.
|
|
305
|
+
// `normalize` is a parameter, so this function cannot assume it preserved the command
|
|
306
|
+
// separators (folding them away is exactly the bug this node fixed). A line break anywhere
|
|
307
|
+
// INSIDE the command means more than one command → never classifiable. `.trim()` first: a
|
|
308
|
+
// purely leading/trailing break separates nothing, and models routinely emit `"npm test\n"`.
|
|
309
|
+
if (LINE_BREAK_RE.test(command.trim()))
|
|
310
|
+
return null;
|
|
297
311
|
const normalized = normalize(command);
|
|
298
312
|
if (!normalized)
|
|
299
313
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arity.js","sourceRoot":"","sources":["../../../src/core/shell/arity.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"arity.js","sourceRoot":"","sources":["../../../src/core/shell/arity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a raw command into the segments a shell would run, normalized and **case preserved**.
|
|
3
|
+
*
|
|
4
|
+
* Substitution bodies are segments in their own right: `echo $(npm publish)` runs `npm publish`,
|
|
5
|
+
* so a deny entry for `npm publish` must see it. Splitting on `$(`, `` ` `` and `)` yields the
|
|
6
|
+
* body as its own segment (and leaves harmless empty fragments, which are dropped).
|
|
7
|
+
*
|
|
8
|
+
* Case is preserved because the matcher folds it per list and per matcher — a `regexp` entry is
|
|
9
|
+
* compiled exactly as the user wrote it, and folding here would decide that for it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function commandSegments(command: string): string[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/shell/denylist
|
|
3
|
+
*
|
|
4
|
+
* **Command segmentation** — splitting a command into every command a shell would actually run.
|
|
5
|
+
*
|
|
6
|
+
* This is what lets a restrictive rule (`deny`, `escalate`) see inside a compound command. The
|
|
7
|
+
* asymmetry it serves lives in `core/approvals/matcher.ts`, which is the one comparison engine:
|
|
8
|
+
*
|
|
9
|
+
* - **No allow entry matches a command that does not statically resolve.** Composition,
|
|
10
|
+
* substitution and redirection are a non-match for every allow entry, so a grant can never be
|
|
11
|
+
* extended with a `; rm -rf /`.
|
|
12
|
+
* - **A deny or escalate entry MAY match one**, and is compared against every segment as well as
|
|
13
|
+
* the whole string, because a prohibition that catches something unresolvable errs in the
|
|
14
|
+
* direction that costs nothing. Without that, `git push --force; ls` would sail straight past a
|
|
15
|
+
* declared deny entry for `git push --force` — a prohibition any trailing `; ls` defeats is not a
|
|
16
|
+
* prohibition.
|
|
17
|
+
*/
|
|
18
|
+
import { COMMAND_SEPARATOR_CLASS, normalizeCommand } from '#src/core/shell/normalize.js';
|
|
19
|
+
/** Splits the normalized command at every point where a shell would begin a NEW command. */
|
|
20
|
+
const SEGMENT_SPLIT_RE = new RegExp(`[${COMMAND_SEPARATOR_CLASS}]|\\$\\(|\\)|\``, 'g');
|
|
21
|
+
/**
|
|
22
|
+
* Split a raw command into the segments a shell would run, normalized and **case preserved**.
|
|
23
|
+
*
|
|
24
|
+
* Substitution bodies are segments in their own right: `echo $(npm publish)` runs `npm publish`,
|
|
25
|
+
* so a deny entry for `npm publish` must see it. Splitting on `$(`, `` ` `` and `)` yields the
|
|
26
|
+
* body as its own segment (and leaves harmless empty fragments, which are dropped).
|
|
27
|
+
*
|
|
28
|
+
* Case is preserved because the matcher folds it per list and per matcher — a `regexp` entry is
|
|
29
|
+
* compiled exactly as the user wrote it, and folding here would decide that for it.
|
|
30
|
+
*/
|
|
31
|
+
export function commandSegments(command) {
|
|
32
|
+
return normalizeCommand(command)
|
|
33
|
+
.split(SEGMENT_SPLIT_RE)
|
|
34
|
+
.map((segment) => segment.trim())
|
|
35
|
+
.filter((segment) => segment.length > 0);
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=denylist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"denylist.js","sourceRoot":"","sources":["../../../src/core/shell/denylist.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEzF,4FAA4F;AAC5F,MAAM,gBAAgB,GAAG,IAAI,MAAM,CAAC,IAAI,uBAAuB,iBAAiB,EAAE,GAAG,CAAC,CAAC;AAEvF;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,OAAO,gBAAgB,CAAC,OAAO,CAAC;SAC7B,KAAK,CAAC,gBAAgB,CAAC;SACvB,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;SAChC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC7C,CAAC"}
|
|
@@ -10,6 +10,34 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Patterned after hermes-agent `tools/approval.py:_normalize_command_for_detection`.
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* SECURITY / EXT-55 — the single source of truth for **where a new command begins**.
|
|
15
|
+
*
|
|
16
|
+
* This is a regex character-class BODY (embed it as `` `[${COMMAND_SEPARATOR_CLASS}]` ``) listing
|
|
17
|
+
* every character at which the shell stops one command and starts the next: `;`, `&` (hence
|
|
18
|
+
* `&&`), `|` (hence `||`), and a LINE BREAK. Both consumers of the normalized form build their
|
|
19
|
+
* patterns from it — the allow-list classifier's fail-closed check
|
|
20
|
+
* ({@link import('./arity.js').classifyCommand}) and the agent hardline blocklist's pattern
|
|
21
|
+
* terminators (`@gaunt-sloth/agent` `tools/shell/hardline`) — so the two layers can never again
|
|
22
|
+
* disagree about what a separator is.
|
|
23
|
+
*
|
|
24
|
+
* The layers disagreed before EXT-55: `;`/`&&`/`|` made a command ambiguous (fail-closed) but a
|
|
25
|
+
* newline did not, because {@link normalizeCommand} folded it to a SPACE. `ls -la\nrm -rf /` was
|
|
26
|
+
* therefore classified as the single command `ls`, and an ordinary `ls` grant auto-approved it.
|
|
27
|
+
*
|
|
28
|
+
* `\r` is listed defensively: {@link normalizeCommand} canonicalizes CR/CRLF to LF, so a
|
|
29
|
+
* normalized string never contains one — but a pattern matched against a RAW command still fails
|
|
30
|
+
* closed.
|
|
31
|
+
*/
|
|
32
|
+
export declare const COMMAND_SEPARATOR_CLASS = ";&|\\n\\r";
|
|
33
|
+
/** {@link COMMAND_SEPARATOR_CLASS} as a ready-made single-character matcher. */
|
|
34
|
+
export declare const COMMAND_SEPARATOR_RE: RegExp;
|
|
35
|
+
/**
|
|
36
|
+
* Matches any line break — the separator {@link normalizeCommand} used to destroy (EXT-55).
|
|
37
|
+
* Exported so a caller can ask "is this more than one command?" of a RAW string without
|
|
38
|
+
* depending on someone else's normalizer having preserved the boundary.
|
|
39
|
+
*/
|
|
40
|
+
export declare const LINE_BREAK_RE: RegExp;
|
|
13
41
|
/**
|
|
14
42
|
* Normalize a command string before dangerous-pattern matching.
|
|
15
43
|
*
|
|
@@ -17,9 +45,21 @@
|
|
|
17
45
|
* - strip ANSI escape sequences (CSI / OSC / lone-escape),
|
|
18
46
|
* - drop null bytes,
|
|
19
47
|
* - Unicode NFKC fold (fullwidth `rm` → `rm`, etc.),
|
|
48
|
+
* - canonicalize every line ending (CRLF / lone CR) to a bare `\n`,
|
|
20
49
|
* - collapse shell backslash-escapes (`r\m` → `rm`, `\-rf` → `-rf`),
|
|
21
50
|
* - drop empty-string literals that split tokens (`r''m` / `r""m` → `rm`),
|
|
22
|
-
* - fold runs of whitespace (
|
|
51
|
+
* - fold runs of HORIZONTAL whitespace (spaces/tabs) to single spaces,
|
|
52
|
+
* - collapse each run of line breaks to a single `\n` — **which survives**, because a line break
|
|
53
|
+
* is a command separator, not padding (EXT-55) — and trim the ends.
|
|
54
|
+
*
|
|
55
|
+
* EXT-55: the last two steps used to be one `\s+ → ' '` fold, which erased the command boundary
|
|
56
|
+
* and let `ls -la\nrm -rf /` be read as the single command `ls`. A line break now reaches every
|
|
57
|
+
* consumer intact, exactly like `;`. Leading/trailing breaks are still trimmed, so the very
|
|
58
|
+
* common `"npm test\n"` tool argument remains ONE command and keeps matching the allow-list.
|
|
59
|
+
*
|
|
60
|
+
* A backslash before a line break (a shell line continuation) is deliberately NOT joined: folding
|
|
61
|
+
* it away would re-open the same hole for `ls \<newline>rm -rf /`. Keeping the boundary makes such
|
|
62
|
+
* a command categorically ambiguous, which costs a prompt, not a failure.
|
|
23
63
|
*
|
|
24
64
|
* This is intentionally lossy: the normalized form is ONLY used for detection,
|
|
25
65
|
* never for execution (the original command is what runs).
|
|
@@ -19,6 +19,40 @@ const ANSI_OSC = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
|
|
|
19
19
|
const ANSI_LONE = /\x1b[@-Z\\-_]?/g;
|
|
20
20
|
// Null bytes.
|
|
21
21
|
const NULL_BYTES = /\x00/g;
|
|
22
|
+
// Any line ending — CRLF, a lone CR, or a lone LF. Canonicalized to `\n` by normalizeCommand.
|
|
23
|
+
const LINE_ENDINGS = /\r\n?/g;
|
|
24
|
+
// A run of HORIZONTAL whitespace: whitespace that is not a line break (spaces, tabs, NBSP, …).
|
|
25
|
+
const HORIZONTAL_WS = /[^\S\n]+/g;
|
|
26
|
+
// A run of line breaks together with any whitespace around it.
|
|
27
|
+
const LINE_BREAK_RUN = /\s*\n\s*/g;
|
|
28
|
+
/**
|
|
29
|
+
* SECURITY / EXT-55 — the single source of truth for **where a new command begins**.
|
|
30
|
+
*
|
|
31
|
+
* This is a regex character-class BODY (embed it as `` `[${COMMAND_SEPARATOR_CLASS}]` ``) listing
|
|
32
|
+
* every character at which the shell stops one command and starts the next: `;`, `&` (hence
|
|
33
|
+
* `&&`), `|` (hence `||`), and a LINE BREAK. Both consumers of the normalized form build their
|
|
34
|
+
* patterns from it — the allow-list classifier's fail-closed check
|
|
35
|
+
* ({@link import('./arity.js').classifyCommand}) and the agent hardline blocklist's pattern
|
|
36
|
+
* terminators (`@gaunt-sloth/agent` `tools/shell/hardline`) — so the two layers can never again
|
|
37
|
+
* disagree about what a separator is.
|
|
38
|
+
*
|
|
39
|
+
* The layers disagreed before EXT-55: `;`/`&&`/`|` made a command ambiguous (fail-closed) but a
|
|
40
|
+
* newline did not, because {@link normalizeCommand} folded it to a SPACE. `ls -la\nrm -rf /` was
|
|
41
|
+
* therefore classified as the single command `ls`, and an ordinary `ls` grant auto-approved it.
|
|
42
|
+
*
|
|
43
|
+
* `\r` is listed defensively: {@link normalizeCommand} canonicalizes CR/CRLF to LF, so a
|
|
44
|
+
* normalized string never contains one — but a pattern matched against a RAW command still fails
|
|
45
|
+
* closed.
|
|
46
|
+
*/
|
|
47
|
+
export const COMMAND_SEPARATOR_CLASS = ';&|\\n\\r';
|
|
48
|
+
/** {@link COMMAND_SEPARATOR_CLASS} as a ready-made single-character matcher. */
|
|
49
|
+
export const COMMAND_SEPARATOR_RE = new RegExp(`[${COMMAND_SEPARATOR_CLASS}]`);
|
|
50
|
+
/**
|
|
51
|
+
* Matches any line break — the separator {@link normalizeCommand} used to destroy (EXT-55).
|
|
52
|
+
* Exported so a caller can ask "is this more than one command?" of a RAW string without
|
|
53
|
+
* depending on someone else's normalizer having preserved the boundary.
|
|
54
|
+
*/
|
|
55
|
+
export const LINE_BREAK_RE = /[\n\r]/;
|
|
22
56
|
/**
|
|
23
57
|
* Normalize a command string before dangerous-pattern matching.
|
|
24
58
|
*
|
|
@@ -26,9 +60,21 @@ const NULL_BYTES = /\x00/g;
|
|
|
26
60
|
* - strip ANSI escape sequences (CSI / OSC / lone-escape),
|
|
27
61
|
* - drop null bytes,
|
|
28
62
|
* - Unicode NFKC fold (fullwidth `rm` → `rm`, etc.),
|
|
63
|
+
* - canonicalize every line ending (CRLF / lone CR) to a bare `\n`,
|
|
29
64
|
* - collapse shell backslash-escapes (`r\m` → `rm`, `\-rf` → `-rf`),
|
|
30
65
|
* - drop empty-string literals that split tokens (`r''m` / `r""m` → `rm`),
|
|
31
|
-
* - fold runs of whitespace (
|
|
66
|
+
* - fold runs of HORIZONTAL whitespace (spaces/tabs) to single spaces,
|
|
67
|
+
* - collapse each run of line breaks to a single `\n` — **which survives**, because a line break
|
|
68
|
+
* is a command separator, not padding (EXT-55) — and trim the ends.
|
|
69
|
+
*
|
|
70
|
+
* EXT-55: the last two steps used to be one `\s+ → ' '` fold, which erased the command boundary
|
|
71
|
+
* and let `ls -la\nrm -rf /` be read as the single command `ls`. A line break now reaches every
|
|
72
|
+
* consumer intact, exactly like `;`. Leading/trailing breaks are still trimmed, so the very
|
|
73
|
+
* common `"npm test\n"` tool argument remains ONE command and keeps matching the allow-list.
|
|
74
|
+
*
|
|
75
|
+
* A backslash before a line break (a shell line continuation) is deliberately NOT joined: folding
|
|
76
|
+
* it away would re-open the same hole for `ls \<newline>rm -rf /`. Keeping the boundary makes such
|
|
77
|
+
* a command categorically ambiguous, which costs a prompt, not a failure.
|
|
32
78
|
*
|
|
33
79
|
* This is intentionally lossy: the normalized form is ONLY used for detection,
|
|
34
80
|
* never for execution (the original command is what runs).
|
|
@@ -41,13 +87,21 @@ export function normalizeCommand(command) {
|
|
|
41
87
|
c = c.replace(NULL_BYTES, '');
|
|
42
88
|
// Unicode compatibility fold (fullwidth → ASCII, etc.).
|
|
43
89
|
c = c.normalize('NFKC');
|
|
90
|
+
// EXT-55: canonicalize line endings FIRST so CR and CRLF are the same separator as LF for every
|
|
91
|
+
// step below (and for every consumer of the normalized form).
|
|
92
|
+
c = c.replace(LINE_ENDINGS, '\n');
|
|
44
93
|
// Collapse backslash-escapes: `\x` → `x` (prevents `r\m -rf /` bypass).
|
|
45
94
|
// Applied before empty-string stripping so `r\m` and `r''m` both fold.
|
|
95
|
+
// `[^\n]` keeps a backslash line-continuation intact — see the docblock.
|
|
46
96
|
c = c.replace(/\\([^\n])/g, '$1');
|
|
47
97
|
// Drop empty-string literals used to split a token: `r''m` / `r""m` → `rm`.
|
|
48
98
|
c = c.replace(/''|""/g, '');
|
|
49
|
-
// Fold
|
|
50
|
-
c = c.replace(
|
|
51
|
-
|
|
99
|
+
// Fold runs of horizontal whitespace to a single space…
|
|
100
|
+
c = c.replace(HORIZONTAL_WS, ' ');
|
|
101
|
+
// …then collapse each run of line breaks (with the whitespace around it) to a single `\n`,
|
|
102
|
+
// which SURVIVES normalization as the command separator it is (EXT-55).
|
|
103
|
+
c = c.replace(LINE_BREAK_RUN, '\n');
|
|
104
|
+
// Trim the ends — including a leading/trailing break, which separates nothing.
|
|
105
|
+
return c.trim();
|
|
52
106
|
}
|
|
53
107
|
//# sourceMappingURL=normalize.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../../src/core/shell/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,uEAAuE;AACvE,yCAAyC;AACzC,MAAM,QAAQ,GAAG,0BAA0B,CAAC;AAC5C,kDAAkD;AAClD,MAAM,QAAQ,GAAG,gCAAgC,CAAC;AAClD,8DAA8D;AAC9D,MAAM,SAAS,GAAG,iBAAiB,CAAC;AACpC,cAAc;AACd,MAAM,UAAU,GAAG,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../../src/core/shell/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,uEAAuE;AACvE,yCAAyC;AACzC,MAAM,QAAQ,GAAG,0BAA0B,CAAC;AAC5C,kDAAkD;AAClD,MAAM,QAAQ,GAAG,gCAAgC,CAAC;AAClD,8DAA8D;AAC9D,MAAM,SAAS,GAAG,iBAAiB,CAAC;AACpC,cAAc;AACd,MAAM,UAAU,GAAG,OAAO,CAAC;AAC3B,8FAA8F;AAC9F,MAAM,YAAY,GAAG,QAAQ,CAAC;AAC9B,+FAA+F;AAC/F,MAAM,aAAa,GAAG,WAAW,CAAC;AAClC,+DAA+D;AAC/D,MAAM,cAAc,GAAG,WAAW,CAAC;AAEnC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,WAAW,CAAC;AAEnD,gFAAgF;AAChF,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,MAAM,CAAC,IAAI,uBAAuB,GAAG,CAAC,CAAC;AAE/E;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,QAAQ,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,IAAI,CAAC,GAAG,OAAO,CAAC;IAChB,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC5B,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC5B,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC7B,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC9B,wDAAwD;IACxD,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACxB,gGAAgG;IAChG,8DAA8D;IAC9D,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAClC,wEAAwE;IACxE,uEAAuE;IACvE,yEAAyE;IACzE,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAClC,4EAA4E;IAC5E,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC5B,wDAAwD;IACxD,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IAClC,2FAA2F;IAC3F,wEAAwE;IACxE,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;IACpC,+EAA+E;IAC/E,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;AAClB,CAAC"}
|