bopodev-api 0.1.34 → 0.1.36
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/lib/instance-paths.ts +5 -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 +82 -3
- package/src/routes/observability.ts +222 -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 +81 -6
- package/src/services/company-file-import-service.ts +221 -31
- package/src/services/company-knowledge-file-service.ts +361 -0
- package/src/services/company-skill-file-service.ts +151 -2
- 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/validation/issue-routes.ts +19 -2
- package/src/worker/scheduler.ts +21 -1
- package/src/services/company-export-service.ts +0 -63
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { PluginManifestV2 } from "bopodev-contracts";
|
|
2
|
+
import { PluginInvocationResultSchema, PluginManifestV2Schema } from "bopodev-contracts";
|
|
3
|
+
import type { BopoDb } from "bopodev-db";
|
|
4
|
+
import { appendPluginRun, listCompanyPluginConfigs } from "bopodev-db";
|
|
5
|
+
import { pluginWorkerHost } from "./plugin-worker-host";
|
|
6
|
+
|
|
7
|
+
const lastJobRun = new Map<string, number>();
|
|
8
|
+
|
|
9
|
+
function shouldRunJob(companyId: string, pluginId: string, jobKey: string) {
|
|
10
|
+
const key = `${companyId}:${pluginId}:${jobKey}`;
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
const previous = lastJobRun.get(key) ?? 0;
|
|
13
|
+
if (now - previous < 55_000) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
lastJobRun.set(key, now);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runPluginJobSweep(db: BopoDb, companyId: string) {
|
|
21
|
+
const rows = await listCompanyPluginConfigs(db, companyId);
|
|
22
|
+
for (const row of rows) {
|
|
23
|
+
if (!row.enabled) continue;
|
|
24
|
+
const manifest = parseManifest(row.manifestJson);
|
|
25
|
+
if (!manifest || manifest.jobs.length === 0) continue;
|
|
26
|
+
for (const job of manifest.jobs) {
|
|
27
|
+
if (!shouldRunJob(companyId, row.pluginId, job.jobKey)) continue;
|
|
28
|
+
const startedAt = Date.now();
|
|
29
|
+
try {
|
|
30
|
+
const result = await pluginWorkerHost.invoke(manifest, {
|
|
31
|
+
method: "plugin.job",
|
|
32
|
+
params: {
|
|
33
|
+
companyId,
|
|
34
|
+
pluginId: row.pluginId,
|
|
35
|
+
jobKey: job.jobKey,
|
|
36
|
+
schedule: job.schedule
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const validated = PluginInvocationResultSchema.parse(result);
|
|
40
|
+
await appendPluginRun(db, {
|
|
41
|
+
companyId,
|
|
42
|
+
runId: null,
|
|
43
|
+
pluginId: row.pluginId,
|
|
44
|
+
hook: `job:${job.jobKey}`,
|
|
45
|
+
status: validated.status,
|
|
46
|
+
durationMs: Date.now() - startedAt,
|
|
47
|
+
diagnosticsJson: JSON.stringify(validated.diagnostics ?? {}),
|
|
48
|
+
error: validated.status === "failed" || validated.status === "blocked" ? validated.summary : null
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
await appendPluginRun(db, {
|
|
52
|
+
companyId,
|
|
53
|
+
runId: null,
|
|
54
|
+
pluginId: row.pluginId,
|
|
55
|
+
hook: `job:${job.jobKey}`,
|
|
56
|
+
status: "failed",
|
|
57
|
+
durationMs: Date.now() - startedAt,
|
|
58
|
+
error: String(error)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseManifest(value: string | null | undefined): PluginManifestV2 | null {
|
|
66
|
+
if (!value) return null;
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(value) as unknown;
|
|
69
|
+
const result = PluginManifestV2Schema.safeParse(parsed);
|
|
70
|
+
return result.success ? result.data : null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
|
-
import { PluginManifestSchema, type PluginManifest } from "bopodev-contracts";
|
|
4
|
+
import { PluginManifestSchema, PluginManifestV2Schema, type PluginManifest } from "bopodev-contracts";
|
|
4
5
|
|
|
5
6
|
export type FilesystemPluginManifestLoadResult = {
|
|
6
7
|
manifests: PluginManifest[];
|
|
@@ -29,7 +30,7 @@ export async function loadFilesystemPluginManifests(): Promise<FilesystemPluginM
|
|
|
29
30
|
}
|
|
30
31
|
try {
|
|
31
32
|
const parsed = JSON.parse(raw) as unknown;
|
|
32
|
-
const manifest = PluginManifestSchema.parse(parsed);
|
|
33
|
+
const manifest = normalizeFilesystemManifest(manifestPath, PluginManifestSchema.parse(parsed));
|
|
33
34
|
manifests.push(manifest);
|
|
34
35
|
} catch (error) {
|
|
35
36
|
warnings.push(`Invalid plugin manifest at '${manifestPath}': ${String(error)}`);
|
|
@@ -40,7 +41,24 @@ export async function loadFilesystemPluginManifests(): Promise<FilesystemPluginM
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export function resolvePluginManifestsDir() {
|
|
43
|
-
|
|
44
|
+
if (process.env.BOPO_PLUGIN_MANIFESTS_DIR) {
|
|
45
|
+
return process.env.BOPO_PLUGIN_MANIFESTS_DIR;
|
|
46
|
+
}
|
|
47
|
+
const localPlugins = resolve(process.cwd(), "plugins");
|
|
48
|
+
if (directoryHasPluginManifests(localPlugins)) {
|
|
49
|
+
return localPlugins;
|
|
50
|
+
}
|
|
51
|
+
const repoRootPlugins = resolve(process.cwd(), "..", "..", "plugins");
|
|
52
|
+
if (directoryHasPluginManifests(repoRootPlugins)) {
|
|
53
|
+
return repoRootPlugins;
|
|
54
|
+
}
|
|
55
|
+
if (existsSync(localPlugins)) {
|
|
56
|
+
return localPlugins;
|
|
57
|
+
}
|
|
58
|
+
if (existsSync(repoRootPlugins)) {
|
|
59
|
+
return repoRootPlugins;
|
|
60
|
+
}
|
|
61
|
+
return localPlugins;
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
export async function writePluginManifestToFilesystem(manifest: PluginManifest) {
|
|
@@ -53,6 +71,29 @@ export async function writePluginManifestToFilesystem(manifest: PluginManifest)
|
|
|
53
71
|
return manifestPath;
|
|
54
72
|
}
|
|
55
73
|
|
|
74
|
+
export async function writePackagedPluginManifestToFilesystem(
|
|
75
|
+
manifest: PluginManifest,
|
|
76
|
+
input: {
|
|
77
|
+
sourceType: "builtin" | "registry" | "local_path" | "archive_url";
|
|
78
|
+
sourceRef?: string;
|
|
79
|
+
integrity?: string;
|
|
80
|
+
buildHash?: string;
|
|
81
|
+
}
|
|
82
|
+
) {
|
|
83
|
+
const nextManifest = {
|
|
84
|
+
...manifest,
|
|
85
|
+
apiVersion: "2",
|
|
86
|
+
install: {
|
|
87
|
+
sourceType: input.sourceType,
|
|
88
|
+
sourceRef: input.sourceRef,
|
|
89
|
+
integrity: input.integrity,
|
|
90
|
+
buildHash: input.buildHash,
|
|
91
|
+
installedAt: new Date().toISOString()
|
|
92
|
+
}
|
|
93
|
+
} as PluginManifest;
|
|
94
|
+
return writePluginManifestToFilesystem(nextManifest);
|
|
95
|
+
}
|
|
96
|
+
|
|
56
97
|
export async function deletePluginManifestFromFilesystem(pluginId: string) {
|
|
57
98
|
const pluginRoot = resolvePluginManifestsDir();
|
|
58
99
|
const safeDirName = sanitizePluginDirectoryName(pluginId);
|
|
@@ -63,3 +104,37 @@ export async function deletePluginManifestFromFilesystem(pluginId: string) {
|
|
|
63
104
|
function sanitizePluginDirectoryName(pluginId: string) {
|
|
64
105
|
return pluginId.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
65
106
|
}
|
|
107
|
+
|
|
108
|
+
function directoryHasPluginManifests(dir: string) {
|
|
109
|
+
if (!existsSync(dir)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
114
|
+
return entries.some((entry) => entry.isDirectory() && existsSync(resolve(dir, entry.name, "plugin.json")));
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeFilesystemManifest(manifestPath: string, manifest: PluginManifest): PluginManifest {
|
|
121
|
+
const pluginDir = resolve(manifestPath, "..");
|
|
122
|
+
const parsedV2 = PluginManifestV2Schema.safeParse(manifest);
|
|
123
|
+
if (!parsedV2.success) {
|
|
124
|
+
return manifest;
|
|
125
|
+
}
|
|
126
|
+
const v2 = parsedV2.data;
|
|
127
|
+
const worker = resolve(pluginDir, v2.entrypoints.worker);
|
|
128
|
+
const ui = v2.entrypoints.ui ? resolve(pluginDir, v2.entrypoints.ui) : undefined;
|
|
129
|
+
return {
|
|
130
|
+
...v2,
|
|
131
|
+
runtime: {
|
|
132
|
+
...v2.runtime,
|
|
133
|
+
entrypoint: v2.runtime.type === "stdio" || v2.runtime.type === "http" ? resolve(pluginDir, v2.runtime.entrypoint) : v2.runtime.entrypoint
|
|
134
|
+
},
|
|
135
|
+
entrypoints: {
|
|
136
|
+
worker,
|
|
137
|
+
ui
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
|
|
3
|
+
export type PluginRpcRequest = {
|
|
4
|
+
jsonrpc: "2.0";
|
|
5
|
+
id: string;
|
|
6
|
+
method: string;
|
|
7
|
+
params?: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type PluginRpcSuccess = {
|
|
11
|
+
jsonrpc: "2.0";
|
|
12
|
+
id: string;
|
|
13
|
+
result: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type PluginRpcFailure = {
|
|
17
|
+
jsonrpc: "2.0";
|
|
18
|
+
id: string;
|
|
19
|
+
error: {
|
|
20
|
+
code: number;
|
|
21
|
+
message: string;
|
|
22
|
+
data?: unknown;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type PluginRpcResponse = PluginRpcSuccess | PluginRpcFailure;
|
|
27
|
+
|
|
28
|
+
export function createPluginRpcRequest(method: string, params: unknown, id: string): PluginRpcRequest {
|
|
29
|
+
return {
|
|
30
|
+
jsonrpc: "2.0",
|
|
31
|
+
id,
|
|
32
|
+
method,
|
|
33
|
+
params
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function encodePluginRpcMessage(value: PluginRpcRequest | PluginRpcResponse): string {
|
|
38
|
+
return `${JSON.stringify(value)}\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function decodePluginRpcMessage(line: string): PluginRpcResponse | null {
|
|
42
|
+
let parsed: unknown;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(line) as unknown;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!parsed || typeof parsed !== "object") {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const obj = parsed as Record<string, unknown>;
|
|
52
|
+
if (obj.jsonrpc !== "2.0" || typeof obj.id !== "string") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if ("result" in obj) {
|
|
56
|
+
return {
|
|
57
|
+
jsonrpc: "2.0",
|
|
58
|
+
id: obj.id,
|
|
59
|
+
result: obj.result
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if ("error" in obj && obj.error && typeof obj.error === "object") {
|
|
63
|
+
const err = obj.error as Record<string, unknown>;
|
|
64
|
+
if (typeof err.code === "number" && typeof err.message === "string") {
|
|
65
|
+
return {
|
|
66
|
+
jsonrpc: "2.0",
|
|
67
|
+
id: obj.id,
|
|
68
|
+
error: {
|
|
69
|
+
code: err.code,
|
|
70
|
+
message: err.message,
|
|
71
|
+
data: err.data
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class PluginRpcLineBuffer extends EventEmitter<{
|
|
80
|
+
message: [PluginRpcResponse];
|
|
81
|
+
}> {
|
|
82
|
+
private buffer = "";
|
|
83
|
+
|
|
84
|
+
push(chunk: string) {
|
|
85
|
+
this.buffer += chunk;
|
|
86
|
+
while (true) {
|
|
87
|
+
const idx = this.buffer.indexOf("\n");
|
|
88
|
+
if (idx === -1) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
const line = this.buffer.slice(0, idx).trim();
|
|
92
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
93
|
+
if (!line) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const parsed = decodePluginRpcMessage(line);
|
|
97
|
+
if (parsed) {
|
|
98
|
+
this.emit("message", parsed);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|