Skip to content

feat(agent): add extensible web workbench and plugins - #65

Merged
QiaolongLi1201 merged 5 commits into
mainfrom
codex/moss-web-workbench
Aug 18, 2026
Merged

feat(agent): add extensible web workbench and plugins#65
QiaolongLi1201 merged 5 commits into
mainfrom
codex/moss-web-workbench

Conversation

@QiaolongLi1201

Copy link
Copy Markdown
Collaborator

Summary

  • replace the inline Web shell with a React/Vite Moss workbench and shared design-system tokens
  • add a trusted plugin manifest, CLI lifecycle, Web slots, runtime isolation, and owner-aware tool replacement
  • bundle an attributed DeepSeek Harness skill as the opt-in official plugin official:deepseek-harness
  • harden installs with exact npm versions, disabled-by-default activation, symlink containment, setup preflight workers, and heartbeat-backed cross-process registry locking

Verification

  • npm run verify
  • focused plugin lifecycle, lock compromise/heartbeat, startup isolation, Web API, and real Chromium tests
  • final code review: no remaining P0/P1 findings

Notes

The Web UI keeps Moss branding while aligning the reference workbench interaction model. Third-party plugin JavaScript remains explicitly trusted code and is never described as sandboxed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd7d691a56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -1,2 +1,13 @@
export { startMossWebServer } from './web-server.js';
export type { MossWebServerHandle, MossWebServerOptions } from './web-server.js';
export { MOSS_WEB_SLOTS, MOSS_WEB_THEME_TOKENS } from './web-contracts.js';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Re-export the documented Web server API

The ./web barrel no longer exports startMossWebServer, MossWebServerHandle, or MossWebServerOptions, even though this commit promotes the server to @beta and documents these symbols as the supported @rdk-moss/agent/web API. Consequently, both existing imports and the newly documented embedding path fail because the package subpath exposes only constants and DTO types. Restore the server exports and cover them in web-public-api.spec.mjs.

AGENTS.md reference: AGENTS.md:L144-L144

