@north-light/cli 0.3.554 → 0.3.555
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/dist/core/canvas/export-sqlite.js +3 -3
- package/dist/core/canvas/schema-chain.d.ts +1 -2
- package/dist/core/canvas/schema-chain.js +1 -1
- package/dist/core/canvas/schema-pg/002-fleet.sql +600 -0
- package/dist/core/canvas/schema-pg.sql +8 -14
- package/dist/core/canvas/schema-version.d.ts +1 -1
- package/dist/core/canvas/schema-version.js +1 -1
- package/dist/core/grants/mirror.d.ts +2 -2
- package/dist/core/grants/mirror.js +2 -2
- package/dist/core/ledger/backfill.d.ts +1 -2
- package/dist/core/ledger/backfill.js +5 -8
- package/dist/core/ledger/index.d.ts +1 -1
- package/dist/core/ledger/index.js +1 -1
- package/dist/core/ledger/limits.d.ts +0 -5
- package/dist/core/ledger/limits.js +2 -2
- package/dist/core/ledger/totals.d.ts +2 -1
- package/dist/core/ledger/totals.js +4 -4
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +13 -13
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var V=Object.defineProperty;var
|
|
2
|
-
${
|
|
3
|
-
`)}`)}
|
|
1
|
+
var V=Object.defineProperty;var w=(i,a)=>V(i,"name",{value:a,configurable:!0});import{DatabaseSync as W}from"node:sqlite";import{existsSync as Y,mkdirSync as D,mkdtempSync as J,renameSync as X,rmSync as P}from"node:fs";import{tmpdir as Q}from"node:os";import{join as A}from"node:path";import j from"pg";import{migrate as z,MIGRATIONS as I}from"./migrations.js";import{canvasDbPath as Z}from"./paths.js";import{canvasSearchPathOptions as ee}from"./schema-version.js";import{EXPORT_MARK_KEY as F,IMPORT_MARK_KEY as te,OBJECT_COLUMNS as ne,pgColumns as se,quote as c,TABLE_ORDER as C}from"./import-sqlite.js";const oe=5e3,re=new Set(["environments","relations","schemas","charges","charge_totals","limits","viewer_attachments","reconciler_leases","environment_repos","conversation_entries","conversation_parts","charge_chunks","profiles","profile_projects","profile_defaults","scope_settings","scope_bookkeeping","scope_marketplaces","scope_plugins","person_settings","model_accounts","model_account_cooldowns","model_api_keys","model_settings","model_sign_in_flows","env_vars"]),ae={exposures:"node, object, channel",rules:"object, position",edges:"type, src, dst",unresolved_links:"src, name",revision_parents:"child, parent",canvas_meta:"key",runtime_state:"key",message_causes:"message_id",peer_ingested:"key",peer_ingested_destinations:"key",message_sequence:"id",messages:"seq",review_comment_events:"seq",audit_rows:"id",peer_outbox:"id",app_users:"uid"},ie={objects:"id",revisions:"rev_id",watches:"watch_id"},H=new Set(["daemon_owner","schema_version","schema_min_reader","ledger_migrated",te,F]);function ce(i){const a=J(A(Q(),"crtr-export-")),s=process.env.CRTR_HOME;process.env.CRTR_HOME=a,D(A(a,"runtime","nodes"),{recursive:!0});try{const o=new W(i);return o.exec("PRAGMA journal_mode = DELETE;"),o.exec("PRAGMA foreign_keys = ON;"),z(o),o.exec("DELETE FROM canvas_meta; DELETE FROM message_sequence;"),o}finally{s===void 0?delete process.env.CRTR_HOME:process.env.CRTR_HOME=s,P(a,{recursive:!0,force:!0})}}w(ce,"createEmptyDatabase");async function Re(i,a=()=>{}){const s=Z();if(Y(s))throw new Error(`${s} already exists; refusing to overwrite it`);D(A(s,".."),{recursive:!0,mode:448});const o=`${s}.exporting-${process.pid}`,p=Date.now(),t=new j.Client({connectionString:i,application_name:`crtr-export-${process.pid}`,options:ee(),types:{getTypeParser:w(((f,r)=>f===20?d=>Number(d):j.types.getTypeParser(f,r)),"getTypeParser")}});await t.connect();let m;try{if((await t.query("SELECT value FROM canvas_meta WHERE key = 'schema_version'").catch(()=>({rows:[]}))).rows.length===0)throw new Error("the Postgres database has no canvas schema; nothing to export");m=ce(o);const r=m;r.exec("PRAGMA foreign_keys = OFF;");const d=await se(t),L=[...d.keys()].filter(e=>!re.has(e)&&!C.includes(e));if(L.length>0)throw new Error(`export: Postgres tables with no export rule: ${L.join(", ")}`);await t.query("BEGIN READ ONLY ISOLATION LEVEL REPEATABLE READ");const E={};for(const[e,S]of Object.entries(ie)){const h=new Map;for(const _ of(await t.query(`SELECT ${S} AS id, seq FROM ${e}`)).rows)h.set(_.id,Number(_.seq));E[e]=h}const B=E.objects;let M=0;r.exec("BEGIN");for(const e of C){const S=Date.now(),h=new Set(r.prepare(`PRAGMA table_info(${c(e)})`).all().map(n=>n.name)),_=d.get(e);if(_===void 0)throw new Error(`export: Postgres has no table ${e}`);const G=_.some(n=>n.name==="seq"),O=_.filter(n=>h.has(n.name)),K=new Set(ne[e]??[]),g=e==="objects"||e==="revisions"||e==="watches"||e==="watch_subscriptions",$=O.map(n=>n.type==="jsonb"?`${c(n.name)}::text AS ${c(n.name)}`:c(n.name));e==="watches"&&$.push("watch_id AS _watch_id"),g&&$.push("seq AS _seq");const R=O.map(n=>c(n.name)),U=r.prepare(`INSERT INTO ${c(e)} (${g?["rowid",...R]:R}) VALUES (${(g?["?",...R]:R).map(()=>"?").join(", ")})`),q=G?"seq":ae[e],b=`export_${e}`;await t.query(`DECLARE ${b} NO SCROLL CURSOR FOR SELECT ${$.join(", ")} FROM ${c(e)}${e==="canvas_meta"?` WHERE key NOT IN (${[...H].map(n=>`'${n}'`).join(", ")})`:""}${q===void 0?"":` ORDER BY ${q}`}`);let T=0;for(;;){const n=await t.query(`FETCH ${oe} FROM ${b}`);if(n.rows.length===0)break;for(const k of n.rows){const N=g?[Number(k._seq)]:[];for(const u of O){let l=k[u.name];e==="revision_parents"&&(u.name==="child"||u.name==="parent")?l=x(E.revisions,"revision",e,u.name,l):e==="watch_subscriptions"&&u.name==="watch"?l=x(E.watches,"watch",e,u.name,l):K.has(u.name)&&(l=x(B,"object",e,u.name,l)),N.push(l??null)}U.run(...N)}T+=n.rows.length}await t.query(`CLOSE ${b}`),M+=T,a(`export ${e}: ${T} rows in ${Date.now()-S} ms`)}r.exec("COMMIT"),await t.query("COMMIT");const y=r.prepare("PRAGMA foreign_key_check").all();if(y.length>0)throw new Error(`export: ${y.length} foreign key violation(s) in the exported canvas.db, first: ${JSON.stringify(y[0])}`);await pe(t,r);const v=r.prepare("PRAGMA user_version").get().user_version;if(v!==I.length)throw new Error(`export: exported canvas.db is at user_version ${v}, expected ${I.length}`);return r.close(),m=void 0,await t.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",[F,JSON.stringify({at:new Date().toISOString(),path:s})]),X(o,s),{path:s,rows:M,ms:Date.now()-p,userVersion:v}}catch(f){await t.query("ROLLBACK").catch(()=>{});try{m?.close()}catch{}for(const r of["","-wal","-shm","-journal"])P(`${o}${r}`,{force:!0});throw f}finally{await t.end().catch(()=>{})}}w(Re,"exportPostgresToSqlite");function x(i,a,s,o,p){if(p==null)return null;const t=i.get(p);if(t===void 0)throw new Error(`export: ${s}.${o} references ${a} ${String(p)}, which was not exported (`);return t}w(x,"mapId");async function pe(i,a){const s=[];for(const o of C){const p=Number((await i.query(`SELECT count(*) AS n FROM ${c(o)}`)).rows[0].n);let t=Number(a.prepare(`SELECT count(*) AS n FROM ${c(o)}`).get().n),m=p;o==="canvas_meta"&&(m=Number((await i.query("SELECT count(*) AS n FROM canvas_meta WHERE key <> ALL($1)",[[...H]])).rows[0].n),t=Number(a.prepare("SELECT count(*) AS n FROM canvas_meta").get().n)),m!==t&&s.push(`${o}: Postgres ${m} rows, SQLite ${t}`)}if(s.length>0)throw new Error(`Postgres \u2192 SQLite export verification failed:
|
|
2
|
+
${s.join(`
|
|
3
|
+
`)}`)}w(pe,"verify");export{Re as exportPostgresToSqlite};
|
|
@@ -29,6 +29,5 @@ export interface SchemaChainOptions {
|
|
|
29
29
|
/** Bring the connected database to this build's schema. Takes `pg_advisory_lock` for the session and reads the version under
|
|
30
30
|
* it, so processes opening together queue and a late one applies nothing. Refuses a database whose `schema_min_reader` is above
|
|
31
31
|
* this build's version; opens one whose `schema_version` is above it when `schema_min_reader` allows. A database with no
|
|
32
|
-
* `schema_version` gets `schema-pg.sql`, which runs as one multi-statement simple query, one transaction.
|
|
33
|
-
* baseline version first runs the file's `-- <name>:begin … -- <name>:end` blocks, in file order, each idempotent (S1 deletes this). */
|
|
32
|
+
* `schema_version` gets `schema-pg.sql`, which runs as one multi-statement simple query, one transaction. */
|
|
34
33
|
export declare function ensureSchema(client: pg.Client, options?: SchemaChainOptions): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var w=Object.defineProperty;var i=(r,e)=>w(r,"name",{value:e,configurable:!0});import{readdirSync as _,readFileSync as d}from"node:fs";import{dirname as v,join as m}from"node:path";import{fileURLToPath as y}from"node:url";import{CANVAS_SCHEMA_VERSION as l,readStoredVersions as h}from"./schema-version.js";const E=v(y(import.meta.url)),S=m(E,"schema-pg.sql"),$=m(E,"schema-pg"),f=7427001,u=1,T=5e3,p=12e4,A="55P03",g=/^(\d{3})-([a-z0-9][a-z0-9-]*)\.sql$/;function q(r=$){const e=[];for(const t of _(r).sort()){if(t===".gitkeep")continue;const s=g.exec(t);if(s===null)throw new Error(`schema delta directory ${r}: ${t} is not named NNN-<name>.sql`);e.push({version:Number(s[1]),name:s[2],file:m(r,t)})}return e.forEach((t,s)=>{if(t.version!==u+1+s)throw new Error(`schema delta directory ${r}: expected delta ${u+1+s} but found ${t.version}-${t.name}.sql; deltas are numbered contiguously from ${u+1}`)}),e}i(q,"listDeltas");const L=i(r=>new Promise(e=>setTimeout(e,r)),"sleep");async function M(r,e,t){const s=d(e.file,"utf8"),a=Date.now();let o=250;for(let n=1;;n++)try{if(await r.query("BEGIN"),await r.query(`SET LOCAL lock_timeout = '${t.lockTimeoutMs}ms'`),await r.query(s),(await r.query("UPDATE canvas_meta SET value = $1 WHERE key = 'schema_version' AND value = $2",[String(e.version),String(e.version-1)])).rowCount!==1)throw new Error(`schema step ${e.version} (${e.name}): schema_version was not ${e.version-1}; another writer changed the schema outside the chain`);await r.query("COMMIT"),t.emit?.("info","schema.step.applied",{version:e.version,name:e.name,ms:Date.now()-a,attempts:n});return}catch(c){if(await r.query("ROLLBACK").catch(()=>{}),c.code!==A)throw c;if(Date.now()+o-a>t.retryBudgetMs)throw new Error(`schema step ${e.version} (${e.name}) could not get its locks within ${t.retryBudgetMs} ms (${n} attempts); a long transaction holds a table it needs`,{cause:c});t.emit?.("warn","schema.step.lock_timeout",{version:e.version,name:e.name,attempt:n,retry_in_ms:o}),await L(o),o=Math.min(o*2,1e4)}}i(M,"applyStep");async function O(r,e={}){const t=e.version??l,s=q(e.deltaDir);if(e.schema!==void 0){const a=`"${e.schema.replace(/"/g,'""')}"`;await r.query(`CREATE SCHEMA IF NOT EXISTS ${a}`),await r.query(`SET search_path TO ${a}`)}await r.query("SELECT pg_advisory_lock($1)",[f]);try{let a=await h(r);if(a!==void 0&&t<a.minReader)throw new Error(`unsupported canvas schema: this build is at version ${t} but the database requires readers at version ${a.minReader} or above (it is at ${a.version}); this build is older than the database. Select a newer runtime generation.`);if(a===void 0&&(await r.query(d(S,"utf8")),a=await h(r),a===void 0))throw new Error("schema-pg.sql did not write canvas_meta.schema_version");let o=a.version;for(const n of s)n.version<=o||(o=n.version,await M(r,n,{lockTimeoutMs:e.lockTimeoutMs??T,retryBudgetMs:e.retryBudgetMs??p,emit:e.emit}));if(o<t)throw new Error(`canvas schema version ${o} does not match this build's ${t}: no delta reaches it`)}finally{await r.query("SELECT pg_advisory_unlock($1)",[f]).catch(()=>{})}}i(O,"ensureSchema");export{u as BASELINE_VERSION,T as DEFAULT_STEP_LOCK_TIMEOUT_MS,p as DEFAULT_STEP_RETRY_BUDGET_MS,O as ensureSchema,q as listDeltas};
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
-- 002-fleet.sql — cycle 5 additions (design-fleet "Data model"). One transaction, additions only; the removals ship in 003.
|
|
2
|
+
-- Opens with the text of the three blocks that joined version 1 after release (environments, ledger, permissions), verbatim and
|
|
3
|
+
-- idempotent, so a version-1 database with or without them reaches the same schema as a fresh one. A pure function of the baseline.
|
|
4
|
+
CREATE TABLE IF NOT EXISTS environments (
|
|
5
|
+
object text PRIMARY KEY REFERENCES objects(id) ON DELETE CASCADE,
|
|
6
|
+
kind text NOT NULL CHECK (kind IN ('machine')),
|
|
7
|
+
machine_id text NOT NULL UNIQUE,
|
|
8
|
+
last_seen_at text NOT NULL
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS charges (
|
|
12
|
+
id text PRIMARY KEY,
|
|
13
|
+
request text,
|
|
14
|
+
at text NOT NULL,
|
|
15
|
+
month text NOT NULL,
|
|
16
|
+
kind text NOT NULL CHECK (kind IN ('model','tool','sandbox','job','storage','take')),
|
|
17
|
+
what text NOT NULL,
|
|
18
|
+
units jsonb,
|
|
19
|
+
usd numeric(14,6),
|
|
20
|
+
payer text NOT NULL,
|
|
21
|
+
funding text NOT NULL CHECK (funding IN ('bill','subscription','key','endpoint','take','legacy')),
|
|
22
|
+
sub text,
|
|
23
|
+
grantee text NOT NULL,
|
|
24
|
+
node text,
|
|
25
|
+
run text,
|
|
26
|
+
provider text,
|
|
27
|
+
caused_by text,
|
|
28
|
+
vendor_ref text,
|
|
29
|
+
refused jsonb
|
|
30
|
+
);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS charges_month_payer ON charges(month, payer);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS charges_request ON charges(request) WHERE request IS NOT NULL;
|
|
33
|
+
CREATE INDEX IF NOT EXISTS charges_caused_by ON charges(caused_by) WHERE caused_by IS NOT NULL;
|
|
34
|
+
CREATE INDEX IF NOT EXISTS charges_run ON charges(run) WHERE run IS NOT NULL;
|
|
35
|
+
CREATE INDEX IF NOT EXISTS charges_grantee_month ON charges(grantee, month);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS charge_totals (
|
|
38
|
+
month text NOT NULL,
|
|
39
|
+
payer text NOT NULL,
|
|
40
|
+
funding text NOT NULL,
|
|
41
|
+
sub text NOT NULL DEFAULT '',
|
|
42
|
+
grantee text NOT NULL,
|
|
43
|
+
provider text NOT NULL DEFAULT '',
|
|
44
|
+
usd numeric(16,6) NOT NULL DEFAULT 0,
|
|
45
|
+
PRIMARY KEY (month, payer, funding, sub, grantee, provider)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS limits (
|
|
49
|
+
id text PRIMARY KEY,
|
|
50
|
+
payer text NOT NULL,
|
|
51
|
+
funding text,
|
|
52
|
+
grantee text,
|
|
53
|
+
provider text,
|
|
54
|
+
sub text,
|
|
55
|
+
usd_per_month numeric(14,6) NOT NULL CHECK (usd_per_month >= 0),
|
|
56
|
+
source text NOT NULL CHECK (source IN ('grant_sync','person','operator')),
|
|
57
|
+
updated_at text NOT NULL
|
|
58
|
+
);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS limits_payer ON limits(payer);
|
|
60
|
+
|
|
61
|
+
ALTER TABLE nodes ADD COLUMN IF NOT EXISTS caused_by text;
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
64
|
+
resource_type text NOT NULL,
|
|
65
|
+
resource_id text NOT NULL,
|
|
66
|
+
relation text NOT NULL,
|
|
67
|
+
subject_type text NOT NULL,
|
|
68
|
+
subject_id text NOT NULL,
|
|
69
|
+
subject_relation text NOT NULL DEFAULT '',
|
|
70
|
+
written_by text NOT NULL,
|
|
71
|
+
created_at text NOT NULL,
|
|
72
|
+
PRIMARY KEY (resource_type, resource_id, relation, subject_type, subject_id, subject_relation)
|
|
73
|
+
);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS relations_subject ON relations(subject_type, subject_id, relation);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS relations_resource ON relations(resource_id);
|
|
76
|
+
|
|
77
|
+
CREATE TABLE IF NOT EXISTS schemas (
|
|
78
|
+
namespace text PRIMARY KEY,
|
|
79
|
+
app text NOT NULL,
|
|
80
|
+
version bigint NOT NULL,
|
|
81
|
+
source text NOT NULL,
|
|
82
|
+
compiled_json text NOT NULL,
|
|
83
|
+
registered_at text NOT NULL
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
ALTER TABLE nodes ADD COLUMN IF NOT EXISTS app text;
|
|
87
|
+
ALTER TABLE objects ADD COLUMN IF NOT EXISTS schema_type text;
|
|
88
|
+
ALTER TABLE revisions ADD COLUMN IF NOT EXISTS acting_app text;
|
|
89
|
+
ALTER TABLE revisions ADD COLUMN IF NOT EXISTS acting_for text;
|
|
90
|
+
CREATE INDEX IF NOT EXISTS objects_owner_live ON objects(owner) WHERE deleted_at IS NULL;
|
|
91
|
+
|
|
92
|
+
UPDATE objects SET privacy = 'private' WHERE privacy = 'friends';
|
|
93
|
+
ALTER TABLE objects DROP CONSTRAINT IF EXISTS objects_privacy_check,
|
|
94
|
+
ADD CONSTRAINT objects_privacy_check CHECK (privacy IN ('public','private'));
|
|
95
|
+
|
|
96
|
+
CREATE OR REPLACE VIEW relation_source AS
|
|
97
|
+
SELECT resource_type, resource_id, relation, subject_type, subject_id, subject_relation, written_by FROM relations
|
|
98
|
+
UNION ALL
|
|
99
|
+
SELECT 'crtr/object', id, 'parent', 'crtr/object', owner, '', 'owner' FROM objects
|
|
100
|
+
WHERE owner IS NOT NULL AND (deleted_at IS NULL OR type = 'node');
|
|
101
|
+
|
|
102
|
+
DROP VIEW IF EXISTS node_rows;
|
|
103
|
+
CREATE VIEW node_rows AS
|
|
104
|
+
SELECT n.*,
|
|
105
|
+
o.name,
|
|
106
|
+
o.kind,
|
|
107
|
+
oi.external_id AS grantee,
|
|
108
|
+
CASE WHEN e.state = 'running' THEN e.pid ELSE NULL END AS pi_pid,
|
|
109
|
+
CASE WHEN e.state = 'running' THEN e.process_identity ELSE NULL END AS pi_pid_identity,
|
|
110
|
+
e.turn_open_at,
|
|
111
|
+
e.turn_closed_at,
|
|
112
|
+
s.session_id AS pi_session_id,
|
|
113
|
+
s.transcript_path AS pi_session_file,
|
|
114
|
+
f.fault_id,
|
|
115
|
+
f.link AS fault_link,
|
|
116
|
+
f.op AS fault_op,
|
|
117
|
+
f.kind AS fault_kind,
|
|
118
|
+
f.message AS fault_message,
|
|
119
|
+
f.since AS fault_since,
|
|
120
|
+
f.operation_id AS fault_operation_id,
|
|
121
|
+
f.anchor_entry_id AS fault_anchor_entry_id,
|
|
122
|
+
f.retry_disposition AS fault_retry_disposition,
|
|
123
|
+
f.retry_by AS fault_retry_by,
|
|
124
|
+
f.retry_attempt AS fault_retry_attempt,
|
|
125
|
+
f.retry_max AS fault_retry_max,
|
|
126
|
+
f.retry_next_at AS fault_retry_next_at,
|
|
127
|
+
f.provider_error_json AS fault_provider_error_json
|
|
128
|
+
FROM nodes n
|
|
129
|
+
JOIN objects o ON o.id = n.node_id
|
|
130
|
+
LEFT JOIN identities oi ON oi.object = COALESCE(n.app, o.owner)
|
|
131
|
+
LEFT JOIN executions e ON e.node_id = n.node_id AND e.state != 'exited'
|
|
132
|
+
LEFT JOIN sessions s ON s.node_id = n.node_id AND s.unbound_at IS NULL
|
|
133
|
+
LEFT JOIN node_faults f ON f.node_id = n.node_id AND f.active = 1;
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
-- ---------------------------------------------------------------- executions: the fleet registry becomes a row
|
|
137
|
+
|
|
138
|
+
ALTER TABLE executions
|
|
139
|
+
ADD COLUMN runner text NOT NULL DEFAULT 'process' CHECK (runner IN ('process', 'blaxel')),
|
|
140
|
+
ADD COLUMN sandbox_name text,
|
|
141
|
+
ADD COLUMN attempt bigint NOT NULL DEFAULT 1,
|
|
142
|
+
ADD COLUMN release text,
|
|
143
|
+
ADD COLUMN heartbeat_at text,
|
|
144
|
+
ADD COLUMN awake_since text,
|
|
145
|
+
ADD COLUMN launch_json text,
|
|
146
|
+
ADD COLUMN stop_requested_at text,
|
|
147
|
+
ADD COLUMN stop_after_turn bigint NOT NULL DEFAULT 0 CHECK (stop_after_turn IN (0, 1)),
|
|
148
|
+
ADD COLUMN stop_reason text,
|
|
149
|
+
ADD COLUMN charged_through text,
|
|
150
|
+
ADD COLUMN exited_at text,
|
|
151
|
+
ADD COLUMN idle_at text;
|
|
152
|
+
|
|
153
|
+
-- ---------------------------------------------------------------- nodes: supervision state, prompts and claims as columns
|
|
154
|
+
|
|
155
|
+
ALTER TABLE nodes
|
|
156
|
+
ADD COLUMN initial_prompt text,
|
|
157
|
+
ADD COLUMN yield_note text,
|
|
158
|
+
ADD COLUMN situational_card text,
|
|
159
|
+
ADD COLUMN tool_summaries_json text,
|
|
160
|
+
ADD COLUMN turn_replies_json text,
|
|
161
|
+
ADD COLUMN unattended_since text,
|
|
162
|
+
ADD COLUMN stall_noticed_at text,
|
|
163
|
+
ADD COLUMN wedge_noticed_at text,
|
|
164
|
+
ADD COLUMN park_requested_at text,
|
|
165
|
+
ADD COLUMN park_activity bigint NOT NULL DEFAULT 0 CHECK (park_activity IN (0, 1)),
|
|
166
|
+
ADD COLUMN idle_release_attempts bigint NOT NULL DEFAULT 0,
|
|
167
|
+
ADD COLUMN idle_release_last_attempt_at text,
|
|
168
|
+
ADD COLUMN idle_release_acked_count bigint NOT NULL DEFAULT 0,
|
|
169
|
+
ADD COLUMN recovery_held_until text,
|
|
170
|
+
ADD COLUMN recovery_held_by text,
|
|
171
|
+
ADD COLUMN stream_message_seq bigint,
|
|
172
|
+
ADD COLUMN stream_block text,
|
|
173
|
+
ADD COLUMN spend_refusal text;
|
|
174
|
+
|
|
175
|
+
-- `situational_card` joins the column list so a live broker sees the change.
|
|
176
|
+
DROP TRIGGER IF EXISTS crtr_sig_node_upd ON nodes;
|
|
177
|
+
CREATE TRIGGER crtr_sig_node_upd AFTER UPDATE OF
|
|
178
|
+
description, title, icon, mode, lifecycle, status, cwd, parent, fork_from, profile_id,
|
|
179
|
+
review_binding_json, persona_ack_json, intent, situational_card
|
|
180
|
+
ON nodes FOR EACH ROW EXECUTE FUNCTION crtr_signal_node();
|
|
181
|
+
|
|
182
|
+
-- `node_rows` is `SELECT n.*`: its columns are fixed when the view is created, so it is created again after the columns above.
|
|
183
|
+
DROP VIEW IF EXISTS node_rows;
|
|
184
|
+
CREATE VIEW node_rows AS
|
|
185
|
+
SELECT n.*,
|
|
186
|
+
o.name,
|
|
187
|
+
o.kind,
|
|
188
|
+
oi.external_id AS grantee,
|
|
189
|
+
CASE WHEN e.state = 'running' THEN e.pid ELSE NULL END AS pi_pid,
|
|
190
|
+
CASE WHEN e.state = 'running' THEN e.process_identity ELSE NULL END AS pi_pid_identity,
|
|
191
|
+
e.turn_open_at,
|
|
192
|
+
e.turn_closed_at,
|
|
193
|
+
s.session_id AS pi_session_id,
|
|
194
|
+
s.transcript_path AS pi_session_file,
|
|
195
|
+
f.fault_id,
|
|
196
|
+
f.link AS fault_link,
|
|
197
|
+
f.op AS fault_op,
|
|
198
|
+
f.kind AS fault_kind,
|
|
199
|
+
f.message AS fault_message,
|
|
200
|
+
f.since AS fault_since,
|
|
201
|
+
f.operation_id AS fault_operation_id,
|
|
202
|
+
f.anchor_entry_id AS fault_anchor_entry_id,
|
|
203
|
+
f.retry_disposition AS fault_retry_disposition,
|
|
204
|
+
f.retry_by AS fault_retry_by,
|
|
205
|
+
f.retry_attempt AS fault_retry_attempt,
|
|
206
|
+
f.retry_max AS fault_retry_max,
|
|
207
|
+
f.retry_next_at AS fault_retry_next_at,
|
|
208
|
+
f.provider_error_json AS fault_provider_error_json
|
|
209
|
+
FROM nodes n
|
|
210
|
+
JOIN objects o ON o.id = n.node_id
|
|
211
|
+
LEFT JOIN identities oi ON oi.object = COALESCE(n.app, o.owner)
|
|
212
|
+
LEFT JOIN executions e ON e.node_id = n.node_id AND e.state != 'exited'
|
|
213
|
+
LEFT JOIN sessions s ON s.node_id = n.node_id AND s.unbound_at IS NULL
|
|
214
|
+
LEFT JOIN node_faults f ON f.node_id = n.node_id AND f.active = 1;
|
|
215
|
+
|
|
216
|
+
-- ---------------------------------------------------------------- viewers
|
|
217
|
+
|
|
218
|
+
CREATE TABLE viewer_attachments (
|
|
219
|
+
connection_id text PRIMARY KEY,
|
|
220
|
+
node_id text NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,
|
|
221
|
+
api_process_id text NOT NULL,
|
|
222
|
+
attends bigint NOT NULL CHECK (attends IN (0, 1)),
|
|
223
|
+
presented_at text,
|
|
224
|
+
opened_at text NOT NULL,
|
|
225
|
+
expires_at text NOT NULL
|
|
226
|
+
);
|
|
227
|
+
CREATE INDEX idx_viewer_attachments_node ON viewer_attachments(node_id, expires_at);
|
|
228
|
+
|
|
229
|
+
-- ---------------------------------------------------------------- small row-backed state
|
|
230
|
+
|
|
231
|
+
ALTER TABLE node_faults ADD COLUMN nudged_at text;
|
|
232
|
+
ALTER TABLE structured_requests
|
|
233
|
+
ADD COLUMN reprompt_count bigint NOT NULL DEFAULT 0,
|
|
234
|
+
ADD COLUMN reprompted_at bigint;
|
|
235
|
+
|
|
236
|
+
CREATE TABLE reconciler_leases (
|
|
237
|
+
lane text NOT NULL,
|
|
238
|
+
key text NOT NULL,
|
|
239
|
+
owner text NOT NULL,
|
|
240
|
+
expires_at text NOT NULL,
|
|
241
|
+
cursor text,
|
|
242
|
+
PRIMARY KEY (lane, key)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
-- Claims of the lanes that have none today (deadline, structured requests, review submit, fault retry, storage due work, bash jobs).
|
|
246
|
+
ALTER TABLE nodes ADD COLUMN claimed_by text, ADD COLUMN lease_expires_at text;
|
|
247
|
+
ALTER TABLE structured_requests ADD COLUMN claimed_by text, ADD COLUMN lease_expires_at text;
|
|
248
|
+
ALTER TABLE human_requests ADD COLUMN claimed_by text, ADD COLUMN lease_expires_at text;
|
|
249
|
+
ALTER TABLE node_faults ADD COLUMN claimed_by text, ADD COLUMN lease_expires_at text;
|
|
250
|
+
ALTER TABLE uploads ADD COLUMN claimed_by text, ADD COLUMN lease_expires_at text;
|
|
251
|
+
ALTER TABLE shares ADD COLUMN claimed_by text, ADD COLUMN lease_expires_at text;
|
|
252
|
+
|
|
253
|
+
ALTER TABLE jobs
|
|
254
|
+
ADD COLUMN output_key text,
|
|
255
|
+
ADD COLUMN deadline_at text,
|
|
256
|
+
ADD COLUMN claimed_by text,
|
|
257
|
+
ADD COLUMN lease_expires_at text,
|
|
258
|
+
ALTER COLUMN job_dir DROP NOT NULL,
|
|
259
|
+
ALTER COLUMN log_path DROP NOT NULL;
|
|
260
|
+
|
|
261
|
+
-- ---------------------------------------------------------------- environments (Blaxel sandboxes)
|
|
262
|
+
|
|
263
|
+
ALTER TABLE environments DROP CONSTRAINT IF EXISTS environments_kind_check;
|
|
264
|
+
-- `machine_id` becomes nullable for a Blaxel environment. "Required for a machine environment" is enforced by the code that writes the row: a new CHECK would narrow an existing table, which an adding delta may not do (003 can add it).
|
|
265
|
+
ALTER TABLE environments ADD CONSTRAINT environments_kind_check CHECK (kind IN ('machine', 'blaxel'));
|
|
266
|
+
ALTER TABLE environments
|
|
267
|
+
ALTER COLUMN machine_id DROP NOT NULL,
|
|
268
|
+
ADD COLUMN memory_mb bigint NOT NULL DEFAULT 2048,
|
|
269
|
+
ADD COLUMN recipe_revision bigint NOT NULL DEFAULT 0,
|
|
270
|
+
ADD COLUMN setup_script text,
|
|
271
|
+
ADD COLUMN pi_packages text,
|
|
272
|
+
ADD COLUMN image_revision bigint,
|
|
273
|
+
ADD COLUMN image_ref text,
|
|
274
|
+
ADD COLUMN image_error text,
|
|
275
|
+
ADD COLUMN image_building_at text;
|
|
276
|
+
|
|
277
|
+
-- A change of `image_revision` is the wake for nodes waiting on their environment's first image (design: `crtr_signal` kind `environment_image`).
|
|
278
|
+
CREATE FUNCTION crtr_signal_environment_image() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
279
|
+
BEGIN
|
|
280
|
+
PERFORM pg_notify('crtr_signal', 'environment_image:' || NEW.object);
|
|
281
|
+
RETURN NULL;
|
|
282
|
+
END $$;
|
|
283
|
+
CREATE TRIGGER crtr_sig_environment_image AFTER UPDATE OF image_revision ON environments
|
|
284
|
+
FOR EACH ROW WHEN (OLD.image_revision IS DISTINCT FROM NEW.image_revision) EXECUTE FUNCTION crtr_signal_environment_image();
|
|
285
|
+
|
|
286
|
+
CREATE TABLE environment_repos (
|
|
287
|
+
environment text NOT NULL REFERENCES environments(object) ON DELETE CASCADE,
|
|
288
|
+
position bigint NOT NULL,
|
|
289
|
+
name text NOT NULL,
|
|
290
|
+
github_full_name text NOT NULL,
|
|
291
|
+
default_branch text NOT NULL,
|
|
292
|
+
PRIMARY KEY (environment, position),
|
|
293
|
+
UNIQUE (environment, name)
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
-- ---------------------------------------------------------------- conversations
|
|
297
|
+
|
|
298
|
+
CREATE TABLE conversation_entries (
|
|
299
|
+
conversation_id text NOT NULL,
|
|
300
|
+
entry_id text NOT NULL,
|
|
301
|
+
seq bigint GENERATED BY DEFAULT AS IDENTITY UNIQUE,
|
|
302
|
+
body text NOT NULL,
|
|
303
|
+
bytes bigint NOT NULL,
|
|
304
|
+
execution_id text,
|
|
305
|
+
created text NOT NULL,
|
|
306
|
+
PRIMARY KEY (conversation_id, entry_id)
|
|
307
|
+
);
|
|
308
|
+
CREATE INDEX idx_conversation_entries_order ON conversation_entries(conversation_id, seq);
|
|
309
|
+
|
|
310
|
+
CREATE TABLE conversation_parts (
|
|
311
|
+
conversation_id text NOT NULL,
|
|
312
|
+
seq bigint NOT NULL,
|
|
313
|
+
blob_key text NOT NULL,
|
|
314
|
+
bytes bigint NOT NULL,
|
|
315
|
+
entries bigint NOT NULL,
|
|
316
|
+
created text NOT NULL,
|
|
317
|
+
PRIMARY KEY (conversation_id, seq)
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
-- ---------------------------------------------------------------- money ledger
|
|
321
|
+
|
|
322
|
+
ALTER TABLE charges
|
|
323
|
+
ADD COLUMN account text,
|
|
324
|
+
ADD COLUMN billed_usd numeric(14,6),
|
|
325
|
+
ADD COLUMN price_id text,
|
|
326
|
+
ADD COLUMN priced_at text,
|
|
327
|
+
ADD COLUMN invoice_id text;
|
|
328
|
+
CREATE INDEX charges_account_invoice ON charges(account, invoice_id, month);
|
|
329
|
+
|
|
330
|
+
ALTER TABLE charge_totals ADD COLUMN billed_usd numeric(16,6) NOT NULL DEFAULT 0;
|
|
331
|
+
|
|
332
|
+
-- Keeps `charge_totals.billed_usd`. It leaves `usd` to the application on INSERT (every build up to this one adds it with
|
|
333
|
+
-- `addToTotals`, and a build rolled back to must not count it twice); an UPDATE of a row's amounts or key moves both
|
|
334
|
+
-- amounts, which no earlier build does. Refused rows and `take` rows are never rolled up, as in `rebuildTotals`.
|
|
335
|
+
CREATE FUNCTION charge_totals_rollup() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
336
|
+
BEGIN
|
|
337
|
+
IF TG_OP = 'UPDATE' AND OLD.refused IS NULL AND OLD.kind <> 'take' THEN
|
|
338
|
+
INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd, billed_usd)
|
|
339
|
+
VALUES (OLD.month, OLD.payer, OLD.funding, COALESCE(OLD.sub, ''), OLD.grantee, COALESCE(OLD.provider, ''),
|
|
340
|
+
-COALESCE(OLD.usd, 0), -COALESCE(OLD.billed_usd, 0))
|
|
341
|
+
ON CONFLICT (month, payer, funding, sub, grantee, provider) DO UPDATE
|
|
342
|
+
SET usd = charge_totals.usd + EXCLUDED.usd, billed_usd = charge_totals.billed_usd + EXCLUDED.billed_usd;
|
|
343
|
+
END IF;
|
|
344
|
+
IF NEW.refused IS NULL AND NEW.kind <> 'take' AND (TG_OP = 'UPDATE' OR COALESCE(NEW.billed_usd, 0) <> 0) THEN
|
|
345
|
+
INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd, billed_usd)
|
|
346
|
+
VALUES (NEW.month, NEW.payer, NEW.funding, COALESCE(NEW.sub, ''), NEW.grantee, COALESCE(NEW.provider, ''),
|
|
347
|
+
CASE WHEN TG_OP = 'UPDATE' THEN COALESCE(NEW.usd, 0) ELSE 0 END, COALESCE(NEW.billed_usd, 0))
|
|
348
|
+
ON CONFLICT (month, payer, funding, sub, grantee, provider) DO UPDATE
|
|
349
|
+
SET usd = charge_totals.usd + EXCLUDED.usd, billed_usd = charge_totals.billed_usd + EXCLUDED.billed_usd;
|
|
350
|
+
END IF;
|
|
351
|
+
RETURN NULL;
|
|
352
|
+
END $$;
|
|
353
|
+
CREATE TRIGGER charges_rollup_insert AFTER INSERT ON charges
|
|
354
|
+
FOR EACH ROW EXECUTE FUNCTION charge_totals_rollup();
|
|
355
|
+
CREATE TRIGGER charges_rollup_update AFTER UPDATE ON charges
|
|
356
|
+
FOR EACH ROW WHEN (OLD.usd IS DISTINCT FROM NEW.usd OR OLD.billed_usd IS DISTINCT FROM NEW.billed_usd
|
|
357
|
+
OR OLD.month IS DISTINCT FROM NEW.month OR OLD.payer IS DISTINCT FROM NEW.payer
|
|
358
|
+
OR OLD.funding IS DISTINCT FROM NEW.funding OR OLD.sub IS DISTINCT FROM NEW.sub
|
|
359
|
+
OR OLD.grantee IS DISTINCT FROM NEW.grantee OR OLD.provider IS DISTINCT FROM NEW.provider
|
|
360
|
+
OR OLD.refused IS DISTINCT FROM NEW.refused OR OLD.kind IS DISTINCT FROM NEW.kind)
|
|
361
|
+
EXECUTE FUNCTION charge_totals_rollup();
|
|
362
|
+
|
|
363
|
+
ALTER TABLE limits ADD COLUMN account text, ALTER COLUMN payer DROP NOT NULL;
|
|
364
|
+
ALTER TABLE limits DROP CONSTRAINT IF EXISTS limits_source_check;
|
|
365
|
+
ALTER TABLE limits ADD CONSTRAINT limits_source_check CHECK (source IN ('grant_sync', 'person', 'operator', 'billing'));
|
|
366
|
+
|
|
367
|
+
CREATE FUNCTION crtr_signal_limits() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
368
|
+
BEGIN
|
|
369
|
+
IF TG_OP = 'DELETE' THEN
|
|
370
|
+
PERFORM pg_notify('crtr_signal', 'spend_limits:' || OLD.id);
|
|
371
|
+
RETURN OLD;
|
|
372
|
+
END IF;
|
|
373
|
+
PERFORM pg_notify('crtr_signal', 'spend_limits:' || NEW.id);
|
|
374
|
+
RETURN NEW;
|
|
375
|
+
END $$;
|
|
376
|
+
CREATE TRIGGER crtr_sig_limits AFTER INSERT OR UPDATE OR DELETE ON limits
|
|
377
|
+
FOR EACH ROW EXECUTE FUNCTION crtr_signal_limits();
|
|
378
|
+
|
|
379
|
+
CREATE TABLE charge_chunks (
|
|
380
|
+
month text NOT NULL,
|
|
381
|
+
payer text NOT NULL,
|
|
382
|
+
blob_key text NOT NULL,
|
|
383
|
+
"rows" bigint NOT NULL,
|
|
384
|
+
usd numeric(16,6),
|
|
385
|
+
billed_usd numeric(16,6),
|
|
386
|
+
first_at text NOT NULL,
|
|
387
|
+
last_at text NOT NULL,
|
|
388
|
+
PRIMARY KEY (month, payer)
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
-- ---------------------------------------------------------------- grants: the one copy, written by the directory too
|
|
392
|
+
|
|
393
|
+
ALTER TABLE grants
|
|
394
|
+
ADD COLUMN created_at text,
|
|
395
|
+
ADD COLUMN computed_rev bigint,
|
|
396
|
+
ADD COLUMN app_pays_until_vendor bigint NOT NULL DEFAULT 0 CHECK (app_pays_until_vendor IN (0, 1)),
|
|
397
|
+
ADD COLUMN removal_done_at text,
|
|
398
|
+
ALTER COLUMN scopes_json DROP NOT NULL,
|
|
399
|
+
ALTER COLUMN resolved_scopes_json DROP NOT NULL,
|
|
400
|
+
ALTER COLUMN dependencies_json DROP NOT NULL,
|
|
401
|
+
ALTER COLUMN synced_at DROP NOT NULL;
|
|
402
|
+
-- The old unique index (grantee, sub) stays until 003 drops it: a removal.
|
|
403
|
+
CREATE UNIQUE INDEX idx_grants_runtime_sub_grantee ON grants(runtime, sub, grantee);
|
|
404
|
+
|
|
405
|
+
CREATE FUNCTION crtr_signal_grant() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
406
|
+
BEGIN
|
|
407
|
+
IF TG_OP = 'DELETE' THEN
|
|
408
|
+
PERFORM pg_notify('crtr_signal', 'grant:' || OLD.id);
|
|
409
|
+
RETURN OLD;
|
|
410
|
+
END IF;
|
|
411
|
+
PERFORM pg_notify('crtr_signal', 'grant:' || NEW.id);
|
|
412
|
+
RETURN NEW;
|
|
413
|
+
END $$;
|
|
414
|
+
CREATE TRIGGER crtr_sig_grant_ins_del AFTER INSERT OR DELETE ON grants
|
|
415
|
+
FOR EACH ROW EXECUTE FUNCTION crtr_signal_grant();
|
|
416
|
+
CREATE TRIGGER crtr_sig_grant_upd AFTER UPDATE OF
|
|
417
|
+
id, runtime, sub, grantee, rev, scopes_json, resolved_scopes_json, spend_cap, expires_at, status, revoked_at,
|
|
418
|
+
approved_at, payer, synced_at, dependencies_json, run_notice_url, inference_endpoint_json, created_at,
|
|
419
|
+
computed_rev, app_pays_until_vendor
|
|
420
|
+
ON grants FOR EACH ROW EXECUTE FUNCTION crtr_signal_grant();
|
|
421
|
+
|
|
422
|
+
-- The `grant:<id>` limit row of cycle 4's rule (`grantLimit` in grants/mirror.ts): an active grant with a cap, not paid by the
|
|
423
|
+
-- app, and not `app:terminal`'s, becomes {payer = sub, grantee, any funding, usd_per_month = spend_cap}; any other grant has none.
|
|
424
|
+
-- A negative cap fails the limits CHECK and aborts the grant write, as replaceGrantLimits' RangeError did.
|
|
425
|
+
CREATE FUNCTION crtr_grant_limit() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
426
|
+
BEGIN
|
|
427
|
+
IF TG_OP = 'DELETE' THEN
|
|
428
|
+
DELETE FROM limits WHERE source = 'grant_sync' AND id = 'grant:' || OLD.id;
|
|
429
|
+
RETURN OLD;
|
|
430
|
+
END IF;
|
|
431
|
+
IF NEW.status = 'active' AND NEW.spend_cap IS NOT NULL
|
|
432
|
+
AND NEW.payer IS DISTINCT FROM 'app' AND NEW.grantee <> 'app:terminal' THEN
|
|
433
|
+
INSERT INTO limits (id, payer, grantee, usd_per_month, source, updated_at)
|
|
434
|
+
VALUES ('grant:' || NEW.id, NEW.sub, NEW.grantee, NEW.spend_cap::numeric, 'grant_sync',
|
|
435
|
+
to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'))
|
|
436
|
+
ON CONFLICT (id) DO UPDATE SET payer = EXCLUDED.payer, funding = NULL, grantee = EXCLUDED.grantee, provider = NULL, sub = NULL,
|
|
437
|
+
account = NULL, usd_per_month = EXCLUDED.usd_per_month, source = 'grant_sync', updated_at = EXCLUDED.updated_at
|
|
438
|
+
WHERE limits.source = 'grant_sync';
|
|
439
|
+
ELSE
|
|
440
|
+
DELETE FROM limits WHERE source = 'grant_sync' AND id = 'grant:' || NEW.id;
|
|
441
|
+
END IF;
|
|
442
|
+
RETURN NULL;
|
|
443
|
+
END $$;
|
|
444
|
+
CREATE TRIGGER crtr_grant_limit AFTER INSERT OR UPDATE OR DELETE ON grants
|
|
445
|
+
FOR EACH ROW EXECUTE FUNCTION crtr_grant_limit();
|
|
446
|
+
|
|
447
|
+
-- ---------------------------------------------------------------- person stores: profiles, scope settings, model credentials, env vars
|
|
448
|
+
|
|
449
|
+
CREATE TABLE profiles (
|
|
450
|
+
id text PRIMARY KEY,
|
|
451
|
+
person text NOT NULL,
|
|
452
|
+
name text NOT NULL,
|
|
453
|
+
created_at text NOT NULL,
|
|
454
|
+
last_used_at text,
|
|
455
|
+
paused_at text,
|
|
456
|
+
default_kind text,
|
|
457
|
+
grantee text,
|
|
458
|
+
metadata_json text NOT NULL DEFAULT '{}',
|
|
459
|
+
delivery_limits_json text NOT NULL DEFAULT '{}',
|
|
460
|
+
environment_id text,
|
|
461
|
+
deleting_since text,
|
|
462
|
+
UNIQUE (person, name)
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
CREATE TABLE profile_projects (
|
|
466
|
+
profile_id text NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
467
|
+
path text NOT NULL,
|
|
468
|
+
position bigint NOT NULL,
|
|
469
|
+
PRIMARY KEY (profile_id, path),
|
|
470
|
+
UNIQUE (profile_id, position)
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
CREATE TABLE profile_defaults (
|
|
474
|
+
person text NOT NULL,
|
|
475
|
+
directory text NOT NULL,
|
|
476
|
+
profile_id text NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
477
|
+
PRIMARY KEY (person, directory)
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
-- `owner_key` is `user` or `profile:<profile id>`, as in the scoped SQLite store; project scopes stay local files.
|
|
481
|
+
CREATE TABLE scope_settings (
|
|
482
|
+
person text NOT NULL,
|
|
483
|
+
owner_key text NOT NULL,
|
|
484
|
+
raw_json text NOT NULL,
|
|
485
|
+
PRIMARY KEY (person, owner_key)
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
CREATE TABLE scope_bookkeeping (
|
|
489
|
+
person text NOT NULL,
|
|
490
|
+
owner_key text NOT NULL,
|
|
491
|
+
last_self_check text,
|
|
492
|
+
active_canvas text,
|
|
493
|
+
PRIMARY KEY (person, owner_key)
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
CREATE TABLE scope_marketplaces (
|
|
497
|
+
person text NOT NULL,
|
|
498
|
+
owner_key text NOT NULL,
|
|
499
|
+
name text NOT NULL,
|
|
500
|
+
last_updated text,
|
|
501
|
+
url text,
|
|
502
|
+
ref text,
|
|
503
|
+
installed_at text,
|
|
504
|
+
PRIMARY KEY (person, owner_key, name)
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
CREATE TABLE scope_plugins (
|
|
508
|
+
person text NOT NULL,
|
|
509
|
+
owner_key text NOT NULL,
|
|
510
|
+
name text NOT NULL,
|
|
511
|
+
last_updated text,
|
|
512
|
+
etag text,
|
|
513
|
+
checked_at text,
|
|
514
|
+
enabled bigint NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
|
515
|
+
source_marketplace text,
|
|
516
|
+
version text,
|
|
517
|
+
source_path text,
|
|
518
|
+
PRIMARY KEY (person, owner_key, name)
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
CREATE TABLE person_settings (
|
|
522
|
+
person text PRIMARY KEY,
|
|
523
|
+
plugins_revision bigint NOT NULL DEFAULT 0
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
CREATE TABLE model_accounts (
|
|
527
|
+
person text NOT NULL,
|
|
528
|
+
provider_id text NOT NULL,
|
|
529
|
+
id text NOT NULL,
|
|
530
|
+
label text NOT NULL,
|
|
531
|
+
label_key text NOT NULL,
|
|
532
|
+
position bigint NOT NULL,
|
|
533
|
+
last_attempt_at bigint NOT NULL DEFAULT 0,
|
|
534
|
+
last_rate_limited_at bigint NOT NULL DEFAULT 0,
|
|
535
|
+
rate_limited_until bigint NOT NULL DEFAULT 0,
|
|
536
|
+
auth_failure text CHECK (auth_failure IS NULL OR auth_failure = 'invalid_grant'),
|
|
537
|
+
refresh_sealed bytea,
|
|
538
|
+
access_sealed bytea,
|
|
539
|
+
expires bigint,
|
|
540
|
+
vendor_account_id text,
|
|
541
|
+
client_id text,
|
|
542
|
+
key_id text,
|
|
543
|
+
PRIMARY KEY (person, provider_id, id),
|
|
544
|
+
UNIQUE (person, provider_id, label_key)
|
|
545
|
+
);
|
|
546
|
+
CREATE INDEX model_accounts_order ON model_accounts(person, provider_id, position);
|
|
547
|
+
|
|
548
|
+
CREATE TABLE model_account_cooldowns (
|
|
549
|
+
person text NOT NULL,
|
|
550
|
+
provider_id text NOT NULL,
|
|
551
|
+
account_id text NOT NULL,
|
|
552
|
+
family text NOT NULL,
|
|
553
|
+
rate_limited_until bigint NOT NULL,
|
|
554
|
+
last_rate_limited_at bigint NOT NULL,
|
|
555
|
+
PRIMARY KEY (person, provider_id, account_id, family)
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
CREATE TABLE model_api_keys (
|
|
559
|
+
person text NOT NULL,
|
|
560
|
+
provider_id text NOT NULL,
|
|
561
|
+
credential_sealed bytea NOT NULL,
|
|
562
|
+
key_id text NOT NULL,
|
|
563
|
+
updated_at text NOT NULL,
|
|
564
|
+
PRIMARY KEY (person, provider_id)
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
CREATE TABLE model_settings (
|
|
568
|
+
person text PRIMARY KEY,
|
|
569
|
+
default_fallback_strength text,
|
|
570
|
+
revert_when_available bigint CHECK (revert_when_available IS NULL OR revert_when_available IN (0, 1)),
|
|
571
|
+
revision bigint NOT NULL DEFAULT 0,
|
|
572
|
+
openai_device_id text
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
CREATE TABLE model_sign_in_flows (
|
|
576
|
+
flow_id text PRIMARY KEY,
|
|
577
|
+
provider text NOT NULL,
|
|
578
|
+
person text NOT NULL,
|
|
579
|
+
verifier_sealed bytea NOT NULL,
|
|
580
|
+
key_id text NOT NULL,
|
|
581
|
+
expires_at text NOT NULL
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
CREATE TABLE env_vars (
|
|
585
|
+
person text,
|
|
586
|
+
owner_kind text NOT NULL CHECK (owner_kind IN ('profile', 'app', 'deployment')),
|
|
587
|
+
owner_id text NOT NULL,
|
|
588
|
+
name text NOT NULL,
|
|
589
|
+
value_sealed bytea NOT NULL,
|
|
590
|
+
key_id text NOT NULL,
|
|
591
|
+
updated_at text NOT NULL,
|
|
592
|
+
CHECK ((owner_kind = 'deployment') = (person IS NULL))
|
|
593
|
+
);
|
|
594
|
+
-- A deployment row has no person; NULLs are distinct in a plain UNIQUE, so the person is folded into the key.
|
|
595
|
+
CREATE UNIQUE INDEX env_vars_key ON env_vars(COALESCE(person, ''), owner_kind, owner_id, name);
|
|
596
|
+
|
|
597
|
+
-- ---------------------------------------------------------------- reader floor
|
|
598
|
+
|
|
599
|
+
-- The previous build must still open this database: every step above only adds (D73).
|
|
600
|
+
INSERT INTO canvas_meta (key, value) VALUES ('schema_min_reader', '1') ON CONFLICT (key) DO NOTHING;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
-- identity columns for insertion order, jsonb where JSON functions run, bytea blobs, PL/pgSQL
|
|
4
4
|
-- triggers, and pg_notify triggers for the broker-signal bus.
|
|
5
5
|
--
|
|
6
|
-
--
|
|
7
|
-
-- later change is a
|
|
6
|
+
-- Frozen: this file is schema version 1. schema-chain.ts applies it once, in one transaction, when canvas_meta has no
|
|
7
|
+
-- `schema_version` row, then each numbered delta in schema-pg/. Every later change is a delta, never an edit of this file.
|
|
8
8
|
-- Types: SQLite INTEGER (64-bit) is bigint everywhere, including 0/1 flags and epoch-ms columns.
|
|
9
9
|
|
|
10
10
|
-- ---------------------------------------------------------------- objects and their satellites
|
|
@@ -73,16 +73,14 @@ CREATE TABLE jobs (
|
|
|
73
73
|
log_path text NOT NULL
|
|
74
74
|
);
|
|
75
75
|
|
|
76
|
-
-- The environments an object can run in (design: Environment module). Added after schema version 1 shipped
|
|
77
|
-
--
|
|
78
|
-
-- environments:begin
|
|
76
|
+
-- The environments an object can run in (design: Environment module). Added after schema version 1 shipped; delta
|
|
77
|
+
-- 002 opens with this block's text so a version-1 database that predates it gains it.
|
|
79
78
|
CREATE TABLE IF NOT EXISTS environments (
|
|
80
79
|
object text PRIMARY KEY REFERENCES objects(id) ON DELETE CASCADE,
|
|
81
80
|
kind text NOT NULL CHECK (kind IN ('machine')),
|
|
82
81
|
machine_id text NOT NULL UNIQUE,
|
|
83
82
|
last_seen_at text NOT NULL
|
|
84
83
|
);
|
|
85
|
-
-- environments:end
|
|
86
84
|
|
|
87
85
|
CREATE TABLE identities (
|
|
88
86
|
object text PRIMARY KEY REFERENCES objects(id) ON DELETE CASCADE,
|
|
@@ -942,11 +940,10 @@ CREATE TABLE peer_ingested_destinations (
|
|
|
942
940
|
-- ---------------------------------------------------------------- money ledger (cycle 4)
|
|
943
941
|
|
|
944
942
|
-- One `charges` row per charge, each naming its cause (`caused_by`); one `charge_totals` rollup upserted in the same
|
|
945
|
-
-- transaction as each charge; one `limits` row shape. Added after schema version 1 shipped
|
|
946
|
-
--
|
|
943
|
+
-- transaction as each charge; one `limits` row shape. Added after schema version 1 shipped; delta 002 opens with
|
|
944
|
+
-- this block's text (idempotent, additive). It sits BEFORE the permissions block on
|
|
947
945
|
-- purpose: that block drops and recreates `node_rows` (`SELECT n.*`), which only carries `nodes.caused_by` when the column
|
|
948
946
|
-- exists first. The whole block is one multi-statement query, so it applies entirely or not at all.
|
|
949
|
-
-- ledger:begin
|
|
950
947
|
CREATE TABLE IF NOT EXISTS charges (
|
|
951
948
|
id text PRIMARY KEY,
|
|
952
949
|
request text,
|
|
@@ -998,15 +995,13 @@ CREATE TABLE IF NOT EXISTS limits (
|
|
|
998
995
|
CREATE INDEX IF NOT EXISTS limits_payer ON limits(payer);
|
|
999
996
|
|
|
1000
997
|
ALTER TABLE nodes ADD COLUMN IF NOT EXISTS caused_by text;
|
|
1001
|
-
-- ledger:end
|
|
1002
998
|
|
|
1003
999
|
-- ---------------------------------------------------------------- permissions (cycle 3)
|
|
1004
1000
|
|
|
1005
1001
|
-- One permission model: relation facts, app schemas, the columns the evaluator and owner tree need, the narrowed privacy
|
|
1006
1002
|
-- levels, `relation_source` (facts plus each live object's, and each reaped node's, owner presented as its `parent`) and `node_rows` with `grantee` read
|
|
1007
|
-
-- from `nodes.app`. Added after schema version 1 shipped
|
|
1008
|
-
-- (idempotent
|
|
1009
|
-
-- permissions:begin
|
|
1003
|
+
-- from `nodes.app`. Added after schema version 1 shipped; delta 002 opens with this block's text
|
|
1004
|
+
-- (idempotent). The whole block is one multi-statement query, so it applies entirely or not at all.
|
|
1010
1005
|
CREATE TABLE IF NOT EXISTS relations (
|
|
1011
1006
|
resource_type text NOT NULL,
|
|
1012
1007
|
resource_id text NOT NULL,
|
|
@@ -1078,7 +1073,6 @@ SELECT n.*,
|
|
|
1078
1073
|
LEFT JOIN executions e ON e.node_id = n.node_id AND e.state != 'exited'
|
|
1079
1074
|
LEFT JOIN sessions s ON s.node_id = n.node_id AND s.unbound_at IS NULL
|
|
1080
1075
|
LEFT JOIN node_faults f ON f.node_id = n.node_id AND f.active = 1;
|
|
1081
|
-
-- permissions:end
|
|
1082
1076
|
|
|
1083
1077
|
-- ---------------------------------------------------------------- triggers
|
|
1084
1078
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The canvas schema version this build supports: the number of the highest delta in `schema-pg/` (the baseline
|
|
2
2
|
* `schema-pg.sql` is version 1). `scripts/install-runtime.mjs` reads this module from a candidate generation to refuse
|
|
3
3
|
* one below the live database's `schema_min_reader`. */
|
|
4
|
-
export declare const CANVAS_SCHEMA_VERSION =
|
|
4
|
+
export declare const CANVAS_SCHEMA_VERSION = 2;
|
|
5
5
|
/** The Postgres schema holding the canvas tables: `CRTR_DATABASE_SCHEMA` (`crtr` in the cloud deploy). Unset or empty
|
|
6
6
|
* means the role's own search_path (`public` on a Mac and on a VM). A name that is not a plain identifier is an error:
|
|
7
7
|
* it is put in a `search_path` connection option. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var m=Object.defineProperty;var r=(n,e)=>m(n,"name",{value:e,configurable:!0});const A=
|
|
1
|
+
var m=Object.defineProperty;var r=(n,e)=>m(n,"name",{value:e,configurable:!0});const A=2;function _(n=process.env){const e=n.CRTR_DATABASE_SCHEMA?.trim();if(e){if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(e))throw new Error(`CRTR_DATABASE_SCHEMA must be a plain identifier (letters, digits, underscore); got ${JSON.stringify(e)}`);return e}}r(_,"canvasDatabaseSchema");function h(n=process.env){return n.CRTR_SCHEMA_DATABASE_URL?.trim()||void 0}r(h,"canvasSchemaDatabaseUrl");function E(n=process.env){const e=_(n);return e===void 0?void 0:`-c search_path=${e}`}r(E,"canvasSearchPathOptions");async function v(n,e){const t=e===void 0?"canvas_meta":`"${e.replace(/"/g,'""')}".canvas_meta`;if((await n.query(`SELECT to_regclass('${t.replace(/'/g,"''")}') IS NOT NULL AS found`)).rows[0].found!==!0)return;const u=(await n.query(`SELECT key, value FROM ${t} WHERE key IN ('schema_version', 'schema_min_reader')`)).rows,o=r(c=>{const i=u.find(f=>f.key===c);if(i===void 0)return;const d=Number(i.value);if(!Number.isSafeInteger(d))throw new Error(`canvas_meta.${c} is not an integer: ${String(i.value)}`);return d},"value"),s=o("schema_version");return s===void 0?void 0:{version:s,minReader:o("schema_min_reader")??s}}r(v,"readStoredVersions");async function l(n,e,t){const a=await v(n,t);if(a!==void 0&&a.minReader>e)throw new Error(`refusing to select runtime with canvas schema ${e}: the live database requires readers at version ${a.minReader} or above (it is at ${a.version}); install a descendant build instead`)}r(l,"assertCandidateMayOpen");export{A as CANVAS_SCHEMA_VERSION,l as assertCandidateMayOpen,_ as canvasDatabaseSchema,h as canvasSchemaDatabaseUrl,E as canvasSearchPathOptions,v as readStoredVersions};
|
|
@@ -108,8 +108,8 @@ export interface TeamRecord {
|
|
|
108
108
|
}
|
|
109
109
|
/** The `teams` a grant sync answer carried; absent (an unjoined install, an older directory) reads as none. */
|
|
110
110
|
export declare function teamsOf(value: unknown): TeamRecord[];
|
|
111
|
-
/** Replace the mirror with `grants
|
|
112
|
-
* `
|
|
111
|
+
/** Replace the mirror with `grants` and the `written_by = 'directory'` team `member` facts with `teams`, in one transaction. The
|
|
112
|
+
* ledger's `grant:<id>` limit rows follow from the `grants` trigger `crtr_grant_limit` (schema delta 002). */
|
|
113
113
|
export declare function replaceGrants(grants: readonly GrantRecord[], syncedAt: string, source: string, teams?: readonly TeamRecord[]): Promise<void>;
|
|
114
114
|
/** Bumped by every `replaceGrants`. A launch compiled against one generation
|
|
115
115
|
* and started under another may hold mounts the new mirror no longer grants. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var m=Object.defineProperty;var t=(e,r)=>m(e,"name",{value:r,configurable:!0});import{createHash as S}from"node:crypto";import{parseScope as T}from"@north-light/identity";import{openDb as
|
|
1
|
+
var m=Object.defineProperty;var t=(e,r)=>m(e,"name",{value:r,configurable:!0});import{createHash as S}from"node:crypto";import{parseScope as T}from"@north-light/identity";import{openDb as c,withCanvasWrite as O}from"../canvas/db.js";import{isUnsandboxedMachine as v}from"../runtime/unsandboxed.js";import{replaceWrittenBy as w,userSubject as x}from"../permissions/facts.js";import{GROUP_TYPE as R}from"../permissions/builtin.js";const A=["not_started","pending","ready","failed","unavailable"],N=["ultra","strong","medium","light"];function p(e){if(!Array.isArray(e))throw new TypeError("grant scopes must be a list");return e.map(r=>{if(!r||typeof r!="object"||typeof r.scope!="string"||!["always","ask"].includes(r.mode))throw new TypeError("invalid grant scope entry");return{scope:r.scope,mode:r.mode}})}t(p,"entries");function h(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{state:r,message:n,next:i,step:o}=e;return typeof r!="string"||!A.includes(r)||n!==void 0&&typeof n!="string"||o!==void 0&&typeof o!="string"||typeof i!="string"||!i?null:{state:r,...n===void 0?{}:{message:n},next:i,...o===void 0?{}:{step:o}}}t(h,"dependencyOf");function y(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const r={};for(const[n,i]of Object.entries(e)){const o=h(i);o&&(r[n]=o)}return r}t(y,"dependenciesOf");function b(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{base_url:r,rungs:n}=e;if(typeof r!="string"||!r||!n||typeof n!="object"||Array.isArray(n))return null;const i={};for(const o of N){const a=n[o];typeof a=="string"&&a&&(i[o]=a)}return Object.keys(i).length?{base_url:r.replace(/\/+$/,""),rungs:i}:null}t(b,"inferenceEndpointOf");function f(e){return{id:e.id,runtime:e.runtime,sub:e.sub,grantee:e.grantee,rev:e.rev,scopes:p(JSON.parse(e.scopes_json)),resolved_scopes:p(JSON.parse(e.resolved_scopes_json)),spend_cap:e.spend_cap,expires_at:e.expires_at,status:e.status,revoked_at:e.revoked_at,approved_at:e.approved_at,payer:e.payer,dependencies:y(JSON.parse(e.dependencies_json)),run_notice_url:e.run_notice_url??null,inference_endpoint:e.inference_endpoint_json?b(JSON.parse(e.inference_endpoint_json)):null,synced_at:e.synced_at}}t(f,"record");function u(e){if(!e||e.status!=="active")return null;try{return F(f(e))}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(u,"active");function l(e){return e.grantee!=="app:terminal"&&v()}t(l,"mirrorOnly");async function I(){return(await L()).filter(e=>!l(e))}t(I,"localGrants");async function L(){return(await(await c()).prepare("SELECT * FROM grants").all()).flatMap(e=>{try{return[f(e)]}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return[];throw r}})}t(L,"allGrants");function j(e){return e.expires_at!==null&&!(Date.parse(e.expires_at)>Date.now())}t(j,"grantExpired");function F(e){if(e.status!=="active"||j(e))return null;try{const r=e.resolved_scopes.map(({scope:n})=>T(n));return{...e,resolved:e.resolved_scopes.flatMap(({mode:n},i)=>n==="always"?[r[i]]:[])}}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(F,"activeRecord");function d(e){return Array.isArray(e)?`[${e.map(d).join(",")}]`:e&&typeof e=="object"?`{${Object.keys(e).sort().filter(r=>e[r]!==void 0).map(r=>`${JSON.stringify(r)}:${d(e[r])}`).join(",")}}`:JSON.stringify(e)??"null"}t(d,"canonicalJson");function U(e){const r={rev:e.rev,resolved_scopes:e.resolved_scopes,spend_cap:e.spend_cap,payer:e.payer,dependencies:e.dependencies??{},...e.inference_endpoint?{inference_endpoint:e.inference_endpoint}:{}};return S("sha256").update(d(r)).digest("base64url").slice(0,22)}t(U,"grantVersion");async function $(e){return!!await(await c()).prepare("SELECT 1 FROM grants WHERE id = ?").get(e)}t($,"hasGrant");async function P(e){return await(await c()).prepare("SELECT rev, status FROM grants WHERE id = ?").get(e)??null}t(P,"grantSyncState");async function V(e,r){return await(await c()).prepare("SELECT rev, status FROM grants WHERE grantee = ? AND sub = ?").get(e,r)??null}t(V,"grantSyncStateFor");async function M(e){const r=await(await c()).prepare("SELECT * FROM grants WHERE id = ?").get(e);if(!r)return null;try{return f(r)}catch(n){if(n instanceof TypeError||n instanceof SyntaxError)return null;throw n}}t(M,"grantRecord");async function B(e){return(e?(await M(e))?.payer:null)??"user"}t(B,"payerOfGrant");async function X(e){return u(await(await c()).prepare("SELECT * FROM grants WHERE id = ?").get(e))}t(X,"activeGrant");async function Y(e,r){return u(await(await c()).prepare("SELECT * FROM grants WHERE grantee = ? AND sub = ?").get(e,r))}t(Y,"grantFor");async function q(e){if(l({grantee:e}))return null;const r=await(await c()).prepare("SELECT * FROM grants WHERE grantee = ? AND status = 'active'").all(e);if(r.length>1)throw new Error(`multiple active grants for ${e}`);return u(r[0])}t(q,"activeGrantForApp");async function z(e){const r=await(await c()).prepare("SELECT run_notice_url AS url, sub, grantee FROM grants WHERE id = ? AND status = 'active'").get(e);return r&&r.url&&!l(r)?{url:r.url,sub:r.sub,grantee:r.grantee}:null}t(z,"runNoticeTarget");async function K(){const e=await(await c()).prepare("SELECT * FROM grants WHERE grantee = 'app:terminal' AND status = 'active'").all();if(e.length>1)throw new Error("multiple active terminal grants");return u(e[0])}t(K,"terminalGrant");async function Q(){return(await(await c()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value??null}t(Q,"grantsSource");async function Z(e){return(await(await c()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value===e}t(Z,"hasSyncedGrants");async function ee(e){return(await e.prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value!=="local"?0:(await e.prepare("DELETE FROM runtime_state WHERE key = 'grants_source'").run(),Number((await e.prepare("DELETE FROM grants").run()).changes))}t(ee,"dropLocalGrantMirror");function re(e){if(e===void 0)return[];if(!Array.isArray(e))throw new TypeError("invalid grant sync response");return e.map(r=>{const{team:n,members:i}=r??{};if(typeof n!="string"||!/^team:\S+$/.test(n)||!Array.isArray(i)||i.some(o=>typeof o!="string"||!/^user:[^\s*]+$/.test(o)))throw new TypeError("invalid grant sync response");return{team:n,members:i}})}t(re,"teamsOf");async function te(e,r,n,i=[]){const o=new Set;for(const a of e){if(o.has(a.id)||!a.id||!a.runtime||!a.sub||!a.grantee||!Number.isInteger(a.rev)||!["active","revoked","removed"].includes(a.status))throw new TypeError("invalid grant sync response");o.add(a.id),p(a.scopes),p(a.resolved_scopes)}await O(async a=>{await a.exec("DELETE FROM grants");const E=a.prepare(`INSERT INTO grants
|
|
2
2
|
(id, runtime, sub, grantee, rev, scopes_json, resolved_scopes_json, spend_cap, expires_at,
|
|
3
3
|
status, revoked_at, approved_at, payer, dependencies_json, run_notice_url, inference_endpoint_json, synced_at)
|
|
4
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);for(const
|
|
4
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);for(const s of e)await E.run(s.id,s.runtime,s.sub,s.grantee,s.rev,JSON.stringify(s.scopes),JSON.stringify(s.resolved_scopes),s.spend_cap,s.expires_at,s.status,s.revoked_at,s.approved_at,s.payer,JSON.stringify(y(s.dependencies)),s.run_notice_url??null,s.inference_endpoint?JSON.stringify(s.inference_endpoint):null,s.synced_at);await w(a,"directory",R,"member",i.flatMap(({team:s,members:_})=>_.map(g=>({resourceId:s,subject:x(g)})))),await a.prepare("INSERT INTO runtime_state (key, value) VALUES ('grants_source', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(n),await a.prepare("INSERT INTO runtime_state (key, value) VALUES ('grants_generation', '1') ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(runtime_state.value AS INTEGER) + 1 AS TEXT)").run()})}t(te,"replaceGrants");async function ne(){const e=await(await c()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_generation'").get();return e?Number(e.value):0}t(ne,"grantsGeneration");export{X as activeGrant,q as activeGrantForApp,F as activeRecord,L as allGrants,y as dependenciesOf,ee as dropLocalGrantMirror,j as grantExpired,Y as grantFor,M as grantRecord,P as grantSyncState,V as grantSyncStateFor,U as grantVersion,ne as grantsGeneration,Q as grantsSource,$ as hasGrant,Z as hasSyncedGrants,b as inferenceEndpointOf,I as localGrants,l as mirrorOnly,B as payerOfGrant,te as replaceGrants,z as runNoticeTarget,re as teamsOf,K as terminalGrant};
|
|
@@ -4,9 +4,8 @@ export declare const LEGACY_ID_SQL = "'legacy:' || u.grantee || ':' || u.month |
|
|
|
4
4
|
export interface BackfillResult {
|
|
5
5
|
migrated: boolean;
|
|
6
6
|
charges: number;
|
|
7
|
-
limits: number;
|
|
8
7
|
}
|
|
9
|
-
/** Copy `usage` into `charges` as legacy rows
|
|
8
|
+
/** Copy `usage` into `charges` as legacy rows and rebuild `charge_totals` from them, then
|
|
10
9
|
* verify per `(grantee, month, payer, source, served_model)` that usd and token sums equal the `usage` row's, null kept. A
|
|
11
10
|
* second call sees the marker and does nothing. A failed verification rolls the whole thing back and throws. */
|
|
12
11
|
export declare function backfillLedger(person: string): Promise<BackfillResult>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var
|
|
2
|
-
SELECT ${
|
|
1
|
+
var E=Object.defineProperty;var t=(u,e)=>E(u,"name",{value:e,configurable:!0});import{withCanvasWrite as i}from"../canvas/db.js";import{rebuildTotals as o}from"./totals.js";const r="ledger_migrated",a="'legacy:' || u.grantee || ':' || u.month || ':' || u.payer || ':' || u.source || ':' || u.served_model";async function p(u){return await i(async e=>{if(await e.prepare("SELECT value FROM canvas_meta WHERE key = ?").get(r)!==void 0)return{migrated:!1,charges:0};const n=await e.prepare(`INSERT INTO charges (id, request, at, month, kind, what, units, usd, payer, funding, sub, grantee, provider)
|
|
2
|
+
SELECT ${a},
|
|
3
3
|
NULL, u.month || '-01T00:00:00.000Z', u.month,
|
|
4
4
|
CASE WHEN u.source = 'model' THEN 'model' ELSE 'tool' END,
|
|
5
5
|
CASE WHEN u.source = 'model' THEN u.served_model ELSE substr(u.source, 10) END,
|
|
@@ -9,14 +9,11 @@ var s=Object.defineProperty;var u=(t,e)=>s(t,"name",{value:e,configurable:!0});i
|
|
|
9
9
|
CASE WHEN u.payer = 'app' THEN 'endpoint' WHEN u.source = 'model' THEN 'legacy' ELSE 'bill' END,
|
|
10
10
|
?::text, u.grantee,
|
|
11
11
|
CASE WHEN u.source = 'model' THEN NULL ELSE 'app:' || substr(u.source, 10) END
|
|
12
|
-
FROM usage u ON CONFLICT (id) DO NOTHING`).run(
|
|
13
|
-
|
|
14
|
-
FROM grants WHERE status = 'active' AND spend_cap IS NOT NULL AND COALESCE(payer, 'user') <> 'app' AND grantee <> 'app:terminal'
|
|
15
|
-
ON CONFLICT (id) DO NOTHING`).run(new Date().toISOString())).changes}u(c,"backfillLimits");async function d(t){const e=await t.prepare(`SELECT u.grantee, u.month, u.payer, u.source, u.served_model FROM usage u
|
|
16
|
-
LEFT JOIN charges c ON c.id = ${r}
|
|
12
|
+
FROM usage u ON CONFLICT (id) DO NOTHING`).run(u,u);return await o(e),await s(e),await e.prepare("INSERT INTO canvas_meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value").run(r,new Date().toISOString()),{migrated:!0,charges:n.changes}})}t(p,"backfillLedger");async function s(u){const e=await u.prepare(`SELECT u.grantee, u.month, u.payer, u.source, u.served_model FROM usage u
|
|
13
|
+
LEFT JOIN charges c ON c.id = ${a}
|
|
17
14
|
WHERE c.id IS NULL
|
|
18
15
|
OR c.usd IS DISTINCT FROM u.usd_equivalent::numeric(14,6)
|
|
19
16
|
OR (u.source = 'model' AND (COALESCE((c.units ->> 'input')::bigint, -1) <> u.input_tokens
|
|
20
17
|
OR COALESCE((c.units ->> 'output')::bigint, -1) <> u.output_tokens
|
|
21
18
|
OR COALESCE((c.units ->> 'cache_read')::bigint, -1) <> u.cached_tokens))
|
|
22
|
-
LIMIT 5`).all();if(e.length>0)throw new Error(`ledger backfill verification failed; first differing usage rows: ${JSON.stringify(e)}`)}
|
|
19
|
+
LIMIT 5`).all();if(e.length>0)throw new Error(`ledger backfill verification failed; first differing usage rows: ${JSON.stringify(e)}`)}t(s,"verify");export{r as LEDGER_MIGRATED_KEY,a as LEGACY_ID_SQL,p as backfillLedger};
|
|
@@ -3,7 +3,7 @@ export * from './months.js';
|
|
|
3
3
|
export { payerOf, type Payer, type PayerCall } from './payer.js';
|
|
4
4
|
export { writeCharge, receiptChargeId, takePct, chargeOf, type WriteResult } from './charges.js';
|
|
5
5
|
export { check, findRefusal, refusalError, refusedMark, userActionOf, type Candidate, type CallCandidate, type StartCandidate, type Refusal } from './check.js';
|
|
6
|
-
export { upsertLimit, deleteLimit,
|
|
6
|
+
export { upsertLimit, deleteLimit, listLimits } from './limits.js';
|
|
7
7
|
export { usageFor, listCharges, monthTotals, assertReference, type ChargePage, type ListFilter, type MonthTotals } from './read.js';
|
|
8
8
|
export { backfillLedger, LEDGER_MIGRATED_KEY, type BackfillResult } from './backfill.js';
|
|
9
9
|
export { rebuildTotals, rebuildTotalsFor } from './totals.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./types.js";export*from"./months.js";import{payerOf as
|
|
1
|
+
export*from"./types.js";export*from"./months.js";import{payerOf as f}from"./payer.js";import{writeCharge as i,receiptChargeId as s,takePct as m,chargeOf as l}from"./charges.js";import{check as x,findRefusal as c,refusalError as u,refusedMark as d,userActionOf as g}from"./check.js";import{upsertLimit as E,deleteLimit as L,listLimits as k}from"./limits.js";import{usageFor as R,listCharges as T,monthTotals as b,assertReference as C}from"./read.js";import{backfillLedger as A,LEDGER_MIGRATED_KEY as D}from"./backfill.js";import{rebuildTotals as G,rebuildTotalsFor as I}from"./totals.js";export{D as LEDGER_MIGRATED_KEY,C as assertReference,A as backfillLedger,l as chargeOf,x as check,L as deleteLimit,c as findRefusal,T as listCharges,k as listLimits,b as monthTotals,f as payerOf,G as rebuildTotals,I as rebuildTotalsFor,s as receiptChargeId,u as refusalError,d as refusedMark,m as takePct,E as upsertLimit,R as usageFor,g as userActionOf,i as writeCharge};
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { type CanvasDb } from '../canvas/db.js';
|
|
2
1
|
import type { Limit, LimitInput, LimitSource } from './types.js';
|
|
3
|
-
export declare const grantLimitId: (grantId: string) => string;
|
|
4
2
|
/** Insert or replace one limit row by id. */
|
|
5
3
|
export declare function upsertLimit(input: LimitInput): Promise<Limit>;
|
|
6
4
|
/** Delete one limit row; true when it existed. */
|
|
7
5
|
export declare function deleteLimit(id: string): Promise<boolean>;
|
|
8
|
-
/** A grant's limit rows in one transaction: delete every `grant_sync` row of this grant, then write `rows`. The first row's id
|
|
9
|
-
* is `grant:<grant id>`, later ones `grant:<grant id>#<n>`. Only `grant_sync` rows are touched. */
|
|
10
|
-
export declare function replaceGrantLimits(grantId: string, rows: ReadonlyArray<Omit<LimitInput, 'id' | 'source'>>, db?: CanvasDb): Promise<void>;
|
|
11
6
|
/** Limit rows, optionally of one source or one payer, in id order. */
|
|
12
7
|
export declare function listLimits(filter?: {
|
|
13
8
|
source?: LimitSource;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var d=Object.defineProperty;var t=(r,e)=>d(r,"name",{value:e,configurable:!0});import{openDb as i,withCanvasWrite as u}from"../canvas/db.js";const a="id, payer, funding, grantee, provider, sub, usd_per_month, source, updated_at";async function o(r){if(!Number.isFinite(r.usd_per_month)||r.usd_per_month<0)throw new RangeError("usd_per_month must be a number of at least 0");const e={funding:null,grantee:null,provider:null,sub:null,...r,updated_at:new Date().toISOString()};return await u(async n=>{await n.prepare(`INSERT INTO limits (${a}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2
2
|
ON CONFLICT (id) DO UPDATE SET payer = excluded.payer, funding = excluded.funding, grantee = excluded.grantee,
|
|
3
|
-
provider = excluded.provider, sub = excluded.sub, usd_per_month = excluded.usd_per_month, source = excluded.source, updated_at = excluded.updated_at`).run(e.id,e.payer,e.funding,e.grantee,e.provider,e.sub,e.usd_per_month,e.source,e.updated_at)}),e}
|
|
3
|
+
provider = excluded.provider, sub = excluded.sub, usd_per_month = excluded.usd_per_month, source = excluded.source, updated_at = excluded.updated_at`).run(e.id,e.payer,e.funding,e.grantee,e.provider,e.sub,e.usd_per_month,e.source,e.updated_at)}),e}t(o,"upsertLimit");async function c(r){return await u(async e=>(await e.prepare("DELETE FROM limits WHERE id = ?").run(r)).changes>0)}t(c,"deleteLimit");async function l(r={}){const e=[],n=[];return r.source!==void 0&&(e.push("source = ?"),n.push(r.source)),r.payer!==void 0&&(e.push("payer = ?"),n.push(r.payer)),await(await i()).prepareUncached(`SELECT ${a} FROM limits${e.length?` WHERE ${e.join(" AND ")}`:""} ORDER BY id`).all(...n)}t(l,"listLimits");export{c as deleteLimit,l as listLimits,o as upsertLimit};
|
|
@@ -12,7 +12,8 @@ export declare function addToTotals(db: CanvasDb, charge: {
|
|
|
12
12
|
provider: string | null;
|
|
13
13
|
usd: number | null;
|
|
14
14
|
}): Promise<void>;
|
|
15
|
-
/** Rebuild the whole rollup from `charges
|
|
15
|
+
/** Rebuild the whole rollup from `charges`, `billed_usd` with `usd`: a rebuild that dropped it would zero the billed totals
|
|
16
|
+
* 002's trigger keeps, and an older build that may still open the database must not do that either. */
|
|
16
17
|
export declare function rebuildTotals(db: CanvasDb): Promise<void>;
|
|
17
18
|
/** Delete the rollup rows that name `a` or `b` as payer, sub or grantee and rebuild them from `charges`, so two rows that
|
|
18
19
|
* now share a key are summed once. For person substitution, in the same transaction as the rename. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
2
|
-
ON CONFLICT (month, payer, funding, sub, grantee, provider) DO UPDATE SET usd = charge_totals.usd + excluded.usd`).run(e.month,e.payer,e.funding,a(e.sub),e.grantee,a(e.provider),e.usd??0)}t(
|
|
3
|
-
FROM charges WHERE refused IS NULL AND kind <> 'take'`,
|
|
4
|
-
AND (payer IN (?, ?) OR sub IN (?, ?) OR grantee IN (?, ?)) ${
|
|
1
|
+
var o=Object.defineProperty;var t=(r,e)=>o(r,"name",{value:e,configurable:!0});const a=t(r=>r??"","absent");async function E(r,e){await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2
|
+
ON CONFLICT (month, payer, funding, sub, grantee, provider) DO UPDATE SET usd = charge_totals.usd + excluded.usd`).run(e.month,e.payer,e.funding,a(e.sub),e.grantee,a(e.provider),e.usd??0)}t(E,"addToTotals");const u=`SELECT month, payer, funding, COALESCE(sub, ''), grantee, COALESCE(provider, ''), COALESCE(SUM(usd), 0), COALESCE(SUM(billed_usd), 0)
|
|
3
|
+
FROM charges WHERE refused IS NULL AND kind <> 'take'`,s="GROUP BY month, payer, funding, COALESCE(sub, ''), grantee, COALESCE(provider, '')";async function p(r){await r.prepare("DELETE FROM charge_totals").run(),await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd, billed_usd) ${u} ${s}`).run()}t(p,"rebuildTotals");async function i(r,e,n){await r.prepare("DELETE FROM charge_totals WHERE payer IN (?, ?) OR sub IN (?, ?) OR grantee IN (?, ?)").run(e,n,e,n,e,n),await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd, billed_usd) ${u}
|
|
4
|
+
AND (payer IN (?, ?) OR sub IN (?, ?) OR grantee IN (?, ?)) ${s}`).run(e,n,e,n,e,n)}t(i,"rebuildTotalsFor");export{a as absent,E as addToTotals,p as rebuildTotals,i as rebuildTotalsFor};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.555",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
},
|
|
142
142
|
"license": "UNLICENSED",
|
|
143
143
|
"dependencies": {
|
|
144
|
-
"@north-light/identity": "0.3.
|
|
144
|
+
"@north-light/identity": "0.3.555",
|
|
145
145
|
"@earendil-works/pi-agent-core": "1.1.0",
|
|
146
146
|
"@earendil-works/pi-ai": "1.1.0",
|
|
147
147
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.555",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/cli",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.555",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
12
|
"workspaces": [
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@milkdown/preset-gfm": "^7.22.3",
|
|
25
25
|
"@milkdown/prose": "^7.22.3",
|
|
26
26
|
"@milkdown/utils": "^7.22.3",
|
|
27
|
-
"@north-light/identity": "0.3.
|
|
27
|
+
"@north-light/identity": "0.3.555",
|
|
28
28
|
"cron-parser": "^5.6.0",
|
|
29
29
|
"esbuild": "^0.27.7",
|
|
30
30
|
"jose": "^6.2.12",
|
|
@@ -12455,17 +12455,17 @@
|
|
|
12455
12455
|
},
|
|
12456
12456
|
"packages/crouter-api": {
|
|
12457
12457
|
"name": "@north-light/api",
|
|
12458
|
-
"version": "0.3.
|
|
12458
|
+
"version": "0.3.555",
|
|
12459
12459
|
"license": "UNLICENSED"
|
|
12460
12460
|
},
|
|
12461
12461
|
"packages/crouter-env-docker": {
|
|
12462
12462
|
"name": "@north-light/env-docker",
|
|
12463
|
-
"version": "0.3.
|
|
12463
|
+
"version": "0.3.555",
|
|
12464
12464
|
"license": "UNLICENSED"
|
|
12465
12465
|
},
|
|
12466
12466
|
"packages/crouter-identity": {
|
|
12467
12467
|
"name": "@north-light/identity",
|
|
12468
|
-
"version": "0.3.
|
|
12468
|
+
"version": "0.3.555",
|
|
12469
12469
|
"license": "UNLICENSED",
|
|
12470
12470
|
"dependencies": {
|
|
12471
12471
|
"jose": "^6.2.1"
|
|
@@ -12473,21 +12473,21 @@
|
|
|
12473
12473
|
},
|
|
12474
12474
|
"packages/crouter-plugin": {
|
|
12475
12475
|
"name": "@north-light/plugin",
|
|
12476
|
-
"version": "0.3.
|
|
12476
|
+
"version": "0.3.555",
|
|
12477
12477
|
"license": "UNLICENSED",
|
|
12478
12478
|
"dependencies": {
|
|
12479
|
-
"@north-light/api": "0.3.
|
|
12480
|
-
"@north-light/identity": "0.3.
|
|
12479
|
+
"@north-light/api": "0.3.555",
|
|
12480
|
+
"@north-light/identity": "0.3.555"
|
|
12481
12481
|
}
|
|
12482
12482
|
},
|
|
12483
12483
|
"packages/crouter-sdk": {
|
|
12484
12484
|
"name": "@north-light/sdk",
|
|
12485
|
-
"version": "0.3.
|
|
12485
|
+
"version": "0.3.555",
|
|
12486
12486
|
"license": "UNLICENSED",
|
|
12487
12487
|
"dependencies": {
|
|
12488
|
-
"@north-light/api": "0.3.
|
|
12489
|
-
"@north-light/identity": "0.3.
|
|
12490
|
-
"@north-light/plugin": "0.3.
|
|
12488
|
+
"@north-light/api": "0.3.555",
|
|
12489
|
+
"@north-light/identity": "0.3.555",
|
|
12490
|
+
"@north-light/plugin": "0.3.555",
|
|
12491
12491
|
"jose": "^6.2.1"
|
|
12492
12492
|
},
|
|
12493
12493
|
"bin": {
|