@wardby/cli 0.1.0 → 0.2.1

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 (168) hide show
  1. package/README.md +183 -96
  2. package/deploy/local/docker-compose.yml +5 -5
  3. package/dist/claude-coding-worker/driver.d.ts +1 -0
  4. package/dist/claude-coding-worker/driver.js +4 -1
  5. package/dist/claude-coding-worker/main.js +3 -2
  6. package/dist/cli-help.d.ts +1 -1
  7. package/dist/cli-help.js +5 -0
  8. package/dist/cli.js +0 -0
  9. package/dist/coding/protocol.d.ts +28 -3
  10. package/dist/coding/protocol.js +53 -2
  11. package/dist/coding-worker/artifact.d.ts +12 -0
  12. package/dist/coding-worker/artifact.js +48 -6
  13. package/dist/coding-worker/driver.d.ts +1 -0
  14. package/dist/coding-worker/driver.js +4 -1
  15. package/dist/coding-worker/errors.d.ts +7 -0
  16. package/dist/coding-worker/errors.js +18 -0
  17. package/dist/coding-worker/main.js +3 -2
  18. package/dist/core/budget-groups.d.ts +1 -1
  19. package/dist/core/coding-queue.d.ts +1 -1
  20. package/dist/core/datastores.d.ts +1 -1
  21. package/dist/core/db.d.ts +36 -2
  22. package/dist/core/db.js +69 -2
  23. package/dist/core/dispatch.d.ts +16 -1
  24. package/dist/core/dispatch.js +33 -5
  25. package/dist/core/http-runtime.js +3 -4
  26. package/dist/core/lease.d.ts +1 -1
  27. package/dist/core/private-directory.d.ts +20 -0
  28. package/dist/core/private-directory.js +36 -0
  29. package/dist/core/reconciler.d.ts +1 -1
  30. package/dist/core/runner.d.ts +1 -1
  31. package/dist/core/scheduler.d.ts +1 -1
  32. package/dist/core/secrets.d.ts +1 -1
  33. package/dist/core/subagent-memory-tools.d.ts +1 -1
  34. package/dist/core/webhooks.d.ts +1 -1
  35. package/dist/env.js +8 -1
  36. package/dist/generated/prisma/browser.d.ts +228 -0
  37. package/dist/generated/prisma/browser.js +17 -0
  38. package/dist/generated/prisma/client.d.ts +247 -0
  39. package/dist/generated/prisma/client.js +34 -0
  40. package/dist/generated/prisma/commonInputTypes.d.ts +809 -0
  41. package/dist/generated/prisma/commonInputTypes.js +10 -0
  42. package/dist/generated/prisma/enums.d.ts +53 -0
  43. package/dist/generated/prisma/enums.js +54 -0
  44. package/dist/generated/prisma/internal/class.d.ts +462 -0
  45. package/dist/generated/prisma/internal/class.js +49 -0
  46. package/dist/generated/prisma/internal/prismaNamespace.d.ts +3325 -0
  47. package/dist/generated/prisma/internal/prismaNamespace.js +455 -0
  48. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +449 -0
  49. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +426 -0
  50. package/dist/generated/prisma/models/Agent.d.ts +3267 -0
  51. package/dist/generated/prisma/models/Agent.js +1 -0
  52. package/dist/generated/prisma/models/AgentDatastore.d.ts +1210 -0
  53. package/dist/generated/prisma/models/AgentDatastore.js +1 -0
  54. package/dist/generated/prisma/models/AgentMemory.d.ts +986 -0
  55. package/dist/generated/prisma/models/AgentMemory.js +1 -0
  56. package/dist/generated/prisma/models/AgentSecret.d.ts +1212 -0
  57. package/dist/generated/prisma/models/AgentSecret.js +1 -0
  58. package/dist/generated/prisma/models/AgentSubAgent.d.ts +1217 -0
  59. package/dist/generated/prisma/models/AgentSubAgent.js +1 -0
  60. package/dist/generated/prisma/models/AgentTool.d.ts +1309 -0
  61. package/dist/generated/prisma/models/AgentTool.js +1 -0
  62. package/dist/generated/prisma/models/AuthFormChallenge.d.ts +1298 -0
  63. package/dist/generated/prisma/models/AuthFormChallenge.js +1 -0
  64. package/dist/generated/prisma/models/AuthLoginKey.d.ts +1245 -0
  65. package/dist/generated/prisma/models/AuthLoginKey.js +1 -0
  66. package/dist/generated/prisma/models/AuthRateLimit.d.ts +979 -0
  67. package/dist/generated/prisma/models/AuthRateLimit.js +1 -0
  68. package/dist/generated/prisma/models/AuthSession.d.ts +1376 -0
  69. package/dist/generated/prisma/models/AuthSession.js +1 -0
  70. package/dist/generated/prisma/models/AuthUser.d.ts +1645 -0
  71. package/dist/generated/prisma/models/AuthUser.js +1 -0
  72. package/dist/generated/prisma/models/BudgetGroup.d.ts +1548 -0
  73. package/dist/generated/prisma/models/BudgetGroup.js +1 -0
  74. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +1430 -0
  75. package/dist/generated/prisma/models/CodingAgentProfile.js +1 -0
  76. package/dist/generated/prisma/models/CodingProxyRequest.d.ts +1749 -0
  77. package/dist/generated/prisma/models/CodingProxyRequest.js +1 -0
  78. package/dist/generated/prisma/models/CodingProxySession.d.ts +1505 -0
  79. package/dist/generated/prisma/models/CodingProxySession.js +1 -0
  80. package/dist/generated/prisma/models/CodingRun.d.ts +2402 -0
  81. package/dist/generated/prisma/models/CodingRun.js +1 -0
  82. package/dist/generated/prisma/models/Datastore.d.ts +1434 -0
  83. package/dist/generated/prisma/models/Datastore.js +1 -0
  84. package/dist/generated/prisma/models/DatastoreEntry.d.ts +1314 -0
  85. package/dist/generated/prisma/models/DatastoreEntry.js +1 -0
  86. package/dist/generated/prisma/models/OAuthAuthorizationCode.d.ts +1526 -0
  87. package/dist/generated/prisma/models/OAuthAuthorizationCode.js +1 -0
  88. package/dist/generated/prisma/models/OAuthAuthorizationRequest.d.ts +1350 -0
  89. package/dist/generated/prisma/models/OAuthAuthorizationRequest.js +1 -0
  90. package/dist/generated/prisma/models/OAuthClient.d.ts +1289 -0
  91. package/dist/generated/prisma/models/OAuthClient.js +1 -0
  92. package/dist/generated/prisma/models/OAuthFamily.d.ts +1540 -0
  93. package/dist/generated/prisma/models/OAuthFamily.js +1 -0
  94. package/dist/generated/prisma/models/OAuthGrant.d.ts +1280 -0
  95. package/dist/generated/prisma/models/OAuthGrant.js +1 -0
  96. package/dist/generated/prisma/models/Principal.d.ts +1790 -0
  97. package/dist/generated/prisma/models/Principal.js +1 -0
  98. package/dist/generated/prisma/models/Run.d.ts +2281 -0
  99. package/dist/generated/prisma/models/Run.js +1 -0
  100. package/dist/generated/prisma/models/SchedulerLease.d.ts +970 -0
  101. package/dist/generated/prisma/models/SchedulerLease.js +1 -0
  102. package/dist/generated/prisma/models/Secret.d.ts +1399 -0
  103. package/dist/generated/prisma/models/Secret.js +1 -0
  104. package/dist/generated/prisma/models/SecretElicitationOutcome.d.ts +973 -0
  105. package/dist/generated/prisma/models/SecretElicitationOutcome.js +1 -0
  106. package/dist/generated/prisma/models/Task.d.ts +1176 -0
  107. package/dist/generated/prisma/models/Task.js +1 -0
  108. package/dist/generated/prisma/models/Tool.d.ts +1478 -0
  109. package/dist/generated/prisma/models/Tool.js +1 -0
  110. package/dist/generated/prisma/models/Webhook.d.ts +1386 -0
  111. package/dist/generated/prisma/models/Webhook.js +1 -0
  112. package/dist/generated/prisma/models.d.ts +32 -0
  113. package/dist/generated/prisma/models.js +1 -0
  114. package/dist/import/create.d.ts +1 -1
  115. package/dist/import/create.js +1 -1
  116. package/dist/import/index.d.ts +1 -1
  117. package/dist/mcp/auth/ownership.d.ts +19 -19
  118. package/dist/mcp/auth/principal.d.ts +1 -1
  119. package/dist/mcp/auth/resource-server.d.ts +1 -1
  120. package/dist/mcp/auth/self-hosted/browser.js +20 -6
  121. package/dist/mcp/auth/self-hosted/cli.d.ts +1 -1
  122. package/dist/mcp/auth/self-hosted/credentials.d.ts +1 -1
  123. package/dist/mcp/auth/self-hosted/rate-limit.d.ts +1 -1
  124. package/dist/mcp/auth/self-hosted/session.d.ts +1 -1
  125. package/dist/mcp/context.d.ts +1 -1
  126. package/dist/mcp/server.d.ts +1 -1
  127. package/dist/mcp/shared/page-style.d.ts +16 -0
  128. package/dist/mcp/shared/page-style.js +129 -0
  129. package/dist/mcp/tasks/manager.d.ts +1 -1
  130. package/dist/mcp/tools/agents.js +1 -1
  131. package/dist/mcp/tools/datastore.js +1 -1
  132. package/dist/mcp/tools/scheduling.js +1 -1
  133. package/dist/mcp/tools/secret-elicitation-form.d.ts +1 -1
  134. package/dist/mcp/tools/secret-elicitation-form.js +10 -28
  135. package/dist/mcp/tools/secret-elicitation.d.ts +1 -1
  136. package/dist/mcp/tools/subagents.js +1 -1
  137. package/dist/mcp/tools/tools.js +1 -1
  138. package/dist/mcp/transport/streamable-http.d.ts +1 -1
  139. package/dist/mcp/unattended-schedules.d.ts +1 -1
  140. package/dist/mcp/webhooks/ingress.d.ts +1 -1
  141. package/dist/providers/auth/index.d.ts +1 -1
  142. package/dist/providers/auth/self-hosted.d.ts +2 -2
  143. package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -1
  144. package/dist/providers/coding-proxy/runtime.d.ts +1 -1
  145. package/dist/providers/datastore/postgres.d.ts +1 -1
  146. package/dist/providers/executor/build.d.ts +1 -1
  147. package/dist/providers/executor/composition.d.ts +1 -1
  148. package/dist/providers/executor/container.d.ts +1 -1
  149. package/dist/providers/executor/container.js +10 -3
  150. package/dist/providers/executor/dbos.d.ts +1 -1
  151. package/dist/providers/jobs/docker.d.ts +11 -0
  152. package/dist/providers/jobs/docker.js +34 -18
  153. package/dist/providers/jobs/kubernetes.js +5 -10
  154. package/dist/providers/jobs/types.d.ts +2 -0
  155. package/dist/providers/memory/postgres.d.ts +1 -1
  156. package/dist/providers/vcs/git.js +2 -1
  157. package/dist/quickstart/config.d.ts +30 -0
  158. package/dist/quickstart/config.js +106 -0
  159. package/dist/quickstart/index.d.ts +5 -0
  160. package/dist/quickstart/index.js +527 -0
  161. package/dist/quickstart/migrate.d.ts +23 -0
  162. package/dist/quickstart/migrate.js +125 -0
  163. package/dist/wardby-bin.js +35 -9
  164. package/package.json +22 -9
  165. package/prisma/migrate.config.mjs +20 -0
  166. package/prisma/schema.prisma +10 -2
  167. package/dist/providers/auth/release-gate.d.ts +0 -1
  168. package/dist/providers/auth/release-gate.js +0 -4
