UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

403 lines • 18.5 kB
/** * Organization context, owned by the Node SDK: the ONE resolver every * org-scoped command consumes, and the verbs that select, report, and bind * the current organization. * * The org is the ownership root — it owns projects, holds memberships, and is * the scope for rooms, escalations, members, grants, and audit. Precedence is * four INTENT CLASSES, highest first, and inside each class an organization * named directly outranks one derived from a project named in that same class: * * 1. flag `org` (--org) else `project` (--project) -> its org * 2. environment RUN402_ORG, else the org half else RUN402_PROJECT_ID -> its org * of RUN402_ROOM * 3. binding the `org` key of the nearest .run402(.local).json * 3.5 vault pin `r402.orgId` in this checkout's local git config * 4. profile state the profile's selected org else its active project -> org * * Naming a project IS naming its organization, so a stale profile selection * never outranks it; classes 3 and 4 are what let an org that owns no project * be addressed at all. * * Two things this deliberately does NOT do: * - Infer the org from the caller's memberships, at any count: membership is * server state that changes without the caller acting. * - Validate membership locally: a well-formed id goes to the server and the * server's answer is surfaced as returned, never retried against a lower * class and never rewritten into a local not-found. */ import { basename } from "node:path"; import { describeRejectedValue } from "../../core-dist/redact.js"; import { findBindingKey, readBindingFile, updateBindingFile } from "../../core-dist/binding-file.js"; import { clearActiveOrgId, getActiveOrgId, setActiveOrgId, } from "../../core-dist/profile-state.js"; import { getActiveProjectId, getProject, updateProject } from "../../core-dist/keystore.js"; import { LocalError } from "../errors.js"; import { Orgs } from "../namespaces/org.js"; import { Rooms } from "../namespaces/rooms.js"; import { Projects } from "../namespaces/projects.js"; import { localAction } from "./local-actions.js"; export const ORG_ENV = "RUN402_ORG"; export const ROOM_ENV = "RUN402_ROOM"; export const PROJECT_ENV = "RUN402_PROJECT_ID"; /** `org_id` is a UUID at every API boundary. */ export const ORG_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; const CONTEXT = "resolving the organization"; const trimmed = (v) => (typeof v === "string" && v.trim() ? v.trim() : null); export function listOrgsAction() { return localAction("edit_request", { command: "run402 orgs list", why: "List the organizations this wallet belongs to.", }); } /** The recovery actions an `ORG_REQUIRED` refusal carries: every way to supply an org. */ export function orgRequiredActions() { return [ localAction("edit_request", { command: "run402 orgs use <org_id>", why: "Select a current organization for this wallet profile.", }), listOrgsAction(), localAction("edit_request", { command: "run402 <command> --org <org_id>", why: "Name the organization on this one call.", }), localAction("edit_request", { command: "export RUN402_ORG=<org_id>", why: "Name the organization for every call in this shell (harness wiring).", }), localAction("edit_request", { command: `echo '{"org":"<org_id>"}' > .run402.json`, why: "Bind this checkout to an organization so every agent in it inherits the same one.", }), localAction("edit_request", { command: "run402 projects use <project_id>", why: "Select a project; its owning organization becomes the current one.", }), ]; } /** * Shape-validate an organization id from a local source. Never checks * membership. A rejected value may be a pasted secret, so it is redacted. */ export function assertOrgIdShape(orgId, origin) { if (ORG_ID_RE.test(orgId)) return orgId; throw new LocalError(`Invalid organization id ${JSON.stringify(describeRejectedValue(orgId))} (from ${origin}).`, CONTEXT, { code: "BAD_ORG_ID", hint: "An org_id is a UUID. Run 'run402 orgs list' to see the organizations you belong to.", details: { org_id: describeRejectedValue(orgId), origin }, next_actions: [listOrgsAction()], }); } /** The `ORG_REQUIRED` refusal, optionally naming a rejected positional. */ export function orgRequiredError(opts = {}) { const rejected = opts.rejectedPositional; return new LocalError(typeof rejected === "string" ? `No organization specified and no current organization set (${JSON.stringify(rejected)} is not an org_id — an org_id is a UUID).` : "No organization specified and no current organization set.", CONTEXT, { code: "ORG_REQUIRED", hint: opts.positionalHint ? `Pass --org <org_id> (or a leading <org_id> positional), set ${ORG_ENV}, bind this directory in .run402.json, or run: run402 orgs use <org_id>` : `Pass --org <org_id>, set ${ORG_ENV}, bind this directory in .run402.json, or run: run402 orgs use <org_id>`, ...(typeof rejected === "string" ? { details: { rejected_positional: rejected } } : {}), next_actions: orgRequiredActions(), }); } /** The org half of `RUN402_ROOM=<org_id>/<room_key>`, or null. */ function orgFromRoomEnv(env) { const raw = trimmed(env[ROOM_ENV]); if (!raw) return null; const slash = raw.indexOf("/"); if (slash <= 0 || slash === raw.length - 1) return null; return raw.slice(0, slash); } /** * `r402.orgId` from `cwd`'s local git config: a vault checkout's pin first, * then the bare org/room pin a room-only `rooms join` writes. Best-effort: * no repository, no pin, or a malformed value is simply null. */ async function readVaultPinnedOrgId(cwd) { try { const { readPinnedVaultRepo, readPinnedRoomBinding } = await import("./vault-address.js"); const pinned = await readPinnedVaultRepo(cwd); const orgId = trimmed(pinned?.org_id); if (orgId && ORG_ID_RE.test(orgId)) return orgId; const bare = await readPinnedRoomBinding(cwd); const bareOrgId = trimmed(bare?.org_id); return bareOrgId && ORG_ID_RE.test(bareOrgId) ? bareOrgId : null; } catch { return null; } } /** A directory name coerced into a legal room key, or null. */ function roomKeyFromDir(dir) { const base = basename(dir); const slug = base.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+/, "").slice(0, 64); return /^[a-z0-9]/.test(slug) ? slug : null; } /** Provenance triple for command output: bounded, never a resolution trace. */ export function orgProvenance(resolved) { return resolved ? { org_id: resolved.orgId, org_source: resolved.source, org_source_detail: resolved.sourceDetail } : { org_id: null, org_source: null, org_source_detail: null }; } /** * `r.orgs` on the Node entry: the isomorphic org collection and identity * verbs plus the local organization context. */ export class NodeOrgs extends Orgs { #client; #rooms; #projects; /** * @param namespaces the reads the resolver makes (a project's org, the * project listing); default to fresh namespaces on `client`. */ constructor(client, namespaces = {}) { super(client); this.#client = client; this.#rooms = namespaces.rooms ?? new Rooms(client); this.#projects = namespaces.projects ?? new Projects(client); } /** * A project's owning org: the local key cache when it carries one, else one * read. An explicitly named project (`required`) that cannot be resolved * throws — falling to a lower class would act on a different org than the * caller asked for; an implicit one returns null. */ async #orgOfProject(projectId, required) { const cached = (await this.#client.getProjectCredentials(projectId))?.org_id; if (typeof cached === "string" && cached.length > 0) return cached; try { return (await this.#rooms.forProject(projectId)).orgId; } catch (err) { if (!required) return null; throw err; } } /** * The one resolver. Returns the org with its provenance, or null when * `optional` and nothing supplies one; throws `BAD_ORG_ID`, `AMBIGUOUS_ORG`, * or `ORG_REQUIRED` (each with its recovery actions) otherwise. */ async resolve(input = {}, opts = {}) { const { env = process.env, cwd = process.cwd(), optional = false, allowConflict = false } = opts; // Class 1: flag. const orgFlag = trimmed(input.org); if (orgFlag) return { orgId: assertOrgIdShape(orgFlag, "--org"), source: "flag", sourceDetail: "--org" }; const projectFlag = trimmed(input.project); if (projectFlag) { const orgId = await this.#orgOfProject(projectFlag, true); if (orgId) return { orgId, source: "flag", sourceDetail: "--project" }; } // Classes 2 and 3: environment vs binding (the conflict pair). const bindingHit = findBindingKey(cwd, "org"); const bindingOrg = bindingHit ? assertOrgIdShape(bindingHit.value, bindingHit.file) : null; const envDirect = trimmed(env[ORG_ENV]); const envRoomOrg = envDirect ? null : orgFromRoomEnv(env); const envProject = envDirect || envRoomOrg ? null : trimmed(env[PROJECT_ENV]); let envOrg = null; let envDetail = null; if (envDirect) { envOrg = assertOrgIdShape(envDirect, ORG_ENV); envDetail = ORG_ENV; } else if (envRoomOrg) { envOrg = assertOrgIdShape(envRoomOrg, ROOM_ENV); envDetail = ROOM_ENV; } else if (envProject && bindingOrg) { // Only worth a lookup when a binding exists to disagree with. envOrg = await this.#orgOfProject(envProject, false); envDetail = PROJECT_ENV; } if (envOrg && bindingOrg && envOrg !== bindingOrg && !allowConflict) { // A binding deliberately declared by a checkout outranks the profile // selection silently; an ambient env var disagreeing with a committed // file is the surprise worth stopping on. throw new LocalError(`Ambiguous organization: ${envDetail}=${envOrg} but ${bindingHit.file} binds ${bindingOrg}.`, CONTEXT, { code: "AMBIGUOUS_ORG", hint: `Resolve with one of: pass --org <org_id>, unset ${envDetail}, or edit the binding file.`, details: { candidates: [ { org_id: envOrg, source: "env", source_detail: envDetail }, { org_id: bindingOrg, source: "binding", source_detail: bindingHit.file }, ], }, next_actions: [ localAction("edit_request", { command: "run402 <command> --org <org_id>", why: "The flag resolves the conflict for this call.", }), ], }); } if (envOrg) return { orgId: envOrg, source: "env", sourceDetail: envDetail }; if (envProject) { const orgId = await this.#orgOfProject(envProject, false); if (orgId) return { orgId, source: "env", sourceDetail: PROJECT_ENV }; } if (bindingOrg) return { orgId: bindingOrg, source: "binding", sourceDetail: bindingHit.file }; // Class 3.5: this checkout's vault pin. const pinnedOrg = await readVaultPinnedOrgId(cwd); if (pinnedOrg) return { orgId: pinnedOrg, source: "vault_pin", sourceDetail: "r402.orgId (local git config)" }; // Class 4: profile state. const selected = trimmed(getActiveOrgId()); if (selected) return { orgId: assertOrgIdShape(selected, "orgs use"), source: "profile", sourceDetail: "orgs use" }; const activeProject = trimmed(getActiveProjectId()); if (activeProject) { const orgId = await this.#orgOfProject(activeProject, false); if (orgId) return { orgId, source: "profile", sourceDetail: "projects use" }; } if (optional) return null; throw orgRequiredError(); } /** Select the current organization for the active wallet profile (`orgs use`). */ async use(orgId) { setActiveOrgId(assertOrgIdShape(orgId, "orgs use")); return { org_id: orgId, selected: true, scope: "wallet_profile" }; } /** * The organization this directory and profile resolve to, with provenance, * beside the profile's own selection. An empty selection is an explicit * null state. Never fails on ambiguity: the command that reports the * selection stays usable while it is ambiguous. */ async current(opts = {}) { const resolved = await this.resolve({}, { ...opts, optional: true, allowConflict: true }); return { ...orgProvenance(resolved), selected_org_id: this.selected() }; } /** The profile's selected organization, without running the chain. */ selected() { return trimmed(getActiveOrgId()); } /** Clear the profile's selected organization. */ async clear() { const previous = this.selected(); clearActiveOrgId(); return { org_id: null, selected: false, previous_org_id: previous ?? null }; } /** * Write this checkout's org (and room) into `.run402.json`. With no org, the * caller's sole membership is used and written down: an explicit, recorded * act, unlike the chain, which never infers. Two or more memberships refuse. */ async bind(orgId, opts = {}) { const cwd = opts.cwd ?? process.cwd(); let chosen = orgId || null; let picked = "flag"; if (!chosen) { const rows = await this.list(); if (rows.length === 0) { throw new LocalError("This wallet is a member of no organization yet.", "binding the organization", { code: "NO_ORGS", hint: "Run 'run402 init' to provision one, or ask an owner to add you with 'run402 orgs members add'.", next_actions: [localAction("initialize_wallet", { command: "run402 init", why: "Provision this wallet's organization, then retry." })], }); } if (rows.length > 1) { throw new LocalError(`This wallet belongs to ${rows.length} organizations — name the one to bind.`, "binding the organization", { code: "AMBIGUOUS_ORG", hint: "run402 orgs bind --org <org_id>", details: { orgs: rows.map((o) => ({ org_id: o.org_id, display_name: o.display_name ?? null, role: o.role ?? null })) }, next_actions: [localAction("edit_request", { command: "run402 orgs bind --org <org_id>", why: "Name which organization this checkout coordinates in." })], }); } chosen = rows[0].org_id; picked = "sole_membership"; } const room = opts.room ?? roomKeyFromDir(cwd); const { contents, file } = updateBindingFile(cwd, { org: assertOrgIdShape(chosen, picked === "flag" ? "--org" : "orgs list"), ...(room ? { room } : {}), }); return { org_id: chosen, room_key: room ?? null, org_source: picked, file: ".run402.json", path: file, bound: true, safe_to_commit: true, note: "Safe to commit — an org id is an identifier, not a credential; authorization stays server-side.", binding: contents, }; } /** Remove the `org` and `room` keys from `./.run402.json`; the file goes when nothing else is left. */ async unbind(opts = {}) { const cwd = opts.cwd ?? process.cwd(); const previous = readBindingFile(cwd); const { contents, removed } = updateBindingFile(cwd, { org: null, room: null }); return { file: ".run402.json", unbound: previous.org !== undefined || previous.room !== undefined, removed, binding: contents, }; } /** * Stamp a project's owning organization as the profile's selection * (`projects use` does this: a project determines its org unambiguously). * Best-effort: returns the org, or null, and never throws. */ async selectFromProject(projectId) { try { const orgId = await this.#orgOfProject(projectId, false); if (orgId) { setActiveOrgId(orgId); return orgId; } } catch { /* best-effort */ } return null; } /** * The organization that OWNS a specific project, or null. Distinct from * {@link resolve} (which org a command is addressed at): an `--org` override * here would be a mis-binding. Remembered in the local project entry after * the first lookup, so the vault scaffold adds no network read on a * returning machine; an exact id match is required. */ async owningOrgOf(projectId) { const cached = getProject(projectId)?.org_id; if (typeof cached === "string" && cached.length > 0) return cached; try { const listed = await this.#projects.list(); const rows = Array.isArray(listed?.projects) ? listed.projects : []; const row = rows.find((p) => (p?.id ?? p?.project_id) === projectId); const orgId = row?.org_id; if (typeof orgId !== "string" || orgId.length === 0) return null; try { updateProject(projectId, { org_id: orgId }); } catch { /* caching is an optimization */ } return orgId; } catch { return null; } } } //# sourceMappingURL=org-context.js.map