@claude-flow/plugin-agent-federation 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2a/agent-card.d.ts +130 -0
- package/dist/a2a/consume.d.ts +34 -0
- package/dist/a2a/planner-descriptor.d.ts +40 -0
- package/dist/a2a/well-known.d.ts +46 -0
- package/dist/application/claim-checker.d.ts +27 -0
- package/dist/application/federation-breaker-service.d.ts +192 -0
- package/dist/application/federation-coordinator.d.ts +211 -0
- package/dist/application/inbound-dispatcher.d.ts +113 -0
- package/dist/application/policy-engine.d.ts +42 -0
- package/dist/application/policy-engine.d.ts.map +1 -1
- package/dist/application/policy-engine.js +11 -1
- package/dist/application/policy-engine.js.map +1 -1
- package/dist/application/spend-reporter.d.ts +150 -0
- package/dist/application/trust-evaluator.d.ts +74 -0
- package/dist/bin.d.ts +13 -0
- package/dist/cli-commands.d.ts +7 -0
- package/dist/domain/entities/federation-envelope.d.ts +45 -0
- package/dist/domain/entities/federation-node.d.ts +104 -0
- package/dist/domain/entities/federation-session.d.ts +55 -0
- package/dist/domain/entities/trust-level.d.ts +17 -0
- package/dist/domain/entities/trust-level.d.ts.map +1 -1
- package/dist/domain/entities/trust-level.js +9 -2
- package/dist/domain/entities/trust-level.js.map +1 -1
- package/dist/domain/services/audit-service.d.ts +69 -0
- package/dist/domain/services/discovery-service.d.ts +76 -0
- package/dist/domain/services/handshake-service.d.ts +62 -0
- package/dist/domain/services/pii-pipeline-service.d.ts +65 -0
- package/dist/domain/services/routing-service.d.ts +54 -0
- package/dist/domain/services/wg-firewall-service.d.ts +71 -0
- package/dist/domain/services/wg-mesh-service.d.ts +156 -0
- package/dist/domain/services/wg-witness-service.d.ts +98 -0
- package/dist/domain/value-objects/federation-budget.d.ts +89 -0
- package/dist/domain/value-objects/federation-node-state.d.ts +92 -0
- package/dist/domain/value-objects/wg-config.d.ts +70 -0
- package/dist/index.d.ts +24 -0
- package/dist/mcp-tools.d.ts +10 -0
- package/dist/plugin.d.ts +19 -0
- package/dist/transport/midstream-aware-loader.d.ts +72 -0
- package/dist/version.d.ts +3 -0
- package/package.json +2 -2
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A (Agent2Agent, Linux Foundation) Agent Card adapter — cards only.
|
|
3
|
+
*
|
|
4
|
+
* Maps ruflo's bespoke federation identity (`FederationManifest` /
|
|
5
|
+
* `FederationNode`) to and from a spec-compliant A2A Agent Card so ruflo
|
|
6
|
+
* nodes are discoverable by A2A peers and A2A peers appear in federation
|
|
7
|
+
* discovery. This is an ADAPTER over the existing federation schema, not a
|
|
8
|
+
* rewrite — A2A Tasks/messaging are deliberately out of scope.
|
|
9
|
+
*
|
|
10
|
+
* Spec: A2A Protocol 1.0.0 — https://a2a-protocol.org/latest/specification/
|
|
11
|
+
* (schema source of truth: specification/a2a.proto in a2aproject/A2A).
|
|
12
|
+
* Required AgentCard fields per spec §4.4.1: name, description,
|
|
13
|
+
* supportedInterfaces, version, capabilities, defaultInputModes,
|
|
14
|
+
* defaultOutputModes, skills.
|
|
15
|
+
*/
|
|
16
|
+
import { FederationNode } from '../domain/entities/federation-node.js';
|
|
17
|
+
import type { FederationManifest } from '../domain/services/discovery-service.js';
|
|
18
|
+
/** A2A protocol version this adapter targets. */
|
|
19
|
+
export declare const A2A_PROTOCOL_VERSION = "1.0";
|
|
20
|
+
/**
|
|
21
|
+
* Well-known URI for Agent Card discovery (A2A 1.0 §8.2 + IANA registration
|
|
22
|
+
* §14). NOTE: earlier drafts used `/.well-known/agent.json`; 0.3.0+ settled
|
|
23
|
+
* on `agent-card.json`.
|
|
24
|
+
*/
|
|
25
|
+
export declare const A2A_WELL_KNOWN_PATH = "/.well-known/agent-card.json";
|
|
26
|
+
/**
|
|
27
|
+
* Open-form protocol binding string advertising ruflo's federation wire
|
|
28
|
+
* (Ed25519-signed envelopes over WebSocket/QUIC). Spec §4.4.6 explicitly
|
|
29
|
+
* allows non-core bindings: "This is an open form string, to be easily
|
|
30
|
+
* extended for other protocol bindings."
|
|
31
|
+
*/
|
|
32
|
+
export declare const RUFLO_FEDERATION_BINDING = "RUFLO-FEDERATION";
|
|
33
|
+
/**
|
|
34
|
+
* Extension URI carrying the bespoke federation identity inside the card
|
|
35
|
+
* (spec §4.6 AgentExtension). Enables lossless round-trip in fromAgentCard.
|
|
36
|
+
*/
|
|
37
|
+
export declare const RUFLO_FEDERATION_EXTENSION_URI = "urn:ruflo:federation:manifest:v1";
|
|
38
|
+
export interface A2AAgentInterface {
|
|
39
|
+
readonly url: string;
|
|
40
|
+
readonly protocolBinding: string;
|
|
41
|
+
readonly protocolVersion: string;
|
|
42
|
+
readonly tenant?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface A2AAgentSkill {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly description: string;
|
|
48
|
+
readonly tags: readonly string[];
|
|
49
|
+
readonly examples?: readonly string[];
|
|
50
|
+
readonly inputModes?: readonly string[];
|
|
51
|
+
readonly outputModes?: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
export interface A2AAgentExtension {
|
|
54
|
+
readonly uri: string;
|
|
55
|
+
readonly description?: string;
|
|
56
|
+
readonly required?: boolean;
|
|
57
|
+
readonly params?: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
export interface A2AAgentCapabilities {
|
|
60
|
+
readonly streaming?: boolean;
|
|
61
|
+
readonly pushNotifications?: boolean;
|
|
62
|
+
readonly extensions?: readonly A2AAgentExtension[];
|
|
63
|
+
readonly extendedAgentCard?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface A2AAgentProvider {
|
|
66
|
+
readonly url: string;
|
|
67
|
+
readonly organization: string;
|
|
68
|
+
}
|
|
69
|
+
export interface A2AAgentCard {
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly description: string;
|
|
72
|
+
readonly supportedInterfaces: readonly A2AAgentInterface[];
|
|
73
|
+
readonly version: string;
|
|
74
|
+
readonly capabilities: A2AAgentCapabilities;
|
|
75
|
+
readonly defaultInputModes: readonly string[];
|
|
76
|
+
readonly defaultOutputModes: readonly string[];
|
|
77
|
+
readonly skills: readonly A2AAgentSkill[];
|
|
78
|
+
readonly provider?: A2AAgentProvider;
|
|
79
|
+
readonly documentationUrl?: string;
|
|
80
|
+
readonly iconUrl?: string;
|
|
81
|
+
readonly securitySchemes?: Record<string, unknown>;
|
|
82
|
+
readonly securityRequirements?: readonly Record<string, readonly string[]>[];
|
|
83
|
+
readonly signatures?: readonly {
|
|
84
|
+
protected: string;
|
|
85
|
+
signature: string;
|
|
86
|
+
}[];
|
|
87
|
+
}
|
|
88
|
+
export interface ToAgentCardOptions {
|
|
89
|
+
/** Human-readable agent name; defaults to `ruflo-federation/<nodeId>`. */
|
|
90
|
+
readonly name?: string;
|
|
91
|
+
/** Human-readable description override. */
|
|
92
|
+
readonly description?: string;
|
|
93
|
+
readonly provider?: A2AAgentProvider;
|
|
94
|
+
readonly documentationUrl?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Extra spec-core interfaces to advertise ahead of the federation binding
|
|
97
|
+
* (e.g. a JSONRPC endpoint once A2A messaging lands). First entry is the
|
|
98
|
+
* preferred interface per spec.
|
|
99
|
+
*/
|
|
100
|
+
readonly additionalInterfaces?: readonly A2AAgentInterface[];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Generate a spec-compliant A2A Agent Card from a ruflo federation manifest.
|
|
104
|
+
* Every agent type becomes an AgentSkill; the federation endpoint becomes a
|
|
105
|
+
* `RUFLO-FEDERATION` interface; the bespoke identity (nodeId, publicKey,
|
|
106
|
+
* complianceModes, manifest signature) rides in a spec §4.6 extension so
|
|
107
|
+
* `fromAgentCard` can restore it.
|
|
108
|
+
*/
|
|
109
|
+
export declare function toAgentCard(manifest: FederationManifest, options?: ToAgentCardOptions): A2AAgentCard;
|
|
110
|
+
export interface AgentCardValidation {
|
|
111
|
+
readonly valid: boolean;
|
|
112
|
+
readonly errors: readonly string[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Structural validation against the A2A 1.0 AgentCard REQUIRED fields.
|
|
116
|
+
* Intentionally strict on required shape, lenient on unknown extra fields
|
|
117
|
+
* (forward compatibility with future spec minors).
|
|
118
|
+
*/
|
|
119
|
+
export declare function validateAgentCard(value: unknown): AgentCardValidation;
|
|
120
|
+
/**
|
|
121
|
+
* Map a validated remote A2A Agent Card into the bespoke federation registry
|
|
122
|
+
* shape (a `FederationNode`) so A2A peers appear in federation discovery.
|
|
123
|
+
*
|
|
124
|
+
* Trust posture: A2A cards are self-describing metadata, NOT authenticated
|
|
125
|
+
* federation manifests — consumed peers always enter at
|
|
126
|
+
* `TrustLevel.UNTRUSTED` regardless of embedded identity claims. Trust is
|
|
127
|
+
* earned afterwards through the normal handshake + TrustEvaluator path.
|
|
128
|
+
*/
|
|
129
|
+
export declare function fromAgentCard(card: A2AAgentCard, sourceUrl?: string): FederationNode;
|
|
130
|
+
//# sourceMappingURL=agent-card.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Agent Card consumption — fetch + validate a remote card and map it
|
|
3
|
+
* into the bespoke federation registry so A2A peers appear in federation
|
|
4
|
+
* discovery. Cards only (no A2A Tasks/messaging in this iteration).
|
|
5
|
+
*/
|
|
6
|
+
import type { DiscoveryService } from '../domain/services/discovery-service.js';
|
|
7
|
+
import type { FederationNode } from '../domain/entities/federation-node.js';
|
|
8
|
+
import { type A2AAgentCard } from './agent-card.js';
|
|
9
|
+
export interface FetchAgentCardOptions {
|
|
10
|
+
/** Injectable fetch for tests / restricted environments. Default: globalThis.fetch. */
|
|
11
|
+
readonly fetchImpl?: typeof fetch;
|
|
12
|
+
readonly timeoutMs?: number;
|
|
13
|
+
/** Response size cap in bytes (defense against decompression floods). Default 256 KiB. */
|
|
14
|
+
readonly maxBytes?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface FetchAgentCardResult {
|
|
17
|
+
readonly card: A2AAgentCard;
|
|
18
|
+
readonly sourceUrl: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the card URL: a bare base URL (`https://host[:port]` or a path-less
|
|
22
|
+
* URL) gets the A2A well-known path appended; anything already pointing at a
|
|
23
|
+
* JSON document is used as-is.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveAgentCardUrl(baseOrCardUrl: string): string;
|
|
26
|
+
/** Fetch and structurally validate a remote A2A Agent Card. */
|
|
27
|
+
export declare function fetchAgentCard(baseOrCardUrl: string, options?: FetchAgentCardOptions): Promise<FetchAgentCardResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Fetch a remote Agent Card and register the peer in federation discovery.
|
|
30
|
+
* The peer enters at TrustLevel.UNTRUSTED (see fromAgentCard) — the card is
|
|
31
|
+
* self-asserted metadata, and trust is earned through the normal handshake.
|
|
32
|
+
*/
|
|
33
|
+
export declare function consumeAgentCard(discovery: DiscoveryService, baseOrCardUrl: string, options?: FetchAgentCardOptions): Promise<FederationNode>;
|
|
34
|
+
//# sourceMappingURL=consume.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type A2AAgentCard } from './agent-card.js';
|
|
2
|
+
/**
|
|
3
|
+
* Host-owned planner semantics for a registered capability identifier.
|
|
4
|
+
* Remote registration metadata may select an identifier, but never supplies
|
|
5
|
+
* these planner-visible strings directly.
|
|
6
|
+
*/
|
|
7
|
+
export interface PlannerCapabilityDefinition {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly functionality: string;
|
|
10
|
+
readonly inputSpecification: string;
|
|
11
|
+
readonly outputSpecification: string;
|
|
12
|
+
readonly usageConstraints: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export interface PlannerDescriptorPolicy {
|
|
15
|
+
/** Resolve only identifiers the host has explicitly approved for planning. */
|
|
16
|
+
readonly resolveCapability: (canonicalSkillId: string) => PlannerCapabilityDefinition | undefined;
|
|
17
|
+
readonly maximumCapabilities?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PlannerSafeAgentDescriptor {
|
|
20
|
+
readonly schema: 'ruflo/planner-safe-agent-descriptor/v1';
|
|
21
|
+
readonly source: 'a2a-agent-card';
|
|
22
|
+
readonly capabilities: readonly PlannerCapabilityDefinition[];
|
|
23
|
+
readonly authority: 'none';
|
|
24
|
+
}
|
|
25
|
+
export interface PlannerDescriptorProjection {
|
|
26
|
+
readonly descriptor: PlannerSafeAgentDescriptor;
|
|
27
|
+
readonly rejectedSkillCount: number;
|
|
28
|
+
readonly duplicateSkillCount: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Project a third-party A2A Agent Card into planner-visible metadata.
|
|
32
|
+
*
|
|
33
|
+
* Security boundary: card descriptions, skill descriptions, examples, tags,
|
|
34
|
+
* provider text, documentation URLs, extension parameters, and all other
|
|
35
|
+
* remote prose are deliberately absent from the return type. Only a bounded
|
|
36
|
+
* canonical skill identifier may cross the registration boundary, and its
|
|
37
|
+
* planner semantics come from a host-controlled resolver.
|
|
38
|
+
*/
|
|
39
|
+
export declare function projectPlannerSafeAgentCard(card: A2AAgentCard, policy: PlannerDescriptorPolicy): PlannerDescriptorProjection;
|
|
40
|
+
//# sourceMappingURL=planner-descriptor.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A well-known Agent Card endpoint (A2A 1.0 §8.2 discovery + §8.6 caching).
|
|
3
|
+
*
|
|
4
|
+
* Serves `GET /.well-known/agent-card.json` from a minimal `node:http`
|
|
5
|
+
* server. Security posture follows ADR-166 (mcp-bridge unauthenticated-RCE
|
|
6
|
+
* remediation): binds 127.0.0.1 by default and REFUSES a non-loopback bind
|
|
7
|
+
* unless the caller passes `allowNonLoopback: true` explicitly. The endpoint
|
|
8
|
+
* is read-only (GET/HEAD on one exact path), exposes only the public card,
|
|
9
|
+
* and is entirely opt-in — nothing here weakens the federation transport's
|
|
10
|
+
* existing bind or auth behavior.
|
|
11
|
+
*/
|
|
12
|
+
import { type A2AAgentCard } from './agent-card.js';
|
|
13
|
+
export interface AgentCardServerOptions {
|
|
14
|
+
/** Card supplier — re-invoked per request so a late-published manifest is picked up. */
|
|
15
|
+
readonly getCard: () => A2AAgentCard | null;
|
|
16
|
+
readonly port: number;
|
|
17
|
+
/** Bind host. Default 127.0.0.1 (ADR-166 posture). */
|
|
18
|
+
readonly host?: string;
|
|
19
|
+
/** Serve path. Default A2A_WELL_KNOWN_PATH. */
|
|
20
|
+
readonly path?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Explicit opt-in required to bind anything other than loopback. Without
|
|
23
|
+
* it a non-loopback host is rejected at start (fail-closed).
|
|
24
|
+
*/
|
|
25
|
+
readonly allowNonLoopback?: boolean;
|
|
26
|
+
/** Cache-Control max-age seconds (spec §8.6.1). Default 300. */
|
|
27
|
+
readonly cacheMaxAgeSeconds?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface AgentCardServerHandle {
|
|
30
|
+
readonly url: string;
|
|
31
|
+
readonly host: string;
|
|
32
|
+
readonly port: number;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Start the well-known Agent Card HTTP endpoint.
|
|
38
|
+
*
|
|
39
|
+
* Responses:
|
|
40
|
+
* GET/HEAD <path> → 200 application/json (+ Cache-Control, ETag per §8.6)
|
|
41
|
+
* or 503 when no card is available yet
|
|
42
|
+
* other method → 405
|
|
43
|
+
* other path → 404
|
|
44
|
+
*/
|
|
45
|
+
export declare function startAgentCardServer(options: AgentCardServerOptions): Promise<AgentCardServerHandle>;
|
|
46
|
+
//# sourceMappingURL=well-known.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FederationClaimType, PolicyEngineDeps } from './policy-engine.js';
|
|
2
|
+
export type FederationAuthorizationMode = 'legacy' | 'observe' | 'enforce';
|
|
3
|
+
export interface FederationClaimCheckerConfig {
|
|
4
|
+
mode?: FederationAuthorizationMode;
|
|
5
|
+
grantedClaims?: readonly string[];
|
|
6
|
+
onObservation?: (claim: FederationClaimType, granted: boolean) => void;
|
|
7
|
+
}
|
|
8
|
+
export interface FederationClaimChecker {
|
|
9
|
+
mode: FederationAuthorizationMode;
|
|
10
|
+
grantedClaims: ReadonlySet<FederationClaimType>;
|
|
11
|
+
checkClaim: PolicyEngineDeps['checkClaim'];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compatibility bridge for the legacy federation policy engine.
|
|
15
|
+
*
|
|
16
|
+
* This removes the anonymous `() => true` production stub and makes
|
|
17
|
+
* compatibility behavior explicit:
|
|
18
|
+
* - legacy: preserve pre-ADR-325 behavior;
|
|
19
|
+
* - observe: calculate and report missing grants without blocking;
|
|
20
|
+
* - enforce: default deny unless the exact claim is configured.
|
|
21
|
+
*
|
|
22
|
+
* ADR-324 policy adapters can supply the `grantedClaims` set after evaluating
|
|
23
|
+
* the request; ownership-changing federation messages remain disabled in the
|
|
24
|
+
* default message policy until the full ingress PEP is composed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createFederationClaimChecker(config?: FederationClaimCheckerConfig): FederationClaimChecker;
|
|
27
|
+
//# sourceMappingURL=claim-checker.d.ts.map
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
/**
|
|
136
|
+
* ADR-111 Phase 3 — callback invoked after the breaker applies a successful
|
|
137
|
+
* state transition. Used by the coordinator to propagate suspends to the
|
|
138
|
+
* WG mesh layer (clearing AllowedIPs). Not invoked for `apply=false` dry runs
|
|
139
|
+
* or `NONE` / `REACTIVATE_ELIGIBLE` decisions. Errors thrown from the callback
|
|
140
|
+
* are swallowed at the breaker — the entity transition has already happened,
|
|
141
|
+
* and side-effect failures shouldn't unwind the breaker's internal state.
|
|
142
|
+
*/
|
|
143
|
+
export type BreakerTransitionListener = (node: FederationNode, decision: BreakerDecision) => void | Promise<void>;
|
|
144
|
+
export declare class FederationBreakerService {
|
|
145
|
+
private readonly policy;
|
|
146
|
+
private readonly samples;
|
|
147
|
+
private readonly maxSamplesPerPeer;
|
|
148
|
+
private readonly onTransition?;
|
|
149
|
+
constructor(policy?: BreakerPolicy, maxSamplesPerPeer?: number, onTransition?: BreakerTransitionListener);
|
|
150
|
+
/** Read the active policy (handy for tests + doctor surface). */
|
|
151
|
+
getPolicy(): BreakerPolicy;
|
|
152
|
+
/**
|
|
153
|
+
* Record a send outcome for a peer. Called by the coordinator after each
|
|
154
|
+
* `routing.send` completion. Returns a BreakerDecision describing what,
|
|
155
|
+
* if anything, the breaker would do for this peer right now — but does
|
|
156
|
+
* NOT apply it (the caller asks `evaluate(node)` if they want the action
|
|
157
|
+
* to actually happen). This split lets the coordinator decide whether to
|
|
158
|
+
* apply on every outcome or batch via tick.
|
|
159
|
+
*
|
|
160
|
+
* Negative tokens/usd are clamped to 0. A peer cannot inflate its own
|
|
161
|
+
* remaining cost ceiling by reporting credits.
|
|
162
|
+
*/
|
|
163
|
+
recordOutcome(outcome: SendOutcome, now?: Date): void;
|
|
164
|
+
/**
|
|
165
|
+
* Evaluate the breaker for one peer and apply the resulting transition
|
|
166
|
+
* to the entity. Returns the decision so callers can observe the outcome.
|
|
167
|
+
*
|
|
168
|
+
* `apply` defaults to true — set to false to get the dry-run decision
|
|
169
|
+
* without mutating the entity (useful for the doctor surface).
|
|
170
|
+
*/
|
|
171
|
+
evaluate(node: FederationNode, now?: Date, apply?: boolean): BreakerDecision;
|
|
172
|
+
/**
|
|
173
|
+
* Periodic tick — evaluate all known peers. Intended to be called from a
|
|
174
|
+
* timer or after each batch of outcomes. Returns the decisions for
|
|
175
|
+
* observability.
|
|
176
|
+
*/
|
|
177
|
+
tick(nodes: readonly FederationNode[], now?: Date): BreakerDecision[];
|
|
178
|
+
/** Drop a peer's sample buffer (e.g. after reactivate or evict). */
|
|
179
|
+
forget(nodeId: string): void;
|
|
180
|
+
/**
|
|
181
|
+
* Snapshot of per-peer aggregates for the doctor surface. Pure read —
|
|
182
|
+
* does not mutate the buffer.
|
|
183
|
+
*/
|
|
184
|
+
snapshot(nodeId: string, now?: Date): {
|
|
185
|
+
readonly nodeId: string;
|
|
186
|
+
readonly sampleCount: number;
|
|
187
|
+
readonly cumUsdInWindow: number;
|
|
188
|
+
readonly failureRatioInWindow: number | null;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export {};
|
|
192
|
+
//# sourceMappingURL=federation-breaker-service.d.ts.map
|