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

743 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> {
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<void> {
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<unknown>;
}
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);
});
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<T>(key: string): T {
return vscode.workspace.getConfiguration("productos").get<T>(key)!;
}
export function backendUrl() {
return cfg<string>("backendUrl");
}
export function tenantId() {
return cfg<string>("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() {}
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
}