@gjermundgaraba/effect-actions 0.10.0 → 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.
Files changed (214) hide show
  1. package/README.md +103 -43
  2. package/dist/Action.d.mts +31 -118
  3. package/dist/Action.mjs +15 -53
  4. package/dist/ActionCli.d.mts +60 -28
  5. package/dist/ActionCli.mjs +54 -48
  6. package/dist/ActionHttp.d.mts +18 -67
  7. package/dist/ActionHttp.mjs +20 -70
  8. package/dist/ActionMcp.d.mts +25 -31
  9. package/dist/ActionMcp.mjs +140 -74
  10. package/dist/ActionRpc.d.mts +147 -0
  11. package/dist/ActionRpc.mjs +113 -0
  12. package/dist/ActionToolkit.d.mts +5 -8
  13. package/dist/ActionToolkit.mjs +3 -3
  14. package/dist/Authentication.d.mts +15 -15
  15. package/dist/Authentication.mjs +42 -96
  16. package/dist/Testing.d.mts +32 -25
  17. package/dist/Testing.mjs +45 -30
  18. package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
  19. package/dist/authentication/provider.mjs +12 -0
  20. package/dist/authentication/refusal.mjs +36 -0
  21. package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
  22. package/dist/cli/commands.mjs +302 -0
  23. package/dist/{internal → contract}/call.d.mts +2 -2
  24. package/dist/contract/call.mjs +10 -0
  25. package/dist/{internal → contract}/errors.d.mts +13 -10
  26. package/dist/{internal → contract}/errors.mjs +9 -14
  27. package/dist/{internal → contract}/implementation.d.mts +37 -21
  28. package/dist/{internal → contract}/implementation.mjs +18 -49
  29. package/dist/contract/media.d.mts +13 -0
  30. package/dist/contract/media.mjs +18 -0
  31. package/dist/contract/rules.d.mts +25 -0
  32. package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
  33. package/dist/{internal → http}/client.d.mts +7 -9
  34. package/dist/http/client.mjs +17 -0
  35. package/dist/http/declared.mjs +20 -0
  36. package/dist/http/schema-errors.mjs +9 -0
  37. package/dist/mcp/protocol.d.mts +17 -0
  38. package/dist/mcp/protocol.mjs +149 -0
  39. package/dist/stdio/console.mjs +64 -0
  40. package/dist/{internal → testing}/memory.d.mts +1 -1
  41. package/dist/{internal → testing}/memory.mjs +5 -17
  42. package/dist/toolkit/tools.mjs +29 -0
  43. package/docs/Action-failures.md +47 -0
  44. package/docs/Action-in-process.md +93 -0
  45. package/docs/Action.md +206 -424
  46. package/docs/ActionCli-failures.md +102 -0
  47. package/docs/ActionCli.md +206 -292
  48. package/docs/ActionHttp-failures.md +83 -0
  49. package/docs/ActionHttp.md +202 -281
  50. package/docs/ActionMcp-failures.md +31 -0
  51. package/docs/ActionMcp.md +229 -193
  52. package/docs/ActionRpc-failures.md +21 -0
  53. package/docs/ActionRpc.md +178 -0
  54. package/docs/ActionToolkit-failures.md +15 -0
  55. package/docs/ActionToolkit.md +100 -117
  56. package/docs/Authentication-advanced.md +139 -0
  57. package/docs/Authentication-failures.md +99 -0
  58. package/docs/Authentication.md +142 -392
  59. package/docs/CONTEXT.md +39 -23
  60. package/docs/README.md +71 -35
  61. package/docs/Testing-failures.md +20 -0
  62. package/docs/Testing.md +186 -151
  63. package/docs/guarantees.md +35 -95
  64. package/docs/setup-failures.md +34 -0
  65. package/docs/setup.md +88 -87
  66. package/examples/README.md +68 -0
  67. package/examples/assistant/README.md +92 -0
  68. package/examples/assistant/src/agent.ts +92 -0
  69. package/examples/assistant/src/cli.ts +34 -0
  70. package/examples/assistant/src/commands.ts +25 -0
  71. package/examples/assistant/src/identity/Identity.ts +33 -0
  72. package/examples/assistant/src/identity/authorize.ts +24 -0
  73. package/examples/assistant/src/identity/handlers.ts +10 -0
  74. package/examples/assistant/src/identity/members.ts +9 -0
  75. package/examples/assistant/src/identity/permissions.ts +23 -0
  76. package/examples/assistant/src/scripted.ts +23 -0
  77. package/examples/assistant/src/tasks/Tasks.ts +63 -0
  78. package/examples/assistant/src/tasks/handlers.ts +25 -0
  79. package/examples/assistant/src/tasks/store.ts +96 -0
  80. package/examples/assistant/src/toolkit.ts +30 -0
  81. package/examples/assistant/test/commands.test.ts +57 -0
  82. package/examples/assistant/test/identity/identity.test.ts +43 -0
  83. package/examples/assistant/test/tasks/store.test.ts +48 -0
  84. package/examples/assistant/test/tasks/tasks.test.ts +75 -0
  85. package/examples/assistant/test/toolkit.test.ts +121 -0
  86. package/examples/kitchen-sink/README.md +317 -0
  87. package/examples/kitchen-sink/src/Actions.ts +19 -0
  88. package/examples/kitchen-sink/src/Api.ts +24 -0
  89. package/examples/kitchen-sink/src/Rpc.ts +12 -0
  90. package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
  91. package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
  92. package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
  93. package/examples/kitchen-sink/src/admin/grants.ts +22 -0
  94. package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
  95. package/examples/kitchen-sink/src/admin/keys.ts +8 -0
  96. package/examples/kitchen-sink/src/agent.ts +47 -0
  97. package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
  98. package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
  99. package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
  100. package/examples/kitchen-sink/src/assistant/handlers.ts +146 -0
  101. package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
  102. package/examples/kitchen-sink/src/attachments/download.ts +49 -0
  103. package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
  104. package/examples/kitchen-sink/src/attachments/store.ts +58 -0
  105. package/examples/kitchen-sink/src/browser.ts +33 -0
  106. package/examples/kitchen-sink/src/cli.ts +85 -0
  107. package/examples/kitchen-sink/src/config.ts +18 -0
  108. package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
  109. package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
  110. package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
  111. package/examples/kitchen-sink/src/identity/directory.ts +52 -0
  112. package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
  113. package/examples/kitchen-sink/src/identity/members.ts +13 -0
  114. package/examples/kitchen-sink/src/identity/quota.ts +62 -0
  115. package/examples/kitchen-sink/src/issues/Issues.ts +109 -0
  116. package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
  117. package/examples/kitchen-sink/src/issues/guide.ts +37 -0
  118. package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
  119. package/examples/kitchen-sink/src/issues/store.ts +96 -0
  120. package/examples/kitchen-sink/src/issues/write-limit.ts +41 -0
  121. package/examples/kitchen-sink/src/memory.ts +30 -0
  122. package/examples/kitchen-sink/src/ops.ts +42 -0
  123. package/examples/kitchen-sink/src/request-policy.ts +63 -0
  124. package/examples/kitchen-sink/src/routes.ts +120 -0
  125. package/examples/kitchen-sink/src/sdk.ts +46 -0
  126. package/examples/kitchen-sink/src/seed.ts +65 -0
  127. package/examples/kitchen-sink/src/server.ts +62 -0
  128. package/examples/kitchen-sink/src/stdio.ts +34 -0
  129. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
  130. package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
  131. package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
  132. package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
  133. package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
  134. package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
  135. package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
  136. package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
  137. package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
  138. package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
  139. package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
  140. package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
  141. package/examples/kitchen-sink/test/routes.test.ts +123 -0
  142. package/examples/kitchen-sink/test/support/scripted.ts +40 -0
  143. package/examples/live-rpc/README.md +107 -0
  144. package/examples/live-rpc/src/Rpc.ts +8 -0
  145. package/examples/live-rpc/src/browser.ts +98 -0
  146. package/examples/live-rpc/src/chat/Chat.ts +75 -0
  147. package/examples/live-rpc/src/chat/authorize.ts +16 -0
  148. package/examples/live-rpc/src/chat/handlers.ts +50 -0
  149. package/examples/live-rpc/src/chat/room.ts +23 -0
  150. package/examples/live-rpc/src/chat/store.ts +54 -0
  151. package/examples/live-rpc/src/client.ts +37 -0
  152. package/examples/live-rpc/src/identity/Identity.ts +19 -0
  153. package/examples/live-rpc/src/identity/members.ts +8 -0
  154. package/examples/live-rpc/src/identity/verifier.ts +23 -0
  155. package/examples/live-rpc/src/routes.ts +22 -0
  156. package/examples/live-rpc/src/server.ts +43 -0
  157. package/examples/live-rpc/test/browser.test.ts +105 -0
  158. package/examples/live-rpc/test/chat/chat.test.ts +69 -0
  159. package/examples/live-rpc/test/routes.test.ts +99 -0
  160. package/examples/live-rpc/test/server.test.ts +75 -0
  161. package/examples/mcp-tools/README.md +136 -0
  162. package/examples/mcp-tools/src/instructions.ts +12 -0
  163. package/examples/mcp-tools/src/printers/Printers.ts +119 -0
  164. package/examples/mcp-tools/src/printers/fake.ts +115 -0
  165. package/examples/mcp-tools/src/printers/farm.ts +136 -0
  166. package/examples/mcp-tools/src/printers/handlers.ts +29 -0
  167. package/examples/mcp-tools/src/routes.ts +52 -0
  168. package/examples/mcp-tools/src/server.ts +30 -0
  169. package/examples/mcp-tools/src/stdio.ts +26 -0
  170. package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
  171. package/examples/mcp-tools/test/routes.test.ts +202 -0
  172. package/examples/notes-service/README.md +108 -0
  173. package/examples/notes-service/src/Api.ts +13 -0
  174. package/examples/notes-service/src/browser.ts +41 -0
  175. package/examples/notes-service/src/cli.ts +22 -0
  176. package/examples/notes-service/src/commands.ts +89 -0
  177. package/examples/notes-service/src/identity/Identity.ts +31 -0
  178. package/examples/notes-service/src/identity/authorize.ts +18 -0
  179. package/examples/notes-service/src/identity/handlers.ts +10 -0
  180. package/examples/notes-service/src/identity/users.ts +9 -0
  181. package/examples/notes-service/src/identity/verifier.ts +36 -0
  182. package/examples/notes-service/src/notes/Notes.ts +91 -0
  183. package/examples/notes-service/src/notes/handlers.ts +33 -0
  184. package/examples/notes-service/src/notes/store.ts +73 -0
  185. package/examples/notes-service/src/routes.ts +45 -0
  186. package/examples/notes-service/src/server.ts +26 -0
  187. package/examples/notes-service/src/status/Status.ts +10 -0
  188. package/examples/notes-service/src/status/handlers.ts +14 -0
  189. package/examples/notes-service/src/stdio.ts +25 -0
  190. package/examples/notes-service/test/commands.test.ts +134 -0
  191. package/examples/notes-service/test/notes/notes.test.ts +103 -0
  192. package/examples/notes-service/test/routes.test.ts +84 -0
  193. package/examples/quickstart/README.md +31 -0
  194. package/examples/quickstart/src/Api.ts +5 -0
  195. package/examples/quickstart/src/client.ts +13 -0
  196. package/examples/quickstart/src/greeting/Greeting.ts +12 -0
  197. package/examples/quickstart/src/greeting/handlers.ts +6 -0
  198. package/examples/quickstart/src/routes.ts +11 -0
  199. package/examples/quickstart/src/server.ts +16 -0
  200. package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
  201. package/package.json +32 -14
  202. package/dist/internal/actions.d.mts +0 -9
  203. package/dist/internal/authentication.mjs +0 -11
  204. package/dist/internal/call.mjs +0 -12
  205. package/dist/internal/cli.mjs +0 -328
  206. package/dist/internal/client.mjs +0 -25
  207. package/dist/internal/console.mjs +0 -77
  208. package/dist/internal/declared.d.mts +0 -13
  209. package/dist/internal/declared.mjs +0 -34
  210. package/dist/internal/mcp.d.mts +0 -11
  211. package/dist/internal/mcp.mjs +0 -52
  212. package/dist/internal/refusal.mjs +0 -67
  213. package/dist/internal/schema-errors.mjs +0 -20
  214. package/dist/internal/tools.mjs +0 -44
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from "@effect/vitest";
2
+ import { Effect, Ref } from "effect";
3
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
4
+ import { identity } from "../../src/identity/handlers.js";
5
+ import { CurrentCaller, type Member, type Operator } from "../../src/identity/Identity.js";
6
+ import { members } from "../../src/identity/members.js";
7
+ import { type Permission, Permissions } from "../../src/identity/permissions.js";
8
+
9
+ const as = (caller: Member | Operator) => Effect.provideService(CurrentCaller, caller);
10
+
11
+ const operator: Operator = { kind: "operator", id: "root" };
12
+
13
+ describe("identity, in process", () => {
14
+ it.effect("answers each call with its own caller, from one client", () =>
15
+ Effect.gen(function* () {
16
+ const client = yield* Action.client(identity);
17
+
18
+ expect(yield* client.whoAmI().pipe(as(members.ada))).toEqual({ id: "ada", kind: "member" });
19
+ expect(yield* client.whoAmI().pipe(as(operator))).toEqual({ id: "root", kind: "operator" });
20
+ }).pipe(Effect.provide(Permissions.layerMemory)),
21
+ );
22
+
23
+ it.effect("gets the permission store once and asks it on every call", () =>
24
+ Effect.gen(function* () {
25
+ const granted = yield* Ref.make<ReadonlyArray<Permission>>([]);
26
+
27
+ // The store is a startup service, so it is provided where the client is acquired: the
28
+ // authorizer is built there. No call is given it again.
29
+ const client = yield* Action.client(identity).pipe(
30
+ Effect.provideService(Permissions, Permissions.of({ granted: () => Ref.get(granted) })),
31
+ );
32
+
33
+ const refusal = yield* Effect.flip(client.whoAmI().pipe(as(members.ada)));
34
+
35
+ expect(refusal).toBeInstanceOf(Action.Forbidden);
36
+ expect(refusal.message).toBe("Requires tasks:read.");
37
+
38
+ yield* Ref.set(granted, ["tasks:read"]);
39
+
40
+ expect(yield* client.whoAmI().pipe(as(members.ada))).toEqual({ id: "ada", kind: "member" });
41
+ }),
42
+ );
43
+ });
@@ -0,0 +1,48 @@
1
+ import { NodeServices } from "@effect/platform-node";
2
+ import { describe, expect, it } from "@effect/vitest";
3
+ import { ConfigProvider, Effect, FileSystem, Layer, Path } from "effect";
4
+ import { TaskStore } from "../../src/tasks/store.js";
5
+
6
+ // The file store, on a file of the test's own: `ASSISTANT_TASKS` is set through a
7
+ // `ConfigProvider`, as the environment sets it for an entrypoint. The directory is removed
8
+ // with the test's scope.
9
+ const fileStore = Effect.gen(function* () {
10
+ const files = yield* FileSystem.FileSystem;
11
+ const path = yield* Path.Path;
12
+ const directory = yield* files.makeTempDirectoryScoped();
13
+ const tasks = path.join(directory, "tasks.json");
14
+ const config = ConfigProvider.fromUnknown({ ASSISTANT_TASKS: tasks });
15
+
16
+ return TaskStore.layerFile.pipe(Layer.provide(ConfigProvider.layer(config)));
17
+ });
18
+
19
+ describe("the file store", () => {
20
+ it.effect("gives each of several concurrent adds, as one model turn makes them, its own id", () =>
21
+ Effect.gen(function* () {
22
+ const store = yield* TaskStore;
23
+
24
+ const added = yield* Effect.all(
25
+ ["Book the venue", "Send invitations", "Order lunch"].map((title) =>
26
+ store.add("ada", title, "ada"),
27
+ ),
28
+ { concurrency: "unbounded" },
29
+ );
30
+
31
+ expect(added.map(({ id }) => id)).toEqual(["1", "2", "3"]);
32
+ expect(yield* store.list("ada")).toEqual(added);
33
+ }).pipe(Effect.provide(Layer.unwrap(fileStore)), Effect.provide(NodeServices.layer)),
34
+ );
35
+
36
+ it.effect("keeps tasks for the next process: a layer built again reads them", () =>
37
+ Effect.gen(function* () {
38
+ const store = yield* fileStore;
39
+ const add = Effect.flatMap(TaskStore, (tasks) => tasks.add("root", "Rotate keys", "ada"));
40
+ const list = Effect.flatMap(TaskStore, (tasks) => tasks.list(undefined));
41
+
42
+ // `local` builds the layer anew for each, as each run of the agent or of a command does.
43
+ const task = yield* add.pipe(Effect.provide(store, { local: true }));
44
+
45
+ expect(yield* list.pipe(Effect.provide(store, { local: true }))).toEqual([task]);
46
+ }).pipe(Effect.provide(NodeServices.layer)),
47
+ );
48
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "@effect/vitest";
2
+ import { Effect, Layer } from "effect";
3
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
4
+ import { CurrentCaller, type Member, type Operator } from "../../src/identity/Identity.js";
5
+ import { members } from "../../src/identity/members.js";
6
+ import { Permissions } from "../../src/identity/permissions.js";
7
+ import { tasks } from "../../src/tasks/handlers.js";
8
+ import { TaskNotFound } from "../../src/tasks/Tasks.js";
9
+ import { TaskStore } from "../../src/tasks/store.js";
10
+
11
+ // What the implementation does, with no model and no command line: `Action.client` runs the
12
+ // same decoding, `authorize` and handler every surface runs. Each call names its caller.
13
+ const as = (caller: Member | Operator) => Effect.provideService(CurrentCaller, caller);
14
+
15
+ const operator: Operator = { kind: "operator", id: "root" };
16
+
17
+ // A test supplies its services as an entrypoint does: here the store is memory.
18
+ const services = Layer.mergeAll(TaskStore.layerMemory, Permissions.layerMemory);
19
+
20
+ describe("tasks, in process", () => {
21
+ it.effect("records which caller added a task, a member or the operator", () =>
22
+ Effect.gen(function* () {
23
+ const client = yield* Action.client(tasks);
24
+
25
+ yield* client.addTask({ title: "Book the venue", assignee: "ada" }).pipe(as(members.ada));
26
+ yield* client.addTask({ title: "Send invitations", assignee: "grace" }).pipe(as(operator));
27
+
28
+ const { tasks: listed } = yield* client.listTasks().pipe(as(members.grace));
29
+
30
+ expect(listed.map(({ id, addedBy }) => [id, addedBy])).toEqual([
31
+ ["1", "ada"],
32
+ ["2", "root"],
33
+ ]);
34
+ expect(yield* client.listTasks({ assignee: "grace" }).pipe(as(members.grace))).toMatchObject({
35
+ tasks: [{ title: "Send invitations" }],
36
+ });
37
+ }).pipe(Effect.provide(services)),
38
+ );
39
+
40
+ it.effect("refuses a write from a member the store grants reading alone", () =>
41
+ Effect.gen(function* () {
42
+ const client = yield* Action.client(tasks);
43
+ const add = client.addTask({ title: "Order lunch", assignee: "guest" });
44
+
45
+ const refusal = yield* Effect.flip(add.pipe(as(members.guest)));
46
+
47
+ // `toEqual` on an error compares its fields and never its message, so the message is
48
+ // asserted on its own.
49
+ expect(refusal).toBeInstanceOf(Action.Forbidden);
50
+ expect(refusal.message).toBe("Requires tasks:write.");
51
+ expect(yield* client.listTasks().pipe(as(members.guest))).toEqual({ tasks: [] });
52
+ }).pipe(Effect.provide(services)),
53
+ );
54
+
55
+ it.effect("admits the operator, whom the permission store does not know", () =>
56
+ Effect.gen(function* () {
57
+ const client = yield* Action.client(tasks);
58
+ const add = client.addTask({ title: "Rotate the keys", assignee: "ada" });
59
+ const { id } = yield* add.pipe(as(operator));
60
+
61
+ expect(yield* client.completeTask({ id }).pipe(as(operator))).toMatchObject({ done: true });
62
+ }).pipe(Effect.provide(services)),
63
+ );
64
+
65
+ it.effect("fails with the declared error for a task that does not exist", () =>
66
+ Effect.gen(function* () {
67
+ const client = yield* Action.client(tasks);
68
+ const annotate = client.annotateTask({ id: "9", notes: "Urgent." });
69
+
70
+ expect(yield* Effect.flip(annotate.pipe(as(members.ada)))).toEqual(
71
+ new TaskNotFound({ id: "9" }),
72
+ );
73
+ }).pipe(Effect.provide(services)),
74
+ );
75
+ });
@@ -0,0 +1,121 @@
1
+ import { describe, expect, it } from "@effect/vitest";
2
+ import { Effect, Layer, Stream } from "effect";
3
+ import { Chat, LanguageModel } from "effect/ai";
4
+ import type { Member } from "../src/identity/Identity.js";
5
+ import { members } from "../src/identity/members.js";
6
+ import { Permissions } from "../src/identity/permissions.js";
7
+ import { scripted } from "../src/scripted.js";
8
+ import { TaskStore } from "../src/tasks/store.js";
9
+ import { actingFor, layer, toolkit } from "../src/toolkit.js";
10
+
11
+ // What the toolkit adds to the implementations: a caller per call, and a model's approvals.
12
+ // The handlers run in this process, on a store in memory the tests can read too.
13
+ const tools = layer.pipe(
14
+ Layer.provideMerge(Layer.mergeAll(TaskStore.layerMemory, Permissions.layerMemory)),
15
+ );
16
+
17
+ // A model's call of `addTask`, as a provider sends it.
18
+ const adding = (id: string, assignee: string) =>
19
+ ({ type: "tool-call", id, name: "addTask", params: { title: "Order lunch", assignee } }) as const;
20
+
21
+ // Which calls of a turn ran, and which wait for a person.
22
+ const outcome = (response: LanguageModel.GenerateTextResponse<typeof toolkit.tools, "opaque">) => ({
23
+ ran: response.toolResults.map(({ id }) => id),
24
+ waiting: response.content.flatMap((part) =>
25
+ part.type === "tool-approval-request" ? [part.toolCallId] : [],
26
+ ),
27
+ });
28
+
29
+ describe("the toolkit", () => {
30
+ it.effect("runs each call as the member provided around it", () =>
31
+ Effect.gen(function* () {
32
+ const { handle } = yield* toolkit;
33
+
34
+ // A call by hand, with the JSON arguments a model sends. The member goes around the whole
35
+ // call, the collecting of its results included.
36
+ const add = handle("addTask", { title: "Order lunch", assignee: "guest" }).pipe(
37
+ Effect.flatMap(Stream.runCollect),
38
+ );
39
+
40
+ // A refusal is not raised: it is a result the model reads, as a declared error is.
41
+ expect(yield* add.pipe(actingFor(members.guest))).toMatchObject([
42
+ { isFailure: true, result: { message: "Requires tasks:write." } },
43
+ ]);
44
+ expect(yield* add.pipe(actingFor(members.ada))).toMatchObject([
45
+ { isFailure: false, result: { id: "1", addedBy: "ada" } },
46
+ ]);
47
+ }).pipe(Effect.provide(tools)),
48
+ );
49
+
50
+ it.effect("asks for approval before a model adds to a list that is not its caller's", () =>
51
+ Effect.gen(function* () {
52
+ // `LanguageModel` is what enforces approval: a call by hand, as above, is never asked.
53
+ // The model answers each member with the same two calls.
54
+ const plan = (member: Member) =>
55
+ LanguageModel.generateText({ prompt: "Plan the launch.", toolkit }).pipe(
56
+ actingFor(member),
57
+ Effect.map(outcome),
58
+ Effect.provide(scripted([adding("for ada", "ada"), adding("for grace", "grace")])),
59
+ );
60
+
61
+ expect(yield* plan(members.ada)).toEqual({ ran: ["for ada"], waiting: ["for grace"] });
62
+ expect(yield* plan(members.grace)).toEqual({ ran: ["for grace"], waiting: ["for ada"] });
63
+
64
+ // A call that waits has not run.
65
+ const store = yield* TaskStore;
66
+
67
+ expect((yield* store.list(undefined)).map(({ addedBy }) => addedBy)).toEqual([
68
+ "ada",
69
+ "grace",
70
+ ]);
71
+ }).pipe(Effect.provide(tools)),
72
+ );
73
+
74
+ it.effect("authorizes an approved call too: approval lets no one do what they may not", () =>
75
+ Effect.gen(function* () {
76
+ // The `Chat` keeps the request in its history, which the answer is matched against.
77
+ const chat = yield* Chat.empty;
78
+
79
+ const asked = yield* chat
80
+ .generateText({ prompt: "Add lunch to Ada's list.", toolkit })
81
+ .pipe(actingFor(members.guest));
82
+
83
+ expect(outcome(asked)).toEqual({ ran: [], waiting: ["for ada"] });
84
+
85
+ // The approved call runs in the turn that carries the answer, as that turn's caller.
86
+ const answered = yield* chat
87
+ .generateText({
88
+ prompt: [
89
+ {
90
+ role: "tool",
91
+ content: asked.content.flatMap((part) =>
92
+ part.type === "tool-approval-request"
93
+ ? [
94
+ {
95
+ type: "tool-approval-response",
96
+ approvalId: part.approvalId,
97
+ approved: true,
98
+ },
99
+ ]
100
+ : [],
101
+ ),
102
+ },
103
+ ],
104
+ toolkit,
105
+ })
106
+ .pipe(actingFor(members.guest));
107
+
108
+ expect(answered.toolResults).toMatchObject([
109
+ {
110
+ id: "for ada",
111
+ isFailure: true,
112
+ result: { message: "Requires tasks:write." },
113
+ },
114
+ ]);
115
+
116
+ const store = yield* TaskStore;
117
+
118
+ expect(yield* store.list(undefined)).toEqual([]);
119
+ }).pipe(Effect.provide(scripted([adding("for ada", "ada")])), Effect.provide(tools)),
120
+ );
121
+ });
@@ -0,0 +1,317 @@
1
+ # kitchen-sink
2
+
3
+ The reference application: one multi-tenant issue tracker that uses every module of the library
4
+ and nearly every option. Members of a workspace file, discuss and close issues, and attach
5
+ screenshots, over HTTP, MCP, Effect RPC, a CLI and an assistant, in process and behind a route.
6
+ Operators suspend workspaces on a second port, or from a console on the host.
7
+
8
+ Open it when you need a working use of one feature: the [index](#index) maps each export and
9
+ option to the file that shows it. To start an application, copy
10
+ [notes-service](../notes-service/) instead; it is this layout at a size you can read in one
11
+ sitting.
12
+
13
+ ## Run it
14
+
15
+ From the repository root, after `vp install` (from an installed package, see
16
+ [Run one](../README.md#run-one)):
17
+
18
+ ```sh
19
+ node --import tsx examples/kitchen-sink/src/server.ts # members on 127.0.0.1:3000, operators on :3001
20
+ node --import tsx examples/kitchen-sink/src/seed.ts # files ACME-1 with a screenshot and a comment
21
+
22
+ export TRACKER_TOKEN=ada TRACKER_WORKSPACE=acme # a demo member, see below
23
+ node --import tsx examples/kitchen-sink/src/cli.ts --help
24
+ node --import tsx examples/kitchen-sink/src/cli.ts list-issues -n 5
25
+ node --import tsx examples/kitchen-sink/src/cli.ts get-issue ACME-1
26
+ echo "It crashes on an empty draft" | node --import tsx examples/kitchen-sink/src/cli.ts create-issue --title Crash
27
+ node --import tsx examples/kitchen-sink/src/cli.ts ask-assistant --prompt "Close what is done." # answers with a call to approve
28
+
29
+ # The admin port takes an operator key, not a member's token.
30
+ curl -s 127.0.0.1:3001/admin/suspendWorkspace -H 'Content-Type: application/json' \
31
+ -H 'X-Operator-Key: ops-key' -d '{"workspace":"acme"}'
32
+
33
+ node --import tsx examples/kitchen-sink/src/ops.ts list-workspaces # the operators' console, no server
34
+ node --import tsx examples/kitchen-sink/src/agent.ts # the assistant, with a scripted model
35
+
36
+ vp test examples/kitchen-sink
37
+ ```
38
+
39
+ `PORT`, `ADMIN_PORT`, `PUBLIC_URL` and `MCP_ORIGIN` configure the server; `TRACKER_URL` points the
40
+ CLI and the seed at it. `stdio.ts` is an MCP server for a local agent to start as a subprocess.
41
+ `sdk.ts` and `browser.ts` are imported by client code, not run. The OpenAPI document is at
42
+ `/api/v1/openapi.json`, Swagger UI at `/docs`, the MCP endpoint at `/mcp`, the socket at `/rpc`.
43
+
44
+ Everything is a demo: stores are memory, and the credentials are fixed strings. Replace both
45
+ before deploying anything.
46
+
47
+ | Credential | Who | May |
48
+ | ------------------------------------- | ---------------------- | -------------------------------------- |
49
+ | `Authorization: Bearer ada` | ada, in acme or globex | read and write in acme, read in globex |
50
+ | `Authorization: Bearer linus` | linus, in acme | read |
51
+ | `Authorization: Bearer grace` | grace, in globex | read and write |
52
+ | `Cookie: tracker_session=ada-at-acme` | ada in acme, on `/rpc` | read and write |
53
+ | `X-Operator-Key: ops-key` | operator `ops` | list, suspend and resume workspaces |
54
+ | `X-Operator-Key: support-key` | operator `support` | list workspaces |
55
+
56
+ A bearer request names its workspace in `X-Workspace: acme`. `kay` (token `kay`, session
57
+ `kay-at-acme`) is a member of acme granted no scope yet. The session cookie also opens
58
+ `GET /attachments/<id>`, the image for a page of the web app; a token holder calls `getAttachment`.
59
+
60
+ ## Layout
61
+
62
+ ```text
63
+ src/
64
+ ├── identity/ who is calling
65
+ │ ├── Identity.ts contract: CurrentMember, scopes, the Login and Session descriptors, whoAmI
66
+ │ ├── members.ts DEMO ONLY: the people of the demo
67
+ │ ├── directory.ts Directory: who a token or a session names
68
+ │ ├── authenticate.ts Authentication.layer for both descriptors, OAuth discovery
69
+ │ ├── authorize.ts the members' rule: readOnly to a scope
70
+ │ ├── quota.ts the request quota, as HTTP and as RPC middleware
71
+ │ └── handlers.ts
72
+ ├── workspaces/ the tenants
73
+ │ ├── Workspaces.ts contract: Workspace, the public getStatus
74
+ │ ├── registry.ts WorkspaceRegistry: the workspaces and their suspension
75
+ │ ├── tenant.ts Tenant, and the router middleware that resolves it per request
76
+ │ └── handlers.ts
77
+ ├── issues/
78
+ │ ├── Issues.ts contract: schemas, errors, five actions
79
+ │ ├── store.ts IssueStore, scoped by workspace
80
+ │ ├── write-limit.ts the write limit, wrapped around handlers
81
+ │ ├── handlers.ts Action.implement over the store
82
+ │ ├── guide.ts a native MCP resource and prompt
83
+ │ └── assistant.ts the actions as a model's Toolkit, with approval
84
+ ├── assistant/ the model loop, served: a turn per request
85
+ │ ├── Assistant.ts contract: a turn, askAssistant and answerAssistant
86
+ │ ├── conversations.ts Conversations: each member's Chat, one turn at a time, ended where a turn fails
87
+ │ ├── demo-model.ts DEMO ONLY: a scripted model
88
+ │ └── handlers.ts the loop, approvals across requests, what a turn reports
89
+ ├── attachments/
90
+ │ ├── Attachments.ts contract: an image in an input and in a success
91
+ │ ├── store.ts
92
+ │ ├── handlers.ts
93
+ │ └── download.ts GET /attachments/:id, the host's own route, for the web app
94
+ ├── admin/ the operators, a second audience
95
+ │ ├── Admin.ts contract: Operator, the OperatorKey descriptor, three actions
96
+ │ ├── grants.ts Grants: what each operator may do
97
+ │ ├── authorize.ts the operators' rule, built from Grants
98
+ │ ├── keys.ts DEMO ONLY: the operator keys
99
+ │ ├── authenticate.ts Authentication.layer for OperatorKey
100
+ │ └── handlers.ts
101
+ ├── Actions.ts which actions each surface gets
102
+ ├── Api.ts the two HTTP bindings
103
+ ├── Rpc.ts the RPC binding
104
+ ├── config.ts what a deployment sets
105
+ ├── request-policy.ts the Host and Origin policy, and CORS
106
+ ├── routes.ts every surface of both ports, and the builders they share
107
+ ├── memory.ts DEMO ONLY: the services in memory
108
+ ├── server.ts entrypoint: both ports, the rpcs over a WebSocket
109
+ ├── stdio.ts entrypoint: MCP for a local agent
110
+ ├── cli.ts entrypoint: the members' commands, calling the server
111
+ ├── ops.ts entrypoint: the operators' console, in process
112
+ ├── agent.ts entrypoint: the assistant
113
+ ├── seed.ts entrypoint: an Effect program calling the API
114
+ ├── sdk.ts entrypoint: a promise client
115
+ └── browser.ts entrypoint: the web app's RPC client
116
+ test/
117
+ ├── routes.test.ts the host's surfaces: one port over HTTP and MCP, then both ports
118
+ ├── request-policy.test.ts raw requests to each port as a web handler
119
+ ├── issues/ in process, with Action.client; the assistant's tools
120
+ ├── assistant/ the assistant over HTTP, with a scripted model
121
+ ├── support/ the scripted model the assistant's tests give the routes
122
+ ├── identity/ what authentication adds: tokens over HTTP, sessions over RPC
123
+ ├── attachments/ images over HTTP and MCP; the download route
124
+ └── admin/ the operators' grants; the trusted operator
125
+ ```
126
+
127
+ - **A folder per domain.** `identity/` and `admin/` are the two audiences; `workspaces/`,
128
+ `issues/` and `attachments/` are what they work on. In each, the PascalCase file is the
129
+ contract and imports no server code; the lowercase files run on the server.
130
+ - **Top-level files are the host's.** `Actions.ts`, `Api.ts` and `Rpc.ts` say what is served
131
+ where, and stay browser-safe. `routes.ts` builds the surfaces. Each entrypoint chooses the
132
+ services, here from `memory.ts`. `server.ts` also chooses the rpcs' protocol, and a local
133
+ one lists the implementations it serves itself.
134
+ - **Three callers' credentials, three descriptors.** API clients, the CLI and MCP clients send
135
+ a bearer token (`Login`). The web app sends its session cookie (`Session`) for the same
136
+ identity, on its own binding. Operators send a key (`OperatorKey`) for another identity, on
137
+ another port.
138
+ - **Two limits, in two places.** The request quota is HTTP and RPC middleware
139
+ (`identity/quota.ts`): it counts before the input is decoded. The write limit is the
140
+ handlers' (`issues/write-limit.ts`): it holds on every surface, MCP and the assistant
141
+ included.
142
+
143
+ ## Where to look
144
+
145
+ | To see | Read |
146
+ | ----------------------------------------------------------------------- | ------------------------------------------------------- |
147
+ | How one request is authenticated, placed in a tenant, authorized | `src/identity/authenticate.ts`, `src/routes.ts` |
148
+ | Two audiences on two ports over one set of services | `src/server.ts`, `src/routes.ts`, `test/routes.test.ts` |
149
+ | Which actions each surface serves, and why one is no tool | `src/Actions.ts` |
150
+ | A limit before decoding and a limit in the handlers | `src/identity/quota.ts`, `src/issues/write-limit.ts` |
151
+ | An image for a model and a client (an action), and for a page (a route) | `src/attachments/` |
152
+ | A caller the host names itself | `src/stdio.ts`, `src/ops.ts`, `src/agent.ts` |
153
+ | A model loop behind a route, approvals answered in a later request | `src/assistant/handlers.ts`, `src/routes.ts` |
154
+ | What to test in process and what over the wire | `test/issues/issues.test.ts`, `test/routes.test.ts` |
155
+
156
+ ## Index
157
+
158
+ Each export and option the example uses, and where. A symbol in parentheses is the name to
159
+ search for in the file.
160
+
161
+ ### Action
162
+
163
+ | Export or option | File |
164
+ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
165
+ | `make`: `input`, `success`, `error`, `readOnly` | `src/issues/Issues.ts` |
166
+ | `make`: `caller: Action.Anyone`, a public action | `src/workspaces/Workspaces.ts` (`GetStatus`) |
167
+ | `make`: `caller`, an identity service | `src/identity/Identity.ts` (`CurrentMember`, `WhoAmI`) |
168
+ | `make`: `mcp` (`title`, hints) | `src/issues/Issues.ts` (`CreateIssue`, `CloseIssue`) |
169
+ | `make`: no `input` | `src/identity/Identity.ts` (`WhoAmI`) |
170
+ | `make`: an optional and a defaulted input field | `src/issues/Issues.ts` (`ListIssues`) |
171
+ | `make`: errors with `httpApiStatus` | `src/issues/Issues.ts` (`IssueNotFound`, `RateLimited`) |
172
+ | `make`: a shared error list | `src/issues/Issues.ts` (`IssueWriteErrors`), `src/attachments/Attachments.ts` |
173
+ | `make`: an option bundle, spread | `src/issues/Issues.ts` (`change`), `src/admin/Admin.ts` (`onWorkspace`) |
174
+ | `Image`, in a success and in an input | `src/attachments/Attachments.ts` |
175
+ | `byName` | `src/issues/guide.ts` (`tools`) |
176
+ | `implement`: one action, a plain handler | `src/identity/handlers.ts` |
177
+ | `implement`: one action, a builder | `src/workspaces/handlers.ts` |
178
+ | `implement`: several actions sharing a builder | `src/issues/handlers.ts` |
179
+ | Startup services and per-request services | `src/issues/handlers.ts` |
180
+ | A rate limit in handlers, shared by several | `src/issues/write-limit.ts`, `src/issues/handlers.ts`, `src/attachments/handlers.ts` |
181
+ | `implement`: `authorize`, a rule from `readOnly` | `src/identity/authorize.ts` |
182
+ | `implement`: `authorize`, an Effect building it | `src/admin/authorize.ts` |
183
+ | `allowAll` | `src/identity/handlers.ts` |
184
+ | `Forbidden` with `scopes`, and without | `src/identity/authorize.ts`, `src/admin/authorize.ts` |
185
+ | `Unauthenticated` | `src/identity/directory.ts`, `src/admin/authenticate.ts` |
186
+ | `InvalidInput` with `issues`, from a handler | `src/attachments/handlers.ts` |
187
+ | `layer`, one builder run for an implementation two ports serve | `src/routes.ts` (`builders`), `src/server.ts`, `test/routes.test.ts` |
188
+ | `client` | `src/agent.ts`, `test/issues/issues.test.ts` |
189
+ | `client`: `actions` | `test/issues/issues.test.ts` |
190
+ | `Any`, an authorizer's parameter | `src/identity/authorize.ts` |
191
+
192
+ ### Authentication
193
+
194
+ | Export or option | File |
195
+ | -------------------------------------------------------------- | --------------------------------------------------------------- |
196
+ | `make`, Bearer, with a declared `error` (503) | `src/identity/Identity.ts` (`Login`, `IssuerUnavailable`) |
197
+ | `make`: `security`, a cookie, for the same identity | `src/identity/Identity.ts` (`Session`) |
198
+ | `make`: `security`, an API key in a header | `src/admin/Admin.ts` (`OperatorKey`) |
199
+ | `layer`, a verifier built from startup services | `src/identity/authenticate.ts` |
200
+ | `layer`, a plain verifier | `src/admin/authenticate.ts` |
201
+ | `layer`: `protectedResource`, an Effect over `Config` | `src/identity/authenticate.ts` (`protectedResource`) |
202
+ | `ProtectedResource` | `src/identity/authenticate.ts` |
203
+ | A request service the verifier reads, and its order | `src/workspaces/tenant.ts`, `src/routes.ts` (`publicRoutes`) |
204
+ | `protect`, a route of the host's own, by the cookie descriptor | `src/attachments/download.ts` |
205
+ | A trusted identity no verifier returns | `src/admin/Admin.ts`, `src/admin/authenticate.ts`, `src/ops.ts` |
206
+ | A verifier's declared error over MCP | `test/identity/identity.test.ts` |
207
+ | A test replacing a verifier's service | `test/identity/identity.test.ts` (`issuerDown`) |
208
+
209
+ ### ActionHttp
210
+
211
+ | Export or option | File |
212
+ | -------------------------------------------------------------- | --------------------------------------------------------------------------- |
213
+ | `make`: `prefix`, `authentication`, `error` | `src/Api.ts` (`Http`) |
214
+ | A second binding for a second identity | `src/Api.ts` (`AdminHttp`) |
215
+ | One action in two bindings | `src/Api.ts` (`GetStatus`), `src/routes.ts` (`adminRoutes`) |
216
+ | `layer`: `actions` | `src/routes.ts` (`api`) |
217
+ | `layer`: `middleware` | `src/routes.ts` (`api`), `src/identity/quota.ts` (`HttpQuota`) |
218
+ | Global router middleware, before authentication and discovery | `src/request-policy.ts` (`requestPolicy`), `src/routes.ts` (`publicRoutes`) |
219
+ | `Http.api`, `Http.prefix`: the OpenAPI document and Swagger UI | `src/routes.ts` (`documentation`) |
220
+ | A request body limit | `src/server.ts` |
221
+ | Two `HttpRouter.serve`, what they share provided outside both | `src/server.ts` |
222
+ | `client`: `baseUrl`, `transformClient` | `src/seed.ts` |
223
+ | `fetchClient` | `src/sdk.ts` |
224
+ | Declared errors handled by tag | `src/seed.ts`, `src/sdk.ts` |
225
+
226
+ ### ActionMcp
227
+
228
+ | Export or option | File |
229
+ | ------------------------------------------------ | --------------------------------------------------- |
230
+ | `layerHttp`, public and protected tools together | `src/routes.ts` (`mcp`) |
231
+ | `layerHttp`: `authentication`, `instructions` | `src/routes.ts` (`mcp`) |
232
+ | `layerHttp`: `actions`, one action left off | `src/routes.ts` (`mcp`), `src/Actions.ts` (`Tools`) |
233
+ | `layerHttp`: `allowedOrigins` | `src/routes.ts` (`mcp`), `test/routes.test.ts` |
234
+ | `layerHttp`: `features`, a resource and a prompt | `src/issues/guide.ts` |
235
+ | CORS for a browser MCP client | `src/request-policy.ts` (`cors`) |
236
+ | `runStdio`, the host naming the caller | `src/stdio.ts` |
237
+ | An image block from a media field | `src/attachments/Attachments.ts` (`GetAttachment`) |
238
+
239
+ ### ActionRpc
240
+
241
+ | Export or option | File |
242
+ | --------------------------------- | ------------------------------------------------------------- |
243
+ | `make`: `authentication`, `error` | `src/Rpc.ts` |
244
+ | `layer`: `middleware` | `src/routes.ts` (`rpc`), `src/identity/quota.ts` (`RpcQuota`) |
245
+ | A WebSocket protocol | `src/server.ts` (`socket`) |
246
+ | `client`, over a WebSocket | `src/browser.ts` |
247
+ | A credential per message | `test/identity/session.test.ts` (`as`) |
248
+
249
+ ### ActionToolkit
250
+
251
+ | Export or option | File |
252
+ | --------------------------------------------------- | ------------------------------------ |
253
+ | `make`: `actions`, `needsApproval` (a boolean rule) | `src/issues/assistant.ts` |
254
+ | `toolkit` given to `LanguageModel` | `src/issues/assistant.ts` (`triage`) |
255
+ | `layer`, and the caller per turn | `src/agent.ts` |
256
+ | `toolkit` and the model yielded in a builder | `src/assistant/handlers.ts` |
257
+ | `layer` provided to the routes | `src/routes.ts` (`assistantTools`) |
258
+ | An approval answered in a later request | `src/assistant/handlers.ts` |
259
+ | `tools.handle`, a call made by hand | `test/issues/assistant.test.ts` |
260
+
261
+ ### ActionCli
262
+
263
+ | Export or option | File |
264
+ | -------------------------------------------------------------- | ------------------------------------------ |
265
+ | `remote`: `name`, `commands` | `src/cli.ts` |
266
+ | `commands`: `stdin`, `positional`, `aliases`, `render`, `name` | `src/cli.ts` |
267
+ | A client from `Config`, its credentials optional | `src/cli.ts` |
268
+ | `make`, with a trusted identity the host supplies | `src/ops.ts` |
269
+ | `logToStderr` | `src/cli.ts`, `src/ops.ts`, `src/stdio.ts` |
270
+
271
+ ### Testing
272
+
273
+ | Export or option | File |
274
+ | ------------------------------------------------ | ------------------------------------------------------------- |
275
+ | `layer(routes)` | `test/identity/identity.test.ts` |
276
+ | `layer(routes)`, twice, over shared builders | `test/routes.test.ts` (`shared`) |
277
+ | `layer(handler)`, over `HttpRouter.toWebHandler` | `test/request-policy.test.ts` |
278
+ | `mcpClient`: `transformClient` | `test/routes.test.ts` |
279
+ | `mcpClient`, decoding an image | `test/attachments/attachments.test.ts` |
280
+ | `McpCallError` | `test/identity/identity.test.ts` |
281
+ | `mcpRequest`: `headers`, `url` | `test/attachments/attachments.test.ts`, `test/routes.test.ts` |
282
+ | RPC in memory, over Effect's HTTP protocol | `test/identity/session.test.ts` |
283
+ | A service the test and the routes both read | `test/identity/session.test.ts` (`served`) |
284
+ | A test replacing one service | `test/identity/identity.test.ts` (`servedWith`) |
285
+ | `Action.client`, a caller per call | `test/issues/issues.test.ts` |
286
+ | A scripted `LanguageModel` | `test/issues/assistant.test.ts`, `test/support/scripted.ts` |
287
+ | A test replacing the model the routes are given | `test/assistant/assistant.test.ts` (`served`) |
288
+ | An error's message asserted beside its fields | `test/issues/issues.test.ts` |
289
+
290
+ ## Not shown
291
+
292
+ Left out because this application has no honest place for them:
293
+
294
+ - `Authentication.refusalResponse` and `bearerTokenOf`. They serve a caller the router never
295
+ routes, such as a Node `upgrade` handler. Every caller here is routed: the socket is Effect's
296
+ own WebSocket route, authenticated per message.
297
+ - A descriptor with `HttpApiSecurity.basic`, and an API key in the query.
298
+ - `Action.implement` with a plain record of handlers: every implementation of several actions
299
+ here has a builder. `implementation.actions`. `mcp._meta`.
300
+ - `ActionCli.command` and `remoteCommand`, one action as one command; `make`'s and `remote`'s
301
+ `actions`; `remote`'s static `client` option. Both CLIs here are aggregates of every action,
302
+ and the remote one reads its client from `Config`. `subcommands` and `toUserError`, a command
303
+ written by hand: [notes-service](../notes-service/) has one.
304
+ - `ActionRpc.layer`'s `actions`, `Rpc.group`, and `ActionRpc.client`'s options.
305
+ - A binding's other fields, `Http.actions`, `Http.error` and `Http.authentication`.
306
+ - `ActionMcp`'s `path`, `description`, `websiteUrl`, `icons` and `extensions`; `fetchClient`'s
307
+ `fetch`.
308
+ - `needsApproval` as an Effect reading the caller: the rule here is a boolean of the action.
309
+ - `Action.InvalidInput.fromSchemaError`, the `Action.Refusal` and `Action.BuiltIn` schemas, and the
310
+ helper types: `Action.Issue`, `Errors`, `Handlers`, `Client`, `AnyImplementation`,
311
+ `BuildServices`, `ActionHttp.Client`, `Authentication.Any`.
312
+ - A handler sending MCP notifications through `McpServer.McpServer`, and one reading
313
+ `McpSchema.McpRequestContext`.
314
+ - A tenant taken from a subdomain. The workspace is a header, so the OAuth resource the server
315
+ publishes is one fixed URL.
316
+ - A second way to the attachment's bytes for token holders: the download route takes the web
317
+ app's cookie alone, and an API client reads the image through `getAttachment`.
@@ -0,0 +1,19 @@
1
+ import { Assistant } from "./assistant/Assistant.js";
2
+ import { AttachImage, GetAttachment } from "./attachments/Attachments.js";
3
+ import { WhoAmI } from "./identity/Identity.js";
4
+ import { Issues } from "./issues/Issues.js";
5
+ import { GetStatus } from "./workspaces/Workspaces.js";
6
+
7
+ // Which actions each surface gets. A contract says nothing about where it is served: each
8
+ // surface takes a list, and these are the lists, built from the contracts themselves.
9
+
10
+ /** What a signed-in member may call: every protected action of the public port. */
11
+ export const MemberActions = [WhoAmI, ...Issues, AttachImage, GetAttachment, ...Assistant] as const;
12
+
13
+ /**
14
+ * The MCP tools, for the HTTP endpoint and the stdio server alike. `attachImage` is left out:
15
+ * a model should not carry an image's bytes as an argument. So is the assistant: it is a model
16
+ * itself, and its tools are already here. An action added to a domain later is no tool until
17
+ * it is listed here.
18
+ */
19
+ export const Tools = [GetStatus, WhoAmI, ...Issues, GetAttachment] as const;