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
|
@@ -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,
|
|
206
|
-
const
|
|
207
|
-
const
|
|
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
|
|
227
|
-
await
|
|
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
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
195
|
+
pluginId: plugin.pluginId,
|
|
196
|
+
hook: parsedHook,
|
|
197
|
+
status: "blocked",
|
|
198
|
+
durationMs: Date.now() - startedAt,
|
|
199
|
+
error: msg
|
|
334
200
|
});
|
|
335
|
-
|
|
336
|
-
promptAppends.push(processed.promptAppend);
|
|
337
|
-
}
|
|
201
|
+
continue;
|
|
338
202
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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:
|
|
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
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
}
|