Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 26 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ MAKEFLAGS += -j
endif

# Common esbuild flags for CLI API bundle (ESM format for trpc-cli)
ESBUILD_CLI_FLAGS := --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:jsonc-parser --external:@trpc/server --external:ssh2 --external:cpu-features --external:@1password/sdk --external:@1password/sdk-core --banner:js="import{createRequire}from'module';globalThis.require=createRequire(import.meta.url);"
ESBUILD_CLI_FLAGS := --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:jsonc-parser --external:@trpc/server --external:ssh2 --external:cpu-features --external:typescript --external:@1password/sdk --external:@1password/sdk-core --banner:js="import{createRequire}from'module';globalThis.require=createRequire(import.meta.url);"

# Common esbuild flags for server runtime Docker bundle.
# Place runtime bundles under dist/runtime so frontend dist/*.js layers remain stable.
Expand All @@ -71,6 +71,7 @@ include fmt.mk
.PHONY: benchmark-terminal
.PHONY: ensure-deps rebuild-native mux
.PHONY: check-eager-imports check-bundle-size check-startup
.PHONY: bundled-extensions-validate bundled-extensions-build bundled-extensions-assemble

# Build tools
TSGO := bun run node_modules/@typescript/native-preview/bin/tsgo.js
Expand Down Expand Up @@ -130,7 +131,7 @@ rebuild-native: node_modules/.installed ## Rebuild native modules (node-pty, Duc
@echo "Native modules rebuilt successfully"

# Run compiled CLI with trailing arguments (builds only if missing)
mux: ## Run the compiled mux CLI (e.g., make mux server --port 3000)
mux: bundled-extensions-assemble ## Run the compiled mux CLI (e.g., make mux server --port 3000)
@test -f dist/cli/index.js -a -f dist/cli/api.mjs || $(MAKE) build-main
@node dist/cli/index.js $(filter-out $@,$(MAKECMDGOALS))

Expand All @@ -149,7 +150,7 @@ help: ## Show this help message

## Development
ifeq ($(OS),Windows_NT)
dev: node_modules/.installed build-main ## Start development server (Vite + nodemon watcher for Windows compatibility)
dev: node_modules/.installed build-main bundled-extensions-assemble ## Start development server (Vite + nodemon watcher for Windows compatibility)
@echo "Starting dev mode (3 watchers: nodemon for main process, esbuild for api, vite for renderer)..."
# On Windows, use npm run because bunx doesn't correctly pass arguments to concurrently
# https://github.com/oven-sh/bun/issues/18275
Expand All @@ -159,15 +160,15 @@ dev: node_modules/.installed build-main ## Start development server (Vite + node
'npx esbuild src/cli/api.ts $(ESBUILD_CLI_FLAGS) --watch' \
"vite"
else
dev: node_modules/.installed build-main build-preload ## Start development server (Vite + tsgo watcher for 10x faster type checking)
dev: node_modules/.installed build-main build-preload bundled-extensions-assemble ## Start development server (Vite + tsgo watcher for 10x faster type checking)
@bun x concurrently -k \
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
'bun x esbuild src/cli/api.ts $(ESBUILD_CLI_FLAGS) --watch' \
"vite"
endif

