Compare commits

...

2 Commits

Author SHA1 Message Date
neriousy d372c72ccd refactor(core): scope skill bypass to tool context 2026-08-21 13:44:17 +00:00
neriousy 9a3768522f fix(core): allow explicitly referenced skills 2026-08-21 13:30:17 +00:00
5 changed files with 30 additions and 8 deletions
+4
View File
@@ -369,6 +369,9 @@ const layer = Layer.effect(
toolChoice: stepLimitReached ? "none" : undefined,
webSocket: "session",
})
const userRequests = loaded.messages
.findLast((message) => message.type === "user")
?.skills?.map((skill) => ({ action: "skill", resource: skill.id }))
yield* diagnosePromptCache(session.id, prepared.request)
const executeTool = (input: Parameters<typeof prepared.executeTool>[0]) => {
if (stepLimitReached) return new Tool.Error({ message: "Tools are disabled after the maximum agent steps" })
@@ -457,6 +460,7 @@ const layer = Layer.effect(
agent: agent.id,
messageID: assistantMessageID,
call: event,
userRequests,
// Progress is ephemeral, not durable history: nothing to order.
progress: (update) => publisher.progress(event.id, update),
}),
+3
View File
@@ -37,6 +37,7 @@ export interface Snapshot {
readonly agent: Agent.ID
readonly messageID: SessionMessage.ID
readonly call: ToolCall
readonly userRequests?: Tool.Context["userRequests"]
readonly progress?: (update: Tool.Metadata) => Effect.Effect<void>
}) => Effect.Effect<Tool.Result & { readonly content: ReadonlyArray<Tool.Content> }, Tool.Error>
}
@@ -234,6 +235,7 @@ const layer = Layer.effect(
readonly agent: Agent.ID
readonly messageID: SessionMessage.ID
readonly call: ToolCall
readonly userRequests?: Tool.Context["userRequests"]
readonly progress?: (update: Tool.Metadata) => Effect.Effect<void>
}) => {
const context: Tool.Context = {
@@ -241,6 +243,7 @@ const layer = Layer.effect(
agent: input.agent,
messageID: input.messageID,
id: Tool.CallID.make(input.call.id),
...(input.userRequests?.length ? { userRequests: input.userRequests } : {}),
progress: input.progress ?? (() => Effect.void),
}
if (input.call.name === "execute" && codemodeTool)
+9 -8
View File
@@ -51,14 +51,15 @@ export const Plugin = {
const skill = current.find((skill) => skill.id === input.id)
if (!skill) return yield* unableToLoad(input.id)
return yield* Effect.gen(function* () {
yield* permission.assert({
action: name,
resources: [skill.id],
save: [skill.id],
sessionID: context.sessionID,
agent: context.agent,
source: { type: "tool", messageID: context.messageID, id: context.id },
})
if (!context.userRequests?.some((request) => request.action === name && request.resource === skill.id))
yield* permission.assert({
action: name,
resources: [skill.id],
save: [skill.id],
sessionID: context.sessionID,
agent: context.agent,
source: { type: "tool", messageID: context.messageID, id: context.id },
})
const directory = path.dirname(skill.location)
const files =
path.basename(skill.location) === "SKILL.md"
+13
View File
@@ -131,12 +131,25 @@ describe("SkillTool", () => {
yield* executeTool(registry, {
sessionID,
...toolIdentity,
userRequests: [{ action: "skill", resource: "other" }],
call: { type: "tool-call", id: "call-denied-skill", name: "skill", input: { id: "effect" } },
}),
).toEqual({
status: "error",
error: { type: "permission.rejected", message: "Permission denied: skill" },
})
expect(
yield* executeTool(registry, {
sessionID,
...toolIdentity,
userRequests: [{ action: "skill", resource: "effect" }],
call: { type: "tool-call", id: "call-user-skill", name: "skill", input: { id: "effect" } },
}),
).toMatchObject({
status: "completed",
content: [{ type: "text", text: Skill.toModelOutput(info, [reference]) }],
})
expect(assertions).toHaveLength(3)
deny = false
const flat = Skill.Info.make({
id: Skill.ID.make("public"),
+1
View File
@@ -16,6 +16,7 @@ export interface Context {
readonly agent: Agent.ID
readonly messageID: SessionMessage.ID
readonly id: CallID
readonly userRequests?: ReadonlyArray<{ readonly action: string; readonly resource: string }>
readonly progress: (update: Metadata) => Effect.Effect<void>
}