@moxxy/sdk 0.33.0 → 0.35.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/README.md +1 -1
- package/dist/audit-export.d.ts +51 -0
- package/dist/audit-export.d.ts.map +1 -0
- package/dist/audit-export.js +4 -0
- package/dist/audit-export.js.map +1 -0
- package/dist/audit.d.ts +86 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +35 -0
- package/dist/audit.js.map +1 -0
- package/dist/compactor-helpers.d.ts +1 -1
- package/dist/compactor-helpers.js +1 -1
- package/dist/define.d.ts +2 -0
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +1 -0
- package/dist/define.js.map +1 -1
- package/dist/egress.d.ts +109 -0
- package/dist/egress.d.ts.map +1 -0
- package/dist/egress.js +228 -0
- package/dist/egress.js.map +1 -0
- package/dist/events.d.ts +18 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/fs-utils.d.ts +48 -0
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +114 -1
- package/dist/fs-utils.js.map +1 -1
- package/dist/hooks.d.ts +8 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/principal-os.d.ts +14 -0
- package/dist/principal-os.d.ts.map +1 -0
- package/dist/principal-os.js +48 -0
- package/dist/principal-os.js.map +1 -0
- package/dist/principal.d.ts +54 -0
- package/dist/principal.d.ts.map +1 -0
- package/dist/principal.js +18 -0
- package/dist/principal.js.map +1 -0
- package/dist/redact.d.ts +36 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +88 -0
- package/dist/redact.js.map +1 -0
- package/dist/schemas.d.ts +18 -18
- package/dist/secret-provider.d.ts +60 -0
- package/dist/secret-provider.d.ts.map +1 -0
- package/dist/secret-provider.js +59 -0
- package/dist/secret-provider.js.map +1 -0
- package/dist/server.d.ts +3 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +6 -1
- package/dist/server.js.map +1 -1
- package/dist/session-like.d.ts +3 -0
- package/dist/session-like.d.ts.map +1 -1
- package/dist/signature.d.ts +21 -0
- package/dist/signature.d.ts.map +1 -0
- package/dist/signature.js +32 -0
- package/dist/signature.js.map +1 -0
- package/dist/tool-icon.d.ts +22 -0
- package/dist/tool-icon.d.ts.map +1 -0
- package/dist/tool-icon.js +44 -0
- package/dist/tool-icon.js.map +1 -0
- package/dist/tool.d.ts +8 -0
- package/dist/tool.d.ts.map +1 -1
- package/package.json +9 -2
- package/src/audit-export.ts +57 -0
- package/src/audit.ts +137 -0
- package/src/compactor-helpers.test.ts +1 -1
- package/src/compactor-helpers.ts +1 -1
- package/src/cross-process-lock.test.ts +23 -14
- package/src/define.ts +3 -0
- package/src/egress.test.ts +238 -0
- package/src/egress.ts +274 -0
- package/src/events.ts +18 -0
- package/src/fs-utils.test.ts +98 -2
- package/src/fs-utils.ts +120 -1
- package/src/hooks.ts +8 -0
- package/src/host-credential.test.ts +39 -0
- package/src/index.ts +49 -0
- package/src/plugin.ts +10 -0
- package/src/principal-os.ts +48 -0
- package/src/principal.test.ts +47 -0
- package/src/principal.ts +60 -0
- package/src/redact.test.ts +91 -0
- package/src/redact.ts +91 -0
- package/src/secret-provider.ts +84 -0
- package/src/server.ts +17 -0
- package/src/session-like.ts +3 -0
- package/src/signature.ts +34 -0
- package/src/tool-icon.test.ts +46 -0
- package/src/tool-icon.ts +46 -0
- package/src/tool.ts +8 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tool looks like, as a semantic name rather than a glyph.
|
|
3
|
+
*
|
|
4
|
+
* A CLOSED vocabulary on purpose. Surfaces render wildly differently: the
|
|
5
|
+
* desktop draws SVG, the TUI prints a character, a future web surface might do
|
|
6
|
+
* something else again. If this were a free string, a plugin could name an icon
|
|
7
|
+
* no surface owns and every surface would have to guess, so in practice
|
|
8
|
+
* everything would fall back and the field would be decorative. A fixed set
|
|
9
|
+
* means each surface maps it exhaustively and a plugin's choice actually
|
|
10
|
+
* survives to the screen.
|
|
11
|
+
*
|
|
12
|
+
* The names describe WHAT THE TOOL TOUCHES, not what it looks like, so a
|
|
13
|
+
* surface is free to change its artwork without every plugin becoming wrong.
|
|
14
|
+
*
|
|
15
|
+
* Adding a member is a breaking change for surfaces (their maps stop being
|
|
16
|
+
* exhaustive and stop compiling), which is the intended cost: a new icon should
|
|
17
|
+
* be a deliberate decision across surfaces, not a silent fallback to a wrench.
|
|
18
|
+
*/
|
|
19
|
+
export const TOOL_ICONS = [
|
|
20
|
+
'file',
|
|
21
|
+
'folder',
|
|
22
|
+
'search',
|
|
23
|
+
'edit',
|
|
24
|
+
'diff',
|
|
25
|
+
'terminal',
|
|
26
|
+
'globe',
|
|
27
|
+
'chat',
|
|
28
|
+
'workflow',
|
|
29
|
+
'agent',
|
|
30
|
+
'settings',
|
|
31
|
+
'lock',
|
|
32
|
+
'plug',
|
|
33
|
+
'mic',
|
|
34
|
+
'speaker',
|
|
35
|
+
'smartphone',
|
|
36
|
+
'workspace',
|
|
37
|
+
'clipboard',
|
|
38
|
+
'spark',
|
|
39
|
+
'wrench',
|
|
40
|
+
];
|
|
41
|
+
export function isToolIcon(value) {
|
|
42
|
+
return typeof value === 'string' && TOOL_ICONS.includes(value);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=tool-icon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-icon.js","sourceRoot":"","sources":["../src/tool-icon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,MAAM;IACN,UAAU;IACV,OAAO;IACP,MAAM;IACN,UAAU;IACV,OAAO;IACP,UAAU;IACV,MAAM;IACN,MAAM;IACN,KAAK;IACL,SAAS;IACT,YAAY;IACZ,WAAW;IACX,WAAW;IACX,OAAO;IACP,QAAQ;CACA,CAAC;AAIX,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,UAAoC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC5F,CAAC"}
|
package/dist/tool.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { z } from 'zod';
|
|
2
2
|
import type { EventLogReader } from './log.js';
|
|
3
|
+
import type { ToolIcon } from './tool-icon.js';
|
|
3
4
|
import type { PermissionRule } from './permission.js';
|
|
4
5
|
import type { SessionId, ToolCallId, TurnId } from './ids.js';
|
|
5
6
|
import type { SubagentSpawner } from './subagent.js';
|
|
@@ -182,6 +183,13 @@ export interface ToolDef {
|
|
|
182
183
|
readonly handler: (input: unknown, ctx: ToolContext) => Promise<unknown> | unknown;
|
|
183
184
|
/** Opt-in presentation hint. See `ToolCompactPresentation`. */
|
|
184
185
|
readonly compact?: ToolCompactPresentation;
|
|
186
|
+
/**
|
|
187
|
+
* What this tool touches, so surfaces can show it. Without this a surface can
|
|
188
|
+
* only guess from the NAME, which works for the handful of built-ins it was
|
|
189
|
+
* written against and gives every plugin-contributed tool the same generic
|
|
190
|
+
* fallback. See {@link ToolIcon}.
|
|
191
|
+
*/
|
|
192
|
+
readonly icon?: ToolIcon;
|
|
185
193
|
/**
|
|
186
194
|
* Optional capability declaration. Advisory unless the user enables
|
|
187
195
|
* `@moxxy/plugin-security`, at which point the active `Isolator`
|
package/dist/tool.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,cAAc,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClF,mFAAmF;IACnF,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,qEAAqE;IACrE,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;IACzD,qDAAqD;IACrD,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACzE;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;IACvE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,CACE,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,EAC5B,IAAI,CAAC,EAAE,gBAAgB,GACtB,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE;QACf,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACzD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACxD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACxD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC1D,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAC;IACrC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,UAAU,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC;IAC7B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/D;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBAAuB;IACtC,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAChE;+EAC2E;IAC3E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,UAAU,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC;IACrC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACnF,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,iBAAiB,CAAC;CACxC"}
|
|
1
|
+
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moxxy/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Typed public surface for the moxxy framework: event types, define* factories, lifecycle hook signatures.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"moxxy",
|
|
@@ -67,15 +67,22 @@
|
|
|
67
67
|
"access": "public"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"zod": "^3.24.0"
|
|
70
|
+
"zod": "^3.24.0",
|
|
71
|
+
"undici": "^6.21.0"
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
73
74
|
"typescript": "^5.7.3",
|
|
75
|
+
"undici": "^6.21.0",
|
|
74
76
|
"vitest": "^2.1.8",
|
|
75
77
|
"zod": "^3.24.0",
|
|
76
78
|
"@moxxy/vitest-preset": "0.0.0",
|
|
77
79
|
"@moxxy/tsconfig": "0.0.0"
|
|
78
80
|
},
|
|
81
|
+
"peerDependenciesMeta": {
|
|
82
|
+
"undici": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
79
86
|
"scripts": {
|
|
80
87
|
"build": "tsc -p tsconfig.json",
|
|
81
88
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AuditRecord } from './audit.js';
|
|
2
|
+
import type { Principal } from './principal.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shipping the local audit trail somewhere central.
|
|
6
|
+
*
|
|
7
|
+
* Separate from `AuditSinkDef` on purpose. A sink is the WRITE path: it is
|
|
8
|
+
* handed each record as it happens. An exporter is a READER of the trail that
|
|
9
|
+
* has already been durably written, driven by a checkpoint.
|
|
10
|
+
*
|
|
11
|
+
* That split is what makes shipping survivable. A network sink has to decide,
|
|
12
|
+
* while a turn is running, what to do when the collector is down: block the
|
|
13
|
+
* turn, drop the record, or buffer it in memory and lose it on exit. An
|
|
14
|
+
* exporter has no such dilemma, because the record is already on disk in a
|
|
15
|
+
* hash-chained file. A failed export is retried from the checkpoint, and the
|
|
16
|
+
* worst case is that central visibility lags, never that the record is gone.
|
|
17
|
+
*/
|
|
18
|
+
export interface AuditExportResource {
|
|
19
|
+
/** Which machine. The whole point of central collection is telling them apart. */
|
|
20
|
+
readonly host: string;
|
|
21
|
+
readonly cliVersion: string;
|
|
22
|
+
/** Whoever the surface could establish was acting; absent on an unattributed host. */
|
|
23
|
+
readonly principal?: Principal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AuditExportBatch {
|
|
27
|
+
readonly records: ReadonlyArray<AuditRecord>;
|
|
28
|
+
readonly resource: AuditExportResource;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AuditExportContext {
|
|
32
|
+
/** Destination, as configured. Exporter-defined; an endpoint URL for OTLP. */
|
|
33
|
+
readonly endpoint: string;
|
|
34
|
+
/** Exporter-specific settings from `audit.export`, already placeholder-resolved. */
|
|
35
|
+
readonly settings: Readonly<Record<string, unknown>>;
|
|
36
|
+
readonly signal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AuditExporterDef {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly description?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Ship one batch.
|
|
44
|
+
*
|
|
45
|
+
* THE CONTRACT: resolving means the destination has DURABLY accepted these
|
|
46
|
+
* records, because the checkpoint advances on resolve and those records are
|
|
47
|
+
* never offered again. An exporter that resolves on "request sent" turns a
|
|
48
|
+
* collector-side rejection into silent data loss. When in doubt, throw:
|
|
49
|
+
* re-sending a batch is cheap and records carry a hash the collector can
|
|
50
|
+
* deduplicate on, whereas a skipped batch is unrecoverable.
|
|
51
|
+
*/
|
|
52
|
+
send(batch: AuditExportBatch, ctx: AuditExportContext): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function defineAuditExporter(def: AuditExporterDef): AuditExporterDef {
|
|
56
|
+
return def;
|
|
57
|
+
}
|
package/src/audit.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { MoxxyEvent, MoxxyEventType } from './events.js';
|
|
2
|
+
import type { SessionId, TurnId } from './ids.js';
|
|
3
|
+
import type { Principal } from './principal.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The audit trail: a tamper-evident record of what the agent did, who it acted
|
|
7
|
+
* for, and what was allowed or denied, in a form that can LEAVE the machine.
|
|
8
|
+
*
|
|
9
|
+
* Distinct from the event log, and the distinction is the whole point. The
|
|
10
|
+
* event log is the conversation: complete, replayable, local, and full of
|
|
11
|
+
* payloads nobody wants shipped to a SIEM. An audit record is the receipt:
|
|
12
|
+
* bounded, redacted, hash-chained, and self-attributing, so one line is
|
|
13
|
+
* meaningful on its own without replaying a session to interpret it.
|
|
14
|
+
*
|
|
15
|
+
* A swappable block like every other, mirroring `EventStoreDef`: core seeds a
|
|
16
|
+
* protected local floor, plugins register alternatives (syslog, OTel, S3,
|
|
17
|
+
* webhook), and the operator activates one by name.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** What kind of thing happened. Coarser than `MoxxyEventType` on purpose: an
|
|
21
|
+
* auditor reasons about categories of action, not about the loop's internals. */
|
|
22
|
+
export type AuditAction =
|
|
23
|
+
/** The security-relevant config in force, recorded once per session. Without
|
|
24
|
+
* it a trail says what was done but not what the rules were at the time,
|
|
25
|
+
* which is the first question asked when reviewing a past run. */
|
|
26
|
+
| 'policy'
|
|
27
|
+
| 'prompt'
|
|
28
|
+
| 'tool.request'
|
|
29
|
+
| 'tool.approved'
|
|
30
|
+
| 'tool.denied'
|
|
31
|
+
| 'tool.result'
|
|
32
|
+
| 'skill.invoked'
|
|
33
|
+
| 'skill.created'
|
|
34
|
+
| 'plugin.registered'
|
|
35
|
+
| 'plugin.unregistered'
|
|
36
|
+
| 'abort'
|
|
37
|
+
/** Token counts for one provider call. Metadata only, never content, which
|
|
38
|
+
* is what makes cost attribution auditable without the trail carrying the
|
|
39
|
+
* conversation. */
|
|
40
|
+
| 'usage'
|
|
41
|
+
| 'error'
|
|
42
|
+
| 'other';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One record. Every field is either metadata or already-redacted, so the whole
|
|
46
|
+
* object is safe to forward.
|
|
47
|
+
*/
|
|
48
|
+
export interface AuditRecord {
|
|
49
|
+
/** Position in this sink's chain. Contiguous from 0; a gap is evidence. */
|
|
50
|
+
readonly seq: number;
|
|
51
|
+
readonly ts: number;
|
|
52
|
+
readonly sessionId: SessionId;
|
|
53
|
+
readonly turnId: TurnId;
|
|
54
|
+
readonly action: AuditAction;
|
|
55
|
+
/** The underlying event type, kept so a record can be traced back. */
|
|
56
|
+
readonly eventType: MoxxyEventType;
|
|
57
|
+
/**
|
|
58
|
+
* Who it was done for. Absent means unattributed, which is itself worth
|
|
59
|
+
* auditing: it says the surface could not establish an identity.
|
|
60
|
+
*/
|
|
61
|
+
readonly actor?: Principal;
|
|
62
|
+
/** Bounded, redacted, action-specific detail. Never raw payloads. */
|
|
63
|
+
readonly detail?: Readonly<Record<string, unknown>>;
|
|
64
|
+
/** Hash of the previous record, or null for the first. */
|
|
65
|
+
readonly prevHash: string | null;
|
|
66
|
+
/** Hash over this record's own fields plus {@link prevHash}. */
|
|
67
|
+
readonly hash: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A record before the chain fields are computed. */
|
|
71
|
+
export type UnchainedAuditRecord = Omit<AuditRecord, 'seq' | 'prevHash' | 'hash'>;
|
|
72
|
+
|
|
73
|
+
export interface AuditSinkScope {
|
|
74
|
+
readonly sessionId: SessionId;
|
|
75
|
+
readonly cwd: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface AuditSinkSession {
|
|
79
|
+
/**
|
|
80
|
+
* Append one record.
|
|
81
|
+
*
|
|
82
|
+
* Takes an UNCHAINED record: sequencing and any tamper-evidence are the
|
|
83
|
+
* sink's own concern, because each sink has its own ordering. The local file
|
|
84
|
+
* chains by day; a syslog sink has no chain at all; a remote service assigns
|
|
85
|
+
* its own sequence. Handing every sink a pre-sealed `seq`/`hash` would be a
|
|
86
|
+
* lie for all but one of them.
|
|
87
|
+
*
|
|
88
|
+
* MUST NOT throw: a failing audit sink degrades loudly, it never takes down
|
|
89
|
+
* the turn it is recording. Report failure by returning false.
|
|
90
|
+
*/
|
|
91
|
+
write(record: UnchainedAuditRecord): Promise<boolean>;
|
|
92
|
+
/** Flush anything buffered. Called on session close. */
|
|
93
|
+
close(): Promise<void>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface AuditSinkDef {
|
|
97
|
+
readonly name: string;
|
|
98
|
+
readonly description?: string;
|
|
99
|
+
open(scope: AuditSinkScope): AuditSinkSession;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Freeze an audit-sink spec, mirroring the other `defineX` factories. */
|
|
103
|
+
export function defineAuditSink(def: AuditSinkDef): AuditSinkDef {
|
|
104
|
+
return Object.freeze({ ...def });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Which event types produce an audit record, and under what category.
|
|
109
|
+
*
|
|
110
|
+
* Everything absent from this map is conversation, not audit: assistant text,
|
|
111
|
+
* reasoning, provider request/response, compaction, elision, mode iterations.
|
|
112
|
+
* They belong in the event log and would only bloat a trail an auditor has to
|
|
113
|
+
* read. Tool and permission traffic is what actually gets reviewed, so it is
|
|
114
|
+
* named precisely.
|
|
115
|
+
*
|
|
116
|
+
* An exhaustive map rather than a `default:` so adding an event type is a
|
|
117
|
+
* deliberate decision about whether it is auditable, not a silent 'other'.
|
|
118
|
+
*/
|
|
119
|
+
const AUDITED: Partial<Record<MoxxyEventType, AuditAction>> = {
|
|
120
|
+
user_prompt: 'prompt',
|
|
121
|
+
tool_call_requested: 'tool.request',
|
|
122
|
+
tool_call_approved: 'tool.approved',
|
|
123
|
+
tool_call_denied: 'tool.denied',
|
|
124
|
+
tool_result: 'tool.result',
|
|
125
|
+
skill_invoked: 'skill.invoked',
|
|
126
|
+
skill_created: 'skill.created',
|
|
127
|
+
plugin_registered: 'plugin.registered',
|
|
128
|
+
plugin_unregistered: 'plugin.unregistered',
|
|
129
|
+
abort: 'abort',
|
|
130
|
+
provider_response: 'usage',
|
|
131
|
+
error: 'error',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** The audit category for an event, or null when it is conversation only. */
|
|
135
|
+
export function auditActionOf(event: MoxxyEvent): AuditAction | null {
|
|
136
|
+
return AUDITED[event.type] ?? null;
|
|
137
|
+
}
|
|
@@ -247,7 +247,7 @@ describe('runCompactionIfNeeded', () => {
|
|
|
247
247
|
compactor,
|
|
248
248
|
model: 'claude-opus-4-7', // the listed descriptor
|
|
249
249
|
contextWindow: 800_000,
|
|
250
|
-
ctxModelId: 'claude-opus-4-8', // …but the session runs
|
|
250
|
+
ctxModelId: 'claude-opus-4-8', // …but the session runs a retired, unlisted id
|
|
251
251
|
});
|
|
252
252
|
await runCompactionIfNeeded(ctx);
|
|
253
253
|
// Falls back to models[0].contextWindow instead of bailing.
|
package/src/compactor-helpers.ts
CHANGED
|
@@ -118,7 +118,7 @@ function safeJsonLen(v: unknown): number {
|
|
|
118
118
|
*
|
|
119
119
|
* `config.model` is a free-form, unvalidated string, and providers happily
|
|
120
120
|
* serve ids that aren't in their fixed descriptor list — a newer release
|
|
121
|
-
* (`claude-opus-
|
|
121
|
+
* (`claude-opus-5`), a dated id, or a model registered at runtime via
|
|
122
122
|
* provider-admin. So an exact `models.find(m => m.id === ctx.model)` often
|
|
123
123
|
* MISSES, and both auto-compaction and auto-elision used to silently turn into
|
|
124
124
|
* permanent no-ops for the whole session (the context then grows unbounded and
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { mkdtemp, rm, readdir } from 'node:fs/promises';
|
|
1
|
+
import { mkdtemp, rm, readdir, utimes } from 'node:fs/promises';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
3
|
+
import path, { join } from 'node:path';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
5
|
import { CrossProcessFireLock } from './cross-process-lock.js';
|
|
6
6
|
|
|
7
|
-
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
8
|
-
|
|
9
7
|
describe('CrossProcessFireLock', () => {
|
|
10
8
|
let dir: string;
|
|
11
9
|
beforeEach(async () => {
|
|
@@ -40,23 +38,34 @@ describe('CrossProcessFireLock', () => {
|
|
|
40
38
|
});
|
|
41
39
|
|
|
42
40
|
it('reclaims a stale marker once its TTL elapses (crashed holder)', async () => {
|
|
43
|
-
const
|
|
41
|
+
const ttlMs = 1_000;
|
|
42
|
+
const lock = new CrossProcessFireLock({ dir, ttlMs });
|
|
44
43
|
expect(await lock.claim('k@1')).toBe(true);
|
|
45
44
|
expect(await lock.claim('k@1')).toBe(false);
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
45
|
+
// The previous holder "crashed": advance the CLOCK past the TTL rather than
|
|
46
|
+
// sleeping, so the test asserts the staleness rule and not the accuracy of
|
|
47
|
+
// a timer on a loaded machine.
|
|
48
|
+
expect(await lock.claim('k@1', Date.now() + ttlMs * 5)).toBe(true);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
// Age is set on the FILES and the clock is passed in, rather than produced by
|
|
52
|
+
// sleeping. Expiry is `now - mtime > ttlMs`, so a sleep-based version races
|
|
53
|
+
// the scheduler: any pause between claiming the fresh marker and sweeping
|
|
54
|
+
// that exceeds the TTL expires it too, and a short TTL makes that pause
|
|
55
|
+
// trivial to hit on a loaded machine.
|
|
51
56
|
it('sweep removes expired markers and leaves fresh ones', async () => {
|
|
52
|
-
const
|
|
57
|
+
const ttlMs = 1_000;
|
|
58
|
+
const lock = new CrossProcessFireLock({ dir, ttlMs });
|
|
53
59
|
await lock.claim('old@1');
|
|
54
|
-
await sleep(40);
|
|
55
60
|
await lock.claim('fresh@1');
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
61
|
+
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const stale = new Date(now - ttlMs * 5);
|
|
64
|
+
await utimes(join(dir, 'old@1.lock'), stale, stale);
|
|
65
|
+
await utimes(join(dir, 'fresh@1.lock'), new Date(now), new Date(now));
|
|
66
|
+
|
|
67
|
+
expect(await lock.sweep(now)).toBe(1);
|
|
68
|
+
expect(await readdir(dir)).toEqual(['fresh@1.lock']);
|
|
60
69
|
});
|
|
61
70
|
|
|
62
71
|
it('sanitizes unsafe key characters into a single marker file', async () => {
|
package/src/define.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { PermissionRule } from './permission.js';
|
|
|
9
9
|
import type { Plugin, PluginSpec } from './plugin.js';
|
|
10
10
|
import type { ProviderDef } from './provider.js';
|
|
11
11
|
import type { SkillDef, SkillFrontmatter } from './skill.js';
|
|
12
|
+
import type { ToolIcon } from './tool-icon.js';
|
|
12
13
|
import type { HostedTool, ToolCompactPresentation, ToolContext, ToolDef } from './tool.js';
|
|
13
14
|
import type { ToolIsolationSpec } from './isolation.js';
|
|
14
15
|
import type { TranscriberDef } from './transcriber.js';
|
|
@@ -46,6 +47,7 @@ export function defineTool<S extends z.ZodTypeAny, O = unknown>(spec: {
|
|
|
46
47
|
permission?: PermissionRule;
|
|
47
48
|
handler: (input: z.output<S>, ctx: ToolContext) => Promise<O> | O;
|
|
48
49
|
compact?: ToolCompactPresentation;
|
|
50
|
+
icon?: ToolIcon;
|
|
49
51
|
isolation?: ToolIsolationSpec;
|
|
50
52
|
}): ToolDef {
|
|
51
53
|
return Object.freeze({
|
|
@@ -58,6 +60,7 @@ export function defineTool<S extends z.ZodTypeAny, O = unknown>(spec: {
|
|
|
58
60
|
permission: spec.permission,
|
|
59
61
|
handler: spec.handler as (input: unknown, ctx: ToolContext) => Promise<unknown> | unknown,
|
|
60
62
|
compact: spec.compact,
|
|
63
|
+
icon: spec.icon,
|
|
61
64
|
isolation: spec.isolation,
|
|
62
65
|
});
|
|
63
66
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
hasProxy,
|
|
4
|
+
installEgressProxy,
|
|
5
|
+
parseNoProxy,
|
|
6
|
+
readProxyEnv,
|
|
7
|
+
redactProxyUrl,
|
|
8
|
+
shouldBypassProxy,
|
|
9
|
+
} from './egress.js';
|
|
10
|
+
|
|
11
|
+
describe('readProxyEnv', () => {
|
|
12
|
+
it('reads uppercase and lowercase forms', () => {
|
|
13
|
+
expect(readProxyEnv({ HTTPS_PROXY: 'http://p:3128' })).toEqual({ httpsProxy: 'http://p:3128' });
|
|
14
|
+
expect(readProxyEnv({ https_proxy: 'http://p:3128' })).toEqual({ httpsProxy: 'http://p:3128' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Lowercase wins by convention (curl, requests, Go): a shell exporting both
|
|
18
|
+
// usually set the lowercase one deliberately and more recently.
|
|
19
|
+
it('prefers the lowercase form when both are set', () => {
|
|
20
|
+
const env = { https_proxy: 'http://lower:1', HTTPS_PROXY: 'http://upper:2' };
|
|
21
|
+
expect(readProxyEnv(env).httpsProxy).toBe('http://lower:1');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('treats blank and whitespace values as unset', () => {
|
|
25
|
+
expect(readProxyEnv({ HTTPS_PROXY: '', HTTP_PROXY: ' ' })).toEqual({});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// A plaintext-only proxy declared via http_proxy must not silently swallow
|
|
29
|
+
// TLS traffic; https origins go direct instead of failing confusingly.
|
|
30
|
+
it('does not fall back to http_proxy for https origins', () => {
|
|
31
|
+
expect(readProxyEnv({ HTTP_PROXY: 'http://p:3128' })).toEqual({ httpProxy: 'http://p:3128' });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('parseNoProxy', () => {
|
|
36
|
+
it('splits on commas and whitespace, lowercases, and drops blanks', () => {
|
|
37
|
+
expect(parseNoProxy(' localhost, .Corp.Example 10.0.0.1 ,, ')).toEqual([
|
|
38
|
+
'localhost',
|
|
39
|
+
'.corp.example',
|
|
40
|
+
'10.0.0.1',
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns an empty list for undefined', () => {
|
|
45
|
+
expect(parseNoProxy(undefined)).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('shouldBypassProxy', () => {
|
|
50
|
+
const bypass = (origin: string, rules: string): boolean =>
|
|
51
|
+
shouldBypassProxy(origin, parseNoProxy(rules));
|
|
52
|
+
|
|
53
|
+
it('bypasses everything on *', () => {
|
|
54
|
+
expect(bypass('https://api.anthropic.com', '*')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('matches an exact host on any port', () => {
|
|
58
|
+
expect(bypass('http://localhost:8080/x', 'localhost')).toBe(true);
|
|
59
|
+
expect(bypass('https://localhost', 'localhost')).toBe(true);
|
|
60
|
+
expect(bypass('https://notlocalhost', 'localhost')).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// A leading dot covers the apex too, matching curl. Getting this wrong sends
|
|
64
|
+
// internal traffic through the proxy, which is exactly what NO_PROXY exists
|
|
65
|
+
// to prevent.
|
|
66
|
+
it('matches subdomains and the apex for a dotted rule', () => {
|
|
67
|
+
expect(bypass('https://api.corp.example', '.corp.example')).toBe(true);
|
|
68
|
+
expect(bypass('https://corp.example', '.corp.example')).toBe(true);
|
|
69
|
+
expect(bypass('https://deep.api.corp.example', '.corp.example')).toBe(true);
|
|
70
|
+
expect(bypass('https://evilcorp.example', '.corp.example')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('treats *.domain the same as .domain', () => {
|
|
74
|
+
expect(bypass('https://api.corp.example', '*.corp.example')).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('honours a port qualifier', () => {
|
|
78
|
+
expect(bypass('http://registry.internal:8081/x', 'registry.internal:8081')).toBe(true);
|
|
79
|
+
expect(bypass('http://registry.internal:9999/x', 'registry.internal:8081')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('resolves the implicit port from the scheme', () => {
|
|
83
|
+
expect(bypass('https://svc.internal/x', 'svc.internal:443')).toBe(true);
|
|
84
|
+
expect(bypass('http://svc.internal/x', 'svc.internal:80')).toBe(true);
|
|
85
|
+
expect(bypass('https://svc.internal/x', 'svc.internal:80')).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// `::1` must not be read as host ":" on port "1".
|
|
89
|
+
it('does not mistake an IPv6 literal for a host:port rule', () => {
|
|
90
|
+
expect(bypass('http://[::1]:7000/x', '::1')).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('is case-insensitive on the host', () => {
|
|
94
|
+
expect(bypass('https://API.Corp.Example', '.corp.example')).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('never bypasses with no rules, or on an unparseable origin', () => {
|
|
98
|
+
expect(bypass('https://api.anthropic.com', '')).toBe(false);
|
|
99
|
+
expect(shouldBypassProxy('not a url', ['*.corp.example'])).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('redactProxyUrl', () => {
|
|
104
|
+
it('masks embedded credentials', () => {
|
|
105
|
+
expect(redactProxyUrl('http://alice:hunter2@proxy.corp:3128')).toBe('http://***:***@proxy.corp:3128/');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('leaves a credential-free url intact', () => {
|
|
109
|
+
expect(redactProxyUrl('http://proxy.corp:3128')).toBe('http://proxy.corp:3128/');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Never echo something unparseable: it might be a secret pasted by mistake.
|
|
113
|
+
it('reveals nothing for an unparseable value', () => {
|
|
114
|
+
expect(redactProxyUrl('hunter2')).toBe('<invalid proxy url>');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('installEgressProxy', () => {
|
|
119
|
+
/**
|
|
120
|
+
* Stand-in for undici. Each agent reports its own label through the dispatch
|
|
121
|
+
* handler, so `routeOf` observes WHICH agent the router picked for an origin,
|
|
122
|
+
* which is the whole contract worth testing here.
|
|
123
|
+
*/
|
|
124
|
+
const fakeUndici = () => {
|
|
125
|
+
let installed: { dispatch: (o: { origin?: string }, h: unknown) => boolean } | null = null;
|
|
126
|
+
const agent = (label: string) => ({
|
|
127
|
+
dispatch: (_o: unknown, h: { onPick?: (l: string) => void }) => {
|
|
128
|
+
h.onPick?.(label);
|
|
129
|
+
return true;
|
|
130
|
+
},
|
|
131
|
+
close: async () => {},
|
|
132
|
+
destroy: async () => {},
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
routeOf: (origin: string): string => {
|
|
136
|
+
let picked = '';
|
|
137
|
+
installed!.dispatch({ origin }, {
|
|
138
|
+
onPick: (l: string) => {
|
|
139
|
+
picked = l;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return picked;
|
|
143
|
+
},
|
|
144
|
+
module: {
|
|
145
|
+
Agent: function Agent() {
|
|
146
|
+
return agent('direct');
|
|
147
|
+
} as never,
|
|
148
|
+
ProxyAgent: function ProxyAgent(o: { uri: string }) {
|
|
149
|
+
return agent(`proxy:${o.uri}`);
|
|
150
|
+
} as never,
|
|
151
|
+
setGlobalDispatcher: (d: never) => {
|
|
152
|
+
installed = d;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
it('no-ops when no proxy is configured', async () => {
|
|
159
|
+
const status = await installEgressProxy({});
|
|
160
|
+
expect(status).toEqual({ enabled: false, reason: 'no proxy configured' });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('reports and skips an unparseable proxy url instead of throwing', async () => {
|
|
164
|
+
const warn = vi.fn();
|
|
165
|
+
const status = await installEgressProxy({ httpsProxy: 'not a url' }, { warn });
|
|
166
|
+
expect(status.enabled).toBe(false);
|
|
167
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('https_proxy is not a valid URL'));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// A proxy is configured but the dependency is missing: degrade to direct with
|
|
171
|
+
// a loud warning rather than a failed boot.
|
|
172
|
+
it('warns and degrades when undici cannot be loaded', async () => {
|
|
173
|
+
const warn = vi.fn();
|
|
174
|
+
const status = await installEgressProxy(
|
|
175
|
+
{ httpsProxy: 'http://p:3128' },
|
|
176
|
+
{ warn, loadUndici: () => Promise.reject(new Error('not installed')) },
|
|
177
|
+
);
|
|
178
|
+
expect(status.enabled).toBe(false);
|
|
179
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('`undici` package is unavailable'));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('installs a dispatcher and redacts credentials in the reported status', async () => {
|
|
183
|
+
const u = fakeUndici();
|
|
184
|
+
const status = await installEgressProxy(
|
|
185
|
+
{ httpsProxy: 'http://bob:secret@p:3128', noProxy: 'localhost' },
|
|
186
|
+
{ loadUndici: async () => u.module as never },
|
|
187
|
+
);
|
|
188
|
+
expect(status.enabled).toBe(true);
|
|
189
|
+
expect(status.httpsProxy).toBe('http://***:***@p:3128/');
|
|
190
|
+
expect(status.httpsProxy).not.toContain('secret');
|
|
191
|
+
expect(status.noProxy).toEqual(['localhost']);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('routes https through the proxy and bypassed hosts direct', async () => {
|
|
195
|
+
const u = fakeUndici();
|
|
196
|
+
await installEgressProxy(
|
|
197
|
+
{ httpsProxy: 'http://p:3128', noProxy: '.corp.example' },
|
|
198
|
+
{ loadUndici: async () => u.module as never },
|
|
199
|
+
);
|
|
200
|
+
expect(u.routeOf('https://api.anthropic.com')).toBe('proxy:http://p:3128');
|
|
201
|
+
expect(u.routeOf('https://git.corp.example')).toBe('direct');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Only http_proxy set: https origins must go direct, not through it.
|
|
205
|
+
it('sends https direct when only an http proxy is configured', async () => {
|
|
206
|
+
const u = fakeUndici();
|
|
207
|
+
await installEgressProxy(
|
|
208
|
+
{ httpProxy: 'http://p:3128' },
|
|
209
|
+
{ loadUndici: async () => u.module as never },
|
|
210
|
+
);
|
|
211
|
+
expect(u.routeOf('http://example.com')).toBe('proxy:http://p:3128');
|
|
212
|
+
expect(u.routeOf('https://example.com')).toBe('direct');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('hasProxy', () => {
|
|
217
|
+
it('is true when either proxy is set', () => {
|
|
218
|
+
expect(hasProxy({})).toBe(false);
|
|
219
|
+
expect(hasProxy({ noProxy: 'localhost' })).toBe(false);
|
|
220
|
+
expect(hasProxy({ httpProxy: 'http://p' })).toBe(true);
|
|
221
|
+
expect(hasProxy({ httpsProxy: 'http://p' })).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('shouldBypassProxy IPv6 port qualifiers', () => {
|
|
226
|
+
const bypass = (origin: string, rules: string): boolean =>
|
|
227
|
+
shouldBypassProxy(origin, parseNoProxy(rules));
|
|
228
|
+
|
|
229
|
+
// A port on an IPv6 address requires brackets, exactly as in a URL.
|
|
230
|
+
it('honours a bracketed IPv6 host:port rule', () => {
|
|
231
|
+
expect(bypass('http://[::1]:7000/x', '[::1]:7000')).toBe(true);
|
|
232
|
+
expect(bypass('http://[::1]:9999/x', '[::1]:7000')).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('matches a bracketed IPv6 rule on any port', () => {
|
|
236
|
+
expect(bypass('http://[::1]:7000/x', '[::1]')).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
});
|