bopodev-api 0.1.33 → 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.
Files changed (90) hide show
  1. package/package.json +5 -5
  2. package/src/app.ts +4 -2
  3. package/src/assets/starter-packs/customer-support-excellence.zip +0 -0
  4. package/src/assets/starter-packs/devrel-growth.zip +0 -0
  5. package/src/assets/starter-packs/product-delivery-trio.zip +0 -0
  6. package/src/assets/starter-packs/revenue-gtm-b2b.zip +0 -0
  7. package/src/assets/starter-packs/sources/customer-support-excellence/.bopo.yaml +129 -0
  8. package/src/assets/starter-packs/sources/customer-support-excellence/COMPANY.md +7 -0
  9. package/src/assets/starter-packs/sources/customer-support-excellence/README.md +3 -0
  10. package/src/assets/starter-packs/sources/customer-support-excellence/agents/founder-ceo/HEARTBEAT.md +5 -0
  11. package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-lead/HEARTBEAT.md +5 -0
  12. package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-specialist/HEARTBEAT.md +5 -0
  13. package/src/assets/starter-packs/sources/customer-support-excellence/projects/knowledge-base/PROJECT.md +7 -0
  14. package/src/assets/starter-packs/sources/customer-support-excellence/projects/quality/PROJECT.md +7 -0
  15. package/src/assets/starter-packs/sources/customer-support-excellence/projects/queue/PROJECT.md +7 -0
  16. package/src/assets/starter-packs/sources/customer-support-excellence/skills/kb-article-skeleton/SKILL.md +17 -0
  17. package/src/assets/starter-packs/sources/customer-support-excellence/skills/ticket-response-playbook/SKILL.md +15 -0
  18. package/src/assets/starter-packs/sources/customer-support-excellence/tasks/daily-queue-standup/TASK.md +11 -0
  19. package/src/assets/starter-packs/sources/customer-support-excellence/tasks/kb-gap-sweep/TASK.md +11 -0
  20. package/src/assets/starter-packs/sources/devrel-growth/.bopo.yaml +128 -0
  21. package/src/assets/starter-packs/sources/devrel-growth/COMPANY.md +7 -0
  22. package/src/assets/starter-packs/sources/devrel-growth/README.md +3 -0
  23. package/src/assets/starter-packs/sources/devrel-growth/agents/content-producer/HEARTBEAT.md +5 -0
  24. package/src/assets/starter-packs/sources/devrel-growth/agents/devrel-lead/HEARTBEAT.md +5 -0
  25. package/src/assets/starter-packs/sources/devrel-growth/agents/founder-ceo/HEARTBEAT.md +5 -0
  26. package/src/assets/starter-packs/sources/devrel-growth/projects/community/PROJECT.md +7 -0
  27. package/src/assets/starter-packs/sources/devrel-growth/projects/docs-education/PROJECT.md +7 -0
  28. package/src/assets/starter-packs/sources/devrel-growth/projects/partners/PROJECT.md +7 -0
  29. package/src/assets/starter-packs/sources/devrel-growth/skills/changelog-to-post/SKILL.md +14 -0
  30. package/src/assets/starter-packs/sources/devrel-growth/skills/tutorial-outline/SKILL.md +15 -0
  31. package/src/assets/starter-packs/sources/devrel-growth/tasks/community-health-review/TASK.md +11 -0
  32. package/src/assets/starter-packs/sources/devrel-growth/tasks/weekly-content-plan/TASK.md +11 -0
  33. package/src/assets/starter-packs/sources/product-delivery-trio/.bopo.yaml +138 -0
  34. package/src/assets/starter-packs/sources/product-delivery-trio/COMPANY.md +7 -0
  35. package/src/assets/starter-packs/sources/product-delivery-trio/README.md +9 -0
  36. package/src/assets/starter-packs/sources/product-delivery-trio/agents/engineer-ic/HEARTBEAT.md +5 -0
  37. package/src/assets/starter-packs/sources/product-delivery-trio/agents/founder-ceo/HEARTBEAT.md +6 -0
  38. package/src/assets/starter-packs/sources/product-delivery-trio/agents/product-lead/HEARTBEAT.md +5 -0
  39. package/src/assets/starter-packs/sources/product-delivery-trio/projects/delivery/PROJECT.md +7 -0
  40. package/src/assets/starter-packs/sources/product-delivery-trio/projects/quality/PROJECT.md +7 -0
  41. package/src/assets/starter-packs/sources/product-delivery-trio/projects/strategy/PROJECT.md +7 -0
  42. package/src/assets/starter-packs/sources/product-delivery-trio/skills/issue-triage/SKILL.md +21 -0
  43. package/src/assets/starter-packs/sources/product-delivery-trio/skills/rca-template/SKILL.md +16 -0
  44. package/src/assets/starter-packs/sources/product-delivery-trio/tasks/release-hygiene/TASK.md +11 -0
  45. package/src/assets/starter-packs/sources/product-delivery-trio/tasks/weekly-leadership-sync/TASK.md +11 -0
  46. package/src/assets/starter-packs/sources/revenue-gtm-b2b/.bopo.yaml +132 -0
  47. package/src/assets/starter-packs/sources/revenue-gtm-b2b/COMPANY.md +7 -0
  48. package/src/assets/starter-packs/sources/revenue-gtm-b2b/README.md +3 -0
  49. package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/founder-ceo/HEARTBEAT.md +5 -0
  50. package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/gtm-lead/HEARTBEAT.md +5 -0
  51. package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/pipeline-owner/HEARTBEAT.md +5 -0
  52. package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/customer-success/PROJECT.md +7 -0
  53. package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/deals/PROJECT.md +7 -0
  54. package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/pipeline/PROJECT.md +7 -0
  55. package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/discovery-call-brief/SKILL.md +14 -0
  56. package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/icp-scoring/SKILL.md +20 -0
  57. package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/pipeline-hygiene/TASK.md +11 -0
  58. package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/weekly-revenue-review/TASK.md +11 -0
  59. package/src/lib/agent-issue-permissions.ts +56 -0
  60. package/src/lib/builtin-bopo-skills/bopodev-control-plane.md +7 -0
  61. package/src/realtime/office-space.ts +7 -0
  62. package/src/routes/agents.ts +23 -1
  63. package/src/routes/assistant.ts +40 -1
  64. package/src/routes/companies.ts +227 -15
  65. package/src/routes/issues.ts +48 -0
  66. package/src/routes/plugins.ts +393 -103
  67. package/src/routes/{loops.ts → routines.ts} +72 -76
  68. package/src/scripts/onboard-seed.ts +2 -0
  69. package/src/server.ts +3 -1
  70. package/src/services/company-assistant-context-snapshot.ts +4 -2
  71. package/src/services/company-assistant-service.ts +17 -15
  72. package/src/services/company-file-archive-service.ts +56 -3
  73. package/src/services/company-file-import-service.ts +210 -31
  74. package/src/services/governance-service.ts +58 -3
  75. package/src/services/heartbeat-service/heartbeat-run.ts +7 -0
  76. package/src/services/plugin-artifact-installer.ts +115 -0
  77. package/src/services/plugin-artifact-store.ts +28 -0
  78. package/src/services/plugin-capability-policy.ts +31 -0
  79. package/src/services/plugin-jobs-service.ts +74 -0
  80. package/src/services/plugin-manifest-loader.ts +78 -3
  81. package/src/services/plugin-rpc.ts +102 -0
  82. package/src/services/plugin-runtime.ts +240 -209
  83. package/src/services/plugin-worker-host.ts +167 -0
  84. package/src/services/starter-pack-registry.ts +68 -0
  85. package/src/services/template-apply-service.ts +3 -1
  86. package/src/services/template-catalog.ts +29 -0
  87. package/src/services/work-loop-service/work-loop-service.ts +18 -18
  88. package/src/shutdown/graceful-shutdown.ts +3 -1
  89. package/src/worker/scheduler.ts +21 -1
  90. package/src/services/company-export-service.ts +0 -63
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  PluginHook,
3
+ PluginCapabilityNamespace,
3
4
  PluginInvocationResult,
