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
6 changes: 4 additions & 2 deletions mods/sec-default/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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 `$`

Expand Down
4 changes: 4 additions & 0 deletions mods/sec-default/hooks/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
13 changes: 13 additions & 0 deletions mods/sec-default/tests/fixtures/export-command.ts
Original file line number Diff line number Diff line change
@@ -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,
}
18 changes: 18 additions & 0 deletions mods/sec-default/tests/fixtures/exporting.ts
Original file line number Diff line number Diff line change
@@ -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' }
})
},
}
3 changes: 3 additions & 0 deletions mods/sec-default/tests/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -28,5 +30,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 changes: 16 additions & 0 deletions mods/sec-default/tests/fixtures/withholding.ts
Original file line number Diff line number Diff line change
@@ -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.event === 'withheld'
? { deny: 'kept from the collector' }
: next({ ...e, props: { ...e.props, edited: true } }),
)
},
}
20 changes: 20 additions & 0 deletions mods/sec-default/tests/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,26 @@ describe('register', () => {
},
)

test(
'a collector record passes over the plugins the person installed',
{ plugins: [Fixtures.withholding, Fixtures.exporting] },
async ($, on) => {
const reached: unknown[] = []
on('telemetry.log', { to: 'collector' }, ($, e) => {
reached.push([e.event, e.props?.edited])

return { value: undefined }
})

await $.command.run(Fixtures.EXPORT_COMMAND)

expect(reached).toEqual([
['kept', undefined],
['withheld', undefined],
])
},
)

test(
"a user plugin's rewrite of policy is skipped for every other reader",
{
Expand Down
Loading