sec-default: collector records continue past the user tier

This commit is contained in:
poteat
2026-09-26 14:24:53 -07:00
parent 7779afb12e
commit 3bcaee1f5d
5 changed files with 56 additions and 2 deletions
+4 -2
View File
@@ -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 `$`
+4
View File
@@ -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))
+1
View File
@@ -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 '.'
+16
View File
@@ -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' },
)
},
}
+31
View File
@@ -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",
{