Useful? React with 👍 / 👎.

);
}
await mkdir(this.npmRoot, { recursive: true });
await this.npmRunner('npm', {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Invoke the npm shim correctly on Windows

When a Windows user runs moss plugins add package@version, this calls runProcess('npm', ...), whose spawn path does not enable a shell and therefore cannot execute the standard npm.cmd shim. The repository already handles this exact constraint in src/cli/update.ts by enabling the shell for the default npm command; use the same platform-aware approach here so npm-backed plugin installation does not fail with ENOENT on Windows.

AGENTS.md reference: AGENTS.md:L57-L57

Useful? React with 👍 / 👎.

response: http.ServerResponse,
filename: 'workbench.css' | 'workbench.js'
): Promise<void> {
const body = await readFile(new URL(`./client/${filename}`, import.meta.url), 'utf8');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serve the workbench when running the source CLI

In the documented npm run cli -- web development path, import.meta.url points into src/web-ui, so this attempts to read src/web-ui/client/workbench.js; only workbench.tsx exists there because the JavaScript bundle is generated under dist during a build. The HTML loads but the script request fails, leaving a blank workbench. Either resolve built assets independently of the executing source path or provide a source-mode asset build/loader.

AGENTS.md reference: packages/moss-agent/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Comment on lines +296 to +299
useEffect(() => {
void refresh().catch(() => setOnline(false));
const onlineListener = () => void refresh().catch(() => setOnline(false));
const offlineListener = () => setOnline(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore automatic retries after a transient disconnect

If the initial refresh or a later request fails while the browser remains network-online—for example, while the local Moss process restarts—this only sets online to false and retries solely on a browser online event. A localhost process restart does not normally change browser connectivity, so the page remains permanently in “Reconnecting” state after the server returns. Schedule a bounded refresh retry as the previous client did, and cancel it during cleanup.

Useful? React with 👍 / 👎.

Comment on lines +291 to +295
const root = await this.resolveSource(source);
const manifest = await readMossPluginManifest(root);
const existing = (await this.readRegistry()).plugins;
if (existing.some(({ id }) => id === manifest.id)) {
throw invalidManifest(`plugin already installed: ${manifest.id}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Check duplicates before mutating shared npm installs

For an npm source, resolveSource() installs into the shared prefix before the registry checks whether the manifest ID is already installed. If an enabled foo@1.0.0 is registered and the user attempts moss plugins add foo@2.0.0, npm replaces node_modules/foo with v2, then this duplicate check reports failure and leaves the v1 registry record untouched. On the next startup that supposedly failed and disabled-by-default addition is nevertheless loaded as v2. Install into a staging location and validate duplicates before replacing any path referenced by the live registry.

Useful? React with 👍 / 👎.

Comment on lines +10 to +11
export async function runPluginsCommand(args: readonly string[]): Promise<void> {
const registry = new InstalledPluginRegistry({ configDir: resolveConfigDir() });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor --config-file for plugin registry storage

When the CLI is invoked with --config-file /path/to/config.json, the dispatcher loads that file but this command still constructs the registry under the default resolveConfigDir(). Agent startup and the Web host likewise load plugins from the default directory, so plugin operations performed for an explicit portable config are written to and composed from an unrelated registry. Pass the resolved config path's directory through the command and runtime wiring, as other config-owned state does.

Useful? React with 👍 / 👎.

Comment on lines +474 to +477
<button
className={session.sessionId === sessionId ? 'active' : ''}
key={session.sessionId}
onClick={() => void openSession(session)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Isolate a running stream from session navigation

While a turn is running, the recent-session buttons remain enabled and openSession() replaces the global sessionId and items, but the old fetch continues feeding applyStreamEvent() into that same global timeline. Selecting another session therefore displays the previous session's streamed text and tools in the newly selected conversation, and Stop then targets the newly selected session instead of the actual running one. Disable or cancel-and-await navigation during a turn, or key stream state and cancellation by the originating session.

Useful? React with 👍 / 👎.


async function validatePluginSetup(entry: InstalledMossPlugin): Promise<void> {
const manifest = await readMossPluginManifest(entry.root);
const worker = new Worker(new URL('./plugin-setup-worker.js', import.meta.url), {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve the setup worker when running the source CLI

In the documented source-mode CLI, import.meta.url points into src/plugins, but this constructs a worker URL for src/plugins/plugin-setup-worker.js; only plugin-setup-worker.ts exists until the package is built. As a result, npm run cli -- plugins enable <id> and plugins doctor reject every setup validation with a worker module-not-found error. Resolve a source-compatible worker entry or ensure the development command builds and uses the emitted worker first.

AGENTS.md reference: packages/moss-agent/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Comment on lines +208 to +212
async function snapshotActiveWebContributions(
registry: InstalledPluginRegistry | undefined,
activePluginIds: ReadonlySet<string>
): Promise<readonly ActiveWebContribution[]> {
if (!registry) return Object.freeze([]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make registered Web contributions consumable

For an embedding host that installs a MossPlugin using context.registerWebContribution() and then starts the Web server without an installed-plugin registry, this immediately returns no contributions. Even when configDir is supplied, the server rebuilds contributions only from moss.plugin.json and never consumes the definitions staged on the active plugin host, whose snapshot retains only their IDs. Thus the newly public registration API cannot mount any programmatic plugin UI; preserve and resolve the staged contribution records or narrow the public contract to manifest-only contributions.

AGENTS.md reference: AGENTS.md:L144-L144

Useful? React with 👍 / 👎.

Comment on lines +399 to +400
if (active.has(sessionId)) {
return sendJson(response, 409, { error: 'session already has an active turn' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow a reloaded browser to control an active turn

If the page reloads or its streaming connection drops during a long turn, the server keeps the session in active, but the new workbench initializes its local running state to false and does not derive it from the running task snapshot. The Stop control is therefore absent, while any attempted prompt receives this 409 and is treated by the client as a connection failure. Until the old model call finishes, the user can neither reconnect to nor cancel it; expose active-turn state and cancellation after bootstrap, or abort the turn when its response closes.

Useful? React with 👍 / 👎.

@QiaolongLi1201
QiaolongLi1201 merged commit 5ad2b20 into main Aug 18, 2026
10 checks passed
QiaolongLi1201 added a commit that referenced this pull request Aug 19, 2026
feat(agent): add extensible web workbench and plugins
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant