@kybernesis/arp-create-adapter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/cli.cjs +135 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +128 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +102 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
- package/templates/python/MIGRATION.md.hbs +34 -0
- package/templates/python/README.md.hbs +37 -0
- package/templates/python/arp_adapter_{{frameworkSnake}}/__init__.py.hbs +77 -0
- package/templates/python/pyproject.toml.hbs +21 -0
- package/templates/python/tests/test_conformance.py.hbs +42 -0
- package/templates/ts/MIGRATION.md.hbs +44 -0
- package/templates/ts/README.md.hbs +35 -0
- package/templates/ts/package.json.hbs +34 -0
- package/templates/ts/src/index.ts.hbs +142 -0
- package/templates/ts/src/types.ts.hbs +46 -0
- package/templates/ts/tests/conformance.test.ts.hbs +32 -0
- package/templates/ts/tsconfig.json +15 -0
- package/templates/ts/tsup.config.ts +13 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { dirname, resolve, join, relative } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import Handlebars from 'handlebars';
|
|
5
|
+
|
|
6
|
+
// src/scaffold.ts
|
|
7
|
+
var __filename$1 = fileURLToPath(import.meta.url);
|
|
8
|
+
var __dirname$1 = dirname(__filename$1);
|
|
9
|
+
var DEFAULT_TEMPLATES_DIR = resolve(__dirname$1, "..", "templates");
|
|
10
|
+
async function scaffoldAdapter(options) {
|
|
11
|
+
const framework = options.framework.trim().toLowerCase();
|
|
12
|
+
if (!/^[a-z][a-z0-9-]*$/.test(framework)) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`framework slug "${options.framework}" must match /^[a-z][a-z0-9-]*$/`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const displayName = options.displayName ?? toDisplayName(framework);
|
|
18
|
+
const arpVersion = options.arpVersion ?? "^0.1.0";
|
|
19
|
+
const templateRoot = options.templatesDir ? resolve(options.templatesDir, options.language) : join(DEFAULT_TEMPLATES_DIR, options.language);
|
|
20
|
+
if (!existsSync(templateRoot)) {
|
|
21
|
+
throw new Error(`template directory missing: ${templateRoot}`);
|
|
22
|
+
}
|
|
23
|
+
const context = {
|
|
24
|
+
framework,
|
|
25
|
+
frameworkPascal: toPascal(framework),
|
|
26
|
+
frameworkCamel: toCamel(framework),
|
|
27
|
+
frameworkSnake: framework.replace(/-/g, "_"),
|
|
28
|
+
frameworkUpper: framework.toUpperCase().replace(/-/g, "_"),
|
|
29
|
+
displayName,
|
|
30
|
+
arpVersion,
|
|
31
|
+
/** Best-effort Python-compatible version range — strip leading `^`. */
|
|
32
|
+
pythonArpVersion: arpVersion.startsWith("^") ? `>=${arpVersion.slice(1)}` : arpVersion,
|
|
33
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34
|
+
};
|
|
35
|
+
const created = [];
|
|
36
|
+
const skipped = [];
|
|
37
|
+
for (const rel of walk(templateRoot)) {
|
|
38
|
+
const src = join(templateRoot, rel);
|
|
39
|
+
const dst = join(options.out, renderFilename(rel, context));
|
|
40
|
+
if (existsSync(dst) && !options.force) {
|
|
41
|
+
skipped.push(dst);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const raw = readFileSync(src, "utf8");
|
|
45
|
+
const rendered = rel.endsWith(".hbs") ? Handlebars.compile(raw, { noEscape: true })(context) : raw;
|
|
46
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
47
|
+
writeFileSync(dst, rendered);
|
|
48
|
+
created.push(dst);
|
|
49
|
+
}
|
|
50
|
+
const summary = [
|
|
51
|
+
`Scaffolded @kybernesis/arp-adapter-${framework} (${options.language}) at ${options.out}.`,
|
|
52
|
+
`Created ${created.length} file(s), skipped ${skipped.length}.`,
|
|
53
|
+
"",
|
|
54
|
+
"Next steps:",
|
|
55
|
+
` 1. cd ${relative(process.cwd(), options.out) || options.out}`,
|
|
56
|
+
options.language === "ts" ? " 2. pnpm install" : " 2. uv sync # or: python -m pip install -e .",
|
|
57
|
+
" 3. Implement src/* to map your framework's public extension points to ArpAgent.",
|
|
58
|
+
" 4. Run the conformance test: pnpm test (or: uv run pytest)",
|
|
59
|
+
" 5. See docs/ARP-adapter-authoring-guide.md for the full contract."
|
|
60
|
+
].join("\n");
|
|
61
|
+
return { createdFiles: created, skippedFiles: skipped, summary };
|
|
62
|
+
}
|
|
63
|
+
function walk(dir) {
|
|
64
|
+
const out = [];
|
|
65
|
+
function recurse(sub) {
|
|
66
|
+
const abs = join(dir, sub);
|
|
67
|
+
for (const entry of readdirSync(abs)) {
|
|
68
|
+
const fullRel = sub ? join(sub, entry) : entry;
|
|
69
|
+
const fullAbs = join(dir, fullRel);
|
|
70
|
+
const st = statSync(fullAbs);
|
|
71
|
+
if (st.isDirectory()) recurse(fullRel);
|
|
72
|
+
else out.push(fullRel);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
recurse("");
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function renderFilename(rel, ctx) {
|
|
79
|
+
const withoutHbs = rel.endsWith(".hbs") ? rel.slice(0, -".hbs".length) : rel;
|
|
80
|
+
return Handlebars.compile(withoutHbs, { noEscape: true })(ctx);
|
|
81
|
+
}
|
|
82
|
+
function toPascal(slug) {
|
|
83
|
+
return slug.split("-").filter(Boolean).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
84
|
+
}
|
|
85
|
+
function toCamel(slug) {
|
|
86
|
+
const pascal = toPascal(slug);
|
|
87
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
88
|
+
}
|
|
89
|
+
function toDisplayName(slug) {
|
|
90
|
+
return slug.split("-").filter(Boolean).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { scaffoldAdapter };
|
|
94
|
+
//# sourceMappingURL=index.js.map
|
|
95
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scaffold.ts"],"names":["__filename","__dirname"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Core scaffolder. Reads a template directory, renders each file through\n * Handlebars, and writes to the target path.\n *\n * Templates live under `packages/create-adapter/templates/<lang>/` and the\n * scaffolder resolves them relative to the compiled `dist/` via\n * `fileURLToPath(import.meta.url)`. Consumers using the programmatic API\n * in a non-standard layout can pass `templatesDir` directly.\n */\n\nimport { readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport Handlebars from 'handlebars';\n\nexport type SupportedLanguage = 'ts' | 'python';\n\nexport interface ScaffoldOptions {\n /** Framework slug (kebab-case). Used in file paths + imports. */\n framework: string;\n /** Human-readable name (e.g. \"KyberBot\"). Used in README, error strings. */\n displayName?: string;\n /** Target language. */\n language: SupportedLanguage;\n /** Destination directory — will be created if missing. */\n out: string;\n /** Override the template root. Default: bundled templates. */\n templatesDir?: string;\n /** ARP spec version pinned in the generated package.json. */\n arpVersion?: string;\n /** When true, overwrite existing files. Default false. */\n force?: boolean;\n}\n\nexport interface ScaffoldResult {\n createdFiles: string[];\n skippedFiles: string[];\n summary: string;\n}\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst DEFAULT_TEMPLATES_DIR = resolve(__dirname, '..', 'templates');\n\nexport async function scaffoldAdapter(\n options: ScaffoldOptions,\n): Promise<ScaffoldResult> {\n const framework = options.framework.trim().toLowerCase();\n if (!/^[a-z][a-z0-9-]*$/.test(framework)) {\n throw new Error(\n `framework slug \"${options.framework}\" must match /^[a-z][a-z0-9-]*$/`,\n );\n }\n const displayName = options.displayName ?? toDisplayName(framework);\n const arpVersion = options.arpVersion ?? '^0.1.0';\n\n const templateRoot = options.templatesDir\n ? resolve(options.templatesDir, options.language)\n : join(DEFAULT_TEMPLATES_DIR, options.language);\n if (!existsSync(templateRoot)) {\n throw new Error(`template directory missing: ${templateRoot}`);\n }\n\n const context = {\n framework,\n frameworkPascal: toPascal(framework),\n frameworkCamel: toCamel(framework),\n frameworkSnake: framework.replace(/-/g, '_'),\n frameworkUpper: framework.toUpperCase().replace(/-/g, '_'),\n displayName,\n arpVersion,\n /** Best-effort Python-compatible version range — strip leading `^`. */\n pythonArpVersion: arpVersion.startsWith('^')\n ? `>=${arpVersion.slice(1)}`\n : arpVersion,\n generatedAt: new Date().toISOString(),\n };\n\n const created: string[] = [];\n const skipped: string[] = [];\n\n for (const rel of walk(templateRoot)) {\n const src = join(templateRoot, rel);\n const dst = join(options.out, renderFilename(rel, context));\n if (existsSync(dst) && !options.force) {\n skipped.push(dst);\n continue;\n }\n const raw = readFileSync(src, 'utf8');\n const rendered = rel.endsWith('.hbs')\n ? Handlebars.compile(raw, { noEscape: true })(context)\n : raw;\n mkdirSync(dirname(dst), { recursive: true });\n writeFileSync(dst, rendered);\n created.push(dst);\n }\n\n const summary = [\n `Scaffolded @kybernesis/arp-adapter-${framework} (${options.language}) at ${options.out}.`,\n `Created ${created.length} file(s), skipped ${skipped.length}.`,\n '',\n 'Next steps:',\n ` 1. cd ${relative(process.cwd(), options.out) || options.out}`,\n options.language === 'ts'\n ? ' 2. pnpm install'\n : ' 2. uv sync # or: python -m pip install -e .',\n ' 3. Implement src/* to map your framework\\'s public extension points to ArpAgent.',\n ' 4. Run the conformance test: pnpm test (or: uv run pytest)',\n ' 5. See docs/ARP-adapter-authoring-guide.md for the full contract.',\n ].join('\\n');\n\n return { createdFiles: created, skippedFiles: skipped, summary };\n}\n\nfunction walk(dir: string): string[] {\n const out: string[] = [];\n function recurse(sub: string) {\n const abs = join(dir, sub);\n for (const entry of readdirSync(abs)) {\n const fullRel = sub ? join(sub, entry) : entry;\n const fullAbs = join(dir, fullRel);\n const st = statSync(fullAbs);\n if (st.isDirectory()) recurse(fullRel);\n else out.push(fullRel);\n }\n }\n recurse('');\n return out;\n}\n\nfunction renderFilename(rel: string, ctx: Record<string, string>): string {\n // Trim trailing `.hbs` and run Handlebars on the path (for filenames\n // like `src/{{framework}}.ts.hbs`).\n const withoutHbs = rel.endsWith('.hbs') ? rel.slice(0, -'.hbs'.length) : rel;\n return Handlebars.compile(withoutHbs, { noEscape: true })(ctx);\n}\n\nfunction toPascal(slug: string): string {\n return slug\n .split('-')\n .filter(Boolean)\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n .join('');\n}\n\nfunction toCamel(slug: string): string {\n const pascal = toPascal(slug);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n\nfunction toDisplayName(slug: string): string {\n return slug\n .split('-')\n .filter(Boolean)\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n .join(' ');\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kybernesis/arp-create-adapter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffolds a conformance-passing ARP framework adapter in TypeScript or Python.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/KybernesisAI/arp.git",
|
|
9
|
+
"directory": "packages/create-adapter"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.cjs",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"create-arp-adapter": "./dist/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"templates",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^12.1.0",
|
|
35
|
+
"handlebars": "^4.7.8"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.9.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"lint": "eslint src tests"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Migrating {{displayName}} (Python) to ARP
|
|
2
|
+
|
|
3
|
+
## Before
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from {{framework}} import {{frameworkPascal}}
|
|
7
|
+
|
|
8
|
+
agent = {{frameworkPascal}}( ... )
|
|
9
|
+
await agent.start()
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## After
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from arp_sdk import ArpAgent
|
|
16
|
+
from {{framework}} import {{frameworkPascal}}
|
|
17
|
+
from arp_adapter_{{frameworkSnake}} import with_arp
|
|
18
|
+
|
|
19
|
+
arp = await ArpAgent.from_handoff("./arp-handoff.json")
|
|
20
|
+
framework = {{frameworkPascal}}( ... )
|
|
21
|
+
await with_arp(framework, agent=arp)
|
|
22
|
+
await framework.start()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## What ARP adds
|
|
26
|
+
|
|
27
|
+
- Cedar PDP check before every tool invocation.
|
|
28
|
+
- Obligation pipeline (redact, rate limit, watermark) applied to outbound replies.
|
|
29
|
+
- Append-only hash-chained audit log per connection.
|
|
30
|
+
|
|
31
|
+
## What doesn't change
|
|
32
|
+
|
|
33
|
+
- No prompt / model changes.
|
|
34
|
+
- No fork of {{displayName}} — the adapter uses its documented public API only.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# arp-adapter-{{framework}} (Python)
|
|
2
|
+
|
|
3
|
+
ARP adapter for **{{displayName}}**.
|
|
4
|
+
|
|
5
|
+
Generated by [`@kybernesis/arp-create-adapter`](https://npmjs.com/package/@kybernesis/arp-create-adapter).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install arp-adapter-{{framework}} arp-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from arp_sdk import ArpAgent
|
|
17
|
+
from arp_adapter_{{frameworkSnake}} import with_arp
|
|
18
|
+
from {{framework}} import {{frameworkPascal}}
|
|
19
|
+
|
|
20
|
+
agent = await ArpAgent.from_handoff("./arp-handoff.json")
|
|
21
|
+
framework = {{frameworkPascal}}( ... )
|
|
22
|
+
await with_arp(framework, agent=agent)
|
|
23
|
+
await framework.start()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Next steps
|
|
27
|
+
|
|
28
|
+
1. Replace `{{frameworkPascal}}Like` in `arp_adapter_{{frameworkSnake}}/__init__.py` with the real extension interface {{displayName}} exposes.
|
|
29
|
+
2. Wire the ARP hooks to the framework's real middleware / event-handler methods.
|
|
30
|
+
3. Run `uv run pytest` (or `pytest`) to exercise the scaffold conformance test.
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
- **Never fork {{displayName}}.** Public API only.
|
|
35
|
+
- **Adapter stays ≤ 1000 lines.**
|
|
36
|
+
- **Pass the ARP testkit audit** when integrated with a real agent.
|
|
37
|
+
- See `docs/ARP-adapter-authoring-guide.md`.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
arp_adapter_{{frameworkSnake}} — ARP adapter for {{displayName}} (Python).
|
|
3
|
+
|
|
4
|
+
Generated on {{generatedAt}} by @kybernesis/arp-create-adapter.
|
|
5
|
+
|
|
6
|
+
TODO (you): replace the placeholder `{{frameworkPascal}}Like` protocol
|
|
7
|
+
below with the real {{displayName}} extension interface (middleware,
|
|
8
|
+
decorator, plugin) and wire `with_arp` into whichever hook names
|
|
9
|
+
{{displayName}} actually exposes.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any, Awaitable, Callable, Protocol
|
|
15
|
+
|
|
16
|
+
from arp_sdk import ArpAgent, guard_action # type: ignore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class {{frameworkPascal}}Like(Protocol):
|
|
20
|
+
id: str
|
|
21
|
+
|
|
22
|
+
def use_tool_middleware(
|
|
23
|
+
self,
|
|
24
|
+
middleware: Callable[[dict[str, Any], Callable[[], Awaitable[Any]]], Awaitable[Any]],
|
|
25
|
+
) -> None: ...
|
|
26
|
+
|
|
27
|
+
def on_peer_message(
|
|
28
|
+
self,
|
|
29
|
+
handler: Callable[[dict[str, Any]], Awaitable[dict[str, Any]]],
|
|
30
|
+
) -> None: ...
|
|
31
|
+
|
|
32
|
+
async def start(self) -> None: ...
|
|
33
|
+
async def stop(self) -> None: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def with_arp(
|
|
37
|
+
framework: {{frameworkPascal}}Like,
|
|
38
|
+
*,
|
|
39
|
+
agent: ArpAgent,
|
|
40
|
+
) -> {{frameworkPascal}}Like:
|
|
41
|
+
"""Register ARP hooks on `framework` using `agent`."""
|
|
42
|
+
|
|
43
|
+
async def tool_mw(ctx: dict[str, Any], nxt: Callable[[], Awaitable[Any]]) -> Any:
|
|
44
|
+
result = await guard_action(
|
|
45
|
+
agent,
|
|
46
|
+
connection_id=ctx["connection_id"],
|
|
47
|
+
action=ctx["tool_name"],
|
|
48
|
+
resource={"type": "Tool", "id": ctx["tool_name"]},
|
|
49
|
+
context=ctx.get("args", {}),
|
|
50
|
+
run=nxt,
|
|
51
|
+
)
|
|
52
|
+
if not result.allow:
|
|
53
|
+
return {"error": "denied_by_arp", "reason": result.reason}
|
|
54
|
+
return result.data
|
|
55
|
+
|
|
56
|
+
async def inbound(msg: dict[str, Any]) -> dict[str, Any]:
|
|
57
|
+
connection_id = msg.get("connection_id") or msg.get("body", {}).get("connection_id")
|
|
58
|
+
if not connection_id:
|
|
59
|
+
return {"body": {"error": "missing_connection_id"}}
|
|
60
|
+
res = await guard_action(
|
|
61
|
+
agent,
|
|
62
|
+
connection_id=connection_id,
|
|
63
|
+
action=msg["action"],
|
|
64
|
+
resource={"type": "Message", "id": msg["id"]},
|
|
65
|
+
context=msg.get("body", {}),
|
|
66
|
+
run=lambda: {"ok": True},
|
|
67
|
+
)
|
|
68
|
+
if not res.allow:
|
|
69
|
+
return {"body": {"error": "denied_by_arp", "reason": res.reason}}
|
|
70
|
+
return {"body": res.data or {}}
|
|
71
|
+
|
|
72
|
+
framework.use_tool_middleware(tool_mw)
|
|
73
|
+
framework.on_peer_message(inbound)
|
|
74
|
+
return framework
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = ["with_arp", "{{frameworkPascal}}Like"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "arp-adapter-{{framework}}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "ARP adapter for {{displayName}} (Python)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "ARP Adapter Author", email = "you@example.com" }]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"arp-sdk{{pythonArpVersion}}",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.23"]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["hatchling"]
|
|
18
|
+
build-backend = "hatchling.build"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["arp_adapter_{{frameworkSnake}}"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Baseline conformance test.
|
|
3
|
+
|
|
4
|
+
The real conformance test — pairing with the ARP reference agents and
|
|
5
|
+
running `arp-testkit audit` — lives in the ARP monorepo's
|
|
6
|
+
`tests/phase-6` suite. Point it at this adapter once wiring is done.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Fake{{frameworkPascal}}:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self.id = "fake-{{framework}}"
|
|
16
|
+
self.tool_hook_registered = False
|
|
17
|
+
self.peer_hook_registered = False
|
|
18
|
+
|
|
19
|
+
def use_tool_middleware(self, _mw) -> None:
|
|
20
|
+
self.tool_hook_registered = True
|
|
21
|
+
|
|
22
|
+
def on_peer_message(self, _handler) -> None:
|
|
23
|
+
self.peer_hook_registered = True
|
|
24
|
+
|
|
25
|
+
async def start(self) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
async def stop(self) -> None:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.asyncio
|
|
33
|
+
async def test_with_arp_registers_hooks():
|
|
34
|
+
from arp_adapter_{{frameworkSnake}} import with_arp
|
|
35
|
+
|
|
36
|
+
class FakeAgent:
|
|
37
|
+
did = "did:web:test.agent"
|
|
38
|
+
|
|
39
|
+
fw = Fake{{frameworkPascal}}()
|
|
40
|
+
await with_arp(fw, agent=FakeAgent()) # type: ignore[arg-type]
|
|
41
|
+
assert fw.tool_hook_registered
|
|
42
|
+
assert fw.peer_hook_registered
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Migrating {{displayName}} to ARP
|
|
2
|
+
|
|
3
|
+
## Before
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { {{displayName}} } from '{{framework}}';
|
|
7
|
+
const agent = new {{displayName}}({ /* ... */ });
|
|
8
|
+
await agent.start();
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## After
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { {{displayName}} } from '{{framework}}';
|
|
15
|
+
import { withArp } from '@kybernesis/arp-adapter-{{framework}}';
|
|
16
|
+
|
|
17
|
+
const guarded = withArp(new {{displayName}}({ /* ... */ }), {
|
|
18
|
+
handoff: './arp-handoff.json',
|
|
19
|
+
});
|
|
20
|
+
await guarded.start();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What ARP adds
|
|
24
|
+
|
|
25
|
+
- Cedar PDP check before every tool call.
|
|
26
|
+
- Obligation pipeline (redact, rate limit, watermark) applied to every outbound reply.
|
|
27
|
+
- Append-only audit log per connection.
|
|
28
|
+
- Lifecycle events (`revocation`, `rotation`, `pairing`) via `guarded.agent.on(...)`.
|
|
29
|
+
|
|
30
|
+
## What doesn't change
|
|
31
|
+
|
|
32
|
+
- No prompt / model changes.
|
|
33
|
+
- No fork of {{displayName}} — the adapter uses its documented public API only.
|
|
34
|
+
|
|
35
|
+
## FAQ
|
|
36
|
+
|
|
37
|
+
**Will my tools still work?**
|
|
38
|
+
Yes. The middleware only wraps the invocation — your tool runs unchanged when the PDP allows.
|
|
39
|
+
|
|
40
|
+
**Latency?**
|
|
41
|
+
The PDP is in-process Cedar-WASM; <5 ms per check on a realistic policy bundle.
|
|
42
|
+
|
|
43
|
+
**Debuggability?**
|
|
44
|
+
Every decision is appended to `<dataDir>/audit/<connection_id>.jsonl` (hash-chained). Use `arp-testkit audit <agent>` to verify.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @kybernesis/arp-adapter-{{framework}}
|
|
2
|
+
|
|
3
|
+
ARP adapter for **{{displayName}}**.
|
|
4
|
+
|
|
5
|
+
Generated by [`@kybernesis/arp-create-adapter`](https://npmjs.com/package/@kybernesis/arp-create-adapter). Next steps:
|
|
6
|
+
|
|
7
|
+
1. Replace the placeholder `{{frameworkPascal}}Like` structural type in `src/types.ts` with the real {{displayName}} extension interface you depend on (middleware, plugin hook, decorator — whatever shape {{displayName}} publishes).
|
|
8
|
+
2. Wire `useToolMiddleware` / `onPeerMessage` in `src/index.ts` to the framework's real hook names.
|
|
9
|
+
3. Run `pnpm test` and then hook this adapter into your own agent.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @kybernesis/arp-adapter-{{framework}} @kybernesis/arp-sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { {{displayName}} } from '{{framework}}';
|
|
21
|
+
import { withArp } from '@kybernesis/arp-adapter-{{framework}}';
|
|
22
|
+
|
|
23
|
+
const framework = new {{displayName}}({ /* normal config */ });
|
|
24
|
+
const guarded = withArp(framework, {
|
|
25
|
+
handoff: './arp-handoff.json',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await guarded.start();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Further reading
|
|
32
|
+
|
|
33
|
+
- [`docs/ARP-adapter-authoring-guide.md`](https://github.com/KybernesisAI/arp/blob/main/docs/ARP-adapter-authoring-guide.md) — contract + anti-patterns.
|
|
34
|
+
- [`docs/ARP-installation-and-hosting.md`](https://github.com/KybernesisAI/arp/blob/main/docs/ARP-installation-and-hosting.md) — the 5 integration points.
|
|
35
|
+
- [`docs/ARP-policy-examples.md`](https://github.com/KybernesisAI/arp/blob/main/docs/ARP-policy-examples.md) — what a PDP decision looks like from the SDK's perspective.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kybernesis/arp-adapter-{{framework}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ARP adapter for {{displayName}} — wraps the framework's public extension API with the five ARP integration points.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist", "README.md", "MIGRATION.md"],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"lint": "eslint src tests"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@kybernesis/arp-sdk": "{{arpVersion}}",
|
|
26
|
+
"@kybernesis/arp-spec": "{{arpVersion}}"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.9.0",
|
|
30
|
+
"tsup": "^8.3.5",
|
|
31
|
+
"typescript": "~5.5.4",
|
|
32
|
+
"vitest": "^2.1.5"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kybernesis/arp-adapter-{{framework}}
|
|
3
|
+
*
|
|
4
|
+
* Generated on {{generatedAt}} by @kybernesis/arp-create-adapter.
|
|
5
|
+
*
|
|
6
|
+
* Implements the five ARP integration points
|
|
7
|
+
* (see ARP-installation-and-hosting.md §8) on top of {{displayName}}'s
|
|
8
|
+
* public extension surface.
|
|
9
|
+
*
|
|
10
|
+
* TODO (you): replace the placeholder `{{frameworkPascal}}Like` surface in
|
|
11
|
+
* src/types.ts with the actual {{displayName}} public interface you
|
|
12
|
+
* depend on, and wire `useToolMiddleware` / `onPeerMessage` to whatever
|
|
13
|
+
* real hook names the framework uses.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ArpAgent,
|
|
18
|
+
guardAction,
|
|
19
|
+
type ArpAgentOptions,
|
|
20
|
+
type Resource,
|
|
21
|
+
} from '@kybernesis/arp-sdk';
|
|
22
|
+
import type { HandoffBundle } from '@kybernesis/arp-spec';
|
|
23
|
+
import type {
|
|
24
|
+
{{frameworkPascal}}Like,
|
|
25
|
+
{{frameworkPascal}}Message,
|
|
26
|
+
{{frameworkPascal}}ToolContext,
|
|
27
|
+
} from './types.js';
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
{{frameworkPascal}}Like,
|
|
31
|
+
{{frameworkPascal}}Message,
|
|
32
|
+
{{frameworkPascal}}MessageHandler,
|
|
33
|
+
{{frameworkPascal}}Reply,
|
|
34
|
+
{{frameworkPascal}}ToolContext,
|
|
35
|
+
{{frameworkPascal}}ToolMiddleware,
|
|
36
|
+
} from './types.js';
|
|
37
|
+
|
|
38
|
+
export interface {{frameworkPascal}}ArpOptions
|
|
39
|
+
extends Omit<ArpAgentOptions, 'onIncoming'> {
|
|
40
|
+
handoff?: HandoffBundle | string | Record<string, unknown>;
|
|
41
|
+
agent?: ArpAgent;
|
|
42
|
+
port?: number;
|
|
43
|
+
toolMapping?: (ctx: {{frameworkPascal}}ToolContext) => {
|
|
44
|
+
action: string;
|
|
45
|
+
resource: Resource;
|
|
46
|
+
context?: Record<string, unknown>;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ArpWrapped{{frameworkPascal}}<F extends {{frameworkPascal}}Like> {
|
|
51
|
+
framework: F;
|
|
52
|
+
agent: ArpAgent;
|
|
53
|
+
start(): Promise<void>;
|
|
54
|
+
stop(graceMs?: number): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function withArp<F extends {{frameworkPascal}}Like>(
|
|
58
|
+
framework: F,
|
|
59
|
+
options: {{frameworkPascal}}ArpOptions,
|
|
60
|
+
): ArpWrapped{{frameworkPascal}}<F> {
|
|
61
|
+
let agent: ArpAgent | null = options.agent ?? null;
|
|
62
|
+
let started = false;
|
|
63
|
+
const port = options.port ?? 4500;
|
|
64
|
+
|
|
65
|
+
const toolMapping =
|
|
66
|
+
options.toolMapping ??
|
|
67
|
+
((ctx: {{frameworkPascal}}ToolContext) => ({
|
|
68
|
+
action: ctx.toolName,
|
|
69
|
+
resource: { type: 'Tool', id: ctx.toolName },
|
|
70
|
+
context: ctx.args,
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
async function ensureAgent(): Promise<ArpAgent> {
|
|
74
|
+
if (agent) return agent;
|
|
75
|
+
if (!options.handoff) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'@kybernesis/arp-adapter-{{framework}}: options.handoff or options.agent required',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const { agent: _a, handoff: _h, port: _p, toolMapping: _tm, ...rest } = options;
|
|
81
|
+
void _a; void _h; void _p; void _tm;
|
|
82
|
+
agent = await ArpAgent.fromHandoff(options.handoff, rest);
|
|
83
|
+
return agent;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
framework.useToolMiddleware(async (ctx, next) => {
|
|
87
|
+
const a = await ensureAgent();
|
|
88
|
+
const mapping = toolMapping(ctx);
|
|
89
|
+
const result = await guardAction(a, {
|
|
90
|
+
connectionId: ctx.connectionId,
|
|
91
|
+
action: mapping.action,
|
|
92
|
+
resource: mapping.resource,
|
|
93
|
+
...(mapping.context !== undefined ? { context: mapping.context } : {}),
|
|
94
|
+
run: () => next(),
|
|
95
|
+
});
|
|
96
|
+
if (!result.allow) {
|
|
97
|
+
return { error: 'denied_by_arp', reason: result.reason };
|
|
98
|
+
}
|
|
99
|
+
return result.data;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
framework.onPeerMessage(async (msg: {{frameworkPascal}}Message) => {
|
|
103
|
+
const a = await ensureAgent();
|
|
104
|
+
const connectionId = msg.connectionId ?? (typeof msg.body['connection_id'] === 'string' ? msg.body['connection_id'] : null);
|
|
105
|
+
if (!connectionId) return { body: { error: 'missing_connection_id' } };
|
|
106
|
+
const res = await guardAction(a, {
|
|
107
|
+
connectionId,
|
|
108
|
+
action: msg.action,
|
|
109
|
+
resource: { type: 'Message', id: msg.id },
|
|
110
|
+
context: msg.body,
|
|
111
|
+
run: async () => ({ ok: true }),
|
|
112
|
+
});
|
|
113
|
+
if (!res.allow) return { body: { error: 'denied_by_arp', reason: res.reason } };
|
|
114
|
+
return { body: (res.data as Record<string, unknown>) ?? {} };
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
framework,
|
|
119
|
+
get agent() {
|
|
120
|
+
if (!agent) throw new Error('agent not started');
|
|
121
|
+
return agent;
|
|
122
|
+
},
|
|
123
|
+
async start() {
|
|
124
|
+
const a = await ensureAgent();
|
|
125
|
+
if (!started) {
|
|
126
|
+
await a.start({ port });
|
|
127
|
+
started = true;
|
|
128
|
+
}
|
|
129
|
+
await framework.start();
|
|
130
|
+
},
|
|
131
|
+
async stop(graceMs = 5000) {
|
|
132
|
+
try {
|
|
133
|
+
await framework.stop();
|
|
134
|
+
} finally {
|
|
135
|
+
if (agent && started) {
|
|
136
|
+
await agent.stop({ graceMs });
|
|
137
|
+
started = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structural interface describing {{displayName}}'s public
|
|
3
|
+
* extension surface that this adapter consumes. Replace the placeholder
|
|
4
|
+
* shape below with whatever {{displayName}} actually exposes (middleware,
|
|
5
|
+
* plugin, hooks, decorators).
|
|
6
|
+
*
|
|
7
|
+
* Rule: the adapter MUST only depend on documented public API. Never fork
|
|
8
|
+
* framework source or reach into internals.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface {{frameworkPascal}}Like {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
/** Register middleware for every outbound tool / action call. */
|
|
14
|
+
useToolMiddleware(mw: {{frameworkPascal}}ToolMiddleware): void;
|
|
15
|
+
/** Register an inbound-message handler. */
|
|
16
|
+
onPeerMessage(handler: {{frameworkPascal}}MessageHandler): void;
|
|
17
|
+
/** Start / stop — framework lifecycle. */
|
|
18
|
+
start(): Promise<void>;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface {{frameworkPascal}}ToolContext {
|
|
23
|
+
connectionId: string;
|
|
24
|
+
toolName: string;
|
|
25
|
+
args: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type {{frameworkPascal}}ToolMiddleware = (
|
|
29
|
+
ctx: {{frameworkPascal}}ToolContext,
|
|
30
|
+
next: () => Promise<unknown>,
|
|
31
|
+
) => Promise<unknown>;
|
|
32
|
+
|
|
33
|
+
export interface {{frameworkPascal}}Message {
|
|
34
|
+
id: string;
|
|
35
|
+
connectionId?: string;
|
|
36
|
+
action: string;
|
|
37
|
+
body: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface {{frameworkPascal}}Reply {
|
|
41
|
+
body: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type {{frameworkPascal}}MessageHandler = (
|
|
45
|
+
msg: {{frameworkPascal}}Message,
|
|
46
|
+
) => Promise<{{frameworkPascal}}Reply | void>;
|