feat: add auth broker p0 skeleton
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
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 DEFAULT_CAPABILITIES = [
|
||||
"github.auth.status",
|
||||
"github.issue.list",
|
||||
"github.issue.read",
|
||||
"github.pr.list",
|
||||
"github.pr.read",
|
||||
"github.pr.create",
|
||||
"github.pr.comment.create",
|
||||
"github.pr.preflight.dry-run",
|
||||
] as const;
|
||||
|
||||
type BrokerCommand = "contract" | "credential-request" | "pr-preflight" | "health";
|
||||
type RunnerDisposition = "ready" | "infra-blocked" | "business-failed";
|
||||
|
||||
interface BrokerAdapterOptions {
|
||||
command: BrokerCommand;
|
||||
repo: string;
|
||||
operation: string;
|
||||
dryRun: boolean;
|
||||
endpoint: string | null;
|
||||
base: string;
|
||||
head: string;
|
||||
issueNumber: number | null;
|
||||
}
|
||||
|
||||
function hasEnvKey(name: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(process.env, name);
|
||||
}
|
||||
|
||||
function stringOption(args: string[], name: string): string | undefined {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) return undefined;
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.length === 0) throw new Error(`${name} requires a non-empty value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function sanitizeEndpoint(value: string): string {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (parsed.username.length > 0) parsed.username = "***";
|
||||
if (parsed.password.length > 0) parsed.password = "***";
|
||||
if (parsed.search.length > 0) parsed.search = "?...";
|
||||
parsed.hash = "";
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return value.includes("?") ? `${value.split("?")[0]}?...` : value;
|
||||
}
|
||||
}
|
||||
|
||||
function numberOption(args: string[], name: string): number | null {
|
||||
const raw = stringOption(args, name);
|
||||
if (raw === undefined) return null;
|
||||
const value = Number(raw);
|
||||
if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function firstConfiguredBrokerUrl(): string | null {
|
||||
for (const key of BROKER_URL_ENV_KEYS) {
|
||||
if (hasEnvKey(key)) return `<${key}>`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseOptions(args: string[]): BrokerAdapterOptions {
|
||||
const rawCommand = args[0] ?? "contract";
|
||||
if (!["contract", "credential-request", "pr-preflight", "health"].includes(rawCommand)) {
|
||||
throw new Error(`unknown auth-broker command: ${rawCommand}`);
|
||||
}
|
||||
const command = rawCommand as BrokerCommand;
|
||||
const endpoint = stringOption(args, "--endpoint");
|
||||
return {
|
||||
command,
|
||||
repo: stringOption(args, "--repo") ?? DEFAULT_REPO,
|
||||
operation: stringOption(args, "--operation") ?? (command === "pr-preflight" ? "github.pr.preflight.dry-run" : "github.auth.status"),
|
||||
dryRun: args.includes("--dry-run") || command === "contract" || command === "credential-request" || command === "pr-preflight" || command === "health",
|
||||
endpoint: endpoint === undefined ? firstConfiguredBrokerUrl() : sanitizeEndpoint(endpoint),
|
||||
base: stringOption(args, "--base") ?? DEFAULT_BASE,
|
||||
head: stringOption(args, "--head") ?? "<head-branch>",
|
||||
issueNumber: numberOption(args, "--issue"),
|
||||
};
|
||||
}
|
||||
|
||||
function runnerEnvTokenCoverage(): Record<string, unknown> {
|
||||
const present = SECRET_ENV_KEYS.filter((key) => hasEnvKey(key));
|
||||
return {
|
||||
ok: present.length > 0,
|
||||
source: present.length > 0 ? "runner-env" : null,
|
||||
checkedKeys: SECRET_ENV_KEYS,
|
||||
presentKeys: present,
|
||||
missingKeys: SECRET_ENV_KEYS.filter((key) => !present.includes(key)),
|
||||
presenceOnly: true,
|
||||
valuesRead: false,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function brokerCoverage(endpoint: string | null): Record<string, unknown> {
|
||||
return {
|
||||
ok: endpoint !== null,
|
||||
source: endpoint === null ? null : "auth-broker",
|
||||
endpoint: endpoint ?? null,
|
||||
credentialRef: endpoint === null ? null : "github:unidesk-dev",
|
||||
scope: endpoint === null ? null : "broker-held-github-credential",
|
||||
runnerEnvTokenRequired: false,
|
||||
valuesRead: false,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function brokerNeededResult(options: BrokerAdapterOptions): Record<string, unknown> {
|
||||
return {
|
||||
ok: false,
|
||||
failureKind: "auth-missing",
|
||||
degradedReason: "broker-needed",
|
||||
runnerDisposition: "infra-blocked" as RunnerDisposition,
|
||||
retryable: false,
|
||||
brokerNeeded: true,
|
||||
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),
|
||||
next: [
|
||||
"configure UNIDESK_AUTH_BROKER_URL or AUTH_BROKER_URL for broker-backed runner auth",
|
||||
"keep GH_TOKEN/GITHUB_TOKEN out of ordinary runner env once broker mode is enabled",
|
||||
],
|
||||
redaction: {
|
||||
valuesRead: false,
|
||||
valuesPrinted: false,
|
||||
forbiddenOutputKeys: ["token", "secret", "authorization", "cookie"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function auditEventShape(options: BrokerAdapterOptions): Record<string, unknown> {
|
||||
return {
|
||||
requestId: "authbroker-contract-request",
|
||||
observedAt: "ISO-8601 timestamp",
|
||||
caller: { plane: "code-queue", taskId: null, queueId: null },
|
||||
operation: options.operation,
|
||||
repo: options.repo,
|
||||
resource: options.command === "pr-preflight"
|
||||
? { base: options.base, head: options.head, issueNumber: options.issueNumber }
|
||||
: null,
|
||||
dryRun: true,
|
||||
credentialRef: "github:unidesk-dev",
|
||||
credentialKind: "github-rest-token-ref",
|
||||
credentialValuePrinted: false,
|
||||
upstream: { method: "planned", path: "planned GitHub REST path without query secrets" },
|
||||
status: "HTTP status",
|
||||
ok: "boolean",
|
||||
failureKind: null,
|
||||
degradedReason: null,
|
||||
runnerDisposition: "ready|infra-blocked|business-failed",
|
||||
retryable: "boolean",
|
||||
durationMs: "integer",
|
||||
redaction: { valuesPrinted: false },
|
||||
};
|
||||
}
|
||||
|
||||
function plannedCredentialRequest(options: BrokerAdapterOptions): Record<string, unknown> {
|
||||
return {
|
||||
requestId: "authbroker-cli-dry-run",
|
||||
caller: { plane: "manual-cli" },
|
||||
repo: options.repo,
|
||||
operation: options.operation,
|
||||
dryRun: true,
|
||||
params: options.command === "pr-preflight"
|
||||
? { base: options.base, head: options.head, issueNumber: options.issueNumber }
|
||||
: {},
|
||||
};
|
||||
}
|
||||
|
||||
function readyContract(options: BrokerAdapterOptions): Record<string, unknown> {
|
||||
return {
|
||||
ok: true,
|
||||
runnerDisposition: "ready" as RunnerDisposition,
|
||||
failureKind: null,
|
||||
degradedReason: null,
|
||||
brokerNeeded: false,
|
||||
dryRun: true,
|
||||
mutation: false,
|
||||
capabilities: [...DEFAULT_CAPABILITIES],
|
||||
tokenCoverage: {
|
||||
ok: true,
|
||||
source: "auth-broker",
|
||||
scope: "broker-held-github-credential",
|
||||
runnerEnvTokenRequired: false,
|
||||
valuesRead: false,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
brokerCoverage: brokerCoverage(options.endpoint),
|
||||
credentialRequest: plannedCredentialRequest(options),
|
||||
auditEventShape: auditEventShape(options),
|
||||
prCapabilityContract: {
|
||||
targetBranch: options.base,
|
||||
headBranch: options.head,
|
||||
systemGhBinaryRequiredForWrites: false,
|
||||
preflightCreatesPr: false,
|
||||
preflightMergesPr: false,
|
||||
brokerProxy: {
|
||||
ok: true,
|
||||
operations: ["github.auth.status", "github.issue.read", "github.pr.read", "github.pr.create"],
|
||||
writesRemote: false,
|
||||
},
|
||||
pushDryRun: {
|
||||
runnerLocal: true,
|
||||
coveredByBroker: false,
|
||||
},
|
||||
},
|
||||
redaction: {
|
||||
valuesRead: false,
|
||||
valuesPrinted: false,
|
||||
secretKeysBlocked: ["token", "secret", "authorization", "cookie"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function contractResult(options: BrokerAdapterOptions): Record<string, unknown> {
|
||||
return {
|
||||
ok: true,
|
||||
service: "auth-broker",
|
||||
phase: "p0",
|
||||
commands: [
|
||||
"bun scripts/cli.ts auth-broker contract",
|
||||
"bun scripts/cli.ts auth-broker health --dry-run",
|
||||
"bun scripts/cli.ts auth-broker credential-request --operation github.pr.create --repo pikasTech/unidesk --dry-run",
|
||||
"bun scripts/cli.ts auth-broker pr-preflight --repo pikasTech/unidesk --base master --head <head-branch> --issue 59 --dry-run",
|
||||
],
|
||||
capabilities: [...DEFAULT_CAPABILITIES],
|
||||
permissionBoundary: {
|
||||
allowedRepos: [DEFAULT_REPO],
|
||||
liveGithubWrites: false,
|
||||
arbitraryGhApi: false,
|
||||
registryCredentials: false,
|
||||
deployPermissions: false,
|
||||
},
|
||||
runnerNoTokenResult: brokerNeededResult({ ...options, endpoint: null }),
|
||||
readyShape: readyContract({ ...options, endpoint: "<UNIDESK_AUTH_BROKER_URL>" }),
|
||||
};
|
||||
}
|
||||
|
||||
export function authBrokerHelp(): unknown {
|
||||
return {
|
||||
command: "auth-broker",
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts auth-broker contract",
|
||||
"bun scripts/cli.ts auth-broker health --dry-run [--endpoint URL]",
|
||||
"bun scripts/cli.ts auth-broker credential-request --operation github.pr.create --repo pikasTech/unidesk --dry-run [--endpoint URL]",
|
||||
"bun scripts/cli.ts auth-broker pr-preflight --repo pikasTech/unidesk --base master --head <head-branch> [--issue N] --dry-run [--endpoint URL]",
|
||||
],
|
||||
boundary: [
|
||||
"P0 adapter is contract/dry-run only and never starts a service.",
|
||||
"GH_TOKEN and GITHUB_TOKEN values are not read or printed; only key presence is reported.",
|
||||
"No dry-run command writes GitHub, registry, deploy, filesystem credential, or service state.",
|
||||
],
|
||||
reference: "docs/reference/auth-broker.md",
|
||||
};
|
||||
}
|
||||
|
||||
export function runAuthBrokerCommand(args: string[]): Record<string, unknown> {
|
||||
if (args.some((arg) => arg === "help" || arg === "--help" || arg === "-h")) return authBrokerHelp() as Record<string, unknown>;
|
||||
const options = parseOptions(args);
|
||||
if (options.command === "contract") return contractResult(options);
|
||||
|
||||
const envCoverage = runnerEnvTokenCoverage();
|
||||
const broker = brokerCoverage(options.endpoint);
|
||||
if (broker.ok !== true) return brokerNeededResult(options);
|
||||
|
||||
if (options.command === "health") {
|
||||
return {
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
mutation: false,
|
||||
service: "auth-broker",
|
||||
phase: "p0",
|
||||
brokerCoverage: broker,
|
||||
tokenCoverage: envCoverage,
|
||||
healthRequest: {
|
||||
method: "GET",
|
||||
path: "/health",
|
||||
wouldCallBroker: false,
|
||||
},
|
||||
capabilities: [...DEFAULT_CAPABILITIES],
|
||||
redaction: { valuesRead: false, valuesPrinted: false },
|
||||
};
|
||||
}
|
||||
|
||||
return readyContract(options);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ const syntaxFiles = [
|
||||
"scripts/cli.ts",
|
||||
"scripts/src/check.ts",
|
||||
"scripts/src/artifact-registry.ts",
|
||||
"scripts/src/auth-broker.ts",
|
||||
"scripts/src/code-queue.ts",
|
||||
"scripts/src/command.ts",
|
||||
"scripts/src/decision-center.ts",
|
||||
@@ -27,6 +28,7 @@ const syntaxFiles = [
|
||||
"scripts/host-codex-commander-contract-test.ts",
|
||||
"scripts/host-codex-commander-no-daemon-smoke-contract-test.ts",
|
||||
"scripts/host-codex-commander-skeleton-contract-test.ts",
|
||||
"scripts/auth-broker-contract-test.ts",
|
||||
"src/components/frontend/src/index.ts",
|
||||
"src/components/frontend/src/app.tsx",
|
||||
"src/components/frontend/src/decision-center.tsx",
|
||||
@@ -271,6 +273,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
fileItem("AGENTS.md"),
|
||||
fileItem("TEST.md"),
|
||||
fileItem("docs/reference/artifact-registry.md"),
|
||||
fileItem("docs/reference/auth-broker.md"),
|
||||
fileItem("docker-compose.yml"),
|
||||
fileItem("src/components/backend-core/Cargo.toml"),
|
||||
fileItem("src/components/backend-core/Cargo.lock"),
|
||||
@@ -314,6 +317,11 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
fileItem("scripts/code-queue-pr-preflight-example.ts"),
|
||||
fileItem("scripts/schedule-cli-contract-test.ts"),
|
||||
fileItem("scripts/src/artifact-registry.ts"),
|
||||
fileItem("scripts/src/auth-broker.ts"),
|
||||
fileItem("scripts/auth-broker-contract-test.ts"),
|
||||
fileItem("src/components/microservices/auth-broker/Cargo.toml"),
|
||||
fileItem("src/components/microservices/auth-broker/Dockerfile"),
|
||||
fileItem("src/components/microservices/auth-broker/src/main.rs"),
|
||||
fileItem("scripts/artifact-consumer-dry-run-matrix-test.ts"),
|
||||
fileItem("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml"),
|
||||
);
|
||||
@@ -342,6 +350,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(commandItem("schedule:cli-contract", ["bun", "scripts/schedule-cli-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:issue-guard-contract", ["bun", "scripts/gh-cli-issue-guard-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:pr-contract", ["bun", "scripts/gh-cli-pr-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("auth-broker:p0-contract", ["bun", "scripts/auth-broker-contract-test.ts"], 30_000));
|
||||
} else {
|
||||
items.push(skippedItem("typescript:scripts", "scripts TypeScript typecheck is opt-in", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("code-queue:prompt-observation-contract", "prompt observation contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
@@ -360,6 +369,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(skippedItem("schedule:cli-contract", "Schedule CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("gh:issue-guard-contract", "GitHub issue CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("gh:pr-contract", "GitHub PR CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("auth-broker:p0-contract", "Auth Broker P0 skeleton and CLI adapter contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
}
|
||||
if (options.logs) {
|
||||
items.push(unifiedLogRotationItem());
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ghHelp } from "./gh";
|
||||
import { authBrokerHelp } from "./auth-broker";
|
||||
|
||||
export function rootHelp(): unknown {
|
||||
return {
|
||||
@@ -43,6 +44,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "deploy check|plan|apply [--file deploy.json|--env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force]", description: "Reconcile services from origin/master:deploy.json environments; --commit overrides one reviewed artifact consumer such as frontend for release/v1 validation or rollback. code-queue artifact consumption is dev-only." },
|
||||
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
|
||||
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including D601 direct, k3s-managed, and code-queue dev-only consumers." },
|
||||
{ command: "auth-broker contract|health --dry-run|credential-request --dry-run|pr-preflight --dry-run", description: "Inspect the P0 Rust auth broker and CLI adapter contract without reading token values, writing GitHub, or starting services." },
|
||||
{ command: "gh auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, hard delete unsupported, and merge blocked." },
|
||||
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract, no-daemon smoke plan, and dry-run preview; exposes local health, state, trace summary, and approval draft helpers without live bridges or message sends." },
|
||||
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
|
||||
@@ -371,6 +373,7 @@ export function staticNamespaceHelp(args: string[]): unknown | null {
|
||||
if (top === "e2e") return e2eHelp();
|
||||
if (top === "dev-env") return devEnvHelp();
|
||||
if (top === "artifact-registry") return artifactRegistryHelp();
|
||||
if (top === "auth-broker") return authBrokerHelp();
|
||||
if (top === "gh") return ghHelp();
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user