diff --git a/docs/design/extension-management-v2.md b/docs/design/extension-management-v2.md index 1b23ed8d2d6..b68d1c5049d 100644 --- a/docs/design/extension-management-v2.md +++ b/docs/design/extension-management-v2.md @@ -103,9 +103,9 @@ type InitialActivation = | { scope: 'workspace'; workspaceId: string }; ``` -The daemon install endpoint accepts HTTPS Git, GitHub Release, and npm sources -under the public network policy. SSH and local/link sources remain local CLI -features. Update preserves the extension id, +The daemon install endpoint accepts HTTPS Git, GitHub Release, npm, and +absolute-path local sources. SSH and link sources remain local CLI features. +Update preserves the extension id, manifest name, settings, and activation policy. “Already current” is a successful `updated: false` result. Uninstall is idempotent and removes both the artifact and policy. diff --git a/packages/cli/src/serve/routes/workspace-extensions-controller.test.ts b/packages/cli/src/serve/routes/workspace-extensions-controller.test.ts index b8cc2437ef9..9e553d480f1 100644 --- a/packages/cli/src/serve/routes/workspace-extensions-controller.test.ts +++ b/packages/cli/src/serve/routes/workspace-extensions-controller.test.ts @@ -49,6 +49,21 @@ describe('createExtensionsController', () => { vi.restoreAllMocks(); }); + it('does not impose a public-only extension network policy', () => { + const controller = createExtensionsController({ + boundWorkspace: '/work/bound', + bridge: {} as AcpSessionBridge, + workspace: {} as DaemonWorkspaceService, + isWorkspaceTrusted: () => true, + }); + + const manager = controller.createExtensionManager() as unknown as { + networkPolicy?: string; + }; + + expect(manager.networkPolicy).toBeUndefined(); + }); + it('releases the commit lane when a manual refresh times out', async () => { vi.useFakeTimers(); let refreshCalls = 0; diff --git a/packages/cli/src/serve/routes/workspace-extensions-controller.ts b/packages/cli/src/serve/routes/workspace-extensions-controller.ts index 6fd48a145c2..e8345d0cae2 100644 --- a/packages/cli/src/serve/routes/workspace-extensions-controller.ts +++ b/packages/cli/src/serve/routes/workspace-extensions-controller.ts @@ -309,7 +309,6 @@ export function createExtensionsController( getWorkspaceTrustStatus(loadSettings(workspaceDir).merged, workspaceDir) .effective.state === 'trusted', requestConsent: () => Promise.resolve(), - networkPolicy: 'public', requestSetting: interactions?.requestSetting ?? (async (setting: ExtensionSetting) => { diff --git a/packages/cli/src/serve/routes/workspace-extensions.ts b/packages/cli/src/serve/routes/workspace-extensions.ts index 5854f37016c..b9e1095a967 100644 --- a/packages/cli/src/serve/routes/workspace-extensions.ts +++ b/packages/cli/src/serve/routes/workspace-extensions.ts @@ -341,9 +341,7 @@ const validateExtensionSourceMetadata = ( const parsed = parsePotentialSourceUrl(installMetadata.source); return ( !!parsed && - (installMetadata.networkPolicy === 'public' - ? parsed.protocol === 'https:' - : parsed.protocol === 'https:' || parsed.protocol === 'ssh:') && + parsed.protocol === 'https:' && !isBlockedAuthProviderHost(parsed.hostname) ); }; @@ -1108,9 +1106,7 @@ export function registerWorkspaceExtensionRoutes( isPortableAbsolutePath(sourceValue) || sourceValue.startsWith('.'); if (localSource) { try { - const metadata = await parseInstallSource(sourceValue, { - networkPolicy: 'public', - }); + const metadata = await parseInstallSource(sourceValue); assertDaemonExtensionInstallSource( metadata, sourceValue, @@ -1165,9 +1161,7 @@ export function registerWorkspaceExtensionRoutes( res, async (extensionManager, _signal, context, operationId) => { const prepared = await context!.prepare(async (signal) => { - const installMetadata = await parseInstallSource(sourceValue, { - networkPolicy: 'public', - }); + const installMetadata = await parseInstallSource(sourceValue); assertDaemonExtensionInstallSource( installMetadata, @@ -1970,9 +1964,7 @@ export function registerWorkspaceExtensionRoutes( gitCredential?.persistence === 'one_time' ? {} : { source: sourceValue }, async (extensionManager, _signal, context) => { const prepared = await context!.prepare(async (signal) => { - const metadata = await parseInstallSource(sourceValue, { - networkPolicy: 'public', - }); + const metadata = await parseInstallSource(sourceValue); assertDaemonExtensionInstallSource( metadata, sourceValue, diff --git a/packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts b/packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts index 1021a9a164c..78a1ad82ee7 100644 --- a/packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts +++ b/packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts @@ -1890,6 +1890,9 @@ describe('extension management v2 REST', () => { }, }), ); + expect( + prepareInstall.mock.calls.at(-1)?.[0].installMetadata, + ).not.toHaveProperty('networkPolicy'); expect(JSON.stringify(operation)).not.toContain('fine-grained-token'); expect(JSON.stringify(h.primary.bridge)).not.toContain( 'fine-grained-token', diff --git a/packages/cli/src/serve/server.test.ts b/packages/cli/src/serve/server.test.ts index 4aab38d1e0c..fc3691b2706 100644 --- a/packages/cli/src/serve/server.test.ts +++ b/packages/cli/src/serve/server.test.ts @@ -8056,6 +8056,7 @@ describe('createServeApp', () => { persistence: expected, }, }); + expect(captured?.installMetadata).not.toHaveProperty('networkPolicy'); await vi.waitFor(() => expect(bridge.extensionEvents.at(-1)).toMatchObject({ status: 'installed',