@gaunt-sloth/core 2.0.0-alpha.9 → 2.0.0-beta.0
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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* GS2-47 — a shared, reusable secret-redaction pass for diagnostic output.
|
|
4
|
+
*
|
|
5
|
+
* Built for `/debug-dump` ({@link file://./debugDump.ts}) but deliberately generic and
|
|
6
|
+
* dependency-free — it takes `env` as a PARAMETER rather than reading `process.env`, so the pass is
|
|
7
|
+
* pure, deterministic in tests, and reusable by GS2-48's crash-report handler (which hard-depends on
|
|
8
|
+
* this module). Nothing here reads ambient process state.
|
|
9
|
+
*
|
|
10
|
+
* DESIGN DECISION (settled by the coordinator, GS2-47): **PATTERN-ONLY** redaction — we do NOT scan
|
|
11
|
+
* for high-entropy strings. Rationale: pattern-only is safe to reason about and near-zero
|
|
12
|
+
* false-positive; entropy scanning catches marginally more but produces false-positive redactions
|
|
13
|
+
* the user has to squint through, degrading the dump's debug value. A high-entropy scanner is a
|
|
14
|
+
* DELIBERATELY-DEFERRED option (a config surface for it could be added later if ever warranted) and
|
|
15
|
+
* is intentionally NOT implemented here.
|
|
16
|
+
*
|
|
17
|
+
* Three techniques, in priority order:
|
|
18
|
+
* 1. [load-bearing] Known-secret VALUE substitution — collect the literal values of secret-named
|
|
19
|
+
* env vars + inline config secrets, then substitute every occurrence across ALL artifacts. This
|
|
20
|
+
* catches a leaked key wherever it surfaces (config, env, transcript, log) with no guessing.
|
|
21
|
+
* 2. Provider key/token PATTERNS — a tight, documented, prefix-anchored set of well-known key
|
|
22
|
+
* shapes plus explicit auth-header contexts. We never blanket-redact long alphanumeric strings.
|
|
23
|
+
* 3. Sensitive config-FIELD masking — mask the VALUES of secret-named config keys while preserving
|
|
24
|
+
* structure (the key stays; only the value becomes the marker).
|
|
25
|
+
*
|
|
26
|
+
* Fail-safe throughout: on any internal error the functions redact MORE (return the marker), never
|
|
27
|
+
* less — a redaction hiccup must never cause raw content to be emitted.
|
|
28
|
+
*/
|
|
29
|
+
/** The visible, greppable placeholder substituted for every redacted secret value. */
|
|
30
|
+
export const REDACTED = '<redacted>';
|
|
31
|
+
/**
|
|
32
|
+
* Minimum length for a collected VALUE to be treated as a substitutable secret literal (technique
|
|
33
|
+
* 1). Guards against redacting trivial values ("1", "on", a short model name) that happen to be the
|
|
34
|
+
* value of a secret-named env var — substituting those everywhere would gut the dump's debug value
|
|
35
|
+
* with false positives (exactly the trap the pattern-only decision avoids). Short sensitive *config*
|
|
36
|
+
* fields are still masked structurally by key name (technique 3), so nothing sensitive slips through
|
|
37
|
+
* this floor.
|
|
38
|
+
*/
|
|
39
|
+
const MIN_SECRET_LITERAL_LENGTH = 6;
|
|
40
|
+
/** Max recursion depth for the config walks — a guard against pathological/deep object graphs. */
|
|
41
|
+
const MAX_REDACT_DEPTH = 12;
|
|
42
|
+
/**
|
|
43
|
+
* env-var NAME shapes whose VALUE is a secret to substitute everywhere (technique 1): `*_API_KEY`,
|
|
44
|
+
* `*_TOKEN`, `*_SECRET`, `*_KEY`, and anything containing `PASSWORD`. Case-insensitive.
|
|
45
|
+
*/
|
|
46
|
+
const SECRET_ENV_NAME_RE = /(?:_API_KEY|_TOKEN|_SECRET|_KEY)$|PASSWORD/i;
|
|
47
|
+
/**
|
|
48
|
+
* Config-FIELD NAME shapes whose VALUE is masked in place (technique 3), matched case-insensitively
|
|
49
|
+
* at any depth. Mirrors `configCommand.ts`'s `redactConfigForPrint` set for cross-surface
|
|
50
|
+
* consistency. NOTE: `apiKeyEnvironmentVariable` also matches `api?key`, but it holds a var NAME
|
|
51
|
+
* (not a secret) and is what technique 1 reads to find the real key's value — so it is EXCLUDED via
|
|
52
|
+
* {@link NON_SECRET_KEY_NAMES}.
|
|
53
|
+
*/
|
|
54
|
+
const SECRET_KEY_RE = /(api[-_]?key|secret|token|password|passwd|authorization|bearer|credential)/i;
|
|
55
|
+
/** Field names that match {@link SECRET_KEY_RE} but must NOT be masked (they hold a var name). */
|
|
56
|
+
const NON_SECRET_KEY_NAMES = new Set(['apikeyenvironmentvariable']);
|
|
57
|
+
/**
|
|
58
|
+
* GS2-66 (residual 1) — building blocks for the `Authorization`-header VALUE grammar. GS2-54 bounded
|
|
59
|
+
* the value to `<scheme?> <one token>` where a token is {@link AUTH_TOKEN}; a credential with a char
|
|
60
|
+
* OUTSIDE that charset (`clientid:secret`, `token="…"`, `k=v,k=v`, Digest `response="…"`) left a raw
|
|
61
|
+
* tail beside the marker. These fragments extend the value across the STRUCTURED credential
|
|
62
|
+
* components (colon-joined `id:secret`, quoted `k="v"`, comma/semicolon-separated param lists) WITHOUT
|
|
63
|
+
* re-introducing the greedy prose-swallow GS2-47 removed: horizontal whitespace only (`[ \t]`, never
|
|
64
|
+
* a newline), a tight colon (no surrounding ws), and a param-continuation (`,` or `;`) whose atom must
|
|
65
|
+
* be *param-shaped* (contain `=` or be quoted) so `Authorization: … ; then we retried` keeps its prose.
|
|
66
|
+
*/
|
|
67
|
+
// A credential "word": the same token charset the SigV4 / standalone-`Bearer` rules already use.
|
|
68
|
+
const AUTH_TOKEN = String.raw `[A-Za-z0-9._~+/=-]+`;
|
|
69
|
+
// A quoted credential value (`"…"` / `'…'`) that never spans a newline — `k="v"` params + bare quotes.
|
|
70
|
+
const AUTH_QUOTED = String.raw `"[^"\n]*"|'[^'\n]*'`;
|
|
71
|
+
// One credential atom: a token optionally glued to a quoted value (`token="…"`), or a bare quote.
|
|
72
|
+
const AUTH_ATOM = `(?:${AUTH_TOKEN}(?:${AUTH_QUOTED})?|${AUTH_QUOTED})`;
|
|
73
|
+
// A colon-joined chain (`clientid:secret`); the colon is TIGHT (no surrounding whitespace) so a
|
|
74
|
+
// following `Header: value` pair or a prose colon on the same line is not chained in.
|
|
75
|
+
const AUTH_COLON_CHAIN = `${AUTH_ATOM}(?::${AUTH_ATOM})*`;
|
|
76
|
+
// A `,`- or `;`-separated PARAM continuation (`, key2=v2`; `;key2=v2`; Digest `, realm="…"`): the atom
|
|
77
|
+
// after the separator must be param-shaped — `key=…` or quoted — so a `,`/`;` followed by prose is NOT
|
|
78
|
+
// swallowed. (M1, GS2-71: `;` joined the accepted separators alongside `,`; the atom shape is unchanged,
|
|
79
|
+
// so `Authorization: … ; then we retried` still keeps its prose — same as the `,` case always has.)
|
|
80
|
+
const AUTH_PARAM = `(?:[A-Za-z0-9._~+/-]+=(?:${AUTH_QUOTED}|${AUTH_TOKEN})?|${AUTH_QUOTED})`;
|
|
81
|
+
// The full value: an optional scheme word (`Bearer` / `ApiKey` / `Digest` / …) + horizontal ws, then
|
|
82
|
+
// a colon-chain, then zero+ `,`- or `;`-separated params. The `(?!AWS4-HMAC-SHA256[ \t]+Credential=)`
|
|
83
|
+
// lookahead excludes ONLY a genuine SigV4 header (scheme immediately followed by `Credential=`) — its
|
|
84
|
+
// `SignedHeaders=…` is deliberately preserved, so it is owned solely by the Credential/Signature
|
|
85
|
+
// rule. The guard is narrow ON PURPOSE: a malformed/bare `AWS4-HMAC-SHA256 <token>` (no
|
|
86
|
+
// `Credential=`) still falls through here and IS redacted, so the "redact any scheme" contract holds.
|
|
87
|
+
const AUTH_VALUE = `(?!AWS4-HMAC-SHA256[ \\t]+Credential=)(?:${AUTH_TOKEN}[ \\t]+)?${AUTH_COLON_CHAIN}` +
|
|
88
|
+
`(?:[ \\t]*[,;][ \\t]*${AUTH_PARAM})*`;
|
|
89
|
+
// The header prefix (name + separator + optional opening quote), captured so `$1` is preserved. Its
|
|
90
|
+
// `\s*` gaps CAN include a newline (over-redacts a value on the next line — SAFE direction, never a
|
|
91
|
+
// leak); the VALUE above uses only horizontal ws and so never crosses a newline.
|
|
92
|
+
const AUTH_HEADER_PREFIX = String.raw `(\bAuthorization["']?\s*[:=]\s*["']?)`;
|
|
93
|
+
/**
|
|
94
|
+
* Provider key / auth-header PATTERNS (technique 2) — a tight, prefix-anchored set. Each entry is
|
|
95
|
+
* `[regex, replacement]`. Kept deliberately narrow (well-known key prefixes + explicit auth
|
|
96
|
+
* contexts); we do NOT blanket-redact long alphanumeric strings — that IS the entropy trap the
|
|
97
|
+
* pattern-only decision avoids. A long opaque token is only redacted when it sits in an auth context
|
|
98
|
+
* (`Bearer …` / `Authorization: …`), never on its own.
|
|
99
|
+
*/
|
|
100
|
+
const PROVIDER_PATTERNS = [
|
|
101
|
+
// OpenAI (`sk-…`) and Anthropic (`sk-ant-…`, also `sk-`-prefixed) secret keys.
|
|
102
|
+
[/\bsk-(?:ant-)?[A-Za-z0-9_-]{16,}/g, REDACTED],
|
|
103
|
+
// Google API keys (`AIza…`).
|
|
104
|
+
[/\bAIza[0-9A-Za-z_-]{16,}/g, REDACTED],
|
|
105
|
+
// xAI (`xai-…`).
|
|
106
|
+
[/\bxai-[A-Za-z0-9_-]{16,}/g, REDACTED],
|
|
107
|
+
// Groq (`gsk_…`).
|
|
108
|
+
[/\bgsk_[A-Za-z0-9_-]{16,}/g, REDACTED],
|
|
109
|
+
// GS2-54 (gap 2) — GitHub tokens. Classic/scoped PATs share the `gh[oprsu]_` prefix (personal
|
|
110
|
+
// `ghp_`, OAuth `gho_`, user-to-server `ghu_`, server-to-server `ghs_`, refresh `ghr_`); the value
|
|
111
|
+
// is base62 (no `_`). Prefix-anchored + a length floor, same tight style as the provider keys —
|
|
112
|
+
// NOT a blanket long-alphanumeric redaction (that IS the entropy trap avoided).
|
|
113
|
+
[/\bgh[oprsu]_[A-Za-z0-9]{16,}/g, REDACTED],
|
|
114
|
+
// GS2-54 (gap 2) — GitHub fine-grained PATs (`github_pat_…`); the value carries an inner `_`, so
|
|
115
|
+
// its charset includes `_`. Distinct prefix from the classic rule above (`github_` ≠ `gh[oprsu]_`).
|
|
116
|
+
[/\bgithub_pat_[A-Za-z0-9_]{16,}/g, REDACTED],
|
|
117
|
+
// GS2-54 (gap 2) + GS2-66 (residual 2) — credentials embedded in a URL. Redact the whole userinfo
|
|
118
|
+
// (`user:password@` AND the colon-less bare-token form `opaquetoken@`, e.g. a PAT in a clone URL),
|
|
119
|
+
// KEEP the scheme + host so the artifact stays debuggable. GS2-54 required a `:` in the userinfo
|
|
120
|
+
// (`[^\s/@:]+:[^\s/@]+@`), so `https://opaquetoken@host` slipped through; the charset now allows the
|
|
121
|
+
// userinfo to include an internal `:` (or none) and simply stops at `@`. Bounded: userinfo can't
|
|
122
|
+
// contain `/` or whitespace, so it never swallows the rest of the URL/line. A URL WITHOUT userinfo
|
|
123
|
+
// (`https://host:port/path`) has no `@` after the authority and is left untouched.
|
|
124
|
+
[/(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, `$1${REDACTED}@`],
|
|
125
|
+
// GS2-54 (gap 1) + GS2-66 (residual 1 & 4) — an `Authorization` header value
|
|
126
|
+
// (`Authorization: <scheme?> <credential>` / `"Authorization":"…"`): keep the header name +
|
|
127
|
+
// separator ($1), redact the value REGARDLESS OF SCHEME. Generalized beyond the old fixed
|
|
128
|
+
// `Bearer|Basic|Token|Digest|Negotiate` list so a non-standard scheme (`ApiKey <secret>`, an
|
|
129
|
+
// unknown scheme) no longer leaks its token beside the marker.
|
|
130
|
+
//
|
|
131
|
+
// GS2-66 extends the value across STRUCTURED credential components (see {@link AUTH_VALUE}):
|
|
132
|
+
// `clientid:secret`, `token="…"`, `k=v,k=v` / `k=v;k=v` lists and a Digest `response="…"` no longer
|
|
133
|
+
// leave a raw tail past the token charset. It still cannot swallow following PROSE: the credential
|
|
134
|
+
// body is horizontal-whitespace-only and a `,`/`;` param-continuation must be param-shaped (GS2-47
|
|
135
|
+
// invariant, preserved by GS2-71's M1 widening of the separator to `[,;]`).
|
|
136
|
+
//
|
|
137
|
+
// NEWLINE precision (residual 4): the *credential body* uses only `[ \t]` and so never crosses a
|
|
138
|
+
// newline — but the header-PREFIX `\s*` gaps CAN span `\n` (`Authorization:\n<token>`), which
|
|
139
|
+
// over-redacts a next-line value in the SAFE direction (never a leak), not something this rule
|
|
140
|
+
// guards against. An optional value-quote is consumed so JSON `"Authorization":"…"` works too.
|
|
141
|
+
//
|
|
142
|
+
// A genuine SigV4 header is EXCLUDED here via `(?!AWS4-HMAC-SHA256[ \t]+Credential=)` (were it
|
|
143
|
+
// consumed, its intentionally-kept `SignedHeaders=…` would be eaten): SigV4 is owned entirely by
|
|
144
|
+
// the next rule, which redacts its `Credential=…`/`Signature=…` and leaves the scheme +
|
|
145
|
+
// `SignedHeaders` visible. The guard is scoped to a real SigV4 shape so a malformed bare-token
|
|
146
|
+
// `AWS4-HMAC-SHA256 <token>` is NOT let through unredacted. Runs before the standalone-`Bearer`
|
|
147
|
+
// rule so a normal header collapses to a single marker.
|
|
148
|
+
[new RegExp(AUTH_HEADER_PREFIX + AUTH_VALUE, 'gi'), `$1${REDACTED}`],
|
|
149
|
+
// GS2-54 (gap 1) — AWS SigV4 authorization components. The header is comma-separated `key=value`
|
|
150
|
+
// pairs; redact the SENSITIVE `Credential=…` (access-key id) and `Signature=…` values wherever they
|
|
151
|
+
// appear (header value OR a presigned-URL `X-Amz-Credential=…&X-Amz-Signature=…` query), keeping the
|
|
152
|
+
// key name for debuggability. `SignedHeaders=…` is just header names and is intentionally NOT
|
|
153
|
+
// touched. The value charset excludes whitespace/comma/quote/`&`, so it stops at the component
|
|
154
|
+
// boundary and never swallows the next pair or trailing prose.
|
|
155
|
+
[/\b(Credential|Signature)=[A-Za-z0-9._~+/=-]+/gi, `$1=${REDACTED}`],
|
|
156
|
+
// A standalone `Bearer <token>` — keep the scheme word, redact only the credential. A long opaque
|
|
157
|
+
// token is redacted ONLY in this auth context, never on its own (that is the entropy trap avoided).
|
|
158
|
+
[/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, `$1${REDACTED}`],
|
|
159
|
+
];
|
|
160
|
+
/**
|
|
161
|
+
* Collect the literal secret VALUES to substitute everywhere (technique 1), from two sources:
|
|
162
|
+
* - process-env vars whose NAME matches {@link SECRET_ENV_NAME_RE}, PLUS the specific var(s) named
|
|
163
|
+
* by any `apiKeyEnvironmentVariable` in the config;
|
|
164
|
+
* - non-empty inline secret field values in the config (technique 3's field names), so a key pasted
|
|
165
|
+
* inline is scrubbed wherever it *also* surfaces (transcript, log, env), not only in config.json.
|
|
166
|
+
*
|
|
167
|
+
* `env` is a PARAMETER — never read ambiently — so the pass is pure, reusable (GS2-48) and
|
|
168
|
+
* deterministic in tests. Values shorter than {@link MIN_SECRET_LITERAL_LENGTH} are skipped. The
|
|
169
|
+
* result is returned longest-first so an overlapping-substring secret can't leave a shorter one
|
|
170
|
+
* partially intact. Never throws (a hostile config getter is swallowed — patterns + structural
|
|
171
|
+
* masking still apply).
|
|
172
|
+
*/
|
|
173
|
+
export function collectSecretValues(config, env) {
|
|
174
|
+
const values = new Set();
|
|
175
|
+
const add = (v) => {
|
|
176
|
+
if (typeof v === 'string' && v.length >= MIN_SECRET_LITERAL_LENGTH)
|
|
177
|
+
values.add(v);
|
|
178
|
+
};
|
|
179
|
+
// (a) env vars whose NAME looks secret → their VALUE is a literal to scrub.
|
|
180
|
+
try {
|
|
181
|
+
for (const [name, value] of Object.entries(env ?? {})) {
|
|
182
|
+
if (SECRET_ENV_NAME_RE.test(name))
|
|
183
|
+
add(value);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// ignore — a broken env object is not fatal to redaction.
|
|
188
|
+
}
|
|
189
|
+
// (b) walk the config for `apiKeyEnvironmentVariable` (→ read that env var's value) and inline
|
|
190
|
+
// secret-named field values.
|
|
191
|
+
const seen = new WeakSet();
|
|
192
|
+
const walk = (node, depth) => {
|
|
193
|
+
if (node === null || typeof node !== 'object' || depth > MAX_REDACT_DEPTH)
|
|
194
|
+
return;
|
|
195
|
+
if (seen.has(node))
|
|
196
|
+
return;
|
|
197
|
+
seen.add(node);
|
|
198
|
+
if (Array.isArray(node)) {
|
|
199
|
+
for (const item of node)
|
|
200
|
+
walk(item, depth + 1);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const [key, value] of Object.entries(node)) {
|
|
204
|
+
if (key.toLowerCase() === 'apikeyenvironmentvariable' && typeof value === 'string') {
|
|
205
|
+
add(env?.[value]); // resolve the NAMED env var's VALUE (the var name itself is not a secret)
|
|
206
|
+
}
|
|
207
|
+
else if (SECRET_KEY_RE.test(key) && !NON_SECRET_KEY_NAMES.has(key.toLowerCase())) {
|
|
208
|
+
add(value);
|
|
209
|
+
}
|
|
210
|
+
walk(value, depth + 1);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
walk(config, 0);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// fail safe: a hostile getter threw — we simply have fewer literals; patterns + structural
|
|
218
|
+
// masking still cover the artifacts. Never rethrow (redaction must not break the dump).
|
|
219
|
+
}
|
|
220
|
+
return [...values].sort((a, b) => b.length - a.length);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Redact a STRING: substitute every known literal secret value (technique 1, longest-first), then
|
|
224
|
+
* apply the provider key/auth patterns (technique 2). Never throws — on any error it returns the
|
|
225
|
+
* fully-withheld marker (fail safe: redact MORE, never emit the raw text). Applying it twice is
|
|
226
|
+
* safe (re-redacting already-redacted text is a no-op).
|
|
227
|
+
*/
|
|
228
|
+
export function redactText(text, secrets) {
|
|
229
|
+
try {
|
|
230
|
+
let out = text;
|
|
231
|
+
for (const secret of secrets) {
|
|
232
|
+
if (secret)
|
|
233
|
+
out = out.split(secret).join(REDACTED); // literal — no regex escaping needed
|
|
234
|
+
}
|
|
235
|
+
for (const [re, replacement] of PROVIDER_PATTERNS) {
|
|
236
|
+
out = out.replace(re, replacement);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return REDACTED; // fail safe
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Deep-redact an arbitrary value for serialization: mask the VALUES of secret-named fields
|
|
246
|
+
* (technique 3 — structure preserved: the key stays, only its value becomes the marker) and run
|
|
247
|
+
* {@link redactText} over every string leaf (techniques 1 + 2). Circular refs are broken; functions
|
|
248
|
+
* and bigints are rendered the way `safeStringify` would, so the result is JSON-safe. Pure — never
|
|
249
|
+
* mutates the input. `apiKeyEnvironmentVariable` is intentionally NOT masked (it is a var name).
|
|
250
|
+
*
|
|
251
|
+
* Intended for the CONFIG artifact (where field-name masking is wanted). Non-config artifacts use
|
|
252
|
+
* {@link redactText} over their stringified form instead (literal + pattern only), so a legitimate
|
|
253
|
+
* `token`/`secret`-named field in tool output is not blanket-masked.
|
|
254
|
+
*/
|
|
255
|
+
export function redactValue(value, secrets) {
|
|
256
|
+
const seen = new WeakSet();
|
|
257
|
+
const walk = (node, depth) => {
|
|
258
|
+
if (typeof node === 'string')
|
|
259
|
+
return redactText(node, secrets);
|
|
260
|
+
if (typeof node === 'function')
|
|
261
|
+
return `[Function: ${node.name || 'anonymous'}]`;
|
|
262
|
+
if (typeof node === 'bigint')
|
|
263
|
+
return node.toString();
|
|
264
|
+
if (node === null || typeof node !== 'object')
|
|
265
|
+
return node;
|
|
266
|
+
if (seen.has(node))
|
|
267
|
+
return '[Circular]';
|
|
268
|
+
if (depth > MAX_REDACT_DEPTH)
|
|
269
|
+
return '[Truncated]';
|
|
270
|
+
seen.add(node);
|
|
271
|
+
if (Array.isArray(node))
|
|
272
|
+
return node.map((item) => walk(item, depth + 1));
|
|
273
|
+
const out = {};
|
|
274
|
+
for (const [key, val] of Object.entries(node)) {
|
|
275
|
+
if (SECRET_KEY_RE.test(key) && !NON_SECRET_KEY_NAMES.has(key.toLowerCase())) {
|
|
276
|
+
out[key] = REDACTED; // mask the value, keep the key — shape is part of the debug signal
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
out[key] = walk(val, depth + 1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
};
|
|
284
|
+
return walk(value, 0);
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=redactSecrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redactSecrets.js","sourceRoot":"","sources":["../../src/utils/redactSecrets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared code-mode system-prompt augmentations (GS2-27).
|
|
3
|
+
*
|
|
4
|
+
* These notes describe capabilities of the agent itself — the opt-in `run_shell_command` tool and
|
|
5
|
+
* the real filesystem cwd — rather than of any one graph builder, which is why they live in core
|
|
6
|
+
* rather than beside the agent that composes them: a note composed inside one backend is a note
|
|
7
|
+
* the next backend silently loses, the drift GS2-21 fixed for the main prompt.
|
|
8
|
+
*/
|
|
9
|
+
import type { McpServerInstruction } from '#src/core/types.js';
|
|
10
|
+
import { type FilesystemToolsConfig } from '#src/config/filesystem-tools.js';
|
|
11
|
+
/**
|
|
12
|
+
* EXT-26: the platform-agnostic tail shared by both {@link appendOsShellNote} branches.
|
|
13
|
+
*
|
|
14
|
+
* The recurring failure mode on non-POSIX hosts is not just wrong command NAMES but shell
|
|
15
|
+
* REDIRECTION quoting: a grouped/multi-line `echo` redirect on cmd.exe reported success yet wrote
|
|
16
|
+
* a 0-byte file. So on every platform we steer file creation/mutation to the built-in
|
|
17
|
+
* `write_file`/`edit_file` tools (which never touch the shell's quoting) and keep each shell
|
|
18
|
+
* command a single line. Kept short — this is prompt text an LLM reads, not documentation.
|
|
19
|
+
*/
|
|
20
|
+
export declare const OS_SHELL_GUIDANCE: string;
|
|
21
|
+
/**
|
|
22
|
+
* EXT-26: append an OS + shell-dialect note to the composed code-mode system prompt.
|
|
23
|
+
*
|
|
24
|
+
* The model was never told its host OS or which shell `run_shell_command` uses, so on
|
|
25
|
+
* non-POSIX hosts it defaulted to POSIX idioms that fail (ran `ls` where cmd.exe has `dir`, a
|
|
26
|
+
* multi-line echo-redirect that wrote 0 bytes, a PowerShell here-string, `python -c` multi-line).
|
|
27
|
+
* This is ORTHOGONAL to the EXT-13/16/22 path-namespace notes: those say WHERE the model is (path
|
|
28
|
+
* form); this says WHAT shell it speaks (dialect). Backend-agnostic — the lean backend also exposes
|
|
29
|
+
* `run_shell_command`, so GS2-27 composes it in the shared path.
|
|
30
|
+
*
|
|
31
|
+
* The shell is derived from the SAME rule Node's `spawn(command, { shell: true })` uses — exactly
|
|
32
|
+
* how `run_shell_command` spawns (GthDevToolkit spawn) — so on `win32` it is cmd.exe (via
|
|
33
|
+
* `%ComSpec%`) and on POSIX it is `/bin/sh` (POSIX sh, NOT guaranteed bash). Computed from
|
|
34
|
+
* `process.platform` at call time so the text is correct per host. Returns the note alone when
|
|
35
|
+
* there is no base prompt. A single injection is authoritative (nothing in the base prompt
|
|
36
|
+
* contradicts shell dialect), so unlike EXT-22 no correction middleware is needed.
|
|
37
|
+
*/
|
|
38
|
+
export declare function appendOsShellNote(systemPrompt: string | undefined): string;
|
|
39
|
+
/**
|
|
40
|
+
* EXT-13 (part b): append a real-cwd / path-model note to the composed code-mode system prompt.
|
|
41
|
+
*
|
|
42
|
+
* Code mode runs in REAL-path mode, so the filesystem tools and `run_shell_command` share one
|
|
43
|
+
* real-absolute-path namespace rooted at `cwd`. The shared `.gsloth.code.md` prompt already tells
|
|
44
|
+
* the model "the current working directory is provided to you separately"; this note is what
|
|
45
|
+
* provides it — without it the model assumes `/` is cwd and hands `/`-rooted paths to the real-fs
|
|
46
|
+
* shell. The cwd is injected dynamically (never baked into the .md). Backend-agnostic — the lean
|
|
47
|
+
* backend also runs real-fs code sessions and previously received NO cwd value, so GS2-27 composes
|
|
48
|
+
* it in the shared path. Returns the note alone when there is no base prompt.
|
|
49
|
+
*/
|
|
50
|
+
export declare function appendCwdNote(systemPrompt: string | undefined, cwd: string): string;
|
|
51
|
+
/** GS2-35 — the configured Git co-author identity (both fields optional; each defaults on its own). */
|
|
52
|
+
export interface CommitCoAuthor {
|
|
53
|
+
name?: string;
|
|
54
|
+
email?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* GS2-35/EXT-83: append the commit-writing rules to the composed code-mode system prompt.
|
|
58
|
+
*
|
|
59
|
+
* Gaunt Sloth has **no dedicated git-commit tool** — the agent commits by calling
|
|
60
|
+
* `run_shell_command` with `git commit`, composing the message (including any trailer) itself. That
|
|
61
|
+
* leaves three things it must be told, and all three live here because all three are about
|
|
62
|
+
* committing:
|
|
63
|
+
*
|
|
64
|
+
* 1. **WHO the co-author is.** Left unguided, models emit their own model name from trained habit,
|
|
65
|
+
* which is factually wrong: the commit was produced by *Gaunt Sloth*, not by the model. The note
|
|
66
|
+
* states the exact trailer to emit. EXT-83 — rather than enumerate model names not to write (a
|
|
67
|
+
* denylist is stale the day a new vendor ships, and an enumeration beside a catch-all teaches the
|
|
68
|
+
* model that the list is the rule), the correct name is SUPPLIED: the resolved
|
|
69
|
+
* {@link ResolvedModelIdentity} decorates the DEFAULT name as `Gaunt Sloth (provider:model)`, so
|
|
70
|
+
* the real model is named while the authorship stays Gaunt Sloth's.
|
|
71
|
+
* 2. **HOW the message reaches git.** A commit message passed inline in a double-quoted shell
|
|
72
|
+
* argument is EXPANDED BY THE SHELL before git runs, so a message that quotes code the way
|
|
73
|
+
* ordinary technical prose does is executed as a command. The note states that mechanism rather
|
|
74
|
+
* than merely forbidding the construct — naming a construct without its mechanism has been
|
|
75
|
+
* measured not to work. A file path carries no shell metacharacters, so the file form removes the
|
|
76
|
+
* failure mode instead of asking the model to avoid it.
|
|
77
|
+
* 3. **HOW the change is staged.** Rule 2 leaves the message file untracked in the working tree at
|
|
78
|
+
* `git add` time, so an unscoped `git add -A` / `git add .` / `git commit -a` sweeps it into the
|
|
79
|
+
* commit and onto the pull request. The note states that mechanism, steers staging to named
|
|
80
|
+
* paths, and allows the last resort for a change too large to enumerate only once the message
|
|
81
|
+
* file is out of the tree — an escape hatch that kept the file would reintroduce exactly the
|
|
82
|
+
* defect the rule closes. It binds at `git add` time, MID-flow while the model is still acting,
|
|
83
|
+
* because the tidy-up step at the end is the one that gets dropped. The scratchpad sentence
|
|
84
|
+
* beside it names NO path: the write tool refuses any path outside the working folder at every
|
|
85
|
+
* approval mode, so naming one buys a refused call. EXT-97 is the interim mitigation; the
|
|
86
|
+
* mechanism that owns the file end to end is EXT-93.
|
|
87
|
+
*
|
|
88
|
+
* **The clause that names the writing tool is gated on `filesystem`** (EXT-84), through the one
|
|
89
|
+
* shared derivation {@link isWriteFileToolRegistered} — the same interpretation that decides which
|
|
90
|
+
* tools are actually registered. Naming an unregistered tool while forbidding both the shell
|
|
91
|
+
* fallback and the inline flag leaves the model NO compliant path, and its likeliest recovery is
|
|
92
|
+
* the inline form this note exists to prevent. So when the write tool is registered the note names
|
|
93
|
+
* it literally (the overwhelmingly common case, and a literal name is what makes the instruction
|
|
94
|
+
* actionable); when it is not, the note names no tool, keeps both prohibitions and the file form,
|
|
95
|
+
* and supplies the compliant path that remains: do not commit, and hand the message back to the
|
|
96
|
+
* user. That branch states no availability claim of its own — the backends read the same
|
|
97
|
+
* `filesystem` value but register filesystem tools differently, so a note asserting "you have no
|
|
98
|
+
* file-writing tool" could be flatly false on one of them.
|
|
99
|
+
*
|
|
100
|
+
* The note's prose carries **no backtick and no other markup** — including no angle-bracket
|
|
101
|
+
* placeholder: it is the one piece of guidance whose subject is how to write a commit message, so
|
|
102
|
+
* quoting its own examples in backticks would demonstrate the exact style rule 2 exists to stop, and
|
|
103
|
+
* an angle-bracket placeholder copied literally is itself a shell input redirect. The `<email>` of
|
|
104
|
+
* the trailer line is the exception the RFC form requires, and is scoped out of the scan.
|
|
105
|
+
*
|
|
106
|
+
* The identity is config-driven (`commit.coAuthor` in {@link import('#src/config/types.js').GthConfig}).
|
|
107
|
+
* Each field falls back INDEPENDENTLY to the Gaunt Sloth account
|
|
108
|
+
* ({@link DEFAULT_COMMIT_CO_AUTHOR_NAME} / {@link DEFAULT_COMMIT_CO_AUTHOR_EMAIL}) — so a partial
|
|
109
|
+
* override (name only, or a config that bypassed the loader) still yields a complete trailer, and a
|
|
110
|
+
* fully-absent config yields the default account. Blank/whitespace values are treated as unset. An
|
|
111
|
+
* EXPLICITLY CONFIGURED name is emitted verbatim, with no identity spliced in — the user asked for
|
|
112
|
+
* that string; the identity decorates only the default. An unresolvable identity (`undefined`) falls
|
|
113
|
+
* back to the plain default name, never to a placeholder.
|
|
114
|
+
*
|
|
115
|
+
* Backend-agnostic: composed through the shared code path, so any backend injects it (the
|
|
116
|
+
* git-commit capability rides on `run_shell_command`, exposed in code mode). Returns the note alone
|
|
117
|
+
* when there is no base prompt.
|
|
118
|
+
*/
|
|
119
|
+
export declare function appendCommitCoAuthorNote(systemPrompt: string | undefined, coAuthor?: CommitCoAuthor, modelIdentity?: ResolvedModelIdentity, filesystem?: FilesystemToolsConfig): string;
|
|
120
|
+
/**
|
|
121
|
+
* GS2-34/GS2-53 — the resolved active-model identity, as a STRUCTURED value.
|
|
122
|
+
*
|
|
123
|
+
* `hasProvider` is the authoritative "a real provider half was resolved" signal — carried
|
|
124
|
+
* explicitly rather than inferred from `identity.includes(':')`, because a bare model name can
|
|
125
|
+
* itself contain a colon (e.g. an Ollama/HF tag `gemma3:27b`) and would otherwise be mistaken for a
|
|
126
|
+
* `provider:model` string. It is true iff a non-empty provider (configured `type` OR a non-empty
|
|
127
|
+
* `_llmType()`) formed the leading `provider:` segment; false when `identity` is the bare model.
|
|
128
|
+
*/
|
|
129
|
+
export interface ResolvedModelIdentity {
|
|
130
|
+
/** `provider:model` when a provider resolved, otherwise the bare `model`. */
|
|
131
|
+
identity: string;
|
|
132
|
+
/** True iff a real provider formed the `provider:` segment (never merely a colon in the model). */
|
|
133
|
+
hasProvider: boolean;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* GS2-34: resolve the active model identity from the effective config, for injection into the
|
|
137
|
+
* system prompt by {@link appendModelContextNote}.
|
|
138
|
+
*
|
|
139
|
+
* Both halves are read the SAME way the rest of gsloth already surfaces the active model:
|
|
140
|
+
* - MODEL: `config.modelDisplayName` (the string the status line renders — set by the loader from
|
|
141
|
+
* `llm.model`), falling back to the live model's own `model` field.
|
|
142
|
+
* - PROVIDER: the configured `config.modelProviderType` (the raw `llm.type` the loader stashed —
|
|
143
|
+
* `openrouter`/`deepseek`/`xai`/`anthropic`/…) when present, otherwise the live LangChain
|
|
144
|
+
* model's `_llmType()` (the source the AG-UI `/info` endpoint reports). GS2-53 — preferring the
|
|
145
|
+
* configured `type` is what stops a provider being labelled with the client it happens to be
|
|
146
|
+
* built on: `_llmType()` is the model CLASS's own label and does not track the gth provider
|
|
147
|
+
* namespace, so `huggingface` (a `ChatOpenAI` aimed at the HF router) reports `openai`, and
|
|
148
|
+
* `google-genai` and `vertexai` both report `google`. It is also the safer source in general:
|
|
149
|
+
* `_llmType()` is the model class's opinion, while the `type` is
|
|
150
|
+
* the user's own declaration, so a provider swapped onto a different client (as `openrouter`
|
|
151
|
+
* was) keeps its configured label without this file needing to know. The `type` is absent for
|
|
152
|
+
* module configs (which
|
|
153
|
+
* hand us an already-built LLM), where we fall back to `_llmType()` unchanged. The MODEL half is
|
|
154
|
+
* always exact.
|
|
155
|
+
*
|
|
156
|
+
* Returns `{ identity: 'provider:model', hasProvider: true }` when both resolve, `{ identity:
|
|
157
|
+
* 'model', hasProvider: false }` when only the model resolves, and `undefined` when the model is
|
|
158
|
+
* unknown (a provider with no model is not a usable identity) — in which case {@link
|
|
159
|
+
* appendModelContextNote} injects nothing, leaving the prompt exactly as before. `hasProvider` is
|
|
160
|
+
* the authoritative provider-present flag (GS2-53), so a bare model whose NAME contains a colon
|
|
161
|
+
* (e.g. `gemma3:27b`) is correctly reported as `hasProvider: false`. `_llmType()` is called
|
|
162
|
+
* defensively (guarded) so a provider whose accessor throws can never break prompt assembly (and is
|
|
163
|
+
* skipped entirely when a configured `type` is present).
|
|
164
|
+
*/
|
|
165
|
+
export declare function resolveModelIdentity(config: {
|
|
166
|
+
llm?: {
|
|
167
|
+
_llmType?: () => string;
|
|
168
|
+
model?: string;
|
|
169
|
+
};
|
|
170
|
+
modelDisplayName?: string;
|
|
171
|
+
modelProviderType?: string;
|
|
172
|
+
} | null | undefined): ResolvedModelIdentity | undefined;
|
|
173
|
+
/**
|
|
174
|
+
* GS2-34: append the active model-identity note to the composed system prompt.
|
|
175
|
+
*
|
|
176
|
+
* The agent otherwise has no reliable knowledge of which `provider:model` is serving it, so it
|
|
177
|
+
* cannot answer "what model are you?" accurately or reason about its own capabilities/limits. This
|
|
178
|
+
* injects a single first-party line naming the resolved identity (see {@link resolveModelIdentity}).
|
|
179
|
+
*
|
|
180
|
+
* Injected in EVERY mode (chat/ask/code/exec), NOT gated to `code` like the cwd/os-shell/commit
|
|
181
|
+
* notes: "which model are you?" can be asked in any session, so the identity must be visible
|
|
182
|
+
* everywhere. Config-gated by `injectModelContext` (default ON): a caller passes an `undefined`
|
|
183
|
+
* `modelIdentity` — because the config opted out (`injectModelContext: false`) or because no model
|
|
184
|
+
* could be resolved — and the base prompt is returned UNCHANGED (no line), preserving the current
|
|
185
|
+
* prompt byte-for-byte.
|
|
186
|
+
*
|
|
187
|
+
* A short capability note from the GS2-6 model catalog is a DEFERRED follow-up (GS2-6 has not
|
|
188
|
+
* landed): this injects the bare `provider:model` identity only.
|
|
189
|
+
*/
|
|
190
|
+
export declare function appendModelContextNote(systemPrompt: string | undefined, modelIdentity: ResolvedModelIdentity | undefined): string | undefined;
|
|
191
|
+
/**
|
|
192
|
+
* EXT-32: hard per-server cap on injected MCP instruction length.
|
|
193
|
+
*
|
|
194
|
+
* A connected MCP server could advertise a very long `instructions` string that would then ride
|
|
195
|
+
* along in EVERY turn's system prompt and bloat context. This is a simple defensive constant (no
|
|
196
|
+
* config-schema plumbing — that stays out of this node): text beyond the cap is dropped and a
|
|
197
|
+
* truncation marker is appended so the model knows it was clipped. Generous enough that realistic
|
|
198
|
+
* server instructions pass through untouched.
|
|
199
|
+
*/
|
|
200
|
+
export declare const MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER = 4000;
|
|
201
|
+
/** EXT-32: truncation marker appended when a server's instructions exceed the per-server cap. */
|
|
202
|
+
export declare const MCP_INSTRUCTIONS_TRUNCATION_MARKER = "\u2026 [truncated]";
|
|
203
|
+
/**
|
|
204
|
+
* EXT-32: append connected MCP servers' discovery `instructions` to the composed system prompt.
|
|
205
|
+
*
|
|
206
|
+
* Each connected MCP server may return an `instructions` string in its `initialize` handshake that
|
|
207
|
+
* describes how to use its tools. We surface those to the model — but the text is **server-supplied
|
|
208
|
+
* and therefore a prompt-injection surface**, so it is:
|
|
209
|
+
* - fenced in an explicit `[BEGIN/END MCP SERVER-PROVIDED CONTEXT]` block (never blended into
|
|
210
|
+
* first-party prompt text),
|
|
211
|
+
* - labelled per server (`--- Server: "name" ---`) so the model can attribute each block, and
|
|
212
|
+
* - bracketed by a leading framing line AND a trailing first-party reassertion, so the LAST thing
|
|
213
|
+
* the model reads is gsloth's own authority, not the server text — server instructions may not
|
|
214
|
+
* override the system instructions, safety rules, or the user's directives.
|
|
215
|
+
*
|
|
216
|
+
* Backend-agnostic: composed through the shared path, so any backend injects it.
|
|
217
|
+
* Empty/absent contributes NOTHING: when no server supplied
|
|
218
|
+
* (non-whitespace) instructions the base prompt is returned unchanged — no empty header, no dangling
|
|
219
|
+
* label. Each server's text is trimmed and capped at {@link MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER}.
|
|
220
|
+
* Returns the block alone when there is no base prompt.
|
|
221
|
+
*/
|
|
222
|
+
export declare function appendMcpServerInstructionsNote(systemPrompt: string | undefined, instructions: McpServerInstruction[] | undefined): string | undefined;
|