{"slug": "separating-a-vs-code-extension-from-a-typescript-core-architecture-lessons-from", "title": "Separating a VS Code Extension from a TypeScript Core: Architecture Lessons from Aqiron Security", "summary": "A developer behind Aqiron Security, an open-source application security project, separated the product's security and runtime logic from its VS Code extension into a standalone Node.js and TypeScript core process. The extension now acts as a client, launching the core locally and communicating over newline-delimited JSON via stdin/stdout, so the core no longer depends on the VS Code API. The design keeps packages/core private and bundled into the extension, with the boundary intended to allow future packaging changes.", "body_md": "When I started building Aqiron Security, the natural approach was to keep the security logic inside the VS Code extension.\n\nThat works.\n\nAt least initially.\n\nBut as the project started accumulating scanner orchestration, finding normalization, correlation, project intelligence, RAG, reporting, and AI operations, I started running into a deeper architectural question:\n\nShould the VS Code extension actually own the application's core runtime?\n\nI decided the answer should be no.\n\nAqiron Security is an open-source application security project. The current product is a VS Code extension, but I wanted the security/runtime layer to have a much cleaner boundary from the client.\n\nThe result is a design where the extension acts as the client and starts a separate Node.js process containing the TypeScript core.\n\nThis article explains why I made that separation, how the current architecture works, and some of the trade-offs I have encountered.\n\nThe original problem\n\nA VS Code extension has a lot of responsibilities already.\n\nIt deals with things like:\n\nextension activation\n\ncommands\n\ndiagnostics\n\nthe VS Code API\n\nwebview communication\n\neditor state\n\nuser settings\n\nUI lifecycle\n\nSecurity scanning introduces another class of responsibilities:\n\nstarting external tools\n\nparsing scanner output\n\nnormalizing findings\n\ncorrelating duplicate results\n\ngenerating reports\n\nmanaging long-running operations\n\ncancellation\n\nAI operations\n\nworkspace analysis\n\nPutting all of that into one runtime makes the boundary blurry.\n\nThe code may still work, but over time the architecture starts answering questions like:\n\n\"Does this security service need VS Code?\"\n\nThat is a dangerous dependency to create if the answer does not actually need to be yes.\n\nThe current Aqiron architecture looks roughly like this:\n\n`┌──────────────────────────────────────────────┐`\n\n│              VS Code Extension                │\n\n│                                              │\n\n│ activation / commands / diagnostics          │\n\n│ React webview / settings / workspace UI      │\n\n│ ScanController / client-side services        │\n\n└──────────────────────┬───────────────────────┘\n\n                       │\n\n                       │ CoreClient\n\n                       │ CoreProcessManager\n\n                       │\n\n                       │ newline-delimited JSON\n\n                       ▼\n\n┌──────────────────────────────────────────────┐\n\n│            Aqiron Core Runtime               │\n\n│            Node.js + TypeScript              │\n\n│                                              │\n\n│ protocol / cancellation / adapters           │\n\n│ scanning / analysis / correlation            │\n\n│ reports / RAG / AI operations                │\n\n└──────────────────────┬───────────────────────┘\n\n                       │\n\n          ┌────────────┴────────────┐\n\n          ▼                         ▼\n\n   Native Aqiron rules       External scanners\n\n                             Trivy / Semgrep\n\n                             OSV-Scanner /\n\n                             Betterleaks / MobSF\n\n          │                         │\n\n          └────────────┬────────────┘\n\n                       ▼\n\n                Unified findings\n\n                       ▼\n\n             correlation + graph\n\n                       ▼\n\n                  reports\n\nThis is not a cloud architecture.\n\nThe current project is local-first. The extension starts the core process locally and communicates with it over standard input/output. The repository currently keeps `packages/core` private and bundles it into the extension rather than publishing it as a separate npm package.\n\nThat distinction is important.\n\nThe boundary exists today, but the eventual product packaging can evolve later.\n\nThere were several reasons.\n\nThe first reason is architectural independence.\n\nThe core should not need to know that VS Code exists.\n\nIdeally, security logic should be able to operate on concepts like:\n\n`workspace`\n\nscan\n\nfinding\n\nproject\n\nreport\n\nAI request\n\nrather than:\n\n`vscode.workspace`\n\nvscode.window\n\nWebviewPanel\n\nTextDocument\n\nDiagnosticCollection\n\nThe extension is responsible for translating between the developer environment and the core.\n\nThat gives me a much cleaner dependency direction:\n\n`VS Code client`\n\n      ↓\n\n    Core\n\n`VS Code ↔ Security logic ↔ VS Code`\n\nUsing a separate process also creates a runtime boundary.\n\nThe extension host and the security core no longer execute as one giant logical process.\n\nThat matters for long-running operations.\n\nA scan might involve:\n\n`discover files`\n\n      ↓\n\nrun multiple tools\n\n      ↓\n\nparse results\n\n      ↓\n\nnormalize findings\n\n      ↓\n\ncorrelate findings\n\n      ↓\n\nbuild relationships\n\n      ↓\n\ngenerate reports\n\nThat's a very different workload from handling an editor command or updating a sidebar.\n\nWith a separate core process, the extension can treat the security engine more like a service.\n\nThat makes lifecycle handling, restart behavior, and failure boundaries easier to reason about.\n\nThis was probably the most valuable part of the architecture.\n\nOnce the extension and core became separate processes, they couldn't casually call each other's internal functions anymore.\n\nThey needed a protocol.\n\nThe current protocol is intentionally simple:\n\n`stdin/stdout`\n\n+\n\nnewline-delimited JSON\n\nA request looks conceptually like:\n\n```\ninterface CoreRequestMessage {\n  id: string;\n  type: \"request\";\n  method: string;\n  params?: unknown;\n}\n```\n\nAnd a response:\n\n```\ninterface CoreResponseMessage {\n  id: string;\n  type: \"response\";\n  success: boolean;\n  result?: unknown;\n  error?: CoreProtocolError;\n}\n```\n\nThere are also event messages for asynchronous pipeline updates:\n\n```\ninterface CoreEventMessage {\n  type: \"event\";\n  event: string;\n  requestId?: string;\n  payload?: unknown;\n}\n```\n\nThe actual protocol also has a versioned handshake.\n\nFor example, the current runtime exposes a protocol version and core version and can report compatibility states such as:\n\n`compatible`\n\nprotocol-mismatch\n\nextension-too-old\n\ncore-too-old\n\nunsupported\n\nThat gives us an explicit compatibility boundary instead of relying on both sides silently assuming they agree.\n\nI deliberately didn't start with something complicated.\n\nThe current transport is essentially:\n\n`message 1\\n`\n\nmessage 2\\n\n\nmessage 3\\n\n\nwhere each line contains one JSON message.\n\nThat gives us a few useful properties:\n\nYou can literally look at the communication stream.\n\nMalformed input can be identified and rejected.\n\nThe extension starts the process locally and communicates through stdio.\n\nThe wire format is JSON rather than TypeScript-specific objects.\n\nThat last point matters.\n\nThe implementation is TypeScript, but the protocol doesn't fundamentally need to be.\n\nOnce requests and responses cross a process boundary, we need a way to know which response belongs to which request.\n\nThat's why requests carry an ID.\n\nFor example:\n\n`request id: abc123`\n\nmethod: scan.start\n\nThe response can return:\n\n`id: abc123`\n\nsuccess: true\n\nWithout this, concurrent operations become painful to reason about.\n\nThe ID becomes the connection between:\n\nrequest\n\n   ↓\n\ncore operation\n\n   ↓\n\nresponse\n\nand also gives us something useful for cancellation and pipeline events.\n\nSecurity operations shouldn't be treated as unstoppable functions.\n\nImagine starting a deep scan and then closing the workspace.\n\nOr starting an AI analysis and then deciding you don't need it anymore.\n\nThe architecture therefore includes explicit cancellation operations.\n\nThe current core protocol exposes operations such as:\n\n`core.cancel`\n\nscan.cancel\n\nai.cancel\n\nand the runtime propagates cancellation through the relevant cancellation sources and scanner context.\n\nThis is one of those details that seems unnecessary until you have a real long-running operation.\n\nThen it becomes essential.\n\nOne of the goals of the boundary is to let the client ask for a scan without knowing the implementation details.\n\nConceptually:\n\n```\nawait coreClient.startScan({\n  workspaceRoot,\n  mode: \"deep\",\n  trusted: true\n});\n```\n\nThe extension doesn't need to know:\n\nwhich scanners are installed\n\nhow scanner output is parsed\n\nhow findings are normalized\n\nhow correlation works\n\nhow reports are generated\n\nThose concerns belong to the core pipeline.\n\nThe current core pipeline can combine native Aqiron rules with optional external scanners, normalize results into `UnifiedFinding[]`, then pass them through correlation/graph processing and report generation.\n\nThat separation is the main reason I like this architecture.\n\nA scanner may produce one format.\n\nAnother scanner produces something completely different.\n\n`Scanner A`\n\nseverity = HIGH\n\nfile = foo.dart\n\nline = 41\n\nwhile another might report:\n\n`Scanner B`\n\nlevel = error\n\npath = foo.dart\n\nstartLine = 41\n\nThe core shouldn't force the rest of the system to understand every scanner's native format.\n\nInstead, scanner-specific parsers convert the output into a common model.\n\n`external scanner`\n\n       ↓\n\nscanner-specific parser\n\n       ↓\n\nUnifiedFinding\n\n       ↓\n\ncorrelation\n\n       ↓\n\nreport\n\nThis is one of the biggest advantages of having an application-level core rather than scattering scanner logic throughout the VS Code extension.\n\nThis is important because architecture diagrams can easily make an early project look more mature than it actually is.\n\nAqiron is currently version 0.0.1 and under active development.\n\nThere are still intentional limitations.\n\nworkspace operations currently require a Flutter workspace\n\nexternal scanners are optional\n\nthe core is still bundled into the extension\n\nquick file scans use a separate direct extension path\n\nthe project does not yet have independently published Core, CLI, or Desktop packages\n\nSo the current architecture is not:\n\n`Aqiron Core npm package\n\n        ↓\n\nVS Code\n\nCLI\n\nDesktop\n\nNot yet.\n\nIt's closer to:\n\nVS Code\n\n   ↓\n\ninternal Core process\n\n   ↓\n\nbundled runtime`\n\nThat is an important distinction.\n\nThe architecture is being prepared for broader reuse without prematurely creating a bunch of packages that don't yet need to exist.\n\n**Why I didn't immediately split everything into repositories**\n\nThis was another deliberate decision.\n\nIt would be easy to say:\n\n`aqiron-core`\n\naqiron-security-vscode\n\naqiron-security-cli\n\naqiron-security-desktop\n\nand create four repositories immediately.\n\nBut that would add operational complexity before the products existed.\n\nYou would now have to manage:\n\npackage publishing\n\nversion coordination\n\ncross-repository changes\n\nrelease synchronization\n\ndependency management\n\ncontributor workflow across multiple repositories\n\nThe current repository gives me a cleaner intermediate step:\n\n`src/`\n\npackages/core/\n\nwith an explicit runtime boundary.\n\nWhen multiple clients become real products, the repository structure can change.\n\nUntil then, the architecture can evolve without forcing the project to pay the cost of premature distribution.\n\nThe biggest win isn't actually \"using IPC.\"\n\nThe bigger win is making **the boundary explicit.**\n\nThe VS Code extension owns the developer environment.\n\nThe core owns security operations.\n\nThe protocol connects them.\n\nThat gives us a mental model like:\n\n`Client responsibilities\n\n    ↓\n\nUI\n\nVS Code\n\ncommands\n\ndiagnostics\n\nworkspace interaction\n\n```\n      │\n      │ protocol\n      ▼\n```\n\nCore responsibilities\n\n    ↓\n\nscanning\n\nnormalization\n\ncorrelation\n\nRAG\n\nAI operations\n\nreports\n\n`\n\nThat's much easier to reason about than a single giant extension runtime.\n\nThe architecture also introduces new problems.\n\nA process boundary is not free.\n\nNow we have to care about:\n\nprocess startup time\n\nrestart behavior\n\nmalformed messages\n\nprotocol compatibility\n\nstderr/stdout handling\n\npartial failures\n\ncancellation\n\nshutdown\n\nconcurrent requests\n\nserialization overhead\n\nIn other words:\n\n**We traded code coupling for process-boundary complexity.**\n\nI think that's a reasonable trade for Aqiron, but it isn't automatically the right choice for every VS Code extension.\n\nIf your project is a small command-based extension with a few hundred lines of logic, this architecture would probably be overkill.\n\nFor a growing security platform with multiple subsystems and long-running operations, the boundary becomes much more interesting.\n\nThe long-term idea is not \"make a complicated VS Code extension.\"\n\nIt's to make the security core reusable.\n\nThe future might eventually look like:\n\n```\n                Aqiron Core\n               /     |      \\\n              /      |       \\\n             ↓       ↓        ↓\n\n         VS Code    CLI     Desktop\n```\n\nBut I don't need to build all three clients today.\n\nRight now, I'm using the VS Code extension as the first real client and using the Core boundary to keep the architecture ready for future evolution.\n\nThat's the part I'm most interested in getting right.\n\nThe biggest lesson I've taken from this project is that architecture isn't about drawing the biggest possible diagram.\n\nIt's about deciding where responsibilities should stop.\n\nFor Aqiron, the important boundary became:\n\nVS Code is the client.\n\nThe TypeScript runtime is the security engine.\n\nIPC is the contract between them.\n\nThat doesn't mean the architecture is finished.\n\nIt means there is now a clear place to evolve it.\n\nI'm still working through the trade-offs, so I'd be interested in hearing from people who have built:\n\nVS Code extensions with external processes\n\nTypeScript/Node developer tools\n\nlanguage-server-style architectures\n\nsecurity scanners\n\nCLI + GUI products sharing a common runtime\n\n**Aqiron Security is open source and currently under active development.**", "url": "https://wpnews.pro/news/separating-a-vs-code-extension-from-a-typescript-core-architecture-lessons-from", "canonical_source": "https://dev.to/aqiron-security/separating-a-vs-code-extension-from-a-typescript-core-architecture-lessons-from-aqiron-security-4mc5", "published_at": "2026-09-23 14:51:53+00:00", "updated_at": "2026-09-23 14:58:39.658525+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-agents"], "entities": ["Aqiron Security", "VS Code", "Node.js", "TypeScript", "Trivy", "Semgrep", "OSV-Scanner", "MobSF"], "alternates": {"html": "https://wpnews.pro/news/separating-a-vs-code-extension-from-a-typescript-core-architecture-lessons-from", "markdown": "https://wpnews.pro/news/separating-a-vs-code-extension-from-a-typescript-core-architecture-lessons-from.md", "text": "https://wpnews.pro/news/separating-a-vs-code-extension-from-a-typescript-core-architecture-lessons-from.txt", "jsonld": "https://wpnews.pro/news/separating-a-vs-code-extension-from-a-typescript-core-architecture-lessons-from.jsonld"}}