@moxxy/core 0.3.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 (332) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/events/factory.d.ts +6 -0
  4. package/dist/events/factory.d.ts.map +1 -0
  5. package/dist/events/factory.js +14 -0
  6. package/dist/events/factory.js.map +1 -0
  7. package/dist/events/log.d.ts +92 -0
  8. package/dist/events/log.d.ts.map +1 -0
  9. package/dist/events/log.js +244 -0
  10. package/dist/events/log.js.map +1 -0
  11. package/dist/index.d.ts +40 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +41 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/logger.d.ts +16 -0
  16. package/dist/logger.d.ts.map +1 -0
  17. package/dist/logger.js +27 -0
  18. package/dist/logger.js.map +1 -0
  19. package/dist/permissions/engine.d.ts +94 -0
  20. package/dist/permissions/engine.d.ts.map +1 -0
  21. package/dist/permissions/engine.js +203 -0
  22. package/dist/permissions/engine.js.map +1 -0
  23. package/dist/permissions/resolvers.d.ts +8 -0
  24. package/dist/permissions/resolvers.d.ts.map +1 -0
  25. package/dist/permissions/resolvers.js +8 -0
  26. package/dist/permissions/resolvers.js.map +1 -0
  27. package/dist/plugins/discovery.d.ts +9 -0
  28. package/dist/plugins/discovery.d.ts.map +1 -0
  29. package/dist/plugins/discovery.js +112 -0
  30. package/dist/plugins/discovery.js.map +1 -0
  31. package/dist/plugins/host-options.d.ts +72 -0
  32. package/dist/plugins/host-options.d.ts.map +1 -0
  33. package/dist/plugins/host-options.js +2 -0
  34. package/dist/plugins/host-options.js.map +1 -0
  35. package/dist/plugins/host.d.ts +53 -0
  36. package/dist/plugins/host.d.ts.map +1 -0
  37. package/dist/plugins/host.js +269 -0
  38. package/dist/plugins/host.js.map +1 -0
  39. package/dist/plugins/lifecycle.d.ts +31 -0
  40. package/dist/plugins/lifecycle.d.ts.map +1 -0
  41. package/dist/plugins/lifecycle.js +100 -0
  42. package/dist/plugins/lifecycle.js.map +1 -0
  43. package/dist/plugins/loader.d.ts +7 -0
  44. package/dist/plugins/loader.d.ts.map +1 -0
  45. package/dist/plugins/loader.js +68 -0
  46. package/dist/plugins/loader.js.map +1 -0
  47. package/dist/plugins/package-requirements.d.ts +16 -0
  48. package/dist/plugins/package-requirements.d.ts.map +1 -0
  49. package/dist/plugins/package-requirements.js +52 -0
  50. package/dist/plugins/package-requirements.js.map +1 -0
  51. package/dist/plugins/registry-kinds.d.ts +58 -0
  52. package/dist/plugins/registry-kinds.d.ts.map +1 -0
  53. package/dist/plugins/registry-kinds.js +139 -0
  54. package/dist/plugins/registry-kinds.js.map +1 -0
  55. package/dist/plugins/toposort.d.ts +24 -0
  56. package/dist/plugins/toposort.d.ts.map +1 -0
  57. package/dist/plugins/toposort.js +67 -0
  58. package/dist/plugins/toposort.js.map +1 -0
  59. package/dist/preferences.d.ts +40 -0
  60. package/dist/preferences.d.ts.map +1 -0
  61. package/dist/preferences.js +58 -0
  62. package/dist/preferences.js.map +1 -0
  63. package/dist/registries/active-backend-registry.d.ts +65 -0
  64. package/dist/registries/active-backend-registry.d.ts.map +1 -0
  65. package/dist/registries/active-backend-registry.js +108 -0
  66. package/dist/registries/active-backend-registry.js.map +1 -0
  67. package/dist/registries/active-def-registry.d.ts +59 -0
  68. package/dist/registries/active-def-registry.d.ts.map +1 -0
  69. package/dist/registries/active-def-registry.js +63 -0
  70. package/dist/registries/active-def-registry.js.map +1 -0
  71. package/dist/registries/agents.d.ts +16 -0
  72. package/dist/registries/agents.d.ts.map +1 -0
  73. package/dist/registries/agents.js +17 -0
  74. package/dist/registries/agents.js.map +1 -0
  75. package/dist/registries/cache-strategies.d.ts +12 -0
  76. package/dist/registries/cache-strategies.d.ts.map +1 -0
  77. package/dist/registries/cache-strategies.js +13 -0
  78. package/dist/registries/cache-strategies.js.map +1 -0
  79. package/dist/registries/channels.d.ts +16 -0
  80. package/dist/registries/channels.d.ts.map +1 -0
  81. package/dist/registries/channels.js +35 -0
  82. package/dist/registries/channels.js.map +1 -0
  83. package/dist/registries/commands.d.ts +29 -0
  84. package/dist/registries/commands.d.ts.map +1 -0
  85. package/dist/registries/commands.js +72 -0
  86. package/dist/registries/commands.js.map +1 -0
  87. package/dist/registries/compactors.d.ts +12 -0
  88. package/dist/registries/compactors.d.ts.map +1 -0
  89. package/dist/registries/compactors.js +13 -0
  90. package/dist/registries/compactors.js.map +1 -0
  91. package/dist/registries/def-map-registry.d.ts +33 -0
  92. package/dist/registries/def-map-registry.d.ts.map +1 -0
  93. package/dist/registries/def-map-registry.js +36 -0
  94. package/dist/registries/def-map-registry.js.map +1 -0
  95. package/dist/registries/embedders.d.ts +16 -0
  96. package/dist/registries/embedders.d.ts.map +1 -0
  97. package/dist/registries/embedders.js +17 -0
  98. package/dist/registries/embedders.js.map +1 -0
  99. package/dist/registries/isolators.d.ts +21 -0
  100. package/dist/registries/isolators.d.ts.map +1 -0
  101. package/dist/registries/isolators.js +32 -0
  102. package/dist/registries/isolators.js.map +1 -0
  103. package/dist/registries/modes.d.ts +28 -0
  104. package/dist/registries/modes.d.ts.map +1 -0
  105. package/dist/registries/modes.js +81 -0
  106. package/dist/registries/modes.js.map +1 -0
  107. package/dist/registries/providers.d.ts +44 -0
  108. package/dist/registries/providers.d.ts.map +1 -0
  109. package/dist/registries/providers.js +95 -0
  110. package/dist/registries/providers.js.map +1 -0
  111. package/dist/registries/skills.d.ts +24 -0
  112. package/dist/registries/skills.d.ts.map +1 -0
  113. package/dist/registries/skills.js +81 -0
  114. package/dist/registries/skills.js.map +1 -0
  115. package/dist/registries/surfaces.d.ts +13 -0
  116. package/dist/registries/surfaces.d.ts.map +1 -0
  117. package/dist/registries/surfaces.js +14 -0
  118. package/dist/registries/surfaces.js.map +1 -0
  119. package/dist/registries/synthesizers.d.ts +26 -0
  120. package/dist/registries/synthesizers.d.ts.map +1 -0
  121. package/dist/registries/synthesizers.js +34 -0
  122. package/dist/registries/synthesizers.js.map +1 -0
  123. package/dist/registries/tools.d.ts +49 -0
  124. package/dist/registries/tools.d.ts.map +1 -0
  125. package/dist/registries/tools.js +98 -0
  126. package/dist/registries/tools.js.map +1 -0
  127. package/dist/registries/transcribers.d.ts +17 -0
  128. package/dist/registries/transcribers.d.ts.map +1 -0
  129. package/dist/registries/transcribers.js +18 -0
  130. package/dist/registries/transcribers.js.map +1 -0
  131. package/dist/registries/tunnel-providers.d.ts +12 -0
  132. package/dist/registries/tunnel-providers.d.ts.map +1 -0
  133. package/dist/registries/tunnel-providers.js +13 -0
  134. package/dist/registries/tunnel-providers.js.map +1 -0
  135. package/dist/registries/view-renderers.d.ts +13 -0
  136. package/dist/registries/view-renderers.d.ts.map +1 -0
  137. package/dist/registries/view-renderers.js +14 -0
  138. package/dist/registries/view-renderers.js.map +1 -0
  139. package/dist/registries/workflow-executors.d.ts +13 -0
  140. package/dist/registries/workflow-executors.d.ts.map +1 -0
  141. package/dist/registries/workflow-executors.js +14 -0
  142. package/dist/registries/workflow-executors.js.map +1 -0
  143. package/dist/requirements.d.ts +59 -0
  144. package/dist/requirements.d.ts.map +1 -0
  145. package/dist/requirements.js +149 -0
  146. package/dist/requirements.js.map +1 -0
  147. package/dist/run-turn.d.ts +6 -0
  148. package/dist/run-turn.d.ts.map +1 -0
  149. package/dist/run-turn.js +141 -0
  150. package/dist/run-turn.js.map +1 -0
  151. package/dist/session-runtime.d.ts +67 -0
  152. package/dist/session-runtime.d.ts.map +1 -0
  153. package/dist/session-runtime.js +2 -0
  154. package/dist/session-runtime.js.map +1 -0
  155. package/dist/session.d.ts +203 -0
  156. package/dist/session.d.ts.map +1 -0
  157. package/dist/session.js +399 -0
  158. package/dist/session.js.map +1 -0
  159. package/dist/sessions/persistence.d.ts +184 -0
  160. package/dist/sessions/persistence.d.ts.map +1 -0
  161. package/dist/sessions/persistence.js +442 -0
  162. package/dist/sessions/persistence.js.map +1 -0
  163. package/dist/setup-agent.d.ts +91 -0
  164. package/dist/setup-agent.d.ts.map +1 -0
  165. package/dist/setup-agent.js +138 -0
  166. package/dist/setup-agent.js.map +1 -0
  167. package/dist/skills/index.d.ts +4 -0
  168. package/dist/skills/index.d.ts.map +1 -0
  169. package/dist/skills/index.js +4 -0
  170. package/dist/skills/index.js.map +1 -0
  171. package/dist/skills/loader.d.ts +16 -0
  172. package/dist/skills/loader.d.ts.map +1 -0
  173. package/dist/skills/loader.js +99 -0
  174. package/dist/skills/loader.js.map +1 -0
  175. package/dist/skills/parse.d.ts +5 -0
  176. package/dist/skills/parse.d.ts.map +1 -0
  177. package/dist/skills/parse.js +8 -0
  178. package/dist/skills/parse.js.map +1 -0
  179. package/dist/skills/synthesize-draft.d.ts +8 -0
  180. package/dist/skills/synthesize-draft.d.ts.map +1 -0
  181. package/dist/skills/synthesize-draft.js +32 -0
  182. package/dist/skills/synthesize-draft.js.map +1 -0
  183. package/dist/skills/synthesize.d.ts +33 -0
  184. package/dist/skills/synthesize.d.ts.map +1 -0
  185. package/dist/skills/synthesize.js +241 -0
  186. package/dist/skills/synthesize.js.map +1 -0
  187. package/dist/subagents/events.d.ts +20 -0
  188. package/dist/subagents/events.d.ts.map +1 -0
  189. package/dist/subagents/events.js +146 -0
  190. package/dist/subagents/events.js.map +1 -0
  191. package/dist/subagents/registry.d.ts +40 -0
  192. package/dist/subagents/registry.d.ts.map +1 -0
  193. package/dist/subagents/registry.js +42 -0
  194. package/dist/subagents/registry.js.map +1 -0
  195. package/dist/subagents/run-child.d.ts +43 -0
  196. package/dist/subagents/run-child.d.ts.map +1 -0
  197. package/dist/subagents/run-child.js +276 -0
  198. package/dist/subagents/run-child.js.map +1 -0
  199. package/dist/subagents/spawn.d.ts +8 -0
  200. package/dist/subagents/spawn.d.ts.map +1 -0
  201. package/dist/subagents/spawn.js +8 -0
  202. package/dist/subagents/spawn.js.map +1 -0
  203. package/dist/subagents/tools.d.ts +4 -0
  204. package/dist/subagents/tools.d.ts.map +1 -0
  205. package/dist/subagents/tools.js +13 -0
  206. package/dist/subagents/tools.js.map +1 -0
  207. package/dist/subagents.d.ts +3 -0
  208. package/dist/subagents.d.ts.map +1 -0
  209. package/dist/subagents.js +3 -0
  210. package/dist/subagents.js.map +1 -0
  211. package/dist/surfaces/host.d.ts +53 -0
  212. package/dist/surfaces/host.d.ts.map +1 -0
  213. package/dist/surfaces/host.js +177 -0
  214. package/dist/surfaces/host.js.map +1 -0
  215. package/dist/tunnel/localhost.d.ts +7 -0
  216. package/dist/tunnel/localhost.d.ts.map +1 -0
  217. package/dist/tunnel/localhost.js +21 -0
  218. package/dist/tunnel/localhost.js.map +1 -0
  219. package/dist/usage-stats.d.ts +42 -0
  220. package/dist/usage-stats.d.ts.map +1 -0
  221. package/dist/usage-stats.js +83 -0
  222. package/dist/usage-stats.js.map +1 -0
  223. package/dist/view/default-renderer.d.ts +7 -0
  224. package/dist/view/default-renderer.d.ts.map +1 -0
  225. package/dist/view/default-renderer.js +14 -0
  226. package/dist/view/default-renderer.js.map +1 -0
  227. package/dist/view/parse.d.ts +7 -0
  228. package/dist/view/parse.d.ts.map +1 -0
  229. package/dist/view/parse.js +479 -0
  230. package/dist/view/parse.js.map +1 -0
  231. package/package.json +61 -0
  232. package/src/events/factory.ts +27 -0
  233. package/src/events/log.test.ts +470 -0
  234. package/src/events/log.ts +256 -0
  235. package/src/index.ts +102 -0
  236. package/src/logger.ts +46 -0
  237. package/src/permissions/engine.test.ts +206 -0
  238. package/src/permissions/engine.ts +247 -0
  239. package/src/permissions/resolvers.test.ts +40 -0
  240. package/src/permissions/resolvers.ts +17 -0
  241. package/src/plugins/discovery.test.ts +115 -0
  242. package/src/plugins/discovery.ts +124 -0
  243. package/src/plugins/host-options.ts +73 -0
  244. package/src/plugins/host-view-tunnel.test.ts +86 -0
  245. package/src/plugins/host.test.ts +434 -0
  246. package/src/plugins/host.ts +327 -0
  247. package/src/plugins/lifecycle.test.ts +187 -0
  248. package/src/plugins/lifecycle.ts +136 -0
  249. package/src/plugins/loader.ts +79 -0
  250. package/src/plugins/package-requirements.ts +50 -0
  251. package/src/plugins/registry-kinds.test.ts +162 -0
  252. package/src/plugins/registry-kinds.ts +210 -0
  253. package/src/plugins/toposort.test.ts +66 -0
  254. package/src/plugins/toposort.ts +69 -0
  255. package/src/preferences.test.ts +105 -0
  256. package/src/preferences.ts +84 -0
  257. package/src/registries/active-backend-registry.ts +142 -0
  258. package/src/registries/active-def-registry.test.ts +103 -0
  259. package/src/registries/active-def-registry.ts +97 -0
  260. package/src/registries/agents.ts +18 -0
  261. package/src/registries/cache-strategies.ts +14 -0
  262. package/src/registries/channels.ts +42 -0
  263. package/src/registries/commands.test.ts +71 -0
  264. package/src/registries/commands.ts +78 -0
  265. package/src/registries/compactors.ts +14 -0
  266. package/src/registries/def-map-registry.test.ts +67 -0
  267. package/src/registries/def-map-registry.ts +59 -0
  268. package/src/registries/embedders.test.ts +54 -0
  269. package/src/registries/embedders.ts +18 -0
  270. package/src/registries/isolators.test.ts +43 -0
  271. package/src/registries/isolators.ts +38 -0
  272. package/src/registries/modes.test.ts +66 -0
  273. package/src/registries/modes.ts +80 -0
  274. package/src/registries/providers.test.ts +50 -0
  275. package/src/registries/providers.ts +97 -0
  276. package/src/registries/semantics.test.ts +117 -0
  277. package/src/registries/skills.ts +87 -0
  278. package/src/registries/surfaces.ts +18 -0
  279. package/src/registries/synthesizers.test.ts +63 -0
  280. package/src/registries/synthesizers.ts +36 -0
  281. package/src/registries/tools.test.ts +142 -0
  282. package/src/registries/tools.ts +145 -0
  283. package/src/registries/transcribers.test.ts +70 -0
  284. package/src/registries/transcribers.ts +19 -0
  285. package/src/registries/tunnel-providers.test.ts +30 -0
  286. package/src/registries/tunnel-providers.ts +14 -0
  287. package/src/registries/view-renderers.test.ts +68 -0
  288. package/src/registries/view-renderers.ts +15 -0
  289. package/src/registries/workflow-executors.test.ts +53 -0
  290. package/src/registries/workflow-executors.ts +15 -0
  291. package/src/requirements.targetinfo.test.ts +70 -0
  292. package/src/requirements.test.ts +111 -0
  293. package/src/requirements.ts +262 -0
  294. package/src/run-turn.test.ts +178 -0
  295. package/src/run-turn.ts +156 -0
  296. package/src/session-runtime.ts +75 -0
  297. package/src/session.test.ts +216 -0
  298. package/src/session.ts +491 -0
  299. package/src/sessions/persistence.flush.test.ts +100 -0
  300. package/src/sessions/persistence.ready.test.ts +91 -0
  301. package/src/sessions/persistence.test.ts +274 -0
  302. package/src/sessions/persistence.ts +498 -0
  303. package/src/setup-agent.test.ts +72 -0
  304. package/src/setup-agent.ts +189 -0
  305. package/src/skills/index.ts +14 -0
  306. package/src/skills/loader.test.ts +102 -0
  307. package/src/skills/loader.ts +114 -0
  308. package/src/skills/parse.test.ts +59 -0
  309. package/src/skills/parse.ts +11 -0
  310. package/src/skills/synthesize-draft.test.ts +79 -0
  311. package/src/skills/synthesize-draft.ts +46 -0
  312. package/src/skills/synthesize.test.ts +170 -0
  313. package/src/skills/synthesize.ts +304 -0
  314. package/src/subagents/events.test.ts +56 -0
  315. package/src/subagents/events.ts +194 -0
  316. package/src/subagents/registry.ts +62 -0
  317. package/src/subagents/run-child.test.ts +185 -0
  318. package/src/subagents/run-child.ts +425 -0
  319. package/src/subagents/spawn.ts +12 -0
  320. package/src/subagents/tools.ts +19 -0
  321. package/src/subagents.ts +2 -0
  322. package/src/surfaces/host.test.ts +330 -0
  323. package/src/surfaces/host.ts +192 -0
  324. package/src/tunnel/localhost.test.ts +22 -0
  325. package/src/tunnel/localhost.ts +21 -0
  326. package/src/usage-stats.test.ts +77 -0
  327. package/src/usage-stats.ts +119 -0
  328. package/src/view/default-renderer.test.ts +29 -0
  329. package/src/view/default-renderer.ts +14 -0
  330. package/src/view/parse-extended.test.ts +255 -0
  331. package/src/view/parse.test.ts +232 -0
  332. package/src/view/parse.ts +520 -0
