Repository navigation
feat(vdd): bootstrap_host one-call provisioning + VDD chain (V-001→TK-006) #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| # Project Constitution | ||
|
|
||
| Status: Active | ||
| Version: 1.0.0 | ||
| Last updated: 2026-10-03 | ||
|
|
||
| > Impact Chain: Phase 0 — Constitution (immutable) | ||
| > Governs every later VDD phase. Amendments require `vdd_amend` and a | ||
| > `docs/CHANGELOG_WORKBENCH.md` entry. | ||
|
|
||
| ## Mission | ||
|
|
||
| Provide a reproducible, self-documenting, recoverable OpenCode development | ||
| workstation — configuration, agent skills, MCP stack, and the | ||
| `opencode-workbench` MCP server that clones the whole profile onto any Linux | ||
| machine (local or SSH). The repository is the single source of truth; runtime | ||
| drift is a defect. | ||
|
|
||
| ## Architecture Principles | ||
|
|
||
| - **Repository is the source of truth**: runtime configuration (on the build | ||
| box) must match repository configuration. Any divergence is a bug. | ||
| - **Secrets never touch disk**: values flow | ||
| `build-box Secrets → devcontainer.json remoteEnv → process env → {env:VAR}`. | ||
| No secret values, prefixes, or partials in repo files. | ||
| - **Thin launcher, single app**: the root package is a launcher; the only | ||
| application code is `mcp-server/`. There is no second app to invent. | ||
| - **MCP-first**: tooling is exposed as MCP servers/tools with Zod-validated | ||
| inputs; the clone connector never reads or transmits secret values. | ||
| - **Compute is remote**: builds, tests, lint and typecheck run on a compute box | ||
| (`cs run`) or CI — never on the editing workstation. | ||
| - **Documentation lives with code**: docs describe the system as it is, not as | ||
| imagined. Drift is detectable, not tolerated. | ||
|
|
||
| ## Technology Stack | ||
|
|
||
| | Layer | Choice | Notes | | ||
| |-------|--------|-------| | ||
| | Language | TypeScript 5.6.x | ESM (`"type": "module"`), `strict: true`, `noUncheckedIndexedAccess: true` | | ||
| | Runtime | Node.js >= 20 | TS target/module `ES2022` / `NodeNext` | | ||
| | Package manager | pnpm 9 | Matches `pnpm-lock.yaml`; pnpm 10 rejects the lockfile | | ||
| | Protocol SDK | `@modelcontextprotocol/sdk` ^1.31 | stdio + Streamable HTTP transports | | ||
| | Validation | Zod ^4.6 | All tool inputs/outputs | | ||
| | Tests | Vitest ^3 (`--root mcp-server`) | `pnpm test`, `pnpm test:coverage` | | ||
| | Build | `tsc -p mcp-server/tsconfig.json` | Output `mcp-server/dist` | | ||
| | Hosted endpoint | Vercel serverless functions (`api/*.js`, `vercel.json`) | Glama / remote connector | | ||
| | Database | None in project code | Workbench runtime wires PostgreSQL via MCP for other projects | | ||
| | Scripts | Bash (`scripts/`) + Node (`scripts/selftest`) | Inventories, backup, recovery, secret scan | | ||
|
|
||
| ## Security Constraints | ||
|
|
||
| - No secret values, prefixes, or partials in repository files — enforced by | ||
| `.gitignore` (`.env*`, `*.pem`, `*.key`, `credentials*`, `secrets*`) and | ||
| gitleaks (`secret-scan.yml`, `.gitleaks.toml`). | ||
| - The clone connector writes an empty `~/.env.workbench` template; it must | ||
| never read or transmit secret values. | ||
| - All external input at a tool boundary is validated with Zod. | ||
| - Never log tokens, passwords, or PII. | ||
| - New secrets are documented by name and purpose only in | ||
| `docs/architecture/secrets.md` — never by value. | ||
| - `scripts/security/scan-secrets.sh` must pass before commit. | ||
|
|
||
| ## Naming Conventions | ||
|
|
||
| - Files: kebab-case (`run-master-backup.sh`, `mcp-server/src/render.ts`) | ||
| - Variables/functions: camelCase | ||
| - Types/interfaces: PascalCase | ||
| - Constants / env vars: SCREAMING_SNAKE_CASE | ||
| - MCP tools: snake_case verbs (`inspect_target`, `plan_clone`, `apply_clone`) | ||
| - VDD IDs: `V-###` vision, `S-###` strategy, `T-###` tactics, `A-###` action | ||
| items, `SP-##` specs, `PL-##` plans, `TK-##` tasks | ||
|
|
||
| ## Banned Patterns | ||
|
|
||
| - No `any` in TypeScript (strict + `noUncheckedIndexedAccess`). | ||
| - No plain `.js` source files in `src/` — TypeScript only. | ||
| - No default exports when a named export suffices. | ||
| - No `.then()` chains where `async/await` applies. | ||
| - No secret values or prefixes committed, printed, or logged. | ||
| - No hardcoded absolute build-box paths in committed config without a | ||
| documented reason (known defect: `DB_PATH` hardcodes `/workspaces/workbench`). | ||
| - No new app code outside `mcp-server/` (the root package is a launcher). | ||
| - No comments added to source unless requested. | ||
|
|
||
| ## File Structure Rules | ||
|
|
||
| ``` | ||
| mcp-server/ # the opencode-workbench MCP server (src/ + test/) | ||
| src/ # inspect / plan / apply / verify; local + SSH + HTTP | ||
| plugins/ # OpenCode plugins (memory.ts, doc-tools.ts) | ||
| scripts/ # backup / inventory / security / recovery / maintenance / selftest | ||
| vendor/ # vendored MCP source (perplexity-agent-mcp, browserless-mcp) | ||
| configs/ # reference MCP / shell / git / provider configuration | ||
| docs/ # charter, architecture, recovery, prompts, audit, VDD-derived records | ||
| .opencode/skills/ # the only tracked content under .opencode/ | ||
| api/ # Vercel serverless handlers for the hosted connector | ||
| constitution.md # this file (VDD Phase 0) | ||
| vdd/ # VDD artifacts (vision → strategy → tactics → specs; Phase 1+) | ||
| ``` | ||
|
|
||
| ## Domain Primitives | ||
|
|
||
| - workstation-config | ||
| - mcp-server | ||
| - agent-skills | ||
| - automation-scripts | ||
| - recovery-and-backup | ||
|
|
||
| ## Open Questions / Deferred Decisions | ||
|
|
||
| - [PENDING] VDD adoption scope — is the full 8-phase chain required for this | ||
| repo, or only Phases 0–3 (constitution/vision/strategy/tactics) over the | ||
| existing hand-rolled `docs/` system? (Prior review found 0 VDD artifacts.) | ||
| - [PENDING] MCP server count is stated 19 / 18 / 29 / 35 across | ||
| `README.md`, `AGENTS.md`, and `opencode.json`; reconcile and add a CI | ||
| assertion. | ||
| - [PENDING] `DB_PATH` path drift (`/workspaces/workbench` vs the actual | ||
| `/workspaces/<repo-name>`); backlog I13/I21. | ||
| - [PENDING] `WORKFLOW_STATE.md` staleness and whether the AI-Web-App and | ||
| Web-Scraper tracks remain in scope. |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| /** | ||
| * `bootstrap_host` orchestration: take a bare Linux target (local or SSH) to a | ||
| * VDD-configured OpenCode workstation in one call. | ||
| * | ||
| * Order: inspect → platform plan (dry-run unless `upgrade`) → apply the profile | ||
| * (installs latest-stable OpenCode) → capture the resolved version → verify. | ||
| * Never reads or transmits secret values; the env template is names-only. | ||
| */ | ||
| import type { Target } from "./target.js"; | ||
| import { | ||
| apply, | ||
| inspect, | ||
| verify, | ||
| type ApplyResult, | ||
| type CloneOptions, | ||
| type VerifyResult, | ||
| } from "./clone.js"; | ||
| import { | ||
| buildUpgradePlan, | ||
| planPlatform, | ||
| parseVersion, | ||
| type PackageFamily, | ||
| type PackageManagerId, | ||
| type PackagePlan, | ||
| } from "./platform.js"; | ||
|
|
||
| export interface BootstrapOptions extends CloneOptions { | ||
| /** Execute the platform upgrade (default false: plan only). */ | ||
| upgrade?: boolean; | ||
| /** Use non-interactive upgrade flags (default true). */ | ||
| assumeYes?: boolean; | ||
| /** Pin a specific OpenCode version instead of latest stable. */ | ||
| opencodeVersion?: string; | ||
| } | ||
|
|
||
| export interface PlatformReport { | ||
| osId: string | null; | ||
| osIdLike: string[]; | ||
| osName: string | null; | ||
| osVersion: string | null; | ||
| kernel: string | null; | ||
| arch: string | null; | ||
| packageManager: PackageManagerId | null; | ||
| family: PackageFamily; | ||
| } | ||
|
|
||
| export interface OpenCodeInstall { | ||
| installed: boolean; | ||
| version: string | null; | ||
| requested: string | null; | ||
| } | ||
|
|
||
| export interface BootstrapResult { | ||
| target: string; | ||
| platform: PlatformReport; | ||
| upgrade: PackagePlan; | ||
| opencode: OpenCodeInstall; | ||
| apply: ApplyResult; | ||
| verify: VerifyResult; | ||
| warnings: string[]; | ||
| } | ||
|
|
||
| function platformReport(info: { | ||
| osId?: string; | ||
| osIdLike?: string; | ||
| osName?: string; | ||
| osVersion?: string; | ||
| kernel?: string; | ||
| arch?: string; | ||
| packageManager?: string; | ||
| }): PlatformReport { | ||
| const packageManager = planPlatform(info); | ||
| const family: PackageFamily = packageManager | ||
| ? buildUpgradePlan(packageManager).family | ||
| : "unknown"; | ||
| return { | ||
| osId: info.osId ?? null, | ||
| osIdLike: (info.osIdLike ?? "") | ||
| .toLowerCase() | ||
| .split(/\s+/) | ||
| .filter(Boolean), | ||
| osName: info.osName ?? null, | ||
| osVersion: info.osVersion ?? null, | ||
| kernel: info.kernel ?? null, | ||
| arch: info.arch ?? null, | ||
| packageManager, | ||
| family, | ||
| }; | ||
| } | ||
|
|
||
| async function captureOpencodeVersion(target: Target, requested: string | null): Promise<string | null> { | ||
| const res = await target.run( | ||
| `export PATH="$HOME/.opencode/bin:$PATH"\nopencode --version 2>/dev/null | head -n1\n`, | ||
| { timeoutMs: 30_000 }, | ||
| ); | ||
| const parsed = parseVersion(res.stdout); | ||
| return parsed ?? requested; | ||
| } | ||
|
|
||
| export async function bootstrap( | ||
| target: Target, | ||
| options: BootstrapOptions = {}, | ||
| ): Promise<BootstrapResult> { | ||
| const warnings: string[] = []; | ||
| const info = await inspect(target); | ||
| const platform = platformReport(info); | ||
| const upgrade = buildUpgradePlan(platform.packageManager, { | ||
| assumeYes: options.assumeYes !== false, | ||
| }); | ||
|
|
||
| if (!platform.packageManager) { | ||
| warnings.push( | ||
| `unsupported package manager (osId=${platform.osId ?? "unknown"}); platform upgrade skipped`, | ||
| ); | ||
| } | ||
|
|
||
| // Optional, explicitly-requested platform upgrade. Dry-run by default. | ||
| if (options.upgrade && !options.dryRun && platform.packageManager) { | ||
| for (const command of upgrade.commands) { | ||
| const res = await target.run(`${command}\n`, { timeoutMs: 1_800_000 }); | ||
| upgrade.output.push(`${command} (exit ${res.code})`); | ||
| } | ||
| upgrade.executed = true; | ||
| upgrade.rebootAdvisory = true; | ||
| } | ||
|
|
||
| // Pin a specific version first if requested, then let apply fill the rest. | ||
| if (options.opencodeVersion && !options.dryRun) { | ||
| await target.run( | ||
| `curl -fsSL https://opencode.ai/install | bash -s -- --version ${options.opencodeVersion} || true\n`, | ||
| { timeoutMs: 300_000 }, | ||
| ); | ||
| } | ||
|
|
||
| const applyResult = await apply(target, options); | ||
| const opencodeInstalled = applyResult.installed.some((i) => i.id === "opencode"); | ||
| const version = options.dryRun | ||
| ? options.opencodeVersion ?? null | ||
| : await captureOpencodeVersion(target, options.opencodeVersion ?? null); | ||
| const verifyResult = await verify(target, options); | ||
|
|
||
| return { | ||
| target: target.label, | ||
| platform, | ||
| upgrade, | ||
| opencode: { | ||
| installed: opencodeInstalled, | ||
| version, | ||
| requested: options.opencodeVersion ?? null, | ||
| }, | ||
| apply: applyResult, | ||
| verify: verifyResult, | ||
| warnings, | ||
| }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unquoted interpolation of
options.opencodeVersioninto a target shell command allows arbitrary command execution on the target host.