@animalabs/agent-framework 0.30.0 → 0.32.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/README.md +71 -8
- package/dist/src/agent.d.ts +29 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +82 -17
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +138 -102
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1133 -752
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +24 -17
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +198 -144
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +5 -7
- package/dist/src/host-presentation.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +2 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- 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 +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +2 -20
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +175 -70
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +14 -2
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +6 -12
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +65 -24
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +4 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +90 -25
- package/src/api/mcp-server-core.ts +10 -3
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/framework.ts +1163 -770
- package/src/gate/event-gate.ts +220 -145
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +4 -7
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +2 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +187 -85
- package/src/tune-out/tools.ts +14 -2
- package/src/types/agent.ts +6 -12
- package/src/types/framework.ts +66 -24
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +4 -3
- package/dist/src/reasoning-loss-notice.d.ts +0 -23
- package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
- package/dist/src/reasoning-loss-notice.js +0 -52
- package/dist/src/reasoning-loss-notice.js.map +0 -1
- package/dist/src/reasoning-loss.d.ts +0 -24
- package/dist/src/reasoning-loss.d.ts.map +0 -1
- package/dist/src/reasoning-loss.js +0 -102
- package/dist/src/reasoning-loss.js.map +0 -1
- package/src/reasoning-loss-notice.ts +0 -64
- package/src/reasoning-loss.ts +0 -120
package/src/gate/event-gate.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { NOTICE_TAG } from '../notice-tags.js';
|
|
15
|
-
import { cutToFit, suppliedValue } from '../supplied-text.js';
|
|
15
|
+
import { cutToFit, quotedJson, quotedValue, suppliedValue } from '../supplied-text.js';
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
17
17
|
import { basename, dirname, join } from 'node:path';
|
|
18
18
|
import { GateScript } from './gate-script.js';
|
|
@@ -483,10 +483,10 @@ function beforeWriting(err: unknown): boolean {
|
|
|
483
483
|
* "policies" that isn't a list and holds something. `runs` is what the gate
|
|
484
484
|
* runs meanwhile (rulesRun).
|
|
485
485
|
*/
|
|
486
|
-
function noListRefusal(noList: string, runs: string): string {
|
|
486
|
+
function noListRefusal(noList: string, runs: string, path: string): string {
|
|
487
487
|
return `${noList}, so it has no list of rules a change could edit without writing over what it holds, and nothing `
|
|
488
|
-
+ `was saved. Meanwhile the gate runs ${runs}. Until gate.json is an object whose "policies" is a list
|
|
489
|
-
+ 'rules, a change is refused the same way.';
|
|
488
|
+
+ `was saved. Meanwhile the gate runs ${runs}. Until gate.json, at ${path}, is an object whose "policies" is a list `
|
|
489
|
+
+ 'holding its rules, a change is refused the same way.';
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
/**
|
|
@@ -495,15 +495,15 @@ function noListRefusal(noList: string, runs: string): string {
|
|
|
495
495
|
* written over it, so what it holds, often an operator's unfinished edit, stays
|
|
496
496
|
* as it is. `runs` is what the gate runs meanwhile (rulesRun).
|
|
497
497
|
*/
|
|
498
|
-
function unusableRefusal(unusable: { error: string; notJson?: true }, runs: string): string {
|
|
498
|
+
function unusableRefusal(unusable: { error: string; notJson?: true }, runs: string, path: string): string {
|
|
499
499
|
if (!unusable.notJson) {
|
|
500
|
-
return `gate.json couldn't be read (${unusable.error}), so a change can't edit it, and nothing was written
|
|
501
|
-
+ `Meanwhile the gate runs ${runs}. Whoever runs this host can make gate.json
|
|
502
|
-
+ 'refused the same way.';
|
|
500
|
+
return `gate.json, at ${path}, couldn't be read (${unusable.error}), so a change can't edit it, and nothing was written `
|
|
501
|
+
+ `over it. Meanwhile the gate runs ${runs}. Whoever runs this host can make gate.json a file the gate can read; `
|
|
502
|
+
+ 'until then, a change is refused the same way.';
|
|
503
503
|
}
|
|
504
|
-
return `gate.json isn't JSON the gate can read (${unusable.error}), so a change can't edit its rules without
|
|
505
|
-
+ `over what it holds, and nothing was saved. Meanwhile the gate runs ${runs}. Until gate.json is JSON, a change
|
|
506
|
-
+ 'refused the same way.';
|
|
504
|
+
return `gate.json, at ${path}, isn't JSON the gate can read (${unusable.error}), so a change can't edit its rules without `
|
|
505
|
+
+ `writing over what it holds, and nothing was saved. Meanwhile the gate runs ${runs}. Until gate.json is JSON, a change `
|
|
506
|
+
+ 'is refused the same way.';
|
|
507
507
|
}
|
|
508
508
|
|
|
509
509
|
/**
|
|
@@ -514,13 +514,19 @@ function unusableRefusal(unusable: { error: string; notJson?: true }, runs: stri
|
|
|
514
514
|
* (notUsedClause, describeStart) say which rules keep gate.json out of use
|
|
515
515
|
* and each fix.
|
|
516
516
|
*/
|
|
517
|
-
function notUsedRefusal(runs: string): string {
|
|
518
|
-
return
|
|
517
|
+
function notUsedRefusal(runs: string, path: string): string {
|
|
518
|
+
return `gate.json, at ${path}, isn't used at this start, which runs as one without it does, and a change would put gate.json's rules in `
|
|
519
519
|
+ `force in place of what that runs, so nothing was saved. Meanwhile the gate runs ${runs}. gate_status says what keeps `
|
|
520
520
|
+ 'gate.json out of use and what would put it in use, such as giving it rules under "policies", or fixing the starting '
|
|
521
521
|
+ 'rules, which whoever runs this host can do, for the next start; until then, a change is refused the same way.';
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
function savedOnlyInForce(path: string, remedy: string | undefined): string {
|
|
525
|
+
return remedy === undefined
|
|
526
|
+
? ''
|
|
527
|
+
: ` gate.json, at ${path}, isn't in force, and a change is saved only if it brings it into force. ${remedy}`;
|
|
528
|
+
}
|
|
529
|
+
|
|
524
530
|
/**
|
|
525
531
|
* A value as a message quotes it: its JSON, cut short when long, never inside a
|
|
526
532
|
* surrogate pair, since the message reaches the agent's context.
|
|
@@ -531,7 +537,8 @@ export function shownValue(value: unknown): string {
|
|
|
531
537
|
if (typeof value === 'number' && !Number.isFinite(value)) return String(value);
|
|
532
538
|
let json: string;
|
|
533
539
|
try {
|
|
534
|
-
|
|
540
|
+
const stringified = JSON.stringify(value);
|
|
541
|
+
json = stringified === undefined ? String(value) : quotedJson(stringified);
|
|
535
542
|
} catch {
|
|
536
543
|
json = String(value);
|
|
537
544
|
}
|
|
@@ -548,9 +555,22 @@ function joinedList(items: readonly string[], joiner: 'and' | 'or' = 'and'): str
|
|
|
548
555
|
return items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} ${joiner} ${items[items.length - 1]}`;
|
|
549
556
|
}
|
|
550
557
|
|
|
558
|
+
const asGateNames = (path: string): string => path;
|
|
559
|
+
|
|
560
|
+
function namedBy(names: Readonly<Record<string, string>> | undefined): (path: string) => string {
|
|
561
|
+
if (!names) return asGateNames;
|
|
562
|
+
return (path) => {
|
|
563
|
+
for (let end = path.length; end > 0; end = path.lastIndexOf('.', end - 1)) {
|
|
564
|
+
const head = path.slice(0, end);
|
|
565
|
+
if (Object.hasOwn(names, head)) return `${names[head]}${path.slice(end)}`;
|
|
566
|
+
}
|
|
567
|
+
return path;
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
551
571
|
/** `"a"`, `"a" and "b"`, `"a", "b" and "c"` (or "or"). */
|
|
552
572
|
function quotedList(names: readonly string[], joiner: 'and' | 'or' = 'and'): string {
|
|
553
|
-
return joinedList(names.map((n) =>
|
|
573
|
+
return joinedList(names.map((n) => quotedValue(n)), joiner);
|
|
554
574
|
}
|
|
555
575
|
|
|
556
576
|
/** 1st, 2nd, 3rd, 4th, 11th, 21st. */
|
|
@@ -611,7 +631,7 @@ function fieldNames(
|
|
|
611
631
|
|
|
612
632
|
/** `"tags" (did you mean "tagsAny", "tagsAll" or "tagsNone"?)` */
|
|
613
633
|
function slipShown(slip: Slip): string {
|
|
614
|
-
return
|
|
634
|
+
return `${quotedValue(slip.key)} (did you mean ${quotedList(slip.meant, 'or')}?)`;
|
|
615
635
|
}
|
|
616
636
|
|
|
617
637
|
/** Names an object has that are fields read at another level, with where that is (`homes`). */
|
|
@@ -651,22 +671,22 @@ function misplacedShown(m: Misplaced): string {
|
|
|
651
671
|
* The levels of a gate.json the gate reads fields at, each as a message names
|
|
652
672
|
* it, `file` naming the file itself.
|
|
653
673
|
*/
|
|
654
|
-
function fieldLevels(file: string): ReadonlyArray<{ name: string; fields: readonly string[] }> {
|
|
674
|
+
function fieldLevels(file: string, named: (path: string) => string = asGateNames): ReadonlyArray<{ name: string; fields: readonly string[] }> {
|
|
655
675
|
return [
|
|
656
676
|
{ name: file, fields: CONFIG_FIELDS },
|
|
657
677
|
{ name: 'a rule', fields: POLICY_FIELDS },
|
|
658
678
|
{ name: '"match"', fields: MATCH_FIELDS },
|
|
659
679
|
{ name: '"match.filter"', fields: FILTER_FIELDS },
|
|
660
680
|
{ name: '"behavior"', fields: BEHAVIOR_KINDS },
|
|
661
|
-
{ name: '
|
|
662
|
-
{ name: '
|
|
663
|
-
{ name: '
|
|
681
|
+
{ name: `"${named('behavior.rate_limit')}"`, fields: RATE_LIMIT_FIELDS },
|
|
682
|
+
{ name: `"${named('behavior.passive_sample')}"`, fields: PASSIVE_SAMPLE_FIELDS },
|
|
683
|
+
{ name: `"${named('behavior.judge')}"`, fields: JUDGE_FIELDS },
|
|
664
684
|
];
|
|
665
685
|
}
|
|
666
686
|
|
|
667
687
|
/** Where a name is a field the gate reads, at a level other than the one whose fields are `here`. */
|
|
668
|
-
function homesOf(file: string, here: readonly string[]): (key: string) => string[] {
|
|
669
|
-
return (key) => fieldLevels(file)
|
|
688
|
+
function homesOf(file: string, here: readonly string[], named?: (path: string) => string): (key: string) => string[] {
|
|
689
|
+
return (key) => fieldLevels(file, named)
|
|
670
690
|
.filter((level) => level.fields !== here && level.fields.includes(key))
|
|
671
691
|
.map((level) => level.name);
|
|
672
692
|
}
|
|
@@ -798,7 +818,7 @@ interface RuleProblem {
|
|
|
798
818
|
|
|
799
819
|
/** A rule as the gate reads it. */
|
|
800
820
|
interface RuleReading {
|
|
801
|
-
/** How messages name it: `
|
|
821
|
+
/** How messages name it: `Rule "x"`, or its place when it has no name. */
|
|
802
822
|
where: string;
|
|
803
823
|
/** The rule as read, unless a problem has no reading. */
|
|
804
824
|
policy?: GatePolicy;
|
|
@@ -833,6 +853,7 @@ interface Reader {
|
|
|
833
853
|
where: string;
|
|
834
854
|
/** How messages name the file the rule is in, for a field of the file's own put in the rule. */
|
|
835
855
|
file: string;
|
|
856
|
+
named: (path: string) => string;
|
|
836
857
|
problems: RuleProblem[];
|
|
837
858
|
notes: string[];
|
|
838
859
|
/** Set where its rate limit's tokens are a number not over 0, so it wakes on none of what it matches. */
|
|
@@ -859,14 +880,15 @@ function readFieldNames(
|
|
|
859
880
|
misplacedReadOf: (m: Misplaced) => string | undefined = () => undefined,
|
|
860
881
|
): void {
|
|
861
882
|
const { slips, unread } = fieldNames(obj, fields);
|
|
862
|
-
const { misplaced, rest } = placedElsewhere(unread, homesOf(r.file, fields));
|
|
863
|
-
|
|
883
|
+
const { misplaced, rest } = placedElsewhere(unread, homesOf(r.file, fields, r.named));
|
|
884
|
+
const shown = level === null ? null : r.named(level);
|
|
885
|
+
noteUnread(shown ? `${r.where}: ${shown}` : r.where, rest, fields, r.notes);
|
|
864
886
|
for (const slip of slips) {
|
|
865
|
-
r.problems.push({ text: `${
|
|
887
|
+
r.problems.push({ text: `${shown ?? 'it'} has ${slipShown(slip)}, which the gate doesn't read`, read: readOf(slip) });
|
|
866
888
|
}
|
|
867
889
|
for (const m of misplaced) {
|
|
868
890
|
r.problems.push({
|
|
869
|
-
text: `${
|
|
891
|
+
text: `${shown ?? 'it'} has ${misplacedShown(m)}, which the gate doesn't read here`,
|
|
870
892
|
read: misplacedReadOf(m) ?? (m.keys.length === 1 ? SLIP_READ : 'so they do nothing'),
|
|
871
893
|
});
|
|
872
894
|
}
|
|
@@ -881,7 +903,7 @@ function droppedEntries(count: number): string {
|
|
|
881
903
|
* A rule, read as written, or past each problem it has. `unnamed` names a rule
|
|
882
904
|
* without a name in messages, and `file` the file it's in.
|
|
883
905
|
*/
|
|
884
|
-
function readRule(raw: unknown, unnamed: string, file = 'gate.json'): RuleReading {
|
|
906
|
+
function readRule(raw: unknown, unnamed: string, file = 'gate.json', named: (path: string) => string = asGateNames): RuleReading {
|
|
885
907
|
if (!isPlainObject(raw)) {
|
|
886
908
|
return {
|
|
887
909
|
where: unnamed,
|
|
@@ -891,7 +913,7 @@ function readRule(raw: unknown, unnamed: string, file = 'gate.json'): RuleReadin
|
|
|
891
913
|
};
|
|
892
914
|
}
|
|
893
915
|
const name = nameOf(raw);
|
|
894
|
-
const r: Reader = { where: name ? `
|
|
916
|
+
const r: Reader = { where: name ? `Rule ${quotedValue(name)}` : unnamed, file, named, problems: [], notes: [] };
|
|
895
917
|
const obj = withoutEmpty(r.where, raw, r.notes, RULE_LEFT_OUT);
|
|
896
918
|
// Field names first, so a slip of one is named before what its absence does.
|
|
897
919
|
readFieldNames(r, null, obj, POLICY_FIELDS, (slip) => {
|
|
@@ -914,7 +936,7 @@ function readRule(raw: unknown, unnamed: string, file = 'gate.json'): RuleReadin
|
|
|
914
936
|
const { behavior, kind } = readBehavior(r, obj.behavior);
|
|
915
937
|
const match = readMatch(r, obj.match);
|
|
916
938
|
const resets = readResets(r, obj.resets);
|
|
917
|
-
const passthrough = readPassthrough(r, obj.passthrough,
|
|
939
|
+
const passthrough = readPassthrough(r, obj.passthrough, kind);
|
|
918
940
|
const reading: RuleReading = {
|
|
919
941
|
where: r.where, problems: r.problems, notes: r.notes, quiet: kind === 'skip' || kind === 'defer' || r.noTokens === true,
|
|
920
942
|
};
|
|
@@ -969,7 +991,7 @@ function readBehavior(r: Reader, value: unknown): { behavior?: GateBehavior | un
|
|
|
969
991
|
}
|
|
970
992
|
const behavior = READ_KIND[kind](r, b[kind]);
|
|
971
993
|
for (const other of kinds.slice(1)) {
|
|
972
|
-
const unread: Reader = { where: r.where, file: r.file, problems: [], notes: [] };
|
|
994
|
+
const unread: Reader = { where: r.where, file: r.file, named: r.named, problems: [], notes: [] };
|
|
973
995
|
READ_KIND[other](unread, b[other]);
|
|
974
996
|
for (const p of unread.problems) r.problems.push({ text: p.text, read: `which goes unread with the rest of "${other}"` });
|
|
975
997
|
}
|
|
@@ -985,18 +1007,18 @@ function readBehavior(r: Reader, value: unknown): { behavior?: GateBehavior | un
|
|
|
985
1007
|
*/
|
|
986
1008
|
function readDebounce(r: Reader, ms: unknown): GateBehavior | undefined {
|
|
987
1009
|
if (typeof ms !== 'number' || !Number.isFinite(ms)) {
|
|
988
|
-
r.problems.push({ text:
|
|
1010
|
+
r.problems.push({ text: `${r.named('behavior.debounce')} is ${shownValue(ms)}, where a number of milliseconds is needed, such as 60000` });
|
|
989
1011
|
return undefined;
|
|
990
1012
|
}
|
|
991
1013
|
if (ms < MIN_DEBOUNCE_MS) {
|
|
992
1014
|
r.problems.push({
|
|
993
|
-
text:
|
|
1015
|
+
text: `${r.named('behavior.debounce')} is ${ms} ms, under the ${MIN_DEBOUNCE_MS} ms minimum`, read: `so it's read as ${MIN_DEBOUNCE_MS} ms`,
|
|
994
1016
|
});
|
|
995
1017
|
return { debounce: MIN_DEBOUNCE_MS };
|
|
996
1018
|
}
|
|
997
1019
|
if (ms > MAX_DEBOUNCE_MS) {
|
|
998
1020
|
r.problems.push({
|
|
999
|
-
text:
|
|
1021
|
+
text: `${r.named('behavior.debounce')} is ${ms} ms, over the ${MAX_DEBOUNCE_MS} ms maximum`, read: `so it's read as ${MAX_DEBOUNCE_MS} ms`,
|
|
1000
1022
|
});
|
|
1001
1023
|
return { debounce: MAX_DEBOUNCE_MS };
|
|
1002
1024
|
}
|
|
@@ -1019,27 +1041,27 @@ function readDebounce(r: Reader, ms: unknown): GateBehavior | undefined {
|
|
|
1019
1041
|
function readRateLimit(r: Reader, value: unknown): GateBehavior | undefined {
|
|
1020
1042
|
if (!isPlainObject(value)) {
|
|
1021
1043
|
r.problems.push({
|
|
1022
|
-
text:
|
|
1044
|
+
text: `${r.named('behavior.rate_limit')} is ${shownValue(value)}, where an object is needed, such as {"tokens": 3, "refillIntervalMs": 60000}`,
|
|
1023
1045
|
});
|
|
1024
1046
|
return undefined;
|
|
1025
1047
|
}
|
|
1026
|
-
const conf = withoutEmpty(`${r.where}: behavior.rate_limit`, value, r.notes, { keyBy: 'every event the rule matches shares one bucket' });
|
|
1048
|
+
const conf = withoutEmpty(`${r.where}: ${r.named('behavior.rate_limit')}`, value, r.notes, { keyBy: 'every event the rule matches shares one bucket' });
|
|
1027
1049
|
readFieldNames(r, 'behavior.rate_limit', conf, RATE_LIMIT_FIELDS);
|
|
1028
1050
|
const tokens = conf.tokens;
|
|
1029
1051
|
const refill = conf.refillIntervalMs;
|
|
1030
1052
|
const tokensUsable = typeof tokens === 'number' && Number.isFinite(tokens) && tokens > 0;
|
|
1031
1053
|
if (typeof tokens === 'number' && !(tokens > 0)) r.noTokens = true;
|
|
1032
1054
|
if (!tokensUsable) {
|
|
1033
|
-
r.problems.push({ text:
|
|
1055
|
+
r.problems.push({ text: `${r.named('behavior.rate_limit.tokens')} is ${shownValue(tokens)}, where a positive number is needed` });
|
|
1034
1056
|
}
|
|
1035
1057
|
const refillUsable = typeof refill === 'number' && Number.isFinite(refill);
|
|
1036
1058
|
if (!refillUsable) {
|
|
1037
1059
|
r.problems.push({
|
|
1038
|
-
text:
|
|
1060
|
+
text: `${r.named('behavior.rate_limit.refillIntervalMs')} is ${shownValue(refill)}, where a positive number of milliseconds is needed`,
|
|
1039
1061
|
});
|
|
1040
1062
|
} else if (refill <= 0) {
|
|
1041
1063
|
r.problems.push({
|
|
1042
|
-
text:
|
|
1064
|
+
text: `${r.named('behavior.rate_limit.refillIntervalMs')} is ${refill}, where a positive number of milliseconds is needed`,
|
|
1043
1065
|
read: refill === 0
|
|
1044
1066
|
? 'so its tokens refill at once, and it limits nothing it matches'
|
|
1045
1067
|
: "so its tokens never refill, and once they're spent it wakes the agent on nothing it matches",
|
|
@@ -1059,21 +1081,21 @@ function readRateLimit(r: Reader, value: unknown): GateBehavior | undefined {
|
|
|
1059
1081
|
*/
|
|
1060
1082
|
function readPassiveSample(r: Reader, value: unknown): GateBehavior | undefined {
|
|
1061
1083
|
if (!isPlainObject(value)) {
|
|
1062
|
-
r.problems.push({ text:
|
|
1084
|
+
r.problems.push({ text: `${r.named('behavior.passive_sample')} is ${shownValue(value)}, where an object is needed, such as {"every": 10}` });
|
|
1063
1085
|
return undefined;
|
|
1064
1086
|
}
|
|
1065
|
-
const conf = withoutEmpty(`${r.where}: behavior.passive_sample`, value, r.notes, { keyBy: 'every event the rule matches shares one count' });
|
|
1087
|
+
const conf = withoutEmpty(`${r.where}: ${r.named('behavior.passive_sample')}`, value, r.notes, { keyBy: 'every event the rule matches shares one count' });
|
|
1066
1088
|
readFieldNames(r, 'behavior.passive_sample', conf, PASSIVE_SAMPLE_FIELDS);
|
|
1067
1089
|
const given = conf.every;
|
|
1068
1090
|
let every: number | undefined;
|
|
1069
1091
|
if (typeof given !== 'number' || !Number.isFinite(given)) {
|
|
1070
|
-
r.problems.push({ text:
|
|
1092
|
+
r.problems.push({ text: `${r.named('behavior.passive_sample.every')} is ${shownValue(given)}, where a whole number of 1 or more is needed` });
|
|
1071
1093
|
} else if (Number.isInteger(given) && given > 0) {
|
|
1072
1094
|
every = given;
|
|
1073
1095
|
} else {
|
|
1074
1096
|
every = given <= 0 ? 1 : Math.ceil(given);
|
|
1075
1097
|
r.problems.push({
|
|
1076
|
-
text:
|
|
1098
|
+
text: `${r.named('behavior.passive_sample.every')} is ${given}, ${given <= 0 ? 'under 1' : 'not a whole number'}`,
|
|
1077
1099
|
read: `so it's read as ${every}`,
|
|
1078
1100
|
});
|
|
1079
1101
|
}
|
|
@@ -1096,11 +1118,11 @@ function readPassiveSample(r: Reader, value: unknown): GateBehavior | undefined
|
|
|
1096
1118
|
function readJudge(r: Reader, value: unknown): GateBehavior | undefined {
|
|
1097
1119
|
if (!isPlainObject(value)) {
|
|
1098
1120
|
r.problems.push({
|
|
1099
|
-
text:
|
|
1121
|
+
text: `${r.named('behavior.judge')} is ${shownValue(value)}, where an object is needed, such as {"debounceMs": 30000, "instructions": "..."}`,
|
|
1100
1122
|
});
|
|
1101
1123
|
return undefined;
|
|
1102
1124
|
}
|
|
1103
|
-
const conf = withoutEmpty(`${r.where}: behavior.judge`, value, r.notes, {
|
|
1125
|
+
const conf = withoutEmpty(`${r.where}: ${r.named('behavior.judge')}`, value, r.notes, {
|
|
1104
1126
|
maxWaitMs: 'a batch is judged only after its quiet period',
|
|
1105
1127
|
instructions: 'the judge decides without the agent\'s word',
|
|
1106
1128
|
});
|
|
@@ -1109,16 +1131,16 @@ function readJudge(r: Reader, value: unknown): GateBehavior | undefined {
|
|
|
1109
1131
|
const ms = conf.debounceMs;
|
|
1110
1132
|
let debounceMs: number | undefined;
|
|
1111
1133
|
if (typeof ms !== 'number' || !Number.isFinite(ms)) {
|
|
1112
|
-
r.problems.push({ text:
|
|
1134
|
+
r.problems.push({ text: `${r.named('behavior.judge.debounceMs')} is ${shownValue(ms)}, where a number of milliseconds is needed, such as 30000` });
|
|
1113
1135
|
ok = false;
|
|
1114
1136
|
} else if (ms < MIN_DEBOUNCE_MS) {
|
|
1115
1137
|
r.problems.push({
|
|
1116
|
-
text:
|
|
1138
|
+
text: `${r.named('behavior.judge.debounceMs')} is ${ms} ms, under the ${MIN_DEBOUNCE_MS} ms minimum`, read: `so it's read as ${MIN_DEBOUNCE_MS} ms`,
|
|
1117
1139
|
});
|
|
1118
1140
|
debounceMs = MIN_DEBOUNCE_MS;
|
|
1119
1141
|
} else if (ms > MAX_DEBOUNCE_MS) {
|
|
1120
1142
|
r.problems.push({
|
|
1121
|
-
text:
|
|
1143
|
+
text: `${r.named('behavior.judge.debounceMs')} is ${ms} ms, over the ${MAX_DEBOUNCE_MS} ms maximum`, read: `so it's read as ${MAX_DEBOUNCE_MS} ms`,
|
|
1122
1144
|
});
|
|
1123
1145
|
debounceMs = MAX_DEBOUNCE_MS;
|
|
1124
1146
|
} else {
|
|
@@ -1128,19 +1150,19 @@ function readJudge(r: Reader, value: unknown): GateBehavior | undefined {
|
|
|
1128
1150
|
const wait = conf.maxWaitMs;
|
|
1129
1151
|
if (wait !== undefined) {
|
|
1130
1152
|
if (typeof wait !== 'number' || !Number.isFinite(wait)) {
|
|
1131
|
-
r.problems.push({ text:
|
|
1153
|
+
r.problems.push({ text: `${r.named('behavior.judge.maxWaitMs')} is ${shownValue(wait)}, where a number of milliseconds is needed, such as 120000` });
|
|
1132
1154
|
ok = false;
|
|
1133
1155
|
} else {
|
|
1134
1156
|
const floor = debounceMs ?? MIN_DEBOUNCE_MS;
|
|
1135
1157
|
if (wait < floor) {
|
|
1136
1158
|
r.problems.push({
|
|
1137
|
-
text:
|
|
1159
|
+
text: `${r.named('behavior.judge.maxWaitMs')} is ${wait} ms, under the rule's ${floor} ms quiet period`,
|
|
1138
1160
|
read: `so it's read as ${floor} ms`,
|
|
1139
1161
|
});
|
|
1140
1162
|
maxWaitMs = floor;
|
|
1141
1163
|
} else if (wait > MAX_JUDGE_WAIT_MS) {
|
|
1142
1164
|
r.problems.push({
|
|
1143
|
-
text:
|
|
1165
|
+
text: `${r.named('behavior.judge.maxWaitMs')} is ${wait} ms, over the ${MAX_JUDGE_WAIT_MS} ms maximum`,
|
|
1144
1166
|
read: `so it's read as ${MAX_JUDGE_WAIT_MS} ms`,
|
|
1145
1167
|
});
|
|
1146
1168
|
maxWaitMs = MAX_JUDGE_WAIT_MS;
|
|
@@ -1153,11 +1175,11 @@ function readJudge(r: Reader, value: unknown): GateBehavior | undefined {
|
|
|
1153
1175
|
let instructions: string | undefined;
|
|
1154
1176
|
if (said !== undefined) {
|
|
1155
1177
|
if (typeof said !== 'string') {
|
|
1156
|
-
r.problems.push({ text:
|
|
1178
|
+
r.problems.push({ text: `${r.named('behavior.judge.instructions')} is ${shownValue(said)}, where text is needed` });
|
|
1157
1179
|
ok = false;
|
|
1158
1180
|
} else if (said.length > MAX_JUDGE_INSTRUCTIONS) {
|
|
1159
1181
|
r.problems.push({
|
|
1160
|
-
text:
|
|
1182
|
+
text: `${r.named('behavior.judge.instructions')} is ${said.length} characters long, over the ${MAX_JUDGE_INSTRUCTIONS}-character maximum`,
|
|
1161
1183
|
});
|
|
1162
1184
|
ok = false;
|
|
1163
1185
|
} else {
|
|
@@ -1184,7 +1206,7 @@ function readKeyBy(r: Reader, kind: 'rate_limit' | 'passive_sample', value: unkn
|
|
|
1184
1206
|
if (value === undefined) return undefined;
|
|
1185
1207
|
if (typeof value === 'string') return value;
|
|
1186
1208
|
r.problems.push({
|
|
1187
|
-
text: `behavior.${kind}.keyBy is ${shownValue(value)}, where a field name is needed, such as "channelId"`,
|
|
1209
|
+
text: `${r.named(`behavior.${kind}.keyBy`)} is ${shownValue(value)}, where a field name is needed, such as "channelId"`,
|
|
1188
1210
|
read: `so it does nothing, and every event the rule matches shares one ${kind === 'rate_limit' ? 'bucket' : 'count'}`,
|
|
1189
1211
|
});
|
|
1190
1212
|
return undefined;
|
|
@@ -1299,7 +1321,10 @@ function readMatch(r: Reader, value: unknown): GatePolicyMatch {
|
|
|
1299
1321
|
new RegExp(f.pattern);
|
|
1300
1322
|
match.filter = { type, pattern: f.pattern };
|
|
1301
1323
|
} catch (e) {
|
|
1302
|
-
r.problems.push({
|
|
1324
|
+
r.problems.push({
|
|
1325
|
+
text: `match.filter.pattern isn't a regular expression the gate can read (${quotedValue(errorText(e))}), so fix it, `
|
|
1326
|
+
+ 'or give match.filter.type "text" to look for the pattern as plain text',
|
|
1327
|
+
});
|
|
1303
1328
|
}
|
|
1304
1329
|
}
|
|
1305
1330
|
}
|
|
@@ -1341,18 +1366,17 @@ function readResets(r: Reader, value: unknown): string[] | undefined {
|
|
|
1341
1366
|
* always, defer, skip or a debounce it would be a no-op at best and a
|
|
1342
1367
|
* misleading promise at worst, so it's refused rather than ignored.
|
|
1343
1368
|
*/
|
|
1344
|
-
function readPassthrough(
|
|
1345
|
-
r: Reader, value: unknown, behavior: GateBehavior | undefined, kind: BehaviorKind | undefined,
|
|
1346
|
-
): boolean {
|
|
1369
|
+
function readPassthrough(r: Reader, value: unknown, kind: BehaviorKind | undefined): boolean {
|
|
1347
1370
|
if (value === undefined || value === false) return false;
|
|
1348
1371
|
if (value !== true) {
|
|
1349
1372
|
r.problems.push({ text: `passthrough is ${shownValue(value)}, where true or false is needed` });
|
|
1350
1373
|
return false;
|
|
1351
1374
|
}
|
|
1352
1375
|
if (kind === undefined || kind === 'rate_limit' || kind === 'passive_sample') return true;
|
|
1376
|
+
const given = kind === 'always' || kind === 'defer' || kind === 'skip' ? `behavior "${kind}"` : r.named(`behavior.${kind}`);
|
|
1353
1377
|
r.problems.push({
|
|
1354
|
-
text: `passthrough is only
|
|
1355
|
-
+
|
|
1378
|
+
text: `passthrough is true, and only a ${r.named('behavior.rate_limit')} or ${r.named('behavior.passive_sample')} rule passes on `
|
|
1379
|
+
+ `an event it matches without waking on it; with ${given}, leave passthrough out`,
|
|
1356
1380
|
});
|
|
1357
1381
|
return false;
|
|
1358
1382
|
}
|
|
@@ -1518,7 +1542,7 @@ function readConfig(
|
|
|
1518
1542
|
}
|
|
1519
1543
|
|
|
1520
1544
|
const rules = policies.map((rule, i) => {
|
|
1521
|
-
const reading = readRule(rule, `The ${ordinal(i + 1)}
|
|
1545
|
+
const reading = readRule(rule, `The ${ordinal(i + 1)} rule`, file);
|
|
1522
1546
|
return { raw: rule, reading, outcome: ruleOutcome(reading) };
|
|
1523
1547
|
});
|
|
1524
1548
|
if (defaultBehavior !== 'always') {
|
|
@@ -1604,7 +1628,7 @@ function readLastRecipe(path: string): { recipe: WrittenConfig | null; problem?:
|
|
|
1604
1628
|
if (read.refusedWhole !== undefined) return { recipe: null, problem: "it isn't read as the starting rules are" };
|
|
1605
1629
|
const rules = read.rules.map((rule) => rule.raw);
|
|
1606
1630
|
const twice = duplicateName(rules);
|
|
1607
|
-
if (twice !== undefined) return { recipe: null, problem: `it names the rule
|
|
1631
|
+
if (twice !== undefined) return { recipe: null, problem: `it names the rule ${quotedValue(twice)} twice` };
|
|
1608
1632
|
return { recipe: { rules, default: isPlainObject(raw) ? raw.default : undefined } };
|
|
1609
1633
|
}
|
|
1610
1634
|
|
|
@@ -1614,7 +1638,7 @@ function readLastRecipe(path: string): { recipe: WrittenConfig | null; problem?:
|
|
|
1614
1638
|
* wrote as read agrees with the same rule written more briefly.
|
|
1615
1639
|
*/
|
|
1616
1640
|
function ruleMeaning(rule: unknown): string {
|
|
1617
|
-
const reading = readRule(rule, 'A
|
|
1641
|
+
const reading = readRule(rule, 'A rule');
|
|
1618
1642
|
return reading.policy ? `read ${canonical(reading.policy)}` : `written ${canonical(rule)}`;
|
|
1619
1643
|
}
|
|
1620
1644
|
|
|
@@ -1725,7 +1749,7 @@ function notUsedClause(write: Extract<StartWrite, { kind: 'start-without' }>, st
|
|
|
1725
1749
|
*/
|
|
1726
1750
|
function outOfForce(write: Extract<StartWrite, { kind: 'start-without' }>): { said: string; fix: string; nextStart: boolean } | null {
|
|
1727
1751
|
const nameless = write.nameless ?? 0;
|
|
1728
|
-
const left = [...(write.leftOut ?? []).map((name) =>
|
|
1752
|
+
const left = [...(write.leftOut ?? []).map((name) => quotedValue(name)), ...(nameless === 0 ? [] : [nameless === 1 ? 'one with no name' : `${nameless} with no name`])];
|
|
1729
1753
|
const held = write.heldOut ?? [];
|
|
1730
1754
|
const said: string[] = [];
|
|
1731
1755
|
const fix: string[] = [];
|
|
@@ -1954,14 +1978,14 @@ export function findShadowedPolicies(policies: GatePolicy[], eventTypesFrom?: Ev
|
|
|
1954
1978
|
* three it is (the rule never applies; it misses some of its events; it
|
|
1955
1979
|
* may not be meant for those events at all) and what to do. */
|
|
1956
1980
|
export function formatShadowWarning(w: ShadowWarning): string {
|
|
1957
|
-
const later =
|
|
1958
|
-
const earlier =
|
|
1959
|
-
const move = `move ${later} above ${earlier} (wake_add_rule with the whole rule and insertBefore:
|
|
1981
|
+
const later = quotedValue(w.later);
|
|
1982
|
+
const earlier = quotedValue(w.earlier);
|
|
1983
|
+
const move = `move ${later} above ${earlier} (wake_add_rule with the whole rule and insertBefore: ${quotedValue(w.earlier)})`;
|
|
1960
1984
|
if (w.whole) {
|
|
1961
1985
|
return `${later} never applies: ${earlier} comes first and matches every event ${later} would. `
|
|
1962
1986
|
+ `If ${later} should apply, ${move}, or narrow ${earlier}.`;
|
|
1963
1987
|
}
|
|
1964
|
-
const types = w.eventTypes === 'all' ? 'all' : w.eventTypes.join(', ');
|
|
1988
|
+
const types = w.eventTypes === 'all' ? 'all' : w.eventTypes.map((type) => quotedValue(type).slice(1, -1)).join(', ');
|
|
1965
1989
|
if (w.known) {
|
|
1966
1990
|
return `${earlier} comes first and takes ${later}'s ${types} events, so ${later} applies only to its others. `
|
|
1967
1991
|
+ `If it should apply to those too, ${move}.`;
|
|
@@ -1994,20 +2018,23 @@ function afterChange(name: string, callback: () => void): void {
|
|
|
1994
2018
|
}
|
|
1995
2019
|
|
|
1996
2020
|
/** The privileged-users file's error when some entries of its list aren't user ids. */
|
|
1997
|
-
function skippedPrivilegedIds(skipped: unknown[], loaded: number): string {
|
|
2021
|
+
function skippedPrivilegedIds(skipped: unknown[], loaded: number, path: string): string {
|
|
1998
2022
|
const one = skipped.length === 1;
|
|
1999
2023
|
const shown = skipped.slice(0, 5).map((x) => {
|
|
2000
2024
|
if (typeof x === 'number' && Number.isInteger(x)) {
|
|
2001
2025
|
return `${x} (a number this large can't be read exactly: write the id as a string, in quotes)`;
|
|
2002
2026
|
}
|
|
2003
|
-
const json =
|
|
2027
|
+
const json = quotedValue(x);
|
|
2004
2028
|
return json.length > 40 ? `${safeSlice(json, 0, 39)}…` : json;
|
|
2005
2029
|
});
|
|
2006
2030
|
const more = skipped.length > shown.length ? `, and ${skipped.length - shown.length} more` : '';
|
|
2007
|
-
const rest = loaded === 0
|
|
2008
|
-
|
|
2031
|
+
const rest = loaded === 0
|
|
2032
|
+
? 'no id loaded, so no one is privileged'
|
|
2033
|
+
: `the ${loaded === 1 ? 'one id that did load is' : `${loaded} ids that did load are`} privileged`;
|
|
2034
|
+
return `${skipped.length} ${one ? 'entry' : 'entries'} in the privileged-users file, at ${path}, ${one ? "isn't a user id" : "aren't user ids"}, `
|
|
2009
2035
|
+ `so ${one ? 'it was' : 'they were'} skipped: ${shown.join(', ')}${more}. A user id is a non-empty string, or a safe integer `
|
|
2010
|
-
+ `(a whole number within 2^53 - 1 of zero); ${rest}
|
|
2036
|
+
+ `(a whole number within 2^53 - 1 of zero); ${rest}. Once ${one ? 'that entry is a user id' : 'those entries are user ids'}, the gate `
|
|
2037
|
+
+ 'reads the file again at its next check for changes.';
|
|
2011
2038
|
}
|
|
2012
2039
|
|
|
2013
2040
|
// ---------------------------------------------------------------------------
|
|
@@ -2039,6 +2066,7 @@ export class EventGate {
|
|
|
2039
2066
|
/** Whether gate.json's error is one that keeps what it holds out of force
|
|
2040
2067
|
* (configUnreadable), so gate_status says what's in force instead. */
|
|
2041
2068
|
private configUnused = false;
|
|
2069
|
+
private configFix: string | null = null;
|
|
2042
2070
|
private privilegedError: string | null = null;
|
|
2043
2071
|
/** What the start did with the host's starting rules that it couldn't use
|
|
2044
2072
|
* or add (describeStart), kept apart from gate.json's own error, until the
|
|
@@ -2119,8 +2147,8 @@ export class EventGate {
|
|
|
2119
2147
|
private sleepSuppressed = 0;
|
|
2120
2148
|
private sleepTimer: ReturnType<typeof setTimeout> | null = null;
|
|
2121
2149
|
private sleepAgent: string | undefined;
|
|
2122
|
-
// From dispose()
|
|
2123
|
-
//
|
|
2150
|
+
// From dispose() on, no sleep or self-wake timer is armed, though a turn a
|
|
2151
|
+
// failed stop() left running can still ask for one.
|
|
2124
2152
|
private disposed = false;
|
|
2125
2153
|
|
|
2126
2154
|
// Self-wake timers per agent (skip_reply's wake_in_seconds). Unlike sleep,
|
|
@@ -2366,7 +2394,7 @@ export class EventGate {
|
|
|
2366
2394
|
}
|
|
2367
2395
|
const twice = duplicateName(starting.rules.map((rule) => rule.raw));
|
|
2368
2396
|
if (twice !== undefined) {
|
|
2369
|
-
this.startMergeNote = `At start, the recipe names the rule
|
|
2397
|
+
this.startMergeNote = `At start, the recipe names the rule ${quotedValue(twice)} twice, so gate.json was seeded with it as given and it `
|
|
2370
2398
|
+ "wasn't recorded as the recipe last applied: a later start merges the recipe by name, once gate.json and the recipe "
|
|
2371
2399
|
+ 'name each rule once.';
|
|
2372
2400
|
} else {
|
|
@@ -2560,7 +2588,13 @@ export class EventGate {
|
|
|
2560
2588
|
try {
|
|
2561
2589
|
raw = readFileSync(this.configPath, 'utf-8');
|
|
2562
2590
|
} catch (err) {
|
|
2563
|
-
if (!isMissing(err))
|
|
2591
|
+
if (!isMissing(err)) {
|
|
2592
|
+
const why = quotedValue(errorText(err));
|
|
2593
|
+
return this.configUnreadable(
|
|
2594
|
+
why, `gate.json, at ${this.configPath}, can't be read: ${why}`,
|
|
2595
|
+
'Whoever runs this host can make it a file the gate can read, and the gate reads it again at its next check for changes.',
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2564
2598
|
// No file is no problem with it: the rules in force stay.
|
|
2565
2599
|
this.configError = null;
|
|
2566
2600
|
this.configUnused = false;
|
|
@@ -2579,12 +2613,25 @@ export class EventGate {
|
|
|
2579
2613
|
try {
|
|
2580
2614
|
parsed = parseGateJson(raw);
|
|
2581
2615
|
} catch (err) {
|
|
2582
|
-
|
|
2616
|
+
const why = quotedValue(errorText(err));
|
|
2617
|
+
return {
|
|
2618
|
+
...this.configUnreadable(
|
|
2619
|
+
why, `gate.json, at ${this.configPath}, isn't JSON: ${why}`,
|
|
2620
|
+
"Once it's JSON, the gate reads it again at its next check for changes.",
|
|
2621
|
+
),
|
|
2622
|
+
notJson: true,
|
|
2623
|
+
};
|
|
2583
2624
|
}
|
|
2584
2625
|
const read = readConfig(parsed, 'gate.json');
|
|
2585
2626
|
// JSON the gate doesn't use is still the operator's file, edited as written.
|
|
2586
2627
|
if ('refused' in read) {
|
|
2587
|
-
|
|
2628
|
+
const problem = read.refused.startsWith('gate.json ')
|
|
2629
|
+
? `gate.json, at ${this.configPath},${read.refused.slice('gate.json'.length)}`
|
|
2630
|
+
: read.refused;
|
|
2631
|
+
return {
|
|
2632
|
+
...this.configUnreadable(read.refused, problem, 'Once gate.json is mended, the gate reads it again at its next check for changes.'),
|
|
2633
|
+
state: 'refused', listed: read.listed, silenced: read.silenced,
|
|
2634
|
+
};
|
|
2588
2635
|
}
|
|
2589
2636
|
// A field the gate doesn't read does nothing, and a rule is read past a
|
|
2590
2637
|
// problem or left out, so the file loads, and each is named.
|
|
@@ -2598,20 +2645,20 @@ export class EventGate {
|
|
|
2598
2645
|
* gate.json's current error, replacing the last one, and traced and logged
|
|
2599
2646
|
* when it's new, so an edit that doesn't load is in the log once.
|
|
2600
2647
|
*/
|
|
2601
|
-
private configUnreadable(
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
console.error(`[gate] ${this.configPath} couldn't be used (${msg}), so what it holds isn't in force`);
|
|
2648
|
+
private configUnreadable(why: string, problem: string, fix: string | null = null): { state: 'unreadable'; error: string } {
|
|
2649
|
+
if (problem !== this.configError) {
|
|
2650
|
+
console.error(`[gate] ${this.configPath} couldn't be used (${why}), so what it holds isn't in force${fix ? `. ${fix}` : ''}`);
|
|
2605
2651
|
this.emitTrace({
|
|
2606
2652
|
type: 'gate:config-error',
|
|
2607
|
-
error:
|
|
2653
|
+
error: why,
|
|
2608
2654
|
configPath: this.configPath,
|
|
2609
2655
|
timestamp: Date.now(),
|
|
2610
2656
|
});
|
|
2611
2657
|
}
|
|
2612
|
-
this.configError =
|
|
2658
|
+
this.configError = problem;
|
|
2659
|
+
this.configFix = fix;
|
|
2613
2660
|
this.configUnused = true;
|
|
2614
|
-
return { state: 'unreadable', error:
|
|
2661
|
+
return { state: 'unreadable', error: why };
|
|
2615
2662
|
}
|
|
2616
2663
|
|
|
2617
2664
|
/**
|
|
@@ -2620,7 +2667,8 @@ export class EventGate {
|
|
|
2620
2667
|
*/
|
|
2621
2668
|
private configErrorShown(): string | null {
|
|
2622
2669
|
if (this.configError === null || !this.configUnused) return this.configError;
|
|
2623
|
-
return `${sentence(this.configError)} So what gate.json holds isn't in force, and the gate runs ${this.rulesRun()}
|
|
2670
|
+
return `${sentence(this.configError)} So what gate.json holds isn't in force, and the gate runs ${this.rulesRun()}.`
|
|
2671
|
+
+ (this.configFix ? ` ${this.configFix}` : '');
|
|
2624
2672
|
}
|
|
2625
2673
|
|
|
2626
2674
|
/** What the gate runs, by where its rules came from, for a text that says so: "the host's starting rules". */
|
|
@@ -2665,13 +2713,13 @@ export class EventGate {
|
|
|
2665
2713
|
| { top: Record<string, unknown>; rules: unknown[]; remedy?: string }
|
|
2666
2714
|
| { refused: string } {
|
|
2667
2715
|
const read = this.readConfigFile();
|
|
2668
|
-
if (read.state === 'unreadable') return { refused: unusableRefusal(read, this.rulesRun()) };
|
|
2716
|
+
if (read.state === 'unreadable') return { refused: unusableRefusal(read, this.rulesRun(), this.configPath) };
|
|
2669
2717
|
if (read.state === 'loaded' || read.state === 'refused') {
|
|
2670
|
-
if ('noList' in read.listed) return { refused: noListRefusal(read.listed.noList, this.rulesRun()) };
|
|
2718
|
+
if ('noList' in read.listed) return { refused: noListRefusal(read.listed.noList, this.rulesRun(), this.configPath) };
|
|
2671
2719
|
// Rules not gate.json's in force while it holds the text in force: what a
|
|
2672
2720
|
// start ran in place of one with no rules of its own, until it changes.
|
|
2673
2721
|
if (read.state === 'loaded' && read.text === this.configText && this.configSource !== 'file') {
|
|
2674
|
-
return { refused: notUsedRefusal(this.rulesRun()) };
|
|
2722
|
+
return { refused: notUsedRefusal(this.rulesRun(), this.configPath) };
|
|
2675
2723
|
}
|
|
2676
2724
|
const edit = { top: read.listed.top, rules: [...read.listed.rules] };
|
|
2677
2725
|
if (read.state === 'loaded') return edit;
|
|
@@ -2688,9 +2736,9 @@ export class EventGate {
|
|
|
2688
2736
|
return {
|
|
2689
2737
|
...edit,
|
|
2690
2738
|
remedy: sharing === 1
|
|
2691
|
-
? `${removes ? 'Replacing or removing' : 'Replacing'}
|
|
2739
|
+
? `${removes ? 'Replacing or removing' : 'Replacing'} ${quotedValue(only)} by name would bring gate.json into force.`
|
|
2692
2740
|
: sharing > 1
|
|
2693
|
-
? `gate.json has ${sharing} rules named
|
|
2741
|
+
? `gate.json has ${sharing} rules named ${quotedValue(only)}, and a change by that name replaces the first of them`
|
|
2694
2742
|
+ `${removes ? ' or removes them all' : ''}.`
|
|
2695
2743
|
: 'No change through the gate can bring it into force, so gate.json has to be fixed by an edit of its own; '
|
|
2696
2744
|
+ 'until then, a change is refused the same way.',
|
|
@@ -2864,9 +2912,18 @@ export class EventGate {
|
|
|
2864
2912
|
options?: {
|
|
2865
2913
|
position?: 'append' | 'prepend' | { before: string } | { after: string };
|
|
2866
2914
|
onIgnoredFields?: (notes: string[]) => void;
|
|
2915
|
+
/** The caller's own name for a field a refusal names, keyed by gate.json's name for it: one of
|
|
2916
|
+
* `behavior.debounce`, `behavior.rate_limit`, `behavior.passive_sample` and `behavior.judge`, or
|
|
2917
|
+
* `position.before` and `position.after` for a placement. A key names the fields under it too:
|
|
2918
|
+
* `behavior.rate_limit` gives `behavior.rate_limit.tokens` its name, and a longer key wins.
|
|
2919
|
+
* Other fields keep gate.json's names.
|
|
2920
|
+
*
|
|
2921
|
+
* @internal For `wake_add_rule`'s refusals; not a host API. */
|
|
2922
|
+
fieldNames?: Readonly<Record<string, string>>;
|
|
2867
2923
|
},
|
|
2868
2924
|
): GatePolicy {
|
|
2869
|
-
const
|
|
2925
|
+
const named = namedBy(options?.fieldNames);
|
|
2926
|
+
const reading = readRule(rawPolicy, 'A rule', 'gate.json', named);
|
|
2870
2927
|
if (reading.problems.length > 0 || !reading.policy) throw new Error(refusalOf(reading));
|
|
2871
2928
|
const policy = reading.policy;
|
|
2872
2929
|
const ignored = notesOf(reading);
|
|
@@ -2886,9 +2943,26 @@ export class EventGate {
|
|
|
2886
2943
|
const anchorName = 'before' in position ? position.before : position.after;
|
|
2887
2944
|
const anchorIdx = rules.findIndex((rule) => nameOf(rule) === anchorName);
|
|
2888
2945
|
if (anchorIdx < 0) {
|
|
2946
|
+
const field = named('before' in position ? 'position.before' : 'position.after');
|
|
2947
|
+
const others = [...new Set(rules.map(nameOf).filter((name): name is string => name !== undefined))];
|
|
2948
|
+
if (idx >= 0 && anchorName === policy.name) {
|
|
2949
|
+
throw new Error(
|
|
2950
|
+
`${reading.where} wasn't saved: ${field} is ${quotedValue(anchorName)}, the rule being saved, and a rule can't be `
|
|
2951
|
+
+ 'placed beside itself. '
|
|
2952
|
+
+ (others.length > 0
|
|
2953
|
+
? `The other rules are ${quotedList(others)}. Give ${field} one of those to place the rule beside it, or leave it out`
|
|
2954
|
+
: `There are no other rules, so leave ${field} out`)
|
|
2955
|
+
+ ' to replace the rule where it is.'
|
|
2956
|
+
+ savedOnlyInForce(this.configPath, remedy),
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2889
2959
|
throw new Error(
|
|
2890
|
-
|
|
2891
|
-
+
|
|
2960
|
+
`${reading.where} wasn't saved: ${field} is ${quotedValue(anchorName)}, and no rule has that name. `
|
|
2961
|
+
+ (others.length > 0
|
|
2962
|
+
? `The other rules are ${quotedList(others)}. Give ${field} one of those to place the rule beside it, or leave it out`
|
|
2963
|
+
: `There are no other rules, so leave ${field} out`)
|
|
2964
|
+
+ ' to add the rule at the end, or, where a rule already has its name, to replace that rule where it is.'
|
|
2965
|
+
+ savedOnlyInForce(this.configPath, remedy),
|
|
2892
2966
|
);
|
|
2893
2967
|
}
|
|
2894
2968
|
rules.splice('before' in position ? anchorIdx : anchorIdx + 1, 0, policy);
|
|
@@ -2963,6 +3037,16 @@ export class EventGate {
|
|
|
2963
3037
|
return current.policies.map((p) => p.name);
|
|
2964
3038
|
}
|
|
2965
3039
|
|
|
3040
|
+
/** @internal For `wake_add_rule`'s and `wake_remove_rule`'s refusals; not a host API. */
|
|
3041
|
+
ruleNamesForChange(): { names: string[]; note: string } | { refused: string } {
|
|
3042
|
+
const edit = this.rulesForChange();
|
|
3043
|
+
if ('refused' in edit) return { refused: edit.refused };
|
|
3044
|
+
return {
|
|
3045
|
+
names: [...new Set(edit.rules.map(nameOf).filter((name): name is string => name !== undefined))],
|
|
3046
|
+
note: savedOnlyInForce(this.configPath, edit.remedy),
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
|
|
2966
3050
|
/**
|
|
2967
3051
|
* Persist gate.json and apply it in memory together: `top`, its own fields,
|
|
2968
3052
|
* with `rules` as its rule list. Only what loads is written: a change to a
|
|
@@ -2989,15 +3073,16 @@ export class EventGate {
|
|
|
2989
3073
|
const read = readConfig(JSON.parse(text), 'gate.json');
|
|
2990
3074
|
if ('refused' in read) {
|
|
2991
3075
|
throw new Error(
|
|
2992
|
-
`Nothing was saved: with this change, gate.json wouldn't be used. ${sentence(read.refused)}
|
|
3076
|
+
`Nothing was saved: with this change, gate.json, at ${this.configPath}, wouldn't be used. ${sentence(read.refused)}`
|
|
3077
|
+
+ (remedy ? ` ${remedy}` : ''),
|
|
2993
3078
|
);
|
|
2994
3079
|
}
|
|
2995
3080
|
// One the gate doesn't use, and that a change can mend, holds back what a
|
|
2996
3081
|
// rule it leaves out would wake on, so its default holds events back.
|
|
2997
3082
|
if (remedy !== undefined && read.config.policies.length === 0) {
|
|
2998
3083
|
throw new Error(
|
|
2999
|
-
|
|
3000
|
-
+ `event. Meanwhile the gate runs ${this.rulesRun()}. ${remedy}`,
|
|
3084
|
+
`Nothing was saved: with this change, gate.json, at ${this.configPath}, would have no rules the gate reads, and its default `
|
|
3085
|
+
+ `would hold back every event. Meanwhile the gate runs ${this.rulesRun()}. ${remedy}`,
|
|
3001
3086
|
);
|
|
3002
3087
|
}
|
|
3003
3088
|
try {
|
|
@@ -3679,7 +3764,7 @@ export class EventGate {
|
|
|
3679
3764
|
} catch (err) {
|
|
3680
3765
|
const one = events.length === 1;
|
|
3681
3766
|
console.error(
|
|
3682
|
-
`[gate]
|
|
3767
|
+
`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'} couldn't be delivered `
|
|
3683
3768
|
+ `(${errorText(err)}), so the batch is dropped`,
|
|
3684
3769
|
);
|
|
3685
3770
|
return;
|
|
@@ -3877,7 +3962,7 @@ export class EventGate {
|
|
|
3877
3962
|
).finally(() => {
|
|
3878
3963
|
if (state.timer) clearTimeout(state.timer);
|
|
3879
3964
|
}).catch((err: unknown) => {
|
|
3880
|
-
console.error(`[gate]
|
|
3965
|
+
console.error(`[gate] Rule ${quotedValue(policyName)}: the judge's verdict couldn't be applied (${errorText(err)})`);
|
|
3881
3966
|
});
|
|
3882
3967
|
}
|
|
3883
3968
|
|
|
@@ -3948,8 +4033,8 @@ export class EventGate {
|
|
|
3948
4033
|
const n = state.events.length;
|
|
3949
4034
|
const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
|
|
3950
4035
|
wakes.push(state.judge
|
|
3951
|
-
? { at: state.dueAt, certain: false, what: `rule
|
|
3952
|
-
: { at: state.dueAt, certain: true, what: `rule
|
|
4036
|
+
? { at: state.dueAt, certain: false, what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
|
|
4037
|
+
: { at: state.dueAt, certain: true, what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
|
|
3953
4038
|
}
|
|
3954
4039
|
return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
3955
4040
|
}
|
|
@@ -3999,7 +4084,7 @@ export class EventGate {
|
|
|
3999
4084
|
if (failed && (err as { providerAdmission?: unknown } | undefined)?.providerAdmission === 'deferred') {
|
|
4000
4085
|
const wakes = this.judgeOnError === 'wake';
|
|
4001
4086
|
console.error(
|
|
4002
|
-
`[gate]
|
|
4087
|
+
`[gate] Rule ${quotedValue(policyName)}: the reply judge made no call (${errorText(err)}), so its batch of ${state.events.length} `
|
|
4003
4088
|
+ `${state.events.length === 1 ? 'event' : 'events'} ${wakes ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`,
|
|
4004
4089
|
);
|
|
4005
4090
|
if (this.disposed) return;
|
|
@@ -4031,7 +4116,7 @@ export class EventGate {
|
|
|
4031
4116
|
const wake = outcome === 'reply' || (outcome === 'error' && this.judgeOnError === 'wake');
|
|
4032
4117
|
if (failed) {
|
|
4033
4118
|
console.error(
|
|
4034
|
-
`[gate]
|
|
4119
|
+
`[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${reason}), so its batch of ${state.events.length} `
|
|
4035
4120
|
+ `${state.events.length === 1 ? 'event' : 'events'} ${wake ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`,
|
|
4036
4121
|
);
|
|
4037
4122
|
}
|
|
@@ -4125,11 +4210,10 @@ export class EventGate {
|
|
|
4125
4210
|
|
|
4126
4211
|
/** A turn that continues another (a budget restart, a tool-result-guard
|
|
4127
4212
|
* retry) holds the entry its predecessor kept for it, and takes it again
|
|
4128
|
-
* where that entry ended in between
|
|
4129
|
-
*
|
|
4130
|
-
*
|
|
4131
|
-
*
|
|
4132
|
-
* still running. */
|
|
4213
|
+
* where that entry ended in between, as at a provider hold that kept the
|
|
4214
|
+
* turn from starting, whose end queues it again. Unlike
|
|
4215
|
+
* onInferenceStarted it leaves a pending self-wake alone: the logical
|
|
4216
|
+
* turn that armed it is the one still running. */
|
|
4133
4217
|
onInferenceResumed(agentName: string): void {
|
|
4134
4218
|
this.inferring.add(agentName);
|
|
4135
4219
|
if (this.changesJudgedContext(agentName)) this.stopJudging();
|
|
@@ -4254,7 +4338,7 @@ export class EventGate {
|
|
|
4254
4338
|
// Arm the self-wake: when the window elapses, clear sleep and request an
|
|
4255
4339
|
// inference so the agent resumes on its own (not dependent on a heartbeat
|
|
4256
4340
|
// tick or an incoming message). Cancelled by clearSleep()/`wake`, and by
|
|
4257
|
-
// dispose(), after which
|
|
4341
|
+
// dispose(), after which nothing arms it.
|
|
4258
4342
|
this.armSleepWake();
|
|
4259
4343
|
this.reloadPrivilegedIfChanged();
|
|
4260
4344
|
this.emitTrace({
|
|
@@ -4268,24 +4352,11 @@ export class EventGate {
|
|
|
4268
4352
|
return { until: this.sleepUntil };
|
|
4269
4353
|
}
|
|
4270
4354
|
|
|
4271
|
-
/**
|
|
4272
|
-
* Arm timers again for a lifecycle that starts after a stop(), and the
|
|
4273
|
-
* sleep's wake first: dispose() clears its timer and keeps the sleep. A
|
|
4274
|
-
* sleep still ahead wakes the agent at its end; one that ended while
|
|
4275
|
-
* stopped wakes it now, since that wake is what the agent asked for. With
|
|
4276
|
-
* no sleep, wakeFromSleep does nothing.
|
|
4277
|
-
*/
|
|
4278
|
-
resumeSleep(): void {
|
|
4279
|
-
this.disposed = false;
|
|
4280
|
-
if (this.sleepUntil > this.now()) this.armSleepWake();
|
|
4281
|
-
else this.wakeFromSleep('sleep-expired');
|
|
4282
|
-
}
|
|
4283
|
-
|
|
4284
4355
|
/**
|
|
4285
4356
|
* Arm the sleep's wake for its end by the gate's own clock. One timer holds
|
|
4286
4357
|
* at most MAX_TIMER_MS, about 24.8 days, so a longer sleep waits in steps,
|
|
4287
4358
|
* and wakeFromSleep reads the clock as each one fires. Nothing is armed
|
|
4288
|
-
*
|
|
4359
|
+
* once the gate is disposed.
|
|
4289
4360
|
*/
|
|
4290
4361
|
private armSleepWake(): void {
|
|
4291
4362
|
if (this.sleepTimer) { clearTimeout(this.sleepTimer); this.sleepTimer = null; }
|
|
@@ -4394,9 +4465,7 @@ export class EventGate {
|
|
|
4394
4465
|
* fires early (Node's can, by a millisecond), or a clock set back.
|
|
4395
4466
|
*/
|
|
4396
4467
|
private wakeFromSleep(reason: string): void {
|
|
4397
|
-
// The timer that called it has fired
|
|
4398
|
-
// already over, one may still be armed, and it goes, so no stale step of
|
|
4399
|
-
// it can fire later.
|
|
4468
|
+
// The timer that called it has fired.
|
|
4400
4469
|
if (this.sleepTimer) { clearTimeout(this.sleepTimer); this.sleepTimer = null; }
|
|
4401
4470
|
if (this.sleepUntil === 0) return;
|
|
4402
4471
|
if (this.sleepUntil > this.now()) {
|
|
@@ -4453,14 +4522,20 @@ export class EventGate {
|
|
|
4453
4522
|
* each a non-empty string or a safe integer (a whole number within 2^53 - 1 of zero). An absent file
|
|
4454
4523
|
* means no one is privileged. A file that can't be read, isn't JSON, or
|
|
4455
4524
|
* holds no list leaves the list that last loaded in this process in effect
|
|
4456
|
-
* (none, at startup), with its error in gate_status until
|
|
4457
|
-
*
|
|
4458
|
-
* the file's error, and the rest load. With `unlessUnchanged`, a file whose
|
|
4525
|
+
* (none, at startup), with its error in gate_status until a later read
|
|
4526
|
+
* loads it, finds it gone, or fails another way. In a list, an entry that isn't a user id is skipped, and named
|
|
4527
|
+
* in the file's error, and the rest load. With `unlessUnchanged`, a file whose
|
|
4459
4528
|
* text is the text last read isn't parsed again.
|
|
4460
4529
|
*/
|
|
4461
4530
|
private loadPrivileged(opts?: { unlessUnchanged?: boolean }): void {
|
|
4462
4531
|
if (!this.privilegedUsersPath) return;
|
|
4463
|
-
const
|
|
4532
|
+
const file = `The privileged-users file, at ${this.privilegedUsersPath},`;
|
|
4533
|
+
const held = this.privilegedUserIds.size;
|
|
4534
|
+
const meanwhile = held === 0
|
|
4535
|
+
? 'no one is privileged'
|
|
4536
|
+
: `${held === 1 ? 'the one id of the list that last loaded stays' : `the ${held} ids of the list that last loaded stay`} privileged`;
|
|
4537
|
+
const revoke = 'Replacing it with a file holding [], or removing it, makes no one privileged at the gate\'s next check for changes.';
|
|
4538
|
+
const failed = (problem: string, fix: string): string => `${file} ${problem}, so ${meanwhile}. ${fix} ${revoke}`;
|
|
4464
4539
|
let raw: string;
|
|
4465
4540
|
try {
|
|
4466
4541
|
raw = readFileSync(this.privilegedUsersPath, 'utf8');
|
|
@@ -4472,24 +4547,28 @@ export class EventGate {
|
|
|
4472
4547
|
this.privilegedUserIds = new Set();
|
|
4473
4548
|
this.privilegedError = null;
|
|
4474
4549
|
} else {
|
|
4475
|
-
this.privilegedError = failed(
|
|
4550
|
+
this.privilegedError = failed(
|
|
4551
|
+
`can't be read: ${quotedValue(errorText(err))}`,
|
|
4552
|
+
'Whoever runs this host can make it a file the gate can read, and the gate reads it again at its next check for changes.',
|
|
4553
|
+
);
|
|
4476
4554
|
}
|
|
4477
4555
|
return;
|
|
4478
4556
|
}
|
|
4479
4557
|
if (opts?.unlessUnchanged && raw === this.privilegedText) return;
|
|
4480
4558
|
this.privilegedText = raw;
|
|
4559
|
+
const fixed = 'Once it holds a list of user ids, the gate loads it at its next check for changes.';
|
|
4481
4560
|
let ids: unknown;
|
|
4482
4561
|
try {
|
|
4483
4562
|
const parsed: unknown = parseGateJson(raw);
|
|
4484
4563
|
ids = Array.isArray(parsed) ? parsed : (parsed as { userIds?: unknown } | null)?.userIds;
|
|
4485
4564
|
} catch (err) {
|
|
4486
|
-
this.privilegedError = failed(
|
|
4565
|
+
this.privilegedError = failed(`isn't JSON: ${quotedValue(errorText(err))}`, fixed);
|
|
4487
4566
|
return;
|
|
4488
4567
|
}
|
|
4489
4568
|
// A file that parses but holds no list hasn't loaded either: it used to
|
|
4490
4569
|
// mean no one is privileged, without a word.
|
|
4491
4570
|
if (!Array.isArray(ids)) {
|
|
4492
|
-
this.privilegedError = failed('
|
|
4571
|
+
this.privilegedError = failed('holds no list of user ids, where one is needed, such as ["123"] or {"userIds": ["123"]}', fixed);
|
|
4493
4572
|
return;
|
|
4494
4573
|
}
|
|
4495
4574
|
const loaded = new Set<string>();
|
|
@@ -4502,7 +4581,7 @@ export class EventGate {
|
|
|
4502
4581
|
else skipped.push(id);
|
|
4503
4582
|
}
|
|
4504
4583
|
this.privilegedUserIds = loaded;
|
|
4505
|
-
this.privilegedError = skipped.length > 0 ? skippedPrivilegedIds(skipped, loaded.size) : null;
|
|
4584
|
+
this.privilegedError = skipped.length > 0 ? skippedPrivilegedIds(skipped, loaded.size, this.privilegedUsersPath) : null;
|
|
4506
4585
|
}
|
|
4507
4586
|
|
|
4508
4587
|
/** At most once a second, the privileged-users file read again, and loaded when its text has changed. */
|
|
@@ -4660,9 +4739,9 @@ export class EventGate {
|
|
|
4660
4739
|
this.selfWakeDue.clear();
|
|
4661
4740
|
// The sleep stays, with its note and agent, and its timer goes: fired
|
|
4662
4741
|
// after a stop(), its wake reached a framework that had stopped, and the
|
|
4663
|
-
// timer held the process until the sleep's end.
|
|
4664
|
-
//
|
|
4665
|
-
//
|
|
4742
|
+
// timer held the process until the sleep's end. Nothing arms one after
|
|
4743
|
+
// this, nor a self-wake: a turn a failed stop() left running can still
|
|
4744
|
+
// call sleep or skip_reply.
|
|
4666
4745
|
if (this.sleepTimer) { clearTimeout(this.sleepTimer); this.sleepTimer = null; }
|
|
4667
4746
|
this.disposed = true;
|
|
4668
4747
|
this.rateLimitBuckets.clear();
|
|
@@ -4672,13 +4751,9 @@ export class EventGate {
|
|
|
4672
4751
|
this.inferenceBuffer = [];
|
|
4673
4752
|
// stop() disposes the gate once its streams and event loop have ended (a
|
|
4674
4753
|
// turn still compiling then is dropped at its next check, before it sends
|
|
4675
|
-
// anything)
|
|
4676
|
-
//
|
|
4677
|
-
//
|
|
4678
|
-
// agent's next turn ended. A turn that continues after the start() takes
|
|
4679
|
-
// its entry again (onInferenceResumed). A start() made during the stop()
|
|
4680
|
-
// is refused, so no turn of a new lifecycle can have begun before this
|
|
4681
|
-
// runs.
|
|
4754
|
+
// anything). An agent left here by a turn whose exit didn't end it (a
|
|
4755
|
+
// retry stop() cancelled, a successor stop() refused) would make every
|
|
4756
|
+
// debounced batch buffer until that agent's next turn ended.
|
|
4682
4757
|
this.inferring.clear();
|
|
4683
4758
|
}
|
|
4684
4759
|
}
|