@synadia-ai/agent-fabric-claude-code 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +10 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +201 -0
- package/README.md +465 -2
- package/dist/index.cjs +277 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +248 -0
- package/dist/index.js.map +1 -0
- package/hooks/hooks.json +15 -0
- package/hooks/session-start.sh +28 -0
- package/package.json +87 -4
- package/skills/scratchpad/LICENSE +201 -0
- package/skills/scratchpad/SKILL.md +209 -0
- package/skills/scratchpad/THIRD_PARTY_NOTICES +1067 -0
- package/skills/scratchpad/bin/BUILD.json +367 -0
- package/skills/scratchpad/bin/COMMIT +1 -0
- package/skills/scratchpad/bin/sp-darwin-amd64 +0 -0
- package/skills/scratchpad/bin/sp-darwin-arm64 +0 -0
- package/skills/scratchpad/bin/sp-linux-amd64 +0 -0
- package/skills/scratchpad/bin/sp-linux-arm64 +0 -0
- package/skills/scratchpad/scripts/sp +231 -0
package/README.md
CHANGED
|
@@ -1,3 +1,466 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @synadia-ai/agent-fabric-claude-code
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Synadia Agent Fabric add-on for the Claude Code channel plugin
|
|
4
|
+
(`nats-channel`, `agents/claude-code` in
|
|
5
|
+
[`synadia-agents`](https://github.com/synadia-ai/synadia-agents)). One
|
|
6
|
+
extension module in the shape the plugin loads: named in
|
|
7
|
+
`SYNADIA_CLAUDE_CODE_EXTENSIONS`, it puts the fabric package's tracing on the
|
|
8
|
+
plugin's service and on the client its agent tools prompt through, binds
|
|
9
|
+
each served prompt to the Claude Code session it was delivered to with the
|
|
10
|
+
signed `served` pair, its `end` at the turn's `Stop` so the closing model
|
|
11
|
+
call falls inside the window, and puts the `409` loop guard in front of the
|
|
12
|
+
channel's handler. With a ScratchPad volume configured it also gives
|
|
13
|
+
Claude Code's model ScratchPad, through a skill this package carries as a
|
|
14
|
+
Claude Code plugin of its own (see "ScratchPad"). Without it the plugin is
|
|
15
|
+
the plain plugin. The contract it fills is the plugin's
|
|
16
|
+
`agents/EXTENSIONS.md`; the spec is
|
|
17
|
+
[`docs/plugins.md`](https://github.com/synadia-ai/agent-fabric-sdk/blob/main/docs/plugins.md) §2, §3.1, §3.4 and §9.13.
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
The factory, the module's default export, calls `fabricTracing()` once and
|
|
22
|
+
hands the plugin its three hooks, so the session is on the wire exactly what
|
|
23
|
+
an SDK-built agent is:
|
|
24
|
+
|
|
25
|
+
- **On the service, per prompt:** the envelope's `thread_id` and `root_id`
|
|
26
|
+
are adopted, or a root minted when neither is there; a half pair or a
|
|
27
|
+
malformed id is refused with `400` before the handler runs; the handler
|
|
28
|
+
runs inside the trace scope.
|
|
29
|
+
- **On the client Claude Code's agent tools prompt through, per prompt:**
|
|
30
|
+
the child thread is minted and the pair put into the envelope, and the
|
|
31
|
+
signed `edge` record is published on `TRACE.edges` before the prompt goes
|
|
32
|
+
out, with the model's tool-call ID, which the plugin takes from its
|
|
33
|
+
`PreToolUse` hook and passes to `execute()`. A delegation Claude Code's
|
|
34
|
+
model makes hangs under its tool call in the console.
|
|
35
|
+
- **On the heartbeat:** `records_published` and `records_dropped`, the
|
|
36
|
+
`served` records counted with the edges.
|
|
37
|
+
|
|
38
|
+
The harness piece is the join of Claude Code's own model calls. Claude Code
|
|
39
|
+
sends `X-Claude-Code-Session-ID` on every model request, and the proxy files
|
|
40
|
+
the calls as `harness: claude`, the session id their `harness_thread_id`.
|
|
41
|
+
The plugin is an MCP server inside the session and cannot touch those
|
|
42
|
+
headers, so the add-on publishes the binding:
|
|
43
|
+
|
|
44
|
+
| Event | What the add-on does |
|
|
45
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
46
|
+
| `sessionStarted(session, source)` | remembers the session Claude Code is in now, as the plugin's `SessionStart` hook reported it (also once at start, for the session recorded before the server came up) |
|
|
47
|
+
| `promptAccepted(request)` | keeps `activeTrace()`, the scope tracing bound for this request, by `request.id`; the session is `request.sessionId`, else the remembered one; when it is a Claude Code session id, `beginTurn(scope, "claude")` and `bind(session)` on the fabric package's served publisher, which publishes `start` stamped with the arrival |
|
|
48
|
+
| `aroundToolCall(request, tool, run)` | binds the request's scope around `run`, so the edge of a `prompt_agent` Claude Code's model calls is filed under the served thread; the plugin already re-enters the handler's context through a snapshot before it calls this, so it is a no-op rebind |
|
|
49
|
+
| `promptEnded(request, outcome, at, reason?)` | `error` and `timeout`: `settle(outcome, at, reason)` at once. `ok` with the reason `final_text`, which the plugin reports when the model never called `reply` and the plugin sent the turn's final text as the reply at the turn's `Stop`: at once too, the turn being over. Any other `ok`, which the plugin reports when `reply` completed the request: the turn is not over, since Claude Code makes at least one more model call after the reply to write its closing text, so the pair stays open until a `Stop` after `at`, or for at most `stopWaitMs` (120 s), then ends at `at`. The plugin's `reason` goes on `end` as the record's `reason` (version 2) |
|
|
50
|
+
| `turnStopped(session, at)` | the `Stop` hook ran: every turn of that session answered before `at` ends with `ok` at `at`, the oldest first. So two prompts answered in one Claude Code turn share its end, and a prompt answered after this stop waits for the next |
|
|
51
|
+
|
|
52
|
+
A prompt that arrives while no session is known gets no pair and one log
|
|
53
|
+
line (`NO_SESSION_LINE`); its scope is still kept, so its model's
|
|
54
|
+
delegations are still filed under the served thread. The session is bound
|
|
55
|
+
at arrival: a `/clear` during a turn does not move the binding.
|
|
56
|
+
|
|
57
|
+
The pair goes through the fabric package's `servedPublisher()`, made at
|
|
58
|
+
`started` from the plugin's client: signed as the plugin's identity with the
|
|
59
|
+
record id as the nonce and the `Nats-Msg-Id`, on the edge subject, in the
|
|
60
|
+
order the records were due, so a pair's `end` never overtakes its `start`.
|
|
61
|
+
`stopping` ends every open turn — an answered one at its reply's time, one
|
|
62
|
+
still open now with `error` and the reason `shutdown` — and flushes. The plugin calls it before it
|
|
63
|
+
fails the requests still open, and no `Stop` comes after it.
|
|
64
|
+
|
|
65
|
+
**The `409` loop guard.** Claude Code cannot serve a prompt while its model
|
|
66
|
+
waits on a call to another agent, so a prompt of a tree it is waiting in is
|
|
67
|
+
refused at once with `409` instead of waiting behind the wait it is part of.
|
|
68
|
+
It waits from the moment its model sends a prompt until every call the
|
|
69
|
+
served request has open has settled: a blocking `prompt_agent` until it
|
|
70
|
+
returns, a call started with `wait: false` for as long as it is open. Once
|
|
71
|
+
none is open, a prompt of the tree is admitted, though the turn goes on.
|
|
72
|
+
The guard is the fabric package's `loopGuard()`, two more interceptors
|
|
73
|
+
after tracing's and its `aroundToolCall` inside the add-on's; the PI and
|
|
74
|
+
OpenClaw add-ons return the same.
|
|
75
|
+
|
|
76
|
+
**What it does not do.** No `toolExtensions`: a harness reads no ScratchPad
|
|
77
|
+
reference in the MVP. No header on Claude Code's calls: the plugin cannot
|
|
78
|
+
add one, which is why the pair exists. Nothing with the tool-call ID: the
|
|
79
|
+
plugin's `PreToolUse` hook gives it to the tools, and tracing writes it.
|
|
80
|
+
|
|
81
|
+
## Naming it
|
|
82
|
+
|
|
83
|
+
Installed where Claude Code runs, `npm install @synadia-ai/agent-fabric-claude-code`,
|
|
84
|
+
and named to the plugin by absolute path (a package directory with `dist/`
|
|
85
|
+
built); a package name resolves from the plugin's own directory upward,
|
|
86
|
+
which for a plugin in Claude Code's plugin cache is rarely where the add-on
|
|
87
|
+
is:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
SYNADIA_CLAUDE_CODE_EXTENSIONS=/opt/fabric/node_modules/@synadia-ai/agent-fabric-claude-code claude …
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Claude Code hands its environment to the plugin's MCP server, so the
|
|
94
|
+
variable set where `claude` starts reaches the plugin. Or in the channel's
|
|
95
|
+
`config.json` (`~/.claude/channels/nats/`, or `NATS_STATE_DIR`), where an
|
|
96
|
+
entry may carry options:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"extensions": [
|
|
101
|
+
{
|
|
102
|
+
"module": "/opt/fabric/node_modules/@synadia-ai/agent-fabric-claude-code",
|
|
103
|
+
"options": { "edgeSubject": "TRACE.edges" }
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`SYNADIA_CLAUDE_CODE_EXTENSIONS` wins over `SYNADIA_AGENT_EXTENSIONS`, which
|
|
110
|
+
wins over the config field. The channel's `service registered` log line and
|
|
111
|
+
its `request_info` tool list `agent-fabric` when it is loaded.
|
|
112
|
+
|
|
113
|
+
## The settings
|
|
114
|
+
|
|
115
|
+
- **The edge subject:** the entry's `options.edgeSubject`, else the variable
|
|
116
|
+
`SYNADIA_FABRIC_EDGE_SUBJECT`, else `TRACE.edges`. The `served` pair goes
|
|
117
|
+
on the same subject. `null` in the options is propagate-only: lineage
|
|
118
|
+
forwarded, no record of either kind published. A subject that can never
|
|
119
|
+
be published to stops the factory; the plugin logs
|
|
120
|
+
`extension "…" not loaded: …` and starts plain.
|
|
121
|
+
- **`options.stopWaitMs`:** how long an answered turn waits for the `Stop`
|
|
122
|
+
hook, 120000 by default. A value that is not a number of ms ≥ 0 stops the
|
|
123
|
+
factory the same way.
|
|
124
|
+
|
|
125
|
+
There is no `tracing` setting: the add-on's presence is the switch.
|
|
126
|
+
|
|
127
|
+
## Identity
|
|
128
|
+
|
|
129
|
+
Records are signed, so the plugin needs `senderIdentity: "signed"` with a
|
|
130
|
+
NATS context carrying Claude Code's own credentials. With identity off the
|
|
131
|
+
add-on warns once at `started`, the served publisher warns once more when
|
|
132
|
+
the first record is due, every record owed counts as dropped on the
|
|
133
|
+
heartbeat, and Claude Code's calls stand as a harness thread of their own,
|
|
134
|
+
unbound.
|
|
135
|
+
|
|
136
|
+
## Running Claude Code on the fabric
|
|
137
|
+
|
|
138
|
+
Measured on Claude Code 2.1.281 (2026-09-24).
|
|
139
|
+
|
|
140
|
+
1. **Claude Code and bun.** `claude` on `PATH`, and `bun`: the plugin's
|
|
141
|
+
server and hooks run on it.
|
|
142
|
+
2. **The add-on, installed and named.** Install it from npm, `dist/`
|
|
143
|
+
included:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
npm install @synadia-ai/agent-fabric-claude-code
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
and set `SYNADIA_CLAUDE_CODE_EXTENSIONS` to the installed package's
|
|
150
|
+
absolute path, `<prefix>/node_modules/@synadia-ai/agent-fabric-claude-code`,
|
|
151
|
+
in the environment `claude` starts in (see "Naming it"). Export `PWD`
|
|
152
|
+
too: the channel plugin's `.mcp.json` hands it to its server. The same
|
|
153
|
+
directory is the Claude Code plugin `agent-fabric`, which Claude Code
|
|
154
|
+
loads with a second `--plugin-dir` for ScratchPad (see "ScratchPad").
|
|
155
|
+
To develop the add-on, name this repository's `plugins/claude-code`
|
|
156
|
+
instead, with `dist/` built (see "Development").
|
|
157
|
+
|
|
158
|
+
3. **The channel plugin, from the checkout until it is published.** This
|
|
159
|
+
loads it for the session as `nats-channel@inline`, hooks included; its
|
|
160
|
+
bundle is committed, nothing is built there:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
claude --plugin-dir <synadia-agents>/agents/claude-code \
|
|
164
|
+
--dangerously-load-development-channels plugin:nats-channel@inline
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
4. **The identity.** The channel's `config.json` with a NATS context of
|
|
168
|
+
Claude Code's own and `senderIdentity: "signed"` (see "Identity"), and
|
|
169
|
+
`permissions.allow: ["mcp__plugin_nats-channel_nats"]` in Claude Code's
|
|
170
|
+
`settings.json`, so a turn driven from NATS does not stop at an approval
|
|
171
|
+
nobody sees.
|
|
172
|
+
5. **A virtual key naming Anthropic as its provider.** Claude Code is
|
|
173
|
+
Anthropic's product, its documented gateway shape is a proxy in front of
|
|
174
|
+
Anthropic, and an Anthropic key means Anthropic bills its owner; the
|
|
175
|
+
fabric adds the trace, the identity and the usage record, and never
|
|
176
|
+
routes Claude Code to another model provider. So the key is issued for
|
|
177
|
+
Claude Code's identity with the proxy's built-in `anthropic` provider
|
|
178
|
+
(on a test rig where that name is taken, a provider of another name at
|
|
179
|
+
`https://api.anthropic.com` on the Anthropic shape), the tenant's
|
|
180
|
+
Anthropic API key as the provider key in the record, and the harness
|
|
181
|
+
`claude`, without which the proxy files each call as a thread of its own.
|
|
182
|
+
6. **Two keys and the base URL.** Both are set, and Claude Code sends both:
|
|
183
|
+
- `ANTHROPIC_API_KEY`: an Anthropic API key of the user's own, sent as
|
|
184
|
+
`x-api-key`. Claude Code turns its channel on only when a feature flag
|
|
185
|
+
says so, and it fetches its flags from Anthropic directly, never
|
|
186
|
+
through the base URL, and only with an Anthropic credential; with the
|
|
187
|
+
virtual key alone the channel is ignored. It may be
|
|
188
|
+
the same key as the provider key of step 5, or another of the
|
|
189
|
+
tenant's.
|
|
190
|
+
- `ANTHROPIC_AUTH_TOKEN`: the virtual key, sent as
|
|
191
|
+
`Authorization: Bearer` next to it. The proxy reads the virtual key
|
|
192
|
+
from `Authorization` first and deletes both headers before it sets the
|
|
193
|
+
provider's own, so the Anthropic key reaches no provider.
|
|
194
|
+
- `ANTHROPIC_BASE_URL`: the proxy's bare origin, no `/api`; Claude Code
|
|
195
|
+
appends `/v1/messages` and the proxy forwards it to Anthropic as it is.
|
|
196
|
+
|
|
197
|
+
Claude Code warns at start that both keys are set and "auth may not work
|
|
198
|
+
as expected". That is expected: sending both is the point.
|
|
199
|
+
|
|
200
|
+
7. **No model variables.** Claude Code runs its own default models, the
|
|
201
|
+
main loop and the side calls, every call through the proxy, recorded
|
|
202
|
+
with Anthropic's usage, cache reads and writes included.
|
|
203
|
+
8. **A home outside any git repository.** `HOME`, `CLAUDE_CONFIG_DIR` and
|
|
204
|
+
the channel's `NATS_STATE_DIR` in a directory of the instance's own,
|
|
205
|
+
never `~/.claude`, and not inside a git working tree: from inside one,
|
|
206
|
+
Claude Code sends that repository's `CLAUDE.md` files and its git status
|
|
207
|
+
to the model with every call. Outside one it offers the
|
|
208
|
+
model the skills in the `.claude/skills` of every directory above its
|
|
209
|
+
working directory, the user's own included; `--disable-slash-commands`
|
|
210
|
+
removes them, and with them the Skill tool, so not with
|
|
211
|
+
ScratchPad.
|
|
212
|
+
9. **A terminal, or a pseudo-terminal.** Interactive only: `claude -p`
|
|
213
|
+
registers on the bus and turns no prompt into a turn.
|
|
214
|
+
Answer the development-channels warning once and the channel is on;
|
|
215
|
+
from a script, `tmux` with one Enter does the same.
|
|
216
|
+
|
|
217
|
+
## What the console shows
|
|
218
|
+
|
|
219
|
+
The served thread with the session's model calls joined through the pair,
|
|
220
|
+
the closing call inside the window, the thread's outcome and its duration
|
|
221
|
+
from `start` to `end`, `harness: claude` at depth `full` with both halves;
|
|
222
|
+
a subagent's calls as children of the session's thread. A delegation
|
|
223
|
+
Claude Code's model makes: it discovers and prompts another agent, whose
|
|
224
|
+
thread sits under its `prompt_agent` tool call with Claude Code its
|
|
225
|
+
verified caller. Everything the session
|
|
226
|
+
does between the two records is filed under the prompt: local typing
|
|
227
|
+
during a NATS-driven turn, and a second NATS prompt answered in the same
|
|
228
|
+
turn, share the window.
|
|
229
|
+
|
|
230
|
+
One call can fall outside the pair. At the end of each turn Claude Code
|
|
231
|
+
asks its model what the user might type next; when that call's model
|
|
232
|
+
reaches for a tool, Claude Code calls it once more after the turn's `Stop`,
|
|
233
|
+
and the add-on's `end`. The console shows that call as a root of its own
|
|
234
|
+
on the session id, with no caller. The pair is the turn, and
|
|
235
|
+
the reader infers nothing beyond it: a limit, not a bug.
|
|
236
|
+
|
|
237
|
+
## Stated honestly
|
|
238
|
+
|
|
239
|
+
- **Without the plugin's hooks** (`disableAllHooks`, or `bun` not on the
|
|
240
|
+
hooks' `PATH`) there is no `SessionStart`, so the session comes from
|
|
241
|
+
`CLAUDE_CODE_SESSION_ID` in the server's environment if at all, and no
|
|
242
|
+
`Stop`, so `end` carries the reply's time after the bound and the closing
|
|
243
|
+
call falls outside the window.
|
|
244
|
+
- **The bound delays `end`**: with no `Stop`, the pair stays open for
|
|
245
|
+
`stopWaitMs` after the reply.
|
|
246
|
+
- **A `Stop` names the turn it ended by time only.** Two prompts answered in
|
|
247
|
+
one turn share its end; a prompt answered after a stop that the server's
|
|
248
|
+
poll saw late (250 ms) could take that stop's time. Claude Code gives
|
|
249
|
+
nothing finer.
|
|
250
|
+
- **The two keys are what Claude Code's warning calls unsupported.** Both
|
|
251
|
+
sent together, the virtual key as `Bearer` and the Anthropic key as
|
|
252
|
+
`x-api-key`, was measured on 2.1.281; Claude Code does not
|
|
253
|
+
document it. Re-check it on a release that touches channels or gateways.
|
|
254
|
+
- **An Anthropic credential on the machine.** The channel is behind a flag
|
|
255
|
+
only an Anthropic credential fetches, so every Claude Code on the fabric
|
|
256
|
+
carries an Anthropic API key of its own, and could call Anthropic with it
|
|
257
|
+
directly. Its model calls are traced because it is pointed at the proxy,
|
|
258
|
+
by configuration, not by anything the add-on enforces.
|
|
259
|
+
- **An interactive session.** No headless mode serves a prompt; a script
|
|
260
|
+
drives a terminal.
|
|
261
|
+
- **The context Claude Code sends.** Whatever its working directory
|
|
262
|
+
offers goes to the model with every call: a repository's `CLAUDE.md` and
|
|
263
|
+
git status from inside one, the skills of the directories above it from
|
|
264
|
+
outside one. The home's place decides it.
|
|
265
|
+
- **ScratchPad's grant is per turn.** Claude Code applies a skill's
|
|
266
|
+
`allowed-tools` in the turn that loaded the skill, and clears it with the
|
|
267
|
+
next message. A later prompt whose turn runs `scripts/sp` without loading
|
|
268
|
+
the skill again sends the caller a question for each call. The model
|
|
269
|
+
loaded it in every measured turn; Claude Code gives no rule that lasts
|
|
270
|
+
the session short of the operator's own `allow`.
|
|
271
|
+
- **ScratchPad without the hook.** With hooks off, the Bash tool gets no
|
|
272
|
+
`SP_ENV_FILE`, and `scripts/sp` refuses to run unconfigured, unless the
|
|
273
|
+
operator sets the variables in the environment `claude` starts in, which
|
|
274
|
+
gives the connection but no served prompt's trace or caller.
|
|
275
|
+
|
|
276
|
+
## ScratchPad
|
|
277
|
+
|
|
278
|
+
The package is also a Claude Code plugin, `agent-fabric`
|
|
279
|
+
(`.claude-plugin/plugin.json`), with two pieces: ScratchPad's skill, the
|
|
280
|
+
one every add-on ships, in `skills/scratchpad/` (`SKILL.md`, `scripts/sp`,
|
|
281
|
+
and `bin/` with the sp command line for darwin and linux on arm64 and
|
|
282
|
+
amd64, installed from `../scratchpad-skill` by `npm run build:sp`), and a
|
|
283
|
+
`SessionStart` hook. Claude Code loads the skill as
|
|
284
|
+
`agent-fabric:scratchpad`; the model runs `scripts/sp` through its Bash
|
|
285
|
+
tool. Nothing restricts Bash or any other tool.
|
|
286
|
+
|
|
287
|
+
**The operator's steps.** Besides the steps above:
|
|
288
|
+
|
|
289
|
+
1. The package loaded as a plugin too, a second `--plugin-dir`:
|
|
290
|
+
|
|
291
|
+
```sh
|
|
292
|
+
claude --plugin-dir <synadia-agents>/agents/claude-code --plugin-dir <add-on> \
|
|
293
|
+
--dangerously-load-development-channels plugin:nats-channel@inline
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
2. Permission questions to the caller, and the skill allowed, in the
|
|
297
|
+
channel's `config.json` and Claude Code's `settings.json`:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{ "permissions": { "mode": "query" } }
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"permissions": {
|
|
306
|
+
"defaultMode": "default",
|
|
307
|
+
"allow": ["mcp__plugin_nats-channel_nats", "Skill(agent-fabric:scratchpad)"]
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`defaultMode: "default"` matters: Claude Code 2.1.283 can start in auto
|
|
313
|
+
mode, where its own classifier approves or blocks a tool call, so no
|
|
314
|
+
permission question ever reaches the caller.
|
|
315
|
+
|
|
316
|
+
3. A volume reserved for Claude Code's identity by the ScratchPad operator,
|
|
317
|
+
in the extension's entry in the channel's `config.json`, or as
|
|
318
|
+
`SCRATCHPAD_VOLUME` in the environment `claude` starts in:
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
"extensions": [{ "module": "<add-on>", "options": { "scratchpad": { "volume": "CcVol" } } }]
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Keep `Bash` and `Skill` out of the settings' `deny`, and do not start
|
|
325
|
+
Claude Code with `--disable-slash-commands`, which removes the Skill tool.
|
|
326
|
+
|
|
327
|
+
**The first prompt of a session.** Claude Code can serve a session's first
|
|
328
|
+
prompt before it lists the session's skills to the model. An instruction
|
|
329
|
+
that says "use ScratchPad only when the skill is listed" therefore fails
|
|
330
|
+
once: the model answers that it cannot read a reference. Tell the model to
|
|
331
|
+
call the skill by name (`agent-fabric:scratchpad`) whenever it holds a
|
|
332
|
+
reference, and to ask for the text only if the skill does not exist.
|
|
333
|
+
|
|
334
|
+
**Permission questions.** The skill's copy here has one line ScratchPad's
|
|
335
|
+
own skill has and the shared one does not:
|
|
336
|
+
`allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/sp *)`. It restricts
|
|
337
|
+
nothing; it lets the model run `scripts/sp` by its absolute path without a
|
|
338
|
+
question, in the turn that loaded the skill. Claude Code asks before it
|
|
339
|
+
loads a skill that carries such a line, and `Skill(agent-fabric:scratchpad)`
|
|
340
|
+
in the settings' `allow` answers that once for all. Every other command
|
|
341
|
+
follows the operator's rules: with `permissions.mode: "query"` the plugin
|
|
342
|
+
sends each question to the caller of the served prompt, who answers yes
|
|
343
|
+
or no. The skill tells the model to run `scripts/sp` as a command of its
|
|
344
|
+
own, since a command that wraps it (a shell variable for its path, a pipe
|
|
345
|
+
out of it, `;` or `&&`) is a different command, and asks.
|
|
346
|
+
|
|
347
|
+
**Where the settings come from.** The factory resolves each from the
|
|
348
|
+
entry's `options.scratchpad` (`volume`, `dir`, `url`, `creds`), else the
|
|
349
|
+
variable (`SCRATCHPAD_VOLUME`, `SCRATCHPAD_DIR`, `NATS_URL`, `NATS_CREDS`),
|
|
350
|
+
else, for the URL and the credentials file, the plugin's NATS context. The
|
|
351
|
+
workspace defaults to `scratchpad/` in the channel's state directory
|
|
352
|
+
(`$NATS_STATE_DIR/scratchpad`), created if missing; keep it across
|
|
353
|
+
restarts, since it holds the volume's journal. Without a volume ScratchPad
|
|
354
|
+
is off.
|
|
355
|
+
|
|
356
|
+
**How `scripts/sp` gets them.** The add-on runs in the plugin's MCP server,
|
|
357
|
+
a sibling of the Bash tool's processes, so it cannot set their
|
|
358
|
+
environment. It writes the variables to
|
|
359
|
+
`$NATS_STATE_DIR/agent-fabric/<CLAUDE_PID>.env` (mode 600, rewritten whole
|
|
360
|
+
on every change, removed when the server stops), and the package's
|
|
361
|
+
`SessionStart` hook names that file to the Bash tool as `SP_ENV_FILE`,
|
|
362
|
+
through `CLAUDE_ENV_FILE`. `scripts/sp` reads it on every call:
|
|
363
|
+
|
|
364
|
+
| Variable | Written | `scripts/sp` adds |
|
|
365
|
+
| ------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------- |
|
|
366
|
+
| `SP_NATS_URL`, `SP_NATS_CREDS` | at start | `--url`, `--creds`, every call |
|
|
367
|
+
| `SCRATCHPAD_DIR` | at start | `--dir`, every call |
|
|
368
|
+
| `SCRATCHPAD_VOLUME` | at start | `--volume`, on `init` only |
|
|
369
|
+
| `SP_CREATOR` | at start: Claude Code's prompt address | `--creator`, on `publish-file` |
|
|
370
|
+
| `SP_THREAD_ID`, `SP_ROOT_ID` | per served prompt, from its arrival to its end | `--thread-id`, `--root-id`, every call |
|
|
371
|
+
| `SP_CALLER_ACCOUNT`, `SP_CALLER_USER` | the same, for a verified caller | `--grantee-account`, `--grantee-user`, on `grant` and after `publish-file` |
|
|
372
|
+
| `SP_GRANTS_FILE` | at start: `<workspace>.grants` | nothing: it records there each grant sp confirmed, for grant requests |
|
|
373
|
+
|
|
374
|
+
A `publish-file` during a served prompt with a verified caller also grants
|
|
375
|
+
that caller read on what it published; a `grant` goes to that caller and no
|
|
376
|
+
other. When a second prompt arrives in the same turn, the file names it
|
|
377
|
+
until it ends, then the first again.
|
|
378
|
+
|
|
379
|
+
**References in and out.** With ScratchPad on, the add-on registers Claude
|
|
380
|
+
Code as reading references (`scratchpad_refs_ok: "true"` through the
|
|
381
|
+
extension's `metadata`), and the references its model puts into
|
|
382
|
+
`prompt_agent` go through the fabric package's ScratchPad extension, as an
|
|
383
|
+
SDK-built agent's do: granted to a receiver that reads references, removed
|
|
384
|
+
with a note for any other.
|
|
385
|
+
|
|
386
|
+
**Grant requests.** A caller Claude Code handed a reference to may hand it on
|
|
387
|
+
through its own agent tools; its ScratchPad extension then asks the creator,
|
|
388
|
+
Claude Code, to grant the receiver read (the grant request, `grant-request.ts` in
|
|
389
|
+
the fabric package). The add-on answers on Claude Code's service, by the rule every
|
|
390
|
+
SDK-built agent answers by: the reference must point into Claude Code's volume, the
|
|
391
|
+
requester must hold a live read grant Claude Code made it first-hand, the grantee
|
|
392
|
+
must be another agent, and the grant is one path, read only, ten minutes at
|
|
393
|
+
most, recorded as made on request, which entitles no one to ask again. The
|
|
394
|
+
grants Claude Code's model makes go through `scripts/sp`, which the add-on does not
|
|
395
|
+
see, so `scripts/sp` records each one sp confirms in `SP_GRANTS_FILE`
|
|
396
|
+
(`<workspace>.grants`), and the add-on counts a live one as first-hand: it
|
|
397
|
+
went to the agent whose prompt Claude Code was serving.
|
|
398
|
+
|
|
399
|
+
**Measured** on Claude Code 2.1.283 on 2026-09-27 (`docs/plugins.md`
|
|
400
|
+
§9.13): given a reference, the model read it through the skill, published
|
|
401
|
+
its own file, which the caller read, and handed that file to another agent
|
|
402
|
+
through `prompt_agent`, which read it; no permission question reached the
|
|
403
|
+
caller.
|
|
404
|
+
|
|
405
|
+
## Public API
|
|
406
|
+
|
|
407
|
+
| | |
|
|
408
|
+
| ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
409
|
+
| `default`, `fabricExtension(ctx, env?)` | the factory of the contract: resolves ScratchPad, then builds the extension; `env` is where the variables are read, `process.env` by default |
|
|
410
|
+
| `createFabricExtension(ctx, env?, scratchPad?)` | the same, synchronous, from settings already resolved |
|
|
411
|
+
| `SCRATCHPAD_OPTION`, `SP_VARS`, `SP_ENV_FILE_VAR`, `ScratchPadHandOn`, `SpEnvFile`, `spEnvFilePath(stateDir, env)`, `spBinary(env)` | ScratchPad's option key, the variables `scripts/sp` reads, the file they go to and the variable that names it, the tool extension, the packaged sp's path |
|
|
412
|
+
| `EXTENSION_NAME`, `IDENTITY_OFF_WARNING`, `NO_SESSION_LINE`, `SERVED_HARNESS`, `DEFAULT_STOP_WAIT_MS` | the constants |
|
|
413
|
+
| `isClaudeSessionId(v)` | the session-id rule the pair is published under, the pre-split plugin's rule, header-safe |
|
|
414
|
+
|
|
415
|
+
The contract's types, the setting's precedence (`resolveEdgeSubject`) and
|
|
416
|
+
the loop guard (`loopGuard`) are `@synadia-ai/agent-fabric`'s, shared by
|
|
417
|
+
every add-on under `plugins/`.
|
|
418
|
+
|
|
419
|
+
## Development
|
|
420
|
+
|
|
421
|
+
Built like the fabric package (tsup, ESM and CJS, vitest, eslint,
|
|
422
|
+
prettier). It links `@synadia-ai/agent-fabric` at `file:../../typescript`,
|
|
423
|
+
built first, and the two protocol SDK packages by the same `file:` links
|
|
424
|
+
the fabric package uses; its `package-lock.json` is committed, and a clean clone
|
|
425
|
+
installs with `npm ci`. Nothing of Claude Code is a dependency. The tests: unit, a
|
|
426
|
+
fake of the plugin driving the events in the MCP server's order
|
|
427
|
+
(`test/support/fake-plugin.ts`: the handler open until the reply, a tool
|
|
428
|
+
call re-entered through the handler's snapshot); integration, the factory's
|
|
429
|
+
hooks on a real `AgentService` and `Agents` over a local `nats-server` in
|
|
430
|
+
operator mode, the pair and the edges on the wire validated against
|
|
431
|
+
`test-fixtures/schemas/served.json` and `edge.json`, signed, one id three
|
|
432
|
+
ways, `end` at the stop; ScratchPad's file, the hook and the plugin's
|
|
433
|
+
manifest (unit), and the references handed on (integration).
|
|
434
|
+
|
|
435
|
+
`npm run build:sp` installs the skill from `../scratchpad-skill` into
|
|
436
|
+
`skills/scratchpad`, with the `allowed-tools` line. It downloads the four
|
|
437
|
+
published binaries pinned in `../scratchpad-skill/release.json` and verifies
|
|
438
|
+
their archive and executable hashes with Node 20+. `npm pack` runs it too.
|
|
439
|
+
`skills/` is gitignored. No Go compiler or service checkout is required.
|
|
440
|
+
The [binary guide](https://github.com/synadia-ai/agent-fabric-sdk/blob/main/docs/scratchpad-binaries.md) covers authenticated
|
|
441
|
+
downloads, offline archives, cache checks, and the included licenses.
|
|
442
|
+
|
|
443
|
+
```sh
|
|
444
|
+
(cd ../../typescript && npm install && npm run build)
|
|
445
|
+
npm install
|
|
446
|
+
npm run build:sp
|
|
447
|
+
npm run typecheck && npm run lint && npm run format:check
|
|
448
|
+
npm test
|
|
449
|
+
npm run build
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## License
|
|
453
|
+
|
|
454
|
+
Apache-2.0, like the fabric package. See [LICENSE](https://github.com/synadia-ai/agent-fabric-sdk/blob/main/plugins/claude-code/LICENSE).
|
|
455
|
+
|
|
456
|
+
Short artifact references
|
|
457
|
+
-------------------------
|
|
458
|
+
|
|
459
|
+
The shared wrapper and grant records accept `sp-r1:` short tokens and existing `sp-ref:` tokens.
|
|
460
|
+
Publication uses the short form when the service and the selected client support it.
|
|
461
|
+
Short-token reads and resolution require a compatible `sp` binary selected through
|
|
462
|
+
`SCRATCHPAD_BIN`. The bundled CLI release is pinned independently; this change
|
|
463
|
+
does not advance that pin.
|
|
464
|
+
Copy the returned token unchanged into prompts and replies.
|
|
465
|
+
The token survives client restarts; each content read still requires current authorization.
|
|
466
|
+
See the [native client contract](https://github.com/synadia-ai/agent-fabric-sdk/blob/main/docs/scratchpad-native.md) for recovery and capability fallback.
|