ifeq ($(OS),Windows_NT)
dev-server: node_modules/.installed build-main ## Start server mode with hot reload (backend :3000 + frontend :5173). Use VITE_HOST=0.0.0.0 VITE_ALLOWED_HOSTS=<public-host> for remote access
dev-server: node_modules/.installed build-main bundled-extensions-assemble ## Start server mode with hot reload (backend :3000 + frontend :5173). Use VITE_HOST=0.0.0.0 VITE_ALLOWED_HOSTS=<public-host> for remote access
@echo "Starting dev-server..."
@echo " Backend (IPC/WebSocket): http://$(or $(BACKEND_HOST),127.0.0.1):$(or $(BACKEND_PORT),3000)"
@echo " Frontend (with HMR): http://$(or $(VITE_HOST),localhost):$(or $(VITE_PORT),5173)"
Expand All @@ -180,7 +181,7 @@ dev-server: node_modules/.installed build-main ## Start server mode with hot rel
"set NODE_ENV=development&& nodemon --watch dist/cli/index.js --watch dist/cli/server.js --delay 500ms dist/cli/index.js server --no-auth --host $(or $(BACKEND_HOST),127.0.0.1) --port $(or $(BACKEND_PORT),3000)" \
"set MUX_VITE_HOST=$(or $(VITE_HOST),127.0.0.1)&& set MUX_VITE_PORT=$(or $(VITE_PORT),5173)&& set MUX_VITE_ALLOWED_HOSTS=$(VITE_ALLOWED_HOSTS)&& set MUX_BACKEND_PORT=$(or $(BACKEND_PORT),3000)&& vite"
else
dev-server: node_modules/.installed build-main ## Start server mode with hot reload (backend :3000 + frontend :5173). Use VITE_HOST=0.0.0.0 VITE_ALLOWED_HOSTS=<public-host> for remote access
dev-server: node_modules/.installed build-main bundled-extensions-assemble ## Start server mode with hot reload (backend :3000 + frontend :5173). Use VITE_HOST=0.0.0.0 VITE_ALLOWED_HOSTS=<public-host> for remote access
@echo "Starting dev-server..."
@echo " Backend (IPC/WebSocket): http://$(or $(BACKEND_HOST),127.0.0.1):$(or $(BACKEND_PORT),3000)"
@echo " Frontend (with HMR): http://$(or $(VITE_HOST),localhost):$(or $(VITE_PORT),5173)"
Expand All @@ -202,11 +203,24 @@ dev-desktop-sandbox: ## Start an isolated Electron dev instance (fresh MUX_ROOT
dev-server-sandbox: ## Start an isolated dev-server instance (fresh MUX_ROOT + free ports)
@bun scripts/dev-server-sandbox.ts $(DEV_SERVER_SANDBOX_ARGS)

start: node_modules/.installed build-main build-preload build-static ## Build and start Electron app
start: node_modules/.installed build-main build-preload build-static bundled-extensions-assemble ## Build and start Electron app
@NODE_ENV=development bunx electron --remote-debugging-port=9222 .

## Bundled Extensions pipeline
# Source the bundled-extensions script for validate/build/assemble. Outputs land
# under build/extensions/ (the "<build>/extensions/" path electron-builder will
# pick up via extraResources in US-020).
bundled-extensions-validate: node_modules/.installed ## Validate each packages/<name> manifest via the production Manifest Validator
@bun scripts/bundled-extensions.ts validate

bundled-extensions-build: node_modules/.installed ## Run each bundled extension package's build script (no-op when absent)
@bun scripts/bundled-extensions.ts build

bundled-extensions-assemble: node_modules/.installed bundled-extensions-validate bundled-extensions-build ## Pack and extract bundled extensions into build/extensions/ (deterministic, offline)
@bun scripts/bundled-extensions.ts assemble

## Build targets (can run in parallel)
build: node_modules/.installed src/version.ts build-renderer build-main build-preload build-icons build-static ## Build all targets
build: node_modules/.installed src/version.ts build-renderer build-main build-preload build-icons build-static bundled-extensions-assemble ## Build all targets

build-main: node_modules/.installed dist/cli/index.js dist/cli/api.mjs ## Build main process

Expand Down Expand Up @@ -327,7 +341,7 @@ build/icon.png: docs/img/logo-white.svg scripts/generate-icons.ts
## Quality checks (can run in parallel)
# Keep the default local path fast. Docs link crawling and lockfile-free bench-agent
# verification stay in static-check-full so local validation remains responsive.
static-check: lint typecheck fmt-check check-eager-imports check-code-docs-links lint-shellcheck lint-hadolint ## Run fast local static checks
static-check: lint typecheck fmt-check check-eager-imports check-code-docs-links lint-shellcheck lint-hadolint bundled-extensions-validate ## Run fast local static checks

static-check-full: static-check check-bench-agent check-docs-links ## Run the full CI static check suite

Expand Down Expand Up @@ -433,11 +447,11 @@ check-deadcode: node_modules/.installed ## Check for potential dead code (manual
|| echo "✓ No obvious dead code found"

## Testing
test-integration: node_modules/.installed build-main ## Run all tests (unit + integration)
test-integration: node_modules/.installed build-main bundled-extensions-assemble ## Run all tests (unit + integration)
@bun test src
@TEST_INTEGRATION=1 bun x jest tests

test-unit: node_modules/.installed build-main ## Run unit tests
test-unit: node_modules/.installed build-main bundled-extensions-assemble ## Run unit tests
@bun test src
@bun test ./tests/ui/storybook/

