@agent-delivery-harness/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,705 @@
1
+ /**
2
+ * Neutral provider-rail adapter at the CLI command boundary.
3
+ *
4
+ * The envelope and state machine are the vendored `delivery-provider-rails/1`
5
+ * contract. Its `payload`, `details`, and `result` objects remain opaque. This
6
+ * adapter assigns one adopter-owned meaning inside `terminal.result`:
7
+ * `manifestPath` names a delivery-evidence manifest that must be accepted by
8
+ * the existing recorder before a successful provider attempt can become green
9
+ * evidence. Live obligations reuse the evaluator's existing LiveProviderResult
10
+ * shape; recorded obligations reuse SubmissionOutcome and its evidence records.
11
+ */
12
+ import {
13
+ BlockedError,
14
+ canonicalize,
15
+ createBlocker,
16
+ type Blocker,
17
+ type LiveProviderResult,
18
+ type SubmissionOutcome,
19
+ type SubmissionRecord,
20
+ } from "@agent-delivery-harness/kernel";
21
+ import { spawn } from "node:child_process";
22
+ import { createInterface } from "node:readline";
23
+
24
+ export const DELIVERY_PROVIDER_RAILS_VERSION = "delivery-provider-rails/1" as const;
25
+
26
+ type JsonObject = Readonly<Record<string, unknown>>;
27
+ type TerminalOutcome = "success" | "blocked" | "failed" | "cancelled" | "indeterminate";
28
+ type EventKind = "progress" | "evidence" | "blocker" | "terminal";
29
+
30
+ export interface ProviderRailNegotiate {
31
+ readonly kind: "negotiate";
32
+ readonly supportedVersions: readonly string[];
33
+ }
34
+
35
+ export interface ProviderRailNegotiation {
36
+ readonly kind: "negotiation";
37
+ readonly outcome: "supported" | "unsupported";
38
+ readonly selectedVersion: typeof DELIVERY_PROVIDER_RAILS_VERSION | null;
39
+ readonly supportedVersions: readonly [typeof DELIVERY_PROVIDER_RAILS_VERSION];
40
+ }
41
+
42
+ export interface ProviderRailRequest {
43
+ readonly kind: "request";
44
+ readonly version: typeof DELIVERY_PROVIDER_RAILS_VERSION;
45
+ readonly requestId: string;
46
+ readonly idempotencyKey: string;
47
+ readonly payload: JsonObject;
48
+ }
49
+
50
+ interface ProviderRailEventBase {
51
+ readonly version: typeof DELIVERY_PROVIDER_RAILS_VERSION;
52
+ readonly requestId: string;
53
+ readonly sequence: number;
54
+ readonly summary: string;
55
+ }
56
+
57
+ export interface ProviderRailProgress extends ProviderRailEventBase {
58
+ readonly kind: "progress";
59
+ readonly details?: JsonObject;
60
+ }
61
+
62
+ export interface ProviderRailEvidence extends ProviderRailEventBase {
63
+ readonly kind: "evidence";
64
+ readonly evidenceId: string;
65
+ readonly details?: JsonObject;
66
+ }
67
+
68
+ export interface ProviderRailBlocker extends ProviderRailEventBase {
69
+ readonly kind: "blocker";
70
+ readonly blockerId: string;
71
+ readonly action?: string;
72
+ readonly details?: JsonObject;
73
+ }
74
+
75
+ export interface ProviderRailTerminal extends ProviderRailEventBase {
76
+ readonly kind: "terminal";
77
+ readonly outcome: TerminalOutcome;
78
+ readonly action?: string;
79
+ readonly details?: JsonObject;
80
+ readonly result?: JsonObject;
81
+ }
82
+
83
+ export interface ProviderRailCancel {
84
+ readonly kind: "cancel";
85
+ readonly version: typeof DELIVERY_PROVIDER_RAILS_VERSION;
86
+ readonly requestId: string;
87
+ readonly cancellationId: string;
88
+ readonly reason?: string;
89
+ }
90
+
91
+ export type ProviderRailEvent = ProviderRailProgress | ProviderRailEvidence | ProviderRailBlocker | ProviderRailTerminal;
92
+ export type ProviderRailMessage = ProviderRailNegotiate | ProviderRailNegotiation | ProviderRailRequest | ProviderRailEvent | ProviderRailCancel;
93
+
94
+ export interface ProviderRailConsumption {
95
+ readonly status: "supported" | "unsupported" | "malformed" | "success" | "blocked" | "failed" | "cancelled" | "indeterminate";
96
+ readonly acceptedCount: number;
97
+ readonly duplicateCount: number;
98
+ readonly rejectedCount: number;
99
+ readonly events: readonly ProviderRailEvent[];
100
+ readonly terminal: ProviderRailTerminal | null;
101
+ }
102
+
103
+ export interface ConsumeProviderRailOptions {
104
+ readonly requestId?: string;
105
+ readonly cancellationAccepted?: boolean;
106
+ readonly interrupted?: boolean;
107
+ readonly afterInterruption?: readonly unknown[];
108
+ }
109
+
110
+ function isObject(value: unknown): value is Record<string, unknown> {
111
+ return value !== null && typeof value === "object" && !Array.isArray(value);
112
+ }
113
+
114
+ function exactMembers(value: Record<string, unknown>, required: readonly string[], optional: readonly string[] = []): boolean {
115
+ const allowed = new Set([...required, ...optional]);
116
+ return required.every((member) => Object.hasOwn(value, member)) && Object.keys(value).every((member) => allowed.has(member));
117
+ }
118
+
119
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
120
+
121
+ function identifier(value: unknown): value is string {
122
+ return typeof value === "string" && value.length >= 1 && value.length <= 64 && IDENTIFIER.test(value);
123
+ }
124
+
125
+ function text(value: unknown): value is string {
126
+ return typeof value === "string" && value.length >= 1 && value.length <= 1024;
127
+ }
128
+
129
+ function opaque(value: unknown): value is JsonObject {
130
+ return isObject(value);
131
+ }
132
+
133
+ function sequence(value: unknown): value is number {
134
+ return typeof value === "number" && Number.isInteger(value) && value >= 1;
135
+ }
136
+
137
+ function validNegotiation(value: unknown): value is ProviderRailNegotiation {
138
+ if (!isObject(value) || !exactMembers(value, ["kind", "outcome", "selectedVersion", "supportedVersions"])) return false;
139
+ if (value["kind"] !== "negotiation") return false;
140
+ if (!Array.isArray(value["supportedVersions"]) || value["supportedVersions"].length !== 1 || value["supportedVersions"][0] !== DELIVERY_PROVIDER_RAILS_VERSION) return false;
141
+ if (value["outcome"] === "supported") return value["selectedVersion"] === DELIVERY_PROVIDER_RAILS_VERSION;
142
+ if (value["outcome"] === "unsupported") return value["selectedVersion"] === null;
143
+ return false;
144
+ }
145
+
146
+ function eventBase(value: Record<string, unknown>): boolean {
147
+ return (
148
+ value["version"] === DELIVERY_PROVIDER_RAILS_VERSION &&
149
+ identifier(value["requestId"]) &&
150
+ sequence(value["sequence"]) &&
151
+ text(value["summary"])
152
+ );
153
+ }
154
+
155
+ function validEvent(value: unknown): value is ProviderRailEvent {
156
+ if (!isObject(value) || typeof value["kind"] !== "string") return false;
157
+ switch (value["kind"]) {
158
+ case "progress":
159
+ return exactMembers(value, ["kind", "requestId", "sequence", "summary", "version"], ["details"]) && eventBase(value) && (value["details"] === undefined || opaque(value["details"]));
160
+ case "evidence":
161
+ return exactMembers(value, ["evidenceId", "kind", "requestId", "sequence", "summary", "version"], ["details"]) && eventBase(value) && identifier(value["evidenceId"]) && (value["details"] === undefined || opaque(value["details"]));
162
+ case "blocker":
163
+ return exactMembers(value, ["blockerId", "kind", "requestId", "sequence", "summary", "version"], ["action", "details"]) && eventBase(value) && identifier(value["blockerId"]) && (value["action"] === undefined || text(value["action"])) && (value["details"] === undefined || opaque(value["details"]));
164
+ case "terminal":
165
+ return (
166
+ exactMembers(value, ["kind", "outcome", "requestId", "sequence", "summary", "version"], ["action", "details", "result"]) &&
167
+ eventBase(value) &&
168
+ ["success", "blocked", "failed", "cancelled", "indeterminate"].includes(String(value["outcome"])) &&
169
+ (value["action"] === undefined || text(value["action"])) &&
170
+ (value["details"] === undefined || opaque(value["details"])) &&
171
+ (value["result"] === undefined || opaque(value["result"]))
172
+ );
173
+ default:
174
+ return false;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Contract consumer used by both the process adapter and the shared vectors.
180
+ * Terminal finality is checked before message shape, exactly as the contract
181
+ * requires: late malformed or cross-attempt bytes cannot reopen an outcome.
182
+ */
183
+ export function consumeProviderRailMessages(
184
+ messages: readonly unknown[],
185
+ options: ConsumeProviderRailOptions = {},
186
+ ): ProviderRailConsumption {
187
+ let negotiated = false;
188
+ let status: ProviderRailConsumption["status"] = "malformed";
189
+ let terminalClosed = false;
190
+ let failedClosed = false;
191
+ let activeRequestId = options.requestId;
192
+ let acceptedCount = 0;
193
+ let duplicateCount = 0;
194
+ let rejectedCount = 0;
195
+ let terminal: ProviderRailTerminal | null = null;
196
+ const events: ProviderRailEvent[] = [];
197
+ const seenByRequest = new Map<string, Map<number, string>>();
198
+
199
+ const accept = (message: unknown): void => {
200
+ if (terminalClosed || failedClosed) {
201
+ rejectedCount += 1;
202
+ return;
203
+ }
204
+ if (validNegotiation(message)) {
205
+ negotiated = false;
206
+ if (seenByRequest.size > 0) {
207
+ status = "malformed";
208
+ failedClosed = true;
209
+ return;
210
+ }
211
+ if (message.outcome === "supported") {
212
+ negotiated = true;
213
+ status = "supported";
214
+ } else {
215
+ status = "unsupported";
216
+ failedClosed = true;
217
+ }
218
+ return;
219
+ }
220
+ if (!negotiated || !validEvent(message)) {
221
+ status = "malformed";
222
+ failedClosed = true;
223
+ return;
224
+ }
225
+ if (activeRequestId === undefined) activeRequestId = message.requestId;
226
+ else if (message.requestId !== activeRequestId) {
227
+ status = "malformed";
228
+ failedClosed = true;
229
+ return;
230
+ }
231
+ const encoded = canonicalize(message);
232
+ const seen = seenByRequest.get(message.requestId) ?? new Map<number, string>();
233
+ seenByRequest.set(message.requestId, seen);
234
+ const prior = seen.get(message.sequence);
235
+ if (prior !== undefined) {
236
+ if (prior === encoded) duplicateCount += 1;
237
+ else {
238
+ status = "malformed";
239
+ failedClosed = true;
240
+ }
241
+ return;
242
+ }
243
+ const expected = Math.max(0, ...seen.keys()) + 1;
244
+ if (message.sequence !== expected) {
245
+ status = "malformed";
246
+ failedClosed = true;
247
+ return;
248
+ }
249
+ if (
250
+ options.cancellationAccepted === true &&
251
+ message.kind === "terminal" &&
252
+ message.outcome !== "cancelled" &&
253
+ message.outcome !== "indeterminate"
254
+ ) {
255
+ status = "malformed";
256
+ rejectedCount += 1;
257
+ failedClosed = true;
258
+ return;
259
+ }
260
+ seen.set(message.sequence, encoded);
261
+ acceptedCount += 1;
262
+ events.push(message);
263
+ if (message.kind === "terminal") {
264
+ status = message.outcome;
265
+ terminal = message;
266
+ terminalClosed = true;
267
+ }
268
+ };
269
+
270
+ for (const message of messages) accept(message);
271
+ if (options.interrupted === true && negotiated && !terminalClosed && !failedClosed) {
272
+ status = "indeterminate";
273
+ terminalClosed = true;
274
+ }
275
+ for (const message of options.afterInterruption ?? []) accept(message);
276
+
277
+ return { status, acceptedCount, duplicateCount, rejectedCount, events, terminal };
278
+ }
279
+
280
+ export interface ProviderRailSession {
281
+ send(message: ProviderRailMessage): Promise<void>;
282
+ /** `null` means the provider process or transport closed. */
283
+ receive(): Promise<unknown | null>;
284
+ close(options?: { readonly terminationGraceMs?: number }): Promise<void>;
285
+ }
286
+
287
+ export interface OpenProviderRailProcessInput {
288
+ readonly command: readonly [string, ...string[]];
289
+ readonly cwd: string;
290
+ readonly env: NodeJS.ProcessEnv;
291
+ }
292
+
293
+ const DEFAULT_PROVIDER_RAIL_DEADLINE_MS = 10 * 60_000;
294
+ const DEFAULT_TERMINATION_GRACE_MS = 1_000;
295
+
296
+ class RailLifecycleEnded extends Error {
297
+ readonly causeKind: "deadline" | "abort";
298
+
299
+ constructor(causeKind: "deadline" | "abort") {
300
+ super(causeKind === "deadline" ? "Provider lifecycle deadline expired." : "Provider lifecycle was aborted.");
301
+ this.name = "RailLifecycleEnded";
302
+ this.causeKind = causeKind;
303
+ }
304
+ }
305
+
306
+ /** One total lifecycle budget shared by negotiation, writes, and event waits. */
307
+ class RailDeadline {
308
+ readonly signal: AbortSignal;
309
+ readonly #controller = new AbortController();
310
+ readonly #timer: ReturnType<typeof setTimeout>;
311
+ readonly #parent: AbortSignal | undefined;
312
+ readonly #onParentAbort: () => void;
313
+
314
+ constructor(timeoutMs: number, parent?: AbortSignal) {
315
+ this.signal = this.#controller.signal;
316
+ this.#parent = parent;
317
+ this.#onParentAbort = () => this.#controller.abort(new RailLifecycleEnded("abort"));
318
+ parent?.addEventListener("abort", this.#onParentAbort, { once: true });
319
+ if (parent?.aborted === true) this.#onParentAbort();
320
+ this.#timer = setTimeout(
321
+ () => this.#controller.abort(new RailLifecycleEnded("deadline")),
322
+ Math.max(1, Math.floor(timeoutMs)),
323
+ );
324
+ }
325
+
326
+ async wait<T>(operation: () => Promise<T>): Promise<T> {
327
+ if (this.signal.aborted) throw this.reason();
328
+ const pending = operation();
329
+ let onAbort: (() => void) | undefined;
330
+ const aborted = new Promise<never>((_resolve, reject) => {
331
+ onAbort = () => reject(this.reason());
332
+ this.signal.addEventListener("abort", onAbort, { once: true });
333
+ });
334
+ try {
335
+ return await Promise.race([pending, aborted]);
336
+ } finally {
337
+ if (onAbort !== undefined) this.signal.removeEventListener("abort", onAbort);
338
+ }
339
+ }
340
+
341
+ dispose(): void {
342
+ clearTimeout(this.#timer);
343
+ this.#parent?.removeEventListener("abort", this.#onParentAbort);
344
+ }
345
+
346
+ private reason(): RailLifecycleEnded {
347
+ return this.signal.reason instanceof RailLifecycleEnded ? this.signal.reason : new RailLifecycleEnded("abort");
348
+ }
349
+ }
350
+
351
+ async function settlesWithin(operation: Promise<unknown>, milliseconds: number): Promise<boolean> {
352
+ let timer: ReturnType<typeof setTimeout> | undefined;
353
+ const elapsed = new Promise<false>((resolve) => {
354
+ timer = setTimeout(() => resolve(false), Math.max(1, Math.floor(milliseconds)));
355
+ });
356
+ try {
357
+ return await Promise.race([operation.then(() => true, () => true), elapsed]);
358
+ } finally {
359
+ if (timer !== undefined) clearTimeout(timer);
360
+ }
361
+ }
362
+
363
+ /** Opens the contract over newline-delimited JSON on a provider subprocess's stdio. */
364
+ export function openProviderRailProcess(input: OpenProviderRailProcessInput): Promise<ProviderRailSession> {
365
+ const [executable, ...args] = input.command;
366
+ const child = spawn(executable, args, {
367
+ cwd: input.cwd,
368
+ env: input.env,
369
+ stdio: ["pipe", "pipe", "pipe"],
370
+ shell: false,
371
+ });
372
+ // A deadline may destroy the child while a bounded stdin write is pending.
373
+ // The write callback still reports that failure; this listener prevents the
374
+ // stream's parallel `error` event from becoming an uncaught exception.
375
+ child.stdin.on("error", () => {});
376
+ const lines = createInterface({ input: child.stdout });
377
+ // Diagnostics stay provider-owned; drain the pipe so a noisy provider cannot
378
+ // deadlock while the typed stdout rail is waiting for its terminal event.
379
+ child.stderr.resume();
380
+ const queued: unknown[] = [];
381
+ const waiters: Array<(value: unknown | null) => void> = [];
382
+ let closed = false;
383
+ let resolveClosed: (() => void) | undefined;
384
+ const childClosed = new Promise<void>((resolve) => {
385
+ resolveClosed = resolve;
386
+ });
387
+ let closing: Promise<void> | undefined;
388
+
389
+ const deliver = (value: unknown | null): void => {
390
+ const waiter = waiters.shift();
391
+ if (waiter !== undefined) waiter(value);
392
+ else if (value !== null) queued.push(value);
393
+ };
394
+
395
+ lines.on("line", (line) => {
396
+ try {
397
+ const parsed = JSON.parse(line) as unknown;
398
+ // `null` is the session's transport-close sentinel. Preserve a provider
399
+ // that actually emits JSON null as malformed contract input instead.
400
+ deliver(parsed === null ? line : parsed);
401
+ } catch {
402
+ // Keep malformed bytes as a value the closed-envelope validator rejects.
403
+ deliver(line);
404
+ }
405
+ });
406
+ const close = (): void => {
407
+ if (closed) return;
408
+ closed = true;
409
+ while (waiters.length > 0) waiters.shift()?.(null);
410
+ resolveClosed?.();
411
+ };
412
+ child.once("close", close);
413
+ child.once("error", close);
414
+
415
+ return Promise.resolve({
416
+ async send(message) {
417
+ if (closed || child.stdin.destroyed) throw new Error("Provider transport is closed.");
418
+ await new Promise<void>((resolve, reject) => {
419
+ child.stdin.write(`${JSON.stringify(message)}\n`, (error) => (error === null || error === undefined ? resolve() : reject(error)));
420
+ });
421
+ },
422
+ receive() {
423
+ const next = queued.shift();
424
+ if (next !== undefined) return Promise.resolve(next);
425
+ if (closed) return Promise.resolve(null);
426
+ return new Promise<unknown | null>((resolve) => waiters.push(resolve));
427
+ },
428
+ close(options = {}) {
429
+ closing ??= (async () => {
430
+ lines.close();
431
+ if (!child.stdin.destroyed) child.stdin.end();
432
+ if (closed || child.exitCode !== null || child.signalCode !== null) {
433
+ await childClosed;
434
+ return;
435
+ }
436
+ child.kill("SIGTERM");
437
+ const grace = options.terminationGraceMs ?? DEFAULT_TERMINATION_GRACE_MS;
438
+ if (!(await settlesWithin(childClosed, grace)) && child.exitCode === null && child.signalCode === null) {
439
+ child.kill("SIGKILL");
440
+ }
441
+ await childClosed;
442
+ })();
443
+ return closing;
444
+ },
445
+ });
446
+ }
447
+
448
+ export interface ProviderRailAttemptInput {
449
+ readonly providerId: string;
450
+ readonly requestId: string;
451
+ readonly idempotencyKey: string;
452
+ readonly payload: JsonObject;
453
+ readonly requiresEvidence: boolean;
454
+ }
455
+
456
+ export interface ProviderRailAttemptOptions {
457
+ readonly open: () => Promise<ProviderRailSession>;
458
+ readonly publishManifest?: (manifestPath: string) => Promise<SubmissionOutcome>;
459
+ readonly signal?: AbortSignal;
460
+ readonly cancellationId?: string;
461
+ readonly deadlineMs?: number;
462
+ readonly terminationGraceMs?: number;
463
+ }
464
+
465
+ export type ProviderRailInvocationResult =
466
+ | {
467
+ readonly kind: "success";
468
+ readonly status: "success";
469
+ readonly liveResult: LiveProviderResult;
470
+ readonly events: readonly ProviderRailEvent[];
471
+ readonly records: readonly SubmissionRecord[];
472
+ }
473
+ | {
474
+ readonly kind: "interrupted";
475
+ readonly status: "cancelled" | "indeterminate" | "malformed";
476
+ readonly runId: string;
477
+ readonly blockers: readonly Blocker[];
478
+ }
479
+ | {
480
+ readonly kind: "blocked";
481
+ readonly status: Exclude<ProviderRailConsumption["status"], "supported" | "success" | "cancelled">;
482
+ readonly runId: string;
483
+ readonly blockers: readonly Blocker[];
484
+ };
485
+
486
+ const RETRY_PROVIDER = {
487
+ id: "retry-provider",
488
+ kind: "retry" as const,
489
+ summary: "Start a new provider attempt after checking the provider process and retained diagnostics.",
490
+ };
491
+
492
+ function detailsOf(value: unknown): string | undefined {
493
+ if (value === undefined) return undefined;
494
+ if (value instanceof Error) return value.message;
495
+ try {
496
+ return canonicalize(value);
497
+ } catch {
498
+ return String(value);
499
+ }
500
+ }
501
+
502
+ function railBlocker(providerId: string, status: string, summary: string, details?: unknown, action?: string): Blocker {
503
+ return createBlocker({
504
+ code: `provider_rail_${status}`,
505
+ source: { kind: "provider", id: providerId },
506
+ summary,
507
+ ...(details === undefined ? {} : { details: detailsOf(details) }),
508
+ remediations: [
509
+ action === undefined
510
+ ? RETRY_PROVIDER
511
+ : { id: "follow-provider-action", kind: "manual_action", summary: action },
512
+ ],
513
+ });
514
+ }
515
+
516
+ function outcomeBlockers(providerId: string, consumption: ProviderRailConsumption): readonly Blocker[] {
517
+ const reported = consumption.events.filter((event): event is ProviderRailBlocker => event.kind === "blocker");
518
+ if (reported.length > 0) {
519
+ return reported.map((event) =>
520
+ railBlocker(
521
+ providerId,
522
+ "blocked",
523
+ event.summary,
524
+ { blockerId: event.blockerId, ...(event.details === undefined ? {} : { details: event.details }) },
525
+ event.action,
526
+ ),
527
+ );
528
+ }
529
+ const terminal = consumption.terminal;
530
+ return [
531
+ railBlocker(
532
+ providerId,
533
+ consumption.status,
534
+ terminal?.summary ?? `Provider ${providerId} ended ${consumption.status}.`,
535
+ terminal?.details,
536
+ terminal?.action,
537
+ ),
538
+ ];
539
+ }
540
+
541
+ function manifestPathOf(terminal: ProviderRailTerminal): string | undefined {
542
+ const value = terminal.result?.["manifestPath"];
543
+ return typeof value === "string" && value.length > 0 ? value : undefined;
544
+ }
545
+
546
+ /**
547
+ * Runs one negotiated provider attempt. A successful terminal is provisional:
548
+ * when recorded evidence is required, the adapter publishes the returned
549
+ * manifest through the existing recorder first and exposes green only after
550
+ * that atomic publication reports acceptance.
551
+ */
552
+ export async function invokeProviderRail(
553
+ input: ProviderRailAttemptInput,
554
+ options: ProviderRailAttemptOptions,
555
+ ): Promise<ProviderRailInvocationResult> {
556
+ const deadline = new RailDeadline(options.deadlineMs ?? DEFAULT_PROVIDER_RAIL_DEADLINE_MS, options.signal);
557
+ const terminationGraceMs = options.terminationGraceMs ?? DEFAULT_TERMINATION_GRACE_MS;
558
+ let session: ProviderRailSession | undefined;
559
+ let requestStarted = false;
560
+ try {
561
+ session = await deadline.wait(options.open);
562
+ const activeSession = session;
563
+ await deadline.wait(() => activeSession.send({ kind: "negotiate", supportedVersions: [DELIVERY_PROVIDER_RAILS_VERSION] }));
564
+ const negotiation = await deadline.wait(() => activeSession.receive());
565
+ if (negotiation === null) {
566
+ return {
567
+ kind: "blocked",
568
+ status: "indeterminate",
569
+ runId: input.requestId,
570
+ blockers: [railBlocker(input.providerId, "indeterminate", "The provider transport closed before version negotiation completed.")],
571
+ };
572
+ }
573
+ const received: unknown[] = [negotiation];
574
+ const negotiated = consumeProviderRailMessages(received, { requestId: input.requestId });
575
+ if (negotiated.status === "unsupported") {
576
+ return { kind: "blocked", status: "unsupported", runId: input.requestId, blockers: outcomeBlockers(input.providerId, negotiated) };
577
+ }
578
+ if (negotiated.status !== "supported") {
579
+ return { kind: "blocked", status: "malformed", runId: input.requestId, blockers: outcomeBlockers(input.providerId, negotiated) };
580
+ }
581
+
582
+ requestStarted = true;
583
+ await deadline.wait(() =>
584
+ activeSession.send({
585
+ kind: "request",
586
+ version: DELIVERY_PROVIDER_RAILS_VERSION,
587
+ requestId: input.requestId,
588
+ idempotencyKey: input.idempotencyKey,
589
+ payload: input.payload,
590
+ }),
591
+ );
592
+
593
+ for (;;) {
594
+ const message = await deadline.wait(() => activeSession.receive());
595
+ if (message === null) {
596
+ const interrupted = consumeProviderRailMessages(received, {
597
+ requestId: input.requestId,
598
+ interrupted: true,
599
+ });
600
+ return { kind: "blocked", status: "indeterminate", runId: input.requestId, blockers: outcomeBlockers(input.providerId, interrupted) };
601
+ }
602
+ received.push(message);
603
+ const consumption = consumeProviderRailMessages(received, {
604
+ requestId: input.requestId,
605
+ });
606
+ if (consumption.status === "supported") continue;
607
+ if (consumption.status === "malformed") {
608
+ return { kind: "blocked", status: "malformed", runId: input.requestId, blockers: outcomeBlockers(input.providerId, consumption) };
609
+ }
610
+ if (consumption.terminal === null) continue;
611
+ // Terminal is absorbing. Once it is accepted, a later local signal must
612
+ // not emit cancellation or replace the provider's completed outcome.
613
+ deadline.dispose();
614
+ if (consumption.status === "cancelled") {
615
+ return { kind: "interrupted", status: "cancelled", runId: input.requestId, blockers: outcomeBlockers(input.providerId, consumption) };
616
+ }
617
+ if (consumption.status !== "success") {
618
+ return { kind: "blocked", status: consumption.status, runId: input.requestId, blockers: outcomeBlockers(input.providerId, consumption) };
619
+ }
620
+
621
+ let records: readonly SubmissionRecord[] = [];
622
+ if (input.requiresEvidence) {
623
+ const manifestPath = manifestPathOf(consumption.terminal);
624
+ if (manifestPath === undefined || options.publishManifest === undefined) {
625
+ return {
626
+ kind: "blocked",
627
+ status: "malformed",
628
+ runId: input.requestId,
629
+ blockers: [railBlocker(input.providerId, "malformed", "Provider success did not identify a manifest for retained evidence publication.")],
630
+ };
631
+ }
632
+ let publication: SubmissionOutcome;
633
+ try {
634
+ publication = await options.publishManifest(manifestPath);
635
+ } catch (error) {
636
+ return {
637
+ kind: "blocked",
638
+ status: "failed",
639
+ runId: input.requestId,
640
+ blockers:
641
+ error instanceof BlockedError
642
+ ? error.blockers
643
+ : [railBlocker(input.providerId, "failed", "Provider evidence publication did not complete.", error)],
644
+ };
645
+ }
646
+ if (publication.status !== "accepted") {
647
+ return {
648
+ kind: "blocked",
649
+ status: "failed",
650
+ runId: input.requestId,
651
+ blockers:
652
+ publication.blockers.length > 0
653
+ ? publication.blockers
654
+ : [railBlocker(input.providerId, "failed", "Provider evidence publication was not accepted.")],
655
+ };
656
+ }
657
+ records = publication.records;
658
+ }
659
+
660
+ return {
661
+ kind: "success",
662
+ status: "success",
663
+ liveResult: { providerId: input.providerId, runId: input.requestId, status: "green", findings: [] },
664
+ events: consumption.events,
665
+ records,
666
+ };
667
+ }
668
+ } catch (error) {
669
+ if (session !== undefined && requestStarted && error instanceof RailLifecycleEnded) {
670
+ await settlesWithin(
671
+ Promise.resolve().then(() =>
672
+ session?.send({
673
+ kind: "cancel",
674
+ version: DELIVERY_PROVIDER_RAILS_VERSION,
675
+ requestId: input.requestId,
676
+ cancellationId: options.cancellationId ?? `cancel-${input.requestId}`,
677
+ reason: error.causeKind === "deadline" ? "Consumer deadline expired" : "Consumer interrupted the provider attempt",
678
+ }),
679
+ ),
680
+ terminationGraceMs,
681
+ );
682
+ }
683
+ const summary =
684
+ error instanceof RailLifecycleEnded
685
+ ? error.causeKind === "deadline"
686
+ ? "The provider lifecycle deadline expired without a trustworthy terminal outcome."
687
+ : "The provider invocation was interrupted without a trustworthy terminal outcome."
688
+ : session === undefined
689
+ ? "The provider process could not be started."
690
+ : "The provider transport closed without a trustworthy terminal outcome.";
691
+ const blockers = [railBlocker(input.providerId, "indeterminate", summary, error)];
692
+ return error instanceof RailLifecycleEnded && error.causeKind === "abort"
693
+ ? { kind: "interrupted", status: "indeterminate", runId: input.requestId, blockers }
694
+ : { kind: "blocked", status: "indeterminate", runId: input.requestId, blockers };
695
+ } finally {
696
+ deadline.dispose();
697
+ // Transport teardown after an accepted terminal cannot rewrite that
698
+ // terminal. The process/session is still closed on every path.
699
+ try {
700
+ await session?.close({ terminationGraceMs });
701
+ } catch {
702
+ // Best effort only; pre-terminal transport failures are mapped above.
703
+ }
704
+ }
705
+ }