Add two defenses against malicious plugins from uncontrolled sources:
1. **Pinned-ref enforcement** (resolveAndStage): github:// install/download
specs without a #<tag/sha> suffix are now rejected with HTTP 422. A
mutable default-branch tip could change between audit and install,
silently swapping in untrusted code. Override via PLUGIN_ALLOW_UNPINNED=true.
2. **SHA-256 content integrity** (installRequest.sha256): callers may
supply the expected hex SHA-256 of the fetched plugin.yaml. When present,
resolveAndStage verifies the digest after staging; a mismatch aborts the
install with HTTP 422 and cleans up the staging dir.
Updated TestPluginDownload_GithubSchemeStreamsTarball to use a pinned ref
(#v1.0.0) so it reflects the new security requirement.
Tests: 4 new (TestPluginInstall_SHA256Mismatch_AbortsInstall,
TestPluginInstall_SHA256Match_Succeeds, TestPluginInstall_UnpinnedRef_Rejected,
TestPluginInstall_PinnedRef_Accepted). All 15 packages green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #724 (workspace hibernation) claimed migration number 029.
Renaming to 030 to resolve the sequence collision before merging #651.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a dedicated section mapping the four overlapping terms (Skills,
Plugins, Agents, Hooks) plus Instructions and Agentic Workflows between
awesome-copilot and Molecule vocabulary. Closes#734.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two defenses against GLOBAL-scope agent memory injection attacks:
1. Recall delimiter: Search() wraps every GLOBAL-scope memory value
with a non-instructable prefix before returning it to MCP clients:
[MEMORY id=<uuid> scope=GLOBAL from=<workspace_id>]: <value>
This prevents stored content (e.g. "IGNORE ALL PREVIOUS INSTRUCTIONS")
from being parsed as instructions in the agent's context window.
Raw DB content is unchanged — the wrapper is applied on read only.
2. Write audit log: Commit() writes an activity_log entry with
activity_type='memory_write_global' whenever a GLOBAL memory is
stored. The entry records a SHA-256 hash of the content (never
plaintext) alongside memory_id and namespace for forensic replay.
Audit failure is non-fatal — a logging error must not roll back
a successful write.
Tests:
- TestRecallMemory_GlobalScope_HasDelimiter — verifies exact delimiter
format [MEMORY id=... scope=GLOBAL from=...]: <value>
- TestCommitMemory_GlobalScope_AuditLogEntry — verifies activity_logs
INSERT fires on every GLOBAL write (via mock.ExpectationsWereMet)
- TestMemoriesCommit_Global_AsRoot — updated to expect the audit INSERT
All 16 Go test packages pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #763 (feat/issue-733-agents-md-impl) branched before PR #743 landed the
claude-opus-4-7 model default upgrade. config.py still had the old
claude-sonnet-4-6 default, which would have silently regressed the upgrade.
Restore both occurrences:
- WorkspaceConfig.model default: claude-sonnet-4-6 → claude-opus-4-7
- load_config() fallback: claude-sonnet-4-6 → claude-opus-4-7
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Turns the QA TDD spec from PR #755 GREEN: all 14 tests pass.
Changes:
- workspace-template/agents_md.py (new): generate_agents_md(config_dir, output_path)
Writes AAIF-compliant AGENTS.md with name, role, description, A2A endpoint,
and MCP tools sections. AGENT_URL env var overrides the derived localhost URL.
Falls back to description when role is absent (graceful legacy compat).
Always overwrites — no stale-file guard.
- workspace-template/config.py: add role field to WorkspaceConfig
New top-level field `role: str = ""` with load_config support.
Falls back to description in agents_md.py for backward compat.
- workspace-template/main.py: wire generate_agents_md into startup (step 1a)
Fires after load_config + preflight. Non-fatal: exception is caught and
printed as a warning so a bad /workspace mount never kills the agent.
- workspace-template/tests/test_agents_md.py (new): pulled from PR #755 branch
Test results:
pytest tests/test_agents_md.py -v → 14 passed (was: 14 RED / import error)
pytest (full suite) → 1044 passed, 2 xfailed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added an early guard in ProxyA2A() that rejects HTTP requests whose
X-Workspace-ID header passes isSystemCaller() with 403 Forbidden.
Legitimate system callers (webhooks, scheduler, restart_context) call
proxyA2ARequest() directly via ProxyA2ARequest() and never send HTTP
headers with system-caller prefixes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Baidu MeDo hackathon integration was sitting in builtin_tools/ as dead
code — not imported by any loader but shipped with every workspace image,
misleadingly suggesting it was a core builtin.
Changes:
- Move builtin_tools/medo.py → plugins/molecule-medo/skills/medo-tools/scripts/medo.py
(git detects this as a rename — no code changes, identical tool surface)
- Add plugins/molecule-medo/plugin.yaml (manifest: name, version, runtimes, tags)
- Add plugins/molecule-medo/skills/medo-tools/SKILL.md (frontmatter + setup docs)
- Move workspace-template/tests/test_medo.py → plugins/molecule-medo/tests/test_medo.py
(update _MEDO_PATH to resolve from plugin root; add conftest.py for langchain mock)
- Update .gitignore: change /plugins/ blanket ignore to /plugins/* so this plugin
can be tracked until it gets its own standalone repo
Acceptance criteria met:
- builtin_tools/medo.py removed from core
- plugins/molecule-medo/ created with identical tool surface (9/9 tests pass)
- cd workspace-template && pytest → 1021 passed, 2 xfailed (no regression)
- MEDO_API_KEY was never in default provisioning (.env.example / config.py clean)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AuditTrailPanel SidePanel tab showing the workspace audit ledger from
GET /workspaces/:id/audit with cursor-based pagination (?cursor=, ?limit=50)
- Color-coded event-type badges: delegation=blue-500, decision=violet-500,
gate=yellow-500, hitl=orange-500
- chain_valid=false renders red tamper warning indicator
- Event-type filter bar (All / Delegation / Decision / Gate / HITL) resets
pagination and reloads with ?event_type= param
- Relative timestamps refreshed every 30 s without re-fetching
- Empty state with icon and descriptive copy
- Toolbar Audit button (ledger icon) switches panel to audit tab for
selected workspace, or shows toast if no workspace is selected
- 29 new unit tests across formatAuditRelativeTime, AuditEntryRow, and
AuditTrailPanel component integration suites
- Update SidePanel.tabs.test.tsx for 13-tab count and audit as last tab
- Add setPanelTab to Canvas test store mocks (Toolbar now reads it)
Closes#753
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AuditTrailPanel SidePanel tab showing the workspace audit ledger from
GET /workspaces/:id/audit with cursor-based pagination (?cursor=, ?limit=50)
- Color-coded event-type badges: delegation=blue-500, decision=violet-500,
gate=yellow-500, hitl=orange-500
- chain_valid=false renders red ⚠ tamper warning indicator
- Event-type filter bar (All / Delegation / Decision / Gate / HITL) resets
pagination and reloads with ?event_type= param
- Relative timestamps refreshed every 30 s without re-fetching
- Empty state with ⊟ icon and descriptive copy
- Toolbar "Audit" button (ledger icon) switches panel to audit tab for
selected workspace, or shows toast if no workspace is selected
- 29 new unit tests across formatAuditRelativeTime, AuditEntryRow, and
AuditTrailPanel component integration suites
- Update SidePanel.tabs.test.tsx for 13-tab count and "audit" as last tab
- Add setPanelTab to Canvas test store mocks (Toolbar now reads it)
Closes#753
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New A2ATopologyOverlay component polls /activity fan-out every 60s and
writes directed edges to a2aEdges store slice (separate from topology edges)
- buildA2AEdges aggregates delegate rows per source→target pair; violet-500
animated edge when last call <5 min ago, blue-500 static otherwise
- Toolbar toggle persists to localStorage (molecule:show-a2a-edges)
- Canvas.tsx merges a2aEdges into allEdges via useMemo; pointerEvents:none
on all edge elements keeps nodes draggable
- 24 new unit tests across pure function, helper, and component suites
- Fix Canvas.a11y and Canvas.pan-to-node store mocks (missing A2A fields)
Closes#744
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New A2ATopologyOverlay component polls /activity fan-out every 60s and
writes directed edges to a2aEdges store slice (separate from topology edges)
- buildA2AEdges aggregates delegate rows per source→target pair; violet-500
animated edge when last call <5 min ago, blue-500 static otherwise
- Toolbar toggle persists to localStorage (molecule:show-a2a-edges)
- Canvas.tsx merges a2aEdges into allEdges via useMemo; pointerEvents:none
on all edge elements keeps nodes draggable
- 24 new unit tests across pure function, helper, and component suites
- Fix Canvas.a11y and Canvas.pan-to-node store mocks (missing A2A fields)
Closes#744
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `spike/issue-742-managed-agents-executor/` with:
- `demo.py`: standalone Python script that authenticates to the Managed Agents
beta API, provisions an environment + agent, starts a session, runs two
conversational turns (with cross-turn state recall verification), and prints
cold-start and per-turn latency measurements.
- `README.md`: full integration assessment covering provisioner changes needed,
A2A routing conflict (primary blocker — sessions have no addressable URL),
cost model, API gaps table, and a no-ship recommendation with a 3-week effort
estimate if we proceeded anyway.
Recommendation: no-ship for primary executor. Revisit as a batch/cron worker
in Phase H once Molecule's MCP server is feature-complete.
Closes#745. References #742.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the anthropic:claude-sonnet-4-6 default across config, handlers,
env example, and litellm proxy config. All tests updated to match the new
default; sonnet-4-6 alias kept in litellm_config.yml for pinned workspaces.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The BE's tests (AdminTokenSet_*, FailOpen_*) validated the core AdminAuth
contract on /admin/secrets. These table-driven additions pin the same contract
on the three routes explicitly named in the #684 security report, each with
three scenarios: workspace token rejected, correct ADMIN_TOKEN accepted, no
bearer rejected.
Routes covered:
GET /admin/liveness
GET /admin/github-installation-token
GET /approvals/pending
When ADMIN_TOKEN is set (tier 2), ValidateAnyToken is never called — the
env-var comparison short-circuits before any DB lookup. The mock sets only
HasAnyLiveTokenGlobal and nothing else; an extra DB expectation would itself
be a test bug (calling it proves the middleware regressed to tier 3).
All 18 TestAdminAuth_684* tests pass. Full go test ./... is green across all
15 platform packages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend Engineer's PR #729 introduces ADMIN_TOKEN — when set, only that value
is accepted on /admin/* and /approvals/* routes, replacing the vulnerable
workspace-bearer fallback. Without the env var wired into deployments the fix
is code-only and the vulnerability stays open in every running instance.
Changes:
- `docker-compose.yml`: adds ADMIN_TOKEN env var to the platform service
(blank default = backward-compat fallback, i.e. still vulnerable until set).
NOTE: docker-compose.infra.yml has no platform service — the platform lives
only in the full-stack docker-compose.yml, so that is the correct file.
- `.env.example`: documents ADMIN_TOKEN with generation instructions and a
clear warning that it must be set to close#684.
- `infra/scripts/setup.sh`: prints a visible warning when ADMIN_TOKEN is unset
so operators know the vulnerability is still open in that deployment.
- `CLAUDE.md`: adds ADMIN_TOKEN to the env vars reference section.
No Go code changed — go build ./... passes clean.
Part of fix for #684 / PR #729
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
goose donated to Linux Foundation AAIF (alongside MCP + AGENTS.md) — AGENTS.md
standard could become workspace-template interop requirement (GH #733).
awesome-copilot (30k★) is a direct terminology-collision risk: Skills/Plugins/
Agents/Hooks all overlap with Molecule vocab at different meanings (GH #734).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The hibernation PR (5c1a9d0) accidentally removed the delivery_confirmed
fix that was introduced for issue #689. When io.ReadAll fails after the
target has already responded with headers (200-399), the message WAS
delivered — stripping delivery_confirmed from the error response caused
callers to treat a successful send as a hard failure.
Restore the full original body-read error block:
- deliveryConfirmed flag (true when status 200-399)
- log line with status/bytes_read context
- logA2ASuccess call when deliveryConfirmed (audit trail accuracy)
- proxyA2AError.Response includes "delivery_confirmed" field so callers
can distinguish "not delivered" from "delivered, body lost"
The hibernation auto-wake feature (resolveAgentURL status='hibernated'
check) is orthogonal and untouched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Blast-radius isolation gap: AdminAuth called ValidateAnyToken which
accepted any live workspace bearer token. A compromised workspace agent
could present its own token to GET /admin/github-installation-token and
steal the platform's GitHub App credential, or hit /approvals/pending to
enumerate cross-workspace approvals.
Fix: introduce a dedicated admin credential tier via ADMIN_TOKEN env var.
When set, AdminAuth verifies the bearer against that secret exclusively
(crypto/subtle constant-time comparison). Workspace tokens are rejected
outright — no DB lookup occurs. When ADMIN_TOKEN is not set the previous
behaviour is preserved as a deprecated backward-compat fallback (tier 3)
so existing deployments without the env var don't break immediately.
Credential tiers (evaluated in order):
1. Fail-open — no live tokens globally (fresh install / pre-Phase-30)
2. ADMIN_TOKEN match — env var set, bearer must equal it exactly
3. Fallback (deprecated) — any valid workspace token (ADMIN_TOKEN unset)
Operators should set ADMIN_TOKEN=<openssl rand -base64 32> to fully close
the blast-radius gap. Tier 3 will be removed in a future release.
Fixes#684.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs caused enabled schedules to silently disappear from the fire query
(which requires next_run_at IS NOT NULL AND next_run_at <= now()):
Bug 1 - fireSchedule() and recordSkipped(): when ComputeNextRun returned an
error, nextRunPtr stayed nil and UPDATE SET next_run_at = $2 wrote NULL.
Fix: change to COALESCE($2, next_run_at) so the existing DB value is preserved
when $2 is NULL, and log the error explicitly.
Bug 2 - org importer (handlers/org.go): nextRun, _ := ComputeNextRun(...)
silently discarded the error. A bad cron expression would pass time.Time{}
(zero value) to the INSERT. Fix: surface the error, log it, and skip the
schedule INSERT via continue.
Bug 3 - no startup repair: schedules already NULL'd by the pre-fix binary
would never recover. Fix: Start() now calls repairNullNextRunAt() once on
boot, recomputing next_run_at for every enabled schedule with a NULL value.
Tests: TestFireSchedule_ComputeNextRunError, TestRecordSkipped_ComputeNextRunError,
TestRepairNullNextRunAt_RepairsRows, TestRepairNullNextRunAt_DBError_NoPanic,
TestOrgImport_ScheduleComputeError (all pass).
Fixes#722
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>