@graphorin/agent 0.5.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/errors/index.d.ts +170 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +204 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/evaluator-optimizer/index.d.ts +91 -0
- package/dist/evaluator-optimizer/index.d.ts.map +1 -0
- package/dist/evaluator-optimizer/index.js +85 -0
- package/dist/evaluator-optimizer/index.js.map +1 -0
- package/dist/factory.d.ts +13 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +1853 -0
- package/dist/factory.js.map +1 -0
- package/dist/fallback/index.d.ts +52 -0
- package/dist/fallback/index.d.ts.map +1 -0
- package/dist/fallback/index.js +53 -0
- package/dist/fallback/index.js.map +1 -0
- package/dist/fanout/index.d.ts +142 -0
- package/dist/fanout/index.d.ts.map +1 -0
- package/dist/fanout/index.js +252 -0
- package/dist/fanout/index.js.map +1 -0
- package/dist/filters/index.d.ts +137 -0
- package/dist/filters/index.d.ts.map +1 -0
- package/dist/filters/index.js +273 -0
- package/dist/filters/index.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/ids.js +46 -0
- package/dist/internal/ids.js.map +1 -0
- package/dist/internal/usage-accumulator.js +62 -0
- package/dist/internal/usage-accumulator.js.map +1 -0
- package/dist/lateral-leak/causality-monitor.d.ts +97 -0
- package/dist/lateral-leak/causality-monitor.d.ts.map +1 -0
- package/dist/lateral-leak/causality-monitor.js +139 -0
- package/dist/lateral-leak/causality-monitor.js.map +1 -0
- package/dist/lateral-leak/index.d.ts +4 -0
- package/dist/lateral-leak/index.js +5 -0
- package/dist/lateral-leak/merge-guard.d.ts +89 -0
- package/dist/lateral-leak/merge-guard.d.ts.map +1 -0
- package/dist/lateral-leak/merge-guard.js +65 -0
- package/dist/lateral-leak/merge-guard.js.map +1 -0
- package/dist/lateral-leak/protocol-guard.d.ts +76 -0
- package/dist/lateral-leak/protocol-guard.d.ts.map +1 -0
- package/dist/lateral-leak/protocol-guard.js +147 -0
- package/dist/lateral-leak/protocol-guard.js.map +1 -0
- package/dist/preferred-model/index.d.ts +53 -0
- package/dist/preferred-model/index.d.ts.map +1 -0
- package/dist/preferred-model/index.js +141 -0
- package/dist/preferred-model/index.js.map +1 -0
- package/dist/progress/index.d.ts +62 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/index.js +150 -0
- package/dist/progress/index.js.map +1 -0
- package/dist/run-state/index.d.ts +152 -0
- package/dist/run-state/index.d.ts.map +1 -0
- package/dist/run-state/index.js +311 -0
- package/dist/run-state/index.js.map +1 -0
- package/dist/tooling/adapters.js +154 -0
- package/dist/tooling/adapters.js.map +1 -0
- package/dist/tooling/catalogue.js +37 -0
- package/dist/tooling/catalogue.js.map +1 -0
- package/dist/tooling/dataflow.js +99 -0
- package/dist/tooling/dataflow.js.map +1 -0
- package/dist/tooling/registry-build.js +85 -0
- package/dist/tooling/registry-build.js.map +1 -0
- package/dist/types.d.ts +413 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +115 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
//#region src/lateral-leak/causality-monitor.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default denial-pattern catalogue. The agent runtime extends this
|
|
4
|
+
* list when the operator supplies their own patterns.
|
|
5
|
+
*
|
|
6
|
+
* @stable
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_DENIAL_PATTERNS = [
|
|
9
|
+
/SecretAccessDenied/i,
|
|
10
|
+
/ToolApprovalDenied/i,
|
|
11
|
+
/HandoffPermissionDenied/i,
|
|
12
|
+
/SandboxViolation/i
|
|
13
|
+
];
|
|
14
|
+
/** Default chain depth when not specified. */
|
|
15
|
+
const DEFAULT_MAX_CHAIN_DEPTH = 32;
|
|
16
|
+
/**
|
|
17
|
+
* In-memory primitive instantiated per `RunContext`. Bounded-depth
|
|
18
|
+
* append discipline keeps the memory footprint trivial even on long
|
|
19
|
+
* runs.
|
|
20
|
+
*
|
|
21
|
+
* @stable
|
|
22
|
+
*/
|
|
23
|
+
var CausalityMonitor = class {
|
|
24
|
+
strictness;
|
|
25
|
+
maxChainDepth;
|
|
26
|
+
denialPatterns;
|
|
27
|
+
auditAllChains;
|
|
28
|
+
#chain = [];
|
|
29
|
+
constructor(cfg) {
|
|
30
|
+
this.strictness = cfg.strictness;
|
|
31
|
+
this.maxChainDepth = cfg.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH;
|
|
32
|
+
this.denialPatterns = cfg.denialPatterns ? [...DEFAULT_DENIAL_PATTERNS, ...cfg.denialPatterns] : DEFAULT_DENIAL_PATTERNS;
|
|
33
|
+
this.auditAllChains = cfg.auditAllChains ?? false;
|
|
34
|
+
}
|
|
35
|
+
/** Snapshot the current causality chain. */
|
|
36
|
+
get chain() {
|
|
37
|
+
return this.#chain.slice();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Append an entry to the causality chain, dropping the oldest
|
|
41
|
+
* when the chain exceeds `maxChainDepth`. Bounded-length, no PII,
|
|
42
|
+
* no secret values — entries are short opaque strings like
|
|
43
|
+
* `tool:slack-notify`, `tool.error:SecretAccessDenied`,
|
|
44
|
+
* `subagent:research-east`, `compaction:auto-trigger`.
|
|
45
|
+
*/
|
|
46
|
+
recordCall(entry) {
|
|
47
|
+
if (this.strictness === "off") return;
|
|
48
|
+
if (entry.length === 0) return;
|
|
49
|
+
this.#chain.push(entry);
|
|
50
|
+
if (this.#chain.length > this.maxChainDepth) this.#chain.shift();
|
|
51
|
+
}
|
|
52
|
+
/** Reset the chain — e.g. on `agent.run` boundary. */
|
|
53
|
+
reset() {
|
|
54
|
+
this.#chain = [];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Inspect a candidate assistant-visible string and return whether
|
|
58
|
+
* the lateral-leak defense should fire. Pure decision based on
|
|
59
|
+
* the current chain + the operator-extensible denial patterns.
|
|
60
|
+
*/
|
|
61
|
+
checkMessage(content) {
|
|
62
|
+
if (this.strictness === "off") return {
|
|
63
|
+
leakDetected: false,
|
|
64
|
+
severity: "info",
|
|
65
|
+
causalityChain: [],
|
|
66
|
+
decision: "detect",
|
|
67
|
+
vector: "causality-laundering"
|
|
68
|
+
};
|
|
69
|
+
if (!this.#chain.some((entry) => this.denialPatterns.some((pat) => pat.test(entry)))) return {
|
|
70
|
+
leakDetected: false,
|
|
71
|
+
severity: "info",
|
|
72
|
+
causalityChain: this.chain,
|
|
73
|
+
decision: "detect",
|
|
74
|
+
vector: "causality-laundering"
|
|
75
|
+
};
|
|
76
|
+
let matchedPattern;
|
|
77
|
+
for (const pat of this.denialPatterns) if (pat.test(content)) {
|
|
78
|
+
matchedPattern = pat.source;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (!(matchedPattern !== void 0)) return {
|
|
82
|
+
leakDetected: false,
|
|
83
|
+
severity: "info",
|
|
84
|
+
causalityChain: this.chain,
|
|
85
|
+
decision: "detect",
|
|
86
|
+
vector: "causality-laundering"
|
|
87
|
+
};
|
|
88
|
+
switch (this.strictness) {
|
|
89
|
+
case "detect": return {
|
|
90
|
+
leakDetected: true,
|
|
91
|
+
severity: "info",
|
|
92
|
+
causalityChain: this.chain,
|
|
93
|
+
...matchedPattern !== void 0 ? { matchedPattern } : {},
|
|
94
|
+
decision: "detect",
|
|
95
|
+
vector: "causality-laundering"
|
|
96
|
+
};
|
|
97
|
+
case "detect-and-flag": return {
|
|
98
|
+
leakDetected: true,
|
|
99
|
+
severity: "warn",
|
|
100
|
+
causalityChain: this.chain,
|
|
101
|
+
...matchedPattern !== void 0 ? { matchedPattern } : {},
|
|
102
|
+
decision: "flag",
|
|
103
|
+
vector: "causality-laundering"
|
|
104
|
+
};
|
|
105
|
+
case "detect-and-block": return {
|
|
106
|
+
leakDetected: true,
|
|
107
|
+
severity: "block",
|
|
108
|
+
causalityChain: this.chain,
|
|
109
|
+
...matchedPattern !== void 0 ? { matchedPattern } : {},
|
|
110
|
+
decision: "block",
|
|
111
|
+
vector: "causality-laundering"
|
|
112
|
+
};
|
|
113
|
+
default:
|
|
114
|
+
this.strictness;
|
|
115
|
+
return {
|
|
116
|
+
leakDetected: false,
|
|
117
|
+
severity: "info",
|
|
118
|
+
causalityChain: this.chain,
|
|
119
|
+
decision: "detect",
|
|
120
|
+
vector: "causality-laundering"
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Drain the chain to the audit log on `agent.run` completion or
|
|
126
|
+
* `agent.abort`. The runtime supplies the audit emitter — the
|
|
127
|
+
* primitive itself is storage-agnostic.
|
|
128
|
+
*/
|
|
129
|
+
flush(reason) {
|
|
130
|
+
return {
|
|
131
|
+
chain: this.chain,
|
|
132
|
+
reason
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
//#endregion
|
|
138
|
+
export { CausalityMonitor, DEFAULT_DENIAL_PATTERNS, DEFAULT_MAX_CHAIN_DEPTH };
|
|
139
|
+
//# sourceMappingURL=causality-monitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"causality-monitor.js","names":["DEFAULT_DENIAL_PATTERNS: ReadonlyArray<RegExp>","#chain","matchedPattern: string | undefined"],"sources":["../../src/lateral-leak/causality-monitor.ts"],"sourcesContent":["/**\n * `CausalityMonitor` — lateral-leak defense primitive that maintains\n * a per-`RunContext` `causalityChain` of bounded depth and refuses\n * to let an assistant message reference information about a denied\n * earlier action.\n *\n * The monitor is opt-in: agents declare `causalityMonitor:\n * { strictness, denialPatterns?, ... }` on `createAgent({...})`.\n * When the field is absent, the runtime instantiates a no-op monitor\n * that records nothing and never flags.\n *\n * @packageDocumentation\n */\n\nimport type { LateralLeakVector } from '@graphorin/core';\n\n/**\n * Operator-tunable strictness level. Default `'detect-and-flag'`\n * for cloud-tier providers; `'detect'` for loopback providers;\n * `'off'` for v0.1-alpha backward compatibility.\n *\n * @stable\n */\nexport type CausalityMonitorStrictness = 'off' | 'detect' | 'detect-and-flag' | 'detect-and-block';\n\n/**\n * Per-agent configuration accepted by `createAgent({ causalityMonitor })`.\n *\n * @stable\n */\nexport interface CausalityMonitorConfig {\n readonly strictness: CausalityMonitorStrictness;\n /** Maximum depth of the chain. Default `32`. */\n readonly maxChainDepth?: number;\n /** Operator-extensible denial patterns. */\n readonly denialPatterns?: ReadonlyArray<RegExp>;\n /**\n * When `true`, emit the chain on every `checkMessage(...)` call\n * (high-cardinality; opt-in for compliance audits). Default\n * `false` — only emit on detected leaks.\n */\n readonly auditAllChains?: boolean;\n}\n\n/**\n * Default denial-pattern catalogue. The agent runtime extends this\n * list when the operator supplies their own patterns.\n *\n * @stable\n */\nexport const DEFAULT_DENIAL_PATTERNS: ReadonlyArray<RegExp> = [\n /SecretAccessDenied/i,\n /ToolApprovalDenied/i,\n /HandoffPermissionDenied/i,\n /SandboxViolation/i,\n];\n\n/** Default chain depth when not specified. */\nexport const DEFAULT_MAX_CHAIN_DEPTH = 32;\n\n/**\n * Result returned by {@link CausalityMonitor.checkMessage}.\n *\n * @stable\n */\nexport interface CausalityMonitorCheck {\n readonly leakDetected: boolean;\n readonly severity: 'info' | 'warn' | 'block';\n readonly causalityChain: ReadonlyArray<string>;\n readonly matchedPattern?: string;\n readonly decision: 'detect' | 'flag' | 'strip' | 'block';\n readonly vector: LateralLeakVector;\n}\n\n/**\n * In-memory primitive instantiated per `RunContext`. Bounded-depth\n * append discipline keeps the memory footprint trivial even on long\n * runs.\n *\n * @stable\n */\nexport class CausalityMonitor {\n readonly strictness: CausalityMonitorStrictness;\n readonly maxChainDepth: number;\n readonly denialPatterns: ReadonlyArray<RegExp>;\n readonly auditAllChains: boolean;\n #chain: string[] = [];\n\n constructor(cfg: CausalityMonitorConfig) {\n this.strictness = cfg.strictness;\n this.maxChainDepth = cfg.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH;\n this.denialPatterns = cfg.denialPatterns\n ? [...DEFAULT_DENIAL_PATTERNS, ...cfg.denialPatterns]\n : DEFAULT_DENIAL_PATTERNS;\n this.auditAllChains = cfg.auditAllChains ?? false;\n }\n\n /** Snapshot the current causality chain. */\n get chain(): ReadonlyArray<string> {\n return this.#chain.slice();\n }\n\n /**\n * Append an entry to the causality chain, dropping the oldest\n * when the chain exceeds `maxChainDepth`. Bounded-length, no PII,\n * no secret values — entries are short opaque strings like\n * `tool:slack-notify`, `tool.error:SecretAccessDenied`,\n * `subagent:research-east`, `compaction:auto-trigger`.\n */\n recordCall(entry: string): void {\n if (this.strictness === 'off') return;\n if (entry.length === 0) return;\n this.#chain.push(entry);\n if (this.#chain.length > this.maxChainDepth) {\n this.#chain.shift();\n }\n }\n\n /** Reset the chain — e.g. on `agent.run` boundary. */\n reset(): void {\n this.#chain = [];\n }\n\n /**\n * Inspect a candidate assistant-visible string and return whether\n * the lateral-leak defense should fire. Pure decision based on\n * the current chain + the operator-extensible denial patterns.\n */\n checkMessage(content: string): CausalityMonitorCheck {\n if (this.strictness === 'off') {\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: [],\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n const chainHasDenial = this.#chain.some((entry) =>\n this.denialPatterns.some((pat) => pat.test(entry)),\n );\n if (!chainHasDenial) {\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: this.chain,\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n let matchedPattern: string | undefined;\n for (const pat of this.denialPatterns) {\n if (pat.test(content)) {\n matchedPattern = pat.source;\n break;\n }\n }\n const leakDetected = matchedPattern !== undefined;\n if (!leakDetected) {\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: this.chain,\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n switch (this.strictness) {\n case 'detect':\n return {\n leakDetected: true,\n severity: 'info',\n causalityChain: this.chain,\n ...(matchedPattern !== undefined ? { matchedPattern } : {}),\n decision: 'detect',\n vector: 'causality-laundering',\n };\n case 'detect-and-flag':\n return {\n leakDetected: true,\n severity: 'warn',\n causalityChain: this.chain,\n ...(matchedPattern !== undefined ? { matchedPattern } : {}),\n decision: 'flag',\n vector: 'causality-laundering',\n };\n case 'detect-and-block':\n return {\n leakDetected: true,\n severity: 'block',\n causalityChain: this.chain,\n ...(matchedPattern !== undefined ? { matchedPattern } : {}),\n decision: 'block',\n vector: 'causality-laundering',\n };\n default: {\n const _exhaustive: never = this.strictness;\n void _exhaustive;\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: this.chain,\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n }\n }\n\n /**\n * Drain the chain to the audit log on `agent.run` completion or\n * `agent.abort`. The runtime supplies the audit emitter — the\n * primitive itself is storage-agnostic.\n */\n flush(reason: 'agent.run.complete' | 'agent.abort'): {\n readonly chain: ReadonlyArray<string>;\n readonly reason: 'agent.run.complete' | 'agent.abort';\n } {\n return { chain: this.chain, reason };\n }\n}\n"],"mappings":";;;;;;;AAkDA,MAAaA,0BAAiD;CAC5D;CACA;CACA;CACA;CACD;;AAGD,MAAa,0BAA0B;;;;;;;;AAuBvC,IAAa,mBAAb,MAA8B;CAC5B,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAS;CACT,SAAmB,EAAE;CAErB,YAAY,KAA6B;AACvC,OAAK,aAAa,IAAI;AACtB,OAAK,gBAAgB,IAAI,iBAAiB;AAC1C,OAAK,iBAAiB,IAAI,iBACtB,CAAC,GAAG,yBAAyB,GAAG,IAAI,eAAe,GACnD;AACJ,OAAK,iBAAiB,IAAI,kBAAkB;;;CAI9C,IAAI,QAA+B;AACjC,SAAO,MAAKC,MAAO,OAAO;;;;;;;;;CAU5B,WAAW,OAAqB;AAC9B,MAAI,KAAK,eAAe,MAAO;AAC/B,MAAI,MAAM,WAAW,EAAG;AACxB,QAAKA,MAAO,KAAK,MAAM;AACvB,MAAI,MAAKA,MAAO,SAAS,KAAK,cAC5B,OAAKA,MAAO,OAAO;;;CAKvB,QAAc;AACZ,QAAKA,QAAS,EAAE;;;;;;;CAQlB,aAAa,SAAwC;AACnD,MAAI,KAAK,eAAe,MACtB,QAAO;GACL,cAAc;GACd,UAAU;GACV,gBAAgB,EAAE;GAClB,UAAU;GACV,QAAQ;GACT;AAKH,MAAI,CAHmB,MAAKA,MAAO,MAAM,UACvC,KAAK,eAAe,MAAM,QAAQ,IAAI,KAAK,MAAM,CAAC,CACnD,CAEC,QAAO;GACL,cAAc;GACd,UAAU;GACV,gBAAgB,KAAK;GACrB,UAAU;GACV,QAAQ;GACT;EAEH,IAAIC;AACJ,OAAK,MAAM,OAAO,KAAK,eACrB,KAAI,IAAI,KAAK,QAAQ,EAAE;AACrB,oBAAiB,IAAI;AACrB;;AAIJ,MAAI,EADiB,mBAAmB,QAEtC,QAAO;GACL,cAAc;GACd,UAAU;GACV,gBAAgB,KAAK;GACrB,UAAU;GACV,QAAQ;GACT;AAEH,UAAQ,KAAK,YAAb;GACE,KAAK,SACH,QAAO;IACL,cAAc;IACd,UAAU;IACV,gBAAgB,KAAK;IACrB,GAAI,mBAAmB,SAAY,EAAE,gBAAgB,GAAG,EAAE;IAC1D,UAAU;IACV,QAAQ;IACT;GACH,KAAK,kBACH,QAAO;IACL,cAAc;IACd,UAAU;IACV,gBAAgB,KAAK;IACrB,GAAI,mBAAmB,SAAY,EAAE,gBAAgB,GAAG,EAAE;IAC1D,UAAU;IACV,QAAQ;IACT;GACH,KAAK,mBACH,QAAO;IACL,cAAc;IACd,UAAU;IACV,gBAAgB,KAAK;IACrB,GAAI,mBAAmB,SAAY,EAAE,gBAAgB,GAAG,EAAE;IAC1D,UAAU;IACV,QAAQ;IACT;GACH;AAC6B,SAAK;AAEhC,WAAO;KACL,cAAc;KACd,UAAU;KACV,gBAAgB,KAAK;KACrB,UAAU;KACV,QAAQ;KACT;;;;;;;;CAUP,MAAM,QAGJ;AACA,SAAO;GAAE,OAAO,KAAK;GAAO;GAAQ"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ChildTrustInput, ContentOriginKind, MergeBiasDecision, MergeGuardConfig, TrustClass, computeSourceTrust, evaluateMerge } from "./merge-guard.js";
|
|
2
|
+
import { CausalityMonitor, CausalityMonitorCheck, CausalityMonitorConfig, CausalityMonitorStrictness, DEFAULT_DENIAL_PATTERNS, DEFAULT_MAX_CHAIN_DEPTH } from "./causality-monitor.js";
|
|
3
|
+
import { GuardOutcome, ProtocolBoundary, ProtocolEscapePolicy, ProtocolGuardConfig, guardOutboundContent, resolvePolicy } from "./protocol-guard.js";
|
|
4
|
+
export { CausalityMonitor, type CausalityMonitorCheck, type CausalityMonitorConfig, type CausalityMonitorStrictness, type ChildTrustInput, type ContentOriginKind, DEFAULT_DENIAL_PATTERNS, DEFAULT_MAX_CHAIN_DEPTH, type GuardOutcome, type MergeBiasDecision, type MergeGuardConfig, type ProtocolBoundary, type ProtocolEscapePolicy, type ProtocolGuardConfig, type TrustClass, computeSourceTrust, evaluateMerge, guardOutboundContent, resolvePolicy };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { computeSourceTrust, evaluateMerge } from "./merge-guard.js";
|
|
2
|
+
import { CausalityMonitor, DEFAULT_DENIAL_PATTERNS, DEFAULT_MAX_CHAIN_DEPTH } from "./causality-monitor.js";
|
|
3
|
+
import { guardOutboundContent, resolvePolicy } from "./protocol-guard.js";
|
|
4
|
+
|
|
5
|
+
export { CausalityMonitor, DEFAULT_DENIAL_PATTERNS, DEFAULT_MAX_CHAIN_DEPTH, computeSourceTrust, evaluateMerge, guardOutboundContent, resolvePolicy };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//#region src/lateral-leak/merge-guard.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `MergeAgentSidewaysInjectionGuard` — layered on top of the
|
|
4
|
+
* `Agent.fanOut(...)` `'judge-merge'` strategy. Computes a
|
|
5
|
+
* per-child `sourceTrust` score in `[0.0, 1.0]` and flags merges
|
|
6
|
+
* where a low-trust child's contribution-weight exceeds the
|
|
7
|
+
* configured `maxLowTrustWeight` threshold.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Trust-class baseline used by the guard's `sourceTrust`
|
|
13
|
+
* computation. Mirrors the DEC-149 trust-class taxonomy from
|
|
14
|
+
* `@graphorin/provider`.
|
|
15
|
+
*
|
|
16
|
+
* @stable
|
|
17
|
+
*/
|
|
18
|
+
type TrustClass = 'loopback' | 'public-tls' | 'public-mtls' | 'untrusted-skill';
|
|
19
|
+
/**
|
|
20
|
+
* Tool-source provenance multiplier. Mirrors the `ContentOrigin`
|
|
21
|
+
* annotation from `@graphorin/memory.context-engine`.
|
|
22
|
+
*
|
|
23
|
+
* @stable
|
|
24
|
+
*/
|
|
25
|
+
type ContentOriginKind = 'built-in' | 'user-defined' | 'trusted-skill' | 'untrusted-skill' | 'mcp' | 'web-search';
|
|
26
|
+
/**
|
|
27
|
+
* Per-agent guard configuration accepted by
|
|
28
|
+
* `createAgent({ mergeGuard })`.
|
|
29
|
+
*
|
|
30
|
+
* @stable
|
|
31
|
+
*/
|
|
32
|
+
interface MergeGuardConfig {
|
|
33
|
+
readonly strictness: 'off' | 'detect' | 'detect-and-flag' | 'detect-and-block';
|
|
34
|
+
/** Default `0.3`. */
|
|
35
|
+
readonly maxLowTrustWeight?: number;
|
|
36
|
+
/** Default `0.5`. */
|
|
37
|
+
readonly lowTrustThreshold?: number;
|
|
38
|
+
/** Operator overrides for known agent ids. */
|
|
39
|
+
readonly sourceTrustOverrides?: Readonly<Record<string, number>>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Per-child input descriptor for {@link computeSourceTrust}.
|
|
43
|
+
*
|
|
44
|
+
* @stable
|
|
45
|
+
*/
|
|
46
|
+
interface ChildTrustInput {
|
|
47
|
+
readonly agentId: string;
|
|
48
|
+
readonly trustClass: TrustClass;
|
|
49
|
+
readonly origin: ContentOriginKind;
|
|
50
|
+
/** Rolling trust score in `[0.0, 1.0]`. Defaults to `1.0`. */
|
|
51
|
+
readonly historyAdjustment?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Compose `baseline * provenance * historyAdjustment` and clamp.
|
|
55
|
+
*
|
|
56
|
+
* @stable
|
|
57
|
+
*/
|
|
58
|
+
declare function computeSourceTrust(input: ChildTrustInput, overrides?: Readonly<Record<string, number>>): number;
|
|
59
|
+
/**
|
|
60
|
+
* Pure decision returned by {@link evaluateMerge}.
|
|
61
|
+
*
|
|
62
|
+
* @stable
|
|
63
|
+
*/
|
|
64
|
+
interface MergeBiasDecision {
|
|
65
|
+
readonly biased: boolean;
|
|
66
|
+
readonly offendingChild?: string;
|
|
67
|
+
readonly contributionWeight?: number;
|
|
68
|
+
readonly sourceTrust?: number;
|
|
69
|
+
readonly decision: 'pass-through' | 'flag' | 'block';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Evaluate whether the merge is biased — a child with
|
|
73
|
+
* `sourceTrust < lowTrustThreshold` contributing more than
|
|
74
|
+
* `maxLowTrustWeight` of the merged output.
|
|
75
|
+
*
|
|
76
|
+
* Inputs are pre-computed per-child trust scores together with the
|
|
77
|
+
* estimated contribution weights (token-count overlap between each
|
|
78
|
+
* child's output and the merged output, normalized to sum to ~1.0).
|
|
79
|
+
*
|
|
80
|
+
* @stable
|
|
81
|
+
*/
|
|
82
|
+
declare function evaluateMerge(perChild: ReadonlyArray<{
|
|
83
|
+
readonly agentId: string;
|
|
84
|
+
readonly sourceTrust: number;
|
|
85
|
+
readonly contributionWeight: number;
|
|
86
|
+
}>, cfg: MergeGuardConfig): MergeBiasDecision;
|
|
87
|
+
//#endregion
|
|
88
|
+
export { ChildTrustInput, ContentOriginKind, MergeBiasDecision, MergeGuardConfig, TrustClass, computeSourceTrust, evaluateMerge };
|
|
89
|
+
//# sourceMappingURL=merge-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-guard.d.ts","names":[],"sources":["../../src/lateral-leak/merge-guard.ts"],"sourcesContent":[],"mappings":";;AAiBA;AAeA;AAuBA;AAeA;AAaA;;;;;AAiBA;AAmBA;;;;;KAtGY,UAAA;;;;;;;KAeA,iBAAA;;;;;;;UAuBK,gBAAA;;;;;;;kCAOiB,SAAS;;;;;;;UAQ1B,eAAA;;uBAEM;mBACJ;;;;;;;;;iBAUH,kBAAA,QACP,6BACI,SAAS;;;;;;UAeL,iBAAA;;;;;;;;;;;;;;;;;;iBAmBD,aAAA,WACJ;;;;SAKL,mBACJ"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//#region src/lateral-leak/merge-guard.ts
|
|
2
|
+
const TRUST_CLASS_BASELINE = {
|
|
3
|
+
loopback: .9,
|
|
4
|
+
"public-tls": .7,
|
|
5
|
+
"public-mtls": .8,
|
|
6
|
+
"untrusted-skill": .3
|
|
7
|
+
};
|
|
8
|
+
const ORIGIN_MULTIPLIER = {
|
|
9
|
+
"built-in": 1,
|
|
10
|
+
"user-defined": .9,
|
|
11
|
+
"trusted-skill": .85,
|
|
12
|
+
"untrusted-skill": .4,
|
|
13
|
+
mcp: .7,
|
|
14
|
+
"web-search": .5
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Compose `baseline * provenance * historyAdjustment` and clamp.
|
|
18
|
+
*
|
|
19
|
+
* @stable
|
|
20
|
+
*/
|
|
21
|
+
function computeSourceTrust(input, overrides = {}) {
|
|
22
|
+
const override = overrides[input.agentId];
|
|
23
|
+
if (override !== void 0) return Math.max(0, Math.min(1, override));
|
|
24
|
+
const baseline = TRUST_CLASS_BASELINE[input.trustClass];
|
|
25
|
+
const provenance = ORIGIN_MULTIPLIER[input.origin];
|
|
26
|
+
const history = input.historyAdjustment ?? 1;
|
|
27
|
+
return Math.max(0, Math.min(1, baseline * provenance * history));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Evaluate whether the merge is biased — a child with
|
|
31
|
+
* `sourceTrust < lowTrustThreshold` contributing more than
|
|
32
|
+
* `maxLowTrustWeight` of the merged output.
|
|
33
|
+
*
|
|
34
|
+
* Inputs are pre-computed per-child trust scores together with the
|
|
35
|
+
* estimated contribution weights (token-count overlap between each
|
|
36
|
+
* child's output and the merged output, normalized to sum to ~1.0).
|
|
37
|
+
*
|
|
38
|
+
* @stable
|
|
39
|
+
*/
|
|
40
|
+
function evaluateMerge(perChild, cfg) {
|
|
41
|
+
if (cfg.strictness === "off") return {
|
|
42
|
+
biased: false,
|
|
43
|
+
decision: "pass-through"
|
|
44
|
+
};
|
|
45
|
+
const lowTrustThreshold = cfg.lowTrustThreshold ?? .5;
|
|
46
|
+
const maxLowTrustWeight = cfg.maxLowTrustWeight ?? .3;
|
|
47
|
+
for (const child of perChild) if (child.sourceTrust < lowTrustThreshold && child.contributionWeight > maxLowTrustWeight) {
|
|
48
|
+
const decision = cfg.strictness === "detect-and-block" ? "block" : cfg.strictness === "detect-and-flag" ? "flag" : "pass-through";
|
|
49
|
+
return {
|
|
50
|
+
biased: true,
|
|
51
|
+
offendingChild: child.agentId,
|
|
52
|
+
contributionWeight: child.contributionWeight,
|
|
53
|
+
sourceTrust: child.sourceTrust,
|
|
54
|
+
decision
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
biased: false,
|
|
59
|
+
decision: "pass-through"
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { computeSourceTrust, evaluateMerge };
|
|
65
|
+
//# sourceMappingURL=merge-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-guard.js","names":["TRUST_CLASS_BASELINE: Record<TrustClass, number>","ORIGIN_MULTIPLIER: Record<ContentOriginKind, number>","decision: MergeBiasDecision['decision']"],"sources":["../../src/lateral-leak/merge-guard.ts"],"sourcesContent":["/**\n * `MergeAgentSidewaysInjectionGuard` — layered on top of the\n * `Agent.fanOut(...)` `'judge-merge'` strategy. Computes a\n * per-child `sourceTrust` score in `[0.0, 1.0]` and flags merges\n * where a low-trust child's contribution-weight exceeds the\n * configured `maxLowTrustWeight` threshold.\n *\n * @packageDocumentation\n */\n\n/**\n * Trust-class baseline used by the guard's `sourceTrust`\n * computation. Mirrors the DEC-149 trust-class taxonomy from\n * `@graphorin/provider`.\n *\n * @stable\n */\nexport type TrustClass = 'loopback' | 'public-tls' | 'public-mtls' | 'untrusted-skill';\n\nconst TRUST_CLASS_BASELINE: Record<TrustClass, number> = {\n loopback: 0.9,\n 'public-tls': 0.7,\n 'public-mtls': 0.8,\n 'untrusted-skill': 0.3,\n};\n\n/**\n * Tool-source provenance multiplier. Mirrors the `ContentOrigin`\n * annotation from `@graphorin/memory.context-engine`.\n *\n * @stable\n */\nexport type ContentOriginKind =\n | 'built-in'\n | 'user-defined'\n | 'trusted-skill'\n | 'untrusted-skill'\n | 'mcp'\n | 'web-search';\n\nconst ORIGIN_MULTIPLIER: Record<ContentOriginKind, number> = {\n 'built-in': 1.0,\n 'user-defined': 0.9,\n 'trusted-skill': 0.85,\n 'untrusted-skill': 0.4,\n mcp: 0.7,\n 'web-search': 0.5,\n};\n\n/**\n * Per-agent guard configuration accepted by\n * `createAgent({ mergeGuard })`.\n *\n * @stable\n */\nexport interface MergeGuardConfig {\n readonly strictness: 'off' | 'detect' | 'detect-and-flag' | 'detect-and-block';\n /** Default `0.3`. */\n readonly maxLowTrustWeight?: number;\n /** Default `0.5`. */\n readonly lowTrustThreshold?: number;\n /** Operator overrides for known agent ids. */\n readonly sourceTrustOverrides?: Readonly<Record<string, number>>;\n}\n\n/**\n * Per-child input descriptor for {@link computeSourceTrust}.\n *\n * @stable\n */\nexport interface ChildTrustInput {\n readonly agentId: string;\n readonly trustClass: TrustClass;\n readonly origin: ContentOriginKind;\n /** Rolling trust score in `[0.0, 1.0]`. Defaults to `1.0`. */\n readonly historyAdjustment?: number;\n}\n\n/**\n * Compose `baseline * provenance * historyAdjustment` and clamp.\n *\n * @stable\n */\nexport function computeSourceTrust(\n input: ChildTrustInput,\n overrides: Readonly<Record<string, number>> = {},\n): number {\n const override = overrides[input.agentId];\n if (override !== undefined) return Math.max(0, Math.min(1, override));\n const baseline = TRUST_CLASS_BASELINE[input.trustClass];\n const provenance = ORIGIN_MULTIPLIER[input.origin];\n const history = input.historyAdjustment ?? 1.0;\n return Math.max(0, Math.min(1, baseline * provenance * history));\n}\n\n/**\n * Pure decision returned by {@link evaluateMerge}.\n *\n * @stable\n */\nexport interface MergeBiasDecision {\n readonly biased: boolean;\n readonly offendingChild?: string;\n readonly contributionWeight?: number;\n readonly sourceTrust?: number;\n readonly decision: 'pass-through' | 'flag' | 'block';\n}\n\n/**\n * Evaluate whether the merge is biased — a child with\n * `sourceTrust < lowTrustThreshold` contributing more than\n * `maxLowTrustWeight` of the merged output.\n *\n * Inputs are pre-computed per-child trust scores together with the\n * estimated contribution weights (token-count overlap between each\n * child's output and the merged output, normalized to sum to ~1.0).\n *\n * @stable\n */\nexport function evaluateMerge(\n perChild: ReadonlyArray<{\n readonly agentId: string;\n readonly sourceTrust: number;\n readonly contributionWeight: number;\n }>,\n cfg: MergeGuardConfig,\n): MergeBiasDecision {\n if (cfg.strictness === 'off') {\n return { biased: false, decision: 'pass-through' };\n }\n const lowTrustThreshold = cfg.lowTrustThreshold ?? 0.5;\n const maxLowTrustWeight = cfg.maxLowTrustWeight ?? 0.3;\n for (const child of perChild) {\n if (child.sourceTrust < lowTrustThreshold && child.contributionWeight > maxLowTrustWeight) {\n const decision: MergeBiasDecision['decision'] =\n cfg.strictness === 'detect-and-block'\n ? 'block'\n : cfg.strictness === 'detect-and-flag'\n ? 'flag'\n : 'pass-through';\n return {\n biased: true,\n offendingChild: child.agentId,\n contributionWeight: child.contributionWeight,\n sourceTrust: child.sourceTrust,\n decision,\n };\n }\n }\n return { biased: false, decision: 'pass-through' };\n}\n"],"mappings":";AAmBA,MAAMA,uBAAmD;CACvD,UAAU;CACV,cAAc;CACd,eAAe;CACf,mBAAmB;CACpB;AAgBD,MAAMC,oBAAuD;CAC3D,YAAY;CACZ,gBAAgB;CAChB,iBAAiB;CACjB,mBAAmB;CACnB,KAAK;CACL,cAAc;CACf;;;;;;AAoCD,SAAgB,mBACd,OACA,YAA8C,EAAE,EACxC;CACR,MAAM,WAAW,UAAU,MAAM;AACjC,KAAI,aAAa,OAAW,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC;CACrE,MAAM,WAAW,qBAAqB,MAAM;CAC5C,MAAM,aAAa,kBAAkB,MAAM;CAC3C,MAAM,UAAU,MAAM,qBAAqB;AAC3C,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,WAAW,aAAa,QAAQ,CAAC;;;;;;;;;;;;;AA2BlE,SAAgB,cACd,UAKA,KACmB;AACnB,KAAI,IAAI,eAAe,MACrB,QAAO;EAAE,QAAQ;EAAO,UAAU;EAAgB;CAEpD,MAAM,oBAAoB,IAAI,qBAAqB;CACnD,MAAM,oBAAoB,IAAI,qBAAqB;AACnD,MAAK,MAAM,SAAS,SAClB,KAAI,MAAM,cAAc,qBAAqB,MAAM,qBAAqB,mBAAmB;EACzF,MAAMC,WACJ,IAAI,eAAe,qBACf,UACA,IAAI,eAAe,oBACjB,SACA;AACR,SAAO;GACL,QAAQ;GACR,gBAAgB,MAAM;GACtB,oBAAoB,MAAM;GAC1B,aAAa,MAAM;GACnB;GACD;;AAGL,QAAO;EAAE,QAAQ;EAAO,UAAU;EAAgB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region src/lateral-leak/protocol-guard.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Protocol/header injection guard — escapes control characters in
|
|
4
|
+
* tool result bodies before they cross internal-service delivery
|
|
5
|
+
* boundaries (SSE, WebSocket, REST body, HTTP header, audit row).
|
|
6
|
+
*
|
|
7
|
+
* The default policy mirrors the deployment-posture matrix from the
|
|
8
|
+
* lateral-leak design (DEC-171 / suggested ADR-059):
|
|
9
|
+
*
|
|
10
|
+
* - `'sse' | 'http-header'` → `'strict'` (escape control chars to
|
|
11
|
+
* `\xNN` hex literals; the safest default for cleartext frame
|
|
12
|
+
* protocols).
|
|
13
|
+
* - `'ws' | 'rest-body'` → `'replace'` (replace with the Unicode
|
|
14
|
+
* replacement character `\uFFFD`).
|
|
15
|
+
* - `'audit'` → `'strict'` (regulated deployments).
|
|
16
|
+
* - `'reject'` is operator opt-in — the guard throws
|
|
17
|
+
* {@link ProtocolInjectionRejectError} when control characters are
|
|
18
|
+
* detected.
|
|
19
|
+
*
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Per-boundary identifier used by the runtime when calling the
|
|
24
|
+
* guard.
|
|
25
|
+
*
|
|
26
|
+
* @stable
|
|
27
|
+
*/
|
|
28
|
+
type ProtocolBoundary = 'sse' | 'http-header' | 'ws' | 'rest-body' | 'audit';
|
|
29
|
+
/**
|
|
30
|
+
* Per-boundary escape policy.
|
|
31
|
+
*
|
|
32
|
+
* @stable
|
|
33
|
+
*/
|
|
34
|
+
type ProtocolEscapePolicy = 'strict' | 'replace' | 'reject';
|
|
35
|
+
/**
|
|
36
|
+
* Configurable per-boundary policy table. Operators may override
|
|
37
|
+
* specific boundaries via `Agent.protocolGuard?: { ... }`.
|
|
38
|
+
*
|
|
39
|
+
* @stable
|
|
40
|
+
*/
|
|
41
|
+
interface ProtocolGuardConfig {
|
|
42
|
+
readonly sse?: ProtocolEscapePolicy;
|
|
43
|
+
readonly httpHeader?: ProtocolEscapePolicy;
|
|
44
|
+
readonly ws?: ProtocolEscapePolicy;
|
|
45
|
+
readonly restBody?: ProtocolEscapePolicy;
|
|
46
|
+
readonly audit?: ProtocolEscapePolicy;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolved policy lookup. Pure function — no side effects.
|
|
50
|
+
*
|
|
51
|
+
* @stable
|
|
52
|
+
*/
|
|
53
|
+
declare function resolvePolicy(boundary: ProtocolBoundary, cfg?: ProtocolGuardConfig): ProtocolEscapePolicy;
|
|
54
|
+
/**
|
|
55
|
+
* Outcome of {@link guardOutboundContent}.
|
|
56
|
+
*
|
|
57
|
+
* @stable
|
|
58
|
+
*/
|
|
59
|
+
interface GuardOutcome {
|
|
60
|
+
readonly content: string;
|
|
61
|
+
readonly escapedCharCount: number;
|
|
62
|
+
readonly matchedPattern?: string;
|
|
63
|
+
readonly decision: 'pass-through' | 'escaped' | 'replaced' | 'rejected';
|
|
64
|
+
readonly boundary: ProtocolBoundary;
|
|
65
|
+
readonly policy: ProtocolEscapePolicy;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Apply the configured escape policy to a single string body. Pure
|
|
69
|
+
* — never mutates inputs.
|
|
70
|
+
*
|
|
71
|
+
* @stable
|
|
72
|
+
*/
|
|
73
|
+
declare function guardOutboundContent(input: string, boundary: ProtocolBoundary, cfg?: ProtocolGuardConfig): GuardOutcome;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { GuardOutcome, ProtocolBoundary, ProtocolEscapePolicy, ProtocolGuardConfig, guardOutboundContent, resolvePolicy };
|
|
76
|
+
//# sourceMappingURL=protocol-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-guard.d.ts","names":[],"sources":["../../src/lateral-leak/protocol-guard.ts"],"sourcesContent":[],"mappings":";;AA6BA;AAOA;AAQA;;;;;;;AAaA;;;;;AA+CA;AAgEA;;;;;;;;;;KA3IY,gBAAA;;;;;;KAOA,oBAAA;;;;;;;UAQK,mBAAA;iBACA;wBACO;gBACR;sBACM;mBACH;;;;;;;iBAQH,aAAA,WACJ,wBACL,sBACJ;;;;;;UA4Cc,YAAA;;;;;qBAKI;mBACF;;;;;;;;iBA0DH,oBAAA,0BAEJ,wBACL,sBACJ"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ProtocolInjectionRejectError } from "../errors/index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lateral-leak/protocol-guard.ts
|
|
4
|
+
/**
|
|
5
|
+
* Protocol/header injection guard — escapes control characters in
|
|
6
|
+
* tool result bodies before they cross internal-service delivery
|
|
7
|
+
* boundaries (SSE, WebSocket, REST body, HTTP header, audit row).
|
|
8
|
+
*
|
|
9
|
+
* The default policy mirrors the deployment-posture matrix from the
|
|
10
|
+
* lateral-leak design (DEC-171 / suggested ADR-059):
|
|
11
|
+
*
|
|
12
|
+
* - `'sse' | 'http-header'` → `'strict'` (escape control chars to
|
|
13
|
+
* `\xNN` hex literals; the safest default for cleartext frame
|
|
14
|
+
* protocols).
|
|
15
|
+
* - `'ws' | 'rest-body'` → `'replace'` (replace with the Unicode
|
|
16
|
+
* replacement character `\uFFFD`).
|
|
17
|
+
* - `'audit'` → `'strict'` (regulated deployments).
|
|
18
|
+
* - `'reject'` is operator opt-in — the guard throws
|
|
19
|
+
* {@link ProtocolInjectionRejectError} when control characters are
|
|
20
|
+
* detected.
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Resolved policy lookup. Pure function — no side effects.
|
|
26
|
+
*
|
|
27
|
+
* @stable
|
|
28
|
+
*/
|
|
29
|
+
function resolvePolicy(boundary, cfg = {}) {
|
|
30
|
+
switch (boundary) {
|
|
31
|
+
case "sse": return cfg.sse ?? "strict";
|
|
32
|
+
case "http-header": return cfg.httpHeader ?? "strict";
|
|
33
|
+
case "ws": return cfg.ws ?? "replace";
|
|
34
|
+
case "rest-body": return cfg.restBody ?? "replace";
|
|
35
|
+
case "audit": return cfg.audit ?? "strict";
|
|
36
|
+
default: return "strict";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function buildControlCharsRegex() {
|
|
40
|
+
const codePoints = [];
|
|
41
|
+
for (let n = 0; n < 32; n++) {
|
|
42
|
+
if (n === 9 || n === 10 || n === 13) continue;
|
|
43
|
+
codePoints.push(n);
|
|
44
|
+
}
|
|
45
|
+
codePoints.push(127);
|
|
46
|
+
const cls = codePoints.map((c) => `\\x${c.toString(16).padStart(2, "0")}`).join("");
|
|
47
|
+
return new RegExp(`[${cls}]`, "g");
|
|
48
|
+
}
|
|
49
|
+
const CONTROL_CHARS_RE = buildControlCharsRegex();
|
|
50
|
+
const SSE_FRAME_RE = /\r?\n\r?\n/g;
|
|
51
|
+
const HEADER_CRLF_RE = /\r\n/g;
|
|
52
|
+
function escapeStrict(input, boundary) {
|
|
53
|
+
let count = 0;
|
|
54
|
+
let out = input.replace(CONTROL_CHARS_RE, (ch) => {
|
|
55
|
+
count += 1;
|
|
56
|
+
return `\\x${ch.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
|
57
|
+
});
|
|
58
|
+
if (boundary === "sse") out = out.replace(SSE_FRAME_RE, (m) => {
|
|
59
|
+
count += 1;
|
|
60
|
+
return m.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
61
|
+
});
|
|
62
|
+
if (boundary === "http-header" || boundary === "audit") out = out.replace(HEADER_CRLF_RE, () => {
|
|
63
|
+
count += 1;
|
|
64
|
+
return "\\r\\n";
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
content: out,
|
|
68
|
+
count
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function replaceUFFFD(input) {
|
|
72
|
+
let count = 0;
|
|
73
|
+
return {
|
|
74
|
+
content: input.replace(CONTROL_CHARS_RE, () => {
|
|
75
|
+
count += 1;
|
|
76
|
+
return "�";
|
|
77
|
+
}),
|
|
78
|
+
count
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function findMatchedPattern(input) {
|
|
82
|
+
if (CONTROL_CHARS_RE.test(input)) {
|
|
83
|
+
CONTROL_CHARS_RE.lastIndex = 0;
|
|
84
|
+
return "control-char";
|
|
85
|
+
}
|
|
86
|
+
if (SSE_FRAME_RE.test(input)) {
|
|
87
|
+
SSE_FRAME_RE.lastIndex = 0;
|
|
88
|
+
return "sse-frame";
|
|
89
|
+
}
|
|
90
|
+
if (HEADER_CRLF_RE.test(input)) {
|
|
91
|
+
HEADER_CRLF_RE.lastIndex = 0;
|
|
92
|
+
return "header-crlf";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Apply the configured escape policy to a single string body. Pure
|
|
97
|
+
* — never mutates inputs.
|
|
98
|
+
*
|
|
99
|
+
* @stable
|
|
100
|
+
*/
|
|
101
|
+
function guardOutboundContent(input, boundary, cfg = {}) {
|
|
102
|
+
const policy = resolvePolicy(boundary, cfg);
|
|
103
|
+
const matchedPattern = findMatchedPattern(input);
|
|
104
|
+
if (matchedPattern === void 0) return {
|
|
105
|
+
content: input,
|
|
106
|
+
escapedCharCount: 0,
|
|
107
|
+
decision: "pass-through",
|
|
108
|
+
boundary,
|
|
109
|
+
policy
|
|
110
|
+
};
|
|
111
|
+
switch (policy) {
|
|
112
|
+
case "strict": {
|
|
113
|
+
const r = escapeStrict(input, boundary);
|
|
114
|
+
return {
|
|
115
|
+
content: r.content,
|
|
116
|
+
escapedCharCount: r.count,
|
|
117
|
+
matchedPattern,
|
|
118
|
+
decision: "escaped",
|
|
119
|
+
boundary,
|
|
120
|
+
policy
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case "replace": {
|
|
124
|
+
const r = replaceUFFFD(input);
|
|
125
|
+
return {
|
|
126
|
+
content: r.content,
|
|
127
|
+
escapedCharCount: r.count,
|
|
128
|
+
matchedPattern,
|
|
129
|
+
decision: "replaced",
|
|
130
|
+
boundary,
|
|
131
|
+
policy
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
case "reject": throw new ProtocolInjectionRejectError(boundary, matchedPattern);
|
|
135
|
+
default: return {
|
|
136
|
+
content: input,
|
|
137
|
+
escapedCharCount: 0,
|
|
138
|
+
decision: "pass-through",
|
|
139
|
+
boundary,
|
|
140
|
+
policy
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
//#endregion
|
|
146
|
+
export { guardOutboundContent, resolvePolicy };
|
|
147
|
+
//# sourceMappingURL=protocol-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-guard.js","names":["codePoints: number[]"],"sources":["../../src/lateral-leak/protocol-guard.ts"],"sourcesContent":["/**\n * Protocol/header injection guard — escapes control characters in\n * tool result bodies before they cross internal-service delivery\n * boundaries (SSE, WebSocket, REST body, HTTP header, audit row).\n *\n * The default policy mirrors the deployment-posture matrix from the\n * lateral-leak design (DEC-171 / suggested ADR-059):\n *\n * - `'sse' | 'http-header'` → `'strict'` (escape control chars to\n * `\\xNN` hex literals; the safest default for cleartext frame\n * protocols).\n * - `'ws' | 'rest-body'` → `'replace'` (replace with the Unicode\n * replacement character `\\uFFFD`).\n * - `'audit'` → `'strict'` (regulated deployments).\n * - `'reject'` is operator opt-in — the guard throws\n * {@link ProtocolInjectionRejectError} when control characters are\n * detected.\n *\n * @packageDocumentation\n */\n\nimport { ProtocolInjectionRejectError } from '../errors/index.js';\n\n/**\n * Per-boundary identifier used by the runtime when calling the\n * guard.\n *\n * @stable\n */\nexport type ProtocolBoundary = 'sse' | 'http-header' | 'ws' | 'rest-body' | 'audit';\n\n/**\n * Per-boundary escape policy.\n *\n * @stable\n */\nexport type ProtocolEscapePolicy = 'strict' | 'replace' | 'reject';\n\n/**\n * Configurable per-boundary policy table. Operators may override\n * specific boundaries via `Agent.protocolGuard?: { ... }`.\n *\n * @stable\n */\nexport interface ProtocolGuardConfig {\n readonly sse?: ProtocolEscapePolicy;\n readonly httpHeader?: ProtocolEscapePolicy;\n readonly ws?: ProtocolEscapePolicy;\n readonly restBody?: ProtocolEscapePolicy;\n readonly audit?: ProtocolEscapePolicy;\n}\n\n/**\n * Resolved policy lookup. Pure function — no side effects.\n *\n * @stable\n */\nexport function resolvePolicy(\n boundary: ProtocolBoundary,\n cfg: ProtocolGuardConfig = {},\n): ProtocolEscapePolicy {\n switch (boundary) {\n case 'sse':\n return cfg.sse ?? 'strict';\n case 'http-header':\n return cfg.httpHeader ?? 'strict';\n case 'ws':\n return cfg.ws ?? 'replace';\n case 'rest-body':\n return cfg.restBody ?? 'replace';\n case 'audit':\n return cfg.audit ?? 'strict';\n default: {\n const _exhaustive: never = boundary;\n void _exhaustive;\n return 'strict';\n }\n }\n}\n\n// Control-character catalogue used to detect injection attempts at\n// the framework's outbound delivery boundaries. Building the regex\n// programmatically avoids embedding control bytes in the source —\n// the bytes are exactly the ones a malicious tool result might\n// inject to escape framing on cleartext protocols.\nfunction buildControlCharsRegex(): RegExp {\n const codePoints: number[] = [];\n for (let n = 0; n < 0x20; n++) {\n if (n === 0x09 || n === 0x0a || n === 0x0d) continue;\n codePoints.push(n);\n }\n codePoints.push(0x7f);\n const cls = codePoints.map((c) => `\\\\x${c.toString(16).padStart(2, '0')}`).join('');\n return new RegExp(`[${cls}]`, 'g');\n}\nconst CONTROL_CHARS_RE = buildControlCharsRegex();\nconst SSE_FRAME_RE = /\\r?\\n\\r?\\n/g;\nconst HEADER_CRLF_RE = /\\r\\n/g;\n\n/**\n * Outcome of {@link guardOutboundContent}.\n *\n * @stable\n */\nexport interface GuardOutcome {\n readonly content: string;\n readonly escapedCharCount: number;\n readonly matchedPattern?: string;\n readonly decision: 'pass-through' | 'escaped' | 'replaced' | 'rejected';\n readonly boundary: ProtocolBoundary;\n readonly policy: ProtocolEscapePolicy;\n}\n\nfunction escapeStrict(\n input: string,\n boundary: ProtocolBoundary,\n): { content: string; count: number } {\n let count = 0;\n let out = input.replace(CONTROL_CHARS_RE, (ch) => {\n count += 1;\n return `\\\\x${ch.charCodeAt(0).toString(16).padStart(2, '0')}`;\n });\n if (boundary === 'sse') {\n out = out.replace(SSE_FRAME_RE, (m) => {\n count += 1;\n return m.replace(/\\n/g, '\\\\n').replace(/\\r/g, '\\\\r');\n });\n }\n if (boundary === 'http-header' || boundary === 'audit') {\n out = out.replace(HEADER_CRLF_RE, () => {\n count += 1;\n return '\\\\r\\\\n';\n });\n }\n return { content: out, count };\n}\n\nfunction replaceUFFFD(input: string): { content: string; count: number } {\n let count = 0;\n const out = input.replace(CONTROL_CHARS_RE, () => {\n count += 1;\n return '\\uFFFD';\n });\n return { content: out, count };\n}\n\nfunction findMatchedPattern(input: string): string | undefined {\n if (CONTROL_CHARS_RE.test(input)) {\n CONTROL_CHARS_RE.lastIndex = 0;\n return 'control-char';\n }\n if (SSE_FRAME_RE.test(input)) {\n SSE_FRAME_RE.lastIndex = 0;\n return 'sse-frame';\n }\n if (HEADER_CRLF_RE.test(input)) {\n HEADER_CRLF_RE.lastIndex = 0;\n return 'header-crlf';\n }\n return undefined;\n}\n\n/**\n * Apply the configured escape policy to a single string body. Pure\n * — never mutates inputs.\n *\n * @stable\n */\nexport function guardOutboundContent(\n input: string,\n boundary: ProtocolBoundary,\n cfg: ProtocolGuardConfig = {},\n): GuardOutcome {\n const policy = resolvePolicy(boundary, cfg);\n const matchedPattern = findMatchedPattern(input);\n if (matchedPattern === undefined) {\n return {\n content: input,\n escapedCharCount: 0,\n decision: 'pass-through',\n boundary,\n policy,\n };\n }\n switch (policy) {\n case 'strict': {\n const r = escapeStrict(input, boundary);\n return {\n content: r.content,\n escapedCharCount: r.count,\n matchedPattern,\n decision: 'escaped',\n boundary,\n policy,\n };\n }\n case 'replace': {\n const r = replaceUFFFD(input);\n return {\n content: r.content,\n escapedCharCount: r.count,\n matchedPattern,\n decision: 'replaced',\n boundary,\n policy,\n };\n }\n case 'reject':\n throw new ProtocolInjectionRejectError(boundary, matchedPattern);\n default: {\n const _exhaustive: never = policy;\n void _exhaustive;\n return {\n content: input,\n escapedCharCount: 0,\n decision: 'pass-through',\n boundary,\n policy,\n };\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,SAAgB,cACd,UACA,MAA2B,EAAE,EACP;AACtB,SAAQ,UAAR;EACE,KAAK,MACH,QAAO,IAAI,OAAO;EACpB,KAAK,cACH,QAAO,IAAI,cAAc;EAC3B,KAAK,KACH,QAAO,IAAI,MAAM;EACnB,KAAK,YACH,QAAO,IAAI,YAAY;EACzB,KAAK,QACH,QAAO,IAAI,SAAS;EACtB,QAGE,QAAO;;;AAUb,SAAS,yBAAiC;CACxC,MAAMA,aAAuB,EAAE;AAC/B,MAAK,IAAI,IAAI,GAAG,IAAI,IAAM,KAAK;AAC7B,MAAI,MAAM,KAAQ,MAAM,MAAQ,MAAM,GAAM;AAC5C,aAAW,KAAK,EAAE;;AAEpB,YAAW,KAAK,IAAK;CACrB,MAAM,MAAM,WAAW,KAAK,MAAM,MAAM,EAAE,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,GAAG;AACnF,QAAO,IAAI,OAAO,IAAI,IAAI,IAAI,IAAI;;AAEpC,MAAM,mBAAmB,wBAAwB;AACjD,MAAM,eAAe;AACrB,MAAM,iBAAiB;AAgBvB,SAAS,aACP,OACA,UACoC;CACpC,IAAI,QAAQ;CACZ,IAAI,MAAM,MAAM,QAAQ,mBAAmB,OAAO;AAChD,WAAS;AACT,SAAO,MAAM,GAAG,WAAW,EAAE,CAAC,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI;GAC3D;AACF,KAAI,aAAa,MACf,OAAM,IAAI,QAAQ,eAAe,MAAM;AACrC,WAAS;AACT,SAAO,EAAE,QAAQ,OAAO,MAAM,CAAC,QAAQ,OAAO,MAAM;GACpD;AAEJ,KAAI,aAAa,iBAAiB,aAAa,QAC7C,OAAM,IAAI,QAAQ,sBAAsB;AACtC,WAAS;AACT,SAAO;GACP;AAEJ,QAAO;EAAE,SAAS;EAAK;EAAO;;AAGhC,SAAS,aAAa,OAAmD;CACvE,IAAI,QAAQ;AAKZ,QAAO;EAAE,SAJG,MAAM,QAAQ,wBAAwB;AAChD,YAAS;AACT,UAAO;IACP;EACqB;EAAO;;AAGhC,SAAS,mBAAmB,OAAmC;AAC7D,KAAI,iBAAiB,KAAK,MAAM,EAAE;AAChC,mBAAiB,YAAY;AAC7B,SAAO;;AAET,KAAI,aAAa,KAAK,MAAM,EAAE;AAC5B,eAAa,YAAY;AACzB,SAAO;;AAET,KAAI,eAAe,KAAK,MAAM,EAAE;AAC9B,iBAAe,YAAY;AAC3B,SAAO;;;;;;;;;AAWX,SAAgB,qBACd,OACA,UACA,MAA2B,EAAE,EACf;CACd,MAAM,SAAS,cAAc,UAAU,IAAI;CAC3C,MAAM,iBAAiB,mBAAmB,MAAM;AAChD,KAAI,mBAAmB,OACrB,QAAO;EACL,SAAS;EACT,kBAAkB;EAClB,UAAU;EACV;EACA;EACD;AAEH,SAAQ,QAAR;EACE,KAAK,UAAU;GACb,MAAM,IAAI,aAAa,OAAO,SAAS;AACvC,UAAO;IACL,SAAS,EAAE;IACX,kBAAkB,EAAE;IACpB;IACA,UAAU;IACV;IACA;IACD;;EAEH,KAAK,WAAW;GACd,MAAM,IAAI,aAAa,MAAM;AAC7B,UAAO;IACL,SAAS,EAAE;IACX,kBAAkB,EAAE;IACpB;IACA,UAAU;IACV;IACA;IACD;;EAEH,KAAK,SACH,OAAM,IAAI,6BAA6B,UAAU,eAAe;EAClE,QAGE,QAAO;GACL,SAAS;GACT,kBAAkB;GAClB,UAAU;GACV;GACA;GACD"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ModelHint, ModelSpec, Provider } from "@graphorin/core";
|
|
2
|
+
|
|
3
|
+
//#region src/preferred-model/index.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Result returned by {@link resolvePreferredModel}.
|
|
7
|
+
*
|
|
8
|
+
* @stable
|
|
9
|
+
*/
|
|
10
|
+
interface PreferredModelResolution {
|
|
11
|
+
readonly resolvedProvider: Provider;
|
|
12
|
+
readonly resolvedModelId: string;
|
|
13
|
+
readonly source: 'prepare-step' | 'tier-map' | 'spec' | 'agent-preferred' | 'fallthrough-default';
|
|
14
|
+
readonly hintApplied?: ModelHint;
|
|
15
|
+
readonly fallthroughReason?: 'tier-not-mapped' | 'provider-unavailable' | 'override-takes-precedence';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pure inputs to {@link resolvePreferredModel}.
|
|
19
|
+
*
|
|
20
|
+
* @stable
|
|
21
|
+
*/
|
|
22
|
+
interface ResolvePreferredModelInput {
|
|
23
|
+
readonly prepareStepProvider?: Provider;
|
|
24
|
+
readonly toolPreferredModels: ReadonlyArray<ModelHint | ModelSpec | undefined>;
|
|
25
|
+
readonly agentPreferredModel?: ModelHint | ModelSpec;
|
|
26
|
+
readonly agentDefaultProvider: Provider;
|
|
27
|
+
readonly modelTierMap?: Partial<Record<ModelHint, ModelSpec>>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Pick the highest-cost tier across the supplied per-tool hints.
|
|
31
|
+
* Explicit `ModelSpec` entries are treated as the highest tier
|
|
32
|
+
* (`'smart'`) for tie-breaking — the conservative-correctness rule
|
|
33
|
+
* documented in DEC-169 / suggested ADR-057.
|
|
34
|
+
*
|
|
35
|
+
* Returns the picked hint together with the original `ModelSpec`
|
|
36
|
+
* (when an explicit spec won the tie-break).
|
|
37
|
+
*
|
|
38
|
+
* @stable
|
|
39
|
+
*/
|
|
40
|
+
declare function pickTopTierAcrossTools(hints: ReadonlyArray<ModelHint | ModelSpec | undefined>): {
|
|
41
|
+
readonly hint: ModelHint;
|
|
42
|
+
readonly spec?: ModelSpec;
|
|
43
|
+
} | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Walk the precedence ladder and return the resolved provider for a
|
|
46
|
+
* single agent step. Pure function — no side effects.
|
|
47
|
+
*
|
|
48
|
+
* @stable
|
|
49
|
+
*/
|
|
50
|
+
declare function resolvePreferredModel(input: ResolvePreferredModelInput): PreferredModelResolution;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { PreferredModelResolution, ResolvePreferredModelInput, pickTopTierAcrossTools, resolvePreferredModel };
|
|
53
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/preferred-model/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UAkCiB,wBAAA;6BACY;;;yBAGJ;;;;;;;;UAYR,0BAAA;iCACgB;gCACD,cAAc,YAAY;iCACzB,YAAY;iCACZ;0BACP,QAAQ,OAAO,WAAW;;;;;;;;;;;;;iBA4BpC,sBAAA,QACP,cAAc,YAAY;iBACf;kBAA2B;;;;;;;;iBAkC/B,qBAAA,QAA6B,6BAA6B"}
|