Skip to content

Commit b46db91

Browse files
Implement workflow fleet canvas runtime
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent b514372 commit b46db91

3 files changed

Lines changed: 1555 additions & 78 deletions

File tree

Lines changed: 171 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,182 @@
1-
// Extension: process-workflow-fleet
2-
// Inspect Process-PSModule caller workflows and prepare safe v8 migrations.
3-
//
4-
// This single-file skeleton is a starting point. For more complex canvases
5-
// (multiple actions with non-trivial logic, shared state, a custom renderer,
6-
// etc.) prefer splitting things out: move each action handler into its own
7-
// function, extract `open`/`onClose` into helpers, and pull large units
8-
// (renderer assets, schema definitions, shared utilities) into sibling files
9-
// imported from this entry point. Keep extension.mjs focused on wiring.
1+
import { dirname } from "node:path";
2+
import { fileURLToPath } from "node:url";
103

11-
import { createServer } from "node:http";
12-
import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
4+
import {
5+
CanvasError,
6+
createCanvas,
7+
joinSession,
8+
} from "@github/copilot-sdk/extension";
139

14-
// One local HTTP server per open canvas instance. Each instance gets its own
15-
// ephemeral port so multiple canvases (or multiple opens of the same canvas)
16-
// don't collide. Replace this with your real renderer — point a static-file
17-
// server, a Vite/Next dev server, or any framework you like at the same URL.
18-
const servers = new Map();
10+
import {
11+
createFleetService,
12+
resolveRepositoryRoot,
13+
} from "./fleet-service.mjs";
1914

20-
function renderHtml(instanceId) {
21-
return `<!doctype html>
22-
<html>
23-
<head><meta charset="utf-8" /><title>process-workflow-fleet</title></head>
24-
<body style="font-family: system-ui; padding: 1rem;">
25-
<h1>process-workflow-fleet</h1>
26-
<p>Hello from a local canvas server.</p>
27-
<p>Instance: <code>${instanceId}</code></p>
28-
</body>
29-
</html>`;
30-
}
15+
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
16+
const repositoryRoot = resolveRepositoryRoot({
17+
currentWorkingDirectory: process.cwd(),
18+
moduleDirectory,
19+
});
3120

32-
async function startServer(instanceId) {
33-
const server = createServer((req, res) => {
34-
res.setHeader("Content-Type", "text/html; charset=utf-8");
35-
res.end(renderHtml(instanceId));
36-
});
37-
// Port 0 = let the OS pick a free ephemeral port. Bind to loopback only.
38-
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
39-
const address = server.address();
40-
const port = typeof address === "object" && address ? address.port : 0;
41-
return { server, url: `http://127.0.0.1:${port}/` };
42-
}
21+
let session;
22+
const fleet = createFleetService({
23+
getSession: () => session,
24+
repositoryRoot,
25+
});
4326

