@gaunt-sloth/core 2.0.0-alpha.26 → 2.0.0-alpha.28
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/GthAbstractAgent.d.ts +1 -19
- package/dist/core/GthAbstractAgent.js +13 -65
- package/dist/core/GthAbstractAgent.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.js +16 -11
- 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 +79 -39
- package/dist/core/shell/rater.js +132 -71
- 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/providers/openrouter.d.ts +3 -4
- package/dist/providers/openrouter.js +15 -30
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/runtime/askStructured.js +7 -5
- package/dist/runtime/askStructured.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/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 +6 -1
- package/schema/gsloth-config.schema.json +3 -1
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* `GthDeepAgent`.
|
|
16
16
|
*/
|
|
17
17
|
import { DEFAULT_COMMIT_CO_AUTHOR_EMAIL, DEFAULT_COMMIT_CO_AUTHOR_NAME } from '#src/constants.js';
|
|
18
|
+
import { capUntrustedText, defangUntrustedDelimiters, MCP_FENCE_BEGIN, MCP_FENCE_END, } from '#src/utils/untrustedText.js';
|
|
18
19
|
/**
|
|
19
20
|
* EXT-26: the platform-agnostic tail shared by both {@link appendOsShellNote} branches.
|
|
20
21
|
*
|
|
@@ -84,34 +85,64 @@ export function appendCwdNote(systemPrompt, cwd) {
|
|
|
84
85
|
return systemPrompt ? `${systemPrompt}\n\n${cwdNote}` : cwdNote;
|
|
85
86
|
}
|
|
86
87
|
/**
|
|
87
|
-
* GS2-35: append the commit
|
|
88
|
+
* GS2-35/EXT-83: append the commit-writing rules to the composed code-mode system prompt.
|
|
88
89
|
*
|
|
89
90
|
* Gaunt Sloth has **no dedicated git-commit tool** — the agent commits by calling
|
|
90
|
-
* `run_shell_command` with `git commit`, composing the message (including any trailer) itself.
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* the
|
|
91
|
+
* `run_shell_command` with `git commit`, composing the message (including any trailer) itself. That
|
|
92
|
+
* leaves two things it must be told, and both live here because both are about committing:
|
|
93
|
+
*
|
|
94
|
+
* 1. **WHO the co-author is.** Left unguided, models emit their own model name from trained habit,
|
|
95
|
+
* which is factually wrong: the commit was produced by *Gaunt Sloth*, not by the model. The note
|
|
96
|
+
* states the exact trailer to emit. EXT-83 — rather than enumerate model names not to write (a
|
|
97
|
+
* denylist is stale the day a new vendor ships, and an enumeration beside a catch-all teaches the
|
|
98
|
+
* model that the list is the rule), the correct name is SUPPLIED: the resolved
|
|
99
|
+
* {@link ResolvedModelIdentity} decorates the DEFAULT name as `Gaunt Sloth (provider:model)`, so
|
|
100
|
+
* the real model is named while the authorship stays Gaunt Sloth's.
|
|
101
|
+
* 2. **HOW the message reaches git.** A commit message passed inline in a double-quoted shell
|
|
102
|
+
* argument is EXPANDED BY THE SHELL before git runs, so a message that quotes code the way
|
|
103
|
+
* ordinary technical prose does is executed as a command. The note states that mechanism rather
|
|
104
|
+
* than merely forbidding the construct — naming a construct without its mechanism has been
|
|
105
|
+
* measured not to work. A file path carries no shell metacharacters, so the file form removes the
|
|
106
|
+
* failure mode instead of asking the model to avoid it.
|
|
107
|
+
*
|
|
108
|
+
* The note's prose carries **no backtick and no other markup** — including no angle-bracket
|
|
109
|
+
* placeholder: it is the one piece of guidance whose subject is how to write a commit message, so
|
|
110
|
+
* quoting its own examples in backticks would demonstrate the exact style rule 2 exists to stop, and
|
|
111
|
+
* an angle-bracket placeholder copied literally is itself a shell input redirect. The `<email>` of
|
|
112
|
+
* the trailer line is the exception the RFC form requires, and is scoped out of the scan.
|
|
95
113
|
*
|
|
96
114
|
* The identity is config-driven (`commit.coAuthor` in {@link import('#src/config/types.js').GthConfig}).
|
|
97
115
|
* Each field falls back INDEPENDENTLY to the Gaunt Sloth account
|
|
98
116
|
* ({@link DEFAULT_COMMIT_CO_AUTHOR_NAME} / {@link DEFAULT_COMMIT_CO_AUTHOR_EMAIL}) — so a partial
|
|
99
117
|
* override (name only, or a config that bypassed the loader) still yields a complete trailer, and a
|
|
100
|
-
* fully-absent config yields the default account. Blank/whitespace values are treated as unset.
|
|
118
|
+
* fully-absent config yields the default account. Blank/whitespace values are treated as unset. An
|
|
119
|
+
* EXPLICITLY CONFIGURED name is emitted verbatim, with no identity spliced in — the user asked for
|
|
120
|
+
* that string; the identity decorates only the default. An unresolvable identity (`undefined`) falls
|
|
121
|
+
* back to the plain default name, never to a placeholder.
|
|
101
122
|
*
|
|
102
123
|
* Backend-agnostic: composed through the shared code path so BOTH the lean `GthLangChainAgent` and
|
|
103
124
|
* the deep `GthDeepAgent` inject it (the git-commit capability rides on `run_shell_command`, which
|
|
104
125
|
* both backends expose in code mode). Returns the note alone when there is no base prompt.
|
|
105
126
|
*/
|
|
106
|
-
export function appendCommitCoAuthorNote(systemPrompt, coAuthor) {
|
|
107
|
-
|
|
127
|
+
export function appendCommitCoAuthorNote(systemPrompt, coAuthor, modelIdentity) {
|
|
128
|
+
// EXT-83 — ONE place composes the name, so the parenthesised shape stays a one-line change. The
|
|
129
|
+
// identity decorates ONLY the default name: a configured name is the user's own string and is
|
|
130
|
+
// emitted verbatim, and an unresolved identity yields the bare default (never a placeholder).
|
|
131
|
+
const configuredName = coAuthor?.name?.trim();
|
|
132
|
+
const identity = modelIdentity?.identity?.trim();
|
|
133
|
+
const name = configuredName ||
|
|
134
|
+
(identity ? `${DEFAULT_COMMIT_CO_AUTHOR_NAME} (${identity})` : DEFAULT_COMMIT_CO_AUTHOR_NAME);
|
|
108
135
|
const email = coAuthor?.email?.trim() || DEFAULT_COMMIT_CO_AUTHOR_EMAIL;
|
|
109
136
|
const note = 'When you create a git commit, add EXACTLY this co-author trailer line (on its own line, at ' +
|
|
110
137
|
`the end of the commit message):\nCo-Authored-By: ${name} <${email}>\n` +
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'
|
|
114
|
-
'
|
|
138
|
+
'Emit at most this one Co-Authored-By trailer.\n' +
|
|
139
|
+
'Write the commit message in plain English: say what changed and why, and keep code, shell ' +
|
|
140
|
+
'commands, backticks and markup out of it.\n' +
|
|
141
|
+
'Never pass a commit message inline with the -m option: inside double quotes a POSIX shell ' +
|
|
142
|
+
'expands backtick and dollar-parenthesis constructs before git ever runs, so a message that ' +
|
|
143
|
+
'quotes code is executed as a command. Write the message to a file with the write_file tool ' +
|
|
144
|
+
'(never with shell echo or a heredoc, which put the same text back into a shell argument), ' +
|
|
145
|
+
'then commit it with git commit -F followed by that file path.';
|
|
115
146
|
return systemPrompt ? `${systemPrompt}\n\n${note}` : note;
|
|
116
147
|
}
|
|
117
148
|
/**
|
|
@@ -208,46 +239,13 @@ export function appendModelContextNote(systemPrompt, modelIdentity) {
|
|
|
208
239
|
export const MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER = 4000;
|
|
209
240
|
/** EXT-32: truncation marker appended when a server's instructions exceed the per-server cap. */
|
|
210
241
|
export const MCP_INSTRUCTIONS_TRUNCATION_MARKER = '… [truncated]';
|
|
211
|
-
/** EXT-32: the ONLY real structural delimiters in the composed block (emitted by this helper). */
|
|
212
|
-
const MCP_FENCE_BEGIN = '[BEGIN MCP SERVER-PROVIDED CONTEXT]';
|
|
213
|
-
const MCP_FENCE_END = '[END MCP SERVER-PROVIDED CONTEXT]';
|
|
214
|
-
/**
|
|
215
|
-
* EXT-32 (security): neutralize the block's structural delimiters inside UNTRUSTED server text.
|
|
216
|
-
*
|
|
217
|
-
* `getInstructions()` is fully server-controlled, so a malicious/compromised MCP server can emit
|
|
218
|
-
* text that forges the fence tokens or a per-server label — closing the fence early so its lines
|
|
219
|
-
* land OUTSIDE the visual boundary, or impersonating another server. Before fencing, we defang any
|
|
220
|
-
* occurrence in the server text of:
|
|
221
|
-
* - the fence tokens `[BEGIN|END MCP SERVER-PROVIDED CONTEXT]` (bracket run collapsed so they can
|
|
222
|
-
* no longer be read as the real delimiter), and
|
|
223
|
-
* - a per-server label line `--- Server: …` (the leading `---` run broken so it can't masquerade
|
|
224
|
-
* as one of our labels).
|
|
225
|
-
* After this, the ONLY real delimiters in the composed block are the ones this helper emits. The
|
|
226
|
-
* server NAME in our own label comes from trusted config keys, so it is not sanitized — only the
|
|
227
|
-
* server-supplied CONTENT is. Whitespace-tolerant matching (`\s+`, optional bracket padding, `-{3,}`)
|
|
228
|
-
* so trivial spacing variants can't slip a delimiter through.
|
|
229
|
-
*/
|
|
230
|
-
function defangMcpDelimiters(text) {
|
|
231
|
-
return text
|
|
232
|
-
.replace(/\[\s*(BEGIN|END)\s+MCP\s+SERVER-PROVIDED\s+CONTEXT\s*\]/gi, (_m, kw) => `(server text: ${kw.toUpperCase()} MCP SERVER-PROVIDED CONTEXT)`)
|
|
233
|
-
.replace(/-{3,}(\s*Server\s*:)/gi, '- - -$1');
|
|
234
|
-
}
|
|
235
242
|
/**
|
|
236
243
|
* EXT-32: cap server text at {@link MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER}, SURROGATE-SAFE.
|
|
237
244
|
*
|
|
238
|
-
*
|
|
239
|
-
* half-code-unit. If the cut would land between a high and low surrogate, back off one code unit so
|
|
240
|
-
* the pair is kept whole (dropped entirely rather than split). Appends the truncation marker only
|
|
241
|
-
* when text is actually clipped.
|
|
245
|
+
* Thin binding of the shared {@link capUntrustedText} to this block's own cap and marker.
|
|
242
246
|
*/
|
|
243
247
|
function capMcpText(text) {
|
|
244
|
-
|
|
245
|
-
return text;
|
|
246
|
-
let end = MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER;
|
|
247
|
-
const code = text.charCodeAt(end - 1);
|
|
248
|
-
if (code >= 0xd800 && code <= 0xdbff)
|
|
249
|
-
end -= 1; // don't split a surrogate pair
|
|
250
|
-
return `${text.slice(0, end).trimEnd()}\n${MCP_INSTRUCTIONS_TRUNCATION_MARKER}`;
|
|
248
|
+
return capUntrustedText(text, MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER, MCP_INSTRUCTIONS_TRUNCATION_MARKER);
|
|
251
249
|
}
|
|
252
250
|
/**
|
|
253
251
|
* EXT-32: append connected MCP servers' discovery `instructions` to the composed system prompt.
|
|
@@ -277,7 +275,7 @@ export function appendMcpServerInstructionsNote(systemPrompt, instructions) {
|
|
|
277
275
|
// SECURITY: defang the untrusted server text's structural delimiters BEFORE fencing, then cap
|
|
278
276
|
// (surrogate-safe). Order matters — defang first so a forged fence/label can't survive into the
|
|
279
277
|
// composed block; cap after so the final per-server size stays bounded.
|
|
280
|
-
const safe = capMcpText(
|
|
278
|
+
const safe = capMcpText(defangUntrustedDelimiters(text));
|
|
281
279
|
blocks.push(`--- Server: "${entry.server}" ---\n${safe}`);
|
|
282
280
|
}
|
|
283
281
|
if (blocks.length === 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"systemPromptNotes.js","sourceRoot":"","sources":["../../src/utils/systemPromptNotes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"systemPromptNotes.js","sourceRoot":"","sources":["../../src/utils/systemPromptNotes.ts"],"names":[],"mappings":"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"}
|
|
@@ -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.28",
|
|
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": {
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"@langchain/groq": "^1.3.0",
|
|
45
46
|
"@langchain/ollama": "^1.3.0",
|
|
46
47
|
"@langchain/openai": "^1.5.1",
|
|
48
|
+
"@langchain/openrouter": "^0.4.5",
|
|
47
49
|
"@langchain/xai": "^1.4.1"
|
|
48
50
|
},
|
|
49
51
|
"peerDependenciesMeta": {
|
|
@@ -65,6 +67,9 @@
|
|
|
65
67
|
"@langchain/openai": {
|
|
66
68
|
"optional": true
|
|
67
69
|
},
|
|
70
|
+
"@langchain/openrouter": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
68
73
|
"@langchain/xai": {
|
|
69
74
|
"optional": true
|
|
70
75
|
}
|