@qijenchen/governance 0.1.0-beta.94
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/README.md +89 -0
- package/bin/attest.mjs +4 -0
- package/bin/check.mjs +4 -0
- package/bin/doctor.mjs +4 -0
- package/bin/generate.mjs +4 -0
- package/bin/governance.mjs +4 -0
- package/bin/hook.mjs +4 -0
- package/bin/upgrade.mjs +4 -0
- package/canonical/gates.json +42 -0
- package/canonical/manifest.json +476 -0
- package/canonical/plugin-aliases.json +25 -0
- package/canonical/provider-lifecycle.json +48 -0
- package/canonical/providers.json +455 -0
- package/canonical/roles.json +12 -0
- package/canonical/rules.json +81 -0
- package/canonical/schemas/attestation.schema.json +61 -0
- package/canonical/schemas/diagnostic.schema.json +37 -0
- package/canonical/schemas/gates.schema.json +27 -0
- package/canonical/schemas/lock.schema.json +189 -0
- package/canonical/schemas/manifest.schema.json +103 -0
- package/canonical/schemas/plugin-aliases.schema.json +59 -0
- package/canonical/schemas/provider-hook-coverage.schema.json +173 -0
- package/canonical/schemas/provider-lifecycle.schema.json +126 -0
- package/canonical/schemas/providers.schema.json +917 -0
- package/canonical/schemas/roles.schema.json +27 -0
- package/canonical/schemas/rules.schema.json +46 -0
- package/canonical/schemas/upgrade-plan.schema.json +31 -0
- package/package.json +42 -0
- package/src/authority-decision-evidence.mjs +413 -0
- package/src/canonical-order.mjs +8 -0
- package/src/carrier-projection.mjs +407 -0
- package/src/cli.mjs +114 -0
- package/src/closed-tool-execution.mjs +1001 -0
- package/src/common.mjs +278 -0
- package/src/contract.mjs +760 -0
- package/src/hook-api.mjs +107 -0
- package/src/index.mjs +14 -0
- package/src/provider-hook-normalization.mjs +1646 -0
- package/src/provider-review-binding.mjs +2377 -0
- package/src/snapshot.mjs +520 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://qijenchen.dev/schemas/governance/roles-v1.json",
|
|
4
|
+
"title": "Governance role registry",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "roles"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"roles": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"minItems": 1,
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"required": ["id", "providerSelection", "requiredGateIds", "requiredSourceIds"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
20
|
+
"providerSelection": { "const": "all-registered" },
|
|
21
|
+
"requiredGateIds": { "type": "array", "minItems": 1, "items": { "type": "string" } },
|
|
22
|
+
"requiredSourceIds": { "type": "array", "minItems": 1, "items": { "type": "string" } }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://qijenchen.dev/schemas/governance/rules-v1.json",
|
|
4
|
+
"title": "Governance rule registry",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "rules"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"rules": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"minItems": 1,
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"required": ["ruleId", "severity", "source", "appliesToRoles", "match", "blocking", "providerCoverage", "gateIds"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"ruleId": { "type": "string", "pattern": "^GOV-[A-Z]+-[0-9]{3}$" },
|
|
20
|
+
"severity": { "enum": ["critical", "major", "minor", "info"] },
|
|
21
|
+
"source": { "type": "string", "minLength": 1 },
|
|
22
|
+
"appliesToRoles": { "type": "array", "minItems": 1, "items": { "type": "string" } },
|
|
23
|
+
"match": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["kind"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"kind": { "enum": ["protected-generated-path", "registry-invariant"] },
|
|
29
|
+
"pathTemplate": { "type": "string" }
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"blocking": { "type": "boolean" },
|
|
33
|
+
"providerCoverage": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"required": ["*"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"*": { "enum": ["native", "adapter", "hard-gate", "unsupported"] }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"gateIds": { "type": "array", "minItems": 1, "items": { "type": "string" } }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://qijenchen.dev/schemas/governance-upgrade-plan.schema.json",
|
|
4
|
+
"title": "Governance upgrade plan",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "command", "role", "outputDirectory", "lockFile", "contractDigest", "currentDigest", "expectedDigest", "actions"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": { "const": 1 },
|
|
9
|
+
"command": { "const": "upgrade.plan" },
|
|
10
|
+
"role": { "type": "string" },
|
|
11
|
+
"outputDirectory": { "type": "string" },
|
|
12
|
+
"lockFile": { "type": "string" },
|
|
13
|
+
"contractDigest": { "$ref": "#/$defs/digest" },
|
|
14
|
+
"currentDigest": { "$ref": "#/$defs/digest" },
|
|
15
|
+
"expectedDigest": { "$ref": "#/$defs/digest" },
|
|
16
|
+
"actions": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": ["action", "path"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"action": { "enum": ["add", "remove", "replace-content", "replace-mode", "replace-type"] },
|
|
23
|
+
"path": { "type": "string" }
|
|
24
|
+
},
|
|
25
|
+
"additionalProperties": false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"$defs": { "digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" } },
|
|
30
|
+
"additionalProperties": false
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qijenchen/governance",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "git+https://github.com/ajenchen/design-system.git"
|
|
6
|
+
},
|
|
7
|
+
"version": "0.1.0-beta.94",
|
|
8
|
+
"description": "Provider-neutral DS-author control plane for deterministic snapshots, adapters, checks, and evidence manifests.",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"license": "UNLICENSED",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"canonical",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"qijenchen-governance": "./bin/governance.mjs",
|
|
22
|
+
"qijenchen-governance-generate": "./bin/generate.mjs",
|
|
23
|
+
"qijenchen-governance-check": "./bin/check.mjs",
|
|
24
|
+
"qijenchen-governance-doctor": "./bin/doctor.mjs",
|
|
25
|
+
"qijenchen-governance-hook": "./bin/hook.mjs",
|
|
26
|
+
"qijenchen-governance-attest": "./bin/attest.mjs",
|
|
27
|
+
"qijenchen-governance-upgrade": "./bin/upgrade.mjs"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./src/index.mjs",
|
|
31
|
+
"./closed-tool-execution": "./src/closed-tool-execution.mjs",
|
|
32
|
+
"./hook": "./src/hook-api.mjs",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test test/*.test.mjs"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"ajv": "8.20.0",
|
|
40
|
+
"ajv-formats": "3.0.1"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
export const AUTHORITY_DECISION_RECEIPT_KIND = 'provider-neutral-authority-decision-receipt'
|
|
5
|
+
export const AUTHORITY_DECISION_RECEIPT_SCHEMA_VERSION = 2
|
|
6
|
+
export const AUTHORITY_DECISION_POLICY_ID = 'canonical-decision-authority-v1'
|
|
7
|
+
export const AUTHORITY_DECISION_CLASSIFIER_ID = 'approval-evidence-v1'
|
|
8
|
+
|
|
9
|
+
const AUTHORITY_START = '<!-- canonical-decision-authority:start -->'
|
|
10
|
+
const AUTHORITY_END = '<!-- canonical-decision-authority:end -->'
|
|
11
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/
|
|
12
|
+
const PROVIDER_ID = /^[a-z][a-z0-9-]*$/
|
|
13
|
+
const RUNTIME_SURFACE = /^[a-z0-9][a-z0-9._/-]{0,255}$/
|
|
14
|
+
const MAX_TARGET_BYTES = 16 * 1024
|
|
15
|
+
const MAX_OPERATION_BYTES = 16 * 1024 * 1024
|
|
16
|
+
const MAX_USER_MESSAGE_BYTES = 4 * 1024 * 1024
|
|
17
|
+
const MAX_USER_MESSAGES_BYTES = 16 * 1024 * 1024
|
|
18
|
+
const MAX_USER_MESSAGE_COUNT = 4096
|
|
19
|
+
const MAX_SCOPE_VALUE_BYTES = 4096
|
|
20
|
+
|
|
21
|
+
function canonicalize(value) {
|
|
22
|
+
if (Array.isArray(value)) return value.map(canonicalize)
|
|
23
|
+
if (value && typeof value === 'object') {
|
|
24
|
+
return Object.fromEntries(
|
|
25
|
+
Object.keys(value)
|
|
26
|
+
.filter((key) => value[key] !== undefined)
|
|
27
|
+
.sort((left, right) => Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8')))
|
|
28
|
+
.map((key) => [key, canonicalize(value[key])]),
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stableJson(value) {
|
|
35
|
+
return `${JSON.stringify(canonicalize(value), null, 2)}\n`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function digest(value) {
|
|
39
|
+
const bytes = Buffer.isBuffer(value)
|
|
40
|
+
? value
|
|
41
|
+
: Buffer.from(typeof value === 'string' ? value : stableJson(value))
|
|
42
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const RECEIPT_KEYS = Object.freeze([
|
|
46
|
+
'binding',
|
|
47
|
+
'classification',
|
|
48
|
+
'classificationMode',
|
|
49
|
+
'kind',
|
|
50
|
+
'receiptDigest',
|
|
51
|
+
'schemaVersion',
|
|
52
|
+
'source',
|
|
53
|
+
'trust',
|
|
54
|
+
])
|
|
55
|
+
const BINDING_KEYS = Object.freeze([
|
|
56
|
+
'operationEvidenceDigest',
|
|
57
|
+
'providerId',
|
|
58
|
+
'runtimeSurface',
|
|
59
|
+
'scopeNonceDigest',
|
|
60
|
+
'sessionDigest',
|
|
61
|
+
'target',
|
|
62
|
+
'userMessagesDigest',
|
|
63
|
+
])
|
|
64
|
+
const SOURCE_KEYS = Object.freeze([
|
|
65
|
+
'authorityPolicy',
|
|
66
|
+
'authorityPolicyDigest',
|
|
67
|
+
'classifier',
|
|
68
|
+
'classifierDigest',
|
|
69
|
+
'providerRegistry',
|
|
70
|
+
'providerRegistryDigest',
|
|
71
|
+
'receiptContract',
|
|
72
|
+
'receiptContractDigest',
|
|
73
|
+
])
|
|
74
|
+
const TRUST_KEYS = Object.freeze([
|
|
75
|
+
'contentAddressed',
|
|
76
|
+
'promotionEligible',
|
|
77
|
+
'rawAuthorityTextStored',
|
|
78
|
+
'rawOperationTextStored',
|
|
79
|
+
'runtimeCertification',
|
|
80
|
+
])
|
|
81
|
+
const CLASSIFICATION_KEYS = Object.freeze([
|
|
82
|
+
'allowedScopes',
|
|
83
|
+
'decision',
|
|
84
|
+
'decisionDomain',
|
|
85
|
+
'decisionMessageSha256',
|
|
86
|
+
'deniedOrAmbiguousScopes',
|
|
87
|
+
'humanOnlyScopes',
|
|
88
|
+
'latestUserMessageSha256',
|
|
89
|
+
'operationEvidenceSha256',
|
|
90
|
+
'reasonCode',
|
|
91
|
+
'target',
|
|
92
|
+
'targetBinding',
|
|
93
|
+
])
|
|
94
|
+
const CLASSIFICATION_SCOPE_IDS = new Set([
|
|
95
|
+
'account-holder-platform-action',
|
|
96
|
+
'approved-ui-ux-implementation',
|
|
97
|
+
'consumer-template-adoption',
|
|
98
|
+
'credential-reference-required',
|
|
99
|
+
'current-target-operation',
|
|
100
|
+
'engineering-execution',
|
|
101
|
+
'external-activation',
|
|
102
|
+
'github-configuration',
|
|
103
|
+
'legal-account-organization-business-decision',
|
|
104
|
+
'mechanical-approved-ssot-projection',
|
|
105
|
+
'package-release',
|
|
106
|
+
'plan-external-spend',
|
|
107
|
+
'product-ui-ux-change',
|
|
108
|
+
'product-ui-ux-decision',
|
|
109
|
+
'protected-git-delivery',
|
|
110
|
+
'rollback-recovery',
|
|
111
|
+
'rollout',
|
|
112
|
+
'runtime-certification',
|
|
113
|
+
'testing-and-harness',
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
export class AuthorityDecisionEvidenceError extends Error {
|
|
117
|
+
constructor(code, detail) {
|
|
118
|
+
super(`authority decision evidence blocked:${code}:${detail}`)
|
|
119
|
+
this.name = 'AuthorityDecisionEvidenceError'
|
|
120
|
+
this.code = code
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function block(code, detail) {
|
|
125
|
+
throw new AuthorityDecisionEvidenceError(code, detail)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function exactObject(value, keys, label) {
|
|
129
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
130
|
+
block('RECEIPT_SHAPE_INVALID', `${label} must be an object`)
|
|
131
|
+
}
|
|
132
|
+
const actual = Object.keys(value).sort()
|
|
133
|
+
const expected = [...keys].sort()
|
|
134
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
135
|
+
block('RECEIPT_SHAPE_INVALID', `${label} has an open or incomplete shape`)
|
|
136
|
+
}
|
|
137
|
+
return value
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function exactString(value, label, maximumBytes, { pattern = null, allowEmpty = false } = {}) {
|
|
141
|
+
if (typeof value !== 'string' || (!allowEmpty && value.length === 0)) {
|
|
142
|
+
block('INPUT_INVALID', `${label} must be ${allowEmpty ? 'a' : 'a non-empty'} string`)
|
|
143
|
+
}
|
|
144
|
+
const bytes = Buffer.byteLength(value, 'utf8')
|
|
145
|
+
if (bytes > maximumBytes || value.includes('\0')) {
|
|
146
|
+
block('INPUT_INVALID', `${label} exceeds its closed byte or character contract`)
|
|
147
|
+
}
|
|
148
|
+
if (pattern && !pattern.test(value)) block('INPUT_INVALID', `${label} has an invalid format`)
|
|
149
|
+
return value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function exactBytes(value, label) {
|
|
153
|
+
if (!(typeof value === 'string' || Buffer.isBuffer(value) || ArrayBuffer.isView(value))) {
|
|
154
|
+
block('INPUT_INVALID', `${label} bytes are unavailable`)
|
|
155
|
+
}
|
|
156
|
+
return Buffer.isBuffer(value) ? Buffer.from(value) : Buffer.from(value)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function exactDigest(value, label) {
|
|
160
|
+
if (typeof value !== 'string' || !SHA256.test(value)) {
|
|
161
|
+
block('RECEIPT_SHAPE_INVALID', `${label} is not a canonical sha256 digest`)
|
|
162
|
+
}
|
|
163
|
+
return value
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function exactBoolean(value, expected, label) {
|
|
167
|
+
if (value !== expected) block('RECEIPT_SHAPE_INVALID', `${label} is invalid`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizedUserMessages(value) {
|
|
171
|
+
if (!Array.isArray(value) || value.length > MAX_USER_MESSAGE_COUNT) {
|
|
172
|
+
block('INPUT_INVALID', 'userMessages must be one bounded array')
|
|
173
|
+
}
|
|
174
|
+
let aggregate = 0
|
|
175
|
+
const messages = value.map((message, index) => {
|
|
176
|
+
exactString(message, `userMessages[${index}]`, MAX_USER_MESSAGE_BYTES, { allowEmpty: false })
|
|
177
|
+
aggregate += Buffer.byteLength(message, 'utf8')
|
|
178
|
+
if (aggregate > MAX_USER_MESSAGES_BYTES) {
|
|
179
|
+
block('INPUT_INVALID', 'userMessages exceed the aggregate byte contract')
|
|
180
|
+
}
|
|
181
|
+
return message
|
|
182
|
+
})
|
|
183
|
+
return messages
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function registeredProvider(registry, providerId) {
|
|
187
|
+
if (!registry || typeof registry !== 'object' || Array.isArray(registry)
|
|
188
|
+
|| !Array.isArray(registry.providers)) {
|
|
189
|
+
block('PROVIDER_REGISTRY_INVALID', 'provider registry has no closed provider list')
|
|
190
|
+
}
|
|
191
|
+
exactString(providerId, 'providerId', 128, { pattern: PROVIDER_ID })
|
|
192
|
+
const matches = registry.providers.filter((provider) => provider?.id === providerId)
|
|
193
|
+
if (matches.length !== 1) {
|
|
194
|
+
block(matches.length === 0 ? 'PROVIDER_UNKNOWN' : 'PROVIDER_REGISTRY_INVALID', providerId)
|
|
195
|
+
}
|
|
196
|
+
return matches[0]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function canonicalAuthorityPolicy(bytes) {
|
|
200
|
+
const text = exactBytes(bytes, 'authority policy').toString('utf8')
|
|
201
|
+
const start = text.indexOf(AUTHORITY_START)
|
|
202
|
+
const end = text.indexOf(AUTHORITY_END)
|
|
203
|
+
if (start < 0 || end < start
|
|
204
|
+
|| text.indexOf(AUTHORITY_START, start + AUTHORITY_START.length) >= 0
|
|
205
|
+
|| text.indexOf(AUTHORITY_END, end + AUTHORITY_END.length) >= 0) {
|
|
206
|
+
block('AUTHORITY_POLICY_INVALID', 'canonical authority markers must occur exactly once')
|
|
207
|
+
}
|
|
208
|
+
return `${text.slice(start, end + AUTHORITY_END.length)}\n`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function classifierFunctions(classifier) {
|
|
212
|
+
if (!classifier || typeof classifier !== 'object'
|
|
213
|
+
|| typeof classifier.classifyLatestAuthorization !== 'function'
|
|
214
|
+
|| typeof classifier.classifyOperationAuthorization !== 'function') {
|
|
215
|
+
block('CLASSIFIER_INVALID', 'canonical authority classifier functions are unavailable')
|
|
216
|
+
}
|
|
217
|
+
return classifier
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function classificationScopes(value, label) {
|
|
221
|
+
if (!Array.isArray(value)
|
|
222
|
+
|| value.some((scope) => typeof scope !== 'string' || !CLASSIFICATION_SCOPE_IDS.has(scope))) {
|
|
223
|
+
block('CLASSIFIER_INVALID', `${label} is not one closed scope list`)
|
|
224
|
+
}
|
|
225
|
+
const sorted = [...value].sort((left, right) => (
|
|
226
|
+
Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'))
|
|
227
|
+
))
|
|
228
|
+
if (new Set(value).size !== value.length
|
|
229
|
+
|| value.some((scope, index) => scope !== sorted[index])) {
|
|
230
|
+
block('CLASSIFIER_INVALID', `${label} is not sorted and unique`)
|
|
231
|
+
}
|
|
232
|
+
return value
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function classificationResult(value) {
|
|
236
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
237
|
+
|| !['approved', 'blocked'].includes(value.decision)
|
|
238
|
+
|| !['engineering-remediation', 'product-ui-ux', 'unknown'].includes(value.decisionDomain)
|
|
239
|
+
|| typeof value.reasonCode !== 'string'
|
|
240
|
+
|| typeof value.operationEvidenceSha256 !== 'string'
|
|
241
|
+
|| !/^[a-f0-9]{64}$/.test(value.operationEvidenceSha256)) {
|
|
242
|
+
block('CLASSIFIER_INVALID', 'canonical classifier returned an invalid decision')
|
|
243
|
+
}
|
|
244
|
+
const actualKeys = Object.keys(value).sort()
|
|
245
|
+
const expectedKeys = [...CLASSIFICATION_KEYS].sort()
|
|
246
|
+
if (actualKeys.length !== expectedKeys.length
|
|
247
|
+
|| actualKeys.some((key, index) => key !== expectedKeys[index])) {
|
|
248
|
+
block('CLASSIFIER_INVALID', 'canonical classifier returned an open or incomplete decision')
|
|
249
|
+
}
|
|
250
|
+
const scopeLists = [
|
|
251
|
+
classificationScopes(value.allowedScopes, 'classification.allowedScopes'),
|
|
252
|
+
classificationScopes(value.humanOnlyScopes, 'classification.humanOnlyScopes'),
|
|
253
|
+
classificationScopes(
|
|
254
|
+
value.deniedOrAmbiguousScopes,
|
|
255
|
+
'classification.deniedOrAmbiguousScopes',
|
|
256
|
+
),
|
|
257
|
+
]
|
|
258
|
+
const allScopes = scopeLists.flat()
|
|
259
|
+
if (new Set(allScopes).size !== allScopes.length) {
|
|
260
|
+
block('CLASSIFIER_INVALID', 'classification scope lists overlap')
|
|
261
|
+
}
|
|
262
|
+
return canonicalize(value)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function receiptPayload({
|
|
266
|
+
authorityPolicyBytes,
|
|
267
|
+
classifierSourceBytes,
|
|
268
|
+
receiptContractSourceBytes,
|
|
269
|
+
classifier,
|
|
270
|
+
providerRegistry,
|
|
271
|
+
providerRegistrySource = 'packages/governance/canonical/providers.json',
|
|
272
|
+
providerId,
|
|
273
|
+
runtimeSurface,
|
|
274
|
+
sessionId,
|
|
275
|
+
scopeNonce,
|
|
276
|
+
target,
|
|
277
|
+
operationText,
|
|
278
|
+
userMessages,
|
|
279
|
+
}) {
|
|
280
|
+
registeredProvider(providerRegistry, providerId)
|
|
281
|
+
exactString(providerRegistrySource, 'providerRegistrySource', 512)
|
|
282
|
+
if (providerRegistrySource.includes('\\')
|
|
283
|
+
|| providerRegistrySource.startsWith('/')
|
|
284
|
+
|| providerRegistrySource.split('#', 1)[0].split('/').some((segment) => (
|
|
285
|
+
!segment || segment === '.' || segment === '..'
|
|
286
|
+
))) {
|
|
287
|
+
block('INPUT_INVALID', 'providerRegistrySource is not one canonical repository reference')
|
|
288
|
+
}
|
|
289
|
+
exactString(runtimeSurface, 'runtimeSurface', 256, { pattern: RUNTIME_SURFACE })
|
|
290
|
+
exactString(sessionId, 'sessionId', MAX_SCOPE_VALUE_BYTES)
|
|
291
|
+
exactString(scopeNonce, 'scopeNonce', MAX_SCOPE_VALUE_BYTES)
|
|
292
|
+
exactString(target, 'target', MAX_TARGET_BYTES)
|
|
293
|
+
exactString(operationText, 'operationText', MAX_OPERATION_BYTES, { allowEmpty: true })
|
|
294
|
+
const messages = normalizedUserMessages(userMessages)
|
|
295
|
+
const functions = classifierFunctions(classifier)
|
|
296
|
+
const classificationMode = messages.length ? 'user-context' : 'operation-only'
|
|
297
|
+
const classification = classificationResult(messages.length
|
|
298
|
+
? functions.classifyLatestAuthorization(messages.at(-1), {
|
|
299
|
+
target,
|
|
300
|
+
operationText,
|
|
301
|
+
userMessages: messages,
|
|
302
|
+
})
|
|
303
|
+
: functions.classifyOperationAuthorization({ target, operationText }))
|
|
304
|
+
const authorityPolicy = canonicalAuthorityPolicy(authorityPolicyBytes)
|
|
305
|
+
const classifierBytes = exactBytes(classifierSourceBytes, 'classifier source')
|
|
306
|
+
const receiptContractBytes = exactBytes(receiptContractSourceBytes, 'receipt contract source')
|
|
307
|
+
return canonicalize({
|
|
308
|
+
schemaVersion: AUTHORITY_DECISION_RECEIPT_SCHEMA_VERSION,
|
|
309
|
+
kind: AUTHORITY_DECISION_RECEIPT_KIND,
|
|
310
|
+
source: {
|
|
311
|
+
authorityPolicy: 'AGENTS.md#canonical-decision-authority',
|
|
312
|
+
authorityPolicyDigest: digest(authorityPolicy),
|
|
313
|
+
classifier: 'packages/design-system/ds-canonical/hooks/lib/approval-evidence.mjs',
|
|
314
|
+
classifierDigest: digest(classifierBytes),
|
|
315
|
+
providerRegistry: providerRegistrySource,
|
|
316
|
+
providerRegistryDigest: digest(providerRegistry),
|
|
317
|
+
receiptContract: 'packages/governance/src/authority-decision-evidence.mjs',
|
|
318
|
+
receiptContractDigest: digest(receiptContractBytes),
|
|
319
|
+
},
|
|
320
|
+
binding: {
|
|
321
|
+
providerId,
|
|
322
|
+
runtimeSurface,
|
|
323
|
+
sessionDigest: digest(sessionId),
|
|
324
|
+
scopeNonceDigest: digest(scopeNonce),
|
|
325
|
+
userMessagesDigest: digest(messages),
|
|
326
|
+
target: classification.target,
|
|
327
|
+
operationEvidenceDigest: `sha256:${classification.operationEvidenceSha256}`,
|
|
328
|
+
},
|
|
329
|
+
classificationMode,
|
|
330
|
+
classification,
|
|
331
|
+
trust: {
|
|
332
|
+
contentAddressed: true,
|
|
333
|
+
runtimeCertification: 'not-certified',
|
|
334
|
+
promotionEligible: false,
|
|
335
|
+
rawAuthorityTextStored: false,
|
|
336
|
+
rawOperationTextStored: false,
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function prepareAuthorityDecisionReceipt(input = {}) {
|
|
342
|
+
const payload = receiptPayload(input)
|
|
343
|
+
return canonicalize({
|
|
344
|
+
...payload,
|
|
345
|
+
receiptDigest: digest(payload),
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function validateAuthorityDecisionReceiptShape(receipt) {
|
|
350
|
+
exactObject(receipt, RECEIPT_KEYS, 'receipt')
|
|
351
|
+
if (receipt.schemaVersion !== AUTHORITY_DECISION_RECEIPT_SCHEMA_VERSION
|
|
352
|
+
|| receipt.kind !== AUTHORITY_DECISION_RECEIPT_KIND) {
|
|
353
|
+
block('RECEIPT_SHAPE_INVALID', 'receipt identity is invalid')
|
|
354
|
+
}
|
|
355
|
+
exactObject(receipt.source, SOURCE_KEYS, 'receipt.source')
|
|
356
|
+
exactObject(receipt.binding, BINDING_KEYS, 'receipt.binding')
|
|
357
|
+
exactObject(receipt.trust, TRUST_KEYS, 'receipt.trust')
|
|
358
|
+
for (const [key, value] of Object.entries(receipt.source)) {
|
|
359
|
+
if (key.endsWith('Digest')) exactDigest(value, `receipt.source.${key}`)
|
|
360
|
+
else exactString(value, `receipt.source.${key}`, 512)
|
|
361
|
+
}
|
|
362
|
+
for (const key of [
|
|
363
|
+
'operationEvidenceDigest',
|
|
364
|
+
'scopeNonceDigest',
|
|
365
|
+
'sessionDigest',
|
|
366
|
+
'userMessagesDigest',
|
|
367
|
+
]) exactDigest(receipt.binding[key], `receipt.binding.${key}`)
|
|
368
|
+
exactString(receipt.binding.providerId, 'receipt.binding.providerId', 128, { pattern: PROVIDER_ID })
|
|
369
|
+
exactString(receipt.binding.runtimeSurface, 'receipt.binding.runtimeSurface', 256, { pattern: RUNTIME_SURFACE })
|
|
370
|
+
exactString(receipt.binding.target, 'receipt.binding.target', MAX_TARGET_BYTES)
|
|
371
|
+
if (!['user-context', 'operation-only'].includes(receipt.classificationMode)) {
|
|
372
|
+
block('RECEIPT_SHAPE_INVALID', 'classificationMode is invalid')
|
|
373
|
+
}
|
|
374
|
+
classificationResult(receipt.classification)
|
|
375
|
+
exactBoolean(receipt.trust.contentAddressed, true, 'receipt.trust.contentAddressed')
|
|
376
|
+
exactBoolean(receipt.trust.promotionEligible, false, 'receipt.trust.promotionEligible')
|
|
377
|
+
exactBoolean(receipt.trust.rawAuthorityTextStored, false, 'receipt.trust.rawAuthorityTextStored')
|
|
378
|
+
exactBoolean(receipt.trust.rawOperationTextStored, false, 'receipt.trust.rawOperationTextStored')
|
|
379
|
+
if (receipt.trust.runtimeCertification !== 'not-certified') {
|
|
380
|
+
block('RECEIPT_SHAPE_INVALID', 'receipt must not claim runtime certification')
|
|
381
|
+
}
|
|
382
|
+
exactDigest(receipt.receiptDigest, 'receipt.receiptDigest')
|
|
383
|
+
const { receiptDigest, ...payload } = receipt
|
|
384
|
+
if (digest(payload) !== receiptDigest) {
|
|
385
|
+
block('RECEIPT_DIGEST_MISMATCH', 'receipt content address does not match its payload')
|
|
386
|
+
}
|
|
387
|
+
return true
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function verifyAuthorityDecisionReceipt({ receipt, ...expectedInput } = {}) {
|
|
391
|
+
validateAuthorityDecisionReceiptShape(receipt)
|
|
392
|
+
const expected = prepareAuthorityDecisionReceipt(expectedInput)
|
|
393
|
+
const checks = [
|
|
394
|
+
['source.authorityPolicyDigest', receipt.source.authorityPolicyDigest, expected.source.authorityPolicyDigest, 'AUTHORITY_POLICY_STALE'],
|
|
395
|
+
['source.classifierDigest', receipt.source.classifierDigest, expected.source.classifierDigest, 'CLASSIFIER_STALE'],
|
|
396
|
+
['source.providerRegistryDigest', receipt.source.providerRegistryDigest, expected.source.providerRegistryDigest, 'PROVIDER_REGISTRY_STALE'],
|
|
397
|
+
['source.receiptContractDigest', receipt.source.receiptContractDigest, expected.source.receiptContractDigest, 'RECEIPT_CONTRACT_STALE'],
|
|
398
|
+
['binding.providerId', receipt.binding.providerId, expected.binding.providerId, 'PROVIDER_SUBSTITUTION'],
|
|
399
|
+
['binding.runtimeSurface', receipt.binding.runtimeSurface, expected.binding.runtimeSurface, 'RUNTIME_SURFACE_SUBSTITUTION'],
|
|
400
|
+
['binding.sessionDigest', receipt.binding.sessionDigest, expected.binding.sessionDigest, 'SESSION_SUBSTITUTION'],
|
|
401
|
+
['binding.scopeNonceDigest', receipt.binding.scopeNonceDigest, expected.binding.scopeNonceDigest, 'REPLAY_OR_SCOPE_MISMATCH'],
|
|
402
|
+
['binding.userMessagesDigest', receipt.binding.userMessagesDigest, expected.binding.userMessagesDigest, 'AUTHORITY_CONTEXT_SUBSTITUTION'],
|
|
403
|
+
['binding.target', receipt.binding.target, expected.binding.target, 'TARGET_SUBSTITUTION'],
|
|
404
|
+
['binding.operationEvidenceDigest', receipt.binding.operationEvidenceDigest, expected.binding.operationEvidenceDigest, 'OPERATION_SUBSTITUTION'],
|
|
405
|
+
]
|
|
406
|
+
for (const [label, actual, expectedValue, code] of checks) {
|
|
407
|
+
if (actual !== expectedValue) block(code, label)
|
|
408
|
+
}
|
|
409
|
+
if (stableJson(receipt) !== stableJson(expected)) {
|
|
410
|
+
block('RECEIPT_NONCANONICAL', 'receipt differs from the current canonical classifier result')
|
|
411
|
+
}
|
|
412
|
+
return true
|
|
413
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
|
|
3
|
+
// Zero-I/O ordering primitive shared by authority, package, generated adapter, fork, and
|
|
4
|
+
// product-template runtimes. Canonical artifacts preserve code points exactly and compare
|
|
5
|
+
// their UTF-8 bytes; host locale collation and implicit Unicode normalization are forbidden.
|
|
6
|
+
export function compareUtf8Bytes(left, right) {
|
|
7
|
+
return Buffer.compare(Buffer.from(String(left), 'utf8'), Buffer.from(String(right), 'utf8'))
|
|
8
|
+
}
|