Files

211 lines
7.9 KiB
TypeScript

import { createOrgConnection, createPluginWithSkill, denFetch } from "../evals/packages/behaviors/src/index.ts";
import type { DenSession } from "../evals/packages/behaviors/src/index.ts";
import { app } from "../evals/packages/env/src/desktop-app.ts";
import type { App } from "../evals/packages/env/src/desktop-app.ts";
import { createAdmin, createOrg, inviteMember, server } from "../evals/packages/env/src/den.ts";
import type { Den, DenOrgHandle } from "../evals/packages/env/src/den.ts";
import { mcpMock } from "../evals/packages/env/src/mock.ts";
import { resolvePlace } from "../evals/packages/env/src/place.ts";
import type { Place } from "../evals/packages/env/src/place.ts";
import { hold } from "../packages/world/src/hold.ts";
export const ACME_DOCS_ORGANIZATION_NAME = "Acme Robotics";
export const ACME_DOCS_MEMBER = "jordan";
export const ACME_DOCS_APP = "docs";
export const ACME_DOCS_WORKSPACE = "/tmp/acme/acme-robotics";
export const ACME_DOCS_PROMPT_CARDS = [
{
title: "Prepare a customer briefing",
prompt: "Review this workspace and prepare a briefing with customer goals, recent decisions, risks, and next steps.",
},
{
title: "Turn meeting notes into action",
prompt: "Turn the latest meeting notes into an action plan with owners, deadlines, dependencies, and open questions.",
},
{
title: "Draft the weekly project update",
prompt: "Summarize project progress, risks, decisions, and next week's priorities for the leadership team.",
},
];
const plugins = [
{
name: "Customer Research",
description: "Prepare for sales calls with a structured company brief.",
skillName: "customer-research",
skillDescription: "Research a company and summarize key facts before a sales call.",
skillBody: "# Instructions\n\n1. Gather the company's product, size, and recent news.\n2. Summarize the three facts that matter for this call.\n3. Suggest one opening question.",
},
{
name: "Weekly Status Report",
description: "Draft the weekly status update from recent activity.",
skillName: "weekly-status-report",
skillDescription: "Draft the weekly status update from this week's activity.",
skillBody: "# Instructions\n\n1. Collect what shipped, what slipped, and what is blocked.\n2. Write a five-line update in the team's usual format.",
},
{
name: "Meeting Notes",
description: "Turn a transcript into structured meeting notes.",
skillName: "meeting-notes",
skillDescription: "Turn a meeting transcript into decisions, owners, and follow-ups.",
skillBody: "# Instructions\n\n1. Extract decisions, owners, and deadlines from the transcript.\n2. List open questions at the end.",
},
];
export interface AcmeDocsWorld {
den: Den;
org: DenOrgHandle;
docs: App;
pluginIds: string[];
app(name: string): App;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function auth(session: DenSession): Record<string, string> {
return { authorization: `Bearer ${session.token}` };
}
async function memberId(session: DenSession, organizationId: string): Promise<string> {
const route = "/v1/me/orgs";
const result = await denFetch(session, route, { headers: auth(session) });
const organizations = isRecord(result.body) && Array.isArray(result.body.orgs)
? result.body.orgs.filter(isRecord)
: [];
const organization = organizations.find((candidate) => candidate.id === organizationId);
const id = organization && typeof organization.membershipId === "string"
? organization.membershipId
: organization && typeof organization.orgMemberId === "string"
? organization.orgMemberId
: "";
if (!result.response.ok || !id) {
throw new Error(`GET ${route} could not resolve Jordan's organization member id: HTTP ${result.response.status} ${result.text.slice(0, 500)}`);
}
return id;
}
async function enableDocsCapabilities(admin: DenSession, organizationId: string): Promise<void> {
const route = `/v1/admin/organizations/${organizationId}/capabilities`;
const result = await denFetch(admin, route, {
method: "PUT",
headers: auth(admin),
body: JSON.stringify({ capabilities: { mcpConnections: true, cloud: true } }),
});
if (!result.response.ok) {
throw new Error(`PUT ${route} failed: HTTP ${result.response.status} ${result.text.slice(0, 500)}`);
}
}
async function createProductOperationsPolicy(
den: Den,
organizationId: string,
jordan: DenSession,
): Promise<void> {
const jordanId = await memberId(jordan, organizationId);
const teamRoute = "/v1/teams";
const team = await denFetch(den.admin, teamRoute, {
method: "POST",
headers: { ...auth(den.admin), "x-openwork-org-id": organizationId },
body: JSON.stringify({ name: "Product Operations", memberIds: [jordanId] }),
});
const teamRecord = isRecord(team.body) && isRecord(team.body.team) ? team.body.team : null;
const teamId = teamRecord && typeof teamRecord.id === "string" ? teamRecord.id : "";
if (team.response.status !== 201 || !teamId) {
throw new Error(`POST ${teamRoute} failed: HTTP ${team.response.status} ${team.text.slice(0, 500)}`);
}
const policyRoute = "/v1/desktop-policies";
const policy = await denFetch(den.admin, policyRoute, {
method: "POST",
headers: { ...auth(den.admin), "x-openwork-org-id": organizationId },
body: JSON.stringify({
policyName: "Product operations prompts",
priority: 100,
isEnabled: true,
policy: {
onboardingPrompts: ACME_DOCS_PROMPT_CARDS.map((card) => card.prompt),
onboardingPromptDescriptions: ACME_DOCS_PROMPT_CARDS.map((card) => card.title),
},
memberIds: [jordanId],
teamIds: [teamId],
}),
});
if (policy.response.status !== 201) {
throw new Error(`POST ${policyRoute} failed: HTTP ${policy.response.status} ${policy.text.slice(0, 500)}`);
}
}
/** The organization, content, witness connection, policy, and member desktop used by docs shots. */
export async function bootAcmeDocs(
stack: AsyncDisposableStack,
place: Place,
): Promise<AcmeDocsWorld> {
const den = stack.use(await server({
place,
provision: false,
web: true,
mocks: { slack: mcpMock() },
env: { DEN_BOOTSTRAP_ADMIN_EMAILS: "alex@acme.dev" },
}));
const admin = await createAdmin(den, { name: "Alex Rivera", email: "alex@acme.dev" });
const org = stack.use(await createOrg(den, ACME_DOCS_ORGANIZATION_NAME));
const jordan = await inviteMember(den, ACME_DOCS_MEMBER, {
name: "Jordan Lee",
email: "jordan@acme.dev",
});
await enableDocsCapabilities(admin, org.id);
const pluginIds: string[] = [];
for (const plugin of plugins) {
const created = await createPluginWithSkill(admin, plugin);
pluginIds.push(created.id);
}
const slack = den.mocks.slack;
if (!slack) throw new Error("The Slack MCP witness did not boot.");
await createOrgConnection(admin, {
name: "Slack",
url: slack.mcpUrl,
authType: "oauth",
credentialMode: "per_member",
access: { orgWide: true },
});
await createProductOperationsPolicy(den, org.id, jordan);
const docs = stack.use(await app({
den,
place,
as: ACME_DOCS_MEMBER,
workspacePath: ACME_DOCS_WORKSPACE,
}));
return {
den,
org,
docs,
pluginIds,
app(name) {
if (name !== ACME_DOCS_APP) throw new Error(`Unknown Acme docs app ${JSON.stringify(name)}.`);
return docs;
},
};
}
export async function main(): Promise<void> {
await using stack = new AsyncDisposableStack();
const world = await bootAcmeDocs(stack, resolvePlace());
const slack = world.den.mocks.slack;
if (!slack) throw new Error("The Slack MCP witness did not boot.");
await hold({
name: "acme-docs",
outputs: {
denWeb: world.den.ref.webUrl,
denApi: world.den.ref.apiUrl,
docsCdp: world.docs.handle.cdpUrl,
slackMcp: slack.mcpUrl,
},
});
}
if (import.meta.main) await main();