feat(coding-agent): add extension project trust decisions

This commit is contained in:
Mario Zechner
2026-06-08 12:58:49 +02:00
parent dce3e28516
commit 718215bd95
18 changed files with 631 additions and 86 deletions
@@ -98,6 +98,10 @@ export type {
MessageUpdateEvent,
ModelSelectEvent,
ModelSelectSource,
ProjectTrustContext,
ProjectTrustEvent,
ProjectTrustEventResult,
ProjectTrustHandler,
// Provider Registration
ProviderConfig,
ProviderModelConfig,
@@ -410,15 +410,20 @@ export async function loadExtensionFromFactory(
/**
* Load extensions from paths.
*/
export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {
export async function loadExtensions(
paths: string[],
cwd: string,
eventBus?: EventBus,
runtime?: ExtensionRuntime,
): Promise<LoadExtensionsResult> {
const extensions: Extension[] = [];
const errors: Array<{ path: string; error: string }> = [];
const resolvedCwd = resolvePath(cwd);
const resolvedEventBus = eventBus ?? createEventBus();
const runtime = createExtensionRuntime();
const resolvedRuntime = runtime ?? createExtensionRuntime();
for (const extPath of paths) {
const { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, runtime);
const { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, resolvedRuntime);
if (error) {
errors.push({ path: extPath, error });
@@ -433,7 +438,7 @@ export async function loadExtensions(paths: string[], cwd: string, eventBus?: Ev
return {
extensions,
errors,
runtime,
runtime: resolvedRuntime,
};
}
@@ -35,9 +35,13 @@ import type {
InputEvent,
InputEventResult,
InputSource,
LoadExtensionsResult,
MessageEndEvent,
MessageEndEventResult,
MessageRenderer,
ProjectTrustContext,
ProjectTrustEvent,
ProjectTrustEventResult,
ProviderConfig,
RegisteredCommand,
RegisteredTool,
@@ -116,6 +120,7 @@ interface BeforeAgentStartCombinedResult {
type RunnerEmitEvent = Exclude<
ExtensionEvent,
| ToolCallEvent
| ProjectTrustEvent
| ToolResultEvent
| UserBashEvent
| ContextEvent
@@ -189,6 +194,35 @@ export async function emitSessionShutdownEvent(
return false;
}
export async function emitProjectTrustEvent(
extensionsResult: LoadExtensionsResult,
event: ProjectTrustEvent,
ctx: ProjectTrustContext,
): Promise<{ result?: ProjectTrustEventResult; errors: ExtensionError[] }> {
const errors: ExtensionError[] = [];
for (const ext of extensionsResult.extensions) {
// A single extension may register multiple handlers for the same event.
// The first project_trust handler that returns a decision wins.
const handlers = ext.handlers.get("project_trust");
if (!handlers || handlers.length === 0) continue;
for (const handler of handlers) {
try {
const handlerResult = (await handler(event, ctx)) as ProjectTrustEventResult;
return { result: handlerResult, errors };
} catch (error) {
errors.push({
extensionPath: ext.path,
event: event.type,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
}
}
}
return { errors };
}
const noOpUIContext: ExtensionUIContext = {
select: async () => undefined,
confirm: async () => false,
@@ -495,9 +495,31 @@ export function defineTool<TParams extends TSchema, TDetails = unknown, TState =
}
// ============================================================================
// Resource Events
// Startup/Resource Events
// ============================================================================
export interface ProjectTrustEvent {
type: "project_trust";
cwd: string;
}
export interface ProjectTrustEventResult {
trusted: boolean;
remember?: boolean;
}
export interface ProjectTrustContext {
cwd: string;
mode: ExtensionMode;
hasUI: boolean;
ui: Pick<ExtensionUIContext, "select" | "confirm" | "input" | "notify">;
}
export type ProjectTrustHandler = (
event: ProjectTrustEvent,
ctx: ProjectTrustContext,
) => Promise<ProjectTrustEventResult> | ProjectTrustEventResult;
/** Fired after session_start to allow extensions to provide additional resource paths. */
export interface ResourcesDiscoverEvent {
type: "resources_discover";
@@ -957,6 +979,7 @@ export function isToolCallEventType(toolName: string, event: ToolCallEvent): boo
/** Union of all event types */
export type ExtensionEvent =
| ProjectTrustEvent
| ResourcesDiscoverEvent
| SessionEvent
| ContextEvent
@@ -1095,6 +1118,7 @@ export interface ExtensionAPI {
// Event Subscription
// =========================================================================
on(event: "project_trust", handler: ProjectTrustHandler): void;
on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
on(