@animalabs/agent-framework 0.9.0 → 0.11.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/dist/src/agent.d.ts +1 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +72 -4
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +699 -48
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +62 -7
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +333 -22
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/types.d.ts +56 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +11 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +21 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +2 -2
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +57 -13
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +21 -5
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +23 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +53 -3
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +11 -2
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +27 -11
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +9 -6
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/queue.d.ts.map +1 -1
- package/dist/src/queue.js +9 -2
- package/dist/src/queue.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +12 -2
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +12 -2
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +6 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +1 -1
- package/dist/src/types/trace.d.ts +7 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/busy-agent-scheduler.test.js +29 -0
- package/dist/test/busy-agent-scheduler.test.js.map +1 -1
- package/dist/test/discord-history-delete-window.test.d.ts +2 -0
- package/dist/test/discord-history-delete-window.test.d.ts.map +1 -0
- package/dist/test/discord-history-delete-window.test.js +81 -0
- package/dist/test/discord-history-delete-window.test.js.map +1 -0
- package/dist/test/discord-history-sync.test.d.ts +2 -0
- package/dist/test/discord-history-sync.test.d.ts.map +1 -0
- package/dist/test/discord-history-sync.test.js +56 -0
- package/dist/test/discord-history-sync.test.js.map +1 -0
- package/dist/test/event-gate-passthrough.test.d.ts +15 -0
- package/dist/test/event-gate-passthrough.test.d.ts.map +1 -0
- package/dist/test/event-gate-passthrough.test.js +289 -0
- package/dist/test/event-gate-passthrough.test.js.map +1 -0
- package/dist/test/explicit-prose-routing.test.js +179 -7
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/framework.test.js +33 -0
- package/dist/test/framework.test.js.map +1 -1
- package/dist/test/prose-stream-router.test.js +43 -0
- package/dist/test/prose-stream-router.test.js.map +1 -1
- package/dist/test/provider-acceleration-cooldown.test.d.ts +2 -0
- package/dist/test/provider-acceleration-cooldown.test.d.ts.map +1 -0
- package/dist/test/provider-acceleration-cooldown.test.js +286 -0
- package/dist/test/provider-acceleration-cooldown.test.js.map +1 -0
- package/dist/test/puppet-tool-call.test.d.ts +2 -0
- package/dist/test/puppet-tool-call.test.d.ts.map +1 -0
- package/dist/test/puppet-tool-call.test.js +106 -0
- package/dist/test/puppet-tool-call.test.js.map +1 -0
- package/dist/test/release-changelog.test.d.ts +2 -0
- package/dist/test/release-changelog.test.d.ts.map +1 -0
- package/dist/test/release-changelog.test.js +159 -0
- package/dist/test/release-changelog.test.js.map +1 -0
- package/dist/test/tool-result-spill.test.d.ts +1 -1
- package/dist/test/tool-result-spill.test.js +18 -9
- package/dist/test/tool-result-spill.test.js.map +1 -1
- package/dist/test/workspace-path-containment.test.d.ts +19 -0
- package/dist/test/workspace-path-containment.test.d.ts.map +1 -0
- package/dist/test/workspace-path-containment.test.js +91 -0
- package/dist/test/workspace-path-containment.test.js.map +1 -0
- package/dist/test/workspace-watcher.test.js +15 -3
- package/dist/test/workspace-watcher.test.js.map +1 -1
- package/dist/test/xml-round-persist.test.d.ts +19 -0
- package/dist/test/xml-round-persist.test.d.ts.map +1 -0
- package/dist/test/xml-round-persist.test.js +235 -0
- package/dist/test/xml-round-persist.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/agent.ts +1 -1
- package/src/framework.ts +733 -48
- package/src/gate/event-gate.ts +355 -24
- package/src/gate/types.ts +58 -0
- package/src/mcpl/prose-grammar.ts +21 -0
- package/src/mcpl/prose-stream-router.ts +48 -14
- package/src/modules/discord/index.ts +24 -6
- package/src/modules/workspace/index.ts +53 -3
- package/src/modules/workspace/sync.ts +39 -10
- package/src/modules/workspace/watcher.ts +9 -6
- package/src/queue.ts +8 -2
- package/src/tool-result-history.ts +12 -2
- package/src/types/agent.ts +6 -1
- package/src/types/framework.ts +1 -1
- package/src/types/trace.ts +13 -0
|
@@ -11,12 +11,24 @@
|
|
|
11
11
|
* inference tokens, not whether the agent should know about the event.
|
|
12
12
|
*/
|
|
13
13
|
import type { ToolDefinition } from '../types/events.js';
|
|
14
|
-
import type { GateConfig, GatePolicy, GateDecision, GateEventInfo, GateStatus } from './types.js';
|
|
14
|
+
import type { GateConfig, GatePolicy, GateDecision, GateEventInfo, GateStatus, ShadowWarning, GateProbeRow } from './types.js';
|
|
15
15
|
export type { GateConfig, GateOptions, GateDecision, GateEventInfo } from './types.js';
|
|
16
16
|
interface TraceEventLike {
|
|
17
17
|
type: string;
|
|
18
18
|
[key: string]: unknown;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Find rules made unreachable by earlier, broader rules (first match wins).
|
|
22
|
+
* The 2026-08-08 Mythos incident is the canonical catch: a prepended
|
|
23
|
+
* `{scope:[mcpl:channel-incoming]}` sampler silently made the
|
|
24
|
+
* direct-address and mention rules dead for the whole incoming lane.
|
|
25
|
+
* `passthrough` observers shadow nothing — non-firing matches fall through.
|
|
26
|
+
*/
|
|
27
|
+
export declare function findShadowedPolicies(policies: GatePolicy[]): ShadowWarning[];
|
|
28
|
+
/** Human-readable one-liner for a ShadowWarning, used in tool results,
|
|
29
|
+
* traces, and gate_status alike so the same hazard reads the same way
|
|
30
|
+
* everywhere. */
|
|
31
|
+
export declare function formatShadowWarning(w: ShadowWarning): string;
|
|
20
32
|
export declare class EventGate {
|
|
21
33
|
private configPath;
|
|
22
34
|
private config;
|
|
@@ -36,6 +48,8 @@ export declare class EventGate {
|
|
|
36
48
|
private inferring;
|
|
37
49
|
private inferenceBuffer;
|
|
38
50
|
private stats;
|
|
51
|
+
private shadowWarningsCache;
|
|
52
|
+
private shadowWarningsFingerprint;
|
|
39
53
|
private totalEvaluations;
|
|
40
54
|
private defaultTriggered;
|
|
41
55
|
private defaultSkipped;
|
|
@@ -87,13 +101,21 @@ export declare class EventGate {
|
|
|
87
101
|
* without waiting for the throttled mtime reload.
|
|
88
102
|
*
|
|
89
103
|
* The freshest on-disk config is re-read first, so a concurrent operator edit
|
|
90
|
-
* isn't clobbered. A policy with the same `name` is replaced IN PLACE
|
|
91
|
-
* preserved);
|
|
92
|
-
* —
|
|
93
|
-
*
|
|
104
|
+
* isn't clobbered. A policy with the same `name` is replaced IN PLACE when no
|
|
105
|
+
* `position` is given (order preserved); WITH a `position` it is moved there
|
|
106
|
+
* — so re-issuing a rule is also how you reposition it. New policies append
|
|
107
|
+
* by default; `'prepend'` puts one at the very front (ahead of EVERYTHING,
|
|
108
|
+
* including addressed-message wake rules — see the shadow lint), and the
|
|
109
|
+
* anchored forms `{before: name}` / `{after: name}` insert relative to an
|
|
110
|
+
* existing rule, which is almost always the safer way to order a sampler
|
|
111
|
+
* or debounce against a specific catch-all.
|
|
94
112
|
*/
|
|
95
113
|
addPolicy(rawPolicy: unknown, options?: {
|
|
96
|
-
position?: 'append' | 'prepend'
|
|
114
|
+
position?: 'append' | 'prepend' | {
|
|
115
|
+
before: string;
|
|
116
|
+
} | {
|
|
117
|
+
after: string;
|
|
118
|
+
};
|
|
97
119
|
}): GatePolicy;
|
|
98
120
|
/**
|
|
99
121
|
* Remove a policy by name from gate.json and clear its live state. Any pending
|
|
@@ -115,7 +137,6 @@ export declare class EventGate {
|
|
|
115
137
|
* if the write fails (disk/permission) — callers surface it as a tool error.
|
|
116
138
|
*/
|
|
117
139
|
private writeConfig;
|
|
118
|
-
private matchPolicy;
|
|
119
140
|
private compiledMatches;
|
|
120
141
|
evaluate(info: GateEventInfo): GateDecision;
|
|
121
142
|
private computeDecision;
|
|
@@ -137,6 +158,40 @@ export declare class EventGate {
|
|
|
137
158
|
* — those are historical and shouldn't reset on a config edit.
|
|
138
159
|
*/
|
|
139
160
|
private clearPolicyState;
|
|
161
|
+
/**
|
|
162
|
+
* Non-mutating twin of the behavior decision: would this policy trigger for
|
|
163
|
+
* this event, given CURRENT counter/bucket state? Reads state, never writes
|
|
164
|
+
* it — no stats, no traces, no counter increments, no debounce batching.
|
|
165
|
+
*/
|
|
166
|
+
private wouldTrigger;
|
|
167
|
+
/**
|
|
168
|
+
* Evaluate an event against the declarative policy table WITHOUT side
|
|
169
|
+
* effects: no counters advance, no debounce batches form, no stats or
|
|
170
|
+
* traces are recorded, and sleep suppression is ignored (a probe asks
|
|
171
|
+
* about the table, not the moment). gate.js is not consulted — a script
|
|
172
|
+
* may be stateful, and a probe must never perturb state.
|
|
173
|
+
*/
|
|
174
|
+
probe(info: GateEventInfo): GateDecision;
|
|
175
|
+
/**
|
|
176
|
+
* Probe a canonical set of synthetic chat/heartbeat events and report which
|
|
177
|
+
* rule wins each and whether it would wake. Backs the before/after table in
|
|
178
|
+
* the wake_add_rule result — the point where a rule change's real semantics
|
|
179
|
+
* must become legible (af#105; the Mythos stand-back-density incident would
|
|
180
|
+
* have been visible as `dm: discord-direct-address → stand-back-density`).
|
|
181
|
+
* Probes are representative shapes, not an exhaustive event space — rules
|
|
182
|
+
* keyed on specific channels/mounts may not match any probe.
|
|
183
|
+
*/
|
|
184
|
+
probeTable(): GateProbeRow[];
|
|
185
|
+
/** Current shadow-lint findings for the live policy order. Fresh compute —
|
|
186
|
+
* cheap (policy lists are tens of entries at most). */
|
|
187
|
+
lintShadows(): ShadowWarning[];
|
|
188
|
+
/**
|
|
189
|
+
* Recompute shadow warnings and, when the set changed and is non-empty,
|
|
190
|
+
* emit a `gate:shadow-warnings` trace so the hazard lands in ops
|
|
191
|
+
* observability at the moment the order changed — not at the next missed
|
|
192
|
+
* DM. Called from writeConfig and reloadIfChanged.
|
|
193
|
+
*/
|
|
194
|
+
private refreshShadowWarnings;
|
|
140
195
|
/**
|
|
141
196
|
* Returns a callback compatible with McplServerConfig.shouldTriggerInference.
|
|
142
197
|
* Used by the framework to wire the gate into PushHandler and ChannelRegistry.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-gate.d.ts","sourceRoot":"","sources":["../../../src/gate/event-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EAGV,YAAY,EACZ,aAAa,EAEb,UAAU,
|
|
1
|
+
{"version":3,"file":"event-gate.d.ts","sourceRoot":"","sources":["../../../src/gate/event-gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -238,12 +238,138 @@ function validatePolicy(raw) {
|
|
|
238
238
|
if (names.length > 0)
|
|
239
239
|
resets = names;
|
|
240
240
|
}
|
|
241
|
+
// Validate passthrough — only meaningful on the counting behaviors, where
|
|
242
|
+
// "matched but didn't fire" is a distinct outcome that can fall through.
|
|
243
|
+
// On always/defer/skip/debounce the flag would be a no-op at best and a
|
|
244
|
+
// misleading promise at worst, so reject it loudly instead of ignoring it.
|
|
245
|
+
let passthrough;
|
|
246
|
+
if (obj.passthrough !== undefined) {
|
|
247
|
+
if (typeof obj.passthrough !== 'boolean') {
|
|
248
|
+
throw new Error(`Policy "${obj.name}": passthrough must be a boolean`);
|
|
249
|
+
}
|
|
250
|
+
if (obj.passthrough) {
|
|
251
|
+
const counting = typeof behavior === 'object'
|
|
252
|
+
&& ('rate_limit' in behavior || 'passive_sample' in behavior);
|
|
253
|
+
if (!counting) {
|
|
254
|
+
throw new Error(`Policy "${obj.name}": passthrough is only valid with rate_limit or `
|
|
255
|
+
+ `passive_sample behaviors — for "${formatBehavior(behavior)}" there is `
|
|
256
|
+
+ 'no non-firing match to fall through on');
|
|
257
|
+
}
|
|
258
|
+
passthrough = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
241
261
|
const policy = { name: obj.name, match, behavior };
|
|
242
262
|
if (resets)
|
|
243
263
|
policy.resets = resets;
|
|
264
|
+
if (passthrough)
|
|
265
|
+
policy.passthrough = passthrough;
|
|
244
266
|
return policy;
|
|
245
267
|
}
|
|
246
268
|
// ---------------------------------------------------------------------------
|
|
269
|
+
// Shadow lint — prove (conservatively) when rule ordering makes a rule dead
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
/** Set-inclusion helper for exact-string lists. */
|
|
272
|
+
function subsetOf(a, b) {
|
|
273
|
+
return a.every((x) => b.includes(x));
|
|
274
|
+
}
|
|
275
|
+
/** A glob field on the EARLIER rule is at least as broad as the LATER rule's
|
|
276
|
+
* when it's absent, the universal "*", or literally identical. Anything
|
|
277
|
+
* cleverer (real glob subsumption) is out of conservative scope. */
|
|
278
|
+
function globAtLeastAsBroad(earlier, later) {
|
|
279
|
+
if (earlier === undefined || earlier === '*')
|
|
280
|
+
return true;
|
|
281
|
+
return earlier === later;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Event types for which `earlier` provably matches EVERY event that `later`
|
|
285
|
+
* would match — i.e. the types for which `later` is unreachable when
|
|
286
|
+
* `earlier` sits before it and consumes its matches. Returns null when
|
|
287
|
+
* subsumption can't be proven (which is not proof of safety — the lint is
|
|
288
|
+
* deliberately one-sided so every warning it emits is true).
|
|
289
|
+
*/
|
|
290
|
+
function subsumedEventTypes(earlier, later) {
|
|
291
|
+
const e = earlier.match;
|
|
292
|
+
const l = later.match;
|
|
293
|
+
// Scope: which of `later`'s event types does `earlier` cover?
|
|
294
|
+
let covered;
|
|
295
|
+
if (!e.scope || e.scope.length === 0) {
|
|
296
|
+
covered = l.scope && l.scope.length > 0 ? l.scope : 'all';
|
|
297
|
+
}
|
|
298
|
+
else if (!l.scope || l.scope.length === 0) {
|
|
299
|
+
// `later` matches all types; `earlier` only consumes its own scope, so
|
|
300
|
+
// the shadow is partial — exactly those types.
|
|
301
|
+
covered = e.scope;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const overlap = l.scope.filter((t) => e.scope.includes(t));
|
|
305
|
+
if (overlap.length === 0)
|
|
306
|
+
return null;
|
|
307
|
+
covered = overlap;
|
|
308
|
+
}
|
|
309
|
+
// Every remaining discriminator on `earlier` must be at least as broad as
|
|
310
|
+
// `later`'s, otherwise some `later` events slip past `earlier`.
|
|
311
|
+
if (!globAtLeastAsBroad(e.source, l.source))
|
|
312
|
+
return null;
|
|
313
|
+
if (!globAtLeastAsBroad(e.channel, l.channel))
|
|
314
|
+
return null;
|
|
315
|
+
if (!globAtLeastAsBroad(e.mount, l.mount))
|
|
316
|
+
return null;
|
|
317
|
+
if (!globAtLeastAsBroad(e.pathGlob, l.pathGlob))
|
|
318
|
+
return null;
|
|
319
|
+
if (e.filter) {
|
|
320
|
+
if (!l.filter || e.filter.type !== l.filter.type || e.filter.pattern !== l.filter.pattern) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// metadataTrue / tagsAny are OR-lists: earlier subsumes when later's list
|
|
325
|
+
// is a subset of earlier's (any event satisfying later then satisfies
|
|
326
|
+
// earlier too). tagsAll / tagsNone are AND/NOT constraints: earlier
|
|
327
|
+
// subsumes when its constraints are a subset of later's.
|
|
328
|
+
if (e.metadataTrue && e.metadataTrue.length > 0) {
|
|
329
|
+
if (!l.metadataTrue || !subsetOf(l.metadataTrue, e.metadataTrue))
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
if (e.tagsAny && e.tagsAny.length > 0) {
|
|
333
|
+
if (!l.tagsAny || !subsetOf(l.tagsAny, e.tagsAny))
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
if (e.tagsAll && !subsetOf(e.tagsAll, l.tagsAll ?? []))
|
|
337
|
+
return null;
|
|
338
|
+
if (e.tagsNone && !subsetOf(e.tagsNone, l.tagsNone ?? []))
|
|
339
|
+
return null;
|
|
340
|
+
return covered;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Find rules made unreachable by earlier, broader rules (first match wins).
|
|
344
|
+
* The 2026-08-08 Mythos incident is the canonical catch: a prepended
|
|
345
|
+
* `{scope:[mcpl:channel-incoming]}` sampler silently made the
|
|
346
|
+
* direct-address and mention rules dead for the whole incoming lane.
|
|
347
|
+
* `passthrough` observers shadow nothing — non-firing matches fall through.
|
|
348
|
+
*/
|
|
349
|
+
export function findShadowedPolicies(policies) {
|
|
350
|
+
const out = [];
|
|
351
|
+
for (let i = 0; i < policies.length; i++) {
|
|
352
|
+
const earlier = policies[i];
|
|
353
|
+
if (earlier.passthrough)
|
|
354
|
+
continue;
|
|
355
|
+
for (let j = i + 1; j < policies.length; j++) {
|
|
356
|
+
const covered = subsumedEventTypes(earlier, policies[j]);
|
|
357
|
+
if (covered) {
|
|
358
|
+
out.push({ earlier: earlier.name, later: policies[j].name, eventTypes: covered });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
/** Human-readable one-liner for a ShadowWarning, used in tool results,
|
|
365
|
+
* traces, and gate_status alike so the same hazard reads the same way
|
|
366
|
+
* everywhere. */
|
|
367
|
+
export function formatShadowWarning(w) {
|
|
368
|
+
const types = w.eventTypes === 'all' ? 'ALL event types' : w.eventTypes.join(', ');
|
|
369
|
+
return `rule "${w.earlier}" makes "${w.later}" unreachable for ${types} `
|
|
370
|
+
+ `(first match wins — every event "${w.later}" would match is consumed by "${w.earlier}" first)`;
|
|
371
|
+
}
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
247
373
|
// EventGate
|
|
248
374
|
// ---------------------------------------------------------------------------
|
|
249
375
|
export class EventGate {
|
|
@@ -271,6 +397,10 @@ export class EventGate {
|
|
|
271
397
|
inferenceBuffer = [];
|
|
272
398
|
// Per-policy stats
|
|
273
399
|
stats = new Map();
|
|
400
|
+
// Shadow-lint cache (see refreshShadowWarnings) — recomputed on every
|
|
401
|
+
// config change, fingerprinted so an unchanged hazard set doesn't re-trace.
|
|
402
|
+
shadowWarningsCache = [];
|
|
403
|
+
shadowWarningsFingerprint = '[]';
|
|
274
404
|
// Observability counters for events that didn't match any policy
|
|
275
405
|
totalEvaluations = 0;
|
|
276
406
|
defaultTriggered = 0;
|
|
@@ -360,6 +490,7 @@ export class EventGate {
|
|
|
360
490
|
// Load config
|
|
361
491
|
this.config = this.loadConfig() ?? opts.initialConfig ?? { ...DEFAULT_CONFIG };
|
|
362
492
|
this.compiledPolicies = this.compilePolicies(this.config);
|
|
493
|
+
this.refreshShadowWarnings('startup');
|
|
363
494
|
// Programmable gate: an optional agent-authored `gate.js` next to gate.json.
|
|
364
495
|
// Absent ⇒ inactive (zero overhead beyond a throttled stat check).
|
|
365
496
|
this.gateScript = new GateScript(join(dirname(this.configPath), 'gate.js'), opts.scriptTimeoutMs ?? 50, this.now);
|
|
@@ -470,6 +601,7 @@ export class EventGate {
|
|
|
470
601
|
this.config = newConfig;
|
|
471
602
|
this.compiledPolicies = this.compilePolicies(newConfig);
|
|
472
603
|
this.lastReloadTimestamp = now;
|
|
604
|
+
this.refreshShadowWarnings('reload');
|
|
473
605
|
this.emitTrace({
|
|
474
606
|
type: 'gate:config-reloaded',
|
|
475
607
|
configPath: this.configPath,
|
|
@@ -497,24 +629,42 @@ export class EventGate {
|
|
|
497
629
|
* without waiting for the throttled mtime reload.
|
|
498
630
|
*
|
|
499
631
|
* The freshest on-disk config is re-read first, so a concurrent operator edit
|
|
500
|
-
* isn't clobbered. A policy with the same `name` is replaced IN PLACE
|
|
501
|
-
* preserved);
|
|
502
|
-
* —
|
|
503
|
-
*
|
|
632
|
+
* isn't clobbered. A policy with the same `name` is replaced IN PLACE when no
|
|
633
|
+
* `position` is given (order preserved); WITH a `position` it is moved there
|
|
634
|
+
* — so re-issuing a rule is also how you reposition it. New policies append
|
|
635
|
+
* by default; `'prepend'` puts one at the very front (ahead of EVERYTHING,
|
|
636
|
+
* including addressed-message wake rules — see the shadow lint), and the
|
|
637
|
+
* anchored forms `{before: name}` / `{after: name}` insert relative to an
|
|
638
|
+
* existing rule, which is almost always the safer way to order a sampler
|
|
639
|
+
* or debounce against a specific catch-all.
|
|
504
640
|
*/
|
|
505
641
|
addPolicy(rawPolicy, options) {
|
|
506
642
|
const policy = validatePolicy(rawPolicy);
|
|
507
643
|
const current = this.loadConfig() ?? this.config;
|
|
508
644
|
const policies = [...current.policies];
|
|
509
645
|
const idx = policies.findIndex((p) => p.name === policy.name);
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
else if (options?.position === 'prepend') {
|
|
514
|
-
policies.unshift(policy);
|
|
646
|
+
const position = options?.position;
|
|
647
|
+
if (idx >= 0 && position === undefined) {
|
|
648
|
+
policies[idx] = policy; // replace in place, order preserved
|
|
515
649
|
}
|
|
516
650
|
else {
|
|
517
|
-
|
|
651
|
+
if (idx >= 0)
|
|
652
|
+
policies.splice(idx, 1); // repositioning a known rule
|
|
653
|
+
if (position === 'prepend') {
|
|
654
|
+
policies.unshift(policy);
|
|
655
|
+
}
|
|
656
|
+
else if (position !== undefined && typeof position === 'object') {
|
|
657
|
+
const anchorName = 'before' in position ? position.before : position.after;
|
|
658
|
+
const anchorIdx = policies.findIndex((p) => p.name === anchorName);
|
|
659
|
+
if (anchorIdx < 0) {
|
|
660
|
+
throw new Error(`addPolicy: no policy named "${anchorName}" to anchor on — `
|
|
661
|
+
+ `have: [${policies.map((p) => p.name).join(', ')}]`);
|
|
662
|
+
}
|
|
663
|
+
policies.splice('before' in position ? anchorIdx : anchorIdx + 1, 0, policy);
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
policies.push(policy);
|
|
667
|
+
}
|
|
518
668
|
}
|
|
519
669
|
this.writeConfig({ ...current, policies });
|
|
520
670
|
this.emitTrace({
|
|
@@ -570,6 +720,7 @@ export class EventGate {
|
|
|
570
720
|
writeFileSync(this.configPath, JSON.stringify(config, null, 2) + '\n');
|
|
571
721
|
this.config = config;
|
|
572
722
|
this.compiledPolicies = this.compilePolicies(config);
|
|
723
|
+
this.refreshShadowWarnings('policy-mutation');
|
|
573
724
|
this.configSource = 'file';
|
|
574
725
|
this.configErrors = [];
|
|
575
726
|
this.lastReloadTimestamp = Date.now();
|
|
@@ -581,16 +732,9 @@ export class EventGate {
|
|
|
581
732
|
}
|
|
582
733
|
}
|
|
583
734
|
// =========================================================================
|
|
584
|
-
// Policy matching — first match wins
|
|
735
|
+
// Policy matching — first match wins (passthrough observers excepted; the
|
|
736
|
+
// match/decide loop lives inline in evaluate() and probe())
|
|
585
737
|
// =========================================================================
|
|
586
|
-
matchPolicy(info) {
|
|
587
|
-
for (const compiled of this.compiledPolicies) {
|
|
588
|
-
if (this.compiledMatches(compiled, info)) {
|
|
589
|
-
return compiled.policy;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
738
|
compiledMatches(compiled, info) {
|
|
595
739
|
const match = compiled.policy.match;
|
|
596
740
|
// Scope check
|
|
@@ -694,11 +838,33 @@ export class EventGate {
|
|
|
694
838
|
// Programmable gate (gate.js) takes precedence when present and returns a
|
|
695
839
|
// behavior; null/undefined (inactive, error, or a deliberate pass) falls
|
|
696
840
|
// through to the declarative gate.json policies below.
|
|
841
|
+
//
|
|
842
|
+
// Declarative policies: first match wins — EXCEPT `passthrough` observers,
|
|
843
|
+
// which consume the event only when their counting behavior actually
|
|
844
|
+
// fires. A passthrough policy that matched-but-didn't-fire records its
|
|
845
|
+
// count and evaluation continues, so a density sampler placed early can
|
|
846
|
+
// no longer swallow the addressed-message rules behind it.
|
|
697
847
|
const scripted = this.gateScript.evaluate(info);
|
|
698
|
-
|
|
699
|
-
const
|
|
848
|
+
let decision = null;
|
|
849
|
+
const observed = [];
|
|
850
|
+
if (scripted != null) {
|
|
851
|
+
decision = this.computeDecision({ name: 'gate.js', match: {}, behavior: scripted }, info);
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
for (const compiled of this.compiledPolicies) {
|
|
855
|
+
if (!this.compiledMatches(compiled, info))
|
|
856
|
+
continue;
|
|
857
|
+
const candidate = this.computeDecision(compiled.policy, info);
|
|
858
|
+
if (candidate.trigger || !compiled.policy.passthrough) {
|
|
859
|
+
decision = candidate;
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
observed.push(compiled.policy.name); // counted, didn't fire, fell through
|
|
863
|
+
}
|
|
864
|
+
}
|
|
700
865
|
this.totalEvaluations++;
|
|
701
|
-
if (!
|
|
866
|
+
if (!decision) {
|
|
867
|
+
decision = this.computeDecision(null, info);
|
|
702
868
|
const bucket = this.defaultByEventType.get(info.eventType)
|
|
703
869
|
?? { triggered: 0, skipped: 0 };
|
|
704
870
|
if (decision.trigger) {
|
|
@@ -711,6 +877,8 @@ export class EventGate {
|
|
|
711
877
|
}
|
|
712
878
|
this.defaultByEventType.set(info.eventType, bucket);
|
|
713
879
|
}
|
|
880
|
+
if (observed.length > 0)
|
|
881
|
+
decision.observed = observed;
|
|
714
882
|
this.emitTrace({
|
|
715
883
|
type: 'gate:decision',
|
|
716
884
|
eventType: info.eventType,
|
|
@@ -719,6 +887,7 @@ export class EventGate {
|
|
|
719
887
|
matchedPolicy: decision.policyName,
|
|
720
888
|
trigger: decision.trigger,
|
|
721
889
|
behavior: formatBehavior(decision.behavior),
|
|
890
|
+
...(observed.length > 0 ? { observed } : {}),
|
|
722
891
|
timestamp: this.now(),
|
|
723
892
|
});
|
|
724
893
|
return decision;
|
|
@@ -866,6 +1035,147 @@ export class EventGate {
|
|
|
866
1035
|
this.passiveSampleFires.delete(name);
|
|
867
1036
|
}
|
|
868
1037
|
// =========================================================================
|
|
1038
|
+
// Dry-run probes — side-effect-free "what would this event do right now"
|
|
1039
|
+
// =========================================================================
|
|
1040
|
+
/**
|
|
1041
|
+
* Non-mutating twin of the behavior decision: would this policy trigger for
|
|
1042
|
+
* this event, given CURRENT counter/bucket state? Reads state, never writes
|
|
1043
|
+
* it — no stats, no traces, no counter increments, no debounce batching.
|
|
1044
|
+
*/
|
|
1045
|
+
wouldTrigger(policy, info) {
|
|
1046
|
+
const b = policy.behavior;
|
|
1047
|
+
if (b === 'always')
|
|
1048
|
+
return true;
|
|
1049
|
+
if (b === 'defer' || b === 'skip')
|
|
1050
|
+
return false;
|
|
1051
|
+
if (typeof b === 'object') {
|
|
1052
|
+
if ('debounce' in b)
|
|
1053
|
+
return false; // batches; never triggers inline
|
|
1054
|
+
if ('rate_limit' in b) {
|
|
1055
|
+
const cfg = b.rate_limit;
|
|
1056
|
+
const key = this.resolveKey(info.metadata, cfg.keyBy);
|
|
1057
|
+
const bucket = this.rateLimitBuckets.get(policy.name)?.get(key);
|
|
1058
|
+
if (!bucket)
|
|
1059
|
+
return true; // fresh bucket starts full
|
|
1060
|
+
let tokens = bucket.tokens;
|
|
1061
|
+
if (tokens < cfg.tokens) {
|
|
1062
|
+
const elapsed = Math.max(0, this.now() - bucket.lastRefill);
|
|
1063
|
+
tokens = Math.min(cfg.tokens, tokens + Math.floor(elapsed / cfg.refillIntervalMs));
|
|
1064
|
+
}
|
|
1065
|
+
return tokens > 0;
|
|
1066
|
+
}
|
|
1067
|
+
if ('passive_sample' in b) {
|
|
1068
|
+
const cfg = b.passive_sample;
|
|
1069
|
+
const key = this.resolveKey(info.metadata, cfg.keyBy);
|
|
1070
|
+
const count = this.passiveSampleCounters.get(policy.name)?.get(key) ?? 0;
|
|
1071
|
+
return count + 1 >= cfg.every;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Evaluate an event against the declarative policy table WITHOUT side
|
|
1078
|
+
* effects: no counters advance, no debounce batches form, no stats or
|
|
1079
|
+
* traces are recorded, and sleep suppression is ignored (a probe asks
|
|
1080
|
+
* about the table, not the moment). gate.js is not consulted — a script
|
|
1081
|
+
* may be stateful, and a probe must never perturb state.
|
|
1082
|
+
*/
|
|
1083
|
+
probe(info) {
|
|
1084
|
+
for (const compiled of this.compiledPolicies) {
|
|
1085
|
+
if (!this.compiledMatches(compiled, info))
|
|
1086
|
+
continue;
|
|
1087
|
+
const trigger = this.wouldTrigger(compiled.policy, info);
|
|
1088
|
+
if (trigger || !compiled.policy.passthrough) {
|
|
1089
|
+
return { trigger, policyName: compiled.policy.name, behavior: compiled.policy.behavior };
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const trigger = (this.config.default ?? 'always') === 'always';
|
|
1093
|
+
return { trigger, policyName: null, behavior: this.config.default ?? 'always' };
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Probe a canonical set of synthetic chat/heartbeat events and report which
|
|
1097
|
+
* rule wins each and whether it would wake. Backs the before/after table in
|
|
1098
|
+
* the wake_add_rule result — the point where a rule change's real semantics
|
|
1099
|
+
* must become legible (af#105; the Mythos stand-back-density incident would
|
|
1100
|
+
* have been visible as `dm: discord-direct-address → stand-back-density`).
|
|
1101
|
+
* Probes are representative shapes, not an exhaustive event space — rules
|
|
1102
|
+
* keyed on specific channels/mounts may not match any probe.
|
|
1103
|
+
*/
|
|
1104
|
+
probeTable() {
|
|
1105
|
+
const chat = (label, eventType, metadata, tags) => ({
|
|
1106
|
+
label,
|
|
1107
|
+
info: {
|
|
1108
|
+
content: `[probe] ${label}`,
|
|
1109
|
+
eventType,
|
|
1110
|
+
serverId: 'discord',
|
|
1111
|
+
channelId: '',
|
|
1112
|
+
metadata: { ...metadata, eventType },
|
|
1113
|
+
tags,
|
|
1114
|
+
},
|
|
1115
|
+
});
|
|
1116
|
+
const probes = [
|
|
1117
|
+
chat('dm (open channel)', 'mcpl:channel-incoming', { isDM: true, isMention: true }, ['chat:dm', 'chat:private', 'chat:addressed', 'chat:from-human']),
|
|
1118
|
+
chat('dm (closed channel)', 'mcpl:push-event', { isDM: true, isMention: true }, ['chat:dm', 'chat:private', 'chat:addressed', 'chat:from-human']),
|
|
1119
|
+
chat('explicit mention (open channel)', 'mcpl:channel-incoming', { isMention: true, isExplicitMention: true }, ['chat:mention', 'chat:addressed', 'chat:from-human']),
|
|
1120
|
+
chat('explicit mention (closed channel)', 'mcpl:push-event', { isMention: true, isExplicitMention: true }, ['chat:mention', 'chat:addressed', 'chat:from-human']),
|
|
1121
|
+
chat('reply to you', 'mcpl:channel-incoming', { isMention: true, isReplyToBot: true }, ['chat:reply', 'chat:addressed', 'chat:from-human']),
|
|
1122
|
+
chat('ambient (human)', 'mcpl:channel-incoming', {}, ['chat:ambient', 'chat:from-human']),
|
|
1123
|
+
chat('ambient (bot)', 'mcpl:channel-incoming', { isBot: true }, ['chat:ambient', 'chat:from-bot']),
|
|
1124
|
+
{
|
|
1125
|
+
label: 'heartbeat',
|
|
1126
|
+
info: {
|
|
1127
|
+
content: '[probe] heartbeat',
|
|
1128
|
+
eventType: 'mcpl:push-event',
|
|
1129
|
+
serverId: 'heartbeat',
|
|
1130
|
+
channelId: '',
|
|
1131
|
+
metadata: { source: 'heartbeat', eventType: 'mcpl:push-event' },
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
];
|
|
1135
|
+
return probes.map(({ label, info }) => {
|
|
1136
|
+
const d = this.probe(info);
|
|
1137
|
+
return {
|
|
1138
|
+
probe: label,
|
|
1139
|
+
eventType: info.eventType,
|
|
1140
|
+
policy: d.policyName,
|
|
1141
|
+
behavior: d.policyName === null
|
|
1142
|
+
? `default:${formatBehavior(d.behavior)}`
|
|
1143
|
+
: formatBehavior(d.behavior),
|
|
1144
|
+
wouldWake: d.trigger,
|
|
1145
|
+
};
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
// =========================================================================
|
|
1149
|
+
// Shadow lint surface
|
|
1150
|
+
// =========================================================================
|
|
1151
|
+
/** Current shadow-lint findings for the live policy order. Fresh compute —
|
|
1152
|
+
* cheap (policy lists are tens of entries at most). */
|
|
1153
|
+
lintShadows() {
|
|
1154
|
+
return findShadowedPolicies(this.config.policies);
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Recompute shadow warnings and, when the set changed and is non-empty,
|
|
1158
|
+
* emit a `gate:shadow-warnings` trace so the hazard lands in ops
|
|
1159
|
+
* observability at the moment the order changed — not at the next missed
|
|
1160
|
+
* DM. Called from writeConfig and reloadIfChanged.
|
|
1161
|
+
*/
|
|
1162
|
+
refreshShadowWarnings(reason) {
|
|
1163
|
+
const warnings = this.lintShadows();
|
|
1164
|
+
const fingerprint = JSON.stringify(warnings);
|
|
1165
|
+
if (fingerprint === this.shadowWarningsFingerprint)
|
|
1166
|
+
return;
|
|
1167
|
+
this.shadowWarningsFingerprint = fingerprint;
|
|
1168
|
+
this.shadowWarningsCache = warnings;
|
|
1169
|
+
if (warnings.length > 0) {
|
|
1170
|
+
this.emitTrace({
|
|
1171
|
+
type: 'gate:shadow-warnings',
|
|
1172
|
+
reason,
|
|
1173
|
+
warnings: warnings.map(formatShadowWarning),
|
|
1174
|
+
timestamp: this.now(),
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
// =========================================================================
|
|
869
1179
|
// shouldTriggerInference callback adapter
|
|
870
1180
|
// =========================================================================
|
|
871
1181
|
/**
|
|
@@ -1269,6 +1579,7 @@ export class EventGate {
|
|
|
1269
1579
|
script: this.gateScript.status(),
|
|
1270
1580
|
policies,
|
|
1271
1581
|
errors: [...this.configErrors],
|
|
1582
|
+
shadowWarnings: this.shadowWarningsCache.map(formatShadowWarning),
|
|
1272
1583
|
totalEvaluations: this.totalEvaluations,
|
|
1273
1584
|
defaultDecisions: {
|
|
1274
1585
|
triggered: this.defaultTriggered,
|