@webpresso/agent-kit 3.3.1 → 3.3.3
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/catalog/AGENTS.md.tpl +3 -5
- package/catalog/agent/rules/pre-implementation.md +15 -8
- package/catalog/agent/skills/ultragoal/SKILL.md +5 -18
- package/catalog/agent/skills/verify/SKILL.md +12 -12
- package/catalog/compose/admin-blocks-registry.json +659 -0
- package/dist/esm/blueprint/core/parser.js +2 -4
- package/dist/esm/blueprint/core/validation/criteria.js +2 -1
- package/dist/esm/blueprint/core/validation/task-blocks.js +7 -1
- package/dist/esm/blueprint/lifecycle/review-provenance.js +85 -30
- package/dist/esm/blueprint/markdown/task-heading.d.ts +21 -0
- package/dist/esm/blueprint/markdown/task-heading.js +25 -0
- package/dist/esm/blueprint/trust/command-runner.d.ts +40 -0
- package/dist/esm/blueprint/trust/command-runner.js +265 -41
- package/dist/esm/build/cli-mcp-parity.js +6 -0
- package/dist/esm/build/package-manifest.js +7 -2
- package/dist/esm/ci/native-session-memory-cache.d.ts +2 -1
- package/dist/esm/ci/native-session-memory-cache.js +2 -1
- package/dist/esm/ci/release-progress.d.ts +37 -0
- package/dist/esm/ci/release-progress.js +139 -0
- package/dist/esm/cli/cli.d.ts +1 -1
- package/dist/esm/cli/cli.js +9 -0
- package/dist/esm/cli/commands/audit-core.js +9 -1
- package/dist/esm/cli/commands/audit.js +16 -5
- package/dist/esm/cli/commands/blueprint/mutations.js +2 -3
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +18 -3
- package/dist/esm/cli/commands/init/mcp-spec.js +54 -6
- package/dist/esm/cli/commands/init/scaffold-agents-md.js +5 -7
- package/dist/esm/cli/commands/package-manager.js +8 -2
- package/dist/esm/cli/commands/release-progress.d.ts +53 -0
- package/dist/esm/cli/commands/release-progress.js +155 -0
- package/dist/esm/cli/commands/review.js +74 -11
- package/dist/esm/cli/utils.d.ts +8 -1
- package/dist/esm/cli/utils.js +12 -2
- package/dist/esm/compose/compose.d.ts +38 -0
- package/dist/esm/compose/compose.js +143 -0
- package/dist/esm/compose/escape-html.d.ts +5 -0
- package/dist/esm/compose/escape-html.js +12 -0
- package/dist/esm/compose/ops-report-render.d.ts +5 -0
- package/dist/esm/compose/ops-report-render.js +71 -0
- package/dist/esm/compose/ops-report-schema.d.ts +42 -0
- package/dist/esm/compose/ops-report-schema.js +46 -0
- package/dist/esm/compose/preview.d.ts +14 -0
- package/dist/esm/compose/preview.js +32 -0
- package/dist/esm/compose/registry-resolve.d.ts +47 -0
- package/dist/esm/compose/registry-resolve.js +133 -0
- package/dist/esm/git/changed-files.d.ts +20 -0
- package/dist/esm/git/changed-files.js +63 -0
- package/dist/esm/hooks/pretool-guard/validators/plan-frontmatter.js +3 -2
- package/dist/esm/hooks/stop/qa-changed-files.js +6 -1
- package/dist/esm/mcp/blueprint/handlers/document-mutations.js +2 -1
- package/dist/esm/mcp/blueprint/handlers/task-advance.js +2 -12
- package/dist/esm/mcp/blueprint/handlers/validate.js +6 -0
- package/dist/esm/mcp/tools/_names.d.ts +1 -1
- package/dist/esm/mcp/tools/_names.js +4 -0
- package/dist/esm/mcp/tools/_registry.js +8 -0
- package/dist/esm/mcp/tools/release-progress.d.ts +9 -0
- package/dist/esm/mcp/tools/release-progress.js +110 -0
- package/dist/esm/mcp/tools/wp-ui-blocks-list.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-blocks-list.js +43 -0
- package/dist/esm/mcp/tools/wp-ui-compose.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-compose.js +62 -0
- package/dist/esm/mcp/tools/wp-ui-preview.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-preview.js +52 -0
- package/dist/esm/package.json +2 -0
- package/dist/esm/review/delivery-verifier.d.ts +33 -2
- package/dist/esm/review/delivery-verifier.js +130 -24
- package/dist/esm/review/execution/review-checkout.d.ts +1 -0
- package/dist/esm/review/execution/review-checkout.js +386 -23
- package/dist/esm/review/execution/sandbox/adapter.d.ts +37 -0
- package/dist/esm/review/execution/sandbox/adapter.js +39 -0
- package/dist/esm/review/execution/sandbox/env.d.ts +12 -0
- package/dist/esm/review/execution/sandbox/env.js +37 -0
- package/dist/esm/review/execution/sandbox/index.d.ts +5 -0
- package/dist/esm/review/execution/sandbox/index.js +4 -0
- package/dist/esm/review/execution/sandbox/policy.d.ts +19 -0
- package/dist/esm/review/execution/sandbox/policy.js +95 -0
- package/dist/esm/review/execution/sandbox/probe.d.ts +20 -0
- package/dist/esm/review/execution/sandbox/probe.js +97 -0
- package/dist/esm/review/execution/sandbox/shell-quote.d.ts +14 -0
- package/dist/esm/review/execution/sandbox/shell-quote.js +18 -0
- package/dist/esm/review/execution/sandbox/tmpdir.d.ts +9 -0
- package/dist/esm/review/execution/sandbox/tmpdir.js +36 -0
- package/dist/esm/review/execution/sandbox/types.d.ts +31 -0
- package/dist/esm/review/execution/sandbox/types.js +6 -0
- package/dist/esm/review/execution/supervisor.js +44 -5
- package/dist/esm/review/execution/types.d.ts +2 -0
- package/package.json +24 -12
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wp_release_progress` — read-only Release workflow phase reporter for agents.
|
|
3
|
+
*
|
|
4
|
+
* Shares {@link runReleaseProgress} with the CLI so MCP and `wp release-progress`
|
|
5
|
+
* never diverge. Does not mutate GitHub; only reads via `gh`.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { runReleaseProgress, } from "#cli/commands/release-progress.js";
|
|
9
|
+
import { createSummaryOutputSchema, createSummaryResult } from "./_shared/result.js";
|
|
10
|
+
import { readonlyOpsBaseSchema } from "./_readonly-ops.js";
|
|
11
|
+
const inputSchema = readonlyOpsBaseSchema
|
|
12
|
+
.extend({
|
|
13
|
+
runId: z.union([z.string().min(1), z.number().int().positive()]).optional(),
|
|
14
|
+
repo: z.string().min(1).optional(),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
const releaseProgressDetailsSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
runId: z.string(),
|
|
20
|
+
repo: z.string(),
|
|
21
|
+
phase: z.enum(["detect", "native-matrix", "publish", "done", "failed"]),
|
|
22
|
+
detail: z.string(),
|
|
23
|
+
native: z.object({
|
|
24
|
+
done: z.number().int().nonnegative(),
|
|
25
|
+
total: z.number().int().nonnegative(),
|
|
26
|
+
failed: z.number().int().nonnegative(),
|
|
27
|
+
inProgress: z.number().int().nonnegative(),
|
|
28
|
+
}),
|
|
29
|
+
publishRunning: z.boolean(),
|
|
30
|
+
publishDone: z.boolean(),
|
|
31
|
+
publishFailed: z.boolean(),
|
|
32
|
+
line: z.string(),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
35
|
+
const outputSchema = createSummaryOutputSchema({
|
|
36
|
+
details: releaseProgressDetailsSchema,
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Build the tool descriptor. Tests inject gh seams; production uses defaults.
|
|
40
|
+
*/
|
|
41
|
+
export function createReleaseProgressTool(deps = {}) {
|
|
42
|
+
return {
|
|
43
|
+
name: "wp_release_progress",
|
|
44
|
+
description: "Read-only GitHub Actions Release workflow phase reporter (detect | native-matrix | publish | done | failed) with session-memory native matrix counts. Use instead of silent npm view polls after Version Packages merge. Does not mutate GitHub. Prefer this over shelling `wp release-progress` when available.",
|
|
45
|
+
inputSchema,
|
|
46
|
+
outputSchema,
|
|
47
|
+
annotations: {
|
|
48
|
+
title: "Release progress",
|
|
49
|
+
readOnlyHint: true,
|
|
50
|
+
destructiveHint: false,
|
|
51
|
+
idempotentHint: true,
|
|
52
|
+
openWorldHint: true,
|
|
53
|
+
},
|
|
54
|
+
handler: async (raw) => {
|
|
55
|
+
const input = inputSchema.parse(raw ?? {});
|
|
56
|
+
const repo = input.repo ?? "webpresso/agent-kit";
|
|
57
|
+
try {
|
|
58
|
+
const result = runReleaseProgress({
|
|
59
|
+
runId: input.runId,
|
|
60
|
+
repo,
|
|
61
|
+
json: false,
|
|
62
|
+
fetchJobs: deps.fetchJobs,
|
|
63
|
+
resolveLatestRunId: deps.resolveLatestRunId,
|
|
64
|
+
// Suppress CLI text I/O; structuredContent is the MCP contract.
|
|
65
|
+
stdout: { write: () => undefined },
|
|
66
|
+
stderr: { write: () => undefined },
|
|
67
|
+
});
|
|
68
|
+
const { progress, runId, line } = result;
|
|
69
|
+
const failed = progress.phase === "failed";
|
|
70
|
+
return createSummaryResult({
|
|
71
|
+
passed: !failed,
|
|
72
|
+
summary: line,
|
|
73
|
+
details: {
|
|
74
|
+
runId,
|
|
75
|
+
repo,
|
|
76
|
+
phase: progress.phase,
|
|
77
|
+
detail: progress.detail,
|
|
78
|
+
native: progress.native,
|
|
79
|
+
publishRunning: progress.publishRunning,
|
|
80
|
+
publishDone: progress.publishDone,
|
|
81
|
+
publishFailed: progress.publishFailed,
|
|
82
|
+
line,
|
|
83
|
+
},
|
|
84
|
+
warnings: failed ? [progress.detail] : [],
|
|
85
|
+
}, { isError: failed });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
+
return createSummaryResult({
|
|
90
|
+
passed: false,
|
|
91
|
+
summary: `release-progress failed: ${message}`,
|
|
92
|
+
details: {
|
|
93
|
+
runId: input.runId === undefined ? "" : String(input.runId),
|
|
94
|
+
repo,
|
|
95
|
+
phase: "failed",
|
|
96
|
+
detail: message,
|
|
97
|
+
native: { done: 0, total: 0, failed: 0, inProgress: 0 },
|
|
98
|
+
publishRunning: false,
|
|
99
|
+
publishDone: false,
|
|
100
|
+
publishFailed: false,
|
|
101
|
+
line: `release-progress phase=failed native=0/0 ${message}`,
|
|
102
|
+
},
|
|
103
|
+
warnings: [message],
|
|
104
|
+
}, { isError: true });
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const tool = createReleaseProgressTool();
|
|
110
|
+
export default tool;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveBlockRegistry } from "#compose/registry-resolve.js";
|
|
3
|
+
const inputSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
cwd: z.string().optional(),
|
|
6
|
+
profile: z.enum(["admin-crud", "ops-report"]).optional(),
|
|
7
|
+
})
|
|
8
|
+
.strict();
|
|
9
|
+
const tool = {
|
|
10
|
+
name: "wp_ui_blocks_list",
|
|
11
|
+
description: "List the agent UI block catalog for composition. Resolves project `.webpresso/generated/frontend/blocks/registry.json` when present, otherwise the offline catalog snapshot. Always returns source (project|snapshot), registryVersion, and warnings (e.g. snapshot used inside a webpresso tree without a generated registry).",
|
|
12
|
+
inputSchema,
|
|
13
|
+
annotations: {
|
|
14
|
+
title: "UI blocks list",
|
|
15
|
+
readOnlyHint: true,
|
|
16
|
+
destructiveHint: false,
|
|
17
|
+
idempotentHint: true,
|
|
18
|
+
openWorldHint: false,
|
|
19
|
+
},
|
|
20
|
+
handler: async (raw) => {
|
|
21
|
+
const input = inputSchema.parse(raw ?? {});
|
|
22
|
+
const resolved = resolveBlockRegistry({ cwd: input.cwd });
|
|
23
|
+
// admin-crud: full registry (list/detail/form). ops-report: no admin blocks.
|
|
24
|
+
const blocks = input.profile === "ops-report" ? [] : resolved.registry.blocks;
|
|
25
|
+
const summary = `UI blocks: ${blocks.length} (source=${resolved.source}, version=${resolved.registryVersion})`;
|
|
26
|
+
const payload = {
|
|
27
|
+
passed: true,
|
|
28
|
+
summary,
|
|
29
|
+
source: resolved.source,
|
|
30
|
+
registryVersion: resolved.registryVersion,
|
|
31
|
+
path: resolved.path,
|
|
32
|
+
warnings: [...resolved.warnings],
|
|
33
|
+
profiles: ["admin-crud", "ops-report"],
|
|
34
|
+
blocks,
|
|
35
|
+
webpressoPresentWithoutRegistry: resolved.webpressoPresentWithoutRegistry,
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: summary }],
|
|
39
|
+
structuredContent: payload,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
export default tool;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { composeValidate } from "#compose/compose.js";
|
|
3
|
+
const inputSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
cwd: z.string().optional(),
|
|
6
|
+
profile: z.enum(["admin-crud", "ops-report"]),
|
|
7
|
+
kind: z.enum(["list", "detail", "form"]).optional(),
|
|
8
|
+
spec: z.unknown(),
|
|
9
|
+
})
|
|
10
|
+
.strict()
|
|
11
|
+
.superRefine((value, ctx) => {
|
|
12
|
+
if (value.profile === "admin-crud" && value.kind === undefined) {
|
|
13
|
+
ctx.addIssue({
|
|
14
|
+
code: "custom",
|
|
15
|
+
message: "kind is required for admin-crud (list|detail|form)",
|
|
16
|
+
path: ["kind"],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const tool = {
|
|
21
|
+
name: "wp_ui_compose",
|
|
22
|
+
description: "Validate a profile-discriminated UI compose input. admin-crud: validate list/detail/form specs against the block registry (returns input spec unchanged when legal). ops-report: validate flat OpsReportSpec. Does not write files or open a browser.",
|
|
23
|
+
inputSchema,
|
|
24
|
+
annotations: {
|
|
25
|
+
title: "UI compose validate",
|
|
26
|
+
readOnlyHint: true,
|
|
27
|
+
destructiveHint: false,
|
|
28
|
+
idempotentHint: true,
|
|
29
|
+
openWorldHint: false,
|
|
30
|
+
},
|
|
31
|
+
handler: async (raw) => {
|
|
32
|
+
const input = inputSchema.parse(raw ?? {});
|
|
33
|
+
const composeInput = input.profile === "admin-crud"
|
|
34
|
+
? {
|
|
35
|
+
profile: "admin-crud",
|
|
36
|
+
kind: input.kind,
|
|
37
|
+
spec: input.spec && typeof input.spec === "object" && !Array.isArray(input.spec)
|
|
38
|
+
? input.spec
|
|
39
|
+
: {},
|
|
40
|
+
}
|
|
41
|
+
: { profile: "ops-report", spec: input.spec };
|
|
42
|
+
const result = composeValidate(composeInput, { cwd: input.cwd });
|
|
43
|
+
const summary = result.ok
|
|
44
|
+
? `compose ok (${result.profile})`
|
|
45
|
+
: `compose failed (${result.profile}): ${result.diagnostics.map((d) => d.message).join("; ")}`;
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: summary }],
|
|
48
|
+
structuredContent: {
|
|
49
|
+
passed: result.ok,
|
|
50
|
+
summary,
|
|
51
|
+
profile: result.profile,
|
|
52
|
+
ok: result.ok,
|
|
53
|
+
spec: result.spec,
|
|
54
|
+
diagnostics: result.diagnostics,
|
|
55
|
+
registrySource: result.registrySource,
|
|
56
|
+
registryVersion: result.registryVersion,
|
|
57
|
+
warnings: result.warnings,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
export default tool;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { previewOpsReport } from "#compose/preview.js";
|
|
3
|
+
const inputSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
cwd: z.string().optional(),
|
|
6
|
+
profile: z.literal("ops-report"),
|
|
7
|
+
spec: z.unknown(),
|
|
8
|
+
})
|
|
9
|
+
.strict();
|
|
10
|
+
const tool = {
|
|
11
|
+
name: "wp_ui_preview",
|
|
12
|
+
description: "Validate an ops-report compose spec, write self-contained HTML under the worktree state surface (or .webpresso/compose fallback), and return { path } only. Does not open a browser. admin-crud is not supported for preview.",
|
|
13
|
+
inputSchema,
|
|
14
|
+
annotations: {
|
|
15
|
+
title: "UI ops-report preview",
|
|
16
|
+
readOnlyHint: false,
|
|
17
|
+
destructiveHint: false,
|
|
18
|
+
idempotentHint: true,
|
|
19
|
+
openWorldHint: false,
|
|
20
|
+
},
|
|
21
|
+
handler: async (raw) => {
|
|
22
|
+
try {
|
|
23
|
+
const input = inputSchema.parse(raw ?? {});
|
|
24
|
+
const { path } = previewOpsReport({ cwd: input.cwd, spec: input.spec });
|
|
25
|
+
const summary = `ops-report preview written: ${path}`;
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text", text: summary }],
|
|
28
|
+
structuredContent: {
|
|
29
|
+
passed: true,
|
|
30
|
+
summary,
|
|
31
|
+
path,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const message = error instanceof z.ZodError
|
|
37
|
+
? `Invalid wp_ui_preview input: ${error.issues.map((i) => i.message).join("; ")} (ops-report profile only; admin-crud is not supported)`
|
|
38
|
+
: error instanceof Error
|
|
39
|
+
? error.message
|
|
40
|
+
: String(error);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: message }],
|
|
43
|
+
structuredContent: {
|
|
44
|
+
passed: false,
|
|
45
|
+
summary: message,
|
|
46
|
+
},
|
|
47
|
+
isError: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
export default tool;
|
package/dist/esm/package.json
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"#worktrees/*": "./worktrees/*.js",
|
|
26
26
|
"#fleet/*.js": "./fleet/*.js",
|
|
27
27
|
"#fleet/*": "./fleet/*.js",
|
|
28
|
+
"#compose/*.js": "./compose/*.js",
|
|
29
|
+
"#compose/*": "./compose/*.js",
|
|
28
30
|
"#cli/auto-update/*.js": "./cli/auto-update/*.js",
|
|
29
31
|
"#cli/auto-update/*": "./cli/auto-update/*.js",
|
|
30
32
|
"#cli/*.js": "./cli/*.js",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { executeAllowedWpCommand } from "#trust/command-runner.js";
|
|
1
|
+
import { executeAllowedWpCommand, type AllowedWpCommandResult } from "#trust/command-runner.js";
|
|
2
2
|
import type { ReviewTargetCheckout } from "#review/execution/review-checkout.js";
|
|
3
|
+
import type { SandboxAvailability } from "#review/execution/sandbox/types.js";
|
|
3
4
|
export interface ReviewGateVerificationCommand {
|
|
4
5
|
readonly command: string;
|
|
5
6
|
readonly outcome: "passed" | "failed" | "timed-out" | "deferred";
|
|
@@ -7,12 +8,29 @@ export interface ReviewGateVerificationCommand {
|
|
|
7
8
|
readonly exitCode: number | null;
|
|
8
9
|
readonly signal?: string;
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* How the gate commands were isolated. `srt` = OS-sandboxed; `disabled-by-operator`
|
|
13
|
+
* = an explicit `WP_REVIEW_UNSANDBOXED_GATES=1` override ran them unsandboxed;
|
|
14
|
+
* `unavailable` = no backend and no override, so no gate ran (fail-closed).
|
|
15
|
+
*/
|
|
16
|
+
export type ReviewSandboxMode = "srt" | "disabled-by-operator" | "unavailable";
|
|
10
17
|
export interface ReviewGateVerification {
|
|
11
18
|
readonly status: "passed" | "failed";
|
|
12
19
|
readonly commands: readonly ReviewGateVerificationCommand[];
|
|
13
20
|
readonly failureCode?: string;
|
|
14
21
|
readonly logPath?: string;
|
|
22
|
+
readonly sandbox?: ReviewSandboxMode;
|
|
23
|
+
readonly sandboxReason?: string;
|
|
15
24
|
}
|
|
25
|
+
/** Runs one gate command in the review checkout and returns its raw result. */
|
|
26
|
+
export type RunSandboxedGate = (args: {
|
|
27
|
+
readonly command: string;
|
|
28
|
+
readonly executionRoot: string;
|
|
29
|
+
readonly logRoot: string;
|
|
30
|
+
readonly runTmpDir: string;
|
|
31
|
+
readonly projectRoot: string;
|
|
32
|
+
readonly signal?: AbortSignal;
|
|
33
|
+
}) => Promise<AllowedWpCommandResult>;
|
|
16
34
|
export interface VerifyDeliveryGatesInput {
|
|
17
35
|
readonly purpose: "plan" | "delivery";
|
|
18
36
|
readonly blueprintContent: string;
|
|
@@ -20,8 +38,21 @@ export interface VerifyDeliveryGatesInput {
|
|
|
20
38
|
readonly gateId: string;
|
|
21
39
|
readonly checkout: ReviewTargetCheckout;
|
|
22
40
|
readonly signal?: AbortSignal;
|
|
41
|
+
/** Legacy/override execution path (unsandboxed). */
|
|
23
42
|
readonly executeCommand?: typeof executeAllowedWpCommand;
|
|
43
|
+
/** Test seam: resolve the sandbox backend. */
|
|
44
|
+
readonly resolveAvailability?: () => Promise<SandboxAvailability>;
|
|
45
|
+
/** Test seam: run a single sandboxed gate. */
|
|
46
|
+
readonly runSandboxedGate?: RunSandboxedGate;
|
|
24
47
|
}
|
|
25
|
-
export declare function verifyDeliveryPromotionGates(input: VerifyDeliveryGatesInput): ReviewGateVerification | undefined
|
|
48
|
+
export declare function verifyDeliveryPromotionGates(input: VerifyDeliveryGatesInput): Promise<ReviewGateVerification | undefined>;
|
|
49
|
+
/**
|
|
50
|
+
* Builds the real {@link RunSandboxedGate}. `cleanupSandbox` is a test-only
|
|
51
|
+
* seam (defaults to the real {@link cleanupSandboxCommand}) so unit tests can
|
|
52
|
+
* force the fail-closed "passing gate, leaky sandbox" path without a real srt
|
|
53
|
+
* backend; every non-test caller gets the real cleanup and byte-identical
|
|
54
|
+
* behavior to before this seam existed.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createRunSandboxedGate(cleanupSandbox?: () => Promise<void>): RunSandboxedGate;
|
|
26
57
|
export declare function verificationPromptSummary(verification: ReviewGateVerification | undefined): string | undefined;
|
|
27
58
|
export declare function verificationArtifactSection(verification: ReviewGateVerification | undefined): string;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
1
3
|
import path from "node:path";
|
|
2
|
-
import { executeAllowedWpCommand, relativeCommandLogPath, } from "#trust/command-runner.js";
|
|
4
|
+
import { executeAllowedWpCommand, rejectedInvocationResult, relativeCommandLogPath, resolveInvocationFromCommand, runResolvedInvocationAsync, } from "#trust/command-runner.js";
|
|
3
5
|
import { parseTrustDossier } from "#trust/dossier.js";
|
|
4
6
|
import { parseAllowedWpCommand } from "#trust/gates.js";
|
|
5
|
-
|
|
7
|
+
import { buildSandboxedGateEnv } from "#review/execution/sandbox/env.js";
|
|
8
|
+
import { cleanupSandboxCommand, wrapGateForSandbox } from "#review/execution/sandbox/adapter.js";
|
|
9
|
+
import { resolveSandboxAvailability } from "#review/execution/sandbox/probe.js";
|
|
10
|
+
const UNSANDBOXED_OVERRIDE_ENV = "WP_REVIEW_UNSANDBOXED_GATES";
|
|
11
|
+
export async function verifyDeliveryPromotionGates(input) {
|
|
6
12
|
if (input.purpose !== "delivery")
|
|
7
13
|
return undefined;
|
|
8
14
|
const parsed = parseTrustDossier(input.blueprintContent);
|
|
@@ -12,48 +18,82 @@ export function verifyDeliveryPromotionGates(input) {
|
|
|
12
18
|
parsed.violations[0]?.message === "missing Trust Dossier section") {
|
|
13
19
|
return undefined;
|
|
14
20
|
}
|
|
21
|
+
return { status: "failed", commands: [], failureCode: "malformed-trust-dossier" };
|
|
22
|
+
}
|
|
23
|
+
const gates = parsed.dossier?.gates ?? [];
|
|
24
|
+
if (gates.length === 0)
|
|
25
|
+
return undefined;
|
|
26
|
+
const overrideUnsandboxed = process.env[UNSANDBOXED_OVERRIDE_ENV] === "1";
|
|
27
|
+
const availability = await (input.resolveAvailability?.() ?? resolveSandboxAvailability());
|
|
28
|
+
if (availability.kind === "none" && !overrideUnsandboxed) {
|
|
15
29
|
return {
|
|
16
30
|
status: "failed",
|
|
17
31
|
commands: [],
|
|
18
|
-
failureCode: "
|
|
32
|
+
failureCode: "sandbox-unavailable",
|
|
33
|
+
sandbox: "unavailable",
|
|
34
|
+
sandboxReason: availability.reason,
|
|
19
35
|
};
|
|
20
36
|
}
|
|
21
|
-
const
|
|
22
|
-
if (gates.length === 0)
|
|
23
|
-
return undefined;
|
|
24
|
-
const commands = [];
|
|
25
|
-
const executeCommand = input.executeCommand ?? executeAllowedWpCommand;
|
|
37
|
+
const sandbox = overrideUnsandboxed ? "disabled-by-operator" : "srt";
|
|
26
38
|
const logRoot = path.join(input.projectRoot, ".webpresso", "logs", "delivery-verification", input.gateId);
|
|
27
|
-
|
|
39
|
+
const runTmpDir = sandbox === "srt" ? mkdtempSync(path.join(tmpdir(), "wp-review-run-")) : undefined;
|
|
40
|
+
try {
|
|
41
|
+
return await runGateLoop(input, { gates, sandbox, logRoot, runTmpDir });
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (runTmpDir !== undefined)
|
|
45
|
+
rmSync(runTmpDir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function runGateLoop(input, ctx) {
|
|
49
|
+
const commands = [];
|
|
50
|
+
const runSandboxedGate = input.runSandboxedGate ?? defaultRunSandboxedGate;
|
|
51
|
+
for (const gate of ctx.gates) {
|
|
28
52
|
if (input.signal?.aborted) {
|
|
29
53
|
return {
|
|
30
54
|
status: "failed",
|
|
31
55
|
commands,
|
|
32
56
|
failureCode: "verification-cancelled",
|
|
57
|
+
sandbox: ctx.sandbox,
|
|
33
58
|
};
|
|
34
59
|
}
|
|
35
60
|
if (isApprovalDependentLifecycleGate(gate.command)) {
|
|
36
|
-
commands.push({
|
|
37
|
-
command: gate.command,
|
|
38
|
-
outcome: "deferred",
|
|
39
|
-
durationMs: 0,
|
|
40
|
-
exitCode: null,
|
|
41
|
-
});
|
|
61
|
+
commands.push({ command: gate.command, outcome: "deferred", durationMs: 0, exitCode: null });
|
|
42
62
|
continue;
|
|
43
63
|
}
|
|
44
|
-
const result =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
const result = ctx.sandbox === "srt"
|
|
65
|
+
? await runSandboxedGate({
|
|
66
|
+
command: gate.command,
|
|
67
|
+
executionRoot: input.checkout.cwd,
|
|
68
|
+
logRoot: ctx.logRoot,
|
|
69
|
+
runTmpDir: ctx.runTmpDir ?? "",
|
|
70
|
+
projectRoot: input.projectRoot,
|
|
71
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
72
|
+
})
|
|
73
|
+
: (input.executeCommand ?? executeAllowedWpCommand)({
|
|
74
|
+
command: gate.command,
|
|
75
|
+
executionRoot: input.checkout.cwd,
|
|
76
|
+
logRoot: ctx.logRoot,
|
|
77
|
+
env: { CI: "1", NO_COLOR: "1", WP_FORCE_SOURCE: "1" },
|
|
78
|
+
});
|
|
50
79
|
commands.push(publicCommandResult(result));
|
|
80
|
+
// A cancellation DURING a gate (abort mid-execution) must report as
|
|
81
|
+
// cancelled, not a generic command failure.
|
|
82
|
+
if (input.signal?.aborted || result.failureCode === "command-cancelled") {
|
|
83
|
+
return {
|
|
84
|
+
status: "failed",
|
|
85
|
+
commands,
|
|
86
|
+
failureCode: "verification-cancelled",
|
|
87
|
+
sandbox: ctx.sandbox,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
51
90
|
if (result.outcome !== "passed") {
|
|
52
91
|
return {
|
|
53
92
|
status: "failed",
|
|
54
93
|
commands,
|
|
55
94
|
failureCode: result.failureCode ?? "verification-command-failed",
|
|
56
95
|
logPath: relativeCommandLogPath(input.projectRoot, result.logPath),
|
|
96
|
+
sandbox: ctx.sandbox,
|
|
57
97
|
};
|
|
58
98
|
}
|
|
59
99
|
const changes = input.checkout.changes();
|
|
@@ -63,12 +103,76 @@ export function verifyDeliveryPromotionGates(input) {
|
|
|
63
103
|
commands,
|
|
64
104
|
failureCode: "verification-worktree-contaminated",
|
|
65
105
|
logPath: relativeCommandLogPath(input.projectRoot, result.logPath),
|
|
106
|
+
sandbox: ctx.sandbox,
|
|
66
107
|
};
|
|
67
108
|
}
|
|
68
109
|
}
|
|
110
|
+
return { status: "passed", commands, sandbox: ctx.sandbox };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Builds the real {@link RunSandboxedGate}. `cleanupSandbox` is a test-only
|
|
114
|
+
* seam (defaults to the real {@link cleanupSandboxCommand}) so unit tests can
|
|
115
|
+
* force the fail-closed "passing gate, leaky sandbox" path without a real srt
|
|
116
|
+
* backend; every non-test caller gets the real cleanup and byte-identical
|
|
117
|
+
* behavior to before this seam existed.
|
|
118
|
+
*/
|
|
119
|
+
export function createRunSandboxedGate(cleanupSandbox = cleanupSandboxCommand) {
|
|
120
|
+
return async (args) => {
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
const env = buildSandboxedGateEnv(args.runTmpDir);
|
|
123
|
+
let invocation;
|
|
124
|
+
try {
|
|
125
|
+
invocation = resolveInvocationFromCommand(args.executionRoot, args.command, env, {
|
|
126
|
+
replaceProcessEnv: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return rejectedInvocationResult(args.command, args.logRoot, startedAt, error);
|
|
131
|
+
}
|
|
132
|
+
let result;
|
|
133
|
+
try {
|
|
134
|
+
// The wrap is INSIDE the try so a wrap failure (unresolvable srt, empty
|
|
135
|
+
// argv) still produces a persisted failed verification instead of escaping.
|
|
136
|
+
const wrapped = await wrapGateForSandbox(invocation, {
|
|
137
|
+
checkoutRoot: realpathSync(args.executionRoot),
|
|
138
|
+
runTmpDir: realpathSync(args.runTmpDir),
|
|
139
|
+
projectRoot: args.projectRoot,
|
|
140
|
+
});
|
|
141
|
+
result = await runResolvedInvocationAsync({ command: wrapped.command, args: [...wrapped.args], env: wrapped.env }, {
|
|
142
|
+
command: args.command,
|
|
143
|
+
executionRoot: args.executionRoot,
|
|
144
|
+
logRoot: args.logRoot,
|
|
145
|
+
startedAt,
|
|
146
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
result = rejectedInvocationResult(args.command, args.logRoot, startedAt, error);
|
|
151
|
+
}
|
|
152
|
+
// Sandbox cleanup is part of the gate's integrity: an unbalanced srt counter
|
|
153
|
+
// or leftover mounts must NOT be reported as a clean pass. If cleanup fails
|
|
154
|
+
// after an otherwise-passing gate, convert the result to a failed
|
|
155
|
+
// verification and keep the gate output as secondary evidence. A gate that
|
|
156
|
+
// already failed keeps its more-informative primary failure.
|
|
157
|
+
try {
|
|
158
|
+
await cleanupSandbox();
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (result.outcome === "passed")
|
|
162
|
+
return sandboxCleanupFailedResult(result, error);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const defaultRunSandboxedGate = createRunSandboxedGate();
|
|
168
|
+
function sandboxCleanupFailedResult(gateResult, error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
170
|
return {
|
|
70
|
-
|
|
71
|
-
|
|
171
|
+
...gateResult,
|
|
172
|
+
outcome: "failed",
|
|
173
|
+
failureCode: "sandbox-cleanup-failed",
|
|
174
|
+
errorMessage: `sandbox cleanup did not complete after a passing gate: ${message}`,
|
|
175
|
+
stderr: `${gateResult.stderr}\n[sandbox-cleanup-failed] ${message}`.trim(),
|
|
72
176
|
};
|
|
73
177
|
}
|
|
74
178
|
export function verificationPromptSummary(verification) {
|
|
@@ -77,7 +181,7 @@ export function verificationPromptSummary(verification) {
|
|
|
77
181
|
if (verification.status !== "passed")
|
|
78
182
|
return undefined;
|
|
79
183
|
return [
|
|
80
|
-
|
|
184
|
+
`Host verification before outside review: passed (sandbox: ${verification.sandbox ?? "unknown"}).`,
|
|
81
185
|
...verification.commands.map((command, index) => `${index + 1}. ${command.command}: ${command.outcome === "deferred" ? "deferred until review publication" : "passed"}`),
|
|
82
186
|
].join("\n");
|
|
83
187
|
}
|
|
@@ -97,6 +201,8 @@ export function verificationArtifactSection(verification) {
|
|
|
97
201
|
"## Host Verification",
|
|
98
202
|
"",
|
|
99
203
|
`Status: ${verification.status}`,
|
|
204
|
+
`Sandbox: ${verification.sandbox ?? "unknown"}`,
|
|
205
|
+
...(verification.sandboxReason ? [`Sandbox detail: ${verification.sandboxReason}`] : []),
|
|
100
206
|
...verification.commands.map((command, index) => `${index + 1}. ${command.command} -> ${command.outcome} (${command.durationMs}ms, exit ${command.exitCode ?? "null"})`),
|
|
101
207
|
...(verification.failureCode ? [`Failure code: ${verification.failureCode}`] : []),
|
|
102
208
|
...(verification.logPath ? [`Log: ${verification.logPath}`] : []),
|
|
@@ -6,6 +6,7 @@ export interface ReviewTargetCheckout {
|
|
|
6
6
|
}
|
|
7
7
|
declare function statusEntryPath(line: string): string;
|
|
8
8
|
export declare function createReviewTargetCheckout(repositoryRoot: string, targetSha: string): ReviewTargetCheckout;
|
|
9
|
+
export declare function assertDependencyContainment(checkoutRoot: string): void;
|
|
9
10
|
/** Test-only surface for porcelain path parsing. */
|
|
10
11
|
export declare const reviewCheckoutInternals: {
|
|
11
12
|
statusEntryPath: typeof statusEntryPath;
|