archctx-contracts 0.1.5
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/fixtures/boundary/agent-job-expired.json +33 -0
- package/fixtures/boundary/architecture-event-extension.json +41 -0
- package/fixtures/boundary/architecture-node-extension.json +13 -0
- package/fixtures/boundary/attestation-v2-error.json +43 -0
- package/fixtures/boundary/check-delivery-dead-letter.json +14 -0
- package/fixtures/boundary/evidence-binding-context-only.json +20 -0
- package/fixtures/boundary/practice-disabled-overlay.json +59 -0
- package/fixtures/boundary/recommendation-deferred.json +19 -0
- package/fixtures/boundary/review-challenge-v2-organization.json +15 -0
- package/fixtures/boundary/runner-identity-organization-scope.json +16 -0
- package/fixtures/invalid/adapter-fidelity-reverse-sync.json +9 -0
- package/fixtures/invalid/agent-job-direct-mutation.json +32 -0
- package/fixtures/invalid/architecture-event-source-body-field.json +39 -0
- package/fixtures/invalid/architecture-snapshot-unknown-mode.json +29 -0
- package/fixtures/invalid/attestation-v2-finding-field.json +39 -0
- package/fixtures/invalid/check-delivery-legacy-check-name.json +14 -0
- package/fixtures/invalid/cloud-egress-envelope-private-field.json +12 -0
- package/fixtures/invalid/compatibility-illegal-reason.json +17 -0
- package/fixtures/invalid/evidence-binding-summary-substring-authority.json +17 -0
- package/fixtures/invalid/evidence-item-source-body-field.json +27 -0
- package/fixtures/invalid/explorer-projection-write-field.json +21 -0
- package/fixtures/invalid/investigation-report-direct-mutation.json +38 -0
- package/fixtures/invalid/investigation-report-missing-proposed-delta.json +19 -0
- package/fixtures/invalid/node-unknown-field.json +10 -0
- package/fixtures/invalid/notification-event-code-field.json +11 -0
- package/fixtures/invalid/practice-policy-empty-test-evidence.json +15 -0
- package/fixtures/invalid/practice-policy-negative-recommendation-budget.json +10 -0
- package/fixtures/invalid/practice-unknown-field.json +56 -0
- package/fixtures/invalid/projection-target-invalid-ownership.json +18 -0
- package/fixtures/invalid/recommendation-feedback-implicit-acceptance.json +29 -0
- package/fixtures/invalid/recommendation-run-raw-input-field.json +33 -0
- package/fixtures/invalid/recommendation-source-body-field.json +17 -0
- package/fixtures/invalid/review-challenge-v2-private-field.json +16 -0
- package/fixtures/valid/adapter-fidelity.json +9 -0
- package/fixtures/valid/agent-job.json +33 -0
- package/fixtures/valid/architecture-candidate-delta-policy.json +45 -0
- package/fixtures/valid/architecture-candidate-delta.json +188 -0
- package/fixtures/valid/architecture-event.json +39 -0
- package/fixtures/valid/architecture-intervention.json +36 -0
- package/fixtures/valid/architecture-node.json +25 -0
- package/fixtures/valid/architecture-relation.json +14 -0
- package/fixtures/valid/architecture-snapshot.json +31 -0
- package/fixtures/valid/architecture-subject-selector.json +9 -0
- package/fixtures/valid/attestation-v2.json +38 -0
- package/fixtures/valid/attestation.json +25 -0
- package/fixtures/valid/changeset.json +30 -0
- package/fixtures/valid/chatgpt-ga-tool.json +9 -0
- package/fixtures/valid/check-delivery.json +14 -0
- package/fixtures/valid/cloud-egress-envelope.json +11 -0
- package/fixtures/valid/compatibility-contract.json +19 -0
- package/fixtures/valid/constraint.json +22 -0
- package/fixtures/valid/cross-repo-relation.json +23 -0
- package/fixtures/valid/device-identity.json +10 -0
- package/fixtures/valid/entitlement.json +12 -0
- package/fixtures/valid/evidence-binding.json +17 -0
- package/fixtures/valid/evidence-item.json +29 -0
- package/fixtures/valid/explorer-projection.json +53 -0
- package/fixtures/valid/explorer-service.json +11 -0
- package/fixtures/valid/external-document-resource.json +29 -0
- package/fixtures/valid/governance-key-status.json +11 -0
- package/fixtures/valid/investigation-report.json +38 -0
- package/fixtures/valid/landscape.json +36 -0
- package/fixtures/valid/likec4-mapping.json +14 -0
- package/fixtures/valid/notification-event.json +10 -0
- package/fixtures/valid/notification-provider.json +12 -0
- package/fixtures/valid/org-runner-identity.json +10 -0
- package/fixtures/valid/practice-catalog-manifest.json +16 -0
- package/fixtures/valid/practice-check-result.json +17 -0
- package/fixtures/valid/practice-checkpoint.json +94 -0
- package/fixtures/valid/practice-guidance.json +42 -0
- package/fixtures/valid/practice-match.json +23 -0
- package/fixtures/valid/practice-policy.json +33 -0
- package/fixtures/valid/practice-profile.json +22 -0
- package/fixtures/valid/practice-source.json +19 -0
- package/fixtures/valid/practice-waiver.json +14 -0
- package/fixtures/valid/practice.json +93 -0
- package/fixtures/valid/product-version-manifest.json +51 -0
- package/fixtures/valid/projection-target.json +18 -0
- package/fixtures/valid/recommendation-feedback.json +29 -0
- package/fixtures/valid/recommendation-run.json +33 -0
- package/fixtures/valid/recommendation.json +17 -0
- package/fixtures/valid/retrieval-config.json +20 -0
- package/fixtures/valid/retrieval-decision.json +26 -0
- package/fixtures/valid/retrieval-eval.json +14 -0
- package/fixtures/valid/review-challenge-v2.json +15 -0
- package/fixtures/valid/review-result.json +32 -0
- package/fixtures/valid/runner-identity.json +13 -0
- package/fixtures/valid/structurizr-mapping.json +15 -0
- package/fixtures/valid/task-context.json +23 -0
- package/package.json +17 -0
- package/src/control-plane-routes.ts +35 -0
- package/src/external-docs.ts +137 -0
- package/src/github-governance.ts +850 -0
- package/src/index.ts +9 -0
- package/src/ledger.ts +649 -0
- package/src/ports.ts +385 -0
- package/src/practices.ts +433 -0
- package/src/product-version.ts +108 -0
- package/src/schema.ts +145 -0
- package/src/validator.ts +95 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import { canonicalize, digestJson, type Json } from "./schema";
|
|
2
|
+
|
|
3
|
+
export const DEVELOPER_REVIEW_CHECK_NAME = "ArchContext / Developer Review" as const;
|
|
4
|
+
export const ORGANIZATION_RUNNER_CHECK_NAME = "ArchContext / Organization Runner" as const;
|
|
5
|
+
|
|
6
|
+
export const GOVERNANCE_CHECK_NAMES = [
|
|
7
|
+
DEVELOPER_REVIEW_CHECK_NAME,
|
|
8
|
+
ORGANIZATION_RUNNER_CHECK_NAME
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export type GovernanceCheckName = (typeof GOVERNANCE_CHECK_NAMES)[number];
|
|
12
|
+
export type GovernanceTrustLevel = "developer" | "organization";
|
|
13
|
+
export type RequiredTrust = GovernanceTrustLevel;
|
|
14
|
+
export type GovernanceFeatureFlagName = "developerCheck" | "organizationCheck" | "requiredTrust";
|
|
15
|
+
export type GovernanceFeatureFlagDecisionReason =
|
|
16
|
+
| "enabled"
|
|
17
|
+
| "developer-check-disabled"
|
|
18
|
+
| "organization-check-disabled"
|
|
19
|
+
| "required-trust-disabled";
|
|
20
|
+
export type ReviewChallengeStatus = "PENDING" | "LEASED" | "SUBMITTED" | "VERIFIED" | "REJECTED" | "SUPERSEDED" | "EXPIRED";
|
|
21
|
+
export type CheckDeliveryStatus = "PENDING" | "PUBLISHED" | "RETRYING" | "DEAD_LETTER";
|
|
22
|
+
export type AttestationResult = "pass" | "fail" | "error";
|
|
23
|
+
export type GitHubAppPermissionLevel = "none" | "read" | "write";
|
|
24
|
+
export type CallerProvidedAttestationField = (typeof CALLER_PROVIDED_ATTESTATION_FIELDS)[number];
|
|
25
|
+
export type LlmAdvisoryForbiddenField = (typeof LLM_ADVISORY_FORBIDDEN_FIELDS)[number];
|
|
26
|
+
export type RunnerIdentityStatus = "active" | "rotating" | "revoked";
|
|
27
|
+
export type RunnerIdentityScope =
|
|
28
|
+
| { kind: "repository"; repositoryIds: number[] }
|
|
29
|
+
| { kind: "organization" };
|
|
30
|
+
export type CreateAttestationV2Input = Omit<AttestationV2, "schemaVersion" | "attestationId" | "signature"> & {
|
|
31
|
+
attestationId?: string;
|
|
32
|
+
signature?: AttestationV2["signature"];
|
|
33
|
+
};
|
|
34
|
+
export type CreateRunnerIdentityInput = Omit<RunnerIdentity, "schemaVersion" | "repositoryIds" | "scope" | "rotatedAt" | "revokedAt"> & {
|
|
35
|
+
repositoryIds?: number[];
|
|
36
|
+
scope?: RunnerIdentityScope;
|
|
37
|
+
rotatedAt?: string | null;
|
|
38
|
+
revokedAt?: string | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface GovernanceFeatureFlags {
|
|
42
|
+
schemaVersion: "archcontext.governance-feature-flags/v1";
|
|
43
|
+
developerCheck: boolean;
|
|
44
|
+
organizationCheck: boolean;
|
|
45
|
+
requiredTrust: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface GovernanceFeatureFlagDecision {
|
|
49
|
+
schemaVersion: "archcontext.governance-feature-flag-decision/v1";
|
|
50
|
+
allowed: boolean;
|
|
51
|
+
reason: GovernanceFeatureFlagDecisionReason;
|
|
52
|
+
requiredTrust: RequiredTrust;
|
|
53
|
+
checkName: GovernanceCheckName;
|
|
54
|
+
flags: GovernanceFeatureFlags;
|
|
55
|
+
disabledFlag?: GovernanceFeatureFlagName;
|
|
56
|
+
metadataDigest: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_GOVERNANCE_FEATURE_FLAGS: GovernanceFeatureFlags = {
|
|
60
|
+
schemaVersion: "archcontext.governance-feature-flags/v1",
|
|
61
|
+
developerCheck: true,
|
|
62
|
+
organizationCheck: true,
|
|
63
|
+
requiredTrust: true
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
export interface DevicePrivateKeySignerPort {
|
|
67
|
+
signWithDevicePrivateKey(input: { keyRef: string; payload: string | Uint8Array }): string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const CALLER_PROVIDED_ATTESTATION_FIELDS = [
|
|
71
|
+
"result",
|
|
72
|
+
"reviewDigest",
|
|
73
|
+
"policyDigest",
|
|
74
|
+
"modelDigest",
|
|
75
|
+
"signature",
|
|
76
|
+
"practiceEnforcement",
|
|
77
|
+
"practiceViolations",
|
|
78
|
+
"waiversApplied",
|
|
79
|
+
"actionsRequired",
|
|
80
|
+
"practiceCatalogDigest",
|
|
81
|
+
"practicePolicyDigest",
|
|
82
|
+
"practiceCheckResultDigest"
|
|
83
|
+
] as const;
|
|
84
|
+
export const LLM_ADVISORY_FORBIDDEN_FIELDS = [
|
|
85
|
+
...CALLER_PROVIDED_ATTESTATION_FIELDS,
|
|
86
|
+
"conclusion",
|
|
87
|
+
"checkConclusion",
|
|
88
|
+
"attestationResult"
|
|
89
|
+
] as const;
|
|
90
|
+
|
|
91
|
+
export const GITHUB_APP_PERMISSION_MANIFEST = {
|
|
92
|
+
schemaVersion: "archcontext.github-app-permission-manifest/v1",
|
|
93
|
+
repositoryPermissions: {
|
|
94
|
+
metadata: "read",
|
|
95
|
+
pull_requests: "read",
|
|
96
|
+
checks: "write",
|
|
97
|
+
statuses: "write",
|
|
98
|
+
contents: "none"
|
|
99
|
+
},
|
|
100
|
+
forbiddenByDefault: [
|
|
101
|
+
"actions",
|
|
102
|
+
"administration",
|
|
103
|
+
"deployments",
|
|
104
|
+
"issues",
|
|
105
|
+
"members",
|
|
106
|
+
"secrets",
|
|
107
|
+
"workflows"
|
|
108
|
+
],
|
|
109
|
+
conditionalPermissions: {
|
|
110
|
+
commit_statuses: {
|
|
111
|
+
default: "none",
|
|
112
|
+
implemented: "write",
|
|
113
|
+
decisionGate: "FG2-02 / FG2-EG6",
|
|
114
|
+
reason: "GitHub ruleset expected-source App binding requires statuses:write; runtime still publishes Checks, not commit statuses."
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
subscribedEvents: [
|
|
118
|
+
"installation",
|
|
119
|
+
"installation_repositories",
|
|
120
|
+
"pull_request.opened",
|
|
121
|
+
"pull_request.reopened",
|
|
122
|
+
"pull_request.synchronize",
|
|
123
|
+
"pull_request.closed",
|
|
124
|
+
"check_run.rerequested"
|
|
125
|
+
]
|
|
126
|
+
} as const;
|
|
127
|
+
|
|
128
|
+
export const CHALLENGE_STATUS_TRANSITIONS: Record<ReviewChallengeStatus, readonly ReviewChallengeStatus[]> = {
|
|
129
|
+
PENDING: ["LEASED", "SUPERSEDED", "EXPIRED"],
|
|
130
|
+
LEASED: ["SUBMITTED", "SUPERSEDED", "EXPIRED"],
|
|
131
|
+
SUBMITTED: ["VERIFIED", "REJECTED", "SUPERSEDED", "EXPIRED"],
|
|
132
|
+
VERIFIED: [],
|
|
133
|
+
REJECTED: [],
|
|
134
|
+
SUPERSEDED: [],
|
|
135
|
+
EXPIRED: []
|
|
136
|
+
} as const;
|
|
137
|
+
|
|
138
|
+
export const CHECK_DELIVERY_STATUS_TRANSITIONS: Record<CheckDeliveryStatus, readonly CheckDeliveryStatus[]> = {
|
|
139
|
+
PENDING: ["PUBLISHED", "RETRYING", "DEAD_LETTER"],
|
|
140
|
+
RETRYING: ["PUBLISHED", "RETRYING", "DEAD_LETTER"],
|
|
141
|
+
PUBLISHED: [],
|
|
142
|
+
DEAD_LETTER: ["PENDING"]
|
|
143
|
+
} as const;
|
|
144
|
+
|
|
145
|
+
export const RUNNER_IDENTITY_STATUS_TRANSITIONS: Record<RunnerIdentityStatus, readonly RunnerIdentityStatus[]> = {
|
|
146
|
+
active: ["rotating", "revoked"],
|
|
147
|
+
rotating: ["active", "revoked"],
|
|
148
|
+
revoked: []
|
|
149
|
+
} as const;
|
|
150
|
+
|
|
151
|
+
export function findCallerProvidedAttestationFields(value: unknown): CallerProvidedAttestationField[] {
|
|
152
|
+
const found = new Set<CallerProvidedAttestationField>();
|
|
153
|
+
collectCallerProvidedAttestationFields(value, found);
|
|
154
|
+
return CALLER_PROVIDED_ATTESTATION_FIELDS.filter((field) => found.has(field));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function assertNoCallerProvidedAttestationFields(value: unknown, boundary = "review request"): void {
|
|
158
|
+
const fields = findCallerProvidedAttestationFields(value);
|
|
159
|
+
if (fields.length > 0) throw new Error(`${boundary}-caller-provided-attestation-field-forbidden: ${fields.join(",")}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function findLlmAdvisoryForbiddenFields(value: unknown): LlmAdvisoryForbiddenField[] {
|
|
163
|
+
const found = new Set<LlmAdvisoryForbiddenField>();
|
|
164
|
+
collectForbiddenFields(value, found, LLM_ADVISORY_FORBIDDEN_FIELDS);
|
|
165
|
+
return LLM_ADVISORY_FORBIDDEN_FIELDS.filter((field) => found.has(field));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function assertNoLlmAdvisoryConclusionFields(value: unknown): void {
|
|
169
|
+
const fields = findLlmAdvisoryForbiddenFields(value);
|
|
170
|
+
if (fields.length > 0) throw new Error(`llm-advisory-conclusion-field-forbidden: ${fields.join(",")}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function createAttestationV2(input: CreateAttestationV2Input): AttestationV2 {
|
|
174
|
+
return {
|
|
175
|
+
schemaVersion: "archcontext.attestation/v2",
|
|
176
|
+
attestationId: input.attestationId ?? attestationV2Id(input),
|
|
177
|
+
challengeId: input.challengeId,
|
|
178
|
+
installationId: input.installationId,
|
|
179
|
+
repositoryId: input.repositoryId,
|
|
180
|
+
pullRequestNumber: input.pullRequestNumber,
|
|
181
|
+
headSha: input.headSha,
|
|
182
|
+
baseSha: input.baseSha,
|
|
183
|
+
mergeBaseSha: input.mergeBaseSha,
|
|
184
|
+
headTreeOid: input.headTreeOid,
|
|
185
|
+
worktreeDigest: input.worktreeDigest,
|
|
186
|
+
modelDigest: input.modelDigest,
|
|
187
|
+
policyDigest: input.policyDigest,
|
|
188
|
+
codeFactsDigest: input.codeFactsDigest,
|
|
189
|
+
reviewDigest: input.reviewDigest,
|
|
190
|
+
result: input.result,
|
|
191
|
+
...(input.errorCode === undefined ? {} : { errorCode: input.errorCode }),
|
|
192
|
+
execution: input.execution,
|
|
193
|
+
runtime: input.runtime,
|
|
194
|
+
nonce: input.nonce,
|
|
195
|
+
startedAt: input.startedAt,
|
|
196
|
+
completedAt: input.completedAt,
|
|
197
|
+
expiresAt: input.expiresAt,
|
|
198
|
+
signature: input.signature ?? { algorithm: "ed25519", value: "" }
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function unsignedAttestationV2(attestation: AttestationV2): AttestationV2 {
|
|
203
|
+
return {
|
|
204
|
+
...attestation,
|
|
205
|
+
signature: {
|
|
206
|
+
...attestation.signature,
|
|
207
|
+
value: ""
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function canonicalAttestationV2(attestation: AttestationV2): string {
|
|
213
|
+
return canonicalize(unsignedAttestationV2(attestation) as unknown as Json);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function attestationV2Digest(attestation: AttestationV2): string {
|
|
217
|
+
return digestJson(unsignedAttestationV2(attestation) as unknown as Json);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function attestationV2Id(input: Omit<CreateAttestationV2Input, "attestationId">): string {
|
|
221
|
+
const digest = digestJson({
|
|
222
|
+
schemaVersion: "archcontext.attestation/v2",
|
|
223
|
+
challengeId: input.challengeId,
|
|
224
|
+
installationId: input.installationId,
|
|
225
|
+
repositoryId: input.repositoryId,
|
|
226
|
+
pullRequestNumber: input.pullRequestNumber,
|
|
227
|
+
headSha: input.headSha,
|
|
228
|
+
baseSha: input.baseSha,
|
|
229
|
+
mergeBaseSha: input.mergeBaseSha,
|
|
230
|
+
headTreeOid: input.headTreeOid,
|
|
231
|
+
worktreeDigest: input.worktreeDigest,
|
|
232
|
+
modelDigest: input.modelDigest,
|
|
233
|
+
policyDigest: input.policyDigest,
|
|
234
|
+
codeFactsDigest: input.codeFactsDigest,
|
|
235
|
+
reviewDigest: input.reviewDigest,
|
|
236
|
+
result: input.result,
|
|
237
|
+
...(input.errorCode === undefined ? {} : { errorCode: input.errorCode }),
|
|
238
|
+
execution: input.execution,
|
|
239
|
+
runtime: input.runtime,
|
|
240
|
+
nonce: input.nonce,
|
|
241
|
+
startedAt: input.startedAt,
|
|
242
|
+
completedAt: input.completedAt,
|
|
243
|
+
expiresAt: input.expiresAt,
|
|
244
|
+
signature: { algorithm: input.signature?.algorithm ?? "ed25519", value: "" }
|
|
245
|
+
} as unknown as Json);
|
|
246
|
+
return `att_${digest.slice("sha256:".length, "sha256:".length + 16)}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function collectCallerProvidedAttestationFields(value: unknown, found: Set<CallerProvidedAttestationField>): void {
|
|
250
|
+
collectForbiddenFields(value, found, CALLER_PROVIDED_ATTESTATION_FIELDS);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function collectForbiddenFields<T extends string>(value: unknown, found: Set<T>, forbiddenFields: readonly T[]): void {
|
|
254
|
+
if (!value || typeof value !== "object") return;
|
|
255
|
+
if (Array.isArray(value)) {
|
|
256
|
+
for (const item of value) collectForbiddenFields(item, found, forbiddenFields);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
260
|
+
if ((forbiddenFields as readonly string[]).includes(key)) {
|
|
261
|
+
found.add(key as T);
|
|
262
|
+
}
|
|
263
|
+
collectForbiddenFields(child, found, forbiddenFields);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export type GovernanceReasonCode =
|
|
268
|
+
| "ATTESTATION_SCHEMA_UNSUPPORTED"
|
|
269
|
+
| "CHALLENGE_NOT_FOUND"
|
|
270
|
+
| "CHALLENGE_EXPIRED"
|
|
271
|
+
| "CHALLENGE_SUPERSEDED"
|
|
272
|
+
| "CHALLENGE_ALREADY_CONSUMED"
|
|
273
|
+
| "NONCE_MISMATCH"
|
|
274
|
+
| "REPOSITORY_MISMATCH"
|
|
275
|
+
| "PULL_REQUEST_MISMATCH"
|
|
276
|
+
| "HEAD_SHA_MISMATCH"
|
|
277
|
+
| "BASE_SHA_MISMATCH"
|
|
278
|
+
| "TREE_OID_MISMATCH"
|
|
279
|
+
| "TRUST_LEVEL_MISMATCH"
|
|
280
|
+
| "DIGEST_INVALID"
|
|
281
|
+
| "RUNNER_NOT_FOUND"
|
|
282
|
+
| "RUNNER_REVOKED"
|
|
283
|
+
| "RUNNER_SCOPE_MISMATCH"
|
|
284
|
+
| "WORKFLOW_REF_MISMATCH"
|
|
285
|
+
| "DEVICE_REVOKED"
|
|
286
|
+
| "RUNTIME_VERSION_UNSUPPORTED"
|
|
287
|
+
| "SIGNATURE_INVALID"
|
|
288
|
+
| "PAYLOAD_PRIVACY_VIOLATION"
|
|
289
|
+
| "CHECK_DELIVERY_FAILED"
|
|
290
|
+
| "CHECK_DELIVERY_MAX_ATTEMPTS";
|
|
291
|
+
|
|
292
|
+
export const GOVERNANCE_REASON_CATALOG: Record<GovernanceReasonCode, { retryable: boolean; action: string }> = {
|
|
293
|
+
ATTESTATION_SCHEMA_UNSUPPORTED: { retryable: true, action: "rerun-with-attestation-v2" },
|
|
294
|
+
CHALLENGE_NOT_FOUND: { retryable: false, action: "refresh-current-challenge" },
|
|
295
|
+
CHALLENGE_EXPIRED: { retryable: true, action: "lease-new-challenge" },
|
|
296
|
+
CHALLENGE_SUPERSEDED: { retryable: true, action: "review-latest-head" },
|
|
297
|
+
CHALLENGE_ALREADY_CONSUMED: { retryable: false, action: "do-not-reuse-attestation" },
|
|
298
|
+
NONCE_MISMATCH: { retryable: false, action: "restart-review-session" },
|
|
299
|
+
REPOSITORY_MISMATCH: { retryable: false, action: "verify-repository-binding" },
|
|
300
|
+
PULL_REQUEST_MISMATCH: { retryable: false, action: "verify-pull-request-binding" },
|
|
301
|
+
HEAD_SHA_MISMATCH: { retryable: true, action: "fetch-current-head-and-rerun" },
|
|
302
|
+
BASE_SHA_MISMATCH: { retryable: true, action: "refresh-pull-request-metadata" },
|
|
303
|
+
TREE_OID_MISMATCH: { retryable: true, action: "recreate-clean-worktree" },
|
|
304
|
+
TRUST_LEVEL_MISMATCH: { retryable: false, action: "use-required-review-mode" },
|
|
305
|
+
DIGEST_INVALID: { retryable: false, action: "rerun-review-session" },
|
|
306
|
+
RUNNER_NOT_FOUND: { retryable: false, action: "register-runner-identity" },
|
|
307
|
+
RUNNER_REVOKED: { retryable: false, action: "register-replacement-runner-key" },
|
|
308
|
+
RUNNER_SCOPE_MISMATCH: { retryable: false, action: "register-runner-for-repository" },
|
|
309
|
+
WORKFLOW_REF_MISMATCH: { retryable: false, action: "use-approved-runner-workflow" },
|
|
310
|
+
DEVICE_REVOKED: { retryable: false, action: "register-new-device-key" },
|
|
311
|
+
RUNTIME_VERSION_UNSUPPORTED: { retryable: true, action: "upgrade-archctx-runtime" },
|
|
312
|
+
SIGNATURE_INVALID: { retryable: false, action: "restart-review-session" },
|
|
313
|
+
PAYLOAD_PRIVACY_VIOLATION: { retryable: false, action: "remove-private-content-from-payload" },
|
|
314
|
+
CHECK_DELIVERY_FAILED: { retryable: true, action: "retry-check-delivery" },
|
|
315
|
+
CHECK_DELIVERY_MAX_ATTEMPTS: { retryable: false, action: "manual-replay-or-rerequest-check" }
|
|
316
|
+
} as const;
|
|
317
|
+
|
|
318
|
+
export interface ReviewChallengeV2 {
|
|
319
|
+
schemaVersion: "archcontext.review-challenge/v2";
|
|
320
|
+
challengeId: string;
|
|
321
|
+
installationId: number;
|
|
322
|
+
repositoryId: number;
|
|
323
|
+
pullRequestNumber: number;
|
|
324
|
+
headSha: string;
|
|
325
|
+
baseSha: string;
|
|
326
|
+
nonce: string;
|
|
327
|
+
requiredTrust: RequiredTrust;
|
|
328
|
+
policyProfileId: string;
|
|
329
|
+
createdAt: string;
|
|
330
|
+
expiresAt: string;
|
|
331
|
+
status: ReviewChallengeStatus;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface AttestationV2 {
|
|
335
|
+
schemaVersion: "archcontext.attestation/v2";
|
|
336
|
+
attestationId: string;
|
|
337
|
+
challengeId: string;
|
|
338
|
+
installationId: number;
|
|
339
|
+
repositoryId: number;
|
|
340
|
+
pullRequestNumber: number;
|
|
341
|
+
headSha: string;
|
|
342
|
+
baseSha: string;
|
|
343
|
+
mergeBaseSha: string;
|
|
344
|
+
headTreeOid: string;
|
|
345
|
+
worktreeDigest: string;
|
|
346
|
+
modelDigest: string;
|
|
347
|
+
policyDigest: string;
|
|
348
|
+
codeFactsDigest: string;
|
|
349
|
+
reviewDigest: string;
|
|
350
|
+
result: AttestationResult;
|
|
351
|
+
errorCode?: string;
|
|
352
|
+
execution:
|
|
353
|
+
| {
|
|
354
|
+
trustLevel: "developer";
|
|
355
|
+
source: "clean-commit-worktree";
|
|
356
|
+
principalId: string;
|
|
357
|
+
publicKeyId: string;
|
|
358
|
+
}
|
|
359
|
+
| {
|
|
360
|
+
trustLevel: "organization";
|
|
361
|
+
source: "organization-runner-checkout";
|
|
362
|
+
principalId: string;
|
|
363
|
+
publicKeyId: string;
|
|
364
|
+
runnerId: string;
|
|
365
|
+
workflowRef: string;
|
|
366
|
+
runId: string;
|
|
367
|
+
runAttempt: number;
|
|
368
|
+
};
|
|
369
|
+
runtime: {
|
|
370
|
+
version: string;
|
|
371
|
+
buildDigest: string;
|
|
372
|
+
codeGraphVersion: string;
|
|
373
|
+
capabilitiesDigest: string;
|
|
374
|
+
};
|
|
375
|
+
nonce: string;
|
|
376
|
+
startedAt: string;
|
|
377
|
+
completedAt: string;
|
|
378
|
+
expiresAt: string;
|
|
379
|
+
signature: {
|
|
380
|
+
algorithm: "ed25519";
|
|
381
|
+
value: string;
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export interface DeviceIdentity {
|
|
386
|
+
schemaVersion: "archcontext.device-identity/v1";
|
|
387
|
+
deviceId: string;
|
|
388
|
+
accountId: string;
|
|
389
|
+
publicKeyId: string;
|
|
390
|
+
publicKeyFingerprint: string;
|
|
391
|
+
status: "active" | "revoked";
|
|
392
|
+
createdAt: string;
|
|
393
|
+
revokedAt?: string | null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export interface RunnerIdentity {
|
|
397
|
+
schemaVersion: "archcontext.runner-identity/v1";
|
|
398
|
+
runnerId: string;
|
|
399
|
+
installationId: number;
|
|
400
|
+
repositoryIds: number[];
|
|
401
|
+
scope?: RunnerIdentityScope;
|
|
402
|
+
workflowRef: string;
|
|
403
|
+
publicKeyId: string;
|
|
404
|
+
publicKeyFingerprint: string;
|
|
405
|
+
status: RunnerIdentityStatus;
|
|
406
|
+
createdAt: string;
|
|
407
|
+
rotatedAt?: string | null;
|
|
408
|
+
revokedAt?: string | null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface GovernanceKeyStatus {
|
|
412
|
+
schemaVersion: "archcontext.governance-key-status/v1";
|
|
413
|
+
publicKeyId: string;
|
|
414
|
+
ownerKind: "device" | "runner";
|
|
415
|
+
ownerId: string;
|
|
416
|
+
fingerprint: string;
|
|
417
|
+
status: "active" | "rotating" | "revoked";
|
|
418
|
+
createdAt: string;
|
|
419
|
+
rotatedAt?: string | null;
|
|
420
|
+
revokedAt?: string | null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface CheckDelivery {
|
|
424
|
+
schemaVersion: "archcontext.check-delivery/v1";
|
|
425
|
+
deliveryId: string;
|
|
426
|
+
challengeId: string;
|
|
427
|
+
checkRunId?: string | null;
|
|
428
|
+
checkName: GovernanceCheckName;
|
|
429
|
+
headSha: string;
|
|
430
|
+
status: CheckDeliveryStatus;
|
|
431
|
+
attemptCount: number;
|
|
432
|
+
nextAttemptAt?: string | null;
|
|
433
|
+
lastErrorCode?: GovernanceReasonCode | null;
|
|
434
|
+
createdAt: string;
|
|
435
|
+
updatedAt: string;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export interface CloudEgressEnvelope {
|
|
439
|
+
schemaVersion: "archcontext.cloud-egress/v1";
|
|
440
|
+
requestId: string;
|
|
441
|
+
category: "github.metadata" | "github.pull-head" | "github.check-list-for-ref" | "github.check-create" | "github.check-update";
|
|
442
|
+
method: "GET" | "POST" | "PATCH";
|
|
443
|
+
host: "api.github.com";
|
|
444
|
+
pathTemplate: string;
|
|
445
|
+
statusCode: number;
|
|
446
|
+
latencyMs: number;
|
|
447
|
+
recordedAt: string;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export interface RepositoryMetadata {
|
|
451
|
+
installationId: number;
|
|
452
|
+
repositoryId: number;
|
|
453
|
+
owner: string;
|
|
454
|
+
name: string;
|
|
455
|
+
visibility: "public" | "private";
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export interface PullHeadMetadata {
|
|
459
|
+
installationId: number;
|
|
460
|
+
repositoryId: number;
|
|
461
|
+
pullRequestNumber: number;
|
|
462
|
+
headSha: string;
|
|
463
|
+
baseSha: string;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export interface ReviewChallengePullHeadIdentity {
|
|
467
|
+
installationId: number;
|
|
468
|
+
repositoryId: number;
|
|
469
|
+
pullRequestNumber: number;
|
|
470
|
+
headSha: string;
|
|
471
|
+
baseSha: string;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export interface ReviewChallengePullHeadVerification {
|
|
475
|
+
schemaVersion: "archcontext.review-challenge-pull-head-verification/v1";
|
|
476
|
+
accepted: boolean;
|
|
477
|
+
reasonCode?: Extract<GovernanceReasonCode, "REPOSITORY_MISMATCH" | "PULL_REQUEST_MISMATCH" | "HEAD_SHA_MISMATCH" | "BASE_SHA_MISMATCH">;
|
|
478
|
+
challengeId: string;
|
|
479
|
+
expected: ReviewChallengePullHeadIdentity;
|
|
480
|
+
observed: PullHeadMetadata;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export interface CreateGovernanceCheckInput {
|
|
484
|
+
installationId: number;
|
|
485
|
+
repositoryId: number;
|
|
486
|
+
pullRequestNumber: number;
|
|
487
|
+
headSha: string;
|
|
488
|
+
name: GovernanceCheckName;
|
|
489
|
+
status: "queued" | "in_progress" | "completed";
|
|
490
|
+
externalId?: string;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export interface UpdateGovernanceCheckInput {
|
|
494
|
+
installationId: number;
|
|
495
|
+
repositoryId: number;
|
|
496
|
+
checkRunId: string;
|
|
497
|
+
name: GovernanceCheckName;
|
|
498
|
+
status: "queued" | "in_progress" | "completed";
|
|
499
|
+
conclusion?: "success" | "failure" | "neutral" | "cancelled" | "timed_out" | "action_required";
|
|
500
|
+
output: {
|
|
501
|
+
title: string;
|
|
502
|
+
summary: string;
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export interface CheckReference {
|
|
507
|
+
checkRunId: string;
|
|
508
|
+
htmlUrl?: string;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export interface CheckRunReference extends CheckReference {
|
|
512
|
+
name: GovernanceCheckName;
|
|
513
|
+
headSha: string;
|
|
514
|
+
status: CreateGovernanceCheckInput["status"];
|
|
515
|
+
conclusion?: NonNullable<UpdateGovernanceCheckInput["conclusion"]> | null;
|
|
516
|
+
output?: {
|
|
517
|
+
title?: string | null;
|
|
518
|
+
summary?: string | null;
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export interface GitHubGovernancePort {
|
|
523
|
+
getRepositoryMetadata(input: { installationId: number; repositoryId: number }): Promise<RepositoryMetadata>;
|
|
524
|
+
getPullHeadMetadata(input: {
|
|
525
|
+
installationId: number;
|
|
526
|
+
repositoryId: number;
|
|
527
|
+
pullRequestNumber: number;
|
|
528
|
+
}): Promise<PullHeadMetadata>;
|
|
529
|
+
listCheckRunsForRef(input: {
|
|
530
|
+
installationId: number;
|
|
531
|
+
repositoryId: number;
|
|
532
|
+
ref: string;
|
|
533
|
+
name: GovernanceCheckName;
|
|
534
|
+
}): Promise<CheckRunReference[]>;
|
|
535
|
+
createCheckRun(input: CreateGovernanceCheckInput): Promise<CheckReference>;
|
|
536
|
+
updateCheckRun(input: UpdateGovernanceCheckInput): Promise<void>;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function canTransitionChallenge(from: ReviewChallengeStatus, to: ReviewChallengeStatus): boolean {
|
|
540
|
+
if (!isReviewChallengeStatus(from) || !isReviewChallengeStatus(to)) return false;
|
|
541
|
+
return CHALLENGE_STATUS_TRANSITIONS[from].includes(to);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function assertCanTransitionChallenge(from: ReviewChallengeStatus, to: ReviewChallengeStatus): void {
|
|
545
|
+
if (!isReviewChallengeStatus(from)) throw new Error(`challenge-status-invalid: ${String(from)}`);
|
|
546
|
+
if (!isReviewChallengeStatus(to)) throw new Error(`challenge-status-invalid: ${String(to)}`);
|
|
547
|
+
if (!canTransitionChallenge(from, to)) throw new Error(`challenge-transition-invalid: ${from}->${to}`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function transitionReviewChallengeStatus(challenge: ReviewChallengeV2, to: ReviewChallengeStatus): ReviewChallengeV2 {
|
|
551
|
+
assertCanTransitionChallenge(challenge.status, to);
|
|
552
|
+
return { ...challenge, status: to };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function canTransitionCheckDelivery(from: CheckDeliveryStatus, to: CheckDeliveryStatus): boolean {
|
|
556
|
+
return CHECK_DELIVERY_STATUS_TRANSITIONS[from].includes(to);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function createRunnerIdentity(input: CreateRunnerIdentityInput): RunnerIdentity {
|
|
560
|
+
const scope = normalizeRunnerIdentityScope(input.scope ?? { kind: "repository", repositoryIds: input.repositoryIds ?? [] });
|
|
561
|
+
const repositoryIds = scope.kind === "organization" ? [] : scope.repositoryIds;
|
|
562
|
+
const identity: RunnerIdentity = {
|
|
563
|
+
schemaVersion: "archcontext.runner-identity/v1",
|
|
564
|
+
runnerId: input.runnerId,
|
|
565
|
+
installationId: input.installationId,
|
|
566
|
+
repositoryIds,
|
|
567
|
+
scope,
|
|
568
|
+
workflowRef: input.workflowRef,
|
|
569
|
+
publicKeyId: input.publicKeyId,
|
|
570
|
+
publicKeyFingerprint: input.publicKeyFingerprint,
|
|
571
|
+
status: input.status,
|
|
572
|
+
createdAt: input.createdAt,
|
|
573
|
+
rotatedAt: input.rotatedAt ?? null,
|
|
574
|
+
revokedAt: input.revokedAt ?? null
|
|
575
|
+
};
|
|
576
|
+
assertRunnerIdentity(identity);
|
|
577
|
+
return identity;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function assertRunnerIdentity(value: unknown): asserts value is RunnerIdentity {
|
|
581
|
+
const record = requireRecord(value, "runner-identity");
|
|
582
|
+
assertKnownRunnerIdentityKeys(record);
|
|
583
|
+
if (record.schemaVersion !== "archcontext.runner-identity/v1") throw new Error("runner-identity-schemaVersion-invalid");
|
|
584
|
+
const runnerId = requireRunnerId(record.runnerId);
|
|
585
|
+
const installationId = requirePositiveInteger(record.installationId, "installationId", "runner-identity");
|
|
586
|
+
const repositoryIds = normalizeRepositoryIds(readRepositoryIds(record.repositoryIds, "repositoryIds"));
|
|
587
|
+
const scope = normalizeRunnerIdentityScope(record.scope === undefined ? { kind: "repository", repositoryIds } : record.scope);
|
|
588
|
+
if (scope.kind === "repository" && repositoryIds.length === 0) throw new Error("runner-identity-repositoryIds-empty");
|
|
589
|
+
if (scope.kind === "repository" && !sameNumberSet(repositoryIds, scope.repositoryIds)) {
|
|
590
|
+
throw new Error("runner-identity-scope-repositoryIds-mismatch");
|
|
591
|
+
}
|
|
592
|
+
if (scope.kind === "organization" && repositoryIds.length !== 0) {
|
|
593
|
+
throw new Error("runner-identity-organization-scope-repositoryIds-must-be-empty");
|
|
594
|
+
}
|
|
595
|
+
requireWorkflowRef(record.workflowRef);
|
|
596
|
+
requireNonEmptyString(record.publicKeyId, "publicKeyId", "runner-identity");
|
|
597
|
+
requireSha256Digest(record.publicKeyFingerprint, "publicKeyFingerprint", "runner-identity");
|
|
598
|
+
const status = requireRunnerIdentityStatus(record.status);
|
|
599
|
+
requireIsoTimestamp(record.createdAt, "createdAt", "runner-identity");
|
|
600
|
+
const rotatedAt = readNullableIsoTimestamp(record.rotatedAt, "rotatedAt", "runner-identity");
|
|
601
|
+
const revokedAt = readNullableIsoTimestamp(record.revokedAt, "revokedAt", "runner-identity");
|
|
602
|
+
if (status === "rotating" && !rotatedAt) throw new Error("runner-identity-rotating-requires-rotatedAt");
|
|
603
|
+
if (status === "revoked" && !revokedAt) throw new Error("runner-identity-revoked-requires-revokedAt");
|
|
604
|
+
if (status !== "revoked" && revokedAt) throw new Error("runner-identity-active-revokedAt-invalid");
|
|
605
|
+
if (!runnerId || installationId <= 0) throw new Error("runner-identity-invalid");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function canTransitionRunnerIdentityStatus(from: RunnerIdentityStatus, to: RunnerIdentityStatus): boolean {
|
|
609
|
+
return isRunnerIdentityStatus(from) && isRunnerIdentityStatus(to) && RUNNER_IDENTITY_STATUS_TRANSITIONS[from].includes(to);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function assertCanTransitionRunnerIdentityStatus(from: RunnerIdentityStatus, to: RunnerIdentityStatus): void {
|
|
613
|
+
if (!isRunnerIdentityStatus(from)) throw new Error(`runner-identity-status-invalid: ${String(from)}`);
|
|
614
|
+
if (!isRunnerIdentityStatus(to)) throw new Error(`runner-identity-status-invalid: ${String(to)}`);
|
|
615
|
+
if (!canTransitionRunnerIdentityStatus(from, to)) throw new Error(`runner-identity-transition-invalid: ${from}->${to}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function transitionRunnerIdentityStatus(identity: RunnerIdentity, to: RunnerIdentityStatus, changedAt: string): RunnerIdentity {
|
|
619
|
+
assertRunnerIdentity(identity);
|
|
620
|
+
assertCanTransitionRunnerIdentityStatus(identity.status, to);
|
|
621
|
+
requireIsoTimestamp(changedAt, "changedAt", "runner-identity");
|
|
622
|
+
const next: RunnerIdentity = {
|
|
623
|
+
...identity,
|
|
624
|
+
status: to,
|
|
625
|
+
rotatedAt: to === "rotating" || (identity.status === "rotating" && to === "active") ? changedAt : identity.rotatedAt ?? null,
|
|
626
|
+
revokedAt: to === "revoked" ? changedAt : null
|
|
627
|
+
};
|
|
628
|
+
assertRunnerIdentity(next);
|
|
629
|
+
return next;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function runnerIdentityMatchesScope(identity: RunnerIdentity, input: { installationId: number; repositoryId: number; workflowRef?: string }): boolean {
|
|
633
|
+
assertRunnerIdentity(identity);
|
|
634
|
+
const installationId = requirePositiveInteger(input.installationId, "installationId", "runner-identity-scope");
|
|
635
|
+
const repositoryId = requirePositiveInteger(input.repositoryId, "repositoryId", "runner-identity-scope");
|
|
636
|
+
if (identity.installationId !== installationId) return false;
|
|
637
|
+
if (input.workflowRef !== undefined && identity.workflowRef !== input.workflowRef) return false;
|
|
638
|
+
const scope = runnerIdentityEffectiveScope(identity);
|
|
639
|
+
return scope.kind === "organization" || scope.repositoryIds.includes(repositoryId);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export function runnerIdentityEffectiveScope(identity: RunnerIdentity): RunnerIdentityScope {
|
|
643
|
+
assertRunnerIdentity(identity);
|
|
644
|
+
return identity.scope ?? { kind: "repository", repositoryIds: normalizeRepositoryIds(identity.repositoryIds) };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export function runnerIdentityKeyStatus(identity: RunnerIdentity): GovernanceKeyStatus {
|
|
648
|
+
assertRunnerIdentity(identity);
|
|
649
|
+
return {
|
|
650
|
+
schemaVersion: "archcontext.governance-key-status/v1",
|
|
651
|
+
publicKeyId: identity.publicKeyId,
|
|
652
|
+
ownerKind: "runner",
|
|
653
|
+
ownerId: identity.runnerId,
|
|
654
|
+
fingerprint: identity.publicKeyFingerprint,
|
|
655
|
+
status: identity.status,
|
|
656
|
+
createdAt: identity.createdAt,
|
|
657
|
+
rotatedAt: identity.rotatedAt ?? null,
|
|
658
|
+
revokedAt: identity.revokedAt ?? null
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function satisfiesRequiredTrust(attestationTrustLevel: GovernanceTrustLevel, requiredTrust: RequiredTrust): boolean {
|
|
663
|
+
if (requiredTrust === "organization") return attestationTrustLevel === "organization";
|
|
664
|
+
return attestationTrustLevel === "developer" || attestationTrustLevel === "organization";
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function checkNameForRequiredTrust(requiredTrust: RequiredTrust): GovernanceCheckName {
|
|
668
|
+
return requiredTrust === "organization" ? ORGANIZATION_RUNNER_CHECK_NAME : DEVELOPER_REVIEW_CHECK_NAME;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function requiredTrustForCheckName(checkName: GovernanceCheckName): RequiredTrust {
|
|
672
|
+
return checkName === ORGANIZATION_RUNNER_CHECK_NAME ? "organization" : "developer";
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export function normalizeGovernanceFeatureFlags(flags: Partial<GovernanceFeatureFlags> | undefined): GovernanceFeatureFlags {
|
|
676
|
+
if (flags?.schemaVersion !== undefined && flags.schemaVersion !== DEFAULT_GOVERNANCE_FEATURE_FLAGS.schemaVersion) {
|
|
677
|
+
throw new Error("governance-feature-flags-schemaVersion-invalid");
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
schemaVersion: DEFAULT_GOVERNANCE_FEATURE_FLAGS.schemaVersion,
|
|
681
|
+
developerCheck: readGovernanceFeatureFlag(flags?.developerCheck, DEFAULT_GOVERNANCE_FEATURE_FLAGS.developerCheck, "developerCheck"),
|
|
682
|
+
organizationCheck: readGovernanceFeatureFlag(flags?.organizationCheck, DEFAULT_GOVERNANCE_FEATURE_FLAGS.organizationCheck, "organizationCheck"),
|
|
683
|
+
requiredTrust: readGovernanceFeatureFlag(flags?.requiredTrust, DEFAULT_GOVERNANCE_FEATURE_FLAGS.requiredTrust, "requiredTrust")
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function evaluateGovernanceFeatureFlags(input: {
|
|
688
|
+
requiredTrust: RequiredTrust;
|
|
689
|
+
flags?: Partial<GovernanceFeatureFlags>;
|
|
690
|
+
}): GovernanceFeatureFlagDecision {
|
|
691
|
+
const requiredTrust = requireRequiredTrust(input.requiredTrust);
|
|
692
|
+
const flags = normalizeGovernanceFeatureFlags(input.flags);
|
|
693
|
+
const checkName = checkNameForRequiredTrust(requiredTrust);
|
|
694
|
+
const disabled =
|
|
695
|
+
requiredTrust === "organization" && !flags.requiredTrust
|
|
696
|
+
? { disabledFlag: "requiredTrust" as const, reason: "required-trust-disabled" as const }
|
|
697
|
+
: checkName === DEVELOPER_REVIEW_CHECK_NAME && !flags.developerCheck
|
|
698
|
+
? { disabledFlag: "developerCheck" as const, reason: "developer-check-disabled" as const }
|
|
699
|
+
: checkName === ORGANIZATION_RUNNER_CHECK_NAME && !flags.organizationCheck
|
|
700
|
+
? { disabledFlag: "organizationCheck" as const, reason: "organization-check-disabled" as const }
|
|
701
|
+
: undefined;
|
|
702
|
+
const reason: GovernanceFeatureFlagDecisionReason = disabled?.reason ?? "enabled";
|
|
703
|
+
const base = {
|
|
704
|
+
schemaVersion: "archcontext.governance-feature-flag-decision/v1" as const,
|
|
705
|
+
allowed: disabled === undefined,
|
|
706
|
+
reason,
|
|
707
|
+
requiredTrust,
|
|
708
|
+
checkName,
|
|
709
|
+
flags,
|
|
710
|
+
...(disabled === undefined ? {} : { disabledFlag: disabled.disabledFlag })
|
|
711
|
+
};
|
|
712
|
+
return {
|
|
713
|
+
...base,
|
|
714
|
+
metadataDigest: digestJson(base as unknown as Json)
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export function assertGovernanceFeatureFlagsAllow(input: {
|
|
719
|
+
requiredTrust: RequiredTrust;
|
|
720
|
+
flags?: Partial<GovernanceFeatureFlags>;
|
|
721
|
+
}): GovernanceFeatureFlagDecision {
|
|
722
|
+
const decision = evaluateGovernanceFeatureFlags(input);
|
|
723
|
+
if (!decision.allowed) throw new Error(`governance-feature-disabled: ${decision.reason}`);
|
|
724
|
+
return decision;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function isReviewChallengeStatus(value: unknown): value is ReviewChallengeStatus {
|
|
728
|
+
return typeof value === "string" && value in CHALLENGE_STATUS_TRANSITIONS;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function requireRequiredTrust(value: unknown): RequiredTrust {
|
|
732
|
+
if (value === "developer" || value === "organization") return value;
|
|
733
|
+
throw new Error("governance-requiredTrust-invalid");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function readGovernanceFeatureFlag(value: unknown, fallback: boolean, label: GovernanceFeatureFlagName): boolean {
|
|
737
|
+
if (value === undefined) return fallback;
|
|
738
|
+
if (typeof value !== "boolean") throw new Error(`governance-feature-flag-${label}-invalid`);
|
|
739
|
+
return value;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function isRunnerIdentityStatus(value: unknown): value is RunnerIdentityStatus {
|
|
743
|
+
return typeof value === "string" && value in RUNNER_IDENTITY_STATUS_TRANSITIONS;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function normalizeRunnerIdentityScope(value: unknown): RunnerIdentityScope {
|
|
747
|
+
const record = requireRecord(value, "runner-identity-scope");
|
|
748
|
+
const kind = requireNonEmptyString(record.kind, "kind", "runner-identity-scope");
|
|
749
|
+
if (kind === "organization") {
|
|
750
|
+
assertKnownKeys(record, new Set(["kind"]), "runner-identity-scope");
|
|
751
|
+
return { kind: "organization" };
|
|
752
|
+
}
|
|
753
|
+
if (kind === "repository") {
|
|
754
|
+
assertKnownKeys(record, new Set(["kind", "repositoryIds"]), "runner-identity-scope");
|
|
755
|
+
const repositoryIds = normalizeRepositoryIds(readRepositoryIds(record.repositoryIds, "scope.repositoryIds"));
|
|
756
|
+
if (repositoryIds.length === 0) throw new Error("runner-identity-scope-repositoryIds-empty");
|
|
757
|
+
return { kind: "repository", repositoryIds };
|
|
758
|
+
}
|
|
759
|
+
throw new Error("runner-identity-scope-kind-invalid");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function normalizeRepositoryIds(value: number[]): number[] {
|
|
763
|
+
const ids = [...new Set(value)].sort((a, b) => a - b);
|
|
764
|
+
return ids;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function readRepositoryIds(value: unknown, label: string): number[] {
|
|
768
|
+
if (!Array.isArray(value)) throw new Error(`runner-identity-${label}-invalid`);
|
|
769
|
+
return value.map((item, index) => requirePositiveInteger(item, `${label}[${index}]`, "runner-identity"));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function sameNumberSet(a: number[], b: number[]): boolean {
|
|
773
|
+
const left = normalizeRepositoryIds(a);
|
|
774
|
+
const right = normalizeRepositoryIds(b);
|
|
775
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function assertKnownRunnerIdentityKeys(record: Record<string, unknown>): void {
|
|
779
|
+
assertKnownKeys(record, new Set([
|
|
780
|
+
"schemaVersion",
|
|
781
|
+
"runnerId",
|
|
782
|
+
"installationId",
|
|
783
|
+
"repositoryIds",
|
|
784
|
+
"scope",
|
|
785
|
+
"workflowRef",
|
|
786
|
+
"publicKeyId",
|
|
787
|
+
"publicKeyFingerprint",
|
|
788
|
+
"status",
|
|
789
|
+
"createdAt",
|
|
790
|
+
"rotatedAt",
|
|
791
|
+
"revokedAt"
|
|
792
|
+
]), "runner-identity");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function assertKnownKeys(record: Record<string, unknown>, allowed: Set<string>, prefix: string): void {
|
|
796
|
+
for (const key of Object.keys(record)) {
|
|
797
|
+
if (!allowed.has(key)) throw new Error(`${prefix}-unknown-field: ${key}`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
802
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label}-invalid`);
|
|
803
|
+
return value as Record<string, unknown>;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function requireRunnerId(value: unknown): string {
|
|
807
|
+
const text = requireNonEmptyString(value, "runnerId", "runner-identity");
|
|
808
|
+
if (!/^runner_[A-Za-z0-9_.-]+$/.test(text)) throw new Error("runner-identity-runnerId-invalid");
|
|
809
|
+
return text;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function requireWorkflowRef(value: unknown): string {
|
|
813
|
+
const text = requireNonEmptyString(value, "workflowRef", "runner-identity");
|
|
814
|
+
if (!/^[^/\s]+\/[^/\s]+\/\.github\/workflows\/[^@\s]+@refs\/(heads|tags)\/[^@\s]+$/.test(text) && !/^[^/\s]+\/[^/\s]+\/\.github\/workflows\/[^@\s]+@[a-f0-9]{40}$/i.test(text)) {
|
|
815
|
+
throw new Error("runner-identity-workflowRef-invalid");
|
|
816
|
+
}
|
|
817
|
+
return text;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function requirePositiveInteger(value: unknown, label: string, prefix: string): number {
|
|
821
|
+
if (!Number.isInteger(value) || Number(value) <= 0) throw new Error(`${prefix}-${label}-invalid`);
|
|
822
|
+
return Number(value);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function requireNonEmptyString(value: unknown, label: string, prefix: string): string {
|
|
826
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`${prefix}-${label}-invalid`);
|
|
827
|
+
return value;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function requireSha256Digest(value: unknown, label: string, prefix: string): string {
|
|
831
|
+
const text = requireNonEmptyString(value, label, prefix);
|
|
832
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(text)) throw new Error(`${prefix}-${label}-invalid`);
|
|
833
|
+
return text;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function requireRunnerIdentityStatus(value: unknown): RunnerIdentityStatus {
|
|
837
|
+
if (!isRunnerIdentityStatus(value)) throw new Error("runner-identity-status-invalid");
|
|
838
|
+
return value;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function requireIsoTimestamp(value: unknown, label: string, prefix: string): string {
|
|
842
|
+
const text = requireNonEmptyString(value, label, prefix);
|
|
843
|
+
if (!Number.isFinite(Date.parse(text))) throw new Error(`${prefix}-${label}-invalid`);
|
|
844
|
+
return text;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function readNullableIsoTimestamp(value: unknown, label: string, prefix: string): string | null {
|
|
848
|
+
if (value === undefined || value === null) return null;
|
|
849
|
+
return requireIsoTimestamp(value, label, prefix);
|
|
850
|
+
}
|