bopodev-api 0.1.32 → 0.1.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bopodev-api",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "files": [
@@ -17,18 +17,20 @@
17
17
  "fflate": "^0.8.2",
18
18
  "multer": "^2.1.1",
19
19
  "nanoid": "^5.1.5",
20
+ "turndown": "^7.2.2",
20
21
  "ws": "^8.19.0",
21
22
  "yaml": "^2.8.3",
22
23
  "zod": "^4.1.5",
23
- "bopodev-agent-sdk": "0.1.32",
24
- "bopodev-contracts": "0.1.32",
25
- "bopodev-db": "0.1.32"
24
+ "bopodev-agent-sdk": "0.1.34",
25
+ "bopodev-db": "0.1.34",
26
+ "bopodev-contracts": "0.1.34"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@types/archiver": "^7.0.0",
29
30
  "@types/cors": "^2.8.19",
30
31
  "@types/express": "^5.0.3",
31
32
  "@types/multer": "^2.1.0",
33
+ "@types/turndown": "^5.0.6",
32
34
  "@types/ws": "^8.18.1",
33
35
  "tsx": "^4.20.5"
34
36
  },
@@ -1,4 +1,9 @@
1
- import { AgentRuntimeConfigSchema, type AgentRuntimeConfig, type ThinkingEffort } from "bopodev-contracts";
1
+ import {
2
+ AgentRuntimeConfigSchema,
3
+ companySkillAllowlistOnly,
4
+ type AgentRuntimeConfig,
5
+ type ThinkingEffort
6
+ } from "bopodev-contracts";
2
7
  import { normalizeAbsolutePath } from "./instance-paths";
3
8
 
4
9
  export type LegacyRuntimeFields = {
@@ -16,6 +21,8 @@ export type LegacyRuntimeFields = {
16
21
  allowWebSearch?: boolean;
17
22
  };
18
23
  runtimeEnv?: Record<string, string>;
24
+ /** Company skill ids only (bundled ids are stripped on normalize). */
25
+ enabledSkillIds?: string[] | null;
19
26
  };
20
27
 
