import { Effect } from "effect";import { Data } from "effect"; import type { HandleBridge, HandleRootSpec } from "../seams/tool-sandbox";import { ToolSandboxError } from "../seams/tool-sandbox";import type { ScopeDbHandle } from "../seams/scope-db";import type { ConnectionDecl } from "../pipeline/descriptor"; // ---------------------------------------------------------------------------// Connection DI: declare-then-bind. A tool's descriptor declares `connections`// (role -> integration); before execution each role is bound to the user's// connection(s) explicitly. A missing binding is a typed error naming role +// surface (no auto-pick). The handler receives pre-bound clients whose method// calls route through the platform invoke path.//// The routing is a seam: `ClientResolver` turns a (integration, connection,// method path, args) into a JSON result. The self-hosted resolver calls the// integration's real API through the connection credential (policy/audit// applies there); a test resolver returns canned data. `db` is bound to the// invoking scope's ScopeDb. Everything crossing is JSON (the cloud resolver is// an RPC).// --------------------------------------------------------------------------- export class BindingError extends Data.TaggedError("BindingError")<{ readonly message: string; readonly role: string; readonly surface: string;}> {} /** One bound connection for a role. Fan-out roles bind an ordered set. */export type RoleBinding = | { readonly kind: "single"; readonly connection: string } | { readonly kind: "array"; readonly connections: readonly string[] }; /** The user's bindings for a tool invocation: role -> bound connection(s). */export type Bindings = Readonly<Record<string, RoleBinding>>; /** Resolves a single method call against a bound connection to a JSON result. * This is where the platform invoke path (credentials, policy, audit) lives. */export interface ClientResolver { readonly call: (input: { readonly integration: string; readonly connection: string; readonly path: readonly string[]; readonly args: readonly unknown[]; }) => Effect.Effect<unknown, BindingError>;} export interface BindingContext { /** Declared connection roles from the tool descriptor. */ readonly declared: Readonly<Record<string, ConnectionDecl>>; /** The user's bindings (role -> connection[s]). Missing => typed error. */ readonly bindings: Bindings; /** The invoking scope's app database (bound to the `db` root). */ readonly db: ScopeDbHandle; /** Routes a bound method call to the real integration. */ readonly resolver: ClientResolver;} /** * Validate bindings against declarations and compute the sandbox handle roots: * `db` is always a single root; each declared role becomes a single or array * root. Fails (typed) when a declared role has no binding, naming role + * surface — the "missing binding is a typed error" rule. */export const rootsFor = ( declared: Readonly<Record<string, ConnectionDecl>>, bindings: Bindings,): Effect.Effect<Readonly<Record<string, HandleRootSpec>>, BindingError> => Effect.gen(function* () { const roots: Record<string, HandleRootSpec> = { db: { kind: "single" } }; for (const [role, decl] of Object.entries(declared)) { if (decl.kind === "catalog") { // Open-world proxy: parse + record, but execution is NotImplemented in // this build. Bind a single root; the resolver throws if called. roots[role] = { kind: "single" }; continue; } const binding = bindings[role]; if (!binding) { return yield* new BindingError({ message: `no connection bound for role "${role}" (surface "${decl.integration}")`, role, surface: decl.integration, }); } if (decl.kind === "array") { if (binding.kind !== "array") { return yield* new BindingError({ message: `role "${role}" is a fan-out (connections("${decl.integration}")) and needs an array binding`, role, surface: decl.integration, }); } roots[role] = { kind: "array", count: binding.connections.length }; } else { if (binding.kind !== "single") { return yield* new BindingError({ message: `role "${role}" is a single connection and needs a single binding`, role, surface: decl.integration, }); } roots[role] = { kind: "single" }; } } return roots; }); // Parse a fan-out root name back into (role, index): `inboxes#1` -> {inboxes,1}.const parseRoot = (root: string): { role: string; index?: number } => { const hash = root.indexOf("#"); if (hash === -1) return { role: root }; return { role: root.slice(0, hash), index: Number(root.slice(hash + 1)) };}; // Root names the bridge treats as reserved: nothing the sandbox sends may// address them as a connection role. `db` is handled explicitly first; any other// reserved prefix (a future internal handle) is rejected rather than silently// resolved. (Grafted strictness from A: reject unexpected/reserved calls.)const RESERVED_ROOTS = new Set(["__proto__", "constructor", "prototype"]); const invokeErr = (message: string): ToolSandboxError => new ToolSandboxError({ kind: "invoke", message }); // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: `cause` is a typed value with a `message` field, not an unknown errorconst taggedMessage = (cause: { readonly message: string }): string => cause.message; /** * Build the HandleBridge the sandbox calls out through. `db` routes to the * scope database; a declared role routes to its bound connection through the * `ClientResolver`. The dispatch is STRICT: an empty/malformed path, a reserved * root, an undeclared root, or an out-of-range fan-out index is a typed error, * never a silent success (the cloud RPC backing must be able to trust the same * shape). A `.account` read on a client returns bound-connection metadata * without a round-trip. */export const buildBridge = (context: BindingContext): HandleBridge => ({ call: ({ root, path, args }) => { // Strictness: every bridge call must name a root and a non-empty method path. if (typeof root !== "string" || root.length === 0) { return Effect.fail(invokeErr("bridge call is missing a handle root")); } if (!Array.isArray(path) || path.length === 0) { return Effect.fail(invokeErr(`bridge call to "${root}" has an empty method path`)); } if (RESERVED_ROOTS.has(root)) { return Effect.fail(invokeErr(`reserved handle root is not callable: ${root}`)); } if (root === "db") { // The scope db handle exposes `sql` as a tagged template; the injected // client calls `db.sql(templateStrings, ...values)`. When routed through // the bridge, `path = ["sql"]` and args = [stringsArray, ...values]. if (path.length === 1 && path[0] === "sql") { const [strings, ...values] = args as [TemplateStringsArray, ...unknown[]]; return context.db .sql(strings, ...values) .pipe( Effect.mapError( (cause) => new ToolSandboxError({ kind: "invoke", message: taggedMessage(cause), cause }), ), ); } return Effect.fail(invokeErr(`unsupported db call: ${path.join(".")}`)); } const { role, index } = parseRoot(root); const decl = context.declared[role]; if (!decl) { return Effect.fail(invokeErr(`undeclared handle root: ${root}`)); } if (decl.kind === "catalog") { return Effect.fail( invokeErr("catalog() open-world proxy execution is not implemented in this build"), ); } const binding = context.bindings[role]; // Fan-out roots MUST carry a valid index within the bound array; a single // role MUST NOT carry an index. (Grafted bounds-checking from A.) if (decl.kind === "array") { if (binding?.kind !== "array") { return Effect.fail(invokeErr(`role "${role}" is a fan-out but is not bound to an array`)); } if ( index === undefined || !Number.isInteger(index) || index < 0 || index >= binding.connections.length ) { return Effect.fail(invokeErr(`fan-out handle "${root}" addresses an out-of-range index`)); } } else if (index !== undefined) { return Effect.fail(invokeErr(`single-connection role "${role}" was addressed with an index`)); } const connectionName = binding?.kind === "array" ? binding.connections[index ?? 0] : binding?.kind === "single" ? binding.connection : undefined; if (!connectionName) { return Effect.fail(invokeErr(`no binding for role ${role}`)); } if (path.length === 1 && path[0] === "account") { // Clients read `.account.email` / `.account.login`; expose both keys. return Effect.succeed({ email: connectionName, login: connectionName, name: connectionName }); } return context.resolver .call({ integration: decl.integration, connection: connectionName, path, args }) .pipe( Effect.mapError( (cause) => new ToolSandboxError({ kind: "invoke", message: taggedMessage(cause), cause }), ), ); },});
import { Effect } from "effect";
import { Data } from "effect";
import type { HandleBridge, HandleRootSpec } from "../seams/tool-sandbox";
import { ToolSandboxError } from "../seams/tool-sandbox";
import type { ScopeDbHandle } from "../seams/scope-db";
import type { ConnectionDecl } from "../pipeline/descriptor";
// ---------------------------------------------------------------------------
// Connection DI: declare-then-bind. A tool's descriptor declares `connections`
// (role -> integration); before execution each role is bound to the user's
// connection(s) explicitly. A missing binding is a typed error naming role +
// surface (no auto-pick). The handler receives pre-bound clients whose method
// calls route through the platform invoke path.
//
// The routing is a seam: `ClientResolver` turns a (integration, connection,
// method path, args) into a JSON result. The self-hosted resolver calls the
// integration's real API through the connection credential (policy/audit
// applies there); a test resolver returns canned data. `db` is bound to the
// invoking scope's ScopeDb. Everything crossing is JSON (the cloud resolver is
// an RPC).
// ---------------------------------------------------------------------------
export class BindingError extends Data.TaggedError("BindingError")<{
readonly message: string;
readonly role: string;
readonly surface: string;
}> {}
/** One bound connection for a role. Fan-out roles bind an ordered set. */
export type RoleBinding =
| { readonly kind: "single"; readonly connection: string }
| { readonly kind: "array"; readonly connections: readonly string[] };
/** The user's bindings for a tool invocation: role -> bound connection(s). */
export type Bindings = Readonly<Record<string, RoleBinding>>;
/** Resolves a single method call against a bound connection to a JSON result.
* This is where the platform invoke path (credentials, policy, audit) lives. */
export interface ClientResolver {
readonly call: (input: {
readonly integration: string;
readonly connection: string;
readonly path: readonly string[];
readonly args: readonly unknown[];
}) => Effect.Effect<unknown, BindingError>;
}
export interface BindingContext {
/** Declared connection roles from the tool descriptor. */
readonly declared: Readonly<Record<string, ConnectionDecl>>;
/** The user's bindings (role -> connection[s]). Missing => typed error. */
readonly bindings: Bindings;
/** The invoking scope's app database (bound to the `db` root). */
readonly db: ScopeDbHandle;
/** Routes a bound method call to the real integration. */
readonly resolver: ClientResolver;
}
/**
* Validate bindings against declarations and compute the sandbox handle roots:
* `db` is always a single root; each declared role becomes a single or array
* root. Fails (typed) when a declared role has no binding, naming role +
* surface — the "missing binding is a typed error" rule.
*/
export const rootsFor = (
declared: Readonly<Record<string, ConnectionDecl>>,
bindings: Bindings,
): Effect.Effect<Readonly<Record<string, HandleRootSpec>>, BindingError> =>
Effect.gen(function* () {
const roots: Record<string, HandleRootSpec> = { db: { kind: "single" } };
for (const [role, decl] of Object.entries(declared)) {
if (decl.kind === "catalog") {
// Open-world proxy: parse + record, but execution is NotImplemented in
// this build. Bind a single root; the resolver throws if called.
roots[role] = { kind: "single" };
continue;
}
const binding = bindings[role];
if (!binding) {
return yield* new BindingError({
message: `no connection bound for role "${role}" (surface "${decl.integration}")`,
role,
surface: decl.integration,
});
}
if (decl.kind === "array") {
if (binding.kind !== "array") {
return yield* new BindingError({
message: `role "${role}" is a fan-out (connections("${decl.integration}")) and needs an array binding`,
role,
surface: decl.integration,
});
}
roots[role] = { kind: "array", count: binding.connections.length };
} else {
if (binding.kind !== "single") {
return yield* new BindingError({
message: `role "${role}" is a single connection and needs a single binding`,
role,
surface: decl.integration,
});
}
roots[role] = { kind: "single" };
}
}
return roots;
});
// Parse a fan-out root name back into (role, index): `inboxes#1` -> {inboxes,1}.
const parseRoot = (root: string): { role: string; index?: number } => {
const hash = root.indexOf("#");
if (hash === -1) return { role: root };
return { role: root.slice(0, hash), index: Number(root.slice(hash + 1)) };
};
// Root names the bridge treats as reserved: nothing the sandbox sends may
// address them as a connection role. `db` is handled explicitly first; any other
// reserved prefix (a future internal handle) is rejected rather than silently
// resolved. (Grafted strictness from A: reject unexpected/reserved calls.)
const RESERVED_ROOTS = new Set(["__proto__", "constructor", "prototype"]);
const invokeErr = (message: string): ToolSandboxError =>
new ToolSandboxError({ kind: "invoke", message });
// oxlint-disable-next-line executor/no-unknown-error-message -- boundary: `cause` is a typed value with a `message` field, not an unknown error
const taggedMessage = (cause: { readonly message: string }): string => cause.message;
/**
* Build the HandleBridge the sandbox calls out through. `db` routes to the
* scope database; a declared role routes to its bound connection through the
* `ClientResolver`. The dispatch is STRICT: an empty/malformed path, a reserved
* root, an undeclared root, or an out-of-range fan-out index is a typed error,
* never a silent success (the cloud RPC backing must be able to trust the same
* shape). A `.account` read on a client returns bound-connection metadata
* without a round-trip.
*/
export const buildBridge = (context: BindingContext): HandleBridge => ({
call: ({ root, path, args }) => {
// Strictness: every bridge call must name a root and a non-empty method path.
if (typeof root !== "string" || root.length === 0) {
return Effect.fail(invokeErr("bridge call is missing a handle root"));
}
if (!Array.isArray(path) || path.length === 0) {
return Effect.fail(invokeErr(`bridge call to "${root}" has an empty method path`));
}
if (RESERVED_ROOTS.has(root)) {
return Effect.fail(invokeErr(`reserved handle root is not callable: ${root}`));
}
if (root === "db") {
// The scope db handle exposes `sql` as a tagged template; the injected
// client calls `db.sql(templateStrings, ...values)`. When routed through
// the bridge, `path = ["sql"]` and args = [stringsArray, ...values].
if (path.length === 1 && path[0] === "sql") {
const [strings, ...values] = args as [TemplateStringsArray, ...unknown[]];
return context.db
.sql(strings, ...values)
.pipe(
Effect.mapError(
(cause) =>
new ToolSandboxError({ kind: "invoke", message: taggedMessage(cause), cause }),
),
);
}
return Effect.fail(invokeErr(`unsupported db call: ${path.join(".")}`));
}
const { role, index } = parseRoot(root);
const decl = context.declared[role];
if (!decl) {
return Effect.fail(invokeErr(`undeclared handle root: ${root}`));
}
if (decl.kind === "catalog") {
return Effect.fail(
invokeErr("catalog() open-world proxy execution is not implemented in this build"),
);
}
const binding = context.bindings[role];
// Fan-out roots MUST carry a valid index within the bound array; a single
// role MUST NOT carry an index. (Grafted bounds-checking from A.)
if (decl.kind === "array") {
if (binding?.kind !== "array") {
return Effect.fail(invokeErr(`role "${role}" is a fan-out but is not bound to an array`));
}
if (
index === undefined ||
!Number.isInteger(index) ||
index < 0 ||
index >= binding.connections.length
) {
return Effect.fail(invokeErr(`fan-out handle "${root}" addresses an out-of-range index`));
}
} else if (index !== undefined) {
return Effect.fail(invokeErr(`single-connection role "${role}" was addressed with an index`));
}
const connectionName =
binding?.kind === "array"
? binding.connections[index ?? 0]
: binding?.kind === "single"
? binding.connection
: undefined;
if (!connectionName) {
return Effect.fail(invokeErr(`no binding for role ${role}`));
}
if (path.length === 1 && path[0] === "account") {
// Clients read `.account.email` / `.account.login`; expose both keys.
return Effect.succeed({ email: connectionName, login: connectionName, name: connectionName });
}
return context.resolver
.call({ integration: decl.integration, connection: connectionName, path, args })
.pipe(
Effect.mapError(
(cause) => new ToolSandboxError({ kind: "invoke", message: taggedMessage(cause), cause }),
),
);
},
});