Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8da1118
feat(ai): add multi-tenant support for workflows and MCP server
kojiwakayama Jan 10, 2026
cc91486
feat(ai/workflow): add worker for automatic stalled workflow recovery
kojiwakayama Jan 10, 2026
306fb7e
feat(ai/workflow): add K8s Job manager for multi-tenant isolation
kojiwakayama Jan 10, 2026
7acfe87
fix(ai/workflow): recover stalled workflows in JobManager
kojiwakayama Jan 10, 2026
115128f
refactor(ai/workflow): simplify worker code
kojiwakayama Jan 10, 2026
26bd611
fix: auto-fix CI failures
github-actions[bot] Jan 10, 2026
ee17545
refactor(ai/workflow): abstract JobExecutor for runtime flexibility
kojiwakayama Jan 10, 2026
a58b21e
docs(ai/workflow): add JobExecutor documentation
kojiwakayama Jan 10, 2026
99168f5
feat(ai/workflow): add dynamic workflow discovery for runtime loading
kojiwakayama Jan 10, 2026
6de6a48
feat(ai/workflow): add Claude Code SDK integration
kojiwakayama Jan 11, 2026
cd485bf
feat(ai/workflow): add streaming support for Claude Code agent
kojiwakayama Jan 11, 2026
4e2f4a2
docs(ai/workflow): add deployment architecture for Claude Code agents
kojiwakayama Jan 11, 2026
0af7415
feat(ai/workflow): add bidirectional WebSocket streaming
kojiwakayama Jan 11, 2026
ccb9939
feat(ai/workflow): add workspace sync for Claude Code file operations
kojiwakayama Jan 11, 2026
5534d2d
fix(ai/workflow): address security and reliability issues in workflow…
kojiwakayama Jan 11, 2026
fad5ac7
style: format code
kojiwakayama Jan 11, 2026
86ef45f
fix(ai/workflow): resolve type errors in streaming agent and tool
kojiwakayama Jan 11, 2026
be5beae
fix(test): add readiness check to RSC streaming DOM test
kojiwakayama Jan 11, 2026
7d6b1d0
style: format test files
kojiwakayama Jan 11, 2026
31b5fe1
fix(security): address additional vulnerabilities found in review
github-actions[bot] Jan 11, 2026
0631d35
fix(security): fix path traversal in uploadChanges method
github-actions[bot] Jan 11, 2026
cd7b8c1
fix(reliability): fix timer leak in bash command execution
github-actions[bot] Jan 11, 2026
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
206 changes: 206 additions & 0 deletions src/ai/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/**
* Context-Aware API Module
*
* Provides framework utilities that automatically use the current tenant context.
* Tools and workflows can import and use these without passing any context parameters.
*
* @example
* ```typescript
* import { api } from "veryfront/ai";
*
* const myTool = {
* id: "fetch-file",
* execute: async (input) => {
* // Just use api - it automatically knows the current project
* const content = await api.files.read(input.path);
* return content;
* },
* };
* ```
*/

import { getWorkflowTenant } from "./workflow/executor/step-executor.ts";
import { getCurrentRequestContext } from "../platform/adapters/fs/veryfront/multi-project-adapter.ts";
import { VeryfrontAPIClient } from "../platform/adapters/veryfront-api-client/client.ts";

/**
* Validate that a project slug is safe and well-formed.
* Prevents path traversal and injection attacks.
*
* Valid slugs: alphanumeric characters, hyphens, underscores
* Max length: 128 characters
*/
function isValidProjectSlug(slug: string): boolean {
if (!slug || typeof slug !== "string") {
return false;
}

// Check length
if (slug.length > 128) {
return false;
}

// Only allow alphanumeric, hyphens, and underscores
// Must start with alphanumeric
const validSlugPattern = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
return validSlugPattern.test(slug);
}

/**
* Get the current tenant context from either workflow execution or request context.
* @throws Error if no tenant context is available or if validation fails
*/
function getTenant() {
// Check workflow context first (for tool execution within workflows)
// Then fall back to request context (for direct API route calls)
const tenant = getWorkflowTenant() ?? getCurrentRequestContext();

if (!tenant) {
throw new Error(
"No tenant context available. " +
"This API must be called within a request or workflow execution. " +
"If you're calling this from a standalone script, you need to wrap " +
"your code with runWithContext() first.",
);
}

// Validate tenant fields to prevent injection attacks
if (!isValidProjectSlug(tenant.projectSlug)) {
throw new Error(
`Invalid project slug: "${tenant.projectSlug}". ` +
"Project slugs must be 1-128 characters, start with alphanumeric, " +
"and contain only alphanumeric characters, hyphens, or underscores.",
);
}

return tenant;
}

/**
* Create a VeryfrontAPIClient configured for the current tenant.
* Each call creates a new client instance configured with the current tenant's credentials.
*/
function getClient(): VeryfrontAPIClient {
const tenant = getTenant();

const client = new VeryfrontAPIClient({
apiBaseUrl: Deno.env.get("VERYFRONT_API_URL") || "https://api.veryfront.com",
proxyMode: true,
projectId: tenant.projectId,
projectSlug: tenant.projectSlug,
});

client.setRequestToken(tenant.token);
client.setProjectSlug(tenant.projectSlug);

return client;
}

