@infra-tools/agentic-ui-copilot-skill
v1.2.3
Published
GitHub Copilot Extensions adapter for the @infra-tools/agentic-ui platform. Wraps the Copilot skill webhook protocol: verifies the GitHub signed request, parses incoming messages, exposes a typed handler seam, and streams OpenAI-shaped SSE chunks back. Se
Downloads
194
Readme
@infra-tools/agentic-ui-copilot-skill
GitHub Copilot Extensions adapter for the @infra-tools/agentic-ui
platform. Lets GitHub Copilot Chat (across VS Code, JetBrains,
github.com) invoke any tool registered with our catalog through
a thin server-side webhook.
This is Path 2a in docs/plans/teams-copilot-integration-plan.md and is shipped under the architectural contract of ADR-041.
Why a separate package
GitHub Copilot Chat's wire format is OpenAI-shaped Chat Completion SSE chunks, plus a few Copilot-specific headers and a signed-request verification step. Wrapping every adopter's agent server in that boilerplate is exactly what the adapter pattern (ADR-006) was written for: one new package, the catalog and the chat shell stay untouched.
What's in it
The package is intentionally transport + protocol only. It does
NOT embed a model, does NOT depend on Angular, does NOT depend on
the catalog server. Adopters bring their own LLM behind a
SkillHandler and their own auth layer in front.
| Export | Purpose |
|---|---|
| verifyCopilotRequest(opts) | Validate the X-GitHub-Public-Key-Signature header against GitHub's published keys |
| resolveCopilotKey(keyId) | Fetch + cache a key from api.github.com/meta/public_keys/copilot_api |
| parseCopilotRequest(json) | Type-guarded read of {messages, copilot_thread_id, copilot_skills} |
| readCopilotIdentity(headers) | Pull the GitHub user / enterprise / orgs from request headers |
| encodeAsChunk(event, opts) | Serialise one SkillEvent as an OpenAI Chat Completion chunk |
| streamCopilotResponse(events, write, opts) | Drive a SkillHandler's async-iter into the SSE stream + terminal [DONE] |
| createCopilotSkillMiddleware({ handler, ... }) | Connect-style middleware that ties everything together |
| Types: SkillEvent, SkillHandler, CopilotRequestBody, CopilotIdentity, CopilotMessage | Public types |
Wire it up (Express, ~20 lines)
import express from 'express';
import {
createCopilotSkillMiddleware,
type SkillHandler,
} from '@infra-tools/agentic-ui-copilot-skill';
// Plug your own LLM behind this handler. The example uses a stub
// that just echoes back. In production you'd call Gemini / OpenAI
// / Anthropic with the tool catalog from your catalog server.
const handler: SkillHandler = async function*({ body, identity }) {
yield { type: 'text-delta', delta: `Hi ${identity.githubUserLogin ?? 'there'}.` };
yield { type: 'text-delta', delta: '\n\nReceived ' + body.messages.length + ' message(s).' };
yield { type: 'finish', reason: 'stop' };
};
const app = express();
app.post(
'/copilot/skill',
express.raw({ type: 'application/json', limit: '2mb' }),
createCopilotSkillMiddleware({
handler,
skipSignatureVerification: process.env['NODE_ENV'] !== 'production',
}),
);
app.listen(8080, () => console.log('skill listening on :8080'));The middleware:
- Reads the raw body (raw, not parsed -- needed for signature verification).
- Verifies the GitHub signature unless
skipSignatureVerificationis set (typically only in local dev). - Parses the JSON body and rejects 400 on malformed payloads.
- Streams chunks via
streamCopilotResponse.
Wire to a real agent
Replace the stub handler with your existing agent loop. If
you're using @infra-tools/agentic-ui-server's AG-UI Agent
interface, the shape maps cleanly:
const handler: SkillHandler = async function*({ body, identity, signal }) {
// 1. Map GitHub identity -> catalog principal.
const principal = await mapGitHubIdentityToCatalog(identity);
// 2. Run the agent against your registered tools.
const stream = yourAgent.run({
messages: body.messages,
principal,
signal,
});
// 3. Bridge AG-UI events -> SkillEvent.
for await (const ev of stream) {
if (ev.type === 'TEXT_MESSAGE_CONTENT') {
yield { type: 'text-delta', delta: ev.delta };
} else if (ev.type === 'TOOL_CALL_START') {
// accumulate args until TOOL_CALL_END, then yield
} else if (ev.type === 'TOOL_CALL_RESULT') {
yield { type: 'tool-call', toolCallId: ev.id, name: ev.name, args: ev.args };
yield { type: 'tool-result', toolCallId: ev.id, result: ev.result };
} else if (ev.type === 'RUN_FINISHED') {
yield { type: 'finish', reason: 'stop' };
}
}
};GitHub App + Marketplace listing
Production deployment needs:
- GitHub App registration — one per environment. The app's
webhook URL points at your
POST /copilot/skillendpoint. The app declares theread:userOAuth scope so identity headers land on the webhook. - Copilot Extension manifest at the repo root, declaring the skill name + webhook URL.
- Marketplace listing if you want public availability. Private org installs work without listing.
See docs/cookbook/github-copilot-extension.md for the end-to-end walkthrough.
Spec coverage
17 tests cover the protocol layer:
parseCopilotRequest: valid body, dropped-field handling, null on missing fields, optionalcopilot_skills.readCopilotIdentity: header presence + multi-value parsing.encodeAsChunk: text deltas (role on first chunk only), tool calls, finish reason mapping.streamCopilotResponse: chunk ordering, terminal[DONE], synthetic finish when handler forgets.verifyCopilotRequest: missing headers, unknown key, tampered signature, valid signature (real ECDSA P-256 round-trip).
Run npm test from the package directory.
What this does NOT do
- No LLM. Adopters bring their own.
- No tool registry sync. The catalog server is the source of truth; this skill consumes whatever tools the handler advertises to its LLM.
- No audit fan-in. ADR-041 D3 adds an
originfield to audit events; adopters wiring this skill should passorigin: 'github-copilot'when their handler calls the catalog audit endpoint. - No long-running operations. Copilot Chat is request/response;
long F5 tools should kick off the operation, return an
opId, and let users check status with a follow-up turn.
Status
v0.1.0 — protocol layer + middleware shipped, two-week vertical slice from the integration plan. Not yet wired against a live LLM in this repo; first adopter integration is the next milestone.
