@uchibeke/gallivant 0.0.0-stage → 0.2.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 (168) hide show
  1. package/CONTRIBUTING.md +26 -0
  2. package/LICENSE +204 -0
  3. package/README.md +201 -2
  4. package/SECURITY.md +19 -0
  5. package/bin/gallivant.mjs +257 -0
  6. package/bin/package-plugin.mjs +16 -0
  7. package/bin/release-live.mjs +29 -0
  8. package/bin/release-package.mjs +23 -0
  9. package/bin/session-runtime.mjs +13 -0
  10. package/bin/task-delivery-state.mjs +12 -0
  11. package/docs/README.md +70 -0
  12. package/docs/agent-setup.md +214 -0
  13. package/docs/app.md +111 -0
  14. package/docs/architecture.md +265 -0
  15. package/docs/chains.md +102 -0
  16. package/docs/codex-cloud.md +284 -0
  17. package/docs/commands.md +620 -0
  18. package/docs/config.md +430 -0
  19. package/docs/execution.md +796 -0
  20. package/docs/guardrails.md +311 -0
  21. package/docs/install.md +338 -0
  22. package/docs/launch.md +42 -0
  23. package/docs/learning.md +369 -0
  24. package/docs/network-recovery.md +101 -0
  25. package/docs/openai-agents.md +457 -0
  26. package/docs/prd.md +246 -0
  27. package/docs/preparation.md +110 -0
  28. package/docs/release-qualification.md +196 -0
  29. package/docs/releasing.md +140 -0
  30. package/docs/runtime-environment.md +113 -0
  31. package/docs/task-delivery.md +476 -0
  32. package/docs/tasks.md +739 -0
  33. package/lib/aport.mjs +95 -0
  34. package/lib/attribution.mjs +95 -0
  35. package/lib/block.mjs +89 -0
  36. package/lib/capabilities.mjs +3 -0
  37. package/lib/chains.mjs +222 -0
  38. package/lib/codex_cloud.mjs +160 -0
  39. package/lib/codex_cloud_config.mjs +20 -0
  40. package/lib/codex_cloud_input.mjs +49 -0
  41. package/lib/codex_cloud_result.mjs +74 -0
  42. package/lib/commands/doctor.mjs +457 -0
  43. package/lib/commands/init.mjs +34 -0
  44. package/lib/commands/learn.mjs +54 -0
  45. package/lib/commands/new.mjs +47 -0
  46. package/lib/commands/ready.mjs +45 -0
  47. package/lib/commands/register.mjs +195 -0
  48. package/lib/commands/resume.mjs +224 -0
  49. package/lib/commands/serve.mjs +349 -0
  50. package/lib/commands/start.mjs +247 -0
  51. package/lib/commands/start_services.mjs +22 -0
  52. package/lib/commands/status.mjs +150 -0
  53. package/lib/commands/task.mjs +73 -0
  54. package/lib/commands/up.mjs +228 -0
  55. package/lib/commands/why.mjs +143 -0
  56. package/lib/config.mjs +461 -0
  57. package/lib/context.mjs +100 -0
  58. package/lib/control.mjs +202 -0
  59. package/lib/control_contract.mjs +16 -0
  60. package/lib/coordinator.mjs +19 -0
  61. package/lib/credential.mjs +161 -0
  62. package/lib/daemon.mjs +156 -0
  63. package/lib/deliverable.mjs +357 -0
  64. package/lib/events.mjs +261 -0
  65. package/lib/executable.mjs +31 -0
  66. package/lib/execution.mjs +631 -0
  67. package/lib/execution_contract.mjs +15 -0
  68. package/lib/execution_input.mjs +4 -0
  69. package/lib/execution_io.mjs +94 -0
  70. package/lib/execution_journal.mjs +69 -0
  71. package/lib/execution_preparation.mjs +29 -0
  72. package/lib/execution_result.mjs +266 -0
  73. package/lib/fs.mjs +133 -0
  74. package/lib/gh.mjs +289 -0
  75. package/lib/git.mjs +434 -0
  76. package/lib/harness.mjs +521 -0
  77. package/lib/harness_log.mjs +432 -0
  78. package/lib/hash.mjs +16 -0
  79. package/lib/identity.mjs +98 -0
  80. package/lib/learning.mjs +654 -0
  81. package/lib/learning_contract.mjs +2 -0
  82. package/lib/learning_data.mjs +135 -0
  83. package/lib/learning_input.mjs +111 -0
  84. package/lib/learning_qualification.mjs +67 -0
  85. package/lib/learning_signer.mjs +151 -0
  86. package/lib/learning_store.mjs +88 -0
  87. package/lib/log.mjs +53 -0
  88. package/lib/network_diagnostics.mjs +64 -0
  89. package/lib/network_recovery.mjs +42 -0
  90. package/lib/notify.mjs +58 -0
  91. package/lib/openai_agents.mjs +373 -0
  92. package/lib/openai_agents_config.mjs +14 -0
  93. package/lib/openai_agents_http.mjs +87 -0
  94. package/lib/openai_agents_input.mjs +15 -0
  95. package/lib/openai_agents_sources.mjs +112 -0
  96. package/lib/owner_delivery.mjs +443 -0
  97. package/lib/plugin_package.mjs +340 -0
  98. package/lib/prd.mjs +635 -0
  99. package/lib/preparation_agents.mjs +33 -0
  100. package/lib/preparation_codex.mjs +212 -0
  101. package/lib/probe.mjs +126 -0
  102. package/lib/prompt.mjs +238 -0
  103. package/lib/provider_health.mjs +55 -0
  104. package/lib/public-format.mjs +31 -0
  105. package/lib/public_output.mjs +49 -0
  106. package/lib/release_package.mjs +154 -0
  107. package/lib/repository_files.mjs +86 -0
  108. package/lib/run.mjs +385 -0
  109. package/lib/run_log.mjs +94 -0
  110. package/lib/runtime-environment.mjs +87 -0
  111. package/lib/runtime_dependencies.mjs +15 -0
  112. package/lib/runtime_node.mjs +107 -0
  113. package/lib/runtime_state.mjs +174 -0
  114. package/lib/sources/index.mjs +27 -0
  115. package/lib/sources/prd.mjs +50 -0
  116. package/lib/start_contract.mjs +37 -0
  117. package/lib/state.mjs +246 -0
  118. package/lib/task_allowance.mjs +31 -0
  119. package/lib/task_completion.mjs +61 -0
  120. package/lib/task_contract.mjs +450 -0
  121. package/lib/task_delivery.mjs +219 -0
  122. package/lib/task_delivery_cancel.mjs +34 -0
  123. package/lib/task_delivery_contract.mjs +61 -0
  124. package/lib/task_delivery_mcp.mjs +40 -0
  125. package/lib/task_delivery_store.mjs +43 -0
  126. package/lib/task_delivery_view.mjs +88 -0
  127. package/lib/task_environment.mjs +281 -0
  128. package/lib/task_prepare.mjs +224 -0
  129. package/lib/task_store.mjs +38 -0
  130. package/lib/tasks.mjs +513 -0
  131. package/lib/tick.mjs +1635 -0
  132. package/lib/time.mjs +36 -0
  133. package/lib/tracker/assets.mjs +43 -0
  134. package/lib/tracker/pages.mjs +320 -0
  135. package/lib/tracker/runs.mjs +102 -0
  136. package/lib/tracker/stream.mjs +74 -0
  137. package/lib/tracker_process.mjs +107 -0
  138. package/lib/verification_sandbox.mjs +181 -0
  139. package/node_modules/smol-toml/LICENSE +24 -0
  140. package/node_modules/smol-toml/README.md +605 -0
  141. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  142. package/node_modules/smol-toml/dist/date.js +156 -0
  143. package/node_modules/smol-toml/dist/error.d.ts +37 -0
  144. package/node_modules/smol-toml/dist/error.js +67 -0
  145. package/node_modules/smol-toml/dist/extract.js +289 -0
  146. package/node_modules/smol-toml/dist/index.cjs +770 -0
  147. package/node_modules/smol-toml/dist/index.d.ts +44 -0
  148. package/node_modules/smol-toml/dist/index.js +34 -0
  149. package/node_modules/smol-toml/dist/parse.d.ts +73 -0
  150. package/node_modules/smol-toml/dist/parse.js +164 -0
  151. package/node_modules/smol-toml/dist/primitive.js +151 -0
  152. package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
  153. package/node_modules/smol-toml/dist/stringify.js +219 -0
  154. package/node_modules/smol-toml/dist/struct.js +156 -0
  155. package/node_modules/smol-toml/dist/util.d.ts +43 -0
  156. package/node_modules/smol-toml/dist/util.js +57 -0
  157. package/node_modules/smol-toml/package.json +68 -0
  158. package/npm-shrinkwrap.json +1823 -0
  159. package/package.json +105 -4
  160. package/templates/claude-settings.json +18 -0
  161. package/templates/passport.gallivant.json +83 -0
  162. package/templates/plugin/bundled-runtime.json +26 -0
  163. package/templates/plugin/schemas/mcp.schema.json +120 -0
  164. package/templates/plugin/schemas/plugin.schema.json +65 -0
  165. package/templates/plugin/task-delivery.SKILL.md +60 -0
  166. package/templates/prd.md +32 -0
  167. package/templates/tracker/tracker.css +301 -0
  168. package/templates/tracker/tracker.js +721 -0
