@desplega.ai/agent-swarm 1.83.1 → 1.84.0
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 +158 -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/http/webhooks.ts +101 -0
- package/src/integrations/kapso/client.ts +198 -0
- package/src/integrations/kapso/config.ts +104 -0
- package/src/integrations/kapso/inbound.ts +111 -0
- 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/server.ts +14 -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/kapso-client.test.ts +94 -0
- package/src/tests/kapso-inbound.test.ts +198 -0
- 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/tool-annotations.test.ts +3 -2
- 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/register-kapso-number.ts +210 -0
- 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/tools/templates.ts +35 -0
- package/src/tools/tool-config.ts +6 -0
- package/src/tools/whatsapp-message.ts +135 -0
- package/src/types.ts +6 -2
- package/src/utils/error-tracker.ts +122 -9
- package/templates/skills/agentmail-sending/SKILL.md +49 -0
- package/templates/skills/kapso-whatsapp/SKILL.md +383 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ProviderName } from "../types";
|
|
2
|
+
|
|
3
|
+
export type ResumeSessionSource = "task" | "parent";
|
|
4
|
+
|
|
5
|
+
export interface ResumeSessionCandidate {
|
|
6
|
+
source: ResumeSessionSource;
|
|
7
|
+
sessionId?: string | null;
|
|
8
|
+
taskId?: string;
|
|
9
|
+
provider?: ProviderName;
|
|
10
|
+
providerMeta?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ResumeSessionSkip {
|
|
14
|
+
source: ResumeSessionSource;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
provider?: ProviderName;
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ResumeSessionResolution {
|
|
21
|
+
resumeSessionId?: string;
|
|
22
|
+
source?: ResumeSessionSource;
|
|
23
|
+
provider?: ProviderName;
|
|
24
|
+
skipped: ResumeSessionSkip[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
28
|
+
|
|
29
|
+
const RESUMABLE_PROVIDERS = new Set<ProviderName>(["claude", "claude-managed", "codex"]);
|
|
30
|
+
|
|
31
|
+
export function isClaudeCliSessionId(sessionId: string): boolean {
|
|
32
|
+
return UUID_RE.test(sessionId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeStoredProvider(candidate: ResumeSessionCandidate): ProviderName | undefined {
|
|
36
|
+
if (candidate.provider === "claude" && candidate.providerMeta?.managed === true) {
|
|
37
|
+
return "claude-managed";
|
|
38
|
+
}
|
|
39
|
+
return candidate.provider;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function providerSupportsResume(provider: ProviderName): boolean {
|
|
43
|
+
return RESUMABLE_PROVIDERS.has(provider);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveResumeSession(
|
|
47
|
+
currentProvider: ProviderName,
|
|
48
|
+
candidates: ResumeSessionCandidate[],
|
|
49
|
+
): ResumeSessionResolution {
|
|
50
|
+
const skipped: ResumeSessionSkip[] = [];
|
|
51
|
+
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
const sessionId = candidate.sessionId?.trim();
|
|
54
|
+
if (!sessionId) continue;
|
|
55
|
+
|
|
56
|
+
const storedProvider = normalizeStoredProvider(candidate);
|
|
57
|
+
|
|
58
|
+
if (!storedProvider) {
|
|
59
|
+
if (currentProvider === "claude" && isClaudeCliSessionId(sessionId)) {
|
|
60
|
+
return {
|
|
61
|
+
resumeSessionId: sessionId,
|
|
62
|
+
source: candidate.source,
|
|
63
|
+
provider: "claude",
|
|
64
|
+
skipped,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
skipped.push({
|
|
69
|
+
source: candidate.source,
|
|
70
|
+
sessionId,
|
|
71
|
+
reason:
|
|
72
|
+
currentProvider === "claude"
|
|
73
|
+
? "legacy Claude resume requires a UUID session id"
|
|
74
|
+
: "stored session provider is unknown",
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (storedProvider !== currentProvider) {
|
|
80
|
+
skipped.push({
|
|
81
|
+
source: candidate.source,
|
|
82
|
+
sessionId,
|
|
83
|
+
provider: storedProvider,
|
|
84
|
+
reason: `stored session provider ${storedProvider} does not match current provider ${currentProvider}`,
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!providerSupportsResume(currentProvider)) {
|
|
90
|
+
skipped.push({
|
|
91
|
+
source: candidate.source,
|
|
92
|
+
sessionId,
|
|
93
|
+
provider: storedProvider,
|
|
94
|
+
reason: `provider ${currentProvider} does not support runner resume`,
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (currentProvider === "claude" && !isClaudeCliSessionId(sessionId)) {
|
|
100
|
+
skipped.push({
|
|
101
|
+
source: candidate.source,
|
|
102
|
+
sessionId,
|
|
103
|
+
provider: storedProvider,
|
|
104
|
+
reason: "Claude CLI --resume requires a UUID session id",
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
resumeSessionId: sessionId,
|
|
111
|
+
source: candidate.source,
|
|
112
|
+
provider: storedProvider,
|
|
113
|
+
skipped,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { skipped };
|
|
118
|
+
}
|
package/src/commands/runner.ts
CHANGED
|
@@ -38,7 +38,11 @@ import { getApiKey } from "../utils/api-key.ts";
|
|
|
38
38
|
import { computeBudgetBackoffMs } from "../utils/budget-backoff.ts";
|
|
39
39
|
import { getContextWindowSize } from "../utils/context-window.ts";
|
|
40
40
|
import { type CredentialSelection, resolveCredentialPools } from "../utils/credentials.ts";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
isRateLimitMessage,
|
|
43
|
+
MAX_RATE_LIMIT_RESET_MS,
|
|
44
|
+
parseRateLimitResetTime,
|
|
45
|
+
} from "../utils/error-tracker.ts";
|
|
42
46
|
import { resolveHarnessProvider } from "../utils/harness-provider.ts";
|
|
43
47
|
import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
|
|
44
48
|
import { scrubSecrets } from "../utils/secret-scrubber.ts";
|
|
@@ -53,6 +57,11 @@ import {
|
|
|
53
57
|
reportCredStatus,
|
|
54
58
|
reportLatestModel,
|
|
55
59
|
} from "./provider-credentials.ts";
|
|
60
|
+
import {
|
|
61
|
+
type ResumeSessionCandidate,
|
|
62
|
+
type ResumeSessionResolution,
|
|
63
|
+
resolveResumeSession,
|
|
64
|
+
} from "./resume-session.ts";
|
|
56
65
|
// Side-effect import: registers runner trigger/resumption templates
|
|
57
66
|
import "./templates.ts";
|
|
58
67
|
|
|
@@ -990,6 +999,8 @@ async function getPausedTasksFromAPI(config: ApiConfig): Promise<
|
|
|
990
999
|
task: string;
|
|
991
1000
|
progress?: string;
|
|
992
1001
|
claudeSessionId?: string;
|
|
1002
|
+
provider?: ProviderName;
|
|
1003
|
+
providerMeta?: Record<string, unknown>;
|
|
993
1004
|
parentTaskId?: string;
|
|
994
1005
|
dir?: string;
|
|
995
1006
|
vcsRepo?: string;
|
|
@@ -1022,6 +1033,8 @@ async function getPausedTasksFromAPI(config: ApiConfig): Promise<
|
|
|
1022
1033
|
task: string;
|
|
1023
1034
|
progress?: string;
|
|
1024
1035
|
claudeSessionId?: string;
|
|
1036
|
+
provider?: ProviderName;
|
|
1037
|
+
providerMeta?: Record<string, unknown>;
|
|
1025
1038
|
parentTaskId?: string;
|
|
1026
1039
|
dir?: string;
|
|
1027
1040
|
vcsRepo?: string;
|
|
@@ -1424,24 +1437,51 @@ async function saveProviderSessionIdOnActiveSession(
|
|
|
1424
1437
|
});
|
|
1425
1438
|
}
|
|
1426
1439
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1440
|
+
interface ProviderSessionInfo {
|
|
1441
|
+
sessionId: string | null;
|
|
1442
|
+
provider?: ProviderName;
|
|
1443
|
+
providerMeta?: Record<string, unknown>;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/** Fetch provider session metadata for a task (for resume continuity). */
|
|
1447
|
+
async function fetchProviderSessionInfo(
|
|
1429
1448
|
apiUrl: string,
|
|
1430
1449
|
apiKey: string,
|
|
1431
1450
|
taskId: string,
|
|
1432
|
-
): Promise<
|
|
1451
|
+
): Promise<ProviderSessionInfo | null> {
|
|
1433
1452
|
const headers: Record<string, string> = {};
|
|
1434
1453
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
1435
1454
|
try {
|
|
1436
1455
|
const response = await fetch(`${apiUrl}/api/tasks/${taskId}`, { headers });
|
|
1437
1456
|
if (!response.ok) return null;
|
|
1438
|
-
const data = (await response.json()) as {
|
|
1439
|
-
|
|
1457
|
+
const data = (await response.json()) as {
|
|
1458
|
+
claudeSessionId?: string;
|
|
1459
|
+
provider?: ProviderName;
|
|
1460
|
+
providerMeta?: Record<string, unknown>;
|
|
1461
|
+
};
|
|
1462
|
+
return {
|
|
1463
|
+
sessionId: data.claudeSessionId || null,
|
|
1464
|
+
provider: data.provider,
|
|
1465
|
+
providerMeta: data.providerMeta,
|
|
1466
|
+
};
|
|
1440
1467
|
} catch {
|
|
1441
1468
|
return null;
|
|
1442
1469
|
}
|
|
1443
1470
|
}
|
|
1444
1471
|
|
|
1472
|
+
function logResumeResolution(role: string, resolution: ResumeSessionResolution): void {
|
|
1473
|
+
for (const skipped of resolution.skipped) {
|
|
1474
|
+
console.warn(
|
|
1475
|
+
`[${role}] Skipping ${skipped.source} session resume ${skipped.sessionId.slice(0, 8)}: ${skipped.reason}`,
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if (resolution.resumeSessionId) {
|
|
1480
|
+
const source = resolution.source === "parent" ? "parent session" : "task's own session";
|
|
1481
|
+
console.log(`[${role}] Resuming ${source} ${resolution.resumeSessionId.slice(0, 8)}`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1445
1485
|
/** Register an active session with the API (fire-and-forget) */
|
|
1446
1486
|
async function registerActiveSession(
|
|
1447
1487
|
config: ApiConfig,
|
|
@@ -2102,6 +2142,7 @@ async function spawnProviderProcess(
|
|
|
2102
2142
|
iteration: number;
|
|
2103
2143
|
taskId?: string;
|
|
2104
2144
|
model?: string;
|
|
2145
|
+
resumeSessionId?: string;
|
|
2105
2146
|
harnessProvider: ProviderName;
|
|
2106
2147
|
cwd?: string;
|
|
2107
2148
|
vcsRepo?: string;
|
|
@@ -2167,6 +2208,7 @@ async function spawnProviderProcess(
|
|
|
2167
2208
|
vcsRepo: opts.vcsRepo,
|
|
2168
2209
|
logFile: opts.logFile,
|
|
2169
2210
|
additionalArgs: opts.additionalArgs,
|
|
2211
|
+
resumeSessionId: opts.resumeSessionId,
|
|
2170
2212
|
iteration: opts.iteration,
|
|
2171
2213
|
env: freshEnv as Record<string, string>,
|
|
2172
2214
|
// Propagate the selected OAuth slot so the adapter refreshes back to the
|
|
@@ -2813,54 +2855,61 @@ async function checkCompletedProcesses(
|
|
|
2813
2855
|
if (result.exitCode !== 0 && result.failureReason) {
|
|
2814
2856
|
failureReason = result.failureReason;
|
|
2815
2857
|
console.log(`[${role}] Detected error for task ${taskId.slice(0, 8)}: ${failureReason}`);
|
|
2858
|
+
}
|
|
2816
2859
|
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2860
|
+
// If rate-limited and we know which key was used, report it.
|
|
2861
|
+
// Codex adapter prefixes failure reasons with `[rate-limit]` /
|
|
2862
|
+
// `[usage-limit]` (see codex-adapter.formatTerminalError); Claude
|
|
2863
|
+
// surfaces "rate limit" / "hit your limit" via SessionErrorTracker.
|
|
2864
|
+
//
|
|
2865
|
+
// The gate must also fire on a bare structured rate_limit_event: a
|
|
2866
|
+
// `status: "rejected"` event sets result.rateLimitResetAt but does NOT
|
|
2867
|
+
// set hasErrors(), so failureReason can be empty even though the key is
|
|
2868
|
+
// exhausted. Gating on rateLimitResetAt != null ensures the structured
|
|
2869
|
+
// event alone still triggers the cooldown.
|
|
2870
|
+
if (
|
|
2871
|
+
credentialInfo &&
|
|
2872
|
+
(result.rateLimitResetAt != null ||
|
|
2873
|
+
(failureReason != null && isRateLimitMessage(failureReason)))
|
|
2874
|
+
) {
|
|
2875
|
+
// Three-tier reset-time resolver (most to least precise):
|
|
2876
|
+
// Tier 1: structured rate_limit_event from Claude CLI (resetsAt epoch sec)
|
|
2877
|
+
// Tier 2: regex on the error message (e.g. "resets 3pm (UTC)")
|
|
2878
|
+
// Tier 3: 5-min hard fallback — only when both structured and regex fail
|
|
2879
|
+
// Tiers 1 & 2 are clamped to [now+60s, now+7d] (weekly limits reset ~2 days out).
|
|
2880
|
+
const clampResetTime = (isoString: string): string => {
|
|
2881
|
+
const nowMs = Date.now();
|
|
2882
|
+
const minMs = nowMs + 60_000;
|
|
2883
|
+
const maxMs = nowMs + MAX_RATE_LIMIT_RESET_MS;
|
|
2884
|
+
const candidateMs = new Date(isoString).getTime();
|
|
2885
|
+
return new Date(Math.min(Math.max(candidateMs, minMs), maxMs)).toISOString();
|
|
2886
|
+
};
|
|
2837
2887
|
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2888
|
+
let rateLimitedUntil: string;
|
|
2889
|
+
if (result.rateLimitResetAt) {
|
|
2890
|
+
rateLimitedUntil = clampResetTime(result.rateLimitResetAt);
|
|
2891
|
+
console.log(`[credentials] Rate limit reset from rate_limit_event: ${rateLimitedUntil}`);
|
|
2892
|
+
} else if (failureReason != null) {
|
|
2893
|
+
const parsedResetTime = parseRateLimitResetTime(failureReason);
|
|
2894
|
+
if (parsedResetTime) {
|
|
2895
|
+
rateLimitedUntil = clampResetTime(parsedResetTime);
|
|
2841
2896
|
console.log(
|
|
2842
|
-
`[credentials]
|
|
2897
|
+
`[credentials] Parsed rate limit reset time from error: ${rateLimitedUntil}`,
|
|
2843
2898
|
);
|
|
2844
2899
|
} else {
|
|
2845
|
-
|
|
2846
|
-
if (parsedResetTime) {
|
|
2847
|
-
rateLimitedUntil = clampResetTime(parsedResetTime);
|
|
2848
|
-
console.log(
|
|
2849
|
-
`[credentials] Parsed rate limit reset time from error: ${rateLimitedUntil}`,
|
|
2850
|
-
);
|
|
2851
|
-
} else {
|
|
2852
|
-
rateLimitedUntil = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
|
2853
|
-
}
|
|
2900
|
+
rateLimitedUntil = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
|
2854
2901
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
apiConfig.apiKey,
|
|
2858
|
-
credentialInfo.keyType,
|
|
2859
|
-
credentialInfo.keySuffix,
|
|
2860
|
-
credentialInfo.keyIndex,
|
|
2861
|
-
rateLimitedUntil,
|
|
2862
|
-
).catch(() => {});
|
|
2902
|
+
} else {
|
|
2903
|
+
rateLimitedUntil = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
|
2863
2904
|
}
|
|
2905
|
+
reportKeyRateLimit(
|
|
2906
|
+
apiConfig.apiUrl,
|
|
2907
|
+
apiConfig.apiKey,
|
|
2908
|
+
credentialInfo.keyType,
|
|
2909
|
+
credentialInfo.keySuffix,
|
|
2910
|
+
credentialInfo.keyIndex,
|
|
2911
|
+
rateLimitedUntil,
|
|
2912
|
+
).catch(() => {});
|
|
2864
2913
|
}
|
|
2865
2914
|
await ensureTaskFinished(
|
|
2866
2915
|
apiConfig,
|
|
@@ -3703,18 +3752,30 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3703
3752
|
console.log(`[${role}] Injected relevant memories into resumed task prompt`);
|
|
3704
3753
|
}
|
|
3705
3754
|
|
|
3706
|
-
// Resolve
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3755
|
+
// Resolve provider-aware resume: prefer own session, then parent.
|
|
3756
|
+
const resumeCandidates: ResumeSessionCandidate[] = [
|
|
3757
|
+
{
|
|
3758
|
+
source: "task",
|
|
3759
|
+
taskId: task.id,
|
|
3760
|
+
sessionId: task.claudeSessionId,
|
|
3761
|
+
provider: task.provider,
|
|
3762
|
+
providerMeta: task.providerMeta,
|
|
3763
|
+
},
|
|
3764
|
+
];
|
|
3765
|
+
if (task.parentTaskId) {
|
|
3766
|
+
const parentSession = await fetchProviderSessionInfo(apiUrl, apiKey, task.parentTaskId);
|
|
3767
|
+
if (parentSession?.sessionId) {
|
|
3768
|
+
resumeCandidates.push({
|
|
3769
|
+
source: "parent",
|
|
3770
|
+
taskId: task.parentTaskId,
|
|
3771
|
+
sessionId: parentSession.sessionId,
|
|
3772
|
+
provider: parentSession.provider,
|
|
3773
|
+
providerMeta: parentSession.providerMeta,
|
|
3774
|
+
});
|
|
3716
3775
|
}
|
|
3717
3776
|
}
|
|
3777
|
+
const resumeResolution = resolveResumeSession(state.harnessProvider, resumeCandidates);
|
|
3778
|
+
logResumeResolution(role, resumeResolution);
|
|
3718
3779
|
|
|
3719
3780
|
// Spawn Claude process for resumed task
|
|
3720
3781
|
iteration++;
|
|
@@ -3780,7 +3841,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3780
3841
|
prompt: resumePrompt,
|
|
3781
3842
|
logFile,
|
|
3782
3843
|
systemPrompt: resolvedSystemPrompt,
|
|
3783
|
-
additionalArgs:
|
|
3844
|
+
additionalArgs: opts.additionalArgs,
|
|
3845
|
+
resumeSessionId: resumeResolution.resumeSessionId,
|
|
3784
3846
|
role,
|
|
3785
3847
|
apiUrl,
|
|
3786
3848
|
apiKey,
|
|
@@ -4045,20 +4107,27 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4045
4107
|
}
|
|
4046
4108
|
}
|
|
4047
4109
|
|
|
4048
|
-
// Resolve
|
|
4049
|
-
let
|
|
4110
|
+
// Resolve provider-aware resume for child tasks with parentTaskId.
|
|
4111
|
+
let resumeSessionId: string | undefined;
|
|
4050
4112
|
const taskObj = trigger.task as { parentTaskId?: string } | undefined;
|
|
4051
4113
|
if (taskObj?.parentTaskId) {
|
|
4052
|
-
const
|
|
4114
|
+
const parentSession = await fetchProviderSessionInfo(
|
|
4053
4115
|
apiUrl,
|
|
4054
4116
|
apiKey,
|
|
4055
4117
|
taskObj.parentTaskId,
|
|
4056
4118
|
);
|
|
4057
|
-
if (
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4119
|
+
if (parentSession?.sessionId) {
|
|
4120
|
+
const resumeResolution = resolveResumeSession(state.harnessProvider, [
|
|
4121
|
+
{
|
|
4122
|
+
source: "parent",
|
|
4123
|
+
taskId: taskObj.parentTaskId,
|
|
4124
|
+
sessionId: parentSession.sessionId,
|
|
4125
|
+
provider: parentSession.provider,
|
|
4126
|
+
providerMeta: parentSession.providerMeta,
|
|
4127
|
+
},
|
|
4128
|
+
]);
|
|
4129
|
+
logResumeResolution(role, resumeResolution);
|
|
4130
|
+
resumeSessionId = resumeResolution.resumeSessionId;
|
|
4062
4131
|
} else {
|
|
4063
4132
|
console.log(`[${role}] Child task — parent session ID not found, starting fresh`);
|
|
4064
4133
|
}
|
|
@@ -4197,7 +4266,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4197
4266
|
prompt: triggerPrompt,
|
|
4198
4267
|
logFile,
|
|
4199
4268
|
systemPrompt: taskSystemPrompt,
|
|
4200
|
-
additionalArgs:
|
|
4269
|
+
additionalArgs: opts.additionalArgs,
|
|
4270
|
+
resumeSessionId,
|
|
4201
4271
|
role,
|
|
4202
4272
|
apiUrl,
|
|
4203
4273
|
apiKey,
|
package/src/http/core.ts
CHANGED
|
@@ -240,7 +240,10 @@ export async function handleCore(
|
|
|
240
240
|
// fall through to the bearer check (fail-closed).
|
|
241
241
|
if (apiKey) {
|
|
242
242
|
const pathSegments = getPathSegments(req.url || "");
|
|
243
|
-
|
|
243
|
+
const isUserMcpRoute = req.url === "/mcp-user";
|
|
244
|
+
// `/mcp-user` runs its own `aswt_`-token auth in `handleMcpUser`; the swarm
|
|
245
|
+
// API key must not gate it.
|
|
246
|
+
if (!isUserMcpRoute && !isPublicRoute(req.method, pathSegments)) {
|
|
244
247
|
const authHeader = req.headers.authorization;
|
|
245
248
|
const providedKey = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
246
249
|
|
package/src/http/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { handleKv } from "./kv";
|
|
|
43
43
|
import { handleMcp } from "./mcp";
|
|
44
44
|
import { handleMcpOAuth, startMcpOAuthPendingGc, stopMcpOAuthPendingGc } from "./mcp-oauth";
|
|
45
45
|
import { handleMcpServers } from "./mcp-servers";
|
|
46
|
+
import { handleMcpUser } from "./mcp-user";
|
|
46
47
|
import { handleMemory } from "./memory";
|
|
47
48
|
import { handlePageProxy } from "./page-proxy";
|
|
48
49
|
import { handlePages } from "./pages";
|
|
@@ -89,6 +90,8 @@ const apiKey = getApiKey();
|
|
|
89
90
|
const globalState = globalThis as typeof globalThis & {
|
|
90
91
|
__httpServer?: Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
91
92
|
__transports?: Record<string, StreamableHTTPServerTransport>;
|
|
93
|
+
__transportsUser?: Record<string, StreamableHTTPServerTransport>;
|
|
94
|
+
__sessionUsers?: Record<string, string>;
|
|
92
95
|
__sigintRegistered?: boolean;
|
|
93
96
|
__runId?: string;
|
|
94
97
|
};
|
|
@@ -100,6 +103,9 @@ if (globalState.__httpServer) {
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
const transports: Record<string, StreamableHTTPServerTransport> = globalState.__transports ?? {};
|
|
106
|
+
const transportsUser: Record<string, StreamableHTTPServerTransport> =
|
|
107
|
+
globalState.__transportsUser ?? {};
|
|
108
|
+
const sessionUsers: Record<string, string> = globalState.__sessionUsers ?? {};
|
|
103
109
|
|
|
104
110
|
const httpServer = createHttpServer(async (req, res) => {
|
|
105
111
|
const startTime = performance.now();
|
|
@@ -234,6 +240,7 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
234
240
|
() => handleInboxState(req, res, pathSegments, queryParams),
|
|
235
241
|
() => handleTaskTemplates(req, res, pathSegments, queryParams),
|
|
236
242
|
() => handleMcp(req, res, transports),
|
|
243
|
+
() => handleMcpUser(req, res, transportsUser, sessionUsers),
|
|
237
244
|
];
|
|
238
245
|
|
|
239
246
|
try {
|
|
@@ -271,6 +278,8 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
271
278
|
// Store references in globalThis for hot reload persistence
|
|
272
279
|
globalState.__httpServer = httpServer;
|
|
273
280
|
globalState.__transports = transports;
|
|
281
|
+
globalState.__transportsUser = transportsUser;
|
|
282
|
+
globalState.__sessionUsers = sessionUsers;
|
|
274
283
|
|
|
275
284
|
async function shutdown() {
|
|
276
285
|
console.log("Shutting down HTTP server...");
|
|
@@ -303,6 +312,13 @@ async function shutdown() {
|
|
|
303
312
|
delete transports[id];
|
|
304
313
|
}
|
|
305
314
|
|
|
315
|
+
for (const [id, transport] of Object.entries(transportsUser)) {
|
|
316
|
+
console.log(`[HTTP] Closing user transport ${id}`);
|
|
317
|
+
transport.close();
|
|
318
|
+
delete transportsUser[id];
|
|
319
|
+
delete sessionUsers[id];
|
|
320
|
+
}
|
|
321
|
+
|
|
306
322
|
// Close all active connections forcefully
|
|
307
323
|
httpServer.closeAllConnections();
|
|
308
324
|
httpServer.close(() => {
|
package/src/http/integrations.ts
CHANGED
|
@@ -45,6 +45,20 @@ const claudeManagedTestRoute = route({
|
|
|
45
45
|
},
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
const mcpUserConfigRoute = route({
|
|
49
|
+
method: "get",
|
|
50
|
+
path: "/api/integrations/mcp-user/config",
|
|
51
|
+
pattern: ["api", "integrations", "mcp-user", "config"],
|
|
52
|
+
summary: "Get server-derived config for end-user MCP clients.",
|
|
53
|
+
tags: ["Integrations"],
|
|
54
|
+
responses: {
|
|
55
|
+
200: {
|
|
56
|
+
description:
|
|
57
|
+
"Server-derived MCP user config. `mcpBaseUrl` is the API server base URL and `mcpUserUrl` appends `/mcp-user`.",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
48
62
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
49
63
|
|
|
50
64
|
/**
|
|
@@ -73,6 +87,12 @@ function resolveConfigValue(key: string): string | null {
|
|
|
73
87
|
return null;
|
|
74
88
|
}
|
|
75
89
|
|
|
90
|
+
function resolveMcpBaseUrl(): string {
|
|
91
|
+
const configured = resolveConfigValue("MCP_BASE_URL");
|
|
92
|
+
const fallback = `http://localhost:${process.env.PORT || "3013"}`;
|
|
93
|
+
return (configured || fallback).replace(/\/+$/, "");
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
// ─── Public handler factory ──────────────────────────────────────────────────
|
|
77
97
|
|
|
78
98
|
/**
|
|
@@ -89,6 +109,12 @@ export function createIntegrationsHandler(deps: TestConnectionDeps = {}) {
|
|
|
89
109
|
res: ServerResponse,
|
|
90
110
|
pathSegments: string[],
|
|
91
111
|
): Promise<boolean> {
|
|
112
|
+
if (mcpUserConfigRoute.match(req.method, pathSegments)) {
|
|
113
|
+
const mcpBaseUrl = resolveMcpBaseUrl();
|
|
114
|
+
json(res, { mcpBaseUrl, mcpUserUrl: `${mcpBaseUrl}/mcp-user` });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
if (claudeManagedTestRoute.match(req.method, pathSegments)) {
|
|
93
119
|
const apiKey = resolveConfigValue("ANTHROPIC_API_KEY");
|
|
94
120
|
const agentId = resolveConfigValue("MANAGED_AGENT_ID");
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { resolveUserByToken } from "@/be/users";
|
|
6
|
+
import { createUserServer } from "@/server-user";
|
|
7
|
+
import type { User } from "@/types";
|
|
8
|
+
|
|
9
|
+
function unauthorized(res: ServerResponse): true {
|
|
10
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
11
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function extractBearer(req: IncomingMessage): string | null {
|
|
16
|
+
const header = req.headers.authorization;
|
|
17
|
+
if (!header?.startsWith("Bearer ")) return null;
|
|
18
|
+
const token = header.slice("Bearer ".length).trim();
|
|
19
|
+
return token.startsWith("aswt_") ? token : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveActiveUser(req: IncomingMessage): User | null {
|
|
23
|
+
const token = extractBearer(req);
|
|
24
|
+
if (!token) return null;
|
|
25
|
+
const user = resolveUserByToken(token);
|
|
26
|
+
if (!user || user.status !== "active") return null;
|
|
27
|
+
return user;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleMcpUser(
|
|
31
|
+
req: IncomingMessage,
|
|
32
|
+
res: ServerResponse,
|
|
33
|
+
transports: Record<string, StreamableHTTPServerTransport>,
|
|
34
|
+
sessionUsers: Record<string, string>,
|
|
35
|
+
): Promise<boolean> {
|
|
36
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
37
|
+
|
|
38
|
+
if (req.url !== "/mcp-user") {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const user = resolveActiveUser(req);
|
|
43
|
+
if (!user) return unauthorized(res);
|
|
44
|
+
|
|
45
|
+
if (sessionId && transports[sessionId] && sessionUsers[sessionId] !== user.id) {
|
|
46
|
+
return unauthorized(res);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (req.method === "POST") {
|
|
50
|
+
const chunks: Buffer[] = [];
|
|
51
|
+
for await (const chunk of req) {
|
|
52
|
+
chunks.push(chunk);
|
|
53
|
+
}
|
|
54
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
55
|
+
|
|
56
|
+
let transport: StreamableHTTPServerTransport;
|
|
57
|
+
|
|
58
|
+
if (sessionId && transports[sessionId]) {
|
|
59
|
+
transport = transports[sessionId];
|
|
60
|
+
} else if (!sessionId && isInitializeRequest(body)) {
|
|
61
|
+
transport = new StreamableHTTPServerTransport({
|
|
62
|
+
sessionIdGenerator: () => randomUUID(),
|
|
63
|
+
onsessioninitialized: (id) => {
|
|
64
|
+
transports[id] = transport;
|
|
65
|
+
sessionUsers[id] = user.id;
|
|
66
|
+
},
|
|
67
|
+
onsessionclosed: (id) => {
|
|
68
|
+
delete transports[id];
|
|
69
|
+
delete sessionUsers[id];
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
transport.onclose = () => {
|
|
74
|
+
if (transport.sessionId) {
|
|
75
|
+
delete transports[transport.sessionId];
|
|
76
|
+
delete sessionUsers[transport.sessionId];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const server = createUserServer(user);
|
|
81
|
+
await server.connect(transport);
|
|
82
|
+
} else {
|
|
83
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
84
|
+
res.end(
|
|
85
|
+
JSON.stringify({
|
|
86
|
+
jsonrpc: "2.0",
|
|
87
|
+
error: { code: -32000, message: "Invalid session" },
|
|
88
|
+
id: null,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await transport.handleRequest(req, res, body);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (req.method === "GET" || req.method === "DELETE") {
|
|
99
|
+
if (sessionId && transports[sessionId]) {
|
|
100
|
+
await transports[sessionId].handleRequest(req, res);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
res.writeHead(400);
|
|
104
|
+
res.end("Invalid session");
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
res.writeHead(405);
|
|
109
|
+
res.end("Method not allowed");
|
|
110
|
+
return true;
|
|
111
|
+
}
|