@desplega.ai/agent-swarm 1.83.1 → 1.83.2
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/openapi.json +139 -8
- package/package.json +1 -1
- package/src/artifact-sdk/server.ts +23 -1
- package/src/be/budget-admission.ts +28 -4
- package/src/be/budget-refusal-notify.ts +19 -3
- package/src/be/db-queries/oauth.ts +43 -0
- package/src/be/db.ts +35 -2
- package/src/be/migrations/074_user_budget_scope.sql +85 -0
- package/src/commands/resume-session.ts +118 -0
- package/src/commands/runner.ts +137 -67
- package/src/http/core.ts +4 -1
- package/src/http/index.ts +16 -0
- package/src/http/integrations.ts +26 -0
- package/src/http/mcp-user.ts +111 -0
- package/src/http/poll.ts +19 -5
- package/src/http/schedules.ts +1 -1
- package/src/http/users.ts +107 -2
- package/src/jira/client.ts +3 -5
- package/src/jira/oauth.ts +1 -0
- package/src/jira/sync.ts +2 -2
- package/src/oauth/ensure-token.ts +1 -0
- package/src/oauth/wrapper.ts +38 -7
- package/src/providers/claude-adapter.ts +7 -2
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +30 -0
- package/src/providers/opencode-adapter.ts +149 -14
- package/src/providers/pi-mono-adapter.ts +41 -1
- package/src/providers/types.ts +1 -1
- package/src/server-user.ts +117 -0
- package/src/tests/artifact-sdk.test.ts +23 -19
- package/src/tests/budget-user-scope.test.ts +376 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/codex-adapter.test.ts +192 -0
- package/src/tests/codex-rate-limit-parse.test.ts +256 -0
- package/src/tests/db-queries-oauth.test.ts +43 -0
- package/src/tests/ensure-token.test.ts +93 -0
- package/src/tests/error-tracker.test.ts +52 -0
- package/src/tests/fetch-resolved-env.test.ts +33 -20
- package/src/tests/http-users.test.ts +29 -1
- package/src/tests/mcp-user-route.test.ts +325 -0
- package/src/tests/opencode-adapter.test.ts +75 -0
- package/src/tests/pi-mono-adapter.test.ts +21 -1
- package/src/tests/rate-limit-event.test.ts +69 -6
- package/src/tests/resume-session.test.ts +93 -0
- package/src/tests/task-tools-ctx.test.ts +100 -0
- package/src/tests/task-tools-ownership.test.ts +167 -0
- package/src/tests/user-token-routes.test.ts +221 -0
- package/src/tools/cancel-task.ts +137 -83
- package/src/tools/get-task-details.ts +73 -59
- package/src/tools/get-tasks.ts +134 -126
- package/src/tools/send-task.ts +312 -312
- package/src/tools/task-action.ts +464 -367
- package/src/tools/task-tool-ctx.ts +43 -0
- package/src/types.ts +6 -2
- package/src/utils/error-tracker.ts +122 -9
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.83.
|
|
5
|
+
"version": "1.83.2",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
|
@@ -1440,7 +1440,8 @@
|
|
|
1440
1440
|
"type": "string",
|
|
1441
1441
|
"enum": [
|
|
1442
1442
|
"global",
|
|
1443
|
-
"agent"
|
|
1443
|
+
"agent",
|
|
1444
|
+
"user"
|
|
1444
1445
|
]
|
|
1445
1446
|
},
|
|
1446
1447
|
"scopeId": {
|
|
@@ -1526,7 +1527,8 @@
|
|
|
1526
1527
|
"type": "string",
|
|
1527
1528
|
"enum": [
|
|
1528
1529
|
"agent",
|
|
1529
|
-
"global"
|
|
1530
|
+
"global",
|
|
1531
|
+
"user"
|
|
1530
1532
|
]
|
|
1531
1533
|
},
|
|
1532
1534
|
"agentSpendUsd": {
|
|
@@ -1553,6 +1555,18 @@
|
|
|
1553
1555
|
"null"
|
|
1554
1556
|
]
|
|
1555
1557
|
},
|
|
1558
|
+
"userSpendUsd": {
|
|
1559
|
+
"type": [
|
|
1560
|
+
"number",
|
|
1561
|
+
"null"
|
|
1562
|
+
]
|
|
1563
|
+
},
|
|
1564
|
+
"userBudgetUsd": {
|
|
1565
|
+
"type": [
|
|
1566
|
+
"number",
|
|
1567
|
+
"null"
|
|
1568
|
+
]
|
|
1569
|
+
},
|
|
1556
1570
|
"followUpTaskId": {
|
|
1557
1571
|
"type": [
|
|
1558
1572
|
"string",
|
|
@@ -1600,7 +1614,8 @@
|
|
|
1600
1614
|
"type": "string",
|
|
1601
1615
|
"enum": [
|
|
1602
1616
|
"global",
|
|
1603
|
-
"agent"
|
|
1617
|
+
"agent",
|
|
1618
|
+
"user"
|
|
1604
1619
|
]
|
|
1605
1620
|
},
|
|
1606
1621
|
"required": true,
|
|
@@ -1631,7 +1646,8 @@
|
|
|
1631
1646
|
"type": "string",
|
|
1632
1647
|
"enum": [
|
|
1633
1648
|
"global",
|
|
1634
|
-
"agent"
|
|
1649
|
+
"agent",
|
|
1650
|
+
"user"
|
|
1635
1651
|
]
|
|
1636
1652
|
},
|
|
1637
1653
|
"scopeId": {
|
|
@@ -1680,7 +1696,8 @@
|
|
|
1680
1696
|
"type": "string",
|
|
1681
1697
|
"enum": [
|
|
1682
1698
|
"global",
|
|
1683
|
-
"agent"
|
|
1699
|
+
"agent",
|
|
1700
|
+
"user"
|
|
1684
1701
|
]
|
|
1685
1702
|
},
|
|
1686
1703
|
"required": true,
|
|
@@ -1729,7 +1746,8 @@
|
|
|
1729
1746
|
"type": "string",
|
|
1730
1747
|
"enum": [
|
|
1731
1748
|
"global",
|
|
1732
|
-
"agent"
|
|
1749
|
+
"agent",
|
|
1750
|
+
"user"
|
|
1733
1751
|
]
|
|
1734
1752
|
},
|
|
1735
1753
|
"scopeId": {
|
|
@@ -1778,7 +1796,8 @@
|
|
|
1778
1796
|
"type": "string",
|
|
1779
1797
|
"enum": [
|
|
1780
1798
|
"global",
|
|
1781
|
-
"agent"
|
|
1799
|
+
"agent",
|
|
1800
|
+
"user"
|
|
1782
1801
|
]
|
|
1783
1802
|
},
|
|
1784
1803
|
"required": true,
|
|
@@ -3362,6 +3381,24 @@
|
|
|
3362
3381
|
}
|
|
3363
3382
|
}
|
|
3364
3383
|
},
|
|
3384
|
+
"/api/integrations/mcp-user/config": {
|
|
3385
|
+
"get": {
|
|
3386
|
+
"summary": "Get server-derived config for end-user MCP clients.",
|
|
3387
|
+
"tags": [
|
|
3388
|
+
"Integrations"
|
|
3389
|
+
],
|
|
3390
|
+
"security": [
|
|
3391
|
+
{
|
|
3392
|
+
"bearerAuth": []
|
|
3393
|
+
}
|
|
3394
|
+
],
|
|
3395
|
+
"responses": {
|
|
3396
|
+
"200": {
|
|
3397
|
+
"description": "Server-derived MCP user config. `mcpBaseUrl` is the API server base URL and `mcpUserUrl` appends `/mcp-user`."
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
},
|
|
3365
3402
|
"/api/kv/{key}": {
|
|
3366
3403
|
"get": {
|
|
3367
3404
|
"summary": "Get a KV entry by key (namespace resolved from request headers)",
|
|
@@ -10989,6 +11026,100 @@
|
|
|
10989
11026
|
}
|
|
10990
11027
|
}
|
|
10991
11028
|
},
|
|
11029
|
+
"/api/users/{id}/mcp-tokens": {
|
|
11030
|
+
"post": {
|
|
11031
|
+
"summary": "Mint a one-time plaintext MCP token for a user",
|
|
11032
|
+
"description": "Returns the plaintext token exactly once. Subsequent reads only expose token summaries.",
|
|
11033
|
+
"tags": [
|
|
11034
|
+
"Users"
|
|
11035
|
+
],
|
|
11036
|
+
"security": [
|
|
11037
|
+
{
|
|
11038
|
+
"bearerAuth": []
|
|
11039
|
+
}
|
|
11040
|
+
],
|
|
11041
|
+
"parameters": [
|
|
11042
|
+
{
|
|
11043
|
+
"schema": {
|
|
11044
|
+
"type": "string"
|
|
11045
|
+
},
|
|
11046
|
+
"required": true,
|
|
11047
|
+
"name": "id",
|
|
11048
|
+
"in": "path"
|
|
11049
|
+
}
|
|
11050
|
+
],
|
|
11051
|
+
"requestBody": {
|
|
11052
|
+
"content": {
|
|
11053
|
+
"application/json": {
|
|
11054
|
+
"schema": {
|
|
11055
|
+
"type": "object",
|
|
11056
|
+
"properties": {
|
|
11057
|
+
"label": {
|
|
11058
|
+
"type": [
|
|
11059
|
+
"string",
|
|
11060
|
+
"null"
|
|
11061
|
+
]
|
|
11062
|
+
}
|
|
11063
|
+
}
|
|
11064
|
+
}
|
|
11065
|
+
}
|
|
11066
|
+
}
|
|
11067
|
+
},
|
|
11068
|
+
"responses": {
|
|
11069
|
+
"200": {
|
|
11070
|
+
"description": "Minted token plaintext, token summary and composed user"
|
|
11071
|
+
},
|
|
11072
|
+
"401": {
|
|
11073
|
+
"description": "Unauthorized"
|
|
11074
|
+
},
|
|
11075
|
+
"404": {
|
|
11076
|
+
"description": "User not found"
|
|
11077
|
+
}
|
|
11078
|
+
}
|
|
11079
|
+
}
|
|
11080
|
+
},
|
|
11081
|
+
"/api/users/{id}/mcp-tokens/{tokenId}": {
|
|
11082
|
+
"delete": {
|
|
11083
|
+
"summary": "Revoke a user's MCP token",
|
|
11084
|
+
"tags": [
|
|
11085
|
+
"Users"
|
|
11086
|
+
],
|
|
11087
|
+
"security": [
|
|
11088
|
+
{
|
|
11089
|
+
"bearerAuth": []
|
|
11090
|
+
}
|
|
11091
|
+
],
|
|
11092
|
+
"parameters": [
|
|
11093
|
+
{
|
|
11094
|
+
"schema": {
|
|
11095
|
+
"type": "string"
|
|
11096
|
+
},
|
|
11097
|
+
"required": true,
|
|
11098
|
+
"name": "id",
|
|
11099
|
+
"in": "path"
|
|
11100
|
+
},
|
|
11101
|
+
{
|
|
11102
|
+
"schema": {
|
|
11103
|
+
"type": "string"
|
|
11104
|
+
},
|
|
11105
|
+
"required": true,
|
|
11106
|
+
"name": "tokenId",
|
|
11107
|
+
"in": "path"
|
|
11108
|
+
}
|
|
11109
|
+
],
|
|
11110
|
+
"responses": {
|
|
11111
|
+
"200": {
|
|
11112
|
+
"description": "Composed user after token revocation"
|
|
11113
|
+
},
|
|
11114
|
+
"401": {
|
|
11115
|
+
"description": "Unauthorized"
|
|
11116
|
+
},
|
|
11117
|
+
"404": {
|
|
11118
|
+
"description": "User or token not found"
|
|
11119
|
+
}
|
|
11120
|
+
}
|
|
11121
|
+
}
|
|
11122
|
+
},
|
|
10992
11123
|
"/api/users/{id}/merge": {
|
|
10993
11124
|
"post": {
|
|
10994
11125
|
"summary": "Merge another user into this one — moves identities + email aliases, deletes source",
|
package/package.json
CHANGED
|
@@ -22,6 +22,28 @@ export interface ArtifactServer {
|
|
|
22
22
|
tunnel: ReturnType<typeof createTunnel> extends Promise<infer T> ? T | null : never;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const NativeResponse = globalThis.Response;
|
|
26
|
+
|
|
27
|
+
type NativeResponseArgs = ConstructorParameters<typeof Response>;
|
|
28
|
+
|
|
29
|
+
export function createBunResponse(
|
|
30
|
+
body?: NativeResponseArgs[0],
|
|
31
|
+
init?: NativeResponseArgs[1],
|
|
32
|
+
): Response {
|
|
33
|
+
return new NativeResponse(body, init);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createBunHonoFetchHandler(app: Hono): (req: Request) => Promise<Response> {
|
|
37
|
+
return async (req: Request) => {
|
|
38
|
+
const response = await app.fetch(req);
|
|
39
|
+
return createBunResponse(response.body, {
|
|
40
|
+
status: response.status,
|
|
41
|
+
statusText: response.statusText,
|
|
42
|
+
headers: response.headers,
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
export function createArtifactServer(opts: ArtifactServerOptions): ArtifactServer {
|
|
26
48
|
const agentId = process.env.AGENT_ID || "unknown";
|
|
27
49
|
const apiKey = getApiKey();
|
|
@@ -100,7 +122,7 @@ export function createArtifactServer(opts: ArtifactServerOptions): ArtifactServe
|
|
|
100
122
|
|
|
101
123
|
async start() {
|
|
102
124
|
actualPort = opts.port || (await getAvailablePort());
|
|
103
|
-
server = Bun.serve({ port: actualPort, fetch: app
|
|
125
|
+
server = Bun.serve({ port: actualPort, fetch: createBunHonoFetchHandler(app) });
|
|
104
126
|
artifact.port = actualPort;
|
|
105
127
|
|
|
106
128
|
const subdomain = opts.subdomain || `${agentId}-${opts.name}`;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// /api/poll pool, MCP `task-action` `accept`) translate refusals into the
|
|
9
9
|
// `budget_refused` trigger envelope.
|
|
10
10
|
|
|
11
|
-
import { getBudget, getDailySpendForAgent, getDailySpendGlobal } from "./db";
|
|
11
|
+
import { getBudget, getDailySpendForAgent, getDailySpendForUser, getDailySpendGlobal } from "./db";
|
|
12
12
|
|
|
13
13
|
export interface BudgetAdmissionAllowed {
|
|
14
14
|
allowed: true;
|
|
@@ -16,11 +16,13 @@ export interface BudgetAdmissionAllowed {
|
|
|
16
16
|
|
|
17
17
|
export interface BudgetAdmissionRefused {
|
|
18
18
|
allowed: false;
|
|
19
|
-
cause: "agent" | "global";
|
|
19
|
+
cause: "agent" | "global" | "user";
|
|
20
20
|
agentSpend?: number;
|
|
21
21
|
agentBudget?: number;
|
|
22
22
|
globalSpend?: number;
|
|
23
23
|
globalBudget?: number;
|
|
24
|
+
userSpend?: number;
|
|
25
|
+
userBudget?: number;
|
|
24
26
|
/** ISO 8601 of the next UTC midnight (the moment daily spend rolls over). */
|
|
25
27
|
resetAt: string;
|
|
26
28
|
}
|
|
@@ -60,12 +62,17 @@ function nextUtcMidnight(now: Date): string {
|
|
|
60
62
|
* 0. Kill-switch (`BUDGET_ADMISSION_DISABLED=true`) ⇒ allowed.
|
|
61
63
|
* 1. Global budget set + global daily spend ≥ ceiling ⇒ refused (`global`).
|
|
62
64
|
* 2. Agent budget set + agent daily spend ≥ ceiling ⇒ refused (`agent`).
|
|
63
|
-
* 3.
|
|
65
|
+
* 3. User budget set + user's task spend ≥ ceiling ⇒ refused (`user`).
|
|
66
|
+
* 4. Otherwise ⇒ allowed.
|
|
64
67
|
*
|
|
65
68
|
* Global is checked first by design: a tripped global budget halts the entire
|
|
66
69
|
* swarm regardless of any single agent's spend.
|
|
67
70
|
*/
|
|
68
|
-
export function canClaim(
|
|
71
|
+
export function canClaim(
|
|
72
|
+
agentId: string,
|
|
73
|
+
nowUtc: Date,
|
|
74
|
+
requestedByUserId?: string,
|
|
75
|
+
): BudgetAdmissionResult {
|
|
69
76
|
if (process.env.BUDGET_ADMISSION_DISABLED === "true") {
|
|
70
77
|
if (!killSwitchWarned) {
|
|
71
78
|
killSwitchWarned = true;
|
|
@@ -109,6 +116,23 @@ export function canClaim(agentId: string, nowUtc: Date): BudgetAdmissionResult {
|
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
// 3. Per-user budget gate. Only applies to tasks tied to a canonical user.
|
|
120
|
+
if (requestedByUserId) {
|
|
121
|
+
const userBudget = getBudget("user", requestedByUserId);
|
|
122
|
+
if (userBudget !== null) {
|
|
123
|
+
const userSpend = getDailySpendForUser(requestedByUserId, dateUtc);
|
|
124
|
+
if (userSpend >= userBudget.dailyBudgetUsd) {
|
|
125
|
+
return {
|
|
126
|
+
allowed: false,
|
|
127
|
+
cause: "user",
|
|
128
|
+
userSpend,
|
|
129
|
+
userBudget: userBudget.dailyBudgetUsd,
|
|
130
|
+
resetAt,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
112
136
|
return { allowed: true };
|
|
113
137
|
}
|
|
114
138
|
|
|
@@ -18,22 +18,28 @@ import {
|
|
|
18
18
|
createTaskExtended,
|
|
19
19
|
getAgentById,
|
|
20
20
|
getLeadAgent,
|
|
21
|
+
getUserById,
|
|
21
22
|
setBudgetRefusalFollowUpTaskId,
|
|
22
23
|
} from "./db";
|
|
23
24
|
|
|
24
25
|
export interface BudgetRefusalContext {
|
|
25
26
|
/** The task that was refused (provides Slack context, description). */
|
|
26
|
-
task: Pick<
|
|
27
|
+
task: Pick<
|
|
28
|
+
AgentTask,
|
|
29
|
+
"id" | "task" | "requestedByUserId" | "slackChannelId" | "slackThreadTs" | "slackUserId"
|
|
30
|
+
>;
|
|
27
31
|
/** Refusing agent id. */
|
|
28
32
|
agentId: string;
|
|
29
33
|
/** UTC date `YYYY-MM-DD` used as the dedup key alongside `task.id`. */
|
|
30
34
|
date: string;
|
|
31
35
|
/** Refusal cause. */
|
|
32
|
-
cause: "agent" | "global";
|
|
36
|
+
cause: "agent" | "global" | "user";
|
|
33
37
|
agentSpendUsd?: number;
|
|
34
38
|
agentBudgetUsd?: number;
|
|
35
39
|
globalSpendUsd?: number;
|
|
36
40
|
globalBudgetUsd?: number;
|
|
41
|
+
userSpendUsd?: number;
|
|
42
|
+
userBudgetUsd?: number;
|
|
37
43
|
/** ISO 8601 of the next UTC midnight (when the daily budget resets). */
|
|
38
44
|
resetAt: string;
|
|
39
45
|
}
|
|
@@ -49,6 +55,9 @@ function formatSpendSummary(ctx: BudgetRefusalContext): string {
|
|
|
49
55
|
if (ctx.cause === "agent") {
|
|
50
56
|
return `${fmt(ctx.agentSpendUsd)} / ${fmt(ctx.agentBudgetUsd)}`;
|
|
51
57
|
}
|
|
58
|
+
if (ctx.cause === "user") {
|
|
59
|
+
return `${fmt(ctx.userSpendUsd)} / ${fmt(ctx.userBudgetUsd)}`;
|
|
60
|
+
}
|
|
52
61
|
return `${fmt(ctx.globalSpendUsd)} / ${fmt(ctx.globalBudgetUsd)}`;
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -79,6 +88,10 @@ export function emitBudgetRefusalSideEffects(ctx: BudgetRefusalContext, inserted
|
|
|
79
88
|
if (leadAgent) {
|
|
80
89
|
const refusingAgent = getAgentById(ctx.agentId);
|
|
81
90
|
const agentName = refusingAgent?.name || ctx.agentId.slice(0, 8);
|
|
91
|
+
const userName = ctx.task.requestedByUserId
|
|
92
|
+
? (getUserById(ctx.task.requestedByUserId)?.name ??
|
|
93
|
+
ctx.task.requestedByUserId.slice(0, 8))
|
|
94
|
+
: undefined;
|
|
82
95
|
const taskDesc = ctx.task.task.slice(0, 200);
|
|
83
96
|
const spendSummary = formatSpendSummary(ctx);
|
|
84
97
|
|
|
@@ -86,7 +99,8 @@ export function emitBudgetRefusalSideEffects(ctx: BudgetRefusalContext, inserted
|
|
|
86
99
|
"task.budget.refused",
|
|
87
100
|
{
|
|
88
101
|
cause: ctx.cause,
|
|
89
|
-
agent_name:
|
|
102
|
+
agent_name:
|
|
103
|
+
ctx.cause === "user" && userName ? `${agentName} for user ${userName}` : agentName,
|
|
90
104
|
task_desc: taskDesc,
|
|
91
105
|
spend_summary: spendSummary,
|
|
92
106
|
reset_at: ctx.resetAt,
|
|
@@ -135,6 +149,8 @@ export function emitBudgetRefusalSideEffects(ctx: BudgetRefusalContext, inserted
|
|
|
135
149
|
agentBudgetUsd: ctx.agentBudgetUsd,
|
|
136
150
|
globalSpendUsd: ctx.globalSpendUsd,
|
|
137
151
|
globalBudgetUsd: ctx.globalBudgetUsd,
|
|
152
|
+
userSpendUsd: ctx.userSpendUsd,
|
|
153
|
+
userBudgetUsd: ctx.userBudgetUsd,
|
|
138
154
|
resetAt: ctx.resetAt,
|
|
139
155
|
});
|
|
140
156
|
});
|
|
@@ -127,6 +127,49 @@ export function storeOAuthTokens(
|
|
|
127
127
|
.run(provider, data.accessToken, data.refreshToken ?? null, data.expiresAt, data.scope ?? null);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
export function updateOAuthTokensAfterRefresh(
|
|
131
|
+
provider: string,
|
|
132
|
+
expectedRefreshToken: string,
|
|
133
|
+
data: {
|
|
134
|
+
accessToken: string;
|
|
135
|
+
refreshToken: string;
|
|
136
|
+
expiresAt: string;
|
|
137
|
+
scope?: string | null;
|
|
138
|
+
},
|
|
139
|
+
): void {
|
|
140
|
+
const result = getDb()
|
|
141
|
+
.query(
|
|
142
|
+
`UPDATE oauth_tokens
|
|
143
|
+
SET accessToken = ?,
|
|
144
|
+
refreshToken = ?,
|
|
145
|
+
expiresAt = ?,
|
|
146
|
+
scope = COALESCE(?, scope),
|
|
147
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
148
|
+
WHERE provider = ? AND refreshToken = ?`,
|
|
149
|
+
)
|
|
150
|
+
.run(
|
|
151
|
+
data.accessToken,
|
|
152
|
+
data.refreshToken,
|
|
153
|
+
data.expiresAt,
|
|
154
|
+
data.scope ?? null,
|
|
155
|
+
provider,
|
|
156
|
+
expectedRefreshToken,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (result.changes === 1) return;
|
|
160
|
+
|
|
161
|
+
const current = getOAuthTokens(provider);
|
|
162
|
+
if (!current) {
|
|
163
|
+
throw new Error(`OAuth token refresh persistence failed for ${provider}: token row missing`);
|
|
164
|
+
}
|
|
165
|
+
if (current.refreshToken !== expectedRefreshToken) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`OAuth token refresh persistence failed for ${provider}: stored refresh token changed during refresh`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`OAuth token refresh persistence failed for ${provider}: no rows updated`);
|
|
171
|
+
}
|
|
172
|
+
|
|
130
173
|
export function deleteOAuthTokens(provider: string): void {
|
|
131
174
|
getDb().query("DELETE FROM oauth_tokens WHERE provider = ?").run(provider);
|
|
132
175
|
}
|
package/src/be/db.ts
CHANGED
|
@@ -1401,6 +1401,8 @@ export interface TaskFilters {
|
|
|
1401
1401
|
source?: AgentTaskSource[];
|
|
1402
1402
|
/** ISO 8601 timestamp; only return tasks where createdAt >= this. */
|
|
1403
1403
|
createdAfter?: string;
|
|
1404
|
+
/** Only return tasks requested by this canonical user. NULL rows are excluded. */
|
|
1405
|
+
requestedByUserId?: string;
|
|
1404
1406
|
limit?: number;
|
|
1405
1407
|
offset?: number;
|
|
1406
1408
|
includeHeartbeat?: boolean;
|
|
@@ -1484,6 +1486,11 @@ export function getAllTasks(
|
|
|
1484
1486
|
params.push(filters.createdAfter);
|
|
1485
1487
|
}
|
|
1486
1488
|
|
|
1489
|
+
if (filters?.requestedByUserId) {
|
|
1490
|
+
conditions.push("requestedByUserId = ?");
|
|
1491
|
+
params.push(filters.requestedByUserId);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1487
1494
|
// Exclude system/heartbeat tasks by default. The flag is still called
|
|
1488
1495
|
// `includeHeartbeat` for backward compat with existing API callers, but we
|
|
1489
1496
|
// also gate boot-triage + heartbeat-checklist behind it since those are
|
|
@@ -9684,6 +9691,8 @@ interface BudgetRefusalNotificationRow {
|
|
|
9684
9691
|
agent_budget_usd: number | null;
|
|
9685
9692
|
global_spend_usd: number | null;
|
|
9686
9693
|
global_budget_usd: number | null;
|
|
9694
|
+
user_spend_usd: number | null;
|
|
9695
|
+
user_budget_usd: number | null;
|
|
9687
9696
|
follow_up_task_id: string | null;
|
|
9688
9697
|
createdAt: number;
|
|
9689
9698
|
}
|
|
@@ -9714,6 +9723,8 @@ function rowToBudgetRefusalNotification(
|
|
|
9714
9723
|
agentBudgetUsd: row.agent_budget_usd ?? undefined,
|
|
9715
9724
|
globalSpendUsd: row.global_spend_usd ?? undefined,
|
|
9716
9725
|
globalBudgetUsd: row.global_budget_usd ?? undefined,
|
|
9726
|
+
userSpendUsd: row.user_spend_usd ?? undefined,
|
|
9727
|
+
userBudgetUsd: row.user_budget_usd ?? undefined,
|
|
9717
9728
|
followUpTaskId: row.follow_up_task_id ?? undefined,
|
|
9718
9729
|
createdAt: row.createdAt,
|
|
9719
9730
|
};
|
|
@@ -9964,6 +9975,24 @@ export function getDailySpendGlobal(dateUtc: string): number {
|
|
|
9964
9975
|
return row?.total ?? 0;
|
|
9965
9976
|
}
|
|
9966
9977
|
|
|
9978
|
+
/**
|
|
9979
|
+
* Sum of `totalCostUsd` across all `session_costs` rows whose task was
|
|
9980
|
+
* requested by a given user on a given UTC calendar day. `dateUtc` MUST be
|
|
9981
|
+
* `'YYYY-MM-DD'` (UTC). Costs are joined through `agent_tasks` deliberately;
|
|
9982
|
+
* `session_costs` stays task/session-scoped and does not grow a userId column.
|
|
9983
|
+
*/
|
|
9984
|
+
export function getDailySpendForUser(userId: string, dateUtc: string): number {
|
|
9985
|
+
const row = getDb()
|
|
9986
|
+
.prepare<CoalesceSumRow, [string, string]>(
|
|
9987
|
+
`SELECT COALESCE(SUM(sc.totalCostUsd), 0) AS total
|
|
9988
|
+
FROM session_costs sc
|
|
9989
|
+
JOIN agent_tasks t ON sc.taskId = t.id
|
|
9990
|
+
WHERE t.requestedByUserId = ? AND substr(sc.createdAt, 1, 10) = ?`,
|
|
9991
|
+
)
|
|
9992
|
+
.get(userId, dateUtc);
|
|
9993
|
+
return row?.total ?? 0;
|
|
9994
|
+
}
|
|
9995
|
+
|
|
9967
9996
|
export interface RecordBudgetRefusalNotificationInput {
|
|
9968
9997
|
taskId: string;
|
|
9969
9998
|
date: string;
|
|
@@ -9973,6 +10002,8 @@ export interface RecordBudgetRefusalNotificationInput {
|
|
|
9973
10002
|
agentBudgetUsd?: number;
|
|
9974
10003
|
globalSpendUsd?: number;
|
|
9975
10004
|
globalBudgetUsd?: number;
|
|
10005
|
+
userSpendUsd?: number;
|
|
10006
|
+
userBudgetUsd?: number;
|
|
9976
10007
|
}
|
|
9977
10008
|
|
|
9978
10009
|
/**
|
|
@@ -9991,8 +10022,8 @@ export function recordBudgetRefusalNotification(input: RecordBudgetRefusalNotifi
|
|
|
9991
10022
|
const result = db
|
|
9992
10023
|
.prepare(
|
|
9993
10024
|
`INSERT OR IGNORE INTO budget_refusal_notifications
|
|
9994
|
-
(task_id, date, agent_id, cause, agent_spend_usd, agent_budget_usd, global_spend_usd, global_budget_usd, follow_up_task_id, createdAt)
|
|
9995
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)`,
|
|
10025
|
+
(task_id, date, agent_id, cause, agent_spend_usd, agent_budget_usd, global_spend_usd, global_budget_usd, user_spend_usd, user_budget_usd, follow_up_task_id, createdAt)
|
|
10026
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)`,
|
|
9996
10027
|
)
|
|
9997
10028
|
.run(
|
|
9998
10029
|
input.taskId,
|
|
@@ -10003,6 +10034,8 @@ export function recordBudgetRefusalNotification(input: RecordBudgetRefusalNotifi
|
|
|
10003
10034
|
input.agentBudgetUsd ?? null,
|
|
10004
10035
|
input.globalSpendUsd ?? null,
|
|
10005
10036
|
input.globalBudgetUsd ?? null,
|
|
10037
|
+
input.userSpendUsd ?? null,
|
|
10038
|
+
input.userBudgetUsd ?? null,
|
|
10006
10039
|
now,
|
|
10007
10040
|
);
|
|
10008
10041
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
-- 074_user_budget_scope.sql
|
|
2
|
+
-- Add per-user budget enforcement for client-side MCP users.
|
|
3
|
+
--
|
|
4
|
+
-- SQLite cannot widen a CHECK constraint in place, so recreate the affected
|
|
5
|
+
-- tables and preserve their data. The `budgets` table gains scope='user'.
|
|
6
|
+
-- `budget_refusal_notifications` gains cause='user' and optional user
|
|
7
|
+
-- spend/budget audit columns so claim-time user-budget refusals can share the
|
|
8
|
+
-- existing dedup + lead-notification rail.
|
|
9
|
+
|
|
10
|
+
CREATE TABLE budgets_new (
|
|
11
|
+
scope TEXT NOT NULL,
|
|
12
|
+
scope_id TEXT NOT NULL,
|
|
13
|
+
daily_budget_usd REAL NOT NULL,
|
|
14
|
+
createdAt INTEGER NOT NULL,
|
|
15
|
+
lastUpdatedAt INTEGER NOT NULL,
|
|
16
|
+
PRIMARY KEY (scope, scope_id),
|
|
17
|
+
CHECK (scope IN ('global', 'agent', 'user')),
|
|
18
|
+
CHECK (daily_budget_usd >= 0)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
INSERT INTO budgets_new (scope, scope_id, daily_budget_usd, createdAt, lastUpdatedAt)
|
|
22
|
+
SELECT scope, scope_id, daily_budget_usd, createdAt, lastUpdatedAt
|
|
23
|
+
FROM budgets;
|
|
24
|
+
|
|
25
|
+
DROP TABLE budgets;
|
|
26
|
+
ALTER TABLE budgets_new RENAME TO budgets;
|
|
27
|
+
|
|
28
|
+
INSERT OR IGNORE INTO budgets (scope, scope_id, daily_budget_usd, createdAt, lastUpdatedAt)
|
|
29
|
+
SELECT
|
|
30
|
+
'user',
|
|
31
|
+
id,
|
|
32
|
+
dailyBudgetUsd,
|
|
33
|
+
CAST(strftime('%s', 'now') AS INTEGER) * 1000,
|
|
34
|
+
CAST(strftime('%s', 'now') AS INTEGER) * 1000
|
|
35
|
+
FROM users
|
|
36
|
+
WHERE dailyBudgetUsd IS NOT NULL;
|
|
37
|
+
|
|
38
|
+
CREATE TABLE budget_refusal_notifications_new (
|
|
39
|
+
task_id TEXT NOT NULL,
|
|
40
|
+
date TEXT NOT NULL,
|
|
41
|
+
agent_id TEXT NOT NULL,
|
|
42
|
+
cause TEXT NOT NULL,
|
|
43
|
+
agent_spend_usd REAL,
|
|
44
|
+
agent_budget_usd REAL,
|
|
45
|
+
global_spend_usd REAL,
|
|
46
|
+
global_budget_usd REAL,
|
|
47
|
+
user_spend_usd REAL,
|
|
48
|
+
user_budget_usd REAL,
|
|
49
|
+
follow_up_task_id TEXT,
|
|
50
|
+
createdAt INTEGER NOT NULL,
|
|
51
|
+
PRIMARY KEY (task_id, date),
|
|
52
|
+
CHECK (cause IN ('agent', 'global', 'user'))
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
INSERT INTO budget_refusal_notifications_new (
|
|
56
|
+
task_id,
|
|
57
|
+
date,
|
|
58
|
+
agent_id,
|
|
59
|
+
cause,
|
|
60
|
+
agent_spend_usd,
|
|
61
|
+
agent_budget_usd,
|
|
62
|
+
global_spend_usd,
|
|
63
|
+
global_budget_usd,
|
|
64
|
+
user_spend_usd,
|
|
65
|
+
user_budget_usd,
|
|
66
|
+
follow_up_task_id,
|
|
67
|
+
createdAt
|
|
68
|
+
)
|
|
69
|
+
SELECT
|
|
70
|
+
task_id,
|
|
71
|
+
date,
|
|
72
|
+
agent_id,
|
|
73
|
+
cause,
|
|
74
|
+
agent_spend_usd,
|
|
75
|
+
agent_budget_usd,
|
|
76
|
+
global_spend_usd,
|
|
77
|
+
global_budget_usd,
|
|
78
|
+
NULL,
|
|
79
|
+
NULL,
|
|
80
|
+
follow_up_task_id,
|
|
81
|
+
createdAt
|
|
82
|
+
FROM budget_refusal_notifications;
|
|
83
|
+
|
|
84
|
+
DROP TABLE budget_refusal_notifications;
|
|
85
|
+
ALTER TABLE budget_refusal_notifications_new RENAME TO budget_refusal_notifications;
|