@relayplane/proxy 1.9.25 → 1.9.27
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/kill-switch.d.ts +83 -0
- package/dist/kill-switch.d.ts.map +1 -0
- package/dist/kill-switch.js +208 -0
- package/dist/kill-switch.js.map +1 -0
- package/dist/spec-match-plugin.d.ts +99 -0
- package/dist/spec-match-plugin.d.ts.map +1 -0
- package/dist/spec-match-plugin.js +206 -0
- package/dist/spec-match-plugin.js.map +1 -0
- package/dist/tenant-isolation.d.ts +121 -0
- package/dist/tenant-isolation.d.ts.map +1 -0
- package/dist/tenant-isolation.js +272 -0
- package/dist/tenant-isolation.js.map +1 -0
- package/dist/tool-search-semantic.d.ts +12 -0
- package/dist/tool-search-semantic.d.ts.map +1 -0
- package/dist/tool-search-semantic.js +52 -0
- package/dist/tool-search-semantic.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kill-Switch API
|
|
3
|
+
*
|
|
4
|
+
* Instantly halts ALL traffic for a specific tenant. Designed for:
|
|
5
|
+
* - Runaway agent loops detected by anomaly monitoring
|
|
6
|
+
* - Billing emergencies (unexpected spend spike)
|
|
7
|
+
* - Security incidents requiring immediate isolation
|
|
8
|
+
* - Client-requested suspension
|
|
9
|
+
*
|
|
10
|
+
* The kill-switch is checked in-memory first (O(1), no disk I/O) on every
|
|
11
|
+
* request so it activates within a single request cycle — effectively
|
|
12
|
+
* instantaneous. The flag is also persisted to disk so it survives proxy
|
|
13
|
+
* restarts.
|
|
14
|
+
*
|
|
15
|
+
* HTTP API (served by the proxy server):
|
|
16
|
+
* POST /v1/tenants/:tenantId/kill { reason?: string } → activates
|
|
17
|
+
* DELETE /v1/tenants/:tenantId/kill → lifts
|
|
18
|
+
* GET /v1/tenants/:tenantId/kill → status
|
|
19
|
+
*/
|
|
20
|
+
export interface KillSwitchEntry {
|
|
21
|
+
tenant_id: string;
|
|
22
|
+
active: boolean;
|
|
23
|
+
reason?: string;
|
|
24
|
+
activated_at?: string;
|
|
25
|
+
activated_by?: string;
|
|
26
|
+
lifted_at?: string;
|
|
27
|
+
lifted_by?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface KillSwitchStore {
|
|
30
|
+
entries: Record<string, KillSwitchEntry>;
|
|
31
|
+
updated_at: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ActivateOptions {
|
|
34
|
+
reason?: string;
|
|
35
|
+
activated_by?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface LiftOptions {
|
|
38
|
+
lifted_by?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* KillSwitchManager provides instant tenant traffic halting with persistence.
|
|
42
|
+
*
|
|
43
|
+
* Thread-safety note: this is single-process Node.js, so the in-memory Set
|
|
44
|
+
* is the authoritative source of truth for the hot path. Disk writes are
|
|
45
|
+
* fire-and-forget for persistence across restarts.
|
|
46
|
+
*/
|
|
47
|
+
export declare class KillSwitchManager {
|
|
48
|
+
private storePath;
|
|
49
|
+
private activeKillSwitches;
|
|
50
|
+
private entries;
|
|
51
|
+
constructor(storePath?: string);
|
|
52
|
+
private load;
|
|
53
|
+
private persist;
|
|
54
|
+
/**
|
|
55
|
+
* Activate kill-switch for a tenant. Subsequent isActive() checks return
|
|
56
|
+
* true immediately (in-memory). Fire-and-forget persist to disk.
|
|
57
|
+
*/
|
|
58
|
+
activate(tenantId: string, options?: ActivateOptions): KillSwitchEntry;
|
|
59
|
+
/**
|
|
60
|
+
* Lift kill-switch for a tenant. Requests resume immediately.
|
|
61
|
+
* Returns false if no active kill-switch was found.
|
|
62
|
+
*/
|
|
63
|
+
lift(tenantId: string, options?: LiftOptions): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Check if a kill-switch is active for a tenant.
|
|
66
|
+
* O(1) — reads only from the in-memory Set.
|
|
67
|
+
*/
|
|
68
|
+
isActive(tenantId: string): boolean;
|
|
69
|
+
/** Get the full kill-switch entry for a tenant. */
|
|
70
|
+
getEntry(tenantId: string): KillSwitchEntry | undefined;
|
|
71
|
+
/** List all kill-switch entries (active and historical). */
|
|
72
|
+
listAll(): KillSwitchEntry[];
|
|
73
|
+
/** List only currently active kill-switches. */
|
|
74
|
+
listActive(): KillSwitchEntry[];
|
|
75
|
+
/**
|
|
76
|
+
* Build the response payload for a blocked request.
|
|
77
|
+
* Use with HTTP 429 status and x-relayplane-kill-switch: true header.
|
|
78
|
+
*/
|
|
79
|
+
buildBlockedResponse(tenantId: string): object;
|
|
80
|
+
}
|
|
81
|
+
export declare function getKillSwitchManager(storePath?: string): KillSwitchManager;
|
|
82
|
+
export declare function resetKillSwitchManager(): void;
|
|
83
|
+
//# sourceMappingURL=kill-switch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kill-switch.d.ts","sourceRoot":"","sources":["../src/kill-switch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAQD;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,kBAAkB,CAA0B;IACpD,OAAO,CAAC,OAAO,CAA2C;gBAE9C,SAAS,CAAC,EAAE,MAAM;IAM9B,OAAO,CAAC,IAAI;IAcZ,OAAO,CAAC,OAAO;IAef;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,eAAe;IAwB1E;;;OAGG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO;IAwB1D;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAInC,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIvD,4DAA4D;IAC5D,OAAO,IAAI,eAAe,EAAE;IAI5B,gDAAgD;IAChD,UAAU,IAAI,eAAe,EAAE;IAI/B;;;OAGG;IACH,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;CAa/C;AAKD,wBAAgB,oBAAoB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAG1E;AAED,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Kill-Switch API
|
|
4
|
+
*
|
|
5
|
+
* Instantly halts ALL traffic for a specific tenant. Designed for:
|
|
6
|
+
* - Runaway agent loops detected by anomaly monitoring
|
|
7
|
+
* - Billing emergencies (unexpected spend spike)
|
|
8
|
+
* - Security incidents requiring immediate isolation
|
|
9
|
+
* - Client-requested suspension
|
|
10
|
+
*
|
|
11
|
+
* The kill-switch is checked in-memory first (O(1), no disk I/O) on every
|
|
12
|
+
* request so it activates within a single request cycle — effectively
|
|
13
|
+
* instantaneous. The flag is also persisted to disk so it survives proxy
|
|
14
|
+
* restarts.
|
|
15
|
+
*
|
|
16
|
+
* HTTP API (served by the proxy server):
|
|
17
|
+
* POST /v1/tenants/:tenantId/kill { reason?: string } → activates
|
|
18
|
+
* DELETE /v1/tenants/:tenantId/kill → lifts
|
|
19
|
+
* GET /v1/tenants/:tenantId/kill → status
|
|
20
|
+
*/
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
24
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
25
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(o, k2, desc);
|
|
28
|
+
}) : (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
o[k2] = m[k];
|
|
31
|
+
}));
|
|
32
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
33
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
34
|
+
}) : function(o, v) {
|
|
35
|
+
o["default"] = v;
|
|
36
|
+
});
|
|
37
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
38
|
+
var ownKeys = function(o) {
|
|
39
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
40
|
+
var ar = [];
|
|
41
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
42
|
+
return ar;
|
|
43
|
+
};
|
|
44
|
+
return ownKeys(o);
|
|
45
|
+
};
|
|
46
|
+
return function (mod) {
|
|
47
|
+
if (mod && mod.__esModule) return mod;
|
|
48
|
+
var result = {};
|
|
49
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.KillSwitchManager = void 0;
|
|
56
|
+
exports.getKillSwitchManager = getKillSwitchManager;
|
|
57
|
+
exports.resetKillSwitchManager = resetKillSwitchManager;
|
|
58
|
+
const fs = __importStar(require("fs"));
|
|
59
|
+
const path = __importStar(require("path"));
|
|
60
|
+
const os = __importStar(require("os"));
|
|
61
|
+
function resolveRelayplaneDir() {
|
|
62
|
+
const homeOverride = process.env['RELAYPLANE_HOME_OVERRIDE'];
|
|
63
|
+
const base = homeOverride ?? os.homedir();
|
|
64
|
+
return path.join(base, '.relayplane');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* KillSwitchManager provides instant tenant traffic halting with persistence.
|
|
68
|
+
*
|
|
69
|
+
* Thread-safety note: this is single-process Node.js, so the in-memory Set
|
|
70
|
+
* is the authoritative source of truth for the hot path. Disk writes are
|
|
71
|
+
* fire-and-forget for persistence across restarts.
|
|
72
|
+
*/
|
|
73
|
+
class KillSwitchManager {
|
|
74
|
+
storePath;
|
|
75
|
+
activeKillSwitches = new Set();
|
|
76
|
+
entries = new Map();
|
|
77
|
+
constructor(storePath) {
|
|
78
|
+
const dir = resolveRelayplaneDir();
|
|
79
|
+
this.storePath = storePath ?? path.join(dir, 'kill-switches.json');
|
|
80
|
+
this.load();
|
|
81
|
+
}
|
|
82
|
+
load() {
|
|
83
|
+
if (fs.existsSync(this.storePath)) {
|
|
84
|
+
try {
|
|
85
|
+
const store = JSON.parse(fs.readFileSync(this.storePath, 'utf-8'));
|
|
86
|
+
for (const [id, entry] of Object.entries(store.entries)) {
|
|
87
|
+
this.entries.set(id, entry);
|
|
88
|
+
if (entry.active)
|
|
89
|
+
this.activeKillSwitches.add(id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Corrupt store — start fresh
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
persist() {
|
|
98
|
+
const dir = path.dirname(this.storePath);
|
|
99
|
+
if (!fs.existsSync(dir))
|
|
100
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
const store = {
|
|
102
|
+
entries: Object.fromEntries(this.entries),
|
|
103
|
+
updated_at: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
// Atomic write
|
|
106
|
+
const tmp = this.storePath + '.tmp';
|
|
107
|
+
fs.writeFileSync(tmp, JSON.stringify(store, null, 2));
|
|
108
|
+
fs.renameSync(tmp, this.storePath);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Activate kill-switch for a tenant. Subsequent isActive() checks return
|
|
112
|
+
* true immediately (in-memory). Fire-and-forget persist to disk.
|
|
113
|
+
*/
|
|
114
|
+
activate(tenantId, options = {}) {
|
|
115
|
+
const now = new Date().toISOString();
|
|
116
|
+
const entry = {
|
|
117
|
+
tenant_id: tenantId,
|
|
118
|
+
active: true,
|
|
119
|
+
reason: options.reason,
|
|
120
|
+
activated_at: now,
|
|
121
|
+
activated_by: options.activated_by,
|
|
122
|
+
};
|
|
123
|
+
// In-memory first (hot path)
|
|
124
|
+
this.activeKillSwitches.add(tenantId);
|
|
125
|
+
this.entries.set(tenantId, entry);
|
|
126
|
+
// Persist (async-ish — but Node.js writeFileSync is sync, this is fast enough)
|
|
127
|
+
try {
|
|
128
|
+
this.persist();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Don't throw — in-memory state is authoritative
|
|
132
|
+
}
|
|
133
|
+
return entry;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Lift kill-switch for a tenant. Requests resume immediately.
|
|
137
|
+
* Returns false if no active kill-switch was found.
|
|
138
|
+
*/
|
|
139
|
+
lift(tenantId, options = {}) {
|
|
140
|
+
if (!this.activeKillSwitches.has(tenantId))
|
|
141
|
+
return false;
|
|
142
|
+
const now = new Date().toISOString();
|
|
143
|
+
const existing = this.entries.get(tenantId);
|
|
144
|
+
const entry = {
|
|
145
|
+
...(existing ?? { tenant_id: tenantId }),
|
|
146
|
+
active: false,
|
|
147
|
+
lifted_at: now,
|
|
148
|
+
lifted_by: options.lifted_by,
|
|
149
|
+
};
|
|
150
|
+
this.activeKillSwitches.delete(tenantId);
|
|
151
|
+
this.entries.set(tenantId, entry);
|
|
152
|
+
try {
|
|
153
|
+
this.persist();
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// In-memory state is authoritative
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Check if a kill-switch is active for a tenant.
|
|
162
|
+
* O(1) — reads only from the in-memory Set.
|
|
163
|
+
*/
|
|
164
|
+
isActive(tenantId) {
|
|
165
|
+
return this.activeKillSwitches.has(tenantId);
|
|
166
|
+
}
|
|
167
|
+
/** Get the full kill-switch entry for a tenant. */
|
|
168
|
+
getEntry(tenantId) {
|
|
169
|
+
return this.entries.get(tenantId);
|
|
170
|
+
}
|
|
171
|
+
/** List all kill-switch entries (active and historical). */
|
|
172
|
+
listAll() {
|
|
173
|
+
return Array.from(this.entries.values());
|
|
174
|
+
}
|
|
175
|
+
/** List only currently active kill-switches. */
|
|
176
|
+
listActive() {
|
|
177
|
+
return Array.from(this.entries.values()).filter(e => e.active);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Build the response payload for a blocked request.
|
|
181
|
+
* Use with HTTP 429 status and x-relayplane-kill-switch: true header.
|
|
182
|
+
*/
|
|
183
|
+
buildBlockedResponse(tenantId) {
|
|
184
|
+
const entry = this.entries.get(tenantId);
|
|
185
|
+
return {
|
|
186
|
+
error: {
|
|
187
|
+
type: 'kill_switch_active',
|
|
188
|
+
message: `All traffic for tenant '${tenantId}' has been suspended.`,
|
|
189
|
+
tenant_id: tenantId,
|
|
190
|
+
activated_at: entry?.activated_at,
|
|
191
|
+
reason: entry?.reason ?? 'No reason provided.',
|
|
192
|
+
contact: 'Contact your RelayPlane administrator to lift the kill-switch.',
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
exports.KillSwitchManager = KillSwitchManager;
|
|
198
|
+
/** Singleton instance. */
|
|
199
|
+
let _instance;
|
|
200
|
+
function getKillSwitchManager(storePath) {
|
|
201
|
+
if (!_instance)
|
|
202
|
+
_instance = new KillSwitchManager(storePath);
|
|
203
|
+
return _instance;
|
|
204
|
+
}
|
|
205
|
+
function resetKillSwitchManager() {
|
|
206
|
+
_instance = undefined;
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=kill-switch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kill-switch.js","sourceRoot":"","sources":["../src/kill-switch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec-Match Verification Plugin
|
|
3
|
+
*
|
|
4
|
+
* Before an agent marks a task complete, the orchestrator POSTs the task's
|
|
5
|
+
* acceptance criteria + the agent's output (diff, text, screenshots) to
|
|
6
|
+
* RelayPlane's /v1/spec-match endpoint. This plugin uses a cheap LLM
|
|
7
|
+
* (Haiku by default) to evaluate whether the output satisfies each criterion.
|
|
8
|
+
*
|
|
9
|
+
* Returns a structured pass/fail result with per-criterion evidence and
|
|
10
|
+
* confidence scores. Only pass:true results should proceed to production.
|
|
11
|
+
*
|
|
12
|
+
* Use in Matt's swarm: every coder task dispatch includes acceptance_criteria.
|
|
13
|
+
* On completion, orchestrator calls spec-match. Failing results trigger retry
|
|
14
|
+
* with escalation before the verifier agent sees the output.
|
|
15
|
+
*/
|
|
16
|
+
export interface AcceptanceCriterion {
|
|
17
|
+
/** Short identifier, e.g. "hero-headline-changed" */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Human-readable description of what must be true */
|
|
20
|
+
description: string;
|
|
21
|
+
/** How critical this criterion is */
|
|
22
|
+
severity: 'blocker' | 'major' | 'minor';
|
|
23
|
+
}
|
|
24
|
+
export interface SpecMatchRequest {
|
|
25
|
+
/** Task title or identifier for trace logging */
|
|
26
|
+
task_id: string;
|
|
27
|
+
/** Tenant this evaluation belongs to */
|
|
28
|
+
tenant_id?: string;
|
|
29
|
+
/** List of acceptance criteria to evaluate */
|
|
30
|
+
acceptance_criteria: AcceptanceCriterion[];
|
|
31
|
+
/**
|
|
32
|
+
* The agent's output to evaluate against criteria.
|
|
33
|
+
* At least one of diff, output_text, or screenshots must be provided.
|
|
34
|
+
*/
|
|
35
|
+
diff?: string;
|
|
36
|
+
output_text?: string;
|
|
37
|
+
/** Base64-encoded PNG screenshots, or URLs */
|
|
38
|
+
screenshots?: string[];
|
|
39
|
+
/** Override the default evaluation model */
|
|
40
|
+
model_override?: string;
|
|
41
|
+
/** If true, include the full LLM prompt/response in the result */
|
|
42
|
+
debug?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface CriterionResult {
|
|
45
|
+
criterion_id: string;
|
|
46
|
+
criterion_description: string;
|
|
47
|
+
met: boolean;
|
|
48
|
+
evidence: string;
|
|
49
|
+
confidence: 'high' | 'medium' | 'low';
|
|
50
|
+
severity: 'blocker' | 'major' | 'minor';
|
|
51
|
+
}
|
|
52
|
+
export interface SpecMatchResult {
|
|
53
|
+
/** True if all blocker criteria are met */
|
|
54
|
+
pass: boolean;
|
|
55
|
+
/** 0–100 score (percentage of weighted criteria met) */
|
|
56
|
+
score: number;
|
|
57
|
+
/** Per-criterion results */
|
|
58
|
+
criteria_results: CriterionResult[];
|
|
59
|
+
/** Criteria with severity=blocker that were not met */
|
|
60
|
+
blockers: string[];
|
|
61
|
+
/** Criteria with severity=major that were not met */
|
|
62
|
+
warnings: string[];
|
|
63
|
+
/** Model used for evaluation */
|
|
64
|
+
model_used: string;
|
|
65
|
+
/** Estimated cost of the spec-match call in USD */
|
|
66
|
+
cost_usd: number;
|
|
67
|
+
/** Trace ID for audit linking */
|
|
68
|
+
trace_id: string;
|
|
69
|
+
/** ISO timestamp */
|
|
70
|
+
evaluated_at: string;
|
|
71
|
+
/** Only present when request.debug=true */
|
|
72
|
+
debug_prompt?: string;
|
|
73
|
+
debug_response?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface SpecMatchPluginOptions {
|
|
76
|
+
/** Anthropic-compatible base URL (defaults to https://api.anthropic.com) */
|
|
77
|
+
apiBaseUrl?: string;
|
|
78
|
+
/** API key for the evaluation model */
|
|
79
|
+
apiKey?: string;
|
|
80
|
+
/** Model to use for evaluation (default: claude-haiku-4-5-20251001) */
|
|
81
|
+
defaultModel?: string;
|
|
82
|
+
/** Max tokens for the evaluation response (default: 2048) */
|
|
83
|
+
maxTokens?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* SpecMatchPlugin evaluates agent output against acceptance criteria using
|
|
87
|
+
* an LLM judge. Use this at task completion before marking work as done.
|
|
88
|
+
*/
|
|
89
|
+
export declare class SpecMatchPlugin {
|
|
90
|
+
private apiBaseUrl;
|
|
91
|
+
private apiKey;
|
|
92
|
+
private defaultModel;
|
|
93
|
+
private maxTokens;
|
|
94
|
+
constructor(options?: SpecMatchPluginOptions);
|
|
95
|
+
evaluate(request: SpecMatchRequest): Promise<SpecMatchResult>;
|
|
96
|
+
}
|
|
97
|
+
export declare function getSpecMatchPlugin(options?: SpecMatchPluginOptions): SpecMatchPlugin;
|
|
98
|
+
export declare function resetSpecMatchPlugin(): void;
|
|
99
|
+
//# sourceMappingURL=spec-match-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-match-plugin.d.ts","sourceRoot":"","sources":["../src/spec-match-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,QAAQ,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,mBAAmB,EAAE,mBAAmB,EAAE,CAAC;IAC3C;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,GAAG,EAAE,OAAO,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtC,QAAQ,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,eAAe;IAC9B,2CAA2C;IAC3C,IAAI,EAAE,OAAO,CAAC;IACd,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,uDAAuD;IACvD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsDD;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAS;gBAEd,OAAO,GAAE,sBAA2B;IAO1C,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;CAiIpE;AAKD,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,eAAe,CAGpF;AAED,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Spec-Match Verification Plugin
|
|
4
|
+
*
|
|
5
|
+
* Before an agent marks a task complete, the orchestrator POSTs the task's
|
|
6
|
+
* acceptance criteria + the agent's output (diff, text, screenshots) to
|
|
7
|
+
* RelayPlane's /v1/spec-match endpoint. This plugin uses a cheap LLM
|
|
8
|
+
* (Haiku by default) to evaluate whether the output satisfies each criterion.
|
|
9
|
+
*
|
|
10
|
+
* Returns a structured pass/fail result with per-criterion evidence and
|
|
11
|
+
* confidence scores. Only pass:true results should proceed to production.
|
|
12
|
+
*
|
|
13
|
+
* Use in Matt's swarm: every coder task dispatch includes acceptance_criteria.
|
|
14
|
+
* On completion, orchestrator calls spec-match. Failing results trigger retry
|
|
15
|
+
* with escalation before the verifier agent sees the output.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.SpecMatchPlugin = void 0;
|
|
19
|
+
exports.getSpecMatchPlugin = getSpecMatchPlugin;
|
|
20
|
+
exports.resetSpecMatchPlugin = resetSpecMatchPlugin;
|
|
21
|
+
/** Haiku price per 1M tokens (input/output) for cost estimation */
|
|
22
|
+
const HAIKU_INPUT_COST_PER_1M = 0.80;
|
|
23
|
+
const HAIKU_OUTPUT_COST_PER_1M = 4.00;
|
|
24
|
+
function buildEvaluationPrompt(request) {
|
|
25
|
+
const criteriaList = request.acceptance_criteria
|
|
26
|
+
.map((c, i) => `${i + 1}. [${c.id}] (${c.severity}) ${c.description}`)
|
|
27
|
+
.join('\n');
|
|
28
|
+
const outputSections = [];
|
|
29
|
+
if (request.diff)
|
|
30
|
+
outputSections.push(`<diff>\n${request.diff}\n</diff>`);
|
|
31
|
+
if (request.output_text)
|
|
32
|
+
outputSections.push(`<output>\n${request.output_text}\n</output>`);
|
|
33
|
+
if (request.screenshots?.length) {
|
|
34
|
+
outputSections.push(`<screenshots>${request.screenshots.length} screenshot(s) provided.</screenshots>`);
|
|
35
|
+
}
|
|
36
|
+
return `You are a strict QA evaluator. Evaluate whether the agent's output satisfies each acceptance criterion below.
|
|
37
|
+
|
|
38
|
+
<acceptance_criteria>
|
|
39
|
+
${criteriaList}
|
|
40
|
+
</acceptance_criteria>
|
|
41
|
+
|
|
42
|
+
<agent_output>
|
|
43
|
+
${outputSections.join('\n\n')}
|
|
44
|
+
</agent_output>
|
|
45
|
+
|
|
46
|
+
For each criterion, respond with a JSON array. Each element must be:
|
|
47
|
+
{
|
|
48
|
+
"criterion_id": "<id from above>",
|
|
49
|
+
"met": true|false,
|
|
50
|
+
"evidence": "<one sentence citing specific evidence from the output>",
|
|
51
|
+
"confidence": "high"|"medium"|"low"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Rules:
|
|
55
|
+
- met:true only when you see clear, direct evidence in the output
|
|
56
|
+
- confidence:high = definitive evidence; medium = likely but not certain; low = cannot tell
|
|
57
|
+
- For missing/unclear evidence, met:false with confidence:low
|
|
58
|
+
- Do not infer; evaluate only what is present
|
|
59
|
+
|
|
60
|
+
Respond with ONLY the JSON array. No preamble.`;
|
|
61
|
+
}
|
|
62
|
+
function estimateCost(inputTokens, outputTokens) {
|
|
63
|
+
return (inputTokens / 1_000_000) * HAIKU_INPUT_COST_PER_1M +
|
|
64
|
+
(outputTokens / 1_000_000) * HAIKU_OUTPUT_COST_PER_1M;
|
|
65
|
+
}
|
|
66
|
+
function buildTraceId() {
|
|
67
|
+
return `sm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* SpecMatchPlugin evaluates agent output against acceptance criteria using
|
|
71
|
+
* an LLM judge. Use this at task completion before marking work as done.
|
|
72
|
+
*/
|
|
73
|
+
class SpecMatchPlugin {
|
|
74
|
+
apiBaseUrl;
|
|
75
|
+
apiKey;
|
|
76
|
+
defaultModel;
|
|
77
|
+
maxTokens;
|
|
78
|
+
constructor(options = {}) {
|
|
79
|
+
this.apiBaseUrl = options.apiBaseUrl ?? (process.env['ANTHROPIC_BASE_URL'] ?? 'https://api.anthropic.com');
|
|
80
|
+
this.apiKey = options.apiKey ?? (process.env['ANTHROPIC_API_KEY'] ?? '');
|
|
81
|
+
this.defaultModel = options.defaultModel ?? 'claude-haiku-4-5-20251001';
|
|
82
|
+
this.maxTokens = options.maxTokens ?? 2048;
|
|
83
|
+
}
|
|
84
|
+
async evaluate(request) {
|
|
85
|
+
const traceId = buildTraceId();
|
|
86
|
+
const model = request.model_override ?? this.defaultModel;
|
|
87
|
+
const prompt = buildEvaluationPrompt(request);
|
|
88
|
+
let rawResponse = '';
|
|
89
|
+
let inputTokens = 0;
|
|
90
|
+
let outputTokens = 0;
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(`${this.apiBaseUrl}/v1/messages`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
'content-type': 'application/json',
|
|
96
|
+
'x-api-key': this.apiKey,
|
|
97
|
+
'anthropic-version': '2023-06-01',
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
model,
|
|
101
|
+
max_tokens: this.maxTokens,
|
|
102
|
+
messages: [{ role: 'user', content: prompt }],
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const errText = await response.text();
|
|
107
|
+
throw new Error(`Spec-match LLM call failed: ${response.status} ${errText}`);
|
|
108
|
+
}
|
|
109
|
+
const body = await response.json();
|
|
110
|
+
rawResponse = body.content.find(c => c.type === 'text')?.text ?? '[]';
|
|
111
|
+
inputTokens = body.usage?.input_tokens ?? Math.ceil(prompt.length / 4);
|
|
112
|
+
outputTokens = body.usage?.output_tokens ?? Math.ceil(rawResponse.length / 4);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
// Return a graceful failure result rather than throwing
|
|
116
|
+
return {
|
|
117
|
+
pass: false,
|
|
118
|
+
score: 0,
|
|
119
|
+
criteria_results: request.acceptance_criteria.map(c => ({
|
|
120
|
+
criterion_id: c.id,
|
|
121
|
+
criterion_description: c.description,
|
|
122
|
+
met: false,
|
|
123
|
+
evidence: `Spec-match evaluation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
124
|
+
confidence: 'low',
|
|
125
|
+
severity: c.severity,
|
|
126
|
+
})),
|
|
127
|
+
blockers: request.acceptance_criteria.filter(c => c.severity === 'blocker').map(c => c.id),
|
|
128
|
+
warnings: request.acceptance_criteria.filter(c => c.severity === 'major').map(c => c.id),
|
|
129
|
+
model_used: model,
|
|
130
|
+
cost_usd: 0,
|
|
131
|
+
trace_id: traceId,
|
|
132
|
+
evaluated_at: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Parse LLM response
|
|
136
|
+
let llmResults = [];
|
|
137
|
+
try {
|
|
138
|
+
// Strip markdown fences if present
|
|
139
|
+
const cleaned = rawResponse.replace(/^```json\n?/i, '').replace(/\n?```$/i, '').trim();
|
|
140
|
+
llmResults = JSON.parse(cleaned);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Fallback: all criteria unmet if we can't parse
|
|
144
|
+
llmResults = request.acceptance_criteria.map(c => ({
|
|
145
|
+
criterion_id: c.id,
|
|
146
|
+
met: false,
|
|
147
|
+
evidence: 'Could not parse evaluator response.',
|
|
148
|
+
confidence: 'low',
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
// Build per-criterion results
|
|
152
|
+
const criteriaMap = new Map(request.acceptance_criteria.map(c => [c.id, c]));
|
|
153
|
+
const criteriaResults = llmResults.map(r => {
|
|
154
|
+
const criterion = criteriaMap.get(r.criterion_id);
|
|
155
|
+
return {
|
|
156
|
+
criterion_id: r.criterion_id,
|
|
157
|
+
criterion_description: criterion?.description ?? r.criterion_id,
|
|
158
|
+
met: r.met,
|
|
159
|
+
evidence: r.evidence,
|
|
160
|
+
confidence: r.confidence,
|
|
161
|
+
severity: criterion?.severity ?? 'minor',
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
// Score: weighted by severity (blocker=3, major=2, minor=1)
|
|
165
|
+
const weights = { blocker: 3, major: 2, minor: 1 };
|
|
166
|
+
const totalWeight = criteriaResults.reduce((sum, c) => sum + weights[c.severity], 0);
|
|
167
|
+
const metWeight = criteriaResults
|
|
168
|
+
.filter(c => c.met)
|
|
169
|
+
.reduce((sum, c) => sum + weights[c.severity], 0);
|
|
170
|
+
const score = totalWeight > 0 ? Math.round((metWeight / totalWeight) * 100) : 100;
|
|
171
|
+
const blockers = criteriaResults
|
|
172
|
+
.filter(c => c.severity === 'blocker' && !c.met)
|
|
173
|
+
.map(c => c.criterion_id);
|
|
174
|
+
const warnings = criteriaResults
|
|
175
|
+
.filter(c => c.severity === 'major' && !c.met)
|
|
176
|
+
.map(c => c.criterion_id);
|
|
177
|
+
const result = {
|
|
178
|
+
pass: blockers.length === 0,
|
|
179
|
+
score,
|
|
180
|
+
criteria_results: criteriaResults,
|
|
181
|
+
blockers,
|
|
182
|
+
warnings,
|
|
183
|
+
model_used: model,
|
|
184
|
+
cost_usd: estimateCost(inputTokens, outputTokens),
|
|
185
|
+
trace_id: traceId,
|
|
186
|
+
evaluated_at: new Date().toISOString(),
|
|
187
|
+
};
|
|
188
|
+
if (request.debug) {
|
|
189
|
+
result.debug_prompt = prompt;
|
|
190
|
+
result.debug_response = rawResponse;
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
exports.SpecMatchPlugin = SpecMatchPlugin;
|
|
196
|
+
/** Singleton instance. */
|
|
197
|
+
let _instance;
|
|
198
|
+
function getSpecMatchPlugin(options) {
|
|
199
|
+
if (!_instance)
|
|
200
|
+
_instance = new SpecMatchPlugin(options);
|
|
201
|
+
return _instance;
|
|
202
|
+
}
|
|
203
|
+
function resetSpecMatchPlugin() {
|
|
204
|
+
_instance = undefined;
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=spec-match-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-match-plugin.js","sourceRoot":"","sources":["../src/spec-match-plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant Isolation — per-tenant request scoping for agent swarms.
|
|
3
|
+
*
|
|
4
|
+
* Each tenant gets an isolated lane: separate budget tracking, rate limits,
|
|
5
|
+
* audit namespace, and kill-switch flag. Tenant A's runaway agent can never
|
|
6
|
+
* affect Tenant B's traffic or billing.
|
|
7
|
+
*
|
|
8
|
+
* Tenants are identified by the `x-tenant-id` request header or by an API
|
|
9
|
+
* key prefix registered in config. The proxy strips tenant headers before
|
|
10
|
+
* forwarding to providers.
|
|
11
|
+
*/
|
|
12
|
+
export type TenantTier = 'free' | 'starter' | 'pro' | 'max' | 'enterprise';
|
|
13
|
+
export interface TenantConfig {
|
|
14
|
+
label: string;
|
|
15
|
+
tier: TenantTier;
|
|
16
|
+
/** Hard daily spend cap in USD. Requests exceeding this are blocked. */
|
|
17
|
+
budget_usd_per_day?: number;
|
|
18
|
+
/** Hard monthly spend cap in USD. */
|
|
19
|
+
budget_usd_per_month?: number;
|
|
20
|
+
/** Allowlist of model IDs this tenant may use. Empty = all models allowed. */
|
|
21
|
+
allowed_models?: string[];
|
|
22
|
+
/** Denylist of model IDs. Checked after allowed_models. */
|
|
23
|
+
denied_models?: string[];
|
|
24
|
+
/** Maximum requests per minute for this tenant. */
|
|
25
|
+
rpm_limit?: number;
|
|
26
|
+
/** When true, ALL requests from this tenant are immediately rejected (kill-switch). */
|
|
27
|
+
kill_switch?: boolean;
|
|
28
|
+
/** Timestamp when the kill-switch was activated. */
|
|
29
|
+
kill_switch_activated_at?: string;
|
|
30
|
+
/** Human-readable reason for the kill-switch. */
|
|
31
|
+
kill_switch_reason?: string;
|
|
32
|
+
/** Metadata tags for dashboards and audit logs. */
|
|
33
|
+
tags?: Record<string, string>;
|
|
34
|
+
created_at: string;
|
|
35
|
+
updated_at: string;
|
|
36
|
+
}
|
|
37
|
+
export interface TenantSpend {
|
|
38
|
+
tenant_id: string;
|
|
39
|
+
date: string;
|
|
40
|
+
spend_usd: number;
|
|
41
|
+
request_count: number;
|
|
42
|
+
last_request_at: string;
|
|
43
|
+
}
|
|
44
|
+
export interface TenantRequestContext {
|
|
45
|
+
tenant_id: string;
|
|
46
|
+
trace_id: string;
|
|
47
|
+
request_id: string;
|
|
48
|
+
timestamp: string;
|
|
49
|
+
}
|
|
50
|
+
export interface TenantCheckResult {
|
|
51
|
+
allowed: boolean;
|
|
52
|
+
tenant_id: string;
|
|
53
|
+
reason?: string;
|
|
54
|
+
/** Set when kill-switch is active */
|
|
55
|
+
kill_switch_active?: boolean;
|
|
56
|
+
/** Set when a budget limit would be exceeded */
|
|
57
|
+
budget_exceeded?: boolean;
|
|
58
|
+
/** Set when the requested model is not allowed */
|
|
59
|
+
model_denied?: boolean;
|
|
60
|
+
/** Current daily spend for this tenant */
|
|
61
|
+
daily_spend_usd?: number;
|
|
62
|
+
/** Remaining daily budget */
|
|
63
|
+
daily_budget_remaining_usd?: number;
|
|
64
|
+
}
|
|
65
|
+
export interface TenantIsolatorOptions {
|
|
66
|
+
/** Path to the tenants config file. Defaults to ~/.relayplane/tenants.json */
|
|
67
|
+
configPath?: string;
|
|
68
|
+
/** Path to the spend tracking SQLite DB or JSON store. Defaults to ~/.relayplane/tenant-spend.json */
|
|
69
|
+
spendStorePath?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Manages per-tenant isolation: configuration, budget enforcement, and
|
|
73
|
+
* kill-switch state. Designed to be instantiated once and reused per request.
|
|
74
|
+
*/
|
|
75
|
+
export declare class TenantIsolator {
|
|
76
|
+
private configPath;
|
|
77
|
+
private spendStorePath;
|
|
78
|
+
private tenants;
|
|
79
|
+
private spendCache;
|
|
80
|
+
private killSwitchCache;
|
|
81
|
+
constructor(options?: TenantIsolatorOptions);
|
|
82
|
+
private load;
|
|
83
|
+
private save;
|
|
84
|
+
private saveSpend;
|
|
85
|
+
/** Register or update a tenant configuration. */
|
|
86
|
+
upsertTenant(tenantId: string, config: Omit<TenantConfig, 'created_at' | 'updated_at'>): TenantConfig;
|
|
87
|
+
/** Remove a tenant and all their spend records. */
|
|
88
|
+
deleteTenant(tenantId: string): boolean;
|
|
89
|
+
/** Get a tenant config by ID. Returns undefined for unknown tenants. */
|
|
90
|
+
getTenant(tenantId: string): TenantConfig | undefined;
|
|
91
|
+
/** List all tenants. */
|
|
92
|
+
listTenants(): Array<{
|
|
93
|
+
id: string;
|
|
94
|
+
config: TenantConfig;
|
|
95
|
+
}>;
|
|
96
|
+
/**
|
|
97
|
+
* Extract tenant ID from an incoming request.
|
|
98
|
+
* Priority: x-tenant-id header > API key prefix registered in config.
|
|
99
|
+
* Returns 'default' if no tenant can be identified.
|
|
100
|
+
*/
|
|
101
|
+
extractTenantId(headers: Record<string, string | string[] | undefined>, apiKey?: string): string;
|
|
102
|
+
/**
|
|
103
|
+
* Check whether a request from a tenant should be allowed.
|
|
104
|
+
* Checks kill-switch, model allowlist/denylist, and budget caps.
|
|
105
|
+
*/
|
|
106
|
+
checkRequest(tenantId: string, model?: string, estimatedCostUsd?: number): TenantCheckResult;
|
|
107
|
+
/** Record spend for a tenant after a successful request. */
|
|
108
|
+
recordSpend(tenantId: string, costUsd: number): void;
|
|
109
|
+
/** Get today's spend for a tenant. */
|
|
110
|
+
getDailySpend(tenantId: string): number;
|
|
111
|
+
/**
|
|
112
|
+
* Generate the request context headers to inject into downstream traces.
|
|
113
|
+
* The proxy adds these before forwarding and strips them from the outbound
|
|
114
|
+
* request to the provider.
|
|
115
|
+
*/
|
|
116
|
+
buildRequestContext(tenantId: string): TenantRequestContext;
|
|
117
|
+
}
|
|
118
|
+
export declare function getTenantIsolator(options?: TenantIsolatorOptions): TenantIsolator;
|
|
119
|
+
/** Reset the singleton (for tests). */
|
|
120
|
+
export declare function resetTenantIsolator(): void;
|
|
121
|
+
//# sourceMappingURL=tenant-isolation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-isolation.d.ts","sourceRoot":"","sources":["../src/tenant-isolation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tenant Isolation — per-tenant request scoping for agent swarms.
|
|
4
|
+
*
|
|
5
|
+
* Each tenant gets an isolated lane: separate budget tracking, rate limits,
|
|
6
|
+
* audit namespace, and kill-switch flag. Tenant A's runaway agent can never
|
|
7
|
+
* affect Tenant B's traffic or billing.
|
|
8
|
+
*
|
|
9
|
+
* Tenants are identified by the `x-tenant-id` request header or by an API
|
|
10
|
+
* key prefix registered in config. The proxy strips tenant headers before
|
|
11
|
+
* forwarding to providers.
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.TenantIsolator = void 0;
|
|
48
|
+
exports.getTenantIsolator = getTenantIsolator;
|
|
49
|
+
exports.resetTenantIsolator = resetTenantIsolator;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const crypto = __importStar(require("crypto"));
|
|
54
|
+
function resolveRelayplaneDir() {
|
|
55
|
+
const homeOverride = process.env['RELAYPLANE_HOME_OVERRIDE'];
|
|
56
|
+
const base = homeOverride ?? os.homedir();
|
|
57
|
+
return path.join(base, '.relayplane');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Manages per-tenant isolation: configuration, budget enforcement, and
|
|
61
|
+
* kill-switch state. Designed to be instantiated once and reused per request.
|
|
62
|
+
*/
|
|
63
|
+
class TenantIsolator {
|
|
64
|
+
configPath;
|
|
65
|
+
spendStorePath;
|
|
66
|
+
tenants = new Map();
|
|
67
|
+
spendCache = new Map();
|
|
68
|
+
killSwitchCache = new Set();
|
|
69
|
+
constructor(options = {}) {
|
|
70
|
+
const dir = resolveRelayplaneDir();
|
|
71
|
+
this.configPath = options.configPath ?? path.join(dir, 'tenants.json');
|
|
72
|
+
this.spendStorePath = options.spendStorePath ?? path.join(dir, 'tenant-spend.json');
|
|
73
|
+
this.load();
|
|
74
|
+
}
|
|
75
|
+
load() {
|
|
76
|
+
if (fs.existsSync(this.configPath)) {
|
|
77
|
+
try {
|
|
78
|
+
const raw = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
|
|
79
|
+
this.tenants = new Map(Object.entries(raw));
|
|
80
|
+
// Seed in-memory kill-switch cache from persisted state
|
|
81
|
+
for (const [id, cfg] of this.tenants) {
|
|
82
|
+
if (cfg.kill_switch)
|
|
83
|
+
this.killSwitchCache.add(id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Corrupt config — start with empty map
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (fs.existsSync(this.spendStorePath)) {
|
|
91
|
+
try {
|
|
92
|
+
const raw = JSON.parse(fs.readFileSync(this.spendStorePath, 'utf-8'));
|
|
93
|
+
this.spendCache = new Map(Object.entries(raw));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Corrupt spend store — start fresh
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
save() {
|
|
101
|
+
const dir = path.dirname(this.configPath);
|
|
102
|
+
if (!fs.existsSync(dir))
|
|
103
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
const obj = {};
|
|
105
|
+
for (const [id, cfg] of this.tenants)
|
|
106
|
+
obj[id] = cfg;
|
|
107
|
+
fs.writeFileSync(this.configPath, JSON.stringify(obj, null, 2));
|
|
108
|
+
}
|
|
109
|
+
saveSpend() {
|
|
110
|
+
const dir = path.dirname(this.spendStorePath);
|
|
111
|
+
if (!fs.existsSync(dir))
|
|
112
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
113
|
+
const obj = {};
|
|
114
|
+
for (const [id, spend] of this.spendCache)
|
|
115
|
+
obj[id] = spend;
|
|
116
|
+
fs.writeFileSync(this.spendStorePath, JSON.stringify(obj, null, 2));
|
|
117
|
+
}
|
|
118
|
+
/** Register or update a tenant configuration. */
|
|
119
|
+
upsertTenant(tenantId, config) {
|
|
120
|
+
const now = new Date().toISOString();
|
|
121
|
+
const existing = this.tenants.get(tenantId);
|
|
122
|
+
const full = {
|
|
123
|
+
...config,
|
|
124
|
+
created_at: existing?.created_at ?? now,
|
|
125
|
+
updated_at: now,
|
|
126
|
+
};
|
|
127
|
+
this.tenants.set(tenantId, full);
|
|
128
|
+
if (full.kill_switch) {
|
|
129
|
+
this.killSwitchCache.add(tenantId);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.killSwitchCache.delete(tenantId);
|
|
133
|
+
}
|
|
134
|
+
this.save();
|
|
135
|
+
return full;
|
|
136
|
+
}
|
|
137
|
+
/** Remove a tenant and all their spend records. */
|
|
138
|
+
deleteTenant(tenantId) {
|
|
139
|
+
const existed = this.tenants.delete(tenantId);
|
|
140
|
+
this.spendCache.delete(tenantId);
|
|
141
|
+
this.killSwitchCache.delete(tenantId);
|
|
142
|
+
if (existed) {
|
|
143
|
+
this.save();
|
|
144
|
+
this.saveSpend();
|
|
145
|
+
}
|
|
146
|
+
return existed;
|
|
147
|
+
}
|
|
148
|
+
/** Get a tenant config by ID. Returns undefined for unknown tenants. */
|
|
149
|
+
getTenant(tenantId) {
|
|
150
|
+
return this.tenants.get(tenantId);
|
|
151
|
+
}
|
|
152
|
+
/** List all tenants. */
|
|
153
|
+
listTenants() {
|
|
154
|
+
return Array.from(this.tenants.entries()).map(([id, config]) => ({ id, config }));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Extract tenant ID from an incoming request.
|
|
158
|
+
* Priority: x-tenant-id header > API key prefix registered in config.
|
|
159
|
+
* Returns 'default' if no tenant can be identified.
|
|
160
|
+
*/
|
|
161
|
+
extractTenantId(headers, apiKey) {
|
|
162
|
+
// 1. Explicit header
|
|
163
|
+
const header = headers['x-tenant-id'];
|
|
164
|
+
if (header)
|
|
165
|
+
return Array.isArray(header) ? header[0] : header;
|
|
166
|
+
// 2. API key prefix match
|
|
167
|
+
if (apiKey) {
|
|
168
|
+
for (const [id, cfg] of this.tenants) {
|
|
169
|
+
if (cfg.tags?.['api_key_prefix'] && apiKey.startsWith(cfg.tags['api_key_prefix'])) {
|
|
170
|
+
return id;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return 'default';
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Check whether a request from a tenant should be allowed.
|
|
178
|
+
* Checks kill-switch, model allowlist/denylist, and budget caps.
|
|
179
|
+
*/
|
|
180
|
+
checkRequest(tenantId, model, estimatedCostUsd = 0) {
|
|
181
|
+
// Fast path: in-memory kill-switch check (no disk I/O)
|
|
182
|
+
if (this.killSwitchCache.has(tenantId)) {
|
|
183
|
+
return { allowed: false, tenant_id: tenantId, kill_switch_active: true, reason: 'Kill-switch is active for this tenant.' };
|
|
184
|
+
}
|
|
185
|
+
const config = this.tenants.get(tenantId);
|
|
186
|
+
if (!config) {
|
|
187
|
+
// Unknown tenant — allow by default (open proxy mode)
|
|
188
|
+
return { allowed: true, tenant_id: tenantId };
|
|
189
|
+
}
|
|
190
|
+
// Model allowlist check
|
|
191
|
+
if (model && config.allowed_models && config.allowed_models.length > 0) {
|
|
192
|
+
if (!config.allowed_models.includes(model)) {
|
|
193
|
+
return { allowed: false, tenant_id: tenantId, model_denied: true, reason: `Model '${model}' is not in the allowlist for tenant '${tenantId}'.` };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Model denylist check
|
|
197
|
+
if (model && config.denied_models && config.denied_models.includes(model)) {
|
|
198
|
+
return { allowed: false, tenant_id: tenantId, model_denied: true, reason: `Model '${model}' is denied for tenant '${tenantId}'.` };
|
|
199
|
+
}
|
|
200
|
+
// Budget check
|
|
201
|
+
if (config.budget_usd_per_day !== undefined) {
|
|
202
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
203
|
+
const spendKey = `${tenantId}:${today}`;
|
|
204
|
+
const spend = this.spendCache.get(spendKey);
|
|
205
|
+
const currentSpend = spend?.spend_usd ?? 0;
|
|
206
|
+
const remaining = config.budget_usd_per_day - currentSpend;
|
|
207
|
+
if (estimatedCostUsd > 0 && currentSpend + estimatedCostUsd > config.budget_usd_per_day) {
|
|
208
|
+
return {
|
|
209
|
+
allowed: false,
|
|
210
|
+
tenant_id: tenantId,
|
|
211
|
+
budget_exceeded: true,
|
|
212
|
+
daily_spend_usd: currentSpend,
|
|
213
|
+
daily_budget_remaining_usd: Math.max(0, remaining),
|
|
214
|
+
reason: `Daily budget of $${config.budget_usd_per_day} exceeded for tenant '${tenantId}'.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
allowed: true,
|
|
219
|
+
tenant_id: tenantId,
|
|
220
|
+
daily_spend_usd: currentSpend,
|
|
221
|
+
daily_budget_remaining_usd: Math.max(0, remaining),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return { allowed: true, tenant_id: tenantId };
|
|
225
|
+
}
|
|
226
|
+
/** Record spend for a tenant after a successful request. */
|
|
227
|
+
recordSpend(tenantId, costUsd) {
|
|
228
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
229
|
+
const spendKey = `${tenantId}:${today}`;
|
|
230
|
+
const existing = this.spendCache.get(spendKey);
|
|
231
|
+
const now = new Date().toISOString();
|
|
232
|
+
this.spendCache.set(spendKey, {
|
|
233
|
+
tenant_id: tenantId,
|
|
234
|
+
date: today,
|
|
235
|
+
spend_usd: (existing?.spend_usd ?? 0) + costUsd,
|
|
236
|
+
request_count: (existing?.request_count ?? 0) + 1,
|
|
237
|
+
last_request_at: now,
|
|
238
|
+
});
|
|
239
|
+
this.saveSpend();
|
|
240
|
+
}
|
|
241
|
+
/** Get today's spend for a tenant. */
|
|
242
|
+
getDailySpend(tenantId) {
|
|
243
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
244
|
+
return this.spendCache.get(`${tenantId}:${today}`)?.spend_usd ?? 0;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Generate the request context headers to inject into downstream traces.
|
|
248
|
+
* The proxy adds these before forwarding and strips them from the outbound
|
|
249
|
+
* request to the provider.
|
|
250
|
+
*/
|
|
251
|
+
buildRequestContext(tenantId) {
|
|
252
|
+
return {
|
|
253
|
+
tenant_id: tenantId,
|
|
254
|
+
trace_id: `trace_${crypto.randomBytes(8).toString('hex')}`,
|
|
255
|
+
request_id: `req_${crypto.randomBytes(6).toString('hex')}`,
|
|
256
|
+
timestamp: new Date().toISOString(),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
exports.TenantIsolator = TenantIsolator;
|
|
261
|
+
/** Singleton instance for use across the proxy server. */
|
|
262
|
+
let _instance;
|
|
263
|
+
function getTenantIsolator(options) {
|
|
264
|
+
if (!_instance)
|
|
265
|
+
_instance = new TenantIsolator(options);
|
|
266
|
+
return _instance;
|
|
267
|
+
}
|
|
268
|
+
/** Reset the singleton (for tests). */
|
|
269
|
+
function resetTenantIsolator() {
|
|
270
|
+
_instance = undefined;
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=tenant-isolation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-isolation.js","sourceRoot":"","sources":["../src/tenant-isolation.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAySH,8CAGC;AAGD,kDAEC;AA/SD,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AACzB,+CAAiC;AAmEjC,SAAS,oBAAoB;IAC3B,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;IAC7D,MAAM,IAAI,GAAG,YAAY,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;IAC1C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,MAAa,cAAc;IACjB,UAAU,CAAS;IACnB,cAAc,CAAS;IACvB,OAAO,GAA8B,IAAI,GAAG,EAAE,CAAC;IAC/C,UAAU,GAA6B,IAAI,GAAG,EAAE,CAAC;IACjD,eAAe,GAAgB,IAAI,GAAG,EAAE,CAAC;IAEjD,YAAY,UAAiC,EAAE;QAC7C,MAAM,GAAG,GAAG,oBAAoB,EAAE,CAAC;QACnC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QACvE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;QACpF,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAEO,IAAI;QACV,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YACnC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAiC,CAAC;gBAClG,IAAI,CAAC,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC5C,wDAAwD;gBACxD,KAAK,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;oBACrC,IAAI,GAAG,CAAC,WAAW;wBAAE,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACpD,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,wCAAwC;YAC1C,CAAC;QACH,CAAC;QACD,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;YACvC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAgC,CAAC;gBACrG,IAAI,CAAC,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YACjD,CAAC;YAAC,MAAM,CAAC;gBACP,oCAAoC;YACtC,CAAC;QACH,CAAC;IACH,CAAC;IAEO,IAAI;QACV,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC1C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChE,MAAM,GAAG,GAAiC,EAAE,CAAC;QAC7C,KAAK,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO;YAAE,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;QACpD,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAClE,CAAC;IAEO,SAAS;QACf,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChE,MAAM,GAAG,GAAgC,EAAE,CAAC;QAC5C,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU;YAAE,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC;QAC3D,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,iDAAiD;IACjD,YAAY,CAAC,QAAgB,EAAE,MAAuD;QACpF,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,IAAI,GAAiB;YACzB,GAAG,MAAM;YACT,UAAU,EAAE,QAAQ,EAAE,UAAU,IAAI,GAAG;YACvC,UAAU,EAAE,GAAG;SAChB,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACjC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IAED,mDAAmD;IACnD,YAAY,CAAC,QAAgB;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACjC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACtC,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,wEAAwE;IACxE,SAAS,CAAC,QAAgB;QACxB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IAED,wBAAwB;IACxB,WAAW;QACT,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IACpF,CAAC;IAED;;;;OAIG;IACH,eAAe,CAAC,OAAsD,EAAE,MAAe;QACrF,qBAAqB;QACrB,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;QACtC,IAAI,MAAM;YAAE,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QAE9D,0BAA0B;QAC1B,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACrC,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC;oBAClF,OAAO,EAAE,CAAC;gBACZ,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;;OAGG;IACH,YAAY,CAAC,QAAgB,EAAE,KAAc,EAAE,gBAAgB,GAAG,CAAC;QACjE,uDAAuD;QACvD,IAAI,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YACvC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,wCAAwC,EAAE,CAAC;QAC7H,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,sDAAsD;YACtD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;QAChD,CAAC;QAED,wBAAwB;QACxB,IAAI,KAAK,IAAI,MAAM,CAAC,cAAc,IAAI,MAAM,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvE,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC3C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,KAAK,yCAAyC,QAAQ,IAAI,EAAE,CAAC;YACnJ,CAAC;QACH,CAAC;QAED,uBAAuB;QACvB,IAAI,KAAK,IAAI,MAAM,CAAC,aAAa,IAAI,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,KAAK,2BAA2B,QAAQ,IAAI,EAAE,CAAC;QACrI,CAAC;QAED,eAAe;QACf,IAAI,MAAM,CAAC,kBAAkB,KAAK,SAAS,EAAE,CAAC;YAC5C,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACpD,MAAM,QAAQ,GAAG,GAAG,QAAQ,IAAI,KAAK,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC5C,MAAM,YAAY,GAAG,KAAK,EAAE,SAAS,IAAI,CAAC,CAAC;YAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,kBAAkB,GAAG,YAAY,CAAC;YAE3D,IAAI,gBAAgB,GAAG,CAAC,IAAI,YAAY,GAAG,gBAAgB,GAAG,MAAM,CAAC,kBAAkB,EAAE,CAAC;gBACxF,OAAO;oBACL,OAAO,EAAE,KAAK;oBACd,SAAS,EAAE,QAAQ;oBACnB,eAAe,EAAE,IAAI;oBACrB,eAAe,EAAE,YAAY;oBAC7B,0BAA0B,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC;oBAClD,MAAM,EAAE,oBAAoB,MAAM,CAAC,kBAAkB,yBAAyB,QAAQ,IAAI;iBAC3F,CAAC;YACJ,CAAC;YAED,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,QAAQ;gBACnB,eAAe,EAAE,YAAY;gBAC7B,0BAA0B,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC;aACnD,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IAChD,CAAC;IAED,4DAA4D;IAC5D,WAAW,CAAC,QAAgB,EAAE,OAAe;QAC3C,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpD,MAAM,QAAQ,GAAG,GAAG,QAAQ,IAAI,KAAK,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC/C,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAErC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE;YAC5B,SAAS,EAAE,QAAQ;YACnB,IAAI,EAAE,KAAK;YACX,SAAS,EAAE,CAAC,QAAQ,EAAE,SAAS,IAAI,CAAC,CAAC,GAAG,OAAO;YAC/C,aAAa,EAAE,CAAC,QAAQ,EAAE,aAAa,IAAI,CAAC,CAAC,GAAG,CAAC;YACjD,eAAe,EAAE,GAAG;SACrB,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAED,sCAAsC;IACtC,aAAa,CAAC,QAAgB;QAC5B,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,QAAQ,IAAI,KAAK,EAAE,CAAC,EAAE,SAAS,IAAI,CAAC,CAAC;IACrE,CAAC;IAED;;;;OAIG;IACH,mBAAmB,CAAC,QAAgB;QAClC,OAAO;YACL,SAAS,EAAE,QAAQ;YACnB,QAAQ,EAAE,SAAS,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE;YAC1D,UAAU,EAAE,OAAO,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE;YAC1D,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC;IACJ,CAAC;CACF;AAlND,wCAkNC;AAED,0DAA0D;AAC1D,IAAI,SAAqC,CAAC;AAE1C,SAAgB,iBAAiB,CAAC,OAA+B;IAC/D,IAAI,CAAC,SAAS;QAAE,SAAS,GAAG,IAAI,cAAc,CAAC,OAAO,CAAC,CAAC;IACxD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,uCAAuC;AACvC,SAAgB,mBAAmB;IACjC,SAAS,GAAG,SAAS,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SearchableTool {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class SemanticToolSearch {
|
|
6
|
+
private tools;
|
|
7
|
+
private vectors;
|
|
8
|
+
get size(): number;
|
|
9
|
+
indexTools(tools: SearchableTool[]): void;
|
|
10
|
+
search(query: string, topK: number): SearchableTool[];
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=tool-search-semantic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-search-semantic.d.ts","sourceRoot":"","sources":["../src/tool-search-semantic.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AA6BD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,OAAO,CAA6B;IAE5C,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,UAAU,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,IAAI;IAOzC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE;CAUtD"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SemanticToolSearch = void 0;
|
|
4
|
+
function tokenize(text) {
|
|
5
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(t => t.length > 1);
|
|
6
|
+
}
|
|
7
|
+
function buildVector(tokens) {
|
|
8
|
+
const freq = new Map();
|
|
9
|
+
for (const t of tokens) {
|
|
10
|
+
freq.set(t, (freq.get(t) ?? 0) + 1);
|
|
11
|
+
}
|
|
12
|
+
return freq;
|
|
13
|
+
}
|
|
14
|
+
function cosineSimilarity(a, b) {
|
|
15
|
+
let dot = 0;
|
|
16
|
+
let normA = 0;
|
|
17
|
+
let normB = 0;
|
|
18
|
+
for (const [term, countA] of a) {
|
|
19
|
+
dot += countA * (b.get(term) ?? 0);
|
|
20
|
+
normA += countA * countA;
|
|
21
|
+
}
|
|
22
|
+
for (const [, countB] of b) {
|
|
23
|
+
normB += countB * countB;
|
|
24
|
+
}
|
|
25
|
+
if (normA === 0 || normB === 0)
|
|
26
|
+
return 0;
|
|
27
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
28
|
+
}
|
|
29
|
+
class SemanticToolSearch {
|
|
30
|
+
tools = [];
|
|
31
|
+
vectors = [];
|
|
32
|
+
get size() {
|
|
33
|
+
return this.tools.length;
|
|
34
|
+
}
|
|
35
|
+
indexTools(tools) {
|
|
36
|
+
this.tools = [...tools];
|
|
37
|
+
this.vectors = tools.map(t => buildVector(tokenize(`${t.name} ${t.description}`)));
|
|
38
|
+
}
|
|
39
|
+
search(query, topK) {
|
|
40
|
+
if (this.tools.length === 0)
|
|
41
|
+
return [];
|
|
42
|
+
const queryVec = buildVector(tokenize(query));
|
|
43
|
+
const scored = this.tools.map((tool, i) => ({
|
|
44
|
+
tool,
|
|
45
|
+
score: cosineSimilarity(queryVec, this.vectors[i]),
|
|
46
|
+
}));
|
|
47
|
+
scored.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name));
|
|
48
|
+
return scored.slice(0, topK).map(s => s.tool);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.SemanticToolSearch = SemanticToolSearch;
|
|
52
|
+
//# sourceMappingURL=tool-search-semantic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-search-semantic.js","sourceRoot":"","sources":["../src/tool-search-semantic.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayplane/proxy",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.27",
|
|
4
4
|
"description": "Open source cost intelligence proxy for AI agents. Cut LLM costs ~80% with smart model routing. Dashboard, policy engine, 11 providers. MIT licensed.",
|
|
5
5
|
"homepage": "https://relayplane.com",
|
|
6
6
|
"repository": {
|