showrunner-agent-staging 0.0.0 → 0.1.3-staging.1049
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 +50 -9
- package/dist/src/adapter-runner.d.ts +11 -0
- package/dist/src/adapter-runner.js +216 -0
- package/dist/src/adapter-state-store.d.ts +8 -0
- package/dist/src/adapter-state-store.js +80 -0
- package/dist/src/adapter.d.ts +240 -0
- package/dist/src/adapter.js +296 -0
- package/dist/src/adapters/acp/acp-adapter.d.ts +24 -0
- package/dist/src/adapters/acp/acp-adapter.js +286 -0
- package/dist/src/adapters/acp/acp-config.d.ts +59 -0
- package/dist/src/adapters/acp/acp-config.js +92 -0
- package/dist/src/adapters/acp/acp-permission-mapper.d.ts +9 -0
- package/dist/src/adapters/acp/acp-permission-mapper.js +105 -0
- package/dist/src/adapters/acp/acp-runtime.d.ts +55 -0
- package/dist/src/adapters/acp/acp-runtime.js +213 -0
- package/dist/src/adapters/claude/claude-adapter.d.ts +57 -0
- package/dist/src/adapters/claude/claude-adapter.js +358 -0
- package/dist/src/adapters/claude/claude-message-mapper.d.ts +61 -0
- package/dist/src/adapters/claude/claude-message-mapper.js +186 -0
- package/dist/src/adapters/claude/claude-permission-mapper.d.ts +12 -0
- package/dist/src/adapters/claude/claude-permission-mapper.js +124 -0
- package/dist/src/adapters/codex/app-server-client.d.ts +43 -0
- package/dist/src/adapters/codex/app-server-client.js +200 -0
- package/dist/src/adapters/codex/codex-adapter.d.ts +130 -0
- package/dist/src/adapters/codex/codex-adapter.js +538 -0
- package/dist/src/adapters/codex/codex-input-mapper.d.ts +4 -0
- package/dist/src/adapters/codex/codex-input-mapper.js +85 -0
- package/dist/src/agent-tools.d.ts +344 -0
- package/dist/src/agent-tools.js +106 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +164 -0
- package/dist/src/client.d.ts +260 -0
- package/dist/src/client.js +682 -0
- package/dist/src/context-tools.d.ts +40 -0
- package/dist/src/context-tools.js +146 -0
- package/dist/src/file-tools.d.ts +40 -0
- package/dist/src/file-tools.js +82 -0
- package/dist/src/handler.d.ts +34 -0
- package/dist/src/handler.js +257 -0
- package/dist/src/host-tool-risk.d.ts +3 -0
- package/dist/src/host-tool-risk.js +265 -0
- package/dist/src/index.d.ts +26 -0
- package/dist/src/index.js +26 -0
- package/dist/src/instance-lock.d.ts +19 -0
- package/dist/src/instance-lock.js +93 -0
- package/dist/src/known-secrets.d.ts +4 -0
- package/dist/src/known-secrets.js +40 -0
- package/dist/src/logger.d.ts +11 -0
- package/dist/src/logger.js +60 -0
- package/dist/src/media-input.d.ts +12 -0
- package/dist/src/media-input.js +129 -0
- package/dist/src/note-tools.d.ts +100 -0
- package/dist/src/note-tools.js +91 -0
- package/dist/src/output-vocabulary.d.ts +16 -0
- package/dist/src/output-vocabulary.js +231 -0
- package/dist/src/pieces.d.ts +48 -0
- package/dist/src/pieces.js +73 -0
- package/dist/src/protocol.d.ts +657 -0
- package/dist/src/protocol.js +361 -0
- package/dist/src/state-store.d.ts +42 -0
- package/dist/src/state-store.js +217 -0
- package/dist/src/storage.d.ts +8 -0
- package/dist/src/storage.js +81 -0
- package/dist/src/todo-tools.d.ts +83 -0
- package/dist/src/todo-tools.js +136 -0
- package/dist/src/working-directory.d.ts +1 -0
- package/dist/src/working-directory.js +11 -0
- package/package.json +39 -9
- package/test/adapters/acp/fake-acp-agent.mjs +265 -0
package/README.md
CHANGED
|
@@ -1,13 +1,54 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Showrunner Agent Connector
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
change that reaches Showrunner's staging environment and is pinned to that environment's servers.
|
|
3
|
+
`showrunner-agent` connects a user-owned provider process to one Showrunner agent installation. It accepts durable, ordered events, runs one provider adapter, and sends idempotent commands through the connector v1 API.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
The connector for Showrunner itself is [`showrunner-agent`](https://www.npmjs.com/package/showrunner-agent).
|
|
5
|
+
## Run
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm ci
|
|
9
|
+
npm run build
|
|
10
|
+
npm test
|
|
11
|
+
npx showrunner-agent connect \
|
|
12
|
+
--api <api-base-url> \
|
|
13
|
+
--pairing-code ABCD-EFGH \
|
|
14
|
+
--adapter codex
|
|
15
|
+
```
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
Don't type the connect command from here — the agent editor in the app prints one with the pairing
|
|
18
|
+
code and API URL already filled in for the build you are running (`ConnectCommandBuilder`). Its API
|
|
19
|
+
URL comes from `scripts/dev-stack.sh` (`API_PORT`) for a numbered dev stack, from `applicationUrl`
|
|
20
|
+
in `api/src/Bylines.Messenger.Api/Properties/launchSettings.json` for stack 0, and from the hosted
|
|
21
|
+
domain otherwise.
|
|
22
|
+
|
|
23
|
+
Released builds install the connector from npm, and it ships with the API rather than on a pipeline
|
|
24
|
+
of its own — the same way `agent-gateway` does. Staging and production are separate npm packages,
|
|
25
|
+
each published by its own API deploy after the deploy succeeds: `showrunner-agent-staging`
|
|
26
|
+
(`<version>-staging.<run>` on every merge) and `showrunner-agent` (the `package.json` version, once
|
|
27
|
+
per release). Separate packages because a staging build is pinned to the staging servers, and
|
|
28
|
+
because npm allows one trusted publisher per package. Both publish by trusted publishing (OIDC, no
|
|
29
|
+
stored token). Cutting a production release therefore means bumping `version` in `package.json`;
|
|
30
|
+
`external-agent-gateway-commit-check` fails a PR that changes the connector without bumping it. A
|
|
31
|
+
local build runs this working tree instead, via the `npm link` that `dev-stack.sh up` performs.
|
|
32
|
+
|
|
33
|
+
Supported CLI adapters are `custom`, `codex`, `claude`, and `acp`. A custom module must default-export an `AgentAdapter` (or export it as `adapter`). Every adapter accepts `--cwd /absolute/project/path` to choose its working directory; ACP additionally requires `--agent <id>`. The supported ACP IDs and invocation shapes live in `src/adapters/acp/acp-config.ts`.
|
|
34
|
+
|
|
35
|
+
The connector credential is issued by Showrunner pairing and stored by `FileConnectorCredentialStorage` in the platform data directory selected by `env-paths`. The directory and file are enforced as modes 0700 and 0600. Pairing, access, and refresh values are never accepted as provider configuration and are redacted from logs.
|
|
36
|
+
|
|
37
|
+
## Provider credentials and isolation
|
|
38
|
+
|
|
39
|
+
- Codex launches `codex app-server` over stdio. Authentication comes from the locally installed Codex process and its normal OpenAI credential configuration. App-server endpoints are limited to stdio, Unix sockets, or loopback unless an embedding application explicitly opts out. See the current [Codex app-server documentation](https://learn.chatgpt.com/docs/app-server).
|
|
40
|
+
- Claude is transparently re-hosted with `@anthropic-ai/claude-agent-sdk` through the user's installed Claude Code binary and its existing sign-in. Executable discovery is defined by `findClaudeCodeExecutable` in `src/adapters/claude/claude-adapter.ts`.
|
|
41
|
+
- ACP agents use their CLI's configured credentials. ACP accepts external MCP server descriptors rather than this connector's in-process tool callbacks, so the generic adapter currently mounts no Showrunner tools and is reply-only. Permission requests still route through the owner approval flow and never auto-approve.
|
|
42
|
+
- Custom adapters own their provider credential loading. Keep secrets outside the repository and do not put them in Showrunner messages, adapter state, command errors, or structured log fields.
|
|
43
|
+
|
|
44
|
+
Run each connector under a dedicated OS account/container with access only to its intended working directory, network destinations, and provider credentials. Do not share a home directory or cloud credential chain with unrelated workloads. Disable unused MCP servers and prefer read-only scopes.
|
|
45
|
+
|
|
46
|
+
## Untrusted content and host policy
|
|
47
|
+
|
|
48
|
+
Events label message authors as `owner`, `other_participant`, `agent`, or `system`. Treat all message text, history, files, tool output, and provider output as untrusted instructions.
|
|
49
|
+
|
|
50
|
+
An agent is reachable only from its own 1:1 chat with its owner: `AddUsersToStream` refuses a `Runner`-type stream, and grants are minted only when that chat is created. Every message an agent sees is therefore the owner's or its own, so the connector does not track per-invocation content trust.
|
|
51
|
+
|
|
52
|
+
Host tools are gated on the risk of the tool itself. Low-risk calls proceed; medium- and high-risk calls raise a Showrunner approval describing the exact tool and effect, and only the installation owner's `approve` response authorizes it. Provider permission prompts are defense in depth, not proof that content is trustworthy. Opening agents to multi-participant streams would require reinstating a content-trust signal before that reasoning holds.
|
|
53
|
+
|
|
54
|
+
The API kill switch and per-provider flags should remain off until the relevant adapter conformance, redaction, revocation, and permission matrices pass.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExternalAgentClient } from "./client.js";
|
|
2
|
+
import type { AgentEventProcessor } from "./handler.js";
|
|
3
|
+
import type { AgentEvent } from "./protocol.js";
|
|
4
|
+
import { type AdapterStateStore, type AgentAdapter, type AgentStreamScope, type AdapterLogger } from "./adapter.js";
|
|
5
|
+
export declare function createAdapterProcessor(input: {
|
|
6
|
+
adapter: AgentAdapter;
|
|
7
|
+
client: () => ExternalAgentClient;
|
|
8
|
+
state: AdapterStateStore;
|
|
9
|
+
logger: AdapterLogger;
|
|
10
|
+
}): AgentEventProcessor;
|
|
11
|
+
export declare function invocationScope(event: AgentEvent): AgentStreamScope;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { NamespacedAdapterStateStore, providerResourceId, } from "./adapter.js";
|
|
3
|
+
export function createAdapterProcessor(input) {
|
|
4
|
+
return async (event, handlerContext) => {
|
|
5
|
+
const client = input.client();
|
|
6
|
+
const commands = new InvocationAdapterClient(client, event, handlerContext);
|
|
7
|
+
await client.startInvocation(event.invocation.id, {
|
|
8
|
+
commandId: commandId(event, "invocation.start"),
|
|
9
|
+
externalSessionId: handlerContext.session.externalSessionId,
|
|
10
|
+
requestedDeadlineSeconds: Math.max(1, Math.ceil((Date.parse(event.invocation.deadlineAt) - Date.now()) / 1_000)),
|
|
11
|
+
});
|
|
12
|
+
const trust = event.trust;
|
|
13
|
+
const result = await input.adapter.handle(event, {
|
|
14
|
+
installationId: event.installation.id,
|
|
15
|
+
streamId: event.stream.id,
|
|
16
|
+
sessionId: event.session.id,
|
|
17
|
+
invocationId: event.invocation.id,
|
|
18
|
+
providerKind: input.adapter.kind,
|
|
19
|
+
externalSessionId: handlerContext.session.externalSessionId,
|
|
20
|
+
model: event.installation.model,
|
|
21
|
+
reasoningEffort: event.installation.reasoningEffort,
|
|
22
|
+
client: commands,
|
|
23
|
+
state: new NamespacedAdapterStateStore(input.state, input.adapter.kind, event.installation.id, event.session.id),
|
|
24
|
+
trust,
|
|
25
|
+
scope: invocationScope(event),
|
|
26
|
+
logger: input.logger,
|
|
27
|
+
}, handlerContext.signal);
|
|
28
|
+
await client.completeInvocation(event.invocation.id, {
|
|
29
|
+
commandId: commandId(event, "invocation.complete"),
|
|
30
|
+
status: result.status === "waiting" ? "aborted" : result.status,
|
|
31
|
+
providerModel: result.providerModel,
|
|
32
|
+
usage: normalizeUsage(result.usage),
|
|
33
|
+
error: result.error,
|
|
34
|
+
});
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function invocationScope(event) {
|
|
39
|
+
return {
|
|
40
|
+
isAgentChat: event.stream.isAgentChat,
|
|
41
|
+
streamDisplayName: event.stream.displayName,
|
|
42
|
+
canReadThreadHistory: event.stream.streamSettings.canReadThreadHistory,
|
|
43
|
+
canReadRecentMessages: event.stream.streamSettings.canReadRecentMessages,
|
|
44
|
+
canSearch: event.stream.streamSettings.canSearch,
|
|
45
|
+
canReadFiles: event.stream.streamSettings.canReadFiles,
|
|
46
|
+
canReadNotes: event.stream.streamSettings.canReadNotes,
|
|
47
|
+
canCreateMessages: event.stream.streamSettings.canCreateMessages,
|
|
48
|
+
canCreateFiles: event.stream.streamSettings.canCreateFiles,
|
|
49
|
+
canCreateToDos: event.stream.streamSettings.canCreateToDos,
|
|
50
|
+
canCreateNotes: event.stream.streamSettings.canCreateNotes,
|
|
51
|
+
canReact: event.stream.streamSettings.canReact,
|
|
52
|
+
canRequestInput: event.stream.streamSettings.canRequestInput,
|
|
53
|
+
rememberApprovals: event.stream.streamSettings.rememberApprovals,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
class InvocationAdapterClient {
|
|
57
|
+
#client;
|
|
58
|
+
#event;
|
|
59
|
+
#context;
|
|
60
|
+
#sequence = 0;
|
|
61
|
+
constructor(client, event, context) {
|
|
62
|
+
this.#client = client;
|
|
63
|
+
this.#event = event;
|
|
64
|
+
this.#context = context;
|
|
65
|
+
}
|
|
66
|
+
async startActivity(input) {
|
|
67
|
+
await this.#client.createActivity(this.#event.invocation.id, {
|
|
68
|
+
commandId: this.#next("activity.create"), activityId: input.activityId, label: input.label,
|
|
69
|
+
type: input.type ?? "default", orderIndex: input.orderIndex ?? this.#sequence,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async completeActivity(activityId, outcome = "completed") {
|
|
73
|
+
await this.#client.completeActivity(this.#event.invocation.id, activityId, {
|
|
74
|
+
commandId: this.#next("activity.complete"), outcome,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async sendMessage(pieces) {
|
|
78
|
+
await this.#client.createMessage(this.#event.invocation.id, {
|
|
79
|
+
commandId: this.#nextForContent("message.create", pieces),
|
|
80
|
+
parentMessageId: this.#event.event.type === "message.created"
|
|
81
|
+
? this.#event.event.replyTarget.parentMessageId ?? this.#event.event.message.id
|
|
82
|
+
: null,
|
|
83
|
+
rootAssetId: this.#event.event.type === "message.created" ? this.#event.event.replyTarget.rootAssetId : null,
|
|
84
|
+
pieces,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
createFileDownloadLink(assetId) {
|
|
88
|
+
return this.#client.createFileDownloadLink(this.#event.invocation.id, assetId, {
|
|
89
|
+
commandId: this.#next("file.download_link.create"),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async sendReaction(messageId, emoji) {
|
|
93
|
+
await this.#client.createReaction(this.#event.invocation.id, { commandId: this.#next("reaction.create"), messageId, emoji });
|
|
94
|
+
}
|
|
95
|
+
async readNote(noteId) {
|
|
96
|
+
return (await this.#client.readNote(this.#event.invocation.id, noteId, {
|
|
97
|
+
commandId: this.#next("note.read"),
|
|
98
|
+
})).note;
|
|
99
|
+
}
|
|
100
|
+
async createNote(noteId, textAttachments) {
|
|
101
|
+
return (await this.#client.createNote(this.#event.invocation.id, {
|
|
102
|
+
commandId: this.#next("note.create"), noteId, textAttachments,
|
|
103
|
+
})).note;
|
|
104
|
+
}
|
|
105
|
+
async updateNote(noteId, baseVersion, textAttachments) {
|
|
106
|
+
return (await this.#client.updateNote(this.#event.invocation.id, noteId, {
|
|
107
|
+
commandId: this.#next("note.update"), baseVersion, textAttachments,
|
|
108
|
+
})).note;
|
|
109
|
+
}
|
|
110
|
+
createToDo(input) {
|
|
111
|
+
return this.#client.createToDo(this.#event.invocation.id, {
|
|
112
|
+
commandId: this.#next("todo.create"), ...input,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
updateToDo(todoId, input) {
|
|
116
|
+
return this.#client.updateToDo(this.#event.invocation.id, todoId, {
|
|
117
|
+
commandId: this.#next("todo.update"), ...input,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
completeToDo(todoId, version) {
|
|
121
|
+
return this.#client.completeToDo(this.#event.invocation.id, todoId, {
|
|
122
|
+
commandId: this.#next("todo.complete"), version,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
readThreadContext(options) {
|
|
126
|
+
return this.#client.readThread(this.#event.invocation.id, options);
|
|
127
|
+
}
|
|
128
|
+
readRecentContext(options) {
|
|
129
|
+
return this.#client.readRecent(this.#event.invocation.id, options);
|
|
130
|
+
}
|
|
131
|
+
searchContext(query, options) {
|
|
132
|
+
return this.#client.search(this.#event.invocation.id, query, options);
|
|
133
|
+
}
|
|
134
|
+
readAsset(assetId) {
|
|
135
|
+
return this.#client.readAsset(this.#event.invocation.id, assetId);
|
|
136
|
+
}
|
|
137
|
+
uploadFile(input) {
|
|
138
|
+
return this.#client.uploadFile(this.#event.invocation.id, {
|
|
139
|
+
issueCommandId: this.#next("file.upload_link.create"),
|
|
140
|
+
finalizeCommandId: this.#next("file.upload.complete"),
|
|
141
|
+
...input,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async requestQuestion(input) {
|
|
145
|
+
const commandId = this.#next("question.create");
|
|
146
|
+
const response = await this.#client.createQuestion(this.#event.invocation.id, { commandId, ...input });
|
|
147
|
+
const questionId = requiredResourceId(response.resourceId, "question");
|
|
148
|
+
await this.#client.waitForInput(this.#event.invocation.id, { commandId: this.#next("invocation.wait"), pendingActionId: questionId });
|
|
149
|
+
const answer = await this.#context.waitForInput(questionId);
|
|
150
|
+
if (answer.type !== "question.answered")
|
|
151
|
+
throw new Error("Expected a question answer.");
|
|
152
|
+
return answer;
|
|
153
|
+
}
|
|
154
|
+
async requestApproval(input) {
|
|
155
|
+
const response = await this.#client.createApproval(this.#event.invocation.id, {
|
|
156
|
+
commandId: this.#next("approval.create"), ...input,
|
|
157
|
+
parentMessageId: this.#event.event.type === "message.created" ? this.#event.event.message.id : null,
|
|
158
|
+
});
|
|
159
|
+
const approvalId = requiredResourceId(response.resourceId, "approval");
|
|
160
|
+
await this.#client.waitForInput(this.#event.invocation.id, { commandId: this.#next("invocation.wait"), pendingActionId: approvalId });
|
|
161
|
+
const answer = await this.#context.waitForInput(approvalId);
|
|
162
|
+
if (answer.type !== "approval.answered")
|
|
163
|
+
throw new Error("Expected an approval answer.");
|
|
164
|
+
return answer;
|
|
165
|
+
}
|
|
166
|
+
hasActiveHostToolGrant(key) {
|
|
167
|
+
return this.#client.hasActiveHostToolGrant(this.#event.invocation.id, key);
|
|
168
|
+
}
|
|
169
|
+
async createHostToolGrant(key) {
|
|
170
|
+
await this.#client.createHostToolGrant(this.#event.invocation.id, {
|
|
171
|
+
commandId: this.#next("approval-grant.create"),
|
|
172
|
+
...key,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async recordHostToolExecution(request, options) {
|
|
176
|
+
await this.#client.recordHostToolExecution(this.#event.invocation.id, {
|
|
177
|
+
commandId: this.#next("host-tool.execution"),
|
|
178
|
+
capability: request.capability,
|
|
179
|
+
toolName: request.toolName,
|
|
180
|
+
effectSummary: request.effectSummary,
|
|
181
|
+
workspaceRoot: request.workspaceRoot ?? null,
|
|
182
|
+
viaRememberedGrant: options.viaRememberedGrant,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
#next(operation) {
|
|
186
|
+
this.#sequence += 1;
|
|
187
|
+
return commandId(this.#event, `${operation}:${this.#sequence}`);
|
|
188
|
+
}
|
|
189
|
+
// A message is identified by what it says. Two connectors that both claim one delivery each count
|
|
190
|
+
// their own calls, so an ordinal would make one reply two commands and post it twice; the ordinal
|
|
191
|
+
// still advances so activity ordering is unchanged.
|
|
192
|
+
#nextForContent(operation, content) {
|
|
193
|
+
this.#sequence += 1;
|
|
194
|
+
const digest = createHash("sha256").update(JSON.stringify(content)).digest("hex");
|
|
195
|
+
return commandId(this.#event, `${operation}:${digest}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function commandId(event, operation) {
|
|
199
|
+
return providerResourceId("showrunner-command", `${event.deliveryId}:${operation}`);
|
|
200
|
+
}
|
|
201
|
+
function requiredResourceId(value, kind) {
|
|
202
|
+
if (value === null)
|
|
203
|
+
throw new Error(`Showrunner did not return a ${kind} id.`);
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
function normalizeUsage(usage) {
|
|
207
|
+
if (usage === undefined)
|
|
208
|
+
return null;
|
|
209
|
+
return {
|
|
210
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
211
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
212
|
+
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
213
|
+
cacheCreationTokens: usage.cacheCreationTokens ?? 0,
|
|
214
|
+
costMicros: usage.costMicros,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AdapterStateStore } from "./adapter.js";
|
|
2
|
+
/** Durable provider-session state. Values are isolated by the namespaced store passed to adapters. */
|
|
3
|
+
export declare class FileAdapterStateStore implements AdapterStateStore {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(filePath?: string);
|
|
6
|
+
get(namespace: string, key: string): Promise<unknown>;
|
|
7
|
+
set(namespace: string, key: string, value: unknown): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { chmod, lstat, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import envPaths from "env-paths";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
const persistedAdapterStateSchema = z.object({
|
|
7
|
+
version: z.literal(1),
|
|
8
|
+
values: z.record(z.string(), z.unknown()),
|
|
9
|
+
});
|
|
10
|
+
/** Durable provider-session state. Values are isolated by the namespaced store passed to adapters. */
|
|
11
|
+
export class FileAdapterStateStore {
|
|
12
|
+
#filePath;
|
|
13
|
+
#operation = Promise.resolve();
|
|
14
|
+
constructor(filePath = join(envPaths("showrunner-agent").data, "adapter-state.json")) {
|
|
15
|
+
this.#filePath = filePath;
|
|
16
|
+
}
|
|
17
|
+
get(namespace, key) {
|
|
18
|
+
return this.#serialized(async () => structuredClone((await this.#load()).values[stateKey(namespace, key)]));
|
|
19
|
+
}
|
|
20
|
+
set(namespace, key, value) {
|
|
21
|
+
return this.#serialized(async () => {
|
|
22
|
+
const state = await this.#load();
|
|
23
|
+
state.values[stateKey(namespace, key)] = structuredClone(value);
|
|
24
|
+
await this.#save(state);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
#serialized(operation) {
|
|
28
|
+
const result = this.#operation.then(operation, operation);
|
|
29
|
+
this.#operation = result.then(() => undefined, () => undefined);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
async #load() {
|
|
33
|
+
try {
|
|
34
|
+
const stat = await lstat(this.#filePath);
|
|
35
|
+
if ((stat.mode & 0o077) !== 0) {
|
|
36
|
+
throw new Error("Connector adapter state file must have mode 600.");
|
|
37
|
+
}
|
|
38
|
+
return persistedAdapterStateSchema.parse(JSON.parse(await readFile(this.#filePath, "utf8")));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
42
|
+
return { version: 1, values: {} };
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async #save(state) {
|
|
48
|
+
const validated = persistedAdapterStateSchema.parse(state);
|
|
49
|
+
const directory = dirname(this.#filePath);
|
|
50
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
51
|
+
await chmod(directory, 0o700);
|
|
52
|
+
const temporaryPath = `${this.#filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
53
|
+
const handle = await open(temporaryPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
54
|
+
try {
|
|
55
|
+
await handle.writeFile(`${JSON.stringify(validated, null, 2)}\n`, "utf8");
|
|
56
|
+
await handle.sync();
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await handle.close();
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
await rename(temporaryPath, this.#filePath);
|
|
63
|
+
await chmod(this.#filePath, 0o600);
|
|
64
|
+
const directoryHandle = await open(directory, constants.O_RDONLY);
|
|
65
|
+
try {
|
|
66
|
+
await directoryHandle.sync();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await directoryHandle.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function stateKey(namespace, key) {
|
|
79
|
+
return `${namespace}\u0000${key}`;
|
|
80
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { ActivityOutcome, ActivityType, AgentEvent, AgentInputResponse, ApprovalDecision, ApprovalRiskLevel, CommandResponse, ContextAsset, ContextPage, FileDownloadLink, OutputMessagePiece, QuestionSelectionType } from "./protocol.js";
|
|
2
|
+
import type { Note, TextAttachment } from "./protocol.js";
|
|
3
|
+
export type ContextPageOptions = {
|
|
4
|
+
cursor?: string | null;
|
|
5
|
+
limit?: number;
|
|
6
|
+
};
|
|
7
|
+
/** Due-date fields shared by task creation and task updates. */
|
|
8
|
+
export type ToDoScheduleInput = {
|
|
9
|
+
dueAt?: string | null;
|
|
10
|
+
dueAtHasTime: boolean;
|
|
11
|
+
timezone?: string | null;
|
|
12
|
+
};
|
|
13
|
+
export type AgentAdapterKind = "custom" | "codex" | "claude" | "openclaw" | "acp";
|
|
14
|
+
export type AdapterMediaCapabilities = {
|
|
15
|
+
image: "native" | "unsupported";
|
|
16
|
+
voice: "transcript" | "unsupported";
|
|
17
|
+
video: "unsupported";
|
|
18
|
+
};
|
|
19
|
+
export declare const adapterMediaCapabilities: Record<AgentAdapterKind, AdapterMediaCapabilities>;
|
|
20
|
+
export type ProviderUsage = {
|
|
21
|
+
inputTokens?: number;
|
|
22
|
+
outputTokens?: number;
|
|
23
|
+
cacheReadTokens?: number;
|
|
24
|
+
cacheCreationTokens?: number;
|
|
25
|
+
costMicros?: number;
|
|
26
|
+
};
|
|
27
|
+
export type AgentAdapterResult = {
|
|
28
|
+
status: "completed" | "failed" | "waiting" | "aborted";
|
|
29
|
+
externalSessionId?: string;
|
|
30
|
+
externalRunId?: string;
|
|
31
|
+
providerModel?: string;
|
|
32
|
+
usage?: ProviderUsage;
|
|
33
|
+
error?: {
|
|
34
|
+
code: string;
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/** One model the user's own CLI offers, as reported to the server for the installation's model picker. */
|
|
39
|
+
export type AgentModelOption = {
|
|
40
|
+
/** The value to pass back as the invocation's model. */
|
|
41
|
+
id: string;
|
|
42
|
+
/** The CLI's own human-readable label for the model. */
|
|
43
|
+
displayName: string;
|
|
44
|
+
};
|
|
45
|
+
export declare const maxReportedModelOptions = 64;
|
|
46
|
+
export declare const maxModelOptionFieldLength = 100;
|
|
47
|
+
export interface AgentAdapter {
|
|
48
|
+
readonly kind: AgentAdapterKind;
|
|
49
|
+
readonly version: string;
|
|
50
|
+
readonly mediaCapabilities: AdapterMediaCapabilities;
|
|
51
|
+
handle(event: AgentEvent, context: AgentInvocationContext, signal: AbortSignal): Promise<AgentAdapterResult>;
|
|
52
|
+
/**
|
|
53
|
+
* The models this adapter's CLI offers, or null when the CLI has no enumeration surface.
|
|
54
|
+
* Null and an empty list both mean "no picker" — neither is ever a fabricated catalog.
|
|
55
|
+
*/
|
|
56
|
+
listModels?(signal: AbortSignal): Promise<AgentModelOption[] | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Optional one-time pre-warm run once at connector startup, after connect, so the first
|
|
59
|
+
* real invocation doesn't pay a lazy handshake cost. Must be idempotent and non-fatal:
|
|
60
|
+
* the connector fires it and forgets, and a cold first invocation is an acceptable fallback.
|
|
61
|
+
* Only adapters with a reusable, session-independent warm-up implement it.
|
|
62
|
+
*/
|
|
63
|
+
warmUp?(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Bounds and de-duplicates a CLI's self-reported model list so the handshake payload stays within
|
|
67
|
+
* the registration schema no matter how much the provider returns. Returns undefined when nothing
|
|
68
|
+
* usable survives, so the handshake omits the field rather than reporting an empty picker.
|
|
69
|
+
*/
|
|
70
|
+
export declare function sanitizeModelOptions(models: AgentModelOption[] | null): AgentModelOption[] | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Enumerates an adapter's models for the handshake. A provider that cannot enumerate, returns
|
|
73
|
+
* nothing usable, or fails outright all resolve to undefined: reporting no list leaves the
|
|
74
|
+
* installation on free-text entry, which is always preferable to blocking the connection.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveAdapterModels(adapter: Pick<AgentAdapter, "listModels">, signal: AbortSignal, onFailure?: (error: unknown) => void): Promise<AgentModelOption[] | undefined>;
|
|
77
|
+
export interface AdapterStateStore {
|
|
78
|
+
get(namespace: string, key: string): Promise<unknown>;
|
|
79
|
+
set(namespace: string, key: string, value: unknown): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
export declare class MemoryAdapterStateStore implements AdapterStateStore {
|
|
82
|
+
#private;
|
|
83
|
+
get(namespace: string, key: string): Promise<unknown>;
|
|
84
|
+
set(namespace: string, key: string, value: unknown): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
export declare class NamespacedAdapterStateStore implements AdapterStateStore {
|
|
87
|
+
#private;
|
|
88
|
+
constructor(store: AdapterStateStore, ...segments: string[]);
|
|
89
|
+
get(namespace: string, key: string): Promise<unknown>;
|
|
90
|
+
set(namespace: string, key: string, value: unknown): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
export interface AdapterClient {
|
|
93
|
+
startActivity(input: {
|
|
94
|
+
activityId: string;
|
|
95
|
+
label: string;
|
|
96
|
+
type?: ActivityType;
|
|
97
|
+
orderIndex?: number;
|
|
98
|
+
}): Promise<void>;
|
|
99
|
+
completeActivity(activityId: string, outcome?: ActivityOutcome): Promise<void>;
|
|
100
|
+
sendMessage(pieces: OutputMessagePiece[]): Promise<void>;
|
|
101
|
+
createFileDownloadLink(assetId: string): Promise<FileDownloadLink>;
|
|
102
|
+
sendReaction?(messageId: string, emoji: string): Promise<void>;
|
|
103
|
+
readNote(noteId: string): Promise<Note>;
|
|
104
|
+
createNote(noteId: string, textAttachments: TextAttachment[]): Promise<Note>;
|
|
105
|
+
updateNote(noteId: string, baseVersion: number, textAttachments: TextAttachment[]): Promise<Note>;
|
|
106
|
+
createToDo(input: ToDoScheduleInput & {
|
|
107
|
+
textAttachments: TextAttachment[];
|
|
108
|
+
}): Promise<CommandResponse>;
|
|
109
|
+
updateToDo(todoId: string, input: ToDoScheduleInput & {
|
|
110
|
+
version: string;
|
|
111
|
+
textAttachments?: TextAttachment[] | null;
|
|
112
|
+
}): Promise<CommandResponse>;
|
|
113
|
+
completeToDo(todoId: string, version: string): Promise<CommandResponse>;
|
|
114
|
+
readThreadContext(options: ContextPageOptions): Promise<ContextPage>;
|
|
115
|
+
readRecentContext(options: ContextPageOptions): Promise<ContextPage>;
|
|
116
|
+
searchContext(query: string, options: ContextPageOptions): Promise<ContextPage>;
|
|
117
|
+
readAsset(assetId: string): Promise<ContextAsset>;
|
|
118
|
+
uploadFile(input: {
|
|
119
|
+
fileName: string;
|
|
120
|
+
mimeType: string;
|
|
121
|
+
bytes: Uint8Array;
|
|
122
|
+
folderId?: string | null;
|
|
123
|
+
}): Promise<{
|
|
124
|
+
assetId: string;
|
|
125
|
+
command: CommandResponse;
|
|
126
|
+
}>;
|
|
127
|
+
requestQuestion(input: {
|
|
128
|
+
title: string;
|
|
129
|
+
body: string;
|
|
130
|
+
selectionType: QuestionSelectionType;
|
|
131
|
+
choices: Array<{
|
|
132
|
+
id: string;
|
|
133
|
+
label: string;
|
|
134
|
+
}>;
|
|
135
|
+
allowWriteIn: boolean;
|
|
136
|
+
allowSkip: boolean;
|
|
137
|
+
autoResolutionMs: number;
|
|
138
|
+
}): Promise<Extract<AgentInputResponse, {
|
|
139
|
+
type: "question.answered";
|
|
140
|
+
}>>;
|
|
141
|
+
requestApproval(input: {
|
|
142
|
+
title: string;
|
|
143
|
+
body: string;
|
|
144
|
+
effectDetail: string;
|
|
145
|
+
toolName: string;
|
|
146
|
+
capability: HostToolCapability;
|
|
147
|
+
riskLevel: ApprovalRiskLevel;
|
|
148
|
+
availableDecisions: ApprovalDecision[];
|
|
149
|
+
expiresInSeconds: number;
|
|
150
|
+
canRemember: boolean;
|
|
151
|
+
}): Promise<Extract<AgentInputResponse, {
|
|
152
|
+
type: "approval.answered";
|
|
153
|
+
}>>;
|
|
154
|
+
hasActiveHostToolGrant(key: HostToolGrantKey): Promise<boolean>;
|
|
155
|
+
createHostToolGrant(key: HostToolGrantKey & {
|
|
156
|
+
approvalId: string;
|
|
157
|
+
}): Promise<void>;
|
|
158
|
+
recordHostToolExecution(request: HostToolRequest, options: {
|
|
159
|
+
viaRememberedGrant: boolean;
|
|
160
|
+
}): Promise<void>;
|
|
161
|
+
}
|
|
162
|
+
/** What an adapter may write to the connector's own log. A failure reaches the chat only as a
|
|
163
|
+
* sanitized provider error, so the operator's terminal is the one place the cause can be read in
|
|
164
|
+
* full — `SafeStructuredLogger` satisfies this and redacts tokens on the way out. */
|
|
165
|
+
export interface AdapterLogger {
|
|
166
|
+
info(event: string, fields?: Record<string, unknown>): void;
|
|
167
|
+
warn(event: string, fields?: Record<string, unknown>): void;
|
|
168
|
+
error(event: string, fields?: Record<string, unknown>): void;
|
|
169
|
+
}
|
|
170
|
+
export interface AgentInvocationContext {
|
|
171
|
+
installationId: string;
|
|
172
|
+
streamId: string;
|
|
173
|
+
sessionId: string;
|
|
174
|
+
invocationId: string;
|
|
175
|
+
providerKind: AgentAdapterKind;
|
|
176
|
+
externalSessionId: string | null;
|
|
177
|
+
model: string | null;
|
|
178
|
+
reasoningEffort: string | null;
|
|
179
|
+
client: AdapterClient;
|
|
180
|
+
state: AdapterStateStore;
|
|
181
|
+
trust: {
|
|
182
|
+
ownerProfileId: string;
|
|
183
|
+
containsNonOwnerContent: boolean;
|
|
184
|
+
};
|
|
185
|
+
scope: AgentStreamScope;
|
|
186
|
+
logger: AdapterLogger;
|
|
187
|
+
}
|
|
188
|
+
/** What the owner has already granted this agent on the stream the invocation runs in. */
|
|
189
|
+
export type AgentStreamScope = {
|
|
190
|
+
isAgentChat: boolean;
|
|
191
|
+
streamDisplayName: string | null;
|
|
192
|
+
canReadThreadHistory: boolean;
|
|
193
|
+
canReadRecentMessages: boolean;
|
|
194
|
+
canSearch: boolean;
|
|
195
|
+
canReadFiles: boolean;
|
|
196
|
+
canReadNotes: boolean;
|
|
197
|
+
canCreateMessages: boolean;
|
|
198
|
+
canCreateFiles: boolean;
|
|
199
|
+
canCreateToDos: boolean;
|
|
200
|
+
canCreateNotes: boolean;
|
|
201
|
+
canReact: boolean;
|
|
202
|
+
canRequestInput: boolean;
|
|
203
|
+
/** Owner's per-chat switch for "remember this" on host-tool approvals. */
|
|
204
|
+
rememberApprovals: boolean;
|
|
205
|
+
};
|
|
206
|
+
export type HostToolCapability = "filesystem_write" | "shell" | "network" | "credentials" | "mcp_write";
|
|
207
|
+
export type HostToolRequest = {
|
|
208
|
+
providerRequestId?: string;
|
|
209
|
+
capability: HostToolCapability;
|
|
210
|
+
toolName: string;
|
|
211
|
+
effectSummary: string;
|
|
212
|
+
effectIsGeneric?: boolean;
|
|
213
|
+
command?: string;
|
|
214
|
+
targetPath?: string;
|
|
215
|
+
cwd?: string;
|
|
216
|
+
workspaceRoot?: string;
|
|
217
|
+
};
|
|
218
|
+
export type HostToolGrantKey = {
|
|
219
|
+
installationId: string;
|
|
220
|
+
streamId: string;
|
|
221
|
+
workspaceRoot: string;
|
|
222
|
+
capability: HostToolCapability;
|
|
223
|
+
toolName: string;
|
|
224
|
+
effectHash: string;
|
|
225
|
+
};
|
|
226
|
+
export type HostToolApprovalResult = {
|
|
227
|
+
allowed: boolean;
|
|
228
|
+
viaRememberedGrant: boolean;
|
|
229
|
+
};
|
|
230
|
+
export declare function requestHostToolApproval(context: AgentInvocationContext, request: HostToolRequest, input: {
|
|
231
|
+
title: string;
|
|
232
|
+
body: string;
|
|
233
|
+
}, signal?: AbortSignal): Promise<HostToolApprovalResult>;
|
|
234
|
+
export declare function sanitizeProviderError(provider: string, cause?: unknown): {
|
|
235
|
+
code: string;
|
|
236
|
+
message: string;
|
|
237
|
+
};
|
|
238
|
+
export declare function sanitizeApprovalText(value: string, limit: number): string;
|
|
239
|
+
export declare function messageText(event: AgentEvent): string;
|
|
240
|
+
export declare function providerResourceId(namespace: string, value: string): string;
|