v1.8.0
Security
- The Tools API no longer serves anonymous requests.
authMiddlewarecontained
if (!apiKey) return;— and no key was configured on any documented install path, so
every route was open, including the threePOST /api/v1/executor/*routes that run
commands. The bypass is deleted. A key is now required on every non-public route;
/health,/swagger,/swagger/json,/uiand/ui/stay public (the Docker
healthcheck depends on/health, and/uiserves only a static shell whose every data
call is still authenticated). - A key is auto-provisioned on first start, so existing installs keep working. The
precedence chain is the documented one:MASSA_AI_API_KEYenv >security.apiKeyin
~/.config/massa-ai/config.json> a freshly generated 32-byte hex key persisted there.
The path is logged once at startup; the value never is. The API refuses to start only
when no key exists and the config file cannot be written. Concurrent cold starts elect
a single writer through an atomic exclusive-create lock, so every process agrees on the
key that is actually on disk — re-reading after a write was implemented first and proven
insufficient, since any read-then-return is invalidated by a later writer. - CORS is an explicit allowlist instead of a wildcard.
.use(cors())accepted every
origin. Origins now come fromMASSA_AI_API_CORS_ORIGINS/security.corsOrigins;
unset means{ origin: false, credentials: false }, and*combined with credentials is
rejected at startup rather than served. - The Web UI authenticates itself without a new login surface.
/uireceives the key
in a<meta name="massa-ai-api-key">tag only when the request's remote address is
loopback; other callers get the shell plus a configure-access state. Loopback has three
spellings on this stack (127.0.0.0/8,::1,::ffff:127.0.0.1) and all three are
accepted. Known limitation: a reverse proxy terminating on loopback appears local.
Seedocs/web-ui-access.md. - Every documented install path now provisions a key, including the lifecycle hook
binary, which readsconfig.jsondirectly rather than importing@massa-ai/shared(it
is fire-and-forget and silent-degrading, so a 401 would have stopped observation capture
with no visible symptom). Two further defects surfaced here:setup-local-first.sh
regeneratedconfig.jsonwholesale and so silently rotated the credential on every
re-run, and the composemcpservice had neither a volume nor a key, so on a default
docker compose upit could not learn the key theapiservice provisioned. Both images
now setXDG_CONFIG_HOME=/datasoconfig.jsonlands inside the mounted volume. - The inert admin-preservation middleware is removed. It gated six endpoints on a user
count that was always zero, so it never denied anything. Those endpoints are protected by
the mandatory key like every other route, and a parameterised test asserts 401 for each.
Changed
-
BREAKING — embedding failures now throw instead of returning random vectors. With no
reachable provider,EmbeddingServicereturnedMath.random()vectors under a
NODE_ENVcheck, andgetDimensions()fell back to 384. Those vectors were stored and
searched as if they were real, so retrieval silently returned nonsense. Both branches now
throw;store_memoryandupdate_memoryreturn{ success: false }, and HyDE degrades
through the existingQUERY_UNDERSTANDING_UNAVAILABLEsignal.Action required if you ever ran without a reachable embedding provider. A 384-d
random vector is indistinguishable from a real embedding after the fact, so there is no
detector and no repair: re-index affected projects (reindexwithforce: true) to
overwrite them. Memories stored during such a window cannot be recovered by re-indexing
and need to be re-created. -
Executor responses report the sandbox mode that was actually used.
MASSA_AI_EXECUTOR_SANDBOX=autosilently degrades tononewhen no sandbox tool is
present, and nothing in the response said so. Everyexecute,execute_fileand
batch_executeresult now carriessandboxMode, and one warning per process names the
missing tool whenautoresolves tonone. An explicitnonewarns nothing. Theauto
default and its best-effort fallback are unchanged (AD-007).
Fixed
- Graph-neighbor search results are scoped to the searched project.
memory_edgeshas
noproject_id, so BFS walks edges globally; the graph stream checked onlydeleted_at
before pushing a neighbor's content into the result set, letting a single cross-project
edge surface another project's memory. The read seam now filters by project, using the
caller's id unresolved and skipping when it is absent — the exact scope semantics of
every other read seam the search fuses. - A stale-generation retry no longer leaks its managed-run heartbeat. The retry
returned from inside thetry, reaching none of the three teardown sites, so each
abandoned attempt left a loop renewing a lease its run no longer owned for the life of
the process. Exhausting all three retries leaked three loops. - Namespace-imported callees resolve against their own module first. The resolver's
documented order was inverted, soimport * as Utils from './utils'; Utils.parse(x)
bound to whichever other file in the project happened to export a top-levelparse. - Centrality is loaded under the canonical project id. Indexing with a retired project
alias queried centrality for a project that owns no symbols, so every chunk was written
withcentralityScore: 0— silently, because 0 is also the legitimate "not computed yet"
value. Both load sites are fixed, including the incremental reindex path that
auto-reindex actually takes. - An observation written under a retired alias is findable by its canonical id in the
same tick. The synchronous mirror was keyed on the caller's id until the asynchronous
persist resolved the alias a tick later, so a reader holding the post-rename id missed a
write that had already returned. A bounded residual remains for an alias this process has
never resolved — nothing can consult the database synchronously — and it is documented at
the call site and pinned by a test rather than left implicit. - Plugin bundles never appeared in any host's plugins screen. The installers wrote
MCP entries, hooks, skills and agent files, but no host plugin registry — the thing
/plugin(Claude Code),/plugins(Codex) and Cursor's plugin list actually read. All
four hosts reported✓ installedwhile showing nothing.apps/claude-plugin/install.sh
andapps/codex-plugin/install.shnow delegate registration toclaude plugin marketplace add+claude plugin installandcodex plugin marketplace add+codex plugin addrespectively, guarded by a capability probe so a missing or older CLI falls
back to the previous file-only behaviour and prints the manual command instead of
failing. - Cursor plugin installed to a path Cursor does not scan. The bundle went to
~/.cursor/plugins/massa-ai/; Cursor discovers local plugins under
~/.cursor/plugins/local/<name>/. Installing now targets the correct directory and
removes a pre-fix copy so the two cannot both be discovered and double-register hooks.
This path is derived from Cursor's plugin documentation and is not verified against a
running Cursor.app — unlike the Claude and Codex routes, which were verified
end-to-end. Please report if Cursor still does not list the plugin. - Codex plugin registration escaped a sandboxed install via
CODEX_HOME. The Codex
app exportsCODEX_HOMEinto every shell it spawns, and thecodexCLI prefers it over
$HOME.apps/codex-plugin/install.shresolves its own paths from$HOMEalone, so an
install run against an overridden$HOME(tests,--target) wrote files into the
sandbox while registering the marketplace into the real~/.codex— and an
--uninstallin a test suite silently removed a live[marketplaces.massa-ai]table.
The CLI is now pinned to the same root the installer writes. Note the pre-existing
divergence left in place: the file-copy half still ignoresCODEX_HOMEentirely. - Claude file-route artifacts survived an upgrade. A user who had installed via the
old file route kept their loose~/.claude/commands/massa-ai-*.md, subagent files and
mergedsettings.jsonhooks after upgrading, while the newly-registered plugin supplied
the same three — double-firing every lifecycle event. The installer now removes those
artifacts when it takes the plugin route.
Added
--plugin-source local|copy|autoonscripts/install-harness.sh, mirroring
--mcp-source. A host plugin registry stores the marketplace root path, so
registering a live checkout makes the install die when that checkout moves — measured:
Claude reportsfailed to load: cache-miss, andcodex plugin listthen errors for
every configured marketplace, not just massa-ai.copymaterialises a stable
marketplace root under~/.config/massa-ai/marketplace;autopickslocalin a
checkout andcopyotherwise (npx / published installs). The OpenCode plugin symlink
honours the same root.- Root
.cursor-plugin/marketplace.json, the Cursor counterpart to the existing
.claude-plugin/marketplace.jsonand.agents/plugins/marketplace.json. MASSA_AI_SKIP_PLUGIN_REGISTRY=1opt-out, which also pins the file-route test suites so
their outcome no longer depends on whether the machine running them has the host CLI
installed.scripts/tests/test-plugin-registry-registration.sh(45 assertions). The CLI-dependent
scenarios skip rather than fail whenclaude/codexare absent, which is the case
on every CI runner — that surface is covered by the capability probe and by manual
verification, not by CI.