@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.6 → 0.1.8
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/bin/adl +2 -2
- package/bin/lean-ctx-observe-strip +76 -3
- package/bin/reconcile +460 -240
- package/bin/validate-manifest +36 -16
- package/package.json +1 -1
- package/packages/agent-hooks/src/deploy/deploy.ts +98 -18
- package/packages/agent-hooks/src/deploy/doctor.ts +61 -11
- package/packages/agent-hooks/src/deploy/json-merge.ts +430 -18
- package/packages/agent-hooks/src/deploy/lock.ts +36 -2
- package/packages/agent-hooks/src/lean-ctx/env.ts +17 -0
- package/packages/agent-hooks/src/lean-ctx/mcp.ts +4 -2
- package/packages/agent-settings/assets/lean-ctx/config.toml +3 -0
- package/packages/agent-settings/assets/lean-ctx/gitignore +4 -0
- package/packages/agent-settings/assets/lean-ctx/layout.toml +2 -0
- package/packages/agent-settings/src/deploy/agents-deploy.ts +10 -5
- package/packages/agent-settings/src/deploy/doctor.ts +39 -3
- package/packages/agent-settings/src/deploy/lean-ctx-deploy.ts +104 -12
- package/packages/agent-settings/src/deploy/lean-ctx-merge.ts +580 -0
- package/packages/agent-settings/src/deploy/lock.ts +4 -1
- package/packages/agent-settings/src/deploy/mcp-deploy.ts +9 -6
- package/packages/agent-settings/src/deploy/settings.ts +6 -3
- package/packages/agent-skills/dist/deploy.d.ts +2 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +15 -8
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/dist/manifest.d.ts +1 -0
- package/packages/agent-skills/dist/manifest.d.ts.map +1 -1
- package/packages/agent-skills/dist/manifest.js +6 -2
- package/packages/agent-skills/dist/manifest.js.map +1 -1
- package/packages/agent-skills/src/deploy.ts +15 -8
- package/packages/agent-skills/src/manifest.ts +5 -2
- package/packages/azdo/src/azdo-repo-doctor.ts +401 -91
- package/packages/azdo/src/lib/graph-identity.ts +190 -3
- package/packages/fleet/jobs/git-maintain/src/main.ts +305 -45
- package/packages/fleet/manifest.json +6 -4
- package/packages/fleet/manifest.schema.json +15 -5
- package/packages/repo-controls/README.md +13 -6
- package/packages/repo-controls/assets/adl-config.default.toml +1 -0
- package/packages/repo-controls/src/cli.ts +6 -1
- package/packages/repo-controls/src/gitignore-block.ts +17 -0
- package/packages/repo-controls/src/install.ts +88 -6
|
@@ -31,10 +31,32 @@
|
|
|
31
31
|
* values below are the same ones already read live and recorded in
|
|
32
32
|
* ADL-ISMS-Policies/docs/scripts/enact-agent-git-permissions.py and
|
|
33
33
|
* .../docs/evidence/vanta-documents/100-.../azure-devops-git-security-namespace.json)
|
|
34
|
-
* - Graph — Subject Query (resolve
|
|
34
|
+
* - Graph — Subject Query (resolve agent-tarun/agent-vivek/agent-reviewer's
|
|
35
|
+
* descriptor by display name — each is an Azure AD SERVICE PRINCIPAL
|
|
36
|
+
* ("aadsp") subject, verified live 2026-09-30; subjectKind must include
|
|
37
|
+
* 'ServicePrincipal' or the query returns no match at all, which is
|
|
38
|
+
* exactly what the permissions.<agent>.not-found drift below reported
|
|
39
|
+
* before this fix)
|
|
35
40
|
* POST https://vssps.dev.azure.com/{org}/_apis/graph/subjectquery?api-version=7.1-preview.1
|
|
36
|
-
* body: { query: <displayName>, subjectKind: ['User'] }
|
|
41
|
+
* body: { query: <displayName>, subjectKind: ['User', 'ServicePrincipal'] }
|
|
37
42
|
* https://learn.microsoft.com/en-us/rest/api/azure/devops/graph/subject-query/query
|
|
43
|
+
* - Identities — Read Identities, `subjectDescriptors` + `queryMembership=ExpandedUp`
|
|
44
|
+
* (an agent's own LEGACY descriptor — verified live 2026-09-30:
|
|
45
|
+
* agent-tarun/agent-vivek/agent-reviewer hold NO direct ACE anywhere on
|
|
46
|
+
* adl-agent-control or agent-identities; their real Contribute /
|
|
47
|
+
* Contribute-to-pull-requests bits come entirely from group membership,
|
|
48
|
+
* which Access Control Lists Query below can only expand server-side
|
|
49
|
+
* when given a LEGACY descriptor, never a raw Graph one — and that
|
|
50
|
+
* server-side expansion already correctly combines group membership
|
|
51
|
+
* WITH token-level inheritance, which is why this checker queries the
|
|
52
|
+
* ACL with ONLY the agent's own legacy descriptor and trusts its
|
|
53
|
+
* `effectiveAllow`/`effectiveDeny` directly, never a client-side union
|
|
54
|
+
* across separately-queried group descriptors — see
|
|
55
|
+
* lib/graph-identity.ts's `resolveLegacyDescriptor()` doc comment,
|
|
56
|
+
* "SECOND REVIEW", for the full live evidence and why an earlier
|
|
57
|
+
* per-group-union attempt was wrong.)
|
|
58
|
+
* GET https://vssps.dev.azure.com/{org}/_apis/identities?subjectDescriptors=<descriptor>&queryMembership=ExpandedUp&api-version=7.1
|
|
59
|
+
* https://learn.microsoft.com/en-us/rest/api/azure/devops/ims/identities/read-identities
|
|
38
60
|
* - Identities — Read Identities (resolve a BUILD SERVICE's descriptor by
|
|
39
61
|
* exact display name — a build service is not a Graph 'User'/'Group'
|
|
40
62
|
* subject, so Subject Query above cannot find it; used only by
|
|
@@ -81,7 +103,12 @@
|
|
|
81
103
|
|
|
82
104
|
import { resolveAgentIdentity } from "./lib/agent-identity.ts";
|
|
83
105
|
import { AdoClient, resolveOrg, resolveProject, resolveRepository } from "./lib/client.ts";
|
|
84
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
fetchJson,
|
|
108
|
+
isProjectBuildServiceDescriptor,
|
|
109
|
+
resolveAgentLegacyDescriptor,
|
|
110
|
+
resolveIdentityByDisplayName,
|
|
111
|
+
} from "./lib/graph-identity.ts";
|
|
85
112
|
|
|
86
113
|
/** Treat blank and whitespace-only env overrides as unset, like lib/client.ts's own `configured()`. */
|
|
87
114
|
function configured(value: string | undefined): string | undefined {
|
|
@@ -95,9 +122,10 @@ function orgBaseUrl(): string {
|
|
|
95
122
|
}
|
|
96
123
|
|
|
97
124
|
// fetchJson/postJson (bearer-token GET/POST against an arbitrary Azure DevOps
|
|
98
|
-
// host) and
|
|
99
|
-
// ./lib/graph-identity.ts — this file only adds its own
|
|
100
|
-
// accesscontrollists host); vsspsBaseUrl() moved with the
|
|
125
|
+
// host) and resolveAgentLegacyDescriptor (Graph Subject Query + Identities —
|
|
126
|
+
// ExpandedUp) live in ./lib/graph-identity.ts — this file only adds its own
|
|
127
|
+
// orgBaseUrl() (the accesscontrollists host); vsspsBaseUrl() moved with the
|
|
128
|
+
// Graph helpers.
|
|
101
129
|
|
|
102
130
|
// -------------------------------------------------------------------------
|
|
103
131
|
// The standard, in one place.
|
|
@@ -157,6 +185,18 @@ export const REPO_STANDARD = {
|
|
|
157
185
|
// repeats a bit name or a token shape in two places.
|
|
158
186
|
permissions: {
|
|
159
187
|
namespaceId: GIT_REPOSITORIES_NAMESPACE_ID,
|
|
188
|
+
/**
|
|
189
|
+
* Tarun, 2026-09-30: "leave [agent-reviewer's] permissions as they are
|
|
190
|
+
* and stop flagging it outside Enact." agent-reviewer's own
|
|
191
|
+
* `genericContribute` DENY check below (the Reader-only / no-Contribute
|
|
192
|
+
* rule) is scoped to the Enact project only — the SAME
|
|
193
|
+
* `STRICT_MAIN_BRANCH_PROJECT` mechanism (`repoInfo.project.name`,
|
|
194
|
+
* case-insensitive) the main-branch rules already use, enforced in
|
|
195
|
+
* `checkAgentBits()`'s own `ENACT_ONLY_AGENT_DENY` check just below.
|
|
196
|
+
* Every OTHER agent-permission check here — agent-tarun's and
|
|
197
|
+
* agent-vivek's full allow/deny sets, and agent-reviewer's own required
|
|
198
|
+
* `pullRequestContribute` — stays unconditional, for every project.
|
|
199
|
+
*/
|
|
160
200
|
agents: {
|
|
161
201
|
"agent-tarun": {
|
|
162
202
|
allow: ["genericContribute", "pullRequestContribute"],
|
|
@@ -187,12 +227,28 @@ export const REPO_STANDARD = {
|
|
|
187
227
|
},
|
|
188
228
|
/**
|
|
189
229
|
* checkBuildServiceCanRelease() (file header, "checkBuildServiceCanRelease()"):
|
|
190
|
-
* the exact three
|
|
191
|
-
*
|
|
192
|
-
*
|
|
230
|
+
* the exact three permissions the release flow needs from whichever
|
|
231
|
+
* build service identity actually runs the pipeline — the real failure
|
|
232
|
+
* this check exists for was a 403 on `pullRequestContribute`.
|
|
233
|
+
*
|
|
234
|
+
* `policyExempt` ("Bypass policies when pushing") is checked at the
|
|
235
|
+
* `integration` REF token, never the repo-scope token — verified live
|
|
236
|
+
* 2026-09-30 against adl-agent-control: the Infrastructure-AzDo Build
|
|
237
|
+
* Service demonstrably bypassed `integration`'s own policies to push
|
|
238
|
+
* `chore: bump version 0.1.6 to 0.1.7 ***NO_CI***` (c966e6a) straight to
|
|
239
|
+
* that protected branch, yet its repo-scope effective allow bitmask
|
|
240
|
+
* (16430) does not include PolicyExempt (128) at all — only its
|
|
241
|
+
* `refs/heads/integration`-scope effective allow (16558) does. Checking
|
|
242
|
+
* `policyExempt` at repo scope produced a false
|
|
243
|
+
* `permissions.release.<slug>.missing-policyExempt` finding that
|
|
244
|
+
* contradicted this observed reality. `genericContribute` and
|
|
245
|
+
* `pullRequestContribute` stay checked at repo scope — both bits ARE
|
|
246
|
+
* present there for real, and PR operations are a repo-level, not
|
|
247
|
+
* branch-level, permission.
|
|
193
248
|
*/
|
|
194
249
|
buildServiceRelease: {
|
|
195
|
-
|
|
250
|
+
repoScoped: ["genericContribute", "pullRequestContribute"] as const,
|
|
251
|
+
integrationRefScoped: ["policyExempt"] as const,
|
|
196
252
|
},
|
|
197
253
|
},
|
|
198
254
|
} as const;
|
|
@@ -219,7 +275,7 @@ export const GIT_REPO_PERMISSION_UI_LABEL: Record<PermissionBitName, string> = {
|
|
|
219
275
|
interface GitRepositoryResponse {
|
|
220
276
|
id: string;
|
|
221
277
|
defaultBranch?: string;
|
|
222
|
-
project?: { id?: string };
|
|
278
|
+
project?: { id?: string; name?: string };
|
|
223
279
|
}
|
|
224
280
|
|
|
225
281
|
interface GitRefsResponse {
|
|
@@ -265,6 +321,39 @@ interface AccessControlListsResponse {
|
|
|
265
321
|
}>;
|
|
266
322
|
}
|
|
267
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Tarun, 2026-09-30: the strict main-branch rules below — the build service
|
|
326
|
+
* must not have PolicyExempt (bypass) on refs/heads/main
|
|
327
|
+
* (`permissions.build-service.has-policyExempt-main`) and main's
|
|
328
|
+
* minimum-reviewer policy must have creatorVoteCounts=false
|
|
329
|
+
* (`main.minimum-reviewers-misconfigured: creatorVoteCounts must be false`)
|
|
330
|
+
* — apply ONLY to repositories in the Azure DevOps project "Enact".
|
|
331
|
+
* Repositories in "Infrastructure-AzDo" (the small internal tool repos:
|
|
332
|
+
* adl-agent-control, agent-identities, …) keep their existing settings and
|
|
333
|
+
* must not be flagged for these two; every other check in this file stays
|
|
334
|
+
* exactly as is, for every project. Real evidence this fix responds to:
|
|
335
|
+
* `adl repo doctor --repo agent-identities` (Infrastructure-AzDo) reported
|
|
336
|
+
* both findings before this change.
|
|
337
|
+
*
|
|
338
|
+
* Scoped from the REPOSITORY'S OWN project, as Git Repositories — Get
|
|
339
|
+
* Repository's response names it (`repoInfo.project.name`) — NEVER
|
|
340
|
+
* `resolveProject()`/cwd/env (independent review, 2026-09-30: cwd/env name
|
|
341
|
+
* the project the CALLING checkout happens to sit in, which can differ from
|
|
342
|
+
* the project the repository being CHECKED actually lives in; the doctor
|
|
343
|
+
* already resolves the repository by name against whichever project cwd/env
|
|
344
|
+
* picked, but the compliance decision must follow the checked repository,
|
|
345
|
+
* not the caller's own working directory). Compared case-insensitively,
|
|
346
|
+
* since Azure DevOps project names are case-preserving but not
|
|
347
|
+
* case-sensitive for identity purposes. No environment variable controls
|
|
348
|
+
* this; a per-repository override, if one is ever genuinely needed, belongs
|
|
349
|
+
* in that repository's own adl-config.toml / manifest, never an env var.
|
|
350
|
+
*/
|
|
351
|
+
export const STRICT_MAIN_BRANCH_PROJECT = "Enact";
|
|
352
|
+
|
|
353
|
+
function strictMainBranchRulesApply(project: string): boolean {
|
|
354
|
+
return project.toLowerCase() === STRICT_MAIN_BRANCH_PROJECT.toLowerCase();
|
|
355
|
+
}
|
|
356
|
+
|
|
268
357
|
export interface RepoDoctorDrift {
|
|
269
358
|
id: string;
|
|
270
359
|
message: string;
|
|
@@ -367,7 +456,11 @@ function checkMainRequiredReviewer(policies: readonly PolicyConfiguration[], dri
|
|
|
367
456
|
});
|
|
368
457
|
}
|
|
369
458
|
|
|
370
|
-
function checkMainMinimumReviewers(
|
|
459
|
+
function checkMainMinimumReviewers(
|
|
460
|
+
policies: readonly PolicyConfiguration[],
|
|
461
|
+
strictMainBranchRules: boolean,
|
|
462
|
+
drifts: RepoDoctorDrift[],
|
|
463
|
+
): void {
|
|
371
464
|
const minReviewer = enabledPolicyOfType(policies, POLICY_TYPE.minimumReviewers);
|
|
372
465
|
if (!minReviewer) {
|
|
373
466
|
drifts.push({
|
|
@@ -382,7 +475,9 @@ function checkMainMinimumReviewers(policies: readonly PolicyConfiguration[], dri
|
|
|
382
475
|
if (minReviewer.isBlocking !== expected.isBlocking) problems.push(`isBlocking must be ${expected.isBlocking}`);
|
|
383
476
|
if ((settings.minimumApproverCount ?? 0) < expected.minimumApproverCount)
|
|
384
477
|
problems.push(`minimumApproverCount must be at least ${expected.minimumApproverCount}`);
|
|
385
|
-
|
|
478
|
+
// Enact-only (STRICT_MAIN_BRANCH_PROJECT above): a repo outside Enact keeps
|
|
479
|
+
// its own creatorVoteCounts setting, flagged neither way.
|
|
480
|
+
if (strictMainBranchRules && Boolean(settings.creatorVoteCounts) !== expected.creatorVoteCounts)
|
|
386
481
|
problems.push(`creatorVoteCounts must be ${expected.creatorVoteCounts}`);
|
|
387
482
|
if (Boolean(settings.resetOnSourcePush) !== expected.resetOnSourcePush)
|
|
388
483
|
problems.push(`resetOnSourcePush must be ${expected.resetOnSourcePush}`);
|
|
@@ -437,10 +532,11 @@ function checkMainCommentsAndBuild(
|
|
|
437
532
|
function checkMainPolicies(
|
|
438
533
|
policies: readonly PolicyConfiguration[],
|
|
439
534
|
hasPipeline: boolean,
|
|
535
|
+
strictMainBranchRules: boolean,
|
|
440
536
|
drifts: RepoDoctorDrift[],
|
|
441
537
|
): void {
|
|
442
538
|
checkMainRequiredReviewer(policies, drifts);
|
|
443
|
-
checkMainMinimumReviewers(policies, drifts);
|
|
539
|
+
checkMainMinimumReviewers(policies, strictMainBranchRules, drifts);
|
|
444
540
|
checkMainMergeStrategy(policies, drifts);
|
|
445
541
|
checkMainCommentsAndBuild(policies, hasPipeline, drifts);
|
|
446
542
|
}
|
|
@@ -462,50 +558,128 @@ async function fetchAccessControlLists(
|
|
|
462
558
|
return fetchJson<AccessControlListsResponse>(url, "Access control lists query");
|
|
463
559
|
}
|
|
464
560
|
|
|
465
|
-
|
|
561
|
+
/**
|
|
562
|
+
* Comfortably under every documented and empirically observed URL-length
|
|
563
|
+
* limit for this API (independent review, 2026-09-30: "keep the
|
|
564
|
+
* query-string size bounded" — an identity's transitive group-membership
|
|
565
|
+
* count is unbounded in principle, and three agents' descriptor sets get
|
|
566
|
+
* combined into one call). `fetchEffectivePermissions()` below chunks
|
|
567
|
+
* automatically rather than ever sending one unbounded `descriptors` list —
|
|
568
|
+
* never a silent truncation: a failed chunk's fetch throws, same as a
|
|
569
|
+
* failed single-descriptor call always did.
|
|
570
|
+
*/
|
|
571
|
+
export const MAX_DESCRIPTORS_PER_ACL_QUERY = 50;
|
|
572
|
+
|
|
573
|
+
function chunk<T>(items: readonly T[], size: number): T[][] {
|
|
574
|
+
const chunks: T[][] = [];
|
|
575
|
+
for (let i = 0; i < items.length; i += size) chunks.push(items.slice(i, i + size));
|
|
576
|
+
return chunks;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export interface EffectivePermissionsResult {
|
|
580
|
+
byDescriptor: Map<string, { allow: number; deny: number }>;
|
|
581
|
+
/**
|
|
582
|
+
* Requested descriptors that never ended up in `byDescriptor` — either
|
|
583
|
+
* ABSENT from every chunk's `acesDictionary` entirely, or present but with
|
|
584
|
+
* NO `extendedInfo` key at all (checked via `'extendedInfo' in ace`, which
|
|
585
|
+
* distinguishes "the key is not there" from "the key is `{}`"). Both mean
|
|
586
|
+
* the effective bits could not actually be confirmed for that descriptor
|
|
587
|
+
* at this token. An entry that HAS `extendedInfo`, even `extendedInfo: {}`
|
|
588
|
+
* (a real, normal shape: the server computed it and every field happened
|
|
589
|
+
* to be zero/omitted), is NOT unresolved — it lands in `byDescriptor` with
|
|
590
|
+
* allow/deny defaulted to 0 exactly as before (independent review,
|
|
591
|
+
* 2026-09-30, LOW: "an entry that HAS extendedInfo with zero-valued
|
|
592
|
+
* fields omitted is normal and stays as today"). The caller MUST check
|
|
593
|
+
* this list and raise its own loud, distinct finding naming the identity
|
|
594
|
+
* and the token — never silently default an unresolved descriptor to
|
|
595
|
+
* `{allow: 0, deny: 0}`, which previously made a real "cannot confirm"
|
|
596
|
+
* look identical to "confirmed: nothing granted" (e.g. the Enact
|
|
597
|
+
* build-service refs/heads/main PolicyExempt DENY check silently PASSED
|
|
598
|
+
* when its own ACL entry was missing, instead of failing loudly).
|
|
599
|
+
*/
|
|
600
|
+
unresolved: string[];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Exported for testing: proves the chunking guard above merges every chunk's result, including the last one, rather than sending — or silently dropping — one unbounded query. */
|
|
604
|
+
export async function fetchEffectivePermissions(
|
|
466
605
|
namespaceId: string,
|
|
467
606
|
token: string,
|
|
468
607
|
descriptors: readonly string[],
|
|
469
|
-
): Promise<
|
|
608
|
+
): Promise<EffectivePermissionsResult> {
|
|
470
609
|
const byDescriptor = new Map<string, { allow: number; deny: number }>();
|
|
471
|
-
if (descriptors.length === 0) return byDescriptor;
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
for (const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
610
|
+
if (descriptors.length === 0) return { byDescriptor, unresolved: [] };
|
|
611
|
+
for (const batch of chunk(descriptors, MAX_DESCRIPTORS_PER_ACL_QUERY)) {
|
|
612
|
+
const result = await fetchAccessControlLists(namespaceId, token, batch);
|
|
613
|
+
for (const acl of result.value ?? []) {
|
|
614
|
+
for (const [descriptor, ace] of Object.entries(acl.acesDictionary ?? {})) {
|
|
615
|
+
if (!("extendedInfo" in ace)) continue; // see EffectivePermissionsResult.unresolved's doc comment
|
|
616
|
+
byDescriptor.set(descriptor, {
|
|
617
|
+
allow: ace.extendedInfo?.effectiveAllow ?? ace.allow ?? 0,
|
|
618
|
+
deny: ace.extendedInfo?.effectiveDeny ?? ace.deny ?? 0,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
479
621
|
}
|
|
480
622
|
}
|
|
481
|
-
|
|
623
|
+
const unresolved = descriptors.filter((descriptor) => !byDescriptor.has(descriptor));
|
|
624
|
+
return { byDescriptor, unresolved };
|
|
482
625
|
}
|
|
483
626
|
|
|
484
|
-
/**
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
627
|
+
/**
|
|
628
|
+
* Look up `descriptor`'s effective ACE in `effective`, or push a loud,
|
|
629
|
+
* distinct `permissions.<idFragment>.acl-entry-missing<idSuffix>` finding
|
|
630
|
+
* naming `description` and `token` and return undefined — the caller must
|
|
631
|
+
* then skip whatever bit checks it was about to run for this identity/token
|
|
632
|
+
* pair, never fall back to `{allow: 0, deny: 0}` (see
|
|
633
|
+
* `EffectivePermissionsResult.unresolved`'s doc comment for why that
|
|
634
|
+
* silently hid real forbidden-bit findings).
|
|
635
|
+
*/
|
|
636
|
+
function resolveEffectiveAce(
|
|
637
|
+
effective: EffectivePermissionsResult,
|
|
638
|
+
descriptor: string,
|
|
639
|
+
idFragment: string,
|
|
640
|
+
idSuffix: string,
|
|
641
|
+
description: string,
|
|
642
|
+
tokenLabel: string,
|
|
643
|
+
drifts: RepoDoctorDrift[],
|
|
644
|
+
): { allow: number; deny: number } | undefined {
|
|
645
|
+
const ace = effective.byDescriptor.get(descriptor);
|
|
646
|
+
if (ace) return ace;
|
|
647
|
+
drifts.push({
|
|
648
|
+
id: `permissions.${idFragment}.acl-entry-missing${idSuffix}`,
|
|
649
|
+
message: `could not confirm ${description}'s effective permissions on ${tokenLabel} — its Access Control Lists Query entry was missing from the response (or had no extendedInfo at all), never treated as "no permission"`,
|
|
650
|
+
});
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* `ace.allow` bits with `ace.deny` bits removed — Azure DevOps: an explicit
|
|
656
|
+
* Deny always beats an Allow FOR THE SAME EFFECTIVE RESULT. `ace` here is
|
|
657
|
+
* always the SERVER's own already-combined `effectiveAllow`/`effectiveDeny`
|
|
658
|
+
* for ONE descriptor at ONE token (see `checkAgentPermissions()`'s doc
|
|
659
|
+
* comment for why this file never reconstructs that combination itself from
|
|
660
|
+
* several descriptors — the server already does it correctly, including
|
|
661
|
+
* token-level precedence a client-side union cannot see).
|
|
662
|
+
*/
|
|
663
|
+
function effectiveAllowBits(ace: { allow: number; deny: number }): number {
|
|
664
|
+
return ace.allow & ~ace.deny;
|
|
499
665
|
}
|
|
500
666
|
|
|
667
|
+
/** The ONE agent-permission check scoped to Enact (Tarun, 2026-09-30) — see `REPO_STANDARD.permissions.agents`'s own doc comment above for why. */
|
|
668
|
+
const ENACT_ONLY_AGENT_DENY: { readonly name: string; readonly bit: PermissionBitName } = {
|
|
669
|
+
name: "agent-reviewer",
|
|
670
|
+
bit: "genericContribute",
|
|
671
|
+
};
|
|
672
|
+
|
|
501
673
|
function checkAgentBits(
|
|
502
674
|
name: string,
|
|
503
675
|
ace: { allow: number; deny: number },
|
|
504
676
|
expected: { allow: readonly PermissionBitName[]; deny: readonly PermissionBitName[] },
|
|
677
|
+
strictMainBranchRules: boolean,
|
|
505
678
|
drifts: RepoDoctorDrift[],
|
|
506
679
|
): void {
|
|
680
|
+
const effective = effectiveAllowBits(ace);
|
|
507
681
|
for (const bitName of expected.allow) {
|
|
508
|
-
if ((
|
|
682
|
+
if ((effective & GIT_REPO_PERMISSION_BIT[bitName]) === 0) {
|
|
509
683
|
drifts.push({
|
|
510
684
|
id: `permissions.${name}.missing-${bitName}`,
|
|
511
685
|
message: `${name} does not effectively have ${bitName} on this repository`,
|
|
@@ -513,7 +687,10 @@ function checkAgentBits(
|
|
|
513
687
|
}
|
|
514
688
|
}
|
|
515
689
|
for (const bitName of expected.deny) {
|
|
516
|
-
if (
|
|
690
|
+
if (!strictMainBranchRules && name === ENACT_ONLY_AGENT_DENY.name && bitName === ENACT_ONLY_AGENT_DENY.bit) {
|
|
691
|
+
continue; // Tarun, 2026-09-30: leave agent-reviewer's permissions as they are outside Enact
|
|
692
|
+
}
|
|
693
|
+
if ((effective & GIT_REPO_PERMISSION_BIT[bitName]) !== 0) {
|
|
517
694
|
drifts.push({
|
|
518
695
|
id: `permissions.${name}.has-${bitName}`,
|
|
519
696
|
message: `${name} effectively has ${bitName} on this repository, which the standard forbids`,
|
|
@@ -522,9 +699,59 @@ function checkAgentBits(
|
|
|
522
699
|
}
|
|
523
700
|
}
|
|
524
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Resolves every agent's own LEGACY descriptor — see
|
|
704
|
+
* `resolveAgentLegacyDescriptor()`'s doc comment in lib/graph-identity.ts
|
|
705
|
+
* for the full live evidence this is built on (agent-tarun/agent-vivek/
|
|
706
|
+
* agent-reviewer hold no direct ACE at all in this org; their real
|
|
707
|
+
* Contribute/Contribute-to-pull-requests bits come entirely from group
|
|
708
|
+
* membership, and the Access Control Lists Query API cannot evaluate a raw
|
|
709
|
+
* Graph descriptor at all). `not-found` (Graph could not resolve the
|
|
710
|
+
* display name) and `expansion-failed` (Graph resolved it, but Identities —
|
|
711
|
+
* ExpandedUp could not) are reported as DISTINCT drifts rather than one
|
|
712
|
+
* generic failure — each names a different hop and a different fix.
|
|
713
|
+
* Identity lookups are cached across all three agents inside
|
|
714
|
+
* lib/graph-identity.ts (resetLegacyDescriptorCacheForTests()).
|
|
715
|
+
*/
|
|
716
|
+
async function resolveAgentDescriptors(drifts: RepoDoctorDrift[]): Promise<Map<string, string>> {
|
|
717
|
+
const descriptorByAgent = new Map<string, string>();
|
|
718
|
+
for (const name of Object.keys(REPO_STANDARD.permissions.agents)) {
|
|
719
|
+
const resolved = await resolveAgentLegacyDescriptor(name);
|
|
720
|
+
if ("error" in resolved) {
|
|
721
|
+
const message =
|
|
722
|
+
resolved.error === "not-found"
|
|
723
|
+
? `could not resolve a Graph descriptor for '${name}' (Graph subject query returned no exact display-name match)`
|
|
724
|
+
: `resolved a Graph descriptor for '${name}' but could not expand it to a legacy identity (Identities — Read Identities with queryMembership=ExpandedUp returned no match)`;
|
|
725
|
+
drifts.push({ id: `permissions.${name}.${resolved.error}`, message });
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
descriptorByAgent.set(name, resolved.descriptor);
|
|
729
|
+
}
|
|
730
|
+
return descriptorByAgent;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Independent review, 2026-09-30 (second pass — design correction): an
|
|
735
|
+
* earlier version of this check queried EACH agent's own descriptor AND
|
|
736
|
+
* every group it transitively belongs to, then unioned their allow/deny
|
|
737
|
+
* bits client-side. That is wrong in general: the security service's own
|
|
738
|
+
* per-descriptor evaluation already combines group membership WITH
|
|
739
|
+
* token-level inheritance using real precedence (an explicit Allow at the
|
|
740
|
+
* repo-scope token can override an inherited Deny that came down from the
|
|
741
|
+
* project-scope token) — live-verified on agent-tarun's real legacy
|
|
742
|
+
* descriptor (`effectiveAllow: 82038` with no direct ACE anywhere, i.e.
|
|
743
|
+
* fully server-computed from its group). A client-side union of
|
|
744
|
+
* separately-queried group ACEs cannot see WHICH token level a bit came
|
|
745
|
+
* from and can therefore disagree with the server: a false `missing-*`,
|
|
746
|
+
* or worse, silently hiding a real forbidden bit the server would report.
|
|
747
|
+
* The fix is to query the ACL with ONLY each agent's own legacy descriptor
|
|
748
|
+
* and trust ITS `effectiveAllow`/`effectiveDeny` directly — never to
|
|
749
|
+
* reconstruct that answer from parts.
|
|
750
|
+
*/
|
|
525
751
|
async function checkAgentPermissions(
|
|
526
752
|
projectId: string | undefined,
|
|
527
753
|
repositoryId: string,
|
|
754
|
+
strictMainBranchRules: boolean,
|
|
528
755
|
drifts: RepoDoctorDrift[],
|
|
529
756
|
): Promise<void> {
|
|
530
757
|
if (!projectId) {
|
|
@@ -544,7 +771,9 @@ async function checkAgentPermissions(
|
|
|
544
771
|
for (const [name, expected] of Object.entries(REPO_STANDARD.permissions.agents)) {
|
|
545
772
|
const descriptor = descriptorByAgent.get(name);
|
|
546
773
|
if (!descriptor) continue; // already reported above
|
|
547
|
-
|
|
774
|
+
const ace = resolveEffectiveAce(effective, descriptor, name, "", name, "this repository", drifts);
|
|
775
|
+
if (!ace) continue; // already reported by resolveEffectiveAce()
|
|
776
|
+
checkAgentBits(name, ace, expected, strictMainBranchRules, drifts);
|
|
548
777
|
}
|
|
549
778
|
}
|
|
550
779
|
|
|
@@ -580,8 +809,7 @@ async function resolveBuildServiceDescriptor(namespaceId: string, projectId: str
|
|
|
580
809
|
const result = await fetchAccessControlLists(namespaceId, `repoV2/${projectId}`);
|
|
581
810
|
for (const acl of result.value ?? []) {
|
|
582
811
|
for (const descriptor of Object.keys(acl.acesDictionary ?? {})) {
|
|
583
|
-
if (descriptor
|
|
584
|
-
return descriptor;
|
|
812
|
+
if (isProjectBuildServiceDescriptor(descriptor, projectId)) return descriptor;
|
|
585
813
|
}
|
|
586
814
|
}
|
|
587
815
|
return undefined;
|
|
@@ -597,6 +825,7 @@ async function checkBuildServicePermissions(
|
|
|
597
825
|
namespaceId: string,
|
|
598
826
|
projectId: string | undefined,
|
|
599
827
|
repositoryId: string,
|
|
828
|
+
strictMainBranchRules: boolean,
|
|
600
829
|
drifts: RepoDoctorDrift[],
|
|
601
830
|
): Promise<void> {
|
|
602
831
|
if (!projectId) return; // already reported by checkAgentPermissions()
|
|
@@ -610,40 +839,73 @@ async function checkBuildServicePermissions(
|
|
|
610
839
|
return;
|
|
611
840
|
}
|
|
612
841
|
|
|
842
|
+
// Deny-aware throughout (effectiveAllowBits()'s own doc comment): a
|
|
843
|
+
// single descriptor can itself carry both an allow and a deny
|
|
844
|
+
// bit (effectiveAllow/effectiveDeny are independent fields on the same
|
|
845
|
+
// ACE) — the build service's own group-membership situation is different
|
|
846
|
+
// from the agents' (live evidence: it holds direct ACEs everywhere this
|
|
847
|
+
// function checks, no group expansion needed), but the SAME deny-wins
|
|
848
|
+
// arithmetic applies regardless of where the bits came from.
|
|
613
849
|
const repoToken = `repoV2/${projectId}/${repositoryId}`;
|
|
614
|
-
const repoEffective = (
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
850
|
+
const repoEffective = resolveEffectiveAce(
|
|
851
|
+
await fetchEffectivePermissions(namespaceId, repoToken, [descriptor]),
|
|
852
|
+
descriptor,
|
|
853
|
+
"build-service",
|
|
854
|
+
"",
|
|
855
|
+
"the project build service",
|
|
856
|
+
"this repository",
|
|
857
|
+
drifts,
|
|
858
|
+
);
|
|
859
|
+
if (repoEffective) {
|
|
860
|
+
for (const bitName of REPO_STANDARD.permissions.buildService.repo.allow) {
|
|
861
|
+
if ((effectiveAllowBits(repoEffective) & GIT_REPO_PERMISSION_BIT[bitName]) === 0) {
|
|
862
|
+
drifts.push({
|
|
863
|
+
id: `permissions.build-service.missing-${bitName}`,
|
|
864
|
+
message: `the project build service does not effectively have ${bitName} on this repository`,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
624
867
|
}
|
|
625
868
|
}
|
|
626
869
|
|
|
627
870
|
const integrationToken = `${repoToken}/refs/heads/${refNameHexUtf16Le("integration")}`;
|
|
628
|
-
const integrationEffective = (
|
|
871
|
+
const integrationEffective = resolveEffectiveAce(
|
|
872
|
+
await fetchEffectivePermissions(namespaceId, integrationToken, [descriptor]),
|
|
629
873
|
descriptor,
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
874
|
+
"build-service",
|
|
875
|
+
"-integration",
|
|
876
|
+
"the project build service",
|
|
877
|
+
"refs/heads/integration",
|
|
878
|
+
drifts,
|
|
879
|
+
);
|
|
880
|
+
if (integrationEffective) {
|
|
881
|
+
for (const bitName of REPO_STANDARD.permissions.buildService.integrationRef.allow) {
|
|
882
|
+
if ((effectiveAllowBits(integrationEffective) & GIT_REPO_PERMISSION_BIT[bitName]) === 0) {
|
|
883
|
+
drifts.push({
|
|
884
|
+
id: `permissions.build-service.missing-${bitName}-integration`,
|
|
885
|
+
message: `the project build service does not effectively have ${bitName} on refs/heads/integration`,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
637
888
|
}
|
|
638
889
|
}
|
|
639
890
|
|
|
891
|
+
// Enact-only (STRICT_MAIN_BRANCH_PROJECT above): a repo outside Enact keeps
|
|
892
|
+
// whatever PolicyExempt the build service already has on main, flagged
|
|
893
|
+
// neither way — skip the ACL read entirely rather than fetch-and-ignore.
|
|
894
|
+
if (!strictMainBranchRules) return;
|
|
895
|
+
|
|
640
896
|
const mainToken = `${repoToken}/refs/heads/${refNameHexUtf16Le("main")}`;
|
|
641
|
-
const mainEffective = (
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
897
|
+
const mainEffective = resolveEffectiveAce(
|
|
898
|
+
await fetchEffectivePermissions(namespaceId, mainToken, [descriptor]),
|
|
899
|
+
descriptor,
|
|
900
|
+
"build-service",
|
|
901
|
+
"-main",
|
|
902
|
+
"the project build service",
|
|
903
|
+
"refs/heads/main",
|
|
904
|
+
drifts,
|
|
905
|
+
);
|
|
906
|
+
if (!mainEffective) return;
|
|
645
907
|
for (const bitName of REPO_STANDARD.permissions.buildService.mainRef.deny) {
|
|
646
|
-
if ((mainEffective
|
|
908
|
+
if ((effectiveAllowBits(mainEffective) & GIT_REPO_PERMISSION_BIT[bitName]) !== 0) {
|
|
647
909
|
drifts.push({
|
|
648
910
|
id: `permissions.build-service.has-${bitName}-main`,
|
|
649
911
|
message: `the project build service effectively has ${bitName} on refs/heads/main, which the standard forbids`,
|
|
@@ -672,6 +934,16 @@ function releaseFixPath(repository: string, identityDisplayName: string, bitName
|
|
|
672
934
|
return `Project settings → Repositories → \`${repository}\` → Security → \`${identityDisplayName}\` → \`${GIT_REPO_PERMISSION_UI_LABEL[bitName]}\` = Allow`;
|
|
673
935
|
}
|
|
674
936
|
|
|
937
|
+
/** Same as releaseFixPath() but for a bit that is granted per-BRANCH (verified live 2026-09-30: PolicyExempt on this org's build service lives on refs/heads/integration's own security page, never the repo-root one). */
|
|
938
|
+
function releaseFixPathForRef(
|
|
939
|
+
repository: string,
|
|
940
|
+
ref: string,
|
|
941
|
+
identityDisplayName: string,
|
|
942
|
+
bitName: PermissionBitName,
|
|
943
|
+
): string {
|
|
944
|
+
return `Repos → \`${repository}\` → Branches → \`${ref}\` → ⋯ → Branch security → \`${identityDisplayName}\` → \`${GIT_REPO_PERMISSION_UI_LABEL[bitName]}\` = Allow`;
|
|
945
|
+
}
|
|
946
|
+
|
|
675
947
|
/**
|
|
676
948
|
* Which build service identity actually runs the pipeline, per Build —
|
|
677
949
|
* General Settings — Get's `enforceJobAuthScope` (file header) — "Limit job
|
|
@@ -693,15 +965,21 @@ async function fetchEnforceJobAuthScope(c: AdoClient): Promise<boolean | undefin
|
|
|
693
965
|
|
|
694
966
|
/**
|
|
695
967
|
* One identity's answer to "can it release": resolve its descriptor by exact
|
|
696
|
-
* display name (Identities — Read Identities, file header), then check
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
968
|
+
* display name (Identities — Read Identities, file header), then check
|
|
969
|
+
* `buildServiceRelease.repoScoped` on the repository token AND
|
|
970
|
+
* `buildServiceRelease.integrationRefScoped` on the `integration` ref token
|
|
971
|
+
* — NOT the same token for all three (see REPO_STANDARD.permissions.
|
|
972
|
+
* buildServiceRelease's own doc comment: PolicyExempt is a per-BRANCH grant
|
|
973
|
+
* in this org, verified live against real bypass-on-push evidence, and
|
|
974
|
+
* checking it at repo scope produced a false "missing" finding). Every
|
|
975
|
+
* failure mode is its own loud, distinct drift — an unresolvable identity,
|
|
976
|
+
* an ACL the calling agent cannot read, and each missing permission named
|
|
977
|
+
* with its fix path — never a silent pass.
|
|
701
978
|
*/
|
|
702
979
|
async function checkOneBuildServiceCanRelease(
|
|
703
980
|
namespaceId: string,
|
|
704
981
|
repoToken: string,
|
|
982
|
+
integrationRefToken: string,
|
|
705
983
|
repository: string,
|
|
706
984
|
displayName: string,
|
|
707
985
|
drifts: RepoDoctorDrift[],
|
|
@@ -727,26 +1005,41 @@ async function checkOneBuildServiceCanRelease(
|
|
|
727
1005
|
return;
|
|
728
1006
|
}
|
|
729
1007
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1008
|
+
// LOW, independent review 2026-09-30: name WHICH token failed — "the ACL"
|
|
1009
|
+
// alone left a human unable to tell a repo-scope 403 from a
|
|
1010
|
+
// refs/heads/integration-scope one without re-running this by hand.
|
|
1011
|
+
async function effectiveAt(token: string, tokenLabel: string): Promise<{ allow: number; deny: number } | undefined> {
|
|
1012
|
+
let effective: EffectivePermissionsResult;
|
|
1013
|
+
try {
|
|
1014
|
+
effective = await fetchEffectivePermissions(namespaceId, token, [descriptor!]);
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1017
|
+
drifts.push({
|
|
1018
|
+
id: `permissions.release.${slug}.acl-read-forbidden`,
|
|
1019
|
+
message: `could not read the Git Repositories security ACL for '${displayName}' on '${repository}' (${tokenLabel} token) — the calling agent identity may lack rights to read repository security, so this cannot be confirmed either way: ${detail}`,
|
|
1020
|
+
});
|
|
1021
|
+
return undefined;
|
|
1022
|
+
}
|
|
1023
|
+
return resolveEffectiveAce(effective, descriptor!, `release.${slug}`, "", `'${displayName}'`, tokenLabel, drifts);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const repoEffective = await effectiveAt(repoToken, "repo-scope");
|
|
1027
|
+
if (!repoEffective) return;
|
|
1028
|
+
for (const bitName of REPO_STANDARD.permissions.buildServiceRelease.repoScoped) {
|
|
1029
|
+
if ((effectiveAllowBits(repoEffective) & GIT_REPO_PERMISSION_BIT[bitName]) !== 0) continue;
|
|
738
1030
|
drifts.push({
|
|
739
|
-
id: `permissions.release.${slug}.
|
|
740
|
-
message: `
|
|
1031
|
+
id: `permissions.release.${slug}.missing-${bitName}`,
|
|
1032
|
+
message: `'${displayName}' does not effectively have ${GIT_REPO_PERMISSION_UI_LABEL[bitName]} on '${repository}' — the release pipeline will 403 the way agent-identities build 4897 did. Fix: ${releaseFixPath(repository, displayName, bitName)}`,
|
|
741
1033
|
});
|
|
742
|
-
return;
|
|
743
1034
|
}
|
|
744
1035
|
|
|
745
|
-
|
|
746
|
-
|
|
1036
|
+
const integrationEffective = await effectiveAt(integrationRefToken, "refs/heads/integration");
|
|
1037
|
+
if (!integrationEffective) return;
|
|
1038
|
+
for (const bitName of REPO_STANDARD.permissions.buildServiceRelease.integrationRefScoped) {
|
|
1039
|
+
if ((effectiveAllowBits(integrationEffective) & GIT_REPO_PERMISSION_BIT[bitName]) !== 0) continue;
|
|
747
1040
|
drifts.push({
|
|
748
1041
|
id: `permissions.release.${slug}.missing-${bitName}`,
|
|
749
|
-
message: `'${displayName}' does not effectively have ${GIT_REPO_PERMISSION_UI_LABEL[bitName]} on '${repository}' —
|
|
1042
|
+
message: `'${displayName}' does not effectively have ${GIT_REPO_PERMISSION_UI_LABEL[bitName]} on 'refs/heads/integration' of '${repository}' — its own version-bump commit would be blocked by integration's own policies. Fix: ${releaseFixPathForRef(repository, "integration", displayName, bitName)}`,
|
|
750
1043
|
});
|
|
751
1044
|
}
|
|
752
1045
|
}
|
|
@@ -779,8 +1072,9 @@ async function checkBuildServiceCanRelease(
|
|
|
779
1072
|
: [projectScoped, collectionScoped];
|
|
780
1073
|
|
|
781
1074
|
const repoToken = `repoV2/${projectId}/${repositoryId}`;
|
|
1075
|
+
const integrationRefToken = `${repoToken}/refs/heads/${refNameHexUtf16Le("integration")}`;
|
|
782
1076
|
for (const displayName of displayNames) {
|
|
783
|
-
await checkOneBuildServiceCanRelease(namespaceId, repoToken, repository, displayName, drifts);
|
|
1077
|
+
await checkOneBuildServiceCanRelease(namespaceId, repoToken, integrationRefToken, repository, displayName, drifts);
|
|
784
1078
|
}
|
|
785
1079
|
}
|
|
786
1080
|
|
|
@@ -825,14 +1119,30 @@ export async function repoDoctorCheck(options: RepoDoctorOptions = {}): Promise<
|
|
|
825
1119
|
});
|
|
826
1120
|
}
|
|
827
1121
|
|
|
1122
|
+
const repositoryProjectName = repoInfo.project?.name;
|
|
1123
|
+
if (!repositoryProjectName) {
|
|
1124
|
+
drifts.push({
|
|
1125
|
+
id: "branches.no-project-name",
|
|
1126
|
+
message:
|
|
1127
|
+
"Git Repositories — Get Repository response did not include a project name; cannot determine whether the Enact-only main-branch rules (STRICT_MAIN_BRANCH_PROJECT) apply to this repository",
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
const strictMainBranchRules = repositoryProjectName ? strictMainBranchRulesApply(repositoryProjectName) : false;
|
|
1131
|
+
|
|
828
1132
|
const hasPipeline = await repositoryHasAPipeline(c, repositoryId);
|
|
829
1133
|
const integrationPolicies = await fetchBranchPolicies(c, repositoryId, "integration");
|
|
830
1134
|
const mainPolicies = await fetchBranchPolicies(c, repositoryId, "main");
|
|
831
1135
|
checkIntegrationPolicies(integrationPolicies, hasPipeline, drifts);
|
|
832
|
-
checkMainPolicies(mainPolicies, hasPipeline, drifts);
|
|
1136
|
+
checkMainPolicies(mainPolicies, hasPipeline, strictMainBranchRules, drifts);
|
|
833
1137
|
|
|
834
|
-
await checkAgentPermissions(projectId, repositoryId, drifts);
|
|
835
|
-
await checkBuildServicePermissions(
|
|
1138
|
+
await checkAgentPermissions(projectId, repositoryId, strictMainBranchRules, drifts);
|
|
1139
|
+
await checkBuildServicePermissions(
|
|
1140
|
+
REPO_STANDARD.permissions.namespaceId,
|
|
1141
|
+
projectId,
|
|
1142
|
+
repositoryId,
|
|
1143
|
+
strictMainBranchRules,
|
|
1144
|
+
drifts,
|
|
1145
|
+
);
|
|
836
1146
|
await checkBuildServiceCanRelease(c, REPO_STANDARD.permissions.namespaceId, projectId, repositoryId, repository, drifts);
|
|
837
1147
|
|
|
838
1148
|
return { repository, compliant: drifts.length === 0, drifts };
|