|
| 1 | +# Cartridge Template |
| 2 | + |
| 3 | +This template provides a starting point for creating new cartridges for the BoJ server. |
| 4 | + |
| 5 | +## Structure |
| 6 | + |
| 7 | +``` |
| 8 | +cartridge-template/ |
| 9 | +├── README.md # This file |
| 10 | +├── cartridge.json # Cartridge metadata |
| 11 | +├── abi/ |
| 12 | +│ └── README.adoc # ABI layer documentation |
| 13 | +├── ffi/ |
| 14 | +│ ├── README.adoc # FFI layer documentation |
| 15 | +│ ├── build.zig # Build configuration |
| 16 | +│ └── cartridge_ffi.zig # FFI implementation |
| 17 | +└── adapter/ |
| 18 | + └── README.adoc # Adapter layer documentation |
| 19 | +``` |
| 20 | + |
| 21 | +## Getting Started |
| 22 | + |
| 23 | +1. **Copy the Template**: |
| 24 | + ```bash |
| 25 | + cp -r templates/cartridge-template cartridges/my-new-cartridge |
| 26 | + ``` |
| 27 | + |
| 28 | +2. **Update Metadata**: |
| 29 | + - Edit `cartridge.json` to reflect your cartridge's metadata. |
| 30 | + - Update the `name`, `version`, `description`, and other fields. |
| 31 | + |
| 32 | +3. **Implement the ABI Layer**: |
| 33 | + - Define the abstract interfaces and types in the `abi/` directory. |
| 34 | + - Use Idris2 for type safety and correctness. |
| 35 | + |
| 36 | +4. **Implement the FFI Layer**: |
| 37 | + - Implement the foreign function interface in the `ffi/` directory. |
| 38 | + - Use Zig for high-performance bindings. |
| 39 | + |
| 40 | +5. **Implement the Adapter Layer**: |
| 41 | + - Implement the actual functionality in the `adapter/` directory. |
| 42 | + - Use Zig for the adapter layer. |
| 43 | + |
| 44 | +6. **Add Tests**: |
| 45 | + - Add tests to the `ffi/` directory to ensure your cartridge works as expected. |
| 46 | + - Use Zig's testing framework. |
| 47 | + |
| 48 | +7. **Update Documentation**: |
| 49 | + - Update the `README.adoc` files in each directory to reflect your cartridge's purpose, boundaries, invariants, and execution surfaces. |
| 50 | + |
| 51 | +## Cartridge Metadata |
| 52 | + |
| 53 | +The `cartridge.json` file contains metadata about your cartridge. Here's an example: |
| 54 | + |
| 55 | +```json |
| 56 | +{ |
| 57 | + "name": "my-new-cartridge", |
| 58 | + "version": "0.1.0", |
| 59 | + "description": "A brief description of your cartridge", |
| 60 | + "domain": "Domain", |
| 61 | + "tier": "Ayo", |
| 62 | + "protocols": ["MCP", "REST"], |
| 63 | + "auth": { |
| 64 | + "method": "none" |
| 65 | + }, |
| 66 | + "ports": { |
| 67 | + "allowed": [], |
| 68 | + "denied": [] |
| 69 | + }, |
| 70 | + "tools": [ |
| 71 | + { |
| 72 | + "name": "tool_name", |
| 73 | + "description": "A brief description of the tool", |
| 74 | + "inputSchema": { |
| 75 | + "type": "object", |
| 76 | + "properties": {}, |
| 77 | + "required": [] |
| 78 | + } |
| 79 | + } |
| 80 | + ] |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +## FFI Implementation |
| 85 | + |
| 86 | +The `ffi/cartridge_ffi.zig` file contains the FFI implementation. Here's a basic template: |
| 87 | + |
| 88 | +```zig |
| 89 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 90 | +// Copyright (c) 2026 Your Name <your.email@example.com> |
| 91 | +
|
| 92 | +const std = @import("std"); |
| 93 | +const shim = @import("cartridge_shim.zig"); |
| 94 | +
|
| 95 | +const CARTRIDGE_NAME_PTR: [*:0]const u8 = "my-new-cartridge"; |
| 96 | +const CARTRIDGE_VERSION_PTR: [*:0]const u8 = "0.1.0"; |
| 97 | +
|
| 98 | +export fn boj_cartridge_init() callconv(.c) c_int { |
| 99 | + return 0; |
| 100 | +} |
| 101 | +
|
| 102 | +export fn boj_cartridge_deinit() callconv(.c) void {} |
| 103 | +
|
| 104 | +export fn boj_cartridge_name() callconv(.c) [*:0]const u8 { |
| 105 | + return CARTRIDGE_NAME_PTR; |
| 106 | +} |
| 107 | +
|
| 108 | +export fn boj_cartridge_version() callconv(.c) [*:0]const u8 { |
| 109 | + return CARTRIDGE_VERSION_PTR; |
| 110 | +} |
| 111 | +
|
| 112 | +export fn boj_cartridge_invoke( |
| 113 | + tool_name: [*c]const u8, |
| 114 | + json_args: [*c]const u8, |
| 115 | + out_buf: [*c]u8, |
| 116 | + in_out_len: [*c]usize, |
| 117 | +) callconv(.c) i32 { |
| 118 | + _ = json_args; |
| 119 | +
|
| 120 | + if (shim.invokeArgsNull(tool_name, out_buf, in_out_len)) return shim.RC_BAD_ARGS; |
| 121 | +
|
| 122 | + const body: []const u8 = if (shim.toolIs(tool_name, "tool_name")) |
| 123 | + "{\"result\":{}}" |
| 124 | + else |
| 125 | + return shim.RC_UNKNOWN_TOOL; |
| 126 | +
|
| 127 | + return shim.writeResult(out_buf, in_out_len, body); |
| 128 | +} |
| 129 | +
|
| 130 | +// Tests |
| 131 | +test "boj_cartridge_name returns my-new-cartridge" { |
| 132 | + const n = std.mem.span(boj_cartridge_name()); |
| 133 | + try std.testing.expectEqualStrings("my-new-cartridge", n); |
| 134 | +} |
| 135 | +
|
| 136 | +test "boj_cartridge_version returns semver" { |
| 137 | + const v = std.mem.span(boj_cartridge_version()); |
| 138 | + try std.testing.expectEqualStrings("0.1.0", v); |
| 139 | +} |
| 140 | +
|
| 141 | +test "boj_cartridge_init returns 0" { |
| 142 | + try std.testing.expectEqual(@as(c_int, 0), boj_cartridge_init()); |
| 143 | +} |
| 144 | +
|
| 145 | +test "invoke unknown tool returns -1" { |
| 146 | + var buf: [256]u8 = undefined; |
| 147 | + var len: usize = buf.len; |
| 148 | + const rc = boj_cartridge_invoke("nope", "{}", &buf, &len); |
| 149 | + try std.testing.expectEqual(@as(i32, -1), rc); |
| 150 | +} |
| 151 | +
|
| 152 | +test "invoke known tool writes JSON and returns 0" { |
| 153 | + var buf: [256]u8 = undefined; |
| 154 | + var len: usize = buf.len; |
| 155 | + const rc = boj_cartridge_invoke("tool_name", "{}", &buf, &len); |
| 156 | + try std.testing.expectEqual(@as(i32, 0), rc); |
| 157 | + try std.testing.expect(std.mem.indexOf(u8, buf[0..len], "result") != null); |
| 158 | +} |
| 159 | +
|
| 160 | +test "invoke with too-small buffer returns -3 and sets required length" { |
| 161 | + var buf: [4]u8 = undefined; |
| 162 | + var len: usize = buf.len; |
| 163 | + const rc = boj_cartridge_invoke("tool_name", "{}", &buf, &len); |
| 164 | + try std.testing.expectEqual(@as(i32, -3), rc); |
| 165 | + try std.testing.expect(len > 4); |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +## Build Configuration |
| 170 | + |
| 171 | +The `ffi/build.zig` file contains the build configuration. Here's a basic template: |
| 172 | + |
| 173 | +```zig |
| 174 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 175 | +
|
| 176 | +const std = @import("std"); |
| 177 | +
|
| 178 | +pub fn build(b: *std.Build) void { |
| 179 | + const target = b.standardTargetOptions(.{}); |
| 180 | + const optimize = b.standardOptimizeOption(.{}); |
| 181 | +
|
| 182 | + const ffi_mod = b.addModule("my_new_cartridge", .{ |
| 183 | + .root_source_file = b.path("cartridge_ffi.zig"), |
| 184 | + .target = target, |
| 185 | + .optimize = optimize, |
| 186 | + }); |
| 187 | +
|
| 188 | + const lib = b.addLibrary(.{ |
| 189 | + .name = "my_new_cartridge", |
| 190 | + .root_module = ffi_mod, |
| 191 | + .linkage = .dynamic, |
| 192 | + }); |
| 193 | + lib.linkLibC(); |
| 194 | + b.installArtifact(lib); |
| 195 | +
|
| 196 | + const tests = b.addTest(.{ .root_module = ffi_mod }); |
| 197 | + tests.linkLibC(); |
| 198 | + const run_tests = b.addRunArtifact(tests); |
| 199 | + const test_step = b.step("test", "Run FFI tests"); |
| 200 | + test_step.dependOn(&run_tests.step); |
| 201 | +} |
| 202 | +``` |
| 203 | + |
| 204 | +## Documentation |
| 205 | + |
| 206 | +Update the `README.adoc` files in each directory to reflect your cartridge's purpose, boundaries, invariants, and execution surfaces. Here's an example for the `ffi/` directory: |
| 207 | + |
| 208 | +```adoc |
| 209 | += My New Cartridge FFI Layer |
| 210 | + |
| 211 | +== Purpose |
| 212 | + |
| 213 | +This layer provides the foreign function interface for the My New Cartridge cartridge. It bridges the Idris2 ABI layer with the Zig adapter layer. |
| 214 | + |
| 215 | +== Boundaries |
| 216 | + |
| 217 | +- **Input**: JSON arguments from the MCP bridge. |
| 218 | +- **Output**: JSON responses to the MCP bridge. |
| 219 | +- **Dependencies**: None. |
| 220 | +
|
| 221 | +== Invariants |
| 222 | + |
| 223 | +- The FFI layer must be thread-safe. |
| 224 | +- The FFI layer must handle errors gracefully. |
| 225 | +- The FFI layer must validate input arguments. |
| 226 | + |
| 227 | +== Execution Surfaces |
| 228 | + |
| 229 | +- **Entry Points**: `boj_cartridge_init`, `boj_cartridge_deinit`, `boj_cartridge_name`, `boj_cartridge_version`, `boj_cartridge_invoke`. |
| 230 | +- **Error Handling**: Returns appropriate error codes for invalid inputs or failed operations. |
| 231 | +- **Testing**: Unit tests for each function. |
| 232 | +``` |
| 233 | + |
| 234 | +## Testing |
| 235 | + |
| 236 | +Run the tests using the following command: |
| 237 | + |
| 238 | +```bash |
| 239 | +cd ffi && zig build test |
| 240 | +``` |
| 241 | + |
| 242 | +## Building |
| 243 | + |
| 244 | +Build the shared library using the following command: |
| 245 | + |
| 246 | +```bash |
| 247 | +cd ffi && zig build |
| 248 | +``` |
| 249 | + |
| 250 | +## License |
| 251 | + |
| 252 | +This cartridge is licensed under the PMPL-1.0-or-later license. See the LICENSE file for more information. |
0 commit comments