@vellumai/assistant 0.12.0-staging.1 → 0.12.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/Dockerfile +5 -0
- package/docs/architecture/turn-actor.md +9 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/app-icons/package.json +18 -0
- package/node_modules/@vellumai/app-icons/src/index.test.ts +85 -0
- package/node_modules/@vellumai/app-icons/src/index.ts +387 -0
- package/node_modules/@vellumai/app-icons/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +15 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/openapi.yaml +628 -0
- package/package.json +3 -1
- package/scripts/smoke-container-workspace-dependencies.ts +19 -0
- package/src/__tests__/app-builder-icon-names.test.ts +29 -0
- package/src/__tests__/assistant-attachment-directive.test.ts +4 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +2 -0
- package/src/__tests__/conversation-attachments.test.ts +142 -0
- package/src/__tests__/conversation-delete-activation-progress.test.ts +145 -0
- package/src/__tests__/conversation-event-sink.test.ts +50 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -2
- package/src/__tests__/conversation-queue.test.ts +297 -0
- package/src/__tests__/credential-routes.test.ts +185 -6
- package/src/__tests__/drain-kick-guard.test.ts +2 -0
- package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
- package/src/__tests__/messaging-send-tool.test.ts +42 -0
- package/src/__tests__/oauth-commands-routes.test.ts +47 -0
- package/src/__tests__/subagent-manager-notify.test.ts +25 -4
- package/src/activation/progress-store.test.ts +1564 -0
- package/src/activation/progress-store.ts +1296 -0
- package/src/activation/turn-hooks.test.ts +246 -0
- package/src/activation/turn-hooks.ts +126 -0
- package/src/api/events/subagent-status-changed.ts +7 -5
- package/src/api/responses/activation.ts +136 -0
- package/src/apps/app-store.ts +8 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +7 -5
- package/src/cli/commands/__tests__/cli-test-harness.ts +12 -3
- package/src/cli/commands/channels/__tests__/channels.test.ts +2 -0
- package/src/cli/commands/channels/__tests__/request.test.ts +191 -0
- package/src/cli/commands/channels/index.help.ts +70 -18
- package/src/cli/commands/channels/index.ts +17 -6
- package/src/cli/commands/channels/request.ts +66 -0
- package/src/cli/commands/oauth/request.test.ts +2 -0
- package/src/cli/commands/oauth/request.ts +227 -186
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -1
- package/src/config/bundled-skills/app-builder/TOOLS.json +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +8 -4
- package/src/config/feature-flag-registry.json +35 -5
- package/src/daemon/assistant-attachments.ts +11 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +5 -0
- package/src/daemon/conversation-agent-loop.ts +71 -3
- package/src/daemon/conversation-attachments.ts +42 -1
- package/src/daemon/conversation-event-sink.ts +25 -0
- package/src/daemon/conversation-process.ts +69 -21
- package/src/daemon/conversation-store.ts +4 -3
- package/src/daemon/conversation-surfaces.ts +19 -4
- package/src/daemon/message-types/sync.ts +2 -0
- package/src/ipc/assistant-server.ts +2 -0
- package/src/ipc/routes/__tests__/activation-sync-ipc-routes.test.ts +51 -0
- package/src/ipc/routes/activation-sync-ipc-routes.ts +42 -0
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/proactive-home-thread.test.ts +111 -0
- package/src/notifications/conversation-pairing.ts +47 -5
- package/src/notifications/delivered-post-record.ts +3 -0
- package/src/persistence/__tests__/slack-thread-root-evidence.test.ts +102 -0
- package/src/persistence/conversation-crud.ts +39 -0
- package/src/persistence/delivery-crud.ts +13 -0
- package/src/plugins/AGENTS.md +1 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +37 -0
- package/src/runtime/routes/__tests__/user-routes-notices.test.ts +248 -0
- package/src/runtime/routes/activation-routes.test.ts +415 -0
- package/src/runtime/routes/activation-routes.ts +172 -0
- package/src/runtime/routes/credential-routes.ts +46 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/oauth-commands-routes.ts +21 -8
- package/src/runtime/routes/platform-managed-credentials.ts +48 -0
- package/src/runtime/routes/secret-routes.ts +3 -12
- package/src/runtime/routes/user-route-resolution.ts +21 -0
- package/src/runtime/routes/user-routes.ts +112 -9
- package/src/runtime/sync/activation-sidecar-publish.test.ts +78 -0
- package/src/runtime/sync/documents-sidecar-publish.test.ts +3 -0
- package/src/runtime/sync/resource-sync-events.ts +23 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +36 -0
- package/src/runtime/sync/worker-daemon-notify.ts +39 -1
- package/src/tools/apps/executors.ts +8 -8
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// The command runs the authenticated-request route in-process, as `oauth
|
|
4
|
+
// request` does. The route is stubbed so the tests see exactly what provider
|
|
5
|
+
// key and request shape the command hands it; IPC is stubbed to prove the
|
|
6
|
+
// command never takes that path. The exit-code helpers stay real, so the
|
|
7
|
+
// tests assert the mapping that ships.
|
|
8
|
+
const ipcCalls: string[] = [];
|
|
9
|
+
const handleRequestCalls: Array<{ body: Record<string, unknown> }> = [];
|
|
10
|
+
|
|
11
|
+
const actualCliClient = await import("../../../../ipc/cli-client.js");
|
|
12
|
+
mock.module("../../../../ipc/cli-client.js", () => ({
|
|
13
|
+
...actualCliClient,
|
|
14
|
+
cliIpcCall: async (method: string) => {
|
|
15
|
+
ipcCalls.push(method);
|
|
16
|
+
return { ok: false, error: `Unexpected IPC method ${method}` };
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
mock.module("../../../../runtime/routes/oauth-commands-routes.js", () => ({
|
|
21
|
+
handleRequest: async (args: { body: Record<string, unknown> }) => {
|
|
22
|
+
handleRequestCalls.push(args);
|
|
23
|
+
return {
|
|
24
|
+
ok: true,
|
|
25
|
+
status: 200,
|
|
26
|
+
headers: { "content-type": "application/json" },
|
|
27
|
+
body: { ok: true, user_id: "U0BOT" },
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import { botProviderForChannel } from "@vellumai/service-contracts/channels";
|
|
33
|
+
import type { Command } from "commander";
|
|
34
|
+
|
|
35
|
+
import { runCliCommand } from "../../__tests__/cli-test-harness.js";
|
|
36
|
+
import { registerChannelsCommand } from "../index.js";
|
|
37
|
+
import { REQUESTABLE_CHANNELS } from "../request.js";
|
|
38
|
+
|
|
39
|
+
const { exitCodeFromIpcResult } = actualCliClient;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
ipcCalls.length = 0;
|
|
43
|
+
handleRequestCalls.length = 0;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The shipped registration, plus the `--json` leaf option the program
|
|
48
|
+
* normally attaches after every command is registered.
|
|
49
|
+
*/
|
|
50
|
+
function registerChannels(program: Command): void {
|
|
51
|
+
registerChannelsCommand(program);
|
|
52
|
+
program.commands
|
|
53
|
+
.find((c) => c.name() === "channels")
|
|
54
|
+
?.commands.find((c) => c.name() === "request")
|
|
55
|
+
?.option("--json");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function runChannelsRequest(args: string[]) {
|
|
59
|
+
return runCliCommand(registerChannels, ["channels", "request", ...args]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("channel to bot provider", () => {
|
|
63
|
+
test("names the bot credential's provider for every built-in bot channel", () => {
|
|
64
|
+
// The map is the contract's, not a copy: the command answers exactly what
|
|
65
|
+
// CHANNEL_BOT_PROVIDER says, including telegram, whose bot key carries no
|
|
66
|
+
// suffix because Telegram has no user-identity integration.
|
|
67
|
+
expect(botProviderForChannel("slack")).toBe("slack_channel");
|
|
68
|
+
expect(botProviderForChannel("discord")).toBe("discord_channel");
|
|
69
|
+
expect(botProviderForChannel("telegram")).toBe("telegram");
|
|
70
|
+
expect([...REQUESTABLE_CHANNELS].sort()).toEqual([
|
|
71
|
+
"discord",
|
|
72
|
+
"slack",
|
|
73
|
+
"telegram",
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a channel without a bot credential resolves to nothing", () => {
|
|
78
|
+
expect(botProviderForChannel("phone")).toBeUndefined();
|
|
79
|
+
expect(botProviderForChannel("vellum")).toBeUndefined();
|
|
80
|
+
expect(botProviderForChannel("not-a-channel")).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("an inherited object property is not a channel", () => {
|
|
84
|
+
expect(botProviderForChannel("constructor")).toBeUndefined();
|
|
85
|
+
expect(botProviderForChannel("toString")).toBeUndefined();
|
|
86
|
+
expect(botProviderForChannel("__proto__")).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("assistant channels request", () => {
|
|
91
|
+
test("requests as the channel's bot, in-process, never over IPC", async () => {
|
|
92
|
+
const { stdout, exitCode } = await runChannelsRequest([
|
|
93
|
+
"slack",
|
|
94
|
+
"-s",
|
|
95
|
+
"/auth.test",
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
expect(exitCode).toBe(0);
|
|
99
|
+
expect(ipcCalls).toEqual([]);
|
|
100
|
+
expect(handleRequestCalls).toEqual([
|
|
101
|
+
{ body: { provider: "slack_channel", url: "/auth.test" } },
|
|
102
|
+
]);
|
|
103
|
+
expect(stdout).toContain("U0BOT");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("forwards method and JSON body the way oauth request does", async () => {
|
|
107
|
+
await runChannelsRequest([
|
|
108
|
+
"slack",
|
|
109
|
+
"-s",
|
|
110
|
+
"-X",
|
|
111
|
+
"POST",
|
|
112
|
+
"-d",
|
|
113
|
+
'{"channel":"D0123456789","limit":20}',
|
|
114
|
+
"/conversations.history",
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
expect(handleRequestCalls).toEqual([
|
|
118
|
+
{
|
|
119
|
+
body: {
|
|
120
|
+
provider: "slack_channel",
|
|
121
|
+
url: "/conversations.history",
|
|
122
|
+
method: "POST",
|
|
123
|
+
parsed_data: { channel: "D0123456789", limit: 20 },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("never offers the person's OAuth integration for a bot channel", async () => {
|
|
130
|
+
await runChannelsRequest(["discord", "-s", "/users/@me"]);
|
|
131
|
+
|
|
132
|
+
expect(handleRequestCalls[0]?.body.provider).toBe("discord_channel");
|
|
133
|
+
expect(handleRequestCalls[0]?.body).not.toHaveProperty("account");
|
|
134
|
+
expect(handleRequestCalls[0]?.body).not.toHaveProperty("client_id");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a channel with no bot credential is refused before any request", async () => {
|
|
138
|
+
const { stderr, exitCode } = await runChannelsRequest([
|
|
139
|
+
"phone",
|
|
140
|
+
"/anything",
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
expect(exitCode).toBe(1);
|
|
144
|
+
expect(handleRequestCalls).toEqual([]);
|
|
145
|
+
expect(stderr).toContain('Channel "phone" has no bot credential');
|
|
146
|
+
expect(stderr).toContain("plugins search");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("names the channel command, not the OAuth one, in its diagnostics hint", async () => {
|
|
150
|
+
mock.module("../../../../runtime/routes/oauth-commands-routes.js", () => ({
|
|
151
|
+
handleRequest: async () => {
|
|
152
|
+
throw new Error("boom");
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
const { stderr, exitCode } = await runChannelsRequest([
|
|
157
|
+
"slack",
|
|
158
|
+
"/auth.test",
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
expect(exitCode).toBe(1);
|
|
162
|
+
expect(stderr).toContain("assistant channels get slack");
|
|
163
|
+
expect(stderr).not.toContain("oauth providers get");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a structured route failure keeps the JSON envelope and the hint", async () => {
|
|
167
|
+
const { RouteError } = await import("../../../../runtime/routes/errors.js");
|
|
168
|
+
mock.module("../../../../runtime/routes/oauth-commands-routes.js", () => ({
|
|
169
|
+
handleRequest: async () => {
|
|
170
|
+
throw new RouteError(
|
|
171
|
+
"Provider not configured: slack_channel",
|
|
172
|
+
"NOT_FOUND",
|
|
173
|
+
404,
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
const { stdout, exitCode } = await runChannelsRequest([
|
|
179
|
+
"slack",
|
|
180
|
+
"--json",
|
|
181
|
+
"/auth.test",
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
// The exit code is the one the route's status maps to for every command.
|
|
185
|
+
expect(exitCode).toBe(exitCodeFromIpcResult({ statusCode: 404 }));
|
|
186
|
+
const envelope = JSON.parse(stdout.trim());
|
|
187
|
+
expect(envelope.ok).toBe(false);
|
|
188
|
+
expect(envelope.error).toContain("Provider not configured");
|
|
189
|
+
expect(envelope.error).toContain("assistant channels get slack");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -1,40 +1,92 @@
|
|
|
1
1
|
/** Declarative help for the `assistant channels` command. */
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
CHANNEL_BOT_PROVIDER,
|
|
5
|
+
CHANNEL_IDS,
|
|
6
|
+
} from "@vellumai/service-contracts/channels";
|
|
7
|
+
|
|
3
8
|
import type { CliCommandHelp } from "../../lib/cli-command-help.js";
|
|
4
9
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"a2a",
|
|
15
|
-
] as const;
|
|
10
|
+
/**
|
|
11
|
+
* The channel ids the readiness route accepts: the canonical vocabulary. A
|
|
12
|
+
* channel with no built-in probe reports as unsupported rather than being
|
|
13
|
+
* refused, so the list is the contract's, not a copy kept here.
|
|
14
|
+
*/
|
|
15
|
+
export const KNOWN_CHANNELS: readonly string[] = CHANNEL_IDS;
|
|
16
|
+
|
|
17
|
+
/** The channels whose bot the assistant can act as. */
|
|
18
|
+
const BOT_CHANNELS = Object.keys(CHANNEL_BOT_PROVIDER).join(", ");
|
|
16
19
|
|
|
17
20
|
export const CHANNELS_PLUGIN_SEARCH_HINT =
|
|
18
21
|
"If the channel you are looking for is not listed, search the plugin marketplace with 'assistant plugins search <name>'.";
|
|
19
22
|
|
|
20
23
|
export const channelsHelp: CliCommandHelp = {
|
|
21
24
|
name: "channels",
|
|
22
|
-
description: "Inspect and
|
|
25
|
+
description: "Inspect messaging channels and act as their bots",
|
|
23
26
|
helpText: `
|
|
24
|
-
Channels are the messaging surfaces the assistant talks over.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
Channels are the messaging surfaces the assistant talks over. Channel ids:
|
|
28
|
+
${KNOWN_CHANNELS.join(", ")}. A channel without a built-in readiness probe
|
|
29
|
+
reports as unsupported.
|
|
27
30
|
|
|
28
31
|
${CHANNELS_PLUGIN_SEARCH_HINT} Plugins can bundle additional channels
|
|
29
32
|
from other Vellum users.
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
Two identities can reach a channel's platform. 'request' acts as the
|
|
35
|
+
assistant's own bot on that channel (${BOT_CHANNELS}), resolving the bot
|
|
36
|
+
credential from the channel id so no token is handled. Acting as a person
|
|
37
|
+
through their OAuth integration is 'assistant oauth request'.
|
|
33
38
|
|
|
34
39
|
Examples:
|
|
35
40
|
$ assistant channels list
|
|
36
|
-
$ assistant channels get slack
|
|
41
|
+
$ assistant channels get slack
|
|
42
|
+
$ assistant channels request slack /auth.test --json
|
|
43
|
+
$ assistant channels request slack -X POST \\
|
|
44
|
+
-d '{"channel":"D0123456789","limit":20}' /conversations.history --json`,
|
|
37
45
|
subcommands: [
|
|
46
|
+
{
|
|
47
|
+
name: "request",
|
|
48
|
+
args: "<channel> <url>",
|
|
49
|
+
description:
|
|
50
|
+
"Call the channel's platform API as the assistant's own bot on that channel (curl-like interface)",
|
|
51
|
+
// The request-shaping options (-X, -H, -d, -G, -I, -o, -s, -v, -i) are
|
|
52
|
+
// registered imperatively in request.ts: the repeatable "-H, --header"
|
|
53
|
+
// flag needs a Commander collect parser the declarative contract cannot
|
|
54
|
+
// express, and the options must keep their registration order around
|
|
55
|
+
// it.
|
|
56
|
+
helpText: `
|
|
57
|
+
Makes one authenticated request to the channel's platform API as the
|
|
58
|
+
assistant's bot. The bot credential is the one the channel's setup wizard
|
|
59
|
+
stored; it is resolved from the channel and sent from inside the assistant,
|
|
60
|
+
never printed or passed on the command line. Acting as a person through
|
|
61
|
+
their OAuth integration is a different identity and stays on
|
|
62
|
+
'assistant oauth request --provider <key>'.
|
|
63
|
+
|
|
64
|
+
This command can do anything the bot's API allows, including sending,
|
|
65
|
+
editing, deleting, uploading, and reacting, so it is classified high risk
|
|
66
|
+
and asks for approval like any action with irreversible effects. Reads are
|
|
67
|
+
not distinguished from writes: the effect is the endpoint's. To send a
|
|
68
|
+
message, use the messaging tool, which records what it sent; do not post
|
|
69
|
+
through this command.
|
|
70
|
+
|
|
71
|
+
Arguments:
|
|
72
|
+
<channel> One of: ${BOT_CHANNELS}. A channel with no bot credential of
|
|
73
|
+
its own (phone, vellum) is refused.
|
|
74
|
+
<url> The API method path, relative to the platform's API host
|
|
75
|
+
(for Slack, '/conversations.history'; for Discord, '/users/@me';
|
|
76
|
+
for Telegram, '/getMe'). The provider supplies the host.
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
-X <method> HTTP method (default: GET; POST when -d is given).
|
|
80
|
+
-H 'Key: Value' Request header, repeatable.
|
|
81
|
+
-d <data> Request body: inline JSON, @file, or @- for stdin.
|
|
82
|
+
--json Machine-readable envelope: ok, status, headers, body.
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
$ assistant channels request slack /auth.test --json
|
|
86
|
+
$ assistant channels request slack -X POST \\
|
|
87
|
+
-d '{"channel":"D0123456789","limit":100}' /conversations.history --json
|
|
88
|
+
$ assistant channels request discord /users/@me --json`,
|
|
89
|
+
},
|
|
38
90
|
{
|
|
39
91
|
name: "list",
|
|
40
92
|
description: "Show readiness state for every configured channel",
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `assistant channels
|
|
2
|
+
* `assistant channels`: inspect messaging channels, and act as their bots.
|
|
3
3
|
*
|
|
4
|
-
* list
|
|
5
|
-
* get <channel>
|
|
4
|
+
* list : overview of every channel + ready state
|
|
5
|
+
* get <channel> : detailed live snapshot of a single channel
|
|
6
|
+
* request <channel> <url> : call the channel's platform API as its bot
|
|
6
7
|
*
|
|
7
8
|
* `get` always re-runs remote probes (it invalidates the readiness cache
|
|
8
9
|
* before reading), so the CLI answer matches the live source-of-truth.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* `request` is the bot-identity door (see ./request.ts): it resolves the
|
|
12
|
+
* channel's bot credential and makes the same authenticated request
|
|
13
|
+
* `oauth request` makes, without the caller naming a provider.
|
|
14
|
+
*
|
|
15
|
+
* A mutating `refresh` verb (for reconnecting channels, e.g. supplying
|
|
16
|
+
* fresh Slack tokens) is intentionally not part of this group.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
import type { Command } from "commander";
|
|
@@ -21,6 +25,7 @@ import { registerCommand } from "../../lib/register-command.js";
|
|
|
21
25
|
import { log } from "../../logger.js";
|
|
22
26
|
import { shouldOutputJson, writeOutput } from "../../output.js";
|
|
23
27
|
import { CHANNELS_PLUGIN_SEARCH_HINT, channelsHelp } from "./index.help.js";
|
|
28
|
+
import { registerChannelsRequestCommand } from "./request.js";
|
|
24
29
|
|
|
25
30
|
// ---------------------------------------------------------------------------
|
|
26
31
|
// Snapshot shape
|
|
@@ -200,6 +205,12 @@ export function registerChannelsCommand(program: Command): void {
|
|
|
200
205
|
}
|
|
201
206
|
},
|
|
202
207
|
);
|
|
208
|
+
|
|
209
|
+
// -----------------------------------------------------------------------
|
|
210
|
+
// request: call the channel's platform API as its bot
|
|
211
|
+
// -----------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
registerChannelsRequestCommand(channels);
|
|
203
214
|
},
|
|
204
215
|
});
|
|
205
216
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `assistant channels request`: call a channel's platform API as the
|
|
3
|
+
* assistant's own bot on that channel.
|
|
4
|
+
*
|
|
5
|
+
* This is the bot-identity door. A channel's bot credential (the token the
|
|
6
|
+
* setup wizard stored) and a person's OAuth integration for the same brand
|
|
7
|
+
* are different identities that reach different things, and the provider
|
|
8
|
+
* keys do not say which is which (`slack_channel` is the bot, `slack` is the
|
|
9
|
+
* person; `telegram` is the bot). Keying the request on the channel id
|
|
10
|
+
* resolves the bot credential through the one map that owns that fact,
|
|
11
|
+
* `CHANNEL_BOT_PROVIDER`, so a caller names the channel and never chooses a
|
|
12
|
+
* provider. Acting as the person is `oauth request`.
|
|
13
|
+
*
|
|
14
|
+
* The request itself is the same authenticated request `oauth request`
|
|
15
|
+
* makes: the route resolves the provider's connection and injects the
|
|
16
|
+
* credential, so no token is ever handled here. A channel this map does not
|
|
17
|
+
* name has no bot credential of its own (phone, vellum, platform) or is not
|
|
18
|
+
* built in, and the command says so rather than guessing a provider.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
botProviderForChannel,
|
|
23
|
+
CHANNEL_BOT_PROVIDER,
|
|
24
|
+
} from "@vellumai/service-contracts/channels";
|
|
25
|
+
import type { Command } from "commander";
|
|
26
|
+
|
|
27
|
+
import { subcommand } from "../../lib/cli-command-help.js";
|
|
28
|
+
import { writeError } from "../../output.js";
|
|
29
|
+
import {
|
|
30
|
+
attachRequestOptions,
|
|
31
|
+
type AuthenticatedRequestOptions,
|
|
32
|
+
runAuthenticatedRequest,
|
|
33
|
+
} from "../oauth/request.js";
|
|
34
|
+
import { CHANNELS_PLUGIN_SEARCH_HINT } from "./index.help.js";
|
|
35
|
+
|
|
36
|
+
/** The channels whose bot credential this command can act with. */
|
|
37
|
+
export const REQUESTABLE_CHANNELS: readonly string[] =
|
|
38
|
+
Object.keys(CHANNEL_BOT_PROVIDER);
|
|
39
|
+
|
|
40
|
+
export function registerChannelsRequestCommand(channels: Command): void {
|
|
41
|
+
attachRequestOptions(subcommand(channels, "request")).action(
|
|
42
|
+
async (
|
|
43
|
+
channel: string,
|
|
44
|
+
url: string,
|
|
45
|
+
opts: AuthenticatedRequestOptions,
|
|
46
|
+
cmd: Command,
|
|
47
|
+
) => {
|
|
48
|
+
const providerKey = botProviderForChannel(channel);
|
|
49
|
+
if (!providerKey) {
|
|
50
|
+
writeError(
|
|
51
|
+
cmd,
|
|
52
|
+
`Channel "${channel}" has no bot credential to request with. Channels with one: ${REQUESTABLE_CHANNELS.join(", ")}. ${CHANNELS_PLUGIN_SEARCH_HINT}`,
|
|
53
|
+
);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await runAuthenticatedRequest({
|
|
58
|
+
providerKey,
|
|
59
|
+
url,
|
|
60
|
+
opts,
|
|
61
|
+
diagnosticsHint: `For channel diagnostics, run 'assistant channels get ${channel}'.`,
|
|
62
|
+
cmd,
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -33,6 +33,8 @@ mock.module("../../../ipc/cli-client.js", () => ({
|
|
|
33
33
|
exitFromIpcResult: (r: { error?: string }) => {
|
|
34
34
|
throw new Error(r.error ?? "IPC error");
|
|
35
35
|
},
|
|
36
|
+
exitCodeFromIpcResult: (r: { statusCode?: number }) =>
|
|
37
|
+
r.statusCode === undefined ? 10 : r.statusCode >= 500 ? 3 : 1,
|
|
36
38
|
}));
|
|
37
39
|
|
|
38
40
|
mock.module("../../../runtime/routes/oauth-commands-routes.js", () => ({
|