@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,407 @@
|
|
|
1
|
+
import { stableJson } from './common.mjs'
|
|
2
|
+
|
|
3
|
+
export const EXTERNAL_ACTIVATION_CARRIER_PATH = 'infra/governance/external-activation-requirements.json'
|
|
4
|
+
export const PROVIDER_CERTIFICATION_CARRIER_PATH = 'infra/governance/providers/certifications.json'
|
|
5
|
+
export const REVIEW_CAPABILITY_CERTIFICATION_CARRIER_PATH = 'infra/governance/providers/review-capability-certifications.json'
|
|
6
|
+
|
|
7
|
+
export const GOVERNANCE_CARRIER_PROJECTION_IDS = Object.freeze([
|
|
8
|
+
'external-activation-state-v1',
|
|
9
|
+
'provider-certification-state-v1',
|
|
10
|
+
'review-capability-certification-state-v1',
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
export const GOVERNANCE_CARRIER_MAX_BYTES = 8 * 1024 * 1024
|
|
14
|
+
const UTF8 = new TextDecoder('utf-8', { fatal: true })
|
|
15
|
+
const ACTIVATION_STATE_KEYS = Object.freeze(['status', 'evidence', 'observedAt', 'expiresAt'])
|
|
16
|
+
const CERTIFICATION_RECORD_KEYS = new Set([
|
|
17
|
+
'id',
|
|
18
|
+
'provider',
|
|
19
|
+
'runtimeKind',
|
|
20
|
+
'providerVersion',
|
|
21
|
+
'adapterVersion',
|
|
22
|
+
'surface',
|
|
23
|
+
'repositoryRole',
|
|
24
|
+
'status',
|
|
25
|
+
'platformMatrix',
|
|
26
|
+
'certifiedAt',
|
|
27
|
+
'expiresAt',
|
|
28
|
+
'runtimeEvidence',
|
|
29
|
+
'checks',
|
|
30
|
+
'limitations',
|
|
31
|
+
])
|
|
32
|
+
const CERTIFICATION_TARGET_KEYS = new Set([
|
|
33
|
+
'id',
|
|
34
|
+
'operatingSystem',
|
|
35
|
+
'platform',
|
|
36
|
+
'arch',
|
|
37
|
+
'executionEnvironment',
|
|
38
|
+
'distributionVersion',
|
|
39
|
+
'pathClass',
|
|
40
|
+
'status',
|
|
41
|
+
'limitations',
|
|
42
|
+
'certifiedAt',
|
|
43
|
+
'expiresAt',
|
|
44
|
+
'runtimeEvidence',
|
|
45
|
+
])
|
|
46
|
+
const REVIEW_CAPABILITY_CERTIFICATION_KEYS = new Set([
|
|
47
|
+
'id',
|
|
48
|
+
'reviewProfileId',
|
|
49
|
+
'reviewProfileDigest',
|
|
50
|
+
'providerId',
|
|
51
|
+
'modelReleaseId',
|
|
52
|
+
'entitlementId',
|
|
53
|
+
'status',
|
|
54
|
+
'assuranceTier',
|
|
55
|
+
'reasoningTier',
|
|
56
|
+
'computeTier',
|
|
57
|
+
'certifiedAt',
|
|
58
|
+
'expiresAt',
|
|
59
|
+
'evidence',
|
|
60
|
+
])
|
|
61
|
+
const TARGET_AXES = Object.freeze([
|
|
62
|
+
'id',
|
|
63
|
+
'operatingSystem',
|
|
64
|
+
'platform',
|
|
65
|
+
'arch',
|
|
66
|
+
'executionEnvironment',
|
|
67
|
+
'distributionVersion',
|
|
68
|
+
'pathClass',
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
function invariant(condition, message) {
|
|
72
|
+
if (!condition) throw new Error(`governance carrier projection blocked:${message}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function plainObject(value, label) {
|
|
76
|
+
invariant(value && typeof value === 'object' && !Array.isArray(value), `${label} must be an object`)
|
|
77
|
+
invariant([Object.prototype, null].includes(Object.getPrototypeOf(value)), `${label} must be a plain object`)
|
|
78
|
+
return value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function exactKeys(value, expected, label) {
|
|
82
|
+
plainObject(value, label)
|
|
83
|
+
const actual = Object.keys(value).sort()
|
|
84
|
+
const wanted = [...expected].sort()
|
|
85
|
+
invariant(actual.length === wanted.length && actual.every((key, index) => key === wanted[index]), `${label} has an invalid or open shape`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function allowedKeys(value, allowed, required, label) {
|
|
89
|
+
plainObject(value, label)
|
|
90
|
+
const keys = Object.keys(value)
|
|
91
|
+
invariant(required.every(key => keys.includes(key)) && keys.every(key => allowed.has(key)), `${label} has an invalid or open shape`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertNoDuplicateJsonObjectKeys(text, label) {
|
|
95
|
+
const stack = []
|
|
96
|
+
let rootConsumed = false
|
|
97
|
+
const expectsValue = () => {
|
|
98
|
+
if (stack.length === 0) return !rootConsumed
|
|
99
|
+
const frame = stack.at(-1)
|
|
100
|
+
return frame.kind === 'array' ? frame.state === 'value-or-end' : frame.state === 'value'
|
|
101
|
+
}
|
|
102
|
+
const consumeValue = () => {
|
|
103
|
+
invariant(expectsValue(), `${label} has an invalid JSON token position`)
|
|
104
|
+
if (stack.length === 0) rootConsumed = true
|
|
105
|
+
else stack.at(-1).state = 'comma-or-end'
|
|
106
|
+
}
|
|
107
|
+
const stringEnd = start => {
|
|
108
|
+
let escaped = false
|
|
109
|
+
for (let index = start + 1; index < text.length; index += 1) {
|
|
110
|
+
const character = text[index]
|
|
111
|
+
if (escaped) {
|
|
112
|
+
escaped = false
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if (character === '\\') {
|
|
116
|
+
escaped = true
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
if (character === '"') return index
|
|
120
|
+
}
|
|
121
|
+
return -1
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (let index = 0; index < text.length;) {
|
|
125
|
+
const character = text[index]
|
|
126
|
+
if (/\s/u.test(character)) {
|
|
127
|
+
index += 1
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
const frame = stack.at(-1)
|
|
131
|
+
if (character === '{' || character === '[') {
|
|
132
|
+
consumeValue()
|
|
133
|
+
stack.push(character === '{'
|
|
134
|
+
? { kind: 'object', state: 'key-or-end', keys: new Set() }
|
|
135
|
+
: { kind: 'array', state: 'value-or-end' })
|
|
136
|
+
index += 1
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
if (character === '}' || character === ']') {
|
|
140
|
+
invariant(frame && ((character === '}') === (frame.kind === 'object')), `${label} has mismatched JSON delimiters`)
|
|
141
|
+
invariant(
|
|
142
|
+
frame.kind === 'object'
|
|
143
|
+
? ['key-or-end', 'comma-or-end'].includes(frame.state)
|
|
144
|
+
: ['value-or-end', 'comma-or-end'].includes(frame.state),
|
|
145
|
+
`${label} has an incomplete JSON container`,
|
|
146
|
+
)
|
|
147
|
+
stack.pop()
|
|
148
|
+
index += 1
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
if (character === ',') {
|
|
152
|
+
invariant(frame?.state === 'comma-or-end', `${label} has an invalid JSON comma`)
|
|
153
|
+
frame.state = frame.kind === 'object' ? 'key-or-end' : 'value-or-end'
|
|
154
|
+
index += 1
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (character === ':') {
|
|
158
|
+
invariant(frame?.kind === 'object' && frame.state === 'colon', `${label} has an invalid JSON colon`)
|
|
159
|
+
frame.state = 'value'
|
|
160
|
+
index += 1
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
if (character === '"') {
|
|
164
|
+
const end = stringEnd(index)
|
|
165
|
+
invariant(end > index, `${label} has an unterminated JSON string`)
|
|
166
|
+
const token = text.slice(index, end + 1)
|
|
167
|
+
if (frame?.kind === 'object' && frame.state === 'key-or-end') {
|
|
168
|
+
const key = JSON.parse(token)
|
|
169
|
+
invariant(!frame.keys.has(key), `${label} contains duplicate object key:${key}`)
|
|
170
|
+
frame.keys.add(key)
|
|
171
|
+
frame.state = 'colon'
|
|
172
|
+
} else {
|
|
173
|
+
consumeValue()
|
|
174
|
+
}
|
|
175
|
+
index = end + 1
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
let end = index + 1
|
|
179
|
+
while (end < text.length && !/[\s,\]}]/u.test(text[end])) end += 1
|
|
180
|
+
consumeValue()
|
|
181
|
+
index = end
|
|
182
|
+
}
|
|
183
|
+
invariant(rootConsumed && stack.length === 0, `${label} has an incomplete JSON document`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseCarrierJson(bytes, label) {
|
|
187
|
+
invariant(Buffer.isBuffer(bytes), `${label} bytes must be a Buffer`)
|
|
188
|
+
invariant(bytes.length > 0 && bytes.length <= GOVERNANCE_CARRIER_MAX_BYTES, `${label} byte length is outside the closed bound`)
|
|
189
|
+
invariant(!(bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf), `${label} must not contain a UTF-8 BOM`)
|
|
190
|
+
let text
|
|
191
|
+
try {
|
|
192
|
+
text = UTF8.decode(bytes)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw new Error(`governance carrier projection blocked:${label} is not canonical UTF-8`, { cause: error })
|
|
195
|
+
}
|
|
196
|
+
assertNoDuplicateJsonObjectKeys(text, label)
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(text)
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new Error(`governance carrier projection blocked:${label} is invalid JSON:${error.message}`, { cause: error })
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function externalActivationProjection(bytes) {
|
|
205
|
+
const ledger = parseCarrierJson(bytes, 'external activation ledger')
|
|
206
|
+
exactKeys(ledger, ['$schema', 'schemaVersion', 'policy', 'requirements'], 'external activation ledger')
|
|
207
|
+
invariant(ledger.$schema === 'schemas/external-activation-requirements.schema.json' && ledger.schemaVersion === 4, 'external activation ledger identity/version is invalid')
|
|
208
|
+
plainObject(ledger.policy, 'external activation policy binding')
|
|
209
|
+
invariant(Array.isArray(ledger.requirements) && ledger.requirements.length > 0, 'external activation requirements must be a non-empty array')
|
|
210
|
+
const ids = new Set()
|
|
211
|
+
const projected = structuredClone(ledger)
|
|
212
|
+
for (const [index, requirement] of projected.requirements.entries()) {
|
|
213
|
+
plainObject(requirement, `external activation requirement[${index}]`)
|
|
214
|
+
invariant(typeof requirement.id === 'string' && requirement.id.length > 0 && !ids.has(requirement.id), `external activation requirement id is invalid or duplicated:${String(requirement.id)}`)
|
|
215
|
+
invariant(ACTIVATION_STATE_KEYS.every(key => Object.hasOwn(requirement, key)), `external activation requirement state is incomplete:${requirement.id}`)
|
|
216
|
+
for (const key of ACTIVATION_STATE_KEYS) delete requirement[key]
|
|
217
|
+
ids.add(requirement.id)
|
|
218
|
+
}
|
|
219
|
+
return Buffer.from(stableJson(projected))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function projectedCertificationTarget(target, recordId, targetIds) {
|
|
223
|
+
allowedKeys(
|
|
224
|
+
target,
|
|
225
|
+
CERTIFICATION_TARGET_KEYS,
|
|
226
|
+
[...TARGET_AXES, 'status', 'limitations'],
|
|
227
|
+
`certification ${recordId} target`,
|
|
228
|
+
)
|
|
229
|
+
invariant(typeof target.id === 'string' && target.id.length > 0 && !targetIds.has(target.id), `certification ${recordId} target id is invalid or duplicated:${String(target.id)}`)
|
|
230
|
+
invariant(['certified', 'not-certified', 'unsupported'].includes(target.status), `certification ${recordId}/${target.id} status is invalid`)
|
|
231
|
+
invariant(Array.isArray(target.limitations), `certification ${recordId}/${target.id} limitations must be an array`)
|
|
232
|
+
const projection = Object.fromEntries(TARGET_AXES.map(axis => {
|
|
233
|
+
invariant(typeof target[axis] === 'string' && target[axis].length > 0, `certification ${recordId}/${target.id} ${axis} is invalid`)
|
|
234
|
+
return [axis, target[axis]]
|
|
235
|
+
}))
|
|
236
|
+
if (target.status === 'unsupported') {
|
|
237
|
+
invariant(target.limitations.length > 0 && target.limitations.every(item => typeof item === 'string' && item.length > 0), `certification ${recordId}/${target.id} unsupported limitations are invalid`)
|
|
238
|
+
projection.certificationClass = 'unsupported'
|
|
239
|
+
projection.limitations = structuredClone(target.limitations)
|
|
240
|
+
} else {
|
|
241
|
+
projection.certificationClass = 'certifiable'
|
|
242
|
+
}
|
|
243
|
+
targetIds.add(target.id)
|
|
244
|
+
return projection
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function providerCertificationProjection(bytes) {
|
|
248
|
+
const ledger = parseCarrierJson(bytes, 'provider certification ledger')
|
|
249
|
+
exactKeys(ledger, ['$schema', 'schemaVersion', 'certifications'], 'provider certification ledger')
|
|
250
|
+
invariant(ledger.$schema === '../schemas/provider-surface-certification.schema.json' && ledger.schemaVersion === 2, 'provider certification ledger identity/version is invalid')
|
|
251
|
+
invariant(Array.isArray(ledger.certifications), 'provider certifications must be an array')
|
|
252
|
+
const ids = new Set()
|
|
253
|
+
const projected = {
|
|
254
|
+
$schema: ledger.$schema,
|
|
255
|
+
schemaVersion: ledger.schemaVersion,
|
|
256
|
+
certifications: ledger.certifications.map((certification, index) => {
|
|
257
|
+
allowedKeys(
|
|
258
|
+
certification,
|
|
259
|
+
CERTIFICATION_RECORD_KEYS,
|
|
260
|
+
[
|
|
261
|
+
'id',
|
|
262
|
+
'provider',
|
|
263
|
+
'runtimeKind',
|
|
264
|
+
'providerVersion',
|
|
265
|
+
'adapterVersion',
|
|
266
|
+
'surface',
|
|
267
|
+
'repositoryRole',
|
|
268
|
+
'status',
|
|
269
|
+
'platformMatrix',
|
|
270
|
+
'checks',
|
|
271
|
+
'limitations',
|
|
272
|
+
],
|
|
273
|
+
`certification[${index}]`,
|
|
274
|
+
)
|
|
275
|
+
invariant(typeof certification.id === 'string' && certification.id.length > 0 && !ids.has(certification.id), `certification id is invalid or duplicated:${String(certification.id)}`)
|
|
276
|
+
for (const key of ['provider', 'runtimeKind', 'providerVersion', 'adapterVersion', 'surface', 'repositoryRole']) {
|
|
277
|
+
invariant(typeof certification[key] === 'string' && certification[key].length > 0, `certification ${certification.id} ${key} is invalid`)
|
|
278
|
+
}
|
|
279
|
+
invariant(Array.isArray(certification.platformMatrix) && certification.platformMatrix.length > 0, `certification ${certification.id} platformMatrix is empty`)
|
|
280
|
+
const record = {
|
|
281
|
+
id: certification.id,
|
|
282
|
+
provider: certification.provider,
|
|
283
|
+
runtimeKind: certification.runtimeKind,
|
|
284
|
+
providerVersion: certification.providerVersion,
|
|
285
|
+
adapterVersion: certification.adapterVersion,
|
|
286
|
+
surface: certification.surface,
|
|
287
|
+
repositoryRole: certification.repositoryRole,
|
|
288
|
+
platformMatrix: [],
|
|
289
|
+
}
|
|
290
|
+
if (Object.hasOwn(certification, 'certifiedAt')) record.certifiedAt = certification.certifiedAt
|
|
291
|
+
if (Object.hasOwn(certification, 'expiresAt')) record.expiresAt = certification.expiresAt
|
|
292
|
+
const targetIds = new Set()
|
|
293
|
+
record.platformMatrix = certification.platformMatrix.map(target => projectedCertificationTarget(target, certification.id, targetIds))
|
|
294
|
+
ids.add(certification.id)
|
|
295
|
+
return record
|
|
296
|
+
}),
|
|
297
|
+
}
|
|
298
|
+
return Buffer.from(stableJson(projected))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function reviewCapabilityCertificationProjection(bytes) {
|
|
302
|
+
const ledger = parseCarrierJson(bytes, 'review capability certification ledger')
|
|
303
|
+
exactKeys(
|
|
304
|
+
ledger,
|
|
305
|
+
['$schema', 'schemaVersion', 'kind', 'certifications'],
|
|
306
|
+
'review capability certification ledger',
|
|
307
|
+
)
|
|
308
|
+
invariant(
|
|
309
|
+
ledger.$schema === '../schemas/review-capability-certifications.schema.json'
|
|
310
|
+
&& ledger.schemaVersion === 1
|
|
311
|
+
&& ledger.kind === 'review-capability-certification-ledger',
|
|
312
|
+
'review capability certification ledger identity/version is invalid',
|
|
313
|
+
)
|
|
314
|
+
invariant(Array.isArray(ledger.certifications),
|
|
315
|
+
'review capability certifications must be an array')
|
|
316
|
+
const ids = new Set()
|
|
317
|
+
for (const [index, certification] of ledger.certifications.entries()) {
|
|
318
|
+
allowedKeys(
|
|
319
|
+
certification,
|
|
320
|
+
REVIEW_CAPABILITY_CERTIFICATION_KEYS,
|
|
321
|
+
[...REVIEW_CAPABILITY_CERTIFICATION_KEYS],
|
|
322
|
+
`review capability certification[${index}]`,
|
|
323
|
+
)
|
|
324
|
+
invariant(
|
|
325
|
+
typeof certification.id === 'string'
|
|
326
|
+
&& certification.id.length > 0
|
|
327
|
+
&& !ids.has(certification.id),
|
|
328
|
+
`review capability certification id is invalid or duplicated:${String(certification.id)}`,
|
|
329
|
+
)
|
|
330
|
+
ids.add(certification.id)
|
|
331
|
+
}
|
|
332
|
+
return Buffer.from(stableJson({
|
|
333
|
+
$schema: ledger.$schema,
|
|
334
|
+
schemaVersion: ledger.schemaVersion,
|
|
335
|
+
kind: ledger.kind,
|
|
336
|
+
certifications: [],
|
|
337
|
+
}))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const PROJECTIONS = Object.freeze({
|
|
341
|
+
'external-activation-state-v1': Object.freeze({
|
|
342
|
+
path: EXTERNAL_ACTIVATION_CARRIER_PATH,
|
|
343
|
+
project: externalActivationProjection,
|
|
344
|
+
}),
|
|
345
|
+
'provider-certification-state-v1': Object.freeze({
|
|
346
|
+
path: PROVIDER_CERTIFICATION_CARRIER_PATH,
|
|
347
|
+
project: providerCertificationProjection,
|
|
348
|
+
}),
|
|
349
|
+
'review-capability-certification-state-v1': Object.freeze({
|
|
350
|
+
path: REVIEW_CAPABILITY_CERTIFICATION_CARRIER_PATH,
|
|
351
|
+
project: reviewCapabilityCertificationProjection,
|
|
352
|
+
}),
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
export function projectGovernanceCarrierBytes({ path, projectionId, bytes } = {}) {
|
|
356
|
+
const definition = PROJECTIONS[projectionId]
|
|
357
|
+
invariant(definition, `unknown projection id:${String(projectionId)}`)
|
|
358
|
+
invariant(path === definition.path, `projection ${projectionId} is not registered for path:${String(path)}`)
|
|
359
|
+
return definition.project(bytes)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function governanceCarrierProjectionMap(sources) {
|
|
363
|
+
invariant(Array.isArray(sources), 'manifest sources must be an array')
|
|
364
|
+
const bindings = new Map()
|
|
365
|
+
for (const source of sources) {
|
|
366
|
+
if (source?.carrierProjection === undefined) continue
|
|
367
|
+
const definition = PROJECTIONS[source.carrierProjection]
|
|
368
|
+
invariant(definition, `manifest source ${String(source?.id)} uses an unknown carrier projection:${String(source?.carrierProjection)}`)
|
|
369
|
+
invariant(source.path === definition.path, `manifest source ${String(source?.id)} projection/path pair is not registered`)
|
|
370
|
+
invariant(!Array.isArray(source.excludes) || source.excludes.length === 0, `projected manifest source ${String(source?.id)} cannot declare directory exclusions`)
|
|
371
|
+
invariant(!bindings.has(source.path), `manifest carrier projection path is duplicated:${source.path}`)
|
|
372
|
+
bindings.set(source.path, Object.freeze({ path: source.path, projection: source.carrierProjection }))
|
|
373
|
+
}
|
|
374
|
+
return bindings
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function assertCanonicalGovernanceCarrierProjectionMap(bindings) {
|
|
378
|
+
invariant(bindings instanceof Map, 'carrier projection bindings must be a Map')
|
|
379
|
+
const expected = Object.values(PROJECTIONS)
|
|
380
|
+
.map(definition => definition.path)
|
|
381
|
+
.sort()
|
|
382
|
+
const actual = [...bindings.keys()].sort()
|
|
383
|
+
invariant(actual.length === expected.length && actual.every((path, index) => path === expected[index]), 'canonical carrier projection closure is incomplete or contains extra paths')
|
|
384
|
+
return true
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function excludedBySource(sourcePath, excludes, path) {
|
|
388
|
+
for (const raw of excludes ?? []) {
|
|
389
|
+
const exclusion = raw.replace(/\/+$/, '')
|
|
390
|
+
invariant(exclusion.startsWith(`${sourcePath}/`), `source exclusion escapes projected source:${raw}`)
|
|
391
|
+
if (path === exclusion || path.startsWith(`${exclusion}/`)) return true
|
|
392
|
+
}
|
|
393
|
+
return false
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function governanceCarrierProjectionsForSource(source, bindings) {
|
|
397
|
+
invariant(bindings instanceof Map, 'carrier projection bindings must be a Map')
|
|
398
|
+
const sourcePath = String(source?.path ?? '').replace(/\/+$/, '')
|
|
399
|
+
invariant(sourcePath.length > 0, 'source path is empty')
|
|
400
|
+
return [...bindings.values()]
|
|
401
|
+
.filter(binding => (
|
|
402
|
+
(binding.path === sourcePath || binding.path.startsWith(`${sourcePath}/`))
|
|
403
|
+
&& !excludedBySource(sourcePath, source?.excludes, binding.path)
|
|
404
|
+
))
|
|
405
|
+
.sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)))
|
|
406
|
+
.map(binding => ({ path: binding.path, projection: binding.projection }))
|
|
407
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import {
|
|
3
|
+
diagnostic,
|
|
4
|
+
humanResult,
|
|
5
|
+
readStdin,
|
|
6
|
+
result,
|
|
7
|
+
stableJson,
|
|
8
|
+
} from './common.mjs'
|
|
9
|
+
import { evaluateHook } from './hook-api.mjs'
|
|
10
|
+
import {
|
|
11
|
+
applyUpgrade,
|
|
12
|
+
attestRepository,
|
|
13
|
+
checkRepository,
|
|
14
|
+
doctorRepository,
|
|
15
|
+
generateRepository,
|
|
16
|
+
planUpgrade,
|
|
17
|
+
} from './snapshot.mjs'
|
|
18
|
+
import { assertArtifactSchema, inspectContract } from './contract.mjs'
|
|
19
|
+
|
|
20
|
+
const commands = new Set(['generate', 'check', 'doctor', 'hook', 'attest', 'upgrade'])
|
|
21
|
+
|
|
22
|
+
function usage(message = 'A command is required.') {
|
|
23
|
+
return result('usage', [diagnostic({
|
|
24
|
+
ruleId: 'GOV-CONTRACT-001',
|
|
25
|
+
severity: 'critical',
|
|
26
|
+
kind: 'usage',
|
|
27
|
+
outcome: 'ERROR',
|
|
28
|
+
message: `${message} Commands: generate, check, doctor, hook, attest, upgrade plan, upgrade apply.`,
|
|
29
|
+
})])
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parse(argv, fixedCommand) {
|
|
33
|
+
const args = [...argv]
|
|
34
|
+
const command = fixedCommand || args.shift()
|
|
35
|
+
if (!commands.has(command)) return { error: usage(command ? `Unknown command: ${command}.` : undefined) }
|
|
36
|
+
let upgradeAction
|
|
37
|
+
if (command === 'upgrade') upgradeAction = args.shift()
|
|
38
|
+
const options = {}
|
|
39
|
+
const booleanOptions = new Set(['json', 'hooks-off', 'help'])
|
|
40
|
+
const keyMap = new Map([
|
|
41
|
+
['repo', 'repoRoot'],
|
|
42
|
+
['manifest', 'manifestPath'],
|
|
43
|
+
['role', 'role'],
|
|
44
|
+
['output', 'outputDirectory'],
|
|
45
|
+
['lock', 'lockFile'],
|
|
46
|
+
['provider', 'provider'],
|
|
47
|
+
['plan-file', 'planFile'],
|
|
48
|
+
['attestation-file', 'attestationFile'],
|
|
49
|
+
['at', 'issuedAt'],
|
|
50
|
+
['git-head', 'gitHead'],
|
|
51
|
+
])
|
|
52
|
+
while (args.length) {
|
|
53
|
+
const token = args.shift()
|
|
54
|
+
if (!token.startsWith('--')) return { error: usage(`Unexpected argument: ${token}.`) }
|
|
55
|
+
const rawKey = token.slice(2)
|
|
56
|
+
if (booleanOptions.has(rawKey)) {
|
|
57
|
+
options[rawKey === 'hooks-off' ? 'hooksOff' : rawKey] = true
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
const key = keyMap.get(rawKey)
|
|
61
|
+
if (!key) return { error: usage(`Unknown option: ${token}.`) }
|
|
62
|
+
const value = args.shift()
|
|
63
|
+
if (value == null || value.startsWith('--')) return { error: usage(`${token} requires a value.`) }
|
|
64
|
+
options[key] = value
|
|
65
|
+
}
|
|
66
|
+
if (options.repoRoot) options.repoRoot = resolve(options.repoRoot)
|
|
67
|
+
if (options.manifestPath) options.manifestPath = resolve(options.manifestPath)
|
|
68
|
+
return { command, upgradeAction, options }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function hookCommand(options) {
|
|
72
|
+
let input
|
|
73
|
+
try {
|
|
74
|
+
const raw = await readStdin()
|
|
75
|
+
input = raw.trim() ? JSON.parse(raw) : {}
|
|
76
|
+
} catch {
|
|
77
|
+
input = null
|
|
78
|
+
}
|
|
79
|
+
const evaluated = await evaluateHook({ ...options, input })
|
|
80
|
+
return { value: evaluated.result, exitCode: evaluated.exitCode }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function execute(parsed) {
|
|
84
|
+
const { command, upgradeAction, options } = parsed
|
|
85
|
+
if (options.help) return { value: usage(), exitCode: 1 }
|
|
86
|
+
if (command === 'generate') return { value: await generateRepository(options) }
|
|
87
|
+
if (command === 'check') return { value: await checkRepository(options) }
|
|
88
|
+
if (command === 'doctor') return { value: await doctorRepository(options) }
|
|
89
|
+
if (command === 'hook') {
|
|
90
|
+
if (!options.provider) return { value: usage('hook requires --provider.'), exitCode: 2 }
|
|
91
|
+
return hookCommand(options)
|
|
92
|
+
}
|
|
93
|
+
if (command === 'attest') return { value: await attestRepository(options) }
|
|
94
|
+
if (command === 'upgrade') {
|
|
95
|
+
if (upgradeAction === 'plan') return { value: await planUpgrade(options) }
|
|
96
|
+
if (upgradeAction === 'apply') return { value: await applyUpgrade(options) }
|
|
97
|
+
return { value: usage('upgrade requires plan or apply.') }
|
|
98
|
+
}
|
|
99
|
+
return { value: usage() }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function runCli({ argv = process.argv.slice(2), fixedCommand } = {}) {
|
|
103
|
+
const parsed = parse(argv, fixedCommand)
|
|
104
|
+
const executed = parsed.error ? { value: parsed.error } : await execute(parsed)
|
|
105
|
+
const value = executed.value
|
|
106
|
+
if (!parsed.error) {
|
|
107
|
+
const inspected = await inspectContract(parsed.options)
|
|
108
|
+
if (inspected.contract) await assertArtifactSchema(inspected.contract, 'diagnostic', value)
|
|
109
|
+
}
|
|
110
|
+
process.stdout.write(parsed.options?.json ? stableJson(value) : `${humanResult(value)}\n`)
|
|
111
|
+
const successful = ['PASS', 'PLAN', 'APPLIED'].includes(value.outcome)
|
|
112
|
+
process.exitCode = executed.exitCode ?? (successful ? 0 : 1)
|
|
113
|
+
return value
|
|
114
|
+
}
|