runcastle 0.1.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/dist/add-PRGLHAVL.js +298 -0
- package/dist/add-PRGLHAVL.js.map +1 -0
- package/dist/chunk-44KDPUYJ.js +49 -0
- package/dist/chunk-44KDPUYJ.js.map +1 -0
- package/dist/chunk-5VPKMXSV.js +190 -0
- package/dist/chunk-5VPKMXSV.js.map +1 -0
- package/dist/chunk-6CWCR5SO.js +24 -0
- package/dist/chunk-6CWCR5SO.js.map +1 -0
- package/dist/chunk-AHRTPV2C.js +1082 -0
- package/dist/chunk-AHRTPV2C.js.map +1 -0
- package/dist/chunk-LP5MV76P.js +33 -0
- package/dist/chunk-LP5MV76P.js.map +1 -0
- package/dist/chunk-OJ356IMJ.js +136 -0
- package/dist/chunk-OJ356IMJ.js.map +1 -0
- package/dist/chunk-PRNBOQHW.js +32 -0
- package/dist/chunk-PRNBOQHW.js.map +1 -0
- package/dist/chunk-SNUH2UT6.js +95 -0
- package/dist/chunk-SNUH2UT6.js.map +1 -0
- package/dist/chunk-UCYVC3H6.js +108 -0
- package/dist/chunk-UCYVC3H6.js.map +1 -0
- package/dist/chunk-Y5P5BQWF.js +136 -0
- package/dist/chunk-Y5P5BQWF.js.map +1 -0
- package/dist/chunk-ZQ3BK4ZD.js +134 -0
- package/dist/chunk-ZQ3BK4ZD.js.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/info-U6AZZ2TV.js +78 -0
- package/dist/info-U6AZZ2TV.js.map +1 -0
- package/dist/init-MOJNLWXK.js +212 -0
- package/dist/init-MOJNLWXK.js.map +1 -0
- package/dist/logout-YPGZ6BB6.js +27 -0
- package/dist/logout-YPGZ6BB6.js.map +1 -0
- package/dist/publish-W37Y6TO6.js +211 -0
- package/dist/publish-W37Y6TO6.js.map +1 -0
- package/dist/search-2KNAXPM5.js +55 -0
- package/dist/search-2KNAXPM5.js.map +1 -0
- package/dist/update-NIIUPL2E.js +123 -0
- package/dist/update-NIIUPL2E.js.map +1 -0
- package/dist/validate-GFDSEVPV.js +12 -0
- package/dist/validate-GFDSEVPV.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
// ../schema/dist/index.js
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import semver from "semver";
|
|
4
|
+
import { z as z2 } from "zod";
|
|
5
|
+
import semver2 from "semver";
|
|
6
|
+
import {
|
|
7
|
+
Node,
|
|
8
|
+
SyntaxKind,
|
|
9
|
+
VariableDeclarationKind
|
|
10
|
+
} from "ts-morph";
|
|
11
|
+
import { Project } from "ts-morph";
|
|
12
|
+
import { Node as Node2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
13
|
+
import { Node as Node3, SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
14
|
+
import { Node as Node4, SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
15
|
+
import { z as z3 } from "zod";
|
|
16
|
+
var PRODUCT_NAME = "runcastle";
|
|
17
|
+
var DEFAULT_REGISTRY_URL = "https://runcastle.dev";
|
|
18
|
+
var SCHEMA_VERSION = 1;
|
|
19
|
+
var SCHEMA_ID = `${DEFAULT_REGISTRY_URL}/schema/v1.json`;
|
|
20
|
+
var PAYLOAD_DIR = ".sandcastle";
|
|
21
|
+
var DEFAULT_ENTRYPOINT = `${PAYLOAD_DIR}/main.ts`;
|
|
22
|
+
var SANDCASTLE_PACKAGE_NAME = "@ai-hero/sandcastle";
|
|
23
|
+
var MAX_PACKAGE_SIZE_BYTES = 2 * 1024 * 1024;
|
|
24
|
+
var MAX_FILE_COUNT = 100;
|
|
25
|
+
var MAX_FILE_SIZE_BYTES = 512 * 1024;
|
|
26
|
+
var MAX_DESCRIPTION_LENGTH = 280;
|
|
27
|
+
var MAX_TAGS = 8;
|
|
28
|
+
var MIN_README_LENGTH = 300;
|
|
29
|
+
var SLUG_REGEX = /^[a-z0-9-]{3,64}$/;
|
|
30
|
+
var ENV_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
31
|
+
var ALLOWED_EXTENSIONS = [
|
|
32
|
+
".ts",
|
|
33
|
+
".mts",
|
|
34
|
+
".md",
|
|
35
|
+
".json",
|
|
36
|
+
".yaml",
|
|
37
|
+
".yml",
|
|
38
|
+
".gitignore",
|
|
39
|
+
".env.example",
|
|
40
|
+
".sh"
|
|
41
|
+
];
|
|
42
|
+
var ALLOWED_FILENAMES = [
|
|
43
|
+
"Dockerfile",
|
|
44
|
+
"Containerfile",
|
|
45
|
+
".gitignore",
|
|
46
|
+
".env.example"
|
|
47
|
+
];
|
|
48
|
+
var WINDOWS_RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
49
|
+
"CON",
|
|
50
|
+
"PRN",
|
|
51
|
+
"AUX",
|
|
52
|
+
"NUL",
|
|
53
|
+
...Array.from({ length: 9 }, (_, i) => `COM${i + 1}`),
|
|
54
|
+
...Array.from({ length: 9 }, (_, i) => `LPT${i + 1}`)
|
|
55
|
+
]);
|
|
56
|
+
var SANDBOX_PROVIDERS = [
|
|
57
|
+
"docker",
|
|
58
|
+
"podman",
|
|
59
|
+
"vercel",
|
|
60
|
+
"custom"
|
|
61
|
+
];
|
|
62
|
+
var AGENT_PROVIDERS = [
|
|
63
|
+
"claude-code",
|
|
64
|
+
"codex",
|
|
65
|
+
"pi",
|
|
66
|
+
"opencode",
|
|
67
|
+
"cursor",
|
|
68
|
+
"copilot"
|
|
69
|
+
];
|
|
70
|
+
var AGENT_FACTORY_NAMES = [
|
|
71
|
+
"claudeCode",
|
|
72
|
+
"codex",
|
|
73
|
+
"pi",
|
|
74
|
+
"opencode"
|
|
75
|
+
];
|
|
76
|
+
var FACTORY_TO_PROVIDER = {
|
|
77
|
+
claudeCode: "claude-code",
|
|
78
|
+
codex: "codex",
|
|
79
|
+
pi: "pi",
|
|
80
|
+
opencode: "opencode"
|
|
81
|
+
};
|
|
82
|
+
var ALLOWED_IMPORT_ROOTS = [
|
|
83
|
+
SANDCASTLE_PACKAGE_NAME,
|
|
84
|
+
"zod"
|
|
85
|
+
];
|
|
86
|
+
var ErrorCode = {
|
|
87
|
+
E_SCHEMA_INVALID: "E_SCHEMA_INVALID",
|
|
88
|
+
E_MISSING_FILE: "E_MISSING_FILE",
|
|
89
|
+
E_UNLISTED_FILE: "E_UNLISTED_FILE",
|
|
90
|
+
E_UNSAFE_PATH: "E_UNSAFE_PATH",
|
|
91
|
+
E_ENV_FILE_PRESENT: "E_ENV_FILE_PRESENT",
|
|
92
|
+
E_SECRET_DETECTED: "E_SECRET_DETECTED",
|
|
93
|
+
E_ENV_MISMATCH: "E_ENV_MISMATCH",
|
|
94
|
+
E_HOOKS_MISMATCH: "E_HOOKS_MISMATCH",
|
|
95
|
+
E_HOOKS_NOT_STATIC: "E_HOOKS_NOT_STATIC",
|
|
96
|
+
E_FORBIDDEN_IMPORT: "E_FORBIDDEN_IMPORT",
|
|
97
|
+
E_UNDECLARED_SHELL_EXPANSION: "E_UNDECLARED_SHELL_EXPANSION",
|
|
98
|
+
E_PACKAGE_TOO_LARGE: "E_PACKAGE_TOO_LARGE",
|
|
99
|
+
E_TOO_MANY_FILES: "E_TOO_MANY_FILES",
|
|
100
|
+
E_FILE_TOO_LARGE: "E_FILE_TOO_LARGE",
|
|
101
|
+
E_BINARY_FILE: "E_BINARY_FILE",
|
|
102
|
+
E_FORBIDDEN_EXTENSION: "E_FORBIDDEN_EXTENSION"
|
|
103
|
+
};
|
|
104
|
+
var WarningCode = {
|
|
105
|
+
W_VERSION_RANGE_STALE: "W_VERSION_RANGE_STALE",
|
|
106
|
+
W_NO_TOPOLOGY: "W_NO_TOPOLOGY",
|
|
107
|
+
W_SHORT_README: "W_SHORT_README",
|
|
108
|
+
W_NAMESPACE_UNSAFE_PATHS: "W_NAMESPACE_UNSAFE_PATHS"
|
|
109
|
+
};
|
|
110
|
+
var semverString = z.string().refine((v) => semver.valid(v, { loose: false }) !== null, {
|
|
111
|
+
message: "must be a strict semver version (e.g. 1.2.3)"
|
|
112
|
+
});
|
|
113
|
+
var semverRange = z.string().refine((v) => semver.validRange(v, { loose: false }) !== null, {
|
|
114
|
+
message: "must be a valid semver range (e.g. >=0.12.0 <0.13.0)"
|
|
115
|
+
});
|
|
116
|
+
var AgentSchema = z.object({
|
|
117
|
+
role: z.string().min(1).describe("The agent's role name as used in the orchestration code (e.g. planner, implementer, reviewer)."),
|
|
118
|
+
provider: z.enum(AGENT_PROVIDERS).describe("Agent factory / provider used for this role."),
|
|
119
|
+
model: z.string().min(1).describe("Model identifier passed to the provider (e.g. claude-opus-4-8)."),
|
|
120
|
+
parallel: z.boolean().optional().describe("Whether this role fans out into parallel instances.")
|
|
121
|
+
});
|
|
122
|
+
var SandcastleCompatSchema = z.object({
|
|
123
|
+
packageName: z.string().min(1).default(SANDCASTLE_PACKAGE_NAME).describe("The orchestration package. Defaults to @ai-hero/sandcastle."),
|
|
124
|
+
versionRange: semverRange.describe(
|
|
125
|
+
"Semver range of Sandcastle versions the workflow is tested against (e.g. >=0.12.0 <0.13.0)."
|
|
126
|
+
),
|
|
127
|
+
entrypoint: z.string().min(1).default(DEFAULT_ENTRYPOINT).describe("Path to the orchestration entrypoint that calls run(). Defaults to .sandcastle/main.ts.")
|
|
128
|
+
});
|
|
129
|
+
var EnvVarSchema = z.object({
|
|
130
|
+
name: z.string().regex(ENV_NAME_REGEX, "invalid environment variable name").describe("Environment variable name (shell-safe identifier)."),
|
|
131
|
+
required: z.boolean().describe("Whether the workflow fails without this variable set."),
|
|
132
|
+
description: z.string().optional().describe("What the variable is for \u2014 shown to installers.")
|
|
133
|
+
});
|
|
134
|
+
var DisclosuresSchema = z.object({
|
|
135
|
+
hostHooks: z.array(z.string()).default([]).describe("Every host.* hook command run on the consumer's machine, verbatim as string literals."),
|
|
136
|
+
sandboxHooks: z.array(z.string()).default([]).describe("Every sandbox.* hook command run inside the sandbox, verbatim as string literals."),
|
|
137
|
+
networkAccess: z.string().optional().describe("Free-text note describing what the workflow accesses over the network."),
|
|
138
|
+
usesShellExpansion: z.boolean().default(false).describe("Whether any prompt file contains !`command` shell-expansion blocks.")
|
|
139
|
+
});
|
|
140
|
+
var TopologyNodeSchema = z.object({
|
|
141
|
+
id: z.string().min(1).describe("Unique node identifier, referenced by edges."),
|
|
142
|
+
role: z.string().min(1).nullable().describe("The agent role this node represents, or null for non-agent nodes (e.g. a git step)."),
|
|
143
|
+
label: z.string().min(1).describe("Display label rendered in the diagram."),
|
|
144
|
+
kind: z.string().min(1).optional().describe('Optional node flavor (e.g. "git") \u2014 non-agent nodes get a muted rendering.'),
|
|
145
|
+
fanOut: z.boolean().optional().describe("Marks a node that fans out into parallel instances \u2014 rendered as a stacked card.")
|
|
146
|
+
});
|
|
147
|
+
var TopologyEdgeSchema = z.object({
|
|
148
|
+
from: z.string().min(1).describe("Source node id."),
|
|
149
|
+
to: z.string().min(1).describe("Target node id."),
|
|
150
|
+
label: z.string().optional().describe("Optional edge label (e.g. a condition)."),
|
|
151
|
+
loop: z.boolean().optional().describe("Marks a feedback edge (e.g. reviewer sending work back) \u2014 rendered dashed.")
|
|
152
|
+
});
|
|
153
|
+
var TopologySchema = z.object({
|
|
154
|
+
nodes: z.array(TopologyNodeSchema).describe("Graph nodes \u2014 one per agent role or auxiliary step."),
|
|
155
|
+
edges: z.array(TopologyEdgeSchema).describe("Directed edges between node ids.")
|
|
156
|
+
});
|
|
157
|
+
var ManifestSchema = z.object({
|
|
158
|
+
$schema: z.string().optional().describe("Optional JSON Schema URL so editors provide autocomplete and inline validation."),
|
|
159
|
+
schemaVersion: z.literal(SCHEMA_VERSION).describe("Manifest format version. Always 1 for now."),
|
|
160
|
+
// Identity
|
|
161
|
+
name: z.string().min(1).max(120).describe("Human-readable display name shown on cards and the detail page."),
|
|
162
|
+
slug: z.string().regex(SLUG_REGEX, "slug must match [a-z0-9-]{3,64}").describe("URL-safe identifier [a-z0-9-]{3,64}, unique per author; forms the install handle author/slug."),
|
|
163
|
+
version: semverString.describe(
|
|
164
|
+
"Strict semver (e.g. 1.2.0). Versions are immutable \u2014 republishing a version is rejected with 409."
|
|
165
|
+
),
|
|
166
|
+
description: z.string().min(1).max(MAX_DESCRIPTION_LENGTH).describe(`One-paragraph summary (max ${MAX_DESCRIPTION_LENGTH} chars) shown on cards and in search.`),
|
|
167
|
+
license: z.string().min(1).describe("SPDX license identifier for the package contents (e.g. MIT)."),
|
|
168
|
+
tags: z.array(z.string().min(1)).max(MAX_TAGS).default([]).describe(`Up to ${MAX_TAGS} tags from an open vocabulary, used for browse filters and search.`),
|
|
169
|
+
// Compatibility
|
|
170
|
+
sandcastle: SandcastleCompatSchema.describe(
|
|
171
|
+
"Compatibility block describing the Sandcastle runtime the workflow targets."
|
|
172
|
+
),
|
|
173
|
+
// Declared runtime metadata
|
|
174
|
+
agents: z.array(AgentSchema).default([]).describe("The agents the workflow orchestrates \u2014 declared metadata cross-checked against the entrypoint."),
|
|
175
|
+
sandboxProviders: z.array(z.enum(SANDBOX_PROVIDERS)).default([]).describe("Sandbox environments the workflow supports (subset of docker | podman | vercel | custom)."),
|
|
176
|
+
// Environment
|
|
177
|
+
env: z.array(EnvVarSchema).default([]).describe("Environment variables the user must provide; cross-checked against .env.example."),
|
|
178
|
+
// Disclosures
|
|
179
|
+
disclosures: DisclosuresSchema.describe(
|
|
180
|
+
"The declared side-effect surface (hooks, network, shell expansion). Mismatches with detected behavior block publish."
|
|
181
|
+
),
|
|
182
|
+
// Topology (optional — absence produces a warning)
|
|
183
|
+
topology: TopologySchema.optional().describe(
|
|
184
|
+
"The node/edge graph describing the orchestration shape. Optional \u2014 drives the site diagram, cards, and OG images."
|
|
185
|
+
),
|
|
186
|
+
// Files to install
|
|
187
|
+
files: z.array(z.string().min(1)).min(1).max(MAX_FILE_COUNT).describe("Exhaustive list of files the package installs, relative to the package root (all under .sandcastle/)."),
|
|
188
|
+
// Optional metadata
|
|
189
|
+
repositoryDirectory: z.string().optional().describe("Directory within the repository containing the package, when it isn't at the repo root."),
|
|
190
|
+
// http(s) only — package content is hostile input (§8.6); a javascript: URL
|
|
191
|
+
// here would become a live link on the workflow page.
|
|
192
|
+
homepage: z.string().url().refine((u) => /^https?:\/\//i.test(u), {
|
|
193
|
+
message: "homepage must be an http(s) URL"
|
|
194
|
+
}).optional().describe("Optional project homepage URL (http(s) only)."),
|
|
195
|
+
postInstallNotes: z.string().optional().describe("Plain text printed by the CLI after a successful install (printed, never executed).")
|
|
196
|
+
});
|
|
197
|
+
function parseManifest(value) {
|
|
198
|
+
const result = ManifestSchema.safeParse(value);
|
|
199
|
+
if (result.success) return { ok: true, manifest: result.data };
|
|
200
|
+
return { ok: false, error: result.error };
|
|
201
|
+
}
|
|
202
|
+
function manifestJsonSchema() {
|
|
203
|
+
const schema = z2.toJSONSchema(ManifestSchema, {
|
|
204
|
+
io: "input",
|
|
205
|
+
target: "draft-2020-12",
|
|
206
|
+
unrepresentable: "any"
|
|
207
|
+
});
|
|
208
|
+
schema.$id = SCHEMA_ID;
|
|
209
|
+
schema.title = "Runcastle workflow.json";
|
|
210
|
+
schema.description = "Manifest for a Runcastle workflow package (Sandcastle orchestration).";
|
|
211
|
+
return schema;
|
|
212
|
+
}
|
|
213
|
+
var MANIFEST_JSON_SCHEMA = manifestJsonSchema();
|
|
214
|
+
var SECRET_RULES = [
|
|
215
|
+
{
|
|
216
|
+
id: "anthropic-api-key",
|
|
217
|
+
description: "Anthropic API key (sk-ant-)",
|
|
218
|
+
pattern: /sk-ant-[A-Za-z0-9_-]{16,}/g
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "openai-project-key",
|
|
222
|
+
description: "OpenAI project key (sk-proj-)",
|
|
223
|
+
pattern: /sk-proj-[A-Za-z0-9_-]{16,}/g
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "openai-api-key",
|
|
227
|
+
description: "OpenAI API key (sk-)",
|
|
228
|
+
// Classic sk- keys: sk- followed by >=20 base62 chars, not preceded by a
|
|
229
|
+
// word char and not one of the more specific sk-ant-/sk-proj- prefixes.
|
|
230
|
+
pattern: /(?<![A-Za-z0-9_-])sk-(?!ant-|proj-)[A-Za-z0-9]{20,}/g
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "github-pat-classic",
|
|
234
|
+
description: "GitHub personal access token (ghp_)",
|
|
235
|
+
pattern: /ghp_[A-Za-z0-9]{36,}/g
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "github-pat-fine-grained",
|
|
239
|
+
description: "GitHub fine-grained PAT (github_pat_)",
|
|
240
|
+
pattern: /github_pat_[A-Za-z0-9_]{22,}/g
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "github-oauth-token",
|
|
244
|
+
description: "GitHub OAuth token (gho_)",
|
|
245
|
+
pattern: /gho_[A-Za-z0-9]{36,}/g
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "aws-access-key-id",
|
|
249
|
+
description: "AWS access key id (AKIA/ASIA)",
|
|
250
|
+
pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: "aws-secret-access-key",
|
|
254
|
+
description: "AWS secret access key (contextual)",
|
|
255
|
+
// Require an aws-secret context word near a 40-char base64 secret to keep
|
|
256
|
+
// false positives on arbitrary hashes/blobs low.
|
|
257
|
+
pattern: /aws.{0,20}?(?:secret|sak).{0,20}?['"]?([A-Za-z0-9/+=]{40})['"]?/gis
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: "private-key-block",
|
|
261
|
+
description: "PEM PRIVATE KEY block",
|
|
262
|
+
pattern: /-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----/g
|
|
263
|
+
}
|
|
264
|
+
];
|
|
265
|
+
function scanForSecrets(content) {
|
|
266
|
+
const findings = [];
|
|
267
|
+
for (const rule of SECRET_RULES) {
|
|
268
|
+
rule.pattern.lastIndex = 0;
|
|
269
|
+
let m;
|
|
270
|
+
while ((m = rule.pattern.exec(content)) !== null) {
|
|
271
|
+
findings.push({
|
|
272
|
+
ruleId: rule.id,
|
|
273
|
+
description: rule.description,
|
|
274
|
+
match: m[0],
|
|
275
|
+
index: m.index
|
|
276
|
+
});
|
|
277
|
+
if (m.index === rule.pattern.lastIndex) rule.pattern.lastIndex++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return findings;
|
|
281
|
+
}
|
|
282
|
+
function createSourceFile(content) {
|
|
283
|
+
const project = new Project({
|
|
284
|
+
useInMemoryFileSystem: true,
|
|
285
|
+
skipAddingFilesFromTsConfig: true,
|
|
286
|
+
skipFileDependencyResolution: true,
|
|
287
|
+
skipLoadingLibFiles: true,
|
|
288
|
+
compilerOptions: {
|
|
289
|
+
allowJs: true
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
return project.createSourceFile("entrypoint.mts", content, {
|
|
293
|
+
overwrite: true
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function readStringLiteral(node) {
|
|
297
|
+
if (!node) return void 0;
|
|
298
|
+
if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
299
|
+
return node.getLiteralText();
|
|
300
|
+
}
|
|
301
|
+
return void 0;
|
|
302
|
+
}
|
|
303
|
+
function extractCommand(entry, out, reasons) {
|
|
304
|
+
if (!Node.isObjectLiteralExpression(entry)) {
|
|
305
|
+
reasons.push("hook entry is not an object literal");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const commandProp = entry.getProperty("command");
|
|
309
|
+
if (!commandProp || !Node.isPropertyAssignment(commandProp)) {
|
|
310
|
+
reasons.push("hook entry has no static `command` property");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const value = readStringLiteral(commandProp.getInitializer());
|
|
314
|
+
if (value === void 0) {
|
|
315
|
+
reasons.push("hook `command` is not a string literal");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
out.push(value);
|
|
319
|
+
}
|
|
320
|
+
function extractGroup(groupValue, out, reasons) {
|
|
321
|
+
if (!groupValue) return;
|
|
322
|
+
if (!Node.isObjectLiteralExpression(groupValue)) {
|
|
323
|
+
reasons.push("hook group is not an object literal");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
for (const prop of groupValue.getProperties()) {
|
|
327
|
+
if (!Node.isPropertyAssignment(prop)) {
|
|
328
|
+
reasons.push("hook event is not a static property");
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const arr = prop.getInitializer();
|
|
332
|
+
if (!arr || !Node.isArrayLiteralExpression(arr)) {
|
|
333
|
+
reasons.push("hook event value is not an array literal");
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
for (const el of arr.getElements()) {
|
|
337
|
+
if (Node.isSpreadElement(el)) {
|
|
338
|
+
reasons.push("hook array uses a spread element");
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
extractCommand(el, out, reasons);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function extractHooksObject(hooksObject, hostOut, sandboxOut, reasons) {
|
|
346
|
+
const host = hooksObject.getProperty("host");
|
|
347
|
+
if (host) {
|
|
348
|
+
if (Node.isPropertyAssignment(host)) {
|
|
349
|
+
extractGroup(host.getInitializer(), hostOut, reasons);
|
|
350
|
+
} else {
|
|
351
|
+
reasons.push("`host` hooks are not a static property");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const sandbox = hooksObject.getProperty("sandbox");
|
|
355
|
+
if (sandbox) {
|
|
356
|
+
if (Node.isPropertyAssignment(sandbox)) {
|
|
357
|
+
extractGroup(sandbox.getInitializer(), sandboxOut, reasons);
|
|
358
|
+
} else {
|
|
359
|
+
reasons.push("`sandbox` hooks are not a static property");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function resolveObjectLiteral(node, source) {
|
|
364
|
+
let current = node;
|
|
365
|
+
while (current && (Node.isAsExpression(current) || Node.isSatisfiesExpression(current) || Node.isParenthesizedExpression(current))) {
|
|
366
|
+
current = current.getExpression();
|
|
367
|
+
}
|
|
368
|
+
if (!current) return void 0;
|
|
369
|
+
if (Node.isObjectLiteralExpression(current)) return current;
|
|
370
|
+
if (Node.isIdentifier(current)) {
|
|
371
|
+
const name = current.getText();
|
|
372
|
+
for (const stmt of source.getVariableStatements()) {
|
|
373
|
+
if (stmt.getDeclarationKind() !== VariableDeclarationKind.Const) continue;
|
|
374
|
+
for (const decl of stmt.getDeclarations()) {
|
|
375
|
+
if (decl.getName() !== name) continue;
|
|
376
|
+
const init = decl.getInitializer();
|
|
377
|
+
if (!init) return void 0;
|
|
378
|
+
let unwrapped = init;
|
|
379
|
+
while (Node.isAsExpression(unwrapped) || Node.isSatisfiesExpression(unwrapped) || Node.isParenthesizedExpression(unwrapped)) {
|
|
380
|
+
unwrapped = unwrapped.getExpression();
|
|
381
|
+
}
|
|
382
|
+
return Node.isObjectLiteralExpression(unwrapped) ? unwrapped : void 0;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return void 0;
|
|
387
|
+
}
|
|
388
|
+
function extractHooks(entrypointContent) {
|
|
389
|
+
const source = createSourceFile(entrypointContent);
|
|
390
|
+
const hostCommands = [];
|
|
391
|
+
const sandboxCommands = [];
|
|
392
|
+
const dynamicReasons = [];
|
|
393
|
+
const objects = source.getDescendantsOfKind(
|
|
394
|
+
SyntaxKind.ObjectLiteralExpression
|
|
395
|
+
);
|
|
396
|
+
for (const obj of objects) {
|
|
397
|
+
const hooksProp = obj.getProperty("hooks");
|
|
398
|
+
if (!hooksProp) continue;
|
|
399
|
+
let value;
|
|
400
|
+
if (Node.isPropertyAssignment(hooksProp)) {
|
|
401
|
+
value = hooksProp.getInitializer();
|
|
402
|
+
} else if (Node.isShorthandPropertyAssignment(hooksProp)) {
|
|
403
|
+
value = hooksProp.getNameNode();
|
|
404
|
+
} else {
|
|
405
|
+
dynamicReasons.push("`hooks` is not a static property assignment");
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const literal2 = value ? resolveObjectLiteral(value, source) : void 0;
|
|
409
|
+
if (!literal2) {
|
|
410
|
+
dynamicReasons.push(
|
|
411
|
+
"`hooks` value is not an object literal (or a same-file `const` object literal)"
|
|
412
|
+
);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
extractHooksObject(literal2, hostCommands, sandboxCommands, dynamicReasons);
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
static: dynamicReasons.length === 0,
|
|
419
|
+
hostCommands,
|
|
420
|
+
sandboxCommands,
|
|
421
|
+
dynamicReasons
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function isAllowedImport(specifier) {
|
|
425
|
+
if (specifier.startsWith("node:")) return true;
|
|
426
|
+
for (const root of ALLOWED_IMPORT_ROOTS) {
|
|
427
|
+
if (specifier === root || specifier.startsWith(`${root}/`)) return true;
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
function analyzeImports(entrypointContent) {
|
|
432
|
+
const source = createSourceFile(entrypointContent);
|
|
433
|
+
const specifiers = [];
|
|
434
|
+
for (const decl of source.getImportDeclarations()) {
|
|
435
|
+
specifiers.push(decl.getModuleSpecifierValue());
|
|
436
|
+
}
|
|
437
|
+
for (const decl of source.getExportDeclarations()) {
|
|
438
|
+
const value = decl.getModuleSpecifierValue();
|
|
439
|
+
if (value) specifiers.push(value);
|
|
440
|
+
}
|
|
441
|
+
for (const call of source.getDescendantsOfKind(SyntaxKind2.CallExpression)) {
|
|
442
|
+
const expr = call.getExpression();
|
|
443
|
+
const isRequire = Node2.isIdentifier(expr) && expr.getText() === "require";
|
|
444
|
+
const isDynamicImport = expr.getKind() === SyntaxKind2.ImportKeyword;
|
|
445
|
+
if (!isRequire && !isDynamicImport) continue;
|
|
446
|
+
const arg = call.getArguments()[0];
|
|
447
|
+
if (arg && (Node2.isStringLiteral(arg) || Node2.isNoSubstitutionTemplateLiteral(arg))) {
|
|
448
|
+
specifiers.push(arg.getLiteralText());
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return specifiers.map((specifier) => ({
|
|
452
|
+
specifier,
|
|
453
|
+
allowed: isAllowedImport(specifier)
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
function forbiddenImports(entrypointContent) {
|
|
457
|
+
const seen = /* @__PURE__ */ new Set();
|
|
458
|
+
const out = [];
|
|
459
|
+
for (const ref of analyzeImports(entrypointContent)) {
|
|
460
|
+
if (!ref.allowed && !seen.has(ref.specifier)) {
|
|
461
|
+
seen.add(ref.specifier);
|
|
462
|
+
out.push(ref.specifier);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
var SHELL_EXPANSION_RE = /!`([^`]+)`/g;
|
|
468
|
+
function detectShellExpansions(content) {
|
|
469
|
+
const out = [];
|
|
470
|
+
SHELL_EXPANSION_RE.lastIndex = 0;
|
|
471
|
+
let m;
|
|
472
|
+
while ((m = SHELL_EXPANSION_RE.exec(content)) !== null) {
|
|
473
|
+
out.push({ command: m[1] ?? "", index: m.index });
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
function literal(node) {
|
|
478
|
+
if (!node) return void 0;
|
|
479
|
+
if (Node3.isStringLiteral(node) || Node3.isNoSubstitutionTemplateLiteral(node)) {
|
|
480
|
+
return node.getLiteralText();
|
|
481
|
+
}
|
|
482
|
+
return void 0;
|
|
483
|
+
}
|
|
484
|
+
function findNamespaceUnsafePaths(entrypointContent) {
|
|
485
|
+
const source = createSourceFile(entrypointContent);
|
|
486
|
+
const out = [];
|
|
487
|
+
const prefix = `${PAYLOAD_DIR}/`;
|
|
488
|
+
for (const prop of source.getDescendantsOfKind(
|
|
489
|
+
SyntaxKind3.PropertyAssignment
|
|
490
|
+
)) {
|
|
491
|
+
if (prop.getName() !== "promptFile") continue;
|
|
492
|
+
const value = literal(prop.getInitializer());
|
|
493
|
+
if (value === void 0) continue;
|
|
494
|
+
const normalized = value.replace(/^\.\//, "");
|
|
495
|
+
if (normalized.startsWith(prefix)) {
|
|
496
|
+
out.push({ path: value });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
501
|
+
function normalizePath(p) {
|
|
502
|
+
return p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/{2,}/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
503
|
+
}
|
|
504
|
+
function basename(p) {
|
|
505
|
+
const norm = normalizePath(p);
|
|
506
|
+
const idx = norm.lastIndexOf("/");
|
|
507
|
+
return idx === -1 ? norm : norm.slice(idx + 1);
|
|
508
|
+
}
|
|
509
|
+
var PURE_EXTENSIONS = ALLOWED_EXTENSIONS.filter(
|
|
510
|
+
(e) => !ALLOWED_FILENAMES.includes(e)
|
|
511
|
+
);
|
|
512
|
+
function extensionAllowed(path) {
|
|
513
|
+
const base = basename(path);
|
|
514
|
+
if (ALLOWED_FILENAMES.includes(base)) return true;
|
|
515
|
+
return PURE_EXTENSIONS.some((ext) => base.endsWith(ext));
|
|
516
|
+
}
|
|
517
|
+
var PAYLOAD_PREFIX = `${PAYLOAD_DIR}/`;
|
|
518
|
+
function inPayload(path) {
|
|
519
|
+
return path === PAYLOAD_DIR || path.startsWith(PAYLOAD_PREFIX);
|
|
520
|
+
}
|
|
521
|
+
function unsafePathReason(raw) {
|
|
522
|
+
if (typeof raw !== "string" || raw.trim() === "") return "path is empty";
|
|
523
|
+
for (let i = 0; i < raw.length; i++) {
|
|
524
|
+
const code = raw.charCodeAt(i);
|
|
525
|
+
if (code < 32 || code === 127) return "path contains control characters";
|
|
526
|
+
}
|
|
527
|
+
if (raw.includes("\\")) return "path contains a backslash";
|
|
528
|
+
if (raw.includes(":")) {
|
|
529
|
+
return "path contains a colon (drive letter or NTFS alternate data stream)";
|
|
530
|
+
}
|
|
531
|
+
if (raw.startsWith("/")) return "path is absolute";
|
|
532
|
+
const norm = normalizePath(raw);
|
|
533
|
+
if (norm === "") return "path is empty";
|
|
534
|
+
for (const seg of norm.split("/")) {
|
|
535
|
+
if (seg === "..") return "path contains a '..' traversal segment";
|
|
536
|
+
const stem = seg.split(".")[0].toUpperCase();
|
|
537
|
+
if (WINDOWS_RESERVED_NAMES.has(stem)) {
|
|
538
|
+
return `path uses the Windows reserved name "${seg}"`;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!inPayload(norm)) {
|
|
542
|
+
return `path must live under ${PAYLOAD_DIR}/`;
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
547
|
+
function decodeFile(file) {
|
|
548
|
+
if (typeof file.content === "string") {
|
|
549
|
+
return {
|
|
550
|
+
text: file.content,
|
|
551
|
+
isBinary: file.content.includes("\0")
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
const bytes = file.content;
|
|
555
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
556
|
+
if (bytes[i] === 0) return { text: null, isBinary: true };
|
|
557
|
+
}
|
|
558
|
+
const text = utf8Decoder.decode(bytes);
|
|
559
|
+
const isBinary = text.includes("\uFFFD");
|
|
560
|
+
return { text: isBinary ? null : text, isBinary };
|
|
561
|
+
}
|
|
562
|
+
function parseEnvExampleKeys(content) {
|
|
563
|
+
const keys = /* @__PURE__ */ new Set();
|
|
564
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
565
|
+
const line = rawLine.trim();
|
|
566
|
+
if (!line || line.startsWith("#")) continue;
|
|
567
|
+
const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
|
|
568
|
+
if (m?.[1]) keys.add(m[1]);
|
|
569
|
+
}
|
|
570
|
+
return keys;
|
|
571
|
+
}
|
|
572
|
+
var Report = class {
|
|
573
|
+
errors = [];
|
|
574
|
+
warnings = [];
|
|
575
|
+
error(code, message, file) {
|
|
576
|
+
this.errors.push({ code, message, ...file ? { file } : {} });
|
|
577
|
+
}
|
|
578
|
+
warn(code, message, file) {
|
|
579
|
+
this.warnings.push({ code, message, ...file ? { file } : {} });
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
function validatePackage(pkg, options = {}) {
|
|
583
|
+
const report = new Report();
|
|
584
|
+
const files = /* @__PURE__ */ new Map();
|
|
585
|
+
for (const [rawPath, file] of pkg.files) {
|
|
586
|
+
files.set(normalizePath(rawPath), file);
|
|
587
|
+
}
|
|
588
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
589
|
+
const textOf = (path) => {
|
|
590
|
+
if (!decoded.has(path)) {
|
|
591
|
+
const f = files.get(path);
|
|
592
|
+
decoded.set(path, f ? decodeFile(f) : { text: null, isBinary: false });
|
|
593
|
+
}
|
|
594
|
+
return decoded.get(path).text;
|
|
595
|
+
};
|
|
596
|
+
runStructuralChecks(files, decoded, report);
|
|
597
|
+
const manifestFile = files.get("workflow.json");
|
|
598
|
+
if (!manifestFile) {
|
|
599
|
+
report.error(
|
|
600
|
+
ErrorCode.E_MISSING_FILE,
|
|
601
|
+
"workflow.json is required at the package root",
|
|
602
|
+
"workflow.json"
|
|
603
|
+
);
|
|
604
|
+
return finalize(report);
|
|
605
|
+
}
|
|
606
|
+
const manifestText = textOf("workflow.json");
|
|
607
|
+
let manifest;
|
|
608
|
+
if (manifestText === null) {
|
|
609
|
+
report.error(
|
|
610
|
+
ErrorCode.E_SCHEMA_INVALID,
|
|
611
|
+
"workflow.json is not readable UTF-8 text",
|
|
612
|
+
"workflow.json"
|
|
613
|
+
);
|
|
614
|
+
} else {
|
|
615
|
+
let json;
|
|
616
|
+
try {
|
|
617
|
+
json = JSON.parse(manifestText);
|
|
618
|
+
} catch (e) {
|
|
619
|
+
report.error(
|
|
620
|
+
ErrorCode.E_SCHEMA_INVALID,
|
|
621
|
+
`workflow.json is not valid JSON: ${e.message}`,
|
|
622
|
+
"workflow.json"
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if (json !== void 0) {
|
|
626
|
+
const parsed = parseManifest(json);
|
|
627
|
+
if (parsed.ok) {
|
|
628
|
+
manifest = parsed.manifest;
|
|
629
|
+
} else {
|
|
630
|
+
for (const issue of parsed.error.issues) {
|
|
631
|
+
const path = issue.path.join(".");
|
|
632
|
+
report.error(
|
|
633
|
+
ErrorCode.E_SCHEMA_INVALID,
|
|
634
|
+
`${path ? `${path}: ` : ""}${issue.message}`,
|
|
635
|
+
"workflow.json"
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const readmeText = textOf("README.md");
|
|
642
|
+
if (readmeText === null && !files.has("README.md")) {
|
|
643
|
+
report.error(
|
|
644
|
+
ErrorCode.E_MISSING_FILE,
|
|
645
|
+
"README.md is required at the package root",
|
|
646
|
+
"README.md"
|
|
647
|
+
);
|
|
648
|
+
} else if (readmeText !== null && readmeText.trim().length < MIN_README_LENGTH) {
|
|
649
|
+
report.warn(
|
|
650
|
+
WarningCode.W_SHORT_README,
|
|
651
|
+
`README is shorter than ${MIN_README_LENGTH} characters`,
|
|
652
|
+
"README.md"
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
if (manifest) {
|
|
656
|
+
runManifestChecks(manifest, files, textOf, report, options);
|
|
657
|
+
}
|
|
658
|
+
return finalize(report);
|
|
659
|
+
}
|
|
660
|
+
function finalize(report) {
|
|
661
|
+
return {
|
|
662
|
+
ok: report.errors.length === 0,
|
|
663
|
+
errors: report.errors,
|
|
664
|
+
warnings: report.warnings
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function isRealEnvFile(path) {
|
|
668
|
+
const base = basename(path);
|
|
669
|
+
return base === ".env" || base.startsWith(".env.") && base !== ".env.example";
|
|
670
|
+
}
|
|
671
|
+
function runStructuralChecks(files, decoded, report) {
|
|
672
|
+
if (files.size > MAX_FILE_COUNT) {
|
|
673
|
+
report.error(
|
|
674
|
+
ErrorCode.E_TOO_MANY_FILES,
|
|
675
|
+
`package has ${files.size} files (max ${MAX_FILE_COUNT})`
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
let totalBytes = 0;
|
|
679
|
+
for (const [path, file] of files) {
|
|
680
|
+
totalBytes += file.sizeBytes;
|
|
681
|
+
if (isRealEnvFile(path)) {
|
|
682
|
+
report.error(
|
|
683
|
+
ErrorCode.E_ENV_FILE_PRESENT,
|
|
684
|
+
"a real .env file must not be included in the package",
|
|
685
|
+
path
|
|
686
|
+
);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (file.sizeBytes > MAX_FILE_SIZE_BYTES) {
|
|
690
|
+
report.error(
|
|
691
|
+
ErrorCode.E_FILE_TOO_LARGE,
|
|
692
|
+
`file is ${file.sizeBytes} bytes (max ${MAX_FILE_SIZE_BYTES})`,
|
|
693
|
+
path
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
if (!decoded.has(path)) decoded.set(path, decodeFile(file));
|
|
697
|
+
const dec = decoded.get(path);
|
|
698
|
+
if (dec.isBinary) {
|
|
699
|
+
report.error(
|
|
700
|
+
ErrorCode.E_BINARY_FILE,
|
|
701
|
+
"binary files are not allowed; text files only",
|
|
702
|
+
path
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
if (!extensionAllowed(path)) {
|
|
706
|
+
report.error(
|
|
707
|
+
ErrorCode.E_FORBIDDEN_EXTENSION,
|
|
708
|
+
`file extension is not on the allowlist (${basename(path)})`,
|
|
709
|
+
path
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
if (!dec.isBinary && dec.text !== null) {
|
|
713
|
+
const findings = scanForSecrets(dec.text);
|
|
714
|
+
if (findings.length > 0) {
|
|
715
|
+
const rules = [...new Set(findings.map((f) => f.description))].join(", ");
|
|
716
|
+
report.error(
|
|
717
|
+
ErrorCode.E_SECRET_DETECTED,
|
|
718
|
+
`possible secret detected (${rules})`,
|
|
719
|
+
path
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (totalBytes > MAX_PACKAGE_SIZE_BYTES) {
|
|
725
|
+
report.error(
|
|
726
|
+
ErrorCode.E_PACKAGE_TOO_LARGE,
|
|
727
|
+
`package is ${totalBytes} bytes (max ${MAX_PACKAGE_SIZE_BYTES})`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function runManifestChecks(manifest, files, textOf, report, options) {
|
|
732
|
+
checkSafePaths(manifest, report);
|
|
733
|
+
const listed = new Set(manifest.files.map(normalizePath));
|
|
734
|
+
for (const path of listed) {
|
|
735
|
+
if (!files.has(path)) {
|
|
736
|
+
report.error(
|
|
737
|
+
ErrorCode.E_MISSING_FILE,
|
|
738
|
+
"file listed in manifest.files is missing from the package",
|
|
739
|
+
path
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
for (const path of files.keys()) {
|
|
744
|
+
if (inPayload(path) && !listed.has(path) && !isRealEnvFile(path)) {
|
|
745
|
+
report.error(
|
|
746
|
+
ErrorCode.E_UNLISTED_FILE,
|
|
747
|
+
"package file under .sandcastle/ is not listed in manifest.files",
|
|
748
|
+
path
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const entrypoint = normalizePath(
|
|
753
|
+
manifest.sandcastle.entrypoint || DEFAULT_ENTRYPOINT
|
|
754
|
+
);
|
|
755
|
+
const entrypointText = files.has(entrypoint) ? textOf(entrypoint) : null;
|
|
756
|
+
if (!files.has(entrypoint)) {
|
|
757
|
+
report.error(
|
|
758
|
+
ErrorCode.E_MISSING_FILE,
|
|
759
|
+
"the manifest entrypoint is missing from the package",
|
|
760
|
+
entrypoint
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
checkEnvConsistency(manifest, files, textOf, report);
|
|
764
|
+
if (entrypointText !== null) {
|
|
765
|
+
checkHooks(manifest, entrypoint, entrypointText, report);
|
|
766
|
+
checkImports(entrypoint, entrypointText, report);
|
|
767
|
+
checkNamespaceSafety(entrypoint, entrypointText, report);
|
|
768
|
+
}
|
|
769
|
+
checkShellExpansion(manifest, files, textOf, report);
|
|
770
|
+
if (!manifest.topology) {
|
|
771
|
+
report.warn(WarningCode.W_NO_TOPOLOGY, "workflow declares no topology");
|
|
772
|
+
}
|
|
773
|
+
if (options.latestSandcastleVersion) {
|
|
774
|
+
const latest = options.latestSandcastleVersion;
|
|
775
|
+
const range = manifest.sandcastle.versionRange;
|
|
776
|
+
if (semver2.validRange(range) && semver2.valid(latest)) {
|
|
777
|
+
if (!semver2.satisfies(latest, range)) {
|
|
778
|
+
report.warn(
|
|
779
|
+
WarningCode.W_VERSION_RANGE_STALE,
|
|
780
|
+
`sandcastle.versionRange "${range}" does not include the latest known Sandcastle release (${latest})`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function checkSafePaths(manifest, report) {
|
|
787
|
+
for (const raw of manifest.files) {
|
|
788
|
+
const reason = unsafePathReason(raw);
|
|
789
|
+
if (reason) {
|
|
790
|
+
report.error(
|
|
791
|
+
ErrorCode.E_UNSAFE_PATH,
|
|
792
|
+
`unsafe install path in manifest.files \u2014 ${reason}`,
|
|
793
|
+
typeof raw === "string" ? raw : void 0
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const entrypoint = manifest.sandcastle.entrypoint || DEFAULT_ENTRYPOINT;
|
|
798
|
+
const entryReason = unsafePathReason(entrypoint);
|
|
799
|
+
if (entryReason) {
|
|
800
|
+
report.error(
|
|
801
|
+
ErrorCode.E_UNSAFE_PATH,
|
|
802
|
+
`unsafe sandcastle.entrypoint \u2014 ${entryReason}`,
|
|
803
|
+
entrypoint
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function checkEnvConsistency(manifest, files, textOf, report) {
|
|
808
|
+
const envExamplePath = `${PAYLOAD_DIR}/.env.example`;
|
|
809
|
+
const exampleText = files.has(envExamplePath) ? textOf(envExamplePath) : null;
|
|
810
|
+
const exampleKeys = exampleText ? parseEnvExampleKeys(exampleText) : /* @__PURE__ */ new Set();
|
|
811
|
+
const manifestKeys = new Set(manifest.env.map((e) => e.name));
|
|
812
|
+
if (exampleKeys.size === 0 && manifestKeys.size === 0) return;
|
|
813
|
+
const missingFromManifest = [...exampleKeys].filter((k) => !manifestKeys.has(k));
|
|
814
|
+
const missingFromExample = [...manifestKeys].filter((k) => !exampleKeys.has(k));
|
|
815
|
+
if (missingFromManifest.length > 0 || missingFromExample.length > 0) {
|
|
816
|
+
const parts = [];
|
|
817
|
+
if (missingFromManifest.length > 0) {
|
|
818
|
+
parts.push(
|
|
819
|
+
`in .env.example but not manifest.env: ${missingFromManifest.join(", ")}`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
if (missingFromExample.length > 0) {
|
|
823
|
+
parts.push(
|
|
824
|
+
`in manifest.env but not .env.example: ${missingFromExample.join(", ")}`
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
report.error(
|
|
828
|
+
ErrorCode.E_ENV_MISMATCH,
|
|
829
|
+
`env vars inconsistent \u2014 ${parts.join("; ")}`,
|
|
830
|
+
envExamplePath
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function checkHooks(manifest, entrypoint, entrypointText, report) {
|
|
835
|
+
const extraction = extractHooks(entrypointText);
|
|
836
|
+
if (!extraction.static) {
|
|
837
|
+
report.error(
|
|
838
|
+
ErrorCode.E_HOOKS_NOT_STATIC,
|
|
839
|
+
`hooks must be statically analyzable: ${[...new Set(extraction.dynamicReasons)].join("; ")}`,
|
|
840
|
+
entrypoint
|
|
841
|
+
);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const detectedHost = new Set(extraction.hostCommands);
|
|
845
|
+
const detectedSandbox = new Set(extraction.sandboxCommands);
|
|
846
|
+
const declaredHost = new Set(manifest.disclosures.hostHooks);
|
|
847
|
+
const declaredSandbox = new Set(manifest.disclosures.sandboxHooks);
|
|
848
|
+
const hostMismatch = !setsEqual(detectedHost, declaredHost);
|
|
849
|
+
const sandboxMismatch = !setsEqual(detectedSandbox, declaredSandbox);
|
|
850
|
+
if (hostMismatch || sandboxMismatch) {
|
|
851
|
+
const details = [];
|
|
852
|
+
if (hostMismatch) {
|
|
853
|
+
details.push(
|
|
854
|
+
`host hooks \u2014 declared [${[...declaredHost].join(", ")}] vs detected [${[...detectedHost].join(", ")}]`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
if (sandboxMismatch) {
|
|
858
|
+
details.push(
|
|
859
|
+
`sandbox hooks \u2014 declared [${[...declaredSandbox].join(", ")}] vs detected [${[...detectedSandbox].join(", ")}]`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
report.error(
|
|
863
|
+
ErrorCode.E_HOOKS_MISMATCH,
|
|
864
|
+
`disclosures.*Hooks do not match the entrypoint: ${details.join("; ")}`,
|
|
865
|
+
entrypoint
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function checkImports(entrypoint, entrypointText, report) {
|
|
870
|
+
for (const specifier of forbiddenImports(entrypointText)) {
|
|
871
|
+
report.error(
|
|
872
|
+
ErrorCode.E_FORBIDDEN_IMPORT,
|
|
873
|
+
`entrypoint imports "${specifier}" \u2014 only @ai-hero/sandcastle, zod, and node:* builtins are allowed`,
|
|
874
|
+
entrypoint
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function checkNamespaceSafety(entrypoint, entrypointText, report) {
|
|
879
|
+
const unsafe = findNamespaceUnsafePaths(entrypointText);
|
|
880
|
+
if (unsafe.length > 0) {
|
|
881
|
+
const paths = unsafe.map((u) => u.path).join(", ");
|
|
882
|
+
report.warn(
|
|
883
|
+
WarningCode.W_NAMESPACE_UNSAFE_PATHS,
|
|
884
|
+
`promptFile paths anchored to ${PAYLOAD_DIR}/ may break under a namespaced install: ${paths}`,
|
|
885
|
+
entrypoint
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function checkShellExpansion(manifest, files, textOf, report) {
|
|
890
|
+
if (manifest.disclosures.usesShellExpansion) return;
|
|
891
|
+
for (const path of files.keys()) {
|
|
892
|
+
if (!inPayload(path) || !path.endsWith(".md")) continue;
|
|
893
|
+
const text = textOf(path);
|
|
894
|
+
if (text === null) continue;
|
|
895
|
+
const blocks = detectShellExpansions(text);
|
|
896
|
+
if (blocks.length > 0) {
|
|
897
|
+
report.error(
|
|
898
|
+
ErrorCode.E_UNDECLARED_SHELL_EXPANSION,
|
|
899
|
+
"prompt file contains !`command` shell expansion but disclosures.usesShellExpansion is false",
|
|
900
|
+
path
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function setsEqual(a, b) {
|
|
906
|
+
if (a.size !== b.size) return false;
|
|
907
|
+
for (const v of a) if (!b.has(v)) return false;
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
var FACTORY_SET = new Set(AGENT_FACTORY_NAMES);
|
|
911
|
+
function readStatic(node) {
|
|
912
|
+
if (!node) return void 0;
|
|
913
|
+
if (Node4.isStringLiteral(node) || Node4.isNoSubstitutionTemplateLiteral(node)) {
|
|
914
|
+
return node.getLiteralText();
|
|
915
|
+
}
|
|
916
|
+
if (Node4.isNumericLiteral(node)) return Number(node.getLiteralText());
|
|
917
|
+
if (node.getKind() === SyntaxKind4.TrueKeyword) return true;
|
|
918
|
+
if (node.getKind() === SyntaxKind4.FalseKeyword) return false;
|
|
919
|
+
if (node.getKind() === SyntaxKind4.NullKeyword) return null;
|
|
920
|
+
return void 0;
|
|
921
|
+
}
|
|
922
|
+
function readOptions(node) {
|
|
923
|
+
const out = {};
|
|
924
|
+
if (!node || !Node4.isObjectLiteralExpression(node)) return out;
|
|
925
|
+
for (const prop of node.getProperties()) {
|
|
926
|
+
if (!Node4.isPropertyAssignment(prop)) continue;
|
|
927
|
+
const value = readStatic(prop.getInitializer());
|
|
928
|
+
if (value !== void 0) out[prop.getName()] = value;
|
|
929
|
+
}
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
932
|
+
function detectAgents(entrypointContent) {
|
|
933
|
+
const source = createSourceFile(entrypointContent);
|
|
934
|
+
const out = [];
|
|
935
|
+
for (const call of source.getDescendantsOfKind(SyntaxKind4.CallExpression)) {
|
|
936
|
+
const expr = call.getExpression();
|
|
937
|
+
if (!Node4.isIdentifier(expr)) continue;
|
|
938
|
+
const name = expr.getText();
|
|
939
|
+
if (!FACTORY_SET.has(name)) continue;
|
|
940
|
+
const factory = name;
|
|
941
|
+
const args = call.getArguments();
|
|
942
|
+
const model = readStatic(args[0]);
|
|
943
|
+
const options = readOptions(args[1]);
|
|
944
|
+
out.push({
|
|
945
|
+
factory,
|
|
946
|
+
provider: FACTORY_TO_PROVIDER[factory],
|
|
947
|
+
...typeof model === "string" ? { model } : {},
|
|
948
|
+
options
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
return out;
|
|
952
|
+
}
|
|
953
|
+
var ErrorEnvelopeSchema = z3.object({
|
|
954
|
+
error: z3.object({
|
|
955
|
+
code: z3.string(),
|
|
956
|
+
message: z3.string()
|
|
957
|
+
})
|
|
958
|
+
});
|
|
959
|
+
var RepoRefSchema = z3.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, "expected owner/name");
|
|
960
|
+
var CommitShaSchema = z3.string().regex(/^[0-9a-f]{40}$|^[0-9a-f]{64}$/, "expected a git commit SHA");
|
|
961
|
+
var DirectorySchema = z3.string().max(256, "directory must be at most 256 characters").regex(
|
|
962
|
+
/^[A-Za-z0-9._/-]+$/,
|
|
963
|
+
"directory may only contain A-Za-z0-9._/- (no backslash, colon, or whitespace)"
|
|
964
|
+
).refine((d) => !d.startsWith("/"), { message: "directory must be relative" }).refine((d) => !d.split("/").includes(".."), {
|
|
965
|
+
message: "directory must not contain a '..' traversal segment"
|
|
966
|
+
});
|
|
967
|
+
var FileChecksumSchema = z3.object({
|
|
968
|
+
path: z3.string().min(1),
|
|
969
|
+
sha256: z3.string().regex(/^[0-9a-f]{64}$/, "expected a sha256 hex digest"),
|
|
970
|
+
sizeBytes: z3.number().int().nonnegative(),
|
|
971
|
+
/** URL to fetch the cached content (registry cache endpoint). */
|
|
972
|
+
downloadUrl: z3.string().optional()
|
|
973
|
+
});
|
|
974
|
+
var FileListingSchema = z3.array(FileChecksumSchema);
|
|
975
|
+
var PublishRequestSchema = z3.object({
|
|
976
|
+
manifest: ManifestSchema,
|
|
977
|
+
repo: RepoRefSchema,
|
|
978
|
+
directory: DirectorySchema.optional(),
|
|
979
|
+
commitSha: CommitShaSchema
|
|
980
|
+
});
|
|
981
|
+
var VersionRecordSchema = z3.object({
|
|
982
|
+
version: z3.string(),
|
|
983
|
+
commitSha: CommitShaSchema,
|
|
984
|
+
manifest: ManifestSchema,
|
|
985
|
+
publishedAt: z3.string()
|
|
986
|
+
});
|
|
987
|
+
var WorkflowStatsSchema = z3.object({
|
|
988
|
+
downloads: z3.number().int().nonnegative(),
|
|
989
|
+
stars: z3.number().int().nonnegative()
|
|
990
|
+
});
|
|
991
|
+
var WorkflowRecordSchema = z3.object({
|
|
992
|
+
author: z3.string(),
|
|
993
|
+
slug: z3.string(),
|
|
994
|
+
repo: RepoRefSchema,
|
|
995
|
+
directory: z3.string().optional(),
|
|
996
|
+
version: z3.string(),
|
|
997
|
+
commitSha: CommitShaSchema,
|
|
998
|
+
manifest: ManifestSchema,
|
|
999
|
+
files: FileListingSchema,
|
|
1000
|
+
stats: WorkflowStatsSchema,
|
|
1001
|
+
unlisted: z3.boolean().default(false),
|
|
1002
|
+
publishedAt: z3.string(),
|
|
1003
|
+
versions: z3.array(z3.string()).default([])
|
|
1004
|
+
});
|
|
1005
|
+
var BrowseSortSchema = z3.enum(["downloads", "newest", "name"]);
|
|
1006
|
+
var BrowseQuerySchema = z3.object({
|
|
1007
|
+
q: z3.string().optional(),
|
|
1008
|
+
tags: z3.union([z3.string(), z3.array(z3.string())]).transform(
|
|
1009
|
+
(v) => Array.isArray(v) ? v : v.split(",").map((s) => s.trim()).filter(Boolean)
|
|
1010
|
+
).optional(),
|
|
1011
|
+
agentProvider: z3.enum(AGENT_PROVIDERS).optional(),
|
|
1012
|
+
sandboxProvider: z3.enum(SANDBOX_PROVIDERS).optional(),
|
|
1013
|
+
sort: BrowseSortSchema.default("downloads"),
|
|
1014
|
+
page: z3.coerce.number().int().positive().default(1)
|
|
1015
|
+
});
|
|
1016
|
+
var AgentBadgeSchema = z3.object({
|
|
1017
|
+
role: z3.string(),
|
|
1018
|
+
provider: z3.enum(AGENT_PROVIDERS),
|
|
1019
|
+
model: z3.string()
|
|
1020
|
+
});
|
|
1021
|
+
var WorkflowCardSchema = z3.object({
|
|
1022
|
+
author: z3.string(),
|
|
1023
|
+
slug: z3.string(),
|
|
1024
|
+
name: z3.string(),
|
|
1025
|
+
description: z3.string(),
|
|
1026
|
+
tags: z3.array(z3.string()),
|
|
1027
|
+
agents: z3.array(AgentBadgeSchema),
|
|
1028
|
+
sandboxProviders: z3.array(z3.enum(SANDBOX_PROVIDERS)),
|
|
1029
|
+
downloads: z3.number().int().nonnegative(),
|
|
1030
|
+
stars: z3.number().int().nonnegative(),
|
|
1031
|
+
hasTopology: z3.boolean(),
|
|
1032
|
+
version: z3.string()
|
|
1033
|
+
});
|
|
1034
|
+
var BrowseResponseSchema = z3.object({
|
|
1035
|
+
items: z3.array(WorkflowCardSchema),
|
|
1036
|
+
page: z3.number().int().positive(),
|
|
1037
|
+
pageSize: z3.number().int().positive(),
|
|
1038
|
+
total: z3.number().int().nonnegative()
|
|
1039
|
+
});
|
|
1040
|
+
var DownloadPingSchema = z3.object({
|
|
1041
|
+
/** Random anonymous install id (no PII). */
|
|
1042
|
+
anonInstallId: z3.string().min(1)
|
|
1043
|
+
});
|
|
1044
|
+
var ReportReasonSchema = z3.enum([
|
|
1045
|
+
"malware",
|
|
1046
|
+
"secret-leak",
|
|
1047
|
+
"spam",
|
|
1048
|
+
"policy-violation",
|
|
1049
|
+
"other"
|
|
1050
|
+
]);
|
|
1051
|
+
var ReportRequestSchema = z3.object({
|
|
1052
|
+
reason: ReportReasonSchema,
|
|
1053
|
+
details: z3.string().max(2e3).optional()
|
|
1054
|
+
});
|
|
1055
|
+
var SandcastleVersionsSchema = z3.object({
|
|
1056
|
+
latest: z3.string(),
|
|
1057
|
+
distTags: z3.record(z3.string(), z3.string()),
|
|
1058
|
+
refreshedAt: z3.string()
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
export {
|
|
1062
|
+
PRODUCT_NAME,
|
|
1063
|
+
DEFAULT_REGISTRY_URL,
|
|
1064
|
+
SCHEMA_VERSION,
|
|
1065
|
+
SCHEMA_ID,
|
|
1066
|
+
PAYLOAD_DIR,
|
|
1067
|
+
DEFAULT_ENTRYPOINT,
|
|
1068
|
+
SANDCASTLE_PACKAGE_NAME,
|
|
1069
|
+
SANDBOX_PROVIDERS,
|
|
1070
|
+
parseManifest,
|
|
1071
|
+
extractHooks,
|
|
1072
|
+
detectShellExpansions,
|
|
1073
|
+
normalizePath,
|
|
1074
|
+
validatePackage,
|
|
1075
|
+
detectAgents,
|
|
1076
|
+
ErrorEnvelopeSchema,
|
|
1077
|
+
PublishRequestSchema,
|
|
1078
|
+
VersionRecordSchema,
|
|
1079
|
+
WorkflowRecordSchema,
|
|
1080
|
+
BrowseResponseSchema
|
|
1081
|
+
};
|
|
1082
|
+
//# sourceMappingURL=chunk-AHRTPV2C.js.map
|