Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 53 additions & 40 deletions packages/core/sdk/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import {
import { isFirstPartyOAuthClientSlug, type OAuthService } from "./oauth-client";
import type { FirstPartyOAuthClientConfig } from "./oauth-client";
import {
combineEffectivePolicies,
comparePolicyRow,
isValidPattern,
matchPattern,
Expand Down Expand Up @@ -4801,13 +4802,15 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
readonly kind: "provider";
readonly provider: ToolPolicyProvider;
readonly rules: readonly ToolPolicyProviderRule[] | null;
readonly globalRows: readonly ToolPolicyRow[];
}
| {
readonly kind: "prepared";
readonly resolve: (input: {
readonly toolId: string;
readonly defaultRequiresApproval?: boolean;
}) => EffectivePolicy;
readonly globalRows: readonly ToolPolicyRow[];
};

const compareProviderPolicyRule = (
Expand Down Expand Up @@ -4842,50 +4845,60 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
};

const listActivePolicyRuleSet = (): Effect.Effect<ActivePolicyRuleSet, StorageFailure> =>
activeToolPolicyProvider
? // Batched per-operation resolver: fetch all policy + connection state
// once, then resolve every tool in this operation against that
// snapshot. Avoids the per-tool resolve N+1 on the list surface.
activeToolPolicyProvider.prepare
? activeToolPolicyProvider
.prepare()
.pipe(Effect.map((resolve) => ({ kind: "prepared" as const, resolve })))
: activeToolPolicyProvider.resolve
? Effect.succeed({
kind: "provider" as const,
provider: activeToolPolicyProvider,
rules: null,
})
: activeToolPolicyProvider.list().pipe(
Effect.map((rules) => ({
kind: "provider" as const,
provider: activeToolPolicyProvider!,
rules,
})),
)
: core
.findMany("tool_policy", {})
.pipe(Effect.map((rows) => ({ kind: "global" as const, rows })));
Effect.gen(function* () {
const globalRows = yield* core.findMany("tool_policy", {});
if (!activeToolPolicyProvider) {
return { kind: "global" as const, rows: globalRows };
}
if (activeToolPolicyProvider.prepare) {
const resolve = yield* activeToolPolicyProvider.prepare();
return { kind: "prepared" as const, resolve, globalRows };
}
if (activeToolPolicyProvider.resolve) {
return {
kind: "provider" as const,
provider: activeToolPolicyProvider,
rules: null,
globalRows,
};
}
const rules = yield* activeToolPolicyProvider.list();
return {
kind: "provider" as const,
provider: activeToolPolicyProvider,
rules,
globalRows,
};
});

const resolvePolicyFromRuleSet = (
toolId: string,
ruleSet: ActivePolicyRuleSet,
defaultRequiresApproval?: boolean,
): Effect.Effect<EffectivePolicy, StorageFailure> =>
ruleSet.kind === "prepared"
? Effect.succeed(ruleSet.resolve({ toolId, defaultRequiresApproval }))
: ruleSet.kind === "provider"
? ruleSet.provider.resolve
? ruleSet.provider.resolve({ toolId, defaultRequiresApproval })
: Effect.succeed(resolveProviderPolicyFromRules(toolId, ruleSet.rules ?? []))
: Effect.succeed(
resolveEffectivePolicy(
toolId,
ruleSet.rows,
ownerRankForRow,
defaultRequiresApproval,
),
);
Effect.gen(function* () {
if (ruleSet.kind === "global") {
return resolveEffectivePolicy(
toolId,
ruleSet.rows,
ownerRankForRow,
defaultRequiresApproval,
);
}
const globalPolicy = resolveEffectivePolicy(
toolId,
ruleSet.globalRows,
ownerRankForRow,
defaultRequiresApproval,
);
const providerPolicy =
ruleSet.kind === "prepared"
? ruleSet.resolve({ toolId, defaultRequiresApproval })
: ruleSet.provider.resolve
? yield* ruleSet.provider.resolve({ toolId, defaultRequiresApproval })
: resolveProviderPolicyFromRules(toolId, ruleSet.rules ?? []);
return combineEffectivePolicies(providerPolicy, globalPolicy);
});

// ------------------------------------------------------------------
// Tools (read surface)
Expand Down Expand Up @@ -5437,7 +5450,6 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
): Effect.Effect<EffectivePolicy, StorageFailure> =>
Effect.gen(function* () {
const parsed = parseToolAddress(String(address));
const policyRows = yield* core.findMany("tool_policy", {});
const toolId = parsed
? `${parsed.integration}.${parsed.owner}.${parsed.connection}.${parsed.tool}`
: String(address);
Expand All @@ -5458,7 +5470,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
requiresApproval = annotations?.requiresApproval;
}
}
return resolveEffectivePolicy(toolId, policyRows, ownerRankForRow, requiresApproval);
const policyRules = yield* listActivePolicyRuleSet();
return yield* resolvePolicyFromRuleSet(toolId, policyRules, requiresApproval);
});

// ------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions packages/core/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ export {
matchPattern,
isValidPattern,
effectivePolicyFromSorted,
combineEffectivePolicies,
moreRestrictivePolicy,
ToolPolicyActionSchema,
type ToolPolicy,
type CreateToolPolicyInput,
Expand Down
124 changes: 124 additions & 0 deletions packages/core/sdk/src/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,130 @@ describe("active tool-policy provider", () => {
expect(Predicate.isTagged("ToolBlockedError")(blocked.failure)).toBe(true);
}),
);

