Compare commits

...
2 changed files with 55 additions and 2 deletions
+6 -2
View File
@@ -273,9 +273,13 @@ const layer = Layer.effect(
}),
)
// A tool is hidden from the model only when no resource could get past `deny`. Each rule's resource
// pattern matches itself as a literal, so the patterns for this action are a complete set of probes:
// a later broader rule overrides a probe exactly when it also covers every resource the probe covers.
// The extra "*" probe stands for resources no narrow rule covers, which fall back to the default `ask`.
const whollyDisabled = (action: string, rules: Permission.Ruleset) => {
const rule = rules.findLast((rule) => Wildcard.match(action, rule.action))
return rule?.resource === "*" && rule.effect === "deny"
const probes = rules.filter((rule) => Wildcard.match(action, rule.action)).map((rule) => rule.resource)
return [...probes, "*"].every((resource) => Permission.evaluate(action, resource, rules).effect === "deny")
}
const formatSchemaIssue = SchemaIssue.makeFormatterDefault()
+49
View File
@@ -706,6 +706,55 @@ describe("Tool", () => {
}),
)
it.effect("hides tools whose narrower trailing rules cannot get past deny", () =>
Effect.gen(function* () {
const service = yield* Tool.Service
yield* transform(service, { bash: make() }, { codemode: false })
const names = (permissions: Permission.Ruleset) =>
toolDefinitions(service, permissions).pipe(Effect.map((definitions) => definitions.map((tool) => tool.name)))
// trailing narrow deny rules leave every call denied
expect(
yield* names([
{ action: "*", resource: "*", effect: "deny" },
{ action: "bash", resource: "git *", effect: "deny" },
]),
).toEqual([])
// without a catch-all, uncovered resources fall back to ask
expect(yield* names([{ action: "bash", resource: "rm*", effect: "deny" }])).toEqual(["bash", "execute"])
// a narrow ask superseded by the same narrow deny admits nothing
expect(
yield* names([
{ action: "*", resource: "*", effect: "deny" },
{ action: "bash", resource: "rm*", effect: "ask" },
{ action: "bash", resource: "rm*", effect: "deny" },
]),
).toEqual([])
// a trailing narrow ask or allow still admits some calls
expect(
yield* names([
{ action: "*", resource: "*", effect: "deny" },
{ action: "bash", resource: "rm*", effect: "ask" },
]),
).toEqual(["bash"])
// a narrow allow that a later broader deny supersedes admits nothing
expect(
yield* names([
{ action: "bash", resource: "git *", effect: "allow" },
{ action: "bash", resource: "*", effect: "deny" },
]),
).toEqual(["execute"])
// a later narrower deny does not swallow the broader allow before it
expect(
yield* names([
{ action: "bash", resource: "*", effect: "deny" },
{ action: "bash", resource: "git *", effect: "allow" },
{ action: "bash", resource: "git push*", effect: "deny" },
]),
).toEqual(["bash", "execute"])
}),
)
it.effect("keeps permission options isolated between registrations", () =>
Effect.gen(function* () {
const service = yield* Tool.Service