44-
const session = await joinSession({
45-
canvases: [
46-
createCanvas({
47-
id: "process-workflow-fleet",
48-
displayName: "process-workflow-fleet",
49-
description: "Example canvas - replace with your implementation",
50-
// Optional JSON Schema describing the input passed to open():
51-
// inputSchema: { type: "object", properties: {} },
52-
actions: [
53-
{
54-
name: "example_action",
55-
description: "Example agent-callable action on this canvas",
56-
// Optional JSON Schema for the action input:
57-
// inputSchema: { type: "object", properties: {} },
58-
handler: async (ctx) => {
59-
return { ok: true, instanceId: ctx.instanceId };
27+
const canvas = createCanvas({
28+
id: "process-workflow-fleet",
29+
displayName: "Process workflow fleet",
30+
description:
31+
"Inspect Process-PSModule caller workflows, compare them with the v8 contract, and request repository-scoped migrations.",
32+
inputSchema: {
33+
type: "object",
34+
additionalProperties: false,
35+
properties: {
36+
organization: {
37+
type: "string",
38+
minLength: 1,
39+
maxLength: 100,
40+
pattern: "^[A-Za-z0-9][A-Za-z0-9-]*$",
41+
},
42+
},
43+
},
44+
actions: [
45+
{
46+
name: "refresh_inventory",
47+
description:
48+
"Refresh the authenticated GitHub inventory and replace prior canvas data fail-closed.",
49+
inputSchema: {
50+
type: "object",
51+
additionalProperties: false,
52+
properties: {
53+
organization: {
54+
type: "string",
55+
minLength: 1,
56+
maxLength: 100,
57+
pattern: "^[A-Za-z0-9][A-Za-z0-9-]*$",
58+
},
59+
repositories: {
60+
type: "array",
61+
uniqueItems: true,
62+
maxItems: 500,
63+
items: {
64+
type: "string",
65+
minLength: 1,
66+
maxLength: 200,
67+
pattern:
68+
"^[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?$",
69+
},
70+
},
71+
includeArchived: {
72+
type: "boolean",
6073
},
6174
},
62-
],
63-
// Called when the agent or host opens the canvas. We boot a local
64-
// HTTP server on an ephemeral port and hand its URL back to the
65-
// host so it can render the canvas. Re-opens with the same
66-
// instanceId reuse the existing server.
67-
open: async (ctx) => {
68-
let entry = servers.get(ctx.instanceId);
69-
if (!entry) {
70-
entry = await startServer(ctx.instanceId);
71-
servers.set(ctx.instanceId, entry);
72-
}
73-
return {
74-
title: "process-workflow-fleet",
75-
url: entry.url,
76-
};
7775
},
78-
// Tear the per-instance server down when the canvas is closed so
79-
// ports are not leaked across the lifetime of the extension.
80-
onClose: async (ctx) => {
81-
const entry = servers.get(ctx.instanceId);
82-
if (entry) {
83-
servers.delete(ctx.instanceId);
84-
await new Promise((resolve) => entry.server.close(() => resolve()));
85-
}
76+
handler: async (ctx) => fleet.refreshInventory(ctx.input ?? {}),
77+
},
78+
{
79+
name: "get_summary",
80+
description:
81+
"Return refresh health and v8 compliance counts for the current workspace inventory.",
82+
inputSchema: {
83+
type: "object",
84+
additionalProperties: false,
85+
properties: {},
8686
},
87-
}),
87+
handler: async () => fleet.getSummary(),
88+
},
89+
{
90+
name: "get_repository",
91+
description:
92+
"Return normalized workflow evidence and the migration delta for one repository.",
93+
inputSchema: {
94+
type: "object",
95+
additionalProperties: false,
96+
required: ["repository"],
97+
properties: {
98+
repository: {
99+
type: "string",
100+
minLength: 1,
101+
maxLength: 200,
102+
},
103+
},
104+
},
105+
handler: async (ctx) => fleet.getRepository(ctx.input.repository),
106+
},
107+
{
108+
name: "set_selection",
109+
description:
110+
"Persist the selected repository identities for this session workspace.",
111+
inputSchema: {
112+
type: "object",
113+
additionalProperties: false,
114+
required: ["repositories"],
115+
properties: {
116+
repositories: {
117+
type: "array",
118+
uniqueItems: true,
119+
maxItems: 500,
120+
items: {
121+
type: "string",
122+
minLength: 1,
123+
maxLength: 200,
124+
},
125+
},
126+
},
127+
},
128+
handler: async (ctx) => fleet.setSelection(ctx.input.repositories),
129+
},
130+
{
131+
name: "request_migration",
132+
description:
133+
"Preview or confirm an agent-orchestrated migration request for selected repositories; never mutates repositories directly.",
134+
inputSchema: {
135+
type: "object",
136+
additionalProperties: false,
137+
properties: {
138+
repositories: {
139+
type: "array",
140+
uniqueItems: true,
141+
maxItems: 500,
142+
items: {
143+
type: "string",
144+
minLength: 1,
145+
maxLength: 200,
146+
},
147+
},
148+
dryRun: {
149+
type: "boolean",
150+
},
151+
confirmed: {
152+
type: "boolean",
153+
},
154+
},
155+
},
156+
handler: async (ctx) => fleet.requestMigration(ctx.input ?? {}),
157+
},
88158
],
159+
open: async (ctx) => {
160+
const state = await fleet.ensureState({
161+
organization: ctx.input?.organization,
162+
});
163+
const entry = await fleet.openPanel(ctx.instanceId);
164+
return {
165+
title: "Process workflow fleet",
166+
status:
167+
state.inventoryStatus === "ready"
168+
? `${state.records.length} workflows`
169+
: "Refresh required",
170+
url: entry.url,
171+
};
172+
},
173+
onClose: async (ctx) => {
174+
await fleet.closePanel(ctx.instanceId);
175+
},
176+
});
177+
178+
session = await joinSession({
179+
canvases: [canvas],
89180
});
181+
182+
fleet.setCanvasErrorFactory((code, message) => new CanvasError(code, message));

0 commit comments

Comments
 (0)