@wardby/cli 0.8.0 → 0.9.0

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 (123) hide show
  1. package/deploy/local/docker-compose.quickstart-coding.yml +5 -0
  2. package/dist/claude-plugin/command.d.ts +4 -1
  3. package/dist/claude-plugin/command.js +7 -4
  4. package/dist/cli.js +24 -0
  5. package/dist/coding/transcript/recorder.js +2 -8
  6. package/dist/core/dispatch.js +3 -0
  7. package/dist/core/engine-native.d.ts +2 -0
  8. package/dist/core/engine-native.js +13 -0
  9. package/dist/core/error-code.js +1 -0
  10. package/dist/core/listen-address.d.ts +10 -0
  11. package/dist/core/listen-address.js +14 -0
  12. package/dist/core/power-idle.d.ts +8 -2
  13. package/dist/core/power-idle.js +17 -3
  14. package/dist/core/power.d.ts +5 -0
  15. package/dist/core/power.js +5 -0
  16. package/dist/core/retention-days.d.ts +5 -0
  17. package/dist/core/retention-days.js +14 -0
  18. package/dist/core/run-steps/config.d.ts +25 -0
  19. package/dist/core/run-steps/config.js +40 -0
  20. package/dist/core/run-steps/control-chars.d.ts +16 -0
  21. package/dist/core/run-steps/control-chars.js +26 -0
  22. package/dist/core/run-steps/detail.d.ts +32 -0
  23. package/dist/core/run-steps/detail.js +240 -0
  24. package/dist/core/run-steps/extract.d.ts +52 -0
  25. package/dist/core/run-steps/extract.js +467 -0
  26. package/dist/core/run-steps/ingress.d.ts +51 -0
  27. package/dist/core/run-steps/ingress.js +160 -0
  28. package/dist/core/run-steps/last-step.d.ts +15 -0
  29. package/dist/core/run-steps/last-step.js +35 -0
  30. package/dist/core/run-steps/native.d.ts +48 -0
  31. package/dist/core/run-steps/native.js +164 -0
  32. package/dist/core/run-steps/seen.d.ts +16 -0
  33. package/dist/core/run-steps/seen.js +35 -0
  34. package/dist/core/run-steps/summarize.d.ts +57 -0
  35. package/dist/core/run-steps/summarize.js +304 -0
  36. package/dist/core/run-steps/sweep.d.ts +21 -0
  37. package/dist/core/run-steps/sweep.js +41 -0
  38. package/dist/core/run-steps/types.d.ts +89 -0
  39. package/dist/core/run-steps/types.js +39 -0
  40. package/dist/core/run-steps/wire.d.ts +35 -0
  41. package/dist/core/run-steps/wire.js +47 -0
  42. package/dist/core/run-steps/writer.d.ts +49 -0
  43. package/dist/core/run-steps/writer.js +279 -0
  44. package/dist/core/runner.d.ts +4 -0
  45. package/dist/core/runner.js +15 -1
  46. package/dist/generated/prisma/browser.d.ts +8 -0
  47. package/dist/generated/prisma/client.d.ts +8 -0
  48. package/dist/generated/prisma/commonInputTypes.d.ts +30 -0
  49. package/dist/generated/prisma/enums.d.ts +6 -0
  50. package/dist/generated/prisma/enums.js +5 -0
  51. package/dist/generated/prisma/internal/class.d.ts +11 -0
  52. package/dist/generated/prisma/internal/class.js +4 -4
  53. package/dist/generated/prisma/internal/prismaNamespace.d.ts +103 -1
  54. package/dist/generated/prisma/internal/prismaNamespace.js +19 -1
  55. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +19 -0
  56. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +19 -1
  57. package/dist/generated/prisma/models/Agent.d.ts +101 -1
  58. package/dist/generated/prisma/models/CodingProxySession.d.ts +41 -1
  59. package/dist/generated/prisma/models/Run.d.ts +503 -1
  60. package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
  61. package/dist/generated/prisma/models/RunStep.js +1 -0
  62. package/dist/generated/prisma/models.d.ts +1 -0
  63. package/dist/help-index.json +102 -14
  64. package/dist/mcp/auth/access.d.ts +1 -0
  65. package/dist/mcp/context.d.ts +3 -0
  66. package/dist/mcp/index.js +10 -0
  67. package/dist/mcp/tools/agents.js +37 -7
  68. package/dist/mcp/tools/power.js +2 -2
  69. package/dist/mcp/tools/run-access.d.ts +7 -0
  70. package/dist/mcp/tools/run-access.js +14 -0
  71. package/dist/mcp/tools/run-steps.d.ts +16 -0
  72. package/dist/mcp/tools/run-steps.js +85 -0
  73. package/dist/mcp/tools/runs.js +10 -2
  74. package/dist/native-worker/gateway-server.d.ts +4 -1
  75. package/dist/native-worker/gateway-server.js +10 -2
  76. package/dist/native-worker/gateway.d.ts +3 -0
  77. package/dist/native-worker/gateway.js +8 -0
  78. package/dist/native-worker/http-server.d.ts +2 -4
  79. package/dist/native-worker/http-server.js +2 -8
  80. package/dist/providers/coding-proxy/memory-ledger.js +1 -0
  81. package/dist/providers/coding-proxy/prisma-ledger.js +9 -3
  82. package/dist/providers/coding-proxy/proxy.d.ts +26 -0
  83. package/dist/providers/coding-proxy/proxy.js +133 -42
  84. package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
  85. package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
  86. package/dist/providers/coding-proxy/runtime.d.ts +7 -0
  87. package/dist/providers/coding-proxy/runtime.js +55 -3
  88. package/dist/providers/coding-proxy/types.d.ts +5 -0
  89. package/dist/providers/engine/types.d.ts +26 -0
  90. package/dist/providers/executor/container.d.ts +14 -0
  91. package/dist/providers/executor/container.js +8 -1
  92. package/dist/providers/jobs/kubernetes.d.ts +8 -0
  93. package/dist/providers/jobs/kubernetes.js +22 -4
  94. package/dist/quickstart-images.json +1 -1
  95. package/dist/serve.js +26 -18
  96. package/dist/viewer/api-schema.d.ts +66 -0
  97. package/dist/viewer/api-schema.js +31 -0
  98. package/dist/viewer/openapi.js +2 -1
  99. package/dist/viewer/run-detail.d.ts +2 -0
  100. package/dist/viewer/run-detail.js +26 -1
  101. package/docs/claude-code-plugin.md +57 -13
  102. package/docs/coding-agent-setup.md +17 -0
  103. package/docs/coding-worker-isolation.md +16 -0
  104. package/docs/debugging-agent-runs.md +112 -0
  105. package/docs/getting-started-gke.md +28 -7
  106. package/docs/mcp-servers.md +9 -0
  107. package/docs/native-sandbox.md +6 -0
  108. package/docs/security-deployment.md +51 -0
  109. package/docs/viewer-api.md +21 -12
  110. package/docs/viewer-api.openapi.yml +78 -2
  111. package/help/admin-viewer.md +1 -0
  112. package/help/claude-code-run-pane.md +27 -4
  113. package/help/deploy-gke.md +5 -0
  114. package/help/errors/coding-mcp-gateway-unavailable.md +46 -0
  115. package/help/low-power-mode.md +19 -8
  116. package/help/mcp.md +2 -0
  117. package/help/resume-runs-locally.md +4 -0
  118. package/help/run-steps.md +140 -0
  119. package/package.json +1 -1
  120. package/prisma/migrations/20261110000000_run_steps/migration.sql +26 -0
  121. package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
  122. package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
  123. package/prisma/schema.prisma +52 -0