Expand Down Expand Up @@ -597,7 +611,7 @@ benchmark-terminal: ## Run Terminal-Bench 2.0 with Harbor (use TB_HARBOR_PACKAGE
## Clean
clean: ## Clean build artifacts
@echo "Cleaning build artifacts..."
@rm -rf dist release build/icon.icns build/icon.png
@rm -rf dist release build/icon.icns build/icon.png build/extensions
@echo "Done!"

## Startup Performance Checks
Expand Down
25 changes: 25 additions & 0 deletions docs/adr/0001-permissions-are-requests-not-grants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Effect capabilities require local approval; registration capabilities are auto-approved

Extension Modules declare capability classes in their statically extracted `manifest.capabilities` object. Those declarations are author-controlled requests, not authority. V1 splits capabilities into **Registration Capabilities** and **Effect Capabilities**. Registration Capabilities, initially only `skills`, are auto-approved after root trust and enablement because they only register host-validated descriptors. Effect Capabilities such as shell, network, secrets, workspace files, git, or model access require explicit **Capability Approval** stored in Mux-controlled local state outside project repositories.

## Considered Options

- **Manifest capabilities are grants.** Rejected: a repository or downloaded source could self-authorize dangerous host APIs by changing `extension.ts`.
- **Keep the package-prototype `requestedPermissions` / Grant Record model.** Rejected: it was designed around package manifests, inferred contribution permissions, and distribution identity drift. Extension Modules need a smaller capability-class model where `ctx` is the enforcement boundary.
- **Require approval for all registration capabilities.** Rejected for skills-first v1 because skill registration is host-validated, contained, and already lower precedence than user project/global skills.
- **Trust + enable grants every declared capability.** Rejected: project-local code and fetched source must not receive shell/network/secrets authority just because a root was trusted for inspection.

## Decision

- The Static Manifest declares capability classes.
- Registration Capability use must be declared, but is auto-approved after trust + enablement.
- Effect Capability use requires a local Capability Approval scoped by root/project/global scope and Extension Name.
- Unapproved Effect Capability namespaces remain visible on `ctx` with `requested`, `approved`, `available`, and `reason` metadata, and throw typed errors when used.
- Capability drift is based on requested Effect Capability expansion/strengthening. Source or content changes alone do not revoke existing approvals.

## Consequences

- V1 can remove package-version and package-rename regrant churn.
- The Settings UI must distinguish requested/approved/unavailable Effect Capabilities from auto-approved registration capabilities.
- Project repositories may declare source locks, but cannot commit approvals or trust decisions.
- The first implementation can ship with only `capabilities.skills = true` and no dangerous Effect Capability API.
26 changes: 26 additions & 0 deletions docs/adr/0002-stable-v1-excludes-code-execution-surfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Extension code executes only after trust in a QuickJS-based host

The package prototype excluded extension-authored code execution. The Extension Module architecture intentionally introduces `activate(ctx)`, but only after a trust ladder: pre-trust project-local roots are existence-only, static manifests are extracted without execution, Registration Discovery runs only after root trust, and Full Activation runs only after trust, enablement, and applicable approvals.

## Considered Options

- **Keep v1 purely declarative.** Rejected: the new folder/`extension.ts` model is meant to support Mux-native authoring, hot reload, and contribution registration without duplicating every contribution in a manifest.
- **Execute `extension.ts` in Node/Electron.** Rejected: extension code must not inherit ambient Node, filesystem, process, network, or renderer authority.
- **Run sandboxed Registration Discovery before trust.** Rejected: even no-op registration collection still executes extension code and can burn CPU, throw, or use any sandbox bug before user consent.
- **Require static contribution lists in the manifest.** Rejected: it duplicates registration code and diverges from the desired `activate(ctx)` authoring model.

## Decision

- `extension.ts` exports a statically extractable `manifest` and may export `activate(ctx)`.
- Mux never executes `extension.ts` before the relevant root is trusted.
- After trust, Mux runs **Registration Discovery** in QuickJS with collector registration APIs and unavailable effect APIs.
- After enablement and approvals, Mux runs **Full Activation** in a long-lived QuickJS Extension Host Session.
- Full Activation may publish only contributions observed during Registration Discovery.
- `activate(ctx)` may be async, but activation is bounded by timeouts and atomic cleanup.

