Skip to content

Commit a184b7a

Browse files
authored
Add workspace policy checker (#179)
1 parent 6366c6c commit a184b7a

6 files changed

Lines changed: 499 additions & 2 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"package-distribution-smoke": "tsx scripts/package-distribution-smoke.ts",
3535
"release:package": "tsx scripts/package-release-artifact.ts",
3636
"policy-validation-smoke": "tsx scripts/policy-validation-smoke.ts",
37+
"workspace-policy-smoke": "tsx scripts/workspace-policy-smoke.ts",
3738
"artifact-contract-smoke": "tsx scripts/artifact-contract-smoke.ts",
3839
"external-adapter-contract-smoke": "tsx scripts/external-adapter-contract-smoke.ts",
3940
"runtime-episode-smoke": "tsx scripts/runtime-episode-smoke.ts",
@@ -57,7 +58,7 @@
5758
"recipe-staged-files-smoke": "tsx scripts/recipe-staged-files-smoke.ts",
5859
"wp-codebox": "node packages/cli/dist/index.js",
5960
"wordpress-plugin-smoke": "php tests/smoke-wordpress-plugin.php",
60-
"check": "npm run build && npm run discovery-command-smoke && npm run theme-check-normalization-smoke && npm run agent-sandbox-code-smoke && npm run policy-validation-smoke && npm run wordpress-plugin-smoke && npm run package-distribution-smoke && npm run artifact-contract-smoke && npm run external-adapter-contract-smoke && npm run runtime-episode-smoke && npm run core-phpunit-command-smoke && npm run plugin-check-normalization-smoke && npm run recipe-bench-smoke && npm run recipe-dry-run-smoke && npm run recipe-workflow-phases-smoke && npm run recipe-site-seed-smoke && npm run recipe-staged-files-smoke && npm run preview-port-smoke && npm run preview-public-url-canonical-smoke && npm run preview-response-body-smoke && npm run boot-preview-smoke && npm run blueprint-validation-smoke && npm run browser-probe-artifact-smoke && npm run wp-codebox -- run --mount ./examples/simple-plugin:/wordpress/wp-content/plugins/simple-plugin --command wordpress.run-php --arg code-file=./examples/simple-plugin/probe.php --artifacts ./artifacts --json"
61+
"check": "npm run build && npm run discovery-command-smoke && npm run theme-check-normalization-smoke && npm run agent-sandbox-code-smoke && npm run policy-validation-smoke && npm run workspace-policy-smoke && npm run wordpress-plugin-smoke && npm run package-distribution-smoke && npm run artifact-contract-smoke && npm run external-adapter-contract-smoke && npm run runtime-episode-smoke && npm run core-phpunit-command-smoke && npm run plugin-check-normalization-smoke && npm run recipe-bench-smoke && npm run recipe-dry-run-smoke && npm run recipe-workflow-phases-smoke && npm run recipe-site-seed-smoke && npm run recipe-staged-files-smoke && npm run preview-port-smoke && npm run preview-public-url-canonical-smoke && npm run preview-response-body-smoke && npm run boot-preview-smoke && npm run blueprint-validation-smoke && npm run browser-probe-artifact-smoke && npm run wp-codebox -- run --mount ./examples/simple-plugin:/wordpress/wp-content/plugins/simple-plugin --command wordpress.run-php --arg code-file=./examples/simple-plugin/probe.php --artifacts ./artifacts --json"
6162
},
6263
"workspaces": [
6364
"packages/*"

packages/cli/src/index.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { basename, dirname, join, resolve } from "node:path"
88
import { Readable } from "node:stream"
99
import { pipeline } from "node:stream/promises"
1010
import { promisify } from "node:util"
11-
import { SANDBOX_DMC_PARENT_ONLY_ABILITIES, SANDBOX_DMC_SAFE_ABILITIES, SANDBOX_WORKSPACE_ROOT, createRuntime, validateRuntimePolicy, type ArtifactBundle, type ExecutionResult, type MountSpec, type Runtime, type RuntimeInfo, type RuntimePolicy, type SandboxWorkspaceContract, type SandboxWorkspaceMode, type WorkspaceRecipe, type WorkspaceRecipeExtraPlugin, type WorkspaceRecipeSiteSeed, type WorkspaceRecipeStagedFile, type WorkspaceRecipeWorkspace } from "@chubes4/wp-codebox-core"
11+
import { SANDBOX_DMC_PARENT_ONLY_ABILITIES, SANDBOX_DMC_SAFE_ABILITIES, SANDBOX_WORKSPACE_ROOT, checkWorkspacePolicy, createRuntime, validateRuntimePolicy, type ArtifactBundle, type ExecutionResult, type MountSpec, type Runtime, type RuntimeInfo, type RuntimePolicy, type SandboxWorkspaceContract, type SandboxWorkspaceMode, type WorkspacePolicyResult, type WorkspaceRecipe, type WorkspaceRecipeExtraPlugin, type WorkspaceRecipeSiteSeed, type WorkspaceRecipeStagedFile, type WorkspaceRecipeWorkspace } from "@chubes4/wp-codebox-core"
1212
import { createPlaygroundRuntimeBackend } from "@chubes4/wp-codebox-playground"
1313
import { agentRuntimeProbeCode, agentSandboxRunCode, resolveSandboxTaskCode } from "./agent-code.js"
1414
import { captureStdout, printBatchHumanOutput, printBlueprintValidateHumanOutput, printBootHumanOutput, printCommandCatalogHumanOutput, printHelp, printHumanOutput, printRecipeHumanOutput, printRecipeSchemaHumanOutput, printRecipeValidateHumanOutput, serializeError } from "./output.js"
@@ -130,6 +130,14 @@ interface RecipeValidateOptions {
130130
json: boolean
131131
}
132132

133+
interface WorkspacePolicyOptions {
134+
workspaceRoot: string
135+
writableRoots: string[]
136+
hiddenPaths: string[]
137+
gitBacked: boolean
138+
json: boolean
139+
}
140+
133141
interface RecipeValidationIssue {
134142
code: string
135143
path: string
@@ -943,6 +951,30 @@ async function main(args: string[]): Promise<number> {
943951
return output.success ? 0 : 1
944952
}
945953

954+
if (command === "workspace-policy") {
955+
const subcommand = args.shift()
956+
if (subcommand !== "check") {
957+
console.error(`Unknown workspace-policy command: ${subcommand ?? ""}`)
958+
printHelp()
959+
return 1
960+
}
961+
962+
const options = parseWorkspacePolicyOptions(args)
963+
const output = await checkWorkspacePolicy({
964+
workspaceRoot: options.workspaceRoot,
965+
writableRoots: options.writableRoots,
966+
hiddenPaths: options.hiddenPaths,
967+
gitBacked: options.gitBacked,
968+
})
969+
if (!options.json) {
970+
printWorkspacePolicyHumanOutput(output)
971+
return output.passed ? 0 : 1
972+
}
973+
974+
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`)
975+
return output.passed ? 0 : 1
976+
}
977+
946978
if (command === "commands") {
947979
const json = parseDiscoveryJsonOption(args)
948980
const output = commandCatalogOutput()
@@ -1061,6 +1093,74 @@ function parseDiscoveryJsonOption(args: string[]): boolean {
10611093
return json
10621094
}
10631095

1096+
function parseWorkspacePolicyOptions(args: string[]): WorkspacePolicyOptions {
1097+
const options: WorkspacePolicyOptions = {
1098+
workspaceRoot: process.cwd(),
1099+
writableRoots: [],
1100+
hiddenPaths: [],
1101+
gitBacked: false,
1102+
json: false,
1103+
}
1104+
1105+
while (args.length > 0) {
1106+
const arg = args.shift()
1107+
if (!arg) {
1108+
continue
1109+
}
1110+
1111+
if (arg === "--json") {
1112+
options.json = true
1113+
continue
1114+
}
1115+
if (arg === "--git") {
1116+
options.gitBacked = true
1117+
continue
1118+
}
1119+
1120+
const value = args.shift()
1121+
if (!value) {
1122+
throw new Error(`Missing value for ${arg}`)
1123+
}
1124+
1125+
switch (arg) {
1126+
case "--workspace":
1127+
case "--workspace-root":
1128+
options.workspaceRoot = resolve(value)
1129+
break
1130+
case "--writable-root":
1131+
case "--writable":
1132+
options.writableRoots.push(value)
1133+
break
1134+
case "--hidden-path":
1135+
case "--hidden":
1136+
options.hiddenPaths.push(value)
1137+
break
1138+
default:
1139+
throw new Error(`Unknown option: ${arg}`)
1140+
}
1141+
}
1142+
1143+
if (options.writableRoots.length === 0) {
1144+
throw new Error("At least one --writable-root is required")
1145+
}
1146+
1147+
return options
1148+
}
1149+
1150+
function printWorkspacePolicyHumanOutput(output: WorkspacePolicyResult): void {
1151+
console.log(output.passed ? "Workspace policy passed" : "Workspace policy failed")
1152+
console.log(`Policy: ${output.policy_sha256}`)
1153+
if (output.violations.length === 0) {
1154+
return
1155+
}
1156+
1157+
console.log("Violations:")
1158+
for (const violation of output.violations) {
1159+
console.log(`- ${violation.code}: ${violation.path}`)
1160+
console.log(` ${violation.message}`)
1161+
}
1162+
}
1163+
10641164
function commandCatalogOutput(): CommandCatalogOutput {
10651165
return {
10661166
schema: "wp-codebox/command-catalog/v1",

packages/cli/src/output.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export function printHelp(): void {
231231
console.log(`Usage:
232232
wp-codebox commands [--json]
233233
wp-codebox schema recipe [--json]
234+
wp-codebox workspace-policy check --workspace-root <path> --writable-root <path> [options]
234235
wp-codebox recipe validate --recipe <path> [--json]
235236
wp-codebox validate-blueprint --blueprint <json|file> [options]
236237
wp-codebox recipe-run --recipe <path> [options]
@@ -259,6 +260,14 @@ Options:
259260
--dry-run Validate recipe-run and emit a resolved JSON plan without booting Playground or writing temp workspaces.
260261
--json Emit machine-readable JSON.
261262
263+
Workspace policy:
264+
--workspace-root <dir>
265+
Workspace root to inspect. Defaults to the current directory.
266+
--writable-root <path>
267+
Relative path that may be changed. Repeatable.
268+
--hidden-path <path> Relative path that must not be changed or exposed. Repeatable.
269+
--git Use git status metadata, including ignored and unmerged entries.
270+
262271
Discovery:
263272
commands Print supported runtime and recipe command metadata.
264273
schema recipe Print the wp-codebox/workspace-recipe/v1 JSON Schema.

packages/runtime-core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export * from "./workspace-policy.js"
2+
13
export type RuntimeBackendKind = "wordpress-playground" | (string & {})
24

35
export const SANDBOX_WORKSPACE_ROOT = "/workspace"

0 commit comments

Comments
 (0)