copilot-tap-extension 2.0.7 → 2.0.9
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/README.md +4 -1
- package/SOUL.md +51 -0
- package/bin/install.mjs +7 -1
- package/dist/copilot-instructions.md +15 -0
- package/dist/extension.mjs +823 -29
- package/dist/skills/tap-goal/SKILL.md +13 -2
- package/dist/skills/tap-loop/SKILL.md +6 -0
- package/dist/skills/tap-monitor/SKILL.md +19 -3
- package/dist/skills/tap-orchestrate/SKILL.md +81 -0
- package/dist/version.json +1 -1
- package/docs/adr/0001-persistent-config-default-ownership.md +33 -0
- package/docs/adr/0002-local-provider-gateway-runtime-security.md +36 -0
- package/docs/adr/0003-emitter-delivery-lifecycle.md +68 -0
- package/docs/adr/0004-persistent-config-canonical-streams.md +86 -0
- package/docs/adr/0005-provider-sdk-push-and-dynamic-tools.md +48 -0
- package/docs/adr/0006-command-emitter-cwd-workspace-boundary.md +46 -0
- package/docs/adr/0007-runtime-session-workspace-context.md +62 -0
- package/docs/evals.md +41 -0
- package/docs/evolution-of-tap-icon.html +989 -0
- package/docs/providers.md +242 -0
- package/docs/recipes/adaptive-agent.md +303 -0
- package/docs/recipes/agent-brainstorm/100-extension-ideas.md +288 -0
- package/docs/recipes/agent-brainstorm/deep-ideas.md +216 -0
- package/docs/recipes/ambient-guardian.md +314 -0
- package/docs/recipes/browser-bridge.md +162 -0
- package/docs/recipes/codex-goals-for-tap-goal.md +136 -0
- package/docs/recipes/copilot-sdk-canvas.md +147 -0
- package/docs/recipes/deferred-cognition.md +310 -0
- package/docs/recipes/provider-integration-patterns.md +93 -0
- package/docs/recipes/provider-interface-advanced.md +1364 -0
- package/docs/recipes/provider-interface-core-profile.md +568 -0
- package/docs/recipes/tap-control-plane-roadmap.md +60 -0
- package/docs/recipes/universal-tool-gateway.md +202 -0
- package/docs/reference.md +229 -0
- package/docs/use-cases.md +348 -0
- package/package.json +4 -1
- package/providers/detour/README.md +84 -0
- package/providers/detour/bridge.js +219 -0
- package/providers/detour/index.mjs +322 -0
- package/providers/detour/package-lock.json +577 -0
- package/providers/detour/package.json +19 -0
- package/providers/detour/scripts/build.mjs +31 -0
- package/providers/detour/src/bridge.js +256 -0
- package/providers/detour/src/contracts.js +40 -0
- package/providers/detour/src/inspector.js +260 -0
- package/providers/detour/src/inspector.test.mjs +53 -0
- package/providers/detour/src/panel.js +465 -0
- package/providers/detour/src/provider-core.js +233 -0
- package/providers/detour/src/provider-core.test.mjs +185 -0
- package/providers/detour/src/react-context-core.js +143 -0
- package/providers/detour/src/react-context.js +44 -0
- package/providers/detour/src/react-context.test.mjs +41 -0
- package/providers/templates/README.md +23 -0
- package/providers/templates/ci-review-provider.mjs +46 -0
- package/providers/templates/detour-workflow-provider.mjs +41 -0
- package/providers/templates/jira-github-provider.mjs +42 -0
- package/providers/templates/provider-utils.mjs +45 -0
- package/providers/templates/sast-triage-provider.mjs +51 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# Provider Interface — Core Profile
|
|
2
|
+
|
|
3
|
+
The Core Profile is the minimal subset of the [Provider Interface v2](./provider-interface-v2.md).
|
|
4
|
+
It covers token-authenticated project providers that expose tools to a single Copilot session.
|
|
5
|
+
Read **only this document** to build a working provider.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌─────────────────┐ WebSocket (JSON) ┌─────────────────┐
|
|
13
|
+
│ Gateway │◄── ws://localhost:9400 ──► │ Provider │
|
|
14
|
+
│ │ │ (your process) │
|
|
15
|
+
│ Owns Copilot SDK │ ── sessions ──────────► │ Knows nothing │
|
|
16
|
+
│ Runs WS server │ ◄── auth ───────────── │ about Copilot │
|
|
17
|
+
│ Registers tools │ ── hello.ack ─────────► │ Declares tools │
|
|
18
|
+
│ Dispatches calls │ ◄── hello ──────────── │ Handles calls │
|
|
19
|
+
│ │ ── tool.call ─────────► │ │
|
|
20
|
+
│ │ ◄── tool.result ────── │ │
|
|
21
|
+
│ │ ◄── push ───────────── │ Pushes events │
|
|
22
|
+
│ │ ◄── tools.update ───── │ Updates tools │
|
|
23
|
+
└─────────────────┘ └─────────────────┘
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Connection state machine
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
AwaitAuth ──auth──► AwaitHello ──hello──► Bound ──goodbye/disconnect──► Disconnected
|
|
32
|
+
│ │ │
|
|
33
|
+
└── error ◄──────────┴── error ◄────────┘
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| State | Provider may send | Gateway sends |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| **AwaitAuth** | `auth` | `sessions` or `error` |
|
|
39
|
+
| **AwaitHello** | `hello` | `hello.ack` or `error` |
|
|
40
|
+
| **Bound** | `tool.result`, `push`, `tools.update`, `goodbye` | `tool.call`, `tool.cancel`, `session.lifecycle`, `error` |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Authentication
|
|
45
|
+
|
|
46
|
+
The gateway generates a unique token for the running gateway instance. Providers can discover it from:
|
|
47
|
+
|
|
48
|
+
- `TAP_PROVIDER_TOKEN` when launched with the Copilot environment.
|
|
49
|
+
- `<COPILOT_HOME or ~/.copilot>/extensions/tap/.provider-token` when launched from a sibling terminal or by SDK auto-discovery.
|
|
50
|
+
|
|
51
|
+
The gateway writes the token file with restrictive permissions and removes it when the gateway stops. Provider sends the token as the first message:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{ "type": "auth", "token": "ptk-a8f3..." }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Success →** gateway sends `sessions`. **Failure →** `error { code: "AUTH_FAILED" }`, connection closed.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Messages (12 types)
|
|
62
|
+
|
|
63
|
+
### `auth` — Provider → Gateway
|
|
64
|
+
|
|
65
|
+
First message. Required fields: `type`, `token`.
|
|
66
|
+
|
|
67
|
+
### `sessions` — Gateway → Provider
|
|
68
|
+
|
|
69
|
+
Sent after successful auth. Provider picks one `id` for `hello`.
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{ "type": "sessions", "active": [{ "id": "abc123", "label": "PR #42", "cwd": "/code/foo" }] }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `hello` — Provider → Gateway
|
|
76
|
+
|
|
77
|
+
Register tools, bind to a session.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"type": "hello", "name": "my-provider", "protocolVersion": 2,
|
|
82
|
+
"session": "abc123",
|
|
83
|
+
"tools": [{
|
|
84
|
+
"name": "greet", "description": "Say hello",
|
|
85
|
+
"parameters": { "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] }
|
|
86
|
+
}]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Field | Required | Description |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `name` | yes | Stable provider identity |
|
|
93
|
+
| `protocolVersion` | yes | Must be `2` |
|
|
94
|
+
| `session` | yes | Session `id` from `sessions.active` |
|
|
95
|
+
| `tools` | no | Array of tool defs. Each: `name`, `description`, `parameters` (JSON Schema). Optional `timeout` (ms). |
|
|
96
|
+
|
|
97
|
+
**Success →** `hello.ack`. **Failure →** `error` (`INVALID_SESSION`, `UNSUPPORTED_VERSION`, `TOOL_CONFLICT`).
|
|
98
|
+
|
|
99
|
+
### `hello.ack` — Gateway → Provider
|
|
100
|
+
|
|
101
|
+
Provider is now **Bound**.
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{ "type": "hello.ack", "protocolVersion": 2, "providerId": "p-8f3a", "sessionId": "abc123" }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`providerId` is a stable debug ID included in subsequent `error` messages. `sessionId` echoes the session selected by `hello`.
|
|
108
|
+
|
|
109
|
+
### `tool.call` — Gateway → Provider
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{ "type": "tool.call", "id": "call-123", "sessionId": "abc123", "tool": "greet", "args": { "name": "Alice" } }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Provider **must** respond with exactly one `tool.result` for this `id`.
|
|
116
|
+
|
|
117
|
+
### `tool.result` — Provider → Gateway
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{ "type": "tool.result", "id": "call-123", "data": "Hello, Alice!" }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
On failure, use `error` + `errorCode` instead of `data`:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{ "type": "tool.result", "id": "call-123", "error": "Not found", "errorCode": "NOT_FOUND" }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Error codes: `NOT_FOUND`, `TIMEOUT`, `CANCELLED`, `INTERNAL`. Exactly one of `data` or `error` should be present.
|
|
130
|
+
|
|
131
|
+
### `push` — Provider → Gateway
|
|
132
|
+
|
|
133
|
+
Bound providers can push events to their selected session:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{ "type": "push", "level": "inject", "event": "Page asks for help", "stream": "detour" }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
| Field | Required | Description |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `level` | yes | `keep`, `surface`, or `inject` |
|
|
142
|
+
| `event` | yes | Non-empty event text |
|
|
143
|
+
| `stream` | no | EventStream name. Defaults to the provider name in the SDK. |
|
|
144
|
+
| `sessionId` | no | Must match the session selected in `hello` when supplied. |
|
|
145
|
+
| `metadata` | no | JSON object stored with the stream entry. |
|
|
146
|
+
|
|
147
|
+
Delivery:
|
|
148
|
+
|
|
149
|
+
- `keep` stores the event in the stream only.
|
|
150
|
+
- `surface` stores and logs the event to the Copilot timeline.
|
|
151
|
+
- `inject` stores, logs, and sends the event into the session.
|
|
152
|
+
|
|
153
|
+
### `tools.update` — Provider → Gateway
|
|
154
|
+
|
|
155
|
+
Replace this provider's complete tool list while Bound:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"type": "tools.update",
|
|
160
|
+
"tools": [{
|
|
161
|
+
"name": "greet", "description": "Say hello",
|
|
162
|
+
"parameters": { "type": "object", "properties": { "name": { "type": "string" } } }
|
|
163
|
+
}]
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The `tools` field uses the same validation rules as `hello.tools` and remains capped at 100 tools. If `sessionId` is supplied it must match the session selected in `hello`.
|
|
168
|
+
|
|
169
|
+
Success is silent and triggers the same debounced `registerTools()` + extension reload path used for provider connect/disconnect. On failure, the gateway sends `error` and keeps the previous tool list active. In-flight calls to tools removed by a successful update are not cancelled; they continue to result, timeout, cancellation, or disconnect.
|
|
170
|
+
|
|
171
|
+
### `tool.cancel` — Gateway → Provider
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{ "type": "tool.cancel", "id": "call-123", "sessionId": "abc123", "reason": "timeout" }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Provider **must** respond: `{ "type": "tool.result", "id": "call-123", "error": "Cancelled", "errorCode": "CANCELLED" }`.
|
|
178
|
+
|
|
179
|
+
### `session.lifecycle` — Gateway → Provider
|
|
180
|
+
|
|
181
|
+
Always sent, no opt-in. Three states:
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{ "type": "session.lifecycle", "sessionId": "abc123", "state": "started" }
|
|
185
|
+
{ "type": "session.lifecycle", "sessionId": "abc123", "state": "idle" }
|
|
186
|
+
{ "type": "session.lifecycle", "sessionId": "abc123", "state": "shutdown.pending", "deadline": 10000 }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
On `shutdown.pending`: clean up within `deadline` ms, then send `goodbye`. Gateway tears down after the deadline regardless.
|
|
190
|
+
|
|
191
|
+
### `error` — Gateway → Provider
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{ "type": "error", "code": "INVALID_SESSION", "message": "Session def456 does not exist",
|
|
195
|
+
"replyTo": "hello", "providerId": "p-8f3a" }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Fields: `code` (required), `message` (required), `replyTo` (optional), `providerId` (optional), `sessionId` (optional).
|
|
199
|
+
|
|
200
|
+
### `goodbye` — Provider → Gateway
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{ "type": "goodbye", "reason": "shutting down" }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Send before closing the WebSocket. `reason` is optional.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Protocol rules
|
|
211
|
+
|
|
212
|
+
### Forward compatibility
|
|
213
|
+
|
|
214
|
+
- **Ignore unknown fields** in any message.
|
|
215
|
+
- **Ignore unknown gateway→provider message types** (log and discard, do not disconnect).
|
|
216
|
+
- Gateway **rejects** unknown provider→gateway types with `error { code: "UNKNOWN_TYPE" }`.
|
|
217
|
+
|
|
218
|
+
### Version negotiation
|
|
219
|
+
|
|
220
|
+
Provider sends `protocolVersion: 2` in `hello`. Gateway echoes its version in `hello.ack`. Mismatch → `error { code: "UNSUPPORTED_VERSION" }`, connection closed.
|
|
221
|
+
|
|
222
|
+
### Ordering
|
|
223
|
+
|
|
224
|
+
Per-connection FIFO. Messages on a single WebSocket arrive in send order.
|
|
225
|
+
|
|
226
|
+
### Tool call terminal semantics
|
|
227
|
+
|
|
228
|
+
Each `tool.call` has **one** terminal outcome. First terminal message wins:
|
|
229
|
+
|
|
230
|
+
1. `tool.result` arrives → complete. Later duplicates silently ignored.
|
|
231
|
+
2. `tool.cancel` sent → provider **must** reply `tool.result { errorCode: "CANCELLED" }`. Non-cancelled results arriving after cancel are ignored.
|
|
232
|
+
3. Provider disconnects with in-flight calls → gateway returns `errorCode: "DISCONNECTED"` to Copilot. **Not** replayed on reconnect.
|
|
233
|
+
4. Malformed JSON, oversized messages, or invalid `tool.result` messages that
|
|
234
|
+
cannot be correlated while calls are in flight fail fast. With one pending
|
|
235
|
+
call, the gateway rejects that call with the protocol/validation error. With
|
|
236
|
+
multiple pending calls, the gateway disconnects the provider and fails all
|
|
237
|
+
pending calls with `DISCONNECTED`.
|
|
238
|
+
|
|
239
|
+
### Disconnect cleanup
|
|
240
|
+
|
|
241
|
+
On disconnect (clean or crash): all tools removed, all in-flight calls failed with `DISCONNECTED`. Reconnect = fresh start (re-auth, new `hello`).
|
|
242
|
+
|
|
243
|
+
### Gateway crash recovery
|
|
244
|
+
|
|
245
|
+
Gateway is a detached background process. Crash = **all state lost**. Providers detect via `onclose`, reconnect fresh when a new gateway spawns.
|
|
246
|
+
|
|
247
|
+
### Payload limits
|
|
248
|
+
|
|
249
|
+
| Limit | Value |
|
|
250
|
+
|---|---|
|
|
251
|
+
| `tool.result` max size | 5 MB |
|
|
252
|
+
| All other messages | 2 MB |
|
|
253
|
+
| Max tools per provider | 100 |
|
|
254
|
+
|
|
255
|
+
Exceeding → `error { code: "PAYLOAD_TOO_LARGE" }`.
|
|
256
|
+
|
|
257
|
+
If an oversized or malformed bound-provider message arrives while ambiguous
|
|
258
|
+
tool calls are pending, the terminal semantics above also apply so in-flight
|
|
259
|
+
calls cannot remain pending forever.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Error codes
|
|
264
|
+
|
|
265
|
+
| Code | Fatal? | Recovery |
|
|
266
|
+
|---|---|---|
|
|
267
|
+
| `AUTH_FAILED` | **Yes** | Connection closes. Check `TAP_PROVIDER_TOKEN`. |
|
|
268
|
+
| `UNSUPPORTED_VERSION` | **Yes** | Connection closes. Update `protocolVersion`. |
|
|
269
|
+
| `INVALID_SESSION` | No | Pick a different session from the last `sessions` message. |
|
|
270
|
+
| `TOOL_CONFLICT` | No | Rename the tool, re-send `hello`. |
|
|
271
|
+
| `PAYLOAD_TOO_LARGE` | No | Reduce payload, retry. |
|
|
272
|
+
| `RATE_LIMITED` | No | Back off 1s, retry. |
|
|
273
|
+
| `INVALID_JSON` | No | Fix message, retry. |
|
|
274
|
+
| `UNKNOWN_TYPE` | No | Check `protocolVersion`. |
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Complete example — Node.js
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
import WebSocket from "ws";
|
|
282
|
+
|
|
283
|
+
const TOKEN = process.env.TAP_PROVIDER_TOKEN;
|
|
284
|
+
|
|
285
|
+
function connect() {
|
|
286
|
+
const ws = new WebSocket("ws://localhost:9400");
|
|
287
|
+
|
|
288
|
+
ws.on("open", () => {
|
|
289
|
+
ws.send(JSON.stringify({ type: "auth", token: TOKEN }));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
ws.on("message", (raw) => {
|
|
293
|
+
const msg = JSON.parse(raw);
|
|
294
|
+
|
|
295
|
+
switch (msg.type) {
|
|
296
|
+
case "sessions":
|
|
297
|
+
if (!msg.active.length) { ws.close(); return; }
|
|
298
|
+
ws.send(JSON.stringify({
|
|
299
|
+
type: "hello", name: "example-provider", protocolVersion: 2,
|
|
300
|
+
session: msg.active[0].id,
|
|
301
|
+
tools: [{
|
|
302
|
+
name: "greet", description: "Greet someone by name",
|
|
303
|
+
parameters: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: { name: { type: "string" } },
|
|
306
|
+
required: ["name"]
|
|
307
|
+
}
|
|
308
|
+
}]
|
|
309
|
+
}));
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case "hello.ack":
|
|
313
|
+
console.log(`Registered as ${msg.providerId}`);
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
case "tool.call":
|
|
317
|
+
ws.send(JSON.stringify({
|
|
318
|
+
type: "tool.result", id: msg.id,
|
|
319
|
+
data: `Hello, ${msg.args.name}!`
|
|
320
|
+
}));
|
|
321
|
+
break;
|
|
322
|
+
|
|
323
|
+
case "tool.cancel":
|
|
324
|
+
ws.send(JSON.stringify({
|
|
325
|
+
type: "tool.result", id: msg.id,
|
|
326
|
+
error: "Cancelled", errorCode: "CANCELLED"
|
|
327
|
+
}));
|
|
328
|
+
break;
|
|
329
|
+
|
|
330
|
+
case "session.lifecycle":
|
|
331
|
+
if (msg.state === "shutdown.pending") {
|
|
332
|
+
ws.send(JSON.stringify({ type: "goodbye", reason: "session ending" }));
|
|
333
|
+
ws.close();
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
|
|
337
|
+
case "error":
|
|
338
|
+
console.error(`Error [${msg.code}]: ${msg.message}`);
|
|
339
|
+
if (msg.code === "AUTH_FAILED" || msg.code === "UNSUPPORTED_VERSION") ws.close();
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
default: break; // forward compat: ignore unknown types
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
ws.on("close", () => setTimeout(connect, 5000));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
connect();
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Complete example — Python
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
import asyncio, json, os, websockets
|
|
356
|
+
|
|
357
|
+
TOKEN = os.environ["TAP_PROVIDER_TOKEN"]
|
|
358
|
+
|
|
359
|
+
async def connect():
|
|
360
|
+
while True:
|
|
361
|
+
try:
|
|
362
|
+
async with websockets.connect("ws://localhost:9400") as ws:
|
|
363
|
+
await ws.send(json.dumps({"type": "auth", "token": TOKEN}))
|
|
364
|
+
|
|
365
|
+
async for raw in ws:
|
|
366
|
+
msg = json.loads(raw)
|
|
367
|
+
|
|
368
|
+
if msg["type"] == "sessions":
|
|
369
|
+
if not msg["active"]:
|
|
370
|
+
return
|
|
371
|
+
await ws.send(json.dumps({
|
|
372
|
+
"type": "hello", "name": "example-provider",
|
|
373
|
+
"protocolVersion": 2, "session": msg["active"][0]["id"],
|
|
374
|
+
"tools": [{
|
|
375
|
+
"name": "greet", "description": "Greet someone by name",
|
|
376
|
+
"parameters": {
|
|
377
|
+
"type": "object",
|
|
378
|
+
"properties": {"name": {"type": "string"}},
|
|
379
|
+
"required": ["name"],
|
|
380
|
+
},
|
|
381
|
+
}],
|
|
382
|
+
}))
|
|
383
|
+
|
|
384
|
+
elif msg["type"] == "hello.ack":
|
|
385
|
+
print(f"Registered as {msg['providerId']}")
|
|
386
|
+
|
|
387
|
+
elif msg["type"] == "tool.call":
|
|
388
|
+
await ws.send(json.dumps({
|
|
389
|
+
"type": "tool.result", "id": msg["id"],
|
|
390
|
+
"data": f"Hello, {msg['args']['name']}!",
|
|
391
|
+
}))
|
|
392
|
+
|
|
393
|
+
elif msg["type"] == "tool.cancel":
|
|
394
|
+
await ws.send(json.dumps({
|
|
395
|
+
"type": "tool.result", "id": msg["id"],
|
|
396
|
+
"error": "Cancelled", "errorCode": "CANCELLED",
|
|
397
|
+
}))
|
|
398
|
+
|
|
399
|
+
elif msg["type"] == "session.lifecycle":
|
|
400
|
+
if msg["state"] == "shutdown.pending":
|
|
401
|
+
await ws.send(json.dumps({"type": "goodbye", "reason": "session ending"}))
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
elif msg["type"] == "error":
|
|
405
|
+
print(f"Error [{msg['code']}]: {msg['message']}")
|
|
406
|
+
if msg["code"] in ("AUTH_FAILED", "UNSUPPORTED_VERSION"):
|
|
407
|
+
return
|
|
408
|
+
# else: ignore unknown types (forward compat)
|
|
409
|
+
|
|
410
|
+
except (ConnectionError, websockets.ConnectionClosed):
|
|
411
|
+
print("Disconnected. Reconnecting in 5s...")
|
|
412
|
+
await asyncio.sleep(5)
|
|
413
|
+
|
|
414
|
+
asyncio.run(connect())
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Message summary
|
|
420
|
+
|
|
421
|
+
| Direction | Type | When | Response expected? |
|
|
422
|
+
|---|---|---|---|
|
|
423
|
+
| Provider → Gateway | `auth` | First message | `sessions` or `error` |
|
|
424
|
+
| Gateway → Provider | `sessions` | After auth | Provider sends `hello` |
|
|
425
|
+
| Provider → Gateway | `hello` | After `sessions` | `hello.ack` or `error` |
|
|
426
|
+
| Gateway → Provider | `hello.ack` | After `hello` | — |
|
|
427
|
+
| Gateway → Provider | `tool.call` | Copilot invokes tool | `tool.result` (**required**) |
|
|
428
|
+
| Provider → Gateway | `tool.result` | After `tool.call`/`tool.cancel` | — |
|
|
429
|
+
| Gateway → Provider | `tool.cancel` | Timeout/interrupt | `tool.result` with `CANCELLED` (**required**) |
|
|
430
|
+
| Provider → Gateway | `push` | Store/surface/inject provider event | — |
|
|
431
|
+
| Provider → Gateway | `tools.update` | Replace provider tool list | — |
|
|
432
|
+
| Gateway → Provider | `session.lifecycle` | State change | `goodbye` on shutdown (recommended) |
|
|
433
|
+
| Gateway → Provider | `error` | Invalid message | — |
|
|
434
|
+
| Provider → Gateway | `goodbye` | Before disconnect | — |
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Dynamic tool registration
|
|
439
|
+
|
|
440
|
+
The Copilot SDK does **not** expose a `tools.add` or `tools.update` RPC. Tools are declared once at session creation/resume time and sent to the CLI in that initial handshake. To add or remove provider tools mid-session the Gateway uses two mechanisms together:
|
|
441
|
+
|
|
442
|
+
### 1. Local handler map — `session.registerTools()`
|
|
443
|
+
|
|
444
|
+
`CopilotSession.registerTools(tools)` replaces the in-memory handler map (a `Map<name, handler>`). This controls which tool calls the SDK can dispatch locally. The Gateway calls this whenever the combined set of tap + provider tools changes:
|
|
445
|
+
|
|
446
|
+
```js
|
|
447
|
+
// Provider connects — merge its tools into the existing set
|
|
448
|
+
session.registerTools([...tapTools, ...providerTools]);
|
|
449
|
+
|
|
450
|
+
// Provider disconnects — remove its tools
|
|
451
|
+
session.registerTools([...tapTools]);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
This alone is **not sufficient** — the CLI still has the old tool list from the original session handshake.
|
|
455
|
+
|
|
456
|
+
### 2. Extension reload — `session.rpc.extensions.reload()`
|
|
457
|
+
|
|
458
|
+
The SDK exposes an experimental RPC:
|
|
459
|
+
|
|
460
|
+
```js
|
|
461
|
+
await session.rpc.extensions.reload();
|
|
462
|
+
// Tells the CLI to reload this extension, which re-runs joinSession()
|
|
463
|
+
// and picks up the updated tool list
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
This triggers a full re-join: the CLI tears down the current extension session and calls `joinSession()` again, which sends the new `tools` array via the `session.resume` RPC.
|
|
467
|
+
|
|
468
|
+
### Gateway lifecycle
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
Provider connects
|
|
472
|
+
→ Gateway validates auth, receives hello with tool defs
|
|
473
|
+
→ Gateway merges provider tools into the session tool set
|
|
474
|
+
→ Gateway calls session.registerTools([...tapTools, ...providerTools])
|
|
475
|
+
→ Gateway calls session.rpc.extensions.reload()
|
|
476
|
+
→ CLI re-joins, sees updated tools, provider tools become available
|
|
477
|
+
|
|
478
|
+
Provider sends tools.update
|
|
479
|
+
→ Gateway validates replacement tool defs
|
|
480
|
+
→ Gateway replaces that provider's tools in the merged session tool set
|
|
481
|
+
→ Gateway calls session.registerTools([...tapTools, ...providerTools])
|
|
482
|
+
→ Gateway calls session.rpc.extensions.reload()
|
|
483
|
+
→ CLI re-joins, sees updated tools
|
|
484
|
+
|
|
485
|
+
Provider disconnects
|
|
486
|
+
→ Gateway removes provider tools from the session tool set
|
|
487
|
+
→ Gateway calls session.registerTools([...tapTools])
|
|
488
|
+
→ Gateway calls session.rpc.extensions.reload()
|
|
489
|
+
→ CLI re-joins, provider tools disappear cleanly
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Surviving reloads — `globalThis` singleton
|
|
493
|
+
|
|
494
|
+
`session.rpc.extensions.reload()` re-runs the extension entry point (`extension.mjs`) from scratch. A naïve implementation would lose all runtime state (running emitters, stream history, config). The solution is to cache the runtime on `globalThis` so it persists across reloads:
|
|
495
|
+
|
|
496
|
+
```js
|
|
497
|
+
// extension.mjs — reload-safe pattern
|
|
498
|
+
import { joinSession } from "@github/copilot-sdk/extension";
|
|
499
|
+
import { createCopilotChannelsRuntime } from "./tap-runtime.mjs";
|
|
500
|
+
|
|
501
|
+
// First run: creates runtime and caches it.
|
|
502
|
+
// Reload: reuses the existing runtime — emitters, streams, config all intact.
|
|
503
|
+
const runtime = globalThis.__tapRuntime ??= createCopilotChannelsRuntime({
|
|
504
|
+
cwd: process.cwd()
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const session = await joinSession({
|
|
508
|
+
tools: runtime.tools, // includes provider tools if any are connected
|
|
509
|
+
hooks: runtime.hooks
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
runtime.attachSession(session); // re-wires session port to the new session handle
|
|
513
|
+
|
|
514
|
+
session.on("session.shutdown", () => {
|
|
515
|
+
void runtime.stopAllEmitters();
|
|
516
|
+
});
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
The existing `sessionPort` abstraction already supports session swapping via `attachSession()`, so the new session handle is wired in cleanly without disrupting running emitters or streams.
|
|
520
|
+
|
|
521
|
+
### Reload frequency — one per provider, not per tool
|
|
522
|
+
|
|
523
|
+
A provider sends **one `hello` message** containing **all** its tools:
|
|
524
|
+
|
|
525
|
+
```json
|
|
526
|
+
{
|
|
527
|
+
"type": "hello",
|
|
528
|
+
"tools": [tool1, tool2, ..., tool10]
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**1 provider connection = 1 `hello` = 1 reload**, regardless of how many tools it declares.
|
|
533
|
+
|
|
534
|
+
For multiple providers connecting around the same time, the Gateway should **debounce** the reload call to batch them into a single reload:
|
|
535
|
+
|
|
536
|
+
```js
|
|
537
|
+
let reloadTimer = null;
|
|
538
|
+
function scheduleReload() {
|
|
539
|
+
clearTimeout(reloadTimer);
|
|
540
|
+
reloadTimer = setTimeout(() => {
|
|
541
|
+
session.registerTools([...tapTools, ...allProviderTools]);
|
|
542
|
+
session.rpc.extensions.reload();
|
|
543
|
+
}, 200); // batch connections within a 200ms window
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
This ensures that even 5 providers connecting simultaneously result in a single reload.
|
|
548
|
+
|
|
549
|
+
### Caveats
|
|
550
|
+
|
|
551
|
+
- `session.rpc.extensions.reload()` is marked **`@experimental`** in the SDK (`dist/generated/rpc.js`). Its behavior or availability may change.
|
|
552
|
+
- There is no incremental tool update — each reload sends the full tool list. This is fine for the expected scale (≤100 tools per provider, per spec).
|
|
553
|
+
- `tools.update` replaces the provider's full tool list. The minimal core profile does not implement request acknowledgments, revision numbers, or per-tool deltas.
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## What's not in Core
|
|
558
|
+
|
|
559
|
+
These are in the [full spec](./provider-interface-v2.md), not here:
|
|
560
|
+
|
|
561
|
+
- External/browser providers and pairing auth
|
|
562
|
+
- `"all"` session binding, `session.ready`
|
|
563
|
+
- Hooks, gates, transforms
|
|
564
|
+
- Stream filters and `stream.query`
|
|
565
|
+
- Advanced dynamic updates (`hooks.update`, `context.update`, `filter.set`) and update revisions/acks
|
|
566
|
+
- Multi-instance providers, reconnect tokens
|
|
567
|
+
- Concurrency limits, tool progress (`tool.progress`)
|
|
568
|
+
- Session events (`session.event`, `subscribe`)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Tap control-plane roadmap
|
|
2
|
+
|
|
3
|
+
This document records the implementation slices that connect the Codex/Copilot
|
|
4
|
+
extensibility audit to concrete tap capabilities.
|
|
5
|
+
|
|
6
|
+
## Implemented in this release slice
|
|
7
|
+
|
|
8
|
+
- **Emitter-run traces**: diagnostics now retain structured trace records for
|
|
9
|
+
scheduled emitter runs, including trace id, emitter id, run index, duration,
|
|
10
|
+
status, and error.
|
|
11
|
+
- **Diagnostics control tower foundation**: the diagnostics canvas now surfaces
|
|
12
|
+
trace counts, recent emitter-run traces, and goal EventStream ledgers.
|
|
13
|
+
- **Goal evidence tools**: `tap_verify_goal_output` and `tap_audit_claims`
|
|
14
|
+
verify workspace files, EventStream contents, and caller-supplied command
|
|
15
|
+
evidence without secretly executing commands.
|
|
16
|
+
- **Mode-aware session state**: `tap_get_session_state` reads current mode,
|
|
17
|
+
model, tasks, schedules, canvases, and UI capabilities without mutating the
|
|
18
|
+
session.
|
|
19
|
+
- **Guarded mode switching**: `tap_set_session_mode` can change
|
|
20
|
+
interactive/plan/autopilot mode only with explicit confirmation.
|
|
21
|
+
- **Structured session records**: traces and stream posts are mirrored into
|
|
22
|
+
session-workspace JSONL collections and can be inspected with
|
|
23
|
+
`tap_query_records`.
|
|
24
|
+
- **Eval metadata hooks**: eval cases can now declare required observations,
|
|
25
|
+
prohibited claims, rubrics, deterministic assertions, and pass/fail examples;
|
|
26
|
+
the judge prompt consumes those fields when present.
|
|
27
|
+
- **Monitor audit trail**: `/tap-monitor` companion prompts now write structured
|
|
28
|
+
REVIEW RECORD entries.
|
|
29
|
+
- **Orchestration foundation**: `/tap-orchestrate` defines the coordinator and
|
|
30
|
+
role-emitter pattern for gated multi-agent workflows.
|
|
31
|
+
- **Provider recipe set**: reusable patterns now cover Jira/GitHub, CI auto-fix,
|
|
32
|
+
code review, SAST triage, and browser/Detour workflows.
|
|
33
|
+
|
|
34
|
+
## Deferred deeper work
|
|
35
|
+
|
|
36
|
+
- **Full trace span hierarchy**: current traces are run-level records. A future
|
|
37
|
+
slice should add child spans for line routing, provider calls, tool calls, and
|
|
38
|
+
prompt dispatch using W3C `traceparent` where available.
|
|
39
|
+
- **SQLite ledgers**: current records are session-workspace JSONL artifacts. A
|
|
40
|
+
future slice can move those collections into the SDK Session FS SQLite
|
|
41
|
+
provider for richer querying.
|
|
42
|
+
- **HALO-style optimizer**: eval metadata is now accepted, but the ranked
|
|
43
|
+
recommendation handoff still needs an `evals/optimize` command.
|
|
44
|
+
- **Elicitation-backed mode policy**: mode switching is guarded by explicit tool
|
|
45
|
+
confirmation. A future slice can add host elicitation prompts and ownership
|
|
46
|
+
policy before permitting mode mutation.
|
|
47
|
+
- **Goal timeline graph**: the canvas has trace and goal panels, but not a
|
|
48
|
+
timeline graph or per-span drilldown yet.
|
|
49
|
+
- **Production provider examples**: recipes are documented; full provider
|
|
50
|
+
implementations for Jira/GitHub and CI review remain future work.
|
|
51
|
+
|
|
52
|
+
## Validation expectation
|
|
53
|
+
|
|
54
|
+
Every control-plane slice should prove:
|
|
55
|
+
|
|
56
|
+
1. Runtime behavior has focused tests.
|
|
57
|
+
2. Tool surfaces have schema/handler tests.
|
|
58
|
+
3. Docs name the evidence surface and safety constraints.
|
|
59
|
+
4. Diagnostics expose the new state.
|
|
60
|
+
5. Smoke evals still show tap tools available.
|