diff --git a/.gitignore b/.gitignore index 99faaf4..621e491 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,9 @@ .altius/ *.pem *.key + +# ide/ (Altius IDE fork scaffold): upstream checkout + JS build output, never checked in +/ide/vscode/ +**/node_modules/ +/ide/extensions/*/out/ +*.vsix diff --git a/ide/README.md b/ide/README.md new file mode 100644 index 0000000..0eccfc9 --- /dev/null +++ b/ide/README.md @@ -0,0 +1,90 @@ +# Altius IDE (fork scaffold) + +Altius IDE is a from-source, rebranded build of [VS Code +OSS](https://github.com/microsoft/vscode) with an Altius-owned extension +bundled in by default, turning it into an agentic Solana development +environment: fleet dispatch, guarded security scanning, and TxGuard-guided +deploys, all inside the editor rather than a separate terminal/PWA. + +**Status: scaffold, not a shipped binary.** Building a full custom VS Code +distribution — Electron packaging, installers, code-signing, an update +server — is a substantial, ongoing engineering effort in its own right, well +beyond what one change can produce. What lives here is the real, reusable +part of that effort: + +- a branding overlay (`product.json`) and a merge script that applies it to + an upstream checkout, following the same pattern + [VSCodium](https://github.com/VSCodium/vscodium) uses to de-Microsoft and + rebrand VS Code OSS; +- a working, compilable extension (`extensions/altius-agent`) that *is* the + actual product surface — it runs today in stock VS Code, unmodified, and + is the thing `bootstrap.sh` bundles as built-in once you do run the fork + build; +- a bootstrap script that clones the pinned upstream tag and wires the two + together. + +## Why an extension instead of patching VS Code's source + +The agentic Solana workflows (dispatch a fleet run, review scan findings, +walk a guarded deploy) don't need forked editor internals — they need a +sidebar, a webview, a diagnostics collection, and an output channel, all of +which are stable extension APIs. Keeping that logic in +`extensions/altius-agent` means: + +- it's useful immediately, in any VS Code install, without anyone building + the fork; +- it's what the fork build bundles as a built-in (see below), so the fork + itself only needs branding + packaging changes, not a maintained source + patch set against upstream. + +If the fork later needs true editor-level changes (a custom activity bar +default, a different welcome page), those become entries under `patches/` +applied by `bootstrap.sh` — none exist yet because nothing so far requires +touching VS Code's own source. + +## Layout + +``` +ide/ + product.json Altius IDE branding overlay (name, app id, Open VSX gallery, telemetry off) + build/ + bootstrap.sh clones microsoft/vscode @ pinned tag, merges product.json, bundles the extension + merge-product-json.js deep-merge helper used by bootstrap.sh + patches/ reserved for upstream source patches (empty until one is needed) + extensions/ + altius-agent/ the bundled extension — see extensions/altius-agent/README.md + vscode/ gitignored; created by bootstrap.sh, not checked in +``` + +## Building the fork + +Requires Node 20+, and enough disk/bandwidth for a shallow VS Code clone +(~500MB) plus its own toolchain. + +```sh +./ide/build/bootstrap.sh +cd ide/extensions/altius-agent && npm install && npm run compile && cd - +cd ide/vscode +corepack enable && yarn install +yarn compile +./scripts/code.sh # or scripts\code.bat on Windows +``` + +`ALTIUS_VSCODE_REF` overrides the pinned upstream tag (default set in +`bootstrap.sh`). Bump it deliberately, not automatically — VS Code forks +that auto-track upstream `main` inherit unreviewed churn. + +## Trying the extension without the fork + +The extension is the part worth using today. Open +`ide/extensions/altius-agent` in stock VS Code, `npm install`, press F5 to +launch an Extension Development Host, and the same "Altius" activity-bar +view, findings tree, and guarded deploy commands are available — see +`extensions/altius-agent/README.md`. + +## Distribution (not yet built) + +Turning a local `code.sh` build into something installable (per-OS +packaging, an update feed, code signing) is deliberately out of scope here; +it's a packaging/ops project, not an editor-feature one, and shouldn't block +the extension from being useful on its own. diff --git a/ide/build/bootstrap.sh b/ide/build/bootstrap.sh new file mode 100755 index 0000000..75a4cb7 --- /dev/null +++ b/ide/build/bootstrap.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Bootstraps a full "Altius IDE" build on top of a pristine microsoft/vscode +# checkout: clones the pinned upstream tag, deep-merges ide/product.json over +# it, and bundles the altius-agent extension as a built-in. +# +# This does the actual fork checkout + merge; it does NOT run the VS Code +# build itself (that's `yarn && yarn compile` / `./scripts/code.sh`, see +# ide/README.md) — those steps need a full Node/native-modules toolchain and +# a large download that don't belong in an unattended script. +set -euo pipefail + +IDE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VSCODE_DIR="${IDE_DIR}/vscode" +VSCODE_REF="${ALTIUS_VSCODE_REF:-1.94.2}" # pinned upstream tag; bump deliberately + +if [ -d "${VSCODE_DIR}/.git" ]; then + echo "==> ${VSCODE_DIR} already exists, skipping clone (delete it to re-clone)" +else + echo "==> Cloning microsoft/vscode @ ${VSCODE_REF} into ${VSCODE_DIR}" + git clone --depth 1 --branch "${VSCODE_REF}" \ + https://github.com/microsoft/vscode.git "${VSCODE_DIR}" +fi + +echo "==> Merging ide/product.json over vscode/product.json" +node "${IDE_DIR}/build/merge-product-json.js" \ + "${VSCODE_DIR}/product.json" \ + "${IDE_DIR}/product.json" \ + "${VSCODE_DIR}/product.json" + +echo "==> Bundling altius-agent as a built-in extension" +mkdir -p "${VSCODE_DIR}/extensions/altius-agent" +rsync -a --delete \ + --exclude node_modules \ + --exclude out \ + --exclude '.vsix' \ + "${IDE_DIR}/extensions/altius-agent/" \ + "${VSCODE_DIR}/extensions/altius-agent/" + +cat <<'EOF' + +==> Bootstrap complete. + +Next steps (inside ide/vscode), see ide/README.md for details: + 1. corepack enable && yarn install + 2. yarn compile + 3. ./scripts/code.sh (Linux/macOS) or .\scripts\code.bat (Windows) + +The bundled altius-agent extension still needs its own compile step first: + (cd ide/extensions/altius-agent && npm install && npm run compile) +EOF diff --git a/ide/build/merge-product-json.js b/ide/build/merge-product-json.js new file mode 100644 index 0000000..b1931d6 --- /dev/null +++ b/ide/build/merge-product-json.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node +// Deep-merges an Altius branding overlay over a base VS Code OSS +// product.json, writing the result to `out`. `overlay` values win on +// conflicts; arrays are replaced wholesale rather than concatenated. +"use strict"; + +const fs = require("fs"); + +const [, , basePath, overlayPath, outPath] = process.argv; +if (!basePath || !overlayPath || !outPath) { + console.error( + "usage: merge-product-json.js ", + ); + process.exit(1); +} + +function readJson(path) { + return JSON.parse(fs.readFileSync(path, "utf8")); +} + +function deepMerge(base, overlay) { + if (Array.isArray(overlay)) return overlay; + if (typeof overlay !== "object" || overlay === null) return overlay; + const out = { ...(typeof base === "object" && base ? base : {}) }; + for (const [key, value] of Object.entries(overlay)) { + out[key] = deepMerge(out[key], value); + } + return out; +} + +const base = readJson(basePath); +const overlay = readJson(overlayPath); +const merged = deepMerge(base, overlay); + +fs.writeFileSync(outPath, JSON.stringify(merged, null, 2) + "\n"); +console.log(`wrote ${outPath}`); diff --git a/ide/extensions/altius-agent/.vscodeignore b/ide/extensions/altius-agent/.vscodeignore new file mode 100644 index 0000000..41e2bec --- /dev/null +++ b/ide/extensions/altius-agent/.vscodeignore @@ -0,0 +1,6 @@ +.vscode/** +src/** +.gitignore +tsconfig.json +**/*.map +node_modules/** diff --git a/ide/extensions/altius-agent/README.md b/ide/extensions/altius-agent/README.md new file mode 100644 index 0000000..f118510 --- /dev/null +++ b/ide/extensions/altius-agent/README.md @@ -0,0 +1,58 @@ +# Altius Agent + +A VS Code extension for agentic Solana development against +[Altius Code](https://github.com/daemon-blockint-tech/altius-code): dispatch +fleet runs, review guarded security scan findings inline, and walk a +TxGuard-guided deploy — without leaving the editor. + +This is the extension bundled as a built-in by the Altius IDE fork scaffold +(`ide/`), but it is a normal extension: it runs in stock VS Code today. + +## Features + +- **Fleet Dispatch** (Altius activity bar view) — send a prompt to an agent + (`altius`, `security`, `browser`, ...) via a running `altius fleet serve` + instance, watch runs update, and resume/deny/cancel runs that pause for + approval. Talks to the same [BeeAI ACP](https://agentcommunicationprotocol.dev) + `/runs*` HTTP API as the project's existing PWA thin client + (`crates/altius-cli/assets/pwa`). +- **Security Findings** (Altius activity bar view) — `Altius: Scan Project + for Security Findings` runs `altius scan --format json`, surfaces findings + as editor diagnostics (Problems panel, inline squiggles) and as a tree + grouped by severity; click a finding to jump to its location. +- **Guarded Deploy** — `Altius: Guarded Deploy (Dry Run)` runs `altius + deploy --dry-run` (policy + mandatory simulation only; `FailClosed` + guarantees nothing is ever signed). `Altius: Guarded Deploy (Sign & + Submit)` runs the real pipeline behind a modal warning and a typed + confirmation, streaming TxGuard's policy/simulate/diff/audit/sign steps to + the "Altius" output channel. + +## Requirements + +- The `altius` CLI on `PATH` (or set `altius.cliPath`) — see the [repo + README](../../../README.md#ways-to-run) to build it. +- For Fleet Dispatch: a running `altius fleet serve` instance (defaults to + `http://127.0.0.1:8788`, overridable via `altius.fleetUrl`). Set a bearer + token with `Altius: Set Fleet Bearer Token` if the server requires one. +- For deploy: `altius-signerd` running and `ALTIUS_SIGNER_SOCKET` set in the + environment VS Code was launched from (same requirement as the CLI). + +## Settings + +| Setting | Default | Description | +|---|---|---| +| `altius.cliPath` | `altius` | Path to the `altius` binary. | +| `altius.fleetUrl` | `http://127.0.0.1:8788` | Base URL of `altius fleet serve`. | +| `altius.scanPath` | *(workspace root)* | Path passed to `altius scan --path`, relative to the workspace root. | +| `altius.scanChain` | `auto` | Chain family passed to `altius scan --chain`. | +| `altius.deployProjectPath` | *(workspace root)* | Path passed to `altius deploy --project`, relative to the workspace root. | + +## Development + +```sh +npm install +npm run compile # or: npm run watch +``` + +Press `F5` in VS Code (with this folder open) to launch an Extension +Development Host with the extension loaded. diff --git a/ide/extensions/altius-agent/media/altius.svg b/ide/extensions/altius-agent/media/altius.svg new file mode 100644 index 0000000..bc4cf91 --- /dev/null +++ b/ide/extensions/altius-agent/media/altius.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ide/extensions/altius-agent/package-lock.json b/ide/extensions/altius-agent/package-lock.json new file mode 100644 index 0000000..b2402fd --- /dev/null +++ b/ide/extensions/altius-agent/package-lock.json @@ -0,0 +1,59 @@ +{ + "name": "altius-agent", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "altius-agent", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^20.11.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.4.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/ide/extensions/altius-agent/package.json b/ide/extensions/altius-agent/package.json new file mode 100644 index 0000000..ab27953 --- /dev/null +++ b/ide/extensions/altius-agent/package.json @@ -0,0 +1,128 @@ +{ + "name": "altius-agent", + "displayName": "Altius Agent", + "description": "Agentic Solana development: fleet dispatch, guarded security scanning, and TxGuard-guided deploys, inside the editor.", + "version": "0.1.0", + "publisher": "altius", + "private": true, + "license": "Apache-2.0", + "engines": { + "vscode": "^1.85.0" + }, + "categories": ["Other", "Linters"], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./out/extension.js", + "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "altius", + "title": "Altius", + "icon": "media/altius.svg" + } + ] + }, + "views": { + "altius": [ + { + "type": "webview", + "id": "altius.dispatchView", + "name": "Fleet Dispatch" + }, + { + "id": "altius.findingsView", + "name": "Security Findings" + } + ] + }, + "commands": [ + { + "command": "altius.runScan", + "title": "Altius: Scan Project for Security Findings", + "icon": "$(search)" + }, + { + "command": "altius.clearFindings", + "title": "Altius: Clear Security Findings", + "icon": "$(clear-all)" + }, + { + "command": "altius.deployDryRun", + "title": "Altius: Guarded Deploy (Dry Run)", + "icon": "$(rocket)" + }, + { + "command": "altius.deployLive", + "title": "Altius: Guarded Deploy (Sign & Submit)" + }, + { + "command": "altius.setFleetToken", + "title": "Altius: Set Fleet Bearer Token" + }, + { + "command": "altius.focusDispatch", + "title": "Altius: Focus Fleet Dispatch" + }, + { + "command": "altius.revealFinding", + "title": "Altius: Reveal Finding in Editor" + } + ], + "menus": { + "view/title": [ + { + "command": "altius.runScan", + "when": "view == altius.findingsView", + "group": "navigation" + }, + { + "command": "altius.clearFindings", + "when": "view == altius.findingsView", + "group": "navigation" + } + ] + }, + "configuration": { + "title": "Altius", + "properties": { + "altius.cliPath": { + "type": "string", + "default": "altius", + "description": "Path to the `altius` binary used for scan/deploy commands. Defaults to resolving `altius` from PATH." + }, + "altius.scanPath": { + "type": "string", + "default": "", + "description": "Path to scan, relative to the workspace root. Empty scans the whole workspace root." + }, + "altius.scanChain": { + "type": "string", + "default": "auto", + "description": "Chain family passed to `altius scan --chain` (e.g. auto, svm, evm)." + }, + "altius.fleetUrl": { + "type": "string", + "default": "http://127.0.0.1:8788", + "description": "Base URL of a running `altius fleet serve` instance (BeeAI ACP run lifecycle)." + }, + "altius.deployProjectPath": { + "type": "string", + "default": "", + "description": "Project path passed to `altius deploy --project`, relative to the workspace root. Empty uses the workspace root." + } + } + } + }, + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -w -p ./", + "lint": "tsc --noEmit -p ./" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.4.0" + } +} diff --git a/ide/extensions/altius-agent/src/beeacpClient.ts b/ide/extensions/altius-agent/src/beeacpClient.ts new file mode 100644 index 0000000..be32e30 --- /dev/null +++ b/ide/extensions/altius-agent/src/beeacpClient.ts @@ -0,0 +1,152 @@ +// Thin BeeAI ACP client mirroring the wire shape served by +// `altius fleet serve` (crates/altius-protocol/src/beeacp). Kept dependency +// free (uses the extension host's global fetch) so this compiles without +// pulling in an HTTP library. + +export type RunStatus = + | "created" + | "in-progress" + | "awaiting" + | "completed" + | "failed" + | "cancelled"; + +export interface MessagePart { + content_type: string; + content: string; +} + +export interface Message { + role: string; + parts: MessagePart[]; +} + +export interface LamportDelta { + account: string; + delta_lamports: string; +} + +export interface TransactionPreview { + action_summary?: string; + lamport_deltas?: LamportDelta[]; + invoked_programs?: string[]; + compute_units_consumed?: number; + compute_unit_limit?: number; +} + +export interface RunApproval { + summary: string; + reason?: string; + node?: string; + kind: "generic" | "transaction"; + transaction?: TransactionPreview; +} + +export interface Run { + run_id: string; + agent_name: string; + status: RunStatus; + input: Message[]; + output: Message[]; + error?: string; + approval?: RunApproval; + created_at: string; + finished_at?: string; +} + +export interface ApprovalDecision { + approved: boolean; + note?: string; +} + +export interface ResumeRunRequest { + message?: Message; + decision?: ApprovalDecision; +} + +export interface BeeAcpClientOptions { + baseUrl: string; + token?: string; +} + +export function userTextMessage(text: string): Message { + return { role: "user", parts: [{ content_type: "text/plain", content: text }] }; +} + +export class BeeAcpClient { + private readonly baseUrl: string; + private readonly token?: string; + + constructor(options: BeeAcpClientOptions) { + this.baseUrl = options.baseUrl.replace(/\/+$/, ""); + this.token = options.token; + } + + private authHeaders(extra?: Record): Record { + const headers: Record = { "content-type": "application/json", ...extra }; + if (this.token) headers.authorization = `Bearer ${this.token}`; + return headers; + } + + private async request(path: string, init?: RequestInit): Promise { + const response = await fetch(`${this.baseUrl}${path}`, { + ...init, + headers: this.authHeaders(init?.headers as Record | undefined), + }); + const text = await response.text(); + let body: unknown = null; + try { + body = text ? JSON.parse(text) : null; + } catch { + body = { raw: text }; + } + if (!response.ok) { + const message = extractErrorMessage(body) ?? `${response.status} ${response.statusText}`; + throw new Error(message); + } + return body as T; + } + + listRuns(): Promise { + return this.request("/runs"); + } + + getRun(id: string): Promise { + return this.request(`/runs/${encodeURIComponent(id)}`); + } + + createRun(agentName: string, input: Message[]): Promise { + return this.request("/runs", { + method: "POST", + body: JSON.stringify({ agent_name: agentName, input }), + }); + } + + resumeRun(id: string, body: ResumeRunRequest = {}): Promise { + return this.request(`/runs/${encodeURIComponent(id)}`, { + method: "POST", + body: JSON.stringify(body), + }); + } + + cancelRun(id: string): Promise { + return this.request(`/runs/${encodeURIComponent(id)}/cancel`, { + method: "POST", + body: "{}", + }); + } +} + +function extractErrorMessage(body: unknown): string | undefined { + if (!body || typeof body !== "object") return undefined; + const record = body as Record; + const error = record.error; + if (error && typeof error === "object") { + const message = (error as Record).message; + if (typeof message === "string") return message; + } + if (typeof error === "string") return error; + if (typeof record.message === "string") return record.message; + if (typeof record.detail === "string") return record.detail; + return undefined; +} diff --git a/ide/extensions/altius-agent/src/cliRunner.ts b/ide/extensions/altius-agent/src/cliRunner.ts new file mode 100644 index 0000000..9ce5214 --- /dev/null +++ b/ide/extensions/altius-agent/src/cliRunner.ts @@ -0,0 +1,55 @@ +import { spawn } from "child_process"; + +export interface CliResult { + code: number | null; + stdout: string; + stderr: string; +} + +export interface RunCliOptions { + cwd?: string; + onStdoutLine?: (line: string) => void; + onStderrLine?: (line: string) => void; +} + +/** Runs `bin args...` to completion, buffering full stdout/stderr. */ +export function runCli(bin: string, args: string[], options: RunCliOptions = {}): Promise { + return new Promise((resolve, reject) => { + const child = spawn(bin, args, { cwd: options.cwd, shell: false }); + let stdout = ""; + let stderr = ""; + let stdoutTail = ""; + let stderrTail = ""; + + child.stdout.on("data", (chunk: Buffer) => { + const text = chunk.toString("utf8"); + stdout += text; + if (options.onStdoutLine) { + stdoutTail = emitLines(stdoutTail + text, options.onStdoutLine); + } + }); + child.stderr.on("data", (chunk: Buffer) => { + const text = chunk.toString("utf8"); + stderr += text; + if (options.onStderrLine) { + stderrTail = emitLines(stderrTail + text, options.onStderrLine); + } + }); + child.on("error", (err) => { + reject(new Error(`failed to run \`${bin}\`: ${err.message}`)); + }); + child.on("close", (code) => { + if (stdoutTail && options.onStdoutLine) options.onStdoutLine(stdoutTail); + if (stderrTail && options.onStderrLine) options.onStderrLine(stderrTail); + resolve({ code, stdout, stderr }); + }); + }); +} + +/** Emits complete lines from `buffer` via `onLine`, returning the unterminated remainder. */ +function emitLines(buffer: string, onLine: (line: string) => void): string { + const lines = buffer.split("\n"); + const remainder = lines.pop() ?? ""; + for (const line of lines) onLine(line); + return remainder; +} diff --git a/ide/extensions/altius-agent/src/config.ts b/ide/extensions/altius-agent/src/config.ts new file mode 100644 index 0000000..39d03fb --- /dev/null +++ b/ide/extensions/altius-agent/src/config.ts @@ -0,0 +1,59 @@ +import * as vscode from "vscode"; + +const SECTION = "altius"; +const TOKEN_SECRET_KEY = "altius.fleetToken"; + +export function workspaceRoot(): string | undefined { + return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; +} + +export function cliPath(): string { + return vscode.workspace.getConfiguration(SECTION).get("cliPath", "altius"); +} + +export function scanPath(): string { + const root = workspaceRoot(); + const relative = vscode.workspace.getConfiguration(SECTION).get("scanPath", ""); + if (!root) return relative || "."; + return relative ? joinPath(root, relative) : root; +} + +export function scanChain(): string { + return vscode.workspace.getConfiguration(SECTION).get("scanChain", "auto"); +} + +export function deployProjectPath(): string { + const root = workspaceRoot(); + const relative = vscode.workspace + .getConfiguration(SECTION) + .get("deployProjectPath", ""); + if (!root) return relative || "."; + return relative ? joinPath(root, relative) : root; +} + +export function fleetUrl(): string { + return vscode.workspace + .getConfiguration(SECTION) + .get("fleetUrl", "http://127.0.0.1:8788") + .replace(/\/+$/, ""); +} + +export async function fleetToken(secrets: vscode.SecretStorage): Promise { + return secrets.get(TOKEN_SECRET_KEY); +} + +export async function setFleetToken( + secrets: vscode.SecretStorage, + token: string, +): Promise { + if (token) { + await secrets.store(TOKEN_SECRET_KEY, token); + } else { + await secrets.delete(TOKEN_SECRET_KEY); + } +} + +function joinPath(root: string, relative: string): string { + const sep = root.endsWith("/") || root.endsWith("\\") ? "" : "/"; + return `${root}${sep}${relative}`; +} diff --git a/ide/extensions/altius-agent/src/deployController.ts b/ide/extensions/altius-agent/src/deployController.ts new file mode 100644 index 0000000..b38c332 --- /dev/null +++ b/ide/extensions/altius-agent/src/deployController.ts @@ -0,0 +1,74 @@ +import * as vscode from "vscode"; +import { runCli } from "./cliRunner"; +import * as config from "./config"; + +const CONFIRM_PHRASE = "DEPLOY"; + +export class DeployController { + constructor(private readonly output: vscode.OutputChannel) {} + + /** `altius deploy --dry-run`: policy + mandatory simulation only, `FailClosed` guarantees nothing is ever signed. */ + async dryRun(): Promise { + await this.runDeploy(["--dry-run"], "dry run (policy + simulate only, nothing signed)"); + } + + /** + * `altius deploy --yes`: runs the full guarded pipeline + * (policy → simulate → diff → approve → audit → sign) with `AutoApprove` + * in place of the CLI's interactive terminal prompt, since a spawned, + * non-TTY child process cannot answer that prompt. This is the same + * `--yes` flag documented for headless/CI use in + * crates/altius-cli/src/deploy_command.rs — TxGuard's policy, simulation, + * and audit trail still run unchanged; only the interactive y/N step + * moves to this confirmation dialog instead. + */ + async liveDeploy(): Promise { + const warned = await vscode.window.showWarningMessage( + "This signs and broadcasts real transactions through TxGuard (policy + simulation still apply, but nothing is dry-run). This cannot be undone.", + { modal: true }, + "Continue", + ); + if (warned !== "Continue") return; + + const typed = await vscode.window.showInputBox({ + prompt: `Type ${CONFIRM_PHRASE} to confirm signing and broadcasting`, + placeHolder: CONFIRM_PHRASE, + ignoreFocusOut: true, + }); + if (typed !== CONFIRM_PHRASE) { + void vscode.window.showInformationMessage("Altius: deploy cancelled (confirmation text did not match)."); + return; + } + + await this.runDeploy(["--yes"], "live (signing and broadcasting via TxGuard)"); + } + + private async runDeploy(extraArgs: string[], label: string): Promise { + const bin = config.cliPath(); + const project = config.deployProjectPath(); + const args = ["deploy", "--project", project, ...extraArgs]; + + this.output.show(true); + this.output.appendLine(`\n$ ${bin} ${args.join(" ")}`); + this.output.appendLine(`(${label})`); + + await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: `Altius: deploy (${label})` }, + async () => { + const result = await runCli(bin, args, { + cwd: project, + onStdoutLine: (line) => this.output.appendLine(line), + onStderrLine: (line) => this.output.appendLine(line), + }); + + if (result.code === 0) { + void vscode.window.showInformationMessage(`Altius deploy (${label}) finished.`); + } else { + void vscode.window.showErrorMessage( + `Altius deploy (${label}) failed (exit ${result.code}). See "Altius" output channel.`, + ); + } + }, + ); + } +} diff --git a/ide/extensions/altius-agent/src/dispatchViewProvider.ts b/ide/extensions/altius-agent/src/dispatchViewProvider.ts new file mode 100644 index 0000000..bb0f0cd --- /dev/null +++ b/ide/extensions/altius-agent/src/dispatchViewProvider.ts @@ -0,0 +1,307 @@ +import * as vscode from "vscode"; +import { BeeAcpClient, Run, userTextMessage } from "./beeacpClient"; +import * as config from "./config"; + +type FromWebviewMessage = + | { type: "dispatch"; agentName: string; prompt: string } + | { type: "refresh" } + | { type: "selectRun"; runId: string } + | { type: "resume"; runId: string; approved: boolean; note?: string } + | { type: "cancel"; runId: string }; + +type ToWebviewMessage = + | { type: "runs"; runs: Run[] } + | { type: "selected"; run: Run | null } + | { type: "error"; message: string } + | { type: "dispatching"; value: boolean }; + +const POLL_INTERVAL_MS = 2000; + +export class DispatchViewProvider implements vscode.WebviewViewProvider { + static readonly viewType = "altius.dispatchView"; + + private view: vscode.WebviewView | undefined; + private pollHandle: ReturnType | undefined; + private selectedRunId: string | undefined; + + constructor( + private readonly extensionUri: vscode.Uri, + private readonly secrets: vscode.SecretStorage, + private readonly output: vscode.OutputChannel, + ) {} + + resolveWebviewView(webviewView: vscode.WebviewView): void { + this.view = webviewView; + webviewView.webview.options = { enableScripts: true, localResourceRoots: [this.extensionUri] }; + webviewView.webview.html = this.getHtml(webviewView.webview); + + webviewView.webview.onDidReceiveMessage((message: FromWebviewMessage) => { + void this.handleMessage(message); + }); + + webviewView.onDidChangeVisibility(() => { + if (webviewView.visible) { + this.startPolling(); + } else { + this.stopPolling(); + } + }); + webviewView.onDidDispose(() => this.stopPolling()); + + this.startPolling(); + } + + reveal(): void { + this.view?.show?.(true); + } + + private async client(): Promise { + return new BeeAcpClient({ baseUrl: config.fleetUrl(), token: await config.fleetToken(this.secrets) }); + } + + private startPolling(): void { + this.stopPolling(); + void this.refresh(); + this.pollHandle = setInterval(() => void this.refresh(), POLL_INTERVAL_MS); + } + + private stopPolling(): void { + if (this.pollHandle) clearInterval(this.pollHandle); + this.pollHandle = undefined; + } + + private post(message: ToWebviewMessage): void { + void this.view?.webview.postMessage(message); + } + + private async refresh(): Promise { + if (!this.view?.visible) return; + try { + const client = await this.client(); + const runs = await client.listRuns(); + runs.sort((a, b) => b.created_at.localeCompare(a.created_at)); + this.post({ type: "runs", runs }); + if (this.selectedRunId) { + const selected = runs.find((run) => run.run_id === this.selectedRunId) ?? null; + this.post({ type: "selected", run: selected }); + } + } catch (err) { + this.post({ type: "error", message: `Cannot reach fleet at ${config.fleetUrl()}: ${String(err)}` }); + } + } + + private async handleMessage(message: FromWebviewMessage): Promise { + try { + const client = await this.client(); + switch (message.type) { + case "dispatch": { + if (!message.prompt.trim()) return; + this.post({ type: "dispatching", value: true }); + const run = await client.createRun(message.agentName, [userTextMessage(message.prompt)]); + this.selectedRunId = run.run_id; + this.post({ type: "dispatching", value: false }); + await this.refresh(); + break; + } + case "refresh": + await this.refresh(); + break; + case "selectRun": + this.selectedRunId = message.runId; + await this.refresh(); + break; + case "resume": + await client.resumeRun(message.runId, { + decision: { approved: message.approved, note: message.note }, + }); + await this.refresh(); + break; + case "cancel": + await client.cancelRun(message.runId); + await this.refresh(); + break; + } + } catch (err) { + this.output.appendLine(`dispatch error: ${String(err)}`); + this.post({ type: "error", message: String(err) }); + this.post({ type: "dispatching", value: false }); + } + } + + private getHtml(webview: vscode.Webview): string { + const nonce = getNonce(); + const csp = [ + `default-src 'none'`, + `img-src ${webview.cspSource}`, + `style-src ${webview.cspSource} 'unsafe-inline'`, + `script-src 'nonce-${nonce}'`, + ].join("; "); + + return /* html */ ` + + + + + + + + +
+ + + +
+
+

