agent-hitch 0.2.0 → 0.2.1

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 (263) hide show
  1. package/README.md +16 -10
  2. package/README.zh-CN.md +13 -8
  3. package/dist/bin/hitch.js +1 -1
  4. package/dist/bin/hitch.js.map +1 -1
  5. package/dist/scripts/check-architecture.js +159 -0
  6. package/dist/scripts/check-architecture.js.map +1 -0
  7. package/dist/scripts/check-release.js +1 -1
  8. package/dist/scripts/check-release.js.map +1 -1
  9. package/dist/src/adapters/catalog.js +55 -0
  10. package/dist/src/adapters/catalog.js.map +1 -0
  11. package/dist/src/adapters/contract.js +2 -0
  12. package/dist/src/adapters/contract.js.map +1 -0
  13. package/dist/src/adapters/discovery.js +22 -0
  14. package/dist/src/adapters/discovery.js.map +1 -0
  15. package/dist/src/adapters/index.js +3 -0
  16. package/dist/src/adapters/index.js.map +1 -0
  17. package/dist/src/adapters/providers/claude.js +69 -0
  18. package/dist/src/adapters/providers/claude.js.map +1 -0
  19. package/dist/src/adapters/providers/codex.js +74 -0
  20. package/dist/src/adapters/providers/codex.js.map +1 -0
  21. package/dist/src/adapters/providers/deepseek.js +51 -0
  22. package/dist/src/adapters/providers/deepseek.js.map +1 -0
  23. package/dist/src/adapters/providers/opencode.js +73 -0
  24. package/dist/src/adapters/providers/opencode.js.map +1 -0
  25. package/dist/src/adapters/providers/pi.js +82 -0
  26. package/dist/src/adapters/providers/pi.js.map +1 -0
  27. package/dist/src/adapters/providers/shared.js +82 -0
  28. package/dist/src/adapters/providers/shared.js.map +1 -0
  29. package/dist/src/artifacts/index.js +3 -0
  30. package/dist/src/artifacts/index.js.map +1 -0
  31. package/dist/src/artifacts/integrity.js +188 -0
  32. package/dist/src/artifacts/integrity.js.map +1 -0
  33. package/dist/src/artifacts/preparer.js +379 -0
  34. package/dist/src/artifacts/preparer.js.map +1 -0
  35. package/dist/src/artifacts/store.js +87 -0
  36. package/dist/src/artifacts/store.js.map +1 -0
  37. package/dist/src/artifacts/types.js +2 -0
  38. package/dist/src/artifacts/types.js.map +1 -0
  39. package/dist/src/backends/contract.js +2 -0
  40. package/dist/src/backends/contract.js.map +1 -0
  41. package/dist/src/{harbor-backend.js → backends/harbor/backend.js} +39 -14
  42. package/dist/src/backends/harbor/backend.js.map +1 -0
  43. package/dist/src/backends/harbor/index.js +4 -0
  44. package/dist/src/backends/harbor/index.js.map +1 -0
  45. package/dist/src/backends/harbor/local-git-transport.js +447 -0
  46. package/dist/src/backends/harbor/local-git-transport.js.map +1 -0
  47. package/dist/src/{eval-tools.js → backends/harbor/tools.js} +2 -6
  48. package/dist/src/backends/harbor/tools.js.map +1 -0
  49. package/dist/src/backends/index.js +4 -0
  50. package/dist/src/backends/index.js.map +1 -0
  51. package/dist/src/cli/arguments.js +123 -0
  52. package/dist/src/cli/arguments.js.map +1 -0
  53. package/dist/src/cli/commands/compare.js +55 -0
  54. package/dist/src/cli/commands/compare.js.map +1 -0
  55. package/dist/src/cli/commands/daemon.js +106 -0
  56. package/dist/src/cli/commands/daemon.js.map +1 -0
  57. package/dist/src/cli/commands/eval.js +126 -0
  58. package/dist/src/cli/commands/eval.js.map +1 -0
  59. package/dist/src/cli/commands/feedback.js +84 -0
  60. package/dist/src/cli/commands/feedback.js.map +1 -0
  61. package/dist/src/cli/commands/inspect.js +20 -0
  62. package/dist/src/cli/commands/inspect.js.map +1 -0
  63. package/dist/src/cli/commands/list.js +17 -0
  64. package/dist/src/cli/commands/list.js.map +1 -0
  65. package/dist/src/cli/commands/prepare.js +21 -0
  66. package/dist/src/cli/commands/prepare.js.map +1 -0
  67. package/dist/src/cli/commands/resolve.js +17 -0
  68. package/dist/src/cli/commands/resolve.js.map +1 -0
  69. package/dist/src/cli/commands/run.js +83 -0
  70. package/dist/src/cli/commands/run.js.map +1 -0
  71. package/dist/src/cli/commands/runs.js +45 -0
  72. package/dist/src/cli/commands/runs.js.map +1 -0
  73. package/dist/src/cli/commands/trajectory.js +31 -0
  74. package/dist/src/cli/commands/trajectory.js.map +1 -0
  75. package/dist/src/cli/commands/workspace.js +44 -0
  76. package/dist/src/cli/commands/workspace.js.map +1 -0
  77. package/dist/src/cli/index.js +2 -0
  78. package/dist/src/cli/index.js.map +1 -0
  79. package/dist/src/cli/main.js +48 -0
  80. package/dist/src/cli/main.js.map +1 -0
  81. package/dist/src/cli/output.js +96 -0
  82. package/dist/src/cli/output.js.map +1 -0
  83. package/dist/src/controller-runtime/index.js +3 -0
  84. package/dist/src/controller-runtime/index.js.map +1 -0
  85. package/dist/src/controller-runtime/store.js +2 -7
  86. package/dist/src/controller-runtime/store.js.map +1 -1
  87. package/dist/src/daemon/auth.js +108 -0
  88. package/dist/src/daemon/auth.js.map +1 -0
  89. package/dist/src/daemon/client.js +76 -0
  90. package/dist/src/daemon/client.js.map +1 -0
  91. package/dist/src/daemon/index.js +5 -0
  92. package/dist/src/daemon/index.js.map +1 -0
  93. package/dist/src/daemon/launcher.js +24 -0
  94. package/dist/src/daemon/launcher.js.map +1 -0
  95. package/dist/src/{scheduler.js → daemon/scheduler.js} +6 -7
  96. package/dist/src/daemon/scheduler.js.map +1 -0
  97. package/dist/src/{daemon.js → daemon/server.js} +10 -155
  98. package/dist/src/daemon/server.js.map +1 -0
  99. package/dist/src/domain/artifacts.js +2 -0
  100. package/dist/src/domain/artifacts.js.map +1 -0
  101. package/dist/src/domain/controller-runtime.js +2 -0
  102. package/dist/src/domain/controller-runtime.js.map +1 -0
  103. package/dist/src/domain/eval-records.js +2 -0
  104. package/dist/src/domain/eval-records.js.map +1 -0
  105. package/dist/src/domain/evals.js +2 -0
  106. package/dist/src/domain/evals.js.map +1 -0
  107. package/dist/src/domain/feedback.js +2 -0
  108. package/dist/src/domain/feedback.js.map +1 -0
  109. package/dist/src/domain/{types.js → ids.js} +1 -1
  110. package/dist/src/domain/ids.js.map +1 -0
  111. package/dist/src/domain/index.js +11 -0
  112. package/dist/src/domain/index.js.map +1 -0
  113. package/dist/src/domain/revisions.js +2 -0
  114. package/dist/src/domain/revisions.js.map +1 -0
  115. package/dist/src/domain/runs.js +3 -0
  116. package/dist/src/domain/runs.js.map +1 -0
  117. package/dist/src/domain/trajectories.js +2 -0
  118. package/dist/src/domain/trajectories.js.map +1 -0
  119. package/dist/src/domain/{validate.js → validation.js} +194 -5
  120. package/dist/src/domain/validation.js.map +1 -0
  121. package/dist/src/domain/workspaces.js +2 -0
  122. package/dist/src/domain/workspaces.js.map +1 -0
  123. package/dist/src/evals/events.js +67 -0
  124. package/dist/src/evals/events.js.map +1 -0
  125. package/dist/src/evals/index.js +5 -0
  126. package/dist/src/evals/index.js.map +1 -0
  127. package/dist/src/evals/records.js +55 -0
  128. package/dist/src/evals/records.js.map +1 -0
  129. package/dist/src/evals/request.js +109 -0
  130. package/dist/src/evals/request.js.map +1 -0
  131. package/dist/src/evals/service.js +227 -0
  132. package/dist/src/evals/service.js.map +1 -0
  133. package/dist/src/evals/trial-import.js +394 -0
  134. package/dist/src/evals/trial-import.js.map +1 -0
  135. package/dist/src/feedback/index.js +2 -0
  136. package/dist/src/feedback/index.js.map +1 -0
  137. package/dist/src/feedback/service.js +3 -4
  138. package/dist/src/feedback/service.js.map +1 -1
  139. package/dist/src/{config.js → foundation/config.js} +1 -0
  140. package/dist/src/foundation/config.js.map +1 -0
  141. package/dist/src/foundation/errors.js.map +1 -0
  142. package/dist/src/foundation/executable.js +135 -0
  143. package/dist/src/foundation/executable.js.map +1 -0
  144. package/dist/src/foundation/fs.js.map +1 -0
  145. package/dist/src/foundation/hash.js +24 -0
  146. package/dist/src/foundation/hash.js.map +1 -0
  147. package/dist/src/foundation/index.js +11 -0
  148. package/dist/src/foundation/index.js.map +1 -0
  149. package/dist/src/foundation/line-stream.js.map +1 -0
  150. package/dist/src/foundation/locks.js +114 -0
  151. package/dist/src/foundation/locks.js.map +1 -0
  152. package/dist/src/{package-root.js → foundation/package-root.js} +3 -2
  153. package/dist/src/foundation/package-root.js.map +1 -0
  154. package/dist/src/foundation/process.js.map +1 -0
  155. package/dist/src/revisions/index.js +4 -0
  156. package/dist/src/revisions/index.js.map +1 -0
  157. package/dist/src/{harness-reference.js → revisions/reference.js} +16 -2
  158. package/dist/src/revisions/reference.js.map +1 -0
  159. package/dist/src/revisions/resolver.js +25 -0
  160. package/dist/src/revisions/resolver.js.map +1 -0
  161. package/dist/src/revisions/sources/git.js +180 -0
  162. package/dist/src/revisions/sources/git.js.map +1 -0
  163. package/dist/src/revisions/sources/installed.js +45 -0
  164. package/dist/src/revisions/sources/installed.js.map +1 -0
  165. package/dist/src/revisions/sources/npm.js +97 -0
  166. package/dist/src/revisions/sources/npm.js.map +1 -0
  167. package/dist/src/runs/compare.js +132 -0
  168. package/dist/src/runs/compare.js.map +1 -0
  169. package/dist/src/{events.js → runs/events.js} +1 -2
  170. package/dist/src/runs/events.js.map +1 -0
  171. package/dist/src/{engine.js → runs/executor.js} +166 -151
  172. package/dist/src/runs/executor.js.map +1 -0
  173. package/dist/src/runs/finalizer.js +22 -0
  174. package/dist/src/runs/finalizer.js.map +1 -0
  175. package/dist/src/runs/identity.js +35 -0
  176. package/dist/src/runs/identity.js.map +1 -0
  177. package/dist/src/runs/index.js +11 -0
  178. package/dist/src/runs/index.js.map +1 -0
  179. package/dist/src/runs/manifest.js +70 -0
  180. package/dist/src/runs/manifest.js.map +1 -0
  181. package/dist/src/runs/outcome.js +49 -0
  182. package/dist/src/runs/outcome.js.map +1 -0
  183. package/dist/src/runs/query.js +104 -0
  184. package/dist/src/runs/query.js.map +1 -0
  185. package/dist/src/runs/queued.js +54 -0
  186. package/dist/src/runs/queued.js.map +1 -0
  187. package/dist/src/runs/records.js +288 -0
  188. package/dist/src/runs/records.js.map +1 -0
  189. package/dist/src/runs/request.js +169 -0
  190. package/dist/src/runs/request.js.map +1 -0
  191. package/dist/src/trajectories/format.js +1 -1
  192. package/dist/src/trajectories/format.js.map +1 -1
  193. package/dist/src/trajectories/index.js +7 -0
  194. package/dist/src/trajectories/index.js.map +1 -0
  195. package/dist/src/trajectories/projector.js +1 -1
  196. package/dist/src/trajectories/projector.js.map +1 -1
  197. package/dist/src/trajectories/provider-capture.js +133 -0
  198. package/dist/src/trajectories/provider-capture.js.map +1 -0
  199. package/dist/src/trajectories/providers/deepseek.js +179 -0
  200. package/dist/src/trajectories/providers/deepseek.js.map +1 -0
  201. package/dist/src/trajectories/store.js +148 -8
  202. package/dist/src/trajectories/store.js.map +1 -1
  203. package/dist/src/workspaces/copy.js +142 -0
  204. package/dist/src/workspaces/copy.js.map +1 -0
  205. package/dist/src/workspaces/digest.js +47 -0
  206. package/dist/src/workspaces/digest.js.map +1 -0
  207. package/dist/src/workspaces/git.js +164 -0
  208. package/dist/src/workspaces/git.js.map +1 -0
  209. package/dist/src/workspaces/index.js +6 -0
  210. package/dist/src/workspaces/index.js.map +1 -0
  211. package/dist/src/workspaces/lifecycle.js +313 -0
  212. package/dist/src/workspaces/lifecycle.js.map +1 -0
  213. package/dist/src/workspaces/planner.js +80 -0
  214. package/dist/src/workspaces/planner.js.map +1 -0
  215. package/dist/src/workspaces/store.js +25 -0
  216. package/dist/src/workspaces/store.js.map +1 -0
  217. package/dist/src/workspaces/types.js +4 -0
  218. package/dist/src/workspaces/types.js.map +1 -0
  219. package/dist/src/workspaces/utils.js +56 -0
  220. package/dist/src/workspaces/utils.js.map +1 -0
  221. package/docs/schemas/eval-request.schema.json +2 -0
  222. package/docs/schemas/eval-result.schema.json +36 -1
  223. package/docs/schemas/local-git-transport-manifest.schema.json +34 -0
  224. package/docs/schemas/result.schema.json +9 -0
  225. package/docs/schemas/run-context.schema.json +39 -0
  226. package/docs/schemas/run-manifest.schema.json +81 -0
  227. package/docs/schemas/run-request.schema.json +16 -1
  228. package/docs/schemas/trajectory-ref.schema.json +66 -23
  229. package/integrations/harbor/hitch_harbor_agent.py +375 -1
  230. package/package.json +21 -3
  231. package/dist/src/adapters.js +0 -461
  232. package/dist/src/adapters.js.map +0 -1
  233. package/dist/src/artifacts.js +0 -1081
  234. package/dist/src/artifacts.js.map +0 -1
  235. package/dist/src/cli.js +0 -699
  236. package/dist/src/cli.js.map +0 -1
  237. package/dist/src/config.js.map +0 -1
  238. package/dist/src/daemon.js.map +0 -1
  239. package/dist/src/domain/types.js.map +0 -1
  240. package/dist/src/domain/validate.js.map +0 -1
  241. package/dist/src/engine.js.map +0 -1
  242. package/dist/src/errors.js.map +0 -1
  243. package/dist/src/eval-tools.js.map +0 -1
  244. package/dist/src/evals.js +0 -313
  245. package/dist/src/evals.js.map +0 -1
  246. package/dist/src/events.js.map +0 -1
  247. package/dist/src/fs.js.map +0 -1
  248. package/dist/src/harbor-backend.js.map +0 -1
  249. package/dist/src/harness-reference.js.map +0 -1
  250. package/dist/src/line-stream.js.map +0 -1
  251. package/dist/src/locks.js +0 -38
  252. package/dist/src/locks.js.map +0 -1
  253. package/dist/src/package-root.js.map +0 -1
  254. package/dist/src/process.js.map +0 -1
  255. package/dist/src/registry.js +0 -81
  256. package/dist/src/registry.js.map +0 -1
  257. package/dist/src/scheduler.js.map +0 -1
  258. package/dist/src/workspaces.js +0 -858
  259. package/dist/src/workspaces.js.map +0 -1
  260. /package/dist/src/{errors.js → foundation/errors.js} +0 -0
  261. /package/dist/src/{fs.js → foundation/fs.js} +0 -0
  262. /package/dist/src/{line-stream.js → foundation/line-stream.js} +0 -0
  263. /package/dist/src/{process.js → foundation/process.js} +0 -0
