From 3bcaee1f5d70bb3ccdfe5b1180a0f30e8f2b0780 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 26 Sep 2026 14:24:53 -0700 Subject: [PATCH 1/2] sec-default: collector records continue past the user tier --- mods/sec-default/README.md | 6 ++-- mods/sec-default/hooks/register.ts | 4 +++ mods/sec-default/tests/fixtures/index.ts | 1 + .../sec-default/tests/fixtures/withholding.ts | 16 ++++++++++ mods/sec-default/tests/register.test.ts | 31 +++++++++++++++++++ 5 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 mods/sec-default/tests/fixtures/withholding.ts diff --git a/mods/sec-default/README.md b/mods/sec-default/README.md index 7152f7e96f..158fd8248e 100644 --- a/mods/sec-default/README.md +++ b/mods/sec-default/README.md @@ -26,6 +26,7 @@ settings it decides by. | `classic.*` | Continue past the user tier: the organization's settings hooks see the engine's input and their answer stands. | | `prompt.section`, `prompt.context`, `skill.prompt`, `attribution.text` | Continue past the user tier: managed CLAUDE.md, rules and policy skills reach the model as written. A person's plugins keep `prompt.submit` and its additive context. | | `settings.read` | Continue past the user tier: no user hook rewrites what any caller reads as settings, this plugin's own policy reads included. | +| `telemetry.log` `{ to: "collector" }` | Continue past the user tier: no plugin a person installed drops or rewrites a record on its way to the collector the organization configured. The organization's own plugins, in `prepend` and `append`, still may. | | `tool.describe`, `command.describe`, `agent.offer`, `agent.spawn` | When the subject's pinned `e.provider.tier` is `prepend` or `append` (a policy-installed plugin, the managed folder, a policy MCP server), continue past the user tier; a subject provided by `user`, `builtin` or `core` passes. | | `tool.register` | A caller in `prepend` or `append` continues past the user tier. A `user`-tier caller is refused by name while managed settings hold `allowedMcpServers` (set at all, empty included); otherwise it passes. | | `tool.list` | The tools of the organization's managed MCP servers are listed as the organization's tiers listed them; every other tool as the user tier left it. With no policy to read, or a refusal from either listing, the organization's listing stands whole. | @@ -34,8 +35,9 @@ settings it decides by. ## What it hooks `classic.*`, `prompt.section`, `prompt.context`, `skill.prompt`, -`attribution.text`, `settings.read`, `tool.describe`, `command.describe`, -`agent.offer`, `agent.spawn`, `tool.register`, `tool.list`. +`attribution.text`, `settings.read`, `telemetry.log` (the collector's +stream), `tool.describe`, `command.describe`, `agent.offer`, `agent.spawn`, +`tool.register`, `tool.list`. ## What it calls on `$` diff --git a/mods/sec-default/hooks/register.ts b/mods/sec-default/hooks/register.ts index 06e0417479..79802e1c17 100644 --- a/mods/sec-default/hooks/register.ts +++ b/mods/sec-default/hooks/register.ts @@ -26,6 +26,10 @@ export function register(on: On) { on('settings.read', ($, e, next) => next.to(e, 'append')) + on('telemetry.log', { to: 'collector' }, ($, e, next) => + next.to(e, 'append'), + ) + on('tool.describe', ($, e, next) => pastUsers(e, next)) on('command.describe', ($, e, next) => pastUsers(e, next)) on('agent.offer', ($, e, next) => pastUsers(e, next)) diff --git a/mods/sec-default/tests/fixtures/index.ts b/mods/sec-default/tests/fixtures/index.ts index 7dacbf05cc..9d50f76370 100644 --- a/mods/sec-default/tests/fixtures/index.ts +++ b/mods/sec-default/tests/fixtures/index.ts @@ -28,5 +28,6 @@ export * from './tools.js' export * from './tools-command.js' export * from './tools-registered.js' export * from './user-reachable-providers.js' +export * from './withholding.js' export * as default from '.' diff --git a/mods/sec-default/tests/fixtures/withholding.ts b/mods/sec-default/tests/fixtures/withholding.ts new file mode 100644 index 0000000000..ad25573645 --- /dev/null +++ b/mods/sec-default/tests/fixtures/withholding.ts @@ -0,0 +1,16 @@ +import type { Plugin } from 'claude-code/testing' + +/** + * A plugin the person installed that keeps the collector record named + * `withheld` from the collector and marks every other one `edited`. + */ +export const withholding: Plugin = { + name: 'withholding', + register(on) { + on('telemetry.log', { to: 'collector' }, ($, e, next) => + e.to === 'collector' && e.event !== 'withheld' + ? next({ ...e, attributes: { ...e.attributes, edited: true } }) + : { deny: 'kept from the collector' }, + ) + }, +} diff --git a/mods/sec-default/tests/register.test.ts b/mods/sec-default/tests/register.test.ts index 08cb85d9b5..d375609aaf 100644 --- a/mods/sec-default/tests/register.test.ts +++ b/mods/sec-default/tests/register.test.ts @@ -137,6 +137,37 @@ describe('register', () => { }, ) + test( + 'a collector record passes over the plugins the person installed', + { plugins: [Fixtures.withholding] }, + async ($, on) => { + const reached: unknown[] = [] + on('telemetry.log', { to: 'collector' }, ($, e) => { + reached.push([e.event, e.to === 'collector' && e.attributes.edited]) + + return { value: undefined } + }) + + await $.telemetry.log({ + to: 'collector', + event: 'kept', + attributes: {}, + loggedAt: '2026-09-26T10:00:00.000Z', + }) + await $.telemetry.log({ + to: 'collector', + event: 'withheld', + attributes: {}, + loggedAt: '2026-09-26T10:00:01.000Z', + }) + + expect(reached).toEqual([ + ['kept', undefined], + ['withheld', undefined], + ]) + }, + ) + test( "a user plugin's rewrite of policy is skipped for every other reader", { From d825ab00421a9731f76874da826ac14e13174234 Mon Sep 17 00:00:00 2001 From: poteat Date: Sun, 27 Sep 2026 15:20:17 -0700 Subject: [PATCH 2/2] sec-default: the collector row's test logs through a built-in's command and reads the entry as the telemetry types give it --- .../tests/fixtures/export-command.ts | 13 +++++++++++++ mods/sec-default/tests/fixtures/exporting.ts | 18 ++++++++++++++++++ mods/sec-default/tests/fixtures/index.ts | 2 ++ mods/sec-default/tests/fixtures/withholding.ts | 6 +++--- mods/sec-default/tests/register.test.ts | 17 +++-------------- 5 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 mods/sec-default/tests/fixtures/export-command.ts create mode 100644 mods/sec-default/tests/fixtures/exporting.ts diff --git a/mods/sec-default/tests/fixtures/export-command.ts b/mods/sec-default/tests/fixtures/export-command.ts new file mode 100644 index 0000000000..85dde3088f --- /dev/null +++ b/mods/sec-default/tests/fixtures/export-command.ts @@ -0,0 +1,13 @@ +import type { CommandRunInput } from 'claude-code' + +import { FULLSCREEN } from './fullscreen.js' + +/** + * `/export` as the person types it: the exporting plugin's command. + */ +export const EXPORT_COMMAND: CommandRunInput = { + command: 'export', + args: '', + origin: { kind: 'composer' }, + presentation: FULLSCREEN, +} diff --git a/mods/sec-default/tests/fixtures/exporting.ts b/mods/sec-default/tests/fixtures/exporting.ts new file mode 100644 index 0000000000..e8d1781efd --- /dev/null +++ b/mods/sec-default/tests/fixtures/exporting.ts @@ -0,0 +1,18 @@ +import type { Plugin } from 'claude-code/testing' + +/** + * A built-in whose `/export` logs two records for the collector, `kept` and + * `withheld`, and says so. + */ +export const exporting: Plugin = { + name: 'exporting', + tier: 'builtin', + register(on) { + on('command.run', { command: 'export' }, async $ => { + await $.telemetry.log({ to: 'collector', event: 'kept' }) + await $.telemetry.log({ to: 'collector', event: 'withheld' }) + + return { text: 'exported' } + }) + }, +} diff --git a/mods/sec-default/tests/fixtures/index.ts b/mods/sec-default/tests/fixtures/index.ts index 9d50f76370..c305901082 100644 --- a/mods/sec-default/tests/fixtures/index.ts +++ b/mods/sec-default/tests/fixtures/index.ts @@ -4,6 +4,8 @@ export * from './allowlist.js' export * from './command-described.js' export * from './denying.js' export * from './dropping.js' +export * from './export-command.js' +export * from './exporting.js' export * from './fullscreen.js' export * from './listing.js' export * from './managed-policy.js' diff --git a/mods/sec-default/tests/fixtures/withholding.ts b/mods/sec-default/tests/fixtures/withholding.ts index ad25573645..0c0a2983da 100644 --- a/mods/sec-default/tests/fixtures/withholding.ts +++ b/mods/sec-default/tests/fixtures/withholding.ts @@ -8,9 +8,9 @@ export const withholding: Plugin = { name: 'withholding', register(on) { on('telemetry.log', { to: 'collector' }, ($, e, next) => - e.to === 'collector' && e.event !== 'withheld' - ? next({ ...e, attributes: { ...e.attributes, edited: true } }) - : { deny: 'kept from the collector' }, + e.event === 'withheld' + ? { deny: 'kept from the collector' } + : next({ ...e, props: { ...e.props, edited: true } }), ) }, } diff --git a/mods/sec-default/tests/register.test.ts b/mods/sec-default/tests/register.test.ts index d375609aaf..33ea4dbed7 100644 --- a/mods/sec-default/tests/register.test.ts +++ b/mods/sec-default/tests/register.test.ts @@ -139,27 +139,16 @@ describe('register', () => { test( 'a collector record passes over the plugins the person installed', - { plugins: [Fixtures.withholding] }, + { plugins: [Fixtures.withholding, Fixtures.exporting] }, async ($, on) => { const reached: unknown[] = [] on('telemetry.log', { to: 'collector' }, ($, e) => { - reached.push([e.event, e.to === 'collector' && e.attributes.edited]) + reached.push([e.event, e.props?.edited]) return { value: undefined } }) - await $.telemetry.log({ - to: 'collector', - event: 'kept', - attributes: {}, - loggedAt: '2026-09-26T10:00:00.000Z', - }) - await $.telemetry.log({ - to: 'collector', - event: 'withheld', - attributes: {}, - loggedAt: '2026-09-26T10:00:01.000Z', - }) + await $.command.run(Fixtures.EXPORT_COMMAND) expect(reached).toEqual([ ['kept', undefined],