bopodev-api 0.1.14 → 0.1.16
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 +4 -4
- package/src/app.ts +57 -1
- package/src/context.ts +3 -0
- package/src/lib/agent-config.ts +10 -1
- package/src/lib/git-runtime.ts +447 -0
- package/src/lib/instance-paths.ts +75 -10
- package/src/lib/workspace-policy.ts +153 -10
- package/src/middleware/request-actor.ts +67 -2
- package/src/realtime/hub.ts +31 -2
- package/src/routes/agents.ts +146 -107
- package/src/routes/auth.ts +54 -0
- package/src/routes/companies.ts +2 -0
- package/src/routes/governance.ts +8 -0
- package/src/routes/issues.ts +23 -10
- package/src/routes/projects.ts +361 -63
- package/src/routes/templates.ts +439 -0
- package/src/scripts/backfill-project-workspaces.ts +61 -24
- package/src/scripts/db-init.ts +7 -1
- package/src/scripts/onboard-seed.ts +140 -12
- package/src/security/actor-token.ts +133 -0
- package/src/security/deployment-mode.ts +73 -0
- package/src/server.ts +72 -4
- package/src/services/governance-service.ts +122 -15
- package/src/services/heartbeat-service.ts +136 -36
- package/src/services/plugin-runtime.ts +2 -2
- package/src/services/template-apply-service.ts +138 -0
- package/src/services/template-catalog.ts +325 -0
- package/src/services/template-preview-service.ts +78 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { and, eq } from "drizzle-orm";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { AgentCreateRequestSchema } from "bopodev-contracts";
|
|
4
|
+
import { AgentCreateRequestSchema, TemplateManifestDefault, TemplateManifestSchema } from "bopodev-contracts";
|
|
5
5
|
import type { BopoDb } from "bopodev-db";
|
|
6
6
|
import {
|
|
7
7
|
approvalRequests,
|
|
@@ -9,11 +9,16 @@ import {
|
|
|
9
9
|
createGoal,
|
|
10
10
|
createIssue,
|
|
11
11
|
createProject,
|
|
12
|
+
createProjectWorkspace,
|
|
13
|
+
getCurrentTemplateVersion,
|
|
14
|
+
getTemplate,
|
|
12
15
|
goals,
|
|
13
16
|
listAgents,
|
|
14
17
|
listIssues,
|
|
18
|
+
listProjectWorkspaces,
|
|
15
19
|
listProjects,
|
|
16
20
|
projects,
|
|
21
|
+
updateProjectWorkspace,
|
|
17
22
|
updatePluginConfig
|
|
18
23
|
} from "bopodev-db";
|
|
19
24
|
import {
|
|
@@ -24,8 +29,14 @@ import {
|
|
|
24
29
|
runtimeConfigToStateBlobPatch
|
|
25
30
|
} from "../lib/agent-config";
|
|
26
31
|
import { resolveOpencodeRuntimeModel } from "../lib/opencode-model";
|
|
27
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
normalizeCompanyWorkspacePath,
|
|
34
|
+
resolveAgentFallbackWorkspacePath,
|
|
35
|
+
resolveProjectWorkspacePath
|
|
36
|
+
} from "../lib/instance-paths";
|
|
37
|
+
import { assertRuntimeCwdForCompany, hasText, resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
28
38
|
import { appendDurableFact } from "./memory-file-service";
|
|
39
|
+
import { applyTemplateManifest } from "./template-apply-service";
|
|
29
40
|
|
|
30
41
|
const approvalGatedActions = new Set([
|
|
31
42
|
"hire_agent",
|
|
@@ -34,7 +45,8 @@ const approvalGatedActions = new Set([
|
|
|
34
45
|
"pause_agent",
|
|
35
46
|
"terminate_agent",
|
|
36
47
|
"promote_memory_fact",
|
|
37
|
-
"grant_plugin_capabilities"
|
|
48
|
+
"grant_plugin_capabilities",
|
|
49
|
+
"apply_template"
|
|
38
50
|
]);
|
|
39
51
|
|
|
40
52
|
const hireAgentPayloadSchema = AgentCreateRequestSchema.extend({
|
|
@@ -75,6 +87,11 @@ const grantPluginCapabilitiesPayloadSchema = z.object({
|
|
|
75
87
|
grantedCapabilities: z.array(z.string().min(1)).default([]),
|
|
76
88
|
config: z.record(z.string(), z.unknown()).default({})
|
|
77
89
|
});
|
|
90
|
+
const applyTemplatePayloadSchema = z.object({
|
|
91
|
+
templateId: z.string().min(1),
|
|
92
|
+
templateVersion: z.string().min(1),
|
|
93
|
+
variables: z.record(z.string(), z.unknown()).default({})
|
|
94
|
+
});
|
|
78
95
|
const AGENT_STARTUP_PROJECT_NAME = "Agent Onboarding";
|
|
79
96
|
const AGENT_STARTUP_TASK_MARKER = "[bopodev:onboarding:agent-startup:v1]";
|
|
80
97
|
|
|
@@ -121,7 +138,7 @@ export async function resolveApproval(
|
|
|
121
138
|
let execution:
|
|
122
139
|
| {
|
|
123
140
|
applied: boolean;
|
|
124
|
-
entityType?: "agent" | "goal" | "memory";
|
|
141
|
+
entityType?: "agent" | "goal" | "memory" | "template";
|
|
125
142
|
entityId?: string;
|
|
126
143
|
entity?: Record<string, unknown>;
|
|
127
144
|
}
|
|
@@ -195,6 +212,13 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
|
|
|
195
212
|
},
|
|
196
213
|
defaultRuntimeCwd
|
|
197
214
|
});
|
|
215
|
+
if (runtimeConfig.runtimeCwd) {
|
|
216
|
+
try {
|
|
217
|
+
runtimeConfig.runtimeCwd = assertRuntimeCwdForCompany(companyId, runtimeConfig.runtimeCwd, "runtimeCwd");
|
|
218
|
+
} catch (error) {
|
|
219
|
+
throw new GovernanceError(String(error));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
198
222
|
runtimeConfig.runtimeModel = await resolveOpencodeRuntimeModel(parsed.data.providerType, runtimeConfig);
|
|
199
223
|
runtimeConfig.runtimeModel = resolveRuntimeModelForProvider(parsed.data.providerType, runtimeConfig.runtimeModel);
|
|
200
224
|
if (providerRequiresNamedModel(parsed.data.providerType) && !hasText(runtimeConfig.runtimeModel)) {
|
|
@@ -339,7 +363,7 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
|
|
|
339
363
|
});
|
|
340
364
|
return {
|
|
341
365
|
applied: true,
|
|
342
|
-
entityType: "
|
|
366
|
+
entityType: "template" as const,
|
|
343
367
|
entityId: parsed.data.pluginId,
|
|
344
368
|
entity: {
|
|
345
369
|
pluginId: parsed.data.pluginId,
|
|
@@ -349,6 +373,45 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
|
|
|
349
373
|
}
|
|
350
374
|
};
|
|
351
375
|
}
|
|
376
|
+
if (action === "apply_template") {
|
|
377
|
+
const parsed = applyTemplatePayloadSchema.safeParse(payload);
|
|
378
|
+
if (!parsed.success) {
|
|
379
|
+
throw new GovernanceError("Approval payload for template apply is invalid.");
|
|
380
|
+
}
|
|
381
|
+
const template = await getTemplate(db, companyId, parsed.data.templateId);
|
|
382
|
+
if (!template) {
|
|
383
|
+
throw new GovernanceError("Template not found for apply request.");
|
|
384
|
+
}
|
|
385
|
+
const version =
|
|
386
|
+
(await getCurrentTemplateVersion(db, companyId, parsed.data.templateId)) ??
|
|
387
|
+
null;
|
|
388
|
+
if (!version) {
|
|
389
|
+
throw new GovernanceError("Template version not found for apply request.");
|
|
390
|
+
}
|
|
391
|
+
const manifest = parsePayload(version.manifestJson);
|
|
392
|
+
const parsedManifest = TemplateManifestSchema.safeParse(manifest);
|
|
393
|
+
const normalizedManifest = parsedManifest.success
|
|
394
|
+
? parsedManifest.data
|
|
395
|
+
: TemplateManifestSchema.parse(TemplateManifestDefault);
|
|
396
|
+
const applied = await applyTemplateManifest(db, {
|
|
397
|
+
companyId,
|
|
398
|
+
templateId: template.id,
|
|
399
|
+
templateVersion: version.version,
|
|
400
|
+
templateVersionId: version.id,
|
|
401
|
+
manifest: normalizedManifest,
|
|
402
|
+
variables: parsed.data.variables
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
applied: applied.applied,
|
|
406
|
+
entityType: "template" as const,
|
|
407
|
+
entityId: template.id,
|
|
408
|
+
entity: {
|
|
409
|
+
id: template.id,
|
|
410
|
+
installId: applied.installId ?? null,
|
|
411
|
+
summary: applied.summary
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
352
415
|
|
|
353
416
|
throw new GovernanceError(`Unsupported approval action: ${action}`);
|
|
354
417
|
}
|
|
@@ -370,6 +433,7 @@ async function ensureAgentStartupProject(db: BopoDb, companyId: string) {
|
|
|
370
433
|
const projects = await listProjects(db, companyId);
|
|
371
434
|
const existing = projects.find((project) => project.name === AGENT_STARTUP_PROJECT_NAME);
|
|
372
435
|
if (existing) {
|
|
436
|
+
await ensureProjectPrimaryWorkspace(db, companyId, existing.id, AGENT_STARTUP_PROJECT_NAME);
|
|
373
437
|
return existing.id;
|
|
374
438
|
}
|
|
375
439
|
const created = await createProject(db, {
|
|
@@ -377,9 +441,50 @@ async function ensureAgentStartupProject(db: BopoDb, companyId: string) {
|
|
|
377
441
|
name: AGENT_STARTUP_PROJECT_NAME,
|
|
378
442
|
description: "Operating baseline tasks for newly approved hires."
|
|
379
443
|
});
|
|
444
|
+
if (!created) {
|
|
445
|
+
throw new Error("Failed to create startup project.");
|
|
446
|
+
}
|
|
447
|
+
await ensureProjectPrimaryWorkspace(db, companyId, created.id, AGENT_STARTUP_PROJECT_NAME);
|
|
380
448
|
return created.id;
|
|
381
449
|
}
|
|
382
450
|
|
|
451
|
+
async function ensureProjectPrimaryWorkspace(db: BopoDb, companyId: string, projectId: string, projectName: string) {
|
|
452
|
+
const existingWorkspaces = await listProjectWorkspaces(db, companyId, projectId);
|
|
453
|
+
const existingPrimary = existingWorkspaces.find((workspace) => workspace.isPrimary);
|
|
454
|
+
if (existingPrimary) {
|
|
455
|
+
if (existingPrimary.cwd) {
|
|
456
|
+
const normalized = normalizeCompanyWorkspacePath(companyId, existingPrimary.cwd);
|
|
457
|
+
await mkdir(normalized, { recursive: true });
|
|
458
|
+
}
|
|
459
|
+
return existingPrimary;
|
|
460
|
+
}
|
|
461
|
+
const defaultWorkspaceCwd = resolveProjectWorkspacePath(companyId, projectId);
|
|
462
|
+
await mkdir(defaultWorkspaceCwd, { recursive: true });
|
|
463
|
+
const fallbackWorkspace = existingWorkspaces[0];
|
|
464
|
+
if (fallbackWorkspace) {
|
|
465
|
+
const normalizedCwd = fallbackWorkspace.cwd?.trim()
|
|
466
|
+
? normalizeCompanyWorkspacePath(companyId, fallbackWorkspace.cwd)
|
|
467
|
+
: defaultWorkspaceCwd;
|
|
468
|
+
if (normalizedCwd) {
|
|
469
|
+
await mkdir(normalizedCwd, { recursive: true });
|
|
470
|
+
}
|
|
471
|
+
return updateProjectWorkspace(db, {
|
|
472
|
+
companyId,
|
|
473
|
+
projectId,
|
|
474
|
+
id: fallbackWorkspace.id,
|
|
475
|
+
cwd: normalizedCwd,
|
|
476
|
+
isPrimary: true
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return createProjectWorkspace(db, {
|
|
480
|
+
companyId,
|
|
481
|
+
projectId,
|
|
482
|
+
name: projectName,
|
|
483
|
+
cwd: defaultWorkspaceCwd,
|
|
484
|
+
isPrimary: true
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
383
488
|
async function ensureAgentStartupIssue(
|
|
384
489
|
db: BopoDb,
|
|
385
490
|
companyId: string,
|
|
@@ -388,7 +493,7 @@ async function ensureAgentStartupIssue(
|
|
|
388
493
|
role: string
|
|
389
494
|
) {
|
|
390
495
|
const title = `Set up ${role} operating files`;
|
|
391
|
-
const body = buildAgentStartupTaskBody(agentId);
|
|
496
|
+
const body = buildAgentStartupTaskBody(companyId, agentId);
|
|
392
497
|
const existingIssues = await listIssues(db, companyId);
|
|
393
498
|
const existing = existingIssues.find(
|
|
394
499
|
(issue) =>
|
|
@@ -414,24 +519,26 @@ async function ensureAgentStartupIssue(
|
|
|
414
519
|
return created.id;
|
|
415
520
|
}
|
|
416
521
|
|
|
417
|
-
function buildAgentStartupTaskBody(agentId: string) {
|
|
418
|
-
const
|
|
522
|
+
function buildAgentStartupTaskBody(companyId: string, agentId: string) {
|
|
523
|
+
const agentWorkspaceRoot = resolveAgentFallbackWorkspacePath(companyId, agentId);
|
|
524
|
+
const agentOperatingFolder = `${agentWorkspaceRoot}/operating`;
|
|
419
525
|
return [
|
|
420
526
|
AGENT_STARTUP_TASK_MARKER,
|
|
421
527
|
"",
|
|
422
528
|
`Create your operating baseline before starting feature delivery work.`,
|
|
423
529
|
"",
|
|
424
|
-
`1. Create
|
|
530
|
+
`1. Create your operating folder at \`${agentOperatingFolder}/\` (system path, outside project workspaces).`,
|
|
425
531
|
"2. Author these files with your own responsibilities and working style:",
|
|
426
|
-
` - \`${
|
|
427
|
-
` - \`${
|
|
428
|
-
` - \`${
|
|
429
|
-
` - \`${
|
|
430
|
-
`3. Update your own agent runtime config via \`PUT /agents/:agentId\` and set \`runtimeConfig.bootstrapPrompt\` to reference \`${
|
|
532
|
+
` - \`${agentOperatingFolder}/AGENTS.md\``,
|
|
533
|
+
` - \`${agentOperatingFolder}/HEARTBEAT.md\``,
|
|
534
|
+
` - \`${agentOperatingFolder}/SOUL.md\``,
|
|
535
|
+
` - \`${agentOperatingFolder}/TOOLS.md\``,
|
|
536
|
+
`3. Update your own agent runtime config via \`PUT /agents/:agentId\` and set \`runtimeConfig.bootstrapPrompt\` to reference \`${agentOperatingFolder}/AGENTS.md\` as your primary guide.`,
|
|
431
537
|
"4. Post an issue comment summarizing completed setup artifacts.",
|
|
432
538
|
"",
|
|
433
539
|
"Safety checks:",
|
|
434
|
-
"- Do not
|
|
540
|
+
"- Do not write operating/system files under any project workspace folder.",
|
|
541
|
+
"- Do not overwrite another agent's operating folder.",
|
|
435
542
|
"- Keep content original to your role and scope."
|
|
436
543
|
].join("\n");
|
|
437
544
|
}
|
|
@@ -25,8 +25,9 @@ import {
|
|
|
25
25
|
} from "bopodev-db";
|
|
26
26
|
import { appendAuditEvent, appendCost } from "bopodev-db";
|
|
27
27
|
import { parseRuntimeConfigFromAgentRow } from "../lib/agent-config";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
28
|
+
import { bootstrapRepositoryWorkspace, ensureIsolatedGitWorktree, GitRuntimeError } from "../lib/git-runtime";
|
|
29
|
+
import { isInsidePath, normalizeCompanyWorkspacePath, resolveProjectWorkspacePath } from "../lib/instance-paths";
|
|
30
|
+
import { assertRuntimeCwdForCompany, getProjectWorkspaceContextMap, hasText, resolveAgentFallbackWorkspace } from "../lib/workspace-policy";
|
|
30
31
|
import type { RealtimeHub } from "../realtime/hub";
|
|
31
32
|
import { createHeartbeatRunsRealtimeEvent } from "../realtime/heartbeat-runs";
|
|
32
33
|
import { publishOfficeOccupantForAgent } from "../realtime/office-space";
|
|
@@ -773,6 +774,16 @@ export async function runHeartbeatForAgent(
|
|
|
773
774
|
externalAbortSignal: activeRunAbort.signal
|
|
774
775
|
});
|
|
775
776
|
executionSummary = execution.summary;
|
|
777
|
+
const normalizedUsage = execution.usage ?? {
|
|
778
|
+
inputTokens: Math.max(0, execution.tokenInput),
|
|
779
|
+
cachedInputTokens: 0,
|
|
780
|
+
outputTokens: Math.max(0, execution.tokenOutput),
|
|
781
|
+
...(execution.usdCost > 0 ? { costUsd: execution.usdCost } : {}),
|
|
782
|
+
...(execution.summary ? { summary: execution.summary } : {})
|
|
783
|
+
};
|
|
784
|
+
const effectiveTokenInput = normalizedUsage.inputTokens + normalizedUsage.cachedInputTokens;
|
|
785
|
+
const effectiveTokenOutput = normalizedUsage.outputTokens;
|
|
786
|
+
const effectiveRuntimeUsdCost = normalizedUsage.costUsd ?? (execution.usdCost > 0 ? execution.usdCost : 0);
|
|
776
787
|
const afterAdapterHook = await runPluginHook(db, {
|
|
777
788
|
hook: "afterAdapterExecute",
|
|
778
789
|
context: {
|
|
@@ -806,8 +817,10 @@ export async function runHeartbeatForAgent(
|
|
|
806
817
|
runtimeModelId: effectivePricingModelId ?? runtimeModelId,
|
|
807
818
|
pricingProviderType: effectivePricingProviderType,
|
|
808
819
|
pricingModelId: effectivePricingModelId,
|
|
809
|
-
tokenInput:
|
|
810
|
-
tokenOutput:
|
|
820
|
+
tokenInput: effectiveTokenInput,
|
|
821
|
+
tokenOutput: effectiveTokenOutput,
|
|
822
|
+
runtimeUsdCost: effectiveRuntimeUsdCost,
|
|
823
|
+
failureType: readTraceString(execution.trace, "failureType"),
|
|
811
824
|
issueId: primaryIssueId,
|
|
812
825
|
projectId: primaryProjectId,
|
|
813
826
|
agentId,
|
|
@@ -865,8 +878,8 @@ export async function runHeartbeatForAgent(
|
|
|
865
878
|
if (
|
|
866
879
|
execution.nextState ||
|
|
867
880
|
executionUsdCost > 0 ||
|
|
868
|
-
|
|
869
|
-
|
|
881
|
+
effectiveTokenInput > 0 ||
|
|
882
|
+
effectiveTokenOutput > 0 ||
|
|
870
883
|
execution.status !== "skipped"
|
|
871
884
|
) {
|
|
872
885
|
await db
|
|
@@ -875,7 +888,7 @@ export async function runHeartbeatForAgent(
|
|
|
875
888
|
stateBlob: JSON.stringify(execution.nextState ?? state),
|
|
876
889
|
runtimeModel: effectivePricingModelId ?? persistedRuntime.runtimeModel ?? null,
|
|
877
890
|
usedBudgetUsd: sql`${agents.usedBudgetUsd} + ${executionUsdCost}`,
|
|
878
|
-
tokenUsage: sql`${agents.tokenUsage} + ${
|
|
891
|
+
tokenUsage: sql`${agents.tokenUsage} + ${effectiveTokenInput + effectiveTokenOutput}`,
|
|
879
892
|
updatedAt: new Date()
|
|
880
893
|
})
|
|
881
894
|
.where(and(eq(agents.companyId, companyId), eq(agents.id, agentId)));
|
|
@@ -883,8 +896,8 @@ export async function runHeartbeatForAgent(
|
|
|
883
896
|
|
|
884
897
|
const shouldAdvanceIssuesToReview = shouldPromoteIssuesToReview({
|
|
885
898
|
summary: execution.summary,
|
|
886
|
-
tokenInput:
|
|
887
|
-
tokenOutput:
|
|
899
|
+
tokenInput: effectiveTokenInput,
|
|
900
|
+
tokenOutput: effectiveTokenOutput,
|
|
888
901
|
usdCost: executionUsdCost,
|
|
889
902
|
trace: executionTrace,
|
|
890
903
|
outcome: executionOutcome
|
|
@@ -929,8 +942,8 @@ export async function runHeartbeatForAgent(
|
|
|
929
942
|
summary: execution.summary,
|
|
930
943
|
outcome: executionOutcome,
|
|
931
944
|
usage: {
|
|
932
|
-
tokenInput:
|
|
933
|
-
tokenOutput:
|
|
945
|
+
tokenInput: effectiveTokenInput,
|
|
946
|
+
tokenOutput: effectiveTokenOutput,
|
|
934
947
|
usdCost: executionUsdCost
|
|
935
948
|
}
|
|
936
949
|
}
|
|
@@ -1062,9 +1075,9 @@ export async function runHeartbeatForAgent(
|
|
|
1062
1075
|
outcome: executionOutcome,
|
|
1063
1076
|
issueIds,
|
|
1064
1077
|
usage: {
|
|
1065
|
-
tokenInput:
|
|
1066
|
-
tokenOutput:
|
|
1067
|
-
usdCost:
|
|
1078
|
+
tokenInput: effectiveTokenInput,
|
|
1079
|
+
tokenOutput: effectiveTokenOutput,
|
|
1080
|
+
usdCost: executionUsdCost,
|
|
1068
1081
|
source: readTraceString(execution.trace, "usageSource") ?? "unknown"
|
|
1069
1082
|
},
|
|
1070
1083
|
trace: execution.trace ?? null,
|
|
@@ -1401,7 +1414,10 @@ async function buildHeartbeatContext(
|
|
|
1401
1414
|
.where(and(eq(projects.companyId, companyId), inArray(projects.id, projectIds)))
|
|
1402
1415
|
: [];
|
|
1403
1416
|
const projectNameById = new Map(projectRows.map((row) => [row.id, row.name]));
|
|
1404
|
-
const
|
|
1417
|
+
const projectWorkspaceContextMap = await getProjectWorkspaceContextMap(db, companyId, projectIds);
|
|
1418
|
+
const projectWorkspaceMap = new Map(
|
|
1419
|
+
Array.from(projectWorkspaceContextMap.entries()).map(([projectId, context]) => [projectId, context.cwd])
|
|
1420
|
+
);
|
|
1405
1421
|
const issueIds = input.workItems.map((item) => item.id);
|
|
1406
1422
|
const attachmentRows =
|
|
1407
1423
|
issueIds.length > 0
|
|
@@ -1432,6 +1448,9 @@ async function buildHeartbeatContext(
|
|
|
1432
1448
|
for (const row of attachmentRows) {
|
|
1433
1449
|
const projectWorkspace = projectWorkspaceMap.get(row.projectId) ?? resolveProjectWorkspacePath(companyId, row.projectId);
|
|
1434
1450
|
const absolutePath = resolve(projectWorkspace, row.relativePath);
|
|
1451
|
+
if (!isInsidePath(projectWorkspace, absolutePath)) {
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1435
1454
|
const existing = attachmentsByIssue.get(row.issueId) ?? [];
|
|
1436
1455
|
existing.push({
|
|
1437
1456
|
id: row.id,
|
|
@@ -1761,7 +1780,7 @@ async function resolveRuntimeWorkspaceForWorkItems(
|
|
|
1761
1780
|
db: BopoDb,
|
|
1762
1781
|
companyId: string,
|
|
1763
1782
|
agentId: string,
|
|
1764
|
-
workItems: Array<{ project_id: string }>,
|
|
1783
|
+
workItems: Array<{ id?: string; project_id: string }>,
|
|
1765
1784
|
runtime:
|
|
1766
1785
|
| {
|
|
1767
1786
|
command?: string;
|
|
@@ -1785,22 +1804,76 @@ async function resolveRuntimeWorkspaceForWorkItems(
|
|
|
1785
1804
|
const normalizedRuntimeCwd = runtime?.cwd?.trim();
|
|
1786
1805
|
const warnings: string[] = [];
|
|
1787
1806
|
const projectIds = Array.from(new Set(workItems.map((item) => item.project_id)));
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
let selectedProjectWorkspace: string | null = null;
|
|
1807
|
+
const projectWorkspaceContextMap = await getProjectWorkspaceContextMap(db, companyId, projectIds);
|
|
1791
1808
|
for (const projectId of projectIds) {
|
|
1792
|
-
const
|
|
1793
|
-
if (
|
|
1794
|
-
|
|
1795
|
-
|
|
1809
|
+
const projectContext = projectWorkspaceContextMap.get(projectId);
|
|
1810
|
+
if (!projectContext) {
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
const mode = projectContext.policy?.mode ?? "project_primary";
|
|
1814
|
+
const baseWorkspaceCwd = hasText(projectContext.cwd)
|
|
1815
|
+
? normalizeCompanyWorkspacePath(companyId, projectContext.cwd as string)
|
|
1816
|
+
: projectContext.repoUrl
|
|
1817
|
+
? resolveProjectWorkspacePath(companyId, projectId)
|
|
1818
|
+
: null;
|
|
1819
|
+
if (mode === "agent_default" && hasText(normalizedRuntimeCwd)) {
|
|
1820
|
+
const boundedRuntimeCwd = assertRuntimeCwdForCompany(companyId, normalizedRuntimeCwd!, "runtime.cwd");
|
|
1821
|
+
return {
|
|
1822
|
+
source: "agent_runtime",
|
|
1823
|
+
warnings,
|
|
1824
|
+
runtime: {
|
|
1825
|
+
...runtime,
|
|
1826
|
+
cwd: boundedRuntimeCwd
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
if (!baseWorkspaceCwd) {
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
let selectedWorkspaceCwd = normalizeCompanyWorkspacePath(companyId, baseWorkspaceCwd);
|
|
1834
|
+
await mkdir(baseWorkspaceCwd, { recursive: true });
|
|
1835
|
+
try {
|
|
1836
|
+
if (hasText(projectContext.repoUrl)) {
|
|
1837
|
+
const bootstrap = await bootstrapRepositoryWorkspace({
|
|
1838
|
+
companyId,
|
|
1839
|
+
projectId,
|
|
1840
|
+
cwd: baseWorkspaceCwd,
|
|
1841
|
+
repoUrl: projectContext.repoUrl as string,
|
|
1842
|
+
repoRef: projectContext.repoRef,
|
|
1843
|
+
policy: projectContext.policy,
|
|
1844
|
+
runtimeEnv: runtime?.env
|
|
1845
|
+
});
|
|
1846
|
+
selectedWorkspaceCwd = normalizeCompanyWorkspacePath(companyId, bootstrap.cwd);
|
|
1847
|
+
}
|
|
1848
|
+
if (
|
|
1849
|
+
mode === "isolated" &&
|
|
1850
|
+
projectContext.policy?.strategy?.type === "git_worktree" &&
|
|
1851
|
+
resolveGitWorktreeIsolationEnabled()
|
|
1852
|
+
) {
|
|
1853
|
+
const projectIssue = workItems.find((item) => item.project_id === projectId);
|
|
1854
|
+
const worktree = await ensureIsolatedGitWorktree({
|
|
1855
|
+
companyId,
|
|
1856
|
+
repoCwd: selectedWorkspaceCwd,
|
|
1857
|
+
projectId,
|
|
1858
|
+
agentId,
|
|
1859
|
+
issueId: projectIssue?.id ?? null,
|
|
1860
|
+
repoRef: projectContext.repoRef,
|
|
1861
|
+
policy: projectContext.policy
|
|
1862
|
+
});
|
|
1863
|
+
selectedWorkspaceCwd = normalizeCompanyWorkspacePath(companyId, worktree.cwd);
|
|
1864
|
+
} else if (mode === "isolated" && projectContext.policy?.strategy?.type === "git_worktree") {
|
|
1865
|
+
warnings.push(
|
|
1866
|
+
"Project execution workspace policy mode 'isolated' is configured with git_worktree, but BOPO_ENABLE_GIT_WORKTREE_ISOLATION is disabled. Falling back to primary project workspace."
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
const message = error instanceof GitRuntimeError ? error.message : String(error);
|
|
1871
|
+
warnings.push(`Workspace bootstrap failed for project '${projectId}': ${message}`);
|
|
1796
1872
|
}
|
|
1797
|
-
}
|
|
1798
1873
|
|
|
1799
|
-
|
|
1800
|
-
await mkdir(selectedProjectWorkspace, { recursive: true });
|
|
1801
|
-
if (hasText(normalizedRuntimeCwd) && normalizedRuntimeCwd !== selectedProjectWorkspace) {
|
|
1874
|
+
if (hasText(normalizedRuntimeCwd) && normalizedRuntimeCwd !== selectedWorkspaceCwd) {
|
|
1802
1875
|
warnings.push(
|
|
1803
|
-
`Runtime cwd '${normalizedRuntimeCwd}' was overridden to project workspace '${
|
|
1876
|
+
`Runtime cwd '${normalizedRuntimeCwd}' was overridden to project workspace '${selectedWorkspaceCwd}' for assigned work.`
|
|
1804
1877
|
);
|
|
1805
1878
|
}
|
|
1806
1879
|
return {
|
|
@@ -1808,27 +1881,28 @@ async function resolveRuntimeWorkspaceForWorkItems(
|
|
|
1808
1881
|
warnings,
|
|
1809
1882
|
runtime: {
|
|
1810
1883
|
...runtime,
|
|
1811
|
-
cwd:
|
|
1884
|
+
cwd: selectedWorkspaceCwd
|
|
1812
1885
|
}
|
|
1813
1886
|
};
|
|
1814
1887
|
}
|
|
1815
1888
|
|
|
1816
1889
|
if (projectIds.length > 0) {
|
|
1817
|
-
warnings.push("Assigned project has no
|
|
1890
|
+
warnings.push("Assigned project has no primary workspace cwd/repo configured. Falling back to agent workspace.");
|
|
1818
1891
|
}
|
|
1819
1892
|
|
|
1820
1893
|
if (hasText(normalizedRuntimeCwd)) {
|
|
1894
|
+
const boundedRuntimeCwd = assertRuntimeCwdForCompany(companyId, normalizedRuntimeCwd!, "runtime.cwd");
|
|
1821
1895
|
return {
|
|
1822
1896
|
source: "agent_runtime",
|
|
1823
1897
|
warnings,
|
|
1824
1898
|
runtime: {
|
|
1825
1899
|
...runtime,
|
|
1826
|
-
cwd:
|
|
1900
|
+
cwd: boundedRuntimeCwd
|
|
1827
1901
|
}
|
|
1828
1902
|
};
|
|
1829
1903
|
}
|
|
1830
1904
|
|
|
1831
|
-
const fallbackWorkspace = resolveAgentFallbackWorkspace(companyId, agentId);
|
|
1905
|
+
const fallbackWorkspace = normalizeCompanyWorkspacePath(companyId, resolveAgentFallbackWorkspace(companyId, agentId));
|
|
1832
1906
|
await mkdir(fallbackWorkspace, { recursive: true });
|
|
1833
1907
|
warnings.push(`Runtime cwd was not configured. Falling back to '${fallbackWorkspace}'.`);
|
|
1834
1908
|
return {
|
|
@@ -1841,6 +1915,13 @@ async function resolveRuntimeWorkspaceForWorkItems(
|
|
|
1841
1915
|
};
|
|
1842
1916
|
}
|
|
1843
1917
|
|
|
1918
|
+
function resolveGitWorktreeIsolationEnabled() {
|
|
1919
|
+
const value = String(process.env.BOPO_ENABLE_GIT_WORKTREE_ISOLATION ?? "")
|
|
1920
|
+
.trim()
|
|
1921
|
+
.toLowerCase();
|
|
1922
|
+
return value === "1" || value === "true";
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1844
1925
|
function resolveStaleRunThresholdMs() {
|
|
1845
1926
|
const parsed = Number(process.env.BOPO_HEARTBEAT_STALE_RUN_MS ?? 10 * 60 * 1000);
|
|
1846
1927
|
if (!Number.isFinite(parsed) || parsed < 1_000) {
|
|
@@ -2430,6 +2511,8 @@ async function appendFinishedRunCostEntry(input: {
|
|
|
2430
2511
|
pricingModelId?: string | null;
|
|
2431
2512
|
tokenInput: number;
|
|
2432
2513
|
tokenOutput: number;
|
|
2514
|
+
runtimeUsdCost?: number;
|
|
2515
|
+
failureType?: string | null;
|
|
2433
2516
|
issueId?: string | null;
|
|
2434
2517
|
projectId?: string | null;
|
|
2435
2518
|
agentId?: string | null;
|
|
@@ -2446,24 +2529,41 @@ async function appendFinishedRunCostEntry(input: {
|
|
|
2446
2529
|
});
|
|
2447
2530
|
|
|
2448
2531
|
const shouldPersist = input.status === "ok" || input.status === "failed";
|
|
2449
|
-
|
|
2532
|
+
const runtimeUsdCost = Math.max(0, input.runtimeUsdCost ?? 0);
|
|
2533
|
+
const pricedUsdCost = Math.max(0, pricingDecision.usdCost);
|
|
2534
|
+
const shouldUseRuntimeUsdCost = pricedUsdCost <= 0 && runtimeUsdCost > 0;
|
|
2535
|
+
const baseUsdCost = shouldUseRuntimeUsdCost ? runtimeUsdCost : pricedUsdCost;
|
|
2536
|
+
const effectiveUsdCost =
|
|
2537
|
+
baseUsdCost > 0
|
|
2538
|
+
? baseUsdCost
|
|
2539
|
+
: input.status === "failed" && input.failureType !== "spawn_error"
|
|
2540
|
+
? 0.000001
|
|
2541
|
+
: 0;
|
|
2542
|
+
const effectivePricingSource = pricingDecision.pricingSource;
|
|
2543
|
+
const shouldPersistWithUsage =
|
|
2544
|
+
shouldPersist && (input.tokenInput > 0 || input.tokenOutput > 0 || effectiveUsdCost > 0);
|
|
2545
|
+
if (shouldPersistWithUsage) {
|
|
2450
2546
|
await appendCost(input.db, {
|
|
2451
2547
|
companyId: input.companyId,
|
|
2452
2548
|
providerType: input.providerType,
|
|
2453
2549
|
runtimeModelId: input.runtimeModelId,
|
|
2454
2550
|
pricingProviderType: pricingDecision.pricingProviderType,
|
|
2455
2551
|
pricingModelId: pricingDecision.pricingModelId,
|
|
2456
|
-
pricingSource:
|
|
2552
|
+
pricingSource: effectivePricingSource,
|
|
2457
2553
|
tokenInput: input.tokenInput,
|
|
2458
2554
|
tokenOutput: input.tokenOutput,
|
|
2459
|
-
usdCost:
|
|
2555
|
+
usdCost: effectiveUsdCost.toFixed(6),
|
|
2460
2556
|
issueId: input.issueId ?? null,
|
|
2461
2557
|
projectId: input.projectId ?? null,
|
|
2462
2558
|
agentId: input.agentId ?? null
|
|
2463
2559
|
});
|
|
2464
2560
|
}
|
|
2465
2561
|
|
|
2466
|
-
return
|
|
2562
|
+
return {
|
|
2563
|
+
...pricingDecision,
|
|
2564
|
+
pricingSource: effectivePricingSource,
|
|
2565
|
+
usdCost: effectiveUsdCost
|
|
2566
|
+
};
|
|
2467
2567
|
}
|
|
2468
2568
|
|
|
2469
2569
|
function isHeartbeatDue(cronExpression: string, lastRunAt: Date | null, now: Date) {
|
|
@@ -214,8 +214,8 @@ export async function ensureCompanyBuiltinPluginDefaults(db: BopoDb, companyId:
|
|
|
214
214
|
const existing = await listCompanyPluginConfigs(db, companyId);
|
|
215
215
|
const existingIds = new Set(existing.map((row) => row.pluginId));
|
|
216
216
|
const defaults = [
|
|
217
|
-
{ pluginId: "trace-exporter", enabled:
|
|
218
|
-
{ pluginId: "memory-enricher", enabled:
|
|
217
|
+
{ pluginId: "trace-exporter", enabled: false, priority: 40 },
|
|
218
|
+
{ pluginId: "memory-enricher", enabled: false, priority: 60 },
|
|
219
219
|
{ pluginId: "queue-publisher", enabled: false, priority: 80 },
|
|
220
220
|
{ pluginId: "heartbeat-tagger", enabled: false, priority: 90 }
|
|
221
221
|
];
|