@@ -20,6 +20,11 @@ services:
20
20
  # by default: the proxy then answers /mcp with 503 and coding agents simply
21
21
  # get no MCP tools.
22
22
  # WARDBY_MCP_GATEWAY_URL: http://wardby-native-gateway:8790
23
+ # Optional: the server's run-steps listener (WARDBY_RUN_STEPS_LISTEN), so coding runs record
24
+ # their steps (docs/debugging-agent-runs.md#run-steps), for example
25
+ # http://host.docker.internal:8792 with the server listening on 127.0.0.1:8792 (Docker Desktop).
26
+ # Unset by default: coding runs then record no steps.
27
+ WARDBY_RUN_STEPS_URL: ${WARDBY_RUN_STEPS_URL:-}
23
28
  # Run dependency cache. Off unless WARDBY_DEPENDENCY_CACHE=true is in the
24
29
  # quickstart env file (.wardby/.env) or the shell: then the proxy keeps
25
30
  # snapshots on the named volume below. The flag alone switches it; the
@@ -1,7 +1,10 @@
1
1
  export declare const PLUGIN = "wardby-runs";
2
2
  export declare const DEFAULT_MARKETPLACE = "wardby/wardby";
3
3
  export declare const MCP_SERVER = "wardby";
4
- /** The pane's read-only tools on a server: what auto mode needs allowed. stop_run is never added: it changes a run. */
4
+ /**
5
+ * The pane's read-only tools on a server: what auto mode needs allowed. stop_run is never added: it
6
+ * changes a run. get_run_steps backs Recent steps and Steps; against an older server the rule is harmless.
7
+ */
5
8
  export declare function paneTools(server: string): string[];
6
9
  export declare const PANE_TOOLS: string[];
7
10
  export declare const CLAUDE_PLUGIN_USAGE = "usage: wardby claude-plugin install [--marketplace <owner/repo|path>] [--server-url <url>]\n [--allow-tools | --no-allow-tools] [--fullscreen] [--yes]\n wardby claude-plugin uninstall\n wardby claude-plugin status\n\nSet up the wardby-runs Claude Code plugin: a pane listing your wardby runs.\n\n --server-name <name> The MCP server the pane uses, as /mcp lists it (default wardby); its\n allow rules follow the name\n --marketplace <src> Where to install it from (default wardby/wardby; an operator's own\n marketplace repository or a local folder works too)\n --server-url <url> Register the wardby MCP server at this URL first (for example\n https://wardby.example.com/mcp) when Claude Code has none named wardby\n --allow-tools Allow the pane's read-only wardby tools for auto mode without asking\n --no-allow-tools Leave permission rules alone\n --fullscreen Also switch Claude Code's terminal to its fullscreen view, where the pane\n is a side bar that takes clicks (applies to every session)\n --yes, -y Answer yes to every question";
