@bongos/core 1.18.18 → 1.19.2
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 +515 -195
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/README.md +8 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +179 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +2 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +16 -16
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +70 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +99 -14
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +86 -1
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +128 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +124 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- package/tests/worktree_guard.mjs +2 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* governance.css — Page-local styles for the Governance page (roles /
|
|
2
|
+
permissions / assignments — BV1.R111, ADR 0151). Built on the shared design
|
|
3
|
+
tokens (style.css), mirroring gate.css's card + pill idiom; the ?v= cache-bust
|
|
4
|
+
is stamped from this file's content hash at boot (src/asset-version.js). */
|
|
5
|
+
|
|
6
|
+
.gov-h { color: var(--ink); font-size: 16px; font-weight: 600; margin: 22px 0 8px; }
|
|
7
|
+
.gov-h--minor { font-size: 13px; margin: 18px 0 6px; }
|
|
8
|
+
.gov-grid-note { color: var(--ink-soft); font-size: 12.5px; margin: 0 0 12px; }
|
|
9
|
+
.gov-sealed, .gov-empty { color: var(--ink-soft); padding: 16px 0; }
|
|
10
|
+
|
|
11
|
+
/* Two columns on wide screens: role rail left, permission grid right. */
|
|
12
|
+
.gov-layout { display: grid; grid-template-columns: 260px 1fr; gap: 22px; align-items: start; }
|
|
13
|
+
@media (max-width: 760px) { .gov-layout { grid-template-columns: 1fr; } }
|
|
14
|
+
|
|
15
|
+
/* ---- role list ---------------------------------------------------------- */
|
|
16
|
+
.gov-role-list { display: flex; flex-direction: column; gap: 8px; }
|
|
17
|
+
.gov-role {
|
|
18
|
+
text-align: left; font: inherit; cursor: pointer;
|
|
19
|
+
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
20
|
+
background: var(--surface); padding: 10px 12px;
|
|
21
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
22
|
+
}
|
|
23
|
+
.gov-role:hover { background: var(--surface-2); }
|
|
24
|
+
.gov-role[aria-pressed="true"] { border-color: var(--accent); box-shadow: inset 2px 0 0 var(--accent); }
|
|
25
|
+
.gov-role__head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
26
|
+
.gov-role__label { font-weight: 600; color: var(--ink); font-size: 13.5px; }
|
|
27
|
+
.gov-role__meta { color: var(--ink-faint); font-size: 11.5px; }
|
|
28
|
+
|
|
29
|
+
.gov-pill {
|
|
30
|
+
font-size: 10.5px; font-weight: 600;
|
|
31
|
+
letter-spacing: 0.04em; text-transform: uppercase;
|
|
32
|
+
padding: 1px 7px; border-radius: 999px; white-space: nowrap;
|
|
33
|
+
border: 1px solid var(--border); color: var(--ink-soft); background: var(--surface-2);
|
|
34
|
+
}
|
|
35
|
+
.gov-pill--seeded { background: var(--info-weak); border-color: color-mix(in srgb, var(--info) 30%, transparent); color: var(--info); }
|
|
36
|
+
.gov-pill--custom { background: var(--ok-weak, var(--surface-2)); border-color: color-mix(in srgb, var(--ok) 30%, transparent); color: var(--ok); }
|
|
37
|
+
.gov-pill--locked { background: var(--danger-weak); border-color: color-mix(in srgb, var(--danger) 30%, transparent); color: var(--danger); }
|
|
38
|
+
.gov-pill--floor { font-variant-numeric: tabular-nums; }
|
|
39
|
+
|
|
40
|
+
/* ---- permission grid ---------------------------------------------------- */
|
|
41
|
+
.gov-section { margin: 0 0 18px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
|
|
42
|
+
.gov-section__head {
|
|
43
|
+
padding: 8px 14px; font-size: 11.5px; font-weight: 600;
|
|
44
|
+
letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-soft);
|
|
45
|
+
background: var(--surface-2); border-bottom: 1px solid var(--border);
|
|
46
|
+
}
|
|
47
|
+
.gov-perm { display: flex; align-items: center; gap: 10px; padding: 8px 14px; border-bottom: 1px solid var(--border); }
|
|
48
|
+
.gov-perm:last-child { border-bottom: 0; }
|
|
49
|
+
.gov-perm__main { flex: 1 1 auto; min-width: 0; }
|
|
50
|
+
.gov-perm__key {
|
|
51
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
|
|
52
|
+
color: var(--accent-text); background: var(--surface-2);
|
|
53
|
+
padding: 1px 5px; border-radius: 4px;
|
|
54
|
+
}
|
|
55
|
+
.gov-perm__guards { color: var(--ink-soft); font-size: 12px; margin-top: 2px; overflow-wrap: anywhere; }
|
|
56
|
+
|
|
57
|
+
/* The toggle: a small pill button. Held = filled; not held = outline. */
|
|
58
|
+
.gov-toggle {
|
|
59
|
+
font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
|
|
60
|
+
padding: 4px 12px; border-radius: 999px; white-space: nowrap;
|
|
61
|
+
border: 1px solid var(--border-strong); background: var(--surface); color: var(--ink-soft);
|
|
62
|
+
}
|
|
63
|
+
.gov-toggle[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
64
|
+
.gov-toggle:hover:not(:disabled) { border-color: var(--accent); }
|
|
65
|
+
.gov-toggle:disabled { opacity: 0.55; cursor: default; }
|
|
66
|
+
.gov-held-note { color: var(--ok); font-size: 12px; white-space: nowrap; }
|
|
67
|
+
|
|
68
|
+
/* ---- create / assign forms ---------------------------------------------- */
|
|
69
|
+
.gov-create, .gov-assign { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
|
|
70
|
+
.gov-assign { flex-direction: row; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
|
|
71
|
+
.gov-input {
|
|
72
|
+
font: inherit; font-size: 13px; color: var(--ink);
|
|
73
|
+
background: var(--surface); border: 1px solid var(--border-strong);
|
|
74
|
+
border-radius: var(--radius-sm); padding: 7px 10px;
|
|
75
|
+
}
|
|
76
|
+
.gov-input:focus { outline: none; border-color: var(--accent); }
|
|
77
|
+
.gov-input--id { width: 110px; }
|
|
78
|
+
.gov-btn {
|
|
79
|
+
font: inherit; font-size: 13px; font-weight: 600;
|
|
80
|
+
padding: 7px 16px; border: 1px solid var(--accent);
|
|
81
|
+
background: var(--accent); color: #fff;
|
|
82
|
+
border-radius: var(--radius-sm); cursor: pointer; align-self: flex-start;
|
|
83
|
+
}
|
|
84
|
+
.gov-btn:hover:not(:disabled) { background: var(--accent-deep); border-color: var(--accent-deep); }
|
|
85
|
+
.gov-btn:disabled { opacity: 0.45; cursor: default; }
|
|
86
|
+
.gov-btn--ghost { background: var(--surface); color: var(--ink); border-color: var(--border-strong); }
|
|
87
|
+
.gov-btn--ghost:hover:not(:disabled) { background: var(--surface-2); }
|
|
88
|
+
.gov-btn--small { font-size: 12px; padding: 4px 10px; }
|
|
89
|
+
|
|
90
|
+
/* ---- assignments + principals ------------------------------------------ */
|
|
91
|
+
.gov-assign-row { display: flex; align-items: center; gap: 10px; padding: 8px 14px; border-bottom: 1px solid var(--border); }
|
|
92
|
+
.gov-assign-row:last-child { border-bottom: 0; }
|
|
93
|
+
.gov-assign-row__who { flex: 1 1 auto; color: var(--ink); font-size: 13px; min-width: 0; overflow-wrap: anywhere; }
|
|
94
|
+
.gov-assign-row__who .muted { color: var(--ink-faint); }
|
|
95
|
+
.gov-principal { padding: 8px 14px; border-bottom: 1px solid var(--border); font-size: 12.5px; color: var(--ink-soft); }
|
|
96
|
+
.gov-principal:last-child { border-bottom: 0; }
|
|
97
|
+
.gov-principal code {
|
|
98
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
|
|
99
|
+
color: var(--accent-text); background: var(--surface-2); padding: 1px 5px; border-radius: 4px;
|
|
100
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Governance — {{worldName}}</title>
|
|
7
|
+
<meta name="description" content="Roles and permissions — create a custom role, grant everyday permissions, assign it to a builder. Trust-boundary powers stay locked.">
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
12
|
+
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
13
|
+
<link rel="stylesheet" href="/builders/governance.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body data-page="governance" data-page-title="Governance">
|
|
16
|
+
|
|
17
|
+
<div class="app">
|
|
18
|
+
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
19
|
+
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
20
|
+
<div class="app-body">
|
|
21
|
+
<header class="app-topbar" id="app-topbar"></header>
|
|
22
|
+
<div class="app-content">
|
|
23
|
+
<div class="page-head"><p class="page-head__sub" id="gov-sub">Loading…</p></div>
|
|
24
|
+
|
|
25
|
+
<main>
|
|
26
|
+
<p class="gov-sealed" id="gov-sealed" hidden>Archons only.</p>
|
|
27
|
+
|
|
28
|
+
<section id="gov-body" hidden>
|
|
29
|
+
<div class="gov-layout">
|
|
30
|
+
<div class="gov-roles-col">
|
|
31
|
+
<h2 class="gov-h">Roles</h2>
|
|
32
|
+
<div class="gov-role-list" id="gov-role-list"></div>
|
|
33
|
+
<form class="gov-create" id="gov-create-form" autocomplete="off">
|
|
34
|
+
<h3 class="gov-h gov-h--minor">New custom role</h3>
|
|
35
|
+
<input class="gov-input" id="gov-create-key" placeholder="role-key (kebab-case)" maxlength="64" required>
|
|
36
|
+
<input class="gov-input" id="gov-create-label" placeholder="Label" maxlength="200" required>
|
|
37
|
+
<input class="gov-input" id="gov-create-desc" placeholder="Description (optional)" maxlength="2000">
|
|
38
|
+
<button class="gov-btn" type="submit">Create role</button>
|
|
39
|
+
</form>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="gov-grid-col">
|
|
43
|
+
<h2 class="gov-h" id="gov-grid-title">Permissions</h2>
|
|
44
|
+
<p class="gov-grid-note" id="gov-grid-note"></p>
|
|
45
|
+
<div id="gov-grid"></div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<h2 class="gov-h">Assignments</h2>
|
|
50
|
+
<p class="gov-grid-note">Hand a custom role to a builder. Rank roles are assigned by rank itself and cannot be granted here.</p>
|
|
51
|
+
<form class="gov-assign" id="gov-assign-form" autocomplete="off">
|
|
52
|
+
<input class="gov-input gov-input--id" id="gov-assign-builder" type="number" min="1" placeholder="Builder id" required>
|
|
53
|
+
<select class="gov-input" id="gov-assign-role" required></select>
|
|
54
|
+
<button class="gov-btn" type="submit">Assign</button>
|
|
55
|
+
</form>
|
|
56
|
+
<div id="gov-assignments"></div>
|
|
57
|
+
|
|
58
|
+
<h2 class="gov-h">Machine principals</h2>
|
|
59
|
+
<p class="gov-grid-note">Capabilities held by a machine identity, never by a person or a role. Shown for completeness — nothing here can be granted.</p>
|
|
60
|
+
<div id="gov-principals"></div>
|
|
61
|
+
</section>
|
|
62
|
+
</main>
|
|
63
|
+
|
|
64
|
+
<footer class="app-foot" id="app-foot"></footer>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
70
|
+
|
|
71
|
+
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
72
|
+
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
73
|
+
<script src="/builders/palette.js" defer></script>
|
|
74
|
+
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
75
|
+
<script src="/builders/governance.js?v=2026-08-06-governance"></script>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// public-builders/governance.js — Governance (/builders/governance).
|
|
2
|
+
//
|
|
3
|
+
// The roles/permissions/assignments tab (BV1.R111, ADR 0151 §2) over the R110
|
|
4
|
+
// CRUD routes. Discord-style: pick a role on the left, toggle its OPERATIONAL
|
|
5
|
+
// permissions in the grid, assign custom roles to builders below. Two walls the
|
|
6
|
+
// SERVER enforces and this page only renders honestly (ADR 0016 — nothing here
|
|
7
|
+
// grants anything):
|
|
8
|
+
// • `system` (trust-boundary) permissions render LOCKED — visible, never
|
|
9
|
+
// togglable; the API refuses them as `permission_not_grantable`.
|
|
10
|
+
// • the four seeded rank-roles are read-only — their grants derive from
|
|
11
|
+
// catalog floors and their assignment from builders.rank (R95b), so both
|
|
12
|
+
// toggle + assign are disabled and the API refuses `seeded_role_immutable`
|
|
13
|
+
// / `seeded_role_assignment_forbidden`.
|
|
14
|
+
//
|
|
15
|
+
// Like The Gate, this reads /api/bongos/me for a friendly Sealed notice —
|
|
16
|
+
// DEFENCE IN DEPTH only. Every /governance/* endpoint (reads included) is ALSO
|
|
17
|
+
// gated server-side on the `governance.manage` atom (archon floor). On 401 →
|
|
18
|
+
// web sign-in.
|
|
19
|
+
//
|
|
20
|
+
// DELIBERATELY NOT IN THE SIDEBAR YET: the nav entry + the server page gate are
|
|
21
|
+
// R113 (the next slice) — tests/hall_nav.mjs pins the Governance nav group and
|
|
22
|
+
// grows with that slice, not this one. Until then the page is reached by URL.
|
|
23
|
+
|
|
24
|
+
(() => {
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const API = '/api/bongos';
|
|
28
|
+
// Generated typed client (task 1996 / R09), served as window.BongosClient by
|
|
29
|
+
// /lib/bongos-client.js. baseUrl:'' passes the full `${API}${path}` URLs below
|
|
30
|
+
// through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
|
|
31
|
+
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
32
|
+
const { escapeHtml } = window.OTB;
|
|
33
|
+
const toast = (msg) => window.OTB.toast(msg);
|
|
34
|
+
|
|
35
|
+
// ---- fetch helpers (mirror gate.js) --------------------------------------
|
|
36
|
+
|
|
37
|
+
async function fetchJson(path) {
|
|
38
|
+
const res = await api.request('GET', `${API}${path}`);
|
|
39
|
+
if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw { kind: '401' }; }
|
|
40
|
+
if (res.status === 403) { throw { kind: '403' }; }
|
|
41
|
+
if (!res.ok) throw new Error(`${path} → ${res.status}`);
|
|
42
|
+
return res.data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function sendJson(method, path, body) {
|
|
46
|
+
const res = await api.request(method, `${API}${path}`, { body: body || {} });
|
|
47
|
+
if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw { kind: '401' }; }
|
|
48
|
+
const data = res.data || {};
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const err = data.error || {};
|
|
51
|
+
throw new Error(err.message && err.message !== err.code ? err.message : friendlyError(err.code) || `${path} → ${res.status}`);
|
|
52
|
+
}
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The R110 refusal codes, translated for the person clicking.
|
|
57
|
+
const ERROR_COPY = {
|
|
58
|
+
permission_not_grantable: 'That is a trust-boundary permission — it can never be granted to a custom role.',
|
|
59
|
+
seeded_role_immutable: 'Rank roles are read-only: their permissions derive from the catalog, not from this page.',
|
|
60
|
+
seeded_role_assignment_forbidden: 'Rank roles are assigned by rank itself — change the builder’s rank instead.',
|
|
61
|
+
role_exists: 'A role with that key already exists.',
|
|
62
|
+
role_not_found: 'That role no longer exists — refreshing.',
|
|
63
|
+
builder_not_found: 'No builder with that id.',
|
|
64
|
+
unknown_permission: 'The permission catalog does not define that key.',
|
|
65
|
+
bad_role_key: 'Role keys are kebab-case: lowercase letters, digits, dashes.',
|
|
66
|
+
validation_failed: 'Missing or invalid fields.',
|
|
67
|
+
};
|
|
68
|
+
const friendlyError = (code) => ERROR_COPY[code] || null;
|
|
69
|
+
|
|
70
|
+
// ---- state ---------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
let roles = []; // [{ role_key, label, description, is_system, seeded_rank, permissions: [] }]
|
|
73
|
+
let permissions = []; // catalog rows: [{ key, system, floor, guards, grantable }]
|
|
74
|
+
let principals = []; // [{ system_role, permission, guards }]
|
|
75
|
+
let assignments = []; // [{ builder_id, github_login, display_name, rank, role_key, role_label, is_system }]
|
|
76
|
+
let selectedRoleKey = null;
|
|
77
|
+
|
|
78
|
+
const selectedRole = () => roles.find((r) => r.role_key === selectedRoleKey) || null;
|
|
79
|
+
|
|
80
|
+
// ---- render: role list ---------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function renderRoles() {
|
|
83
|
+
const list = document.getElementById('gov-role-list');
|
|
84
|
+
list.innerHTML = '';
|
|
85
|
+
for (const role of roles) {
|
|
86
|
+
const pill = role.is_system
|
|
87
|
+
? `<span class="gov-pill gov-pill--seeded">rank role</span>`
|
|
88
|
+
: `<span class="gov-pill gov-pill--custom">custom</span>`;
|
|
89
|
+
const btn = document.createElement('button');
|
|
90
|
+
btn.type = 'button';
|
|
91
|
+
btn.className = 'gov-role';
|
|
92
|
+
btn.setAttribute('aria-pressed', role.role_key === selectedRoleKey ? 'true' : 'false');
|
|
93
|
+
btn.innerHTML =
|
|
94
|
+
`<span class="gov-role__head"><span class="gov-role__label">${escapeHtml(role.label || role.role_key)}</span>${pill}</span>
|
|
95
|
+
<span class="gov-role__meta"><code>${escapeHtml(role.role_key)}</code> · ${(role.permissions || []).length} permission${(role.permissions || []).length === 1 ? '' : 's'}</span>`;
|
|
96
|
+
btn.addEventListener('click', () => { selectedRoleKey = role.role_key; renderRoles(); renderGrid(); });
|
|
97
|
+
list.appendChild(btn);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- render: permission grid --------------------------------------------
|
|
102
|
+
|
|
103
|
+
function permRow(p, role) {
|
|
104
|
+
const held = !!role && (role.permissions || []).includes(p.key);
|
|
105
|
+
const row = document.createElement('div');
|
|
106
|
+
row.className = 'gov-perm';
|
|
107
|
+
|
|
108
|
+
let control;
|
|
109
|
+
if (!p.grantable) {
|
|
110
|
+
// The R110 wall, rendered: visible, never togglable.
|
|
111
|
+
control = `<span class="gov-pill gov-pill--locked" title="Trust-boundary permission — non-grantable (ADR 0151)">locked</span>`;
|
|
112
|
+
} else if (!role) {
|
|
113
|
+
control = '';
|
|
114
|
+
} else if (role.is_system) {
|
|
115
|
+
// Seeded rank-role: show what it holds, allow nothing.
|
|
116
|
+
control = held ? `<span class="gov-held-note">held via rank</span>` : '';
|
|
117
|
+
} else {
|
|
118
|
+
control = `<button type="button" class="gov-toggle" aria-pressed="${held ? 'true' : 'false'}" data-perm="${escapeHtml(p.key)}">${held ? 'granted' : 'grant'}</button>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
row.innerHTML =
|
|
122
|
+
`<div class="gov-perm__main">
|
|
123
|
+
<code class="gov-perm__key">${escapeHtml(p.key)}</code>
|
|
124
|
+
<span class="gov-pill gov-pill--floor">${escapeHtml(p.floor || 'principal')}</span>
|
|
125
|
+
<div class="gov-perm__guards">${escapeHtml(p.guards || '')}</div>
|
|
126
|
+
</div>
|
|
127
|
+
${control}`;
|
|
128
|
+
|
|
129
|
+
const btn = row.querySelector('button.gov-toggle');
|
|
130
|
+
if (btn) btn.addEventListener('click', () => toggleGrant(role, p.key, !held, btn));
|
|
131
|
+
return row;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function section(title, rows) {
|
|
135
|
+
const el = document.createElement('section');
|
|
136
|
+
el.className = 'gov-section';
|
|
137
|
+
el.innerHTML = `<div class="gov-section__head">${escapeHtml(title)}</div>`;
|
|
138
|
+
for (const r of rows) el.appendChild(r);
|
|
139
|
+
return el;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderGrid() {
|
|
143
|
+
const grid = document.getElementById('gov-grid');
|
|
144
|
+
const role = selectedRole();
|
|
145
|
+
document.getElementById('gov-grid-title').textContent = role
|
|
146
|
+
? `Permissions — ${role.label || role.role_key}` : 'Permissions';
|
|
147
|
+
document.getElementById('gov-grid-note').textContent = !role
|
|
148
|
+
? 'Pick a role to see and edit what it may do.'
|
|
149
|
+
: role.is_system
|
|
150
|
+
? 'A rank role’s permissions derive from the catalog and cannot be edited here.'
|
|
151
|
+
: 'Toggle the everyday permissions this role carries. Locked rows are trust-boundary powers no custom role can ever hold.';
|
|
152
|
+
|
|
153
|
+
grid.innerHTML = '';
|
|
154
|
+
const humanPerms = permissions.filter((p) => !principals.some((pr) => pr.permission === p.key));
|
|
155
|
+
const locked = humanPerms.filter((p) => !p.grantable);
|
|
156
|
+
const operational = humanPerms.filter((p) => p.grantable);
|
|
157
|
+
grid.appendChild(section(`Trust boundary — locked (${locked.length})`, locked.map((p) => permRow(p, role))));
|
|
158
|
+
grid.appendChild(section(`Operational (${operational.length})`, operational.map((p) => permRow(p, role))));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function toggleGrant(role, permissionKey, grant, btn) {
|
|
162
|
+
btn.disabled = true;
|
|
163
|
+
try {
|
|
164
|
+
await sendJson(grant ? 'PUT' : 'DELETE', `/governance/roles/${encodeURIComponent(role.role_key)}/permissions/${encodeURIComponent(permissionKey)}`);
|
|
165
|
+
toast(grant ? `Granted ${permissionKey}.` : `Revoked ${permissionKey}.`);
|
|
166
|
+
await reloadRoles();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err && err.kind === '401') return;
|
|
169
|
+
toast(`Could not ${grant ? 'grant' : 'revoke'}: ${err.message || err}`);
|
|
170
|
+
btn.disabled = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---- render: assignments -------------------------------------------------
|
|
175
|
+
|
|
176
|
+
function renderAssignments() {
|
|
177
|
+
const box = document.getElementById('gov-assignments');
|
|
178
|
+
box.innerHTML = '';
|
|
179
|
+
if (assignments.length === 0) {
|
|
180
|
+
box.innerHTML = `<p class="gov-empty">No role assignments yet.</p>`;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const wrap = document.createElement('section');
|
|
184
|
+
wrap.className = 'gov-section';
|
|
185
|
+
for (const a of assignments) {
|
|
186
|
+
const row = document.createElement('div');
|
|
187
|
+
row.className = 'gov-assign-row';
|
|
188
|
+
const who = `${escapeHtml(a.display_name || a.github_login || `builder ${a.builder_id}`)} <span class="muted">(#${escapeHtml(String(a.builder_id))} · ${escapeHtml(a.rank || '')})</span>`;
|
|
189
|
+
const rolePill = a.is_system
|
|
190
|
+
? `<span class="gov-pill gov-pill--seeded">${escapeHtml(a.role_label || a.role_key)} · via rank</span>`
|
|
191
|
+
: `<span class="gov-pill gov-pill--custom">${escapeHtml(a.role_label || a.role_key)}</span>`;
|
|
192
|
+
const action = a.is_system ? '' : `<button type="button" class="gov-btn gov-btn--ghost gov-btn--small" data-act="unassign">Remove</button>`;
|
|
193
|
+
row.innerHTML = `<span class="gov-assign-row__who">${who}</span>${rolePill}${action}`;
|
|
194
|
+
const btn = row.querySelector('[data-act=unassign]');
|
|
195
|
+
if (btn) btn.addEventListener('click', () => unassign(a, btn));
|
|
196
|
+
wrap.appendChild(row);
|
|
197
|
+
}
|
|
198
|
+
box.appendChild(wrap);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderAssignSelect() {
|
|
202
|
+
const sel = document.getElementById('gov-assign-role');
|
|
203
|
+
const custom = roles.filter((r) => !r.is_system);
|
|
204
|
+
sel.innerHTML = custom.length
|
|
205
|
+
? custom.map((r) => `<option value="${escapeHtml(r.role_key)}">${escapeHtml(r.label || r.role_key)}</option>`).join('')
|
|
206
|
+
: `<option value="" disabled selected>No custom roles yet</option>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function unassign(a, btn) {
|
|
210
|
+
btn.disabled = true;
|
|
211
|
+
try {
|
|
212
|
+
await sendJson('DELETE', `/governance/assignments/${encodeURIComponent(a.builder_id)}/${encodeURIComponent(a.role_key)}`);
|
|
213
|
+
toast(`Removed ${a.role_label || a.role_key} from builder ${a.builder_id}.`);
|
|
214
|
+
await reloadAssignments();
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (err && err.kind === '401') return;
|
|
217
|
+
toast(`Could not remove: ${err.message || err}`);
|
|
218
|
+
btn.disabled = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- render: principals --------------------------------------------------
|
|
223
|
+
|
|
224
|
+
function renderPrincipals() {
|
|
225
|
+
const box = document.getElementById('gov-principals');
|
|
226
|
+
box.innerHTML = '';
|
|
227
|
+
if (principals.length === 0) {
|
|
228
|
+
box.innerHTML = `<p class="gov-empty">No machine principals declared.</p>`;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const wrap = document.createElement('section');
|
|
232
|
+
wrap.className = 'gov-section';
|
|
233
|
+
for (const p of principals) {
|
|
234
|
+
const row = document.createElement('div');
|
|
235
|
+
row.className = 'gov-principal';
|
|
236
|
+
row.innerHTML = `<code>${escapeHtml(p.system_role)}</code> holds <code>${escapeHtml(p.permission)}</code> — ${escapeHtml(p.guards || '')} <span class="gov-pill gov-pill--locked">locked</span>`;
|
|
237
|
+
wrap.appendChild(row);
|
|
238
|
+
}
|
|
239
|
+
box.appendChild(wrap);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---- loads ---------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
async function reloadRoles() {
|
|
245
|
+
const data = await fetchJson('/governance/roles');
|
|
246
|
+
roles = data.roles || [];
|
|
247
|
+
if (!roles.some((r) => r.role_key === selectedRoleKey)) selectedRoleKey = roles.length ? roles[0].role_key : null;
|
|
248
|
+
renderRoles();
|
|
249
|
+
renderGrid();
|
|
250
|
+
renderAssignSelect();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function reloadAssignments() {
|
|
254
|
+
const data = await fetchJson('/governance/assignments');
|
|
255
|
+
assignments = data.assignments || [];
|
|
256
|
+
renderAssignments();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function load() {
|
|
260
|
+
const perms = await fetchJson('/governance/permissions');
|
|
261
|
+
permissions = perms.permissions || [];
|
|
262
|
+
principals = perms.principals || [];
|
|
263
|
+
await reloadRoles();
|
|
264
|
+
await reloadAssignments();
|
|
265
|
+
renderPrincipals();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---- forms ---------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
function wireForms() {
|
|
271
|
+
document.getElementById('gov-create-form').addEventListener('submit', async (e) => {
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
const btn = e.target.querySelector('button');
|
|
274
|
+
btn.disabled = true;
|
|
275
|
+
const body = {
|
|
276
|
+
role_key: document.getElementById('gov-create-key').value.trim(),
|
|
277
|
+
label: document.getElementById('gov-create-label').value.trim(),
|
|
278
|
+
};
|
|
279
|
+
const desc = document.getElementById('gov-create-desc').value.trim();
|
|
280
|
+
if (desc) body.description = desc;
|
|
281
|
+
try {
|
|
282
|
+
const r = await sendJson('POST', '/governance/roles', body);
|
|
283
|
+
toast(`Created role ${r.role ? r.role.role_key : body.role_key}.`);
|
|
284
|
+
e.target.reset();
|
|
285
|
+
selectedRoleKey = body.role_key;
|
|
286
|
+
await reloadRoles();
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (err && err.kind === '401') return;
|
|
289
|
+
toast(`Could not create role: ${err.message || err}`);
|
|
290
|
+
} finally {
|
|
291
|
+
btn.disabled = false;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
document.getElementById('gov-assign-form').addEventListener('submit', async (e) => {
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
const btn = e.target.querySelector('button');
|
|
298
|
+
const builderId = Number(document.getElementById('gov-assign-builder').value);
|
|
299
|
+
const roleKey = document.getElementById('gov-assign-role').value;
|
|
300
|
+
if (!roleKey) { toast('Create a custom role first.'); return; }
|
|
301
|
+
btn.disabled = true;
|
|
302
|
+
try {
|
|
303
|
+
await sendJson('POST', '/governance/assignments', { builder_id: builderId, role_key: roleKey });
|
|
304
|
+
toast(`Assigned ${roleKey} to builder ${builderId}.`);
|
|
305
|
+
e.target.reset();
|
|
306
|
+
await reloadAssignments();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (err && err.kind === '401') return;
|
|
309
|
+
toast(`Could not assign: ${err.message || err}`);
|
|
310
|
+
} finally {
|
|
311
|
+
btn.disabled = false;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---- init ----------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
function showSealed() {
|
|
319
|
+
document.getElementById('gov-sealed').hidden = false;
|
|
320
|
+
document.getElementById('gov-body').hidden = true;
|
|
321
|
+
document.getElementById('gov-sub').textContent = 'Restricted.';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function init() {
|
|
325
|
+
// Confirm rank for a friendly sealed notice; the server gates the page and
|
|
326
|
+
// every /governance endpoint on `governance.manage` regardless (cosmetic
|
|
327
|
+
// defence-in-depth). governance.manage is system:true, so only the archon
|
|
328
|
+
// rank-role can hold it — rank is an accurate client-side proxy.
|
|
329
|
+
let me;
|
|
330
|
+
try {
|
|
331
|
+
me = await fetchJson('/me');
|
|
332
|
+
} catch (err) {
|
|
333
|
+
if (err && err.kind === '401') return;
|
|
334
|
+
if (err && err.kind === '403') { showSealed(); return; }
|
|
335
|
+
me = null;
|
|
336
|
+
}
|
|
337
|
+
const rank = (me && (me.builder || me).rank || '').toLowerCase();
|
|
338
|
+
if (rank !== 'archon') { showSealed(); return; }
|
|
339
|
+
|
|
340
|
+
document.getElementById('gov-sub').textContent = 'Roles, permissions, and who holds them — the trust boundary stays locked.';
|
|
341
|
+
document.getElementById('gov-body').hidden = false;
|
|
342
|
+
wireForms();
|
|
343
|
+
try {
|
|
344
|
+
await load();
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (err && err.kind === '401') return;
|
|
347
|
+
if (err && err.kind === '403') { showSealed(); return; }
|
|
348
|
+
document.getElementById('gov-grid').innerHTML = `<p class="gov-empty">Could not load governance data just now.</p>`;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
init();
|
|
353
|
+
})();
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// public-builders/harbor.js — the Harbor (/builders/harbor).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// block a builder from
|
|
3
|
+
// Metic+ dev-box management view (idea 257 / task 1163; Archon-only until ADR 0157
|
|
4
|
+
// moved `box.fleet.manage` to the working rank). Every builder's dev box is rendered
|
|
5
|
+
// as a card on the dock; a Metic or Archon can close any box and block a builder from
|
|
6
|
+
// opening new ones.
|
|
6
7
|
//
|
|
7
8
|
// Like the Watch page, this reads /api/bongos/me to learn the viewer's rank and
|
|
8
|
-
// shows
|
|
9
|
-
// endpoints (/boxes, /boxes/:id/close, /boxes/:id/block) are ALSO
|
|
10
|
-
// server-side (ADR 0016), and the page itself is gated by
|
|
11
|
-
//
|
|
9
|
+
// shows a restricted notice below the Metic floor — DEFENCE IN DEPTH only. The data +
|
|
10
|
+
// mutation endpoints (/boxes, /boxes/:id/close, /boxes/:id/block) are ALSO gated
|
|
11
|
+
// server-side on `box.fleet.manage` (ADR 0016), and the page itself is gated by
|
|
12
|
+
// serve-internal.js requireGovernancePage — whose own floor is `page.view.governance`,
|
|
13
|
+
// metic since ADR 0157. On a 401 it redirects into the web sign-in flow.
|
|
12
14
|
|
|
13
15
|
(() => {
|
|
14
16
|
'use strict';
|
|
@@ -324,7 +326,11 @@
|
|
|
324
326
|
me = null;
|
|
325
327
|
}
|
|
326
328
|
const rank = (me && (me.builder || me).rank || '').toLowerCase();
|
|
327
|
-
|
|
329
|
+
// ADR 0157: Harbor sits at the `page.view.governance` floor (metic), and the fleet
|
|
330
|
+
// data it reads is `box.fleet.manage` (also metic now). Cosmetic —
|
|
331
|
+
// requireGovernancePage + the route gates are the wall; this just picks body vs
|
|
332
|
+
// sealed notice.
|
|
333
|
+
if (rank !== 'metic' && rank !== 'archon') { showSealed(); return; }
|
|
328
334
|
|
|
329
335
|
document.getElementById('harbor-sub').textContent = 'Each builder’s dev box at a glance.';
|
|
330
336
|
document.getElementById('harbor-body').hidden = false;
|
|
@@ -137,7 +137,9 @@
|
|
|
137
137
|
try {
|
|
138
138
|
const me = await getJSON(`${API}/me`, { softAuth: true }).catch(() => null);
|
|
139
139
|
const rank = me && me.builder ? me.builder.rank : null;
|
|
140
|
-
|
|
140
|
+
// ADR 0157: `provisioning.fleet.manage` moved archon→metic, so the fleet section
|
|
141
|
+
// reveals for Metic+ too. Cosmetic — the route gates are the wall.
|
|
142
|
+
if (rank !== 'metic' && rank !== 'archon') return;
|
|
141
143
|
const [fleet, ledger] = await Promise.all([
|
|
142
144
|
getJSON(`${API}/provisioning/fleet`).catch(() => ({ instances: [] })),
|
|
143
145
|
getJSON(`${API}/provisioning/fleet/cost-ledger`).catch(() => null),
|
|
@@ -137,6 +137,12 @@
|
|
|
137
137
|
hosting_shape: 'standalone',
|
|
138
138
|
domain: domain || undefined,
|
|
139
139
|
target_ref: `${owner}/${repo}`,
|
|
140
|
+
// The mode is LOAD-BEARING for the control-plane runner, not just for which
|
|
141
|
+
// checklist we render (task 1002562): 'adopt' makes it clone + layer onto the
|
|
142
|
+
// owner's existing history instead of pushing a fresh tree, which an existing
|
|
143
|
+
// repo rejects. This form already tracked `mode` purely for renderNextSteps —
|
|
144
|
+
// it has to reach the server too.
|
|
145
|
+
onboard_mode: mode,
|
|
140
146
|
});
|
|
141
147
|
const instanceId = data && data.instance ? data.instance.id : null;
|
|
142
148
|
q('#np-form').hidden = true;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>People — {{worldName}}</title>
|
|
7
|
+
<meta name="description" content="The builder directory: everyone building across {{worldName}} projects, searchable and ranked by track record.">
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
12
|
+
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
13
|
+
</head>
|
|
14
|
+
<body data-page="people" data-page-title="People">
|
|
15
|
+
|
|
16
|
+
<div class="app">
|
|
17
|
+
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
18
|
+
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
19
|
+
<div class="app-body">
|
|
20
|
+
<header class="app-topbar" id="app-topbar"></header>
|
|
21
|
+
<div class="app-content">
|
|
22
|
+
<div class="page-head">
|
|
23
|
+
<p class="page-head__sub">Everyone building across hosted projects, ranked by track record. Descriptive only — cross-project numbers never carry authority here.</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<main>
|
|
27
|
+
<section class="scroll" id="people-scroll" aria-labelledby="people-h">
|
|
28
|
+
<h2 id="people-h" class="scroll__h">Builder directory</h2>
|
|
29
|
+
<div id="people-filters"></div>
|
|
30
|
+
<div id="people-body">
|
|
31
|
+
<div class="profile__placeholder">Loading the directory…</div>
|
|
32
|
+
</div>
|
|
33
|
+
</section>
|
|
34
|
+
</main>
|
|
35
|
+
|
|
36
|
+
<footer class="app-foot" id="app-foot"></footer>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
42
|
+
|
|
43
|
+
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
44
|
+
chrome exists before the page script runs. -->
|
|
45
|
+
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
46
|
+
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
47
|
+
<script src="/builders/palette.js" defer></script>
|
|
48
|
+
<script src="/builders/hall-kit.js"></script>
|
|
49
|
+
<script src="/builders/people.js"></script>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|