@exponential_app/engine 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/chunks/{index-ax4aay30.js → index-3nf9f7pv.js} +2 -2
- package/dist/chunks/{index-ans80t68.js → index-3rgr16n2.js} +67 -6
- package/dist/chunks/{index-e49v2qpf.js.map → index-3rgr16n2.js.map} +1 -1
- package/dist/chunks/{index-n0acffvs.js → index-79swfbsm.js} +3 -3
- package/dist/chunks/{index-nrr8x2kq.js → index-knntt4cb.js} +2 -2
- package/dist/chunks/{index-e49v2qpf.js → index-pqpnz4ns.js} +38 -2
- package/dist/chunks/{index-ans80t68.js.map → index-pqpnz4ns.js.map} +1 -1
- package/dist/chunks/{index-t89cvabm.js → index-q4zws02e.js} +2 -2
- package/dist/chunks/{index-zw0575tv.js → index-sk8b0dh4.js} +4 -4
- package/dist/chunks/{index-zw0575tv.js.map → index-sk8b0dh4.js.map} +1 -1
- package/dist/chunks/{index-md3hz6r2.js → index-tvsykgcv.js} +3 -3
- package/dist/chunks/{index-md3hz6r2.js.map → index-tvsykgcv.js.map} +2 -2
- package/dist/chunks/{index-4ntgtkmb.js → index-v0m7r4y3.js} +90 -4
- package/dist/chunks/{index-4ntgtkmb.js.map → index-v0m7r4y3.js.map} +3 -3
- package/dist/consent.js +2 -2
- package/dist/index.js +136 -8
- package/dist/index.js.map +5 -4
- package/dist/outbox.js +1 -1
- package/dist/pdf.js +1 -1
- package/dist/providers.js +2 -2
- package/dist/runner.js +2 -2
- package/dist/tools.js +2 -2
- package/dist/types/domain-contract/src/apps.d.ts +18 -0
- package/dist/types/eengine/fixtures/providers.json +1 -1
- package/dist/types/eengine/src/capabilities.d.ts +2 -0
- package/dist/types/eengine/src/index.d.ts +1 -0
- package/dist/types/eengine/src/instance.d.ts +9 -0
- package/dist/types/eengine/src/synced.d.ts +59 -0
- package/fixtures/capabilities.json +65 -4
- package/fixtures/outbox.json +15 -1
- package/fixtures/providers.json +1 -1
- package/package.json +1 -1
- /package/dist/chunks/{index-ax4aay30.js.map → index-3nf9f7pv.js.map} +0 -0
- /package/dist/chunks/{index-n0acffvs.js.map → index-79swfbsm.js.map} +0 -0
- /package/dist/chunks/{index-nrr8x2kq.js.map → index-knntt4cb.js.map} +0 -0
- /package/dist/chunks/{index-t89cvabm.js.map → index-q4zws02e.js.map} +0 -0
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
"// EXP-1490: one member's connection to one upstream (a team MCP server an\n// app binds to). Pure and byte-locked by `fixtures/upstream.json` (its\n// `$comment` is the spec).\n\nimport fixture from \"../fixtures/upstream.json\" with { type: \"json\" }\nimport { canonicalJson, upstreamDestructive } from \"./pure\"\n\nexport const UPSTREAM_STATES = fixture.states as readonly UpstreamState[]\nexport const UPSTREAM_LABELS = fixture.labels as Record<UpstreamState, string>\nexport const UPSTREAM_ERRORS = fixture.errors\n\nexport type UpstreamState = `disconnected` | `connecting` | `connected` | `expired` | `reconnect` | `failed`\nexport type UpstreamEvent =\n | `connect`\n | `callbackOk`\n | `callbackError`\n | `flowExpired`\n | `secretSet`\n | `expire`\n | `refreshOk`\n | `refreshFailed`\n | `unauthorized`\n | `disconnect`\n | `serverChanged`\n\nconst TRANSITIONS = fixture.transitions as Record<string, Record<string, UpstreamState>>\n\n/** The next state; an event a state does not list changes nothing. */\nexport function upstreamStep(state: UpstreamState, event: UpstreamEvent): UpstreamState {\n const any = TRANSITIONS[`*`]?.[event]\n if (any) return any\n return TRANSITIONS[state]?.[event] ?? state\n}\n\n/** May the broker call through it (an expired token is refreshed first)? */\nexport function upstreamCallable(state: UpstreamState): boolean {\n return (fixture.callable as readonly string[]).includes(state)\n}\n\n/** The upstreams the consent page offers to connect: every server the\n * chosen apps declare whose member state is not callable (no state =\n * `disconnected`), sorted, each once. A server that no longer exists is\n * not offered (its bindings answer `upstreamMissing`). */\nexport function missingUpstreams(\n declared: readonly (readonly string[])[],\n states: Readonly<Record<string, UpstreamState>>,\n servers: readonly string[]\n): string[] {\n const names = new Set(declared.flat().filter((name) => servers.includes(name)))\n return [...names].filter((name) => !upstreamCallable(states[name] ?? `disconnected`)).sort()\n}\n\n// VAPP-254: an app's per-app server mapping (`mapping` in the fixture: its\n// `$comment` is the spec).\n\nexport const UPSTREAM_MAPPING = fixture.mapping\n\nexport interface UpstreamServerRef {\n id: string\n name: string\n}\n\nexport type UpstreamResolution<T> = { server: T; source: `mapped` | `name` }\n\n/** The team server filling a declared upstream: a binding on the team, else\n * the server of exactly that name, else null. */\nexport function resolveUpstreamServer<T extends UpstreamServerRef>(upstream: string, bound: string | null, servers: readonly T[]): UpstreamResolution<T> | null {\n const mapped = bound === null ? undefined : servers.find((s) => s.id === bound)\n if (mapped) return { server: mapped, source: `mapped` }\n const named = servers.find((s) => s.name === upstream)\n return named ? { server: named, source: `name` } : null\n}\n\nexport type UpstreamMappingWrite = { op: `set`; serverId: string } | { op: `clear` } | { error: string }\n\n/** What an owner's pick writes: the exact-name default keeps no row. */\nexport function upstreamMappingWrite(upstream: string, serverId: string | null, servers: readonly UpstreamServerRef[]): UpstreamMappingWrite {\n if (serverId === null) return { op: `clear` }\n const server = servers.find((s) => s.id === serverId)\n if (!server) return { error: UPSTREAM_ERRORS.missing }\n return server.name === upstream ? { op: `clear` } : { op: `set`, serverId }\n}\n\n/** The audit row's `tool` for a mapping change. */\nexport function upstreamMappingTool(upstream: string, server: string | null): string {\n return `upstream:${upstream}=${server ?? `auto`}`.slice(0, 128)\n}\n\n// VAPP-230 (eApps M1): `upstream:<server>/<tool>?<args>` read bindings\n// (`binding` in the fixture: its `$comment` is the spec).\n\nexport const UPSTREAM_BINDING = {\n scheme: fixture.binding.scheme,\n cacheSeconds: fixture.binding.cacheSeconds,\n banner: fixture.binding.banner,\n connect: fixture.binding.connect,\n errors: fixture.binding.errors,\n} as const\n\nexport interface UpstreamBinding {\n server: string\n tool: string\n args: Record<string, unknown>\n}\n\nconst SOURCE = /^([a-zA-Z][a-zA-Z0-9+.-]*):([^?]+)(?:\\?(.*))?$/\nconst JSON_NUMBER = /^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$/\n\nfunction decode(part: string): string {\n try {\n return decodeURIComponent(part.replace(/\\+/g, ` `))\n } catch {\n return part\n }\n}\n\n/** A param's text as an argument: JSON literals, numbers, objects and\n * arrays read as JSON; anything else stays the string. */\nexport function upstreamArg(text: string): unknown {\n if (text === `true`) return true\n if (text === `false`) return false\n if (text === `null`) return null\n if (JSON_NUMBER.test(text)) return Number(text)\n if (text.startsWith(`{`) || text.startsWith(`[`)) {\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n }\n return text\n}\n\n/** `upstream:crm/search?q=acme` → `{server: \"crm\", tool: \"search\", args:\n * {q: \"acme\"}}` (eUI's source grammar; the name splits at its LAST `/`),\n * null when it is not an upstream source. */\nexport function parseUpstreamSource(source: string): UpstreamBinding | null {\n const m = SOURCE.exec(source.trim())\n if (!m || m[1]!.toLowerCase() !== UPSTREAM_BINDING.scheme) return null\n const name = decode(m[2]!)\n const slash = name.lastIndexOf(`/`)\n if (slash <= 0 || slash === name.length - 1) return null\n const params: Record<string, string> = {}\n for (const pair of (m[3] ?? ``).split(`&`)) {\n if (!pair) continue\n const eq = pair.indexOf(`=`)\n const key = decode(eq < 0 ? pair : pair.slice(0, eq))\n if (!key) continue\n params[key] = eq < 0 ? `` : decode(pair.slice(eq + 1))\n }\n const args: Record<string, unknown> = {}\n for (const key of Object.keys(params).sort()) args[key] = upstreamArg(params[key]!)\n return { server: name.slice(0, slash), tool: name.slice(slash + 1), args }\n}\n\n/** Exponential's install check: every `upstream:` source of a package's\n * template bindings parses and names one of its `upstreams`. */\nexport function upstreamDeclareIssues(sources: readonly string[], upstreams: readonly string[]): { index: number; code: string }[] {\n const issues: { index: number; code: string }[] = []\n sources.forEach((source, index) => {\n if (!new RegExp(`^${UPSTREAM_BINDING.scheme}:`, `i`).test(source.trim())) return\n const parsed = parseUpstreamSource(source)\n if (!parsed) issues.push({ index, code: UPSTREAM_BINDING.errors.invalid })\n else if (!upstreams.includes(parsed.server)) issues.push({ index, code: UPSTREAM_BINDING.errors.notDeclared })\n })\n return issues\n}\n\n/** The broker's call-time check against the tool's live annotations and the\n * package's `sources` (an exact `upstream:<server>/<tool>` vouches for a\n * tool that sets no readOnlyHint): null = it reads, else the error. */\nexport function upstreamReadError(\n binding: Pick<UpstreamBinding, `server` | `tool`>,\n annotations: { readOnlyHint?: unknown; destructiveHint?: unknown } | null | undefined,\n sources: readonly string[]\n): string | null {\n if (upstreamDestructive(binding.tool, annotations as { destructiveHint?: boolean } | null | undefined)) return UPSTREAM_BINDING.errors.destructive\n if (annotations?.readOnlyHint === true) return null\n if (annotations?.readOnlyHint === false) return UPSTREAM_BINDING.errors.notReadOnly\n return sources.includes(`${UPSTREAM_BINDING.scheme}:${binding.server}/${binding.tool}`) ? null : UPSTREAM_BINDING.errors.notReadOnly\n}\n\n/** The cache key of one read: member, server id, tool and canonical args. */\nexport function upstreamCacheKey(member: string, serverId: string, tool: string, args: Record<string, unknown>): string {\n return [member, serverId, tool, canonicalJson(args)].join(`\\n`)\n}\n\n/** An MCP tools/call result as the binding's value. */\nexport function upstreamBindingValue(result: Record<string, unknown>): { value: unknown } | { error: string } {\n if (result.isError === true) return { error: UPSTREAM_ERRORS.failed }\n const structured = result.structuredContent\n if (structured && typeof structured === `object` && !Array.isArray(structured)) return { value: structured }\n const text = Array.isArray(result.content)\n ? (result.content.find((c) => c && typeof c === `object` && (c as { type?: unknown }).type === `text` && typeof (c as { text?: unknown }).text === `string`) as { text: string } | undefined)\n : undefined\n if (!text) return { value: null }\n try {\n return { value: JSON.parse(text.text) }\n } catch {\n return { value: text.text }\n }\n}\n\n/** One host answer of `apps.upstreamRead`. */\nexport type UpstreamReadAnswer = { ok: true; value: unknown } | { ok: false; error: string; server: string; offline?: string }\n\n/** The host's banners: one per server whose read answered\n * upstreamDisconnected, sorted, each once. */\nexport function upstreamBanners(answers: readonly UpstreamReadAnswer[]): string[] {\n const servers = new Set(answers.flatMap((a) => (!a.ok && a.error === UPSTREAM_ERRORS.disconnected ? [a.server] : [])))\n return [...servers].sort().map((server) => UPSTREAM_BINDING.banner.replace(`{server}`, server))\n}\n\n// VAPP-239 (eApps M2): device-routed upstreams (`device` in the fixture: its\n// `$comment` is the spec).\n\nconst device = fixture.device\n\nexport const UPSTREAM_DEVICE = {\n ttlSeconds: device.ttlSeconds,\n answerTimeoutMs: device.answerTimeoutMs,\n maxAnswerBytes: device.maxAnswerBytes,\n errors: device.errors,\n copy: device.copy,\n} as const\n\nconst fill = (template: string, vars: Record<string, string>): string => template.replace(/\\{(\\w+)\\}/g, (m, k: string) => vars[k] ?? m)\n\n/** A server row as the router reads it: its transport and the device it\n * names (with the device's owner and label), if any. */\nexport interface RoutedServer {\n name: string\n transport: string\n device: { id: string; ownerId: string; label: string } | null\n}\n\nexport type UpstreamRoute = { route: `server` } | { route: `device`; deviceId: string } | { error: string; message: string }\n\n/** Where one call to `server` by `callerId` (the member, or an automation's\n * sponsor) runs: the server, the owner's device, or a refusal. */\nexport function upstreamRoute(server: RoutedServer, callerId: string): UpstreamRoute {\n if (!server.device) {\n if (server.transport === `http`) return { route: `server` }\n return { error: device.errors.noDevice, message: fill(device.copy.noDevice, { server: server.name }) }\n }\n if (server.device.ownerId !== callerId) {\n return { error: device.errors.notOwner, message: fill(device.copy.notOwner, { server: server.name, device: server.device.label }) }\n }\n return { route: `device`, deviceId: server.device.id }\n}\n\n/** What the relay answered `POST /devices/:userId/:deviceId/upstream` with. */\nexport type DeviceRelayOutcome = { delivered: true } | { delivered: false; reason: string }\n\n/** The device's (opened) answer: the tool's result, or why it did not run. */\nexport type DeviceAnswer = { ok: true; result: Record<string, unknown> } | { ok: false; reason: `vetoed` | `refused` | `failed`; error: string }\n\nexport type DeviceCallOutcome = { result: Record<string, unknown> } | { error: string; message: string; offline?: string }\n\n/** One device call's outcome for the broker. `answer` = null when the relay\n * delivered nothing that opens. */\nexport function deviceCallOutcome(server: string, deviceLabel: string, relay: DeviceRelayOutcome, answer: DeviceAnswer | null): DeviceCallOutcome {\n const vars = { server, device: deviceLabel }\n if (!relay.delivered && relay.reason === `device_offline`) {\n return { error: device.errors.offline, message: fill(device.copy.offline, vars), offline: deviceLabel }\n }\n if (!relay.delivered || !answer) return { error: device.errors.noAnswer, message: fill(device.copy.noAnswer, vars) }\n if (answer.ok) return { result: answer.result }\n if (answer.reason === `vetoed`) {\n const destructive = answer.error === UPSTREAM_BINDING.errors.destructive\n const known = destructive || answer.error === `accessTooLow` || answer.error === UPSTREAM_BINDING.errors.notReadOnly\n return {\n error: known ? answer.error : UPSTREAM_BINDING.errors.notReadOnly,\n message: `${server}'s tool is ${destructive ? `destructive` : `not read-only`}: it does not run here`,\n }\n }\n if (answer.reason === `refused`) return { error: UPSTREAM_ERRORS.disconnected, message: `${server} refused your connection; reconnect` }\n return { error: UPSTREAM_ERRORS.failed, message: `${server}: ${answer.error}` }\n}\n\n/** Whether a device may claim a call: the member's own, on that device,\n * unexpired (`expiresAt` ms) and not claimed yet. */\nexport function deviceCallClaimable(\n call: { userId: string; deviceId: string; expiresAt: number; claimed: boolean },\n claim: { userId: string; deviceId: string },\n now: number\n): boolean {\n return !call.claimed && call.expiresAt > now && call.userId === claim.userId && call.deviceId === claim.deviceId\n}\n\n/** One host banner: its text and whether it carries the Connect action. */\nexport interface UpstreamHostBanner {\n server: string\n text: string\n connect: boolean\n}\n\n/** The host's banners ×4: `Connect <server>` for an upstreamDisconnected\n * read, the offline copy for a device that is offline; one per server,\n * sorted by server. */\nexport function upstreamHostBanners(answers: readonly UpstreamReadAnswer[]): UpstreamHostBanner[] {\n const byServer = new Map<string, UpstreamHostBanner>()\n for (const a of answers) {\n if (a.ok || byServer.has(a.server)) continue\n if (a.error === UPSTREAM_ERRORS.disconnected) byServer.set(a.server, { server: a.server, text: UPSTREAM_BINDING.banner.replace(`{server}`, a.server), connect: true })\n else if (a.error === device.errors.offline && a.offline) byServer.set(a.server, { server: a.server, text: fill(device.copy.offline, { server: a.server, device: a.offline }), connect: false })\n }\n return [...byServer.values()].sort((x, y) => (x.server < y.server ? -1 : x.server > y.server ? 1 : 0))\n}\n\nconst LAN_SUFFIXES = [`.local`, `.lan`, `.home.arpa`, `.internal`]\n\nfunction privateIpv4(host: string): boolean {\n const m = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(host)\n if (!m) return false\n const [a, b] = [Number(m[1]), Number(m[2])]\n if (m.slice(1).some((p) => Number(p) > 255)) return false\n return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168)\n}\n\n/** A server row's URL rule: https anywhere; http on loopback or a LAN host\n * (a device-routed server's reach). */\nexport function mcpUrlAllowed(value: string): boolean {\n let url: URL\n try {\n url = new URL(value.trim())\n } catch {\n return false\n }\n if (url.protocol === `https:`) return true\n if (url.protocol !== `http:`) return false\n const host = url.hostname.toLowerCase()\n return host === `localhost` || host === `127.0.0.1` || privateIpv4(host) || LAN_SUFFIXES.some((suffix) => host.endsWith(suffix))\n}\n",
|
|
12
12
|
"// EXP-1507: eMCP's LOCAL HUB, several MCP servers behind one loopback\n// endpoint with a grant per run and an `mcp_calls` row per call. Pure and\n// locked by `fixtures/hub.json` (its `$comment` is the spec); the `emcp`\n// crate's `hub` replays the same cases.\n\nimport fixture from \"../fixtures/hub.json\" with { type: \"json\" }\nimport { type Level, toolVisible } from \"./access\"\nimport { argsHash, type CallStatus } from \"./audit\"\nimport { listToolNames, splitToolName, type UpstreamAnnotations, upstreamAccess } from \"./exposes\"\n\nexport type HubOutcome = keyof typeof fixture.outcomes\n\n/** What a run may reach: a level per mounted server (not named = none). */\nexport interface HubGrant {\n servers: Record<string, Exclude<Level, `none`>>\n teamId: string | null\n userId: string | null\n}\n\nexport interface HubTool {\n name: string\n annotations?: UpstreamAnnotations | null\n}\n\nexport interface HubServer {\n name: string\n tools: readonly HubTool[]\n}\n\nexport interface HubRoute {\n server: string\n tool: string\n}\n\nconst SERVER_NAME = new RegExp(fixture.serverNamePattern)\n\n/** May a server be mounted under `name`? */\nexport function hubServerNameValid(name: string): boolean {\n return name.length <= fixture.serverNameMax && SERVER_NAME.test(name)\n}\n\n/** The run's tools/list: every tool its grant lets it see, qualified. */\nexport function hubToolList(grant: HubGrant, servers: readonly HubServer[]): { app: string; tool: string; name: string }[] {\n const visible = servers.map((server) => {\n const level: Level = grant.servers[server.name] ?? `none`\n return {\n slug: server.name,\n exposes: server.tools.filter(\n (tool) => !tool.name.includes(`__`) && toolVisible(level, upstreamAccess(tool.annotations)),\n ),\n }\n })\n return listToolNames(visible).filter((listed) => splitToolName(listed.name) !== null)\n}\n\n/** Where a call goes: a name on the run's own list, else null (unknown). */\nexport function hubRoute(grant: HubGrant, servers: readonly HubServer[], name: string): HubRoute | null {\n const listed = hubToolList(grant, servers).find((tool) => tool.name === name)\n return listed ? { server: listed.app, tool: listed.tool } : null\n}\n\n/** The row's status and error code for an outcome. */\nexport function hubOutcomeStatus(outcome: HubOutcome): { status: CallStatus; error: string | null } {\n return { status: fixture.outcomes[outcome] as CallStatus, error: fixture.errors[outcome] }\n}\n\nexport interface HubAuditRow {\n id: string\n teamId: string | null\n userId: string | null\n clientId: string\n appId: null\n tool: string\n status: CallStatus\n error: string | null\n latencyMs: number\n argsHash: string\n createdAt: string\n}\n\n/** The `mcp_calls` row of one hub call (`outcome` = `unknown` when it was\n * not routed). */\nexport function hubAuditRow(input: {\n id: string\n grant: HubGrant\n run: string\n name: string\n args: unknown\n outcome: HubOutcome\n latencyMs: number\n createdAtMs: number\n}): HubAuditRow {\n const { status, error } = hubOutcomeStatus(input.outcome)\n return {\n id: input.id,\n teamId: input.grant.teamId,\n userId: input.grant.userId,\n clientId: input.run,\n appId: null,\n tool: input.name,\n status,\n error,\n latencyMs: Math.max(0, Math.floor(input.latencyMs)),\n argsHash: argsHash(input.args),\n createdAt: new Date(input.createdAtMs).toISOString(),\n }\n}\n",
|
|
13
13
|
"// #VAPP-220 (eApps A1): an app's AUTOMATIONS: one trigger (a schedule, a\n// record write, a team event) runs one `exposes`-style binding in the server\n// broker, on the published package, as the automation's sponsor. Pure and\n// locked by `fixtures/automations.json` (its `$comment` is the spec); the\n// server (`apps/web/src/lib/apps/automations.ts`) and the Rust crate `emcp`\n// (`contract/automations.rs`) replay the same cases.\n\nimport fixture from \"../fixtures/automations.json\" with { type: \"json\" }\nimport { agentAuditTool, resolveArgs, runVia, validateExposes, type AgentRun, type Expose, type ExposeRun } from \"./exposes\"\n\nexport const AUTOMATIONS_LIMITS = fixture.limits\nexport const AUTOMATION_ERRORS: readonly string[] = fixture.errors\n\nexport type AutomationTrigger =\n | { schedule: { cron: string; tz?: string } }\n | { record: { kind: string; op: `create` | `update` | `delete` } }\n | { event: string }\n | { webhook: { name: string } }\n\nexport interface Automation {\n name: string\n on: AutomationTrigger\n run: ExposeRun\n /** A member's email; default = the publisher (`resolveSponsor`). */\n sponsor?: string\n}\n\nexport type TriggerKind = `schedule` | `record` | `event` | `webhook`\n\nexport interface AutomationError {\n index: number | null\n code: string\n}\n\nexport interface AutomationContext {\n kinds: readonly string[]\n upstreams: readonly string[]\n}\n\nconst isObject = (v: unknown): v is Record<string, unknown> => !!v && typeof v === `object` && !Array.isArray(v)\nconst NAME = new RegExp(fixture.limits.namePattern)\nconst EVENT = new RegExp(fixture.limits.eventPattern)\nconst CRON_FIELD = new RegExp(fixture.cronFieldPattern)\nconst TRIGGER_KINDS: readonly string[] = fixture.triggerKinds\nconst RECORD_OPS: readonly string[] = fixture.recordTriggerOps\nconst ROOTS = fixture.templateRoots as Record<TriggerKind, string[]>\n\n/** The kind of an automation's `on` (exactly one key), else null. */\nexport function triggerKind(on: unknown): TriggerKind | null {\n if (!isObject(on)) return null\n const keys = TRIGGER_KINDS.filter((k) => k in on)\n return keys.length === 1 ? (keys[0] as TriggerKind) : null\n}\n\n/** One cron field's values (`*`, lists, ranges, steps; digits only), or null. */\nexport function cronFieldValues(raw: string, min: number, max: number): number[] | null {\n const out = new Set<number>()\n for (const part of raw.split(`,`)) {\n const [range, step, ...extra] = part.split(`/`)\n if (extra.length > 0 || range === undefined || range === ``) return null\n let by = 1\n if (step !== undefined) {\n if (!/^\\d+$/.test(step) || Number(step) < 1) return null\n by = Number(step)\n }\n let lo: number\n let hi: number\n if (range === `*`) {\n lo = min\n hi = max\n } else {\n const bounds = range.split(`-`)\n if (bounds.length > 2 || !bounds.every((b) => /^\\d+$/.test(b))) return null\n lo = Number(bounds[0])\n hi = bounds.length === 2 ? Number(bounds[1]) : step !== undefined ? max : lo\n }\n if (lo < min || hi > max || lo > hi) return null\n for (let v = lo; v <= hi; v += by) out.add(v)\n }\n return [...out].sort((a, b) => a - b)\n}\n\n/** A schedule's check: null = fine, else `schedule` | `scheduleTooOften`.\n * The gap = the smallest distance between the minutes of a day it fires\n * on (minute × hour, circular over the day). */\nexport function scheduleError(schedule: unknown): `schedule` | `scheduleTooOften` | null {\n if (!isObject(schedule) || typeof schedule.cron !== `string`) return `schedule`\n if (schedule.tz !== undefined && (typeof schedule.tz !== `string` || schedule.tz.trim() === ``)) return `schedule`\n const fields = schedule.cron.trim().split(/\\s+/)\n if (fields.length !== 5 || !fields.every((f) => CRON_FIELD.test(f))) return `schedule`\n const minutes = cronFieldValues(fields[0]!, 0, 59)\n const hours = cronFieldValues(fields[1]!, 0, 23)\n if (!minutes || !hours) return `schedule`\n const day = hours.flatMap((h) => minutes.map((m) => h * 60 + m))\n if (day.length < 2) return null\n let gap = 1440 - day[day.length - 1]! + day[0]!\n for (let i = 1; i < day.length; i++) gap = Math.min(gap, day[i]! - day[i - 1]!)\n return gap < fixture.limits.minScheduleGapMinutes ? `scheduleTooOften` : null\n}\n\nconst PLACEHOLDER = /\\$\\{([^}]*)\\}/g\nconst ROOT_PATH = /^([A-Za-z_][A-Za-z0-9_]*)((?:\\.[A-Za-z_][A-Za-z0-9_]*)*)$/\n\n/** Every `${…}` of a value is a root the trigger carries (`templateRoots`):\n * `record.<path>`, `event.<path>`, `firedAt`. */\nexport function templatesValid(value: unknown, kind: TriggerKind): boolean {\n const roots = ROOTS[kind]\n const ok = (text: string) =>\n [...text.matchAll(PLACEHOLDER)].every((m) => {\n const parts = m[1]!.match(ROOT_PATH)\n if (!parts || !roots.includes(parts[1]!)) return false\n return parts[1] === `firedAt` ? parts[2] === `` : parts[2] !== ``\n })\n const walk = (v: unknown): boolean =>\n typeof v === `string` ? ok(v) : Array.isArray(v) ? v.every(walk) : isObject(v) ? Object.values(v).every(walk) : true\n return walk(value)\n}\n\n/** Rewrites `${record.…}`, `${event.…}`, `${firedAt}` to exposes'\n * `${input.<root>…}`, so `resolveArgs` fills them. */\nexport function toInputTemplates<T>(value: T): T {\n const walk = (v: unknown): unknown => {\n if (typeof v === `string`) return v.replace(/\\$\\{(record|event|firedAt)(?=[.}])/g, (_, root: string) => `\\${input.${root}`)\n if (Array.isArray(v)) return v.map(walk)\n if (isObject(v)) return Object.fromEntries(Object.entries(v).map(([k, item]) => [k, walk(item)]))\n return v\n }\n return walk(value) as T\n}\n\n/** The run with its templates filled from the trigger's input\n * `{record?, event?, firedAt}`. */\nexport function resolveAutomationRun(run: ExposeRun, input: Record<string, unknown>): ExposeRun {\n return resolveArgs(toInputTemplates(run), input) as ExposeRun\n}\n\n/** The run as an exposes binding (the broker runs it like a tool). */\nexport function automationExpose(automation: Pick<Automation, `name` | `run`>): Expose {\n return {\n name: automation.name,\n title: automation.name,\n description: automation.name,\n input: { type: `object`, properties: { record: {}, event: {}, firedAt: {} } },\n access: `write`,\n run: toInputTemplates(automation.run),\n }\n}\n\n/** Every error of an app's `automations`, in order (empty = valid). */\nexport function validateAutomations(list: unknown, ctx: AutomationContext): AutomationError[] {\n if (!Array.isArray(list)) return [{ index: null, code: `list` }]\n const errors: AutomationError[] = []\n if (list.length > fixture.limits.maxAutomations) errors.push({ index: null, code: `tooMany` })\n const seen = new Set<string>()\n const hooks = new Set<string>()\n list.forEach((item, index) => {\n const fail = (code: string) => void errors.push({ index, code })\n if (!isObject(item)) return fail(`shape`)\n const name = item.name\n if (typeof name !== `string` || !NAME.test(name) || name.length > fixture.limits.nameMax) fail(`name`)\n else if (seen.has(name)) fail(`duplicate`)\n else seen.add(name)\n const kind = triggerKind(item.on)\n if (!kind) return fail(`on`)\n const on = item.on as Record<string, unknown>\n if (kind === `schedule`) {\n const error = scheduleError(on.schedule)\n if (error) fail(error)\n } else if (kind === `record`) {\n const record = on.record\n if (!isObject(record) || typeof record.op !== `string` || !RECORD_OPS.includes(record.op)) return fail(`on`)\n if (typeof record.kind !== `string` || !ctx.kinds.includes(record.kind)) fail(`kind`)\n } else if (kind === `webhook`) {\n // #VAPP-237 (A3): a hook name, unique among the app's webhooks.\n const hook = isObject(on.webhook) ? on.webhook.name : undefined\n if (typeof hook !== `string` || !NAME.test(hook) || hook.length > fixture.limits.nameMax || hooks.has(hook)) fail(`webhook`)\n else hooks.add(hook)\n } else if (typeof on.event !== `string` || !EVENT.test(on.event) || on.event.length > fixture.limits.eventMax) fail(`event`)\n if (item.sponsor !== undefined && (typeof item.sponsor !== `string` || !item.sponsor.includes(`@`))) fail(`sponsor`)\n const run = item.run\n if (!isObject(run)) return fail(`run`)\n if (!templatesValid(run, kind)) fail(`template`)\n if (typeof run.action === `string` && run.action.startsWith(`exponential_apps_`)) return fail(`chain`)\n for (const error of validateExposes([automationExpose({ name: `automation`, run: run as ExposeRun })], ctx)) {\n if (error.code !== `template` && error.code !== `accessTooLow`) fail(error.code)\n }\n })\n return errors\n}\n\n/** The trigger a record write, an event or a webhook delivery is, for\n * `matchAutomations`; `byAgent` = the slug of the agent whose session\n * tree caused it. */\nexport type FiredBy = ({ record: { kind: string; op: string } } | { event: string } | { webhook: string }) & { byAgent?: string | null }\n\n/** The automations a record write or event fires, in package order. LOOP\n * GUARD: a write or event caused by an automation run fires nothing; one\n * caused from an agent's tree (`byAgent`) fires no hand-off to that agent. */\nexport function matchAutomations(list: unknown, firedBy: FiredBy, fromAutomation: boolean): string[] {\n if (fromAutomation || !Array.isArray(list)) return []\n const out: string[] = []\n for (const item of list) {\n if (!isObject(item) || typeof item.name !== `string`) continue\n if (firedBy.byAgent && isObject(item.run) && item.run.agent === firedBy.byAgent) continue\n const kind = triggerKind(item.on)\n const on = item.on as Record<string, unknown>\n if (kind === `record` && `record` in firedBy && isObject(on.record) && on.record.kind === firedBy.record.kind && on.record.op === firedBy.record.op) out.push(item.name)\n else if (kind === `event` && `event` in firedBy && on.event === firedBy.event) out.push(item.name)\n else if (kind === `webhook` && `webhook` in firedBy && isObject(on.webhook) && on.webhook.name === firedBy.webhook) out.push(item.name)\n }\n return out\n}\n\nexport interface SponsorMember {\n id: string\n email: string\n role: string\n /** The member's app access (`none` | `read` | `write`). */\n level: string\n}\n\nexport type SponsorState = (typeof fixture.sponsorStates)[number]\n\nexport type SponsorResult = { userId: string } | { paused: SponsorState }\n\n/** C23: who an automation acts as, or why it is paused. */\nexport function resolveSponsor(input: { declared: string | null | undefined; publishedBy: string | null | undefined; members: readonly SponsorMember[] }): SponsorResult {\n const publisher = input.publishedBy ? input.members.find((m) => m.id === input.publishedBy) : undefined\n let sponsor: SponsorMember | undefined = publisher\n const declared = input.declared?.trim().toLowerCase()\n if (declared && declared !== publisher?.email.toLowerCase()) {\n if (!publisher) return { paused: `sponsorGone` }\n if (publisher.role !== `owner`) return { paused: `sponsorNotAllowed` }\n sponsor = input.members.find((m) => m.email.toLowerCase() === declared)\n }\n if (!sponsor) return { paused: `sponsorGone` }\n if (sponsor.level !== `write`) return { paused: `sponsorReadOnly` }\n return { userId: sponsor.id }\n}\n\n/** Whether another run may start (the app's runs of the last 60 s). */\nexport function rateAllows(runsLastMinute: number): boolean {\n return runsLastMinute < fixture.limits.runsPerMinute\n}\n\n/** The audit row's `client_id`. */\nexport function automationClientId(name: string): string {\n return `${fixture.limits.clientPrefix}${name}`\n}\n\n/** The audit row's `tool`. */\nexport function automationTool(run: ExposeRun): string {\n const via = runVia(run)\n if (via === `records`) {\n const records = (run as { records: { op: string; kind: string } }).records\n return `records.${records.op}:${records.kind}`\n }\n if (via === `action`) return (run as { action: string }).action\n if (via === `agent`) return agentAuditTool((run as AgentRun).agent)\n const upstream = run as { upstream: string; tool: string }\n return `${upstream.upstream}/${upstream.tool}`\n}\n",
|
|
14
|
-
"// VAPP-260: the Engine's capability catalog and its ONE broker (app UI and\n// agents alike). Pure and locked by `fixtures/capabilities.json` (its\n// `$comment` is the spec); the `eengine` crate replays the same cases.\n\nimport { argsHash, hubOutcomeStatus, hubServerNameValid, type CallStatus, type HubOutcome } from \"@exp/emcp\"\n\nimport fixture from \"../fixtures/capabilities.json\" with { type: \"json\" }\n\nexport type ConsentKind = `ask` | `picker`\nexport type GrantValue = `granted` | `denied`\nexport type Grants = Record<string, GrantValue>\nexport type CallFrom = `ui` | `agent`\n\nexport interface CapabilityScope {\n id: string\n verb: string\n consent: ConsentKind\n tools: readonly string[]\n hostOnly?: boolean\n origins?: boolean\n}\n\nexport interface CapabilityDef {\n name: string\n label: string\n scopes: readonly CapabilityScope[]\n}\n\n/** One entry of a package's capability manifest (format 5 `capabilities`). */\nexport interface ManifestEntry {\n id: string\n why: string\n origins?: readonly string[]\n}\n\nexport const CAPABILITIES = fixture.capabilities as readonly CapabilityDef[]\nexport const RESERVED_CAPABILITY_NAMES: readonly string[] = fixture.reservedNames\nconst SCOPE = new RegExp(fixture.scopePattern)\n\n/** Is `id` a scope id of the right form (`<capability>.<scope>`)? */\nexport const scopeIdValid = (id: string): boolean => SCOPE.test(id)\n\n/** May a capability be mounted under `name`? A hub server name that is not reserved. */\nexport const capabilityNameValid = (name: string): boolean => hubServerNameValid(name) && !RESERVED_CAPABILITY_NAMES.includes(name)\n\nexport const capabilityDef = (name: string): CapabilityDef | null => CAPABILITIES.find((c) => c.name === name) ?? null\n\n/** The catalog scope with `id`, with its capability. */\nexport function scopeDef(id: string): { capability: CapabilityDef; scope: CapabilityScope } | null {\n for (const capability of CAPABILITIES) {\n const scope = capability.scopes.find((s) => s.id === id)\n if (scope) return { capability, scope }\n }\n return null\n}\n\nconst NAME_PART = /^[a-z][a-z0-9-]*$/\n\n/** A call's name → `{capability, tool}`: `<capability>__<tool>` from an agent,\n * `<capability>_<tool>` from app UI (round 9 §6.3: split at the first `_`); null for any other form or a reserved name. */\nexport function parseCapabilityCall(from: CallFrom, name: string): { capability: string; tool: string } | null {\n const sep = from === `agent` ? `__` : `_`\n const at = name.indexOf(sep)\n if (at < 0) return null\n const capability = name.slice(0, at)\n const tool = name.slice(at + sep.length)\n if (!NAME_PART.test(capability) || !NAME_PART.test(tool)) return null\n if (RESERVED_CAPABILITY_NAMES.includes(capability)) return null\n return { capability, tool }\n}\n\nexport type BrokerVerdictKind = `call` | `ask` | `denied` | `unknown` | `unavailable`\n\nexport interface BrokerVerdict {\n verdict: BrokerVerdictKind\n capability: string | null\n tool: string | null\n scope: string | null\n reason: string | null\n}\n\nexport interface BrokerCall {\n from: CallFrom\n name: string\n arguments?: unknown\n}\n\nexport interface BrokerInput {\n manifest: readonly ManifestEntry[]\n grants: Grants\n /** The capability names this host mounts. */\n provided: readonly string[]\n foreground: boolean\n call: BrokerCall\n}\n\nconst UNKNOWN: BrokerVerdict = { verdict: `unknown`, capability: null, tool: null, scope: null, reason: `unknownTool` }\n\n/** An `https://host[:port]` origin of a URL, or null. */\nexport function urlOrigin(url: unknown): string | null {\n if (typeof url !== `string`) return null\n try {\n const parsed = new URL(url)\n return parsed.origin === `null` ? null : parsed.origin\n } catch {\n return null\n }\n}\n\n/** The broker's verdict for one call (capabilities.json `decideCases`). */\nexport function brokerDecide(input: BrokerInput): BrokerVerdict {\n const parsed = parseCapabilityCall(input.call.from, input.call.name)\n if (!parsed) return UNKNOWN\n const capability = capabilityDef(parsed.capability)\n const scope = capability?.scopes.find((s) => s.tools.includes(parsed.tool))\n if (!capability || !scope) return UNKNOWN\n const at = (verdict: BrokerVerdictKind, reason: string | null): BrokerVerdict => ({ verdict, capability: capability.name, tool: parsed.tool, scope: scope.id, reason })\n if (scope.hostOnly) return at(`denied`, `hostOnly`)\n const entry = input.manifest.find((m) => m.id === scope.id)\n if (!entry) return at(`denied`, `notDeclared`)\n if (!input.provided.includes(capability.name)) return at(`unavailable`, `unavailable`)\n if (scope.origins) {\n const args = input.call.arguments\n const origin = urlOrigin(args && typeof args === `object` ? (args as Record<string, unknown>).url : undefined)\n if (!origin || !(entry.origins ?? []).includes(origin)) return at(`denied`, `originNotDeclared`)\n }\n if (scope.consent === `picker`) return input.foreground ? at(`call`, null) : at(`denied`, `needsForeground`)\n const grant = input.grants[scope.id]\n if (grant === `granted`) return at(`call`, null)\n if (grant === `denied`) return at(`denied`, `consentDenied`)\n return input.foreground ? at(`ask`, null) : at(`denied`, `consentNeeded`)\n}\n\n/** An agent's tools/list: every tool of a declared, provided, non-host-only scope. */\nexport function agentToolList(manifest: readonly ManifestEntry[], provided: readonly string[]): string[] {\n const names: string[] = []\n for (const capability of CAPABILITIES) {\n if (!provided.includes(capability.name)) continue\n for (const scope of capability.scopes) {\n if (scope.hostOnly || !manifest.some((m) => m.id === scope.id)) continue\n for (const tool of scope.tools) names.push(`${capability.name}__${tool}`)\n }\n }\n return names.sort()\n}\n\nexport interface BrokerAuditRow {\n appId: string\n clientId: string\n tool: string\n status: CallStatus\n error: string | null\n argsHash: string\n}\n\n/** The audit row of one brokered call. `client` = the agent run's id (null for UI);\n * `outcome` = the call's outcome when it ran (`verdict: call`), else ignored. */\nexport function brokerAuditRow(input: { appId: string; client: string | null; call: BrokerCall; verdict: BrokerVerdict; outcome: HubOutcome | null }): BrokerAuditRow {\n const { appId, client, call, verdict } = input\n const tool = verdict.capability && verdict.tool ? `${verdict.capability}__${verdict.tool}` : call.from === `ui` ? call.name.replace(`_`, `__`) : call.name\n const base = { appId, clientId: call.from === `ui` ? `ui` : (client ?? ``), tool, argsHash: argsHash(call.arguments ?? {}) }\n if (verdict.verdict === `call`) {\n const { status, error } = hubOutcomeStatus(input.outcome ?? `ok`)\n return { ...base, status, error }\n }\n return { ...base, status: `denied`, error: verdict.reason }\n}\n"
|
|
14
|
+
"// VAPP-260: the Engine's capability catalog and its ONE broker (app UI and\n// agents alike). Pure and locked by `fixtures/capabilities.json` (its\n// `$comment` is the spec); the `eengine` crate replays the same cases.\n\nimport { argsHash, hubOutcomeStatus, hubServerNameValid, type CallStatus, type HubOutcome } from \"@exp/emcp\"\n\nimport fixture from \"../fixtures/capabilities.json\" with { type: \"json\" }\n\nexport type ConsentKind = `ask` | `picker`\nexport type GrantValue = `granted` | `denied`\nexport type Grants = Record<string, GrantValue>\nexport type CallFrom = `ui` | `agent`\n\nexport interface CapabilityScope {\n id: string\n verb: string\n consent: ConsentKind\n tools: readonly string[]\n hostOnly?: boolean\n origins?: boolean\n}\n\nexport interface CapabilityDef {\n name: string\n label: string\n scopes: readonly CapabilityScope[]\n /** #VAPP-273: a capability one host alone provides (`ede`); every other host answers `unavailable`. */\n host?: string\n}\n\n/** One entry of a package's capability manifest (format 5 `capabilities`). */\nexport interface ManifestEntry {\n id: string\n why: string\n origins?: readonly string[]\n}\n\nexport const CAPABILITIES = fixture.capabilities as readonly CapabilityDef[]\nexport const RESERVED_CAPABILITY_NAMES: readonly string[] = fixture.reservedNames\nconst SCOPE = new RegExp(fixture.scopePattern)\n\n/** Is `id` a scope id of the right form (`<capability>.<scope>`)? */\nexport const scopeIdValid = (id: string): boolean => SCOPE.test(id)\n\n/** May a capability be mounted under `name`? A hub server name that is not reserved. */\nexport const capabilityNameValid = (name: string): boolean => hubServerNameValid(name) && !RESERVED_CAPABILITY_NAMES.includes(name)\n\nexport const capabilityDef = (name: string): CapabilityDef | null => CAPABILITIES.find((c) => c.name === name) ?? null\n\n/** The catalog scope with `id`, with its capability. */\nexport function scopeDef(id: string): { capability: CapabilityDef; scope: CapabilityScope } | null {\n for (const capability of CAPABILITIES) {\n const scope = capability.scopes.find((s) => s.id === id)\n if (scope) return { capability, scope }\n }\n return null\n}\n\nconst NAME_PART = /^[a-z][a-z0-9-]*$/\n\n/** A call's name → `{capability, tool}`: `<capability>__<tool>` from an agent,\n * `<capability>_<tool>` from app UI (round 9 §6.3: split at the first `_`); null for any other form or a reserved name. */\nexport function parseCapabilityCall(from: CallFrom, name: string): { capability: string; tool: string } | null {\n const sep = from === `agent` ? `__` : `_`\n const at = name.indexOf(sep)\n if (at < 0) return null\n const capability = name.slice(0, at)\n const tool = name.slice(at + sep.length)\n if (!NAME_PART.test(capability) || !NAME_PART.test(tool)) return null\n if (RESERVED_CAPABILITY_NAMES.includes(capability)) return null\n return { capability, tool }\n}\n\nexport type BrokerVerdictKind = `call` | `ask` | `denied` | `unknown` | `unavailable`\n\nexport interface BrokerVerdict {\n verdict: BrokerVerdictKind\n capability: string | null\n tool: string | null\n scope: string | null\n reason: string | null\n}\n\nexport interface BrokerCall {\n from: CallFrom\n name: string\n arguments?: unknown\n}\n\nexport interface BrokerInput {\n manifest: readonly ManifestEntry[]\n grants: Grants\n /** The capability names this host mounts. */\n provided: readonly string[]\n foreground: boolean\n call: BrokerCall\n}\n\nconst UNKNOWN: BrokerVerdict = { verdict: `unknown`, capability: null, tool: null, scope: null, reason: `unknownTool` }\n\n/** An `https://host[:port]` origin of a URL, or null. */\nexport function urlOrigin(url: unknown): string | null {\n if (typeof url !== `string`) return null\n try {\n const parsed = new URL(url)\n return parsed.origin === `null` ? null : parsed.origin\n } catch {\n return null\n }\n}\n\n/** The broker's verdict for one call (capabilities.json `decideCases`). */\nexport function brokerDecide(input: BrokerInput): BrokerVerdict {\n const parsed = parseCapabilityCall(input.call.from, input.call.name)\n if (!parsed) return UNKNOWN\n const capability = capabilityDef(parsed.capability)\n const scope = capability?.scopes.find((s) => s.tools.includes(parsed.tool))\n if (!capability || !scope) return UNKNOWN\n const at = (verdict: BrokerVerdictKind, reason: string | null): BrokerVerdict => ({ verdict, capability: capability.name, tool: parsed.tool, scope: scope.id, reason })\n if (scope.hostOnly) return at(`denied`, `hostOnly`)\n const entry = input.manifest.find((m) => m.id === scope.id)\n if (!entry) return at(`denied`, `notDeclared`)\n if (!input.provided.includes(capability.name)) return at(`unavailable`, `unavailable`)\n if (scope.origins) {\n const args = input.call.arguments\n const origin = urlOrigin(args && typeof args === `object` ? (args as Record<string, unknown>).url : undefined)\n if (!origin || !(entry.origins ?? []).includes(origin)) return at(`denied`, `originNotDeclared`)\n }\n if (scope.consent === `picker`) return input.foreground ? at(`call`, null) : at(`denied`, `needsForeground`)\n const grant = input.grants[scope.id]\n if (grant === `granted`) return at(`call`, null)\n if (grant === `denied`) return at(`denied`, `consentDenied`)\n return input.foreground ? at(`ask`, null) : at(`denied`, `consentNeeded`)\n}\n\n/** An agent's tools/list: every tool of a declared, provided, non-host-only scope. */\nexport function agentToolList(manifest: readonly ManifestEntry[], provided: readonly string[]): string[] {\n const names: string[] = []\n for (const capability of CAPABILITIES) {\n if (!provided.includes(capability.name)) continue\n for (const scope of capability.scopes) {\n if (scope.hostOnly || !manifest.some((m) => m.id === scope.id)) continue\n for (const tool of scope.tools) names.push(`${capability.name}__${tool}`)\n }\n }\n return names.sort()\n}\n\nexport interface BrokerAuditRow {\n appId: string\n clientId: string\n tool: string\n status: CallStatus\n error: string | null\n argsHash: string\n}\n\n/** The audit row of one brokered call. `client` = the agent run's id (null for UI);\n * `outcome` = the call's outcome when it ran (`verdict: call`), else ignored. */\nexport function brokerAuditRow(input: { appId: string; client: string | null; call: BrokerCall; verdict: BrokerVerdict; outcome: HubOutcome | null }): BrokerAuditRow {\n const { appId, client, call, verdict } = input\n const tool = verdict.capability && verdict.tool ? `${verdict.capability}__${verdict.tool}` : call.from === `ui` ? call.name.replace(`_`, `__`) : call.name\n const base = { appId, clientId: call.from === `ui` ? `ui` : (client ?? ``), tool, argsHash: argsHash(call.arguments ?? {}) }\n if (verdict.verdict === `call`) {\n const { status, error } = hubOutcomeStatus(input.outcome ?? `ok`)\n return { ...base, status, error }\n }\n return { ...base, status: `denied`, error: verdict.reason }\n}\n"
|
|
15
15
|
],
|
|
16
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AASO,IAAM,oBAAoB,oBAAQ;AA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
|
|
16
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AASO,IAAM,oBAAoB,oBAAQ;AA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",
|
|
17
17
|
"debugId": "7E5EC23E0DE4BB9C64756E2164756E21",
|
|
18
18
|
"names": []
|
|
19
19
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
contract_default
|
|
3
|
-
} from "./index-
|
|
3
|
+
} from "./index-pqpnz4ns.js";
|
|
4
4
|
// packages/domain-contract/fixtures/app-share.json
|
|
5
5
|
var app_share_default = {
|
|
6
6
|
$comment: "VAPP-241 (eApps P1, docs/spec/apps.md §9.4, §18 item 5) PUBLIC LINKS ×4. A package declares `share: [{name, mode, kind, fields?, query?, template?, expires?}]` (validated by eUI: packages/exponential-ui/docs/package-format.md \"Public links\"). An owner turns one on in the app's Settings tab: a row of `app_share_links` with a random token (`tokenBytes` bytes, unpadded base64url), the URL `urlPrefix`<token> on the server's base url. STATES (`linkState`, `stateCases`): no row or a revoked one = `off`; expired (`expires_at` <= now) = `expired`; the package no longer declares the share = `orphaned` (a guest gets `copy.gone`, the Settings row stays until revoked); else `on`. A guest of any state but `on` gets the one page `copy.gone`, the same answer as an unknown token, so a token's history never leaks. ENABLE = a new row (`enabled_by` = the owner, `expires_at` = now + the share's `expires` days, none = never). ROTATE = a new token on a row that is not revoked (`rotated_at`; a revoked one = not found): the old URL stops at once, `expires_at` and the sponsor stay. REVOKE = `revoked_at`; enabling again mints a NEW token. EXPIRE (VAPP-247) = `expires_at` = now + `days`, no `days` (or null) = never, on a link that is on: the token and the sponsor stay; a revoked or expired one = not found (enabling mints a new token with the package's expiry). It lasts until the next enable. Owners only (tRPC `appAccess.setShareLink {appId, name, action: enable | rotate | revoke | expire, days?}`); the rows of the PUBLISHED package's shares ride on `appAccess.appTools` `shares` (`url` only for owners), no new synced shape. SUBMIT: a guest fills one RecordForm of the kind and creates exactly one record: `submitFields` (`fieldCases`): `fields` when declared (order kept), else the Data tab's order; files, computed and undeclared names dropped; a required field left out of `fields` is an install issue (the guest could never submit). REFS: a guest has no picker and no read of members, issues or other records, so a ref field (`records.<kind>.refs`: member, issue or another kind's record) is never guest-facing: a required ref on a submit link with no `fields` and any `fields` naming a ref are install issues (`package-share.json`, TS + Rust), the default field lists drop refs, and the server ignores a ref a guest posts (so a guest cannot probe which members or issues exist). The record = the share's sponsor: `author_id` = `created_by` = `enabled_by`, `share_name` = the share's name, R4 history `via` = `share:<name>`. A sponsor who is no longer a member or an owner closes the link (`copy.gone`). Honeypot: the form carries the invisible field `honeypot`; filled = answered like a success, nothing stored, audited `honeypot`. CAPS: `caps.perIpPerHour` submissions per hashed IP per link and `caps.perLinkPerHour` per link (`capCases`; over = HTTP 429 `copy.tooMany`). Every accepted POST takes its slot under the link's lock BEFORE it writes, so parallel requests cannot pass a cap, and a refusal never counts toward a cap: `rate_limited` rows are not counted, so one IP cannot close a link for everyone, and an IP keeps at most one such row per hour. Honeypot rows do count; a filled honeypot over a cap still answers like a success, nothing stored. The plan's record quota and the schema validate as any write (an invalid body = 400 with the field issues, shown inline; the quota = `copy.closed`). Bodies ≤ `caps.maxBodyBytes`, refused without reading the rest (413). A body that is not a JSON object = 400. A value Postgres refuses (a NUL) = 400, audited `invalid`. A token outside the unpadded base64url alphabet is an unknown token. GET answers at most `caps.viewPerIpPerHour` per IP (in-process, 429 `copy.tooMany`), `no-store`. AUDIT: every submission = one `app_share_submissions` row {link, record?, ip_hash (HMAC of the IP, never the IP), outcome (`outcomes`), created_at}, kept `caps.auditDays` days. VIEW: a frozen query, computed by the server per request, no shape and no live sync: the share's `query` narrowed to `viewFields` (`fieldCases`: `fields`, else every schema field; the query's `expand` is ignored; reserved keys other than `id` and `updatedAt`, authors and computed names outside `fields` dropped), at most `caps.viewRows` rows, rendered as a read-only RecordTable (`editable: false`, no Add) or, with the share's `template` (VAPP-246, the install rules: `package-share.json`), as that template: the guest gets it in a one-template package with the narrowed kind, no queries, actions or functions, every binding reading the same snapshot, and the server re-checks it per request (a refused one = `copy.gone`); never another kind, no member data, no `exp:` source. DISPLAY (`bylineCases`, replayed ×4 wherever the By/author label shows): a record with `shareName` reads `copy.guest` instead of its author, and a history event with a `share:` via reads it as its actor (`historyCases`). SETTINGS (testId `shareLinks`, shown when the published package declares shares; everyone reads it, owners act): the existing group heading `copy.title`, one ListRow per share (testId `shareRow`): title = the share's name, subtitle = `rowSubtitle` (`rowCases`): `{mode label} · {state label}`, with the expiry date when on; trailing a Switch (owner; reads on for `on` and `orphaned` (`switchCases`), so an orphaned link can be revoked; on = enable, off = revoke) and, while on, the `…` menu `copy.copyLink` / `copy.newLink`, then one item per `expiryChoices` in order (`label`; picking one = EXPIRE with its `days`, null = never); the row's subtitle then reads the new date. A failed action = the failed-action toast. A guest's page whose package the host refuses or cannot paint (#VAPP-251) = the same `copy.gone`, never the validator's message.",
|
|
@@ -4882,7 +4882,7 @@ then left`
|
|
|
4882
4882
|
};
|
|
4883
4883
|
// packages/domain-contract/fixtures/apps.json
|
|
4884
4884
|
var apps_default = {
|
|
4885
|
-
$comment: "VAPP-110 Exponential apps ×4 (web routes/t/$teamSlug/apps/*, desktop crates/ui apps screens, iOS Apps/*, Android ui/apps/*). An APP = a team-scoped declarative package (the Exponential UI SDK's format: manifest + A2UI templates + bindings + a function allowlist, data never code) rendered by the Exponential UI renderers through the client's app host (`exp:` bindings over the synced store, `harness.*` functions behind the consent card, the URL policy). ONE look ×4. THE LIST (`apps` view; VAPP-242: PHONES ONLY = the Apps tab (web < md alike); web ≥ md, the IDE and Settings have NO Apps list page and no Apps nav row: the sidebar's APPS group lists the apps, see DESKTOP NAV): a Section band `copy.title` whose trailing action pill `copy.newApp` (ui-add, testId `newApp`) opens THE NEW MENU, then ListRows = BoardGlyph (icon + colour, the board set) + name (medium) + the version pill = `listPill` (V1, #VAPP-210: the PUBLISHED version `copy.version` `v{n}`; `copy.noVersion` = the DRAFT badge, muted, while published_version = 0) + a trailing `…` menu (`copy.menu.*`, `run.menu`: Edit app (`copy.menu.edit`, ui-edit, the creator or an owner) = THE EDITOR, Export (published > 0), Delete → the confirm prompt `copy.deleteTitle`/`copy.deleteBody`, members may delete only their own apps, owners every app; no Rename: the agent names an app, the settings page renames it). Exponential ships NO built-in apps (2026-10-24): the list is the team's synced apps only. Rows sort by name (case-insensitive). A TAP on a row RUNS the app (THE RUN VIEW, pushed), never the editor. Empty (a team with no apps): the list-size EmptyState `copy.empty` under the band. THE NEW MENU (VAPP-242, testId `newMenu`): every apps `+` (the sidebar group's `+`, the IDE rail's `+`, the phone list band's pill) opens THE existing Menu (no new component) with `run.newMenu` in order: `copy.newApp` (ui-add) = NEW APP, then `copy.import` (ui-file) = IMPORT. NEW APP (VAPP-176, EDITOR-FIRST, no dialog, no form, no `/settings/apps/new`): EVERY New app entry (THE NEW MENU's first item, the settings nav's `@new-app`) calls tRPC `apps.create {teamId}` with NO other field = a DRAFT named `copy.untitled` (slug `slugify(copy.untitled)` de-duplicated with `-2`, `-3`; no icon, no colour; head_version 0) and opens its editor at once. A DRAFT = an app with no version yet (head_version 0); its version pill reads `copy.noVersion` (`Draft`). THE AGENT NAMES IT: a draft's editor brief tells the editor session to pass name, icon, colour and description on its first `apps upsert`; a rename while the app is still a draft (that upsert, or the settings page) RE-DERIVES the slug from the new name (`slugify`, the app's own slug not counted as taken), so every editor keys the app by ID and follows a slug change in place (web: the route is replaced). CLEANUP: leaving the editor of an EMPTY draft (no version and no editor session, i.e. nothing was ever sent) calls tRPC `apps.discardDraft {id}`, which hard-deletes it only when it is still empty and the caller created it (anything else = a no-op), so abandoned drafts never pile up. IMPORT (app-export.json) = THE NEW MENU's second item `copy.import` (the import dialog on web ≥ md + the IDE, the sheet on phones); an imported app opens its editor. THE EDITOR (`app` view, route `/t/$teamSlug/app-editor/$appSlug` (VAPP-242: `run.editorPath`), VAPP-177 `editorTabs`; reached ONLY from the sidebar row's hover gear, the `…` › `copy.menu.edit` of a phone list row or THE RUN VIEW, the Settings nav's `@apps` rows, NEW APP and IMPORT; a plain row click RUNS the app): header = BoardGlyph + name + the version pill (`listPill`) + PUBLISH (V1, #VAPP-210: on web ≥ md and desktop a small primary Button `copy.publish` before the tabs, on phones the FIRST item of the `…` menu (icon `ui-submit`), testId `publish`, while `canPublish` and the viewer may write the app (access `write`) = tRPC `apps.publish {id}` → toast `copy.publishedToast`, a failure = the failed-action toast); on web ≥ md and desktop the header ends in a SegmentedControl of `editorTabs.wide` (VAPP-178 adds Data: Preview | Data | Versions | Settings; phones Chat | Preview | Data; the DATA tab = app-records.json `dataTab`) (`copy.tabs.*`, testId `tabs`) and the BODY = two panes, the chat `panes.chat` : the TAB pane `panes.preview` (fr), a hairline between (the chat pane has no header strip of its own); the editor opens on `editorTabs.default` (no tab is kept), a deep link `?tab=<id>` (web) or the settings nav (desktop) opens that tab. Phones (and web < md): a SegmentedControl of `editorTabs.phone` under the header, ONE pane (opens on `editorTabs.phoneDefault`), and the header's trailing `…` menu (testId `menu`) = `editorTabs.menu` (`copy.menu.*`): Versions (the count as trailing meta) and Settings open THE SAME tab bodies as a sheet titled by the item, Export (head > 0 only) the export sheet, Delete app (destructive; the creator or an owner) the delete confirm prompt; a phone deep link to `versions`/`settings` opens that sheet over the Chat tab. A DRAFT's editor: the two panes as always on web ≥ md and desktop, the Chat tab on phones; while its chat is empty the chat pane shows the page-size EmptyState (`copy.starterTitle`, the app glyph) whose actions are one `mode: action` Pill per `copy.starters` entry (a tap SENDS that text as the first message, disabled like the composer without a device), the composer is FOCUSED and its placeholder is `copy.draftPlaceholder`; the preview pane = the page-size EmptyState `copy.previewEmpty`. CHAT = the member's EDITOR SESSION with the app (a root with `sessions.app_id` + `user_id` = me): the same channel rows as a team agent's conversation (ChannelMessageRow: avatar / the app glyph, name, time, body; cards answer in place) and THE composer (`copy.composerPlaceholder`); no session yet = the same pane, empty, whose composer's first send is `sessions.send({appId, text, deviceId})` (the device = the caller's default device, `launch-device`; none online = the composer disabled with `copy.noDevice`). `copy.chatEmpty` sits in the empty chat. LIFETIME: entering the editor starts nothing; leaving it (route change, tab close) ENDS the member's live editor session (`sessions.end` on the root: the tree); the next send revives the same root (FEED-134: the row goes idle again with its context, so the chat is that root's messages). DRAFT VS PUBLISHED (V1, #VAPP-210, spec §2.3): every version write (`apps upsert`, Restore) makes a new HEAD = the editor's DRAFT and never changes what members run; `apps.package` is the PUBLISHED version's package (`published_version`; 0 = never published) and `apps.draft_package` the head's while the head is ahead (NULL when head = published, so the head's package is `draft_package ?? package`). Members, the exposed MCP tools (the broker) and automations run the PUBLISHED package; PUBLISH (the header button, a Versions row's button, the viewing banner, MCP `apps publish {version?}`) moves the pointer to any version (an older one = a rollback). An imported app file and an app created WITH a package (MCP `apps upsert` on a new slug) are published at once (v1); every later version is a draft. Records validate against the published kind schema, or the draft's while a draft exists (an editor seeds new kinds). PREVIEW = which version: `shownVersion` (`publishCases`): an explicitly viewed version (Versions › Preview), else nothing (head 0), else the HEAD while the viewer's own editor session is live (not ended) or nothing is published, else the PUBLISHED version; it renders that package's first template (VAPP-179: `previewTemplates`, see `preview.$comment`) (`applyTemplate`, surface id = the slug) in a frame `preview.radius` px / `preview.padding` px with the theme's background; the apps shape carries both packages, so every MCP `apps upsert` repaints the editing member's preview live. No version yet: the page-size EmptyState `copy.previewEmpty` in the frame. The HostBanner (offline / catalog update) sits above the surface; the consent card is the client's. VERSIONS (VAPP-177: the `versions` TAB on web ≥ md and desktop, the menu's sheet on phones; none yet = the list-size EmptyState `copy.versionsEmpty`): ONE grouped list (testId `versions`), rows newest first = the `copy.version` pill (`v{n}`) + `note` (`copy.noNote` muted when empty) over a muted caption `who · relative time` (a person's name, or `copy.byAgent`); trailing (`versionRow`): the PUBLISHED row = the `copy.published` badge; the HEAD row while ahead of it = the `copy.head` badge + a `copy.publish` outline button (testId `versionPublish`); every other row = a `copy.preview` ghost button (testId `versionPreview`) + a `copy.publish` outline button (testId `versionPublish`). PREVIEW switches to the Preview tab (phones: closes the sheet, the Preview tab) showing that version: a banner `copy.viewing` with `copy.publish` (not on the published version), `copy.restore` (not on the head) and `copy.head` (back to the preview's default) above the surface; RESTORE (the row's button or the banner's) = tRPC `apps.restore` → a NEW head version `copy.restoredNote` (`Restored v{n}`), toast `copy.restored`, the preview back on the head. CAPS: contract `apps` (slug, name, description, note, package bytes, components per template). RECORDS (VAPP-122, fixtures/app-records.json): an app keeps its own data as records (`records: {<kind>: {schema}}` in the manifest; `records:<kind>?…` reads them live, `records.upsert/delete` write them, no consent card); the editor's SETTINGS tab (×4, VAPP-177) shows the count as one muted line under the description: `recordsCountLabel` (`copy.records*`: `1,240 records`, with the plan quota `1,240 of 25,000 records`, `No records`); the quota (plan-limits `appRecords`, cloud only) refuses a write with the plan-limit error, shown as a toast. TOOLS (VAPP-120, fixtures/app-actions.json): every MCP operation is a typed host function `exponential.<noun>.<op>`; the editor's SETTINGS tab (×4) shows a `copy.tools` group under the records line: `copy.tools.title` with `copy.tools.hint`, one ListRow per function the PUBLISHED package declares (the head's while nothing is published; V1) (the display title + the function name muted + a Switch = the grant, a team OWNER only, disabled for everyone else; a destructive action shows `copy.tools.always` in place of the switch: it can never be granted), `copy.tools.none` when it declares none; `apps.granted_tools` syncs on the apps shape; a switch writes the whole list WITH the version on screen (`apps.update {grantedTools, headVersion}`, VAPP-156; the field carries `grantVersion` = the published version, else the head): a version published meanwhile refuses with CONFLICT; the grants follow the published package (an owner's publish keeps the still-declared ones, anyone else's clears them) (the failed-action toast), the row resyncs and the owner grants again; a granted call runs without the consent card, an ungranted or destructive one asks with the operation's words. FEEDBACK: a failed create/delete/restore = toast (feedback-states.json); an invalid name inline. Model = pure `apps.ts` (TS reference): every mirror replays `slugifyCases`, `versionLabelCases`, `sortAppsCases` and `publishCases`. DESKTOP NAV (web ≥ md + the IDE rail): no plain Apps nav row; an APPS sidebar GROUP drawn exactly like Agents and Boards, between them: the label `copy.title` (VAPP-242: NOT clickable, there is no Apps page), a `+` group action = THE NEW MENU, one compact row per app (its glyph in its colour + name → THE RUN VIEW, the active row while it runs), the hover gear (the creator or an owner) → THE EDITOR (its default tab). SETTINGS mirrors Agents: the settings nav's `Apps` group lists the apps (`@apps`, each → THE EDITOR on its Settings tab) + `@new-app` (= New app: a draft, its editor); VAPP-242: NO index page (Settings → Apps and the phones' Team-page Apps row are gone, settings-nav.json). THE SETTINGS TAB (VAPP-177, testId `settings`): the identity row (icon + colour + name, the name a deferred write), the description card (saved on blur), the records line, the RECORD ACCESS group (#VAPP-243, `policyRowCases`: each declared kind's effective policy, read-only), the Tools group (the grant switches, an owner only), the ACCOUNTS group and the MCP TOOLS group (#VAPP-243, `accountRowCases`: the viewer's own upstream connections with Connect / Reconnect, and the exposed tools' per-tool switch, an owner only), the AUTOMATIONS group (#VAPP-220, testId `automations`, shown when the PUBLISHED package declares automations; everyone reads it, nothing to switch: one row per automation (testId `automationRow`), title = its name, a second line = `automationRow(row).trigger` (#VAPP-243: + ` · ` + `automationSponsor(row)`, WHO it runs as or why it is paused, `automationSponsorCases`), a trailing Pill = `automationRow(row).label`; the rows come from tRPC `appAccess.appTools` `automations`, `automationRowCases`), then a row with `copy.menu.export` (head > 0) and `copy.menu.delete` (destructive, at the end; the creator or an owner) → the delete prompt, which leaves for the team's home (phones: the Apps tab); at most `editorTabs.settingsMaxWidth` px wide on web ≥ md and desktop. Everyone else sees it read-only. Phones get it (grants included) as the menu's Settings sheet. (VAPP-176: no create mode). PHONES: the Apps LIST is the APPS TAB (chats.json `shell.tabs`, #EXP-1379), a top-level page under the shared page header (`copy.title`, search + inbox circles); its `copy.newApp` pill (THE NEW MENU) stays on the Section band. VAPP-242: a tap RUNS the app (THE RUN VIEW pushed over the tab); the editor sits behind the row's / the run view's `…` › `copy.menu.edit`. VAPP-129's FLOATING layout (the chat in THE Dock over the preview, its per-device `exp.apps.editorLayout`, Move to the side / Minimize) is GONE since VAPP-177: the chat is always docked beside the tabs. TEMPLATES (VAPP-234, app-templates.json): a draft's starters end in one more pill `Start from a template` opening the import dialog/sheet, whose templates step imports a signed template as a finished app (v1 published) and opens its editor. VAPP-221: `appFiles` = the app hosts' FileUpload uploads (files on records). THE RUN VIEW (VAPP-242, Danny 2026-11-03; testId `run`; web routes `/t/$teamSlug/apps/$appSlug` and `/t/$teamSlug/apps/$appSlug/$screen?<params>`, desktop + natives the `appRun` route): the app as a member uses it, FULL SCREEN. It runs the PUBLISHED package (`apps.package`, never the draft, never Run as; `published_version` 0 = the page-size EmptyState `copy.runEmpty` with the app glyph, centred) through THE SAME app host and pieces as the editor's PREVIEW (no fork: records with `me` = the viewer, the published grants `granted_tools`, the app access level (`read` = the eUI readOnly paint), the HostBanner, the consent card, uploads, the media rule) and the `screens` shell (`screenShell(n, phone)` with `phone` = the device is a phone or web < md; no screens = the package's first template by id). No preview toolbar, no frame chooser, no template Select. LAYOUT: the surface fills the content area edge to edge (no frame border or radius; `preview.padding` inside the surface pane; the shell's sidebar column / phone tab bar as in the preview). Web ≥ md + the IDE: no header of its own (the sidebar's app row is the active one). Phones + web < md: the detail header (back, BoardGlyph + the app's name, a trailing `…` menu testId `runMenu` = `run.menu`: `copy.menu.edit` (the creator or an owner) → THE EDITOR, `copy.menu.export` (published > 0), `copy.menu.delete` (the creator or an owner, destructive) → the delete prompt). PHONES + web < md: every apps `…` and THE NEW MENU open THE Menu's bottom sheet titled by the app's name (THE NEW MENU: `copy.newApp`), never a popover. WHICH SCREEN = the preview's rule (`resolveScreen`; a repaint on a new published version keeps the screen when it still exists). `app.navigate`: on web REPLACES the route with `screenPath` (history replace); desktop + natives keep it in memory. DEEP LINKS (`screenPath`, app-link.json `appScreen`) land HERE; an old web link `/t/$teamSlug/apps/$appSlug?tab=<id>` (the editor's former path) REPLACES itself with THE EDITOR on that tab. A slug change follows in place like the editor (keyed by id). A PACKAGE THAT NO LONGER VALIDATES (#VAPP-251: published before a validator rule tightened, or a catalog change): it never takes the shell down, THE RUN VIEW and THE EDITOR's preview alike. `surfaceState(hasPackage, applied, hasSurface)` (`surfaceStateCases`) = `needsUpdate` once the host had its chance and holds no surface (the host refused the package or the template made nothing; web also: ANY throw while building or painting the surface, an error boundary per preview that resets when the package changes, so a republished package repaints without a reload, and catches nothing of the shell around it). The pane then shows ONLY the page-size EmptyState `copy.needsUpdate` with the app glyph, centred, no screens shell and no host banner; the sidebar, header and tabs stay, and the `…` menu keeps working. The validator's message never reaches the screen: it goes to the client's log. A guest gets `copy.gone` (app-share.json).",
|
|
4885
|
+
$comment: "VAPP-110 Exponential apps ×4 (web routes/t/$teamSlug/apps/*, desktop crates/ui apps screens, iOS Apps/*, Android ui/apps/*). An APP = a team-scoped declarative package (the Exponential UI SDK's format: manifest + A2UI templates + bindings + a function allowlist, data never code) rendered by the Exponential UI renderers through the client's app host (`exp:` bindings over the synced store, `harness.*` functions behind the consent card, the URL policy). ONE look ×4. THE LIST (`apps` view; VAPP-242: PHONES ONLY = the Apps tab (web < md alike); web ≥ md, the IDE and Settings have NO Apps list page and no Apps nav row: the sidebar's APPS group lists the apps, see DESKTOP NAV): a Section band `copy.title` whose trailing action pill `copy.newApp` (ui-add, testId `newApp`) opens THE NEW MENU, then ListRows = BoardGlyph (icon + colour, the board set) + name (medium) + the version pill = `listPill` (V1, #VAPP-210: the PUBLISHED version `copy.version` `v{n}`; `copy.noVersion` = the DRAFT badge, muted, while published_version = 0) + a trailing `…` menu (`copy.menu.*`, `run.menu`: Edit app (`copy.menu.edit`, ui-edit, the creator or an owner) = THE EDITOR, Export (published > 0), Delete → the confirm prompt `copy.deleteTitle`/`copy.deleteBody`, members may delete only their own apps, owners every app; no Rename: the agent names an app, the settings page renames it). Exponential ships NO built-in apps (2026-10-24): the list is the team's synced apps only. Rows sort by name (case-insensitive). A TAP on a row RUNS the app (THE RUN VIEW, pushed), never the editor. Empty (a team with no apps): the list-size EmptyState `copy.empty` under the band. THE NEW MENU (VAPP-242, testId `newMenu`): every apps `+` (the sidebar group's `+`, the IDE rail's `+`, the phone list band's pill) opens THE existing Menu (no new component) with `run.newMenu` in order: `copy.newApp` (ui-add) = NEW APP, then `copy.import` (ui-file) = IMPORT. NEW APP (VAPP-176, EDITOR-FIRST, no dialog, no form, no `/settings/apps/new`): EVERY New app entry (THE NEW MENU's first item, the settings nav's `@new-app`) calls tRPC `apps.create {teamId}` with NO other field = a DRAFT named `copy.untitled` (slug `slugify(copy.untitled)` de-duplicated with `-2`, `-3`; no icon, no colour; head_version 0) and opens its editor at once. A DRAFT = an app with no version yet (head_version 0); its version pill reads `copy.noVersion` (`Draft`). THE AGENT NAMES IT: a draft's editor brief tells the editor session to pass name, icon, colour and description on its first `apps upsert`; a rename while the app is still a draft (that upsert, or the settings page) RE-DERIVES the slug from the new name (`slugify`, the app's own slug not counted as taken), so every editor keys the app by ID and follows a slug change in place (web: the route is replaced). CLEANUP: leaving the editor of an EMPTY draft (no version and no editor session, i.e. nothing was ever sent) calls tRPC `apps.discardDraft {id}`, which hard-deletes it only when it is still empty and the caller created it (anything else = a no-op), so abandoned drafts never pile up. IMPORT (app-export.json) = THE NEW MENU's second item `copy.import` (the import dialog on web ≥ md + the IDE, the sheet on phones); an imported app opens its editor. THE EDITOR (`app` view, route `/t/$teamSlug/app-editor/$appSlug` (VAPP-242: `run.editorPath`), VAPP-177 `editorTabs`; reached ONLY from the sidebar row's hover gear, the `…` › `copy.menu.edit` of a phone list row or THE RUN VIEW, the Settings nav's `@apps` rows, NEW APP and IMPORT; a plain row click RUNS the app): header = BoardGlyph + name + the version pill (`listPill`) + PUBLISH (V1, #VAPP-210: on web ≥ md and desktop a small primary Button `copy.publish` before the tabs, on phones the FIRST item of the `…` menu (icon `ui-submit`), testId `publish`, while `canPublish` and the viewer may write the app (access `write`) = tRPC `apps.publish {id}` → toast `copy.publishedToast`, a failure = the failed-action toast); on web ≥ md and desktop the header ends in a SegmentedControl of `editorTabs.wide` (VAPP-178 adds Data: Preview | Data | Versions | Settings; phones Chat | Preview | Data; the DATA tab = app-records.json `dataTab`) (`copy.tabs.*`, testId `tabs`) and the BODY = two panes, the chat `panes.chat` : the TAB pane `panes.preview` (fr), a hairline between (the chat pane has no header strip of its own); the editor opens on `editorTabs.default` (no tab is kept), a deep link `?tab=<id>` (web) or the settings nav (desktop) opens that tab. Phones (and web < md): a SegmentedControl of `editorTabs.phone` under the header, ONE pane (opens on `editorTabs.phoneDefault`), and the header's trailing `…` menu (testId `menu`) = `editorTabs.menu` (`copy.menu.*`): Versions (the count as trailing meta) and Settings open THE SAME tab bodies as a sheet titled by the item, Export (head > 0 only) the export sheet, Delete app (destructive; the creator or an owner) the delete confirm prompt; a phone deep link to `versions`/`settings` opens that sheet over the Chat tab. A DRAFT's editor: the two panes as always on web ≥ md and desktop, the Chat tab on phones; while its chat is empty the chat pane shows the page-size EmptyState (`copy.starterTitle`, the app glyph) whose actions are one `mode: action` Pill per `copy.starters` entry (a tap SENDS that text as the first message, disabled like the composer without a device), the composer is FOCUSED and its placeholder is `copy.draftPlaceholder`; the preview pane = the page-size EmptyState `copy.previewEmpty`. CHAT = the member's EDITOR SESSION with the app (a root with `sessions.app_id` + `user_id` = me): the same channel rows as a team agent's conversation (ChannelMessageRow: avatar / the app glyph, name, time, body; cards answer in place) and THE composer (`copy.composerPlaceholder`); no session yet = the same pane, empty, whose composer's first send is `sessions.send({appId, text, deviceId})` (the device = EDITOR DEVICE below; none online = the composer disabled with `copy.noDevice`). `copy.chatEmpty` sits in the empty chat. EDITOR DEVICE (#FEED-204, `editorDevice`, `editorDeviceCases`): the composer's `+` carries THE launch composer's Device › row (composer-menu.json `device`, surfaces `session`/`agent`, no new component: THE device picker's rows, value = the device's name) over the member's online devices (with an editor root: minus the targets session-device-move.json refuses). `editorDevice(root, candidates, remembered)`: no root or an ENDED root = free to change: the REMEMBERED pick when it is a candidate, else the ended root's own device, else the default device, else the first candidate, else the ended root's device (nothing online; the send waits there as it always did). A LIVE root (queued, idle, running: anything not ended) = `locked`: the row names the root's device, disabled (a live move would end the root and continue it as a child, breaking ONE root per member; leaving the editor ends the root, so the change is one re-entry away). `move` = an ended root's next send goes elsewhere: that send is `sessions.send({appId, text, deviceId})` (`messageApp` → `reviveRoot` re-targets the row and says so in the chat, and the brief rides again for the new device), every other send to an existing root stays `{sessionId}`. A pick is remembered per app on THIS client (web localStorage `exp.apps.editorDevice.<appId>`, iOS UserDefaults, Android SharedPreferences, desktop ui_prefs), never in `devices.setDefault`; a remembered device that is offline is kept and simply loses to the fallbacks until it is back. Starters send on the same device. LIFETIME: entering the editor starts nothing; leaving it (route change, tab close) ENDS the member's live editor session (`sessions.end` on the root: the tree); the next send revives the same root (FEED-134: the row goes idle again with its context, so the chat is that root's messages). DRAFT VS PUBLISHED (V1, #VAPP-210, spec §2.3): every version write (`apps upsert`, Restore) makes a new HEAD = the editor's DRAFT and never changes what members run; `apps.package` is the PUBLISHED version's package (`published_version`; 0 = never published) and `apps.draft_package` the head's while the head is ahead (NULL when head = published, so the head's package is `draft_package ?? package`). Members, the exposed MCP tools (the broker) and automations run the PUBLISHED package; PUBLISH (the header button, a Versions row's button, the viewing banner, MCP `apps publish {version?}`) moves the pointer to any version (an older one = a rollback). An imported app file and an app created WITH a package (MCP `apps upsert` on a new slug) are published at once (v1); every later version is a draft. Records validate against the published kind schema, or the draft's while a draft exists (an editor seeds new kinds). PREVIEW = which version: `shownVersion` (`publishCases`): an explicitly viewed version (Versions › Preview), else nothing (head 0), else the HEAD while the viewer's own editor session is live (not ended) or nothing is published, else the PUBLISHED version; it renders that package's first template (VAPP-179: `previewTemplates`, see `preview.$comment`) (`applyTemplate`, surface id = the slug) in a frame `preview.radius` px / `preview.padding` px with the theme's background; the apps shape carries both packages, so every MCP `apps upsert` repaints the editing member's preview live. No version yet: the page-size EmptyState `copy.previewEmpty` in the frame. The HostBanner (offline / catalog update) sits above the surface; the consent card is the client's. VERSIONS (VAPP-177: the `versions` TAB on web ≥ md and desktop, the menu's sheet on phones; none yet = the list-size EmptyState `copy.versionsEmpty`): ONE grouped list (testId `versions`), rows newest first = the `copy.version` pill (`v{n}`) + `note` (`copy.noNote` muted when empty) over a muted caption `who · relative time` (a person's name, or `copy.byAgent`); trailing (`versionRow`): the PUBLISHED row = the `copy.published` badge; the HEAD row while ahead of it = the `copy.head` badge + a `copy.publish` outline button (testId `versionPublish`); every other row = a `copy.preview` ghost button (testId `versionPreview`) + a `copy.publish` outline button (testId `versionPublish`). PREVIEW switches to the Preview tab (phones: closes the sheet, the Preview tab) showing that version: a banner `copy.viewing` with `copy.publish` (not on the published version), `copy.restore` (not on the head) and `copy.head` (back to the preview's default) above the surface; RESTORE (the row's button or the banner's) = tRPC `apps.restore` → a NEW head version `copy.restoredNote` (`Restored v{n}`), toast `copy.restored`, the preview back on the head. CAPS: contract `apps` (slug, name, description, note, package bytes, components per template). RECORDS (VAPP-122, fixtures/app-records.json): an app keeps its own data as records (`records: {<kind>: {schema}}` in the manifest; `records:<kind>?…` reads them live, `records.upsert/delete` write them, no consent card); the editor's SETTINGS tab (×4, VAPP-177) shows the count as one muted line under the description: `recordsCountLabel` (`copy.records*`: `1,240 records`, with the plan quota `1,240 of 25,000 records`, `No records`); the quota (plan-limits `appRecords`, cloud only) refuses a write with the plan-limit error, shown as a toast. TOOLS (VAPP-120, fixtures/app-actions.json): every MCP operation is a typed host function `exponential.<noun>.<op>`; the editor's SETTINGS tab (×4) shows a `copy.tools` group under the records line: `copy.tools.title` with `copy.tools.hint`, one ListRow per function the PUBLISHED package declares (the head's while nothing is published; V1) (the display title + the function name muted + a Switch = the grant, a team OWNER only, disabled for everyone else; a destructive action shows `copy.tools.always` in place of the switch: it can never be granted), `copy.tools.none` when it declares none; `apps.granted_tools` syncs on the apps shape; a switch writes the whole list WITH the version on screen (`apps.update {grantedTools, headVersion}`, VAPP-156; the field carries `grantVersion` = the published version, else the head): a version published meanwhile refuses with CONFLICT; the grants follow the published package (an owner's publish keeps the still-declared ones, anyone else's clears them) (the failed-action toast), the row resyncs and the owner grants again; a granted call runs without the consent card, an ungranted or destructive one asks with the operation's words. FEEDBACK: a failed create/delete/restore = toast (feedback-states.json); an invalid name inline. Model = pure `apps.ts` (TS reference): every mirror replays `slugifyCases`, `versionLabelCases`, `sortAppsCases` and `publishCases`. DESKTOP NAV (web ≥ md + the IDE rail): no plain Apps nav row; an APPS sidebar GROUP drawn exactly like Agents and Boards, between them: the label `copy.title` (VAPP-242: NOT clickable, there is no Apps page), a `+` group action = THE NEW MENU, one compact row per app (its glyph in its colour + name → THE RUN VIEW, the active row while it runs), the hover gear (#FEED-203: EVERY member, every row; keyboard focus reveals it too; read-only below the creator or an owner, as THE SETTINGS TAB says) → THE EDITOR on its Settings tab. SETTINGS mirrors Agents: the settings nav's `Apps` group lists the apps (`@apps`, each → THE EDITOR on its Settings tab) + `@new-app` (= New app: a draft, its editor); VAPP-242: NO index page (Settings → Apps and the phones' Team-page Apps row are gone, settings-nav.json). THE SETTINGS TAB (VAPP-177, testId `settings`): the identity row (icon + colour + name, the name a deferred write), the description card (saved on blur), the records line, the RECORD ACCESS group (#VAPP-243, `policyRowCases`: each declared kind's effective policy, read-only), the Tools group (the grant switches, an owner only), the ACCOUNTS group and the MCP TOOLS group (#VAPP-243, `accountRowCases`: the viewer's own upstream connections with Connect / Reconnect, and the exposed tools' per-tool switch, an owner only), the AUTOMATIONS group (#VAPP-220, testId `automations`, shown when the PUBLISHED package declares automations; everyone reads it, nothing to switch: one row per automation (testId `automationRow`), title = its name, a second line = `automationRow(row).trigger` (#VAPP-243: + ` · ` + `automationSponsor(row)`, WHO it runs as or why it is paused, `automationSponsorCases`), a trailing Pill = `automationRow(row).label`; the rows come from tRPC `appAccess.appTools` `automations`, `automationRowCases`), then a row with `copy.menu.export` (head > 0) and `copy.menu.delete` (destructive, at the end; the creator or an owner) → the delete prompt, which leaves for the team's home (phones: the Apps tab); at most `editorTabs.settingsMaxWidth` px wide on web ≥ md and desktop. Everyone else sees it read-only. Phones get it (grants included) as the menu's Settings sheet. (VAPP-176: no create mode). PHONES: the Apps LIST is the APPS TAB (chats.json `shell.tabs`, #EXP-1379), a top-level page under the shared page header (`copy.title`, search + inbox circles); its `copy.newApp` pill (THE NEW MENU) stays on the Section band. VAPP-242: a tap RUNS the app (THE RUN VIEW pushed over the tab); the editor sits behind the row's / the run view's `…` › `copy.menu.edit`. VAPP-129's FLOATING layout (the chat in THE Dock over the preview, its per-device `exp.apps.editorLayout`, Move to the side / Minimize) is GONE since VAPP-177: the chat is always docked beside the tabs. TEMPLATES (VAPP-234, app-templates.json): a draft's starters end in one more pill `Start from a template` opening the import dialog/sheet, whose templates step imports a signed template as a finished app (v1 published) and opens its editor. VAPP-221: `appFiles` = the app hosts' FileUpload uploads (files on records). THE RUN VIEW (VAPP-242, Danny 2026-11-03; testId `run`; web routes `/t/$teamSlug/apps/$appSlug` and `/t/$teamSlug/apps/$appSlug/$screen?<params>`, desktop + natives the `appRun` route): the app as a member uses it, FULL SCREEN. It runs the PUBLISHED package (`apps.package`, never the draft, never Run as; `published_version` 0 = the page-size EmptyState `copy.runEmpty` with the app glyph, centred) through THE SAME app host and pieces as the editor's PREVIEW (no fork: records with `me` = the viewer, the published grants `granted_tools`, the app access level (`read` = the eUI readOnly paint), the HostBanner, the consent card, uploads, the media rule) and the `screens` shell (`screenShell(n, phone)` with `phone` = the device is a phone or web < md; no screens = the package's first template by id). No preview toolbar, no frame chooser, no template Select. LAYOUT: the surface fills the content area edge to edge (no frame border or radius; `preview.padding` inside the surface pane; the shell's sidebar column / phone tab bar as in the preview). Web ≥ md + the IDE: no header of its own (the sidebar's app row is the active one). Phones + web < md: the detail header (back, BoardGlyph + the app's name, a trailing `…` menu testId `runMenu` = `run.menu`: `copy.menu.edit` (the creator or an owner) → THE EDITOR, `copy.menu.export` (published > 0), `copy.menu.delete` (the creator or an owner, destructive) → the delete prompt). PHONES + web < md: every apps `…` and THE NEW MENU open THE Menu's bottom sheet titled by the app's name (THE NEW MENU: `copy.newApp`), never a popover. WHICH SCREEN = the preview's rule (`resolveScreen`; a repaint on a new published version keeps the screen when it still exists). `app.navigate`: on web REPLACES the route with `screenPath` (history replace); desktop + natives keep it in memory. DEEP LINKS (`screenPath`, app-link.json `appScreen`) land HERE; an old web link `/t/$teamSlug/apps/$appSlug?tab=<id>` (the editor's former path) REPLACES itself with THE EDITOR on that tab. A slug change follows in place like the editor (keyed by id). A PACKAGE THAT NO LONGER VALIDATES (#VAPP-251: published before a validator rule tightened, or a catalog change): it never takes the shell down, THE RUN VIEW and THE EDITOR's preview alike. `surfaceState(hasPackage, applied, hasSurface)` (`surfaceStateCases`) = `needsUpdate` once the host had its chance and holds no surface (the host refused the package or the template made nothing; web also: ANY throw while building or painting the surface, an error boundary per preview that resets when the package changes, so a republished package repaints without a reload, and catches nothing of the shell around it). The pane then shows ONLY the page-size EmptyState `copy.needsUpdate` with the app glyph, centred, no screens shell and no host banner; the sidebar, header and tabs stay, and the `…` menu keeps working. The validator's message never reaches the screen: it goes to the client's log. A guest gets `copy.gone` (app-share.json).",
|
|
4886
4886
|
copy: {
|
|
4887
4887
|
nav: "Apps",
|
|
4888
4888
|
title: "Apps",
|
|
@@ -5508,6 +5508,92 @@ var apps_default = {
|
|
|
5508
5508
|
}
|
|
5509
5509
|
]
|
|
5510
5510
|
},
|
|
5511
|
+
editorDeviceCases: [
|
|
5512
|
+
{
|
|
5513
|
+
name: "no root, nothing remembered: the default device",
|
|
5514
|
+
root: null,
|
|
5515
|
+
candidates: [{ deviceId: "mint", isDefault: false }, { deviceId: "mac", isDefault: true }],
|
|
5516
|
+
remembered: null,
|
|
5517
|
+
expected: { deviceId: "mac", locked: false, move: false }
|
|
5518
|
+
},
|
|
5519
|
+
{
|
|
5520
|
+
name: "no root, no default: the first candidate",
|
|
5521
|
+
root: null,
|
|
5522
|
+
candidates: [{ deviceId: "mint", isDefault: false }, { deviceId: "pi", isDefault: false }],
|
|
5523
|
+
remembered: null,
|
|
5524
|
+
expected: { deviceId: "mint", locked: false, move: false }
|
|
5525
|
+
},
|
|
5526
|
+
{
|
|
5527
|
+
name: "no root, the remembered device online beats the default",
|
|
5528
|
+
root: null,
|
|
5529
|
+
candidates: [{ deviceId: "mint", isDefault: false }, { deviceId: "mac", isDefault: true }],
|
|
5530
|
+
remembered: "mint",
|
|
5531
|
+
expected: { deviceId: "mint", locked: false, move: false }
|
|
5532
|
+
},
|
|
5533
|
+
{
|
|
5534
|
+
name: "no root, the remembered device offline: the default",
|
|
5535
|
+
root: null,
|
|
5536
|
+
candidates: [{ deviceId: "mac", isDefault: true }],
|
|
5537
|
+
remembered: "mint",
|
|
5538
|
+
expected: { deviceId: "mac", locked: false, move: false }
|
|
5539
|
+
},
|
|
5540
|
+
{
|
|
5541
|
+
name: "no root, nothing online: no device",
|
|
5542
|
+
root: null,
|
|
5543
|
+
candidates: [],
|
|
5544
|
+
remembered: "mint",
|
|
5545
|
+
expected: { deviceId: null, locked: false, move: false }
|
|
5546
|
+
},
|
|
5547
|
+
{
|
|
5548
|
+
name: "a live root is locked on its device, whatever is remembered",
|
|
5549
|
+
root: { status: "running", deviceId: "pi" },
|
|
5550
|
+
candidates: [{ deviceId: "mac", isDefault: true }, { deviceId: "pi", isDefault: false }],
|
|
5551
|
+
remembered: "mac",
|
|
5552
|
+
expected: { deviceId: "pi", locked: true, move: false }
|
|
5553
|
+
},
|
|
5554
|
+
{
|
|
5555
|
+
name: "an idle root is live too",
|
|
5556
|
+
root: { status: "idle", deviceId: "pi" },
|
|
5557
|
+
candidates: [{ deviceId: "mac", isDefault: true }],
|
|
5558
|
+
remembered: null,
|
|
5559
|
+
expected: { deviceId: "pi", locked: true, move: false }
|
|
5560
|
+
},
|
|
5561
|
+
{
|
|
5562
|
+
name: "a queued root is live too",
|
|
5563
|
+
root: { status: "queued", deviceId: "pi" },
|
|
5564
|
+
candidates: [{ deviceId: "mac", isDefault: true }],
|
|
5565
|
+
remembered: "mac",
|
|
5566
|
+
expected: { deviceId: "pi", locked: true, move: false }
|
|
5567
|
+
},
|
|
5568
|
+
{
|
|
5569
|
+
name: "an ended root stays on its device while it is online",
|
|
5570
|
+
root: { status: "ended", deviceId: "pi" },
|
|
5571
|
+
candidates: [{ deviceId: "mac", isDefault: true }, { deviceId: "pi", isDefault: false }],
|
|
5572
|
+
remembered: null,
|
|
5573
|
+
expected: { deviceId: "pi", locked: false, move: false }
|
|
5574
|
+
},
|
|
5575
|
+
{
|
|
5576
|
+
name: "an ended root moves to the remembered device",
|
|
5577
|
+
root: { status: "ended", deviceId: "pi" },
|
|
5578
|
+
candidates: [{ deviceId: "mac", isDefault: true }, { deviceId: "pi", isDefault: false }],
|
|
5579
|
+
remembered: "mac",
|
|
5580
|
+
expected: { deviceId: "mac", locked: false, move: true }
|
|
5581
|
+
},
|
|
5582
|
+
{
|
|
5583
|
+
name: "an ended root on an offline device moves to the default",
|
|
5584
|
+
root: { status: "ended", deviceId: "pi" },
|
|
5585
|
+
candidates: [{ deviceId: "mint", isDefault: false }, { deviceId: "mac", isDefault: true }],
|
|
5586
|
+
remembered: "gone",
|
|
5587
|
+
expected: { deviceId: "mac", locked: false, move: true }
|
|
5588
|
+
},
|
|
5589
|
+
{
|
|
5590
|
+
name: "an ended root with nothing online stays where it is",
|
|
5591
|
+
root: { status: "ended", deviceId: "pi" },
|
|
5592
|
+
candidates: [],
|
|
5593
|
+
remembered: "mac",
|
|
5594
|
+
expected: { deviceId: "pi", locked: false, move: false }
|
|
5595
|
+
}
|
|
5596
|
+
],
|
|
5511
5597
|
runAs: {
|
|
5512
5598
|
$comment: "VAPP-198 (eApps R3, spec §11) RUN AS <member>: in THE PREVIEW TOOLBAR (`preview`: web ≥ md + desktop; phones have no toolbar, so no Run as), for a team OWNER only, a Select (testId `previewRunAs`) after the frame control: `runAsOptions(members, me, role)` = null (no Select) unless the viewer is an owner and the team has another member; else `copy.runAs.me` (value \"\") first, then every OTHER member by name (case-insensitive, then id; no name = the id). Picking a member renders the preview AS them: the host's `me` is them, a `records:` binding reads only the rows their team role and their app access may read (packages/exponential-ui fixtures/record-policy.json `readable`; the owner's device holds every row), and every write or host call (records.*, actions' writes, MCP and typed actions) is refused with the toast `copy.runAs.writesOff` ({name} = their name) and does nothing: a preview never writes as someone else. `exp:` sources stay the owner's (the preview cannot read another member's issues). Switching apps or leaving the editor drops back to Me."
|
|
5513
5599
|
},
|
|
@@ -6779,5 +6865,5 @@ var outboxMode2 = (hasSync) => hasSync ? `queue` : `direct`;
|
|
|
6779
6865
|
|
|
6780
6866
|
export { outboxWrites2, outboxRequest2, outboxRows2, outboxOutcome2, outboxVerb2, outboxFlush2, outboxReflected2, outboxPrune2, outboxPendingLabel2, outboxMode2 };
|
|
6781
6867
|
|
|
6782
|
-
//# debugId=
|
|
6783
|
-
//# sourceMappingURL=index-
|
|
6868
|
+
//# debugId=E4BC8F0513ABCBE964756E2164756E21
|
|
6869
|
+
//# sourceMappingURL=index-v0m7r4y3.js.map
|