




















@@ -222,6 +222,14 @@ export function isDependencyGuardAuthorizedForHead(comment, currentHeadSha) {
222222);
223223}
224224225+export function isDependencyGuardTrustedForHead(comment, currentHeadSha) {
226+return (
227+Boolean(currentHeadSha) &&
228+comment?.body?.includes("### Dependency graph changes noted") === true &&
229+dependencyGuardCommentHeadSha(comment) === currentHeadSha
230+);
231+}
232+225233export function securityApproverSet(value) {
226234return new Set(
227235String(value ?? "")
@@ -277,7 +285,7 @@ export function renderAuthorizedDependencyComment(override) {
277285"",
278286"### Dependency graph change authorized",
279287"",
280-"This PR includes dependency graph changes. A member of `@openclaw/openclaw-secops` authorized this exact head SHA with `/allow-dependencies-change`.",
288+"This PR includes dependency graph changes. A repository admin or member of `@openclaw/openclaw-secops` authorized this exact head SHA with `/allow-dependencies-change`.",
281289"",
282290`- Approved SHA: ${markdownCode(override.sha)}`,
283291`- Approved by: @${sanitizeDisplayValue(override.login)}`,
@@ -289,6 +297,22 @@ export function renderAuthorizedDependencyComment(override) {
289297return lines.join("\n");
290298}
291299300+export function renderTrustedDependencyComment({ actor, headSha }) {
301+return [
302+dependencyGraphGuardMarker,
303+"",
304+"### Dependency graph changes noted",
305+"",
306+"This PR includes dependency graph changes. The dependency guard is informational because the PR author is a repository admin or a member of `@openclaw/openclaw-secops`.",
307+"",
308+`- Current SHA: ${markdownCode(headSha ?? "<head-sha>")}`,
309+`- Trusted actor: @${sanitizeDisplayValue(actor.login)}`,
310+`- Trusted role: ${markdownCode(actor.reason)}`,
311+"",
312+"Security review is still recommended before merge when the dependency graph change is intentional.",
313+].join("\n");
314+}
315+292316export function renderAutoscrubbedDependencyComment({ baseBranch, lockfileChanges, commitSha }) {
293317const safeBranch = sanitizeDisplayValue(baseBranch ?? "main");
294318const fileLines = lockfileChanges.map((path) => `- ${markdownCode(path)}`);
@@ -361,14 +385,14 @@ export function renderBlockedDependencyComment({
361385"",
362386"### Dependency graph changes are blocked",
363387"",
364-"OpenClaw does not accept dependency graph changes through PRs unless security explicitly authorizes the current head SHA. Dependency updates are generated internally by maintainers so external PRs cannot change the resolved graph.",
388+"OpenClaw does not accept dependency graph changes through PRs unless a repository admin or security explicitly authorizes the current head SHA. Dependency updates are generated internally by maintainers so external PRs cannot change the resolved graph.",
365389"",
366390"Detected dependency graph changes:",
367391 ...reasons,
368392 ...autoscrubLines,
369393 ...removalSteps,
370394"",
371-"If this PR intentionally needs a dependency graph change, ask a member of `@openclaw/openclaw-secops` to comment:",
395+"If this PR intentionally needs a dependency graph change, ask a repository admin or member of `@openclaw/openclaw-secops` to comment:",
372396"",
373397"```text",
374398allowDependenciesCommand,
@@ -415,6 +439,44 @@ function renderAutoscrubStatusLines(status) {
415439return [];
416440}
417441442+export function dependencyGuardTrustedActorCandidates({ pullRequest, event, currentHeadSha }) {
443+const eventHeadSha = event?.pull_request?.head?.sha;
444+const eventAfterSha = event?.after;
445+const eventMatchesCurrentHead =
446+Boolean(currentHeadSha) &&
447+(eventHeadSha === currentHeadSha || eventAfterSha === currentHeadSha);
448+if (!eventMatchesCurrentHead) {
449+return [];
450+}
451+const candidates = [];
452+const seen = new Set();
453+for (const [source, login] of [["pull request author", pullRequest?.user?.login]]) {
454+if (typeof login !== "string" || login.length === 0) {
455+continue;
456+}
457+const normalizedLogin = login.toLowerCase();
458+if (seen.has(normalizedLogin)) {
459+continue;
460+}
461+seen.add(normalizedLogin);
462+candidates.push({ login, source });
463+}
464+return candidates;
465+}
466+467+export async function findTrustedDependencyGuardActor({ candidates, isDependencyApprover }) {
468+for (const candidate of candidates) {
469+const role = await isDependencyApprover(candidate.login);
470+if (role) {
471+return {
472+login: candidate.login,
473+reason: `${candidate.source}; ${role}`,
474+};
475+}
476+}
477+return null;
478+}
479+418480function renderManifestChangeLine(change) {
419481return `- ${markdownCode(change.path)} changed ${change.fields.map(markdownCode).join(", ")}.`;
420482}
@@ -794,6 +856,98 @@ async function main() {
794856return;
795857}
796858859+const membershipCache = new Map();
860+const permissionCache = new Map();
861+const isSecurityMember = async (login) => {
862+const normalizedLogin = login.toLowerCase();
863+if (explicitSecurityApprovers.has(normalizedLogin)) {
864+return true;
865+}
866+if (membershipCache.has(normalizedLogin)) {
867+return membershipCache.get(normalizedLogin);
868+}
869+try {
870+const membership = await api.request(
871+`/orgs/${owner}/teams/${securityTeamSlug}/memberships/${encodeURIComponent(login)}`,
872+);
873+const allowed = membership?.state === "active";
874+membershipCache.set(normalizedLogin, allowed);
875+return allowed;
876+} catch (error) {
877+if (error?.status !== 404) {
878+console.warn(`Could not verify ${login} against ${securityTeamSlug}: ${error.message}`);
879+}
880+membershipCache.set(normalizedLogin, false);
881+return false;
882+}
883+};
884+const isRepositoryAdmin = async (login) => {
885+const normalizedLogin = login.toLowerCase();
886+if (permissionCache.has(normalizedLogin)) {
887+return permissionCache.get(normalizedLogin);
888+}
889+try {
890+const result = await api.request(
891+`/repos/${owner}/${repo}/collaborators/${encodeURIComponent(login)}/permission`,
892+);
893+const allowed = result?.permission === "admin";
894+permissionCache.set(normalizedLogin, allowed);
895+return allowed;
896+} catch (error) {
897+if (error?.status !== 404) {
898+console.warn(`Could not verify repository permission for ${login}: ${error.message}`);
899+}
900+permissionCache.set(normalizedLogin, false);
901+return false;
902+}
903+};
904+const isDependencyApprover = async (login) => {
905+if (await isSecurityMember(login)) {
906+return securityTeamSlug;
907+}
908+if (await isRepositoryAdmin(login)) {
909+return "repository admin";
910+}
911+return null;
912+};
913+const currentHeadSha = pullRequest.head?.sha;
914+if (isDependencyGuardTrustedForHead(existingGuardComment, currentHeadSha)) {
915+if (mode === "detect") {
916+await setOutput("autoscrub", "false");
917+}
918+await writeSummary(
919+[
920+"## Dependency Guard",
921+"",
922+`Dependency graph change remains informational for a trusted actor at ${markdownCode(currentHeadSha)}.`,
923+].join("\n"),
924+);
925+console.log("Dependency graph change remains informational for this head SHA.");
926+return;
927+}
928+const trustedActor = await findTrustedDependencyGuardActor({
929+candidates: dependencyGuardTrustedActorCandidates({ pullRequest, event, currentHeadSha }),
930+ isDependencyApprover,
931+});
932+if (trustedActor) {
933+if (mode === "detect") {
934+await setOutput("autoscrub", "false");
935+}
936+await upsertComment(
937+existingGuardComment,
938+renderTrustedDependencyComment({ actor: trustedActor, headSha: currentHeadSha }),
939+);
940+await writeSummary(
941+[
942+"## Dependency Guard",
943+"",
944+`Dependency graph change noted for trusted actor @${sanitizeDisplayValue(trustedActor.login)} and allowed to continue.`,
945+].join("\n"),
946+);
947+console.log("Dependency graph change noted for trusted actor; guard is informational.");
948+return;
949+}
950+797951const autoscrubCandidate = shouldAutoscrubDependencyLockfiles({
798952 dependencyFiles,
799953 lockfileChanges,
@@ -891,32 +1045,6 @@ async function main() {
8911045};
8921046}
8931047}
894-895-const membershipCache = new Map();
896-const isSecurityMember = async (login) => {
897-const normalizedLogin = login.toLowerCase();
898-if (explicitSecurityApprovers.size > 0) {
899-return explicitSecurityApprovers.has(normalizedLogin);
900-}
901-if (membershipCache.has(login)) {
902-return membershipCache.get(login);
903-}
904-try {
905-const membership = await api.request(
906-`/orgs/${owner}/teams/${securityTeamSlug}/memberships/${encodeURIComponent(login)}`,
907-);
908-const allowed = membership?.state === "active";
909-membershipCache.set(login, allowed);
910-return allowed;
911-} catch (error) {
912-if (error?.status !== 404) {
913-console.warn(`Could not verify ${login} against ${securityTeamSlug}: ${error.message}`);
914-}
915-membershipCache.set(login, false);
916-return false;
917-}
918-};
919-const currentHeadSha = pullRequest.head?.sha;
9201048if (isDependencyGuardAuthorizedForHead(existingGuardComment, currentHeadSha)) {
9211049await writeSummary(
9221050[
@@ -931,7 +1059,7 @@ async function main() {
9311059const override = await findDependencyOverrideCommandAsync({
9321060 comments,
9331061expectedSha: dependencyOverrideExpectedSha(existingGuardComment, currentHeadSha),
934- isSecurityMember,
1062+isSecurityMember: async (login) => Boolean(await isDependencyApprover(login)),
9351063newerThan: existingGuardComment?.updated_at ?? existingGuardComment?.created_at,
9361064});
9371065if (override) {
@@ -943,7 +1071,7 @@ async function main() {
9431071`Dependency graph change authorized by @${sanitizeDisplayValue(override.login)} for ${markdownCode(override.sha)}.`,
9441072].join("\n"),
9451073);
946-console.log("Dependency graph change authorized by security override.");
1074+console.log("Dependency graph change authorized by trusted override.");
9471075return;
9481076}
9491077@@ -958,9 +1086,11 @@ async function main() {
9581086}),
9591087);
9601088await writeSummary(
961-"## Dependency Guard\n\nDependency graph changes are blocked without a current secops override.",
1089+"## Dependency Guard\n\nDependency graph changes are blocked without a current admin or secops override.",
1090+);
1091+throw new Error(
1092+"Dependency graph changes require removal or a current admin or secops override.",
9621093);
963-throw new Error("Dependency graph changes require removal or a current secops override.");
9641094}
96510959661096if (import.meta.url === `file://${process.argv[1]}`) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。