@gaunt-sloth/core 2.0.0-alpha.25 → 2.0.0-alpha.27
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/dist/config/schema.js +64 -15
- package/dist/config/schema.js.map +1 -1
- package/dist/config/types.d.ts +17 -8
- package/dist/config/types.js.map +1 -1
- package/dist/constants.d.ts +7 -4
- package/dist/constants.js +7 -4
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +10 -4
- package/dist/core/GthAgentRunner.js +24 -7
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.d.ts +16 -0
- package/dist/core/GthLangChainAgent.js +36 -15
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/launchBanner.js +36 -17
- package/dist/core/launchBanner.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/openWorld.d.ts +137 -12
- package/dist/core/shell/openWorld.js +677 -12
- package/dist/core/shell/openWorld.js.map +1 -1
- package/dist/core/shell/rater.d.ts +68 -38
- package/dist/core/shell/rater.js +105 -61
- package/dist/core/shell/rater.js.map +1 -1
- package/dist/core/shell/rejection.d.ts +7 -4
- package/dist/core/shell/rejection.js +3 -3
- package/dist/core/shell/rejection.js.map +1 -1
- package/dist/core/toolDisplay.d.ts +12 -3
- package/dist/core/toolDisplay.js +27 -7
- package/dist/core/toolDisplay.js.map +1 -1
- package/dist/utils/displayWidth.d.ts +30 -0
- package/dist/utils/displayWidth.js +140 -0
- package/dist/utils/displayWidth.js.map +1 -0
- package/dist/utils/systemPromptNotes.d.ts +28 -8
- package/dist/utils/systemPromptNotes.js +47 -49
- package/dist/utils/systemPromptNotes.js.map +1 -1
- package/dist/utils/untrustedText.d.ts +66 -0
- package/dist/utils/untrustedText.js +80 -0
- package/dist/utils/untrustedText.js.map +1 -0
- package/package.json +2 -1
- package/schema/gsloth-config.schema.json +3 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module utils/untrustedText
|
|
3
|
+
*
|
|
4
|
+
* **The structural delimiters gsloth wraps untrusted text in, and the one function that neutralizes
|
|
5
|
+
* a forged one.**
|
|
6
|
+
*
|
|
7
|
+
* Text that arrived from outside this process — an MCP server's discovery `instructions`, a command
|
|
8
|
+
* string the model composed from an issue body or a fetched page — is quoted into the model's
|
|
9
|
+
* context in several places. Wherever it is, the same two things have to be true:
|
|
10
|
+
*
|
|
11
|
+
* 1. the quoted text is visibly fenced, so the model can tell data from first-party instruction; and
|
|
12
|
+
* 2. **the quoted text cannot forge the fence** and escape it.
|
|
13
|
+
*
|
|
14
|
+
* (2) is the load-bearing half, and it is why the delimiters and the defang live together in one
|
|
15
|
+
* module rather than beside each consumer. A second defang written for a second consumer is how one
|
|
16
|
+
* of them comes to know about a delimiter the other emits: a forged `[END …]` token that the
|
|
17
|
+
* emitting site happens not to defang closes its fence early and the attacker's lines land OUTSIDE
|
|
18
|
+
* the boundary, reading as first-party text. {@link defangUntrustedDelimiters} knows **every**
|
|
19
|
+
* delimiter we emit, so every consumer is protected by every arm.
|
|
20
|
+
*
|
|
21
|
+
* **Defang BEFORE wrapping, always.** That ordering is the whole mechanism; wrapping first and
|
|
22
|
+
* sanitizing after would sanitize a string that already contains the real delimiters.
|
|
23
|
+
*/
|
|
24
|
+
/** The MCP discovery-instructions fence (EXT-32), emitted by `utils/systemPromptNotes.ts`. */
|
|
25
|
+
export declare const MCP_FENCE_BEGIN = "[BEGIN MCP SERVER-PROVIDED CONTEXT]";
|
|
26
|
+
export declare const MCP_FENCE_END = "[END MCP SERVER-PROVIDED CONTEXT]";
|
|
27
|
+
/**
|
|
28
|
+
* EXT-65 — the fence a refused command is quoted back inside, emitted by
|
|
29
|
+
* `core/shell/abstention.ts`. A command the gate could not parse is frequently a command the model
|
|
30
|
+
* assembled out of text it read somewhere, so quoting it back for the model to rewrite is quoting
|
|
31
|
+
* untrusted bytes into the model's context.
|
|
32
|
+
*/
|
|
33
|
+
export declare const QUOTED_COMMAND_FENCE_BEGIN = "[BEGIN QUOTED COMMAND TEXT]";
|
|
34
|
+
export declare const QUOTED_COMMAND_FENCE_END = "[END QUOTED COMMAND TEXT]";
|
|
35
|
+
/** How much of a refused command is quoted back. Generous; a realistic command is far shorter. */
|
|
36
|
+
export declare const QUOTED_COMMAND_MAX_CHARS = 2000;
|
|
37
|
+
/** Appended when {@link QUOTED_COMMAND_MAX_CHARS} actually clipped the quoted command. */
|
|
38
|
+
export declare const QUOTED_COMMAND_TRUNCATION_MARKER = "\u2026 [truncated]";
|
|
39
|
+
/**
|
|
40
|
+
* Neutralize every structural delimiter this codebase emits, inside UNTRUSTED text.
|
|
41
|
+
*
|
|
42
|
+
* The text is fully attacker-influenceable, so it may forge:
|
|
43
|
+
* - either fence's tokens (`[BEGIN|END MCP SERVER-PROVIDED CONTEXT]`,
|
|
44
|
+
* `[BEGIN|END QUOTED COMMAND TEXT]`) — the bracket run is collapsed so they can no longer be
|
|
45
|
+
* read as the real delimiter, while staying legible to a reader who wants to see what was
|
|
46
|
+
* attempted; and
|
|
47
|
+
* - a per-server label line `--- Server: …` (EXT-32) — the leading `---` run is broken so it
|
|
48
|
+
* cannot masquerade as one of ours.
|
|
49
|
+
*
|
|
50
|
+
* After this, the ONLY real delimiters in a composed block are the ones the caller emits. Names we
|
|
51
|
+
* put in our OWN labels come from trusted config keys and are not sanitized — only the untrusted
|
|
52
|
+
* CONTENT is.
|
|
53
|
+
*
|
|
54
|
+
* Whitespace-tolerant (`\s+`, optional bracket padding, `-{3,}`) so trivial spacing variants cannot
|
|
55
|
+
* slip a delimiter through, and case-insensitive so neither can a lowercase one.
|
|
56
|
+
*/
|
|
57
|
+
export declare function defangUntrustedDelimiters(text: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Truncate untrusted text at `maxChars`, SURROGATE-SAFE, appending `marker` only when text was
|
|
60
|
+
* actually clipped.
|
|
61
|
+
*
|
|
62
|
+
* A naive `slice(0, N)` can split a surrogate pair (e.g. an emoji) at the boundary and emit a lone
|
|
63
|
+
* half-code-unit. If the cut would land between a high and a low surrogate, back off one code unit
|
|
64
|
+
* so the pair is kept whole (dropped entirely rather than split).
|
|
65
|
+
*/
|
|
66
|
+
export declare function capUntrustedText(text: string, maxChars: number, marker: string): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module utils/untrustedText
|
|
3
|
+
*
|
|
4
|
+
* **The structural delimiters gsloth wraps untrusted text in, and the one function that neutralizes
|
|
5
|
+
* a forged one.**
|
|
6
|
+
*
|
|
7
|
+
* Text that arrived from outside this process — an MCP server's discovery `instructions`, a command
|
|
8
|
+
* string the model composed from an issue body or a fetched page — is quoted into the model's
|
|
9
|
+
* context in several places. Wherever it is, the same two things have to be true:
|
|
10
|
+
*
|
|
11
|
+
* 1. the quoted text is visibly fenced, so the model can tell data from first-party instruction; and
|
|
12
|
+
* 2. **the quoted text cannot forge the fence** and escape it.
|
|
13
|
+
*
|
|
14
|
+
* (2) is the load-bearing half, and it is why the delimiters and the defang live together in one
|
|
15
|
+
* module rather than beside each consumer. A second defang written for a second consumer is how one
|
|
16
|
+
* of them comes to know about a delimiter the other emits: a forged `[END …]` token that the
|
|
17
|
+
* emitting site happens not to defang closes its fence early and the attacker's lines land OUTSIDE
|
|
18
|
+
* the boundary, reading as first-party text. {@link defangUntrustedDelimiters} knows **every**
|
|
19
|
+
* delimiter we emit, so every consumer is protected by every arm.
|
|
20
|
+
*
|
|
21
|
+
* **Defang BEFORE wrapping, always.** That ordering is the whole mechanism; wrapping first and
|
|
22
|
+
* sanitizing after would sanitize a string that already contains the real delimiters.
|
|
23
|
+
*/
|
|
24
|
+
/** The MCP discovery-instructions fence (EXT-32), emitted by `utils/systemPromptNotes.ts`. */
|
|
25
|
+
export const MCP_FENCE_BEGIN = '[BEGIN MCP SERVER-PROVIDED CONTEXT]';
|
|
26
|
+
export const MCP_FENCE_END = '[END MCP SERVER-PROVIDED CONTEXT]';
|
|
27
|
+
/**
|
|
28
|
+
* EXT-65 — the fence a refused command is quoted back inside, emitted by
|
|
29
|
+
* `core/shell/abstention.ts`. A command the gate could not parse is frequently a command the model
|
|
30
|
+
* assembled out of text it read somewhere, so quoting it back for the model to rewrite is quoting
|
|
31
|
+
* untrusted bytes into the model's context.
|
|
32
|
+
*/
|
|
33
|
+
export const QUOTED_COMMAND_FENCE_BEGIN = '[BEGIN QUOTED COMMAND TEXT]';
|
|
34
|
+
export const QUOTED_COMMAND_FENCE_END = '[END QUOTED COMMAND TEXT]';
|
|
35
|
+
/** How much of a refused command is quoted back. Generous; a realistic command is far shorter. */
|
|
36
|
+
export const QUOTED_COMMAND_MAX_CHARS = 2000;
|
|
37
|
+
/** Appended when {@link QUOTED_COMMAND_MAX_CHARS} actually clipped the quoted command. */
|
|
38
|
+
export const QUOTED_COMMAND_TRUNCATION_MARKER = '… [truncated]';
|
|
39
|
+
/**
|
|
40
|
+
* Neutralize every structural delimiter this codebase emits, inside UNTRUSTED text.
|
|
41
|
+
*
|
|
42
|
+
* The text is fully attacker-influenceable, so it may forge:
|
|
43
|
+
* - either fence's tokens (`[BEGIN|END MCP SERVER-PROVIDED CONTEXT]`,
|
|
44
|
+
* `[BEGIN|END QUOTED COMMAND TEXT]`) — the bracket run is collapsed so they can no longer be
|
|
45
|
+
* read as the real delimiter, while staying legible to a reader who wants to see what was
|
|
46
|
+
* attempted; and
|
|
47
|
+
* - a per-server label line `--- Server: …` (EXT-32) — the leading `---` run is broken so it
|
|
48
|
+
* cannot masquerade as one of ours.
|
|
49
|
+
*
|
|
50
|
+
* After this, the ONLY real delimiters in a composed block are the ones the caller emits. Names we
|
|
51
|
+
* put in our OWN labels come from trusted config keys and are not sanitized — only the untrusted
|
|
52
|
+
* CONTENT is.
|
|
53
|
+
*
|
|
54
|
+
* Whitespace-tolerant (`\s+`, optional bracket padding, `-{3,}`) so trivial spacing variants cannot
|
|
55
|
+
* slip a delimiter through, and case-insensitive so neither can a lowercase one.
|
|
56
|
+
*/
|
|
57
|
+
export function defangUntrustedDelimiters(text) {
|
|
58
|
+
return text
|
|
59
|
+
.replace(/\[\s*(BEGIN|END)\s+MCP\s+SERVER-PROVIDED\s+CONTEXT\s*\]/gi, (_m, kw) => `(server text: ${kw.toUpperCase()} MCP SERVER-PROVIDED CONTEXT)`)
|
|
60
|
+
.replace(/\[\s*(BEGIN|END)\s+QUOTED\s+COMMAND\s+TEXT\s*\]/gi, (_m, kw) => `(quoted text: ${kw.toUpperCase()} QUOTED COMMAND TEXT)`)
|
|
61
|
+
.replace(/-{3,}(\s*Server\s*:)/gi, '- - -$1');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Truncate untrusted text at `maxChars`, SURROGATE-SAFE, appending `marker` only when text was
|
|
65
|
+
* actually clipped.
|
|
66
|
+
*
|
|
67
|
+
* A naive `slice(0, N)` can split a surrogate pair (e.g. an emoji) at the boundary and emit a lone
|
|
68
|
+
* half-code-unit. If the cut would land between a high and a low surrogate, back off one code unit
|
|
69
|
+
* so the pair is kept whole (dropped entirely rather than split).
|
|
70
|
+
*/
|
|
71
|
+
export function capUntrustedText(text, maxChars, marker) {
|
|
72
|
+
if (text.length <= maxChars)
|
|
73
|
+
return text;
|
|
74
|
+
let end = maxChars;
|
|
75
|
+
const code = text.charCodeAt(end - 1);
|
|
76
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
77
|
+
end -= 1; // don't split a surrogate pair
|
|
78
|
+
return `${text.slice(0, end).trimEnd()}\n${marker}`;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=untrustedText.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"untrustedText.js","sourceRoot":"","sources":["../../src/utils/untrustedText.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,8FAA8F;AAC9F,MAAM,CAAC,MAAM,eAAe,GAAG,qCAAqC,CAAC;AACrE,MAAM,CAAC,MAAM,aAAa,GAAG,mCAAmC,CAAC;AAEjE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,6BAA6B,CAAC;AACxE,MAAM,CAAC,MAAM,wBAAwB,GAAG,2BAA2B,CAAC;AAEpE,kGAAkG;AAClG,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAE7C,0FAA0F;AAC1F,MAAM,CAAC,MAAM,gCAAgC,GAAG,eAAe,CAAC;AAEhE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,IAAI;SACR,OAAO,CACN,2DAA2D,EAC3D,CAAC,EAAE,EAAE,EAAU,EAAE,EAAE,CAAC,iBAAiB,EAAE,CAAC,WAAW,EAAE,+BAA+B,CACrF;SACA,OAAO,CACN,mDAAmD,EACnD,CAAC,EAAE,EAAE,EAAU,EAAE,EAAE,CAAC,iBAAiB,EAAE,CAAC,WAAW,EAAE,uBAAuB,CAC7E;SACA,OAAO,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,QAAgB,EAAE,MAAc;IAC7E,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,IAAI,GAAG,GAAG,QAAQ,CAAC;IACnB,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACtC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM;QAAE,GAAG,IAAI,CAAC,CAAC,CAAC,+BAA+B;IAC/E,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,MAAM,EAAE,CAAC;AACtD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaunt-sloth/core",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.27",
|
|
4
4
|
"description": "Core utilities and types for Gaunt Sloth",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Andrew Kondratev",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"jiti": "^2.7.0",
|
|
36
36
|
"jsonc-parser": "^3.3.1",
|
|
37
37
|
"langchain": "^1.5.4",
|
|
38
|
+
"string-width": "^8.2.2",
|
|
38
39
|
"zod": "^4.4.3"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|