@anthropic-ai/sdk 0.116.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.
- package/CHANGELOG.md +39 -0
- package/lib/BetaMessageStream.d.mts.map +1 -1
- package/lib/BetaMessageStream.d.ts.map +1 -1
- package/lib/BetaMessageStream.js +12 -5
- package/lib/BetaMessageStream.js.map +1 -1
- package/lib/BetaMessageStream.mjs +12 -5
- package/lib/BetaMessageStream.mjs.map +1 -1
- package/lib/MessageStream.d.mts.map +1 -1
- package/lib/MessageStream.d.ts.map +1 -1
- package/lib/MessageStream.js +9 -4
- package/lib/MessageStream.js.map +1 -1
- package/lib/MessageStream.mjs +9 -4
- package/lib/MessageStream.mjs.map +1 -1
- package/lib/environments/worker.d.mts.map +1 -1
- package/lib/environments/worker.d.ts.map +1 -1
- package/lib/environments/worker.js +29 -3
- package/lib/environments/worker.js.map +1 -1
- package/lib/environments/worker.mjs +29 -3
- package/lib/environments/worker.mjs.map +1 -1
- package/lib/tools/BetaToolRunner.d.mts.map +1 -1
- package/lib/tools/BetaToolRunner.d.ts.map +1 -1
- package/lib/tools/BetaToolRunner.js +11 -0
- package/lib/tools/BetaToolRunner.js.map +1 -1
- package/lib/tools/BetaToolRunner.mjs +11 -0
- package/lib/tools/BetaToolRunner.mjs.map +1 -1
- package/package.json +9 -15
- package/resources/beta/beta.d.mts +2 -2
- package/resources/beta/beta.d.mts.map +1 -1
- package/resources/beta/beta.d.ts +2 -2
- package/resources/beta/beta.d.ts.map +1 -1
- package/resources/beta/beta.js.map +1 -1
- package/resources/beta/beta.mjs.map +1 -1
- package/resources/beta/dreams.d.mts +46 -6
- package/resources/beta/dreams.d.mts.map +1 -1
- package/resources/beta/dreams.d.ts +46 -6
- package/resources/beta/dreams.d.ts.map +1 -1
- package/resources/beta/index.d.mts +1 -1
- package/resources/beta/index.d.mts.map +1 -1
- package/resources/beta/index.d.ts +1 -1
- package/resources/beta/index.d.ts.map +1 -1
- package/resources/beta/index.js.map +1 -1
- package/resources/beta/index.mjs.map +1 -1
- package/resources/beta/messages/messages.js +1 -1
- package/resources/beta/messages/messages.js.map +1 -1
- package/resources/beta/messages/messages.mjs +1 -1
- package/resources/beta/messages/messages.mjs.map +1 -1
- package/resources/beta/user-profiles.d.mts +5 -5
- package/resources/beta/user-profiles.d.ts +5 -5
- package/resources/messages/messages.js +1 -1
- package/resources/messages/messages.js.map +1 -1
- package/resources/messages/messages.mjs +1 -1
- package/resources/messages/messages.mjs.map +1 -1
- package/src/lib/BetaMessageStream.ts +15 -5
- package/src/lib/MessageStream.ts +11 -4
- package/src/lib/environments/worker.ts +28 -3
- package/src/lib/tools/BetaToolRunner.ts +11 -0
- package/src/resources/beta/beta.ts +6 -0
- package/src/resources/beta/dreams.ts +54 -5
- package/src/resources/beta/index.ts +3 -0
- package/src/resources/beta/messages/messages.ts +1 -1
- package/src/resources/beta/user-profiles.ts +5 -5
- package/src/resources/messages/messages.ts +1 -1
- package/src/tools/agent-toolset/fs-util.ts +43 -23
- package/src/tools/agent-toolset/node.ts +1 -1
- package/src/tools/agent-toolset/skills.ts +127 -31
- package/src/version.ts +1 -1
- package/tools/agent-toolset/fs-util.d.mts +14 -2
- package/tools/agent-toolset/fs-util.d.mts.map +1 -1
- package/tools/agent-toolset/fs-util.d.ts +14 -2
- package/tools/agent-toolset/fs-util.d.ts.map +1 -1
- package/tools/agent-toolset/fs-util.js +46 -24
- package/tools/agent-toolset/fs-util.js.map +1 -1
- package/tools/agent-toolset/fs-util.mjs +45 -24
- package/tools/agent-toolset/fs-util.mjs.map +1 -1
- package/tools/agent-toolset/node.js +1 -1
- package/tools/agent-toolset/node.js.map +1 -1
- package/tools/agent-toolset/node.mjs +1 -1
- package/tools/agent-toolset/node.mjs.map +1 -1
- package/tools/agent-toolset/skills.d.mts +26 -3
- package/tools/agent-toolset/skills.d.mts.map +1 -1
- package/tools/agent-toolset/skills.d.ts +26 -3
- package/tools/agent-toolset/skills.d.ts.map +1 -1
- package/tools/agent-toolset/skills.js +114 -29
- package/tools/agent-toolset/skills.js.map +1 -1
- package/tools/agent-toolset/skills.mjs +113 -30
- package/tools/agent-toolset/skills.mjs.map +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -184,8 +184,8 @@ export interface BetaUserProfile {
|
|
|
184
184
|
external_id?: string | null;
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
|
-
*
|
|
188
|
-
* resold-to company's name.
|
|
187
|
+
* Real-world name of the entity this profile represents (company or individual).
|
|
188
|
+
* For `resold` this is the resold-to company's name.
|
|
189
189
|
*/
|
|
190
190
|
name?: string | null;
|
|
191
191
|
}
|
|
@@ -229,9 +229,9 @@ export interface UserProfileCreateParams {
|
|
|
229
229
|
metadata?: { [key: string]: string };
|
|
230
230
|
|
|
231
231
|
/**
|
|
232
|
-
* Body param:
|
|
233
|
-
*
|
|
234
|
-
* Maximum 255 characters.
|
|
232
|
+
* Body param: Optional for all profiles. Real-world name of the entity this
|
|
233
|
+
* profile represents (company or individual); for `resold` profiles, the resold-to
|
|
234
|
+
* company's name where known. Maximum 255 characters.
|
|
235
235
|
*/
|
|
236
236
|
name?: string | null;
|
|
237
237
|
|
|
@@ -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);
|
|
@@ -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
|
|
57
|
+
} catch (realpathErr) {
|
|
58
|
+
let isLink: boolean;
|
|
44
59
|
try {
|
|
45
60
|
isLink = (await fs.lstat(prefix)).isSymbolicLink();
|
|
46
|
-
} catch {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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).
|
|
128
|
-
*
|
|
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
|
|
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}: ${
|
|
172
|
+
return `${file}: ${code !== undefined ? `i/o error (${code})` : 'i/o error'}`;
|
|
153
173
|
}
|
|
154
174
|
}
|
|
@@ -470,7 +470,7 @@ export function betaReadTool(ctx: AgentToolContext): BetaRunnableTool {
|
|
|
470
470
|
if (e instanceof ToolError) throw e;
|
|
471
471
|
throw new ToolError(`read: ${fsErrorMessage(e, file_path)}`);
|
|
472
472
|
}
|
|
473
|
-
if (!view_range) return data;
|
|
473
|
+
if (!view_range?.length) return data;
|
|
474
474
|
if (view_range.length !== 2) throw new ToolError('read: view_range must be [start_line, end_line]');
|
|
475
475
|
const [startLine, endLine] = view_range as [number, number];
|
|
476
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
|
|
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
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* `
|
|
133
|
-
*
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 (
|
|
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
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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(
|
|
230
|
+
function archiveTopDir(names: string[]): string {
|
|
173
231
|
let top: string | undefined;
|
|
174
232
|
let nested = false;
|
|
175
|
-
for (const raw of
|
|
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)
|
|
199
|
-
*
|
|
200
|
-
*
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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).
|
|
47
|
-
*
|
|
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;
|
|
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"}
|
|
@@ -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).
|
|
47
|
-
*
|
|
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.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs-util.d.ts","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;
|
|
1
|
+
{"version":3,"file":"fs-util.d.ts","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"}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.FILE_CREATE_MODE = exports.DIR_CREATE_MODE = void 0;
|
|
10
|
+
exports.errnoCode = errnoCode;
|
|
10
11
|
exports.canonicalize = canonicalize;
|
|
11
12
|
exports.confineToRoot = confineToRoot;
|
|
12
13
|
exports.atomicWriteFile = atomicWriteFile;
|
|
@@ -29,12 +30,25 @@ async function realpathOrSelf(p) {
|
|
|
29
30
|
return p;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
/** Matches Linux MAXSYMLINKS, the threshold at which `realpath` itself reports ELOOP. */
|
|
34
|
+
const MAX_SYMLINK_HOPS = 40;
|
|
35
|
+
/** The `code` of a Node system error, or `undefined` for anything else. */
|
|
36
|
+
function errnoCode(err) {
|
|
37
|
+
const code = err?.code;
|
|
38
|
+
return typeof code === 'string' ? code : undefined;
|
|
39
|
+
}
|
|
32
40
|
/**
|
|
33
41
|
* Fully resolve `abs`: `realpath` the longest existing ancestor and re-append
|
|
34
42
|
* the rest, but never re-append a component that is itself a symlink — read the
|
|
35
43
|
* link and continue from its target instead. This handles paths being created
|
|
36
44
|
* (write/edit) without letting a symlink leaf (e.g. a dangling one pointing
|
|
37
45
|
* outside a confinement root) slip through unresolved.
|
|
46
|
+
*
|
|
47
|
+
* Returns a symlink-free path or throws an errno-carrying error (`ELOOP` for a
|
|
48
|
+
* cycle or more than {@link MAX_SYMLINK_HOPS} links, the `lstat`/`realpath`
|
|
49
|
+
* error for an unreadable component); it never returns `abs` unresolved. Only
|
|
50
|
+
* symlink hops count against the cap, so any depth of not-yet-existing
|
|
51
|
+
* directories still resolves.
|
|
38
52
|
*/
|
|
39
53
|
async function canonicalize(abs) {
|
|
40
54
|
const tail = [];
|
|
@@ -45,30 +59,28 @@ async function canonicalize(abs) {
|
|
|
45
59
|
try {
|
|
46
60
|
real = await fs.realpath(prefix);
|
|
47
61
|
}
|
|
48
|
-
catch {
|
|
49
|
-
let isLink
|
|
62
|
+
catch (realpathErr) {
|
|
63
|
+
let isLink;
|
|
50
64
|
try {
|
|
51
65
|
isLink = (await fs.lstat(prefix)).isSymbolicLink();
|
|
52
66
|
}
|
|
53
|
-
catch {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
throw new ToolError_1.ToolError(`path ${JSON.stringify(abs)} has too many levels of symbolic links`);
|
|
63
|
-
}
|
|
64
|
-
prefix = path.resolve(path.dirname(prefix), await fs.readlink(prefix));
|
|
67
|
+
catch (lstatErr) {
|
|
68
|
+
const code = errnoCode(lstatErr);
|
|
69
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR')
|
|
70
|
+
throw lstatErr;
|
|
71
|
+
const parent = path.dirname(prefix);
|
|
72
|
+
if (parent === prefix)
|
|
73
|
+
throw lstatErr;
|
|
74
|
+
tail.push(path.basename(prefix));
|
|
75
|
+
prefix = parent;
|
|
65
76
|
continue;
|
|
66
77
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
if (!isLink)
|
|
79
|
+
throw realpathErr;
|
|
80
|
+
if (++hops > MAX_SYMLINK_HOPS) {
|
|
81
|
+
throw Object.assign(new Error('too many levels of symbolic links'), { code: 'ELOOP' });
|
|
82
|
+
}
|
|
83
|
+
prefix = path.resolve(path.dirname(prefix), await fs.readlink(prefix));
|
|
72
84
|
continue;
|
|
73
85
|
}
|
|
74
86
|
return tail.length ? path.join(real, ...tail.reverse()) : real;
|
|
@@ -83,6 +95,9 @@ async function canonicalize(abs) {
|
|
|
83
95
|
* leaf, even a dangling one) is resolved before the confinement check, and the
|
|
84
96
|
* resolved path is what the caller then operates on, so a symlink inside `root`
|
|
85
97
|
* that points outside it can neither pass the check nor be followed afterwards.
|
|
98
|
+
* `..` is collapsed lexically before any symlink is followed. A path that cannot
|
|
99
|
+
* be resolved (symlink loop, unreadable component) is rejected with a
|
|
100
|
+
* `ToolError` naming `p`, never the host's absolute path.
|
|
86
101
|
*
|
|
87
102
|
* Residual TOCTOU: a component could still be swapped for a symlink between this
|
|
88
103
|
* call and the eventual `fs` operation. Closing that fully needs per-component
|
|
@@ -95,7 +110,13 @@ async function confineToRoot(root, p, opts) {
|
|
|
95
110
|
const abs = path.resolve(realRoot, p);
|
|
96
111
|
if (allowOutside)
|
|
97
112
|
return abs;
|
|
98
|
-
|
|
113
|
+
let real;
|
|
114
|
+
try {
|
|
115
|
+
real = await canonicalize(abs);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
throw new ToolError_1.ToolError(fsErrorMessage(err, `path ${JSON.stringify(p)}`));
|
|
119
|
+
}
|
|
99
120
|
if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
|
|
100
121
|
throw new ToolError_1.ToolError(`path ${JSON.stringify(p)} escapes workdir`);
|
|
101
122
|
}
|
|
@@ -128,11 +149,12 @@ async function atomicWriteFile(targetPath, content) {
|
|
|
128
149
|
/**
|
|
129
150
|
* Map a thrown filesystem error to a consistent, language-independent message,
|
|
130
151
|
* so the model sees the same wording regardless of the runtime (Node's raw
|
|
131
|
-
* `ENOENT: no such file...` text would otherwise leak through).
|
|
132
|
-
*
|
|
152
|
+
* `ENOENT: no such file...` text would otherwise leak through). Codes we don't
|
|
153
|
+
* special-case render as the bare code, never Node's message, which embeds the
|
|
154
|
+
* host's absolute path.
|
|
133
155
|
*/
|
|
134
156
|
function fsErrorMessage(err, file) {
|
|
135
|
-
const code = err
|
|
157
|
+
const code = errnoCode(err);
|
|
136
158
|
switch (code) {
|
|
137
159
|
case 'ENOENT':
|
|
138
160
|
return `${file}: no such file or directory`;
|
|
@@ -153,7 +175,7 @@ function fsErrorMessage(err, file) {
|
|
|
153
175
|
case 'ENFILE':
|
|
154
176
|
return `${file}: too many open files`;
|
|
155
177
|
default:
|
|
156
|
-
return `${file}: ${
|
|
178
|
+
return `${file}: ${code !== undefined ? `i/o error (${code})` : 'i/o error'}`;
|
|
157
179
|
}
|
|
158
180
|
}
|
|
159
181
|
//# sourceMappingURL=fs-util.js.map
|