fix: clarify pr preflight auth scopes

This commit is contained in:
Codex
2026-05-22 17:59:03 +00:00
parent 05b77151b8
commit b8b38bd44f
4 changed files with 172 additions and 12 deletions
@@ -342,6 +342,11 @@ async function main(): Promise<void> {
const fallbackLocalGap = asRecord(fallback.localObservationGap);
assertCondition(fallbackLocalGap.kind === "runner-local-observation-gap", "healthy remote fallback should classify local backend-core absence as runner-local observation gap", fallbackLocalGap);
assertCondition(fallbackLocalGap.schedulerStoppage === false, "local observation gap must not imply scheduler stoppage", fallbackLocalGap);
assertCondition(fallback.localObservation === undefined, "healthy remote fallback default output should omit full local observation", fallback);
assertCondition(fallback.remoteObservation === undefined, "healthy remote fallback default output should omit full remote observation", fallback);
assertCondition(asRecord(fallback.disclosure).fullObservationsOmitted === true, "healthy remote fallback should disclose omitted full observations", fallback.disclosure);
assertCondition(asRecord(fallback.localObservationSummary).failureKind === "target-stack-not-running", "healthy remote fallback should keep bounded local observation summary", fallback.localObservationSummary);
assertCondition(asRecord(fallback.remoteObservationSummary).ok === true, "healthy remote fallback should keep bounded remote observation summary", fallback.remoteObservationSummary);
const fallbackPreflight = asRecord(fallback.preflight);
assertCondition(fallbackPreflight.ok === true, "remote fallback preflight should stay ready", fallbackPreflight);
assertCondition(asRecord(fallbackPreflight.tokenCoverage).source === "GH_TOKEN", "token source should be GH_TOKEN", fallbackPreflight.tokenCoverage);
@@ -399,11 +404,45 @@ async function main(): Promise<void> {
assertCondition(directAuthMissingRecord.degradedReason === "GH_TOKEN/GITHUB_TOKEN missing", "auth missing should state token gap", directAuthMissingRecord);
const directAuthObservationGap = asRecord(directAuthMissingRecord.observationGap);
assertCondition(directAuthObservationGap.kind === "runner-local-observation-gap", "auth missing after remote fallback should keep local backend-core absence scoped as runner-local observation gap", directAuthObservationGap);
const directAuthSummary = asRecord(directAuthMissingRecord.authScopeSummary);
const directAuthScopeBoundary = asRecord(directAuthMissingRecord.scopeBoundary);
const directAuthActiveRunner = asRecord(directAuthMissingRecord.activeRunnerDevContainer);
const directAuthRecommendedActions = Array.isArray(directAuthMissingRecord.recommendedActions) ? directAuthMissingRecord.recommendedActions : [];
assertCondition(directAuthSummary.schedulerAuthMissingIsScoped === true, "remote auth-missing should lead with scheduler-scoped auth summary", directAuthSummary);
assertCondition(String(directAuthSummary.interpretation ?? "").includes("does not prove"), "remote auth summary must not imply active runner PR incapability", directAuthSummary);
assertCondition(directAuthScopeBoundary.scopesAreIndependent === true, "remote auth-missing must distinguish scheduler env from active runner dev container", directAuthScopeBoundary);
assertCondition(directAuthScopeBoundary.schedulerAuthMissingDoesNotMeanActiveRunnerCannotCreatePr === true, "remote auth-missing should expose the explicit PR capability boundary", directAuthScopeBoundary);
assertCondition(String(directAuthScopeBoundary.authMissingInterpretation ?? "").includes("do not simplify"), "remote auth-missing must warn against overbroad interpretation", directAuthScopeBoundary);
assertCondition(directAuthActiveRunner.notEquivalentToSchedulerEnv === true, "active runner token capability must be a separate scope", directAuthActiveRunner);
assertCondition(Array.isArray(directAuthMissingRecord.recommendedActions), "remote auth-missing should expose bounded recommended actions", directAuthMissingRecord.recommendedActions);
assertCondition(directAuthRecommendedActions.length === 3, "remote auth-missing recommended actions should stay bounded", directAuthRecommendedActions);
assertCondition(directAuthRecommendedActions.some((action) => asRecord(action).action === "verify-current-runner-auth"), "remote auth-missing should recommend active runner auth status first", directAuthRecommendedActions);
assertCondition(directAuthRecommendedActions.some((action) => String(asRecord(action).command ?? "").includes("gh pr create") && asRecord(action).writesRemote === false), "remote auth-missing should recommend PR create dry-run", directAuthRecommendedActions);
assertCondition(directAuthMissingRecord.localObservation === undefined, "auth-missing remote fallback default output should omit full local observation", directAuthMissingRecord);
assertCondition(directAuthMissingRecord.remoteObservation === undefined, "auth-missing remote fallback default output should omit full remote observation", directAuthMissingRecord);
assertCondition(asRecord(directAuthMissingRecord.disclosure).fullObservationsOmitted === true, "auth-missing remote fallback should disclose omitted full observations", directAuthMissingRecord.disclosure);
assertCondition(asRecord(directAuthMissingRecord.localObservationSummary).failureKind === "target-stack-not-running", "auth-missing remote fallback should keep bounded local observation summary", directAuthMissingRecord.localObservationSummary);
assertCondition(asRecord(asRecord(directAuthMissingRecord.remoteObservationSummary).tokenCoverage).scope === "scheduler-runner-env", "auth-missing remote fallback should keep bounded remote token scope", directAuthMissingRecord.remoteObservationSummary);
const directAuthMissingFull = await codexPrPreflightQueryForTest(["--remote", "--full"], {
config: { network: { publicHost: "74.48.78.17", frontend: { port: 18081 } } } as unknown as UniDeskConfig,
coreFetch: () => localBackendCoreMissingFixture(),
remoteMainServerPrPreflight: () => remoteControlPlaneResult({
ok: false,
failureKind: "auth-missing",
degradedReason: "GH_TOKEN/GITHUB_TOKEN missing",
runnerDisposition: "infra-blocked",
tokenCoverage: {
ok: false,
source: null,
missing: ["GH_TOKEN", "GITHUB_TOKEN"],
scope: "scheduler-runner-env",
},
}),
});
const directAuthMissingFullRecord = asRecord(directAuthMissingFull);
assertCondition(directAuthMissingFullRecord.localObservation !== undefined, "--full should retain full local observation", directAuthMissingFullRecord);
assertCondition(directAuthMissingFullRecord.remoteObservation !== undefined, "--full should retain full remote observation", directAuthMissingFullRecord);
const gitRemoteGap = remoteControlPlaneResult({
ok: false,
@@ -747,17 +786,30 @@ async function main(): Promise<void> {
assertCondition(missingTokenRecord.ok === false, "missing-token branch should fail", missingTokenRecord);
assertCondition(missingTokenRecord.failureKind === "auth-missing", "missing-token branch should classify auth-missing", missingTokenRecord);
assertCondition(missingTokenRecord.degradedReason === "auth-broker-needed", "missing-token branch should expose broker-needed degraded reason", missingTokenRecord);
const missingTokenTopSummary = asRecord(missingTokenRecord.authScopeSummary);
const missingTokenTopScopeBoundary = asRecord(missingTokenRecord.scopeBoundary);
const missingTokenTopActions = Array.isArray(missingTokenRecord.recommendedActions) ? missingTokenRecord.recommendedActions : [];
const missingTokenPreflight = asRecord(missingTokenRecord.preflight);
const missingTokenAuthBroker = asRecord(missingTokenPreflight.authBroker);
const missingTokenSummary = asRecord(missingTokenPreflight.authScopeSummary);
const missingTokenScopeBoundary = asRecord(missingTokenPreflight.scopeBoundary);
const missingTokenActiveRunner = asRecord(missingTokenPreflight.activeRunnerDevContainer);
const missingTokenActions = Array.isArray(missingTokenPreflight.recommendedActions) ? missingTokenPreflight.recommendedActions : [];
const missingTokenCapability = asRecord(missingTokenAuthBroker.capability);
assertCondition(missingTokenTopSummary.schedulerAuthMissingIsScoped === true, "missing-token top-level summary should expose scoped scheduler auth missing", missingTokenTopSummary);
assertCondition(missingTokenTopScopeBoundary.schedulerAuthMissingDoesNotMeanActiveRunnerCannotCreatePr === true, "missing-token top-level boundary should be prominent", missingTokenTopScopeBoundary);
assertCondition(Array.isArray(missingTokenRecord.recommendedActions) && missingTokenTopActions.length === 3, "missing-token top-level recommended actions should stay bounded", missingTokenRecord.recommendedActions);
assertCondition(missingTokenAuthBroker.source === "broker/auth-broker-needed", "missing-token branch should expose broker/auth-broker-needed", missingTokenAuthBroker);
assertCondition(missingTokenAuthBroker.nextAction === "configure-auth-broker-or-env-token", "missing-token branch should expose nextAction", missingTokenAuthBroker);
assertCondition(missingTokenCapability.source === "missing-token", "missing-token branch should expose missing-token capability", missingTokenCapability);
assertCondition(missingTokenCapability.systemGhBinaryRequiredForWrites === false, "missing-token branch should still not require system gh binary for UniDesk gh CLI", missingTokenCapability);
assertCondition(missingTokenSummary.schedulerAuthMissingIsScoped === true, "missing-token compact preflight should expose scoped scheduler auth missing", missingTokenSummary);
assertCondition(String(missingTokenScopeBoundary.currentRunnerCheck ?? "").includes("gh auth status"), "missing-token branch should point to active runner auth check", missingTokenScopeBoundary);
assertCondition(String(missingTokenScopeBoundary.currentRunnerCheck ?? "").includes("gh pr create --dry-run"), "missing-token branch should point to active runner PR create dry-run", missingTokenScopeBoundary);
assertCondition(missingTokenActiveRunner.relationToRemotePreflight === "independent-scope; scheduler-runner-env auth-missing does not prove the active runner/dev container lacks GitHub PR capability", "missing-token branch should not overstate active runner PR capability", missingTokenActiveRunner);
assertCondition(Array.isArray(missingTokenPreflight.recommendedActions) && missingTokenActions.length === 3, "missing-token compact recommended actions should stay bounded", missingTokenPreflight.recommendedActions);
assertCondition(missingTokenActions.some((action) => String(asRecord(action).command ?? "").includes("gh auth status")), "missing-token branch should recommend gh auth status", missingTokenActions);
assertCondition(missingTokenActions.some((action) => String(asRecord(action).command ?? "").includes("gh pr create") && asRecord(action).writesRemote === false), "missing-token branch should recommend PR create dry-run without writes", missingTokenActions);
process.stdout.write(`${JSON.stringify({
ok: true,
+118 -10
View File
@@ -2897,25 +2897,74 @@ function activeRunnerDevContainerCapability(): Record<string, unknown> {
function prPreflightScopeBoundary(tokenCoverage: Record<string, unknown> | null): Record<string, unknown> {
const schedulerScope = typeof tokenCoverage?.scope === "string" ? tokenCoverage.scope : "scheduler-runner-env";
return {
headline: "scheduler-runner-env auth-missing is not active runner/dev container PR incapability",
schedulerPreflightScope: schedulerScope,
activeRunnerDevContainerScope: "current-cli-process",
scopesAreIndependent: true,
schedulerAuthMissingDoesNotMeanActiveRunnerCannotCreatePr: true,
authMissingInterpretation: "auth-missing from codex pr-preflight --remote is scoped to the scheduler/runtime preflight surface; do not simplify it to 'the active runner cannot create PRs'",
currentRunnerCheck: "use activeRunnerDevContainer plus bun scripts/cli.ts gh auth status --repo pikasTech/unidesk for the current dev container",
currentRunnerCheck: "use activeRunnerDevContainer plus bun scripts/cli.ts gh auth status --repo pikasTech/unidesk and gh pr create --dry-run for the current dev container",
valuesPrinted: false,
};
}
function prPreflightRecommendedActions(tokenCoverage: Record<string, unknown> | null): Record<string, unknown>[] {
const schedulerReady = tokenCoverage?.ok === true;
return [
{
scope: "active-runner-dev-container",
priority: "first",
action: "verify-current-runner-auth",
command: "bun scripts/cli.ts gh auth status --repo pikasTech/unidesk",
writesRemote: false,
},
{
scope: "active-runner-dev-container",
priority: "first",
action: "verify-current-runner-pr-create-plan",
command: "bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --title <title> --body-file <file> --base master --head <head> --dry-run",
writesRemote: false,
},
{
scope: "scheduler-runtime",
priority: schedulerReady ? "long-term" : "required-for-scheduler-preflight",
action: "configure-scheduler-auth-source",
command: "configure auth-broker or inject GH_TOKEN/GITHUB_TOKEN via the scheduler runtime secret",
writesRemote: false,
},
];
}
function prPreflightAuthScopeSummary(tokenCoverage: Record<string, unknown> | null, activeRunnerDevContainer: Record<string, unknown>): Record<string, unknown> {
const schedulerReady = tokenCoverage?.ok === true;
return {
schedulerPreflightScope: typeof tokenCoverage?.scope === "string" ? tokenCoverage.scope : "scheduler-runner-env",
schedulerAuthReady: schedulerReady,
schedulerAuthSource: tokenCoverage?.source ?? null,
schedulerAuthMissingIsScoped: !schedulerReady,
activeRunnerDevContainerScope: activeRunnerDevContainer.scope ?? "current-cli-process",
activeRunnerTokenCandidatePresent: activeRunnerDevContainer.ok === true,
interpretation: schedulerReady
? "scheduler preflight has GitHub auth coverage; active runner PR creation still needs repo-native dry-run verification before real writes"
: "scheduler-runner-env auth-missing does not prove the active runner/dev container lacks PR create/comment capability",
valuesPrinted: false,
};
}
function decoratePrPreflightScopeBoundary(record: Record<string, unknown>): Record<string, unknown> {
const preflight = asRecord(record.preflight);
const tokenCoverage = asRecord(preflight?.tokenCoverage ?? record.tokenCoverage);
const tokenCoverage = asRecord(record.tokenCoverage) ?? asRecord(preflight?.tokenCoverage);
const scopeBoundary = prPreflightScopeBoundary(tokenCoverage);
const activeRunnerDevContainer = activeRunnerDevContainerCapability();
const authScopeSummary = prPreflightAuthScopeSummary(tokenCoverage, activeRunnerDevContainer);
const recommendedActions = prPreflightRecommendedActions(tokenCoverage);
return {
...record,
authScopeSummary,
scopeBoundary,
activeRunnerDevContainer,
...(preflight === null ? {} : { preflight: { ...preflight, scopeBoundary } }),
recommendedActions,
...(preflight === null ? {} : { preflight: { ...preflight, authScopeSummary, scopeBoundary, recommendedActions } }),
};
}
@@ -2943,6 +2992,53 @@ function prPreflightObservationGap(kind: Exclude<CodeQueueObservationGapKind, nu
};
}
function prPreflightObservationSummary(record: Record<string, unknown> | null): Record<string, unknown> | null {
if (record === null) return null;
const preflight = asRecord(record.preflight);
const tokenCoverage = asRecord(record.tokenCoverage) ?? asRecord(preflight?.tokenCoverage);
const controlPlane = asRecord(record.controlPlane);
const targetStack = asRecord(record.targetStack);
return {
ok: record.ok ?? null,
runnerDisposition: record.runnerDisposition ?? null,
failureKind: record.failureKind ?? null,
degradedReason: record.degradedReason ?? null,
controlPlane: controlPlane === null ? null : {
mode: controlPlane.mode ?? null,
localBackendCoreMissing: controlPlane.localBackendCoreMissing ?? null,
remoteFallbackUsed: controlPlane.remoteFallbackUsed ?? null,
},
targetStack: targetStack === null ? null : {
missingContainers: Array.isArray(targetStack.missingContainers) ? targetStack.missingContainers.map(String) : [],
verifyOnlyObserved: targetStack.verifyOnlyObserved ?? false,
},
tokenCoverage: tokenCoverage === null ? null : {
ok: tokenCoverage.ok ?? null,
source: tokenCoverage.source ?? null,
credentialSource: tokenCoverage.credentialSource ?? null,
scope: tokenCoverage.scope ?? null,
missing: Array.isArray(tokenCoverage.missing) ? tokenCoverage.missing.map(String) : [],
},
};
}
function maybeFullPrPreflightObservations(options: CodexPrPreflightOptions, localRecord: Record<string, unknown>, remoteRecord: Record<string, unknown>): Record<string, unknown> {
if (options.full) {
return {
localObservation: localRecord,
remoteObservation: remoteRecord,
};
}
return {
localObservationSummary: prPreflightObservationSummary(localRecord),
remoteObservationSummary: prPreflightObservationSummary(remoteRecord),
disclosure: {
fullObservationsOmitted: true,
expandWith: "bun scripts/cli.ts codex pr-preflight --remote --full",
},
};
}
function compactPrRuntimePreflight(preflight: Record<string, unknown>, options: CodexPrPreflightOptions): Record<string, unknown> {
const pull = asRecord(preflight.pullRequestDelivery) ?? {};
const tools = asRecord(pull.tools) ?? {};
@@ -2978,6 +3074,10 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
const pushDryRunRef = options.pushDryRunRef ?? defaultPushDryRunRef;
const targetBranch = "master";
const expectedHeadBranch = options.prCreateDryRunHead ?? (typeof git.branch === "string" && git.branch.length > 0 ? git.branch : "<head-branch>");
const scopeBoundary = prPreflightScopeBoundary(tokenCoverage);
const activeRunnerDevContainer = activeRunnerDevContainerCapability();
const authScopeSummary = prPreflightAuthScopeSummary(tokenCoverage, activeRunnerDevContainer);
const recommendedActions = prPreflightRecommendedActions(tokenCoverage);
const result: Record<string, unknown> = {
ok,
failureKind,
@@ -2992,7 +3092,9 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
},
tokenCoverage,
authBroker: authBrokerNeededStatus(tokenCoverage, authBrokerRuntime, systemGhBinary, unideskGhCli),
scopeBoundary: prPreflightScopeBoundary(tokenCoverage),
authScopeSummary,
scopeBoundary,
recommendedActions,
prCapabilityContract: {
targetBranch,
tokenSource: tokenCoverage.source,
@@ -3089,12 +3191,12 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
limitations,
risks,
runnerDisposition: ok ? "ready" : "infra-blocked",
activeRunnerDevContainer: activeRunnerDevContainerCapability(),
activeRunnerDevContainer,
recoveryHint: ok
? tokenCoverage.source === "auth-broker"
? "Runner PR workflow has auth-broker coverage for GitHub REST preflight; real PR creation still requires commander authorization."
: "Runner PR workflow has env-token coverage for the scheduler."
: "Scheduler preflight lacks GitHub auth coverage. Configure auth-broker or inject GH_TOKEN/GITHUB_TOKEN into the scheduler runtime secret for scheduler-scoped admission; separately check activeRunnerDevContainer and gh auth status before declaring the current runner unable to create/comment PRs.",
: "Scheduler preflight lacks GitHub auth coverage for scheduler-scoped admission only. First verify the active task with repo-native gh auth status and PR create dry-run; long-term configure auth-broker or inject GH_TOKEN/GITHUB_TOKEN into the scheduler runtime secret.",
commands: {
local: "bun scripts/cli.ts gh auth status --repo pikasTech/unidesk",
runner: "bun scripts/cli.ts codex pr-preflight --remote",
@@ -3230,8 +3332,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP
localBackendCoreMissing: true,
remoteFallbackUsed: true,
},
localObservation: localRecord,
remoteObservation: remoteRecord,
...maybeFullPrPreflightObservations(options, localRecord, remoteRecord),
});
}
return decoratePrPreflightScopeBoundary({
@@ -3250,8 +3351,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP
localBackendCoreMissing: true,
remoteFallbackUsed: true,
},
localObservation: localRecord,
remoteObservation: remoteRecord,
...maybeFullPrPreflightObservations(options, localRecord, remoteRecord),
});
}
}
@@ -3347,6 +3447,10 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP
runnerDisposition: compact.runnerDisposition,
failureKind: compact.failureKind ?? null,
degradedReason: compact.degradedReason ?? null,
authScopeSummary: compact.authScopeSummary,
scopeBoundary: compact.scopeBoundary,
activeRunnerDevContainer: compact.activeRunnerDevContainer,
recommendedActions: compact.recommendedActions,
upstream: { ok: response.ok, status: response.status },
controlPlane: {
mode: "local-backend-core",
@@ -3399,6 +3503,10 @@ export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher:
runnerDisposition: compact.runnerDisposition,
failureKind: compact.failureKind ?? null,
degradedReason: compact.degradedReason ?? null,
authScopeSummary: compact.authScopeSummary,
scopeBoundary: compact.scopeBoundary,
activeRunnerDevContainer: compact.activeRunnerDevContainer,
recommendedActions: compact.recommendedActions,
upstream: { ok: response.ok, status: response.status },
controlPlane: {
mode: "remote-frontend",