1) Generate Control Plane API scaffold Folder layout backend/control-plane/ package.json tsconfig.json src/ index.ts config.ts auth.ts registry.ts types.ts storage/ firestore.ts gcs.ts routes/ tools.ts runs.ts health.ts .env.example backend/control-plane/package.json { "name": "@productos/control-plane", "version": "0.1.0", "private": true, "type": "module", "main": "dist/index.js", "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc -p tsconfig.json", "start": "node dist/index.js", "lint": "eslint ." }, "dependencies": { "@google-cloud/firestore": "^7.11.0", "@google-cloud/storage": "^7.14.0", "@fastify/cors": "^9.0.1", "@fastify/helmet": "^12.0.0", "@fastify/rate-limit": "^9.1.0", "fastify": "^4.28.1", "zod": "^3.23.8", "nanoid": "^5.0.7" }, "devDependencies": { "@types/node": "^22.0.0", "tsx": "^4.19.0", "typescript": "^5.5.4", "eslint": "^9.8.0" } } backend/control-plane/tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "ES2022", "moduleResolution": "Bundler", "outDir": "dist", "rootDir": "src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "types": ["node"] } } backend/control-plane/.env.example PORT=8080 GCP_PROJECT_ID=your-project-id GCS_BUCKET_ARTIFACTS=productos-artifacts-dev FIRESTORE_COLLECTION_RUNS=runs FIRESTORE_COLLECTION_TOOLS=tools # If you put behind IAP / OAuth later, validate ID tokens here: AUTH_MODE=dev # dev | oauth backend/control-plane/src/config.ts export const config = { port: Number(process.env.PORT ?? 8080), projectId: process.env.GCP_PROJECT_ID ?? "", artifactsBucket: process.env.GCS_BUCKET_ARTIFACTS ?? "", runsCollection: process.env.FIRESTORE_COLLECTION_RUNS ?? "runs", toolsCollection: process.env.FIRESTORE_COLLECTION_TOOLS ?? "tools", authMode: process.env.AUTH_MODE ?? "dev" }; backend/control-plane/src/types.ts export type ToolRisk = "low" | "medium" | "high"; export type ToolDef = { name: string; description: string; risk: ToolRisk; executor: { kind: "http"; url: string; // executor base url path: string; // executor endpoint path }; inputSchema: unknown; // JSON Schema object outputSchema?: unknown; // JSON Schema object }; export type ToolInvokeRequest = { tool: string; tenant_id: string; workspace_id?: string; input: unknown; dry_run?: boolean; }; export type RunStatus = "queued" | "running" | "succeeded" | "failed"; export type RunRecord = { run_id: string; tenant_id: string; tool: string; status: RunStatus; created_at: string; updated_at: string; input: unknown; output?: unknown; error?: { message: string; details?: unknown }; artifacts?: { bucket: string; prefix: string }; }; backend/control-plane/src/auth.ts import { FastifyRequest } from "fastify"; import { config } from "./config.js"; /** * V1: dev mode = trust caller (or a shared API key later). * V2: validate Google OAuth/IAP identity token and map to tenant/org. */ export async function requireAuth(req: FastifyRequest) { if (config.authMode === "dev") return; // Placeholder for OAuth/IAP verification: // - read Authorization: Bearer // - verify token (Google JWKS) // - attach req.user throw new Error("AUTH_MODE oauth not yet implemented"); } backend/control-plane/src/storage/firestore.ts import { Firestore } from "@google-cloud/firestore"; import { config } from "../config.js"; import type { RunRecord, ToolDef } from "../types.js"; const db = new Firestore({ projectId: config.projectId }); export async function saveRun(run: RunRecord): Promise { await db.collection(config.runsCollection).doc(run.run_id).set(run, { merge: true }); } export async function getRun(runId: string): Promise { const snap = await db.collection(config.runsCollection).doc(runId).get(); return snap.exists ? (snap.data() as RunRecord) : null; } export async function saveTool(tool: ToolDef): Promise { await db.collection(config.toolsCollection).doc(tool.name).set(tool, { merge: true }); } export async function listTools(): Promise { const snap = await db.collection(config.toolsCollection).get(); return snap.docs.map(d => d.data() as ToolDef); } backend/control-plane/src/storage/gcs.ts import { Storage } from "@google-cloud/storage"; import { config } from "../config.js"; const storage = new Storage({ projectId: config.projectId }); export async function writeArtifactText(prefix: string, filename: string, content: string) { const bucket = storage.bucket(config.artifactsBucket); const file = bucket.file(`${prefix}/${filename}`); await file.save(content, { contentType: "text/plain" }); return { bucket: config.artifactsBucket, path: `${prefix}/${filename}` }; } backend/control-plane/src/registry.ts import type { ToolDef } from "./types.js"; import { listTools } from "./storage/firestore.js"; /** * Simple registry. V2: cache + versioning + per-tenant overrides. */ export async function getRegistry(): Promise> { const tools = await listTools(); return Object.fromEntries(tools.map(t => [t.name, t])); } backend/control-plane/src/routes/health.ts import type { FastifyInstance } from "fastify"; export async function healthRoutes(app: FastifyInstance) { app.get("/healthz", async () => ({ ok: true })); } backend/control-plane/src/routes/tools.ts import type { FastifyInstance } from "fastify"; import { nanoid } from "nanoid"; import { requireAuth } from "../auth.js"; import { getRegistry } from "../registry.js"; import { saveRun } from "../storage/firestore.js"; import { writeArtifactText } from "../storage/gcs.js"; import type { RunRecord, ToolInvokeRequest } from "../types.js"; async function postJson(url: string, body: unknown) { const res = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); if (!res.ok) { const txt = await res.text(); throw new Error(`Executor error ${res.status}: ${txt}`); } return res.json() as Promise; } export async function toolRoutes(app: FastifyInstance) { app.get("/tools", async (req) => { await requireAuth(req); const registry = await getRegistry(); return { tools: Object.values(registry) }; }); app.post<{ Body: ToolInvokeRequest }>("/tools/invoke", async (req) => { await requireAuth(req); const body = req.body; const registry = await getRegistry(); const tool = registry[body.tool]; if (!tool) return app.httpErrors.notFound(`Unknown tool: ${body.tool}`); const runId = `run_${new Date().toISOString().replace(/[-:.TZ]/g, "")}_${nanoid(8)}`; const now = new Date().toISOString(); const run: RunRecord = { run_id: runId, tenant_id: body.tenant_id, tool: body.tool, status: "queued", created_at: now, updated_at: now, input: body.input, artifacts: { bucket: process.env.GCS_BUCKET_ARTIFACTS ?? "", prefix: `runs/${runId}` } }; await saveRun(run); // record input artifact await writeArtifactText(`runs/${runId}`, "input.json", JSON.stringify(body, null, 2)); // execute (sync for v1; v2: push to Cloud Tasks / Workflows) try { run.status = "running"; run.updated_at = new Date().toISOString(); await saveRun(run); if (body.dry_run) { run.status = "succeeded"; run.output = { dry_run: true }; run.updated_at = new Date().toISOString(); await saveRun(run); await writeArtifactText(`runs/${runId}`, "output.json", JSON.stringify(run.output, null, 2)); return { run_id: runId, status: run.status }; } const execUrl = `${tool.executor.url}${tool.executor.path}`; const output = await postJson(execUrl, { run_id: runId, tenant_id: body.tenant_id, workspace_id: body.workspace_id, input: body.input }); run.status = "succeeded"; run.output = output; run.updated_at = new Date().toISOString(); await saveRun(run); await writeArtifactText(`runs/${runId}`, "output.json", JSON.stringify(output, null, 2)); return { run_id: runId, status: run.status }; } catch (e: any) { run.status = "failed"; run.error = { message: e?.message ?? "Unknown error" }; run.updated_at = new Date().toISOString(); await saveRun(run); await writeArtifactText(`runs/${runId}`, "error.json", JSON.stringify(run.error, null, 2)); return { run_id: runId, status: run.status }; } }); } backend/control-plane/src/routes/runs.ts import type { FastifyInstance } from "fastify"; import { requireAuth } from "../auth.js"; import { getRun } from "../storage/firestore.js"; export async function runRoutes(app: FastifyInstance) { app.get("/runs/:run_id", async (req) => { await requireAuth(req); // @ts-expect-error fastify param typing const runId = req.params.run_id as string; const run = await getRun(runId); if (!run) return app.httpErrors.notFound("Run not found"); return run; }); // V1: logs are stored as artifacts in GCS; IDE can fetch by signed URL later app.get("/runs/:run_id/logs", async (req) => { await requireAuth(req); // stub return { note: "V1: logs are in GCS artifacts under runs//" }; }); } backend/control-plane/src/index.ts import Fastify from "fastify"; import cors from "@fastify/cors"; import helmet from "@fastify/helmet"; import rateLimit from "@fastify/rate-limit"; import { config } from "./config.js"; import { healthRoutes } from "./routes/health.js"; import { toolRoutes } from "./routes/tools.js"; import { runRoutes } from "./routes/runs.js"; const app = Fastify({ logger: true }); await app.register(cors, { origin: true }); await app.register(helmet); await app.register(rateLimit, { max: 300, timeWindow: "1 minute" }); await app.register(healthRoutes); await app.register(toolRoutes); await app.register(runRoutes); app.listen({ port: config.port, host: "0.0.0.0" }).catch((err) => { app.log.error(err); process.exit(1); }); 2) Generate Tool Registry schema You want two things: A human-editable YAML (source of truth) A JSON Schema to validate tool definitions contracts/tool-registry.yaml (example) version: 1 tools: cloudrun.deploy_service: description: Deploy a Cloud Run service via Cloud Build. risk: medium executor: kind: http url: https://deploy-executor-xxxxx.a.run.app path: /execute/deploy inputSchema: type: object required: [service_name, repo, ref, env] properties: service_name: { type: string } repo: { type: string, description: "Git repo URL" } ref: { type: string, description: "Branch, tag, or commit SHA" } env: { type: string, enum: ["dev", "staging", "prod"] } outputSchema: type: object properties: service_url: { type: string } revision: { type: string } analytics.get_funnel_summary: description: Return funnel metrics for a tenant and time window. risk: low executor: kind: http url: https://analytics-executor-xxxxx.a.run.app path: /execute/funnel inputSchema: type: object required: [range_days] properties: range_days: { type: integer, minimum: 1, maximum: 365 } segment: { type: object } contracts/tool-registry.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://productos.dev/schemas/tool-registry.schema.json", "type": "object", "required": ["version", "tools"], "properties": { "version": { "type": "integer", "minimum": 1 }, "tools": { "type": "object", "additionalProperties": { "$ref": "#/$defs/ToolDef" } } }, "$defs": { "ToolDef": { "type": "object", "required": ["description", "risk", "executor", "inputSchema"], "properties": { "description": { "type": "string" }, "risk": { "type": "string", "enum": ["low", "medium", "high"] }, "executor": { "$ref": "#/$defs/Executor" }, "inputSchema": { "type": "object" }, "outputSchema": { "type": "object" } }, "additionalProperties": false }, "Executor": { "type": "object", "required": ["kind", "url", "path"], "properties": { "kind": { "type": "string", "enum": ["http"] }, "url": { "type": "string" }, "path": { "type": "string" } }, "additionalProperties": false } } } 3) Generate VSCodium extension skeleton Folder layout client-ide/extensions/gcp-productos/ package.json tsconfig.json src/extension.ts src/api.ts src/ui.ts media/icon.png (optional) client-ide/extensions/gcp-productos/package.json { "name": "gcp-productos", "displayName": "GCP Product OS", "description": "Product-centric panels (Code, Marketing, Analytics, Growth...) and backend tool invocation.", "version": "0.0.1", "publisher": "productos", "engines": { "vscode": "^1.90.0" }, "categories": ["Other"], "activationEvents": ["onStartupFinished"], "main": "./dist/extension.js", "contributes": { "commands": [ { "command": "productos.configure", "title": "Product OS: Configure Backend" }, { "command": "productos.tools.list", "title": "Product OS: List Tools" }, { "command": "productos.tools.invoke", "title": "Product OS: Invoke Tool" }, { "command": "productos.runs.open", "title": "Product OS: Open Run" } ], "configuration": { "title": "Product OS", "properties": { "productos.backendUrl": { "type": "string", "default": "http://localhost:8080", "description": "Control Plane API base URL" }, "productos.tenantId": { "type": "string", "default": "t_dev", "description": "Tenant ID for tool calls" } } } }, "scripts": { "build": "tsc -p tsconfig.json", "watch": "tsc -w -p tsconfig.json" }, "devDependencies": { "@types/node": "^22.0.0", "@types/vscode": "^1.90.0", "typescript": "^5.5.4" } } client-ide/extensions/gcp-productos/tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "CommonJS", "outDir": "dist", "rootDir": "src", "strict": true, "esModuleInterop": true, "skipLibCheck": true } } client-ide/extensions/gcp-productos/src/api.ts import * as vscode from "vscode"; function cfg(key: string): T { return vscode.workspace.getConfiguration("productos").get(key)!; } export function backendUrl() { return cfg("backendUrl"); } export function tenantId() { return cfg("tenantId"); } export async function listTools(): Promise { const res = await fetch(`${backendUrl()}/tools`); if (!res.ok) throw new Error(await res.text()); const json = await res.json(); return json.tools ?? []; } export async function invokeTool(tool: string, input: any) { const res = await fetch(`${backendUrl()}/tools/invoke`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ tool, tenant_id: tenantId(), input }) }); if (!res.ok) throw new Error(await res.text()); return res.json(); } export async function getRun(runId: string) { const res = await fetch(`${backendUrl()}/runs/${runId}`); if (!res.ok) throw new Error(await res.text()); return res.json(); } client-ide/extensions/gcp-productos/src/ui.ts import * as vscode from "vscode"; import { getRun } from "./api"; export async function showJson(title: string, obj: any) { const doc = await vscode.workspace.openTextDocument({ content: JSON.stringify(obj, null, 2), language: "json" }); await vscode.window.showTextDocument(doc, { preview: false }); vscode.window.setStatusBarMessage(title, 3000); } export async function openRun(runId: string) { const run = await getRun(runId); await showJson(`Run ${runId}`, run); } client-ide/extensions/gcp-productos/src/extension.ts import * as vscode from "vscode"; import { invokeTool, listTools } from "./api"; import { openRun, showJson } from "./ui"; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand("productos.configure", async () => { const backendUrl = await vscode.window.showInputBox({ prompt: "Control Plane backend URL" }); if (!backendUrl) return; await vscode.workspace.getConfiguration("productos").update("backendUrl", backendUrl, vscode.ConfigurationTarget.Global); vscode.window.showInformationMessage(`Product OS backend set: ${backendUrl}`); }) ); context.subscriptions.push( vscode.commands.registerCommand("productos.tools.list", async () => { const tools = await listTools(); await showJson("Tools", tools); }) ); context.subscriptions.push( vscode.commands.registerCommand("productos.tools.invoke", async () => { const tools = await listTools(); const pick = await vscode.window.showQuickPick( tools.map((t: any) => ({ label: t.name, description: t.description })), { placeHolder: "Select a tool to invoke" } ); if (!pick) return; const inputText = await vscode.window.showInputBox({ prompt: "Tool input JSON", value: "{}" }); if (!inputText) return; const input = JSON.parse(inputText); const result = await invokeTool(pick.label, input); await showJson("Invoke Result", result); if (result?.run_id) { const open = await vscode.window.showInformationMessage(`Run started: ${result.run_id}`, "Open Run"); if (open === "Open Run") await openRun(result.run_id); } }) ); context.subscriptions.push( vscode.commands.registerCommand("productos.runs.open", async () => { const runId = await vscode.window.showInputBox({ prompt: "Run ID" }); if (!runId) return; await openRun(runId); }) ); } export function deactivate() {} 4) Generate Terraform base This is a minimal hub-style baseline: GCS bucket for artifacts Firestore (Native) for runs/tools Cloud Run service for Control Plane Service accounts + IAM Placeholders for executor services Folder layout infra/terraform/ providers.tf variables.tf outputs.tf main.tf iam.tf infra/terraform/providers.tf terraform { required_version = ">= 1.5.0" required_providers { google = { source = "hashicorp/google" version = "~> 5.30" } } } provider "google" { project = var.project_id region = var.region } infra/terraform/variables.tf variable "project_id" { type = string } variable "region" { type = string default = "us-central1" } variable "artifact_bucket_name" { type = string } variable "control_plane_image" { type = string description = "Container image URI for control-plane (Artifact Registry)." } infra/terraform/main.tf resource "google_storage_bucket" "artifacts" { name = var.artifact_bucket_name location = var.region uniform_bucket_level_access = true versioning { enabled = true } } # Firestore (Native mode) – requires enabling in console once per project (or via API depending on org policy). resource "google_firestore_database" "default" { name = "(default)" location_id = var.region type = "FIRESTORE_NATIVE" } resource "google_service_account" "control_plane_sa" { account_id = "sa-control-plane" display_name = "Product OS Control Plane" } resource "google_cloud_run_v2_service" "control_plane" { name = "control-plane" location = var.region template { service_account = google_service_account.control_plane_sa.email containers { image = var.control_plane_image env { name = "GCP_PROJECT_ID" value = var.project_id } env { name = "GCS_BUCKET_ARTIFACTS" value = google_storage_bucket.artifacts.name } env { name = "AUTH_MODE" value = "dev" } } } } # Public access optional; prefer IAM auth in production. resource "google_cloud_run_v2_service_iam_member" "control_plane_public" { name = google_cloud_run_v2_service.control_plane.name location = var.region role = "roles/run.invoker" member = "allUsers" } infra/terraform/iam.tf # Allow control-plane to write artifacts in GCS resource "google_storage_bucket_iam_member" "control_plane_bucket_writer" { bucket = google_storage_bucket.artifacts.name role = "roles/storage.objectAdmin" member = "serviceAccount:${google_service_account.control_plane_sa.email}" } # Firestore access for run/tool metadata resource "google_project_iam_member" "control_plane_firestore" { project = var.project_id role = "roles/datastore.user" member = "serviceAccount:${google_service_account.control_plane_sa.email}" } # Placeholder: executor services will each have their own service accounts. # Control-plane should be granted roles/run.invoker on each executor service once created. infra/terraform/outputs.tf output "control_plane_url" { value = google_cloud_run_v2_service.control_plane.uri } output "artifact_bucket" { value = google_storage_bucket.artifacts.name }