4
5
  PluginManifest,
5
6
  PluginPromptExecutionResult,
@@ -10,6 +11,8 @@ import {
10
11
  PluginHookSchema,
11
12
  PluginInvocationResultSchema,
12
13
  PluginManifestSchema,
14
+ PluginManifestV2Schema,
15
+ PLUGIN_CAPABILITY_RISK,
13
16
  PluginPromptExecutionResultSchema,
14
17
  PluginTraceEventSchema,
15
18
  PluginWebhookRequestSchema
@@ -17,13 +20,16 @@ import {
17
20
  import type { BopoDb } from "bopodev-db";
18
21
  import {
19
22
  appendAuditEvent,
23
+ deletePluginById,
20
24
  appendPluginRun,
21
25
  listCompanyPluginConfigs,
26
+ listPlugins,
22
27
  upsertPlugin,
23
28
  updatePluginConfig
24
29
  } from "bopodev-db";
25
30
  import { loadFilesystemPluginManifests } from "./plugin-manifest-loader";
26
31
  import { executePluginWebhooks } from "./plugin-webhook-executor";
32
+ import { pluginWorkerHost } from "./plugin-worker-host";
27
33
 
28
34
  type HookContext = {
29
35
  companyId: string;
@@ -49,147 +55,8 @@ export type PluginHookResult = {
49
55
  promptAppend: string | null;
50
56
  };
51
57
 
52
- type BuiltinPluginExecutor = (context: HookContext) => Promise<PluginInvocationResult> | PluginInvocationResult;
53
-
54
58
  const HIGH_RISK_CAPABILITIES = new Set(["network", "queue_publish", "issue_write", "write_memory"]);
55
59
 
56
- const builtinPluginDefinitions = [
57
- {
58
- id: "trace-exporter",
59
- version: "0.1.0",
60
- displayName: "Trace Exporter",
61
- description: "Emit normalized heartbeat trace events for downstream observability sinks.",
62
- kind: "lifecycle",
63
- hooks: ["afterAdapterExecute", "onError", "afterPersist"],
64
- capabilities: ["emit_audit"],
65
- runtime: {
66
- type: "builtin",
67
- entrypoint: "builtin:trace-exporter",
68
- timeoutMs: 5000,
69
- retryCount: 0
70
- }
71
- },
72
- {
73
- id: "memory-enricher",
74
- version: "0.1.0",
75
- displayName: "Memory Enricher",
76
- description: "Derive and dedupe memory candidate facts from heartbeat outcomes.",
77
- kind: "lifecycle",
78
- hooks: ["afterAdapterExecute", "afterPersist"],
79
- capabilities: ["read_memory", "emit_audit"],
80
- runtime: {
81
- type: "builtin",
82
- entrypoint: "builtin:memory-enricher",
83
- timeoutMs: 5000,
84
- retryCount: 0
85
- }
86
- },
87
- {
88
- id: "queue-publisher",
89
- version: "0.1.0",
90
- displayName: "Queue Publisher",
91
- description: "Publish heartbeat completion/failure payloads to queue integrations.",
92
- kind: "integration",
93
- hooks: ["afterPersist", "onError"],
94
- capabilities: ["queue_publish", "network", "emit_audit"],
95
- runtime: {
96
- type: "builtin",
97
- entrypoint: "builtin:queue-publisher",
98
- timeoutMs: 5000,
99
- retryCount: 0
100
- }
101
- },
102
- {
103
- id: "heartbeat-tagger",
104
- version: "0.1.0",
105
- displayName: "Heartbeat Tagger",
106
- description: "Attach a simple diagnostic tag to heartbeat plugin runs.",
107
- kind: "lifecycle",
108
- hooks: ["afterAdapterExecute"],
109
- capabilities: ["emit_audit"],
110
- runtime: {
111
- type: "builtin",
112
- entrypoint: "builtin:heartbeat-tagger",
113
- timeoutMs: 3000,
114
- retryCount: 0
115
- }
116
- }
117
- ] as const;
118
-
119
- const builtinExecutors: Record<string, BuiltinPluginExecutor> = {
120
- "trace-exporter": async (context) => ({
121
- status: "ok",
122
- summary: "trace-exporter emitted heartbeat trace metadata",
123
- blockers: [],
124
- diagnostics: {
125
- runId: context.runId,
126
- providerType: context.providerType ?? null,
127
- status: context.status ?? null
128
- }
129
- }),
130
- "memory-enricher": async (context) => ({
131
- status: "ok",
132
- summary: "memory-enricher evaluated summary for memory candidates",
133
- blockers: [],
134
- diagnostics: {
135
- runId: context.runId,
136
- summaryPresent: typeof context.summary === "string" && context.summary.trim().length > 0,
137
- usefulnessScore: scoreMemorySummaryUsefulness(context.summary ?? ""),
138
- outcomeKind:
139
- context.outcome && typeof context.outcome === "object" && "kind" in context.outcome
140
- ? String((context.outcome as Record<string, unknown>).kind ?? "")
141
- : ""
142
- }
143
- }),
144
- "queue-publisher": async (context) => ({
145
- status: "ok",
146
- summary: "queue-publisher prepared outbound heartbeat event",
147
- blockers: [],
148
- diagnostics: {
149
- runId: context.runId,
150
- status: context.status ?? null,
151
- eventType: context.error ? "heartbeat.failed" : "heartbeat.completed"
152
- }
153
- }),
154
- "heartbeat-tagger": async (context) => ({
155
- status: "ok",
156
- summary: "heartbeat-tagger attached diagnostic tag",
157
- blockers: [],
158
- diagnostics: {
159
- tag: "hello-plugin",
160
- runId: context.runId,
161
- providerType: context.providerType ?? null
162
- }
163
- })
164
- };
165
-
166
- function scoreMemorySummaryUsefulness(summary: string) {
167
- const normalized = summary.trim();
168
- if (!normalized) {
169
- return 0;
170
- }
171
- const tokenCount = normalized
172
- .toLowerCase()
173
- .replace(/[^a-z0-9\s]/g, " ")
174
- .split(/\s+/)
175
- .filter((entry) => entry.length >= 3).length;
176
- const evidenceTerms = /\b(test|validated|verified|implemented|deployed|fixed|refactor|migration|metric)\b/i.test(normalized);
177
- const blockersTerms = /\b(blocked|failed|unknown|maybe)\b/i.test(normalized);
178
- let score = 0.3;
179
- if (tokenCount >= 20) {
180
- score += 0.3;
181
- } else if (tokenCount >= 8) {
182
- score += 0.15;
183
- }
184
- if (evidenceTerms) {
185
- score += 0.25;
186
- }
187
- if (!blockersTerms) {
188
- score += 0.15;
189
- }
190
- return Number(Math.min(1, Math.max(0, score)).toFixed(3));
191
- }
192
-
193
60
  export function pluginSystemEnabled() {
194
61
  const disabled = process.env.BOPO_PLUGIN_SYSTEM_DISABLED;
195
62
  if (disabled === "1" || disabled === "true") {
@@ -202,9 +69,15 @@ export function pluginSystemEnabled() {
202
69
  return true;
203
70
  }
204
71
 
205
- export async function ensureBuiltinPluginsRegistered(db: BopoDb, companyIds: string[] = []) {
206
- const manifests = builtinPluginDefinitions.map((definition) => PluginManifestSchema.parse(definition));
207
- const manifestIds = new Set(manifests.map((manifest) => manifest.id));
72
+ export async function ensureBuiltinPluginsRegistered(db: BopoDb, _companyIds: string[] = []) {
73
+ const existing = await listPlugins(db);
74
+ for (const plugin of existing) {
75
+ if (plugin.runtimeEntrypoint.startsWith("builtin:")) {
76
+ await deletePluginById(db, plugin.id);
77
+ }
78
+ }
79
+ const manifests: PluginManifest[] = [];
80
+ const manifestIds = new Set<string>();
208
81
  const fileManifestResult = await loadFilesystemPluginManifests();
209
82
  for (const warning of fileManifestResult.warnings) {
210
83
  // eslint-disable-next-line no-console
@@ -223,8 +96,22 @@ export async function ensureBuiltinPluginsRegistered(db: BopoDb, companyIds: str
223
96
  for (const manifest of manifests) {
224
97
  await registerPluginManifest(db, manifest);
225
98
  }
226
- for (const companyId of companyIds) {
227
- await ensureCompanyBuiltinPluginDefaults(db, companyId);
99
+ for (const companyId of _companyIds) {
100
+ const existingConfigs = await listCompanyPluginConfigs(db, companyId);
101
+ const installedIds = new Set(existingConfigs.map((row) => row.pluginId));
102
+ for (const manifest of manifests) {
103
+ if (installedIds.has(manifest.id)) {
104
+ continue;
105
+ }
106
+ await updatePluginConfig(db, {
107
+ companyId,
108
+ pluginId: manifest.id,
109
+ enabled: false,
110
+ priority: 100,
111
+ configJson: "{}",
112
+ grantedCapabilitiesJson: "[]"
113
+ });
114
+ }
228
115
  }
229
116
  }
230
117
 
@@ -242,30 +129,6 @@ export async function registerPluginManifest(db: BopoDb, manifest: PluginManifes
242
129
  });
243
130
  }
244
131
 
245
- export async function ensureCompanyBuiltinPluginDefaults(db: BopoDb, companyId: string) {
246
- const existing = await listCompanyPluginConfigs(db, companyId);
247
- const existingIds = new Set(existing.map((row) => row.pluginId));
248
- const defaults = [
249
- { pluginId: "trace-exporter", enabled: false, priority: 40 },
250
- { pluginId: "memory-enricher", enabled: false, priority: 60 },
251
- { pluginId: "queue-publisher", enabled: false, priority: 80 },
252
- { pluginId: "heartbeat-tagger", enabled: false, priority: 90 }
253
- ];
254
- for (const entry of defaults) {
255
- if (existingIds.has(entry.pluginId)) {
256
- continue;
257
- }
258
- await updatePluginConfig(db, {
259
- companyId,
260
- pluginId: entry.pluginId,
261
- enabled: entry.enabled,
262
- priority: entry.priority,
263
- configJson: "{}",
264
- grantedCapabilitiesJson: "[]"
265
- });
266
- }
267
- }
268
-
269
132
  export async function runPluginHook(
270
133
  db: BopoDb,
271
134
  input: {
@@ -297,7 +160,6 @@ export async function runPluginHook(
297
160
  .filter((row) => row.hooks.includes(parsedHook));
298
161
 
299
162
  const failures: string[] = [];
300
- const promptAppends: string[] = [];
301
163
  let applied = 0;
302
164
  for (const plugin of candidates) {
303
165
  const startedAt = Date.now();
@@ -319,34 +181,29 @@ export async function runPluginHook(
319
181
  });
320
182
  continue;
321
183
  }
322
- const promptResult = await executePromptPlugin(plugin.manifest, plugin.pluginId, input.context, {
323
- hook: parsedHook,
324
- pluginConfig: safeParseJsonRecord(plugin.configJson)
325
- });
326
- if (promptResult) {
327
- const processed = await processPromptPluginResult(db, {
328
- pluginId: plugin.pluginId,
184
+ const namespaceViolation = resolveMissingCapabilityNamespace(
185
+ plugin.manifest,
186
+ safeParseJsonRecord(plugin.configJson),
187
+ ["events.subscribe"]
188
+ );
189
+ if (namespaceViolation) {
190
+ const msg = `plugin '${plugin.pluginId}' requires granted capability namespace '${namespaceViolation}'`;
191
+ failures.push(msg);
192
+ await appendPluginRun(db, {
329
193
  companyId: input.context.companyId,
330
194
  runId: input.context.runId,
331
- requestId: input.context.requestId,
332
- pluginCapabilities: plugin.caps,
333
- promptResult
195
+ pluginId: plugin.pluginId,
196
+ hook: parsedHook,
197
+ status: "blocked",
198
+ durationMs: Date.now() - startedAt,
199
+ error: msg
334
200
  });
335
- if (processed.promptAppend) {
336
- promptAppends.push(processed.promptAppend);
337
- }
201
+ continue;
338
202
  }
339
- const result =
340
- promptResult && plugin.manifest?.runtime.type === "prompt"
341
- ? ({
342
- status: "ok",
343
- summary: "prompt plugin applied runtime patches",
344
- blockers: [],
345
- diagnostics: {
346
- source: "prompt-runtime"
347
- }
348
- } as PluginInvocationResult)
349
- : await executePlugin(plugin.pluginId, input.context);
203
+ if (plugin.manifest?.runtime.type === "prompt") {
204
+ throw new Error(`plugin '${plugin.pluginId}' uses removed prompt runtime; install a worker package plugin`);
205
+ }
206
+ const result = await executePluginWithRuntime(plugin.pluginId, plugin.manifest, parsedHook, input.context);
350
207
  const validated = PluginInvocationResultSchema.parse(result);
351
208
  await appendPluginRun(db, {
352
209
  companyId: input.context.companyId,
@@ -355,10 +212,7 @@ export async function runPluginHook(
355
212
  hook: parsedHook,
356
213
  status: validated.status,
357
214
  durationMs: Date.now() - startedAt,
358
- diagnosticsJson: JSON.stringify({
359
- ...(validated.diagnostics ?? {}),
360
- promptAppendApplied: promptResult?.promptAppend ?? null
361
- }),
215
+ diagnosticsJson: JSON.stringify(validated.diagnostics ?? {}),
362
216
  error: validated.status === "failed" || validated.status === "blocked" ? validated.summary : null
363
217
  });
364
218
  if (validated.status === "failed" || validated.status === "blocked") {
@@ -400,7 +254,7 @@ export async function runPluginHook(
400
254
  blocked,
401
255
  applied,
402
256
  failures,
403
- promptAppend: promptAppends.length > 0 ? promptAppends.join("\n\n") : null
257
+ promptAppend: null
404
258
  };
405
259
  }
406
260
 
@@ -441,6 +295,145 @@ function safeParseManifest(value: string | null | undefined) {
441
295
  }
442
296
  }
443
297
 
298
+ export function isLegacyPluginManifest(manifest: PluginManifest | null) {
299
+ return !manifest || !("apiVersion" in manifest) || manifest.apiVersion !== "2";
300
+ }
301
+
302
+ export async function invokePluginWorkerEndpoint(
303
+ db: BopoDb,
304
+ input: {
305
+ companyId: string;
306
+ pluginId: string;
307
+ endpointType: "action" | "data";
308
+ endpointKey: string;
309
+ payload?: Record<string, unknown>;
310
+ }
311
+ ) {
312
+ const rows = await listCompanyPluginConfigs(db, input.companyId);
313
+ const row = rows.find((entry) => entry.pluginId === input.pluginId);
314
+ if (!row || !row.enabled) {
315
+ throw new Error(`plugin '${input.pluginId}' is not installed or enabled for this company`);
316
+ }
317
+ const manifest = safeParseManifest(row.manifestJson);
318
+ if (!manifest) {
319
+ throw new Error(`plugin '${input.pluginId}' manifest is invalid`);
320
+ }
321
+ const parsedV2 = PluginManifestV2Schema.safeParse(manifest);
322
+ if (!parsedV2.success) {
323
+ throw new Error(`plugin '${input.pluginId}' does not support worker endpoints`);
324
+ }
325
+ const requiredNamespace = input.endpointType === "action" ? "actions.execute" : "data.read";
326
+ const namespaceViolation = resolveMissingCapabilityNamespace(parsedV2.data, safeParseJsonRecord(row.configJson), [
327
+ requiredNamespace
328
+ ]);
329
+ if (namespaceViolation) {
330
+ throw new Error(`plugin '${input.pluginId}' requires granted capability namespace '${namespaceViolation}'`);
331
+ }
332
+ const result = await pluginWorkerHost.invoke(parsedV2.data, {
333
+ method: input.endpointType === "action" ? "plugin.action" : "plugin.data",
334
+ params: {
335
+ key: input.endpointKey,
336
+ companyId: input.companyId,
337
+ payload: input.payload ?? {}
338
+ }
339
+ });
340
+ return result;
341
+ }
342
+
343
+ export async function invokePluginWorkerHealth(
344
+ db: BopoDb,
345
+ input: {
346
+ companyId: string;
347
+ pluginId: string;
348
+ }
349
+ ) {
350
+ const rows = await listCompanyPluginConfigs(db, input.companyId);
351
+ const row = rows.find((entry) => entry.pluginId === input.pluginId);
352
+ if (!row || !row.enabled) {
353
+ throw new Error(`plugin '${input.pluginId}' is not installed or enabled for this company`);
354
+ }
355
+ const manifest = safeParseManifest(row.manifestJson);
356
+ if (!manifest) {
357
+ throw new Error(`plugin '${input.pluginId}' manifest is invalid`);
358
+ }
359
+ return await pluginWorkerHost.invoke(manifest, {
360
+ method: "plugin.health",
361
+ params: {
362
+ companyId: input.companyId
363
+ }
364
+ });
365
+ }
366
+
367
+ export async function invokePluginWorkerWebhook(
368
+ db: BopoDb,
369
+ input: {
370
+ companyId: string;
371
+ pluginId: string;
372
+ endpointKey: string;
373
+ payload?: Record<string, unknown>;
374
+ headers?: Record<string, string>;
375
+ }
376
+ ) {
377
+ const rows = await listCompanyPluginConfigs(db, input.companyId);
378
+ const row = rows.find((entry) => entry.pluginId === input.pluginId);
379
+ if (!row || !row.enabled) {
380
+ throw new Error(`plugin '${input.pluginId}' is not installed or enabled for this company`);
381
+ }
382
+ const manifest = safeParseManifest(row.manifestJson);
383
+ if (!manifest) {
384
+ throw new Error(`plugin '${input.pluginId}' manifest is invalid`);
385
+ }
386
+ const config = safeParseJsonRecord(row.configJson);
387
+ const webhook = manifest.webhooks.find((entry) => entry.endpointKey === input.endpointKey);
388
+ if (!webhook) {
389
+ throw new Error(`plugin '${input.pluginId}' does not declare webhook '${input.endpointKey}'`);
390
+ }
391
+ const secretHeader = webhook.secretHeader?.toLowerCase();
392
+ if (secretHeader) {
393
+ const secretsMap =
394
+ typeof config._webhookSecrets === "object" && config._webhookSecrets !== null
395
+ ? (config._webhookSecrets as Record<string, unknown>)
396
+ : {};
397
+ const expected = secretsMap[input.endpointKey];
398
+ const actual = input.headers?.[secretHeader];
399
+ if (typeof expected === "string" && expected.length > 0 && actual !== expected) {
400
+ throw new Error(`webhook signature check failed for endpoint '${input.endpointKey}'`);
401
+ }
402
+ }
403
+ const namespaceViolation = resolveMissingCapabilityNamespace(manifest, config, ["webhooks.handle"]);
404
+ if (namespaceViolation) {
405
+ throw new Error(`plugin '${input.pluginId}' requires granted capability namespace '${namespaceViolation}'`);
406
+ }
407
+ return await pluginWorkerHost.invoke(manifest, {
408
+ method: "plugin.webhook",
409
+ params: {
410
+ companyId: input.companyId,
411
+ endpointKey: input.endpointKey,
412
+ payload: input.payload ?? {},
413
+ headers: input.headers ?? {}
414
+ }
415
+ });
416
+ }
417
+
418
+ export async function resolvePluginUiEntrypoint(
419
+ db: BopoDb,
420
+ input: {
421
+ companyId: string;
422
+ pluginId: string;
423
+ }
424
+ ) {
425
+ const rows = await listCompanyPluginConfigs(db, input.companyId);
426
+ const row = rows.find((entry) => entry.pluginId === input.pluginId);
427
+ if (!row || !row.enabled) {
428
+ throw new Error(`plugin '${input.pluginId}' is not installed or enabled for this company`);
429
+ }
430
+ const manifest = safeParseManifest(row.manifestJson);
431
+ if (!manifest) {
432
+ throw new Error(`plugin '${input.pluginId}' manifest is invalid`);
433
+ }
434
+ return manifest.entrypoints.ui ?? null;
435
+ }
436
+
444
437
  async function executePromptPlugin(
445
438
  manifest: PluginManifest | null,
446
439
  pluginId: string,
@@ -598,15 +591,53 @@ function renderPromptTemplate(
598
591
  .replaceAll("{{traceEvents}}", JSON.stringify(input.traceEvents));
599
592
  }
600
593
 
601
- async function executePlugin(pluginId: string, context: HookContext): Promise<PluginInvocationResult> {
602
- const executor = builtinExecutors[pluginId];
603
- if (!executor) {
604
- return {
605
- status: "skipped",
606
- summary: `No executor is registered for plugin '${pluginId}'.`,
607
- blockers: [],
608
- diagnostics: { pluginId }
609
- };
610
- }
611
- return executor(context);
594
+ async function executePluginWithRuntime(
595
+ pluginId: string,
596
+ manifest: PluginManifest | null,
597
+ hook: PluginHook,
598
+ context: HookContext
599
+ ): Promise<PluginInvocationResult> {
600
+ if (!manifest) {
601
+ throw new Error(`plugin '${pluginId}' manifest is missing`);
602
+ }
603
+ if (manifest.runtime.type === "builtin") {
604
+ throw new Error(`plugin '${pluginId}' uses removed builtin runtime; install a package plugin`);
605
+ }
606
+ const workerResult = await pluginWorkerHost.invoke(manifest, {
607
+ method: "plugin.hook",
608
+ params: {
609
+ hook,
610
+ context
611
+ }
612
+ });
613
+ return PluginInvocationResultSchema.parse(workerResult);
614
+ }
615
+
616
+ function resolveMissingCapabilityNamespace(
617
+ manifest: PluginManifest | null,
618
+ config: Record<string, unknown>,
619
+ requiredNamespaces: PluginCapabilityNamespace[]
620
+ ) {
621
+ if (!manifest || !("capabilityNamespaces" in manifest)) {
622
+ return null;
623
+ }
624
+ if (requiredNamespaces.length === 0) {
625
+ return null;
626
+ }
627
+ const requested = manifest.capabilityNamespaces ?? [];
628
+ if (requested.length === 0) {
629
+ return null;
630
+ }
631
+ const grantedRaw = config._grantedCapabilityNamespaces;
632
+ const granted = Array.isArray(grantedRaw) ? grantedRaw.map((value) => String(value)) : [];
633
+ for (const namespace of requiredNamespaces) {
634
+ if (!requested.includes(namespace)) {
635
+ continue;
636
+ }
637
+ const risk = PLUGIN_CAPABILITY_RISK[namespace];
638
+ if ((risk === "elevated" || risk === "restricted") && !granted.includes(namespace)) {
639
+ return namespace;
640
+ }
641
+ }
642
+ return null;
612
643
  }