@@ -19,9 +19,12 @@ import { parseArgs } from "node:util";
19
19
  export const PLUGIN = "wardby-runs";
20
20
  export const DEFAULT_MARKETPLACE = "wardby/wardby";
21
21
  export const MCP_SERVER = "wardby";
22
- /** The pane's read-only tools on a server: what auto mode needs allowed. stop_run is never added: it changes a run. */
22
+ /**
23
+ * The pane's read-only tools on a server: what auto mode needs allowed. stop_run is never added: it
24
+ * changes a run. get_run_steps backs Recent steps and Steps; against an older server the rule is harmless.
25
+ */
23
26
  export function paneTools(server) {
24
- return ["list_runs", "list_agents", "get_run", "get_run_transcript"].map((tool) => `mcp__${server}__${tool}`);
27
+ return ["list_runs", "list_agents", "get_run", "get_run_transcript", "get_run_steps"].map((tool) => `mcp__${server}__${tool}`);
25
28
  }
26
29
  export const PANE_TOOLS = paneTools(MCP_SERVER);
27
30
  /** How long one `claude` call may take: `claude mcp get` can check its server, which shouldn't stall doctor. */
@@ -289,7 +292,7 @@ export function pluginStatus(deps, serverName = MCP_SERVER) {
289
292
  deps.out(`${server ? "✓" : "✗"} ${serverName} MCP server ${server ? "configured" : "not configured"}`);
290
293
  deps.out(missing.length === 0
291
294
  ? `✓ the pane's tools are allowed in ${settingsPath}`
292
- : `! not allowed in ${settingsPath} (auto mode refuses them): ${missing.join(", ")}`);
295
+ : `! not allowed in ${settingsPath} (auto mode refuses them): ${missing.join(", ")}. Re-run \`wardby claude-plugin install --allow-tools\` to add them.`);
293
296
  return plugin && server ? 0 : 1;
294
297
  }
