@byollm/server 0.1.0-alpha.0
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/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/chunk-7RKXFPBZ.js +72 -0
- package/dist/chunk-7RKXFPBZ.js.map +1 -0
- package/dist/chunk-HL6EYHQ7.js +422 -0
- package/dist/chunk-HL6EYHQ7.js.map +1 -0
- package/dist/delivery-36nIe-b3.d.ts +73 -0
- package/dist/handlers-D7lWfwno.d.ts +52 -0
- package/dist/index.d.ts +223 -0
- package/dist/index.js +634 -0
- package/dist/index.js.map +1 -0
- package/dist/next.d.ts +33 -0
- package/dist/next.js +17 -0
- package/dist/next.js.map +1 -0
- package/dist/store-D23N6iiP.d.ts +255 -0
- package/dist/supabase/index.d.ts +30 -0
- package/dist/supabase/index.js +414 -0
- package/dist/supabase/index.js.map +1 -0
- package/package.json +52 -0
- package/supabase/migrations/20260809000000_byollm_runner.sql +507 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
-- BYOLLM runner tables, RLS, and the atomic claim RPC.
|
|
2
|
+
--
|
|
3
|
+
-- Platform conventions from the of-tomorrow-framework's `the-system.md` apply:
|
|
4
|
+
-- RLS is the only permission system, every function ships explicit
|
|
5
|
+
-- REVOKE/GRANT (PostgREST exposes functions at /rest/v1/rpc/ by default), and
|
|
6
|
+
-- helpers are wrapped `(select ...)` for the initplan.
|
|
7
|
+
--
|
|
8
|
+
-- Tables are prefixed `byollm_*` so the app folder stays a portable unit.
|
|
9
|
+
|
|
10
|
+
-- ---------------------------------------------------------------------------
|
|
11
|
+
-- Enums
|
|
12
|
+
-- ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
create type byollm_job_state as enum (
|
|
15
|
+
'queued', 'claimed', 'running', 'ok', 'error', 'canceled', 'expired'
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
create type byollm_audience as enum ('self', 'named', 'public');
|
|
19
|
+
|
|
20
|
+
create type byollm_offer_scope as enum ('self', 'named', 'public');
|
|
21
|
+
|
|
22
|
+
-- ---------------------------------------------------------------------------
|
|
23
|
+
-- Runners
|
|
24
|
+
-- ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
create table byollm_runners (
|
|
27
|
+
id uuid primary key default gen_random_uuid(),
|
|
28
|
+
-- Exactly one user. This column is the whole of PAIR_ONE_USER.
|
|
29
|
+
owner uuid not null references auth.users (id) on delete cascade,
|
|
30
|
+
-- SHA-256 hex of the bearer token. The token itself is never stored.
|
|
31
|
+
token_hash text not null unique,
|
|
32
|
+
label text not null,
|
|
33
|
+
platform text not null check (platform in ('darwin', 'linux', 'win32')),
|
|
34
|
+
daemon_version text not null,
|
|
35
|
+
capabilities jsonb not null default '[]'::jsonb,
|
|
36
|
+
paused boolean not null default false,
|
|
37
|
+
-- Set once. A revoked runner never un-revokes.
|
|
38
|
+
revoked_at timestamptz,
|
|
39
|
+
last_heartbeat_at timestamptz not null default now(),
|
|
40
|
+
created_at timestamptz not null default now()
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
create index byollm_runners_owner_idx on byollm_runners (owner);
|
|
44
|
+
create index byollm_runners_live_idx
|
|
45
|
+
on byollm_runners (last_heartbeat_at)
|
|
46
|
+
where revoked_at is null and paused = false;
|
|
47
|
+
|
|
48
|
+
-- ---------------------------------------------------------------------------
|
|
49
|
+
-- Pairings (device-code flow)
|
|
50
|
+
-- ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
create table byollm_pairings (
|
|
53
|
+
device_code_hash text primary key,
|
|
54
|
+
-- Short code the user reads. Unique among live pairings.
|
|
55
|
+
user_code text not null unique,
|
|
56
|
+
state text not null default 'pending'
|
|
57
|
+
check (state in ('pending', 'approved', 'denied')),
|
|
58
|
+
owner uuid references auth.users (id) on delete cascade,
|
|
59
|
+
runner_id uuid references byollm_runners (id) on delete set null,
|
|
60
|
+
-- Held until the daemon's next poll collects it, then nulled. Delivered once.
|
|
61
|
+
runner_token_once text,
|
|
62
|
+
label text not null,
|
|
63
|
+
platform text not null,
|
|
64
|
+
daemon_version text not null,
|
|
65
|
+
capabilities jsonb not null default '[]'::jsonb,
|
|
66
|
+
expires_at timestamptz not null,
|
|
67
|
+
created_at timestamptz not null default now()
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
create index byollm_pairings_user_code_idx on byollm_pairings (user_code);
|
|
71
|
+
create index byollm_pairings_expiry_idx on byollm_pairings (expires_at);
|
|
72
|
+
|
|
73
|
+
-- ---------------------------------------------------------------------------
|
|
74
|
+
-- Jobs
|
|
75
|
+
-- ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
create table byollm_jobs (
|
|
78
|
+
id uuid primary key default gen_random_uuid(),
|
|
79
|
+
kind text not null,
|
|
80
|
+
payload jsonb not null,
|
|
81
|
+
audience byollm_audience not null default 'self',
|
|
82
|
+
owner uuid not null references auth.users (id) on delete cascade,
|
|
83
|
+
-- Server-side restriction on which runner owners may take a `named` job.
|
|
84
|
+
-- Defence in depth: the daemon's own local allowlist is the enforcing side.
|
|
85
|
+
audience_allow uuid[] ,
|
|
86
|
+
depends_on uuid[] not null default '{}',
|
|
87
|
+
state byollm_job_state not null default 'queued',
|
|
88
|
+
lease_runner uuid references byollm_runners (id) on delete set null,
|
|
89
|
+
lease_expires_at timestamptz,
|
|
90
|
+
-- When the job became claimable. THE TTL CLOCK STARTS HERE, not at
|
|
91
|
+
-- created_at: a dependent job must not expire for waiting on a slow
|
|
92
|
+
-- dependency, and a reclaimed job must not expire for time spent being
|
|
93
|
+
-- actively worked on. Null means still blocked on a dependency.
|
|
94
|
+
claimable_at timestamptz,
|
|
95
|
+
ttl_ms integer not null default 900000 check (ttl_ms > 0),
|
|
96
|
+
-- Absolute lifetime bound, unaffected by reclaim.
|
|
97
|
+
deadline_at timestamptz,
|
|
98
|
+
-- Runners that released this job with reason 'refused'.
|
|
99
|
+
refused_by uuid[] not null default '{}',
|
|
100
|
+
attempts integer not null default 0,
|
|
101
|
+
outcome jsonb,
|
|
102
|
+
provenance jsonb,
|
|
103
|
+
created_at timestamptz not null default now(),
|
|
104
|
+
updated_at timestamptz not null default now()
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
create index byollm_jobs_claimable_idx
|
|
108
|
+
on byollm_jobs (claimable_at)
|
|
109
|
+
where state = 'queued';
|
|
110
|
+
create index byollm_jobs_owner_idx on byollm_jobs (owner);
|
|
111
|
+
create index byollm_jobs_lease_idx
|
|
112
|
+
on byollm_jobs (lease_expires_at)
|
|
113
|
+
where state in ('claimed', 'running');
|
|
114
|
+
create index byollm_jobs_depends_idx on byollm_jobs using gin (depends_on);
|
|
115
|
+
|
|
116
|
+
-- ---------------------------------------------------------------------------
|
|
117
|
+
-- Cancel requests
|
|
118
|
+
-- ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
-- A separate table rather than a column on the job: a cancel is a *request*
|
|
121
|
+
-- that the holding runner has not yet acknowledged, and the job's own state
|
|
122
|
+
-- must keep saying `running` until the runner reports back. Folding the two
|
|
123
|
+
-- together would make "cancel asked for" and "cancel happened" look alike.
|
|
124
|
+
create table byollm_job_cancels (
|
|
125
|
+
job_id uuid primary key references byollm_jobs (id) on delete cascade,
|
|
126
|
+
requested_at timestamptz not null default now()
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
-- ---------------------------------------------------------------------------
|
|
130
|
+
-- Dependency gating: unblock dependents when a job reaches `ok`
|
|
131
|
+
-- ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
create or replace function byollm_unblock_dependents()
|
|
134
|
+
returns trigger
|
|
135
|
+
language plpgsql
|
|
136
|
+
security definer
|
|
137
|
+
set search_path = public
|
|
138
|
+
as $$
|
|
139
|
+
begin
|
|
140
|
+
-- Only `ok` unblocks. A dependency that errored leaves its dependents
|
|
141
|
+
-- blocked rather than releasing them into a run whose input never arrived —
|
|
142
|
+
-- the chain stops where it broke.
|
|
143
|
+
if new.state = 'ok' and (old.state is distinct from 'ok') then
|
|
144
|
+
update byollm_jobs dependent
|
|
145
|
+
set claimable_at = now(),
|
|
146
|
+
updated_at = now()
|
|
147
|
+
where dependent.claimable_at is null
|
|
148
|
+
and new.id = any (dependent.depends_on)
|
|
149
|
+
and not exists (
|
|
150
|
+
select 1
|
|
151
|
+
from byollm_jobs dep
|
|
152
|
+
where dep.id = any (dependent.depends_on)
|
|
153
|
+
and dep.state is distinct from 'ok'
|
|
154
|
+
);
|
|
155
|
+
end if;
|
|
156
|
+
return new;
|
|
157
|
+
end;
|
|
158
|
+
$$;
|
|
159
|
+
|
|
160
|
+
create trigger byollm_jobs_unblock_dependents
|
|
161
|
+
after update of state on byollm_jobs
|
|
162
|
+
for each row
|
|
163
|
+
execute function byollm_unblock_dependents();
|
|
164
|
+
|
|
165
|
+
-- ---------------------------------------------------------------------------
|
|
166
|
+
-- Expiry sweep: leases first, then TTL. Idempotent — firing twice is safe.
|
|
167
|
+
-- ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
create or replace function byollm_expire_due()
|
|
170
|
+
returns integer
|
|
171
|
+
language plpgsql
|
|
172
|
+
security definer
|
|
173
|
+
set search_path = public
|
|
174
|
+
as $$
|
|
175
|
+
declare
|
|
176
|
+
changed integer := 0;
|
|
177
|
+
n integer;
|
|
178
|
+
begin
|
|
179
|
+
-- 1. Reclaim lapsed leases. A job whose runner died returns to the queue
|
|
180
|
+
-- with its TTL clock RESTARTED: it has not been waiting, it has been
|
|
181
|
+
-- worked on, and expiring it here would throw away the recovery that
|
|
182
|
+
-- lease reclaim exists to provide.
|
|
183
|
+
update byollm_jobs
|
|
184
|
+
set state = 'queued',
|
|
185
|
+
lease_runner = null,
|
|
186
|
+
lease_expires_at = null,
|
|
187
|
+
claimable_at = now(),
|
|
188
|
+
updated_at = now()
|
|
189
|
+
where state in ('claimed', 'running')
|
|
190
|
+
and lease_expires_at is not null
|
|
191
|
+
and lease_expires_at <= now();
|
|
192
|
+
get diagnostics n = row_count;
|
|
193
|
+
changed := changed + n;
|
|
194
|
+
|
|
195
|
+
-- 2. Expire what has genuinely sat unclaimed past its TTL, plus anything
|
|
196
|
+
-- past its absolute deadline.
|
|
197
|
+
update byollm_jobs
|
|
198
|
+
set state = 'expired',
|
|
199
|
+
lease_runner = null,
|
|
200
|
+
lease_expires_at = null,
|
|
201
|
+
updated_at = now()
|
|
202
|
+
where state = 'queued'
|
|
203
|
+
and (
|
|
204
|
+
(claimable_at is not null
|
|
205
|
+
and claimable_at + (ttl_ms || ' milliseconds')::interval <= now())
|
|
206
|
+
or (deadline_at is not null and deadline_at <= now())
|
|
207
|
+
);
|
|
208
|
+
get diagnostics n = row_count;
|
|
209
|
+
changed := changed + n;
|
|
210
|
+
|
|
211
|
+
return changed;
|
|
212
|
+
end;
|
|
213
|
+
$$;
|
|
214
|
+
|
|
215
|
+
-- ---------------------------------------------------------------------------
|
|
216
|
+
-- The atomic claim (CLAIM_ATOMIC)
|
|
217
|
+
-- ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
create or replace function byollm_claim_jobs(
|
|
220
|
+
p_runner_id uuid,
|
|
221
|
+
p_capabilities jsonb,
|
|
222
|
+
p_max integer,
|
|
223
|
+
p_lease_ms integer
|
|
224
|
+
)
|
|
225
|
+
returns setof byollm_jobs
|
|
226
|
+
language plpgsql
|
|
227
|
+
security definer
|
|
228
|
+
set search_path = public
|
|
229
|
+
as $$
|
|
230
|
+
declare
|
|
231
|
+
v_owner uuid;
|
|
232
|
+
v_revoked timestamptz;
|
|
233
|
+
v_kinds text[];
|
|
234
|
+
begin
|
|
235
|
+
select owner, revoked_at into v_owner, v_revoked
|
|
236
|
+
from byollm_runners where id = p_runner_id;
|
|
237
|
+
|
|
238
|
+
if v_owner is null then
|
|
239
|
+
raise exception 'unknown runner';
|
|
240
|
+
end if;
|
|
241
|
+
if v_revoked is not null then
|
|
242
|
+
raise exception 'runner is revoked';
|
|
243
|
+
end if;
|
|
244
|
+
|
|
245
|
+
perform byollm_expire_due();
|
|
246
|
+
|
|
247
|
+
-- Kinds this runner is advertising *in this request*. A daemon that just
|
|
248
|
+
-- lost a backend must not be handed work for it.
|
|
249
|
+
select array_agg(value ->> 'kind') into v_kinds
|
|
250
|
+
from jsonb_array_elements(p_capabilities);
|
|
251
|
+
|
|
252
|
+
return query
|
|
253
|
+
with candidate as (
|
|
254
|
+
select j.id
|
|
255
|
+
from byollm_jobs j
|
|
256
|
+
where j.state = 'queued'
|
|
257
|
+
and j.claimable_at is not null
|
|
258
|
+
and j.claimable_at <= now()
|
|
259
|
+
and j.kind = any (v_kinds)
|
|
260
|
+
and not (p_runner_id = any (j.refused_by))
|
|
261
|
+
-- Dependency gating, belt and braces alongside claimable_at.
|
|
262
|
+
and not exists (
|
|
263
|
+
select 1 from byollm_jobs dep
|
|
264
|
+
where dep.id = any (j.depends_on) and dep.state is distinct from 'ok'
|
|
265
|
+
)
|
|
266
|
+
-- The audience rules, server side. The daemon enforces them too; this
|
|
267
|
+
-- is defence in depth, and the `named` case is deliberately permissive
|
|
268
|
+
-- here because the server cannot see a remote daemon's local allowlist.
|
|
269
|
+
and byollm_audience_admits(j, p_runner_id, v_owner, p_capabilities)
|
|
270
|
+
order by j.claimable_at
|
|
271
|
+
limit p_max
|
|
272
|
+
for update skip locked
|
|
273
|
+
)
|
|
274
|
+
update byollm_jobs j
|
|
275
|
+
set state = 'claimed',
|
|
276
|
+
lease_runner = p_runner_id,
|
|
277
|
+
lease_expires_at = now() + (p_lease_ms || ' milliseconds')::interval,
|
|
278
|
+
attempts = j.attempts + 1,
|
|
279
|
+
updated_at = now()
|
|
280
|
+
from candidate c
|
|
281
|
+
where j.id = c.id
|
|
282
|
+
returning j.*;
|
|
283
|
+
end;
|
|
284
|
+
$$;
|
|
285
|
+
|
|
286
|
+
-- The audience decision, mirroring `matchAudience` in @byollm/protocol.
|
|
287
|
+
create or replace function byollm_audience_admits(
|
|
288
|
+
p_job byollm_jobs,
|
|
289
|
+
p_runner_id uuid,
|
|
290
|
+
p_runner_owner uuid,
|
|
291
|
+
p_capabilities jsonb
|
|
292
|
+
)
|
|
293
|
+
returns boolean
|
|
294
|
+
language plpgsql
|
|
295
|
+
stable
|
|
296
|
+
security definer
|
|
297
|
+
set search_path = public
|
|
298
|
+
as $$
|
|
299
|
+
declare
|
|
300
|
+
v_cap jsonb;
|
|
301
|
+
v_scope text;
|
|
302
|
+
v_backend text;
|
|
303
|
+
v_same_owner boolean := (p_job.owner = p_runner_owner);
|
|
304
|
+
begin
|
|
305
|
+
select value into v_cap
|
|
306
|
+
from jsonb_array_elements(p_capabilities)
|
|
307
|
+
where value ->> 'kind' = p_job.kind
|
|
308
|
+
limit 1;
|
|
309
|
+
|
|
310
|
+
if v_cap is null then
|
|
311
|
+
return false;
|
|
312
|
+
end if;
|
|
313
|
+
|
|
314
|
+
v_scope := v_cap ->> 'offerScope';
|
|
315
|
+
v_backend := v_cap ->> 'backendId';
|
|
316
|
+
|
|
317
|
+
-- Side 1: does the job's audience admit this runner's owner?
|
|
318
|
+
if p_job.audience = 'self' and not v_same_owner then
|
|
319
|
+
return false;
|
|
320
|
+
end if;
|
|
321
|
+
if p_job.audience = 'named'
|
|
322
|
+
and not v_same_owner
|
|
323
|
+
and p_job.audience_allow is not null
|
|
324
|
+
and not (p_runner_owner = any (p_job.audience_allow)) then
|
|
325
|
+
return false;
|
|
326
|
+
end if;
|
|
327
|
+
|
|
328
|
+
-- A daemon always runs its own owner's work.
|
|
329
|
+
if v_same_owner then
|
|
330
|
+
return true;
|
|
331
|
+
end if;
|
|
332
|
+
|
|
333
|
+
-- The subscription self-lock. Applied here regardless of the scope the
|
|
334
|
+
-- daemon advertised: a widened scope on a subscription backend is refused,
|
|
335
|
+
-- not obeyed (SUBSCRIPTION_SELF_LOCK).
|
|
336
|
+
if v_backend = 'claude-cli' then
|
|
337
|
+
return false;
|
|
338
|
+
end if;
|
|
339
|
+
|
|
340
|
+
-- Side 2: does the backend's offer scope admit the job's owner?
|
|
341
|
+
if v_scope = 'self' then
|
|
342
|
+
return false;
|
|
343
|
+
end if;
|
|
344
|
+
if v_scope = 'named' then
|
|
345
|
+
-- The server cannot verify a remote daemon's local allowlist and must not
|
|
346
|
+
-- pretend to. It offers the job; the daemon refuses if its own list says
|
|
347
|
+
-- no, and the refusal is remembered in refused_by.
|
|
348
|
+
return true;
|
|
349
|
+
end if;
|
|
350
|
+
return v_scope = 'public';
|
|
351
|
+
end;
|
|
352
|
+
$$;
|
|
353
|
+
|
|
354
|
+
-- ---------------------------------------------------------------------------
|
|
355
|
+
-- RLS
|
|
356
|
+
-- ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
alter table byollm_runners enable row level security;
|
|
359
|
+
alter table byollm_jobs enable row level security;
|
|
360
|
+
alter table byollm_pairings enable row level security;
|
|
361
|
+
alter table byollm_job_cancels enable row level security;
|
|
362
|
+
|
|
363
|
+
-- A user sees and manages their own runners, and nobody else's.
|
|
364
|
+
create policy byollm_runners_owner_select on byollm_runners
|
|
365
|
+
for select using ((select auth.uid()) = owner);
|
|
366
|
+
create policy byollm_runners_owner_update on byollm_runners
|
|
367
|
+
for update using ((select auth.uid()) = owner);
|
|
368
|
+
create policy byollm_runners_owner_delete on byollm_runners
|
|
369
|
+
for delete using ((select auth.uid()) = owner);
|
|
370
|
+
|
|
371
|
+
-- A user sees their own jobs. Community jobs they volunteered to run are
|
|
372
|
+
-- visible through the claim RPC, which is security definer — deliberately
|
|
373
|
+
-- not through a broad select policy, so browsing other people's prompts is
|
|
374
|
+
-- not possible even for a willing volunteer.
|
|
375
|
+
create policy byollm_jobs_owner_select on byollm_jobs
|
|
376
|
+
for select using ((select auth.uid()) = owner);
|
|
377
|
+
create policy byollm_jobs_owner_insert on byollm_jobs
|
|
378
|
+
for insert with check ((select auth.uid()) = owner);
|
|
379
|
+
|
|
380
|
+
-- Pairings are approved through the RPC below, never written directly.
|
|
381
|
+
create policy byollm_pairings_owner_select on byollm_pairings
|
|
382
|
+
for select using ((select auth.uid()) = owner);
|
|
383
|
+
|
|
384
|
+
-- A user may ask to cancel their own job, and see that they asked.
|
|
385
|
+
create policy byollm_job_cancels_owner_select on byollm_job_cancels
|
|
386
|
+
for select using (
|
|
387
|
+
exists (
|
|
388
|
+
select 1 from byollm_jobs j
|
|
389
|
+
where j.id = byollm_job_cancels.job_id
|
|
390
|
+
and j.owner = (select auth.uid())
|
|
391
|
+
)
|
|
392
|
+
);
|
|
393
|
+
create policy byollm_job_cancels_owner_insert on byollm_job_cancels
|
|
394
|
+
for insert with check (
|
|
395
|
+
exists (
|
|
396
|
+
select 1 from byollm_jobs j
|
|
397
|
+
where j.id = byollm_job_cancels.job_id
|
|
398
|
+
and j.owner = (select auth.uid())
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
-- ---------------------------------------------------------------------------
|
|
403
|
+
-- Pairing approval — the one door for creating a runner
|
|
404
|
+
-- ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
create or replace function byollm_approve_pairing(
|
|
407
|
+
p_user_code text,
|
|
408
|
+
p_token_hash text
|
|
409
|
+
)
|
|
410
|
+
returns byollm_runners
|
|
411
|
+
language plpgsql
|
|
412
|
+
security definer
|
|
413
|
+
set search_path = public
|
|
414
|
+
as $$
|
|
415
|
+
declare
|
|
416
|
+
v_pairing byollm_pairings;
|
|
417
|
+
v_runner byollm_runners;
|
|
418
|
+
v_owner uuid := (select auth.uid());
|
|
419
|
+
begin
|
|
420
|
+
-- The owner comes from the caller's own session. A daemon can never assert
|
|
421
|
+
-- who it is; that is the whole reason pairing is interactive.
|
|
422
|
+
if v_owner is null then
|
|
423
|
+
raise exception 'approving a pairing requires an authenticated user';
|
|
424
|
+
end if;
|
|
425
|
+
|
|
426
|
+
select * into v_pairing from byollm_pairings
|
|
427
|
+
where user_code = p_user_code for update;
|
|
428
|
+
|
|
429
|
+
if v_pairing is null then
|
|
430
|
+
raise exception 'unknown pairing code';
|
|
431
|
+
end if;
|
|
432
|
+
if v_pairing.expires_at <= now() then
|
|
433
|
+
raise exception 'pairing code has expired';
|
|
434
|
+
end if;
|
|
435
|
+
if v_pairing.state <> 'pending' then
|
|
436
|
+
raise exception 'pairing is already %', v_pairing.state;
|
|
437
|
+
end if;
|
|
438
|
+
|
|
439
|
+
insert into byollm_runners (owner, token_hash, label, platform,
|
|
440
|
+
daemon_version, capabilities)
|
|
441
|
+
values (v_owner, p_token_hash, v_pairing.label, v_pairing.platform,
|
|
442
|
+
v_pairing.daemon_version, v_pairing.capabilities)
|
|
443
|
+
returning * into v_runner;
|
|
444
|
+
|
|
445
|
+
update byollm_pairings
|
|
446
|
+
set state = 'approved', owner = v_owner, runner_id = v_runner.id
|
|
447
|
+
where device_code_hash = v_pairing.device_code_hash;
|
|
448
|
+
|
|
449
|
+
return v_runner;
|
|
450
|
+
end;
|
|
451
|
+
$$;
|
|
452
|
+
|
|
453
|
+
-- ---------------------------------------------------------------------------
|
|
454
|
+
-- Grants — REVOKE from PUBLIC, GRANT deliberately (the house rule)
|
|
455
|
+
-- ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
-- Table privileges, granted deliberately rather than inherited.
|
|
458
|
+
--
|
|
459
|
+
-- Supabase images have historically granted blanket privileges on new public
|
|
460
|
+
-- tables to anon/authenticated/service_role, and relying on that is how this
|
|
461
|
+
-- migration passed on one CLI version and failed with "permission denied" on a
|
|
462
|
+
-- newer one. RLS decides *which rows*; these decide *which operations* — both
|
|
463
|
+
-- are required, and neither is a substitute for the other.
|
|
464
|
+
--
|
|
465
|
+
-- Revoke first, then grant. Supabase images ship `ALTER DEFAULT PRIVILEGES`
|
|
466
|
+
-- that hand `anon` and `authenticated` ALL privileges on every new public
|
|
467
|
+
-- table — verified on a fresh database, where `anon` arrived holding
|
|
468
|
+
-- INSERT/UPDATE/DELETE on all four of these. RLS still refuses every row, so
|
|
469
|
+
-- nothing was exposed, but "we grant anon nothing" is only true if we take
|
|
470
|
+
-- away what the image already gave. Revoking makes this file authoritative
|
|
471
|
+
-- instead of dependent on whichever image happens to be running.
|
|
472
|
+
revoke all on byollm_jobs, byollm_runners, byollm_pairings, byollm_job_cancels
|
|
473
|
+
from anon, authenticated;
|
|
474
|
+
|
|
475
|
+
-- `anon` is deliberately granted nothing back: no part of this schema is
|
|
476
|
+
-- reachable without a session.
|
|
477
|
+
grant select, insert on byollm_jobs to authenticated;
|
|
478
|
+
grant select, update, delete on byollm_runners to authenticated;
|
|
479
|
+
grant select on byollm_pairings to authenticated;
|
|
480
|
+
grant select, insert on byollm_job_cancels to authenticated;
|
|
481
|
+
|
|
482
|
+
-- The protocol handler authenticates runners with their own bearer tokens,
|
|
483
|
+
-- which are not Supabase sessions, so it runs as the service role.
|
|
484
|
+
grant select, insert, update, delete
|
|
485
|
+
on byollm_jobs, byollm_runners, byollm_pairings, byollm_job_cancels
|
|
486
|
+
to service_role;
|
|
487
|
+
|
|
488
|
+
revoke all on function byollm_claim_jobs(uuid, jsonb, integer, integer) from public;
|
|
489
|
+
revoke all on function byollm_audience_admits(byollm_jobs, uuid, uuid, jsonb) from public;
|
|
490
|
+
revoke all on function byollm_expire_due() from public;
|
|
491
|
+
revoke all on function byollm_approve_pairing(text, text) from public;
|
|
492
|
+
revoke all on function byollm_unblock_dependents() from public;
|
|
493
|
+
|
|
494
|
+
-- The claim RPC is called by the protocol handler with the service role, not
|
|
495
|
+
-- by a browser: a runner authenticates with its bearer token, which is not a
|
|
496
|
+
-- Supabase session.
|
|
497
|
+
grant execute on function byollm_claim_jobs(uuid, jsonb, integer, integer) to service_role;
|
|
498
|
+
grant execute on function byollm_expire_due() to service_role;
|
|
499
|
+
|
|
500
|
+
-- Approval happens in the browser, in the user's own session.
|
|
501
|
+
grant execute on function byollm_approve_pairing(text, text) to authenticated;
|
|
502
|
+
|
|
503
|
+
-- ---------------------------------------------------------------------------
|
|
504
|
+
-- Realtime — the app's delivery channel
|
|
505
|
+
-- ---------------------------------------------------------------------------
|
|
506
|
+
|
|
507
|
+
alter publication supabase_realtime add table byollm_jobs;
|