Update documentation files

This commit is contained in:
2026-01-21 15:35:57 -08:00
parent cb8ff46020
commit 57b9ce2f1a
5 changed files with 3272 additions and 0 deletions

View File

@@ -0,0 +1,743 @@
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
}