@curatelabs/graphforge-agent-skills 0.5.1

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.
@@ -0,0 +1,422 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open, realpath } from "node:fs/promises";
3
+ import { isAbsolute, parse, relative, resolve, sep } from "node:path";
4
+
5
+ export const ADAPTER_CONTRACT_VERSION = 1;
6
+ export const PROJECT_FORMAT = "graphforge-project/v1\n";
7
+ export const VALUE_BUDGETS = Object.freeze({
8
+ maxDepth: 16,
9
+ maxEntries: 4096,
10
+ maxStringLength: 4096,
11
+ });
12
+
13
+ const SAFE_NATIVE_CODE = /^GF_[A-Z0-9_]{1,72}$/;
14
+ const SENSITIVE_CODE = /(?:SECRET|TOKEN|PASSWORD|CREDENTIAL|API_KEY|PRIVATE_KEY)/;
15
+ const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
16
+
17
+ export class AgentAdapterError extends Error {
18
+ constructor(code, message, details = {}) {
19
+ super(message);
20
+ this.name = "AgentAdapterError";
21
+ this.code = code;
22
+ this.contractVersion = ADAPTER_CONTRACT_VERSION;
23
+ try {
24
+ this.details = canonicalize(details);
25
+ } catch {
26
+ this.details = { details_omitted: true };
27
+ }
28
+ }
29
+
30
+ toJSON() {
31
+ return {
32
+ code: this.code,
33
+ contract_version: this.contractVersion,
34
+ details: this.details,
35
+ message: this.message,
36
+ };
37
+ }
38
+ }
39
+
40
+ export function normalizeGraphForgeError(error) {
41
+ if (error instanceof AgentAdapterError) return error;
42
+ const nativeCode =
43
+ typeof error?.code === "string" &&
44
+ SAFE_NATIVE_CODE.test(error.code) &&
45
+ !SENSITIVE_CODE.test(error.code)
46
+ ? error.code
47
+ : "GF_AGENT_GRAPHFORGE_ERROR";
48
+ return new AgentAdapterError(nativeCode, "GraphForge operation failed");
49
+ }
50
+
51
+ export function requestSubprocess() {
52
+ throw new AgentAdapterError(
53
+ "GF_AGENT_SUBPROCESS_UNSUPPORTED",
54
+ "subprocess execution is not supported by the shared adapter",
55
+ );
56
+ }
57
+
58
+ export async function validateProjectPath({ path, cwd = process.cwd() }) {
59
+ if (
60
+ typeof path !== "string" ||
61
+ path.length === 0 ||
62
+ path.length > 4096 ||
63
+ CONTROL_CHARACTER.test(path) ||
64
+ path.split(/[\\/]+/).includes("..")
65
+ ) {
66
+ throw new AgentAdapterError(
67
+ "GF_AGENT_INVALID_PROJECT_PATH",
68
+ "project path is not a safe bounded path",
69
+ );
70
+ }
71
+ const candidate = resolve(cwd, path);
72
+ if (!isAbsolute(path)) {
73
+ const fromRoot = relative(resolve(cwd), candidate);
74
+ if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
75
+ throw new AgentAdapterError(
76
+ "GF_AGENT_INVALID_PROJECT_PATH",
77
+ "project path must remain within the discovery root",
78
+ );
79
+ }
80
+ }
81
+ if (await containsSymlink(candidate)) {
82
+ throw new AgentAdapterError(
83
+ "GF_AGENT_INVALID_PROJECT_PATH",
84
+ "project path must not contain symlinks",
85
+ );
86
+ }
87
+ return candidate;
88
+ }
89
+
90
+ export async function discoverProject({ candidates, cwd = process.cwd() } = {}) {
91
+ const inputs = candidates ?? [cwd];
92
+ if (!Array.isArray(inputs) || inputs.length === 0) {
93
+ throw new AgentAdapterError(
94
+ "GF_AGENT_PROJECT_NOT_FOUND",
95
+ "no GraphForge project candidates were provided",
96
+ );
97
+ }
98
+
99
+ const matches = [];
100
+ let unsupportedCount = 0;
101
+ for (const input of inputs) {
102
+ if (
103
+ typeof input !== "string" ||
104
+ input.length === 0 ||
105
+ input.length > 4096 ||
106
+ CONTROL_CHARACTER.test(input)
107
+ ) {
108
+ throw new AgentAdapterError(
109
+ "GF_AGENT_INVALID_PROJECT_PATH",
110
+ "project candidates must be bounded paths without control characters",
111
+ );
112
+ }
113
+ const segments = input.split(/[\\/]+/);
114
+ if (segments.includes("..")) {
115
+ throw new AgentAdapterError(
116
+ "GF_AGENT_INVALID_PROJECT_PATH",
117
+ "project candidates must not contain parent traversal",
118
+ );
119
+ }
120
+ const candidate = resolve(cwd, input);
121
+ if (!isAbsolute(input)) {
122
+ const fromRoot = relative(resolve(cwd), candidate);
123
+ if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
124
+ throw new AgentAdapterError(
125
+ "GF_AGENT_INVALID_PROJECT_PATH",
126
+ "project candidates must remain within the discovery root",
127
+ );
128
+ }
129
+ }
130
+ if (await containsSymlink(candidate)) continue;
131
+ const stat = await lstat(candidate).catch(() => null);
132
+ if (!stat?.isDirectory() || stat.isSymbolicLink()) continue;
133
+ const canonical = await realpath(candidate);
134
+ const markerPath = resolve(canonical, "FORMAT");
135
+ const handle = await open(markerPath, constants.O_RDONLY | constants.O_NOFOLLOW).catch(
136
+ () => null,
137
+ );
138
+ if (!handle) continue;
139
+ let marker = null;
140
+ try {
141
+ const markerStat = await handle.stat();
142
+ if (!markerStat.isFile()) continue;
143
+ if (markerStat.size > 64) {
144
+ unsupportedCount += 1;
145
+ continue;
146
+ }
147
+ marker = await handle.readFile("utf8");
148
+ } catch {
149
+ continue;
150
+ } finally {
151
+ await handle.close();
152
+ }
153
+ if (marker === PROJECT_FORMAT) matches.push(canonical);
154
+ else if (marker !== null) unsupportedCount += 1;
155
+ }
156
+
157
+ const unique = [...new Set(matches)].sort();
158
+ if (unique.length === 0) {
159
+ if (unsupportedCount > 0) {
160
+ throw new AgentAdapterError(
161
+ "GF_AGENT_PROJECT_UNSUPPORTED",
162
+ "only unsupported GraphForge project formats were discovered",
163
+ { candidate_count: unsupportedCount },
164
+ );
165
+ }
166
+ throw new AgentAdapterError(
167
+ "GF_AGENT_PROJECT_NOT_FOUND",
168
+ "no supported GraphForge project was discovered",
169
+ );
170
+ }
171
+ if (unique.length !== 1) {
172
+ throw new AgentAdapterError(
173
+ "GF_AGENT_PROJECT_AMBIGUOUS",
174
+ "multiple supported GraphForge projects were discovered",
175
+ { candidate_count: unique.length },
176
+ );
177
+ }
178
+ return unique[0];
179
+ }
180
+
181
+ async function containsSymlink(candidate) {
182
+ const { root } = parse(candidate);
183
+ let current = root;
184
+ for (const segment of candidate.slice(root.length).split(sep).filter(Boolean)) {
185
+ current = resolve(current, segment);
186
+ const stat = await lstat(current).catch(() => null);
187
+ if (!stat) return false;
188
+ if (stat.isSymbolicLink()) return true;
189
+ }
190
+ return false;
191
+ }
192
+
193
+ const WRITE_MODES = new Set(["single_writer", "queued_writer", "optimistic_multi_writer"]);
194
+
195
+ export function normalizeWriteOptions(options = {}) {
196
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
197
+ throw new AgentAdapterError(
198
+ "GF_AGENT_ADAPTER_CONFIGURATION",
199
+ "write options must be an object",
200
+ );
201
+ }
202
+ const { writeMode = "single_writer", writeQueueCapacity = 64, maxRebaseAttempts = 3 } = options;
203
+ if (!WRITE_MODES.has(writeMode)) {
204
+ throw new AgentAdapterError(
205
+ "GF_AGENT_ADAPTER_CONFIGURATION",
206
+ "write mode must be single_writer, queued_writer, or optimistic_multi_writer",
207
+ );
208
+ }
209
+ if (
210
+ !Number.isInteger(writeQueueCapacity) ||
211
+ writeQueueCapacity < 1 ||
212
+ writeQueueCapacity > 65_536
213
+ ) {
214
+ throw new AgentAdapterError(
215
+ "GF_AGENT_ADAPTER_CONFIGURATION",
216
+ "write queue capacity must be an integer between 1 and 65536",
217
+ );
218
+ }
219
+ if (!Number.isInteger(maxRebaseAttempts) || maxRebaseAttempts < 0 || maxRebaseAttempts > 32) {
220
+ throw new AgentAdapterError(
221
+ "GF_AGENT_ADAPTER_CONFIGURATION",
222
+ "max rebase attempts must be an integer between 0 and 32",
223
+ );
224
+ }
225
+ return { maxRebaseAttempts, writeMode, writeQueueCapacity };
226
+ }
227
+
228
+ export async function openProject({
229
+ path,
230
+ GraphForge,
231
+ tableFromIPC,
232
+ requiredCapabilities = {},
233
+ writeOptions,
234
+ }) {
235
+ if (typeof GraphForge !== "function" || typeof tableFromIPC !== "function") {
236
+ throw new AgentAdapterError(
237
+ "GF_AGENT_ADAPTER_CONFIGURATION",
238
+ "GraphForge and tableFromIPC shipped surfaces are required",
239
+ );
240
+ }
241
+ const projectPath = await discoverProject({ candidates: [path] });
242
+ const normalizedWriteOptions = normalizeWriteOptions(writeOptions);
243
+ let graph;
244
+ try {
245
+ await validateProjectPath({ path: projectPath });
246
+ graph = new GraphForge(projectPath, normalizedWriteOptions);
247
+ const capabilities = capabilitiesFromTable(tableFromIPC(await graph.projectCapabilities()));
248
+ requireCapabilities(capabilities, requiredCapabilities);
249
+ return { capabilities, graph, path: projectPath };
250
+ } catch (error) {
251
+ try {
252
+ graph?.close?.();
253
+ } catch {
254
+ // Cleanup must not replace the structured open/capability failure.
255
+ }
256
+ throw normalizeGraphForgeError(error);
257
+ }
258
+ }
259
+
260
+ export function capabilitiesFromTable(table) {
261
+ const ids = table?.getChild?.("capability_id");
262
+ const versions = table?.getChild?.("capability_version");
263
+ const statuses = table?.getChild?.("status");
264
+ if (!Number.isInteger(table?.numRows) || !ids || !versions) {
265
+ throw new AgentAdapterError(
266
+ "GF_AGENT_INVALID_CAPABILITY_TABLE",
267
+ "GraphForge returned an invalid capability table",
268
+ );
269
+ }
270
+ const result = {};
271
+ for (let row = 0; row < table.numRows; row += 1) {
272
+ const id = ids.get(row);
273
+ const version = Number(versions.get(row));
274
+ const status = statuses?.get(row) ?? "supported";
275
+ if (typeof id !== "string" || !Number.isSafeInteger(version)) {
276
+ throw new AgentAdapterError(
277
+ "GF_AGENT_INVALID_CAPABILITY_TABLE",
278
+ "GraphForge returned an invalid capability row",
279
+ );
280
+ }
281
+ if (Object.hasOwn(result, id)) {
282
+ throw new AgentAdapterError(
283
+ "GF_AGENT_INVALID_CAPABILITY_TABLE",
284
+ "GraphForge returned duplicate capability rows",
285
+ );
286
+ }
287
+ result[id] = { status, version };
288
+ }
289
+ return canonicalize(result);
290
+ }
291
+
292
+ export function requireCapabilities(actual, required) {
293
+ for (const [id, version] of Object.entries(required).sort()) {
294
+ if (
295
+ typeof id !== "string" ||
296
+ id.length === 0 ||
297
+ !Number.isSafeInteger(version) ||
298
+ version < 1
299
+ ) {
300
+ throw new AgentAdapterError(
301
+ "GF_AGENT_ADAPTER_CONFIGURATION",
302
+ "required capabilities must use non-empty IDs and positive integer versions",
303
+ );
304
+ }
305
+ const capability = actual[id];
306
+ if (!capability) {
307
+ throw new AgentAdapterError(
308
+ "GF_AGENT_CAPABILITY_MISSING",
309
+ `required GraphForge capability is missing: ${id}`,
310
+ { capability_id: id, required_version: version },
311
+ );
312
+ }
313
+ if (capability.status !== "supported" || capability.version !== version) {
314
+ throw new AgentAdapterError(
315
+ "GF_AGENT_CAPABILITY_UNSUPPORTED",
316
+ `unsupported GraphForge capability version: ${id}@${capability.version}`,
317
+ {
318
+ actual_status: capability.status,
319
+ actual_version: capability.version,
320
+ capability_id: id,
321
+ required_version: version,
322
+ },
323
+ );
324
+ }
325
+ }
326
+ }
327
+
328
+ export function uuidToString(value) {
329
+ if (typeof value === "string") {
330
+ const normalized = value.toLowerCase();
331
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(normalized)) {
332
+ return normalized;
333
+ }
334
+ }
335
+ if (ArrayBuffer.isView(value) && value.byteLength === 16) {
336
+ const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
337
+ const hex = bytes.toString("hex");
338
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
339
+ }
340
+ throw new AgentAdapterError("GF_AGENT_INVALID_UUID", "expected a canonical UUID");
341
+ }
342
+
343
+ export function tableToJson(table) {
344
+ if (!Number.isInteger(table?.numRows) || !Array.isArray(table?.schema?.fields)) {
345
+ throw new AgentAdapterError("GF_AGENT_INVALID_ARROW_TABLE", "expected an Apache Arrow table");
346
+ }
347
+ const fields = table.schema.fields.map(({ name }) => name);
348
+ return Array.from({ length: table.numRows }, (_, row) =>
349
+ Object.fromEntries(fields.map((name) => [name, jsonValue(table.getChild(name).get(row))])),
350
+ );
351
+ }
352
+
353
+ export function stableJson(value) {
354
+ return `${JSON.stringify(canonicalize(value))}\n`;
355
+ }
356
+
357
+ function jsonValue(value) {
358
+ if (typeof value === "bigint") return value.toString();
359
+ if (ArrayBuffer.isView(value) && value.byteLength === 16) return uuidToString(value);
360
+ if (Array.isArray(value)) return canonicalize(value);
361
+ if (value && typeof value === "object") return canonicalize(value);
362
+ return value;
363
+ }
364
+
365
+ function canonicalize(value) {
366
+ const seen = new WeakSet();
367
+ let entries = 0;
368
+
369
+ function visit(item, depth) {
370
+ entries += 1;
371
+ if (entries > VALUE_BUDGETS.maxEntries || depth > VALUE_BUDGETS.maxDepth) {
372
+ throw new AgentAdapterError(
373
+ "GF_AGENT_VALUE_BUDGET_EXCEEDED",
374
+ "value exceeds the shared adapter budget",
375
+ );
376
+ }
377
+ if (typeof item === "string" && item.length > VALUE_BUDGETS.maxStringLength) {
378
+ throw new AgentAdapterError(
379
+ "GF_AGENT_VALUE_BUDGET_EXCEEDED",
380
+ "value exceeds the shared adapter budget",
381
+ );
382
+ }
383
+ if (typeof item === "bigint") return item.toString();
384
+ if (ArrayBuffer.isView(item)) {
385
+ if (item.byteLength > VALUE_BUDGETS.maxEntries) {
386
+ throw new AgentAdapterError(
387
+ "GF_AGENT_VALUE_BUDGET_EXCEEDED",
388
+ "value exceeds the shared adapter budget",
389
+ );
390
+ }
391
+ return item.byteLength === 16 ? uuidToString(item) : Array.from(item);
392
+ }
393
+ if (!item || typeof item !== "object") return item;
394
+ if (seen.has(item)) {
395
+ throw new AgentAdapterError("GF_AGENT_CYCLIC_VALUE", "cyclic values are not supported");
396
+ }
397
+ seen.add(item);
398
+ try {
399
+ if (Array.isArray(item)) return item.map((child) => visit(child, depth + 1));
400
+ if (typeof item[Symbol.iterator] === "function") {
401
+ return Array.from(item, (child) => visit(child, depth + 1)).sort(compareCanonical);
402
+ }
403
+ return Object.fromEntries(
404
+ Object.entries(item)
405
+ .sort(([left], [right]) => compareCodeUnits(left, right))
406
+ .map(([key, child]) => [visit(key, depth + 1), visit(child, depth + 1)]),
407
+ );
408
+ } finally {
409
+ seen.delete(item);
410
+ }
411
+ }
412
+
413
+ return visit(value, 0);
414
+ }
415
+
416
+ function compareCanonical(left, right) {
417
+ return compareCodeUnits(JSON.stringify(left), JSON.stringify(right));
418
+ }
419
+
420
+ function compareCodeUnits(left, right) {
421
+ return left < right ? -1 : left > right ? 1 : 0;
422
+ }
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+
5
+ const packageRoot = new URL("../", import.meta.url);
6
+ const packageMetadata = JSON.parse(readFileSync(new URL("package.json", packageRoot), "utf8"));
7
+ const compatibility = JSON.parse(readFileSync(new URL("compatibility.json", packageRoot), "utf8"));
8
+
9
+ const usage = `Usage: graphforge-agent-skills <command>
10
+
11
+ Commands:
12
+ compatibility --json Print the machine-readable GraphForge compatibility contract
13
+ --version Print the package version
14
+ --help Print this help
15
+
16
+ Workflow skills and runtime adapters land in later tracked slices.`;
17
+
18
+ const args = process.argv.slice(2);
19
+
20
+ if (args.length === 1 && args[0] === "--version") {
21
+ process.stdout.write(`${packageMetadata.version}\n`);
22
+ } else if (args.length === 2 && args[0] === "compatibility" && args[1] === "--json") {
23
+ process.stdout.write(`${JSON.stringify(compatibility)}\n`);
24
+ } else if (args.length === 0 || (args.length === 1 && args[0] === "--help")) {
25
+ process.stdout.write(`${usage}\n`);
26
+ } else {
27
+ process.stderr.write(`${usage}\n`);
28
+ process.exitCode = 2;
29
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "schema_version": 1,
3
+ "package": "@curatelabs/graphforge-agent-skills",
4
+ "package_version": "0.5.1",
5
+ "graphforge_release": "0.5.1",
6
+ "graphforge_version_range": ">=0.5.0 <0.6.0",
7
+ "security_contract": {
8
+ "subprocess": "unsupported",
9
+ "project_symlinks": "rejected",
10
+ "max_value_depth": 16,
11
+ "max_value_entries": 4096,
12
+ "max_value_string_length": 4096
13
+ }
14
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@curatelabs/graphforge-agent-skills",
3
+ "version": "0.5.1",
4
+ "description": "NPX-distributed agent skills and adapters for GraphForge",
5
+ "license": "Apache-2.0",
6
+ "homepage": "https://docs.graphforge.sh/",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/CurateLabs/graphforge.git",
10
+ "directory": "packages/agent-skills"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/CurateLabs/graphforge/issues"
14
+ },
15
+ "keywords": [
16
+ "graphforge",
17
+ "agent-skills",
18
+ "knowledge-graph",
19
+ "ai-agents",
20
+ "npx"
21
+ ],
22
+ "type": "module",
23
+ "bin": {
24
+ "graphforge-agent-skills": "bin/graphforge-agent-skills.js"
25
+ },
26
+ "files": [
27
+ "adapter/",
28
+ "bin/",
29
+ "schemas/",
30
+ "skills/",
31
+ "workflows/",
32
+ "compatibility.json",
33
+ "LICENSE",
34
+ "NOTICE",
35
+ "README.md"
36
+ ],
37
+ "exports": {
38
+ ".": "./adapter/index.js",
39
+ "./workflows": "./workflows/index.js",
40
+ "./schemas": "./schemas/validator.js",
41
+ "./schemas/input-envelope-v1.json": "./schemas/input-envelope-v1.json",
42
+ "./schemas/output-envelope-v1.json": "./schemas/output-envelope-v1.json",
43
+ "./schemas/skill-manifest-v1.json": "./schemas/skill-manifest-v1.json",
44
+ "./compatibility.json": "./compatibility.json"
45
+ },
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "graphforgeCompatibility": {
50
+ "schemaVersion": 1,
51
+ "release": "0.5.1",
52
+ "range": ">=0.5.0 <0.6.0"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public",
56
+ "registry": "https://registry.npmjs.org/"
57
+ },
58
+ "scripts": {
59
+ "test": "node --test tests/*.test.mjs",
60
+ "test:offline": "node tests/offline-pack-smoke.mjs",
61
+ "test:rc-native": "node scripts/run-native-rc-e2e.mjs",
62
+ "example:analyst": "node examples/analyst-agent.mjs",
63
+ "example:developer": "node examples/developer-agent.mjs",
64
+ "pack:local": "npm pack --ignore-scripts",
65
+ "format:check": "prettier --check package.json compatibility.json README.md adapter bin tests schemas skills workflows rc examples scripts"
66
+ }
67
+ }
@@ -0,0 +1,27 @@
1
+ # Agent skill schemas
2
+
3
+ GraphForge agent skills use three JSON Schema 2020-12 contracts. Each contract
4
+ is closed (`additionalProperties: false`) and carries the exact integer
5
+ `schema_version` it accepts:
6
+
7
+ - `skill-manifest-v1.json` describes a skill and its required GraphForge
8
+ capabilities.
9
+ - `input-envelope-v1.json` carries one skill invocation.
10
+ - `output-envelope-v1.json` carries either a successful result or a structured
11
+ error.
12
+
13
+ Import `validateSkillManifest`, `validateSkillInput`, or `validateSkillOutput`
14
+ from `@curatelabs/graphforge-agent-skills/schemas`. Validation is local and deterministic;
15
+ it does not open a GraphForge project, execute a skill, or access the network.
16
+ Each function returns `{ valid, diagnostics }`. Diagnostics contain only a
17
+ stable code, schema path, and fixed message. They never echo rejected values,
18
+ are sorted by path and code, and are capped at eight entries.
19
+
20
+ Envelope payloads are recursively bounded to depth 16, 4,096 visited entries,
21
+ and 4,096 characters per string. Cycles are rejected. The JSON schemas bound
22
+ nested strings, arrays, and objects; the dependency-free validator additionally
23
+ enforces the aggregate entry and depth budgets with fixed diagnostics.
24
+
25
+ Version 1 identifiers are lowercase kebab-case. Request IDs are lowercase
26
+ hyphenated UUIDs. Unknown fields, missing fields, malformed values, and any
27
+ schema version other than `1` fail closed.
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://graphforge.dev/schemas/agent-skills/input-envelope-v1.json",
4
+ "title": "GraphForge agent skill input envelope v1",
5
+ "$defs": {
6
+ "payloadValue": {
7
+ "anyOf": [
8
+ { "type": "null" },
9
+ { "type": "boolean" },
10
+ { "type": "number" },
11
+ { "type": "string", "maxLength": 4096 },
12
+ {
13
+ "type": "array",
14
+ "maxItems": 4096,
15
+ "items": { "$ref": "#/$defs/payloadValue" }
16
+ },
17
+ {
18
+ "type": "object",
19
+ "maxProperties": 128,
20
+ "additionalProperties": { "$ref": "#/$defs/payloadValue" }
21
+ }
22
+ ]
23
+ }
24
+ },
25
+ "type": "object",
26
+ "additionalProperties": false,
27
+ "required": ["schema_version", "skill_id", "request_id", "input"],
28
+ "properties": {
29
+ "schema_version": { "const": 1 },
30
+ "skill_id": {
31
+ "type": "string",
32
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
33
+ "maxLength": 64
34
+ },
35
+ "request_id": {
36
+ "type": "string",
37
+ "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
38
+ },
39
+ "input": {
40
+ "type": "object",
41
+ "maxProperties": 128,
42
+ "additionalProperties": { "$ref": "#/$defs/payloadValue" }
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,76 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://graphforge.dev/schemas/agent-skills/output-envelope-v1.json",
4
+ "title": "GraphForge agent skill output envelope v1",
5
+ "$defs": {
6
+ "payloadValue": {
7
+ "anyOf": [
8
+ { "type": "null" },
9
+ { "type": "boolean" },
10
+ { "type": "number" },
11
+ { "type": "string", "maxLength": 4096 },
12
+ {
13
+ "type": "array",
14
+ "maxItems": 4096,
15
+ "items": { "$ref": "#/$defs/payloadValue" }
16
+ },
17
+ {
18
+ "type": "object",
19
+ "maxProperties": 128,
20
+ "additionalProperties": { "$ref": "#/$defs/payloadValue" }
21
+ }
22
+ ]
23
+ }
24
+ },
25
+ "type": "object",
26
+ "additionalProperties": false,
27
+ "required": ["schema_version", "skill_id", "request_id", "status"],
28
+ "properties": {
29
+ "schema_version": { "const": 1 },
30
+ "skill_id": {
31
+ "type": "string",
32
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
33
+ "maxLength": 64
34
+ },
35
+ "request_id": {
36
+ "type": "string",
37
+ "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
38
+ },
39
+ "status": { "enum": ["ok", "error"] },
40
+ "output": {
41
+ "type": "object",
42
+ "maxProperties": 128,
43
+ "additionalProperties": { "$ref": "#/$defs/payloadValue" }
44
+ },
45
+ "error": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "required": ["code", "message"],
49
+ "properties": {
50
+ "code": {
51
+ "type": "string",
52
+ "pattern": "^GF_AGENT_[A-Z0-9_]+$",
53
+ "maxLength": 80
54
+ },
55
+ "message": { "type": "string", "minLength": 1, "maxLength": 500 },
56
+ "details": {
57
+ "type": "object",
58
+ "maxProperties": 64,
59
+ "additionalProperties": { "$ref": "#/$defs/payloadValue" }
60
+ }
61
+ }
62
+ }
63
+ },
64
+ "oneOf": [
65
+ {
66
+ "properties": { "status": { "const": "ok" } },
67
+ "required": ["output"],
68
+ "not": { "required": ["error"] }
69
+ },
70
+ {
71
+ "properties": { "status": { "const": "error" } },
72
+ "required": ["error"],
73
+ "not": { "required": ["output"] }
74
+ }
75
+ ]
76
+ }