Runs

+
+
+ + + +`; + } +} + +function getNonce(): string { + let text = ""; + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} diff --git a/ide/extensions/altius-agent/src/extension.ts b/ide/extensions/altius-agent/src/extension.ts new file mode 100644 index 0000000..6e28c61 --- /dev/null +++ b/ide/extensions/altius-agent/src/extension.ts @@ -0,0 +1,56 @@ +import * as vscode from "vscode"; +import * as config from "./config"; +import { DeployController } from "./deployController"; +import { DispatchViewProvider } from "./dispatchViewProvider"; +import { Finding } from "./findings"; +import { FindingsTreeProvider } from "./findingsTreeProvider"; +import { ScanController } from "./scanController"; + +export function activate(context: vscode.ExtensionContext): void { + const output = vscode.window.createOutputChannel("Altius"); + context.subscriptions.push(output); + + const diagnostics = vscode.languages.createDiagnosticCollection("altius"); + context.subscriptions.push(diagnostics); + + const findingsTree = new FindingsTreeProvider(); + const findingsView = vscode.window.createTreeView("altius.findingsView", { + treeDataProvider: findingsTree, + }); + context.subscriptions.push(findingsView); + + const scanController = new ScanController(diagnostics, findingsTree, output); + const deployController = new DeployController(output); + + const dispatchProvider = new DispatchViewProvider(context.extensionUri, context.secrets, output); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(DispatchViewProvider.viewType, dispatchProvider), + ); + + context.subscriptions.push( + vscode.commands.registerCommand("altius.runScan", () => scanController.runScan()), + vscode.commands.registerCommand("altius.clearFindings", () => scanController.clear()), + vscode.commands.registerCommand("altius.deployDryRun", () => deployController.dryRun()), + vscode.commands.registerCommand("altius.deployLive", () => deployController.liveDeploy()), + vscode.commands.registerCommand("altius.focusDispatch", () => dispatchProvider.reveal()), + vscode.commands.registerCommand("altius.revealFinding", (finding: Finding) => + scanController.revealFinding(finding), + ), + vscode.commands.registerCommand("altius.setFleetToken", async () => { + const token = await vscode.window.showInputBox({ + prompt: "Bearer token for the running `altius fleet serve` instance (blank to clear)", + password: true, + ignoreFocusOut: true, + }); + if (token === undefined) return; + await config.setFleetToken(context.secrets, token); + void vscode.window.showInformationMessage( + token ? "Altius: fleet token saved." : "Altius: fleet token cleared.", + ); + }), + ); +} + +export function deactivate(): void { + // Nothing to tear down: disposables are owned by context.subscriptions. +} diff --git a/ide/extensions/altius-agent/src/findings.ts b/ide/extensions/altius-agent/src/findings.ts new file mode 100644 index 0000000..8498aec --- /dev/null +++ b/ide/extensions/altius-agent/src/findings.ts @@ -0,0 +1,51 @@ +// Mirrors the JSON shape of altius_findings::{ScanReport, Finding} — +// see crates/altius-findings/src/{report,finding,severity}.rs. Kept as a +// plain structural type so `altius scan --format json` output can be +// JSON.parse()'d straight into this shape. + +export type Severity = "info" | "low" | "medium" | "high" | "critical"; +export type Confidence = "low" | "medium" | "high"; + +export interface FindingLocation { + file: string; + start_line?: number; + end_line?: number; + start_column?: number; + end_column?: number; + snippet?: string; +} + +export interface Finding { + id: string; + chain: string; + pattern_id: string; + severity: Severity; + confidence: Confidence; + title: string; + description: string; + location: FindingLocation; + attack_scenario?: string; + recommendation?: string; + tool: string; + fingerprint: string; +} + +export interface ScanReport { + target: string; + chain?: string; + findings: Finding[]; + scanners: string[]; + notes?: string; +} + +const SEVERITY_ORDER: Record = { + critical: 0, + high: 1, + medium: 2, + low: 3, + info: 4, +}; + +export function compareBySeverity(a: Finding, b: Finding): number { + return SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]; +} diff --git a/ide/extensions/altius-agent/src/findingsTreeProvider.ts b/ide/extensions/altius-agent/src/findingsTreeProvider.ts new file mode 100644 index 0000000..89088e5 --- /dev/null +++ b/ide/extensions/altius-agent/src/findingsTreeProvider.ts @@ -0,0 +1,107 @@ +import * as vscode from "vscode"; +import { compareBySeverity, Finding, Severity } from "./findings"; + +type TreeNode = SeverityGroupNode | FindingNode; + +class SeverityGroupNode { + readonly kind = "group" as const; + constructor( + readonly severity: Severity, + readonly findings: Finding[], + ) {} +} + +class FindingNode { + readonly kind = "finding" as const; + constructor(readonly finding: Finding) {} +} + +const SEVERITY_ICONS: Record = { + critical: new vscode.ThemeIcon("error", new vscode.ThemeColor("errorForeground")), + high: new vscode.ThemeIcon("error"), + medium: new vscode.ThemeIcon("warning"), + low: new vscode.ThemeIcon("info"), + info: new vscode.ThemeIcon("info"), +}; + +export class FindingsTreeProvider implements vscode.TreeDataProvider { + private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event; + + private findings: Finding[] = []; + private lastScannedTarget: string | undefined; + + setFindings(findings: Finding[], target?: string): void { + this.findings = [...findings].sort(compareBySeverity); + this.lastScannedTarget = target; + this.onDidChangeTreeDataEmitter.fire(); + } + + clear(): void { + this.findings = []; + this.lastScannedTarget = undefined; + this.onDidChangeTreeDataEmitter.fire(); + } + + getTreeItem(node: TreeNode): vscode.TreeItem { + if (node.kind === "group") { + const item = new vscode.TreeItem( + `${capitalize(node.severity)} (${node.findings.length})`, + vscode.TreeItemCollapsibleState.Expanded, + ); + item.iconPath = SEVERITY_ICONS[node.severity]; + item.contextValue = "altiusSeverityGroup"; + return item; + } + const finding = node.finding; + const item = new vscode.TreeItem(finding.title, vscode.TreeItemCollapsibleState.None); + const location = finding.location.start_line + ? `${finding.location.file}:${finding.location.start_line}` + : finding.location.file; + item.description = location; + item.tooltip = new vscode.MarkdownString( + `**${finding.title}** (${finding.severity}, ${finding.confidence} confidence)\n\n` + + `${finding.description}\n\n` + + (finding.recommendation ? `_Recommendation:_ ${finding.recommendation}\n\n` : "") + + `\`${finding.pattern_id}\` · ${location}`, + ); + item.iconPath = SEVERITY_ICONS[finding.severity]; + item.contextValue = "altiusFinding"; + item.command = { + command: "altius.revealFinding", + title: "Reveal Finding", + arguments: [finding], + }; + return item; + } + + getChildren(node?: TreeNode): TreeNode[] { + if (!node) { + const groups = new Map(); + for (const finding of this.findings) { + const bucket = groups.get(finding.severity) ?? []; + bucket.push(finding); + groups.set(finding.severity, bucket); + } + return [...groups.entries()] + .sort(([a], [b]) => compareBySeverity({ severity: a } as Finding, { severity: b } as Finding)) + .map(([severity, findings]) => new SeverityGroupNode(severity, findings)); + } + if (node.kind === "group") { + return node.findings.map((finding) => new FindingNode(finding)); + } + return []; + } + + get scannedTarget(): string | undefined { + return this.lastScannedTarget; + } + + get count(): number { + return this.findings.length; + } +} + +function capitalize(text: string): string { + return text.charAt(0).toUpperCase() + text.slice(1); +} diff --git a/ide/extensions/altius-agent/src/scanController.ts b/ide/extensions/altius-agent/src/scanController.ts new file mode 100644 index 0000000..3c1c68a --- /dev/null +++ b/ide/extensions/altius-agent/src/scanController.ts @@ -0,0 +1,128 @@ +import * as path from "path"; +import * as vscode from "vscode"; +import { runCli } from "./cliRunner"; +import * as config from "./config"; +import { Finding, ScanReport, Severity } from "./findings"; +import { FindingsTreeProvider } from "./findingsTreeProvider"; + +const SEVERITY_TO_DIAGNOSTIC: Record = { + critical: vscode.DiagnosticSeverity.Error, + high: vscode.DiagnosticSeverity.Error, + medium: vscode.DiagnosticSeverity.Warning, + low: vscode.DiagnosticSeverity.Information, + info: vscode.DiagnosticSeverity.Hint, +}; + +export class ScanController { + constructor( + private readonly diagnostics: vscode.DiagnosticCollection, + private readonly tree: FindingsTreeProvider, + private readonly output: vscode.OutputChannel, + ) {} + + clear(): void { + this.diagnostics.clear(); + this.tree.clear(); + } + + async runScan(): Promise { + const target = config.scanPath(); + const chain = config.scanChain(); + const bin = config.cliPath(); + + await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: `Altius: scanning ${target}` }, + async () => { + this.output.appendLine(`$ ${bin} scan --path ${target} --chain ${chain} --format json`); + const result = await runCli(bin, [ + "scan", + "--path", + target, + "--chain", + chain, + "--format", + "json", + ]); + + if (result.stderr.trim()) { + this.output.appendLine(result.stderr.trim()); + } + + let report: ScanReport; + try { + report = JSON.parse(result.stdout) as ScanReport; + } catch { + this.output.show(true); + void vscode.window.showErrorMessage( + `Altius scan produced no parseable output (exit ${result.code}). See "Altius" output channel.`, + ); + return; + } + + this.applyReport(report, target); + + const high = report.findings.filter( + (f) => f.severity === "high" || f.severity === "critical", + ).length; + const message = `Altius scan: ${report.findings.length} finding(s)${ + high ? `, ${high} high/critical` : "" + }`; + this.output.appendLine(message); + if (high > 0) { + void vscode.window.showWarningMessage(message); + } else { + void vscode.window.showInformationMessage(message); + } + }, + ); + } + + private applyReport(report: ScanReport, scanRoot: string): void { + this.diagnostics.clear(); + this.tree.setFindings(report.findings, report.target); + + const byFile = new Map(); + for (const finding of report.findings) { + const absolutePath = path.isAbsolute(finding.location.file) + ? finding.location.file + : path.resolve(scanRoot, finding.location.file); + const line = Math.max(0, (finding.location.start_line ?? 1) - 1); + const endLine = Math.max(line, (finding.location.end_line ?? finding.location.start_line ?? 1) - 1); + const startCol = Math.max(0, (finding.location.start_column ?? 1) - 1); + const endCol = Math.max(startCol + 1, (finding.location.end_column ?? finding.location.start_column ?? 2) - 1); + const range = new vscode.Range(line, startCol, endLine, endCol); + + const diagnostic = new vscode.Diagnostic( + range, + `${finding.title}: ${finding.description}`, + SEVERITY_TO_DIAGNOSTIC[finding.severity], + ); + diagnostic.source = `altius (${finding.tool})`; + diagnostic.code = finding.pattern_id; + const bucket = byFile.get(absolutePath) ?? []; + bucket.push(diagnostic); + byFile.set(absolutePath, bucket); + } + + for (const [file, fileDiagnostics] of byFile) { + this.diagnostics.set(vscode.Uri.file(file), fileDiagnostics); + } + } + + async revealFinding(finding: Finding): Promise { + const target = this.tree.scannedTarget ?? config.scanPath(); + const absolutePath = path.isAbsolute(finding.location.file) + ? finding.location.file + : path.resolve(target, finding.location.file); + const line = Math.max(0, (finding.location.start_line ?? 1) - 1); + try { + const document = await vscode.workspace.openTextDocument(absolutePath); + const editor = await vscode.window.showTextDocument(document); + const position = new vscode.Position(line, 0); + editor.selection = new vscode.Selection(position, position); + editor.revealRange(new vscode.Range(position, position), vscode.TextEditorRevealType.InCenter); + } catch (err) { + void vscode.window.showErrorMessage(`Altius: cannot open ${absolutePath}: ${String(err)}`); + } + } +} diff --git a/ide/extensions/altius-agent/tsconfig.json b/ide/extensions/altius-agent/tsconfig.json new file mode 100644 index 0000000..e7fdc06 --- /dev/null +++ b/ide/extensions/altius-agent/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "lib": ["ES2022"], + "outDir": "out", + "rootDir": "src", + "sourceMap": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ide/product.json b/ide/product.json new file mode 100644 index 0000000..ab84ee1 --- /dev/null +++ b/ide/product.json @@ -0,0 +1,35 @@ +{ + "_comment": "Branding overlay for a VS Code OSS build named 'Altius IDE'. Applied on top of microsoft/vscode's own product.json by ide/build/bootstrap.sh (deep-merged, this file wins). Field names/meaning follow the VS Code OSS product.json contract used by community builds (VSCodium etc.).", + "nameShort": "Altius IDE", + "nameLong": "Altius IDE", + "applicationName": "altius-ide", + "dataFolderName": ".altius-ide", + "win32MutexName": "altiuside", + "win32DirName": "Altius IDE", + "win32NameVersion": "Altius IDE", + "win32RegValueName": "AltiusIDE", + "win32AppId": "REPLACE_WITH_GENERATED_GUID_win32AppId", + "win32x64AppId": "REPLACE_WITH_GENERATED_GUID_win32x64AppId", + "win32arm64AppId": "REPLACE_WITH_GENERATED_GUID_win32arm64AppId", + "win32AppUserModelId": "Altius.AltiusIDE", + "win32ShellNameShort": "Altius IDE", + "darwinBundleIdentifier": "tech.altius.ide", + "linuxIconName": "altius-ide", + "licenseUrl": "https://github.com/daemon-blockint-tech/altius-code/blob/main/LICENSE", + "reportIssueUrl": "https://github.com/daemon-blockint-tech/altius-code/issues/new", + "documentationUrl": "https://github.com/daemon-blockint-tech/altius-code#readme", + "requestFeatureUrl": "https://github.com/daemon-blockint-tech/altius-code/issues/new", + "urlProtocol": "altius-ide", + "quality": "stable", + "extensionsGallery": { + "_comment": "No official VS Code Marketplace access without Microsoft's terms. Point at Open VSX, the community-run alternative, same as VSCodium.", + "serviceUrl": "https://open-vsx.org/vscode/gallery", + "itemUrl": "https://open-vsx.org/vscode/item" + }, + "builtInExtensions": [], + "extensionAllowedProposedApi": [], + "enableTelemetry": false, + "aiConfig": { + "ariaKey": "" + } +}