@actagent/file-transfer 2026.6.2
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/actagent.plugin.json +50 -0
- package/index.test.ts +93 -0
- package/index.ts +121 -0
- package/package.json +18 -0
- package/src/node-host/dir-fetch.test.ts +131 -0
- package/src/node-host/dir-fetch.ts +363 -0
- package/src/node-host/dir-list.test.ts +169 -0
- package/src/node-host/dir-list.ts +155 -0
- package/src/node-host/file-fetch.test.ts +254 -0
- package/src/node-host/file-fetch.ts +203 -0
- package/src/node-host/file-write.test.ts +378 -0
- package/src/node-host/file-write.ts +280 -0
- package/src/node-host/path-errors.ts +112 -0
- package/src/shared/audit.ts +98 -0
- package/src/shared/errors.test.ts +63 -0
- package/src/shared/errors.ts +68 -0
- package/src/shared/lazy-node-invoke-policy.test.ts +102 -0
- package/src/shared/lazy-node-invoke-policy.ts +36 -0
- package/src/shared/mime.test.ts +61 -0
- package/src/shared/mime.ts +30 -0
- package/src/shared/node-invoke-policy-commands.ts +9 -0
- package/src/shared/node-invoke-policy.test.ts +763 -0
- package/src/shared/node-invoke-policy.ts +947 -0
- package/src/shared/params.test.ts +42 -0
- package/src/shared/params.ts +60 -0
- package/src/shared/policy.test.ts +568 -0
- package/src/shared/policy.ts +383 -0
- package/src/tools/descriptors.ts +145 -0
- package/src/tools/dir-fetch-tool.test.ts +194 -0
- package/src/tools/dir-fetch-tool.ts +660 -0
- package/src/tools/dir-list-tool.ts +79 -0
- package/src/tools/file-fetch-tool.test.ts +82 -0
- package/src/tools/file-fetch-tool.ts +133 -0
- package/src/tools/file-write-tool.test.ts +30 -0
- package/src/tools/file-write-tool.ts +122 -0
- package/src/tools/node-tool-invoke.ts +97 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// File Transfer tests cover dir fetch tool plugin behavior.
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { validateTarUncompressedBudget } from "./dir-fetch-tool.js";
|
|
9
|
+
|
|
10
|
+
let tmpRoot: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tmpRoot = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "dir-fetch-tool-test-")));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
async function tarDirectory(dir: string): Promise<Buffer> {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const tarBin = process.platform !== "win32" ? "/usr/bin/tar" : "tar";
|
|
23
|
+
const child = spawn(tarBin, ["-czf", "-", "-C", dir, "."], {
|
|
24
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
25
|
+
});
|
|
26
|
+
const chunks: Buffer[] = [];
|
|
27
|
+
let stderr = "";
|
|
28
|
+
child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
29
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
30
|
+
stderr += chunk.toString();
|
|
31
|
+
});
|
|
32
|
+
child.on("close", (code) => {
|
|
33
|
+
if (code !== 0) {
|
|
34
|
+
reject(new Error(`tar exited ${code}: ${stderr}`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
resolve(Buffer.concat(chunks));
|
|
38
|
+
});
|
|
39
|
+
child.on("error", reject);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const testUnlessWindows = process.platform === "win32" ? it.skip : it;
|
|
44
|
+
|
|
45
|
+
function mockTarSpawn(
|
|
46
|
+
script: (
|
|
47
|
+
child: EventEmitter & {
|
|
48
|
+
kill: ReturnType<typeof vi.fn>;
|
|
49
|
+
stderr: EventEmitter;
|
|
50
|
+
stdin: EventEmitter & { end: () => void };
|
|
51
|
+
stdout: EventEmitter;
|
|
52
|
+
},
|
|
53
|
+
) => void,
|
|
54
|
+
) {
|
|
55
|
+
return vi.fn(() => {
|
|
56
|
+
const child = new EventEmitter() as EventEmitter & {
|
|
57
|
+
kill: ReturnType<typeof vi.fn>;
|
|
58
|
+
stderr: EventEmitter;
|
|
59
|
+
stdin: EventEmitter & { end: () => void };
|
|
60
|
+
stdout: EventEmitter;
|
|
61
|
+
};
|
|
62
|
+
child.stdout = new EventEmitter();
|
|
63
|
+
child.stderr = new EventEmitter();
|
|
64
|
+
child.stdin = new EventEmitter() as EventEmitter & { end: () => void };
|
|
65
|
+
child.kill = vi.fn();
|
|
66
|
+
child.stdin.end = () => {
|
|
67
|
+
queueMicrotask(() => script(child));
|
|
68
|
+
};
|
|
69
|
+
return child;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("validateTarUncompressedBudget", () => {
|
|
74
|
+
testUnlessWindows(
|
|
75
|
+
"rejects an archive before extraction when expanded bytes exceed budget",
|
|
76
|
+
async () => {
|
|
77
|
+
await fs.writeFile(path.join(tmpRoot, "zeros.txt"), "0".repeat(128));
|
|
78
|
+
const tarBuffer = await tarDirectory(tmpRoot);
|
|
79
|
+
|
|
80
|
+
await expect(validateTarUncompressedBudget(tarBuffer, 64)).resolves.toEqual({
|
|
81
|
+
ok: false,
|
|
82
|
+
reason: "archive expands past uncompressed budget 64 bytes",
|
|
83
|
+
});
|
|
84
|
+
await expect(validateTarUncompressedBudget(tarBuffer, 256)).resolves.toEqual({
|
|
85
|
+
ok: true,
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("dir.fetch tar validation", () => {
|
|
92
|
+
it("ignores late stdin EPIPE after tar listing has already settled", async () => {
|
|
93
|
+
vi.resetModules();
|
|
94
|
+
vi.doMock("node:child_process", async (importOriginal) => {
|
|
95
|
+
const actual = await importOriginal<typeof import("node:child_process")>();
|
|
96
|
+
return {
|
|
97
|
+
...actual,
|
|
98
|
+
spawn: vi.fn(() => {
|
|
99
|
+
const child = new EventEmitter() as EventEmitter & {
|
|
100
|
+
kill: ReturnType<typeof vi.fn>;
|
|
101
|
+
stderr: EventEmitter;
|
|
102
|
+
stdin: EventEmitter & { end: () => void };
|
|
103
|
+
stdout: EventEmitter;
|
|
104
|
+
};
|
|
105
|
+
const stdout = new EventEmitter();
|
|
106
|
+
const stderr = new EventEmitter();
|
|
107
|
+
const stdin = new EventEmitter() as EventEmitter & { end: () => void };
|
|
108
|
+
child.stdout = stdout;
|
|
109
|
+
child.stderr = stderr;
|
|
110
|
+
child.stdin = stdin;
|
|
111
|
+
child.kill = vi.fn();
|
|
112
|
+
stdin.end = () => {
|
|
113
|
+
queueMicrotask(() => {
|
|
114
|
+
stderr.emit("data", Buffer.from("invalid archive"));
|
|
115
|
+
child.emit("close", 2);
|
|
116
|
+
stdin.emit("error", Object.assign(new Error("write EPIPE"), { code: "EPIPE" }));
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
return child;
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const { testing } = await import("./dir-fetch-tool.js");
|
|
126
|
+
await expect(testing.preValidateTarball(Buffer.from("x"))).resolves.toEqual({
|
|
127
|
+
ok: false,
|
|
128
|
+
reason: "tar -tzf exited 2: invalid archive",
|
|
129
|
+
});
|
|
130
|
+
} finally {
|
|
131
|
+
vi.doUnmock("node:child_process");
|
|
132
|
+
vi.resetModules();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("stops tar name listing once the entry cap is exceeded", async () => {
|
|
137
|
+
vi.resetModules();
|
|
138
|
+
const tarLines = Array.from({ length: 5001 }, (_, index) => `file-${index}`).join("\n") + "\n";
|
|
139
|
+
const spawnMock = mockTarSpawn((child) => {
|
|
140
|
+
child.stdout.emit("data", Buffer.from(tarLines));
|
|
141
|
+
});
|
|
142
|
+
vi.doMock("node:child_process", async (importOriginal) => {
|
|
143
|
+
const actual = await importOriginal<typeof import("node:child_process")>();
|
|
144
|
+
return {
|
|
145
|
+
...actual,
|
|
146
|
+
spawn: spawnMock,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const { testing } = await import("./dir-fetch-tool.js");
|
|
152
|
+
await expect(testing.preValidateTarball(Buffer.from("x"))).resolves.toEqual({
|
|
153
|
+
ok: false,
|
|
154
|
+
reason: "archive contains 5001 entries; limit 5000",
|
|
155
|
+
});
|
|
156
|
+
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
157
|
+
const child = spawnMock.mock.results[0]?.value;
|
|
158
|
+
expect(child?.kill).toHaveBeenCalledWith("SIGKILL");
|
|
159
|
+
} finally {
|
|
160
|
+
vi.doUnmock("node:child_process");
|
|
161
|
+
vi.resetModules();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("keeps recent tar stderr when listing fails noisily", async () => {
|
|
166
|
+
vi.resetModules();
|
|
167
|
+
const oldNoise = "old-noise\n".repeat(600);
|
|
168
|
+
const recent = "recent-invalid-archive-details\n".repeat(12);
|
|
169
|
+
vi.doMock("node:child_process", async (importOriginal) => {
|
|
170
|
+
const actual = await importOriginal<typeof import("node:child_process")>();
|
|
171
|
+
return {
|
|
172
|
+
...actual,
|
|
173
|
+
spawn: mockTarSpawn((child) => {
|
|
174
|
+
child.stderr.emit("data", Buffer.from(oldNoise));
|
|
175
|
+
child.stderr.emit("data", Buffer.from(recent));
|
|
176
|
+
child.emit("close", 2);
|
|
177
|
+
}),
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const { testing } = await import("./dir-fetch-tool.js");
|
|
183
|
+
const result = await testing.preValidateTarball(Buffer.from("x"));
|
|
184
|
+
expect(result.ok).toBe(false);
|
|
185
|
+
if (!result.ok) {
|
|
186
|
+
expect(result.reason).toContain(recent.slice(-200));
|
|
187
|
+
expect(result.reason).not.toContain(oldNoise.slice(0, 40));
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
vi.doUnmock("node:child_process");
|
|
191
|
+
vi.resetModules();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|