pilotswarm-sdk 0.5.41 → 0.5.43
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/README.md +2 -1
- package/api/index.d.ts +22 -1
- package/api/index.js +5 -0
- package/api/src/api-client.js +20 -1
- package/api/src/http-api-transport.js +102 -1
- package/api/src/protocol.js +39 -4
- package/api/src/surface-contract.js +44 -0
- package/dist/agent-manager-tools.d.ts +2 -0
- package/dist/agent-manager-tools.d.ts.map +1 -1
- package/dist/agent-manager-tools.js +24 -15
- package/dist/agent-manager-tools.js.map +1 -1
- package/dist/agent-package-service.d.ts +9 -0
- package/dist/agent-package-service.d.ts.map +1 -1
- package/dist/agent-package-service.js +46 -2
- package/dist/agent-package-service.js.map +1 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +82 -19
- package/dist/client.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +3538 -0
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +25 -0
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +43 -4
- package/dist/cms.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +9 -1
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +40 -8
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts +410 -1
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +1254 -78
- package/dist/management-client.js.map +1 -1
- package/dist/model-providers.d.ts +13 -3
- package/dist/model-providers.d.ts.map +1 -1
- package/dist/model-providers.js +50 -13
- package/dist/model-providers.js.map +1 -1
- package/dist/orchestration/index.d.ts +2 -2
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/lifecycle.d.ts.map +1 -1
- package/dist/orchestration/lifecycle.js +6 -3
- package/dist/orchestration/lifecycle.js.map +1 -1
- package/dist/orchestration/queue.d.ts.map +1 -1
- package/dist/orchestration/queue.js +1 -0
- package/dist/orchestration/queue.js.map +1 -1
- package/dist/orchestration/runtime.d.ts +1 -1
- package/dist/orchestration/state.d.ts +20 -0
- package/dist/orchestration/state.d.ts.map +1 -1
- package/dist/orchestration/state.js +1 -0
- package/dist/orchestration/state.js.map +1 -1
- package/dist/orchestration/turn.d.ts.map +1 -1
- package/dist/orchestration/turn.js +134 -3
- package/dist/orchestration/turn.js.map +1 -1
- package/dist/orchestration-registry.d.ts.map +1 -1
- package/dist/orchestration-registry.js +4 -2
- package/dist/orchestration-registry.js.map +1 -1
- package/dist/orchestration-version.d.ts +1 -1
- package/dist/orchestration-version.js +1 -1
- package/dist/orchestration.d.ts +2 -2
- package/dist/orchestration.js +1 -1
- package/dist/orchestration_1_0_46.d.ts +1 -1
- package/dist/orchestration_1_0_47.d.ts +1 -1
- package/dist/orchestration_1_0_48.d.ts +1 -1
- package/dist/orchestration_1_0_49.d.ts +1 -1
- package/dist/orchestration_1_0_50.d.ts +1 -1
- package/dist/orchestration_1_0_68/agents.d.ts +61 -0
- package/dist/orchestration_1_0_68/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_68/agents.js +840 -0
- package/dist/orchestration_1_0_68/agents.js.map +1 -0
- package/dist/orchestration_1_0_68/index.d.ts +30 -0
- package/dist/orchestration_1_0_68/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_68/index.js +13 -0
- package/dist/orchestration_1_0_68/index.js.map +1 -0
- package/dist/orchestration_1_0_68/lifecycle.d.ts +49 -0
- package/dist/orchestration_1_0_68/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_68/lifecycle.js +944 -0
- package/dist/orchestration_1_0_68/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_68/queue.d.ts +7 -0
- package/dist/orchestration_1_0_68/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_68/queue.js +804 -0
- package/dist/orchestration_1_0_68/queue.js.map +1 -0
- package/dist/orchestration_1_0_68/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_68/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_68/runtime.js +228 -0
- package/dist/orchestration_1_0_68/runtime.js.map +1 -0
- package/dist/orchestration_1_0_68/state.d.ts +176 -0
- package/dist/orchestration_1_0_68/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_68/state.js +158 -0
- package/dist/orchestration_1_0_68/state.js.map +1 -0
- package/dist/orchestration_1_0_68/turn.d.ts +34 -0
- package/dist/orchestration_1_0_68/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_68/turn.js +1211 -0
- package/dist/orchestration_1_0_68/turn.js.map +1 -0
- package/dist/orchestration_1_0_68/utils.d.ts +39 -0
- package/dist/orchestration_1_0_68/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_68/utils.js +316 -0
- package/dist/orchestration_1_0_68/utils.js.map +1 -0
- package/dist/provider-budgets.d.ts +127 -0
- package/dist/provider-budgets.d.ts.map +1 -0
- package/dist/provider-budgets.js +182 -0
- package/dist/provider-budgets.js.map +1 -0
- package/dist/provider-catalog.d.ts +125 -0
- package/dist/provider-catalog.d.ts.map +1 -0
- package/dist/provider-catalog.js +299 -0
- package/dist/provider-catalog.js.map +1 -0
- package/dist/provider-store.d.ts +383 -0
- package/dist/provider-store.d.ts.map +1 -0
- package/dist/provider-store.js +514 -0
- package/dist/provider-store.js.map +1 -0
- package/dist/provider-tools.d.ts +47 -0
- package/dist/provider-tools.d.ts.map +1 -0
- package/dist/provider-tools.js +585 -0
- package/dist/provider-tools.js.map +1 -0
- package/dist/session-manager.d.ts +23 -2
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +144 -6
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +4 -3
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +202 -11
- package/dist/session-proxy.js.map +1 -1
- package/dist/system-agents.d.ts +12 -0
- package/dist/system-agents.d.ts.map +1 -1
- package/dist/system-agents.js +54 -12
- package/dist/system-agents.js.map +1 -1
- package/dist/types.d.ts +19 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/web/generated-op-methods.d.ts +208 -2
- package/dist/web/generated-op-methods.d.ts.map +1 -1
- package/dist/web/generated-op-methods.js +50 -0
- package/dist/web/generated-op-methods.js.map +1 -1
- package/dist/web/web-management-client.d.ts +112 -2
- package/dist/web/web-management-client.d.ts.map +1 -1
- package/dist/web/web-management-client.js +163 -0
- package/dist/web/web-management-client.js.map +1 -1
- package/dist/worker.d.ts +9 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +124 -19
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/plugins/mgmt/agents/token-manager.agent.md +183 -0
- package/plugins/system/agents/default.agent.md +21 -7
- package/plugins/system/skills/canvas-apps/SKILL.md +158 -0
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { RESPONSE_LATEST_KEY, commandResponseKey, stopTurnQueueName, sanitizePromptAttachmentRefs, } from "./types.js";
|
|
14
14
|
import { SYSTEM_USER_PRINCIPAL } from "./cms.js";
|
|
15
|
+
import { ProviderError } from "./provider-store.js";
|
|
16
|
+
import { PROVIDER_BUDGET_WAKE_PROMPT } from "./provider-budgets.js";
|
|
15
17
|
import { LOCAL_DEFAULT_USER_PRINCIPAL } from "./session-owner-utils.js";
|
|
16
18
|
import { normalizeMessageSender } from "./message-sender.js";
|
|
17
19
|
import { isEnhancedFactStore, EnhancedFactsUnsupportedError } from "./facts-store.js";
|
|
@@ -19,11 +21,16 @@ import { resolveStorageConfig } from "./storage-config.js";
|
|
|
19
21
|
import { getDuroxideStorageProvider, getRuntimeStorageProvider } from "./storage-providers.js";
|
|
20
22
|
import { SessionDumper } from "./session-dumper.js";
|
|
21
23
|
import { computeSessionFootprint, FootprintCache } from "./footprint.js";
|
|
22
|
-
import { loadModelProviders } from "./model-providers.js";
|
|
24
|
+
import { loadModelProviders, loadModelProviderTypes } from "./model-providers.js";
|
|
25
|
+
import { bootstrapProviders, resolveProviderCredential, resolveRuntimeModelSelection } from "./provider-catalog.js";
|
|
23
26
|
import { deriveStatusFromCmsAndRuntime, shouldSyncCompletedStatus, shouldSyncFailedStatus } from "./session-status.js";
|
|
24
27
|
import { assertUnambiguousProvider, isWebOptions } from "./web/api-connection.js";
|
|
25
28
|
import { WebPilotSwarmManagementClient } from "./web/web-management-client.js";
|
|
26
29
|
import { loadSystemAgentConfigs, resolveSystemAgentSessionPlans, startSystemAgents, } from "./system-agents.js";
|
|
30
|
+
import { AgentPackageValidationError, deleteAgentPackageEverywhere, fetchAgentPackageTarGz, publishAgentPackageDir, publishPackedAgentPackage, readAgentPackageVersionEntries, resolveAgentPackageVersion, } from "./agent-package-service.js";
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import os from "node:os";
|
|
33
|
+
import path from "node:path";
|
|
27
34
|
// duroxide is CommonJS — use createRequire for ESM compatibility
|
|
28
35
|
import { createRequire } from "node:module";
|
|
29
36
|
const require = createRequire(import.meta.url);
|
|
@@ -49,6 +56,22 @@ function assertValidDate(value, label) {
|
|
|
49
56
|
throw new Error(`${label} must be a valid Date`);
|
|
50
57
|
}
|
|
51
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* A default is one tuple, saved whole. With no provider there is nothing to
|
|
61
|
+
* run, so the other three go with it — carrying a model for a provider that
|
|
62
|
+
* was cleared would leave a half-default nobody can act on.
|
|
63
|
+
*/
|
|
64
|
+
function normalizeDefaultTuple(tuple) {
|
|
65
|
+
const provider = String(tuple?.provider ?? "").trim() || null;
|
|
66
|
+
if (!provider)
|
|
67
|
+
return { provider: null, model: null, reasoning: null, context: null };
|
|
68
|
+
return {
|
|
69
|
+
provider,
|
|
70
|
+
model: String(tuple?.model ?? "").trim() || null,
|
|
71
|
+
reasoning: String(tuple?.reasoning ?? "").trim() || null,
|
|
72
|
+
context: String(tuple?.context ?? "").trim() || null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
52
75
|
function isTerminalOrchestrationStatus(status) {
|
|
53
76
|
return status === "Completed" || status === "Failed" || status === "Terminated";
|
|
54
77
|
}
|
|
@@ -128,7 +151,7 @@ function normalizeSystemRestartDisposition(disposition) {
|
|
|
128
151
|
return "terminate";
|
|
129
152
|
if (disposition === "hard_delete" || disposition === "hardDelete")
|
|
130
153
|
return "hard_delete";
|
|
131
|
-
throw new Error(`Unsupported system session restart disposition: ${String(disposition)}`);
|
|
154
|
+
throw Object.assign(new Error(`Unsupported system session restart disposition: ${String(disposition)}`), { code: "INVALID_REQUEST" });
|
|
132
155
|
}
|
|
133
156
|
function sessionViewFromCmsRow(row) {
|
|
134
157
|
const liveStatus = row.state || "pending";
|
|
@@ -152,6 +175,7 @@ function sessionViewFromCmsRow(row) {
|
|
|
152
175
|
serviceOf: row.serviceOf ?? undefined,
|
|
153
176
|
model: row.model ?? undefined,
|
|
154
177
|
reasoningEffort: row.reasoningEffort ?? undefined,
|
|
178
|
+
contextTier: row.contextTier ?? undefined,
|
|
155
179
|
shortSummary: row.shortSummary ?? undefined,
|
|
156
180
|
summaryState: row.summaryState ?? undefined,
|
|
157
181
|
summaryUpdatedAt: row.summaryUpdatedAt?.getTime(),
|
|
@@ -162,6 +186,7 @@ function sessionViewFromCmsRow(row) {
|
|
|
162
186
|
rootSessionId: row.rootSessionId ?? row.sessionId,
|
|
163
187
|
};
|
|
164
188
|
}
|
|
189
|
+
export const PROVIDER_USAGE_DIMENSIONS = ["session", "user", "provider", "model", "agent"];
|
|
165
190
|
// ─── Management Client ──────────────────────────────────────────
|
|
166
191
|
function normalizeShareSlot(slot) {
|
|
167
192
|
const n = Math.floor(Number(slot));
|
|
@@ -177,6 +202,7 @@ export class PilotSwarmManagementClient {
|
|
|
177
202
|
_duroxideClient = null;
|
|
178
203
|
_modelProviders = null;
|
|
179
204
|
_systemAgents = [];
|
|
205
|
+
_artifactStore = null;
|
|
180
206
|
_activeStatusWaitControllers = new Set();
|
|
181
207
|
_activeStatusWaitPromises = new Set();
|
|
182
208
|
_started = false;
|
|
@@ -198,6 +224,7 @@ export class PilotSwarmManagementClient {
|
|
|
198
224
|
return new WebPilotSwarmManagementClient(options);
|
|
199
225
|
}
|
|
200
226
|
this.config = options;
|
|
227
|
+
this._artifactStore = options.artifactStore ?? null;
|
|
201
228
|
}
|
|
202
229
|
// ─── Lifecycle ───────────────────────────────────────────
|
|
203
230
|
async start() {
|
|
@@ -255,7 +282,14 @@ export class PilotSwarmManagementClient {
|
|
|
255
282
|
}
|
|
256
283
|
}
|
|
257
284
|
// Load model providers
|
|
258
|
-
this._modelProviders =
|
|
285
|
+
this._modelProviders = loadModelProviderTypes(this.config.modelProvidersPath);
|
|
286
|
+
const legacyProviders = loadModelProviders(this.config.modelProvidersPath);
|
|
287
|
+
if (this._catalog.providers && legacyProviders) {
|
|
288
|
+
await bootstrapProviders(this._catalog.providers, {
|
|
289
|
+
providers: legacyProviders.allProviders,
|
|
290
|
+
defaultModel: legacyProviders.defaultModel,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
259
293
|
this._systemAgents = loadSystemAgentConfigs({
|
|
260
294
|
pluginDirs: this.config.pluginDirs,
|
|
261
295
|
disableManagementAgents: this.config.disableManagementAgents,
|
|
@@ -354,8 +388,8 @@ export class PilotSwarmManagementClient {
|
|
|
354
388
|
candidate.sessionId === target ||
|
|
355
389
|
`session-${candidate.sessionId}` === target);
|
|
356
390
|
if (!plan) {
|
|
357
|
-
throw new Error(`System agent "${target}" is not known to this management client. ` +
|
|
358
|
-
`Configure pluginDirs/systemAgents so the client can recreate the system session.`);
|
|
391
|
+
throw Object.assign(new Error(`System agent "${target}" is not known to this management client. ` +
|
|
392
|
+
`Configure pluginDirs/systemAgents so the client can recreate the system session.`), { code: "NOT_FOUND" });
|
|
359
393
|
}
|
|
360
394
|
return plan;
|
|
361
395
|
}
|
|
@@ -672,6 +706,7 @@ export class PilotSwarmManagementClient {
|
|
|
672
706
|
isSystem: row.isSystem || undefined,
|
|
673
707
|
model: row.model ?? undefined,
|
|
674
708
|
reasoningEffort: row.reasoningEffort ?? undefined,
|
|
709
|
+
contextTier: row.contextTier ?? undefined,
|
|
675
710
|
shortSummary: row.shortSummary ?? undefined,
|
|
676
711
|
summaryState: row.summaryState ?? undefined,
|
|
677
712
|
summaryUpdatedAt: row.summaryUpdatedAt?.getTime(),
|
|
@@ -945,10 +980,42 @@ export class PilotSwarmManagementClient {
|
|
|
945
980
|
const trimmed = String(model || "").trim();
|
|
946
981
|
if (!trimmed)
|
|
947
982
|
throw new Error("setSessionModel requires a model id");
|
|
948
|
-
const
|
|
949
|
-
|
|
983
|
+
const session = await this.getSession(sessionId).catch(() => null);
|
|
984
|
+
if (!session)
|
|
985
|
+
throw new Error(`Session ${sessionId.slice(0, 8)} was not found`);
|
|
986
|
+
const store = this._requireProviders();
|
|
987
|
+
if (!this._modelProviders)
|
|
988
|
+
throw new Error("Provider type catalog is unavailable");
|
|
989
|
+
const actor = session.owner
|
|
990
|
+
? await store.lookupUserId(session.owner)
|
|
991
|
+
: null;
|
|
992
|
+
const credentials = await store.allCredentials();
|
|
993
|
+
let selection;
|
|
994
|
+
try {
|
|
995
|
+
selection = resolveRuntimeModelSelection(this._modelProviders, credentials, {
|
|
996
|
+
requestedModel: trimmed,
|
|
997
|
+
requestedReasoning: opts?.reasoningEffort,
|
|
998
|
+
requestedContext: opts?.contextTier,
|
|
999
|
+
eligible: session.isSystem
|
|
1000
|
+
? (provider) => provider.class === "shared" || provider.systemUseEnabled === true
|
|
1001
|
+
: (provider) => provider.class === "shared" || (actor !== null && provider.ownerUserId === actor),
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
catch (error) {
|
|
1005
|
+
if (error?.code === "MODEL_AMBIGUOUS")
|
|
1006
|
+
throw error;
|
|
1007
|
+
if (error?.code === "MODEL_UNRESOLVED") {
|
|
1008
|
+
throw Object.assign(new Error(`Unknown model: ${trimmed}. ${error.message}`), { code: "MODEL_UNRESOLVED" });
|
|
1009
|
+
}
|
|
1010
|
+
throw new Error(`Unknown model: ${trimmed}. ${error?.message || String(error)}`);
|
|
1011
|
+
}
|
|
1012
|
+
const credential = credentials.find((provider) => provider.name === selection.provider);
|
|
1013
|
+
const modelName = selection.model.slice(selection.model.indexOf(":") + 1);
|
|
1014
|
+
const match = this._providerTypeModels(credential.typeId)
|
|
1015
|
+
.find((candidate) => candidate.modelName === modelName);
|
|
950
1016
|
if (!match)
|
|
951
|
-
throw new Error(`Unknown model: ${
|
|
1017
|
+
throw new Error(`Unknown model: ${selection.model}`);
|
|
1018
|
+
const resolvedModel = selection.model;
|
|
952
1019
|
// Reasoning effort is applied ONLY when the caller asked for one.
|
|
953
1020
|
// When omitted, the key is left out of the command args entirely so
|
|
954
1021
|
// the orchestration's set_model handler PRESERVES the session's
|
|
@@ -960,7 +1027,7 @@ export class PilotSwarmManagementClient {
|
|
|
960
1027
|
if (nextReasoningEffort) {
|
|
961
1028
|
const supported = match.supportedReasoningEfforts ?? [];
|
|
962
1029
|
if (!supported.includes(nextReasoningEffort)) {
|
|
963
|
-
throw new Error(`Model ${
|
|
1030
|
+
throw new Error(`Model ${resolvedModel} does not support reasoning effort '${nextReasoningEffort}'`);
|
|
964
1031
|
}
|
|
965
1032
|
}
|
|
966
1033
|
// Context-window tier follows the same preserve-when-omitted contract.
|
|
@@ -969,17 +1036,14 @@ export class PilotSwarmManagementClient {
|
|
|
969
1036
|
if (nextContextTier) {
|
|
970
1037
|
const supported = match.supportedContextTiers ?? [];
|
|
971
1038
|
if (!supported.includes(nextContextTier)) {
|
|
972
|
-
throw new Error(`Model ${
|
|
1039
|
+
throw new Error(`Model ${resolvedModel} does not support context tier '${nextContextTier}'`);
|
|
973
1040
|
}
|
|
974
1041
|
}
|
|
975
|
-
const session = await this.getSession(sessionId).catch(() => null);
|
|
976
|
-
if (!session)
|
|
977
|
-
throw new Error(`Session ${sessionId.slice(0, 8)} was not found`);
|
|
978
1042
|
await this.sendCommand(sessionId, {
|
|
979
1043
|
cmd: "set_model",
|
|
980
1044
|
id: buildLifecycleCommandId("set-model"),
|
|
981
1045
|
args: {
|
|
982
|
-
model:
|
|
1046
|
+
model: resolvedModel,
|
|
983
1047
|
...(hasExplicitEffort ? { reasoningEffort: nextReasoningEffort } : {}),
|
|
984
1048
|
...(hasExplicitContextTier ? { contextTier: nextContextTier } : {}),
|
|
985
1049
|
source: opts?.source ?? "user",
|
|
@@ -1000,18 +1064,55 @@ export class PilotSwarmManagementClient {
|
|
|
1000
1064
|
if (!session)
|
|
1001
1065
|
throw new Error(`Session ${sessionId.slice(0, 8)} was not found`);
|
|
1002
1066
|
if (session.isSystem) {
|
|
1003
|
-
throw new Error("System sessions are excluded from regeneration (use restartSystemSession)");
|
|
1067
|
+
throw Object.assign(new Error("System sessions are excluded from regeneration (use restartSystemSession)"), { code: "REGENERATE_UNSUPPORTED" });
|
|
1004
1068
|
}
|
|
1005
1069
|
if (session.serviceKind) {
|
|
1006
|
-
throw new Error("Service sessions are runtime machinery and are excluded from regeneration");
|
|
1070
|
+
throw Object.assign(new Error("Service sessions are runtime machinery and are excluded from regeneration"), { code: "REGENERATE_UNSUPPORTED" });
|
|
1007
1071
|
}
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1072
|
+
const store = this._requireProviders();
|
|
1073
|
+
if (!this._modelProviders)
|
|
1074
|
+
throw new Error("Provider type catalog is unavailable");
|
|
1075
|
+
const actor = session.owner ? await store.lookupUserId(session.owner) : null;
|
|
1076
|
+
const credentials = await store.allCredentials();
|
|
1077
|
+
const eligible = (provider) => provider.class === "shared" || (actor !== null && provider.ownerUserId === actor);
|
|
1078
|
+
const resolveRequested = (model, reasoning, context) => resolveRuntimeModelSelection(this._modelProviders, credentials, {
|
|
1079
|
+
requestedModel: model,
|
|
1080
|
+
requestedReasoning: reasoning,
|
|
1081
|
+
requestedContext: context,
|
|
1082
|
+
eligible,
|
|
1083
|
+
});
|
|
1084
|
+
let targetModel = opts?.model;
|
|
1085
|
+
let distillerModel = opts?.distillerModel;
|
|
1086
|
+
let distillerReasoningEffort = opts?.distillerReasoningEffort;
|
|
1087
|
+
let distillerContextTier = opts?.distillerContextTier;
|
|
1088
|
+
if (targetModel) {
|
|
1089
|
+
try {
|
|
1090
|
+
targetModel = resolveRequested(targetModel).model;
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
if (error?.code === "MODEL_AMBIGUOUS")
|
|
1094
|
+
throw error;
|
|
1095
|
+
if (error?.code === "MODEL_UNRESOLVED") {
|
|
1096
|
+
throw Object.assign(new Error(`Unknown model: ${opts?.model}. ${error.message}`), { code: "MODEL_UNRESOLVED" });
|
|
1097
|
+
}
|
|
1098
|
+
throw new Error(`Unknown model: ${opts?.model}. ${error?.message || String(error)}`);
|
|
1099
|
+
}
|
|
1012
1100
|
}
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1101
|
+
if (distillerModel) {
|
|
1102
|
+
try {
|
|
1103
|
+
const resolved = resolveRequested(distillerModel, distillerReasoningEffort, distillerContextTier);
|
|
1104
|
+
distillerModel = resolved.model;
|
|
1105
|
+
distillerReasoningEffort = resolved.reasoning ?? undefined;
|
|
1106
|
+
distillerContextTier = resolved.context ?? undefined;
|
|
1107
|
+
}
|
|
1108
|
+
catch (error) {
|
|
1109
|
+
if (error?.code === "MODEL_AMBIGUOUS")
|
|
1110
|
+
throw error;
|
|
1111
|
+
if (error?.code === "MODEL_UNRESOLVED") {
|
|
1112
|
+
throw Object.assign(new Error(`Unknown distiller model: ${opts?.distillerModel}. ${error.message}`), { code: "MODEL_UNRESOLVED" });
|
|
1113
|
+
}
|
|
1114
|
+
throw new Error(`Unknown distiller model: ${opts?.distillerModel}. ${error?.message || String(error)}`);
|
|
1115
|
+
}
|
|
1015
1116
|
}
|
|
1016
1117
|
// Dead-model guard: the reborn session's grounding turn runs on the
|
|
1017
1118
|
// session's model. If that model no longer resolves (a common regen
|
|
@@ -1019,8 +1120,16 @@ export class PilotSwarmManagementClient {
|
|
|
1019
1120
|
// discard the transcript and then wedge on the dead model. Require a
|
|
1020
1121
|
// live replacement rather than making a broken session strictly worse.
|
|
1021
1122
|
const currentModel = session.model;
|
|
1022
|
-
if (!
|
|
1023
|
-
|
|
1123
|
+
if (!targetModel && currentModel) {
|
|
1124
|
+
try {
|
|
1125
|
+
resolveRequested(currentModel);
|
|
1126
|
+
}
|
|
1127
|
+
catch (error) {
|
|
1128
|
+
if (error?.code === "MODEL_AMBIGUOUS")
|
|
1129
|
+
throw error;
|
|
1130
|
+
throw Object.assign(new Error(`Session model '${currentModel}' is no longer available; pass a replacement model to regenerate this session. `
|
|
1131
|
+
+ `${error?.message || String(error)}`), { code: "MODEL_UNRESOLVED" });
|
|
1132
|
+
}
|
|
1024
1133
|
}
|
|
1025
1134
|
const attemptId = buildLifecycleCommandId("regenerate");
|
|
1026
1135
|
await this.sendCommand(sessionId, {
|
|
@@ -1030,10 +1139,10 @@ export class PilotSwarmManagementClient {
|
|
|
1030
1139
|
...(opts?.handoff ? { handoff: String(opts.handoff).slice(0, 4_000) } : {}),
|
|
1031
1140
|
...(opts?.instructions ? { instructions: String(opts.instructions).slice(0, 4_000) } : {}),
|
|
1032
1141
|
...(opts?.distillMode === "deterministic" ? { distill_mode: "deterministic" } : {}),
|
|
1033
|
-
...(
|
|
1034
|
-
...(
|
|
1035
|
-
...(
|
|
1036
|
-
...(
|
|
1142
|
+
...(distillerModel ? { distillerModel } : {}),
|
|
1143
|
+
...(distillerReasoningEffort ? { distillerReasoningEffort } : {}),
|
|
1144
|
+
...(distillerContextTier ? { distillerContextTier } : {}),
|
|
1145
|
+
...(targetModel ? { model: targetModel } : {}),
|
|
1037
1146
|
...(opts?.force ? { force: true } : {}),
|
|
1038
1147
|
source: opts?.source ?? "operator",
|
|
1039
1148
|
},
|
|
@@ -1107,7 +1216,8 @@ export class PilotSwarmManagementClient {
|
|
|
1107
1216
|
async restartSystemSession(agentIdOrSessionId, options) {
|
|
1108
1217
|
this._ensureStarted();
|
|
1109
1218
|
if (!options?.disposition) {
|
|
1110
|
-
throw new Error("restartSystemSession requires a disposition: complete, terminate, or hard_delete"
|
|
1219
|
+
throw Object.assign(new Error("restartSystemSession requires a disposition: complete, terminate, or hard_delete "
|
|
1220
|
+
+ "(HTTP callers send it inside the options envelope: {\"options\":{\"disposition\":\"complete\"}})"), { code: "INVALID_REQUEST" });
|
|
1111
1221
|
}
|
|
1112
1222
|
const disposition = normalizeSystemRestartDisposition(options.disposition);
|
|
1113
1223
|
const plan = this._resolveSystemAgentPlan(agentIdOrSessionId);
|
|
@@ -1115,64 +1225,153 @@ export class PilotSwarmManagementClient {
|
|
|
1115
1225
|
const reason = options.reason ?? `Restarting system session ${plan.agent.id}`;
|
|
1116
1226
|
const existingRow = await this._catalog.getSession(sessionId);
|
|
1117
1227
|
const previousSessionExisted = Boolean(existingRow);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
if (
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
await this._waitForSession(sessionId, (current) => current != null && (current.status === "completed" || current.status === "failed" || current.status === "cancelled"), options.timeoutMs ?? SESSION_COMMAND_SETTLE_TIMEOUT_MS);
|
|
1132
|
-
}
|
|
1133
|
-
catch (err) {
|
|
1134
|
-
if (!isIgnorableRestartCommandError(err))
|
|
1135
|
-
throw err;
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
await this._deleteSystemOrchestrationInstance(sessionId);
|
|
1139
|
-
await this._archiveSystemSessionForRestart(sessionId, "completed", reason);
|
|
1228
|
+
const providerStore = this._catalog.providers;
|
|
1229
|
+
let persistedRoute = null;
|
|
1230
|
+
let persistedSource = null;
|
|
1231
|
+
if (providerStore) {
|
|
1232
|
+
const [defaults, overrides, credentials] = await Promise.all([
|
|
1233
|
+
providerStore.getDefaults(null),
|
|
1234
|
+
providerStore.listSystemAgentModels(),
|
|
1235
|
+
providerStore.allCredentials(),
|
|
1236
|
+
]);
|
|
1237
|
+
const override = overrides.find((candidate) => candidate.agentId === plan.agent.id);
|
|
1238
|
+
if (override) {
|
|
1239
|
+
persistedRoute = override;
|
|
1240
|
+
persistedSource = "agent_override";
|
|
1140
1241
|
}
|
|
1141
|
-
else if (
|
|
1142
|
-
|
|
1143
|
-
|
|
1242
|
+
else if (defaults.system.provider && defaults.system.model) {
|
|
1243
|
+
persistedRoute = defaults.system;
|
|
1244
|
+
persistedSource = "system_default";
|
|
1144
1245
|
}
|
|
1145
1246
|
else {
|
|
1146
|
-
|
|
1147
|
-
|
|
1247
|
+
persistedRoute = this._firstAvailableTuple(credentials, (provider) => provider.class === "shared" || provider.systemUseEnabled === true);
|
|
1248
|
+
persistedSource = persistedRoute ? "first_available" : null;
|
|
1148
1249
|
}
|
|
1149
1250
|
}
|
|
1150
|
-
|
|
1251
|
+
let defaultModel = options.model
|
|
1252
|
+
?? persistedRoute?.model
|
|
1253
|
+
?? existingRow?.model
|
|
1254
|
+
?? this._modelProviders?.defaultModel;
|
|
1151
1255
|
if (!defaultModel) {
|
|
1152
1256
|
throw new Error("Cannot restart system session without a configured default model");
|
|
1153
1257
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1258
|
+
let restartReasoning = Object.prototype.hasOwnProperty.call(options, "reasoningEffort")
|
|
1259
|
+
? options.reasoningEffort ?? null
|
|
1260
|
+
: persistedRoute?.reasoning ?? existingRow?.reasoningEffort ?? null;
|
|
1261
|
+
let restartContext = Object.prototype.hasOwnProperty.call(options, "contextTier")
|
|
1262
|
+
? options.contextTier ?? null
|
|
1263
|
+
: persistedRoute?.context ?? existingRow?.contextTier ?? null;
|
|
1264
|
+
let restartSource = options.modelResolutionSource ?? persistedSource ?? existingRow?.modelResolutionSource ?? "manual_restart";
|
|
1265
|
+
if (providerStore) {
|
|
1266
|
+
if (!this._modelProviders) {
|
|
1267
|
+
throw new Error("Cannot restart system session without a provider type catalog");
|
|
1268
|
+
}
|
|
1269
|
+
const credentials = await providerStore.allCredentials();
|
|
1270
|
+
const validated = resolveRuntimeModelSelection(this._modelProviders, credentials, {
|
|
1271
|
+
requestedModel: defaultModel,
|
|
1272
|
+
requestedReasoning: restartReasoning,
|
|
1273
|
+
requestedContext: restartContext,
|
|
1274
|
+
eligible: (provider) => provider.class === "shared" || provider.systemUseEnabled === true,
|
|
1275
|
+
});
|
|
1276
|
+
defaultModel = validated.model;
|
|
1277
|
+
restartReasoning = validated.reasoning;
|
|
1278
|
+
restartContext = validated.context;
|
|
1279
|
+
if (!options.modelResolutionSource && !persistedSource)
|
|
1280
|
+
restartSource = validated.source;
|
|
1281
|
+
}
|
|
1282
|
+
const operationId = options.operationId ?? `manual:${buildLifecycleCommandId("system-restart")}`;
|
|
1283
|
+
const claimId = buildLifecycleCommandId("system-restart-claim");
|
|
1284
|
+
if (providerStore) {
|
|
1285
|
+
const claim = await providerStore.claimSystemRestart({
|
|
1286
|
+
agentId: plan.agent.id,
|
|
1287
|
+
operationId,
|
|
1288
|
+
claimId,
|
|
1289
|
+
model: defaultModel,
|
|
1290
|
+
reasoning: restartReasoning,
|
|
1291
|
+
context: restartContext,
|
|
1292
|
+
disposition,
|
|
1293
|
+
});
|
|
1294
|
+
if (claim !== "claimed") {
|
|
1295
|
+
return {
|
|
1296
|
+
agentId: plan.agent.id,
|
|
1297
|
+
agentName: plan.agent.name,
|
|
1298
|
+
sessionId,
|
|
1299
|
+
disposition,
|
|
1300
|
+
previousSessionExisted,
|
|
1301
|
+
startResults: [],
|
|
1302
|
+
skippedReason: claim,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (existingRow && !existingRow.isSystem) {
|
|
1307
|
+
throw new Error(`Session ${sessionId.slice(0, 8)} exists but is not a system session`);
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
if (existingRow) {
|
|
1311
|
+
if (disposition === "complete") {
|
|
1312
|
+
const view = await this.getSession(sessionId).catch(() => null);
|
|
1313
|
+
if (view && view.status !== "completed" && view.status !== "failed" && view.status !== "cancelled") {
|
|
1314
|
+
try {
|
|
1315
|
+
await this.sendCommand(sessionId, {
|
|
1316
|
+
cmd: "done",
|
|
1317
|
+
id: buildLifecycleCommandId("done-system-restart"),
|
|
1318
|
+
args: { reason },
|
|
1319
|
+
});
|
|
1320
|
+
await this._waitForSession(sessionId, (current) => current != null && (current.status === "completed" || current.status === "failed" || current.status === "cancelled"), options.timeoutMs ?? SESSION_COMMAND_SETTLE_TIMEOUT_MS);
|
|
1321
|
+
}
|
|
1322
|
+
catch (err) {
|
|
1323
|
+
if (!isIgnorableRestartCommandError(err))
|
|
1324
|
+
throw err;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
await this._deleteSystemOrchestrationInstance(sessionId);
|
|
1328
|
+
await this._archiveSystemSessionForRestart(sessionId, "completed", reason);
|
|
1329
|
+
}
|
|
1330
|
+
else if (disposition === "terminate") {
|
|
1331
|
+
await this._terminateSystemOrchestrationInstance(sessionId, reason);
|
|
1332
|
+
await this._archiveSystemSessionForRestart(sessionId, "cancelled", `Terminated for restart: ${reason}`);
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
await this._deleteSystemOrchestrationInstance(sessionId);
|
|
1336
|
+
await this._archiveSystemSessionForRestart(sessionId, "failed", `Hard-deleted for restart: ${reason}`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const startResults = await startSystemAgents({
|
|
1340
|
+
catalog: this._catalog,
|
|
1341
|
+
duroxideClient: this._duroxideClient,
|
|
1342
|
+
agents: this._systemAgents,
|
|
1343
|
+
defaultModel,
|
|
1344
|
+
defaultReasoningEffort: restartReasoning,
|
|
1345
|
+
defaultContextTier: restartContext,
|
|
1346
|
+
modelResolutionSource: restartSource,
|
|
1347
|
+
blobEnabled: this.config.blobEnabled,
|
|
1348
|
+
dehydrateThreshold: this.config.waitThreshold ?? DEFAULT_SYSTEM_AGENT_DEHYDRATE_THRESHOLD,
|
|
1349
|
+
agentId: plan.agent.id,
|
|
1350
|
+
// The retained session id already has ledger rows from earlier
|
|
1351
|
+
// lifetimes; advance its ledger base so the new lifetime's
|
|
1352
|
+
// settles land on fresh keys instead of silently colliding.
|
|
1353
|
+
...(providerStore ? {
|
|
1354
|
+
prepareRetainedSessionLedger: async (sid) => { await providerStore.bumpLedgerBase(sid); },
|
|
1355
|
+
} : {}),
|
|
1356
|
+
});
|
|
1357
|
+
const newSession = await this.getSession(sessionId).catch(() => null);
|
|
1358
|
+
if (!newSession) {
|
|
1359
|
+
throw new Error(`System session ${plan.agent.id} was not recreated`);
|
|
1360
|
+
}
|
|
1361
|
+
await providerStore?.finishSystemRestart(plan.agent.id, claimId, null);
|
|
1362
|
+
return {
|
|
1363
|
+
agentId: plan.agent.id,
|
|
1364
|
+
agentName: plan.agent.name,
|
|
1365
|
+
sessionId,
|
|
1366
|
+
disposition,
|
|
1367
|
+
previousSessionExisted,
|
|
1368
|
+
startResults,
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
catch (error) {
|
|
1372
|
+
await providerStore?.finishSystemRestart(plan.agent.id, claimId, error?.message || String(error));
|
|
1373
|
+
throw error;
|
|
1167
1374
|
}
|
|
1168
|
-
return {
|
|
1169
|
-
agentId: plan.agent.id,
|
|
1170
|
-
agentName: plan.agent.name,
|
|
1171
|
-
sessionId,
|
|
1172
|
-
disposition,
|
|
1173
|
-
previousSessionExisted,
|
|
1174
|
-
startResults,
|
|
1175
|
-
};
|
|
1176
1375
|
}
|
|
1177
1376
|
// ─── Session Events ──────────────────────────────────────
|
|
1178
1377
|
/**
|
|
@@ -2035,6 +2234,956 @@ export class PilotSwarmManagementClient {
|
|
|
2035
2234
|
await this._assertOrchestrationLive(orchId, sessionId, "sendCommand");
|
|
2036
2235
|
await this._duroxideClient.enqueueEvent(orchId, "messages", JSON.stringify({ type: "cmd", ...command }));
|
|
2037
2236
|
}
|
|
2237
|
+
// ─── Provider budgets ────────────────────────────────────
|
|
2238
|
+
//
|
|
2239
|
+
// The capabilities of docs/proposals/providers-and-budgets-surface.md.
|
|
2240
|
+
// Each one resolves the caller to a user id and hands that number, with
|
|
2241
|
+
// the role they signed in as, to a `cms_provider_*` procedure. Nothing
|
|
2242
|
+
// below asks whether the caller is allowed to do the thing: the procedure
|
|
2243
|
+
// raises PROVIDER_FORBIDDEN / PROVIDER_NOT_FOUND when they are not, and
|
|
2244
|
+
// that refusal is the same on every surface because it is made in one
|
|
2245
|
+
// place. See provider-store.ts.
|
|
2246
|
+
_requireProviders() {
|
|
2247
|
+
this._ensureStarted();
|
|
2248
|
+
const providers = this._catalog.providers;
|
|
2249
|
+
if (!providers)
|
|
2250
|
+
throw new Error("Provider budgets are not available on this deployment.");
|
|
2251
|
+
return providers;
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* The caller as the procedures want them: a numeric user id and a role.
|
|
2255
|
+
* The id lookup never creates a row, so an unknown caller stays `null`
|
|
2256
|
+
* rather than being minted by a read.
|
|
2257
|
+
*/
|
|
2258
|
+
async _providerActor(viewer) {
|
|
2259
|
+
const store = this._requireProviders();
|
|
2260
|
+
return {
|
|
2261
|
+
store,
|
|
2262
|
+
actor: await store.lookupUserId(viewer?.principal ?? null),
|
|
2263
|
+
isAdmin: viewer?.isAdmin === true,
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
async _auditProviderMutation(viewer, action, target, details) {
|
|
2267
|
+
await this._catalog.recordAuthzAudit({
|
|
2268
|
+
actor: viewer.principal ? {
|
|
2269
|
+
provider: viewer.principal.provider,
|
|
2270
|
+
subject: viewer.principal.subject,
|
|
2271
|
+
display: viewer.principal.email ?? viewer.principal.displayName ?? null,
|
|
2272
|
+
} : null,
|
|
2273
|
+
action,
|
|
2274
|
+
target,
|
|
2275
|
+
decision: "provider_change",
|
|
2276
|
+
reason: null,
|
|
2277
|
+
details: details ?? null,
|
|
2278
|
+
}).catch(() => { });
|
|
2279
|
+
}
|
|
2280
|
+
_providerTypeModels(typeId) {
|
|
2281
|
+
return this._modelProviders?.getModelsByProvider()
|
|
2282
|
+
.find((group) => group.providerId === typeId)?.models ?? [];
|
|
2283
|
+
}
|
|
2284
|
+
_normalizeProviderTuple(row, tuple) {
|
|
2285
|
+
const provider = String(tuple.provider || "").trim();
|
|
2286
|
+
const requested = String(tuple.model || "").trim();
|
|
2287
|
+
if (!provider || !requested) {
|
|
2288
|
+
throw new ProviderError("PROVIDER_INVALID", "A default needs both a provider and model.");
|
|
2289
|
+
}
|
|
2290
|
+
const prefix = `${provider}:`;
|
|
2291
|
+
const modelName = requested.startsWith(prefix)
|
|
2292
|
+
? requested.slice(prefix.length)
|
|
2293
|
+
: (requested.includes(":") ? "" : requested);
|
|
2294
|
+
if (!modelName) {
|
|
2295
|
+
throw new ProviderError("PROVIDER_INVALID", `The model must belong to "${provider}": write it as "${provider}:<model>".`);
|
|
2296
|
+
}
|
|
2297
|
+
const descriptor = this._providerTypeModels(row.typeId)
|
|
2298
|
+
.find((model) => model.modelName === modelName);
|
|
2299
|
+
if (this._modelProviders && !descriptor) {
|
|
2300
|
+
throw new ProviderError("PROVIDER_INVALID", `Provider type "${row.typeId}" does not offer model "${modelName}".`);
|
|
2301
|
+
}
|
|
2302
|
+
const reasoning = tuple.reasoning ?? descriptor?.defaultReasoningEffort ?? null;
|
|
2303
|
+
if (reasoning && descriptor?.supportedReasoningEfforts?.length
|
|
2304
|
+
&& !descriptor.supportedReasoningEfforts.includes(reasoning)) {
|
|
2305
|
+
throw new ProviderError("PROVIDER_INVALID", `Model ${provider}:${modelName} does not support reasoning effort '${reasoning}'.`);
|
|
2306
|
+
}
|
|
2307
|
+
const context = tuple.context ?? descriptor?.defaultContextTier ?? null;
|
|
2308
|
+
if (context && descriptor?.supportedContextTiers?.length
|
|
2309
|
+
&& !descriptor.supportedContextTiers.includes(context)) {
|
|
2310
|
+
throw new ProviderError("PROVIDER_INVALID", `Model ${provider}:${modelName} does not support context tier '${context}'.`);
|
|
2311
|
+
}
|
|
2312
|
+
return { provider, model: `${provider}:${modelName}`, reasoning, context };
|
|
2313
|
+
}
|
|
2314
|
+
async _validatedDefaultTuple(store, actor, isAdmin, tuple, scope) {
|
|
2315
|
+
if (!tuple.provider)
|
|
2316
|
+
return normalizeDefaultTuple(null);
|
|
2317
|
+
const rows = await store.listProviders(actor, isAdmin);
|
|
2318
|
+
const row = rows.find((candidate) => candidate.name === tuple.provider);
|
|
2319
|
+
if (!row)
|
|
2320
|
+
throw new ProviderError("PROVIDER_NOT_FOUND", `There is no provider named "${tuple.provider}".`);
|
|
2321
|
+
if (scope === "user" && !row.usableByMe) {
|
|
2322
|
+
throw new ProviderError("PROVIDER_NOT_FOUND", `There is no provider named "${tuple.provider}".`);
|
|
2323
|
+
}
|
|
2324
|
+
if (scope === "cluster" && row.class !== "shared") {
|
|
2325
|
+
throw new ProviderError("PROVIDER_INVALID", "The cluster default must be a shared provider.");
|
|
2326
|
+
}
|
|
2327
|
+
if (scope === "system" && !row.systemEligible) {
|
|
2328
|
+
throw new ProviderError("PROVIDER_INVALID", `Provider "${row.name}" is not enabled for system sessions.`);
|
|
2329
|
+
}
|
|
2330
|
+
const normalized = this._normalizeProviderTuple(row, tuple);
|
|
2331
|
+
const credential = scope === "system"
|
|
2332
|
+
? await store.getSystemCredential(row.name)
|
|
2333
|
+
: await store.getCredential(row.name, actor);
|
|
2334
|
+
const modelName = normalized.model.slice(normalized.model.indexOf(":") + 1);
|
|
2335
|
+
if (!credential || !this._modelProviders
|
|
2336
|
+
|| !resolveProviderCredential(this._modelProviders, credential, modelName)) {
|
|
2337
|
+
throw new ProviderError("PROVIDER_INVALID", `Provider "${row.name}" does not have a usable credential and endpoint.`);
|
|
2338
|
+
}
|
|
2339
|
+
return normalized;
|
|
2340
|
+
}
|
|
2341
|
+
_firstAvailableTuple(credentials, eligible) {
|
|
2342
|
+
if (!this._modelProviders)
|
|
2343
|
+
return null;
|
|
2344
|
+
for (const credential of credentials) {
|
|
2345
|
+
if (!eligible(credential))
|
|
2346
|
+
continue;
|
|
2347
|
+
const first = this._providerTypeModels(credential.typeId)[0];
|
|
2348
|
+
if (!first)
|
|
2349
|
+
continue;
|
|
2350
|
+
if (!resolveProviderCredential(this._modelProviders, credential, first.modelName))
|
|
2351
|
+
continue;
|
|
2352
|
+
return {
|
|
2353
|
+
provider: credential.name,
|
|
2354
|
+
model: `${credential.name}:${first.modelName}`,
|
|
2355
|
+
reasoning: first.defaultReasoningEffort ?? null,
|
|
2356
|
+
context: first.defaultContextTier ?? null,
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
return null;
|
|
2360
|
+
}
|
|
2361
|
+
_resolvedDefault(tuple, source) {
|
|
2362
|
+
if (!tuple?.provider || !tuple.model)
|
|
2363
|
+
return null;
|
|
2364
|
+
return {
|
|
2365
|
+
provider: tuple.provider,
|
|
2366
|
+
model: tuple.model,
|
|
2367
|
+
reasoningEffort: tuple.reasoning,
|
|
2368
|
+
contextTier: tuple.context,
|
|
2369
|
+
source,
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
/** Every shared provider plus the caller's own. Admins also see other people's, unusable. */
|
|
2373
|
+
async listProviders(viewer) {
|
|
2374
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2375
|
+
return { providers: await store.listProviders(actor, isAdmin) };
|
|
2376
|
+
}
|
|
2377
|
+
/** Limits, what has been spent against them, reset times, and the caller's own ceiling. */
|
|
2378
|
+
async getProviderStatus(viewer, names) {
|
|
2379
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2380
|
+
return { providers: await store.providerStatus(actor, isAdmin, names ?? null) };
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Everything the provider table draws, in one read.
|
|
2384
|
+
*
|
|
2385
|
+
* Rows come back in render order — shared providers first, then the
|
|
2386
|
+
* caller's own, each immediately followed by its model-scoped limits — so
|
|
2387
|
+
* a table draws the array as it arrives and never asks a second time per
|
|
2388
|
+
* provider.
|
|
2389
|
+
*
|
|
2390
|
+
* Every period reports a used figure whether or not a limit caps it. That
|
|
2391
|
+
* is the fact `getProviderStatus` cannot carry: it lists limits, and a
|
|
2392
|
+
* period with no limit has no limit to list.
|
|
2393
|
+
*/
|
|
2394
|
+
async getProviderUsageGrid(viewer) {
|
|
2395
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2396
|
+
return { rows: await store.usageGrid(actor, isAdmin) };
|
|
2397
|
+
}
|
|
2398
|
+
/** Create a shared provider: anyone in the cluster may spend from it. */
|
|
2399
|
+
/**
|
|
2400
|
+
* Wake every session waiting on this provider.
|
|
2401
|
+
*
|
|
2402
|
+
* The durable timer a paused session sleeps on is only the BACKSTOP. What
|
|
2403
|
+
* should actually release it is the change itself — a limit raised or
|
|
2404
|
+
* removed, an allowance widened, a hold released, a missing name created
|
|
2405
|
+
* again — and without this that promise was never kept: a session paused
|
|
2406
|
+
* on a monthly limit would sleep out the backstop no matter what an
|
|
2407
|
+
* administrator did.
|
|
2408
|
+
*
|
|
2409
|
+
* The wake is a nudge, not a decision. Each woken session re-enters the
|
|
2410
|
+
* gate on its next turn and pauses again if the reason still holds, so a
|
|
2411
|
+
* raise that is still not enough wakes nobody in the end — and waking
|
|
2412
|
+
* after a change that releases no one costs one cheap re-check, because
|
|
2413
|
+
* the gate runs before the model.
|
|
2414
|
+
*
|
|
2415
|
+
* Best-effort throughout. A failed wake leaves the backstop, which is
|
|
2416
|
+
* exactly what it is for.
|
|
2417
|
+
*/
|
|
2418
|
+
async _wakeProvidersPaused(providerName) {
|
|
2419
|
+
const store = this._catalog?.providers;
|
|
2420
|
+
if (!store || !this._duroxideClient)
|
|
2421
|
+
return;
|
|
2422
|
+
try {
|
|
2423
|
+
const sessionIds = await store.pausedFor(providerName);
|
|
2424
|
+
for (const sessionId of sessionIds) {
|
|
2425
|
+
try {
|
|
2426
|
+
// The orchestration id is DERIVED, not stored: getSession
|
|
2427
|
+
// builds this same string at its top and then returns a
|
|
2428
|
+
// view that does not carry it. Reading it back off that
|
|
2429
|
+
// view gave undefined every time, so `if (!orchId)
|
|
2430
|
+
// continue` skipped every session and this whole function
|
|
2431
|
+
// has never woken anything. Releasing a hold, raising a
|
|
2432
|
+
// limit, widening an allowance, removing a limit and
|
|
2433
|
+
// creating a provider were all silent no-ops behind the
|
|
2434
|
+
// catch below, leaving the 6-hour backstop as the only
|
|
2435
|
+
// real release.
|
|
2436
|
+
const orchId = `session-${sessionId}`;
|
|
2437
|
+
await this._catalog.updateSession(sessionId, {
|
|
2438
|
+
state: "running", waitReason: null, lastActiveAt: new Date(),
|
|
2439
|
+
}).catch(() => { });
|
|
2440
|
+
await this._duroxideClient.enqueueEvent(orchId, "messages", JSON.stringify({
|
|
2441
|
+
prompt: PROVIDER_BUDGET_WAKE_PROMPT,
|
|
2442
|
+
}));
|
|
2443
|
+
}
|
|
2444
|
+
catch {
|
|
2445
|
+
// One unreachable session must not stop the others.
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
catch {
|
|
2450
|
+
// The backstop timer still covers every one of them.
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Refuse a provider type this deployment does not describe.
|
|
2455
|
+
*
|
|
2456
|
+
* The database cannot check it: the types live in the model-providers
|
|
2457
|
+
* FILE, which SQL has never read. Without this, a typo created a
|
|
2458
|
+
* provider that listed as an ordinary usable row and could never run a
|
|
2459
|
+
* turn — buildRuntimeRegistry drops an instance whose type it does not
|
|
2460
|
+
* know, silently, so the failure surfaced only as a session that waited
|
|
2461
|
+
* for ever on a name that was right there in the table.
|
|
2462
|
+
*/
|
|
2463
|
+
_assertKnownProviderType(typeId) {
|
|
2464
|
+
const wanted = String(typeId ?? "").trim();
|
|
2465
|
+
const known = this.getModelsByProvider().map((g) => g.providerId);
|
|
2466
|
+
// No catalog at all is not evidence of a bad type — a deployment that
|
|
2467
|
+
// has not loaded one yet would refuse everything.
|
|
2468
|
+
if (known.length === 0)
|
|
2469
|
+
return;
|
|
2470
|
+
if (known.includes(wanted))
|
|
2471
|
+
return;
|
|
2472
|
+
throw new ProviderError("PROVIDER_INVALID", `No provider type named "${wanted}" on this deployment. `
|
|
2473
|
+
+ `It has ${known.slice(0, 6).join(", ")}${known.length > 6 ? ", …" : ""}.`);
|
|
2474
|
+
}
|
|
2475
|
+
async createProvider(viewer, input) {
|
|
2476
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2477
|
+
this._assertKnownProviderType(input.type);
|
|
2478
|
+
const created = await store.createProvider({
|
|
2479
|
+
name: input.name,
|
|
2480
|
+
typeId: input.type,
|
|
2481
|
+
class: "shared",
|
|
2482
|
+
secretRef: input.credentials ?? null,
|
|
2483
|
+
baseUrl: input.baseUrl ?? null,
|
|
2484
|
+
}, actor, isAdmin);
|
|
2485
|
+
await this._auditProviderMutation(viewer, "createProvider", created.name, { class: "shared", type: created.typeId });
|
|
2486
|
+
// A name that did not resolve a moment ago now does.
|
|
2487
|
+
await this._wakeProvidersPaused(input.name);
|
|
2488
|
+
return created;
|
|
2489
|
+
}
|
|
2490
|
+
/** Create a provider of the caller's own, on their own credentials. Nobody else sees it. */
|
|
2491
|
+
async createMyProvider(viewer, input) {
|
|
2492
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2493
|
+
if (actor === null) {
|
|
2494
|
+
throw new ProviderError("PROVIDER_FORBIDDEN", "Sign in to add a personal provider.");
|
|
2495
|
+
}
|
|
2496
|
+
this._assertKnownProviderType(input.type);
|
|
2497
|
+
const created = await store.createProvider({
|
|
2498
|
+
name: input.name,
|
|
2499
|
+
typeId: input.type,
|
|
2500
|
+
class: "personal",
|
|
2501
|
+
ownerUserId: actor,
|
|
2502
|
+
secretRef: input.credentials ?? null,
|
|
2503
|
+
baseUrl: input.baseUrl ?? null,
|
|
2504
|
+
}, actor, isAdmin);
|
|
2505
|
+
await this._wakeProvidersPaused(input.name);
|
|
2506
|
+
await this._auditProviderMutation(viewer, "createMyProvider", created.name, { class: "personal", type: created.typeId });
|
|
2507
|
+
return created;
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Remove a shared provider. Its sessions are not moved anywhere: they
|
|
2511
|
+
* wait on a name that no longer resolves, and the count says how many.
|
|
2512
|
+
*/
|
|
2513
|
+
async deleteProvider(viewer, name) {
|
|
2514
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2515
|
+
const result = { name, waitingSessions: await store.deleteProvider(name, actor, isAdmin) };
|
|
2516
|
+
await this._auditProviderMutation(viewer, "deleteProvider", name, { waitingSessions: result.waitingSessions });
|
|
2517
|
+
return result;
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Remove one of the caller's own providers. The same call as
|
|
2521
|
+
* `deleteProvider`: `cms_provider_delete` refuses a name the caller may
|
|
2522
|
+
* not touch, so the two differ only in which door offers them.
|
|
2523
|
+
*/
|
|
2524
|
+
async deleteMyProvider(viewer, name) {
|
|
2525
|
+
// The /me namespace holds ONLY the caller's own personal providers.
|
|
2526
|
+
// Delegating straight to deleteProvider let an admin delete a SHARED
|
|
2527
|
+
// provider through this route, and answered "forbidden" for shared
|
|
2528
|
+
// names — an existence oracle. Anything that is not the caller's own
|
|
2529
|
+
// personal provider is a name this namespace has never heard of.
|
|
2530
|
+
const { store, actor } = await this._providerActor(viewer);
|
|
2531
|
+
const rows = await store.listProviders(actor, false);
|
|
2532
|
+
const mine = rows.find((row) => row.name === name && row.class === "personal" && row.ownerUserId === actor);
|
|
2533
|
+
if (!mine) {
|
|
2534
|
+
throw new ProviderError("PROVIDER_NOT_FOUND", `there is no provider named "${name}"`);
|
|
2535
|
+
}
|
|
2536
|
+
const result = { name, waitingSessions: await store.deleteProvider(name, actor, false) };
|
|
2537
|
+
await this._auditProviderMutation(viewer, "deleteMyProvider", name, { waitingSessions: result.waitingSessions });
|
|
2538
|
+
return result;
|
|
2539
|
+
}
|
|
2540
|
+
async clearProviderRoutingDependencies(viewer, name) {
|
|
2541
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2542
|
+
const result = await store.clearRoutingDependencies(name, actor, isAdmin);
|
|
2543
|
+
await this._auditProviderMutation(viewer, "clearProviderRoutingDependencies", name, result);
|
|
2544
|
+
return { name, ...result };
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Save one limit. The same (period, scope) replaces what was there.
|
|
2548
|
+
* `seededTokens` is what this period had already spent when the limit
|
|
2549
|
+
* landed — a limit counts from the current window, it never resets it.
|
|
2550
|
+
*/
|
|
2551
|
+
async setProviderLimit(viewer, input) {
|
|
2552
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2553
|
+
// Validate here, not in Postgres: an unparsable or fractional value
|
|
2554
|
+
// used to reach the proc and come back as a raw 22P02 dressed up as
|
|
2555
|
+
// an internal server error.
|
|
2556
|
+
const tokens = Number(input.tokens);
|
|
2557
|
+
if (!Number.isSafeInteger(tokens) || tokens < 0) {
|
|
2558
|
+
throw new ProviderError("PROVIDER_INVALID", "A limit is a whole number of tokens, zero or more.");
|
|
2559
|
+
}
|
|
2560
|
+
const saved = await store.setLimit({
|
|
2561
|
+
name: input.provider,
|
|
2562
|
+
period: input.period,
|
|
2563
|
+
modelQualified: input.model ?? null,
|
|
2564
|
+
limitTokens: tokens,
|
|
2565
|
+
}, actor, isAdmin);
|
|
2566
|
+
await this._wakeProvidersPaused(input.provider);
|
|
2567
|
+
return saved;
|
|
2568
|
+
}
|
|
2569
|
+
/** Drop one limit. `removed` is false when that (period, scope) had none. */
|
|
2570
|
+
async removeProviderLimit(viewer, input) {
|
|
2571
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2572
|
+
const removed = await store.removeLimit(input.provider, input.period, input.model ?? null, actor, isAdmin);
|
|
2573
|
+
await this._wakeProvidersPaused(input.provider);
|
|
2574
|
+
return { removed };
|
|
2575
|
+
}
|
|
2576
|
+
/** The share of each of a shared provider's limits that one person may use. 100 is full. */
|
|
2577
|
+
async setProviderAllowance(viewer, input) {
|
|
2578
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2579
|
+
const allowancePct = await store.setAllowance(input.provider, Number(input.pct), actor, isAdmin);
|
|
2580
|
+
await this._wakeProvidersPaused(input.provider);
|
|
2581
|
+
return { name: input.provider, allowancePct };
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Pause new turns against a provider, independently of any limit. With
|
|
2585
|
+
* neither an end time nor a release the hold has no end, and only
|
|
2586
|
+
* another call lifts it.
|
|
2587
|
+
*/
|
|
2588
|
+
async setProviderHold(viewer, input) {
|
|
2589
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2590
|
+
const release = input.release === true;
|
|
2591
|
+
const untilUtc = release ? null : (input.untilUtc ?? null);
|
|
2592
|
+
if (untilUtc !== null) {
|
|
2593
|
+
const parsed = Date.parse(untilUtc);
|
|
2594
|
+
if (Number.isNaN(parsed)) {
|
|
2595
|
+
throw new ProviderError("PROVIDER_INVALID", `"${untilUtc}" is not a timestamp.`);
|
|
2596
|
+
}
|
|
2597
|
+
// A hold that has already ended holds nothing; accepting one and
|
|
2598
|
+
// echoing it back as set is the write-path lie the 2026-08-21
|
|
2599
|
+
// audit flagged. The Edit sheet validates too, but the API must
|
|
2600
|
+
// not trust the sheet.
|
|
2601
|
+
if (parsed <= Date.now()) {
|
|
2602
|
+
throw new ProviderError("PROVIDER_INVALID", "A hold must end in the future.");
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
const indefinite = !release && !untilUtc;
|
|
2606
|
+
await store.setHold(input.provider, { untilUtc, indefinite }, actor, isAdmin);
|
|
2607
|
+
await this._wakeProvidersPaused(input.provider);
|
|
2608
|
+
return { name: input.provider, holdUntilUtc: untilUtc, holdIndefinite: indefinite };
|
|
2609
|
+
}
|
|
2610
|
+
/** Compatibility read: configured ordinary defaults plus system routing. */
|
|
2611
|
+
async getDefaults(viewer) {
|
|
2612
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2613
|
+
return store.getDefaults(actor);
|
|
2614
|
+
}
|
|
2615
|
+
/** @deprecated Use setModelDefault({ scope: "cluster", ... }). */
|
|
2616
|
+
async setClusterDefault(viewer, tuple) {
|
|
2617
|
+
return this.setModelDefault(viewer, {
|
|
2618
|
+
scope: "cluster",
|
|
2619
|
+
provider: tuple.provider,
|
|
2620
|
+
model: tuple.model,
|
|
2621
|
+
reasoningEffort: tuple.reasoning,
|
|
2622
|
+
contextTier: tuple.context,
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
/** @deprecated Use setModelDefault({ scope: "user", ... }). */
|
|
2626
|
+
async setMyDefault(viewer, tuple) {
|
|
2627
|
+
return this.setModelDefault(viewer, {
|
|
2628
|
+
scope: "user",
|
|
2629
|
+
provider: tuple?.provider ?? null,
|
|
2630
|
+
model: tuple?.model ?? null,
|
|
2631
|
+
reasoningEffort: tuple?.reasoning,
|
|
2632
|
+
contextTier: tuple?.context,
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
async setModelDefault(viewer, input) {
|
|
2636
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2637
|
+
if (input.scope !== "user" && input.scope !== "cluster") {
|
|
2638
|
+
throw new ProviderError("PROVIDER_INVALID", "scope must be user or cluster.");
|
|
2639
|
+
}
|
|
2640
|
+
if (input.scope === "cluster" && !isAdmin) {
|
|
2641
|
+
throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can set the cluster default.");
|
|
2642
|
+
}
|
|
2643
|
+
const requested = normalizeDefaultTuple({
|
|
2644
|
+
provider: input.provider,
|
|
2645
|
+
model: input.model,
|
|
2646
|
+
reasoning: input.reasoningEffort ?? null,
|
|
2647
|
+
context: input.contextTier ?? null,
|
|
2648
|
+
});
|
|
2649
|
+
const next = await this._validatedDefaultTuple(store, actor, isAdmin, requested, input.scope);
|
|
2650
|
+
if (input.scope === "cluster")
|
|
2651
|
+
await store.setClusterDefault(next, isAdmin);
|
|
2652
|
+
else
|
|
2653
|
+
await store.setUserDefault(actor, next);
|
|
2654
|
+
await this._auditProviderMutation(viewer, "setModelDefault", next.provider, {
|
|
2655
|
+
scope: input.scope, model: next.model, cleared: next.provider === null,
|
|
2656
|
+
});
|
|
2657
|
+
return next;
|
|
2658
|
+
}
|
|
2659
|
+
async setProviderSystemUse(viewer, input) {
|
|
2660
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2661
|
+
const provider = String(input.provider || "").trim();
|
|
2662
|
+
if (!provider)
|
|
2663
|
+
throw new ProviderError("PROVIDER_INVALID", "A provider name is required.");
|
|
2664
|
+
if (input.enabled === true) {
|
|
2665
|
+
const rows = await store.listProviders(actor, isAdmin);
|
|
2666
|
+
const row = rows.find((candidate) => candidate.name === provider);
|
|
2667
|
+
if (!row || row.class !== "personal" || !row.usableByMe) {
|
|
2668
|
+
throw new ProviderError("PROVIDER_NOT_FOUND", `There is no provider named "${provider}".`);
|
|
2669
|
+
}
|
|
2670
|
+
const first = row ? this._providerTypeModels(row.typeId)[0] : null;
|
|
2671
|
+
const credential = await store.getCredential(provider, actor);
|
|
2672
|
+
if (!row || !first || !credential || !this._modelProviders
|
|
2673
|
+
|| !resolveProviderCredential(this._modelProviders, credential, first.modelName)) {
|
|
2674
|
+
throw new ProviderError("PROVIDER_INVALID", `Provider "${provider}" does not have a usable credential and endpoint.`);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
const systemUseEnabled = await store.setSystemUse(provider, input.enabled === true, actor, isAdmin);
|
|
2678
|
+
await this._auditProviderMutation(viewer, "setProviderSystemUse", provider, { enabled: systemUseEnabled });
|
|
2679
|
+
return { provider, systemUseEnabled };
|
|
2680
|
+
}
|
|
2681
|
+
async getLegacyProviderMigrationStatus(viewer) {
|
|
2682
|
+
const { store, isAdmin } = await this._providerActor(viewer);
|
|
2683
|
+
if (!isAdmin)
|
|
2684
|
+
throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can read legacy provider migration status.");
|
|
2685
|
+
return store.getLegacyKeyMigrationStatus();
|
|
2686
|
+
}
|
|
2687
|
+
async adoptLegacySystemGitHubCopilotKey(viewer, input) {
|
|
2688
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2689
|
+
if (!isAdmin)
|
|
2690
|
+
throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can adopt the legacy System key.");
|
|
2691
|
+
const adopted = await store.adoptLegacySystemGitHubKey(String(input.name || "").trim(), actor, isAdmin);
|
|
2692
|
+
await this._auditProviderMutation(viewer, "adoptLegacySystemGitHubCopilotKey", adopted.name, {
|
|
2693
|
+
class: "personal", systemUseEnabled: true,
|
|
2694
|
+
});
|
|
2695
|
+
return { provider: adopted, status: await store.getLegacyKeyMigrationStatus() };
|
|
2696
|
+
}
|
|
2697
|
+
async getModelDefaults(viewer) {
|
|
2698
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2699
|
+
const [configured, credentials, systemOverrides] = await Promise.all([
|
|
2700
|
+
store.getDefaults(actor),
|
|
2701
|
+
store.allCredentials(),
|
|
2702
|
+
store.listSystemAgentModels(),
|
|
2703
|
+
]);
|
|
2704
|
+
const clusterFallback = this._firstAvailableTuple(credentials, (credential) => credential.class === "shared");
|
|
2705
|
+
const userFallback = this._firstAvailableTuple(credentials, (credential) => credential.class === "shared" || (actor !== null && credential.ownerUserId === actor));
|
|
2706
|
+
const systemFallback = this._firstAvailableTuple(credentials, (credential) => credential.class === "shared" || credential.systemUseEnabled === true);
|
|
2707
|
+
const resolveConfigured = (candidates, eligible, fallback) => {
|
|
2708
|
+
try {
|
|
2709
|
+
if (candidates.some((candidate) => candidate.tuple.provider || candidate.tuple.model)) {
|
|
2710
|
+
const resolved = resolveRuntimeModelSelection(this._modelProviders, credentials, {
|
|
2711
|
+
defaults: candidates,
|
|
2712
|
+
eligible,
|
|
2713
|
+
});
|
|
2714
|
+
return { tuple: resolved, source: resolved.source, error: null };
|
|
2715
|
+
}
|
|
2716
|
+
return { tuple: fallback, source: "first_available", error: null };
|
|
2717
|
+
}
|
|
2718
|
+
catch (error) {
|
|
2719
|
+
return { tuple: null, source: "first_available", error: error?.message || String(error) };
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
const clusterResolution = this._modelProviders
|
|
2723
|
+
? resolveConfigured([{ tuple: configured.cluster, source: "cluster_default" }], (credential) => credential.class === "shared", clusterFallback)
|
|
2724
|
+
: { tuple: null, source: "first_available", error: "Provider type catalog is unavailable." };
|
|
2725
|
+
const userCandidates = configured.mine.provider || configured.mine.model
|
|
2726
|
+
? [{ tuple: configured.mine, source: "user_default" }]
|
|
2727
|
+
: configured.cluster.provider || configured.cluster.model
|
|
2728
|
+
? [{ tuple: configured.cluster, source: "cluster_default" }]
|
|
2729
|
+
: [];
|
|
2730
|
+
const userResolution = this._modelProviders
|
|
2731
|
+
? resolveConfigured(userCandidates, (credential) => credential.class === "shared" || (actor !== null && credential.ownerUserId === actor), userFallback)
|
|
2732
|
+
: { tuple: null, source: "first_available", error: "Provider type catalog is unavailable." };
|
|
2733
|
+
const systemResolution = this._modelProviders
|
|
2734
|
+
? resolveConfigured([{ tuple: configured.system, source: "system_default" }], (credential) => credential.class === "shared" || credential.systemUseEnabled === true, systemFallback)
|
|
2735
|
+
: { tuple: null, source: "first_available", error: "Provider type catalog is unavailable." };
|
|
2736
|
+
return {
|
|
2737
|
+
userSession: {
|
|
2738
|
+
configured: configured.mine,
|
|
2739
|
+
effective: this._resolvedDefault(userResolution.tuple, userResolution.source),
|
|
2740
|
+
error: userResolution.error,
|
|
2741
|
+
},
|
|
2742
|
+
clusterSession: {
|
|
2743
|
+
configured: configured.cluster,
|
|
2744
|
+
effective: this._resolvedDefault(clusterResolution.tuple, clusterResolution.source),
|
|
2745
|
+
error: clusterResolution.error,
|
|
2746
|
+
},
|
|
2747
|
+
system: {
|
|
2748
|
+
configured: isAdmin ? configured.system : normalizeDefaultTuple(null),
|
|
2749
|
+
effective: isAdmin
|
|
2750
|
+
? this._resolvedDefault(systemResolution.tuple, systemResolution.source)
|
|
2751
|
+
: null,
|
|
2752
|
+
error: isAdmin ? systemResolution.error : null,
|
|
2753
|
+
updatedBy: isAdmin ? configured.systemUpdatedBy : null,
|
|
2754
|
+
updatedAt: isAdmin ? configured.systemUpdatedAt : null,
|
|
2755
|
+
},
|
|
2756
|
+
systemOverrides: isAdmin ? systemOverrides : [],
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
async setSystemModelDefault(viewer, input) {
|
|
2760
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2761
|
+
if (!isAdmin)
|
|
2762
|
+
throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can set the system default.");
|
|
2763
|
+
const requested = normalizeDefaultTuple({
|
|
2764
|
+
provider: input.provider,
|
|
2765
|
+
model: input.model,
|
|
2766
|
+
reasoning: input.reasoningEffort ?? null,
|
|
2767
|
+
context: input.contextTier ?? null,
|
|
2768
|
+
});
|
|
2769
|
+
const configured = await this._validatedDefaultTuple(store, actor, isAdmin, requested, "system");
|
|
2770
|
+
const disposition = input.restartExisting
|
|
2771
|
+
? normalizeSystemRestartDisposition(input.restartExisting.disposition)
|
|
2772
|
+
: null;
|
|
2773
|
+
await store.setSystemDefault(actor, isAdmin, configured);
|
|
2774
|
+
await this._auditProviderMutation(viewer, "setSystemModelDefault", configured.provider, {
|
|
2775
|
+
model: configured.model,
|
|
2776
|
+
cleared: configured.provider === null,
|
|
2777
|
+
restartDisposition: disposition,
|
|
2778
|
+
});
|
|
2779
|
+
const defaults = await this.getModelDefaults(viewer);
|
|
2780
|
+
if (!input.restartExisting) {
|
|
2781
|
+
return { configured, effective: defaults.system.effective, restart: null };
|
|
2782
|
+
}
|
|
2783
|
+
if (!defaults.system.effective) {
|
|
2784
|
+
throw new ProviderError("PROVIDER_INVALID", "No system-eligible provider is available for restart.");
|
|
2785
|
+
}
|
|
2786
|
+
const operationId = [
|
|
2787
|
+
"system-default",
|
|
2788
|
+
disposition,
|
|
2789
|
+
defaults.system.effective.model,
|
|
2790
|
+
defaults.system.effective.reasoningEffort ?? "",
|
|
2791
|
+
defaults.system.effective.contextTier ?? "",
|
|
2792
|
+
].join(":");
|
|
2793
|
+
const overridden = new Set(defaults.systemOverrides.map((override) => override.agentId));
|
|
2794
|
+
const candidatePlans = this._getSystemAgentPlans().filter((plan) => !overridden.has(plan.agent.id));
|
|
2795
|
+
const plans = [];
|
|
2796
|
+
for (const plan of candidatePlans) {
|
|
2797
|
+
const row = await this._catalog.getSession(plan.sessionId).catch(() => null);
|
|
2798
|
+
const rollout = await store.getSystemRestart(plan.agent.id);
|
|
2799
|
+
const retryingSameOperation = rollout?.operationId === operationId
|
|
2800
|
+
&& rollout.status !== "complete";
|
|
2801
|
+
const live = Boolean(row && !row.deletedAt
|
|
2802
|
+
&& !["completed", "failed", "error", "cancelled"].includes(row.state));
|
|
2803
|
+
if (!retryingSameOperation && !live)
|
|
2804
|
+
continue;
|
|
2805
|
+
if (!retryingSameOperation && row.orchestrationId && row.state !== "pending"
|
|
2806
|
+
&& row.model === defaults.system.effective.model
|
|
2807
|
+
&& (row.reasoningEffort ?? null) === (defaults.system.effective.reasoningEffort ?? null)
|
|
2808
|
+
&& (row.contextTier ?? null) === (defaults.system.effective.contextTier ?? null))
|
|
2809
|
+
continue;
|
|
2810
|
+
plans.push(plan);
|
|
2811
|
+
}
|
|
2812
|
+
const failures = [];
|
|
2813
|
+
let restarted = 0;
|
|
2814
|
+
for (const plan of plans) {
|
|
2815
|
+
try {
|
|
2816
|
+
const outcome = await this.restartSystemSession(plan.agent.id, {
|
|
2817
|
+
disposition: disposition,
|
|
2818
|
+
model: defaults.system.effective.model,
|
|
2819
|
+
reasoningEffort: defaults.system.effective.reasoningEffort,
|
|
2820
|
+
contextTier: defaults.system.effective.contextTier,
|
|
2821
|
+
modelResolutionSource: defaults.system.effective.source,
|
|
2822
|
+
operationId,
|
|
2823
|
+
reason: "Applying the configured system model default",
|
|
2824
|
+
});
|
|
2825
|
+
if (!outcome.skippedReason)
|
|
2826
|
+
restarted += 1;
|
|
2827
|
+
}
|
|
2828
|
+
catch (error) {
|
|
2829
|
+
failures.push({ agentId: plan.agent.id, error: error?.message || String(error) });
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
return {
|
|
2833
|
+
configured,
|
|
2834
|
+
effective: defaults.system.effective,
|
|
2835
|
+
restart: { requested: true, disposition: disposition, affected: plans.length, restarted, failures },
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
async setSystemSessionModel(viewer, input) {
|
|
2839
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2840
|
+
if (!isAdmin)
|
|
2841
|
+
throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can set a system-agent model.");
|
|
2842
|
+
const plan = this._resolveSystemAgentPlan(input.agentId);
|
|
2843
|
+
const next = await this._validatedDefaultTuple(store, actor, isAdmin, normalizeDefaultTuple({
|
|
2844
|
+
provider: input.provider,
|
|
2845
|
+
model: input.model,
|
|
2846
|
+
reasoning: input.reasoningEffort ?? null,
|
|
2847
|
+
context: input.contextTier ?? null,
|
|
2848
|
+
}), "system");
|
|
2849
|
+
await store.setSystemAgentModel(plan.agent.id, actor, isAdmin, next);
|
|
2850
|
+
await this._auditProviderMutation(viewer, "setSystemSessionModel", plan.agent.id, {
|
|
2851
|
+
provider: next.provider, model: next.model,
|
|
2852
|
+
});
|
|
2853
|
+
const row = await this._catalog.getSession(plan.sessionId).catch(() => null);
|
|
2854
|
+
if (row && !row.deletedAt && row.orchestrationId) {
|
|
2855
|
+
await this.sendCommand(plan.sessionId, {
|
|
2856
|
+
cmd: "set_model",
|
|
2857
|
+
id: buildLifecycleCommandId("set-system-model"),
|
|
2858
|
+
args: {
|
|
2859
|
+
model: next.model,
|
|
2860
|
+
reasoningEffort: next.reasoning,
|
|
2861
|
+
contextTier: next.context,
|
|
2862
|
+
source: "system_default",
|
|
2863
|
+
},
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
return (await store.listSystemAgentModels()).find((override) => override.agentId === plan.agent.id);
|
|
2867
|
+
}
|
|
2868
|
+
async clearSystemSessionModel(viewer, agentId) {
|
|
2869
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2870
|
+
if (!isAdmin)
|
|
2871
|
+
throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can clear a system-agent model.");
|
|
2872
|
+
const plan = this._resolveSystemAgentPlan(agentId);
|
|
2873
|
+
const cleared = await store.clearSystemAgentModel(plan.agent.id, actor, isAdmin);
|
|
2874
|
+
if (cleared)
|
|
2875
|
+
await this._auditProviderMutation(viewer, "clearSystemSessionModel", plan.agent.id, { cleared: true });
|
|
2876
|
+
if (cleared) {
|
|
2877
|
+
const defaults = await this.getModelDefaults(viewer);
|
|
2878
|
+
const row = await this._catalog.getSession(plan.sessionId).catch(() => null);
|
|
2879
|
+
if (row && !row.deletedAt && row.orchestrationId && defaults.system.effective) {
|
|
2880
|
+
await this.sendCommand(plan.sessionId, {
|
|
2881
|
+
cmd: "set_model",
|
|
2882
|
+
id: buildLifecycleCommandId("clear-system-model"),
|
|
2883
|
+
args: {
|
|
2884
|
+
model: defaults.system.effective.model,
|
|
2885
|
+
reasoningEffort: defaults.system.effective.reasoningEffort,
|
|
2886
|
+
contextTier: defaults.system.effective.contextTier,
|
|
2887
|
+
source: "system_default",
|
|
2888
|
+
},
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
return { agentId: plan.agent.id, cleared };
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Where the tokens went: totals, the daily chart, and one breakdown, over
|
|
2896
|
+
* the same filters. Three reads because they group differently, one
|
|
2897
|
+
* answer because a report that shows a total and a chart from different
|
|
2898
|
+
* filters is a report nobody can act on.
|
|
2899
|
+
*
|
|
2900
|
+
* The breakdown asks for one row more than it returns, so `truncated`
|
|
2901
|
+
* reports the cut rather than guessing at it.
|
|
2902
|
+
*/
|
|
2903
|
+
async getProviderUsage(viewer, query = {}) {
|
|
2904
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2905
|
+
const dimension = PROVIDER_USAGE_DIMENSIONS.includes(query.dimension)
|
|
2906
|
+
? query.dimension
|
|
2907
|
+
: "provider";
|
|
2908
|
+
const limit = clampInteger(query.limit ?? undefined, 40, 1, 200);
|
|
2909
|
+
const filters = {
|
|
2910
|
+
days: clampInteger(query.days ?? undefined, 7, 1, 365),
|
|
2911
|
+
// `mine` beats a supplied id: it is the one form that cannot name
|
|
2912
|
+
// anybody but the caller.
|
|
2913
|
+
ownerUserId: query.mine === true ? actor : (query.ownerUserId ?? null),
|
|
2914
|
+
provider: query.provider ?? null,
|
|
2915
|
+
model: query.model ?? null,
|
|
2916
|
+
sessionId: query.sessionId ?? null,
|
|
2917
|
+
chargeClass: query.chargeClass ?? null,
|
|
2918
|
+
};
|
|
2919
|
+
const [totals, daily, breakdown] = await Promise.all([
|
|
2920
|
+
store.usageTotals(actor, isAdmin, filters),
|
|
2921
|
+
store.usageDaily(actor, isAdmin, filters),
|
|
2922
|
+
store.usageBreakdown(actor, isAdmin, dimension, filters, limit + 1),
|
|
2923
|
+
]);
|
|
2924
|
+
return {
|
|
2925
|
+
totals,
|
|
2926
|
+
daily,
|
|
2927
|
+
breakdown: breakdown.slice(0, limit),
|
|
2928
|
+
dimension,
|
|
2929
|
+
truncated: breakdown.length > limit,
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
/** Sessions waiting on a limit right now, with what is holding each one. */
|
|
2933
|
+
async listPausedSessions(viewer) {
|
|
2934
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
2935
|
+
return { sessions: await store.listPaused(actor, isAdmin) };
|
|
2936
|
+
}
|
|
2937
|
+
// ─── Agent packages / worker registry ────────────────────
|
|
2938
|
+
_agentPackagePrincipal(owner, isAdmin) {
|
|
2939
|
+
if (owner?.provider && owner?.subject) {
|
|
2940
|
+
return { provider: owner.provider, subject: owner.subject };
|
|
2941
|
+
}
|
|
2942
|
+
if (isAdmin)
|
|
2943
|
+
return null;
|
|
2944
|
+
const error = new Error("agent-package operations require an authenticated principal");
|
|
2945
|
+
error.code = "FORBIDDEN";
|
|
2946
|
+
throw error;
|
|
2947
|
+
}
|
|
2948
|
+
_agentPackageSelector(selector, isAdmin = false) {
|
|
2949
|
+
if (!selector || typeof selector !== "object")
|
|
2950
|
+
return null;
|
|
2951
|
+
const scope = selector.scope === "shared" || selector.scope === "user" ? selector.scope : null;
|
|
2952
|
+
const provider = String(selector.ownerProvider || selector.owner?.provider || "").trim();
|
|
2953
|
+
const subject = String(selector.ownerSubject || selector.owner?.subject || "").trim();
|
|
2954
|
+
const owner = isAdmin && provider && subject ? { provider, subject } : null;
|
|
2955
|
+
if (!scope && !owner)
|
|
2956
|
+
return null;
|
|
2957
|
+
return { ...(scope ? { scope } : {}), ...(owner ? { owner } : {}) };
|
|
2958
|
+
}
|
|
2959
|
+
async _mapAgentPackageErrors(run) {
|
|
2960
|
+
try {
|
|
2961
|
+
return await run();
|
|
2962
|
+
}
|
|
2963
|
+
catch (error) {
|
|
2964
|
+
const message = String(error?.message || "");
|
|
2965
|
+
if (/AGENT_(?:PACKAGE|SOURCE)_[A-Z_]*FORBIDDEN/.test(message))
|
|
2966
|
+
error.code = "FORBIDDEN";
|
|
2967
|
+
else if (/AGENT_(?:PACKAGE|SOURCE)_[A-Z_]*NOT_FOUND/.test(message))
|
|
2968
|
+
error.code = "NOT_FOUND";
|
|
2969
|
+
else if (/AGENT_PACKAGE_(?:SEMVER_CONFLICT|SCOPE_MISMATCH|NAME_TAKEN|AMBIGUOUS)/.test(message))
|
|
2970
|
+
error.code = "CONFLICT";
|
|
2971
|
+
else if (/AGENT_PACKAGE_(?:BAD_(?:SCOPE|NAME)|NO_OWNER|RESERVED_NAME)/.test(message))
|
|
2972
|
+
error.code = "INVALID_REQUEST";
|
|
2973
|
+
throw error;
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
_requireAgentPackageArtifacts() {
|
|
2977
|
+
if (!this._artifactStore) {
|
|
2978
|
+
throw new Error("agent-package artifact operations require an artifact store in direct mode");
|
|
2979
|
+
}
|
|
2980
|
+
return this._artifactStore;
|
|
2981
|
+
}
|
|
2982
|
+
async listAgentPackages(owner, isAdmin) {
|
|
2983
|
+
this._ensureStarted();
|
|
2984
|
+
return this._catalog.listAgentPackages(this._agentPackagePrincipal(owner, isAdmin), isAdmin);
|
|
2985
|
+
}
|
|
2986
|
+
async getAgentPackage(name, owner, isAdmin, selector) {
|
|
2987
|
+
this._ensureStarted();
|
|
2988
|
+
return this._catalog.getAgentPackage(name, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin));
|
|
2989
|
+
}
|
|
2990
|
+
async listAgentWorkerState() {
|
|
2991
|
+
this._ensureStarted();
|
|
2992
|
+
return this._catalog.listAgentWorkerState();
|
|
2993
|
+
}
|
|
2994
|
+
async listWorkers() {
|
|
2995
|
+
this._ensureStarted();
|
|
2996
|
+
return this._catalog.listWorkers();
|
|
2997
|
+
}
|
|
2998
|
+
async setAgentPackageScope(name, scope, owner, isAdmin, selector) {
|
|
2999
|
+
this._ensureStarted();
|
|
3000
|
+
const targetScope = scope === "shared" ? "shared" : "user";
|
|
3001
|
+
const principal = this._agentPackagePrincipal(owner, isAdmin);
|
|
3002
|
+
const ownerOverride = this._agentPackageSelector(selector, isAdmin)?.owner ?? null;
|
|
3003
|
+
let sourceSelector = null;
|
|
3004
|
+
if (targetScope === "user")
|
|
3005
|
+
sourceSelector = { scope: "shared", ...(ownerOverride ? { owner: ownerOverride } : {}) };
|
|
3006
|
+
else if (ownerOverride)
|
|
3007
|
+
sourceSelector = { scope: "user", owner: ownerOverride };
|
|
3008
|
+
await this._mapAgentPackageErrors(() => this._catalog.setAgentPackageScope(name, targetScope, principal, isAdmin, sourceSelector));
|
|
3009
|
+
return { ok: true };
|
|
3010
|
+
}
|
|
3011
|
+
async setAgentPackageEnabled(name, enabled, owner, isAdmin, selector) {
|
|
3012
|
+
this._ensureStarted();
|
|
3013
|
+
await this._mapAgentPackageErrors(() => this._catalog.setAgentPackageEnabled(name, Boolean(enabled), this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin)));
|
|
3014
|
+
return { ok: true };
|
|
3015
|
+
}
|
|
3016
|
+
async pinAgentPackageVersion(name, semver, owner, isAdmin, selector) {
|
|
3017
|
+
this._ensureStarted();
|
|
3018
|
+
await this._mapAgentPackageErrors(() => this._catalog.pinAgentPackageVersion(name, semver, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin)));
|
|
3019
|
+
return { ok: true };
|
|
3020
|
+
}
|
|
3021
|
+
async deleteAgentPackage(name, owner, isAdmin, selector) {
|
|
3022
|
+
this._ensureStarted();
|
|
3023
|
+
await this._mapAgentPackageErrors(() => deleteAgentPackageEverywhere({ catalog: this._catalog, artifactStore: this._requireAgentPackageArtifacts() }, name, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin)));
|
|
3024
|
+
return { ok: true };
|
|
3025
|
+
}
|
|
3026
|
+
async publishAgentPackageDirectory(dir, scope, owner, isAdmin, opts = {}) {
|
|
3027
|
+
this._ensureStarted();
|
|
3028
|
+
return this._mapAgentPackageErrors(() => publishAgentPackageDir({ catalog: this._catalog, artifactStore: this._requireAgentPackageArtifacts() }, {
|
|
3029
|
+
dir,
|
|
3030
|
+
scope,
|
|
3031
|
+
owner: this._agentPackagePrincipal(owner, isAdmin),
|
|
3032
|
+
createdBy: opts.createdBy ?? null,
|
|
3033
|
+
isAdmin,
|
|
3034
|
+
...(opts.reservedAgentNames?.length ? { validate: { reservedAgentNames: opts.reservedAgentNames } } : {}),
|
|
3035
|
+
}));
|
|
3036
|
+
}
|
|
3037
|
+
async uploadAgentPackage(files, scope, owner, isAdmin, opts = {}) {
|
|
3038
|
+
const invalid = (message) => Object.assign(new Error(message), { code: "INVALID_REQUEST" });
|
|
3039
|
+
const tooLarge = (message) => Object.assign(new Error(message), { code: "PAYLOAD_TOO_LARGE" });
|
|
3040
|
+
if (!Array.isArray(files) || files.length === 0)
|
|
3041
|
+
throw invalid("upload requires files: [{ path, contentBase64 }]");
|
|
3042
|
+
if (files.length > 2_000)
|
|
3043
|
+
throw tooLarge("upload exceeds 2000 files");
|
|
3044
|
+
const normalizedPaths = files.map((file) => String(file?.path || ""));
|
|
3045
|
+
const pathSet = new Set();
|
|
3046
|
+
for (const rel of normalizedPaths) {
|
|
3047
|
+
if (pathSet.has(rel))
|
|
3048
|
+
throw invalid(`upload contains duplicate file path: ${rel}`);
|
|
3049
|
+
const parts = rel.split("/");
|
|
3050
|
+
for (let index = 1; index < parts.length; index++) {
|
|
3051
|
+
const parent = parts.slice(0, index).join("/");
|
|
3052
|
+
if (pathSet.has(parent))
|
|
3053
|
+
throw invalid(`upload path conflicts with file path: ${parent} and ${rel}`);
|
|
3054
|
+
}
|
|
3055
|
+
for (const existing of pathSet) {
|
|
3056
|
+
if (existing.startsWith(`${rel}/`))
|
|
3057
|
+
throw invalid(`upload path conflicts with file path: ${rel} and ${existing}`);
|
|
3058
|
+
}
|
|
3059
|
+
pathSet.add(rel);
|
|
3060
|
+
}
|
|
3061
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ps-agent-upload-"));
|
|
3062
|
+
try {
|
|
3063
|
+
let total = 0;
|
|
3064
|
+
for (const file of files) {
|
|
3065
|
+
const rel = String(file?.path || "");
|
|
3066
|
+
if (!rel || rel.startsWith("/") || rel.includes("\\") || rel.split("/").some((part) => part === "..")) {
|
|
3067
|
+
throw invalid(`upload file path is not a safe relative path: ${rel}`);
|
|
3068
|
+
}
|
|
3069
|
+
const encoded = String(file?.contentBase64 || "");
|
|
3070
|
+
const remaining = 2 * 1024 * 1024 - total;
|
|
3071
|
+
if (encoded.length > Math.ceil(remaining * 4 / 3) + 4)
|
|
3072
|
+
throw tooLarge("upload exceeds the 2 MB envelope");
|
|
3073
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
3074
|
+
total += bytes.length;
|
|
3075
|
+
if (total > 2 * 1024 * 1024)
|
|
3076
|
+
throw tooLarge("upload exceeds the 2 MB envelope");
|
|
3077
|
+
const target = path.resolve(tmp, rel);
|
|
3078
|
+
if (target === tmp || !target.startsWith(tmp + path.sep))
|
|
3079
|
+
throw invalid(`upload file path escapes the package root: ${rel}`);
|
|
3080
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
3081
|
+
fs.writeFileSync(target, bytes);
|
|
3082
|
+
}
|
|
3083
|
+
return await this.publishAgentPackageDirectory(tmp, scope, owner, isAdmin, opts);
|
|
3084
|
+
}
|
|
3085
|
+
catch (error) {
|
|
3086
|
+
if (error instanceof AgentPackageValidationError) {
|
|
3087
|
+
const wrapped = new Error(error.message);
|
|
3088
|
+
wrapped.code = "VALIDATION_FAILED";
|
|
3089
|
+
wrapped.validation = error.validation;
|
|
3090
|
+
throw wrapped;
|
|
3091
|
+
}
|
|
3092
|
+
throw error;
|
|
3093
|
+
}
|
|
3094
|
+
finally {
|
|
3095
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
async _agentPackageVersion(name, semver, owner, isAdmin, selector) {
|
|
3099
|
+
this._ensureStarted();
|
|
3100
|
+
return resolveAgentPackageVersion(this._catalog, name, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin), semver);
|
|
3101
|
+
}
|
|
3102
|
+
async getAgentPackageTree(name, semver, owner, isAdmin, selector) {
|
|
3103
|
+
this._ensureStarted();
|
|
3104
|
+
const { version, entries } = await readAgentPackageVersionEntries({ catalog: this._catalog, artifactStore: this._requireAgentPackageArtifacts() }, name, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin), semver);
|
|
3105
|
+
const dirs = new Set();
|
|
3106
|
+
const files = entries.map((entry) => {
|
|
3107
|
+
const parts = entry.name.split("/");
|
|
3108
|
+
for (let index = 1; index < parts.length; index++)
|
|
3109
|
+
dirs.add(parts.slice(0, index).join("/"));
|
|
3110
|
+
return { path: entry.name, size: entry.body.length };
|
|
3111
|
+
}).sort((left, right) => left.path.localeCompare(right.path));
|
|
3112
|
+
return { name, semver: version.semver, sha256: version.sha256, dirs: [...dirs].sort(), files };
|
|
3113
|
+
}
|
|
3114
|
+
async getAgentPackageFile(name, semver, filePath, owner, isAdmin, selector) {
|
|
3115
|
+
this._ensureStarted();
|
|
3116
|
+
const { version, entries } = await readAgentPackageVersionEntries({ catalog: this._catalog, artifactStore: this._requireAgentPackageArtifacts() }, name, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin), semver);
|
|
3117
|
+
const entry = entries.find((candidate) => candidate.name === filePath);
|
|
3118
|
+
if (!entry)
|
|
3119
|
+
throw Object.assign(new Error(`"${filePath}" is not in ${name}@${version.semver}`), { code: "NOT_FOUND" });
|
|
3120
|
+
const binary = entry.body.includes(0);
|
|
3121
|
+
let body = entry.body.subarray(0, 256 * 1024);
|
|
3122
|
+
let text = "";
|
|
3123
|
+
if (!binary) {
|
|
3124
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
3125
|
+
for (let trim = 0; trim <= 4; trim++) {
|
|
3126
|
+
try {
|
|
3127
|
+
text = decoder.decode(trim ? body.subarray(0, body.length - trim) : body);
|
|
3128
|
+
if (trim)
|
|
3129
|
+
body = body.subarray(0, body.length - trim);
|
|
3130
|
+
break;
|
|
3131
|
+
}
|
|
3132
|
+
catch {
|
|
3133
|
+
if (trim === 4)
|
|
3134
|
+
text = body.toString("utf8");
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
return {
|
|
3139
|
+
name, semver: version.semver, path: filePath, size: entry.body.length,
|
|
3140
|
+
truncated: entry.body.length > body.length,
|
|
3141
|
+
binary,
|
|
3142
|
+
encoding: binary ? "base64" : "utf8",
|
|
3143
|
+
content: binary ? body.toString("base64") : text,
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
async downloadAgentPackage(name, semver, owner, isAdmin, selector) {
|
|
3147
|
+
this._ensureStarted();
|
|
3148
|
+
const { version } = await this._agentPackageVersion(name, semver, owner, isAdmin, selector);
|
|
3149
|
+
const body = await fetchAgentPackageTarGz(this._requireAgentPackageArtifacts(), version.artifactFilename, version.sha256);
|
|
3150
|
+
return {
|
|
3151
|
+
name,
|
|
3152
|
+
semver: version.semver,
|
|
3153
|
+
sha256: version.sha256,
|
|
3154
|
+
filename: `${name}@${version.semver}.tgz`,
|
|
3155
|
+
contentType: "application/gzip",
|
|
3156
|
+
body,
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
async republishAgentPackageVersion(name, semver, targetScope, owner, isAdmin, opts = {}) {
|
|
3160
|
+
this._ensureStarted();
|
|
3161
|
+
const principal = this._agentPackagePrincipal(owner, isAdmin);
|
|
3162
|
+
const target = targetScope === "shared" ? "shared" : "user";
|
|
3163
|
+
const sourceScope = target === "shared" ? "user" : "shared";
|
|
3164
|
+
return this._mapAgentPackageErrors(async () => {
|
|
3165
|
+
const selectedOwner = this._agentPackageSelector(opts.selector, isAdmin)?.owner ?? null;
|
|
3166
|
+
const { detail, version } = await this._agentPackageVersion(name, semver, principal, isAdmin, {
|
|
3167
|
+
scope: sourceScope,
|
|
3168
|
+
...(selectedOwner ? { owner: selectedOwner } : {}),
|
|
3169
|
+
});
|
|
3170
|
+
const targz = await fetchAgentPackageTarGz(this._requireAgentPackageArtifacts(), version.artifactFilename, version.sha256);
|
|
3171
|
+
const outcome = await publishPackedAgentPackage({ catalog: this._catalog, artifactStore: this._requireAgentPackageArtifacts() }, {
|
|
3172
|
+
manifest: version.manifest,
|
|
3173
|
+
targz,
|
|
3174
|
+
sha256: version.sha256,
|
|
3175
|
+
scope: target,
|
|
3176
|
+
owner: principal,
|
|
3177
|
+
createdBy: `republish of ${sourceScope} ${name}@${version.semver}`
|
|
3178
|
+
+ (opts.createdBy ? ` by ${opts.createdBy}` : ""),
|
|
3179
|
+
isAdmin,
|
|
3180
|
+
sourceId: detail.sourceId ?? null,
|
|
3181
|
+
commitSha: version.commitSha ?? null,
|
|
3182
|
+
});
|
|
3183
|
+
const { manifest: _manifest, ...summary } = outcome;
|
|
3184
|
+
return summary;
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
2038
3187
|
// ─── Models ──────────────────────────────────────────────
|
|
2039
3188
|
/**
|
|
2040
3189
|
* List all available models across all configured providers.
|
|
@@ -2043,6 +3192,7 @@ export class PilotSwarmManagementClient {
|
|
|
2043
3192
|
if (!this._modelProviders)
|
|
2044
3193
|
return [];
|
|
2045
3194
|
return this._modelProviders.allModels.map((m) => ({
|
|
3195
|
+
catalogKind: "provider_type",
|
|
2046
3196
|
qualifiedName: m.qualifiedName,
|
|
2047
3197
|
providerId: m.providerId,
|
|
2048
3198
|
providerType: m.providerType,
|
|
@@ -2057,6 +3207,31 @@ export class PilotSwarmManagementClient {
|
|
|
2057
3207
|
credentialAvailable: this.getModelCredentialStatus(m.qualifiedName).credentialAvailable,
|
|
2058
3208
|
}));
|
|
2059
3209
|
}
|
|
3210
|
+
async listRuntimeModels(viewer) {
|
|
3211
|
+
const typeModels = this.listModels();
|
|
3212
|
+
const { store, actor, isAdmin } = await this._providerActor(viewer);
|
|
3213
|
+
const [providers, credentials] = await Promise.all([
|
|
3214
|
+
store.listProviders(actor, isAdmin),
|
|
3215
|
+
store.allCredentials(),
|
|
3216
|
+
]);
|
|
3217
|
+
const credentialByName = new Map(credentials.map((credential) => [credential.name, credential]));
|
|
3218
|
+
const out = [];
|
|
3219
|
+
for (const provider of providers.filter((candidate) => candidate.usableByMe)) {
|
|
3220
|
+
const credential = credentialByName.get(provider.name);
|
|
3221
|
+
for (const model of typeModels.filter((candidate) => candidate.providerId === provider.typeId)) {
|
|
3222
|
+
const credentialAvailable = Boolean(credential && this._modelProviders
|
|
3223
|
+
&& resolveProviderCredential(this._modelProviders, credential, model.modelName));
|
|
3224
|
+
out.push({
|
|
3225
|
+
...model,
|
|
3226
|
+
catalogKind: "runtime_provider",
|
|
3227
|
+
providerId: provider.name,
|
|
3228
|
+
qualifiedName: `${provider.name}:${model.modelName}`,
|
|
3229
|
+
credentialAvailable,
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
return out;
|
|
3234
|
+
}
|
|
2060
3235
|
/**
|
|
2061
3236
|
* Get models grouped by provider for display.
|
|
2062
3237
|
*/
|
|
@@ -2064,6 +3239,7 @@ export class PilotSwarmManagementClient {
|
|
|
2064
3239
|
if (!this._modelProviders)
|
|
2065
3240
|
return [];
|
|
2066
3241
|
return this._modelProviders.getModelsByProvider().map(g => ({
|
|
3242
|
+
catalogKind: "provider_type",
|
|
2067
3243
|
providerId: g.providerId,
|
|
2068
3244
|
type: g.type,
|
|
2069
3245
|
models: g.models.map((m) => ({
|