graceful-boundaries 1.5.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/CONFORMANCE.md +78 -0
- package/LICENSE +36 -0
- package/LICENSE-SPEC +12 -0
- package/README.md +262 -0
- package/bin/cli.js +23 -0
- package/evals/check.js +1167 -0
- package/package.json +58 -0
- package/schema/limits.schema.json +128 -0
- package/schema/refusal-429.schema.json +8 -0
- package/schema/refusal.schema.json +100 -0
- package/spec.md +1013 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "graceful-boundaries",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "A specification for how services communicate their operational limits to humans and autonomous agents.",
|
|
5
|
+
"license": "CC-BY-4.0 AND MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"graceful-boundaries": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"evals/check.js",
|
|
12
|
+
"schema/",
|
|
13
|
+
"spec.md",
|
|
14
|
+
"CONFORMANCE.md",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"LICENSE-SPEC"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node evals/test-refusal.js && node evals/test-response-classes.js && node evals/test-security.js && node evals/test-conformance.js && node evals/test-discovery.js && node evals/test-action-boundaries.js && node evals/test-proactive-headers.js && node evals/test-html-refusal.js && node evals/test-agentic-surfaces.js && node evals/test-schemas.js && node evals/test-middleware-examples.js && node evals/test-agent-behavior.js",
|
|
23
|
+
"test:agent": "node evals/test-agent-behavior.js",
|
|
24
|
+
"test:schemas": "node evals/test-schemas.js",
|
|
25
|
+
"test:middleware": "node evals/test-middleware-examples.js",
|
|
26
|
+
"test:html": "node evals/test-html-refusal.js",
|
|
27
|
+
"test:refusal": "node evals/test-refusal.js",
|
|
28
|
+
"test:classes": "node evals/test-response-classes.js",
|
|
29
|
+
"test:security": "node evals/test-security.js",
|
|
30
|
+
"test:conformance": "node evals/test-conformance.js",
|
|
31
|
+
"test:discovery": "node evals/test-discovery.js",
|
|
32
|
+
"test:actions": "node evals/test-action-boundaries.js",
|
|
33
|
+
"test:headers": "node evals/test-proactive-headers.js",
|
|
34
|
+
"test:agentic": "node evals/test-agentic-surfaces.js",
|
|
35
|
+
"check": "node evals/check.js"
|
|
36
|
+
},
|
|
37
|
+
"type": "commonjs",
|
|
38
|
+
"author": "PAICE.work PBC <https://paice.work/>",
|
|
39
|
+
"homepage": "https://gracefulboundaries.dev",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/snapsynapse/graceful-boundaries/issues"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/snapsynapse/graceful-boundaries"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"graceful-boundaries",
|
|
49
|
+
"rate-limiting",
|
|
50
|
+
"api",
|
|
51
|
+
"specification",
|
|
52
|
+
"conformance",
|
|
53
|
+
"autonomous-agents",
|
|
54
|
+
"429",
|
|
55
|
+
"structured-refusal",
|
|
56
|
+
"limits-discovery"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://gracefulboundaries.dev/schema/limits.schema.json",
|
|
4
|
+
"title": "Graceful Boundaries limits discovery response",
|
|
5
|
+
"description": "Response shape for the limits discovery endpoint (GET /api/limits or GET /.well-known/limits) per Graceful Boundaries. Spec: https://gracefulboundaries.dev/spec",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["service", "description", "limits"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"service": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"minLength": 1,
|
|
12
|
+
"description": "Service name."
|
|
13
|
+
},
|
|
14
|
+
"description": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"minLength": 1,
|
|
17
|
+
"description": "What the service does."
|
|
18
|
+
},
|
|
19
|
+
"conformance": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"enum": ["not-applicable", "none", "level-1", "level-2", "level-3", "level-4"],
|
|
22
|
+
"description": "Self-declared conformance level. A self-assertion; external agents validate it against actual behavior."
|
|
23
|
+
},
|
|
24
|
+
"changelog": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "URL to a changelog resource for limit changes. Must be a relative path or same-origin absolute URL."
|
|
27
|
+
},
|
|
28
|
+
"feed": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "URL to a feed (JSON Feed, Atom, RSS) for change notifications. Must be a relative path or same-origin absolute URL."
|
|
31
|
+
},
|
|
32
|
+
"extensions": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"additionalProperties": { "type": "string" },
|
|
35
|
+
"description": "Named extension discovery URLs (e.g. actionBoundaries). Each value must be a relative path or same-origin absolute URL. Presence does not affect Level 1-4 conformance."
|
|
36
|
+
},
|
|
37
|
+
"limits": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"description": "Map of endpoint keys to documented endpoint limits. May be empty for not-applicable or none declarations.",
|
|
40
|
+
"additionalProperties": { "$ref": "#/$defs/endpointEntry" }
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"additionalProperties": true,
|
|
44
|
+
"$defs": {
|
|
45
|
+
"endpointEntry": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"required": ["endpoint", "method", "limits"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"endpoint": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"minLength": 1,
|
|
52
|
+
"description": "Path pattern, e.g. /api/scan."
|
|
53
|
+
},
|
|
54
|
+
"method": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"minLength": 1,
|
|
57
|
+
"description": "HTTP method."
|
|
58
|
+
},
|
|
59
|
+
"limits": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"items": { "$ref": "#/$defs/limitEntry" }
|
|
62
|
+
},
|
|
63
|
+
"note": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "Additional context for this endpoint."
|
|
66
|
+
},
|
|
67
|
+
"agentCapable": {
|
|
68
|
+
"type": "boolean",
|
|
69
|
+
"description": "Reserved (roadmap): whether the endpoint is designed for machine consumption."
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"additionalProperties": true
|
|
73
|
+
},
|
|
74
|
+
"limitEntry": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"required": ["type", "maxRequests", "windowSeconds", "description"],
|
|
77
|
+
"properties": {
|
|
78
|
+
"type": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "Limit category: ip-rate, key-rate, user-rate, global-rate, resource-dedup, cooldown, concurrency, quota, cost-limit, burst-rate, or a service-defined type."
|
|
81
|
+
},
|
|
82
|
+
"limitId": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "Stable identifier for this specific limit."
|
|
85
|
+
},
|
|
86
|
+
"limitType": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"description": "Explicit copy of the limit type when multiple limits interact."
|
|
89
|
+
},
|
|
90
|
+
"scope": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Subject of the limit: ip, key, user, resource, or global."
|
|
93
|
+
},
|
|
94
|
+
"maxRequests": {
|
|
95
|
+
"type": "number",
|
|
96
|
+
"description": "Maximum requests in the window."
|
|
97
|
+
},
|
|
98
|
+
"windowSeconds": {
|
|
99
|
+
"type": "number",
|
|
100
|
+
"description": "Window duration in seconds."
|
|
101
|
+
},
|
|
102
|
+
"costMetric": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "Resource unit for quota or cost limits: tokens, credits, bytes, dollars."
|
|
105
|
+
},
|
|
106
|
+
"maxInputBytes": { "type": "number" },
|
|
107
|
+
"maxInputTokens": { "type": "number" },
|
|
108
|
+
"maxOutputTokens": { "type": "number" },
|
|
109
|
+
"maxDurationSeconds": { "type": "number" },
|
|
110
|
+
"maxQueueDepth": { "type": "number" },
|
|
111
|
+
"windowResetAt": {
|
|
112
|
+
"type": ["string", "number"],
|
|
113
|
+
"description": "ISO timestamp or Unix timestamp for the window reset."
|
|
114
|
+
},
|
|
115
|
+
"returnsCached": {
|
|
116
|
+
"type": "boolean",
|
|
117
|
+
"description": "For resource-dedup limits: repeat requests return the cached result as a 200 instead of a 429."
|
|
118
|
+
},
|
|
119
|
+
"description": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"minLength": 1,
|
|
122
|
+
"description": "Human-readable explanation."
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"additionalProperties": true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://gracefulboundaries.dev/schema/refusal-429.schema.json",
|
|
4
|
+
"title": "Graceful Boundaries 429 refusal response",
|
|
5
|
+
"description": "Structured refusal shape for limit-class responses (429). Extends the core refusal shape with the fields Level 1 requires on rate-limit refusals. Spec: https://gracefulboundaries.dev/spec",
|
|
6
|
+
"$ref": "https://gracefulboundaries.dev/schema/refusal.schema.json",
|
|
7
|
+
"required": ["error", "detail", "why", "limit", "retryAfterSeconds"]
|
|
8
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://gracefulboundaries.dev/schema/refusal.schema.json",
|
|
4
|
+
"title": "Graceful Boundaries non-success response",
|
|
5
|
+
"description": "Core structured refusal shape for all non-success HTTP responses (400, 401, 403, 404, 405, 410, 422, 429, 500, 502, 503, 504) per Graceful Boundaries. Spec: https://gracefulboundaries.dev/spec",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["error", "detail", "why"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"error": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"pattern": "^[a-z0-9_]+$",
|
|
12
|
+
"description": "Stable machine-parseable error category in snake_case, e.g. rate_limit_exceeded, invalid_input, not_found."
|
|
13
|
+
},
|
|
14
|
+
"detail": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"minLength": 1,
|
|
17
|
+
"description": "Human-readable explanation of what happened and how to proceed."
|
|
18
|
+
},
|
|
19
|
+
"why": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"minLength": 1,
|
|
22
|
+
"description": "The security, policy, or operational reason this response exists. Must explain the purpose, not restate the error."
|
|
23
|
+
},
|
|
24
|
+
"limit": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "The limit that was exceeded, in human-readable form. Required for 429 responses (see refusal-429.schema.json)."
|
|
27
|
+
},
|
|
28
|
+
"retryAfterSeconds": {
|
|
29
|
+
"type": "integer",
|
|
30
|
+
"minimum": 0,
|
|
31
|
+
"description": "Seconds until the caller can retry. Required for 429 responses; optional for availability errors."
|
|
32
|
+
},
|
|
33
|
+
"limitId": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Stable identifier for the specific limit exceeded."
|
|
36
|
+
},
|
|
37
|
+
"limitType": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Type of the limit exceeded, such as ip-rate or cost-limit."
|
|
40
|
+
},
|
|
41
|
+
"scope": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Subject of the exceeded limit, such as ip, key, user, resource, or global."
|
|
44
|
+
},
|
|
45
|
+
"windowResetAt": {
|
|
46
|
+
"type": ["string", "number"],
|
|
47
|
+
"description": "ISO timestamp or Unix timestamp for the window reset."
|
|
48
|
+
},
|
|
49
|
+
"cached": {
|
|
50
|
+
"type": "boolean",
|
|
51
|
+
"description": "Whether a cached result is available."
|
|
52
|
+
},
|
|
53
|
+
"cachedResultUrl": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "URL to retrieve the cached result. Must be a relative path or same-origin absolute URL (SC-6)."
|
|
56
|
+
},
|
|
57
|
+
"alternativeEndpoint": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "A different endpoint that may serve the need. Must be a relative path or same-origin absolute URL (SC-6)."
|
|
60
|
+
},
|
|
61
|
+
"upgradeUrl": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "Where to get higher limits."
|
|
64
|
+
},
|
|
65
|
+
"humanUrl": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "Browser-friendly URL for human follow-up."
|
|
68
|
+
},
|
|
69
|
+
"field": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"description": "Which input field failed validation (input class)."
|
|
72
|
+
},
|
|
73
|
+
"expected": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "What valid input looks like, stated positively (SC-3)."
|
|
76
|
+
},
|
|
77
|
+
"allowedMethods": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": { "type": "string" },
|
|
80
|
+
"description": "Which HTTP methods are accepted (405 responses)."
|
|
81
|
+
},
|
|
82
|
+
"authUrl": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "Where to obtain credentials (access class)."
|
|
85
|
+
},
|
|
86
|
+
"scanAvailable": {
|
|
87
|
+
"type": "boolean",
|
|
88
|
+
"description": "Whether the caller can create the missing resource (not-found class)."
|
|
89
|
+
},
|
|
90
|
+
"scanUrl": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Endpoint to create the missing resource. Not a trust bypass (SC-8)."
|
|
93
|
+
},
|
|
94
|
+
"statusUrl": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "Status page or health check endpoint (availability class)."
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"additionalProperties": true
|
|
100
|
+
}
|