@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.
- package/deploy/local/docker-compose.quickstart-coding.yml +5 -0
- package/dist/claude-plugin/command.d.ts +4 -1
- package/dist/claude-plugin/command.js +7 -4
- package/dist/cli.js +24 -0
- package/dist/coding/transcript/recorder.js +2 -8
- package/dist/core/dispatch.js +3 -0
- package/dist/core/engine-native.d.ts +2 -0
- package/dist/core/engine-native.js +13 -0
- package/dist/core/error-code.js +1 -0
- package/dist/core/listen-address.d.ts +10 -0
- package/dist/core/listen-address.js +14 -0
- package/dist/core/power-idle.d.ts +8 -2
- package/dist/core/power-idle.js +17 -3
- package/dist/core/power.d.ts +5 -0
- package/dist/core/power.js +5 -0
- package/dist/core/retention-days.d.ts +5 -0
- package/dist/core/retention-days.js +14 -0
- package/dist/core/run-steps/config.d.ts +25 -0
- package/dist/core/run-steps/config.js +40 -0
- package/dist/core/run-steps/control-chars.d.ts +16 -0
- package/dist/core/run-steps/control-chars.js +26 -0
- package/dist/core/run-steps/detail.d.ts +32 -0
- package/dist/core/run-steps/detail.js +240 -0
- package/dist/core/run-steps/extract.d.ts +52 -0
- package/dist/core/run-steps/extract.js +467 -0
- package/dist/core/run-steps/ingress.d.ts +51 -0
- package/dist/core/run-steps/ingress.js +160 -0
- package/dist/core/run-steps/last-step.d.ts +15 -0
- package/dist/core/run-steps/last-step.js +35 -0
- package/dist/core/run-steps/native.d.ts +48 -0
- package/dist/core/run-steps/native.js +164 -0
- package/dist/core/run-steps/seen.d.ts +16 -0
- package/dist/core/run-steps/seen.js +35 -0
- package/dist/core/run-steps/summarize.d.ts +57 -0
- package/dist/core/run-steps/summarize.js +304 -0
- package/dist/core/run-steps/sweep.d.ts +21 -0
- package/dist/core/run-steps/sweep.js +41 -0
- package/dist/core/run-steps/types.d.ts +89 -0
- package/dist/core/run-steps/types.js +39 -0
- package/dist/core/run-steps/wire.d.ts +35 -0
- package/dist/core/run-steps/wire.js +47 -0
- package/dist/core/run-steps/writer.d.ts +49 -0
- package/dist/core/run-steps/writer.js +279 -0
- package/dist/core/runner.d.ts +4 -0
- package/dist/core/runner.js +15 -1
- package/dist/generated/prisma/browser.d.ts +8 -0
- package/dist/generated/prisma/client.d.ts +8 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +30 -0
- package/dist/generated/prisma/enums.d.ts +6 -0
- package/dist/generated/prisma/enums.js +5 -0
- package/dist/generated/prisma/internal/class.d.ts +11 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +103 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +19 -1
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +19 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +19 -1
- package/dist/generated/prisma/models/Agent.d.ts +101 -1
- package/dist/generated/prisma/models/CodingProxySession.d.ts +41 -1
- package/dist/generated/prisma/models/Run.d.ts +503 -1
- package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
- package/dist/generated/prisma/models/RunStep.js +1 -0
- package/dist/generated/prisma/models.d.ts +1 -0
- package/dist/help-index.json +102 -14
- package/dist/mcp/auth/access.d.ts +1 -0
- package/dist/mcp/context.d.ts +3 -0
- package/dist/mcp/index.js +10 -0
- package/dist/mcp/tools/agents.js +37 -7
- package/dist/mcp/tools/power.js +2 -2
- package/dist/mcp/tools/run-access.d.ts +7 -0
- package/dist/mcp/tools/run-access.js +14 -0
- package/dist/mcp/tools/run-steps.d.ts +16 -0
- package/dist/mcp/tools/run-steps.js +85 -0
- package/dist/mcp/tools/runs.js +10 -2
- package/dist/native-worker/gateway-server.d.ts +4 -1
- package/dist/native-worker/gateway-server.js +10 -2
- package/dist/native-worker/gateway.d.ts +3 -0
- package/dist/native-worker/gateway.js +8 -0
- package/dist/native-worker/http-server.d.ts +2 -4
- package/dist/native-worker/http-server.js +2 -8
- package/dist/providers/coding-proxy/memory-ledger.js +1 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +9 -3
- package/dist/providers/coding-proxy/proxy.d.ts +26 -0
- package/dist/providers/coding-proxy/proxy.js +133 -42
- package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
- package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
- package/dist/providers/coding-proxy/runtime.d.ts +7 -0
- package/dist/providers/coding-proxy/runtime.js +55 -3
- package/dist/providers/coding-proxy/types.d.ts +5 -0
- package/dist/providers/engine/types.d.ts +26 -0
- package/dist/providers/executor/container.d.ts +14 -0
- package/dist/providers/executor/container.js +8 -1
- package/dist/providers/jobs/kubernetes.d.ts +8 -0
- package/dist/providers/jobs/kubernetes.js +22 -4
- package/dist/quickstart-images.json +1 -1
- package/dist/serve.js +26 -18
- package/dist/viewer/api-schema.d.ts +66 -0
- package/dist/viewer/api-schema.js +31 -0
- package/dist/viewer/openapi.js +2 -1
- package/dist/viewer/run-detail.d.ts +2 -0
- package/dist/viewer/run-detail.js +26 -1
- package/docs/claude-code-plugin.md +57 -13
- package/docs/coding-agent-setup.md +17 -0
- package/docs/coding-worker-isolation.md +16 -0
- package/docs/debugging-agent-runs.md +112 -0
- package/docs/getting-started-gke.md +28 -7
- package/docs/mcp-servers.md +9 -0
- package/docs/native-sandbox.md +6 -0
- package/docs/security-deployment.md +51 -0
- package/docs/viewer-api.md +21 -12
- package/docs/viewer-api.openapi.yml +78 -2
- package/help/admin-viewer.md +1 -0
- package/help/claude-code-run-pane.md +27 -4
- package/help/deploy-gke.md +5 -0
- package/help/errors/coding-mcp-gateway-unavailable.md +46 -0
- package/help/low-power-mode.md +19 -8
- package/help/mcp.md +2 -0
- package/help/resume-runs-locally.md +4 -0
- package/help/run-steps.md +140 -0
- package/package.json +1 -1
- package/prisma/migrations/20261110000000_run_steps/migration.sql +26 -0
- package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
- package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
- 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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
package/dist/core/dispatch.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/error-code.js
CHANGED
|
@@ -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.
|
|
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
|
-
/**
|
|
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[]>;
|
package/dist/core/power-idle.js
CHANGED
|
@@ -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
|
-
/**
|
|
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 [
|
|
41
|
+
return [
|
|
42
|
+
...new Set([...runs.map((r) => r.id), ...sessions.map((s) => s.runId), ...mcpCodingRuns.map((r) => r.runId)]),
|
|
43
|
+
].sort();
|
|
30
44
|
}
|
package/dist/core/power.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/core/power.js
CHANGED
|
@@ -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;
|