bopodev-api 0.1.34 → 0.1.35
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 +5 -5
- package/src/app.ts +4 -2
- package/src/assets/starter-packs/customer-support-excellence.zip +0 -0
- package/src/assets/starter-packs/devrel-growth.zip +0 -0
- package/src/assets/starter-packs/product-delivery-trio.zip +0 -0
- package/src/assets/starter-packs/revenue-gtm-b2b.zip +0 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/.bopo.yaml +129 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/README.md +3 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/agents/founder-ceo/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-specialist/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/projects/knowledge-base/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/projects/quality/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/projects/queue/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/skills/kb-article-skeleton/SKILL.md +17 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/skills/ticket-response-playbook/SKILL.md +15 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/tasks/daily-queue-standup/TASK.md +11 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/tasks/kb-gap-sweep/TASK.md +11 -0
- package/src/assets/starter-packs/sources/devrel-growth/.bopo.yaml +128 -0
- package/src/assets/starter-packs/sources/devrel-growth/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/README.md +3 -0
- package/src/assets/starter-packs/sources/devrel-growth/agents/content-producer/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/devrel-growth/agents/devrel-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/devrel-growth/agents/founder-ceo/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/devrel-growth/projects/community/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/projects/docs-education/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/projects/partners/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/skills/changelog-to-post/SKILL.md +14 -0
- package/src/assets/starter-packs/sources/devrel-growth/skills/tutorial-outline/SKILL.md +15 -0
- package/src/assets/starter-packs/sources/devrel-growth/tasks/community-health-review/TASK.md +11 -0
- package/src/assets/starter-packs/sources/devrel-growth/tasks/weekly-content-plan/TASK.md +11 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/.bopo.yaml +138 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/README.md +9 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/agents/engineer-ic/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/agents/founder-ceo/HEARTBEAT.md +6 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/agents/product-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/projects/delivery/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/projects/quality/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/projects/strategy/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/skills/issue-triage/SKILL.md +21 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/skills/rca-template/SKILL.md +16 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/tasks/release-hygiene/TASK.md +11 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/tasks/weekly-leadership-sync/TASK.md +11 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/.bopo.yaml +132 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/README.md +3 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/founder-ceo/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/gtm-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/pipeline-owner/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/customer-success/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/deals/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/pipeline/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/discovery-call-brief/SKILL.md +14 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/icp-scoring/SKILL.md +20 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/pipeline-hygiene/TASK.md +11 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/weekly-revenue-review/TASK.md +11 -0
- package/src/lib/agent-issue-permissions.ts +56 -0
- package/src/lib/builtin-bopo-skills/bopodev-control-plane.md +7 -0
- package/src/realtime/office-space.ts +7 -0
- package/src/routes/agents.ts +23 -1
- package/src/routes/assistant.ts +40 -1
- package/src/routes/companies.ts +227 -15
- package/src/routes/issues.ts +48 -0
- package/src/routes/plugins.ts +393 -103
- package/src/routes/{loops.ts → routines.ts} +72 -76
- package/src/scripts/onboard-seed.ts +2 -0
- package/src/server.ts +3 -1
- package/src/services/company-assistant-context-snapshot.ts +4 -2
- package/src/services/company-assistant-service.ts +17 -15
- package/src/services/company-file-archive-service.ts +56 -3
- package/src/services/company-file-import-service.ts +210 -31
- package/src/services/governance-service.ts +58 -3
- package/src/services/heartbeat-service/heartbeat-run.ts +7 -0
- package/src/services/plugin-artifact-installer.ts +115 -0
- package/src/services/plugin-artifact-store.ts +28 -0
- package/src/services/plugin-capability-policy.ts +31 -0
- package/src/services/plugin-jobs-service.ts +74 -0
- package/src/services/plugin-manifest-loader.ts +78 -3
- package/src/services/plugin-rpc.ts +102 -0
- package/src/services/plugin-runtime.ts +240 -209
- package/src/services/plugin-worker-host.ts +167 -0
- package/src/services/starter-pack-registry.ts +68 -0
- package/src/services/template-apply-service.ts +3 -1
- package/src/services/template-catalog.ts +29 -0
- package/src/services/work-loop-service/work-loop-service.ts +18 -18
- package/src/shutdown/graceful-shutdown.ts +3 -1
- package/src/worker/scheduler.ts +21 -1
- package/src/services/company-export-service.ts +0 -63
|
@@ -4,7 +4,7 @@ import { unzipSync } from "fflate";
|
|
|
4
4
|
import { parse as yamlParse } from "yaml";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import type { BopoDb } from "bopodev-db";
|
|
7
|
-
import { createAgent, createCompany, createProject } from "bopodev-db";
|
|
7
|
+
import { createAgent, createCompany, createGoal, createProject, updateAgent, updateGoal } from "bopodev-db";
|
|
8
8
|
import { normalizeRuntimeConfig, runtimeConfigToDb, runtimeConfigToStateBlobPatch } from "../lib/agent-config";
|
|
9
9
|
import {
|
|
10
10
|
resolveAgentMemoryRootPath,
|
|
@@ -16,16 +16,22 @@ import { ensureBuiltinPluginsRegistered } from "./plugin-runtime";
|
|
|
16
16
|
import { ensureCompanyBuiltinTemplateDefaults } from "./template-catalog";
|
|
17
17
|
import { addWorkLoopTrigger, createWorkLoop } from "./work-loop-service/work-loop-service";
|
|
18
18
|
|
|
19
|
-
const EXPORT_SCHEMA = "bopo/company-export/v1";
|
|
19
|
+
export const EXPORT_SCHEMA = "bopo/company-export/v1";
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const goalLevelSchema = z.enum(["company", "project", "agent"]);
|
|
22
|
+
const goalStatusSchema = z.enum(["draft", "active", "completed", "archived"]);
|
|
23
|
+
|
|
24
|
+
export const BopoExportYamlSchema = z.object({
|
|
22
25
|
schema: z.string(),
|
|
23
26
|
company: z.object({
|
|
24
27
|
name: z.string().min(1),
|
|
25
28
|
mission: z.string().nullable().optional(),
|
|
26
29
|
slug: z.string().optional()
|
|
27
30
|
}),
|
|
28
|
-
projects: z.record(
|
|
31
|
+
projects: z.record(
|
|
32
|
+
z.string(),
|
|
33
|
+
z.object({ name: z.string().min(1), description: z.string().nullable().optional(), status: z.string().optional() })
|
|
34
|
+
),
|
|
29
35
|
agents: z.record(
|
|
30
36
|
z.string(),
|
|
31
37
|
z.object({
|
|
@@ -37,9 +43,27 @@ const BopoExportYamlSchema = z.object({
|
|
|
37
43
|
managerSlug: z.string().nullable().optional(),
|
|
38
44
|
providerType: z.string().min(1),
|
|
39
45
|
heartbeatCron: z.string().min(1),
|
|
40
|
-
canHireAgents: z.boolean().optional()
|
|
46
|
+
canHireAgents: z.boolean().optional(),
|
|
47
|
+
canAssignAgents: z.boolean().optional(),
|
|
48
|
+
canCreateIssues: z.boolean().optional(),
|
|
49
|
+
bootstrapPrompt: z.string().nullable().optional(),
|
|
50
|
+
monthlyBudgetUsd: z.union([z.string(), z.number()]).optional()
|
|
41
51
|
})
|
|
42
52
|
),
|
|
53
|
+
goals: z
|
|
54
|
+
.record(
|
|
55
|
+
z.string(),
|
|
56
|
+
z.object({
|
|
57
|
+
level: goalLevelSchema,
|
|
58
|
+
title: z.string().min(1),
|
|
59
|
+
description: z.string().nullable().optional(),
|
|
60
|
+
status: goalStatusSchema.optional(),
|
|
61
|
+
projectSlug: z.string().nullable().optional(),
|
|
62
|
+
parentGoalSlug: z.string().nullable().optional(),
|
|
63
|
+
ownerAgentSlug: z.string().nullable().optional()
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
.optional(),
|
|
43
67
|
routines: z
|
|
44
68
|
.record(
|
|
45
69
|
z.string(),
|
|
@@ -62,6 +86,13 @@ const BopoExportYamlSchema = z.object({
|
|
|
62
86
|
.optional()
|
|
63
87
|
});
|
|
64
88
|
|
|
89
|
+
export type BopoExportDoc = z.infer<typeof BopoExportYamlSchema>;
|
|
90
|
+
|
|
91
|
+
export type ParsedCompanyPackage = {
|
|
92
|
+
doc: BopoExportDoc;
|
|
93
|
+
entries: Record<string, string>;
|
|
94
|
+
};
|
|
95
|
+
|
|
65
96
|
export class CompanyFileImportError extends Error {
|
|
66
97
|
constructor(message: string) {
|
|
67
98
|
super(message);
|
|
@@ -77,7 +108,7 @@ function normalizeZipPath(key: string): string | null {
|
|
|
77
108
|
return t;
|
|
78
109
|
}
|
|
79
110
|
|
|
80
|
-
function decodeZipEntries(buffer: Buffer): Record<string, string> {
|
|
111
|
+
export function decodeZipEntries(buffer: Buffer): Record<string, string> {
|
|
81
112
|
let raw: Record<string, Uint8Array>;
|
|
82
113
|
try {
|
|
83
114
|
raw = unzipSync(new Uint8Array(buffer));
|
|
@@ -99,6 +130,30 @@ function decodeZipEntries(buffer: Buffer): Record<string, string> {
|
|
|
99
130
|
return out;
|
|
100
131
|
}
|
|
101
132
|
|
|
133
|
+
/** Parse and validate a company zip; throws CompanyFileImportError on failure. */
|
|
134
|
+
export function parseCompanyZipBuffer(buffer: Buffer): ParsedCompanyPackage {
|
|
135
|
+
const entries = decodeZipEntries(buffer);
|
|
136
|
+
const yamlText = entries[".bopo.yaml"] ?? entries["bopo.yaml"];
|
|
137
|
+
if (!yamlText?.trim()) {
|
|
138
|
+
throw new CompanyFileImportError('Zip must contain a ".bopo.yaml" manifest at the archive root.');
|
|
139
|
+
}
|
|
140
|
+
let parsedYaml: unknown;
|
|
141
|
+
try {
|
|
142
|
+
parsedYaml = yamlParse(yamlText);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new CompanyFileImportError(".bopo.yaml is not valid YAML.");
|
|
145
|
+
}
|
|
146
|
+
const parsed = BopoExportYamlSchema.safeParse(parsedYaml);
|
|
147
|
+
if (!parsed.success) {
|
|
148
|
+
throw new CompanyFileImportError(`Invalid export manifest: ${parsed.error.message}`);
|
|
149
|
+
}
|
|
150
|
+
const doc = parsed.data;
|
|
151
|
+
if (doc.schema !== EXPORT_SCHEMA) {
|
|
152
|
+
throw new CompanyFileImportError(`Unsupported export schema '${doc.schema}' (expected ${EXPORT_SCHEMA}).`);
|
|
153
|
+
}
|
|
154
|
+
return { doc, entries };
|
|
155
|
+
}
|
|
156
|
+
|
|
102
157
|
const PROVIDER_TYPES = new Set([
|
|
103
158
|
"claude_code",
|
|
104
159
|
"codex",
|
|
@@ -118,36 +173,60 @@ function coerceProviderType(raw: string): AgentProvider {
|
|
|
118
173
|
return PROVIDER_TYPES.has(raw) ? (raw as AgentProvider) : "shell";
|
|
119
174
|
}
|
|
120
175
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!yamlText?.trim()) {
|
|
125
|
-
throw new CompanyFileImportError('Zip must contain a ".bopo.yaml" manifest at the archive root.');
|
|
176
|
+
function formatMonthlyBudgetUsdFromManifest(raw: string | number | undefined): string {
|
|
177
|
+
if (raw === undefined) {
|
|
178
|
+
return "100.0000";
|
|
126
179
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
parsedYaml = yamlParse(yamlText);
|
|
130
|
-
} catch {
|
|
131
|
-
throw new CompanyFileImportError(".bopo.yaml is not valid YAML.");
|
|
180
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
181
|
+
return raw.toFixed(4);
|
|
132
182
|
}
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
135
|
-
|
|
183
|
+
const s = String(raw).trim();
|
|
184
|
+
if (!s) {
|
|
185
|
+
return "100.0000";
|
|
136
186
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
187
|
+
const n = Number(s);
|
|
188
|
+
return Number.isFinite(n) ? n.toFixed(4) : "100.0000";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function sortGoalSlugsForImport(
|
|
192
|
+
goals: Record<string, { parentGoalSlug?: string | null | undefined }>
|
|
193
|
+
): string[] {
|
|
194
|
+
const slugs = Object.keys(goals);
|
|
195
|
+
const slugSet = new Set(slugs);
|
|
196
|
+
const remaining = new Set(slugs);
|
|
197
|
+
const order: string[] = [];
|
|
198
|
+
while (remaining.size > 0) {
|
|
199
|
+
const ready = [...remaining].filter((slug) => {
|
|
200
|
+
const p = goals[slug]?.parentGoalSlug?.trim();
|
|
201
|
+
if (!p) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (!slugSet.has(p)) {
|
|
205
|
+
throw new CompanyFileImportError(`Goal '${slug}' references unknown parent goal slug '${p}'.`);
|
|
206
|
+
}
|
|
207
|
+
return !remaining.has(p);
|
|
208
|
+
});
|
|
209
|
+
if (ready.length === 0) {
|
|
210
|
+
throw new CompanyFileImportError("Circular goal parent chain in manifest.");
|
|
211
|
+
}
|
|
212
|
+
ready.sort((a, b) => a.localeCompare(b));
|
|
213
|
+
for (const s of ready) {
|
|
214
|
+
order.push(s);
|
|
215
|
+
remaining.delete(s);
|
|
216
|
+
}
|
|
140
217
|
}
|
|
218
|
+
return order;
|
|
219
|
+
}
|
|
141
220
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Seeds projects, agents, workspace files, goals, and routines for an existing company.
|
|
223
|
+
* Does not create the company row or call ensureCompanyBuiltinTemplateDefaults.
|
|
224
|
+
*/
|
|
225
|
+
export async function seedOperationalDataFromPackage(db: BopoDb, companyId: string, parsed: ParsedCompanyPackage): Promise<void> {
|
|
226
|
+
const { doc, entries } = parsed;
|
|
147
227
|
const cwd = await resolveDefaultRuntimeCwdForCompany(db, companyId);
|
|
148
228
|
await mkdir(cwd, { recursive: true });
|
|
149
229
|
await ensureBuiltinPluginsRegistered(db, [companyId]);
|
|
150
|
-
await ensureCompanyBuiltinTemplateDefaults(db, companyId);
|
|
151
230
|
|
|
152
231
|
const projectSlugToId = new Map<string, string>();
|
|
153
232
|
const projectStatuses = new Set(["planned", "active", "paused", "blocked", "completed", "archived"]);
|
|
@@ -183,7 +262,11 @@ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Pr
|
|
|
183
262
|
}
|
|
184
263
|
const defaultRt = normalizeRuntimeConfig({
|
|
185
264
|
defaultRuntimeCwd: cwd,
|
|
186
|
-
runtimeConfig: {
|
|
265
|
+
runtimeConfig: {
|
|
266
|
+
runtimeModel: undefined,
|
|
267
|
+
runtimeEnv: {},
|
|
268
|
+
bootstrapPrompt: a.bootstrapPrompt?.trim() || undefined
|
|
269
|
+
}
|
|
187
270
|
});
|
|
188
271
|
const createdAgent = await createAgent(db, {
|
|
189
272
|
companyId,
|
|
@@ -195,8 +278,10 @@ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Pr
|
|
|
195
278
|
name: a.name,
|
|
196
279
|
providerType: coerceProviderType(a.providerType),
|
|
197
280
|
heartbeatCron: a.heartbeatCron,
|
|
198
|
-
monthlyBudgetUsd:
|
|
281
|
+
monthlyBudgetUsd: formatMonthlyBudgetUsdFromManifest(a.monthlyBudgetUsd),
|
|
199
282
|
canHireAgents: a.canHireAgents ?? false,
|
|
283
|
+
canAssignAgents: a.canAssignAgents ?? true,
|
|
284
|
+
canCreateIssues: a.canCreateIssues ?? true,
|
|
200
285
|
...runtimeConfigToDb(defaultRt),
|
|
201
286
|
initialState: runtimeConfigToStateBlobPatch(defaultRt)
|
|
202
287
|
});
|
|
@@ -246,6 +331,54 @@ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Pr
|
|
|
246
331
|
}
|
|
247
332
|
}
|
|
248
333
|
|
|
334
|
+
const goalsManifest = doc.goals ?? {};
|
|
335
|
+
const goalSlugToId = new Map<string, string>();
|
|
336
|
+
for (const slug of sortGoalSlugsForImport(goalsManifest)) {
|
|
337
|
+
const g = goalsManifest[slug]!;
|
|
338
|
+
const level = g.level;
|
|
339
|
+
const projectSlug = g.projectSlug?.trim() || null;
|
|
340
|
+
const projectId = projectSlug ? projectSlugToId.get(projectSlug) ?? null : null;
|
|
341
|
+
if (level === "project" && !projectId) {
|
|
342
|
+
throw new CompanyFileImportError(`Goal '${slug}' (project level) references unknown project slug '${projectSlug ?? ""}'.`);
|
|
343
|
+
}
|
|
344
|
+
if (level === "company" && projectId) {
|
|
345
|
+
throw new CompanyFileImportError(`Goal '${slug}' is company-level but specifies a project.`);
|
|
346
|
+
}
|
|
347
|
+
const parentSlug = g.parentGoalSlug?.trim() || null;
|
|
348
|
+
const parentGoalId = parentSlug ? goalSlugToId.get(parentSlug) ?? null : null;
|
|
349
|
+
if (parentSlug && !parentGoalId) {
|
|
350
|
+
throw new CompanyFileImportError(`Goal '${slug}' references unknown parent goal slug '${parentSlug}'.`);
|
|
351
|
+
}
|
|
352
|
+
const ownerSlug = g.ownerAgentSlug?.trim() || null;
|
|
353
|
+
const ownerAgentId = ownerSlug ? agentSlugToId.get(ownerSlug) ?? null : null;
|
|
354
|
+
if (ownerSlug && !ownerAgentId) {
|
|
355
|
+
throw new CompanyFileImportError(`Goal '${slug}' references unknown owner agent slug '${ownerSlug}'.`);
|
|
356
|
+
}
|
|
357
|
+
const agentLevelProjectId = level === "agent" ? projectId : null;
|
|
358
|
+
if (level === "agent" && g.projectSlug?.trim() && !projectId) {
|
|
359
|
+
throw new CompanyFileImportError(`Goal '${slug}' (agent level) references unknown project slug '${g.projectSlug.trim()}'.`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const created = await createGoal(db, {
|
|
363
|
+
companyId,
|
|
364
|
+
projectId: level === "project" ? projectId : agentLevelProjectId,
|
|
365
|
+
parentGoalId,
|
|
366
|
+
ownerAgentId,
|
|
367
|
+
level,
|
|
368
|
+
title: g.title,
|
|
369
|
+
description: g.description?.trim() || undefined
|
|
370
|
+
});
|
|
371
|
+
goalSlugToId.set(slug, created.id);
|
|
372
|
+
const st = g.status?.trim();
|
|
373
|
+
if (st && st !== "draft") {
|
|
374
|
+
await updateGoal(db, {
|
|
375
|
+
companyId,
|
|
376
|
+
id: created.id,
|
|
377
|
+
status: st
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
249
382
|
const routines = doc.routines ?? {};
|
|
250
383
|
for (const [, r] of Object.entries(routines)) {
|
|
251
384
|
const projectId = projectSlugToId.get(r.projectSlug);
|
|
@@ -266,7 +399,7 @@ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Pr
|
|
|
266
399
|
for (const t of r.triggers) {
|
|
267
400
|
await addWorkLoopTrigger(db, {
|
|
268
401
|
companyId,
|
|
269
|
-
|
|
402
|
+
routineId: loop.id,
|
|
270
403
|
cronExpression: t.cronExpression,
|
|
271
404
|
timezone: t.timezone?.trim() || "UTC",
|
|
272
405
|
label: t.label ?? null,
|
|
@@ -274,6 +407,52 @@ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Pr
|
|
|
274
407
|
});
|
|
275
408
|
}
|
|
276
409
|
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function assertManifestHasCeoAgent(doc: BopoExportDoc): void {
|
|
413
|
+
const hasCeo = Object.values(doc.agents).some((a) => (a.roleKey ?? "").trim().toLowerCase() === "ceo");
|
|
414
|
+
if (!hasCeo) {
|
|
415
|
+
throw new CompanyFileImportError("Company package must include an agent with roleKey 'ceo'.");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function summarizeCompanyPackageForPreview(parsed: ParsedCompanyPackage): {
|
|
420
|
+
companyName: string;
|
|
421
|
+
counts: {
|
|
422
|
+
projects: number;
|
|
423
|
+
agents: number;
|
|
424
|
+
goals: number;
|
|
425
|
+
routines: number;
|
|
426
|
+
skillFiles: number;
|
|
427
|
+
};
|
|
428
|
+
hasCeo: boolean;
|
|
429
|
+
} {
|
|
430
|
+
const doc = parsed.doc;
|
|
431
|
+
const skillFiles = Object.keys(parsed.entries).filter((k) => k.startsWith("skills/") && !k.endsWith("/")).length;
|
|
432
|
+
const hasCeo = Object.values(doc.agents).some((a) => (a.roleKey ?? "").trim().toLowerCase() === "ceo");
|
|
433
|
+
return {
|
|
434
|
+
companyName: doc.company.name,
|
|
435
|
+
counts: {
|
|
436
|
+
projects: Object.keys(doc.projects).length,
|
|
437
|
+
agents: Object.keys(doc.agents).length,
|
|
438
|
+
goals: Object.keys(doc.goals ?? {}).length,
|
|
439
|
+
routines: Object.keys(doc.routines ?? {}).length,
|
|
440
|
+
skillFiles
|
|
441
|
+
},
|
|
442
|
+
hasCeo
|
|
443
|
+
};
|
|
444
|
+
}
|
|
277
445
|
|
|
446
|
+
export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Promise<{ companyId: string; name: string }> {
|
|
447
|
+
const parsed = parseCompanyZipBuffer(buffer);
|
|
448
|
+
const doc = parsed.doc;
|
|
449
|
+
|
|
450
|
+
const created = await createCompany(db, {
|
|
451
|
+
name: doc.company.name,
|
|
452
|
+
mission: doc.company.mission ?? null
|
|
453
|
+
});
|
|
454
|
+
const companyId = created.id;
|
|
455
|
+
await ensureCompanyBuiltinTemplateDefaults(db, companyId);
|
|
456
|
+
await seedOperationalDataFromPackage(db, companyId, parsed);
|
|
278
457
|
return { companyId, name: doc.company.name };
|
|
279
458
|
}
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
AGENT_ROLE_LABELS,
|
|
5
5
|
AgentCreateRequestSchema,
|
|
6
6
|
AgentRoleKeySchema,
|
|
7
|
+
PluginInstallSourceTypeSchema,
|
|
8
|
+
PluginManifestV2Schema,
|
|
7
9
|
TemplateManifestDefault,
|
|
8
10
|
TemplateManifestSchema
|
|
9
11
|
} from "bopodev-contracts";
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
approvalRequests,
|
|
14
16
|
agents,
|
|
15
17
|
appendAuditEvent,
|
|
18
|
+
appendPluginInstall,
|
|
16
19
|
createAgent,
|
|
17
20
|
createGoal,
|
|
18
21
|
createIssue,
|
|
@@ -28,6 +31,7 @@ import {
|
|
|
28
31
|
projects,
|
|
29
32
|
eq,
|
|
30
33
|
updateProjectWorkspace,
|
|
34
|
+
markPluginInstallsSuperseded,
|
|
31
35
|
updatePluginConfig
|
|
32
36
|
} from "bopodev-db";
|
|
33
37
|
import {
|
|
@@ -44,6 +48,8 @@ import {
|
|
|
44
48
|
} from "../lib/instance-paths";
|
|
45
49
|
import { assertRuntimeCwdForCompany, hasText, resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
46
50
|
import { appendDurableFact } from "./memory-file-service";
|
|
51
|
+
import { writePackagedPluginManifestToFilesystem } from "./plugin-manifest-loader";
|
|
52
|
+
import { registerPluginManifest } from "./plugin-runtime";
|
|
47
53
|
import { applyTemplateManifest } from "./template-apply-service";
|
|
48
54
|
|
|
49
55
|
const approvalGatedActions = new Set([
|
|
@@ -97,7 +103,14 @@ const grantPluginCapabilitiesPayloadSchema = z.object({
|
|
|
97
103
|
enabled: z.boolean().optional(),
|
|
98
104
|
priority: z.number().int().min(0).max(1000).optional(),
|
|
99
105
|
grantedCapabilities: z.array(z.string().min(1)).default([]),
|
|
100
|
-
|
|
106
|
+
capabilityNamespaces: z.array(z.string().min(1)).default([]),
|
|
107
|
+
config: z.record(z.string(), z.unknown()).default({}),
|
|
108
|
+
sourceType: PluginInstallSourceTypeSchema.optional(),
|
|
109
|
+
sourceRef: z.string().optional(),
|
|
110
|
+
integrity: z.string().optional(),
|
|
111
|
+
buildHash: z.string().optional(),
|
|
112
|
+
manifestJson: z.string().optional(),
|
|
113
|
+
install: z.boolean().default(true)
|
|
101
114
|
});
|
|
102
115
|
const applyTemplatePayloadSchema = z.object({
|
|
103
116
|
templateId: z.string().min(1),
|
|
@@ -311,6 +324,8 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
|
|
|
311
324
|
heartbeatCron: parsed.data.heartbeatCron,
|
|
312
325
|
monthlyBudgetUsd: parsed.data.monthlyBudgetUsd.toFixed(4),
|
|
313
326
|
canHireAgents: parsed.data.canHireAgents,
|
|
327
|
+
canAssignAgents: parsed.data.canAssignAgents,
|
|
328
|
+
canCreateIssues: parsed.data.canCreateIssues,
|
|
314
329
|
...runtimeConfigToDb(runtimeConfig),
|
|
315
330
|
initialState: runtimeConfigToStateBlobPatch(runtimeConfig)
|
|
316
331
|
});
|
|
@@ -556,13 +571,52 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
|
|
|
556
571
|
if (!parsed.success) {
|
|
557
572
|
throw new GovernanceError("Approval payload for plugin capability grant is invalid.");
|
|
558
573
|
}
|
|
574
|
+
if (parsed.data.manifestJson) {
|
|
575
|
+
let rawManifest: unknown;
|
|
576
|
+
try {
|
|
577
|
+
rawManifest = JSON.parse(parsed.data.manifestJson);
|
|
578
|
+
} catch {
|
|
579
|
+
throw new GovernanceError("Plugin install manifest JSON is invalid.");
|
|
580
|
+
}
|
|
581
|
+
const manifestParsed = PluginManifestV2Schema.safeParse(rawManifest);
|
|
582
|
+
if (!manifestParsed.success) {
|
|
583
|
+
throw new GovernanceError("Plugin install manifest payload failed validation.");
|
|
584
|
+
}
|
|
585
|
+
await writePackagedPluginManifestToFilesystem(manifestParsed.data, {
|
|
586
|
+
sourceType: parsed.data.sourceType ?? "registry",
|
|
587
|
+
sourceRef: parsed.data.sourceRef,
|
|
588
|
+
integrity: parsed.data.integrity,
|
|
589
|
+
buildHash: parsed.data.buildHash
|
|
590
|
+
});
|
|
591
|
+
await registerPluginManifest(db, manifestParsed.data);
|
|
592
|
+
await markPluginInstallsSuperseded(db, {
|
|
593
|
+
companyId,
|
|
594
|
+
pluginId: parsed.data.pluginId
|
|
595
|
+
});
|
|
596
|
+
await appendPluginInstall(db, {
|
|
597
|
+
companyId,
|
|
598
|
+
pluginId: parsed.data.pluginId,
|
|
599
|
+
pluginVersion: manifestParsed.data.version,
|
|
600
|
+
sourceType: parsed.data.sourceType ?? "registry",
|
|
601
|
+
sourceRef: parsed.data.sourceRef ?? null,
|
|
602
|
+
integrity: parsed.data.integrity ?? null,
|
|
603
|
+
buildHash: parsed.data.buildHash ?? null,
|
|
604
|
+
artifactPath: manifestParsed.data.install?.artifactPath ?? null,
|
|
605
|
+
manifestJson: JSON.stringify(manifestParsed.data),
|
|
606
|
+
status: "active"
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
const configWithNamespaces = {
|
|
610
|
+
...parsed.data.config,
|
|
611
|
+
_grantedCapabilityNamespaces: parsed.data.capabilityNamespaces
|
|
612
|
+
};
|
|
559
613
|
await updatePluginConfig(db, {
|
|
560
614
|
companyId,
|
|
561
615
|
pluginId: parsed.data.pluginId,
|
|
562
616
|
enabled: parsed.data.enabled,
|
|
563
617
|
priority: parsed.data.priority,
|
|
564
618
|
grantedCapabilitiesJson: JSON.stringify(parsed.data.grantedCapabilities),
|
|
565
|
-
configJson: JSON.stringify(
|
|
619
|
+
configJson: JSON.stringify(configWithNamespaces)
|
|
566
620
|
});
|
|
567
621
|
return {
|
|
568
622
|
applied: true,
|
|
@@ -572,7 +626,8 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
|
|
|
572
626
|
pluginId: parsed.data.pluginId,
|
|
573
627
|
enabled: parsed.data.enabled ?? null,
|
|
574
628
|
priority: parsed.data.priority ?? null,
|
|
575
|
-
grantedCapabilities: parsed.data.grantedCapabilities
|
|
629
|
+
grantedCapabilities: parsed.data.grantedCapabilities,
|
|
630
|
+
capabilityNamespaces: parsed.data.capabilityNamespaces
|
|
576
631
|
}
|
|
577
632
|
};
|
|
578
633
|
}
|
|
@@ -723,6 +723,8 @@ export async function runHeartbeatForAgent(
|
|
|
723
723
|
agentId: agent.id,
|
|
724
724
|
heartbeatRunId: runId,
|
|
725
725
|
canHireAgents: agent.canHireAgents,
|
|
726
|
+
canAssignAgents: agent.canAssignAgents ?? true,
|
|
727
|
+
canCreateIssues: agent.canCreateIssues ?? true,
|
|
726
728
|
wakeContext: options?.wakeContext
|
|
727
729
|
}),
|
|
728
730
|
...(linkedMaterialized.root ? { BOPODEV_MATERIALIZED_LINKED_SKILLS_ROOT: linkedMaterialized.root } : {})
|
|
@@ -4077,6 +4079,8 @@ function buildHeartbeatRuntimeEnv(input: {
|
|
|
4077
4079
|
agentId: string;
|
|
4078
4080
|
heartbeatRunId: string;
|
|
4079
4081
|
canHireAgents: boolean;
|
|
4082
|
+
canAssignAgents: boolean;
|
|
4083
|
+
canCreateIssues: boolean;
|
|
4080
4084
|
wakeContext?: HeartbeatWakeContext;
|
|
4081
4085
|
}) {
|
|
4082
4086
|
const companyWorkspaceRoot = resolveCompanyWorkspaceRootPath(input.companyId);
|
|
@@ -4085,6 +4089,7 @@ function buildHeartbeatRuntimeEnv(input: {
|
|
|
4085
4089
|
const apiBaseUrl = resolveControlPlaneApiBaseUrl();
|
|
4086
4090
|
// agents:write is required for PUT /agents/:self (bootstrapPrompt, runtimeConfig). Route handlers
|
|
4087
4091
|
// still forbid agents from updating other agents' rows and from POST /agents unless canHireAgents.
|
|
4092
|
+
// Issue create/assignee changes for agent actors are gated by canCreateIssues / canAssignAgents (see issues routes).
|
|
4088
4093
|
const actorPermissions = ["issues:write", "agents:write"].join(",");
|
|
4089
4094
|
const actorHeaders = JSON.stringify({
|
|
4090
4095
|
"x-company-id": input.companyId,
|
|
@@ -4115,6 +4120,8 @@ function buildHeartbeatRuntimeEnv(input: {
|
|
|
4115
4120
|
BOPODEV_REQUEST_HEADERS_JSON: actorHeaders,
|
|
4116
4121
|
BOPODEV_REQUEST_APPROVAL_DEFAULT: "true",
|
|
4117
4122
|
BOPODEV_CAN_HIRE_AGENTS: input.canHireAgents ? "true" : "false",
|
|
4123
|
+
BOPODEV_CAN_ASSIGN_AGENTS: input.canAssignAgents ? "true" : "false",
|
|
4124
|
+
BOPODEV_CAN_CREATE_ISSUES: input.canCreateIssues ? "true" : "false",
|
|
4118
4125
|
...(input.wakeContext?.reason ? { BOPODEV_WAKE_REASON: input.wakeContext.reason } : {}),
|
|
4119
4126
|
...(input.wakeContext?.commentId ? { BOPODEV_WAKE_COMMENT_ID: input.wakeContext.commentId } : {}),
|
|
4120
4127
|
...(input.wakeContext?.issueIds?.length ? { BOPODEV_LINKED_ISSUE_IDS: input.wakeContext.issueIds.join(",") } : {}),
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { PluginManifestV2Schema, type PluginManifestV2 } from "bopodev-contracts";
|
|
8
|
+
import { buildPluginArtifactInstallDir, ensurePluginArtifactsDir, sanitizeArtifactSegment } from "./plugin-artifact-store";
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
type NpmPackResultRow = {
|
|
13
|
+
filename: string;
|
|
14
|
+
integrity?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function installPluginArtifactFromNpm(input: { packageName: string; version?: string }) {
|
|
18
|
+
const packageRef = input.version?.trim() ? `${input.packageName}@${input.version.trim()}` : input.packageName.trim();
|
|
19
|
+
if (!packageRef) {
|
|
20
|
+
throw new Error("packageName is required.");
|
|
21
|
+
}
|
|
22
|
+
const artifactsRoot = await ensurePluginArtifactsDir();
|
|
23
|
+
const tempRoot = await mkdtemp(resolve(tmpdir(), "bopo-plugin-pack-"));
|
|
24
|
+
try {
|
|
25
|
+
const { stdout } = await execFileAsync("npm", ["pack", packageRef, "--json"], {
|
|
26
|
+
cwd: tempRoot,
|
|
27
|
+
maxBuffer: 5 * 1024 * 1024
|
|
28
|
+
});
|
|
29
|
+
const parsed = JSON.parse(stdout.trim()) as NpmPackResultRow[] | NpmPackResultRow;
|
|
30
|
+
const row = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
31
|
+
if (!row?.filename) {
|
|
32
|
+
throw new Error("npm pack did not return a filename.");
|
|
33
|
+
}
|
|
34
|
+
const tarballPath = resolve(tempRoot, row.filename);
|
|
35
|
+
const tarball = await readFile(tarballPath);
|
|
36
|
+
const buildHash = createHash("sha256").update(tarball).digest("hex");
|
|
37
|
+
const installDir = buildPluginArtifactInstallDir({
|
|
38
|
+
pluginId: sanitizeArtifactSegment(input.packageName),
|
|
39
|
+
version: input.version?.trim() || "latest",
|
|
40
|
+
buildHash
|
|
41
|
+
});
|
|
42
|
+
await mkdir(installDir, { recursive: true });
|
|
43
|
+
await execFileAsync("tar", ["-xzf", tarballPath, "-C", installDir], {
|
|
44
|
+
maxBuffer: 5 * 1024 * 1024
|
|
45
|
+
});
|
|
46
|
+
const packageRoot = resolve(installDir, "package");
|
|
47
|
+
const packageJsonRaw = await readFile(resolve(packageRoot, "package.json"), "utf8");
|
|
48
|
+
const packageJsonParsed = JSON.parse(packageJsonRaw) as Record<string, unknown>;
|
|
49
|
+
const manifestPath = resolveManifestPath(packageRoot, packageJsonParsed);
|
|
50
|
+
const manifestRaw = await readFile(manifestPath, "utf8");
|
|
51
|
+
const manifestParsed = PluginManifestV2Schema.parse(JSON.parse(manifestRaw) as unknown);
|
|
52
|
+
const normalizedManifest = normalizeManifestEntrypoints(packageRoot, manifestParsed, {
|
|
53
|
+
packageName: input.packageName.trim(),
|
|
54
|
+
packageRef,
|
|
55
|
+
integrity: row.integrity,
|
|
56
|
+
buildHash,
|
|
57
|
+
artifactPath: packageRoot
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
manifest: normalizedManifest,
|
|
61
|
+
packageRoot,
|
|
62
|
+
packageRef,
|
|
63
|
+
buildHash,
|
|
64
|
+
integrity: row.integrity
|
|
65
|
+
};
|
|
66
|
+
} finally {
|
|
67
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
68
|
+
await mkdir(artifactsRoot, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveManifestPath(packageRoot: string, packageJson: Record<string, unknown>) {
|
|
73
|
+
const bopo = packageJson.bopo;
|
|
74
|
+
if (typeof bopo === "object" && bopo !== null) {
|
|
75
|
+
const maybe = (bopo as Record<string, unknown>).pluginManifest;
|
|
76
|
+
if (typeof maybe === "string" && maybe.trim()) {
|
|
77
|
+
return resolve(packageRoot, maybe);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const legacy = packageJson.bopoPluginManifest;
|
|
81
|
+
if (typeof legacy === "string" && legacy.trim()) {
|
|
82
|
+
return resolve(packageRoot, legacy);
|
|
83
|
+
}
|
|
84
|
+
return resolve(packageRoot, "plugin.json");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeManifestEntrypoints(
|
|
88
|
+
packageRoot: string,
|
|
89
|
+
manifest: PluginManifestV2,
|
|
90
|
+
input: {
|
|
91
|
+
packageName: string;
|
|
92
|
+
packageRef: string;
|
|
93
|
+
integrity?: string;
|
|
94
|
+
buildHash: string;
|
|
95
|
+
artifactPath: string;
|
|
96
|
+
}
|
|
97
|
+
): PluginManifestV2 {
|
|
98
|
+
return {
|
|
99
|
+
...manifest,
|
|
100
|
+
apiVersion: "2",
|
|
101
|
+
entrypoints: {
|
|
102
|
+
worker: resolve(packageRoot, manifest.entrypoints.worker),
|
|
103
|
+
ui: manifest.entrypoints.ui ? resolve(packageRoot, manifest.entrypoints.ui) : undefined
|
|
104
|
+
},
|
|
105
|
+
install: {
|
|
106
|
+
sourceType: "registry",
|
|
107
|
+
sourceRef: input.packageRef,
|
|
108
|
+
integrity: input.integrity,
|
|
109
|
+
buildHash: input.buildHash || randomUUID(),
|
|
110
|
+
installedAt: new Date().toISOString(),
|
|
111
|
+
artifactPath: input.artifactPath,
|
|
112
|
+
packageName: input.packageName
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function resolvePluginArtifactsDir() {
|
|
5
|
+
return process.env.BOPO_PLUGIN_ARTIFACTS_DIR || resolve(process.cwd(), ".bopo", "plugin-artifacts");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function ensurePluginArtifactsDir() {
|
|
9
|
+
const dir = resolvePluginArtifactsDir();
|
|
10
|
+
await mkdir(dir, { recursive: true });
|
|
11
|
+
return dir;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function sanitizeArtifactSegment(value: string) {
|
|
15
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildPluginArtifactInstallDir(input: { pluginId: string; version: string; buildHash: string }) {
|
|
19
|
+
const root = resolvePluginArtifactsDir();
|
|
20
|
+
const plugin = sanitizeArtifactSegment(input.pluginId);
|
|
21
|
+
const version = sanitizeArtifactSegment(input.version);
|
|
22
|
+
const hash = sanitizeArtifactSegment(input.buildHash.slice(0, 16));
|
|
23
|
+
return resolve(root, plugin, `${version}-${hash}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function removePluginArtifactInstallDir(path: string) {
|
|
27
|
+
await rm(path, { recursive: true, force: true });
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PLUGIN_CAPABILITY_RISK, type PluginCapabilityNamespace } from "bopodev-contracts";
|
|
2
|
+
|
|
3
|
+
const LEGACY_HIGH_RISK = new Set(["network", "queue_publish", "issue_write", "write_memory"]);
|
|
4
|
+
|
|
5
|
+
export type PluginTrustLevel = "dev_local" | "verified" | "restricted";
|
|
6
|
+
|
|
7
|
+
function resolveTrustLevel(): PluginTrustLevel {
|
|
8
|
+
const raw = process.env.BOPO_PLUGIN_TRUST_LEVEL;
|
|
9
|
+
if (raw === "dev_local" || raw === "verified" || raw === "restricted") {
|
|
10
|
+
return raw;
|
|
11
|
+
}
|
|
12
|
+
return "verified";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function legacyCapabilitiesRequireApproval(capabilities: string[]) {
|
|
16
|
+
return capabilities.some((cap) => LEGACY_HIGH_RISK.has(cap));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function namespacedCapabilitiesRequireApproval(capabilities: PluginCapabilityNamespace[]) {
|
|
20
|
+
const trustLevel = resolveTrustLevel();
|
|
21
|
+
return capabilities.some((cap) => {
|
|
22
|
+
const risk = PLUGIN_CAPABILITY_RISK[cap];
|
|
23
|
+
if (risk === "restricted") {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (risk === "elevated") {
|
|
27
|
+
return trustLevel !== "dev_local";
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
});
|
|
31
|
+
}
|