Files
master-ai/1.Generate Control Plane API scaffold.md
2026-01-21 15:35:57 -08:00

21 KiB
Raw Permalink Blame History

  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 <id_token> // - 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<RunRecord | null> { 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<ToolDef[]> { 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<Record<string, ToolDef>> { 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/<run_id>/" }; }); }

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); });

  1. 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 } } }

  1. 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<any[]> { 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() {}

  1. 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 }