@bongos/core 1.24.14 → 1.24.15
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/.bongos-core.json +296 -136
- package/.claude/skills/blocker-review/SKILL.md +2 -0
- package/.claude/skills/session-board/SKILL.md +29 -0
- package/.claude/skills/strand-fix/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +14 -0
- package/clients/bongos-client/index.cjs +14 -0
- package/clients/bongos-client/index.d.ts +23 -2
- package/clients/bongos-client/index.mjs +14 -0
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +4 -0
- package/docs/api/openapi.json +369 -6
- package/docs/api-reference.md +19 -4
- package/docs/architecture.md +6 -0
- package/docs/copy-inventory.md +138 -123
- package/docs/copy-registry.json +296 -171
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +2 -2
- package/docs/page-inventory.json +6 -1
- package/docs/page-readings.json +1040 -1021
- package/migrations/core_289_task_short_name.sql +34 -0
- package/modules/ai-studio/db.js +10 -0
- package/modules/ai-studio/gemini.js +73 -5
- package/modules/ai-studio/openrouter.js +17 -2
- package/modules/ai-studio/routes/ai-studio.js +7 -1
- package/modules/ai-studio/studio.js +8 -0
- package/modules/builder-settings/checkout-freshness.js +71 -0
- package/modules/builder-settings/routes/settings.js +26 -0
- package/modules/builder-settings/settings.js +2 -0
- package/modules/economy/migrations/economy_002_project_account.sql +83 -0
- package/modules/economy/module.json +1 -1
- package/modules/economy/project-account.js +166 -0
- package/modules/economy/routes/project-account.js +40 -0
- package/modules/hall-ui/public/ai-studio.css +3 -0
- package/modules/hall-ui/public/ai-studio.html +2 -1
- package/modules/hall-ui/public/ai-studio.js +72 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/github-chip.js +85 -0
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-item.js +10 -3
- package/modules/hall-ui/public/market-watch-item.states.json +41 -0
- package/modules/hall-ui/public/market-watch-lib.js +36 -5
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch-settings.html +1 -1
- package/modules/hall-ui/public/market-watch.css +5 -0
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/my-agents.js +3 -0
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +22 -22
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +28 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/blockers.js +13 -7
- package/modules/lifecycle/db-claim-reads.js +1 -1
- package/modules/lifecycle/db-ship.js +2 -2
- package/modules/lifecycle/db-tasks.js +4 -4
- package/modules/lifecycle/migrations/lifecycle_019_pay_type_gates.sql +50 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/pay-type-gates.js +180 -0
- package/modules/lifecycle/publish-reconciler.js +31 -1
- package/modules/lifecycle/rebase-gate.js +3 -2
- package/modules/lifecycle/routes/claims.js +3 -2
- package/modules/lifecycle/routes/pay-type-gates.js +59 -0
- package/modules/lifecycle/routes/project-settings.js +4 -0
- package/modules/lifecycle/routes/task-write-routes.js +43 -0
- package/modules/lifecycle/ship-card.js +4 -1
- package/modules/lifecycle/task-name-db.js +55 -0
- package/modules/lifecycle/task-name.js +140 -0
- package/modules/market-watch/db.js +10 -1
- package/modules/market-watch/migrations/market_watch_004_entry_corrections.sql +20 -0
- package/modules/market-watch/pages.js +20 -1
- package/modules/market-watch/proposals.js +57 -7
- package/modules/market-watch/routes/pages.js +2 -0
- package/modules/market-watch/routes/rules.js +4 -0
- package/modules/platform-identity/terms-acceptance.js +9 -9
- package/modules/public-landing/public/account-google.states.json +14 -0
- package/modules/public-landing/public/assets/cosmos.css +12 -0
- package/modules/public-landing/public/assets/hub-bar.js +44 -1
- package/modules/public-landing/public/privacy.html +5 -1
- package/modules/public-landing/public/projects-google.states.json +14 -0
- package/modules/public-landing/public/projects.html +39 -0
- package/modules/public-landing/public/terms.html +12 -2
- package/modules/ui-design/kit/fixtures/me-google.json +10 -0
- package/modules/ui-design/kit/fixtures/me-sign-in-methods-google.json +4 -0
- package/modules/ui-design/kit/serve.js +7 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +49 -1
- package/scripts/bongos/claim.js +4 -2
- package/scripts/bongos/land-watch.js +1 -0
- package/scripts/bongos/report-checkout.js +30 -0
- package/scripts/bongos/run-unit-tests.js +11 -0
- package/scripts/bongos/sessions-board.js +264 -0
- package/scripts/bongos/ship-finish.js +1 -0
- package/scripts/bongos/ship-flow.js +5 -2
- package/scripts/bongos/strand-lease.js +5 -3
- package/scripts/bongos/strand-watch.js +38 -7
- package/scripts/gds/report-checkout.js +7 -0
- package/scripts/gds/sessions-board.js +7 -0
- package/src/bongos/agent-standing-rules.js +13 -3
- package/src/bongos/routes/me.js +7 -1
- package/src/module-api.js +1 -1
- package/tests/agent_standing_rules.mjs +18 -1
- package/tests/ai_studio_billing.mjs +13 -0
- package/tests/ai_studio_gemini_models.mjs +81 -0
- package/tests/ai_studio_requests.mjs +24 -0
- package/tests/blocker_detail_hydration.mjs +5 -3
- package/tests/blocker_tasks_named_db.mjs +74 -0
- package/tests/claim_gate_ci_unblock.mjs +1 -1
- package/tests/github_chip.mjs +217 -0
- package/tests/hall_ai_studio.mjs +129 -5
- package/tests/host_topology_skips.mjs +1 -0
- package/tests/hub_account_page.mjs +3 -2
- package/tests/landing_legal_pages.mjs +3 -2
- package/tests/market_watch_data_db.mjs +6 -0
- package/tests/market_watch_pages.mjs +87 -2
- package/tests/market_watch_rules.mjs +101 -5
- package/tests/market_watch_rules_db.mjs +42 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/pay_terms.mjs +8 -0
- package/tests/pay_type_gates.mjs +234 -0
- package/tests/pay_type_gates_db.mjs +84 -0
- package/tests/project_account.mjs +125 -0
- package/tests/project_account_db.mjs +97 -0
- package/tests/self_healing_start.mjs +162 -0
- package/tests/sessions_board.mjs +153 -0
- package/tests/stop_refcheck.mjs +40 -0
- package/tests/task_name.mjs +237 -0
- package/tests/task_name_db.mjs +79 -0
- package/tests/terms_acceptance.mjs +25 -3
- package/tests/terms_grandfather_db.mjs +11 -4
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- core_289 — every task gets a short name of 2-3 plain words (task 1004958).
|
|
2
|
+
--
|
|
3
|
+
-- A session that mentions "task 1004745" tells the person nothing; "task 1004745 -
|
|
4
|
+
-- payout approvals" does. The name is stored so it is the same on every surface.
|
|
5
|
+
-- modules/lifecycle/task-name.js derives it from the title (createTask writes it on
|
|
6
|
+
-- every new task; the publish-reconciler's hourly backfill names the rows created
|
|
7
|
+
-- before this), and the task's author may change it through PATCH /tasks/:id.
|
|
8
|
+
--
|
|
9
|
+
-- The CHECK is the trust boundary, not decoration: startup notes inject text into
|
|
10
|
+
-- every session, so a name is held to at most three lowercase words of [a-z0-9-],
|
|
11
|
+
-- 40 characters in all — a topic and nothing else. task-name.js NAME_RE is the
|
|
12
|
+
-- same pattern; tests/task_name.mjs holds the two equal.
|
|
13
|
+
--
|
|
14
|
+
-- Additive and nullable with no default, so a rolled-back release that never names
|
|
15
|
+
-- the column keeps working, and a row not yet backfilled simply prints "task N".
|
|
16
|
+
|
|
17
|
+
BEGIN;
|
|
18
|
+
|
|
19
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS short_name text;
|
|
20
|
+
ALTER TABLE tasks DROP CONSTRAINT IF EXISTS tasks_short_name_shape;
|
|
21
|
+
ALTER TABLE tasks ADD CONSTRAINT tasks_short_name_shape
|
|
22
|
+
CHECK (short_name IS NULL OR (
|
|
23
|
+
char_length(short_name) <= 40
|
|
24
|
+
AND short_name ~ '^[a-z0-9]+(-[a-z0-9]+)*( [a-z0-9]+(-[a-z0-9]+)*){0,2}$'
|
|
25
|
+
));
|
|
26
|
+
-- The hourly backfill reads only unnamed rows. Once the first pass drains, that is a
|
|
27
|
+
-- handful of titles with no topic word, so a partial index keeps every later pass
|
|
28
|
+
-- a read of those few rows rather than a walk of the whole table.
|
|
29
|
+
CREATE INDEX IF NOT EXISTS tasks_short_name_missing_idx ON tasks (id) WHERE short_name IS NULL;
|
|
30
|
+
|
|
31
|
+
COMMIT;
|
|
32
|
+
|
|
33
|
+
INSERT INTO schema_migrations (version) VALUES ('core_289_task_short_name')
|
|
34
|
+
ON CONFLICT (version) DO NOTHING;
|
package/modules/ai-studio/db.js
CHANGED
|
@@ -166,7 +166,17 @@ async function requestUploads(requestId, executor) {
|
|
|
166
166
|
return rows;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// ---- age (provider terms: AI providers serve adults only) -----------------
|
|
170
|
+
|
|
171
|
+
// The latest age tier recorded for the builder (payout gates' record), or null.
|
|
172
|
+
async function latestAgeTier(builderId, executor) {
|
|
173
|
+
const { rows } = await exec(executor)(
|
|
174
|
+
'SELECT tier FROM payout_age_records WHERE builder_id = $1 ORDER BY id DESC LIMIT 1', [builderId]);
|
|
175
|
+
return rows[0] ? rows[0].tier : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
169
178
|
module.exports = {
|
|
179
|
+
latestAgeTier,
|
|
170
180
|
listKeyMeta, getKeyEnc, setKey, clearKey, appendLedger, recentLedger,
|
|
171
181
|
addUpload, createRequest, markRunning, finishRequest, addOutput, getRequest, listRequests, getFile, requestUploads, countActiveRequests,
|
|
172
182
|
};
|
|
@@ -13,17 +13,71 @@
|
|
|
13
13
|
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
|
+
const crypto = require('node:crypto');
|
|
16
17
|
const { ProviderError } = require('./openrouter');
|
|
17
18
|
|
|
18
19
|
const BASE = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
19
20
|
const MODELS = { reply: 'gemini-2.5-pro', images: 'gemini-2.5-flash-image' };
|
|
20
21
|
const TIMEOUT_MS = 120000;
|
|
22
|
+
const LIST_TIMEOUT_MS = 15000;
|
|
23
|
+
|
|
24
|
+
// Google retires models on short notice, and a retired one answers 404. When
|
|
25
|
+
// that happens we ask the key which models it can use and take the newest one
|
|
26
|
+
// of the same kind, so a retirement never breaks the studio. The pick is
|
|
27
|
+
// remembered per key (under a hash, never the key itself), because two keys
|
|
28
|
+
// can be allowed different models.
|
|
29
|
+
const resolved = new Map();
|
|
30
|
+
const pickSlot = (key, kind) => `${crypto.createHash('sha256').update(String(key)).digest('hex').slice(0, 16)}:${kind}`;
|
|
21
31
|
|
|
22
32
|
function parts(text, files) {
|
|
23
33
|
return [{ text }, ...(files || []).map((f) => ({ inline_data: { mime_type: f.content_type, data: f.base64 } }))];
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
// Google's own reason, folded into the status: one line of printable text,
|
|
37
|
+
// anything shaped like a Google key blanked, at most 160 characters.
|
|
38
|
+
function googleReason(json) {
|
|
39
|
+
const m = json && json.error && typeof json.error.message === 'string' ? json.error.message : '';
|
|
40
|
+
const clean = m.replace(/AIza[\w-]{10,}/g, '[key]').replace(/[^\x20-\x7e]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
41
|
+
return clean ? `: ${clean.replace(/\.$/, '')}` : '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Higher is better: stable over preview, newer version first.
|
|
45
|
+
function versionOf(name) {
|
|
46
|
+
const m = /gemini-(\d+)(?:\.(\d+))?/.exec(name);
|
|
47
|
+
return m ? Number(m[1]) * 100 + Number(m[2] || 0) : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Choose the best model of a kind from a ListModels answer.
|
|
51
|
+
function pickModel(kind, models) {
|
|
52
|
+
const usable = (models || []).filter((m) => m && typeof m.name === 'string'
|
|
53
|
+
&& Array.isArray(m.supportedGenerationMethods) && m.supportedGenerationMethods.includes('generateContent'))
|
|
54
|
+
.map((m) => m.name.replace(/^models\//, ''))
|
|
55
|
+
.filter((n) => /^gemini-/.test(n));
|
|
56
|
+
const fits = kind === 'images'
|
|
57
|
+
? usable.filter((n) => /image/.test(n))
|
|
58
|
+
: usable.filter((n) => /-(pro|flash)\b/.test(n) && !/(image|tts|audio|live|embed|lite|robotics|computer)/.test(n));
|
|
59
|
+
const score = (n) => (/preview|exp/.test(n) ? 0 : 100000)
|
|
60
|
+
+ versionOf(n) * 10
|
|
61
|
+
+ (kind === 'reply' && /-pro\b/.test(n) ? 2 : 0)
|
|
62
|
+
+ (kind === 'images' && !/lite/.test(n) ? 1 : 0);
|
|
63
|
+
return fits.sort((a, b) => score(b) - score(a) || a.localeCompare(b))[0] || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function listModels(fetchImpl, key) {
|
|
67
|
+
const ctl = new AbortController();
|
|
68
|
+
const timer = setTimeout(() => ctl.abort(), LIST_TIMEOUT_MS);
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetchImpl(`${BASE}?pageSize=1000`, { method: 'GET', headers: { 'x-goog-api-key': key }, signal: ctl.signal });
|
|
71
|
+
const json = await res.json();
|
|
72
|
+
return res.ok && json && Array.isArray(json.models) ? json.models : [];
|
|
73
|
+
} catch (_) {
|
|
74
|
+
return [];
|
|
75
|
+
} finally {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function call(fetchImpl, key, model, body) {
|
|
27
81
|
const ctl = new AbortController();
|
|
28
82
|
const timer = setTimeout(() => ctl.abort(), TIMEOUT_MS);
|
|
29
83
|
let res;
|
|
@@ -41,15 +95,29 @@ async function post(fetchImpl, key, model, body) {
|
|
|
41
95
|
}
|
|
42
96
|
let json = null;
|
|
43
97
|
try { json = await res.json(); } catch (_) { /* fall through */ }
|
|
98
|
+
return { res, json };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function post(fetchImpl, key, kind, body) {
|
|
102
|
+
const slot = pickSlot(key, kind);
|
|
103
|
+
const model = resolved.get(slot) || MODELS[kind];
|
|
104
|
+
let { res, json } = await call(fetchImpl, key, model, body);
|
|
105
|
+
if (res.status === 404) {
|
|
106
|
+
const next = pickModel(kind, await listModels(fetchImpl, key));
|
|
107
|
+
if (next && next !== model) {
|
|
108
|
+
({ res, json } = await call(fetchImpl, key, next, body));
|
|
109
|
+
if (res.ok) resolved.set(slot, next);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
44
112
|
if (!res.ok || !json || json.error) {
|
|
45
|
-
throw new ProviderError('provider_error', `Google refused the request (HTTP ${res.status}). Check the key in Settings.`);
|
|
113
|
+
throw new ProviderError('provider_error', `Google refused the request (HTTP ${res.status}${googleReason(json)}). Check the key in Settings.`);
|
|
46
114
|
}
|
|
47
115
|
const cand = json.candidates && json.candidates[0];
|
|
48
116
|
return (cand && cand.content && Array.isArray(cand.content.parts)) ? cand.content.parts : [];
|
|
49
117
|
}
|
|
50
118
|
|
|
51
119
|
async function reply({ fetchImpl, key, system, prompt, files }) {
|
|
52
|
-
const out = await post(fetchImpl, key,
|
|
120
|
+
const out = await post(fetchImpl, key, 'reply', {
|
|
53
121
|
systemInstruction: { parts: [{ text: system }] },
|
|
54
122
|
contents: [{ role: 'user', parts: parts(prompt, files) }],
|
|
55
123
|
});
|
|
@@ -59,7 +127,7 @@ async function reply({ fetchImpl, key, system, prompt, files }) {
|
|
|
59
127
|
}
|
|
60
128
|
|
|
61
129
|
async function images({ fetchImpl, key, system, brief, files }) {
|
|
62
|
-
const out = await post(fetchImpl, key,
|
|
130
|
+
const out = await post(fetchImpl, key, 'images', {
|
|
63
131
|
systemInstruction: { parts: [{ text: system }] },
|
|
64
132
|
contents: [{ role: 'user', parts: parts(brief, (files || []).filter((f) => f.content_type !== 'application/pdf')) }],
|
|
65
133
|
generationConfig: { responseModalities: ['TEXT', 'IMAGE'] },
|
|
@@ -72,4 +140,4 @@ async function images({ fetchImpl, key, system, brief, files }) {
|
|
|
72
140
|
return { images: list, costMicro: null };
|
|
73
141
|
}
|
|
74
142
|
|
|
75
|
-
module.exports = { BASE, reply, images };
|
|
143
|
+
module.exports = { BASE, reply, images, pickModel, googleReason, _resolved: resolved };
|
|
@@ -17,6 +17,21 @@ const REPLY_MODEL = 'google/gemini-2.5-pro';
|
|
|
17
17
|
const IMAGE_MODEL = 'google/gemini-2.5-flash-image';
|
|
18
18
|
const TIMEOUT_MS = 120000;
|
|
19
19
|
|
|
20
|
+
// What OpenRouter may route a call to (provider terms review, 2026-10-30): never
|
|
21
|
+
// a provider that keeps or trains on prompts, only zero-retention endpoints, and
|
|
22
|
+
// optionally only an allow-list of providers whose terms we have checked.
|
|
23
|
+
// OPENROUTER_ZDR=off allow endpoints that are not zero-retention
|
|
24
|
+
// OPENROUTER_ALLOWED_PROVIDERS=a,b route only to these providers
|
|
25
|
+
// OPENROUTER_DISTILLABLE_ONLY=on only models whose makers allow training on outputs
|
|
26
|
+
function providerPolicy(env = process.env) {
|
|
27
|
+
const policy = { data_collection: 'deny' };
|
|
28
|
+
if (env.OPENROUTER_ZDR !== 'off') policy.zdr = true;
|
|
29
|
+
const only = String(env.OPENROUTER_ALLOWED_PROVIDERS || '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
30
|
+
if (only.length) policy.only = only;
|
|
31
|
+
if (env.OPENROUTER_DISTILLABLE_ONLY === 'on') policy.enforce_distillable_text = true;
|
|
32
|
+
return policy;
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
class ProviderError extends Error {
|
|
21
36
|
constructor(code, message) { super(message); this.code = code; }
|
|
22
37
|
}
|
|
@@ -36,7 +51,7 @@ async function post(fetchImpl, key, body) {
|
|
|
36
51
|
res = await fetchImpl(ENDPOINT, {
|
|
37
52
|
method: 'POST',
|
|
38
53
|
headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json', 'x-title': 'Cloud Bongos AI studio' },
|
|
39
|
-
body: JSON.stringify({ ...body, usage: { include: true } }),
|
|
54
|
+
body: JSON.stringify({ ...body, provider: providerPolicy(), usage: { include: true } }),
|
|
40
55
|
signal: ctl.signal,
|
|
41
56
|
});
|
|
42
57
|
} catch (err) {
|
|
@@ -87,4 +102,4 @@ async function images({ fetchImpl, key, system, brief, files }) {
|
|
|
87
102
|
return { images: list, costMicro: out.costMicro };
|
|
88
103
|
}
|
|
89
104
|
|
|
90
|
-
module.exports = { ENDPOINT, ProviderError, reply, images };
|
|
105
|
+
module.exports = { ENDPOINT, ProviderError, providerPolicy, reply, images };
|
|
@@ -69,11 +69,13 @@ function runInBackground(request, choice) {
|
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const ADULTS_ONLY = 'AI Studio is for people 18 and over. Record your age in Settings to use it.';
|
|
73
|
+
|
|
72
74
|
module.exports = function buildAiStudioRouter() {
|
|
73
75
|
const router = express.Router();
|
|
74
76
|
|
|
75
77
|
router.get('/ai-studio/status', api.requireBuilder, api.asyncHandler('GET /ai-studio/status', async (req, res) => {
|
|
76
|
-
res.json({ ok: true, module: 'ai-studio' });
|
|
78
|
+
res.json({ ok: true, module: 'ai-studio', terms: S.PROVIDER_TERMS, adult: S.isAdult(await db.latestAgeTier(req.builder.id)) });
|
|
77
79
|
}));
|
|
78
80
|
|
|
79
81
|
router.get('/ai-studio/keys', api.requireBuilder, api.asyncHandler('GET /ai-studio/keys', async (req, res) => {
|
|
@@ -138,6 +140,7 @@ module.exports = function buildAiStudioRouter() {
|
|
|
138
140
|
// amount asked for arrives whole after the processor's fee (ADR 0392 d.4).
|
|
139
141
|
router.post('/ai-studio/topup', api.requireBuilder, api.asyncHandler('POST /ai-studio/topup', async (req, res) => {
|
|
140
142
|
if (api.validateOrRespond(req, res, { amount_usd: { required: true, type: 'number' } })) return;
|
|
143
|
+
if (!S.isAdult(await db.latestAgeTier(req.builder.id))) return res.fail('adults_only', { status: 403, message: ADULTS_ONLY });
|
|
141
144
|
const b = payments.backend();
|
|
142
145
|
if (b.name === 'none') return res.fail('payments_not_configured', { status: 409, message: 'Paying for generations is not set up here yet.' });
|
|
143
146
|
let quote;
|
|
@@ -152,6 +155,8 @@ module.exports = function buildAiStudioRouter() {
|
|
|
152
155
|
|
|
153
156
|
// One file per call, so a request body stays small; the global 64 KB parser
|
|
154
157
|
// skips this path (src/bongos/routes.js) and this parser takes it.
|
|
158
|
+
// Not adults-gated on purpose: an upload is stored here and reaches no AI
|
|
159
|
+
// company until a request sends it, and /ai-studio/requests is gated.
|
|
155
160
|
router.post('/ai-studio/uploads', api.requireBuilder, express.json({ limit: S.UPLOAD_BODY_LIMIT }), api.asyncHandler('POST /ai-studio/uploads', async (req, res) => {
|
|
156
161
|
if (api.validateOrRespond(req, res, {
|
|
157
162
|
content_type: { required: true, type: 'string', maxLength: 40 },
|
|
@@ -170,6 +175,7 @@ module.exports = function buildAiStudioRouter() {
|
|
|
170
175
|
file_ids: { required: true, type: 'array', maxItems: S.MAX_FILES },
|
|
171
176
|
pay_with: { type: 'string', enum: ['own', 'ours'] },
|
|
172
177
|
})) return;
|
|
178
|
+
if (!S.isAdult(await db.latestAgeTier(req.builder.id))) return res.fail('adults_only', { status: 403, message: ADULTS_ONLY });
|
|
173
179
|
const checked = S.checkRequest(req.body);
|
|
174
180
|
if (checked.error) return res.fail(checked.error, { status: 400, message: S.MESSAGES[checked.error] });
|
|
175
181
|
const stored = (await db.listKeyMeta(req.builder.id)).map((k) => k.provider);
|
|
@@ -115,7 +115,15 @@ function imageBrief(prompt, replyText) {
|
|
|
115
115
|
return `Create finished artwork for this request from the project's artist. Follow the attached sketch's composition.\n\nThe artist asked:\n${prompt}\n\nArt direction:\n${clip(replyText, 1500)}`;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// Shown in the Studio, and agreed to by sending a request (OpenRouter terms 5.1:
|
|
119
|
+
// the people we serve must follow each model maker's terms).
|
|
120
|
+
const PROVIDER_TERMS = 'AI Studio is for people 18 and over. By sending a request you agree to follow the terms and usage policies of the AI company whose model answers it (listed at openrouter.ai), and not to use the results to train a competing AI model.';
|
|
121
|
+
|
|
122
|
+
// The AI providers' terms admit adults only; no recorded age means no.
|
|
123
|
+
const isAdult = (tier) => tier === 'adult';
|
|
124
|
+
|
|
118
125
|
module.exports = {
|
|
126
|
+
PROVIDER_TERMS, isAdult,
|
|
119
127
|
MAX_FILES, MAX_IMAGE_BYTES, MAX_PROMPT, MAX_COST_MICRO, MAX_ACTIVE_REQUESTS, CONTEXT_BUDGET, UPLOAD_BODY_LIMIT, MESSAGES,
|
|
120
128
|
checkFile, checkRequest, forProvider, buildContext, imageBrief,
|
|
121
129
|
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// modules/builder-settings/checkout-freshness.js — how far behind this builder's local
|
|
2
|
+
// project folder is, as their own session last reported it (task 1005021).
|
|
3
|
+
//
|
|
4
|
+
// WHY. A builder's hooks, CLAUDE.md and skills run from the folder on THEIR computer. When
|
|
5
|
+
// that folder falls behind, every session there runs old rules and nobody is told: the
|
|
6
|
+
// owner's sat 1,000 changes behind on a side branch for a week (task 1005015). Session
|
|
7
|
+
// start now heals the common case itself (.claude/hooks/session-start.js); this records
|
|
8
|
+
// what it found, so the server can say so when it could NOT heal — in the standing rules
|
|
9
|
+
// every Conductor relays, stale ones included, and in the hall.
|
|
10
|
+
//
|
|
11
|
+
// Stored in one reserved key of the `behavior_prefs` jsonb (migration 119), the
|
|
12
|
+
// core-move-seen.js way: no migration. Written only by the builder's own session.
|
|
13
|
+
|
|
14
|
+
const RESERVED_KEY = 'checkout_freshness';
|
|
15
|
+
// How far behind before it is worth a word. A day of merges is routine.
|
|
16
|
+
const BEHIND_WORTH_SAYING = 50;
|
|
17
|
+
// A report older than this says nothing about today's folder.
|
|
18
|
+
const REPORT_TTL_MS = 3 * 24 * 60 * 60 * 1000;
|
|
19
|
+
const REASONS = ['up-to-date', 'healed', 'fast-forwarded', 'dirty-tree', 'local-commits-ahead', 'detached-head', 'recent-edits', 'unmerged-branch', 'heal-failed', 'unknown'];
|
|
20
|
+
const BRANCH_RE = /^[A-Za-z0-9._\/-]{1,120}$/;
|
|
21
|
+
|
|
22
|
+
/** Validate a PATCH body { behind, branch, reason }. PURE. */
|
|
23
|
+
function validatePatch(body) {
|
|
24
|
+
if (body == null || typeof body !== 'object' || Array.isArray(body)) return { ok: false, errors: [{ error: 'body_must_be_object' }] };
|
|
25
|
+
const errors = [];
|
|
26
|
+
const behind = Number(body.behind);
|
|
27
|
+
if (!Number.isInteger(behind) || behind < 0 || behind > 1000000) errors.push({ key: 'behind', error: 'invalid_value' });
|
|
28
|
+
const branch = body.branch == null ? '' : String(body.branch);
|
|
29
|
+
if (branch && !BRANCH_RE.test(branch)) errors.push({ key: 'branch', error: 'invalid_value' });
|
|
30
|
+
const reason = String(body.reason || '');
|
|
31
|
+
if (!REASONS.includes(reason)) errors.push({ key: 'reason', error: 'invalid_value', allowed: REASONS });
|
|
32
|
+
const extra = Object.keys(body).filter((k) => !['behind', 'branch', 'reason'].includes(k));
|
|
33
|
+
for (const key of extra) errors.push({ key, error: 'unknown_key' });
|
|
34
|
+
if (errors.length) return { ok: false, errors };
|
|
35
|
+
return { ok: true, report: { behind, branch, reason } };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** behavior_prefs with the report stamped; sibling keys untouched. PURE. */
|
|
39
|
+
function applyReport(existing, report, nowMs = Date.now()) {
|
|
40
|
+
return { ...(existing || {}), [RESERVED_KEY]: { ...report, at: new Date(nowMs).toISOString() } };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What GET /me says about the folder: the last report, whether it is behind enough to
|
|
45
|
+
* matter, and — when it is — one plain sentence for the person and one for their agent.
|
|
46
|
+
* null when nothing was ever reported, or the report is too old to mean anything. PURE.
|
|
47
|
+
*/
|
|
48
|
+
function describe(behaviorPrefs, nowMs = Date.now()) {
|
|
49
|
+
const r = behaviorPrefs && behaviorPrefs[RESERVED_KEY];
|
|
50
|
+
if (!r || typeof r !== 'object' || !Number.isInteger(r.behind)) return null;
|
|
51
|
+
const at = Date.parse(r.at);
|
|
52
|
+
if (!Number.isFinite(at) || nowMs - at > REPORT_TTL_MS) return null;
|
|
53
|
+
const behind = r.behind >= BEHIND_WORTH_SAYING;
|
|
54
|
+
const out = { behind_by: r.behind, branch: r.branch || null, reason: r.reason, reported_at: r.at, behind };
|
|
55
|
+
if (behind) {
|
|
56
|
+
const why = {
|
|
57
|
+
'dirty-tree': 'it has unsaved changes',
|
|
58
|
+
'recent-edits': 'it has recent unsaved changes',
|
|
59
|
+
'local-commits-ahead': 'it has work that is not merged yet',
|
|
60
|
+
'unmerged-branch': 'it is on a branch with work that is not merged yet',
|
|
61
|
+
'detached-head': 'it is not on a branch',
|
|
62
|
+
'heal-failed': 'the automatic update failed',
|
|
63
|
+
}[r.reason] || 'it could not update itself';
|
|
64
|
+
out.line = `Your project folder on this computer is ${r.behind} changes behind, and ${why}, so your AI sessions are running older rules.`;
|
|
65
|
+
out.agent_line = `This person's main project folder is ${r.behind} changes behind main and could not update itself (${r.reason}). `
|
|
66
|
+
+ 'Tell them once, in plain words, and offer to update it for them (save anything unsaved on a backup branch first).';
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { validatePatch, applyReport, describe };
|
|
@@ -31,6 +31,7 @@ const lawLocks = require('../law-locks');
|
|
|
31
31
|
const renderPrefs = require('../render-prefs');
|
|
32
32
|
const profileNudgePrefs = require('../profile-nudge-prefs');
|
|
33
33
|
const coreMoveSeen = require('../core-move-seen');
|
|
34
|
+
const checkoutFreshness = require('../checkout-freshness'); // task 1005021
|
|
34
35
|
const displayName = require('../display-name');
|
|
35
36
|
const filterViews = require('../filter-views');
|
|
36
37
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
@@ -517,6 +518,31 @@ module.exports = function buildBuilderSettingsRouter() {
|
|
|
517
518
|
}
|
|
518
519
|
});
|
|
519
520
|
|
|
521
|
+
// How far behind this builder's local project folder is (task 1005021,
|
|
522
|
+
// checkout-freshness.js). Written by their own session start; read back by GET /me,
|
|
523
|
+
// which tells the person (and their agent) when the folder could not update itself.
|
|
524
|
+
// Body: { behind, branch, reason }.
|
|
525
|
+
//
|
|
526
|
+
// rank: any-builder — a builder's own report about their own computer; same posture
|
|
527
|
+
// as /me/core-move-seen.
|
|
528
|
+
router.patch('/me/checkout-freshness', api.requireBuilder, async (req, res) => {
|
|
529
|
+
if (api.validateOrRespond(req, res, {
|
|
530
|
+
behind: { required: true, type: 'number' },
|
|
531
|
+
branch: { type: 'string', maxLength: 120 },
|
|
532
|
+
reason: { required: true, type: 'string', maxLength: 40 },
|
|
533
|
+
}, { strict: true })) return undefined;
|
|
534
|
+
try {
|
|
535
|
+
const v = checkoutFreshness.validatePatch(req.body || {});
|
|
536
|
+
if (!v.ok) return res.fail('invalid_patch', 422, { errors: v.errors });
|
|
537
|
+
const existing = await db.getBuilderBehaviorPrefs(req.builder.id);
|
|
538
|
+
const saved = await db.setBuilderBehaviorPrefs(req.builder.id, checkoutFreshness.applyReport(existing, v.report));
|
|
539
|
+
res.json({ ok: true, checkout: checkoutFreshness.describe(saved) });
|
|
540
|
+
} catch (err) {
|
|
541
|
+
log.error('PATCH /me/checkout-freshness', err);
|
|
542
|
+
res.fail('write_failed', { status: 500, message: 'internal error' });
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
520
546
|
// Per-builder SAVED FILTER VIEWS (task 1004553; shares the behavior_prefs jsonb /
|
|
521
547
|
// migration 119). A view is a named set of filter values for one hall page. They
|
|
522
548
|
// are PERSONAL: every route reads and writes only req.builder.id's own prefs, so
|
|
@@ -34,6 +34,7 @@ const renderPrefs = require('./render-prefs');
|
|
|
34
34
|
const builderNeeds = require('./builder-needs');
|
|
35
35
|
const lawLocks = require('./law-locks');
|
|
36
36
|
const coreMoveSeen = require('./core-move-seen');
|
|
37
|
+
const checkoutFreshness = require('./checkout-freshness'); // task 1005021
|
|
37
38
|
|
|
38
39
|
module.exports = {
|
|
39
40
|
// --- the raw behavior_prefs blob read (GET /me resolves this once, then shapes
|
|
@@ -57,6 +58,7 @@ module.exports = {
|
|
|
57
58
|
// task 1004731: the core_upgrades row id this builder has acknowledged (the hall's
|
|
58
59
|
// one-time "Upgraded to core X" notice), from the behavior_prefs blob; null if none.
|
|
59
60
|
coreMoveSeen: (behaviorPrefs) => coreMoveSeen.seenId(behaviorPrefs),
|
|
61
|
+
describeCheckout: (behaviorPrefs) => checkoutFreshness.describe(behaviorPrefs),
|
|
60
62
|
|
|
61
63
|
// --- the per-builder model ceiling for a skill (cascade-dispatch clamps to it;
|
|
62
64
|
// never exceeds it). Mirrors the former `skillPrefs.getForBuilder` call.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
-- economy_002_project_account.sql — the append-only record of every movement on the
|
|
2
|
+
-- project's account, each one naming its cause (task 1003182, goal 1000092; ADR 0385
|
|
3
|
+
-- decision 1, ADR 0397 decision 2).
|
|
4
|
+
--
|
|
5
|
+
-- THE ACCOUNT IS NOT HERE. The processor holds the project's money in the project's
|
|
6
|
+
-- connected account (ADR 0397); Bongos holds none and has no balance column. What
|
|
7
|
+
-- this table is: our own record of what moved and why, so the owner can answer
|
|
8
|
+
-- "where did my money go" and so a daily check can compare it to the processor's
|
|
9
|
+
-- figure (a later task). A balance shown from this table is a sum of the record, and
|
|
10
|
+
-- is labelled as ours beside the processor's.
|
|
11
|
+
--
|
|
12
|
+
-- funding money in: a charge settled into the project's account
|
|
13
|
+
-- payout money out to a builder, for a task: names the payee, the task and
|
|
14
|
+
-- the processor transfer
|
|
15
|
+
-- payout_return a payout the processor returned or reversed: money back in
|
|
16
|
+
-- fee a processor or platform fee taken from the account
|
|
17
|
+
-- adjustment a correction: names the movement it corrects; never an edit
|
|
18
|
+
-- hold a task's estimate set aside against the balance (ADR 0386);
|
|
19
|
+
-- moves no money
|
|
20
|
+
-- hold_release a hold let go on ship or release; moves no money
|
|
21
|
+
--
|
|
22
|
+
-- amount_micro is a positive integer count of micro-dollars. The direction comes from
|
|
23
|
+
-- the kind (an adjustment carries its own direction), so a row cannot disagree with
|
|
24
|
+
-- itself about whether it added or took away.
|
|
25
|
+
--
|
|
26
|
+
-- THE CAUSE IS THE POINT. A payout without a payee and a task, a funding or fee
|
|
27
|
+
-- without a processor reference, or an adjustment that corrects nothing is refused by
|
|
28
|
+
-- the DATABASE, not by whichever code path wrote it. One processor reference books one
|
|
29
|
+
-- movement of its kind, so a retried payout cannot pay twice (the idempotency the
|
|
30
|
+
-- payout task builds on). Rows are never updated or deleted: a wrong figure is fixed
|
|
31
|
+
-- by a new row.
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS economy_project_account_movements (
|
|
34
|
+
id BIGSERIAL PRIMARY KEY,
|
|
35
|
+
kind TEXT NOT NULL,
|
|
36
|
+
direction TEXT NOT NULL,
|
|
37
|
+
amount_micro BIGINT NOT NULL,
|
|
38
|
+
builder_id BIGINT REFERENCES builders(id),
|
|
39
|
+
task_id BIGINT REFERENCES tasks(id),
|
|
40
|
+
corrects_id BIGINT REFERENCES economy_project_account_movements(id),
|
|
41
|
+
processor_ref TEXT,
|
|
42
|
+
actor_id BIGINT REFERENCES builders(id),
|
|
43
|
+
note TEXT NOT NULL DEFAULT '',
|
|
44
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
45
|
+
CONSTRAINT economy_project_account_kind_chk CHECK (kind IN
|
|
46
|
+
('funding', 'payout', 'payout_return', 'fee', 'adjustment', 'hold', 'hold_release')),
|
|
47
|
+
CONSTRAINT economy_project_account_direction_chk CHECK (direction IN ('in', 'out', 'none')),
|
|
48
|
+
CONSTRAINT economy_project_account_amount_chk CHECK (amount_micro > 0),
|
|
49
|
+
CONSTRAINT economy_project_account_note_chk CHECK (length(note) <= 300),
|
|
50
|
+
-- the direction is fixed by the kind; only an adjustment chooses
|
|
51
|
+
CONSTRAINT economy_project_account_direction_by_kind_chk CHECK (
|
|
52
|
+
(kind IN ('funding', 'payout_return') AND direction = 'in') OR
|
|
53
|
+
(kind IN ('payout', 'fee') AND direction = 'out') OR
|
|
54
|
+
(kind IN ('hold', 'hold_release') AND direction = 'none') OR
|
|
55
|
+
(kind = 'adjustment' AND direction IN ('in', 'out'))),
|
|
56
|
+
-- every movement names its cause
|
|
57
|
+
CONSTRAINT economy_project_account_cause_chk CHECK (
|
|
58
|
+
(kind = 'payout' AND builder_id IS NOT NULL AND task_id IS NOT NULL AND processor_ref IS NOT NULL) OR
|
|
59
|
+
(kind = 'payout_return' AND builder_id IS NOT NULL AND processor_ref IS NOT NULL) OR
|
|
60
|
+
(kind IN ('funding', 'fee') AND processor_ref IS NOT NULL) OR
|
|
61
|
+
(kind = 'adjustment' AND corrects_id IS NOT NULL AND actor_id IS NOT NULL AND length(note) > 0) OR
|
|
62
|
+
(kind IN ('hold', 'hold_release') AND task_id IS NOT NULL))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
-- one processor reference, one movement of that kind: a retried payout is a no-op
|
|
66
|
+
CREATE UNIQUE INDEX IF NOT EXISTS economy_project_account_one_per_ref
|
|
67
|
+
ON economy_project_account_movements (kind, processor_ref) WHERE processor_ref IS NOT NULL;
|
|
68
|
+
CREATE INDEX IF NOT EXISTS economy_project_account_by_time ON economy_project_account_movements (created_at DESC, id DESC);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS economy_project_account_by_builder ON economy_project_account_movements (builder_id, created_at DESC) WHERE builder_id IS NOT NULL;
|
|
70
|
+
CREATE INDEX IF NOT EXISTS economy_project_account_by_task ON economy_project_account_movements (task_id) WHERE task_id IS NOT NULL;
|
|
71
|
+
|
|
72
|
+
CREATE OR REPLACE FUNCTION economy_project_account_movements_append_only() RETURNS trigger AS $$
|
|
73
|
+
BEGIN
|
|
74
|
+
RAISE EXCEPTION 'economy_project_account_movements is append-only: record a correction as a new adjustment row';
|
|
75
|
+
END;
|
|
76
|
+
$$ LANGUAGE plpgsql;
|
|
77
|
+
|
|
78
|
+
DROP TRIGGER IF EXISTS economy_project_account_movements_no_rewrite ON economy_project_account_movements;
|
|
79
|
+
CREATE TRIGGER economy_project_account_movements_no_rewrite BEFORE UPDATE OR DELETE ON economy_project_account_movements
|
|
80
|
+
FOR EACH ROW EXECUTE FUNCTION economy_project_account_movements_append_only();
|
|
81
|
+
|
|
82
|
+
INSERT INTO schema_migrations (version) VALUES ('economy_002_project_account')
|
|
83
|
+
ON CONFLICT DO NOTHING;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"category": { "parent": "money" },
|
|
8
8
|
"default": true,
|
|
9
9
|
"maintenance": { "status": "core-maintained" },
|
|
10
|
-
"contributes": { "routes": ["cost", "credits", "earnings", "leaderboard", "reward"] },
|
|
10
|
+
"contributes": { "migrations": true, "routes": ["cost", "credits", "earnings", "leaderboard", "project-account", "reward"] },
|
|
11
11
|
"provides": ["reward"],
|
|
12
12
|
"consumes": []
|
|
13
13
|
}
|