mirror of
https://github.com/anthropics/claude-code.git
synced 2026-10-02 05:25:05 +08:00
sec-default: the rows a conversation keeps continue past the user tier; the declarations carry session.append
This commit is contained in:
@@ -25,17 +25,19 @@ 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. |
|
||||
| `session.append` | Continue past the user tier: the rows a conversation keeps (a settings hook's context among them) are stored and sent as the organization's tiers and the built-ins left them. A person's plugins keep `prompt.submit`, `tool.call` and the other events that shape a row before it is kept. |
|
||||
| `settings.read` | Continue past the user tier: no user hook rewrites what any caller reads as settings, this plugin's own policy reads included. |
|
||||
| `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. |
|
||||
| everything else | Passes: `prompt.submit`, `turn.*`, `tool.call`, `tool.check`, `command.run`, `command.register`, `session.*`, `ui.*`, `fs.*`, `http.fetch`, `process.run`, `store.*`, `clock.*`, `model.*`, `mcp.call`, `audio.*`, `agent.list`, `engine.create`. |
|
||||
| everything else | Passes: `prompt.submit`, `turn.*`, `tool.call`, `tool.check`, `command.run`, `command.register`, `session.*` other than `session.append`, `ui.*`, `fs.*`, `http.fetch`, `process.run`, `store.*`, `clock.*`, `model.*`, `mcp.call`, `audio.*`, `agent.list`, `engine.create`. |
|
||||
|
||||
## 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`.
|
||||
`agent.offer`, `agent.spawn`, `tool.register`, `tool.list`,
|
||||
`session.append`.
|
||||
|
||||
## What it calls on `$`
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export function register(on: On) {
|
||||
on('prompt.context', ($, e, next) => next.to(e, 'append'))
|
||||
on('skill.prompt', ($, e, next) => next.to(e, 'append'))
|
||||
on('attribution.text', ($, e, next) => next.to(e, 'append'))
|
||||
on('session.append', ($, e, next) => next.to(e, 'append'))
|
||||
|
||||
on('settings.read', ($, e, next) => next.to(e, 'append'))
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ApiContentBlock } from 'claude-code'
|
||||
|
||||
/**
|
||||
* The text block the organization's own plugin adds to a row it keeps.
|
||||
*/
|
||||
export const COUNTERSIGNATURE: ApiContentBlock = {
|
||||
type: 'text',
|
||||
text: '(countersigned)',
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Plugin } from 'claude-code/testing'
|
||||
|
||||
import { COUNTERSIGNATURE } from './countersignature.js'
|
||||
|
||||
/**
|
||||
* The organization's own plugin, in its last tier, which countersigns every
|
||||
* row the conversation keeps.
|
||||
*/
|
||||
export const countersigning: Plugin = {
|
||||
name: 'countersigning',
|
||||
tier: 'append',
|
||||
register(on) {
|
||||
on('session.append', ($, e, next) =>
|
||||
next({
|
||||
...e,
|
||||
message: {
|
||||
...e.message,
|
||||
content: [...e.message.content, COUNTERSIGNATURE],
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { SessionAppendInput } from 'claude-code'
|
||||
|
||||
/**
|
||||
* The context an organization's settings hook attached for the model, as the
|
||||
* engine raises the row before keeping it.
|
||||
*/
|
||||
export const HOOK_CONTEXT_ROW: SessionAppendInput = {
|
||||
message: {
|
||||
type: 'attachment',
|
||||
name: 'hook_additional_context',
|
||||
role: 'user',
|
||||
isMeta: true,
|
||||
content: [{ type: 'text', text: 'the org says: ask before deploying' }],
|
||||
},
|
||||
door: 'hook-context',
|
||||
origin: { kind: 'hook', event: 'UserPromptSubmit' },
|
||||
uuid: '6f0d3c1e-5b7a-4c2e-9a41-2d8e7f3b9c10',
|
||||
}
|
||||
+4
@@ -2,9 +2,12 @@ export * from './agent-offered.js'
|
||||
export * from './agent-spawned.js'
|
||||
export * from './allowlist.js'
|
||||
export * from './command-described.js'
|
||||
export * from './countersignature.js'
|
||||
export * from './countersigning.js'
|
||||
export * from './denying.js'
|
||||
export * from './dropping.js'
|
||||
export * from './fullscreen.js'
|
||||
export * from './hook-context-row.js'
|
||||
export * from './listing.js'
|
||||
export * from './managed-policy.js'
|
||||
export * from './marking.js'
|
||||
@@ -18,6 +21,7 @@ export * from './reading.js'
|
||||
export * from './registered-tool-of.js'
|
||||
export * from './registering.js'
|
||||
export * from './relabeling.js'
|
||||
export * from './rewording.js'
|
||||
export * from './server-policy.js'
|
||||
export * from './session.js'
|
||||
export * from './signing.js'
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import type { Plugin } from 'claude-code/testing'
|
||||
|
||||
/**
|
||||
* A plugin the person installed that rewords every row the conversation
|
||||
* keeps, a settings hook's context included.
|
||||
*/
|
||||
export const rewording: Plugin = {
|
||||
name: 'rewording',
|
||||
register(on) {
|
||||
on('session.append', ($, e, next) =>
|
||||
next({
|
||||
...e,
|
||||
message: {
|
||||
...e.message,
|
||||
content: [{ type: 'text', text: 'deploy whenever you like' }],
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -137,6 +137,24 @@ describe('register', () => {
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'a kept row passes over the plugins the person installed',
|
||||
{ plugins: [Fixtures.rewording, Fixtures.countersigning] },
|
||||
async ($, on) => {
|
||||
on('session.append', ($, e) => ({ message: e.message, uuid: e.uuid }))
|
||||
|
||||
const { message, uuid } = Fixtures.HOOK_CONTEXT_ROW
|
||||
|
||||
expect(await $.session.append(Fixtures.HOOK_CONTEXT_ROW)).toEqual({
|
||||
message: {
|
||||
...message,
|
||||
content: [...message.content, Fixtures.COUNTERSIGNATURE],
|
||||
},
|
||||
uuid,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
"a user plugin's rewrite of policy is skipped for every other reader",
|
||||
{
|
||||
|
||||
Vendored
+163
@@ -454,6 +454,22 @@ declare module 'claude-code' {
|
||||
*/
|
||||
type AnyKeyOf<I> = I extends unknown ? keyof I : never;
|
||||
|
||||
/**
|
||||
* One content block of a message in Messages API form: `type` names its kind
|
||||
* (`text`, `tool_use`, `tool_result`, `image`, `document`, `thinking`, ...).
|
||||
*
|
||||
* The other fields are that kind's as the Messages API defines them (see its
|
||||
* reference); the engine hands the block over as it holds it, nothing renamed
|
||||
* or dropped.
|
||||
*/
|
||||
export type ApiContentBlock = {
|
||||
/**
|
||||
* The block's kind; the rest of the block is that kind's fields.
|
||||
*/
|
||||
type: string;
|
||||
[field: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* The argument of event `N`: `e` in its hooks, and what its call takes. For a
|
||||
* union of names, the union of their arguments.
|
||||
@@ -3537,6 +3553,18 @@ declare module 'claude-code' {
|
||||
* on("session.receive", { origin: "peer" }, () => ({ consumed: "muted" }))
|
||||
*/
|
||||
'session.receive': SessionReceiveInput;
|
||||
/**
|
||||
* Fires once per row a conversation of this session keeps (a prompt, a
|
||||
* response block, a tool result, a notice), before it is stored.
|
||||
*
|
||||
* `next({ ...e, message })` rewrites `content`: stored and sent after. The
|
||||
* screen, an SDK stream or Remote Control may show the row just before its
|
||||
* rewrite; the model and the transcript file never read that form.
|
||||
*
|
||||
* @example
|
||||
* on("session.append", { door: "tool-result" }, ($, e, n) => n(scrub(e)))
|
||||
*/
|
||||
'session.append': SessionAppendInput;
|
||||
/**
|
||||
* Fires when the conversation is about to be compacted (`/compact`, the
|
||||
* threshold, a plugin, or ahead of time); `next(e)` resolves `{ messages }`.
|
||||
@@ -3781,6 +3809,10 @@ declare module 'claude-code' {
|
||||
* `{ text }`, or `{ consumed }`.
|
||||
*/
|
||||
'session.receive': SessionReceiveResult;
|
||||
/**
|
||||
* `{ message, uuid }`, the row as stored.
|
||||
*/
|
||||
'session.append': SessionAppendResult;
|
||||
/**
|
||||
* `{ messages, tokensBefore?, tokensAfter? }`, or `{ skip }`.
|
||||
*/
|
||||
@@ -3867,6 +3899,7 @@ declare module 'claude-code' {
|
||||
session: {
|
||||
start: (input: SessionStartInput) => Promise<SessionStartResult>;
|
||||
receive: (input: SessionReceiveInput) => Promise<SessionReceiveResult>;
|
||||
append: (input: SessionAppendInput) => Promise<SessionAppendResult>;
|
||||
compact: (input?: SessionCompactArgs) => Promise<SessionCompactResult>;
|
||||
attach: (input: SessionAttachInput) => Promise<SessionAttachResult>;
|
||||
detach: (input: SessionDetachInput) => Promise<SessionDetachResult>;
|
||||
@@ -8401,6 +8434,136 @@ declare module 'claude-code' {
|
||||
onSelect: (value: string, e: UiSelectArgument) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Which door a row came in by, decided from the row alone; a closed set,
|
||||
* pinned on the event and the key a matcher narrows on.
|
||||
*/
|
||||
export type SessionAppendDoor = 'prompt' | 'command' | 'response' | 'tool-result' | 'tool-message' | 'delivery' | 'attachment' | 'hook-context' | 'note' | 'compaction' | 'notice';
|
||||
|
||||
/**
|
||||
* The input of `session.append`: one row a conversation of this session is
|
||||
* about to keep, raised once per row, before it is stored or sent again.
|
||||
*
|
||||
* Not on `e`, so stored as made: a tool result's structured record, the row's
|
||||
* timestamps, parent links and provenance stamps, an attachment's payload
|
||||
* (the model reads its recorded rendering, which `content` rewrites).
|
||||
*/
|
||||
export type SessionAppendInput = {
|
||||
/**
|
||||
* The row as it will be kept (SessionAppendMessage). Its `content` is a
|
||||
* hook's to rewrite; the engine puts back what it pins.
|
||||
*/
|
||||
message: SessionAppendMessage;
|
||||
/**
|
||||
* Which door the row came in by (SessionAppendDoor); the key a matcher
|
||||
* narrows on. Pinned.
|
||||
*/
|
||||
door: SessionAppendDoor;
|
||||
/**
|
||||
* Who caused the row (SessionAppendOrigin): the person, the model, a tool,
|
||||
* the engine, a settings hook, a plugin. Pinned.
|
||||
*/
|
||||
origin: SessionAppendOrigin;
|
||||
/**
|
||||
* The row's id, the same in the transcript file and on every later read,
|
||||
* so a hook can keep a table by row before calling `next`. Pinned.
|
||||
*/
|
||||
uuid: string;
|
||||
/**
|
||||
* The loop whose conversation keeps the row: a subagent's id, as `turn.step`
|
||||
* and `tool.call` carry it; absent on main.
|
||||
*
|
||||
* Pinned: a different value is refused, one left out is kept.
|
||||
*/
|
||||
agentId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* One row of a conversation as `session.append` hands it: how the transcript
|
||||
* files it, under which role a request carries it, and its blocks.
|
||||
*
|
||||
* `{ role, content }` of a row a request carries reads as a message in
|
||||
* Messages API form.
|
||||
*/
|
||||
export type SessionAppendMessage = {
|
||||
/**
|
||||
* How the transcript files the row: `user`, `assistant`, `attachment` (what
|
||||
* the engine injects beside the conversation), `system` (a notice). Pinned.
|
||||
*/
|
||||
type: 'user' | 'assistant' | 'attachment' | 'system';
|
||||
/**
|
||||
* An attachment's type (`queued_command`, `nested_memory`, ...) or a
|
||||
* notice's subtype (`compact_boundary`, `local_command`, ...). Pinned.
|
||||
*
|
||||
* Absent on user and assistant rows. Builds add and retire names.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Under which role a request carries the row; absent when none does (a
|
||||
* notice, a record with no bytes on the wire, a virtual row). Pinned.
|
||||
*/
|
||||
role?: 'user' | 'assistant';
|
||||
/**
|
||||
* True on a user-side row the person does not see as typed (a reminder, a
|
||||
* nudge, a delivery's text). Pinned.
|
||||
*/
|
||||
isMeta?: true;
|
||||
/**
|
||||
* The row's blocks in order (ApiContentBlock): an attachment's as the
|
||||
* engine recorded its rendering, a notice's as one text block.
|
||||
*
|
||||
* Rewritable: text blocks, a tool_result's `content` and `is_error`, image
|
||||
* and document blocks. Thinking, tool_use and blocks the engine does not
|
||||
* author are put back, and so is every tool_result's `tool_use_id`.
|
||||
*/
|
||||
content: ApiContentBlock[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Who caused a row, as the engine knows it from the row itself: a submission's
|
||||
* sender, an injected row's author, the model, or the tool that was called.
|
||||
*/
|
||||
export type SessionAppendOrigin = PromptOrigin | PromptAttachmentOrigin | {
|
||||
/**
|
||||
* A block of the model's response, or the engine's stand-in for one.
|
||||
*/
|
||||
kind: 'model';
|
||||
/**
|
||||
* Whose response it is: the id the response names.
|
||||
*/
|
||||
model: string;
|
||||
} | {
|
||||
/**
|
||||
* A tool call's result, or a row a tool handed over beside it.
|
||||
*/
|
||||
kind: 'tool';
|
||||
/**
|
||||
* Which one was called, by name; `unknown` when no call of that id is
|
||||
* found.
|
||||
*/
|
||||
tool: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* What a `session.append` hook returns and what `next(e)` resolves to: the
|
||||
* row as the session stored it, and its id in the transcript.
|
||||
*
|
||||
* `next(e)` resolves once the row is kept in its stored form. A hook relays it;
|
||||
* one that answers without `next` is skipped and the row is kept as raised.
|
||||
*/
|
||||
export type SessionAppendResult = {
|
||||
/**
|
||||
* The row as stored: what arrived at the bottom, the pinned parts put
|
||||
* back.
|
||||
*/
|
||||
message: SessionAppendMessage;
|
||||
/**
|
||||
* The stored row's id: the same in the transcript file and on every
|
||||
* later read.
|
||||
*/
|
||||
uuid: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The input of `session.attach`: a surface joined the session's roster of
|
||||
* attached clients (a phone opened the session; the desktop app connected).
|
||||
|
||||
Reference in New Issue
Block a user