@bongos/core 1.24.45 → 1.24.46
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 +140 -105
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +15 -0
- package/docs/api/openapi.json +152 -3
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +1102 -1102
- package/docs/copy-registry.json +1538 -1538
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/diagrams/assertions.json +2 -2
- package/docs/page-inventory.json +11 -0
- package/docs/page-readings.json +724 -724
- package/migrations/core_295_runner_logins.sql +50 -0
- package/modules/government/session-scopes.json +5 -0
- package/modules/hall-ui/public/ai-studio.states.json +2 -0
- package/modules/hall-ui/public/atlas.states.json +2 -0
- package/modules/hall-ui/public/board-room.states.json +2 -0
- package/modules/hall-ui/public/city.states.json +2 -0
- package/modules/hall-ui/public/collab.states.json +2 -0
- package/modules/hall-ui/public/commands.states.json +2 -0
- package/modules/hall-ui/public/copy-desk.states.json +2 -0
- package/modules/hall-ui/public/deploy.states.json +2 -0
- package/modules/hall-ui/public/goal-map.states.json +2 -0
- package/modules/hall-ui/public/goals.css +1 -0
- package/modules/hall-ui/public/goals.states.json +2 -0
- package/modules/hall-ui/public/government.states.json +2 -0
- package/modules/hall-ui/public/idea.states.json +2 -0
- package/modules/hall-ui/public/ideas.states.json +2 -0
- package/modules/hall-ui/public/index-founding.states.json +2 -0
- package/modules/hall-ui/public/index-genesis-decide.states.json +2 -0
- package/modules/hall-ui/public/index-genesis-demo.states.json +2 -0
- package/modules/hall-ui/public/index-genesis.states.json +2 -0
- package/modules/hall-ui/public/index.states.json +5 -0
- package/modules/hall-ui/public/market-watch-item.states.json +2 -0
- package/modules/hall-ui/public/market-watch-rules.states.json +2 -0
- package/modules/hall-ui/public/market-watch.states.json +2 -0
- package/modules/hall-ui/public/modules.states.json +2 -0
- package/modules/hall-ui/public/money.states.json +2 -0
- package/modules/hall-ui/public/my-agents.states.json +2 -0
- package/modules/hall-ui/public/notes-panel.css +1 -1
- package/modules/hall-ui/public/primer.states.json +2 -0
- package/modules/hall-ui/public/profile.states.json +2 -0
- package/modules/hall-ui/public/roster.states.json +2 -0
- package/modules/hall-ui/public/settings.states.json +2 -0
- package/modules/hall-ui/public/street-card.states.json +2 -0
- package/modules/hall-ui/public/street.states.json +2 -0
- package/modules/hall-ui/public/studio.states.json +2 -0
- package/modules/hall-ui/public/style.css +27 -24
- package/modules/hall-ui/public/task.states.json +2 -0
- package/modules/hall-ui/public/thinking.states.json +2 -0
- package/modules/hall-ui/public/tweak-editor.states.json +2 -0
- package/modules/hall-ui/public/watch.states.json +2 -0
- package/modules/hall-ui/public/welcome-boot.js +6 -2
- package/modules/hall-ui/public/work-founding.states.json +2 -0
- package/modules/hall-ui/public/work.states.json +3 -1
- package/modules/lifecycle/db-claims.js +6 -3
- package/modules/lifecycle/routes/claims.js +10 -0
- package/modules/lifecycle/routes/tasks.js +1 -1
- package/modules/lifecycle/runner-self-guard.js +19 -10
- package/modules/platform-identity/platform-identity.js +1 -1
- package/modules/platform-identity/tests/platform-identity.mjs +13 -2
- package/modules/provisioning/core-upgrade-authz.js +8 -0
- package/modules/public-landing/public/account.html +1 -0
- package/modules/public-landing/public/community.html +1 -0
- package/modules/public-landing/public/connections.html +1 -0
- package/modules/public-landing/public/contact.html +1 -0
- package/modules/public-landing/public/guild.html +1 -0
- package/modules/public-landing/public/index.html +1 -0
- package/modules/public-landing/public/invitations.html +1 -0
- package/modules/public-landing/public/privacy.html +1 -0
- package/modules/public-landing/public/projects.html +1 -0
- package/modules/public-landing/public/terms.html +1 -0
- package/modules/raise/views/gate.html +1 -0
- package/modules/raise/views/nda.html +1 -0
- package/modules/raise/views/page.html +1 -0
- package/modules/status-ui/public/index.html +1 -0
- package/modules/ui-design/kit/README.md +1 -1
- package/modules/ui-design/kit/fixtures/versions.json +1 -0
- package/modules/ui-design/kit/lib.js +48 -3
- package/modules/ui-design/kit/probe.js +1 -0
- package/modules/ui-design/kit/render.js +2 -0
- package/modules/ui-design/kit/serve.js +1 -0
- package/modules/ui-design/module.json +1 -1
- package/modules/ui-design/public/tokens.css +125 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +14 -0
- package/scripts/bongos/fitness.js +1 -1
- package/scripts/bongos/run-unit-tests.js +3 -0
- package/src/bongos/auth.js +25 -4
- package/src/bongos/route-rank-check.js +5 -0
- package/src/bongos/routes/auth.js +48 -0
- package/src/bongos/runner-login.js +151 -0
- package/src/bongos/serve-internal.js +6 -1
- package/src/module-api.js +1 -1
- package/tests/autobongos_runner_login.mjs +296 -0
- package/tests/autobongos_runner_login_db.mjs +105 -0
- package/tests/design_tokens_shared.mjs +183 -0
- package/tests/government_session_scope.mjs +2 -1
- package/tests/hall_reading_world.mjs +5 -1
- package/tests/hall_tokens.mjs +22 -3
- package/tests/ui_design_kit.mjs +30 -0
- package/tests/welcome_boot_browser.mjs +4 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* modules/ui-design/public/tokens.css — THE SHARED TOKEN SHEET (task 1005081,
|
|
2
|
+
goal 1000138 "One Cloud Bongos design system").
|
|
3
|
+
|
|
4
|
+
One sheet every Cloud Bongos surface loads — the hall, the landing + the
|
|
5
|
+
projects hub, the status page and the raise page — served at the one stable
|
|
6
|
+
path /modules/ui-design/public/tokens.css on every host (serve-internal.js's
|
|
7
|
+
module-asset route; the kit's stub serves the same path).
|
|
8
|
+
|
|
9
|
+
WHAT IS HERE: the named SCALES — type sizes, spacing, corners, shadows,
|
|
10
|
+
motion and stacking layers. Every value was already in use (the hall's token
|
|
11
|
+
block, DESIGN.md's rhythm/shapes/motion), so loading this sheet moves nothing.
|
|
12
|
+
|
|
13
|
+
WHAT IS NOT HERE: the fifteen brand tokens (thirteen colours + two font
|
|
14
|
+
stacks). Those are the instance pack's API — serve-internal.js themeMarkup()
|
|
15
|
+
splices config/branding.*.json theme.ui as a plain :root at the END of <head>,
|
|
16
|
+
after every sheet, so the pack always wins. This sheet declares none of the
|
|
17
|
+
fifteen, so there is nothing for it to win against. No colour literal lives
|
|
18
|
+
here except black with alpha (the shadows) — the Fifteen Rule's one exception.
|
|
19
|
+
|
|
20
|
+
WHY --ds-*: the surfaces grew their own scale names, and some collide with a
|
|
21
|
+
DIFFERENT meaning (cosmos.css spends --line as a colour; raise.css sets
|
|
22
|
+
--radius to 10px). A shared name would silently change whichever surface did
|
|
23
|
+
not define it, so every scale is namespaced and each surface keeps its own
|
|
24
|
+
names as aliases until a later task migrates it (the hall's aliases live in
|
|
25
|
+
modules/hall-ui/public/style.css: --radius → --ds-radius-panel, …).
|
|
26
|
+
|
|
27
|
+
DARK TWINS: the one mode-dependent scale (the shadow) is written twice,
|
|
28
|
+
byte-identical — the switch rule (the hall's html[data-theme], the apex's
|
|
29
|
+
:root[data-mode]) and the prefers-color-scheme twin for first paint.
|
|
30
|
+
|
|
31
|
+
KILL SWITCH: each surface keeps its own `animation/transition: none
|
|
32
|
+
!important` rule; this sheet additionally zeroes its durations under
|
|
33
|
+
prefers-reduced-motion so a rule built on them stays still too. */
|
|
34
|
+
|
|
35
|
+
:root {
|
|
36
|
+
/* ---- type sizes: the steps the hall's 113 literal sizes cluster on, small
|
|
37
|
+
to large. The "-plus" half steps are the ones a later migration
|
|
38
|
+
should collapse into their neighbours. The four fluid roles are
|
|
39
|
+
DESIGN.md's display/headline/figure/title. ---- */
|
|
40
|
+
--ds-text-3xs: 10.5px;
|
|
41
|
+
--ds-text-2xs: 11px;
|
|
42
|
+
--ds-text-2xs-plus: 11.5px;
|
|
43
|
+
--ds-text-xs: 12px;
|
|
44
|
+
--ds-text-xs-plus: 12.5px;
|
|
45
|
+
--ds-text-sm: 13px;
|
|
46
|
+
--ds-text-sm-plus: 13.5px;
|
|
47
|
+
--ds-text-md: 14px;
|
|
48
|
+
--ds-text-md-plus: 14.5px;
|
|
49
|
+
--ds-text-lg: 15px;
|
|
50
|
+
--ds-text-xl: 16px;
|
|
51
|
+
--ds-text-2xl: 17px;
|
|
52
|
+
--ds-text-3xl: 20px;
|
|
53
|
+
--ds-text-4xl: 22px;
|
|
54
|
+
--ds-text-5xl: 26px;
|
|
55
|
+
--ds-text-title: clamp(23px, 2.4vw, 32px);
|
|
56
|
+
--ds-text-headline: clamp(26px, 3.1vw, 42px);
|
|
57
|
+
--ds-text-figure: clamp(30px, 3.7vw, 52px);
|
|
58
|
+
--ds-text-display: clamp(34px, 6.7vw, 96px);
|
|
59
|
+
|
|
60
|
+
/* ---- spacing: DESIGN.md's rhythm (6/10/14/22/34/52) + the band gutters ---- */
|
|
61
|
+
--ds-space-hair: 6px;
|
|
62
|
+
--ds-space-xs: 10px;
|
|
63
|
+
--ds-space-sm: 14px;
|
|
64
|
+
--ds-space-md: 22px;
|
|
65
|
+
--ds-space-lg: 34px;
|
|
66
|
+
--ds-space-xl: 52px;
|
|
67
|
+
--ds-space-gutter: 26px;
|
|
68
|
+
--ds-space-gutter-narrow: 20px;
|
|
69
|
+
|
|
70
|
+
/* ---- the hairline: a WIDTH, never a colour ---- */
|
|
71
|
+
--ds-line: 1px;
|
|
72
|
+
|
|
73
|
+
/* ---- corners: pill or nothing for controls; panels and tiles ---- */
|
|
74
|
+
--ds-radius-pill: 999px;
|
|
75
|
+
--ds-radius-panel: 16px;
|
|
76
|
+
--ds-radius-tile: 10px;
|
|
77
|
+
|
|
78
|
+
/* ---- shadows: the world has no UI shadow vocabulary; this is spent only
|
|
79
|
+
where an element floats over a scrim. Black with alpha. ---- */
|
|
80
|
+
--ds-shadow-pop: 0 18px 40px rgba(0, 0, 0, .28);
|
|
81
|
+
|
|
82
|
+
/* ---- motion: state changes take the one easing; an authored entrance
|
|
83
|
+
takes the exponential ease-out (DESIGN.md, Motion). ---- */
|
|
84
|
+
--ds-dur-fast: 150ms;
|
|
85
|
+
--ds-dur-base: 200ms;
|
|
86
|
+
--ds-dur-slow: 320ms;
|
|
87
|
+
--ds-dur-slower: 420ms;
|
|
88
|
+
--ds-dur-arrive: 700ms;
|
|
89
|
+
--ds-stagger: 70ms;
|
|
90
|
+
--ds-ease: cubic-bezier(.2, .7, .3, 1);
|
|
91
|
+
--ds-ease-arrive: cubic-bezier(.16, 1, .3, 1);
|
|
92
|
+
|
|
93
|
+
/* ---- stacking layers, low to high: the reading hairline under the top
|
|
94
|
+
bar, the chrome, the phone drawer over its scrim, a confirm dialog
|
|
95
|
+
over the drawer, the fullscreen diagram over everything. ---- */
|
|
96
|
+
--ds-z-progress: 25;
|
|
97
|
+
--ds-z-topbar: 30;
|
|
98
|
+
--ds-z-sidebar: 40;
|
|
99
|
+
--ds-z-scrim: 140;
|
|
100
|
+
--ds-z-drawer: 150;
|
|
101
|
+
--ds-z-modal: 160;
|
|
102
|
+
--ds-z-diagram-full: 9999;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ---------- dark: written twice, byte-identical ---------- */
|
|
106
|
+
html[data-theme="dark"], :root[data-mode="dark"] {
|
|
107
|
+
--ds-shadow-pop: 0 18px 40px rgba(0, 0, 0, .62);
|
|
108
|
+
}
|
|
109
|
+
@media (prefers-color-scheme: dark) {
|
|
110
|
+
html:not([data-theme]):not([data-mode="light"]) {
|
|
111
|
+
--ds-shadow-pop: 0 18px 40px rgba(0, 0, 0, .62);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ---------- the kill switch, for the motion scale ---------- */
|
|
116
|
+
@media (prefers-reduced-motion: reduce) {
|
|
117
|
+
:root {
|
|
118
|
+
--ds-dur-fast: 0ms;
|
|
119
|
+
--ds-dur-base: 0ms;
|
|
120
|
+
--ds-dur-slow: 0ms;
|
|
121
|
+
--ds-dur-slower: 0ms;
|
|
122
|
+
--ds-dur-arrive: 0ms;
|
|
123
|
+
--ds-stagger: 0ms;
|
|
124
|
+
}
|
|
125
|
+
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.46",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.46",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.46",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11233,5 +11233,19 @@
|
|
|
11233
11233
|
"id": "1005070",
|
|
11234
11234
|
"text": "The hall's sidebar now appears instantly on every page instead of popping in late, and Building mode shows a single Home button."
|
|
11235
11235
|
}
|
|
11236
|
+
],
|
|
11237
|
+
"1.24.46": [
|
|
11238
|
+
{
|
|
11239
|
+
"id": "1005081",
|
|
11240
|
+
"text": "Cloud Bongos now has one shared set of design measurements (text sizes, spacing, corner shapes, shadows, animation timing and layering) that the builders hall, the landing page, the projects map, the status page and the invest"
|
|
11241
|
+
},
|
|
11242
|
+
{
|
|
11243
|
+
"id": "1004547",
|
|
11244
|
+
"text": "An Autobongos PC can now sign in with a login of its own, so the server always knows its work is automatic and it can never move a live version or flip its own off switch."
|
|
11245
|
+
},
|
|
11246
|
+
{
|
|
11247
|
+
"id": "1005080",
|
|
11248
|
+
"text": "The screenshot checker builders use before shipping a design change shows the real hall pages again, instead of a pop-up covering every page. Two small accessibility fixes came with it: a few tiny links are now big enough to t"
|
|
11249
|
+
}
|
|
11236
11250
|
]
|
|
11237
11251
|
}
|
|
@@ -540,7 +540,7 @@ const KERNEL_FILES = [
|
|
|
540
540
|
'src/bongos/auth-admission.js', // membership kind, admission gate, first-admin seating
|
|
541
541
|
'src/bongos/auth-github.js', // GitHub device + web OAuth flows
|
|
542
542
|
'src/bongos/auth-idp.js', // hub federation, assertion + logout-token verification
|
|
543
|
-
'src/bongos/auth-discord.js',
|
|
543
|
+
'src/bongos/auth-discord.js', 'src/bongos/runner-login.js', // Discord OAuth link + guild membership; an Autobongos PC's own login + its limits (task 1004547; requires only pool/db-kernel)
|
|
544
544
|
'src/bongos/route-rank-check.js', // deterministic route→rank classification
|
|
545
545
|
'src/bongos/permission-path-check.js', // protected-path matcher (PROTECTED_GLOBS, ADR 0043)
|
|
546
546
|
'src/bongos/goal-scope-check.js', // goal create-gate scope wall (composes the 3 matchers above; ADR 0086 §3, BV1.R60)
|
|
@@ -140,6 +140,9 @@ const INTEGRATION = new Set([
|
|
|
140
140
|
// task 1004677: two marked auto-pickup claims racing for the last slot of the
|
|
141
141
|
// monthly cap; only real connections prove the advisory lock. Scratch DB only.
|
|
142
142
|
'autopickup_cap_db',
|
|
143
|
+
// task 1004547: an Autobongos runner's one-time code works once, even when two
|
|
144
|
+
// PCs race it, and not after it expires; only real SQL proves both. Scratch DB.
|
|
145
|
+
'autobongos_runner_login_db',
|
|
143
146
|
'claims_requested_worktree_db',
|
|
144
147
|
'clawback_confirmed_backlog',
|
|
145
148
|
// task 1003113: the copy_desk_flags schema half. The module's own tests are
|
package/src/bongos/auth.js
CHANGED
|
@@ -51,6 +51,8 @@ const { pool } = require('./pool');
|
|
|
51
51
|
// the optional `?.` degrades to a no-op when the module is off (the stage tick is
|
|
52
52
|
// best-effort anyway).
|
|
53
53
|
const seams = require('../module-seams');
|
|
54
|
+
// task 1004547: an Autobongos runner's own login holds no SYSTEM permission.
|
|
55
|
+
const { isRunnerSession, withoutSystemPermissions, capRunnerRank } = require('./runner-login');
|
|
54
56
|
const ic = require('../instance-config');
|
|
55
57
|
const { branding, userAgent } = require('../branding');
|
|
56
58
|
|
|
@@ -284,7 +286,11 @@ async function requireBuilder(req, res, next) {
|
|
|
284
286
|
// rank is the single source of authority (ADR 0016). Read fresh from the
|
|
285
287
|
// DB on every request via lookupSession — never cached, never from the
|
|
286
288
|
// token — so demotion takes effect on the builder's very next request.
|
|
287
|
-
|
|
289
|
+
// An Autobongos runner's own login acts at Metic at most (task 1004547,
|
|
290
|
+
// runner-login.js capRunnerRank), so every requireRank gate and every handler
|
|
291
|
+
// that reads this field sees the capped rank; `runner` says why.
|
|
292
|
+
rank: isRunnerSession(session) ? capRunnerRank(session.rank, RANKS) : session.rank,
|
|
293
|
+
runner: isRunnerSession(session),
|
|
288
294
|
// system_role identifies a non-human service principal (NULL for every
|
|
289
295
|
// human builder). The BFG cross-builder write gate (requireBfgPrincipal)
|
|
290
296
|
// keys on this EXACT value — never on rank — so an Archon token cannot
|
|
@@ -456,7 +462,22 @@ async function pageGateAlsoAdmits(name, session) {
|
|
|
456
462
|
// query fold, not the cross-request memo ADR 0016 forbids. Anything unexpected —
|
|
457
463
|
// no raw keys on the row, an older port without the pure half, a throw — falls
|
|
458
464
|
// through to the DB resolve, so the fold can only ever be an optimization.
|
|
459
|
-
|
|
465
|
+
//
|
|
466
|
+
// A RUNNER SESSION HOLDS NO SYSTEM PERMISSION (task 1004547). `session` is the
|
|
467
|
+
// caller's session row; when its source is 'runner' (an Autobongos PC's own login,
|
|
468
|
+
// src/bongos/runner-login.js) every key the catalog marks system:true is dropped —
|
|
469
|
+
// core.pin.move, autonomy.fence.manage, builder.rank.set and the rest. The owner
|
|
470
|
+
// rule (ADR 0307 §6) is that an unattended machine never moves a live version or
|
|
471
|
+
// touches its own kill switch; this is where the server, which the PC cannot edit,
|
|
472
|
+
// makes that true. An unknown key, or a port without isSystem, drops too.
|
|
473
|
+
async function resolveHeldPermissions(builderId, rawKeys, session = null) {
|
|
474
|
+
const held = await resolveHeldPermissionsForBuilder(builderId, rawKeys);
|
|
475
|
+
if (held === null || !isRunnerSession(session)) return held;
|
|
476
|
+
const governance = seams.resolveOptional('government');
|
|
477
|
+
return withoutSystemPermissions(held, governance && governance.isSystem);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function resolveHeldPermissionsForBuilder(builderId, rawKeys) {
|
|
460
481
|
if (builderId === null || builderId === undefined || builderId === '') return null;
|
|
461
482
|
const governance = seams.resolveOptional('government');
|
|
462
483
|
if (!governance || typeof governance.resolveBuilderPermissions !== 'function') return null;
|
|
@@ -506,7 +527,7 @@ async function runPageGate(name, req, res, next) {
|
|
|
506
527
|
res.redirect(302, signInWithReturn(req));
|
|
507
528
|
return;
|
|
508
529
|
}
|
|
509
|
-
const held = await resolveHeldPermissions(session.builder_id, session.raw_permission_keys);
|
|
530
|
+
const held = await resolveHeldPermissions(session.builder_id, session.raw_permission_keys, session);
|
|
510
531
|
if (held === null || !held.has(permission)) {
|
|
511
532
|
// The rank door said no. A page whose data route admits a SECOND audience
|
|
512
533
|
// asks that door before redirecting (ADR 0210) — consulted here and nowhere
|
|
@@ -770,7 +791,7 @@ function requirePermission(...perms) {
|
|
|
770
791
|
// (no port / malformed port / a throwing resolver) and is always a denial.
|
|
771
792
|
// req.rawPermissionKeys rides the fold (task 1002556) when the session query
|
|
772
793
|
// carried the grant keys; absent, the DB resolve runs exactly as before.
|
|
773
|
-
const held = await resolveHeldPermissions(builderId, req.rawPermissionKeys);
|
|
794
|
+
const held = await resolveHeldPermissions(builderId, req.rawPermissionKeys, req.gdsSession);
|
|
774
795
|
if (held === null) return deny(null);
|
|
775
796
|
if (required.every((key) => held.has(key))) return next();
|
|
776
797
|
return deny(required.filter((key) => held.has(key)));
|
|
@@ -199,6 +199,11 @@ const EXPECTED_RANKS = {
|
|
|
199
199
|
// session revocation (kill another builder's / all sessions)
|
|
200
200
|
'POST /auth/revoke': 'perm:auth.session.revoke',
|
|
201
201
|
'POST /auth/revoke-all': 'perm:auth.session.revoke',
|
|
202
|
+
// An Autobongos runner's own login (task 1004547). Issuing the one-time code is
|
|
203
|
+
// Metic+ (who may run a runner); pinned so a widening cannot hand a Xenos a
|
|
204
|
+
// machine login. The exchange is a public auth-establishment route like
|
|
205
|
+
// /auth/device/exchange (the code IS the credential), so it carries no pin.
|
|
206
|
+
'POST /auth/runner-code/issue': 'metic+archon',
|
|
202
207
|
// deliberately any-builder (task 1003341) — the OWN-scoped half of the same
|
|
203
208
|
// capability. These reach only the caller's own sessions (every statement is
|
|
204
209
|
// keyed on req.builder.id), so a rank floor here would just put an Archon back
|
|
@@ -35,6 +35,8 @@ const { reportActivityRollupForShippedTask } = require('../auth-admission.js');
|
|
|
35
35
|
// endpoint refuses. Same composed answer the gate itself reads.
|
|
36
36
|
const { effectiveJoinDoor } = require('../project-door.js');
|
|
37
37
|
const db = require('../db');
|
|
38
|
+
// task 1004547: an Autobongos PC's own login, from a one-time code.
|
|
39
|
+
const { issueCode: issueRunnerCode, exchangeCode: exchangeRunnerCode } = require('../runner-login');
|
|
38
40
|
const { publicOrigin, parseCookie, validateOrRespond } = require('./_helpers');
|
|
39
41
|
const { accountExistenceReadRateLimit } = require('../middleware/rate-limit');
|
|
40
42
|
const { branding } = require('../../branding');
|
|
@@ -1364,6 +1366,52 @@ module.exports = function buildAuthRouter() {
|
|
|
1364
1366
|
}
|
|
1365
1367
|
});
|
|
1366
1368
|
|
|
1369
|
+
// POST /auth/runner-code/issue — task 1004547 (goal 1000139). A ONE-TIME CODE an
|
|
1370
|
+
// Autobongos PC trades for its own login (src/bongos/runner-login.js). Body:
|
|
1371
|
+
// { label? } — the PC's name, shown later in the builder's sessions list.
|
|
1372
|
+
//
|
|
1373
|
+
// rank: metic+archon — running a runner is Metic+ today (owner, 2026-11-06).
|
|
1374
|
+
// Cookie ONLY, like /auth/cli-token/issue: a bearer — a CLI token or a runner's
|
|
1375
|
+
// own login — must never be able to mint another login. The code is returned
|
|
1376
|
+
// once; only its hash is stored. It works once and expires in 15 minutes.
|
|
1377
|
+
router.post('/auth/runner-code/issue', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
|
|
1378
|
+
if (validateOrRespond(req, res, { label: { type: 'string', maxLength: 200 } })) return;
|
|
1379
|
+
if (!sessionCookieToken(req)) {
|
|
1380
|
+
return res.fail('cookie_required', { status: 403, message: 'A runner code is issued from the builders hall in a browser (Settings), never from a token, so a leaked token cannot mint a login.' });
|
|
1381
|
+
}
|
|
1382
|
+
try {
|
|
1383
|
+
const out = await issueRunnerCode({ builderId: req.builder.id, label: req.body && req.body.label });
|
|
1384
|
+
const issuer = String(resolveEnv('PUBLIC_ORIGIN') || configuredOAuthOrigin() || '').trim().replace(/\/+$/, '');
|
|
1385
|
+
res.status(201).json({ ok: true, code: out.code, expires_at: out.expires_at, api_base: issuer || null });
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
log.error('[gds] POST /auth/runner-code/issue', err && err.message);
|
|
1388
|
+
res.fail('runner_code_failed', { status: 500, message: 'internal error' });
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
// POST /auth/runner-code/exchange — task 1004547. The PC sends the one-time code
|
|
1393
|
+
// and gets back its own login (a 'runner' session). Body: { code }.
|
|
1394
|
+
//
|
|
1395
|
+
// rank: public — auth establishment; the one-time code is the bearer. A wrong,
|
|
1396
|
+
// used or expired code all answer the same 401, so nothing is learned from which.
|
|
1397
|
+
// The router's per-IP write limit (rateLimitMiddleware) is its abuse ceiling; the
|
|
1398
|
+
// code is 256 bits, so guessing one is not a race worth a limiter of its own.
|
|
1399
|
+
router.post('/auth/runner-code/exchange', async (req, res) => {
|
|
1400
|
+
if (validateOrRespond(req, res, { code: { required: true, type: 'string', maxLength: 200 } })) return;
|
|
1401
|
+
try {
|
|
1402
|
+
const out = await exchangeRunnerCode({ code: req.body.code, userAgent: req.headers['user-agent'] ?? null });
|
|
1403
|
+
if (out.refused === 'builder_inactive') return res.fail('builder_inactive', 403);
|
|
1404
|
+
if (out.refused) {
|
|
1405
|
+
return res.fail('runner_code_invalid', { status: 401, message: 'That runner code is wrong, already used or expired. Make a new one in Settings.' });
|
|
1406
|
+
}
|
|
1407
|
+
const issuer = String(resolveEnv('PUBLIC_ORIGIN') || configuredOAuthOrigin() || '').trim().replace(/\/+$/, '');
|
|
1408
|
+
res.json({ ok: true, token: out.token, session_id: out.session_id, builder_login: out.github_login, label: out.label, api_base: issuer || null });
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
log.error('[gds] POST /auth/runner-code/exchange', err && err.message);
|
|
1411
|
+
res.fail('runner_code_failed', { status: 500, message: 'internal error' });
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1367
1415
|
// POST /auth/revoke — V3.R38 / #259. Archon-only. Kill a SINGLE session by
|
|
1368
1416
|
// its primary-key id. Used for surgical lockouts (e.g. a compromised CLI on
|
|
1369
1417
|
// a known builder's laptop) without disturbing that builder's other sessions.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/runner-login.js — an Autobongos runner's OWN login (task 1004547, goal
|
|
4
|
+
// 1000139, ADR 0307 §6).
|
|
5
|
+
//
|
|
6
|
+
// WHY. A runner used to run under its builder's CLI session. The server then could
|
|
7
|
+
// not tell a runner's work from the person's, except by the worktree name the runner
|
|
8
|
+
// chose, and the worker carried the person's full authority (it could move a live
|
|
9
|
+
// core pin, task 1004974). Now the PC gets a login of its own:
|
|
10
|
+
//
|
|
11
|
+
// 1. In the builders hall (a browser, so a cookie session), a Metic+ builder asks
|
|
12
|
+
// for a ONE-TIME CODE (issueCode). The plaintext is shown once; only its sha256
|
|
13
|
+
// is stored. It expires after CODE_TTL_MS and works once.
|
|
14
|
+
// 2. The setup command on the PC trades the code for a session whose source is
|
|
15
|
+
// 'runner' (exchangeCode). That session belongs to the same builder, so the
|
|
16
|
+
// ledger credits them, but the server knows it is a machine working alone.
|
|
17
|
+
// 3. What 'runner' changes, server-side, where the PC cannot reach:
|
|
18
|
+
// - every claim it makes is marked autonomous (claims.autonomous), whatever
|
|
19
|
+
// the worktree is called, and the publish guard reads that mark;
|
|
20
|
+
// - it holds no SYSTEM permission (core.pin.move, autonomy.fence.manage, …),
|
|
21
|
+
// and the project-owner door to a core move is shut to it (auth.js,
|
|
22
|
+
// provisioning core-upgrade-authz.js);
|
|
23
|
+
// - it may not mint another login: issuing a code needs a browser cookie.
|
|
24
|
+
// 4. "Remove this PC" is the ordinary own-session revoke; the next call is a 401.
|
|
25
|
+
//
|
|
26
|
+
// The exchange is unauthenticated by design (the PC has no login yet); the code IS
|
|
27
|
+
// the credential, so it is 256 bits of CSPRNG, single-use, short-lived and looked up
|
|
28
|
+
// by its full digest, exactly as a session bearer is (db-kernel sessionTokenHash).
|
|
29
|
+
|
|
30
|
+
const crypto = require('node:crypto');
|
|
31
|
+
const { pool } = require('./pool');
|
|
32
|
+
const { sessionTokenHash } = require('./db-kernel');
|
|
33
|
+
|
|
34
|
+
const RUNNER_SOURCE = 'runner';
|
|
35
|
+
const CODE_TTL_MS = 15 * 60 * 1000; // 15 minutes: long enough to paste, short enough to waste if leaked
|
|
36
|
+
const CODE_PREFIX = 'abr_'; // recognisable in a pasted command, never part of the secret's entropy
|
|
37
|
+
const LABEL_MAX = 80;
|
|
38
|
+
|
|
39
|
+
const isRunnerSession = (session) => !!session && session.source === RUNNER_SOURCE;
|
|
40
|
+
|
|
41
|
+
function cleanLabel(label) {
|
|
42
|
+
if (typeof label !== 'string') return null;
|
|
43
|
+
const s = label.replace(/[\u0000-\u001f\u007f]/g, ' ').trim().slice(0, LABEL_MAX);
|
|
44
|
+
return s || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Issue a one-time code for `builderId`. Returns { code, expires_at, id }. The
|
|
48
|
+
// plaintext code exists only in this return value.
|
|
49
|
+
async function issueCode({ builderId, label = null, now = Date.now() }, exec = pool) {
|
|
50
|
+
const code = CODE_PREFIX + crypto.randomBytes(32).toString('hex');
|
|
51
|
+
const expiresAt = new Date(now + CODE_TTL_MS);
|
|
52
|
+
const { rows } = await exec.query(
|
|
53
|
+
`INSERT INTO runner_enrollment_codes (builder_id, code_hash, label, expires_at)
|
|
54
|
+
VALUES ($1, $2, $3, $4)
|
|
55
|
+
RETURNING id, expires_at`,
|
|
56
|
+
[builderId, sessionTokenHash(code), cleanLabel(label), expiresAt]
|
|
57
|
+
);
|
|
58
|
+
return { id: rows[0].id, code, expires_at: rows[0].expires_at };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Trade a code for a runner session. Returns { token, session_id, builder_id,
|
|
62
|
+
// github_login, label } or a refusal { refused: 'code_invalid' | 'builder_inactive' }.
|
|
63
|
+
// One transaction: the code is burned (used_at) by the same UPDATE that proves it
|
|
64
|
+
// was live, so two PCs racing one code cannot both win. A wrong, used or expired
|
|
65
|
+
// code all answer the same 'code_invalid', so the route leaks nothing about which.
|
|
66
|
+
async function exchangeCode({ code, userAgent = null }, exec = pool) {
|
|
67
|
+
if (typeof code !== 'string' || !code.startsWith(CODE_PREFIX) || code.length > 200) {
|
|
68
|
+
return { refused: 'code_invalid' };
|
|
69
|
+
}
|
|
70
|
+
const client = typeof exec.connect === 'function' ? await exec.connect() : exec;
|
|
71
|
+
const release = () => { if (client !== exec && typeof client.release === 'function') client.release(); };
|
|
72
|
+
try {
|
|
73
|
+
await client.query('BEGIN');
|
|
74
|
+
const { rows } = await client.query(
|
|
75
|
+
`UPDATE runner_enrollment_codes c
|
|
76
|
+
SET used_at = now()
|
|
77
|
+
FROM builders b
|
|
78
|
+
WHERE c.code_hash = $1
|
|
79
|
+
AND c.used_at IS NULL
|
|
80
|
+
AND c.expires_at > now()
|
|
81
|
+
AND b.id = c.builder_id
|
|
82
|
+
RETURNING c.id, c.builder_id, c.label, b.github_login, b.status`,
|
|
83
|
+
[sessionTokenHash(code)]
|
|
84
|
+
);
|
|
85
|
+
const hit = rows[0];
|
|
86
|
+
if (!hit) {
|
|
87
|
+
await client.query('ROLLBACK');
|
|
88
|
+
return { refused: 'code_invalid' };
|
|
89
|
+
}
|
|
90
|
+
if (hit.status === 'inactive') {
|
|
91
|
+
await client.query('ROLLBACK');
|
|
92
|
+
return { refused: 'builder_inactive' };
|
|
93
|
+
}
|
|
94
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
95
|
+
const ua = [hit.label ? `runner:${hit.label}` : 'runner', userAgent].filter(Boolean).join(' · ').slice(0, 400);
|
|
96
|
+
const ins = await client.query(
|
|
97
|
+
`INSERT INTO builder_sessions (builder_id, token_hash, source, user_agent, expires_at)
|
|
98
|
+
VALUES ($1, $2, $3, $4, NULL)
|
|
99
|
+
RETURNING id`,
|
|
100
|
+
[hit.builder_id, sessionTokenHash(token), RUNNER_SOURCE, ua]
|
|
101
|
+
);
|
|
102
|
+
await client.query(
|
|
103
|
+
`UPDATE runner_enrollment_codes SET used_session_id = $2 WHERE id = $1`,
|
|
104
|
+
[hit.id, ins.rows[0].id]
|
|
105
|
+
);
|
|
106
|
+
await client.query('COMMIT');
|
|
107
|
+
return { token, session_id: ins.rows[0].id, builder_id: hit.builder_id, github_login: hit.github_login, label: hit.label };
|
|
108
|
+
} catch (err) {
|
|
109
|
+
try { await client.query('ROLLBACK'); } catch (_) { /* the original error wins */ }
|
|
110
|
+
throw err;
|
|
111
|
+
} finally {
|
|
112
|
+
release();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The highest rank a runner session ACTS at (task 1004547). A runner does its
|
|
117
|
+
// builder's ordinary work (claim, ship, grade, publish, file a blocker), and every
|
|
118
|
+
// one of those routes is open at Metic. Anything gated above that is an owner's
|
|
119
|
+
// decision, which an unattended machine does not make. `ranks` is the ladder,
|
|
120
|
+
// highest authority first (db-kernel RANKS); an unknown rank is returned as is,
|
|
121
|
+
// because requireRank already fails it closed.
|
|
122
|
+
const RUNNER_RANK_CAP = 'metic';
|
|
123
|
+
function capRunnerRank(rank, ranks) {
|
|
124
|
+
const i = ranks.indexOf(rank);
|
|
125
|
+
const cap = ranks.indexOf(RUNNER_RANK_CAP);
|
|
126
|
+
if (i === -1 || cap === -1) return rank;
|
|
127
|
+
return i < cap ? RUNNER_RANK_CAP : rank;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Drop every SYSTEM permission from a held set, for a runner session. `isSystem` is
|
|
131
|
+
// the government port's catalog.isSystem: true = system, false = not, null =
|
|
132
|
+
// unknown key. Anything but an explicit false is dropped, so an unreadable catalog
|
|
133
|
+
// leaves a runner with less, never more. Without a port at all every key goes.
|
|
134
|
+
function withoutSystemPermissions(held, isSystem) {
|
|
135
|
+
const out = new Set();
|
|
136
|
+
if (typeof isSystem !== 'function') return out;
|
|
137
|
+
for (const key of held) {
|
|
138
|
+
let sys = null;
|
|
139
|
+
try { sys = isSystem(key); } catch (_) { sys = null; }
|
|
140
|
+
if (sys === false) out.add(key);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
isRunnerSession,
|
|
147
|
+
issueCode,
|
|
148
|
+
exchangeCode,
|
|
149
|
+
withoutSystemPermissions,
|
|
150
|
+
capRunnerRank,
|
|
151
|
+
};
|
|
@@ -1214,10 +1214,15 @@ function mountInternalSurfaces(app) {
|
|
|
1214
1214
|
// static handler. Gated per-request on the live enabled set so a disabled module's
|
|
1215
1215
|
// assets 404 (defense in depth behind the no-injection above). Same Cache-Control
|
|
1216
1216
|
// posture as the hall's own scripts.
|
|
1217
|
+
//
|
|
1218
|
+
// Served on EVERY host, the generic surface hosts (status.) included (task
|
|
1219
|
+
// 1005081): the shared token sheet /modules/ui-design/public/tokens.css is one
|
|
1220
|
+
// URL the hall, the landing, the status page and raise all load. A surface host
|
|
1221
|
+
// still answers its own files first — its static handler above falls through
|
|
1222
|
+
// only when the surface dir has no such file.
|
|
1217
1223
|
const MODULE_ASSET_RE = /^\/modules\/([a-z][a-z0-9-]*)\/public\/(.+)$/;
|
|
1218
1224
|
const MODULES_ROOT = path.join(ROOT, 'modules');
|
|
1219
1225
|
app.get(MODULE_ASSET_RE, (req, res, next) => {
|
|
1220
|
-
if (isModuleSurfaceHost(req)) return next();
|
|
1221
1226
|
const m = req.path.match(MODULE_ASSET_RE);
|
|
1222
1227
|
const key = m[1];
|
|
1223
1228
|
let enabled = false;
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.46'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|