@@ -1,1081 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import { constants, createReadStream } from "node:fs";
3
- import { access, chmod, lstat, open, readFile, readdir, readlink, rename, rm, stat, writeFile } from "node:fs/promises";
4
- import path from "node:path";
5
- import { spawn } from "node:child_process";
6
- import { getAdapter } from "./adapters.js";
7
- import { statePaths, SCHEMA_VERSION } from "./config.js";
8
- import { HitchError } from "./errors.js";
9
- import { atomicWriteJSON, ensureDir, readJSON, removeIfExists } from "./fs.js";
10
- import { parseHarnessReference } from "./harness-reference.js";
11
- import { detectVersion, fingerprintExecutable, inspectAgent } from "./registry.js";
12
- import { delay, terminateProcess } from "./process.js";
13
- import { reclaimStaleLock } from "./locks.js";
14
- const RECIPE_VERSION = "1";
15
- const COMMAND_TIMEOUT_MS = 30 * 60 * 1_000;
16
- export async function resolveHarness(referenceValue, { root, env = process.env } = {}) {
17
- const reference = typeof referenceValue === "string" ? parseHarnessReference(referenceValue) : referenceValue;
18
- const adapter = getAdapter(reference.harness_id);
19
- if (reference.selector.type === "installed")
20
- return resolveInstalled(reference, env);
21
- const source = adapter.revision_sources?.[reference.selector.type];
22
- if (!source) {
23
- throw new HitchError(`${reference.harness_id} does not support ${reference.selector.type} revisions`, {
24
- code: "revision_selector_unsupported",
25
- exitCode: 10,
26
- });
27
- }
28
- if (!root)
29
- throw new HitchError("a Hitch state root is required to resolve managed revisions");
30
- if (reference.selector.type === "version")
31
- return resolveVersion(reference, source, env);
32
- return resolveCommit(reference, source, statePaths(root), env);
33
- }
34
- export async function prepareHarness(resolved, { root, env = process.env, signal } = {}) {
35
- if (!resolved || typeof resolved !== "object" || !resolved.identity) {
36
- throw new HitchError("resolved revision is required", { code: "invalid_resolution", exitCode: 2 });
37
- }
38
- const adapter = getAdapter(resolved.harness_id);
39
- if (resolved.source.type === "installed")
40
- return prepareInstalled(resolved, adapter);
41
- if (!root)
42
- throw new HitchError("a Hitch state root is required to prepare managed revisions");
43
- const source = adapter.revision_sources?.[resolved.selector.type];
44
- if (!source) {
45
- throw new HitchError(`${resolved.harness_id} does not support ${resolved.selector.type} revisions`, {
46
- code: "revision_selector_unsupported",
47
- exitCode: 10,
48
- });
49
- }
50
- const paths = statePaths(root);
51
- const preparationKey = digest({
52
- revision_identity: resolved.identity,
53
- recipe_version: RECIPE_VERSION,
54
- recipe: recipeIdentity(source),
55
- platform: process.platform,
56
- architecture: process.arch,
57
- node: process.versions.node,
58
- });
59
- return withArtifactLock(paths, preparationKey, async () => {
60
- const cached = await readCachedArtifact(paths, preparationKey, resolved.identity);
61
- if (cached)
62
- return { ...cached, cache_hit: true };
63
- return resolved.source.type === "npm"
64
- ? prepareNpmArtifact(resolved, adapter, source, paths, preparationKey, env, signal)
65
- : prepareGitArtifact(resolved, adapter, source, paths, preparationKey, env, signal);
66
- }, { ...(signal ? { signal } : {}) });
67
- }
68
- export async function listPreparedArtifacts(harnessId, { root } = {}) {
69
- if (!root)
70
- throw new HitchError("a Hitch state root is required to list prepared artifacts");
71
- const directory = statePaths(root).artifacts;
72
- let entries;
73
- try {
74
- entries = await readdir(directory, { withFileTypes: true });
75
- }
76
- catch (error) {
77
- if (error?.code === "ENOENT")
78
- return [];
79
- throw error;
80
- }
81
- const artifacts = [];
82
- for (const entry of entries) {
83
- if (!entry.isDirectory())
84
- continue;
85
- const manifest = await readJSON(path.join(directory, entry.name, "artifact.json"), null).catch(() => null);
86
- if (!manifest || manifest.harness_id !== harnessId)
87
- continue;
88
- const artifactDirectory = path.join(directory, entry.name);
89
- const ready = await artifactMatches(artifactDirectory, manifest);
90
- artifacts.push({ ...manifest, status: ready ? "ready" : "invalid" });
91
- }
92
- return artifacts.sort((left, right) => String(right.prepared_at || "").localeCompare(String(left.prepared_at || "")));
93
- }
94
- async function resolveInstalled(reference, env) {
95
- let discovered;
96
- try {
97
- discovered = await inspectAgent(reference.harness_id, { env });
98
- }
99
- catch (error) {
100
- throw new HitchError(`failed to inspect installed harness: ${reference.harness_id}`, {
101
- code: "resolution_failed",
102
- exitCode: 4,
103
- cause: error,
104
- });
105
- }
106
- if (discovered.status !== "available" || !discovered.executable || !discovered.identity) {
107
- throw new HitchError(`installed harness not found: ${reference.harness_id}`, {
108
- code: "revision_not_found",
109
- exitCode: 3,
110
- });
111
- }
112
- const identity = digest({
113
- harness_id: reference.harness_id,
114
- source_type: "installed",
115
- executable_identity: discovered.identity,
116
- });
117
- return {
118
- schema_version: SCHEMA_VERSION,
119
- requested_ref: reference.raw,
120
- canonical_ref: reference.canonical,
121
- harness_id: reference.harness_id,
122
- selector: { type: "installed" },
123
- source: {
124
- type: "installed",
125
- executable: discovered.executable,
126
- integrity: discovered.identity,
127
- },
128
- revision: {
129
- type: "installed",
130
- version: discovered.version || null,
131
- },
132
- identity,
133
- resolved_at: new Date().toISOString(),
134
- };
135
- }
136
- async function resolveVersion(reference, source, env) {
137
- const npm = commandExecutable("npm", env);
138
- const packages = source.packages || [source.package];
139
- let result;
140
- let packageName;
141
- let notFoundError;
142
- for (const candidate of packages) {
143
- if (!candidate)
144
- continue;
145
- const spec = `${candidate}@${reference.selector.value}`;
146
- try {
147
- result = await runCommand(npm, ["view", spec, "version", "dist", "--json"], {
148
- env,
149
- failureCode: "resolution_failed",
150
- failureExitCode: 4,
151
- });
152
- packageName = candidate;
153
- break;
154
- }
155
- catch (error) {
156
- if (!/E404|not found|No match/i.test(error.message))
157
- throw error;
158
- notFoundError = error;
159
- }
160
- }
161
- if (!result || !packageName) {
162
- throw new HitchError(`revision not found: ${reference.canonical}`, {
163
- code: "revision_not_found",
164
- exitCode: 3,
165
- cause: notFoundError,
166
- });
167
- }
168
- const spec = `${packageName}@${reference.selector.value}`;
169
- let metadata;
170
- try {
171
- metadata = JSON.parse(result.stdout);
172
- }
173
- catch (error) {
174
- throw new HitchError(`package registry returned invalid metadata for ${spec}`, {
175
- code: "resolution_failed",
176
- exitCode: 4,
177
- cause: error,
178
- });
179
- }
180
- const expectedVersion = reference.selector.value;
181
- if (metadata.version !== expectedVersion) {
182
- throw new HitchError(`package registry resolved ${spec} as ${metadata.version || "an unknown version"}`, {
183
- code: "resolution_failed",
184
- exitCode: 4,
185
- });
186
- }
187
- const integrity = metadata.dist?.integrity || (metadata.dist?.shasum ? `sha1:${metadata.dist.shasum}` : "");
188
- if (!integrity || !metadata.dist?.tarball) {
189
- throw new HitchError(`package registry did not provide immutable distribution metadata for ${spec}`, {
190
- code: "resolution_failed",
191
- exitCode: 4,
192
- });
193
- }
194
- const identity = digest({
195
- harness_id: reference.harness_id,
196
- source_type: "npm",
197
- package: packageName,
198
- version: metadata.version,
199
- integrity,
200
- });
201
- return {
202
- schema_version: SCHEMA_VERSION,
203
- requested_ref: reference.raw,
204
- canonical_ref: reference.canonical,
205
- harness_id: reference.harness_id,
206
- selector: { type: "version", value: expectedVersion },
207
- source: {
208
- type: "npm",
209
- package: packageName,
210
- tarball: sanitizeUrl(metadata.dist.tarball),
211
- integrity,
212
- },
213
- revision: { type: "version", version: metadata.version },
214
- identity,
215
- resolved_at: new Date().toISOString(),
216
- };
217
- }
218
- async function resolveCommit(reference, source, paths, env) {
219
- const selector = reference.selector;
220
- const sourceUrl = selector.source?.url || source.url;
221
- if (!sourceUrl) {
222
- throw new HitchError(`no Git source registered for ${reference.harness_id}`, {
223
- code: "revision_selector_unsupported",
224
- exitCode: 10,
225
- });
226
- }
227
- const localPath = selector.source?.local_path;
228
- const git = commandExecutable("git", env);
229
- let fullCommit;
230
- if (localPath) {
231
- const statusResult = await runCommand(git, ["-C", localPath, "status", "--porcelain"], {
232
- env,
233
- failureCode: "resolution_failed",
234
- failureExitCode: 4,
235
- });
236
- if (statusResult.stdout.trim()) {
237
- throw new HitchError(`local harness repository has uncommitted changes: ${localPath}`, {
238
- code: "dirty_source",
239
- exitCode: 11,
240
- });
241
- }
242
- fullCommit = await revParseCommit(git, localPath, selector.value, env);
243
- }
244
- const cacheDirectory = gitCacheDirectory(paths, sourceUrl);
245
- const requestedCommit = fullCommit || selector.value;
246
- fullCommit = await withFileLock(paths.sourceLocks, digest(sourceUrl), async () => {
247
- await ensureGitCache(git, cacheDirectory, sourceUrl, env);
248
- if (requestedCommit.length >= 40) {
249
- await fetchCommit(git, cacheDirectory, sourceUrl, requestedCommit, env);
250
- }
251
- else {
252
- await fetchAdvertisedRefs(git, cacheDirectory, sourceUrl, env);
253
- }
254
- return revParseBareCommit(git, cacheDirectory, requestedCommit, env);
255
- }, { timeoutCode: "resolution_locked", timeoutExitCode: 4 });
256
- const normalizedSource = sanitizeUrl(sourceUrl);
257
- const identity = digest({
258
- harness_id: reference.harness_id,
259
- source_type: "git",
260
- source: normalizedSource,
261
- commit: fullCommit,
262
- });
263
- return {
264
- schema_version: SCHEMA_VERSION,
265
- requested_ref: reference.raw,
266
- canonical_ref: reference.canonical,
267
- harness_id: reference.harness_id,
268
- selector: { type: "commit", value: selector.value },
269
- source: {
270
- type: "git",
271
- url: normalizedSource,
272
- registered: !selector.source?.explicit,
273
- },
274
- revision: {
275
- type: "commit",
276
- requested_commit: selector.value,
277
- commit: fullCommit,
278
- },
279
- identity,
280
- resolved_at: new Date().toISOString(),
281
- };
282
- }
283
- async function prepareInstalled(resolved, adapter) {
284
- const executable = resolved.source.executable;
285
- if (!executable) {
286
- throw new HitchError("installed resolution is missing its executable", { code: "artifact_integrity_mismatch", exitCode: 5 });
287
- }
288
- let currentIdentity;
289
- try {
290
- currentIdentity = await fingerprintExecutable(executable);
291
- }
292
- catch (error) {
293
- throw new HitchError(`installed harness is no longer readable: ${executable}`, {
294
- code: "artifact_integrity_mismatch",
295
- exitCode: 5,
296
- cause: error,
297
- });
298
- }
299
- if (currentIdentity !== resolved.source.integrity) {
300
- throw new HitchError(`installed harness changed after resolution: ${executable}`, {
301
- code: "artifact_integrity_mismatch",
302
- exitCode: 5,
303
- });
304
- }
305
- return {
306
- schema_version: SCHEMA_VERSION,
307
- artifact_id: digest({ revision_identity: resolved.identity, recipe_version: RECIPE_VERSION }),
308
- harness_id: resolved.harness_id,
309
- revision_identity: resolved.identity,
310
- source_type: "installed",
311
- adapter: adapter.id,
312
- adapter_version: RECIPE_VERSION,
313
- recipe_version: RECIPE_VERSION,
314
- platform: `${process.platform}-${process.arch}`,
315
- entrypoint: executable,
316
- launcher: "direct",
317
- entrypoint_integrity: currentIdentity,
318
- artifact_integrity: currentIdentity,
319
- toolchain: { node: process.version },
320
- resolved_revision: resolved,
321
- executable,
322
- entrypoint_args: [],
323
- observed_version: resolved.revision.version,
324
- external: true,
325
- cache_hit: true,
326
- prepared_at: resolved.resolved_at,
327
- };
328
- }
329
- async function prepareNpmArtifact(resolved, adapter, source, paths, preparationKey, env, signal) {
330
- const staging = await newStagingDirectory(paths, resolved.harness_id);
331
- try {
332
- const npm = commandExecutable("npm", env);
333
- const tarball = resolved.source.tarball;
334
- if (!tarball)
335
- throw new HitchError("npm resolution is missing its tarball", { code: "prepare_failed", exitCode: 5 });
336
- const installMode = source.install_mode || "project";
337
- let nodeModulesDirectory = "node_modules";
338
- let lockIdentity;
339
- if (installMode === "global") {
340
- const packedTarball = await packResolvedNpmTarball(npm, staging, resolved, env, signal);
341
- try {
342
- await runCommand(npm, ["install", "--global", "--prefix", staging, "--no-audit", "--no-fund", packedTarball], {
343
- env,
344
- signal,
345
- failureCode: "prepare_failed",
346
- failureExitCode: 5,
347
- });
348
- }
349
- finally {
350
- await rm(packedTarball, { force: true });
351
- }
352
- nodeModulesDirectory = path.join("lib", "node_modules");
353
- await assertGlobalNpmResolution(staging, resolved);
354
- lockIdentity = digest({
355
- package: resolved.source.package,
356
- version: resolved.revision.version,
357
- integrity: resolved.source.integrity,
358
- });
359
- }
360
- else {
361
- await writeFile(path.join(staging, "package.json"), `${JSON.stringify({ private: true })}\n`, { mode: 0o600 });
362
- await runCommand(npm, ["install", "--no-audit", "--no-fund", "--save-exact", tarball], {
363
- cwd: staging,
364
- env,
365
- signal,
366
- failureCode: "prepare_failed",
367
- failureExitCode: 5,
368
- });
369
- await assertNpmResolution(staging, resolved);
370
- lockIdentity = await optionalFileDigest(path.join(staging, "package-lock.json"));
371
- }
372
- const packageName = resolved.source.package;
373
- if (!packageName)
374
- throw new HitchError("npm resolution is missing its package name", { code: "prepare_failed", exitCode: 5 });
375
- const binName = source.bin;
376
- if (!binName)
377
- throw new HitchError("npm revision source is missing its bin name", { code: "prepare_failed", exitCode: 5 });
378
- const entrypoint = await npmPackageEntrypoint(staging, packageName, binName, nodeModulesDirectory);
379
- const stagedExecutable = path.join(staging, entrypoint);
380
- await assertEntrypoint(stagedExecutable, entrypointLauncher(entrypoint), resolved.canonical_ref);
381
- const toolchain = { node: process.version, npm: await commandVersion(npm, env, signal) };
382
- const launcher = entrypointLauncher(entrypoint);
383
- const invocation = artifactInvocation({ entrypoint, launcher }, staging);
384
- const observedVersion = await detectVersion(invocation.executable, [...invocation.entrypoint_args, ...adapter.version_args]);
385
- throwIfAborted(signal);
386
- assertObservedVersion(observedVersion, resolved.revision.version || "", resolved.canonical_ref);
387
- const entrypointIntegrity = await fingerprintExecutable(stagedExecutable);
388
- const artifactIntegrity = await artifactDirectoryIntegrity(staging);
389
- const artifactId = digest({
390
- preparation_key: preparationKey,
391
- dependency_lock: lockIdentity,
392
- toolchain,
393
- artifact_integrity: artifactIntegrity,
394
- });
395
- const manifest = artifactManifest(resolved, adapter, artifactId, entrypoint, toolchain, {
396
- dependency_lock: lockIdentity,
397
- artifact_integrity: artifactIntegrity,
398
- });
399
- manifest.launcher = launcher;
400
- manifest.observed_version = observedVersion || null;
401
- manifest.entrypoint_integrity = entrypointIntegrity;
402
- await atomicWriteJSON(path.join(staging, "artifact.json"), manifest);
403
- return promoteArtifact(paths, staging, manifest, preparationKey);
404
- }
405
- catch (error) {
406
- await rm(staging, { recursive: true, force: true });
407
- throw normalizePreparationError(error, resolved.canonical_ref);
408
- }
409
- }
410
- async function prepareGitArtifact(resolved, adapter, source, paths, preparationKey, env, signal) {
411
- const staging = await newStagingDirectory(paths, resolved.harness_id);
412
- const sourceDirectory = path.join(staging, "source");
413
- try {
414
- const git = commandExecutable("git", env);
415
- const cacheDirectory = gitCacheDirectory(paths, resolved.source.url || "");
416
- await runCommand(git, ["clone", "--no-hardlinks", "--no-checkout", cacheDirectory, sourceDirectory], {
417
- env,
418
- signal,
419
- failureCode: "prepare_failed",
420
- failureExitCode: 5,
421
- });
422
- await runCommand(git, ["-C", sourceDirectory, "checkout", "--detach", resolved.revision.commit], {
423
- env,
424
- signal,
425
- failureCode: "prepare_failed",
426
- failureExitCode: 5,
427
- });
428
- const toolchain = { node: process.version };
429
- for (const command of source.commands || []) {
430
- const executable = commandExecutable(command.executable, env);
431
- toolchain[command.executable] ||= await commandVersion(executable, env, signal);
432
- await runCommand(executable, command.args || [], {
433
- cwd: path.resolve(sourceDirectory, command.cwd || "."),
434
- env,
435
- signal,
436
- failureCode: "prepare_failed",
437
- failureExitCode: 5,
438
- });
439
- }
440
- const entrypoint = path.join("source", source.entrypoint || "");
441
- const stagedExecutable = path.join(staging, entrypoint);
442
- if (stagedExecutable.endsWith(".js"))
443
- await chmod(stagedExecutable, 0o755).catch(() => { });
444
- await assertEntrypoint(stagedExecutable, entrypointLauncher(entrypoint), resolved.canonical_ref);
445
- const lockIdentity = await sourceLockIdentity(sourceDirectory);
446
- await rm(path.join(sourceDirectory, ".git"), { recursive: true, force: true });
447
- const launcher = entrypointLauncher(entrypoint);
448
- const invocation = artifactInvocation({ entrypoint, launcher }, staging);
449
- const observedVersion = await detectVersion(invocation.executable, [...invocation.entrypoint_args, ...adapter.version_args]);
450
- throwIfAborted(signal);
451
- const entrypointIntegrity = await fingerprintExecutable(stagedExecutable);
452
- const artifactIntegrity = await artifactDirectoryIntegrity(staging);
453
- const artifactId = digest({
454
- preparation_key: preparationKey,
455
- dependency_lock: lockIdentity,
456
- toolchain,
457
- artifact_integrity: artifactIntegrity,
458
- });
459
- const manifest = artifactManifest(resolved, adapter, artifactId, entrypoint, toolchain, {
460
- dependency_lock: lockIdentity,
461
- artifact_integrity: artifactIntegrity,
462
- });
463
- manifest.launcher = launcher;
464
- manifest.observed_version = observedVersion || null;
465
- manifest.entrypoint_integrity = entrypointIntegrity;
466
- await atomicWriteJSON(path.join(staging, "artifact.json"), manifest);
467
- return promoteArtifact(paths, staging, manifest, preparationKey);
468
- }
469
- catch (error) {
470
- await rm(staging, { recursive: true, force: true });
471
- throw normalizePreparationError(error, resolved.canonical_ref);
472
- }
473
- }
474
- function artifactManifest(resolved, adapter, artifactId, entrypoint, toolchain, extra = {}) {
475
- return {
476
- schema_version: SCHEMA_VERSION,
477
- artifact_id: artifactId,
478
- harness_id: resolved.harness_id,
479
- revision_identity: resolved.identity,
480
- source_type: resolved.source.type,
481
- adapter: adapter.id,
482
- adapter_version: RECIPE_VERSION,
483
- recipe_version: RECIPE_VERSION,
484
- platform: `${process.platform}-${process.arch}`,
485
- entrypoint,
486
- toolchain,
487
- resolved_revision: resolved,
488
- ...extra,
489
- prepared_at: new Date().toISOString(),
490
- };
491
- }
492
- async function promoteArtifact(paths, staging, manifest, preparationKey) {
493
- await ensureDir(paths.artifacts);
494
- const artifactDirectory = path.join(paths.artifacts, manifest.artifact_id.replace("sha256:", ""));
495
- try {
496
- await rename(staging, artifactDirectory);
497
- }
498
- catch (error) {
499
- if (!["EEXIST", "ENOTEMPTY"].includes(error?.code || ""))
500
- throw error;
501
- const existing = await readJSON(path.join(artifactDirectory, "artifact.json"), null);
502
- const valid = existing?.artifact_id === manifest.artifact_id
503
- && await artifactMatches(artifactDirectory, existing);
504
- if (!valid) {
505
- const quarantine = path.join(paths.temporary, `invalid-${manifest.artifact_id.replace("sha256:", "")}-${randomBytes(6).toString("hex")}`);
506
- await rename(artifactDirectory, quarantine);
507
- try {
508
- await rename(staging, artifactDirectory);
509
- }
510
- catch (promotionError) {
511
- await rename(quarantine, artifactDirectory).catch(() => { });
512
- throw promotionError;
513
- }
514
- await rm(quarantine, { recursive: true, force: true });
515
- }
516
- else {
517
- await rm(staging, { recursive: true, force: true });
518
- }
519
- }
520
- await atomicWriteJSON(path.join(paths.artifactIndex, `${preparationKey.replace("sha256:", "")}.json`), {
521
- schema_version: SCHEMA_VERSION,
522
- preparation_key: preparationKey,
523
- artifact_id: manifest.artifact_id,
524
- });
525
- return {
526
- ...manifest,
527
- ...artifactInvocation(manifest, artifactDirectory),
528
- cache_hit: false,
529
- };
530
- }
531
- async function readCachedArtifact(paths, preparationKey, expectedRevisionIdentity) {
532
- const index = await readJSON(path.join(paths.artifactIndex, `${preparationKey.replace("sha256:", "")}.json`), null);
533
- if (!index?.artifact_id)
534
- return null;
535
- const directory = path.join(paths.artifacts, index.artifact_id.replace("sha256:", ""));
536
- const manifest = await readJSON(path.join(directory, "artifact.json"), null);
537
- if (!manifest || manifest.artifact_id !== index.artifact_id || manifest.revision_identity !== expectedRevisionIdentity)
538
- return null;
539
- const executable = path.join(directory, manifest.entrypoint);
540
- try {
541
- await access(executable, manifest.launcher === "node" ? constants.R_OK : constants.X_OK);
542
- if (!await artifactMatches(directory, manifest))
543
- return null;
544
- }
545
- catch {
546
- return null;
547
- }
548
- return { ...manifest, ...artifactInvocation(manifest, directory), cache_hit: true };
549
- }
550
- async function ensureGitCache(git, directory, sourceUrl, env) {
551
- try {
552
- await access(path.join(directory, "HEAD"));
553
- try {
554
- await runCommand(git, ["--git-dir", directory, "remote", "set-url", "origin", sourceUrl], {
555
- env,
556
- failureCode: "resolution_failed",
557
- failureExitCode: 4,
558
- });
559
- }
560
- catch {
561
- await runCommand(git, ["--git-dir", directory, "remote", "add", "origin", sourceUrl], {
562
- env,
563
- failureCode: "resolution_failed",
564
- failureExitCode: 4,
565
- });
566
- }
567
- return;
568
- }
569
- catch (error) {
570
- if (error instanceof HitchError)
571
- throw error;
572
- }
573
- await ensureDir(path.dirname(directory));
574
- await runCommand(git, ["init", "--bare", directory], {
575
- env,
576
- failureCode: "resolution_failed",
577
- failureExitCode: 4,
578
- });
579
- await runCommand(git, ["--git-dir", directory, "remote", "add", "origin", sourceUrl], {
580
- env,
581
- failureCode: "resolution_failed",
582
- failureExitCode: 4,
583
- });
584
- }
585
- async function fetchCommit(git, cacheDirectory, sourceUrl, commit, env) {
586
- try {
587
- await runCommand(git, ["--git-dir", cacheDirectory, "fetch", "--no-tags", "origin", commit], {
588
- env,
589
- failureCode: "revision_not_found",
590
- failureExitCode: 3,
591
- });
592
- }
593
- catch {
594
- await fetchAdvertisedRefs(git, cacheDirectory, sourceUrl, env);
595
- }
596
- }
597
- async function fetchAdvertisedRefs(git, cacheDirectory, _sourceUrl, env) {
598
- await runCommand(git, [
599
- "--git-dir", cacheDirectory,
600
- "fetch", "--force", "origin",
601
- "+refs/heads/*:refs/remotes/origin/*",
602
- "+refs/tags/*:refs/tags/*",
603
- ], {
604
- env,
605
- failureCode: "resolution_failed",
606
- failureExitCode: 4,
607
- });
608
- }
609
- async function revParseCommit(git, directory, commit, env) {
610
- try {
611
- const result = await runCommand(git, ["-C", directory, "rev-parse", "--verify", `${commit}^{commit}`], {
612
- env,
613
- failureCode: "revision_not_found",
614
- failureExitCode: 3,
615
- });
616
- return result.stdout.trim().toLowerCase();
617
- }
618
- catch (error) {
619
- throw new HitchError(`revision not found or ambiguous: ${commit}`, {
620
- code: "revision_not_found",
621
- exitCode: 3,
622
- cause: error,
623
- });
624
- }
625
- }
626
- async function revParseBareCommit(git, directory, commit, env) {
627
- try {
628
- const result = await runCommand(git, ["--git-dir", directory, "rev-parse", "--verify", `${commit}^{commit}`], {
629
- env,
630
- failureCode: "revision_not_found",
631
- failureExitCode: 3,
632
- });
633
- return result.stdout.trim().toLowerCase();
634
- }
635
- catch (error) {
636
- throw new HitchError(`revision not found or ambiguous: ${commit}`, {
637
- code: "revision_not_found",
638
- exitCode: 3,
639
- cause: error,
640
- });
641
- }
642
- }
643
- async function withArtifactLock(paths, key, operation, options = {}) {
644
- return withFileLock(paths.artifactLocks, key, operation, options);
645
- }
646
- async function withFileLock(directory, key, operation, { timeoutCode = "prepare_locked", timeoutExitCode = 5, signal, } = {}) {
647
- await ensureDir(directory);
648
- const file = path.join(directory, `${key.replace("sha256:", "")}.lock`);
649
- const owner = randomBytes(12).toString("hex");
650
- let handle;
651
- for (let attempt = 0; attempt < 3_000; attempt += 1) {
652
- throwIfAborted(signal);
653
- try {
654
- handle = await open(file, "wx", 0o600);
655
- try {
656
- await handle.writeFile(`${JSON.stringify({ owner, pid: process.pid, created_at: new Date().toISOString() })}\n`);
657
- }
658
- catch (error) {
659
- await handle.close().catch(() => { });
660
- handle = undefined;
661
- await removeIfExists(file);
662
- throw error;
663
- }
664
- break;
665
- }
666
- catch (error) {
667
- if (error?.code !== "EEXIST")
668
- throw error;
669
- if (await staleLock(file)) {
670
- if (!await reclaimStaleLock(file, staleLock))
671
- await delay(100);
672
- continue;
673
- }
674
- await delay(100);
675
- }
676
- }
677
- if (!handle)
678
- throw new HitchError("timed out waiting for Hitch state lock", { code: timeoutCode, exitCode: timeoutExitCode });
679
- try {
680
- throwIfAborted(signal);
681
- return await operation();
682
- }
683
- finally {
684
- await handle.close().catch(() => { });
685
- const current = await readJSON(file, null).catch(() => null);
686
- if (current?.owner === owner)
687
- await removeIfExists(file);
688
- }
689
- }
690
- async function staleLock(file) {
691
- let lock;
692
- try {
693
- lock = await readJSON(file);
694
- }
695
- catch {
696
- try {
697
- return Date.now() - (await stat(file)).mtimeMs > 2_000;
698
- }
699
- catch {
700
- return true;
701
- }
702
- }
703
- if (!Number.isInteger(lock?.pid))
704
- return true;
705
- try {
706
- process.kill(lock.pid, 0);
707
- return false;
708
- }
709
- catch (error) {
710
- return error?.code === "ESRCH";
711
- }
712
- }
713
- async function runCommand(executable, args, { cwd, env = process.env, failureCode = "internal_error", failureExitCode = 12, timeoutMs = COMMAND_TIMEOUT_MS, signal, } = {}) {
714
- throwIfAborted(signal);
715
- return new Promise((resolve, reject) => {
716
- const child = spawn(executable, args, {
717
- cwd,
718
- env,
719
- stdio: ["ignore", "pipe", "pipe"],
720
- windowsHide: true,
721
- detached: process.platform !== "win32",
722
- });
723
- let stdout = "";
724
- let stderr = "";
725
- let aborted = false;
726
- let settled = false;
727
- const append = (current, chunk) => `${current}${chunk}`.slice(-1_000_000);
728
- child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
729
- child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
730
- const timer = setTimeout(() => terminateProcess(child).catch(() => { }), timeoutMs);
731
- timer.unref?.();
732
- const finish = (callback) => {
733
- if (settled)
734
- return;
735
- settled = true;
736
- clearTimeout(timer);
737
- signal?.removeEventListener("abort", abortHandler);
738
- callback();
739
- };
740
- const abortHandler = () => {
741
- aborted = true;
742
- terminateProcess(child).catch(() => { });
743
- };
744
- signal?.addEventListener("abort", abortHandler, { once: true });
745
- child.once("error", (error) => {
746
- finish(() => reject(new HitchError(`failed to start ${path.basename(executable)}: ${error.message}`, {
747
- code: failureCode,
748
- exitCode: failureExitCode,
749
- cause: error,
750
- })));
751
- });
752
- child.once("close", (code, processSignal) => {
753
- if (aborted)
754
- return finish(() => reject(cancelledError()));
755
- if (code === 0)
756
- return finish(() => resolve({ stdout, stderr }));
757
- const detail = stderr.trim() || stdout.trim();
758
- finish(() => reject(new HitchError(`${path.basename(executable)} exited with code ${code ?? "null"}${processSignal ? ` (${processSignal})` : ""}${detail ? `: ${detail}` : ""}`, { code: failureCode, exitCode: failureExitCode })));
759
- });
760
- });
761
- }
762
- function commandExecutable(command, env) {
763
- const override = {
764
- npm: "HITCH_NPM_PATH",
765
- git: "HITCH_GIT_PATH",
766
- cargo: "HITCH_CARGO_PATH",
767
- bun: "HITCH_BUN_PATH",
768
- pnpm: "HITCH_PNPM_PATH",
769
- }[command];
770
- return override && env[override]?.trim() ? env[override].trim() : command;
771
- }
772
- async function commandVersion(executable, env, signal) {
773
- try {
774
- return (await runCommand(executable, ["--version"], { env, signal, timeoutMs: 5_000 })).stdout.trim() || "unknown";
775
- }
776
- catch (error) {
777
- if (error?.code === "cancelled")
778
- throw error;
779
- return "unknown";
780
- }
781
- }
782
- function recipeIdentity(source) {
783
- return {
784
- type: source.type,
785
- package: source.package,
786
- packages: source.packages,
787
- bin: source.bin,
788
- install_mode: source.install_mode,
789
- url: source.url,
790
- commands: source.commands,
791
- entrypoint: source.entrypoint,
792
- };
793
- }
794
- function digest(value) {
795
- return `sha256:${createHash("sha256").update(canonicalJSON(value)).digest("hex")}`;
796
- }
797
- function canonicalJSON(value) {
798
- if (Array.isArray(value))
799
- return `[${value.map(canonicalJSON).join(",")}]`;
800
- if (value && typeof value === "object") {
801
- const record = value;
802
- return `{${Object.keys(record).filter((key) => record[key] !== undefined).sort().map((key) => `${JSON.stringify(key)}:${canonicalJSON(record[key])}`).join(",")}}`;
803
- }
804
- return JSON.stringify(value);
805
- }
806
- function sanitizeUrl(value) {
807
- try {
808
- const url = new URL(value);
809
- url.username = "";
810
- url.password = "";
811
- url.search = "";
812
- url.hash = "";
813
- return url.href;
814
- }
815
- catch {
816
- return value;
817
- }
818
- }
819
- function gitCacheDirectory(paths, sourceUrl) {
820
- const key = createHash("sha256").update(sourceUrl).digest("hex");
821
- return path.join(paths.sourceCache, `git-${key}`);
822
- }
823
- async function newStagingDirectory(paths, harnessId) {
824
- await ensureDir(paths.temporary);
825
- const directory = path.join(paths.temporary, `${harnessId}-${process.pid}-${Date.now()}-${randomBytes(6).toString("hex")}`);
826
- await ensureDir(directory);
827
- return directory;
828
- }
829
- async function npmPackageEntrypoint(directory, packageName, binName, nodeModulesDirectory = "node_modules") {
830
- const packageDirectory = path.join(directory, nodeModulesDirectory, ...packageName.split("/"));
831
- let metadata;
832
- try {
833
- metadata = JSON.parse(await readFile(path.join(packageDirectory, "package.json"), "utf8"));
834
- }
835
- catch (error) {
836
- throw new HitchError(`installed package has no readable package.json: ${packageName}`, {
837
- code: "artifact_invalid",
838
- exitCode: 5,
839
- cause: error,
840
- });
841
- }
842
- const binTarget = typeof metadata.bin === "string" ? metadata.bin : metadata.bin?.[binName];
843
- if (typeof binTarget !== "string" || !binTarget) {
844
- throw new HitchError(`installed package does not declare the ${binName} executable: ${packageName}`, {
845
- code: "artifact_invalid",
846
- exitCode: 5,
847
- });
848
- }
849
- const absolute = path.resolve(packageDirectory, binTarget);
850
- if (absolute !== packageDirectory && !absolute.startsWith(`${packageDirectory}${path.sep}`)) {
851
- throw new HitchError(`package executable escapes its installation directory: ${packageName}`, {
852
- code: "artifact_invalid",
853
- exitCode: 5,
854
- });
855
- }
856
- return path.relative(directory, absolute);
857
- }
858
- function entrypointLauncher(entrypoint) {
859
- return /\.(?:cjs|mjs|js)$/i.test(entrypoint) ? "node" : "direct";
860
- }
861
- function artifactInvocation(manifest, directory) {
862
- const entrypoint = path.join(directory, manifest.entrypoint);
863
- return manifest.launcher === "node"
864
- ? { executable: process.execPath, entrypoint_args: [entrypoint] }
865
- : { executable: entrypoint, entrypoint_args: [] };
866
- }
867
- async function assertEntrypoint(file, launcher, reference) {
868
- try {
869
- await access(file, launcher === "node" ? constants.R_OK : constants.X_OK);
870
- const info = await stat(file);
871
- if (!info.isFile())
872
- throw new Error("entrypoint is not a file");
873
- }
874
- catch (error) {
875
- throw new HitchError(`prepared artifact has no executable entrypoint for ${reference}: ${file}`, {
876
- code: "artifact_invalid",
877
- exitCode: 5,
878
- cause: error,
879
- });
880
- }
881
- }
882
- async function optionalFileDigest(file) {
883
- try {
884
- return `sha256:${createHash("sha256").update(await readFile(file)).digest("hex")}`;
885
- }
886
- catch (error) {
887
- if (error?.code === "ENOENT")
888
- return null;
889
- throw error;
890
- }
891
- }
892
- async function assertNpmResolution(directory, resolved) {
893
- let lock;
894
- try {
895
- lock = JSON.parse(await readFile(path.join(directory, "package-lock.json"), "utf8"));
896
- }
897
- catch (error) {
898
- throw new HitchError(`npm did not produce a readable dependency lock for ${resolved.canonical_ref}`, {
899
- code: "artifact_invalid",
900
- exitCode: 5,
901
- cause: error,
902
- });
903
- }
904
- const packageName = resolved.source.package;
905
- const installed = packageName ? lock.packages?.[`node_modules/${packageName}`] : undefined;
906
- if (installed?.version !== resolved.revision.version || installed?.integrity !== resolved.source.integrity) {
907
- throw new HitchError(`installed package integrity does not match the resolution for ${resolved.canonical_ref}`, {
908
- code: "artifact_integrity_mismatch",
909
- exitCode: 5,
910
- });
911
- }
912
- }
913
- async function packResolvedNpmTarball(npm, directory, resolved, env, signal) {
914
- const tarball = resolved.source.tarball;
915
- if (!tarball)
916
- throw new HitchError("npm resolution is missing its tarball", { code: "prepare_failed", exitCode: 5 });
917
- const result = await runCommand(npm, ["pack", "--json", "--pack-destination", directory, tarball], {
918
- env,
919
- signal,
920
- failureCode: "prepare_failed",
921
- failureExitCode: 5,
922
- });
923
- let packed;
924
- try {
925
- const records = JSON.parse(result.stdout);
926
- if (Array.isArray(records) && records.length === 1)
927
- packed = records[0];
928
- }
929
- catch {
930
- // The validation error below includes the resolved reference without exposing npm output.
931
- }
932
- const filename = packed?.filename;
933
- if (packed?.name !== resolved.source.package
934
- || packed?.version !== resolved.revision.version
935
- || packed?.integrity !== resolved.source.integrity
936
- || typeof filename !== "string"
937
- || !filename
938
- || path.basename(filename) !== filename) {
939
- throw new HitchError(`packed package integrity does not match the resolution for ${resolved.canonical_ref}`, {
940
- code: "artifact_integrity_mismatch",
941
- exitCode: 5,
942
- });
943
- }
944
- const archive = path.join(directory, filename);
945
- try {
946
- if (!(await lstat(archive)).isFile())
947
- throw new Error("packed archive is not a file");
948
- }
949
- catch (error) {
950
- throw new HitchError(`npm did not produce a readable package archive for ${resolved.canonical_ref}`, {
951
- code: "artifact_invalid",
952
- exitCode: 5,
953
- cause: error,
954
- });
955
- }
956
- return archive;
957
- }
958
- async function assertGlobalNpmResolution(directory, resolved) {
959
- const packageName = resolved.source.package;
960
- let metadata;
961
- try {
962
- metadata = JSON.parse(await readFile(path.join(directory, "lib", "node_modules", ...(packageName || "").split("/"), "package.json"), "utf8"));
963
- }
964
- catch (error) {
965
- throw new HitchError(`npm did not produce a readable global package for ${resolved.canonical_ref}`, {
966
- code: "artifact_invalid",
967
- exitCode: 5,
968
- cause: error,
969
- });
970
- }
971
- if (metadata.name !== packageName || metadata.version !== resolved.revision.version) {
972
- throw new HitchError(`installed package version does not match the resolution for ${resolved.canonical_ref}`, {
973
- code: "artifact_integrity_mismatch",
974
- exitCode: 5,
975
- });
976
- }
977
- }
978
- async function sourceLockIdentity(directory) {
979
- const candidates = ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "bun.lock", path.join("codex-rs", "Cargo.lock")];
980
- const locks = {};
981
- for (const candidate of candidates) {
982
- const identity = await optionalFileDigest(path.join(directory, candidate));
983
- if (identity)
984
- locks[candidate] = identity;
985
- }
986
- return Object.keys(locks).length > 0 ? digest(locks) : null;
987
- }
988
- function normalizePreparationError(error, reference) {
989
- if (error instanceof HitchError)
990
- return error;
991
- return new HitchError(`failed to prepare ${reference}: ${error?.message || String(error)}`, {
992
- code: "prepare_failed",
993
- exitCode: 5,
994
- cause: error,
995
- });
996
- }
997
- async function executableMatches(executable, identity) {
998
- try {
999
- return await fingerprintExecutable(executable) === identity;
1000
- }
1001
- catch {
1002
- return false;
1003
- }
1004
- }
1005
- async function artifactMatches(directory, manifest) {
1006
- if (!manifest?.entrypoint || !manifest.entrypoint_integrity || !manifest.artifact_integrity)
1007
- return false;
1008
- const root = path.resolve(directory);
1009
- const executable = path.resolve(root, manifest.entrypoint);
1010
- if (executable !== root && !executable.startsWith(`${root}${path.sep}`))
1011
- return false;
1012
- if (!await executableMatches(executable, manifest.entrypoint_integrity))
1013
- return false;
1014
- try {
1015
- return await artifactDirectoryIntegrity(directory) === manifest.artifact_integrity;
1016
- }
1017
- catch {
1018
- return false;
1019
- }
1020
- }
1021
- async function artifactDirectoryIntegrity(directory) {
1022
- const root = path.resolve(directory);
1023
- const hash = createHash("sha256");
1024
- await digestArtifactDirectory(hash, root, root, "", true);
1025
- return `sha256:${hash.digest("hex")}`;
1026
- }
1027
- async function digestArtifactDirectory(hash, root, directory, relative, topLevel) {
1028
- const entries = await readdir(directory);
1029
- entries.sort();
1030
- for (const name of entries) {
1031
- if (topLevel && name === "artifact.json")
1032
- continue;
1033
- const absolute = path.join(directory, name);
1034
- const childRelative = relative ? path.join(relative, name) : name;
1035
- const info = await lstat(absolute);
1036
- if (info.isDirectory()) {
1037
- hash.update(`d\0${childRelative}\0${info.mode & 0o7777}\0`);
1038
- await digestArtifactDirectory(hash, root, absolute, childRelative, false);
1039
- }
1040
- else if (info.isFile()) {
1041
- hash.update(`f\0${childRelative}\0${info.mode & 0o7777}\0${info.size}\0`);
1042
- for await (const chunk of createReadStream(absolute))
1043
- hash.update(chunk);
1044
- hash.update("\0");
1045
- }
1046
- else if (info.isSymbolicLink()) {
1047
- const target = await readlink(absolute);
1048
- const resolvedTarget = path.resolve(path.dirname(absolute), target);
1049
- if (resolvedTarget !== root && !resolvedTarget.startsWith(`${root}${path.sep}`)) {
1050
- throw new HitchError(`artifact symlink escapes its installation directory: ${absolute}`, {
1051
- code: "artifact_invalid",
1052
- exitCode: 5,
1053
- });
1054
- }
1055
- hash.update(`l\0${childRelative}\0${target}\0`);
1056
- }
1057
- else {
1058
- throw new HitchError(`unsupported special file in prepared artifact: ${absolute}`, {
1059
- code: "artifact_invalid",
1060
- exitCode: 5,
1061
- });
1062
- }
1063
- }
1064
- }
1065
- function throwIfAborted(signal) {
1066
- if (signal?.aborted)
1067
- throw cancelledError();
1068
- }
1069
- function cancelledError() {
1070
- return new HitchError("harness preparation cancelled", { code: "cancelled", exitCode: 9 });
1071
- }
1072
- function assertObservedVersion(observed, expected, reference) {
1073
- const escaped = expected.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1074
- if (!observed || !new RegExp(`(?:^|\\s)v?${escaped}(?:\\s|$)`).test(observed)) {
1075
- throw new HitchError(`prepared artifact for ${reference} reported ${observed || "no version"}; expected ${expected}`, {
1076
- code: "artifact_invalid",
1077
- exitCode: 5,
1078
- });
1079
- }
1080
- }
1081
- //# sourceMappingURL=artifacts.js.map