feat(coding-agent): expose project trust to extensions

closes #5523
This commit is contained in:
Mario Zechner 2026-06-09 13:02:58 +02:00
parent 081a0a2bef
commit db3f9953ee
8 changed files with 35 additions and 0 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added `ctx.isProjectTrusted()` for extensions to observe the effective project trust decision, including temporary trust decisions ([#5523](https://github.com/earendil-works/pi/issues/5523)).
### Fixed
- Fixed GitHub release notes and interactive changelog links to resolve package-relative documentation URLs correctly ([#5516](https://github.com/earendil-works/pi/issues/5516)).

View file

@ -892,6 +892,12 @@ Current run mode: `"tui"`, `"rpc"`, `"json"`, or `"print"`. Use `ctx.mode === "t
Current working directory.
### ctx.isProjectTrusted()
Returns whether project-local trust is active for the current session context. This includes temporary trust decisions and CLI trust overrides, not just saved decisions in the global trust store.
Use this before reading project-local extension configuration that should only be honored for trusted projects.
### ctx.sessionManager
Read-only access to session state. See [Session Format](session-format.md) for the full SessionManager API and entry types.

View file

@ -2239,6 +2239,7 @@ export class AgentSession {
{
getModel: () => this.model,
isIdle: () => !this.isStreaming,
isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
getSignal: () => this.agent.signal,
abort: () => {
if (this._extensionAbortHandler) {

View file

@ -270,6 +270,7 @@ export class ExtensionRunner {
private errorListeners: Set<ExtensionErrorListener> = new Set();
private getModel: () => Model<any> | undefined = () => undefined;
private isIdleFn: () => boolean = () => true;
private isProjectTrustedFn: () => boolean = () => true;
private getSignalFn: () => AbortSignal | undefined = () => undefined;
private waitForIdleFn: () => Promise<void> = async () => {};
private abortFn: () => void = () => {};
@ -330,6 +331,7 @@ export class ExtensionRunner {
// Context actions (required)
this.getModel = contextActions.getModel;
this.isIdleFn = contextActions.isIdle;
this.isProjectTrustedFn = contextActions.isProjectTrusted;
this.getSignalFn = contextActions.getSignal;
this.abortFn = contextActions.abort;
this.hasPendingMessagesFn = contextActions.hasPendingMessages;
@ -648,6 +650,10 @@ export class ExtensionRunner {
runner.assertActive();
return runner.isIdleFn();
},
isProjectTrusted: () => {
runner.assertActive();
return runner.isProjectTrustedFn();
},
get signal() {
runner.assertActive();
return runner.getSignalFn();

View file

@ -314,6 +314,8 @@ export interface ExtensionContext {
model: Model<any> | undefined;
/** Whether the agent is idle (not streaming) */
isIdle(): boolean;
/** Whether project-local trust is active for this context. */
isProjectTrusted(): boolean;
/** The current abort signal, or undefined when the agent is not streaming. */
signal: AbortSignal | undefined;
/** Abort the current agent operation */
@ -1528,6 +1530,7 @@ export interface ExtensionActions {
export interface ExtensionContextActions {
getModel: () => Model<any> | undefined;
isIdle: () => boolean;
isProjectTrusted: () => boolean;
getSignal: () => AbortSignal | undefined;
abort: () => void;
hasPendingMessages: () => boolean;

View file

@ -1669,6 +1669,7 @@ export class InteractiveMode {
modelRegistry: this.session.modelRegistry,
model: this.session.model,
isIdle: () => !this.session.isStreaming,
isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
signal: this.session.agent.signal,
abort: () => {
this.restoreQueuedMessagesToEditor({ abort: true });

View file

@ -76,6 +76,7 @@ describe("ExtensionRunner", () => {
const extensionContextActions: ExtensionContextActions = {
getModel: () => undefined,
isIdle: () => true,
isProjectTrusted: () => true,
getSignal: () => undefined,
abort: () => {},
hasPendingMessages: () => false,
@ -496,6 +497,18 @@ describe("ExtensionRunner", () => {
expect(ctx.hasUI).toBe(false);
});
it("exposes project trust state on ExtensionContext", async () => {
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
runner.bindCore(extensionActions, {
...extensionContextActions,
isProjectTrusted: () => false,
});
const ctx = runner.createContext();
expect(ctx.isProjectTrusted()).toBe(false);
});
it("exposes rpc mode with hasUI true when an RPC UI context is provided", async () => {
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);

View file

@ -12,6 +12,7 @@ function createContext(tokens: number | null, compact = vi.fn()): ExtensionConte
modelRegistry: {} as ExtensionContext["modelRegistry"],
model: undefined,
isIdle: () => true,
isProjectTrusted: () => true,
signal: undefined,
abort: vi.fn(),
hasPendingMessages: () => false,