Gate deployments with typed release authority
This commit is contained in:
parent
03c6f353a8
commit
3bd250f811
3 changed files with 409 additions and 0 deletions
|
|
@ -8,6 +8,15 @@ import {
|
|||
beginCrossingReceipt,
|
||||
crossingProvenanceFromOptions,
|
||||
} from "./bifrost-crossing-documents.mjs";
|
||||
import {
|
||||
buildAuthorizedRelease,
|
||||
canonicalCommitSha,
|
||||
canonicalRef,
|
||||
canonicalRepository,
|
||||
openRepositoryReleaseAuthorityStore,
|
||||
releaseAuthorityId,
|
||||
revokeAuthorizedRelease,
|
||||
} from "./bifrost-repository-release-authority.mjs";
|
||||
|
||||
const PERSONA_WEBHOOK_NAME = "Bifrost Persona Pipe";
|
||||
const PERSONA_WEBHOOK_CACHE_PATH = resolve(".bifrost/discord-webhook-cache.json");
|
||||
|
|
@ -45,11 +54,101 @@ async function main() {
|
|||
case "other-request":
|
||||
await requestOtherBridgeAction(options);
|
||||
return;
|
||||
case "authorize-release":
|
||||
await authorizeRepositoryRelease(options);
|
||||
return;
|
||||
case "revoke-release":
|
||||
await revokeRepositoryRelease(options);
|
||||
return;
|
||||
default:
|
||||
throw new Error(`Unknown command "${command}". Run "node tools/bifrost-bridge.mjs help".`);
|
||||
}
|
||||
}
|
||||
|
||||
async function authorizeRepositoryRelease(options) {
|
||||
ensureGitHubBridgeGate(options);
|
||||
const commandId = optionalString(options["cultmesh-command-id"]) ?? optionalString(process.env.BIFROST_CULTMESH_COMMAND_ID);
|
||||
if (!commandId) throw new Error("authorize-release requires BIFROST_CULTMESH_COMMAND_ID or --cultmesh-command-id.");
|
||||
const repository = canonicalRepository(requireOption(options, "repository"));
|
||||
const ref = canonicalRef(requireOption(options, "ref"));
|
||||
const commitSha = canonicalCommitSha(requireOption(options, "commit-sha"));
|
||||
const policyDecisionId = requireOption(options, "policy-decision-id");
|
||||
const authorityReference = requireOption(options, "authority-ref");
|
||||
const actorIdentity = requireOption(options, "identity");
|
||||
const sourceKind = requireOption(options, "source-kind");
|
||||
const sourceId = requireOption(options, "source-id");
|
||||
const epiphanyRunId = requireOption(options, "epiphany-run-id");
|
||||
const epiphanyLaneId = requireOption(options, "epiphany-lane-id");
|
||||
const epiphanyAgentIdentity = requireOption(options, "epiphany-agent-identity");
|
||||
const remoteUrl = options["remote-url"] ?? `https://github.com/${repository}.git`;
|
||||
const gitCommand = optionalString(options["git-executable"]) ?? resolveGitCommand();
|
||||
const gitPrefixArgs = optionalString(options["git-exec-args"])?.split("\u001f").filter(Boolean) ?? [];
|
||||
const remote = run(gitCommand, [...gitPrefixArgs, "ls-remote", "--refs", remoteUrl, ref], repoRoot).stdout.trim().split(/\r?\n/).filter(Boolean);
|
||||
if (remote.length !== 1) throw new Error(`Remote ref ${repository}:${ref} did not resolve exactly once.`);
|
||||
const [remoteSha, remoteRef] = remote[0].trim().split(/\s+/);
|
||||
if (canonicalCommitSha(remoteSha) !== commitSha || remoteRef !== ref) {
|
||||
throw new Error(`Remote ref ${repository}:${ref} does not exactly match requested commit ${commitSha}.`);
|
||||
}
|
||||
const storePath = resolve(options["release-authority-store"] ?? process.env.BIFROST_REPOSITORY_RELEASE_AUTHORITY_STORE ?? ".bifrost/repository-release-authority.cc");
|
||||
const store = await openRepositoryReleaseAuthorityStore(storePath);
|
||||
const authorityId = releaseAuthorityId(repository, ref, commitSha);
|
||||
const existing = store.get(authorityId);
|
||||
if (existing) {
|
||||
if (existing.commandId === commandId && existing.status === "authorized") {
|
||||
printJson({ ok: true, action: "authorize-release", authority: existing, idempotent: true });
|
||||
return;
|
||||
}
|
||||
throw new Error(`Release authority ${authorityId} already exists; immutable authority records cannot be replaced.`);
|
||||
}
|
||||
const competing = store.getAll().find((authority) =>
|
||||
authority.repositoryFullName === repository &&
|
||||
authority.upstreamRef === ref &&
|
||||
authority.status === "authorized"
|
||||
);
|
||||
if (competing) {
|
||||
throw new Error(
|
||||
`Release authority ${competing.authorityId} already authorizes ${repository}:${ref}; revoke it before authorizing another commit.`,
|
||||
);
|
||||
}
|
||||
const externalReceiptUrl = `https://github.com/${repository}/commit/${commitSha}`;
|
||||
const crossing = await beginCrossingReceipt({ ...options, "receipt-id": undefined }, {
|
||||
commandId,
|
||||
crossingKind: "github.repository_release_verification",
|
||||
actor: { kind: "service", name: actorIdentity, bifrostIdentity: actorIdentity },
|
||||
source: { kind: sourceKind, id: sourceId },
|
||||
authority: { authorityRef: authorityReference, policyDecisionId },
|
||||
epiphany: { runId: epiphanyRunId, laneId: epiphanyLaneId, agentIdentity: epiphanyAgentIdentity },
|
||||
target: { surface: "GitHub", repositoryFullName: repository, upstreamRef: ref, commitSha },
|
||||
});
|
||||
await crossing.start();
|
||||
await crossing.complete({ externalReceipt: { url: externalReceiptUrl, id: commitSha, repositoryFullName: repository, upstreamRef: ref, commitSha } });
|
||||
const authority = await store.put(buildAuthorizedRelease({
|
||||
repositoryFullName: repository, upstreamRef: ref, commitSha, commandId,
|
||||
crossingReceiptId: crossing.receiptId, policyDecisionId, authorityReference,
|
||||
actorIdentity, sourceKind, sourceId, epiphanyRunId, epiphanyLaneId,
|
||||
epiphanyAgentIdentity, externalReceiptUrl, externalReceiptId: commitSha,
|
||||
authorizedAt: new Date().toISOString(), expiresAt: optionalString(options["expires-at"]) ?? "",
|
||||
}));
|
||||
printJson({ ok: true, action: "authorize-release", authority });
|
||||
}
|
||||
|
||||
async function revokeRepositoryRelease(options) {
|
||||
ensureGitHubBridgeGate(options);
|
||||
const commandId = optionalString(options["cultmesh-command-id"]) ?? optionalString(process.env.BIFROST_CULTMESH_COMMAND_ID);
|
||||
if (!commandId) throw new Error("revoke-release requires BIFROST_CULTMESH_COMMAND_ID or --cultmesh-command-id.");
|
||||
const repository = canonicalRepository(requireOption(options, "repository"));
|
||||
const ref = canonicalRef(requireOption(options, "ref"));
|
||||
const commitSha = canonicalCommitSha(requireOption(options, "commit-sha"));
|
||||
const reason = requireOption(options, "reason");
|
||||
const storePath = resolve(options["release-authority-store"] ?? process.env.BIFROST_REPOSITORY_RELEASE_AUTHORITY_STORE ?? ".bifrost/repository-release-authority.cc");
|
||||
const store = await openRepositoryReleaseAuthorityStore(storePath);
|
||||
const authorityId = releaseAuthorityId(repository, ref, commitSha);
|
||||
const current = store.get(authorityId);
|
||||
if (!current) throw new Error(`Release authority ${authorityId} does not exist.`);
|
||||
const authority = await store.put(revokeAuthorizedRelease(current, { commandId, reason, revokedAt: new Date().toISOString() }));
|
||||
printJson({ ok: true, action: "revoke-release", authority });
|
||||
}
|
||||
|
||||
function loadBifrostLocalEnv() {
|
||||
if (process.env.BIFROST_SKIP_LOCAL_ENV === "true") {
|
||||
return;
|
||||
|
|
@ -1360,6 +1459,8 @@ Commands:
|
|||
discord-dm Send a Discord DM through Bifrost's bridge-owned bot token
|
||||
reddit-post Create a self-post in r/GameCultOrg through the Bifrost Reddit app
|
||||
other-request Record a Bifrost-gated request for a future outside-world surface
|
||||
authorize-release Authorize one exact remote GitHub ref and commit for downstream deployment
|
||||
revoke-release Revoke the current authorization for one GitHub repository ref
|
||||
|
||||
Examples:
|
||||
node tools/bifrost-bridge.mjs github-draft-pr --repo-root E:/Projects/AetheriaLore --identity nibu --title "Nibu: Glitchcraft" --path Aetheria/Articles/Nibu/glitchcraft.md --content-file article.md
|
||||
|
|
|
|||
169
tools/bifrost-repository-release-authority.mjs
Normal file
169
tools/bifrost-repository-release-authority.mjs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { createRequire } from "node:module";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const repoRoot = resolve(import.meta.dirname, "..");
|
||||
const projectsRoot = resolve(repoRoot, "..");
|
||||
const cultLibRoot = resolve(process.env.VOIDBOT_CULTLIB_ROOT || resolve(projectsRoot, "CultLib"));
|
||||
|
||||
export const repositoryReleaseAuthorityDocumentType = "bifrost.repository_release_authority";
|
||||
export const repositoryReleaseAuthoritySchemaId = "bifrost.repository_release_authority.v1";
|
||||
export const defaultRepositoryReleaseAuthorityStorePath = resolve(repoRoot, ".bifrost", "repository-release-authority.cc");
|
||||
|
||||
let cachedRuntime;
|
||||
let cachedDefinition;
|
||||
|
||||
export function repositoryReleaseAuthorityDefinition(defineDocumentTypeInput) {
|
||||
const defineDocumentType = defineDocumentTypeInput ?? loadCultCacheRuntime().defineDocumentType;
|
||||
cachedDefinition ??= defineDocumentType({
|
||||
type: repositoryReleaseAuthorityDocumentType,
|
||||
schemaId: repositoryReleaseAuthoritySchemaId,
|
||||
schemaName: repositoryReleaseAuthorityDocumentType,
|
||||
schemaVersion: repositoryReleaseAuthoritySchemaId,
|
||||
contentHash: repositoryReleaseAuthoritySchemaId,
|
||||
global: false,
|
||||
name: "authorityId",
|
||||
indexes: { repositoryFullName: "repositoryFullName", upstreamRef: "upstreamRef", commitSha: "commitSha", status: "status" },
|
||||
schema: { parse: parseRepositoryReleaseAuthority },
|
||||
members: [
|
||||
{ slot: 1, memberName: "authorityId", typeName: "string", isName: true },
|
||||
{ slot: 2, memberName: "commandId", typeName: "string" },
|
||||
{ slot: 3, memberName: "crossingReceiptId", typeName: "string" },
|
||||
{ slot: 4, memberName: "repositoryFullName", typeName: "string", indexAlias: "repositoryFullName" },
|
||||
{ slot: 5, memberName: "upstreamRef", typeName: "string", indexAlias: "upstreamRef" },
|
||||
{ slot: 6, memberName: "commitSha", typeName: "string", indexAlias: "commitSha" },
|
||||
{ slot: 7, memberName: "decision", typeName: "string" },
|
||||
{ slot: 8, memberName: "status", typeName: "string", indexAlias: "status" },
|
||||
{ slot: 9, memberName: "policyDecisionId", typeName: "string" },
|
||||
{ slot: 10, memberName: "authorityReference", typeName: "string" },
|
||||
{ slot: 11, memberName: "actorIdentity", typeName: "string" },
|
||||
{ slot: 12, memberName: "sourceKind", typeName: "string" },
|
||||
{ slot: 13, memberName: "sourceId", typeName: "string" },
|
||||
{ slot: 14, memberName: "epiphanyRunId", typeName: "string" },
|
||||
{ slot: 15, memberName: "epiphanyLaneId", typeName: "string" },
|
||||
{ slot: 16, memberName: "epiphanyAgentIdentity", typeName: "string" },
|
||||
{ slot: 17, memberName: "externalReceiptUrl", typeName: "string" },
|
||||
{ slot: 18, memberName: "externalReceiptId", typeName: "string" },
|
||||
{ slot: 19, memberName: "authorizedAt", typeName: "string" },
|
||||
{ slot: 20, memberName: "expiresAt", typeName: "string" },
|
||||
{ slot: 21, memberName: "revokedAt", typeName: "string" },
|
||||
{ slot: 22, memberName: "revocationReason", typeName: "string" },
|
||||
],
|
||||
});
|
||||
return cachedDefinition;
|
||||
}
|
||||
|
||||
export async function openRepositoryReleaseAuthorityStore(storePath = defaultRepositoryReleaseAuthorityStorePath) {
|
||||
const { CultCache, SingleFileMessagePackBackingStore } = loadCultCacheRuntime();
|
||||
await mkdir(dirname(storePath), { recursive: true });
|
||||
const cache = CultCache.builder().withDocumentType(repositoryReleaseAuthorityDefinition()).withGenericStore(new SingleFileMessagePackBackingStore(storePath)).build();
|
||||
await cache.pullAllBackingStores();
|
||||
return {
|
||||
storePath,
|
||||
async put(document) {
|
||||
const parsed = parseRepositoryReleaseAuthority(document);
|
||||
await cache.put(repositoryReleaseAuthorityDefinition(), parsed.authorityId, parsed);
|
||||
return parsed;
|
||||
},
|
||||
get(authorityId) { return cache.get(repositoryReleaseAuthorityDefinition(), authorityId); },
|
||||
getAll() { return cache.getAll(repositoryReleaseAuthorityDefinition()); },
|
||||
};
|
||||
}
|
||||
|
||||
export function releaseAuthorityId(repositoryFullName, upstreamRef, commitSha) {
|
||||
return `release:${canonicalRepository(repositoryFullName)}:${canonicalRef(upstreamRef)}:${canonicalCommitSha(commitSha)}`;
|
||||
}
|
||||
|
||||
export function buildAuthorizedRelease(input) {
|
||||
const repositoryFullName = canonicalRepository(input.repositoryFullName);
|
||||
const upstreamRef = canonicalRef(input.upstreamRef);
|
||||
const commitSha = canonicalCommitSha(input.commitSha);
|
||||
return parseRepositoryReleaseAuthority({
|
||||
schemaName: repositoryReleaseAuthorityDocumentType,
|
||||
schemaVersion: repositoryReleaseAuthoritySchemaId,
|
||||
authorityId: releaseAuthorityId(repositoryFullName, upstreamRef, commitSha),
|
||||
commandId: input.commandId,
|
||||
crossingReceiptId: input.crossingReceiptId,
|
||||
repositoryFullName,
|
||||
upstreamRef,
|
||||
commitSha,
|
||||
decision: "authorize",
|
||||
status: "authorized",
|
||||
policyDecisionId: input.policyDecisionId,
|
||||
authorityReference: input.authorityReference,
|
||||
actorIdentity: input.actorIdentity,
|
||||
sourceKind: input.sourceKind,
|
||||
sourceId: input.sourceId,
|
||||
epiphanyRunId: input.epiphanyRunId,
|
||||
epiphanyLaneId: input.epiphanyLaneId,
|
||||
epiphanyAgentIdentity: input.epiphanyAgentIdentity,
|
||||
externalReceiptUrl: input.externalReceiptUrl,
|
||||
externalReceiptId: input.externalReceiptId,
|
||||
authorizedAt: input.authorizedAt,
|
||||
expiresAt: input.expiresAt ?? "",
|
||||
revokedAt: "",
|
||||
revocationReason: "",
|
||||
});
|
||||
}
|
||||
|
||||
export function revokeAuthorizedRelease(current, input) {
|
||||
const parsed = parseRepositoryReleaseAuthority(current);
|
||||
if (parsed.status !== "authorized") throw new Error(`Release authority ${parsed.authorityId} is already revoked.`);
|
||||
return parseRepositoryReleaseAuthority({ ...parsed, status: "revoked", revokedAt: requiredString(input.revokedAt, "revokedAt"), revocationReason: requiredString(input.reason, "revocationReason") });
|
||||
}
|
||||
|
||||
export function canonicalRepository(value) {
|
||||
const repository = requiredString(value, "repositoryFullName").replace(/^https?:\/\/github\.com\//i, "").replace(/\.git$/i, "").replace(/^\/+|\/+$/g, "");
|
||||
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) throw new Error("repositoryFullName must be a canonical GitHub owner/name.");
|
||||
return repository;
|
||||
}
|
||||
|
||||
export function canonicalRef(value) {
|
||||
const ref = requiredString(value, "upstreamRef");
|
||||
if (!/^refs\/(heads|tags)\/[A-Za-z0-9._\/-]+$/.test(ref) || ref.includes("..") || ref.endsWith("/")) throw new Error("upstreamRef must be a canonical refs/heads/... or refs/tags/... name.");
|
||||
return ref;
|
||||
}
|
||||
|
||||
export function canonicalCommitSha(value) {
|
||||
const sha = requiredString(value, "commitSha").toLowerCase();
|
||||
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error("commitSha must be an exact 40-character hexadecimal Git commit SHA.");
|
||||
return sha;
|
||||
}
|
||||
|
||||
function parseRepositoryReleaseAuthority(input) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Bifrost repository release authority must be an object.");
|
||||
const repositoryFullName = canonicalRepository(input.repositoryFullName);
|
||||
const upstreamRef = canonicalRef(input.upstreamRef);
|
||||
const commitSha = canonicalCommitSha(input.commitSha);
|
||||
const document = {
|
||||
schemaName: repositoryReleaseAuthorityDocumentType,
|
||||
schemaVersion: repositoryReleaseAuthoritySchemaId,
|
||||
authorityId: requiredString(input.authorityId, "authorityId"), commandId: requiredString(input.commandId, "commandId"),
|
||||
crossingReceiptId: requiredString(input.crossingReceiptId, "crossingReceiptId"), repositoryFullName, upstreamRef, commitSha,
|
||||
decision: requiredString(input.decision, "decision"), status: requiredString(input.status, "status"), policyDecisionId: requiredString(input.policyDecisionId, "policyDecisionId"),
|
||||
authorityReference: requiredString(input.authorityReference, "authorityReference"), actorIdentity: requiredString(input.actorIdentity, "actorIdentity"),
|
||||
sourceKind: requiredString(input.sourceKind, "sourceKind"), sourceId: requiredString(input.sourceId, "sourceId"),
|
||||
epiphanyRunId: requiredString(input.epiphanyRunId, "epiphanyRunId"), epiphanyLaneId: requiredString(input.epiphanyLaneId, "epiphanyLaneId"), epiphanyAgentIdentity: requiredString(input.epiphanyAgentIdentity, "epiphanyAgentIdentity"),
|
||||
externalReceiptUrl: requiredString(input.externalReceiptUrl, "externalReceiptUrl"), externalReceiptId: requiredString(input.externalReceiptId, "externalReceiptId"),
|
||||
authorizedAt: requiredString(input.authorizedAt, "authorizedAt"), expiresAt: optionalString(input.expiresAt) ?? "", revokedAt: optionalString(input.revokedAt) ?? "", revocationReason: optionalString(input.revocationReason) ?? "",
|
||||
};
|
||||
if (document.authorityId !== releaseAuthorityId(repositoryFullName, upstreamRef, commitSha)) throw new Error("authorityId must be derived from repositoryFullName, upstreamRef, and commitSha.");
|
||||
if (document.decision !== "authorize") throw new Error("Repository release authority decision must be authorize.");
|
||||
if (!new Set(["authorized", "revoked"]).has(document.status)) throw new Error(`Repository release authority status "${document.status}" is not valid.`);
|
||||
if (document.status === "authorized" && (document.revokedAt || document.revocationReason)) throw new Error("Authorized release authority cannot carry revocation state.");
|
||||
if (document.status === "revoked" && (!document.revokedAt || !document.revocationReason)) throw new Error("Revoked release authority requires revocation time and reason.");
|
||||
return document;
|
||||
}
|
||||
|
||||
function loadCultCacheRuntime() {
|
||||
if (cachedRuntime) return cachedRuntime;
|
||||
const candidates = [resolve(cultLibRoot, "packages", "cultcache-ts", "dist", "index.js"), resolve(projectsRoot, "CultCacheTS", "dist", "index.js")];
|
||||
const entryPoint = candidates.find(existsSync);
|
||||
if (!entryPoint) throw new Error(`CultCache TypeScript runtime is unavailable. Tried: ${candidates.join(", ")}`);
|
||||
cachedRuntime = createRequire(entryPoint)(entryPoint);
|
||||
return cachedRuntime;
|
||||
}
|
||||
|
||||
function requiredString(value, field) { const result = optionalString(value); if (!result) throw new Error(`Bifrost repository release authority field "${field}" must be a non-empty string.`); return result; }
|
||||
function optionalString(value) { if (typeof value !== "string") return undefined; const trimmed = value.trim(); return trimmed || undefined; }
|
||||
Loading…
Reference in a new issue