feat(ops): add workspace MCP deployment overlays
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user