(
+ followKey,
+ running,
+ expanded,
+ );
+
if (trace.entries.length === 0) {
return null;
}
@@ -131,7 +146,12 @@ export default function PtcToolTrace({
{localize('com_ui_ptc_trace_title')}
-
+
{/* The retained tail is a window, not the whole program — say so
rather than let the reader assume these are all the calls. */}
{trace.dropped > 0 && (
diff --git a/packages/api/src/agents/handlers.ts b/packages/api/src/agents/handlers.ts
index fa0bcd34bf..4d5cf8428c 100644
--- a/packages/api/src/agents/handlers.ts
+++ b/packages/api/src/agents/handlers.ts
@@ -750,6 +750,35 @@ function filteredToolArgumentsResult(
}
}
+/**
+ * Inner tool names the `name` PII policy would block. `filteredToolArgumentsResult`
+ * inspects `tc.name` for direct calls, but inner calls bypass it entirely — and
+ * the trace event carries the name unconditionally, so without this the trace
+ * becomes the disclosure path the policy exists to close. The eligible map holds
+ * a handful of names, each inspected once per PTC call.
+ */
+function collectFilteredPtcToolNames(
+ names: Iterable,
+ req: ServerRequest | undefined,
+): ReadonlySet | undefined {
+ const filters = req?.config?.filters;
+ if (filters == null || !hasActivePiiFields(filters.toolArguments?.pii, ['name'])) {
+ return undefined;
+ }
+ const blocked = new Set();
+ for (const name of names) {
+ try {
+ if (inspectContent(extractToolArgumentContent({ name }), { filters }) != null) {
+ blocked.add(name);
+ }
+ } catch {
+ /* An un-inspectable name is treated as blocked: fail closed. */
+ blocked.add(name);
+ }
+ }
+ return blocked.size > 0 ? blocked : undefined;
+}
+
function filteredToolOutputResult(
tc: ToolCallRequest,
req: ServerRequest | undefined,
@@ -5036,9 +5065,8 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand
* the sandbox bridge invokes them directly — so when the
* deployment filters tool arguments for PII, the trace
* must not put their values on the wire. */
- const ptcArgumentPii = (
- mergedConfigurable?.req as ServerRequest | undefined
- )?.config?.filters?.toolArguments?.pii;
+ const ptcReq = mergedConfigurable?.req as ServerRequest | undefined;
+ const ptcArgumentPii = ptcReq?.config?.filters?.toolArguments?.pii;
toolCallConfig.toolMap = emitPtcProgress
? instrumentPtcToolMap({
toolMap: eligiblePtcToolMap,
@@ -5051,6 +5079,10 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand
'arguments',
'output',
]),
+ traceExclusions: collectFilteredPtcToolNames(
+ eligiblePtcToolMap.keys(),
+ ptcReq,
+ ),
emit: emitPtcProgress,
})
: eligiblePtcToolMap;
diff --git a/packages/api/src/agents/ptc.spec.ts b/packages/api/src/agents/ptc.spec.ts
index 09ee1595db..3a3f04179a 100644
--- a/packages/api/src/agents/ptc.spec.ts
+++ b/packages/api/src/agents/ptc.spec.ts
@@ -210,6 +210,28 @@ describe('instrumentPtcToolMap', () => {
expect(JSON.stringify(events)).not.toContain('555-01-0000');
});
+ it('emits nothing at all for a tool whose name the policy filters', async () => {
+ const { events, emit } = collect();
+ const toolMap = new Map([
+ ['ok_tool', createTool('ok_tool', async () => 'ok')],
+ ['blocked_name_tool', createTool('blocked_name_tool', async () => 'ok')],
+ ]);
+
+ const instrumented = instrumentPtcToolMap({
+ toolMap,
+ toolCallId: 'call_1',
+ traceExclusions: new Set(['blocked_name_tool']),
+ emit,
+ });
+
+ /** Excluded tools still execute — only their telemetry is suppressed. */
+ await expect(instrumented.get('blocked_name_tool')?.invoke({ a: 1 })).resolves.toBe('ok');
+ await instrumented.get('ok_tool')?.invoke({ a: 1 });
+
+ expect(events.map((e) => e.name)).toEqual(['ok_tool', 'ok_tool']);
+ expect(JSON.stringify(events)).not.toContain('blocked_name_tool');
+ });
+
it('runs the inner call even when the emitter throws', async () => {
const toolMap = new Map([['read_file', createTool('read_file', async () => 'ok')]]);
const instrumented = instrumentPtcToolMap({
diff --git a/packages/api/src/agents/ptc.ts b/packages/api/src/agents/ptc.ts
index a56e3f1221..aaa47d274f 100644
--- a/packages/api/src/agents/ptc.ts
+++ b/packages/api/src/agents/ptc.ts
@@ -95,6 +95,14 @@ export interface InstrumentPtcToolMapParams {
* argument that caused it. The trace still reports name, status and duration.
*/
includePreviews?: boolean;
+ /**
+ * Inner tools whose *name* trips the deployment's PII policy. The event
+ * carries the tool name unconditionally, so a name the `name` filter would
+ * have blocked on a direct call cannot be allowed to ride the trace instead.
+ * These tools still execute — they are simply left unwrapped, so no event
+ * about them is ever emitted.
+ */
+ traceExclusions?: ReadonlySet;
emit: (event: PtcToolCallEvent) => void;
}
@@ -111,6 +119,7 @@ export function instrumentPtcToolMap({
toolCallId,
runId,
includePreviews = true,
+ traceExclusions,
emit,
}: InstrumentPtcToolMapParams): Map {
let sequence = 0;
@@ -125,6 +134,10 @@ export function instrumentPtcToolMap({
const instrumented = new Map();
for (const [name, tool] of toolMap) {
+ if (traceExclusions?.has(name)) {
+ instrumented.set(name, tool);
+ continue;
+ }
instrumented.set(
name,
new Proxy(tool, {