plugins can ship a browser extension, published unauthenticated
The browser relay is the first extension, not the last, so `extension/` is now a directory any plugin may ship — like `assets/` — and the installer zips it to public/plugins/<name>/extension.zip, served by the existing /plugins/* wildcard with no auth. Unauthenticated deliberately. An `<a download>` is a plain navigation with no Bearer header, so an authenticated URL makes Chrome say "Try to sign in to the site. Then, download again" — advice that cannot work, because signing in is not what is missing. Nothing in an extension is secret; the pairing token is, it is entered afterwards, and it is not in the archive. Built at install rather than committed. The file it replaces was hand-built and had drifted 58 lines behind its source, serving an extension missing the multi-session recovery path. zip.ts writes stored entries with fixed timestamps — no host `zip` binary, and identical bytes for an unchanged directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,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 { generatePluginSchemas, pushPluginSchema } from './schema';
|
||||
import { declaredTables, dropTables, existingTables } from './tables';
|
||||
import type { OnLog } from './run-logged';
|
||||
@@ -393,6 +394,12 @@ export async function installPlugin(
|
||||
await step(steps, onStep, `assets: published to /plugins/${appName}/`);
|
||||
}
|
||||
|
||||
// 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`);
|
||||
}
|
||||
|
||||
// What this plugin needs an AGENT to know about itself. A plugin is a new surface arriving at
|
||||
// runtime, and nothing else carries that across: install gitea and no agent learns that `tea` is
|
||||
// authenticated unless the plugin says so. Same shape as the assets above — a directory in the
|
||||
@@ -522,6 +529,7 @@ export async function uninstallPlugin(appName: string, onStep?: OnStep, onLog?:
|
||||
}
|
||||
|
||||
await unpublishAssets(appName);
|
||||
await unpublishExtension(appName).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.
|
||||
|
||||
Reference in New Issue
Block a user