@desplega.ai/agent-swarm 1.94.0 → 1.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/openapi.json +46 -1
- package/package.json +4 -3
- package/src/be/boot-scrub-logs.ts +76 -0
- package/src/be/db.ts +22 -10
- package/src/be/migrations/094_mcp_extra_authorize_params.sql +4 -0
- package/src/be/modelsdev-cache.json +89422 -85636
- package/src/be/skill-sync.ts +4 -4
- package/src/be/swarm-config-guard.ts +8 -0
- package/src/commands/provider-credentials.ts +37 -9
- package/src/commands/runner.ts +28 -0
- package/src/http/agents.ts +1 -0
- package/src/http/config.ts +24 -4
- package/src/http/index.ts +9 -0
- package/src/http/mcp-oauth.ts +14 -0
- package/src/oauth/mcp-wrapper.ts +14 -0
- package/src/prompts/session-templates.ts +21 -0
- package/src/providers/codex-skill-resolver.ts +22 -8
- package/src/providers/opencode-adapter.ts +20 -2
- package/src/providers/pi-mono-adapter.ts +160 -21
- package/src/providers/types.ts +33 -0
- package/src/tests/bedrock-model-groups.test.ts +135 -0
- package/src/tests/credential-check.test.ts +538 -50
- package/src/tests/harness-provider-resolution.test.ts +23 -0
- package/src/tests/mcp-oauth-queries.test.ts +71 -1
- package/src/tests/mcp-oauth-wrapper.test.ts +109 -0
- package/src/tests/opencode-adapter.test.ts +29 -1
- package/src/tests/provider-command-format.test.ts +12 -0
- package/src/tests/secret-scrubber.test.ts +73 -1
- package/src/tests/skill-fs-writer.test.ts +7 -1
- package/src/tests/skill-sync.test.ts +15 -3
- package/src/tools/mcp-servers/mcp-server-create.ts +7 -0
- package/src/tools/mcp-servers/mcp-server-update.ts +8 -0
- package/src/tools/swarm-config/get-config.ts +9 -1
- package/src/tools/swarm-config/list-config.ts +8 -0
- package/src/types.ts +22 -0
- package/src/utils/secret-scrubber.ts +33 -12
- package/src/utils/skill-fs-writer.ts +11 -3
package/src/be/skill-sync.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Filesystem sync for skills.
|
|
3
3
|
*
|
|
4
|
-
* Writes installed skills to
|
|
5
|
-
*
|
|
6
|
-
* so Claude Code, Pi, and Codex discover them natively.
|
|
4
|
+
* Writes installed skills to every local harness skill tree so Claude Code,
|
|
5
|
+
* Pi, Codex, OpenCode, and AGENTS.md-compatible adapters can discover them.
|
|
7
6
|
*
|
|
8
7
|
* This runs on the API side — workers call it via POST /api/skills/sync-filesystem.
|
|
9
8
|
* The actual FS write logic lives in the worker-safe src/utils/skill-fs-writer.ts
|
|
@@ -13,6 +12,7 @@
|
|
|
13
12
|
import { homedir } from "node:os";
|
|
14
13
|
import {
|
|
15
14
|
type SkillFsEntry,
|
|
15
|
+
type SkillHarnessTarget,
|
|
16
16
|
type SkillSyncResult,
|
|
17
17
|
writeSkillsToFilesystem,
|
|
18
18
|
} from "../utils/skill-fs-writer";
|
|
@@ -32,7 +32,7 @@ export type { SkillSyncResult };
|
|
|
32
32
|
*/
|
|
33
33
|
export function syncSkillsToFilesystem(
|
|
34
34
|
agentId: string,
|
|
35
|
-
harnessType:
|
|
35
|
+
harnessType: SkillHarnessTarget = "all",
|
|
36
36
|
homeOverride?: string,
|
|
37
37
|
): SkillSyncResult {
|
|
38
38
|
const skills = getAgentSkills(agentId);
|
|
@@ -58,6 +58,14 @@ const VALIDATED_KEYS: Record<string, (value: unknown) => string | null> = {
|
|
|
58
58
|
if (["true", "false", "1", "0"].includes(normalized)) return null;
|
|
59
59
|
return "Invalid SWARM_USE_CLAUDE_BRIDGE value (must be one of: true, false, 1, 0)";
|
|
60
60
|
},
|
|
61
|
+
// AWS credential mode for the Bedrock path on the pi harness.
|
|
62
|
+
// sdk — AWS SDK default credential chain (env, ~/.aws/*, SSO, IMDS, …)
|
|
63
|
+
// bearer — explicit bearer token via AWS_BEARER_TOKEN_BEDROCK (future/Mantle)
|
|
64
|
+
// When absent the worker infers the mode from MODEL_OVERRIDE (sdk semantics).
|
|
65
|
+
BEDROCK_AUTH_MODE: (value) => {
|
|
66
|
+
if (value === "sdk" || value === "bearer") return null;
|
|
67
|
+
return "Invalid BEDROCK_AUTH_MODE value (must be one of: sdk, bearer)";
|
|
68
|
+
},
|
|
61
69
|
};
|
|
62
70
|
|
|
63
71
|
export function validateConfigValue(key: string, value: unknown): string | null {
|
|
@@ -29,6 +29,21 @@ import { scrubSecrets } from "../utils/secret-scrubber";
|
|
|
29
29
|
|
|
30
30
|
export type SupportedProvider = "claude" | "claude-managed" | "codex" | "devin" | "opencode" | "pi";
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* True when the pi harness should use the AWS SDK Bedrock path: either an
|
|
34
|
+
* explicit `BEDROCK_AUTH_MODE=sdk`, or — preserving prefix-inference semantics —
|
|
35
|
+
* `BEDROCK_AUTH_MODE` absent with a `MODEL_OVERRIDE=amazon-bedrock/*` selection.
|
|
36
|
+
* Single source of truth for the gate so the live-test arm and the worker
|
|
37
|
+
* reconcile loop agree with `checkPiMonoCredentials`.
|
|
38
|
+
*/
|
|
39
|
+
export function isBedrockSdkMode(env: Record<string, string | undefined>): boolean {
|
|
40
|
+
const mode = env.BEDROCK_AUTH_MODE?.toLowerCase();
|
|
41
|
+
return (
|
|
42
|
+
mode === "sdk" ||
|
|
43
|
+
(mode === undefined && Boolean(env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/")))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
/**
|
|
33
48
|
* Static documentation of which env vars each provider considers when running
|
|
34
49
|
* `checkCredentials`. Used by the dashboard to render hints before any worker
|
|
@@ -243,7 +258,7 @@ function parseCodexOAuthAccess(blob: string | undefined): string | null {
|
|
|
243
258
|
* | `codex` | `~/.codex/auth.json` (file) → `CODEX_OAUTH` (env OAuth) → `OPENAI_API_KEY` | OpenAI `/v1/models` (api-key path only) |
|
|
244
259
|
* | `opencode` | `OPENROUTER_API_KEY` → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` (pi-style) | matching provider's `/v1/models` |
|
|
245
260
|
* | `pi` | `OPENROUTER_API_KEY` → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` | matching provider's `/v1/models` |
|
|
246
|
-
* | `pi` (bedrock) | `MODEL_OVERRIDE=amazon-bedrock/*` → AWS SDK default credential chain | presence-only (
|
|
261
|
+
* | `pi` (bedrock) | `MODEL_OVERRIDE=amazon-bedrock/*` → AWS SDK default credential chain | presence-only (real check is the worker-side Bedrock enumeration) |
|
|
247
262
|
* | `devin` | `DEVIN_API_KEY` (+ `DEVIN_API_BASE_URL` override) | `${baseUrl}/v1/sessions?limit=1` |
|
|
248
263
|
*
|
|
249
264
|
* Returns `{ok: true, latency_ms}` on 2xx, `{ok: false, error, latency_ms}`
|
|
@@ -302,14 +317,14 @@ export async function validateProviderCredentials(provider: string): Promise<Liv
|
|
|
302
317
|
}
|
|
303
318
|
case "pi":
|
|
304
319
|
case "opencode": {
|
|
305
|
-
// pi
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
if (provider === "pi" && env
|
|
320
|
+
// For the pi Bedrock path, the real credential check is the AWS SDK
|
|
321
|
+
// enumeration (`ListFoundationModels` + `ListInferenceProfiles`) that
|
|
322
|
+
// `checkProviderCredentials` (the `pi` dynamic-import arm) already ran.
|
|
323
|
+
// That result is already in `buildCredStatusReport` — the live-test is a
|
|
324
|
+
// pass-through / no-op so we never issue a second AWS SDK call here
|
|
325
|
+
// (which would drag the SDK into the wrong binary or make slow IMDS
|
|
326
|
+
// calls on non-EC2 hosts).
|
|
327
|
+
if (provider === "pi" && isBedrockSdkMode(env)) {
|
|
313
328
|
return presenceCheckOk();
|
|
314
329
|
}
|
|
315
330
|
// Both pi-mono and opencode resolve credentials in the same order:
|
|
@@ -402,6 +417,18 @@ export async function buildCredStatusReport(
|
|
|
402
417
|
testedAt: Date.now(),
|
|
403
418
|
};
|
|
404
419
|
}
|
|
420
|
+
// Include the Bedrock enumeration block when the pi probe ran in Bedrock SDK
|
|
421
|
+
// mode (bedrockRegion is only set by checkPiMonoCredentials in that branch).
|
|
422
|
+
const bedrock: AgentCredStatus["bedrock"] =
|
|
423
|
+
presence.bedrockRegion !== undefined
|
|
424
|
+
? {
|
|
425
|
+
region: presence.bedrockRegion,
|
|
426
|
+
probedAt: Date.now(),
|
|
427
|
+
ready: presence.ready,
|
|
428
|
+
models: presence.bedrockModels ?? [],
|
|
429
|
+
error: presence.ready ? undefined : (presence.hint ?? undefined),
|
|
430
|
+
}
|
|
431
|
+
: null;
|
|
405
432
|
return {
|
|
406
433
|
ready: presence.ready,
|
|
407
434
|
missing: presence.missing ?? [],
|
|
@@ -411,6 +438,7 @@ export async function buildCredStatusReport(
|
|
|
411
438
|
latestModel: null,
|
|
412
439
|
reportedAt: Date.now(),
|
|
413
440
|
reportKind: kind,
|
|
441
|
+
bedrock,
|
|
414
442
|
};
|
|
415
443
|
}
|
|
416
444
|
|
package/src/commands/runner.ts
CHANGED
|
@@ -60,6 +60,7 @@ import { resolveClaudeMdPath, syncProfileFilesToServer } from "./profile-sync.ts
|
|
|
60
60
|
import {
|
|
61
61
|
buildCredStatusReport,
|
|
62
62
|
buildLatestModelReport,
|
|
63
|
+
isBedrockSdkMode,
|
|
63
64
|
isCredCheckDisabled,
|
|
64
65
|
reportCredStatus,
|
|
65
66
|
reportLatestModel,
|
|
@@ -436,6 +437,7 @@ const RELOADABLE_ENV_KEYS: ReadonlySet<string> = new Set([
|
|
|
436
437
|
"MODEL_OVERRIDE",
|
|
437
438
|
"AGENT_FS_SHARED_ORG_ID",
|
|
438
439
|
"SWARM_USE_CLAUDE_BRIDGE",
|
|
440
|
+
"BEDROCK_AUTH_MODE",
|
|
439
441
|
]);
|
|
440
442
|
|
|
441
443
|
/**
|
|
@@ -3847,6 +3849,16 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3847
3849
|
let lastHarnessReconcileAt = 0;
|
|
3848
3850
|
const HARNESS_RECONCILE_INTERVAL_MS = 10_000;
|
|
3849
3851
|
|
|
3852
|
+
// Throttle for the periodic Bedrock model-enumeration refresh. The credential
|
|
3853
|
+
// report below only re-runs on a harness_provider change (boot + provider
|
|
3854
|
+
// swap), so enabling Bedrock access after boot would otherwise never reach the
|
|
3855
|
+
// picker. This timer re-runs the enumeration on a fixed interval, decoupled
|
|
3856
|
+
// from the harness-change gate, so the UI stays accurate. 5 minutes keeps it
|
|
3857
|
+
// cheap (one bounded AWS enumeration per tick) while still surfacing newly
|
|
3858
|
+
// granted access within a few minutes.
|
|
3859
|
+
let lastBedrockRefreshAt = 0;
|
|
3860
|
+
const BEDROCK_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
3861
|
+
|
|
3850
3862
|
// Create API config for ping/close
|
|
3851
3863
|
const apiConfig: ApiConfig = { apiUrl, apiKey, agentId };
|
|
3852
3864
|
|
|
@@ -4571,6 +4583,22 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4571
4583
|
.catch((err) =>
|
|
4572
4584
|
console.warn(`[${role}] cred_status post_task report failed (non-fatal): ${err}`),
|
|
4573
4585
|
);
|
|
4586
|
+
} else if (
|
|
4587
|
+
currentHarness === "pi" &&
|
|
4588
|
+
isBedrockSdkMode(process.env) &&
|
|
4589
|
+
Date.now() - lastBedrockRefreshAt > BEDROCK_REFRESH_INTERVAL_MS
|
|
4590
|
+
) {
|
|
4591
|
+
// Bedrock enumeration drifts independently of the harness_provider:
|
|
4592
|
+
// access granted (or revoked) in the AWS console after boot won't flip
|
|
4593
|
+
// the provider, so the harness-change gate above never fires. Re-run the
|
|
4594
|
+
// enumeration on the throttled interval so the picker reflects the live
|
|
4595
|
+
// account state. One bounded AWS round-trip per tick.
|
|
4596
|
+
lastBedrockRefreshAt = Date.now();
|
|
4597
|
+
buildCredStatusReport(currentHarness, process.env, {}, "post_task")
|
|
4598
|
+
.then((snap) => reportCredStatus(apiUrl, apiKey, agentId, snap))
|
|
4599
|
+
.catch((err) =>
|
|
4600
|
+
console.warn(`[${role}] bedrock enumeration refresh failed (non-fatal): ${err}`),
|
|
4601
|
+
);
|
|
4574
4602
|
}
|
|
4575
4603
|
}
|
|
4576
4604
|
|
package/src/http/agents.ts
CHANGED
package/src/http/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
reservedKeyError,
|
|
15
15
|
validateConfigValue,
|
|
16
16
|
} from "../be/swarm-config-guard";
|
|
17
|
+
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
17
18
|
import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
|
|
18
19
|
import { route } from "./route-def";
|
|
19
20
|
import { json, jsonError } from "./utils";
|
|
@@ -152,7 +153,15 @@ export async function handleConfig(
|
|
|
152
153
|
parsed.query.agentId || undefined,
|
|
153
154
|
parsed.query.repoId || undefined,
|
|
154
155
|
);
|
|
155
|
-
|
|
156
|
+
const result = includeSecrets ? configs : maskSecrets(configs);
|
|
157
|
+
if (includeSecrets) {
|
|
158
|
+
for (const c of result) {
|
|
159
|
+
if (c.isSecret && c.value) {
|
|
160
|
+
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
json(res, { configs: result });
|
|
156
165
|
return true;
|
|
157
166
|
}
|
|
158
167
|
|
|
@@ -199,8 +208,11 @@ export async function handleConfig(
|
|
|
199
208
|
jsonError(res, "Config not found", 404);
|
|
200
209
|
return true;
|
|
201
210
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
211
|
+
const singleResult = includeSecrets ? config : maskSecrets([config])[0]!;
|
|
212
|
+
if (includeSecrets && singleResult.isSecret && singleResult.value) {
|
|
213
|
+
registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
|
|
214
|
+
}
|
|
215
|
+
json(res, singleResult);
|
|
204
216
|
return true;
|
|
205
217
|
}
|
|
206
218
|
|
|
@@ -212,7 +224,15 @@ export async function handleConfig(
|
|
|
212
224
|
scope: parsed.query.scope || undefined,
|
|
213
225
|
scopeId: parsed.query.scopeId || undefined,
|
|
214
226
|
});
|
|
215
|
-
|
|
227
|
+
const listResult = includeSecrets ? configs : maskSecrets(configs);
|
|
228
|
+
if (includeSecrets) {
|
|
229
|
+
for (const c of listResult) {
|
|
230
|
+
if (c.isSecret && c.value) {
|
|
231
|
+
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
json(res, { configs: listResult });
|
|
216
236
|
return true;
|
|
217
237
|
}
|
|
218
238
|
|
package/src/http/index.ts
CHANGED
|
@@ -576,6 +576,15 @@ httpServer
|
|
|
576
576
|
.catch((err) => {
|
|
577
577
|
console.error("[boot-reembed-scripts] startup backfill failed (non-fatal):", err);
|
|
578
578
|
});
|
|
579
|
+
|
|
580
|
+
// One-time scrub: retroactively redact any session_logs rows containing
|
|
581
|
+
// sensitive patterns that pre-date the defense-in-depth scrub layer.
|
|
582
|
+
// Idempotent, tracked via seed_state.
|
|
583
|
+
import("../be/boot-scrub-logs")
|
|
584
|
+
.then(({ runBootScrubLogs }) => runBootScrubLogs())
|
|
585
|
+
.catch((err) => {
|
|
586
|
+
console.error("[boot-scrub-logs] startup scrub failed (non-fatal):", err);
|
|
587
|
+
});
|
|
579
588
|
})
|
|
580
589
|
.on("error", (err) => {
|
|
581
590
|
console.error("HTTP Server Error:", err);
|
package/src/http/mcp-oauth.ts
CHANGED
|
@@ -362,6 +362,19 @@ async function prepareAuthorizeFlow(
|
|
|
362
362
|
|
|
363
363
|
const scopes = q.scopes ? splitScopes(q.scopes) : client.scopes;
|
|
364
364
|
|
|
365
|
+
let extraParams: Record<string, string> | undefined;
|
|
366
|
+
if (server.extraAuthorizeParams) {
|
|
367
|
+
try {
|
|
368
|
+
const parsed = JSON.parse(server.extraAuthorizeParams);
|
|
369
|
+
if (parsed && typeof parsed === "object") {
|
|
370
|
+
extraParams = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
// Malformed config must never break the authorize flow — log + ignore.
|
|
374
|
+
console.warn(`[mcp-oauth] Ignoring malformed extraAuthorizeParams for server ${mcpServerId}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
365
378
|
const built = await buildAuthorizeUrl({
|
|
366
379
|
authorizeUrl: client.authorizeUrl,
|
|
367
380
|
tokenUrl: client.tokenUrl,
|
|
@@ -369,6 +382,7 @@ async function prepareAuthorizeFlow(
|
|
|
369
382
|
redirectUri: callbackRedirectUri(),
|
|
370
383
|
scopes,
|
|
371
384
|
resource: client.resourceUrl,
|
|
385
|
+
extraParams,
|
|
372
386
|
});
|
|
373
387
|
|
|
374
388
|
insertMcpOAuthPending({
|
package/src/oauth/mcp-wrapper.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -158,6 +158,27 @@ When you finish a task:
|
|
|
158
158
|
- **Failure**: Use \`store-progress\` with status: "failed" and failureReason: "<what went wrong>"
|
|
159
159
|
|
|
160
160
|
Always include meaningful output - the lead agent reviews your work.
|
|
161
|
+
|
|
162
|
+
#### Credential Hygiene
|
|
163
|
+
|
|
164
|
+
When you retrieve secrets via \`get-config\` (with \`includeSecrets: true\`), **never pass secret values directly on a command line or embed them in tool output**. Command arguments are logged.
|
|
165
|
+
|
|
166
|
+
**Safe pattern:** Write the secret to a temporary \`.env\` file, then source it:
|
|
167
|
+
\`\`\`bash
|
|
168
|
+
# Write to temp file (not logged)
|
|
169
|
+
echo "MY_TOKEN=<value>" > /tmp/.task-env && source /tmp/.task-env
|
|
170
|
+
# Use the variable (value stays out of logs)
|
|
171
|
+
curl -H "Authorization: Bearer $MY_TOKEN" https://api.example.com
|
|
172
|
+
rm /tmp/.task-env
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
**Unsafe pattern (NEVER do this):**
|
|
176
|
+
\`\`\`bash
|
|
177
|
+
# The literal secret appears in the logged command
|
|
178
|
+
curl -H "Authorization: Bearer lin_oauth_abc123..." https://api.example.com
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
The same applies to \`store-progress\` output — never include raw secret values in progress text, output, or failure reasons.
|
|
161
182
|
`,
|
|
162
183
|
variables: [],
|
|
163
184
|
category: "system",
|
|
@@ -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
|
|
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: `[
|
|
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: `[
|
|
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: `[
|
|
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
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { validateOpencodeCredentials } from "../utils/credentials";
|
|
21
21
|
import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
|
|
22
22
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
23
|
+
import { resolveSlashSkillPrompt } from "./codex-skill-resolver";
|
|
23
24
|
import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
|
|
24
25
|
import { readPkgVersion } from "./harness-version";
|
|
25
26
|
import type {
|
|
@@ -102,6 +103,13 @@ function isAssistantMessage(msg: unknown): msg is AssistantMessage {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
const DOCKER_PLUGIN_PATH = "/home/worker/.config/opencode/plugins/agent-swarm.ts";
|
|
106
|
+
|
|
107
|
+
function defaultOpencodeSkillsDir(): string {
|
|
108
|
+
if (process.env.OPENCODE_SKILLS_DIR) {
|
|
109
|
+
return process.env.OPENCODE_SKILLS_DIR;
|
|
110
|
+
}
|
|
111
|
+
return join(process.env.HOME ?? "/home/worker", ".opencode", "skills");
|
|
112
|
+
}
|
|
105
113
|
const MODEL_CACHE_REFRESH_TIMEOUT_MS = 15_000;
|
|
106
114
|
|
|
107
115
|
function isOpenRouterModel(model: string | undefined): boolean {
|
|
@@ -291,6 +299,10 @@ export class OpencodeSession implements ProviderSession {
|
|
|
291
299
|
});
|
|
292
300
|
}
|
|
293
301
|
|
|
302
|
+
emitProviderEvent(event: ProviderEvent): void {
|
|
303
|
+
this.emit(event);
|
|
304
|
+
}
|
|
305
|
+
|
|
294
306
|
onEvent(listener: (event: ProviderEvent) => void): void {
|
|
295
307
|
const wasEmpty = this.listeners.length === 0;
|
|
296
308
|
this.listeners.push(listener);
|
|
@@ -738,13 +750,19 @@ export class OpencodeAdapter implements ProviderAdapter {
|
|
|
738
750
|
|
|
739
751
|
let promptRefreshAttempted = false;
|
|
740
752
|
let promptRefreshPromise: Promise<boolean> | undefined;
|
|
753
|
+
let session: OpencodeSession | undefined;
|
|
741
754
|
const sendPrompt = async () => {
|
|
755
|
+
const resolvedPrompt = await resolveSlashSkillPrompt(config.prompt, {
|
|
756
|
+
providerLabel: "opencode",
|
|
757
|
+
skillsDir: defaultOpencodeSkillsDir(),
|
|
758
|
+
emit: (event) => session?.emitProviderEvent(event),
|
|
759
|
+
});
|
|
742
760
|
await client.session.prompt({
|
|
743
761
|
path: { id: sessionId },
|
|
744
762
|
query: { directory: config.cwd },
|
|
745
763
|
body: {
|
|
746
764
|
agent: agentName,
|
|
747
|
-
parts: [{ type: "text", text:
|
|
765
|
+
parts: [{ type: "text", text: resolvedPrompt }],
|
|
748
766
|
},
|
|
749
767
|
});
|
|
750
768
|
};
|
|
@@ -760,7 +778,7 @@ export class OpencodeAdapter implements ProviderAdapter {
|
|
|
760
778
|
return await promptRefreshPromise;
|
|
761
779
|
};
|
|
762
780
|
|
|
763
|
-
|
|
781
|
+
session = new OpencodeSession(
|
|
764
782
|
sessionId,
|
|
765
783
|
server,
|
|
766
784
|
config.model,
|