/**
* Context-aware API that automatically uses the current tenant.
*
* All methods will throw an error if called outside of a request or workflow context.
*/
export const api = {
/**
* File operations for the current project
*/
files: {
/**
* Read file content by path
* @param path - File path relative to project root (e.g., "/pages/index.tsx")
*/
read: (path: string) => {
const tenant = getTenant();
return getClient().getFileContent(path, tenant.projectId);
},

/**
* List files in the project
* @param cursor - Pagination cursor
* @param limit - Maximum number of files to return
*/
list: (cursor?: string, limit?: number) => {
const tenant = getTenant();
return getClient().listFiles(tenant.projectId, cursor, limit);
},

/**
* List all files in the project (handles pagination automatically)
*/
listAll: () => {
const tenant = getTenant();
return getClient().listAllFiles(tenant.projectId);
},

/**
* Check if a file exists
* @param path - File path relative to project root
*/
exists: (path: string) => {
const tenant = getTenant();
return getClient().fileExists(path, tenant.projectId);
},

/**
* Get file metadata
* @param path - File path relative to project root
*/
metadata: (path: string) => {
const tenant = getTenant();
return getClient().getFileMetadata(path, tenant.projectId);
},

/**
* Search for files matching a pattern
* @param pattern - Search pattern (glob-like)
*/
search: (pattern: string) => {
const tenant = getTenant();
return getClient().searchFiles(pattern, tenant.projectId);
},
},

/**
* Project operations
*/
project: {
/**
* Get current project details
*/
get: () => {
const tenant = getTenant();
if (!tenant.projectId) {
throw new Error("Project ID not available in current context");
}
return getClient().getProject(tenant.projectId);
},

/**
* Get the current project slug
*/
slug: () => getTenant().projectSlug,

/**
* Get the current project ID (if available)
*/
id: () => getTenant().projectId,

/**
* Check if running in production mode
*/
isProduction: () => getTenant().productionMode,
},

/**
* Get the raw tenant context (for advanced use cases)
* @internal
*/
_getTenant: getTenant,

/**
* Get a configured API client (for advanced use cases)
* @internal
*/
_getClient: getClient,
};
19 changes: 19 additions & 0 deletions src/ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,25 @@ export type {
WorkflowStatus,
} from "./workflow/index.ts";

// ============================================================================
// Public API - Context-Aware Utilities
// ============================================================================

/**
* Context-aware API for accessing project resources.
* Automatically uses the current tenant context (from request or workflow execution).
*
* @example
* ```typescript
* import { api } from 'veryfront/ai';
*
* // In a tool or workflow step:
* const content = await api.files.read("/pages/index.tsx");
* const slug = api.project.slug();
* ```
*/
export { api } from "./api.ts";

// ============================================================================
// Public API - Types
// ============================================================================
Expand Down
43 changes: 42 additions & 1 deletion src/ai/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { resourceRegistry } from "./resource.ts";
import { promptRegistry } from "./prompt.ts";
import type { MCPServerConfig } from "../types/mcp.ts";
import { createError, toError } from "@veryfront/errors/veryfront-error.ts";
import { runWithRequestContext } from "../../platform/adapters/fs/veryfront/multi-project-adapter.ts";

/**
* JSON-RPC 2.0 Params type
Expand Down Expand Up @@ -315,10 +316,18 @@ export class MCPServer {
}
}

// Extract tenant context from request headers (set by proxy)
const tenantContext = this.extractTenantFromRequest(request);

// Parse JSON-RPC request
try {
const rpcRequest: JSONRPCRequest = await request.json();
const rpcResponse = await this.handleRequest(rpcRequest);

// Execute request within tenant context if available
// This enables tools to access project-scoped resources via the api module
const rpcResponse = tenantContext
? await runWithRequestContext(tenantContext, () => this.handleRequest(rpcRequest))
: await this.handleRequest(rpcRequest);

return new Response(JSON.stringify(rpcResponse), {
headers: {
Expand Down Expand Up @@ -346,6 +355,38 @@ export class MCPServer {
};
}

/**
* Extract tenant context from request headers
* Returns undefined if required headers are missing
*/
private extractTenantFromRequest(request: Request): {
projectSlug: string;
token: string;
projectId?: string;
productionMode?: boolean;
releaseId?: string | null;
} | undefined {
const token = request.headers.get("x-token");
const projectSlug = request.headers.get("x-project-slug");

// Both token and project slug are required for tenant context
if (!token || !projectSlug) {
return undefined;
}

const projectId = request.headers.get("x-project-id") || undefined;
const environment = request.headers.get("x-environment");
const releaseId = request.headers.get("x-release-id") || null;

return {
projectSlug,
token,
projectId,
productionMode: environment === "production",
releaseId,
};
}

/**
* Validate authentication
*/
Expand Down
Loading