feat: dedicated PVC per agent for OPENCODE_DB (FAR-63, Option B)

Replaces the Option A shared-PVC path implementation with a long-lived
dedicated PVC per agent, mounted at /opencode-db with OPENCODE_DB=/opencode-db.

Changes:
- k8s-client.ts: add getPvc/createPvc/deletePvc CoreV1Api helpers
- execute.ts: add ensureAgentDbPvc() that gets-or-creates a PVC named
  opencode-db-<agentId> before Job creation; pass agentDbClaimName through
  to buildJobManifest; return null for ephemeral mode (emptyDir used instead)
- job-manifest.ts: accept agentDbClaimName on JobBuildInput; mount dedicated
  PVC or emptyDir at /opencode-db; set OPENCODE_DB=/opencode-db; revert init
  container to simple form (no mkdir, no PVC mount)
- config-schema.ts: replace opencodeDbMode/opencodeDbPath with agentDbMode
  (dedicated_pvc|ephemeral, default dedicated_pvc), agentDbStorageClass
  (required for dedicated_pvc), agentDbStorageCapacity (default 1Gi)
- test.ts: add create/delete RBAC checks for persistentvolumeclaims
- pvc.test.ts: unit tests for ensureAgentDbPvc (7 cases incl. error paths)
- 289/289 tests pass; typecheck clean
- No agent-delete hook exists; opencode-db PVC janitor routine is a deferred
  follow-up task

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 12:38:54 +00:00
parent 908880a25b
commit 46ce5cc599
9 changed files with 330 additions and 132 deletions
+43
View File
@@ -174,3 +174,46 @@ export function resetCache(): void {
kcCache.clear();
cachedSelfPod = null;
}
function isNotFound(err: unknown): boolean {
if (!(err instanceof Error)) return false;
const asAny = err as unknown as Record<string, unknown>;
if (typeof asAny.statusCode === "number" && asAny.statusCode === 404) return true;
const resp = asAny.response as Record<string, unknown> | undefined;
return typeof resp?.statusCode === "number" && resp.statusCode === 404;
}
/** Returns the PVC if it exists, or null if not found. Throws on other errors. */
export async function getPvc(
namespace: string,
name: string,
kubeconfigPath?: string,
): Promise<k8s.V1PersistentVolumeClaim | null> {
try {
return await getCoreApi(kubeconfigPath).readNamespacedPersistentVolumeClaim({ name, namespace });
} catch (err) {
if (isNotFound(err)) return null;
throw err;
}
}
export async function createPvc(
namespace: string,
spec: k8s.V1PersistentVolumeClaim,
kubeconfigPath?: string,
): Promise<k8s.V1PersistentVolumeClaim> {
return getCoreApi(kubeconfigPath).createNamespacedPersistentVolumeClaim({ namespace, body: spec });
}
/** Deletes a PVC, ignoring 404 (already gone). */
export async function deletePvc(
namespace: string,
name: string,
kubeconfigPath?: string,
): Promise<void> {
try {
await getCoreApi(kubeconfigPath).deleteNamespacedPersistentVolumeClaim({ name, namespace });
} catch (err) {
if (!isNotFound(err)) throw err;
}
}