@gjermundgaraba/effect-actions 0.9.0 → 0.10.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 +57 -50
- package/dist/Action.d.mts +285 -79
- package/dist/Action.mjs +124 -23
- package/dist/ActionCli.d.mts +102 -31
- package/dist/ActionCli.mjs +114 -16
- package/dist/ActionHttp.d.mts +170 -51
- package/dist/ActionHttp.mjs +130 -79
- package/dist/ActionMcp.d.mts +71 -21
- package/dist/ActionMcp.mjs +108 -81
- package/dist/ActionToolkit.d.mts +56 -27
- package/dist/ActionToolkit.mjs +16 -5
- package/dist/Authentication.d.mts +129 -33
- package/dist/Authentication.mjs +231 -38
- package/dist/Testing.d.mts +82 -57
- package/dist/Testing.mjs +89 -79
- package/dist/internal/actions.d.mts +5 -25
- package/dist/internal/actions.mjs +75 -27
- package/dist/internal/authentication.d.mts +102 -0
- package/dist/internal/authentication.mjs +11 -0
- package/dist/internal/call.d.mts +12 -0
- package/dist/internal/call.mjs +12 -0
- package/dist/internal/cli.d.mts +36 -22
- package/dist/internal/cli.mjs +311 -33
- package/dist/internal/client.d.mts +43 -0
- package/dist/internal/client.mjs +25 -0
- package/dist/internal/console.mjs +77 -0
- package/dist/internal/declared.d.mts +13 -0
- package/dist/internal/declared.mjs +34 -0
- package/dist/internal/errors.d.mts +77 -0
- package/dist/internal/errors.mjs +79 -0
- package/dist/internal/implementation.d.mts +171 -41
- package/dist/internal/implementation.mjs +147 -29
- package/dist/internal/mcp.d.mts +11 -0
- package/dist/internal/mcp.mjs +52 -0
- package/dist/internal/memory.d.mts +6 -0
- package/dist/internal/memory.mjs +38 -0
- package/dist/internal/refusal.mjs +67 -0
- package/dist/internal/schema-errors.mjs +20 -0
- package/dist/internal/tools.mjs +27 -57
- package/docs/Action.md +401 -73
- package/docs/ActionCli.md +280 -51
- package/docs/ActionHttp.md +266 -82
- package/docs/ActionMcp.md +149 -104
- package/docs/ActionToolkit.md +97 -35
- package/docs/Authentication.md +369 -62
- package/docs/CONTEXT.md +23 -36
- package/docs/README.md +29 -73
- package/docs/Testing.md +154 -77
- package/docs/guarantees.md +81 -43
- package/docs/setup.md +90 -27
- package/package.json +7 -15
- package/dist/ActionCatalog.d.mts +0 -30
- package/dist/ActionCatalog.mjs +0 -39
- package/dist/ActionCliClient.d.mts +0 -48
- package/dist/ActionCliClient.mjs +0 -43
- package/dist/ActionGroup.d.mts +0 -44
- package/dist/ActionGroup.mjs +0 -35
- package/dist/ActionHttpClient.d.mts +0 -33
- package/dist/ActionHttpClient.mjs +0 -16
- package/dist/TestingClient.d.mts +0 -17
- package/dist/TestingClient.mjs +0 -22
- package/dist/internal/fetchClient.d.mts +0 -6
- package/dist/internal/fetchClient.mjs +0 -8
- package/dist/internal/tools.d.mts +0 -22
- package/docs/ActionCatalog.md +0 -54
- package/docs/ActionCliClient.md +0 -63
- package/docs/ActionGroup.md +0 -128
- package/docs/ActionHttpClient.md +0 -80
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Refusal, statuses } from "./errors.mjs";
|
|
2
|
+
import { Cause, Context, Effect, Exit, Option, Predicate, Ref, Schema } from "effect";
|
|
3
|
+
import { HttpServerResponse } from "effect/http";
|
|
4
|
+
//#region src/internal/refusal.ts
|
|
5
|
+
/** An RFC 6750 error description: printable ASCII but `"` and `\`. */
|
|
6
|
+
const description = /^[\x20\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
7
|
+
/**
|
|
8
|
+
* An RFC 9110 challenge of `scheme` and the parameters given, each a quoted string whose `"`
|
|
9
|
+
* and `\` are escaped: a metadata URL's query may hold a `\`.
|
|
10
|
+
*/
|
|
11
|
+
const challenge = (scheme, parameters) => {
|
|
12
|
+
const given = parameters.flatMap(([name, value]) => value === void 0 ? [] : [`${name}="${value.replace(/["\\]/g, "\\$&")}"`]);
|
|
13
|
+
return given.length === 0 ? scheme : `${scheme} ${given.join(", ")}`;
|
|
14
|
+
};
|
|
15
|
+
/** An RFC 6750 `Bearer` challenge of the parameters given. */
|
|
16
|
+
const bearer = (parameters) => challenge("Bearer", parameters);
|
|
17
|
+
const encode = Schema.encodeSync(Refusal);
|
|
18
|
+
/** Whether `error` is a built-in refusal, `Unauthenticated` or `Forbidden`. */
|
|
19
|
+
const isRefusal = Schema.is(Refusal);
|
|
20
|
+
/**
|
|
21
|
+
* The RFC 6750 `insufficient_scope` challenge an OAuth client steps up on, for a `Forbidden`
|
|
22
|
+
* naming scopes, naming the resource's metadata URL when there is one.
|
|
23
|
+
*/
|
|
24
|
+
const insufficientScope = (error, metadataUrl) => Predicate.isTagged(error, "Forbidden") && error.scopes !== void 0 ? bearer([
|
|
25
|
+
["error", "insufficient_scope"],
|
|
26
|
+
["scope", error.scopes.join(" ")],
|
|
27
|
+
["resource_metadata", metadataUrl],
|
|
28
|
+
["error_description", description.test(error.message) ? error.message : void 0]
|
|
29
|
+
]) : void 0;
|
|
30
|
+
/** A refusal as its JSON with its status alone, as a scheme no OAuth client acts on answers it. */
|
|
31
|
+
const plain = (error) => HttpServerResponse.jsonUnsafe(encode(error), { status: statuses[error._tag] });
|
|
32
|
+
/**
|
|
33
|
+
* A refusal as its JSON with its status, as every endpoint declares it, and its
|
|
34
|
+
* `insufficient_scope` challenge if it has one.
|
|
35
|
+
*/
|
|
36
|
+
const answer = (error, metadataUrl) => {
|
|
37
|
+
const response = plain(error);
|
|
38
|
+
const scoped = insufficientScope(error, metadataUrl);
|
|
39
|
+
return scoped === void 0 ? response : HttpServerResponse.setHeader(response, "www-authenticate", scoped);
|
|
40
|
+
};
|
|
41
|
+
/** The step-up refusal a tool's call under `answerStepUp` failed with, which answers its request. */
|
|
42
|
+
var SteppedUp = class extends Context.Service()("effect-actions/SteppedUp") {};
|
|
43
|
+
/**
|
|
44
|
+
* Whether `error` is a refusal an OAuth client acts on: `Unauthenticated`, or `Forbidden`
|
|
45
|
+
* naming scopes.
|
|
46
|
+
*/
|
|
47
|
+
const isStepUp = (error) => isRefusal(error) && (!Predicate.isTagged(error, "Forbidden") || error.scopes !== void 0);
|
|
48
|
+
/**
|
|
49
|
+
* A protected MCP tool's `call`, whose step-up refusal answers the request under a Bearer
|
|
50
|
+
* descriptor's `answerStepUp`. Elsewhere it is `call` as it is.
|
|
51
|
+
*/
|
|
52
|
+
const recordStepUp = (call) => Effect.tapError(call, (error) => isStepUp(error) ? Effect.flatMap(Effect.serviceOption(SteppedUp), (slot) => Option.isSome(slot) ? Ref.set(slot.value, Option.some(error)) : Effect.void) : Effect.void);
|
|
53
|
+
/**
|
|
54
|
+
* An MCP request, answered with the step-up refusal a tool's call in it failed with, its
|
|
55
|
+
* status, JSON and challenge naming `metadataUrl`, however the request ended short of a defect
|
|
56
|
+
* or an interruption: a tool's failure is a successful result, and MCP authorization requires
|
|
57
|
+
* 401 or 403.
|
|
58
|
+
*/
|
|
59
|
+
const answerStepUp = (route, metadataUrl) => Effect.gen(function* () {
|
|
60
|
+
const slot = yield* Ref.make(Option.none());
|
|
61
|
+
const exit = yield* Effect.exit(Effect.provideService(route, SteppedUp, slot));
|
|
62
|
+
const refused = yield* Ref.get(slot);
|
|
63
|
+
const broken = Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause));
|
|
64
|
+
return Option.isSome(refused) && !broken ? answer(refused.value, metadataUrl) : yield* exit;
|
|
65
|
+
});
|
|
66
|
+
//#endregion
|
|
67
|
+
export { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain, recordStepUp };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { InvalidInput } from "./errors.mjs";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { HttpApiMiddleware } from "effect/http-api";
|
|
4
|
+
//#region src/internal/schema-errors.ts
|
|
5
|
+
/**
|
|
6
|
+
* `HttpApiBuilder` reports these kinds while encoding the handler's answer, after the
|
|
7
|
+
* handler ran; every other kind (`Payload`, `Params`, `Headers`, `Query`) comes from
|
|
8
|
+
* decoding the request before it.
|
|
9
|
+
*/
|
|
10
|
+
const responseKinds = /* @__PURE__ */ new Set(["Body", "ResponseHeaders"]);
|
|
11
|
+
/**
|
|
12
|
+
* The native middleware answering schema failures: a request that does not decode with
|
|
13
|
+
* `InvalidInput`, the schema's own message and issues, a result that does not encode as a defect,
|
|
14
|
+
* the empty 500 of any other server bug.
|
|
15
|
+
*/
|
|
16
|
+
var SchemaErrors = class extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: [InvalidInput] }) {};
|
|
17
|
+
/** `SchemaErrors`' server side, built by `ActionHttp.layer` alone, so a client's bundle leaves it out. */
|
|
18
|
+
const schemaErrors = () => HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => responseKinds.has(failure.kind) ? Effect.die(failure.cause) : Effect.fail(InvalidInput.fromSchemaError(failure.cause)));
|
|
19
|
+
//#endregion
|
|
20
|
+
export { SchemaErrors, schemaErrors };
|
package/dist/internal/tools.mjs
CHANGED
|
@@ -1,74 +1,44 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { assertOnce, projectedErrors } from "./actions.mjs";
|
|
2
|
+
import { acquire } from "./implementation.mjs";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
4
4
|
import { Tool, Toolkit } from "effect/ai";
|
|
5
5
|
//#region src/internal/tools.ts
|
|
6
|
+
/** `tool` annotated with `value` under `key`, or as it is where `value` is not given. */
|
|
7
|
+
const annotateIf = (tool, key, value) => value === void 0 ? tool : tool.annotate(key, value);
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* it
|
|
9
|
+
* A tool's annotations: read-only exactly when its action is, and the contract's `mcp` ones
|
|
10
|
+
* where given, so the native defaults, MCP's, stand for the others. A read-only tool is not
|
|
11
|
+
* destructive unless it says so, where the native default calls every tool destructive.
|
|
10
12
|
*/
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
app,
|
|
18
|
-
actions
|
|
19
|
-
}];
|
|
20
|
-
});
|
|
21
|
-
/** Flatten the projected contracts only after deciding which groups are served. */
|
|
22
|
-
const entries = (apps) => apps.flatMap(({ app, actions }) => actions.map((action) => ({
|
|
23
|
-
app,
|
|
24
|
-
action
|
|
25
|
-
})));
|
|
26
|
-
const annotate = (tool, mcp) => tool.annotate(Tool.Readonly, mcp.readOnly).annotate(Tool.Destructive, mcp.destructive).annotate(Tool.Idempotent, mcp.idempotent).annotate(Tool.OpenWorld, mcp.openWorld);
|
|
27
|
-
/**
|
|
28
|
-
* A native Effect AI tool. Its schemas retain action transforms and its result
|
|
29
|
-
* is the action result itself, rather than an MCP response envelope.
|
|
30
|
-
*/
|
|
31
|
-
const nativeTool = (action, errors) => annotate(Tool.make(action.mcp.name, {
|
|
32
|
-
description: action.description,
|
|
33
|
-
parameters: action.input,
|
|
34
|
-
success: action.success,
|
|
35
|
-
failure: Schema.Union(errors),
|
|
36
|
-
failureMode: "return"
|
|
37
|
-
}), action.mcp);
|
|
13
|
+
const annotate = (tool, { readOnly, mcp }) => {
|
|
14
|
+
const destructive = mcp.destructiveHint ?? (readOnly ? false : void 0);
|
|
15
|
+
const meta = mcp._meta === void 0 ? void 0 : { ...mcp._meta };
|
|
16
|
+
const hinted = annotateIf(annotateIf(annotateIf(tool.annotate(Tool.Readonly, readOnly), Tool.Destructive, destructive), Tool.Idempotent, mcp.idempotentHint), Tool.OpenWorld, mcp.openWorldHint);
|
|
17
|
+
return annotateIf(annotateIf(hinted, Tool.Title, mcp.title), Tool.Meta, meta);
|
|
18
|
+
};
|
|
38
19
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
20
|
+
* The native tool of `action`. A model and an MCP client speak JSON, so it takes and gives
|
|
21
|
+
* the JSON encoding its schemas advertise, the whole success; handlers and callers see decoded
|
|
22
|
+
* values. A refusal by authentication or `authorize`, or a handler's built-in failure, is the implementation's failure, so
|
|
23
|
+
* it declares the built-in errors alongside the action's own, and returns them as its result.
|
|
41
24
|
*/
|
|
42
|
-
const
|
|
25
|
+
const toolOf = (action) => Tool.make(action.name, {
|
|
43
26
|
description: action.description,
|
|
44
27
|
parameters: Schema.toCodecJson(action.input),
|
|
45
28
|
success: Schema.toCodecJson(action.success),
|
|
46
|
-
failure: Schema.toCodecJson(Schema.Union(
|
|
29
|
+
failure: Schema.toCodecJson(Schema.Union(projectedErrors(action))),
|
|
47
30
|
failureMode: "return"
|
|
48
|
-
})
|
|
49
|
-
const project = (projection, entry, options) => {
|
|
50
|
-
const errors = projectedErrors(entry.action, options.errors);
|
|
51
|
-
const tool = projection === "native" ? nativeTool(entry.action, errors) : mcpTool(entry.action, errors);
|
|
52
|
-
if (projection === "native") return tool;
|
|
53
|
-
return tool.annotate(Tool.Strict, true);
|
|
54
|
-
};
|
|
31
|
+
});
|
|
55
32
|
/**
|
|
56
|
-
* Project
|
|
57
|
-
* Native and MCP differ only in tool codecs; selection, scoped acquisition, dispatch
|
|
58
|
-
* and native Toolkit binding stay identical.
|
|
33
|
+
* Project actions as tools named after them, sharing each implementation's built handlers.
|
|
59
34
|
*/
|
|
60
|
-
const bindTools = (apps, projection
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
assertDistinct("MCP tool", selected.map(({ action }) => action.mcp.name));
|
|
64
|
-
const toolkit = Toolkit.make(...selected.map((entry) => project(projection, entry, options)));
|
|
35
|
+
const bindTools = (apps, projection) => {
|
|
36
|
+
assertOnce(projection.label, apps.flatMap((app) => app.actions));
|
|
37
|
+
const toolkit = Toolkit.make(...apps.flatMap((app) => app.actions.map((action) => annotate(projection.tool(toolOf(action), action, app), action))));
|
|
65
38
|
return {
|
|
66
39
|
toolkit,
|
|
67
|
-
layer: toolkit.toLayer(Effect.map(
|
|
68
|
-
const record = handlers;
|
|
69
|
-
return actions.map((action) => [action.mcp.name, dispatch(app.group, action, record, options.before)]);
|
|
70
|
-
})), (built) => Object.fromEntries(built.flat())))
|
|
40
|
+
layer: toolkit.toLayer(Effect.map(acquire(apps), (bound) => Object.fromEntries(bound.map(([action, run]) => [action.name, projection.handler?.(run, action) ?? run]))))
|
|
71
41
|
};
|
|
72
42
|
};
|
|
73
43
|
//#endregion
|
|
74
|
-
export {
|
|
44
|
+
export { bindTools };
|