@anthropic-ai/sdk 0.115.0 → 0.117.1

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.
Files changed (191) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/client.d.mts +3 -3
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +3 -3
  5. package/client.d.ts.map +1 -1
  6. package/client.js +1 -1
  7. package/client.js.map +1 -1
  8. package/client.mjs +1 -1
  9. package/client.mjs.map +1 -1
  10. package/internal/constants.d.mts.map +1 -1
  11. package/internal/constants.d.ts.map +1 -1
  12. package/internal/constants.js +0 -5
  13. package/internal/constants.js.map +1 -1
  14. package/internal/constants.mjs +0 -5
  15. package/internal/constants.mjs.map +1 -1
  16. package/lib/BetaMessageStream.d.mts.map +1 -1
  17. package/lib/BetaMessageStream.d.ts.map +1 -1
  18. package/lib/BetaMessageStream.js +12 -5
  19. package/lib/BetaMessageStream.js.map +1 -1
  20. package/lib/BetaMessageStream.mjs +12 -5
  21. package/lib/BetaMessageStream.mjs.map +1 -1
  22. package/lib/MessageStream.d.mts.map +1 -1
  23. package/lib/MessageStream.d.ts.map +1 -1
  24. package/lib/MessageStream.js +9 -4
  25. package/lib/MessageStream.js.map +1 -1
  26. package/lib/MessageStream.mjs +9 -4
  27. package/lib/MessageStream.mjs.map +1 -1
  28. package/lib/environments/worker.d.mts.map +1 -1
  29. package/lib/environments/worker.d.ts.map +1 -1
  30. package/lib/environments/worker.js +29 -3
  31. package/lib/environments/worker.js.map +1 -1
  32. package/lib/environments/worker.mjs +29 -3
  33. package/lib/environments/worker.mjs.map +1 -1
  34. package/lib/sessions/accumulate.js +1 -0
  35. package/lib/sessions/accumulate.js.map +1 -1
  36. package/lib/sessions/accumulate.mjs +1 -0
  37. package/lib/sessions/accumulate.mjs.map +1 -1
  38. package/lib/tools/BetaToolRunner.d.mts.map +1 -1
  39. package/lib/tools/BetaToolRunner.d.ts.map +1 -1
  40. package/lib/tools/BetaToolRunner.js +11 -0
  41. package/lib/tools/BetaToolRunner.js.map +1 -1
  42. package/lib/tools/BetaToolRunner.mjs +11 -0
  43. package/lib/tools/BetaToolRunner.mjs.map +1 -1
  44. package/package.json +9 -15
  45. package/resources/beta/agents/agents.d.mts +25 -3
  46. package/resources/beta/agents/agents.d.mts.map +1 -1
  47. package/resources/beta/agents/agents.d.ts +25 -3
  48. package/resources/beta/agents/agents.d.ts.map +1 -1
  49. package/resources/beta/agents/agents.js.map +1 -1
  50. package/resources/beta/agents/agents.mjs.map +1 -1
  51. package/resources/beta/agents/index.d.mts +1 -1
  52. package/resources/beta/agents/index.d.mts.map +1 -1
  53. package/resources/beta/agents/index.d.ts +1 -1
  54. package/resources/beta/agents/index.d.ts.map +1 -1
  55. package/resources/beta/agents/index.js.map +1 -1
  56. package/resources/beta/agents/index.mjs.map +1 -1
  57. package/resources/beta/beta.d.mts +28 -10
  58. package/resources/beta/beta.d.mts.map +1 -1
  59. package/resources/beta/beta.d.ts +28 -10
  60. package/resources/beta/beta.d.ts.map +1 -1
  61. package/resources/beta/beta.js.map +1 -1
  62. package/resources/beta/beta.mjs.map +1 -1
  63. package/resources/beta/deployments.d.mts +16 -1
  64. package/resources/beta/deployments.d.mts.map +1 -1
  65. package/resources/beta/deployments.d.ts +16 -1
  66. package/resources/beta/deployments.d.ts.map +1 -1
  67. package/resources/beta/dreams.d.mts +46 -6
  68. package/resources/beta/dreams.d.mts.map +1 -1
  69. package/resources/beta/dreams.d.ts +46 -6
  70. package/resources/beta/dreams.d.ts.map +1 -1
  71. package/resources/beta/index.d.mts +5 -5
  72. package/resources/beta/index.d.mts.map +1 -1
  73. package/resources/beta/index.d.ts +5 -5
  74. package/resources/beta/index.d.ts.map +1 -1
  75. package/resources/beta/index.js.map +1 -1
  76. package/resources/beta/index.mjs.map +1 -1
  77. package/resources/beta/messages/messages.d.mts +51 -0
  78. package/resources/beta/messages/messages.d.mts.map +1 -1
  79. package/resources/beta/messages/messages.d.ts +51 -0
  80. package/resources/beta/messages/messages.d.ts.map +1 -1
  81. package/resources/beta/messages/messages.js +2 -23
  82. package/resources/beta/messages/messages.js.map +1 -1
  83. package/resources/beta/messages/messages.mjs +2 -23
  84. package/resources/beta/messages/messages.mjs.map +1 -1
  85. package/resources/beta/sessions/events.d.mts +60 -10
  86. package/resources/beta/sessions/events.d.mts.map +1 -1
  87. package/resources/beta/sessions/events.d.ts +60 -10
  88. package/resources/beta/sessions/events.d.ts.map +1 -1
  89. package/resources/beta/sessions/events.js.map +1 -1
  90. package/resources/beta/sessions/events.mjs.map +1 -1
  91. package/resources/beta/sessions/index.d.mts +2 -2
  92. package/resources/beta/sessions/index.d.mts.map +1 -1
  93. package/resources/beta/sessions/index.d.ts +2 -2
  94. package/resources/beta/sessions/index.d.ts.map +1 -1
  95. package/resources/beta/sessions/index.js.map +1 -1
  96. package/resources/beta/sessions/index.mjs.map +1 -1
  97. package/resources/beta/sessions/sessions.d.mts +101 -6
  98. package/resources/beta/sessions/sessions.d.mts.map +1 -1
  99. package/resources/beta/sessions/sessions.d.ts +101 -6
  100. package/resources/beta/sessions/sessions.d.ts.map +1 -1
  101. package/resources/beta/sessions/sessions.js.map +1 -1
  102. package/resources/beta/sessions/sessions.mjs.map +1 -1
  103. package/resources/beta/sessions/threads/threads.d.mts +17 -5
  104. package/resources/beta/sessions/threads/threads.d.mts.map +1 -1
  105. package/resources/beta/sessions/threads/threads.d.ts +17 -5
  106. package/resources/beta/sessions/threads/threads.d.ts.map +1 -1
  107. package/resources/beta/sessions/threads/threads.js.map +1 -1
  108. package/resources/beta/sessions/threads/threads.mjs.map +1 -1
  109. package/resources/beta/user-profiles.d.mts +5 -5
  110. package/resources/beta/user-profiles.d.ts +5 -5
  111. package/resources/beta/webhooks.d.mts +11 -2
  112. package/resources/beta/webhooks.d.mts.map +1 -1
  113. package/resources/beta/webhooks.d.ts +11 -2
  114. package/resources/beta/webhooks.d.ts.map +1 -1
  115. package/resources/index.d.mts +1 -1
  116. package/resources/index.d.mts.map +1 -1
  117. package/resources/index.d.ts +1 -1
  118. package/resources/index.d.ts.map +1 -1
  119. package/resources/index.js.map +1 -1
  120. package/resources/index.mjs.map +1 -1
  121. package/resources/messages/messages.d.mts +52 -1
  122. package/resources/messages/messages.d.mts.map +1 -1
  123. package/resources/messages/messages.d.ts +52 -1
  124. package/resources/messages/messages.d.ts.map +1 -1
  125. package/resources/messages/messages.js +2 -23
  126. package/resources/messages/messages.js.map +1 -1
  127. package/resources/messages/messages.mjs +2 -23
  128. package/resources/messages/messages.mjs.map +1 -1
  129. package/src/client.ts +6 -2
  130. package/src/internal/constants.ts +0 -5
  131. package/src/lib/BetaMessageStream.ts +15 -5
  132. package/src/lib/MessageStream.ts +11 -4
  133. package/src/lib/environments/worker.ts +28 -3
  134. package/src/lib/sessions/accumulate.ts +1 -0
  135. package/src/lib/tools/BetaToolRunner.ts +11 -0
  136. package/src/resources/beta/agents/agents.ts +29 -2
  137. package/src/resources/beta/agents/index.ts +1 -0
  138. package/src/resources/beta/beta.ts +43 -1
  139. package/src/resources/beta/deployments.ts +19 -0
  140. package/src/resources/beta/dreams.ts +54 -5
  141. package/src/resources/beta/index.ts +11 -0
  142. package/src/resources/beta/messages/messages.ts +53 -23
  143. package/src/resources/beta/sessions/events.ts +95 -9
  144. package/src/resources/beta/sessions/index.ts +7 -0
  145. package/src/resources/beta/sessions/sessions.ts +127 -3
  146. package/src/resources/beta/sessions/threads/threads.ts +21 -5
  147. package/src/resources/beta/user-profiles.ts +5 -5
  148. package/src/resources/beta/webhooks.ts +16 -1
  149. package/src/resources/index.ts +2 -0
  150. package/src/resources/messages/messages.ts +53 -25
  151. package/src/tools/agent-toolset/fs-util.ts +43 -23
  152. package/src/tools/agent-toolset/node.browser.ts +14 -0
  153. package/src/tools/agent-toolset/node.ts +20 -9
  154. package/src/tools/agent-toolset/skills.ts +127 -31
  155. package/src/version.ts +1 -1
  156. package/tools/agent-toolset/fs-util.d.mts +14 -2
  157. package/tools/agent-toolset/fs-util.d.mts.map +1 -1
  158. package/tools/agent-toolset/fs-util.d.ts +14 -2
  159. package/tools/agent-toolset/fs-util.d.ts.map +1 -1
  160. package/tools/agent-toolset/fs-util.js +46 -24
  161. package/tools/agent-toolset/fs-util.js.map +1 -1
  162. package/tools/agent-toolset/fs-util.mjs +45 -24
  163. package/tools/agent-toolset/fs-util.mjs.map +1 -1
  164. package/tools/agent-toolset/node.browser.d.mts +9 -0
  165. package/tools/agent-toolset/node.browser.d.mts.map +1 -1
  166. package/tools/agent-toolset/node.browser.d.ts +9 -0
  167. package/tools/agent-toolset/node.browser.d.ts.map +1 -1
  168. package/tools/agent-toolset/node.browser.js +13 -1
  169. package/tools/agent-toolset/node.browser.js.map +1 -1
  170. package/tools/agent-toolset/node.browser.mjs +11 -0
  171. package/tools/agent-toolset/node.browser.mjs.map +1 -1
  172. package/tools/agent-toolset/node.d.mts +9 -0
  173. package/tools/agent-toolset/node.d.mts.map +1 -1
  174. package/tools/agent-toolset/node.d.ts +9 -0
  175. package/tools/agent-toolset/node.d.ts.map +1 -1
  176. package/tools/agent-toolset/node.js +19 -7
  177. package/tools/agent-toolset/node.js.map +1 -1
  178. package/tools/agent-toolset/node.mjs +17 -6
  179. package/tools/agent-toolset/node.mjs.map +1 -1
  180. package/tools/agent-toolset/skills.d.mts +26 -3
  181. package/tools/agent-toolset/skills.d.mts.map +1 -1
  182. package/tools/agent-toolset/skills.d.ts +26 -3
  183. package/tools/agent-toolset/skills.d.ts.map +1 -1
  184. package/tools/agent-toolset/skills.js +114 -29
  185. package/tools/agent-toolset/skills.js.map +1 -1
  186. package/tools/agent-toolset/skills.mjs +113 -30
  187. package/tools/agent-toolset/skills.mjs.map +1 -1
  188. package/version.d.mts +1 -1
  189. package/version.d.ts +1 -1
  190. package/version.js +1 -1
  191. package/version.mjs +1 -1
