@kungfu-tech/buildchain 4.0.1-alpha.2 → 4.0.1-alpha.4

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 (78) hide show
  1. package/architecture/decisions/0003-two-phase-delivery-warrant.md +152 -0
  2. package/architecture/internal-capabilities.json +94 -0
  3. package/architecture/maintainability-policy.json +75 -8
  4. package/architecture/v3-core-mechanism-inventory.json +2 -0
  5. package/architecture/v4-delivery-authority-parity.json +250 -0
  6. package/architecture/v4-delivery-warrant-shadow-fixtures.json +41 -9
  7. package/bin/buildchain.mjs +9 -1
  8. package/contracts/dev-delivery-authority-v2.schema.json +662 -0
  9. package/dist/site/agent-index.json +1 -0
  10. package/dist/site/artifact-schemas.json +2 -0
  11. package/dist/site/buildchain-contract.json +91 -8
  12. package/dist/site/buildchain-site.json +169 -18
  13. package/dist/site/capability-registry.json +2 -2
  14. package/dist/site/cli-registry.json +46 -2
  15. package/dist/site/kfd-claims.json +43 -9
  16. package/dist/site/kfd-upstream-aggregate.json +1 -1
  17. package/dist/site/manual-registry.json +3 -3
  18. package/dist/site/node-api-registry.json +1694 -165
  19. package/dist/site/page-registry.json +162 -11
  20. package/dist/site/public-surface-audit.json +367 -7
  21. package/dist/site/publication-registry.json +4 -4
  22. package/dist/site/release-provenance.json +2 -0
  23. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +662 -0
  24. package/dist/site/site-manifest.json +7 -7
  25. package/dist/site/workflow-registry.json +22 -6
  26. package/docs/MAP.md +1 -0
  27. package/docs/cli-reference.md +211 -13
  28. package/docs/dev-delivery-qualification-landing-adr.md +251 -0
  29. package/docs/dev-delivery-warrant.md +330 -38
  30. package/docs/node-api-reference.md +113 -53
  31. package/package.json +5 -1
  32. package/packages/core/buildchain-contract.js +3 -2
  33. package/packages/core/dev-delivery-authority-candidate.js +270 -0
  34. package/packages/core/dev-delivery-authority-evidence.js +146 -0
  35. package/packages/core/dev-delivery-authority-landing.js +461 -0
  36. package/packages/core/dev-delivery-authority-observation.js +48 -0
  37. package/packages/core/dev-delivery-authority-qualification.js +591 -0
  38. package/packages/core/dev-delivery-authority-settlement.js +213 -0
  39. package/packages/core/dev-delivery-authority-state.js +583 -0
  40. package/packages/core/dev-delivery-candidate-identity.js +13 -0
  41. package/packages/core/dev-delivery-contract-surface.js +76 -0
  42. package/packages/core/dev-delivery-execution-failure.js +133 -0
  43. package/packages/core/dev-delivery-execution-transfer.js +572 -0
  44. package/packages/core/dev-delivery-landing-admission-core.js +119 -0
  45. package/packages/core/dev-delivery-landing-readback.js +598 -0
  46. package/packages/core/dev-delivery-landing-terminal-evidence.js +271 -0
  47. package/packages/core/dev-delivery-landing-testing-port.js +6 -0
  48. package/packages/core/dev-delivery-native-execution.js +110 -0
  49. package/packages/core/dev-delivery-native-proof.js +546 -0
  50. package/packages/core/dev-delivery-process-boundary.js +550 -0
  51. package/packages/core/dev-delivery-provider-attempt.js +127 -0
  52. package/packages/core/dev-delivery-provider-heartbeat.js +370 -0
  53. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  54. package/packages/core/dev-delivery-warrant-qualification.js +145 -0
  55. package/packages/core/dev-delivery-warrant-settlement.js +237 -36
  56. package/packages/core/dev-delivery-warrant-state.js +564 -0
  57. package/packages/core/dev-delivery-warrant.js +321 -368
  58. package/scripts/buildchain-cli-help.mjs +11 -2
  59. package/scripts/check-v4-floating-consumer-policy-contract.mjs +1 -1
  60. package/scripts/dev-delivery-authority-command-adapters.mjs +206 -0
  61. package/scripts/dev-delivery-authority-provider.mjs +28 -0
  62. package/scripts/dev-delivery-authority.mjs +490 -0
  63. package/scripts/dev-delivery-native-run.mjs +177 -0
  64. package/scripts/dev-delivery-process-boundary.mjs +260 -0
  65. package/scripts/dev-delivery-proof.mjs +67 -2
  66. package/scripts/dev-delivery-provider-heartbeat.mjs +215 -0
  67. package/scripts/dev-delivery-two-phase-resume.mjs +345 -0
  68. package/scripts/dev-delivery-two-phase.mjs +573 -0
  69. package/scripts/dev-delivery-warrant-options.mjs +266 -0
  70. package/scripts/dev-delivery-warrant-store.mjs +227 -0
  71. package/scripts/dev-delivery-warrant.mjs +227 -193
  72. package/scripts/dev-pr-auto-merge.mjs +1 -1
  73. package/scripts/dev-pr-delivery-warrant.mjs +10 -0
  74. package/scripts/generate-site-bundle.mjs +26 -4
  75. package/scripts/npm-publish-transaction.mjs +2 -2
  76. package/scripts/release-train-self-dogfood.mjs +94 -92
  77. package/scripts/site-capability-metadata.mjs +12 -0
  78. package/templates/native-dev-delivery.yml +143 -0
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ createDevDeliveryAuthorityState,
6
+ observeDevDeliveryAuthorityState,
7
+ } from "../packages/core/dev-delivery-authority-landing.js";
8
+ import { defaultDevDeliveryStateRef } from "./dev-delivery-warrant.mjs";
9
+ import { GitHubDevDeliveryStore } from "./dev-delivery-warrant.mjs";
10
+ import { flag, hasFlag } from "./dev-delivery-warrant-options.mjs";
11
+ import { runDevDeliveryAuthorityCommandAdapter } from "./dev-delivery-authority-command-adapters.mjs";
12
+ function text(value = "") {
13
+ return String(value ?? "").trim();
14
+ }
15
+ function positiveInteger(value, label, fallback = 0) {
16
+ const parsed = Number(value || fallback);
17
+ if (!Number.isInteger(parsed) || parsed < 1) {
18
+ throw new Error(`${label} must be a positive integer`);
19
+ }
20
+ return parsed;
21
+ }
22
+ function nonNegativeInteger(value, label, fallback = 0) {
23
+ const parsed = Number(value === undefined || value === "" ? fallback : value);
24
+ if (!Number.isInteger(parsed) || parsed < 0)
25
+ throw new Error(`${label} must be a non-negative integer`);
26
+ return parsed;
27
+ }
28
+ function bool(value, fallback = false) {
29
+ if (value === undefined || value === null || value === "") return fallback;
30
+ return ["1", "true", "yes", "on"].includes(text(value).toLowerCase());
31
+ }
32
+ function repository(value) {
33
+ const normalized = text(value);
34
+ if (!/^[^/\s]+\/[^/\s]+$/.test(normalized)) {
35
+ throw new Error("repository must be owner/repo");
36
+ }
37
+ return normalized;
38
+ }
39
+ function branch(value) {
40
+ const normalized = text(value).replace(/^refs\/heads\//, "");
41
+ if (!/^dev\/v\d+\/v\d+\.\d+$/.test(normalized)) {
42
+ throw new Error("branch must be dev/vN/vN.M");
43
+ }
44
+ return normalized;
45
+ }
46
+ export function defaultDevDeliveryAuthorityStateRef(value) {
47
+ return defaultDevDeliveryStateRef(branch(value));
48
+ }
49
+ function stateRef(value, protectedBranch) {
50
+ const expected = defaultDevDeliveryAuthorityStateRef(protectedBranch);
51
+ const normalized = text(value || expected).replace(/^refs\/heads\//, "");
52
+ if (normalized !== expected) {
53
+ throw new Error(
54
+ `state ref must be the canonical authority ref ${expected}`,
55
+ );
56
+ }
57
+ return normalized;
58
+ }
59
+ function authorityPolicy(options) {
60
+ return {
61
+ maxQualificationLeases: positiveInteger(
62
+ options.maxQualificationLeases,
63
+ "maxQualificationLeases",
64
+ 2,
65
+ ),
66
+ qualificationLeaseSeconds: positiveInteger(
67
+ options.qualificationLeaseSeconds,
68
+ "qualificationLeaseSeconds",
69
+ 3600,
70
+ ),
71
+ landingLeaseSeconds: positiveInteger(
72
+ options.landingLeaseSeconds,
73
+ "landingLeaseSeconds",
74
+ 900,
75
+ ),
76
+ maxLandingOvertakes: nonNegativeInteger(
77
+ options.maxLandingOvertakes,
78
+ "maxLandingOvertakes",
79
+ 2,
80
+ ),
81
+ maxQualificationAttempts: positiveInteger(
82
+ options.maxQualificationAttempts,
83
+ "maxQualificationAttempts",
84
+ 3,
85
+ ),
86
+ };
87
+ }
88
+ function normalizedAuthorityOptions(optionsInput) {
89
+ const options = {
90
+ ...optionsInput,
91
+ repository: repository(optionsInput.repository),
92
+ branch: branch(optionsInput.branch),
93
+ now: new Date(optionsInput.now || Date.now()).toISOString(),
94
+ execute: bool(optionsInput.execute),
95
+ };
96
+ options.stateRef = stateRef(optionsInput.stateRef, options.branch);
97
+ options.authorityPolicy = authorityPolicy(options);
98
+ return options;
99
+ }
100
+
101
+ function createAuthorityStore(options, clientInput) {
102
+ return (
103
+ clientInput ||
104
+ new GitHubDevDeliveryStore({
105
+ repository: options.repository,
106
+ token: options.token || process.env.GITHUB_TOKEN,
107
+ apiUrl:
108
+ options.apiUrl ||
109
+ process.env.GITHUB_API_URL ||
110
+ "https://api.github.com",
111
+ createInitialState: ({
112
+ repository: repositoryInput,
113
+ protectedBase,
114
+ now,
115
+ }) =>
116
+ createDevDeliveryAuthorityState({
117
+ repository: repositoryInput,
118
+ protectedBase,
119
+ policy: options.authorityPolicy,
120
+ now,
121
+ }),
122
+ })
123
+ );
124
+ }
125
+
126
+ function assertAuthorityTransitionScope(changed, options) {
127
+ if (
128
+ changed.state.repository !== options.repository ||
129
+ changed.state.protectedBase !== options.branch
130
+ ) {
131
+ throw new Error("authority transition repository or protected base drift");
132
+ }
133
+ }
134
+
135
+ async function writeAuthorityTransition(store, loaded, changed, options) {
136
+ const mutates = changed.state.stateRoot !== loaded.queue.stateRoot;
137
+ if (!options.execute || !mutates) return null;
138
+ if (options.command === "migrate") {
139
+ const currentSource = await store.read({
140
+ stateRef: options.migrationSource.stateRef,
141
+ protectedBase: options.branch,
142
+ now: options.now,
143
+ });
144
+ if (
145
+ !currentSource.exists ||
146
+ currentSource.commitSha !== options.migrationSource.commitSha ||
147
+ currentSource.queue.stateRoot !== options.migrationSource.queue.stateRoot
148
+ ) {
149
+ throw new Error("live v1 authority changed during migration");
150
+ }
151
+ }
152
+ if (
153
+ options.command !== "migrate" &&
154
+ changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot
155
+ ) {
156
+ throw new Error(
157
+ "transition expected-old root does not match loaded authority",
158
+ );
159
+ }
160
+ return store.write({
161
+ stateRef: options.stateRef,
162
+ queue: changed.state,
163
+ expectedCommitSha: loaded.commitSha,
164
+ expectedStateRoot: loaded.queue.stateRoot,
165
+ receiptRoot: changed.receiptRoot,
166
+ });
167
+ }
168
+
169
+ function authorityCommandResult({ loaded, changed, write, options }) {
170
+ return {
171
+ schema: "kungfu.buildchain.dev-delivery-authority-command-result/v1",
172
+ ok: true,
173
+ mode: options.execute ? "execute" : "plan",
174
+ command: options.command,
175
+ stateRef: options.stateRef,
176
+ before: { commitSha: loaded.commitSha, stateRoot: loaded.queue.stateRoot },
177
+ after: {
178
+ commitSha: write?.commitSha || loaded.commitSha,
179
+ stateRoot: changed.state.stateRoot,
180
+ },
181
+ mutationAuthorized: options.execute,
182
+ mutationApplied: Boolean(write),
183
+ receipt: changed.receipt,
184
+ receiptRoot: changed.receiptRoot,
185
+ lease: changed.lease || null,
186
+ warrant: changed.warrant || changed.state.landingWarrant || null,
187
+ observation: observeDevDeliveryAuthorityState(changed.state, {
188
+ now: options.now,
189
+ }),
190
+ migrationSource: options.migrationSource
191
+ ? {
192
+ stateRef: options.migrationSource.stateRef,
193
+ commitSha: options.migrationSource.commitSha,
194
+ stateRoot: options.migrationSource.queue.stateRoot,
195
+ }
196
+ : null,
197
+ };
198
+ }
199
+
200
+ export async function runDevDeliveryAuthorityCommand(
201
+ optionsInput = {},
202
+ clientInput,
203
+ ) {
204
+ const options = normalizedAuthorityOptions(optionsInput);
205
+ const store = createAuthorityStore(options, clientInput);
206
+ const loaded = await store.read({
207
+ stateRef: options.stateRef,
208
+ protectedBase: options.branch,
209
+ now: options.now,
210
+ });
211
+ if (options.command === "migrate") {
212
+ if (!loaded.exists) {
213
+ throw new Error(
214
+ `live v1 authority state ref ${options.stateRef} is missing`,
215
+ );
216
+ }
217
+ options.migrationSource = {
218
+ ...loaded,
219
+ stateRef: options.stateRef,
220
+ };
221
+ }
222
+ const operation = await runDevDeliveryAuthorityCommandAdapter(
223
+ loaded,
224
+ options,
225
+ );
226
+ if (operation.result) return operation.result;
227
+ assertAuthorityTransitionScope(operation.changed, options);
228
+ const write = await writeAuthorityTransition(
229
+ store,
230
+ loaded,
231
+ operation.changed,
232
+ options,
233
+ );
234
+ return authorityCommandResult({
235
+ loaded,
236
+ changed: operation.changed,
237
+ write,
238
+ options,
239
+ });
240
+ }
241
+ export function devDeliveryAuthorityCliOptions(
242
+ args = [],
243
+ environment = process.env,
244
+ ) {
245
+ const [command = "", ...rest] = args;
246
+ return {
247
+ command,
248
+ repository: flag(rest, "repository", environment.GITHUB_REPOSITORY),
249
+ branch: flag(
250
+ rest,
251
+ "branch",
252
+ environment.BUILDCHAIN_DEV_DELIVERY_BRANCH || environment.GITHUB_BASE_REF,
253
+ ),
254
+ stateRef: flag(
255
+ rest,
256
+ "state-ref",
257
+ environment.BUILDCHAIN_DEV_AUTHORITY_STATE_REF,
258
+ ),
259
+ expectedOldStateRoot: flag(
260
+ rest,
261
+ "expected-old",
262
+ environment.BUILDCHAIN_DEV_AUTHORITY_EXPECTED_OLD,
263
+ ),
264
+ pullRequestNumber: flag(
265
+ rest,
266
+ "pull-request",
267
+ environment.BUILDCHAIN_DEV_DELIVERY_PR_NUMBER,
268
+ ),
269
+ candidateId: flag(
270
+ rest,
271
+ "candidate-id",
272
+ environment.BUILDCHAIN_DEV_DELIVERY_CANDIDATE_ID,
273
+ ),
274
+ sourceHead: flag(
275
+ rest,
276
+ "source-head",
277
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_HEAD,
278
+ ),
279
+ assignmentRoot: flag(
280
+ rest,
281
+ "assignment-root",
282
+ environment.BUILDCHAIN_DEV_DELIVERY_ASSIGNMENT_ROOT,
283
+ ),
284
+ initiativeRoot: flag(
285
+ rest,
286
+ "initiative-root",
287
+ environment.BUILDCHAIN_DEV_DELIVERY_INITIATIVE_ROOT,
288
+ ),
289
+ sourceIdentityRoot: flag(
290
+ rest,
291
+ "source-identity-root",
292
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_IDENTITY_ROOT,
293
+ ),
294
+ sourcePatchRoot: flag(
295
+ rest,
296
+ "source-patch-root",
297
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PATCH_ROOT,
298
+ ),
299
+ sourceProofRoot: flag(
300
+ rest,
301
+ "source-proof-root",
302
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PROOF_ROOT,
303
+ ),
304
+ planRoot: flag(
305
+ rest,
306
+ "plan-root",
307
+ environment.BUILDCHAIN_DEV_DELIVERY_PLAN_ROOT,
308
+ ),
309
+ closureRoot: flag(
310
+ rest,
311
+ "closure-root",
312
+ environment.BUILDCHAIN_DEV_DELIVERY_CLOSURE_ROOT,
313
+ ),
314
+ dependencyRoot: flag(
315
+ rest,
316
+ "dependency-root",
317
+ environment.BUILDCHAIN_DEV_DELIVERY_DEPENDENCY_ROOT,
318
+ ),
319
+ toolchainRoot: flag(
320
+ rest,
321
+ "toolchain-root",
322
+ environment.BUILDCHAIN_DEV_DELIVERY_TOOLCHAIN_ROOT,
323
+ ),
324
+ environmentRoot: flag(
325
+ rest,
326
+ "environment-root",
327
+ environment.BUILDCHAIN_DEV_DELIVERY_ENVIRONMENT_ROOT,
328
+ ),
329
+ affectedPaths: flag(
330
+ rest,
331
+ "affected-paths-json",
332
+ environment.BUILDCHAIN_DEV_DELIVERY_AFFECTED_PATHS || "[]",
333
+ ),
334
+ shardEvidenceRoots: flag(
335
+ rest,
336
+ "shard-evidence-roots-json",
337
+ environment.BUILDCHAIN_DEV_DELIVERY_SHARD_EVIDENCE_ROOTS || "[]",
338
+ ),
339
+ nativeCommand: flag(
340
+ rest,
341
+ "native-command",
342
+ environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_COMMAND,
343
+ ),
344
+ nativeCommandRoot: flag(
345
+ rest,
346
+ "native-command-root",
347
+ environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_COMMAND_ROOT,
348
+ ),
349
+ sourceProofPath: flag(
350
+ rest,
351
+ "source-proof",
352
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PROOF,
353
+ ),
354
+ nativeProofPath: flag(
355
+ rest,
356
+ "native-proof",
357
+ environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_PROOF,
358
+ ),
359
+ qualificationContractPath: flag(
360
+ rest,
361
+ "qualification-contract",
362
+ environment.BUILDCHAIN_DEV_QUALIFICATION_CONTRACT,
363
+ ),
364
+ providerAttemptPath: flag(
365
+ rest,
366
+ "provider-attempt",
367
+ environment.BUILDCHAIN_DEV_LANDING_PROVIDER_ATTEMPT,
368
+ ),
369
+ deliveryClass: flag(
370
+ rest,
371
+ "delivery-class",
372
+ environment.BUILDCHAIN_DEV_DELIVERY_CLASS,
373
+ ),
374
+ qualificationDomains: flag(
375
+ rest,
376
+ "qualification-domains",
377
+ environment.BUILDCHAIN_DEV_QUALIFICATION_DOMAINS || "[]",
378
+ ),
379
+ authorityToken: flag(
380
+ rest,
381
+ "authority-token",
382
+ environment.BUILDCHAIN_DEV_AUTHORITY_TOKEN,
383
+ ),
384
+ authorityGeneration: flag(
385
+ rest,
386
+ "authority-generation",
387
+ environment.BUILDCHAIN_DEV_AUTHORITY_GENERATION,
388
+ ),
389
+ providerRunId: environment.GITHUB_RUN_ID,
390
+ providerRunAttempt: environment.GITHUB_RUN_ATTEMPT,
391
+ transferRoot: flag(rest, "transfer-root"),
392
+ finalizerBoundaryRoot: flag(rest, "finalizer-boundary-root"),
393
+ nativeJobId: flag(rest, "native-job-id"),
394
+ sealJobId: flag(rest, "seal-job-id"),
395
+ evidenceRoot: flag(
396
+ rest,
397
+ "evidence-root",
398
+ environment.BUILDCHAIN_DEV_DELIVERY_EVIDENCE_ROOT,
399
+ ),
400
+ mergeGroupHead: flag(
401
+ rest,
402
+ "merge-group-head",
403
+ environment.BUILDCHAIN_DEV_MERGE_GROUP_HEAD,
404
+ ),
405
+ outcome: flag(rest, "outcome", environment.BUILDCHAIN_DEV_DELIVERY_OUTCOME),
406
+ reason: flag(rest, "reason", environment.BUILDCHAIN_DEV_DELIVERY_REASON),
407
+ leaseSeconds: flag(
408
+ rest,
409
+ "lease-seconds",
410
+ environment.BUILDCHAIN_DEV_AUTHORITY_LEASE_SECONDS,
411
+ ),
412
+ maxQualificationLeases: flag(
413
+ rest,
414
+ "max-qualification-leases",
415
+ environment.BUILDCHAIN_DEV_MAX_QUALIFICATION_LEASES || "2",
416
+ ),
417
+ qualificationLeaseSeconds: flag(
418
+ rest,
419
+ "qualification-lease-seconds",
420
+ environment.BUILDCHAIN_DEV_QUALIFICATION_LEASE_SECONDS || "3600",
421
+ ),
422
+ landingLeaseSeconds: flag(
423
+ rest,
424
+ "landing-lease-seconds",
425
+ environment.BUILDCHAIN_DEV_LANDING_LEASE_SECONDS || "900",
426
+ ),
427
+ maxLandingOvertakes: flag(
428
+ rest,
429
+ "max-landing-overtakes",
430
+ environment.BUILDCHAIN_DEV_MAX_LANDING_OVERTAKES || "2",
431
+ ),
432
+ maxQualificationAttempts: flag(
433
+ rest,
434
+ "max-qualification-attempts",
435
+ environment.BUILDCHAIN_DEV_MAX_QUALIFICATION_ATTEMPTS || "3",
436
+ ),
437
+ now: flag(rest, "now", environment.BUILDCHAIN_DEV_DELIVERY_NOW),
438
+ outputPath: flag(
439
+ rest,
440
+ "output",
441
+ environment.BUILDCHAIN_DEV_DELIVERY_OUTPUT ||
442
+ ".buildchain/dev-delivery/authority-result.json",
443
+ ),
444
+ execute: hasFlag(rest, "execute"),
445
+ json: hasFlag(rest, "json"),
446
+ };
447
+ }
448
+ function usage() {
449
+ return "Usage: buildchain dev authority <migrate|submit|lease-qualification|heartbeat-qualification|complete-qualification|lease-landing|heartbeat-landing|recover|admit-merge-group|settle|observe> --repository <owner/repo> --branch <dev/vN/vN.M> [--environment-root <root>] [--qualification-domains <json>] [--provider-attempt <admitted-attempt.json>] [--execute] [--output <file>] [--json]\n";
450
+ }
451
+ async function main() {
452
+ const args = process.argv.slice(2);
453
+ if (args.length === 0 || hasFlag(args, "help")) {
454
+ process.stdout.write(usage());
455
+ return;
456
+ }
457
+ const options = devDeliveryAuthorityCliOptions(args);
458
+ if (
459
+ ![
460
+ "submit",
461
+ "migrate",
462
+ "lease-qualification",
463
+ "heartbeat-qualification",
464
+ "complete-qualification",
465
+ "lease-landing",
466
+ "heartbeat-landing",
467
+ "recover",
468
+ "admit-merge-group",
469
+ "settle",
470
+ "observe",
471
+ ].includes(options.command)
472
+ ) {
473
+ throw new Error(usage().trim());
474
+ }
475
+ const result = await runDevDeliveryAuthorityCommand(options);
476
+ fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
477
+ fs.writeFileSync(options.outputPath, `${JSON.stringify(result, null, 2)}\n`);
478
+ if (options.json)
479
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
480
+ else
481
+ process.stdout.write(
482
+ `Buildchain dev authority ${options.command}: ${result.mode}\nResult: ${options.outputPath}\n`,
483
+ );
484
+ }
485
+ if (import.meta.url === `file://${process.argv[1]}`) {
486
+ main().catch((error) => {
487
+ console.error(`buildchain dev authority: ${error.message}`);
488
+ process.exit(1);
489
+ });
490
+ }
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import {
4
+ createNativeCommandContract,
5
+ createNativeExecutionBinding,
6
+ createNativeExecutionReceipt,
7
+ } from "../packages/core/dev-delivery-warrant.js";
8
+ import { assertCredentiallessProcessAncestry } from "../packages/core/dev-delivery-process-boundary.js";
9
+
10
+ const NATIVE_CHILD_ENVIRONMENT_ALLOWLIST = new Set([
11
+ "CI",
12
+ "COLORTERM",
13
+ "FORCE_COLOR",
14
+ "HOME",
15
+ "LANG",
16
+ "LC_ALL",
17
+ "LC_CTYPE",
18
+ "LOGNAME",
19
+ "NO_COLOR",
20
+ "PATH",
21
+ "SHELL",
22
+ "SYSTEMROOT",
23
+ "TEMP",
24
+ "TERM",
25
+ "TMP",
26
+ "TMPDIR",
27
+ "USER",
28
+ ]);
29
+
30
+ export function createNativeChildEnvironment(controllerEnvironment = {}) {
31
+ return Object.fromEntries(
32
+ Object.entries(controllerEnvironment).filter(
33
+ ([name, value]) =>
34
+ NATIVE_CHILD_ENVIRONMENT_ALLOWLIST.has(name.toUpperCase()) &&
35
+ typeof value === "string",
36
+ ),
37
+ );
38
+ }
39
+
40
+ export async function runNativeWithHeartbeat({
41
+ command,
42
+ cwd = process.cwd(),
43
+ heartbeat,
44
+ executionBinding = {},
45
+ intervalMs = 30_000,
46
+ terminationGraceMs = 10_000,
47
+ terminationKillMs = 5_000,
48
+ spawnImpl = spawn,
49
+ controllerEnvironment = process.env,
50
+ ancestryCheck = assertCredentiallessProcessAncestry,
51
+ now = () => new Date().toISOString(),
52
+ } = {}) {
53
+ if (!String(command || "").trim()) {
54
+ throw new Error("native command is required");
55
+ }
56
+ if (typeof heartbeat !== "function") {
57
+ throw new Error("heartbeat callback is required");
58
+ }
59
+ const commandContract = createNativeCommandContract(command);
60
+ if (
61
+ executionBinding.nativeCommandRoot &&
62
+ executionBinding.nativeCommandRoot !== commandContract.commandRoot
63
+ ) {
64
+ throw new Error(
65
+ "native command does not match the authorized command root",
66
+ );
67
+ }
68
+ const boundExecution = createNativeExecutionBinding({
69
+ ...executionBinding,
70
+ nativeCommandRoot: commandContract.commandRoot,
71
+ });
72
+ ancestryCheck();
73
+ const startedAt = now();
74
+ try {
75
+ await heartbeat();
76
+ } catch (error) {
77
+ throw new Error(`native heartbeat failed: ${error.message}`);
78
+ }
79
+ let heartbeatCount = 1;
80
+ let heartbeatError = null;
81
+ let heartbeatRunning = false;
82
+ let childExited = false;
83
+ let forceKillTimer = null;
84
+ let terminationTimer = null;
85
+ let rejectTermination;
86
+ const terminationFailure = new Promise((_, reject) => {
87
+ rejectTermination = reject;
88
+ });
89
+ const child = spawnImpl("bash", ["-lc", command], {
90
+ cwd,
91
+ env: createNativeChildEnvironment(controllerEnvironment),
92
+ stdio: "inherit",
93
+ detached: process.platform !== "win32",
94
+ });
95
+ const signalChild = (signal) => {
96
+ if (childExited) return;
97
+ if (process.platform !== "win32" && Number.isInteger(child.pid)) {
98
+ try {
99
+ process.kill(-child.pid, signal);
100
+ return;
101
+ } catch {
102
+ // Fall through to the direct child handle for mocked or already-exiting processes.
103
+ }
104
+ }
105
+ child.kill(signal);
106
+ };
107
+ const terminateForFenceLoss = () => {
108
+ signalChild("SIGTERM");
109
+ forceKillTimer = setTimeout(() => {
110
+ signalChild("SIGKILL");
111
+ terminationTimer = setTimeout(() => {
112
+ if (!childExited) {
113
+ const error = new Error(
114
+ "native worker did not stop after fencing loss",
115
+ );
116
+ error.code = "native-worker-termination-unproven";
117
+ error.workerTerminationProven = false;
118
+ rejectTermination(error);
119
+ }
120
+ }, terminationKillMs);
121
+ terminationTimer.unref?.();
122
+ }, terminationGraceMs);
123
+ forceKillTimer.unref?.();
124
+ };
125
+ const beat = async () => {
126
+ if (heartbeatRunning || heartbeatError) return;
127
+ heartbeatRunning = true;
128
+ try {
129
+ await heartbeat();
130
+ heartbeatCount += 1;
131
+ } catch (error) {
132
+ heartbeatError = error;
133
+ terminateForFenceLoss();
134
+ } finally {
135
+ heartbeatRunning = false;
136
+ }
137
+ };
138
+ const timer = setInterval(beat, intervalMs);
139
+ const exitPromise = new Promise((resolve, reject) => {
140
+ child.once("error", reject);
141
+ child.once("exit", (code, signal) => {
142
+ childExited = true;
143
+ resolve({ code, signal });
144
+ });
145
+ });
146
+ const exit = await Promise.race([exitPromise, terminationFailure]).finally(
147
+ () => {
148
+ clearInterval(timer);
149
+ if (forceKillTimer) clearTimeout(forceKillTimer);
150
+ if (terminationTimer) clearTimeout(terminationTimer);
151
+ },
152
+ );
153
+ while (heartbeatRunning) {
154
+ await new Promise((resolve) => setTimeout(resolve, 10));
155
+ }
156
+ if (heartbeatError) {
157
+ throw new Error(`native heartbeat failed: ${heartbeatError.message}`);
158
+ }
159
+ if (exit.code !== 0) {
160
+ throw new Error(
161
+ `native command failed with ${exit.signal || `exit ${exit.code}`}`,
162
+ );
163
+ }
164
+ await beat();
165
+ if (heartbeatError) {
166
+ throw new Error(`native heartbeat failed: ${heartbeatError.message}`);
167
+ }
168
+ const completedAt = now();
169
+ return createNativeExecutionReceipt({
170
+ outcome: "succeeded",
171
+ commandRoot: commandContract.commandRoot,
172
+ executionBinding: boundExecution,
173
+ startedAt,
174
+ completedAt,
175
+ heartbeatCount,
176
+ });
177
+ }