Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/validate-inventory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ jobs:
print(f" MCP: {s}")
PYEOF

- name: Assert docs match opencode.json MCP count
run: |
python3 << 'PYEOF'
import json, sys
with open('opencode.json') as f:
mcp = json.load(f).get('mcp', {})
total = len(mcp)
enabled = sum(1 for v in mcp.values() if v.get('enabled', True))
checks = [('README.md', f'{total} configured'), ('AGENTS.md', f'{total} MCP servers')]
fails = []
for path, needle in checks:
if needle not in open(path).read():
fails.append(f'{path}: missing "{needle}"')
if fails:
print('DRIFT: documented MCP count does not match opencode.json')
for f in fails:
print(' -', f)
print(f' actual: {total} configured, {enabled} enabled')
sys.exit(1)
print(f'Docs match config: {total} configured, {enabled} enabled')
PYEOF

- name: Check skills count
run: |
count=$(find .opencode/skills -name "SKILL.md" 2>/dev/null | wc -l)
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ offloaded to a compute box (`cs run "pnpm test"`) or CI — do not run them loca

- `mcp-server/` — the `opencode-workbench` MCP server + connector (TypeScript).
Tools: `inspect_target`, `plan_clone`, `apply_clone`, `verify_clone`,
`install_component`, `workbench_info`. Local + SSH transports.
`install_component`, `bootstrap_host`, `workbench_info`. Local + SSH transports.
See `mcp-server/README.md` and `docs/architecture/clone-mcp.md`.
- `connector.json` — connector manifest / registration snippet.
- `plugins/` — the `memory.ts` + `doc-tools.ts` OpenCode plugins (portable,
Expand All @@ -23,7 +23,8 @@ offloaded to a compute box (`cs run "pnpm test"`) or CI — do not run them loca
The build box is provisioned by the equivalent manual steps documented here
(opencode CLI, npm-global MCP servers, vendored MCPs, Docker containers,
toolchain) — see `docs/architecture/workstation-playbook.md`.
- `opencode.json` — the canonical workbench config: 29 MCP servers, all secrets
- `opencode.json` — the canonical workbench config: 35 MCP servers (33 enabled,
2 disabled: `google-search`, `google-workspace`), all secrets
use `{env:VAR}` syntax. Copied to both `/workspaces/workbench/`
(project config) and `~/.config/opencode/opencode.json` (global) on the build box.
If you edit the repo copy, re-sync both.
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ opencode-workbench/
| Category | Details |
|----------|---------|
| **OpenCode** | CLI v1.18.5, DeepSeek V4 Pro model, LSP enabled |
| **MCP Servers** | 35 configured (see [`docs/architecture/mcp-inventory.md`](docs/architecture/mcp-inventory.md)) |
| **MCP Servers** | 35 configured (33 enabled, 2 disabled), 9 remote + 26 local |
| **Agent Skills** | 45 reusable skills in `.opencode/skills/` |
| **Infrastructure** | PostgreSQL 16 (Docker), Browserless Chromium (Docker) |
| **Tools** | pandoc, jq, miller, sqlite3, GitHub CLI, Playwright Chromium |
Expand Down Expand Up @@ -162,12 +162,12 @@ Full details: [`docs/recovery/RECOVERY_PLAYBOOK.md`](docs/recovery/RECOVERY_PLAY

## MCP Architecture

| Type | Count | Servers |
|------|-------|---------|
| Remote | 4 | context7, gh_grep, clerk, vercel (OAuth) |
| Local (npm) | 11 | brave-search, postgres, playwright, shadcn, echarts, mermaid, saga, swagger-testcase, design-system, figma, sentry |
| Local (vendored) | 2 | perplexity-agent-mcp, browserless-mcp |
| Local (binary) | 1 | github-mcp-server |
| Type | Count | Notes |
|------|-------|-------|
| Remote | 9 | context7, gh_grep, vdd, exa, cloudflare, clerk, vercel (token auth), sentry, stripe |
| Local | 26 | npm / vendored / binary stdio servers |

Totals: **35 configured** (33 enabled, 2 disabled: `google-search`, `google-workspace`).

See: [`docs/architecture/mcp-inventory.md`](docs/architecture/mcp-inventory.md)

Expand Down
120 changes: 120 additions & 0 deletions constitution.md
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.
31 changes: 31 additions & 0 deletions docs/CHANGELOG_WORKBENCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,37 @@

---

## [2026-10-03] — VDD adoption + `bootstrap_host`

### Added
- **VDD chain** (Phase 0–8): `constitution.md`, `vdd/vision.md`, `vdd/strategy.md`,
`vdd/tactics.md`, `vdd/specs/bootstrap-bare-host/{spec,plan,data-model,tasks}.md`
+ `contracts/bootstrap_host.md`, `vdd/gates/G1–G7.md`,
`vdd/impact-report.generated.md`.
- **`bootstrap_host` MCP tool** — one-call provisioning of a bare Linux target
(local or SSH): kernel-up platform scan + dry-run upgrade plan, latest-stable
OpenCode install with recorded version pin, VDD profile apply, verify, and a
target-free `help` parameter. (`mcp-server/src/bootstrap.ts`, `platform.ts`)
- New unit tests `mcp-server/test/platform.test.ts` (20) and a `help` test.

### Changed
- `mcp-server/src/probe.ts` + `clone.ts`: emit `osIdLike` (`ID_LIKE`) for exact
distro-family detection.
- Reconciled MCP counts: `README.md` (19→35) and `AGENTS.md` (29→35 configured,
33 enabled, 2 disabled).
- CI `validate-inventory.yml`: assert documented MCP counts match `opencode.json`.

### Fixed
- Canonical non-interactive upgrade flag placement (`apt-get -y dist-upgrade`).
- `AGENTS.md` tool list now includes `bootstrap_host`.

### Known Issues
- A-005/A-006 partially traced; README free-text counts now asserted by CI.
- Constitution `[PENDING]` decisions: VDD adoption scope, `DB_PATH` drift,
`WORKFLOW_STATE.md` staleness.

---

## [2026-07-27] — Master Backup

### Changed
Expand Down
1 change: 1 addition & 0 deletions mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ From a checkout, use the local build instead:
| `apply_clone` | Clone the profile + install missing components + write rendered config. Idempotent. |
| `install_component` | Install a single component by id. |
| `verify_clone` | Re-check config, env template, and every component. |
| `bootstrap_host` | One-call provisioning: kernel-up platform scan + dry-run upgrade plan, latest-stable OpenCode + version pin, apply, verify; `help:true` documents parameters without touching the target. |
| `list_required_credentials` | Value-blind: which credentials are missing and how to acquire each. |
| `run_auth_flow` | Emit-and-verify acquisition guidance for one credential. |

Expand Down
155 changes: 155 additions & 0 deletions mcp-server/src/bootstrap.ts
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);

@vercel vercel Bot Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unquoted interpolation of options.opencodeVersion into a target shell command allows arbitrary command execution on the target host.

Fix on Vercel

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,
};
}
Loading
Loading