@hasna/connectors 1.1.13 → 1.1.15
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/bin/index.js +99 -0
- package/bin/mcp.js +112 -1
- package/bin/serve.js +95 -0
- package/dist/db/database.d.ts +2 -0
- package/dist/db/locks.d.ts +59 -0
- package/dist/db/locks.test.d.ts +1 -0
- package/dist/db/rate.d.ts +57 -0
- package/dist/db/rate.test.d.ts +1 -0
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -10117,6 +10117,24 @@ function migrate(db) {
|
|
|
10117
10117
|
created_at TEXT NOT NULL
|
|
10118
10118
|
)
|
|
10119
10119
|
`);
|
|
10120
|
+
db.run(`
|
|
10121
|
+
CREATE TABLE IF NOT EXISTS resource_locks (
|
|
10122
|
+
id TEXT PRIMARY KEY,
|
|
10123
|
+
resource_type TEXT NOT NULL CHECK(resource_type IN ('connector', 'agent', 'profile', 'token')),
|
|
10124
|
+
resource_id TEXT NOT NULL,
|
|
10125
|
+
agent_id TEXT NOT NULL,
|
|
10126
|
+
lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
|
|
10127
|
+
locked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10128
|
+
expires_at TEXT NOT NULL
|
|
10129
|
+
)
|
|
10130
|
+
`);
|
|
10131
|
+
db.run(`
|
|
10132
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
|
|
10133
|
+
ON resource_locks(resource_type, resource_id)
|
|
10134
|
+
WHERE lock_type = 'exclusive'
|
|
10135
|
+
`);
|
|
10136
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id)`);
|
|
10137
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at)`);
|
|
10120
10138
|
}
|
|
10121
10139
|
var DB_DIR, DB_PATH, _db = null;
|
|
10122
10140
|
var init_database = __esm(() => {
|
|
@@ -10190,6 +10208,78 @@ var init_agents = __esm(() => {
|
|
|
10190
10208
|
AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
|
|
10191
10209
|
});
|
|
10192
10210
|
|
|
10211
|
+
// src/db/rate.ts
|
|
10212
|
+
function ensureRateTable(db) {
|
|
10213
|
+
db.run(`
|
|
10214
|
+
CREATE TABLE IF NOT EXISTS connector_rate_usage (
|
|
10215
|
+
agent_id TEXT NOT NULL,
|
|
10216
|
+
connector TEXT NOT NULL,
|
|
10217
|
+
window_start TEXT NOT NULL,
|
|
10218
|
+
call_count INTEGER NOT NULL DEFAULT 0,
|
|
10219
|
+
PRIMARY KEY (agent_id, connector, window_start)
|
|
10220
|
+
)
|
|
10221
|
+
`);
|
|
10222
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_rate_usage_window ON connector_rate_usage(connector, window_start)`);
|
|
10223
|
+
}
|
|
10224
|
+
function countActiveAgents(db) {
|
|
10225
|
+
const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS2).toISOString();
|
|
10226
|
+
const row = db.query("SELECT COUNT(*) as count FROM agents WHERE last_seen_at > ?").get(cutoff);
|
|
10227
|
+
return Math.max(1, row?.count ?? 1);
|
|
10228
|
+
}
|
|
10229
|
+
function currentWindowStart() {
|
|
10230
|
+
const now3 = Date.now();
|
|
10231
|
+
const windowMs = WINDOW_SECONDS * 1000;
|
|
10232
|
+
return new Date(Math.floor(now3 / windowMs) * windowMs).toISOString();
|
|
10233
|
+
}
|
|
10234
|
+
function checkRateBudget(agentId, connector, connectorLimit, consume = true, db) {
|
|
10235
|
+
const d = db ?? getDatabase();
|
|
10236
|
+
ensureRateTable(d);
|
|
10237
|
+
const activeAgents = countActiveAgents(d);
|
|
10238
|
+
const budget = Math.max(1, Math.floor(connectorLimit / activeAgents));
|
|
10239
|
+
const windowStart = currentWindowStart();
|
|
10240
|
+
const windowMs = WINDOW_SECONDS * 1000;
|
|
10241
|
+
const windowEnd = new Date(Math.floor(Date.now() / windowMs) * windowMs + windowMs);
|
|
10242
|
+
const windowResetsIn = windowEnd.getTime() - Date.now();
|
|
10243
|
+
const row = d.query("SELECT call_count FROM connector_rate_usage WHERE agent_id = ? AND connector = ? AND window_start = ?").get(agentId, connector, windowStart);
|
|
10244
|
+
const used = row?.call_count ?? 0;
|
|
10245
|
+
if (used >= budget) {
|
|
10246
|
+
return {
|
|
10247
|
+
exceeded: true,
|
|
10248
|
+
connector,
|
|
10249
|
+
agent_id: agentId,
|
|
10250
|
+
budget,
|
|
10251
|
+
used,
|
|
10252
|
+
active_agents: activeAgents,
|
|
10253
|
+
window_resets_in_ms: windowResetsIn,
|
|
10254
|
+
message: `Rate budget exceeded for "${connector}" (${used}/${budget} calls used, ${activeAgents} active agent${activeAgents === 1 ? "" : "s"} sharing limit of ${connectorLimit}/min). Resets in ${Math.ceil(windowResetsIn / 1000)}s.`
|
|
10255
|
+
};
|
|
10256
|
+
}
|
|
10257
|
+
if (consume) {
|
|
10258
|
+
d.run(`INSERT INTO connector_rate_usage (agent_id, connector, window_start, call_count)
|
|
10259
|
+
VALUES (?, ?, ?, 1)
|
|
10260
|
+
ON CONFLICT(agent_id, connector, window_start) DO UPDATE SET call_count = call_count + 1`, [agentId, connector, windowStart]);
|
|
10261
|
+
}
|
|
10262
|
+
return {
|
|
10263
|
+
connector,
|
|
10264
|
+
agent_id: agentId,
|
|
10265
|
+
limit: connectorLimit,
|
|
10266
|
+
active_agents: activeAgents,
|
|
10267
|
+
budget,
|
|
10268
|
+
used: consume ? used + 1 : used,
|
|
10269
|
+
remaining: consume ? budget - used - 1 : budget - used,
|
|
10270
|
+
window_start: windowStart,
|
|
10271
|
+
window_resets_in_ms: windowResetsIn
|
|
10272
|
+
};
|
|
10273
|
+
}
|
|
10274
|
+
function getRateBudget(agentId, connector, connectorLimit, db) {
|
|
10275
|
+
return checkRateBudget(agentId, connector, connectorLimit, false, db);
|
|
10276
|
+
}
|
|
10277
|
+
var AGENT_ACTIVE_WINDOW_MS2, WINDOW_SECONDS = 60;
|
|
10278
|
+
var init_rate = __esm(() => {
|
|
10279
|
+
init_database();
|
|
10280
|
+
AGENT_ACTIVE_WINDOW_MS2 = 30 * 60 * 1000;
|
|
10281
|
+
});
|
|
10282
|
+
|
|
10193
10283
|
// src/server/serve.ts
|
|
10194
10284
|
var exports_serve = {};
|
|
10195
10285
|
__export(exports_serve, {
|
|
@@ -10493,6 +10583,14 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
10493
10583
|
deleteAgent(agent.id);
|
|
10494
10584
|
return json({ success: true }, 200, port);
|
|
10495
10585
|
}
|
|
10586
|
+
const rateMatch = path.match(/^\/api\/rate\/([^/]+)\/([^/]+)$/);
|
|
10587
|
+
if (rateMatch && method === "GET") {
|
|
10588
|
+
const [, agentId, connector] = rateMatch;
|
|
10589
|
+
const limit = parseInt(url2.searchParams.get("limit") || "60", 10);
|
|
10590
|
+
const consume = url2.searchParams.get("consume") === "true";
|
|
10591
|
+
const result = consume ? checkRateBudget(agentId, connector, limit) : getRateBudget(agentId, connector, limit);
|
|
10592
|
+
return json(result, 200, port);
|
|
10593
|
+
}
|
|
10496
10594
|
const profilesMatch = path.match(/^\/api\/connectors\/([^/]+)\/profiles$/);
|
|
10497
10595
|
if (profilesMatch && method === "GET") {
|
|
10498
10596
|
const name = profilesMatch[1];
|
|
@@ -10712,6 +10810,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
10712
10810
|
var activityLog, MAX_ACTIVITY_LOG = 100, MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE;
|
|
10713
10811
|
var init_serve = __esm(() => {
|
|
10714
10812
|
init_agents();
|
|
10813
|
+
init_rate();
|
|
10715
10814
|
init_registry();
|
|
10716
10815
|
init_installer();
|
|
10717
10816
|
init_auth();
|
package/bin/mcp.js
CHANGED
|
@@ -26009,6 +26009,24 @@ function migrate(db) {
|
|
|
26009
26009
|
created_at TEXT NOT NULL
|
|
26010
26010
|
)
|
|
26011
26011
|
`);
|
|
26012
|
+
db.run(`
|
|
26013
|
+
CREATE TABLE IF NOT EXISTS resource_locks (
|
|
26014
|
+
id TEXT PRIMARY KEY,
|
|
26015
|
+
resource_type TEXT NOT NULL CHECK(resource_type IN ('connector', 'agent', 'profile', 'token')),
|
|
26016
|
+
resource_id TEXT NOT NULL,
|
|
26017
|
+
agent_id TEXT NOT NULL,
|
|
26018
|
+
lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
|
|
26019
|
+
locked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
26020
|
+
expires_at TEXT NOT NULL
|
|
26021
|
+
)
|
|
26022
|
+
`);
|
|
26023
|
+
db.run(`
|
|
26024
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
|
|
26025
|
+
ON resource_locks(resource_type, resource_id)
|
|
26026
|
+
WHERE lock_type = 'exclusive'
|
|
26027
|
+
`);
|
|
26028
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id)`);
|
|
26029
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at)`);
|
|
26012
26030
|
}
|
|
26013
26031
|
|
|
26014
26032
|
// src/db/agents.ts
|
|
@@ -26068,10 +26086,79 @@ function listAgents(db) {
|
|
|
26068
26086
|
const d = db ?? getDatabase();
|
|
26069
26087
|
return d.query("SELECT * FROM agents ORDER BY name").all();
|
|
26070
26088
|
}
|
|
26089
|
+
|
|
26090
|
+
// src/db/rate.ts
|
|
26091
|
+
var AGENT_ACTIVE_WINDOW_MS2 = 30 * 60 * 1000;
|
|
26092
|
+
var WINDOW_SECONDS = 60;
|
|
26093
|
+
function ensureRateTable(db) {
|
|
26094
|
+
db.run(`
|
|
26095
|
+
CREATE TABLE IF NOT EXISTS connector_rate_usage (
|
|
26096
|
+
agent_id TEXT NOT NULL,
|
|
26097
|
+
connector TEXT NOT NULL,
|
|
26098
|
+
window_start TEXT NOT NULL,
|
|
26099
|
+
call_count INTEGER NOT NULL DEFAULT 0,
|
|
26100
|
+
PRIMARY KEY (agent_id, connector, window_start)
|
|
26101
|
+
)
|
|
26102
|
+
`);
|
|
26103
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_rate_usage_window ON connector_rate_usage(connector, window_start)`);
|
|
26104
|
+
}
|
|
26105
|
+
function countActiveAgents(db) {
|
|
26106
|
+
const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS2).toISOString();
|
|
26107
|
+
const row = db.query("SELECT COUNT(*) as count FROM agents WHERE last_seen_at > ?").get(cutoff);
|
|
26108
|
+
return Math.max(1, row?.count ?? 1);
|
|
26109
|
+
}
|
|
26110
|
+
function currentWindowStart() {
|
|
26111
|
+
const now3 = Date.now();
|
|
26112
|
+
const windowMs = WINDOW_SECONDS * 1000;
|
|
26113
|
+
return new Date(Math.floor(now3 / windowMs) * windowMs).toISOString();
|
|
26114
|
+
}
|
|
26115
|
+
function checkRateBudget(agentId, connector, connectorLimit, consume = true, db) {
|
|
26116
|
+
const d = db ?? getDatabase();
|
|
26117
|
+
ensureRateTable(d);
|
|
26118
|
+
const activeAgents = countActiveAgents(d);
|
|
26119
|
+
const budget = Math.max(1, Math.floor(connectorLimit / activeAgents));
|
|
26120
|
+
const windowStart = currentWindowStart();
|
|
26121
|
+
const windowMs = WINDOW_SECONDS * 1000;
|
|
26122
|
+
const windowEnd = new Date(Math.floor(Date.now() / windowMs) * windowMs + windowMs);
|
|
26123
|
+
const windowResetsIn = windowEnd.getTime() - Date.now();
|
|
26124
|
+
const row = d.query("SELECT call_count FROM connector_rate_usage WHERE agent_id = ? AND connector = ? AND window_start = ?").get(agentId, connector, windowStart);
|
|
26125
|
+
const used = row?.call_count ?? 0;
|
|
26126
|
+
if (used >= budget) {
|
|
26127
|
+
return {
|
|
26128
|
+
exceeded: true,
|
|
26129
|
+
connector,
|
|
26130
|
+
agent_id: agentId,
|
|
26131
|
+
budget,
|
|
26132
|
+
used,
|
|
26133
|
+
active_agents: activeAgents,
|
|
26134
|
+
window_resets_in_ms: windowResetsIn,
|
|
26135
|
+
message: `Rate budget exceeded for "${connector}" (${used}/${budget} calls used, ${activeAgents} active agent${activeAgents === 1 ? "" : "s"} sharing limit of ${connectorLimit}/min). Resets in ${Math.ceil(windowResetsIn / 1000)}s.`
|
|
26136
|
+
};
|
|
26137
|
+
}
|
|
26138
|
+
if (consume) {
|
|
26139
|
+
d.run(`INSERT INTO connector_rate_usage (agent_id, connector, window_start, call_count)
|
|
26140
|
+
VALUES (?, ?, ?, 1)
|
|
26141
|
+
ON CONFLICT(agent_id, connector, window_start) DO UPDATE SET call_count = call_count + 1`, [agentId, connector, windowStart]);
|
|
26142
|
+
}
|
|
26143
|
+
return {
|
|
26144
|
+
connector,
|
|
26145
|
+
agent_id: agentId,
|
|
26146
|
+
limit: connectorLimit,
|
|
26147
|
+
active_agents: activeAgents,
|
|
26148
|
+
budget,
|
|
26149
|
+
used: consume ? used + 1 : used,
|
|
26150
|
+
remaining: consume ? budget - used - 1 : budget - used,
|
|
26151
|
+
window_start: windowStart,
|
|
26152
|
+
window_resets_in_ms: windowResetsIn
|
|
26153
|
+
};
|
|
26154
|
+
}
|
|
26155
|
+
function getRateBudget(agentId, connector, connectorLimit, db) {
|
|
26156
|
+
return checkRateBudget(agentId, connector, connectorLimit, false, db);
|
|
26157
|
+
}
|
|
26071
26158
|
// package.json
|
|
26072
26159
|
var package_default = {
|
|
26073
26160
|
name: "@hasna/connectors",
|
|
26074
|
-
version: "1.1.
|
|
26161
|
+
version: "1.1.14",
|
|
26075
26162
|
description: "Open source connector library - Install API connectors with a single command",
|
|
26076
26163
|
type: "module",
|
|
26077
26164
|
bin: {
|
|
@@ -26668,6 +26755,30 @@ server.registerTool("list_agents", {
|
|
|
26668
26755
|
const agents = listAgents();
|
|
26669
26756
|
return { content: [{ type: "text", text: JSON.stringify(agents, null, 2) }] };
|
|
26670
26757
|
});
|
|
26758
|
+
server.registerTool("check_rate_budget", {
|
|
26759
|
+
title: "Check Rate Budget",
|
|
26760
|
+
description: "Consume one rate budget unit for an agent+connector. Returns budget status or RateExceededError.",
|
|
26761
|
+
inputSchema: {
|
|
26762
|
+
agent_id: exports_external.string(),
|
|
26763
|
+
connector: exports_external.string(),
|
|
26764
|
+
limit: exports_external.number().describe("Connector's documented rate limit (calls/min)")
|
|
26765
|
+
}
|
|
26766
|
+
}, async ({ agent_id, connector, limit }) => {
|
|
26767
|
+
const result = checkRateBudget(agent_id, connector, limit);
|
|
26768
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
26769
|
+
});
|
|
26770
|
+
server.registerTool("get_rate_budget", {
|
|
26771
|
+
title: "Get Rate Budget",
|
|
26772
|
+
description: "Peek at rate budget status without consuming a unit.",
|
|
26773
|
+
inputSchema: {
|
|
26774
|
+
agent_id: exports_external.string(),
|
|
26775
|
+
connector: exports_external.string(),
|
|
26776
|
+
limit: exports_external.number().describe("Connector's documented rate limit (calls/min)")
|
|
26777
|
+
}
|
|
26778
|
+
}, async ({ agent_id, connector, limit }) => {
|
|
26779
|
+
const result = getRateBudget(agent_id, connector, limit);
|
|
26780
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
26781
|
+
});
|
|
26671
26782
|
async function main() {
|
|
26672
26783
|
const transport = new StdioServerTransport;
|
|
26673
26784
|
await server.connect(transport);
|
package/bin/serve.js
CHANGED
|
@@ -53,6 +53,24 @@ function migrate(db) {
|
|
|
53
53
|
created_at TEXT NOT NULL
|
|
54
54
|
)
|
|
55
55
|
`);
|
|
56
|
+
db.run(`
|
|
57
|
+
CREATE TABLE IF NOT EXISTS resource_locks (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
resource_type TEXT NOT NULL CHECK(resource_type IN ('connector', 'agent', 'profile', 'token')),
|
|
60
|
+
resource_id TEXT NOT NULL,
|
|
61
|
+
agent_id TEXT NOT NULL,
|
|
62
|
+
lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
|
|
63
|
+
locked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
64
|
+
expires_at TEXT NOT NULL
|
|
65
|
+
)
|
|
66
|
+
`);
|
|
67
|
+
db.run(`
|
|
68
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
|
|
69
|
+
ON resource_locks(resource_type, resource_id)
|
|
70
|
+
WHERE lock_type = 'exclusive'
|
|
71
|
+
`);
|
|
72
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id)`);
|
|
73
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at)`);
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
// src/db/agents.ts
|
|
@@ -117,6 +135,75 @@ function deleteAgent(id, db) {
|
|
|
117
135
|
return d.run("DELETE FROM agents WHERE id = ?", [id]).changes > 0;
|
|
118
136
|
}
|
|
119
137
|
|
|
138
|
+
// src/db/rate.ts
|
|
139
|
+
var AGENT_ACTIVE_WINDOW_MS2 = 30 * 60 * 1000;
|
|
140
|
+
var WINDOW_SECONDS = 60;
|
|
141
|
+
function ensureRateTable(db) {
|
|
142
|
+
db.run(`
|
|
143
|
+
CREATE TABLE IF NOT EXISTS connector_rate_usage (
|
|
144
|
+
agent_id TEXT NOT NULL,
|
|
145
|
+
connector TEXT NOT NULL,
|
|
146
|
+
window_start TEXT NOT NULL,
|
|
147
|
+
call_count INTEGER NOT NULL DEFAULT 0,
|
|
148
|
+
PRIMARY KEY (agent_id, connector, window_start)
|
|
149
|
+
)
|
|
150
|
+
`);
|
|
151
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_rate_usage_window ON connector_rate_usage(connector, window_start)`);
|
|
152
|
+
}
|
|
153
|
+
function countActiveAgents(db) {
|
|
154
|
+
const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS2).toISOString();
|
|
155
|
+
const row = db.query("SELECT COUNT(*) as count FROM agents WHERE last_seen_at > ?").get(cutoff);
|
|
156
|
+
return Math.max(1, row?.count ?? 1);
|
|
157
|
+
}
|
|
158
|
+
function currentWindowStart() {
|
|
159
|
+
const now3 = Date.now();
|
|
160
|
+
const windowMs = WINDOW_SECONDS * 1000;
|
|
161
|
+
return new Date(Math.floor(now3 / windowMs) * windowMs).toISOString();
|
|
162
|
+
}
|
|
163
|
+
function checkRateBudget(agentId, connector, connectorLimit, consume = true, db) {
|
|
164
|
+
const d = db ?? getDatabase();
|
|
165
|
+
ensureRateTable(d);
|
|
166
|
+
const activeAgents = countActiveAgents(d);
|
|
167
|
+
const budget = Math.max(1, Math.floor(connectorLimit / activeAgents));
|
|
168
|
+
const windowStart = currentWindowStart();
|
|
169
|
+
const windowMs = WINDOW_SECONDS * 1000;
|
|
170
|
+
const windowEnd = new Date(Math.floor(Date.now() / windowMs) * windowMs + windowMs);
|
|
171
|
+
const windowResetsIn = windowEnd.getTime() - Date.now();
|
|
172
|
+
const row = d.query("SELECT call_count FROM connector_rate_usage WHERE agent_id = ? AND connector = ? AND window_start = ?").get(agentId, connector, windowStart);
|
|
173
|
+
const used = row?.call_count ?? 0;
|
|
174
|
+
if (used >= budget) {
|
|
175
|
+
return {
|
|
176
|
+
exceeded: true,
|
|
177
|
+
connector,
|
|
178
|
+
agent_id: agentId,
|
|
179
|
+
budget,
|
|
180
|
+
used,
|
|
181
|
+
active_agents: activeAgents,
|
|
182
|
+
window_resets_in_ms: windowResetsIn,
|
|
183
|
+
message: `Rate budget exceeded for "${connector}" (${used}/${budget} calls used, ${activeAgents} active agent${activeAgents === 1 ? "" : "s"} sharing limit of ${connectorLimit}/min). Resets in ${Math.ceil(windowResetsIn / 1000)}s.`
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (consume) {
|
|
187
|
+
d.run(`INSERT INTO connector_rate_usage (agent_id, connector, window_start, call_count)
|
|
188
|
+
VALUES (?, ?, ?, 1)
|
|
189
|
+
ON CONFLICT(agent_id, connector, window_start) DO UPDATE SET call_count = call_count + 1`, [agentId, connector, windowStart]);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
connector,
|
|
193
|
+
agent_id: agentId,
|
|
194
|
+
limit: connectorLimit,
|
|
195
|
+
active_agents: activeAgents,
|
|
196
|
+
budget,
|
|
197
|
+
used: consume ? used + 1 : used,
|
|
198
|
+
remaining: consume ? budget - used - 1 : budget - used,
|
|
199
|
+
window_start: windowStart,
|
|
200
|
+
window_resets_in_ms: windowResetsIn
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function getRateBudget(agentId, connector, connectorLimit, db) {
|
|
204
|
+
return checkRateBudget(agentId, connector, connectorLimit, false, db);
|
|
205
|
+
}
|
|
206
|
+
|
|
120
207
|
// src/server/serve.ts
|
|
121
208
|
import { join as join6, dirname as dirname3, extname, basename } from "path";
|
|
122
209
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
@@ -6979,6 +7066,14 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
6979
7066
|
deleteAgent(agent.id);
|
|
6980
7067
|
return json({ success: true }, 200, port);
|
|
6981
7068
|
}
|
|
7069
|
+
const rateMatch = path.match(/^\/api\/rate\/([^/]+)\/([^/]+)$/);
|
|
7070
|
+
if (rateMatch && method === "GET") {
|
|
7071
|
+
const [, agentId, connector] = rateMatch;
|
|
7072
|
+
const limit = parseInt(url2.searchParams.get("limit") || "60", 10);
|
|
7073
|
+
const consume = url2.searchParams.get("consume") === "true";
|
|
7074
|
+
const result = consume ? checkRateBudget(agentId, connector, limit) : getRateBudget(agentId, connector, limit);
|
|
7075
|
+
return json(result, 200, port);
|
|
7076
|
+
}
|
|
6982
7077
|
const profilesMatch = path.match(/^\/api\/connectors\/([^/]+)\/profiles$/);
|
|
6983
7078
|
if (profilesMatch && method === "GET") {
|
|
6984
7079
|
const name = profilesMatch[1];
|
package/dist/db/database.d.ts
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource lock helpers for open-connectors.
|
|
3
|
+
*
|
|
4
|
+
* Same interface as open-mementos src/db/locks.ts — standard pattern across
|
|
5
|
+
* all @hasna apps. Resource types are scoped to what connectors manages:
|
|
6
|
+
* connector — the connector itself (install/uninstall ops)
|
|
7
|
+
* agent — agent records
|
|
8
|
+
* profile — connector auth profiles
|
|
9
|
+
* token — OAuth token refresh operations
|
|
10
|
+
*/
|
|
11
|
+
import type { Database } from "bun:sqlite";
|
|
12
|
+
export type ResourceType = "connector" | "agent" | "profile" | "token";
|
|
13
|
+
export type LockType = "advisory" | "exclusive";
|
|
14
|
+
export interface ResourceLock {
|
|
15
|
+
id: string;
|
|
16
|
+
resource_type: ResourceType;
|
|
17
|
+
resource_id: string;
|
|
18
|
+
agent_id: string;
|
|
19
|
+
lock_type: LockType;
|
|
20
|
+
locked_at: string;
|
|
21
|
+
expires_at: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Acquire a lock on a resource.
|
|
25
|
+
* - advisory: multiple agents can hold advisory locks simultaneously
|
|
26
|
+
* - exclusive: only one agent can hold an exclusive lock at a time
|
|
27
|
+
*
|
|
28
|
+
* Returns the lock if acquired, null if blocked by an existing exclusive lock.
|
|
29
|
+
* TTL is in seconds (default: 5 minutes).
|
|
30
|
+
*/
|
|
31
|
+
export declare function acquireLock(agentId: string, resourceType: ResourceType, resourceId: string, lockType?: LockType, ttlSeconds?: number, db?: Database): ResourceLock | null;
|
|
32
|
+
/**
|
|
33
|
+
* Release a specific lock by ID. Only the owning agent can release it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function releaseLock(lockId: string, agentId: string, db?: Database): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Release all locks held by an agent on a specific resource.
|
|
38
|
+
*/
|
|
39
|
+
export declare function releaseResourceLocks(agentId: string, resourceType: ResourceType, resourceId: string, db?: Database): number;
|
|
40
|
+
/**
|
|
41
|
+
* Release all locks held by an agent (e.g., on session end).
|
|
42
|
+
*/
|
|
43
|
+
export declare function releaseAllAgentLocks(agentId: string, db?: Database): number;
|
|
44
|
+
/**
|
|
45
|
+
* Check if a resource is currently locked. Returns active lock(s) or empty array.
|
|
46
|
+
*/
|
|
47
|
+
export declare function checkLock(resourceType: ResourceType, resourceId: string, lockType?: LockType, db?: Database): ResourceLock[];
|
|
48
|
+
/**
|
|
49
|
+
* Check if a specific agent holds a lock on a resource.
|
|
50
|
+
*/
|
|
51
|
+
export declare function agentHoldsLock(agentId: string, resourceType: ResourceType, resourceId: string, lockType?: LockType, db?: Database): ResourceLock | null;
|
|
52
|
+
/**
|
|
53
|
+
* List all active locks for an agent.
|
|
54
|
+
*/
|
|
55
|
+
export declare function listAgentLocks(agentId: string, db?: Database): ResourceLock[];
|
|
56
|
+
/**
|
|
57
|
+
* Delete all expired locks. Called automatically by other lock functions.
|
|
58
|
+
*/
|
|
59
|
+
export declare function cleanExpiredLocks(db?: Database): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-aware rate budget splitting for connector operations.
|
|
3
|
+
*
|
|
4
|
+
* When multiple agents share a connector, the connector's rate limit is split
|
|
5
|
+
* evenly among active agents (last heartbeat < 30min). Each agent gets:
|
|
6
|
+
* budget = Math.floor(connectorRateLimit / activeAgentCount)
|
|
7
|
+
*
|
|
8
|
+
* Rate usage is tracked per-agent-per-connector in the DB:
|
|
9
|
+
* connector_rate_usage(agent_id, connector, window_start, call_count)
|
|
10
|
+
*
|
|
11
|
+
* The window resets every WINDOW_SECONDS (default: 60s = per-minute rate).
|
|
12
|
+
*/
|
|
13
|
+
import type { Database } from "bun:sqlite";
|
|
14
|
+
export interface RateBudget {
|
|
15
|
+
connector: string;
|
|
16
|
+
agent_id: string;
|
|
17
|
+
limit: number;
|
|
18
|
+
active_agents: number;
|
|
19
|
+
budget: number;
|
|
20
|
+
used: number;
|
|
21
|
+
remaining: number;
|
|
22
|
+
window_start: string;
|
|
23
|
+
window_resets_in_ms: number;
|
|
24
|
+
}
|
|
25
|
+
export interface RateExceededError {
|
|
26
|
+
exceeded: true;
|
|
27
|
+
connector: string;
|
|
28
|
+
agent_id: string;
|
|
29
|
+
budget: number;
|
|
30
|
+
used: number;
|
|
31
|
+
active_agents: number;
|
|
32
|
+
window_resets_in_ms: number;
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function isRateExceeded(result: RateBudget | RateExceededError): result is RateExceededError;
|
|
36
|
+
/**
|
|
37
|
+
* Ensure the rate_usage table exists (idempotent).
|
|
38
|
+
*/
|
|
39
|
+
export declare function ensureRateTable(db: Database): void;
|
|
40
|
+
/**
|
|
41
|
+
* Check and optionally consume one rate budget unit for an agent+connector.
|
|
42
|
+
*
|
|
43
|
+
* @param agentId - Agent ID (from agents table)
|
|
44
|
+
* @param connector - Connector name (e.g. "stripe")
|
|
45
|
+
* @param connectorLimit - The connector's documented rate limit (calls/min)
|
|
46
|
+
* @param consume - If true, increment the call counter. If false, just peek.
|
|
47
|
+
* @param db - Optional DB instance (defaults to singleton)
|
|
48
|
+
*/
|
|
49
|
+
export declare function checkRateBudget(agentId: string, connector: string, connectorLimit: number, consume?: boolean, db?: Database): RateBudget | RateExceededError;
|
|
50
|
+
/**
|
|
51
|
+
* Get rate budget status without consuming a unit.
|
|
52
|
+
*/
|
|
53
|
+
export declare function getRateBudget(agentId: string, connector: string, connectorLimit: number, db?: Database): RateBudget | RateExceededError;
|
|
54
|
+
/**
|
|
55
|
+
* Clean up old rate windows (older than 2 windows).
|
|
56
|
+
*/
|
|
57
|
+
export declare function cleanExpiredRateWindows(db?: Database): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|