Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a072cc8
feat: add LinkCode plugin marketplace with manifest-driven settings a…
adminliu-main Aug 24, 2026
2509c6c
test(daemon): add plugin marketplace e2e and dev marketplace fixture
adminliu-main Aug 24, 2026
6373d0c
fix(daemon): harden plugin install staging, settings rollback, and ma…
adminliu-main Aug 24, 2026
05b49de
fix: address plugin marketplace review feedback and split the mail pl…
adminliu-main Aug 25, 2026
a02615f
fix(plugin-marketplace): harden recovery and legacy refresh
adminliu-main Aug 25, 2026
444224b
fix(plugin-marketplace): roll back failed registry writes
adminliu-main Aug 25, 2026
d5edb11
fix(plugin-marketplace): harden client mutation state
adminliu-main Aug 25, 2026
d4b9136
docs(plugin-store): document the harmless publish/persist crash window
adminliu-main Aug 25, 2026
3050e53
feat(schema): type-check plugin setting defaults and add secret-prese…
adminliu-main Aug 27, 2026
4249757
feat(engine): manifest-authoritative plugin-config writes, secret-pre…
adminliu-main Aug 27, 2026
f9858ab
feat(plugin-store): crash-safe uninstall with retry markers, fail-clo…
adminliu-main Aug 27, 2026
debfb28
feat(workbench): secret-presence-aware plugin settings forms with dae…
adminliu-main Aug 27, 2026
7657386
fix(plugin-store): purge inherited settings when reinstalling over a …
adminliu-main Aug 27, 2026
ea4d7e5
fix(wire): require configuredSecrets presence bits on plugin-config f…
adminliu-main Aug 27, 2026
c3b3be4
fix(engine): reject empty-string setting writes at the daemon config …
adminliu-main Aug 27, 2026
f12c51a
perf(engine): resolve codex native mcp names once per session start
adminliu-main Aug 27, 2026
c16e3ea
perf(engine): skip codex native MCP discovery when nothing can inject
adminliu-main Aug 27, 2026
7733052
fix(plugin-store): purge pending-uninstall settings before the regist…
adminliu-main Aug 27, 2026
60867e4
fix(plugin-store): throw on failed pending-uninstall purge during rei…
adminliu-main Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/daemon/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ Runs via `tsx` in dev (`pnpm -F @linkcode/daemon dev`) and a `tsup` bundle in pr
round-trip through this store can (`__tests__/session-store.test.ts`).
- **`runtime.json`** — endpoint discovery (`{name,pid,startedAt,listeners:[{type,url}]}`), written
`0600` only AFTER every listener binds and removed on graceful `SIGINT`/`SIGTERM` shutdown.
- **Marketplace sources are trusted code origins.** Installed manifests may launch their declared
command, args, and env; SRI pins bytes to the index but does not make a hostile index safe. Keep
adding sources config/env-only until an explicit consent and trust design is approved.

## Ports & one-per-profile

Expand Down
217 changes: 217 additions & 0 deletions apps/daemon/e2e/plugin-marketplace.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import assert from 'node:assert/strict';
import type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { LinkCodeClient } from '@linkcode/client-core';
import { SocketIoTransport } from '@linkcode/transport';
import { wait } from 'foxts/wait';
import { waitFor } from 'foxts/wait-for';

const daemonDir = resolve(import.meta.dirname, '..');
const repoRoot = resolve(daemonDir, '..', '..');
const marketplaceScript = join(repoRoot, 'scripts', 'dev-marketplace.mts');
const fixtureIndex = join(repoRoot, 'node_modules', '.cache', 'dev-marketplace', 'index.json');

const MARKETPLACE_ID = 'linkcode-official';
const PLUGIN_ID = 'linkcode/echo';
const PLUGIN_VERSION = '0.1.0';
const SECRET_TOKEN = 'e2e-secret-token';

async function freePort(): Promise<number> {
const server = createServer();
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
assert(address && typeof address !== 'string');
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
return address.port;
}

