feat(ops): add workspace MCP deployment overlays

This commit is contained in:
Codex
2026-07-19 12:34:06 +03:00
parent 56b766b23b
commit 847a08da93
28 changed files with 9865 additions and 0 deletions
@@ -0,0 +1,332 @@
import type { AgentSessionRecord } from "../repositories/agents.js";
export class TaskerAdapterNotConfiguredError extends Error {
constructor() {
super("NODEDC_INTERNAL_ACCESS_TOKEN is required for Tasker adapter calls.");
this.name = "TaskerAdapterNotConfiguredError";
}
}
export class TaskerAdapterError extends Error {
constructor(
message: string,
readonly statusCode: number,
readonly payload: unknown
) {
super(message);
this.name = "TaskerAdapterError";
}
}
export class TaskerAdapterUnavailableError extends Error {
constructor(readonly causeError: unknown) {
super("Tasker internal adapter is unavailable.");
this.name = "TaskerAdapterUnavailableError";
}
}
export type TaskerClientConfig = {
baseUrl: string;
internalAccessToken?: string;
};
export type TaskerAgentContext = {
agentId: string;
ownerUserId: string;
tokenId: string;
};
export type GrantedProjectInput = {
workspace_slug: string;
project_id: string | null;
mode: string;
scopes: string[];
};
export type ListGrantedProjectsInput = {
grants: GrantedProjectInput[];
};
export type CreateIssueInput = {
project_id: string;
workspace_slug?: string | null;
title: string;
description?: string;
priority?: "none" | "low" | "medium" | "high" | "urgent";
structured_blocks?: unknown[];
};
export type CreateProjectInput = {
workspace_slug: string;
name: string;
identifier?: string;
description?: string;
};
export type GetIssueInput = {
project_id: string;
workspace_slug?: string | null;
};
export type UpdateIssueInput = {
project_id: string;
workspace_slug?: string | null;
title?: string;
description?: string;
priority?: "none" | "low" | "medium" | "high" | "urgent";
structured_blocks?: unknown[];
};
export type MoveIssueInput = {
project_id: string;
workspace_slug?: string | null;
state_id: string;
};
export type CommentInput = {
project_id: string;
workspace_slug?: string | null;
body: string;
};
export type AttachFileInput = {
project_id: string;
workspace_slug?: string | null;
file_name: string;
mime_type: string;
content_base64: string;
};
export type SetLabelsInput = {
project_id: string;
workspace_slug?: string | null;
label_ids: string[];
};
export type EnsureLabelsInput = {
project_id: string;
workspace_slug?: string | null;
labels: Array<{
name: string;
color?: string;
}>;
};
export type AssignIssueInput = {
project_id: string;
workspace_slug?: string | null;
member_ids: string[];
};
export class TaskerClient {
constructor(private readonly config: TaskerClientConfig) {}
async listGrantedProjects(session: AgentSessionRecord): Promise<unknown> {
return this.request("/api/internal/nodedc/agent/projects/resolve", {
method: "POST",
session,
body: {
grants: session.grants.map((grant) => ({
workspace_slug: grant.workspaceSlug,
project_id: grant.projectId,
mode: grant.mode,
scopes: grant.scopes,
})),
} satisfies ListGrantedProjectsInput,
});
}
async getProjectContext(session: AgentSessionRecord, projectId: string, workspaceSlug?: string | null): Promise<unknown> {
const searchParams = new URLSearchParams();
if (workspaceSlug) {
searchParams.set("workspace_slug", workspaceSlug);
}
return this.request(`/api/internal/nodedc/agent/projects/${encodeURIComponent(projectId)}/context?${searchParams.toString()}`, {
method: "GET",
session,
});
}
async createProject(session: AgentSessionRecord, input: CreateProjectInput): Promise<unknown> {
return this.request("/api/internal/nodedc/agent/projects", {
method: "POST",
session,
body: input,
});
}
async listIssues(session: AgentSessionRecord, projectId: string, workspaceSlug?: string | null, query?: string): Promise<unknown> {
const searchParams = new URLSearchParams({ project_id: projectId });
if (workspaceSlug) {
searchParams.set("workspace_slug", workspaceSlug);
}
if (query) {
searchParams.set("query", query);
}
return this.request(`/api/internal/nodedc/agent/issues?${searchParams.toString()}`, {
method: "GET",
session,
});
}
async getIssue(session: AgentSessionRecord, issueId: string, input: GetIssueInput): Promise<unknown> {
const searchParams = new URLSearchParams({ project_id: input.project_id });
if (input.workspace_slug) {
searchParams.set("workspace_slug", input.workspace_slug);
}
return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}?${searchParams.toString()}`, {
method: "GET",
session,
});
}
async createIssue(session: AgentSessionRecord, input: CreateIssueInput): Promise<unknown> {
return this.request("/api/internal/nodedc/agent/issues", {
method: "POST",
session,
body: input,
});
}
async updateIssue(session: AgentSessionRecord, issueId: string, input: UpdateIssueInput): Promise<unknown> {
return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}`, {
method: "PATCH",
session,
body: input,
});
}
async moveIssue(session: AgentSessionRecord, issueId: string, input: MoveIssueInput): Promise<unknown> {
return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}/move`, {
method: "POST",
session,
body: input,
});
}
async appendComment(session: AgentSessionRecord, issueId: string, input: CommentInput): Promise<unknown> {
return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}/comments`, {
method: "POST",
session,
body: input,
});
}
async attachFile(session: AgentSessionRecord, issueId: string, input: AttachFileInput): Promise<unknown> {
return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}/attachments`, {
method: "POST",
session,
body: input,
});
}
async setLabels(session: AgentSessionRecord, issueId: string, input: SetLabelsInput): Promise<unknown> {
return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}/labels`, {
method: "PUT",
session,
body: input,
});
}
async ensureLabels(session: AgentSessionRecord, input: EnsureLabelsInput): Promise<unknown> {
return this.request(`/api/internal/nodedc/agent/projects/${encodeURIComponent(input.project_id)}/labels/ensure`, {
method: "POST",
session,
body: input,
});
}
async assignIssue(session: AgentSessionRecord, issueId: string, input: AssignIssueInput): Promise<unknown> {
return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}/assignees`, {
method: "PUT",
session,
body: input,
});
}
private async request(
path: string,
input: {
method: "GET" | "POST" | "PATCH" | "PUT";
session: AgentSessionRecord;
body?: unknown;
}
): Promise<unknown> {
if (!this.config.internalAccessToken) {
throw new TaskerAdapterNotConfiguredError();
}
const response = await this.fetchTasker(path, input);
const payload = await readResponsePayload(response);
if (!response.ok) {
throw new TaskerAdapterError("Tasker internal adapter request failed.", response.status, payload);
}
return payload;
}
private async fetchTasker(
path: string,
input: {
method: "GET" | "POST" | "PATCH" | "PUT";
session: AgentSessionRecord;
body?: unknown;
}
): Promise<Response> {
try {
const requestBody = attachAgentMetadata(input.session, input.body);
return await fetch(new URL(path, this.config.baseUrl), {
method: input.method,
headers: {
Authorization: `Bearer ${this.config.internalAccessToken}`,
"Content-Type": "application/json",
"X-NODEDC-Agent-Id": input.session.agent.id,
"X-NODEDC-Agent-Owner-User-Id": input.session.agent.ownerUserId,
"X-NODEDC-Agent-Token-Id": input.session.token.id,
},
body: requestBody === undefined ? undefined : JSON.stringify(requestBody),
});
} catch (error) {
throw new TaskerAdapterUnavailableError(error);
}
}
}
function attachAgentMetadata(session: AgentSessionRecord, body: unknown): unknown {
if (body === undefined || !isPlainRecord(body)) {
return body;
}
return {
...body,
_agent: {
display_name: session.agent.displayName,
avatar_url: session.agent.avatarUrl,
},
};
}
function isPlainRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
async function readResponsePayload(response: Response): Promise<unknown> {
const text = await response.text();
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch {
return text;
}
}