feat(security): plugin architecture + Aikido Safe Chain install-gate + Patches-style /security - #123
Merged
Merged
Conversation
Migrate the remaining exact-pin overrides to security floors so none can become
a stale downgrade pin or block a future patch (follow-up to the qs >=6.15.2 fix):
postcss 8.5.15 -> ^8.5.15 (stay in 8.x — Next/Tailwind need postcss 8)
ip-address 10.2.0 -> ^10.2.0
vite 8.0.13 -> ^8.0.13
esbuild 0.28.0 -> >=0.28.0 (0.x minors are breaking-by-convention, so a
caret would block esbuild's security minors)
No resolution change: all four resolve to the same versions as before; the
lockfile diff is override-spec-only. typecheck clean, 108 tests pass, cve-lite
offline scan findingCount 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… self-patch (#109) Applying a patch runs an install that churns node_modules; if the target project's dev server is live (esp. Turbopack) it loses node_modules/next mid-reinstall and dies. This was self-inflicted when hexops patched itself — the apply request is served by the very process the install kills, surfacing to the user as "Failed to fetch". New guard in process-manager (TDD, injectable deps): - decideDevServerGuard: block-self | orchestrate | passthrough. - isHexopsSelf: target path === hexops cwd (the self-patch case — which cannot stop→apply→restart itself, so it is refused with a clear "apply from CLI" message rather than crashing). - runWithDevServerGuard: for a tracked managed dev server, stop → run → clear .next → restart (restored even if the operation throws); ProcessEntry now records the start mode so the restart is faithful. Wired into the apply paths: - /update and /security/cve-lite/[id]/fix — full block-self + orchestrate. - /override-remove — block-self (before editing package.json) + orchestrate. - /escalate — block-self only; orchestrated stop/restart is a follow-up. Responses now carry a devServerGuard summary; self-patch returns 409. Verified: 120 tests pass (12 new), typecheck clean, production build ✓. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
InstallGatePlugin (wrapInstall) + ComplianceAuditPlugin (audit) sharing SecurityPluginBase. Forward declaration for ComplianceReport — concrete shape lands with the first audit plugin. No registry, no consumers yet. Refs spec docs/superpowers/specs/2026-05-26-security-refactor-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Skeleton for SECURITY_PLUGINS array. First plugin registered in a later commit so each step stays reviewable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ProjectConfig gains optional plugins?: Record<string, ProjectPluginConfig>. getProjectPluginConfig / isPluginEnabledForProject (default false, opt-in) + setProjectPluginConfig with injected writer for testability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each plugin's isAvailable + renderCard runs concurrently. A throwing plugin becomes an error-status card and does not affect siblings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
isSafeChainAvailable shells safe-chain --version (probe injectable). PluginHostStatus carries version on success, reason+installHint on failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces head of [pnpm|npm|yarn, ...args] with the matching aikido-* wrapper. Absolute paths handled via basename. No-op for unknown heads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure parser: exit 0 → not blocked; non-zero + BLOCK marker → blocked with parsed advisory refs + tail-line message; non-zero without marker → treated as regular install failure (caller bubbles up). Fixtures captured. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Assembles availability + rewrite + parse-result into an InstallGatePlugin. renderCard branches on host availability + per-project enablement. wrapInstall is a no-op unless both conditions hold; otherwise rewrites the command and parses the result. Registered as the first SECURITY_PLUGINS entry. Internals exposed under _internals for dep-injected tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chains wrapInstall calls left-to-right; processResult fans out onResult in reverse and surfaces the first-blocking plugin plus a per-plugin run summary. Pure logic, no IO. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns the static plugin registry with each plugin's host status (isAvailable result). Per-project status served by a sibling route. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns per-project plugin status (host availability + renderCard payload). 400 on missing projectId, 404 on unknown plugin or project. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches src/app/patches/page.tsx header exactly: border-b border-zinc-800 px-6 py-4 + h1 text-xl font-semibold + subtitle text-xs text-zinc-500 + right-aligned actions (last-scan span + Rescan Button with RefreshCw animate-spin). Uses shadcn Button + lucide-react + cn util. Component only — not wired into /security/page.tsx (that lands in T18). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches src/app/patches/page.tsx summary bar (lines 1075-1102): border-b border-zinc-800 px-6 py-3 bg-zinc-900/50 + horizontal flex row of conditional [colored dot + count] chips per severity tone. Empty state shows 'No findings' in text-green-400. Tones: red/orange/yellow/zinc for critical/high/medium/low. Component only — not wired into /security/page.tsx (that lands in T18). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three components: SourceCard (per ScanSource), PluginCard (per SecurityPlugin), SourcePluginCards (grid container). Cards use the existing zinc/dot tonal language. This row is the plugin-driven slot — new plugins registered in src/lib/security/plugins/ surface here automatically. Not wired into /security/page.tsx yet (T18 does that). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the SecurityHeader + SecuritySummaryBar + SourcePluginCards row at the top of the /security main content area. Restyle in place: keeps FleetProjectRail, CveLiteToolbar, CveLiteScanControls, CveLiteManage, PendingCommitBanner, FixPlan, CveLiteFindings, ConfirmDialog all intact. Replaces the inline SourceStrip with the new card-style row that hosts both ScanSources (pnpm-audit, grype, cve-lite) and SecurityPlugins (Safe Chain). New plugins registered in src/lib/security/plugins/ appear here automatically. Plugin status is fetched per-project via /api/security/plugins + /api/security/plugins/[id]/status (T10 + T11). Severity counts on the summary bar are derived from the existing CVE Lite report. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors src/app/api/projects/[id]/holds/route.ts conventions: typed body
interface (no zod), getProjects()+findIndex+saveConfig pattern,
{success,error} response shape. AUTO_APPLY_ENABLED guard returns 409
matching /update.
Persists via setProjectPluginConfig (T3) — writer captures the index
update + saves the whole hexops.config.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Minimal viable installGate integration: installPackages gains optional
installBinOverride that does a regex-replace on the leading shell token
of the built install command. /update computes the override before the
install call by asking each enabled installGate plugin's wrapInstall
([packageManager]) and taking the rewritten command head.
Safe Chain therefore routes through aikido-pnpm/aikido-npm/aikido-yarn
when enabled for a project; blocks fail the install naturally via the
existing error path. Audit-log meta gains an optional installGate
{ plugin, binOverride } annotation. No new response branches.
A richer integration that threads applyInstallGate.processResult into
the install spawn + adds a 409 'blocked' response is a follow-up — the
plugin orchestrator (T9) is already in place for that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-plugin detail page reached via the Configure link on the Safe Chain card on /security. Sections: header, host-status, per-project enable table (Disable/Enable buttons gated when host is missing). Consumes GET /api/security/plugins, GET /api/security/plugins/safe-chain/status, POST /api/projects/[id]/plugins/safe-chain. Visual chrome mirrors SecurityHeader: border-b border-zinc-800, text-xl/sm font weights matching the rest of the security restyle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/security is now a true fleet view that mirrors /patches' grouped mode. Each project is a collapsible accordion row (border + chevron + name + finding-count badge + per-project Scan button on the header). Expanding shows that project's full CVE Lite UI inline (toolbar, scan controls, manage strip, SourcePluginCards, FixPlan, CveLiteFindings, commit banner, confirm dialog). All per-project state, fetches, handlers, and effects moved from SecurityHubInner into a self-contained ProjectSecurityAccordion component. Parent page becomes a ~95-line orchestrator: fleet-aggregate SecurityHeader + SecuritySummaryBar (sums across projects) + a list of accordions. FleetProjectRail no longer imported by /security (the file itself stays in src/components/security/ for other potential consumers). URL ?project=<id> still works: that project starts expanded on mount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… state
SecurityHeader's Rescan button used to just re-read the
/api/security/findings cache, leaving the header stuck at '0 findings
across 0 sources / Never scanned' in a fresh worktree. Now it POSTs
/api/projects/[id]/security-scan for every project (mapWithConcurrency
limit 3) and shows 'Scanning N/M projects…' progress until done.
Empty-state copy replaces the misleading zero-counts with
'{N} projects · no scans cached yet'.
Per-accordion Scan button now also POSTs the 3-source scan endpoint
before the cve-lite scan and pings onAnyDataChanged so the header's
fleet aggregate updates as each project lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Header gets:
- 'Scan ▾' button-group (no dropdown-menu in ui/): Scan All / pnpm /
grype / cve-lite. Per-tool fleet scans use ?sources=<csv> query.
- 'Sync OSV DB' button — exposes the existing /api/security/cve-lite/sync
endpoint at fleet level (was previously only reachable through each
project's CveLiteManage strip).
- Per-source progress meters strip rendered during in-flight scans. Three
slim bars (one per source), each advances as per-project responses land
with that source in their results. Only the selected sources' bars
render when running a single-tool scan.
Backend: POST /api/projects/[id]/security-scan accepts optional
?sources=<csv> query param. Filters SOURCES before invoking
scanProjectWithSources. CAVEAT: partial-source scans overwrite the
merged cache with only the scanned sources' results. Cache merging
across partial scans is a follow-up if it becomes a problem.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each project's collapsed accordion row now shows per-severity pills (critical/high/medium/low) when findings exist — replaces the single "N findings" outline badge. Pills only render for severity counts > 0, ordered worst-first. Green ✓ Clean badge unchanged when no findings. Color palette mirrors SecuritySummaryBar + Patches. Per-project severity counts aggregated in the page-level refresh() alongside the existing fleet aggregate and passed to each accordion as an optional prop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pills on the collapsed row show 3-source merged severity counts. Until now the expanded body only rendered cve-lite-only data, so a row with grype-discovered findings looked empty when expanded. Add an 'All findings (N)' section at the top of the expanded body that renders the same merged-findings data the pills are derived from. Each row: severity chip + package + version + advisory id(s) + sources chips + optional fix-version hint + divergent badge. Capped at 50 rows with overflow text; existing CveLite fix-plan + per-finding Apply UI stays below for cve-lite-driven workflows. Per-project findings array piped from the page-level /api/security/findings fetch as a new accordion prop alongside severity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…urceCard status SourceCard: 'ok' status no longer reads green when the source has findings. Tone now picked from (status, findingCount): ok+0 → green 'clean'; ok+>0 → amber 'N findings'; failed → red; unavailable → zinc; timeout → orange. Finding count moves into the right-side label; subtitle keeps just the ScanSource label + ms. Accordion 'All findings' section is now collapsible (defaults open when N≤10, collapsed otherwise to avoid 41+ row sprawl). Inside, flat finding rows are replaced by per-package groupings: one row per unique package@version with worst-severity chip + per-severity counts + sources union + optional fix version. Click a package row to expand its individual CVE rows. Each CVE row's advisory-id area shows a native title attr hover with the finding's title, detail (truncated), CVSS score, and references (shadcn HoverCard not available; native title fallback used). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
grype walks node_modules and reports vulnerabilities in embedded binaries (e.g. stdlib@go1.23.12 inside node_modules/esbuild/bin/esbuild). The 'package' grype reports is meaningless for remediation — the actionable unit is the parent npm package that ships the binary. New helper deriveParentPackage(finding) parses finding.path for the outermost node_modules/<pkg>/ segment (scope-aware). The accordion groups by parent package when one can be derived, falling back to the existing package@version grouping otherwise. Header shows '<parent> via <reported>' when they differ. Also adds 'security' to LogCategory union (needed for B). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rail
Suppressing findings without accountability is anti-security. Replace
the would-be 'suppress' button with a proper exception/deviation system
backed by durable storage + classification + audit log.
Model (src/lib/security/exceptions.ts):
- SecurityException = { id, projectId, parentPackage, scope:'parent-
package', classification, reason, notes?, createdBy, createdAt,
expiresAt?, reviewedBy?, reviewedAt?, revokedAt?, revokedBy?,
revokeReason? }. Classifications: risk-accepted, false-positive,
compensating-control, deferred, unfixable, deviation.
- Storage: .hexops/exceptions-<projectId>.json — durable, NOT under
cache/. JSON file per project, append-mostly so revoke history
survives.
- isExceptionActive() respects revokedAt + expiresAt; expiry lapses
automatically on the next read.
- activeExceptionParentSet(projectId) is the consumer-friendly query.
- _setExceptionsDirForTest() mirrors the persistence.ts test pattern.
API:
- GET /api/projects/[id]/security/exceptions — list all
- POST /api/projects/[id]/security/exceptions — file new
- POST /api/projects/[id]/security/exceptions/[id]/revoke — soft-delete
All mutations gated on AUTO_APPLY_ENABLED + audited via
logger.info('security', 'exception_filed' | 'exception_revoked', …).
Added 'security' to LogCategory union + updated exhaustive maps in
log-viewer.tsx and log-reader.ts.
UI:
- File-exception button on each actionable parent-package row in the
'All findings' section. Click opens ExceptionDialog (classification
dropdown + reason + optional notes + optional expiry).
- New 'Active exceptions (N)' section in the accordion body lists
open exceptions with classification badge, expiry, and revoke
button.
- Page-level severity aggregation + pills + fleet SummaryBar now
EXCLUDE findings whose parent package has an active exception.
Suppressed findings stay visible in the per-row 'Active exceptions'
section.
Future (not in scope): fleet /security/exceptions overview page,
separation-of-duties approval workflow, expiry notifications, SARIF
export of exceptions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 'File exception' button on a PackageRow was nested inside the outer
whole-row click target which was a <button>. HTML disallows nested
button elements; React + Turbopack surface this as a hydration error.
Convert the outer click target to a <div role='button' tabIndex={0}>
with onClick + onKeyDown (Enter/Space) + aria-expanded + aria-controls.
Inner File-exception button stays as a real <button>. Visual chrome
unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Accepted risk should be visually loud, not buried in the same zinc palette as the rest of the chrome. Active exceptions section + each row get an amber treatment (border, bg, AlertTriangle icon). Collapsed accordion rows show an amber 'N exception(s)' chip alongside the severity pills so reviewers see at a glance which projects have accepted-risk surface. Each exception row is now click-to-expand (chevron + aria-controls) revealing the full reason, notes, filed-by, dates, expiry. Edit button alongside Revoke opens the ExceptionDialog in edit mode (pre-filled values, 'Save changes' label) and PATCHes the new endpoint at /api/projects/[id]/security/exceptions/[exceptionId]. Audit log gets exception_modified with the changed fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-project finding-states index at .hexops/finding-states-<id>.json
keyed by dedupKey. Runner diffs each scan against stored states and
emits logger.info('security', 'finding_detected' | 'finding_redetected'
| 'finding_resolved') with the finding id, severity, package, sources,
and advisory ids in meta. Every finding now has an audit trail of its
existence, not just operator actions on it.
UI: finding rows show 'first seen <relative>' from the persisted
firstSeenAt; findings detected within the last 24h get a 'new' chip.
SourceCards + PluginCards get scope subtitles ('lockfile scanner',
'filesystem/binary scanner', 'install-time interceptor') so
discrepancies between scanners are legible — lockfile scanners can't
catch what filesystem scanners find in embedded binaries, and the UI
should say so.
/api/security/findings now returns findingStates per project alongside
the existing findings/sources data; the security page propagates it
into each accordion which passes it to FindingRow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously rendered as plain zinc text — easy to miss next to the severity pills, exception chip, and new chip. Convert to a pill with cyan tones (border-cyan-500/30 text-cyan-300 bg-cyan-500/10), distinct from every other color already in play in the security UI. Applied on both FindingRow (per-CVE) and PackageRow (group-level). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each parent-package row in 'All findings' expands to a remediation
panel showing: detection source(s), package(s), fix version(s) with
the cyan pill, and an amber note when the fix version targets an
embedded artifact rather than the npm package directly.
Four actions per row:
- Apply fix… opens RemediationDialog (target version + override
checkbox); submits to /api/projects/[id]/update
with auditContext.source = 'grype'; opens the
existing PendingCommitBanner on success.
- Override pin… same dialog, override defaults on.
- Send to Patches anchor link to /patches?project=<id>#pkg-<name>
(the existing patches view; package anchor is
best-effort — future work to add a real filter
hook).
- View references modal listing every advisory URL across the
group's CVEs.
RemediationDialog pre-fills target version: for direct-match findings,
the first version from grype's fixedIn; for parent-embedded findings,
'latest' plus an amber note explaining the npm-level remediation maps
to the parent package rather than the embedded artifact.
Patch-scanner does NOT yet ingest grype findings — 'Send to Patches'
is a deep-link only. A real injection / queue hook is tracked separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RemediationDialog no longer closes when /update returns. It stays open
through the full Apply pipeline:
configuring → installing → rescanning → verifying → outcome
Auto-rescan: after the install, POST /api/projects/[id]/security-scan
to refresh the merged 3-source cache. No more relying on the user to
hit Rescan manually after a fix.
Outcome verification: re-pull /api/security/findings and count how
many of this group's findings remain. Result drives one of three
outcome states inside the dialog:
- resolved — all findings cleared
- partial — some findings cleared
- unresolved — no findings cleared (upstream may not have shipped
a fix yet; common for grype binary findings)
The unresolved/partial outcome surfaces a 'File exception' shortcut
so the user can pivot from 'tried to fix' to 'documented exception'
in one move — important when upstream is the blocker.
Error state surfaces the install/rescan/verify error inline.
Streaming install stdout to the client (SSE) is tracked separately
as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every Apply now gets a generated attemptId (rem_<uuid>). All log events
for that attempt carry the same attemptId in meta so the lifecycle can
be reconstructed via grep:
remediation_initiated { attemptId, source, parameters }
remediation_install_complete { attemptId, source, packages, ... }
remediation_install_failed { attemptId, source, attemptedPackages }
remediation_completed { attemptId, outcome: { status,
previousFindingCount,
currentFindingCount,
findingsCovered, findingsResolved,
findingsRemaining } }
The existing security_remediation_applied event is renamed to
remediation_install_complete (semantically clearer) and threaded with
attemptId. The /update route now logs remediation_initiated BEFORE the
install runs so failure cases still have a record of intent.
New endpoint: POST /api/projects/[id]/security/remediation/[attemptId]
/complete. Client POSTs the verified outcome (resolved | partial |
unresolved | error) after the rescan + verify phases. No AUTO_APPLY
gate — it's audit metadata, not a mutation.
RemediationDialog surfaces the tracking ID in the outcome panel so
the user has a change-control reference to cite.
Also renames the cve-lite --fix route's security_remediation_applied
emit to remediation_install_complete under the 'security' category for
consistency. No remaining callers of the old event name.
cve-lite Apply also flows through /update so it now gets the same
remediation_initiated + remediation_install_complete trail with its
own attemptId once cve-lite's submit handler is updated to generate
one (small follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Close the gap left by commit 02b82a1 (which wired change-control logging on the grype path only). applyOne (per-finding cve-lite Apply) and fixAll (cve-lite --fix bulk) now both generate a rem_<uuid> attemptId, thread it through to /update or /api/security/cve-lite/[id]/fix in the auditContext, and POST to /security/remediation/[attemptId]/complete after the rescan + commit-banner steps. The cve-lite fix route additionally logs remediation_initiated before the fix runs and remediation_install_failed on the failure branches — mirroring the /update route's pattern. cve-lite Apply now produces the same threaded log trail as grype: remediation_initiated → remediation_install_complete → remediation_completed, all sharing one attemptId per Apply. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…te self-link (#117) #116 — applyInstallGate (apply-install-gate.ts) and runAllPluginCards (runner.ts) were scaffolding-only. /update hand-rolls a minimal single-plugin binary-swap and never calls processResult; the plugins GET route is host-only (no project context); and the accordion builds PluginCardEntry[] client-side from the per-plugin status endpoint — so neither orchestrator had a prod caller or a near-term one. Removed both modules + their tests. Preserved the live UI contract by moving the PluginCardEntry type into types.ts and repointing its five importers. Richer multi-plugin chaining + processResult block-parsing stays the #116 follow-up (design in git history + spec), to build when a second installGate plugin or block-parsing UX actually lands. #117 — dropped sourceDeepLinks={{ 'cve-lite': '/security' }} from the security accordion; the cve-lite card's "Open dashboard" linked back to the same page. SourceCard gates the link on deepLinkHref, so removing the prop simply hides it; the optional prop stays supported for future legitimate cross-page links. Verified: typecheck clean, 215 tests pass, build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces a two-track security plugin architecture alongside the existing
ScanSourcestack, ships Aikido Safe Chain as the first install-gate plugin, and restyles/securityinto a Patches-style per-project accordion. Folds in two pre-held fixes that are ancestors of this branch (#109 dev-server-aware patching, override-pin → floor migration).What's included
Plugin architecture — additive; existing
ScanSourcepath untouchedSecurityPlugindiscriminated union (installGate | complianceAudit) insrc/lib/security/plugins/Aikido Safe Chain (first
installGateplugin)/update: when enabled for a project, the install binary is swapped toaikido-pnpmto interpose on the install/security/safe-chaindetail page with a per-project enable tableNew APIs
GET /api/security/plugins— registry + host availabilityGET /api/security/plugins/[id]/status?projectId=…— per-project cardPOST /api/projects/[id]/plugins/[pluginId]— enable/disable (AUTO_APPLY-gated)/security restyle
FleetProjectRail; newProjectSecurityAccordion(Patches-style collapsible rows) owns per-project CVE Lite stateSecurityHeader+ fleet-aggregateSecuritySummaryBar+SourcePluginCards(sources + plugins row)Remediation & audit
SecurityExceptiontracking (classification + audit trail),RemediationPanel(multi-option grype remediation), change-control-style attempt logging on Apply pathsBundled pre-held fixes (ancestors of this branch)
d020f56— dev-server-aware patching: refuse self-patch (409), stop→apply→restart for tracked servers (Guard patch apply when the target project's dev server is running (Turbopack node_modules churn) #109)167ce28— convert override pins to>=security floorsCloses
Follow-ups (NOT closed by this PR)
applyInstallGate/runAllPluginCardsscaffolding was removed in6fa88c8(it was dead code; the prod paths hand-roll the minimal versions). Reframe Wire applyInstallGate (and runAllPluginCards) into production paths #116 to the richer orchestration work (multi-plugin chaining +processResultblock-parsing + batch card rendering), to build when a 2nd installGate plugin or block UX actually lands.aikido-pnpmbinary yet. The feature is opt-in and inert until Safe Chain is installed and enabled per-project. Run T21 before enabling it in any real environment.Verification
/security,/security/safe-chain, and the plugin APIs are in the manifestMerge note
37 commits. Two prior squash-merges on this repo silently dropped commits pushed near merge time (#104, #108). Prefer a merge commit or rebase-merge to preserve history, or diff
mainagainst the branch tree after a squash to confirm nothing dropped (especially the security-relevantd020f56/167ce28).🤖 Generated with Claude Code