@byollm/server 0.1.0-alpha.9 → 0.1.0-alpha.91
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 +180 -9
- package/dist/{chunk-4NIHWQAT.js → chunk-36Y77FUD.js} +106 -50
- package/dist/chunk-36Y77FUD.js.map +1 -0
- package/dist/{chunk-7RKXFPBZ.js → chunk-I3ER27QG.js} +19 -5
- package/dist/chunk-I3ER27QG.js.map +1 -0
- package/dist/{delivery-36nIe-b3.d.ts → delivery-CaGbp0Tc.d.ts} +35 -5
- package/dist/{handlers-DgW0QNTf.d.ts → handlers-CTV3Jc6Q.d.ts} +2 -2
- package/dist/index.d.ts +92 -24
- package/dist/index.js +317 -104
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +2 -2
- package/dist/next.js +1 -1
- package/dist/{store-Cj5b6A9j.d.ts → store-Cx2_bck1.d.ts} +130 -18
- package/dist/supabase/index.d.ts +2 -2
- package/dist/supabase/index.js +62 -29
- package/dist/supabase/index.js.map +1 -1
- package/package.json +2 -2
- package/supabase/migrations/20260819000000_drop_runner_token.sql +87 -0
- package/supabase/migrations/20260819010000_completed_by_lease_id.sql +25 -0
- package/supabase/migrations/20260821000000_rename_collected.sql +91 -0
- package/supabase/migrations/20260824000000_one_vocabulary.sql +109 -0
- package/supabase/migrations/20260825000000_job_service.sql +20 -0
- package/supabase/migrations/20260827000000_job_purpose.sql +36 -0
- package/dist/chunk-4NIHWQAT.js.map +0 -1
- package/dist/chunk-7RKXFPBZ.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/supabase/realtime.ts","../../src/supabase/index.ts"],"sourcesContent":["import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { DeliveredResult } from \"@byollm/protocol\";\nimport {\n NoRunnerAvailableError,\n ResultTimeoutError,\n type PollingDeliveryDeps,\n type ResultDelivery,\n type WaitOptions,\n} from \"../delivery.js\";\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60_000;\n/**\n * How long a sustained no-runner signal must persist before it is believed.\n * A daemon restarting must not fail every job in flight.\n */\nconst NO_RUNNER_GRACE_MS = 10_000;\n\n/**\n * Realtime delivery: the app learns a job finished when Postgres says so.\n *\n * byollm_003 Rev 1 requires the server→app path be an explicit channel rather\n * than an implied in-request `await`. Polling is the portable default;\n * this is the one worth having when the app is already on Supabase, because\n * a result arrives in milliseconds instead of on the next poll tick.\n *\n * The no-runner watch still polls, deliberately: runner liveness is a\n * *derived* signal (nobody with matching capability has heartbeated lately),\n * and there is no row change to subscribe to for \"something stopped\n * happening\".\n */\nexport function supabaseRealtimeDelivery(\n client: SupabaseClient,\n): (deps: PollingDeliveryDeps) => ResultDelivery {\n return (deps) => new SupabaseRealtimeDelivery(client, deps);\n}\n\nclass SupabaseRealtimeDelivery implements ResultDelivery {\n readonly #client: SupabaseClient;\n readonly #deps: PollingDeliveryDeps;\n\n constructor(client: SupabaseClient, deps: PollingDeliveryDeps) {\n this.#client = client;\n this.#deps = deps;\n }\n\n async waitFor(\n jobId: string,\n options: WaitOptions = {},\n ): Promise<DeliveredResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n // Read first. The job may already be terminal, and subscribing to a\n // channel for an event that has already happened waits forever.\n const current = await this.#deps.read(jobId);\n if (current && isTerminal(current.state)) return current;\n\n // Declared before the subscription so the channel callback closes over a\n // `settled` that already exists. Every async path below routes its failure\n // here: a rejection that escapes this object becomes an unhandled\n // rejection, and an unhandled rejection ends the process.\n const settled = Promise.withResolvers<DeliveredResult>();\n this.#resolve = settled.resolve;\n\n const channel = this.#client.channel(`byollm_job_${jobId}`).on(\n \"postgres_changes\",\n {\n event: \"UPDATE\",\n schema: \"public\",\n table: \"byollm_jobs\",\n filter: `id=eq.${jobId}`,\n },\n () => {\n this.#check(jobId).catch(settled.reject);\n },\n );\n\n // `subscribe()` returns the channel, not a promise — awaiting it would be\n // a no-op that reads as if it waited for the subscription to be live.\n channel.subscribe();\n\n // A second read after subscribing closes the race where the job finished\n // between the first read and the subscription taking effect.\n this.#check(jobId).catch(settled.reject);\n\n const timer = setTimeout(() => {\n settled.reject(new ResultTimeoutError(jobId, timeoutMs));\n }, timeoutMs);\n\n const watcher = this.#watchAvailability(jobId, options, settled);\n const abort = (): void => {\n settled.reject(new Error(\"wait aborted\"));\n };\n options.signal?.addEventListener(\"abort\", abort, { once: true });\n\n try {\n return await settled.promise;\n } finally {\n clearTimeout(timer);\n clearInterval(watcher);\n options.signal?.removeEventListener(\"abort\", abort);\n await this.#client.removeChannel(channel);\n }\n }\n\n #resolve: ((result: DeliveredResult) => void) | undefined;\n\n async #check(jobId: string): Promise<void> {\n const current = await this.#deps.read(jobId);\n if (current && isTerminal(current.state)) this.#resolve?.(current);\n }\n\n /** Poll runner liveness; there is no row event for \"nothing is happening\". */\n #watchAvailability(\n jobId: string,\n options: WaitOptions,\n settled: PromiseWithResolvers<DeliveredResult>,\n ): NodeJS.Timeout {\n let noRunnerSince: number | null = null;\n\n return setInterval(() => {\n // `.catch`, not `void`. Two things in here can reject — the store read\n // and the caller's own `onNoRunner` — and discarding either made a\n // transient store error, or an app whose fallback throws, terminate the\n // process. The caller is awaiting `result()`; that is where a failure\n // belongs, and it is what the polling channel already does by virtue of\n // running inside the awaited chain. A delivery adapter must not change\n // what a failure means.\n (async () => {\n const availability = await this.#deps.availability(jobId);\n if (availability.available || availability.blocked) {\n noRunnerSince = null;\n return;\n }\n noRunnerSince ??= Date.now();\n if (Date.now() - noRunnerSince < NO_RUNNER_GRACE_MS) return;\n\n const reason = availability.reason ?? \"no-runner-online\";\n const substitute = await options.onNoRunner?.(reason);\n if (substitute) {\n settled.resolve(substitute);\n } else {\n settled.reject(new NoRunnerAvailableError(jobId, reason));\n }\n })().catch(settled.reject);\n }, 2_000);\n }\n}\n\nfunction isTerminal(state: string): boolean {\n return (\n state === \"ok\" ||\n state === \"error\" ||\n state === \"canceled\" ||\n state === \"expired\"\n );\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type {\n Capability,\n JobOutcome,\n JobState,\n PublicIdentity,\n} from \"@byollm/protocol\";\nimport type {\n StoredJobInput,\n JobRecord,\n PairingRecord,\n RunnerRecord,\n} from \"../records.js\";\nimport type {\n ApproveArgs,\n ByollmStore,\n ClaimArgs,\n AdoptArgs,\n CompleteArgs,\n CompleteResult,\n LeaseRef,\n ReleaseArgs,\n RenewArgs,\n RenewResult,\n TouchArgs,\n} from \"../store.js\";\n\n/**\n * `@byollm/server/supabase` — the first-party Supabase adapter.\n *\n * The piece the of-tomorrow-framework's runner module consumes verbatim.\n * Migrations ship in `supabase/migrations`; the atomic claim lives in a\n * `security definer` RPC using `FOR UPDATE SKIP LOCKED`, and the audience\n * rules are mirrored in SQL so the server refuses independently of the daemon\n * (byollm_003 §Server-side MUSTs).\n *\n * Requires the **service role** key: a runner authenticates with a bearer\n * token of its own, which is not a Supabase session, so the protocol handler\n * cannot run under RLS as the runner's user. RLS still governs everything the\n * *browser* does — the app-side policies in the migration are what protect\n * one user's jobs from another.\n *\n * @packageDocumentation\n */\n\n/** Row shape of `byollm_jobs`. */\ninterface JobRow {\n id: string;\n kind: string;\n envelope: unknown;\n size_class: \"small\" | \"medium\" | \"large\" | \"unbounded\";\n audience: \"self\" | \"named\" | \"public\";\n owner: string;\n audience_allow: string[] | null;\n depends_on: string[];\n state: JobState;\n lease_id: string | null;\n lease_runner: string | null;\n lease_expires_at: string | null;\n claimable_at: string | null;\n ttl_ms: number;\n deadline_at: string | null;\n refused_by: string[];\n attempts: number;\n outcome: JobOutcome | null;\n provenance: JobRecord[\"provenance\"];\n created_at: string;\n updated_at: string;\n}\n\n/** Row shape of `byollm_runners`. */\ninterface RunnerRow {\n id: string;\n owner: string;\n token_hash: string;\n label: string;\n platform: \"darwin\" | \"linux\" | \"win32\";\n daemon_version: string;\n capabilities: Capability[];\n device: PublicIdentity;\n paused: boolean;\n revoked_at: string | null;\n last_heartbeat_at: string;\n created_at: string;\n}\n\n/** Row shape of `byollm_pairings`. */\ninterface PairingRow {\n device_code_hash: string;\n user_code: string;\n state: \"pending\" | \"approved\" | \"denied\";\n owner: string | null;\n runner_id: string | null;\n runner_token_once: string | null;\n label: string;\n platform: \"darwin\" | \"linux\" | \"win32\";\n daemon_version: string;\n capabilities: Capability[];\n device: PublicIdentity;\n expires_at: string;\n created_at: string;\n}\n\nconst ms = (iso: string | null): number | null =>\n iso === null ? null : Date.parse(iso);\n\nconst iso = (epochMs: number): string => new Date(epochMs).toISOString();\n\nfunction toJob(row: JobRow): JobRecord {\n const leaseExpires = ms(row.lease_expires_at);\n return {\n id: row.id,\n kind: row.kind as JobRecord[\"kind\"],\n envelope: row.envelope as JobRecord[\"envelope\"],\n sizeClass: row.size_class,\n audience: row.audience,\n owner: row.owner,\n audienceAllow: row.audience_allow ?? undefined,\n dependsOn: row.depends_on,\n state: row.state,\n lease:\n // Keyed on the lease id, not the runner. A relayed grant has no runner\n // row to point at (see AdoptArgs), and reading the lease as absent\n // because `lease_runner` is null would make an actively-held job look\n // claimable — the exact bug `adopt` exists to prevent.\n leaseExpires !== null && row.lease_id !== null\n ? {\n id: row.lease_id,\n runnerId: row.lease_runner ?? \"\",\n expiresAt: leaseExpires,\n }\n : null,\n createdAt: Date.parse(row.created_at),\n claimableAt: ms(row.claimable_at),\n ttlMs: row.ttl_ms,\n deadlineAt: ms(row.deadline_at),\n refusedBy: row.refused_by,\n attempts: row.attempts,\n outcome: row.outcome,\n provenance: row.provenance,\n updatedAt: Date.parse(row.updated_at),\n };\n}\n\n/**\n * A PostgREST filter matching exactly these (job, lease) pairs.\n *\n * Not two `IN` lists: `id IN (…) AND lease_id IN (…)` is a cross product, and\n * while UUID uniqueness makes a mismatch improbable, \"improbable\" is not the\n * property a lease check should rest on. This says what it means.\n */\nconst leasePairs = (leases: readonly LeaseRef[]): string =>\n leases.map((l) => `and(id.eq.${l.jobId},lease_id.eq.${l.leaseId})`).join(\",\");\n\nfunction toRunner(row: RunnerRow): RunnerRecord {\n return {\n id: row.id,\n owner: row.owner,\n tokenHash: row.token_hash,\n label: row.label,\n platform: row.platform,\n daemonVersion: row.daemon_version,\n capabilities: row.capabilities,\n device: row.device,\n paused: row.paused,\n revokedAt: ms(row.revoked_at),\n lastHeartbeatAt: Date.parse(row.last_heartbeat_at),\n createdAt: Date.parse(row.created_at),\n };\n}\n\nfunction toPairing(row: PairingRow): PairingRecord {\n return {\n deviceCodeHash: row.device_code_hash,\n userCode: row.user_code,\n state: row.state,\n owner: row.owner,\n runnerId: row.runner_id,\n runnerTokenOnce: row.runner_token_once,\n label: row.label,\n platform: row.platform,\n daemonVersion: row.daemon_version,\n capabilities: row.capabilities,\n device: row.device,\n expiresAt: Date.parse(row.expires_at),\n createdAt: Date.parse(row.created_at),\n };\n}\n\nexport interface SupabaseStoreOptions {\n /** A client built with the **service role** key. */\n readonly client: SupabaseClient;\n /** Default TTL for a job once claimable. */\n readonly defaultTtlMs?: number;\n}\n\n/** Build the Supabase-backed store. */\nexport function supabaseStore(options: SupabaseStoreOptions): ByollmStore {\n const db = options.client;\n const defaultTtlMs = options.defaultTtlMs ?? 15 * 60_000;\n\n /**\n * Narrow one PostgREST response, or throw with the Postgres message.\n *\n * `supabase-js` types rows as `any` unless the project has generated\n * database types, so the assertion has to live somewhere. Confining it to\n * these two helpers — against the row interfaces declared above — keeps\n * every call site typed and leaves exactly one place to review.\n */\n /* eslint-disable @typescript-eslint/no-unnecessary-type-parameters --\n T appears only in the return type because these helpers *are* the cast.\n That is the point: one reviewable place where PostgREST's `any` becomes\n one of the row interfaces above. */\n function unwrap<T>(result: {\n data: unknown;\n error: { message: string } | null;\n }): T {\n if (result.error) throw new Error(`supabase: ${result.error.message}`);\n if (result.data === null || result.data === undefined) {\n throw new Error(\"supabase: no data returned\");\n }\n return result.data as T;\n }\n\n /** Same, but a missing row is a legitimate answer rather than an error. */\n function unwrapMaybe<T>(result: {\n data: unknown;\n error: { message: string } | null;\n }): T | null {\n if (result.error) throw new Error(`supabase: ${result.error.message}`);\n return (result.data ?? null) as T | null;\n }\n /* eslint-enable @typescript-eslint/no-unnecessary-type-parameters */\n\n return {\n // -- jobs ---------------------------------------------------------------\n\n async create(input: StoredJobInput, now: number): Promise<JobRecord> {\n const dependsOn = [...(input.dependsOn ?? [])];\n\n // A job with dependencies starts blocked; the trigger sets\n // `claimable_at` when the last one reaches `ok`, which is where its TTL\n // clock starts.\n let claimableAt: string | null = iso(now);\n if (dependsOn.length > 0) {\n const deps = unwrap<JobRow[]>(\n await db.from(\"byollm_jobs\").select(\"id,state\").in(\"id\", dependsOn),\n ) as { id: string; state: JobState }[];\n const allDone =\n deps.length === dependsOn.length &&\n deps.every((dep) => dep.state === \"ok\");\n claimableAt = allDone ? iso(now) : null;\n }\n\n const row = {\n id: input.id,\n kind: input.kind,\n envelope: input.envelope,\n size_class: input.sizeClass,\n audience: input.audience ?? \"self\",\n owner: input.owner,\n audience_allow: input.audienceAllow ? [...input.audienceAllow] : null,\n depends_on: dependsOn,\n claimable_at: claimableAt,\n ttl_ms: input.ttlMs ?? defaultTtlMs,\n deadline_at:\n input.deadlineAt === undefined ? null : iso(input.deadlineAt),\n };\n\n // Idempotent by caller-supplied id, matching the reference store: an\n // app's retry must not duplicate work.\n const inserted = unwrapMaybe<JobRow>(\n await db\n .from(\"byollm_jobs\")\n .upsert(row, { onConflict: \"id\", ignoreDuplicates: true })\n .select()\n .maybeSingle(),\n );\n\n if (inserted) return toJob(inserted);\n const existing = unwrap<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", input.id).single(),\n );\n return toJob(existing);\n },\n\n async get(jobId: string): Promise<JobRecord | null> {\n const row = unwrapMaybe<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", jobId).maybeSingle(),\n );\n return row === null ? null : toJob(row);\n },\n\n async claim(args: ClaimArgs): Promise<JobRecord[]> {\n // One RPC, one transaction, `FOR UPDATE SKIP LOCKED` inside\n // ({@link MUSTS.CLAIM_ATOMIC}).\n const rows = unwrap<JobRow[]>(\n await db.rpc(\"byollm_claim_jobs\", {\n p_runner_id: args.runnerId,\n p_capabilities: args.capabilities,\n p_max: args.max,\n p_lease_ms: args.leaseMs,\n }),\n );\n return rows.map(toJob);\n },\n\n async renewLeases(args: RenewArgs): Promise<RenewResult> {\n await db.rpc(\"byollm_expire_due\");\n if (args.leases.length === 0) return { renewed: [], lost: [] };\n\n const expiresAt = iso(args.now + args.leaseMs);\n const renewedRows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({\n state: \"running\",\n lease_expires_at: expiresAt,\n updated_at: iso(args.now),\n })\n .eq(\"lease_runner\", args.runnerId)\n .or(leasePairs(args.leases))\n .in(\"state\", [\"claimed\", \"running\"])\n .select(\"id\"),\n );\n\n const renewedIds = new Set(renewedRows.map((row) => row.id));\n return {\n renewed: renewedRows.map((row) => ({\n jobId: row.id,\n expiresAt: args.now + args.leaseMs,\n })),\n // Anything the runner thinks it holds but did not renew is gone.\n lost: args.leases\n .map((l) => l.jobId)\n .filter((id) => !renewedIds.has(id)),\n };\n },\n\n async adopt(args: AdoptArgs): Promise<JobRecord | null> {\n // The predicates are the guard, evaluated in the database rather than\n // read-then-written here: `state in (queued, claimed)` is what makes\n // adopting a terminal or expired job impossible under concurrency.\n const rows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({\n state: \"claimed\",\n lease_id: args.leaseId,\n // Left null on purpose: `lease_runner` is a foreign key into\n // `byollm_runners`, and a relayed device has no row there. See\n // AdoptArgs — the site records the grant, not a machine it has\n // no relationship with.\n lease_runner: null,\n lease_expires_at: iso(args.expiresAt),\n updated_at: iso(args.now),\n })\n .eq(\"id\", args.jobId)\n .in(\"state\", [\"queued\", \"claimed\"])\n .select(),\n );\n const written = rows[0];\n return written === undefined ? null : toJob(written);\n },\n\n async complete(args: CompleteArgs): Promise<CompleteResult> {\n const state: JobState =\n args.outcome.outcome === \"ok\"\n ? \"ok\"\n : args.outcome.outcome === \"canceled\"\n ? \"canceled\"\n : \"error\";\n\n // The `in('state', ...)` predicate is the idempotency guard: a job that\n // already reached a terminal state matches nothing, so the first\n // outcome wins ({@link MUSTS.RESULT_IDEMPOTENT}). The `lease_runner`\n // predicate is {@link MUSTS.LEASE_HONORED}.\n // The `in('state', ...)` predicate is the idempotency guard. The\n // second predicate is LEASE_HONORED, and which column carries it\n // depends on the plane: a direct runner is named by id, a relayed\n // grant only by its lease. Built as a query rather than branched into\n // two, so there is one update statement and no chance of the two\n // drifting.\n let update = db\n .from(\"byollm_jobs\")\n .update({\n state,\n lease_runner: null,\n lease_expires_at: null,\n outcome: args.outcome,\n provenance: args.provenance,\n updated_at: iso(args.now),\n })\n .eq(\"id\", args.jobId)\n .in(\"state\", [\"claimed\", \"running\"]);\n update =\n args.holder.by === \"runner\"\n ? update.eq(\"lease_runner\", args.holder.runnerId)\n : update.eq(\"lease_id\", args.holder.leaseId);\n const rows = unwrap<JobRow[]>(await update.select());\n\n const written = rows[0];\n if (written === undefined) {\n const current = unwrapMaybe<JobRow>(\n await db\n .from(\"byollm_jobs\")\n .select()\n .eq(\"id\", args.jobId)\n .maybeSingle(),\n );\n return {\n accepted: false,\n job: current === null ? null : toJob(current),\n };\n }\n return { accepted: true, job: toJob(written) };\n },\n\n async release(args: ReleaseArgs): Promise<string[]> {\n if (args.leases.length === 0) return [];\n\n const held = unwrap<{ id: string; refused_by: string[] }[]>(\n await db\n .from(\"byollm_jobs\")\n .select(\"id,refused_by\")\n .eq(\"lease_runner\", args.runnerId)\n .or(leasePairs(args.leases)),\n );\n\n const released: string[] = [];\n for (const row of held) {\n const refusedBy =\n args.reason === \"refused\"\n ? [...new Set([...row.refused_by, args.runnerId])]\n : row.refused_by;\n\n const { error } = await db\n .from(\"byollm_jobs\")\n .update({\n state: \"queued\",\n lease_id: null,\n lease_runner: null,\n lease_expires_at: null,\n // Newly available again, so the TTL clock restarts.\n claimable_at: iso(args.now),\n refused_by: refusedBy,\n updated_at: iso(args.now),\n })\n .eq(\"id\", row.id)\n .eq(\"lease_runner\", args.runnerId);\n if (error) throw new Error(`supabase: ${error.message}`);\n released.push(row.id);\n }\n return released;\n },\n\n async expireDue(_now: number): Promise<JobRecord[]> {\n // The sweep is a single idempotent SQL function; it reports a count\n // rather than rows, and the caller only needs to know it ran.\n const { error } = await db.rpc(\"byollm_expire_due\");\n if (error) throw new Error(`supabase: ${error.message}`);\n return [];\n },\n\n async cancel(jobId: string, now: number): Promise<JobRecord | null> {\n const current = unwrapMaybe<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", jobId).maybeSingle(),\n );\n if (current === null) return null;\n\n if (current.state === \"queued\") {\n const rows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({ state: \"canceled\", updated_at: iso(now) })\n .eq(\"id\", jobId)\n .eq(\"state\", \"queued\")\n .select(),\n );\n const canceled = rows[0];\n return toJob(canceled ?? current);\n }\n\n if (current.state === \"claimed\" || current.state === \"running\") {\n // Held by a runner: the cancel travels on the next heartbeat and the\n // runner reports `canceled` itself.\n const { error } = await db\n .from(\"byollm_job_cancels\")\n .upsert({ job_id: jobId, requested_at: iso(now) });\n if (error) throw new Error(`supabase: ${error.message}`);\n }\n return toJob(current);\n },\n\n async listClaimedBy(runnerId: string): Promise<JobRecord[]> {\n const rows = unwrap<JobRow[]>(\n await db.from(\"byollm_jobs\").select().eq(\"lease_runner\", runnerId),\n );\n return rows.map(toJob);\n },\n\n async listCancelRequests(runnerId: string): Promise<string[]> {\n const rows = unwrap<{ job_id: string }[]>(\n await db\n .from(\"byollm_job_cancels\")\n .select(\"job_id, byollm_jobs!inner(lease_runner)\")\n .eq(\"byollm_jobs.lease_runner\", runnerId),\n ) as { job_id: string }[];\n return rows.map((row) => row.job_id);\n },\n\n // -- pairing and runners -------------------------------------------------\n\n /**\n * The push seam (byollm_009 §8.3), over Postgres Realtime.\n *\n * Native here, which is the point of requiring it of every adapter: the\n * backend that can push does, the one that cannot polls, and the\n * interface does not change again when streaming arrives.\n */\n subscribe(jobId: string, onChange: () => void): () => void {\n const channel = db\n .channel(`byollm_job_${jobId}`)\n .on(\n \"postgres_changes\",\n {\n event: \"UPDATE\",\n schema: \"public\",\n table: \"byollm_jobs\",\n filter: `id=eq.${jobId}`,\n },\n () => {\n onChange();\n },\n )\n .subscribe();\n\n let live = true;\n return () => {\n if (!live) return;\n live = false;\n // `removeChannel` is async and nothing awaits an unsubscribe, so the\n // rejection is routed rather than dropped — an unhandled one here\n // would end the process (see the Realtime delivery channel).\n void db.removeChannel(channel).catch(() => undefined);\n };\n },\n\n async createPairing(record: PairingRecord): Promise<void> {\n const { error } = await db.from(\"byollm_pairings\").insert({\n device_code_hash: record.deviceCodeHash,\n device: record.device,\n user_code: record.userCode,\n state: record.state,\n label: record.label,\n platform: record.platform,\n daemon_version: record.daemonVersion,\n capabilities: record.capabilities,\n expires_at: iso(record.expiresAt),\n });\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async getPairingByDeviceCodeHash(\n hash: string,\n ): Promise<PairingRecord | null> {\n const row = unwrapMaybe<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"device_code_hash\", hash)\n .maybeSingle(),\n );\n return row === null ? null : toPairing(row);\n },\n\n async getPairingByUserCode(\n userCode: string,\n ): Promise<PairingRecord | null> {\n const row = unwrapMaybe<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"user_code\", userCode)\n .maybeSingle(),\n );\n return row === null ? null : toPairing(row);\n },\n\n async approvePairing(args: ApproveArgs): Promise<RunnerRecord> {\n // Deliberately *not* the browser RPC: this path runs under the service\n // role with an `owner` the caller has already authenticated. Apps using\n // Supabase Auth in the browser should call `byollm_approve_pairing`\n // instead, which takes the owner from `auth.uid()` and cannot be told\n // who the user is.\n const pairing = unwrap<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"user_code\", args.userCode)\n .single(),\n );\n\n if (Date.parse(pairing.expires_at) <= args.now) {\n throw new Error(\"pairing code has expired\");\n }\n if (pairing.state !== \"pending\") {\n throw new Error(`pairing is already ${pairing.state}`);\n }\n\n const runner = unwrap<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .insert({\n owner: args.owner,\n token_hash: args.tokenHash,\n label: pairing.label,\n platform: pairing.platform,\n daemon_version: pairing.daemon_version,\n capabilities: pairing.capabilities,\n // Carried from the pairing, exactly as the SQL RPC does. There\n // are two approval paths — this service-role one and\n // `byollm_approve_pairing` for browser callers — and a field\n // added to one and not the other produces a runner that is\n // correct through one door and broken through the other.\n device: pairing.device,\n })\n .select()\n .single(),\n );\n\n const { error } = await db\n .from(\"byollm_pairings\")\n .update({\n state: \"approved\",\n owner: args.owner,\n runner_id: runner.id,\n runner_token_once: args.runnerToken,\n })\n .eq(\"device_code_hash\", pairing.device_code_hash);\n if (error) throw new Error(`supabase: ${error.message}`);\n\n return toRunner(runner);\n },\n\n async denyPairing(userCode: string, _now: number): Promise<void> {\n const { error } = await db\n .from(\"byollm_pairings\")\n .update({ state: \"denied\" })\n .eq(\"user_code\", userCode);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async consumePairingToken(deviceCodeHash: string): Promise<void> {\n const { error } = await db\n .from(\"byollm_pairings\")\n .update({ runner_token_once: null })\n .eq(\"device_code_hash\", deviceCodeHash);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async getRunnerByTokenHash(hash: string): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .select()\n .eq(\"token_hash\", hash)\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\n },\n\n async getRunner(runnerId: string): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .select()\n .eq(\"id\", runnerId)\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\n },\n\n async touchRunner(args: TouchArgs): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .update({\n capabilities: args.capabilities,\n daemon_version: args.daemonVersion,\n paused: args.paused,\n last_heartbeat_at: iso(args.now),\n })\n .eq(\"id\", args.runnerId)\n .select()\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\n },\n\n async revokeRunner(runnerId: string, now: number): Promise<void> {\n // `is('revoked_at', null)` keeps revocation one-way: an already-revoked\n // runner keeps its first revocation time.\n const { error } = await db\n .from(\"byollm_runners\")\n .update({ revoked_at: iso(now) })\n .eq(\"id\", runnerId)\n .is(\"revoked_at\", null);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async listRunners(owner?: string): Promise<RunnerRecord[]> {\n const query = db.from(\"byollm_runners\").select();\n const rows = unwrap<RunnerRow[]>(\n owner === undefined ? await query : await query.eq(\"owner\", owner),\n );\n return rows.map(toRunner);\n },\n };\n}\n\nexport { supabaseRealtimeDelivery } from \"./realtime.js\";\n"],"mappings":"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","names":["iso"]}
|
|
1
|
+
{"version":3,"sources":["../../src/supabase/realtime.ts","../../src/supabase/index.ts"],"sourcesContent":["import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { DeliveredResult } from \"@byollm/protocol\";\nimport {\n NoRunnerAvailableError,\n ResultTimeoutError,\n type PollingDeliveryDeps,\n type ResultDelivery,\n type WaitOptions,\n labelFallback,\n} from \"../delivery.js\";\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60_000;\n/**\n * How long a sustained no-runner signal must persist before it is believed.\n * A daemon restarting must not fail every job in flight.\n */\nconst NO_RUNNER_GRACE_MS = 10_000;\n\n/**\n * Realtime delivery: the app learns a job finished when Postgres says so.\n *\n * byollm_003 Rev 1 requires the server→app path be an explicit channel rather\n * than an implied in-request `await`. Polling is the portable default;\n * this is the one worth having when the app is already on Supabase, because\n * a result arrives in milliseconds instead of on the next poll tick.\n *\n * The no-runner watch still polls, deliberately: runner liveness is a\n * *derived* signal (nobody with matching capability has heartbeated lately),\n * and there is no row change to subscribe to for \"something stopped\n * happening\".\n */\nexport function supabaseRealtimeDelivery(\n client: SupabaseClient,\n): (deps: PollingDeliveryDeps) => ResultDelivery {\n return (deps) => new SupabaseRealtimeDelivery(client, deps);\n}\n\nclass SupabaseRealtimeDelivery implements ResultDelivery {\n readonly #client: SupabaseClient;\n readonly #deps: PollingDeliveryDeps;\n\n constructor(client: SupabaseClient, deps: PollingDeliveryDeps) {\n this.#client = client;\n this.#deps = deps;\n }\n\n async waitFor(\n jobId: string,\n options: WaitOptions = {},\n ): Promise<DeliveredResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n // Read first. The job may already be terminal, and subscribing to a\n // channel for an event that has already happened waits forever.\n const current = await this.#deps.read(jobId);\n if (current && isTerminal(current.state)) return current;\n\n // Declared before the subscription so the channel callback closes over a\n // `settled` that already exists. Every async path below routes its failure\n // here: a rejection that escapes this object becomes an unhandled\n // rejection, and an unhandled rejection ends the process.\n const settled = Promise.withResolvers<DeliveredResult>();\n // Keyed by job, not held as one field — see `#resolvers`.\n this.#resolvers.set(jobId, settled.resolve);\n\n const channel = this.#client.channel(`byollm_job_${jobId}`).on(\n \"postgres_changes\",\n {\n event: \"UPDATE\",\n schema: \"public\",\n table: \"byollm_jobs\",\n filter: `id=eq.${jobId}`,\n },\n () => {\n this.#check(jobId).catch(settled.reject);\n },\n );\n\n // `subscribe()` returns the channel, not a promise — awaiting it would be\n // a no-op that reads as if it waited for the subscription to be live.\n channel.subscribe();\n\n // A second read after subscribing closes the race where the job finished\n // between the first read and the subscription taking effect.\n this.#check(jobId).catch(settled.reject);\n\n const timer = setTimeout(() => {\n settled.reject(new ResultTimeoutError(jobId, timeoutMs));\n }, timeoutMs);\n\n const watcher = this.#watchAvailability(jobId, options, settled);\n const abort = (): void => {\n settled.reject(new Error(\"wait aborted\"));\n };\n options.signal?.addEventListener(\"abort\", abort, { once: true });\n\n try {\n return await settled.promise;\n } finally {\n clearTimeout(timer);\n clearInterval(watcher);\n this.#resolvers.delete(jobId);\n options.signal?.removeEventListener(\"abort\", abort);\n await this.#client.removeChannel(channel);\n }\n }\n\n /**\n * One resolver per job in flight — P0, 2026-09-02.\n *\n * This was a single field, `#resolve`, assigned by every `waitFor`. One\n * delivery object serves a whole app, so two concurrent waits meant the\n * second assignment clobbered the first, and then:\n *\n * 1. `waitFor(A)` sets the resolver.\n * 2. `waitFor(B)` overwrites it.\n * 3. A's row event arrives, `#check(A)` reads A's result — and resolves\n * **B's** promise with it.\n * 4. A never resolves and waits out its timeout.\n *\n * So an app awaiting two jobs at once got one answer under the wrong job\n * id, with the wrong text, silently — and a spurious timeout beside it. No\n * error anywhere; the failure is that the caller believes it.\n *\n * A map, and the entry is removed in the same `finally` that tears down the\n * channel. A resolver that outlived its wait would be a leak that also\n * resolves a promise nobody is holding.\n */\n readonly #resolvers = new Map<string, (result: DeliveredResult) => void>();\n\n async #check(jobId: string): Promise<void> {\n const current = await this.#deps.read(jobId);\n // The job's own resolver. `#check` has always taken a `jobId` and read\n // the right row; what it did with the answer was the bug.\n if (current && isTerminal(current.state)) {\n this.#resolvers.get(jobId)?.(current);\n }\n }\n\n /** Poll runner liveness; there is no row event for \"nothing is happening\". */\n #watchAvailability(\n jobId: string,\n options: WaitOptions,\n settled: PromiseWithResolvers<DeliveredResult>,\n ): NodeJS.Timeout {\n let noRunnerSince: number | null = null;\n\n return setInterval(() => {\n // `.catch`, not `void`. Two things in here can reject — the store read\n // and the caller's own `onNoRunner` — and discarding either made a\n // transient store error, or an app whose fallback throws, terminate the\n // process. The caller is awaiting `result()`; that is where a failure\n // belongs, and it is what the polling channel already does by virtue of\n // running inside the awaited chain. A delivery adapter must not change\n // what a failure means.\n (async () => {\n // No instrument, no question — see `PollingDeliveryDeps`. On the\n // cloud lane `runnerAvailability` refuses rather than reporting a\n // zero it cannot see, and this timer used to reject the caller's\n // `result()` with that refusal every two seconds.\n const availability = await this.#deps.availability?.(jobId);\n if (\n availability === undefined ||\n availability.available ||\n availability.blocked\n ) {\n noRunnerSince = null;\n return;\n }\n noRunnerSince ??= Date.now();\n if (Date.now() - noRunnerSince < NO_RUNNER_GRACE_MS) return;\n\n const reason = availability.reason ?? \"no-runner-online\";\n const substitute = await options.onNoRunner?.(reason);\n if (substitute !== undefined) {\n // The same labelling the polling channel applies, from the same\n // function — {@link MUSTS.FALLBACK_LABELED} cannot depend on which\n // store an app happened to choose.\n settled.resolve(labelFallback(jobId, substitute));\n } else {\n settled.reject(new NoRunnerAvailableError(jobId, reason));\n }\n })().catch(settled.reject);\n }, 2_000);\n }\n}\n\nfunction isTerminal(state: string): boolean {\n return (\n state === \"ok\" ||\n state === \"error\" ||\n state === \"canceled\" ||\n state === \"expired\"\n );\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type {\n Audience,\n Capability,\n JobOutcome,\n JobState,\n PublicIdentity,\n} from \"@byollm/protocol\";\nimport type {\n StoredJobInput,\n JobRecord,\n PairingRecord,\n RunnerRecord,\n} from \"../records.js\";\nimport type {\n ApproveArgs,\n ByollmStore,\n ClaimArgs,\n AdoptArgs,\n CompleteArgs,\n CompleteResult,\n LeaseRef,\n ReleaseArgs,\n RenewArgs,\n RenewResult,\n TouchArgs,\n} from \"../store.js\";\n\n/**\n * `@byollm/server/supabase` — the first-party Supabase adapter.\n *\n * The piece the of-tomorrow-framework's runner module consumes verbatim.\n * Migrations ship in `supabase/migrations`; the atomic claim lives in a\n * `security definer` RPC using `FOR UPDATE SKIP LOCKED`, and the audience\n * rules are mirrored in SQL so the server refuses independently of the daemon\n * (byollm_003 §Server-side MUSTs).\n *\n * Requires the **service role** key: a runner authenticates with a bearer\n * token of its own, which is not a Supabase session, so the protocol handler\n * cannot run under RLS as the runner's user. RLS still governs everything the\n * *browser* does — the app-side policies in the migration are what protect\n * one user's jobs from another.\n *\n * @packageDocumentation\n */\n\n/** Row shape of `byollm_jobs`. */\ninterface JobRow {\n id: string;\n kind: string;\n envelope: unknown;\n size_class: \"small\" | \"medium\" | \"large\" | \"unbounded\";\n /**\n * Typed as {@link Audience} rather than re-spelled, after a spelling of it\n * here outlived the enum by a week.\n *\n * `public` was removed on 2026-08-26 and this column may still hold it in a\n * database written before then. Nothing in this adapter validates a row —\n * `kind` and `envelope` are both plain casts — so this is a contract with\n * the schema, not a check, and a legacy row is a **migration** obligation\n * rather than a runtime one. Recorded so the migration is written on\n * purpose: a `public` row must be resolved by the deploy, never quietly\n * reinterpreted here as something narrower.\n */\n audience: Audience;\n /** byollm_016 Amendment L. Null for every job that named no purpose. */\n purpose: string | null;\n owner: string;\n audience_allow: string[] | null;\n depends_on: string[];\n state: JobState;\n lease_id: string | null;\n lease_runner: string | null;\n completed_by_lease_id: string | null;\n lease_expires_at: string | null;\n claimable_at: string | null;\n ttl_ms: number;\n deadline_at: string | null;\n refused_by: string[];\n attempts: number;\n outcome: JobOutcome | null;\n provenance: JobRecord[\"provenance\"];\n created_at: string;\n updated_at: string;\n}\n\n/** Row shape of `byollm_runners`. */\ninterface RunnerRow {\n id: string;\n owner: string;\n label: string;\n platform: \"darwin\" | \"linux\" | \"win32\";\n daemon_version: string;\n capabilities: Capability[];\n device: PublicIdentity;\n paused: boolean;\n revoked_at: string | null;\n last_heartbeat_at: string;\n created_at: string;\n}\n\n/** Row shape of `byollm_pairings`. */\ninterface PairingRow {\n device_code_hash: string;\n user_code: string;\n state: \"pending\" | \"approved\" | \"denied\";\n owner: string | null;\n runner_id: string | null;\n collected_at: string | null;\n label: string;\n platform: \"darwin\" | \"linux\" | \"win32\";\n daemon_version: string;\n capabilities: Capability[];\n device: PublicIdentity;\n expires_at: string;\n created_at: string;\n}\n\nconst ms = (iso: string | null): number | null =>\n iso === null ? null : Date.parse(iso);\n\nconst iso = (epochMs: number): string => new Date(epochMs).toISOString();\n\nfunction toJob(row: JobRow): JobRecord {\n const leaseExpires = ms(row.lease_expires_at);\n return {\n id: row.id,\n kind: row.kind as JobRecord[\"kind\"],\n envelope: row.envelope as JobRecord[\"envelope\"],\n sizeClass: row.size_class,\n audience: row.audience,\n purpose: row.purpose ?? undefined,\n owner: row.owner,\n audienceAllow: row.audience_allow ?? undefined,\n dependsOn: row.depends_on,\n state: row.state,\n completedByLeaseId: row.completed_by_lease_id ?? null,\n lease:\n // Keyed on the lease id, not the runner. A relayed grant has no runner\n // row to point at (see AdoptArgs), and reading the lease as absent\n // because `lease_runner` is null would make an actively-held job look\n // claimable — the exact bug `adopt` exists to prevent.\n leaseExpires !== null && row.lease_id !== null\n ? {\n id: row.lease_id,\n runnerId: row.lease_runner ?? \"\",\n expiresAt: leaseExpires,\n }\n : null,\n createdAt: Date.parse(row.created_at),\n claimableAt: ms(row.claimable_at),\n ttlMs: row.ttl_ms,\n deadlineAt: ms(row.deadline_at),\n refusedBy: row.refused_by,\n attempts: row.attempts,\n outcome: row.outcome,\n provenance: row.provenance,\n updatedAt: Date.parse(row.updated_at),\n };\n}\n\n/**\n * A PostgREST filter matching exactly these (job, lease) pairs.\n *\n * Not two `IN` lists: `id IN (…) AND lease_id IN (…)` is a cross product, and\n * while UUID uniqueness makes a mismatch improbable, \"improbable\" is not the\n * property a lease check should rest on. This says what it means.\n */\nconst leasePairs = (leases: readonly LeaseRef[]): string =>\n leases.map((l) => `and(id.eq.${l.jobId},lease_id.eq.${l.leaseId})`).join(\",\");\n\nfunction toRunner(row: RunnerRow): RunnerRecord {\n return {\n id: row.id,\n owner: row.owner,\n label: row.label,\n platform: row.platform,\n daemonVersion: row.daemon_version,\n capabilities: row.capabilities,\n device: row.device,\n paused: row.paused,\n revokedAt: ms(row.revoked_at),\n lastHeartbeatAt: Date.parse(row.last_heartbeat_at),\n createdAt: Date.parse(row.created_at),\n };\n}\n\nfunction toPairing(row: PairingRow): PairingRecord {\n return {\n deviceCodeHash: row.device_code_hash,\n userCode: row.user_code,\n state: row.state,\n owner: row.owner,\n runnerId: row.runner_id,\n // Collected when it has a timestamp. This was `runner_token_once ===\n // null` — a nulled token standing in for a fact about delivery, which is\n // one field doing two jobs (cloud_008 §2.4a).\n collected: row.collected_at !== null,\n label: row.label,\n platform: row.platform,\n daemonVersion: row.daemon_version,\n capabilities: row.capabilities,\n device: row.device,\n expiresAt: Date.parse(row.expires_at),\n createdAt: Date.parse(row.created_at),\n };\n}\n\nexport interface SupabaseStoreOptions {\n /** A client built with the **service role** key. */\n readonly client: SupabaseClient;\n /** Default TTL for a job once claimable. */\n readonly defaultTtlMs?: number;\n}\n\n/** Build the Supabase-backed store. */\nexport function supabaseStore(options: SupabaseStoreOptions): ByollmStore {\n const db = options.client;\n const defaultTtlMs = options.defaultTtlMs ?? 15 * 60_000;\n\n /**\n * Narrow one PostgREST response, or throw with the Postgres message.\n *\n * `supabase-js` types rows as `any` unless the project has generated\n * database types, so the assertion has to live somewhere. Confining it to\n * these two helpers — against the row interfaces declared above — keeps\n * every call site typed and leaves exactly one place to review.\n */\n /* eslint-disable @typescript-eslint/no-unnecessary-type-parameters --\n T appears only in the return type because these helpers *are* the cast.\n That is the point: one reviewable place where PostgREST's `any` becomes\n one of the row interfaces above. */\n function unwrap<T>(result: {\n data: unknown;\n error: { message: string } | null;\n }): T {\n if (result.error) throw new Error(`supabase: ${result.error.message}`);\n if (result.data === null || result.data === undefined) {\n throw new Error(\"supabase: no data returned\");\n }\n return result.data as T;\n }\n\n /** Same, but a missing row is a legitimate answer rather than an error. */\n function unwrapMaybe<T>(result: {\n data: unknown;\n error: { message: string } | null;\n }): T | null {\n if (result.error) throw new Error(`supabase: ${result.error.message}`);\n return (result.data ?? null) as T | null;\n }\n /* eslint-enable @typescript-eslint/no-unnecessary-type-parameters */\n\n return {\n // -- jobs ---------------------------------------------------------------\n\n async create(input: StoredJobInput, now: number): Promise<JobRecord> {\n const dependsOn = [...(input.dependsOn ?? [])];\n\n // A job with dependencies starts blocked; the trigger sets\n // `claimable_at` when the last one reaches `ok`, which is where its TTL\n // clock starts.\n let claimableAt: string | null = iso(now);\n if (dependsOn.length > 0) {\n const deps = unwrap<JobRow[]>(\n await db.from(\"byollm_jobs\").select(\"id,state\").in(\"id\", dependsOn),\n ) as { id: string; state: JobState }[];\n const allDone =\n deps.length === dependsOn.length &&\n deps.every((dep) => dep.state === \"ok\");\n claimableAt = allDone ? iso(now) : null;\n }\n\n const row = {\n id: input.id,\n kind: input.kind,\n envelope: input.envelope,\n size_class: input.sizeClass,\n audience: input.audience ?? \"private\",\n purpose: input.purpose ?? null,\n owner: input.owner,\n audience_allow: input.audienceAllow ? [...input.audienceAllow] : null,\n depends_on: dependsOn,\n claimable_at: claimableAt,\n ttl_ms: input.ttlMs ?? defaultTtlMs,\n deadline_at:\n input.deadlineAt === undefined ? null : iso(input.deadlineAt),\n };\n\n // Idempotent by caller-supplied id, matching the reference store: an\n // app's retry must not duplicate work.\n const inserted = unwrapMaybe<JobRow>(\n await db\n .from(\"byollm_jobs\")\n .upsert(row, { onConflict: \"id\", ignoreDuplicates: true })\n .select()\n .maybeSingle(),\n );\n\n if (inserted) return toJob(inserted);\n const existing = unwrap<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", input.id).single(),\n );\n return toJob(existing);\n },\n\n async get(jobId: string): Promise<JobRecord | null> {\n const row = unwrapMaybe<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", jobId).maybeSingle(),\n );\n return row === null ? null : toJob(row);\n },\n\n async claim(args: ClaimArgs): Promise<JobRecord[]> {\n // One RPC, one transaction, `FOR UPDATE SKIP LOCKED` inside\n // ({@link MUSTS.CLAIM_ATOMIC}).\n const rows = unwrap<JobRow[]>(\n await db.rpc(\"byollm_claim_jobs\", {\n p_runner_id: args.runnerId,\n p_capabilities: args.capabilities,\n p_max: args.max,\n p_lease_ms: args.leaseMs,\n }),\n );\n return rows.map(toJob);\n },\n\n async renewLeases(args: RenewArgs): Promise<RenewResult> {\n await db.rpc(\"byollm_expire_due\");\n if (args.leases.length === 0) return { renewed: [], lost: [] };\n\n const expiresAt = iso(args.now + args.leaseMs);\n const renewedRows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({\n state: \"running\",\n lease_expires_at: expiresAt,\n updated_at: iso(args.now),\n })\n .eq(\"lease_runner\", args.runnerId)\n .or(leasePairs(args.leases))\n .in(\"state\", [\"claimed\", \"running\"])\n .select(\"id\"),\n );\n\n const renewedIds = new Set(renewedRows.map((row) => row.id));\n return {\n renewed: renewedRows.map((row) => ({\n jobId: row.id,\n expiresAt: args.now + args.leaseMs,\n })),\n // Anything the runner thinks it holds but did not renew is gone,\n // named by the grant it asked about rather than by a bare id — V1-3.\n lost: args.leases.filter((lease) => !renewedIds.has(lease.jobId)),\n };\n },\n\n async adopt(args: AdoptArgs): Promise<JobRecord | null> {\n // The predicates are the guard, evaluated in the database rather than\n // read-then-written here: `state in (queued, claimed)` is what makes\n // adopting a terminal or expired job impossible under concurrency.\n const rows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({\n state: \"claimed\",\n lease_id: args.leaseId,\n // Left null on purpose: `lease_runner` is a foreign key into\n // `byollm_runners`, and a relayed device has no row there. See\n // AdoptArgs — the site records the grant, not a machine it has\n // no relationship with.\n lease_runner: null,\n lease_expires_at: iso(args.expiresAt),\n updated_at: iso(args.now),\n })\n .eq(\"id\", args.jobId)\n .in(\"state\", [\"queued\", \"claimed\"])\n .select(),\n );\n const written = rows[0];\n return written === undefined ? null : toJob(written);\n },\n\n async complete(args: CompleteArgs): Promise<CompleteResult> {\n const state: JobState =\n args.outcome.outcome === \"ok\"\n ? \"ok\"\n : args.outcome.outcome === \"canceled\"\n ? \"canceled\"\n : \"error\";\n\n // The `in('state', ...)` predicate is the idempotency guard: a job that\n // already reached a terminal state matches nothing, so the first\n // outcome wins ({@link MUSTS.RESULT_IDEMPOTENT}). The `lease_runner`\n // predicate is {@link MUSTS.LEASE_HONORED}.\n // The `in('state', ...)` predicate is the idempotency guard. The\n // second predicate is LEASE_HONORED, and which column carries it\n // depends on the plane: a direct runner is named by id, a relayed\n // grant only by its lease. Built as a query rather than branched into\n // two, so there is one update statement and no chance of the two\n // drifting.\n let update = db\n .from(\"byollm_jobs\")\n .update({\n state,\n lease_runner: null,\n lease_expires_at: null,\n // Which grant recorded it, kept after the lease is dropped — §3.6.\n completed_by_lease_id:\n args.holder.by === \"lease\" ? args.holder.leaseId : null,\n outcome: args.outcome,\n provenance: args.provenance,\n updated_at: iso(args.now),\n })\n .eq(\"id\", args.jobId)\n .in(\"state\", [\"claimed\", \"running\"]);\n update =\n args.holder.by === \"runner\"\n ? update.eq(\"lease_runner\", args.holder.runnerId)\n : update.eq(\"lease_id\", args.holder.leaseId);\n const rows = unwrap<JobRow[]>(await update.select());\n\n const written = rows[0];\n if (written === undefined) {\n const current = unwrapMaybe<JobRow>(\n await db\n .from(\"byollm_jobs\")\n .select()\n .eq(\"id\", args.jobId)\n .maybeSingle(),\n );\n const job = current === null ? null : toJob(current);\n // Terminal before holder — cloud_008 §3.6. The update above matched\n // nothing for one of two reasons, and the caller is owed the\n // difference: the device that already recorded this job hears\n // \"duplicate\", and anybody else hears the same refusal they would get\n // for a job that is not terminal, so a job id is not a terminality\n // probe.\n //\n // Decided on the row that is there rather than by a second predicate,\n // because the update is the atomic part and this is only a diagnosis\n // of why it matched nothing.\n const duplicate =\n job !== null &&\n job.provenance?.runnerId === args.runnerId &&\n (job.state === \"ok\" ||\n job.state === \"error\" ||\n job.state === \"canceled\") &&\n args.holder.by === \"lease\" &&\n job.completedByLeaseId !== null &&\n job.completedByLeaseId === args.holder.leaseId;\n return duplicate\n ? { accepted: false, duplicate: true, job }\n : { accepted: false, job };\n }\n return { accepted: true, job: toJob(written) };\n },\n\n async release(args: ReleaseArgs): Promise<string[]> {\n if (args.leases.length === 0) return [];\n\n const held = unwrap<{ id: string; refused_by: string[] }[]>(\n await db\n .from(\"byollm_jobs\")\n .select(\"id,refused_by\")\n .eq(\"lease_runner\", args.runnerId)\n .or(leasePairs(args.leases)),\n );\n\n const released: string[] = [];\n for (const row of held) {\n const refusedBy =\n args.reason === \"refused\"\n ? [...new Set([...row.refused_by, args.runnerId])]\n : row.refused_by;\n\n const { error } = await db\n .from(\"byollm_jobs\")\n .update({\n state: \"queued\",\n lease_id: null,\n lease_runner: null,\n lease_expires_at: null,\n // Newly available again, so the TTL clock restarts.\n claimable_at: iso(args.now),\n refused_by: refusedBy,\n updated_at: iso(args.now),\n })\n .eq(\"id\", row.id)\n .eq(\"lease_runner\", args.runnerId);\n if (error) throw new Error(`supabase: ${error.message}`);\n released.push(row.id);\n }\n return released;\n },\n\n async expireDue(_now: number): Promise<JobRecord[]> {\n // The sweep is a single idempotent SQL function; it reports a count\n // rather than rows, and the caller only needs to know it ran.\n const { error } = await db.rpc(\"byollm_expire_due\");\n if (error) throw new Error(`supabase: ${error.message}`);\n return [];\n },\n\n async cancel(jobId: string, now: number): Promise<JobRecord | null> {\n const current = unwrapMaybe<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", jobId).maybeSingle(),\n );\n if (current === null) return null;\n\n if (current.state === \"queued\") {\n const rows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({ state: \"canceled\", updated_at: iso(now) })\n .eq(\"id\", jobId)\n .eq(\"state\", \"queued\")\n .select(),\n );\n const canceled = rows[0];\n return toJob(canceled ?? current);\n }\n\n if (current.state === \"claimed\" || current.state === \"running\") {\n // Held by a runner: the cancel travels on the next heartbeat and the\n // runner reports `canceled` itself.\n const { error } = await db\n .from(\"byollm_job_cancels\")\n .upsert({ job_id: jobId, requested_at: iso(now) });\n if (error) throw new Error(`supabase: ${error.message}`);\n }\n return toJob(current);\n },\n\n async listClaimedBy(runnerId: string): Promise<JobRecord[]> {\n const rows = unwrap<JobRow[]>(\n await db.from(\"byollm_jobs\").select().eq(\"lease_runner\", runnerId),\n );\n return rows.map(toJob);\n },\n\n async listCancelRequests(runnerId: string): Promise<LeaseRef[]> {\n // The lease comes back with the row — V1-3. A bare job id is ambiguous\n // to a daemon serving two sites that chose the same one, and the lease\n // is already on the joined row.\n const rows = unwrap<{ job_id: string; byollm_jobs: unknown }[]>(\n await db\n .from(\"byollm_job_cancels\")\n .select(\"job_id, byollm_jobs!inner(lease_runner, lease_id)\")\n .eq(\"byollm_jobs.lease_runner\", runnerId),\n ) as {\n job_id: string;\n byollm_jobs: { lease_id: string | null };\n }[];\n return rows\n .filter((row) => row.byollm_jobs.lease_id !== null)\n .map((row) => ({\n jobId: row.job_id,\n leaseId: row.byollm_jobs.lease_id ?? \"\",\n }));\n },\n\n // -- pairing and runners -------------------------------------------------\n\n /**\n * The push seam (byollm_009 §8.3), over Postgres Realtime.\n *\n * Native here, which is the point of requiring it of every adapter: the\n * backend that can push does, the one that cannot polls, and the\n * interface does not change again when streaming arrives.\n */\n subscribe(jobId: string, onChange: () => void): () => void {\n const channel = db\n .channel(`byollm_job_${jobId}`)\n .on(\n \"postgres_changes\",\n {\n event: \"UPDATE\",\n schema: \"public\",\n table: \"byollm_jobs\",\n filter: `id=eq.${jobId}`,\n },\n () => {\n onChange();\n },\n )\n .subscribe();\n\n let live = true;\n return () => {\n if (!live) return;\n live = false;\n // `removeChannel` is async and nothing awaits an unsubscribe, so the\n // rejection is routed rather than dropped — an unhandled one here\n // would end the process (see the Realtime delivery channel).\n void db.removeChannel(channel).catch(() => undefined);\n };\n },\n\n async createPairing(record: PairingRecord): Promise<void> {\n const { error } = await db.from(\"byollm_pairings\").insert({\n device_code_hash: record.deviceCodeHash,\n device: record.device,\n user_code: record.userCode,\n state: record.state,\n label: record.label,\n platform: record.platform,\n daemon_version: record.daemonVersion,\n capabilities: record.capabilities,\n expires_at: iso(record.expiresAt),\n });\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async getPairingByDeviceCodeHash(\n hash: string,\n ): Promise<PairingRecord | null> {\n const row = unwrapMaybe<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"device_code_hash\", hash)\n .maybeSingle(),\n );\n return row === null ? null : toPairing(row);\n },\n\n async getPairingByUserCode(\n userCode: string,\n ): Promise<PairingRecord | null> {\n const row = unwrapMaybe<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"user_code\", userCode)\n .maybeSingle(),\n );\n return row === null ? null : toPairing(row);\n },\n\n async approvePairing(args: ApproveArgs): Promise<RunnerRecord> {\n // Deliberately *not* the browser RPC: this path runs under the service\n // role with an `owner` the caller has already authenticated. Apps using\n // Supabase Auth in the browser should call `byollm_approve_pairing`\n // instead, which takes the owner from `auth.uid()` and cannot be told\n // who the user is.\n const pairing = unwrap<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"user_code\", args.userCode)\n .single(),\n );\n\n if (Date.parse(pairing.expires_at) <= args.now) {\n throw new Error(\"pairing code has expired\");\n }\n if (pairing.state !== \"pending\") {\n throw new Error(`pairing is already ${pairing.state}`);\n }\n\n const runner = unwrap<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .insert({\n owner: args.owner,\n label: pairing.label,\n platform: pairing.platform,\n daemon_version: pairing.daemon_version,\n capabilities: pairing.capabilities,\n // Carried from the pairing, exactly as the SQL RPC does. There\n // are two approval paths — this service-role one and\n // `byollm_approve_pairing` for browser callers — and a field\n // added to one and not the other produces a runner that is\n // correct through one door and broken through the other.\n device: pairing.device,\n })\n .select()\n .single(),\n );\n\n const { error } = await db\n .from(\"byollm_pairings\")\n .update({\n state: \"approved\",\n owner: args.owner,\n runner_id: runner.id,\n // Marks the approval collectable — cloud_008 §2.4. The column held\n // a bearer token; it now holds a marker, and the next migration\n // renames it. Written as a constant rather than left null because\n // `collected` reads `=== null`, and a schema change and a code\n // change landing in one step is how a rollback strands rows.\n collected_at: null,\n })\n .eq(\"device_code_hash\", pairing.device_code_hash);\n if (error) throw new Error(`supabase: ${error.message}`);\n\n return toRunner(runner);\n },\n\n async denyPairing(userCode: string, _now: number): Promise<void> {\n const { error } = await db\n .from(\"byollm_pairings\")\n .update({ state: \"denied\" })\n .eq(\"user_code\", userCode);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async consumePairingToken(deviceCodeHash: string): Promise<void> {\n const { error } = await db\n .from(\"byollm_pairings\")\n // The database's clock, not this process's — the same rule the\n // relay's lease stamps follow: two writers measuring one fact against\n // two clocks is how a \"collected\" row looks uncollected.\n .update({ collected_at: \"now()\" })\n .eq(\"device_code_hash\", deviceCodeHash);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async getRunner(runnerId: string): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .select()\n .eq(\"id\", runnerId)\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\n },\n\n async touchRunner(args: TouchArgs): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .update({\n capabilities: args.capabilities,\n daemon_version: args.daemonVersion,\n paused: args.paused,\n last_heartbeat_at: iso(args.now),\n })\n .eq(\"id\", args.runnerId)\n .select()\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\n },\n\n async revokeRunner(runnerId: string, now: number): Promise<void> {\n // `is('revoked_at', null)` keeps revocation one-way: an already-revoked\n // runner keeps its first revocation time.\n const { error } = await db\n .from(\"byollm_runners\")\n .update({ revoked_at: iso(now) })\n .eq(\"id\", runnerId)\n .is(\"revoked_at\", null);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async listRunners(owner?: string): Promise<RunnerRecord[]> {\n const query = db.from(\"byollm_runners\").select();\n const rows = unwrap<RunnerRow[]>(\n owner === undefined ? await query : await query.eq(\"owner\", owner),\n );\n return rows.map(toRunner);\n },\n };\n}\n\nexport { supabaseRealtimeDelivery } from \"./realtime.js\";\n"],"mappings":"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","names":["iso"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byollm/server",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.91",
|
|
4
4
|
"description": "Framework-agnostic BYOLLM protocol handlers, a reference in-memory store, a Next.js mount, and a Supabase adapter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"node": ">=22.14"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@byollm/protocol": "0.1.0-alpha.
|
|
33
|
+
"@byollm/protocol": "0.1.0-alpha.91"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@supabase/supabase-js": "^2.58.0"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- The bearer token nobody used — cloud_008 §2.4, finding 37.
|
|
2
|
+
--
|
|
3
|
+
-- `byollm_runners.token_hash` held the SHA-256 of a token minted at pairing,
|
|
4
|
+
-- returned to the daemon, written to its pairings file, and then **never
|
|
5
|
+
-- sent, never looked up and never compared**. The only reader was
|
|
6
|
+
-- `getRunnerByTokenHash`, which both store adapters implemented and nothing
|
|
7
|
+
-- called except a test asserting it returns null.
|
|
8
|
+
--
|
|
9
|
+
-- That is not dead wire in the ordinary sense. It was a *secret*: minted,
|
|
10
|
+
-- transmitted once, and written to two disks at rest for no purpose. A
|
|
11
|
+
-- credential with no consumer cannot be used correctly and can still leak,
|
|
12
|
+
-- which makes it strictly a liability.
|
|
13
|
+
--
|
|
14
|
+
-- `REQUESTS_SIGNED_NOT_BEARER` was the rule the whole time and was enforced
|
|
15
|
+
-- the whole time: every authenticated call is signed by the device's pinned
|
|
16
|
+
-- identity key, and `C016` proves an endpoint refuses a bearer token. This
|
|
17
|
+
-- drops the thing the MUST is named after.
|
|
18
|
+
--
|
|
19
|
+
-- `byollm_pairings.runner_token_once` stays and now carries a marker rather
|
|
20
|
+
-- than a secret. It is the deliver-once flag — a replayed device code must
|
|
21
|
+
-- get nothing — a real property that was riding on the token's nullability.
|
|
22
|
+
-- Renaming a column and changing the code that reads it in one step is how a
|
|
23
|
+
-- rollback strands rows, so the rename waits for a later release.
|
|
24
|
+
|
|
25
|
+
alter table byollm_runners drop column if exists token_hash;
|
|
26
|
+
|
|
27
|
+
-- The browser-facing approval path takes one fewer argument. Replaced with
|
|
28
|
+
-- the old signature dropped rather than left beside it: two functions with
|
|
29
|
+
-- one name is how a caller ends up invoking the one nobody maintains.
|
|
30
|
+
--
|
|
31
|
+
-- Everything else is unchanged from the original, deliberately — the owner
|
|
32
|
+
-- still comes from `auth.uid()` because a daemon can never assert who it is,
|
|
33
|
+
-- which is the whole reason pairing is interactive.
|
|
34
|
+
drop function if exists byollm_approve_pairing(text, text);
|
|
35
|
+
|
|
36
|
+
create or replace function byollm_approve_pairing(
|
|
37
|
+
p_user_code text
|
|
38
|
+
)
|
|
39
|
+
returns byollm_runners
|
|
40
|
+
language plpgsql
|
|
41
|
+
security definer
|
|
42
|
+
set search_path = public
|
|
43
|
+
as $$
|
|
44
|
+
declare
|
|
45
|
+
v_pairing byollm_pairings;
|
|
46
|
+
v_runner byollm_runners;
|
|
47
|
+
v_owner uuid := (select auth.uid());
|
|
48
|
+
begin
|
|
49
|
+
if v_owner is null then
|
|
50
|
+
raise exception 'approving a pairing requires an authenticated user';
|
|
51
|
+
end if;
|
|
52
|
+
|
|
53
|
+
select * into v_pairing from byollm_pairings
|
|
54
|
+
where user_code = p_user_code for update;
|
|
55
|
+
|
|
56
|
+
if v_pairing is null then
|
|
57
|
+
raise exception 'unknown pairing code';
|
|
58
|
+
end if;
|
|
59
|
+
if v_pairing.expires_at <= now() then
|
|
60
|
+
raise exception 'pairing code has expired';
|
|
61
|
+
end if;
|
|
62
|
+
if v_pairing.state <> 'pending' then
|
|
63
|
+
raise exception 'pairing is already %', v_pairing.state;
|
|
64
|
+
end if;
|
|
65
|
+
|
|
66
|
+
insert into byollm_runners (owner, label, platform,
|
|
67
|
+
daemon_version, capabilities, device)
|
|
68
|
+
values (v_owner, v_pairing.label, v_pairing.platform,
|
|
69
|
+
v_pairing.daemon_version, v_pairing.capabilities, v_pairing.device)
|
|
70
|
+
returning * into v_runner;
|
|
71
|
+
|
|
72
|
+
-- The marker, not a token: this is what makes a replayed device code get
|
|
73
|
+
-- nothing. The service-role path in `supabase/index.ts` writes the same
|
|
74
|
+
-- value, and the two approval doors have to agree — a field set by one and
|
|
75
|
+
-- not the other produces a runner that is correct through one and broken
|
|
76
|
+
-- through the other.
|
|
77
|
+
update byollm_pairings
|
|
78
|
+
set state = 'approved',
|
|
79
|
+
owner = v_owner,
|
|
80
|
+
runner_id = v_runner.id,
|
|
81
|
+
runner_token_once = 'pending-collection'
|
|
82
|
+
where device_code_hash = v_pairing.device_code_hash;
|
|
83
|
+
|
|
84
|
+
return v_runner;
|
|
85
|
+
end;
|
|
86
|
+
$$;
|
|
87
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- Which grant recorded a result — cloud_008 §3.6.
|
|
2
|
+
--
|
|
3
|
+
-- `complete` now checks terminal state **before** the holder, so
|
|
4
|
+
-- `RESULT_IDEMPOTENT` is enforced by the branch named after it rather than as
|
|
5
|
+
-- a side effect of the lease being nulled on success. Answering a replay
|
|
6
|
+
-- correctly means knowing which grant recorded the result, and `lease_id` is
|
|
7
|
+
-- cleared at completion by design: "who holds this" and "who finished this"
|
|
8
|
+
-- are different questions with different lifetimes.
|
|
9
|
+
--
|
|
10
|
+
-- ## Why this is its own file
|
|
11
|
+
--
|
|
12
|
+
-- It was first appended to `20260819000000_drop_runner_token.sql`, which had
|
|
13
|
+
-- already shipped in alpha.19. An applied migration is immutable — that is the
|
|
14
|
+
-- whole reason a migrations folder can be trusted — and the fact that this one
|
|
15
|
+
-- is only days old and probably applied nowhere does not make editing it a
|
|
16
|
+
-- different act. The rule is worth more than the tidiness.
|
|
17
|
+
--
|
|
18
|
+
-- ## Nullable, and not backfilled
|
|
19
|
+
--
|
|
20
|
+
-- Rows completed before this migration have no recorded grant, so a replay of
|
|
21
|
+
-- one of them is answered as a plain refusal rather than as a duplicate. That
|
|
22
|
+
-- is the safe direction: it withholds a reassurance rather than inventing one,
|
|
23
|
+
-- and the daemons that produced those rows stopped retrying long ago.
|
|
24
|
+
alter table byollm_jobs
|
|
25
|
+
add column if not exists completed_by_lease_id text;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
-- The deliver-once flag stops pretending to be a token — cloud_008 §2.4a.
|
|
2
|
+
--
|
|
3
|
+
-- `20260819000000_drop_runner_token` removed the bearer credential and left
|
|
4
|
+
-- `byollm_pairings.runner_token_once` carrying a marker string, with the
|
|
5
|
+
-- reason written down: *"Renaming a column and changing the code that reads
|
|
6
|
+
-- it in one step is how a rollback strands rows, so the rename waits for a
|
|
7
|
+
-- later release."*
|
|
8
|
+
--
|
|
9
|
+
-- This is that release. The rename lands with the code that reads it, in one
|
|
10
|
+
-- step, because the transitional shape exists to protect a party who has not
|
|
11
|
+
-- agreed to change — and pre-1.0, with one deployment and one operator, that
|
|
12
|
+
-- party is us. Carrying a column named after a secret it no longer holds is
|
|
13
|
+
-- a comment that has to be re-read by everybody who meets it.
|
|
14
|
+
--
|
|
15
|
+
-- The property is unchanged and is the reason the column survives at all: a
|
|
16
|
+
-- replayed device code must get nothing, or a code left in a shell history is
|
|
17
|
+
-- a second pairing. That was riding on a token's nullability, which was one
|
|
18
|
+
-- field doing two jobs — and only one of them load-bearing.
|
|
19
|
+
|
|
20
|
+
alter table byollm_pairings
|
|
21
|
+
rename column runner_token_once to collected_at;
|
|
22
|
+
|
|
23
|
+
-- A timestamp rather than a marker string. `'pending-collection'` was the
|
|
24
|
+
-- shape a nulled token left behind; what the flag actually records is *when*
|
|
25
|
+
-- the approval was handed over, which is worth having when somebody asks why
|
|
26
|
+
-- a pairing did not complete.
|
|
27
|
+
alter table byollm_pairings
|
|
28
|
+
alter column collected_at type timestamptz
|
|
29
|
+
using case when collected_at is null then now() else null end;
|
|
30
|
+
|
|
31
|
+
comment on column byollm_pairings.collected_at is
|
|
32
|
+
'When the approval was collected. Null until then — a replayed device code '
|
|
33
|
+
'gets nothing. cloud_008 §2.4a; this was runner_token_once.';
|
|
34
|
+
|
|
35
|
+
-- The approval function moves with the column it writes. Recreated whole
|
|
36
|
+
-- rather than patched, because two functions with one name is how a caller
|
|
37
|
+
-- ends up invoking the one nobody maintains — the argument the migration
|
|
38
|
+
-- before this one made when it replaced the old signature.
|
|
39
|
+
|
|
40
|
+
create or replace function byollm_approve_pairing(
|
|
41
|
+
p_user_code text
|
|
42
|
+
)
|
|
43
|
+
returns byollm_runners
|
|
44
|
+
language plpgsql
|
|
45
|
+
security definer
|
|
46
|
+
set search_path = public
|
|
47
|
+
as $$
|
|
48
|
+
declare
|
|
49
|
+
v_pairing byollm_pairings;
|
|
50
|
+
v_runner byollm_runners;
|
|
51
|
+
v_owner uuid := (select auth.uid());
|
|
52
|
+
begin
|
|
53
|
+
if v_owner is null then
|
|
54
|
+
raise exception 'approving a pairing requires an authenticated user';
|
|
55
|
+
end if;
|
|
56
|
+
|
|
57
|
+
select * into v_pairing from byollm_pairings
|
|
58
|
+
where user_code = p_user_code for update;
|
|
59
|
+
|
|
60
|
+
if v_pairing is null then
|
|
61
|
+
raise exception 'unknown pairing code';
|
|
62
|
+
end if;
|
|
63
|
+
if v_pairing.expires_at <= now() then
|
|
64
|
+
raise exception 'pairing code has expired';
|
|
65
|
+
end if;
|
|
66
|
+
if v_pairing.state <> 'pending' then
|
|
67
|
+
raise exception 'pairing is already %', v_pairing.state;
|
|
68
|
+
end if;
|
|
69
|
+
|
|
70
|
+
insert into byollm_runners (owner, label, platform,
|
|
71
|
+
daemon_version, capabilities, device)
|
|
72
|
+
values (v_owner, v_pairing.label, v_pairing.platform,
|
|
73
|
+
v_pairing.daemon_version, v_pairing.capabilities, v_pairing.device)
|
|
74
|
+
returning * into v_runner;
|
|
75
|
+
|
|
76
|
+
-- The marker, not a token: this is what makes a replayed device code get
|
|
77
|
+
-- nothing. The service-role path in `supabase/index.ts` writes the same
|
|
78
|
+
-- value, and the two approval doors have to agree — a field set by one and
|
|
79
|
+
-- not the other produces a runner that is correct through one and broken
|
|
80
|
+
-- through the other.
|
|
81
|
+
update byollm_pairings
|
|
82
|
+
set state = 'approved',
|
|
83
|
+
owner = v_owner,
|
|
84
|
+
runner_id = v_runner.id,
|
|
85
|
+
collected_at = null
|
|
86
|
+
where device_code_hash = v_pairing.device_code_hash;
|
|
87
|
+
|
|
88
|
+
return v_runner;
|
|
89
|
+
end;
|
|
90
|
+
$$;
|
|
91
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
-- One sharing vocabulary, in the database too — byollm_016.
|
|
2
|
+
--
|
|
3
|
+
-- `private | team | public` replaced `self | named | public` everywhere else
|
|
4
|
+
-- in this release. The enums did not move with it, and the gap was invisible
|
|
5
|
+
-- to `pnpm run verify`: the TypeScript compiled, the unit suites passed
|
|
6
|
+
-- against the in-memory store, and only the freeze gate — which runs the same
|
|
7
|
+
-- code against real Postgres — refused the insert with `invalid input value
|
|
8
|
+
-- for enum byollm_audience: "private"`. A schema is not typechecked by the
|
|
9
|
+
-- language that talks to it.
|
|
10
|
+
--
|
|
11
|
+
-- Values are renamed rather than the enums recreated. `alter type ... rename
|
|
12
|
+
-- value` keeps every existing row's identity: the label changes, the stored
|
|
13
|
+
-- value does not move, and nothing has to be rewritten or backfilled. A
|
|
14
|
+
-- drop-and-recreate would need every column that uses the type dropped first,
|
|
15
|
+
-- which is how a rename turns into data loss.
|
|
16
|
+
--
|
|
17
|
+
-- Landing in one step, with the code that reads it, for the reason
|
|
18
|
+
-- `20260821000000_rename_collected` gives: transitional shapes exist to
|
|
19
|
+
-- protect a party who has not agreed to change, and pre-1.0 that party is us.
|
|
20
|
+
|
|
21
|
+
alter type byollm_audience rename value 'self' to 'private';
|
|
22
|
+
alter type byollm_audience rename value 'named' to 'team';
|
|
23
|
+
|
|
24
|
+
alter type byollm_offer_scope rename value 'self' to 'private';
|
|
25
|
+
alter type byollm_offer_scope rename value 'named' to 'team';
|
|
26
|
+
|
|
27
|
+
-- The column default is stored as a reference to the value, not as its text,
|
|
28
|
+
-- so the rename already carried it. Restated anyway: a default that reads
|
|
29
|
+
-- `'self'` in a dump nobody re-ran is exactly the kind of thing that gets
|
|
30
|
+
-- copied into the next schema.
|
|
31
|
+
alter table byollm_jobs
|
|
32
|
+
alter column audience set default 'private';
|
|
33
|
+
|
|
34
|
+
comment on column byollm_jobs.audience is
|
|
35
|
+
'Who this job may run for: private (the owner alone), team (the owner and '
|
|
36
|
+
'the named allowlist), public (anyone). byollm_016; these were self/named.';
|
|
37
|
+
|
|
38
|
+
-- Recreated whole rather than patched — two functions with one name is how a
|
|
39
|
+
-- caller ends up invoking the one nobody maintains. The logic is unchanged;
|
|
40
|
+
-- only the vocabulary moves.
|
|
41
|
+
create or replace function byollm_audience_admits(
|
|
42
|
+
p_job byollm_jobs,
|
|
43
|
+
p_runner_id uuid,
|
|
44
|
+
p_runner_owner uuid,
|
|
45
|
+
p_capabilities jsonb
|
|
46
|
+
)
|
|
47
|
+
returns boolean
|
|
48
|
+
language plpgsql
|
|
49
|
+
stable
|
|
50
|
+
security definer
|
|
51
|
+
set search_path = public
|
|
52
|
+
as $$
|
|
53
|
+
declare
|
|
54
|
+
v_cap jsonb;
|
|
55
|
+
v_scope text;
|
|
56
|
+
v_backend text;
|
|
57
|
+
v_same_owner boolean := (p_job.owner = p_runner_owner);
|
|
58
|
+
begin
|
|
59
|
+
select value into v_cap
|
|
60
|
+
from jsonb_array_elements(p_capabilities)
|
|
61
|
+
where value ->> 'kind' = p_job.kind
|
|
62
|
+
limit 1;
|
|
63
|
+
|
|
64
|
+
if v_cap is null then
|
|
65
|
+
return false;
|
|
66
|
+
end if;
|
|
67
|
+
|
|
68
|
+
v_scope := v_cap ->> 'offerScope';
|
|
69
|
+
v_backend := v_cap ->> 'backendId';
|
|
70
|
+
|
|
71
|
+
-- Side 1: does the job's audience admit this runner's owner?
|
|
72
|
+
if p_job.audience = 'private' and not v_same_owner then
|
|
73
|
+
return false;
|
|
74
|
+
end if;
|
|
75
|
+
if p_job.audience = 'team'
|
|
76
|
+
and not v_same_owner
|
|
77
|
+
and p_job.audience_allow is not null
|
|
78
|
+
and not (p_runner_owner = any (p_job.audience_allow)) then
|
|
79
|
+
return false;
|
|
80
|
+
end if;
|
|
81
|
+
|
|
82
|
+
-- A daemon always runs its own owner's work.
|
|
83
|
+
if v_same_owner then
|
|
84
|
+
return true;
|
|
85
|
+
end if;
|
|
86
|
+
|
|
87
|
+
-- The subscription self-lock. Applied here regardless of the scope the
|
|
88
|
+
-- daemon advertised: a widened scope on a subscription backend is refused,
|
|
89
|
+
-- not obeyed (SUBSCRIPTION_SELF_LOCK).
|
|
90
|
+
if v_backend = 'claude-cli' then
|
|
91
|
+
return false;
|
|
92
|
+
end if;
|
|
93
|
+
|
|
94
|
+
-- Side 2: does the backend's offer scope admit the job's owner?
|
|
95
|
+
if v_scope = 'private' then
|
|
96
|
+
return false;
|
|
97
|
+
end if;
|
|
98
|
+
if v_scope = 'team' then
|
|
99
|
+
-- The server cannot verify a remote daemon's local allowlist and must not
|
|
100
|
+
-- pretend to. It offers the job; the daemon refuses if its own list says
|
|
101
|
+
-- no, and the refusal is remembered in refused_by. In this build that
|
|
102
|
+
-- allowlist is the *only* enforcement — there is no central roster yet.
|
|
103
|
+
return true;
|
|
104
|
+
end if;
|
|
105
|
+
return v_scope = 'public';
|
|
106
|
+
end;
|
|
107
|
+
$$;
|
|
108
|
+
|
|
109
|
+
revoke all on function byollm_audience_admits(byollm_jobs, uuid, uuid, jsonb) from public;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- A job may name which service should answer it — byollm_016 Phase B.
|
|
2
|
+
--
|
|
3
|
+
-- Nullable with no default, and the null means something specific: this job
|
|
4
|
+
-- named nothing, so the device owner's default answers. That is every job
|
|
5
|
+
-- enqueued before this column existed, which is why there is no backfill —
|
|
6
|
+
-- the absent value already says the right thing about them.
|
|
7
|
+
--
|
|
8
|
+
-- A *key* rather than a value. It holds a name from the device owner's own
|
|
9
|
+
-- config, so it means nothing off that machine and resolves through their
|
|
10
|
+
-- config or resolves nowhere. No model, no base URL, no flags: the amended
|
|
11
|
+
-- NO_PAYLOAD_ROUTING permits selection and forbids description, and a column
|
|
12
|
+
-- that could hold `claude-opus-5` would be the wrong shape for that rule.
|
|
13
|
+
|
|
14
|
+
alter table byollm_jobs
|
|
15
|
+
add column if not exists service text;
|
|
16
|
+
|
|
17
|
+
comment on column byollm_jobs.service is
|
|
18
|
+
'Which of the device owner''s advertised services should answer. Null means '
|
|
19
|
+
'their default. A key from their config, never a model or URL — byollm_016 '
|
|
20
|
+
'Phase B, byollm_009 Amendment D.';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- A job names one of the *site's* declared purposes — byollm_016 Amendment L.
|
|
2
|
+
--
|
|
3
|
+
-- This replaces `service`, added two days ago, which named one of the device
|
|
4
|
+
-- owner's advertised services. That was safe because it was a key rather than
|
|
5
|
+
-- a value — a name from somebody's own config, meaningless off their machine.
|
|
6
|
+
-- Amendment L goes further: a site does not name the owner's things at all.
|
|
7
|
+
-- It declares purposes of its own, each person maps those purposes to their
|
|
8
|
+
-- own services on the consent screen, and a control plane joins the two when
|
|
9
|
+
-- it signs a grant.
|
|
10
|
+
--
|
|
11
|
+
-- So the vocabulary that crossed the boundary no longer does. The refusal
|
|
12
|
+
-- machinery that existed to stop a site probing service names — one collapsed
|
|
13
|
+
-- reason for "no such service" and "not offered to you" — retires with it,
|
|
14
|
+
-- because a name that never crosses cannot be enumerated across.
|
|
15
|
+
--
|
|
16
|
+
-- Renamed rather than dropped-and-added: the column's *shape* is unchanged
|
|
17
|
+
-- (nullable text, no default, null meaning "nothing named") and a rename
|
|
18
|
+
-- keeps every row's identity, its grants and its policies. What changes is
|
|
19
|
+
-- whose namespace the string belongs to.
|
|
20
|
+
--
|
|
21
|
+
-- **The values are not migrated, and that is deliberate.** Any row still
|
|
22
|
+
-- holding a service name holds a name from the wrong namespace: it is one of
|
|
23
|
+
-- the device owner's services, and this column now means one of the site's
|
|
24
|
+
-- purposes. There is no mapping between them that this migration could know.
|
|
25
|
+
-- Pre-1.0, nothing has shipped with either, and a null here reads as "named
|
|
26
|
+
-- nothing" — which is the truth about a job enqueued before purposes existed.
|
|
27
|
+
|
|
28
|
+
alter table byollm_jobs
|
|
29
|
+
rename column service to purpose;
|
|
30
|
+
|
|
31
|
+
update byollm_jobs set purpose = null where purpose is not null;
|
|
32
|
+
|
|
33
|
+
comment on column byollm_jobs.purpose is
|
|
34
|
+
'Which of the enqueuing site''s declared purposes this job serves. Null '
|
|
35
|
+
'means it named none. A key in the site''s own namespace — never a service, '
|
|
36
|
+
'model or URL — byollm_016 Amendment L.';
|