-
Notifications
You must be signed in to change notification settings - Fork 763
added session and always permissions #389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,33 @@ const DEFAULT_ALLOW_LIST = [ | |
| let cachedAllowList: string[] | null = null; | ||
| let cachedMtimeMs: number | null = null; | ||
|
|
||
| /** In-memory session allowlist — resets on app restart */ | ||
| const sessionAllowSet = new Set<string>(); | ||
|
|
||
| export function addToSessionAllowList(commands: string[]): void { | ||
|
||
| for (const cmd of commands) { | ||
| const normalized = cmd.trim().toLowerCase(); | ||
| if (normalized) sessionAllowSet.add(normalized); | ||
| } | ||
| } | ||
|
|
||
| export async function addToSecurityConfig(commands: string[]): Promise<void> { | ||
| ensureSecurityConfigSync(); | ||
| const current = readAllowList(); | ||
| const merged = new Set(current); | ||
| for (const cmd of commands) { | ||
| const normalized = cmd.trim().toLowerCase(); | ||
| if (normalized) merged.add(normalized); | ||
| } | ||
| await fsPromises.writeFile( | ||
| SECURITY_CONFIG_PATH, | ||
| JSON.stringify(Array.from(merged).sort(), null, 2) + "\n", | ||
| "utf8", | ||
| ); | ||
| // Reset cache so next read picks up the new file | ||
| resetSecurityAllowListCache(); | ||
| } | ||
|
|
||
| /** | ||
| * Async function to ensure security config file exists. | ||
| * Called explicitly at app startup via initConfigs(). | ||
|
|
@@ -99,18 +126,28 @@ export function getSecurityAllowList(): string[] { | |
| ensureSecurityConfigSync(); | ||
| try { | ||
| const stats = fs.statSync(SECURITY_CONFIG_PATH); | ||
| let fileList: string[]; | ||
|
||
| if (cachedAllowList && cachedMtimeMs === stats.mtimeMs) { | ||
| return cachedAllowList; | ||
| fileList = cachedAllowList; | ||
| } else { | ||
| fileList = readAllowList(); | ||
| cachedAllowList = fileList; | ||
| cachedMtimeMs = stats.mtimeMs; | ||
| } | ||
|
|
||
| const allowList = readAllowList(); | ||
| cachedAllowList = allowList; | ||
| cachedMtimeMs = stats.mtimeMs; | ||
| return allowList; | ||
| // Merge session allowlist | ||
| if (sessionAllowSet.size === 0) return fileList; | ||
| const merged = new Set(fileList); | ||
| for (const cmd of sessionAllowSet) merged.add(cmd); | ||
| return Array.from(merged); | ||
| } catch { | ||
| cachedAllowList = null; | ||
| cachedMtimeMs = null; | ||
| return readAllowList(); | ||
| const fileList = readAllowList(); | ||
| if (sessionAllowSet.size === 0) return fileList; | ||
| const merged = new Set(fileList); | ||
| for (const cmd of sessionAllowSet) merged.add(cmd); | ||
| return Array.from(merged); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,8 @@ import { IBus } from "../application/lib/bus.js"; | |
| import { IAbortRegistry } from "./abort-registry.js"; | ||
| import { IRunsLock } from "./lock.js"; | ||
| import { forceCloseAllMcpClients } from "../mcp/mcp.js"; | ||
| import { extractCommandNames } from "../application/lib/command-executor.js"; | ||
| import { addToSessionAllowList, addToSecurityConfig } from "../config/security.js"; | ||
|
|
||
| export async function createRun(opts: z.infer<typeof CreateRunOptions>): Promise<z.infer<typeof Run>> { | ||
| const repo = container.resolve<IRunsRepo>('runsRepo'); | ||
|
|
@@ -26,9 +28,23 @@ export async function createMessage(runId: string, message: string): Promise<str | |
| } | ||
|
|
||
| export async function authorizePermission(runId: string, ev: z.infer<typeof ToolPermissionAuthorizePayload>): Promise<void> { | ||
| const { scope, command, ...rest } = ev; | ||
|
|
||
| // Handle scope side-effects when approving | ||
| if (rest.response === "approve" && command && scope && scope !== "once") { | ||
| const commandNames = extractCommandNames(command); | ||
|
||
| if (commandNames.length > 0) { | ||
| if (scope === "session") { | ||
| addToSessionAllowList(commandNames); | ||
| } else if (scope === "always") { | ||
| await addToSecurityConfig(commandNames); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const repo = container.resolve<IRunsRepo>('runsRepo'); | ||
| const event: z.infer<typeof ToolPermissionResponseEvent> = { | ||
| ...ev, | ||
| ...rest, | ||
| runId, | ||
| type: "tool-permission-response", | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,6 +106,9 @@ export const ToolPermissionAuthorizePayload = ToolPermissionResponseEvent.pick({ | |
| subflow: true, | ||
| toolCallId: true, | ||
| response: true, | ||
| }).extend({ | ||
|
||
| scope: z.enum(["once", "session", "always"]).optional(), | ||
| command: z.string().optional(), | ||
| }); | ||
|
|
||
| export const AskHumanResponsePayload = AskHumanResponseEvent.pick({ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this approach following any documented practices for parsing commands? If so, can we include a reference to that? If not, can we use a proven parsing-based solution here?