import { z } from "zod"; export const platformSchema = z.enum(["ios", "android"]); export type Platform = z.infer; export const deviceClassSchema = z.enum([ "phone", "tablet", "watch", "tv", "vision", "auto", "desktop", ]); export type DeviceClass = z.infer; const deviceStateSchema = z.enum([ "provisioning", "ready", "leased", "reclaiming", "quarantined", "absent", "shutdown", "deleted", ]); const absentReasonSchema = z.enum(["locked", "untrusted", "not-present"]); const devicePresenceSchema = z.discriminatedUnion("present", [ z.object({ present: z.literal(true) }), z.object({ present: z.literal(false), reason: absentReasonSchema }), ]); const deviceModeSchema = z.enum(["slim", "full"]); const leaseIdentitySchema = z.enum(["reusable", "fresh"]); const physicalClassSchema = z.enum(["phone", "tablet"]); function checkDeviceType( device: { readonly physical: boolean; readonly mode?: unknown; readonly class?: unknown }, context: z.RefinementCtx, requireClass: boolean, ): void { if (device.physical) { if (device.mode !== undefined) { context.addIssue({ code: "custom", message: "A physical device has no mode", path: ["mode"], }); } if (requireClass && device.class === undefined) { context.addIssue({ code: "custom", message: "A physical device has a class", path: ["class"], }); } return; } if (device.mode === undefined) { context.addIssue({ code: "custom", message: "A virtual device has a mode", path: ["mode"], }); } if (device.class !== undefined) { context.addIssue({ code: "custom", message: "A virtual device has no class", path: ["class"], }); } } const IMAGE_TAG_MAX = 64; /** * An image tag as a lease request names it, such as `google_apis_playstore`: the word the catalog * lists as an image's `tag`. Bounded here, before a driver sees it, because it arrives over the * wire and is stored with the request and on the device. */ export const imageTagSchema = z .string() .min(1) .max(IMAGE_TAG_MAX) .regex(/^[A-Za-z0-9_.-]+$/, "imageTag may contain only letters, digits, '_', '.' and '-'"); /** why: a baseline's name is its handle on the command line and in a path-like URL, so it is lowercase and starts with a letter or digit. */ export const baselineNameSchema = z .string() .min(1) .max(64) .regex( /^[a-z0-9][a-z0-9._-]*$/, "baseline name may contain only a-z, 0-9, '.', '_' and '-', and must start with a letter or digit", ); // oxlint-disable-next-line no-control-regex -- refusing control characters is the whole point. const NO_CONTROL_CHARACTERS = /^[^\u0000-\u001f\u007f]*$/; export const baselineNoteSchema = z .string() .max(200) .regex(NO_CONTROL_CHARACTERS, "baseline note may not contain control characters"); /** why: the bound a worker id has on the wire, shared by the events page, a baseline entry and `baseline.remove`. */ export const WORKER_ID_MAX = 128; /** why: one baseline as `baseline.save` and `baseline.list` answer it; `sizeBytes` is absent when the source's size could not be counted. */ export const baselineEntrySchema = z.object({ name: baselineNameSchema, version: z.number().int().min(1), platform: platformSchema, model: z.string().max(64), osVersion: z.string().max(64), mode: z.enum(["slim", "full"]), imageTag: imageTagSchema.optional(), note: baselineNoteSchema.optional(), sizeBytes: z.number().int().min(0).optional(), savedAt: z.string().max(64), staleReason: z.string().max(300).optional(), /** why: set only by a gateway's `baseline.list`: the worker that holds the baseline, what `--worker` takes. */ workerId: z.string().min(1).max(WORKER_ID_MAX).optional(), }); export type BaselineEntry = z.infer; const REQUESTER_ID_MAX = 256; const GATEWAY_PREFIX = /^gw:[^:]{0,100}:/; /** * A requester id as any input names it: printable, no control characters, at most 256 characters. * why: 256 holds every principal Simlock builds (a sign-in-proxy `proxy:#` reaches 255). * why: a gateway hands its worker `gw::`, so the limit counts what * follows that prefix and the worker accepts the namespaced form of an id the gateway let in. */ export const requesterIdSchema = z .string() .refine( (id) => !/[\p{Cc}\u2028\u2029]/u.test(id), "requesterId must not contain control characters", ) .refine( (id) => id.replace(GATEWAY_PREFIX, "").length <= REQUESTER_ID_MAX, `requesterId may be at most ${String(REQUESTER_ID_MAX)} characters`, ); const deviceSpecSchema = z.object({ physical: z.boolean(), platform: platformSchema, model: z.string(), osVersion: z.string(), imageTag: imageTagSchema.optional(), baseline: z.object({ name: baselineNameSchema, version: z.number().int().min(1) }).optional(), }); const QUARANTINE_MESSAGE_MAX = 300; const quarantineCauseSchema = z .object({ kind: z.enum(["purge-failed", "shutdown-failed", "delete-failed", "stalled"]), stalledState: z.enum(["provisioning", "reclaiming"]).optional(), message: z .string() .min(1) .max(QUARANTINE_MESSAGE_MAX) .refine((text) => !/[\p{Cc}\u2028\u2029]/u.test(text)) .optional(), }) .refine((cause) => cause.kind === "stalled" ? cause.stalledState !== undefined && cause.message === undefined : cause.stalledState === undefined, ); /** * The one place a failure's text becomes a quarantine cause's `message`: control characters to * spaces, trimmed, and over the bound cut to end with `…` inside it. Empty gives no message. */ export function quarantineCauseMessage(text: string): string | undefined { const cleaned = text.replace(CONTROL_CHARACTERS, " ").trim(); if (cleaned === "") return undefined; if (cleaned.length <= QUARANTINE_MESSAGE_MAX) return cleaned; return boundDriverSummary(cleaned, QUARANTINE_MESSAGE_MAX); } /** The longest `detail` a `driver-unavailable` finding carries. */ export const DRIVER_DETAIL_MAX = 1000; /** Text cut to `max` characters, ending in `…` inside it and never inside a surrogate pair. */ export function boundDriverSummary(text: string, max: number): string { if (text.length <= max) return text; let cut = text.slice(0, max - 1); const last = cut.charCodeAt(cut.length - 1); if (last >= 0xd800 && last <= 0xdbff) cut = cut.slice(0, -1); return `${cut}…`; } interface CauseCarrier { readonly quarantineCause?: { readonly message?: string | undefined } | undefined; } function withoutMessage(device: T): T { if (device.quarantineCause?.message === undefined) return device; const { message: _message, ...kept } = device.quarantineCause; return { ...device, quarantineCause: kept }; } /** * What an `agent` session may read of a `status.get` answer: every quarantine cause, in `devices` * and in each `workers[].devices`, without its failure text. The one place the role rule lives. */ export function withoutQuarantineMessages< S extends { readonly devices?: readonly CauseCarrier[] | undefined; readonly workers?: | readonly { readonly devices?: readonly CauseCarrier[] | undefined }[] | undefined; }, >(status: S): S { return { ...status, ...(status.devices === undefined ? {} : { devices: status.devices.map(withoutMessage) }), ...(status.workers === undefined ? {} : { workers: status.workers.map((worker) => worker.devices === undefined ? worker : { ...worker, devices: worker.devices.map(withoutMessage) }, ), }), }; } export const deviceRecordSchema = z .object({ id: z.string(), driverDeviceId: z.string(), spec: deviceSpecSchema, state: deviceStateSchema, driverData: z.unknown().optional(), createdAt: z.number(), lastLeaseEndedAt: z.number().optional(), foreignStateDetectedAt: z.number().optional(), foreignProvenanceDetectedAt: z.number().optional(), recoveringSince: z.number().optional(), recoveryAttempts: z.number().optional(), quarantinedAt: z.number().optional(), quarantineAttempts: z.number().optional(), quarantineNextRetryAt: z.number().optional(), quarantineCause: quarantineCauseSchema.optional(), address: z.string().optional(), absentReason: absentReasonSchema.optional(), physical: z.boolean(), mode: deviceModeSchema.optional(), class: physicalClassSchema.optional(), leaseIdentity: leaseIdentitySchema.optional(), transitionAgeMs: z.number().optional(), stalled: z.boolean().optional(), servesDefaultMode: z.boolean().optional(), presence: devicePresenceSchema.optional(), }) .superRefine((device, context) => checkDeviceType(device, context, true)); /** * `driverDeviceId` is here only for a physical device: its canonical ID is how an operator names * it to `device remove`, so every role may see it; a virtual device's driver address is not * actionable for a caller that does not hold it, so it is dropped. */ export const statusDeviceSchema = z .object({ id: z.string(), spec: deviceSpecSchema, state: deviceStateSchema, physical: z.boolean(), mode: deviceModeSchema.optional(), class: physicalClassSchema.optional(), driverDeviceId: z.string().optional(), servesDefaultMode: z.boolean(), foreignStateDetectedAt: z.number().optional(), foreignProvenanceDetectedAt: z.number().optional(), quarantineAttempts: z.number().optional(), quarantineNextRetryAt: z.number().optional(), quarantineCause: quarantineCauseSchema.optional(), absentReason: absentReasonSchema.optional(), transitionAgeMs: z.number().optional(), stalled: z.boolean().optional(), presence: devicePresenceSchema.optional(), workerId: z.string().optional(), }) .superRefine((device, context) => checkDeviceType(device, context, true)) // why: `list.get` records carry a virtual device's driver address, and a worker view parses them // through this schema: it drops the address of a device whose canonical ID is not shown. .transform((device): typeof device => { if (device.physical || device.driverDeviceId === undefined) return device; const { driverDeviceId: _address, ...rest } = device; return rest; }); export const grantedDeviceSchema = z .object({ id: z.string(), driverDeviceId: z.string(), spec: deviceSpecSchema, /** * why: The driver-reported address (see `DriverDevice.address`), current as of this device's * last `ready` transition. Undefined for a device still `provisioning` (never made ready * yet) and for a device that was shut down, which has no address to reach. It becomes * defined the next time the device is made ready (`bootForLease`/`readyProvisionedForLease`); * nothing here ever guesses at a value it wasn't told. */ address: z.string().optional(), physical: z.boolean(), mode: deviceModeSchema.optional(), class: physicalClassSchema.optional(), }) .superRefine((device, context) => checkDeviceType(device, context, false)); /** * Mirrors `LeaseRecord` (src/core/domain.ts) field for field -- there is no decoration on top * of it any more. Includes `ownerId` (ADR 0003 §4): the session principal that requested the * lease, distinct from `requesterId` (attribution, defaults to the principal but may differ * per request on the same connection). * * ADR 0004: there is one kind of lease, so `mode` is gone; `ttlMs` and `lastRenewedAt` are * stored fields of the record rather than anything derived. `ttlMs` is the width this lease * was granted with, or last renewed with when a renew named one -- what a body-less * `lease.renew` re-applies, which is why it travels with the record instead of the caller * having to remember it. `lastRenewedAt` is written at grant and on every renew; it replaces * the old derived `lastHeartbeatAt` decoration, which was computed as `ttlDeadline - * heldTtlBackstopMs` and has no answer once every lease carries its own TTL. */ export const leaseRecordSchema = z.object({ id: z.string(), deviceId: z.string(), requesterId: z.string(), ownerId: z.string(), grantedAt: z.number(), ttlMs: z.number(), ttlDeadline: z.number(), lastRenewedAt: z.number(), idChosenByRequester: z.boolean(), worker: z.object({ id: z.string(), label: z.string().optional() }).optional(), }); const leaseTimingSchema = z.object({ estimatedProvisionMs: z.number(), estimatedBootMs: z.number(), estimatedReclaimMs: z.number(), estimatedReadyMs: z.number(), }); const leaseEnvironmentSchema = z.record(z.string(), z.string()); export const leaseGrantSchema = z.object({ device: grantedDeviceSchema, /** * why: Always present, `{}` at the least: containment cuts both ways, and a grant that did not * carry it would hand back a `driverDeviceId` no documented workflow can address. */ environment: leaseEnvironmentSchema, lease: leaseRecordSchema, timing: leaseTimingSchema, }); const requestedDeviceSchema = z.object({ platform: platformSchema, model: z.string().optional(), class: deviceClassSchema.optional(), osVersion: z.string().optional(), mode: deviceModeSchema.optional(), imageTag: imageTagSchema.optional(), baseline: baselineNameSchema.optional(), physical: z.literal(true).optional(), }); /** `grant` is stored as granted -- a later renew does not move its `ttlDeadline`. */ export const leaseRequestRecordSchema = z.object({ id: z.string(), requesterId: z.string(), ownerId: z.string(), idempotencyKey: z.string().optional(), request: requestedDeviceSchema, createdAt: z.number(), state: z.enum(["open", "granted", "failed", "cancelled"]), settledAt: z.number().optional(), grant: leaseGrantSchema.optional(), failure: z.object({ code: z.string(), message: z.string() }).optional(), }); const waitingReasonSchema = z.enum([ "behind-earlier-request", "device-limit", "ram-budget", "global-running-limit", "platform-running-limit", "waiting-for-device", "no-free-physical", ]); export const capacityReasonSchema = z.enum([ "device-limit", "ram-budget", "global-running-limit", "platform-running-limit", ]); const gatewayWaitingReasonSchema = z.enum([ "disconnected", "incompatible", "not-ready", "unhealthy", "drained", "updating", "busy", "no-driver", "unknown-model", "runtime-missing", "no-baseline", "baseline-mismatch", "baseline-stale", "capability-missing", "full", "no-free-physical", "not-chosen", ]); const waitingRequestShape = z.object({ id: z.string(), requesterId: z.string(), spec: requestedDeviceSchema, createdAt: z.number(), stage: z.enum(["queued", "starting"]), queuePosition: z.number().int().positive().optional(), reason: waitingReasonSchema.optional(), workerId: z.string().optional(), workers: z .array( z.object({ workerId: z.string(), reason: gatewayWaitingReasonSchema, capacityReason: capacityReasonSchema.optional(), }), ) .optional(), }); /** * A request still waiting for a device (`list.get` with `requests`, `status.get`'s `waiting`). * `stage` is `queued` while the request holds a place in the queue, with that place in * `queuePosition`, counted from 1 the way `lease.queued` counts it; `starting` while the daemon is * working on it: placing it as it arrives, or finding, making, booting or downloading a device * for it. `reason` says why a `queued` request is not placed yet, from its latest attempt. * `workerId` names the worker whose own queue the request waits in; only a gateway sets it. The request's idempotency key and owner are * never part of it: a key lets its holder replay the request, and neither is the operator's. */ export const waitingRequestSchema = Object.assign( waitingRequestShape .refine((request) => request.reason === undefined || request.stage === "queued", { message: "a starting request has no reason", path: ["reason"], }) .refine( (request) => (request.workers ?? []).every( (entry) => entry.capacityReason === undefined || entry.reason === "full", ), { message: "a capacity reason belongs to full", path: ["workers"] }, ) .refine( (request) => new Set((request.workers ?? []).map((entry) => entry.workerId)).size === (request.workers ?? []).length, { message: "a worker is listed once", path: ["workers"] }, ), // why: the refinement hides the object's `shape`, which a reader of the field list still needs. { shape: waitingRequestShape.shape }, ); /** * The scoped command `simlock simctl` / `simlock adb` runs on the caller's behalf. The command * is spawned as-is, so the shape is validated at the boundary like every other output: an * argument list that is not entirely strings would otherwise stringify into whatever the tool * made of it. */ export const passthroughCommandSchema = z.object({ args: z.array(z.string()), command: z.string().min(1), env: z.record(z.string(), z.string()), }); export const leaseProgressSchema = z.discriminatedUnion("stage", [ z.object({ stage: z.literal("queued"), queuePosition: z.number() }), z.object({ stage: z.literal("downloading"), component: z.string().max(64), waiting: z.boolean(), percent: z.number().int().min(0).max(100).optional(), }), z.object({ stage: z.literal("provisioning"), etaMs: z.number() }), z.object({ stage: z.literal("booting"), etaMs: z.number() }), z.object({ stage: z.literal("reclaiming"), etaMs: z.number() }), ]); const runningCapacityEntrySchema = z.object({ running: z.number(), maxRunning: z.number(), reserved: z.number(), overLimit: z.boolean(), }); const platformCapacityEntrySchema = runningCapacityEntrySchema.extend({ limit: z.number(), warm: z.number(), used: z.number(), atRamBudget: z.boolean(), }); export const statusCapacitySchema = z.object({ ios: platformCapacityEntrySchema, android: platformCapacityEntrySchema, global: runningCapacityEntrySchema.extend({ warm: z.number() }), /** why: present only under a strategy that keeps a RAM budget. `usedBytes` counts every * device neither deleted nor shut down, by the mode it reports, without in-flight provisioning. */ ramBudget: z .object({ limitBytes: z.number().finite(), usedBytes: z.number().finite().nonnegative(), overLimit: z.boolean(), }) .optional(), }); /** Bounds on the warm pool block: a gateway parses it off the wire from every worker. */ /** why: the most configured targets the config load accepts (the block itself takes `WARM_POOL_ENTRIES_MAX`), and the longest model or OS version the block names. */ export const WARM_POOL_TARGETS_MAX = 256; export const WARM_POOL_NAME_MAX = 256; /** why: the most learned kinds the pool keeps per platform, so the most one `warm.forget` answers (a match names one platform). */ export const WARM_POOL_LEARNED_KINDS_MAX = 32; /** why: the configured targets plus the learned kinds of both platforms the pool keeps. */ const WARM_POOL_ENTRIES_MAX = WARM_POOL_TARGETS_MAX + 2 * WARM_POOL_LEARNED_KINDS_MAX; const warmPoolShortSchema = z.enum([ "disabled", "no-driver", "runtime-missing", "unknown-model", "unresolvable", "boot-failed", "device-limit", "running-limit", "reserve", "ram-budget", ]); const warmPoolTargetSchema = z.object({ platform: platformSchema, model: z.string().max(WARM_POOL_NAME_MAX), osVersion: z.string().max(WARM_POOL_NAME_MAX).optional(), mode: z.enum(["slim", "full"]), imageTag: imageTagSchema.optional(), baseline: baselineNameSchema.optional(), origin: z.enum(["configured", "learned"]), count: z.number().int().nonnegative(), ready: z.number().int().nonnegative(), booting: z.number().int().nonnegative(), short: warmPoolShortSchema.optional(), }); /** The warm pool as `status.get` and a worker's view report it. */ export const statusWarmPoolSchema = z.object({ enabled: z.boolean(), reserveRunning: z.object({ ios: z.number().int().nonnegative(), android: z.number().int().nonnegative(), }), targets: z.array(warmPoolTargetSchema).max(WARM_POOL_ENTRIES_MAX), }); export const daemonHealthSchema = z.enum(["starting", "running", "failed"]); const UPDATE_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/; const UPDATE_VERSION_MAX = 64; const UPDATE_MESSAGE_MAX = 2000; const updateVersionSchema = z.string().max(UPDATE_VERSION_MAX); export const updateRequestedVersionSchema = updateVersionSchema.regex(UPDATE_VERSION_PATTERN); const updateStateBase = { from: updateVersionSchema, to: updateVersionSchema, forced: z.boolean(), startedAt: z.number().int().min(0), }; const updateStateSchema = z.discriminatedUnion("stage", [ z.object({ stage: z.literal("waiting"), leases: z.number().int().min(0), ...updateStateBase }), z.object({ stage: z.literal("installing"), ...updateStateBase }), z.object({ stage: z.literal("restarting"), ...updateStateBase }), ]); export const updateFailureSchema = z.object({ stage: z.enum(["installing", "restarting"]), from: updateVersionSchema, to: updateVersionSchema, message: z.string().max(UPDATE_MESSAGE_MAX), at: z.number().int().min(0), }); export type UpdateState = z.infer; export type UpdateFailure = z.infer; export const daemonUpdateFields = { version: updateVersionSchema, update: updateStateSchema.nullable(), lastUpdateFailure: updateFailureSchema.nullable(), latestVersion: updateVersionSchema.nullable(), }; const CATALOG_NAME_MAX = 256; const CATALOG_ALIASES_PER_MODEL_MAX = 32; const CATALOG_ALIASED_MODELS_MAX = 4096; const CATALOG_IMAGE_FIELD_MAX = 128; const CATALOG_IMAGES_MAX = 1024; const CATALOG_CUSTOM_MODELS_MAX = CATALOG_ALIASED_MODELS_MAX; /** * The list bounds above, for a producer that merges lists the schema checked one by one: a * gateway's union of valid worker catalogs must itself fit, or its own clients refuse it. */ export const CATALOG_LIST_LIMITS = { aliasedModels: CATALOG_ALIASED_MODELS_MAX, aliasesPerModel: CATALOG_ALIASES_PER_MODEL_MAX, customModels: CATALOG_CUSTOM_MODELS_MAX, images: CATALOG_IMAGES_MAX, } as const; const catalogImageSchema = z.object({ runtime: z.string().max(CATALOG_IMAGE_FIELD_MAX), tag: z.string().max(CATALOG_IMAGE_FIELD_MAX), abi: z.string().max(CATALOG_IMAGE_FIELD_MAX), }); const MAX_HOST_STRING_LENGTH = 128; const MAX_HOST_TOOLS = 32; const hostStringSchema = z.string().min(1).max(MAX_HOST_STRING_LENGTH); export const hostFactsSchema = z.object({ os: hostStringSchema, osVersion: hostStringSchema, arch: hostStringSchema, tools: z .array( z.object({ platform: platformSchema, name: hostStringSchema, version: hostStringSchema, build: hostStringSchema.optional(), }), ) .max(MAX_HOST_TOOLS), }); interface HostFactsShape { readonly arch: string; readonly os: string; readonly osVersion: string; readonly tools: readonly unknown[]; } /** * Brings host facts a daemon read from its own machine within `hostFactsSchema`'s bounds, so an * odd value cannot fail the daemon's own `status.get` output check and take its liveness probe * down (ADR 0008 §7). A tool that does not fit is left out, like one that cannot be read; a host * string that does not fit is cut to length; the list stops at its maximum. Lives beside the * schema so the bounds are written once. */ export function fitHostFacts(host: Host): Host { const fit = (value: string) => value.slice(0, MAX_HOST_STRING_LENGTH); const tool = hostFactsSchema.shape.tools.element; return { ...host, arch: fit(host.arch), os: fit(host.os), osVersion: fit(host.osVersion), tools: host.tools.filter((entry) => tool.safeParse(entry).success).slice(0, MAX_HOST_TOOLS), }; } const MAX_LOAD_AGE_MS = 86_400_000; const loadNumber = z.number().finite().nonnegative(); const loadCpuSchema = z.object({ busyPercent: loadNumber.max(100), cores: loadNumber.positive(), limited: z.boolean(), }); const loadMemorySchema = z.object({ totalBytes: loadNumber, availableBytes: loadNumber, swapUsedBytes: loadNumber, pressure: z.enum(["normal", "warn", "critical"]).optional(), limited: z.boolean(), }); const loadDiskSchema = z.object({ freeBytes: loadNumber, totalBytes: loadNumber }); export const hostLoadSchema = z.object({ ageMs: z.number().int().min(0).max(MAX_LOAD_AGE_MS), cpu: loadCpuSchema.optional(), memory: loadMemorySchema.optional(), disk: loadDiskSchema.optional(), }); // why: a view stores when the load was read (`readAt`, the viewer's clock), not its age, so the age stays right as the view sits. const workerLoadSchema = z.object({ readAt: z.number().int().min(0), cpu: loadCpuSchema.optional(), memory: loadMemorySchema.optional(), disk: loadDiskSchema.optional(), }); /** ADR 0029 §4: what `status.get` carries about the machine's load, as old as its last read. */ export type StatusLoad = z.infer; /** * Brings a reading within `hostLoadSchema`'s bounds, so an odd figure cannot fail the daemon's own * `status.get` output check (ADR 0029 §4): `ageMs` is capped at one day and a part whose figures * break the schema is left out, as one that could not be read. */ export function fitHostLoad(load: Load | undefined): Load | undefined { if (load === undefined) return undefined; const { cpu, memory, disk, ...rest } = load; const ageMs = Number.isFinite(load.ageMs) ? Math.min(MAX_LOAD_AGE_MS, Math.max(0, Math.round(load.ageMs))) : MAX_LOAD_AGE_MS; return { ...rest, ageMs, ...(cpu !== undefined && loadCpuSchema.safeParse(cpu).success && { cpu }), ...(memory !== undefined && loadMemorySchema.safeParse(memory).success && { memory }), ...(disk !== undefined && loadDiskSchema.safeParse(disk).success && { disk }), } as Load; } export const platformCatalogSchema = z.object({ platform: platformSchema, models: z.array(z.string()), runtimes: z.array(z.string()), defaultRuntime: z.string().optional(), modelRuntimes: z.record(z.string(), z.array(z.string())), modelAliases: z .record( z.string().max(CATALOG_NAME_MAX), z.array(z.string().max(CATALOG_NAME_MAX)).max(CATALOG_ALIASES_PER_MODEL_MAX), ) .refine((aliases) => Object.keys(aliases).length <= CATALOG_ALIASED_MODELS_MAX, { message: `modelAliases lists more than ${CATALOG_ALIASED_MODELS_MAX} models`, }), modelClasses: z .record(z.string().max(CATALOG_NAME_MAX), deviceClassSchema) .refine((classes) => Object.keys(classes).length <= CATALOG_ALIASED_MODELS_MAX), classDefaults: z.partialRecord(deviceClassSchema, z.string().max(CATALOG_NAME_MAX)), images: z.array(catalogImageSchema).max(CATALOG_IMAGES_MAX).optional(), customModels: z.array(z.string().max(CATALOG_NAME_MAX)).max(CATALOG_CUSTOM_MODELS_MAX).optional(), /** * why: ADR 0005 §21: on a gateway, `models`/`runtimes` are the *union* over the fleet, and these * two maps say which workers each entry came from (`{"iPhone 16": ["wrk_a", "wrk_b"]}`). * Additive and gateway-only: a worker's own catalog needs no attribution, and a renderer * that predates the fleet still reads `models`/`runtimes` unchanged. Kept as annotations * beside the flat lists rather than as a change to their element type precisely so that * stays true. */ modelWorkers: z.record(z.string(), z.array(z.string())).optional(), runtimeWorkers: z.record(z.string(), z.array(z.string())).optional(), }); /** * Brings a catalog entry a daemon built from its own machine within `customModels`' bounds, so * a custom profile name the platform's tools never checked cannot fail the whole `catalog.get` * answer. A name that does not fit loses its mark and stays in `models`; the list stops at its * maximum, and an empty list is left out. Lives beside the schema so the bounds are written once. */ export function fitPlatformCatalog< Entry extends { readonly customModels?: readonly string[]; readonly modelClasses?: Readonly>; readonly classDefaults?: Readonly>>; }, >(entry: Entry): Entry { const { classDefaults, customModels, modelClasses, ...rest } = entry; const fitting = (customModels ?? []) .filter((model) => model.length <= CATALOG_NAME_MAX) .slice(0, CATALOG_CUSTOM_MODELS_MAX); return { ...rest, ...(modelClasses === undefined ? {} : { modelClasses: Object.fromEntries( Object.entries(modelClasses).filter(([model]) => model.length <= CATALOG_NAME_MAX), ), }), ...(classDefaults === undefined ? {} : { classDefaults: Object.fromEntries( Object.entries(classDefaults).filter(([, model]) => model.length <= CATALOG_NAME_MAX), ), }), ...(fitting.length === 0 ? {} : { customModels: fitting }), } as unknown as Entry; } export const proposalSchema = z.object({ action: z.enum(["shutdown", "destroy"]), reason: z.string(), rule: z.string(), target: z.string(), }); export const cleanupRuleSummarySchema = z.object({ name: z.string() }); /** why: Mirrors `DriverDevice` (src/core/driver.ts) -- `driverData` stays opaque `unknown` on * purpose, exactly as the core treats it. */ const driverDeviceSchema = z.object({ deviceId: z.string(), driverData: z.unknown().optional(), address: z.string(), mode: deviceModeSchema.optional(), }); export const capabilityNameSchema = z.string().regex(/^[a-z][a-z0-9-]{0,31}$/); // why: mirrors the `DoctorFinding` union (src/core/doctor.ts) field for field, so a new member lands in both. const doctorFindingSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("registry-device-missing"), deviceId: z.string(), platform: platformSchema, }), z.object({ kind: z.literal("orphan-device"), device: driverDeviceSchema, platform: platformSchema, }), z.object({ kind: z.literal("orphan-process"), device: driverDeviceSchema, platform: platformSchema, }), z.object({ kind: z.literal("expired-live-lease"), leaseId: z.string(), deviceId: z.string() }), z.object({ kind: z.literal("foreign-state-change"), deviceId: z.string(), platform: platformSchema, expected: z.enum(["running", "stopped"]), observed: z.enum(["running", "stopped"]), }), z.object({ kind: z.literal("foreign-provenance-change"), deviceId: z.string(), platform: platformSchema, detail: z.enum(["erased", "mark-mismatch", "durable-mark-missing"]), }), z.object({ kind: z.literal("stalled-transition"), deviceId: z.string(), platform: platformSchema, state: z.enum(["provisioning", "reclaiming"]), enteredAt: z.number(), ageMs: z.number(), thresholdMs: z.number(), }), z.object({ kind: z.literal("driver-advisory"), platform: platformSchema, code: z.string(), message: z.string(), }), z.object({ kind: z.literal("driver-unavailable"), platform: platformSchema, reason: z.string().max(64), detail: z.string().max(DRIVER_DETAIL_MAX), }), z.object({ kind: z.literal("service-broken"), manager: z.enum(["launchd", "systemd"]), name: z.string(), missing: z.array(z.string()).min(1), remedy: z.string(), }), z.object({ kind: z.literal("prerequisite-missing"), platform: platformSchema, prerequisite: z.string(), message: z.string(), remedy: z.string(), }), z.object({ kind: z.literal("warm-pool-target-unreachable"), platform: platformSchema.optional(), target: z.string(), reason: z.enum(["runtime-missing", "unknown-model", "over-limit"]), message: z.string(), remedy: z.string(), }), z.object({ kind: z.literal("module-unavailable"), module: capabilityNameSchema, message: z.string(), remedy: z.string(), }), ]); const doctorSourceSchema = z.object({ platform: platformSchema, kind: z.enum(["seed", "baseline"]), model: z.string().max(200), osVersion: z.string().max(200), imageTag: z.string().max(200).optional(), sizeBytes: z.number().int().nonnegative().optional(), staleReason: z.string().max(300).optional(), orphan: z.literal(true).optional(), baselineName: baselineNameSchema.optional(), baselineVersion: z.number().int().min(1).optional(), }); export const doctorReportSchema = z.object({ findings: z.array(doctorFindingSchema).max(100_000), sources: z.array(doctorSourceSchema).max(100_000), }); export const nukeReportSchema = z.object({ deletedDevices: z.array(z.string()), releasedLeaseIds: z.array(z.string()), removedBaselines: z.array(baselineNameSchema).max(1000), }); const MAX_DATE_MS = 8.64e15; export const historyStatusSchema = z.object({ opened: z.boolean(), writing: z.boolean(), setAsideAt: z.number().min(-MAX_DATE_MS).max(MAX_DATE_MS).optional(), rebuiltAt: z.number().min(-MAX_DATE_MS).max(MAX_DATE_MS).optional(), rebuildFailed: z.literal(true).optional(), }); export const EVENT_ID_PATTERN = /^evt_[A-Za-z0-9_-]{1,64}$/; export const eventEnvelopeSchema = z.object({ id: z.string().regex(EVENT_ID_PATTERN), seq: z.number(), timestamp: z.number().min(-MAX_DATE_MS).max(MAX_DATE_MS), event: z.string(), payload: z.unknown().optional(), module: z.string(), }); const platformCapacityOptionsSchema = z.object({ maxDevices: z.number().optional(), maxRunning: z.number().optional(), }); const fixedCapacityConfigSchema = z.object({ strategy: z.literal("fixed"), config: z.object({ maxRunning: z.number(), ios: platformCapacityOptionsSchema.optional(), android: platformCapacityOptionsSchema.optional(), }), }); const capacityLimitsSchema = z.object({ maxRunning: z.number(), ios: z.object({ maxDevices: z.number(), maxRunning: z.number() }), android: z.object({ maxDevices: z.number(), maxRunning: z.number() }), }); const resourceCapacityConfigSchema = z.object({ strategy: z.literal("resource"), config: z.object({ limits: capacityLimitsSchema, ramBudget: z.object({ iosBytesPerDevice: z.number(), iosSlimBytesPerDevice: z.number().optional(), androidBytesPerDevice: z.number(), androidSlimBytesPerDevice: z.number().optional(), }), }), }); const capacityConfigSchema = z.discriminatedUnion("strategy", [ fixedCapacityConfigSchema, resourceCapacityConfigSchema, ]); const defaultModelsSchema = z.partialRecord(deviceClassSchema, z.array(z.string().min(1)).min(1)); const proxyAuthSchema = z.object({ header: z.string(), jwksUrl: z.string(), issuer: z.string(), audience: z.string(), signOutUrl: z.string().optional(), }); export const configSchema = z.object({ mode: z.enum(["worker", "gateway"]), capabilities: z.record( capabilityNameSchema, z.object({ enabled: z.boolean() }).catchall(z.unknown()), ), capacity: capacityConfigSchema, downloads: z.object({ policy: z.enum(["never", "on-request", "always"]), acceptAndroidLicenses: z.boolean(), timeoutMs: z.number(), }), idle: z.object({ shutdownAfterMs: z.number(), deleteAfterMs: z.number(), }), update: z.object({ check: z.boolean() }), warmPool: z.object({ enabled: z.boolean(), learn: z.boolean().optional(), reserveRunning: z.object({ ios: z.number(), android: z.number() }), maxConcurrentBoots: z.number(), targets: z.array( z.object({ platform: z.enum(["ios", "android"]), model: z.string(), osVersion: z.string().optional(), mode: z.enum(["slim", "full"]).optional(), count: z.number(), }), ), quarantine: z.object({ maxRetries: z.number(), retryBackoffMs: z.number(), retryBackoffMultiplier: z.number(), maxRetryBackoffMs: z.number(), }), }), lease: z.object({ defaultTtlMs: z.number(), maxTtlMs: z.number(), identity: z.object({ ios: leaseIdentitySchema, android: leaseIdentitySchema, }), requestRetentionMs: z.number(), maxRequestRecords: z.number(), maxPerRequester: z.union([ z.number(), z.object({ ios: z.number().optional(), android: z.number().optional() }), ]), endWaitMs: z.number(), }), exec: z.object({ timeoutMs: z.number() }), diskPressure: z.object({ freeBytesThreshold: z.number() }), eventBuffer: z.object({ capacity: z.number() }), log: z.object({ level: z.enum(["debug", "info", "warn", "error"]), rotateBytes: z.number(), }), // why: Optional: a newer gateway parses this from an older worker's `config.get`. eventLog: z .object({ retention: z.number().optional(), maxBytes: z.number().optional(), }) .optional(), metrics: z.object({ retention: z.number() }).optional(), http: z.object({ enabled: z.boolean(), host: z.string(), port: z.number(), proxyAuth: proxyAuthSchema.optional(), }), health: z.object({ enabled: z.boolean(), probeIntervalMs: z.number(), stableObservations: z.number(), maxRecoveryAttempts: z.number(), recoveryBackoffMs: z.number(), maxConcurrentRecoveries: z.number(), }), ios: z.object({ defaultMode: deviceModeSchema, defaultModels: defaultModelsSchema, slim: z.object({ categories: z.array(z.string()).optional(), bootTimeoutMs: z.number(), }), }), android: z.object({ defaultModels: defaultModelsSchema, emulator: z.object({ headless: z.boolean(), gpu: z.string().min(1), audio: z.boolean(), bootAnimation: z.boolean(), }), }), gateway: z.object({ url: z.string().optional(), token: z.string().optional(), label: z.string().optional(), disconnectedRetentionMs: z.number(), execTimeoutMs: z.number(), leaseRequestTimeoutMs: z.number(), routing: z.enum(["warm-then-free"]), /** * why: `format` and `events` are re-declared, not imported from core; a test checks `format` against core's list. `secret` is optional so a `slack` destination parses. */ alerts: z.object({ destinations: z.array( z.object({ name: z.string(), format: z.enum(["generic", "slack"]), url: z.string(), events: z.array(z.string()), secret: z.string().optional(), }), ), }), }), stalledTransition: z.object({ thresholdMultiplier: z.number(), minimumThresholdMs: z.number(), }), }); /** * `TokenRole` (agent/operator/worker token store roles, src/auth/tokens.ts) is a * deliberately different vocabulary from the daemon session `Role` (agent/admin, see * `roles.ts`) -- a `token.create --role operator` mints a credential that *resolves to* the * admin session role at `hello` (ADR 0003 §5), it is not itself that role. Keeping the two * enums textually distinct here (rather than reusing `roleSchema`) is deliberate, not an * oversight. * * ADR 0005 §8/§25 adds `worker`: a join token, presented by a worker when it opens its uplink * to a gateway. It resolves to no session role at all -- it can open an uplink and nothing * else, and a `worker`-role token on a `/v1` route is `403` (see `src/http/auth.ts`). That is * why it is a third value here rather than a reuse of `agent`: the whole point is a credential * whose authority is a single transport, not a set of operations. */ export const tokenRoleSchema = z.enum(["agent", "operator", "worker"]); export const tokenRecordSchema = z.object({ id: z.string(), role: tokenRoleSchema, label: z.string().optional(), createdAt: z.number(), // why: the uplink cuts a claimed worker id to 128 characters before the token is pinned to it. workerId: z.string().max(128).optional(), }); /** * Which mode a daemon runs in (ADR 0005 §1). A worker owns devices -- drivers, registry, * capacity, leases -- and is what every simlock daemon has been until now, so it is the * default. A gateway owns none of that and fronts the workers connected to it. One process, * one mode. */ export const daemonModeSchema = z.enum(["worker", "gateway"]); const protocolRangeShapeSchema = z.object({ min: z.number().int(), max: z.number().int() }); const workerConnectionStateSchema = z.enum(["connected", "disconnected", "incompatible"]); const MAX_LISTED_INSTALLS = 16; const MAX_INSTALL_COMPONENT_LENGTH = 64; /** * why: One component install a daemon has waiting or running. A worker's list reaches the gateway as * a claim (safety rule 10), so the bounds cut rather than refuse: an over-long list keeps its * first (oldest) 16 entries and an over-long `component` its first 64 characters, and the view * stays usable instead of failing its whole refresh over one field. */ const installInProgressSchema = z.object({ platform: platformSchema, component: z.string().transform((value) => value.slice(0, MAX_INSTALL_COMPONENT_LENGTH)), state: z.enum(["waiting", "downloading"]), since: z.number(), waiters: z.number().int().nonnegative(), }); function boundedInstalls(entry: Entry) { return z.array(entry).transform((installs) => installs.slice(0, MAX_LISTED_INSTALLS)); } const workerInstallsSchema = boundedInstalls(installInProgressSchema); /** `status.get`'s installs. On a gateway each entry names the worker it runs on. */ export const statusInstallsSchema = boundedInstalls( installInProgressSchema.extend({ workerId: z.string().optional() }), ); /** * The bound above, for a producer that merges lists the schema checked one by one: a gateway's * fleet list must itself fit, or the schema cuts it wherever the merge left it. */ export const INSTALL_LIST_LIMIT = MAX_LISTED_INSTALLS; // why: \p{Cc} is exactly U+0000-U+001F and U+007F-U+009F, the spec's control characters. const CONTROL_CHARACTERS = /[\p{Cc}\u2028\u2029]/gu; const CAPABILITY_REASON_MAX = 500; export const capabilitySchema = z .object({ name: capabilityNameSchema, state: z.enum(["available", "unavailable", "off"]), reason: z .string() .min(1) .max(CAPABILITY_REASON_MAX) .refine((text) => !/[\p{Cc}\u2028\u2029]/u.test(text)) .optional(), }) .refine((entry) => (entry.state === "unavailable") === (entry.reason !== undefined)); /** The most capabilities one daemon reports. */ export const MAX_CAPABILITIES = 16; export const capabilityListSchema = z .array(capabilitySchema) .max(MAX_CAPABILITIES) .refine((list) => new Set(list.map((entry) => entry.name)).size === list.length); export function capabilityReason(text: string): string { let cut = text.replace(CONTROL_CHARACTERS, " ").trim().slice(0, CAPABILITY_REASON_MAX); const last = cut.charCodeAt(cut.length - 1); if (last >= 0xd800 && last <= 0xdbff) cut = cut.slice(0, -1); return cut === "" ? "unknown" : cut; } /** * What the gateway currently knows about one worker (ADR 0005's vocabulary table, §7, §31). * Rebuilt over the uplink, never persisted: a gateway restart re-derives every field from the * workers that reconnect (§30). * * The optional fields are exactly the ones that come from a `status.get`/`catalog.get` round * trip the gateway may not have been able to make: an `incompatible` worker is never asked * anything, and a `disconnected` view keeps whatever the last successful refresh saw. They are * absent rather than zeroed on purpose -- "no capacity reported" and "no capacity free" are * different facts, and a console that dims one must not read the other as a full machine. * The same holds for a worker whose health is `starting`: it answers `status.get` with `daemon` * and `host` only, so its view has `health` and `host` and none of `capabilities`, * `capacity`, `catalog`, `catalogReadAt`, `devices`, `installs`, `leases`, `physicalQueueDepth`, * `queueDepth`, `waiting` or `warmPool` -- never empty ones, which would * read as "nothing is leased" about a registry nobody has checked yet. * * A worker answers `worker.list` with one of these about itself (ADR 0012 §1): `connected`, * never drained, `lastSeenAt` the time of the call, and every reported field filled from its * own reads by `workerViewFields` (`worker-view.ts`), the same builder a gateway uses. */ export const workerViewSchema = z.object({ capabilities: capabilityListSchema.optional(), load: workerLoadSchema.optional(), id: z.string(), label: z.string().optional(), connection: workerConnectionStateSchema, drained: z.boolean(), lastSeenAt: z.number(), health: daemonHealthSchema.optional(), version: z.string().optional(), protocol: z .object({ gateway: protocolRangeShapeSchema, worker: protocolRangeShapeSchema }) .optional(), capacity: statusCapacitySchema.optional(), warmPool: statusWarmPoolSchema.optional(), downloads: z .object({ policy: z.enum(["never", "on-request", "always"]), /** why: Optional so a gateway on an older build still parses a view. */ timeoutMs: z.number().optional(), }) .optional(), /** why: A worker whose own cap is lower still refuses a request the gateway accepted; this lets the gateway notice. */ lease: z.object({ maxTtlMs: z.number() }).optional(), queueDepth: z.number().optional(), physicalQueueDepth: z.number().optional(), leases: z.array(leaseRecordSchema).optional(), devices: z.array(statusDeviceSchema).optional(), catalog: z.array(platformCatalogSchema).optional(), /** why: absent while a worker is starting, `[]` when it holds none: the worker's own `baseline.list`, read with the catalog. */ baselines: z.array(baselineEntrySchema).max(1000).optional(), /** why: Cleared on connect, so a just-connected worker has none: it tells a catalog read and found empty from one not yet arrived. */ catalogReadAt: z.number().optional(), host: hostFactsSchema.optional(), installs: workerInstallsSchema.optional(), waiting: z.array(waitingRequestSchema).optional(), update: daemonUpdateFields.update.optional(), lastUpdateFailure: daemonUpdateFields.lastUpdateFailure.optional(), latestVersion: daemonUpdateFields.latestVersion.optional(), }); export const statusLeaseSchema = leaseRecordSchema.extend({ workerId: z.string().optional(), // why: the part of a gateway lease id after `.`, so a relayed event naming the worker's lease can be matched; set only by a gateway's `lease.list`, `lease.get` and `list.get kind=leases`. workerLeaseId: z.string().optional(), }); const inboxGroupSchema = z.enum(["Broken", "Needs a look", "Heads-up"]); // why: an Inbox item names the worker it is about by the view's fields that the page does not read from the worker list, plus two counts the view's lists would otherwise have to be sent for. const inboxWorkerSchema = workerViewSchema .omit({ baselines: true, catalog: true, devices: true, downloads: true, leases: true }) .extend({ heldLeases: z.number().int().nonnegative(), leasedDevices: z.number().int().nonnegative(), }); const inboxDeviceSchema = statusDeviceSchema.and( z.object({ stateEnteredAt: z.number().optional(), workerId: z.string(), workerLabel: z.string().optional(), }), ); const inboxLeaseSchema = statusLeaseSchema.extend({ holder: z.string(), model: z.string(), }); const inboxItemBase = { group: inboxGroupSchema, key: z.string() }; /** * One thing that needs the operator (`inbox.get`): the rules in `inbox.ts` decide it. Each item * carries a small copy of the one subject it is about. */ export const inboxItemSchema = z.union([ z.object({ ...inboxItemBase, condition: z.enum(["disconnected", "incompatible", "drained", "update failed"]), worker: inboxWorkerSchema, }), z.object({ ...inboxItemBase, condition: z.literal("over RAM budget"), limitBytes: z.number(), usedBytes: z.number(), worker: inboxWorkerSchema, }), z.object({ ...inboxItemBase, condition: z.enum(["quarantined", "stalled", "absent"]), device: inboxDeviceSchema, }), z.object({ ...inboxItemBase, condition: z.literal("waiting"), request: waitingRequestSchema, }), z.object({ ...inboxItemBase, condition: z.literal("ending"), lease: inboxLeaseSchema, }), ]); const USAGE_MAX_WINDOW_MS = 90 * 24 * 60 * 60 * 1000; /** `usage.get`'s input: wire input, so both ends are bounded before anything is read (safety rule 10). */ export const usageWindowSchema = z .object({ from: z.number().int().min(0).max(MAX_DATE_MS), includeLedger: z.boolean().optional(), to: z.number().int().min(0).max(MAX_DATE_MS), }) .refine((window) => window.from < window.to, { message: "from must be before to" }) .refine((window) => window.to - window.from <= USAGE_MAX_WINDOW_MS, { message: "the window must not be longer than 90 days", }); const usageSamplesSchema = z.object({ count: z.number().int().nonnegative(), max: z.number().nullable(), p50: z.number().nullable(), p95: z.number().nullable(), }); const usageCountsSchema = z.record(z.string(), z.number().int().nonnegative()); const usageFiguresSchema = z.object({ bySource: z.object({ booted: z.number().int().nonnegative(), provisioned: z.number().int().nonnegative(), unknown: z.number().int().nonnegative().optional(), warm: z.number().int().nonnegative(), }), boot: usageSamplesSchema, declined: z.number().int().nonnegative(), failures: z.object({ byEvent: usageCountsSchema }), granted: z.number().int().nonnegative(), held: usageSamplesSchema, incidents: z.object({ crashRecovered: z.number().int().nonnegative(), lost: z.number().int().nonnegative(), quarantineRecovered: z.number().int().nonnegative(), quarantined: z.number().int().nonnegative(), }), probes: z.number().int().nonnegative().optional(), provisioning: usageSamplesSchema, queue: z.object({ meanDepth: z.number().nullable(), peakDepth: z.number().nullable() }), rejected: z.object({ byReason: usageCountsSchema, total: z.number().int().nonnegative(), }), requests: z.number().int().nonnegative(), turnaround: usageSamplesSchema, utilisation: z.object({ ram: z .object({ limitBytes: z.number(), meanBytes: z.number(), peakBytes: z.number(), }) .optional(), slots: z.object({ max: z.number().nullable(), mean: z.number().nullable(), peak: z.number().nullable(), }), }), wait: usageSamplesSchema, }); export const USAGE_LEDGER_MAX_ENTRIES = 100_000; const usageLedgerEntrySchema = z.object({ endReason: z.string().optional(), endedAt: z.number().optional(), grantedAt: z.number().optional(), heldMs: z.number().optional(), label: z.string().optional(), model: z.string().optional(), outcome: z.enum(["granted", "rejected", "declined", "open"]), platform: z.string(), probe: z.boolean(), reason: z.string().optional(), rejectedAt: z.number().optional(), requestId: z.string(), requestedAt: z.number(), requester: z.string(), source: z.string().optional(), turnaroundMs: z.number().optional(), waitMs: z.number().optional(), workerId: z.string().optional(), }); export type UsageLedgerEntry = z.infer; export const usageOutputSchema = z.object({ bucketMs: z.number().int().positive(), coversFrom: z.number(), ledgerTruncated: z.literal(true).optional(), requests: z.array(usageLedgerEntrySchema).optional(), partial: z.boolean(), platforms: z.object({ android: usageFiguresSchema, ios: usageFiguresSchema }), requesters: z.array( z.object({ granted: z.number().int().nonnegative(), heldTotalMs: z.number().nonnegative(), id: z.string(), label: z.string().optional(), rejected: z.number().int().nonnegative(), requests: z.number().int().nonnegative(), }), ), series: z.array( z.object({ at: z.number(), leasesHeld: z.number().nullable(), queueDepth: z.number().nullable(), ramUsedBytes: z.number().optional(), slotsMax: z.number().nullable(), slotsUsed: z.number().nullable(), waiting: z.number().nullable(), }), ), totals: usageFiguresSchema, window: z.object({ from: z.number(), to: z.number() }), workers: z.array(usageFiguresSchema.extend({ id: z.string(), label: z.string().optional() })), }); export type UsageFigures = z.infer; export type UsageOutput = z.infer;