@claude-flow/plugin-agent-federation 1.0.0-alpha.5 → 1.0.0-alpha.7
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/application/federation-breaker-service.d.ts +182 -0
- package/dist/application/federation-breaker-service.d.ts.map +1 -0
- package/dist/application/federation-breaker-service.js +251 -0
- package/dist/application/federation-breaker-service.js.map +1 -0
- package/dist/application/federation-coordinator.d.ts +72 -1
- package/dist/application/federation-coordinator.d.ts.map +1 -1
- package/dist/application/federation-coordinator.js +164 -2
- package/dist/application/federation-coordinator.js.map +1 -1
- package/dist/application/policy-engine.d.ts.map +1 -1
- package/dist/application/policy-engine.js +9 -0
- package/dist/application/policy-engine.js.map +1 -1
- package/dist/domain/entities/federation-envelope.d.ts +1 -1
- package/dist/domain/entities/federation-envelope.d.ts.map +1 -1
- package/dist/domain/entities/federation-envelope.js +3 -0
- package/dist/domain/entities/federation-envelope.js.map +1 -1
- package/dist/domain/entities/federation-node.d.ts +55 -0
- package/dist/domain/entities/federation-node.d.ts.map +1 -1
- package/dist/domain/entities/federation-node.js +86 -0
- package/dist/domain/entities/federation-node.js.map +1 -1
- package/dist/domain/value-objects/federation-budget.d.ts +89 -0
- package/dist/domain/value-objects/federation-budget.d.ts.map +1 -0
- package/dist/domain/value-objects/federation-budget.js +165 -0
- package/dist/domain/value-objects/federation-budget.js.map +1 -0
- package/dist/domain/value-objects/federation-node-state.d.ts +92 -0
- package/dist/domain/value-objects/federation-node-state.d.ts.map +1 -0
- package/dist/domain/value-objects/federation-node-state.js +100 -0
- package/dist/domain/value-objects/federation-node-state.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +98 -2
- package/dist/mcp-tools.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation budget + hop counter (ADR-097 Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Carried in {@link FederationEnvelope} alongside the existing
|
|
5
|
+
* sourceNodeId/targetNodeId/payload. Sender supplies the original limits;
|
|
6
|
+
* each outbound hop increments hopCount and decrements remaining budget.
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 enforces at the *send* side because there is no inbound
|
|
9
|
+
* dispatcher today (sendToNode is a stub — see researcher report).
|
|
10
|
+
* Phase 2 will move the decrement to receive once the inbound path lands.
|
|
11
|
+
*
|
|
12
|
+
* Security invariants pinned by the reviewer (audit_1776853149979 follow-up):
|
|
13
|
+
*
|
|
14
|
+
* 1. validateBudget rejects NaN, ±Infinity, negative numbers, and
|
|
15
|
+
* non-integer hop counts. Returns a discriminated result so callers
|
|
16
|
+
* cannot accidentally use a bad budget unchecked.
|
|
17
|
+
* 2. enforceBudget runs check-then-decrement in a single synchronous
|
|
18
|
+
* function with no awaits inside — two concurrent send calls cannot
|
|
19
|
+
* both pass the pre-check.
|
|
20
|
+
* 3. Errors return constant strings ("HOP_LIMIT_EXCEEDED" /
|
|
21
|
+
* "BUDGET_EXCEEDED") with no remaining-budget echo so a caller can't
|
|
22
|
+
* use the error response as an oracle for the threshold.
|
|
23
|
+
*/
|
|
24
|
+
/** Maximum hops a federation message may travel before the breaker opens. */
|
|
25
|
+
export declare const DEFAULT_MAX_HOPS = 8;
|
|
26
|
+
/**
|
|
27
|
+
* Caller-supplied budget. All fields are optional; omitted fields are
|
|
28
|
+
* treated as Infinity for the corresponding axis. Created via validateBudget;
|
|
29
|
+
* never construct directly.
|
|
30
|
+
*/
|
|
31
|
+
export interface Budget {
|
|
32
|
+
readonly maxHops: number;
|
|
33
|
+
readonly maxTokens: number;
|
|
34
|
+
readonly maxUsd: number;
|
|
35
|
+
}
|
|
36
|
+
/** Discriminated result of validating raw user input into a Budget. */
|
|
37
|
+
export type BudgetValidationResult = {
|
|
38
|
+
ok: true;
|
|
39
|
+
budget: Budget;
|
|
40
|
+
} | {
|
|
41
|
+
ok: false;
|
|
42
|
+
error: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Validate a raw budget object. Pure, no side effects. Caller obtains either
|
|
46
|
+
* a guaranteed-safe Budget or a structured error suitable for logging.
|
|
47
|
+
*
|
|
48
|
+
* Accepts undefined/null and returns a default unbounded budget (maxHops =
|
|
49
|
+
* DEFAULT_MAX_HOPS, tokens/usd = Infinity). This preserves backward
|
|
50
|
+
* compatibility for callers that don't pass a budget at all.
|
|
51
|
+
*/
|
|
52
|
+
export declare function validateBudget(raw: unknown, overrideMaxHops?: unknown): BudgetValidationResult;
|
|
53
|
+
/**
|
|
54
|
+
* Enforcement decision returned by enforceBudget. Discriminated so the
|
|
55
|
+
* caller can map directly to a constant-string error — no remaining-budget
|
|
56
|
+
* echo on the failure side (anti-oracle).
|
|
57
|
+
*/
|
|
58
|
+
export type BudgetEnforcement = {
|
|
59
|
+
ok: true;
|
|
60
|
+
nextHopCount: number;
|
|
61
|
+
remaining: Budget;
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
reason: 'HOP_LIMIT_EXCEEDED' | 'BUDGET_EXCEEDED';
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Atomic (single synchronous function, no internal awaits) hop check.
|
|
68
|
+
*
|
|
69
|
+
* Returns the hopCount value the OUTBOUND envelope should carry plus the
|
|
70
|
+
* remaining budget that downstream peers will see. Callers must use the
|
|
71
|
+
* returned values; mutating the input Budget is forbidden (it's readonly).
|
|
72
|
+
*
|
|
73
|
+
* Contract:
|
|
74
|
+
* - hopCount represents how many hops the message has already taken.
|
|
75
|
+
* - The next outbound hop increments it: nextHopCount = hopCount + 1.
|
|
76
|
+
* - The check fires AFTER increment: nextHopCount must be <= maxHops.
|
|
77
|
+
* - This means maxHops=0 refuses to forward at all, which matches the
|
|
78
|
+
* ADR's "no remote delegation allowed" semantic.
|
|
79
|
+
*
|
|
80
|
+
* tokensSpent / usdSpent are caller-reported actuals from the immediately
|
|
81
|
+
* preceding leg (or 0 on the originator). The check fires on the
|
|
82
|
+
* *post-spend* totals so a leg that overshoots is caught before the next
|
|
83
|
+
* outbound dispatch.
|
|
84
|
+
*/
|
|
85
|
+
export declare function enforceBudget(budget: Budget, hopCount: number, spent?: {
|
|
86
|
+
tokens: number;
|
|
87
|
+
usd: number;
|
|
88
|
+
}): BudgetEnforcement;
|
|
89
|
+
//# sourceMappingURL=federation-budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federation-budget.d.ts","sourceRoot":"","sources":["../../../src/domain/value-objects/federation-budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAQlC;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,uEAAuE;AACvE,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,EACZ,eAAe,CAAC,EAAE,OAAO,GACxB,sBAAsB,CA6DxB;AAkCD;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,oBAAoB,GAAG,iBAAiB,CAAA;CAAE,CAAC;AAEpE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAA0B,GAC7D,iBAAiB,CAuBnB"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation budget + hop counter (ADR-097 Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Carried in {@link FederationEnvelope} alongside the existing
|
|
5
|
+
* sourceNodeId/targetNodeId/payload. Sender supplies the original limits;
|
|
6
|
+
* each outbound hop increments hopCount and decrements remaining budget.
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 enforces at the *send* side because there is no inbound
|
|
9
|
+
* dispatcher today (sendToNode is a stub — see researcher report).
|
|
10
|
+
* Phase 2 will move the decrement to receive once the inbound path lands.
|
|
11
|
+
*
|
|
12
|
+
* Security invariants pinned by the reviewer (audit_1776853149979 follow-up):
|
|
13
|
+
*
|
|
14
|
+
* 1. validateBudget rejects NaN, ±Infinity, negative numbers, and
|
|
15
|
+
* non-integer hop counts. Returns a discriminated result so callers
|
|
16
|
+
* cannot accidentally use a bad budget unchecked.
|
|
17
|
+
* 2. enforceBudget runs check-then-decrement in a single synchronous
|
|
18
|
+
* function with no awaits inside — two concurrent send calls cannot
|
|
19
|
+
* both pass the pre-check.
|
|
20
|
+
* 3. Errors return constant strings ("HOP_LIMIT_EXCEEDED" /
|
|
21
|
+
* "BUDGET_EXCEEDED") with no remaining-budget echo so a caller can't
|
|
22
|
+
* use the error response as an oracle for the threshold.
|
|
23
|
+
*/
|
|
24
|
+
/** Maximum hops a federation message may travel before the breaker opens. */
|
|
25
|
+
export const DEFAULT_MAX_HOPS = 8;
|
|
26
|
+
/** Hard ceiling on caller-supplied maxUsd to prevent absurd integer math. */
|
|
27
|
+
const MAX_USD_CEILING = 1_000_000;
|
|
28
|
+
/** Hard ceiling on caller-supplied maxTokens. 1B tokens is more than any real session. */
|
|
29
|
+
const MAX_TOKENS_CEILING = 1_000_000_000;
|
|
30
|
+
/** Hard ceiling on hop count. */
|
|
31
|
+
const MAX_HOPS_CEILING = 64;
|
|
32
|
+
/**
|
|
33
|
+
* Validate a raw budget object. Pure, no side effects. Caller obtains either
|
|
34
|
+
* a guaranteed-safe Budget or a structured error suitable for logging.
|
|
35
|
+
*
|
|
36
|
+
* Accepts undefined/null and returns a default unbounded budget (maxHops =
|
|
37
|
+
* DEFAULT_MAX_HOPS, tokens/usd = Infinity). This preserves backward
|
|
38
|
+
* compatibility for callers that don't pass a budget at all.
|
|
39
|
+
*/
|
|
40
|
+
export function validateBudget(raw, overrideMaxHops) {
|
|
41
|
+
// Default unbounded budget when caller passes nothing.
|
|
42
|
+
if (raw == null && overrideMaxHops == null) {
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
budget: {
|
|
46
|
+
maxHops: DEFAULT_MAX_HOPS,
|
|
47
|
+
maxTokens: Number.POSITIVE_INFINITY,
|
|
48
|
+
maxUsd: Number.POSITIVE_INFINITY,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Caller passed a bare maxHops without a budget object — accept it as a
|
|
53
|
+
// hop-only budget. Useful for "no remote delegation allowed" via
|
|
54
|
+
// maxHops: 0.
|
|
55
|
+
if (raw == null && overrideMaxHops != null) {
|
|
56
|
+
const hopCheck = checkHops(overrideMaxHops);
|
|
57
|
+
if (!hopCheck.ok)
|
|
58
|
+
return hopCheck;
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
budget: {
|
|
62
|
+
maxHops: hopCheck.value,
|
|
63
|
+
maxTokens: Number.POSITIVE_INFINITY,
|
|
64
|
+
maxUsd: Number.POSITIVE_INFINITY,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
69
|
+
return { ok: false, error: 'budget must be an object' };
|
|
70
|
+
}
|
|
71
|
+
const obj = raw;
|
|
72
|
+
const hopsRaw = overrideMaxHops ?? obj.maxHops;
|
|
73
|
+
const hopCheck = hopsRaw == null
|
|
74
|
+
? { ok: true, value: DEFAULT_MAX_HOPS }
|
|
75
|
+
: checkHops(hopsRaw);
|
|
76
|
+
if (!hopCheck.ok)
|
|
77
|
+
return hopCheck;
|
|
78
|
+
const tokensCheck = obj.maxTokens == null
|
|
79
|
+
? { ok: true, value: Number.POSITIVE_INFINITY }
|
|
80
|
+
: checkBound(obj.maxTokens, MAX_TOKENS_CEILING, 'maxTokens', true);
|
|
81
|
+
if (!tokensCheck.ok)
|
|
82
|
+
return tokensCheck;
|
|
83
|
+
const usdCheck = obj.maxUsd == null
|
|
84
|
+
? { ok: true, value: Number.POSITIVE_INFINITY }
|
|
85
|
+
: checkBound(obj.maxUsd, MAX_USD_CEILING, 'maxUsd', false);
|
|
86
|
+
if (!usdCheck.ok)
|
|
87
|
+
return usdCheck;
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
budget: {
|
|
91
|
+
maxHops: hopCheck.value,
|
|
92
|
+
maxTokens: tokensCheck.value,
|
|
93
|
+
maxUsd: usdCheck.value,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function checkHops(v) {
|
|
98
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || !Number.isInteger(v)) {
|
|
99
|
+
return { ok: false, error: 'maxHops must be a finite integer' };
|
|
100
|
+
}
|
|
101
|
+
if (v < 0)
|
|
102
|
+
return { ok: false, error: 'maxHops must be >= 0' };
|
|
103
|
+
if (v > MAX_HOPS_CEILING) {
|
|
104
|
+
return { ok: false, error: `maxHops exceeds ceiling (${MAX_HOPS_CEILING})` };
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, value: v };
|
|
107
|
+
}
|
|
108
|
+
function checkBound(v, ceiling, label, requireInteger) {
|
|
109
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
110
|
+
return { ok: false, error: `${label} must be a finite number` };
|
|
111
|
+
}
|
|
112
|
+
if (requireInteger && !Number.isInteger(v)) {
|
|
113
|
+
return { ok: false, error: `${label} must be an integer` };
|
|
114
|
+
}
|
|
115
|
+
if (v < 0)
|
|
116
|
+
return { ok: false, error: `${label} must be >= 0` };
|
|
117
|
+
if (v > ceiling) {
|
|
118
|
+
return { ok: false, error: `${label} exceeds ceiling (${ceiling})` };
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, value: v };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Atomic (single synchronous function, no internal awaits) hop check.
|
|
124
|
+
*
|
|
125
|
+
* Returns the hopCount value the OUTBOUND envelope should carry plus the
|
|
126
|
+
* remaining budget that downstream peers will see. Callers must use the
|
|
127
|
+
* returned values; mutating the input Budget is forbidden (it's readonly).
|
|
128
|
+
*
|
|
129
|
+
* Contract:
|
|
130
|
+
* - hopCount represents how many hops the message has already taken.
|
|
131
|
+
* - The next outbound hop increments it: nextHopCount = hopCount + 1.
|
|
132
|
+
* - The check fires AFTER increment: nextHopCount must be <= maxHops.
|
|
133
|
+
* - This means maxHops=0 refuses to forward at all, which matches the
|
|
134
|
+
* ADR's "no remote delegation allowed" semantic.
|
|
135
|
+
*
|
|
136
|
+
* tokensSpent / usdSpent are caller-reported actuals from the immediately
|
|
137
|
+
* preceding leg (or 0 on the originator). The check fires on the
|
|
138
|
+
* *post-spend* totals so a leg that overshoots is caught before the next
|
|
139
|
+
* outbound dispatch.
|
|
140
|
+
*/
|
|
141
|
+
export function enforceBudget(budget, hopCount, spent = { tokens: 0, usd: 0 }) {
|
|
142
|
+
const nextHopCount = hopCount + 1;
|
|
143
|
+
if (nextHopCount > budget.maxHops) {
|
|
144
|
+
return { ok: false, reason: 'HOP_LIMIT_EXCEEDED' };
|
|
145
|
+
}
|
|
146
|
+
// Negative spend is a programming error; treat as 0 rather than refunding
|
|
147
|
+
// the budget (which would let a malicious caller inflate remaining).
|
|
148
|
+
const ts = Math.max(0, spent.tokens);
|
|
149
|
+
const us = Math.max(0, spent.usd);
|
|
150
|
+
const remainingTokens = budget.maxTokens - ts;
|
|
151
|
+
const remainingUsd = budget.maxUsd - us;
|
|
152
|
+
if (remainingTokens < 0 || remainingUsd < 0) {
|
|
153
|
+
return { ok: false, reason: 'BUDGET_EXCEEDED' };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
nextHopCount,
|
|
158
|
+
remaining: {
|
|
159
|
+
maxHops: budget.maxHops,
|
|
160
|
+
maxTokens: remainingTokens,
|
|
161
|
+
maxUsd: remainingUsd,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=federation-budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federation-budget.js","sourceRoot":"","sources":["../../../src/domain/value-objects/federation-budget.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation peer state machine (ADR-097 Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 shipped the budget envelope + enforcer that travels with every
|
|
5
|
+
* outbound message. Phase 2 adds the per-peer breaker: when a peer crosses a
|
|
6
|
+
* cost or failure threshold, transition it to SUSPENDED so subsequent
|
|
7
|
+
* `federation_send` calls short-circuit. After a cooldown the peer can
|
|
8
|
+
* recover; if it stays bad the breaker escalates to EVICTED.
|
|
9
|
+
*
|
|
10
|
+
* This module is the *value object* layer — pure, no I/O. The breaker
|
|
11
|
+
* service (Phase 2.b, separate file) reads cost-tracker telemetry, evaluates
|
|
12
|
+
* thresholds, and calls `transition` on the FederationNode entity. Decoupling
|
|
13
|
+
* the state machine from the policy lets tests pin transitions without
|
|
14
|
+
* standing up the cost bus.
|
|
15
|
+
*
|
|
16
|
+
* Security invariants pinned by the spec:
|
|
17
|
+
*
|
|
18
|
+
* 1. EVICTED is terminal under normal flow. The only way out is an
|
|
19
|
+
* explicit `reactivate` call (operator-initiated). This prevents a
|
|
20
|
+
* misbehaving peer from auto-recovering through the cooldown.
|
|
21
|
+
* 2. canTransition is the single source of truth for legality. Any path
|
|
22
|
+
* that mutates state MUST consult it; direct field writes are not
|
|
23
|
+
* permitted (entity getter/setter shape enforces this).
|
|
24
|
+
* 3. Reasons are constant strings, no remaining-budget echo on failure
|
|
25
|
+
* (matches Phase 1's anti-oracle posture).
|
|
26
|
+
* 4. Default cooldown is conservative (30 min) so a transient spike
|
|
27
|
+
* cannot ping-pong a peer ACTIVE↔SUSPENDED at high frequency.
|
|
28
|
+
*/
|
|
29
|
+
/** Lifecycle state of a federation peer. */
|
|
30
|
+
export declare enum FederationNodeState {
|
|
31
|
+
/** Default; healthy. `federation_send` accepts deliveries. */
|
|
32
|
+
ACTIVE = "ACTIVE",
|
|
33
|
+
/** Breaker tripped. Sends short-circuit with PEER_SUSPENDED. */
|
|
34
|
+
SUSPENDED = "SUSPENDED",
|
|
35
|
+
/** Removed from active rotation. Sends short-circuit with PEER_EVICTED. */
|
|
36
|
+
EVICTED = "EVICTED"
|
|
37
|
+
}
|
|
38
|
+
/** Reason a peer was suspended/evicted. Constant strings — no echo of caps. */
|
|
39
|
+
export type SuspensionReason = 'COST_THRESHOLD_EXCEEDED' | 'FAILURE_RATIO_EXCEEDED' | 'MANUAL_SUSPEND' | 'GRACE_PERIOD_EXPIRED' | 'MANUAL_EVICT';
|
|
40
|
+
/**
|
|
41
|
+
* Default cooldown a SUSPENDED peer must wait before being eligible for
|
|
42
|
+
* SUSPENDED → ACTIVE. 30 minutes matches the ADR-097 Part 2 spec and is
|
|
43
|
+
* intentionally well past the prompt-cache window so a recovery probe is
|
|
44
|
+
* cheap to amortize.
|
|
45
|
+
*/
|
|
46
|
+
export declare const DEFAULT_SUSPENSION_COOLDOWN_MS: number;
|
|
47
|
+
/**
|
|
48
|
+
* Default age at which a continuously-SUSPENDED peer is auto-escalated to
|
|
49
|
+
* EVICTED. 24 hours per spec — long enough to ride out a cost-tracker
|
|
50
|
+
* outage without false eviction, short enough that a genuinely-broken peer
|
|
51
|
+
* doesn't linger forever.
|
|
52
|
+
*/
|
|
53
|
+
export declare const DEFAULT_AUTO_EVICTION_AGE_MS: number;
|
|
54
|
+
/** A successful transition's reason payload (for the audit trail). */
|
|
55
|
+
export interface TransitionReason {
|
|
56
|
+
readonly reason: SuspensionReason;
|
|
57
|
+
/** Optional opaque correlation key — task ID, breaker run ID, etc. */
|
|
58
|
+
readonly correlationId?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Static legality table for state transitions. Returns true iff the
|
|
62
|
+
* `from -> to` edge is in the allowed set. Used by the entity to reject
|
|
63
|
+
* illegal mutations before applying them.
|
|
64
|
+
*
|
|
65
|
+
* Allowed edges:
|
|
66
|
+
* ACTIVE → SUSPENDED (breaker trips)
|
|
67
|
+
* SUSPENDED → ACTIVE (cooldown + probe success)
|
|
68
|
+
* SUSPENDED → EVICTED (grace expired, or manual evict)
|
|
69
|
+
* ACTIVE → EVICTED (manual evict only — skips suspend)
|
|
70
|
+
* EVICTED → ACTIVE (manual reactivate only)
|
|
71
|
+
*
|
|
72
|
+
* Disallowed (anything else, including self-loops, is a no-op error).
|
|
73
|
+
*/
|
|
74
|
+
export declare function canTransition(from: FederationNodeState, to: FederationNodeState): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Should a SUSPENDED peer be eligible for SUSPENDED → ACTIVE based purely
|
|
77
|
+
* on elapsed time? The breaker still has to confirm health via probe before
|
|
78
|
+
* it actually calls reactivate; this just answers "is the cooldown done?"
|
|
79
|
+
*
|
|
80
|
+
* Pure function of (suspendedAt, now, cooldownMs) so it's trivially
|
|
81
|
+
* deterministic in tests.
|
|
82
|
+
*/
|
|
83
|
+
export declare function isCooldownElapsed(suspendedAt: Date, now: Date, cooldownMs?: number): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Should a SUSPENDED peer be auto-escalated to EVICTED based on the age of
|
|
86
|
+
* its suspension? Returns true once the peer has been continuously
|
|
87
|
+
* SUSPENDED past the configured limit (default 24h). The breaker calls
|
|
88
|
+
* this on each tick and, if true, issues the SUSPENDED → EVICTED
|
|
89
|
+
* transition with reason GRACE_PERIOD_EXPIRED.
|
|
90
|
+
*/
|
|
91
|
+
export declare function shouldAutoEvict(suspendedAt: Date, now: Date, ageLimitMs?: number): boolean;
|
|
92
|
+
//# sourceMappingURL=federation-node-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federation-node-state.d.ts","sourceRoot":"","sources":["../../../src/domain/value-objects/federation-node-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,4CAA4C;AAC5C,oBAAY,mBAAmB;IAC7B,8DAA8D;IAC9D,MAAM,WAAW;IACjB,gEAAgE;IAChE,SAAS,cAAc;IACvB,2EAA2E;IAC3E,OAAO,YAAY;CACpB;AAED,+EAA+E;AAC/E,MAAM,MAAM,gBAAgB,GACxB,yBAAyB,GACzB,wBAAwB,GACxB,gBAAgB,GAChB,sBAAsB,GACtB,cAAc,CAAC;AAEnB;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,QAAiB,CAAC;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,QAAsB,CAAC;AAEhE,sEAAsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,mBAAmB,EACzB,EAAE,EAAE,mBAAmB,GACtB,OAAO,CAUT;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,IAAI,EACjB,GAAG,EAAE,IAAI,EACT,UAAU,GAAE,MAAuC,GAClD,OAAO,CAET;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,IAAI,EACjB,GAAG,EAAE,IAAI,EACT,UAAU,GAAE,MAAqC,GAChD,OAAO,CAET"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation peer state machine (ADR-097 Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 shipped the budget envelope + enforcer that travels with every
|
|
5
|
+
* outbound message. Phase 2 adds the per-peer breaker: when a peer crosses a
|
|
6
|
+
* cost or failure threshold, transition it to SUSPENDED so subsequent
|
|
7
|
+
* `federation_send` calls short-circuit. After a cooldown the peer can
|
|
8
|
+
* recover; if it stays bad the breaker escalates to EVICTED.
|
|
9
|
+
*
|
|
10
|
+
* This module is the *value object* layer — pure, no I/O. The breaker
|
|
11
|
+
* service (Phase 2.b, separate file) reads cost-tracker telemetry, evaluates
|
|
12
|
+
* thresholds, and calls `transition` on the FederationNode entity. Decoupling
|
|
13
|
+
* the state machine from the policy lets tests pin transitions without
|
|
14
|
+
* standing up the cost bus.
|
|
15
|
+
*
|
|
16
|
+
* Security invariants pinned by the spec:
|
|
17
|
+
*
|
|
18
|
+
* 1. EVICTED is terminal under normal flow. The only way out is an
|
|
19
|
+
* explicit `reactivate` call (operator-initiated). This prevents a
|
|
20
|
+
* misbehaving peer from auto-recovering through the cooldown.
|
|
21
|
+
* 2. canTransition is the single source of truth for legality. Any path
|
|
22
|
+
* that mutates state MUST consult it; direct field writes are not
|
|
23
|
+
* permitted (entity getter/setter shape enforces this).
|
|
24
|
+
* 3. Reasons are constant strings, no remaining-budget echo on failure
|
|
25
|
+
* (matches Phase 1's anti-oracle posture).
|
|
26
|
+
* 4. Default cooldown is conservative (30 min) so a transient spike
|
|
27
|
+
* cannot ping-pong a peer ACTIVE↔SUSPENDED at high frequency.
|
|
28
|
+
*/
|
|
29
|
+
/** Lifecycle state of a federation peer. */
|
|
30
|
+
export var FederationNodeState;
|
|
31
|
+
(function (FederationNodeState) {
|
|
32
|
+
/** Default; healthy. `federation_send` accepts deliveries. */
|
|
33
|
+
FederationNodeState["ACTIVE"] = "ACTIVE";
|
|
34
|
+
/** Breaker tripped. Sends short-circuit with PEER_SUSPENDED. */
|
|
35
|
+
FederationNodeState["SUSPENDED"] = "SUSPENDED";
|
|
36
|
+
/** Removed from active rotation. Sends short-circuit with PEER_EVICTED. */
|
|
37
|
+
FederationNodeState["EVICTED"] = "EVICTED";
|
|
38
|
+
})(FederationNodeState || (FederationNodeState = {}));
|
|
39
|
+
/**
|
|
40
|
+
* Default cooldown a SUSPENDED peer must wait before being eligible for
|
|
41
|
+
* SUSPENDED → ACTIVE. 30 minutes matches the ADR-097 Part 2 spec and is
|
|
42
|
+
* intentionally well past the prompt-cache window so a recovery probe is
|
|
43
|
+
* cheap to amortize.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_SUSPENSION_COOLDOWN_MS = 30 * 60 * 1000;
|
|
46
|
+
/**
|
|
47
|
+
* Default age at which a continuously-SUSPENDED peer is auto-escalated to
|
|
48
|
+
* EVICTED. 24 hours per spec — long enough to ride out a cost-tracker
|
|
49
|
+
* outage without false eviction, short enough that a genuinely-broken peer
|
|
50
|
+
* doesn't linger forever.
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_AUTO_EVICTION_AGE_MS = 24 * 60 * 60 * 1000;
|
|
53
|
+
/**
|
|
54
|
+
* Static legality table for state transitions. Returns true iff the
|
|
55
|
+
* `from -> to` edge is in the allowed set. Used by the entity to reject
|
|
56
|
+
* illegal mutations before applying them.
|
|
57
|
+
*
|
|
58
|
+
* Allowed edges:
|
|
59
|
+
* ACTIVE → SUSPENDED (breaker trips)
|
|
60
|
+
* SUSPENDED → ACTIVE (cooldown + probe success)
|
|
61
|
+
* SUSPENDED → EVICTED (grace expired, or manual evict)
|
|
62
|
+
* ACTIVE → EVICTED (manual evict only — skips suspend)
|
|
63
|
+
* EVICTED → ACTIVE (manual reactivate only)
|
|
64
|
+
*
|
|
65
|
+
* Disallowed (anything else, including self-loops, is a no-op error).
|
|
66
|
+
*/
|
|
67
|
+
export function canTransition(from, to) {
|
|
68
|
+
if (from === to)
|
|
69
|
+
return false;
|
|
70
|
+
if (from === FederationNodeState.ACTIVE) {
|
|
71
|
+
return to === FederationNodeState.SUSPENDED || to === FederationNodeState.EVICTED;
|
|
72
|
+
}
|
|
73
|
+
if (from === FederationNodeState.SUSPENDED) {
|
|
74
|
+
return to === FederationNodeState.ACTIVE || to === FederationNodeState.EVICTED;
|
|
75
|
+
}
|
|
76
|
+
// EVICTED is terminal except for explicit operator reactivate.
|
|
77
|
+
return to === FederationNodeState.ACTIVE;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Should a SUSPENDED peer be eligible for SUSPENDED → ACTIVE based purely
|
|
81
|
+
* on elapsed time? The breaker still has to confirm health via probe before
|
|
82
|
+
* it actually calls reactivate; this just answers "is the cooldown done?"
|
|
83
|
+
*
|
|
84
|
+
* Pure function of (suspendedAt, now, cooldownMs) so it's trivially
|
|
85
|
+
* deterministic in tests.
|
|
86
|
+
*/
|
|
87
|
+
export function isCooldownElapsed(suspendedAt, now, cooldownMs = DEFAULT_SUSPENSION_COOLDOWN_MS) {
|
|
88
|
+
return now.getTime() - suspendedAt.getTime() >= cooldownMs;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Should a SUSPENDED peer be auto-escalated to EVICTED based on the age of
|
|
92
|
+
* its suspension? Returns true once the peer has been continuously
|
|
93
|
+
* SUSPENDED past the configured limit (default 24h). The breaker calls
|
|
94
|
+
* this on each tick and, if true, issues the SUSPENDED → EVICTED
|
|
95
|
+
* transition with reason GRACE_PERIOD_EXPIRED.
|
|
96
|
+
*/
|
|
97
|
+
export function shouldAutoEvict(suspendedAt, now, ageLimitMs = DEFAULT_AUTO_EVICTION_AGE_MS) {
|
|
98
|
+
return now.getTime() - suspendedAt.getTime() >= ageLimitMs;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=federation-node-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federation-node-state.js","sourceRoot":"","sources":["../../../src/domain/value-objects/federation-node-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,4CAA4C;AAC5C,MAAM,CAAN,IAAY,mBAOX;AAPD,WAAY,mBAAmB;IAC7B,8DAA8D;IAC9D,wCAAiB,CAAA;IACjB,gEAAgE;IAChE,8CAAuB,CAAA;IACvB,2EAA2E;IAC3E,0CAAmB,CAAA;AACrB,CAAC,EAPW,mBAAmB,KAAnB,mBAAmB,QAO9B;AAUD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE7D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAShE;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAyB,EACzB,EAAuB;IAEvB,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAC9B,IAAI,IAAI,KAAK,mBAAmB,CAAC,MAAM,EAAE,CAAC;QACxC,OAAO,EAAE,KAAK,mBAAmB,CAAC,SAAS,IAAI,EAAE,KAAK,mBAAmB,CAAC,OAAO,CAAC;IACpF,CAAC;IACD,IAAI,IAAI,KAAK,mBAAmB,CAAC,SAAS,EAAE,CAAC;QAC3C,OAAO,EAAE,KAAK,mBAAmB,CAAC,MAAM,IAAI,EAAE,KAAK,mBAAmB,CAAC,OAAO,CAAC;IACjF,CAAC;IACD,+DAA+D;IAC/D,OAAO,EAAE,KAAK,mBAAmB,CAAC,MAAM,CAAC;AAC3C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,WAAiB,EACjB,GAAS,EACT,aAAqB,8BAA8B;IAEnD,OAAO,GAAG,CAAC,OAAO,EAAE,GAAG,WAAW,CAAC,OAAO,EAAE,IAAI,UAAU,CAAC;AAC7D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,WAAiB,EACjB,GAAS,EACT,aAAqB,4BAA4B;IAEjD,OAAO,GAAG,CAAC,OAAO,EAAE,GAAG,WAAW,CAAC,OAAO,EAAE,IAAI,UAAU,CAAC;AAC7D,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { AgentFederationPlugin } from './plugin.js';
|
|
2
|
-
export { FederationNode, type FederationNodeProps, type FederationNodeCapabilities, type FederationNodeMetadata } from './domain/entities/federation-node.js';
|
|
2
|
+
export { FederationNode, type FederationNodeProps, type FederationNodeCapabilities, type FederationNodeMetadata, type FederationNodeStateRecord } from './domain/entities/federation-node.js';
|
|
3
|
+
export { FederationNodeState, type SuspensionReason, type TransitionReason, canTransition, isCooldownElapsed, shouldAutoEvict, DEFAULT_SUSPENSION_COOLDOWN_MS, DEFAULT_AUTO_EVICTION_AGE_MS, } from './domain/value-objects/federation-node-state.js';
|
|
4
|
+
export { FederationBreakerService, evaluatePolicy, DEFAULT_BREAKER_POLICY, DEFAULT_MAX_SAMPLES_PER_PEER, type BreakerPolicy, type BreakerDecision, type SendOutcome, } from './application/federation-breaker-service.js';
|
|
3
5
|
export { FederationSession, type FederationSessionProps, type SessionMetrics } from './domain/entities/federation-session.js';
|
|
4
6
|
export { FederationEnvelope, type FederationEnvelopeProps, type FederationMessageType, type PIIScanResult, type PIIScanDetection, type PIIScanAction, CONSENSUS_REQUIRED_TYPES, } from './domain/entities/federation-envelope.js';
|
|
5
7
|
export { TrustLevel, TRUST_TRANSITION_THRESHOLDS, CAPABILITY_GATES, isOperationAllowed, getTrustLevelLabel, type TrustTransitionThreshold, } from './domain/entities/trust-level.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,KAAK,0BAA0B,EAAE,KAAK,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,KAAK,0BAA0B,EAAE,KAAK,sBAAsB,EAAE,KAAK,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AAC9L,OAAO,EACL,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,8BAA8B,EAC9B,4BAA4B,GAC7B,MAAM,iDAAiD,CAAC;AACzD,OAAO,EACL,wBAAwB,EACxB,cAAc,EACd,sBAAsB,EACtB,4BAA4B,EAC5B,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,WAAW,GACjB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,KAAK,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAC9H,OAAO,EACL,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,wBAAwB,GACzB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EACL,UAAU,EACV,2BAA2B,EAC3B,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,wBAAwB,GAC9B,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,kBAAkB,EAClB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,GAC5B,MAAM,2CAA2C,CAAC;AACnD,OAAO,EACL,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAC/B,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,GACxB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EACL,cAAc,EACd,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,GACxB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,qBAAqB,EACrB,KAAK,2BAA2B,EAChC,KAAK,gBAAgB,GACtB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,GACtB,MAAM,gCAAgC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { AgentFederationPlugin } from './plugin.js';
|
|
2
2
|
export { FederationNode } from './domain/entities/federation-node.js';
|
|
3
|
+
export { FederationNodeState, canTransition, isCooldownElapsed, shouldAutoEvict, DEFAULT_SUSPENSION_COOLDOWN_MS, DEFAULT_AUTO_EVICTION_AGE_MS, } from './domain/value-objects/federation-node-state.js';
|
|
4
|
+
export { FederationBreakerService, evaluatePolicy, DEFAULT_BREAKER_POLICY, DEFAULT_MAX_SAMPLES_PER_PEER, } from './application/federation-breaker-service.js';
|
|
3
5
|
export { FederationSession } from './domain/entities/federation-session.js';
|
|
4
6
|
export { FederationEnvelope, CONSENSUS_REQUIRED_TYPES, } from './domain/entities/federation-envelope.js';
|
|
5
7
|
export { TrustLevel, TRUST_TRANSITION_THRESHOLDS, CAPABILITY_GATES, isOperationAllowed, getTrustLevelLabel, } from './domain/entities/trust-level.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,EAAE,cAAc,EAA0H,MAAM,sCAAsC,CAAC;AAC9L,OAAO,EACL,mBAAmB,EAGnB,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,8BAA8B,EAC9B,4BAA4B,GAC7B,MAAM,iDAAiD,CAAC;AACzD,OAAO,EACL,wBAAwB,EACxB,cAAc,EACd,sBAAsB,EACtB,4BAA4B,GAI7B,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAoD,MAAM,yCAAyC,CAAC;AAC9H,OAAO,EACL,kBAAkB,EAMlB,wBAAwB,GACzB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EACL,UAAU,EACV,2BAA2B,EAC3B,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GAEnB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,kBAAkB,GASnB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EACL,gBAAgB,GAKjB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,gBAAgB,GAMjB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,cAAc,GAKf,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,YAAY,GAUb,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EACL,cAAc,GAKf,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,qBAAqB,GAGtB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,YAAY,GAKb,MAAM,gCAAgC,CAAC"}
|
package/dist/mcp-tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-tools.d.ts","sourceRoot":"","sources":["../src/mcp-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6CAA6C,CAAC;AACjF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAGrF,KAAK,iBAAiB,GAAG,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC5D,KAAK,aAAa,GAAG,MAAM,aAAa,GAAG,IAAI,CAAC;AAMhD,wBAAgB,cAAc,CAC5B,cAAc,EAAE,iBAAiB,EACjC,UAAU,EAAE,aAAa,GACxB,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"mcp-tools.d.ts","sourceRoot":"","sources":["../src/mcp-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6CAA6C,CAAC;AACjF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAGrF,KAAK,iBAAiB,GAAG,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC5D,KAAK,aAAa,GAAG,MAAM,aAAa,GAAG,IAAI,CAAC;AAMhD,wBAAgB,cAAc,CAC5B,cAAc,EAAE,iBAAiB,EACjC,UAAU,EAAE,aAAa,GACxB,iBAAiB,EAAE,CAsUrB"}
|
package/dist/mcp-tools.js
CHANGED
|
@@ -77,7 +77,7 @@ export function createMcpTools(getCoordinator, getContext) {
|
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
name: 'federation_send',
|
|
80
|
-
description: 'Send a message to a federated peer through the PII pipeline and security gates',
|
|
80
|
+
description: 'Send a message to a federated peer through the PII pipeline and security gates. Optional budget controls (ADR-097): maxHops defaults to 8 to prevent recursive delegation; maxTokens/maxUsd cap cumulative spend across the hop chain.',
|
|
81
81
|
pluginName: '@claude-flow/plugin-agent-federation',
|
|
82
82
|
version: '1.0.0-alpha.1',
|
|
83
83
|
inputSchema: {
|
|
@@ -86,12 +86,44 @@ export function createMcpTools(getCoordinator, getContext) {
|
|
|
86
86
|
targetNodeId: { type: 'string', description: 'Target node ID' },
|
|
87
87
|
messageType: { type: 'string', description: 'Message type (task-assignment, memory-query, context-share, etc.)' },
|
|
88
88
|
payload: { type: 'object', description: 'Message payload' },
|
|
89
|
+
// ADR-097 Phase 1: optional budget envelope. Backward compatible —
|
|
90
|
+
// omitting these preserves the legacy unbounded-tokens/USD path,
|
|
91
|
+
// with maxHops still defaulting to 8 to defang recursion loops.
|
|
92
|
+
budget: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
description: 'Optional cumulative spend budget for this delegation chain',
|
|
95
|
+
properties: {
|
|
96
|
+
maxTokens: { type: 'number', description: 'Hard cap on Σ tokens across hops' },
|
|
97
|
+
maxUsd: { type: 'number', description: 'Hard cap on Σ USD spent' },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
maxHops: {
|
|
101
|
+
type: 'number',
|
|
102
|
+
description: 'Maximum hops this message may travel (default 8, 0 disallows remote delegation)',
|
|
103
|
+
},
|
|
104
|
+
hopCount: {
|
|
105
|
+
type: 'number',
|
|
106
|
+
description: 'How many hops this message has already taken (0 on the originator)',
|
|
107
|
+
},
|
|
108
|
+
spent: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
description: 'Caller-reported usage from previous legs (cumulative)',
|
|
111
|
+
properties: {
|
|
112
|
+
tokens: { type: 'number' },
|
|
113
|
+
usd: { type: 'number' },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
89
116
|
},
|
|
90
117
|
required: ['targetNodeId', 'messageType', 'payload'],
|
|
91
118
|
},
|
|
92
119
|
handler: async (params) => {
|
|
93
120
|
const coordinator = requireCoordinator();
|
|
94
|
-
const result = await coordinator.sendMessage(params['targetNodeId'], params['messageType'], params['payload']
|
|
121
|
+
const result = await coordinator.sendMessage(params['targetNodeId'], params['messageType'], params['payload'], {
|
|
122
|
+
budget: params['budget'],
|
|
123
|
+
maxHops: params['maxHops'],
|
|
124
|
+
hopCount: params['hopCount'],
|
|
125
|
+
spent: params['spent'],
|
|
126
|
+
});
|
|
95
127
|
return textResult(JSON.stringify(result, null, 2), !result.success);
|
|
96
128
|
},
|
|
97
129
|
},
|
|
@@ -182,6 +214,70 @@ export function createMcpTools(getCoordinator, getContext) {
|
|
|
182
214
|
return textResult(JSON.stringify(events, null, 2));
|
|
183
215
|
},
|
|
184
216
|
},
|
|
217
|
+
{
|
|
218
|
+
name: 'federation_breaker_status',
|
|
219
|
+
description: 'ADR-097 Phase 4: per-peer circuit-breaker state snapshot. Returns each known peer with its lifecycle state (ACTIVE/SUSPENDED/EVICTED), when it changed, and why. Combine with federation_evict / federation_reactivate to operate the breaker manually.',
|
|
220
|
+
pluginName: '@claude-flow/plugin-agent-federation',
|
|
221
|
+
version: '1.0.0-alpha.1',
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: 'object',
|
|
224
|
+
properties: {
|
|
225
|
+
nodeId: { type: 'string', description: 'Optional: filter to a single peer' },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
handler: async (params) => {
|
|
229
|
+
const coordinator = requireCoordinator();
|
|
230
|
+
const states = coordinator.getPeerStates();
|
|
231
|
+
const filter = params['nodeId'];
|
|
232
|
+
const filtered = filter ? states.filter((s) => s.nodeId === filter) : states;
|
|
233
|
+
const counts = coordinator.getPeerStateCounts();
|
|
234
|
+
return textResult(JSON.stringify({ counts, peers: filtered }, null, 2));
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'federation_evict',
|
|
239
|
+
description: 'ADR-097 Phase 4: operator-initiated evict for a peer. Marks the peer EVICTED so subsequent federation_send calls short-circuit with PEER_EVICTED. Reversible only via federation_reactivate (operator override).',
|
|
240
|
+
pluginName: '@claude-flow/plugin-agent-federation',
|
|
241
|
+
version: '1.0.0-alpha.1',
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {
|
|
245
|
+
nodeId: { type: 'string', description: 'Target peer to evict' },
|
|
246
|
+
correlationId: { type: 'string', description: 'Optional audit-trail correlation key (operator ticket, etc.)' },
|
|
247
|
+
},
|
|
248
|
+
required: ['nodeId'],
|
|
249
|
+
},
|
|
250
|
+
handler: async (params) => {
|
|
251
|
+
const coordinator = requireCoordinator();
|
|
252
|
+
const ok = await coordinator.evictPeer(params['nodeId'], 'MANUAL_EVICT', params['correlationId']);
|
|
253
|
+
const msg = ok
|
|
254
|
+
? `Evicted peer ${params['nodeId']}`
|
|
255
|
+
: `No transition (peer unknown or already EVICTED): ${params['nodeId']}`;
|
|
256
|
+
return textResult(msg, !ok);
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'federation_reactivate',
|
|
261
|
+
description: 'ADR-097 Phase 4: operator-initiated reactivate for a SUSPENDED or EVICTED peer. Used after a health probe confirms recovery, or as an operator override on a manual evict. The breaker does NOT auto-reactivate; this MCP tool is the explicit lever.',
|
|
262
|
+
pluginName: '@claude-flow/plugin-agent-federation',
|
|
263
|
+
version: '1.0.0-alpha.1',
|
|
264
|
+
inputSchema: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
nodeId: { type: 'string', description: 'Target peer to reactivate' },
|
|
268
|
+
correlationId: { type: 'string', description: 'Optional audit-trail correlation key (probe ID, ticket, etc.)' },
|
|
269
|
+
},
|
|
270
|
+
required: ['nodeId'],
|
|
271
|
+
},
|
|
272
|
+
handler: async (params) => {
|
|
273
|
+
const coordinator = requireCoordinator();
|
|
274
|
+
const ok = await coordinator.reactivatePeer(params['nodeId'], params['correlationId']);
|
|
275
|
+
const msg = ok
|
|
276
|
+
? `Reactivated peer ${params['nodeId']}`
|
|
277
|
+
: `No transition (peer unknown or already ACTIVE): ${params['nodeId']}`;
|
|
278
|
+
return textResult(msg, !ok);
|
|
279
|
+
},
|
|
280
|
+
},
|
|
185
281
|
{
|
|
186
282
|
name: 'federation_consensus',
|
|
187
283
|
description: 'Propose a federated consensus operation across all active peers',
|