import { r as __exportAll } from "./chunk-BSw8zbkd.mjs";
import { i as initialize, t as approveChanges } from "./creators-D4wiR4rK.mjs";
import { S as setOwner, r as enableService, u as addPackage, x as setLabel } from "./creators-BPl0_8CT.mjs";
import { t as DocumentChangeType } from "./dist-CTdaa-y9.mjs";
import { t as require_main } from "./main-BY-64ifo.mjs";
import { r as getRedeemedKeyCiphertext, t as ACCESS_CODES_TRANSIT_TENANT } from "./resolvers-DMenezt1.mjs";
import { n as OpenBaoTransitClient, t as createSecretsService } from "./secrets-service-TFqI9ubU.mjs";
import { i as getTenantId, n as deleteEnvironmentFromGitops, r as gcOrphanTenantDirs, t as removeEnvironmentRecord } from "./cleanup-CRExGoFl.mjs";
import { randomBytes } from "node:crypto";
import { BaseSubgraph } from "@powerhousedao/reactor-api";
const schema = require_main().gql`
  type ClaimStudioEnvironmentResult {
    documentId: String!
    subdomain: String!
    tenantId: String!
  }

  type VetraStudioPoolMutations {
    "Claim a warm studio env for the authenticated invite-code caller. Null when none available."
    claimStudioEnvironment: ClaimStudioEnvironmentResult
  }

  type StudioPoolConfig {
    "The vetra-cli version the pool currently provisions (from STUDIO_POOL_VERSION)."
    version: String!
  }

  type VetraStudioPoolQueries {
    "Live provisioning config so clients source the version instead of baking it into a cacheable bundle."
    config: StudioPoolConfig!
  }

  type Query {
    VetraStudioPool: VetraStudioPoolQueries
  }

  type Mutation {
    VetraStudioPool: VetraStudioPoolMutations!
  }
`;
//#endregion
//#region subgraphs/vetra-studio-pool/resolvers.ts
/** Mirror vetra-access-codes `callerDid` so the attached-key lookup matches the redemption. */
function callerDid(u) {
	return `did:pkh:${u.networkId}:${u.chainId}:${u.address.toLowerCase()}`;
}
function createResolvers(deps) {
	return {
		Query: { VetraStudioPool: () => ({}) },
		VetraStudioPoolQueries: { config: () => ({ version: deps.version }) },
		Mutation: { VetraStudioPool: () => ({}) },
		VetraStudioPoolMutations: { claimStudioEnvironment: async (_p, _a, ctx) => {
			if (!ctx.user) throw new Error("UNAUTHENTICATED");
			return deps.claim(callerDid(ctx.user));
		} }
	};
}
//#endregion
//#region subgraphs/vetra-studio-pool/config.ts
const DEFAULTS = {
	size: 5,
	version: "0.0.1-dev.19",
	sizeName: "VETRA_AGENT_XXL",
	registry: "https://registry.dev.vetra.io",
	switchboardUrl: "https://switchboard.staging.vetra.io"
};
/**
* Read STUDIO_POOL_* from an env bag. The in-process keeper is "enabled" only
* when a positive, explicit STUDIO_POOL_SIZE was provided — so the pool never
* runs by accident in a Switchboard that didn't opt in.
*/
function loadPoolConfig(env) {
	const raw = env.STUDIO_POOL_SIZE;
	const parsed = raw === void 0 ? NaN : Number.parseInt(raw, 10);
	const explicitValid = Number.isInteger(parsed) && parsed >= 0;
	return {
		size: explicitValid ? parsed : DEFAULTS.size,
		version: env.STUDIO_POOL_VERSION ?? DEFAULTS.version,
		sizeName: env.STUDIO_POOL_SIZE_NAME ?? DEFAULTS.sizeName,
		registry: env.STUDIO_POOL_REGISTRY ?? DEFAULTS.registry,
		switchboardUrl: env.STUDIO_POOL_SWITCHBOARD_URL ?? DEFAULTS.switchboardUrl,
		enabled: explicitValid && parsed > 0
	};
}
//#endregion
//#region subgraphs/vetra-studio-pool/pool-db.ts
function makeClaimDb(db) {
	return { async claimOneAvailable(addr, version, nowIso) {
		return await db.updateTable("environments").set({
			poolState: "CLAIMED",
			claimedBy: addr,
			claimedAt: nowIso
		}).where("id", "=", db.selectFrom("environments").select("id").where("poolState", "=", "AVAILABLE").where("status", "=", "READY").where("pinnedVersion", "=", version).orderBy("id").limit(1).forUpdate().skipLocked()).returning([
			"id",
			"tenantId",
			"subdomain",
			"poolState"
		]).executeTakeFirst() ?? null;
	} };
}
/** Keeper-side helpers over the shared `environments` table. */
function makeKeeperDb(db) {
	return {
		async listPoolRows() {
			return await db.selectFrom("environments").select([
				"id",
				"poolState",
				"pinnedVersion",
				"status"
			]).where("poolState", "in", [
				"WARMING",
				"AVAILABLE",
				"CLAIMED",
				"FAILED"
			]).execute();
		},
		async seedWarming(input) {
			await db.insertInto("environments").values({
				id: input.id,
				subdomain: input.subdomain,
				tenantId: input.tenantId,
				poolState: "WARMING",
				pinnedVersion: input.pinnedVersion
			}).onConflict((oc) => oc.column("id").doUpdateSet({
				poolState: "WARMING",
				pinnedVersion: input.pinnedVersion
			}).where("environments.poolState", "is", null)).execute();
		},
		async promoteReadyToAvailable() {
			await db.updateTable("environments").set({ poolState: "AVAILABLE" }).where("poolState", "=", "WARMING").where("status", "=", "READY").execute();
		},
		async clearPoolState(ids) {
			if (ids.length === 0) return;
			await db.updateTable("environments").set({ poolState: null }).where("id", "in", ids).execute();
		}
	};
}
//#endregion
//#region subgraphs/vetra-studio-pool/claim.ts
const SECRET_NAMES = [
	"ANTHROPIC_API_KEY",
	"VETRA_ANTHROPIC_API_KEY",
	"VETRA_CLI_ANTHROPIC_API_KEY"
];
const SECRET_RETRIES = 3;
async function withRetry(fn, retries, sleep) {
	let lastErr;
	for (let attempt = 0; attempt < retries; attempt++) try {
		await fn();
		return;
	} catch (err) {
		lastErr = err;
		if (attempt < retries - 1) await sleep(2 ** attempt * 100);
	}
	throw lastErr;
}
/**
* Atomically claim one warm env for the caller `did` (a `did:pkh:...`), transfer
* ownership (system SET_OWNER on the owner-null env), and inject the caller's
* attached key. The key lookup is keyed by the full DID (matching how the
* redemption was recorded); `claimedBy`/owner use the bare address. Returns null
* when the caller has no key or the pool is empty (frontend then cold-falls-back).
*
* Self-healing: transient secret writes are retried; on ANY post-assign failure
* the half-claimed env is TERMINATED (not left as an owned, key-less orphan) and
* the keeper provisions a replacement, so a failed claim leaks nothing.
*/
async function claimWarmEnvironment(d, did) {
	const addr = (did.split(":").pop() ?? did).toLowerCase();
	const sleep = d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
	const apiKey = await d.getKeyForDid(did);
	if (!apiKey) {
		d.logger.info(`[studio-pool] claim: no attached key for ${addr}`);
		return null;
	}
	const env = await d.claimDb.claimOneAvailable(addr, d.cfg.version, d.nowIso());
	if (!env || !env.tenantId) {
		d.logger.info(`[studio-pool] claim: pool empty for ${addr}`);
		return null;
	}
	try {
		const secretEntries = [
			...SECRET_NAMES.map((key) => ({
				key,
				value: apiKey
			})),
			{
				key: "ADMINS",
				value: addr
			},
			{
				key: "VETRA_SESSION_EXPORT_SECRET",
				value: randomBytes(32).toString("hex")
			}
		];
		await withRetry(() => d.setSecrets(env.tenantId, secretEntries), SECRET_RETRIES, sleep);
		await d.setOwner(env.id, addr);
		d.logger.info(`[studio-pool] claimed ${env.id} (${env.tenantId}) for ${addr}`);
		return {
			documentId: env.id,
			subdomain: env.subdomain ?? "",
			tenantId: env.tenantId
		};
	} catch (err) {
		d.logger.warn(`[studio-pool] claim post-assign failed for ${env.id}; deleting: ${String(err)}`);
		await d.deleteEnv(env.id).catch((e) => d.logger.warn(`[studio-pool] cleanup delete failed for ${env.id}: ${String(e)}`));
		return null;
	}
}
//#endregion
//#region shared/subdomain-generator.ts
const ADJECTIVES = [
	"bright",
	"calm",
	"cool",
	"crisp",
	"bold",
	"eager",
	"fair",
	"fast",
	"glad",
	"happy",
	"keen",
	"kind",
	"lush",
	"mild",
	"neat",
	"nice",
	"pure",
	"rare",
	"safe",
	"slim",
	"soft",
	"sure",
	"tall",
	"true",
	"vast",
	"warm",
	"wise",
	"blue",
	"gold",
	"jade",
	"ruby",
	"sage",
	"teal",
	"wild",
	"zesty",
	"amber",
	"brave",
	"clear",
	"deep",
	"fresh",
	"green",
	"ivory",
	"light",
	"noble",
	"prime",
	"rapid",
	"sharp",
	"swift",
	"vivid",
	"coral"
];
const ANIMALS = [
	"bear",
	"bird",
	"bull",
	"colt",
	"crab",
	"crow",
	"deer",
	"dove",
	"duck",
	"fawn",
	"fish",
	"frog",
	"goat",
	"hawk",
	"hare",
	"ibis",
	"kite",
	"lamb",
	"lark",
	"lion",
	"lynx",
	"mole",
	"moth",
	"newt",
	"orca",
	"puma",
	"quail",
	"robin",
	"seal",
	"slug",
	"swan",
	"toad",
	"vole",
	"wasp",
	"wolf",
	"wren",
	"yak",
	"ant",
	"bat",
	"bee",
	"cat",
	"cod",
	"cow",
	"dog",
	"eel",
	"elk",
	"emu",
	"fox",
	"gnu",
	"hen"
];
function hashUUID(uuid) {
	let hash = 0;
	const clean = uuid.replace(/-/g, "");
	for (let i = 0; i < clean.length; i++) {
		const char = clean.charCodeAt(i);
		hash = (hash << 5) - hash + char | 0;
	}
	return Math.abs(hash);
}
function generateSubdomain(documentId) {
	const hash = hashUUID(documentId);
	return `${ADJECTIVES[hash % ADJECTIVES.length]}-${ANIMALS[Math.floor(hash / ADJECTIVES.length) % ANIMALS.length]}-${documentId.replace(/-/g, "").slice(0, 8).toLowerCase()}`;
}
//#endregion
//#region subgraphs/vetra-studio-pool/create-env.ts
/**
* Create one ownerless, key-less studio environment and drive it to
* CHANGES_APPROVED so the existing processor deploys it. No SET_OWNER here — the
* env stays owner-null until claimed (claim sets the owner via system action).
*
* Actions are system-signed (no user in context), so `assertOwner` passes on the
* owner-null doc without auto-claiming. Order mirrors the frontend cold flow.
*/
async function createStudioEnvironmentDoc(reactor, cfg) {
	const documentId = await reactor.createDocument();
	const subdomain = generateSubdomain(documentId);
	const tenantId = getTenantId(subdomain, documentId);
	await reactor.execute(documentId, "main", [
		setLabel({ label: "Vetra Studio" }),
		initialize({
			genericSubdomain: subdomain,
			genericBaseDomain: "vetra.io",
			defaultPackageRegistry: cfg.registry
		}),
		addPackage({
			packageName: "vetra-cli",
			version: cfg.version
		}),
		enableService({
			type: "CLINT",
			prefix: "vetra-agent",
			clintConfig: {
				package: {
					registry: cfg.registry,
					name: "vetra-cli",
					version: cfg.version
				},
				env: [
					{
						name: "VETRA_OBSERVABILITY_CONSENT",
						value: "granted",
						isSecret: false
					},
					{
						name: "VETRA_REQUIRE_API_KEY",
						value: "true",
						isSecret: false
					},
					{
						name: "VETRA_CLOUD_SWITCHBOARD_URL",
						value: cfg.switchboardUrl,
						isSecret: false
					},
					{
						name: "PH_REGISTRY_URL",
						value: cfg.registry,
						isSecret: false
					}
				],
				serviceCommand: "vetra",
				selectedRessource: cfg.sizeName
			},
			selectedRessource: cfg.sizeName
		}),
		approveChanges({})
	]);
	return {
		documentId,
		subdomain,
		tenantId
	};
}
//#endregion
//#region subgraphs/vetra-studio-pool/reconcile.ts
/**
* Statuses that mean the env is dead or going away. A pool row in any of these
* is a "zombie" — its poolState must be cleared so it's neither counted toward
* the pool nor claimable. NOTE: the document model misspells the failure status
* as "DEPLOYMENt_FAILED" (lowercase t) — match it exactly.
*/
const DEAD_STATUSES = new Set([
	"TERMINATING",
	"DESTROYED",
	"ARCHIVED",
	"DEPLOYMENt_FAILED",
	"STOPPED"
]);
function isDead(status) {
	return status !== null && DEAD_STATUSES.has(status);
}
/**
* Decide the keeper's actions from the current pool rows:
*  - `toClear`: any pool row (WARMING/AVAILABLE/CLAIMED) whose env is dead →
*    clear its poolState (so recycled/terminated/failed envs stop counting and
*    can't be claimed).
*  - `toRecycle`: unclaimed LIVE envs on a stale version → terminate.
*  - `toCreate`: deficit of LIVE current-version unclaimed envs vs target size.
*
* Only live (non-dead) WARMING/AVAILABLE envs count toward the pool, so a
* WARMING env stuck in DEPLOYMENt_FAILED is cleared and replaced rather than
* permanently occupying a slot.
*/
function computePoolPlan(rows, target) {
	const toTerminate = rows.filter((r) => r.poolState === "FAILED" || (r.poolState === "WARMING" || r.poolState === "AVAILABLE") && isDead(r.status)).map((r) => r.id);
	const toClear = rows.filter((r) => r.poolState === "CLAIMED" && isDead(r.status)).map((r) => r.id);
	const liveUnclaimed = rows.filter((r) => (r.poolState === "WARMING" || r.poolState === "AVAILABLE") && !isDead(r.status));
	const staleLive = liveUnclaimed.filter((r) => r.pinnedVersion !== target.version);
	const currentLive = liveUnclaimed.filter((r) => r.pinnedVersion === target.version);
	return {
		toCreate: Math.max(0, target.size - currentLive.length),
		toRecycle: staleLive.map((r) => r.id),
		toTerminate,
		toClear
	};
}
//#endregion
//#region subgraphs/vetra-studio-pool/keeper.ts
const DEFAULT_INTERVAL_MS = 3e4;
/**
* In-process worker that keeps `cfg.size` warm envs AVAILABLE. Runs inside the
* Switchboard reactor (a subgraph starts it in onSetup), so it creates envs via
* the reactor client directly — no wallet, no separate service.
*/
var PoolKeeper = class {
	timer = null;
	constructor(d, intervalMs = DEFAULT_INTERVAL_MS) {
		this.d = d;
		this.intervalMs = intervalMs;
	}
	start() {
		if (this.timer) return;
		const tick = () => {
			this.reconcileOnce().catch((err) => this.d.logger.warn(`[studio-pool] keeper tick failed: ${String(err)}`));
		};
		tick();
		this.timer = setInterval(tick, this.intervalMs);
	}
	stop() {
		if (this.timer) clearInterval(this.timer);
		this.timer = null;
	}
	async reconcileOnce() {
		if (!this.d.cfg.enabled) return;
		await this.d.db.promoteReadyToAvailable();
		const plan = computePoolPlan(await this.d.db.listPoolRows(), {
			size: this.d.cfg.size,
			version: this.d.cfg.version
		});
		if (plan.toClear.length > 0) try {
			await this.d.db.clearPoolState(plan.toClear);
			this.d.logger.info(`[studio-pool] cleared ${plan.toClear.length} dead pool row(s)`);
		} catch (err) {
			this.d.logger.warn(`[studio-pool] clear failed: ${String(err)}`);
		}
		for (const id of plan.toTerminate) try {
			await this.d.deleteEnv(id);
			this.d.logger.info(`[studio-pool] terminated dead warm env ${id}`);
		} catch (err) {
			this.d.logger.warn(`[studio-pool] terminate ${id} failed: ${String(err)}`);
		}
		for (const id of plan.toRecycle) try {
			await this.d.deleteEnv(id);
			this.d.logger.info(`[studio-pool] recycled (deleted) stale env ${id}`);
		} catch (err) {
			this.d.logger.warn(`[studio-pool] recycle ${id} failed: ${String(err)}`);
		}
		for (let i = 0; i < plan.toCreate; i++) {
			let created;
			try {
				created = await this.d.createEnv();
			} catch (err) {
				this.d.logger.warn(`[studio-pool] create failed: ${String(err)}`);
				continue;
			}
			try {
				await this.d.db.seedWarming({
					id: created.documentId,
					subdomain: created.subdomain,
					tenantId: created.tenantId,
					pinnedVersion: this.d.cfg.version
				});
				this.d.logger.info(`[studio-pool] warming ${created.documentId} (${created.tenantId})`);
			} catch (err) {
				this.d.logger.warn(`[studio-pool] seed failed for ${created.documentId}; deleting orphan: ${String(err)}`);
				await this.d.deleteEnv(created.documentId).catch(() => {});
			}
		}
	}
};
//#endregion
//#region subgraphs/vetra-studio-pool/index.ts
var vetra_studio_pool_exports = /* @__PURE__ */ __exportAll({ VetraStudioPoolSubgraph: () => VetraStudioPoolSubgraph });
const DEFAULT_TRANSIT_ROLE = "vetra-secrets";
const ENV_DOC_TYPE = "powerhouse/vetra-cloud-environment";
/**
* Warm pool for Vetra Studio. Two responsibilities, both in-process:
*  - `claimStudioEnvironment` mutation: atomically assigns a warm env to an
*    invite-code caller, transfers ownership (system SET_OWNER), and injects the
*    code's attached key (reusing the access-codes lookup + secrets service).
*  - PoolKeeper worker: creates/maintains STUDIO_POOL_SIZE warm envs via the
*    reactor client directly (no wallet, no separate service). Started only when
*    STUDIO_POOL_SIZE > 0.
*/
var VetraStudioPoolSubgraph = class extends BaseSubgraph {
	name = "vetra-studio-pool";
	typeDefs = schema;
	resolvers = {};
	additionalContextFields = {};
	keeper = null;
	unsubscribeDocumentDeleted = null;
	gcTimer = null;
	async onSetup() {
		const cfg = loadPoolConfig(process.env);
		const openbaoAddr = process.env.OPENBAO_ADDR;
		if (!openbaoAddr) {
			console.warn("[studio-pool] OPENBAO_ADDR unset — claim + keeper disabled");
			return;
		}
		const transit = new OpenBaoTransitClient({
			addr: openbaoAddr,
			role: process.env.OPENBAO_TRANSIT_ROLE ?? DEFAULT_TRANSIT_ROLE,
			keyNamePrefix: process.env.OPENBAO_TRANSIT_KEY_PREFIX
		});
		const envDb = await this.relationalDb.createNamespace("vetra-cloud-environments");
		const secretsDb = await this.relationalDb.createNamespace("vetra-cloud-secrets");
		const accessDb = await this.relationalDb.createNamespace("vetra-access-codes");
		const secretsService = createSecretsService({
			db: secretsDb,
			transit
		});
		const claimDb = makeClaimDb(envDb);
		const reactor = {
			createDocument: async () => {
				return (await this.reactorClient.createEmpty(ENV_DOC_TYPE, {})).header.id;
			},
			execute: (documentId, branch, actions) => this.reactorClient.execute(documentId, branch, actions).then(() => void 0)
		};
		const deleteEnv = (documentId) => this.reactorClient.deleteDocument(documentId).then(() => void 0);
		const claim = (did) => claimWarmEnvironment({
			claimDb,
			getKeyForDid: async (d) => {
				const ct = await getRedeemedKeyCiphertext(accessDb, d);
				return ct === null ? null : transit.decrypt(ACCESS_CODES_TRANSIT_TENANT, ct);
			},
			setOwner: (documentId, address) => reactor.execute(documentId, "main", [setOwner({ address })]),
			setSecrets: (tenantId, entries) => secretsService.setSecrets(tenantId, entries),
			deleteEnv,
			cfg: { version: cfg.version },
			nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
			logger: console
		}, did);
		this.resolvers = createResolvers({
			claim,
			version: cfg.version
		});
		this.unsubscribeDocumentDeleted = this.reactorClient.subscribe({ type: ENV_DOC_TYPE }, (event) => {
			if (event.type !== DocumentChangeType.Deleted) return;
			const id = event.context?.childId;
			if (!id) return;
			this.reconcileDeletedEnvironment(envDb, id);
		});
		if (cfg.enabled) {
			this.keeper = new PoolKeeper({
				db: makeKeeperDb(envDb),
				createEnv: () => createStudioEnvironmentDoc(reactor, {
					version: cfg.version,
					sizeName: cfg.sizeName,
					registry: cfg.registry,
					switchboardUrl: cfg.switchboardUrl
				}),
				deleteEnv,
				cfg,
				logger: console
			});
			this.keeper.start();
			console.info(`[studio-pool] keeper started (size=${cfg.size}, version=${cfg.version})`);
		}
		if ((process.env.GITOPS_ORPHAN_GC_ENABLED ?? "false").toLowerCase() === "true") {
			const intervalMs = Number(process.env.GITOPS_ORPHAN_GC_INTERVAL_MS) || 6e5;
			const gcTick = async () => {
				try {
					const rows = await envDb.selectFrom("environments").select(["id", "subdomain"]).where("subdomain", "is not", null).execute();
					await gcOrphanTenantDirs(new Set(rows.filter((r) => r.subdomain).map((r) => getTenantId(r.subdomain, r.id))));
				} catch (e) {
					console.warn(`[studio-pool] orphan-GC tick failed: ${String(e)}`);
				}
			};
			setTimeout(() => void gcTick(), 6e4);
			this.gcTimer = setInterval(() => void gcTick(), intervalMs);
			console.info(`[studio-pool] orphan-dir GC enabled (interval=${intervalMs}ms)`);
		}
	}
	/**
	* Remove a deleted environment's read-model row and tear down its gitops
	* tenant directory. Defensive: a failure for one document must never kill the
	* subscription, so the whole body is wrapped in try/catch.
	*/
	async reconcileDeletedEnvironment(envDb, id) {
		try {
			const removed = await removeEnvironmentRecord(envDb, id);
			if (removed?.subdomain) try {
				await deleteEnvironmentFromGitops(getTenantId(removed.subdomain, id));
			} catch (e) {
				console.warn(`[studio-pool] gitops cleanup failed for deleted env ${id}: ${String(e)}`);
			}
		} catch (e) {
			console.warn(`[studio-pool] failed to reconcile deleted document ${id}: ${String(e)}`);
		}
	}
	async onDisconnect() {
		this.unsubscribeDocumentDeleted?.();
		this.unsubscribeDocumentDeleted = null;
		this.keeper?.stop();
		this.keeper = null;
		if (this.gcTimer) clearInterval(this.gcTimer);
		this.gcTimer = null;
	}
};
//#endregion
export { vetra_studio_pool_exports as n, VetraStudioPoolSubgraph as t };

//# sourceMappingURL=vetra-studio-pool-DX_3oCK0.mjs.map