@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.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/events/factory.d.ts +6 -0
- package/dist/events/factory.d.ts.map +1 -0
- package/dist/events/factory.js +14 -0
- package/dist/events/factory.js.map +1 -0
- package/dist/events/log.d.ts +92 -0
- package/dist/events/log.d.ts.map +1 -0
- package/dist/events/log.js +244 -0
- package/dist/events/log.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +27 -0
- package/dist/logger.js.map +1 -0
- package/dist/permissions/engine.d.ts +94 -0
- package/dist/permissions/engine.d.ts.map +1 -0
- package/dist/permissions/engine.js +203 -0
- package/dist/permissions/engine.js.map +1 -0
- package/dist/permissions/resolvers.d.ts +8 -0
- package/dist/permissions/resolvers.d.ts.map +1 -0
- package/dist/permissions/resolvers.js +8 -0
- package/dist/permissions/resolvers.js.map +1 -0
- package/dist/plugins/discovery.d.ts +9 -0
- package/dist/plugins/discovery.d.ts.map +1 -0
- package/dist/plugins/discovery.js +112 -0
- package/dist/plugins/discovery.js.map +1 -0
- package/dist/plugins/host-options.d.ts +72 -0
- package/dist/plugins/host-options.d.ts.map +1 -0
- package/dist/plugins/host-options.js +2 -0
- package/dist/plugins/host-options.js.map +1 -0
- package/dist/plugins/host.d.ts +53 -0
- package/dist/plugins/host.d.ts.map +1 -0
- package/dist/plugins/host.js +269 -0
- package/dist/plugins/host.js.map +1 -0
- package/dist/plugins/lifecycle.d.ts +31 -0
- package/dist/plugins/lifecycle.d.ts.map +1 -0
- package/dist/plugins/lifecycle.js +100 -0
- package/dist/plugins/lifecycle.js.map +1 -0
- package/dist/plugins/loader.d.ts +7 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +68 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/package-requirements.d.ts +16 -0
- package/dist/plugins/package-requirements.d.ts.map +1 -0
- package/dist/plugins/package-requirements.js +52 -0
- package/dist/plugins/package-requirements.js.map +1 -0
- package/dist/plugins/registry-kinds.d.ts +58 -0
- package/dist/plugins/registry-kinds.d.ts.map +1 -0
- package/dist/plugins/registry-kinds.js +139 -0
- package/dist/plugins/registry-kinds.js.map +1 -0
- package/dist/plugins/toposort.d.ts +24 -0
- package/dist/plugins/toposort.d.ts.map +1 -0
- package/dist/plugins/toposort.js +67 -0
- package/dist/plugins/toposort.js.map +1 -0
- package/dist/preferences.d.ts +40 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +58 -0
- package/dist/preferences.js.map +1 -0
- package/dist/registries/active-backend-registry.d.ts +65 -0
- package/dist/registries/active-backend-registry.d.ts.map +1 -0
- package/dist/registries/active-backend-registry.js +108 -0
- package/dist/registries/active-backend-registry.js.map +1 -0
- package/dist/registries/active-def-registry.d.ts +59 -0
- package/dist/registries/active-def-registry.d.ts.map +1 -0
- package/dist/registries/active-def-registry.js +63 -0
- package/dist/registries/active-def-registry.js.map +1 -0
- package/dist/registries/agents.d.ts +16 -0
- package/dist/registries/agents.d.ts.map +1 -0
- package/dist/registries/agents.js +17 -0
- package/dist/registries/agents.js.map +1 -0
- package/dist/registries/cache-strategies.d.ts +12 -0
- package/dist/registries/cache-strategies.d.ts.map +1 -0
- package/dist/registries/cache-strategies.js +13 -0
- package/dist/registries/cache-strategies.js.map +1 -0
- package/dist/registries/channels.d.ts +16 -0
- package/dist/registries/channels.d.ts.map +1 -0
- package/dist/registries/channels.js +35 -0
- package/dist/registries/channels.js.map +1 -0
- package/dist/registries/commands.d.ts +29 -0
- package/dist/registries/commands.d.ts.map +1 -0
- package/dist/registries/commands.js +72 -0
- package/dist/registries/commands.js.map +1 -0
- package/dist/registries/compactors.d.ts +12 -0
- package/dist/registries/compactors.d.ts.map +1 -0
- package/dist/registries/compactors.js +13 -0
- package/dist/registries/compactors.js.map +1 -0
- package/dist/registries/def-map-registry.d.ts +33 -0
- package/dist/registries/def-map-registry.d.ts.map +1 -0
- package/dist/registries/def-map-registry.js +36 -0
- package/dist/registries/def-map-registry.js.map +1 -0
- package/dist/registries/embedders.d.ts +16 -0
- package/dist/registries/embedders.d.ts.map +1 -0
- package/dist/registries/embedders.js +17 -0
- package/dist/registries/embedders.js.map +1 -0
- package/dist/registries/isolators.d.ts +21 -0
- package/dist/registries/isolators.d.ts.map +1 -0
- package/dist/registries/isolators.js +32 -0
- package/dist/registries/isolators.js.map +1 -0
- package/dist/registries/modes.d.ts +28 -0
- package/dist/registries/modes.d.ts.map +1 -0
- package/dist/registries/modes.js +81 -0
- package/dist/registries/modes.js.map +1 -0
- package/dist/registries/providers.d.ts +44 -0
- package/dist/registries/providers.d.ts.map +1 -0
- package/dist/registries/providers.js +95 -0
- package/dist/registries/providers.js.map +1 -0
- package/dist/registries/skills.d.ts +24 -0
- package/dist/registries/skills.d.ts.map +1 -0
- package/dist/registries/skills.js +81 -0
- package/dist/registries/skills.js.map +1 -0
- package/dist/registries/surfaces.d.ts +13 -0
- package/dist/registries/surfaces.d.ts.map +1 -0
- package/dist/registries/surfaces.js +14 -0
- package/dist/registries/surfaces.js.map +1 -0
- package/dist/registries/synthesizers.d.ts +26 -0
- package/dist/registries/synthesizers.d.ts.map +1 -0
- package/dist/registries/synthesizers.js +34 -0
- package/dist/registries/synthesizers.js.map +1 -0
- package/dist/registries/tools.d.ts +49 -0
- package/dist/registries/tools.d.ts.map +1 -0
- package/dist/registries/tools.js +98 -0
- package/dist/registries/tools.js.map +1 -0
- package/dist/registries/transcribers.d.ts +17 -0
- package/dist/registries/transcribers.d.ts.map +1 -0
- package/dist/registries/transcribers.js +18 -0
- package/dist/registries/transcribers.js.map +1 -0
- package/dist/registries/tunnel-providers.d.ts +12 -0
- package/dist/registries/tunnel-providers.d.ts.map +1 -0
- package/dist/registries/tunnel-providers.js +13 -0
- package/dist/registries/tunnel-providers.js.map +1 -0
- package/dist/registries/view-renderers.d.ts +13 -0
- package/dist/registries/view-renderers.d.ts.map +1 -0
- package/dist/registries/view-renderers.js +14 -0
- package/dist/registries/view-renderers.js.map +1 -0
- package/dist/registries/workflow-executors.d.ts +13 -0
- package/dist/registries/workflow-executors.d.ts.map +1 -0
- package/dist/registries/workflow-executors.js +14 -0
- package/dist/registries/workflow-executors.js.map +1 -0
- package/dist/requirements.d.ts +59 -0
- package/dist/requirements.d.ts.map +1 -0
- package/dist/requirements.js +149 -0
- package/dist/requirements.js.map +1 -0
- package/dist/run-turn.d.ts +6 -0
- package/dist/run-turn.d.ts.map +1 -0
- package/dist/run-turn.js +141 -0
- package/dist/run-turn.js.map +1 -0
- package/dist/session-runtime.d.ts +67 -0
- package/dist/session-runtime.d.ts.map +1 -0
- package/dist/session-runtime.js +2 -0
- package/dist/session-runtime.js.map +1 -0
- package/dist/session.d.ts +203 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +399 -0
- package/dist/session.js.map +1 -0
- package/dist/sessions/persistence.d.ts +184 -0
- package/dist/sessions/persistence.d.ts.map +1 -0
- package/dist/sessions/persistence.js +442 -0
- package/dist/sessions/persistence.js.map +1 -0
- package/dist/setup-agent.d.ts +91 -0
- package/dist/setup-agent.d.ts.map +1 -0
- package/dist/setup-agent.js +138 -0
- package/dist/setup-agent.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +4 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/loader.d.ts +16 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +99 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/parse.d.ts +5 -0
- package/dist/skills/parse.d.ts.map +1 -0
- package/dist/skills/parse.js +8 -0
- package/dist/skills/parse.js.map +1 -0
- package/dist/skills/synthesize-draft.d.ts +8 -0
- package/dist/skills/synthesize-draft.d.ts.map +1 -0
- package/dist/skills/synthesize-draft.js +32 -0
- package/dist/skills/synthesize-draft.js.map +1 -0
- package/dist/skills/synthesize.d.ts +33 -0
- package/dist/skills/synthesize.d.ts.map +1 -0
- package/dist/skills/synthesize.js +241 -0
- package/dist/skills/synthesize.js.map +1 -0
- package/dist/subagents/events.d.ts +20 -0
- package/dist/subagents/events.d.ts.map +1 -0
- package/dist/subagents/events.js +146 -0
- package/dist/subagents/events.js.map +1 -0
- package/dist/subagents/registry.d.ts +40 -0
- package/dist/subagents/registry.d.ts.map +1 -0
- package/dist/subagents/registry.js +42 -0
- package/dist/subagents/registry.js.map +1 -0
- package/dist/subagents/run-child.d.ts +43 -0
- package/dist/subagents/run-child.d.ts.map +1 -0
- package/dist/subagents/run-child.js +276 -0
- package/dist/subagents/run-child.js.map +1 -0
- package/dist/subagents/spawn.d.ts +8 -0
- package/dist/subagents/spawn.d.ts.map +1 -0
- package/dist/subagents/spawn.js +8 -0
- package/dist/subagents/spawn.js.map +1 -0
- package/dist/subagents/tools.d.ts +4 -0
- package/dist/subagents/tools.d.ts.map +1 -0
- package/dist/subagents/tools.js +13 -0
- package/dist/subagents/tools.js.map +1 -0
- package/dist/subagents.d.ts +3 -0
- package/dist/subagents.d.ts.map +1 -0
- package/dist/subagents.js +3 -0
- package/dist/subagents.js.map +1 -0
- package/dist/surfaces/host.d.ts +53 -0
- package/dist/surfaces/host.d.ts.map +1 -0
- package/dist/surfaces/host.js +177 -0
- package/dist/surfaces/host.js.map +1 -0
- package/dist/tunnel/localhost.d.ts +7 -0
- package/dist/tunnel/localhost.d.ts.map +1 -0
- package/dist/tunnel/localhost.js +21 -0
- package/dist/tunnel/localhost.js.map +1 -0
- package/dist/usage-stats.d.ts +42 -0
- package/dist/usage-stats.d.ts.map +1 -0
- package/dist/usage-stats.js +83 -0
- package/dist/usage-stats.js.map +1 -0
- package/dist/view/default-renderer.d.ts +7 -0
- package/dist/view/default-renderer.d.ts.map +1 -0
- package/dist/view/default-renderer.js +14 -0
- package/dist/view/default-renderer.js.map +1 -0
- package/dist/view/parse.d.ts +7 -0
- package/dist/view/parse.d.ts.map +1 -0
- package/dist/view/parse.js +479 -0
- package/dist/view/parse.js.map +1 -0
- package/package.json +61 -0
- package/src/events/factory.ts +27 -0
- package/src/events/log.test.ts +470 -0
- package/src/events/log.ts +256 -0
- package/src/index.ts +102 -0
- package/src/logger.ts +46 -0
- package/src/permissions/engine.test.ts +206 -0
- package/src/permissions/engine.ts +247 -0
- package/src/permissions/resolvers.test.ts +40 -0
- package/src/permissions/resolvers.ts +17 -0
- package/src/plugins/discovery.test.ts +115 -0
- package/src/plugins/discovery.ts +124 -0
- package/src/plugins/host-options.ts +73 -0
- package/src/plugins/host-view-tunnel.test.ts +86 -0
- package/src/plugins/host.test.ts +434 -0
- package/src/plugins/host.ts +327 -0
- package/src/plugins/lifecycle.test.ts +187 -0
- package/src/plugins/lifecycle.ts +136 -0
- package/src/plugins/loader.ts +79 -0
- package/src/plugins/package-requirements.ts +50 -0
- package/src/plugins/registry-kinds.test.ts +162 -0
- package/src/plugins/registry-kinds.ts +210 -0
- package/src/plugins/toposort.test.ts +66 -0
- package/src/plugins/toposort.ts +69 -0
- package/src/preferences.test.ts +105 -0
- package/src/preferences.ts +84 -0
- package/src/registries/active-backend-registry.ts +142 -0
- package/src/registries/active-def-registry.test.ts +103 -0
- package/src/registries/active-def-registry.ts +97 -0
- package/src/registries/agents.ts +18 -0
- package/src/registries/cache-strategies.ts +14 -0
- package/src/registries/channels.ts +42 -0
- package/src/registries/commands.test.ts +71 -0
- package/src/registries/commands.ts +78 -0
- package/src/registries/compactors.ts +14 -0
- package/src/registries/def-map-registry.test.ts +67 -0
- package/src/registries/def-map-registry.ts +59 -0
- package/src/registries/embedders.test.ts +54 -0
- package/src/registries/embedders.ts +18 -0
- package/src/registries/isolators.test.ts +43 -0
- package/src/registries/isolators.ts +38 -0
- package/src/registries/modes.test.ts +66 -0
- package/src/registries/modes.ts +80 -0
- package/src/registries/providers.test.ts +50 -0
- package/src/registries/providers.ts +97 -0
- package/src/registries/semantics.test.ts +117 -0
- package/src/registries/skills.ts +87 -0
- package/src/registries/surfaces.ts +18 -0
- package/src/registries/synthesizers.test.ts +63 -0
- package/src/registries/synthesizers.ts +36 -0
- package/src/registries/tools.test.ts +142 -0
- package/src/registries/tools.ts +145 -0
- package/src/registries/transcribers.test.ts +70 -0
- package/src/registries/transcribers.ts +19 -0
- package/src/registries/tunnel-providers.test.ts +30 -0
- package/src/registries/tunnel-providers.ts +14 -0
- package/src/registries/view-renderers.test.ts +68 -0
- package/src/registries/view-renderers.ts +15 -0
- package/src/registries/workflow-executors.test.ts +53 -0
- package/src/registries/workflow-executors.ts +15 -0
- package/src/requirements.targetinfo.test.ts +70 -0
- package/src/requirements.test.ts +111 -0
- package/src/requirements.ts +262 -0
- package/src/run-turn.test.ts +178 -0
- package/src/run-turn.ts +156 -0
- package/src/session-runtime.ts +75 -0
- package/src/session.test.ts +216 -0
- package/src/session.ts +491 -0
- package/src/sessions/persistence.flush.test.ts +100 -0
- package/src/sessions/persistence.ready.test.ts +91 -0
- package/src/sessions/persistence.test.ts +274 -0
- package/src/sessions/persistence.ts +498 -0
- package/src/setup-agent.test.ts +72 -0
- package/src/setup-agent.ts +189 -0
- package/src/skills/index.ts +14 -0
- package/src/skills/loader.test.ts +102 -0
- package/src/skills/loader.ts +114 -0
- package/src/skills/parse.test.ts +59 -0
- package/src/skills/parse.ts +11 -0
- package/src/skills/synthesize-draft.test.ts +79 -0
- package/src/skills/synthesize-draft.ts +46 -0
- package/src/skills/synthesize.test.ts +170 -0
- package/src/skills/synthesize.ts +304 -0
- package/src/subagents/events.test.ts +56 -0
- package/src/subagents/events.ts +194 -0
- package/src/subagents/registry.ts +62 -0
- package/src/subagents/run-child.test.ts +185 -0
- package/src/subagents/run-child.ts +425 -0
- package/src/subagents/spawn.ts +12 -0
- package/src/subagents/tools.ts +19 -0
- package/src/subagents.ts +2 -0
- package/src/surfaces/host.test.ts +330 -0
- package/src/surfaces/host.ts +192 -0
- package/src/tunnel/localhost.test.ts +22 -0
- package/src/tunnel/localhost.ts +21 -0
- package/src/usage-stats.test.ts +77 -0
- package/src/usage-stats.ts +119 -0
- package/src/view/default-renderer.test.ts +29 -0
- package/src/view/default-renderer.ts +14 -0
- package/src/view/parse-extended.test.ts +255 -0
- package/src/view/parse.test.ts +232 -0
- 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
|
+
});
|