{"version":3,"file":"permission-arbiter.d.ts","sourceRoot":"","sources":["../../../src/watchdog/permission-arbiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAE/E,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAiBhF,MAAM,WAAW,wBAAwB;IACxC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACzC,GAAG,EAAE,gBAAgB,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,gCAAgC;IAChD,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACpB;AAOD,wBAAgB,+BAA+B,CAAC,OAAO,GAAE,gCAAqC,6EAsI7F;AAED,eAAO,MAAM,yBAAyB,2EAAoC,CAAC","sourcesContent":["import { Agent, type AgentTool, type StreamFn } from \"@lpb-work/pi-agent-core\";\nimport { streamSimple } from \"@lpb-work/pi-ai/compat\";\nimport { convertToLlm, type ExtensionContext } from \"@lpb-work/pi-coding-agent\";\nimport { type Static, Type } from \"typebox\";\nimport { appendPermissionAudit, permissionArgsPreview } from \"../runs/shared/permissions.ts\";\nimport { decodeChildWatchdogConfig } from \"./child-status.ts\";\nimport { childResolvedConfig } from \"./register-child.ts\";\nimport { resolveWatchdogReviewModel } from \"./review.ts\";\n\nconst PermissionDecisionParams = Type.Object(\n\t{\n\t\tdecision: Type.String({ enum: [\"approve\", \"deny\"] }),\n\t\treason: Type.String({ description: \"One concise reason for this exact decision.\" }),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype PermissionDecisionParams = Static<typeof PermissionDecisionParams>;\n\nexport interface WatchdogPermissionResult {\n\tapproved: boolean;\n\treason: string;\n\tsource: \"watchdog\";\n}\n\nexport interface WatchdogPermissionRequest {\n\tctx: ExtensionContext;\n\ttoolName: string;\n\targs: unknown;\n\trawWatchdogConfig?: string;\n\tauditPath?: string;\n\tsignal?: AbortSignal;\n}\n\nexport interface WatchdogPermissionArbiterOptions {\n\tstreamFn?: StreamFn;\n}\n\nfunction conciseReason(value: string): string {\n\tconst trimmed = value.trim();\n\treturn trimmed ? trimmed.slice(0, 500) : \"Watchdog returned an empty reason.\";\n}\n\nexport function createWatchdogPermissionArbiter(options: WatchdogPermissionArbiterOptions = {}) {\n\treturn async (request: WatchdogPermissionRequest): Promise<WatchdogPermissionResult> => {\n\t\tconst preview = permissionArgsPreview(request.args);\n\t\tconst createdAt = Date.now();\n\t\tconst auditBase = {\n\t\t\ttype: \"permission.request\",\n\t\t\tcreatedAt,\n\t\t\ttoolName: request.toolName,\n\t\t\tpreview,\n\t\t\tmatchedRule: \"ask\",\n\t\t\tdecisionSource: \"watchdog\",\n\t\t};\n\t\tappendPermissionAudit(request.auditPath, auditBase);\n\t\tconst finish = (approved: boolean, reason: string, decision: string): WatchdogPermissionResult => {\n\t\t\tappendPermissionAudit(request.auditPath, {\n\t\t\t\ttype: \"permission.decision\",\n\t\t\t\tcreatedAt: Date.now(),\n\t\t\t\trequestCreatedAt: createdAt,\n\t\t\t\ttoolName: request.toolName,\n\t\t\t\tdecision,\n\t\t\t\tapproved,\n\t\t\t\tdecisionSource: \"watchdog\",\n\t\t\t\treason: conciseReason(reason),\n\t\t\t});\n\t\t\treturn { approved, reason: conciseReason(reason), source: \"watchdog\" };\n\t\t};\n\n\t\tlet childConfig;\n\t\ttry {\n\t\t\tchildConfig = decodeChildWatchdogConfig(request.rawWatchdogConfig);\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\t\treturn finish(false, `Watchdog permission arbiter configuration is invalid: ${reason}`, \"unavailable\");\n\t\t}\n\t\tif (!childConfig)\n\t\t\treturn finish(\n\t\t\t\tfalse,\n\t\t\t\t\"Watchdog permission arbiter is unavailable because the child watchdog is disabled.\",\n\t\t\t\t\"unavailable\",\n\t\t\t);\n\t\tif (request.signal?.aborted || request.ctx.signal?.aborted)\n\t\t\treturn finish(false, \"Watchdog permission decision was cancelled.\", \"cancelled\");\n\n\t\tlet decision: PermissionDecisionParams | undefined;\n\t\tconst tool: AgentTool<typeof PermissionDecisionParams, { recorded: boolean }> = {\n\t\t\tname: \"watchdog_permission_decision\",\n\t\t\tlabel: \"Watchdog permission decision\",\n\t\t\tdescription: \"Approve or deny this exact child tool call. Call exactly once.\",\n\t\t\tparameters: PermissionDecisionParams,\n\t\t\texecutionMode: \"sequential\",\n\t\t\tasync execute(_toolCallId, params) {\n\t\t\t\tif (!decision) decision = params;\n\t\t\t\treturn { content: [{ type: \"text\", text: \"Permission decision recorded.\" }], details: { recorded: true } };\n\t\t\t},\n\t\t};\n\n\t\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\t\tlet agent: Agent | undefined;\n\t\ttry {\n\t\t\tconst config = childResolvedConfig(childConfig);\n\t\t\tconst selection = await resolveWatchdogReviewModel(request.ctx, config);\n\t\t\tconst auth = selection.auth;\n\t\t\tconst registeredProvider = (\n\t\t\t\trequest.ctx.modelRegistry as {\n\t\t\t\t\tgetRegisteredProviderConfig?: (\n\t\t\t\t\t\tprovider: string,\n\t\t\t\t\t) => { api?: string; streamSimple?: StreamFn } | undefined;\n\t\t\t\t}\n\t\t\t).getRegisteredProviderConfig?.(selection.model.provider);\n\t\t\tconst baseStreamFn =\n\t\t\t\toptions.streamFn ??\n\t\t\t\t(registeredProvider?.streamSimple && registeredProvider.api === selection.model.api\n\t\t\t\t\t? registeredProvider.streamSimple\n\t\t\t\t\t: streamSimple);\n\t\t\tconst streamFn: StreamFn = (model, context, streamOptions) =>\n\t\t\t\tbaseStreamFn(model, context, {\n\t\t\t\t\t...streamOptions,\n\t\t\t\t\t...(auth.apiKey ? { apiKey: auth.apiKey } : {}),\n\t\t\t\t\tenv: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,\n\t\t\t\t\theaders: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },\n\t\t\t\t});\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt: [\n\t\t\t\t\t\t\"You are the pi-subagents watchdog permission arbiter.\",\n\t\t\t\t\t\t\"Decide only whether this exact non-bash child tool call should proceed.\",\n\t\t\t\t\t\t\"Call watchdog_permission_decision exactly once with approve or deny and a concise reason.\",\n\t\t\t\t\t\t\"Deny when uncertain. Do not produce freeform advice or ask the parent orchestrator.\",\n\t\t\t\t\t].join(\"\\n\"),\n\t\t\t\t\tmodel: selection.model,\n\t\t\t\t\tthinkingLevel: selection.thinkingLevel,\n\t\t\t\t\ttools: [tool],\n\t\t\t\t},\n\t\t\t\tconvertToLlm,\n\t\t\t\tstreamFn,\n\t\t\t\tgetApiKey: (providerName) => (providerName === selection.model.provider ? auth.apiKey : undefined),\n\t\t\t\tbeforeToolCall: async ({ toolCall }) =>\n\t\t\t\t\ttoolCall.name === tool.name\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: { block: true, reason: `Permission arbiter tool '${toolCall.name}' is not allowed.` },\n\t\t\t\ttoolExecution: \"sequential\",\n\t\t\t});\n\t\t\tconst abort = () => agent?.abort();\n\t\t\trequest.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\t\trequest.ctx.signal?.addEventListener(\"abort\", abort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst prompt = `Tool: ${request.toolName}\\nRedacted arguments: ${preview}`;\n\t\t\t\tawait Promise.race([\n\t\t\t\t\tagent.prompt(prompt),\n\t\t\t\t\tnew Promise<never>((_, reject) => {\n\t\t\t\t\t\ttimeout = setTimeout(() => {\n\t\t\t\t\t\t\tagent?.abort();\n\t\t\t\t\t\t\treject(new Error(\"Watchdog permission decision timed out.\"));\n\t\t\t\t\t\t}, childConfig.agentEndTimeoutMs);\n\t\t\t\t\t}),\n\t\t\t\t]);\n\t\t\t} finally {\n\t\t\t\trequest.signal?.removeEventListener(\"abort\", abort);\n\t\t\t\trequest.ctx.signal?.removeEventListener(\"abort\", abort);\n\t\t\t}\n\t\t\tif (!decision) return finish(false, \"Watchdog permission arbiter returned no decision.\", \"malformed\");\n\t\t\tconst approved = decision.decision === \"approve\";\n\t\t\treturn finish(approved, decision.reason, decision.decision);\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\t\treturn finish(\n\t\t\t\tfalse,\n\t\t\t\t`Watchdog permission arbiter failed closed: ${reason}`,\n\t\t\t\treason.includes(\"timed out\") ? \"timeout\" : \"error\",\n\t\t\t);\n\t\t} finally {\n\t\t\tif (timeout) clearTimeout(timeout);\n\t\t}\n\t};\n}\n\nexport const requestWatchdogPermission = createWatchdogPermissionArbiter();\n"]}