name a published extension for itself, not "extension.zip"

extension.zip says nothing once it is in a Downloads folder — not what it is, not which server it came
from, and not which of two it is. The name comes from the extension's own manifest.json, which already
carries what a browser will show: "Officer Browser Relay" becomes officer-browser-relay.zip.

Generic, since extensions are meant to be a marketplace of them: any plugin shipping extension/ gets a
file named after its own manifest, falling back to <appName>-extension.zip when that cannot be read —
still better than extension.zip, and it never throws, because a broken extension manifest should fail
the install for its own reason rather than because a file could not be named.

Uninstall removes both names. An install from earlier today published extension.zip, and leaving that
behind would serve a stale extension for a plugin that is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 01:52:24 +00:00
co-authored by Claude Opus 5
parent cfb2f98ccb
commit d043e6650a
2 changed files with 38 additions and 6 deletions
+5 -3
View File
@@ -1,3 +1,4 @@
import { join } from 'node:path';
import {
deleteScreenForAllUsers,
getDockPaths,
@@ -20,7 +21,7 @@ import { publishAffordances, unpublishAffordances } from '../affordances/publish
import { publishNotifications, unpublishNotifications } from '../notifications/publish';
import { publishSkills, unpublishSkills } from '../skills/publish';
import { refreshPluginMounts, snapshotPlugins } from './mount';
import { publishExtension, unpublishExtension } from './zip';
import { publishExtension, unpublishExtension, extensionFileName } from './zip';
import { generatePluginSchemas, pushPluginSchema } from './schema';
import { declaredTables, dropTables, existingTables } from './tables';
import type { OnLog } from './run-logged';
@@ -397,7 +398,8 @@ export async function installPlugin(
// A browser extension, zipped from `extension/` and served UNAUTHENTICATED — see zip.ts. Built here
// rather than committed, so it cannot fall behind the directory it is made from.
if (await publishExtension(appName, plugin.dir).catch(() => false)) {
await step(steps, onStep, `extension: published to /plugins/${appName}/extension.zip`);
const file = extensionFileName(appName, join(plugin.dir, 'extension'));
await step(steps, onStep, `extension: published to /plugins/${appName}/${file}`);
}
// What this plugin needs an AGENT to know about itself. A plugin is a new surface arriving at
@@ -529,7 +531,7 @@ export async function uninstallPlugin(appName: string, onStep?: OnStep, onLog?:
}
await unpublishAssets(appName);
await unpublishExtension(appName).catch(() => {});
await unpublishExtension(appName, plugin?.dir).catch(() => {});
// Same reasoning: a copy, with the original still in the plugin's directory. Removed so an agent stops
// being told about a surface that is no longer there — an affordance for an uninstalled plugin is
// worse than none, because it reads as true.