@piagent/platform 1.0.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 (159) hide show
  1. package/.gitattributes +7 -0
  2. package/AGENTS.md +31 -0
  3. package/CHANGELOG.md +296 -0
  4. package/CODE_OF_CONDUCT.md +36 -0
  5. package/CONTRIBUTING.md +66 -0
  6. package/LICENSE +21 -0
  7. package/README.md +588 -0
  8. package/SECURITY.md +38 -0
  9. package/adapters/backend-api/profile.json +88 -0
  10. package/adapters/be-readonly-fe/profile.json +120 -0
  11. package/adapters/data/profile.json +88 -0
  12. package/adapters/devops/profile.json +92 -0
  13. package/adapters/docs/profile.json +74 -0
  14. package/adapters/fullstack/profile.json +94 -0
  15. package/adapters/generic/profile.json +95 -0
  16. package/adapters/mobile/profile.json +87 -0
  17. package/adapters/node-typescript/profile.json +76 -0
  18. package/adapters/python/profile.json +75 -0
  19. package/adapters/web-frontend/profile.json +101 -0
  20. package/catalog/capabilities.json +206 -0
  21. package/docs/architecture.md +64 -0
  22. package/docs/auto-delegation-policy.md +192 -0
  23. package/docs/capability-packs.md +234 -0
  24. package/docs/command-reference-vietnamese.md +484 -0
  25. package/docs/context-window-policy.md +85 -0
  26. package/docs/distribution-standard.md +229 -0
  27. package/docs/herdr-workflow.md +58 -0
  28. package/docs/mcp-and-tools.md +316 -0
  29. package/docs/memory-policy.md +164 -0
  30. package/docs/model-options.md +165 -0
  31. package/docs/oauth-providers.md +53 -0
  32. package/docs/operator-manual-vietnamese.md +1080 -0
  33. package/docs/package-architecture-notes.md +101 -0
  34. package/docs/project-adapters.md +174 -0
  35. package/docs/project-onboarding.md +135 -0
  36. package/docs/publishing-for-teams.md +64 -0
  37. package/docs/quality-benchmark.md +74 -0
  38. package/docs/quickstart-vietnamese.md +350 -0
  39. package/docs/release-install-policy.md +223 -0
  40. package/docs/runtime-harness-standard.md +118 -0
  41. package/docs/runtime-policy-design.md +132 -0
  42. package/docs/runtime-quality-baseline.md +257 -0
  43. package/docs/security-redaction-benchmark.md +56 -0
  44. package/docs/security-threat-model.md +66 -0
  45. package/docs/subagent-orchestration-capabilities.md +104 -0
  46. package/docs/subagents-and-multiagent.md +346 -0
  47. package/docs/task-implementation-contract.md +200 -0
  48. package/docs/team-onboarding.md +333 -0
  49. package/docs/usage-observability.md +130 -0
  50. package/docs/vercel-docs-site.md +115 -0
  51. package/docs/workflow-recipes.md +125 -0
  52. package/docs-site/assets/piagent-logo.svg +35 -0
  53. package/docs-site/favicon.svg +18 -0
  54. package/docs-site/index.html +1613 -0
  55. package/docs-site/vercel.json +24 -0
  56. package/evals/fixtures/action-proposal.invalid.json +34 -0
  57. package/evals/fixtures/action-proposal.valid.json +34 -0
  58. package/evals/fixtures/capability-pack.invalid.json +123 -0
  59. package/evals/fixtures/capability-pack.valid.json +123 -0
  60. package/evals/fixtures/capability-recipe.invalid.json +81 -0
  61. package/evals/fixtures/capability-recipe.valid.json +82 -0
  62. package/evals/fixtures/context-index.invalid.json +25 -0
  63. package/evals/fixtures/context-index.valid.json +25 -0
  64. package/evals/fixtures/eval-scenario.invalid.json +40 -0
  65. package/evals/fixtures/eval-scenario.valid.json +45 -0
  66. package/evals/fixtures/project-profile.invalid.json +88 -0
  67. package/evals/fixtures/project-profile.valid.json +95 -0
  68. package/evals/fixtures/task-contract.invalid.json +73 -0
  69. package/evals/fixtures/task-contract.valid.json +74 -0
  70. package/evals/golden/enforcement-decisions.json +329 -0
  71. package/evals/scenarios/capability-resolution.json +45 -0
  72. package/package.json +132 -0
  73. package/packages/piagent-core/README.md +147 -0
  74. package/packages/piagent-core/capabilities/capability-core.js +1023 -0
  75. package/packages/piagent-core/capabilities/capability-sources.js +265 -0
  76. package/packages/piagent-core/extensions/guard-io.js +45 -0
  77. package/packages/piagent-core/extensions/guard-shell-analysis.ts +558 -0
  78. package/packages/piagent-core/extensions/guard-types.ts +406 -0
  79. package/packages/piagent-core/extensions/piagent-guard.ts +5760 -0
  80. package/packages/piagent-core/extensions/policy-core.js +1070 -0
  81. package/packages/piagent-core/extensions/redaction-core.js +4 -0
  82. package/packages/piagent-core/extensions/runtime-evidence.js +215 -0
  83. package/packages/piagent-core/package.json +46 -0
  84. package/packages/piagent-core/policies/base-policy.json +249 -0
  85. package/packages/piagent-core/prompts/be-to-fe.md +82 -0
  86. package/packages/piagent-core/prompts/commit.md +27 -0
  87. package/packages/piagent-core/prompts/discuss.md +35 -0
  88. package/packages/piagent-core/prompts/memory-policy.md +52 -0
  89. package/packages/piagent-core/prompts/model-options.md +68 -0
  90. package/packages/piagent-core/prompts/onboard-project.md +147 -0
  91. package/packages/piagent-core/prompts/piagent-commands.md +68 -0
  92. package/packages/piagent-core/prompts/plan.md +35 -0
  93. package/packages/piagent-core/prompts/platform-improve.md +69 -0
  94. package/packages/piagent-core/prompts/pr.md +26 -0
  95. package/packages/piagent-core/prompts/review.md +38 -0
  96. package/packages/piagent-core/prompts/scout.md +50 -0
  97. package/packages/piagent-core/prompts/task.md +69 -0
  98. package/packages/piagent-core/security/sensitive-data.js +187 -0
  99. package/packages/piagent-core/skills/piagent-ops/SKILL.md +44 -0
  100. package/packages/piagent-core/skills/piagent-source-cache/SKILL.md +59 -0
  101. package/packages/piagent-core/skills/piagent-source-cache/checkout-source-repo.sh +144 -0
  102. package/packages/piagent-core/subagents/piagent-oracle.md +29 -0
  103. package/packages/piagent-core/subagents/piagent-planner.md +46 -0
  104. package/packages/piagent-core/subagents/piagent-reviewer.md +48 -0
  105. package/packages/piagent-core/subagents/piagent-scout.md +37 -0
  106. package/packages/piagent-core/subagents/piagent-worker.md +44 -0
  107. package/packs/engineering-base/pack.json +123 -0
  108. package/packs/engineering-base/recipes/bounded-change.json +82 -0
  109. package/packs/web-delivery/pack.json +72 -0
  110. package/packs/web-delivery/recipes/verified-web-change.json +57 -0
  111. package/schemas/action-proposal.schema.json +90 -0
  112. package/schemas/capability-pack.schema.json +190 -0
  113. package/schemas/capability-recipe.schema.json +72 -0
  114. package/schemas/context-index.schema.json +258 -0
  115. package/schemas/eval-scenario.schema.json +67 -0
  116. package/schemas/project-profile.schema.json +555 -0
  117. package/schemas/task-contract.schema.json +314 -0
  118. package/scripts/audit-runtime-host.sh +78 -0
  119. package/scripts/bootstrap-local.sh +25 -0
  120. package/scripts/capability-catalog.mjs +290 -0
  121. package/scripts/check-runtime-advisories.mjs +114 -0
  122. package/scripts/configure-mcp.sh +301 -0
  123. package/scripts/configure-model-scope.sh +143 -0
  124. package/scripts/configure-subagents.sh +241 -0
  125. package/scripts/import-agent-instructions.mjs +199 -0
  126. package/scripts/init-project.sh +369 -0
  127. package/scripts/install-global.sh +532 -0
  128. package/scripts/link-project.sh +25 -0
  129. package/scripts/migrate-project-state.mjs +176 -0
  130. package/scripts/pi-auto.sh +79 -0
  131. package/scripts/pi-model-catalog.sh +154 -0
  132. package/scripts/pi-session-stats.sh +135 -0
  133. package/scripts/piagent-cli.mjs +53 -0
  134. package/scripts/profile-doctor.sh +238 -0
  135. package/scripts/quality-benchmark.sh +185 -0
  136. package/scripts/redaction-benchmark.mjs +227 -0
  137. package/scripts/runtime-policy-smoke.sh +48 -0
  138. package/scripts/setup.sh +425 -0
  139. package/scripts/team-doctor.sh +461 -0
  140. package/scripts/verify-local.sh +540 -0
  141. package/scripts/verify-release-identity.mjs +85 -0
  142. package/scripts/verify-vercel-link.mjs +64 -0
  143. package/templates/global/mcp.json +9 -0
  144. package/templates/global/settings.json +37 -0
  145. package/templates/project/.mcp.json +9 -0
  146. package/templates/project/.pi/context-index.json +25 -0
  147. package/templates/project/.pi/gitignore.template +22 -0
  148. package/templates/project/.pi/mcp.json +9 -0
  149. package/templates/project/.pi/memory/MEMORY.md +11 -0
  150. package/templates/project/.pi/memory/memory_summary.md +9 -0
  151. package/templates/project/.pi/piagent-profile.json +107 -0
  152. package/templates/project/.pi/project-context.md +30 -0
  153. package/templates/project/.pi/settings.json +17 -0
  154. package/templates/project/.pi/task-contract.template.json +74 -0
  155. package/templates/project/.pi/tech-context/README.md +7 -0
  156. package/templates/project/.pi/tech-stack.json +10 -0
  157. package/templates/project/AGENTS.md +34 -0
  158. package/templates/project/REVIEW_GUIDELINES.md +23 -0
  159. package/types/pi-runtime-shims.d.ts +13 -0
