@cat-factory/server 0.159.0 → 0.161.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/ContainerAgentExecutor.d.ts.map +1 -1
- package/dist/agents/ContainerAgentExecutor.js +5 -2
- package/dist/agents/ContainerAgentExecutor.js.map +1 -1
- package/dist/agents/jobBody.d.ts.map +1 -1
- package/dist/agents/jobBody.js +6 -3
- package/dist/agents/jobBody.js.map +1 -1
- package/dist/events/machineSubscribe.d.ts +56 -0
- package/dist/events/machineSubscribe.d.ts.map +1 -0
- package/dist/events/machineSubscribe.js +75 -0
- package/dist/events/machineSubscribe.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/events/EventsRelayController.d.ts +3 -1
- package/dist/modules/events/EventsRelayController.d.ts.map +1 -1
- package/dist/modules/events/EventsRelayController.js +50 -1
- package/dist/modules/events/EventsRelayController.js.map +1 -1
- package/dist/persistence/rpc-allowlist.d.ts +37 -0
- package/dist/persistence/rpc-allowlist.d.ts.map +1 -0
- package/dist/persistence/rpc-allowlist.js +901 -0
- package/dist/persistence/rpc-allowlist.js.map +1 -0
- package/dist/persistence/rpc.d.ts +6 -34
- package/dist/persistence/rpc.d.ts.map +1 -1
- package/dist/persistence/rpc.js +6 -864
- package/dist/persistence/rpc.js.map +1 -1
- package/package.json +8 -8
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
// The mothership-mode persistence ALLOW-LIST — the default-deny table naming every repository
|
|
2
|
+
// method a mothership-mode node may invoke over `POST /internal/persistence`, and the scope rule
|
|
3
|
+
// that binds each call to an account.
|
|
4
|
+
//
|
|
5
|
+
// It lives beside the protocol + dispatcher (`rpc.ts`) rather than inside it because the two grow
|
|
6
|
+
// on completely different schedules: the protocol is stable, while this table is the initiative's
|
|
7
|
+
// living surface — every mothership slice widens it, so it is the file a reader goes to when
|
|
8
|
+
// asking "is X reachable from a laptop yet?". Splitting it also keeps `rpc.ts` within the
|
|
9
|
+
// file-size budget as the surface keeps growing.
|
|
10
|
+
//
|
|
11
|
+
// See `docs/initiatives/mothership-mode.md` for the per-repository bucket checklist, and
|
|
12
|
+
// `runtimes/node/test/mothership-allowlist.spec.ts` for the drift guard that fails unless EVERY
|
|
13
|
+
// Drizzle repository method is either listed here or explicitly classified.
|
|
14
|
+
/**
|
|
15
|
+
* The mothership-mode persistence allow-list: the core domain repositories plus the
|
|
16
|
+
* workspace-scoped reads a board load (`GET /workspaces/:id`) and an execution exercise.
|
|
17
|
+
* Every method here binds to an account via its {@link ScopeRule} so a call outside the
|
|
18
|
+
* machine token's scope is refused as 404.
|
|
19
|
+
*
|
|
20
|
+
* The cross-service board-composition reads keyed on `serviceIds[]`/`accountId`
|
|
21
|
+
* (`listByServices`, `serviceRepository.listByIds`/`listByAccount`, `countByServiceIds`) and the
|
|
22
|
+
* entity-id-keyed `blockRepository.findById` are allow-listed here too, each bound by the
|
|
23
|
+
* {@link ScopeRule} `serviceList` / `block` / `account` kinds that resolve the entity's owning
|
|
24
|
+
* account server-side before the scope check.
|
|
25
|
+
*
|
|
26
|
+
* Still EXCLUDED (added in later gate slices, with their own scope rules, or kept
|
|
27
|
+
* mothership-internal):
|
|
28
|
+
* - `subscriptionActivationRepository.deleteByExecution` — the activation row is the local
|
|
29
|
+
* `node:sqlite` bucket (per the per-repo checklist), not the remote surface, so it is not
|
|
30
|
+
* exposed here.
|
|
31
|
+
* - Global sweeper methods (`listStale`, `deleteOlderThan`) and high-impact unscoped ops
|
|
32
|
+
* (`workspaceRepository.delete`, `accountRepository.create`).
|
|
33
|
+
*
|
|
34
|
+
* Admin-gated mutations are also EXCLUDED here. The RPC dispatches over the raw repository,
|
|
35
|
+
* bypassing the service layer that normally enforces per-user role checks — e.g.
|
|
36
|
+
* `AccountService.requireAdmin` guards `accountRepository.rename`/`updateSettings` and
|
|
37
|
+
* `membershipRepository.upsert`/`remove`. A machine token is scoped to whole ACCOUNTS, not to
|
|
38
|
+
* a role within them, so exposing those repo methods would let any account member self-promote
|
|
39
|
+
* to admin or rewrite memberships over the wire. They stay mothership-internal until a later
|
|
40
|
+
* slice adds a role dimension to the scope (or routes them through the service). Only the
|
|
41
|
+
* account/membership READS a board load needs are remotely callable. Board-level mutations
|
|
42
|
+
* (`workspaceRepository.rename`/`setDescription`, block/pipeline/execution CRUD) are
|
|
43
|
+
* member-level in the service layer, so they remain. The board mutations that stay OUT are
|
|
44
|
+
* `workspaceRepository.setAccessMode` (the access-mode flip) and `linkAccount` (the legacy-board
|
|
45
|
+
* auto-heal that adopts a board into an account) — both are `members.manage` (admin-tier), so like
|
|
46
|
+
* the account/membership admin mutations they must not be reachable over the role-blind machine RPC.
|
|
47
|
+
*/
|
|
48
|
+
export const REMOTE_PERSISTENCE_METHODS = {
|
|
49
|
+
workspaceRepository: {
|
|
50
|
+
listVisible: { scope: { kind: 'visibility', arg: 0 } },
|
|
51
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
52
|
+
ownerOf: { scope: { kind: 'workspace', arg: 0 } },
|
|
53
|
+
accountOf: { scope: { kind: 'workspace', arg: 0 } },
|
|
54
|
+
// The workspace-RBAC authorization read (the narrow access row that replaces `accountOf`
|
|
55
|
+
// in the gate); workspace-scoped and secret-free, exactly like `accountOf`/`ownerOf`.
|
|
56
|
+
accessRowOf: { scope: { kind: 'workspace', arg: 0 } },
|
|
57
|
+
rename: { scope: { kind: 'workspace', arg: 0 } },
|
|
58
|
+
setDescription: { scope: { kind: 'workspace', arg: 0 } },
|
|
59
|
+
},
|
|
60
|
+
blockRepository: {
|
|
61
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
62
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
63
|
+
insert: { scope: { kind: 'workspace', arg: 0 } },
|
|
64
|
+
update: { scope: { kind: 'workspace', arg: 0 } },
|
|
65
|
+
setService: { scope: { kind: 'workspace', arg: 0 } },
|
|
66
|
+
deleteMany: { scope: { kind: 'workspace', arg: 0 } },
|
|
67
|
+
// Entity-id-keyed (no workspace arg): resolve the block's home workspace's account server-side.
|
|
68
|
+
findById: { scope: { kind: 'block', arg: 0 } },
|
|
69
|
+
// The batched form (the cross-workspace dependency resolution on the run-start path).
|
|
70
|
+
findByIds: { scope: { kind: 'blockList', arg: 0 } },
|
|
71
|
+
// Cross-service: compose a board's blocks from every service it mounts.
|
|
72
|
+
listByServices: { scope: { kind: 'serviceList', arg: 0 } },
|
|
73
|
+
// One bounded page of a service frame's task subtree (the public API's paginated task list).
|
|
74
|
+
listServiceTasks: { scope: { kind: 'workspace', arg: 0 } },
|
|
75
|
+
// The public API's in-flight concurrency backstop (`BoardService.countActiveInternalTasks`),
|
|
76
|
+
// checked before a headless "initiative" run starts so a leaked key can't spin up unbounded
|
|
77
|
+
// LLM work. A workspace-scoped SQL COUNT returning a NUMBER — no row content crosses the
|
|
78
|
+
// wire. Completes the headless surface whose paginated reads (`listServiceTasks` above,
|
|
79
|
+
// `executionRepository.listInternal`) are already remote: without it the cap read throws and
|
|
80
|
+
// the mothership-mode node refuses every public-API run start.
|
|
81
|
+
countActiveInternal: { scope: { kind: 'workspace', arg: 0 } },
|
|
82
|
+
},
|
|
83
|
+
pipelineRepository: {
|
|
84
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
85
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
86
|
+
insert: { scope: { kind: 'workspace', arg: 0 } },
|
|
87
|
+
update: { scope: { kind: 'workspace', arg: 0 } },
|
|
88
|
+
delete: { scope: { kind: 'workspace', arg: 0 } },
|
|
89
|
+
},
|
|
90
|
+
executionRepository: {
|
|
91
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
92
|
+
// Lean live-run projection backing the dispatch guard + resumePaused (workspace-scoped read).
|
|
93
|
+
listLive: { scope: { kind: 'workspace', arg: 0 } },
|
|
94
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
95
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
96
|
+
upsert: { scope: { kind: 'workspace', arg: 0 }, revWriteBack: 1 },
|
|
97
|
+
// The one-live-run-per-block insert used by start/retry/restart. Workspace-scoped like
|
|
98
|
+
// upsert and bumps `execution.rev` in place on the arg-1 instance on a successful insert.
|
|
99
|
+
insertLive: { scope: { kind: 'workspace', arg: 0 }, revWriteBack: 1 },
|
|
100
|
+
compareAndSwap: { scope: { kind: 'workspace', arg: 0 }, revWriteBack: 1 },
|
|
101
|
+
deleteByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
102
|
+
markFailed: { scope: { kind: 'workspace', arg: 0 } },
|
|
103
|
+
// Cross-service: compose a board's runs from every service it mounts.
|
|
104
|
+
listByServices: { scope: { kind: 'serviceList', arg: 0 } },
|
|
105
|
+
// One bounded page of the workspace's headless (`internal`-anchored) runs — the public API's
|
|
106
|
+
// job list. Workspace-scoped like every other list read here.
|
|
107
|
+
listInternal: { scope: { kind: 'workspace', arg: 0 } },
|
|
108
|
+
},
|
|
109
|
+
accountRepository: {
|
|
110
|
+
// Reads only — `rename`/`updateSettings` are admin-gated (see allow-list note above).
|
|
111
|
+
get: { scope: { kind: 'account', arg: 0 } },
|
|
112
|
+
listByIds: { scope: { kind: 'accountList', arg: 0 } },
|
|
113
|
+
findPersonalByUser: { scope: { kind: 'selfUser', arg: 0 } },
|
|
114
|
+
},
|
|
115
|
+
membershipRepository: {
|
|
116
|
+
// Reads only — `upsert`/`remove` are admin-gated (see allow-list note above).
|
|
117
|
+
listByUser: { scope: { kind: 'selfUser', arg: 0 } },
|
|
118
|
+
listByAccount: { scope: { kind: 'account', arg: 0 } },
|
|
119
|
+
get: { scope: { kind: 'account', arg: 0 } },
|
|
120
|
+
},
|
|
121
|
+
// The workspace-RBAC member-tier READS the gate + list path run on every signed request
|
|
122
|
+
// (workspace-rbac slice 3). `get` is the gate's per-request effective-role read — workspace-
|
|
123
|
+
// scoped and secret-free, exactly like `workspaceRepository.accessRowOf`.
|
|
124
|
+
// `getRolesForUserInWorkspaces` is the `GET /workspaces` list-annotation batch read; it is
|
|
125
|
+
// pinned to the CALLER's own id (`selfUser`), so it can only ever return the caller's own
|
|
126
|
+
// membership roles (a board they hold no row in is simply absent — no existence leak), and it
|
|
127
|
+
// returns a serializable `Record` so it round-trips over this RPC. The roster read
|
|
128
|
+
// (`listByWorkspace`/`listWorkspaceIdsForUser`) + the writes (`upsert`/`remove`/
|
|
129
|
+
// `removeByAccountMembership`) stay mothership-internal — the member-management API is a later
|
|
130
|
+
// slice, and the writes are admin-gated (the machine token is role-blind).
|
|
131
|
+
workspaceMemberRepository: {
|
|
132
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
133
|
+
getRolesForUserInWorkspaces: { scope: { kind: 'selfUser', arg: 0 } },
|
|
134
|
+
},
|
|
135
|
+
// --- Member-display read surface ------------------------------------------------
|
|
136
|
+
// The user DISPLAY records the account members panel enriches its roster with
|
|
137
|
+
// (`AccountService.members` → `userRepository.listByIds(memberIds)`) and the single-user display
|
|
138
|
+
// lookup (`get`). These carry only the presentational `UserRecord` (id / name / email / avatarUrl
|
|
139
|
+
// / createdAt) — NOT the password `secret`, which lives on `UserIdentityRecord` and is reachable
|
|
140
|
+
// only via `getIdentity`/`listIdentities` (kept off, like the other identity/auth reads). So the
|
|
141
|
+
// display reads leak no credential and are safe to proxy.
|
|
142
|
+
//
|
|
143
|
+
// Scope: a userId is not itself an account/workspace, so it is bound by CO-MEMBERSHIP — the `user`
|
|
144
|
+
// rule (single id) / `userList` rule (batch) admit a user iff they are a member of one of the
|
|
145
|
+
// token's in-scope accounts, resolved server-side from the account rosters. The roster read only
|
|
146
|
+
// ever passes ids that ARE members of the (in-scope) account it just listed, so the batch check
|
|
147
|
+
// always passes on the real path; a forged out-of-scope id fails closed (404, no existence leak).
|
|
148
|
+
// The `update` write (profile edit) + the identity/auth reads (`findByIdentity`/`findByEmail`/
|
|
149
|
+
// `getIdentity`/`listIdentities`) stay off — they are the account-lifecycle / login surface, not
|
|
150
|
+
// member display, and the identity reads carry the password secret.
|
|
151
|
+
userRepository: {
|
|
152
|
+
get: { scope: { kind: 'user', arg: 0 } },
|
|
153
|
+
listByIds: { scope: { kind: 'userList', arg: 0 } },
|
|
154
|
+
},
|
|
155
|
+
// --- Board-load read surface --------------------------------------------------
|
|
156
|
+
// The workspace-scoped reads a `GET /workspaces/:id` snapshot assembles. Each takes the
|
|
157
|
+
// workspaceId as arg0, so they reuse the `workspace` rule (resolve the owning account, reject
|
|
158
|
+
// out-of-scope as 404). Reads only — no mutation is exposed here.
|
|
159
|
+
//
|
|
160
|
+
// The cross-service reads (`*.listByServices`, `countByServiceIds`, `serviceRepository.*`)
|
|
161
|
+
// compose a board from the services it mounts; their arg0 is `serviceIds[]` (the `serviceList`
|
|
162
|
+
// rule resolves each service's owning account) or an `accountId` (the `account` rule).
|
|
163
|
+
serviceRepository: {
|
|
164
|
+
listByIds: { scope: { kind: 'serviceList', arg: 0 } },
|
|
165
|
+
listByAccount: { scope: { kind: 'account', arg: 0 } },
|
|
166
|
+
// The run path resolves the service that owns a frame block (module materialisation /
|
|
167
|
+
// blueprint reconcile). arg0 is a frame BLOCK id, so the `block` rule resolves it to its
|
|
168
|
+
// home workspace's account server-side.
|
|
169
|
+
getByFrameBlock: { scope: { kind: 'block', arg: 0 } },
|
|
170
|
+
// The batched form of `getByFrameBlock` — the board-composition read that resolves every
|
|
171
|
+
// frame's service in ONE query (the duplicate-service check when linking a monorepo, and the
|
|
172
|
+
// frame-subtree deletion cleanup in `BoardService`). arg0 is a `frameBlockIds[]` array, so the
|
|
173
|
+
// `blockList` rule resolves each frame block's home workspace's account server-side and fails
|
|
174
|
+
// closed on any missing/out-of-scope id (empty input → empty). The remaining service CRUD +
|
|
175
|
+
// `getByRepo` (the GitHub-sync repo→service link) stay off the SPA path — a later slice.
|
|
176
|
+
listByFrameBlocks: { scope: { kind: 'blockList', arg: 0 } },
|
|
177
|
+
// The org-catalog mount flow reads a single service by id before mounting it onto a board
|
|
178
|
+
// (`ServiceMountService.mount` — the cross-org guard that a service is mounted only within
|
|
179
|
+
// its own account). arg0 is a serviceId with no workspace arg, so the `service` rule resolves
|
|
180
|
+
// its owning account server-side.
|
|
181
|
+
get: { scope: { kind: 'service', arg: 0 } },
|
|
182
|
+
},
|
|
183
|
+
// --- Shared-service mount management surface -------------------------------------
|
|
184
|
+
// The org-catalog / shared-service mounting flow a mothership-mode SPA drives
|
|
185
|
+
// (`ServiceMountService` / `ServiceMountController`): mount / unmount / re-layout a shared
|
|
186
|
+
// account service onto a workspace board. The reads that compose the catalog badge
|
|
187
|
+
// (`listByWorkspace`, `countByServiceIds`) were already exposed; these complete the write
|
|
188
|
+
// surface. `get`/`update`/`remove` take the workspaceId as arg0 (the `workspace` rule); the
|
|
189
|
+
// record-based `upsert(mount)` binds on the mount's `workspaceId` FIELD via the `serviceMount`
|
|
190
|
+
// rule. Each is member-level (the mount endpoints are not admin-gated) and workspace-scoped.
|
|
191
|
+
//
|
|
192
|
+
// Cross-org sharing stays enforced at the RPC layer, NOT only in the (bypassed) service layer:
|
|
193
|
+
// the `serviceMount` rule additionally requires the mounted `serviceId` to be owned by the SAME
|
|
194
|
+
// account as the target workspace, so a raw `upsert` can never plant a cross-org mount — even
|
|
195
|
+
// for a machine token that spans several accounts (a user in multiple orgs). Board composition
|
|
196
|
+
// (`blockRepository.listByServices`, `serviceRepository.listByIds`) stays account-scoped as a
|
|
197
|
+
// second line of defence, but it is no longer the sole guard for the mount invariant.
|
|
198
|
+
workspaceMountRepository: {
|
|
199
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
200
|
+
countByServiceIds: { scope: { kind: 'serviceList', arg: 0 } },
|
|
201
|
+
// The real-time fan-out's hot read, and the one method on this table that is NOT a management
|
|
202
|
+
// surface: `FanOutEventPublisher.targets` calls it on EVERY engine event publish to expand the
|
|
203
|
+
// changed block to the set of boards mounting its service. It is therefore load-bearing for
|
|
204
|
+
// mothership mode in a way the others aren't — a mothership-mode node wires the same fan-out
|
|
205
|
+
// decorator, so leaving it off meant every publish rejected with `unknown_method` and the
|
|
206
|
+
// rejection propagated into the run-state emit (`RunStateMachine`), not just into a missed
|
|
207
|
+
// frame. Also drives the mount/unmount live-update of OTHER boards showing the same service
|
|
208
|
+
// (`BoardService`), which was the known gap the mount-management slice left open.
|
|
209
|
+
//
|
|
210
|
+
// arg0 is the ORIGIN workspaceId, so the plain `workspace` rule binds it. The join starts from
|
|
211
|
+
// that in-scope workspace's block and returns workspace IDS only — no row content — and a
|
|
212
|
+
// service can only ever be mounted inside its own account (the `serviceMount` rule makes that
|
|
213
|
+
// non-bypassable), so the result set can never span an account the token doesn't hold.
|
|
214
|
+
listWorkspaceIdsMountingBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
215
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
216
|
+
upsert: { scope: { kind: 'serviceMount', arg: 0 } },
|
|
217
|
+
update: { scope: { kind: 'workspace', arg: 0 } },
|
|
218
|
+
remove: { scope: { kind: 'workspace', arg: 0 } },
|
|
219
|
+
},
|
|
220
|
+
workspaceSettingsRepository: {
|
|
221
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
222
|
+
// The workspace-settings panel saves its edits (e.g. the `storeAgentContext` toggle). The
|
|
223
|
+
// settings endpoints are member-level (not admin-gated), workspace-scoped — the same policy
|
|
224
|
+
// as the block/pipeline mutations above. Completes the read+write settings surface.
|
|
225
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
226
|
+
},
|
|
227
|
+
// Per-user settings (the user-tier spend budget). Self-scoped: a user reads/writes only their
|
|
228
|
+
// OWN row (the `selfUser` rule requires args[0] to equal the token's userId), so both the
|
|
229
|
+
// read (snapshot + spend gate) and the write (the user's own budget edit) are safe over RPC —
|
|
230
|
+
// no admin gating is involved, unlike the account-tier budget (see accountRepository note).
|
|
231
|
+
// Invariant: the user-tier gate/snapshot always passes the CALLER's own userId here — a run's
|
|
232
|
+
// initiator is the mothership laptop's signed-in user (single-user token), and the snapshot
|
|
233
|
+
// passes the viewer's id — so `selfUser` matches by construction. If that ever diverged the
|
|
234
|
+
// read would be denied (404 → the remote proxy throws); the snapshot assembly reads these
|
|
235
|
+
// best-effort (degrading the tier to absent) so a scope mismatch can't 500 the board load.
|
|
236
|
+
userSettingsRepository: {
|
|
237
|
+
get: { scope: { kind: 'selfUser', arg: 0 } },
|
|
238
|
+
upsert: { scope: { kind: 'selfUser', arg: 0 } },
|
|
239
|
+
},
|
|
240
|
+
riskPolicyRepository: {
|
|
241
|
+
list: { scope: { kind: 'workspace', arg: 0 } },
|
|
242
|
+
// The merge lifecycle resolves a task's merge-threshold preset at run time
|
|
243
|
+
// (`resolveRiskPolicy` → the merger/requirements gate), reading the workspace default when
|
|
244
|
+
// the task pins none. Workspace-scoped read on the run path.
|
|
245
|
+
getDefault: { scope: { kind: 'workspace', arg: 0 } },
|
|
246
|
+
// `RiskPolicyService.list` lazily seeds the built-in default for a workspace that has
|
|
247
|
+
// none (a write triggered by the board-load read). Member-level (the preset CRUD is not
|
|
248
|
+
// admin-gated), workspace-scoped — the same policy as the block/pipeline mutations above.
|
|
249
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
250
|
+
// The preset-library editor reads one preset and deletes it. Both take the workspaceId as
|
|
251
|
+
// arg0 and are member-level (the preset CRUD is not admin-gated), completing the merge-preset
|
|
252
|
+
// library management surface (list/getDefault/upsert were already exposed for the board load).
|
|
253
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
254
|
+
remove: { scope: { kind: 'workspace', arg: 0 } },
|
|
255
|
+
},
|
|
256
|
+
// The merge TRACK RECORD is the evidence side of the same merge policy, and every one of its
|
|
257
|
+
// methods takes the workspaceId as arg0 — so the whole surface is proxied, workspace-scoped and
|
|
258
|
+
// member-level exactly like the preset library above. It has to be: `MergeResolver` reads the
|
|
259
|
+
// classification and writes the record ON THE RUN PATH, so a mothership-mode node with these
|
|
260
|
+
// unproxied would resolve every per-class rule against an empty record set (silently reverting
|
|
261
|
+
// to the score ceilings) and lose every merge decision it made.
|
|
262
|
+
mergeTrackRecordRepository: {
|
|
263
|
+
// Run path: the merger step's decision write (first-write-wins) + the notification card's
|
|
264
|
+
// record lookup.
|
|
265
|
+
insertIfAbsent: { scope: { kind: 'workspace', arg: 0 } },
|
|
266
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
267
|
+
getByExecution: { scope: { kind: 'workspace', arg: 0 } },
|
|
268
|
+
// The block-scoped merge controls resolve a block's most recent record to settle + tag it.
|
|
269
|
+
getLatestByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
270
|
+
// External-merge attribution from the webhook ingest, keyed by `(repoId, prNumber)`.
|
|
271
|
+
getByPullRequest: { scope: { kind: 'workspace', arg: 0 } },
|
|
272
|
+
// Settling a decision + recording the reviewer-effort tag.
|
|
273
|
+
patch: { scope: { kind: 'workspace', arg: 0 } },
|
|
274
|
+
// The preset editor's per-class stats (ONE aggregate for every class).
|
|
275
|
+
rollupByClass: { scope: { kind: 'workspace', arg: 0 } },
|
|
276
|
+
},
|
|
277
|
+
// Shared stacks are a workspace-scoped, member-level config library (like merge presets): the
|
|
278
|
+
// Infrastructure panel lists/creates/edits/deletes them and the board-load snapshot reads them.
|
|
279
|
+
// All four repository methods take the workspaceId as arg0 — proxied to the mothership like the
|
|
280
|
+
// other workspace libraries. (The bring-up/teardown LIFECYCLE is a host-Docker service action,
|
|
281
|
+
// not a repository method, so it never crosses the machine API.)
|
|
282
|
+
sharedStackRepository: {
|
|
283
|
+
list: { scope: { kind: 'workspace', arg: 0 } },
|
|
284
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
285
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
286
|
+
remove: { scope: { kind: 'workspace', arg: 0 } },
|
|
287
|
+
},
|
|
288
|
+
modelPresetRepository: {
|
|
289
|
+
list: { scope: { kind: 'workspace', arg: 0 } },
|
|
290
|
+
// The run-start model resolution (`resolvePresetModelForKind` → the personal-credential
|
|
291
|
+
// gate) reads the workspace's default model preset for the dispatched agent kind.
|
|
292
|
+
getDefault: { scope: { kind: 'workspace', arg: 0 } },
|
|
293
|
+
// `ModelPresetService.list` lazily seeds the built-in defaults for a workspace that has none
|
|
294
|
+
// (a write the board-load read triggers), exactly like `riskPolicyRepository.upsert` above.
|
|
295
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
296
|
+
// The model-preset library editor's read-one + delete, the mirror of the merge-preset
|
|
297
|
+
// management pair above. Member-level, workspace-scoped.
|
|
298
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
299
|
+
remove: { scope: { kind: 'workspace', arg: 0 } },
|
|
300
|
+
},
|
|
301
|
+
// --- Agent-context run-path reads -----------------------------------------------
|
|
302
|
+
// `AgentContextBuilder` resolves a block's LINKED docs/tasks for EVERY container agent step
|
|
303
|
+
// (it builds the agent context on each dispatch), so these reads are on the run path, not just
|
|
304
|
+
// the opt-in document/task integrations' own surfaces. arg0 is the workspaceId → `workspace`
|
|
305
|
+
// rule. The document/task SOURCE-PROVIDER + connection surfaces (connect/list/disconnect) are
|
|
306
|
+
// NOT exposed here — they are a later integration slice; only the block-scoped context reads are.
|
|
307
|
+
documentRepository: {
|
|
308
|
+
listByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
309
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
310
|
+
// A URL named in a block's description is resolved against the imported corpus by a
|
|
311
|
+
// canonical-url point lookup (`AgentContextBuilder.resolveLinkedContext`), on the SAME
|
|
312
|
+
// per-dispatch run path as `get`/`listByBlock` above — so it must be allow-listed too
|
|
313
|
+
// (else a task whose description contains any link fails the run with `unknown_method`).
|
|
314
|
+
getByUrl: { scope: { kind: 'workspace', arg: 0 } },
|
|
315
|
+
// Document-authoring run path (WS1): for a doc-aware kind, `AgentContextBuilder` resolves the
|
|
316
|
+
// workspace's linked TEMPLATE (singular) + EXEMPLAR (list) for the block's `docKind` on each
|
|
317
|
+
// dispatch, so both reads are on the run path exactly like `listByBlock`/`getByUrl`. arg0 is
|
|
318
|
+
// the workspaceId → the `workspace` rule. (The role-link WRITE surface + the whole-workspace
|
|
319
|
+
// list back the management UI, not the run path — they stay mothership-internal for now.)
|
|
320
|
+
getRoleLink: { scope: { kind: 'workspace', arg: 0 } },
|
|
321
|
+
listRoleLinks: { scope: { kind: 'workspace', arg: 0 } },
|
|
322
|
+
},
|
|
323
|
+
taskRepository: {
|
|
324
|
+
listByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
325
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
326
|
+
// Same as `documentRepository.getByUrl`: a URL in the description resolves against the
|
|
327
|
+
// imported issue corpus by a point lookup on the run path.
|
|
328
|
+
getByUrl: { scope: { kind: 'workspace', arg: 0 } },
|
|
329
|
+
// The batched counterpart to `get`: `AgentContextBuilder.resolveLinkedContext` resolves the
|
|
330
|
+
// tracker issues a block's description names (Jira keys, `owner/repo#N` refs) in ONE
|
|
331
|
+
// chunked-`IN` read. It is invoked UNCONDITIONALLY on every container-agent dispatch (the
|
|
332
|
+
// call isn't guarded on there being any refs), so it is on the run path exactly like `get`
|
|
333
|
+
// — omit it and EVERY such build fails the run with `unknown_method`. arg0 is the
|
|
334
|
+
// workspaceId → the `workspace` rule.
|
|
335
|
+
listByRefs: { scope: { kind: 'workspace', arg: 0 } },
|
|
336
|
+
},
|
|
337
|
+
// The agent context also resolves the block's provisioned environment per step
|
|
338
|
+
// (`resolveForBlock`/`get`, both workspace-keyed). Reads only — the connect/provision surface
|
|
339
|
+
// (and decrypting a remotely-sealed env cipher, which needs the mothership's key) is a later slice.
|
|
340
|
+
environmentRegistryRepository: {
|
|
341
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
342
|
+
// The per-`(block, service frame)` discovery read. `AgentContextBuilder.resolveEnvironment`
|
|
343
|
+
// (and `RunDispatcher.attachEnvironmentProjection`) resolve the OWN service frame's env by
|
|
344
|
+
// frame on EVERY container-agent dispatch, so this is on the run path exactly like `getByBlock`
|
|
345
|
+
// — omit it and every such build throws `unknown_method`.
|
|
346
|
+
getByBlockAndFrame: { scope: { kind: 'workspace', arg: 0 } },
|
|
347
|
+
// The frame-less (manual / human-test) fallback behind `readRegistryRecord` — on the same
|
|
348
|
+
// container-agent run path as `getByBlockAndFrame` (the own-frame env resolution falls back to
|
|
349
|
+
// it), so omit it and every such build throws `unknown_method`.
|
|
350
|
+
getFramelessByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
351
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
352
|
+
// The workspace-scoped batch read behind `EnvironmentProvisioningService.listHandles`
|
|
353
|
+
// (the environments list endpoint + the frontend UI-test gate's single indexed env read,
|
|
354
|
+
// `AgentContextBuilder.resolveFrontendConfig` — a batch read, not a per-binding point read).
|
|
355
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
356
|
+
},
|
|
357
|
+
// --- Ephemeral-environment backend connection management surface ----------------
|
|
358
|
+
// The environment provider-connection + per-type infra-handler management panels a mothership-mode
|
|
359
|
+
// SPA drives (`EnvironmentController` → `EnvironmentConnectionService`: connect / list / disconnect
|
|
360
|
+
// a backend, and register / test / re-secret / unregister a per-type engine handler). Its
|
|
361
|
+
// controller mounts under `/workspaces/:workspaceId` and is member-level (not admin-gated), so it
|
|
362
|
+
// follows the same policy as the observability / other settings panels above. Reads/deletes take
|
|
363
|
+
// the workspaceId as arg0 (the `workspace` rule); the record-based `upsert(record)` binds on the
|
|
364
|
+
// record's `workspaceId` FIELD (the `workspaceField` rule — the id is a property, not a positional
|
|
365
|
+
// arg). Exposing these makes the environment-connection settings panels functional (persist +
|
|
366
|
+
// read back the redacted summary) in mothership mode.
|
|
367
|
+
//
|
|
368
|
+
// Safe to expose like the observability connection above: the connection record carries the
|
|
369
|
+
// handler secrets as a SEALED blob (`secretsCipher`) — the repo returns it verbatim (it does NOT
|
|
370
|
+
// decrypt); sealing/decryption live in `EnvironmentConnectionService` under the LOCAL key, so no
|
|
371
|
+
// plaintext credential crosses the machine API and the mothership only ever stores ciphertext (the
|
|
372
|
+
// initiative's "the mothership ENCRYPTION_KEY never reaches the laptop" split holds). What this
|
|
373
|
+
// does NOT yet unlock: actually PROVISIONING an environment in mothership mode — the registry
|
|
374
|
+
// WRITE path (`environmentRegistryRepository.insert`/`update`) + decrypting a remotely-sealed
|
|
375
|
+
// access cipher stay off, the later secrets-delegation slice, exactly like the observability gate
|
|
376
|
+
// probe. The `workspaceField` rule binds only the record's top-level `workspaceId` (see its note
|
|
377
|
+
// above), so a connection row can only ever land in the caller's own in-scope workspace.
|
|
378
|
+
environmentConnectionRepository: {
|
|
379
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
380
|
+
getByWorkspaceAndType: { scope: { kind: 'workspace', arg: 0 } },
|
|
381
|
+
upsert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
382
|
+
softDelete: { scope: { kind: 'workspace', arg: 0 } },
|
|
383
|
+
},
|
|
384
|
+
// The workspace-defined custom-manifest-type catalog the infra configurator reads + edits
|
|
385
|
+
// (`EnvironmentConnectionService.listCustomTypes`/`upsertCustomType`/`removeCustomType`, merged
|
|
386
|
+
// with the deployment's registered code types for display). Rows carry NO secrets — just manifest
|
|
387
|
+
// metadata — so the whole CRUD surface is remote. `listByWorkspace`/`remove` take the workspaceId
|
|
388
|
+
// as arg0 (the `workspace` rule); the record-based `upsert(record)` binds on the record's
|
|
389
|
+
// `workspaceId` FIELD (the `workspaceField` rule). Member-level, workspace-scoped — the same policy
|
|
390
|
+
// as the connection surface above, and it completes the environments management panel (the
|
|
391
|
+
// `listHandlers` bundle loads both the connection handlers AND this catalog).
|
|
392
|
+
customManifestTypeRepository: {
|
|
393
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
394
|
+
upsert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
395
|
+
remove: { scope: { kind: 'workspace', arg: 0 } },
|
|
396
|
+
},
|
|
397
|
+
serviceFragmentDefaultsRepository: {
|
|
398
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
399
|
+
// The service-fragment-defaults editor saves the workspace's default fragment set. Member-level,
|
|
400
|
+
// workspace-scoped — completes the read+write surface (`get` was exposed for the board load).
|
|
401
|
+
set: { scope: { kind: 'workspace', arg: 0 } },
|
|
402
|
+
},
|
|
403
|
+
pipelineScheduleRepository: {
|
|
404
|
+
list: { scope: { kind: 'workspace', arg: 0 } },
|
|
405
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
406
|
+
listByServices: { scope: { kind: 'serviceList', arg: 0 } },
|
|
407
|
+
// Recurring-pipeline management, all driven by the local node's `RecurringPipelineController`
|
|
408
|
+
// → `RecurringPipelineService` (CRUD + run history + `runNow`). Every method takes the
|
|
409
|
+
// workspaceId as arg0 and is member-level (the schedule endpoints are not admin-gated).
|
|
410
|
+
// `runNow` fires the schedule in-process, so its `fire()` writes (`insertRun`/`updateRun`/
|
|
411
|
+
// `upsert`) are on the path too — the sweeper-only `listDue`/`pruneRunsBefore` stay
|
|
412
|
+
// mothership-internal (its cron owns them). Completes the schedule management surface (the
|
|
413
|
+
// `list`/`getByBlock`/`listByServices` reads were already exposed).
|
|
414
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
415
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
416
|
+
remove: { scope: { kind: 'workspace', arg: 0 } },
|
|
417
|
+
insertRun: { scope: { kind: 'workspace', arg: 0 } },
|
|
418
|
+
updateRun: { scope: { kind: 'workspace', arg: 0 } },
|
|
419
|
+
listRuns: { scope: { kind: 'workspace', arg: 0 } },
|
|
420
|
+
},
|
|
421
|
+
trackerSettingsRepository: {
|
|
422
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
423
|
+
// The tracker-settings editor persists its config. Member-level, workspace-scoped — completes
|
|
424
|
+
// the read+write surface (`get` was exposed for the board load).
|
|
425
|
+
put: { scope: { kind: 'workspace', arg: 0 } },
|
|
426
|
+
},
|
|
427
|
+
notificationRepository: {
|
|
428
|
+
listOpen: { scope: { kind: 'workspace', arg: 0 } },
|
|
429
|
+
// The inbox act/dismiss/escalate flow re-reads a single notification by id after a run
|
|
430
|
+
// settles (`NotificationService`). `get(workspaceId, id)` is workspace-scoped on arg0.
|
|
431
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
432
|
+
// The merger-less pipeline tail raises a block notification on completion
|
|
433
|
+
// (`pipeline_complete`/`merge_review` → `findOpenByBlock` dedup + `upsertOpenForBlock`), so a
|
|
434
|
+
// run persists its inbox card on the mothership. Workspace-scoped, member-level (the inbox
|
|
435
|
+
// act/dismiss endpoints are not admin-gated) — the same policy as the block/pipeline writes.
|
|
436
|
+
findOpenByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
437
|
+
// The block-less dedup lookup for deployment/workspace-wide cards (`platform_health`). A
|
|
438
|
+
// local node runs the platform-health sweep too, so it proxies this like `findOpenByBlock`.
|
|
439
|
+
// Workspace-scoped, member-level — same policy as the reads above.
|
|
440
|
+
findOpenByType: { scope: { kind: 'workspace', arg: 0 } },
|
|
441
|
+
upsertOpenForBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
442
|
+
// Block-less raises (a card with no `blockId`) and every status transition the inbox
|
|
443
|
+
// performs right after a run settles — act / dismiss / escalate — go through `upsert`
|
|
444
|
+
// (`NotificationService`), not `upsertOpenForBlock`. Workspace-scoped, member-level (the
|
|
445
|
+
// inbox act/dismiss endpoints are not admin-gated) — same policy as the writes above.
|
|
446
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
447
|
+
// The inbox `act` flow atomically claims the open card (`open` → `acted`) BEFORE running
|
|
448
|
+
// its side effect, so a mothership node must proxy the claim like the surrounding
|
|
449
|
+
// get/upsert. Workspace-scoped, member-level — same policy as `upsert`.
|
|
450
|
+
claimForAction: { scope: { kind: 'workspace', arg: 0 } },
|
|
451
|
+
// The escalation sweep's batched write (a local node runs the sweep too, so it must proxy
|
|
452
|
+
// like the listOpen + per-row upsert loop it replaced). Workspace-scoped like `upsert`.
|
|
453
|
+
escalateStaleOpen: { scope: { kind: 'workspace', arg: 0 } },
|
|
454
|
+
},
|
|
455
|
+
// --- Repo-bootstrap management / retry / stop surface ---------------------------
|
|
456
|
+
// The bootstrap flow a mothership-mode SPA drives (`BootstrapController` +
|
|
457
|
+
// `AgentRunController`): start a repo bootstrap, read a single job (the board-card poll), and
|
|
458
|
+
// retry / stop a failed or running one. The board-load reads (`listByWorkspace` /
|
|
459
|
+
// `listByServices`) were already exposed; these complete the surface. `get`/`update` take the
|
|
460
|
+
// workspaceId as arg0 (the `workspace` rule); the record-based `insert(record)` binds on the
|
|
461
|
+
// job's `workspaceId` FIELD (the `workspaceField` rule — the id is a property, not a positional
|
|
462
|
+
// arg). Each is member-level (the bootstrap endpoints are not admin-gated) and workspace-scoped —
|
|
463
|
+
// the same policy as the block/pipeline mutations. The `insert` record's sibling ids (`blockId`,
|
|
464
|
+
// `referenceArchitectureId`) are NOT re-validated over the RPC (see the `workspaceField` note):
|
|
465
|
+
// the row is stored under — and later read by — the bound `workspaceId`, and a foreign
|
|
466
|
+
// `referenceArchitectureId` is harmless because the retry run re-resolves it via the
|
|
467
|
+
// workspace-scoped `referenceArchitectureRepository.get` below, which 404s a cross-workspace id.
|
|
468
|
+
bootstrapJobRepository: {
|
|
469
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
470
|
+
listByServices: { scope: { kind: 'serviceList', arg: 0 } },
|
|
471
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
472
|
+
insert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
473
|
+
update: { scope: { kind: 'workspace', arg: 0 } },
|
|
474
|
+
},
|
|
475
|
+
// The reference-architecture library the bootstrap modal reads + edits, and that a retry
|
|
476
|
+
// re-resolves the base repo from (`referenceArchitectureRepository.get`). Reads/updates/deletes
|
|
477
|
+
// take the workspaceId as arg0 (the `workspace` rule); the record-based `insert(record)` binds on
|
|
478
|
+
// the record's `workspaceId` FIELD (the `workspaceField` rule). Member-level (the reference-arch
|
|
479
|
+
// endpoints are not admin-gated), workspace-scoped — the same policy as the other library editors.
|
|
480
|
+
referenceArchitectureRepository: {
|
|
481
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
482
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
483
|
+
insert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
484
|
+
update: { scope: { kind: 'workspace', arg: 0 } },
|
|
485
|
+
softDelete: { scope: { kind: 'workspace', arg: 0 } },
|
|
486
|
+
},
|
|
487
|
+
// The board's run controls (retry / stop a failed or running run) enter through the unified
|
|
488
|
+
// `agent_runs` table: `AgentRunController` calls `getRef(workspaceId, id)` to resolve the run's
|
|
489
|
+
// KIND, then dispatches to the matching service. `getRef` takes the workspaceId as arg0, so it
|
|
490
|
+
// reuses the `workspace` rule (resolve the owning account, reject out-of-scope as 404). Exposing
|
|
491
|
+
// it makes the EXECUTION-run retry/stop path functional in mothership mode — every downstream
|
|
492
|
+
// read+write those services make (`executionRepository.get/deleteByBlock/upsert/markFailed`,
|
|
493
|
+
// `blockRepository.update`, `pipelineRepository.get`, the budget/binary-storage prechecks) is
|
|
494
|
+
// already allow-listed on the run/start path. The bootstrap + env-config-repair retry branches
|
|
495
|
+
// read their own repos (`bootstrapJobRepository.get`, `referenceArchitectureRepository.get`, …),
|
|
496
|
+
// now allow-listed too (see the bootstrap / reference-architecture / env-config-repair management
|
|
497
|
+
// surface above). The sweeper-only `listStale`/`liveRunIds` stay mothership-internal (its cron
|
|
498
|
+
// owns them).
|
|
499
|
+
agentRunRepository: {
|
|
500
|
+
getRef: { scope: { kind: 'workspace', arg: 0 } },
|
|
501
|
+
},
|
|
502
|
+
tokenUsageRepository: {
|
|
503
|
+
totalsSinceForWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
504
|
+
// The usage report (Usage settings tab) — one workspace-scoped GROUP BY read, same
|
|
505
|
+
// scoping as the workspace spend rollup above.
|
|
506
|
+
usageBreakdownForWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
507
|
+
// Account/user budget-tier rollups (docs/initiatives/tiered-budgets.md), read on the spend
|
|
508
|
+
// gate + the snapshot. Account-scoped and self-user-scoped respectively, mirroring the
|
|
509
|
+
// account read + the per-user settings read above. (Metered WRITEs — `record` — stay out of
|
|
510
|
+
// the allow-list like all high-volume telemetry writes.)
|
|
511
|
+
totalsSinceForAccount: { scope: { kind: 'account', arg: 0 } },
|
|
512
|
+
totalsSinceForUser: { scope: { kind: 'selfUser', arg: 0 } },
|
|
513
|
+
},
|
|
514
|
+
// Telemetry is local-first by design (Phase 5), but two READS are on the synchronous run
|
|
515
|
+
// path before that batch-sync lands — the kaizen grading step summarises an execution's LLM
|
|
516
|
+
// calls. Until Phase 5 they resolve against the mothership's telemetry store. High-volume
|
|
517
|
+
// telemetry WRITES (`record`) stay out of the allow-list — they must never hit the RPC.
|
|
518
|
+
llmCallMetricRepository: {
|
|
519
|
+
summarizeByExecution: { scope: { kind: 'workspace', arg: 0 } },
|
|
520
|
+
},
|
|
521
|
+
// Kaizen grading (the merge lifecycle's quality step) reads its prior grade for a step before
|
|
522
|
+
// (re-)grading and writes the result. Both are workspace-scoped on arg0; the sweeper methods
|
|
523
|
+
// (`listPending`/`claim`) stay mothership-internal.
|
|
524
|
+
//
|
|
525
|
+
// The Kaizen SCREEN read surface is exposed too, so a mothership-mode SPA can display the
|
|
526
|
+
// grading history + per-run grading status (`KaizenController` → `KaizenService.getOverview` /
|
|
527
|
+
// `listForExecution`, both member-level, read-only, mounted under `/workspaces/:workspaceId`):
|
|
528
|
+
// `listByWorkspace(workspaceId, limit?)` (the screen's bounded history) and
|
|
529
|
+
// `listByExecution(workspaceId, executionId)` (the run-window per-step status). Both take the
|
|
530
|
+
// workspaceId as arg0 (the `workspace` rule). The internal-only single-grade `get(workspaceId,
|
|
531
|
+
// id)` is not on any SPA path (the service never calls it), and `listPending`/`claim` are the
|
|
532
|
+
// background sweep's kind-spanning reads — all stay mothership-internal.
|
|
533
|
+
kaizenGradingRepository: {
|
|
534
|
+
getByStep: { scope: { kind: 'workspace', arg: 0 } },
|
|
535
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
536
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
537
|
+
listByExecution: { scope: { kind: 'workspace', arg: 0 } },
|
|
538
|
+
},
|
|
539
|
+
// Mixed (workspaceId + blockId/stage): the workspace arg stays the scope key.
|
|
540
|
+
requirementReviewRepository: {
|
|
541
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
542
|
+
// The requirements gate reads a review by id (`get(workspaceId, id)`) when driving the
|
|
543
|
+
// parked run (re-review / incorporate). Workspace-scoped on arg0.
|
|
544
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
545
|
+
// The reviewer/incorporation companion persists the review as the gate iterates.
|
|
546
|
+
// Member-level (the requirement-review endpoints are not admin-gated), workspace-scoped.
|
|
547
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
548
|
+
// The service drops a block's prior review before a fresh review run
|
|
549
|
+
// (`RequirementReviewService.review`). Workspace-scoped on arg0 — completes the repo.
|
|
550
|
+
deleteByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
551
|
+
},
|
|
552
|
+
// Interactive document-interview sessions (WS5). The doc-authoring RUN PATH reads the
|
|
553
|
+
// converged brief (`getByBlock`, via the agent-context builder on every doc-writer dispatch),
|
|
554
|
+
// and the interview window reads/persists as the gate iterates. All workspace-scoped on arg0,
|
|
555
|
+
// mirroring the requirement-review surface.
|
|
556
|
+
docInterviewRepository: {
|
|
557
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
558
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
559
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
560
|
+
deleteByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
561
|
+
},
|
|
562
|
+
// The merge lifecycle's kaizen step reads any prior verified model/prompt combo
|
|
563
|
+
// (`getByKey(workspaceId, comboKey)`) to skip re-grading. Workspace-scoped on arg0. The Kaizen
|
|
564
|
+
// screen also lists the whole verified-combo library (`listByWorkspace`, part of the same
|
|
565
|
+
// `getOverview` read) — workspace-scoped, read-only, member-level. The sweep's `upsert` (the
|
|
566
|
+
// streak/verified write) stays off the SPA path — kaizen grading is best-effort in mothership
|
|
567
|
+
// mode until the Phase 5 telemetry/local-first sync lands.
|
|
568
|
+
kaizenVerifiedComboRepository: {
|
|
569
|
+
getByKey: { scope: { kind: 'workspace', arg: 0 } },
|
|
570
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
571
|
+
},
|
|
572
|
+
// Env-config-repair (a Tester sub-flow) lists a workspace's repair jobs on the run path
|
|
573
|
+
// (`listByWorkspace`), and the board's run controls retry / stop a failed or running repair run:
|
|
574
|
+
// `get`/`update` take the workspaceId as arg0 (the `workspace` rule), the record-based
|
|
575
|
+
// `insert(record)` binds on the job's `workspaceId` FIELD (the `workspaceField` rule). Retry
|
|
576
|
+
// STARTS a fresh run from the failed job's coords, so it reads the prior job (`get`) then inserts
|
|
577
|
+
// a new one; stop patches the running job (`update`). Member-level, workspace-scoped.
|
|
578
|
+
envConfigRepairJobRepository: {
|
|
579
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
580
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
581
|
+
insert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
582
|
+
update: { scope: { kind: 'workspace', arg: 0 } },
|
|
583
|
+
},
|
|
584
|
+
// The ephemeral-environment SELF-TEST run store (`environment_test_runs`): a member-level,
|
|
585
|
+
// workspace-scoped run-path diagnostic (`EnvironmentTestService` — start / durable poll /
|
|
586
|
+
// stop, plus the snapshot's in-flight-runs read `listRunningByWorkspace`). The whole repo is
|
|
587
|
+
// remote: `get`/`updateIfRunning`/`listRunningByWorkspace` take the workspaceId as arg0 (the
|
|
588
|
+
// `workspace` rule); the record-based `insert(record)` binds on the run's `workspaceId` FIELD
|
|
589
|
+
// (the `workspaceField` rule). The sweeper-only cross-workspace `listStale` stays
|
|
590
|
+
// mothership-internal (its cron owns it), per the global-sweeper exclusion above. The GitHub
|
|
591
|
+
// half of the self-test (branch create/delete via `resolveRunRepoContext`) rides mothership
|
|
592
|
+
// GitHub token delegation (`/internal/github/installation-token`), not this table. What
|
|
593
|
+
// still gates a FULL mothership-mode self-test: the provisioning WRITES
|
|
594
|
+
// (`environmentRegistryRepository.insert`/`update`) stay off until the secrets-delegation
|
|
595
|
+
// slice, so the run's provisioning stage fails cleanly there — the store itself is proxied
|
|
596
|
+
// so the runs surface, clean up, and complete the moment that slice lands.
|
|
597
|
+
environmentTestRunRepository: {
|
|
598
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
599
|
+
insert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
600
|
+
updateIfRunning: { scope: { kind: 'workspace', arg: 0 } },
|
|
601
|
+
listRunningByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
602
|
+
},
|
|
603
|
+
// --- Advanced review / structured-dialogue session surfaces ---------------------
|
|
604
|
+
// The clarity-review (bug-report triage), brainstorm (structured dialogue) and consensus
|
|
605
|
+
// (multi-strategy orchestration) windows mirror the requirements-review surface above: rows
|
|
606
|
+
// scoped by workspace, keyed by block/stage/step, with a live entry per block. A mothership-mode
|
|
607
|
+
// SPA runs and re-reads these reviews, and the services persist/replace them as the window
|
|
608
|
+
// iterates — every method takes the workspaceId as arg0 (the `upsert(workspaceId, review)`
|
|
609
|
+
// signature carries it positionally, so the `workspace` rule binds it, not `workspaceField`).
|
|
610
|
+
// Member-level (none of the review endpoints is admin-gated), workspace-scoped — the same policy
|
|
611
|
+
// as the requirement-review surface. Completes the read+write surface (`getByBlock` /
|
|
612
|
+
// `getByBlockStage` were already exposed for the board load).
|
|
613
|
+
clarityReviewRepository: {
|
|
614
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
615
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
616
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
617
|
+
deleteByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
618
|
+
},
|
|
619
|
+
brainstormSessionRepository: {
|
|
620
|
+
getByBlockStage: { scope: { kind: 'workspace', arg: 0 } },
|
|
621
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
622
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
623
|
+
deleteByBlockStage: { scope: { kind: 'workspace', arg: 0 } },
|
|
624
|
+
},
|
|
625
|
+
// Initiatives (the long-running multi-task work container): the create/read surface the
|
|
626
|
+
// board + tracker window use, plus the planning pipeline's ingest writes. Every method is
|
|
627
|
+
// workspaceId-arg0 scoped; the rev-guarded `compareAndSwap` carries the whole entity as
|
|
628
|
+
// arg1 with the expected rev as arg2. `listExecuting` (the cross-workspace cron sweeper
|
|
629
|
+
// read) is deliberately NOT here — it stays mothership-internal.
|
|
630
|
+
initiativeRepository: {
|
|
631
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
632
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
633
|
+
list: { scope: { kind: 'workspace', arg: 0 } },
|
|
634
|
+
insert: { scope: { kind: 'workspace', arg: 0 } },
|
|
635
|
+
compareAndSwap: { scope: { kind: 'workspace', arg: 0 } },
|
|
636
|
+
delete: { scope: { kind: 'workspace', arg: 0 } },
|
|
637
|
+
},
|
|
638
|
+
consensusSessionRepository: {
|
|
639
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
640
|
+
getByStep: { scope: { kind: 'workspace', arg: 0 } },
|
|
641
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
642
|
+
upsert: { scope: { kind: 'workspace', arg: 0 } },
|
|
643
|
+
},
|
|
644
|
+
// --- Post-release-health / observability settings surface -----------------------
|
|
645
|
+
// The three settings repositories a mothership-mode SPA manages for the post-release-health
|
|
646
|
+
// flow: the (single) observability connection, the per-block monitor/SLO mapping, and the
|
|
647
|
+
// incident-enrichment connection. Their controllers mount under `/workspaces/:workspaceId`
|
|
648
|
+
// and are member-level (not admin-gated), so they follow the same policy as the other
|
|
649
|
+
// settings panels above. Reads/deletes take the workspaceId as arg0 (the `workspace` rule);
|
|
650
|
+
// the record-based `upsert(record)` binds on the record's `workspaceId` FIELD (the
|
|
651
|
+
// `workspaceField` rule — the id is a property, not a positional arg). Exposing them makes
|
|
652
|
+
// the observability / release-health / incident-enrichment editors functional (persist +
|
|
653
|
+
// read back), not read-only, in mothership mode.
|
|
654
|
+
//
|
|
655
|
+
// Scope of what this unlocks: the settings PANELS work end-to-end (save + read back the
|
|
656
|
+
// redacted summary, which never decrypts). The saved connection cannot yet DRIVE a
|
|
657
|
+
// post-release-health gate probe in mothership mode — decrypting the sealed connection cipher
|
|
658
|
+
// at gate-probe time belongs to the later secrets-delegation slice. The connection `get` here
|
|
659
|
+
// returns the FULL record (the sealed `credentials` blob), not the redacted service view: the
|
|
660
|
+
// RPC client is the trusted local node, the blob is sealed and account-scoped, so this matches
|
|
661
|
+
// the existing `environmentRegistryRepository.get` precedent (sealed cipher over the machine
|
|
662
|
+
// API). The record-based `upsert` binds only the top-level `record.workspaceId` (see the
|
|
663
|
+
// `workspaceField` note above) — `releaseHealthConfigRepository`'s `blockId` is NOT
|
|
664
|
+
// re-validated here, so a config can only ever be planted into the caller's own in-scope
|
|
665
|
+
// workspace, never another's.
|
|
666
|
+
observabilityConnectionRepository: {
|
|
667
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
668
|
+
upsert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
669
|
+
delete: { scope: { kind: 'workspace', arg: 0 } },
|
|
670
|
+
},
|
|
671
|
+
releaseHealthConfigRepository: {
|
|
672
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
673
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
674
|
+
upsert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
675
|
+
delete: { scope: { kind: 'workspace', arg: 0 } },
|
|
676
|
+
},
|
|
677
|
+
// The SENSITIVE per-service test credentials, keyed by service-frame block like
|
|
678
|
+
// `releaseHealthConfigRepository` above. `credentials` rides a SEALED blob (sealed/decrypted
|
|
679
|
+
// in the service under the LOCAL key), so no plaintext crosses the machine API — the same
|
|
680
|
+
// precedent as the observability / package-registry connections. The inspector CRUD
|
|
681
|
+
// (`getByBlock`/`deleteByBlock`) + the run-path frame read (`getByBlock`) are workspace-scoped
|
|
682
|
+
// on arg0; the record-based `upsert` binds on its `workspaceId` FIELD. `listByWorkspace` has no
|
|
683
|
+
// consumer yet, so it stays pending (marked in the allow-list completeness test).
|
|
684
|
+
testSecretsRepository: {
|
|
685
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
686
|
+
upsert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
687
|
+
deleteByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
688
|
+
},
|
|
689
|
+
// The per-service PRE-PR VALIDATION CHECKS, keyed by service-frame block like
|
|
690
|
+
// `releaseHealthConfigRepository` above. Nothing sealed — the commands are operator-authored
|
|
691
|
+
// shell strings that run inside the run's own container — so the plain record crosses the
|
|
692
|
+
// machine API. The inspector CRUD (`getByBlock`/`listByWorkspace`/`delete`) and the dispatch's
|
|
693
|
+
// frame read (`getByBlock`) are workspace-scoped on arg0; the record-based `upsert` binds on
|
|
694
|
+
// its `workspaceId` FIELD.
|
|
695
|
+
validationConfigRepository: {
|
|
696
|
+
getByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
697
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
698
|
+
upsert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
699
|
+
delete: { scope: { kind: 'workspace', arg: 0 } },
|
|
700
|
+
},
|
|
701
|
+
incidentEnrichmentConnectionRepository: {
|
|
702
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
703
|
+
upsert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
704
|
+
delete: { scope: { kind: 'workspace', arg: 0 } },
|
|
705
|
+
},
|
|
706
|
+
// The private package-registry connection (sealed npm/GitHub-Packages entries): the
|
|
707
|
+
// settings panel's list/add/remove and the container dispatch's decrypt-time read all
|
|
708
|
+
// ride get/upsert/delete, workspace-scoped like the observability connection above
|
|
709
|
+
// (same sealed-blob-over-the-machine-API precedent).
|
|
710
|
+
packageRegistryConnectionRepository: {
|
|
711
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
712
|
+
upsert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
713
|
+
delete: { scope: { kind: 'workspace', arg: 0 } },
|
|
714
|
+
},
|
|
715
|
+
// --- VCS / GitHub projection READ surface ---------------------------------------
|
|
716
|
+
// The GitHub read models the SPA's VCS board panels display (repos / branches / PRs /
|
|
717
|
+
// issues), served straight from the local projections by `GitHubService` (`container.github`)
|
|
718
|
+
// — fast, rate-limit-free, and NO GitHub API call, so they run unchanged in mothership mode
|
|
719
|
+
// over the remote-sourced projection repos. Each takes the workspaceId as arg0 (the
|
|
720
|
+
// `workspace` rule); reads only.
|
|
721
|
+
//
|
|
722
|
+
// These same reads are ALSO the run path: `resolveRepoTarget` (which runs on EVERY
|
|
723
|
+
// container-agent dispatch to find a block's repo) reads `githubInstallationRepository.
|
|
724
|
+
// getByWorkspace` FIRST and returns null if there's no installation, THEN walks the
|
|
725
|
+
// `github_repos` projection via `repoProjectionRepository.list` and the block ancestry via
|
|
726
|
+
// `blockRepository.get` / `serviceRepository.getByFrameBlock` (both already remote). So
|
|
727
|
+
// closing the run-path gap for real (non-fake-executor) runs needs BOTH the installation
|
|
728
|
+
// read and `list` — allow-listing `list` alone left the resolver failing one call earlier on
|
|
729
|
+
// the un-remoted installation read. `getByWorkspace` is a member-level read (its own binding
|
|
730
|
+
// or the account-shared one), workspace-scoped on arg0.
|
|
731
|
+
//
|
|
732
|
+
// Deliberately EXCLUDED (a later "GitHub sync + repo-write" slice): the projection WRITE
|
|
733
|
+
// surface — `upsertMany` (the sync/webhook ingest; the mothership owns GitHub sync, since the
|
|
734
|
+
// App + webhooks live there), the board-linkage write `repoProjectionRepository.setMonorepo`,
|
|
735
|
+
// the sync cursors (`getCursor`/`setCursor`, keyed on installationId not
|
|
736
|
+
// workspaceId), and `tombstoneMissing`. `repoProjectionRepository.get` stays off too: it backs
|
|
737
|
+
// only `GitHubService.resolve` for the repo-WRITE endpoints (create-branch / open-PR /
|
|
738
|
+
// merge / comment), and exposing it alone would let create-branch/open-PR perform the real
|
|
739
|
+
// GitHub write and THEN fail on the un-remoted `upsertMany` projection refresh — a worse
|
|
740
|
+
// failure than today's clean pre-write refusal. It comes back with the repo-write slice. The
|
|
741
|
+
// rest of `githubInstallationRepository` (installationId-keyed reads, sync/token writes, the
|
|
742
|
+
// fan-out, the cron `listActive`) also stays off — only the workspace-scoped `getByWorkspace`
|
|
743
|
+
// the run path needs is opened here.
|
|
744
|
+
githubInstallationRepository: {
|
|
745
|
+
getByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
746
|
+
},
|
|
747
|
+
repoProjectionRepository: {
|
|
748
|
+
list: { scope: { kind: 'workspace', arg: 0 } },
|
|
749
|
+
},
|
|
750
|
+
branchProjectionRepository: {
|
|
751
|
+
listByRepo: { scope: { kind: 'workspace', arg: 0 } },
|
|
752
|
+
},
|
|
753
|
+
pullRequestProjectionRepository: {
|
|
754
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
755
|
+
},
|
|
756
|
+
issueProjectionRepository: {
|
|
757
|
+
listByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
758
|
+
},
|
|
759
|
+
// --- Self-hosted runner-backend connection surface ------------------------------
|
|
760
|
+
// The workspace's binding to an "agent runner backend" (the manifest HTTP pool / native
|
|
761
|
+
// Kubernetes runner / …) the runner-pool settings panel manages (`RunnerPoolController` →
|
|
762
|
+
// `RunnerPoolConnectionService`: connect / rotate secrets / disconnect / describe / test).
|
|
763
|
+
// The controller mounts under `/workspaces/:workspaceId` and is member-level (not admin-gated),
|
|
764
|
+
// so it follows the same policy as the observability / environment connection panels above.
|
|
765
|
+
// `getByWorkspace`/`softDelete` take the workspaceId as arg0 (the `workspace` rule); the
|
|
766
|
+
// record-based `upsert(record)` binds on the record's `workspaceId` FIELD (the `workspaceField`
|
|
767
|
+
// rule — the id is a property, not a positional arg). Exposing these makes the runner-backend
|
|
768
|
+
// connection panel functional (persist + read back the safe metadata) in mothership mode.
|
|
769
|
+
//
|
|
770
|
+
// Safe to expose like the observability / environment connections: the record carries the
|
|
771
|
+
// backend credentials as a SEALED blob (`secretsCipher`) — the repo returns it verbatim (it
|
|
772
|
+
// does NOT decrypt); sealing/decryption live in `RunnerPoolConnectionService` under the LOCAL
|
|
773
|
+
// key, so no plaintext credential crosses the machine API and the mothership only ever stores
|
|
774
|
+
// ciphertext (the "the mothership ENCRYPTION_KEY never reaches the laptop" split holds). The
|
|
775
|
+
// `workspaceField` rule binds only the record's top-level `workspaceId`, so a connection row can
|
|
776
|
+
// only ever land in the caller's own in-scope workspace.
|
|
777
|
+
runnerPoolConnectionRepository: {
|
|
778
|
+
getByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
779
|
+
upsert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
780
|
+
softDelete: { scope: { kind: 'workspace', arg: 0 } },
|
|
781
|
+
},
|
|
782
|
+
// --- Binary-artifact metadata surface (visual-confirmation gate) -----------------
|
|
783
|
+
// The metadata rows for stored binary blobs (UI screenshots + the reference design images they
|
|
784
|
+
// are reviewed against) the visual-confirmation gate + the artifact controllers read/write
|
|
785
|
+
// (`ArtifactController` / `HarnessArtifactController`, mounted under `/workspaces/:workspaceId`,
|
|
786
|
+
// member-level). Only the METADATA lives in the relational store (D1 ⇄ Postgres) and is proxied
|
|
787
|
+
// here; the BYTES live in the per-account blob backend (R2 / S3 / fs / …), resolved locally, so
|
|
788
|
+
// they never cross this API. Point reads/deletes take the workspaceId as arg0 (the `workspace`
|
|
789
|
+
// rule); the record-based `insert(record)` binds on the record's `workspaceId` FIELD (the
|
|
790
|
+
// `workspaceField` rule). Every read already filters by the (authenticated) workspaceId, so a
|
|
791
|
+
// row's non-authoritative `executionId`/`blockId` need no separate scope check. The retention
|
|
792
|
+
// sweep (`listOlderThan`/`deleteOlderThan`) stays mothership-internal (the mothership owns
|
|
793
|
+
// durable-state retention), like the other global sweeper methods.
|
|
794
|
+
binaryArtifactMetadataStore: {
|
|
795
|
+
insert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
796
|
+
get: { scope: { kind: 'workspace', arg: 0 } },
|
|
797
|
+
listByExecution: { scope: { kind: 'workspace', arg: 0 } },
|
|
798
|
+
countByExecution: { scope: { kind: 'workspace', arg: 0 } },
|
|
799
|
+
listByBlock: { scope: { kind: 'workspace', arg: 0 } },
|
|
800
|
+
delete: { scope: { kind: 'workspace', arg: 0 } },
|
|
801
|
+
},
|
|
802
|
+
// --- Prompt-fragment library management surface ---------------------------------
|
|
803
|
+
// The tenant-scoped prompt-fragment library (ADR 0006) a mothership-mode SPA curates
|
|
804
|
+
// (`FragmentLibraryController` → `FragmentLibraryService`): list / create / update / delete
|
|
805
|
+
// hand-authored fragments at either tier. The library module assembles from
|
|
806
|
+
// `promptFragmentRepository` ALONE (no connection/secret repo — unlike the document/task
|
|
807
|
+
// integrations, whose modules require a decrypt-inside connection repo and so stay off), and its
|
|
808
|
+
// rows carry NO secrets, so the whole management surface is remote. Every method is keyed by an
|
|
809
|
+
// `(ownerKind, ownerId)` PAIR (`ownerKind` ∈ `workspace` | `account`), bound by the `owner` scope
|
|
810
|
+
// rule (positional pair) / `ownerField` rule (the record's fields on `upsert`): a `workspace`
|
|
811
|
+
// owner resolves its account like the `workspace` rule, an `account` owner IS the accountId — so a
|
|
812
|
+
// machine token scoped to one account can never read/write another tenant's fragments. Both tiers'
|
|
813
|
+
// endpoints are member-level (account-tier routes guard on `requireMember`, NOT `requireAdmin`), so
|
|
814
|
+
// this follows the same member-level policy as the other settings/library panels above.
|
|
815
|
+
//
|
|
816
|
+
// The `sourceId`-keyed `listBySource` stays off — it is the repo-sync fan-out read (the mothership
|
|
817
|
+
// owns GitHub sync; the source service is gated on a GitHub client absent on a mothership node), so
|
|
818
|
+
// it is not on the SPA library-management path here.
|
|
819
|
+
promptFragmentRepository: {
|
|
820
|
+
listByOwner: { scope: { kind: 'owner', kindArg: 0, idArg: 1 } },
|
|
821
|
+
get: { scope: { kind: 'owner', kindArg: 0, idArg: 1 } },
|
|
822
|
+
upsert: { scope: { kind: 'ownerField', arg: 0 } },
|
|
823
|
+
softDelete: { scope: { kind: 'owner', kindArg: 0, idArg: 1 } },
|
|
824
|
+
},
|
|
825
|
+
// The fragment-source (repo-linkage) library the SPA lists + links (`FragmentSourceService`), owner
|
|
826
|
+
// scoped exactly like the fragments above. `listByOwner` (the sources list) is bound by the `owner`
|
|
827
|
+
// rule; the record-based `upsert(record)` by `ownerField`. The `sourceId`-keyed reads/writes
|
|
828
|
+
// (`get`/`updateSyncState`/`softDelete`) stay off — they back the repo-SYNC management the
|
|
829
|
+
// mothership owns (the source service needs a GitHub client, which a mothership node does not have),
|
|
830
|
+
// so a later GitHub-sync-in-mothership slice opens them with a source→owner resolver.
|
|
831
|
+
fragmentSourceRepository: {
|
|
832
|
+
listByOwner: { scope: { kind: 'owner', kindArg: 0, idArg: 1 } },
|
|
833
|
+
upsert: { scope: { kind: 'ownerField', arg: 0 } },
|
|
834
|
+
},
|
|
835
|
+
// --- Account onboarding read surface --------------------------------------------
|
|
836
|
+
// The two account-scoped READS a mothership-mode SPA's account/members + email-settings panels
|
|
837
|
+
// drive, both member-level (`AccountController` guards them with `requireMember`, NOT
|
|
838
|
+
// `requireAdmin`). arg0 is an accountId → the `account` rule (reject out-of-scope as 404). The
|
|
839
|
+
// account-lifecycle WRITES stay off: `invitationRepository.create`/`setStatus` (inviting/revoking
|
|
840
|
+
// members is admin-gated), its pre-auth `findByTokenHash`/`get` (the unauthenticated accept-invite
|
|
841
|
+
// lookup — never a scoped-token call), and `emailConnectionRepository.upsert`/`softDelete`
|
|
842
|
+
// (connect/disconnect are admin-gated). The email connection `getByAccount` returns the record with
|
|
843
|
+
// its provider key as a SEALED `apiKeyCipher` blob (the repo does NOT decrypt — sealing/decryption
|
|
844
|
+
// live in the email service; delivery is delegated to the mothership), so no plaintext credential
|
|
845
|
+
// crosses the machine API — the same sealed-blob precedent as the observability/runner connections.
|
|
846
|
+
invitationRepository: {
|
|
847
|
+
listByAccount: { scope: { kind: 'account', arg: 0 } },
|
|
848
|
+
},
|
|
849
|
+
emailConnectionRepository: {
|
|
850
|
+
getByAccount: { scope: { kind: 'account', arg: 0 } },
|
|
851
|
+
},
|
|
852
|
+
// --- Slack integration management surface ---------------------------------------
|
|
853
|
+
// The Slack integration settings a mothership-mode SPA manages (`SlackController` →
|
|
854
|
+
// `SlackConnectionService` / `SlackSettingsService` / `SlackMemberMappingService`): connect /
|
|
855
|
+
// disconnect the per-account Slack workspace, edit the per-workspace notification routing, and
|
|
856
|
+
// maintain the per-account GitHub-user → Slack-member mapping. The controller mounts under
|
|
857
|
+
// `/workspaces/:workspaceId` and is member-level (not admin-gated), so it follows the same policy
|
|
858
|
+
// as the observability / environment / runner-pool connection panels above.
|
|
859
|
+
//
|
|
860
|
+
// Safe to expose exactly like those connection surfaces: the Slack bot token rides a SEALED blob
|
|
861
|
+
// (`tokenCipher`) — the repo returns it verbatim (it does NOT decrypt); sealing/decryption live in
|
|
862
|
+
// the Slack service/channel under the LOCAL key, so no plaintext credential crosses the machine
|
|
863
|
+
// API and the mothership only ever stores ciphertext (the "mothership ENCRYPTION_KEY never reaches
|
|
864
|
+
// the laptop" split holds). The settings + member-mapping rows carry NO secrets at all.
|
|
865
|
+
//
|
|
866
|
+
// Scope of what this unlocks: the settings PANELS work end-to-end (connect / disconnect / route /
|
|
867
|
+
// map + read back the redacted connection view). What it does NOT change: mothership-side Slack
|
|
868
|
+
// DELIVERY of a notification raised by a hosted teammate — that reads + decrypts the token on the
|
|
869
|
+
// mothership, which cannot open a laptop-sealed blob, so it rides the later secrets-delegation
|
|
870
|
+
// slice, exactly like the observability gate probe. Local delivery (the run's own node raised the
|
|
871
|
+
// notification and holds the local key) is unaffected.
|
|
872
|
+
//
|
|
873
|
+
// `slackConnectionRepository` is per-ACCOUNT: `getByAccount`/`softDelete` take the accountId as
|
|
874
|
+
// arg0 (the `account` rule — the local service resolves the workspace → account via the already
|
|
875
|
+
// remote `workspaceRepository.accountOf`, then calls with that in-scope accountId), and the
|
|
876
|
+
// record-based `upsert(record)` binds on the record's `accountId` FIELD (the new `accountField`
|
|
877
|
+
// rule). `getByTeam` is NOT here: it is a GLOBAL teamId → connection lookup used only by the
|
|
878
|
+
// inbound OAuth callback / event webhook, which run on the mothership (never the laptop) and
|
|
879
|
+
// cannot be account-scoped — it stays mothership-internal (classified `sweeper` in the drift
|
|
880
|
+
// guard, the same "unscoped, mothership-internal" bucket as `repoProjectionRepository.listByInstallation`).
|
|
881
|
+
slackConnectionRepository: {
|
|
882
|
+
getByAccount: { scope: { kind: 'account', arg: 0 } },
|
|
883
|
+
upsert: { scope: { kind: 'accountField', arg: 0 } },
|
|
884
|
+
softDelete: { scope: { kind: 'account', arg: 0 } },
|
|
885
|
+
},
|
|
886
|
+
// Per-workspace notification routing (channel per notification kind + a mentions flag). No
|
|
887
|
+
// secrets. `getByWorkspace` takes the workspaceId as arg0 (the `workspace` rule); the
|
|
888
|
+
// record-based `upsert(record)` binds on the record's `workspaceId` FIELD (the `workspaceField` rule).
|
|
889
|
+
slackSettingsRepository: {
|
|
890
|
+
getByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
|
|
891
|
+
upsert: { scope: { kind: 'workspaceField', arg: 0 } },
|
|
892
|
+
},
|
|
893
|
+
// Per-account GitHub-user → Slack-member mapping (for @-mentions). No secrets. Both methods take
|
|
894
|
+
// the accountId as arg0 positionally (`upsert(accountId, entries, at)` — a positional accountId,
|
|
895
|
+
// not a record), so the `account` rule binds both.
|
|
896
|
+
slackMemberMappingRepository: {
|
|
897
|
+
getByAccount: { scope: { kind: 'account', arg: 0 } },
|
|
898
|
+
upsert: { scope: { kind: 'account', arg: 0 } },
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
//# sourceMappingURL=rpc-allowlist.js.map
|