deepline 0.1.181 → 0.1.183
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.
- package/dist/bundling-sources/apps/play-runner-workers/src/child-play-await.ts +203 -6
- package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +8 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +633 -107
- package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +3 -2
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +1932 -1838
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +116 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +542 -78
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/output-datasets.ts +124 -12
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +292 -11
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/run-work-dispatcher.ts +519 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +2381 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +18 -5
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/work-budget.ts +130 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/worker-platform-budget.ts +39 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +2 -0
- package/dist/bundling-sources/sdk/src/client.ts +48 -5
- package/dist/bundling-sources/sdk/src/http.ts +23 -8
- package/dist/bundling-sources/sdk/src/play.ts +200 -26
- package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +56 -3
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/sdk/src/types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +132 -10
- package/dist/bundling-sources/shared_libs/play-runtime/auth-scope-resolver.ts +92 -0
- package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -4
- package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +101 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1857 -314
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +106 -6
- package/dist/bundling-sources/shared_libs/play-runtime/dedup-backend.ts +0 -0
- package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durability-store.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +116 -16
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +186 -20
- package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-ledger-store.ts +133 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +245 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +5 -5
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +14 -0
- package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +52 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +41 -0
- package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/query-result-dataset.ts +120 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +124 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/row-isolation.ts +48 -2
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +158 -15
- package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +45 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +2037 -262
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +42 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +183 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-errors.ts +88 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +8 -1
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +19 -1
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +343 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +43 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +139 -17
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +437 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +83 -6
- package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +24 -17
- package/dist/cli/index.js +85 -23
- package/dist/cli/index.mjs +85 -23
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +274 -35
- package/dist/index.mjs +274 -35
- package/dist/plays/bundle-play-file.mjs +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
export type WorkReceiptStateStatus =
|
|
2
|
+
| 'pending'
|
|
3
|
+
| 'running'
|
|
4
|
+
| 'completed'
|
|
5
|
+
| 'failed'
|
|
6
|
+
| 'skipped';
|
|
7
|
+
|
|
8
|
+
export type WorkReceiptStateInput = {
|
|
9
|
+
status: WorkReceiptStateStatus;
|
|
10
|
+
runId?: string | null;
|
|
11
|
+
failureKind?: 'terminal' | 'repairable' | null;
|
|
12
|
+
leaseId?: string | null;
|
|
13
|
+
leaseOwnerRunId?: string | null;
|
|
14
|
+
leaseOwnerAttempt?: number | null;
|
|
15
|
+
leaseExpiresAt?: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type WorkReceiptLeaseState =
|
|
19
|
+
| { kind: 'none' }
|
|
20
|
+
| {
|
|
21
|
+
kind: 'active';
|
|
22
|
+
leaseId: string;
|
|
23
|
+
ownerRunId: string;
|
|
24
|
+
ownerAttempt: number;
|
|
25
|
+
expiresAt: string;
|
|
26
|
+
expiresAtMs: number;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
kind: 'expired';
|
|
30
|
+
leaseId: string | null;
|
|
31
|
+
ownerRunId: string;
|
|
32
|
+
ownerAttempt: number;
|
|
33
|
+
expiresAt: string;
|
|
34
|
+
expiresAtMs: number;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
kind: 'invalid';
|
|
38
|
+
reason:
|
|
39
|
+
| 'missing_owner'
|
|
40
|
+
| 'missing_expiry'
|
|
41
|
+
| 'missing_lease_id'
|
|
42
|
+
| 'invalid_expiry';
|
|
43
|
+
leaseId: string | null;
|
|
44
|
+
ownerRunId: string | null;
|
|
45
|
+
expiresAt: string | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type WorkReceiptInvalidLeaseReason = Extract<
|
|
49
|
+
WorkReceiptLeaseState,
|
|
50
|
+
{ kind: 'invalid' }
|
|
51
|
+
>['reason'];
|
|
52
|
+
|
|
53
|
+
export type WorkReceiptState =
|
|
54
|
+
| {
|
|
55
|
+
kind: 'pending';
|
|
56
|
+
terminal: false;
|
|
57
|
+
reusable: false;
|
|
58
|
+
lease: WorkReceiptLeaseState;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
kind: 'running';
|
|
62
|
+
terminal: false;
|
|
63
|
+
reusable: false;
|
|
64
|
+
lease: WorkReceiptLeaseState;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
kind: 'completed';
|
|
68
|
+
terminal: true;
|
|
69
|
+
reusable: true;
|
|
70
|
+
lease: WorkReceiptLeaseState;
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
kind: 'skipped';
|
|
74
|
+
terminal: true;
|
|
75
|
+
reusable: true;
|
|
76
|
+
lease: WorkReceiptLeaseState;
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
kind: 'failed';
|
|
80
|
+
terminal: true;
|
|
81
|
+
reusable: false;
|
|
82
|
+
lease: WorkReceiptLeaseState;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type WorkReceiptClaimDecision =
|
|
86
|
+
| { kind: 'claim' }
|
|
87
|
+
| { kind: 'reuse_terminal' }
|
|
88
|
+
| {
|
|
89
|
+
kind: 'blocked_active_lease';
|
|
90
|
+
status: WorkReceiptStateStatus;
|
|
91
|
+
ownerRunId: string | null;
|
|
92
|
+
leaseId: string | null;
|
|
93
|
+
}
|
|
94
|
+
| {
|
|
95
|
+
kind: 'blocked_running';
|
|
96
|
+
ownerRunId: string | null;
|
|
97
|
+
leaseId: string | null;
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
kind: 'blocked_invalid_lease';
|
|
101
|
+
reason: WorkReceiptInvalidLeaseReason;
|
|
102
|
+
status: WorkReceiptStateStatus;
|
|
103
|
+
}
|
|
104
|
+
| { kind: 'blocked_failed' };
|
|
105
|
+
|
|
106
|
+
export type WorkReceiptOwnerOperationDecision =
|
|
107
|
+
| { kind: 'allow' }
|
|
108
|
+
| {
|
|
109
|
+
kind: 'reject';
|
|
110
|
+
reason:
|
|
111
|
+
| 'missing_receipt'
|
|
112
|
+
| 'not_running'
|
|
113
|
+
| 'owner_mismatch'
|
|
114
|
+
| 'attempt_mismatch'
|
|
115
|
+
| 'lease_mismatch'
|
|
116
|
+
| 'missing_request_lease';
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type WorkReceiptOwnerOperationInput = {
|
|
120
|
+
receipt: WorkReceiptStateInput | null;
|
|
121
|
+
ownerRunId?: string | null;
|
|
122
|
+
ownerRunAttempt?: number | null;
|
|
123
|
+
leaseId?: string | null;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function normalize(value: string | null | undefined): string | null {
|
|
127
|
+
const trimmed = value?.trim();
|
|
128
|
+
return trimmed ? trimmed : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeAttempt(value: number | null | undefined): number {
|
|
132
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
133
|
+
? Math.max(0, Math.floor(value))
|
|
134
|
+
: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function classifyWorkReceiptLeaseState(
|
|
138
|
+
receipt: WorkReceiptStateInput,
|
|
139
|
+
nowMs: number,
|
|
140
|
+
): WorkReceiptLeaseState {
|
|
141
|
+
const ownerRunId =
|
|
142
|
+
normalize(receipt.leaseOwnerRunId) ?? normalize(receipt.runId);
|
|
143
|
+
const leaseId = normalize(receipt.leaseId);
|
|
144
|
+
const expiresAt = normalize(receipt.leaseExpiresAt);
|
|
145
|
+
if (!leaseId && !expiresAt) {
|
|
146
|
+
return { kind: 'none' };
|
|
147
|
+
}
|
|
148
|
+
if (!ownerRunId) {
|
|
149
|
+
return {
|
|
150
|
+
kind: 'invalid',
|
|
151
|
+
reason: 'missing_owner',
|
|
152
|
+
leaseId,
|
|
153
|
+
ownerRunId,
|
|
154
|
+
expiresAt,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (!expiresAt) {
|
|
158
|
+
return {
|
|
159
|
+
kind: 'invalid',
|
|
160
|
+
reason: 'missing_expiry',
|
|
161
|
+
leaseId,
|
|
162
|
+
ownerRunId,
|
|
163
|
+
expiresAt,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
167
|
+
if (!Number.isFinite(expiresAtMs)) {
|
|
168
|
+
return {
|
|
169
|
+
kind: 'invalid',
|
|
170
|
+
reason: 'invalid_expiry',
|
|
171
|
+
leaseId,
|
|
172
|
+
ownerRunId,
|
|
173
|
+
expiresAt,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (expiresAtMs <= nowMs) {
|
|
177
|
+
return {
|
|
178
|
+
kind: 'expired',
|
|
179
|
+
leaseId,
|
|
180
|
+
ownerRunId,
|
|
181
|
+
ownerAttempt: normalizeAttempt(receipt.leaseOwnerAttempt),
|
|
182
|
+
expiresAt,
|
|
183
|
+
expiresAtMs,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (!leaseId) {
|
|
187
|
+
return {
|
|
188
|
+
kind: 'invalid',
|
|
189
|
+
reason: 'missing_lease_id',
|
|
190
|
+
leaseId,
|
|
191
|
+
ownerRunId,
|
|
192
|
+
expiresAt,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
kind: 'active',
|
|
197
|
+
leaseId,
|
|
198
|
+
ownerRunId,
|
|
199
|
+
ownerAttempt: normalizeAttempt(receipt.leaseOwnerAttempt),
|
|
200
|
+
expiresAt,
|
|
201
|
+
expiresAtMs,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function classifyWorkReceiptState(
|
|
206
|
+
receipt: WorkReceiptStateInput,
|
|
207
|
+
nowMs: number,
|
|
208
|
+
): WorkReceiptState {
|
|
209
|
+
const lease = classifyWorkReceiptLeaseState(receipt, nowMs);
|
|
210
|
+
switch (receipt.status) {
|
|
211
|
+
case 'pending':
|
|
212
|
+
return { kind: 'pending', terminal: false, reusable: false, lease };
|
|
213
|
+
case 'running':
|
|
214
|
+
return { kind: 'running', terminal: false, reusable: false, lease };
|
|
215
|
+
case 'completed':
|
|
216
|
+
return { kind: 'completed', terminal: true, reusable: true, lease };
|
|
217
|
+
case 'skipped':
|
|
218
|
+
return { kind: 'skipped', terminal: true, reusable: true, lease };
|
|
219
|
+
case 'failed':
|
|
220
|
+
return { kind: 'failed', terminal: true, reusable: false, lease };
|
|
221
|
+
default:
|
|
222
|
+
return assertNever(receipt.status);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function decideWorkReceiptClaim(input: {
|
|
227
|
+
receipt: WorkReceiptStateInput | null;
|
|
228
|
+
claimantRunId?: string | null;
|
|
229
|
+
claimantRunAttempt?: number | null;
|
|
230
|
+
reclaimRunning?: boolean;
|
|
231
|
+
forceRefresh?: boolean;
|
|
232
|
+
forceFailedRefresh?: boolean;
|
|
233
|
+
nowMs: number;
|
|
234
|
+
}): WorkReceiptClaimDecision {
|
|
235
|
+
if (!input.receipt) return { kind: 'claim' };
|
|
236
|
+
const state = classifyWorkReceiptState(input.receipt, input.nowMs);
|
|
237
|
+
if (state.reusable && input.forceRefresh !== true) {
|
|
238
|
+
return { kind: 'reuse_terminal' };
|
|
239
|
+
}
|
|
240
|
+
if (state.kind === 'failed' && input.forceRefresh !== true) {
|
|
241
|
+
if (input.forceFailedRefresh === true) {
|
|
242
|
+
return { kind: 'claim' };
|
|
243
|
+
}
|
|
244
|
+
const claimantRunId = normalize(input.claimantRunId);
|
|
245
|
+
const ownerRunId = normalize(input.receipt.runId);
|
|
246
|
+
const claimantRunAttempt = normalizeAttempt(input.claimantRunAttempt);
|
|
247
|
+
const ownerAttempt = normalizeAttempt(input.receipt.leaseOwnerAttempt);
|
|
248
|
+
if (
|
|
249
|
+
input.receipt.failureKind === 'repairable' &&
|
|
250
|
+
claimantRunId !== null &&
|
|
251
|
+
ownerRunId !== null &&
|
|
252
|
+
(claimantRunId !== ownerRunId || ownerAttempt < claimantRunAttempt)
|
|
253
|
+
) {
|
|
254
|
+
return { kind: 'claim' };
|
|
255
|
+
}
|
|
256
|
+
return { kind: 'blocked_failed' };
|
|
257
|
+
}
|
|
258
|
+
if (state.kind === 'pending' || state.kind === 'running') {
|
|
259
|
+
if (state.lease.kind === 'active') {
|
|
260
|
+
if (
|
|
261
|
+
(state.kind !== 'running' ||
|
|
262
|
+
input.reclaimRunning === true ||
|
|
263
|
+
input.forceRefresh === true) &&
|
|
264
|
+
state.lease.ownerRunId !== null &&
|
|
265
|
+
state.lease.ownerRunId === normalize(input.claimantRunId) &&
|
|
266
|
+
state.lease.ownerAttempt < normalizeAttempt(input.claimantRunAttempt)
|
|
267
|
+
) {
|
|
268
|
+
return { kind: 'claim' };
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
kind: 'blocked_active_lease',
|
|
272
|
+
status: state.kind,
|
|
273
|
+
ownerRunId: state.lease.ownerRunId,
|
|
274
|
+
leaseId: state.lease.leaseId,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (state.lease.kind === 'invalid') {
|
|
278
|
+
return {
|
|
279
|
+
kind: 'blocked_invalid_lease',
|
|
280
|
+
reason: state.lease.reason,
|
|
281
|
+
status: state.kind,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (input.forceRefresh === true && input.reclaimRunning === true) {
|
|
285
|
+
return { kind: 'claim' };
|
|
286
|
+
}
|
|
287
|
+
if (state.kind === 'pending') {
|
|
288
|
+
return { kind: 'claim' };
|
|
289
|
+
}
|
|
290
|
+
if (
|
|
291
|
+
input.reclaimRunning === true &&
|
|
292
|
+
(state.lease.kind === 'expired' || state.lease.kind === 'none')
|
|
293
|
+
) {
|
|
294
|
+
return { kind: 'claim' };
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
kind: 'blocked_running',
|
|
298
|
+
ownerRunId:
|
|
299
|
+
state.lease.kind === 'expired'
|
|
300
|
+
? state.lease.ownerRunId
|
|
301
|
+
: normalize(input.receipt.runId),
|
|
302
|
+
leaseId:
|
|
303
|
+
state.lease.kind === 'expired'
|
|
304
|
+
? state.lease.leaseId
|
|
305
|
+
: normalize(input.receipt.leaseId),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (
|
|
309
|
+
input.forceRefresh === true &&
|
|
310
|
+
(state.kind === 'completed' || state.kind === 'skipped')
|
|
311
|
+
) {
|
|
312
|
+
if (state.lease.kind === 'active') {
|
|
313
|
+
if (
|
|
314
|
+
state.lease.ownerRunId !== null &&
|
|
315
|
+
state.lease.ownerRunId === normalize(input.claimantRunId) &&
|
|
316
|
+
state.lease.ownerAttempt < normalizeAttempt(input.claimantRunAttempt)
|
|
317
|
+
) {
|
|
318
|
+
return { kind: 'claim' };
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
kind: 'blocked_active_lease',
|
|
322
|
+
status: state.kind,
|
|
323
|
+
ownerRunId: state.lease.ownerRunId,
|
|
324
|
+
leaseId: state.lease.leaseId,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (state.lease.kind === 'invalid') {
|
|
328
|
+
return {
|
|
329
|
+
kind: 'blocked_invalid_lease',
|
|
330
|
+
reason: state.lease.reason,
|
|
331
|
+
status: state.kind,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (input.forceRefresh === true) {
|
|
336
|
+
return { kind: 'claim' };
|
|
337
|
+
}
|
|
338
|
+
return { kind: 'claim' };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function ownerRunIdForReceipt(receipt: WorkReceiptStateInput): string | null {
|
|
342
|
+
return normalize(receipt.leaseOwnerRunId) ?? normalize(receipt.runId);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function baseRunningOwnerDecision(
|
|
346
|
+
input: WorkReceiptOwnerOperationInput,
|
|
347
|
+
): WorkReceiptOwnerOperationDecision {
|
|
348
|
+
if (!input.receipt) {
|
|
349
|
+
return { kind: 'reject', reason: 'missing_receipt' };
|
|
350
|
+
}
|
|
351
|
+
if (input.receipt.status !== 'running') {
|
|
352
|
+
return { kind: 'reject', reason: 'not_running' };
|
|
353
|
+
}
|
|
354
|
+
const ownerRunId = normalize(input.ownerRunId);
|
|
355
|
+
if (!ownerRunId || ownerRunIdForReceipt(input.receipt) !== ownerRunId) {
|
|
356
|
+
return { kind: 'reject', reason: 'owner_mismatch' };
|
|
357
|
+
}
|
|
358
|
+
if (
|
|
359
|
+
normalizeAttempt(input.receipt.leaseOwnerAttempt) !==
|
|
360
|
+
normalizeAttempt(input.ownerRunAttempt)
|
|
361
|
+
) {
|
|
362
|
+
return { kind: 'reject', reason: 'attempt_mismatch' };
|
|
363
|
+
}
|
|
364
|
+
return { kind: 'allow' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Law for complete/fail/skip. A terminal write may only be made by the
|
|
369
|
+
* running receipt owner for the same attempt. When the receipt has a lease id,
|
|
370
|
+
* the writer must also present that exact lease id; legacy unleased running
|
|
371
|
+
* receipts remain writable by their run+attempt owner during the migration.
|
|
372
|
+
*/
|
|
373
|
+
export function decideWorkReceiptTerminalWrite(
|
|
374
|
+
input: WorkReceiptOwnerOperationInput,
|
|
375
|
+
): WorkReceiptOwnerOperationDecision {
|
|
376
|
+
const ownerDecision = baseRunningOwnerDecision(input);
|
|
377
|
+
if (ownerDecision.kind === 'reject') return ownerDecision;
|
|
378
|
+
|
|
379
|
+
const receiptLeaseId = normalize(input.receipt?.leaseId);
|
|
380
|
+
if (!receiptLeaseId) return { kind: 'allow' };
|
|
381
|
+
if (receiptLeaseId !== normalize(input.leaseId)) {
|
|
382
|
+
return { kind: 'reject', reason: 'lease_mismatch' };
|
|
383
|
+
}
|
|
384
|
+
return { kind: 'allow' };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Law for a single suspended receipt release. It uses the same ownership fence
|
|
389
|
+
* as terminal writes: only the running owner can release, and a leased receipt
|
|
390
|
+
* requires the exact lease id.
|
|
391
|
+
*/
|
|
392
|
+
export function decideWorkReceiptExactRelease(
|
|
393
|
+
input: WorkReceiptOwnerOperationInput,
|
|
394
|
+
): WorkReceiptOwnerOperationDecision {
|
|
395
|
+
return decideWorkReceiptTerminalWrite(input);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Law for run-fatal teardown. The owner may release all of its still-running
|
|
400
|
+
* receipts for the same attempt without threading every per-receipt lease id.
|
|
401
|
+
* It must never match another run or another attempt.
|
|
402
|
+
*/
|
|
403
|
+
export function decideWorkReceiptOwnerRelease(
|
|
404
|
+
input: WorkReceiptOwnerOperationInput,
|
|
405
|
+
): WorkReceiptOwnerOperationDecision {
|
|
406
|
+
return baseRunningOwnerDecision(input);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Law for heartbeat. Heartbeats are stricter than terminal writes: they renew a
|
|
411
|
+
* specific live lease, so both receipt and request must carry the same lease id.
|
|
412
|
+
*/
|
|
413
|
+
export function decideWorkReceiptHeartbeat(
|
|
414
|
+
input: WorkReceiptOwnerOperationInput,
|
|
415
|
+
): WorkReceiptOwnerOperationDecision {
|
|
416
|
+
const ownerDecision = baseRunningOwnerDecision(input);
|
|
417
|
+
if (ownerDecision.kind === 'reject') return ownerDecision;
|
|
418
|
+
|
|
419
|
+
const requestLeaseId = normalize(input.leaseId);
|
|
420
|
+
if (!requestLeaseId) {
|
|
421
|
+
return { kind: 'reject', reason: 'missing_request_lease' };
|
|
422
|
+
}
|
|
423
|
+
if (normalize(input.receipt?.leaseId) !== requestLeaseId) {
|
|
424
|
+
return { kind: 'reject', reason: 'lease_mismatch' };
|
|
425
|
+
}
|
|
426
|
+
return { kind: 'allow' };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function isReusableWorkReceiptState(
|
|
430
|
+
receipt: WorkReceiptStateInput,
|
|
431
|
+
): boolean {
|
|
432
|
+
return receipt.status === 'completed' || receipt.status === 'skipped';
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function assertNever(value: never): never {
|
|
436
|
+
throw new Error(`Unhandled work receipt state: ${String(value)}`);
|
|
437
|
+
}
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
import { isReusableWorkReceiptState } from './work-receipt-state-machine';
|
|
2
|
+
|
|
3
|
+
export type WorkReceiptFailureKind = 'terminal' | 'repairable';
|
|
4
|
+
|
|
5
|
+
export const WORK_RECEIPT_FAILURE_KIND_CODE: Record<
|
|
6
|
+
WorkReceiptFailureKind,
|
|
7
|
+
number
|
|
8
|
+
> = {
|
|
9
|
+
terminal: 0,
|
|
10
|
+
repairable: 1,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function workReceiptFailureKindFromCode(
|
|
14
|
+
value: unknown,
|
|
15
|
+
): WorkReceiptFailureKind {
|
|
16
|
+
return Number(value) === WORK_RECEIPT_FAILURE_KIND_CODE.repairable
|
|
17
|
+
? 'repairable'
|
|
18
|
+
: 'terminal';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function workReceiptFailureKindCode(
|
|
22
|
+
value: WorkReceiptFailureKind | null | undefined,
|
|
23
|
+
): number {
|
|
24
|
+
return WORK_RECEIPT_FAILURE_KIND_CODE[value ?? 'terminal'];
|
|
25
|
+
}
|
|
26
|
+
|
|
1
27
|
export type WorkReceiptStatus =
|
|
2
28
|
| 'pending'
|
|
3
29
|
| 'running'
|
|
@@ -10,7 +36,12 @@ export type WorkReceipt = {
|
|
|
10
36
|
status: WorkReceiptStatus;
|
|
11
37
|
output?: unknown;
|
|
12
38
|
error?: string | null;
|
|
39
|
+
failureKind?: WorkReceiptFailureKind | null;
|
|
13
40
|
runId?: string | null;
|
|
41
|
+
leaseId?: string | null;
|
|
42
|
+
leaseOwnerRunId?: string | null;
|
|
43
|
+
leaseOwnerAttempt?: number | null;
|
|
44
|
+
leaseExpiresAt?: string | null;
|
|
14
45
|
updatedAt?: string | null;
|
|
15
46
|
};
|
|
16
47
|
|
|
@@ -18,26 +49,57 @@ export type WorkReceiptCommand = {
|
|
|
18
49
|
playName: string;
|
|
19
50
|
runId: string;
|
|
20
51
|
key: string;
|
|
52
|
+
leaseId?: string | null;
|
|
53
|
+
runAttempt?: number | null;
|
|
54
|
+
leaseTtlMs?: number | null;
|
|
55
|
+
leaseAware?: boolean;
|
|
21
56
|
reclaimRunning?: boolean;
|
|
22
57
|
forceRefresh?: boolean;
|
|
58
|
+
forceFailedRefresh?: boolean;
|
|
23
59
|
};
|
|
24
60
|
|
|
25
61
|
export type WorkReceiptBatchClaimCommand = {
|
|
26
62
|
playName: string;
|
|
27
63
|
runId: string;
|
|
28
64
|
keys: string[];
|
|
65
|
+
leaseAware?: boolean;
|
|
29
66
|
reclaimRunning?: boolean;
|
|
30
67
|
forceRefresh?: boolean;
|
|
68
|
+
forceFailedRefresh?: boolean;
|
|
69
|
+
runAttempt?: number | null;
|
|
70
|
+
leaseTtlMs?: number | null;
|
|
31
71
|
};
|
|
32
72
|
|
|
33
73
|
export type WorkReceiptBatchCompleteCommand = {
|
|
34
74
|
playName: string;
|
|
35
|
-
receipts: Array<{
|
|
75
|
+
receipts: Array<{
|
|
76
|
+
runId: string;
|
|
77
|
+
key: string;
|
|
78
|
+
output: unknown;
|
|
79
|
+
leaseId?: string | null;
|
|
80
|
+
runAttempt?: number | null;
|
|
81
|
+
}>;
|
|
36
82
|
};
|
|
37
83
|
|
|
38
84
|
export type WorkReceiptBatchFailCommand = {
|
|
39
85
|
playName: string;
|
|
40
|
-
receipts: Array<{
|
|
86
|
+
receipts: Array<{
|
|
87
|
+
runId: string;
|
|
88
|
+
key: string;
|
|
89
|
+
error: string;
|
|
90
|
+
failureKind?: WorkReceiptFailureKind | null;
|
|
91
|
+
leaseId?: string | null;
|
|
92
|
+
runAttempt?: number | null;
|
|
93
|
+
}>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type WorkReceiptBatchHeartbeatCommand = {
|
|
97
|
+
playName: string;
|
|
98
|
+
runId: string;
|
|
99
|
+
runAttempt?: number | null;
|
|
100
|
+
leaseId: string;
|
|
101
|
+
keys: string[];
|
|
102
|
+
leaseTtlMs?: number | null;
|
|
41
103
|
};
|
|
42
104
|
|
|
43
105
|
export type WorkReceiptGetCommand = {
|
|
@@ -58,6 +120,10 @@ export type WorkReceiptClaim =
|
|
|
58
120
|
disposition: 'running';
|
|
59
121
|
receipt: WorkReceipt;
|
|
60
122
|
}
|
|
123
|
+
| {
|
|
124
|
+
disposition: 'blocked_active_lease';
|
|
125
|
+
receipt: WorkReceipt;
|
|
126
|
+
}
|
|
61
127
|
| {
|
|
62
128
|
disposition: 'failed';
|
|
63
129
|
receipt: WorkReceipt;
|
|
@@ -74,11 +140,22 @@ export type WorkReceiptStore = {
|
|
|
74
140
|
): Promise<WorkReceipt | null>;
|
|
75
141
|
completeReceipts?(
|
|
76
142
|
input: WorkReceiptBatchCompleteCommand,
|
|
77
|
-
): Promise<WorkReceipt
|
|
143
|
+
): Promise<Array<WorkReceipt | null>>;
|
|
78
144
|
failReceipt(
|
|
79
|
-
input: WorkReceiptCommand & {
|
|
145
|
+
input: WorkReceiptCommand & {
|
|
146
|
+
error: string;
|
|
147
|
+
failureKind?: WorkReceiptFailureKind | null;
|
|
148
|
+
},
|
|
149
|
+
): Promise<WorkReceipt | null>;
|
|
150
|
+
failReceipts?(
|
|
151
|
+
input: WorkReceiptBatchFailCommand,
|
|
152
|
+
): Promise<Array<WorkReceipt | null>>;
|
|
153
|
+
releaseReceipt?(
|
|
154
|
+
input: WorkReceiptCommand,
|
|
80
155
|
): Promise<WorkReceipt | null>;
|
|
81
|
-
|
|
156
|
+
heartbeatReceipts?(
|
|
157
|
+
input: WorkReceiptBatchHeartbeatCommand,
|
|
158
|
+
): Promise<Array<WorkReceipt | null>>;
|
|
82
159
|
};
|
|
83
160
|
|
|
84
161
|
const SCOPED_WORK_RECEIPT_KEY_VERSION = 'ctxv2';
|
|
@@ -122,5 +199,5 @@ export function scopedWorkReceiptKeyPrefix(input: {
|
|
|
122
199
|
}
|
|
123
200
|
|
|
124
201
|
export function isReusableWorkReceipt(receipt: WorkReceipt): boolean {
|
|
125
|
-
return receipt
|
|
202
|
+
return isReusableWorkReceiptState(receipt);
|
|
126
203
|
}
|
|
@@ -42,6 +42,12 @@ const TRANSPORT_OWNED_REQUEST_HEADERS = new Set([
|
|
|
42
42
|
'x-forwarded-host',
|
|
43
43
|
'x-forwarded-proto',
|
|
44
44
|
]);
|
|
45
|
+
const CROSS_ORIGIN_REDIRECT_SAFE_HEADERS = new Set([
|
|
46
|
+
'accept',
|
|
47
|
+
'accept-language',
|
|
48
|
+
'content-language',
|
|
49
|
+
'content-type',
|
|
50
|
+
]);
|
|
45
51
|
|
|
46
52
|
function cloneHeaders(headers: RequestInit['headers']): Headers {
|
|
47
53
|
return new Headers(headers);
|
|
@@ -57,6 +63,18 @@ function removeTransportOwnedHeaders(headers: Headers): void {
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
function removeUnsafeCrossOriginRedirectHeaders(headers: Headers): void {
|
|
67
|
+
const namesToDelete: string[] = [];
|
|
68
|
+
for (const [name] of headers) {
|
|
69
|
+
if (!CROSS_ORIGIN_REDIRECT_SAFE_HEADERS.has(name.toLowerCase())) {
|
|
70
|
+
namesToDelete.push(name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const name of namesToDelete) {
|
|
74
|
+
headers.delete(name);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
function withContentLength(
|
|
61
79
|
body: Buffer | string | undefined,
|
|
62
80
|
headers: Headers,
|
|
@@ -222,14 +240,11 @@ function createRequest(
|
|
|
222
240
|
response.on('error', reject);
|
|
223
241
|
response.on('end', () => {
|
|
224
242
|
resolve(
|
|
225
|
-
new Response(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
headers: response.headers as HeadersInit,
|
|
231
|
-
},
|
|
232
|
-
),
|
|
243
|
+
new Response(noBodyResponse ? null : Buffer.concat(chunks), {
|
|
244
|
+
status,
|
|
245
|
+
statusText: response.statusMessage,
|
|
246
|
+
headers: response.headers as HeadersInit,
|
|
247
|
+
}),
|
|
233
248
|
);
|
|
234
249
|
});
|
|
235
250
|
},
|
|
@@ -248,7 +263,6 @@ function initForRedirect(
|
|
|
248
263
|
from: URL,
|
|
249
264
|
to: URL,
|
|
250
265
|
status: number,
|
|
251
|
-
sensitiveHeaders: Iterable<string>,
|
|
252
266
|
): RequestInit {
|
|
253
267
|
const headers = cloneHeaders(init.headers);
|
|
254
268
|
let method = String(init.method ?? 'GET').toUpperCase();
|
|
@@ -265,12 +279,7 @@ function initForRedirect(
|
|
|
265
279
|
}
|
|
266
280
|
|
|
267
281
|
if (from.origin !== to.origin) {
|
|
268
|
-
|
|
269
|
-
deleteHeader(headers, 'cookie');
|
|
270
|
-
deleteHeader(headers, 'proxy-authorization');
|
|
271
|
-
for (const header of sensitiveHeaders) {
|
|
272
|
-
deleteHeader(headers, header);
|
|
273
|
-
}
|
|
282
|
+
removeUnsafeCrossOriginRedirectHeaders(headers);
|
|
274
283
|
}
|
|
275
284
|
|
|
276
285
|
return {
|
|
@@ -290,7 +299,6 @@ export async function safeOutboundFetch(
|
|
|
290
299
|
const redirectMode = init.redirect ?? 'follow';
|
|
291
300
|
const maxRedirects = options.maxRedirects ?? 10;
|
|
292
301
|
const maxResponseBytes = options.maxResponseBytes;
|
|
293
|
-
const sensitiveHeaders = options.sensitiveHeaders ?? [];
|
|
294
302
|
const validateUrl = options.validateUrl;
|
|
295
303
|
let currentUrl = assertPublicHttpUrl(input);
|
|
296
304
|
let currentInit: RequestInit = { ...init, redirect: 'manual' };
|
|
@@ -348,7 +356,6 @@ export async function safeOutboundFetch(
|
|
|
348
356
|
currentUrl,
|
|
349
357
|
nextUrl,
|
|
350
358
|
response.status,
|
|
351
|
-
sensitiveHeaders,
|
|
352
359
|
);
|
|
353
360
|
currentUrl = nextUrl;
|
|
354
361
|
}
|