test+docs: add capability flag tests and documentation
Tests: - adapter-registry: verify capability flags are exposed, undefined when unset, and correctly declared on built-in claude_local adapter - adapter-routes: verify GET /api/adapters includes capabilities object, correct flags for codex_local/process/cursor, and derived supportsSkills Documentation: - docs/adapters/creating-an-adapter.md: new "Capability Flags" section documenting all flags, their defaults, and usage example for external adapter plugins Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -203,6 +203,43 @@ export const sessionCodec: AdapterSessionCodec = {
|
||||
};
|
||||
```
|
||||
|
||||
## Capability Flags
|
||||
|
||||
Adapters can declare what "local" capabilities they support by setting optional fields on the `ServerAdapterModule`. The server and UI use these flags to decide which features to enable for agents using the adapter (instructions bundle editor, skills sync, JWT auth, etc.).
|
||||
|
||||
| Flag | Type | Default | What it controls |
|
||||
|------|------|---------|------------------|
|
||||
| `supportsLocalAgentJwt` | `boolean` | `false` | Whether heartbeat generates a local JWT for the agent |
|
||||
| `supportsInstructionsBundle` | `boolean` | `false` | Managed instructions bundle (AGENTS.md) — server-side resolution + UI editor |
|
||||
| `instructionsPathKey` | `string` | `"instructionsFilePath"` | The `adapterConfig` key that holds the instructions file path |
|
||||
| `requiresMaterializedRuntimeSkills` | `boolean` | `false` | Whether runtime skill entries must be written to disk before execution |
|
||||
|
||||
These flags are exposed via `GET /api/adapters` in a `capabilities` object, along with a derived `supportsSkills` flag (true when `listSkills` or `syncSkills` is defined).
|
||||
|
||||
### Example
|
||||
|
||||
```ts
|
||||
export function createServerAdapter(): ServerAdapterModule {
|
||||
return {
|
||||
type: "my_k8s_adapter",
|
||||
execute: myExecute,
|
||||
testEnvironment: myTestEnvironment,
|
||||
listSkills: myListSkills,
|
||||
syncSkills: mySyncSkills,
|
||||
|
||||
// Capability flags
|
||||
supportsLocalAgentJwt: true,
|
||||
supportsInstructionsBundle: true,
|
||||
instructionsPathKey: "instructionsFilePath",
|
||||
requiresMaterializedRuntimeSkills: true,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
With these flags set, the Paperclip UI will automatically show the instructions bundle editor, skills management tab, and working directory field for agents using this adapter — no Paperclip source changes required.
|
||||
|
||||
If capability flags are not set, the server falls back to legacy hardcoded lists for built-in adapter types. External adapters that omit the flags will default to `false` for all capabilities.
|
||||
|
||||
## Skills Injection
|
||||
|
||||
Make Paperclip skills discoverable to your agent runtime without writing to the agent's working directory:
|
||||
|
||||
@@ -95,6 +95,51 @@ describe("server adapter registry", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("exposes capability flags from registered adapters", () => {
|
||||
const adapterWithCaps: ServerAdapterModule = {
|
||||
type: "external_test",
|
||||
execute: async () => ({ exitCode: 0, signal: null, timedOut: false }),
|
||||
testEnvironment: async () => ({
|
||||
adapterType: "external_test",
|
||||
status: "pass" as const,
|
||||
checks: [],
|
||||
testedAt: new Date(0).toISOString(),
|
||||
}),
|
||||
supportsLocalAgentJwt: true,
|
||||
supportsInstructionsBundle: true,
|
||||
instructionsPathKey: "customPathKey",
|
||||
requiresMaterializedRuntimeSkills: true,
|
||||
};
|
||||
|
||||
registerServerAdapter(adapterWithCaps);
|
||||
|
||||
const resolved = findActiveServerAdapter("external_test");
|
||||
expect(resolved).not.toBeNull();
|
||||
expect(resolved!.supportsInstructionsBundle).toBe(true);
|
||||
expect(resolved!.instructionsPathKey).toBe("customPathKey");
|
||||
expect(resolved!.requiresMaterializedRuntimeSkills).toBe(true);
|
||||
expect(resolved!.supportsLocalAgentJwt).toBe(true);
|
||||
});
|
||||
|
||||
it("returns undefined for capability flags on adapters that do not set them", () => {
|
||||
registerServerAdapter(externalAdapter);
|
||||
|
||||
const resolved = findActiveServerAdapter("external_test");
|
||||
expect(resolved).not.toBeNull();
|
||||
expect(resolved!.supportsInstructionsBundle).toBeUndefined();
|
||||
expect(resolved!.instructionsPathKey).toBeUndefined();
|
||||
expect(resolved!.requiresMaterializedRuntimeSkills).toBeUndefined();
|
||||
});
|
||||
|
||||
it("built-in claude_local adapter declares capability flags", () => {
|
||||
const adapter = findActiveServerAdapter("claude_local");
|
||||
expect(adapter).not.toBeNull();
|
||||
expect(adapter!.supportsInstructionsBundle).toBe(true);
|
||||
expect(adapter!.instructionsPathKey).toBe("instructionsFilePath");
|
||||
expect(adapter!.requiresMaterializedRuntimeSkills).toBe(false);
|
||||
expect(adapter!.supportsLocalAgentJwt).toBe(true);
|
||||
});
|
||||
|
||||
it("switches active adapter behavior back to the builtin when an override is paused", async () => {
|
||||
const builtIn = findServerAdapter("claude_local");
|
||||
expect(builtIn).not.toBeNull();
|
||||
|
||||
@@ -57,6 +57,75 @@ describe("adapter routes", () => {
|
||||
unregisterServerAdapter("claude_local");
|
||||
});
|
||||
|
||||
it("GET /api/adapters includes capabilities object for each adapter", async () => {
|
||||
const app = createApp();
|
||||
|
||||
const res = await request(app).get("/api/adapters");
|
||||
expect(res.status).toBe(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
expect(res.body.length).toBeGreaterThan(0);
|
||||
|
||||
// Every adapter should have a capabilities object
|
||||
for (const adapter of res.body) {
|
||||
expect(adapter.capabilities).toBeDefined();
|
||||
expect(typeof adapter.capabilities.supportsInstructionsBundle).toBe("boolean");
|
||||
expect(typeof adapter.capabilities.supportsSkills).toBe("boolean");
|
||||
expect(typeof adapter.capabilities.supportsLocalAgentJwt).toBe("boolean");
|
||||
expect(typeof adapter.capabilities.requiresMaterializedRuntimeSkills).toBe("boolean");
|
||||
}
|
||||
});
|
||||
|
||||
it("GET /api/adapters returns correct capabilities for built-in adapters", async () => {
|
||||
const app = createApp();
|
||||
|
||||
const res = await request(app).get("/api/adapters");
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// codex_local has instructions bundle + skills + jwt, no materialized skills
|
||||
// (claude_local is overridden by beforeEach, so check codex_local instead)
|
||||
const codexLocal = res.body.find((a: any) => a.type === "codex_local");
|
||||
expect(codexLocal).toBeDefined();
|
||||
expect(codexLocal.capabilities).toMatchObject({
|
||||
supportsInstructionsBundle: true,
|
||||
supportsSkills: true,
|
||||
supportsLocalAgentJwt: true,
|
||||
requiresMaterializedRuntimeSkills: false,
|
||||
});
|
||||
|
||||
// process adapter should have no local capabilities
|
||||
const processAdapter = res.body.find((a: any) => a.type === "process");
|
||||
expect(processAdapter).toBeDefined();
|
||||
expect(processAdapter.capabilities).toMatchObject({
|
||||
supportsInstructionsBundle: false,
|
||||
supportsSkills: false,
|
||||
supportsLocalAgentJwt: false,
|
||||
requiresMaterializedRuntimeSkills: false,
|
||||
});
|
||||
|
||||
// cursor adapter should require materialized runtime skills
|
||||
const cursorAdapter = res.body.find((a: any) => a.type === "cursor");
|
||||
expect(cursorAdapter).toBeDefined();
|
||||
expect(cursorAdapter.capabilities.requiresMaterializedRuntimeSkills).toBe(true);
|
||||
expect(cursorAdapter.capabilities.supportsInstructionsBundle).toBe(true);
|
||||
});
|
||||
|
||||
it("GET /api/adapters derives supportsSkills from listSkills/syncSkills presence", async () => {
|
||||
const app = createApp();
|
||||
|
||||
const res = await request(app).get("/api/adapters");
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// http adapter has no listSkills/syncSkills
|
||||
const httpAdapter = res.body.find((a: any) => a.type === "http");
|
||||
expect(httpAdapter).toBeDefined();
|
||||
expect(httpAdapter.capabilities.supportsSkills).toBe(false);
|
||||
|
||||
// codex_local has listSkills/syncSkills
|
||||
const codexLocal = res.body.find((a: any) => a.type === "codex_local");
|
||||
expect(codexLocal).toBeDefined();
|
||||
expect(codexLocal.capabilities.supportsSkills).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the active adapter when resolving config schema for a paused builtin override", async () => {
|
||||
const app = createApp();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user