@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/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":";;;;;;AAwCA,IAAMA,YAAA,GAAa,aAAA,CAAc,MAAA,CAAA,IAAA,CAAY,GAAG,CAAA;AAChD,IAAMC,WAAA,GAAY,QAAQD,YAAU,CAAA;AAEpC,IAAM,qBAAA,GAAwB,OAAA,CAAQC,WAAA,EAAW,IAAA,EAAM,WAAW,CAAA;AAElE,eAAsB,gBACpB,OAAA,EACyB;AACzB,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,SAAA,CAAU,IAAA,GAAO,WAAA,EAAY;AACvD,EAAA,IAAI,CAAC,mBAAA,CAAoB,IAAA,CAAK,SAAS,CAAA,EAAG;AACxC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,gBAAA,EAAmB,QAAQ,SAAS,CAAA,gCAAA;AAAA,KACtC;AAAA,EACF;AACA,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,WAAA,IAAe,aAAA,CAAc,SAAS,CAAA;AAClE,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,QAAA;AAEzC,EAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,YAAA,GACzB,OAAA,CAAQ,OAAA,CAAQ,YAAA,EAAc,OAAA,CAAQ,QAAQ,CAAA,GAC9C,IAAA,CAAK,qBAAA,EAAuB,OAAA,CAAQ,QAAQ,CAAA;AAChD,EAAA,IAAI,CAAC,UAAA,CAAW,YAAY,CAAA,EAAG;AAC7B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,4BAAA,EAA+B,YAAY,CAAA,CAAE,CAAA;AAAA,EAC/D;AAEA,EAAA,MAAM,OAAA,GAAU;AAAA,IACd,SAAA;AAAA,IACA,eAAA,EAAiB,SAAS,SAAS,CAAA;AAAA,IACnC,cAAA,EAAgB,QAAQ,SAAS,CAAA;AAAA,IACjC,cAAA,EAAgB,SAAA,CAAU,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA;AAAA,IAC3C,gBAAgB,SAAA,CAAU,WAAA,EAAY,CAAE,OAAA,CAAQ,MAAM,GAAG,CAAA;AAAA,IACzD,WAAA;AAAA,IACA,UAAA;AAAA;AAAA,IAEA,gBAAA,EAAkB,UAAA,CAAW,UAAA,CAAW,GAAG,CAAA,GACvC,KAAK,UAAA,CAAW,KAAA,CAAM,CAAC,CAAC,CAAA,CAAA,GACxB,UAAA;AAAA,IACJ,WAAA,EAAA,iBAAa,IAAI,IAAA,EAAK,EAAE,WAAA;AAAY,GACtC;AAEA,EAAA,MAAM,UAAoB,EAAC;AAC3B,EAAA,MAAM,UAAoB,EAAC;AAE3B,EAAA,KAAA,MAAW,GAAA,IAAO,IAAA,CAAK,YAAY,CAAA,EAAG;AACpC,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,YAAA,EAAc,GAAG,CAAA;AAClC,IAAA,MAAM,MAAM,IAAA,CAAK,OAAA,CAAQ,KAAK,cAAA,CAAe,GAAA,EAAK,OAAO,CAAC,CAAA;AAC1D,IAAA,IAAI,UAAA,CAAW,GAAG,CAAA,IAAK,CAAC,QAAQ,KAAA,EAAO;AACrC,MAAA,OAAA,CAAQ,KAAK,GAAG,CAAA;AAChB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,GAAA,GAAM,YAAA,CAAa,GAAA,EAAK,MAAM,CAAA;AACpC,IAAA,MAAM,QAAA,GAAW,GAAA,CAAI,QAAA,CAAS,MAAM,IAChC,UAAA,CAAW,OAAA,CAAQ,GAAA,EAAK,EAAE,QAAA,EAAU,IAAA,EAAM,CAAA,CAAE,OAAO,CAAA,GACnD,GAAA;AACJ,IAAA,SAAA,CAAU,QAAQ,GAAG,CAAA,EAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AAC3C,IAAA,aAAA,CAAc,KAAK,QAAQ,CAAA;AAC3B,IAAA,OAAA,CAAQ,KAAK,GAAG,CAAA;AAAA,EAClB;AAEA,EAAA,MAAM,OAAA,GAAU;AAAA,IACd,sCAAsC,SAAS,CAAA,EAAA,EAAK,QAAQ,QAAQ,CAAA,KAAA,EAAQ,QAAQ,GAAG,CAAA,CAAA,CAAA;AAAA,IACvF,CAAA,QAAA,EAAW,OAAA,CAAQ,MAAM,CAAA,kBAAA,EAAqB,QAAQ,MAAM,CAAA,CAAA,CAAA;AAAA,IAC5D,EAAA;AAAA,IACA,aAAA;AAAA,IACA,CAAA,QAAA,EAAW,SAAS,OAAA,CAAQ,GAAA,IAAO,OAAA,CAAQ,GAAG,CAAA,IAAK,OAAA,CAAQ,GAAG,CAAA,CAAA;AAAA,IAC9D,OAAA,CAAQ,QAAA,KAAa,IAAA,GACjB,mBAAA,GACA,gDAAA;AAAA,IACJ,mFAAA;AAAA,IACA,+DAAA;AAAA,IACA;AAAA,GACF,CAAE,KAAK,IAAI,CAAA;AAEX,EAAA,OAAO,EAAE,YAAA,EAAc,OAAA,EAAS,YAAA,EAAc,SAAS,OAAA,EAAQ;AACjE;AAEA,SAAS,KAAK,GAAA,EAAuB;AACnC,EAAA,MAAM,MAAgB,EAAC;AACvB,EAAA,SAAS,QAAQ,GAAA,EAAa;AAC5B,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,EAAK,GAAG,CAAA;AACzB,IAAA,KAAA,MAAW,KAAA,IAAS,WAAA,CAAY,GAAG,CAAA,EAAG;AACpC,MAAA,MAAM,OAAA,GAAU,GAAA,GAAM,IAAA,CAAK,GAAA,EAAK,KAAK,CAAA,GAAI,KAAA;AACzC,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,EAAK,OAAO,CAAA;AACjC,MAAA,MAAM,EAAA,GAAK,SAAS,OAAO,CAAA;AAC3B,MAAA,IAAI,EAAA,CAAG,WAAA,EAAY,EAAG,OAAA,CAAQ,OAAO,CAAA;AAAA,WAChC,GAAA,CAAI,KAAK,OAAO,CAAA;AAAA,IACvB;AAAA,EACF;AACA,EAAA,OAAA,CAAQ,EAAE,CAAA;AACV,EAAA,OAAO,GAAA;AACT;AAEA,SAAS,cAAA,CAAe,KAAa,GAAA,EAAqC;AAGxE,EAAA,MAAM,UAAA,GAAa,GAAA,CAAI,QAAA,CAAS,MAAM,CAAA,GAAI,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,CAAC,MAAA,CAAO,MAAM,CAAA,GAAI,GAAA;AACzE,EAAA,OAAO,UAAA,CAAW,QAAQ,UAAA,EAAY,EAAE,UAAU,IAAA,EAAM,EAAE,GAAG,CAAA;AAC/D;AAEA,SAAS,SAAS,IAAA,EAAsB;AACtC,EAAA,OAAO,IAAA,CACJ,MAAM,GAAG,CAAA,CACT,OAAO,OAAO,CAAA,CACd,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,CAAC,CAAA,CAAE,aAAY,GAAI,CAAA,CAAE,MAAM,CAAC,CAAC,CAAA,CACjD,IAAA,CAAK,EAAE,CAAA;AACZ;AAEA,SAAS,QAAQ,IAAA,EAAsB;AACrC,EAAA,MAAM,MAAA,GAAS,SAAS,IAAI,CAAA;AAC5B,EAAA,OAAO,MAAA,CAAO,OAAO,CAAC,CAAA,CAAE,aAAY,GAAI,MAAA,CAAO,MAAM,CAAC,CAAA;AACxD;AAEA,SAAS,cAAc,IAAA,EAAsB;AAC3C,EAAA,OAAO,IAAA,CACJ,MAAM,GAAG,CAAA,CACT,OAAO,OAAO,CAAA,CACd,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,CAAC,CAAA,CAAE,aAAY,GAAI,CAAA,CAAE,MAAM,CAAC,CAAC,CAAA,CACjD,IAAA,CAAK,GAAG,CAAA;AACb","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>;