@dsh-enhanced/plugin-control-plane 0.1.48 → 0.1.53

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 (185) hide show
  1. package/README.md +187 -15
  2. package/bin/dsh-live-qualification-authority.js +15 -0
  3. package/bin/dsh-local-release-adapter.js +331 -9
  4. package/bin/dsh-systemd-host-attestor.js +359 -9
  5. package/bin/dsh-systemd-host-authority.js +12 -0
  6. package/lib/adoption-coordinator.d.ts +3 -1
  7. package/lib/adoption-coordinator.d.ts.map +1 -1
  8. package/lib/adoption-coordinator.js +24 -9
  9. package/lib/adoption-coordinator.js.map +1 -1
  10. package/lib/cli.d.ts.map +1 -1
  11. package/lib/cli.js +33 -14
  12. package/lib/cli.js.map +1 -1
  13. package/lib/creation-capability-feedback.d.ts +23 -0
  14. package/lib/creation-capability-feedback.d.ts.map +1 -0
  15. package/lib/creation-capability-feedback.js +132 -0
  16. package/lib/creation-capability-feedback.js.map +1 -0
  17. package/lib/creation-capability-journal.d.ts +66 -0
  18. package/lib/creation-capability-journal.d.ts.map +1 -0
  19. package/lib/creation-capability-journal.js +775 -0
  20. package/lib/creation-capability-journal.js.map +1 -0
  21. package/lib/creation-capability-runtime.d.ts +63 -0
  22. package/lib/creation-capability-runtime.d.ts.map +1 -0
  23. package/lib/creation-capability-runtime.js +578 -0
  24. package/lib/creation-capability-runtime.js.map +1 -0
  25. package/lib/creation-capability-source.d.ts +37 -0
  26. package/lib/creation-capability-source.d.ts.map +1 -0
  27. package/lib/creation-capability-source.js +328 -0
  28. package/lib/creation-capability-source.js.map +1 -0
  29. package/lib/creation-capability-types.d.ts +265 -0
  30. package/lib/creation-capability-types.d.ts.map +1 -0
  31. package/lib/creation-capability-types.js +2 -0
  32. package/lib/creation-capability-types.js.map +1 -0
  33. package/lib/deployment-readiness.d.ts +16 -0
  34. package/lib/deployment-readiness.d.ts.map +1 -1
  35. package/lib/deployment-readiness.js +48 -1
  36. package/lib/deployment-readiness.js.map +1 -1
  37. package/lib/effect-blocked-replay.js +2 -2
  38. package/lib/effect-blocked-replay.js.map +1 -1
  39. package/lib/foreground-deployment-runtime.d.ts +3 -0
  40. package/lib/foreground-deployment-runtime.d.ts.map +1 -1
  41. package/lib/foreground-deployment-runtime.js +55 -13
  42. package/lib/foreground-deployment-runtime.js.map +1 -1
  43. package/lib/foreground-deployment.d.ts.map +1 -1
  44. package/lib/foreground-deployment.js +5 -0
  45. package/lib/foreground-deployment.js.map +1 -1
  46. package/lib/host-attestor.d.ts +2 -0
  47. package/lib/host-attestor.d.ts.map +1 -1
  48. package/lib/host-attestor.js +8 -1
  49. package/lib/host-attestor.js.map +1 -1
  50. package/lib/host-inputs.d.ts +13 -0
  51. package/lib/host-inputs.d.ts.map +1 -0
  52. package/lib/host-inputs.js +162 -0
  53. package/lib/host-inputs.js.map +1 -0
  54. package/lib/host-maintenance.d.ts +61 -0
  55. package/lib/host-maintenance.d.ts.map +1 -0
  56. package/lib/host-maintenance.js +158 -0
  57. package/lib/host-maintenance.js.map +1 -0
  58. package/lib/host-readiness.d.ts +5 -0
  59. package/lib/host-readiness.d.ts.map +1 -0
  60. package/lib/host-readiness.js +22 -0
  61. package/lib/host-readiness.js.map +1 -0
  62. package/lib/index.d.ts +16 -0
  63. package/lib/index.d.ts.map +1 -1
  64. package/lib/index.js +7 -0
  65. package/lib/index.js.map +1 -1
  66. package/lib/live-qualification-authority.d.ts +32 -0
  67. package/lib/live-qualification-authority.d.ts.map +1 -0
  68. package/lib/live-qualification-authority.js +280 -0
  69. package/lib/live-qualification-authority.js.map +1 -0
  70. package/lib/live-qualification-client.d.ts +4 -0
  71. package/lib/live-qualification-client.d.ts.map +1 -0
  72. package/lib/live-qualification-client.js +22 -0
  73. package/lib/live-qualification-client.js.map +1 -0
  74. package/lib/live-qualification-runtime.d.ts +78 -0
  75. package/lib/live-qualification-runtime.d.ts.map +1 -0
  76. package/lib/live-qualification-runtime.js +367 -0
  77. package/lib/live-qualification-runtime.js.map +1 -0
  78. package/lib/live-qualification.d.ts +73 -0
  79. package/lib/live-qualification.d.ts.map +1 -0
  80. package/lib/live-qualification.js +164 -0
  81. package/lib/live-qualification.js.map +1 -0
  82. package/lib/native-source-build-worker.d.ts +4 -0
  83. package/lib/native-source-build-worker.d.ts.map +1 -0
  84. package/lib/native-source-build-worker.js +87 -0
  85. package/lib/native-source-build-worker.js.map +1 -0
  86. package/lib/native-source-build.d.ts +36 -0
  87. package/lib/native-source-build.d.ts.map +1 -0
  88. package/lib/native-source-build.js +143 -0
  89. package/lib/native-source-build.js.map +1 -0
  90. package/lib/native-source-pack.d.ts +11 -0
  91. package/lib/native-source-pack.d.ts.map +1 -0
  92. package/lib/native-source-pack.js +83 -0
  93. package/lib/native-source-pack.js.map +1 -0
  94. package/lib/owner-task-gaps.d.ts +2 -0
  95. package/lib/owner-task-gaps.d.ts.map +1 -1
  96. package/lib/owner-task-gaps.js +9 -1
  97. package/lib/owner-task-gaps.js.map +1 -1
  98. package/lib/runtime-epoch.d.ts +69 -0
  99. package/lib/runtime-epoch.d.ts.map +1 -0
  100. package/lib/runtime-epoch.js +139 -0
  101. package/lib/runtime-epoch.js.map +1 -0
  102. package/lib/runtime-observer-protocol.d.ts +3 -3
  103. package/lib/runtime-observer-protocol.js +5 -5
  104. package/lib/runtime-observer-protocol.js.map +1 -1
  105. package/lib/runtime-observer.d.ts.map +1 -1
  106. package/lib/runtime-observer.js +42 -6
  107. package/lib/runtime-observer.js.map +1 -1
  108. package/lib/service.d.ts +248 -4
  109. package/lib/service.d.ts.map +1 -1
  110. package/lib/service.js +1431 -64
  111. package/lib/service.js.map +1 -1
  112. package/lib/source-adoption-authority.d.ts +3 -0
  113. package/lib/source-adoption-authority.d.ts.map +1 -1
  114. package/lib/source-adoption-authority.js +13 -3
  115. package/lib/source-adoption-authority.js.map +1 -1
  116. package/lib/source-adoption-runner.d.ts +3 -0
  117. package/lib/source-adoption-runner.d.ts.map +1 -1
  118. package/lib/source-adoption-runner.js +16 -4
  119. package/lib/source-adoption-runner.js.map +1 -1
  120. package/lib/source-approval-authority.d.ts.map +1 -1
  121. package/lib/source-approval-authority.js +1 -0
  122. package/lib/source-approval-authority.js.map +1 -1
  123. package/lib/source-baseline.d.ts +37 -0
  124. package/lib/source-baseline.d.ts.map +1 -0
  125. package/lib/source-baseline.js +250 -0
  126. package/lib/source-baseline.js.map +1 -0
  127. package/lib/source-build.d.ts +43 -9
  128. package/lib/source-build.d.ts.map +1 -1
  129. package/lib/source-build.js +670 -57
  130. package/lib/source-build.js.map +1 -1
  131. package/lib/source-context.d.ts +18 -0
  132. package/lib/source-context.d.ts.map +1 -1
  133. package/lib/source-context.js +85 -11
  134. package/lib/source-context.js.map +1 -1
  135. package/lib/source-creation.d.ts +106 -0
  136. package/lib/source-creation.d.ts.map +1 -0
  137. package/lib/source-creation.js +664 -0
  138. package/lib/source-creation.js.map +1 -0
  139. package/lib/source-job-types.d.ts +28 -1
  140. package/lib/source-job-types.d.ts.map +1 -1
  141. package/lib/source-jobs.d.ts +33 -3
  142. package/lib/source-jobs.d.ts.map +1 -1
  143. package/lib/source-jobs.js +420 -43
  144. package/lib/source-jobs.js.map +1 -1
  145. package/lib/source-maintenance.d.ts +66 -0
  146. package/lib/source-maintenance.d.ts.map +1 -0
  147. package/lib/source-maintenance.js +177 -0
  148. package/lib/source-maintenance.js.map +1 -0
  149. package/lib/source-revision.d.ts +57 -0
  150. package/lib/source-revision.d.ts.map +1 -0
  151. package/lib/source-revision.js +175 -0
  152. package/lib/source-revision.js.map +1 -0
  153. package/lib/source-workspace.d.ts +19 -14
  154. package/lib/source-workspace.d.ts.map +1 -1
  155. package/lib/source-workspace.js +90 -17
  156. package/lib/source-workspace.js.map +1 -1
  157. package/lib/sqlite.d.ts +13 -3
  158. package/lib/sqlite.d.ts.map +1 -1
  159. package/lib/sqlite.js +362 -1
  160. package/lib/sqlite.js.map +1 -1
  161. package/lib/store.d.ts +258 -3
  162. package/lib/store.d.ts.map +1 -1
  163. package/lib/store.js +2257 -96
  164. package/lib/store.js.map +1 -1
  165. package/lib/systemd-host-authority.d.ts +68 -0
  166. package/lib/systemd-host-authority.d.ts.map +1 -0
  167. package/lib/systemd-host-authority.js +424 -0
  168. package/lib/systemd-host-authority.js.map +1 -0
  169. package/lib/task-observation-evidence.d.ts +32 -0
  170. package/lib/task-observation-evidence.d.ts.map +1 -0
  171. package/lib/task-observation-evidence.js +62 -0
  172. package/lib/task-observation-evidence.js.map +1 -0
  173. package/lib/task-observation-runtime.d.ts +2 -3
  174. package/lib/task-observation-runtime.d.ts.map +1 -1
  175. package/lib/task-observation-runtime.js +7 -39
  176. package/lib/task-observation-runtime.js.map +1 -1
  177. package/lib/task-observation-store.js +1 -1
  178. package/lib/trust.d.ts.map +1 -1
  179. package/lib/trust.js +0 -0
  180. package/lib/trust.js.map +1 -1
  181. package/lib/types.d.ts +57 -7
  182. package/lib/types.d.ts.map +1 -1
  183. package/lib/version.d.ts +1 -1
  184. package/lib/version.js +1 -1
  185. package/package.json +22 -14
