@claude-flow/plugin-agent-federation 1.0.0-alpha.10

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.
Files changed (90) hide show
  1. package/README.md +45 -0
  2. package/dist/application/federation-breaker-service.d.ts +182 -0
  3. package/dist/application/federation-breaker-service.d.ts.map +1 -0
  4. package/dist/application/federation-breaker-service.js +251 -0
  5. package/dist/application/federation-breaker-service.js.map +1 -0
  6. package/dist/application/federation-coordinator.d.ts +166 -0
  7. package/dist/application/federation-coordinator.d.ts.map +1 -0
  8. package/dist/application/federation-coordinator.js +412 -0
  9. package/dist/application/federation-coordinator.js.map +1 -0
  10. package/dist/application/inbound-dispatcher.d.ts +59 -0
  11. package/dist/application/inbound-dispatcher.d.ts.map +1 -0
  12. package/dist/application/inbound-dispatcher.js +96 -0
  13. package/dist/application/inbound-dispatcher.js.map +1 -0
  14. package/dist/application/policy-engine.d.ts +41 -0
  15. package/dist/application/policy-engine.d.ts.map +1 -0
  16. package/dist/application/policy-engine.js +184 -0
  17. package/dist/application/policy-engine.js.map +1 -0
  18. package/dist/application/spend-reporter.d.ts +150 -0
  19. package/dist/application/spend-reporter.d.ts.map +1 -0
  20. package/dist/application/spend-reporter.js +128 -0
  21. package/dist/application/spend-reporter.js.map +1 -0
  22. package/dist/application/trust-evaluator.d.ts +42 -0
  23. package/dist/application/trust-evaluator.d.ts.map +1 -0
  24. package/dist/application/trust-evaluator.js +132 -0
  25. package/dist/application/trust-evaluator.js.map +1 -0
  26. package/dist/bin.d.ts +13 -0
  27. package/dist/bin.d.ts.map +1 -0
  28. package/dist/bin.js +189 -0
  29. package/dist/bin.js.map +1 -0
  30. package/dist/cli-commands.d.ts +7 -0
  31. package/dist/cli-commands.d.ts.map +1 -0
  32. package/dist/cli-commands.js +198 -0
  33. package/dist/cli-commands.js.map +1 -0
  34. package/dist/domain/entities/federation-envelope.d.ts +45 -0
  35. package/dist/domain/entities/federation-envelope.d.ts.map +1 -0
  36. package/dist/domain/entities/federation-envelope.js +71 -0
  37. package/dist/domain/entities/federation-envelope.js.map +1 -0
  38. package/dist/domain/entities/federation-node.d.ts +104 -0
  39. package/dist/domain/entities/federation-node.d.ts.map +1 -0
  40. package/dist/domain/entities/federation-node.js +152 -0
  41. package/dist/domain/entities/federation-node.js.map +1 -0
  42. package/dist/domain/entities/federation-session.d.ts +55 -0
  43. package/dist/domain/entities/federation-session.d.ts.map +1 -0
  44. package/dist/domain/entities/federation-session.js +105 -0
  45. package/dist/domain/entities/federation-session.js.map +1 -0
  46. package/dist/domain/entities/trust-level.d.ts +17 -0
  47. package/dist/domain/entities/trust-level.d.ts.map +1 -0
  48. package/dist/domain/entities/trust-level.js +35 -0
  49. package/dist/domain/entities/trust-level.js.map +1 -0
  50. package/dist/domain/services/audit-service.d.ts +69 -0
  51. package/dist/domain/services/audit-service.d.ts.map +1 -0
  52. package/dist/domain/services/audit-service.js +149 -0
  53. package/dist/domain/services/audit-service.js.map +1 -0
  54. package/dist/domain/services/discovery-service.d.ts +51 -0
  55. package/dist/domain/services/discovery-service.d.ts.map +1 -0
  56. package/dist/domain/services/discovery-service.js +140 -0
  57. package/dist/domain/services/discovery-service.js.map +1 -0
  58. package/dist/domain/services/handshake-service.d.ts +48 -0
  59. package/dist/domain/services/handshake-service.d.ts.map +1 -0
  60. package/dist/domain/services/handshake-service.js +94 -0
  61. package/dist/domain/services/handshake-service.js.map +1 -0
  62. package/dist/domain/services/pii-pipeline-service.d.ts +65 -0
  63. package/dist/domain/services/pii-pipeline-service.d.ts.map +1 -0
  64. package/dist/domain/services/pii-pipeline-service.js +208 -0
  65. package/dist/domain/services/pii-pipeline-service.js.map +1 -0
  66. package/dist/domain/services/routing-service.d.ts +51 -0
  67. package/dist/domain/services/routing-service.d.ts.map +1 -0
  68. package/dist/domain/services/routing-service.js +147 -0
  69. package/dist/domain/services/routing-service.js.map +1 -0
  70. package/dist/domain/value-objects/federation-budget.d.ts +89 -0
  71. package/dist/domain/value-objects/federation-budget.d.ts.map +1 -0
  72. package/dist/domain/value-objects/federation-budget.js +165 -0
  73. package/dist/domain/value-objects/federation-budget.js.map +1 -0
  74. package/dist/domain/value-objects/federation-node-state.d.ts +92 -0
  75. package/dist/domain/value-objects/federation-node-state.d.ts.map +1 -0
  76. package/dist/domain/value-objects/federation-node-state.js +100 -0
  77. package/dist/domain/value-objects/federation-node-state.js.map +1 -0
  78. package/dist/index.d.ts +17 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +17 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/mcp-tools.d.ts +8 -0
  83. package/dist/mcp-tools.d.ts.map +1 -0
  84. package/dist/mcp-tools.js +343 -0
  85. package/dist/mcp-tools.js.map +1 -0
  86. package/dist/plugin.d.ts +18 -0
  87. package/dist/plugin.d.ts.map +1 -0
  88. package/dist/plugin.js +376 -0
  89. package/dist/plugin.js.map +1 -0
  90. package/package.json +44 -0
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @claude-flow/plugin-agent-federation
2
+
3
+ Cross-installation agent federation with zero-trust security, PII-gated data flow, and compliance-grade audit trails.
4
+
5
+ ## Install + run
6
+
7
+ ```bash
8
+ npx -y -p @claude-flow/plugin-agent-federation@latest ruflo-federation --help
9
+ ```
10
+
11
+ ## Subcommands
12
+
13
+ | Command | Description |
14
+ |---|---|
15
+ | `ruflo-federation init` | Initialize federation on this node (generates keypair) |
16
+ | `ruflo-federation join <peer-url>` | Join a federation by connecting to a peer |
17
+ | `ruflo-federation leave` | Leave the current federation |
18
+ | `ruflo-federation peers` | List known peers and their trust levels |
19
+ | `ruflo-federation peers add <node-id>` | Add a peer to the federation |
20
+ | `ruflo-federation peers remove <node-id>` | Remove a peer |
21
+ | `ruflo-federation status` | Show federation health, sessions, trust levels |
22
+ | `ruflo-federation audit` | Query compliance-grade audit logs |
23
+ | `ruflo-federation trust` | Manage trust scores and tiers |
24
+ | `ruflo-federation config` | Show/update federation config |
25
+
26
+ ## Configuration via `.env`
27
+
28
+ ```bash
29
+ FEDERATION_NODE_NAME=my-node # default: hostname
30
+ FEDERATION_BIND_HOST=0.0.0.0 # default: 0.0.0.0
31
+ FEDERATION_BIND_PORT=8443 # default: 8443
32
+ FEDERATION_TRUST_LEVEL=untrusted # default: untrusted
33
+ ```
34
+
35
+ ## Tests
36
+
37
+ 325 unit tests covering audit, routing, discovery, plugin lifecycle.
38
+
39
+ ```bash
40
+ npm test
41
+ ```
42
+
43
+ ## License
44
+
45
+ MIT — Claude Flow Team.
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Federation circuit breaker service (ADR-097 Phase 2.b).
3
+ *
4
+ * Phase 2.a shipped the FederationNode state machine + canTransition table.
5
+ * This service is what drives transitions: it records per-peer send outcomes
6
+ * (success/failure + spend), evaluates thresholds against the configured
7
+ * policy, and calls suspend()/evict()/reactivate() on the entity.
8
+ *
9
+ * Architecture is two-layer to keep the policy testable in isolation:
10
+ *
11
+ * 1. **evaluatePolicy** (pure function) — given rolling samples for a peer
12
+ * + the policy + now, returns a BreakerDecision describing what should
13
+ * happen. No I/O, no clock, no entity mutation.
14
+ * 2. **FederationBreakerService** — owns the rolling-sample buffer,
15
+ * wraps evaluatePolicy with the entity mutation step, and exposes
16
+ * `recordOutcome` (called by the coordinator on each send completion)
17
+ * + `tick` (called periodically to drive cooldown/auto-evict).
18
+ *
19
+ * The cost-tracker integration (Phase 3 upstream — emits federation_spend
20
+ * events when sends complete) is *not* wired here; this service holds its
21
+ * own in-memory rolling buffer. When Phase 3 upstream lands, the consumer
22
+ * can call `recordOutcome` from the bus subscriber and the buffer becomes
23
+ * a transparent cache. This keeps Phase 2 deliverable without a hard
24
+ * cost-tracker dependency.
25
+ *
26
+ * Security invariants:
27
+ *
28
+ * 1. Negative tokens/usd are clamped to 0 — a malicious receive-side
29
+ * reporter cannot drive an "unsuspend" through credit inflation.
30
+ * 2. Sample buffer is bounded per-peer (DEFAULT_MAX_SAMPLES_PER_PEER) so
31
+ * a high-throughput peer cannot exhaust memory.
32
+ * 3. Policy evaluation is order-independent for SUSPEND triggers —
33
+ * cost OR failure-ratio fires the same SUSPEND. The reason field
34
+ * records which one tripped.
35
+ * 4. evaluatePolicy is pure & deterministic given (samples, policy, now)
36
+ * — no Date.now() calls inside, no random IDs, no hidden state.
37
+ * 5. Auto-eviction only runs from SUSPENDED state and only after the
38
+ * grace age. Cannot skip SUSPENDED for an ACTIVE → EVICTED jump.
39
+ */
40
+ import { FederationNode } from '../domain/entities/federation-node.js';
41
+ import { SuspensionReason } from '../domain/value-objects/federation-node-state.js';
42
+ /** A single send outcome recorded by the coordinator. */
43
+ export interface SendOutcome {
44
+ readonly nodeId: string;
45
+ readonly success: boolean;
46
+ /** Tokens consumed by this leg. Negative values are treated as 0. */
47
+ readonly tokensUsed?: number;
48
+ /** USD spent on this leg. Negative values are treated as 0. */
49
+ readonly usdSpent?: number;
50
+ /** Caller-supplied timestamp (for testability). Defaults to recordOutcome's `now`. */
51
+ readonly at?: Date;
52
+ }
53
+ /** Configurable thresholds. All defaults come from ADR-097 Part 2. */
54
+ export interface BreakerPolicy {
55
+ /** 24h spend cap per peer. Default $5.00. */
56
+ readonly costSuspensionUsd: number;
57
+ /** Window for the cost cap. Default 24h. */
58
+ readonly costWindowMs: number;
59
+ /** Failure ratio that trips the breaker. Default 0.5 (50%). */
60
+ readonly failureRatioThreshold: number;
61
+ /** Window for failure-ratio computation. Default 1h. */
62
+ readonly failureWindowMs: number;
63
+ /** Minimum samples in the failure window before evaluating (anti-noise). Default 10. */
64
+ readonly failureMinSamples: number;
65
+ /** Cooldown before SUSPENDED → reactivate-eligible. Default 30min. */
66
+ readonly cooldownMs: number;
67
+ /** Continuous suspension age before auto-evict. Default 24h. */
68
+ readonly autoEvictionAgeMs: number;
69
+ }
70
+ export declare const DEFAULT_BREAKER_POLICY: BreakerPolicy;
71
+ /** Per-peer sample buffer cap. ~24h of 1-Hz traffic. */
72
+ export declare const DEFAULT_MAX_SAMPLES_PER_PEER = 100000;
73
+ /** A normalized, time-stamped outcome (negatives clamped, `at` resolved). */
74
+ interface NormalizedSample {
75
+ readonly success: boolean;
76
+ readonly tokensUsed: number;
77
+ readonly usdSpent: number;
78
+ readonly at: Date;
79
+ }
80
+ /**
81
+ * What the breaker decided for a peer at evaluation time. The service then
82
+ * applies the action (calls node.suspend()/evict()/reactivate()) but the
83
+ * decision itself is a pure return so tests can assert it without checking
84
+ * mutated state.
85
+ */
86
+ export type BreakerDecision = {
87
+ readonly action: 'NONE';
88
+ readonly nodeId: string;
89
+ } | {
90
+ readonly action: 'SUSPEND';
91
+ readonly nodeId: string;
92
+ readonly reason: SuspensionReason;
93
+ } | {
94
+ readonly action: 'EVICT';
95
+ readonly nodeId: string;
96
+ readonly reason: SuspensionReason;
97
+ } | {
98
+ readonly action: 'REACTIVATE_ELIGIBLE';
99
+ readonly nodeId: string;
100
+ };
101
+ /**
102
+ * Pure policy evaluator. Returns the decision the breaker should apply.
103
+ * No I/O, no clock, no entity mutation — given the same inputs, returns the
104
+ * same output. The service layer is responsible for calling node.suspend()
105
+ * / evict() / reactivate() based on this.
106
+ *
107
+ * Decision rules:
108
+ *
109
+ * For ACTIVE peers:
110
+ * - 24h cost > policy.costSuspensionUsd → SUSPEND/COST_THRESHOLD_EXCEEDED
111
+ * - 1h failure ratio > policy.failureRatioThreshold (≥ failureMinSamples
112
+ * in window) → SUSPEND/FAILURE_RATIO_EXCEEDED
113
+ * - else NONE
114
+ *
115
+ * For SUSPENDED peers:
116
+ * - suspended ≥ autoEvictionAgeMs → EVICT/GRACE_PERIOD_EXPIRED
117
+ * - cooldown elapsed → REACTIVATE_ELIGIBLE (caller probes + reactivates)
118
+ * - else NONE
119
+ *
120
+ * For EVICTED peers: always NONE (terminal under breaker flow).
121
+ *
122
+ * Cost takes priority over failure-ratio when both trip, because cost is
123
+ * the more conservative signal (a malicious peer can manufacture failures
124
+ * by trickling errors but cannot manufacture spend without actually
125
+ * burning budget).
126
+ */
127
+ export declare function evaluatePolicy(samples: readonly NormalizedSample[], node: FederationNode, policy: BreakerPolicy, now: Date): BreakerDecision;
128
+ /**
129
+ * Stateful service: owns the per-peer sample buffer, applies decisions to
130
+ * entities, and exposes the breaker control surface to the coordinator.
131
+ *
132
+ * Construct once per federation coordinator. Inject the policy if you need
133
+ * non-default thresholds (Phase 4 doctor surface will let users tune them).
134
+ */
135
+ export declare class FederationBreakerService {
136
+ private readonly policy;
137
+ private readonly samples;
138
+ private readonly maxSamplesPerPeer;
139
+ constructor(policy?: BreakerPolicy, maxSamplesPerPeer?: number);
140
+ /** Read the active policy (handy for tests + doctor surface). */
141
+ getPolicy(): BreakerPolicy;
142
+ /**
143
+ * Record a send outcome for a peer. Called by the coordinator after each
144
+ * `routing.send` completion. Returns a BreakerDecision describing what,
145
+ * if anything, the breaker would do for this peer right now — but does
146
+ * NOT apply it (the caller asks `evaluate(node)` if they want the action
147
+ * to actually happen). This split lets the coordinator decide whether to
148
+ * apply on every outcome or batch via tick.
149
+ *
150
+ * Negative tokens/usd are clamped to 0. A peer cannot inflate its own
151
+ * remaining cost ceiling by reporting credits.
152
+ */
153
+ recordOutcome(outcome: SendOutcome, now?: Date): void;
154
+ /**
155
+ * Evaluate the breaker for one peer and apply the resulting transition
156
+ * to the entity. Returns the decision so callers can observe the outcome.
157
+ *
158
+ * `apply` defaults to true — set to false to get the dry-run decision
159
+ * without mutating the entity (useful for the doctor surface).
160
+ */
161
+ evaluate(node: FederationNode, now?: Date, apply?: boolean): BreakerDecision;
162
+ /**
163
+ * Periodic tick — evaluate all known peers. Intended to be called from a
164
+ * timer or after each batch of outcomes. Returns the decisions for
165
+ * observability.
166
+ */
167
+ tick(nodes: readonly FederationNode[], now?: Date): BreakerDecision[];
168
+ /** Drop a peer's sample buffer (e.g. after reactivate or evict). */
169
+ forget(nodeId: string): void;
170
+ /**
171
+ * Snapshot of per-peer aggregates for the doctor surface. Pure read —
172
+ * does not mutate the buffer.
173
+ */
174
+ snapshot(nodeId: string, now?: Date): {
175
+ readonly nodeId: string;
176
+ readonly sampleCount: number;
177
+ readonly cumUsdInWindow: number;
178
+ readonly failureRatioInWindow: number | null;
179
+ };
180
+ }
181
+ export {};
182
+ //# sourceMappingURL=federation-breaker-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"federation-breaker-service.d.ts","sourceRoot":"","sources":["../../src/application/federation-breaker-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Federation circuit breaker service (ADR-097 Phase 2.b).
3
+ *
4
+ * Phase 2.a shipped the FederationNode state machine + canTransition table.
5
+ * This service is what drives transitions: it records per-peer send outcomes
6
+ * (success/failure + spend), evaluates thresholds against the configured
7
+ * policy, and calls suspend()/evict()/reactivate() on the entity.
8
+ *
9
+ * Architecture is two-layer to keep the policy testable in isolation:
10
+ *
11
+ * 1. **evaluatePolicy** (pure function) — given rolling samples for a peer
12
+ * + the policy + now, returns a BreakerDecision describing what should
13
+ * happen. No I/O, no clock, no entity mutation.
14
+ * 2. **FederationBreakerService** — owns the rolling-sample buffer,
15
+ * wraps evaluatePolicy with the entity mutation step, and exposes
16
+ * `recordOutcome` (called by the coordinator on each send completion)
17
+ * + `tick` (called periodically to drive cooldown/auto-evict).
18
+ *
19
+ * The cost-tracker integration (Phase 3 upstream — emits federation_spend
20
+ * events when sends complete) is *not* wired here; this service holds its
21
+ * own in-memory rolling buffer. When Phase 3 upstream lands, the consumer
22
+ * can call `recordOutcome` from the bus subscriber and the buffer becomes
23
+ * a transparent cache. This keeps Phase 2 deliverable without a hard
24
+ * cost-tracker dependency.
25
+ *
26
+ * Security invariants:
27
+ *
28
+ * 1. Negative tokens/usd are clamped to 0 — a malicious receive-side
29
+ * reporter cannot drive an "unsuspend" through credit inflation.
30
+ * 2. Sample buffer is bounded per-peer (DEFAULT_MAX_SAMPLES_PER_PEER) so
31
+ * a high-throughput peer cannot exhaust memory.
32
+ * 3. Policy evaluation is order-independent for SUSPEND triggers —
33
+ * cost OR failure-ratio fires the same SUSPEND. The reason field
34
+ * records which one tripped.
35
+ * 4. evaluatePolicy is pure & deterministic given (samples, policy, now)
36
+ * — no Date.now() calls inside, no random IDs, no hidden state.
37
+ * 5. Auto-eviction only runs from SUSPENDED state and only after the
38
+ * grace age. Cannot skip SUSPENDED for an ACTIVE → EVICTED jump.
39
+ */
40
+ import { FederationNodeState, isCooldownElapsed, shouldAutoEvict, DEFAULT_SUSPENSION_COOLDOWN_MS, DEFAULT_AUTO_EVICTION_AGE_MS, } from '../domain/value-objects/federation-node-state.js';
41
+ export const DEFAULT_BREAKER_POLICY = Object.freeze({
42
+ costSuspensionUsd: 5.0,
43
+ costWindowMs: 24 * 60 * 60 * 1000,
44
+ failureRatioThreshold: 0.5,
45
+ failureWindowMs: 60 * 60 * 1000,
46
+ failureMinSamples: 10,
47
+ cooldownMs: DEFAULT_SUSPENSION_COOLDOWN_MS,
48
+ autoEvictionAgeMs: DEFAULT_AUTO_EVICTION_AGE_MS,
49
+ });
50
+ /** Per-peer sample buffer cap. ~24h of 1-Hz traffic. */
51
+ export const DEFAULT_MAX_SAMPLES_PER_PEER = 100_000;
52
+ /**
53
+ * Pure policy evaluator. Returns the decision the breaker should apply.
54
+ * No I/O, no clock, no entity mutation — given the same inputs, returns the
55
+ * same output. The service layer is responsible for calling node.suspend()
56
+ * / evict() / reactivate() based on this.
57
+ *
58
+ * Decision rules:
59
+ *
60
+ * For ACTIVE peers:
61
+ * - 24h cost > policy.costSuspensionUsd → SUSPEND/COST_THRESHOLD_EXCEEDED
62
+ * - 1h failure ratio > policy.failureRatioThreshold (≥ failureMinSamples
63
+ * in window) → SUSPEND/FAILURE_RATIO_EXCEEDED
64
+ * - else NONE
65
+ *
66
+ * For SUSPENDED peers:
67
+ * - suspended ≥ autoEvictionAgeMs → EVICT/GRACE_PERIOD_EXPIRED
68
+ * - cooldown elapsed → REACTIVATE_ELIGIBLE (caller probes + reactivates)
69
+ * - else NONE
70
+ *
71
+ * For EVICTED peers: always NONE (terminal under breaker flow).
72
+ *
73
+ * Cost takes priority over failure-ratio when both trip, because cost is
74
+ * the more conservative signal (a malicious peer can manufacture failures
75
+ * by trickling errors but cannot manufacture spend without actually
76
+ * burning budget).
77
+ */
78
+ export function evaluatePolicy(samples, node, policy, now) {
79
+ if (node.state === FederationNodeState.EVICTED) {
80
+ return { action: 'NONE', nodeId: node.nodeId };
81
+ }
82
+ if (node.state === FederationNodeState.SUSPENDED) {
83
+ if (shouldAutoEvict(node.stateChangedAt, now, policy.autoEvictionAgeMs)) {
84
+ return {
85
+ action: 'EVICT',
86
+ nodeId: node.nodeId,
87
+ reason: 'GRACE_PERIOD_EXPIRED',
88
+ };
89
+ }
90
+ if (isCooldownElapsed(node.stateChangedAt, now, policy.cooldownMs)) {
91
+ return { action: 'REACTIVATE_ELIGIBLE', nodeId: node.nodeId };
92
+ }
93
+ return { action: 'NONE', nodeId: node.nodeId };
94
+ }
95
+ // ACTIVE: check cost first, then failure ratio. Cost takes priority.
96
+ const costCutoff = now.getTime() - policy.costWindowMs;
97
+ let cumUsd = 0;
98
+ for (const s of samples) {
99
+ if (s.at.getTime() >= costCutoff)
100
+ cumUsd += s.usdSpent;
101
+ }
102
+ if (cumUsd > policy.costSuspensionUsd) {
103
+ return {
104
+ action: 'SUSPEND',
105
+ nodeId: node.nodeId,
106
+ reason: 'COST_THRESHOLD_EXCEEDED',
107
+ };
108
+ }
109
+ const failCutoff = now.getTime() - policy.failureWindowMs;
110
+ let total = 0;
111
+ let failures = 0;
112
+ for (const s of samples) {
113
+ if (s.at.getTime() < failCutoff)
114
+ continue;
115
+ total++;
116
+ if (!s.success)
117
+ failures++;
118
+ }
119
+ if (total >= policy.failureMinSamples) {
120
+ const ratio = failures / total;
121
+ if (ratio > policy.failureRatioThreshold) {
122
+ return {
123
+ action: 'SUSPEND',
124
+ nodeId: node.nodeId,
125
+ reason: 'FAILURE_RATIO_EXCEEDED',
126
+ };
127
+ }
128
+ }
129
+ return { action: 'NONE', nodeId: node.nodeId };
130
+ }
131
+ /**
132
+ * Stateful service: owns the per-peer sample buffer, applies decisions to
133
+ * entities, and exposes the breaker control surface to the coordinator.
134
+ *
135
+ * Construct once per federation coordinator. Inject the policy if you need
136
+ * non-default thresholds (Phase 4 doctor surface will let users tune them).
137
+ */
138
+ export class FederationBreakerService {
139
+ policy;
140
+ samples;
141
+ maxSamplesPerPeer;
142
+ constructor(policy = DEFAULT_BREAKER_POLICY, maxSamplesPerPeer = DEFAULT_MAX_SAMPLES_PER_PEER) {
143
+ this.policy = policy;
144
+ this.samples = new Map();
145
+ this.maxSamplesPerPeer = maxSamplesPerPeer;
146
+ }
147
+ /** Read the active policy (handy for tests + doctor surface). */
148
+ getPolicy() {
149
+ return this.policy;
150
+ }
151
+ /**
152
+ * Record a send outcome for a peer. Called by the coordinator after each
153
+ * `routing.send` completion. Returns a BreakerDecision describing what,
154
+ * if anything, the breaker would do for this peer right now — but does
155
+ * NOT apply it (the caller asks `evaluate(node)` if they want the action
156
+ * to actually happen). This split lets the coordinator decide whether to
157
+ * apply on every outcome or batch via tick.
158
+ *
159
+ * Negative tokens/usd are clamped to 0. A peer cannot inflate its own
160
+ * remaining cost ceiling by reporting credits.
161
+ */
162
+ recordOutcome(outcome, now = new Date()) {
163
+ const normalized = {
164
+ success: outcome.success,
165
+ tokensUsed: Math.max(0, outcome.tokensUsed ?? 0),
166
+ usdSpent: Math.max(0, outcome.usdSpent ?? 0),
167
+ at: outcome.at ?? now,
168
+ };
169
+ const buf = this.samples.get(outcome.nodeId) ?? [];
170
+ buf.push(normalized);
171
+ // Bounded buffer: drop oldest if over cap. Keeps memory bounded under
172
+ // sustained high traffic while preserving the most-recent window.
173
+ if (buf.length > this.maxSamplesPerPeer) {
174
+ buf.splice(0, buf.length - this.maxSamplesPerPeer);
175
+ }
176
+ this.samples.set(outcome.nodeId, buf);
177
+ }
178
+ /**
179
+ * Evaluate the breaker for one peer and apply the resulting transition
180
+ * to the entity. Returns the decision so callers can observe the outcome.
181
+ *
182
+ * `apply` defaults to true — set to false to get the dry-run decision
183
+ * without mutating the entity (useful for the doctor surface).
184
+ */
185
+ evaluate(node, now = new Date(), apply = true) {
186
+ const buf = this.samples.get(node.nodeId) ?? [];
187
+ const decision = evaluatePolicy(buf, node, this.policy, now);
188
+ if (!apply)
189
+ return decision;
190
+ switch (decision.action) {
191
+ case 'SUSPEND': {
192
+ const reason = { reason: decision.reason };
193
+ node.suspend(reason, now);
194
+ break;
195
+ }
196
+ case 'EVICT': {
197
+ const reason = { reason: decision.reason };
198
+ node.evict(reason, now);
199
+ break;
200
+ }
201
+ case 'REACTIVATE_ELIGIBLE':
202
+ // The breaker does NOT auto-reactivate. The integrator's health
203
+ // probe is responsible for confirming the peer is healthy and
204
+ // calling node.reactivate() itself. We only signal eligibility.
205
+ break;
206
+ case 'NONE':
207
+ break;
208
+ }
209
+ return decision;
210
+ }
211
+ /**
212
+ * Periodic tick — evaluate all known peers. Intended to be called from a
213
+ * timer or after each batch of outcomes. Returns the decisions for
214
+ * observability.
215
+ */
216
+ tick(nodes, now = new Date()) {
217
+ return nodes.map((n) => this.evaluate(n, now));
218
+ }
219
+ /** Drop a peer's sample buffer (e.g. after reactivate or evict). */
220
+ forget(nodeId) {
221
+ this.samples.delete(nodeId);
222
+ }
223
+ /**
224
+ * Snapshot of per-peer aggregates for the doctor surface. Pure read —
225
+ * does not mutate the buffer.
226
+ */
227
+ snapshot(nodeId, now = new Date()) {
228
+ const buf = this.samples.get(nodeId) ?? [];
229
+ const costCutoff = now.getTime() - this.policy.costWindowMs;
230
+ const failCutoff = now.getTime() - this.policy.failureWindowMs;
231
+ let cumUsd = 0;
232
+ let total = 0;
233
+ let failures = 0;
234
+ for (const s of buf) {
235
+ if (s.at.getTime() >= costCutoff)
236
+ cumUsd += s.usdSpent;
237
+ if (s.at.getTime() >= failCutoff) {
238
+ total++;
239
+ if (!s.success)
240
+ failures++;
241
+ }
242
+ }
243
+ return {
244
+ nodeId,
245
+ sampleCount: buf.length,
246
+ cumUsdInWindow: cumUsd,
247
+ failureRatioInWindow: total > 0 ? failures / total : null,
248
+ };
249
+ }
250
+ }
251
+ //# sourceMappingURL=federation-breaker-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"federation-breaker-service.js","sourceRoot":"","sources":["../../src/application/federation-breaker-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,166 @@
1
+ import { type FederationNodeStateRecord } from '../domain/entities/federation-node.js';
2
+ import { FederationSession } from '../domain/entities/federation-session.js';
3
+ import { type FederationMessageType } from '../domain/entities/federation-envelope.js';
4
+ import { TrustLevel } from '../domain/entities/trust-level.js';
5
+ import { type SuspensionReason } from '../domain/value-objects/federation-node-state.js';
6
+ import { DiscoveryService, type FederationManifest } from '../domain/services/discovery-service.js';
7
+ import { HandshakeService } from '../domain/services/handshake-service.js';
8
+ import { RoutingService, type RoutingResult } from '../domain/services/routing-service.js';
9
+ import { AuditService } from '../domain/services/audit-service.js';
10
+ import { PIIPipelineService } from '../domain/services/pii-pipeline-service.js';
11
+ import { TrustEvaluator } from './trust-evaluator.js';
12
+ import { PolicyEngine } from './policy-engine.js';
13
+ import { enforceBudget, validateBudget } from '../domain/value-objects/federation-budget.js';
14
+ import type { FederationBreakerService } from './federation-breaker-service.js';
15
+ import type { SpendReporter } from './spend-reporter.js';
16
+ /**
17
+ * Optional per-call budget controls (ADR-097 Phase 1). All fields are
18
+ * optional; omitted means "no limit on that axis" (the default unbounded
19
+ * budget still has maxHops = 8 to defang recursive delegation loops).
20
+ */
21
+ export interface SendOptions {
22
+ readonly budget?: {
23
+ maxTokens?: number;
24
+ maxUsd?: number;
25
+ };
26
+ readonly maxHops?: number;
27
+ /**
28
+ * How many hops this message has already traveled (0 on the originator).
29
+ * Phase 1 enforces at the send side because there is no inbound dispatcher
30
+ * yet; callers re-forwarding a received message should pass the original
31
+ * envelope's hopCount here.
32
+ */
33
+ readonly hopCount?: number;
34
+ /** Caller-reported usage from the previous leg, for cumulative checks. */
35
+ readonly spent?: {
36
+ tokens?: number;
37
+ usd?: number;
38
+ };
39
+ }
40
+ export interface FederationCoordinatorConfig {
41
+ readonly nodeId: string;
42
+ readonly publicKey: string;
43
+ readonly endpoint: string;
44
+ readonly capabilities: readonly string[];
45
+ }
46
+ export interface FederationStatus {
47
+ readonly nodeId: string;
48
+ readonly activeSessions: number;
49
+ readonly knownPeers: number;
50
+ readonly trustLevels: Record<string, TrustLevel>;
51
+ readonly healthy: boolean;
52
+ }
53
+ /**
54
+ * Optional integrations (ADR-097 Phase 3 upstream + Phase 2.b breaker
55
+ * wiring). Both are constructor-injected; both default to no-op.
56
+ *
57
+ * - `spendReporter` — invoked by reportSpend() when present; persists
58
+ * the FederationSpendEvent to whatever backend the integrator wired
59
+ * (cost-tracker bus, ruflo memory federation-spend namespace, etc.)
60
+ * - `breakerService` — invoked by reportSpend() when present; calls
61
+ * recordOutcome() so the breaker's in-memory rolling buffer is fed
62
+ * without requiring the integrator to wire two parallel pipelines
63
+ */
64
+ export interface FederationCoordinatorIntegrations {
65
+ readonly spendReporter?: SpendReporter;
66
+ readonly breakerService?: FederationBreakerService;
67
+ }
68
+ export declare class FederationCoordinator {
69
+ private readonly config;
70
+ private readonly discovery;
71
+ private readonly handshake;
72
+ private readonly routing;
73
+ private readonly audit;
74
+ private readonly piiPipeline;
75
+ private readonly trustEvaluator;
76
+ private readonly policyEngine;
77
+ private readonly sessions;
78
+ private initialized;
79
+ private readonly spendReporter?;
80
+ private readonly breakerService?;
81
+ constructor(config: FederationCoordinatorConfig, discovery: DiscoveryService, handshake: HandshakeService, routing: RoutingService, audit: AuditService, piiPipeline: PIIPipelineService, trustEvaluator: TrustEvaluator, policyEngine: PolicyEngine, integrations?: FederationCoordinatorIntegrations);
82
+ initialize(manifest: Omit<FederationManifest, 'signature'>): Promise<void>;
83
+ shutdown(): Promise<void>;
84
+ joinPeer(endpoint: string): Promise<FederationSession>;
85
+ leavePeer(nodeId: string): Promise<void>;
86
+ sendMessage<T>(targetNodeId: string, messageType: FederationMessageType, payload: T, options?: SendOptions): Promise<RoutingResult>;
87
+ /**
88
+ * Re-export the budget primitives so external callers (e.g. follow-up
89
+ * iterations adding receive-side decrement) can reuse them without
90
+ * dipping into domain/value-objects directly.
91
+ */
92
+ static readonly budget: {
93
+ validate: typeof validateBudget;
94
+ enforce: typeof enforceBudget;
95
+ };
96
+ broadcastMessage<T>(messageType: FederationMessageType, payload: T): Promise<RoutingResult[]>;
97
+ handleThreatDetection(nodeId: string): void;
98
+ handleHmacFailure(nodeId: string): void;
99
+ getStatus(): FederationStatus;
100
+ /**
101
+ * ADR-097 Phase 4: per-peer breaker state snapshot for the doctor
102
+ * surface and `federation_breaker_status` MCP tool.
103
+ *
104
+ * Returns one entry per known peer with the entity's stateRecord —
105
+ * what state, when it changed, why, and which caller's correlation
106
+ * key triggered it. Pure read; does not mutate.
107
+ */
108
+ getPeerStates(): readonly (FederationNodeStateRecord & {
109
+ readonly nodeId: string;
110
+ })[];
111
+ /**
112
+ * Aggregated counts for the doctor surface — `{ active: N, suspended: M,
113
+ * evicted: K }`. Cheap O(peers) sweep; safe to call from a status line.
114
+ */
115
+ getPeerStateCounts(): {
116
+ readonly active: number;
117
+ readonly suspended: number;
118
+ readonly evicted: number;
119
+ };
120
+ /**
121
+ * Operator-initiated evict. Returns true on transition, false if the
122
+ * peer was already EVICTED or unknown. Logs to audit either way.
123
+ *
124
+ * Does NOT remove the peer from the discovery registry — `leavePeer`
125
+ * is the registry-removal API. Eviction is the breaker layer; the peer
126
+ * remains queryable so the operator can later reactivate.
127
+ */
128
+ evictPeer(nodeId: string, reason?: SuspensionReason, correlationId?: string): Promise<boolean>;
129
+ /**
130
+ * ADR-097 Phase 3 upstream: report the actual cost of a federated
131
+ * call. Federation doesn't own model pricing, so the integrator calls
132
+ * this after the downstream agent completes.
133
+ *
134
+ * Fans out to:
135
+ * - spendReporter (if injected) — persists to integrator's backend
136
+ * - breakerService.recordOutcome (if injected) — feeds the breaker's
137
+ * in-memory rolling buffer for cost/failure-ratio thresholds
138
+ *
139
+ * Both are no-ops if the corresponding integration isn't wired, so
140
+ * callers don't need to branch on configuration. Negative tokens/usd
141
+ * are clamped to 0 at the breaker layer (anti-credit-inflation); the
142
+ * spend reporter receives the raw values so backends can audit them.
143
+ *
144
+ * Auto-fills `ts` if the caller omits it.
145
+ */
146
+ reportSpend(input: {
147
+ readonly peerId: string;
148
+ readonly taskId?: string;
149
+ readonly tokensUsed: number;
150
+ readonly usdSpent: number;
151
+ readonly success: boolean;
152
+ readonly ts?: string;
153
+ }): Promise<void>;
154
+ /**
155
+ * Operator-initiated reactivate. Used after an integrator-supplied
156
+ * health probe confirms a SUSPENDED peer is healthy, OR as an
157
+ * operator-override escape from EVICTED. Returns true on transition.
158
+ */
159
+ reactivatePeer(nodeId: string, correlationId?: string): Promise<boolean>;
160
+ getSession(sessionId: string): FederationSession | undefined;
161
+ getActiveSessions(): FederationSession[];
162
+ private establishSession;
163
+ private findSessionByNodeId;
164
+ private ensureInitialized;
165
+ }
166
+ //# sourceMappingURL=federation-coordinator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"federation-coordinator.d.ts","sourceRoot":"","sources":["../../src/application/federation-coordinator.ts"],"names":[],"mappings":"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"}