agent-relay-server 0.32.1 → 0.32.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/docs/openapi.json +57 -127
- package/package.json +1 -1
- package/public/assets/{activity-C6nbfryG.js → activity-DT1JGHnp.js} +2 -2
- package/public/assets/{activity-C6nbfryG.js.map → activity-DT1JGHnp.js.map} +1 -1
- package/public/assets/{agent-profiles-FEITAgHs.js → agent-profiles-CrMemMkZ.js} +2 -2
- package/public/assets/{agent-profiles-FEITAgHs.js.map → agent-profiles-CrMemMkZ.js.map} +1 -1
- package/public/assets/{agents-D4S0yIbe.js → agents-Bl-rrgOy.js} +2 -2
- package/public/assets/{agents-D4S0yIbe.js.map → agents-Bl-rrgOy.js.map} +1 -1
- package/public/assets/{analytics-DM2g62T_.js → analytics-a663ak56.js} +2 -2
- package/public/assets/{analytics-DM2g62T_.js.map → analytics-a663ak56.js.map} +1 -1
- package/public/assets/{automation-3D2pQa1C.js → automation-CiaLThdO.js} +2 -2
- package/public/assets/{automation-3D2pQa1C.js.map → automation-CiaLThdO.js.map} +1 -1
- package/public/assets/{branch-state-badge-Bi4IbkOZ.js → branch-state-badge-D4ur3m3_.js} +2 -2
- package/public/assets/{branch-state-badge-Bi4IbkOZ.js.map → branch-state-badge-D4ur3m3_.js.map} +1 -1
- package/public/assets/{channels-QNp7zmA_.js → channels-o9KLTHoK.js} +2 -2
- package/public/assets/{channels-QNp7zmA_.js.map → channels-o9KLTHoK.js.map} +1 -1
- package/public/assets/{chat-jeXt_SFs.js → chat-5hvHZcAe.js} +2 -2
- package/public/assets/{chat-jeXt_SFs.js.map → chat-5hvHZcAe.js.map} +1 -1
- package/public/assets/{connectors-BGJARDui.js → connectors-CdC806mA.js} +2 -2
- package/public/assets/{connectors-BGJARDui.js.map → connectors-CdC806mA.js.map} +1 -1
- package/public/assets/{formatted-body-impl-B7FgqkYL.js → formatted-body-impl-Ca74OAEH.js} +2 -2
- package/public/assets/{formatted-body-impl-B7FgqkYL.js.map → formatted-body-impl-Ca74OAEH.js.map} +1 -1
- package/public/assets/{index-2m9mT8kV.js → index-C_33ymaw.js} +6 -6
- package/public/assets/{index-2m9mT8kV.js.map → index-C_33ymaw.js.map} +1 -1
- package/public/assets/{integrations-CJm8-FcG.js → integrations-1nxMizDY.js} +2 -2
- package/public/assets/{integrations-CJm8-FcG.js.map → integrations-1nxMizDY.js.map} +1 -1
- package/public/assets/{maintenance-CBvZrVAG.js → maintenance-DiFNzNPN.js} +2 -2
- package/public/assets/{maintenance-CBvZrVAG.js.map → maintenance-DiFNzNPN.js.map} +1 -1
- package/public/assets/{managed-agents-Dcmm8YKt.js → managed-agents-Do3dKvfj.js} +2 -2
- package/public/assets/{managed-agents-Dcmm8YKt.js.map → managed-agents-Do3dKvfj.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-7xjqdiEu.js → markdown-preview-impl-CLA0J255.js} +2 -2
- package/public/assets/{markdown-preview-impl-7xjqdiEu.js.map → markdown-preview-impl-CLA0J255.js.map} +1 -1
- package/public/assets/{memory-BmGNW61h.js → memory-IjwqFzBd.js} +2 -2
- package/public/assets/{memory-BmGNW61h.js.map → memory-IjwqFzBd.js.map} +1 -1
- package/public/assets/{messages-BvMMhoy-.js → messages-DjvWqHyn.js} +2 -2
- package/public/assets/{messages-BvMMhoy-.js.map → messages-DjvWqHyn.js.map} +1 -1
- package/public/assets/{orchestrators-DsstaupT.js → orchestrators-D2IqDxDT.js} +2 -2
- package/public/assets/{orchestrators-DsstaupT.js.map → orchestrators-D2IqDxDT.js.map} +1 -1
- package/public/assets/{overview-kK6PTce3.js → overview-DKC3TbAh.js} +2 -2
- package/public/assets/{overview-kK6PTce3.js.map → overview-DKC3TbAh.js.map} +1 -1
- package/public/assets/{pairs-BEFvTW6X.js → pairs-WpKCPE1n.js} +2 -2
- package/public/assets/{pairs-BEFvTW6X.js.map → pairs-WpKCPE1n.js.map} +1 -1
- package/public/assets/{security-Dc5wZwv0.js → security-BF7ZtPQe.js} +2 -2
- package/public/assets/{security-Dc5wZwv0.js.map → security-BF7ZtPQe.js.map} +1 -1
- package/public/assets/{settings-CEtJrORa.js → settings-CQnjrTa-.js} +2 -2
- package/public/assets/{settings-CEtJrORa.js.map → settings-CQnjrTa-.js.map} +1 -1
- package/public/assets/{store-DkmReBlH.js → store-C9VcSo05.js} +2 -2
- package/public/assets/{store-DkmReBlH.js.map → store-C9VcSo05.js.map} +1 -1
- package/public/assets/{tasks-pQKtxqeV.js → tasks-CbN_GSSb.js} +2 -2
- package/public/assets/{tasks-pQKtxqeV.js.map → tasks-CbN_GSSb.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-Cc769mYy.js → terminal-viewer-impl-BJRohThT.js} +2 -2
- package/public/assets/{terminal-viewer-impl-Cc769mYy.js.map → terminal-viewer-impl-BJRohThT.js.map} +1 -1
- package/public/assets/{work-queue-DjAanr02.js → work-queue-C5xLBLmm.js} +2 -2
- package/public/assets/{work-queue-DjAanr02.js.map → work-queue-C5xLBLmm.js.map} +1 -1
- package/public/assets/{workspaces-DLBNyR4k.js → workspaces-D91H3wDX.js} +2 -2
- package/public/assets/{workspaces-DLBNyR4k.js.map → workspaces-D91H3wDX.js.map} +1 -1
- package/public/index.html +2 -2
- package/scripts/orchestrator-spawn-smoke.ts +2 -1
- package/src/automations.ts +2 -4
- package/src/managed-policy.ts +2 -4
- package/src/mcp.ts +3 -3
- package/src/routes.ts +69 -139
- package/src/runtime-tokens.ts +17 -8
- package/src/security.ts +0 -2
package/src/routes.ts
CHANGED
|
@@ -124,7 +124,6 @@ import {
|
|
|
124
124
|
setAgentProfile,
|
|
125
125
|
setConfig,
|
|
126
126
|
setStewardConfig,
|
|
127
|
-
spawnGrantForProfile,
|
|
128
127
|
upsertManagedAgentState,
|
|
129
128
|
updateManagedAgentState,
|
|
130
129
|
} from "./config-store";
|
|
@@ -215,7 +214,7 @@ import { rankRouteCandidates, type RouteAdvisorInput } from "./context-router";
|
|
|
215
214
|
import { MemoryBrokerContractError } from "./memory-broker-contract";
|
|
216
215
|
import { assertMemoryCreateAllowed, assertMemoryUpdateAllowed } from "./memory-security";
|
|
217
216
|
import { captureTaskResultMemory, clearActiveMemories, injectAlwaysReloadMemories, injectMemoryContext, injectMemoryForMessageDelivery, injectMemoryForTaskClaim, memoryBroker, memoryBrokerConfig } from "./memory-service";
|
|
218
|
-
import { postMcp } from "./mcp";
|
|
217
|
+
import { postMcp, selectSpawnOrchestrator, McpNotFoundError } from "./mcp";
|
|
219
218
|
import { readFileSync } from "node:fs";
|
|
220
219
|
import { resolve } from "node:path";
|
|
221
220
|
import { isPathWithinBase } from "./utils";
|
|
@@ -2281,6 +2280,7 @@ function restartSpawnParamsForAgent(
|
|
|
2281
2280
|
policyName,
|
|
2282
2281
|
spawnRequestId: requestId,
|
|
2283
2282
|
createdBy: requestedBy,
|
|
2283
|
+
profile: profileName || undefined,
|
|
2284
2284
|
}),
|
|
2285
2285
|
requestedBy,
|
|
2286
2286
|
requestedAt: Date.now(),
|
|
@@ -2585,76 +2585,95 @@ const deleteAgentTerminalSession: Handler = async (req, params) => {
|
|
|
2585
2585
|
return proxyOrchestratorDelete(req, orchestrator.id, `/api/terminal-guests/${encodeURIComponent(session)}`);
|
|
2586
2586
|
};
|
|
2587
2587
|
|
|
2588
|
+
// Canonical spawn endpoint. The agent is the resource being created; the host is a parameter
|
|
2589
|
+
// (`orchestratorId`, defaulting to where the cwd lives), NOT a path segment — which is why this
|
|
2590
|
+
// supersedes the old `/api/orchestrators/:id/spawn`. ONE spawn path, so the spawn grant (resolved
|
|
2591
|
+
// from the profile inside the token mint) can't be threaded on some routes and forgotten on others.
|
|
2588
2592
|
const postAgentSpawn: Handler = async (req) => {
|
|
2589
2593
|
const parsed = await parseBody<unknown>(req);
|
|
2590
2594
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
2591
2595
|
try {
|
|
2592
2596
|
if (!isRecord(parsed.body)) return error("provider required");
|
|
2593
|
-
const provider = optionalEnum(parsed.body.provider, "provider", SPAWN_PROVIDERS);
|
|
2597
|
+
const provider = optionalEnum(parsed.body.provider, "provider", SPAWN_PROVIDERS) as SpawnProvider | undefined;
|
|
2594
2598
|
if (!provider) return error("provider required");
|
|
2595
|
-
const
|
|
2596
|
-
const approvalMode = optionalEnum(parsed.body.approvalMode, "approvalMode", APPROVAL_MODES, "guarded") as SpawnApprovalMode;
|
|
2599
|
+
const orchestratorId = cleanString(parsed.body.orchestratorId, "orchestratorId", { max: 200 });
|
|
2597
2600
|
const cwd = cleanString(parsed.body.cwd, "cwd", { max: 500 });
|
|
2601
|
+
// Resolves an explicit host, else the host whose baseDir contains cwd, else the lone candidate
|
|
2602
|
+
// (the same resolver the MCP spawn tool uses — one home for host selection).
|
|
2603
|
+
const orch = selectSpawnOrchestrator(provider, orchestratorId, cwd);
|
|
2604
|
+
if (cwd && !isPathWithinBase(cwd, orch.baseDir)) {
|
|
2605
|
+
return error(`cwd must be within orchestrator base directory: ${orch.baseDir}`);
|
|
2606
|
+
}
|
|
2607
|
+
const selection = validateSpawnSelectionForOrchestrator(orch, provider, parsed.body);
|
|
2608
|
+
if (selection instanceof Response) return selection;
|
|
2609
|
+
const approvalMode = optionalEnum(parsed.body.approvalMode, "approvalMode", APPROVAL_MODES, "guarded") as SpawnApprovalMode;
|
|
2598
2610
|
const label = cleanString(parsed.body.label, "label", { max: 120 });
|
|
2599
2611
|
const workspaceMode = optionalEnum(parsed.body.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES, "inherit") as WorkspaceMode;
|
|
2600
2612
|
const profile = cleanString(parsed.body.profile, "profile", { max: 120 });
|
|
2601
|
-
const
|
|
2602
|
-
|
|
2603
|
-
const
|
|
2604
|
-
|
|
2605
|
-
);
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
});
|
|
2616
|
-
if (denied) return denied;
|
|
2613
|
+
const prompt = cleanString(parsed.body.prompt, "prompt", { max: 16000 });
|
|
2614
|
+
const systemPromptAppend = cleanString(parsed.body.systemPromptAppend, "systemPromptAppend", { max: 64_000 });
|
|
2615
|
+
const tags = cleanStringArray(parsed.body.tags, "tags", { itemMax: 80, maxItems: 50 }) ?? [];
|
|
2616
|
+
const capabilities = cleanStringArray(parsed.body.capabilities, "capabilities", { itemMax: 80, maxItems: 50 }) ?? [];
|
|
2617
|
+
const providerArgs = cleanStringArray(parsed.body.providerArgs, "providerArgs", { itemMax: 80, maxItems: 50 }) ?? [];
|
|
2618
|
+
const agentProfile = profile ? getAgentProfile(profile)?.value : undefined;
|
|
2619
|
+
if (profile && !agentProfile) return error("agent profile not found", 404);
|
|
2620
|
+
const policyName = cleanString(parsed.body.policyName, "policyName", { max: 120 });
|
|
2621
|
+
const requestId = cleanString(parsed.body.spawnRequestId, "spawnRequestId", { max: 160 }) ?? spawnRequestId();
|
|
2622
|
+
const denied = authorizeRoute(req, {
|
|
2623
|
+
scope: "agent:write",
|
|
2624
|
+
resource: { orchestratorId: orch.id, cwd: cwd || orch.baseDir, policyName, spawnRequestId: requestId },
|
|
2625
|
+
});
|
|
2626
|
+
if (denied) return denied;
|
|
2617
2627
|
const command = createCommand({
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2628
|
+
type: "agent.spawn",
|
|
2629
|
+
source: "system",
|
|
2630
|
+
target: orch.agentId,
|
|
2631
|
+
correlationId: requestId,
|
|
2632
|
+
params: buildSpawnCommand({
|
|
2633
|
+
provider,
|
|
2634
|
+
modelParams: selection,
|
|
2635
|
+
cwd: cwd || orch.baseDir,
|
|
2636
|
+
workspaceMode,
|
|
2637
|
+
label,
|
|
2638
|
+
tags,
|
|
2639
|
+
capabilities,
|
|
2640
|
+
approvalMode,
|
|
2641
|
+
permissionMode: approvalMode,
|
|
2642
|
+
profile: profile || undefined,
|
|
2643
|
+
agentProfile,
|
|
2644
|
+
providerArgs,
|
|
2645
|
+
prompt,
|
|
2646
|
+
systemPromptAppend,
|
|
2647
|
+
policyName,
|
|
2648
|
+
spawnRequestId: requestId,
|
|
2649
|
+
env: runnerRuntimeTokenEnv({
|
|
2650
|
+
orchestratorId: orch.id,
|
|
2651
|
+
cwd: cwd || orch.baseDir,
|
|
2652
|
+
provider,
|
|
2653
|
+
label,
|
|
2654
|
+
policyName,
|
|
2655
|
+
spawnRequestId: requestId,
|
|
2656
|
+
createdBy: "dashboard",
|
|
2657
|
+
profile: profile || undefined,
|
|
2658
|
+
}),
|
|
2659
|
+
requestedBy: "dashboard",
|
|
2660
|
+
requestedAt: Date.now(),
|
|
2661
|
+
}),
|
|
2662
|
+
});
|
|
2645
2663
|
emitCommand(command);
|
|
2646
2664
|
auditEvent({
|
|
2647
|
-
clientId: "server-agent-spawn-" + provider + "-" +
|
|
2665
|
+
clientId: "server-agent-spawn-" + provider + "-" + command.id,
|
|
2648
2666
|
kind: "state",
|
|
2649
2667
|
title: `${provider} agent spawn requested (via ${orch.id})`,
|
|
2650
2668
|
body: cwd || orch.baseDir,
|
|
2651
2669
|
meta: orch.id,
|
|
2652
2670
|
icon: "ti-plus",
|
|
2653
2671
|
view: "agents",
|
|
2654
|
-
metadata: { provider, orchestratorId: orch.id, approvalMode, workspaceMode, commandId: command.id, ...authAuditMetadata(req) },
|
|
2672
|
+
metadata: { provider, orchestratorId: orch.id, approvalMode, workspaceMode, label, commandId: command.id, ...authAuditMetadata(req) },
|
|
2655
2673
|
});
|
|
2656
2674
|
return json({ ok: true, orchestratorId: orch.id, provider, command }, 202);
|
|
2657
2675
|
} catch (e) {
|
|
2676
|
+
if (e instanceof McpNotFoundError) return error(e.message, 404);
|
|
2658
2677
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
2659
2678
|
if (e instanceof Error) return error(e.message, 400);
|
|
2660
2679
|
throw e;
|
|
@@ -3515,94 +3534,6 @@ function cleanSafeNumber(value: unknown): number | undefined {
|
|
|
3515
3534
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3516
3535
|
}
|
|
3517
3536
|
|
|
3518
|
-
const postOrchestratorSpawn: Handler = async (req, params) => {
|
|
3519
|
-
const parsed = await parseBody<unknown>(req);
|
|
3520
|
-
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
3521
|
-
try {
|
|
3522
|
-
const orch = getOrchestrator(params.id!);
|
|
3523
|
-
if (!orch) return error("orchestrator not found", 404);
|
|
3524
|
-
if (orch.status !== "online") return error("orchestrator is offline", 409);
|
|
3525
|
-
|
|
3526
|
-
if (!isRecord(parsed.body)) return error("body required");
|
|
3527
|
-
const provider = optionalEnum(parsed.body.provider, "provider", SPAWN_PROVIDERS)! as SpawnProvider;
|
|
3528
|
-
const selection = validateSpawnSelectionForOrchestrator(orch, provider, parsed.body);
|
|
3529
|
-
if (selection instanceof Response) return selection;
|
|
3530
|
-
const cwd = cleanString(parsed.body.cwd, "cwd", { max: 500 });
|
|
3531
|
-
if (cwd && !isPathWithinBase(cwd, orch.baseDir)) {
|
|
3532
|
-
return error(`cwd must be within orchestrator base directory: ${orch.baseDir}`);
|
|
3533
|
-
}
|
|
3534
|
-
const label = cleanString(parsed.body.label, "label", { max: 120 });
|
|
3535
|
-
const approvalMode = optionalEnum(parsed.body.approvalMode, "approvalMode", APPROVAL_MODES, "guarded") as SpawnApprovalMode;
|
|
3536
|
-
const prompt = cleanString(parsed.body.prompt, "prompt", { max: 16000 });
|
|
3537
|
-
const systemPromptAppend = cleanString(parsed.body.systemPromptAppend, "systemPromptAppend", { max: 64_000 });
|
|
3538
|
-
const tags = cleanStringArray(parsed.body.tags, "tags", { itemMax: 80, maxItems: 50 }) ?? [];
|
|
3539
|
-
const capabilities = cleanStringArray(parsed.body.capabilities, "capabilities", { itemMax: 80, maxItems: 50 }) ?? [];
|
|
3540
|
-
const providerArgs = cleanStringArray(parsed.body.providerArgs, "providerArgs", { itemMax: 80, maxItems: 50 }) ?? [];
|
|
3541
|
-
const profile = cleanString(parsed.body.profile, "profile", { max: 120 });
|
|
3542
|
-
const workspaceMode = optionalEnum(parsed.body.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES, "inherit") as WorkspaceMode;
|
|
3543
|
-
const agentProfile = profile ? getAgentProfile(profile)?.value : undefined;
|
|
3544
|
-
if (profile && !agentProfile) return error("agent profile not found", 404);
|
|
3545
|
-
const policyName = cleanString(parsed.body.policyName, "policyName", { max: 120 });
|
|
3546
|
-
const requestId = cleanString(parsed.body.spawnRequestId, "spawnRequestId", { max: 160 }) ?? spawnRequestId();
|
|
3547
|
-
const denied = authorizeRoute(req, {
|
|
3548
|
-
scope: "agent:write",
|
|
3549
|
-
resource: { orchestratorId: orch.id, cwd: cwd || orch.baseDir, policyName, spawnRequestId: requestId },
|
|
3550
|
-
});
|
|
3551
|
-
if (denied) return denied;
|
|
3552
|
-
|
|
3553
|
-
const command = createCommand({
|
|
3554
|
-
type: "agent.spawn",
|
|
3555
|
-
source: "system",
|
|
3556
|
-
target: orch.agentId,
|
|
3557
|
-
correlationId: requestId,
|
|
3558
|
-
params: buildSpawnCommand({
|
|
3559
|
-
provider,
|
|
3560
|
-
modelParams: { model: selection.model, providerModel: selection.providerModel, effort: selection.effort },
|
|
3561
|
-
cwd: cwd || orch.baseDir,
|
|
3562
|
-
workspaceMode,
|
|
3563
|
-
label,
|
|
3564
|
-
tags,
|
|
3565
|
-
capabilities,
|
|
3566
|
-
approvalMode,
|
|
3567
|
-
permissionMode: approvalMode,
|
|
3568
|
-
profile,
|
|
3569
|
-
agentProfile,
|
|
3570
|
-
providerArgs,
|
|
3571
|
-
prompt,
|
|
3572
|
-
systemPromptAppend,
|
|
3573
|
-
policyName,
|
|
3574
|
-
spawnRequestId: requestId,
|
|
3575
|
-
env: runnerRuntimeTokenEnv({
|
|
3576
|
-
orchestratorId: orch.id,
|
|
3577
|
-
cwd: cwd || orch.baseDir,
|
|
3578
|
-
provider,
|
|
3579
|
-
label,
|
|
3580
|
-
policyName,
|
|
3581
|
-
spawnRequestId: requestId,
|
|
3582
|
-
createdBy: "dashboard",
|
|
3583
|
-
}),
|
|
3584
|
-
requestedBy: "dashboard",
|
|
3585
|
-
requestedAt: Date.now(),
|
|
3586
|
-
}),
|
|
3587
|
-
});
|
|
3588
|
-
emitCommand(command);
|
|
3589
|
-
auditEvent({
|
|
3590
|
-
clientId: "server-orchestrator-spawn-" + orch.id + "-" + command.id,
|
|
3591
|
-
kind: "state",
|
|
3592
|
-
title: `Spawn ${provider} agent requested`,
|
|
3593
|
-
body: cwd || orch.baseDir,
|
|
3594
|
-
meta: orch.id,
|
|
3595
|
-
icon: "ti-plus",
|
|
3596
|
-
view: "orchestrators",
|
|
3597
|
-
metadata: { orchestratorId: orch.id, provider, approvalMode, workspaceMode, label, commandId: command.id, ...authAuditMetadata(req) },
|
|
3598
|
-
});
|
|
3599
|
-
return json({ ok: true, orchestratorId: orch.id, command }, 202);
|
|
3600
|
-
} catch (e) {
|
|
3601
|
-
if (e instanceof ValidationError) return error(e.message, 400);
|
|
3602
|
-
throw e;
|
|
3603
|
-
}
|
|
3604
|
-
};
|
|
3605
|
-
|
|
3606
3537
|
const ORCH_UPGRADE_DEADLINE_MS = 5 * 60_000;
|
|
3607
3538
|
const ORCH_UPGRADE_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
3608
3539
|
const VALID_ORCH_UPGRADE_PROVIDERS = ["auto", "all", "codex", "claude", "orchestrator"] as const;
|
|
@@ -6698,7 +6629,6 @@ const routes: Route[] = [
|
|
|
6698
6629
|
route("GET", "/api/orchestrators/:id", getOrchestratorById),
|
|
6699
6630
|
route("POST", "/api/orchestrators/:id/heartbeat", postOrchestratorHeartbeat),
|
|
6700
6631
|
route("PATCH", "/api/orchestrators/:id/agents", patchOrchestratorAgents),
|
|
6701
|
-
route("POST", "/api/orchestrators/:id/spawn", postOrchestratorSpawn),
|
|
6702
6632
|
route("POST", "/api/orchestrators/:id/runner-token", postOrchestratorRunnerToken),
|
|
6703
6633
|
route("POST", "/api/orchestrators/:id/actions", postOrchestratorAction),
|
|
6704
6634
|
route("GET", "/api/orchestrators/:id/directories", getOrchestratorDirectories),
|
package/src/runtime-tokens.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createToken, getTokenProfile, revokeToken } from "./token-db";
|
|
2
2
|
import { verifyComponentTokenAllowExpired } from "./security";
|
|
3
|
+
import { spawnGrantForProfile } from "./config-store";
|
|
3
4
|
import type { TokenRecord } from "./types";
|
|
4
5
|
|
|
5
6
|
// Scopes that turn an agent's runtime token into a spawn-capable one (#221). Appended to the
|
|
@@ -89,13 +90,19 @@ export function issueRunnerRuntimeToken(input: {
|
|
|
89
90
|
policyName?: string;
|
|
90
91
|
spawnRequestId?: string;
|
|
91
92
|
createdBy?: string;
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Agent profile name. The spawn grant (the `command:spawn`/`command:shutdown` scope + the
|
|
95
|
+
* live-children quota) is resolved from this profile's `maxSpawnedAgents` HERE, in the single
|
|
96
|
+
* mint, so capability⇒tool holds for EVERY spawn path. Callers pass the profile they already
|
|
97
|
+
* have; they cannot forget to thread the grant (the recurring bug this centralization kills).
|
|
98
|
+
*/
|
|
99
|
+
profile?: string;
|
|
100
|
+
/** Agent-requested spawn → forces a non-spawn-capable child regardless of profile (no grandchildren). */
|
|
101
|
+
agentInitiated?: boolean;
|
|
96
102
|
/** Parent agent id — stamped so the child registers with an authoritative `spawnedBy`. */
|
|
97
103
|
spawnedBy?: string;
|
|
98
104
|
}): RuntimeTokenResult {
|
|
105
|
+
const grant = spawnGrantForProfile(input.profile, input.agentInitiated);
|
|
99
106
|
const subject = input.policyName
|
|
100
107
|
? `runner:policy:${input.policyName}`
|
|
101
108
|
: input.spawnRequestId
|
|
@@ -105,13 +112,13 @@ export function issueRunnerRuntimeToken(input: {
|
|
|
105
112
|
profileId: "provider-agent",
|
|
106
113
|
sub: subject,
|
|
107
114
|
role: "provider",
|
|
108
|
-
scope: runnerScopeWithSpawn(
|
|
115
|
+
scope: runnerScopeWithSpawn(grant.canSpawn),
|
|
109
116
|
constraints: {
|
|
110
117
|
orchestrators: [input.orchestratorId],
|
|
111
118
|
cwdPrefixes: [input.cwd],
|
|
112
119
|
...(input.policyName ? { policies: [input.policyName] } : {}),
|
|
113
120
|
...(input.spawnRequestId ? { spawnRequestIds: [input.spawnRequestId] } : {}),
|
|
114
|
-
...(
|
|
121
|
+
...(grant.canSpawn && grant.maxSpawnedAgents ? { maxSpawnedAgents: grant.maxSpawnedAgents } : {}),
|
|
115
122
|
...(input.spawnedBy ? { spawnedBy: input.spawnedBy } : {}),
|
|
116
123
|
},
|
|
117
124
|
createdBy: input.createdBy ?? "runtime",
|
|
@@ -226,8 +233,10 @@ export function runnerRuntimeTokenEnv(input: {
|
|
|
226
233
|
policyName?: string;
|
|
227
234
|
spawnRequestId?: string;
|
|
228
235
|
createdBy?: string;
|
|
229
|
-
|
|
230
|
-
|
|
236
|
+
/** Agent profile name — spawn grant is resolved from it in the mint (see issueRunnerRuntimeToken). */
|
|
237
|
+
profile?: string;
|
|
238
|
+
/** Agent-requested spawn → forces a non-spawn-capable child (no grandchildren). */
|
|
239
|
+
agentInitiated?: boolean;
|
|
231
240
|
spawnedBy?: string;
|
|
232
241
|
}): Record<string, string> {
|
|
233
242
|
const issued = issueRunnerRuntimeToken(input);
|
package/src/security.ts
CHANGED
|
@@ -170,7 +170,6 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
170
170
|
if (pathname === "/api/orchestrators/bootstrap") return "token:write";
|
|
171
171
|
if (pathname.match(/^\/api\/orchestrators\/[^/]+\/logs\//)) return "logs:read";
|
|
172
172
|
if (pathname.match(/^\/api\/orchestrators\/[^/]+\/terminal\//)) return "terminal:attach";
|
|
173
|
-
if (pathname.match(/^\/api\/orchestrators\/[^/]+\/spawn$/)) return "agent:write";
|
|
174
173
|
if (pathname.startsWith("/api/artifacts")) {
|
|
175
174
|
if (method === "GET" || method === "HEAD") return "artifact:read";
|
|
176
175
|
if (method === "DELETE") return "artifact:admin";
|
|
@@ -242,7 +241,6 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
242
241
|
if (pathname === "/api/mcp") return "mcp:use";
|
|
243
242
|
if (pathname.match(/^\/api\/orchestrators\/[^/]+\/logs\//)) return "logs:read";
|
|
244
243
|
if (pathname.match(/^\/api\/orchestrators\/[^/]+\/terminal\//)) return "terminal:attach";
|
|
245
|
-
if (pathname.match(/^\/api\/orchestrators\/[^/]+\/spawn$/)) return "agent:write";
|
|
246
244
|
if (pathname.startsWith("/api/commands")) {
|
|
247
245
|
if (method === "GET") return "command:read";
|
|
248
246
|
return "command:write";
|