@@ -0,0 +1,340 @@
1
+ import { join, dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { readFileSync, writeFileSync, mkdirSync, lstatSync, realpathSync, readdirSync, utimesSync, chmodSync, rmSync } from "node:fs";
4
+ import { execFileSync } from "node:child_process";
5
+ import { createHash } from "node:crypto";
6
+ import { canonicalPath } from "./fs.mjs";
7
+ import { PNG } from "pngjs";
8
+ import Ajv2020 from "ajv/dist/2020.js";
9
+ import { CORE_VERSION, TASK_OPERATIONS, DELIVERY_VERSION, MAX_TASK_UI_BYTES, uiResourceConfiguration, deliveryError } from "./task_delivery_contract.mjs";
10
+ import { NODE_RUNTIME_REQUIREMENTS } from "./runtime_node.mjs";
11
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const sha = bytes => createHash("sha256").update(bytes).digest("hex");
13
+ const json = value => JSON.stringify(value, null, 2) + "\n";
14
+ // APort's emitted, self-contained MCP resource includes the shared React UI and
15
+ // its CSP-bound stylesheet. Match its bounded resource endpoint, not a tiny
16
+ // fixture's size. Icons and screenshots retain their independent limits.
17
+ const MAX_UI_BYTES = MAX_TASK_UI_BYTES;
18
+ // APort advertises fixed-action controls over the core's generic dispatcher.
19
+ // Accept those documented host names without accepting arbitrary task_* tools.
20
+ const reviewTools = new Set([
21
+ ...TASK_OPERATIONS.map(operation => `task_${operation.name}`),
22
+ "task_recheck", "task_continue", "task_message", "task_interrupt", "task_cancel",
23
+ ]);
24
+ // Main bundles its locked TOML parser for isolated Codex preparation. Keep the
25
+ // dependency exception narrow; do not admit arbitrary node_modules contents.
26
+ const bundledRuntimeFile = path => /^node_modules\/smol-toml\/(?:package\.json|README\.md|LICENSE|dist\/[A-Za-z0-9_-]+\.(?:js|cjs|d\.ts))$/.test(path);
27
+ // npm applies nested ignore files independently of Git's tracked payload.
28
+ export const packagingControl = path => /^(?:(?:bin|lib|templates)\/.*|docs\/)?\.(?:npmignore|gitignore|npmrc)$/.test(path);
29
+ export const runtimeFile = path => !packagingControl(path) && (/^(?:bin\/.*\.mjs|lib\/.*\.mjs|templates\/[^.].*|docs\/[^/]+\.md|README\.md|CONTRIBUTING\.md|SECURITY\.md|LICENSE|package\.json|npm-shrinkwrap\.json)$/.test(path) || bundledRuntimeFile(path));
30
+ export const payloadChange = path => runtimeFile(path) || packagingControl(path) || /^(?:bin|lib|templates)\/(?:.*\/)?$/.test(path);
31
+ export function packagingControlInventory(sourceRoot, sourceCommit) {
32
+ try {
33
+ const paths = execFileSync("git", ["ls-tree", "-r", "-z", "--name-only", sourceCommit], { cwd: sourceRoot, encoding: "utf8", maxBuffer: 16777216 }).split("\0").filter(packagingControl).sort();
34
+ return Object.fromEntries(paths.map(path => [path, sha(execFileSync("git", ["show", `${sourceCommit}:${path}`], { cwd: sourceRoot, maxBuffer: 1048576 }))]));
35
+ } catch { throw deliveryError("PACKAGE_SOURCE", "Cannot inventory committed packaging controls; restore Git access and rebuild"); }
36
+ }
37
+ const ajv = new Ajv2020({ strict: true });
38
+ const schemas = Object.fromEntries(["plugin", "mcp"].map(name => [name, JSON.parse(readFileSync(join(root, "templates/plugin/schemas", `${name}.schema.json`), "utf8"))]));
39
+ // The portable MCP 1.0 schema predates the documented OpenAI auth extension.
40
+ // Keep that vendored source intact and validate only this explicitly supported
41
+ // overlay, rather than allowing arbitrary server fields or secret headers.
42
+ // https://developers.openai.com/plugins/build/plugins#configure-mcp-authentication
43
+ const boundedText = { type: "string", minLength: 1, maxLength: 4000, pattern: "\\S" };
44
+ const strictObject = (properties, required = Object.keys(properties)) => ({ type: "object", properties, required, additionalProperties: false });
45
+ const scopeList = { type: "array", maxItems: 32, uniqueItems: true, items: { type: "string", minLength: 1, maxLength: 128, pattern: "^[\\x21\\x23-\\x5b\\x5d-\\x7e]+$" } };
46
+ const oauthSchema = strictObject({
47
+ type: { const: "oauth" },
48
+ client: strictObject({ mode: { const: "provided" }, clientId: { type: "string", pattern: "^[A-Za-z0-9_-]{1,128}$" }, tokenEndpointAuthMethod: { const: "none" } }),
49
+ authorizationUrl: boundedText, tokenUrl: boundedText,
50
+ // OAuth scopes belong to the public authorization server, not Gallivant's
51
+ // internal delivery scope vocabulary. The deployer supplies its canonical
52
+ // discovery/tool scopes; validate RFC 6749 scope-token syntax without mapping.
53
+ baseScopes: { ...scopeList, minItems: 1 },
54
+ // An explicit empty list suppresses host/client defaults while baseScopes
55
+ // remains included on every request. Omitting this preserves host defaults.
56
+ defaultScopes: scopeList,
57
+ }, ["type", "client", "baseScopes"]);
58
+ const caseProperties = { description: boundedText, prompt: boundedText, tools_triggered: boundedText, expected_behavior: boundedText,
59
+ file_attachment_urls: { type: "array", maxItems: 10, items: boundedText }, expected_output_url: boundedText };
60
+ const reviewSchema = strictObject({
61
+ test_cases: strictObject({
62
+ positive: { type: "array", minItems: 5, maxItems: 5, items: strictObject(caseProperties, ["description", "prompt", "tools_triggered", "expected_behavior"]) },
63
+ negative: { type: "array", minItems: 3, maxItems: 3, items: strictObject(caseProperties, ["description", "prompt"]) },
64
+ }), demo_recording_url: boundedText,
65
+ });
66
+ const publicationSchema = strictObject({ release_notes: boundedText });
67
+ const listingSchema = strictObject({
68
+ longDescription: { ...boundedText, pattern: "^(?=[\\s\\S]*\\S)[^\\x00-\\x09\\x0b-\\x1f\\x7f]+$" },
69
+ // Use the exact category title offered by the current publisher dashboard.
70
+ category: { type: "string", minLength: 1, maxLength: 80, pattern: "^(?=.*\\S)[^\\x00-\\x1f\\x7f]+$" },
71
+ });
72
+ const submissionSchemas = { oauth: oauthSchema, review: reviewSchema, publication: publicationSchema, listing: listingSchema };
73
+ const submissionChecks = Object.fromEntries(Object.entries(submissionSchemas).map(([name, schema]) => [name, ajv.compile(schema)]));
74
+ const openaiMcpSchema = structuredClone(schemas.mcp);
75
+ openaiMcpSchema.$defs.streamableHttpServer.properties.extensions = strictObject({ "com.openai": strictObject({ auth: oauthSchema }) });
76
+ const check = { plugin: ajv.compile(schemas.plugin), mcp: ajv.compile(openaiMcpSchema) };
77
+ // IANA special-purpose registries (2025-10-09). Fail closed for reserved,
78
+ // multicast and transition space without an unambiguous global destination.
79
+ // https://www.iana.org/assignments/iana-ipv4-special-registry
80
+ // https://www.iana.org/assignments/iana-ipv6-special-registry
81
+ const globalIpv4 = host => {
82
+ const [a, b, c, d] = host.split(".").map(Number);
83
+ if (a === 192 && b === 0 && c === 0 && [9, 10].includes(d)) return true;
84
+ return !(a === 0 || a === 10 || a === 127 || a >= 224 ||
85
+ a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 ||
86
+ a === 172 && b >= 16 && b <= 31 ||
87
+ a === 192 && (b === 168 || b === 0 && [0, 2].includes(c) || b === 88 && c === 99) ||
88
+ a === 198 && (b === 18 || b === 19 || b === 51 && c === 100) ||
89
+ a === 203 && b === 0 && c === 113);
90
+ };
91
+ const globalIpv6 = host => {
92
+ const [left, right] = host.slice(1, -1).split("::");
93
+ const head = left ? left.split(":") : [], tail = right ? right.split(":") : [];
94
+ const words = (right === undefined ? head : [...head, ...Array(8 - head.length - tail.length).fill("0"), ...tail]).map(x => parseInt(x, 16));
95
+ const [a, b, c] = words;
96
+ // Apply the IPv4 classification to mapped and well-known NAT64 addresses.
97
+ if (words.slice(0, 5).every(x => x === 0) && words[5] === 0xffff || a === 0x64 && b === 0xff9b && words.slice(2, 6).every(x => x === 0)) {
98
+ return globalIpv4(`${words[6] >> 8}.${words[6] & 255}.${words[7] >> 8}.${words[7] & 255}`);
99
+ }
100
+ if (a < 0x2000 || a > 0x3fff || a === 0x2002 || a === 0x2001 && b === 0xdb8 || a === 0x3fff && b < 0x1000) return false;
101
+ if (a === 0x2001 && b < 0x200) {
102
+ return b === 1 && words.slice(2, 7).every(x => x === 0) && [1, 2, 3].includes(words[7]) ||
103
+ b === 3 || b === 4 && c === 0x112 || b >= 0x20 && b <= 0x3f;
104
+ }
105
+ return true;
106
+ };
107
+ const publicLocalHost = hostname => {
108
+ const host = hostname.toLowerCase().replace(/\.$/, "");
109
+ // WHATWG URL parsing canonicalizes shorthand, decimal and hex IPv4, and IPv6.
110
+ if (host.startsWith("[")) return !globalIpv6(host);
111
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) return !globalIpv4(host);
112
+ return !host.includes(".") || host.length > 253 || !host.split(".").every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) || /(?:^|\.)(?:localhost|localhost\.localdomain|localhost6|localhost6\.localdomain6|ip6-localhost|ip6-loopback|localdomain|local|home\.arpa|onion|alt|internal|example|example\.(?:com|org|net)|invalid|test)$/.test(host);
113
+ };
114
+ function readAsset(name, path, limit, png = false) {
115
+ let stat;
116
+ try { stat = typeof path === "string" && path ? lstatSync(path) : null; } catch { /* report a bounded configuration error */ }
117
+ if (!stat?.isFile() || stat.isSymbolicLink()) throw deliveryError("PACKAGE_ASSET", `${name} must be a regular non-symlink file`);
118
+ const bytes = readFileSync(path);
119
+ if (bytes.length > limit || /\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,})\b|-----BEGIN .*PRIVATE KEY-----|Bearer\s+[A-Za-z0-9._-]+/.test(bytes.toString("utf8"))) throw deliveryError("PACKAGE_ASSET", `${name} exceeds its limit or contains credentials; remove secrets before packaging`);
120
+ if (png) {
121
+ try {
122
+ if (bytes.length < 33 || !bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) || bytes.toString("ascii", 12, 16) !== "IHDR") throw new Error("PNG header");
123
+ const width = bytes.readUInt32BE(16), height = bytes.readUInt32BE(20);
124
+ if (!width || !height || width * height > 16777216) throw new Error("PNG dimensions");
125
+ PNG.sync.read(bytes, { checkCRC: true });
126
+ } catch { throw deliveryError("PACKAGE_ASSET", `${name} must decode as a valid PNG of at most 16777216 pixels; export the image as PNG`); }
127
+ }
128
+ return bytes;
129
+ }
130
+ function screenshots(config) {
131
+ const paths = config.assets?.screenshots ?? [];
132
+ if (!Array.isArray(paths) || paths.length > 8) throw deliveryError("PACKAGE_ASSET", "assets.screenshots must be an array of at most eight authentic screenshot files");
133
+ return Array.from(paths);
134
+ }
135
+ const httpsUrl = (value, name, publicMode, allowQuery = false) => {
136
+ let u; try { u = new URL(value); } catch { throw deliveryError("PACKAGE_CONFIG", `Configure ${name} as an HTTPS URL`); }
137
+ if (u.protocol !== "https:" || u.username || u.password || !allowQuery && u.search || u.hash || publicMode && publicLocalHost(u.hostname)) throw deliveryError("PACKAGE_CONFIG", `${name} must be a real HTTPS URL without credentials${allowQuery ? " or fragment" : ", query or fragment"}`);
138
+ return value;
139
+ };
140
+
141
+ function submissionConfiguration(config, publicMode) {
142
+ const result = {};
143
+ for (const [name, validate] of Object.entries(submissionChecks)) {
144
+ const value = config[name];
145
+ if (value === undefined && (!publicMode || name === "listing")) continue;
146
+ if (!validate(value)) throw deliveryError("PACKAGE_CONFIG", `Configure ${name} using the documented public submission shape; unknown fields, credentials and incomplete review cases are not accepted`);
147
+ // Do not echo supplied values in errors: these inputs must never carry a
148
+ // credential, including inside an otherwise well-formed review description.
149
+ if (/\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,})\b|-----BEGIN .*PRIVATE KEY-----|Bearer\s+[A-Za-z0-9._-]+/.test(JSON.stringify(value))) throw deliveryError("PACKAGE_CONFIG", `Remove credentials from ${name}; use the secure portal for reviewer access`);
150
+ result[name] = value;
151
+ }
152
+ for (const name of ["authorizationUrl", "tokenUrl"]) if (result.oauth?.[name] !== undefined) httpsUrl(result.oauth[name], `oauth.${name}`, publicMode);
153
+ if (result.review) {
154
+ httpsUrl(result.review.demo_recording_url, "review.demo_recording_url", publicMode, true);
155
+ for (const group of ["positive", "negative"]) for (const item of result.review.test_cases[group]) {
156
+ for (const url of [...(item.file_attachment_urls || []), ...(item.expected_output_url ? [item.expected_output_url] : [])]) httpsUrl(url, "review case URL", publicMode, true);
157
+ if (item.tools_triggered) {
158
+ const tools = item.tools_triggered.split(",").map(value => value.trim());
159
+ if (tools.some(name => !reviewTools.has(name))) throw deliveryError("PACKAGE_CONFIG", "Review cases must name actual task tools separated by commas");
160
+ }
161
+ }
162
+ }
163
+ return result;
164
+ }
165
+
166
+ function pluginIdentity(config, internal) {
167
+ if (config.developmentIdentity === undefined) return { name: "aport-task-delivery", displayName: "APort Task Delivery" };
168
+ const value = config.developmentIdentity;
169
+ if (!internal || !value || typeof value !== "object" || Object.keys(value).some(key => !["name", "displayName"].includes(key)) ||
170
+ typeof value.name !== "string" || !/^[a-z][a-z0-9-]{0,48}-preview$/.test(value.name) ||
171
+ typeof value.displayName !== "string" || !/^[^\x00-\x1f\x7f]{1,64}$/.test(value.displayName) || !/preview/i.test(value.displayName)) {
172
+ throw deliveryError("PACKAGE_CONFIG", "developmentIdentity is only for internal builds and must use a distinct -preview name and Preview display name");
173
+ }
174
+ return value;
175
+ }
176
+
177
+ export function validatePublicRelease(config) {
178
+ const errors = [];
179
+ try { pluginIdentity(config, false); } catch (error) { errors.push(error.message); }
180
+ try { submissionConfiguration(config, true); } catch (error) { errors.push(error.message); }
181
+ if (config.publisher !== "APort") errors.push("Set publisher to APort");
182
+ if (!config.supportEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(config.supportEmail)) errors.push("Configure the publisher's supportEmail");
183
+ for (const name of ["endpoint", "homepage", "supportUrl", "privacyUrl", "termsUrl", "licenseUrl"]) {
184
+ try { httpsUrl(config[name], name, true); } catch (e) { errors.push(e.message); }
185
+ }
186
+ try {
187
+ uiResourceConfiguration(config.resource || {});
188
+ for (const [i, value] of [config.resource.domain, ...(config.resource.connectDomains || []), ...(config.resource.resourceDomains || [])].entries()) httpsUrl(value, `resource origin ${i}`, true);
189
+ } catch (e) { errors.push(e.message); }
190
+ if (config.fixture) errors.push("Fixture assets cannot qualify a public release; provide the built APort UI and authentic assets");
191
+ for (const [name, limit] of [["ui", MAX_UI_BYTES], ["icon", 1048576]]) {
192
+ try { readAsset(`assets.${name}`, config.assets?.[name], limit, name === "icon"); }
193
+ catch (error) { errors.push(error.message); }
194
+ }
195
+ try {
196
+ const paths = screenshots(config);
197
+ if (!paths.length) errors.push("Provide authentic host screenshots at assets.screenshots");
198
+ paths.forEach((path, i) => {
199
+ try { readAsset(`assets.screenshots[${i}]`, path, 5242880, true); }
200
+ catch (error) { errors.push(error.message); }
201
+ });
202
+ } catch (error) { errors.push(error.message); }
203
+ return { ok: !errors.length, errors, submissionReady: false, qualification: "M6 live/deployment/host/publisher gates remain mandatory" };
204
+ }
205
+
206
+ export function buildPluginPackage({ config, out, coreVersion, sourceCommit, internal = false }) {
207
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
208
+ if (coreVersion !== pkg.version || coreVersion !== CORE_VERSION) throw deliveryError("PACKAGE_VERSION", "Pass the exact installed core version; build runtime and plugin from that same package");
209
+ if (!/^[a-f0-9]{40}$/.test(sourceCommit || "")) throw deliveryError("PACKAGE_SOURCE", "Supply the exact 40-character source commit");
210
+ let sourceVerified = false, sourceChanges = [];
211
+ try {
212
+ const actualRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
213
+ if (realpathSync(actualRoot) === realpathSync(root)) {
214
+ const actualCommit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
215
+ if (actualCommit !== sourceCommit) throw deliveryError("PACKAGE_SOURCE", "Source commit differs from HEAD; build from the exact selected checkout");
216
+ // Separate additions/deletions keep both sides of a rename observable.
217
+ sourceChanges = execFileSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames", "--ignored=matching"], { cwd: root, encoding: "utf8" }).split("\0").filter(Boolean).map(record => record.slice(3));
218
+ sourceVerified = true;
219
+ }
220
+ } catch (error) {
221
+ if (error.code === "PACKAGE_SOURCE") throw error;
222
+ if (!internal) throw deliveryError("PACKAGE_SOURCE", "Git source verification failed; make HEAD and git status succeed in the exact selected checkout before public packaging");
223
+ }
224
+ if (!internal && !sourceVerified) throw deliveryError("PACKAGE_SOURCE", "Build the public candidate from the exact merged source checkout; an installed tarball cannot attest its supplied commit");
225
+ if (!internal) {
226
+ const verdict = validatePublicRelease(config);
227
+ if (!verdict.ok) throw deliveryError("PACKAGE_RELEASE", verdict.errors.join("\n"));
228
+ }
229
+ const submission = submissionConfiguration(config, !internal);
230
+ const identity = pluginIdentity(config, internal);
231
+ const endpoint = httpsUrl(config.endpoint, "endpoint", !internal), resource = uiResourceConfiguration(config.resource);
232
+ if (config.publisher !== "APort") throw deliveryError("PACKAGE_CONFIG", "APort is the publisher");
233
+ if (!config.assets?.ui) throw deliveryError("PACKAGE_ASSET", "Provide built APort UI HTML; --internal may use an explicitly labeled fixture");
234
+ out = canonicalPath(out);
235
+ const packageRoot = realpathSync(root);
236
+ if (out === packageRoot || out.startsWith(packageRoot + "/")) throw deliveryError("PACKAGE_OUTPUT", "Package output must be outside the source package root");
237
+ mkdirSync(out, { recursive: true });
238
+ if (readdirSync(out).length) throw deliveryError("PACKAGE_OUTPUT", "Use one empty bounded output directory; preserve older artifacts for rollback");
239
+ const stage = join(out, "plugin"); mkdirSync(stage);
240
+ try {
241
+ const hashes = {};
242
+ const emit = (path, bytes) => {
243
+ const file = join(stage, path); mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, bytes, { mode: 0o644 });
244
+ hashes[path] = sha(bytes); utimesSync(file, 946684800, 946684800);
245
+ };
246
+ const asset = (name, path, limit) => {
247
+ // Preflight and emission share file, size and secret constraints.
248
+ emit(name, readAsset(name, path, limit, name.endsWith(".png")));
249
+ };
250
+ asset("assets/task-ui.html", config.assets.ui, MAX_UI_BYTES);
251
+ if (config.assets.icon) asset("assets/icon.png", config.assets.icon, 1048576);
252
+ for (const [i, path] of screenshots(config).entries()) { asset(`assets/screenshot-${i + 1}.png`, path, 5242880); }
253
+ const plugin = { $schema: schemas.plugin.$id, name: identity.name, version: coreVersion,
254
+ description: "Prepare scoped tasks, follow implementation and review the resulting pull request.", author: { name: "APort", ...(config.supportEmail ? { email: config.supportEmail } : {}), ...(config.homepage ? { url: httpsUrl(config.homepage, "homepage", !internal) } : {}) },
255
+ ...(config.homepage ? { homepage: config.homepage } : {}), license: "Apache-2.0", keywords: ["tasks", "pull-request", "aport"],
256
+ extensions: { "com.openai": { interface: { displayName: identity.displayName, developerName: "APort", shortDescription: "Describe work and review a PR", longDescription: "Prepare scoped requirements, choose a configured harness, follow saved progress and review a pull request. Repository access and paid execution allowance require an authorized account.", category: "Productivity", ...submission.listing, capabilities: ["Read", "Write"],
257
+ ...(config.homepage ? { websiteURL: config.homepage } : {}), ...(config.privacyUrl ? { privacyPolicyURL: config.privacyUrl } : {}), ...(config.termsUrl ? { termsOfServiceURL: config.termsUrl } : {}),
258
+ ...(config.supportUrl ? { supportURL: config.supportUrl } : {}), ...(config.assets.icon ? { logo: "./assets/icon.png", composerIcon: "./assets/icon.png" } : {}),
259
+ ...(config.assets.screenshots?.length ? { screenshots: config.assets.screenshots.map((_, i) => `./assets/screenshot-${i + 1}.png`) } : {}), defaultPrompt: ["Prepare a task in my connected repository."] },
260
+ ...(submission.review ? { review: submission.review } : {}), ...(submission.publication ? { publication: submission.publication } : {}) } } };
261
+ const mcp = { $schema: schemas.mcp.$id, mcpServers: { "task-delivery": { type: "streamable-http", url: endpoint,
262
+ ...(submission.oauth ? { extensions: { "com.openai": { auth: submission.oauth } } } : {}) } } };
263
+ if (!check.plugin(plugin) || !check.mcp(mcp)) throw deliveryError("PACKAGE_SCHEMA", json(check.plugin.errors || check.mcp.errors));
264
+ emit("plugin.json", json(plugin)); emit("mcp.json", json(mcp)); emit("LICENSE", readFileSync(join(root, "LICENSE")));
265
+ emit("skills/task-delivery/SKILL.md", readFileSync(join(root, "templates/plugin/task-delivery.SKILL.md")));
266
+ const contracts = { coreVersion, operationVersion: DELIVERY_VERSION, taskVersion: 1, executionVersion: 1 };
267
+ const lock = readFileSync(join(root, "npm-shrinkwrap.json"));
268
+ // npm's allowlist is the runtime payload; tests install this emitted tarball.
269
+ const packed = JSON.parse(execFileSync("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", out], { cwd: root, encoding: "utf8", maxBuffer: 1048576 }))[0];
270
+ for (const file of packed.files) {
271
+ if (!runtimeFile(file.path) || /(?:^|\/)(?:\.env|auth\.json|credentials|config\.json)$/.test(file.path)) throw deliveryError("PACKAGE_CONTENT", `Unexpected runtime file ${file.path}; fix the package allowlist`);
272
+ }
273
+ // Packing runs outside Git's snapshot. Recheck both identity and dirtiness
274
+ // before attesting the tarball; a late edit must not inherit a clean claim.
275
+ if (sourceVerified) {
276
+ try {
277
+ if (execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim() !== sourceCommit) throw new Error("HEAD changed during packing");
278
+ const after = execFileSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames", "--ignored=matching"], { cwd: root, encoding: "utf8" }).split("\0").filter(Boolean).map(record => record.slice(3));
279
+ sourceChanges = [...new Set([...sourceChanges, ...after])];
280
+ } catch {
281
+ sourceVerified = false;
282
+ if (!internal) throw deliveryError("PACKAGE_SOURCE", "Git source verification failed after packing; rebuild from an unchanged selected checkout");
283
+ }
284
+ }
285
+ // Removed payload files are absent from npm's current file list but still
286
+ // invalidate the claimed source commit. Use the same runtime payload policy.
287
+ const payloadChanges = sourceChanges.filter(payloadChange);
288
+ if (!internal && payloadChanges.length) throw deliveryError("PACKAGE_SOURCE", "Commit the package payload before public packaging; executable, recipe, asset or npm packaging-control changes invalidate candidate evidence");
289
+ // Keep control contents out of public artifacts; persist only paths and hashes.
290
+ const packagingControlHashes = sourceVerified ? packagingControlInventory(root, sourceCommit) : null;
291
+ // Hash the emitted bytes, never a checkout that can change during packing.
292
+ const payloadHashes = {};
293
+ let bundledProof;
294
+ if (sourceVerified && !payloadChanges.length) {
295
+ try { bundledProof = JSON.parse(execFileSync("git", ["show", `${sourceCommit}:templates/plugin/bundled-runtime.json`], { cwd: root, encoding: "utf8" })); }
296
+ catch { throw deliveryError("PACKAGE_SOURCE", "Selected commit lacks bundled runtime byte evidence; rebuild with its committed dependency manifest"); }
297
+ const dependency = JSON.parse(lock).packages["node_modules/smol-toml"];
298
+ if (bundledProof.version !== dependency.version || bundledProof.integrity !== dependency.integrity) throw deliveryError("PACKAGE_SOURCE", "Bundled runtime evidence differs from the dependency lock; regenerate and commit both together");
299
+ }
300
+ if (bundledProof) {
301
+ const proven = Object.keys(bundledProof.files).sort();
302
+ const included = packed.files.filter(file => bundledRuntimeFile(file.path)).map(file => file.path).sort();
303
+ if (JSON.stringify(included) !== JSON.stringify(proven)) throw deliveryError("PACKAGE_SOURCE", "Bundled runtime archive paths differ from the complete committed proof; reinstall the locked dependency before rebuilding");
304
+ }
305
+ if (sourceVerified && !payloadChanges.length) {
306
+ let expected;
307
+ try {
308
+ expected = execFileSync("git", ["ls-tree", "-r", "-z", "--name-only", sourceCommit], { cwd: root, encoding: "utf8", maxBuffer: 16777216 })
309
+ .split("\0").filter(path => runtimeFile(path) && !bundledRuntimeFile(path)).sort();
310
+ } catch { throw deliveryError("PACKAGE_SOURCE", "Cannot enumerate the selected commit payload; restore Git access and rebuild"); }
311
+ const included = packed.files.filter(file => !bundledRuntimeFile(file.path)).map(file => file.path).sort();
312
+ if (JSON.stringify(included) !== JSON.stringify(expected)) throw deliveryError("PACKAGE_SOURCE", "First-party archive paths differ from the complete selected commit payload; restore the checkout and package allowlist before rebuilding");
313
+ }
314
+ for (const file of packed.files) {
315
+ const bytes = execFileSync("tar", ["-xOf", join(out, packed.filename), `package/${file.path}`], { maxBuffer: 16777216 });
316
+ payloadHashes[file.path] = sha(bytes);
317
+ if (bundledProof && bundledRuntimeFile(file.path)) {
318
+ if (bundledProof.files[file.path] !== payloadHashes[file.path]) throw deliveryError("PACKAGE_SOURCE", `Packed dependency bytes differ from the selected commit: ${file.path}`);
319
+ } else if (sourceVerified && !payloadChanges.length) {
320
+ let committed;
321
+ try { committed = execFileSync("git", ["show", `${sourceCommit}:${file.path}`], { cwd: root, maxBuffer: 16777216 }); }
322
+ catch { throw deliveryError("PACKAGE_SOURCE", `Packed path is absent from the selected commit: ${file.path}`); }
323
+ if (sha(committed) !== payloadHashes[file.path]) throw deliveryError("PACKAGE_SOURCE", `Packed bytes differ from the selected commit: ${file.path}; rebuild from unchanged sources`);
324
+ }
325
+ }
326
+ const archive = join(out, "aport-task-delivery.zip");
327
+ execFileSync("zip", ["-X", "-q", archive, ...Object.keys(hashes).sort()], { cwd: stage });
328
+ const runtime = { ...contracts, sourceCommit, sourceVerified, uncommittedPayload: payloadChanges, requirements: NODE_RUNTIME_REQUIREMENTS, exports: pkg.exports, endpoint, resource,
329
+ coreArtifact: packed.filename, coreSha256: sha(readFileSync(join(out, packed.filename))), dependencyLockSha256: sha(lock), assetHashes: hashes };
330
+ writeFileSync(join(out, "runtime.json"), json(runtime));
331
+ writeFileSync(join(out, "operations.json"), json(TASK_OPERATIONS));
332
+ writeFileSync(join(out, "RUNTIME.md"), readFileSync(join(root, "docs/task-delivery.md")));
333
+ const manifest = { format: 1, publisher: "APort", classification: internal ? "internal-development-fixture; not-installed-host-evidence" : "public-candidate-unqualified",
334
+ submissionReady: false, ...runtime, hashes: { plugin: sha(readFileSync(archive)), runtime: sha(readFileSync(join(out, "runtime.json"))), operations: sha(readFileSync(join(out, "operations.json"))) },
335
+ payloadHashes, packagingControlHashes, submissionSchemaSha256: sha(json(submissionSchemas)), schemaHashes: Object.fromEntries(Object.keys(schemas).map(n => [schemas[n].$id, sha(readFileSync(join(root, "templates/plugin/schemas", `${n}.schema.json`)))])) };
336
+ writeFileSync(join(out, "manifest.json"), json(manifest));
337
+ for (const name of readdirSync(out)) if (lstatSync(join(out, name)).isFile()) chmodSync(join(out, name), 0o644);
338
+ return manifest;
339
+ } catch (error) { rmSync(out, { recursive: true, force: true }); throw error; }
340
+ }