@flowdot.ai/guardian-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +40 -0
- package/README.md +281 -0
- package/ROADMAP.md +109 -0
- package/dist/audit/attestor.d.ts +102 -0
- package/dist/audit/attestor.d.ts.map +1 -0
- package/dist/audit/attestor.js +103 -0
- package/dist/audit/attestor.js.map +1 -0
- package/dist/audit/chain.d.ts +30 -0
- package/dist/audit/chain.d.ts.map +1 -0
- package/dist/audit/chain.js +65 -0
- package/dist/audit/chain.js.map +1 -0
- package/dist/audit/correlation.d.ts +114 -0
- package/dist/audit/correlation.d.ts.map +1 -0
- package/dist/audit/correlation.js +259 -0
- package/dist/audit/correlation.js.map +1 -0
- package/dist/audit/index.d.ts +13 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +8 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/reader.d.ts +30 -0
- package/dist/audit/reader.d.ts.map +1 -0
- package/dist/audit/reader.js +85 -0
- package/dist/audit/reader.js.map +1 -0
- package/dist/audit/signature.d.ts +39 -0
- package/dist/audit/signature.d.ts.map +1 -0
- package/dist/audit/signature.js +73 -0
- package/dist/audit/signature.js.map +1 -0
- package/dist/audit/stats.d.ts +106 -0
- package/dist/audit/stats.d.ts.map +1 -0
- package/dist/audit/stats.js +196 -0
- package/dist/audit/stats.js.map +1 -0
- package/dist/audit/writer.d.ts +96 -0
- package/dist/audit/writer.d.ts.map +1 -0
- package/dist/audit/writer.js +263 -0
- package/dist/audit/writer.js.map +1 -0
- package/dist/cli/guardian-baseline.d.ts +42 -0
- package/dist/cli/guardian-baseline.d.ts.map +1 -0
- package/dist/cli/guardian-baseline.js +265 -0
- package/dist/cli/guardian-baseline.js.map +1 -0
- package/dist/cli/guardian-correlator.d.ts +47 -0
- package/dist/cli/guardian-correlator.d.ts.map +1 -0
- package/dist/cli/guardian-correlator.js +217 -0
- package/dist/cli/guardian-correlator.js.map +1 -0
- package/dist/cli/guardian-verify.d.ts +30 -0
- package/dist/cli/guardian-verify.d.ts.map +1 -0
- package/dist/cli/guardian-verify.js +149 -0
- package/dist/cli/guardian-verify.js.map +1 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +40 -0
- package/dist/errors.js.map +1 -0
- package/dist/estop/heartbeat.d.ts +94 -0
- package/dist/estop/heartbeat.d.ts.map +1 -0
- package/dist/estop/heartbeat.js +135 -0
- package/dist/estop/heartbeat.js.map +1 -0
- package/dist/estop/hub.d.ts +76 -0
- package/dist/estop/hub.d.ts.map +1 -0
- package/dist/estop/hub.js +167 -0
- package/dist/estop/hub.js.map +1 -0
- package/dist/estop/index.d.ts +12 -0
- package/dist/estop/index.d.ts.map +1 -0
- package/dist/estop/index.js +6 -0
- package/dist/estop/index.js.map +1 -0
- package/dist/estop/local.d.ts +31 -0
- package/dist/estop/local.d.ts.map +1 -0
- package/dist/estop/local.js +101 -0
- package/dist/estop/local.js.map +1 -0
- package/dist/estop/middleware.d.ts +36 -0
- package/dist/estop/middleware.d.ts.map +1 -0
- package/dist/estop/middleware.js +40 -0
- package/dist/estop/middleware.js.map +1 -0
- package/dist/estop/poller.d.ts +36 -0
- package/dist/estop/poller.d.ts.map +1 -0
- package/dist/estop/poller.js +85 -0
- package/dist/estop/poller.js.map +1 -0
- package/dist/estop/types.d.ts +31 -0
- package/dist/estop/types.d.ts.map +1 -0
- package/dist/estop/types.js +5 -0
- package/dist/estop/types.js.map +1 -0
- package/dist/gate/async-callback.d.ts +27 -0
- package/dist/gate/async-callback.d.ts.map +1 -0
- package/dist/gate/async-callback.js +79 -0
- package/dist/gate/async-callback.js.map +1 -0
- package/dist/gate/cli.d.ts +29 -0
- package/dist/gate/cli.d.ts.map +1 -0
- package/dist/gate/cli.js +83 -0
- package/dist/gate/cli.js.map +1 -0
- package/dist/gate/data-channel.d.ts +41 -0
- package/dist/gate/data-channel.d.ts.map +1 -0
- package/dist/gate/data-channel.js +132 -0
- package/dist/gate/data-channel.js.map +1 -0
- package/dist/gate/index.d.ts +13 -0
- package/dist/gate/index.d.ts.map +1 -0
- package/dist/gate/index.js +7 -0
- package/dist/gate/index.js.map +1 -0
- package/dist/gate/options.d.ts +90 -0
- package/dist/gate/options.d.ts.map +1 -0
- package/dist/gate/options.js +131 -0
- package/dist/gate/options.js.map +1 -0
- package/dist/gate/programmatic.d.ts +9 -0
- package/dist/gate/programmatic.d.ts.map +1 -0
- package/dist/gate/programmatic.js +20 -0
- package/dist/gate/programmatic.js.map +1 -0
- package/dist/gate/two-key.d.ts +90 -0
- package/dist/gate/two-key.d.ts.map +1 -0
- package/dist/gate/two-key.js +78 -0
- package/dist/gate/two-key.js.map +1 -0
- package/dist/gate/types.d.ts +25 -0
- package/dist/gate/types.d.ts.map +1 -0
- package/dist/gate/types.js +5 -0
- package/dist/gate/types.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/notify/console.d.ts +13 -0
- package/dist/notify/console.d.ts.map +1 -0
- package/dist/notify/console.js +27 -0
- package/dist/notify/console.js.map +1 -0
- package/dist/notify/index.d.ts +8 -0
- package/dist/notify/index.d.ts.map +1 -0
- package/dist/notify/index.js +4 -0
- package/dist/notify/index.js.map +1 -0
- package/dist/notify/multi.d.ts +14 -0
- package/dist/notify/multi.d.ts.map +1 -0
- package/dist/notify/multi.js +22 -0
- package/dist/notify/multi.js.map +1 -0
- package/dist/notify/types.d.ts +21 -0
- package/dist/notify/types.d.ts.map +1 -0
- package/dist/notify/types.js +5 -0
- package/dist/notify/types.js.map +1 -0
- package/dist/notify/webhook.d.ts +21 -0
- package/dist/notify/webhook.d.ts.map +1 -0
- package/dist/notify/webhook.js +37 -0
- package/dist/notify/webhook.js.map +1 -0
- package/dist/policy/attribution.d.ts +61 -0
- package/dist/policy/attribution.d.ts.map +1 -0
- package/dist/policy/attribution.js +116 -0
- package/dist/policy/attribution.js.map +1 -0
- package/dist/policy/evaluator.d.ts +36 -0
- package/dist/policy/evaluator.d.ts.map +1 -0
- package/dist/policy/evaluator.js +211 -0
- package/dist/policy/evaluator.js.map +1 -0
- package/dist/policy/index.d.ts +11 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +7 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/integrity.d.ts +17 -0
- package/dist/policy/integrity.d.ts.map +1 -0
- package/dist/policy/integrity.js +31 -0
- package/dist/policy/integrity.js.map +1 -0
- package/dist/policy/loader.d.ts +9 -0
- package/dist/policy/loader.d.ts.map +1 -0
- package/dist/policy/loader.js +124 -0
- package/dist/policy/loader.js.map +1 -0
- package/dist/policy/site-key.d.ts +22 -0
- package/dist/policy/site-key.d.ts.map +1 -0
- package/dist/policy/site-key.js +48 -0
- package/dist/policy/site-key.js.map +1 -0
- package/dist/policy/store.d.ts +45 -0
- package/dist/policy/store.d.ts.map +1 -0
- package/dist/policy/store.js +223 -0
- package/dist/policy/store.js.map +1 -0
- package/dist/policy/types.d.ts +72 -0
- package/dist/policy/types.d.ts.map +1 -0
- package/dist/policy/types.js +5 -0
- package/dist/policy/types.js.map +1 -0
- package/dist/runtime/capability.d.ts +125 -0
- package/dist/runtime/capability.d.ts.map +1 -0
- package/dist/runtime/capability.js +121 -0
- package/dist/runtime/capability.js.map +1 -0
- package/dist/runtime/honeytokens.d.ts +104 -0
- package/dist/runtime/honeytokens.d.ts.map +1 -0
- package/dist/runtime/honeytokens.js +115 -0
- package/dist/runtime/honeytokens.js.map +1 -0
- package/dist/runtime/multi-rate-limiter.d.ts +90 -0
- package/dist/runtime/multi-rate-limiter.d.ts.map +1 -0
- package/dist/runtime/multi-rate-limiter.js +133 -0
- package/dist/runtime/multi-rate-limiter.js.map +1 -0
- package/dist/runtime/runtime.d.ts +94 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.js +276 -0
- package/dist/runtime/runtime.js.map +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy types. SPEC §3.
|
|
3
|
+
*/
|
|
4
|
+
/** Persistence scope. */
|
|
5
|
+
export type PolicyScope = 'once' | 'session' | 'forever' | 'banned';
|
|
6
|
+
/** Effective decision. */
|
|
7
|
+
export type PolicyDecision = 'allow' | 'deny' | 'prompt';
|
|
8
|
+
/**
|
|
9
|
+
* Conditional clause: a rule only matches when these model attributes
|
|
10
|
+
* match the request. Strings are matched with the same shell-style globs
|
|
11
|
+
* (`*`, `?`, `[seq]`) used for tool names — except `attribution_path`, which
|
|
12
|
+
* uses flat-glob (where `*` also matches `/`).
|
|
13
|
+
*
|
|
14
|
+
* v0.6+. SPEC §3 open question (resolved).
|
|
15
|
+
*
|
|
16
|
+
* Multiple fields are conjunctive: the rule matches only when every
|
|
17
|
+
* provided clause matches.
|
|
18
|
+
*/
|
|
19
|
+
export interface PolicyWhen {
|
|
20
|
+
/** Match on `model.provider` (e.g., 'anthropic', 'openai', 'ollama'). */
|
|
21
|
+
'model.provider'?: string;
|
|
22
|
+
/** Match on `model.id` (e.g., 'claude-*-4.5*', 'gpt-5*', 'llama3*'). */
|
|
23
|
+
'model.id'?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Match on the rendered attribution path
|
|
26
|
+
* `surface/aggregator/provider/id`. Flat-glob: `*` matches across `/`.
|
|
27
|
+
* Examples: `'*\/RedPill/*\/*'`, `'FlowDot/*\/Anthropic/claude-*-4.*'`,
|
|
28
|
+
* `'*claude-opus*'`. See `policy/attribution.ts`.
|
|
29
|
+
*/
|
|
30
|
+
attribution_path?: string;
|
|
31
|
+
}
|
|
32
|
+
/** A single rule in the policy. */
|
|
33
|
+
export interface PolicyRule {
|
|
34
|
+
/**
|
|
35
|
+
* Tool pattern. Supports shell-style globs ( * ? [seq] ). May also be
|
|
36
|
+
* `category:<name>` to target a category (SPEC §3.7).
|
|
37
|
+
*/
|
|
38
|
+
tool: string;
|
|
39
|
+
/** Persistence scope of this rule. */
|
|
40
|
+
scope: PolicyScope;
|
|
41
|
+
/**
|
|
42
|
+
* Decision when the rule matches. Omit for `scope: banned` (implies `deny`).
|
|
43
|
+
* For `scope: once|session|forever`, defaults to `allow`.
|
|
44
|
+
*/
|
|
45
|
+
decision?: Exclude<PolicyDecision, 'prompt'>;
|
|
46
|
+
/** Optional conditional clause. The rule only matches when all entries hold. */
|
|
47
|
+
when?: PolicyWhen;
|
|
48
|
+
/** Optional human note. Ignored by the evaluator. */
|
|
49
|
+
notes?: string;
|
|
50
|
+
}
|
|
51
|
+
/** Loaded policy: defaults + rules. */
|
|
52
|
+
export interface Policy {
|
|
53
|
+
/** Spec version of the policy file (e.g., '0.2'). */
|
|
54
|
+
version: string;
|
|
55
|
+
/** Agent id this policy applies to. */
|
|
56
|
+
agent_id: string;
|
|
57
|
+
/** Default behavior when no rule matches. */
|
|
58
|
+
defaults: {
|
|
59
|
+
scope: 'prompt' | PolicyScope;
|
|
60
|
+
decision?: Exclude<PolicyDecision, 'prompt'>;
|
|
61
|
+
};
|
|
62
|
+
/** Rules in declaration order. */
|
|
63
|
+
rules: PolicyRule[];
|
|
64
|
+
}
|
|
65
|
+
/** Result of evaluating a tool name against a policy. */
|
|
66
|
+
export interface PolicyEvaluation {
|
|
67
|
+
decision: PolicyDecision;
|
|
68
|
+
matchedRule: PolicyRule | undefined;
|
|
69
|
+
matchedAt: 'exact' | 'wildcard' | 'category' | 'default';
|
|
70
|
+
scope: PolicyScope | 'prompt';
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/policy/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,yBAAyB;AACzB,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEpE,0BAA0B;AAC1B,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAU;IACzB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,mCAAmC;AACnC,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,KAAK,EAAE,WAAW,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IAC7C,gFAAgF;IAChF,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,uCAAuC;AACvC,MAAM,WAAW,MAAM;IACrB,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,QAAQ,EAAE;QACR,KAAK,EAAE,QAAQ,GAAG,WAAW,CAAC;QAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;KAC9C,CAAC;IACF,kCAAkC;IAClC,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,cAAc,CAAC;IACzB,WAAW,EAAE,UAAU,GAAG,SAAS,CAAC;IACpC,SAAS,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;IACzD,KAAK,EAAE,WAAW,GAAG,QAAQ,CAAC;CAC/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/policy/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability tags + sliding-window combination detection. SPEC §4 extension
|
|
3
|
+
* (v0.3.0+).
|
|
4
|
+
*
|
|
5
|
+
* Each tool is tagged with one or more capability classes (the canonical
|
|
6
|
+
* set is documented below but consumers can register additional strings).
|
|
7
|
+
* The supervisor maintains a per-session sliding window of recent tool
|
|
8
|
+
* calls + their capability sets. Rules describe "suspicious combinations"
|
|
9
|
+
* — a list of classes that, if all observed within `window_ms`, fire an
|
|
10
|
+
* event.
|
|
11
|
+
*
|
|
12
|
+
* v0.8 ships YELLOW-LINE ONLY. A combination match writes
|
|
13
|
+
* `x_capability_yellow` to the audit log; dispatch is NOT blocked. The
|
|
14
|
+
* point is to collect calibration data. Red-line auto-stop ships in v0.10
|
|
15
|
+
* after the v0.8 corpus shows zero false positives at proposed thresholds.
|
|
16
|
+
*
|
|
17
|
+
* Pure mechanism: tag lookup + set membership over a fixed-window event
|
|
18
|
+
* list. Constant memory per session (oldest events drop as window slides).
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Canonical capability classes. The string-union form lets consumers extend
|
|
22
|
+
* with their own strings (e.g., `'database-write'` for a SQL-tool surface)
|
|
23
|
+
* without forking the library.
|
|
24
|
+
*
|
|
25
|
+
* Class meanings (canonical):
|
|
26
|
+
* - `read` — pure read of agent-accessible data
|
|
27
|
+
* - `write` — local write of agent-accessible data
|
|
28
|
+
* - `delete` — destruction of agent-accessible data
|
|
29
|
+
* - `execute` — run a subprocess / arbitrary code
|
|
30
|
+
* - `network-egress` — outbound network call
|
|
31
|
+
* - `network-ingress` — accept inbound network call
|
|
32
|
+
* - `credential` — read or write credentials
|
|
33
|
+
* - `system-path` — touch OS-level paths (`/etc`, `~/.ssh`, etc.)
|
|
34
|
+
* - `bulk` — operation over many items (>N, configurable per-tool)
|
|
35
|
+
* - `unknown` — fallback for untagged tools
|
|
36
|
+
*/
|
|
37
|
+
export type CapabilityClass = 'read' | 'write' | 'delete' | 'execute' | 'network-egress' | 'network-ingress' | 'credential' | 'system-path' | 'bulk' | 'unknown' | (string & {});
|
|
38
|
+
/**
|
|
39
|
+
* One rule in the capability-rule set. A rule matches when every class in
|
|
40
|
+
* `combination` has been observed within the last `window_ms`.
|
|
41
|
+
*
|
|
42
|
+
* `level` is `'yellow'` in v0.8 (audit-only). `'red'` (auto-E-stop) lands
|
|
43
|
+
* in v0.10 once Yellow data justifies thresholds.
|
|
44
|
+
*/
|
|
45
|
+
export interface CapabilityRule {
|
|
46
|
+
/** Stable id used in audit records. ASCII, short. */
|
|
47
|
+
id: string;
|
|
48
|
+
/** Free-form human note. Ignored by the matcher. */
|
|
49
|
+
description?: string;
|
|
50
|
+
/** Classes that must all appear within the window. Order is irrelevant. */
|
|
51
|
+
combination: CapabilityClass[];
|
|
52
|
+
/** Time window in milliseconds. */
|
|
53
|
+
window_ms: number;
|
|
54
|
+
/** v0.8: always 'yellow'. v0.10 adds 'red'. */
|
|
55
|
+
level: 'yellow' | 'red';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Event recorded into the sliding window. The supervisor synthesizes these
|
|
59
|
+
* after every dispatched tool call; they hold only what the matcher needs.
|
|
60
|
+
*/
|
|
61
|
+
export interface CapabilityEvent {
|
|
62
|
+
/** Wall-clock ms (monotonic preferred when consumer supplies a `now`). */
|
|
63
|
+
ts: number;
|
|
64
|
+
/** Capability classes of THIS specific tool call. */
|
|
65
|
+
classes: CapabilityClass[];
|
|
66
|
+
/** Audit event_id, copied so a fired rule can cite the contributing events. */
|
|
67
|
+
eventId: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Result of recording one event. Lists every rule that fired on this event.
|
|
71
|
+
* Most events fire nothing; this array is typically empty.
|
|
72
|
+
*/
|
|
73
|
+
export interface CapabilityMatch {
|
|
74
|
+
ruleId: string;
|
|
75
|
+
level: 'yellow' | 'red';
|
|
76
|
+
combination: CapabilityClass[];
|
|
77
|
+
window_ms: number;
|
|
78
|
+
/** event_ids of the events that contributed to the match, in chronological order. */
|
|
79
|
+
contributingEventIds: string[];
|
|
80
|
+
}
|
|
81
|
+
export interface CapabilityWindowOptions {
|
|
82
|
+
rules: CapabilityRule[];
|
|
83
|
+
/** Override for time source (testing). Defaults to Date.now. */
|
|
84
|
+
now?: () => number;
|
|
85
|
+
/**
|
|
86
|
+
* Maximum events kept in the window irrespective of age. Defensive cap so
|
|
87
|
+
* a runaway agent can't grow the buffer without bound. Default 10000.
|
|
88
|
+
*/
|
|
89
|
+
maxEvents?: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Per-session sliding-window state. One instance per supervisor.
|
|
93
|
+
*
|
|
94
|
+
* Memory: O(max window_ms × call rate) in the worst case, capped at
|
|
95
|
+
* `maxEvents`. Events older than the longest rule window are dropped on
|
|
96
|
+
* every `record` call.
|
|
97
|
+
*/
|
|
98
|
+
export declare class CapabilityWindow {
|
|
99
|
+
private readonly rules;
|
|
100
|
+
private readonly now;
|
|
101
|
+
private readonly maxEvents;
|
|
102
|
+
private readonly maxWindowMs;
|
|
103
|
+
private events;
|
|
104
|
+
constructor(options: CapabilityWindowOptions);
|
|
105
|
+
/**
|
|
106
|
+
* Record a tool dispatch + evaluate all rules against the current window.
|
|
107
|
+
*
|
|
108
|
+
* Returns the list of rules that fired (often empty). Caller is
|
|
109
|
+
* responsible for converting fires into audit rows + (in v0.10) E-stop
|
|
110
|
+
* presses.
|
|
111
|
+
*/
|
|
112
|
+
record(classes: CapabilityClass[], eventId: string): CapabilityMatch[];
|
|
113
|
+
/** Snapshot of currently-buffered events (for tests + introspection). */
|
|
114
|
+
buffered(): readonly CapabilityEvent[];
|
|
115
|
+
/**
|
|
116
|
+
* Test whether `rule` is satisfied by the current window ending at `now`.
|
|
117
|
+
* Returns the contributing event ids on match, null otherwise.
|
|
118
|
+
*
|
|
119
|
+
* Algorithm: for each class in `combination`, find the most-recent event
|
|
120
|
+
* (within the window) that includes that class. If every class found a
|
|
121
|
+
* contributor, the rule matches.
|
|
122
|
+
*/
|
|
123
|
+
private evaluateRule;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=capability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../../src/runtime/capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,eAAe,GACvB,MAAM,GACN,OAAO,GACP,QAAQ,GACR,SAAS,GACT,gBAAgB,GAChB,iBAAiB,GACjB,YAAY,GACZ,aAAa,GACb,MAAM,GACN,SAAS,GACT,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,KAAK,EAAE,QAAQ,GAAG,KAAK,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,EAAE,EAAE,MAAM,CAAC;IACX,qDAAqD;IACrD,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,QAAQ,GAAG,KAAK,CAAC;IACxB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,oBAAoB,EAAE,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,MAAM,CAAyB;gBAE3B,OAAO,EAAE,uBAAuB;IAmB5C;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,EAAE;IA0BtE,yEAAyE;IACzE,QAAQ,IAAI,SAAS,eAAe,EAAE;IAItC;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;CA0BrB"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability tags + sliding-window combination detection. SPEC §4 extension
|
|
3
|
+
* (v0.3.0+).
|
|
4
|
+
*
|
|
5
|
+
* Each tool is tagged with one or more capability classes (the canonical
|
|
6
|
+
* set is documented below but consumers can register additional strings).
|
|
7
|
+
* The supervisor maintains a per-session sliding window of recent tool
|
|
8
|
+
* calls + their capability sets. Rules describe "suspicious combinations"
|
|
9
|
+
* — a list of classes that, if all observed within `window_ms`, fire an
|
|
10
|
+
* event.
|
|
11
|
+
*
|
|
12
|
+
* v0.8 ships YELLOW-LINE ONLY. A combination match writes
|
|
13
|
+
* `x_capability_yellow` to the audit log; dispatch is NOT blocked. The
|
|
14
|
+
* point is to collect calibration data. Red-line auto-stop ships in v0.10
|
|
15
|
+
* after the v0.8 corpus shows zero false positives at proposed thresholds.
|
|
16
|
+
*
|
|
17
|
+
* Pure mechanism: tag lookup + set membership over a fixed-window event
|
|
18
|
+
* list. Constant memory per session (oldest events drop as window slides).
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Per-session sliding-window state. One instance per supervisor.
|
|
22
|
+
*
|
|
23
|
+
* Memory: O(max window_ms × call rate) in the worst case, capped at
|
|
24
|
+
* `maxEvents`. Events older than the longest rule window are dropped on
|
|
25
|
+
* every `record` call.
|
|
26
|
+
*/
|
|
27
|
+
export class CapabilityWindow {
|
|
28
|
+
rules;
|
|
29
|
+
now;
|
|
30
|
+
maxEvents;
|
|
31
|
+
maxWindowMs;
|
|
32
|
+
events = [];
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.rules = options.rules;
|
|
35
|
+
this.now = options.now ?? Date.now;
|
|
36
|
+
this.maxEvents = options.maxEvents ?? 10_000;
|
|
37
|
+
this.maxWindowMs = this.rules.reduce((m, r) => Math.max(m, r.window_ms), 0);
|
|
38
|
+
// Validate rule shapes once.
|
|
39
|
+
for (const r of this.rules) {
|
|
40
|
+
if (r.combination.length === 0) {
|
|
41
|
+
throw new Error(`CapabilityRule ${JSON.stringify(r.id)} has empty combination`);
|
|
42
|
+
}
|
|
43
|
+
if (r.window_ms <= 0) {
|
|
44
|
+
throw new Error(`CapabilityRule ${JSON.stringify(r.id)} has non-positive window_ms`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Record a tool dispatch + evaluate all rules against the current window.
|
|
50
|
+
*
|
|
51
|
+
* Returns the list of rules that fired (often empty). Caller is
|
|
52
|
+
* responsible for converting fires into audit rows + (in v0.10) E-stop
|
|
53
|
+
* presses.
|
|
54
|
+
*/
|
|
55
|
+
record(classes, eventId) {
|
|
56
|
+
const ts = this.now();
|
|
57
|
+
const event = { ts, classes, eventId };
|
|
58
|
+
this.events.push(event);
|
|
59
|
+
// Age-based prune.
|
|
60
|
+
if (this.maxWindowMs > 0) {
|
|
61
|
+
const cutoff = ts - this.maxWindowMs;
|
|
62
|
+
while (this.events.length > 0 && this.events[0].ts < cutoff) {
|
|
63
|
+
this.events.shift();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Hard cap.
|
|
67
|
+
while (this.events.length > this.maxEvents) {
|
|
68
|
+
this.events.shift();
|
|
69
|
+
}
|
|
70
|
+
// Evaluate each rule against the current window.
|
|
71
|
+
const matches = [];
|
|
72
|
+
for (const rule of this.rules) {
|
|
73
|
+
const m = this.evaluateRule(rule, ts);
|
|
74
|
+
if (m)
|
|
75
|
+
matches.push(m);
|
|
76
|
+
}
|
|
77
|
+
return matches;
|
|
78
|
+
}
|
|
79
|
+
/** Snapshot of currently-buffered events (for tests + introspection). */
|
|
80
|
+
buffered() {
|
|
81
|
+
return this.events;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Test whether `rule` is satisfied by the current window ending at `now`.
|
|
85
|
+
* Returns the contributing event ids on match, null otherwise.
|
|
86
|
+
*
|
|
87
|
+
* Algorithm: for each class in `combination`, find the most-recent event
|
|
88
|
+
* (within the window) that includes that class. If every class found a
|
|
89
|
+
* contributor, the rule matches.
|
|
90
|
+
*/
|
|
91
|
+
evaluateRule(rule, now) {
|
|
92
|
+
const cutoff = now - rule.window_ms;
|
|
93
|
+
const required = new Set(rule.combination);
|
|
94
|
+
const contributors = new Map();
|
|
95
|
+
for (let i = this.events.length - 1; i >= 0; i--) {
|
|
96
|
+
const ev = this.events[i];
|
|
97
|
+
if (ev.ts < cutoff)
|
|
98
|
+
break;
|
|
99
|
+
for (const cls of ev.classes) {
|
|
100
|
+
if (required.has(cls) && !contributors.has(cls)) {
|
|
101
|
+
contributors.set(cls, ev);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (contributors.size === required.size)
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (contributors.size < required.size)
|
|
108
|
+
return null;
|
|
109
|
+
// Order contributing events chronologically + dedupe (a single
|
|
110
|
+
// multi-class event may satisfy several required classes).
|
|
111
|
+
const ids = Array.from(new Set(Array.from(contributors.values()).sort((a, b) => a.ts - b.ts).map((e) => e.eventId)));
|
|
112
|
+
return {
|
|
113
|
+
ruleId: rule.id,
|
|
114
|
+
level: rule.level,
|
|
115
|
+
combination: rule.combination,
|
|
116
|
+
window_ms: rule.window_ms,
|
|
117
|
+
contributingEventIds: ids,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=capability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.js","sourceRoot":"","sources":["../../src/runtime/capability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honeytoken detection. SPEC §11 (v0.3.0+).
|
|
3
|
+
*
|
|
4
|
+
* The library exposes two zero-false-positive-by-construction defenses:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Value match.** Consumer registers a set of decoy strings or regexes
|
|
7
|
+
* that should NEVER legitimately appear in a tool call's arguments
|
|
8
|
+
* (e.g., a fake `AKIA...` key stashed in a never-read env var, a
|
|
9
|
+
* phantom file path `~/.flowdot/keys/recovery.pem` that exists but
|
|
10
|
+
* contains only a marker). The matcher walks the canonical-JSON
|
|
11
|
+
* representation of every tool call's args; any hit means an agent has
|
|
12
|
+
* been probing.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Phantom tool.** Consumer registers a tool name that is documented
|
|
15
|
+
* internally as "do not call" — never advertised to legitimate agents.
|
|
16
|
+
* Any dispatch to that tool name → immediate fire.
|
|
17
|
+
*
|
|
18
|
+
* Both modes are deterministic predicates over input. Zero false positives
|
|
19
|
+
* by construction: if a legitimate workflow ever references a honeytoken,
|
|
20
|
+
* the operator either misconfigured the set or the workflow actually does
|
|
21
|
+
* touch sensitive data and the supervisor's catch is correct.
|
|
22
|
+
*
|
|
23
|
+
* The library SHIPS NO DEFAULT TOKENS. Anti-pattern: a library shipping
|
|
24
|
+
* fake-looking AWS keys gets picked up by secret scanners + creates support
|
|
25
|
+
* load. Consumers register their own tokens via `HoneytokenSet`.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* A single decoy. Exactly one of `value` or `pattern` must be set.
|
|
29
|
+
*
|
|
30
|
+
* - `value` matches substring (case-sensitive). Use for fixed marker strings.
|
|
31
|
+
* - `pattern` matches against the canonical-JSON of args via regex. Use for
|
|
32
|
+
* shape-based tokens (`/AKIA[0-9A-Z]{16}/`, etc.).
|
|
33
|
+
*/
|
|
34
|
+
export interface Honeytoken {
|
|
35
|
+
/** Stable id. Recorded in audit + estop reason. ASCII, short. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Free-form description for ops review. Ignored by the matcher. */
|
|
38
|
+
description?: string;
|
|
39
|
+
/** Literal substring to match. Mutually exclusive with `pattern`. */
|
|
40
|
+
value?: string;
|
|
41
|
+
/** Regex to test against canonical-JSON of args. Mutually exclusive with `value`. */
|
|
42
|
+
pattern?: RegExp;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A collection of honeytokens for one supervisor instance. Order does not
|
|
46
|
+
* affect detection; the matcher returns the first hit.
|
|
47
|
+
*
|
|
48
|
+
* Ids must be unique within the set.
|
|
49
|
+
*/
|
|
50
|
+
export interface HoneytokenSet {
|
|
51
|
+
/** Stable identifier for the set itself. */
|
|
52
|
+
id: string;
|
|
53
|
+
/** Optional description. */
|
|
54
|
+
description?: string;
|
|
55
|
+
/** Value/pattern tokens scanned against `tool.args`. */
|
|
56
|
+
tokens: Honeytoken[];
|
|
57
|
+
/**
|
|
58
|
+
* Phantom tool names. Any dispatch to one of these names triggers
|
|
59
|
+
* immediately, regardless of args.
|
|
60
|
+
*/
|
|
61
|
+
phantomTools?: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Result of a single match attempt.
|
|
65
|
+
*/
|
|
66
|
+
export type HoneytokenHit = {
|
|
67
|
+
kind: 'value_in_args';
|
|
68
|
+
tokenId: string;
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'phantom_tool';
|
|
71
|
+
toolName: string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Build a honeytoken set with id-uniqueness + xor-validation enforced.
|
|
75
|
+
*
|
|
76
|
+
* Throws on:
|
|
77
|
+
* - empty `tokens` AND empty `phantomTools` (the set would never fire)
|
|
78
|
+
* - duplicate token ids
|
|
79
|
+
* - duplicate phantom tool names
|
|
80
|
+
* - a token with neither `value` nor `pattern`
|
|
81
|
+
* - a token with BOTH `value` and `pattern`
|
|
82
|
+
*/
|
|
83
|
+
export declare function defineHoneytokenSet(id: string, tokens: Honeytoken[], phantomTools?: string[], description?: string): HoneytokenSet;
|
|
84
|
+
/**
|
|
85
|
+
* Test whether a tool name is a phantom tool in the set. Returns the hit
|
|
86
|
+
* descriptor on match, `null` otherwise.
|
|
87
|
+
*/
|
|
88
|
+
export declare function matchPhantomTool(set: HoneytokenSet, toolName: string): HoneytokenHit | null;
|
|
89
|
+
/**
|
|
90
|
+
* Test whether any honeytoken value or pattern appears in the canonical-JSON
|
|
91
|
+
* representation of `args`. Returns the FIRST hit. Order of `tokens` in the
|
|
92
|
+
* set determines which hit wins when multiple match.
|
|
93
|
+
*
|
|
94
|
+
* `args` is canonicalized to JSON before scanning so the matcher catches
|
|
95
|
+
* tokens regardless of nesting depth, key/value placement, or array index.
|
|
96
|
+
*/
|
|
97
|
+
export declare function matchHoneytokenInArgs(set: HoneytokenSet, args: unknown): HoneytokenHit | null;
|
|
98
|
+
/**
|
|
99
|
+
* Compose phantom-tool + value-in-args checks. Phantom-tool match wins over
|
|
100
|
+
* value-in-args when both would fire (phantom-tool is the stronger signal:
|
|
101
|
+
* a literal call to a forbidden name).
|
|
102
|
+
*/
|
|
103
|
+
export declare function checkHoneytoken(set: HoneytokenSet, toolName: string, args: unknown): HoneytokenHit | null;
|
|
104
|
+
//# sourceMappingURL=honeytokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"honeytokens.d.ts","sourceRoot":"","sources":["../../src/runtime/honeytokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,EAAE,EAAE,MAAM,CAAC;IACX,4BAA4B;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,UAAU,EAAE,EACpB,YAAY,GAAE,MAAM,EAAO,EAC3B,WAAW,CAAC,EAAE,MAAM,GACnB,aAAa,CAiCf;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAM3F;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,OAAO,GACZ,aAAa,GAAG,IAAI,CAetB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,aAAa,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,GACZ,aAAa,GAAG,IAAI,CAEtB"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honeytoken detection. SPEC §11 (v0.3.0+).
|
|
3
|
+
*
|
|
4
|
+
* The library exposes two zero-false-positive-by-construction defenses:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Value match.** Consumer registers a set of decoy strings or regexes
|
|
7
|
+
* that should NEVER legitimately appear in a tool call's arguments
|
|
8
|
+
* (e.g., a fake `AKIA...` key stashed in a never-read env var, a
|
|
9
|
+
* phantom file path `~/.flowdot/keys/recovery.pem` that exists but
|
|
10
|
+
* contains only a marker). The matcher walks the canonical-JSON
|
|
11
|
+
* representation of every tool call's args; any hit means an agent has
|
|
12
|
+
* been probing.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Phantom tool.** Consumer registers a tool name that is documented
|
|
15
|
+
* internally as "do not call" — never advertised to legitimate agents.
|
|
16
|
+
* Any dispatch to that tool name → immediate fire.
|
|
17
|
+
*
|
|
18
|
+
* Both modes are deterministic predicates over input. Zero false positives
|
|
19
|
+
* by construction: if a legitimate workflow ever references a honeytoken,
|
|
20
|
+
* the operator either misconfigured the set or the workflow actually does
|
|
21
|
+
* touch sensitive data and the supervisor's catch is correct.
|
|
22
|
+
*
|
|
23
|
+
* The library SHIPS NO DEFAULT TOKENS. Anti-pattern: a library shipping
|
|
24
|
+
* fake-looking AWS keys gets picked up by secret scanners + creates support
|
|
25
|
+
* load. Consumers register their own tokens via `HoneytokenSet`.
|
|
26
|
+
*/
|
|
27
|
+
import { canonicalJsonStringify } from '../audit/chain.js';
|
|
28
|
+
/**
|
|
29
|
+
* Build a honeytoken set with id-uniqueness + xor-validation enforced.
|
|
30
|
+
*
|
|
31
|
+
* Throws on:
|
|
32
|
+
* - empty `tokens` AND empty `phantomTools` (the set would never fire)
|
|
33
|
+
* - duplicate token ids
|
|
34
|
+
* - duplicate phantom tool names
|
|
35
|
+
* - a token with neither `value` nor `pattern`
|
|
36
|
+
* - a token with BOTH `value` and `pattern`
|
|
37
|
+
*/
|
|
38
|
+
export function defineHoneytokenSet(id, tokens, phantomTools = [], description) {
|
|
39
|
+
if (tokens.length === 0 && phantomTools.length === 0) {
|
|
40
|
+
throw new Error('defineHoneytokenSet: provide at least one token or phantom tool');
|
|
41
|
+
}
|
|
42
|
+
const tokenIds = new Set();
|
|
43
|
+
for (const t of tokens) {
|
|
44
|
+
if (tokenIds.has(t.id)) {
|
|
45
|
+
throw new Error(`defineHoneytokenSet: duplicate token id ${JSON.stringify(t.id)}`);
|
|
46
|
+
}
|
|
47
|
+
tokenIds.add(t.id);
|
|
48
|
+
const hasValue = t.value !== undefined;
|
|
49
|
+
const hasPattern = t.pattern !== undefined;
|
|
50
|
+
if (!hasValue && !hasPattern) {
|
|
51
|
+
throw new Error(`defineHoneytokenSet: token ${JSON.stringify(t.id)} must set either value or pattern`);
|
|
52
|
+
}
|
|
53
|
+
if (hasValue && hasPattern) {
|
|
54
|
+
throw new Error(`defineHoneytokenSet: token ${JSON.stringify(t.id)} sets both value and pattern; choose one`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const phantomSet = new Set();
|
|
58
|
+
for (const name of phantomTools) {
|
|
59
|
+
if (phantomSet.has(name)) {
|
|
60
|
+
throw new Error(`defineHoneytokenSet: duplicate phantom tool ${JSON.stringify(name)}`);
|
|
61
|
+
}
|
|
62
|
+
phantomSet.add(name);
|
|
63
|
+
}
|
|
64
|
+
const out = { id, tokens, phantomTools };
|
|
65
|
+
if (description !== undefined)
|
|
66
|
+
out.description = description;
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Test whether a tool name is a phantom tool in the set. Returns the hit
|
|
71
|
+
* descriptor on match, `null` otherwise.
|
|
72
|
+
*/
|
|
73
|
+
export function matchPhantomTool(set, toolName) {
|
|
74
|
+
if (!set.phantomTools)
|
|
75
|
+
return null;
|
|
76
|
+
if (set.phantomTools.includes(toolName)) {
|
|
77
|
+
return { kind: 'phantom_tool', toolName };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Test whether any honeytoken value or pattern appears in the canonical-JSON
|
|
83
|
+
* representation of `args`. Returns the FIRST hit. Order of `tokens` in the
|
|
84
|
+
* set determines which hit wins when multiple match.
|
|
85
|
+
*
|
|
86
|
+
* `args` is canonicalized to JSON before scanning so the matcher catches
|
|
87
|
+
* tokens regardless of nesting depth, key/value placement, or array index.
|
|
88
|
+
*/
|
|
89
|
+
export function matchHoneytokenInArgs(set, args) {
|
|
90
|
+
if (set.tokens.length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
const json = canonicalJsonStringify(args);
|
|
93
|
+
for (const t of set.tokens) {
|
|
94
|
+
if (t.value !== undefined) {
|
|
95
|
+
if (json.includes(t.value)) {
|
|
96
|
+
return { kind: 'value_in_args', tokenId: t.id };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (t.pattern !== undefined) {
|
|
100
|
+
if (t.pattern.test(json)) {
|
|
101
|
+
return { kind: 'value_in_args', tokenId: t.id };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compose phantom-tool + value-in-args checks. Phantom-tool match wins over
|
|
109
|
+
* value-in-args when both would fire (phantom-tool is the stronger signal:
|
|
110
|
+
* a literal call to a forbidden name).
|
|
111
|
+
*/
|
|
112
|
+
export function checkHoneytoken(set, toolName, args) {
|
|
113
|
+
return matchPhantomTool(set, toolName) ?? matchHoneytokenInArgs(set, args);
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=honeytokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"honeytokens.js","sourceRoot":"","sources":["../../src/runtime/honeytokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-capability token-bucket rate limiter. SPEC §5 extension (v0.3.0+).
|
|
3
|
+
*
|
|
4
|
+
* v0.7 of the surface glues used a single global bucket. v0.8 splits the
|
|
5
|
+
* bucket per {@link CapabilityClass}: read-heavy work doesn't get blocked
|
|
6
|
+
* by a writes burst, and an exfil pattern (lots of credential reads +
|
|
7
|
+
* network-egress writes) hits the narrow buckets long before the global
|
|
8
|
+
* rate.
|
|
9
|
+
*
|
|
10
|
+
* A normal-workload session sees zero impact at the conservative defaults
|
|
11
|
+
* documented below. The buckets only bite on bursts that match the exfil
|
|
12
|
+
* shape.
|
|
13
|
+
*
|
|
14
|
+
* Pure mechanism: N token buckets, one per class. Same arithmetic, more
|
|
15
|
+
* dimensions.
|
|
16
|
+
*/
|
|
17
|
+
import type { CapabilityClass } from './capability.js';
|
|
18
|
+
/**
|
|
19
|
+
* Bucket configuration for one capability class.
|
|
20
|
+
*/
|
|
21
|
+
export interface BucketConfig {
|
|
22
|
+
/** Sustained calls per second. */
|
|
23
|
+
maxCallsPerSecond: number;
|
|
24
|
+
/** Burst capacity. Default = maxCallsPerSecond. */
|
|
25
|
+
bucketCapacity?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface MultiRateLimiterOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Per-class buckets. Classes not present in this map get the default
|
|
30
|
+
* fallback bucket. Pass `unknown` explicitly if you want untagged tools
|
|
31
|
+
* to share a specific bucket (vs. the default).
|
|
32
|
+
*/
|
|
33
|
+
buckets: Partial<Record<CapabilityClass, BucketConfig>>;
|
|
34
|
+
/**
|
|
35
|
+
* Fallback for classes not in `buckets`. When omitted, classes without
|
|
36
|
+
* an explicit config are NOT rate-limited (every call allowed).
|
|
37
|
+
*/
|
|
38
|
+
defaultBucket?: BucketConfig;
|
|
39
|
+
/** Time source (testing). */
|
|
40
|
+
now?: () => number;
|
|
41
|
+
}
|
|
42
|
+
export interface ConsumeAllowed {
|
|
43
|
+
allowed: true;
|
|
44
|
+
}
|
|
45
|
+
export interface ConsumeDenied {
|
|
46
|
+
allowed: false;
|
|
47
|
+
/** Which class's bucket denied first. */
|
|
48
|
+
class: CapabilityClass;
|
|
49
|
+
/** Estimated ms until the denied bucket has a token again. */
|
|
50
|
+
retryAfterMs: number;
|
|
51
|
+
}
|
|
52
|
+
export type ConsumeResult = ConsumeAllowed | ConsumeDenied;
|
|
53
|
+
/**
|
|
54
|
+
* Library-recommended defaults. Tuned to never trip normal CLI workloads
|
|
55
|
+
* (read/write at human-edit speed, occasional outbound calls) while
|
|
56
|
+
* catching exfil-shaped bursts in the seconds-window.
|
|
57
|
+
*/
|
|
58
|
+
export declare const DEFAULT_BUCKETS: Partial<Record<CapabilityClass, BucketConfig>>;
|
|
59
|
+
/**
|
|
60
|
+
* Per-capability rate limiter. One {@link Bucket} per class; multi-class
|
|
61
|
+
* tools consume from every relevant bucket atomically (first denial wins;
|
|
62
|
+
* earlier-class buckets already consumed are NOT refunded).
|
|
63
|
+
*
|
|
64
|
+
* "First denial wins, no refund" matches the safety-conservative
|
|
65
|
+
* interpretation: a multi-class tool that's blocked on its rarest
|
|
66
|
+
* capability is fully blocked. The slightly-tighter accounting on
|
|
67
|
+
* already-consumed buckets is acceptable because it errs on the side of
|
|
68
|
+
* slowing the caller (not letting more through).
|
|
69
|
+
*/
|
|
70
|
+
export declare class MultiRateLimiter {
|
|
71
|
+
private readonly buckets;
|
|
72
|
+
private readonly defaultBucket;
|
|
73
|
+
private readonly now;
|
|
74
|
+
private readonly configMap;
|
|
75
|
+
private readonly defaultConfig;
|
|
76
|
+
constructor(options: MultiRateLimiterOptions);
|
|
77
|
+
/**
|
|
78
|
+
* Attempt to consume one token from every relevant bucket. If ANY bucket
|
|
79
|
+
* is empty, return the FIRST class to deny (in iteration order of
|
|
80
|
+
* `classes`). Tokens already consumed from earlier classes in this call
|
|
81
|
+
* are not refunded — see class JSDoc.
|
|
82
|
+
*
|
|
83
|
+
* `unknown` (or any class not in `buckets` and no `defaultBucket`)
|
|
84
|
+
* passes through allowed.
|
|
85
|
+
*/
|
|
86
|
+
tryConsume(classes: readonly CapabilityClass[]): ConsumeResult;
|
|
87
|
+
/** Current token count per class (tests + introspection). */
|
|
88
|
+
snapshot(): Record<string, number>;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=multi-rate-limiter.d.ts.map
|