@storewright/cli 0.14.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 (67) hide show
  1. package/README.md +19 -0
  2. package/VERSION +1 -0
  3. package/bin/storewright.mjs +62 -0
  4. package/contracts/action-registry.json +175 -0
  5. package/contracts/capability-registry.json +63 -0
  6. package/contracts/workflow-manifest.json +207 -0
  7. package/lib/cli/storewright-cli.mjs +259 -0
  8. package/lib/internal/launch-envelope.mjs +223 -0
  9. package/lib/internal/multi-agent-contracts.mjs +137 -0
  10. package/lib/internal/operation-ledger.mjs +190 -0
  11. package/lib/internal/pricing/default-preview-pricing.mjs +181 -0
  12. package/lib/internal/run-state-helpers.mjs +313 -0
  13. package/lib/internal/shopify-operation-adapter.mjs +456 -0
  14. package/package.json +38 -0
  15. package/schemas/action-registry.schema.json +11 -0
  16. package/schemas/agent-report.schema.json +14 -0
  17. package/schemas/approval-grant.schema.json +16 -0
  18. package/schemas/base-theme-report.schema.json +25 -0
  19. package/schemas/brand-identity.schema.json +142 -0
  20. package/schemas/capability-registry.schema.json +11 -0
  21. package/schemas/competitor-audit.schema.json +38 -0
  22. package/schemas/design-direction.schema.json +64 -0
  23. package/schemas/external-operation.schema.json +34 -0
  24. package/schemas/intake-blocked-report.schema.json +76 -0
  25. package/schemas/launch-envelope.schema.json +25 -0
  26. package/schemas/launch-readiness.schema.json +73 -0
  27. package/schemas/media-file-inspection-report.schema.json +223 -0
  28. package/schemas/media-manifest.schema.json +84 -0
  29. package/schemas/merchandising-brief.schema.json +27 -0
  30. package/schemas/normalized-product-catalog.schema.json +42 -0
  31. package/schemas/product-content-generation-input.schema.json +40 -0
  32. package/schemas/product-content-generation-output.schema.json +43 -0
  33. package/schemas/raw-product-candidates.schema.json +32 -0
  34. package/schemas/shopify-access-preflight-report.schema.json +213 -0
  35. package/schemas/shopify-content-sync-report.schema.json +190 -0
  36. package/schemas/shopify-media-map.schema.json +87 -0
  37. package/schemas/shopify-media-upload-report.schema.json +96 -0
  38. package/schemas/shopify-operation-request.schema.json +81 -0
  39. package/schemas/shopify-preflight-report.schema.json +187 -0
  40. package/schemas/store-blueprint.schema.json +112 -0
  41. package/schemas/store-content-generation-output.schema.json +102 -0
  42. package/schemas/store-intake.schema.json +205 -0
  43. package/schemas/store-ops-plan.schema.json +82 -0
  44. package/schemas/storefront-preview-review.schema.json +227 -0
  45. package/schemas/supplier-access-report.schema.json +36 -0
  46. package/schemas/supplier-extraction-report.schema.json +185 -0
  47. package/schemas/theme-build-report.schema.json +43 -0
  48. package/schemas/theme-code-change-summary.schema.json +65 -0
  49. package/schemas/theme-plan.schema.json +26 -0
  50. package/schemas/theme-push-report.schema.json +151 -0
  51. package/schemas/theme-workspace-validation-report.schema.json +61 -0
  52. package/schemas/workflow-manifest.schema.json +29 -0
  53. package/scripts/audit-run-state.mjs +472 -0
  54. package/scripts/execute-shopify-operation.mjs +190 -0
  55. package/scripts/generate-image-assets-openai.mjs +342 -0
  56. package/scripts/generate-media-assets.mjs +121 -0
  57. package/scripts/init-run-state.mjs +69 -0
  58. package/scripts/inspect-media-files.mjs +334 -0
  59. package/scripts/prepare-launch-envelope.mjs +47 -0
  60. package/scripts/shopify-access-preflight.mjs +432 -0
  61. package/scripts/upload-shopify-media.mjs +831 -0
  62. package/scripts/validate-agent-report.mjs +46 -0
  63. package/scripts/validate-artifact.mjs +196 -0
  64. package/scripts/validate-launch-envelope.mjs +50 -0
  65. package/scripts/validate-registries.mjs +50 -0
  66. package/scripts/validate-workflow-manifest.mjs +38 -0
  67. package/scripts/version.mjs +192 -0
