deepline 0.3.50 → 0.3.52
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/sdk/src/index.ts +1 -0
- package/dist/bundling-sources/sdk/src/play.ts +1 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +81 -20
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +52 -21
- package/dist/bundling-sources/shared_libs/play-runtime/secret-capability.ts +5 -1
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +112 -5
- package/dist/bundling-sources/shared_libs/security/safe-fetch.ts +6 -1
- package/dist/cli/index.js +144 -2745
- package/dist/cli/index.mjs +41 -2643
- package/dist/{compiler-manifest-CbzdZrJj.d.mts → compiler-manifest-B47AA7As.d.mts} +46 -4
- package/dist/{compiler-manifest-CbzdZrJj.d.ts → compiler-manifest-B47AA7As.d.ts} +46 -4
- package/dist/index.d.mts +149 -149
- package/dist/index.d.ts +149 -149
- package/dist/index.js +1515 -117
- package/dist/index.mjs +1514 -117
- package/dist/install-integrity.json +2 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +5 -2614
- package/package.json +2 -1
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
} from '../../shared_libs/play-runtime/tool-result.js';
|
|
81
81
|
export { readValue, readList } from '../../shared_libs/play-runtime/tool-result.js';
|
|
82
82
|
import { createDeferredPlayDataset } from '../../shared_libs/plays/dataset.js';
|
|
83
|
+
export { CtxFetchHttpError } from '../../shared_libs/plays/authoring-contract.js';
|
|
83
84
|
import {
|
|
84
85
|
QUERY_RESULT_DATASET_PAGE_SIZE,
|
|
85
86
|
isCustomerDbDatasetTool,
|
|
@@ -199,7 +199,7 @@ export const SDK_RELEASE = {
|
|
|
199
199
|
// available at toolResponse.rawV2 while toolResponse.raw and all declared
|
|
200
200
|
// getters keep their established compatibility behavior.
|
|
201
201
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
202
|
-
version: '0.3.
|
|
202
|
+
version: '0.3.52',
|
|
203
203
|
updateSummary:
|
|
204
204
|
'Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.',
|
|
205
205
|
packageCapabilities: {
|
|
@@ -178,6 +178,7 @@ import {
|
|
|
178
178
|
resolveDurableCallCachePolicy,
|
|
179
179
|
} from './durable-call-cache';
|
|
180
180
|
import {
|
|
181
|
+
CtxFetchHttpError,
|
|
181
182
|
PLAY_AUTHORING_CONTRACT_EDITION,
|
|
182
183
|
normalizePlayAuthoringCustomerDbStatement,
|
|
183
184
|
validateOptionalPlayAuthoringField,
|
|
@@ -287,6 +288,7 @@ import {
|
|
|
287
288
|
isSecretAuthInput,
|
|
288
289
|
isPlaintextSecretPromise,
|
|
289
290
|
isSecretHandle,
|
|
291
|
+
isSecretValue,
|
|
290
292
|
secretAuthEntries,
|
|
291
293
|
secretAuthHeaderMarkers,
|
|
292
294
|
valueContainsSecret,
|
|
@@ -2864,7 +2866,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
2864
2866
|
this.secretRedactor.register(value);
|
|
2865
2867
|
return value;
|
|
2866
2868
|
}
|
|
2867
|
-
|
|
2869
|
+
if (isSecretHandle(secret) || isSecretValue(secret)) {
|
|
2870
|
+
return this.resolveSecretValue(secret);
|
|
2871
|
+
}
|
|
2872
|
+
throw new Error(
|
|
2873
|
+
'ctx.secrets auth requires a resolved string or an approved secret value.',
|
|
2874
|
+
);
|
|
2868
2875
|
}
|
|
2869
2876
|
|
|
2870
2877
|
private async resolveSecretValue(secret: SecretValue): Promise<string> {
|
|
@@ -4251,6 +4258,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
4251
4258
|
) => T;
|
|
4252
4259
|
onClaimedResult?: (output: T, receiptKey: string) => T;
|
|
4253
4260
|
shouldPersistFailure?: (error: unknown) => boolean;
|
|
4261
|
+
transient?: boolean;
|
|
4254
4262
|
markRunningBeforeExecute?: boolean;
|
|
4255
4263
|
requiresExecutionLock?: boolean;
|
|
4256
4264
|
executionLockTtlMs?: number;
|
|
@@ -4260,6 +4268,28 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
4260
4268
|
}) => Promise<T>;
|
|
4261
4269
|
},
|
|
4262
4270
|
): Promise<T> {
|
|
4271
|
+
let releaseExternalCallSlot: (() => void) | null = null;
|
|
4272
|
+
const execute = async (leaseId: string | null): Promise<T> =>
|
|
4273
|
+
await opts.execute({
|
|
4274
|
+
leaseId,
|
|
4275
|
+
retainExternalCallSlot: (release) => {
|
|
4276
|
+
if (releaseExternalCallSlot) {
|
|
4277
|
+
release();
|
|
4278
|
+
throw new Error(
|
|
4279
|
+
`ctx.${operation}(${id}) attempted to retain more than one external-call slot.`,
|
|
4280
|
+
);
|
|
4281
|
+
}
|
|
4282
|
+
releaseExternalCallSlot = release;
|
|
4283
|
+
},
|
|
4284
|
+
});
|
|
4285
|
+
if (opts.transient === true) {
|
|
4286
|
+
try {
|
|
4287
|
+
return await execute(null);
|
|
4288
|
+
} finally {
|
|
4289
|
+
const release = releaseExternalCallSlot as (() => void) | null;
|
|
4290
|
+
release?.();
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4263
4293
|
const stalePolicy = resolveDurableCallCachePolicy(
|
|
4264
4294
|
opts.staleAfterSeconds,
|
|
4265
4295
|
operation === 'step'
|
|
@@ -4279,7 +4309,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
4279
4309
|
staleAfterSeconds: stalePolicy.staleAfterSeconds,
|
|
4280
4310
|
cacheEpochMs: this.durableCallCacheEpochMs,
|
|
4281
4311
|
});
|
|
4282
|
-
let releaseExternalCallSlot: (() => void) | null = null;
|
|
4283
4312
|
try {
|
|
4284
4313
|
return await executeWithDurableRuntimeReceipt<T>({
|
|
4285
4314
|
operation,
|
|
@@ -4307,19 +4336,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
4307
4336
|
toolErrorSchemaVersion: this.currentToolErrorSchemaVersion,
|
|
4308
4337
|
formatError: (error) => this.formatRuntimeError(error),
|
|
4309
4338
|
log: (message) => this.log(message),
|
|
4310
|
-
execute: ({ leaseId }) =>
|
|
4311
|
-
opts.execute({
|
|
4312
|
-
leaseId,
|
|
4313
|
-
retainExternalCallSlot: (release) => {
|
|
4314
|
-
if (releaseExternalCallSlot) {
|
|
4315
|
-
release();
|
|
4316
|
-
throw new Error(
|
|
4317
|
-
`ctx.${operation}(${id}) attempted to retain more than one external-call slot.`,
|
|
4318
|
-
);
|
|
4319
|
-
}
|
|
4320
|
-
releaseExternalCallSlot = release;
|
|
4321
|
-
},
|
|
4322
|
-
}),
|
|
4339
|
+
execute: ({ leaseId }) => execute(leaseId),
|
|
4323
4340
|
});
|
|
4324
4341
|
} finally {
|
|
4325
4342
|
const release = releaseExternalCallSlot as (() => void) | null;
|
|
@@ -10206,9 +10223,31 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10206
10223
|
...secretHeaderMarkers,
|
|
10207
10224
|
},
|
|
10208
10225
|
row: rowFetchScope,
|
|
10226
|
+
// Edition 5 changes non-2xx responses from cached values to
|
|
10227
|
+
// non-cacheable errors. Partition those receipts from editions
|
|
10228
|
+
// 1–4 so a republished artifact makes a fresh request instead of
|
|
10229
|
+
// inheriting a legacy error record.
|
|
10230
|
+
...(this.currentAuthoringContractEdition >= 5
|
|
10231
|
+
? {
|
|
10232
|
+
authoringContractEdition:
|
|
10233
|
+
this.currentAuthoringContractEdition,
|
|
10234
|
+
}
|
|
10235
|
+
: {}),
|
|
10209
10236
|
}),
|
|
10210
10237
|
),
|
|
10211
10238
|
staleAfterSeconds: options?.staleAfterSeconds,
|
|
10239
|
+
transient: options?.transient === true,
|
|
10240
|
+
onRecovered: (output) => {
|
|
10241
|
+
if (!output.ok && this.currentAuthoringContractEdition >= 5) {
|
|
10242
|
+
throw new CtxFetchHttpError(output);
|
|
10243
|
+
}
|
|
10244
|
+
return output;
|
|
10245
|
+
},
|
|
10246
|
+
shouldPersistFailure: (error) =>
|
|
10247
|
+
!(
|
|
10248
|
+
error instanceof CtxFetchHttpError &&
|
|
10249
|
+
this.currentAuthoringContractEdition >= 5
|
|
10250
|
+
),
|
|
10212
10251
|
execute: async ({ retainExternalCallSlot }) => {
|
|
10213
10252
|
const method = (init.method ?? 'GET').toUpperCase();
|
|
10214
10253
|
const secretHeaders = await this.resolveSecretAuth(secretAuth);
|
|
@@ -10216,6 +10255,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10216
10255
|
...normalizeFetchHeaders(init.headers),
|
|
10217
10256
|
...secretHeaders,
|
|
10218
10257
|
};
|
|
10258
|
+
if (headers['user-agent'] === undefined) {
|
|
10259
|
+
headers['user-agent'] = 'Deepline Play Runtime';
|
|
10260
|
+
}
|
|
10219
10261
|
const fetchInit = { ...init, headers };
|
|
10220
10262
|
delete fetchInit.auth;
|
|
10221
10263
|
const boundaryId = this.durableBoundaryId(
|
|
@@ -10234,7 +10276,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10234
10276
|
);
|
|
10235
10277
|
|
|
10236
10278
|
const existing = this.checkpoint.resolvedBoundaries?.[boundaryId];
|
|
10237
|
-
|
|
10279
|
+
// A transient request deliberately has no durable recovery path, so
|
|
10280
|
+
// its response stays in the running Play rather than a checkpoint.
|
|
10281
|
+
if (
|
|
10282
|
+
options?.transient !== true &&
|
|
10283
|
+
existing?.kind === 'fetch' &&
|
|
10284
|
+
'output' in existing
|
|
10285
|
+
) {
|
|
10238
10286
|
this.log(`ctx.fetch(${url}): recovered response from checkpoint`);
|
|
10239
10287
|
if (this.durableDirectToolResultsBackedByReceipts) {
|
|
10240
10288
|
// The outer durable receipt is the replay authority in hosted
|
|
@@ -10315,6 +10363,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10315
10363
|
fetchImpl: this.#options.fetchImpl,
|
|
10316
10364
|
sensitiveHeaders: Object.keys(secretHeaderMarkers),
|
|
10317
10365
|
stripHeadersOnCrossOriginRedirect: true,
|
|
10366
|
+
headersOnCrossOriginRedirect: {
|
|
10367
|
+
'user-agent': 'Deepline Play Runtime',
|
|
10368
|
+
},
|
|
10318
10369
|
}),
|
|
10319
10370
|
});
|
|
10320
10371
|
bodyText = await readCtxFetchBody({
|
|
@@ -10366,6 +10417,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10366
10417
|
`ctx.fetch(${method} ${url}) failed while reading the response body.`,
|
|
10367
10418
|
);
|
|
10368
10419
|
}
|
|
10420
|
+
const rawJson = parseJsonOrNull(bodyText);
|
|
10369
10421
|
const redactedBodyText = this.secretRedactor.redactString(bodyText);
|
|
10370
10422
|
const output: PlayFetchResponse = {
|
|
10371
10423
|
ok: response.ok,
|
|
@@ -10376,12 +10428,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10376
10428
|
Object.fromEntries(response.headers.entries()),
|
|
10377
10429
|
) as Record<string, string>,
|
|
10378
10430
|
bodyText: redactedBodyText,
|
|
10379
|
-
json: this.secretRedactor.redactKnownSecrets(
|
|
10380
|
-
parseJsonOrNull(bodyText),
|
|
10381
|
-
),
|
|
10431
|
+
json: this.secretRedactor.redactKnownSecrets(rawJson),
|
|
10382
10432
|
};
|
|
10383
10433
|
|
|
10384
|
-
|
|
10434
|
+
// Edition 5 adopts normal fetch semantics: a non-2xx response is
|
|
10435
|
+
// a failed durable operation. Throw before checkpoint/receipt
|
|
10436
|
+
// completion so the failure is never cached. Editions 1–4 retain
|
|
10437
|
+
// their response-record behavior for the same upstream response.
|
|
10438
|
+
if (!output.ok && this.currentAuthoringContractEdition >= 5) {
|
|
10439
|
+
throw new CtxFetchHttpError(output);
|
|
10440
|
+
}
|
|
10441
|
+
|
|
10442
|
+
if (
|
|
10443
|
+
options?.transient !== true &&
|
|
10444
|
+
!this.durableDirectToolResultsBackedByReceipts
|
|
10445
|
+
) {
|
|
10385
10446
|
this.checkpoint.resolvedBoundaries = {
|
|
10386
10447
|
...(this.checkpoint.resolvedBoundaries ?? {}),
|
|
10387
10448
|
[boundaryId]: {
|
|
@@ -697,26 +697,39 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
697
697
|
isInFlightRuntimeReceipt(receipt) &&
|
|
698
698
|
typeof receipt.runId === 'string' &&
|
|
699
699
|
receipt.runId.trim() === input.runId;
|
|
700
|
-
const
|
|
701
|
-
kind: 'recovered';
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
700
|
+
const waitForRunningReceiptOrRelease = async (): Promise<
|
|
701
|
+
{ kind: 'recovered'; output: T } | { kind: 'claimed' }
|
|
702
|
+
> => {
|
|
703
|
+
const maxAttempts =
|
|
704
|
+
input.runningReceiptWaitMaxAttempts ?? DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS;
|
|
705
|
+
const delayMs =
|
|
706
|
+
input.runningReceiptWaitDelayMs ?? DURABLE_RECEIPT_WAIT_DELAY_MS;
|
|
707
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
708
|
+
if (attempt > 0) await sleepReceiptWait(delayMs);
|
|
709
|
+
const current = await input.store.get(input.receiptKey);
|
|
710
|
+
if (current?.status === 'completed' || current?.status === 'skipped') {
|
|
711
|
+
return {
|
|
712
|
+
kind: 'recovered',
|
|
713
|
+
output: await recoverCompletedReceipt(current, 'in_flight'),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (current?.status === 'failed') {
|
|
717
|
+
throw runtimeReceiptFailureError(
|
|
718
|
+
current,
|
|
719
|
+
`ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused`,
|
|
720
|
+
input.toolErrorSchemaVersion,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
if (
|
|
724
|
+
(current?.status === 'queued' || current?.status === 'pending') &&
|
|
725
|
+
!current.leaseId &&
|
|
726
|
+
!current.leaseExpiresAt
|
|
727
|
+
) {
|
|
728
|
+
return await reclaimReceipt();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
|
|
732
|
+
};
|
|
720
733
|
const repairOrWaitForRunningReceipt = async (
|
|
721
734
|
receipt: RuntimeStepReceipt,
|
|
722
735
|
): Promise<{ kind: 'recovered'; output: T } | { kind: 'claimed' }> => {
|
|
@@ -726,7 +739,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
726
739
|
return { kind: 'claimed' };
|
|
727
740
|
}
|
|
728
741
|
try {
|
|
729
|
-
return await
|
|
742
|
+
return await waitForRunningReceiptOrRelease();
|
|
730
743
|
} catch (error) {
|
|
731
744
|
if (error instanceof RuntimeReceiptWaitTimeoutError) {
|
|
732
745
|
const recovered = await reclaimReceipt();
|
|
@@ -840,6 +853,24 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
840
853
|
assertNoSecretTaint(result, `ctx.${input.operation} result`);
|
|
841
854
|
} catch (error) {
|
|
842
855
|
if (input.shouldPersistFailure?.(error) === false) {
|
|
856
|
+
// This error is deliberately not a durable execution result (for
|
|
857
|
+
// example, an HTTP non-2xx response under the current ctx.fetch
|
|
858
|
+
// contract). Release only the lease we still own so a later run can
|
|
859
|
+
// make a fresh, idempotency-protected attempt rather than inheriting a
|
|
860
|
+
// running or failed receipt.
|
|
861
|
+
const released = await input.store.release(
|
|
862
|
+
input.receiptKey,
|
|
863
|
+
input.runId,
|
|
864
|
+
ownedLeaseId,
|
|
865
|
+
);
|
|
866
|
+
if (
|
|
867
|
+
!released ||
|
|
868
|
+
(released.status !== 'queued' && released.status !== 'pending')
|
|
869
|
+
) {
|
|
870
|
+
throw new Error(
|
|
871
|
+
`ctx.${input.operation}(${input.id}): non-cacheable execution failed but receipt ownership could not be released: ${input.formatError(error)}.`,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
843
874
|
throw error;
|
|
844
875
|
}
|
|
845
876
|
// The ownership is uncertain, so neither `fail` nor `release` is safe.
|
|
@@ -129,7 +129,11 @@ export function valueContainsSecret(value: unknown): boolean {
|
|
|
129
129
|
const seen = new WeakSet<object>();
|
|
130
130
|
while (pending.length > 0) {
|
|
131
131
|
const candidate = pending.pop();
|
|
132
|
-
if (
|
|
132
|
+
if (
|
|
133
|
+
isSecretValue(candidate) || isSecretAuth(candidate)
|
|
134
|
+
) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
133
137
|
if (typeof candidate === 'string') {
|
|
134
138
|
if (SECRET_HANDLE_MARKER_RE.test(candidate)) return true;
|
|
135
139
|
continue;
|
|
@@ -7,9 +7,11 @@ import type { ToolExecutionErrorSchemaVersion } from './tool-execution-error';
|
|
|
7
7
|
import type { PlayDataset, PlayDatasetInput, PlayDatasetRow } from './dataset';
|
|
8
8
|
|
|
9
9
|
export const LEGACY_PLAY_AUTHORING_CONTRACT_EDITION = 1 as const;
|
|
10
|
-
export const PLAY_AUTHORING_CONTRACT_EDITION =
|
|
10
|
+
export const PLAY_AUTHORING_CONTRACT_EDITION = 5 as const;
|
|
11
11
|
export const PLAY_AUTHORING_INPUT_SCHEMA_SNAPSHOT_EDITION = 3 as const;
|
|
12
|
-
export const SUPPORTED_PLAY_AUTHORING_CONTRACT_EDITIONS = [
|
|
12
|
+
export const SUPPORTED_PLAY_AUTHORING_CONTRACT_EDITIONS = [
|
|
13
|
+
1, 2, 3, 4, 5,
|
|
14
|
+
] as const;
|
|
13
15
|
|
|
14
16
|
export type PlayAuthoringContractEdition =
|
|
15
17
|
(typeof SUPPORTED_PLAY_AUTHORING_CONTRACT_EDITIONS)[number];
|
|
@@ -49,6 +51,47 @@ export const PLAY_AUTHORING_CONTRACT_CHANGELOG = [
|
|
|
49
51
|
newWritesEnd: null,
|
|
50
52
|
readerRemoval: null,
|
|
51
53
|
},
|
|
54
|
+
{
|
|
55
|
+
edition: 5,
|
|
56
|
+
changed:
|
|
57
|
+
'ctx.fetch throws a typed, redacted CtxFetchHttpError for non-2xx responses. Editions 1–4 continue to return the response record for every HTTP status.',
|
|
58
|
+
compatibilityOwner: 'Plays Runtime',
|
|
59
|
+
newWritesEnd: null,
|
|
60
|
+
readerRemoval: null,
|
|
61
|
+
},
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Customer-visible runtime capabilities that are not expressible as one
|
|
66
|
+
* `ctx.*` signature. The generated authoring reference renders this inventory
|
|
67
|
+
* directly; add a row here alongside any new supported runtime global or
|
|
68
|
+
* execution-boundary guarantee.
|
|
69
|
+
*/
|
|
70
|
+
export const PLAY_AUTHORING_RUNTIME_CAPABILITIES = [
|
|
71
|
+
{
|
|
72
|
+
capability: 'Durable external I/O',
|
|
73
|
+
surface: '`ctx.fetch`, `ctx.tools.execute`, `ctx.step`, `ctx.runPlay`',
|
|
74
|
+
contract:
|
|
75
|
+
'Use a `ctx.*` primitive for external work. Each primitive owns durable receipt identity and replay; raw I/O in an authored handler does not.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
capability: 'Web Crypto',
|
|
79
|
+
surface: '`crypto.subtle`',
|
|
80
|
+
contract:
|
|
81
|
+
'WebCrypto is available for standards-based signing, verification, encryption, and key import. Keep private material in `ctx.secrets`; `docs-examples/sdk-v2/github-app-jwt.play.ts` signs an RS256 GitHub App JWT and uses `staleAfterSeconds: 0` for its time-varying auth exchange.',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
capability: 'Workspace secrets',
|
|
85
|
+
surface: '`ctx.secrets.get`, `.bearer`, `.header`',
|
|
86
|
+
contract:
|
|
87
|
+
'Declare the secret at the Play boundary, read it only at runtime, and never return or log it. Secret-bearing HTTP requests require HTTPS.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
capability: 'Freshness',
|
|
91
|
+
surface: '`staleAfterSeconds` on durable calls',
|
|
92
|
+
contract:
|
|
93
|
+
'This is call-receipt freshness: omitted/null reuses forever, zero always executes, and a positive integer is a TTL in seconds. It never changes dataset or request identity.',
|
|
94
|
+
},
|
|
52
95
|
] as const;
|
|
53
96
|
|
|
54
97
|
export const PLAY_AUTHORING_CONTRACT_ISSUE_CODES = [
|
|
@@ -78,6 +121,7 @@ export const PLAY_AUTHORING_CONTRACT_ISSUE_CODES = [
|
|
|
78
121
|
'play_authoring_fetch_secret_requires_tls',
|
|
79
122
|
'play_authoring_fetch_idempotency_required',
|
|
80
123
|
'play_authoring_fetch_key_reused_in_loop',
|
|
124
|
+
'play_authoring_fetch_http_error_branch',
|
|
81
125
|
'play_authoring_binding_invalid',
|
|
82
126
|
'play_authoring_input_schema_unresolved',
|
|
83
127
|
'tool_response_raw_access',
|
|
@@ -842,12 +886,24 @@ export type PlayAuthoringRuntimeStepOptions = {
|
|
|
842
886
|
staleAfterSeconds?: DurableCallStaleAfterSeconds;
|
|
843
887
|
};
|
|
844
888
|
|
|
889
|
+
/**
|
|
890
|
+
* Durable freshness for one `ctx.fetch` call. The key identifies the request;
|
|
891
|
+
* `staleAfterSeconds` controls when that completed request may be reused across
|
|
892
|
+
* Play runs. Omit it (or pass `null`) to reuse indefinitely, pass `0` to
|
|
893
|
+
* execute on every new run, or pass a positive number of seconds for a TTL.
|
|
894
|
+
*
|
|
895
|
+
* Use this for polling or periodically refreshing one external resource. It
|
|
896
|
+
* does not change dataset row identity.
|
|
897
|
+
*
|
|
898
|
+
*/
|
|
845
899
|
export type PlayAuthoringFetchOptions = {
|
|
846
900
|
staleAfterSeconds?: DurableCallStaleAfterSeconds;
|
|
901
|
+
/** Run through Deepline's guarded outbound path without retaining a response receipt or checkpoint. Use this for short-lived credential exchanges or other response data that should stay in the running Play only. */
|
|
902
|
+
transient?: boolean;
|
|
847
903
|
};
|
|
848
904
|
|
|
849
905
|
/**
|
|
850
|
-
*
|
|
906
|
+
* A durable response record, not a WHATWG `Response`: read the already-materialized `bodyText` and `json` properties; do not call `.json()`, `.text()`, or `.body`.
|
|
851
907
|
*
|
|
852
908
|
* @sdkReference runtime 175 PlayFetchResponse
|
|
853
909
|
*/
|
|
@@ -870,6 +926,55 @@ export type PlayAuthoringFetchResponse = {
|
|
|
870
926
|
json: unknown | null;
|
|
871
927
|
};
|
|
872
928
|
|
|
929
|
+
/**
|
|
930
|
+
* Edition 5+ `ctx.fetch` error for a non-2xx response. Its full readable body is secret-redacted; editions 1–4 retain `PlayFetchResponse { ok: false }`.
|
|
931
|
+
*
|
|
932
|
+
* @sdkReference runtime 178 CtxFetchHttpError
|
|
933
|
+
*/
|
|
934
|
+
export class CtxFetchHttpError extends Error {
|
|
935
|
+
/**
|
|
936
|
+
* Play source and the runtime are separately bundled. Recognize the stable
|
|
937
|
+
* public error code so `instanceof` works across that boundary as well.
|
|
938
|
+
*/
|
|
939
|
+
static [Symbol.hasInstance](value: unknown): boolean {
|
|
940
|
+
return (
|
|
941
|
+
value !== null &&
|
|
942
|
+
typeof value === 'object' &&
|
|
943
|
+
(value as { code?: unknown }).code === 'CTX_FETCH_HTTP_ERROR'
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/** Stable machine-readable HTTP-failure code. */
|
|
948
|
+
readonly code = 'CTX_FETCH_HTTP_ERROR' as const;
|
|
949
|
+
/** Upstream HTTP status. */
|
|
950
|
+
readonly status: number;
|
|
951
|
+
/** Upstream HTTP status text. */
|
|
952
|
+
readonly statusText: string;
|
|
953
|
+
/** Final response URL after redirects. */
|
|
954
|
+
readonly url: string;
|
|
955
|
+
/** Secret-redacted response headers. */
|
|
956
|
+
readonly headers: Record<string, string>;
|
|
957
|
+
/** Complete secret-redacted response body. */
|
|
958
|
+
readonly bodyText: string;
|
|
959
|
+
/** Eagerly parsed secret-redacted JSON, or null. */
|
|
960
|
+
readonly json: unknown | null;
|
|
961
|
+
|
|
962
|
+
/** Build the error from the durable response record. */
|
|
963
|
+
constructor(readonly response: PlayAuthoringFetchResponse) {
|
|
964
|
+
const body = response.bodyText || 'empty response body';
|
|
965
|
+
super(
|
|
966
|
+
`ctx.fetch(${response.url}) returned HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}: ${body}`,
|
|
967
|
+
);
|
|
968
|
+
this.name = 'CtxFetchHttpError';
|
|
969
|
+
this.status = response.status;
|
|
970
|
+
this.statusText = response.statusText;
|
|
971
|
+
this.url = response.url;
|
|
972
|
+
this.headers = response.headers;
|
|
973
|
+
this.bodyText = response.bodyText;
|
|
974
|
+
this.json = response.json;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
873
978
|
export type PlayAuthoringCustomerDbQueryOptions = {
|
|
874
979
|
maxRows?: number;
|
|
875
980
|
timeoutMs?: number;
|
|
@@ -1008,7 +1113,8 @@ export interface PlayAuthoringRuntimeContext {
|
|
|
1008
1113
|
): Promise<T>;
|
|
1009
1114
|
|
|
1010
1115
|
/**
|
|
1011
|
-
* Execute a durable
|
|
1116
|
+
* Execute a guarded HTTP request. By default it is durable and replay-safe; `staleAfterSeconds` governs only that completed call receipt, never dataset/request identity. Pass `{ transient: true }` for short-lived credential exchanges or other response data that must not be retained in a receipt or checkpoint. Edition 5+ throws `CtxFetchHttpError` for non-2xx; catch it only when the Play intentionally recovers, otherwise let it fail the Play. Editions 1–4 retain their previous response projections.
|
|
1117
|
+
*
|
|
1012
1118
|
* @sdkReference runtime 170 ctx.fetch(key, url, init)
|
|
1013
1119
|
*/
|
|
1014
1120
|
fetch(
|
|
@@ -2541,8 +2647,9 @@ export const PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
|
|
|
2541
2647
|
"export type PlayCallExecution = 'inline';",
|
|
2542
2648
|
`export type PlayCallOptions = { description: ${cloudReferenceType('ctx.runPlay.options.description')}; execution?: ${cloudReferenceType('ctx.runPlay.options.execution')}; timeoutMs?: ${cloudReferenceType('ctx.runPlay.options.timeoutMs')} };`,
|
|
2543
2649
|
`export type RuntimeStepOptions = { semanticKey?: ${cloudReferenceType('ctx.step.semanticKey')}; staleAfterSeconds?: ${cloudReferenceType('ctx.step.staleAfterSeconds')} };`,
|
|
2544
|
-
`export type FetchOptions = { staleAfterSeconds?: ${cloudReferenceType('ctx.fetch.staleAfterSeconds')} };`,
|
|
2650
|
+
`export type FetchOptions = { staleAfterSeconds?: ${cloudReferenceType('ctx.fetch.staleAfterSeconds')}; transient?: boolean };`,
|
|
2545
2651
|
'export type PlayFetchResponse = { ok: boolean; status: number; statusText: string; url: string; headers: Record<string, string>; bodyText: string; json: unknown | null };',
|
|
2652
|
+
"export declare class CtxFetchHttpError extends Error { readonly code: 'CTX_FETCH_HTTP_ERROR'; readonly response: PlayFetchResponse; readonly status: number; readonly statusText: string; readonly url: string; readonly headers: Record<string, string>; readonly bodyText: string; readonly json: unknown | null; }",
|
|
2546
2653
|
'export type StepResolver<Row, Value> = (row: Row, ctx: DeeplinePlayRuntimeContext, index: number, previousCell?: PreviousCell<Value>) => Value | Promise<Value>;',
|
|
2547
2654
|
'export type DatasetColumnRunInput<Row, Value> = { readonly row: Row; readonly ctx: DeeplinePlayRuntimeContext; readonly index: number; readonly previousCell?: PreviousCell<Value> };',
|
|
2548
2655
|
'export type DatasetColumnDefinition<Row, Value> = { readonly run: (input: DatasetColumnRunInput<Row, Value>) => Value | Promise<Value>; readonly runIf?: (row: Row, index: number) => boolean | Promise<boolean> };',
|
|
@@ -15,6 +15,8 @@ export type SafeFetchOptions = {
|
|
|
15
15
|
sensitiveHeaders?: Iterable<string>;
|
|
16
16
|
/** Drop every caller-supplied header when a redirect changes origin. */
|
|
17
17
|
stripHeadersOnCrossOriginRedirect?: boolean;
|
|
18
|
+
/** Headers the trusted runtime may add after caller headers are stripped. */
|
|
19
|
+
headersOnCrossOriginRedirect?: HeadersInit;
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
function removeHeader(headers: Headers, name: string) {
|
|
@@ -32,6 +34,7 @@ function requestInitForRedirect(
|
|
|
32
34
|
status: number,
|
|
33
35
|
sensitiveHeaders: Iterable<string>,
|
|
34
36
|
stripHeadersOnCrossOriginRedirect: boolean,
|
|
37
|
+
headersOnCrossOriginRedirect: HeadersInit | undefined,
|
|
35
38
|
): RequestInit {
|
|
36
39
|
let headers = new Headers(init.headers);
|
|
37
40
|
let method = String(init.method ?? 'GET').toUpperCase();
|
|
@@ -57,7 +60,7 @@ function requestInitForRedirect(
|
|
|
57
60
|
// Callers that can construct credentials from plaintext secrets cannot
|
|
58
61
|
// reliably identify every transformed value. Do not forward any caller
|
|
59
62
|
// header across an origin boundary in that trust model.
|
|
60
|
-
headers = new Headers();
|
|
63
|
+
headers = new Headers(headersOnCrossOriginRedirect);
|
|
61
64
|
} else {
|
|
62
65
|
removeHeader(headers, 'authorization');
|
|
63
66
|
removeHeader(headers, 'cookie');
|
|
@@ -88,6 +91,7 @@ export async function safePublicFetch(
|
|
|
88
91
|
const sensitiveHeaders = options.sensitiveHeaders ?? [];
|
|
89
92
|
const stripHeadersOnCrossOriginRedirect =
|
|
90
93
|
options.stripHeadersOnCrossOriginRedirect === true;
|
|
94
|
+
const headersOnCrossOriginRedirect = options.headersOnCrossOriginRedirect;
|
|
91
95
|
let currentUrl = assertPublicHttpUrl(input);
|
|
92
96
|
let currentInit: RequestInit = {
|
|
93
97
|
...init,
|
|
@@ -135,6 +139,7 @@ export async function safePublicFetch(
|
|
|
135
139
|
response.status,
|
|
136
140
|
sensitiveHeaders,
|
|
137
141
|
stripHeadersOnCrossOriginRedirect,
|
|
142
|
+
headersOnCrossOriginRedirect,
|
|
138
143
|
);
|
|
139
144
|
currentUrl = nextUrl;
|
|
140
145
|
}
|