@bongos/core 1.19.995 → 1.19.997
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 +27 -17
- package/docs/api/openapi.json +2 -2
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/render-pilot.md +218 -0
- package/modules/lifecycle/routes/goal-route-authz.js +15 -1
- package/modules/lifecycle/routes/goals.js +12 -12
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/prove-strict-versioning.js +1 -1
- package/src/module-api.js +1 -1
- package/tests/goal_archive_disposition.mjs +2 -2
- package/tests/goal_owner_manager_powers.mjs +90 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.997",
|
|
6
|
+
"core_contract": "1.19.997",
|
|
7
|
+
"source_commit": "e3e7048278119785fc3a9a9ca14edefba9ecae54",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-24T00:11:45.316Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 520,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2406,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2952,
|
|
20
|
+
"tree_sha256": "ce61953a922e8482ee9341329d06ba7a71aa4367e0449c4d9a9990de9ff82e23",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2157,7 +2157,7 @@
|
|
|
2157
2157
|
{
|
|
2158
2158
|
"path": "docs/api/openapi.json",
|
|
2159
2159
|
"mode": "0000644",
|
|
2160
|
-
"sha256": "
|
|
2160
|
+
"sha256": "734016d0505821010e06c197ea1987e3eb4e9700d45ad32d982a5b9cd9da5aed"
|
|
2161
2161
|
},
|
|
2162
2162
|
{
|
|
2163
2163
|
"path": "docs/architecture.md",
|
|
@@ -2717,7 +2717,7 @@
|
|
|
2717
2717
|
{
|
|
2718
2718
|
"path": "docs/module-api-changelog.md",
|
|
2719
2719
|
"mode": "0000644",
|
|
2720
|
-
"sha256": "
|
|
2720
|
+
"sha256": "1b16fcc54d5a5c01d44d93ff307eef44ddec9a993bd81db0d41db4b71f4e5654"
|
|
2721
2721
|
},
|
|
2722
2722
|
{
|
|
2723
2723
|
"path": "docs/modules-contract.md",
|
|
@@ -2924,6 +2924,11 @@
|
|
|
2924
2924
|
"mode": "0000644",
|
|
2925
2925
|
"sha256": "f6d2bb1da53f03f54080b8e0aec9f63390d3343c32917b64225ac47ae4ba7b42"
|
|
2926
2926
|
},
|
|
2927
|
+
{
|
|
2928
|
+
"path": "docs/recipes/render-pilot.md",
|
|
2929
|
+
"mode": "0000644",
|
|
2930
|
+
"sha256": "7b5f391285931a5f5e1bd31683702d534e9876d55ce5a0794e7d8eeec52fc2f3"
|
|
2931
|
+
},
|
|
2927
2932
|
{
|
|
2928
2933
|
"path": "docs/recipes/reranking-a-goals-board.md",
|
|
2929
2934
|
"mode": "0000644",
|
|
@@ -6082,12 +6087,12 @@
|
|
|
6082
6087
|
{
|
|
6083
6088
|
"path": "modules/lifecycle/routes/goal-route-authz.js",
|
|
6084
6089
|
"mode": "0000644",
|
|
6085
|
-
"sha256": "
|
|
6090
|
+
"sha256": "89d282e6c5110ae698484ddab698a72f27c6fe509fc97ff628326fe42e79483d"
|
|
6086
6091
|
},
|
|
6087
6092
|
{
|
|
6088
6093
|
"path": "modules/lifecycle/routes/goals.js",
|
|
6089
6094
|
"mode": "0000644",
|
|
6090
|
-
"sha256": "
|
|
6095
|
+
"sha256": "8bd3b013fa96d062f15386e53f43dcdb918baf619c2beb5cf5333ef7052475f3"
|
|
6091
6096
|
},
|
|
6092
6097
|
{
|
|
6093
6098
|
"path": "modules/lifecycle/routes/help-requests.js",
|
|
@@ -8182,12 +8187,12 @@
|
|
|
8182
8187
|
{
|
|
8183
8188
|
"path": "package-lock.json",
|
|
8184
8189
|
"mode": "0000644",
|
|
8185
|
-
"sha256": "
|
|
8190
|
+
"sha256": "93a81b5f6bc16e21a3ad6abd52490ccfbefe0f2d11af8dfd6273f03dd0c91ca0"
|
|
8186
8191
|
},
|
|
8187
8192
|
{
|
|
8188
8193
|
"path": "package.json",
|
|
8189
8194
|
"mode": "0000644",
|
|
8190
|
-
"sha256": "
|
|
8195
|
+
"sha256": "fe2540636dec8118f88d3c037c991fb3f9f784a72edec9569ecce1f09ec4ab97"
|
|
8191
8196
|
},
|
|
8192
8197
|
{
|
|
8193
8198
|
"path": "public-docs/index.html",
|
|
@@ -9122,7 +9127,7 @@
|
|
|
9122
9127
|
{
|
|
9123
9128
|
"path": "scripts/gds/prove-strict-versioning.js",
|
|
9124
9129
|
"mode": "0000644",
|
|
9125
|
-
"sha256": "
|
|
9130
|
+
"sha256": "65bb758becd002ae830a8338ec6ba0bc21ec5ef91ad564c85a2a94b7ab6ffbbc"
|
|
9126
9131
|
},
|
|
9127
9132
|
{
|
|
9128
9133
|
"path": "scripts/gds/provision-byo-host.js",
|
|
@@ -10232,7 +10237,7 @@
|
|
|
10232
10237
|
{
|
|
10233
10238
|
"path": "src/module-api.js",
|
|
10234
10239
|
"mode": "0000644",
|
|
10235
|
-
"sha256": "
|
|
10240
|
+
"sha256": "aa4d373556efe35c070f90b0ff308b302fbb9887f06cd3655d0fb9df8b157faa"
|
|
10236
10241
|
},
|
|
10237
10242
|
{
|
|
10238
10243
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11657,7 +11662,7 @@
|
|
|
11657
11662
|
{
|
|
11658
11663
|
"path": "tests/goal_archive_disposition.mjs",
|
|
11659
11664
|
"mode": "0000644",
|
|
11660
|
-
"sha256": "
|
|
11665
|
+
"sha256": "de5b561ffe78ead5f6a1ad08066aaa2672627f441f8602fd46e1fdac453512e0"
|
|
11661
11666
|
},
|
|
11662
11667
|
{
|
|
11663
11668
|
"path": "tests/goal_archive_hall.mjs",
|
|
@@ -11764,6 +11769,11 @@
|
|
|
11764
11769
|
"mode": "0000644",
|
|
11765
11770
|
"sha256": "c93422d76ff7f0107190c6de1954f110e92a18913fabbcdea2b4f67693d538e6"
|
|
11766
11771
|
},
|
|
11772
|
+
{
|
|
11773
|
+
"path": "tests/goal_owner_manager_powers.mjs",
|
|
11774
|
+
"mode": "0000644",
|
|
11775
|
+
"sha256": "9853fe32906b842e6c78dd024e33bee5c00649ed2de2f6b593d736d0155e7832"
|
|
11776
|
+
},
|
|
11767
11777
|
{
|
|
11768
11778
|
"path": "tests/goal_routes.mjs",
|
|
11769
11779
|
"mode": "0000644",
|
package/docs/api/openapi.json
CHANGED
|
@@ -6127,7 +6127,7 @@
|
|
|
6127
6127
|
"goals"
|
|
6128
6128
|
],
|
|
6129
6129
|
"summary": "POST /goals/:id/archive",
|
|
6130
|
-
"description": "POST /goals/:id/archive — retire a goal (open|achieved → archived). Archived goals are re-openable (below). This is the lifecycle disposition the /goal-review skill reaches for once a goal is achieved (ADR 0086 §6 / BV1.R64). rank: metic+archon
|
|
6130
|
+
"description": "POST /goals/:id/archive — retire a goal (open|achieved → archived). Archived goals are re-openable (below). This is the lifecycle disposition the /goal-review skill reaches for once a goal is achieved (ADR 0086 §6 / BV1.R64). rank: metic+archon + owner|manager|Archon (task 1001739); archive is reversible, so a manager may.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `goal.archive` (all required).",
|
|
6131
6131
|
"x-rank": "metic+archon",
|
|
6132
6132
|
"x-source": "modules/lifecycle/routes/goals.js",
|
|
6133
6133
|
"x-permissions": [
|
|
@@ -7134,7 +7134,7 @@
|
|
|
7134
7134
|
"goals"
|
|
7135
7135
|
],
|
|
7136
7136
|
"summary": "POST /goals/:id/scope",
|
|
7137
|
-
"description": "POST /goals/:id/scope — \"ask to widen\": add module(s) to a goal's scope_modules (ADR 0086 §4 / BV1.R64). Scope is NEVER self-granted, so this is the gated approve-and-apply:
|
|
7137
|
+
"description": "POST /goals/:id/scope — \"ask to widen\": add module(s) to a goal's scope_modules (ADR 0086 §4 / BV1.R64). Scope is NEVER self-granted, so this is the gated approve-and-apply: the goal.scope.widen permission + the caller must be the goal's owner or a manager (or Archon), and a PROTECTED module can only be added by an Archon (mirrors the POST /goals create gate). A plain member therefore cannot widen at all — they ask a Metic+/Archon out-of-band, exactly as joining a protected goal works (R57). The goal must be open. Body: { add: [\"module-key\", ...] }; idempotent (set-union). rank: metic+archon + owner|manager|Archon (task 1001739) — widens the module wall; pinned in route-rank-check.EXPECTED_RANKS.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `goal.scope.widen` (all required).",
|
|
7138
7138
|
"x-rank": "metic+archon",
|
|
7139
7139
|
"x-source": "modules/lifecycle/routes/goals.js",
|
|
7140
7140
|
"x-permissions": [
|
|
@@ -2464,5 +2464,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2464
2464
|
landed since 1.19.993 with no explicit bump. run 35929622479. (task 1002620)
|
|
2465
2465
|
1.19.995 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2466
2466
|
landed since 1.19.994 with no explicit bump. run 35933457545. (task 1002620)
|
|
2467
|
+
1.19.996 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2468
|
+
landed since 1.19.995 with no explicit bump. run 35934673169. (task 1002620)
|
|
2469
|
+
1.19.997 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2470
|
+
landed since 1.19.996 with no explicit bump. run 35937246865. (task 1002620)
|
|
2467
2471
|
---------------------------------------------------------------------------
|
|
2468
2472
|
```
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Recipe — the Render pilot, on a throwaway `mercury` clone
|
|
2
|
+
|
|
3
|
+
The executable half of [task 1004142](https://cloudbongos.com/builders#/task/1004142): stand one Bongos instance up on Render, end to end, and answer the questions [task 1004183](https://cloudbongos.com/builders#/task/1004183) (the standup leg) will be written against. Governed by [ADR 0327](../adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md); platform choice from [ADR 0323](../adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md).
|
|
4
|
+
|
|
5
|
+
**Who runs this.** Whoever holds the Render key — under ADR 0327 that is the *project owner*, and for the pilot it is the platform owner. An agent session cannot run it unless someone has put a key in front of it, and a key must never be pasted into a chat.
|
|
6
|
+
|
|
7
|
+
**The API shape below was read from Render's own reference on 2026-09-23** (`api-docs.render.com`), not inferred. Where this recipe and a live response disagree, the response wins and this file is the bug.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 0. Before you start — three things worth knowing
|
|
12
|
+
|
|
13
|
+
**The pilot costs money, and the free tier cannot prove it.** Done-when clause 4 is "its database is backed up by Render rather than by our timer". Render's **free Postgres has no backups of any kind** — no logical backups, no PITR. Only paid databases are continually backed up (3 days of recovery window on Hobby, 7 on Pro). So the pilot must run a **paid** Postgres or clause 4 is unprovable. Budget `0.1c-256mb` ($6/mo) + `0.5c-512mb` web ($7/mo), prorated for the hours the pilot is up, and tear it down when finished.
|
|
14
|
+
|
|
15
|
+
**The key is account-wide, not workspace-wide.** `GET /owners` returns every workspace the key's *user* can reach, including other people's teams. This is the finding that produced ADR 0327, and step 1 exists to confirm it on live responses before anything is created.
|
|
16
|
+
|
|
17
|
+
**Do not touch live `mercury`.** Row 31 is a real owner's project, serving from our box today. Clone its *repo* to a scratch repo under an account we control, and stand the clone up under a scratch slug. Nothing in this recipe writes to row 31, to `willHermesLines/mercury_bongos`, or to that owner's Render account.
|
|
18
|
+
|
|
19
|
+
Set up once:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export RENDER_TOKEN=... # in your shell, not in a file, not in chat
|
|
23
|
+
export R="curl -sS -H \"Authorization: Bearer $RENDER_TOKEN\" -H 'Content-Type: application/json'"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Base URL is `https://api.render.com/v1`; auth is `Authorization: Bearer <key>`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. The `ownerId` guard — confirm the finding BEFORE creating anything
|
|
31
|
+
|
|
32
|
+
This is the pilot's most important output, because every call the standup leg later writes depends on it.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
curl -sS -H "Authorization: Bearer $RENDER_TOKEN" https://api.render.com/v1/owners | jq
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Each element is `{ owner: { id, name, email, type }, cursor }`, where `type` is `user` or `team`.
|
|
39
|
+
|
|
40
|
+
**Record:** how many workspaces come back, which are not ours, and their ids. Then pick the Bongos workspace's `id` and pin it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export RENDER_OWNER_ID=...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Then prove the hazard rather than assuming it.** List a resource *without* `ownerId` and check whether another workspace's resources appear:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -sS -H "Authorization: Bearer $RENDER_TOKEN" 'https://api.render.com/v1/services?limit=100' \
|
|
50
|
+
| jq '[.[].service | {id, name, ownerId}] | group_by(.ownerId) | map({ownerId: .[0].ownerId, n: length})'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If more than one `ownerId` comes back from a key that was only ever meant for one workspace, the finding is confirmed and the guard is mandatory. **Record the actual output either way** — a negative result is just as useful and would narrow ADR 0327's §"required guard".
|
|
54
|
+
|
|
55
|
+
**The rule the standup leg must implement, whatever this shows:**
|
|
56
|
+
|
|
57
|
+
- Send an explicit `ownerId` on every call. It is *required* on `POST /services` and `POST /postgres`, and it is *optional* on the list calls — which is exactly where the hazard lives.
|
|
58
|
+
- Assert `ownerId` on the **response**, not the request. A request you got right tells you nothing about a response that came from elsewhere.
|
|
59
|
+
- Treat missing or mismatched as a hard error. Never default it, and never derive the workspace by taking the first element of `/owners`.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 2. Clone the repo
|
|
64
|
+
|
|
65
|
+
`mercury`'s instance row carries `target_ref=willHermesLines/mercury_bongos` and `onboard_mode=adopt`. Clone it to a scratch repo you own — this is the pilot's subject and it is disposable.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/willHermesLines/mercury_bongos /tmp/mercury-clone
|
|
69
|
+
cd /tmp/mercury-clone && git remote remove origin
|
|
70
|
+
gh repo create <you>/render-pilot --private --source=. --push
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Record:** whether the repo is public or private, and under which account. That feeds the open question in step 5.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 3. Create the Postgres
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
curl -sS -X POST -H "Authorization: Bearer $RENDER_TOKEN" -H 'Content-Type: application/json' \
|
|
81
|
+
https://api.render.com/v1/postgres -d '{
|
|
82
|
+
"name": "render-pilot-db",
|
|
83
|
+
"ownerId": "'"$RENDER_OWNER_ID"'",
|
|
84
|
+
"plan": "basic_256mb",
|
|
85
|
+
"region": "virginia",
|
|
86
|
+
"version": "16"
|
|
87
|
+
}' | jq
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`plan`, `ownerId`, `name` and `version` are all required. `databaseName` and `databaseUser` are generated if omitted. **`plan` must not be `free`** — see §0. Region `virginia` is the closest US region to the current droplet (`PROVISION_REGION=nyc3`).
|
|
91
|
+
|
|
92
|
+
Then read the connection string:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
curl -sS -H "Authorization: Bearer $RENDER_TOKEN" \
|
|
96
|
+
https://api.render.com/v1/postgres/<postgresId>/connection-info | jq
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Returns `password`, `internalConnectionString`, `externalConnectionString`, `psqlCommand`, and optionally the pooled variants. **Assert the `ownerId` on the create response before going further.**
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 4. Create the web service
|
|
104
|
+
|
|
105
|
+
Bongos reads `DATABASE_URL` first and falls back to the `PG*` family (`src/bongos/pool.js:56`), so Render's connection string wires in directly — **none of the `PGHOST`/`PGDATABASE`/`PGUSER`/`PGPASSWORD` lines `webEnvBody` writes on the box are needed here.** That is the single biggest simplification Render buys, and it is worth stating in the standup leg's comments.
|
|
106
|
+
|
|
107
|
+
The env prefix is **`BONGOS`** (`config/branding.neutral.json` → `envPrefix`). The variables below are the Render translation of `webEnvBody` in `scripts/gds/provision-units.js:192` — read that function, not this list, if the two ever disagree.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
curl -sS -X POST -H "Authorization: Bearer $RENDER_TOKEN" -H 'Content-Type: application/json' \
|
|
111
|
+
https://api.render.com/v1/services -d '{
|
|
112
|
+
"type": "web_service",
|
|
113
|
+
"name": "render-pilot",
|
|
114
|
+
"ownerId": "'"$RENDER_OWNER_ID"'",
|
|
115
|
+
"repo": "https://github.com/<you>/render-pilot",
|
|
116
|
+
"branch": "main",
|
|
117
|
+
"autoDeploy": "yes",
|
|
118
|
+
"serviceDetails": {
|
|
119
|
+
"runtime": "node",
|
|
120
|
+
"plan": "0.5c-512mb",
|
|
121
|
+
"region": "virginia",
|
|
122
|
+
"healthCheckPath": "/healthz",
|
|
123
|
+
"envSpecificDetails": {
|
|
124
|
+
"buildCommand": "npm ci",
|
|
125
|
+
"startCommand": "npm start"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"envVars": [
|
|
129
|
+
{ "key": "DATABASE_URL", "value": "<internalConnectionString>" },
|
|
130
|
+
{ "key": "BONGOS_GITHUB_CLIENT_ID", "value": "" },
|
|
131
|
+
{ "key": "BONGOS_GITHUB_CLIENT_SECRET", "value": "" },
|
|
132
|
+
{ "key": "BONGOS_FIRST_ADMIN", "value": "<your-github-login>" },
|
|
133
|
+
{ "key": "BONGOS_PUBLIC_ORIGIN", "value": "https://render-pilot.onrender.com" },
|
|
134
|
+
{ "key": "BONGOS_BUILDERS_ORIGIN", "value": "https://render-pilot.onrender.com" },
|
|
135
|
+
{ "key": "BONGOS_STATUS_ORIGIN", "value": "https://render-pilot.onrender.com" },
|
|
136
|
+
{ "key": "BONGOS_OAUTH_ORIGIN", "value": "https://render-pilot.onrender.com" }
|
|
137
|
+
]
|
|
138
|
+
}' | jq
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Notes that matter:
|
|
142
|
+
|
|
143
|
+
- **`PORT` is not set by us.** Render injects it; `webEnvBody` writes it because the box allocates ports itself. One more line the standup leg drops.
|
|
144
|
+
- **The four origin overrides are load-bearing, not cosmetic.** Without them `src/branding.js` stays on neutral localhost and the hall never loads (task 1972). They must be the Render hostname, and they must all four be the same host — a single-host instance has no separate `builders.`/`status.` subdomain.
|
|
145
|
+
- **`BONGOS_FIRST_ADMIN` is what seats you as Archon** on first sign-in. Omit it and the instance has no owner and you can never claim it (the 2026-07-07 `demo` regression).
|
|
146
|
+
- **OAuth creds start blank** and are filled after the app exists — see step 5.
|
|
147
|
+
- `PGPOOL_MAX` is read bare and still applies. On `basic_256mb` the connection ceiling is low; if the app struggles to connect, set it small (the box uses `PROVISION_INSTANCE_PGPOOL_MAX`).
|
|
148
|
+
- Do **not** send a `secretFiles` entry for the GitHub App PEM until you know whether the App path is in scope for the pilot; blank OAuth is enough to prove boot.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 5. Sign-in, and the open question the pilot exists to answer
|
|
153
|
+
|
|
154
|
+
Create a GitHub OAuth app (or App) whose callback is the Render hostname, then set `BONGOS_GITHUB_CLIENT_ID` / `_SECRET` via `PATCH /services/{id}/env-vars/{key}` and redeploy.
|
|
155
|
+
|
|
156
|
+
**The question ADR 0327 left for this pilot:** can Render's GitHub App reach a repo under a **personal** account rather than an org, and what does that mean for a project whose repo we do not own? Record:
|
|
157
|
+
|
|
158
|
+
- Which GitHub account/org the pilot repo sits under, and whether Render's GitHub App installed onto it without friction.
|
|
159
|
+
- Whether a *private* repo under someone else's personal account is reachable at all.
|
|
160
|
+
- What the owner has to click, since under ADR 0327 the repo and the Render account are both theirs.
|
|
161
|
+
|
|
162
|
+
This is the answer that decides whether `cloud-host` standup is one API sequence or a sequence plus a human step — and therefore how much friction `cloud-host` actually removes over `byo-host`.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 6. Migrations
|
|
167
|
+
|
|
168
|
+
Run `migrate.sh` against the Render database. Prefer a Render **job** or the service's own shell over your laptop, so the run happens from inside Render's network on the `internalConnectionString`; the `externalConnectionString` works from outside but is not what production will use.
|
|
169
|
+
|
|
170
|
+
Then confirm no schema lag:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
curl -sS https://render-pilot.onrender.com/version | jq
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 7. Custom domain (optional for the pilot, required for the model)
|
|
179
|
+
|
|
180
|
+
ADR 0323 keeps **Cloudflare DNS as the front door** and drops the Caddy site block, because Render terminates TLS. Prove the chain end to end at least once:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
curl -sS -X POST -H "Authorization: Bearer $RENDER_TOKEN" -H 'Content-Type: application/json' \
|
|
184
|
+
https://api.render.com/v1/services/<serviceId>/custom-domains -d '{"name": "render-pilot.cloudbongos.com"}' | jq
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Then point the Cloudflare record at Render and confirm the response header is **no longer `via: 1.1 Caddy`** — that header is precisely how we established the live `mercury` instance is still on our box, and its absence is the cleanest proof the front door moved.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 8. Capture the evidence — the five done-when clauses
|
|
192
|
+
|
|
193
|
+
Record each in the session log, with the actual output, not a claim that it worked:
|
|
194
|
+
|
|
195
|
+
| # | Clause | Evidence |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| 1 | Boots | `GET /healthz` → 200, and the header does **not** say `via: 1.1 Caddy` |
|
|
198
|
+
| 2 | Signs in | the OAuth round trip completes and seats you as Archon |
|
|
199
|
+
| 3 | Migrations run | `migrate.sh` completes; `/version` reports no schema lag |
|
|
200
|
+
| 4 | **Render backs up the DB** | a backup exists in Render's dashboard/API that we did not create — this is the clause that retires `provision-units.js`'s backup service + timer, and it needs a **paid** database |
|
|
201
|
+
| 5 | GitHub App reach | written answer to §5 |
|
|
202
|
+
|
|
203
|
+
Plus the three the standup leg is written from: the **exact call sequence** above with real request/response shapes, the **`ownerId` guard's live behaviour** from §1, and whether the **default plan sizes** held or mercury's larger choice was right.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 9. Tear the clone down
|
|
208
|
+
|
|
209
|
+
The pilot is disposable and it is costing money. Delete the web service, delete the Postgres, delete the scratch repo, and remove the Cloudflare record if you made one.
|
|
210
|
+
|
|
211
|
+
**Note the asymmetry deliberately:** you are deleting resources in *your own* account, by hand. ADR 0327 decision 3 says the platform must never hold the ability to destroy infrastructure it does not own — `provision-teardown.js` retargets to *forgetting*, not to a Render delete call ([task 1004183](https://cloudbongos.com/builders#/task/1004183)). The fact that this step is manual is the design working, not a gap in it.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Sources
|
|
216
|
+
|
|
217
|
+
- [Render API reference](https://api-docs.render.com/reference/introduction) — endpoints, required fields, plan and region enums, read 2026-09-23
|
|
218
|
+
- [Render Postgres Recovery and Backups](https://render.com/docs/postgresql-backups) — free plans have no backups; paid plans get PITR with 3-day (Hobby) / 7-day (Pro) windows
|
|
@@ -14,10 +14,23 @@ const api = require('../../../src/module-api');
|
|
|
14
14
|
const db = require('../db');
|
|
15
15
|
const { isModuleKey, scopeIncludesProtected, moduleKeys } = api.moduleScopeMap;
|
|
16
16
|
const goalScopeCheck = api.goalScopeCheck;
|
|
17
|
-
const { isGoalOwner, authorizeGoalManagement } = require('../goal-authz');
|
|
17
|
+
const { isGoalOwner, isGoalOwnerOrManager, authorizeGoalManagement } = require('../goal-authz');
|
|
18
18
|
const { versionAcceptsNewWork } = require('../db-tasks.js');
|
|
19
19
|
const METIC_PLUS = ['metic', 'archon'];
|
|
20
20
|
|
|
21
|
+
// isOwnerManagerOrArchon — a goal's OWN authority (task 1001739): owner (created_by),
|
|
22
|
+
// manager ('lead' membership) or Archon. Gates archive / reopen / scope-widen; a plain
|
|
23
|
+
// member holds none of these, and no route deletes a goal. An OWNERLESS goal
|
|
24
|
+
// (created_by NULL — the migration-160 system goals) has no owner or manager to
|
|
25
|
+
// ask, so any member keeps the power they had before; only Archons would otherwise
|
|
26
|
+
// be left able to disposition it.
|
|
27
|
+
async function isOwnerManagerOrArchon(builder, goal) {
|
|
28
|
+
if (builder.rank === 'archon') return true;
|
|
29
|
+
if (goal.created_by == null) return db.isGoalMember({ goalId: goal.id, builderId: builder.id });
|
|
30
|
+
const actorMembership = await db.getGoalMember({ goalId: goal.id, builderId: builder.id });
|
|
31
|
+
return isGoalOwnerOrManager({ goal, actorId: builder.id, actorRank: builder.rank, actorMembership });
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
async function isMemberOrArchon(builder, goalId) {
|
|
22
35
|
if (builder.rank === 'archon') return true;
|
|
23
36
|
return db.isGoalMember({ goalId, builderId: builder.id });
|
|
@@ -666,6 +679,7 @@ module.exports = {
|
|
|
666
679
|
decorateGoalScope,
|
|
667
680
|
goalNotOpenRefusal,
|
|
668
681
|
isMemberOrArchon,
|
|
682
|
+
isOwnerManagerOrArchon,
|
|
669
683
|
planGoalArchiveDispositions,
|
|
670
684
|
protectedScopeAdmissionBlock,
|
|
671
685
|
};
|
|
@@ -73,7 +73,7 @@ const JOIN_REQUEST_RESPONSE_ACTIONS = ['approve', 'deny']; // an owner/manager/A
|
|
|
73
73
|
// proceed; a false result means "not a member" → the route should 403. This is the
|
|
74
74
|
// membership half of the wall; the protected-module rank check is enforced per-route
|
|
75
75
|
// (scope is never self-granted).
|
|
76
|
-
const { authorizeCreateJoinRequest, authorizeGoalCreate, authorizeGoalTaskCreate, authorizeInvitationAccept, authorizeInvitationResponse, authorizeJoinRequestResponse, authorizeMembershipKindChange, authorizeOwnershipTransfer, authorizeRescindInvitation, authorizeWithdrawJoinRequest, decorateGoalScope, goalNotOpenRefusal, isMemberOrArchon, planGoalArchiveDispositions, protectedScopeAdmissionBlock } = require('./goal-route-authz.js');
|
|
76
|
+
const { authorizeCreateJoinRequest, authorizeGoalCreate, authorizeGoalTaskCreate, authorizeInvitationAccept, authorizeInvitationResponse, authorizeJoinRequestResponse, authorizeMembershipKindChange, authorizeOwnershipTransfer, authorizeRescindInvitation, authorizeWithdrawJoinRequest, decorateGoalScope, goalNotOpenRefusal, isMemberOrArchon, isOwnerManagerOrArchon, planGoalArchiveDispositions, protectedScopeAdmissionBlock } = require('./goal-route-authz.js');
|
|
77
77
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
78
78
|
const log = api.logger('lifecycle');
|
|
79
79
|
module.exports = function buildGoalsRouter() {
|
|
@@ -1099,7 +1099,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
1099
1099
|
// POST /goals/:id/archive — retire a goal (open|achieved → archived). Archived
|
|
1100
1100
|
// goals are re-openable (below). This is the lifecycle disposition the /goal-review
|
|
1101
1101
|
// skill reaches for once a goal is achieved (ADR 0086 §6 / BV1.R64).
|
|
1102
|
-
// rank: metic+archon
|
|
1102
|
+
// rank: metic+archon + owner|manager|Archon (task 1001739); archive is reversible, so a manager may.
|
|
1103
1103
|
router.post('/goals/:id/archive', auth.requireBuilder, auth.requirePermission('goal.archive'), async (req, res) => {
|
|
1104
1104
|
// ADR 0118 / R12: every write route that reads a body declares it, so an
|
|
1105
1105
|
// unknown field is REJECTED rather than silently ignored — a typo'd
|
|
@@ -1117,8 +1117,8 @@ module.exports = function buildGoalsRouter() {
|
|
|
1117
1117
|
try {
|
|
1118
1118
|
const goal = await db.getGoal(id);
|
|
1119
1119
|
if (!goal) return res.fail('goal_not_found', 404);
|
|
1120
|
-
if (!(await
|
|
1121
|
-
return res.fail('
|
|
1120
|
+
if (!(await isOwnerManagerOrArchon(req.builder, goal))) {
|
|
1121
|
+
return res.fail('not_owner_or_manager', { status: 403, message: 'Only the goal owner or a manager (or an Archon) may archive it.' });
|
|
1122
1122
|
}
|
|
1123
1123
|
if (goal.status === 'archived') {
|
|
1124
1124
|
return res.fail('already_archived', 409);
|
|
@@ -1241,8 +1241,8 @@ module.exports = function buildGoalsRouter() {
|
|
|
1241
1241
|
try {
|
|
1242
1242
|
const goal = await db.getGoal(id);
|
|
1243
1243
|
if (!goal) return res.fail('goal_not_found', 404);
|
|
1244
|
-
if (!(await
|
|
1245
|
-
return res.fail('
|
|
1244
|
+
if (!(await isOwnerManagerOrArchon(req.builder, goal))) {
|
|
1245
|
+
return res.fail('not_owner_or_manager', { status: 403, message: 'Only the goal owner or a manager (or an Archon) may reopen it.' });
|
|
1246
1246
|
}
|
|
1247
1247
|
if (goal.status !== 'archived') {
|
|
1248
1248
|
return res.fail('not_archived', 409, { status: goal.status });
|
|
@@ -1257,12 +1257,12 @@ module.exports = function buildGoalsRouter() {
|
|
|
1257
1257
|
|
|
1258
1258
|
// POST /goals/:id/scope — "ask to widen": add module(s) to a goal's scope_modules
|
|
1259
1259
|
// (ADR 0086 §4 / BV1.R64). Scope is NEVER self-granted, so this is the gated
|
|
1260
|
-
// approve-and-apply:
|
|
1261
|
-
// (or Archon), and a PROTECTED module can only be added by an Archon (mirrors the
|
|
1262
|
-
// POST /goals create gate). A
|
|
1260
|
+
// approve-and-apply: the goal.scope.widen permission + the caller must be the goal's
|
|
1261
|
+
// owner or a manager (or Archon), and a PROTECTED module can only be added by an Archon (mirrors the
|
|
1262
|
+
// POST /goals create gate). A plain member therefore cannot widen at all — they
|
|
1263
1263
|
// ask a Metic+/Archon out-of-band, exactly as joining a protected goal works (R57).
|
|
1264
1264
|
// The goal must be open. Body: { add: ["module-key", ...] }; idempotent (set-union).
|
|
1265
|
-
// rank: metic+archon — widens the module wall; pinned in route-rank-check.EXPECTED_RANKS.
|
|
1265
|
+
// rank: metic+archon + owner|manager|Archon (task 1001739) — widens the module wall; pinned in route-rank-check.EXPECTED_RANKS.
|
|
1266
1266
|
router.post('/goals/:id/scope', auth.requireBuilder, auth.requirePermission('goal.scope.widen'), async (req, res) => {
|
|
1267
1267
|
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
1268
1268
|
if (id === null) return;
|
|
@@ -1288,8 +1288,8 @@ module.exports = function buildGoalsRouter() {
|
|
|
1288
1288
|
if (goal.status !== 'open') {
|
|
1289
1289
|
return res.fail('goal_not_open', 409, { status: goal.status });
|
|
1290
1290
|
}
|
|
1291
|
-
if (!(await
|
|
1292
|
-
return res.fail('
|
|
1291
|
+
if (!(await isOwnerManagerOrArchon(req.builder, goal))) {
|
|
1292
|
+
return res.fail('not_owner_or_manager', { status: 403, message: 'Only the goal owner or a manager (or an Archon) may widen its scope.' });
|
|
1293
1293
|
}
|
|
1294
1294
|
const updated = await db.addGoalScopeModules({ goalId: id, modules: add });
|
|
1295
1295
|
return res.json({ ok: true, goal: updated });
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.997",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.997",
|
|
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.19.
|
|
3
|
+
"version": "1.19.997",
|
|
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",
|
|
@@ -216,7 +216,7 @@ async function runProbes(ctx) {
|
|
|
216
216
|
const r = await probePost(`/goals/${target.id}/archive`, {});
|
|
217
217
|
const after = (await get(`/goals/${target.id}`)).goal;
|
|
218
218
|
out.push({ absent: r.status === 404, id: 'P2', rule: `R10 — bare archive of goal ${target.id} (holds ${held.get(String(target.id))})`,
|
|
219
|
-
status: r.status, refused: r.status === 409, error: r.why, mutated: after.status !== 'open' });
|
|
219
|
+
status: r.status, refused: r.status === 409 || r.status === 403, error: r.why, mutated: after.status !== 'open' });
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// P3 — R12: a version holding open goals refuses a close with no dispositions.
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.997'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -109,12 +109,12 @@ test('the refusal RETURNS the tasks, so the caller can decide without a second f
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
test('the check runs AFTER identity/authorization and BEFORE the status write', () => {
|
|
112
|
-
const auth = ARCHIVE.indexOf('
|
|
112
|
+
const auth = ARCHIVE.indexOf('isOwnerManagerOrArchon');
|
|
113
113
|
const already = ARCHIVE.indexOf("'already_archived'");
|
|
114
114
|
const gate = ARCHIVE.indexOf('openTasksInGoal');
|
|
115
115
|
const write = ARCHIVE.indexOf('db.setGoalStatus');
|
|
116
116
|
assert.ok(auth !== -1 && already !== -1 && gate !== -1 && write !== -1, 'all four landmarks present');
|
|
117
|
-
assert.ok(auth < gate, 'a non-
|
|
117
|
+
assert.ok(auth < gate, 'a non-owner must not learn what a goal is holding');
|
|
118
118
|
assert.ok(already < gate, 're-archiving an archived goal keeps its own answer');
|
|
119
119
|
assert.ok(gate < write, 'a refusal after the write is not a refusal');
|
|
120
120
|
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// tests/goal_owner_manager_powers.mjs — archive / reopen / scope-widen belong to a
|
|
2
|
+
// goal's own authority (task 1001739): the owner (created_by), a manager (a 'lead'
|
|
3
|
+
// membership) or an Archon. A plain member — who used to pass — is refused 403
|
|
4
|
+
// not_owner_or_manager. Drives the REAL router with the db layer stubbed.
|
|
5
|
+
//
|
|
6
|
+
// Run: node tests/goal_owner_manager_powers.mjs
|
|
7
|
+
|
|
8
|
+
import { strict as assert } from 'node:assert';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
process.env.NODE_ENV = 'test';
|
|
12
|
+
|
|
13
|
+
const express = require('express');
|
|
14
|
+
const api = require('../src/module-api.js');
|
|
15
|
+
const db = require('../modules/lifecycle/db.js');
|
|
16
|
+
|
|
17
|
+
let WHO = { id: '10', rank: 'metic' };
|
|
18
|
+
api.requireBuilder = (req, _res, next) => { req.builder = WHO; next(); };
|
|
19
|
+
api.requirePermission = () => (req, _res, next) => next();
|
|
20
|
+
|
|
21
|
+
const OWNER = '10', MANAGER = '11', MEMBER = '12';
|
|
22
|
+
const GOALS = {
|
|
23
|
+
1: { id: 1, status: 'open', title: 'g', version_id: 'V', created_by: OWNER, scope_modules: [] },
|
|
24
|
+
3: { id: 3, status: 'open', title: 'system', version_id: 'V', created_by: null, scope_modules: [] },
|
|
25
|
+
2: { id: 2, status: 'archived', title: 'a', version_id: 'V', created_by: OWNER, scope_modules: [] },
|
|
26
|
+
};
|
|
27
|
+
const KINDS = { [MANAGER]: 'lead', [MEMBER]: 'member' };
|
|
28
|
+
db.getGoal = async (id) => GOALS[Number(id)] ?? null;
|
|
29
|
+
db.getGoalMember = async ({ builderId }) => (KINDS[builderId] ? { builder_id: builderId, membership_kind: KINDS[builderId] } : null);
|
|
30
|
+
db.isGoalMember = async ({ builderId }) => !!KINDS[builderId] || builderId === OWNER;
|
|
31
|
+
db.getVersion = async () => null;
|
|
32
|
+
db.openTasksInGoal = async () => ({ total: 0, tasks: [] });
|
|
33
|
+
db.archiveGoalWithDispositions = async ({ goalId }) => ({ goal: { ...GOALS[goalId], status: 'archived' }, applied: [] });
|
|
34
|
+
db.setGoalStatus = async ({ goalId, status }) => ({ ...GOALS[goalId], status });
|
|
35
|
+
db.addGoalScopeModules = async ({ goalId }) => GOALS[goalId];
|
|
36
|
+
|
|
37
|
+
const app = express();
|
|
38
|
+
app.use(express.json());
|
|
39
|
+
app.use((req, res, next) => {
|
|
40
|
+
res.fail = (code, o, d) => { const x = typeof o === 'number' ? { status: o } : (o || {}); return res.status(x.status || 400).json({ error: code, details: x.details ?? d }); };
|
|
41
|
+
next();
|
|
42
|
+
});
|
|
43
|
+
app.use('/api/gds', require('../modules/lifecycle/routes/goals.js')());
|
|
44
|
+
const server = app.listen(0);
|
|
45
|
+
await new Promise(r => server.once('listening', r));
|
|
46
|
+
const base = `http://127.0.0.1:${server.address().port}/api/gds`;
|
|
47
|
+
const post = async (p, body) => {
|
|
48
|
+
const r = await fetch(base + p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body ?? {}) });
|
|
49
|
+
return { status: r.status, body: await r.json().catch(() => null) };
|
|
50
|
+
};
|
|
51
|
+
const calls = [
|
|
52
|
+
['/goals/1/archive', {}],
|
|
53
|
+
['/goals/2/reopen', {}],
|
|
54
|
+
['/goals/1/scope', { add: ['hall-ui'] }],
|
|
55
|
+
];
|
|
56
|
+
let n = 0;
|
|
57
|
+
for (const [who, label, expectOk] of [
|
|
58
|
+
[{ id: OWNER, rank: 'metic' }, 'owner', true],
|
|
59
|
+
[{ id: MANAGER, rank: 'metic' }, 'manager', true],
|
|
60
|
+
[{ id: '99', rank: 'archon' }, 'archon', true],
|
|
61
|
+
[{ id: MEMBER, rank: 'metic' }, 'plain member', false],
|
|
62
|
+
[{ id: '77', rank: 'metic' }, 'non-member', false],
|
|
63
|
+
]) {
|
|
64
|
+
WHO = who;
|
|
65
|
+
for (const [path, body] of calls) {
|
|
66
|
+
const r = await post(path, body);
|
|
67
|
+
if (expectOk) assert.equal(r.status, 200, `${label} ${path} → ${r.status} ${JSON.stringify(r.body)}`);
|
|
68
|
+
else { assert.equal(r.status, 403, `${label} ${path}`); assert.equal(r.body.error, 'not_owner_or_manager'); }
|
|
69
|
+
n++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// An OWNERLESS goal (created_by NULL, the system catch-alls): a member keeps the
|
|
73
|
+
// power, a non-member has none — otherwise only an Archon could ever disposition it.
|
|
74
|
+
WHO = { id: MEMBER, rank: 'metic' };
|
|
75
|
+
assert.equal((await post('/goals/3/archive')).status, 200); n++;
|
|
76
|
+
WHO = { id: '77', rank: 'metic' };
|
|
77
|
+
assert.equal((await post('/goals/3/archive')).status, 403); n++;
|
|
78
|
+
|
|
79
|
+
// POST /goals creation gate: a protected-module scope is Archon-only.
|
|
80
|
+
WHO = { id: OWNER, rank: 'metic' };
|
|
81
|
+
let c = await post('/goals', { version_id: 'V', title: 't', scope_modules: ['kernel'] });
|
|
82
|
+
assert.equal(c.status, 403); assert.equal(c.body.error, 'archon_required_for_protected_scope'); n++;
|
|
83
|
+
c = await post('/goals', { version_id: 'V', title: 't', scope_modules: ['hall-ui'], admit_reason: 'because reasons' });
|
|
84
|
+
assert.equal(c.body.error, 'archon_required_for_admission'); n++;
|
|
85
|
+
WHO = { id: '99', rank: 'archon' };
|
|
86
|
+
c = await post('/goals', { version_id: 'V', title: 't', scope_modules: ['kernel'] });
|
|
87
|
+
assert.notEqual(c.body && c.body.error, 'archon_required_for_protected_scope'); n++;
|
|
88
|
+
|
|
89
|
+
console.log(`${n} owner/manager/member checks passed against the real router.`);
|
|
90
|
+
server.close();
|