@crewhaus/tool-task 0.1.1 → 0.1.3
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/package.json +14 -19
- package/src/index.test.ts +40 -0
- package/src/index.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-task",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Task(description, prompt, subagent_type?) tool — spawns a sub-agent and returns its final message (Section 13)",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -12,24 +12,24 @@
|
|
|
12
12
|
"test": "bun test src"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@crewhaus/agent-context-isolation": "0.1.
|
|
16
|
-
"@crewhaus/errors": "0.1.
|
|
17
|
-
"@crewhaus/sub-agent-permission-inheritance": "0.1.
|
|
18
|
-
"@crewhaus/tool-builder": "0.1.
|
|
19
|
-
"@crewhaus/tool-catalog": "0.1.
|
|
15
|
+
"@crewhaus/agent-context-isolation": "0.1.3",
|
|
16
|
+
"@crewhaus/errors": "0.1.3",
|
|
17
|
+
"@crewhaus/sub-agent-permission-inheritance": "0.1.3",
|
|
18
|
+
"@crewhaus/tool-builder": "0.1.3",
|
|
19
|
+
"@crewhaus/tool-catalog": "0.1.3",
|
|
20
20
|
"yaml": "^2.6.0",
|
|
21
21
|
"zod": "^3.23.8"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@crewhaus/event-log": "0.1.
|
|
25
|
-
"@crewhaus/permission-engine": "0.1.
|
|
26
|
-
"@crewhaus/run-context": "0.1.
|
|
24
|
+
"@crewhaus/event-log": "0.1.3",
|
|
25
|
+
"@crewhaus/permission-engine": "0.1.3",
|
|
26
|
+
"@crewhaus/run-context": "0.1.3"
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"author": {
|
|
30
30
|
"name": "Max Meier",
|
|
31
|
-
"email": "max@
|
|
32
|
-
"url": "https://
|
|
31
|
+
"email": "max@crewhaus.ai",
|
|
32
|
+
"url": "https://crewhaus.ai"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
@@ -41,12 +41,7 @@
|
|
|
41
41
|
"url": "https://github.com/crewhaus/factory/issues"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
|
-
"access": "
|
|
45
|
-
},
|
|
46
|
-
"files": [
|
|
47
|
-
"src",
|
|
48
|
-
"README.md",
|
|
49
|
-
"LICENSE",
|
|
50
|
-
"NOTICE"
|
|
51
|
-
]
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"files": ["src", "README.md", "LICENSE", "NOTICE"]
|
|
52
47
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -177,6 +177,46 @@ disk instructions`,
|
|
|
177
177
|
resolveSubAgentDefinition("nonexistent", { subAgentDir: "/tmp/does-not-exist-xyz" }),
|
|
178
178
|
).toThrow(/unknown subagent_type/);
|
|
179
179
|
});
|
|
180
|
+
|
|
181
|
+
// SECURITY: subagent_type is model-filled and becomes `join(dir, name+".md")`,
|
|
182
|
+
// so an unsanitized `..`/separator escapes the sub-agent dir and loads an
|
|
183
|
+
// arbitrary host .md as a sub-agent definition. Must be rejected.
|
|
184
|
+
test.each([
|
|
185
|
+
"../../../../etc/passwd",
|
|
186
|
+
"../sibling",
|
|
187
|
+
"foo/bar",
|
|
188
|
+
"foo\\bar",
|
|
189
|
+
"..",
|
|
190
|
+
".",
|
|
191
|
+
"a/../../b",
|
|
192
|
+
])("rejects path-traversal subagent_type %p", (evil) => {
|
|
193
|
+
expect(() =>
|
|
194
|
+
resolveSubAgentDefinition(evil, { subAgentDir: "/tmp/does-not-exist-xyz" }),
|
|
195
|
+
).toThrow(/invalid subagent_type/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("path-traversal is rejected even when an inline map is present (disk path is the sink)", () => {
|
|
199
|
+
expect(() =>
|
|
200
|
+
resolveSubAgentDefinition("../../tmp/evil", {
|
|
201
|
+
subAgents: new Map(),
|
|
202
|
+
subAgentDir: "/tmp/does-not-exist-xyz",
|
|
203
|
+
}),
|
|
204
|
+
).toThrow(/invalid subagent_type/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("ordinary names with dots that are not traversal still resolve via disk", () => {
|
|
208
|
+
const dir = newTempDir();
|
|
209
|
+
try {
|
|
210
|
+
writeFileSync(
|
|
211
|
+
join(dir, "my.agent.md"),
|
|
212
|
+
"---\nname: my.agent\ndescription: d\n---\ndo the thing\n",
|
|
213
|
+
);
|
|
214
|
+
const out = resolveSubAgentDefinition("my.agent", { subAgentDir: dir });
|
|
215
|
+
expect(out.name).toBe("my.agent");
|
|
216
|
+
} finally {
|
|
217
|
+
rmSync(dir, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
180
220
|
});
|
|
181
221
|
|
|
182
222
|
describe("createTaskTool — execute round-trip", () => {
|
package/src/index.ts
CHANGED
|
@@ -143,6 +143,27 @@ function loadSubAgentFromDisk(name: string, dir: string): SubAgentDefinition | n
|
|
|
143
143
|
return parseSubAgentFile(content, name);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* `subagent_type` is a model-filled tool argument and model output is
|
|
148
|
+
* attacker-steerable (via any untrusted input). It becomes a filesystem path
|
|
149
|
+
* segment in `loadSubAgentFromDisk` (`join(dir, `${name}.md`)`), and `join`
|
|
150
|
+
* resolves `..` and path separators — so an unsanitized name like
|
|
151
|
+
* `../../../tmp/evil` reads an arbitrary `.md` on the host and loads it as a
|
|
152
|
+
* sub-agent definition. Reject anything that isn't a single safe path
|
|
153
|
+
* component before it reaches the disk lookup. Inline spec-supplied names are
|
|
154
|
+
* exact-key map lookups that never touch the filesystem, so they are resolved
|
|
155
|
+
* first (above) and are unaffected.
|
|
156
|
+
*/
|
|
157
|
+
function isUnsafeSubAgentName(name: string): boolean {
|
|
158
|
+
return (
|
|
159
|
+
name.includes("/") ||
|
|
160
|
+
name.includes("\\") ||
|
|
161
|
+
name.includes("\0") ||
|
|
162
|
+
name === "." ||
|
|
163
|
+
name === ".."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
146
167
|
export type CreateTaskToolOptions = {
|
|
147
168
|
/** Inline spec-supplied sub-agent definitions (highest priority). */
|
|
148
169
|
readonly subAgents?: ReadonlyMap<string, SubAgentDefinition>;
|
|
@@ -160,6 +181,13 @@ export function resolveSubAgentDefinition(
|
|
|
160
181
|
const inline = opts.subAgents.get(resolveName);
|
|
161
182
|
if (inline !== undefined) return inline;
|
|
162
183
|
}
|
|
184
|
+
// Guard the disk lookup against path traversal (the inline map above is an
|
|
185
|
+
// exact-key lookup and is intentionally not gated).
|
|
186
|
+
if (isUnsafeSubAgentName(resolveName)) {
|
|
187
|
+
throw new SubAgentResolutionError(
|
|
188
|
+
`invalid subagent_type "${resolveName}" — must not contain path separators, "..", ".", or NUL bytes`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
163
191
|
const dir = opts.subAgentDir ?? join(process.cwd(), ".crewhaus", "sub-agents");
|
|
164
192
|
const onDisk = loadSubAgentFromDisk(resolveName, dir);
|
|
165
193
|
if (onDisk !== null) return onDisk;
|