## Consequences

- The PTC QuickJS runtime concepts are reusable, but an extension-host layer must add TypeScript bundling, `mux:*` virtual modules, export handling, retained handler invocation, and lifecycle/disposal management.
- Registration Discovery is a contribution contract, not a side-effect permission grant.
- Failed activation must dispose partial registrations and keep last-good activation during hot reload unless trust/capability revocation requires shutdown.
- Static analysis remains important for forbidden imports/globals, but v1 does not try to prove top-level purity.
25 changes: 25 additions & 0 deletions docs/adr/0003-extension-identity-vs-distribution-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Extension identity is the folder name; source identity lives in locks

The package prototype used a reverse-domain `mux.id` and separate npm Distribution Identity. Extension Modules follow the existing agent skill model instead: an extension is identified by its folder basename, and `manifest.name` in `extension.ts` is required to match that folder. Source provenance, git refs, SHAs, and content hashes live in lock/store metadata rather than the manifest.

## Considered Options

- **Keep reverse-domain `mux.id`.** Rejected: it conflicts with the desired skills-like folder workflow and makes local authoring heavier than necessary.
- **Use git URL or content hash as identity.** Rejected: identity would change across forks, mirrors, local edits, and lock updates. Source identity is provenance, not user-facing extension identity.
- **Use a composite folder + manifest ID.** Rejected: it adds complexity while still making folder rename semantics unclear.
- **Allow manifest name to differ from folder name.** Rejected: Mux skills already require frontmatter `name` to match the parent directory, and extensions should follow that convention.

## Decision

- The **Extension Name** is the kebab-case folder basename.
- `manifest.name` is required and must match the folder name.
- There is no required manifest version and no npm package identity.
- Git/source provenance is represented by **Source Identity** in global/project lock files and the content-addressed store.
- Duplicate Extension Names across roots use skill-like precedence: project-local shadows user-global, which shadows bundled. Core bundled names may be reserved and non-shadowable.

## Consequences

- Renaming an extension folder intentionally renames the extension; old state can be surfaced as stale local state.
- Install commands must parse `manifest.name` before choosing the target active name.
- Lock files key source entries by Extension Name but do not confer trust.
- Project-local extensions cannot inherit global approvals merely by using the same name because approvals are scoped by root/project/global scope.
24 changes: 24 additions & 0 deletions docs/adr/0004-v1-is-an-additive-platform-with-a-demo-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# v1 is an additive skills-only Extension Module release with a bundled demo

Stable v1 ships the folder-based Extension Module platform, not migrations of existing built-in features. The bundled extension set contains a single non-core demo Extension Module that registers one skill through `activate(ctx)`. The platform is always initialized because future built-in skill migrations depend on extension-contributed skills remaining available.

## Considered Options

- **Migration-first v1.** Rejected: moving built-in themes, layouts, runtimes, agents, tools, or secrets to the new host would combine platform risk with migration risk.
- **Ship commands/effect APIs in v1.** Rejected: commands and side-effect APIs require handler invocation, approval UX, and stronger runtime semantics. Skills are enough to validate the source/discovery/activation path.
- **Keep the npm/package prototype as a compatibility path.** Rejected: the feature is experimental and unmerged; carrying two architectures would confuse authors and double the security surface.
- **No demo extension.** Rejected: without a bundled demo, shipped builds would not exercise the end-to-end platform path by default.
- **Expose a platform kill switch.** Rejected: built-in skills may migrate onto Extensions, so a user-facing off switch would remove core functionality and create a degraded experience.

## Decision

- V1 contribution support is skills only.
- The Platform Demo Extension is an Extension Module folder with `extension.ts`, `manifest.name`, `capabilities.skills = true`, and `ctx.skills.register(...)`.
- No existing built-in feature is migrated in v1.
- The Extension Platform has no experiment or Governor kill switch; discovery, Registration Discovery, Full Activation, Settings UI, and skill integration are always available.

## Consequences

- Existing Mux behavior remains unchanged for users who never enable or install third-party extensions.
- Tests and dogfood focus on local/global/project extension source, trust gating, skill registration, shadowing, and always-on availability.
- Future releases can add commands, effect APIs, setup scripts, catalogs, immutable local snapshots, and built-in migrations through separate design passes.
Loading
Loading