21
28
  export type NormalizedRuntimeConfig = {
@@ -26,6 +33,8 @@ export type NormalizedRuntimeConfig = {
26
33
  runtimeModel?: string;
27
34
  runtimeThinkingEffort: ThinkingEffort;
28
35
  bootstrapPrompt?: string;
36
+ /** Omitted = inject all skills (legacy). Present = company skill ids only; bundled skills are always injected. */
37
+ enabledSkillIds?: string[];
29
38
  runtimeTimeoutSec: number;
30
39
  interruptGraceSec: number;
31
40
  runPolicy: {
@@ -109,6 +118,9 @@ export function normalizeRuntimeConfig(input: {
109
118
  if (legacy.runPolicy !== undefined) {
110
119
  merged.runPolicy = legacy.runPolicy;
111
120
  }
121
+ if (legacy.enabledSkillIds !== undefined) {
122
+ merged.enabledSkillIds = legacy.enabledSkillIds;
123
+ }
112
124
 
113
125
  const parsed = AgentRuntimeConfigSchema.partial().parse({
114
126
  ...merged,
@@ -131,7 +143,11 @@ export function normalizeRuntimeConfig(input: {
131
143
  runPolicy: {
132
144
  sandboxMode: parsed.runPolicy?.sandboxMode ?? "workspace_write",
133
145
  allowWebSearch: parsed.runPolicy?.allowWebSearch ?? false
134
- }
146
+ },
147
+ enabledSkillIds:
148
+ parsed.enabledSkillIds === null || parsed.enabledSkillIds === undefined
149
+ ? undefined
150
+ : companySkillAllowlistOnly(parsed.enabledSkillIds)
135
151
  };
136
152
  }
137
153
 
@@ -165,6 +181,8 @@ export function parseRuntimeConfigFromAgentRow(agent: Record<string, unknown>):
165
181
  runtimeModel,
166
182
  runtimeThinkingEffort: parseThinkingEffort(agent.runtimeThinkingEffort),
167
183
  bootstrapPrompt: toText(agent.bootstrapPrompt),
184
+ enabledSkillIds:
185
+ fallback.enabledSkillIds === undefined ? undefined : companySkillAllowlistOnly(fallback.enabledSkillIds),
168
186
  runtimeTimeoutSec: Math.max(0, timeoutSec),
169
187
  interruptGraceSec: Math.max(0, toNumber(agent.interruptGraceSec) ?? 15),
170
188
  runPolicy
@@ -187,14 +205,18 @@ export function runtimeConfigToDb(runtime: NormalizedRuntimeConfig) {
187
205
  }
188
206
 
189
207
  export function runtimeConfigToStateBlobPatch(runtime: NormalizedRuntimeConfig) {
208
+ const runtimePatch: Record<string, unknown> = {
209
+ command: runtime.runtimeCommand,
210
+ args: runtime.runtimeArgs,
211
+ cwd: runtime.runtimeCwd,
212
+ env: runtime.runtimeEnv,
213
+ timeoutMs: runtime.runtimeTimeoutSec > 0 ? runtime.runtimeTimeoutSec * 1000 : undefined
214
+ };
215
+ if (runtime.enabledSkillIds !== undefined) {
216
+ runtimePatch.enabledSkillIds = runtime.enabledSkillIds;
217
+ }
190
218
  return {
191
- runtime: {
192
- command: runtime.runtimeCommand,
193
- args: runtime.runtimeArgs,
194
- cwd: runtime.runtimeCwd,
195
- env: runtime.runtimeEnv,
196
- timeoutMs: runtime.runtimeTimeoutSec > 0 ? runtime.runtimeTimeoutSec * 1000 : undefined
197
- },
219
+ runtime: runtimePatch,
198
220
  promptTemplate: runtime.bootstrapPrompt
199
221
  };
200
222
  }
@@ -208,6 +230,7 @@ function parseRuntimeFromStateBlob(raw: unknown) {
208
230
  env?: Record<string, string>;
209
231
  model?: string;
210
232
  timeoutMs?: number;
233
+ enabledSkillIds?: string[];
211
234
  };
212
235
  }
213
236
  try {
@@ -219,6 +242,7 @@ function parseRuntimeFromStateBlob(raw: unknown) {
219
242
  env?: unknown;
220
243
  model?: unknown;
221
244
  timeoutMs?: unknown;
245
+ enabledSkillIds?: unknown;
222
246
  };
223
247
  };
224
248
  const runtime = parsed.runtime ?? {};
@@ -228,13 +252,37 @@ function parseRuntimeFromStateBlob(raw: unknown) {
228
252
  cwd: typeof runtime.cwd === "string" ? runtime.cwd : undefined,
229
253
  env: toRecord(runtime.env),
230
254
  model: typeof runtime.model === "string" && runtime.model.trim().length > 0 ? runtime.model.trim() : undefined,
231
- timeoutMs: toNumber(runtime.timeoutMs)
255
+ timeoutMs: toNumber(runtime.timeoutMs),
256
+ enabledSkillIds: parseEnabledSkillIdsFromState(runtime.enabledSkillIds)
232
257
  };
233
258
  } catch {
234
259
  return {};
235
260
  }
236
261
  }
237
262
 
263
+ function parseEnabledSkillIdsFromState(raw: unknown): string[] | undefined {
264
+ if (!Array.isArray(raw)) {
265
+ return undefined;
266
+ }
267
+ const out: string[] = [];
268
+ const seen = new Set<string>();
269
+ for (const entry of raw) {
270
+ if (typeof entry !== "string") {
271
+ continue;
272
+ }
273
+ const trimmed = entry.trim();
274
+ if (!trimmed || seen.has(trimmed)) {
275
+ continue;
276
+ }
277
+ seen.add(trimmed);
278
+ out.push(trimmed);
279
+ if (out.length >= 64) {
280
+ break;
281
+ }
282
+ }
283
+ return out;
284
+ }
285
+
238
286
  function parseStringArray(raw: unknown) {
239
287
  if (typeof raw !== "string") {
240
288
  return null;
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: bopodev-control-plane
3
+ description: >
4
+ Coordinate heartbeat work through a control-plane API: assignments, checkout,
5
+ comments, status updates, approvals, and delegation. Use this for orchestration
6
+ only, not domain implementation itself.
7
+ ---
8
+
9
+ # BopoDev Control Plane Skill
10
+
11
+ Use this skill when the agent must interact with the control plane for issue
12
+ coordination (assignment management, workflow state, and delegation).
13
+
14
+ Do not use it for coding implementation details; use normal local tooling for that.
15
+
16
+ ## Required context
17
+
18
+ Expected env variables:
19
+
20
+ - `BOPODEV_AGENT_ID`
21
+ - `BOPODEV_COMPANY_ID`
22
+ - `BOPODEV_RUN_ID`
23
+ - `BOPODEV_API_BASE_URL`
24
+ - `BOPODEV_REQUEST_HEADERS_JSON` (fallback JSON map for required request headers)
25
+ - `BOPODEV_ACTOR_TYPE`
26
+ - `BOPODEV_ACTOR_ID`
27
+ - `BOPODEV_ACTOR_COMPANIES`
28
+ - `BOPODEV_ACTOR_PERMISSIONS`
29
+
30
+ Wake context (optional):
31
+
32
+ - `BOPODEV_TASK_ID`
33
+ - `BOPODEV_WAKE_REASON`
34
+ - `BOPODEV_WAKE_COMMENT_ID`
35
+ - `BOPODEV_APPROVAL_ID`
36
+ - `BOPODEV_APPROVAL_STATUS`
37
+ - `BOPODEV_LINKED_ISSUE_IDS`
38
+
39
+ If control-plane connectivity is unavailable, do not attempt control-plane mutations.
40
+ Fail fast, report the connectivity gap once with the exact error, and avoid repeated retries in the same heartbeat run.
41
+
42
+ ## Heartbeat procedure
43
+
44
+ 1. Resolve identity from env: `BOPODEV_AGENT_ID`, `BOPODEV_COMPANY_ID`, `BOPODEV_RUN_ID`.
45
+ 2. If approval-related wake context exists (env or heartbeat prompt), process linked approvals first.
46
+ 3. Use assigned issues from heartbeat prompt as primary work queue.
47
+ 4. Prioritize `in_progress`, then `todo`; only revisit `blocked` with new context.
48
+ 5. Read issue comments for current context before mutating status.
49
+ 6. Do the work.
50
+ 7. Publish progress and update final state (`done`, `blocked`, `in_review`).
51
+ 8. Delegate through subtasks when decomposition is needed.
52
+
53
+ ## API usage pattern
54
+
55
+ All API routes are rooted at `BOPODEV_API_BASE_URL` (no `/api` prefix in this project).
56
+
57
+ Use direct env vars for request headers (preferred, deterministic):
58
+
59
+ - `x-company-id`
60
+ - `x-actor-type`
61
+ - `x-actor-id`
62
+ - `x-actor-companies`
63
+ - `x-actor-permissions`
64
+
65
+ Recommended curl header pattern (do not parse JSON first):
66
+
67
+ `curl -sS -H "x-company-id: $BOPODEV_COMPANY_ID" -H "x-actor-type: $BOPODEV_ACTOR_TYPE" -H "x-actor-id: $BOPODEV_ACTOR_ID" -H "x-actor-companies: $BOPODEV_ACTOR_COMPANIES" -H "x-actor-permissions: $BOPODEV_ACTOR_PERMISSIONS" ...`
68
+
69
+ Only use `BOPODEV_REQUEST_HEADERS_JSON` as compatibility fallback when direct vars are unavailable.
70
+
71
+ Prefer direct header flags from env when scripting requests. Do not assume `python` is installed.
72
+ If you need a JSON request body, write it to a temp file or heredoc and use `curl --data @file`
73
+ instead of hand-escaping multiline JSON in the shell.
74
+ The runtime shell is `zsh` on macOS. Avoid Bash-only features such as `local -n`,
75
+ `declare -n`, `mapfile`, and `readarray`.
76
+
77
+ When creating hires, set `requestApproval: true` by default (board-level bypass should be rare and explicit).
78
+
79
+ ## Critical safety rules
80
+
81
+ - Heartbeat-assigned issues may already be claimed by the current run. Do not call a
82
+ nonexistent checkout endpoint in this project.
83
+ - Never assume `POST /issues/{issueId}/checkout` exists here.
84
+ - Never assume `GET /agents/{agentId}` exists here.
85
+ - Never retry ownership conflicts (`409`).
86
+ - Never self-assign random work outside assignment/mention handoff rules.
87
+ - Always leave a useful progress or blocker comment before heartbeat exit.
88
+ - If blocked, update state to `blocked` and include a specific unblock path.
89
+ - Do not repeatedly post duplicate blocked comments when nothing changed.
90
+ - Escalate through reporting chain for cross-team blockers.
91
+ - Do not loop on repeated `curl` retries for the same failing endpoint in one run; include one precise failure message and exit.
92
+
93
+ ## Comment style
94
+
95
+ When adding comments, use concise markdown:
96
+
97
+ - one short status line
98
+ - bullet list of what changed or what is blocked
99
+ - links to related entities when available (issue/approval/agent/run)
100
+
101
+ ## Quick endpoint reference
102
+
103
+ | Action | Endpoint |
104
+ | --- | --- |
105
+ | List agents | `GET /agents` |
106
+ | Update agent | `PUT /agents/{agentId}` |
107
+ | Hire request (approval-backed) | `POST /agents` with `requestApproval: true` |
108
+ | List approvals | `GET /governance/approvals` |
109
+ | Read issue comments | `GET /issues/{issueId}/comments` |
110
+ | Add issue comment | `POST /issues/{issueId}/comments` |
111
+ | Update issue | `PUT /issues/{issueId}` |
112
+ | Create subtask issue | `POST /issues` |
113
+
114
+ ## Important route notes
115
+
116
+ - To inspect your own agent, use `GET /agents` and filter by id locally.
117
+ - `GET /agents` returns a wrapped envelope: `{ "ok": true, "data": [...] }`.
118
+ - Treat any non-envelope shape as a hard failure for this run.
119
+ - Recommended deterministic filter:
120
+ `jq -er --arg id "$BOPODEV_AGENT_ID" '.data | if type=="array" then . else error("invalid_agents_payload") end | map(select((.id? // "") == $id)) | .[0]'`
121
+ - Heartbeat runs already claim their assigned issues; move status with `PUT /issues/{issueId}`.
122
+ - For bootstrap prompt updates, prefer a top-level `bootstrapPrompt` field unless you need
123
+ other runtime settings in `runtimeConfig`.
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: bopodev-create-agent
3
+ description: >
4
+ Hire agents through governance-aware control-plane flow: inspect adapter
5
+ options, draft configuration, and submit approval-backed hire requests.
6
+ ---
7
+
8
+ # BopoDev Create Agent Skill
9
+
10
+ Use this skill when creating or revising agent hires in the control plane.
11
+
12
+ ## Preconditions
13
+
14
+ One of the following must be true:
15
+
16
+ - caller has board access, or
17
+ - caller has agent creation permission.
18
+
19
+ If permission is missing, escalate to a manager/board actor.
20
+
21
+ ## Standard workflow
22
+
23
+ 1. Confirm identity and active company context.
24
+ 2. Compare existing agent configurations from `GET /agents` for reusable patterns.
25
+ 3. Choose role, provider, reporting line, and runtime heartbeat profile.
26
+ 4. Draft agent prompt/instructions with role-scoped responsibilities.
27
+ 5. Set `capabilities` (required for every hire): a short plain-language line for the org chart and heartbeat team roster—what this agent does for delegation. If the request came from a delegated hiring issue, prefer `delegationIntent.requestedCapabilities` or the issue metadata `delegatedHiringIntent.requestedCapabilities` when present; otherwise write one from the role and brief.
28
+ 6. Submit hire request and capture approval linkage.
29
+ 7. Track approval state and post follow-up comments with links.
30
+ 8. On approval wake, close or update linked issues accordingly.
31
+
32
+ ## Payload checklist
33
+
34
+ Before submission, ensure payload includes:
35
+
36
+ - `name`
37
+ - `role`
38
+ - `providerType`
39
+ - `heartbeatCron`
40
+ - `monthlyBudgetUsd`
41
+ - optional `managerAgentId`
42
+ - optional `canHireAgents`
43
+ - `capabilities` (short description for org chart and team roster; include on every hire)
44
+ - optional `bootstrapPrompt` (extra standing instructions only; operating docs are injected via heartbeat env) or supported `runtimeConfig`
45
+ - `requestApproval` (defaults to `true`; keep `true` for routine hires)
46
+
47
+ Do not use unsupported fields such as:
48
+
49
+ - `adapterType`
50
+ - `adapterConfig`
51
+ - `reportsTo`
52
+ - arbitrary nested `runtimeConfig` keys outside the supported runtime contract
53
+
54
+ ## Minimal approved shape
55
+
56
+ For a Codex hire, prefer this shape:
57
+
58
+ ```json
59
+ {
60
+ "name": "Founding Engineer",
61
+ "role": "Founding Engineer",
62
+ "capabilities": "Ships product changes with tests, clear handoffs, and accurate issue updates.",
63
+ "providerType": "codex",
64
+ "managerAgentId": "<manager-agent-id>",
65
+ "heartbeatCron": "*/5 * * * *",
66
+ "monthlyBudgetUsd": 100,
67
+ "bootstrapPrompt": "Optional: prefer small PRs and note blockers in employee_comment.",
68
+ "requestApproval": true
69
+ }
70
+ ```
71
+
72
+ If you need multiline prompt text, write the JSON to a temp file or heredoc and submit with
73
+ `curl --data @file` instead of shell-escaping the body inline.
74
+ The runtime shell is `zsh` on macOS, so keep helper scripts POSIX/zsh-compatible and avoid
75
+ Bash-only features like `local -n`, `declare -n`, `mapfile`, and `readarray`.
76
+
77
+ ## Governance rules
78
+
79
+ - Use approval-backed hiring by default.
80
+ - Do not bypass governance for routine hires (only board-level operators should bypass intentionally).
81
+ - If board feedback requests revisions, resubmit with explicit deltas.
82
+ - Keep approval threads updated with issue/agent/approval links.
83
+
84
+ ## Quality bar
85
+
86
+ - Prefer proven adapter/runtime templates over one-off configs.
87
+ - Keep prompts operational and bounded to the role.
88
+ - Do not store plaintext secrets unless strictly required.
89
+ - Validate reporting chain and company ownership before submission.
90
+ - Use deterministic, auditable language in approval comments.
@@ -0,0 +1,36 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { BUILTIN_BOPO_SKILL_IDS } from "bopodev-contracts";
5
+
6
+ const DIR = dirname(fileURLToPath(import.meta.url));
7
+
8
+ export type BuiltinBopoSkill = {
9
+ id: string;
10
+ title: string;
11
+ content: string;
12
+ };
13
+
14
+ const BUILTIN_TITLES: Record<string, string> = {
15
+ "bopodev-control-plane": "Bopo control plane",
16
+ "bopodev-create-agent": "Bopo create agent",
17
+ "para-memory-files": "PARA memory files"
18
+ };
19
+
20
+ function readBundled(id: string, title: string): BuiltinBopoSkill {
21
+ try {
22
+ const content = readFileSync(join(DIR, `${id}.md`), "utf8");
23
+ return { id, title, content };
24
+ } catch {
25
+ return {
26
+ id,
27
+ title,
28
+ content: `# ${title}\n\nBuilt-in skill text is not available in this build (missing bundled copy of \`${id}.md\`).\n`
29
+ };
30
+ }
31
+ }
32
+
33
+ /** Injected into local agent runtimes alongside company `skills/`. Read-only in Settings UI. */
34
+ export const BUILTIN_BOPO_SKILLS: BuiltinBopoSkill[] = BUILTIN_BOPO_SKILL_IDS.map((id) =>
35
+ readBundled(id, BUILTIN_TITLES[id] ?? id)
36
+ );
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: para-memory-files
3
+ description: >
4
+ File-backed memory using PARA (Projects, Areas, Resources, Archives). Use for
5
+ persistent recall across sessions: durable facts, daily notes, and user habits.
6
+ ---
7
+
8
+ # PARA Memory Files
9
+
10
+ Use this skill whenever context must survive beyond the current runtime session.
11
+
12
+ ## Memory layers
13
+
14
+ 1. Knowledge graph (`life/`)
15
+ - entity folders with `summary.md` and `items.yaml`
16
+ - durable, queryable facts
17
+ 2. Daily notes (`memory/YYYY-MM-DD.md`)
18
+ - chronological event log
19
+ - temporary observations before curation
20
+ 3. Tacit memory (`MEMORY.md`)
21
+ - user preferences, work style, collaboration patterns
22
+
23
+ ## PARA organization
24
+
25
+ - `projects/`: active efforts with goals/deadlines
26
+ - `areas/`: ongoing responsibilities
27
+ - `resources/`: reusable reference knowledge
28
+ - `archives/`: inactive entities moved from other buckets
29
+
30
+ ## Operating rules
31
+
32
+ - Write durable facts immediately to `items.yaml`.
33
+ - Keep `summary.md` short and regenerate from active facts.
34
+ - Never delete facts; supersede with status and replacement reference.
35
+ - Move inactive entities to `archives` rather than removing them.
36
+ - Prefer writing to disk over relying on transient model context.
37
+
38
+ ## Recall workflow
39
+
40
+ 1. Capture raw event in daily note.
41
+ 2. Promote durable facts into entity files.
42
+ 3. Update entity summary from durable facts.
43
+ 4. Update tacit memory when user operating patterns become clear.
44
+
45
+ ## Planning notes
46
+
47
+ - Store shared plans in project `plans/` where collaborators can access them.
48
+ - Mark superseded plans to prevent stale guidance drift.
@@ -61,6 +61,11 @@ export function resolveCompanyProjectsWorkspacePath(companyId: string) {
61
61
  return join(resolveBopoInstanceRoot(), "workspaces", safeCompanyId);
62
62
  }
63
63
 
64
+ /** Company-managed runtime skills (`skills/<id>/SKILL.md`), exportable with company zip. */
65
+ export function resolveCompanySkillsPath(companyId: string) {
66
+ return join(resolveCompanyProjectsWorkspacePath(companyId), "skills");
67
+ }
68
+
64
69
  export function resolveAgentFallbackWorkspacePath(companyId: string, agentId: string) {
65
70
  const safeCompanyId = assertPathSegment(companyId, "companyId");
66
71
  const safeAgentId = assertPathSegment(agentId, "agentId");
@@ -62,7 +62,8 @@ const legacyRuntimeConfigSchema = z.object({
62
62
  sandboxMode: z.enum(["workspace_write", "full_access"]).optional(),
63
63
  allowWebSearch: z.boolean().optional()
64
64
  })
65
- .optional()
65
+ .optional(),
66
+ enabledSkillIds: z.array(z.string().min(1)).max(64).nullable().optional()
66
67
  });
67
68
 
68
69
  const createAgentSchema = AgentCreateRequestSchema.extend({
@@ -118,7 +119,8 @@ const UPDATE_AGENT_ALLOWED_KEYS = new Set([
118
119
  "runtimeTimeoutSec",
119
120
  "interruptGraceSec",
120
121
  "runtimeEnv",
121
- "runPolicy"
122
+ "runPolicy",
123
+ "enabledSkillIds"
122
124
  ]);
123
125
  const UPDATE_RUNTIME_CONFIG_ALLOWED_KEYS = new Set([
124
126
  "runtimeCommand",
@@ -130,15 +132,18 @@ const UPDATE_RUNTIME_CONFIG_ALLOWED_KEYS = new Set([
130
132
  "bootstrapPrompt",
131
133
  "runtimeTimeoutSec",
132
134
  "interruptGraceSec",
133
- "runPolicy"
135
+ "runPolicy",
136
+ "enabledSkillIds"
134
137
  ]);
135
138
 
136
139
  function toAgentResponse(agent: Record<string, unknown>) {
140
+ const rt = parseRuntimeConfigFromAgentRow(agent);
137
141
  return {
138
142
  ...agent,
139
143
  monthlyBudgetUsd:
140
144
  typeof agent.monthlyBudgetUsd === "number" ? agent.monthlyBudgetUsd : Number(agent.monthlyBudgetUsd ?? 0),
141
- usedBudgetUsd: typeof agent.usedBudgetUsd === "number" ? agent.usedBudgetUsd : Number(agent.usedBudgetUsd ?? 0)
145
+ usedBudgetUsd: typeof agent.usedBudgetUsd === "number" ? agent.usedBudgetUsd : Number(agent.usedBudgetUsd ?? 0),
146
+ enabledSkillIds: rt.enabledSkillIds === undefined ? null : rt.enabledSkillIds
142
147
  };
143
148
  }
144
149
 
@@ -400,7 +405,8 @@ export function createAgentsRouter(ctx: AppContext) {
400
405
  runtimeTimeoutSec: parsed.data.runtimeTimeoutSec,
401
406
  interruptGraceSec: parsed.data.interruptGraceSec,
402
407
  runtimeEnv: parsed.data.runtimeEnv,
403
- runPolicy: parsed.data.runPolicy
408
+ runPolicy: parsed.data.runPolicy,
409
+ enabledSkillIds: parsed.data.enabledSkillIds
404
410
  },
405
411
  defaultRuntimeCwd
406
412
  });
@@ -408,6 +414,12 @@ export function createAgentsRouter(ctx: AppContext) {
408
414
  } catch (error) {
409
415
  return sendError(res, String(error), 422);
410
416
  }
417
+ const rc = parsed.data.runtimeConfig;
418
+ const hasEnabledSkillIdsKey =
419
+ rc !== undefined && rc !== null && typeof rc === "object" && "enabledSkillIds" in rc;
420
+ if (!hasEnabledSkillIdsKey && parsed.data.enabledSkillIds === undefined) {
421
+ runtimeConfig = { ...runtimeConfig, enabledSkillIds: [] };
422
+ }
411
423
  runtimeConfig.runtimeModel = await resolveOpencodeRuntimeModel(parsed.data.providerType, runtimeConfig);
412
424
  runtimeConfig.runtimeModel = resolveRuntimeModelForProvider(parsed.data.providerType, runtimeConfig.runtimeModel);
413
425
  if (!ensureNamedRuntimeModel(parsed.data.providerType, runtimeConfig.runtimeModel)) {
@@ -560,7 +572,8 @@ export function createAgentsRouter(ctx: AppContext) {
560
572
  parsed.data.runtimeTimeoutSec !== undefined ||
561
573
  parsed.data.interruptGraceSec !== undefined ||
562
574
  parsed.data.runtimeEnv !== undefined ||
563
- parsed.data.runPolicy !== undefined;
575
+ parsed.data.runPolicy !== undefined ||
576
+ parsed.data.enabledSkillIds !== undefined;
564
577
  try {
565
578
  let nextRuntime = {
566
579
  ...existingRuntime,
@@ -581,7 +594,8 @@ export function createAgentsRouter(ctx: AppContext) {
581
594
  runtimeTimeoutSec: parsed.data.runtimeTimeoutSec ?? existingRuntime.runtimeTimeoutSec,
582
595
  interruptGraceSec: parsed.data.interruptGraceSec,
583
596
  runtimeEnv: parsed.data.runtimeEnv,
584
- runPolicy: parsed.data.runPolicy
597
+ runPolicy: parsed.data.runPolicy,
598
+ enabledSkillIds: parsed.data.enabledSkillIds
585
599
  }
586
600
  })
587
601
  : {})