diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json
index b9f7155..219acb6 100644
--- a/.devcontainer/devcontainer-lock.json
+++ b/.devcontainer/devcontainer-lock.json
@@ -1,9 +1,3 @@
{
- "features": {
- "ghcr.io/devcontainers/features/docker-in-docker:4": {
- "version": "4.0.0",
- "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:4fa87399214366e320d489991769c4f3f461e1ffe461f54eea78a41b34945bb5",
- "integrity": "sha256:4fa87399214366e320d489991769c4f3f461e1ffe461f54eea78a41b34945bb5"
- }
- }
+ "features": {}
}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index a64368a..db89583 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -4,58 +4,33 @@
"name": "Node.js & TypeScript",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
- "features": {
- "ghcr.io/devcontainers/features/docker-in-docker:4": {},
- },
"runArgs": [
"--net=host"
],
- // REGRA: com --net=host todos os devcontainers da máquina compartilham a rede
- // do host — porta nova aqui deve evitar faixas já usadas por outros containers.
- // Convenção DESTE repo: gateways dos exemplos 36xx (invoke = apiPort + 10000
- // → 136xx), serverless-offline 3002-3003, dashboards dos exemplos 31xx,
- // consoles de validação dos exemplos 8620-8622, LocalStack 4567/4571-4572,
- // self engine 14566. Os defaults de produto (3100/4566/8000) continuam no
- // código, mas o dev neste container usa os overrides do lss.config.json da
- // raiz (3101/4567/8001) — por isso os defaults não são forwardados aqui.
+ // No features: nothing in this repo shells out to Docker — not the
+ // orchestrator, not the engine, not the test suites (`npm run
+ // test:integration` boots a real stack in-process).
+ //
+ // 14566 LSS — dashboard, REST API and AWS wire, all one listener
+ // 3101 Vite dev server for the dashboard (dev only)
+ // 8001 DynamoDB proxy (opt-in via enableDynamoProxy)
+ // 36xx per-service HTTP APIs · 136xx their invoke listeners
+ // 8622 validation console of examples/self-hosted (npm run console)
"forwardPorts": [
- // Core (dev deste repo)
- 3101, // Dashboard/API (lss.config.json raiz) e UI Vite dev
- 14566, // Self engine (AWS wire)
- 4567, // LocalStack (lss.config.json raiz)
- 8001, // Proxy DynamoDB (lss.config.json raiz, opt-in)
- // serverless-offline (localstack-ultimate)
- 3002, 3003,
- // Lambda API por serviço (exemplos)
- 3610, 3611, 3612, // localstack-free
- 3631, 3632, 3633, 3634, // self-hosted
- // Lambda Invoke por serviço (apiPort + 10000)
- 13610, 13611, 13612, // localstack-free
- 13631, 13632, 13633, 13634, // self-hosted
- // Dashboards dos exemplos (serverPort de cada lss.config.json)
- 3111, 3120, 3140,
- // Consoles de validação dos exemplos (npm run console)
- 8620, 8621, 8622,
- // LocalStack dos exemplos
- 4571, 4572
+ 14566,
+ 3101,
+ 8001,
+ 3631, 3632, 3633, 3634,
+ 13631, 13632, 13633, 13634,
+ 8622
],
"portsAttributes": {
- "3101": { "label": "LSS Dashboard/API (dev) / UI Vite" },
- "14566": { "label": "Self Engine (AWS wire)" },
- "4567": { "label": "LocalStack (config raiz)" },
- "8001": { "label": "DynamoDB Proxy (config raiz)" },
- "3002-3003": { "label": "serverless-offline (localstack-ultimate)" },
- "3610-3612": { "label": "Lambda API (localstack-free)" },
- "3631-3634": { "label": "Lambda API (self-hosted)" },
- "13610-13612": { "label": "Lambda Invoke (localstack-free)" },
- "13631-13634": { "label": "Lambda Invoke (self-hosted)" },
- "3111": { "label": "LSS Dashboard (localstack-ultimate)" },
- "3120": { "label": "LSS Dashboard (localstack-free)" },
- "3140": { "label": "LSS Dashboard (self-hosted)" },
- "8620": { "label": "Console de validação (localstack-free)" },
- "8621": { "label": "Console de validação (localstack-ultimate)" },
- "8622": { "label": "Console de validação (self-hosted)" },
- "4571-4572": { "label": "LocalStack (exemplos)" }
+ "14566": { "label": "LSS (dashboard + API + AWS wire)" },
+ "3101": { "label": "UI Vite dev server" },
+ "8001": { "label": "DynamoDB Proxy (opt-in)" },
+ "3631-3634": { "label": "Lambda API (example self-hosted)" },
+ "13631-13634": { "label": "Lambda Invoke (example self-hosted)" },
+ "8622": { "label": "Validation console (example self-hosted)" }
}
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml
new file mode 100644
index 0000000..9113906
--- /dev/null
+++ b/.github/workflows/pr-preview.yml
@@ -0,0 +1,180 @@
+name: PR Preview Release
+
+# Publishes every push on a pull request to npm under a per-PR dist-tag, so a
+# reviewer can install the branch instead of building it. The version is the
+# manifest version plus the PR number and the run number
+# (1.0.0 -> 1.0.0-pr.42.17), which is a semver PRERELEASE: it sorts BELOW the
+# real 1.0.0 and no default range (`^1.0.0`) will ever resolve to it.
+#
+# `latest` is never touched. `npm publish` writes the `latest` dist-tag unless
+# `--tag` is given, and it does NOT infer a different tag from a prerelease
+# version string — the explicit `--tag` below is the only thing keeping a
+# preview out of `npm i local-serverless-stack`.
+#
+# NOT `pull_request_target`, deliberately. That trigger runs the BASE repo's
+# workflow with access to secrets while checking out PR code, which would hand
+# NPM_TOKEN to anyone who can open a pull request. With plain `pull_request`,
+# GitHub withholds secrets from fork PRs — so the fork case is gated explicitly
+# below and skips with a clear reason instead of failing on a missing token.
+
+# `labeled`/`unlabeled` are in the list because the `publish-preview` label is
+# what gates this workflow: without them, applying the label would sit there
+# doing nothing until someone happened to push again.
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, labeled, unlabeled, closed]
+
+# One preview in flight per PR. Each run composes a unique version, so
+# cancelling a superseded run only means that version never lands.
+concurrency:
+ group: pr-preview-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ publish:
+ name: Publish preview to npm
+ # Two gates:
+ # - Same-repo branches only: a fork PR has no NPM_TOKEN by design.
+ # - The `publish-preview` label. Opt-in rather than every-push, because npm
+ # versions are PERMANENT — `npm unpublish` is restricted to the first 72
+ # hours, so a 30-push PR would otherwise leave 30 versions on the
+ # package's public list forever.
+ # The last clause keeps an unrelated label ("bug", "wontfix") from
+ # triggering a republish: on a `labeled` event, only the label we care about
+ # counts. On push/open/reopen the clause is vacuously true.
+ if: >-
+ github.event.action != 'closed'
+ && github.event.action != 'unlabeled'
+ && github.event.pull_request.head.repo.full_name == github.repository
+ && contains(github.event.pull_request.labels.*.name, 'publish-preview')
+ && (github.event.action != 'labeled' || github.event.label.name == 'publish-preview')
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout PR head
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Compose the preview version
+ id: version
+ run: |
+ set -euo pipefail
+ BASE=$(jq -r '.version' package.json)
+ # Drop any prerelease already on the manifest (1.0.0-rc.1 -> 1.0.0) so
+ # the suffix cannot stack into 1.0.0-rc.1-pr.42.17, which is a
+ # different — and confusingly lower — semver ordering.
+ BASE="${BASE%%-*}"
+ VERSION="${BASE}-pr.${{ github.event.pull_request.number }}.${{ github.run_number }}"
+ TAG="pr-${{ github.event.pull_request.number }}"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+ echo "Publishing $VERSION under dist-tag $TAG"
+
+ # Before the version is stamped: `npm ci` reconciles package.json against
+ # the lockfile, and the stamped preview version exists in neither.
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Stamp the preview version
+ run: npm pkg set version="${{ steps.version.outputs.version }}"
+
+ # `prepublishOnly` builds again on publish; running it here first turns a
+ # build failure into its own red step instead of burying it in the
+ # publish log.
+ - name: Build project
+ run: npm run build
+
+ - name: Publish to npm
+ run: npm publish --access public --tag "${{ steps.version.outputs.tag }}"
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Comment the published version on the PR
+ uses: actions/github-script@v7
+ env:
+ PREVIEW_VERSION: ${{ steps.version.outputs.version }}
+ PREVIEW_TAG: ${{ steps.version.outputs.tag }}
+ with:
+ script: |
+ // One sticky comment per PR, edited in place — a comment per push
+ // would bury the review conversation.
+ const marker = '';
+ const { PREVIEW_VERSION, PREVIEW_TAG } = process.env;
+ const sha = context.payload.pull_request.head.sha.slice(0, 7);
+
+ const body = [
+ marker,
+ `📦 **Published \`local-serverless-stack@${PREVIEW_VERSION}\`** for homologation.`,
+ '',
+ '```bash',
+ `npm i local-serverless-stack@${PREVIEW_TAG} # always the newest build of this PR`,
+ `npm i local-serverless-stack@${PREVIEW_VERSION} # pin this exact build`,
+ '```',
+ '',
+ `Built from ${sha} · dist-tag \`${PREVIEW_TAG}\` · **not** \`latest\`.`,
+ 'The tag is dropped when this PR closes or the `publish-preview` label is removed —',
+ 'the versions themselves stay on npm and remain installable by their exact number.',
+ ].join('\n');
+
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.payload.pull_request.number,
+ per_page: 100,
+ });
+ const existing = comments.find((c) => c.body?.includes(marker));
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.payload.pull_request.number,
+ body,
+ });
+ }
+
+ cleanup:
+ name: Drop the preview dist-tag
+ # The label is the switch, so turning it off retires the preview too —
+ # otherwise `@pr-42` would keep resolving to a build nobody is maintaining.
+ if: >-
+ github.event.pull_request.head.repo.full_name == github.repository
+ && (
+ github.event.action == 'closed'
+ || (github.event.action == 'unlabeled' && github.event.label.name == 'publish-preview')
+ )
+ runs-on: ubuntu-latest
+ steps:
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ registry-url: 'https://registry.npmjs.org'
+
+ # Non-fatal: a PR closed without ever publishing has no tag to remove.
+ # Removing the tag does not unpublish anything — the versions stay
+ # installable by their exact number.
+ - name: Remove the per-PR dist-tag
+ run: |
+ npm dist-tag rm local-serverless-stack "pr-${{ github.event.pull_request.number }}" \
+ || echo "No dist-tag pr-${{ github.event.pull_request.number }} to remove."
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index f71619a..4889c55 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -6,7 +6,6 @@ on:
- main
paths:
- 'package.json'
- - 'packages/*/package.json'
jobs:
check-version:
@@ -14,9 +13,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
root-changed: ${{ steps.check-root.outputs.changed }}
- plugin-changed: ${{ steps.check-plugin.outputs.changed }}
root-version: ${{ steps.get-root-version.outputs.version }}
- plugin-version: ${{ steps.get-plugin-version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -36,19 +33,6 @@ jobs:
echo "⏭️ Root package version unchanged: $CURRENT"
fi
- - name: Check plugin package version change
- id: check-plugin
- run: |
- CURRENT=$(git show HEAD:packages/serverless-plugin/package.json | jq -r '.version')
- PREVIOUS=$(git show HEAD~1:packages/serverless-plugin/package.json | jq -r '.version' || echo "0.0.0")
- if [ "$CURRENT" != "$PREVIOUS" ]; then
- echo "changed=true" >> $GITHUB_OUTPUT
- echo "✅ Plugin package version changed: $PREVIOUS -> $CURRENT"
- else
- echo "changed=false" >> $GITHUB_OUTPUT
- echo "⏭️ Plugin package version unchanged: $CURRENT"
- fi
-
- name: Get root package version
id: get-root-version
run: |
@@ -56,34 +40,6 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Root version: $VERSION"
- - name: Get plugin package version
- id: get-plugin-version
- run: |
- VERSION=$(jq -r '.version' packages/serverless-plugin/package.json)
- echo "version=$VERSION" >> $GITHUB_OUTPUT
- echo "Plugin version: $VERSION"
-
- # Skip integration tests - require Docker/LocalStack (not available in GitHub Actions)
- # Tests are run locally with: npm run test:coverage
- # test:
- # name: Run Tests
- # needs: check-version
- # if: needs.check-version.outputs.root-changed == 'true' || needs.check-version.outputs.plugin-changed == 'true'
- # runs-on: ubuntu-latest
- # steps:
- # - name: Checkout code
- # uses: actions/checkout@v4
- # - name: Setup Node.js
- # uses: actions/setup-node@v4
- # with:
- # node-version: '18'
- # - name: Install dependencies
- # run: npm ci
- # - name: Build project
- # run: npm run build
- # - name: Run tests
- # run: npm test
-
publish-root:
name: Publish Root Package
needs: check-version
@@ -109,29 +65,3 @@ jobs:
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
-
- publish-plugin:
- name: Publish Plugin Package
- needs: check-version
- if: needs.check-version.outputs.plugin-changed == 'true'
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- registry-url: 'https://registry.npmjs.org'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Build plugin
- run: npm run build -w packages/serverless-plugin
-
- - name: Publish plugin to NPM
- run: npm publish -w packages/serverless-plugin --access public
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 613956c..e06b307 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -30,13 +30,13 @@ jobs:
run: npm run test:coverage
integration:
- name: Integration Tests (LocalStack)
+ name: Integration Tests (self engine)
runs-on: ubuntu-latest
- timeout-minutes: 25
- # ubuntu-latest runners ship Docker; LocalStack runs as an unprivileged
- # container. Gated on the token: community images >= 2026.5 require it.
- env:
- LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
+ timeout-minutes: 15
+ # 1.0 removed the LocalStack backend: the engine runs in the orchestrator
+ # process, so this job needs no Docker, no service container and no secret.
+ # It is therefore unconditional — under 0.x it skipped itself whenever the
+ # LocalStack auth token was absent, which was most of the time.
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -48,19 +48,15 @@ jobs:
cache: 'npm'
- name: Install dependencies (root)
- if: env.LOCALSTACK_AUTH_TOKEN != ''
run: npm ci
- name: Install dependencies (sample-microservice fixture)
- if: env.LOCALSTACK_AUTH_TOKEN != ''
run: npm ci --prefix tests/integration/fixtures/sample-microservice
- name: Build project
- if: env.LOCALSTACK_AUTH_TOKEN != ''
run: npm run build
- name: Run integration tests
- if: env.LOCALSTACK_AUTH_TOKEN != ''
run: npm run test:integration
lint:
@@ -86,9 +82,6 @@ jobs:
- name: Check TypeScript (server)
run: npx tsc --project src/server/tsconfig.json --noEmit
- - name: Check TypeScript (plugin)
- run: npx tsc --project packages/serverless-plugin/tsconfig.json --noEmit
-
build:
name: Build Check
runs-on: ubuntu-latest
@@ -115,5 +108,4 @@ jobs:
test -d dist/ui
test -d dist/client
test -f dist/client/index.js
- test -d packages/serverless-plugin/dist
test -f bin/cli.js
diff --git a/.gitignore b/.gitignore
index 6bf42e1..89ecc16 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,9 @@ tests/.lss-client-integration/
# Cache
.cache/
.parcel-cache/
+
+# Vendor source packs downloaded for a generator, never committed. The AWS
+# Architecture Service Icons pack (41 MB) is unzipped here for
+# `npm run icons:aws`; only the curated output in src/ui/src/icons/aws/ is
+# versioned — see src/ui/ui-ux.md §3.
+temp/
diff --git a/.mcp.json b/.mcp.json
new file mode 100644
index 0000000..421344c
--- /dev/null
+++ b/.mcp.json
@@ -0,0 +1,8 @@
+{
+ "mcpServers": {
+ "lss": {
+ "command": "node",
+ "args": ["./bin/cli.js", "mcp"]
+ }
+ }
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index ff186d6..55102d6 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -8,7 +8,7 @@
// ------------------------------------------------------------------
{
"label": "build: all",
- "detail": "UI + server + client + plugin (npm run build)",
+ "detail": "UI + server + client + MCP (npm run build)",
"type": "npm",
"script": "build",
"group": { "kind": "build", "isDefault": true },
@@ -34,32 +34,13 @@
},
{
"label": "dev @ example self-hosted (watch, sem build)",
- "detail": "npm run dev lendo examples/self-hosted/lss.config.json — orquestrador forçado em :3100 (a UI dev espera essa porta); registre com LSS_DASHBOARD_PORT=3100 npm run register:all. UI em :3101",
+ "detail": "npm run dev lendo examples/self-hosted/lss.config.json — LSS em :14566 (dashboard, API e AWS wire), UI Vite em :3101",
"type": "shell",
- "command": "LSS_DASHBOARD_PORT=3100 LSS_CONFIG_PATH=examples/self-hosted/lss.config.json npm run dev",
+ "command": "LSS_CONFIG_PATH=examples/self-hosted/lss.config.json npm run dev",
"isBackground": true,
"icon": { "id": "play-circle", "color": "terminal.ansiCyan" },
"problemMatcher": []
},
- {
- "label": "dev @ example localstack-free (watch, sem build)",
- "detail": "npm run dev lendo examples/localstack-free/lss.config.json — orquestrador forçado em :3100 (a UI dev espera essa porta); exige Docker. UI em :3101",
- "type": "shell",
- "command": "LSS_DASHBOARD_PORT=3100 LSS_CONFIG_PATH=examples/localstack-free/lss.config.json npm run dev",
- "isBackground": true,
- "icon": { "id": "play-circle", "color": "terminal.ansiCyan" },
- "problemMatcher": []
- },
- {
- "label": "dev @ example localstack-ultimate (watch, sem build)",
- "detail": "Carrega examples/localstack-ultimate/.env (token) + lss.config.json e roda npm run dev — orquestrador forçado em :3100 (a UI dev espera essa porta); exige Docker. UI em :3101",
- "type": "shell",
- "command": "set -a; [ -f examples/localstack-ultimate/.env ] && . examples/localstack-ultimate/.env; set +a; LSS_DASHBOARD_PORT=3100 LSS_CONFIG_PATH=examples/localstack-ultimate/lss.config.json npm run dev",
- "isBackground": true,
- "icon": { "id": "play-circle", "color": "terminal.ansiCyan" },
- "problemMatcher": []
- },
-
// ------------------------------------------------------------------
// Tests & validation
// ------------------------------------------------------------------
@@ -90,8 +71,8 @@
"problemMatcher": []
},
{
- "label": "test: integration (Docker + LOCALSTACK_AUTH_TOKEN)",
- "detail": "Suite de integração com LocalStack real — exige Docker e token",
+ "label": "test: integration (self engine, no Docker)",
+ "detail": "Suite de integração ponta a ponta no self engine — sem Docker, sem token",
"type": "npm",
"script": "test:integration",
"group": "test",
@@ -117,9 +98,9 @@
},
{
"label": "typecheck: all projects",
- "detail": "tsc --noEmit em server/client/plugin + vue-tsc na UI",
+ "detail": "tsc --noEmit em server/client/mcp + vue-tsc na UI",
"type": "shell",
- "command": "npx tsc --noEmit -p src/server/tsconfig.json && npx tsc --noEmit -p src/client/tsconfig.json && npx tsc --noEmit -p packages/serverless-plugin/tsconfig.json && cd src/ui && npx vue-tsc --noEmit",
+ "command": "npx tsc --noEmit -p src/server/tsconfig.json && npx tsc --noEmit -p src/client/tsconfig.json && npx tsc --noEmit -p src/mcp/tsconfig.json && cd src/ui && npx vue-tsc --noEmit",
"group": "test",
"icon": { "id": "verified", "color": "terminal.ansiBlue" },
"problemMatcher": ["$tsc"]
@@ -148,7 +129,7 @@
},
{
"label": "example self-hosted: start (no Docker)",
- "detail": "Sobe o LSS com a engine própria na porta 14566",
+ "detail": "Sobe o LSS na 14566 — dashboard, REST API e AWS wire no mesmo listener",
"type": "shell",
"command": "npm run lss:start",
"options": { "cwd": "${workspaceFolder}/examples/self-hosted" },
@@ -214,126 +195,6 @@
],
"icon": { "id": "rocket", "color": "terminal.ansiYellow" },
"problemMatcher": []
- },
-
- // ------------------------------------------------------------------
- // Example: localstack-free (LocalStack community 4.0 via Docker)
- // ------------------------------------------------------------------
- {
- "label": "example localstack-free: start (Docker)",
- "detail": "Sobe o LSS com LocalStack community 4.0 gerenciado (exige Docker, sem token)",
- "type": "shell",
- "command": "npm run lss:start",
- "options": { "cwd": "${workspaceFolder}/examples/localstack-free" },
- "icon": { "id": "server-environment", "color": "terminal.ansiGreen" },
- "problemMatcher": []
- },
- {
- "label": "example localstack-free: register services",
- "type": "shell",
- "command": "npm run register:all",
- "options": { "cwd": "${workspaceFolder}/examples/localstack-free" },
- "icon": { "id": "cloud-upload", "color": "terminal.ansiBlue" },
- "problemMatcher": []
- },
- {
- "label": "example localstack-free: stop",
- "type": "shell",
- "command": "npm run lss:stop",
- "options": { "cwd": "${workspaceFolder}/examples/localstack-free" },
- "icon": { "id": "debug-stop", "color": "terminal.ansiRed" },
- "problemMatcher": []
- },
- {
- "label": "example localstack-free: restart (rebuild local)",
- "detail": "build: all > stop > start — o CLI roda o dist/ compilado, então este é o caminho para ver mudanças locais no exemplo",
- "dependsOrder": "sequence",
- "dependsOn": [
- "build: all",
- "example localstack-free: stop",
- "example localstack-free: start (Docker)"
- ],
- "icon": { "id": "refresh", "color": "terminal.ansiYellow" },
- "problemMatcher": []
- },
-
- // ------------------------------------------------------------------
- // Example: localstack-ultimate (LocalStack Pro via Docker + token)
- // ------------------------------------------------------------------
- {
- "label": "example localstack-ultimate: setup",
- "detail": "npm install no exemplo (SDKs + serverless-offline + plugin LSS)",
- "type": "shell",
- "command": "npm run setup",
- "options": { "cwd": "${workspaceFolder}/examples/localstack-ultimate" },
- "icon": { "id": "desktop-download", "color": "terminal.ansiBlue" },
- "problemMatcher": []
- },
- {
- "label": "example localstack-ultimate: start (Docker + token)",
- "detail": "Sobe o LSS com LocalStack Pro gerenciado — exige Docker e LOCALSTACK_AUTH_TOKEN obrigatório no .env",
- "type": "shell",
- "command": "npm run lss:start",
- "options": { "cwd": "${workspaceFolder}/examples/localstack-ultimate" },
- "icon": { "id": "server-environment", "color": "terminal.ansiGreen" },
- "problemMatcher": []
- },
- {
- "label": "example localstack-ultimate: offline (HTTP :3002)",
- "detail": "sls package + RedrivePolicy (wire-dlq) + serverless-offline — fica em primeiro plano, não encadeie em dependsOn",
- "type": "shell",
- "command": "npm run offline",
- "options": { "cwd": "${workspaceFolder}/examples/localstack-ultimate" },
- "isBackground": true,
- "icon": { "id": "play-circle", "color": "terminal.ansiCyan" },
- "problemMatcher": []
- },
- {
- "label": "example localstack-ultimate: seed",
- "detail": "Recarrega os seeds (Users/Orders/Sessions) no LocalStack Pro — LSS precisa estar de pé",
- "type": "shell",
- "command": "npm run seed",
- "options": { "cwd": "${workspaceFolder}/examples/localstack-ultimate" },
- "icon": { "id": "database", "color": "terminal.ansiYellow" },
- "problemMatcher": []
- },
- {
- "label": "example localstack-ultimate: drive demo",
- "detail": "POST /users e /orders (incl. ordem envenenada p/ DLQ) e lê /users e /audit em :3002",
- "type": "shell",
- "command": "curl -s -X POST http://localhost:3002/dev/users -H 'content-type: application/json' -d '{\"name\":\"Task Runner\",\"email\":\"task@example.com\"}' && echo && echo '--- USERS:' && curl -s http://localhost:3002/dev/users && echo && echo '--- ORDER (SQS -> DynamoDB -> EventBridge):' && curl -s -X POST http://localhost:3002/dev/orders -H 'content-type: application/json' -d '{\"userId\":\"u-alice\",\"items\":[{\"sku\":\"BOOK-02\",\"price\":42,\"qty\":1}]}' && echo && echo '--- POISON ORDER (total 0 -> DLQ apos 3 tentativas, ~30s):' && curl -s -X POST http://localhost:3002/dev/orders -H 'content-type: application/json' -d '{\"userId\":\"u-alice\",\"items\":[{\"sku\":\"BROKEN\",\"price\":0,\"qty\":1}]}' && echo && sleep 5 && echo '--- AUDIT:' && curl -s http://localhost:3002/dev/audit && echo",
- "options": { "cwd": "${workspaceFolder}/examples/localstack-ultimate" },
- "icon": { "id": "run-all", "color": "terminal.ansiCyan" },
- "problemMatcher": []
- },
- {
- "label": "example localstack-ultimate: drive catalog (OpenSearch)",
- "detail": "Indexa produtos e roda busca full-text, filtros e agregações — coleção aoss servida pelo sidecar do LSS (:14567)",
- "type": "shell",
- "command": "curl -s -X POST http://localhost:3002/dev/products -H 'content-type: application/json' -d '{\"name\":\"Wireless Mouse\",\"category\":\"peripherals\",\"price\":25,\"tags\":[\"usb\",\"wireless\"]}' && echo && curl -s -X POST http://localhost:3002/dev/products -H 'content-type: application/json' -d '{\"name\":\"Mechanical Keyboard\",\"category\":\"peripherals\",\"price\":90,\"tags\":[\"usb\"]}' && echo && curl -s -X POST http://localhost:3002/dev/products -H 'content-type: application/json' -d '{\"name\":\"USB Hub\",\"category\":\"accessories\",\"price\":15,\"tags\":[\"usb\"]}' && echo && echo '--- SEARCH q=wireless:' && curl -s 'http://localhost:3002/dev/search?q=wireless' && echo && echo '--- SEARCH category+maxPrice:' && curl -s 'http://localhost:3002/dev/search?category=peripherals&maxPrice=50' && echo && echo '--- STATS:' && curl -s http://localhost:3002/dev/stats && echo",
- "options": { "cwd": "${workspaceFolder}/examples/localstack-ultimate" },
- "icon": { "id": "search", "color": "terminal.ansiCyan" },
- "problemMatcher": []
- },
- {
- "label": "example localstack-ultimate: stop",
- "type": "shell",
- "command": "npm run lss:stop",
- "options": { "cwd": "${workspaceFolder}/examples/localstack-ultimate" },
- "icon": { "id": "debug-stop", "color": "terminal.ansiRed" },
- "problemMatcher": []
- },
- {
- "label": "example localstack-ultimate: restart (rebuild local)",
- "detail": "build: all > stop > start — o CLI roda o dist/ compilado, então este é o caminho para ver mudanças locais no exemplo",
- "dependsOrder": "sequence",
- "dependsOn": [
- "build: all",
- "example localstack-ultimate: stop",
- "example localstack-ultimate: start (Docker + token)"
- ],
- "icon": { "id": "refresh", "color": "terminal.ansiYellow" },
- "problemMatcher": []
}
]
}
diff --git a/AGENTS.md b/AGENTS.md
index 25da080..d6b90ba 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,14 +10,15 @@
LSS is a **local control plane for serverless development**. One orchestrator provisions and serves
every AWS resource your services declare, so a monorepo of 15+ microservices needs a single local
-stack instead of one LocalStack per service. It ships two interchangeable backends: the **self
-engine** (an in-process AWS emulator — no Docker, no token, the project's differentiator) and
-**LocalStack** (community or Pro). It also replaces `serverless-offline` with its own Lambda runtime
+stack instead of one emulator per service. The AWS provider is the **self engine**: an in-process
+emulator — no Docker, no container, no auth token — and since 1.0 the only engine LSS ships. It also
+replaces `serverless-offline` with its own Lambda runtime
and API Gateway emulation.
-Published as the npm package `local-serverless-stack` (CLI: `lss`), plus the workspace package
-`serverless-lss` (the Serverless Framework plugin). Node **>= 20**, CommonJS, npm workspaces
-(`packages/*`, `src/ui`).
+Published as the single npm package `local-serverless-stack` (CLI: `lss`). Services join the stack
+through the orchestrator itself — `lss scan` / `lss register`, the dashboard onboarding or
+`POST /api/services/register` — there is no Serverless Framework plugin anymore (the `serverless-lss`
+package was retired in 1.0). Node **>= 20**, CommonJS, npm workspaces (`src/ui`).
---
@@ -28,10 +29,10 @@ Published as the npm package `local-serverless-stack` (CLI: `lss`), plus the wor
| `src/server/` | The orchestrator: `index.ts` (boot), `routes/` (HTTP API), `services/` (registrar, CFN parser, provisioner, seeds, gateway/lambda managers), `runtime/` (Lambda workers), `dev/` |
| `src/server/engine/` | **The self engine.** `emulators/` (dynamodb, sqs, sns, s3, events, secretsmanager, opensearch, lambda-ctl, sts), `dispatch/` (stream-tailer, sqs-poller, scheduler, dispatcher), `store/` (JSONL snapshot + WAL), `http/` (router, sigv4, protocols), `bus.ts` |
| `src/client/` | `LssClient` — the programmatic API |
+| `src/mcp/` | MCP server (`lss mcp`) — the stack as tools for an AI coding agent |
| `src/ui/` | Vue 3 dashboard (own workspace) |
-| `packages/serverless-plugin/` | The `serverless-lss` plugin (separately versioned & published) |
-| `examples/` | `self-hosted` (self engine, no Docker), `localstack-free`, `localstack-ultimate` |
-| `tests/` | `unit/`, `integration/` (Docker + token gated), `fixtures/` |
+| `examples/` | `self-hosted` — four microservices on the engine (DynamoDB, SQS, S3, EventBridge, OpenSearch, Secrets) |
+| `tests/` | `unit/`, `integration/` (self engine, no Docker, no token), `fixtures/` |
| `docs/` | `FEATURES.md`, `SELF_ENGINE.md`, `CONFIGURATION.md`, `RELEASE.md`, PRDs |
---
@@ -63,14 +64,14 @@ it mirrors CI. Shell equivalent:
npm run lint \
&& npx tsc --noEmit -p src/server/tsconfig.json \
&& npx tsc --noEmit -p src/client/tsconfig.json \
- && npx tsc --noEmit -p packages/serverless-plugin/tsconfig.json \
+ && npx tsc --noEmit -p src/mcp/tsconfig.json \
&& (cd src/ui && npx vue-tsc --noEmit) \
&& npm run test:coverage \
&& npm run build
```
Running only `jest` + `server:build` is **not** sufficient — it misses lint, `vue-tsc`, and the
-UI/client/plugin builds.
+UI/client/MCP builds.
### 3. 100% coverage, globally
@@ -95,8 +96,6 @@ shipping the change, not an afterthought.
End-to-end proof lives in `examples/`, not in ad-hoc scripts:
- `examples/self-hosted` — four microservices on the self engine (DynamoDB, SQS, S3, EventBridge). The default target.
-- `examples/localstack-free` — API Gateway proxy, cross-service Lambda authorizers, shared bus, streams, S3 notifications.
-- `examples/localstack-ultimate` — Pro-only surfaces.
A new engine capability should gain (or extend) a fixture in one of these plus an assertion. When a
full boot e2e isn't practical, drive the real backend from a `tests/unit/engine/wire-*.test.ts`
@@ -107,12 +106,12 @@ instead — that's the established pattern.
## Commands
```bash
-npm run build # ui + server + client + plugin
+npm run build # ui + server + client + mcp
npm run server:build # tsc for orchestrator + engine only
npm run dev # tsx watch (server) + vite (UI)
npm test # jest, unit suite
npm run test:coverage # unit suite + the 100% gate
-npm run test:integration # needs Docker + LOCALSTACK_AUTH_TOKEN
+npm run test:integration # e2e on the self engine — no Docker, no token; run `npm run build` first
npm run lint # eslint (see the warning baseline below)
```
@@ -128,8 +127,10 @@ pipeline — the fastest way to see the self engine work).
`tests/unit/routes/…`. Match the neighbouring file's mocking style before inventing your own.
- `tests/unit/engine/wire-*.test.ts` drive a **real `SelfEngineBackend`** end to end (through the
AWS SDK) rather than mocks — use this shape when proving a provisioning/dispatch behaviour.
-- `tests/integration/` requires Docker and `LOCALSTACK_AUTH_TOKEN`; it skips cleanly without them, so
- never treat "it skipped" as "it passed".
+- `tests/integration/` boots an isolated orchestrator on the self engine (own ports + `stateDir`) and
+ drives it over HTTP. It runs **unconditionally** — no Docker, no auth token, nothing to gate on, so
+ a skip there is a real problem, not the old 0.x "no token, never mind". `client.test.ts` imports the
+ **built** `dist/client`, so run `npm run build` before `npm run test:integration`.
- `tests/fixtures/` holds committed CloudFormation templates and sample microservices.
## Code conventions
@@ -167,5 +168,6 @@ pipeline — the fastest way to see the self engine work).
under parallel load. They pass in isolation. Don't chase them as regressions of your change, but
don't use them as cover for a real failure either.
- CI (`.github/workflows/tests.yml`) runs unit + coverage gate, lint + tsc, build-artifact
- verification, and (token-gated) integration. `publish.yml` publishes to npm when the version
- changes — so a version bump is a release action, not routine housekeeping.
+ verification, and integration — the last one unconditionally, since 1.0 left nothing to gate it on.
+ `publish.yml` publishes to npm when the version changes — so a version bump is a release action,
+ not routine housekeeping.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 814e0ae..f5a4912 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,333 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.0.0] - 2026-07-31
+
+**LSS runs on one engine.** The LocalStack backend is gone — not deprecated, removed. The self
+engine, an in-process AWS emulator with no Docker, no container and no auth token, is what LSS is.
+Migration guide: [docs/MIGRATION-v1.md](docs/MIGRATION-v1.md); for a project already on
+`engine: "self"` it is a matter of deleting a few config keys.
+
+The trigger was evidence, not preference. Both shipped LocalStack examples were run end to end
+against the self engine first: `localstack-ultimate` (45 resources, `eu-west-1`) exercised REST
+payload-v1 routes, a DynamoDB composite key + GSI + stream, SQS with DLQ redrive at
+`maxReceiveCount: 3`, `SNS Publish` from a stream handler, an EventBridge bus with a pattern rule,
+a `rate(2 minutes)` schedule, an S3 notification with a prefix filter and an OpenSearch Serverless
+catalog — **every path fired, zero errors**. `localstack-free` added a REST v1 authorizer, an
+httpApi v2 simple-response authorizer written in TypeScript, and a cross-service authorizer
+resolved by ARN. Nothing the project supported on LocalStack needed LocalStack.
+
+### Removed
+- **The 0.x migration guards.** `engine: "localstack"` (file or `LSS_ENGINE`) and the retired
+ `--external` / `--pro` / `--self-engine` / `--localstack-token` flags no longer raise a dedicated
+ migration error — 1.0 has shipped, so an unknown key or flag is simply an unknown key or flag.
+ [docs/MIGRATION-v1.md](docs/MIGRATION-v1.md) still documents the move.
+- **The LocalStack backend**: `src/server/engine/backends/localstack-backend.ts`,
+ `src/server/services/localstack-manager.ts`, `src/server/engine/aoss-sidecar.ts` and the
+ `EngineBackend` interface. `EngineManager` now owns the one engine.
+- **Config keys**: `engine` (accepted only to reject a 0.x `"localstack"` with a migration error),
+ `mode`, `localstackPort`, `localstackEndpoint`, `localstackEdition`, `localstackVersion`,
+ `localstackImage`, `localstackAuthToken`, `services`, `aossSidecar`. Editing any of them through
+ `PUT /api/config` now answers `unknown config key`.
+- **Env vars**: `LSS_LOCALSTACK_*`, `LOCALSTACK_AUTH_TOKEN`, `LSS_SERVICES`. `LSS_ENGINE` survives
+ only to reject `localstack`.
+- **CLI flags**: `--self-engine`, `--external`, `--pro`, `--localstack-token`. Each exits 1 naming
+ the migration guide — checked before the already-running short-circuit, so a stale script fails
+ visibly instead of looking like a successful no-op.
+- **API fields**: `GET /api/health` → `localstack` (use `engineRunning`); `GET /api/config` →
+ the `localstack` block, `aossSidecar` and `services`. `LssClient`: `HealthStatus.localstack` and
+ `lifecycle.start({ external, pro, localstackToken })`.
+- **Examples**: `examples/localstack-free` and `examples/localstack-ultimate` (760 MB). Their raw
+ `AWS::ApiGatewayV2::*` cross-stack topology was preserved at
+ `tests/integration/fixtures/apigw-raw/`, where its end-to-end test still runs.
+- **The `serverless-lss` plugin package** (`packages/serverless-plugin/`, published separately as
+ `serverless-lss`): services no longer announce themselves from inside `sls package` — a bare
+ `POST /api/services/register { servicePath }` is a complete registration, because the orchestrator
+ packages on demand (`autoPackage`) and reads the service name, `provider.region` and `custom.lss`
+ ports from the packaged `serverless-state.json` itself. Every `plugins: - serverless-lss` entry
+ and `custom.orchestrator` block is dead config; `custom.lss` stays, now read server-side. The npm
+ workspace, the publish lane, the CI typecheck and the example/fixture wiring all went with it.
+- **`serverless-offline` compatibility**: `custom.serverless-offline.httpPort/lambdaPort` are no
+ longer read as port fallbacks (declare `custom.lss.apiPort`/`invokePort`), and nothing registers
+ on `sls offline` anymore — the retired plugin was the only piece that ever did. The LSS runtime
+ replaced offline's execution model back in 0.7.x; this removes the last vestige.
+
+### Added
+- **A live load panel on the Overview.** Which workers are actually resident (against the
+ `maxWarmWorkers` ceiling), what ran in the last 1/2/10 minutes, **how much of it overlapped**, and
+ what that costs the host — resident memory, free/total RAM, 1-minute load normalised by core
+ count. Backed by `GET /api/lambdas/activity` and a stack-wide, log-free ring of invocation spans
+ (`src/server/services/invocation-activity.ts`, capped at 1000): parallelism is reported as the
+ **peak** per time bucket, because an average hides exactly the burst that saturates a laptop. The
+ timeline puts service identity on the row axis rather than on colour (a 40-service monorepo has no
+ readable categorical palette), and a failed invocation carries a shape marker and a counted label
+ as well as the status colour — red/green sit at ΔE 4.4 under deuteranopia, so colour alone would
+ hide every error from a colourblind reader.
+- **One port for everything.** The dashboard, the REST API **and the AWS wire protocols** share one
+ listener on `14566` by default: requests carrying positive AWS evidence (SigV4 `Authorization`,
+ `X-Amz-Target`, any `x-amz-*` header, engine paths like `/_aoss` or `/2015-03-31/`) are demuxed to
+ the engine, everything else to the dashboard/API. Setting `selfEngine.port` different from
+ `serverPort` splits them back into two listeners. `AWS_ENDPOINT`, the dashboard URL and the
+ registration URL are now the same string.
+- **Guided onboarding.** First dashboard visit with no services registered opens a 3-step flow —
+ ports, branding (applied live), then a project scan where you tick services and take them from
+ freshly cloned to registered without a terminal: **Install selected** (`POST
+ /api/services/install`, default `npm install`, first token whitelisted), **Package selected**
+ (`POST /api/services/package`, the effective package command) and **Register selected**.
+ Per-service API/invoke ports and package commands are editable inline and persist to
+ `lss.config.json` as `serviceRuntime`/`servicePackaging` entries — `updateConfig` now merges
+ map blocks **per entry**, so saving one field never drops that entry's or another service's
+ siblings. Reopenable anytime from Settings. This is the plugin's replacement for humans.
+- **Three languages, dashboard and CLI**: English, Brazilian Portuguese and Spanish. The dashboard
+ follows a stored choice, else the browser (`pt` → `pt-BR`, `es-AR` → `es`), switchable from the ⋮
+ menu and remembered per browser; the CLI follows `LSS_LANG`, else `LC_ALL`/`LC_MESSAGES`/`LANG`.
+ Both layers are hand-rolled and dependency-free (`src/ui/src/i18n/`, `bin/i18n.js`) — LSS ships as
+ one npm package and a translation runtime is not a cost to put on every install. A missing key
+ falls back to English and then to the key itself, so an untranslated screen still renders. AWS
+ proper nouns, config keys, commands and flags are deliberately never translated. This also fixes a
+ smaller wart: parts of the CLI (the seed diagnostic, `lss scan`) printed Brazilian Portuguese
+ regardless of environment; the CLI is now English by default and Portuguese on request.
+- **Official AWS service icons in the dashboard.** 64 marks from AWS's own **Architecture Service
+ Icons** pack (the 16 variant — `viewBox 0 0 24 24`, which is TreeUI's icon grid) are vendored as
+ geometry under `src/ui/src/icons/aws/` and registered into the TreeUI icon registry with
+ `registerTreeIcons()`, plus a `TIconRegistry` augmentation so ``
+ typechecks like a built-in. 12 cover what LSS provides today (Lambda, DynamoDB, S3, SQS, SNS,
+ EventBridge, OpenSearch, Secrets Manager, API Gateway, CloudFormation, IAM — also standing for
+ STS — and CloudWatch — also standing for CloudWatch Logs); 52 more are a registered reserve. They
+ replace the generic glyphs on the sidebar, the Overview tiles and coverage rows, every
+ per-service resource breakdown, the Lambda trigger tags and each explorer's headers and empty
+ states — a screen listing eight AWS services no longer distinguishes them by a database/inbox/
+ target vocabulary. The artwork is an AWS trademark, reproduced unmodified and deliberately
+ theme-blind (`src/ui/src/icons/aws/NOTICE.md`); the ~41 MB pack is not committed, and
+ `npm run icons:aws` regenerates the checked-in output from it.
+- **Service discovery**: `GET /api/services/scan` + `lss scan` walk the project root (depth ≤ 6,
+ dependency/build/VCS trees skipped, a service root is a leaf) and report every Serverless/osls
+ service with `installed`/`packaged`/`registered` flags, the effective ports and package command
+ (`serviceRuntime`/`servicePackaging` overlays win over the yml hints — the same precedence
+ registration applies) — hints only, the packaged state stays the authority at register time. `lss register [path...]` (defaults to
+ `.`) is the CLI replacement for automation; `LssClient.services.scan()` and the MCP tools
+ `lss_scan_services`/`lss_register_service` expose the same pair to code and to agents.
+- **MCP server** (`lss mcp`, `src/mcp/`): the running stack as **25 tools** for any Model Context
+ Protocol client (Claude Code included) — inspect resources/queues/tables/buckets/secrets, scan
+ and register services, invoke Lambdas, send messages, block on `await-idle`. Hand-rolled JSON-RPC
+ 2.0 over stdio, zero new dependencies, off until a client is configured ([docs/MCP.md](docs/MCP.md)).
+- **Lambdas are lazy by default**: `lambdaRuntime.lazy` forks a worker on first invocation instead
+ of at registration, `idleTimeoutMs` (60 s) unloads it when quiet, `maxWarmWorkers` (one per GB of
+ RAM, clamped 2–12) caps residency. Measured on 40 services / 400 lambdas / 400 tables:
+ **2.0 GB → 128 MB** at rest, ~20 ms cold start. The host never chokes on a monorepo again; slow
+ is acceptable, swapping is not.
+- **`LSS_ENGINE_DATA_DIR`**: the engine data directory as an env var, so each LSS instance gets its
+ own store with no config file. A second instance needs only
+ `LSS_DASHBOARD_PORT` + `LSS_ENGINE_PORT` + `LSS_ENGINE_DATA_DIR`.
+
+### Changed
+- **The integration suite runs everywhere.** It boots an isolated orchestrator on the self engine
+ instead of a LocalStack container, so it needs no Docker and no secret: **19 end-to-end
+ assertions in ~20 s**, unconditional locally and in CI. Under 0.x the same suite skipped itself
+ whenever the LocalStack auth token was absent — which was most of the time, meaning the project's
+ only end-to-end coverage usually did not run.
+- OpenSearch Serverless is served natively by the engine on its own endpoint; the sidecar that
+ existed only because no LocalStack edition provides `aoss` is gone.
+- `lambdaRuntime.invokeHost` defaults to `127.0.0.1` (nothing runs in a container).
+- The dashboard, the CLI and every log line name the engine instead of LocalStack.
+
+### Security
+- **`POST /api/services/:name/start` was an arbitrary command runner — and it shipped.** This is not
+ a new-in-1.0 defect: the endpoint is present in **0.17.2 and every earlier release**, so any
+ install of those versions carries it. The handler read `command`, `args`, `cwd` and `env` straight
+ off the request body and passed them to `spawn()`; the only check was a four-entry command
+ allowlist that contained `node` and `npx`, and nothing at all validated the rest. So
+ `{"command":"node","args":["-e",""],"cwd":"/"}` ran that JavaScript as the user running
+ the orchestrator, in any directory on the host, with an attacker-chosen environment — the classic
+ full-host compromise, not a sandbox escape. No caller ever sent those fields (the dashboard calls
+ `startService(name)` with no payload, and the CLI has no equivalent), so the whole input surface
+ was reachable only by an attacker. `args`, `cwd` and `env` are now **derived by the server**: a
+ start runs the registered service's own npm start script from the root recorded in that service's
+ cached metadata, and the command allowlist is package managers only — `node` and `npx` are gone,
+ since neither can express "run this service's start script" without also expressing "run this
+ arbitrary program". The request body no longer contributes argv, working directory or environment
+ at all, which is the only shape of this endpoint that is safe to expose on a port with no
+ authentication.
+- **The orchestrator was reachable by anything, from anywhere.** It bound every interface
+ (`listen(PORT)` with no host argument) and answered every origin with
+ `Access-Control-Allow-Origin: *`, with no authentication on any route. That turned the endpoint
+ above from "local privilege you already had" into two remote paths: anyone on the same LAN, VPN or
+ hotel Wi-Fi could POST to it directly, and **any web page you happened to visit** could do it from
+ your own browser — a simple JSON `POST` clears the permissive preflight, and the service name it
+ needs is served, unauthenticated and cross-origin, by `GET /api/services`. The listener now binds
+ **`127.0.0.1` by default**, and CORS defaults to loopback origins (`http://localhost`,
+ `http://127.0.0.1`, `http://[::1]`, any port — the Vite dev dashboard is the only cross-origin
+ caller a default install has), so a page on the internet gets no CORS headers and its preflight
+ fails before the real request is ever sent. Requests without an `Origin` — curl, the CLI,
+ `LssClient`, the MCP server, an AWS SDK — are not browser cross-origin requests and are
+ unaffected. Both halves are **deliberately widenable**, because a loopback-only stack would break
+ the layout LSS is actually used in — see the next bullet. Both are env-only by design:
+ `PUT /api/config` can set neither, because widening a boundary through the API it protects would
+ hand the exposure back to whoever already reached the API.
+- **New: `LSS_CORS_ORIGINS`, and `LSS_BIND_HOST` now means the whole process.** Hardening the
+ default is only half the job; the other half is that the safe default must have a documented way
+ out, or people paste `--disable-web-security` into a browser flag and everyone loses. The common
+ real layout is *LSS in a container, browser on the host, and the developer's own frontends calling
+ LSS directly* — inspecting queues, hitting the emulated API Gateway, invoking Lambdas. A loopback
+ bind refuses the published port, and a loopback-only origin list refuses the frontend. So the
+ opt-in is now **one coherent line** that turns network access on everywhere at once:
+ `LSS_BIND_HOST=0.0.0.0 LSS_CORS_ORIGINS='*' lss start` — or, better, with the origins named:
+ `LSS_CORS_ORIGINS=http://localhost:5173,http://192.168.1.20:5173`. `LSS_CORS_ORIGINS` takes a
+ comma-separated list of exact origins (case and trailing slash forgiven, since a hand-typed env
+ var that silently matches nothing is the worst failure mode for a knob whose job is to unblock a
+ frontend) or a single `*`; unset keeps the loopback default; setting it **replaces** that default
+ rather than extending it. `LSS_BIND_HOST` covers **every** listener the process opens — the
+ orchestrator, each service's API Gateway and Lambda Invoke API port, the split-listener engine and
+ the DynamoDB proxy — so widening is one decision, not four, and there is no half-open state where
+ the dashboard is fenced and the invoke ports are not. Boot prints a single warning naming whichever
+ knobs were widened and how to undo them; an explicit origin list prints nothing, because naming
+ your callers is the outcome the variable is trying to encourage. What widening costs is now a
+ bounded, statable thing: an unauthenticated API on the network, where callers can read and write
+ your local emulator data and read secret values out of the emulated Secrets Manager. The
+ arbitrary-binary paths are gone in this same release — `/start` derives argv, cwd and env
+ server-side, `/install` is shape- and flag-allowlisted, and `packageCommand`/`packageArgs` must
+ match the packaging grammar — so the residual is data exposure in a local dev stack rather than a
+ shell on the host. It is not zero: a caller who can reach the API can still ask the project's own
+ build to run (`npm run
-