@byollm/server 0.1.0-alpha.1 → 0.1.0-alpha.100
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 +256 -18
- package/bin/keygen.mjs +21 -0
- package/dist/chunk-36Y77FUD.js +678 -0
- package/dist/chunk-36Y77FUD.js.map +1 -0
- package/dist/{chunk-7RKXFPBZ.js → chunk-YBNLLQZT.js} +19 -5
- package/dist/chunk-YBNLLQZT.js.map +1 -0
- package/dist/delivery-C5mz_Ykm.d.ts +114 -0
- package/dist/{handlers-D7lWfwno.d.ts → handlers-CTV3Jc6Q.d.ts} +28 -5
- package/dist/index.d.ts +262 -30
- package/dist/index.js +704 -89
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +40 -8
- package/dist/next.js +8 -2
- package/dist/next.js.map +1 -1
- package/dist/store-Cx2_bck1.d.ts +500 -0
- package/dist/supabase/index.d.ts +2 -2
- package/dist/supabase/index.js +151 -51
- package/dist/supabase/index.js.map +1 -1
- package/package.json +16 -4
- package/supabase/migrations/20260809000000_byollm_runner.sql +22 -3
- 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-7RKXFPBZ.js.map +0 -1
- package/dist/chunk-HL6EYHQ7.js +0 -422
- package/dist/chunk-HL6EYHQ7.js.map +0 -1
- package/dist/delivery-36nIe-b3.d.ts +0 -73
- package/dist/store-D23N6iiP.d.ts +0 -255
|
@@ -33,6 +33,9 @@ create table byollm_runners (
|
|
|
33
33
|
platform text not null check (platform in ('darwin', 'linux', 'win32')),
|
|
34
34
|
daemon_version text not null,
|
|
35
35
|
capabilities jsonb not null default '[]'::jsonb,
|
|
36
|
+
-- byollm_009 §5: the device's pinned public identity. What a later
|
|
37
|
+
-- signature verifies against — a runner id names a machine, this proves it.
|
|
38
|
+
device jsonb not null,
|
|
36
39
|
paused boolean not null default false,
|
|
37
40
|
-- Set once. A revoked runner never un-revokes.
|
|
38
41
|
revoked_at timestamptz,
|
|
@@ -63,6 +66,9 @@ create table byollm_pairings (
|
|
|
63
66
|
platform text not null,
|
|
64
67
|
daemon_version text not null,
|
|
65
68
|
capabilities jsonb not null default '[]'::jsonb,
|
|
69
|
+
-- Presented at pair start, so the user approves a *machine* rather than a
|
|
70
|
+
-- code any machine could redeem. Copied onto the runner at approval.
|
|
71
|
+
device jsonb not null,
|
|
66
72
|
expires_at timestamptz not null,
|
|
67
73
|
created_at timestamptz not null default now()
|
|
68
74
|
);
|
|
@@ -77,7 +83,13 @@ create index byollm_pairings_expiry_idx on byollm_pairings (expires_at);
|
|
|
77
83
|
create table byollm_jobs (
|
|
78
84
|
id uuid primary key default gen_random_uuid(),
|
|
79
85
|
kind text not null,
|
|
80
|
-
|
|
86
|
+
-- The work, sealed to the site's own key (byollm_009 §10). The database
|
|
87
|
+
-- holds ciphertext; the application opens it, because the application is
|
|
88
|
+
-- the endpoint. Backups, replicas and anyone with read access see this.
|
|
89
|
+
envelope jsonb not null,
|
|
90
|
+
-- Bucketed at enqueue, where the plaintext was.
|
|
91
|
+
size_class text not null default 'small'
|
|
92
|
+
check (size_class in ('small','medium','large','unbounded')),
|
|
81
93
|
audience byollm_audience not null default 'self',
|
|
82
94
|
owner uuid not null references auth.users (id) on delete cascade,
|
|
83
95
|
-- Server-side restriction on which runner owners may take a `named` job.
|
|
@@ -85,6 +97,10 @@ create table byollm_jobs (
|
|
|
85
97
|
audience_allow uuid[] ,
|
|
86
98
|
depends_on uuid[] not null default '{}',
|
|
87
99
|
state byollm_job_state not null default 'queued',
|
|
100
|
+
-- Identifies this *grant*, not just its holder. A runner can claim, release
|
|
101
|
+
-- and re-claim the same job; without an id per grant a replayed release
|
|
102
|
+
-- lands on a later lease and returns a job to the queue mid-execution.
|
|
103
|
+
lease_id uuid,
|
|
88
104
|
lease_runner uuid references byollm_runners (id) on delete set null,
|
|
89
105
|
lease_expires_at timestamptz,
|
|
90
106
|
-- When the job became claimable. THE TTL CLOCK STARTS HERE, not at
|
|
@@ -182,6 +198,7 @@ begin
|
|
|
182
198
|
-- lease reclaim exists to provide.
|
|
183
199
|
update byollm_jobs
|
|
184
200
|
set state = 'queued',
|
|
201
|
+
lease_id = null,
|
|
185
202
|
lease_runner = null,
|
|
186
203
|
lease_expires_at = null,
|
|
187
204
|
claimable_at = now(),
|
|
@@ -196,6 +213,7 @@ begin
|
|
|
196
213
|
-- past its absolute deadline.
|
|
197
214
|
update byollm_jobs
|
|
198
215
|
set state = 'expired',
|
|
216
|
+
lease_id = null,
|
|
199
217
|
lease_runner = null,
|
|
200
218
|
lease_expires_at = null,
|
|
201
219
|
updated_at = now()
|
|
@@ -273,6 +291,7 @@ begin
|
|
|
273
291
|
)
|
|
274
292
|
update byollm_jobs j
|
|
275
293
|
set state = 'claimed',
|
|
294
|
+
lease_id = gen_random_uuid(),
|
|
276
295
|
lease_runner = p_runner_id,
|
|
277
296
|
lease_expires_at = now() + (p_lease_ms || ' milliseconds')::interval,
|
|
278
297
|
attempts = j.attempts + 1,
|
|
@@ -437,9 +456,9 @@ begin
|
|
|
437
456
|
end if;
|
|
438
457
|
|
|
439
458
|
insert into byollm_runners (owner, token_hash, label, platform,
|
|
440
|
-
daemon_version, capabilities)
|
|
459
|
+
daemon_version, capabilities, device)
|
|
441
460
|
values (v_owner, p_token_hash, v_pairing.label, v_pairing.platform,
|
|
442
|
-
v_pairing.daemon_version, v_pairing.capabilities)
|
|
461
|
+
v_pairing.daemon_version, v_pairing.capabilities, v_pairing.device)
|
|
443
462
|
returning * into v_runner;
|
|
444
463
|
|
|
445
464
|
update byollm_pairings
|
|
@@ -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.';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/delivery.ts"],"sourcesContent":["import type { DeliveredResult } from \"@byollm/protocol\";\n\n/** Why a wait ended without a result. */\nexport class NoRunnerAvailableError extends Error {\n override readonly name = \"NoRunnerAvailableError\";\n constructor(\n readonly jobId: string,\n readonly reason: string,\n ) {\n super(\n `no runner is available to execute job ${jobId} (${reason}). ` +\n `Fall back to a hosted model, or prompt the user to start their runner.`,\n );\n }\n}\n\n/** The wait exceeded its timeout while a runner was still plausibly working. */\nexport class ResultTimeoutError extends Error {\n override readonly name = \"ResultTimeoutError\";\n constructor(\n readonly jobId: string,\n readonly timeoutMs: number,\n ) {\n super(`job ${jobId} did not finish within ${String(timeoutMs)}ms`);\n }\n}\n\nexport interface WaitOptions {\n /** Give up after this long. Default 5 minutes. */\n readonly timeoutMs?: number;\n /**\n * Called instead of throwing when no runner can take the job. Return a\n * substitute result (a hosted-model answer, say) and the wait resolves with\n * it; return nothing and {@link NoRunnerAvailableError} is thrown.\n */\n readonly onNoRunner?: (\n reason: string,\n ) => DeliveredResult | undefined | Promise<DeliveredResult | undefined>;\n /** Abort the wait. */\n readonly signal?: AbortSignal;\n}\n\n/**\n * How an app learns a job finished.\n *\n * byollm_003 Rev 1 is explicit that this is a *channel* — webhook, Realtime\n * subscription, or poll — and never an implied in-request `await`. The\n * polling implementation below is the portable default; the Supabase adapter\n * substitutes Realtime for the same interface.\n */\nexport interface ResultDelivery {\n waitFor(jobId: string, options?: WaitOptions): Promise<DeliveredResult>;\n}\n\nexport interface PollingDeliveryDeps {\n /** Current state of the job, or null if unknown. */\n readonly read: (jobId: string) => Promise<DeliveredResult | null>;\n /** Whether a runner could still take this job. */\n readonly availability: (\n jobId: string,\n ) => Promise<{ available: boolean; reason?: string; blocked: boolean }>;\n readonly sleep?: (ms: number) => Promise<void>;\n /**\n * Injectable clock. It must advance in step with {@link sleep}: a test that\n * stubs one and not the other gets a loop whose grace window never elapses.\n */\n readonly now?: () => number;\n /**\n * How long a sustained no-runner signal must persist before it is believed.\n * Defaults to {@link NO_RUNNER_GRACE_MS}.\n */\n readonly graceMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60_000;\nconst POLL_INTERVAL_MS = 500;\n/**\n * How long to let a job sit with no available runner before giving up.\n *\n * Not zero: a daemon restarting, or one whose heartbeat is momentarily late,\n * would otherwise fail every job in flight. The signal has to be sustained\n * before it is believed.\n */\nconst NO_RUNNER_GRACE_MS = 10_000;\n\nconst defaultSleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * The portable delivery channel: poll the store until the job is terminal.\n *\n * Correct everywhere and adequate for most apps. An adapter with a push\n * channel should replace it — see the Supabase adapter's Realtime delivery.\n */\nexport class PollingDelivery implements ResultDelivery {\n readonly #deps: PollingDeliveryDeps;\n\n constructor(deps: PollingDeliveryDeps) {\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 const sleep = this.#deps.sleep ?? defaultSleep;\n const now = this.#deps.now ?? Date.now;\n const graceMs = this.#deps.graceMs ?? NO_RUNNER_GRACE_MS;\n const started = now();\n let noRunnerSince: number | null = null;\n\n for (;;) {\n options.signal?.throwIfAborted();\n\n const current = await this.#deps.read(jobId);\n if (current && isTerminalState(current.state)) return current;\n\n const availability = await this.#deps.availability(jobId);\n if (availability.available || availability.blocked) {\n // `blocked` means the job is waiting on a dependency, which is not the\n // same event as \"nobody can run this\" ({@link MUSTS.NO_RUNNER_SIGNAL}).\n noRunnerSince = null;\n } else {\n noRunnerSince ??= now();\n if (now() - noRunnerSince >= graceMs) {\n const reason = availability.reason ?? \"no-runner-online\";\n const substitute = await options.onNoRunner?.(reason);\n if (substitute) return substitute;\n throw new NoRunnerAvailableError(jobId, reason);\n }\n }\n\n if (now() - started >= timeoutMs) {\n throw new ResultTimeoutError(jobId, timeoutMs);\n }\n await sleep(POLL_INTERVAL_MS);\n }\n }\n}\n\nfunction isTerminalState(state: string): boolean {\n return (\n state === \"ok\" ||\n state === \"error\" ||\n state === \"canceled\" ||\n state === \"expired\"\n );\n}\n"],"mappings":";AAGO,IAAM,yBAAN,cAAqC,MAAM;AAAA,EAEhD,YACW,OACA,QACT;AACA;AAAA,MACE,yCAAyC,KAAK,KAAK,MAAM;AAAA,IAE3D;AANS;AACA;AAAA,EAMX;AAAA,EAPW;AAAA,EACA;AAAA,EAHO,OAAO;AAU3B;AAGO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAE5C,YACW,OACA,WACT;AACA,UAAM,OAAO,KAAK,0BAA0B,OAAO,SAAS,CAAC,IAAI;AAHxD;AACA;AAAA,EAGX;AAAA,EAJW;AAAA,EACA;AAAA,EAHO,OAAO;AAO3B;AAiDA,IAAM,qBAAqB,IAAI;AAC/B,IAAM,mBAAmB;AAQzB,IAAM,qBAAqB;AAE3B,IAAM,eAAe,CAAC,OACpB,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAQ3C,IAAM,kBAAN,MAAgD;AAAA,EAC5C;AAAA,EAET,YAAY,MAA2B;AACrC,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,MAAM,QACJ,OACA,UAAuB,CAAC,GACE;AAC1B,UAAM,YAAY,QAAQ,aAAa;AACvC,UAAM,QAAQ,KAAK,MAAM,SAAS;AAClC,UAAM,MAAM,KAAK,MAAM,OAAO,KAAK;AACnC,UAAM,UAAU,KAAK,MAAM,WAAW;AACtC,UAAM,UAAU,IAAI;AACpB,QAAI,gBAA+B;AAEnC,eAAS;AACP,cAAQ,QAAQ,eAAe;AAE/B,YAAM,UAAU,MAAM,KAAK,MAAM,KAAK,KAAK;AAC3C,UAAI,WAAW,gBAAgB,QAAQ,KAAK,EAAG,QAAO;AAEtD,YAAM,eAAe,MAAM,KAAK,MAAM,aAAa,KAAK;AACxD,UAAI,aAAa,aAAa,aAAa,SAAS;AAGlD,wBAAgB;AAAA,MAClB,OAAO;AACL,0BAAkB,IAAI;AACtB,YAAI,IAAI,IAAI,iBAAiB,SAAS;AACpC,gBAAM,SAAS,aAAa,UAAU;AACtC,gBAAM,aAAa,MAAM,QAAQ,aAAa,MAAM;AACpD,cAAI,WAAY,QAAO;AACvB,gBAAM,IAAI,uBAAuB,OAAO,MAAM;AAAA,QAChD;AAAA,MACF;AAEA,UAAI,IAAI,IAAI,WAAW,WAAW;AAChC,cAAM,IAAI,mBAAmB,OAAO,SAAS;AAAA,MAC/C;AACA,YAAM,MAAM,gBAAgB;AAAA,IAC9B;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,OAAwB;AAC/C,SACE,UAAU,QACV,UAAU,WACV,UAAU,cACV,UAAU;AAEd;","names":[]}
|