@@ -1,4 +1,4 @@
1
- import type { PrismaClient } from "@prisma/client";
1
+ import type { PrismaClient } from "#prisma";
2
2
  import type { AuthProfile, AuthProvider, AuthTokens, VerifiedToken } from "./types.js";
3
3
  import { Credentials } from "../../mcp/auth/self-hosted/credentials.js";
4
4
  import { Sessions } from "../../mcp/auth/self-hosted/session.js";
@@ -34,7 +34,7 @@ export declare class SelfHostedAuthProvider implements AuthProvider {
34
34
  consentPage(sessionToken: string, interactionId: string): Promise<{
35
35
  interaction: {
36
36
  client: {
37
- metadata: import("@prisma/client/runtime/library").JsonValue;
37
+ metadata: import("@prisma/client/runtime/client").JsonValue;
38
38
  createdAt: Date;
39
39
  clientId: string;
40
40
  };
@@ -1,4 +1,4 @@
1
- import type { PrismaClient } from "@prisma/client";
1
+ import type { PrismaClient } from "#prisma";
2
2
  import type { CreateProxySessionInput, ProxyLedger, ProxyRequest, ProxySession, ProxyUsage, ReserveProxyRequestInput, ReserveProxyRequestResult } from "./types.js";
3
3
  export type PrismaProxyLedgerDb = Pick<PrismaClient, "$transaction" | "$queryRaw" | "$executeRaw">;
4
4
  export declare class PrismaProxyLedger implements ProxyLedger {
@@ -1,4 +1,4 @@
1
- import type { PrismaClient } from "@prisma/client";
1
+ import type { PrismaClient } from "#prisma";
2
2
  import { startDenyPortListener } from "./deny-port.js";
3
3
  import { startCodingProxyServer, type CodingProxyServerHandle } from "./server.js";
4
4
  import type { ProxyAuditSink } from "./types.js";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Postgres-backed default Datastore adapter, via the `DatastoreEntry` table.
3
3
  */
4
- import type { PrismaClient } from "@prisma/client";
4
+ import type { PrismaClient } from "#prisma";
5
5
  import type { Datastore, DatastoreSetOptions, DatastoreValue } from "./types.js";
6
6
  import type { SecretCipher } from "../secrets/types.js";
7
7
  export type DatastoreDb = Pick<PrismaClient, "datastoreEntry" | "datastore" | "$queryRaw">;
@@ -1,4 +1,4 @@
1
- import type { PrismaClient } from "@prisma/client";
1
+ import type { PrismaClient } from "#prisma";
2
2
  import type { ProviderRegistry } from "../index.js";
3
3
  import { type ProviderConfig } from "../../config/providers.js";
4
4
  import type { Executor } from "./types.js";
@@ -1,4 +1,4 @@
1
- import type { PrismaClient } from "@prisma/client";
1
+ import type { PrismaClient } from "#prisma";
2
2
  import { type ProviderConfig } from "../../config/providers.js";
3
3
  import type { KubernetesApi } from "../jobs/kubernetes-api.js";
4
4
  import type { Executor } from "./types.js";
@@ -1,4 +1,4 @@
1
- import type { PrismaClient } from "@prisma/client";
1
+ import type { PrismaClient } from "#prisma";
2
2
  import { type CodingRunObserver } from "../../coding/observability.js";
3
3
  import { type CodingRunResult } from "../../coding/protocol.js";
4
4
  import type { ProxyProtocol } from "../coding-proxy/types.js";
@@ -3,6 +3,7 @@ import { mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { join, resolve, sep } from "node:path";
4
4
  import { CodingProfileSchema } from "../../coding/profile.js";
5
5
  import { logger } from "../../core/logger.js";
6
+ import { ensurePrivateDirectory } from "../../core/private-directory.js";
6
7
  import { codingRunObserver } from "../../coding/observability.js";
7
8
  import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, parseCodingAgentOutputJson, redactAndTruncate, } from "../../coding/protocol.js";
8
9
  import { getModelPricing } from "../llm/pricing.js";
@@ -597,7 +598,7 @@ export class ContainerExecutor {
597
598
  const reason = collected?.diagnostic ?? collected?.reason ?? jobState;
598
599
  this.emit({ stage: "collected", runId: run.runId, jobId: handle.id });
599
600
  const status = jobState === "lost" ? "lost" : "failed";
600
- const failure = this.failure(`job_${reason}`);
601
+ const failure = this.failure(`job_${reason}`, { issues: collected?.diagnosticIssues });
601
602
  await this.options.store.terminate(run.runId, status, failure.error, failure.audit);
602
603
  this.terminal(run, status, failure.audit);
603
604
  return;
@@ -755,7 +756,7 @@ export class ContainerExecutor {
755
756
  throw new Error("coding_provider_not_configured:claude-code");
756
757
  }
757
758
  async writeInput(run, deadlineAt) {
758
- await mkdir(this.artifactRoot, { recursive: true, mode: 0o700 });
759
+ await ensurePrivateDirectory(this.artifactRoot);
759
760
  const rootReal = await realpath(this.artifactRoot);
760
761
  const directory = resolve(rootReal, run.runId);
761
762
  if (!directory.startsWith(`${rootReal}${sep}`))
@@ -881,7 +882,13 @@ export class ContainerExecutor {
881
882
  // keyed by the same diagnostic id. Token-shaped values are redacted, and a
882
883
  // cause chain is kept because the outer message is often just a wrapper.
883
884
  // A user asking to stop is not a failure, so it gets the same id at info.
884
- const line = { diagnosticId, category, reason: describeFailure(error) };
885
+ // `issues` names only which output-schema fields failed (see SAFE_CODING_OUTPUT_ISSUE).
886
+ const line = {
887
+ diagnosticId,
888
+ category,
889
+ reason: describeFailure(error),
890
+ ...(options.issues ? { issues: options.issues } : {}),
891
+ };
885
892
  if (options.cancelled === true)
886
893
  containerLog.info(line, "coding run cancelled; the persisted error is its id only");
887
894
  else
@@ -12,7 +12,7 @@
12
12
  * stale-heartbeat detection is unchanged; the reconciler consults
13
13
  * `recover()` (Task 6) before declaring a DBOS run lost.
14
14
  */
15
- import type { PrismaClient } from "@prisma/client";
15
+ import type { PrismaClient } from "#prisma";
16
16
  import type { ProviderRegistry } from "../index.js";
17
17
  import type { DbosConfig } from "../../config/providers.js";
18
18
  import { type RunnerDb } from "../../core/runner.js";
@@ -1,5 +1,16 @@
1
1
  import type { JobHandle, JobResult, JobSpec, JobStatus, WorkspaceJobLauncher } from "./types.js";
2
2
  export declare const SAFE_WORKER_DIAGNOSTIC: RegExp;
3
+ export interface WorkerDiagnostic {
4
+ diagnostic: string;
5
+ diagnosticIssues?: string[];
6
+ }
7
+ /**
8
+ * Worker output is untrusted: recognizes only the fixed `{"error": code}` line,
9
+ * plus an optional `issues` list whose every entry must pass the shared pattern.
10
+ * A list with any entry that does not is dropped whole rather than filtered, so
11
+ * a malformed worker cannot smuggle a partial value through.
12
+ */
13
+ export declare function parseWorkerDiagnosticLine(line: string): WorkerDiagnostic | undefined;
3
14
  export interface DockerCommandOptions {
4
15
  env?: Record<string, string>;
5
16
  maxOutputBytes?: number;
@@ -3,7 +3,9 @@ import { spawn } from "node:child_process";
3
3
  import { lstat, mkdir, mkdtemp, open, opendir, readdir, readFile, readlink, realpath, rename, rm, unlink, } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, resolve, sep } from "node:path";
5
5
  import { logger } from "../../core/logger.js";
6
- import { parseCodingAgentOutputJson, redactAndTruncate, MAX_CODING_ARTIFACT_BYTES } from "../../coding/protocol.js";
6
+ import { ensurePrivateDirectory } from "../../core/private-directory.js";
7
+ import { readBoundedRegularFile } from "../../coding-worker/artifact.js";
8
+ import { parseCodingAgentOutputJson, redactAndTruncate, MAX_CODING_ARTIFACT_BYTES, MAX_CODING_OUTPUT_ISSUES, SAFE_CODING_OUTPUT_ISSUE, } from "../../coding/protocol.js";
7
9
  import { assertDockerHostSupportsIsolation, assertClaudeAgentContainerInspection, assertClaudeToolRunnerContainerInspection, assertIsolationNetworkInspection, assertKeeperContainerInspection, assertProxyContainerInspection, assertStorageVolumeInspection, assertWorkerContainerInspection, buildDockerIsolationPlan, } from "./docker-isolation.js";
8
10
  import { replaceDirectoryFromStaging } from "./workspace-swap.js";
9
11
  const BACKEND = "docker";
@@ -13,6 +15,31 @@ const MAX_DOCKER_SEED_DIAGNOSTIC_BYTES = 4 * 1024;
13
15
  const MAX_WORKSPACE_ENTRIES = 100_000;
14
16
  const TERMINAL_PHASES = new Set(["succeeded", "failed", "stopped", "lost", "removed"]);
15
17
  export const SAFE_WORKER_DIAGNOSTIC = /^(?:worker_[a-z_]+|coding_[a-z_]+|wardby_[a-z_]+)$/;
18
+ /**
19
+ * Worker output is untrusted: recognizes only the fixed `{"error": code}` line,
20
+ * plus an optional `issues` list whose every entry must pass the shared pattern.
21
+ * A list with any entry that does not is dropped whole rather than filtered, so
22
+ * a malformed worker cannot smuggle a partial value through.
23
+ */
24
+ export function parseWorkerDiagnosticLine(line) {
25
+ let value;
26
+ try {
27
+ value = JSON.parse(line);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ if (typeof value?.error !== "string" || !SAFE_WORKER_DIAGNOSTIC.test(value.error))
33
+ return undefined;
34
+ const issues = value.issues;
35
+ const safeIssues = Array.isArray(issues) &&
36
+ issues.length > 0 &&
37
+ issues.length <= MAX_CODING_OUTPUT_ISSUES &&
38
+ issues.every((issue) => typeof issue === "string" && SAFE_CODING_OUTPUT_ISSUE.test(issue))
39
+ ? issues
40
+ : undefined;
41
+ return { diagnostic: value.error, ...(safeIssues ? { diagnosticIssues: safeIssues } : {}) };
42
+ }
16
43
  const dockerLog = logger.child({ module: "docker-jobs" });
17
44
  /**
18
45
  * Docker failures never include daemon output in their public message -- that
@@ -440,7 +467,7 @@ export class DockerJobLauncher {
440
467
  if (current.phase === "failed" && !current.result.diagnostic) {
441
468
  const diagnostic = await this.readWorkerFailureDiagnostic(current);
442
469
  if (diagnostic) {
443
- current.result = { ...current.result, diagnostic };
470
+ current.result = { ...current.result, ...diagnostic };
444
471
  await this.writeRecord(current);
445
472
  }
446
473
  }
@@ -500,7 +527,7 @@ export class DockerJobLauncher {
500
527
  });
501
528
  }
502
529
  async initialize() {
503
- await mkdir(this.stateRoot, { recursive: true, mode: 0o700 });
530
+ await ensurePrivateDirectory(this.stateRoot);
504
531
  await mkdir(join(this.stateRoot, ".home"), { recursive: true, mode: 0o700 });
505
532
  await realpath(this.stateRoot);
506
533
  const records = await this.allRecords();
@@ -792,13 +819,7 @@ export class DockerJobLauncher {
792
819
  const target = join(staging, "result.json");
793
820
  try {
794
821
  await this.run(["container", "cp", `${this.keeperName(record)}:/run/wardby/storage/output/result.json`, target]);
795
- const metadata = await lstat(target);
796
- if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_CODING_ARTIFACT_BYTES) {
797
- throw new Error("docker_result_artifact_invalid");
798
- }
799
- const raw = await readFile(target, "utf8");
800
- if (Buffer.byteLength(raw, "utf8") > MAX_CODING_ARTIFACT_BYTES)
801
- throw new Error("docker_result_artifact_invalid");
822
+ const raw = await readBoundedRegularFile(target, MAX_CODING_ARTIFACT_BYTES, "docker_result_artifact_invalid");
802
823
  const output = parseCodingAgentOutputJson(raw);
803
824
  if (output.runId !== record.runId)
804
825
  throw new Error("docker_result_run_mismatch");
@@ -818,14 +839,9 @@ export class DockerJobLauncher {
818
839
  const logs = await this.run(["container", "logs", "--tail", "8", record.handle.id]);
819
840
  const raw = `${logs.stdout}\n${logs.stderr}`;
820
841
  for (const line of raw.split("\n").reverse()) {
821
- try {
822
- const value = JSON.parse(line);
823
- if (typeof value.error === "string" && SAFE_WORKER_DIAGNOSTIC.test(value.error))
824
- return value.error;
825
- }
826
- catch {
827
- // Worker output is untrusted; only parse one fixed JSON shape.
828
- }
842
+ const diagnostic = parseWorkerDiagnosticLine(line);
843
+ if (diagnostic)
844
+ return diagnostic;
829
845
  }
830
846
  }
831
847
  catch {
@@ -31,7 +31,7 @@ import { isIP } from "node:net";
31
31
  import { logger } from "../../core/logger.js";
32
32
  import { MAX_CODING_ARTIFACT_BYTES, parseCodingAgentOutputJson } from "../../coding/protocol.js";
33
33
  import { CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "./docker-isolation.js";
34
- import { SAFE_WORKER_DIAGNOSTIC } from "./docker.js";
34
+ import { parseWorkerDiagnosticLine } from "./docker.js";
35
35
  import { KubernetesAlreadyExistsError, KubernetesConflictError } from "./kubernetes-api.js";
36
36
  import { ENFORCEMENT_PROBE_DENY_REACHABLE, ENFORCEMENT_PROBE_PROVEN, ENFORCEMENT_PROBE_DENY_REFUSED, ENFORCEMENT_PROBE_PROXY_UNREACHABLE, KEEPER_CONTAINER, KEEPER_SEEDED_MARKER, KUBERNETES_ISOLATION_ERROR, STORAGE_ROOT, WORKER_CONTAINER, assertRunNetworkPolicyMatches, assertRunPodMatches, buildCapabilitySecret, buildRunNetworkPolicy, buildRunPod, enforcementProbeScript, kubernetesRunNames, kubernetesRunNamesForToken, runLabels, validateKubernetesSpec, } from "./kubernetes-isolation.js";
37
37
  import { readProxyWitness } from "./kubernetes-witness.js";
@@ -428,7 +428,7 @@ export class KubernetesJobLauncher {
428
428
  if (phase === "failed" && !result.diagnostic) {
429
429
  const diagnostic = await this.readWorkerDiagnostic(names);
430
430
  if (diagnostic)
431
- result = { ...result, diagnostic };
431
+ result = { ...result, ...diagnostic };
432
432
  }
433
433
  if (phase === "succeeded" && !result.resultArtifact) {
434
434
  result = { ...result, resultArtifact: await this.readResultArtifact(names, record.runId) };
@@ -715,14 +715,9 @@ export class KubernetesJobLauncher {
715
715
  return undefined;
716
716
  }
717
717
  for (const line of raw.split("\n").reverse()) {
718
- try {
719
- const value = JSON.parse(line);
720
- if (typeof value?.error === "string" && SAFE_WORKER_DIAGNOSTIC.test(value.error))
721
- return value.error;
722
- }
723
- catch {
724
- // Worker output is untrusted; only one fixed JSON shape is recognized.
725
- }
718
+ const diagnostic = parseWorkerDiagnosticLine(line);
719
+ if (diagnostic)
720
+ return diagnostic;
726
721
  }
727
722
  return undefined;
728
723
  }
@@ -45,6 +45,8 @@ export interface JobResult {
45
45
  resultArtifact?: string;
46
46
  /** Fixed worker-owned failure code; never raw container output. */
47
47
  diagnostic?: string;
48
+ /** Output-schema failure locations (`path:code`) that passed SAFE_CODING_OUTPUT_ISSUE; never values. */
49
+ diagnosticIssues?: string[];
48
50
  }
49
51
  /**
50
52
  * Lifecycle invariants implemented by every launcher:
@@ -6,7 +6,7 @@
6
6
  * schema.prisma). Every other operation goes through the normal Prisma
7
7
  * Client since it never touches `contentTsv`.
8
8
  */
9
- import type { PrismaClient } from "@prisma/client";
9
+ import type { PrismaClient } from "#prisma";
10
10
  import type { AgentMemorySearchHit, AgentMemoryStore } from "./types.js";
11
11
  export type AgentMemoryDb = Pick<PrismaClient, "agentMemory" | "$executeRaw" | "$queryRaw">;
12
12
  export declare class PostgresAgentMemory implements AgentMemoryStore {
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { lstat, mkdir, opendir, readFile, readlink, realpath, rm } from "node:fs/promises";
3
3
  import { isAbsolute, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { ensurePrivateDirectory } from "../../core/private-directory.js";
5
6
  import { MAX_REDACTED_SPAN, redactTokenShapedValues, normalizeGitHubRepository, normalizeGitRef, } from "../../coding/protocol.js";
6
7
  import { isSafeGitHubInstallationToken } from "./github.js";
7
8
  export const DEFAULT_MAX_CHANGED_FILES = 1_000;
@@ -220,7 +221,7 @@ export class GitVcsProvider {
220
221
  }
221
222
  async prepareWorkspace(input) {
222
223
  const normalized = this.validateInput(input);
223
- await mkdir(this.rootDir, { recursive: true, mode: 0o700 });
224
+ await ensurePrivateDirectory(this.rootDir);
224
225
  await mkdir(resolve(this.rootDir, ".home"), { recursive: true, mode: 0o700 });
225
226
  const runRoot = resolve(this.rootDir, normalized.runId);
226
227
  if (!directChild(this.rootDir, runRoot))
@@ -0,0 +1,30 @@
1
+ export type QuickstartProvider = "openai" | "anthropic";
2
+ export type McpClient = "none" | "codex" | "claude" | "both";
3
+ export interface QuickstartState {
4
+ version: 1;
5
+ projectDir: string;
6
+ composeProject: string;
7
+ postgresPort: number;
8
+ provider: QuickstartProvider;
9
+ model: string;
10
+ packageVersion: string;
11
+ createdAt: string;
12
+ }
13
+ export interface QuickstartPaths {
14
+ projectDir: string;
15
+ wardbyDir: string;
16
+ envFile: string;
17
+ stateFile: string;
18
+ }
19
+ export declare function resolveProjectDir(cwd?: string, env?: NodeJS.ProcessEnv): string;
20
+ export declare function quickstartPaths(projectDir: string): QuickstartPaths;
21
+ export declare function composeProjectName(projectDir: string): string;
22
+ export declare function parseEnvFile(contents: string): Record<string, string>;
23
+ export declare function readQuickstartEnv(paths: QuickstartPaths): Record<string, string>;
24
+ export declare function writeQuickstartEnv(paths: QuickstartPaths, values: Record<string, string>): void;
25
+ export declare function readQuickstartState(paths: QuickstartPaths): QuickstartState | undefined;
26
+ export declare function writeQuickstartState(paths: QuickstartPaths, state: QuickstartState): void;
27
+ export declare function generatedSecret(): string;
28
+ export declare function ensureWardbyIgnored(projectDir: string): void;
29
+ export declare function defaultModel(provider: QuickstartProvider): string;
30
+ export declare function providerKeyName(provider: QuickstartProvider): "OPENAI_API_KEY" | "ANTHROPIC_API_KEY";
@@ -0,0 +1,106 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { basename, join, resolve } from "node:path";
4
+ export function resolveProjectDir(cwd = process.cwd(), env = process.env) {
5
+ return resolve(env.WARDBY_PROJECT_DIR || cwd);
6
+ }
7
+ export function quickstartPaths(projectDir) {
8
+ const wardbyDir = join(projectDir, ".wardby");
9
+ return {
10
+ projectDir,
11
+ wardbyDir,
12
+ envFile: join(wardbyDir, ".env"),
13
+ stateFile: join(wardbyDir, "state.json"),
14
+ };
15
+ }
16
+ export function composeProjectName(projectDir) {
17
+ const slug = basename(projectDir)
18
+ .toLowerCase()
19
+ .replace(/[^a-z0-9]+/g, "-")
20
+ .replace(/^-+|-+$/g, "")
21
+ .slice(0, 30);
22
+ const digest = createHash("sha256").update(resolve(projectDir)).digest("hex").slice(0, 10);
23
+ return `wardby-${slug || "project"}-${digest}`;
24
+ }
25
+ export function parseEnvFile(contents) {
26
+ const values = {};
27
+ for (const rawLine of contents.split(/\r?\n/)) {
28
+ const line = rawLine.trim();
29
+ if (!line || line.startsWith("#"))
30
+ continue;
31
+ const equals = line.indexOf("=");
32
+ if (equals <= 0)
33
+ continue;
34
+ const key = line.slice(0, equals).trim();
35
+ const rawValue = line.slice(equals + 1).trim();
36
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
37
+ continue;
38
+ if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
39
+ try {
40
+ values[key] = JSON.parse(rawValue);
41
+ continue;
42
+ }
43
+ catch {
44
+ // Preserve malformed quoted values verbatim so setup never destroys them.
45
+ }
46
+ }
47
+ values[key] = rawValue.startsWith("'") && rawValue.endsWith("'") ? rawValue.slice(1, -1) : rawValue;
48
+ }
49
+ return values;
50
+ }
51
+ export function readQuickstartEnv(paths) {
52
+ if (!existsSync(paths.envFile))
53
+ return {};
54
+ return parseEnvFile(readFileSync(paths.envFile, "utf8"));
55
+ }
56
+ export function writeQuickstartEnv(paths, values) {
57
+ mkdirSync(paths.wardbyDir, { recursive: true, mode: 0o700 });
58
+ const heading = [
59
+ "# Managed by `wardby quickstart`.",
60
+ "# Keep this file private; it contains local credentials and encryption keys.",
61
+ ];
62
+ const lines = Object.entries(values)
63
+ .sort(([left], [right]) => left.localeCompare(right))
64
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`);
65
+ writeFileSync(paths.envFile, `${[...heading, ...lines].join("\n")}\n`, { mode: 0o600 });
66
+ chmodSync(paths.wardbyDir, 0o700);
67
+ chmodSync(paths.envFile, 0o600);
68
+ }
69
+ export function readQuickstartState(paths) {
70
+ if (!existsSync(paths.stateFile))
71
+ return undefined;
72
+ const value = JSON.parse(readFileSync(paths.stateFile, "utf8"));
73
+ if (value.version !== 1 ||
74
+ typeof value.projectDir !== "string" ||
75
+ typeof value.composeProject !== "string" ||
76
+ typeof value.postgresPort !== "number" ||
77
+ (value.provider !== "openai" && value.provider !== "anthropic") ||
78
+ typeof value.model !== "string" ||
79
+ typeof value.packageVersion !== "string" ||
80
+ typeof value.createdAt !== "string") {
81
+ throw new Error(`invalid Wardby quickstart state: ${paths.stateFile}`);
82
+ }
83
+ return value;
84
+ }
85
+ export function writeQuickstartState(paths, state) {
86
+ mkdirSync(paths.wardbyDir, { recursive: true, mode: 0o700 });
87
+ writeFileSync(paths.stateFile, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
88
+ chmodSync(paths.stateFile, 0o600);
89
+ }
90
+ export function generatedSecret() {
91
+ return randomBytes(32).toString("hex");
92
+ }
93
+ export function ensureWardbyIgnored(projectDir) {
94
+ const gitignore = join(projectDir, ".gitignore");
95
+ const existing = existsSync(gitignore) ? readFileSync(gitignore, "utf8") : "";
96
+ if (existing.split(/\r?\n/).some((line) => line.trim() === ".wardby/"))
97
+ return;
98
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
99
+ writeFileSync(gitignore, `${existing}${prefix}\n# Wardby local runtime and credentials\n.wardby/\n`);
100
+ }
101
+ export function defaultModel(provider) {
102
+ return provider === "openai" ? "gpt-5.6-luna" : "claude-haiku-4-5";
103
+ }
104
+ export function providerKeyName(provider) {
105
+ return provider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
106
+ }
@@ -0,0 +1,5 @@
1
+ export declare function quickstartCommand(args: string[]): Promise<void>;
2
+ export declare function doctorCommand(args: string[]): Promise<void>;
3
+ export declare function statusCommand(args: string[]): Promise<void>;
4
+ export declare function logsCommand(args: string[]): Promise<void>;
5
+ export declare function downCommand(args: string[]): Promise<void>;