Skip to content

[Security] Reapprove changed plugin artifacts and minimize MCP child environments #233

Description

@fettpl

Audit metadata

  • Priority: P1
  • Estimated effort: M
  • Implementation risk: HIGH
  • Category: security
  • Evidence baseline: 07be5be7ce69bea0c3118744ab90d148b010fce0 (origin/main on 2026-07-17)

Dependencies

  • P1 proposal 021 — Enforce least-privilege connector permissions and action approval. Included in the current P1 issue wave.
  • Audited P2 proposal 028 — Make the standalone agent runtime independently installable and verified. Not filed in this P1-only wave.

Description and impact

An enabled plugin retains tool approval when files change without a version
bump, and version reconciliation can preserve the old allowlist automatically.
Stdio MCP processes also inherit the complete Local Studio environment, which
can expose unrelated credentials to plugin code. Approval must bind to the exact
artifact and child processes must receive only platform essentials plus explicit
connector configuration.

Current state

  • services/agent-runtime/src/plugin-runtime.ts:113-145 records plugin origin as
    ID, version, and binding only.
  • plugin-runtime.ts:426-458 detects only version mismatch and re-enables a
    replacement with the previous allowTools after a probe.
  • services/agent-runtime/src/connector-contract.ts:5-10 has no artifact digest
    in ConnectorOriginSchema.
  • services/agent-runtime/src/mcp-client.ts:73-78 spawns stdio servers with
    { ...process.env, ...(target.env ?? {}) }.
  • frontend/scripts/plugin-runtime.test.ts:166-210 currently expects automatic
    approval migration on version change.

Verification commands

Purpose Command Expected on success
Runtime tests npm --prefix services/agent-runtime test digest, approval, and environment cases pass
Runtime check npm --prefix services/agent-runtime run check exit 0
Repository gate npm run check exit 0
Integration npm run test:integration all pass
Desktop package npm --prefix frontend run desktop:dist production desktop bundle created

Scope

In scope:

  • services/agent-runtime/src/plugin-artifact-digest.ts (create)
  • services/agent-runtime/src/plugin-artifact-digest.test.ts (create)
  • services/agent-runtime/src/plugin-discovery.ts
  • services/agent-runtime/src/plugin-runtime.ts
  • services/agent-runtime/src/connector-contract.ts
  • services/agent-runtime/src/mcp-client.ts
  • Focused plugin/runtime tests covering approval migration and child spawn env

Out of scope:

  • Plugin download/installation; audit proposal 034 designs that lifecycle.
  • OS sandboxing, containers, or rewriting MCP protocol transport.
  • Passing arbitrary parent variables for compatibility.
  • Changing account-adapter connectors unless they use the same stdio path.

Solution design

Compute SHA-256 over a canonical sorted walk containing normalized relative
path, entry type, relevant mode bits, symlink target, and every regular-file
byte. Reject escaping/dangling/cyclic symlinks and special files; ignore mtimes.
Add artifactDigest to plugin bundles and plugin connector origins. Existing
origins without a digest and any digest/version/binding/config change become
disabled or approval_required; never copy allowTools automatically. Explicit
reconnect probes the new connector and records a new approval. Build stdio child
environments from a platform-specific allowlist (PATH, temp/home/locale and
Windows runtime essentials only) plus target.env; unknown parent variables are
absent.

Implementation plan

Step 1: Add deterministic artifact identity

Create a pure Effect-backed digest module with bounded traversal and canonical
encoding. Include all bundle files, including manifests and executables. Keep
errors path-relative and free of file contents.

Verify: digest tests cover stable reorder/mtime, content/mode/path changes,
empty bundle, nested files, symlink escape/cycle, and special files; all pass.

Step 2: Persist the digest in connector origin

Extend the Effect schema with an optional digest for backward decoding, require
it for newly resolved plugin connectors, and propagate the discovered digest.
Treat missing legacy digest as unapproved on next reconciliation.

Verify: schema tests decode legacy origins but runtime tests show they cannot
remain enabled without explicit reconnect.

Step 3: Fail closed on every plugin identity change

Replace automatic allowlist preservation with invalidation when digest, version,
binding, command, arguments, cwd, endpoint, headers, or declared environment
changes. Close pooled connections. Expose an actionable approval-required reason
without filesystem secrets.

Verify: tests mutate one identity field at a time and assert enabled: false,
no tool call, and explicit reconnect requirement.

Step 4: Build a minimal stdio environment

Extract a typed helper that copies only documented platform essentials from
process.env and overlays explicit connector env keys. Filter undefined values;
never retain an undeclared variable. Inject the env source/spawn function in
tests.

Verify: a sentinel parent secret is absent, explicit connector values are
present, and required POSIX/Windows bootstrap keys are covered by table tests.

Step 5: Run all gates and verify the packaged desktop

Run focused runtime, repository, and integration validation. The agent runtime
is bundled into the desktop app, so build desktop:dist, then perform the exact
canonical reinstall from AGENTS.md: remove /Applications/Local Studio.app,
copy frontend/dist-desktop/mac-arm64/Local Studio.app with ditto, remove the
legacy $HOME/Applications/local-studio-mac.app, terminate/re-register/relaunch
Local Studio, and request /api/desktop-health on its persisted embedded port.

Verify: npm --prefix services/agent-runtime run check && npm run check && npm run test:integration && npm --prefix frontend run desktop:dist && git diff --check → all exit 0; the reinstalled canonical app returns HTTP 200 with ok: true from /api/desktop-health.

Test plan

  • Same version with one modified file, mode, manifest, command, or env.
  • Version change with identical/different bytes; both require approval because
    approval origin changed.
  • Legacy origin without digest.
  • Contained and escaping symlinks, cycles, special files, deterministic ordering.
  • Parent env includes a sentinel secret, cloud credentials, and API keys; none
    reach spawn unless explicitly in target.env.
  • Reconnect records the new digest only after successful probe and user action.

Acceptance criteria

  • Every discovered plugin has deterministic artifact identity.
  • New plugin connector origins persist that identity.
  • Any artifact/config identity change disables prior approval.
  • No reconciliation silently carries allowTools forward.
  • Stdio children receive only minimal platform env plus explicit target env.
  • No test/log exposes environment values or plugin file contents.
  • Runtime, repository, and integration gates pass.
  • The canonical desktop app is rebuilt, reinstalled, relaunched, and healthy.

Risks and stop conditions

Stop and report if:

  • A supported bundle requires escaping symlinks or mutable files outside its root.
  • Existing connector persistence cannot represent approval-required state safely.
  • A required runtime depends on an undeclared parent variable; request an
    explicit allowlist decision rather than restoring full inheritance.
  • Digesting a legitimate bundle is unbounded without a product size policy.
  • The canonical desktop reinstall or /api/desktop-health verification fails.

Maintenance considerations

Artifact canonicalization is a security contract: version it before changing its
encoding. Reviewers should scrutinize legacy-origin handling and the negative env
test. audit proposal 034's installer must verify the same digest before activation.


Generated from Local Studio 2.0 main-branch audit proposal 039.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions