@desplega.ai/agent-swarm 1.93.0 → 1.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +2 -2
  2. package/openapi.json +180 -1
  3. package/package.json +4 -3
  4. package/src/be/db.ts +74 -9
  5. package/src/be/migrations/090_model_tiers.sql +2 -0
  6. package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
  7. package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
  8. package/src/be/migrations/093_slack_message_tracking.sql +6 -0
  9. package/src/be/migrations/094_mcp_extra_authorize_params.sql +4 -0
  10. package/src/be/migrations/runner.ts +52 -0
  11. package/src/be/modelsdev-cache.json +2060 -198
  12. package/src/be/scripts/boot-reembed.ts +74 -0
  13. package/src/be/scripts/db.ts +19 -3
  14. package/src/be/seed/index.ts +1 -1
  15. package/src/be/seed/registry.ts +2 -2
  16. package/src/be/seed/runner.ts +5 -5
  17. package/src/be/seed/types.ts +6 -1
  18. package/src/be/seed-pricing.ts +1 -0
  19. package/src/be/seed-scripts/index.ts +3 -2
  20. package/src/be/skill-sync.ts +4 -4
  21. package/src/be/swarm-config-guard.ts +8 -0
  22. package/src/commands/provider-credentials.ts +14 -8
  23. package/src/commands/runner.ts +84 -13
  24. package/src/http/index.ts +13 -2
  25. package/src/http/mcp-oauth.ts +14 -0
  26. package/src/http/metrics.ts +55 -6
  27. package/src/http/schedules.ts +16 -15
  28. package/src/http/script-runs.ts +7 -1
  29. package/src/http/scripts.ts +147 -1
  30. package/src/http/tasks.ts +7 -0
  31. package/src/model-tiers.ts +140 -0
  32. package/src/oauth/mcp-wrapper.ts +14 -0
  33. package/src/providers/claude-managed-models.ts +9 -0
  34. package/src/providers/codex-skill-resolver.ts +22 -8
  35. package/src/providers/opencode-adapter.ts +21 -2
  36. package/src/providers/pi-mono-adapter.ts +143 -26
  37. package/src/providers/types.ts +12 -0
  38. package/src/scheduler/scheduler.ts +22 -34
  39. package/src/server-user.ts +8 -2
  40. package/src/slack/responses.ts +39 -11
  41. package/src/slack/watcher.ts +121 -8
  42. package/src/tests/agents-list-model-display.test.ts +13 -0
  43. package/src/tests/aws-error-classifier.test.ts +148 -0
  44. package/src/tests/claude-managed-adapter.test.ts +12 -0
  45. package/src/tests/context-window.test.ts +7 -0
  46. package/src/tests/credential-check.test.ts +185 -46
  47. package/src/tests/harness-provider-resolution.test.ts +23 -0
  48. package/src/tests/http-api-integration.test.ts +19 -0
  49. package/src/tests/mcp-oauth-queries.test.ts +71 -1
  50. package/src/tests/mcp-oauth-wrapper.test.ts +109 -0
  51. package/src/tests/metrics-http.test.ts +137 -3
  52. package/src/tests/migration-046-budgets.test.ts +33 -0
  53. package/src/tests/migration-runner-regressions.test.ts +69 -0
  54. package/src/tests/model-control.test.ts +162 -46
  55. package/src/tests/opencode-adapter.test.ts +38 -1
  56. package/src/tests/pi-mono-adapter.test.ts +319 -0
  57. package/src/tests/provider-command-format.test.ts +12 -0
  58. package/src/tests/providers/pi-cost.test.ts +9 -0
  59. package/src/tests/runner-fallback-output.test.ts +50 -0
  60. package/src/tests/scripts-boot-reembed.test.ts +163 -0
  61. package/src/tests/scripts-embeddings.test.ts +90 -0
  62. package/src/tests/seed.test.ts +26 -1
  63. package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
  64. package/src/tests/skill-fs-writer.test.ts +7 -1
  65. package/src/tests/skill-sync.test.ts +15 -3
  66. package/src/tests/slack-watcher.test.ts +66 -0
  67. package/src/tests/workflow-agent-task.test.ts +5 -2
  68. package/src/tests/workflow-validation-port-routing.test.ts +181 -0
  69. package/src/tools/mcp-servers/mcp-server-create.ts +7 -0
  70. package/src/tools/mcp-servers/mcp-server-update.ts +8 -0
  71. package/src/tools/memory-get.ts +11 -0
  72. package/src/tools/memory-search.ts +18 -0
  73. package/src/tools/schedules/create-schedule.ts +71 -70
  74. package/src/tools/schedules/update-schedule.ts +43 -31
  75. package/src/tools/send-task.ts +16 -5
  76. package/src/tools/task-action.ts +11 -3
  77. package/src/types.ts +30 -0
  78. package/src/utils/aws-error-classifier.ts +97 -0
  79. package/src/utils/context-window.ts +2 -0
  80. package/src/utils/credentials.test.ts +68 -0
  81. package/src/utils/credentials.ts +44 -3
  82. package/src/utils/pretty-print.ts +25 -10
  83. package/src/utils/skill-fs-writer.ts +11 -3
  84. package/src/workflows/engine.ts +3 -2
  85. package/src/workflows/executors/agent-task.ts +3 -1
