/** * Types for the mojo (@byted/mojo) backend. * * Kept in a separate module so `mojo.ts` (the CLI adapter) and * `mojo-backend.ts` can share them without a cycle, and so the event shapes — * which were established empirically against @byted/mojo 1.0.10 — are * documented in one place. */ /** * USER-CONFIGURABLE mojo settings — the `bots[].mojo` block in bots.json, * `/config set mojo` and the dashboard. * * Deliberately contains ONLY settings with no platform-wide equivalent. Anything * botmux already resolves generically (the binary, working dir, model, approval * bypass, launch prefix) lives on the TOP-LEVEL bot config, is frozen onto the * session at creation, and must not have a second entry point here — see * EffectiveMojoConfig for why that matters. */ export interface MojoConfig { /** `--workspace-id`. */ workspaceId?: string; /** `--agent-id`. Only meaningful when creating (not resuming) a session. */ agentId?: string; /** `--cloud` — run tools in the cloud sandbox instead of on the bot host. */ cloud?: boolean; /** `--idle-timeout `. */ idleTimeoutSec?: number; /** Default true. When false, `--include-partial` is omitted (no deltas). */ stream?: boolean; /** Prepended to every prompt (botmux routing/identity block). */ systemPrompt?: string; /** Literal JWT; wins over `jwtEnv`. */ jwt?: string; /** Env var to read the JWT from. Defaults to `X_JWT_TOKEN`. */ jwtEnv?: string; /** `AGENT_BASE_URL`. */ baseUrl?: string; /** * `AGENT_LOCAL_DAEMON=1` — runs tools on the bot host. Host execution is * the DEFAULT when `cloud` is not enabled (matching every other CLI * adapter); set `cloud: true` for the fully-remote sandbox, or an explicit * `false` here to opt out of host tools without going fully remote. * * Note: `localDaemon: false` alone does NOT restore fully-remote * treatment — isMojoFullyRemote() requires `cloud === true`, so the local * sandbox and device-isolation blockers stay engaged as if the session * ran on this host. */ localDaemon?: boolean; /** `MOJO_PPE_ENV`. */ ppeEnv?: string; /** * Extra env for the spawned CLI. Merged ON TOP of the authoritative env the * worker hands to spawn() (which already carries the BOTMUX_* session * context and per-bot `env`), so an explicit value here wins — mirroring how * RiffBackendConfig.env layers over the session context. */ env?: Record; } /** * Keys that are INTERNAL plumbing and must never be accepted from user config. * * Each has a platform-wide source of truth that is frozen onto the session at * creation (`Session.agentFrozen`), so a second entry point inside the `mojo` * block would let live edits override a frozen identity — e.g. cancelling an * orphaned remote session through a binary or wrapper the bot only gained * afterwards, reaching the wrong gateway or tenant. * * bots.json parsing and `/config set mojo` both reject these outright rather * than dropping them silently, because a silent drop reads as "applied". */ export declare const MOJO_INTERNAL_CONFIG_KEYS: readonly ["bin", "cwd", "model", "disableCliBypass", "wrapperCli", "resumeCliSessionId", "extraCliArgs", "builtinSkillBlock"]; /** The top-level bot field that owns each internal key, for error messages. */ export declare const MOJO_INTERNAL_KEY_OWNER: Readonly>; /** * Control-plane fields that decide WHERE and AS WHOM a session executes: * cloud-vs-host execution, the API endpoint, the PPE profile, and the * workspace/agent it is routed to. * * These are frozen onto the session at creation (see MojoSessionIdentity). A live * bot edit must never retroactively move an existing session between execution * modes or tenants — a cold resume would otherwise resume, or a `/close` cancel, * against a different endpoint than the one that created the remote session. * * Credentials (`jwt` / `jwtEnv` / `env`) are deliberately NOT here: they must * stay live so a rotated token takes effect, and a plaintext JWT must not be * persisted into session state. */ export declare const MOJO_IDENTITY_KEYS: readonly ["cloud", "localDaemon", "baseUrl", "ppeEnv", "workspaceId", "agentId"]; /** * The child's effective environment, layered lowest → highest precedence: * * base (worker-supplied session env / process env) * → bot `env` (bots.json top-level, already sanitized) * → `mojo.env` (bots.json mojo block — highest) * * This exists so the launcher and the backend cannot disagree. They previously * layered independently and the launcher omitted `mojo.env` entirely, so a * wrapper binary was resolved against a PATH the child never actually ran with: * a same-named program earlier on the bot-level PATH shadowed the one the * operator pinned in `mojo.env.PATH`. For a wrapper that carries auth or acts * as a gateway, that means executing under the wrong identity. * * Callers that need control-plane hygiene must still strip * MOJO_CONTROL_ENV_KEYS afterwards; this helper only fixes the layering. */ export declare function buildEffectiveChildEnv(layers: { base?: NodeJS.ProcessEnv; botEnv?: NodeJS.ProcessEnv; mojoEnv?: Record | undefined; }): NodeJS.ProcessEnv; /** * Environment variables that carry the SAME control-plane decisions as the frozen * identity keys. They exist because the mojo CLI reads its endpoint/profile from * env, which makes `env` a back door around the freeze: a live * `env: { AGENT_BASE_URL: ... }` would move an existing session to another tenant * even though `baseUrl` itself is frozen. * * buildEnv() therefore DELETES all of these after merging and re-derives them * from the (frozen) config alone. `X_JWT_TOKEN` is deliberately absent: a rotated * credential must keep taking effect. */ export declare const MOJO_CONTROL_ENV_KEYS: readonly ["AGENT_BASE_URL", "MOJO_PPE_ENV", "AGENT_LOCAL_DAEMON"]; /** * The one env var name the mojo CLI actually reads its credential from. * * `jwtEnv` only tells the DAEMON where to look up the value; buildEnv() always * hands the resolved token to the child as this name. That asymmetry is why the * remote-execution proof exempts this fixed name and never `jwtEnv` — see * mojoUnprovableEnvKeys. */ export declare const MOJO_CANONICAL_JWT_ENV_KEY = "X_JWT_TOKEN"; /** * The outcome of trying to cancel a mojo session's remote lineage. * * Deliberately NOT a boolean. The old `Promise` collapsed two states the * caller must tell apart — "the remote session is provably gone" and "we do not * know" — into one `false`. Its own doc comment claimed a completed session "is * not an error worth surfacing" while the code returned `false` for it, so a close * that awaits proof would refuse forever on a session that had simply finished. * * `already_terminal` therefore requires EVIDENCE from a verified mojo error * code/state, never a text guess: a loose regex over CLI stderr is how a * "finished" and a "cancel is broken" become indistinguishable again, and this * repo already carries that debt once (RESUME_DEAD_RE). Until the real codes are * calibrated against @byted/mojo, the classifier returns `failed` for everything * it cannot prove, which fails CLOSED (row stays open, retryable). */ /** * A LOCAL subtree the close proved quiescent by weak evidence only (or could not * instrument at all), so its containment handle — and the device-isolation * blocker — stays behind. Carried on remote-gone outcomes so the final close can * publish `closed_with_residual` instead of lying with a plain `closed`. */ export type MojoLocalCloseResidual = 'local_subtree_unprovable_on_platform' | 'local_subtree_boundary_unproven'; export type MojoCancelOutcome = /** The cancel call succeeded. The remote session is gone. */ { kind: 'cancelled'; localResidual?: MojoLocalCloseResidual; } /** Proven already finished — carries the verified signal that proved it. */ | { kind: 'already_terminal'; evidence: string; localResidual?: MojoLocalCloseResidual; } /** Unknown or failed. `retryable: false` only for causes a retry cannot fix. */ | { kind: 'failed'; code?: string; message: string; retryable: boolean; }; /** True only when the remote session is provably gone (cancelled or finished). */ export declare function isMojoRemoteGone(outcome: MojoCancelOutcome): boolean; /** The frozen control-plane identity, persisted on the session. */ export type MojoSessionIdentity = Pick; /** * Pick just the control-plane identity out of a (already normalized) config. * Absent keys are omitted rather than stored as undefined, so the frozen record * stays a faithful snapshot of what was actually configured. */ export declare function pickMojoSessionIdentity(cfg: MojoConfig | undefined): MojoSessionIdentity; /** * Which control-plane keys differ between the frozen snapshot and live config, * for the log line explaining why a session kept its original control plane. * Empty array = identical. * * Returns KEY NAMES ONLY, never values. `baseUrl` is a URL that may legitimately * carry userinfo (`https://user:pass@host`) or a signed query * (`?sig=`) — the URL validator allows both, because they are valid * endpoints — so logging old/new values verbatim would write credentials into the * daemon log. The key name is all an operator needs to know what changed; the * value is already in their own config. */ export declare function diffMojoSessionIdentity(frozen: MojoSessionIdentity, live: MojoSessionIdentity): string[]; /** * The config MojoBackend actually runs on: user settings PLUS the platform-owned * launch identity resolved by the worker (live session) or reconstructed by the * daemon from the session's FROZEN values (workerless cancel). * * Never built by hand from user input — always via buildEffectiveMojoConfig(). */ export interface EffectiveMojoConfig extends MojoConfig { /** * Resolved `mojo` executable. From the top-level `cliPathOverride` frozen on * the session, not from the mojo block. */ bin?: string; /** cwd for the spawned CLI — the session working dir. */ cwd?: string; /** `--model`. From the top-level bot model frozen on the session. */ model?: string; /** Top-level opt-out of `--yolo`. */ disableCliBypass?: boolean; /** * Resolved launch prefix from the top-level `wrapperCli` frozen on the * session. Carried here because MojoBackend must re-apply it to EVERY * per-turn invocation (unlike a PTY CLI there is no single long-lived process * to wrap once) and the daemon's workerless cancel path has no worker to * resolve it from. */ wrapperCli?: string; /** Persisted mojo session id, restored across daemon restarts. */ resumeCliSessionId?: string; /** * Generic extra CLI args the worker composed for this session (today: * CLI_EXTRA_ARGS). Passed explicitly rather than through spawn() args so * they land AFTER the backend's own flags on every turn — with a wrapper the * worker used to bake them into the prefix, which put them BEFORE and * silently inverted last-value-wins precedence. */ extraCliArgs?: string[]; /** * Resolved built-in skill delivery for the `prompt` / `off` modes. * * mojo is `injectsSessionContext` + global `skillsDir`, the same shape as * genius/grok — session-manager therefore skips the per-message skill * envelope for it, so the catalog (or the `off` help pointer) can only reach * the agent by riding on the prompt the backend builds. Without this only * `global` did anything and the other two modes silently no-oped. * * Computed by the worker (which owns larkAppId/locale) rather than here: * mode resolution reads bot config, and MojoBackend has neither. */ builtinSkillBlock?: string; } /** `mojo auth status --json`. */ export interface MojoAuthStatus { logged_in?: boolean; identity?: string; mode?: string; source?: string; expires_at?: string; [k: string]: unknown; } /** * `error` is an OBJECT on both envelope shapes — naive interpolation yields * "[object Object]". */ export interface MojoError { code?: string; message?: string; retryable?: boolean; [k: string]: unknown; } /** * Foreground stream events (`-p --output-format stream-json --include-partial`). * * NOTE: this is NOT the `--background` / `session.*` schema-v1 envelope, which * additionally carries schema_version / operation / state / turn_id / * result_complete / interaction. Never assume `state` or `result_complete` * exists here. */ export type MojoStreamEvent = { type: 'system'; subtype?: string; session_id?: string; model?: string; } | { type: 'text_delta'; text?: string; } | { type: 'text'; text?: string; } | { type: 'tool_call'; id?: string; name?: string; input?: unknown; } | { type: 'tool_result'; id?: string; output?: unknown; } | { type: 'result'; status?: string; result?: unknown; session_id?: string; duration_ms?: number; num_tool_calls?: number; warnings?: unknown; error?: MojoError | string | null; } | { type: string; [k: string]: unknown; }; /** One JSON envelope from a `session.*` subcommand. */ export interface MojoCliEnvelope { error?: MojoError | string | null; [k: string]: unknown; } /** Line styles understood by `emitLine`. */ export type MojoLineStyle = 'info' | 'warn' | 'ok' | 'err' | 'title' | 'plain'; /** * Generic, host-owned session settings that must reach the mojo CLI. They are * resolved by botmux (dashboard, `botmux setup`, repo selection, bots.json) and * live OUTSIDE the `mojo` block, so a backend that only reads MojoConfig * would silently ignore all of them. */ export interface MojoGenericLaunchInput { /** BotConfig.cliPathOverride — an operator-pinned mojo binary. */ cliPathOverride?: string; /** Session working dir (repo selection writes this). */ workingDir?: string; /** BotConfig.model / dashboard model picker. */ model?: string; /** BotConfig.disableCliBypass — keep the CLI's own approvals. */ disableCliBypass?: boolean; /** Per-bot env (bots.json `env`), already sanitized by the caller. */ env?: Record; /** Persisted remote lineage (Session.riffParentTaskId, shared by riff/mojo). */ resumeCliSessionId?: string; /** BotConfig.wrapperCli — launch prefix, see EffectiveMojoConfig. */ wrapperCli?: string; /** Generic extra CLI args (CLI_EXTRA_ARGS), applied after the backend flags. */ extraCliArgs?: readonly string[]; /** * Built-in skill catalog / help pointer for `prompt` / `off` mode, already * resolved by the caller. Empty or absent for `global` (files on disk) and * for `dynamic` CLIs. */ builtinSkillBlock?: string; } /** * Combine the user's `mojo` block with the platform-owned launch identity into * the ONE config both the worker (live session) and the daemon (workerless * `/close`) run on. * * Sharing this matters: the daemon cancels an orphaned session WITHOUT a worker, * so it never calls spawn() and cannot pick these up from SpawnOpts. Building * the config only in the worker meant a bot running fine on a custom binary / * per-bot JWT could not be cancelled once its worker died — leaving the remote * session burning cloud sandbox time while still holding injected credentials. * * There is no precedence question to answer here: the launch identity has EXACTLY * one source (`generic`, which callers populate from the session's frozen * values). Anything the user put under those keys in the `mojo` block is stripped * — both config entry points reject them, so reaching this function with one set * means a hand-edited file, and honouring it would let a live edit override a * frozen session identity. */ export declare function buildEffectiveMojoConfig(mojoBlock: MojoConfig | undefined, generic: MojoGenericLaunchInput): EffectiveMojoConfig; /** * Reject platform-owned flags in operator-supplied extra args. * * Handles `--flag value`, `--flag=value` and the bare boolean form, since all * three reach the CLI identically. Returns the offending flags, empty when clean. */ export declare function findReservedMojoCliFlags(args: readonly string[]): string[]; /** * Can this mojo session prove it executes nothing locally? * * A launch prefix (wrapperCli) breaks the proof: it runs BEFORE the binary and * can rewrite the very environment the decision depends on — `env * AGENT_LOCAL_DAEMON=1 mojo` re-enables host execution after buildEnv() set it to * 0, and buildEnv cannot defend against it because the prefix is applied later. * Inspecting the wrapper string is not enough either: a wrapper may be a script * that sets the variable internally. * * So a wrapper makes the session unprovable, and the local sandbox stays engaged. * The launcher's ENV is treated exactly the same way, and for the same reason — * see mojoUnprovableEnvKeys. */ export declare function mojoUnprovableEnvKeys(cfg?: { jwtEnv?: string; env?: Record; }): string[]; /** * Single source of truth for the execution mode a mojo config asks for. * * buildArgs (`--cloud`), buildEnv (`AGENT_LOCAL_DAEMON`) and the spawn audit * log MUST all derive from this one function — they were previously three * hand-kept copies and drifted twice (review F2: the audit label disagreed * with the env for cloud+localDaemon both set, and for non-boolean values * smuggled past strictBoolean by an old frozen snapshot). * * Precedence (review F3): an explicit `localDaemon: true` WINS over * `cloud: true` and suppresses the `--cloud` flag entirely — previously both * were emitted and the CLI received contradictory instructions. This stays * consistent with isMojoFullyRemote(), which already returns false whenever * `localDaemon === true`. * * Strict comparisons on purpose: a non-boolean survivor (e.g. the string * "false" in a frozen snapshot written before strictBoolean existed) fails * closed into the sandbox fallback rather than enabling host execution. */ export declare function deriveMojoExecutionMode(cfg?: { cloud?: boolean; localDaemon?: boolean; }): { agentLocalDaemon: '0' | '1'; passCloudFlag: boolean; label: string; }; export declare function isMojoFullyRemote(cfg?: { cloud?: boolean; localDaemon?: boolean; wrapperCli?: string; jwtEnv?: string; env?: Record; }): boolean; /** The proof inputs, shared by isMojoFullyRemote and its explanation helper. */ export type MojoRemoteProofInput = { cloud?: boolean; localDaemon?: boolean; wrapperCli?: string; jwtEnv?: string; env?: Record; }; /** * WHY this config cannot prove it runs nothing locally — one actionable * explanation, or `undefined` when the proof holds. * * Shared deliberately. There are two independent places that refuse a * not-provably-remote mojo session — the OPTIONAL sandbox gate * (backendSandboxCompatibilityError) and the MANDATORY device-credential * isolation path (which rewrites spawnBin, so MojoBackend.spawn refuses the * wrapper it never asked for) — and they used to explain it differently. The * device path told operators to "run fully remote (cloud on, localDaemon off)" * even when cloud was already on and the real blocker was an env key, which is * advice that cannot be acted on. Both now read from this one function. * * Returns key NAMES only, never values: these strings reach logs and chat, and * one of the keys is by definition the operator's credential variable. * * Ordering mirrors isMojoFullyRemote so the two can never disagree about whether * there is a problem — only about how much detail to print. */ export declare function mojoRemoteProofFailureReason(cfg?: MojoRemoteProofInput): string | undefined; /** * The ONLY setting that may change on a live session: the JWT. * * Deliberately this narrow. The previous version allowed an arbitrary `env` patch, * which review showed is equivalent to replacing the launcher: with no * `cliPathOverride` the backend spawns the bare name `mojo`, so a live * `env: { PATH: }` executes a different binary on the next * turn — and `NODE_OPTIONS` / `LD_PRELOAD` / `DYLD_*` are comparable. Enumerating * dangerous variables cannot be made complete, so `env` is not patchable at all. * * Note this is not about a compromised daemon (which already has more authority * than this); the problem was that a completely legitimate, validated patch could * change what gets executed. * * Everything else in `mojo` (env / stream / systemPrompt / idleTimeoutSec) now * requires a new session, matching how the control plane already behaves. */ export declare const MOJO_LIVE_PATCH_KEYS: readonly ["jwt"]; /** * A COMPLETE snapshot of the live-updatable state, not a sparse diff. * * `null` is a tombstone meaning "cleared"; `undefined` means the daemon has * nothing to say. The distinction matters because a sparse patch could neither * clear a credential nor roll one back: with `undefined` skipped on both sides, * deleting `mojo.jwt` left the backend holding the old token indefinitely. */ export interface MojoLivePatch { /** * Resolved JWT: the literal `mojo.jwt`, or the value read from `jwtEnv` — * resolved DAEMON-side so the backend never receives an env map. `null` * clears it (fall back to whatever the host login provides). */ jwt?: string | null; } /** * Build the complete live snapshot for a session. * * Resolves `jwtEnv` here rather than shipping the env map, so credential rotation * works without giving a patch the power to change PATH / loader variables. * Always returns an explicit value (`null` when there is no credential), because * an omitted field cannot express "cleared". */ export declare function pickMojoLivePatch(cfg: MojoConfig | undefined, sources?: { /** Per-bot `env` (top level). Lower precedence than `mojo.env`. */ genericEnv?: Record; /** Ambient env, lowest precedence. Defaults to the daemon's own. */ ambientEnv?: NodeJS.ProcessEnv; }): MojoLivePatch; /** * Validate a live patch arriving over IPC. * * Separate from normalizeMojoConfig on purpose: that one validates the USER's * config block, where `jwt` must be a non-empty string. A live patch is a * different shape — `null` is a meaningful tombstone — so reusing the config * validator rejected every clear request and the backend never saw it. */ export declare function normalizeMojoLivePatch(raw: unknown): { ok: true; value: MojoLivePatch; } | { ok: false; errors: string[]; }; /** Outcome of validating a raw `mojo` config block. */ export type MojoConfigNormalizeResult = { ok: true; value: MojoConfig; } | { ok: false; errors: string[]; }; /** * Validate a raw `mojo` block from ANY entry point (bots.json, `/config set * mojo`, dashboard, or a defensive check before it reaches the worker). * * Fails closed on: unknown keys (typos like `cluod` would silently disable the * cloud sandbox), internal launch-identity keys (they have top-level owners and * are frozen on the session), and wrong types. * * Type strictness is a SECURITY requirement here, not tidiness — see * strictBoolean for the concrete fail-open it prevents. */ export declare function normalizeMojoConfig(raw: unknown): MojoConfigNormalizeResult; /** Result of one subtree scan during termination. `scanned:false` means the tree * could not be enumerated, which must never be read as "nothing is running". */ export interface MojoTreeScanOutcome { scanned: boolean; pids: number[]; reason?: string; } //# sourceMappingURL=mojo-types.d.ts.map