@@ -0,0 +1,259 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { promisify } from "node:util";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const cliDir = dirname(fileURLToPath(import.meta.url));
11
+ const packageRoot = resolve(cliDir, "..", "..");
12
+ const defaultMarketplaceSource = "git@github.com:HughLee824/storewright.git";
13
+ const defaultMarketplaceName = "storewright";
14
+
15
+ const scriptCommandMap = new Map([
16
+ ["audit-run-state.mjs", ["run", "audit"]],
17
+ ["execute-shopify-operation.mjs", ["shopify", "operation", "execute"]],
18
+ ["generate-image-assets-openai.mjs", ["media", "generate-openai"]],
19
+ ["generate-media-assets.mjs", ["media", "generate"]],
20
+ ["init-run-state.mjs", ["run", "init"]],
21
+ ["inspect-media-files.mjs", ["media", "inspect"]],
22
+ ["prepare-launch-envelope.mjs", ["launch-envelope", "prepare"]],
23
+ ["shopify-access-preflight.mjs", ["shopify", "access-preflight"]],
24
+ ["upload-shopify-media.mjs", ["media", "upload"]],
25
+ ["validate-agent-report.mjs", ["agent-report", "validate"]],
26
+ ["validate-artifact.mjs", ["artifact", "validate"]],
27
+ ["validate-launch-envelope.mjs", ["launch-envelope", "validate"]],
28
+ ["validate-registries.mjs", ["registries", "validate"]],
29
+ ["validate-workflow-manifest.mjs", ["workflow-manifest", "validate"]],
30
+ ["version.mjs", ["version"]]
31
+ ]);
32
+
33
+ const commandScriptMap = new Map(Array.from(scriptCommandMap.entries()).map(([script, command]) => [command.join(" "), script]));
34
+
35
+ export function cliCommandForScript(scriptName) {
36
+ return scriptCommandMap.get(scriptName);
37
+ }
38
+
39
+ function parseTomlName(source) {
40
+ return source.match(/^name = "([^"]+)"/m)?.[1] ?? null;
41
+ }
42
+
43
+ async function codexExec(args, { env = process.env, label = "codex" } = {}) {
44
+ try {
45
+ const result = await execFileAsync("codex", args, {
46
+ env,
47
+ encoding: "utf8",
48
+ maxBuffer: 10 * 1024 * 1024
49
+ });
50
+ return result;
51
+ } catch (error) {
52
+ if (error.code === "ENOENT") {
53
+ throw new Error("Codex CLI was not found on PATH. Install Codex CLI before running storewright codex install.");
54
+ }
55
+ const stderr = String(error.stderr ?? "").trim();
56
+ const stdout = String(error.stdout ?? "").trim();
57
+ const detail = [stderr, stdout].filter(Boolean).join("\n");
58
+ throw new Error(`${label} failed${detail ? `: ${detail}` : ""}`);
59
+ }
60
+ }
61
+
62
+ function parseJsonOutput(stdout, label) {
63
+ const text = stdout.trim();
64
+ try {
65
+ return JSON.parse(text);
66
+ } catch {
67
+ for (let index = text.lastIndexOf("{"); index >= 0; index = text.lastIndexOf("{", index - 1)) {
68
+ try {
69
+ return JSON.parse(text.slice(index));
70
+ } catch {
71
+ // Keep scanning earlier object starts until one parses.
72
+ }
73
+ }
74
+ }
75
+ throw new Error(`${label} did not return valid JSON.`);
76
+ }
77
+
78
+ function findPluginRoot(value) {
79
+ if (typeof value === "string") {
80
+ return existsSync(join(value, ".codex-plugin", "plugin.json")) ? value : null;
81
+ }
82
+ if (!value || typeof value !== "object") {
83
+ return null;
84
+ }
85
+ for (const [key, child] of Object.entries(value)) {
86
+ if (/path|root/i.test(key) && typeof child === "string") {
87
+ const maybeRoot = findPluginRoot(child);
88
+ if (maybeRoot) return maybeRoot;
89
+ }
90
+ }
91
+ for (const child of Object.values(value)) {
92
+ const maybeRoot = findPluginRoot(child);
93
+ if (maybeRoot) return maybeRoot;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ async function readPluginManifest(pluginRoot) {
99
+ return JSON.parse(await readFile(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8"));
100
+ }
101
+
102
+ async function tomlFiles(dir) {
103
+ const entries = await readdir(dir, { withFileTypes: true });
104
+ return entries
105
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".toml"))
106
+ .map((entry) => join(dir, entry.name))
107
+ .sort();
108
+ }
109
+
110
+ async function installProfilesFromPlugin({ pluginRoot, codexHome }) {
111
+ const sourceDir = join(pluginRoot, "storewright", "scaffolds", "codex-agents");
112
+ if (!existsSync(sourceDir)) {
113
+ throw new Error(`Installed Storewright plugin is missing agent scaffolds: ${sourceDir}`);
114
+ }
115
+
116
+ const targetDir = join(codexHome, "agents");
117
+ await mkdir(targetDir, { recursive: true });
118
+ const installedProfiles = [];
119
+ const installedFiles = [];
120
+ const skippedFiles = [];
121
+
122
+ for (const sourcePath of await tomlFiles(sourceDir)) {
123
+ const source = await readFile(sourcePath, "utf8");
124
+ const profileName = parseTomlName(source);
125
+ if (!profileName) {
126
+ throw new Error(`Missing name in ${sourcePath}`);
127
+ }
128
+ const targetPath = join(targetDir, basename(sourcePath));
129
+ if (existsSync(targetPath)) {
130
+ const existingName = parseTomlName(await readFile(targetPath, "utf8"));
131
+ if (!existingName || !existingName.startsWith("storewright-")) {
132
+ skippedFiles.push(targetPath);
133
+ continue;
134
+ }
135
+ }
136
+ await copyFile(sourcePath, targetPath);
137
+ installedProfiles.push(profileName);
138
+ installedFiles.push(targetPath);
139
+ }
140
+
141
+ return { targetDir, installedProfiles, installedFiles, skippedFiles };
142
+ }
143
+
144
+ function normalizeInstallOptions(options = {}) {
145
+ const source = options.source ?? "git";
146
+ const marketplaceName = options.marketplace ?? defaultMarketplaceName;
147
+ if (!["git", "local", "marketplace"].includes(source)) {
148
+ throw new Error("--source must be one of git, local, or marketplace");
149
+ }
150
+ if (source === "local" && !options.marketplaceRoot) {
151
+ throw new Error("--marketplace-root is required when --source local");
152
+ }
153
+ return {
154
+ source,
155
+ marketplaceName,
156
+ marketplaceRoot: options.marketplaceRoot,
157
+ marketplaceSource: options.marketplaceSource ?? defaultMarketplaceSource,
158
+ codexHome: options.codexHome ?? join(homedir(), ".codex"),
159
+ env: options.env ?? process.env
160
+ };
161
+ }
162
+
163
+ export async function installCodexStorewright(options = {}) {
164
+ const normalized = normalizeInstallOptions(options);
165
+ await codexExec(["--help"], { env: normalized.env, label: "codex --help" });
166
+
167
+ const commandResults = [];
168
+ if (normalized.source === "git") {
169
+ const addMarketplace = await codexExec(["plugin", "marketplace", "add", normalized.marketplaceSource, "--json"], {
170
+ env: normalized.env,
171
+ label: "codex plugin marketplace add"
172
+ });
173
+ commandResults.push({ command: "codex plugin marketplace add", stdout: addMarketplace.stdout.trim() });
174
+ } else if (normalized.source === "local") {
175
+ const addMarketplace = await codexExec(["plugin", "marketplace", "add", normalized.marketplaceRoot, "--json"], {
176
+ env: normalized.env,
177
+ label: "codex plugin marketplace add"
178
+ });
179
+ commandResults.push({ command: "codex plugin marketplace add", stdout: addMarketplace.stdout.trim() });
180
+ }
181
+
182
+ const selector = `storewright@${normalized.marketplaceName}`;
183
+ const addPlugin = await codexExec(["plugin", "add", selector, "--json"], {
184
+ env: normalized.env,
185
+ label: "codex plugin add"
186
+ });
187
+ commandResults.push({ command: "codex plugin add", stdout: addPlugin.stdout.trim() });
188
+ const pluginInstall = parseJsonOutput(addPlugin.stdout, "codex plugin add");
189
+ const pluginRoot = findPluginRoot(pluginInstall);
190
+ if (!pluginRoot) {
191
+ throw new Error("codex plugin add did not report an installed Storewright plugin path.");
192
+ }
193
+
194
+ const manifest = await readPluginManifest(pluginRoot);
195
+ const profileInstall = await installProfilesFromPlugin({ pluginRoot, codexHome: normalized.codexHome });
196
+ const report = {
197
+ schemaVersion: "1.0.0",
198
+ installationStatus: "valid",
199
+ runtimeStatus: "requires_new_session",
200
+ runtimeProbeRequired: true,
201
+ sourceMode: normalized.source,
202
+ marketplaceName: normalized.marketplaceName,
203
+ marketplaceSource: normalized.source === "git" ? normalized.marketplaceSource : undefined,
204
+ marketplaceRoot: normalized.source === "local" ? normalized.marketplaceRoot : undefined,
205
+ pluginRoot,
206
+ pluginVersion: manifest.version ?? "unknown",
207
+ profileTargetDir: profileInstall.targetDir,
208
+ installedProfiles: profileInstall.installedProfiles,
209
+ installedFiles: profileInstall.installedFiles,
210
+ skippedFiles: profileInstall.skippedFiles,
211
+ commands: commandResults,
212
+ nextStep: "Start a new Codex session so Storewright agent profiles are loaded into the runtime."
213
+ };
214
+
215
+ const reportDir = join(normalized.codexHome, "storewright");
216
+ await mkdir(reportDir, { recursive: true });
217
+ await writeFile(join(reportDir, "codex-install-report.json"), `${JSON.stringify(report, null, 2)}\n`);
218
+ return report;
219
+ }
220
+
221
+ export function parseCodexInstallArgs(argv) {
222
+ const options = {};
223
+ for (let index = 0; index < argv.length; index += 1) {
224
+ const arg = argv[index];
225
+ const value = argv[index + 1];
226
+ if (arg === "--source") {
227
+ options.source = value;
228
+ index += 1;
229
+ } else if (arg === "--marketplace-root") {
230
+ options.marketplaceRoot = value;
231
+ index += 1;
232
+ } else if (arg === "--marketplace") {
233
+ options.marketplace = value;
234
+ index += 1;
235
+ } else if (arg === "--codex-home") {
236
+ options.codexHome = value;
237
+ index += 1;
238
+ } else if (arg === "--marketplace-source") {
239
+ options.marketplaceSource = value;
240
+ index += 1;
241
+ }
242
+ }
243
+ return options;
244
+ }
245
+
246
+ export function resolveScriptForCommand(argv) {
247
+ for (let length = Math.min(3, argv.length); length >= 1; length -= 1) {
248
+ const key = argv.slice(0, length).join(" ");
249
+ const script = commandScriptMap.get(key);
250
+ if (script) {
251
+ return { script, consumed: length, passthrough: argv.slice(length) };
252
+ }
253
+ }
254
+ return null;
255
+ }
256
+
257
+ export function scriptPathForCommand(script) {
258
+ return join(packageRoot, "scripts", script);
259
+ }
@@ -0,0 +1,223 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { dirname, join, normalize } from "node:path";
5
+
6
+ export async function sha256File(path) {
7
+ const bytes = await readFile(path);
8
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
9
+ }
10
+
11
+ function stableJson(value) {
12
+ return `${JSON.stringify(sortJson(value), null, 2)}\n`;
13
+ }
14
+
15
+ function sha256Text(value) {
16
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
17
+ }
18
+
19
+ export function canonicalLaunchEnvelopeSha256(envelope) {
20
+ const canonicalEnvelope = { ...envelope };
21
+ delete canonicalEnvelope.launchEnvelopeSha256;
22
+ return sha256Text(stableJson(canonicalEnvelope));
23
+ }
24
+
25
+ function sortJson(value) {
26
+ if (Array.isArray(value)) {
27
+ return value.map(sortJson);
28
+ }
29
+ if (value && typeof value === "object") {
30
+ return Object.fromEntries(
31
+ Object.entries(value)
32
+ .sort(([left], [right]) => left.localeCompare(right))
33
+ .map(([key, child]) => [key, sortJson(child)])
34
+ );
35
+ }
36
+ return value;
37
+ }
38
+
39
+ function normalizedRelativePath(path) {
40
+ if (!path || typeof path !== "string") return null;
41
+ if (path.startsWith("/") || path.includes("\\") || path.includes("\0")) return null;
42
+ const normalized = normalize(path);
43
+ if (normalized === "." || normalized.startsWith("..") || normalized.includes("../")) return null;
44
+ return normalized;
45
+ }
46
+
47
+ function assertSafePathSegment(errors, label, value) {
48
+ if (!value || typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(value)) {
49
+ errors.push(`${label} must be a single safe path segment`);
50
+ }
51
+ }
52
+
53
+ function requestedRuntimePolicyErrors(policy) {
54
+ const errors = [];
55
+ if (!policy || typeof policy !== "object") {
56
+ return ["requestedRuntimePolicy is required"];
57
+ }
58
+ if (policy.kind === "permissions-profile") {
59
+ if (!policy.profile) errors.push("requestedRuntimePolicy.profile is required");
60
+ if (Object.hasOwn(policy, "sandboxMode")) {
61
+ errors.push("requestedRuntimePolicy permissions-profile and sandboxMode are mutually exclusive");
62
+ }
63
+ } else if (policy.kind === "legacy-sandbox") {
64
+ if (!policy.sandboxMode) errors.push("requestedRuntimePolicy.sandboxMode is required");
65
+ if (Object.hasOwn(policy, "profile")) {
66
+ errors.push("requestedRuntimePolicy legacy-sandbox and profile are mutually exclusive");
67
+ }
68
+ } else {
69
+ errors.push("requestedRuntimePolicy.kind must be permissions-profile or legacy-sandbox");
70
+ }
71
+ return errors;
72
+ }
73
+
74
+ function assertSafePath(errors, label, path) {
75
+ const normalized = normalizedRelativePath(path);
76
+ if (!normalized) {
77
+ errors.push(`${label} has unsafe path: ${path}`);
78
+ }
79
+ return normalized;
80
+ }
81
+
82
+ export async function createLaunchEnvelope({
83
+ packageRoot,
84
+ runDir,
85
+ runId,
86
+ stageId,
87
+ stageName,
88
+ attemptId,
89
+ ownerProfile,
90
+ runtimeAgentType,
91
+ packetPath,
92
+ specPath,
93
+ gateSpecPath,
94
+ actionPolicyRef,
95
+ requestedRuntimePolicy,
96
+ runtimeToolSnapshot = [],
97
+ effectiveAllowedActions = [],
98
+ resolvedInputs = [],
99
+ declaredOutputs = []
100
+ }) {
101
+ const attemptPrefix = `stages/${stageId}/attempts/${attemptId}`;
102
+ const envelope = {
103
+ schemaVersion: "1.0.0",
104
+ run: { id: runId },
105
+ stage: { id: stageId, name: stageName },
106
+ attempt: { id: attemptId },
107
+ launchEnvelopePath: `${attemptPrefix}/control/launch-envelope.json`,
108
+ ownerProfile,
109
+ runtimeAgentType,
110
+ packetPath,
111
+ specPath,
112
+ gateSpecPath,
113
+ actionPolicyRef,
114
+ requestedRuntimePolicy,
115
+ runtimeToolSnapshot,
116
+ effectiveAllowedActions: [...effectiveAllowedActions].sort(),
117
+ resolvedInputs,
118
+ declaredOutputs,
119
+ pathConstraints: {
120
+ controlPrefix: `${attemptPrefix}/control/`,
121
+ workPrefix: `${attemptPrefix}/work/`
122
+ }
123
+ };
124
+
125
+ const validation = await validateLaunchEnvelope({ packageRoot, runDir, envelope });
126
+ if (!validation.ok) {
127
+ throw new Error(validation.errors.join("\n"));
128
+ }
129
+
130
+ const envelopePath = join(runDir, envelope.launchEnvelopePath);
131
+ await mkdir(dirname(envelopePath), { recursive: true });
132
+ await mkdir(join(runDir, attemptPrefix, "work", "outputs"), { recursive: true });
133
+ envelope.launchEnvelopeSha256 = canonicalLaunchEnvelopeSha256(envelope);
134
+ await writeFile(envelopePath, stableJson(envelope));
135
+ return envelope;
136
+ }
137
+
138
+ export async function validateLaunchEnvelope({ packageRoot, runDir, envelope }) {
139
+ const errors = [];
140
+ errors.push(...requestedRuntimePolicyErrors(envelope.requestedRuntimePolicy));
141
+ assertSafePathSegment(errors, "stage.id", envelope.stage?.id);
142
+ assertSafePathSegment(errors, "attempt.id", envelope.attempt?.id);
143
+
144
+ for (const [label, path] of [
145
+ ["launchEnvelopePath", envelope.launchEnvelopePath],
146
+ ["packetPath", envelope.packetPath],
147
+ ["specPath", envelope.specPath],
148
+ ["gateSpecPath", envelope.gateSpecPath]
149
+ ]) {
150
+ assertSafePath(errors, label, path);
151
+ }
152
+
153
+ for (const [label, path] of [
154
+ ["packetPath", envelope.packetPath],
155
+ ["specPath", envelope.specPath],
156
+ ["gateSpecPath", envelope.gateSpecPath]
157
+ ]) {
158
+ if (path && !existsSync(join(packageRoot, path))) {
159
+ errors.push(`${label} does not exist: ${path}`);
160
+ }
161
+ }
162
+
163
+ const attemptPrefix = `stages/${envelope.stage?.id}/attempts/${envelope.attempt?.id}`;
164
+ const controlPrefix = `${attemptPrefix}/control/`;
165
+ const workPrefix = `${attemptPrefix}/work/`;
166
+ if (envelope.launchEnvelopePath !== `${controlPrefix}launch-envelope.json`) {
167
+ errors.push("launchEnvelopePath must be in the attempt control plane");
168
+ }
169
+
170
+ for (const input of envelope.resolvedInputs ?? []) {
171
+ const path = assertSafePath(errors, `resolvedInputs.${input.artifactId}`, input.path);
172
+ if (path && !path.includes("/work/outputs/")) {
173
+ errors.push(`resolved input must point at accepted work/outputs artifact: ${input.path}`);
174
+ }
175
+ if (path && runDir && !existsSync(join(runDir, path))) {
176
+ errors.push(`resolved input does not exist in run dir: ${input.path}`);
177
+ }
178
+ if (path && runDir && existsSync(join(runDir, path))) {
179
+ const actualSha256 = await sha256File(join(runDir, path));
180
+ if (input.sha256 !== actualSha256) {
181
+ errors.push(`resolved input sha256 does not match file contents: ${input.path}`);
182
+ }
183
+ }
184
+ }
185
+
186
+ for (const output of envelope.declaredOutputs ?? []) {
187
+ const path = assertSafePath(errors, `declaredOutputs.${output.artifactId}`, output.path);
188
+ if (path && !path.startsWith(`${workPrefix}outputs/`)) {
189
+ errors.push(`declared output must stay under current attempt work/outputs: ${output.path}`);
190
+ }
191
+ }
192
+
193
+ const uniqueActions = new Set(envelope.effectiveAllowedActions ?? []);
194
+ if (uniqueActions.size !== (envelope.effectiveAllowedActions ?? []).length) {
195
+ errors.push("effectiveAllowedActions must not contain duplicates");
196
+ }
197
+ if (
198
+ Object.hasOwn(envelope, "launchEnvelopeSha256") &&
199
+ envelope.launchEnvelopeSha256 !== canonicalLaunchEnvelopeSha256(envelope)
200
+ ) {
201
+ errors.push("launchEnvelopeSha256 does not match canonical envelope contents");
202
+ }
203
+
204
+ return { ok: errors.length === 0, errors };
205
+ }
206
+
207
+ function sameJson(left, right) {
208
+ return JSON.stringify(left) === JSON.stringify(right);
209
+ }
210
+
211
+ export function validateAgentReportAgainstEnvelope({ envelope, report }) {
212
+ const errors = [];
213
+ if (report.launchEnvelopeSha256 !== envelope.launchEnvelopeSha256) {
214
+ errors.push("agent report launchEnvelopeSha256 must match envelope");
215
+ }
216
+ if (!sameJson(report.inputArtifacts, envelope.resolvedInputs)) {
217
+ errors.push("agent report inputArtifacts must exactly match envelope resolvedInputs");
218
+ }
219
+ if (!sameJson(report.effectiveAllowedActions, envelope.effectiveAllowedActions)) {
220
+ errors.push("agent report effectiveAllowedActions must exactly match envelope effectiveAllowedActions");
221
+ }
222
+ return { ok: errors.length === 0, errors };
223
+ }
@@ -0,0 +1,137 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ function executorKey(executor) {
6
+ return `${executor.server}:${executor.tool}`;
7
+ }
8
+
9
+ function includesAll(values, required = []) {
10
+ const valueSet = new Set(values ?? []);
11
+ return required.every((value) => valueSet.has(value));
12
+ }
13
+
14
+ export function computeActionExecutionPlan({
15
+ actionRegistry,
16
+ requestedActions = Object.keys(actionRegistry.actions ?? {}),
17
+ availableExecutors = [],
18
+ grantedScopes = [],
19
+ satisfiedPreconditions = []
20
+ }) {
21
+ const availableExecutorKeys = new Set(availableExecutors.map(executorKey));
22
+ const executable = [];
23
+ const blockedActions = [];
24
+
25
+ for (const actionId of requestedActions) {
26
+ const action = actionRegistry.actions?.[actionId];
27
+ if (!action) {
28
+ blockedActions.push({ actionId, reason: "unknown-action" });
29
+ continue;
30
+ }
31
+ if (!availableExecutorKeys.has(executorKey(action.executor))) {
32
+ blockedActions.push({ actionId, reason: "executor-unavailable", executor: action.executor });
33
+ continue;
34
+ }
35
+ if (!includesAll(grantedScopes, action.requiredScopes)) {
36
+ const granted = new Set(grantedScopes);
37
+ blockedActions.push({
38
+ actionId,
39
+ reason: "missing-scopes",
40
+ missingScopes: (action.requiredScopes ?? []).filter((scope) => !granted.has(scope))
41
+ });
42
+ continue;
43
+ }
44
+ if (!includesAll(satisfiedPreconditions, action.requiredPreconditions)) {
45
+ const satisfied = new Set(satisfiedPreconditions);
46
+ blockedActions.push({
47
+ actionId,
48
+ reason: "missing-preconditions",
49
+ missingPreconditions: (action.requiredPreconditions ?? []).filter((precondition) => !satisfied.has(precondition))
50
+ });
51
+ continue;
52
+ }
53
+ executable.push(actionId);
54
+ }
55
+
56
+ return { executableActions: executable.sort(), blockedActions };
57
+ }
58
+
59
+ export function computeExecutableActions(options) {
60
+ return computeActionExecutionPlan(options).executableActions;
61
+ }
62
+
63
+ export function computeEffectiveAllowedActions({
64
+ capabilityCeiling = [],
65
+ stageRequestedActions = [],
66
+ workflowActionPolicy = [],
67
+ executableActions = []
68
+ }) {
69
+ const ceiling = new Set(capabilityCeiling);
70
+ const policy = new Set(workflowActionPolicy);
71
+ const executable = new Set(executableActions);
72
+ return [...new Set(stageRequestedActions)]
73
+ .filter((actionId) => ceiling.has(actionId))
74
+ .filter((actionId) => policy.has(actionId))
75
+ .filter((actionId) => executable.has(actionId))
76
+ .sort();
77
+ }
78
+
79
+ async function readJson(path) {
80
+ return JSON.parse(await readFile(path, "utf8"));
81
+ }
82
+
83
+ function hasUnsafePath(path) {
84
+ return !path || path.includes("..") || path.startsWith("/") || path.includes("\\");
85
+ }
86
+
87
+ export async function validateRegistryConsistency({
88
+ root,
89
+ workflowManifestPath,
90
+ capabilityRegistryPath,
91
+ actionRegistryPath,
92
+ requireWorkflowFiles = true
93
+ }) {
94
+ const errors = [];
95
+ const manifest = await readJson(workflowManifestPath);
96
+ const capabilityRegistry = await readJson(capabilityRegistryPath);
97
+ const actionRegistry = await readJson(actionRegistryPath);
98
+ const actionIds = new Set(Object.keys(actionRegistry.actions ?? {}));
99
+ const profileNames = new Set(Object.keys(capabilityRegistry.profiles ?? {}));
100
+
101
+ for (const stage of manifest.stages ?? []) {
102
+ for (const key of ["packet", "spec", "gateSpec"]) {
103
+ if (hasUnsafePath(stage[key])) {
104
+ errors.push(`${stage.id}.${key} is unsafe: ${stage[key]}`);
105
+ } else if (requireWorkflowFiles && !existsSync(join(root, stage[key]))) {
106
+ errors.push(`${stage.id}.${key} does not exist: ${stage[key]}`);
107
+ }
108
+ }
109
+ if (!profileNames.has(stage.runtimeAgentType)) {
110
+ errors.push(`${stage.id}.runtimeAgentType is not registered: ${stage.runtimeAgentType}`);
111
+ }
112
+ for (const forbiddenKey of ["inputs", "outputs", "artifactSchemas", "stopConditions"]) {
113
+ if (Object.hasOwn(stage, forbiddenKey)) {
114
+ errors.push(`${stage.id} manifest entry must not own ${forbiddenKey}`);
115
+ }
116
+ }
117
+ }
118
+
119
+ for (const [profileName, profile] of Object.entries(capabilityRegistry.profiles ?? {})) {
120
+ for (const actionId of profile.allowedActions ?? []) {
121
+ if (!actionIds.has(actionId)) {
122
+ errors.push(`${profileName} references unknown action ${actionId}`);
123
+ }
124
+ }
125
+ }
126
+
127
+ for (const [actionId, action] of Object.entries(actionRegistry.actions ?? {})) {
128
+ if (action.executor?.tool === actionId) {
129
+ errors.push(`${actionId} executor tool must not equal business action id`);
130
+ }
131
+ if (!Array.isArray(action.requiredScopes)) {
132
+ errors.push(`${actionId} requiredScopes must be an array`);
133
+ }
134
+ }
135
+
136
+ return { ok: errors.length === 0, errors };
137
+ }