async function main(): Promise<void> {
assert(
existsSync(join(daemonDir, 'dist/index.js')),
'daemon dist is missing; run its build first',
);
assert(
existsSync(fixtureIndex),
'dev marketplace fixture is missing; run: node scripts/dev-marketplace.mts --build',
);

const home = mkdtempSync(join(tmpdir(), 'linkcode-marketplace-e2e-'));
const daemonPort = await freePort();
const marketPort = await freePort();
const logs: string[] = [];

const marketplace = spawn(process.execPath, [marketplaceScript], {
cwd: repoRoot,
env: { ...process.env, HOME: home, DEV_MARKETPLACE_PORT: String(marketPort) },
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
marketplace.stdout.on('data', (chunk: Buffer) => logs.push(chunk.toString()));
marketplace.stderr.on('data', (chunk: Buffer) => logs.push(chunk.toString()));

let exit: { code: number | null; signal: NodeJS.Signals | null } | null = null;
const child = spawn(process.execPath, ['--import', './dist/instrument.js', 'dist/index.js'], {
cwd: daemonDir,
env: {
...process.env,
HOME: home,
LINKCODE_HOST: '127.0.0.1',
LINKCODE_PORT: String(daemonPort),
LINKCODE_MARKETPLACE_URL: `http://127.0.0.1:${marketPort}/index.json`,
},
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
child.stdout.on('data', (chunk: Buffer) => logs.push(chunk.toString()));
child.stderr.on('data', (chunk: Buffer) => logs.push(chunk.toString()));
child.once('exit', (code, signal) => {
exit = { code, signal };
});

let client: LinkCodeClient | null = null;
try {
// The tsup bundle is stamped `release`, so state lands in `.linkcode`, not `.linkcode.development`.
const runtimePath = join(home, '.linkcode', 'runtime.json');
const runtime = await waitFor(
() => {
if (exit) throw new Error(`daemon exited during boot: ${JSON.stringify(exit)}`);
if (!existsSync(runtimePath)) return false;
try {
return JSON.parse(readFileSync(runtimePath, 'utf8')) as {
pid: number;
listeners: Array<{ type: string; url: string }>;
};
} catch {
return false;
}
},
100,
AbortSignal.timeout(30000),
);
assert.equal(runtime.pid, child.pid);
const listener = runtime.listeners.find((entry) => entry.type === 'socket.io');
assert(listener, 'runtime.json has no socket.io listener');

client = new LinkCodeClient(new SocketIoTransport({ url: listener.url }), { randomUUID });
await client.connect();

// 1. The env override retargets the built-in official marketplace at the loopback fixture.
const marketplaces = await client.listPluginMarketplaces();
const official = marketplaces.find((entry) => entry.id === MARKETPLACE_ID);
assert(official, 'official marketplace missing from list');
assert.equal(official.source.url, `http://127.0.0.1:${marketPort}/index.json`);

// 2. Refresh pulls the catalog; a second refresh rides the ETag to a 304.
const first = await client.refreshPluginMarketplace(MARKETPLACE_ID);
assert(
first.releases.some(
(entry) =>
entry.pluginId === PLUGIN_ID && entry.release.manifest.version === PLUGIN_VERSION,
),
'catalog does not list linkcode/echo',
);
const second = await client.refreshPluginMarketplace(MARKETPLACE_ID);
assert.equal(second.notModified, true, 'second refresh did not hit the ETag cache');
assert(
second.releases.some(
(entry) =>
entry.pluginId === PLUGIN_ID && entry.release.manifest.version === PLUGIN_VERSION,
),
'304 refresh cleared the cached catalog',
);

// 3. Install from the cached catalog; the package lands in the Store.
const installed = await client.installLinkCodePlugin({
marketplaceId: MARKETPLACE_ID,
pluginId: PLUGIN_ID,
version: PLUGIN_VERSION,
});
assert.equal(installed.pluginId, PLUGIN_ID);
const packageDir = join(home, '.linkcode', 'plugins', 'linkcode', 'echo', PLUGIN_VERSION);
assert(existsSync(join(packageDir, 'manifest.json')), 'installed manifest.json missing');
assert(existsSync(join(packageDir, 'dist', 'index.js')), 'installed dist/index.js missing');

// 4. Settings: masked read shows the schema, set splits secret vs non-secret.
const before = await client.listLinkCodePluginConfigs();
const view = before.find((entry) => entry.id === PLUGIN_ID);
assert(view, 'installed plugin missing from plugin-config.list');
assert(view.settings.token.secret, 'token must be a secret field');
assert.equal(view.values.token, undefined, 'secret value leaked in masked read');

await client.setLinkCodePluginConfig({
pluginId: PLUGIN_ID,
set: { greeting: '你好', token: SECRET_TOKEN, mode: 'shout' },
});
const configFile = JSON.parse(readFileSync(join(home, '.linkcode', 'config.json'), 'utf8')) as {
pluginConfigs?: Record<string, Record<string, unknown>>;
};
const pluginConfig = configFile.pluginConfigs?.[PLUGIN_ID];
assert(pluginConfig, 'installed plugin config missing from config.json');
assert.equal(pluginConfig.greeting, '你好');
assert.equal(pluginConfig.mode, 'shout');
assert(!('token' in pluginConfig), 'secret in config.json');
const secretsFile = JSON.parse(
readFileSync(join(home, '.linkcode', 'secrets.json'), 'utf8'),
) as {
protection: 'os-keyring' | 'plaintext';
};
// A fake HOME has no login keychain, so the vault degrades to plaintext on disk (with a boot
// warning). Either way the token belongs in secrets.json — just never in config.json.
const secretsRaw = readFileSync(join(home, '.linkcode', 'secrets.json'), 'utf8');
if (secretsFile.protection === 'os-keyring') {
assert(!secretsRaw.includes(SECRET_TOKEN), 'token stored in plaintext under os-keyring');
} else {
assert(secretsRaw.includes(SECRET_TOKEN), 'token missing from the vault');
}

const after = await client.listLinkCodePluginConfigs();
const afterView = after.find((entry) => entry.id === PLUGIN_ID);
assert(afterView, 'installed plugin missing after set');
assert.equal(afterView.values.greeting, '你好');
assert.equal(afterView.values.token, undefined, 'secret value leaked after set');

// 5. Uninstall removes the package and prunes its config.
const removed = await client.uninstallLinkCodePlugin(PLUGIN_ID);
assert.equal(removed, PLUGIN_ID);
assert(!existsSync(packageDir), 'package dir survived uninstall');

assert(child.kill('SIGTERM'), 'daemon rejected SIGTERM');
const shutdown = await waitFor(() => exit ?? false, 50, AbortSignal.timeout(10000));
assert.deepEqual(shutdown, { code: 0, signal: null });

// eslint-disable-next-line no-console -- e2e progress line; the daemon's own logs are captured below.
console.log('PASS marketplace refresh (ETag 304), install, settings vault split, uninstall');
} catch (error) {
// eslint-disable-next-line no-console -- dump the captured daemon log on failure for triage.
console.error(logs.join('').slice(-8000));
throw error;
} finally {
client?.dispose();
await stop(child);
await stop(marketplace);
rmSync(home, { recursive: true, force: true });
}
}

async function stop(child: ChildProcess): Promise<void> {
if (child.exitCode !== null || child.signalCode !== null) return;
child.kill('SIGTERM');
await Promise.race([
new Promise<void>((resolve) => {
child.once('exit', () => resolve());
}),
wait(5000).then(() => child.kill('SIGKILL')),
]);
}

void main();
1 change: 1 addition & 0 deletions apps/daemon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"better-sqlite3": "^12.11.1",
"foxts": "^5.8.0",
"pino": "^10.3.1",
"tar": "^7.5.22",
"zod": "catalog:"
},
"devDependencies": {
Expand Down
97 changes: 91 additions & 6 deletions apps/daemon/src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@ describe('saveProviderConfiguration', () => {
});
});

function writeCustomMcpConfig(customMcpServers: unknown): void {
const dir = join(process.env.HOME ?? '', '.linkcode');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'config.json'), JSON.stringify({ customMcpServers }));
}

