@moxxy/sdk 0.33.0 → 0.34.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.
Files changed (90) hide show
  1. package/README.md +1 -1
  2. package/dist/audit-export.d.ts +51 -0
  3. package/dist/audit-export.d.ts.map +1 -0
  4. package/dist/audit-export.js +4 -0
  5. package/dist/audit-export.js.map +1 -0
  6. package/dist/audit.d.ts +86 -0
  7. package/dist/audit.d.ts.map +1 -0
  8. package/dist/audit.js +35 -0
  9. package/dist/audit.js.map +1 -0
  10. package/dist/define.d.ts +2 -0
  11. package/dist/define.d.ts.map +1 -1
  12. package/dist/define.js +1 -0
  13. package/dist/define.js.map +1 -1
  14. package/dist/egress.d.ts +109 -0
  15. package/dist/egress.d.ts.map +1 -0
  16. package/dist/egress.js +228 -0
  17. package/dist/egress.js.map +1 -0
  18. package/dist/events.d.ts +18 -0
  19. package/dist/events.d.ts.map +1 -1
  20. package/dist/fs-utils.d.ts +48 -0
  21. package/dist/fs-utils.d.ts.map +1 -1
  22. package/dist/fs-utils.js +114 -1
  23. package/dist/fs-utils.js.map +1 -1
  24. package/dist/hooks.d.ts +8 -0
  25. package/dist/hooks.d.ts.map +1 -1
  26. package/dist/index.d.ts +12 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +8 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/plugin.d.ts +10 -0
  31. package/dist/plugin.d.ts.map +1 -1
  32. package/dist/principal-os.d.ts +14 -0
  33. package/dist/principal-os.d.ts.map +1 -0
  34. package/dist/principal-os.js +48 -0
  35. package/dist/principal-os.js.map +1 -0
  36. package/dist/principal.d.ts +54 -0
  37. package/dist/principal.d.ts.map +1 -0
  38. package/dist/principal.js +18 -0
  39. package/dist/principal.js.map +1 -0
  40. package/dist/redact.d.ts +36 -0
  41. package/dist/redact.d.ts.map +1 -0
  42. package/dist/redact.js +88 -0
  43. package/dist/redact.js.map +1 -0
  44. package/dist/schemas.d.ts +18 -18
  45. package/dist/secret-provider.d.ts +60 -0
  46. package/dist/secret-provider.d.ts.map +1 -0
  47. package/dist/secret-provider.js +59 -0
  48. package/dist/secret-provider.js.map +1 -0
  49. package/dist/server.d.ts +3 -1
  50. package/dist/server.d.ts.map +1 -1
  51. package/dist/server.js +6 -1
  52. package/dist/server.js.map +1 -1
  53. package/dist/session-like.d.ts +3 -0
  54. package/dist/session-like.d.ts.map +1 -1
  55. package/dist/signature.d.ts +21 -0
  56. package/dist/signature.d.ts.map +1 -0
  57. package/dist/signature.js +32 -0
  58. package/dist/signature.js.map +1 -0
  59. package/dist/tool-icon.d.ts +22 -0
  60. package/dist/tool-icon.d.ts.map +1 -0
  61. package/dist/tool-icon.js +44 -0
  62. package/dist/tool-icon.js.map +1 -0
  63. package/dist/tool.d.ts +8 -0
  64. package/dist/tool.d.ts.map +1 -1
  65. package/package.json +11 -4
  66. package/src/audit-export.ts +57 -0
  67. package/src/audit.ts +137 -0
  68. package/src/cross-process-lock.test.ts +23 -14
  69. package/src/define.ts +3 -0
  70. package/src/egress.test.ts +238 -0
  71. package/src/egress.ts +274 -0
  72. package/src/events.ts +18 -0
  73. package/src/fs-utils.test.ts +98 -2
  74. package/src/fs-utils.ts +120 -1
  75. package/src/hooks.ts +8 -0
  76. package/src/host-credential.test.ts +39 -0
  77. package/src/index.ts +49 -0
  78. package/src/plugin.ts +10 -0
  79. package/src/principal-os.ts +48 -0
  80. package/src/principal.test.ts +47 -0
  81. package/src/principal.ts +60 -0
  82. package/src/redact.test.ts +91 -0
  83. package/src/redact.ts +91 -0
  84. package/src/secret-provider.ts +84 -0
  85. package/src/server.ts +17 -0
  86. package/src/session-like.ts +3 -0
  87. package/src/signature.ts +34 -0
  88. package/src/tool-icon.test.ts +46 -0
  89. package/src/tool-icon.ts +46 -0
  90. package/src/tool.ts +8 -0
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`
@@ -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.33.0",
3
+ "version": "0.34.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,14 +67,21 @@
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
- "@moxxy/vitest-preset": "0.0.0",
77
- "@moxxy/tsconfig": "0.0.0"
78
+ "@moxxy/tsconfig": "0.0.0",
79
+ "@moxxy/vitest-preset": "0.0.0"
80
+ },
81
+ "peerDependenciesMeta": {
82
+ "undici": {
83
+ "optional": true
84
+ }
78
85
  },
79
86
  "scripts": {
80
87
  "build": "tsc -p tsconfig.json",
@@ -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
+ }
@@ -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 lock = new CrossProcessFireLock({ dir, ttlMs: 15 });
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
- await sleep(40);
47
- // The previous holder "crashed"; the marker is now stale and reclaimable.
48
- expect(await lock.claim('k@1')).toBe(true);
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 lock = new CrossProcessFireLock({ dir, ttlMs: 15 });
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
- const removed = await lock.sweep();
57
- expect(removed).toBe(1);
58
- const remaining = await readdir(dir);
59
- expect(remaining).toEqual(['fresh@1.lock']);
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
+ });