@animalabs/agent-framework 0.5.7 → 0.6.2
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 +4 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +15 -0
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +243 -0
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1378 -114
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +64 -2
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +212 -12
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/gate-script-worker.d.ts +2 -0
- package/dist/src/gate/gate-script-worker.d.ts.map +1 -0
- package/dist/src/gate/gate-script-worker.js +69 -0
- package/dist/src/gate/gate-script-worker.js.map +1 -0
- package/dist/src/gate/gate-script.d.ts +44 -0
- package/dist/src/gate/gate-script.d.ts.map +1 -0
- package/dist/src/gate/gate-script.js +164 -0
- package/dist/src/gate/gate-script.js.map +1 -0
- package/dist/src/gate/types.d.ts +18 -3
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +60 -2
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +114 -9
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +7 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +15 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +5 -0
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +64 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +247 -210
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +98 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -0
- package/dist/src/mcpl/transport.js +281 -0
- package/dist/src/mcpl/transport.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +59 -3
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +17 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +101 -6
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/prose-segments.d.ts +18 -0
- package/dist/src/prose-segments.d.ts.map +1 -0
- package/dist/src/prose-segments.js +39 -0
- package/dist/src/prose-segments.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts +2 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.js +57 -0
- package/dist/src/runtime/liveness-watchdog-worker.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog.d.ts +36 -0
- package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog.js +106 -0
- package/dist/src/runtime/liveness-watchdog.js.map +1 -0
- package/dist/src/types/agent.d.ts +47 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +14 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +58 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +42 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/agent-cache-ttl.test.d.ts +2 -0
- package/dist/test/agent-cache-ttl.test.d.ts.map +1 -0
- package/dist/test/agent-cache-ttl.test.js +25 -0
- package/dist/test/agent-cache-ttl.test.js.map +1 -0
- package/dist/test/channel-registry-routing.test.js +108 -1
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/checkpoint-attribution.test.d.ts +2 -0
- package/dist/test/checkpoint-attribution.test.d.ts.map +1 -0
- package/dist/test/checkpoint-attribution.test.js +40 -0
- package/dist/test/checkpoint-attribution.test.js.map +1 -0
- package/dist/test/conversation-fork-injection-scoping.test.d.ts +2 -0
- package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +1 -0
- package/dist/test/conversation-fork-injection-scoping.test.js +184 -0
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts +2 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.js +137 -0
- package/dist/test/event-gate-policy-mutation.test.js.map +1 -0
- package/dist/test/event-gate-script.test.d.ts +2 -0
- package/dist/test/event-gate-script.test.d.ts.map +1 -0
- package/dist/test/event-gate-script.test.js +95 -0
- package/dist/test/event-gate-script.test.js.map +1 -0
- package/dist/test/event-gate-tags.test.d.ts +2 -0
- package/dist/test/event-gate-tags.test.d.ts.map +1 -0
- package/dist/test/event-gate-tags.test.js +76 -0
- package/dist/test/event-gate-tags.test.js.map +1 -0
- package/dist/test/gather-context-timeout.test.d.ts +2 -0
- package/dist/test/gather-context-timeout.test.d.ts.map +1 -0
- package/dist/test/gather-context-timeout.test.js +120 -0
- package/dist/test/gather-context-timeout.test.js.map +1 -0
- package/dist/test/inference-exhaustion-rewind.test.d.ts +2 -0
- package/dist/test/inference-exhaustion-rewind.test.d.ts.map +1 -0
- package/dist/test/inference-exhaustion-rewind.test.js +181 -0
- package/dist/test/inference-exhaustion-rewind.test.js.map +1 -0
- package/dist/test/inference-failure-observability.test.js +4 -0
- package/dist/test/inference-failure-observability.test.js.map +1 -1
- package/dist/test/liveness-watchdog.test.d.ts +2 -0
- package/dist/test/liveness-watchdog.test.d.ts.map +1 -0
- package/dist/test/liveness-watchdog.test.js +78 -0
- package/dist/test/liveness-watchdog.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.d.ts +2 -0
- package/dist/test/mcpl-reconnect-events.test.d.ts.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.js +119 -0
- package/dist/test/mcpl-reconnect-events.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts +2 -0
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js +181 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -0
- package/dist/test/mcpl-request-timeout.test.d.ts +2 -0
- package/dist/test/mcpl-request-timeout.test.d.ts.map +1 -0
- package/dist/test/mcpl-request-timeout.test.js +123 -0
- package/dist/test/mcpl-request-timeout.test.js.map +1 -0
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts +7 -0
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +1 -0
- package/dist/test/mcpl-runtime-lifecycle.test.js +142 -0
- package/dist/test/mcpl-runtime-lifecycle.test.js.map +1 -0
- package/dist/test/mcpl-ws-transport.test.d.ts +2 -0
- package/dist/test/mcpl-ws-transport.test.d.ts.map +1 -0
- package/dist/test/mcpl-ws-transport.test.js +155 -0
- package/dist/test/mcpl-ws-transport.test.js.map +1 -0
- package/dist/test/prose-segments.test.d.ts +2 -0
- package/dist/test/prose-segments.test.d.ts.map +1 -0
- package/dist/test/prose-segments.test.js +52 -0
- package/dist/test/prose-segments.test.js.map +1 -0
- package/dist/test/refusal-rewind.test.d.ts +2 -0
- package/dist/test/refusal-rewind.test.d.ts.map +1 -0
- package/dist/test/refusal-rewind.test.js +108 -0
- package/dist/test/refusal-rewind.test.js.map +1 -0
- package/dist/test/state-scaling-e2e.test.d.ts +22 -0
- package/dist/test/state-scaling-e2e.test.d.ts.map +1 -0
- package/dist/test/state-scaling-e2e.test.js +200 -0
- package/dist/test/state-scaling-e2e.test.js.map +1 -0
- package/dist/test/state-scaling.test.d.ts +12 -0
- package/dist/test/state-scaling.test.d.ts.map +1 -0
- package/dist/test/state-scaling.test.js +243 -0
- package/dist/test/state-scaling.test.js.map +1 -0
- package/dist/test/tool-result-content.test.d.ts +2 -0
- package/dist/test/tool-result-content.test.d.ts.map +1 -0
- package/dist/test/tool-result-content.test.js +37 -0
- package/dist/test/tool-result-content.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.d.ts +19 -0
- package/dist/test/trunk-channel-routing.test.d.ts.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +175 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/agent.ts +16 -0
- package/src/framework.ts +1576 -121
- package/src/gate/event-gate.ts +209 -12
- package/src/gate/gate-script-worker.ts +73 -0
- package/src/gate/gate-script.ts +180 -0
- package/src/gate/types.ts +19 -3
- package/src/index.ts +10 -0
- package/src/mcpl/channel-registry.ts +143 -8
- package/src/mcpl/checkpoint-manager.ts +16 -0
- package/src/mcpl/push-handler.ts +5 -0
- package/src/mcpl/server-connection.ts +262 -227
- package/src/mcpl/transport.ts +307 -0
- package/src/mcpl/types.ts +68 -3
- package/src/module-registry.ts +126 -6
- package/src/prose-segments.ts +42 -0
- package/src/runtime/liveness-watchdog-worker.ts +66 -0
- package/src/runtime/liveness-watchdog.ts +122 -0
- package/src/types/agent.ts +48 -0
- package/src/types/framework.ts +15 -0
- package/src/types/module.ts +65 -0
- package/src/types/trace.ts +51 -0
package/src/gate/event-gate.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
15
|
-
import { dirname } from 'node:path';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { GateScript } from './gate-script.js';
|
|
16
17
|
|
|
17
18
|
import type { ToolDefinition } from '../types/events.js';
|
|
18
19
|
import type {
|
|
@@ -89,6 +90,9 @@ interface CompiledPolicy {
|
|
|
89
90
|
channelRegex?: RegExp;
|
|
90
91
|
mountRegex?: RegExp;
|
|
91
92
|
pathRegex?: RegExp;
|
|
93
|
+
tagsAnyRe?: RegExp[];
|
|
94
|
+
tagsAllRe?: RegExp[];
|
|
95
|
+
tagsNoneRe?: RegExp[];
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
interface PolicyStats {
|
|
@@ -170,8 +174,8 @@ function validateConfig(raw: unknown): GateConfig {
|
|
|
170
174
|
const obj = raw as Record<string, unknown>;
|
|
171
175
|
|
|
172
176
|
const defaultBehavior = obj.default ?? 'always';
|
|
173
|
-
if (defaultBehavior !== 'always' && defaultBehavior !== 'skip') {
|
|
174
|
-
throw new Error(`gate.json "default" must be "always" or "skip", got: ${defaultBehavior}`);
|
|
177
|
+
if (defaultBehavior !== 'always' && defaultBehavior !== 'defer' && defaultBehavior !== 'skip') {
|
|
178
|
+
throw new Error(`gate.json "default" must be "always", "defer", or "skip", got: ${defaultBehavior}`);
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
const policies: GatePolicy[] = [];
|
|
@@ -197,7 +201,7 @@ function validatePolicy(raw: unknown): GatePolicy {
|
|
|
197
201
|
|
|
198
202
|
// Validate behavior
|
|
199
203
|
let behavior: GateBehavior;
|
|
200
|
-
if (obj.behavior === 'always' || obj.behavior === 'skip') {
|
|
204
|
+
if (obj.behavior === 'always' || obj.behavior === 'defer' || obj.behavior === 'skip') {
|
|
201
205
|
behavior = obj.behavior;
|
|
202
206
|
} else if (obj.behavior && typeof obj.behavior === 'object') {
|
|
203
207
|
const b = obj.behavior as Record<string, unknown>;
|
|
@@ -247,7 +251,7 @@ function validatePolicy(raw: unknown): GatePolicy {
|
|
|
247
251
|
}
|
|
248
252
|
behavior = { passive_sample: out };
|
|
249
253
|
} else {
|
|
250
|
-
throw new Error(`Policy "${obj.name}": unknown behavior — expected always | skip | { debounce } | { rate_limit } | { passive_sample }`);
|
|
254
|
+
throw new Error(`Policy "${obj.name}": unknown behavior — expected always | defer | skip | { debounce } | { rate_limit } | { passive_sample }`);
|
|
251
255
|
}
|
|
252
256
|
} else {
|
|
253
257
|
behavior = 'always';
|
|
@@ -266,6 +270,14 @@ function validatePolicy(raw: unknown): GatePolicy {
|
|
|
266
270
|
const fields = m.metadataTrue.filter((s): s is string => typeof s === 'string' && s.length > 0);
|
|
267
271
|
if (fields.length > 0) match.metadataTrue = fields;
|
|
268
272
|
}
|
|
273
|
+
const tagList = (v: unknown): string[] | undefined => {
|
|
274
|
+
if (!Array.isArray(v)) return undefined;
|
|
275
|
+
const out = v.filter((s): s is string => typeof s === 'string' && s.length > 0);
|
|
276
|
+
return out.length > 0 ? out : undefined;
|
|
277
|
+
};
|
|
278
|
+
const tagsAny = tagList(m.tagsAny); if (tagsAny) match.tagsAny = tagsAny;
|
|
279
|
+
const tagsAll = tagList(m.tagsAll); if (tagsAll) match.tagsAll = tagsAll;
|
|
280
|
+
const tagsNone = tagList(m.tagsNone); if (tagsNone) match.tagsNone = tagsNone;
|
|
269
281
|
if (m.filter && typeof m.filter === 'object') {
|
|
270
282
|
const f = m.filter as Record<string, unknown>;
|
|
271
283
|
if ((f.type === 'text' || f.type === 'regex') && typeof f.pattern === 'string') {
|
|
@@ -299,6 +311,8 @@ function validatePolicy(raw: unknown): GatePolicy {
|
|
|
299
311
|
export class EventGate {
|
|
300
312
|
private configPath: string;
|
|
301
313
|
private config: GateConfig;
|
|
314
|
+
/** Optional agent-authored programmable gate (gate.js). */
|
|
315
|
+
private gateScript: GateScript;
|
|
302
316
|
private compiledPolicies: CompiledPolicy[] = [];
|
|
303
317
|
private configMtime: number = 0;
|
|
304
318
|
private lastReloadCheck: number = 0;
|
|
@@ -330,12 +344,16 @@ export class EventGate {
|
|
|
330
344
|
private defaultSkipped = 0;
|
|
331
345
|
private defaultByEventType = new Map<string, { triggered: number; skipped: number }>();
|
|
332
346
|
|
|
333
|
-
// Sleep state (set via the `sleep` tool).
|
|
334
|
-
//
|
|
335
|
-
//
|
|
347
|
+
// Sleep state (set via the `sleep` tool). evaluate() suppresses non-privileged
|
|
348
|
+
// wakes while now() < sleepUntil. A self-wake timer (sleepTimer) fires an
|
|
349
|
+
// inference when the window expires, so `sleep(N)` means "wake me in N
|
|
350
|
+
// seconds" rather than merely lifting suppression and waiting for an external
|
|
351
|
+
// event.
|
|
336
352
|
private sleepUntil = 0;
|
|
337
353
|
private sleepNote: string | undefined;
|
|
338
354
|
private sleepSuppressed = 0;
|
|
355
|
+
private sleepTimer: ReturnType<typeof setTimeout> | null = null;
|
|
356
|
+
private sleepAgent: string | undefined;
|
|
339
357
|
|
|
340
358
|
// Privileged users who may wake the agent through sleep. Hot-reloaded from
|
|
341
359
|
// privilegedUsersPath on change.
|
|
@@ -363,6 +381,8 @@ export class EventGate {
|
|
|
363
381
|
getAgentNames: () => string[];
|
|
364
382
|
/** Optional clock — defaults to Date.now. Tests inject for deterministic time. */
|
|
365
383
|
now?: () => number;
|
|
384
|
+
/** Per-event timeout (ms) for the optional gate.js script. Default 50. */
|
|
385
|
+
scriptTimeoutMs?: number;
|
|
366
386
|
}) {
|
|
367
387
|
this.configPath = opts.configPath;
|
|
368
388
|
this.privilegedUsersPath = opts.privilegedUsersPath;
|
|
@@ -417,6 +437,14 @@ export class EventGate {
|
|
|
417
437
|
// Load config
|
|
418
438
|
this.config = this.loadConfig() ?? opts.initialConfig ?? { ...DEFAULT_CONFIG };
|
|
419
439
|
this.compiledPolicies = this.compilePolicies(this.config);
|
|
440
|
+
|
|
441
|
+
// Programmable gate: an optional agent-authored `gate.js` next to gate.json.
|
|
442
|
+
// Absent ⇒ inactive (zero overhead beyond a throttled stat check).
|
|
443
|
+
this.gateScript = new GateScript(
|
|
444
|
+
join(dirname(this.configPath), 'gate.js'),
|
|
445
|
+
opts.scriptTimeoutMs ?? 50,
|
|
446
|
+
this.now,
|
|
447
|
+
);
|
|
420
448
|
}
|
|
421
449
|
|
|
422
450
|
// =========================================================================
|
|
@@ -466,6 +494,9 @@ export class EventGate {
|
|
|
466
494
|
if (policy.match.pathGlob) {
|
|
467
495
|
compiled.pathRegex = compileGlob(policy.match.pathGlob);
|
|
468
496
|
}
|
|
497
|
+
if (policy.match.tagsAny) compiled.tagsAnyRe = policy.match.tagsAny.map(compileGlob);
|
|
498
|
+
if (policy.match.tagsAll) compiled.tagsAllRe = policy.match.tagsAll.map(compileGlob);
|
|
499
|
+
if (policy.match.tagsNone) compiled.tagsNoneRe = policy.match.tagsNone.map(compileGlob);
|
|
469
500
|
return compiled;
|
|
470
501
|
});
|
|
471
502
|
}
|
|
@@ -534,6 +565,105 @@ export class EventGate {
|
|
|
534
565
|
}
|
|
535
566
|
}
|
|
536
567
|
|
|
568
|
+
// =========================================================================
|
|
569
|
+
// Runtime policy mutation (backs wake_add_rule / wake_remove_rule)
|
|
570
|
+
// =========================================================================
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Add or replace (upsert) a single policy in gate.json and apply it live.
|
|
574
|
+
*
|
|
575
|
+
* The raw policy is validated with the SAME `validatePolicy` used when loading
|
|
576
|
+
* gate.json — an invalid shape throws (nothing is written) and the caller
|
|
577
|
+
* surfaces it as a tool error. On success the policy is persisted to gate.json
|
|
578
|
+
* (so it survives restart and is visible to `workspace--edit _config/gate.json`)
|
|
579
|
+
* and the in-memory config is swapped in immediately, so the rule takes effect
|
|
580
|
+
* without waiting for the throttled mtime reload.
|
|
581
|
+
*
|
|
582
|
+
* The freshest on-disk config is re-read first, so a concurrent operator edit
|
|
583
|
+
* isn't clobbered. A policy with the same `name` is replaced IN PLACE (order
|
|
584
|
+
* preserved); otherwise it is appended, or prepended when `position:'prepend'`
|
|
585
|
+
* — first match wins, so a wake rule that must beat a broad defer/debounce
|
|
586
|
+
* belongs at the front.
|
|
587
|
+
*/
|
|
588
|
+
addPolicy(rawPolicy: unknown, options?: { position?: 'append' | 'prepend' }): GatePolicy {
|
|
589
|
+
const policy = validatePolicy(rawPolicy);
|
|
590
|
+
const current = this.loadConfig() ?? this.config;
|
|
591
|
+
const policies = [...current.policies];
|
|
592
|
+
const idx = policies.findIndex((p) => p.name === policy.name);
|
|
593
|
+
if (idx >= 0) {
|
|
594
|
+
policies[idx] = policy;
|
|
595
|
+
} else if (options?.position === 'prepend') {
|
|
596
|
+
policies.unshift(policy);
|
|
597
|
+
} else {
|
|
598
|
+
policies.push(policy);
|
|
599
|
+
}
|
|
600
|
+
this.writeConfig({ ...current, policies });
|
|
601
|
+
this.emitTrace({
|
|
602
|
+
type: 'gate:policy-added',
|
|
603
|
+
configPath: this.configPath,
|
|
604
|
+
policyName: policy.name,
|
|
605
|
+
replaced: idx >= 0,
|
|
606
|
+
timestamp: Date.now(),
|
|
607
|
+
});
|
|
608
|
+
return policy;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Remove a policy by name from gate.json and clear its live state. Any pending
|
|
613
|
+
* debounce batch is delivered first (matching reloadIfChanged's removal path,
|
|
614
|
+
* so buffered events aren't silently dropped). Returns false if no policy of
|
|
615
|
+
* that name exists (nothing written). Freshest on-disk config is re-read first
|
|
616
|
+
* (see addPolicy).
|
|
617
|
+
*/
|
|
618
|
+
removePolicy(name: string): boolean {
|
|
619
|
+
const current = this.loadConfig() ?? this.config;
|
|
620
|
+
if (!current.policies.some((p) => p.name === name)) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
// Deliver any pending debounce batch, then drop timers/buckets/counters.
|
|
624
|
+
this.clearPolicyState(name, { deliverPendingDebounce: true });
|
|
625
|
+
this.stats.delete(name);
|
|
626
|
+
const policies = current.policies.filter((p) => p.name !== name);
|
|
627
|
+
this.writeConfig({ ...current, policies });
|
|
628
|
+
this.emitTrace({
|
|
629
|
+
type: 'gate:policy-removed',
|
|
630
|
+
configPath: this.configPath,
|
|
631
|
+
policyName: name,
|
|
632
|
+
timestamp: Date.now(),
|
|
633
|
+
});
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* List the current policy names (freshest on-disk view). Cheap helper for
|
|
639
|
+
* tools/UX that want to show what's active without the full status payload.
|
|
640
|
+
*/
|
|
641
|
+
listPolicyNames(): string[] {
|
|
642
|
+
const current = this.loadConfig() ?? this.config;
|
|
643
|
+
return current.policies.map((p) => p.name);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Persist a config to gate.json and apply it in memory together: write the
|
|
648
|
+
* file, swap in the compiled policies, and sync `configMtime` to the new file
|
|
649
|
+
* so `reloadIfChanged` doesn't redundantly re-parse identical content. Throws
|
|
650
|
+
* if the write fails (disk/permission) — callers surface it as a tool error.
|
|
651
|
+
*/
|
|
652
|
+
private writeConfig(config: GateConfig): void {
|
|
653
|
+
mkdirSync(dirname(this.configPath), { recursive: true });
|
|
654
|
+
writeFileSync(this.configPath, JSON.stringify(config, null, 2) + '\n');
|
|
655
|
+
this.config = config;
|
|
656
|
+
this.compiledPolicies = this.compilePolicies(config);
|
|
657
|
+
this.configSource = 'file';
|
|
658
|
+
this.configErrors = [];
|
|
659
|
+
this.lastReloadTimestamp = Date.now();
|
|
660
|
+
try {
|
|
661
|
+
this.configMtime = statSync(this.configPath).mtimeMs;
|
|
662
|
+
} catch {
|
|
663
|
+
// A failed stat only risks one redundant reload; keep the old mtime.
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
537
667
|
// =========================================================================
|
|
538
668
|
// Policy matching — first match wins
|
|
539
669
|
// =========================================================================
|
|
@@ -604,6 +734,16 @@ export class EventGate {
|
|
|
604
734
|
if (!anyTrue) return false;
|
|
605
735
|
}
|
|
606
736
|
|
|
737
|
+
// Tag matching (MCPL RFC-001). Patterns are globs; `tags` is the event's
|
|
738
|
+
// (implication-expanded) tag set. A pattern "matches" if it matches ANY tag.
|
|
739
|
+
if (compiled.tagsAnyRe || compiled.tagsAllRe || compiled.tagsNoneRe) {
|
|
740
|
+
const tags = info.tags ?? [];
|
|
741
|
+
const patternHits = (re: RegExp): boolean => tags.some(t => re.test(t));
|
|
742
|
+
if (compiled.tagsAnyRe && !compiled.tagsAnyRe.some(patternHits)) return false;
|
|
743
|
+
if (compiled.tagsAllRe && !compiled.tagsAllRe.every(patternHits)) return false;
|
|
744
|
+
if (compiled.tagsNoneRe && compiled.tagsNoneRe.some(patternHits)) return false;
|
|
745
|
+
}
|
|
746
|
+
|
|
607
747
|
return true;
|
|
608
748
|
}
|
|
609
749
|
|
|
@@ -643,7 +783,12 @@ export class EventGate {
|
|
|
643
783
|
// Privileged author — fall through to normal policy evaluation.
|
|
644
784
|
}
|
|
645
785
|
|
|
646
|
-
|
|
786
|
+
// Programmable gate (gate.js) takes precedence when present and returns a
|
|
787
|
+
// behavior; null/undefined (inactive, error, or a deliberate pass) falls
|
|
788
|
+
// through to the declarative gate.json policies below.
|
|
789
|
+
const scripted = this.gateScript.evaluate(info);
|
|
790
|
+
const policy: GatePolicy | null =
|
|
791
|
+
scripted != null ? { name: 'gate.js', match: {}, behavior: scripted } : this.matchPolicy(info);
|
|
647
792
|
const decision = this.computeDecision(policy, info);
|
|
648
793
|
|
|
649
794
|
this.totalEvaluations++;
|
|
@@ -696,8 +841,9 @@ export class EventGate {
|
|
|
696
841
|
timestamp: this.now(),
|
|
697
842
|
});
|
|
698
843
|
|
|
699
|
-
if (policy.behavior === 'skip') {
|
|
700
|
-
|
|
844
|
+
if (policy.behavior === 'defer' || policy.behavior === 'skip') {
|
|
845
|
+
// Report the configured name faithfully ('defer' or legacy 'skip').
|
|
846
|
+
return { trigger: false, policyName: policy.name, behavior: policy.behavior };
|
|
701
847
|
}
|
|
702
848
|
|
|
703
849
|
if (policy.behavior === 'always') {
|
|
@@ -854,6 +1000,7 @@ export class EventGate {
|
|
|
854
1000
|
serverId: (metadata.serverId as string) ?? '',
|
|
855
1001
|
channelId: (metadata.channelId as string) ?? '',
|
|
856
1002
|
metadata,
|
|
1003
|
+
tags: Array.isArray(metadata.tags) ? (metadata.tags as string[]) : undefined,
|
|
857
1004
|
});
|
|
858
1005
|
return decision.trigger;
|
|
859
1006
|
};
|
|
@@ -955,6 +1102,18 @@ export class EventGate {
|
|
|
955
1102
|
}
|
|
956
1103
|
}
|
|
957
1104
|
|
|
1105
|
+
/** Live inference-gating snapshot for diagnostics (e.g. a SIGUSR2 dump).
|
|
1106
|
+
* A non-empty `inferring` with a stale timestamp + growing `bufferedEvents`
|
|
1107
|
+
* is the signature of the wake-wedge: an agent stuck mid-"inference" so all
|
|
1108
|
+
* incoming events buffer and never trigger. */
|
|
1109
|
+
inferenceDiagnostics(): { inferring: string[]; bufferedEvents: number; sleepUntil: number } {
|
|
1110
|
+
return {
|
|
1111
|
+
inferring: [...this.inferring],
|
|
1112
|
+
bufferedEvents: this.inferenceBuffer.length,
|
|
1113
|
+
sleepUntil: this.sleepUntil,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
958
1117
|
// =========================================================================
|
|
959
1118
|
// Inference buffer cap
|
|
960
1119
|
// =========================================================================
|
|
@@ -978,11 +1137,17 @@ export class EventGate {
|
|
|
978
1137
|
// =========================================================================
|
|
979
1138
|
|
|
980
1139
|
/** Begin (or extend) a sleep window of `seconds`. Returns the wake time. */
|
|
981
|
-
setSleep(seconds: number, note?: string): { until: number } {
|
|
1140
|
+
setSleep(seconds: number, note?: string, agentName?: string): { until: number } {
|
|
982
1141
|
const ms = Math.max(0, Math.floor(seconds * 1000));
|
|
983
1142
|
this.sleepUntil = this.now() + ms;
|
|
984
1143
|
this.sleepNote = note;
|
|
1144
|
+
this.sleepAgent = agentName;
|
|
985
1145
|
this.sleepSuppressed = 0;
|
|
1146
|
+
// Arm the self-wake: when the window elapses, clear sleep and request an
|
|
1147
|
+
// inference so the agent resumes on its own (not dependent on a heartbeat
|
|
1148
|
+
// tick or an incoming message). Cancelled by clearSleep()/`wake`.
|
|
1149
|
+
if (this.sleepTimer) clearTimeout(this.sleepTimer);
|
|
1150
|
+
this.sleepTimer = setTimeout(() => this.wakeFromSleep('sleep-expired'), ms);
|
|
986
1151
|
this.reloadPrivilegedIfChanged();
|
|
987
1152
|
this.emitTrace({
|
|
988
1153
|
type: 'gate:decision',
|
|
@@ -1000,9 +1165,39 @@ export class EventGate {
|
|
|
1000
1165
|
const wasAsleep = this.sleepUntil > this.now();
|
|
1001
1166
|
this.sleepUntil = 0;
|
|
1002
1167
|
this.sleepNote = undefined;
|
|
1168
|
+
this.sleepAgent = undefined;
|
|
1169
|
+
if (this.sleepTimer) { clearTimeout(this.sleepTimer); this.sleepTimer = null; }
|
|
1003
1170
|
return wasAsleep;
|
|
1004
1171
|
}
|
|
1005
1172
|
|
|
1173
|
+
/**
|
|
1174
|
+
* Self-wake handler: fired by the sleepTimer when a sleep window elapses.
|
|
1175
|
+
* Clears the sleep state and requests an inference so the agent wakes on its
|
|
1176
|
+
* own — this is what makes `sleep(N)` a real "wake me in N seconds" rather
|
|
1177
|
+
* than just a suppression window. No-op if sleep was already cleared (via
|
|
1178
|
+
* `wake`) or re-armed to a later time by a fresh `sleep` call.
|
|
1179
|
+
*/
|
|
1180
|
+
private wakeFromSleep(reason: string): void {
|
|
1181
|
+
this.sleepTimer = null;
|
|
1182
|
+
if (this.sleepUntil === 0 || this.sleepUntil > this.now()) return;
|
|
1183
|
+
const note = this.sleepNote;
|
|
1184
|
+
const agent = this.sleepAgent;
|
|
1185
|
+
this.sleepUntil = 0;
|
|
1186
|
+
this.sleepNote = undefined;
|
|
1187
|
+
this.sleepAgent = undefined;
|
|
1188
|
+
this.emitTrace({
|
|
1189
|
+
type: 'gate:decision',
|
|
1190
|
+
eventType: 'sleep:expired',
|
|
1191
|
+
matchedPolicy: 'asleep',
|
|
1192
|
+
trigger: true,
|
|
1193
|
+
behavior: 'always',
|
|
1194
|
+
timestamp: this.now(),
|
|
1195
|
+
});
|
|
1196
|
+
const detail = note ? `${reason}: ${note}` : reason;
|
|
1197
|
+
const targets = agent ? [agent] : this.getAgentNamesFn();
|
|
1198
|
+
for (const a of targets) this.requestInferenceFn(a, detail, 'sleep');
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1006
1201
|
/** Current sleep state, or null if awake. */
|
|
1007
1202
|
getSleepState(): { until: number; remainingMs: number; note?: string; suppressed: number } | null {
|
|
1008
1203
|
const remainingMs = this.sleepUntil - this.now();
|
|
@@ -1124,6 +1319,7 @@ export class EventGate {
|
|
|
1124
1319
|
configSource: this.configSource,
|
|
1125
1320
|
lastReloadTimestamp: this.lastReloadTimestamp,
|
|
1126
1321
|
default: this.config.default ?? 'always',
|
|
1322
|
+
script: this.gateScript.status(),
|
|
1127
1323
|
policies,
|
|
1128
1324
|
errors: [...this.configErrors],
|
|
1129
1325
|
totalEvaluations: this.totalEvaluations,
|
|
@@ -1140,6 +1336,7 @@ export class EventGate {
|
|
|
1140
1336
|
// =========================================================================
|
|
1141
1337
|
|
|
1142
1338
|
dispose(): void {
|
|
1339
|
+
this.gateScript.dispose();
|
|
1143
1340
|
for (const state of this.debounceTimers.values()) {
|
|
1144
1341
|
clearTimeout(state.timer);
|
|
1145
1342
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker that runs the agent-authored `gate.js` decision function.
|
|
3
|
+
*
|
|
4
|
+
* The agent is trusted (it already has shell + filesystem), so this is NOT a
|
|
5
|
+
* security sandbox — it's a *robustness* boundary: running the script on a
|
|
6
|
+
* separate thread lets the main thread time it out (Atomics.wait) so an
|
|
7
|
+
* accidental infinite loop degrades to "fall through to declarative policies"
|
|
8
|
+
* instead of freezing the agent's own event loop.
|
|
9
|
+
*
|
|
10
|
+
* Protocol:
|
|
11
|
+
* - on startup we import `gate.js` and post `{ type: 'ready', error? }`.
|
|
12
|
+
* - per event the parent posts `{ type: 'eval', event }`; we run the script
|
|
13
|
+
* and write the JSON result into the shared buffer, flip the control word,
|
|
14
|
+
* and notify. The parent is blocked in Atomics.wait until then (or times out).
|
|
15
|
+
*
|
|
16
|
+
* `gate.js` shape (ESM): `export default (event) => behavior | null` where
|
|
17
|
+
* behavior is a GateBehavior ('always' | 'defer' | { debounce } | …) and null
|
|
18
|
+
* means "let the declarative gate.json policies decide". Sync or async.
|
|
19
|
+
*/
|
|
20
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
21
|
+
import { pathToFileURL } from 'node:url';
|
|
22
|
+
|
|
23
|
+
interface WorkerData {
|
|
24
|
+
scriptPath: string;
|
|
25
|
+
sab: SharedArrayBuffer;
|
|
26
|
+
}
|
|
27
|
+
const { scriptPath, sab } = workerData as WorkerData;
|
|
28
|
+
const ctrl = new Int32Array(sab, 0, 2); // [0]=status (0 pending, 1 done), [1]=result byte length
|
|
29
|
+
const data = new Uint8Array(sab, 8);
|
|
30
|
+
const enc = new TextEncoder();
|
|
31
|
+
|
|
32
|
+
type DecideFn = (event: unknown) => unknown;
|
|
33
|
+
let fn: DecideFn | null = null;
|
|
34
|
+
|
|
35
|
+
// Load eagerly and announce readiness, so the parent doesn't time out (and
|
|
36
|
+
// pointlessly respawn) while a cold dynamic import is still in flight.
|
|
37
|
+
void (async () => {
|
|
38
|
+
let error: string | null = null;
|
|
39
|
+
try {
|
|
40
|
+
const mod = (await import(pathToFileURL(scriptPath).href)) as Record<string, unknown>;
|
|
41
|
+
const f = (mod.default ?? mod.decide ?? mod.gate) as unknown;
|
|
42
|
+
if (typeof f !== 'function') {
|
|
43
|
+
error = 'gate.js must export a default function: (event) => behavior | null';
|
|
44
|
+
} else {
|
|
45
|
+
fn = f as DecideFn;
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
error = (e as Error)?.message ?? String(e);
|
|
49
|
+
}
|
|
50
|
+
parentPort?.postMessage({ type: 'ready', error });
|
|
51
|
+
})();
|
|
52
|
+
|
|
53
|
+
function writeResult(obj: unknown): void {
|
|
54
|
+
const bytes = enc.encode(JSON.stringify(obj));
|
|
55
|
+
const n = Math.min(bytes.length, data.length);
|
|
56
|
+
data.set(bytes.subarray(0, n));
|
|
57
|
+
Atomics.store(ctrl, 1, n);
|
|
58
|
+
Atomics.store(ctrl, 0, 1);
|
|
59
|
+
Atomics.notify(ctrl, 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
parentPort?.on('message', (msg: { type?: string; event?: unknown }) => {
|
|
63
|
+
if (msg?.type !== 'eval') return;
|
|
64
|
+
void (async () => {
|
|
65
|
+
if (!fn) return writeResult({ behavior: null }); // load failed → fall through
|
|
66
|
+
try {
|
|
67
|
+
const r = await fn(msg.event); // sync OR async decision functions
|
|
68
|
+
writeResult({ behavior: r ?? null });
|
|
69
|
+
} catch (e) {
|
|
70
|
+
writeResult({ error: (e as Error)?.message ?? String(e) });
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GateScript — runs an agent-authored `gate.js` decision function on a worker
|
|
3
|
+
* thread, synchronously from the gate's perspective, with a hard timeout.
|
|
4
|
+
*
|
|
5
|
+
* The agent is trusted (shell + fs already), so this is a robustness seatbelt,
|
|
6
|
+
* not a security sandbox: a script that throws falls through to the declarative
|
|
7
|
+
* policies (and the error is surfaced via status()); a script that hangs is
|
|
8
|
+
* timed out (Atomics.wait), the wedged worker is terminated and respawned, and
|
|
9
|
+
* the event falls through — so a bad rule can't brick the agent's own attention.
|
|
10
|
+
*
|
|
11
|
+
* Sync model: the main thread posts the event then blocks in Atomics.wait until
|
|
12
|
+
* the worker writes the result into shared memory (or the timeout elapses).
|
|
13
|
+
* Node permits Atomics.wait on the main thread, so evaluate() stays synchronous
|
|
14
|
+
* and the rest of the EventGate is unchanged.
|
|
15
|
+
*/
|
|
16
|
+
import { Worker } from 'node:worker_threads';
|
|
17
|
+
import { existsSync, statSync } from 'node:fs';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import type { GateBehavior, GateEventInfo } from './types.js';
|
|
21
|
+
|
|
22
|
+
const WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), 'gate-script-worker.js');
|
|
23
|
+
const SAB_SIZE = 16_384;
|
|
24
|
+
const RELOAD_THROTTLE_MS = 1000;
|
|
25
|
+
|
|
26
|
+
export interface GateScriptStatus {
|
|
27
|
+
active: boolean;
|
|
28
|
+
ready: boolean;
|
|
29
|
+
path: string;
|
|
30
|
+
runs: number;
|
|
31
|
+
errors: number;
|
|
32
|
+
timeouts: number;
|
|
33
|
+
lastError: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ScriptResult {
|
|
37
|
+
behavior?: GateBehavior | null;
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class GateScript {
|
|
42
|
+
private worker?: Worker;
|
|
43
|
+
private readonly sab = new SharedArrayBuffer(SAB_SIZE);
|
|
44
|
+
private readonly ctrl = new Int32Array(this.sab, 0, 2);
|
|
45
|
+
private readonly data = new Uint8Array(this.sab, 8);
|
|
46
|
+
private readonly dec = new TextDecoder();
|
|
47
|
+
private mtime = 0;
|
|
48
|
+
private lastCheck = 0;
|
|
49
|
+
private active = false;
|
|
50
|
+
private ready = false;
|
|
51
|
+
private lastError: string | null = null;
|
|
52
|
+
private runs = 0;
|
|
53
|
+
private errors = 0;
|
|
54
|
+
private timeouts = 0;
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
private readonly scriptPath: string,
|
|
58
|
+
private readonly timeoutMs = 50,
|
|
59
|
+
private readonly now: () => number = Date.now,
|
|
60
|
+
) {
|
|
61
|
+
this.reloadIfChanged();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Hot-reload: (re)spawn the worker when gate.js appears or changes; stop it
|
|
65
|
+
* when the file is removed. Throttled like the gate's config reload. */
|
|
66
|
+
private reloadIfChanged(): void {
|
|
67
|
+
const t = this.now();
|
|
68
|
+
if (this.lastCheck !== 0 && t - this.lastCheck < RELOAD_THROTTLE_MS) return;
|
|
69
|
+
this.lastCheck = t;
|
|
70
|
+
if (!existsSync(this.scriptPath)) {
|
|
71
|
+
if (this.active) this.stop();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let m = 0;
|
|
75
|
+
try {
|
|
76
|
+
m = statSync(this.scriptPath).mtimeMs;
|
|
77
|
+
} catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (this.active && m === this.mtime) return;
|
|
81
|
+
this.mtime = m;
|
|
82
|
+
this.spawn();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private spawn(): void {
|
|
86
|
+
this.stop();
|
|
87
|
+
try {
|
|
88
|
+
const w = new Worker(WORKER_PATH, {
|
|
89
|
+
workerData: { scriptPath: this.scriptPath, sab: this.sab },
|
|
90
|
+
});
|
|
91
|
+
w.on('message', (m: { type?: string; error?: string | null }) => {
|
|
92
|
+
if (m?.type === 'ready') {
|
|
93
|
+
this.ready = true; // load finished (with or without a load error)
|
|
94
|
+
if (m.error) this.lastError = m.error;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
w.on('error', (e) => {
|
|
98
|
+
this.lastError = e.message;
|
|
99
|
+
});
|
|
100
|
+
w.unref(); // never keep the process alive on the gate script's account
|
|
101
|
+
this.worker = w;
|
|
102
|
+
this.active = true;
|
|
103
|
+
this.lastError = null;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
this.lastError = (e as Error).message;
|
|
106
|
+
this.active = false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private stop(): void {
|
|
111
|
+
if (this.worker) {
|
|
112
|
+
void this.worker.terminate().catch(() => {});
|
|
113
|
+
this.worker = undefined;
|
|
114
|
+
}
|
|
115
|
+
this.active = false;
|
|
116
|
+
this.ready = false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Evaluate the script for one event.
|
|
121
|
+
* - returns a GateBehavior → the script decided
|
|
122
|
+
* - returns null → fall through to the declarative policies
|
|
123
|
+
* - returns undefined → script inactive / errored / timed out (fall through)
|
|
124
|
+
*/
|
|
125
|
+
evaluate(event: GateEventInfo): GateBehavior | null | undefined {
|
|
126
|
+
this.reloadIfChanged();
|
|
127
|
+
// Inactive, or the worker hasn't finished importing gate.js yet → fall
|
|
128
|
+
// through (don't time-out/respawn during a cold start).
|
|
129
|
+
if (!this.active || !this.worker || !this.ready) return undefined;
|
|
130
|
+
|
|
131
|
+
Atomics.store(this.ctrl, 0, 0);
|
|
132
|
+
try {
|
|
133
|
+
this.worker.postMessage({ type: 'eval', event });
|
|
134
|
+
} catch (e) {
|
|
135
|
+
this.lastError = (e as Error).message;
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const r = Atomics.wait(this.ctrl, 0, 0, this.timeoutMs);
|
|
140
|
+
this.runs++;
|
|
141
|
+
if (r === 'timed-out') {
|
|
142
|
+
this.timeouts++;
|
|
143
|
+
this.lastError = `gate.js timed out (>${this.timeoutMs}ms) — terminated and restarted`;
|
|
144
|
+
this.spawn(); // kill the wedged worker, fresh one for next time
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const len = Atomics.load(this.ctrl, 1);
|
|
149
|
+
let parsed: ScriptResult;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(this.dec.decode(this.data.subarray(0, len))) as ScriptResult;
|
|
152
|
+
} catch {
|
|
153
|
+
this.errors++;
|
|
154
|
+
this.lastError = 'gate.js produced an unreadable result';
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
if (parsed.error) {
|
|
158
|
+
this.errors++;
|
|
159
|
+
this.lastError = parsed.error;
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return parsed.behavior ?? null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
status(): GateScriptStatus {
|
|
166
|
+
return {
|
|
167
|
+
active: this.active,
|
|
168
|
+
ready: this.ready,
|
|
169
|
+
path: this.scriptPath,
|
|
170
|
+
runs: this.runs,
|
|
171
|
+
errors: this.errors,
|
|
172
|
+
timeouts: this.timeouts,
|
|
173
|
+
lastError: this.lastError,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
dispose(): void {
|
|
178
|
+
this.stop();
|
|
179
|
+
}
|
|
180
|
+
}
|
package/src/gate/types.ts
CHANGED
|
@@ -52,7 +52,8 @@ export interface PassiveSampleConfig {
|
|
|
52
52
|
/** How a matching policy affects inference triggering. */
|
|
53
53
|
export type GateBehavior =
|
|
54
54
|
| 'always' // Trigger inference immediately
|
|
55
|
-
| '
|
|
55
|
+
| 'defer' // Don't trigger inference (event still enters context). Preferred name.
|
|
56
|
+
| 'skip' // Legacy alias for 'defer' (still accepted).
|
|
56
57
|
| { debounce: number } // Batch events per-policy, deliver after delay (ms)
|
|
57
58
|
| { rate_limit: RateLimitConfig } // Token bucket per `keyBy`; deny when empty
|
|
58
59
|
| { passive_sample: PassiveSampleConfig }; // Fire every Nth match
|
|
@@ -88,6 +89,17 @@ export interface GatePolicyMatch {
|
|
|
88
89
|
* matches if any of those is set. Empty/omitted = no metadata constraint.
|
|
89
90
|
*/
|
|
90
91
|
metadataTrue?: string[];
|
|
92
|
+
/**
|
|
93
|
+
* Tag matching (MCPL RFC-001 event tags). Globs allowed (e.g. `robotics:*`).
|
|
94
|
+
* Evaluated against the event's (implication-expanded) tag set:
|
|
95
|
+
* tagsAny — match if ANY listed pattern matches an event tag
|
|
96
|
+
* tagsAll — match only if EVERY listed pattern matches some event tag
|
|
97
|
+
* tagsNone — match only if NO listed pattern matches any event tag
|
|
98
|
+
* Combined with the other fields by AND, like the rest of the match.
|
|
99
|
+
*/
|
|
100
|
+
tagsAny?: string[];
|
|
101
|
+
tagsAll?: string[];
|
|
102
|
+
tagsNone?: string[];
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
// ---------------------------------------------------------------------------
|
|
@@ -116,7 +128,7 @@ export interface GatePolicy {
|
|
|
116
128
|
export interface GateConfig {
|
|
117
129
|
policies: GatePolicy[];
|
|
118
130
|
/** Behavior when no policy matches. Default: 'always'. */
|
|
119
|
-
default?: 'always' | 'skip';
|
|
131
|
+
default?: 'always' | 'defer' | 'skip';
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
// ---------------------------------------------------------------------------
|
|
@@ -163,6 +175,8 @@ export interface GateEventInfo {
|
|
|
163
175
|
mount?: string;
|
|
164
176
|
/** Mount-prefixed paths touched (for workspace:* events). */
|
|
165
177
|
paths?: string[];
|
|
178
|
+
/** Event tags (MCPL RFC-001), already implication-expanded by the host. */
|
|
179
|
+
tags?: string[];
|
|
166
180
|
}
|
|
167
181
|
|
|
168
182
|
/** Per-policy runtime statistics (for gate:status). */
|
|
@@ -192,7 +206,9 @@ export interface GateStatus {
|
|
|
192
206
|
configPath: string;
|
|
193
207
|
configSource: 'file' | 'initial' | 'default';
|
|
194
208
|
lastReloadTimestamp: number | null;
|
|
195
|
-
default: 'always' | 'skip';
|
|
209
|
+
default: 'always' | 'defer' | 'skip';
|
|
210
|
+
/** Status of the optional programmable gate (gate.js). */
|
|
211
|
+
script: import('./gate-script.js').GateScriptStatus;
|
|
196
212
|
policies: GatePolicyStats[];
|
|
197
213
|
errors: string[];
|
|
198
214
|
/** Total events the gate has evaluated since startup. */
|