describe('loadConfig custom MCP servers', () => {
const validServer = {
id: 'custom-1',
Expand All @@ -329,12 +335,6 @@ describe('loadConfig custom MCP servers', () => {
createdAt: 1,
} as const satisfies CustomMcpServer;

function writeCustomMcpConfig(customMcpServers: unknown): void {
const dir = join(process.env.HOME ?? '', '.linkcode');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'config.json'), JSON.stringify({ customMcpServers }));
}

it('keeps valid servers and drops an invalid one without blanking the rest', () => {
const errorSpy = vi.spyOn(logger, 'warn').mockImplementation(noop);
writeCustomMcpConfig([validServer, { id: 'broken', server: { type: 'stdio' } }]);
Expand Down Expand Up @@ -473,6 +473,91 @@ describe('loadConfig custom MCP servers', () => {
});
});

function writeMarketplacesConfig(marketplaces: unknown): void {
const dir = join(process.env.HOME ?? '', '.linkcode');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'config.json'), JSON.stringify({ marketplaces }));
}

describe('loadConfig marketplaces', () => {
afterEach(() => {
delete process.env.LINKCODE_MARKETPLACE_URL;
});

it('defaults to the official marketplace when config.json names none', () => {
expect(loadConfig(vault).marketplaces).toEqual([
{
id: 'linkcode-official',
displayName: 'LinkCode Official',
source: { type: 'remote', url: 'https://plugins.linkcode.ai/index.json' },
enabled: true,
},
]);
});

it('keeps valid entries and drops invalid or duplicate ones, logging each drop', () => {
const errorSpy = vi.spyOn(logger, 'warn').mockImplementation(noop);
writeMarketplacesConfig([
{
id: 'community',
source: { type: 'remote', url: 'https://example.com/index.json' },
enabled: true,
},
{
id: 'community',
source: { type: 'remote', url: 'https://other.example/index.json' },
enabled: true,
},
{
id: 'insecure',
source: { type: 'remote', url: 'http://example.com/index.json' },
enabled: true,
},
]);

expect(loadConfig(vault).marketplaces).toEqual([
{
id: 'community',
source: { type: 'remote', url: 'https://example.com/index.json' },
enabled: true,
},
]);
expect(errorSpy).toHaveBeenCalledTimes(2);
});

it('falls back to the default when the field is not an array', () => {
const errorSpy = vi.spyOn(logger, 'warn').mockImplementation(noop);
writeMarketplacesConfig({ not: 'an array' });

expect(loadConfig(vault).marketplaces[0]?.id).toBe('linkcode-official');
expect(errorSpy).toHaveBeenCalled();
});

it("lets LINKCODE_MARKETPLACE_URL retarget the official marketplace's index URL", () => {
process.env.LINKCODE_MARKETPLACE_URL = 'https://staging.example/index.json';

expect(loadConfig(vault).marketplaces).toEqual([
{
id: 'linkcode-official',
displayName: 'LinkCode Official',
source: { type: 'remote', url: 'https://staging.example/index.json' },
enabled: true,
},
]);
});

it('ignores a non-HTTPS LINKCODE_MARKETPLACE_URL, logging the drop', () => {
const errorSpy = vi.spyOn(logger, 'warn').mockImplementation(noop);
process.env.LINKCODE_MARKETPLACE_URL = 'http://insecure.example/index.json';

expect(loadConfig(vault).marketplaces[0]?.source).toEqual({
type: 'remote',
url: 'https://plugins.linkcode.ai/index.json',
});
expect(errorSpy).toHaveBeenCalled();
});
});

describe('createProviderConfigStore', () => {
it('does not publish provider, account, or custom MCP state when persistence fails', () => {
const oldProviders = { codex: { enabled: true } } as const;
Expand Down
7 changes: 7 additions & 0 deletions apps/daemon/src/__tests__/fixtures/in-memory-vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export function createInMemoryVault(protection: SecretProtection = 'os-keyring')
return {
protection,
get: (key) => refs.get(prefix + key) ?? null,
keys: () => {
const keys: string[] = [];
for (const ref of refs.keys()) {
if (ref.startsWith(prefix)) keys.push(ref.slice(prefix.length));
}
return keys;
},
set(key, secret) {
refs.set(prefix + key, secret);
},
Expand Down
Loading
Loading