@@ -86,7 +86,7 @@ export class Messages extends APIResource {
86
86
  );
87
87
  }
88
88
 
89
- let timeout = (this._client as any)._options.timeout as number | null;
89
+ let timeout = options?.timeout ?? ((this._client as any)._options.timeout as number | null);
90
90
  if (!body.stream && timeout == null) {
91
91
  const maxNonstreamingTokens = MODEL_NONSTREAMING_TOKENS[body.model] ?? undefined;
92
92
  timeout = this._client.calculateNonstreamingTimeout(body.max_tokens, maxNonstreamingTokens);
@@ -1272,8 +1272,6 @@ export type Model =
1272
1272
  | 'claude-opus-4-5-20251101'
1273
1273
  | 'claude-sonnet-4-5'
1274
1274
  | 'claude-sonnet-4-5-20250929'
1275
- | 'claude-opus-4-1'
1276
- | 'claude-opus-4-1-20250805'
1277
1275
  | (string & {});
1278
1276
 
1279
1277
  export interface OutputConfig {
@@ -1304,28 +1302,7 @@ export interface OutputTokensDetails {
1304
1302
  }
1305
1303
  const DEPRECATED_MODELS: {
1306
1304
  [K in Model]?: string;
1307
- } = {
1308
- 'claude-1.3': 'November 6th, 2024',
1309
- 'claude-1.3-100k': 'November 6th, 2024',
1310
- 'claude-instant-1.1': 'November 6th, 2024',
1311
- 'claude-instant-1.1-100k': 'November 6th, 2024',
1312
- 'claude-instant-1.2': 'November 6th, 2024',
1313
- 'claude-3-sonnet-20240229': 'July 21st, 2025',
1314
- 'claude-3-opus-20240229': 'January 5th, 2026',
1315
- 'claude-2.1': 'July 21st, 2025',
1316
- 'claude-2.0': 'July 21st, 2025',
1317
- 'claude-3-7-sonnet-latest': 'February 19th, 2026',
1318
- 'claude-3-7-sonnet-20250219': 'February 19th, 2026',
1319
- 'claude-3-5-haiku-latest': 'February 19th, 2026',
1320
- 'claude-3-5-haiku-20241022': 'February 19th, 2026',
1321
- 'claude-opus-4-0': 'June 15th, 2026',
1322
- 'claude-opus-4-20250514': 'June 15th, 2026',
1323
- 'claude-sonnet-4-0': 'June 15th, 2026',
1324
- 'claude-sonnet-4-20250514': 'June 15th, 2026',
1325
- 'claude-opus-4-1': 'August 5th, 2026',
1326
- 'claude-opus-4-1-20250805': 'August 5th, 2026',
1327
- 'claude-mythos-preview': 'June 30th, 2026',
1328
- };
1305
+ } = {};
1329
1306
 
1330
1307
  const MODELS_TO_WARN_WITH_THINKING_ENABLED: Model[] = ['claude-mythos-preview', 'claude-opus-4-6'];
1331
1308
 
@@ -1444,12 +1421,28 @@ export type RawMessageStreamEvent =
1444
1421
  | RawContentBlockStopEvent;
1445
1422
 
1446
1423
  export interface RedactedThinkingBlock {
1424
+ /**
1425
+ * The contents of this redacted thinking block, returned when portions of the
1426
+ * model's thinking were safety-redacted. This field is opaque and encrypted, with
1427
+ * no readable content.
1428
+ *
1429
+ * Pass `redacted_thinking` blocks back to the API unchanged when continuing a
1430
+ * multi-turn conversation.
1431
+ *
1432
+ * See
1433
+ * [extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#redacted-thinking-blocks)
1434
+ * for details.
1435
+ */
1447
1436
  data: string;
1448
1437
 
1449
1438
  type: 'redacted_thinking';
1450
1439
  }
1451
1440
 
1452
1441
  export interface RedactedThinkingBlockParam {
1442
+ /**
1443
+ * The `data` value of this redacted thinking block, exactly as returned by the API
1444
+ * in a previous response. Opaque and encrypted; pass it back unchanged.
1445
+ */
1453
1446
  data: string;
1454
1447
 
1455
1448
  type: 'redacted_thinking';
@@ -1584,6 +1577,11 @@ export interface ServerToolUseBlockParam {
1584
1577
  }
1585
1578
 
1586
1579
  export interface SignatureDelta {
1580
+ /**
1581
+ * The `signature` for this thinking block: an opaque value used to verify that the
1582
+ * block was generated by Claude when it is passed back to the API. Delivered in a
1583
+ * `signature_delta` event just before the block's `content_block_stop` event.
1584
+ */
1587
1585
  signature: string;
1588
1586
 
1589
1587
  type: 'signature_delta';
@@ -1767,16 +1765,41 @@ export interface TextEditorCodeExecutionViewResultBlockParam {
1767
1765
  }
1768
1766
 
1769
1767
  export interface ThinkingBlock {
1768
+ /**
1769
+ * A value used to verify that this thinking block was generated by Claude when it
1770
+ * is passed back to the API.
1771
+ *
1772
+ * This is an opaque field and should not be interpreted or parsed. When passing
1773
+ * thinking blocks back to the API (required when using tools with extended
1774
+ * thinking), pass them back exactly as received, with this field intact.
1775
+ *
1776
+ * See
1777
+ * [extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking)
1778
+ * for details.
1779
+ */
1770
1780
  signature: string;
1771
1781
 
1782
+ /**
1783
+ * The text of Claude's thinking process for this block.
1784
+ */
1772
1785
  thinking: string;
1773
1786
 
1774
1787
  type: 'thinking';
1775
1788
  }
1776
1789
 
1777
1790
  export interface ThinkingBlockParam {
1791
+ /**
1792
+ * The `signature` value of this thinking block, exactly as returned by the API in
1793
+ * a previous response. Used to verify that the block was generated by Claude.
1794
+ *
1795
+ * Thinking blocks must be passed back unmodified and in their original order; a
1796
+ * modified block results in a 400 `invalid_request_error`.
1797
+ */
1778
1798
  signature: string;
1779
1799
 
1800
+ /**
1801
+ * The `thinking` text of this block as returned by the API.
1802
+ */
1780
1803
  thinking: string;
1781
1804
 
1782
1805
  type: 'thinking';
@@ -1837,6 +1860,11 @@ export interface ThinkingConfigEnabled {
1837
1860
  export type ThinkingConfigParam = ThinkingConfigEnabled | ThinkingConfigDisabled | ThinkingConfigAdaptive;
1838
1861
 
1839
1862
  export interface ThinkingDelta {
1863
+ /**
1864
+ * The incremental `thinking` text for this content block. Concatenate the
1865
+ * `thinking` values of successive `thinking_delta` events to assemble the block's
1866
+ * full `thinking` value.
1867
+ */
1840
1868
  thinking: string;
1841
1869
 
1842
1870
  type: 'thinking_delta';
@@ -24,12 +24,27 @@ async function realpathOrSelf(p: string): Promise<string> {
24
24
  }
25
25
  }
26
26
 
27
+ /** Matches Linux MAXSYMLINKS, the threshold at which `realpath` itself reports ELOOP. */
28
+ const MAX_SYMLINK_HOPS = 40;
29
+
30
+ /** The `code` of a Node system error, or `undefined` for anything else. */
31
+ export function errnoCode(err: unknown): string | undefined {
32
+ const code = (err as { code?: unknown } | null)?.code;
33
+ return typeof code === 'string' ? code : undefined;
34
+ }
35
+
27
36
  /**
28
37
  * Fully resolve `abs`: `realpath` the longest existing ancestor and re-append
29
38
  * the rest, but never re-append a component that is itself a symlink — read the
30
39
  * link and continue from its target instead. This handles paths being created
31
40
  * (write/edit) without letting a symlink leaf (e.g. a dangling one pointing
32
41
  * outside a confinement root) slip through unresolved.
42
+ *
43
+ * Returns a symlink-free path or throws an errno-carrying error (`ELOOP` for a
44
+ * cycle or more than {@link MAX_SYMLINK_HOPS} links, the `lstat`/`realpath`
45
+ * error for an unreadable component); it never returns `abs` unresolved. Only
46
+ * symlink hops count against the cap, so any depth of not-yet-existing
47
+ * directories still resolves.
33
48
  */
34
49
  export async function canonicalize(abs: string): Promise<string> {
35
50
  const tail: string[] = [];
@@ -39,28 +54,24 @@ export async function canonicalize(abs: string): Promise<string> {
39
54
  let real: string;
40
55
  try {
41
56
  real = await fs.realpath(prefix);
42
- } catch {
43
- let isLink = false;
57
+ } catch (realpathErr) {
58
+ let isLink: boolean;
44
59
  try {
45
60
  isLink = (await fs.lstat(prefix)).isSymbolicLink();
46
- } catch {
47
- /* prefix truly doesn't exist (ENOENT) — fall through and walk up */
48
- }
49
- if (isLink) {
50
- // Resolve the symlink ourselves and retry; `tail` (the part below it)
51
- // still applies to the link's target. The hop cap matches Linux
52
- // MAXSYMLINKS — the same threshold at which `realpath` itself would
53
- // have returned ELOOP — so a cycle of unresolvable links terminates.
54
- if (++hops > 40) {
55
- throw new ToolError(`path ${JSON.stringify(abs)} has too many levels of symbolic links`);
56
- }
57
- prefix = path.resolve(path.dirname(prefix), await fs.readlink(prefix));
61
+ } catch (lstatErr) {
62
+ const code = errnoCode(lstatErr);
63
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') throw lstatErr;
64
+ const parent = path.dirname(prefix);
65
+ if (parent === prefix) throw lstatErr;
66
+ tail.push(path.basename(prefix));
67
+ prefix = parent;
58
68
  continue;
59
69
  }
60
- const parent = path.dirname(prefix);
61
- if (parent === prefix) return abs; // walked past the FS root without a hit
62
- tail.push(path.basename(prefix));
63
- prefix = parent;
70
+ if (!isLink) throw realpathErr;
71
+ if (++hops > MAX_SYMLINK_HOPS) {
72
+ throw Object.assign(new Error('too many levels of symbolic links'), { code: 'ELOOP' });
73
+ }
74
+ prefix = path.resolve(path.dirname(prefix), await fs.readlink(prefix));
64
75
  continue;
65
76
  }
66
77
  return tail.length ? path.join(real, ...tail.reverse()) : real;
@@ -76,6 +87,9 @@ export async function canonicalize(abs: string): Promise<string> {
76
87
  * leaf, even a dangling one) is resolved before the confinement check, and the
77
88
  * resolved path is what the caller then operates on, so a symlink inside `root`
78
89
  * that points outside it can neither pass the check nor be followed afterwards.
90
+ * `..` is collapsed lexically before any symlink is followed. A path that cannot
91
+ * be resolved (symlink loop, unreadable component) is rejected with a
92
+ * `ToolError` naming `p`, never the host's absolute path.
79
93
  *
80
94
  * Residual TOCTOU: a component could still be swapped for a symlink between this
81
95
  * call and the eventual `fs` operation. Closing that fully needs per-component
@@ -91,7 +105,12 @@ export async function confineToRoot(
91
105
  const realRoot = await realpathOrSelf(path.resolve(root));
92
106
  const abs = path.resolve(realRoot, p);
93
107
  if (allowOutside) return abs;
94
- const real = await canonicalize(abs);
108
+ let real: string;
109
+ try {
110
+ real = await canonicalize(abs);
111
+ } catch (err) {
112
+ throw new ToolError(fsErrorMessage(err, `path ${JSON.stringify(p)}`));
113
+ }
95
114
  if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
96
115
  throw new ToolError(`path ${JSON.stringify(p)} escapes workdir`);
97
116
  }
@@ -124,11 +143,12 @@ export async function atomicWriteFile(targetPath: string, content: string): Prom
124
143
  /**
125
144
  * Map a thrown filesystem error to a consistent, language-independent message,
126
145
  * so the model sees the same wording regardless of the runtime (Node's raw
127
- * `ENOENT: no such file...` text would otherwise leak through). Falls back to
128
- * the raw error message for codes we don't special-case.
146
+ * `ENOENT: no such file...` text would otherwise leak through). Codes we don't
147
+ * special-case render as the bare code, never Node's message, which embeds the
148
+ * host's absolute path.
129
149
  */
130
150
  export function fsErrorMessage(err: unknown, file: string): string {
131
- const code = (err as { code?: string } | null)?.code;
151
+ const code = errnoCode(err);
132
152
  switch (code) {
133
153
  case 'ENOENT':
134
154
  return `${file}: no such file or directory`;
@@ -149,6 +169,6 @@ export function fsErrorMessage(err: unknown, file: string): string {
149
169
  case 'ENFILE':
150
170
  return `${file}: too many open files`;
151
171
  default:
152
- return `${file}: ${err instanceof Error ? err.message : String(err)}`;
172
+ return `${file}: ${code !== undefined ? `i/o error (${code})` : 'i/o error'}`;
153
173
  }
154
174
  }
@@ -44,6 +44,20 @@ export function resolvePath(_ctx: AgentToolContext, _p: string): Promise<string>
44
44
  return nodeOnly('resolvePath');
45
45
  }
46
46
 
47
+ /**
48
+ * A bash command exceeded its `timeoutMs`. Carries the timeout so a caller can
49
+ * tell it apart from an abort without matching on the message text.
50
+ */
51
+ export class BashTimeoutError extends AnthropicError {
52
+ readonly timeoutMs: number;
53
+
54
+ constructor(timeoutMs: number) {
55
+ super(`bash command timed out after ${timeoutMs}ms`);
56
+ this.name = 'BashTimeoutError';
57
+ this.timeoutMs = timeoutMs;
58
+ }
59
+ }
60
+
47
61
  export class BashSession {
48
62
  constructor(_dir: string, _env?: NodeJS.ProcessEnv) {
49
63
  nodeOnly('BashSession');
@@ -55,6 +55,20 @@ const GREP_OUTPUT_LIMIT = 100 * 1024;
55
55
  const GREP_MAX_LINE_LENGTH = 2000;
56
56
  const GLOB_RESULT_LIMIT = 200;
57
57
 
58
+ /**
59
+ * A bash command exceeded its `timeoutMs`. Carries the timeout so a caller can
60
+ * tell it apart from an abort without matching on the message text.
61
+ */
62
+ export class BashTimeoutError extends AnthropicError {
63
+ readonly timeoutMs: number;
64
+
65
+ constructor(timeoutMs: number) {
66
+ super(`bash command timed out after ${timeoutMs}ms`);
67
+ this.name = 'BashTimeoutError';
68
+ this.timeoutMs = timeoutMs;
69
+ }
70
+ }
71
+
58
72
  const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
59
73
 
60
74
  // `fs.glob` is Node 22+. `@types/node` may still target an older line, so the
@@ -271,9 +285,9 @@ export class BashSession {
271
285
  }
272
286
  const timeoutMs = opts.timeoutMs ?? BASH_DEFAULT_TIMEOUT_MS;
273
287
  const signal = opts.signal;
274
- if (signal?.aborted) {
275
- throw new AnthropicError('bash command aborted');
276
- }
288
+ // Reject with the signal's own reason, so a caller telling a user cancel
289
+ // apart from an `AbortSignal.timeout()` sees the platform's name intact.
290
+ signal?.throwIfAborted();
277
291
  this.#buf = '';
278
292
  this.#truncated = false;
279
293
  // Per-call nonce so a command that prints a fixed marker can't spoof the
@@ -298,14 +312,11 @@ export class BashSession {
298
312
  await Promise.race([
299
313
  sentinelSeen,
300
314
  new Promise<never>((_, reject) => {
301
- timer = setTimeout(
302
- () => reject(new AnthropicError(`bash command timed out after ${timeoutMs}ms`)),
303
- timeoutMs,
304
- );
315
+ timer = setTimeout(() => reject(new BashTimeoutError(timeoutMs)), timeoutMs);
305
316
  }),
306
317
  new Promise<never>((_, reject) => {
307
318
  if (!signal) return;
308
- onAbort = () => reject(new AnthropicError('bash command aborted'));
319
+ onAbort = () => reject(signal.reason);
309
320
  signal.addEventListener('abort', onAbort, { once: true });
310
321
  }),
311
322
  ]);
@@ -459,7 +470,7 @@ export function betaReadTool(ctx: AgentToolContext): BetaRunnableTool {
459
470
  if (e instanceof ToolError) throw e;
460
471
  throw new ToolError(`read: ${fsErrorMessage(e, file_path)}`);
461
472
  }
462
- if (!view_range) return data;
473
+ if (!view_range?.length) return data;
463
474
  if (view_range.length !== 2) throw new ToolError('read: view_range must be [start_line, end_line]');
464
475
  const [startLine, endLine] = view_range as [number, number];
465
476
  const lines = data.split('\n');
@@ -15,7 +15,7 @@ import { pipeline } from 'node:stream/promises';
15
15
  import type { Anthropic } from '../../client';
16
16
  import { AnthropicError } from '../../core/error';
17
17
  import { loggerFor } from '../../internal/utils/log';
18
- import { DIR_CREATE_MODE } from './fs-util';
18
+ import { DIR_CREATE_MODE, errnoCode } from './fs-util';
19
19
  import type { AgentToolContext } from './node';
20
20
 
21
21
  const execFileAsync = promisify(execFile);
@@ -115,8 +115,8 @@ export async function resolveSkillVersion(
115
115
  }
116
116
 
117
117
  /** Reject archive members that are absolute or contain a `..` component. */
118
- function assertSafeMemberNames(names: string): void {
119
- for (const raw of names.split('\n')) {
118
+ function assertSafeMemberNames(names: string[]): void {
119
+ for (const raw of names) {
120
120
  const entry = raw.trim();
121
121
  if (!entry) continue;
122
122
  if (path.isAbsolute(entry) || entry.split(/[\\/]/).includes('..')) {
@@ -125,19 +125,77 @@ function assertSafeMemberNames(names: string): void {
125
125
  }
126
126
  }
127
127
 
128
+ const INCONSISTENT_LISTING = 'skill archive listing is inconsistent; refusing to extract';
129
+
130
+ /**
131
+ * Type chars (first byte of each `ls`-style line from `unzip -Z` / `tar -tvf`)
132
+ * that denote a regular file or directory. `zipinfo` prints `?` for entries
133
+ * with no Unix type bits, which `unzip` extracts as regular files; GNU tar
134
+ * prints `C` for contiguous files. Everything else — `l` symlink, `h`
135
+ * hardlink, `b`/`c` device, `p` fifo, `s` socket, unknown tar types — is a
136
+ * special member.
137
+ */
138
+ const PLAIN_TYPE_CHARS = { unzip: new Set(['-', 'd', '?']), tar: new Set(['-', 'd', 'C']) };
139
+
140
+ function listingLines(listing: string): string[] {
141
+ const lines = listing.split('\n');
142
+ if (lines[lines.length - 1] === '') lines.pop();
143
+ return lines;
144
+ }
145
+
128
146
  /**
129
- * Reject archives that contain anything other than regular files and
130
- * directories. The type char is the first byte of each `ls`-style line emitted
131
- * by `tar -tvf` / `unzip -Z`: `-` file, `d` dir, `l` symlink, `h` hardlink,
132
- * `b`/`c` device, `p` fifo, `s` socket. A symlink/hardlink member is how an
133
- * archive escapes its extraction dir even when no name contains `..`.
147
+ * A special member is excluded by handing its listed name back to the CLI as
148
+ * a pattern, so the name must be byte-identical to what is stored. `tar`,
149
+ * `bsdtar` and `unzip` print bytes they cannot show literally as `\ooo`, `^X`
150
+ * or `#U` escapes, or as raw non-ASCII; any such name cannot be excluded
151
+ * reliably. A leading `-` would let `unzip` parse the pattern as an option.
134
152
  */
135
- function assertNoSpecialMembers(verboseListing: string): void {
136
- for (const line of verboseListing.split('\n')) {
137
- const type = line.trimStart()[0];
138
- if (type === 'l' || type === 'h' || type === 'b' || type === 'c' || type === 'p' || type === 's') {
139
- throw new AnthropicError('refusing to extract archive with symlink/hardlink/device member');
153
+ function canExcludeVerbatim(cmd: 'unzip' | 'tar', name: string): boolean {
154
+ return /^[\x20-\x7E]+$/.test(name) && !/[\\^#]/.test(name) && !(cmd === 'unzip' && name.startsWith('-'));
155
+ }
156
+
157
+ /**
158
+ * Pair an archive's name listing (`unzip -Z1` / `tar -tf`) with its typed
159
+ * listing (`unzip -Z --h --t` / `tar -tvf`) and split the members into plain
160
+ * (regular file or directory) and special (everything else). Special members
161
+ * are excluded from extraction rather than rejected; the archive is refused
162
+ * only when the two listings disagree in length or a special member's name
163
+ * cannot be passed back to the CLI verbatim (see {@link canExcludeVerbatim}).
164
+ */
165
+ export function classifyArchiveListing(
166
+ cmd: 'unzip' | 'tar',
167
+ names: string,
168
+ typed: string,
169
+ ): { plain: string[]; special: string[] } {
170
+ const nameLines = listingLines(names);
171
+ const typedLines = listingLines(typed);
172
+ if (nameLines.length !== typedLines.length) throw new AnthropicError(INCONSISTENT_LISTING);
173
+ const plain: string[] = [];
174
+ const special: string[] = [];
175
+ nameLines.forEach((name, i) => {
176
+ if (PLAIN_TYPE_CHARS[cmd].has(typedLines[i]!.charAt(0))) {
177
+ plain.push(name);
178
+ return;
140
179
  }
180
+ if (!canExcludeVerbatim(cmd, name)) {
181
+ throw new AnthropicError(
182
+ `refusing to extract archive: cannot safely exclude member ${JSON.stringify(name)}`,
183
+ );
184
+ }
185
+ special.push(name);
186
+ });
187
+ return { plain, special };
188
+ }
189
+
190
+ /**
191
+ * Walk `dir` with `lstat` semantics and reject anything that is not a regular
192
+ * file or directory. Never follows a link and never descends into anything
193
+ * but a real directory.
194
+ */
195
+ export async function assertOnlyPlainEntries(dir: string): Promise<void> {
196
+ for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
197
+ if (entry.isDirectory()) await assertOnlyPlainEntries(path.join(dir, entry.name));
198
+ else if (!entry.isFile()) throw new AnthropicError(INCONSISTENT_LISTING);
141
199
  }
142
200
  }
143
201
 
@@ -152,7 +210,7 @@ async function runArchiveTool(cmd: 'unzip' | 'tar', args: string[]): Promise<str
152
210
  const { stdout } = await execFileAsync(cmd, args);
153
211
  return stdout;
154
212
  } catch (e) {
155
- if (e != null && typeof e === 'object' && (e as { code?: unknown }).code === 'ENOENT') {
213
+ if (errnoCode(e) === 'ENOENT') {
156
214
  throw new AnthropicError(
157
215
  `skill extraction requires the \`${cmd}\` command, but it was not found on PATH`,
158
216
  );
@@ -162,17 +220,17 @@ async function runArchiveTool(cmd: 'unzip' | 'tar', args: string[]): Promise<str
162
220
  }
163
221
 
164
222
  /**
165
- * The single top-level directory shared by every entry in a newline-separated
166
- * archive listing, or `''` if entries don't all live under one common
167
- * directory. Skill bundles are packaged wrapped in one directory named after
168
- * the skill (e.g. `pdf/SKILL.md`, `pdf/scripts/...`); the extractor strips it
169
- * so contents land directly in the skill's dir instead of a redundant nested
170
- * `<skill>/<skill>/` level. A flat or multi-root archive yields `''`.
223
+ * The single top-level directory shared by every entry in an archive listing,
224
+ * or `''` if entries don't all live under one common directory. Skill bundles
225
+ * are packaged wrapped in one directory named after the skill (e.g.
226
+ * `pdf/SKILL.md`, `pdf/scripts/...`); the extractor strips it so contents land
227
+ * directly in the skill's dir instead of a redundant nested `<skill>/<skill>/`
228
+ * level. A flat or multi-root archive yields `''`.
171
229
  */
172
- function archiveTopDir(listing: string): string {
230
+ function archiveTopDir(names: string[]): string {
173
231
  let top: string | undefined;
174
232
  let nested = false;
175
- for (const raw of listing.split('\n')) {
233
+ for (const raw of names) {
176
234
  // Drop `.` / empty segments so a `./pdf/...`-style listing (e.g. from
177
235
  // `tar -C dir .`) is treated the same as `pdf/...`.
178
236
  const parts = raw
@@ -195,9 +253,14 @@ function archiveTopDir(listing: string): string {
195
253
  * to `unzip`/`tar` — consistent with the rest of the toolset, which already
196
254
  * invokes `bash` and `rg`. Both `unzip` and `tar` must be available on `PATH`; a
197
255
  * missing binary surfaces as a clear error (see {@link runArchiveTool}). Refuses
198
- * any member that would escape `dest` (zip-slip / tar-slip), including
199
- * symlink/hardlink members: skill archives come from the API, but skills can be
200
- * third-party.
256
+ * any member that would escape `dest` (zip-slip / tar-slip): skill archives
257
+ * come from the API, but skills can be third-party. Members that are not a
258
+ * regular file or directory (symlink, hardlink, device, fifo) are excluded
259
+ * from extraction rather than rejected; an archive whose special members
260
+ * cannot be excluded reliably is refused (see {@link classifyArchiveListing}).
261
+ * `tar` matches exclusions unanchored, so a plain member sharing a special
262
+ * member's name may be dropped too. The staging tree is verified to hold only
263
+ * regular files and directories before anything is promoted into `dest`.
201
264
  *
202
265
  * The skill bundle's single wrapper directory is stripped: the archive is
203
266
  * extracted into a staging dir and the wrapper's contents are promoted into
@@ -215,6 +278,7 @@ export async function extractSkillArchive(resp: Response, dest: string): Promise
215
278
  fssync.createWriteStream(tmp),
216
279
  );
217
280
  const stage = path.join(path.dirname(dest), `.skill-stage-${process.pid}-${Date.now()}`);
281
+ const excludeFile = path.join(path.dirname(dest), `.skill-exclude-${process.pid}-${Date.now()}`);
218
282
  try {
219
283
  // Sniff the first bytes: zip archives start with "PK\x03\x04"; treat
220
284
  // anything else as a tar.* archive (`tar -xf` autodetects gzip/bzip2/xz).
@@ -224,25 +288,57 @@ export async function extractSkillArchive(resp: Response, dest: string): Promise
224
288
  const archiveCmd = isZip ? 'unzip' : 'tar';
225
289
  // List first, validate, then extract — `tar`/`unzip` will happily write a
226
290
  // `../` member (or follow a symlink member) outside `-C`/`-d` otherwise.
227
- const listing = await runArchiveTool(archiveCmd, isZip ? ['-Z1', tmp] : ['-tf', tmp]);
228
- assertSafeMemberNames(listing);
229
- assertNoSpecialMembers(await runArchiveTool(archiveCmd, isZip ? ['-Z', tmp] : ['-tvf', tmp]));
230
- const top = archiveTopDir(listing);
291
+ const names = await runArchiveTool(archiveCmd, isZip ? ['-Z1', tmp] : ['-tf', tmp]);
292
+ const typed = await runArchiveTool(archiveCmd, isZip ? ['-Z', '--h', '--t', tmp] : ['-tvf', tmp]);
293
+ const { plain, special } = classifyArchiveListing(archiveCmd, names, typed);
294
+ assertSafeMemberNames([...plain, ...special]);
295
+ const top = archiveTopDir(plain);
231
296
  await fs.mkdir(stage, { recursive: true, mode: DIR_CREATE_MODE });
232
- await runArchiveTool(archiveCmd, isZip ? ['-oq', tmp, '-d', stage] : ['-xf', tmp, '-C', stage]);
297
+ // `unzip` exits non-zero when every member is excluded, so only run the
298
+ // extractor when there is something to extract.
299
+ if (plain.length > 0) {
300
+ await runArchiveTool(archiveCmd, await extractArgs(archiveCmd, tmp, stage, special, excludeFile));
301
+ }
302
+ await assertOnlyPlainEntries(stage);
233
303
  // Promote the wrapper's contents (or the staged tree itself, if the
234
304
  // archive wasn't wrapped) into the already-created empty `dest`. `stage`
235
305
  // is a sibling of `dest`, so each rename stays on one filesystem.
236
306
  const srcRoot = top ? path.join(stage, top) : stage;
237
- for (const entry of await fs.readdir(srcRoot)) {
307
+ const entries = await fs.readdir(srcRoot).catch((e: unknown) => {
308
+ throw errnoCode(e) === 'ENOENT' ? new AnthropicError(INCONSISTENT_LISTING) : e;
309
+ });
310
+ for (const entry of entries) {
238
311
  await fs.rename(path.join(srcRoot, entry), path.join(dest, entry));
239
312
  }
240
313
  } finally {
241
314
  await fs.rm(tmp, { force: true });
315
+ await fs.rm(excludeFile, { force: true });
242
316
  await fs.rm(stage, { recursive: true, force: true });
243
317
  }
244
318
  }
245
319
 
320
+ /**
321
+ * Arguments that extract `archive` into `stage` while excluding every member
322
+ * in `special`. Names are glob-escaped because both CLIs treat exclusions as
323
+ * patterns; `tar` reads them from `excludeFile`, `unzip` takes them after
324
+ * `-x`, which must follow `-d` so no pattern is parsed as an option.
325
+ */
326
+ async function extractArgs(
327
+ cmd: 'unzip' | 'tar',
328
+ archive: string,
329
+ stage: string,
330
+ special: string[],
331
+ excludeFile: string,
332
+ ): Promise<string[]> {
333
+ const patterns = special.map((name) => name.replace(/[*?[\\]/g, '\\$&'));
334
+ if (cmd === 'unzip') {
335
+ return ['-oq', archive, '-d', stage, ...(patterns.length > 0 ? ['-x', ...patterns] : [])];
336
+ }
337
+ if (patterns.length === 0) return ['-xf', archive, '-C', stage];
338
+ await fs.writeFile(excludeFile, patterns.join('\n') + '\n', { flag: 'wx', mode: 0o600 });
339
+ return ['-xf', archive, '-C', stage, '-X', excludeFile];
340
+ }
341
+
246
342
  /** Read the first `n` bytes of `file`. */
247
343
  async function readHead(file: string, n: number): Promise<Buffer> {
248
344
  const handle = await fs.open(file, 'r');
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.115.0'; // x-release-please-version
1
+ export const VERSION = '0.117.1'; // x-release-please-version
@@ -8,12 +8,20 @@
8
8
  export declare const DIR_CREATE_MODE = 493;
9
9
  /** Mode for files the file tools create. */
10
10
  export declare const FILE_CREATE_MODE = 420;
11
+ /** The `code` of a Node system error, or `undefined` for anything else. */
12
+ export declare function errnoCode(err: unknown): string | undefined;
11
13
  /**
12
14
  * Fully resolve `abs`: `realpath` the longest existing ancestor and re-append
13
15
  * the rest, but never re-append a component that is itself a symlink — read the
14
16
  * link and continue from its target instead. This handles paths being created
15
17
  * (write/edit) without letting a symlink leaf (e.g. a dangling one pointing
16
18
  * outside a confinement root) slip through unresolved.
19
+ *
20
+ * Returns a symlink-free path or throws an errno-carrying error (`ELOOP` for a
21
+ * cycle or more than {@link MAX_SYMLINK_HOPS} links, the `lstat`/`realpath`
22
+ * error for an unreadable component); it never returns `abs` unresolved. Only
23
+ * symlink hops count against the cap, so any depth of not-yet-existing
24
+ * directories still resolves.
17
25
  */
18
26
  export declare function canonicalize(abs: string): Promise<string>;
19
27
  /**
@@ -25,6 +33,9 @@ export declare function canonicalize(abs: string): Promise<string>;
25
33
  * leaf, even a dangling one) is resolved before the confinement check, and the
26
34
  * resolved path is what the caller then operates on, so a symlink inside `root`
27
35
  * that points outside it can neither pass the check nor be followed afterwards.
36
+ * `..` is collapsed lexically before any symlink is followed. A path that cannot
37
+ * be resolved (symlink loop, unreadable component) is rejected with a
38
+ * `ToolError` naming `p`, never the host's absolute path.
28
39
  *
29
40
  * Residual TOCTOU: a component could still be swapped for a symlink between this
30
41
  * call and the eventual `fs` operation. Closing that fully needs per-component
@@ -43,8 +54,9 @@ export declare function atomicWriteFile(targetPath: string, content: string): Pr
43
54
  /**
44
55
  * Map a thrown filesystem error to a consistent, language-independent message,
45
56
  * so the model sees the same wording regardless of the runtime (Node's raw
46
- * `ENOENT: no such file...` text would otherwise leak through). Falls back to
47
- * the raw error message for codes we don't special-case.
57
+ * `ENOENT: no such file...` text would otherwise leak through). Codes we don't
58
+ * special-case render as the bare code, never Node's message, which embeds the
59
+ * host's absolute path.
48
60
  */
49
61
  export declare function fsErrorMessage(err: unknown, file: string): string;
50
62
  //# sourceMappingURL=fs-util.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fs-util.d.mts","sourceRoot":"","sources":["../../src/tools/agent-toolset/fs-util.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,uFAAuF;AACvF,eAAO,MAAM,eAAe,MAAQ,CAAC;AACrC,4CAA4C;AAC5C,eAAO,MAAM,gBAAgB,MAAQ,CAAC;AAWtC;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkC/D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,IAAI,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GAChC,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBxF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAwBjE"}
1
+ {"version":3,"file":"fs-util.d.mts","sourceRoot":"","sources":["../../src/tools/agent-toolset/fs-util.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,uFAAuF;AACvF,eAAO,MAAM,eAAe,MAAQ,CAAC;AACrC,4CAA4C;AAC5C,eAAO,MAAM,gBAAgB,MAAQ,CAAC;AActC,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAG1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA8B/D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,IAAI,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GAChC,OAAO,CAAC,MAAM,CAAC,CAejB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBxF;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAwBjE"}