@@ -1,27 +1,509 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { chmod, lstat, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
4
- import { tmpdir } from 'node:os';
5
- import { dirname, join, resolve } from 'node:path';
4
+ import { homedir, tmpdir } from 'node:os';
5
+ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
6
6
  import { ControlPlaneCliError } from './errors.js';
7
- import { checkedSourceSnapshot, runLocalCommand } from './source-workspace.js';
7
+ import { verifyRevisedPluginWorkspace } from './source-revision.js';
8
+ import { verifyCreatedPluginWorkspace } from './source-creation.js';
9
+ import { checkedSourceSnapshot, runLocalBuffer, runLocalCommand } from './source-workspace.js';
10
+ /** Keep Config synchronous and independent of the Mac executor package: Linux
11
+ * authority runtimes copy this module without assistant-isolation. The native
12
+ * executor repeats this shape check and verifies the approved bytes at dispatch. */
13
+ function validNativeRuntimeShape(value) {
14
+ if (!value || typeof value !== 'object' || Array.isArray(value))
15
+ return false;
16
+ const runtime = value;
17
+ const hex = (part) => typeof part === 'string' && /^[0-9a-f]{64}$/u.test(part);
18
+ const canonical = (part) => typeof part === 'string' && isAbsolute(part)
19
+ && resolve(part) === part && !part.includes('\0') && !part.includes('\r') && !part.includes('\n');
20
+ const within = (path, root) => path === root || path.startsWith(root + sep);
21
+ return Object.keys(runtime).sort().join() === 'modulesPath,nodePath,nodeSha256,platform,roots,schemaVersion,versions'
22
+ && runtime.schemaVersion === 1 && runtime.platform === `darwin-${process.arch}` && canonical(runtime.nodePath)
23
+ && hex(runtime.nodeSha256) && canonical(runtime.modulesPath) && Array.isArray(runtime.roots)
24
+ && runtime.roots.length >= 1 && runtime.roots.length <= 32 && !!runtime.versions && typeof runtime.versions === 'object'
25
+ && Object.keys(runtime.versions).sort().join() === 'cordis,node,systemPrompt,tools,typescript'
26
+ && Object.values(runtime.versions).every(version => typeof version === 'string' && version.length >= 1 && version.length <= 128)
27
+ && runtime.roots.every(root => !!root && Object.keys(root).sort().join() === 'dev,ino,path,sha256'
28
+ && canonical(root.path) && root.path !== '/' && Number.isSafeInteger(root.dev) && root.dev >= 0
29
+ && Number.isSafeInteger(root.ino) && root.ino >= 0 && hex(root.sha256))
30
+ && new Set(runtime.roots.map(root => root.path)).size === runtime.roots.length
31
+ && runtime.roots.some(root => within(runtime.nodePath, root.path) && runtime.nodePath !== root.path)
32
+ && runtime.roots.some(root => within(runtime.modulesPath, root.path));
33
+ }
8
34
  const imageDigest = /^(?:[a-z0-9][a-z0-9._:/-]*@)?sha256:[a-f0-9]{64}$/u;
9
35
  const marker = /^DSH_PREPARED_PACK\t([^\t\n]+)\t([0-9]+)\t([a-f0-9]{64})\t([^\t\n]+)\t([^\t\n]+)$/mu;
36
+ const packFrame = 'DSH_PREPARED_PACK_BYTES_V1\t';
37
+ const maximumPackBytes = 32 * 1024 * 1024;
38
+ const maximumPackBase64Bytes = Math.ceil(maximumPackBytes / 3) * 4;
10
39
  const repositorySeccompSha256 = 'b1e4b5b709578785bd2aff4a3a344301997571ad0e8ae5747aec176571ddc342';
11
40
  /** Exact in-container command recorded in prepared source evidence. */
12
- export const PREPARED_SOURCE_BUILD_SCRIPT = 'set -eu; checked() { phase="$1"; shift; if "$@" >"/tmp/$phase.log" 2>&1; then return 0; else code=$?; printf "source build phase failed: %s\\n" "$phase" >&2; tail -c 8192 "/tmp/$phase.log" >&2; return "$code"; fi; }; umask 077; mkdir -p /workspace; tar -x -C /workspace; cd /workspace; checked install pnpm install --offline --frozen-lockfile --ignore-scripts; checked check pnpm check; mkdir -p /workspace/.dsh-pack; cd "$PLUGIN_ROOT"; checked pack pnpm pack --pack-destination /workspace/.dsh-pack; set -- /workspace/.dsh-pack/*.tgz; test "$#" = 1; pack="$1"; printf "DSH_PREPARED_PACK\\t%s\\t%s\\t%s\\t%s\\t%s\\n" "${pack##*/}" "$(wc -c < "$pack" | tr -d " ")" "$(sha256sum "$pack" | cut -d " " -f1)" "$(node --version)" "$(pnpm --version)"';
41
+ export const PREPARED_SOURCE_BUILD_SCRIPT = String.raw `set -eu
42
+ checked() {
43
+ phase="$1"; shift
44
+ if "$@" >"/tmp/$phase.log" 2>&1; then return 0; else
45
+ code=$?
46
+ printf 'source build phase failed: %s\n' "$phase" >&2
47
+ tail -c 8192 "/tmp/$phase.log" >&2
48
+ return "$code"
49
+ fi
50
+ }
51
+ umask 077
52
+ mkdir -p /workspace
53
+ archive="$(mktemp /tmp/dsh-source-input.XXXXXX)"
54
+ cat >"$archive"
55
+ archive_sha="$(sha256sum "$archive" | cut -d ' ' -f1)"
56
+ tar -xf "$archive" -C /workspace --no-same-owner --no-same-permissions
57
+ verify_input() {
58
+ current_sha="$(sha256sum "$archive" | cut -d ' ' -f1)"
59
+ if [ "$current_sha" != "$archive_sha" ]; then
60
+ printf 'source build input archive changed\n' >&2
61
+ return 1
62
+ fi
63
+ NODE_OPTIONS= NODE_PATH= node - "$archive" /workspace "$1" <<'DSH_SOURCE_VERIFY'
64
+ const fs = require('node:fs')
65
+ const path = require('node:path')
66
+ const archive = fs.openSync(process.argv[2], 'r')
67
+ const root = path.resolve(process.argv[3])
68
+ const initializing = process.argv[4] === 'init'
69
+ if (!initializing && process.argv[4] !== 'verify') throw new Error('invalid source verification phase')
70
+ const length = fs.fstatSync(archive).size
71
+ const fail = reason => { console.error('source build input changed: ' + reason); process.exit(1) }
72
+ const block = (position, size) => {
73
+ const out = Buffer.alloc(size)
74
+ if (fs.readSync(archive, out, 0, size, position) !== size) fail('archive truncated')
75
+ return out
76
+ }
77
+ const text = bytes => {
78
+ const end = bytes.indexOf(0)
79
+ const slice = end < 0 ? bytes : bytes.subarray(0, end)
80
+ const result = slice.toString('utf8')
81
+ if (!Buffer.from(result, 'utf8').equals(slice)) fail('archive path is not UTF-8')
82
+ return result
83
+ }
84
+ const octal = bytes => {
85
+ const value = text(bytes).trim()
86
+ if (!/^[0-7]+$/.test(value)) fail('unsupported archive number')
87
+ const parsed = Number.parseInt(value, 8)
88
+ if (!Number.isSafeInteger(parsed)) fail('archive number exceeds safe bound')
89
+ return parsed
90
+ }
91
+ const pax = bytes => {
92
+ const values = {}
93
+ for (let at = 0; at < bytes.length;) {
94
+ const space = bytes.indexOf(32, at)
95
+ if (space < 0) fail('malformed archive extension')
96
+ const size = Number(bytes.subarray(at, space).toString('ascii'))
97
+ if (!Number.isSafeInteger(size) || size <= space - at + 2 || at + size > bytes.length || bytes[at + size - 1] !== 10) fail('malformed archive extension')
98
+ const value = bytes.subarray(space + 1, at + size - 1).toString('utf8')
99
+ const equal = value.indexOf('=')
100
+ if (equal < 1) fail('malformed archive extension')
101
+ values[value.slice(0, equal)] = value.slice(equal + 1)
102
+ at += size
103
+ }
104
+ return values
105
+ }
106
+ const archiveKey = raw => {
107
+ const name = raw.startsWith('./') ? raw.slice(2) : raw
108
+ const parts = name.replace(/\/$/, '').split('/')
109
+ if (parts.length === 0 || parts.some(part => part === '' || part === '.' || part === '..' || part.includes('\\') || part.includes('\0'))) fail('unsafe archive path')
110
+ return parts.join('/')
111
+ }
112
+ const checkedPath = raw => {
113
+ const parts = archiveKey(raw).split('/')
114
+ let current = root
115
+ for (let index = 0; index < parts.length - 1; index += 1) {
116
+ current = path.join(current, parts[index])
117
+ const stat = fs.lstatSync(current)
118
+ if (!stat.isDirectory() || stat.isSymbolicLink()) fail('archive parent replaced by a link')
119
+ }
120
+ const full = path.resolve(root, ...parts)
121
+ if (!full.startsWith(root + path.sep)) fail('archive path escaped workspace')
122
+ return full
123
+ }
124
+ let at = 0
125
+ let extension = {}
126
+ const members = []
127
+ const byName = new Map()
128
+ while (at + 512 <= length) {
129
+ const header = block(at, 512)
130
+ if (header.every(byte => byte === 0)) break
131
+ const size = octal(header.subarray(124, 136))
132
+ const mode = octal(header.subarray(100, 108)) & 0o7777
133
+ const kind = String.fromCharCode(header[156] || 48)
134
+ const end = at + 512 + size
135
+ if (!Number.isSafeInteger(end) || end > length) fail('archive member truncated')
136
+ if (kind === 'g' || kind === 'x') {
137
+ if (size > 1_048_576) fail('archive extension exceeds bound')
138
+ const parsed = pax(block(at + 512, size))
139
+ if (kind === 'x') extension = parsed
140
+ } else {
141
+ const prefix = text(header.subarray(345, 500))
142
+ const headerName = text(header.subarray(0, 100))
143
+ const name = extension.path || (prefix ? prefix + '/' + headerName : headerName)
144
+ const key = archiveKey(name)
145
+ if (byName.has(key)) fail('duplicate archive member: ' + name)
146
+ const member = { name, key, kind, size, mode, offset: at + 512,
147
+ link: extension.linkpath || text(header.subarray(157, 257)) }
148
+ members.push(member)
149
+ byName.set(key, member)
150
+ extension = {}
151
+ }
152
+ at = Math.ceil(end / 512) * 512
153
+ }
154
+ if (members.length === 0) fail('archive had no source files')
155
+ // Only immutable package manifests can authorize pnpm 11's pre-install bin
156
+ // normalization. A bin must already have Git's owner executable bit.
157
+ const bins = new Set()
158
+ for (const member of members) {
159
+ if (member.key !== 'package.json' && !/^(?:plugins|packages)\/[a-z0-9][a-z0-9-]*\/package\.json$/.test(member.key)) continue
160
+ if ((member.kind !== '0' && member.kind !== '7') || member.size > 1_048_576) fail('unsafe source package manifest')
161
+ const bytes = block(member.offset, member.size)
162
+ const content = bytes.toString('utf8')
163
+ if (!Buffer.from(content, 'utf8').equals(bytes)) fail('source package manifest is not UTF-8')
164
+ let manifest
165
+ try { manifest = JSON.parse(content) } catch { fail('invalid source package manifest') }
166
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest) || manifest.directories?.bin !== undefined) fail('unsupported source package bin')
167
+ const declared = manifest.bin
168
+ if (declared === undefined) continue
169
+ if (typeof declared !== 'string' && (!declared || typeof declared !== 'object' || Array.isArray(declared))) fail('invalid source package bin')
170
+ const paths = typeof declared === 'string' ? [declared] : Object.values(declared)
171
+ const packageRoot = member.key === 'package.json' ? '' : member.key.slice(0, -'/package.json'.length) + '/'
172
+ for (const raw of paths) {
173
+ if (typeof raw !== 'string' || raw.endsWith('/')) fail('invalid source package bin path')
174
+ const relative = archiveKey(raw)
175
+ const targetKey = packageRoot + relative
176
+ const parts = targetKey.split('/')
177
+ for (let index = 1; index < parts.length; index += 1) {
178
+ const parent = byName.get(parts.slice(0, index).join('/'))
179
+ if (parent && parent.kind !== '5') fail('source package bin parent is not a directory: ' + raw)
180
+ }
181
+ const target = byName.get(targetKey)
182
+ if (target === undefined) {
183
+ if (relative.startsWith('lib/')) continue
184
+ fail('source package bin target is missing: ' + raw)
185
+ }
186
+ if ((target.kind !== '0' && target.kind !== '7') || !(target.mode & 0o100)) fail('source package bin target is not an executable regular input: ' + raw)
187
+ bins.add(target.key)
188
+ }
189
+ }
190
+ const verifiedBins = new Map()
191
+ for (const member of members) {
192
+ const { name, key, kind, size, mode, link } = member
193
+ const full = checkedPath(name)
194
+ const before = fs.lstatSync(full)
195
+ if (kind === '0' || kind === '7') {
196
+ const expectedMode = !initializing && bins.has(key) ? 0o755 : mode & 0o700
197
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size !== size || (before.mode & 0o7777) !== expectedMode) fail('original file replaced: ' + name)
198
+ const input = fs.openSync(full, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
199
+ try {
200
+ const opened = fs.fstatSync(input)
201
+ if (opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== size || opened.nlink !== 1 || (opened.mode & 0o7777) !== expectedMode) fail('original file changed while opened: ' + name)
202
+ for (let offset = 0; offset < size; offset += 65_536) {
203
+ const count = Math.min(65_536, size - offset)
204
+ const expected = block(member.offset + offset, count)
205
+ const actual = Buffer.alloc(count)
206
+ if (fs.readSync(input, actual, 0, count, offset) !== count || !actual.equals(expected)) fail('original file content changed: ' + name)
207
+ }
208
+ const after = fs.fstatSync(input)
209
+ if (after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs || after.nlink !== 1 || (after.mode & 0o7777) !== expectedMode) fail('original file changed while compared: ' + name)
210
+ if (bins.has(key)) verifiedBins.set(key, { dev: after.dev, ino: after.ino, size: after.size, mtimeMs: after.mtimeMs, ctimeMs: after.ctimeMs })
211
+ } finally { fs.closeSync(input) }
212
+ } else if (kind === '5') {
213
+ if (!before.isDirectory() || before.isSymbolicLink() || (before.mode & 0o7777) !== (mode & 0o700)) fail('original directory replaced: ' + name)
214
+ } else if (kind === '2') {
215
+ if (!before.isSymbolicLink() || fs.readlinkSync(full) !== link) fail('original link replaced: ' + name)
216
+ } else fail('unsupported archive member type: ' + name)
217
+ }
218
+ if (initializing) {
219
+ for (const key of bins) {
220
+ const member = byName.get(key)
221
+ const full = checkedPath(member.name)
222
+ const input = fs.openSync(full, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
223
+ try {
224
+ const before = fs.fstatSync(input)
225
+ const verified = verifiedBins.get(key)
226
+ if (!verified || !before.isFile() || before.nlink !== 1 || before.dev !== verified.dev || before.ino !== verified.ino
227
+ || before.size !== verified.size || before.mtimeMs !== verified.mtimeMs || before.ctimeMs !== verified.ctimeMs
228
+ || (before.mode & 0o7777) !== (member.mode & 0o700)) fail('source package bin changed before normalization: ' + key)
229
+ fs.fchmodSync(input, 0o755)
230
+ if ((fs.fstatSync(input).mode & 0o7777) !== 0o755) fail('source package bin normalization failed: ' + key)
231
+ } finally { fs.closeSync(input) }
232
+ }
233
+ }
234
+ fs.closeSync(archive)
235
+ DSH_SOURCE_VERIFY
236
+ current_sha="$(sha256sum "$archive" | cut -d ' ' -f1)"
237
+ if [ "$current_sha" != "$archive_sha" ]; then
238
+ printf 'source build input archive changed\n' >&2
239
+ return 1
240
+ fi
241
+ }
242
+ cd /workspace
243
+ verify_input init
244
+ if [ "$(printenv DSH_SOURCE_TRUST_LOCKFILE 2>/dev/null || true)" = true ]; then
245
+ test "$(pnpm --version)" = 11.7.0 || { printf 'source creation requires pinned pnpm\n' >&2; exit 1; }
246
+ test -f /opt/dsh-source-baseline/pnpm-lock.yaml || { printf 'source creation baseline lock is absent\n' >&2; exit 1; }
247
+ test "$(sha256sum /opt/dsh-source-baseline/pnpm-lock.yaml | cut -d ' ' -f1)" = "$(printenv DSH_SOURCE_BASE_LOCK_SHA256 2>/dev/null || true)" || { printf 'source creation baseline lock differs\n' >&2; exit 1; }
248
+ checked install pnpm install --offline --frozen-lockfile --ignore-scripts --trust-lockfile
249
+ else
250
+ checked install pnpm install --offline --frozen-lockfile --ignore-scripts
251
+ fi
252
+ checked check /bin/sh -ceu 'umask 022; exec pnpm check'
253
+ verify_input verify
254
+ mkdir -p /workspace/.dsh-pack
255
+ cd "$PLUGIN_ROOT"
256
+ checked pack pnpm pack --pack-destination /workspace/.dsh-pack
257
+ verify_input verify
258
+ set -- /workspace/.dsh-pack/*.tgz
259
+ test "$#" = 1
260
+ pack="$1"
261
+ NODE_OPTIONS= NODE_PATH= node - "$archive" "$pack" "$PLUGIN_ROOT" "$archive_sha" <<'DSH_SOURCE_PACK_VERIFY'
262
+ const fs = require('node:fs')
263
+ const crypto = require('node:crypto')
264
+ const childProcess = require('node:child_process')
265
+ const path = require('node:path')
266
+ const zlib = require('node:zlib')
267
+ const fail = reason => { console.error('source packed artifact changed: ' + reason); process.exit(1) }
268
+ if (fs.statSync(process.argv[2]).size > 64 * 1024 * 1024) fail('source archive exceeds verification bound')
269
+ const source = fs.readFileSync(process.argv[2])
270
+ if (crypto.createHash('sha256').update(source).digest('hex') !== process.argv[5]) fail('immutable source archive changed')
271
+ const packedPath = process.argv[3]
272
+ const pluginRoot = process.argv[4]
273
+ if (!/^plugins\/[a-z0-9][a-z0-9-]*$/.test(pluginRoot)) fail('invalid plugin root')
274
+ const packedStat = fs.lstatSync(packedPath)
275
+ if (!packedStat.isFile() || packedStat.isSymbolicLink() || packedStat.nlink !== 1) fail('packed artifact is not a regular file')
276
+ if (packedStat.size > 32 * 1024 * 1024) fail('compressed artifact exceeds bound')
277
+ const packedFd = fs.openSync(packedPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
278
+ const opened = fs.fstatSync(packedFd)
279
+ if (opened.dev !== packedStat.dev || opened.ino !== packedStat.ino || opened.size !== packedStat.size) fail('packed artifact changed while opened')
280
+ const packed = fs.readFileSync(packedFd)
281
+ fs.closeSync(packedFd)
282
+ if (packed.length > 32 * 1024 * 1024) fail('compressed artifact exceeds bound')
283
+ let tar
284
+ try { tar = zlib.gunzipSync(packed, { maxOutputLength: 128 * 1024 * 1024 }) }
285
+ catch { fail('invalid or oversized gzip artifact') }
286
+ const string = value => {
287
+ const end = value.indexOf(0)
288
+ const bytes = end < 0 ? value : value.subarray(0, end)
289
+ const result = bytes.toString('utf8')
290
+ if (!Buffer.from(result, 'utf8').equals(bytes)) fail('invalid UTF-8 tar name')
291
+ return result
292
+ }
293
+ const octal = value => {
294
+ const raw = string(value).trim()
295
+ if (!/^[0-7]+$/.test(raw)) fail('invalid tar size')
296
+ const parsed = Number.parseInt(raw, 8)
297
+ if (!Number.isSafeInteger(parsed)) fail('oversized tar member')
298
+ return parsed
299
+ }
300
+ const pax = bytes => {
301
+ const values = {}
302
+ for (let at = 0; at < bytes.length;) {
303
+ const space = bytes.indexOf(32, at)
304
+ if (space < 0) fail('malformed tar extension')
305
+ const size = Number(bytes.subarray(at, space).toString('ascii'))
306
+ if (!Number.isSafeInteger(size) || size <= space - at + 2 || at + size > bytes.length || bytes[at + size - 1] !== 10) fail('malformed tar extension')
307
+ const value = bytes.subarray(space + 1, at + size - 1).toString('utf8')
308
+ const equal = value.indexOf('=')
309
+ if (equal < 1 || Object.hasOwn(values, value.slice(0, equal))) fail('malformed tar extension')
310
+ values[value.slice(0, equal)] = value.slice(equal + 1)
311
+ at += size
312
+ }
313
+ return values
314
+ }
315
+ const entries = (buffer, prefix) => {
316
+ const result = new Map()
317
+ let at = 0; let extension = {}; let global = {}
318
+ while (at + 512 <= buffer.length) {
319
+ const header = buffer.subarray(at, at + 512)
320
+ if (header.every(byte => byte === 0)) break
321
+ const checksum = octal(header.subarray(148, 156))
322
+ let actualChecksum = 0
323
+ for (let index = 0; index < 512; index += 1) actualChecksum += index >= 148 && index < 156 ? 32 : header[index]
324
+ if (checksum !== actualChecksum) fail('invalid tar header checksum')
325
+ const size = octal(header.subarray(124, 136))
326
+ const end = at + 512 + size
327
+ if (!Number.isSafeInteger(end) || end > buffer.length) fail('truncated tar member')
328
+ const kind = String.fromCharCode(header[156] || 48)
329
+ if (kind === 'g' || kind === 'x') {
330
+ if (size > 1_048_576) fail('oversized tar extension')
331
+ const parsed = pax(buffer.subarray(at + 512, end))
332
+ if (kind === 'g') global = { ...global, ...parsed }; else extension = parsed
333
+ } else {
334
+ const attributes = { ...global, ...extension }
335
+ if (Object.keys(attributes).some(key => !['path', 'mtime', 'atime', 'ctime', 'comment', 'uid', 'gid', 'uname', 'gname', 'mode'].includes(key))) fail('unsupported tar extension')
336
+ const headerName = string(header.subarray(0, 100))
337
+ const pathPrefix = string(header.subarray(345, 500))
338
+ const raw = attributes.path || (pathPrefix ? pathPrefix + '/' + headerName : headerName)
339
+ const name = raw.endsWith('/') ? raw.slice(0, -1) : raw
340
+ if (!name || name.startsWith('/') || name.includes('\\') || name.includes('\0') || name.split('/').some(part => !part || part === '.' || part === '..')) fail('unsafe tar member path')
341
+ if (result.has(name)) fail('duplicate tar member: ' + name)
342
+ if (kind !== '0' && kind !== '5' && kind !== '7') fail('unsupported tar member type: ' + name)
343
+ if (prefix === 'package' && name !== 'package' && !name.startsWith('package/')) fail('tar member outside package')
344
+ result.set(name, { kind, bytes: buffer.subarray(at + 512, end) })
345
+ extension = {}
346
+ }
347
+ at = Math.ceil(end / 512) * 512
348
+ }
349
+ if (at + 1024 > buffer.length || !buffer.subarray(at).every(byte => byte === 0)) fail('tar has no valid trailer')
350
+ return result
351
+ }
352
+ const original = entries(source, 'source')
353
+ const artifact = entries(tar, 'package')
354
+ const required = ['package.json', 'README.md', 'LICENSE', 'cordis.patch.yml']
355
+ for (const relative of required) {
356
+ const expected = original.get(pluginRoot + '/' + relative)
357
+ const actual = artifact.get('package/' + relative)
358
+ if (!expected || expected.kind !== '0' || !actual || actual.kind !== '0') fail('missing fixed package file: ' + relative)
359
+ }
360
+ const json = bytes => { try { return JSON.parse(bytes.toString('utf8')) } catch { fail('invalid package manifest') } }
361
+ const originalManifest = json(original.get(pluginRoot + '/package.json').bytes)
362
+ const actualManifest = json(artifact.get('package/package.json').bytes)
363
+ const expectedName = '@dsh-enhanced/' + pluginRoot.slice('plugins/'.length)
364
+ if (originalManifest.name !== expectedName || actualManifest.name !== expectedName
365
+ || originalManifest.dsh?.bundle?.patch !== './cordis.patch.yml'
366
+ || actualManifest.dsh?.bundle?.patch !== './cordis.patch.yml') fail('package identity or bundle patch changed')
367
+ const catalogs = new Map()
368
+ const workspaceYaml = original.get('pnpm-workspace.yaml')
369
+ if (!workspaceYaml || workspaceYaml.kind !== '0') fail('immutable workspace catalog is missing')
370
+ let catalogSection = ''
371
+ let catalogName = ''
372
+ for (const line of workspaceYaml.bytes.toString('utf8').split(/\r?\n/)) {
373
+ if (line === 'catalog:') { catalogSection = 'default'; catalogName = ''; continue }
374
+ if (line === 'catalogs:') { catalogSection = 'named'; catalogName = ''; continue }
375
+ if (/^[^\s#][^:]*:/u.test(line)) { catalogSection = ''; catalogName = ''; continue }
376
+ if (catalogSection === 'named') {
377
+ const heading = /^ ([a-z][a-z0-9-]*):\s*$/u.exec(line)
378
+ if (heading) { catalogName = heading[1]; continue }
379
+ }
380
+ const pattern = catalogSection === 'default' ? /^ (?:'([^']+)'|"([^"]+)"|([^\s:]+)):\s+(.+?)\s*$/u
381
+ : catalogSection === 'named' && catalogName ? /^ (?:'([^']+)'|"([^"]+)"|([^\s:]+)):\s+(.+?)\s*$/u : null
382
+ const matched = pattern?.exec(line)
383
+ if (!matched) continue
384
+ const name = matched[1] || matched[2] || matched[3]
385
+ const raw = matched[4]
386
+ const value = /^(['"])(.*)\1$/u.exec(raw)?.[2] ?? raw
387
+ const key = (catalogSection === 'named' ? catalogName : '') + '\0' + name
388
+ if (catalogs.has(key) || !value || value.includes('#')) fail('ambiguous immutable workspace catalog')
389
+ catalogs.set(key, value)
390
+ }
391
+ const workspaceVersions = new Map()
392
+ for (const [name, member] of original) {
393
+ if (!/^(?:plugins|packages)\/[^/]+\/package\.json$/u.test(name) || member.kind !== '0') continue
394
+ const manifest = json(member.bytes)
395
+ if (typeof manifest.name !== 'string' || typeof manifest.version !== 'string' || workspaceVersions.has(manifest.name)) fail('ambiguous workspace package identity')
396
+ workspaceVersions.set(manifest.name, manifest.version)
397
+ }
398
+ const dependencies = new Set(['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'])
399
+ for (const key of dependencies) {
400
+ const originalDependencies = originalManifest[key] || {}
401
+ const packedDependencies = actualManifest[key] || {}
402
+ if (!originalDependencies || typeof originalDependencies !== 'object' || Array.isArray(originalDependencies)
403
+ || !packedDependencies || typeof packedDependencies !== 'object' || Array.isArray(packedDependencies)
404
+ || JSON.stringify(Object.keys(originalDependencies).sort()) !== JSON.stringify(Object.keys(packedDependencies).sort())) fail('package dependency names differ')
405
+ for (const [name, value] of Object.entries(originalDependencies)) {
406
+ const packedValue = packedDependencies[name]
407
+ if (typeof value !== 'string' || typeof packedValue !== 'string' || packedValue.length > 256) fail('package dependency value differs')
408
+ let expected = value
409
+ if (value.startsWith('catalog:')) {
410
+ const catalogName = value.slice('catalog:'.length)
411
+ expected = catalogs.get(catalogName + '\0' + name)
412
+ if (typeof expected !== 'string') fail('unresolved immutable workspace catalog')
413
+ } else if (value.startsWith('workspace:')) {
414
+ const selector = value.slice('workspace:'.length)
415
+ const version = workspaceVersions.get(name)
416
+ if (typeof version !== 'string' || !/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u.test(version)
417
+ || !['*', '^', '~'].includes(selector)) fail('unsupported workspace dependency locator')
418
+ expected = selector === '*' ? version : selector + version
419
+ }
420
+ if (packedValue !== expected) fail('package dependency value differs')
421
+ }
422
+ }
423
+ // pnpm 11.7.0 omits these fields, then appends its filtered scripts map.
424
+ // Derive that exact published order from the immutable archive, not the pack workspace.
425
+ const expectedManifest = {}
426
+ for (const [key, value] of Object.entries(originalManifest)) {
427
+ if (!['scripts', 'packageManager', 'pnpm'].includes(key)) expectedManifest[key] = value
428
+ }
429
+ if (originalManifest.scripts != null) {
430
+ if (typeof originalManifest.scripts !== 'object' || Array.isArray(originalManifest.scripts)) fail('invalid source package scripts')
431
+ const scripts = {}
432
+ const omitted = new Set(['prepublishOnly', 'prepack', 'prepare', 'postpack', 'publish', 'postpublish'])
433
+ for (const [key, value] of Object.entries(originalManifest.scripts)) {
434
+ if (!omitted.has(key)) scripts[key] = value
435
+ }
436
+ expectedManifest.scripts = scripts
437
+ }
438
+ if (!actualManifest || typeof actualManifest !== 'object' || Array.isArray(actualManifest)) fail('invalid package manifest')
439
+ for (const key of dependencies) { delete expectedManifest[key]; delete actualManifest[key] }
440
+ if (JSON.stringify(expectedManifest) !== JSON.stringify(actualManifest)) fail('fixed manifest fields differ')
441
+ const requirePublished = (entry, field) => {
442
+ if (typeof entry === 'string') {
443
+ if (!entry.startsWith('./') || entry.includes('*') || entry.split('/').some(part => part === '..')
444
+ || !artifact.has('package/' + entry.slice(2)) || artifact.get('package/' + entry.slice(2)).kind !== '0') fail('published entry is missing: ' + field)
445
+ } else if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
446
+ for (const [key, nested] of Object.entries(entry)) requirePublished(nested, field + '.' + key)
447
+ } else fail('unsupported published entry: ' + field)
448
+ }
449
+ for (const field of ['main', 'types', 'exports', 'imports', 'bin']) {
450
+ if (originalManifest[field] !== undefined) requirePublished(originalManifest[field], field)
451
+ }
452
+ for (const [name, actual] of artifact) {
453
+ if (actual.kind !== '0' && actual.kind !== '7') continue
454
+ const expected = original.get(pluginRoot + name.slice('package'.length))
455
+ if (expected && name !== 'package/package.json' && !actual.bytes.equals(expected.bytes)) fail('fixed package file differs: ' + name)
456
+ if (!expected && !name.startsWith('package/lib/')) fail('unexpected package file: ' + name)
457
+ }
458
+ const pnpmVersion = childProcess.execFileSync('pnpm', ['--version'], { encoding: 'utf8', timeout: 5000 }).trim()
459
+ const digest = crypto.createHash('sha256').update(packed).digest('hex')
460
+ console.log(['DSH_PREPARED_PACK', path.basename(packedPath), packed.length, digest, process.version, pnpmVersion].join('\t'))
461
+ if (process.env.DSH_SOURCE_CAPTURE_PACK === 'true') process.stdout.write('DSH_PREPARED_PACK_BYTES_V1\t' + packed.toString('base64') + '\n')
462
+ DSH_SOURCE_PACK_VERIFY
463
+ `;
13
464
  function sourceBuildLimits(config) {
14
465
  const profile = config.profile ?? 'standard';
15
- return { profile, temporaryMiB: config.temporaryMiB ?? (profile === 'repository' ? 2_048 : 32) };
466
+ return { profile, temporaryMiB: config.temporaryMiB ?? (profile === 'standard' ? 32 : 2_048) };
16
467
  }
17
468
  export function validateSourceBuildConfig(config) {
18
- if (config === null || typeof config !== 'object' || Array.isArray(config) || (config.profile !== undefined && config.profile !== 'standard' && config.profile !== 'repository')
469
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
470
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source build configuration is invalid');
471
+ }
472
+ if ('backend' in config) {
473
+ const allowed = new Set(['backend', 'runtime', 'stateRoot', 'authorityDigest', 'expiresAt', 'maxRuns',
474
+ 'maxTotalDurationMs', 'timeoutMs', 'outputBytes', 'versioning']);
475
+ if (config.backend !== 'macos-seatbelt' || Object.keys(config).some(key => !allowed.has(key))
476
+ || typeof config.stateRoot !== 'string' || !config.stateRoot.startsWith('/') || resolve(config.stateRoot) !== config.stateRoot
477
+ || !/^[a-f0-9]{64}$/u.test(config.authorityDigest)
478
+ || !Number.isSafeInteger(config.expiresAt) || config.expiresAt <= 0
479
+ || !Number.isSafeInteger(config.maxRuns) || config.maxRuns < 1 || config.maxRuns > 1000
480
+ || !Number.isSafeInteger(config.maxTotalDurationMs) || config.maxTotalDurationMs < 60_000 || config.maxTotalDurationMs > 86_400_000
481
+ || !Number.isSafeInteger(config.timeoutMs) || config.timeoutMs < 1_000 || config.timeoutMs > 300_000
482
+ || !Number.isSafeInteger(config.outputBytes) || config.outputBytes < 4096 || config.outputBytes > 1_048_576
483
+ || (config.versioning !== undefined && config.versioning !== 'patch')) {
484
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'native source build configuration is invalid');
485
+ }
486
+ try {
487
+ if (!validNativeRuntimeShape(config.runtime))
488
+ throw new Error('invalid native runtime');
489
+ }
490
+ catch {
491
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'native source runtime configuration is invalid');
492
+ }
493
+ return;
494
+ }
495
+ if (config === null || typeof config !== 'object' || Array.isArray(config) || (config.profile !== undefined && config.profile !== 'standard' && config.profile !== 'repository' && config.profile !== 'portable-repository')
19
496
  || (config.versioning !== undefined && config.versioning !== 'patch')
20
497
  || (config.temporaryMiB !== undefined && !Number.isSafeInteger(config.temporaryMiB))
21
- || (config.repositorySandbox !== undefined && (config.profile !== 'repository' || typeof config.repositorySandbox !== 'object' || config.repositorySandbox === null
498
+ || (config.profile === 'portable-repository' && config.repositorySandbox === undefined)
499
+ || (config.repositorySandbox !== undefined && (config.profile !== 'repository' && config.profile !== 'portable-repository' || typeof config.repositorySandbox !== 'object' || config.repositorySandbox === null
22
500
  || Array.isArray(config.repositorySandbox) || Object.getPrototypeOf(config.repositorySandbox) !== Object.prototype
23
501
  || Object.keys(config.repositorySandbox).length !== 1 || Object.keys(config.repositorySandbox)[0] !== 'seccompPath'
24
502
  || typeof config.repositorySandbox.seccompPath !== 'string' || !config.repositorySandbox.seccompPath.startsWith('/') || config.repositorySandbox.seccompPath.includes('\0')))
503
+ || (config.profile === 'portable-repository' ? typeof config.socketPath !== 'string' : config.socketPath !== undefined)
504
+ || (config.profile === 'portable-repository' ? typeof config.daemonId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._-]{7,127}$/u.test(config.daemonId) || !/^sha256:[a-f0-9]{64}$/u.test(config.image) : config.daemonId !== undefined)
505
+ || (config.socketPath !== undefined && (typeof config.socketPath !== 'string' || !config.socketPath.startsWith('/')
506
+ || resolve(config.socketPath) !== config.socketPath || /[\p{Cc},]/u.test(config.socketPath) || config.socketPath.includes('://')))
25
507
  || typeof config.image !== 'string' || config.image.length > 256 || !imageDigest.test(config.image) || typeof config.dockerPath !== 'string' || !config.dockerPath.startsWith('/') || config.dockerPath.includes('\0')
26
508
  || !Number.isSafeInteger(config.timeoutMs) || config.timeoutMs < 60_000
27
509
  || !Number.isSafeInteger(config.memoryMiB) || config.memoryMiB < 128
@@ -32,7 +514,7 @@ export function validateSourceBuildConfig(config) {
32
514
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source build configuration is invalid');
33
515
  }
34
516
  const limits = sourceBuildLimits(config);
35
- const repository = limits.profile === 'repository';
517
+ const repository = limits.profile !== 'standard';
36
518
  if (config.timeoutMs > (repository ? 1_800_000 : 240_000)
37
519
  || config.memoryMiB > (repository ? 16_384 : 4_096)
38
520
  || config.pidsLimit > (repository ? 1_024 : 512)
@@ -46,8 +528,10 @@ async function snapshotRepositorySeccomp(config, temporary) {
46
528
  const sandbox = config.repositorySandbox;
47
529
  if (sandbox === undefined)
48
530
  return undefined;
49
- if (process.platform !== 'linux' || process.arch !== 'x64')
50
- throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'repository seccomp requires linux x64');
531
+ if (!(process.platform === 'linux' && process.arch === 'x64')
532
+ && !(config.profile === 'portable-repository' && process.platform === 'darwin' && ['x64', 'arm64'].includes(process.arch))) {
533
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'repository seccomp requires an approved native Host');
534
+ }
51
535
  const canonical = await realpath(sandbox.seccompPath);
52
536
  if (canonical !== resolve(sandbox.seccompPath))
53
537
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'repository seccomp profile must be canonical');
@@ -84,6 +568,26 @@ async function verifiedDockerPath(path) {
84
568
  }
85
569
  return canonical;
86
570
  }
571
+ async function verifiedPortableSocket(config) {
572
+ if (config.profile !== 'portable-repository')
573
+ return undefined;
574
+ if (process.platform !== 'darwin' || !['x64', 'arm64'].includes(process.arch)) {
575
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'portable repository requires native macOS');
576
+ }
577
+ const expected = join(homedir(), '.docker', 'run', 'docker.sock');
578
+ if (config.socketPath !== expected || await realpath(expected) !== expected) {
579
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'portable Docker socket must be the owner local socket');
580
+ }
581
+ const socket = await lstat(expected), parent = await lstat(dirname(expected));
582
+ if (!socket.isSocket() || socket.uid !== process.getuid?.() || !parent.isDirectory()
583
+ || parent.uid !== process.getuid?.() || (parent.mode & 0o022) !== 0) {
584
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'portable Docker socket is not owner-safe');
585
+ }
586
+ return expected;
587
+ }
588
+ function portableArgs(socket, configDirectory, args) {
589
+ return socket === undefined ? args : ['--config', configDirectory, '-H', `unix://${socket}`, ...args];
590
+ }
87
591
  async function sourceTree(worktree, baseCommit, scope, environment) {
88
592
  const temporary = await mkdtemp(join(tmpdir(), 'dsh-source-build-index-'));
89
593
  try {
@@ -106,19 +610,67 @@ async function sourceTree(worktree, baseCommit, scope, environment) {
106
610
  * value, or Host path crosses the boundary: the sole input is tar on stdin.
107
611
  */
108
612
  export async function runDockerPreparedChecks(input) {
613
+ if (input.capturePack !== undefined && input.capturePack !== true)
614
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source pack capture request is invalid');
109
615
  if (input.sourceJob !== undefined && (!/^source-job-[a-f0-9]{64}$/u.test(input.sourceJob.id)
110
616
  || input.sourceJob.containerName !== `dsh-${input.sourceJob.id}`))
111
617
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'invalid durable container identity');
112
618
  validateSourceBuildConfig(input.config);
113
619
  const limits = sourceBuildLimits(input.config);
114
620
  const dockerPath = await verifiedDockerPath(input.config.dockerPath);
621
+ const socket = await verifiedPortableSocket(input.config);
115
622
  await input.assertCurrent();
116
- if (input.config.repositorySandbox !== undefined) {
623
+ if (input.config.profile === 'repository' && input.config.repositorySandbox !== undefined) {
117
624
  const version = await dockerControl(dockerPath, ['version', '--format', '{{.Server.Version}}/{{.Server.Os}}/{{.Server.Arch}}'], input.signal);
118
625
  if (version.code !== 0 || version.stdout.trim() !== '29.4.1/linux/amd64')
119
626
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'repository seccomp requires approved Docker runtime 29.4.1/linux/amd64');
120
627
  await input.assertCurrent();
121
628
  }
629
+ if (socket !== undefined) {
630
+ const version = await dockerControl(dockerPath, ['version', '--format', '{{.Server.Version}}/{{.Server.Os}}/{{.Server.Arch}}'], input.signal, socket);
631
+ const architecture = process.arch === 'arm64' ? 'arm64' : 'amd64';
632
+ if (version.code !== 0 || version.stdout.trim() !== `29.4.1/linux/${architecture}`) {
633
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'portable repository requires approved matching Linux Docker runtime');
634
+ }
635
+ const daemon = await dockerControl(dockerPath, ['info', '--format', '{{.ID}}'], input.signal, socket);
636
+ if (daemon.code !== 0 || daemon.stdout.trim() !== input.config.daemonId) {
637
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'portable repository Docker daemon identity differs');
638
+ }
639
+ const image = await dockerControl(dockerPath, ['image', 'inspect', '--format', '{{.Id}}/{{.Os}}/{{.Architecture}}', input.config.image], input.signal, socket);
640
+ if (image.code !== 0 || image.stdout.trim() !== `${input.config.image}/linux/${architecture}`) {
641
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'portable repository image ID or platform differs');
642
+ }
643
+ await input.assertCurrent();
644
+ }
645
+ let creationSnapshot;
646
+ let baseLockDigest;
647
+ const verifyCreation = async () => {
648
+ if (input.creation === undefined && input.revision === undefined)
649
+ return;
650
+ if (input.creation !== undefined && input.revision !== undefined)
651
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source build has conflicting preparation authorities');
652
+ if ((input.creation?.grant ?? input.revision.binding.grant).expiresAt <= Date.now())
653
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source creation grant expired before build');
654
+ const verified = input.revision === undefined ? await verifyCreatedPluginWorkspace({ worktree: input.worktree, baseCommit: input.baseCommit,
655
+ name: input.name, environment: input.environment, signal: input.signal, assertCurrent: input.assertCurrent,
656
+ creation: input.creation }) : await verifyRevisedPluginWorkspace({ worktree: input.worktree, baseCommit: input.baseCommit,
657
+ name: input.name, environment: input.environment, signal: input.signal, assertCurrent: input.assertCurrent,
658
+ revision: input.revision.binding, parentSource: input.revision.parentSource, parentCertificate: input.revision.parentCertificate, files: input.revision.files });
659
+ const expected = [...input.scope].sort();
660
+ const actual = [...verified.scope].sort();
661
+ if (expected.length !== actual.length || expected.some((path, index) => path !== actual[index])) {
662
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source creation build scope differs from verified scope');
663
+ }
664
+ await input.assertCurrent();
665
+ if ((input.creation?.grant ?? input.revision.binding.grant).expiresAt <= Date.now())
666
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source creation grant expired before build');
667
+ };
668
+ if (input.creation !== undefined || input.revision !== undefined) {
669
+ await verifyCreation();
670
+ creationSnapshot = await checkedSourceSnapshot(input.worktree, input.baseCommit, input.scope, input.environment, undefined, input.signal);
671
+ const baseLock = await runLocalBuffer('git', ['show', `${input.baseCommit}:pnpm-lock.yaml`], input.worktree, input.environment, { maximumOutput: 8_388_608, signal: input.signal });
672
+ baseLockDigest = createHash('sha256').update(baseLock).digest('hex');
673
+ }
122
674
  const snapshot = await sourceTree(input.worktree, input.baseCommit, input.scope, input.environment);
