@gjermundgaraba/effect-actions 0.19.0 → 0.21.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/README.md +19 -41
- package/dist/Action.d.mts +140 -48
- package/dist/Action.mjs +88 -44
- package/dist/ActionCli.d.mts +70 -35
- package/dist/ActionCli.mjs +53 -34
- package/dist/ActionHttp.d.mts +70 -38
- package/dist/ActionHttp.mjs +76 -25
- package/dist/ActionMcp.d.mts +56 -24
- package/dist/ActionMcp.mjs +104 -61
- package/dist/ActionRpc.d.mts +222 -57
- package/dist/ActionRpc.mjs +301 -49
- package/dist/ActionToolkit.d.mts +22 -7
- package/dist/ActionToolkit.mjs +37 -14
- package/dist/Authentication.d.mts +42 -12
- package/dist/Authentication.mjs +38 -32
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.d.mts +1 -3
- package/dist/Testing.mjs +2 -3
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -12
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -4
- package/dist/cli/commands.d.mts +9 -7
- package/dist/cli/commands.mjs +85 -35
- package/dist/contract/ast.mjs +5 -3
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -8
- package/dist/contract/call.mjs +49 -8
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +12 -3
- package/dist/contract/caller.mjs +23 -5
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +26 -11
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -5
- package/dist/contract/errors.mjs +2 -3
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hints.mjs +1 -2
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -51
- package/dist/contract/implementation.mjs +94 -60
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/media.d.mts +1 -3
- package/dist/contract/media.mjs +1 -2
- package/dist/contract/names.mjs +15 -6
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +15 -7
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -14
- package/dist/http/client.mjs +52 -14
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +2 -3
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -99
- package/dist/mcp/client.mjs +60 -134
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +40 -15
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +14 -7
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +4 -5
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +57 -61
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -53
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +25 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +19 -17
package/docs/ActionToolkit.md
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
# ActionToolkit
|
|
2
2
|
|
|
3
|
-
Implementations as Effect's native AI `Toolkit`: one tool per action, run in the process that
|
|
4
|
-
calls the model, with no server. Reach for it when a `LanguageModel` or a `Chat` in your program
|
|
5
|
-
calls your actions. Nothing authenticates: the host names the caller. Code of your own calls
|
|
6
|
-
the same implementations typed, with `Action.client`
|
|
7
|
-
([Action.md](Action.md#call-actions-in-process)).
|
|
3
|
+
Implementations as Effect's native AI `Toolkit`: one tool per action, run in the process that calls the model, with no server. Reach for it when a `LanguageModel` or a `Chat` in your program calls your actions. Nothing authenticates: the host names the caller. Code of your own calls the same implementations typed, with `Action.client` ([Action.md](Action.md#call-actions-in-process)).
|
|
8
4
|
|
|
9
5
|
A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [ActionToolkit-failures.md](ActionToolkit-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
|
|
10
6
|
|
|
11
7
|
## Give a model tools
|
|
12
8
|
|
|
13
|
-
`ActionToolkit.make` projects implementations into `{ toolkit, layer }`. Pass `toolkit` to
|
|
14
|
-
`LanguageModel.generateText` or a `Chat`, and provide `layer` once, around the program:
|
|
9
|
+
`ActionToolkit.make` projects implementations into `{ toolkit, layer }`. Pass `toolkit` to `LanguageModel.generateText` or a `Chat`, and provide `layer` once, around the program:
|
|
15
10
|
|
|
16
11
|
```ts example=assistant/src/toolkit.ts
|
|
12
|
+
import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
|
|
17
13
|
import { Effect, Option } from "effect";
|
|
18
14
|
import { Tool, Toolkit } from "effect/ai";
|
|
19
|
-
|
|
15
|
+
|
|
20
16
|
import { identity } from "./identity/handlers.js";
|
|
21
|
-
import { CurrentCaller
|
|
17
|
+
import { CurrentCaller } from "./identity/Identity.js";
|
|
18
|
+
import type { Member } from "./identity/Identity.js";
|
|
22
19
|
import { tasks } from "./tasks/handlers.js";
|
|
23
20
|
|
|
24
21
|
// `actions.layer` holds the handlers, and requires the startup services their builders read;
|
|
@@ -33,8 +30,9 @@ const actions = ActionToolkit.make([tasks, identity], {
|
|
|
33
30
|
Effect.serviceOption(CurrentCaller),
|
|
34
31
|
Option.match({
|
|
35
32
|
onNone: () => true,
|
|
36
|
-
onSome: (caller) =>
|
|
37
|
-
|
|
33
|
+
onSome: (caller) =>
|
|
34
|
+
call.name !== "addTask" || call.input.assignee !== caller.id,
|
|
35
|
+
})
|
|
38
36
|
),
|
|
39
37
|
});
|
|
40
38
|
|
|
@@ -42,104 +40,49 @@ const actions = ActionToolkit.make([tasks, identity], {
|
|
|
42
40
|
// The later definition wins; annotating the generated tool keeps its handler and approval rule.
|
|
43
41
|
export const toolkit = Toolkit.merge(
|
|
44
42
|
actions.toolkit,
|
|
45
|
-
Toolkit.make(actions.toolkit.tools.addTask.annotate(Tool.Title, "Add a task"))
|
|
43
|
+
Toolkit.make(actions.toolkit.tools.addTask.annotate(Tool.Title, "Add a task"))
|
|
46
44
|
);
|
|
47
45
|
|
|
48
|
-
export const layer = actions
|
|
46
|
+
export const { layer } = actions;
|
|
49
47
|
|
|
50
48
|
// One toolkit serves every member, so who a turn runs as is provided around that turn. This is
|
|
51
49
|
// how this example names a model's caller, and it takes a `Member`. The tools themselves accept
|
|
52
50
|
// any `CurrentCaller`, so keeping the operator away from a model is this function's doing, and
|
|
53
51
|
// that of every host using it.
|
|
54
|
-
export const actingFor = (member: Member) =>
|
|
52
|
+
export const actingFor = (member: Member) =>
|
|
53
|
+
Effect.provideService(CurrentCaller, member);
|
|
55
54
|
```
|
|
56
55
|
|
|
57
|
-
[assistant/src/agent.ts](../examples/assistant/src/agent.ts) runs the turns and provides `layer`
|
|
58
|
-
|
|
59
|
-
[
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- **
|
|
64
|
-
[kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts).
|
|
65
|
-
- Where a model must not get every action, list the tools
|
|
66
|
-
([Action.md](Action.md#define-a-contract)):
|
|
67
|
-
[kitchen-sink/src/issues/assistant.ts](../examples/kitchen-sink/src/issues/assistant.ts).
|
|
68
|
-
- A turn's tool calls, the approved calls of an answering turn included, run concurrently unless
|
|
69
|
-
`generateText` is given a `concurrency`:
|
|
70
|
-
[assistant/src/tasks/store.ts](../examples/assistant/src/tasks/store.ts) locks each change.
|
|
71
|
-
- **A model loop behind a route.** Yield `toolkit` and the model in the implementation's
|
|
72
|
-
builder, and provide `layer` to the route's layer,
|
|
73
|
-
`ActionHttp.layer(Http, ask).pipe(Layer.provide(layer))`, so the tools share the routes' build
|
|
74
|
-
([Action.md](Action.md#share-one-build-across-servers)):
|
|
75
|
-
[kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts),
|
|
76
|
-
keeping each caller's `Chat` in
|
|
77
|
-
[conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts), with a scripted
|
|
78
|
-
model in [demo-model.ts](../examples/kitchen-sink/src/assistant/demo-model.ts) that the
|
|
79
|
-
entrypoint provides through [kitchen-sink/src/memory.ts](../examples/kitchen-sink/src/memory.ts),
|
|
80
|
-
tested over HTTP in
|
|
81
|
-
[kitchen-sink/test/assistant/assistant.test.ts](../examples/kitchen-sink/test/assistant/assistant.test.ts).
|
|
82
|
-
Name the files as it does, the route's contract `Assistant.ts` and the loop `handlers.ts`,
|
|
83
|
-
from the first one written.
|
|
84
|
-
- **The real model comes later?** Provide a scripted one from the entrypoint until then, as the
|
|
85
|
-
kitchen-sink does. Reading the model with `Effect.serviceOption` hides the requirement: the
|
|
86
|
-
server builds without one and every request fails.
|
|
56
|
+
[assistant/src/agent.ts](../examples/assistant/src/agent.ts) runs the turns and provides `layer` with its startup services, with a scripted model in [assistant/src/scripted.ts](../examples/assistant/src/scripted.ts). What a call returns, a refusal and arguments that do not decode included, is on `ActionToolkit.make`; the options' rules are on `ActionToolkit.Options`.
|
|
57
|
+
|
|
58
|
+
- **Let the model read its tools' results** by asking again: [kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts).
|
|
59
|
+
- Where a model must not get every action, list the tools ([Action.md](Action.md#define-a-contract)): [kitchen-sink/src/issues/assistant.ts](../examples/kitchen-sink/src/issues/assistant.ts).
|
|
60
|
+
- A turn's tool calls, the approved calls of an answering turn included, run concurrently unless `generateText` is given a `concurrency`: [assistant/src/tasks/store.ts](../examples/assistant/src/tasks/store.ts) locks each change.
|
|
61
|
+
- **A model loop behind a route.** Yield `toolkit` and the model in the implementation's builder, and provide `layer` to the route's layer, `ActionHttp.layer(Http, ask).pipe(Layer.provide(layer))`, so the tools share the routes' build ([Action.md](Action.md#share-one-build-across-servers)): [kitchen-sink/src/assistant/handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts), keeping each caller's `Chat` in [conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts), with a scripted model in [demo-model.ts](../examples/kitchen-sink/src/assistant/demo-model.ts) that the entrypoint provides through [kitchen-sink/src/memory.ts](../examples/kitchen-sink/src/memory.ts), tested over HTTP in [kitchen-sink/test/assistant/assistant.test.ts](../examples/kitchen-sink/test/assistant/assistant.test.ts). Name the files as it does, the route's contract `Assistant.ts` and the loop `handlers.ts`, from the first one written.
|
|
62
|
+
- **The real model comes later?** Provide a scripted one from the entrypoint until then, as the kitchen-sink does. Reading the model with `Effect.serviceOption` hides the requirement: the server builds without one and every request fails.
|
|
87
63
|
|
|
88
64
|
### Customize the native tools
|
|
89
65
|
|
|
90
|
-
Give a generated tool a title, or other annotations, with native `Tool` and `Toolkit` from
|
|
91
|
-
`effect/ai`, as `toolkit` above does; [agent.ts](../examples/assistant/src/agent.ts) reads the
|
|
92
|
-
title when printing approval requests.
|
|
66
|
+
Give a generated tool a title, or other annotations, with native `Tool` and `Toolkit` from `effect/ai`, as `toolkit` above does; [agent.ts](../examples/assistant/src/agent.ts) reads the title when printing approval requests.
|
|
93
67
|
|
|
94
|
-
- To add other native tools, use `Toolkit.merge(toolkit, others)` and provide both handler layers
|
|
95
|
-
with `Layer.mergeAll(layer, othersHandlers)`.
|
|
68
|
+
- To add other native tools, use `Toolkit.merge(toolkit, others)` and provide both handler layers with `Layer.mergeAll(layer, othersHandlers)`.
|
|
96
69
|
|
|
97
70
|
### Call a tool by hand
|
|
98
71
|
|
|
99
|
-
`yield* toolkit` gives `tools.handle(name, arguments)`:
|
|
100
|
-
[assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts). Its rules, where
|
|
101
|
-
to provide the caller among them, are on `ActionToolkit.Tools`.
|
|
72
|
+
`yield* toolkit` gives `tools.handle(name, arguments)`: [assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts). Its rules, where to provide the caller among them, are on `ActionToolkit.Tools`.
|
|
102
73
|
|
|
103
74
|
## Name the caller
|
|
104
75
|
|
|
105
|
-
Provide the identity around each turn, or each call made by hand, as `actingFor` does in
|
|
106
|
-
[assistant/src/toolkit.ts](../examples/assistant/src/toolkit.ts). Why never to `layer`, and that
|
|
107
|
-
a protected tool owes it whatever its handler reads, are on `Action.Options.caller`
|
|
108
|
-
([Action.md](Action.md#say-who-may-call-an-action)).
|
|
76
|
+
Provide the identity around each turn, or each call made by hand, as `actingFor` does in [assistant/src/toolkit.ts](../examples/assistant/src/toolkit.ts). Why never to `layer`, and that a protected tool owes it whatever its handler reads, are on `Action.Options.caller` ([Action.md](Action.md#say-who-may-call-an-action)).
|
|
109
77
|
|
|
110
78
|
## Ask for approval
|
|
111
79
|
|
|
112
|
-
To ask before every write: `needsApproval: (call) => !call.action.readOnly`
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
A call
|
|
118
|
-
|
|
119
|
-
`
|
|
120
|
-
[
|
|
121
|
-
[assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts).
|
|
122
|
-
|
|
123
|
-
- **Each request is answered on its own**: an approved call runs, a denied one does not, and one
|
|
124
|
-
left unanswered stays open for a later turn; to require every answer at once, check it in the
|
|
125
|
-
host. List the open ones with `ActionToolkit.openApprovals(yield* Ref.get(chat.history))`.
|
|
126
|
-
The kitchen-sink refuses an `approvalId` answered twice
|
|
127
|
-
([Assistant.ts](../examples/kitchen-sink/src/assistant/Assistant.ts)), gives each call its own
|
|
128
|
-
id ([demo-model.ts](../examples/kitchen-sink/src/assistant/demo-model.ts)) and lets only its
|
|
129
|
-
owner reach a conversation
|
|
130
|
-
([conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts)).
|
|
131
|
-
- A denied call's result, `{ type: "execution-denied", reason }`, has `reason` `undefined` unless
|
|
132
|
-
the answer gives one, so it is not JSON: a route answering it as a `Schema.Unknown` success
|
|
133
|
-
answers an empty 500.
|
|
134
|
-
- **If a turn fails or is interrupted once an approved call has started**, the call may have
|
|
135
|
-
run: end the conversation and say so in the error, as
|
|
136
|
-
[kitchen-sink/src/assistant/conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts)
|
|
137
|
-
and [handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts) do. What a failed
|
|
138
|
-
`streamText` leaves is on `ActionToolkit.openApprovals`.
|
|
139
|
-
- **Show the person what is asked**: [assistant/src/agent.ts](../examples/assistant/src/agent.ts)
|
|
140
|
-
joins each request to its call by `toolCallId`; how to read the arguments is on
|
|
141
|
-
`ActionToolkit.Approval`.
|
|
142
|
-
- **If repeating a write would create an unwanted additional effect, retries need
|
|
143
|
-
application-owned deduplication.** Keep the same operation key across attempts, as in
|
|
144
|
-
[notes-service's creation example](../examples/notes-service/README.md#retry-a-creation).
|
|
145
|
-
Approval history alone does not make the mutation and its result record atomic.
|
|
80
|
+
To ask before every write: `needsApproval: (call) => !call.action.readOnly` ([kitchen-sink/src/issues/assistant.ts](../examples/kitchen-sink/src/issues/assistant.ts)). A rule reading the call's input and its caller is `toolkit` above; its rules are on `ActionToolkit.Options`.
|
|
81
|
+
|
|
82
|
+
A call that needs approval does not run: the turn ends with a `tool-approval-request` part in `response.content` in place of its result. Start the next turn with the person's answers, in a `Chat`, as the caller who was asked: [assistant/src/agent.ts](../examples/assistant/src/agent.ts) shows both and says why, tested in [assistant/test/toolkit.test.ts](../examples/assistant/test/toolkit.test.ts).
|
|
83
|
+
|
|
84
|
+
- **Each request is answered on its own**: an approved call runs, a denied one does not, and one left unanswered stays open for a later turn; to require every answer at once, check it in the host. List the open ones with `ActionToolkit.openApprovals(yield* Ref.get(chat.history))`. The kitchen-sink refuses an `approvalId` answered twice ([Assistant.ts](../examples/kitchen-sink/src/assistant/Assistant.ts)), gives each call its own id ([demo-model.ts](../examples/kitchen-sink/src/assistant/demo-model.ts)) and lets only its owner reach a conversation ([conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts)).
|
|
85
|
+
- A denied call's result, `{ type: "execution-denied", reason }`, has `reason` `undefined` unless the answer gives one, so it is not JSON: a route answering it as a `Schema.Unknown` success answers an empty 500.
|
|
86
|
+
- **If a turn fails or is interrupted once an approved call has started**, the call may have run: end the conversation and say so in the error, as [kitchen-sink/src/assistant/conversations.ts](../examples/kitchen-sink/src/assistant/conversations.ts) and [handlers.ts](../examples/kitchen-sink/src/assistant/handlers.ts) do. What a failed `streamText` leaves is on `ActionToolkit.openApprovals`.
|
|
87
|
+
- **Show the person what is asked**: [assistant/src/agent.ts](../examples/assistant/src/agent.ts) joins each request to its call by `toolCallId`; how to read the arguments is on `ActionToolkit.Approval`.
|
|
88
|
+
- **If repeating a write would create an unwanted additional effect, retries need application-owned deduplication.** Keep the same operation key across attempts, as in [notes-service's creation example](../examples/notes-service/README.md#retry-a-creation). Approval history alone does not make the mutation and its result record atomic.
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
# Authentication beyond a bearer token
|
|
2
2
|
|
|
3
|
-
A verifier reading the request, an API key or a cookie, a route of your own, a policy before
|
|
4
|
-
authentication, and a refusal outside the router.
|
|
3
|
+
A verifier reading the request, an API key or a cookie, a route of your own, a policy before authentication, and a refusal outside the router.
|
|
5
4
|
|
|
6
|
-
A message the library throws says its fix; for a compiler diagnostic or Effect's own error,
|
|
7
|
-
search [Authentication-failures.md](Authentication-failures.md). For one symbol,
|
|
8
|
-
`grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc
|
|
9
|
-
carries its rules and gotchas, and this page only what an example cannot show.
|
|
5
|
+
A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [Authentication-failures.md](Authentication-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
|
|
10
6
|
|
|
11
7
|
## Read request context in a verifier
|
|
12
8
|
|
|
13
|
-
A verifier may read per-request services: the request, or one of your own, such as the tenant a
|
|
14
|
-
request addresses, which native router middleware provides:
|
|
9
|
+
A verifier may read per-request services: the request, or one of your own, such as the tenant a request addresses, which native router middleware provides:
|
|
15
10
|
|
|
16
11
|
```ts example=kitchen-sink/src/workspaces/tenant.ts
|
|
17
12
|
import { Context, Effect, Option } from "effect";
|
|
@@ -22,51 +17,43 @@ import { HttpRouter, HttpServerRequest } from "effect/http";
|
|
|
22
17
|
* an identity: the verifier reads it to decide which of an account's memberships the caller
|
|
23
18
|
* acts in. Handlers never read it; they read the workspace of the authenticated member.
|
|
24
19
|
*/
|
|
25
|
-
export class Tenant extends Context.Service<Tenant, Option.Option<string>>()(
|
|
20
|
+
export class Tenant extends Context.Service<Tenant, Option.Option<string>>()(
|
|
21
|
+
"tracker/Tenant"
|
|
22
|
+
) {}
|
|
26
23
|
|
|
27
24
|
// Native router middleware. It provides `Tenant` to whatever it is provided to, per request:
|
|
28
25
|
// to the verifier too, when it is provided after the authentication, in a `Layer.provide` of
|
|
29
26
|
// its own: in one array with the authentication it would leave `Tenant` owed, since an array's
|
|
30
27
|
// layers provide to the routes, not to one another.
|
|
31
|
-
export const resolveTenant = HttpRouter.middleware<{ provides: Tenant }>()(
|
|
32
|
-
|
|
33
|
-
Effect.
|
|
34
|
-
|
|
28
|
+
export const resolveTenant = HttpRouter.middleware<{ provides: Tenant }>()(
|
|
29
|
+
(route) =>
|
|
30
|
+
Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) =>
|
|
31
|
+
Effect.provideService(
|
|
32
|
+
route,
|
|
33
|
+
Tenant,
|
|
34
|
+
Option.fromNullishOr(request.headers["x-workspace"])
|
|
35
|
+
)
|
|
36
|
+
)
|
|
35
37
|
);
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
The verifier reading it is `authenticateBearer` in
|
|
39
|
-
[kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts),
|
|
40
|
-
and `publicRoutes` in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts)
|
|
41
|
-
provides the middleware.
|
|
40
|
+
The verifier reading it is `authenticateBearer` in [kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts), and `publicRoutes` in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) provides the middleware.
|
|
42
41
|
|
|
43
42
|
## Use a scheme other than Bearer
|
|
44
43
|
|
|
45
|
-
Name the scheme in `Authentication.Options.security`, whose JSDoc says what each scheme gives
|
|
46
|
-
|
|
47
|
-
[
|
|
48
|
-
`authenticateSession` in
|
|
49
|
-
[kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts)
|
|
50
|
-
for a session cookie beside Bearer, and
|
|
51
|
-
[kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts) and
|
|
52
|
-
[kitchen-sink/src/admin/authenticate.ts](../examples/kitchen-sink/src/admin/authenticate.ts)
|
|
53
|
-
for an API key in a header.
|
|
54
|
-
|
|
55
|
-
For a cookie covering rpcs over a WebSocket, see
|
|
56
|
-
[ActionRpc.md](ActionRpc.md#send-request-context-with-a-call). A scheme the deployment decides,
|
|
57
|
-
such as a cookie name a framework prefixes over HTTPS, is still one declaration, as
|
|
58
|
-
`Authentication.layer` says.
|
|
44
|
+
Name the scheme in `Authentication.Options.security`, whose JSDoc says what each scheme gives up. See `Session` in [kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts) and `authenticateSession` in [kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts) for a session cookie beside Bearer, and [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts) and [kitchen-sink/src/admin/authenticate.ts](../examples/kitchen-sink/src/admin/authenticate.ts) for an API key in a header.
|
|
45
|
+
|
|
46
|
+
For a cookie covering rpcs over a WebSocket, see [ActionRpc.md](ActionRpc.md#send-request-context-with-a-call). A scheme the deployment decides, such as a cookie name a framework prefixes over HTTPS, is still one declaration, as `Authentication.layer` says.
|
|
59
47
|
|
|
60
48
|
## Protect a route of your own
|
|
61
49
|
|
|
62
|
-
A route of the host's own, such as a download or a WebSocket upgrade, takes
|
|
63
|
-
`Authentication.protect(descriptor)`, whose rules, authorization and caching included, are on
|
|
64
|
-
its JSDoc:
|
|
50
|
+
A route of the host's own, such as a download or a WebSocket upgrade, takes `Authentication.protect(descriptor)`, whose rules, authorization and caching included, are on its JSDoc:
|
|
65
51
|
|
|
66
52
|
```ts example=kitchen-sink/src/attachments/download.ts
|
|
53
|
+
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
67
54
|
import { Effect, Layer } from "effect";
|
|
68
55
|
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
69
|
-
|
|
56
|
+
|
|
70
57
|
import { authorize } from "../identity/authorize.js";
|
|
71
58
|
import { CurrentMember, Session } from "../identity/Identity.js";
|
|
72
59
|
import { GetAttachment } from "./Attachments.js";
|
|
@@ -104,26 +91,24 @@ export const download = Layer.unwrap(
|
|
|
104
91
|
});
|
|
105
92
|
}).pipe(
|
|
106
93
|
Effect.catchTag("AttachmentNotFound", () =>
|
|
107
|
-
Effect.succeed(HttpServerResponse.empty({ status: 404 }))
|
|
108
|
-
)
|
|
109
|
-
)
|
|
110
|
-
).pipe(Layer.provide(Authentication.protect(Session).layer))
|
|
111
|
-
)
|
|
94
|
+
Effect.succeed(HttpServerResponse.empty({ status: 404 }))
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
).pipe(Layer.provide(Authentication.protect(Session).layer))
|
|
98
|
+
)
|
|
112
99
|
);
|
|
113
100
|
```
|
|
114
101
|
|
|
115
|
-
[kitchen-sink/test/attachments/attachments.test.ts](../examples/kitchen-sink/test/attachments/attachments.test.ts)
|
|
116
|
-
asserts what it answers.
|
|
102
|
+
[kitchen-sink/test/attachments/attachments.test.ts](../examples/kitchen-sink/test/attachments/attachments.test.ts) asserts what it answers.
|
|
117
103
|
|
|
118
104
|
## Run a request policy before authentication
|
|
119
105
|
|
|
120
|
-
For a check that must refuse a request before its credential is read, such as a `Host` or
|
|
121
|
-
`Origin` allowlist, make global router middleware, `HttpRouter.middleware(check, { global: true })`,
|
|
122
|
-
and provide it in the last `Layer.provide`:
|
|
106
|
+
For a check that must refuse a request before its credential is read, such as a `Host` or `Origin` allowlist, make global router middleware, `HttpRouter.middleware(check, { global: true })`, and provide it in the last `Layer.provide`:
|
|
123
107
|
|
|
124
108
|
```ts example=kitchen-sink/src/request-policy.ts
|
|
125
109
|
import { Effect, Layer, Option } from "effect";
|
|
126
110
|
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
111
|
+
|
|
127
112
|
import { allowedOrigins, publicUrl } from "./config.js";
|
|
128
113
|
|
|
129
114
|
const loopback = ["localhost", "127.0.0.1"];
|
|
@@ -137,28 +122,39 @@ const loopback = ["localhost", "127.0.0.1"];
|
|
|
137
122
|
// authentication, too late.
|
|
138
123
|
export const requestPolicy = HttpRouter.middleware(
|
|
139
124
|
Effect.gen(function* () {
|
|
140
|
-
const hosts = [...loopback, new URL(yield* publicUrl).hostname];
|
|
125
|
+
const hosts = new Set([...loopback, new URL(yield* publicUrl).hostname]);
|
|
141
126
|
const origins = yield* allowedOrigins;
|
|
142
127
|
|
|
143
128
|
return (httpEffect) =>
|
|
144
129
|
Effect.gen(function* () {
|
|
145
130
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
146
|
-
const url = HttpServerRequest.toURL(request.modify({ url: request.originalUrl }));
|
|
147
131
|
|
|
148
|
-
|
|
132
|
+
const url = HttpServerRequest.toURL(
|
|
133
|
+
request.modify({ url: request.originalUrl })
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (Option.isNone(url) || !hosts.has(url.value.hostname)) {
|
|
149
137
|
return HttpServerResponse.text("Host not allowed", { status: 403 });
|
|
150
138
|
}
|
|
151
139
|
|
|
152
|
-
const origin = request.headers
|
|
153
|
-
|
|
154
|
-
|
|
140
|
+
const { origin } = request.headers;
|
|
141
|
+
|
|
142
|
+
// The request's own origin is that of a page served from this port directly. Behind a
|
|
143
|
+
// proxy ending TLS it is an `http:` one from Effect 4.0.6, whose `originalUrl` takes the
|
|
144
|
+
// scheme of the connection, whatever `X-Forwarded-Proto` says, so the public URL's
|
|
145
|
+
// origin is among `origins`.
|
|
146
|
+
if (
|
|
147
|
+
origin !== undefined &&
|
|
148
|
+
origin !== url.value.origin &&
|
|
149
|
+
!origins.includes(origin)
|
|
150
|
+
) {
|
|
155
151
|
return HttpServerResponse.text("Origin not allowed", { status: 403 });
|
|
156
152
|
}
|
|
157
153
|
|
|
158
154
|
return yield* httpEffect;
|
|
159
155
|
});
|
|
160
156
|
}),
|
|
161
|
-
{ global: true }
|
|
157
|
+
{ global: true }
|
|
162
158
|
);
|
|
163
159
|
|
|
164
160
|
// CORS for the one cross-origin caller, a browser MCP client. `HttpRouter.cors` is global to
|
|
@@ -181,18 +177,18 @@ export const cors = Layer.unwrap(
|
|
|
181
177
|
"MCP-Name",
|
|
182
178
|
"Mcp-Session-Id",
|
|
183
179
|
],
|
|
184
|
-
exposedHeaders: [
|
|
185
|
-
|
|
186
|
-
|
|
180
|
+
exposedHeaders: [
|
|
181
|
+
"WWW-Authenticate",
|
|
182
|
+
"MCP-Protocol-Version",
|
|
183
|
+
"Mcp-Session-Id",
|
|
184
|
+
],
|
|
185
|
+
})
|
|
186
|
+
)
|
|
187
187
|
);
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
See `publicRoutes` in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts);
|
|
191
|
-
[kitchen-sink/test/request-policy.test.ts](../examples/kitchen-sink/test/request-policy.test.ts)
|
|
192
|
-
asserts it.
|
|
190
|
+
See `publicRoutes` in [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts); [kitchen-sink/test/request-policy.test.ts](../examples/kitchen-sink/test/request-policy.test.ts) asserts it.
|
|
193
191
|
|
|
194
192
|
## Refuse outside the router
|
|
195
193
|
|
|
196
|
-
For a caller the router never routes, such as a Node `upgrade` handler admitting a socket, read
|
|
197
|
-
the token with `Authentication.bearerTokenOf` and answer a refusal with
|
|
198
|
-
`Authentication.refusalResponse`, whose JSDoc holds the rest.
|
|
194
|
+
For a caller the router never routes, such as a Node `upgrade` handler admitting a socket, read the token with `Authentication.bearerTokenOf` and answer a refusal with `Authentication.refusalResponse`, whose JSDoc holds the rest.
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# Authentication failure modes
|
|
2
2
|
|
|
3
|
-
What you see when something is wrong, and the fix: compiler diagnostics and errors the library
|
|
4
|
-
does not word. Search for a fragment of the message. A message the library throws says its
|
|
5
|
-
fix itself. Tasks: [Authentication.md](Authentication.md),
|
|
6
|
-
[Authentication-advanced.md](Authentication-advanced.md).
|
|
3
|
+
What you see when something is wrong, and the fix: compiler diagnostics and errors the library does not word. Search for a fragment of the message. A message the library throws says its fix itself. Tasks: [Authentication.md](Authentication.md), [Authentication-advanced.md](Authentication-advanced.md).
|
|
7
4
|
|
|
8
5
|
## Types and startup
|
|
9
6
|
|
|
@@ -13,6 +10,10 @@ fix itself. Tasks: [Authentication.md](Authentication.md),
|
|
|
13
10
|
- `is not assignable to type 'Forbidden | Unauthenticated'`, or `Property '…' is missing in type`, at `layer`'s verifier: it fails with an error that is neither a refusal nor one its descriptor declares, or succeeds with a value that is not the identity. Declare the error in the descriptor's `error`, or map it to a refusal where it is the caller's.
|
|
14
11
|
- `Request<"Requires", Tenant>` still owed although the tenant middleware's layer is provided: it is in one `Layer.provide([...])` array with the provider, and an array's layers provide to the routes, not to one another. Provide it in a later `Layer.provide` of its own ([Read request context in a verifier](Authentication-advanced.md#read-request-context-in-a-verifier)).
|
|
15
12
|
|
|
13
|
+
## Signing in
|
|
14
|
+
|
|
15
|
+
- An MCP client reports `Protected resource … does not match expected …` and requests no token: the published `protectedResource.resource` is neither the endpoint's public URL nor its origin. Give one of them.
|
|
16
|
+
|
|
16
17
|
## Scopes
|
|
17
18
|
|
|
18
19
|
- `Schema validation failed` thrown by `new Action.Forbidden({ scopes })`: a scope is no OAuth scope token, being empty or holding a space, `"`, `\` or a character outside printable ASCII. Give each scope as its own element.
|
package/docs/Authentication.md
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
# Authentication
|
|
2
2
|
|
|
3
|
-
How a remote caller proves the identity a protected action declares. `Authentication.make`
|
|
4
|
-
declares it, as a browser-safe descriptor; `Authentication.layer` is that descriptor's provider,
|
|
5
|
-
running the verifier you write. Verifying a token, login and consent stay in the application.
|
|
3
|
+
How a remote caller proves the identity a protected action declares. `Authentication.make` declares it, as a browser-safe descriptor; `Authentication.layer` is that descriptor's provider, running the verifier you write. Verifying a token, login and consent stay in the application.
|
|
6
4
|
|
|
7
|
-
A message the library throws says its fix; for a compiler diagnostic or Effect's own error,
|
|
8
|
-
search [Authentication-failures.md](Authentication-failures.md). For one symbol,
|
|
9
|
-
`grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc
|
|
10
|
-
carries its rules and gotchas, and this page only what an example cannot show.
|
|
5
|
+
A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [Authentication-failures.md](Authentication-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
|
|
11
6
|
|
|
12
7
|
## Declare how callers sign in
|
|
13
8
|
|
|
14
|
-
A protected action declares its identity service, `caller: CurrentUser`
|
|
15
|
-
([Action.md](Action.md#say-who-may-call-an-action)). Declare the descriptor beside that
|
|
16
|
-
service, in the contract file every client of the binding imports:
|
|
9
|
+
A protected action declares its identity service, `caller: CurrentUser` ([Action.md](Action.md#say-who-may-call-an-action)). Declare the descriptor beside that service, in the contract file every client of the binding imports:
|
|
17
10
|
|
|
18
11
|
```ts example=notes-service/src/identity/Identity.ts
|
|
19
|
-
import { Context, Schema } from "effect";
|
|
20
12
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
21
13
|
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
14
|
+
import { Context, Schema } from "effect";
|
|
22
15
|
|
|
23
16
|
export const scopes = { read: "notes:read", write: "notes:write" } as const;
|
|
24
17
|
|
|
@@ -34,7 +27,7 @@ export interface User {
|
|
|
34
27
|
* provides it per request, and a local host, such as the stdio server, provides it itself.
|
|
35
28
|
*/
|
|
36
29
|
export class CurrentUser extends Context.Service<CurrentUser, User>()(
|
|
37
|
-
"notes-service/CurrentUser"
|
|
30
|
+
"notes-service/CurrentUser"
|
|
38
31
|
) {}
|
|
39
32
|
|
|
40
33
|
// How a remote caller proves it is CurrentUser: a bearer token, the default scheme. Clients
|
|
@@ -49,45 +42,36 @@ export const WhoAmI = Action.make("whoAmI", {
|
|
|
49
42
|
});
|
|
50
43
|
```
|
|
51
44
|
|
|
52
|
-
Name it in every binding and MCP endpoint that serves a protected action: an HTTP binding
|
|
53
|
-
([notes-service/src/Api.ts](../examples/notes-service/src/Api.ts)), an MCP endpoint
|
|
54
|
-
([notes-service/src/routes.ts](../examples/notes-service/src/routes.ts)) and an RPC binding
|
|
55
|
-
([live-rpc/src/Rpc.ts](../examples/live-rpc/src/Rpc.ts)); a local surface takes none
|
|
56
|
-
([Action.md](Action.md#say-who-may-call-an-action)). The name's rules are on
|
|
57
|
-
`Authentication.make`, the scheme's on `Authentication.Options.security`; a second credential
|
|
58
|
-
for one identity is in
|
|
59
|
-
[kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts).
|
|
45
|
+
Name it in every binding and MCP endpoint that serves a protected action: an HTTP binding ([notes-service/src/Api.ts](../examples/notes-service/src/Api.ts)), an MCP endpoint ([notes-service/src/routes.ts](../examples/notes-service/src/routes.ts)) and an RPC binding ([live-rpc/src/Rpc.ts](../examples/live-rpc/src/Rpc.ts)); a local surface takes none ([Action.md](Action.md#say-who-may-call-an-action)). The name's rules are on `Authentication.make`, the scheme's on `Authentication.Options.security`; a second credential for one identity is in [kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts). A descriptor of protected [events](Event.md) may also say which subscriber an identity is, its `principal`, for what a surface holds between one subscriber's requests: [build-events/src/identity/Identity.ts](../examples/build-events/src/identity/Identity.ts). When it is required is on `Authentication.Options.principal`; its `schema`, which keeps an identity past a restart, on `Authentication.Options.schema`.
|
|
60
46
|
|
|
61
|
-
- **Protecting an action that tests already call breaks them**, at compile time
|
|
62
|
-
([Testing-failures.md](Testing-failures.md),
|
|
63
|
-
[Authentication-failures.md](Authentication-failures.md#types-and-startup)), and a call over
|
|
64
|
-
HTTP without a credential gets `Unauthenticated` at run time.
|
|
47
|
+
- **Protecting an action that tests already call breaks them**, at compile time ([Testing-failures.md](Testing-failures.md), [Authentication-failures.md](Authentication-failures.md#types-and-startup)), and a call over HTTP without a credential gets `Unauthenticated` at run time.
|
|
65
48
|
|
|
66
49
|
## Verify credentials
|
|
67
50
|
|
|
68
|
-
Write the verifier in a server-only module, give it to `Authentication.layer` and provide that
|
|
69
|
-
layer as [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts) does. What the
|
|
70
|
-
verifier takes, fails with and must check itself is on `Authentication.Verify`; where its layer
|
|
71
|
-
goes, on `Authentication.layer`.
|
|
51
|
+
Write the verifier in a server-only module, give it to `Authentication.layer` and provide that layer as [notes-service/src/routes.ts](../examples/notes-service/src/routes.ts) does. What the verifier takes, fails with and must check itself is on `Authentication.Verify`; where its layer goes, on `Authentication.layer`.
|
|
72
52
|
|
|
73
53
|
```ts example=notes-service/src/identity/verifier.ts
|
|
74
|
-
import { Config, Effect, Redacted } from "effect";
|
|
75
54
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
76
55
|
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
56
|
+
import { Config, Effect, Redacted } from "effect";
|
|
57
|
+
|
|
77
58
|
import { Login, scopes } from "./Identity.js";
|
|
78
59
|
import { users } from "./users.js";
|
|
79
60
|
|
|
80
|
-
const isToken = (token: string): token is keyof typeof users =>
|
|
61
|
+
const isToken = (token: string): token is keyof typeof users =>
|
|
62
|
+
Object.hasOwn(users, token);
|
|
81
63
|
|
|
82
64
|
// DEMO ONLY: a token is a user's name. A real verifier checks a signed token with your
|
|
83
65
|
// authorization server's library, its audience included. An unknown token is the built-in
|
|
84
66
|
// `Unauthenticated`: a 401 every client decodes.
|
|
85
|
-
const verify = (token: Redacted.Redacted
|
|
67
|
+
const verify = (token: Redacted.Redacted) => {
|
|
86
68
|
const name = Redacted.value(token);
|
|
87
69
|
|
|
88
70
|
return isToken(name)
|
|
89
71
|
? Effect.succeed(users[name])
|
|
90
|
-
: Effect.fail(
|
|
72
|
+
: Effect.fail(
|
|
73
|
+
new Action.Unauthenticated({ message: "Unknown demo token." })
|
|
74
|
+
);
|
|
91
75
|
};
|
|
92
76
|
|
|
93
77
|
// Server only: `Login`'s provider. As an OAuth protected resource it also publishes discovery,
|
|
@@ -95,7 +79,9 @@ const verify = (token: Redacted.Redacted<string>) => {
|
|
|
95
79
|
// is the server's public URL, known only at startup, so it is read from config.
|
|
96
80
|
export const authenticate = Authentication.layer(Login, verify, {
|
|
97
81
|
protectedResource: Effect.map(
|
|
98
|
-
Config.String("PUBLIC_URL").pipe(
|
|
82
|
+
Config.String("PUBLIC_URL").pipe(
|
|
83
|
+
Config.withDefault("http://localhost:3000")
|
|
84
|
+
),
|
|
99
85
|
(url) =>
|
|
100
86
|
({
|
|
101
87
|
resource: `${url}/mcp`,
|
|
@@ -103,38 +89,17 @@ export const authenticate = Authentication.layer(Login, verify, {
|
|
|
103
89
|
authorizationServers: ["https://auth.example.com"],
|
|
104
90
|
scopesSupported: [scopes.read, scopes.write],
|
|
105
91
|
scopesRequired: [scopes.read],
|
|
106
|
-
}) satisfies Authentication.ProtectedResource
|
|
92
|
+
}) satisfies Authentication.ProtectedResource
|
|
107
93
|
),
|
|
108
94
|
});
|
|
109
95
|
```
|
|
110
96
|
|
|
111
|
-
For a verifier built from startup services, see `authenticateBearer` in
|
|
112
|
-
[kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts);
|
|
113
|
-
for a failure that is not the caller's, `Authentication.Options.error` and `IssuerUnavailable` in
|
|
114
|
-
[kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts); to
|
|
115
|
-
test as a signed-in caller, [Testing.md](Testing.md#test-as-a-signed-in-caller). What each
|
|
116
|
-
surface answers a missing or refused credential with is on `Action.Unauthenticated` and
|
|
117
|
-
`Action.Forbidden`, as
|
|
118
|
-
[kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts)
|
|
119
|
-
asserts, status and challenge included.
|
|
97
|
+
For a verifier built from startup services, see `authenticateBearer` in [kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts); for a failure that is not the caller's, `Authentication.Options.error` and `IssuerUnavailable` in [kitchen-sink/src/identity/Identity.ts](../examples/kitchen-sink/src/identity/Identity.ts); to test as a signed-in caller, [Testing.md](Testing.md#test-as-a-signed-in-caller). What each surface answers a missing or refused credential with is on `Action.Unauthenticated` and `Action.Forbidden`, as [kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts) asserts, status and challenge included.
|
|
120
98
|
|
|
121
99
|
## Publish OAuth discovery
|
|
122
100
|
|
|
123
|
-
For an MCP endpoint whose users sign in through OAuth, give the Bearer descriptor's `layer` a
|
|
124
|
-
`protectedResource`, as the verifier above does. Its rules are on
|
|
125
|
-
`Authentication.LayerOptions.protectedResource` and on each field of
|
|
126
|
-
`Authentication.ProtectedResource`;
|
|
127
|
-
[kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts)
|
|
128
|
-
asserts the discovery it serves. To restrict who reads it, see
|
|
129
|
-
[Run a request policy before authentication](Authentication-advanced.md#run-a-request-policy-before-authentication).
|
|
101
|
+
For an MCP endpoint whose users sign in through OAuth, give the Bearer descriptor's `layer` a `protectedResource`, as the verifier above does. Its rules are on `Authentication.LayerOptions.protectedResource` and on each field of `Authentication.ProtectedResource`; [kitchen-sink/test/identity/identity.test.ts](../examples/kitchen-sink/test/identity/identity.test.ts) asserts the discovery it serves. To restrict who reads it, see [Run a request policy before authentication](Authentication-advanced.md#run-a-request-policy-before-authentication).
|
|
130
102
|
|
|
131
103
|
### Step up for a scope
|
|
132
104
|
|
|
133
|
-
To have an OAuth client sign in again, or re-authorize with more scopes, fail the authorizer, a
|
|
134
|
-
handler or the verifier with `Action.Unauthenticated`, or with `Action.Forbidden` naming
|
|
135
|
-
`scopes`
|
|
136
|
-
([notes-service/src/identity/authorize.ts](../examples/notes-service/src/identity/authorize.ts));
|
|
137
|
-
`inService` in
|
|
138
|
-
[kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts)
|
|
139
|
-
refuses with none. What each answers on each surface, and when to name scopes, are on those two
|
|
140
|
-
classes.
|
|
105
|
+
To have an OAuth client sign in again, or re-authorize with more scopes, fail the authorizer, a handler or the verifier with `Action.Unauthenticated`, or with `Action.Forbidden` naming `scopes` ([notes-service/src/identity/authorize.ts](../examples/notes-service/src/identity/authorize.ts)); `inService` in [kitchen-sink/src/identity/authenticate.ts](../examples/kitchen-sink/src/identity/authenticate.ts) refuses with none. What each answers on each surface, and when to name scopes, are on those two classes.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Event failure modes
|
|
2
|
+
|
|
3
|
+
What you see when something is wrong, and the fix. Search for a fragment of the message. The tasks are in [Event.md](Event.md).
|
|
4
|
+
|
|
5
|
+
## At `implement`
|
|
6
|
+
|
|
7
|
+
- `is not assignable to parameter of type '... & "A protected event takes { authorize }: your rule, or Event.allowAll"'`: the list holds a protected event type, which takes `{ authorize }`. Give your rule, or `Event.allowAll`.
|
|
8
|
+
- `Type '{ authorize: ... }' is not assignable to type '{ readonly authorize?: "A public-only target takes no authorize" }'`: every event type in the list is public, and no authorizer runs for one. Remove `authorize`.
|
|
9
|
+
- `Object literal may only specify known properties, and 'log' does not exist`, or `Type '{}' is not assignable to type 'undefined'` at a source: it holds both `check` and `log`. Keep one: `check` where a store can be asked what happened since a cursor, `log` where nothing else keeps the occurrences.
|
|
10
|
+
- `Parameter 'input' implicitly has an 'any' type` at a check, a `match` or a `transform`: the builder is wrapped, as in `Effect.gen(…).pipe(Effect.provide(layer))`, and its sources are no longer typed from the contracts. Give `implement` the builder itself.
|
|
11
|
+
|
|
12
|
+
## At `watch`
|
|
13
|
+
|
|
14
|
+
- `Property 'watch' does not exist on type`: the client was given no event implementation. Pass the event implementations beside the action ones, `Action.client([builds, buildEvents])`.
|
|
15
|
+
- `Argument of type 'Event<...>' is not assignable to parameter of type` at `watch`: the client holds no implementation of that event type. Add its implementation to the list.
|