@@ -0,0 +1,256 @@
1
+ import type {
2
+ EmittedEvent,
3
+ EventLogReader,
4
+ MoxxyEvent,
5
+ MoxxyEventOfType,
6
+ MoxxyEventType,
7
+ TurnId,
8
+ } from '@moxxy/sdk';
9
+ import { materializeEvent } from './factory.js';
10
+
11
+ export type EventListener = (event: MoxxyEvent) => void | Promise<void>;
12
+
13
+ export class EventLog implements EventLogReader {
14
+ private readonly events: MoxxyEvent[] = [];
15
+ private readonly listeners = new Set<EventListener>();
16
+ private readonly clearListeners = new Set<() => void>();
17
+ private readonly now: () => number;
18
+ /**
19
+ * Lazy secondary indexes so `ofType`/`byTurn` are O(matches) instead of an
20
+ * O(n) full-array `filter` per call (these back hot paths: token-accounting's
21
+ * `ofType('provider_response')`, lazy-tool gating's
22
+ * `ofType('tool_call_requested')`, and remote-session's per-turn
23
+ * `byTurn(turnId)` priming). Built lazily on first query so a cold/seeded log
24
+ * pays the one-time O(n) build only if anything ever queries it, then kept
25
+ * O(1) per append/ingest. Reset to `null` (rebuild-on-next-query) by
26
+ * `clear`/`rebase`, which mutate `events` wholesale.
27
+ *
28
+ * Each index holds the SAME event object references in their original
29
+ * append order — so a query returns an array deep-equal to the old
30
+ * `events.filter(...)` for every input.
31
+ */
32
+ private byType: Map<MoxxyEventType, MoxxyEvent[]> | null = null;
33
+ private byTurnId: Map<TurnId, MoxxyEvent[]> | null = null;
34
+ /**
35
+ * Seq of the FIRST event this log holds. 0 for an authoring log; a mirror
36
+ * primed by a partial attach replay (runner protocol v6 `replay.start`)
37
+ * rebases to the first replayed seq so `ingest`'s contiguity gate lines up
38
+ * with the runner's stream instead of expecting history we never received.
39
+ * `seq === base + index` for every held event.
40
+ */
41
+ private base = 0;
42
+
43
+ constructor(seed: ReadonlyArray<MoxxyEvent> = [], opts: { now?: () => number } = {}) {
44
+ this.now = opts.now ?? Date.now;
45
+ for (const e of seed) this.events.push(e);
46
+ // Align `base` to the first seeded event so `seq === base + index` holds
47
+ // regardless of where the seed starts. Today every caller seeds a log
48
+ // re-sequenced to 0..n-1 (so this is a no-op), but a caller seeding a tail
49
+ // slice (e.g. seq 50..) would otherwise get silently wrong at()/slice()
50
+ // seq-addressing and an off-by-`base` ingest() contiguity gate.
51
+ if (seed.length > 0) this.base = seed[0]!.seq;
52
+ }
53
+
54
+ get length(): number {
55
+ return this.events.length;
56
+ }
57
+
58
+ /** Seq of the first held event (see {@link rebase}). */
59
+ get baseSeq(): number {
60
+ return this.base;
61
+ }
62
+
63
+ at(seq: number): MoxxyEvent | undefined {
64
+ const index = seq - this.base;
65
+ if (index < 0 || index >= this.events.length) return undefined;
66
+ return this.events[index];
67
+ }
68
+
69
+ slice(from = 0, to: number = this.base + this.events.length): ReadonlyArray<MoxxyEvent> {
70
+ // Seq-addressed, like `at`. Events below the base were never held, so a
71
+ // `from` before it just clamps to everything we have.
72
+ return this.events.slice(Math.max(0, from - this.base), Math.max(0, to - this.base));
73
+ }
74
+
75
+ /** Build both secondary indexes from the current `events` array, preserving
76
+ * append order. Bucket arrays hold the original event references. */
77
+ private buildIndexes(): void {
78
+ const byType = new Map<MoxxyEventType, MoxxyEvent[]>();
79
+ const byTurnId = new Map<TurnId, MoxxyEvent[]>();
80
+ for (const e of this.events) {
81
+ let typeBucket = byType.get(e.type);
82
+ if (!typeBucket) byType.set(e.type, (typeBucket = []));
83
+ typeBucket.push(e);
84
+ let turnBucket = byTurnId.get(e.turnId);
85
+ if (!turnBucket) byTurnId.set(e.turnId, (turnBucket = []));
86
+ turnBucket.push(e);
87
+ }
88
+ this.byType = byType;
89
+ this.byTurnId = byTurnId;
90
+ }
91
+
92
+ /** Append one event to the live indexes (no-op while they're cold). Called
93
+ * after the event is pushed to `events`, so order is preserved. */
94
+ private indexEvent(e: MoxxyEvent): void {
95
+ if (this.byType) {
96
+ const bucket = this.byType.get(e.type);
97
+ if (bucket) bucket.push(e);
98
+ else this.byType.set(e.type, [e]);
99
+ }
100
+ if (this.byTurnId) {
101
+ const bucket = this.byTurnId.get(e.turnId);
102
+ if (bucket) bucket.push(e);
103
+ else this.byTurnId.set(e.turnId, [e]);
104
+ }
105
+ }
106
+
107
+ ofType<T extends MoxxyEventType>(type: T): ReadonlyArray<MoxxyEventOfType<T>> {
108
+ if (!this.byType) this.buildIndexes();
109
+ // The bucket already holds exactly the matching events in append order —
110
+ // identical to the old `events.filter(e => e.type === type)`. Return a copy
111
+ // so callers can't mutate the index (filter() also returned a fresh array).
112
+ const bucket = this.byType!.get(type);
113
+ // Bucket holds only events whose `type === T`, so the cast is sound; route
114
+ // through `unknown` because TS can't narrow the heterogeneous union here.
115
+ return (bucket ? [...bucket] : []) as unknown as ReadonlyArray<MoxxyEventOfType<T>>;
116
+ }
117
+
118
+ byTurn(turnId: TurnId): ReadonlyArray<MoxxyEvent> {
119
+ if (!this.byTurnId) this.buildIndexes();
120
+ const bucket = this.byTurnId!.get(turnId);
121
+ return bucket ? [...bucket] : [];
122
+ }
123
+
124
+ toJSON(): ReadonlyArray<MoxxyEvent> {
125
+ return [...this.events];
126
+ }
127
+
128
+ async append(partial: EmittedEvent): Promise<MoxxyEvent> {
129
+ const event = materializeEvent(partial, this.base + this.events.length, this.now);
130
+ this.events.push(event);
131
+ this.indexEvent(event);
132
+ // Snapshot listeners so a subscribe/unsubscribe during dispatch (e.g.,
133
+ // a runTurn finishing and unsubscribing while we're still mid-fanout)
134
+ // doesn't change the iteration target.
135
+ const snapshot = [...this.listeners];
136
+ for (const fn of snapshot) {
137
+ try {
138
+ await fn(event);
139
+ } catch {
140
+ // Listeners must not block the log; failures are non-fatal here. Hook
141
+ // failures are recorded as ErrorEvents by the dispatcher above this.
142
+ }
143
+ }
144
+ return event;
145
+ }
146
+
147
+ subscribe(fn: EventListener): () => void {
148
+ this.listeners.add(fn);
149
+ return () => this.listeners.delete(fn);
150
+ }
151
+
152
+ /**
153
+ * Append an already-materialized event, preserving its `id`/`seq`/`ts`
154
+ * rather than minting new ones. This is how a thin client mirrors the
155
+ * runner's log: the server is the sole authority for event identity, so the
156
+ * mirror must keep the originals. De-dupes by `seq` (idempotent replay) and
157
+ * fires listeners fire-and-forget - a mirror has no dispatcher to await.
158
+ *
159
+ * Not for normal authoring; use {@link append} for that.
160
+ */
161
+ ingest(event: MoxxyEvent): void {
162
+ // Contiguous, ordered delivery over a single socket means
163
+ // seq === base + index. Accept ONLY the next-expected seq: this drops
164
+ // duplicates (overlap between attach-replay and the live stream,
165
+ // seq < base + length) AND refuses a gap (seq > base + length) rather than
166
+ // pushing it at the wrong index, which would permanently desync `seq` from
167
+ // the array index. A gap can't happen over the reliable in-order
168
+ // transport, so refusing one is fail-safe.
169
+ if (event.seq !== this.base + this.events.length) return;
170
+ this.events.push(event);
171
+ this.indexEvent(event);
172
+ const snapshot = [...this.listeners];
173
+ for (const fn of snapshot) {
174
+ try {
175
+ // Fire-and-forget, but attach a rejection handler: an async listener
176
+ // that rejects must be swallowed the same way append()'s awaited
177
+ // try/catch swallows it — otherwise it surfaces as an unhandled
178
+ // rejection and (under Node's default policy) can kill the process.
179
+ void Promise.resolve(fn(event)).catch(() => {
180
+ // Mirror listeners must not break ingestion.
181
+ });
182
+ } catch {
183
+ // Mirror listeners must not break ingestion (synchronous throw).
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Start this (empty) log at `seq` instead of 0. A mirror primed by a
190
+ * PARTIAL attach replay (`replay: 'none'` / `{ tail }`) calls this with the
191
+ * runner's announced first seq so {@link ingest}'s contiguity gate accepts
192
+ * the stream. Only valid while empty — rebasing held events would detach
193
+ * their seqs from their indices.
194
+ */
195
+ rebase(seq: number): void {
196
+ if (this.events.length > 0) {
197
+ throw new Error(`EventLog.rebase(${seq}): log already holds ${this.events.length} events`);
198
+ }
199
+ if (!Number.isInteger(seq) || seq < 0) {
200
+ throw new Error(`EventLog.rebase(${seq}): seq must be a non-negative integer`);
201
+ }
202
+ this.base = seq;
203
+ // Rebase only runs on an empty log, so the indexes (if warm) are already
204
+ // empty — but null them to stay defensive about the events-array invariant.
205
+ this.byType = null;
206
+ this.byTurnId = null;
207
+ }
208
+
209
+ /**
210
+ * Drop every event from the log. Used by `/new` to start a fresh
211
+ * session without rebuilding the entire Session object — the
212
+ * registries, resolvers, and active provider stay; only the
213
+ * conversation context vanishes. Per-event listeners are NOT
214
+ * notified (there's no "event removed" event in the schema), but
215
+ * {@link onClear} subscribers fire — that's how the persistence
216
+ * sidecar truncates its JSONL (so `--resume` can't resurrect wiped
217
+ * history) and how the runner broadcasts a reset to attached
218
+ * mirrors, in lockstep with the wipe.
219
+ *
220
+ * Safe to call only when no turn is in flight — callers should abort
221
+ * their AbortController and await any pending runTurn() first.
222
+ */
223
+ clear(): void {
224
+ this.events.length = 0;
225
+ // The secondary indexes mirror `events`; drop them so the next query
226
+ // rebuilds from the now-empty array (cheap) rather than serving stale
227
+ // buckets.
228
+ this.byType = null;
229
+ this.byTurnId = null;
230
+ // A session reset restarts the authoritative stream at seq 0 — a rebased
231
+ // mirror must follow, or it would wait forever for seqs that never come.
232
+ this.base = 0;
233
+ const snapshot = [...this.clearListeners];
234
+ for (const fn of snapshot) {
235
+ try {
236
+ fn();
237
+ } catch {
238
+ // A clear listener must not block the wipe.
239
+ }
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Subscribe to {@link clear}. Fires synchronously after the events array
245
+ * empties, so a listener reading the log observes the post-wipe state.
246
+ * Returns the unsubscribe callback.
247
+ */
248
+ onClear(fn: () => void): () => void {
249
+ this.clearListeners.add(fn);
250
+ return () => this.clearListeners.delete(fn);
251
+ }
252
+
253
+ asReader(): EventLogReader {
254
+ return this;
255
+ }
256
+ }
package/src/index.ts ADDED
@@ -0,0 +1,102 @@
1
+ export { Session, type SessionOptions } from './session.js';
2
+ export { runTurn, collectTurn, type RunTurnOptions } from './run-turn.js';
3
+ export {
4
+ setupAgent,
5
+ type Agent,
6
+ type SetupAgentOptions,
7
+ type AgentPreset,
8
+ } from './setup-agent.js';
9
+ // Re-export the @moxxy/sdk types that appear in the setupAgent / Agent surface,
10
+ // so the programmatic API is fully typed from a single `@moxxy/core` import.
11
+ export type { MoxxyEvent, Plugin, ToolDef, PermissionResolver } from '@moxxy/sdk';
12
+ export { createSubagentSpawner, clearRetainedChildren, type SubagentRuntime } from './subagents.js';
13
+ export {
14
+ loadPreferences,
15
+ savePreferences,
16
+ preferencesPath,
17
+ type MoxxyPreferences,
18
+ } from './preferences.js';
19
+ export {
20
+ loadUsageStats,
21
+ mergeUsageStats,
22
+ clearUsageStats,
23
+ usageStatsPath,
24
+ type UsageStatsFile,
25
+ type StoredModelUsage,
26
+ } from './usage-stats.js';
27
+ export { SkillRegistryImpl } from './registries/skills.js';
28
+ export {
29
+ parseSkillFile,
30
+ parseFrontmatter,
31
+ discoverSkills,
32
+ defaultUserSkillsDir,
33
+ defaultProjectSkillsDir,
34
+ synthesizeSkill,
35
+ buildSynthesizeSkillPlugin,
36
+ type SkillLoadOptions,
37
+ type DiscoveredSkill,
38
+ type SynthesizeOptions,
39
+ type SynthesizedSkill,
40
+ } from './skills/index.js';
41
+ export { EventLog, type EventListener } from './events/log.js';
42
+ // newEventId + materializeEvent are module-private helpers used only by
43
+ // EventLog.append; not re-exported.
44
+ export { newTurnId, newSessionId } from './events/factory.js';
45
+ export { ToolRegistryImpl, type ToolRegistry } from './registries/tools.js';
46
+ export { ProviderRegistry } from './registries/providers.js';
47
+ export { ModeRegistry } from './registries/modes.js';
48
+ export { CompactorRegistry } from './registries/compactors.js';
49
+ export { CacheStrategyRegistry } from './registries/cache-strategies.js';
50
+ export { ViewRendererRegistry } from './registries/view-renderers.js';
51
+ export { defaultViewRenderer } from './view/default-renderer.js';
52
+ export { parseView, validateDoc, countNodes } from './view/parse.js';
53
+ export { TunnelProviderRegistry } from './registries/tunnel-providers.js';
54
+ export { localhostTunnel } from './tunnel/localhost.js';
55
+ export { ChannelRegistryImpl } from './registries/channels.js';
56
+ export { AgentRegistry } from './registries/agents.js';
57
+ export { CommandRegistry } from './registries/commands.js';
58
+ export { TranscriberRegistry } from './registries/transcribers.js';
59
+ export { SynthesizerRegistry } from './registries/synthesizers.js';
60
+ export { EmbedderRegistry } from './registries/embedders.js';
61
+ export { IsolatorRegistry as ContributedIsolatorRegistry } from './registries/isolators.js';
62
+ export { RequirementRegistry, type RequirementRegistryOptions } from './requirements.js';
63
+ export {
64
+ SessionPersistence,
65
+ defaultSessionsDir,
66
+ readIndex as readSessionIndex,
67
+ restoreEvents as restoreSessionEvents,
68
+ deleteSession,
69
+ type SessionMeta,
70
+ type SessionPersistenceOpts,
71
+ } from './sessions/persistence.js';
72
+ export {
73
+ PluginHost,
74
+ PluginRequirementError,
75
+ type PluginLoader,
76
+ type PluginSkipReason,
77
+ type PluginSkipRecord,
78
+ type PluginSkipSource,
79
+ type RegisterStaticOptions,
80
+ } from './plugins/host.js';
81
+ export { HookDispatcherImpl } from './plugins/lifecycle.js';
82
+ export { discoverPlugins } from './plugins/discovery.js';
83
+ export { toposortPluginManifests, PluginCycleError } from './plugins/toposort.js';
84
+ export { readPackageMoxxyRequirements } from './plugins/package-requirements.js';
85
+ export { createPluginLoader, type JitiLoaderOptions } from './plugins/loader.js';
86
+ export {
87
+ PermissionEngine,
88
+ permissionPolicySchema,
89
+ type PermissionPolicy,
90
+ type PolicyRule,
91
+ } from './permissions/engine.js';
92
+ export {
93
+ autoAllowResolver,
94
+ denyByDefaultResolver,
95
+ createCallbackResolver,
96
+ createDeferredPermissionResolver,
97
+ type DeferredPermissionResolver,
98
+ type DeferredPermissionResolverOptions,
99
+ type PermissionPromptHandler,
100
+ createAllowListResolver,
101
+ } from './permissions/resolvers.js';
102
+ export { createLogger, silentLogger, type Logger, type LogLevel } from './logger.js';
package/src/logger.ts ADDED
@@ -0,0 +1,46 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+
3
+ export interface Logger {
4
+ debug(msg: string, meta?: Record<string, unknown>): void;
5
+ info(msg: string, meta?: Record<string, unknown>): void;
6
+ warn(msg: string, meta?: Record<string, unknown>): void;
7
+ error(msg: string, meta?: Record<string, unknown>): void;
8
+ child(bindings: Record<string, unknown>): Logger;
9
+ }
10
+
11
+ const levelRank: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };
12
+
13
+ export interface LoggerOptions {
14
+ minLevel?: LogLevel;
15
+ sink?: (line: string) => void;
16
+ bindings?: Record<string, unknown>;
17
+ }
18
+
19
+ export function createLogger(opts: LoggerOptions = {}): Logger {
20
+ const minLevel = opts.minLevel ?? 'info';
21
+ const sink = opts.sink ?? ((line) => process.stderr.write(line + '\n'));
22
+ const bindings = opts.bindings ?? {};
23
+
24
+ const emit = (level: LogLevel, msg: string, meta?: Record<string, unknown>): void => {
25
+ if (levelRank[level] < levelRank[minLevel]) return;
26
+ const payload = { ts: new Date().toISOString(), level, msg, ...bindings, ...(meta ?? {}) };
27
+ sink(JSON.stringify(payload));
28
+ };
29
+
30
+ return {
31
+ debug: (msg, meta) => emit('debug', msg, meta),
32
+ info: (msg, meta) => emit('info', msg, meta),
33
+ warn: (msg, meta) => emit('warn', msg, meta),
34
+ error: (msg, meta) => emit('error', msg, meta),
35
+ child: (extra) =>
36
+ createLogger({ minLevel, sink, bindings: { ...bindings, ...extra } }),
37
+ };
38
+ }
39
+
40
+ export const silentLogger: Logger = {
41
+ debug: () => {},
42
+ info: () => {},
43
+ warn: () => {},
44
+ error: () => {},
45
+ child: () => silentLogger,
46
+ };
@@ -0,0 +1,206 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { promises as fs } from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { asToolCallId } from '@moxxy/sdk';
6
+ import { PermissionEngine } from './engine.js';
7
+
8
+ const call = (name: string, input: unknown = {}) => ({
9
+ callId: asToolCallId('c'),
10
+ name,
11
+ input,
12
+ });
13
+
14
+ describe('PermissionEngine', () => {
15
+ it('returns null when no rules match', () => {
16
+ const e = new PermissionEngine();
17
+ expect(e.check(call('Read'))).toBeNull();
18
+ });
19
+
20
+ it('matches exact allow rule', () => {
21
+ const e = new PermissionEngine({
22
+ allow: [{ name: 'Read' }],
23
+ deny: [],
24
+ });
25
+ expect(e.check(call('Read'))?.mode).toBe('allow');
26
+ });
27
+
28
+ it('deny takes priority over allow', () => {
29
+ const e = new PermissionEngine({
30
+ allow: [{ name: '*' }],
31
+ deny: [{ name: 'Bash' }],
32
+ });
33
+ expect(e.check(call('Bash'))?.mode).toBe('deny');
34
+ expect(e.check(call('Read'))?.mode).toBe('allow');
35
+ });
36
+
37
+ it('supports glob in name pattern', () => {
38
+ const e = new PermissionEngine({
39
+ allow: [{ name: 'mcp-*' }],
40
+ deny: [],
41
+ });
42
+ expect(e.check(call('mcp-filesystem-read'))?.mode).toBe('allow');
43
+ expect(e.check(call('Read'))).toBeNull();
44
+ });
45
+
46
+ it('respects inputMatches via regex', () => {
47
+ const e = new PermissionEngine({
48
+ allow: [{ name: 'Bash', inputMatches: { cmd: '^ls' } }],
49
+ deny: [],
50
+ });
51
+ expect(e.check(call('Bash', { cmd: 'ls -la' }))?.mode).toBe('allow');
52
+ expect(e.check(call('Bash', { cmd: 'rm -rf /' }))).toBeNull();
53
+ });
54
+
55
+ it('inputMatches is an unanchored substring match by design (u40-2)', () => {
56
+ // Documented, stable contract: `inputMatches` values are UNANCHORED regexes
57
+ // (re.test → substring match). An author who needs a full match anchors
58
+ // their own pattern. We pin both halves of that contract so it can never be
59
+ // silently changed (anchoring it would break existing permission files).
60
+ const substring = new PermissionEngine({
61
+ allow: [{ name: 'Read', inputMatches: { path: 'config' } }],
62
+ deny: [],
63
+ });
64
+ // Substring pattern matches anywhere in the candidate — by design.
65
+ expect(substring.check(call('Read', { path: '/etc/config' }))?.mode).toBe('allow');
66
+ expect(substring.check(call('Read', { path: '/etc/config-evil' }))?.mode).toBe('allow');
67
+ expect(substring.check(call('Read', { path: '~/.ssh/config-backup' }))?.mode).toBe('allow');
68
+ // A path that does not contain the pattern at all is not matched.
69
+ expect(substring.check(call('Read', { path: '/etc/passwd' }))).toBeNull();
70
+
71
+ // An author who wants a full match supplies their own ^…$ anchors.
72
+ const anchored = new PermissionEngine({
73
+ allow: [{ name: 'Read', inputMatches: { path: '^/etc/config$' } }],
74
+ deny: [],
75
+ });
76
+ expect(anchored.check(call('Read', { path: '/etc/config' }))?.mode).toBe('allow');
77
+ expect(anchored.check(call('Read', { path: '/etc/config-evil' }))).toBeNull();
78
+ });
79
+
80
+ it('deny rule with an invalid-regex inputMatches still denies (fails closed)', () => {
81
+ // Unbalanced bracket = uncompilable regex. The user clearly intended to
82
+ // block `rm -rf`; the old fallback (literal !== equality) silently turned
83
+ // this into a no-op deny, letting the dangerous command through.
84
+ const e = new PermissionEngine({
85
+ allow: [],
86
+ deny: [{ name: 'Bash', inputMatches: { cmd: 'rm -rf [' } }],
87
+ });
88
+ // A call whose input contains that substring (and any other call to the
89
+ // named tool) must still be denied — never silently allowed.
90
+ expect(e.check(call('Bash', { cmd: 'rm -rf /' }))?.mode).toBe('deny');
91
+ expect(e.check(call('Bash', { cmd: 'echo rm -rf [' }))?.mode).toBe('deny');
92
+ // A call to a different tool is unaffected by the bad rule.
93
+ expect(e.check(call('Read', { cmd: 'whatever' }))).toBeNull();
94
+ });
95
+
96
+ it('allow rule with an invalid-regex inputMatches does NOT over-grant', () => {
97
+ const e = new PermissionEngine({
98
+ allow: [{ name: 'Bash', inputMatches: { cmd: 'ls [' } }],
99
+ deny: [],
100
+ });
101
+ // The bad pattern can never compile, so the allow rule must not grant —
102
+ // not even for a call whose input literally contains the broken text.
103
+ expect(e.check(call('Bash', { cmd: 'ls -la' }))).toBeNull();
104
+ expect(e.check(call('Bash', { cmd: 'ls [' }))).toBeNull();
105
+ });
106
+
107
+ it('inputMatches against a structured (object/array) field matches its JSON form', () => {
108
+ // u40-4: a structured tool-input field used to coerce to '[object Object]',
109
+ // so an inputMatches regex could never match it. It is now JSON-serialized.
110
+ const e = new PermissionEngine({
111
+ allow: [],
112
+ deny: [{ name: 'shell', inputMatches: { argv: '"--force"' } }],
113
+ });
114
+ // The deny rule matches the serialized array element.
115
+ expect(e.check(call('shell', { argv: ['rm', '--force'] }))?.mode).toBe('deny');
116
+ // …and does not match an argv without it.
117
+ expect(e.check(call('shell', { argv: ['ls', '-la'] }))).toBeNull();
118
+ });
119
+
120
+ it('valid-regex deny rule still behaves exactly as before', () => {
121
+ const e = new PermissionEngine({
122
+ allow: [],
123
+ deny: [{ name: 'Bash', inputMatches: { cmd: '^rm ' } }],
124
+ });
125
+ expect(e.check(call('Bash', { cmd: 'rm -rf /' }))?.mode).toBe('deny');
126
+ expect(e.check(call('Bash', { cmd: 'ls -la' }))).toBeNull();
127
+ });
128
+
129
+ it('valid-regex allow rule still behaves exactly as before', () => {
130
+ const e = new PermissionEngine({
131
+ allow: [{ name: 'Bash', inputMatches: { cmd: '^ls' } }],
132
+ deny: [],
133
+ });
134
+ expect(e.check(call('Bash', { cmd: 'ls -la' }))?.mode).toBe('allow');
135
+ expect(e.check(call('Bash', { cmd: 'rm -rf /' }))).toBeNull();
136
+ });
137
+
138
+ it('loads policy from disk and handles ENOENT', async () => {
139
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mox-perm-'));
140
+ const file = path.join(tmp, 'permissions.json');
141
+ const e1 = await PermissionEngine.load(file);
142
+ expect(e1.check(call('Read'))).toBeNull();
143
+
144
+ await fs.writeFile(file, JSON.stringify({ allow: [{ name: 'Read' }], deny: [] }));
145
+ const e2 = await PermissionEngine.load(file);
146
+ expect(e2.check(call('Read'))?.mode).toBe('allow');
147
+ });
148
+
149
+ it('persists addAllow', async () => {
150
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mox-perm-'));
151
+ const file = path.join(tmp, 'permissions.json');
152
+ const e = await PermissionEngine.load(file);
153
+ await e.addAllow({ name: 'Edit' });
154
+ const raw = JSON.parse(await fs.readFile(file, 'utf8'));
155
+ expect(raw.allow[0].name).toBe('Edit');
156
+ });
157
+
158
+ it('persists inputMatches with addAllow (regression for silent drop)', async () => {
159
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mox-perm-'));
160
+ const file = path.join(tmp, 'permissions.json');
161
+ const e = await PermissionEngine.load(file);
162
+ await e.addAllow({
163
+ name: 'Bash',
164
+ inputMatches: { cmd: '^ls' },
165
+ reason: 'safe listings only',
166
+ });
167
+ const raw = JSON.parse(await fs.readFile(file, 'utf8'));
168
+ expect(raw.allow[0]).toEqual({
169
+ name: 'Bash',
170
+ inputMatches: { cmd: '^ls' },
171
+ reason: 'safe listings only',
172
+ });
173
+
174
+ // Reload and verify it still matches correctly.
175
+ const e2 = await PermissionEngine.load(file);
176
+ expect(e2.check(call('Bash', { cmd: 'ls -la' }))?.mode).toBe('allow');
177
+ expect(e2.check(call('Bash', { cmd: 'rm -rf /' }))).toBeNull();
178
+ });
179
+
180
+ it('writes the policy file atomically (tmp+rename, no .tmp residue)', async () => {
181
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mox-perm-'));
182
+ const file = path.join(tmp, 'permissions.json');
183
+ const e = await PermissionEngine.load(file);
184
+ await e.addAllow({ name: 'A' });
185
+ const after = await fs.readdir(tmp);
186
+ expect(after.filter((f) => f.startsWith('permissions.json.tmp.'))).toEqual([]);
187
+ expect(after).toContain('permissions.json');
188
+ });
189
+
190
+ it('serializes concurrent mutators so no rule is lost (in-memory + on disk)', async () => {
191
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mox-perm-'));
192
+ const file = path.join(tmp, 'permissions.json');
193
+ try {
194
+ const e = await PermissionEngine.load(file);
195
+ // Fire many adds without awaiting between them. Without the per-instance
196
+ // mutex, overlapping persists rename out of order and the final file
197
+ // reflects a stale snapshot (rows dropped).
198
+ await Promise.all(Array.from({ length: 20 }, (_, i) => e.addAllow({ name: `tool-${i}` })));
199
+ expect(e.policySnapshot.allow).toHaveLength(20);
200
+ const reloaded = await PermissionEngine.load(file);
201
+ expect(reloaded.policySnapshot.allow).toHaveLength(20);
202
+ } finally {
203
+ await fs.rm(tmp, { recursive: true, force: true });
204
+ }
205
+ });
206
+ });