feat(auth-broker): register dry-run service surface

This commit is contained in:
Codex
2026-05-21 14:37:01 +00:00
parent 7e171dd904
commit ce1f7dc8c4
9 changed files with 511 additions and 6 deletions
+60
View File
@@ -111,6 +111,7 @@ const defaultOptions: ArtifactRegistryOptions = {
deployJsonService: null,
};
const supportedArtifactConsumerServices = [
"auth-broker",
"backend-core",
"baidu-netdisk",
"claudeqq",
@@ -259,6 +260,43 @@ const baiduNetdiskAuthHealthGate: AuthHealthGate = {
};
const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
"auth-broker": {
serviceId: "auth-broker",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/auth-broker",
dockerfile: "src/components/microservices/auth-broker/Dockerfile",
prodLiveApply: "supervisor-only",
prodLiveBlockReason: "auth-broker is registered for source/contract/profile dry-run only; live production apply requires a separate credential mounting and exposure review.",
devLiveApply: "supervisor-only",
devLiveBlockReason: "auth-broker DEV live apply requires explicit operator authorization after reviewing credential mounting, private exposure, and dry-run evidence.",
targets: {
dev: {
targetImage: "auth-broker",
targetCommitImage: (commit: string) => `auth-broker:${commit}`,
deployRef: "deploy.json#environments.dev.services.auth-broker",
compose: {
serviceName: "auth-broker",
containerName: "auth-broker-backend",
deployEnvPrefix: "UNIDESK_AUTH_BROKER_DEPLOY",
healthProbeCommand: "auth-broker --healthcheck",
requireHealthCommit: false,
},
},
prod: {
targetImage: "auth-broker",
targetCommitImage: (commit: string) => `auth-broker:${commit}`,
deployRef: "deploy.json#environments.prod.services.auth-broker",
compose: {
serviceName: "auth-broker",
containerName: "auth-broker-backend",
deployEnvPrefix: "UNIDESK_AUTH_BROKER_DEPLOY",
healthProbeCommand: "auth-broker --healthcheck",
requireHealthCommit: false,
},
},
},
},
"backend-core": {
serviceId: "backend-core",
environment: "prod",
@@ -1435,6 +1473,27 @@ function codeQueueMgrSelfBootstrapGuard(environment: ArtifactDeployEnvironment,
};
}
function authBrokerCredentialMountGuard(environment: ArtifactDeployEnvironment, requiresSupervisorApproval: boolean): Record<string, unknown> {
return {
check: "auth-broker-credential-mount-guard",
serviceId: "auth-broker",
requiresSupervisorApproval,
actorBoundary: "dry-run and contract evidence are allowed, but live broker startup needs explicit review of credential reference mounting and private exposure",
targetScope: "main-server Compose profile auth-broker / container auth-broker-backend only",
composeProfileRequired: "auth-broker",
publicPortAllowed: false,
registryCredentialsProxied: false,
deployCredentialsProxied: false,
environment,
forbiddenActions: [
"write real GitHub token into config.json, deploy.json, or docker-compose.yml",
"publish auth-broker on a public port",
"restart backend-core, provider-gateway, or Code Queue as part of broker dry-run registration",
"grant registry, deploy, database, k3s, provider token, or host SSH permissions",
],
};
}
function artifactConsumerSelfBootstrapGuard(
spec: ArtifactConsumerSpec,
environment: ArtifactDeployEnvironment,
@@ -1442,6 +1501,7 @@ function artifactConsumerSelfBootstrapGuard(
): Record<string, unknown> | undefined {
if (spec.serviceId === "code-queue") return codeQueueSelfBootstrapGuard(environment);
if (spec.serviceId === "code-queue-mgr") return codeQueueMgrSelfBootstrapGuard(environment, requiresSupervisorApproval);
if (spec.serviceId === "auth-broker") return authBrokerCredentialMountGuard(environment, requiresSupervisorApproval);
return undefined;
}
+244 -2
View File
@@ -1,7 +1,13 @@
import { readFileSync } from "node:fs";
import { type UniDeskConfig, readConfig, rootPath } from "./config";
const DEFAULT_REPO = "pikasTech/unidesk";
const DEFAULT_BASE = "master";
const SECRET_ENV_KEYS = ["GH_TOKEN", "GITHUB_TOKEN"] as const;
const BROKER_URL_ENV_KEYS = ["UNIDESK_AUTH_BROKER_URL", "AUTH_BROKER_URL"] as const;
const BROKER_CREDENTIAL_REF_ENV_KEYS = ["UNIDESK_AUTH_BROKER_GITHUB_CREDENTIAL_REF", "AUTH_BROKER_GITHUB_CREDENTIAL_REF"] as const;
const BROKER_CONFIGURED_ENV_KEYS = ["UNIDESK_AUTH_BROKER_GITHUB_CONFIGURED", "AUTH_BROKER_GITHUB_CONFIGURED"] as const;
const AUTH_BROKER_SERVICE_ID = "auth-broker";
const DEFAULT_CAPABILITIES = [
"github.auth.status",
"github.issue.list",
@@ -15,6 +21,7 @@ const DEFAULT_CAPABILITIES = [
type BrokerCommand = "contract" | "credential-request" | "pr-preflight" | "health";
type RunnerDisposition = "ready" | "infra-blocked" | "business-failed";
type ConfigSource = "config.json" | "unavailable";
interface BrokerAdapterOptions {
command: BrokerCommand;
@@ -27,10 +34,76 @@ interface BrokerAdapterOptions {
issueNumber: number | null;
}
interface AuthBrokerServiceRegistration {
ok: boolean;
serviceId: "auth-broker";
source: ConfigSource;
configured: boolean;
providerId: string | null;
deploymentMode: string | null;
proxyMode: string | null;
backendBaseUrl: string | null;
healthPath: string | null;
allowedPathPrefixes: string[];
composeService: string | null;
containerName: string | null;
dockerfile: string | null;
public: boolean | null;
error: string | null;
}
interface ComposeProfileRegistration {
ok: boolean;
source: "docker-compose.yml";
serviceId: "auth-broker";
servicePresent: boolean;
profileGated: boolean;
profiles: string[];
restart: string | null;
publicPortPublished: boolean;
exposesPort: boolean;
mutatesDefaultRuntime: false;
}
interface DeployManifestRegistration {
ok: boolean;
source: "deploy.json";
serviceId: "auth-broker";
prod: { present: boolean; commitId: string | null };
dev: { present: boolean; commitId: string | null };
dryRunOnly: true;
}
interface RuntimeCredentialRefPresence {
ok: boolean;
source: string | null;
configuredFlag: {
present: boolean;
key: string | null;
truthy: boolean;
};
credentialRef: {
present: boolean;
key: string | null;
valuePrinted: false;
valuePreview: string | null;
};
presenceOnly: true;
valuesRead: false;
valuesPrinted: false;
}
function hasEnvKey(name: string): boolean {
return Object.prototype.hasOwnProperty.call(process.env, name);
}
function firstPresentEnvKey(keys: readonly string[]): string | null {
for (const key of keys) {
if (hasEnvKey(key)) return key;
}
return null;
}
function stringOption(args: string[], name: string): string | undefined {
const index = args.indexOf(name);
if (index === -1) return undefined;
@@ -52,6 +125,14 @@ function sanitizeEndpoint(value: string): string {
}
}
function sanitizeCredentialRef(value: string): string {
const trimmed = value.trim();
if (trimmed.length === 0) return "<empty>";
const separator = trimmed.indexOf(":");
if (separator <= 0) return "<credential-ref>";
return `${trimmed.slice(0, separator)}:<ref>`;
}
function numberOption(args: string[], name: string): number | null {
const raw = stringOption(args, name);
if (raw === undefined) return null;
@@ -67,6 +148,160 @@ function firstConfiguredBrokerUrl(): string | null {
return null;
}
function envTruthy(key: string | null): boolean {
if (key === null) return false;
const value = process.env[key]?.trim().toLowerCase();
return value === "1" || value === "true" || value === "yes" || value === "on";
}
function runtimeCredentialRefPresence(): RuntimeCredentialRefPresence {
const configuredKey = firstPresentEnvKey(BROKER_CONFIGURED_ENV_KEYS);
const refKey = firstPresentEnvKey(BROKER_CREDENTIAL_REF_ENV_KEYS);
return {
ok: configuredKey !== null && envTruthy(configuredKey) && refKey !== null,
source: refKey === null ? null : "broker-held-github-credential-ref",
configuredFlag: {
present: configuredKey !== null,
key: configuredKey,
truthy: envTruthy(configuredKey),
},
credentialRef: {
present: refKey !== null,
key: refKey,
valuePrinted: false,
valuePreview: refKey === null ? null : sanitizeCredentialRef(process.env[refKey] ?? ""),
},
presenceOnly: true,
valuesRead: false,
valuesPrinted: false,
};
}
function readConfigRegistration(): AuthBrokerServiceRegistration {
let config: UniDeskConfig;
try {
config = readConfig();
} catch (error) {
return {
ok: false,
serviceId: AUTH_BROKER_SERVICE_ID,
source: "unavailable",
configured: false,
providerId: null,
deploymentMode: null,
proxyMode: null,
backendBaseUrl: null,
healthPath: null,
allowedPathPrefixes: [],
composeService: null,
containerName: null,
dockerfile: null,
public: null,
error: error instanceof Error ? error.message : String(error),
};
}
const service = config.microservices.find((item) => item.id === AUTH_BROKER_SERVICE_ID);
return {
ok: service !== undefined,
serviceId: AUTH_BROKER_SERVICE_ID,
source: "config.json",
configured: service !== undefined,
providerId: service?.providerId ?? null,
deploymentMode: service?.deployment.mode ?? null,
proxyMode: service?.backend.proxyMode ?? null,
backendBaseUrl: service?.backend.nodeBaseUrl ?? null,
healthPath: service?.backend.healthPath ?? null,
allowedPathPrefixes: service?.backend.allowedPathPrefixes ?? [],
composeService: service?.repository.composeService ?? null,
containerName: service?.repository.containerName ?? null,
dockerfile: service?.repository.dockerfile ?? null,
public: service?.backend.public ?? null,
error: service === undefined ? "auth-broker is not registered in config.json microservices" : null,
};
}
function extractComposeServiceBlock(composeText: string, serviceName: string): string {
const lines = composeText.split("\n");
const startLine = lines.findIndex((line) => line === ` ${serviceName}:`);
if (startLine < 0) return "";
let endLine = lines.length;
for (let index = startLine + 1; index < lines.length; index += 1) {
if (/^ [A-Za-z0-9][A-Za-z0-9_-]*:$/u.test(lines[index] ?? "")) {
endLine = index;
break;
}
}
return lines.slice(startLine, endLine).join("\n");
}
function composeProfileRegistration(): ComposeProfileRegistration {
const composeText = readFileSync(rootPath("docker-compose.yml"), "utf8");
const block = extractComposeServiceBlock(composeText, AUTH_BROKER_SERVICE_ID);
const blockLines = block.split("\n");
const profiles: string[] = [];
const profileStart = blockLines.findIndex((line) => /^\s{4}profiles:\s*$/u.test(line));
if (profileStart >= 0) {
for (let index = profileStart + 1; index < blockLines.length; index += 1) {
const line = blockLines[index] ?? "";
if (/^\s{4}[A-Za-z0-9_-]+:/u.test(line)) break;
const profile = line.match(/^\s{6}-\s+"?([A-Za-z0-9_.-]+)"?\s*$/u)?.[1];
if (profile !== undefined) profiles.push(profile);
}
}
const restart = block.match(/^\s{4}restart:\s+"?([^"\n]+)"?\s*$/mu)?.[1] ?? null;
const portsSection = /^\s{4}ports:\s*$/mu.test(block);
const portMapping = /^\s{6}-\s+["']?[^"'\n]*4291:/mu.test(block);
return {
ok: block.length > 0 && profiles.includes(AUTH_BROKER_SERVICE_ID) && !portsSection && !portMapping,
source: "docker-compose.yml",
serviceId: AUTH_BROKER_SERVICE_ID,
servicePresent: block.length > 0,
profileGated: profiles.includes(AUTH_BROKER_SERVICE_ID),
profiles,
restart,
publicPortPublished: portsSection || portMapping,
exposesPort: block.includes('"4291"'),
mutatesDefaultRuntime: false,
};
}
function serviceCommitFromDeployJson(environment: "prod" | "dev"): string | null {
const parsed = JSON.parse(readFileSync(rootPath("deploy.json"), "utf8")) as {
environments?: Record<string, { services?: Array<{ id?: unknown; commitId?: unknown }> }>;
};
const service = parsed.environments?.[environment]?.services?.find((item) => item.id === AUTH_BROKER_SERVICE_ID);
return typeof service?.commitId === "string" ? service.commitId : null;
}
function deployManifestRegistration(): DeployManifestRegistration {
const prodCommit = serviceCommitFromDeployJson("prod");
const devCommit = serviceCommitFromDeployJson("dev");
return {
ok: prodCommit !== null && devCommit !== null,
source: "deploy.json",
serviceId: AUTH_BROKER_SERVICE_ID,
prod: { present: prodCommit !== null, commitId: prodCommit },
dev: { present: devCommit !== null, commitId: devCommit },
dryRunOnly: true,
};
}
function serviceRegistrationContract(): Record<string, unknown> {
return {
config: readConfigRegistration(),
compose: composeProfileRegistration(),
deploy: deployManifestRegistration(),
runtimeCredentialRef: runtimeCredentialRefPresence(),
defaultRuntimeMutation: {
ok: true,
mutatesCurrentProd: false,
composeProfileRequired: AUTH_BROKER_SERVICE_ID,
publicPortPublished: false,
liveDeployPerformed: false,
},
};
}
function parseOptions(args: string[]): BrokerAdapterOptions {
const rawCommand = args[0] ?? "contract";
if (!["contract", "credential-request", "pr-preflight", "health"].includes(rawCommand)) {
@@ -101,11 +336,14 @@ function runnerEnvTokenCoverage(): Record<string, unknown> {
}
function brokerCoverage(endpoint: string | null): Record<string, unknown> {
const credential = runtimeCredentialRefPresence();
return {
ok: endpoint !== null,
source: endpoint === null ? null : "auth-broker",
endpoint: endpoint ?? null,
credentialRef: endpoint === null ? null : "github:unidesk-dev",
credentialRef: endpoint === null ? null : credential.credentialRef.valuePreview ?? "github:<ref>",
credentialRefPresent: credential.credentialRef.present,
credentialRefSourceKey: credential.credentialRef.key,
scope: endpoint === null ? null : "broker-held-github-credential",
runnerEnvTokenRequired: false,
valuesRead: false,
@@ -124,6 +362,7 @@ function brokerNeededResult(options: BrokerAdapterOptions): Record<string, unkno
message: "No auth broker endpoint is configured for this dry-run contract; runner env token coverage is reported only for migration diagnostics.",
tokenCoverage: runnerEnvTokenCoverage(),
brokerCoverage: brokerCoverage(options.endpoint),
serviceRegistration: serviceRegistrationContract(),
authBroker: {
ok: false,
source: "broker/auth-broker-needed",
@@ -158,7 +397,7 @@ function auditEventShape(options: BrokerAdapterOptions): Record<string, unknown>
? { base: options.base, head: options.head, issueNumber: options.issueNumber }
: null,
dryRun: true,
credentialRef: "github:unidesk-dev",
credentialRef: "github:<ref>",
credentialKind: "github-rest-token-ref",
credentialValuePrinted: false,
upstream: { method: "planned", path: "planned GitHub REST path without query secrets" },
@@ -218,6 +457,7 @@ function readyContract(options: BrokerAdapterOptions): Record<string, unknown> {
brokerCoverage: brokerCoverage(options.endpoint),
credentialRequest: plannedCredentialRequest(options),
auditEventShape: auditEventShape(options),
serviceRegistration: serviceRegistrationContract(),
prCapabilityContract: {
targetBranch: options.base,
headBranch: options.head,
@@ -263,6 +503,7 @@ function contractResult(options: BrokerAdapterOptions): Record<string, unknown>
registryCredentials: false,
deployPermissions: false,
},
serviceRegistration: serviceRegistrationContract(),
runnerNoTokenResult: brokerNeededResult({ ...options, endpoint: null }),
readyShape: readyContract({ ...options, endpoint: "<UNIDESK_AUTH_BROKER_URL>" }),
};
@@ -305,6 +546,7 @@ export function runAuthBrokerCommand(args: string[]): Record<string, unknown> {
phase: "p0",
brokerCoverage: broker,
tokenCoverage: envCoverage,
serviceRegistration: serviceRegistrationContract(),
healthRequest: {
method: "GET",
path: "/health",
+5 -3
View File
@@ -149,16 +149,18 @@ const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock";
const unideskRepoUrl = "https://github.com/pikasTech/unidesk";
const d601MaintenanceDeployAllowedServiceIds = new Set<string>(["k3sctl-adapter"]);
const devApplySupportedServiceIds = new Set<string>();
const devArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "claudeqq", "code-queue", "code-queue-mgr", "decision-center", "findjob", "frontend", "mdtodo", "met-nonlinear", "oa-event-flow", "pipeline", "project-manager", "todo-note"]);
const devArtifactConsumerServiceIds = new Set<string>(["auth-broker", "backend-core", "baidu-netdisk", "claudeqq", "code-queue", "code-queue-mgr", "decision-center", "findjob", "frontend", "mdtodo", "met-nonlinear", "oa-event-flow", "pipeline", "project-manager", "todo-note"]);
const devArtifactConsumerProdDesiredFallbackServiceIds = new Set<string>(["code-queue-mgr", "oa-event-flow", "project-manager", "todo-note"]);
const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "claudeqq", "code-queue-mgr", "decision-center", "findjob", "frontend", "k3sctl-adapter", "mdtodo", "met-nonlinear", "oa-event-flow", "pipeline", "project-manager", "todo-note"]);
const prodArtifactConsumerServiceIds = new Set<string>(["auth-broker", "backend-core", "baidu-netdisk", "claudeqq", "code-queue-mgr", "decision-center", "findjob", "frontend", "k3sctl-adapter", "mdtodo", "met-nonlinear", "oa-event-flow", "pipeline", "project-manager", "todo-note"]);
const prodForbiddenTargetSideBuildServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "claudeqq", "decision-center", "findjob", "frontend", "k3sctl-adapter", "mdtodo", "met-nonlinear", "pipeline"]);
const prodArtifactLiveApplyBlockedServiceIds = new Map<string, string>([
["auth-broker", "auth-broker is registered for source/contract/profile dry-run only; live production apply requires a separate credential mounting and exposure review."],
["code-queue-mgr", "code-queue-mgr is the main-server Code Queue control-plane sidecar; live production apply requires explicit supervisor confirmation."],
["met-nonlinear", "met-nonlinear is blocked for live artifact deploy because config.json points at docker/unidesk/Dockerfile.ml while the compose service is met-nonlinear-ts."],
["k3sctl-adapter", "k3sctl-adapter is an infrastructure control bridge; this executor exposes artifact consumer plan/dry-run only. Real production deployment requires supervisor confirmation outside this task."],
]);
const devArtifactLiveApplyBlockedServiceIds = new Map<string, string>([
["auth-broker", "auth-broker is registered for source/contract/profile dry-run only; live DEV apply requires a separate credential mounting and exposure review."],
["code-queue", "Code Queue DEV live apply is self-bootstrap sensitive: a running Code Queue task may produce dry-run evidence only. A human operator or supervisor must explicitly authorize DEV apply outside Code Queue after reviewing the CI artifact digest and dry-run target list."],
]);
const artifactConsumerDryRunBlockedServiceIds = new Map<string, string>([
@@ -3538,7 +3540,7 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
}
const unsupported = unsupportedDevApplyServices(manifest, options.serviceId);
if (unsupported.length > 0) {
throw new Error(`deploy apply --env dev currently supports backend-core/frontend/baidu-netdisk/decision-center/mdtodo/claudeqq/code-queue/project-manager/oa-event-flow/code-queue-mgr/todo-note/findjob/pipeline/met-nonlinear artifact consumers; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
throw new Error(`deploy apply --env dev currently supports auth-broker/backend-core/frontend/baidu-netdisk/decision-center/mdtodo/claudeqq/code-queue/project-manager/oa-event-flow/code-queue-mgr/todo-note/findjob/pipeline/met-nonlinear artifact consumers; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
}
const devArtifactServices = selectedDevArtifactServicesWithProdFallback(manifest, options.serviceId);
const devTargetServices = selectedDevTargetServices(manifest, options.serviceId);
+8
View File
@@ -181,6 +181,14 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO"),
UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT"),
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_AUTH_BROKER_DEPLOY_REF: runtimeSecret("UNIDESK_AUTH_BROKER_DEPLOY_REF"),
UNIDESK_AUTH_BROKER_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_AUTH_BROKER_DEPLOY_SERVICE_ID") || "auth-broker",
UNIDESK_AUTH_BROKER_DEPLOY_REPO: runtimeSecret("UNIDESK_AUTH_BROKER_DEPLOY_REPO"),
UNIDESK_AUTH_BROKER_DEPLOY_COMMIT: runtimeSecret("UNIDESK_AUTH_BROKER_DEPLOY_COMMIT"),
UNIDESK_AUTH_BROKER_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_AUTH_BROKER_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_AUTH_BROKER_GITHUB_CONFIGURED: runtimeSecret("UNIDESK_AUTH_BROKER_GITHUB_CONFIGURED") || "false",
UNIDESK_AUTH_BROKER_GITHUB_CREDENTIAL_REF: runtimeSecret("UNIDESK_AUTH_BROKER_GITHUB_CREDENTIAL_REF") || "github:unidesk-dev",
UNIDESK_AUTH_BROKER_ALLOWED_REPOS: runtimeSecret("UNIDESK_AUTH_BROKER_ALLOWED_REPOS") || "pikasTech/unidesk",
UNIDESK_TODO_NOTE_DEPLOY_REF: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_REF"),
UNIDESK_TODO_NOTE_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_SERVICE_ID") || "todo-note",
UNIDESK_TODO_NOTE_DEPLOY_REPO: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_REPO"),