123
675
  let before;
124
676
  let packageVersion;
@@ -126,6 +678,16 @@ export async function runDockerPreparedChecks(input) {
126
678
  try {
127
679
  seccomp = await snapshotRepositorySeccomp(input.config, snapshot.temporary);
128
680
  before = await checkedSourceSnapshot(input.worktree, input.baseCommit, input.scope, input.environment, snapshot.tree);
681
+ if (creationSnapshot !== undefined) {
682
+ if (before.checkedTreeDigest !== creationSnapshot.checkedTreeDigest || before.checkedPatchDigest !== creationSnapshot.checkedPatchDigest) {
683
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source creation staged tree differs from verified workspace');
684
+ }
685
+ await verifyCreation();
686
+ const current = await checkedSourceSnapshot(input.worktree, input.baseCommit, input.scope, input.environment, undefined, input.signal);
687
+ if (current.checkedTreeDigest !== creationSnapshot.checkedTreeDigest || current.checkedPatchDigest !== creationSnapshot.checkedPatchDigest) {
688
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source creation changed after verification');
689
+ }
690
+ }
129
691
  const packageJson = await runLocalCommand('git', ['show', `${snapshot.tree}:plugins/${input.name}/package.json`], input.worktree, input.environment, { capture: true, maximumOutput: 65_536 });
130
692
  const value = JSON.parse(packageJson)?.version;
131
693
  if (typeof value !== 'string' || !/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(value)) {
@@ -142,7 +704,7 @@ export async function runDockerPreparedChecks(input) {
142
704
  const container = input.sourceJob?.containerName ?? `dsh-source-prepare-${randomUUID()}`;
143
705
  if (input.sourceJob !== undefined) {
144
706
  try {
145
- const existing = await dockerControl(dockerPath, ['container', 'ls', '--all', '--quiet', '--filter', `name=^/${container}$`], input.signal);
707
+ const existing = await dockerControl(dockerPath, ['container', 'ls', '--all', '--quiet', '--filter', `name=^/${container}$`], input.signal, socket);
146
708
  if (existing.code !== 0 || existing.stdout.trim() !== '')
147
709
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'durable source container already exists or cannot be inspected');
148
710
  }
@@ -151,26 +713,35 @@ export async function runDockerPreparedChecks(input) {
151
713
  throw error;
152
714
  }
153
715
  }
716
+ if ((input.creation !== undefined || input.revision !== undefined) && (input.creation?.grant ?? input.revision.binding.grant).expiresAt <= Date.now()) {
717
+ await snapshot.cleanup();
718
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source creation grant expired before build');
719
+ }
154
720
  const script = PREPARED_SOURCE_BUILD_SCRIPT;
155
721
  const args = ['run', '-i', '--pull', 'never', '--name', container, '--label', `dsh.source.tree=${snapshot.tree}`, ...(input.sourceJob === undefined ? [] : ['--label', `dsh.source.job=${input.sourceJob.id}`]), ...(seccomp === undefined ? [] : ['--label', `dsh.source.seccomp.sha256=${seccomp.digest}`]), '--network', 'none', '--read-only', '--cap-drop', 'ALL',
156
722
  '--security-opt', 'no-new-privileges', '--user', '65534:65534', '--pids-limit', String(input.config.pidsLimit),
157
723
  '--memory', `${input.config.memoryMiB}m`, '--memory-swap', `${input.config.memoryMiB}m`, '--cpus', String(input.config.cpus),
158
- '--tmpfs', `/workspace:rw,nosuid,nodev,mode=1777,size=${input.config.workspaceMiB}m${limits.profile === 'repository' ? ',exec' : ''}`, '--tmpfs', `/tmp:rw,nosuid,nodev,mode=1777,size=${limits.temporaryMiB}m${limits.profile === 'repository' ? ',exec' : ''}`,
724
+ '--tmpfs', `/workspace:rw,nosuid,nodev,mode=1777,size=${input.config.workspaceMiB}m${limits.profile !== 'standard' ? ',exec' : ''}`, '--tmpfs', `/tmp:rw,nosuid,nodev,mode=1777,size=${limits.temporaryMiB}m${limits.profile !== 'standard' ? ',exec' : ''}`,
159
725
  '--workdir', '/workspace', '--env', `PLUGIN_ROOT=plugins/${input.name}`, '--env', 'HOME=/tmp', '--env', 'PATH=/usr/local/bin:/usr/bin:/bin',
160
- ...(limits.profile === 'repository' ? ['--env', 'CI=true', '--env', 'VITEST_MAX_WORKERS=1'] : []),
726
+ ...(input.capturePack ? ['--env', 'DSH_SOURCE_CAPTURE_PACK=true'] : []),
727
+ ...(baseLockDigest === undefined ? [] : ['--env', 'DSH_SOURCE_TRUST_LOCKFILE=true', '--env', `DSH_SOURCE_BASE_LOCK_SHA256=${baseLockDigest}`]),
728
+ ...(limits.profile !== 'standard' ? ['--env', 'CI=true', '--env', `VITEST_MAX_WORKERS=${Math.max(1, Math.min(4, Math.floor(input.config.cpus)))}`] : []),
161
729
  // Docker's masked proc submounts prevent procfs mounts in a child user
162
730
  // namespace. This opt-in removes those system-path masks; an empty /sys
163
731
  // compensates for sysfs exposure, but does not restore the /proc masks.
164
732
  ...(seccomp === undefined ? [] : ['--security-opt', `seccomp=${seccomp.path}`,
165
733
  '--security-opt', 'systempaths=unconfined', '--tmpfs', '/sys:ro,nosuid,nodev,noexec,size=1m']),
166
734
  '--entrypoint', '/bin/sh', input.config.image, '-ceu', script];
735
+ const dockerConfig = socket === undefined ? undefined : await mkdtemp(join(tmpdir(), 'dsh-source-docker-config-'));
167
736
  const git = spawn('/usr/bin/git', ['archive', '--format=tar', snapshot.tree], { cwd: input.worktree, env: input.environment, stdio: ['ignore', 'pipe', 'pipe'], shell: false });
168
- const docker = spawn(dockerPath, args, { stdio: ['pipe', 'pipe', 'pipe'], shell: false, env: { PATH: '/usr/bin:/bin' } });
737
+ const docker = spawn(dockerPath, portableArgs(socket, dockerConfig, args), { stdio: ['pipe', 'pipe', 'pipe'], shell: false,
738
+ env: socket === undefined ? { PATH: '/usr/bin:/bin' } : { PATH: '/usr/bin:/bin', HOME: dockerConfig, DOCKER_CONFIG: dockerConfig } });
169
739
  git.stderr.resume();
170
740
  docker.stdin.on('error', () => { });
171
741
  git.stdout.pipe(docker.stdin);
172
742
  const dockerDone = new Promise((resolvePromise, reject) => { docker.once('error', reject); docker.once('close', resolvePromise); });
173
743
  const gitDone = new Promise((resolvePromise, reject) => { git.once('error', reject); git.once('close', code => code === 0 ? resolvePromise() : reject(new ControlPlaneCliError('EXECUTOR_FAILED', `source archive exited ${code}`))); });
744
+ const stdoutLimit = input.config.outputBytes + (input.capturePack ? packFrame.length + maximumPackBase64Bytes + 1 : 0);
174
745
  let stdout = '';
175
746
  let stdoutBytes = 0;
176
747
  let stderr = '';
@@ -181,11 +752,11 @@ export async function runDockerPreparedChecks(input) {
181
752
  stderr = (stderr + chunk.toString('utf8')).slice(-4096);
182
753
  return;
183
754
  }
184
- const remaining = input.config.outputBytes - stdoutBytes;
755
+ const remaining = stdoutLimit - stdoutBytes;
185
756
  if (remaining > 0)
186
757
  stdout += chunk.subarray(0, remaining).toString('utf8');
187
758
  stdoutBytes += chunk.length;
188
- if (stdoutBytes > input.config.outputBytes) {
759
+ if (stdoutBytes > stdoutLimit) {
189
760
  overflow = true;
190
761
  stop();
191
762
  }
@@ -209,10 +780,28 @@ export async function runDockerPreparedChecks(input) {
209
780
  if (code !== 0)
210
781
  throw new ControlPlaneCliError('EXECUTOR_FAILED', `isolated source build failed (${code}): ${stderr.trimEnd()}`);
211
782
  const match = marker.exec(stdout);
212
- if (match !== null && stdout.trim() !== match[0])
213
- throw new ControlPlaneCliError('EXECUTOR_FAILED', 'isolated source build emitted unexpected evidence output');
214
783
  if (match === null)
215
784
  throw new ControlPlaneCliError('EXECUTOR_FAILED', 'isolated source build did not emit its pack evidence marker');
785
+ let packArtifact;
786
+ if (input.capturePack) {
787
+ const prefix = `${match[0]}\n${packFrame}`;
788
+ if (!stdout.startsWith(prefix) || !stdout.endsWith('\n'))
789
+ throw new ControlPlaneCliError('EXECUTOR_FAILED', 'isolated source build emitted an invalid pack capture frame');
790
+ const encoded = stdout.slice(prefix.length, -1);
791
+ const expectedSize = Number(match[2]);
792
+ if (!Number.isSafeInteger(expectedSize) || expectedSize < 1 || expectedSize > maximumPackBytes
793
+ || encoded.length !== Math.ceil(expectedSize / 3) * 4 || !/^[A-Za-z0-9+/]+={0,2}$/u.test(encoded)) {
794
+ throw new ControlPlaneCliError('EXECUTOR_FAILED', 'isolated source build emitted an invalid pack capture frame');
795
+ }
796
+ packArtifact = Buffer.from(encoded, 'base64');
797
+ if (packArtifact.length !== expectedSize || packArtifact.toString('base64') !== encoded
798
+ || createHash('sha256').update(packArtifact).digest('hex') !== match[3]) {
799
+ throw new ControlPlaneCliError('EXECUTOR_FAILED', 'isolated source build pack capture differs from verified evidence');
800
+ }
801
+ }
802
+ else if (stdout.trim() !== match[0]) {
803
+ throw new ControlPlaneCliError('EXECUTOR_FAILED', 'isolated source build emitted unexpected evidence output');
804
+ }
216
805
  const checked = await checkedSourceSnapshot(input.worktree, input.baseCommit, input.scope, input.environment);
217
806
  if (checked.checkedTreeDigest !== before.checkedTreeDigest || checked.checkedPatchDigest !== before.checkedPatchDigest) {
218
807
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'prepared source changed while its isolated build was running');
@@ -220,9 +809,10 @@ export async function runDockerPreparedChecks(input) {
220
809
  const logDigest = createHash('sha256').update(stdout).update('\0').update(stderr).digest('hex');
221
810
  const checkedAt = Date.now();
222
811
  return { treeDigest: checked.checkedTreeDigest, patchDigest: checked.checkedPatchDigest, checkedAt,
812
+ ...(packArtifact === undefined ? {} : { packArtifact }),
223
813
  evidence: Object.freeze({ schemaVersion: 1, kind: 'dsh-source-prepared-evidence',
224
814
  environment: Object.freeze({ npmConfigIgnoreScripts: true, frozenLockfile: true, offline: true, nodeVersion: match[4], pnpmVersion: match[5] }),
225
- commands: Object.freeze([{ command: 'docker', args: Object.freeze([...args]), exitCode: 0, durationMs: Date.now() - started, logDigest }]),
815
+ commands: Object.freeze([{ command: 'docker', args: Object.freeze(portableArgs(socket, dockerConfig, args)), exitCode: 0, durationMs: Date.now() - started, logDigest }]),
226
816
  pack: Object.freeze({ name: match[1], sizeBytes: Number(match[2]), sha256: match[3], version: packageVersion }), preparedAt: input.preparedAt }) };
227
817
  }
228
818
  finally {
@@ -234,12 +824,14 @@ export async function runDockerPreparedChecks(input) {
234
824
  // Failed inspect is ambiguous (daemon loss also exits nonzero). Require
235
825
  // a successful exact-name listing from the daemon to prove absence.
236
826
  if (input.sourceJob === undefined)
237
- await ensureContainerRemoved(dockerPath, container);
827
+ await ensureContainerRemoved(dockerPath, container, socket, input.config.daemonId);
238
828
  else
239
829
  await removeSourceJobContainer(input.config, input.sourceJob);
240
830
  }
241
831
  finally {
242
832
  await snapshot.cleanup();
833
+ if (dockerConfig !== undefined)
834
+ await rm(dockerConfig, { recursive: true, force: true });
243
835
  }
244
836
  }
245
837
  }
@@ -249,61 +841,82 @@ export async function removeSourceJobContainer(config, job) {
249
841
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'invalid durable container identity');
250
842
  validateSourceBuildConfig(config);
251
843
  const path = await verifiedDockerPath(config.dockerPath);
252
- const list = () => dockerControl(path, ['container', 'ls', '--all', '--quiet', '--filter', `name=^/${job.containerName}$`]);
844
+ const socket = await verifiedPortableSocket(config);
845
+ if (socket !== undefined) {
846
+ const daemon = await dockerControl(path, ['info', '--format', '{{.ID}}'], undefined, socket);
847
+ if (daemon.code !== 0 || daemon.stdout.trim() !== config.daemonId) {
848
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'durable container Docker daemon identity differs');
849
+ }
850
+ }
851
+ const list = () => dockerControl(path, ['container', 'ls', '--all', '--quiet', '--filter', `name=^/${job.containerName}$`], undefined, socket);
253
852
  const before = await list();
254
853
  if (before.code !== 0)
255
854
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'durable container absence is unproven');
256
855
  if (before.stdout.trim() !== '') {
257
- const inspected = await dockerControl(path, ['inspect', '--format', '[{{json .Id}},{{json .Name}},{{json .Config.Image}},{{json (index .Config.Labels "dsh.source.job")}}]', job.containerName]);
856
+ const inspected = await dockerControl(path, ['inspect', '--format', '[{{json .Id}},{{json .Name}},{{json .Config.Image}},{{json (index .Config.Labels "dsh.source.job")}}]', job.containerName], undefined, socket);
258
857
  if (inspected.code !== 0)
259
858
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'durable container identity is unproven');
260
859
  const value = JSON.parse(inspected.stdout);
261
860
  if (!Array.isArray(value) || value.length !== 4 || typeof value[0] !== 'string' || !/^[a-f0-9]{64}$/u.test(value[0])
262
861
  || value[1] !== `/${job.containerName}` || value[2] !== config.image || value[3] !== job.id)
263
862
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'durable container ownership mismatch');
264
- await dockerControl(path, ['rm', '-f', value[0]]);
863
+ await dockerControl(path, ['rm', '-f', value[0]], undefined, socket);
265
864
  }