it.effect("enforces workspace require_approval policy over a provider approve rule", () =>
Effect.gen(function* () {
const executor = yield* makeTestExecutor({
plugins: [staticPlugin, policyProviderPlugin] as const,
});

yield* executor.policies.create({
owner: "org",
pattern: "toolkit-fixture.ctl.allowed",
action: "require_approval",
});

const calls = { count: 0 };
const allowed = yield* executor.execute(
ToolAddress.make("toolkit-fixture.ctl.allowed"),
{},
{ onElicitation: recordingHandler(calls) },
);
expect(allowed).toBe("allowed");
expect(calls.count).toBe(1);
}),
);

it.effect("enforces workspace block policy over a provider approve rule", () =>
Effect.gen(function* () {
const executor = yield* makeTestExecutor({
plugins: [staticPlugin, policyProviderPlugin] as const,
});

yield* executor.policies.create({
owner: "org",
pattern: "toolkit-fixture.ctl.allowed",
action: "block",
});

const tools = yield* executor.tools.list();
expect(tools).toHaveLength(0);

const blocked = yield* Effect.result(
executor.execute(ToolAddress.make("toolkit-fixture.ctl.allowed"), {}),
);
expect(Result.isFailure(blocked)).toBe(true);
if (!Result.isFailure(blocked)) return;
expect(Predicate.isTagged("ToolBlockedError")(blocked.failure)).toBe(true);
}),
);

it.effect("enforces provider require_approval rule when workspace has no policy", () =>
Effect.gen(function* () {
const approvalProviderPlugin = definePlugin(() => ({
id: "approval-provider" as const,
storage: () => ({}),
toolPolicyProvider: () => ({
list: () =>
Effect.succeed([
{
id: "require-approval-static",
pattern: "toolkit-fixture.ctl.allowed",
action: "require_approval" as const,
position: "a0",
},
]),
}),
}))();

const executor = yield* makeTestExecutor({
plugins: [staticPlugin, approvalProviderPlugin] as const,
});

const calls = { count: 0 };
const allowed = yield* executor.execute(
ToolAddress.make("toolkit-fixture.ctl.allowed"),
{},
{ onElicitation: recordingHandler(calls) },
);
expect(allowed).toBe("allowed");
expect(calls.count).toBe(1);
}),
);

it.effect("combines prepared provider resolver with workspace policies", () =>
Effect.gen(function* () {
const preparedProviderPlugin = definePlugin(() => ({
id: "prepared-provider" as const,
storage: () => ({}),
toolPolicyProvider: () => ({
list: () => Effect.succeed([]),
prepare: () =>
Effect.succeed((input) =>
input.toolId === "toolkit-fixture.ctl.allowed"
? { action: "approve", source: "user", pattern: "toolkit-fixture.ctl.allowed" }
: { action: "block", source: "user", pattern: "*" },
),
}),
}))();

const executor = yield* makeTestExecutor({
plugins: [staticPlugin, preparedProviderPlugin] as const,
});

yield* executor.policies.create({
owner: "org",
pattern: "toolkit-fixture.ctl.allowed",
action: "require_approval",
});

const calls = { count: 0 };
const allowed = yield* executor.execute(
ToolAddress.make("toolkit-fixture.ctl.allowed"),
{},
{ onElicitation: recordingHandler(calls) },
);
expect(allowed).toBe("allowed");
expect(calls.count).toBe(1);

const blocked = yield* Effect.result(
executor.execute(ToolAddress.make("toolkit-fixture.ctl.hidden"), {}),
);
expect(Result.isFailure(blocked)).toBe(true);
if (!Result.isFailure(blocked)) return;
expect(Predicate.isTagged("ToolBlockedError")(blocked.failure)).toBe(true);
}),
);
});

describe("approve / require_approval interaction with annotations", () => {
Expand Down
33 changes: 32 additions & 1 deletion packages/core/sdk/src/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ const actionRestrictionRank = (action: ToolPolicyAction): number =>
Match.exhaustive,
);

const moreRestrictive = <T extends { readonly action: ToolPolicyAction }>(
export const moreRestrictive = <T extends { readonly action: ToolPolicyAction }>(
current: T | undefined,
candidate: T,
): T => {
Expand All @@ -198,6 +198,37 @@ const moreRestrictive = <T extends { readonly action: ToolPolicyAction }>(
return candidateRank > currentRank ? candidate : current;
};

export const moreRestrictivePolicy = moreRestrictive;

/**
* Combine policy resolution from a scoped tool policy provider (e.g. a toolkit)
* with the ambient workspace/global policy set under the principle of least privilege:
* - If either policy explicitly blocks (or the tool is outside the toolkit connection boundary), it is blocked.
* - If both have explicit user/org rules, the most restrictive user rule wins.
* - If only one has an explicit user/org rule, that user rule takes precedence over plugin default.
* - If neither has an explicit user/org rule, fall back to the most restrictive plugin default.
*/
export const combineEffectivePolicies = (
providerPolicy: EffectivePolicy,
globalPolicy: EffectivePolicy,
): EffectivePolicy => {
if (providerPolicy.action === "block") return providerPolicy;
if (globalPolicy.action === "block") return globalPolicy;

if (providerPolicy.source === "user" && globalPolicy.source === "user") {
return moreRestrictive(providerPolicy, globalPolicy);
}

if (globalPolicy.source === "user") {
return globalPolicy;
}
if (providerPolicy.source === "user") {
return providerPolicy;
}

return moreRestrictive(providerPolicy, globalPolicy);
};

export const resolveToolPolicy = (
toolId: string,
policies: readonly ToolPolicyRow[],
Expand Down
Loading
Loading