295
298
  /**
@@ -334,7 +337,7 @@ export function claudePluginDoctorLines(deps, serverName = MCP_SERVER) {
334
337
  }
335
338
  lines.push(missing.length === 0
336
339
  ? "✓ the pane's tools are allowed for auto mode"
337
- : `! not allowed for auto mode in ${settingsPath}: ${missing.join(", ")} (needed only in auto mode)`);
340
+ : `! not allowed for auto mode in ${settingsPath}: ${missing.join(", ")} (needed only in auto mode; re-run \`claude-plugin install --allow-tools\` to add them)`);
338
341
  return lines;
339
342
  }
340
343
  export async function claudePluginCommand(args, deps = claudePluginDeps()) {
package/dist/cli.js CHANGED
@@ -53,6 +53,8 @@ import { scheduleStartFields } from "./core/schedule-start.js";
53
53
  import { assertAgentModelAvailable } from "./core/run-pricing.js";
54
54
  import { markRunFailedFromExecutorError } from "./core/dispatch.js";
55
55
  import { startScheduler } from "./core/scheduler.js";
56
+ import { RunStepSweeper } from "./core/run-steps/sweep.js";
57
+ import { buildRunStepWriter } from "./core/run-steps/writer.js";
56
58
  import { startReconciler } from "./core/reconciler.js";
57
59
  import { startNotifications } from "./core/notifications.js";
58
60
  import { NativeEngine } from "./core/engine-native.js";
@@ -118,6 +120,19 @@ function buildDatastore(cipher) {
118
120
  function buildMemory(cipher) {
119
121
  return guardAgentMemory(new PostgresAgentMemory(prisma), createDbSecretWriteGuard(prisma, cipher));
120
122
  }
123
+ /** The run-steps writers this process built; each is flushed before the database disconnects. */
124
+ const runStepWriters = [];
125
+ /** The process's run-steps writer; an invalid WARDBY_RUN_STEPS or RUN_STEP_RETENTION_DAYS stops the command with the loader's message. */
126
+ function buildRunSteps(cipher) {
127
+ try {
128
+ const writer = buildRunStepWriter(prisma, cipher);
129
+ runStepWriters.push(writer);
130
+ return writer;
131
+ }
132
+ catch (err) {
133
+ fail(err instanceof Error ? err.message : String(err));
134
+ }
135
+ }
121
136
  function buildSecrets() {
122
137
  const config = loadProviderConfig();
123
138
  try {
@@ -624,6 +639,7 @@ async function run(name) {
624
639
  const secrets = buildSecrets();
625
640
  const datastore = buildDatastore(secrets);
626
641
  const memory = buildMemory(secrets);
642
+ const runSteps = buildRunSteps(secrets);
627
643
  let run;
628
644
  // Ctrl-C (or a SIGTERM) records the run as cancelled rather than leaving a
629
645
  // `running` row behind.
@@ -637,6 +653,7 @@ async function run(name) {
637
653
  memory,
638
654
  reviewHosts: buildReviewHosts(process.env, prisma),
639
655
  issueTrackers: buildIssueTrackers(),
656
+ runSteps,
640
657
  }, prisma, (delta) => {
641
658
  process.stdout.write(delta);
642
659
  }, (created) => {
@@ -793,6 +810,7 @@ async function scheduler(args) {
793
810
  const secrets = buildSecrets();
794
811
  const datastore = buildDatastore(secrets);
795
812
  const memory = buildMemory(secrets);
813
+ const runSteps = buildRunSteps(secrets);
796
814
  const reviewHosts = buildReviewHosts(process.env, prisma);
797
815
  const issueTrackers = buildIssueTrackers();
798
816
  const chat = buildChatProviders();
@@ -810,6 +828,7 @@ async function scheduler(args) {
810
828
  reviewHosts,
811
829
  issueTrackers,
812
830
  repoAccess,
831
+ runSteps,
813
832
  };
814
833
  const nativeExecutor = buildExecutor(config, nativeProviders, prisma);
815
834
  const power = buildPower({ env: process.env, db: prisma });
@@ -845,6 +864,7 @@ async function scheduler(args) {
845
864
  });
846
865
  const notifications = await startNotifications({ db: prisma, chat });
847
866
  const selfDefects = { db: prisma, issueTrackers };
867
+ const runStepSweeper = new RunStepSweeper(prisma);
848
868
  const sched = startScheduler({
849
869
  executor,
850
870
  db: prisma,
@@ -860,6 +880,7 @@ async function scheduler(args) {
860
880
  onStartFailed: (runId, err) => markRunFailedFromExecutorError(prisma, runId, err, selfDefects),
861
881
  });
862
882
  await power.sleepIfIdle().catch((err) => cliLog.warn({ err }, "power_sleep_failed"));
883
+ await runStepSweeper.maybeSweep();
863
884
  },
864
885
  });
865
886
  console.log(`wardby scheduler started (scope "${scope}"). Press Ctrl+C to stop.`);
@@ -916,6 +937,7 @@ async function nativeGateway() {
916
937
  issueTrackers: buildIssueTrackers(),
917
938
  repoAccess: createRepoAccessGate({ db: prisma, hosts: reviewHosts }),
918
939
  executor: new DeferredExecutor(prisma),
940
+ runSteps: buildRunSteps(secrets),
919
941
  };
920
942
  const server = await startGatewayServer(listen, {
921
943
  db: prisma,
@@ -1081,6 +1103,8 @@ async function main() {
1081
1103
  }
1082
1104
  }
1083
1105
  finally {
1106
+ // A run's last steps are written before the connection goes (bounded: flush never rejects).
1107
+ await Promise.all(runStepWriters.map((writer) => writer.flush()));
1084
1108
  await prisma.$disconnect();
1085
1109
  }
1086
1110
  }
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { promisify } from "node:util";
3
3
  import { gzip } from "node:zlib";
4
4
  import { BlobStoreError } from "../../providers/storage/types.js";
5
+ import { loadRetentionDays } from "../../core/retention-days.js";
5
6
  import { withEscapedForms } from "../../core/secret-spans.js";
6
7
  import { REDACTED, redactSecrets } from "../redact.js";
7
8
  import { sanitizeClaudeTranscript } from "./claude-code.js";
@@ -26,14 +27,7 @@ export function loadTranscriptMaxBytes(env = process.env) {
26
27
  return bytes;
27
28
  }
28
29
  export function loadTranscriptRetentionDays(env = process.env) {
29
- const raw = env.RUN_TRANSCRIPT_RETENTION_DAYS?.trim();
30
- if (!raw)
31
- return 30;
32
- const days = Number(raw);
33
- if (!Number.isInteger(days) || days < 1 || days > 3650) {
34
- throw new Error("RUN_TRANSCRIPT_RETENTION_DAYS must be a whole number of days from 1 to 3650.");
35
- }
36
- return days;
30
+ return loadRetentionDays(env, "RUN_TRANSCRIPT_RETENTION_DAYS", 30);
37
31
  }
38
32
  /**
39
33
  * Stores a coding run's session transcript for local resume. The worker's file is not trusted: it is
@@ -16,6 +16,7 @@ import { logger } from "./logger.js";
16
16
  import { CodingModelProviderMismatchError, resolveCodingEntry } from "./run-pricing.js";
17
17
  import { resolveCodingRepository, } from "./coding-repository.js";
18
18
  import { CONTENDED_TX_MAX_WAIT_MS } from "./timing.js";
19
+ import { dispatchRunStepsLevel } from "./run-steps/config.js";
19
20
  import { fileSelfDefectForRun } from "./self-defects.js";
20
21
  const dispatchLog = logger.child({ module: "dispatch" });
21
22
  /** SQLSTATEs worth retrying the persist transaction for: serialization failure, deadlock. */
@@ -539,6 +540,8 @@ export async function dispatchRun(options) {
539
540
  executionManaged: true,
540
541
  // Fixed for the run's life: the executor routes on this, never on the agent's current setting.
541
542
  nativeExecutionMode: agent.kind === "native" ? agent.nativeExecutionMode : null,
543
+ // Fixed for the run's life, like nativeExecutionMode: a later runSteps change reaches only later runs.
544
+ runStepsLevel: dispatchRunStepsLevel(agent),
542
545
  parentRunId: options.parentRunId,
543
546
  parentCallId: options.parentCallId,
544
547
  grantedParentMemoryKeys: options.grantedParentMemoryKeys ?? [],
@@ -16,6 +16,8 @@ export declare class NativeEngine implements Engine {
16
16
  * without it — either way the terminal status is `budget_exhausted`.
17
17
  */
18
18
  private windDown;
19
+ /** Builds an event (only when ctx.onTurn is set) and hands it to ctx.onTurn. A failing hook is logged by name and never affects the run. */
20
+ private emitTurn;
19
21
  private finish;
20
22
  /**
21
23
  * One turn's tool calls, results in call order. The calls are split into
@@ -116,8 +116,10 @@ export class NativeEngine {
116
116
  if (turn.toolCalls.length > 0)
117
117
  assistantMessage.toolCalls = turn.toolCalls;
118
118
  messages.push(assistantMessage);
119
+ this.emitTurn(ctx, () => ({ kind: "turn", turn: turns, text: turn.text, toolCalls: turn.toolCalls }));
119
120
  if (turn.toolCalls.length > 0) {
120
121
  const results = await this.runToolCalls(ctx, step, turns, turn.toolCalls);
122
+ this.emitTurn(ctx, () => ({ kind: "results", turn: turns, toolCalls: turn.toolCalls, results }));
121
123
  for (const [index, toolCall] of turn.toolCalls.entries()) {
122
124
  messages.push({
123
125
  role: "tool",
@@ -159,6 +161,17 @@ export class NativeEngine {
159
161
  }
160
162
  return this.finish("budget_exhausted", turn.text || lastText, turns + 1, finalUsage);
161
163
  }
164
+ /** Builds an event (only when ctx.onTurn is set) and hands it to ctx.onTurn. A failing hook is logged by name and never affects the run. */
165
+ emitTurn(ctx, build) {
166
+ if (!ctx.onTurn)
167
+ return;
168
+ try {
169
+ ctx.onTurn(build());
170
+ }
171
+ catch (err) {
172
+ engineLog.warn({ event: "run_steps_hook_failed", errorName: err instanceof Error ? err.name : typeof err }, "run steps hook failed; the run goes on");
173
+ }
174
+ }
162
175
  finish(status, finalText, turns, usage, error) {
163
176
  return { status, finalText, turns, usage, ...(error ? { error } : {}) };
164
177
  }
@@ -74,6 +74,7 @@ export const CHILD_RUN_FAILURE_CODES = new Set([
74
74
  "coding_failure_cancelled",
75
75
  "coding_failure_executor",
76
76
  "coding_failure_mcp_pins_unavailable",
77
+ "coding_failure_mcp_gateway_unavailable",
77
78
  "coding_provider_quota",
78
79
  "coding_provider_rate_limited",
79
80
  "coding_provider_unavailable",
@@ -0,0 +1,10 @@
1
+ /** A listener's bind address, from a `host:port` or `:port` environment variable. */
2
+ export interface ListenAddress {
3
+ host: string;
4
+ port: number;
5
+ }
6
+ /**
7
+ * Parses a listen variable (`host:port`, or `:port` for every interface); unset or blank =
8
+ * undefined. Anything else throws, naming the variable.
9
+ */
10
+ export declare function parseListenAddress(variable: string, value: string | undefined): ListenAddress | undefined;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Parses a listen variable (`host:port`, or `:port` for every interface); unset or blank =
3
+ * undefined. Anything else throws, naming the variable.
4
+ */
5
+ export function parseListenAddress(variable, value) {
6
+ if (!value?.trim())
7
+ return undefined;
8
+ const match = /^(.*):(\d{1,5})$/.exec(value.trim());
9
+ const port = match ? Number(match[2]) : NaN;
10
+ if (!match || !Number.isInteger(port) || port < 1 || port > 65_535) {
11
+ throw new Error(`${variable} must be host:port or :port (got "${value}").`);
12
+ }
13
+ return { host: match[1] || "0.0.0.0", port };
14
+ }
@@ -2,7 +2,9 @@
2
2
  * Low-power mode's idle rules: whether the coding proxy or native gateway has
3
3
  * had work since `since`. A target is busy while any of its runs is pending
4
4
  * (queued included) or running, or finished at or after `since`; the gateway
5
- * is also busy while any gateway session is active. powerBlockers lists the
5
+ * is also busy while any gateway session is active. The gateway's runs are
6
+ * sandboxed native runs plus coding runs with MCP tools: a coding run's MCP
7
+ * calls go coding proxy -> gateway, so it needs both. powerBlockers lists the
6
8
  * run ids that keep a target busy right now (recent finishes do not count).
7
9
  */
8
10
  import type { PrismaClient } from "#prisma";
@@ -10,5 +12,9 @@ import type { PowerTarget } from "./power.js";
10
12
  export declare function proxyBusy(db: PrismaClient, since: Date): Promise<boolean>;
11
13
  export declare function gatewayBusy(db: PrismaClient, since: Date): Promise<boolean>;
12
14
  export declare function powerBusy(db: PrismaClient): (target: PowerTarget, since: Date) => Promise<boolean>;
13
- /** Run ids keeping `target` busy right now: active or queued runs (plus active gateway sessions for the gateway). */
15
+ /**
16
+ * Run ids keeping `target` busy right now: active or queued runs (plus, for the gateway, active
17
+ * gateway sessions and active or queued coding runs with MCP tools). "Active" for a coding run is
18
+ * the proxy's rule: its run is pending or running.
19
+ */
14
20
  export declare function powerBlockers(db: PrismaClient, target: PowerTarget): Promise<string[]>;
@@ -1,4 +1,6 @@
1
1
  const ACTIVE = { in: ["pending", "running"] };
2
+ /** A coding run whose proxy session holds an MCP token: it has MCP tools, served through the gateway. */
3
+ const WITH_MCP_TOOLS = { proxySession: { is: { mcpTokenHash: { not: null } } } };
2
4
  export async function proxyBusy(db, since) {
3
5
  const n = await db.codingRun.count({
4
6
  where: { run: { OR: [{ status: ACTIVE }, { finishedAt: { gte: since } }] } },
@@ -11,20 +13,32 @@ export async function gatewayBusy(db, since) {
11
13
  });
12
14
  if (runs > 0)
13
15
  return true;
16
+ const mcpCodingRuns = await db.codingRun.count({
17
+ where: { ...WITH_MCP_TOOLS, run: { OR: [{ status: ACTIVE }, { finishedAt: { gte: since } }] } },
18
+ });
19
+ if (mcpCodingRuns > 0)
20
+ return true;
14
21
  return (await db.nativeGatewaySession.count({ where: { status: "active" } })) > 0;
15
22
  }
16
23
  export function powerBusy(db) {
17
24
  return (target, since) => (target === "proxy" ? proxyBusy(db, since) : gatewayBusy(db, since));
18
25
  }
19
- /** Run ids keeping `target` busy right now: active or queued runs (plus active gateway sessions for the gateway). */
26
+ /**
27
+ * Run ids keeping `target` busy right now: active or queued runs (plus, for the gateway, active
28
+ * gateway sessions and active or queued coding runs with MCP tools). "Active" for a coding run is
29
+ * the proxy's rule: its run is pending or running.
30
+ */
20
31
  export async function powerBlockers(db, target) {
21
32
  if (target === "proxy") {
22
33
  const rows = await db.codingRun.findMany({ where: { run: { status: ACTIVE } }, select: { runId: true } });
23
34
  return [...new Set(rows.map((r) => r.runId))].sort();
24
35
  }
25
- const [runs, sessions] = await Promise.all([
36
+ const [runs, sessions, mcpCodingRuns] = await Promise.all([
26
37
  db.run.findMany({ where: { nativeExecutionMode: "sandbox", status: ACTIVE }, select: { id: true } }),
27
38
  db.nativeGatewaySession.findMany({ where: { status: "active" }, select: { runId: true } }),
39
+ db.codingRun.findMany({ where: { ...WITH_MCP_TOOLS, run: { status: ACTIVE } }, select: { runId: true } }),
28
40
  ]);
29
- return [...new Set([...runs.map((r) => r.id), ...sessions.map((s) => s.runId)])].sort();
41
+ return [
42
+ ...new Set([...runs.map((r) => r.id), ...sessions.map((s) => s.runId), ...mcpCodingRuns.map((r) => r.runId)]),
43
+ ].sort();
30
44
  }
@@ -4,6 +4,11 @@ export type PowerTarget = "proxy" | "gateway";
4
4
  export declare const LOW_POWER_WAKE_TIMEOUT = "low_power_wake_timeout";
5
5
  export declare const LOW_POWER_SCALE_FORBIDDEN = "low_power_scale_forbidden";
6
6
  export declare const LOW_POWER_DISABLED = "low_power_disabled";
7
+ /**
8
+ * A coding run with MCP tools could not wake the native gateway that serves them: the run fails
9
+ * before its worker launches (category mcp_gateway_unavailable).
10
+ */
11
+ export declare const CODING_MCP_GATEWAY_UNAVAILABLE = "coding_mcp_gateway_unavailable";
7
12
  /** ISO-8601 time the control plane last scaled the Deployment up from zero. */
8
13
  export declare const POWER_WOKE_AT = "wardby.io/power-woke-at";
9
14
  /**
@@ -4,6 +4,11 @@ const powerLog = logger.child({ module: "power" });
4
4
  export const LOW_POWER_WAKE_TIMEOUT = "low_power_wake_timeout";
5
5
  export const LOW_POWER_SCALE_FORBIDDEN = "low_power_scale_forbidden";
6
6
  export const LOW_POWER_DISABLED = "low_power_disabled";
7
+ /**
8
+ * A coding run with MCP tools could not wake the native gateway that serves them: the run fails
9
+ * before its worker launches (category mcp_gateway_unavailable).
10
+ */
11
+ export const CODING_MCP_GATEWAY_UNAVAILABLE = "coding_mcp_gateway_unavailable";
7
12
  /** ISO-8601 time the control plane last scaled the Deployment up from zero. */
8
13
  export const POWER_WOKE_AT = "wardby.io/power-woke-at";
9
14
  /**
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Reads a retention setting in whole days (1 to 3650) from the environment. An unset or blank value
3
+ * gives `defaultDays`; anything else out of range stops start-up, naming the variable.
4
+ */
5
+ export declare function loadRetentionDays(env: NodeJS.ProcessEnv, name: string, defaultDays: number): number;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Reads a retention setting in whole days (1 to 3650) from the environment. An unset or blank value
3
+ * gives `defaultDays`; anything else out of range stops start-up, naming the variable.
4
+ */
5
+ export function loadRetentionDays(env, name, defaultDays) {
6
+ const raw = env[name]?.trim();
7
+ if (!raw)
8
+ return defaultDays;
9
+ const days = Number(raw);
10
+ if (!Number.isInteger(days) || days < 1 || days > 3650) {
11
+ throw new Error(`${name} must be a whole number of days from 1 to 3650.`);
12
+ }
13
+ return days;
14
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Run steps settings, read at start-up by every process that records or serves steps (the server,
3
+ * `wardby scheduler`, the native gateway). An invalid value stops start-up, naming the variable.
4
+ */
5
+ export type RunStepsLevel = "off" | "summary" | "detail";
6
+ export declare const RUN_STEPS_LEVELS: readonly RunStepsLevel[];
7
+ /** WARDBY_RUN_STEPS: the instance's ceiling. Default summary. */
8
+ export declare function loadRunStepsLevel(env?: NodeJS.ProcessEnv): RunStepsLevel;
9
+ /** A run's level: min(the instance's ceiling, the agent's setting); an unset agent is at summary. */
10
+ export declare function runStepsLevelFor(ceiling: RunStepsLevel, agentSetting: RunStepsLevel | null | undefined): RunStepsLevel;
11
+ /**
12
+ * The level a run records: the one fixed at dispatch, never above this process's current ceiling
13
+ * (lowering the ceiling stops detail for in-flight runs too). A run from before the field is at
14
+ * most summary.
15
+ */
16
+ export declare function recordedRunStepsLevel(runLevel: RunStepsLevel | null | undefined, ceiling: RunStepsLevel): RunStepsLevel;
17
+ /** The level to fix on a run being created for `agent` (dispatch, createRun, delegation). */
18
+ export declare function dispatchRunStepsLevel(agent: {
19
+ runSteps: RunStepsLevel | null;
20
+ }, env?: NodeJS.ProcessEnv): RunStepsLevel;
21
+ /** update_agent / get_agent: the agent asks for more than the instance records (not a refusal). */
22
+ export declare function runStepsAboveCeilingWarning(agentSetting: RunStepsLevel | null | undefined, ceiling: RunStepsLevel): string | undefined;
23
+ export declare const DEFAULT_RUN_STEP_RETENTION_DAYS = 30;
24
+ /** RUN_STEP_RETENTION_DAYS: days a step is kept after it is written. Applies to steps written after a change. */
25
+ export declare function loadRunStepRetentionDays(env?: NodeJS.ProcessEnv): number;
@@ -0,0 +1,40 @@
1
+ import { loadRetentionDays } from "../retention-days.js";
2
+ export const RUN_STEPS_LEVELS = ["off", "summary", "detail"];
3
+ const RANK = { off: 0, summary: 1, detail: 2 };
4
+ /** WARDBY_RUN_STEPS: the instance's ceiling. Default summary. */
5
+ export function loadRunStepsLevel(env = process.env) {
6
+ const raw = env.WARDBY_RUN_STEPS?.trim();
7
+ if (!raw)
8
+ return "summary";
9
+ if (!RUN_STEPS_LEVELS.includes(raw))
10
+ throw new Error("WARDBY_RUN_STEPS must be off, summary or detail.");
11
+ return raw;
12
+ }
13
+ /** A run's level: min(the instance's ceiling, the agent's setting); an unset agent is at summary. */
14
+ export function runStepsLevelFor(ceiling, agentSetting) {
15
+ const asked = agentSetting ?? "summary";
16
+ return RANK[asked] <= RANK[ceiling] ? asked : ceiling;
17
+ }
18
+ /**
19
+ * The level a run records: the one fixed at dispatch, never above this process's current ceiling
20
+ * (lowering the ceiling stops detail for in-flight runs too). A run from before the field is at
21
+ * most summary.
22
+ */
23
+ export function recordedRunStepsLevel(runLevel, ceiling) {
24
+ return runStepsLevelFor(ceiling, runLevel);
25
+ }
26
+ /** The level to fix on a run being created for `agent` (dispatch, createRun, delegation). */
27
+ export function dispatchRunStepsLevel(agent, env = process.env) {
28
+ return runStepsLevelFor(loadRunStepsLevel(env), agent.runSteps);
29
+ }
30
+ /** update_agent / get_agent: the agent asks for more than the instance records (not a refusal). */
31
+ export function runStepsAboveCeilingWarning(agentSetting, ceiling) {
32
+ if (!agentSetting || RANK[agentSetting] <= RANK[ceiling])
33
+ return undefined;
34
+ return `runSteps is ${agentSetting} but this instance allows up to ${ceiling} (WARDBY_RUN_STEPS), so its runs record ${ceiling}. See the run-steps help article.`;
35
+ }
36
+ export const DEFAULT_RUN_STEP_RETENTION_DAYS = 30;
37
+ /** RUN_STEP_RETENTION_DAYS: days a step is kept after it is written. Applies to steps written after a change. */
38
+ export function loadRunStepRetentionDays(env = process.env) {
39
+ return loadRetentionDays(env, "RUN_STEP_RETENTION_DAYS", DEFAULT_RUN_STEP_RETENTION_DAYS);
40
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Step text as plain text: tool output and input are untrusted (repository content, command
3
+ * output), and a reader may show them in a terminal. Whole ANSI escape sequences are removed (CSI
4
+ * such as colour codes; OSC, DCS, SOS, PM and APC strings ended by BEL or ST, such as OSC 8 links
5
+ * and OSC 52 clipboard writes), then every other C0 and C1 control character except `\n` and `\t`.
6
+ * Only a well-formed sequence is removed whole: a bare or malformed ESC (or C1 byte) is removed
7
+ * alone and never takes a printable character after it (`ESC Z` leaves `Z`), so it cannot eat
8
+ * the first character of a secret value. A well-formed CSI can still end in a letter (`ESC [ Z`),
9
+ * which is why the writer masks secret values on the raw text before this runs.
10
+ * `\r\n` becomes `\n`; a lone `\r` is dropped. Removing characters can only join text, never split
11
+ * it, so a secret split by colour codes is whole again for redaction, which runs after this.
12
+ *
13
+ * No imports: dependency-free, so any reader of step text can share it.
14
+ */
15
+ /** `text` without terminal control sequences or control characters other than `\n` and `\t`. */
16
+ export declare function stripControlSequences(text: string): string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Step text as plain text: tool output and input are untrusted (repository content, command
3
+ * output), and a reader may show them in a terminal. Whole ANSI escape sequences are removed (CSI
4
+ * such as colour codes; OSC, DCS, SOS, PM and APC strings ended by BEL or ST, such as OSC 8 links
5
+ * and OSC 52 clipboard writes), then every other C0 and C1 control character except `\n` and `\t`.
6
+ * Only a well-formed sequence is removed whole: a bare or malformed ESC (or C1 byte) is removed
7
+ * alone and never takes a printable character after it (`ESC Z` leaves `Z`), so it cannot eat
8
+ * the first character of a secret value. A well-formed CSI can still end in a letter (`ESC [ Z`),
9
+ * which is why the writer masks secret values on the raw text before this runs.
10
+ * `\r\n` becomes `\n`; a lone `\r` is dropped. Removing characters can only join text, never split
11
+ * it, so a secret split by colour codes is whole again for redaction, which runs after this.
12
+ *
13
+ * No imports: dependency-free, so any reader of step text can share it.
14
+ */
15
+ /* eslint-disable no-control-regex -- matching control characters is this file's purpose */
16
+ /** OSC, DCS, SOS, PM, APC (7-bit or C1 introducer), up to BEL or ST (ESC \ or C1 ST). */
17
+ const STRING_SEQUENCE = /(?:\x1b[\]PX^_]|[\x90\x98\x9d\x9e\x9f])[^\x07\x1b\x9c]*(?:\x07|\x1b\\|\x9c)/g;
18
+ /** CSI (7-bit or C1): parameter bytes, intermediate bytes, one final byte. */
19
+ const CSI_SEQUENCE = /(?:\x1b\[|\x9b)[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
20
+ /** C0 except \t and \n (\r included), DEL, and C1. */
21
+ const CONTROL = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
22
+ /* eslint-enable no-control-regex */
23
+ /** `text` without terminal control sequences or control characters other than `\n` and `\t`. */
24
+ export function stripControlSequences(text) {
25
+ return text.replace(STRING_SEQUENCE, "").replace(CSI_SEQUENCE, "").replace(/\r\n/g, "\n").replace(CONTROL, "");
26
+ }
@@ -0,0 +1,32 @@
1
+ import { type ExtractedResult, type ExtractedStep } from "./types.js";
2
+ /**
3
+ * `text` collapsed as summaries are (collapseWhitespace: runs of whitespace become one space,
4
+ * trimmed), with each collapsed character's [from, to) span in `text`.
5
+ */
6
+ export declare function collapseWithMap(text: string): {
7
+ collapsed: string;
8
+ from: number[];
9
+ to: number[];
10
+ };
11
+ /** The most a cut head's partial first line drop takes (afterCutHead). */
12
+ export declare const CUT_HEAD_DROP_CHARS = 256;
13
+ /** A tool step's stored detail with its output appended, within MAX_STEP_DETAIL_CHARS. */
14
+ export declare function joinDetail(existing: string | null, suffix: string): string;
15
+ export interface StepDetailer {
16
+ /**
17
+ * `raw` (untrusted text before control sequences are stripped) with every form of every secret
18
+ * value, the secret start it ends in, the secret end it starts with when its head was cut, and
19
+ * both at every line and ellipsis boundary, each replaced by `marker`: so a stripped escape
20
+ * sequence cannot take a character of a secret, whole or partial, and hide the rest of it.
21
+ */
22
+ premask(raw: string, headWasCut: boolean, marker: string): string;
23
+ /** A step's detail: a tool's input (or its summary text when no input was sent), or a said step's text. */
24
+ step(step: ExtractedStep): string;
25
+ /** The text appended to a tool step's detail when its result arrives: the heading and the output's redacted end. */
26
+ output(result: Required<Pick<ExtractedResult, "output">> & Pick<ExtractedResult, "outputCut">): string;
27
+ }
28
+ /**
29
+ * Detail texts for one batch, against every secret value delivered to the run (the agent's and the
30
+ * run's own). The forms, and each form's KMP table for the edge checks, are built once per batch.
31
+ */
32
+ export declare function stepDetailer(secretValues: readonly string[]): StepDetailer;