deepline 0.3.31 → 0.3.33

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.
@@ -202,6 +202,7 @@ export async function checkSdkCompatibility(
202
202
  'X-Deepline-SDK-Version': SDK_VERSION,
203
203
  'X-Deepline-API-Major': String(SDK_API_MAJOR),
204
204
  'X-Deepline-API-Contract': SDK_API_CONTRACT,
205
+ 'X-Deepline-CLI-Update-Preferences': '1',
205
206
  },
206
207
  signal: controller.signal,
207
208
  });
@@ -97,9 +97,11 @@ export type SdkSupportPolicy = {
97
97
  * Deliberately not a support floor. `commandMinimumSupported` says an older
98
98
  * client is BROKEN for a command and forces an update; this says only that
99
99
  * an older client never had the command at all. That difference matters in
100
- * both directions: introducing a command must not auto-update anyone, and a
101
- * support floor may not exceed the version being released, while an
102
- * introduction version is a fact about an already-published release.
100
+ * both directions: introducing a command must not independently require an
101
+ * update, and a support floor may not exceed the version being released,
102
+ * while an introduction version is a fact about an already-published
103
+ * release. The general default-on freshness policy may still update an older
104
+ * installed CLI.
103
105
  *
104
106
  * It exists because agent skills are served by the backend and re-synced on
105
107
  * nearly every CLI invocation, while the CLI stays pinned. A release that
@@ -124,8 +126,8 @@ export type SdkSupportPolicy = {
124
126
  directToolsCompatibilityVersions?: readonly string[];
125
127
  /**
126
128
  * Diagnostic freshness threshold reported by `/api/v2/sdk/compat`.
127
- * Stale-but-supported CLIs warn, but self-update is reserved for unsupported
128
- * versions and rollback-directed updates.
129
+ * Ownership-aware installed CLIs auto-update when older; this value remains
130
+ * metadata for clients and operational diagnostics.
129
131
  */
130
132
  autoUpdatePatchLag?: number;
131
133
  };
@@ -142,6 +144,10 @@ export type SdkRelease = {
142
144
  * in its existing update message so older installed CLIs can print it too.
143
145
  */
144
146
  updateSummary?: string;
147
+ /** Capabilities stamped into the published package's `deepline` metadata. */
148
+ packageCapabilities: {
149
+ updatePreferences: 1;
150
+ };
145
151
  /** Named compatibility policies. This is the only authored contract policy. */
146
152
  contracts: DeeplineContractPolicy;
147
153
  /** Public support policy reported by `/api/v2/sdk/compat`. */
@@ -192,9 +198,13 @@ export const SDK_RELEASE = {
192
198
  // 0.3.0 introduces raw-v2: complete scrubbed provider responses are
193
199
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
194
200
  // getters keep their established compatibility behavior.
195
- version: '0.3.31',
201
+ // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
+ version: '0.3.33',
196
203
  updateSummary:
197
- 'New raw-v2 tool responses preserve complete scrubbed provider envelopes at toolResponse.rawV2, including JSON:API included resources, links, cursors, and totals. Existing toolResponse.raw and declared getters keep working unchanged.',
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
+ packageCapabilities: {
206
+ updatePreferences: 1,
207
+ },
198
208
  contracts: {
199
209
  api: {
200
210
  name: 'sdk-http-api',
@@ -221,7 +231,7 @@ export const SDK_RELEASE = {
221
231
  },
222
232
  supportPolicy: {
223
233
  minimumSupported: '0.1.53',
224
- deprecatedBelow: '0.1.219',
234
+ deprecatedBelow: '0.3.1',
225
235
  commandIntroducedIn: [
226
236
  {
227
237
  command: 'notifications',
@@ -49,7 +49,10 @@ import {
49
49
  PLAY_RUNTIME_TEST_FAULT_HEADER,
50
50
  recognizedRuntimeTestFaultCount,
51
51
  } from '@shared_libs/play-runtime/test-runtime-seams';
52
- import { vercelProtectionBypassHeaders } from '@shared_libs/play-runtime/vercel-protection';
52
+ import {
53
+ isVercelSecurityCheckpointResponse,
54
+ vercelProtectionBypassHeaders,
55
+ } from '@shared_libs/play-runtime/vercel-protection';
53
56
  import type { RuntimeReceiptAction } from '@shared_libs/play-runtime/runtime-actions';
54
57
  import { RUNTIME_CAPACITY_POLICY } from '@shared_libs/play-runtime/runtime-capacity-policy';
55
58
  import { RUNTIME_RELIABILITY_POLICY } from '@shared_libs/play-runtime/runtime-reliability-policy';
@@ -478,10 +481,20 @@ function isRetryableAppRuntimeResponse(input: {
478
481
  action: RuntimeApiRequest['action'];
479
482
  status: number;
480
483
  body: string;
484
+ contentType: string | null;
481
485
  }): boolean {
482
486
  if (!isRetryableAppRuntimeAction(input.action)) {
483
487
  return false;
484
488
  }
489
+ // A WAF checkpoint is not a statement from the app about the idempotent
490
+ // projection. Do not make every 403 retryable: only this exact Vercel HTML
491
+ // response on a replay-safe action is an intermediary failure.
492
+ if (
493
+ input.action === 'append_run_events' &&
494
+ isVercelSecurityCheckpointResponse(input)
495
+ ) {
496
+ return true;
497
+ }
485
498
  // The app runtime may explicitly classify an otherwise-client-error status
486
499
  // as transient. Keep the structured delivery contract authoritative rather
487
500
  // than reducing every 4xx to permanent at the worker boundary.
@@ -1273,6 +1286,7 @@ async function postAppRuntimeApi<TResponse>(
1273
1286
  action: body.action,
1274
1287
  status: response.status,
1275
1288
  body: responseText,
1289
+ contentType: response.headers.get('content-type'),
1276
1290
  })
1277
1291
  ) {
1278
1292
  const retryDelayMs = Math.max(
@@ -1299,6 +1313,7 @@ async function postAppRuntimeApi<TResponse>(
1299
1313
  action: body.action,
1300
1314
  status: response.status,
1301
1315
  body: responseText,
1316
+ contentType: response.headers.get('content-type'),
1302
1317
  })
1303
1318
  ? 'retryable_http'
1304
1319
  : 'http_error',
@@ -1328,6 +1343,7 @@ async function postAppRuntimeApi<TResponse>(
1328
1343
  action: body.action,
1329
1344
  status: response.status,
1330
1345
  body: responseText,
1346
+ contentType: response.headers.get('content-type'),
1331
1347
  }),
1332
1348
  detail: summarizeAppRuntimeErrorBody(responseText),
1333
1349
  boundaryLabel,
@@ -1,6 +1,7 @@
1
1
  type FetchLike = typeof fetch;
2
2
 
3
3
  const vercelProtectionCookieCache = new Map<string, Promise<string | null>>();
4
+ const VERCEL_PROTECTION_COOKIE_PREFLIGHT_TIMEOUT_MS = 5_000;
4
5
 
5
6
  async function awaitWithSignal<T>(
6
7
  promise: Promise<T>,
@@ -45,6 +46,25 @@ export function vercelProtectionBypassHeader(
45
46
  return normalized ? { 'x-vercel-protection-bypass': normalized } : {};
46
47
  }
47
48
 
49
+ /**
50
+ * A Vercel Firewall checkpoint is a transport/intermediary response, not an
51
+ * application authorization decision. Keep this deliberately narrow: callers
52
+ * still decide which replay-safe internal action may retry it.
53
+ */
54
+ export function isVercelSecurityCheckpointResponse(input: {
55
+ status: number;
56
+ contentType: string | null | undefined;
57
+ body: string;
58
+ }): boolean {
59
+ if (input.status !== 403) return false;
60
+ if (!/^text\/html(?:\s*;|$)/i.test(input.contentType?.trim() ?? '')) {
61
+ return false;
62
+ }
63
+ return /<title\b[^>]*>\s*Vercel Security Checkpoint\s*<\/title>/i.test(
64
+ input.body,
65
+ );
66
+ }
67
+
48
68
  function setCookieHeaders(headers: Headers): string[] {
49
69
  const getter = (headers as Headers & { getSetCookie?: () => string[] })
50
70
  .getSetCookie;
@@ -75,14 +95,27 @@ export async function resolveVercelProtectionBypassCookie(input: {
75
95
 
76
96
  const cacheKey = `${baseUrl}\n${token}`;
77
97
  const cached = vercelProtectionCookieCache.get(cacheKey);
78
- if (cached) return await awaitWithSignal(cached, input.signal);
98
+ if (cached) {
99
+ try {
100
+ return await awaitWithSignal(cached, input.signal);
101
+ } catch (error) {
102
+ if (input.signal?.aborted) throw input.signal.reason ?? error;
103
+ // A concurrent caller must retain the same header-only fallback as the
104
+ // caller that created the shared preflight promise.
105
+ return null;
106
+ }
107
+ }
79
108
 
80
109
  const promise = (async () => {
81
110
  const url = new URL(`${baseUrl}/api/v2/health`);
82
111
  addVercelProtectionBypassSearchParams(url, token);
112
+ // Keep the cache independent from an individual outbox lease. A caller
113
+ // may stop waiting, but the shared bootstrap is always bounded and either
114
+ // supplies a reusable cookie or evicts itself for a later retry.
83
115
  const response = await (input.fetchImpl ?? fetch)(url.toString(), {
84
116
  headers: { 'x-vercel-protection-bypass': token },
85
- }).catch(() => null);
117
+ signal: AbortSignal.timeout(VERCEL_PROTECTION_COOKIE_PREFLIGHT_TIMEOUT_MS),
118
+ });
86
119
  return response ? cookieHeaderFromSetCookie(response.headers) : null;
87
120
  })();
88
121
  vercelProtectionCookieCache.set(cacheKey, promise);
@@ -91,7 +124,15 @@ export async function resolveVercelProtectionBypassCookie(input: {
91
124
  vercelProtectionCookieCache.delete(cacheKey);
92
125
  }
93
126
  });
94
- return await awaitWithSignal(promise, input.signal);
127
+ try {
128
+ return await awaitWithSignal(promise, input.signal);
129
+ } catch (error) {
130
+ if (input.signal?.aborted) throw input.signal.reason ?? error;
131
+ // The header-only bypass remains useful when cookie bootstrap is briefly
132
+ // unavailable; do not turn a bounded preflight failure into a permanent
133
+ // delivery failure. The rejection handler above evicts this cache entry.
134
+ return null;
135
+ }
95
136
  }
96
137
 
97
138
  export async function vercelProtectionBypassHeaders(input: {