Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions extensions/shared/child-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,10 @@ export const CHILD_EXCLUDED_TOOL_NAMES = [
"bg_watch",
// context-pivot — compaction of the conversation is a parent-only decision
"context_pivot",
// pi-intercom — blockedPackageSources removes this package from concurrent
// child sessions to avoid process.env identity cross-wiring; child allowlists
// must not leak its tool name
"intercom",
] as const;

const PARENT_ONLY_OPENPI_EXTENSION_PATHS = new Set(
Expand Down
52 changes: 50 additions & 2 deletions tests/extensions/shared/child-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1519,8 +1519,13 @@ test("every registered package tool is classified child-safe or excluded (fail-c
}

// The exclusion list must not name tools the package no longer registers
// (except structured_output, a workflow-child tool registered dynamically).
// (except foreign tools from blocked packages like intercom, or dynamically registered child tools).
assert.ok(
excluded.has("intercom"),
"CHILD_EXCLUDED_TOOL_NAMES must include intercom to prevent inheritance leakage",
);
for (const name of excluded) {
if (name === "intercom") continue;
assert.ok(
registered.has(name),
`excluded tool "${name}" is no longer registered; remove it from CHILD_EXCLUDED_TOOL_NAMES`,
Expand Down Expand Up @@ -1557,7 +1562,14 @@ test("git-info exclusion: ENOENT degrades, other errors fail closed", async () =
});

test("child delegation inherits active tools and custom restrictions only narrow", () => {
const parent = ["read", "bash", "web_search", "workflow", "subagent_spawn"];
const parent = [
"read",
"bash",
"web_search",
"workflow",
"subagent_spawn",
"intercom",
];
assert.deepEqual(inheritedChildToolAllowlist(parent), [
"read",
"bash",
Expand All @@ -1569,9 +1581,45 @@ test("child delegation inherits active tools and custom restrictions only narrow
"rg",
"web_search",
"workflow",
"intercom",
]),
["read", "web_search"],
);
assert.deepEqual(inheritedChildToolAllowlist(parent, []), []);
assert.deepEqual(inheritedChildToolAllowlist([], ["bash"]), []);
});

test("tools from blocked packages like pi-intercom are excluded from child allowlist and pass preflight", async () => {
// Simulate a parent session where pi-intercom is active alongside native tools
const parentActiveTools = ["read", "bash", "edit", "intercom"];

// 1. Inherited allowlist must drop intercom
const inherited = inheritedChildToolAllowlist(parentActiveTools);
assert.deepEqual(inherited, ["read", "bash", "edit"]);
assert.equal(inherited.includes("intercom"), false);

// 2. An explicit role allowlist naming intercom must still drop it
const explicitNarrowed = inheritedChildToolAllowlist(parentActiveTools, [
"read",
"intercom",
]);
assert.deepEqual(explicitNarrowed, ["read"]);

// 3. Child tool policy must explicitly exclude intercom
const policy = childToolPolicy(inherited);
assert.ok(policy.excludeTools.includes("intercom"));

// 4. bindChildSessionExtensions preflight must pass even though child session lacks intercom
const mockChildSession = {
async bindExtensions() {},
getActiveToolNames: () => ["read", "bash", "edit"],
getAllTools: () => [{ name: "read" }, { name: "bash" }, { name: "edit" }],
setActiveToolsByName(_names: string[]) {},
};

// Passing the parent tools containing intercom directly to preflight should not throw,
// because effectiveChildToolAllowlist filters it out before checking child availability.
await assert.doesNotReject(
bindChildSessionExtensions(mockChildSession, parentActiveTools),
);
});
5 changes: 4 additions & 1 deletion tests/extensions/shared/tool-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,11 @@ test("an unloaded capability remembers lifecycle state without exposing its tool
});

test("visibility catalog and child-session policy classify the same package tools", () => {
const packageExcluded = CHILD_EXCLUDED_TOOL_NAMES.filter(
(name) => name !== "intercom",
);
assert.deepEqual(
[...OPENPI_TOOL_SURFACE_NAMES].sort(),
[...CHILD_SAFE_PACKAGE_TOOL_NAMES, ...CHILD_EXCLUDED_TOOL_NAMES].sort(),
[...CHILD_SAFE_PACKAGE_TOOL_NAMES, ...packageExcluded].sort(),
);
});
5 changes: 3 additions & 2 deletions tests/extensions/subagents/agent-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,14 +521,14 @@ test("every key the parser reads is accepted without a warning", () => {
});

test("naming a parent-only tool says so instead of calling it a typo", () => {
// subagent_spawn is real and correctly spelled; it is denied. Reporting it
// subagent_spawn and intercom are real and correctly spelled; they are denied. Reporting it
// as "unrecognized … a typo here silently removes a capability" said the
// opposite of what happened.
const parsed = parseAgentType(
`---
name: helper
description: Tries to delegate.
tools: [read, subagent_spawn, gerp]
tools: [read, subagent_spawn, intercom, gerp]
---

Body.
Expand All @@ -538,6 +538,7 @@ Body.
);
const messages = parsed.diagnostics.map((d) => d.message).join("\n");
assert.match(messages, /"subagent_spawn" in helper is a parent-only tool/);
assert.match(messages, /"intercom" in helper is a parent-only tool/);
assert.match(messages, /unrecognized tool "gerp"/);
});

Expand Down
Loading