@@ -48,12 +48,48 @@ function slugify(input: string): string {
48
48
 
49
49
  function validateMetricDefinition(definition: unknown) {
50
50
  const parsed = MetricDefinitionSchema.parse(definition);
51
+ for (const variable of parsed.variables ?? []) {
52
+ if (variable.optionsQuery) {
53
+ assertSelectOnlyQuery(variable.optionsQuery.sql);
54
+ }
55
+ }
51
56
  for (const widget of parsed.widgets) {
52
57
  assertSelectOnlyQuery(widget.query.sql);
53
58
  }
54
59
  return parsed;
55
60
  }
56
61
 
62
+ function resolveVariableOptionValues(variable: MetricVariable) {
63
+ if (!variable.optionsQuery) return variable.options ?? [];
64
+ assertSelectOnlyQuery(variable.optionsQuery.sql);
65
+ const result = executeReadOnlyQuery(variable.optionsQuery.sql, [], HARD_METRIC_MAX_ROWS);
66
+ return result.rows.map((row) => {
67
+ const record = Object.fromEntries(
68
+ result.columns.map((column, index) => [column, row[index] as MetricParam]),
69
+ );
70
+ const value = record[variable.optionsQuery!.valueKey];
71
+ const labelKey = variable.optionsQuery!.labelKey ?? variable.optionsQuery!.valueKey;
72
+ const label = record[labelKey] ?? value;
73
+ return {
74
+ label: label == null ? "" : String(label),
75
+ value: value == null ? null : value,
76
+ };
77
+ });
78
+ }
79
+
80
+ function resolveVariableOptions(metric: Metric) {
81
+ const optionsByKey: Record<string, Array<{ label: string; value: MetricParam }>> = {};
82
+ const variables = (metric.definition.variables ?? []).map((variable) => {
83
+ if (!variable.optionsQuery) {
84
+ return variable;
85
+ }
86
+ const options = resolveVariableOptionValues(variable);
87
+ optionsByKey[variable.key] = options;
88
+ return { ...variable, options };
89
+ });
90
+ return { variables, optionsByKey };
91
+ }
92
+
57
93
  function coerceVariableValue(variable: MetricVariable, raw: unknown): MetricParam {
58
94
  if (raw == null || raw === "") {
59
95
  return variable.defaultValue ?? null;
@@ -71,12 +107,21 @@ function coerceVariableValue(variable: MetricVariable, raw: unknown): MetricPara
71
107
  return String(raw);
72
108
  }
73
109
 
74
- function resolveMetricVariables(metric: Metric, provided: Record<string, unknown>) {
110
+ function resolveMetricVariables(
111
+ metric: Metric,
112
+ provided: Record<string, unknown>,
113
+ dynamicOptionsByKey: Record<string, Array<{ label: string; value: MetricParam }>> = {},
114
+ ) {
75
115
  const values: Record<string, MetricParam> = {};
76
116
  for (const variable of metric.definition.variables ?? []) {
77
- const value = coerceVariableValue(variable, provided[variable.key]);
78
- if (variable.options?.length) {
79
- const allowed = variable.options.some((option) => option.value === value);
117
+ const options = dynamicOptionsByKey[variable.key] ?? variable.options;
118
+ const raw = provided[variable.key];
119
+ const value =
120
+ (raw == null || raw === "") && variable.defaultValue === undefined && options?.length
121
+ ? options[0]!.value
122
+ : coerceVariableValue(variable, raw);
123
+ if (options?.length) {
124
+ const allowed = options.some((option) => option.value === value);
80
125
  if (!allowed) {
81
126
  throw new Error(`Metric variable "${variable.key}" must match one of its options`);
82
127
  }
@@ -125,10 +170,14 @@ function runMetricWidget(widget: MetricWidget, variables: Record<string, MetricP
125
170
  }
126
171
 
127
172
  function runMetric(metric: Metric, providedVariables: Record<string, unknown> = {}) {
128
- const variables = resolveMetricVariables(metric, providedVariables);
173
+ const resolved = resolveVariableOptions(metric);
174
+ const variables = resolveMetricVariables(metric, providedVariables, resolved.optionsByKey);
129
175
  const widgets = metric.definition.widgets.map((widget) => runMetricWidget(widget, variables));
130
176
  return {
131
- metric,
177
+ metric: {
178
+ ...metric,
179
+ definition: { ...metric.definition, variables: resolved.variables },
180
+ },
132
181
  variables,
133
182
  widgets,
134
183
  // Kept as the first widget result for older callers during the PR cycle.
@@ -12,9 +12,8 @@ import {
12
12
  updateScheduledTask,
13
13
  } from "../be/db";
14
14
  import { mergeScheduleTiming, validateRecurringTiming } from "../be/schedules/validate";
15
- import { calculateNextRun } from "../scheduler/scheduler";
16
- import { scheduleContextKey } from "../tasks/context-key";
17
- import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
15
+ import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
16
+ import { calculateNextRun, createStandaloneScheduleTask } from "../scheduler/scheduler";
18
17
  import { getExecutorRegistry } from "../workflows";
19
18
  import { handleScheduleTrigger } from "../workflows/triggers";
20
19
  import { route } from "./route-def";
@@ -41,6 +40,7 @@ const createSchedule = route({
41
40
  enabled: z.boolean().optional(),
42
41
  timezone: z.string().optional(),
43
42
  model: z.string().optional(),
43
+ modelTier: ModelTierSchema.optional(),
44
44
  scheduleType: z.enum(["recurring", "one_time"]).optional(),
45
45
  delayMs: z.number().int().optional(),
46
46
  runAt: z.string().optional(),
@@ -126,6 +126,7 @@ const updateSchedule = route({
126
126
  enabled: z.boolean().optional(),
127
127
  timezone: z.string().optional(),
128
128
  model: z.string().optional(),
129
+ modelTier: ModelTierSchema.nullable().optional(),
129
130
  nextRunAt: z.string().nullable().optional(),
130
131
  }),
131
132
  responses: {
@@ -270,7 +271,7 @@ export async function handleSchedules(
270
271
  enabled: body.enabled,
271
272
  nextRunAt,
272
273
  timezone: body.timezone,
273
- model: body.model,
274
+ ...splitLegacyModelAlias({ model: body.model, modelTier: body.modelTier }),
274
275
  scheduleType: body.scheduleType,
275
276
  });
276
277
 
@@ -333,17 +334,7 @@ export async function handleSchedules(
333
334
  const now = new Date().toISOString();
334
335
 
335
336
  const task = getDb().transaction(() => {
336
- const createdTask = createTaskWithSiblingAwareness(schedule.taskTemplate, {
337
- creatorAgentId: schedule.createdByAgentId,
338
- taskType: schedule.taskType,
339
- tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`, "manual-run"],
340
- priority: schedule.priority,
341
- agentId: schedule.targetAgentId,
342
- model: schedule.model,
343
- scheduleId: schedule.id,
344
- source: "schedule",
345
- contextKey: scheduleContextKey({ scheduleId: schedule.id }),
346
- });
337
+ const createdTask = createStandaloneScheduleTask(schedule, ["manual-run"]);
347
338
 
348
339
  if (schedule.scheduleType === "one_time") {
349
340
  updateScheduledTask(schedule.id, {
@@ -388,6 +379,16 @@ export async function handleSchedules(
388
379
  const parsed = await updateSchedule.parse(req, res, pathSegments, queryParams);
389
380
  if (!parsed) return true;
390
381
  const body = parsed.body as Record<string, unknown>;
382
+ if (parsed.body.model !== undefined || parsed.body.modelTier !== undefined) {
383
+ const normalizedModel = splitLegacyModelAlias({
384
+ model: parsed.body.model,
385
+ modelTier: parsed.body.modelTier,
386
+ });
387
+ if (parsed.body.model !== undefined) body.model = normalizedModel.model ?? null;
388
+ if (parsed.body.modelTier !== undefined || normalizedModel.modelTier) {
389
+ body.modelTier = normalizedModel.modelTier ?? null;
390
+ }
391
+ }
391
392
 
392
393
  const existing = getScheduledTaskById(parsed.params.id);
393
394
  if (!existing) {
@@ -51,6 +51,7 @@ const createScriptRunBodySchema = z.object({
51
51
  const listScriptRunsQuerySchema = z.object({
52
52
  status: ScriptRunStatusSchema.optional(),
53
53
  agentId: z.string().optional(),
54
+ scriptName: z.string().optional(),
54
55
  limit: z.coerce.number().int().min(1).max(500).optional(),
55
56
  offset: z.coerce.number().int().min(0).optional(),
56
57
  });
@@ -367,12 +368,17 @@ export async function handleScriptRuns(
367
368
  const opts = {
368
369
  status: parsed.query.status,
369
370
  agentId: parsed.query.agentId,
371
+ scriptName: parsed.query.scriptName,
370
372
  limit: parsed.query.limit ?? 50,
371
373
  offset: parsed.query.offset ?? 0,
372
374
  };
373
375
  json(res, {
374
376
  runs: listScriptRuns(opts),
375
- total: countScriptRuns({ status: opts.status, agentId: opts.agentId }),
377
+ total: countScriptRuns({
378
+ status: opts.status,
379
+ agentId: opts.agentId,
380
+ scriptName: opts.scriptName,
381
+ }),
376
382
  });
377
383
  return true;
378
384
  }
@@ -2,14 +2,23 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { getAgentById, recordInlineScriptRun, upsertKv } from "../be/db";
4
4
  import { createEvent } from "../be/events";
5
- import { deleteScript, getScript, upsertScriptByName } from "../be/scripts/db";
5
+ import {
6
+ deleteScript,
7
+ getScript,
8
+ getScriptById,
9
+ listScripts,
10
+ listScriptVersions,
11
+ upsertScriptByName,
12
+ } from "../be/scripts/db";
6
13
  import { searchScripts } from "../be/scripts/embeddings";
7
14
  import { extractArgsJsonSchema } from "../be/scripts/extract-schema";
8
15
  import { SCRIPT_SDK_TYPES, SCRIPT_STDLIB_TYPES, typecheckScript } from "../be/scripts/typecheck";
9
16
  import { extractScriptSignature } from "../scripts-runtime/extract-signature";
10
17
  import { runScript } from "../scripts-runtime/loader";
11
18
  import {
19
+ type ScriptDetail,
12
20
  ScriptFsModeSchema,
21
+ type ScriptListItem,
13
22
  type ScriptRecord,
14
23
  type ScriptScope,
15
24
  ScriptScopeSchema,
@@ -52,6 +61,11 @@ const searchBodySchema = z.object({
52
61
  const nameParamsSchema = z.object({ name: scriptNameSchema });
53
62
  const scopeQuerySchema = z.object({ scope: ScriptScopeSchema.default("agent") });
54
63
  const optionalScopeQuerySchema = z.object({ scope: ScriptScopeSchema.optional() });
64
+ const idParamsSchema = z.object({ id: z.string().uuid() });
65
+ const listScriptsQuerySchema = z.object({
66
+ scope: ScriptScopeSchema.optional(),
67
+ includeScratch: z.enum(["true", "false"]).optional(),
68
+ });
55
69
 
56
70
  const upsertRoute = route({
57
71
  method: "post",
@@ -133,6 +147,74 @@ const typesRoute = route({
133
147
  },
134
148
  });
135
149
 
150
+ // ── Dashboard read routes ──
151
+ // The worker-facing routes above resolve scripts relative to the calling agent
152
+ // and therefore requireAgent (X-Agent-ID). The routes below are cross-scope
153
+ // admin reads for the dashboard: API-key auth only, no agent identity — the
154
+ // same model as /api/script-runs.
155
+
156
+ const listScriptsRoute = route({
157
+ method: "get",
158
+ path: "/api/scripts",
159
+ pattern: ["api", "scripts"],
160
+ operationId: "scripts_list",
161
+ summary: "List saved scripts",
162
+ description:
163
+ "Dashboard read: lean projection without source. Scratch scripts are excluded unless includeScratch=true.",
164
+ tags: ["Scripts"],
165
+ query: listScriptsQuerySchema,
166
+ responses: {
167
+ 200: { description: "Saved scripts" },
168
+ 400: { description: "Validation error" },
169
+ },
170
+ });
171
+
172
+ // Declared (and matched) BEFORE the by-id route: the by-id pattern
173
+ // ["api", "scripts", null] matches any single segment, so the literal
174
+ // "type-defs" segment must win first.
175
+ const typeDefsRoute = route({
176
+ method: "get",
177
+ path: "/api/scripts/type-defs",
178
+ pattern: ["api", "scripts", "type-defs"],
179
+ operationId: "scripts_type_defs",
180
+ summary: "Get script SDK and stdlib type definitions",
181
+ description: "Static .d.ts blobs for editor integration (e.g. Monaco extraLibs). Cacheable.",
182
+ tags: ["Scripts"],
183
+ responses: {
184
+ 200: { description: "SDK and stdlib type definition blobs" },
185
+ },
186
+ });
187
+
188
+ const getScriptByIdRoute = route({
189
+ method: "get",
190
+ path: "/api/scripts/{id}",
191
+ pattern: ["api", "scripts", null],
192
+ operationId: "scripts_get",
193
+ summary: "Get a saved script by id",
194
+ description: "Dashboard read: full record including source and parsed signature.",
195
+ tags: ["Scripts"],
196
+ params: idParamsSchema,
197
+ responses: {
198
+ 200: { description: "Script detail" },
199
+ 404: { description: "Script not found" },
200
+ },
201
+ });
202
+
203
+ const listVersionsRoute = route({
204
+ method: "get",
205
+ path: "/api/scripts/{id}/versions",
206
+ pattern: ["api", "scripts", null, "versions"],
207
+ operationId: "scripts_versions",
208
+ summary: "List versions of a saved script",
209
+ description: "Dashboard read: version history, newest first.",
210
+ tags: ["Scripts"],
211
+ params: idParamsSchema,
212
+ responses: {
213
+ 200: { description: "Script versions" },
214
+ 404: { description: "Script not found" },
215
+ },
216
+ });
217
+
136
218
  function requireAgent(res: ServerResponse, agentId: string | undefined) {
137
219
  if (!agentId) {
138
220
  jsonError(res, "X-Agent-ID required for scripts API", 400);
@@ -413,6 +495,70 @@ export async function handleScripts(
413
495
  return true;
414
496
  }
415
497
 
498
+ // ── Dashboard reads (no requireAgent — API-key auth only, like /api/script-runs) ──
499
+
500
+ if (listScriptsRoute.match(req.method, pathSegments)) {
501
+ const parsed = await listScriptsRoute.parse(req, res, pathSegments, queryParams);
502
+ if (!parsed) return true;
503
+ const scripts: ScriptListItem[] = listScripts({
504
+ scope: parsed.query.scope,
505
+ includeScratch: parsed.query.includeScratch === "true",
506
+ }).map((script) => ({
507
+ id: script.id,
508
+ name: script.name,
509
+ scope: script.scope,
510
+ scopeId: script.scopeId,
511
+ description: script.description,
512
+ intent: script.intent,
513
+ version: script.version,
514
+ isScratch: script.isScratch,
515
+ typeChecked: script.typeChecked,
516
+ fsMode: script.fsMode,
517
+ createdByAgentId: script.createdByAgentId,
518
+ createdAt: script.createdAt,
519
+ updatedAt: script.updatedAt,
520
+ }));
521
+ json(res, { scripts });
522
+ return true;
523
+ }
524
+
525
+ // Must be matched before getScriptByIdRoute — its ["api", "scripts", null]
526
+ // pattern would otherwise swallow the literal "type-defs" segment.
527
+ if (typeDefsRoute.match(req.method, pathSegments)) {
528
+ json(res, { sdkTypes: SCRIPT_SDK_TYPES, stdlibTypes: SCRIPT_STDLIB_TYPES });
529
+ return true;
530
+ }
531
+
532
+ if (getScriptByIdRoute.match(req.method, pathSegments)) {
533
+ const parsed = await getScriptByIdRoute.parse(req, res, pathSegments, queryParams);
534
+ if (!parsed) return true;
535
+ const script = getScriptById(parsed.params.id);
536
+ if (!script) {
537
+ jsonError(res, "Script not found", 404);
538
+ return true;
539
+ }
540
+ // `source` is author-supplied TS (same trust surface as script_runs.source,
541
+ // already served raw by GET /api/script-runs/{id}) — no env/secret material.
542
+ const detail: ScriptDetail = {
543
+ ...script,
544
+ signature: JSON.parse(script.signatureJson) as unknown,
545
+ argsJsonSchema: script.argsJsonSchema ? (JSON.parse(script.argsJsonSchema) as unknown) : null,
546
+ };
547
+ json(res, { script: detail });
548
+ return true;
549
+ }
550
+
551
+ if (listVersionsRoute.match(req.method, pathSegments)) {
552
+ const parsed = await listVersionsRoute.parse(req, res, pathSegments, queryParams);
553
+ if (!parsed) return true;
554
+ if (!getScriptById(parsed.params.id)) {
555
+ jsonError(res, "Script not found", 404);
556
+ return true;
557
+ }
558
+ json(res, { versions: listScriptVersions(parsed.params.id) });
559
+ return true;
560
+ }
561
+
416
562
  if (typesRoute.match(req.method, pathSegments)) {
417
563
  const parsed = await typesRoute.parse(req, res, pathSegments, queryParams);
418
564
  if (!parsed) return true;
package/src/http/tasks.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  updateTaskProgress,
24
24
  updateTaskVcs,
25
25
  } from "../be/db";
26
+ import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
26
27
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
27
28
  import { createResumeFollowUp, createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
28
29
  import { telemetry } from "../telemetry";
@@ -91,6 +92,8 @@ const createTask = route({
91
92
  outputSchema: z.record(z.string(), z.unknown()).optional(),
92
93
  contextKey: z.string().optional(),
93
94
  requestedByUserId: z.string().optional(),
95
+ model: z.string().optional(),
96
+ modelTier: ModelTierSchema.optional(),
94
97
  }),
95
98
  responses: {
96
99
  201: { description: "Task created" },
@@ -395,6 +398,10 @@ export async function handleTasks(
395
398
  outputSchema: parsed.body.outputSchema || undefined,
396
399
  contextKey: parsed.body.contextKey || undefined,
397
400
  requestedByUserId,
401
+ ...splitLegacyModelAlias({
402
+ model: parsed.body.model,
403
+ modelTier: parsed.body.modelTier,
404
+ }),
398
405
  });
399
406
 
400
407
  ensure({
@@ -0,0 +1,140 @@
1
+ import { z } from "zod";
2
+ import type { ProviderName } from "./types";
3
+
4
+ export const ModelTierSchema = z.enum(["smol", "regular", "smart", "ultra"]);
5
+ export type ModelTier = z.infer<typeof ModelTierSchema>;
6
+
7
+ export const MODEL_TIERS = ModelTierSchema.options;
8
+
9
+ export const LEGACY_MODEL_TO_TIER: Record<string, ModelTier> = {
10
+ haiku: "smol",
11
+ sonnet: "regular",
12
+ opus: "smart",
13
+ fable: "ultra",
14
+ };
15
+
16
+ export const MODEL_TIER_LABELS: Record<ModelTier, string> = {
17
+ smol: "Smol",
18
+ regular: "Regular",
19
+ smart: "Smart",
20
+ ultra: "Ultra",
21
+ };
22
+
23
+ export const DEFAULT_MODEL_TIER_MAP: Record<ProviderName, Record<ModelTier, string>> = {
24
+ claude: {
25
+ smol: "haiku",
26
+ regular: "sonnet",
27
+ smart: "opus",
28
+ ultra: "fable",
29
+ },
30
+ "claude-managed": {
31
+ smol: "claude-haiku-4-5",
32
+ regular: "claude-sonnet-4-6",
33
+ smart: "claude-opus-4-8",
34
+ ultra: "claude-fable-5",
35
+ },
36
+ codex: {
37
+ smol: "gpt-5.4-mini",
38
+ regular: "gpt-5.4",
39
+ smart: "gpt-5.5",
40
+ ultra: "gpt-5.5",
41
+ },
42
+ pi: {
43
+ smol: "openrouter/deepseek/deepseek-v4-flash",
44
+ regular: "openrouter/deepseek/deepseek-v4-flash",
45
+ smart: "openrouter/deepseek/deepseek-v4-pro",
46
+ ultra: "openrouter/anthropic/claude-opus-4.8",
47
+ },
48
+ opencode: {
49
+ smol: "openrouter/deepseek/deepseek-v4-flash",
50
+ regular: "openrouter/deepseek/deepseek-v4-flash",
51
+ smart: "openrouter/deepseek/deepseek-v4-pro",
52
+ ultra: "openrouter/anthropic/claude-opus-4.8",
53
+ },
54
+ devin: {
55
+ smol: "devin",
56
+ regular: "devin",
57
+ smart: "devin",
58
+ ultra: "devin",
59
+ },
60
+ };
61
+
62
+ export function parseModelTier(value: string | null | undefined): ModelTier | undefined {
63
+ if (!value) return undefined;
64
+ const normalized = value.trim().toLowerCase();
65
+ return ModelTierSchema.safeParse(normalized).success
66
+ ? (normalized as ModelTier)
67
+ : LEGACY_MODEL_TO_TIER[normalized];
68
+ }
69
+
70
+ export function splitLegacyModelAlias(input: {
71
+ model?: string | null;
72
+ modelTier?: string | null;
73
+ }): { model?: string; modelTier?: ModelTier } {
74
+ const explicitTier = parseModelTier(input.modelTier);
75
+ const model = input.model?.trim();
76
+ if (!model) return { modelTier: explicitTier };
77
+
78
+ const legacyTier = parseModelTier(model);
79
+ if (legacyTier && !explicitTier) {
80
+ return { modelTier: legacyTier };
81
+ }
82
+
83
+ return {
84
+ model,
85
+ modelTier: explicitTier,
86
+ };
87
+ }
88
+
89
+ function parseTierMapJson(value: string | undefined): Partial<Record<ModelTier, string>> {
90
+ if (!value) return {};
91
+ try {
92
+ const parsed = JSON.parse(value) as unknown;
93
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
94
+ const result: Partial<Record<ModelTier, string>> = {};
95
+ for (const tier of MODEL_TIERS) {
96
+ const model = (parsed as Record<string, unknown>)[tier];
97
+ if (typeof model === "string" && model.trim()) result[tier] = model.trim();
98
+ }
99
+ return result;
100
+ } catch {
101
+ return {};
102
+ }
103
+ }
104
+
105
+ export function resolveModelTier(opts: {
106
+ tier?: string | null;
107
+ harnessProvider: ProviderName;
108
+ env?: Record<string, string | undefined>;
109
+ }): string | undefined {
110
+ const tier = parseModelTier(opts.tier);
111
+ if (!tier) return undefined;
112
+
113
+ const env = opts.env ?? {};
114
+ const jsonOverrides = parseTierMapJson(env.MODEL_TIER_MAP);
115
+ const envKey = `MODEL_TIER_${tier.toUpperCase()}`;
116
+ const directOverride = env[envKey]?.trim();
117
+ if (directOverride) return directOverride;
118
+ if (jsonOverrides[tier]) return jsonOverrides[tier];
119
+
120
+ return DEFAULT_MODEL_TIER_MAP[opts.harnessProvider]?.[tier];
121
+ }
122
+
123
+ export function resolveTaskModelSelection(opts: {
124
+ model?: string | null;
125
+ modelTier?: string | null;
126
+ harnessProvider: ProviderName;
127
+ env?: Record<string, string | undefined>;
128
+ }): { model?: string; source: "model" | "modelTier" | "none" } {
129
+ const model = opts.model?.trim();
130
+ if (model) return { model, source: "model" };
131
+
132
+ const tierModel = resolveModelTier({
133
+ tier: opts.modelTier,
134
+ harnessProvider: opts.harnessProvider,
135
+ env: opts.env,
136
+ });
137
+ if (tierModel) return { model: tierModel, source: "modelTier" };
138
+
139
+ return { source: "none" };
140
+ }
@@ -287,7 +287,21 @@ export async function buildAuthorizeUrl(input: BuildAuthorizeInput): Promise<Bui
287
287
  url.searchParams.set("resource", input.resource);
288
288
 
289
289
  if (input.extraParams) {
290
+ const RESERVED = new Set([
291
+ "response_type",
292
+ "client_id",
293
+ "redirect_uri",
294
+ "scope",
295
+ "state",
296
+ "code_challenge",
297
+ "code_challenge_method",
298
+ "resource",
299
+ ]);
290
300
  for (const [k, v] of Object.entries(input.extraParams)) {
301
+ if (RESERVED.has(k.toLowerCase())) {
302
+ console.warn(`[mcp-oauth] extraParams key "${k}" is reserved and skipped`);
303
+ continue;
304
+ }
291
305
  url.searchParams.set(k, v);
292
306
  }
293
307
  }
@@ -26,6 +26,7 @@
26
26
  /** Models supported by the managed-agents surface for the swarm worker. */
27
27
  export const CLAUDE_MANAGED_MODELS = [
28
28
  "claude-fable-5",
29
+ "claude-mythos-5",
29
30
  "claude-sonnet-4-6",
30
31
  "claude-opus-4-8",
31
32
  "claude-opus-4-7",
@@ -51,6 +52,8 @@ export interface ClaudeManagedModelPricing {
51
52
  * Anthropic public list pricing. Source:
52
53
  * https://platform.claude.com/docs/en/about-claude/pricing
53
54
  *
55
+ * - claude-fable-5: $10 / $50 / $1.00 / $12.50 (verified 2026-06-10)
56
+ * - claude-mythos-5: $10 / $50 / $1.00 / $12.50 (limited availability, verified 2026-06-10)
54
57
  * - claude-sonnet-4-6: $3 / $15 / $0.30 / $3.75 (in / out / cache-read / cache-write)
55
58
  * - claude-opus-4-8: $5 / $25 / $0.50 / $6.25 (verified 2026-05-28)
56
59
  * - claude-opus-4-7: $15 / $75 / $1.50 / $18.75 (STALE — was correct at launch, Anthropic has since dropped Opus to $5/$25)
@@ -64,6 +67,12 @@ export const CLAUDE_MANAGED_MODEL_PRICING: Record<ClaudeManagedModel, ClaudeMana
64
67
  cacheReadPerMillion: 1.0,
65
68
  cacheWritePerMillion: 12.5,
66
69
  },
70
+ "claude-mythos-5": {
71
+ inputPerMillion: 10.0,
72
+ outputPerMillion: 50.0,
73
+ cacheReadPerMillion: 1.0,
74
+ cacheWritePerMillion: 12.5,
75
+ },
67
76
  "claude-sonnet-4-6": {
68
77
  inputPerMillion: 3.0,
69
78
  outputPerMillion: 15.0,
@@ -63,6 +63,21 @@ export async function resolveCodexPrompt(
63
63
  prompt: string,
64
64
  skillsDir?: string,
65
65
  emit?: (event: ProviderEvent) => void,
66
+ ): Promise<string> {
67
+ return resolveSlashSkillPrompt(prompt, {
68
+ providerLabel: "codex",
69
+ skillsDir: skillsDir ?? defaultSkillsDir(),
70
+ emit,
71
+ });
72
+ }
73
+
74
+ export async function resolveSlashSkillPrompt(
75
+ prompt: string,
76
+ opts: {
77
+ providerLabel: string;
78
+ skillsDir: string;
79
+ emit?: (event: ProviderEvent) => void;
80
+ },
66
81
  ): Promise<string> {
67
82
  if (!prompt) {
68
83
  return prompt;
@@ -81,15 +96,14 @@ export async function resolveCodexPrompt(
81
96
 
82
97
  const commandName: string = match[1];
83
98
  const trailingArgs: string = match[2] ?? "";
84
- const dir = skillsDir ?? defaultSkillsDir();
85
- const skillPath = join(dir, commandName, "SKILL.md");
99
+ const skillPath = join(opts.skillsDir, commandName, "SKILL.md");
86
100
 
87
101
  const file = Bun.file(skillPath);
88
102
  const exists = await file.exists();
89
103
  if (!exists) {
90
- emit?.({
104
+ opts.emit?.({
91
105
  type: "raw_stderr",
92
- content: `[codex] skill resolver: SKILL.md not found for /${commandName} (looked in ${skillPath})\n`,
106
+ content: `[${opts.providerLabel}] skill resolver: SKILL.md not found for /${commandName} (looked in ${skillPath})\n`,
93
107
  });
94
108
  return prompt;
95
109
  }
@@ -99,17 +113,17 @@ export async function resolveCodexPrompt(
99
113
  skillContent = await file.text();
100
114
  } catch (err) {
101
115
  const message = err instanceof Error ? err.message : String(err);
102
- emit?.({
116
+ opts.emit?.({
103
117
  type: "raw_stderr",
104
- content: `[codex] skill resolver: failed to read SKILL.md for /${commandName}: ${message}\n`,
118
+ content: `[${opts.providerLabel}] skill resolver: failed to read SKILL.md for /${commandName}: ${message}\n`,
105
119
  });
106
120
  return prompt;
107
121
  }
108
122
 
109
123
  if (skillContent.length > MAX_SKILL_CHARS) {
110
- emit?.({
124
+ opts.emit?.({
111
125
  type: "raw_stderr",
112
- content: `[codex] skill resolver: SKILL.md for /${commandName} exceeds ${MAX_SKILL_CHARS} chars (${skillContent.length}), truncating\n`,
126
+ content: `[${opts.providerLabel}] skill resolver: SKILL.md for /${commandName} exceeds ${MAX_SKILL_CHARS} chars (${skillContent.length}), truncating\n`,
113
127
  });
114
128
  skillContent = skillContent.slice(0, MAX_SKILL_CHARS);
115
129
  }