@@ -0,0 +1,1023 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { containsSensitiveText } from "../security/sensitive-data.js";
5
+
6
+ const API_VERSION = "piagent/v1";
7
+ const CORE_API_VERSION = 1;
8
+ const MAX_JSON_BYTES = 256 * 1024;
9
+ const MAX_ARTIFACT_BYTES = 2 * 1024 * 1024;
10
+ const PACK_KINDS = new Set(["prompts", "skills", "subagents", "policies", "adapters", "recipes", "evals"]);
11
+ const LIFECYCLES = new Set(["experimental", "stable", "deprecated"]);
12
+ const ACTIVATION_MODES = new Set(["explicit", "profile", "trigger"]);
13
+ const STEP_MODES = new Set(["read-only", "workspace-write", "action-proposal"]);
14
+ const ACTION_TYPES = new Set([
15
+ "git-push",
16
+ "github-pull-request",
17
+ "github-issue",
18
+ "release",
19
+ "publish",
20
+ "deploy",
21
+ "database-change",
22
+ "external-provider-write"
23
+ ]);
24
+ const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
25
+ const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/;
26
+ const IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9._/-]{0,127}$/;
27
+ const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
28
+ const DOMAIN_PATTERN = /^(?:\*\.)?[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/;
29
+ const ISO_UTC_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
30
+ const MAX_ACTION_LIFETIME_MS = 24 * 60 * 60 * 1000;
31
+ const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
32
+
33
+ export class CapabilityValidationError extends Error {
34
+ constructor(message, errors = []) {
35
+ super(message);
36
+ this.name = "CapabilityValidationError";
37
+ this.errors = errors;
38
+ }
39
+ }
40
+
41
+ function isPlainObject(value) {
42
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
43
+ const prototype = Object.getPrototypeOf(value);
44
+ return prototype === Object.prototype || prototype === null;
45
+ }
46
+
47
+ function pushError(errors, location, message) {
48
+ errors.push(`${location}: ${message}`);
49
+ }
50
+
51
+ function compareCodePoints(left, right) {
52
+ const leftText = String(left);
53
+ const rightText = String(right);
54
+ if (leftText < rightText) return -1;
55
+ if (leftText > rightText) return 1;
56
+ return 0;
57
+ }
58
+
59
+ function parseStrictUtcTimestamp(value) {
60
+ if (typeof value !== "string" || !ISO_UTC_TIMESTAMP_PATTERN.test(value)) return Number.NaN;
61
+ const timestamp = Date.parse(value);
62
+ if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) return Number.NaN;
63
+ return timestamp;
64
+ }
65
+
66
+ function rejectUnknownKeys(value, allowed, location, errors) {
67
+ if (!isPlainObject(value)) {
68
+ pushError(errors, location, "must be an object");
69
+ return false;
70
+ }
71
+ for (const key of Object.keys(value)) {
72
+ if (!allowed.has(key)) pushError(errors, `${location}.${key}`, "is not supported");
73
+ }
74
+ return true;
75
+ }
76
+
77
+ function rejectUnsafeKeys(value, location, errors, depth = 0) {
78
+ if (depth > 32) {
79
+ pushError(errors, location, "exceeds the maximum nesting depth");
80
+ return;
81
+ }
82
+ if (Array.isArray(value)) {
83
+ value.forEach((item, index) => rejectUnsafeKeys(item, `${location}[${index}]`, errors, depth + 1));
84
+ return;
85
+ }
86
+ if (!isPlainObject(value)) return;
87
+ for (const [key, item] of Object.entries(value)) {
88
+ if (["__proto__", "prototype", "constructor"].includes(key)) {
89
+ pushError(errors, `${location}.${key}`, "is not allowed");
90
+ }
91
+ rejectUnsafeKeys(item, `${location}.${key}`, errors, depth + 1);
92
+ }
93
+ }
94
+
95
+ function validateString(value, location, errors, options = {}) {
96
+ const { min = 1, max = 256, pattern } = options;
97
+ if (typeof value !== "string") {
98
+ pushError(errors, location, "must be a string");
99
+ return false;
100
+ }
101
+ if (value.length < min || value.length > max) {
102
+ pushError(errors, location, `length must be between ${min} and ${max}`);
103
+ return false;
104
+ }
105
+ if (/\0|[\r\n]/.test(value)) {
106
+ pushError(errors, location, "must not contain control line characters");
107
+ return false;
108
+ }
109
+ if (pattern && !pattern.test(value)) {
110
+ pushError(errors, location, "has an invalid format");
111
+ return false;
112
+ }
113
+ return true;
114
+ }
115
+
116
+ function validateStringArray(value, location, errors, options = {}) {
117
+ const { maxItems = 64, pattern, allowEmpty = true } = options;
118
+ if (!Array.isArray(value)) {
119
+ pushError(errors, location, "must be an array");
120
+ return [];
121
+ }
122
+ if ((!allowEmpty && value.length === 0) || value.length > maxItems) {
123
+ pushError(errors, location, `must contain ${allowEmpty ? "0" : "1"} to ${maxItems} items`);
124
+ }
125
+ const seen = new Set();
126
+ value.forEach((item, index) => {
127
+ if (validateString(item, `${location}[${index}]`, errors, { max: 256, pattern })) {
128
+ if (seen.has(item)) pushError(errors, `${location}[${index}]`, "must be unique");
129
+ seen.add(item);
130
+ }
131
+ });
132
+ return value;
133
+ }
134
+
135
+ function validateMetadata(value, location, errors) {
136
+ if (!rejectUnknownKeys(value, new Set(["name", "version", "owner", "lifecycle", "license", "description", "tags"]), location, errors)) return;
137
+ validateString(value.name, `${location}.name`, errors, { max: 64, pattern: NAME_PATTERN });
138
+ validateString(value.version, `${location}.version`, errors, { max: 64, pattern: VERSION_PATTERN });
139
+ validateString(value.owner, `${location}.owner`, errors, { max: 100, pattern: NAME_PATTERN });
140
+ if (!LIFECYCLES.has(value.lifecycle)) pushError(errors, `${location}.lifecycle`, "must be experimental, stable, or deprecated");
141
+ validateString(value.license, `${location}.license`, errors, { max: 64 });
142
+ validateString(value.description, `${location}.description`, errors, { max: 240 });
143
+ if (!Object.hasOwn(value, "tags")) pushError(errors, `${location}.tags`, "is required");
144
+ else validateStringArray(value.tags, `${location}.tags`, errors, { maxItems: 32, pattern: NAME_PATTERN });
145
+ }
146
+
147
+ function isSafeRelativePath(value, { allowGlob = false } = {}) {
148
+ if (typeof value !== "string" || value.length === 0 || value.length > 512) return false;
149
+ if (value.includes("\\") || value.includes("\0") || path.posix.isAbsolute(value) || /^[A-Za-z]:/.test(value)) return false;
150
+ if (!allowGlob && /[*?\[\]{}]/.test(value)) return false;
151
+ const segments = value.split("/");
152
+ return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
153
+ }
154
+
155
+ function validatePathArray(value, location, errors) {
156
+ const paths = validateStringArray(value, location, errors, { maxItems: 128 });
157
+ paths.forEach((item, index) => {
158
+ if (!isSafeRelativePath(item, { allowGlob: true })) pushError(errors, `${location}[${index}]`, "must be a repository-relative path pattern");
159
+ });
160
+ return paths;
161
+ }
162
+
163
+ function validateArtifactList(value, location, errors) {
164
+ if (!Array.isArray(value)) {
165
+ pushError(errors, location, "must be an array");
166
+ return;
167
+ }
168
+ if (value.length > 128) pushError(errors, location, "must contain at most 128 artifacts");
169
+ const ids = new Set();
170
+ value.forEach((artifact, index) => {
171
+ const itemLocation = `${location}[${index}]`;
172
+ if (!rejectUnknownKeys(artifact, new Set(["id", "path"]), itemLocation, errors)) return;
173
+ if (validateString(artifact.id, `${itemLocation}.id`, errors, { max: 128, pattern: IDENTIFIER_PATTERN })) {
174
+ if (ids.has(artifact.id)) pushError(errors, `${itemLocation}.id`, "must be unique within its artifact type");
175
+ ids.add(artifact.id);
176
+ }
177
+ if (!isSafeRelativePath(artifact.path)) pushError(errors, `${itemLocation}.path`, "must be a repository-relative file path");
178
+ });
179
+ }
180
+
181
+ export function validateCapabilityPack(document, options = {}) {
182
+ const source = options.source ?? "capability pack";
183
+ const errors = [];
184
+ rejectUnsafeKeys(document, source, errors);
185
+ if (!rejectUnknownKeys(document, new Set(["apiVersion", "kind", "metadata", "spec"]), source, errors)) {
186
+ throw new CapabilityValidationError(`${source} is invalid`, errors);
187
+ }
188
+ if (document.apiVersion !== API_VERSION) pushError(errors, `${source}.apiVersion`, `must be ${API_VERSION}`);
189
+ if (document.kind !== "CapabilityPack") pushError(errors, `${source}.kind`, "must be CapabilityPack");
190
+ validateMetadata(document.metadata, `${source}.metadata`, errors);
191
+
192
+ const spec = document.spec;
193
+ if (rejectUnknownKeys(spec, new Set(["coreApiVersion", "requires", "provides", "permissions", "activation", "verification"]), `${source}.spec`, errors)) {
194
+ if (spec.coreApiVersion !== CORE_API_VERSION) pushError(errors, `${source}.spec.coreApiVersion`, `must be ${CORE_API_VERSION}`);
195
+
196
+ if (rejectUnknownKeys(spec.requires, new Set(["packs"]), `${source}.spec.requires`, errors)) {
197
+ const dependencies = spec.requires.packs;
198
+ if (!Array.isArray(dependencies)) {
199
+ pushError(errors, `${source}.spec.requires.packs`, "must be an array");
200
+ } else {
201
+ if (dependencies.length > 64) pushError(errors, `${source}.spec.requires.packs`, "must contain at most 64 items");
202
+ const seen = new Set();
203
+ dependencies.forEach((dependency, index) => {
204
+ const location = `${source}.spec.requires.packs[${index}]`;
205
+ if (!rejectUnknownKeys(dependency, new Set(["name", "version"]), location, errors)) return;
206
+ validateString(dependency.name, `${location}.name`, errors, { max: 64, pattern: NAME_PATTERN });
207
+ validateString(dependency.version, `${location}.version`, errors, { max: 64, pattern: VERSION_PATTERN });
208
+ const key = `${dependency.name}@${dependency.version}`;
209
+ if (seen.has(key)) pushError(errors, location, "must be unique");
210
+ seen.add(key);
211
+ });
212
+ }
213
+ }
214
+
215
+ if (isPlainObject(spec.provides)) {
216
+ for (const key of Object.keys(spec.provides)) {
217
+ if (!PACK_KINDS.has(key)) pushError(errors, `${source}.spec.provides.${key}`, "is not a supported artifact type");
218
+ }
219
+ for (const key of PACK_KINDS) {
220
+ if (!Object.hasOwn(spec.provides, key)) pushError(errors, `${source}.spec.provides.${key}`, "is required");
221
+ else validateArtifactList(spec.provides[key], `${source}.spec.provides.${key}`, errors);
222
+ }
223
+ } else {
224
+ pushError(errors, `${source}.spec.provides`, "must be an object");
225
+ }
226
+
227
+ if (rejectUnknownKeys(spec.permissions, new Set(["capabilities", "filesystemRead", "filesystemWrite", "networkDomains", "externalActions"]), `${source}.spec.permissions`, errors)) {
228
+ validateStringArray(spec.permissions.capabilities, `${source}.spec.permissions.capabilities`, errors, { maxItems: 64, pattern: NAME_PATTERN });
229
+ validatePathArray(spec.permissions.filesystemRead, `${source}.spec.permissions.filesystemRead`, errors);
230
+ validatePathArray(spec.permissions.filesystemWrite, `${source}.spec.permissions.filesystemWrite`, errors);
231
+ validateStringArray(spec.permissions.networkDomains, `${source}.spec.permissions.networkDomains`, errors, { maxItems: 64, pattern: DOMAIN_PATTERN });
232
+ const actions = validateStringArray(spec.permissions.externalActions, `${source}.spec.permissions.externalActions`, errors, { maxItems: 32 });
233
+ actions.forEach((action, index) => {
234
+ if (!ACTION_TYPES.has(action)) pushError(errors, `${source}.spec.permissions.externalActions[${index}]`, "is not a supported action type");
235
+ });
236
+ }
237
+
238
+ if (rejectUnknownKeys(spec.activation, new Set(["mode", "profiles", "triggers"]), `${source}.spec.activation`, errors)) {
239
+ if (!ACTIVATION_MODES.has(spec.activation.mode)) pushError(errors, `${source}.spec.activation.mode`, "must be explicit, profile, or trigger");
240
+ validateStringArray(spec.activation.profiles ?? [], `${source}.spec.activation.profiles`, errors, { maxItems: 64, pattern: NAME_PATTERN });
241
+ const triggers = validateStringArray(spec.activation.triggers ?? [], `${source}.spec.activation.triggers`, errors, { maxItems: 64 });
242
+ if (spec.activation.mode === "trigger" && triggers.length === 0) pushError(errors, `${source}.spec.activation.triggers`, "must not be empty for trigger activation");
243
+ }
244
+
245
+ if (rejectUnknownKeys(spec.verification, new Set(["evalScenarios"]), `${source}.spec.verification`, errors)) {
246
+ validateStringArray(spec.verification.evalScenarios, `${source}.spec.verification.evalScenarios`, errors, { maxItems: 64, pattern: IDENTIFIER_PATTERN });
247
+ }
248
+ }
249
+
250
+ if (errors.length > 0) throw new CapabilityValidationError(`${source} is invalid`, errors);
251
+ return document;
252
+ }
253
+
254
+ export function validateCapabilityRecipe(document, options = {}) {
255
+ const source = options.source ?? "capability recipe";
256
+ const errors = [];
257
+ rejectUnsafeKeys(document, source, errors);
258
+ if (!rejectUnknownKeys(document, new Set(["apiVersion", "kind", "metadata", "spec"]), source, errors)) throw new CapabilityValidationError(`${source} is invalid`, errors);
259
+ if (document.apiVersion !== API_VERSION) pushError(errors, `${source}.apiVersion`, `must be ${API_VERSION}`);
260
+ if (document.kind !== "CapabilityRecipe") pushError(errors, `${source}.kind`, "must be CapabilityRecipe");
261
+ validateMetadata(document.metadata, `${source}.metadata`, errors);
262
+ const spec = document.spec;
263
+ if (rejectUnknownKeys(spec, new Set(["inputs", "steps", "gates"]), `${source}.spec`, errors)) {
264
+ if (!Array.isArray(spec.inputs)) pushError(errors, `${source}.spec.inputs`, "must be an array");
265
+ else {
266
+ if (spec.inputs.length > 32) pushError(errors, `${source}.spec.inputs`, "must contain at most 32 items");
267
+ spec.inputs.forEach((input, index) => {
268
+ const location = `${source}.spec.inputs[${index}]`;
269
+ if (!rejectUnknownKeys(input, new Set(["name", "description", "required"]), location, errors)) return;
270
+ validateString(input.name, `${location}.name`, errors, { max: 64, pattern: NAME_PATTERN });
271
+ validateString(input.description, `${location}.description`, errors, { max: 240 });
272
+ if (typeof input.required !== "boolean") pushError(errors, `${location}.required`, "must be a boolean");
273
+ });
274
+ }
275
+
276
+ const stepIds = new Set();
277
+ const stepDependencies = new Map();
278
+ if (!Array.isArray(spec.steps) || spec.steps.length === 0 || spec.steps.length > 64) {
279
+ pushError(errors, `${source}.spec.steps`, "must contain 1 to 64 steps");
280
+ } else spec.steps.forEach((step, index) => {
281
+ const location = `${source}.spec.steps[${index}]`;
282
+ if (!rejectUnknownKeys(step, new Set(["id", "uses", "mode", "needs", "timeoutSeconds", "retries", "outputs"]), location, errors)) return;
283
+ if (validateString(step.id, `${location}.id`, errors, { max: 64, pattern: NAME_PATTERN })) {
284
+ if (stepIds.has(step.id)) pushError(errors, `${location}.id`, "must be unique");
285
+ stepIds.add(step.id);
286
+ }
287
+ validateString(step.uses, `${location}.uses`, errors, { max: 128, pattern: /^(?:capability|recipe):[a-z0-9][a-z0-9._/-]{0,111}$/ });
288
+ if (!STEP_MODES.has(step.mode)) pushError(errors, `${location}.mode`, "must be read-only, workspace-write, or action-proposal");
289
+ const needs = validateStringArray(step.needs ?? [], `${location}.needs`, errors, { maxItems: 32, pattern: NAME_PATTERN });
290
+ stepDependencies.set(step.id, needs);
291
+ if (!Number.isInteger(step.timeoutSeconds) || step.timeoutSeconds < 1 || step.timeoutSeconds > 3600) pushError(errors, `${location}.timeoutSeconds`, "must be an integer between 1 and 3600");
292
+ if (!Number.isInteger(step.retries) || step.retries < 0 || step.retries > 3) pushError(errors, `${location}.retries`, "must be an integer between 0 and 3");
293
+ validateStringArray(step.outputs ?? [], `${location}.outputs`, errors, { maxItems: 32, pattern: IDENTIFIER_PATTERN });
294
+ });
295
+
296
+ for (const [stepId, dependencies] of stepDependencies) {
297
+ for (const dependency of dependencies) if (!stepIds.has(dependency)) pushError(errors, `${source}.spec.steps.${stepId}.needs`, `unknown step ${dependency}`);
298
+ }
299
+ detectDirectedCycle(stepDependencies, `${source}.spec.steps`, errors);
300
+
301
+ if (rejectUnknownKeys(spec.gates, new Set(["context", "verification", "humanApproval"]), `${source}.spec.gates`, errors)) {
302
+ for (const key of ["context", "verification", "humanApproval"]) {
303
+ if (typeof spec.gates[key] !== "boolean") pushError(errors, `${source}.spec.gates.${key}`, "must be a boolean");
304
+ }
305
+ if (spec.steps?.some((step) => step.mode === "workspace-write") && spec.gates.verification !== true) pushError(errors, `${source}.spec.gates.verification`, "must be true for workspace-write steps");
306
+ if (spec.steps?.some((step) => step.mode === "action-proposal") && spec.gates.humanApproval !== true) pushError(errors, `${source}.spec.gates.humanApproval`, "must be true for action-proposal steps");
307
+ }
308
+ }
309
+ if (errors.length > 0) throw new CapabilityValidationError(`${source} is invalid`, errors);
310
+ return document;
311
+ }
312
+
313
+ export function validateEvalScenario(document, options = {}) {
314
+ const source = options.source ?? "eval scenario";
315
+ const errors = [];
316
+ rejectUnsafeKeys(document, source, errors);
317
+ if (!rejectUnknownKeys(document, new Set(["apiVersion", "kind", "metadata", "spec"]), source, errors)) throw new CapabilityValidationError(`${source} is invalid`, errors);
318
+ if (document.apiVersion !== API_VERSION) pushError(errors, `${source}.apiVersion`, `must be ${API_VERSION}`);
319
+ if (document.kind !== "EvalScenario") pushError(errors, `${source}.kind`, "must be EvalScenario");
320
+ validateMetadata(document.metadata, `${source}.metadata`, errors);
321
+ const spec = document.spec;
322
+ if (rejectUnknownKeys(spec, new Set(["fixture", "profile", "task", "expected", "budget"]), `${source}.spec`, errors)) {
323
+ if (rejectUnknownKeys(spec.fixture, new Set(["path", "digest"]), `${source}.spec.fixture`, errors)) {
324
+ if (!isSafeRelativePath(spec.fixture.path)) pushError(errors, `${source}.spec.fixture.path`, "must be a repository-relative path");
325
+ if (spec.fixture.digest !== undefined && !SHA256_PATTERN.test(spec.fixture.digest)) pushError(errors, `${source}.spec.fixture.digest`, "must be a sha256 digest");
326
+ }
327
+ if (!isSafeRelativePath(spec.profile)) pushError(errors, `${source}.spec.profile`, "must be a repository-relative path");
328
+ validateString(spec.task, `${source}.spec.task`, errors, { max: 8000 });
329
+ if (rejectUnknownKeys(spec.expected, new Set(["verifyCommands", "forbiddenPaths", "forbiddenActions", "requiredArtifacts"]), `${source}.spec.expected`, errors)) {
330
+ validateStringArray(spec.expected.verifyCommands, `${source}.spec.expected.verifyCommands`, errors, { maxItems: 32 });
331
+ validatePathArray(spec.expected.forbiddenPaths, `${source}.spec.expected.forbiddenPaths`, errors);
332
+ validateStringArray(spec.expected.forbiddenActions, `${source}.spec.expected.forbiddenActions`, errors, { maxItems: 32 });
333
+ validateStringArray(spec.expected.requiredArtifacts, `${source}.spec.expected.requiredArtifacts`, errors, { maxItems: 32, pattern: IDENTIFIER_PATTERN });
334
+ }
335
+ if (rejectUnknownKeys(spec.budget, new Set(["maxDurationSeconds", "maxTokens", "maxToolCalls"]), `${source}.spec.budget`, errors)) {
336
+ for (const [key, min, max] of [["maxDurationSeconds", 1, 86400], ["maxTokens", 1, 2_000_000], ["maxToolCalls", 1, 10_000]]) {
337
+ if (!Number.isInteger(spec.budget[key]) || spec.budget[key] < min || spec.budget[key] > max) pushError(errors, `${source}.spec.budget.${key}`, `must be an integer between ${min} and ${max}`);
338
+ }
339
+ }
340
+ }
341
+ if (errors.length > 0) throw new CapabilityValidationError(`${source} is invalid`, errors);
342
+ return document;
343
+ }
344
+
345
+ export function validateExternalActionProposal(document, options = {}) {
346
+ const source = options.source ?? "external action proposal";
347
+ const errors = [];
348
+ const now = options.now ?? Date.now();
349
+ if (!Number.isFinite(now)) throw new CapabilityValidationError(`${source} validation options are invalid`, [`${source}.options.now: must be a finite Unix timestamp`]);
350
+ rejectUnsafeKeys(document, source, errors);
351
+ if (!rejectUnknownKeys(document, new Set(["apiVersion", "kind", "metadata", "spec"]), source, errors)) throw new CapabilityValidationError(`${source} is invalid`, errors);
352
+ if (document.apiVersion !== API_VERSION) pushError(errors, `${source}.apiVersion`, `must be ${API_VERSION}`);
353
+ if (document.kind !== "ExternalActionProposal") pushError(errors, `${source}.kind`, "must be ExternalActionProposal");
354
+ if (rejectUnknownKeys(document.metadata, new Set(["name", "createdAt", "expiresAt"]), `${source}.metadata`, errors)) {
355
+ validateString(document.metadata.name, `${source}.metadata.name`, errors, { max: 64, pattern: NAME_PATTERN });
356
+ const createdAt = parseStrictUtcTimestamp(document.metadata.createdAt);
357
+ const expiresAt = parseStrictUtcTimestamp(document.metadata.expiresAt);
358
+ if (!Number.isFinite(createdAt)) pushError(errors, `${source}.metadata.createdAt`, "must be a canonical UTC timestamp with millisecond precision");
359
+ if (!Number.isFinite(expiresAt)) pushError(errors, `${source}.metadata.expiresAt`, "must be a canonical UTC timestamp with millisecond precision");
360
+ if (Number.isFinite(createdAt) && createdAt > now + MAX_CLOCK_SKEW_MS) pushError(errors, `${source}.metadata.createdAt`, "must not be more than five minutes in the future");
361
+ if (Number.isFinite(expiresAt) && expiresAt <= now) pushError(errors, `${source}.metadata.expiresAt`, "must not be expired");
362
+ if (Number.isFinite(createdAt) && Number.isFinite(expiresAt) && (expiresAt <= createdAt || expiresAt - createdAt > MAX_ACTION_LIFETIME_MS)) pushError(errors, `${source}.metadata.expiresAt`, "must be after createdAt and no more than 24 hours later");
363
+ }
364
+ const spec = document.spec;
365
+ if (rejectUnknownKeys(spec, new Set(["actionType", "target", "summary", "riskLane", "requestedPermissions", "artifacts", "dryRun", "security"]), `${source}.spec`, errors)) {
366
+ if (!ACTION_TYPES.has(spec.actionType)) pushError(errors, `${source}.spec.actionType`, "is not supported");
367
+ if (rejectUnknownKeys(spec.target, new Set(["provider", "resource", "environment"]), `${source}.spec.target`, errors)) {
368
+ validateString(spec.target.provider, `${source}.spec.target.provider`, errors, { max: 64, pattern: NAME_PATTERN });
369
+ validateString(spec.target.resource, `${source}.spec.target.resource`, errors, { max: 256 });
370
+ if (spec.target.environment !== undefined) validateString(spec.target.environment, `${source}.spec.target.environment`, errors, { max: 64, pattern: NAME_PATTERN });
371
+ if (typeof spec.target.resource === "string" && /:\/\/[^/@\s]+:[^/@\s]+@/.test(spec.target.resource)) pushError(errors, `${source}.spec.target.resource`, "must not contain embedded credentials");
372
+ }
373
+ validateString(spec.summary, `${source}.spec.summary`, errors, { max: 500 });
374
+ if (spec.riskLane !== "high-risk") pushError(errors, `${source}.spec.riskLane`, "must be high-risk");
375
+ validateStringArray(spec.requestedPermissions, `${source}.spec.requestedPermissions`, errors, { maxItems: 32, pattern: /^[a-z0-9][a-z0-9:-]{0,127}$/ });
376
+ if (!Array.isArray(spec.artifacts) || spec.artifacts.length > 32) pushError(errors, `${source}.spec.artifacts`, "must be an array with at most 32 items");
377
+ else spec.artifacts.forEach((artifact, index) => {
378
+ const location = `${source}.spec.artifacts[${index}]`;
379
+ if (!rejectUnknownKeys(artifact, new Set(["path", "digest", "mediaType", "byteSize"]), location, errors)) return;
380
+ if (!isSafeRelativePath(artifact.path)) pushError(errors, `${location}.path`, "must be a repository-relative file path");
381
+ if (!SHA256_PATTERN.test(artifact.digest)) pushError(errors, `${location}.digest`, "must be a sha256 digest");
382
+ validateString(artifact.mediaType, `${location}.mediaType`, errors, { max: 128, pattern: /^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i });
383
+ if (!Number.isInteger(artifact.byteSize) || artifact.byteSize < 0 || artifact.byteSize > 10 * 1024 * 1024) pushError(errors, `${location}.byteSize`, "must be an integer between 0 and 10485760");
384
+ });
385
+ if (spec.dryRun !== true) pushError(errors, `${source}.spec.dryRun`, "must be true");
386
+ if (rejectUnknownKeys(spec.security, new Set(["containsSecrets"]), `${source}.spec.security`, errors) && spec.security.containsSecrets !== false) pushError(errors, `${source}.spec.security.containsSecrets`, "must be false");
387
+ }
388
+ scanForSecrets(document, source, errors);
389
+ if (errors.length > 0) throw new CapabilityValidationError(`${source} is invalid`, errors);
390
+ return document;
391
+ }
392
+
393
+ function scanForSecrets(value, location, errors, depth = 0) {
394
+ if (depth > 32) return;
395
+ if (typeof value === "string") {
396
+ if (containsSensitiveText(value)) pushError(errors, location, "contains secret-like material");
397
+ return;
398
+ }
399
+ if (Array.isArray(value)) return value.forEach((item, index) => scanForSecrets(item, `${location}[${index}]`, errors, depth + 1));
400
+ if (!isPlainObject(value)) return;
401
+ for (const [key, item] of Object.entries(value)) scanForSecrets(item, `${location}.${key}`, errors, depth + 1);
402
+ }
403
+
404
+ function detectDirectedCycle(graph, location, errors) {
405
+ const visiting = new Set();
406
+ const visited = new Set();
407
+ const stack = [];
408
+ function visit(node) {
409
+ if (visiting.has(node)) {
410
+ const start = stack.indexOf(node);
411
+ pushError(errors, location, `dependency cycle detected: ${[...stack.slice(start), node].join(" -> ")}`);
412
+ return;
413
+ }
414
+ if (visited.has(node)) return;
415
+ visiting.add(node);
416
+ stack.push(node);
417
+ for (const dependency of graph.get(node) ?? []) if (graph.has(dependency)) visit(dependency);
418
+ stack.pop();
419
+ visiting.delete(node);
420
+ visited.add(node);
421
+ }
422
+ for (const node of graph.keys()) visit(node);
423
+ }
424
+
425
+ export function canonicalize(value) {
426
+ if (Array.isArray(value)) return value.map(canonicalize);
427
+ if (!isPlainObject(value)) return value;
428
+ const output = {};
429
+ for (const key of Object.keys(value).sort()) output[key] = canonicalize(value[key]);
430
+ return output;
431
+ }
432
+
433
+ export function stableJson(value) {
434
+ return `${JSON.stringify(canonicalize(value), null, 2)}\n`;
435
+ }
436
+
437
+ export function sha256(value) {
438
+ const buffer = Buffer.isBuffer(value) ? value : Buffer.from(String(value), "utf8");
439
+ return `sha256:${crypto.createHash("sha256").update(buffer).digest("hex")}`;
440
+ }
441
+
442
+ function readJsonFile(file, maxBytes = MAX_JSON_BYTES) {
443
+ const stat = fs.statSync(file);
444
+ if (!stat.isFile()) throw new CapabilityValidationError(`${file} must be a regular file`);
445
+ if (stat.size > maxBytes) throw new CapabilityValidationError(`${file} exceeds the ${maxBytes}-byte limit`);
446
+ try {
447
+ return JSON.parse(fs.readFileSync(file, "utf8"));
448
+ } catch (error) {
449
+ throw new CapabilityValidationError(`${file} contains invalid JSON`, [error instanceof Error ? error.message : String(error)]);
450
+ }
451
+ }
452
+
453
+ function resolveRegularFile(root, relativePath, maxBytes = MAX_ARTIFACT_BYTES) {
454
+ if (!isSafeRelativePath(relativePath)) throw new CapabilityValidationError(`${relativePath} must be a repository-relative file path`);
455
+ const rootReal = fs.realpathSync(root);
456
+ const candidate = path.resolve(rootReal, relativePath);
457
+ const relative = path.relative(rootReal, candidate);
458
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new CapabilityValidationError(`${relativePath} resolves outside the repository`);
459
+ let current = rootReal;
460
+ for (const segment of relative.split(path.sep)) {
461
+ current = path.join(current, segment);
462
+ const stat = fs.lstatSync(current);
463
+ if (stat.isSymbolicLink()) throw new CapabilityValidationError(`${relativePath} must not traverse a symbolic link`);
464
+ }
465
+ const stat = fs.statSync(candidate);
466
+ if (!stat.isFile()) throw new CapabilityValidationError(`${relativePath} must resolve to a regular file`);
467
+ if (stat.size > maxBytes) throw new CapabilityValidationError(`${relativePath} exceeds the ${maxBytes}-byte limit`);
468
+ return { absolutePath: candidate, byteSize: stat.size };
469
+ }
470
+
471
+ function validateArtifactDocument(kind, file, document) {
472
+ if (kind === "recipes") validateCapabilityRecipe(document, { source: file });
473
+ if (kind === "evals") validateEvalScenario(document, { source: file });
474
+ }
475
+
476
+ function validateEvalResources(root, file, document) {
477
+ const profile = resolveRegularFile(root, document.spec.profile, MAX_JSON_BYTES);
478
+ readJsonFile(profile.absolutePath);
479
+ const fixture = resolveRegularFile(root, document.spec.fixture.path);
480
+ if (document.spec.fixture.digest !== undefined) {
481
+ const actualDigest = sha256(fs.readFileSync(fixture.absolutePath));
482
+ if (actualDigest !== document.spec.fixture.digest) throw new CapabilityValidationError(`${file} fixture digest does not match ${document.spec.fixture.path}`);
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Scan one pack root. Every root — the platform's own and any external source —
488
+ * goes through this same function, so an external tree gets exactly the
489
+ * containment and validation the platform's own packs get.
490
+ */
491
+ function scanPackRoot(root, { origin, source, records, keys }) {
492
+ const rootReal = fs.realpathSync(root);
493
+ const packsDirectory = path.join(rootReal, "packs");
494
+ const packsStat = fs.lstatSync(packsDirectory);
495
+ if (packsStat.isSymbolicLink() || !packsStat.isDirectory()) throw new CapabilityValidationError("packs must be a regular directory");
496
+ const entries = fs.readdirSync(packsDirectory, { withFileTypes: true })
497
+ .filter((entry) => !entry.name.startsWith("."))
498
+ .sort((left, right) => compareCodePoints(left.name, right.name));
499
+ if (entries.length > 256) throw new CapabilityValidationError("packs must contain at most 256 entries");
500
+ for (const entry of entries) {
501
+ if (!entry.isDirectory() || entry.isSymbolicLink?.()) throw new CapabilityValidationError(`packs/${entry.name} must be a regular directory`);
502
+ if (!NAME_PATTERN.test(entry.name)) throw new CapabilityValidationError(`packs/${entry.name} has an invalid directory name`);
503
+ const manifestPath = `packs/${entry.name}/pack.json`;
504
+ const { absolutePath } = resolveRegularFile(rootReal, manifestPath, MAX_JSON_BYTES);
505
+ const manifest = readJsonFile(absolutePath);
506
+ validateCapabilityPack(manifest, { source: manifestPath });
507
+ if (manifest.metadata.name !== entry.name) throw new CapabilityValidationError(`${manifestPath} metadata.name must match its directory name`);
508
+ const key = `${manifest.metadata.name}@${manifest.metadata.version}`;
509
+ // Uniqueness spans every root, not just this one. Two sources shipping the
510
+ // same name@version would otherwise let load order decide which code runs.
511
+ const previousOrigin = keys.get(key);
512
+ if (previousOrigin !== undefined) {
513
+ throw new CapabilityValidationError(`duplicate capability pack ${key} provided by both ${previousOrigin} and ${origin}`);
514
+ }
515
+ keys.set(key, origin);
516
+ const artifacts = [];
517
+ const recipes = [];
518
+ for (const kind of [...PACK_KINDS].sort()) {
519
+ for (const artifact of manifest.spec.provides[kind] ?? []) {
520
+ const resolved = resolveRegularFile(rootReal, artifact.path);
521
+ const bytes = fs.readFileSync(resolved.absolutePath);
522
+ if (kind === "recipes" || kind === "evals") {
523
+ const artifactDocument = readJsonFile(resolved.absolutePath);
524
+ validateArtifactDocument(kind, artifact.path, artifactDocument);
525
+ if (kind === "recipes") {
526
+ if (artifactDocument.metadata.name !== artifact.id) throw new CapabilityValidationError(`${artifact.path} metadata.name must match artifact id ${artifact.id}`);
527
+ recipes.push({
528
+ id: artifact.id,
529
+ uses: artifactDocument.spec.steps
530
+ .filter((step) => step.uses.startsWith("recipe:"))
531
+ .map((step) => step.uses.slice("recipe:".length))
532
+ });
533
+ }
534
+ if (kind === "evals") {
535
+ if (artifactDocument.metadata.name !== artifact.id) throw new CapabilityValidationError(`${artifact.path} metadata.name must match artifact id ${artifact.id}`);
536
+ validateEvalResources(rootReal, artifact.path, artifactDocument);
537
+ }
538
+ }
539
+ artifacts.push({
540
+ id: artifact.id,
541
+ kind,
542
+ path: artifact.path,
543
+ digest: sha256(bytes),
544
+ byteSize: resolved.byteSize
545
+ });
546
+ }
547
+ }
548
+ records.push({
549
+ key,
550
+ origin,
551
+ source,
552
+ manifest,
553
+ manifestPath,
554
+ digest: sha256(stableJson(manifest)),
555
+ artifacts: artifacts.sort(compareArtifact),
556
+ recipes
557
+ });
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Scan the platform's own packs plus any external roots the caller resolved.
563
+ * External roots are produced by capability-sources, which is what verifies
564
+ * they sit inside the project and carry no symbolic links; this function is
565
+ * given directories and treats them all alike.
566
+ *
567
+ * @param {string} root
568
+ * @param {{extraRoots?: Array<{origin: string, root: string, source: string}>}} [options]
569
+ */
570
+ export function scanCapabilityPacks(root, options = {}) {
571
+ const records = [];
572
+ const keys = new Map();
573
+ scanPackRoot(root, { origin: "workspace", source: "workspace", records, keys });
574
+ for (const extra of options.extraRoots ?? []) {
575
+ if (extra.origin === "workspace") throw new CapabilityValidationError("workspace is a reserved capability source name");
576
+ scanPackRoot(extra.root, { origin: extra.origin, source: extra.source, records, keys });
577
+ }
578
+ // The dependency graph spans every root: an external pack may depend on a
579
+ // platform pack, so it can only be checked once they are all present.
580
+ validatePackGraph(records);
581
+ return records;
582
+ }
583
+
584
+ // Manifest paths are root-relative, so the same string names a different file
585
+ // in each root. Errors say which source a pack came from.
586
+ function packLocation(record) {
587
+ return record.origin === "workspace" ? record.manifestPath : `${record.origin}:${record.manifestPath}`;
588
+ }
589
+
590
+ function validatePackGraph(records) {
591
+ const byKey = new Map(records.map((record) => [record.key, record]));
592
+ const graph = new Map();
593
+ const artifactOwners = new Map();
594
+ const errors = [];
595
+ for (const record of records) {
596
+ const dependencies = record.manifest.spec.requires.packs.map((item) => `${item.name}@${item.version}`);
597
+ graph.set(record.key, dependencies);
598
+ for (const dependency of dependencies) if (!byKey.has(dependency)) pushError(errors, packLocation(record), `missing dependency ${dependency}`);
599
+ for (const artifact of record.artifacts) {
600
+ const previous = artifactOwners.get(artifact.id);
601
+ if (previous) pushError(errors, packLocation(record), `artifact id ${artifact.id} conflicts with ${previous}; artifact ids must be globally unique`);
602
+ else artifactOwners.set(artifact.id, `${record.key}/${artifact.kind}`);
603
+ }
604
+ }
605
+ detectDirectedCycle(graph, "capability packs", errors);
606
+ const recipeGraph = new Map(records.flatMap((record) => record.recipes.map((recipe) => [recipe.id, recipe.uses])));
607
+ detectDirectedCycle(recipeGraph, "capability recipes", errors);
608
+ function availableArtifacts(key, visited = new Set()) {
609
+ if (visited.has(key)) return [];
610
+ visited.add(key);
611
+ const record = byKey.get(key);
612
+ if (!record) return [];
613
+ return [
614
+ ...record.artifacts,
615
+ ...record.manifest.spec.requires.packs.flatMap((dependency) => availableArtifacts(`${dependency.name}@${dependency.version}`, visited))
616
+ ];
617
+ }
618
+ for (const record of records) {
619
+ const available = availableArtifacts(record.key);
620
+ const recipes = new Set(available.filter((artifact) => artifact.kind === "recipes").map((artifact) => artifact.id));
621
+ const evals = new Set(available.filter((artifact) => artifact.kind === "evals").map((artifact) => artifact.id));
622
+ for (const recipe of record.recipes) {
623
+ for (const binding of recipe.uses) {
624
+ if (!recipes.has(binding)) pushError(errors, packLocation(record), `recipe binding ${binding} is not provided by this pack or an exact dependency`);
625
+ }
626
+ }
627
+ for (const scenario of record.manifest.spec.verification.evalScenarios) {
628
+ if (!evals.has(scenario)) pushError(errors, packLocation(record), `eval scenario ${scenario} is not provided by this pack or an exact dependency`);
629
+ }
630
+ }
631
+ if (errors.length > 0) throw new CapabilityValidationError("capability dependency graph is invalid", errors);
632
+ }
633
+
634
+ function compareArtifact(left, right) {
635
+ return compareCodePoints(`${left.kind}:${left.id}:${left.path}`, `${right.kind}:${right.id}:${right.path}`);
636
+ }
637
+
638
+ export function buildCapabilityCatalog(root, options = {}) {
639
+ const records = scanCapabilityPacks(root, options);
640
+ return {
641
+ schemaVersion: 1,
642
+ coreApiVersion: CORE_API_VERSION,
643
+ packs: records.map((record) => ({
644
+ name: record.manifest.metadata.name,
645
+ version: record.manifest.metadata.version,
646
+ origin: record.origin,
647
+ source: record.source,
648
+ owner: record.manifest.metadata.owner,
649
+ lifecycle: record.manifest.metadata.lifecycle,
650
+ license: record.manifest.metadata.license,
651
+ description: record.manifest.metadata.description,
652
+ tags: [...record.manifest.metadata.tags].sort(),
653
+ digest: record.digest,
654
+ dependencies: record.manifest.spec.requires.packs.map((item) => ({ ...item })).sort((left, right) => compareCodePoints(`${left.name}@${left.version}`, `${right.name}@${right.version}`)),
655
+ permissions: canonicalize(record.manifest.spec.permissions),
656
+ activation: canonicalize(record.manifest.spec.activation),
657
+ artifacts: record.artifacts
658
+ })).sort((left, right) => compareCodePoints(`${left.name}@${left.version}`, `${right.name}@${right.version}`))
659
+ };
660
+ }
661
+
662
+ function validateProfileSelection(profile, source) {
663
+ const errors = [];
664
+ rejectUnsafeKeys(profile, source, errors);
665
+ const selectors = profile.capabilityPacks ?? [];
666
+ if (!Array.isArray(selectors)) pushError(errors, `${source}.capabilityPacks`, "must be an array");
667
+ else {
668
+ const seen = new Set();
669
+ selectors.forEach((selector, index) => {
670
+ const location = `${source}.capabilityPacks[${index}]`;
671
+ if (!rejectUnknownKeys(selector, new Set(["name", "version"]), location, errors)) return;
672
+ validateString(selector.name, `${location}.name`, errors, { max: 64, pattern: NAME_PATTERN });
673
+ validateString(selector.version, `${location}.version`, errors, { max: 64, pattern: VERSION_PATTERN });
674
+ const key = `${selector.name}@${selector.version}`;
675
+ if (seen.has(key)) pushError(errors, location, "must be unique");
676
+ seen.add(key);
677
+ });
678
+ }
679
+ const policy = profile.capabilityPolicy ?? {};
680
+ if (!rejectUnknownKeys(policy, new Set(["allowedOwners", "allowedLifecycles", "allowedFilesystemRead", "allowedFilesystemWrite", "allowedNetworkDomains", "allowedExternalActions"]), `${source}.capabilityPolicy`, errors)) {
681
+ if (errors.length > 0) throw new CapabilityValidationError(`${source} capability selection is invalid`, errors);
682
+ return;
683
+ }
684
+ validateStringArray(policy.allowedOwners ?? [], `${source}.capabilityPolicy.allowedOwners`, errors, { maxItems: 64, pattern: NAME_PATTERN });
685
+ const lifecycles = validateStringArray(policy.allowedLifecycles ?? [], `${source}.capabilityPolicy.allowedLifecycles`, errors, { maxItems: 3 });
686
+ lifecycles.forEach((item, index) => {
687
+ if (!LIFECYCLES.has(item)) pushError(errors, `${source}.capabilityPolicy.allowedLifecycles[${index}]`, "is not supported");
688
+ });
689
+ validatePathArray(policy.allowedFilesystemRead ?? [], `${source}.capabilityPolicy.allowedFilesystemRead`, errors);
690
+ validatePathArray(policy.allowedFilesystemWrite ?? [], `${source}.capabilityPolicy.allowedFilesystemWrite`, errors);
691
+ validateStringArray(policy.allowedNetworkDomains ?? [], `${source}.capabilityPolicy.allowedNetworkDomains`, errors, { maxItems: 64, pattern: DOMAIN_PATTERN });
692
+ const actions = validateStringArray(policy.allowedExternalActions ?? [], `${source}.capabilityPolicy.allowedExternalActions`, errors, { maxItems: 32 });
693
+ actions.forEach((item, index) => {
694
+ if (!ACTION_TYPES.has(item)) pushError(errors, `${source}.capabilityPolicy.allowedExternalActions[${index}]`, "is not supported");
695
+ });
696
+ if (errors.length > 0) throw new CapabilityValidationError(`${source} capability selection is invalid`, errors);
697
+ }
698
+
699
+ export function validateCapabilityPackageSource(value) {
700
+ const source = value ?? "workspace";
701
+ if (typeof source !== "string" || source.length === 0 || source.length > 512 || /[\0\r\n]/.test(source)) {
702
+ throw new CapabilityValidationError("package source must be a non-empty single-line string of at most 512 characters");
703
+ }
704
+ if (/:\/\/[^/@\s]+:[^/@\s]+@/.test(source) || containsSensitiveText(source)) {
705
+ throw new CapabilityValidationError("package source must not contain credentials");
706
+ }
707
+ const exactReference = "(?:v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z.-]+)?|[a-f0-9]{40})";
708
+ if (source.startsWith("npm:")) {
709
+ const separator = source.lastIndexOf("@");
710
+ const packageName = separator > 4 ? source.slice(4, separator) : "";
711
+ const reference = separator > 4 ? source.slice(separator + 1) : "";
712
+ const validSegment = (segment) => /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/.test(segment) && segment !== "." && segment !== "..";
713
+ const validName = packageName.startsWith("@")
714
+ ? (() => {
715
+ const segments = packageName.slice(1).split("/");
716
+ return segments.length === 2 && segments.every(validSegment);
717
+ })()
718
+ : !packageName.includes("/") && validSegment(packageName);
719
+ if (!validName || !new RegExp(`^${exactReference}$`).test(reference)) {
720
+ throw new CapabilityValidationError("npm package source must use a valid lowercase package name and an exact version");
721
+ }
722
+ }
723
+ if (source.startsWith("git:")) {
724
+ const separator = source.lastIndexOf("@");
725
+ const location = separator > 4 ? source.slice(4, separator) : "";
726
+ const reference = separator > 4 ? source.slice(separator + 1) : "";
727
+ const segments = location.split("/");
728
+ const hostLabels = (segments[0] ?? "").split(".");
729
+ const validHost = hostLabels.length > 0 && hostLabels.every((label) => /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label));
730
+ const validRepository = segments.length >= 2 && segments.slice(1).every((segment) => /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(segment) && segment !== "." && segment !== "..");
731
+ if (!validHost || !validRepository || !new RegExp(`^${exactReference}$`).test(reference)) {
732
+ throw new CapabilityValidationError("git package source must use a valid host/repository and an exact tag or commit");
733
+ }
734
+ }
735
+ if (/^https?:/.test(source)) {
736
+ let remote;
737
+ try {
738
+ remote = new URL(source);
739
+ } catch {
740
+ throw new CapabilityValidationError("HTTP package source must be a valid URL");
741
+ }
742
+ let decodedPath;
743
+ try {
744
+ decodedPath = decodeURIComponent(remote.pathname);
745
+ } catch {
746
+ throw new CapabilityValidationError("HTTP package source path must use valid encoding");
747
+ }
748
+ const pathSegments = decodedPath.split("/").filter(Boolean);
749
+ const validPath = pathSegments.length > 0 && pathSegments.every((segment) => /^[A-Za-z0-9](?:[A-Za-z0-9._@-]*[A-Za-z0-9])?$/.test(segment) && segment !== "." && segment !== "..");
750
+ const validHost = remote.hostname.split(".").every((label) => /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label));
751
+ if (/\s|\/(?:\.{1,2}|%2e(?:%2e)?)(?:\/|$)/i.test(source) || !validHost || remote.username || remote.password || remote.search || !validPath) {
752
+ throw new CapabilityValidationError("HTTP package source must use a credential-free, whitespace-free host and repository path");
753
+ }
754
+ if (!new RegExp(`(?:#|@|/tags/|/archive/(?:refs/tags/)?)${exactReference}(?:\\.tar\\.gz)?$`).test(source)) {
755
+ throw new CapabilityValidationError("HTTP package source must use an exact tag, commit, or versioned archive");
756
+ }
757
+ }
758
+ if (/^[a-z][a-z0-9+.-]*:/i.test(source) && !/^(?:npm|git|https?):/.test(source)) {
759
+ throw new CapabilityValidationError("package source scheme is not supported");
760
+ }
761
+ const localSource = source === "workspace" || path.isAbsolute(source) || source.startsWith("./") || source.startsWith("../");
762
+ const exactRemoteSource = /^(?:npm|git|https?):/.test(source);
763
+ if (!localSource && !exactRemoteSource) throw new CapabilityValidationError("package source must be a local path or an exact supported remote source");
764
+ return source;
765
+ }
766
+
767
+ function readBasePolicyRestrictions(root) {
768
+ const policyPath = path.join(root, "packages", "piagent-core", "policies", "base-policy.json");
769
+ if (!fs.existsSync(policyPath)) return { protectedPaths: [], shellProtectedPaths: [] };
770
+ const policy = readJsonFile(policyPath);
771
+ const protectedPaths = Array.isArray(policy.protectedPaths) ? policy.protectedPaths : [];
772
+ const shellProtectedPaths = Array.isArray(policy.shellProtectedPaths) ? policy.shellProtectedPaths : protectedPaths;
773
+ if (![...protectedPaths, ...shellProtectedPaths].every((item) => typeof item === "string" && isSafeRelativePath(item, { allowGlob: true }))) {
774
+ throw new CapabilityValidationError("base policy contains an invalid protected path");
775
+ }
776
+ return { protectedPaths, shellProtectedPaths };
777
+ }
778
+
779
+ function buildCoreIntegrity(root) {
780
+ const files = [
781
+ "package.json",
782
+ "packages/piagent-core/package.json",
783
+ "packages/piagent-core/policies/base-policy.json",
784
+ "packages/piagent-core/capabilities/capability-core.js",
785
+ // capability-sources.js decides which external trees become readable packs,
786
+ // so tampering with it changes which code the profile can grant.
787
+ "packages/piagent-core/capabilities/capability-sources.js",
788
+ "packages/piagent-core/security/sensitive-data.js",
789
+ "packages/piagent-core/extensions/piagent-guard.ts",
790
+ // guard-io.js loads the project profile and guard-shell-analysis.ts decides
791
+ // what a command is about to reach, so tampering with either changes what
792
+ // the guard enforces. guard-types.ts is deliberately absent: it is erased
793
+ // before execution and cannot alter runtime behaviour.
794
+ "packages/piagent-core/extensions/guard-io.js",
795
+ "packages/piagent-core/extensions/guard-shell-analysis.ts",
796
+ "packages/piagent-core/extensions/policy-core.js",
797
+ "packages/piagent-core/extensions/redaction-core.js",
798
+ "packages/piagent-core/extensions/runtime-evidence.js"
799
+ ];
800
+ return files.map((relativePath) => {
801
+ const resolved = resolveRegularFile(root, relativePath, MAX_ARTIFACT_BYTES);
802
+ return {
803
+ path: relativePath,
804
+ digest: sha256(fs.readFileSync(resolved.absolutePath)),
805
+ byteSize: resolved.byteSize
806
+ };
807
+ });
808
+ }
809
+
810
+ export function resolveCapabilityProfileDocument(root, profile, options = {}) {
811
+ const rootReal = fs.realpathSync(root);
812
+ const profileFile = options.profileFile ?? "piagent-profile.json";
813
+ const packageSource = validateCapabilityPackageSource(options.packageSource);
814
+ validateProfileSelection(profile, profileFile);
815
+ const catalog = buildCapabilityCatalog(rootReal, { extraRoots: options.extraRoots });
816
+ const byKey = new Map(catalog.packs.map((pack) => [`${pack.name}@${pack.version}`, pack]));
817
+ const selected = new Map();
818
+ const visiting = new Set();
819
+ function select(key) {
820
+ if (selected.has(key)) return;
821
+ if (visiting.has(key)) throw new CapabilityValidationError(`dependency cycle detected while resolving ${key}`);
822
+ const pack = byKey.get(key);
823
+ if (!pack) throw new CapabilityValidationError(`profile selects unknown capability pack ${key}`);
824
+ visiting.add(key);
825
+ for (const dependency of pack.dependencies) select(`${dependency.name}@${dependency.version}`);
826
+ visiting.delete(key);
827
+ selected.set(key, pack);
828
+ }
829
+ for (const selector of [...(profile.capabilityPacks ?? [])].sort((left, right) => compareCodePoints(`${left.name}@${left.version}`, `${right.name}@${right.version}`))) {
830
+ const key = `${selector.name}@${selector.version}`;
831
+ const directPack = byKey.get(key);
832
+ if (!directPack) throw new CapabilityValidationError(`profile selects unknown capability pack ${key}`);
833
+ if (directPack.activation.mode === "profile" && !directPack.activation.profiles.includes(profile.mode)) throw new CapabilityValidationError(`${key} is not enabled for profile mode ${profile.mode}`);
834
+ select(key);
835
+ }
836
+
837
+ // Every list defaults to empty, so a profile that declares only some of them
838
+ // denies the rest. Reading a missing list directly would throw a TypeError
839
+ // instead of refusing, which is the same outcome by accident rather than by
840
+ // rule.
841
+ const declaredPolicy = profile.capabilityPolicy ?? {};
842
+ const policy = Object.fromEntries(
843
+ ["allowedOwners", "allowedLifecycles", "allowedFilesystemRead", "allowedFilesystemWrite", "allowedNetworkDomains", "allowedExternalActions"]
844
+ .map((key) => [key, Array.isArray(declaredPolicy[key]) ? declaredPolicy[key] : []])
845
+ );
846
+ const profileCapabilities = new Set(profile.mcpCapabilities ?? []);
847
+ const basePolicy = readBasePolicyRestrictions(rootReal);
848
+ const permissions = {
849
+ capabilities: new Set(),
850
+ filesystemRead: new Set(),
851
+ filesystemWrite: new Set(),
852
+ protectedPaths: new Set([...basePolicy.protectedPaths, ...(profile.protectedPaths ?? [])]),
853
+ readOnlyPaths: new Set(profile.readOnlyPaths ?? []),
854
+ shellProtectedPaths: new Set([...basePolicy.shellProtectedPaths, ...((profile.shellProtectedPaths ?? profile.protectedPaths) ?? []), ...(profile.readOnlyPaths ?? [])]),
855
+ networkDomains: new Set(),
856
+ externalActions: new Set()
857
+ };
858
+ for (const pack of selected.values()) {
859
+ if (!policy.allowedOwners.includes(pack.owner)) throw new CapabilityValidationError(`profile does not allow capability owner ${pack.owner}`);
860
+ if (!policy.allowedLifecycles.includes(pack.lifecycle)) throw new CapabilityValidationError(`profile does not allow ${pack.lifecycle} capability packs`);
861
+ for (const capability of pack.permissions.capabilities) {
862
+ if (!profileCapabilities.has(capability)) throw new CapabilityValidationError(`profile does not grant required capability ${capability}`);
863
+ permissions.capabilities.add(capability);
864
+ }
865
+ for (const item of pack.permissions.filesystemRead) {
866
+ if (!policy.allowedFilesystemRead.includes(item)) throw new CapabilityValidationError(`profile does not allow filesystem read scope ${item}`);
867
+ permissions.filesystemRead.add(item);
868
+ }
869
+ for (const item of pack.permissions.filesystemWrite) {
870
+ if (!policy.allowedFilesystemWrite.includes(item)) throw new CapabilityValidationError(`profile does not allow filesystem write scope ${item}`);
871
+ permissions.filesystemWrite.add(item);
872
+ }
873
+ for (const domain of pack.permissions.networkDomains) {
874
+ if (!policy.allowedNetworkDomains.includes(domain)) throw new CapabilityValidationError(`profile does not allow network domain ${domain}`);
875
+ permissions.networkDomains.add(domain);
876
+ }
877
+ for (const action of pack.permissions.externalActions) {
878
+ if (!policy.allowedExternalActions.includes(action)) throw new CapabilityValidationError(`profile does not allow external action ${action}`);
879
+ permissions.externalActions.add(action);
880
+ }
881
+ }
882
+
883
+ const packageDocument = readJsonFile(path.join(rootReal, "package.json"));
884
+ const runtimeFiles = buildCoreIntegrity(rootReal);
885
+ return {
886
+ schemaVersion: 1,
887
+ core: {
888
+ apiVersion: CORE_API_VERSION,
889
+ packageVersion: packageDocument.version,
890
+ packageSource,
891
+ runtimeFiles,
892
+ packageDigest: sha256(stableJson({
893
+ name: packageDocument.name,
894
+ version: packageDocument.version,
895
+ catalog,
896
+ runtimeFiles
897
+ }))
898
+ },
899
+ profile: {
900
+ projectId: profile.projectId,
901
+ mode: profile.mode,
902
+ file: profileFile,
903
+ digest: sha256(stableJson(profile))
904
+ },
905
+ packs: [...selected.values()].map((pack) => ({
906
+ name: pack.name,
907
+ version: pack.version,
908
+ // Where the pack came from is part of what the lock pins. A pack that
909
+ // silently moves between sources is a substitution, not an update.
910
+ origin: pack.origin,
911
+ source: pack.source,
912
+ owner: pack.owner,
913
+ lifecycle: pack.lifecycle,
914
+ digest: pack.digest
915
+ })),
916
+ permissions: Object.fromEntries(Object.entries(permissions).map(([key, values]) => [key, [...values].sort()])),
917
+ artifacts: [...selected.values()].flatMap((pack) => pack.artifacts.map((artifact) => ({
918
+ pack: `${pack.name}@${pack.version}`,
919
+ ...artifact
920
+ }))).sort(compareArtifact)
921
+ };
922
+ }
923
+
924
+ export function resolveCapabilityProfile(root, profilePath, options = {}) {
925
+ const profileAbsolute = fs.realpathSync(profilePath);
926
+ const profile = readJsonFile(profileAbsolute);
927
+ return resolveCapabilityProfileDocument(root, profile, {
928
+ profileFile: path.basename(profileAbsolute),
929
+ packageSource: options.packageSource,
930
+ extraRoots: options.extraRoots
931
+ });
932
+ }
933
+
934
+ export function verifyCapabilityLock(root, profilePath, lockDocument, options = {}) {
935
+ const expected = resolveCapabilityProfile(root, profilePath, {
936
+ packageSource: options.packageSource,
937
+ extraRoots: options.extraRoots
938
+ });
939
+ const expectedText = stableJson(expected);
940
+ const actualText = stableJson(lockDocument);
941
+ return {
942
+ ok: crypto.timingSafeEqual(Buffer.from(sha256(expectedText)), Buffer.from(sha256(actualText))),
943
+ expectedDigest: sha256(expectedText),
944
+ actualDigest: sha256(actualText),
945
+ expected
946
+ };
947
+ }
948
+
949
+ function assertRegularJsonTarget(target) {
950
+ const parent = path.dirname(target);
951
+ fs.mkdirSync(parent, { recursive: true, mode: 0o755 });
952
+ const parentStat = fs.lstatSync(parent);
953
+ if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) throw new CapabilityValidationError(`${parent} must be a regular directory`);
954
+ if (fs.existsSync(target)) {
955
+ const targetStat = fs.lstatSync(target);
956
+ if (targetStat.isSymbolicLink() || !targetStat.isFile()) throw new CapabilityValidationError(`${target} must be a regular file and not a symbolic link`);
957
+ }
958
+ return parent;
959
+ }
960
+
961
+ function writeTextAtomic(target, text) {
962
+ const parent = assertRegularJsonTarget(target);
963
+ const temporary = path.join(parent, `.${path.basename(target)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`);
964
+ let descriptor;
965
+ try {
966
+ descriptor = fs.openSync(temporary, "wx", 0o644);
967
+ fs.writeFileSync(descriptor, text, "utf8");
968
+ fs.fsyncSync(descriptor);
969
+ fs.closeSync(descriptor);
970
+ descriptor = undefined;
971
+ fs.renameSync(temporary, target);
972
+ try {
973
+ const directoryDescriptor = fs.openSync(parent, "r");
974
+ try {
975
+ fs.fsyncSync(directoryDescriptor);
976
+ } finally {
977
+ fs.closeSync(directoryDescriptor);
978
+ }
979
+ } catch {
980
+ // The file rename remains atomic on platforms that do not support directory fsync.
981
+ }
982
+ } finally {
983
+ if (descriptor !== undefined) fs.closeSync(descriptor);
984
+ if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
985
+ }
986
+ }
987
+
988
+ export function writeJsonAtomic(target, value) {
989
+ writeTextAtomic(target, stableJson(value));
990
+ }
991
+
992
+ export function writeProfileLockAtomic(profileTarget, profileDocument, lockTarget, lockDocument) {
993
+ const profileParent = path.resolve(path.dirname(profileTarget));
994
+ const lockParent = path.resolve(path.dirname(lockTarget));
995
+ if (profileParent !== lockParent) throw new CapabilityValidationError("profile and lock must be written in the same directory");
996
+ assertRegularJsonTarget(profileTarget);
997
+ assertRegularJsonTarget(lockTarget);
998
+
999
+ if (fs.existsSync(lockTarget) && fs.statSync(lockTarget).size > MAX_JSON_BYTES) {
1000
+ throw new CapabilityValidationError(`${lockTarget} exceeds the ${MAX_JSON_BYTES}-byte recovery limit`);
1001
+ }
1002
+ const previousLock = fs.existsSync(lockTarget) ? fs.readFileSync(lockTarget, "utf8") : undefined;
1003
+ writeJsonAtomic(lockTarget, lockDocument);
1004
+ try {
1005
+ writeJsonAtomic(profileTarget, profileDocument);
1006
+ } catch (error) {
1007
+ try {
1008
+ if (previousLock === undefined) fs.unlinkSync(lockTarget);
1009
+ else writeTextAtomic(lockTarget, previousLock);
1010
+ } catch (rollbackError) {
1011
+ const primaryMessage = error instanceof Error ? error.message : String(error);
1012
+ const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
1013
+ throw new CapabilityValidationError("profile update failed and lock rollback did not complete", [primaryMessage, rollbackMessage]);
1014
+ }
1015
+ throw error;
1016
+ }
1017
+ }
1018
+
1019
+ export const capabilityConstants = Object.freeze({
1020
+ apiVersion: API_VERSION,
1021
+ coreApiVersion: CORE_API_VERSION,
1022
+ maxArtifactBytes: MAX_ARTIFACT_BYTES
1023
+ });