266
865
  const after = await list();
267
866
  if (after.code !== 0 || after.stdout.trim() !== '')
268
867
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'durable container cleanup is unproven');
269
868
  }
270
- async function dockerControl(path, args, signal) {
869
+ async function dockerControl(path, args, signal, socket) {
271
870
  signal?.throwIfAborted();
272
- return new Promise((resolvePromise, reject) => {
273
- const child = spawn(path, args, { stdio: ['ignore', 'pipe', 'ignore'], shell: false, env: { PATH: '/usr/bin:/bin' } });
274
- let stdout = '';
275
- let bytes = 0;
276
- let failed = false;
277
- const abort = () => { child.kill('SIGKILL'); };
278
- signal?.addEventListener('abort', abort, { once: true });
279
- if (signal?.aborted)
280
- abort();
281
- const timer = setTimeout(() => { failed = true; child.kill('SIGKILL'); }, 5_000);
282
- const cleanup = () => { clearTimeout(timer); signal?.removeEventListener('abort', abort); };
283
- child.stdout.on('data', (chunk) => {
284
- bytes += chunk.length;
285
- if (bytes > 4_096) {
286
- failed = true;
287
- child.kill('SIGKILL');
288
- }
289
- else
290
- stdout += chunk.toString('utf8');
291
- });
292
- child.once('error', () => { cleanup(); reject(new ControlPlaneCliError('EXECUTOR_FAILED', 'source build control client could not start')); });
293
- child.once('close', code => {
294
- cleanup();
871
+ const configDirectory = socket === undefined ? undefined : await mkdtemp(join(tmpdir(), 'dsh-source-docker-config-'));
872
+ try {
873
+ return await new Promise((resolvePromise, reject) => {
874
+ const child = spawn(path, portableArgs(socket, configDirectory, args), { stdio: ['ignore', 'pipe', 'ignore'], shell: false,
875
+ env: socket === undefined ? { PATH: '/usr/bin:/bin' } : { PATH: '/usr/bin:/bin', HOME: configDirectory, DOCKER_CONFIG: configDirectory } });
876
+ let stdout = '';
877
+ let bytes = 0;
878
+ let failed = false;
879
+ const abort = () => { child.kill('SIGKILL'); };
880
+ signal?.addEventListener('abort', abort, { once: true });
295
881
  if (signal?.aborted)
296
- reject(new ControlPlaneCliError('EXECUTOR_TIMEOUT', 'source build control query was cancelled'));
297
- else if (failed)
298
- reject(new ControlPlaneCliError('EXECUTOR_FAILED', 'source build control query exceeded its resource bound'));
299
- else
300
- resolvePromise({ code, stdout });
882
+ abort();
883
+ const timer = setTimeout(() => { failed = true; child.kill('SIGKILL'); }, 5_000);
884
+ const cleanup = () => { clearTimeout(timer); signal?.removeEventListener('abort', abort); };
885
+ child.stdout.on('data', (chunk) => {
886
+ bytes += chunk.length;
887
+ if (bytes > 4_096) {
888
+ failed = true;
889
+ child.kill('SIGKILL');
890
+ }
891
+ else
892
+ stdout += chunk.toString('utf8');
893
+ });
894
+ child.once('error', () => { cleanup(); reject(new ControlPlaneCliError('EXECUTOR_FAILED', 'source build control client could not start')); });
895
+ child.once('close', code => {
896
+ cleanup();
897
+ if (signal?.aborted)
898
+ reject(new ControlPlaneCliError('EXECUTOR_TIMEOUT', 'source build control query was cancelled'));
899
+ else if (failed)
900
+ reject(new ControlPlaneCliError('EXECUTOR_FAILED', 'source build control query exceeded its resource bound'));
901
+ else
902
+ resolvePromise({ code, stdout });
903
+ });
301
904
  });
302
- });
905
+ }
906
+ finally {
907
+ if (configDirectory !== undefined)
908
+ await rm(configDirectory, { recursive: true, force: true });
909
+ }
303
910
  }
304
- async function ensureContainerRemoved(path, container) {
305
- await dockerControl(path, ['rm', '-f', container]);
306
- const remaining = await dockerControl(path, ['container', 'ls', '--all', '--quiet', '--filter', `name=^/${container}$`]);
911
+ async function ensureContainerRemoved(path, container, socket, daemonId) {
912
+ if (socket !== undefined) {
913
+ const daemon = await dockerControl(path, ['info', '--format', '{{.ID}}'], undefined, socket);
914
+ if (daemon.code !== 0 || daemon.stdout.trim() !== daemonId) {
915
+ throw new ControlPlaneCliError('EXECUTOR_FAILED', 'isolated source build daemon changed before cleanup');
916
+ }
917
+ }
918
+ await dockerControl(path, ['rm', '-f', container], undefined, socket);
919
+ const remaining = await dockerControl(path, ['container', 'ls', '--all', '--quiet', '--filter', `name=^/${container}$`], undefined, socket);
307
920
  if (remaining.code !== 0 || remaining.stdout.trim() !== '') {
308
921
  throw new ControlPlaneCliError('EXECUTOR_FAILED', 'isolated source build container cleanup could not prove quiescence');
309
922
  }