deepline 0.1.132 → 0.1.134

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.
@@ -49,6 +49,11 @@ import {
49
49
  type GovernanceSnapshot,
50
50
  type PlayExecutionGovernor,
51
51
  } from '../../../shared_libs/play-runtime/governor/governor';
52
+ import {
53
+ CTX_FETCH_EGRESS_PROVIDER,
54
+ CTX_FETCH_EGRESS_TOOL_ID,
55
+ resolveBuiltinPacing,
56
+ } from '../../../shared_libs/play-runtime/builtin-pacing';
52
57
  import {
53
58
  CoordinatorRateStateBackend,
54
59
  type CoordinatorRatePort,
@@ -165,6 +170,7 @@ import { safePublicFetch } from '../../../shared_libs/security/safe-fetch';
165
170
  import {
166
171
  assertPublicHttpUrl,
167
172
  isIpAddressLiteral,
173
+ normalizeUrlHostname,
168
174
  UnsafeOutboundUrlError,
169
175
  } from '../../../shared_libs/security/outbound-url-policy';
170
176
  import type {
@@ -475,6 +481,7 @@ async function probeHarnessOnce(
475
481
  */
476
482
  const RUNTIME_API_TIMEOUT_MS = 30_000;
477
483
  const RUNTIME_API_PLAY_RUN_TIMEOUT_MS = 75_000;
484
+ const RUNTIME_API_EGRESS_FETCH_TIMEOUT_MS = 180_000;
478
485
  const RUNTIME_API_INTEGRATION_EXECUTE_TIMEOUT_MS = 180_000;
479
486
  const RUNTIME_API_RETRY_DELAYS_MS = [
480
487
  250, 750, 1500, 3000, 5000, 10000,
@@ -488,9 +495,11 @@ async function fetchRuntimeApi(
488
495
  const timeoutMs =
489
496
  path === '/api/v2/plays/run'
490
497
  ? RUNTIME_API_PLAY_RUN_TIMEOUT_MS
491
- : /^\/api\/v2\/integrations\/[^/]+\/execute$/.test(path)
492
- ? RUNTIME_API_INTEGRATION_EXECUTE_TIMEOUT_MS
493
- : RUNTIME_API_TIMEOUT_MS;
498
+ : path === '/api/v2/plays/internal/egress-fetch'
499
+ ? RUNTIME_API_EGRESS_FETCH_TIMEOUT_MS
500
+ : /^\/api\/v2\/integrations\/[^/]+\/execute$/.test(path)
501
+ ? RUNTIME_API_INTEGRATION_EXECUTE_TIMEOUT_MS
502
+ : RUNTIME_API_TIMEOUT_MS;
494
503
  const controller = new AbortController();
495
504
  let timeout: ReturnType<typeof setTimeout> | null = null;
496
505
  const timeoutPromise = new Promise<never>((_, reject) => {
@@ -2313,6 +2322,20 @@ type WorkerFetchResponse = {
2313
2322
  json: unknown | null;
2314
2323
  };
2315
2324
 
2325
+ type WorkerFetchBodyDescriptor =
2326
+ | { kind: 'none' }
2327
+ | { kind: 'string'; value: string }
2328
+ | { kind: 'urlSearchParams'; value: string };
2329
+
2330
+ type RuntimeEgressFetchPayload = {
2331
+ url: string;
2332
+ method: string;
2333
+ headers: Record<string, string>;
2334
+ body: WorkerFetchBodyDescriptor;
2335
+ redirect?: RequestInit['redirect'];
2336
+ sensitiveHeaders: string[];
2337
+ };
2338
+
2316
2339
  function normalizeFetchHeaders(
2317
2340
  headers: RequestInit['headers'],
2318
2341
  ): Record<string, string> {
@@ -2344,6 +2367,19 @@ function fetchBodyIdentity(body: RequestInit['body']): string | null {
2344
2367
  );
2345
2368
  }
2346
2369
 
2370
+ function fetchBodyDescriptor(
2371
+ body: RequestInit['body'],
2372
+ ): WorkerFetchBodyDescriptor {
2373
+ if (body === undefined || body === null) return { kind: 'none' };
2374
+ if (typeof body === 'string') return { kind: 'string', value: body };
2375
+ if (body instanceof URLSearchParams) {
2376
+ return { kind: 'urlSearchParams', value: body.toString() };
2377
+ }
2378
+ throw new Error(
2379
+ 'ctx.fetch(...) in the Workers backend only supports string or URLSearchParams request bodies for egress.',
2380
+ );
2381
+ }
2382
+
2347
2383
  function parseFetchJsonOrNull(bodyText: string): unknown | null {
2348
2384
  if (!bodyText.trim()) return null;
2349
2385
  try {
@@ -2371,7 +2407,7 @@ async function safeWorkerPublicFetch(
2371
2407
  !allowedOrigins.has(url.origin)
2372
2408
  ) {
2373
2409
  throw new UnsafeOutboundUrlError(
2374
- 'workers_edge ctx.fetch requires a public IP literal target or Deepline runtime origin. Use a Deepline integration tool for other hostname URLs.',
2410
+ 'workers_edge direct fetch is reserved for public IP literals and Deepline runtime origins. Public hostnames must go through the Deepline egress endpoint with safeOutboundFetch.',
2375
2411
  );
2376
2412
  }
2377
2413
  return fetch(url, nextInit);
@@ -2379,6 +2415,121 @@ async function safeWorkerPublicFetch(
2379
2415
  });
2380
2416
  }
2381
2417
 
2418
+ function isDeeplineControlledWorkerFetchHost(hostname: string): boolean {
2419
+ const normalized = normalizeUrlHostname(hostname);
2420
+ return (
2421
+ normalized === 'deepline.com' ||
2422
+ normalized.endsWith('.deepline.com') ||
2423
+ normalized === 'deeplinedeveloper.com' ||
2424
+ normalized.endsWith('.deeplinedeveloper.com')
2425
+ );
2426
+ }
2427
+
2428
+ function shouldUseRuntimeEgressFetch(
2429
+ input: string | URL,
2430
+ allowedOrigins: Iterable<string>,
2431
+ ): boolean {
2432
+ const url = assertPublicHttpUrl(input);
2433
+ const allowedOriginSet = new Set(allowedOrigins);
2434
+ if (allowedOriginSet.has(url.origin)) return false;
2435
+ if (isIpAddressLiteral(url.hostname)) return false;
2436
+ if (isDeeplineControlledWorkerFetchHost(url.hostname)) {
2437
+ throw new UnsafeOutboundUrlError(
2438
+ 'ctx.fetch cannot call a non-runtime Deepline-owned origin from workers_edge. Use the run runtime origin or a Deepline integration tool.',
2439
+ );
2440
+ }
2441
+ return true;
2442
+ }
2443
+
2444
+ function shouldPaceWorkerEgressFetch(
2445
+ input: string | URL,
2446
+ allowedOrigins: Iterable<string>,
2447
+ ): boolean {
2448
+ const url = assertPublicHttpUrl(input);
2449
+ return !new Set(allowedOrigins).has(url.origin);
2450
+ }
2451
+
2452
+ function isWorkerFetchResponse(value: unknown): value is WorkerFetchResponse {
2453
+ if (!isRecord(value)) return false;
2454
+ return (
2455
+ typeof value.ok === 'boolean' &&
2456
+ typeof value.status === 'number' &&
2457
+ typeof value.statusText === 'string' &&
2458
+ typeof value.url === 'string' &&
2459
+ isRecord(value.headers) &&
2460
+ Object.values(value.headers).every((entry) => typeof entry === 'string') &&
2461
+ typeof value.bodyText === 'string' &&
2462
+ 'json' in value
2463
+ );
2464
+ }
2465
+
2466
+ async function postRuntimeEgressFetch(
2467
+ req: RunRequest,
2468
+ payload: RuntimeEgressFetchPayload,
2469
+ onProviderBackpressure?: (retryAfterMs: number) => void,
2470
+ onRetryAttempt?: () => void,
2471
+ ): Promise<WorkerFetchResponse> {
2472
+ let lastError: Error | null = null;
2473
+ for (
2474
+ let attempt = 1;
2475
+ attempt <= WORKER_TOOL_RATE_LIMIT_MAX_ATTEMPTS;
2476
+ attempt += 1
2477
+ ) {
2478
+ const response = await fetchRuntimeApi(
2479
+ req.baseUrl,
2480
+ '/api/v2/plays/internal/egress-fetch',
2481
+ {
2482
+ method: 'POST',
2483
+ headers: {
2484
+ authorization: `Bearer ${req.executorToken}`,
2485
+ 'content-type': 'application/json',
2486
+ },
2487
+ body: JSON.stringify(payload),
2488
+ },
2489
+ );
2490
+ const bodyText = await response.text();
2491
+ let parsed: unknown = null;
2492
+ try {
2493
+ parsed = bodyText.trim() ? JSON.parse(bodyText) : null;
2494
+ } catch {
2495
+ parsed = null;
2496
+ }
2497
+ if (response.ok) {
2498
+ if (!isWorkerFetchResponse(parsed)) {
2499
+ throw new Error(
2500
+ 'ctx.fetch egress returned an invalid response payload.',
2501
+ );
2502
+ }
2503
+ return parsed;
2504
+ }
2505
+
2506
+ const message =
2507
+ isRecord(parsed) && typeof parsed.error === 'string'
2508
+ ? parsed.error
2509
+ : bodyText || `HTTP ${response.status}`;
2510
+ lastError = new Error(`ctx.fetch egress failed: ${message}`);
2511
+ if (response.status !== 429) {
2512
+ throw lastError;
2513
+ }
2514
+
2515
+ const retryAfterSeconds = Number(response.headers.get('retry-after'));
2516
+ const retryAfterMs =
2517
+ Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
2518
+ ? Math.ceil(retryAfterSeconds * 1000)
2519
+ : 1_000;
2520
+ onProviderBackpressure?.(retryAfterMs);
2521
+ if (attempt >= WORKER_TOOL_RATE_LIMIT_MAX_ATTEMPTS) {
2522
+ throw lastError;
2523
+ }
2524
+ onRetryAttempt?.();
2525
+ await sleepWorkerMs(
2526
+ Math.min(5_000, Math.max(retryAfterMs, attempt * 1000)),
2527
+ );
2528
+ }
2529
+
2530
+ throw lastError ?? new Error('ctx.fetch egress failed before execution.');
2531
+ }
2532
+
2382
2533
  function normalizeAllowedWorkerFetchOrigin(rawUrl: string): string | null {
2383
2534
  try {
2384
2535
  return assertPublicHttpUrl(rawUrl).origin;
@@ -3172,6 +3323,8 @@ function createWorkerPacingResolver(req: RunRequest): WorkerPacingResolver {
3172
3323
  return (toolId: string) => {
3173
3324
  const normalized = String(toolId || '').trim();
3174
3325
  if (!normalized) return Promise.resolve(null);
3326
+ const builtin = resolveBuiltinPacing(normalized);
3327
+ if (builtin) return Promise.resolve(builtin);
3175
3328
  const cached = cache.get(normalized);
3176
3329
  if (cached) return cached;
3177
3330
  const promise = (async () => {
@@ -5406,26 +5559,77 @@ function createMinimalWorkerCtx(
5406
5559
  ...normalizeFetchHeaders(init.headers),
5407
5560
  ...secretHeaders,
5408
5561
  };
5409
- const fetchInit = { ...init, headers };
5410
- delete fetchInit.auth;
5411
- const response = await safeWorkerPublicFetch(url, fetchInit, {
5412
- allowedOrigins: getAllowedWorkerFetchOrigins(req),
5413
- sensitiveHeaders: Object.keys(secretHeaderMarkers),
5414
- });
5415
- assertNotAborted(abortSignal);
5416
- const bodyText = await response.text();
5417
- const redactedBodyText = secretRedactor.redactString(bodyText);
5418
- return {
5419
- ok: response.ok,
5420
- status: response.status,
5421
- statusText: response.statusText,
5422
- url: response.url,
5423
- headers: secretRedactor.redact(
5424
- Object.fromEntries(response.headers.entries()),
5425
- ) as Record<string, string>,
5426
- bodyText: redactedBodyText,
5427
- json: secretRedactor.redact(parseFetchJsonOrNull(bodyText)),
5428
- };
5562
+ const allowedOrigins = getAllowedWorkerFetchOrigins(req);
5563
+ const useRuntimeEgressFetch = shouldUseRuntimeEgressFetch(
5564
+ url,
5565
+ allowedOrigins,
5566
+ );
5567
+ const egressSlot = shouldPaceWorkerEgressFetch(url, allowedOrigins)
5568
+ ? await governor.acquireToolSlot(CTX_FETCH_EGRESS_TOOL_ID, {
5569
+ signal: abortSignal,
5570
+ })
5571
+ : null;
5572
+ try {
5573
+ if (useRuntimeEgressFetch) {
5574
+ // Customer-authored plays are allowed to call arbitrary public HTTP(S)
5575
+ // endpoints. The thing we must not do is let an isolate perform DNS
5576
+ // resolution for attacker-controlled hostnames and then trust only
5577
+ // pre-fetch URL validation: the original pentest issue was exactly
5578
+ // redirect/DNS-rebinding SSRF into private metadata or Deepline
5579
+ // runtime addresses. Cloudflare Worker fetch does not expose a
5580
+ // connect-time DNS validation hook, so arbitrary hostnames go through
5581
+ // the Deepline Node egress broker, which uses safeOutboundFetch for
5582
+ // every connection and redirect.
5583
+ //
5584
+ // The egressSlot is acquired before the broker call: ctx.fetch and
5585
+ // generic_http_request share the same generic_http pacing bucket, so
5586
+ // switching syntax cannot bypass arbitrary-HTTP egress limits.
5587
+ // Direct Worker fetch below is still limited to runtime origins and
5588
+ // IP literals; public IP literals also consume this slot because
5589
+ // they are customer egress.
5590
+ const egressResponse = await postRuntimeEgressFetch(
5591
+ req,
5592
+ {
5593
+ url,
5594
+ method,
5595
+ headers,
5596
+ body: fetchBodyDescriptor(init.body),
5597
+ redirect: init.redirect,
5598
+ sensitiveHeaders: Object.keys(secretHeaderMarkers),
5599
+ },
5600
+ (retryAfterMs) =>
5601
+ governor.reportProviderBackpressure({
5602
+ provider: CTX_FETCH_EGRESS_PROVIDER,
5603
+ retryAfterMs,
5604
+ }),
5605
+ () => governor.chargeBudget('retry'),
5606
+ );
5607
+ assertNotAborted(abortSignal);
5608
+ return secretRedactor.redact(egressResponse);
5609
+ }
5610
+ const fetchInit = { ...init, headers };
5611
+ delete fetchInit.auth;
5612
+ const response = await safeWorkerPublicFetch(url, fetchInit, {
5613
+ allowedOrigins,
5614
+ sensitiveHeaders: Object.keys(secretHeaderMarkers),
5615
+ });
5616
+ assertNotAborted(abortSignal);
5617
+ const bodyText = await response.text();
5618
+ const redactedBodyText = secretRedactor.redactString(bodyText);
5619
+ return {
5620
+ ok: response.ok,
5621
+ status: response.status,
5622
+ statusText: response.statusText,
5623
+ url: response.url,
5624
+ headers: secretRedactor.redact(
5625
+ Object.fromEntries(response.headers.entries()),
5626
+ ) as Record<string, string>,
5627
+ bodyText: redactedBodyText,
5628
+ json: secretRedactor.redact(parseFetchJsonOrNull(bodyText)),
5629
+ };
5630
+ } finally {
5631
+ egressSlot?.release();
5632
+ }
5429
5633
  });
5430
5634
  },
5431
5635
  secrets: {
@@ -101,10 +101,10 @@ export const SDK_RELEASE = {
101
101
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
102
102
  // the SDK enrich generator's one-second stale policy.
103
103
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
104
- version: '0.1.132',
104
+ version: '0.1.134',
105
105
  apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
106
106
  supportPolicy: {
107
- latest: '0.1.132',
107
+ latest: '0.1.134',
108
108
  minimumSupported: '0.1.53',
109
109
  deprecatedBelow: '0.1.53',
110
110
  commandMinimumSupported: [
@@ -0,0 +1,48 @@
1
+ import type { PacingRule } from './governor/rate-state-backend';
2
+
3
+ export interface BuiltinPacingPolicy {
4
+ readonly provider: string;
5
+ readonly rules: readonly PacingRule[];
6
+ }
7
+
8
+ /**
9
+ * Shared arbitrary-HTTP egress lane.
10
+ *
11
+ * Deepline exposes a first-class `generic_http_request` integration tool, and
12
+ * play authors also have the more ergonomic `ctx.fetch(...)` primitive. Those
13
+ * are two user-facing spellings of the same risk: customer-controlled outbound
14
+ * HTTP to arbitrary public endpoints.
15
+ *
16
+ * Do not create a separate `ctx.fetch` provider bucket. Both surfaces must pace
17
+ * through the existing `generic_http` provider identity so a play cannot bypass
18
+ * generic HTTP limits by switching syntax.
19
+ */
20
+ export const GENERIC_HTTP_REQUEST_TOOL_ID = 'generic_http_request';
21
+ export const GENERIC_HTTP_PROVIDER = 'generic_http';
22
+ export const GENERIC_HTTP_PROVIDER_SHARED_RULE_ID =
23
+ 'generic_http:provider_shared';
24
+ export const GENERIC_HTTP_PROVIDER_SHARED_REQUESTS_PER_SECOND = 25;
25
+
26
+ export const CTX_FETCH_EGRESS_TOOL_ID = GENERIC_HTTP_REQUEST_TOOL_ID;
27
+ export const CTX_FETCH_EGRESS_PROVIDER = GENERIC_HTTP_PROVIDER;
28
+ export const CTX_FETCH_EGRESS_PACING_RULE = {
29
+ ruleId: GENERIC_HTTP_PROVIDER_SHARED_RULE_ID,
30
+ requestsPerWindow: GENERIC_HTTP_PROVIDER_SHARED_REQUESTS_PER_SECOND,
31
+ windowMs: 1000,
32
+ maxConcurrency: null,
33
+ } satisfies PacingRule;
34
+
35
+ export const CTX_FETCH_EGRESS_PACING = {
36
+ provider: CTX_FETCH_EGRESS_PROVIDER,
37
+ rules: [CTX_FETCH_EGRESS_PACING_RULE],
38
+ } satisfies BuiltinPacingPolicy;
39
+
40
+ export function resolveBuiltinPacing(
41
+ toolId: string,
42
+ ): { provider: string; rules: PacingRule[] } | null {
43
+ if (toolId !== CTX_FETCH_EGRESS_TOOL_ID) return null;
44
+ return {
45
+ provider: CTX_FETCH_EGRESS_PACING.provider,
46
+ rules: [...CTX_FETCH_EGRESS_PACING.rules],
47
+ };
48
+ }
@@ -35,6 +35,10 @@ import {
35
35
  type PacingResolver,
36
36
  type PlayExecutionGovernor,
37
37
  } from './governor/governor';
38
+ import {
39
+ CTX_FETCH_EGRESS_TOOL_ID,
40
+ resolveBuiltinPacing,
41
+ } from './builtin-pacing';
38
42
  import { InMemoryRateStateBackend } from './governor/in-memory-rate-state-backend';
39
43
  import type { PacingRule } from './governor/rate-state-backend';
40
44
  import {
@@ -642,6 +646,8 @@ function createPacingResolver(
642
646
  getToolQueueHints: ContextOptions['getToolQueueHints'],
643
647
  ): PacingResolver {
644
648
  return async (toolId: string) => {
649
+ const builtin = resolveBuiltinPacing(toolId);
650
+ if (builtin) return builtin;
645
651
  const hints = getToolQueueHints ? await getToolQueueHints(toolId) : [];
646
652
  if (hints.length === 0) {
647
653
  return null;
@@ -4147,71 +4153,89 @@ export class PlayContextImpl {
4147
4153
  }
4148
4154
  }
4149
4155
 
4156
+ // ctx.fetch is arbitrary customer-directed egress. Pace it through
4157
+ // the same generic_http_request lane as the explicit Generic HTTP
4158
+ // integration tool so switching syntax cannot bypass per-org limits.
4159
+ const egressSlot = await this.governor.acquireToolSlot(
4160
+ CTX_FETCH_EGRESS_TOOL_ID,
4161
+ );
4150
4162
  let response: Response | null = null;
4151
- const canRetryTransport = ['GET', 'HEAD', 'OPTIONS'].includes(method);
4152
- const { safePublicFetch } = await loadSafeFetch();
4153
- for (
4154
- let attempt = 1;
4155
- attempt <= FETCH_TRANSPORT_MAX_ATTEMPTS;
4156
- attempt += 1
4157
- ) {
4158
- try {
4159
- response = await safePublicFetch(url, fetchInit, {
4160
- fetchImpl: this.#options.fetchImpl,
4161
- sensitiveHeaders: Object.keys(secretHeaderMarkers),
4162
- });
4163
- break;
4164
- } catch (error) {
4165
- if (isUnsafeOutboundUrlError(error)) {
4166
- throw error;
4167
- }
4168
- const message =
4169
- error instanceof Error ? error.message : String(error);
4170
- if (canRetryTransport && attempt < FETCH_TRANSPORT_MAX_ATTEMPTS) {
4171
- this.log(
4172
- `ctx.fetch(${method} ${url}) transport failed on attempt ${attempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}; retrying: ${message}`,
4173
- );
4174
- await new Promise((resolve) =>
4175
- setTimeout(resolve, FETCH_TRANSPORT_RETRY_DELAY_MS * attempt),
4163
+ try {
4164
+ const canRetryTransport = ['GET', 'HEAD', 'OPTIONS'].includes(
4165
+ method,
4166
+ );
4167
+ const { safePublicFetch } = await loadSafeFetch();
4168
+ for (
4169
+ let attempt = 1;
4170
+ attempt <= FETCH_TRANSPORT_MAX_ATTEMPTS;
4171
+ attempt += 1
4172
+ ) {
4173
+ try {
4174
+ response = await safePublicFetch(url, fetchInit, {
4175
+ fetchImpl: this.#options.fetchImpl,
4176
+ sensitiveHeaders: Object.keys(secretHeaderMarkers),
4177
+ });
4178
+ break;
4179
+ } catch (error) {
4180
+ if (isUnsafeOutboundUrlError(error)) {
4181
+ throw error;
4182
+ }
4183
+ const message =
4184
+ error instanceof Error ? error.message : String(error);
4185
+ if (
4186
+ canRetryTransport &&
4187
+ attempt < FETCH_TRANSPORT_MAX_ATTEMPTS
4188
+ ) {
4189
+ this.log(
4190
+ `ctx.fetch(${method} ${url}) transport failed on attempt ${attempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}; retrying: ${message}`,
4191
+ );
4192
+ await new Promise((resolve) =>
4193
+ setTimeout(
4194
+ resolve,
4195
+ FETCH_TRANSPORT_RETRY_DELAY_MS * attempt,
4196
+ ),
4197
+ );
4198
+ continue;
4199
+ }
4200
+ throw new Error(
4201
+ `ctx.fetch(${method} ${url}) failed on attempt ${attempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}: ${message}`,
4176
4202
  );
4177
- continue;
4178
4203
  }
4204
+ }
4205
+ if (!response) {
4179
4206
  throw new Error(
4180
- `ctx.fetch(${method} ${url}) failed on attempt ${attempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}: ${message}`,
4207
+ `ctx.fetch(${method} ${url}) failed before receiving a response.`,
4181
4208
  );
4182
4209
  }
4183
- }
4184
- if (!response) {
4185
- throw new Error(
4186
- `ctx.fetch(${method} ${url}) failed before receiving a response.`,
4187
- );
4188
- }
4189
- const bodyText = await response.text();
4190
- const redactedBodyText = this.secretRedactor.redactString(bodyText);
4191
- const output: PlayFetchResponse = {
4192
- ok: response.ok,
4193
- status: response.status,
4194
- statusText: response.statusText,
4195
- url: response.url,
4196
- headers: this.secretRedactor.redact(
4197
- Object.fromEntries(response.headers.entries()),
4198
- ) as Record<string, string>,
4199
- bodyText: redactedBodyText,
4200
- json: this.secretRedactor.redact(parseJsonOrNull(bodyText)),
4201
- };
4210
+ const bodyText = await response.text();
4211
+ const redactedBodyText = this.secretRedactor.redactString(bodyText);
4212
+ const output: PlayFetchResponse = {
4213
+ ok: response.ok,
4214
+ status: response.status,
4215
+ statusText: response.statusText,
4216
+ url: response.url,
4217
+ headers: this.secretRedactor.redact(
4218
+ Object.fromEntries(response.headers.entries()),
4219
+ ) as Record<string, string>,
4220
+ bodyText: redactedBodyText,
4221
+ json: this.secretRedactor.redact(parseJsonOrNull(bodyText)),
4222
+ };
4202
4223
 
4203
- this.checkpoint.resolvedBoundaries = {
4204
- ...(this.checkpoint.resolvedBoundaries ?? {}),
4205
- [boundaryId]: {
4206
- kind: 'fetch',
4207
- url,
4208
- method,
4209
- output,
4210
- completedAt: Date.now(),
4211
- },
4212
- };
4213
- this.#options.onBatchComplete?.(this.checkpoint);
4214
- return output;
4224
+ this.checkpoint.resolvedBoundaries = {
4225
+ ...(this.checkpoint.resolvedBoundaries ?? {}),
4226
+ [boundaryId]: {
4227
+ kind: 'fetch',
4228
+ url,
4229
+ method,
4230
+ output,
4231
+ completedAt: Date.now(),
4232
+ },
4233
+ };
4234
+ this.#options.onBatchComplete?.(this.checkpoint);
4235
+ return output;
4236
+ } finally {
4237
+ egressSlot.release();
4238
+ }
4215
4239
  },
4216
4240
  markSkipped: (output) => {
4217
4241
  this.log(
@@ -15,6 +15,7 @@ type NodeSafeFetchOptions = {
15
15
  maxRedirects?: number;
16
16
  maxResponseBytes?: number;
17
17
  sensitiveHeaders?: Iterable<string>;
18
+ validateUrl?: (url: URL) => void;
18
19
  };
19
20
 
20
21
  type PreparedBody = {
@@ -24,6 +25,24 @@ type PreparedBody = {
24
25
 
25
26
  export { UnsafeOutboundUrlError };
26
27
 
28
+ const NULL_BODY_STATUS_CODES = new Set([204, 205, 304]);
29
+ const TRANSPORT_OWNED_REQUEST_HEADERS = new Set([
30
+ 'connection',
31
+ 'content-length',
32
+ 'host',
33
+ 'keep-alive',
34
+ 'proxy-authenticate',
35
+ 'proxy-authorization',
36
+ 'te',
37
+ 'trailer',
38
+ 'transfer-encoding',
39
+ 'upgrade',
40
+ 'forwarded',
41
+ 'x-forwarded-for',
42
+ 'x-forwarded-host',
43
+ 'x-forwarded-proto',
44
+ ]);
45
+
27
46
  function cloneHeaders(headers: RequestInit['headers']): Headers {
28
47
  return new Headers(headers);
29
48
  }
@@ -32,14 +51,34 @@ function deleteHeader(headers: Headers, name: string) {
32
51
  if (headers.has(name)) headers.delete(name);
33
52
  }
34
53
 
54
+ function removeTransportOwnedHeaders(headers: Headers): void {
55
+ for (const header of TRANSPORT_OWNED_REQUEST_HEADERS) {
56
+ deleteHeader(headers, header);
57
+ }
58
+ }
59
+
60
+ function withContentLength(
61
+ body: Buffer | string | undefined,
62
+ headers: Headers,
63
+ ): PreparedBody {
64
+ removeTransportOwnedHeaders(headers);
65
+ if (body !== undefined) {
66
+ headers.set(
67
+ 'content-length',
68
+ String(typeof body === 'string' ? Buffer.byteLength(body) : body.length),
69
+ );
70
+ }
71
+ return { body, headers };
72
+ }
73
+
35
74
  async function prepareBody(init: RequestInit): Promise<PreparedBody> {
36
75
  const headers = cloneHeaders(init.headers);
37
76
  const body = init.body;
38
77
  if (body === undefined || body === null) {
39
- return { body: undefined, headers };
78
+ return withContentLength(undefined, headers);
40
79
  }
41
80
  if (typeof body === 'string') {
42
- return { body, headers };
81
+ return withContentLength(body, headers);
43
82
  }
44
83
  if (body instanceof URLSearchParams) {
45
84
  if (!headers.has('content-type')) {
@@ -48,19 +87,19 @@ async function prepareBody(init: RequestInit): Promise<PreparedBody> {
48
87
  'application/x-www-form-urlencoded;charset=UTF-8',
49
88
  );
50
89
  }
51
- return { body: body.toString(), headers };
90
+ return withContentLength(body.toString(), headers);
52
91
  }
53
92
  if (body instanceof ArrayBuffer) {
54
- return { body: Buffer.from(body), headers };
93
+ return withContentLength(Buffer.from(body), headers);
55
94
  }
56
95
  if (ArrayBuffer.isView(body)) {
57
- return {
58
- body: Buffer.from(body.buffer, body.byteOffset, body.byteLength),
96
+ return withContentLength(
97
+ Buffer.from(body.buffer, body.byteOffset, body.byteLength),
59
98
  headers,
60
- };
99
+ );
61
100
  }
62
101
  if (typeof Blob !== 'undefined' && body instanceof Blob) {
63
- return { body: Buffer.from(await body.arrayBuffer()), headers };
102
+ return withContentLength(Buffer.from(await body.arrayBuffer()), headers);
64
103
  }
65
104
 
66
105
  throw new TypeError(
@@ -80,6 +119,10 @@ function validateResolvedAddress(address: string) {
80
119
  }
81
120
  }
82
121
 
122
+ function responseMustNotHaveBody(method: string, status: number): boolean {
123
+ return method === 'HEAD' || NULL_BODY_STATUS_CODES.has(status);
124
+ }
125
+
83
126
  const safeLookup: LookupFunction = (hostname, options, callback) => {
84
127
  const lookupOptions: dns.LookupAllOptions = {
85
128
  all: true,
@@ -129,27 +172,29 @@ function createRequest(
129
172
  ): Promise<Response> {
130
173
  return new Promise((resolve, reject) => {
131
174
  const transport = url.protocol === 'https:' ? https : http;
175
+ const method = String(init.method ?? 'GET').toUpperCase();
132
176
  const request = transport.request(
133
177
  url,
134
178
  {
135
- method: init.method ?? 'GET',
179
+ method,
136
180
  headers: headersToRecord(prepared.headers),
137
181
  agent: false,
138
182
  lookup: safeLookup,
139
183
  signal: init.signal ?? undefined,
140
184
  },
141
185
  (response) => {
186
+ const status = response.statusCode ?? 0;
187
+ const noBodyResponse = responseMustNotHaveBody(method, status);
142
188
  const contentLength = Number(response.headers['content-length']);
143
189
  if (
190
+ !noBodyResponse &&
144
191
  maxResponseBytes !== undefined &&
145
192
  Number.isFinite(contentLength) &&
146
193
  contentLength > maxResponseBytes
147
194
  ) {
148
195
  response.resume();
149
196
  reject(
150
- new Error(
151
- `Response body exceeds ${maxResponseBytes} byte limit.`,
152
- ),
197
+ new Error(`Response body exceeds ${maxResponseBytes} byte limit.`),
153
198
  );
154
199
  return;
155
200
  }
@@ -170,16 +215,21 @@ function createRequest(
170
215
  );
171
216
  return;
172
217
  }
173
- chunks.push(buffer);
218
+ if (!noBodyResponse) {
219
+ chunks.push(buffer);
220
+ }
174
221
  });
175
222
  response.on('error', reject);
176
223
  response.on('end', () => {
177
224
  resolve(
178
- new Response(Buffer.concat(chunks), {
179
- status: response.statusCode ?? 0,
180
- statusText: response.statusMessage,
181
- headers: response.headers as HeadersInit,
182
- }),
225
+ new Response(
226
+ noBodyResponse ? null : Buffer.concat(chunks),
227
+ {
228
+ status,
229
+ statusText: response.statusMessage,
230
+ headers: response.headers as HeadersInit,
231
+ },
232
+ ),
183
233
  );
184
234
  });
185
235
  },
@@ -241,6 +291,7 @@ export async function safeOutboundFetch(
241
291
  const maxRedirects = options.maxRedirects ?? 10;
242
292
  const maxResponseBytes = options.maxResponseBytes;
243
293
  const sensitiveHeaders = options.sensitiveHeaders ?? [];
294
+ const validateUrl = options.validateUrl;
244
295
  let currentUrl = assertPublicHttpUrl(input);
245
296
  let currentInit: RequestInit = { ...init, redirect: 'manual' };
246
297
 
@@ -250,6 +301,7 @@ export async function safeOutboundFetch(
250
301
  redirectCount += 1
251
302
  ) {
252
303
  const hostname = normalizeUrlHostname(currentUrl.hostname);
304
+ validateUrl?.(currentUrl);
253
305
  if (isBlockedIpAddress(hostname)) {
254
306
  throw new UnsafeOutboundUrlError(
255
307
  `Target host "${hostname}" is not allowed.`,
package/dist/cli/index.js CHANGED
@@ -413,10 +413,10 @@ var SDK_RELEASE = {
413
413
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
414
414
  // the SDK enrich generator's one-second stale policy.
415
415
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
416
- version: "0.1.132",
416
+ version: "0.1.134",
417
417
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
418
418
  supportPolicy: {
419
- latest: "0.1.132",
419
+ latest: "0.1.134",
420
420
  minimumSupported: "0.1.53",
421
421
  deprecatedBelow: "0.1.53",
422
422
  commandMinimumSupported: [
@@ -18916,38 +18916,46 @@ function renderSecret(secret) {
18916
18916
  const scope = secret.scope === "play" && secret.playName ? `play:${secret.playName}` : secret.scope;
18917
18917
  return `${secret.name} (${scope}) - ${secret.status}${secret.hasValue ? ", set" : ", empty"}`;
18918
18918
  }
18919
- async function readHiddenLine(prompt) {
18920
- if (!import_node_process.stdin.isTTY || !import_node_process.stdout.isTTY) {
18919
+ async function readHiddenLine(prompt, streams = {}) {
18920
+ const inputStream = streams.input ?? import_node_process.stdin;
18921
+ const outputStream = streams.output ?? import_node_process.stdout;
18922
+ if (!inputStream.isTTY || !outputStream.isTTY) {
18921
18923
  throw new Error(
18922
18924
  "Secret values must be entered from an interactive TTY. Do not pipe, pass, or script secret values."
18923
18925
  );
18924
18926
  }
18925
- import_node_process.stdout.write(prompt);
18926
- const previousRawMode = import_node_process.stdin.isRaw;
18927
- if (typeof import_node_process.stdin.setRawMode === "function") import_node_process.stdin.setRawMode(true);
18927
+ outputStream.write(prompt);
18928
+ const previousRawMode = inputStream.isRaw;
18929
+ const wasPaused = typeof inputStream.isPaused === "function" ? inputStream.isPaused() : false;
18930
+ if (typeof inputStream.setRawMode === "function") {
18931
+ inputStream.setRawMode(true);
18932
+ }
18928
18933
  let value = "";
18929
- import_node_process.stdin.resume();
18934
+ inputStream.resume();
18930
18935
  return await new Promise((resolve13, reject) => {
18931
18936
  let settled = false;
18932
18937
  const cleanup = () => {
18933
- import_node_process.stdin.off("data", onData);
18934
- import_node_process.stdin.off("end", onEnd);
18935
- import_node_process.stdin.off("error", onError);
18936
- if (typeof import_node_process.stdin.setRawMode === "function") {
18937
- import_node_process.stdin.setRawMode(previousRawMode);
18938
+ inputStream.off("data", onData);
18939
+ inputStream.off("end", onEnd);
18940
+ inputStream.off("error", onError);
18941
+ if (typeof inputStream.setRawMode === "function") {
18942
+ inputStream.setRawMode(previousRawMode);
18943
+ }
18944
+ if (wasPaused) {
18945
+ inputStream.pause();
18938
18946
  }
18939
18947
  };
18940
18948
  const finish = (line) => {
18941
18949
  if (settled) return;
18942
18950
  settled = true;
18943
- import_node_process.stdout.write("\n");
18951
+ outputStream.write("\n");
18944
18952
  cleanup();
18945
18953
  resolve13(line);
18946
18954
  };
18947
18955
  const fail = (error) => {
18948
18956
  if (settled) return;
18949
18957
  settled = true;
18950
- import_node_process.stdout.write("\n");
18958
+ outputStream.write("\n");
18951
18959
  cleanup();
18952
18960
  reject(error);
18953
18961
  };
@@ -18979,9 +18987,9 @@ async function readHiddenLine(prompt) {
18979
18987
  };
18980
18988
  const onEnd = () => fail(new Error("Secret input ended before a value was entered."));
18981
18989
  const onError = (error) => fail(error);
18982
- import_node_process.stdin.on("data", onData);
18983
- import_node_process.stdin.once("end", onEnd);
18984
- import_node_process.stdin.once("error", onError);
18990
+ inputStream.on("data", onData);
18991
+ inputStream.once("end", onEnd);
18992
+ inputStream.once("error", onError);
18985
18993
  if (hiddenInputBuffer) {
18986
18994
  const buffered = hiddenInputBuffer;
18987
18995
  hiddenInputBuffer = "";
@@ -21798,6 +21806,23 @@ async function syncSdkSkillsIfNeeded(baseUrl, options = {}) {
21798
21806
  }
21799
21807
 
21800
21808
  // src/cli/commands/update.ts
21809
+ var NPM_SDK_INSTALL_COMMON_FLAGS = ["--no-audit", "--no-fund"];
21810
+ var NPM_SDK_GLOBAL_INSTALL_FLAGS = [
21811
+ "--no-audit",
21812
+ "--no-fund",
21813
+ "--allow-scripts=esbuild"
21814
+ ];
21815
+ var NPM_SDK_SIDECAR_PACKAGE_JSON = `${JSON.stringify(
21816
+ {
21817
+ private: true,
21818
+ allowScripts: {
21819
+ esbuild: true
21820
+ }
21821
+ },
21822
+ null,
21823
+ 2
21824
+ )}
21825
+ `;
21801
21826
  function posixShellQuote(value) {
21802
21827
  return `'${value.replace(/'/g, `'\\''`)}'`;
21803
21828
  }
@@ -21815,6 +21840,16 @@ function buildSourceUpdateCommand(sourceRoot) {
21815
21840
  const cdCommand = process.platform === "win32" ? `cd /d ${quotedRoot}` : `cd ${quotedRoot}`;
21816
21841
  return `${cdCommand} && git fetch origin main --tags && git merge --ff-only origin/main`;
21817
21842
  }
21843
+ function buildSidecarProjectConfigCommand(versionDir, nodeBin) {
21844
+ const script = [
21845
+ "const fs=require('node:fs');",
21846
+ "const path=require('node:path');",
21847
+ "const dir=process.argv[1];",
21848
+ "fs.mkdirSync(dir,{recursive:true});",
21849
+ `fs.writeFileSync(path.join(dir,'package.json'),${JSON.stringify(NPM_SDK_SIDECAR_PACKAGE_JSON)});`
21850
+ ].join("");
21851
+ return `${shellQuote4(nodeBin)} -e ${shellQuote4(script)} ${shellQuote4(versionDir)}`;
21852
+ }
21818
21853
  function sidecarStateDir(input2) {
21819
21854
  const scope = input2.env.DEEPLINE_CONFIG_SCOPE?.trim();
21820
21855
  if (!scope || scope.includes("/") || scope.includes("\\")) {
@@ -21851,7 +21886,8 @@ function resolvePythonSidecarUpdatePlan(options) {
21851
21886
  );
21852
21887
  const packageSpec = options.packageSpec || "deepline@latest";
21853
21888
  const npmCommand = "npm";
21854
- const manualCommand = `${npmCommand} install --prefix ${shellQuote4((0, import_node_path18.join)(stateDir, "versions", "<version>"))} --no-audit --no-fund ${shellQuote4(packageSpec)}`;
21889
+ const versionDir = (0, import_node_path18.join)(stateDir, "versions", "<version>");
21890
+ const manualCommand = `${buildSidecarProjectConfigCommand(versionDir, nodeBin)} && ${npmCommand} install --prefix ${shellQuote4(versionDir)} ${NPM_SDK_INSTALL_COMMON_FLAGS.map(shellQuote4).join(" ")} ${shellQuote4(packageSpec)}`;
21855
21891
  return {
21856
21892
  kind: "python-sidecar",
21857
21893
  stateDir,
@@ -21918,7 +21954,14 @@ function resolveUpdatePlan(options = {}) {
21918
21954
  const command = "npm";
21919
21955
  const packageSpec = options.packageSpec || "deepline@latest";
21920
21956
  const installPrefix = entrypoint ? inferNpmGlobalPrefixFromEntrypoint(entrypoint) : null;
21921
- const args = installPrefix ? ["install", "-g", "--prefix", installPrefix, packageSpec] : ["install", "-g", packageSpec];
21957
+ const args = installPrefix ? [
21958
+ "install",
21959
+ "-g",
21960
+ "--prefix",
21961
+ installPrefix,
21962
+ ...NPM_SDK_GLOBAL_INSTALL_FLAGS,
21963
+ packageSpec
21964
+ ] : ["install", "-g", ...NPM_SDK_GLOBAL_INSTALL_FLAGS, packageSpec];
21922
21965
  return {
21923
21966
  kind: "npm-global",
21924
21967
  command,
@@ -22093,7 +22136,7 @@ async function runPythonSidecarUpdatePlan(plan) {
22093
22136
  );
22094
22137
  (0, import_node_fs15.rmSync)(tempDir, { recursive: true, force: true });
22095
22138
  (0, import_node_fs15.mkdirSync)(tempDir, { recursive: true });
22096
- (0, import_node_fs15.writeFileSync)((0, import_node_path18.join)(tempDir, "package.json"), '{"private":true}\n', "utf8");
22139
+ (0, import_node_fs15.writeFileSync)((0, import_node_path18.join)(tempDir, "package.json"), NPM_SDK_SIDECAR_PACKAGE_JSON);
22097
22140
  const env = {
22098
22141
  ...process.env,
22099
22142
  PATH: `${(0, import_node_path18.dirname)(plan.nodeBin)}${process.platform === "win32" ? ";" : ":"}${process.env.PATH ?? ""}`
@@ -22104,8 +22147,7 @@ async function runPythonSidecarUpdatePlan(plan) {
22104
22147
  "install",
22105
22148
  "--prefix",
22106
22149
  tempDir,
22107
- "--no-audit",
22108
- "--no-fund",
22150
+ ...NPM_SDK_INSTALL_COMMON_FLAGS,
22109
22151
  plan.packageSpec
22110
22152
  ],
22111
22153
  env
@@ -22705,6 +22747,9 @@ function shouldDeferSkillsSyncForCommand() {
22705
22747
  const args = process.argv.slice(2);
22706
22748
  const command = args[0];
22707
22749
  const subcommand = args[1];
22750
+ if (command === "tools" && ["list", "search", "grep", "describe", "get"].includes(subcommand ?? "")) {
22751
+ return true;
22752
+ }
22708
22753
  return (command === "play" || command === "plays") && subcommand === "run" && args.includes("--json");
22709
22754
  }
22710
22755
  function isLegacyNoopInvocation() {
@@ -390,10 +390,10 @@ var SDK_RELEASE = {
390
390
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
391
391
  // the SDK enrich generator's one-second stale policy.
392
392
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
393
- version: "0.1.132",
393
+ version: "0.1.134",
394
394
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
395
395
  supportPolicy: {
396
- latest: "0.1.132",
396
+ latest: "0.1.134",
397
397
  minimumSupported: "0.1.53",
398
398
  deprecatedBelow: "0.1.53",
399
399
  commandMinimumSupported: [
@@ -18932,38 +18932,46 @@ function renderSecret(secret) {
18932
18932
  const scope = secret.scope === "play" && secret.playName ? `play:${secret.playName}` : secret.scope;
18933
18933
  return `${secret.name} (${scope}) - ${secret.status}${secret.hasValue ? ", set" : ", empty"}`;
18934
18934
  }
18935
- async function readHiddenLine(prompt) {
18936
- if (!input.isTTY || !output.isTTY) {
18935
+ async function readHiddenLine(prompt, streams = {}) {
18936
+ const inputStream = streams.input ?? input;
18937
+ const outputStream = streams.output ?? output;
18938
+ if (!inputStream.isTTY || !outputStream.isTTY) {
18937
18939
  throw new Error(
18938
18940
  "Secret values must be entered from an interactive TTY. Do not pipe, pass, or script secret values."
18939
18941
  );
18940
18942
  }
18941
- output.write(prompt);
18942
- const previousRawMode = input.isRaw;
18943
- if (typeof input.setRawMode === "function") input.setRawMode(true);
18943
+ outputStream.write(prompt);
18944
+ const previousRawMode = inputStream.isRaw;
18945
+ const wasPaused = typeof inputStream.isPaused === "function" ? inputStream.isPaused() : false;
18946
+ if (typeof inputStream.setRawMode === "function") {
18947
+ inputStream.setRawMode(true);
18948
+ }
18944
18949
  let value = "";
18945
- input.resume();
18950
+ inputStream.resume();
18946
18951
  return await new Promise((resolve13, reject) => {
18947
18952
  let settled = false;
18948
18953
  const cleanup = () => {
18949
- input.off("data", onData);
18950
- input.off("end", onEnd);
18951
- input.off("error", onError);
18952
- if (typeof input.setRawMode === "function") {
18953
- input.setRawMode(previousRawMode);
18954
+ inputStream.off("data", onData);
18955
+ inputStream.off("end", onEnd);
18956
+ inputStream.off("error", onError);
18957
+ if (typeof inputStream.setRawMode === "function") {
18958
+ inputStream.setRawMode(previousRawMode);
18959
+ }
18960
+ if (wasPaused) {
18961
+ inputStream.pause();
18954
18962
  }
18955
18963
  };
18956
18964
  const finish = (line) => {
18957
18965
  if (settled) return;
18958
18966
  settled = true;
18959
- output.write("\n");
18967
+ outputStream.write("\n");
18960
18968
  cleanup();
18961
18969
  resolve13(line);
18962
18970
  };
18963
18971
  const fail = (error) => {
18964
18972
  if (settled) return;
18965
18973
  settled = true;
18966
- output.write("\n");
18974
+ outputStream.write("\n");
18967
18975
  cleanup();
18968
18976
  reject(error);
18969
18977
  };
@@ -18995,9 +19003,9 @@ async function readHiddenLine(prompt) {
18995
19003
  };
18996
19004
  const onEnd = () => fail(new Error("Secret input ended before a value was entered."));
18997
19005
  const onError = (error) => fail(error);
18998
- input.on("data", onData);
18999
- input.once("end", onEnd);
19000
- input.once("error", onError);
19006
+ inputStream.on("data", onData);
19007
+ inputStream.once("end", onEnd);
19008
+ inputStream.once("error", onError);
19001
19009
  if (hiddenInputBuffer) {
19002
19010
  const buffered = hiddenInputBuffer;
19003
19011
  hiddenInputBuffer = "";
@@ -21829,6 +21837,23 @@ async function syncSdkSkillsIfNeeded(baseUrl, options = {}) {
21829
21837
  }
21830
21838
 
21831
21839
  // src/cli/commands/update.ts
21840
+ var NPM_SDK_INSTALL_COMMON_FLAGS = ["--no-audit", "--no-fund"];
21841
+ var NPM_SDK_GLOBAL_INSTALL_FLAGS = [
21842
+ "--no-audit",
21843
+ "--no-fund",
21844
+ "--allow-scripts=esbuild"
21845
+ ];
21846
+ var NPM_SDK_SIDECAR_PACKAGE_JSON = `${JSON.stringify(
21847
+ {
21848
+ private: true,
21849
+ allowScripts: {
21850
+ esbuild: true
21851
+ }
21852
+ },
21853
+ null,
21854
+ 2
21855
+ )}
21856
+ `;
21832
21857
  function posixShellQuote(value) {
21833
21858
  return `'${value.replace(/'/g, `'\\''`)}'`;
21834
21859
  }
@@ -21846,6 +21871,16 @@ function buildSourceUpdateCommand(sourceRoot) {
21846
21871
  const cdCommand = process.platform === "win32" ? `cd /d ${quotedRoot}` : `cd ${quotedRoot}`;
21847
21872
  return `${cdCommand} && git fetch origin main --tags && git merge --ff-only origin/main`;
21848
21873
  }
21874
+ function buildSidecarProjectConfigCommand(versionDir, nodeBin) {
21875
+ const script = [
21876
+ "const fs=require('node:fs');",
21877
+ "const path=require('node:path');",
21878
+ "const dir=process.argv[1];",
21879
+ "fs.mkdirSync(dir,{recursive:true});",
21880
+ `fs.writeFileSync(path.join(dir,'package.json'),${JSON.stringify(NPM_SDK_SIDECAR_PACKAGE_JSON)});`
21881
+ ].join("");
21882
+ return `${shellQuote4(nodeBin)} -e ${shellQuote4(script)} ${shellQuote4(versionDir)}`;
21883
+ }
21849
21884
  function sidecarStateDir(input2) {
21850
21885
  const scope = input2.env.DEEPLINE_CONFIG_SCOPE?.trim();
21851
21886
  if (!scope || scope.includes("/") || scope.includes("\\")) {
@@ -21882,7 +21917,8 @@ function resolvePythonSidecarUpdatePlan(options) {
21882
21917
  );
21883
21918
  const packageSpec = options.packageSpec || "deepline@latest";
21884
21919
  const npmCommand = "npm";
21885
- const manualCommand = `${npmCommand} install --prefix ${shellQuote4(join13(stateDir, "versions", "<version>"))} --no-audit --no-fund ${shellQuote4(packageSpec)}`;
21920
+ const versionDir = join13(stateDir, "versions", "<version>");
21921
+ const manualCommand = `${buildSidecarProjectConfigCommand(versionDir, nodeBin)} && ${npmCommand} install --prefix ${shellQuote4(versionDir)} ${NPM_SDK_INSTALL_COMMON_FLAGS.map(shellQuote4).join(" ")} ${shellQuote4(packageSpec)}`;
21886
21922
  return {
21887
21923
  kind: "python-sidecar",
21888
21924
  stateDir,
@@ -21949,7 +21985,14 @@ function resolveUpdatePlan(options = {}) {
21949
21985
  const command = "npm";
21950
21986
  const packageSpec = options.packageSpec || "deepline@latest";
21951
21987
  const installPrefix = entrypoint ? inferNpmGlobalPrefixFromEntrypoint(entrypoint) : null;
21952
- const args = installPrefix ? ["install", "-g", "--prefix", installPrefix, packageSpec] : ["install", "-g", packageSpec];
21988
+ const args = installPrefix ? [
21989
+ "install",
21990
+ "-g",
21991
+ "--prefix",
21992
+ installPrefix,
21993
+ ...NPM_SDK_GLOBAL_INSTALL_FLAGS,
21994
+ packageSpec
21995
+ ] : ["install", "-g", ...NPM_SDK_GLOBAL_INSTALL_FLAGS, packageSpec];
21953
21996
  return {
21954
21997
  kind: "npm-global",
21955
21998
  command,
@@ -22124,7 +22167,7 @@ async function runPythonSidecarUpdatePlan(plan) {
22124
22167
  );
22125
22168
  rmSync3(tempDir, { recursive: true, force: true });
22126
22169
  mkdirSync9(tempDir, { recursive: true });
22127
- writeFileSync14(join13(tempDir, "package.json"), '{"private":true}\n', "utf8");
22170
+ writeFileSync14(join13(tempDir, "package.json"), NPM_SDK_SIDECAR_PACKAGE_JSON);
22128
22171
  const env = {
22129
22172
  ...process.env,
22130
22173
  PATH: `${dirname11(plan.nodeBin)}${process.platform === "win32" ? ";" : ":"}${process.env.PATH ?? ""}`
@@ -22135,8 +22178,7 @@ async function runPythonSidecarUpdatePlan(plan) {
22135
22178
  "install",
22136
22179
  "--prefix",
22137
22180
  tempDir,
22138
- "--no-audit",
22139
- "--no-fund",
22181
+ ...NPM_SDK_INSTALL_COMMON_FLAGS,
22140
22182
  plan.packageSpec
22141
22183
  ],
22142
22184
  env
@@ -22736,6 +22778,9 @@ function shouldDeferSkillsSyncForCommand() {
22736
22778
  const args = process.argv.slice(2);
22737
22779
  const command = args[0];
22738
22780
  const subcommand = args[1];
22781
+ if (command === "tools" && ["list", "search", "grep", "describe", "get"].includes(subcommand ?? "")) {
22782
+ return true;
22783
+ }
22739
22784
  return (command === "play" || command === "plays") && subcommand === "run" && args.includes("--json");
22740
22785
  }
22741
22786
  function isLegacyNoopInvocation() {
package/dist/index.js CHANGED
@@ -284,10 +284,10 @@ var SDK_RELEASE = {
284
284
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
285
285
  // the SDK enrich generator's one-second stale policy.
286
286
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
287
- version: "0.1.132",
287
+ version: "0.1.134",
288
288
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
289
289
  supportPolicy: {
290
- latest: "0.1.132",
290
+ latest: "0.1.134",
291
291
  minimumSupported: "0.1.53",
292
292
  deprecatedBelow: "0.1.53",
293
293
  commandMinimumSupported: [
package/dist/index.mjs CHANGED
@@ -206,10 +206,10 @@ var SDK_RELEASE = {
206
206
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
207
207
  // the SDK enrich generator's one-second stale policy.
208
208
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
209
- version: "0.1.132",
209
+ version: "0.1.134",
210
210
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
211
211
  supportPolicy: {
212
- latest: "0.1.132",
212
+ latest: "0.1.134",
213
213
  minimumSupported: "0.1.53",
214
214
  deprecatedBelow: "0.1.53",
215
215
  commandMinimumSupported: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.132",
3
+ "version": "0.1.134",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {