@cat-factory/server 0.226.0 → 0.228.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.
Files changed (98) hide show
  1. package/dist/agents/resolveRepoTarget.d.ts.map +1 -1
  2. package/dist/agents/resolveRepoTarget.js +20 -10
  3. package/dist/agents/resolveRepoTarget.js.map +1 -1
  4. package/dist/app.d.ts +29 -0
  5. package/dist/app.d.ts.map +1 -1
  6. package/dist/app.js +131 -88
  7. package/dist/app.js.map +1 -1
  8. package/dist/github/GitHubPrReportPublisher.d.ts +63 -11
  9. package/dist/github/GitHubPrReportPublisher.d.ts.map +1 -1
  10. package/dist/github/GitHubPrReportPublisher.js +137 -35
  11. package/dist/github/GitHubPrReportPublisher.js.map +1 -1
  12. package/dist/http/workspaceAccess.d.ts +65 -40
  13. package/dist/http/workspaceAccess.d.ts.map +1 -1
  14. package/dist/http/workspaceAccess.js +84 -44
  15. package/dist/http/workspaceAccess.js.map +1 -1
  16. package/dist/modules/agentPrompts/AgentPromptController.js +2 -2
  17. package/dist/modules/agentPrompts/AgentPromptController.js.map +1 -1
  18. package/dist/modules/agentSettings/WorkspaceAgentSettingsController.js +2 -2
  19. package/dist/modules/agentSettings/WorkspaceAgentSettingsController.js.map +1 -1
  20. package/dist/modules/bootstrap/BootstrapController.js +2 -2
  21. package/dist/modules/bootstrap/BootstrapController.js.map +1 -1
  22. package/dist/modules/capabilityCredentials/CapabilityCredentialsController.d.ts +2 -2
  23. package/dist/modules/capabilityCredentials/CapabilityCredentialsController.d.ts.map +1 -1
  24. package/dist/modules/capabilityCredentials/CapabilityCredentialsController.js +4 -11
  25. package/dist/modules/capabilityCredentials/CapabilityCredentialsController.js.map +1 -1
  26. package/dist/modules/consensus/ConsensusGroupController.js +2 -2
  27. package/dist/modules/consensus/ConsensusGroupController.js.map +1 -1
  28. package/dist/modules/documents/DocumentSourceController.d.ts +10 -0
  29. package/dist/modules/documents/DocumentSourceController.d.ts.map +1 -1
  30. package/dist/modules/documents/DocumentSourceController.js +37 -4
  31. package/dist/modules/documents/DocumentSourceController.js.map +1 -1
  32. package/dist/modules/environments/EnvironmentController.d.ts.map +1 -1
  33. package/dist/modules/environments/EnvironmentController.js +6 -2
  34. package/dist/modules/environments/EnvironmentController.js.map +1 -1
  35. package/dist/modules/foundationalServices/FoundationalServiceController.d.ts.map +1 -1
  36. package/dist/modules/foundationalServices/FoundationalServiceController.js +14 -13
  37. package/dist/modules/foundationalServices/FoundationalServiceController.js.map +1 -1
  38. package/dist/modules/fragmentLibrary/FragmentLibraryController.d.ts.map +1 -1
  39. package/dist/modules/fragmentLibrary/FragmentLibraryController.js +17 -11
  40. package/dist/modules/fragmentLibrary/FragmentLibraryController.js.map +1 -1
  41. package/dist/modules/github/GitHubController.js +3 -3
  42. package/dist/modules/github/GitHubController.js.map +1 -1
  43. package/dist/modules/gitlab/GitLabController.js +2 -2
  44. package/dist/modules/gitlab/GitLabController.js.map +1 -1
  45. package/dist/modules/incidentEnrichment/IncidentEnrichmentController.js +2 -2
  46. package/dist/modules/incidentEnrichment/IncidentEnrichmentController.js.map +1 -1
  47. package/dist/modules/merge/RiskPolicyController.js +2 -2
  48. package/dist/modules/merge/RiskPolicyController.js.map +1 -1
  49. package/dist/modules/modelPresets/ModelPresetController.js +2 -2
  50. package/dist/modules/modelPresets/ModelPresetController.js.map +1 -1
  51. package/dist/modules/notificationWebhook/NotificationWebhookController.js +2 -2
  52. package/dist/modules/notificationWebhook/NotificationWebhookController.js.map +1 -1
  53. package/dist/modules/packageRegistries/PackageRegistriesController.js +2 -2
  54. package/dist/modules/packageRegistries/PackageRegistriesController.js.map +1 -1
  55. package/dist/modules/preview/PreviewController.js +2 -2
  56. package/dist/modules/preview/PreviewController.js.map +1 -1
  57. package/dist/modules/providers/ApiKeyController.js +2 -2
  58. package/dist/modules/providers/ApiKeyController.js.map +1 -1
  59. package/dist/modules/providers/VendorCredentialController.js +2 -2
  60. package/dist/modules/providers/VendorCredentialController.js.map +1 -1
  61. package/dist/modules/publicApi/PublicApiKeyController.js +2 -2
  62. package/dist/modules/publicApi/PublicApiKeyController.js.map +1 -1
  63. package/dist/modules/publicApi/openapiDocument.generated.d.ts +1 -1
  64. package/dist/modules/publicApi/openapiDocument.generated.d.ts.map +1 -1
  65. package/dist/modules/publicApi/openapiDocument.generated.js +1 -1
  66. package/dist/modules/publicApi/openapiDocument.generated.js.map +1 -1
  67. package/dist/modules/recurring/TrackerSettingsController.js +2 -2
  68. package/dist/modules/recurring/TrackerSettingsController.js.map +1 -1
  69. package/dist/modules/releaseHealth/ReleaseHealthController.js +2 -2
  70. package/dist/modules/releaseHealth/ReleaseHealthController.js.map +1 -1
  71. package/dist/modules/runners/RunnerPoolController.js +2 -2
  72. package/dist/modules/runners/RunnerPoolController.js.map +1 -1
  73. package/dist/modules/sandbox/SandboxController.js +2 -2
  74. package/dist/modules/sandbox/SandboxController.js.map +1 -1
  75. package/dist/modules/settings/WorkspaceSettingsController.js +2 -2
  76. package/dist/modules/settings/WorkspaceSettingsController.js.map +1 -1
  77. package/dist/modules/sharedStack/SharedStackController.js +2 -2
  78. package/dist/modules/sharedStack/SharedStackController.js.map +1 -1
  79. package/dist/modules/slack/SlackController.js +2 -2
  80. package/dist/modules/slack/SlackController.js.map +1 -1
  81. package/dist/modules/tasks/TaskSourceController.js +2 -2
  82. package/dist/modules/tasks/TaskSourceController.js.map +1 -1
  83. package/dist/modules/testSecrets/TestSecretsController.js +2 -2
  84. package/dist/modules/testSecrets/TestSecretsController.js.map +1 -1
  85. package/dist/modules/toolServers/ToolServerController.d.ts +1 -1
  86. package/dist/modules/toolServers/ToolServerController.d.ts.map +1 -1
  87. package/dist/modules/toolServers/ToolServerController.js +3 -9
  88. package/dist/modules/toolServers/ToolServerController.js.map +1 -1
  89. package/dist/modules/tutorial/TutorialController.js +1 -1
  90. package/dist/modules/tutorial/TutorialController.js.map +1 -1
  91. package/dist/modules/validation/ValidationConfigController.d.ts.map +1 -1
  92. package/dist/modules/validation/ValidationConfigController.js +5 -2
  93. package/dist/modules/validation/ValidationConfigController.js.map +1 -1
  94. package/dist/modules/vcs/VcsConnectController.d.ts +9 -1
  95. package/dist/modules/vcs/VcsConnectController.d.ts.map +1 -1
  96. package/dist/modules/vcs/VcsConnectController.js +11 -3
  97. package/dist/modules/vcs/VcsConnectController.js.map +1 -1
  98. package/package.json +8 -8
@@ -1,8 +1,26 @@
1
- import { spliceManagedSection } from '@cat-factory/kernel';
1
+ import { noopLogger, runBestEffort, spliceManagedSection } from '@cat-factory/kernel';
2
+ import { allPullRequests } from '@cat-factory/contracts';
2
3
  import { githubRepoOrigin } from '../agents/containerAgentBody.js';
4
+ import { splitRepo } from './repoFullName.js';
3
5
  /**
4
- * Upserts the engine's verification report onto a block's pull request as a marker-delimited
5
- * section of the PR description (see `docs/initiatives/pr-verification-report.md`, decision
6
+ * The numeric connection handle the engine VCS client takes, recovered from a target's neutral
7
+ * {@link VcsConnectionRef}.
8
+ *
9
+ * Not kernel's `githubInstallationId`, which refuses a non-GitHub connection: this adapter serves
10
+ * a GitLab deployment too (through `vcsBackedGitHubClient`), and there the same numeric id is the
11
+ * workspace's `github_installations` row that holds its PAT. A non-numeric id is a wiring bug and
12
+ * throws, which the engine records as a failed publish rather than a run failure.
13
+ */
14
+ function connectionId(connection) {
15
+ const id = Number(connection.connectionId);
16
+ if (!Number.isInteger(id)) {
17
+ throw new Error(`PR report target carries an unusable connection id "${connection.connectionId}".`);
18
+ }
19
+ return id;
20
+ }
21
+ /**
22
+ * Upserts the engine's verification report onto a block's pull requests as a marker-delimited
23
+ * section of each PR description (see `docs/initiatives/pr-verification-report.md`, decision
6
24
  * D1: the markers ARE the section's identity, so the write is idempotent across re-runs,
7
25
  * retries and replayed durable steps with no persisted state at all).
8
26
  *
@@ -10,51 +28,135 @@ import { githubRepoOrigin } from '../agents/containerAgentBody.js';
10
28
  * as it is right now, so a human who edited the description while the run was in flight keeps
11
29
  * their edit. An unchanged body short-circuits before the remote write.
12
30
  *
13
- * Scoped to the block's OWN-service pull request. A multi-repo task's peer PRs are a phase-2
14
- * item on the initiative tracker reporting the own-service run's evidence onto a peer repo's
15
- * PR without per-repo composition would be misleading, so it is deliberately not done here.
31
+ * On a MULTI-REPO run (service-connections phase 3) the task opens one PR per repo it changed,
32
+ * and every one of them is a target: a reviewer on a connected service's PR is looking at part
33
+ * of this change and is as entitled to the run's evidence as one on the own-service PR. The
34
+ * engine composes a distinct report per target — the own-service-only sections are withheld
35
+ * from a peer's copy rather than copied onto it.
36
+ *
37
+ * **All addressing happens in {@link resolveTargets}, once.** Each target it returns carries the
38
+ * connection and repo its write needs, so `publish` reads no repository and a run with N pull
39
+ * requests costs one resolution per settlement rather than N. That also removes the question
40
+ * `publish` would otherwise have to answer on every call ("is this still the block's PR?") on a
41
+ * hook that fires on every settled step.
16
42
  */
17
43
  export class GitHubPrReportPublisher {
18
44
  deps;
19
45
  constructor(deps) {
20
46
  this.deps = deps;
21
47
  }
22
- async resolveTarget(workspaceId, blockId) {
23
- const resolved = await this.resolve(workspaceId, blockId);
24
- if (!resolved)
25
- return null;
26
- const { number, repo } = resolved;
27
- const origin = (this.deps.resolveRepoOrigin ?? githubRepoOrigin)(repo);
28
- return { prNumber: number, repo: `${repo.owner}/${repo.name}`, provider: origin.provider };
29
- }
30
- /** The PR number + repo a block's report targets, or null when either is unresolved. */
31
- async resolve(workspaceId, blockId) {
48
+ /**
49
+ * Every pull request of the block's run, own-service first, each addressed for writing.
50
+ *
51
+ * ONE block read and ONE repo resolution for the whole set: the own-service target is
52
+ * resolved first (it is needed regardless, and lets the multi-repo resolver skip re-walking
53
+ * the block's ancestry), then a single `resolveRepoTargets` call covers every peer. Resolving
54
+ * per recorded peer PR would be the N+1 this codebase bans.
55
+ */
56
+ async resolveTargets(workspaceId, blockId) {
32
57
  const block = await this.deps.blockRepository.get(workspaceId, blockId);
33
- const number = block?.pullRequest?.number;
34
- if (number == null)
35
- return null;
36
- const repo = await this.deps.resolveRepoTarget(workspaceId, blockId);
37
- return repo ? { number, repo } : null;
58
+ if (!block)
59
+ return [];
60
+ // `allPullRequests` is the single source of truth for "every PR this task opened", own
61
+ // first then peers — the same enumeration the CI gate aggregates over. The own-service
62
+ // entry is the one carrying no `repo` (its repo is the task's own service).
63
+ const prs = allPullRequests(block);
64
+ if (prs.length === 0)
65
+ return [];
66
+ const targets = [];
67
+ // The own-service PR FIRST when it exists: the engine reads the peer reports' back-pointer
68
+ // off the head of this list rather than re-resolving it. Matched on an ABSENT `repo` rather
69
+ // than a falsy one, and only when the block records an own PR at all: a peer whose recorded
70
+ // repo is the empty string is unaddressable, not the own-service one.
71
+ const own = block.pullRequest ? prs.find((pr) => pr.repo === undefined) : undefined;
72
+ const ownRepo = own ? await this.deps.resolveRepoTarget(workspaceId, blockId) : null;
73
+ if (own?.ref.number != null && ownRepo) {
74
+ targets.push(this.describe(ownRepo, own.ref.number, { role: 'own', url: own.ref.url }));
75
+ }
76
+ // BEST-EFFORT, and deliberately so: the multi-repo resolver THROWS on a workspace with no
77
+ // installation or an involved frame that lost its repo linkage, and letting that propagate
78
+ // would mean one broken peer linkage costs the OWN-SERVICE report too — the one a reviewer is
79
+ // most likely reading. The same "a failure on one target does not cost the others" rule the
80
+ // engine applies when publishing, applied one step earlier where the targets are found.
81
+ const peers = await runBestEffort(this.deps.logger ?? noopLogger, 'pr-report peer target resolution', () => this.peerTargets(workspaceId, blockId, block, prs, ownRepo), { workspaceId, blockId });
82
+ targets.push(...(peers ?? []));
83
+ return targets;
38
84
  }
39
- async publish(workspaceId, blockId, section) {
40
- // Resolved through the SAME helper as `resolveTarget`, so the two can never disagree about
41
- // which PR is "the block's". `publish` re-resolves rather than trusting a target handed in:
42
- // it is a port method other callers may reach directly, and the write must be self-contained.
43
- const block = await this.deps.blockRepository.get(workspaceId, blockId);
44
- const number = block?.pullRequest?.number;
45
- if (number == null)
46
- return { published: false, skipped: 'no_pull_request' };
47
- const repo = await this.deps.resolveRepoTarget(workspaceId, blockId);
48
- if (!repo)
49
- return { published: false, skipped: 'no_repo', prNumber: number };
50
- const ref = { owner: repo.owner, repo: repo.name };
85
+ /**
86
+ * The run's PEER pull requests, addressed through the multi-repo repo resolution.
87
+ *
88
+ * A recorded peer PR is only targetable when two things line up: it carries a `number` (the
89
+ * unit the description write addresses, absent on a ref that was only ever a URL) and its
90
+ * repo is in the resolved checkout set (which is what supplies the connection to write
91
+ * through, and is the platform's own answer about which repos this run touched). A peer that
92
+ * satisfies neither is skipped rather than guessed at — the repo name on a recorded peer PR
93
+ * is harness-reported, and writing a report onto a pull request we cannot confirm the identity
94
+ * of is the one failure mode worse than not writing it.
95
+ */
96
+ async peerTargets(workspaceId, blockId, block, prs, ownRepo) {
97
+ // Every entry that names a repo: the complement of the own-service one above.
98
+ const peers = prs.filter((pr) => pr.repo !== undefined);
99
+ const resolveAll = this.deps.resolveRepoTargets;
100
+ if (!peers.length || !resolveAll)
101
+ return [];
102
+ // The frames to resolve: the task's declared involved services, plus any frame a recorded
103
+ // peer PR attributes itself to. The union rather than either alone, because the declared
104
+ // list is what the run fanned out over while the recorded PRs are what it actually opened,
105
+ // and a peer whose frame left the task's involved list still has an open PR owed a report.
106
+ const frameIds = [
107
+ ...new Set([
108
+ ...(block.involvedServiceIds ?? []),
109
+ ...peers.map((p) => p.frameId).filter((id) => !!id),
110
+ ]),
111
+ ];
112
+ const resolved = await resolveAll(workspaceId, blockId, frameIds, ownRepo ?? undefined);
113
+ const byRepo = new Map(resolved.checkouts.map((c) => [`${c.target.owner}/${c.target.name}`, c.target]));
114
+ const out = [];
115
+ for (const peer of peers) {
116
+ const number = peer.ref.number;
117
+ const repo = peer.repo ? byRepo.get(peer.repo) : undefined;
118
+ if (number == null || !repo)
119
+ continue;
120
+ out.push(this.describe(repo, number, { role: 'peer', frameId: peer.frameId, url: peer.ref.url }));
121
+ }
122
+ return out;
123
+ }
124
+ /**
125
+ * A resolved repo + PR number as a self-describing target: which PR, in which repo, on which
126
+ * provider, through which connection.
127
+ *
128
+ * The connection names the DEPLOYMENT'S provider, not this class's name: a GitLab deployment
129
+ * reaches this adapter through `vcsBackedGitHubClient`, and a target that claimed `github`
130
+ * would mis-route the day a native GitLab publisher selects its adapter from it. Both providers
131
+ * ride the same numeric connection id, because a GitLab workspace's PAT connect reuses the
132
+ * `github_installations` row this id comes from.
133
+ */
134
+ describe(repo, prNumber, scope) {
135
+ const origin = (this.deps.resolveRepoOrigin ?? githubRepoOrigin)(repo);
136
+ return {
137
+ prNumber,
138
+ repo: `${repo.owner}/${repo.name}`,
139
+ provider: origin.provider,
140
+ connection: { provider: origin.provider, connectionId: String(repo.installationId) },
141
+ role: scope.role,
142
+ frameId: scope.frameId ?? null,
143
+ url: scope.url ?? null,
144
+ };
145
+ }
146
+ async publish(_workspaceId, target, section) {
147
+ // No resolution here at all: the target names its own pull request, repo and connection,
148
+ // and the caller composed `section` for exactly that. See the class doc.
149
+ const installationId = connectionId(target.connection);
150
+ const [owner, name] = splitRepo(target.repo);
151
+ const ref = { owner, repo: name };
152
+ const number = target.prNumber;
51
153
  const gh = this.deps.githubClient;
52
- const current = await gh.getPullRequestBody(repo.installationId, ref, number);
154
+ const current = await gh.getPullRequestBody(installationId, ref, number);
53
155
  const next = spliceManagedSection(current, section);
54
156
  if (next === (current ?? '')) {
55
157
  return { published: false, skipped: 'unchanged', prNumber: number };
56
158
  }
57
- await gh.updatePullRequest(repo.installationId, ref, number, { body: next });
159
+ await gh.updatePullRequest(installationId, ref, number, { body: next });
58
160
  return { published: true, prNumber: number };
59
161
  }
60
162
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GitHubPrReportPublisher.js","sourceRoot":"","sources":["../../src/github/GitHubPrReportPublisher.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"GitHubPrReportPublisher.js","sourceRoot":"","sources":["../../src/github/GitHubPrReportPublisher.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { type BlockEditActor } from '@cat-factory/contracts';
2
2
  import { type WorkspaceAccess, type WorkspacePermission } from '@cat-factory/kernel';
3
- import type { Context, MiddlewareHandler } from 'hono';
3
+ import type { Context, Hono } from 'hono';
4
4
  import type { AppEnv, ServerContainer } from './env.js';
5
5
  /**
6
6
  * Resolve a signed-in user's effective access to one board, THROUGH the `workspaceAccess` cache.
@@ -40,66 +40,91 @@ export declare function requirePermission<E extends AppEnv>(c: Context<E>, permi
40
40
  */
41
41
  export declare function blockEditActor<E extends AppEnv>(c: Context<E>): BlockEditActor;
42
42
  /**
43
- * Controller-level admin-tier gate (workspace-rbac, slice 6). Mount ONCE at the top of an
44
- * admin route group `app.use('*', requireWorkspacePermission('integrations.manage'))`
45
- * and every WRITE the controller serves (now and in the future) requires that permission,
46
- * with ZERO per-handler code. Reads (GET/HEAD) pass straight through, so the whole controller
47
- * stays viewer-readable (`workspace.read`, satisfied by the gate resolution) while its mutations
48
- * are admin-only.
43
+ * The controller-level admin-tier gate (workspace-rbac, slice 6): every WRITE served under one of
44
+ * `prefixes` requires `permission`, with ZERO per-handler code. Reads (GET/HEAD) pass straight
45
+ * through, so the controller stays viewer-readable (`workspace.read`, satisfied by the gate
46
+ * resolution) while its mutations are admin-only. Use
47
+ * {@link mountWorkspacePermissionIncludingReads} for the rare controller whose READ is the
48
+ * sensitive half.
49
49
  *
50
- * This is the method-shaped counterpart to the gate's viewer floor: the floor rejects viewer
51
- * writes wholesale (non-GET ⇒ ≥ member) in ONE place; this rejects member writes on the admin
52
- * groups in ONE place per group. Because it is co-located with the controller's mount (not a
53
- * central path→permission table), a new route inherits the correct gate automatically — the
54
- * drift a shadow route-map would suffer can't happen. It runs BEFORE the handler, so an
50
+ * This is the method-shaped counterpart to the gate's viewer floor: the floor rejects viewer writes
51
+ * wholesale (non-GET ⇒ ≥ member) in ONE place; this rejects member writes on the admin groups in
52
+ * ONE place per group. Because it is co-located with the controller's own mount (not a central
53
+ * path→permission table), a new route under a gated prefix inherits the correct gate automatically:
54
+ * the drift a shadow route-map would suffer can't happen. It runs BEFORE the handler, so an
55
55
  * unauthorized caller is refused even when the underlying integration is unwired (a member never
56
- * learns whether Slack/GitHub/etc. is configured).
56
+ * learns whether Slack/GitHub/etc. is configured), and before body validation, so a malformed
57
+ * payload cannot turn a refusal into a 422 that answers the same question.
57
58
  *
58
59
  * `OPTIONS` (CORS preflight) is never gated. Where a controller mixes gated and ungated writes
59
- * under one mount (e.g. workspace create vs rename), call {@link requirePermission} per-handler
60
+ * under one prefix (e.g. workspace create vs rename), call {@link requirePermission} per-handler
60
61
  * instead of mounting this.
61
62
  *
62
- * ⚠️ **A `'*'` mount is NOT scoped to the controller.** `app.route('/workspaces/:workspaceId', sub)`
63
- * re-registers each of `sub`'s entries under that prefix, and a `sub.use('*', …)` becomes
64
- * `ALL /workspaces/:workspaceId/*` on the shared app — so it also matches routes OTHER controllers
65
- * mounted on the same prefix. Hono then runs whichever matching entry was registered first, which
66
- * makes the blast radius depend on the order in `app.ts`: today the run controllers are registered
67
- * before the config ones, so their routes win and the existing `'*'` mounts reach only the config
68
- * controllers listed after them (all admin-tier, which is why nothing has surfaced). Prefer mounting
69
- * on the controller's OWN path patterns (`/thing` and `/thing/*`), which cannot reach a sibling
70
- * whatever the order as {@link requireWorkspacePermissionIncludingReads}'s two call sites do,
71
- * because for them the ordering accident stopped being harmless.
63
+ * **`prefixes` are the controller's OWN top-level paths, and taking them is the whole point of this
64
+ * helper.** `app.route('/workspaces/:workspaceId', sub)` re-registers each of `sub`'s entries under
65
+ * that prefix, so a `sub.use('*', …)` becomes `ALL /workspaces/:workspaceId/*` on the shared app and
66
+ * matches routes OTHER controllers mounted on the same prefix. Hono runs a matching middleware for
67
+ * every route registered after it, which made the blast radius depend on the order in `app.ts`: the
68
+ * admin document and task-source controllers are registered ahead of the human-gate controllers, so
69
+ * their `'*'` mounts refused a plain member's review decisions, requirement answers and initiative
70
+ * writes with `integrations.manage`. Nobody noticed because an account admin resolves as a workspace
71
+ * admin, and dogfooding happens as one. Passing prefixes cannot reach a sibling whatever the order,
72
+ * and it is why no middleware factory is exported from this module: a `'*'` mount is now
73
+ * unrepresentable rather than merely discouraged. `permissionMounts.test.ts` pins the invariant
74
+ * (a member is refused exactly the writes their own controller gates).
75
+ *
76
+ * Each prefix is mounted twice, bare and `/*`, because Hono's `*` does not match the bare prefix.
77
+ * A prefix may carry params (`/services/:blockId/test-secrets`), which is how a controller whose
78
+ * routes hang off a shared first segment gates its own without claiming the segment.
72
79
  */
73
- export declare function requireWorkspacePermission<E extends AppEnv>(permission: WorkspacePermission): MiddlewareHandler<E>;
80
+ export declare function mountWorkspacePermission<E extends AppEnv>(app: Hono<E>, permission: WorkspacePermission, prefixes: readonly string[]): void;
74
81
  /**
75
82
  * The same controller-level gate, applied to READS as well.
76
83
  *
77
- * {@link requireWorkspacePermission} lets GET/HEAD through by design: on almost every admin
84
+ * {@link mountWorkspacePermission} lets GET/HEAD through by design: on almost every admin
78
85
  * controller the permission guards MUTATION and the configuration itself is viewer-readable. A few
79
- * controllers invert that, because their READ is the sensitive half the capability-credential
86
+ * controllers invert that, because their READ is the sensitive half. The capability-credential
80
87
  * checklist and the tool-server inventory both project the credential KEY NAMES this deployment's
81
88
  * capabilities want, which is exactly what the workspace snapshot withholds from a viewer ("no
82
89
  * business learning which environment variables the deployment sets"). Moving the value into a
83
90
  * sealed row did not change that judgement, and neither did adding a second surface over the same
84
- * registry.
91
+ * registry. `vcsConnectController` is the third and the plainest: it serves ONE read and no write,
92
+ * so the writes-only mount left it enforcing nothing at all.
85
93
  *
86
94
  * A separate function rather than an option, so the choice is legible at the mount and a controller
87
95
  * cannot acquire it by a default changing underneath it. `OPTIONS` still passes: a CORS preflight
88
96
  * carries no credentials to judge, and refusing it breaks the browser before the real request that
89
97
  * this gate is here to refuse.
90
98
  *
91
- * The failure this exists to prevent is not hypothetical. Both call sites documented the read as
92
- * gated (in the controller, in the SPA's tab gate, in the store's 403 handling) while the mount let
93
- * every member's GET through, because "gated on `secrets.manage`" reads as covering the controller
94
- * and covered only its writes.
99
+ * The failure this exists to prevent is not hypothetical, and all three call sites hit it: each
100
+ * documented the read as gated (in the controller, in the SPA's tab gate, in the store's 403
101
+ * handling) while the mount let every member's GET through, because "gated on `secrets.manage`"
102
+ * reads as covering the controller and covered only its writes. `permissionMounts.test.ts` now
103
+ * asserts the READS of an `IncludingReads` controller are covered too, so the gap cannot reopen
104
+ * by a prefix going missing.
105
+ */
106
+ export declare function mountWorkspacePermissionIncludingReads<E extends AppEnv>(app: Hono<E>, permission: WorkspacePermission, prefixes: readonly string[]): void;
107
+ /** What one mounted gate enforces: the permission, and whether it covers READS as well. */
108
+ export interface MountedGate {
109
+ permission: WorkspacePermission;
110
+ /** True for {@link mountWorkspacePermissionIncludingReads}, false for the writes-only mount. */
111
+ gatesReads: boolean;
112
+ }
113
+ /**
114
+ * What a mounted gate enforces, or `undefined` for any other middleware.
115
+ *
116
+ * Read off a controller's route table by `permissionMounts.test.ts`, which asserts that a member is
117
+ * refused exactly the writes their own controller gates AND that a gated controller leaves none of
118
+ * its own routes uncovered. It exists because a controller's middleware is not necessarily a
119
+ * permission gate: `executionController` and `notificationController` both mount their own `ALL`
120
+ * middleware, and treating every middleware entry as a gate made the derived expectation wrong for
121
+ * them. Tagging the handler is what lets the invariant be READ from what the mount actually did
122
+ * instead of from a second list somebody has to keep in step.
95
123
  *
96
- * ⚠️ **Mount this on the controller's OWN path patterns, never `'*'`.** See the warning on
97
- * {@link requireWorkspacePermission}: a `'*'` mount lands on `/workspaces/:workspaceId/*` and can
98
- * refuse a SIBLING controller's routes. That is survivable while only writes are gated (the
99
- * siblings it can reach are admin-tier anyway); gating READS makes it fatal, because the same
100
- * pattern then refuses every member-readable GET registered after it — `GET /github/repos` was the
101
- * one that caught it. Two patterns are needed, `/thing` and `/thing/*`: Hono's `*` does not match
102
- * the bare prefix.
124
+ * Read it off a STANDALONE-built controller, not the composed app: `app.route()` passes a sub-app's
125
+ * handlers through untouched only while the sub-app has no error handler of its own, and wraps them
126
+ * (losing this tag) the moment one calls `app.onError`. No controller does today, and the invariant
127
+ * does not depend on none ever doing.
103
128
  */
104
- export declare function requireWorkspacePermissionIncludingReads<E extends AppEnv>(permission: WorkspacePermission): MiddlewareHandler<E>;
129
+ export declare function permissionGateOf(handler: unknown): MountedGate | undefined;
105
130
  //# sourceMappingURL=workspaceAccess.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspaceAccess.d.ts","sourceRoot":"","sources":["../../src/http/workspaceAccess.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AACvF,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAiBvD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAKjC;AA6BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAChD,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EACb,UAAU,EAAE,mBAAmB,GAC9B,IAAI,CASN;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,cAAc,CAI9E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,SAAS,MAAM,EACzD,UAAU,EAAE,mBAAmB,GAC9B,iBAAiB,CAAC,CAAC,CAAC,CAQtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,wCAAwC,CAAC,CAAC,SAAS,MAAM,EACvE,UAAU,EAAE,mBAAmB,GAC9B,iBAAiB,CAAC,CAAC,CAAC,CAKtB"}
1
+ {"version":3,"file":"workspaceAccess.d.ts","sourceRoot":"","sources":["../../src/http/workspaceAccess.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AACvF,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAqB,MAAM,MAAM,CAAA;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAiBvD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,eAAe,EAC1B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAKjC;AA6BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAChD,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EACb,UAAU,EAAE,mBAAmB,GAC9B,IAAI,CASN;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,cAAc,CAI9E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,MAAM,EACvD,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,EACZ,UAAU,EAAE,mBAAmB,EAC/B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,IAAI,CAQN;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sCAAsC,CAAC,CAAC,SAAS,MAAM,EACrE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,EACZ,UAAU,EAAE,mBAAmB,EAC/B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,IAAI,CAKN;AAID,2FAA2F;AAC3F,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,mBAAmB,CAAA;IAC/B,gGAAgG;IAChG,UAAU,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAI1E"}
@@ -93,80 +93,120 @@ export function blockEditActor(c) {
93
93
  return { role: access.role, managesPolicy: access.permissions.has('settings.manage') };
94
94
  }
95
95
  /**
96
- * Controller-level admin-tier gate (workspace-rbac, slice 6). Mount ONCE at the top of an
97
- * admin route group `app.use('*', requireWorkspacePermission('integrations.manage'))`
98
- * and every WRITE the controller serves (now and in the future) requires that permission,
99
- * with ZERO per-handler code. Reads (GET/HEAD) pass straight through, so the whole controller
100
- * stays viewer-readable (`workspace.read`, satisfied by the gate resolution) while its mutations
101
- * are admin-only.
96
+ * The controller-level admin-tier gate (workspace-rbac, slice 6): every WRITE served under one of
97
+ * `prefixes` requires `permission`, with ZERO per-handler code. Reads (GET/HEAD) pass straight
98
+ * through, so the controller stays viewer-readable (`workspace.read`, satisfied by the gate
99
+ * resolution) while its mutations are admin-only. Use
100
+ * {@link mountWorkspacePermissionIncludingReads} for the rare controller whose READ is the
101
+ * sensitive half.
102
102
  *
103
- * This is the method-shaped counterpart to the gate's viewer floor: the floor rejects viewer
104
- * writes wholesale (non-GET ⇒ ≥ member) in ONE place; this rejects member writes on the admin
105
- * groups in ONE place per group. Because it is co-located with the controller's mount (not a
106
- * central path→permission table), a new route inherits the correct gate automatically — the
107
- * drift a shadow route-map would suffer can't happen. It runs BEFORE the handler, so an
103
+ * This is the method-shaped counterpart to the gate's viewer floor: the floor rejects viewer writes
104
+ * wholesale (non-GET ⇒ ≥ member) in ONE place; this rejects member writes on the admin groups in
105
+ * ONE place per group. Because it is co-located with the controller's own mount (not a central
106
+ * path→permission table), a new route under a gated prefix inherits the correct gate automatically:
107
+ * the drift a shadow route-map would suffer can't happen. It runs BEFORE the handler, so an
108
108
  * unauthorized caller is refused even when the underlying integration is unwired (a member never
109
- * learns whether Slack/GitHub/etc. is configured).
109
+ * learns whether Slack/GitHub/etc. is configured), and before body validation, so a malformed
110
+ * payload cannot turn a refusal into a 422 that answers the same question.
110
111
  *
111
112
  * `OPTIONS` (CORS preflight) is never gated. Where a controller mixes gated and ungated writes
112
- * under one mount (e.g. workspace create vs rename), call {@link requirePermission} per-handler
113
+ * under one prefix (e.g. workspace create vs rename), call {@link requirePermission} per-handler
113
114
  * instead of mounting this.
114
115
  *
115
- * ⚠️ **A `'*'` mount is NOT scoped to the controller.** `app.route('/workspaces/:workspaceId', sub)`
116
- * re-registers each of `sub`'s entries under that prefix, and a `sub.use('*', …)` becomes
117
- * `ALL /workspaces/:workspaceId/*` on the shared app — so it also matches routes OTHER controllers
118
- * mounted on the same prefix. Hono then runs whichever matching entry was registered first, which
119
- * makes the blast radius depend on the order in `app.ts`: today the run controllers are registered
120
- * before the config ones, so their routes win and the existing `'*'` mounts reach only the config
121
- * controllers listed after them (all admin-tier, which is why nothing has surfaced). Prefer mounting
122
- * on the controller's OWN path patterns (`/thing` and `/thing/*`), which cannot reach a sibling
123
- * whatever the order as {@link requireWorkspacePermissionIncludingReads}'s two call sites do,
124
- * because for them the ordering accident stopped being harmless.
116
+ * **`prefixes` are the controller's OWN top-level paths, and taking them is the whole point of this
117
+ * helper.** `app.route('/workspaces/:workspaceId', sub)` re-registers each of `sub`'s entries under
118
+ * that prefix, so a `sub.use('*', …)` becomes `ALL /workspaces/:workspaceId/*` on the shared app and
119
+ * matches routes OTHER controllers mounted on the same prefix. Hono runs a matching middleware for
120
+ * every route registered after it, which made the blast radius depend on the order in `app.ts`: the
121
+ * admin document and task-source controllers are registered ahead of the human-gate controllers, so
122
+ * their `'*'` mounts refused a plain member's review decisions, requirement answers and initiative
123
+ * writes with `integrations.manage`. Nobody noticed because an account admin resolves as a workspace
124
+ * admin, and dogfooding happens as one. Passing prefixes cannot reach a sibling whatever the order,
125
+ * and it is why no middleware factory is exported from this module: a `'*'` mount is now
126
+ * unrepresentable rather than merely discouraged. `permissionMounts.test.ts` pins the invariant
127
+ * (a member is refused exactly the writes their own controller gates).
128
+ *
129
+ * Each prefix is mounted twice, bare and `/*`, because Hono's `*` does not match the bare prefix.
130
+ * A prefix may carry params (`/services/:blockId/test-secrets`), which is how a controller whose
131
+ * routes hang off a shared first segment gates its own without claiming the segment.
125
132
  */
126
- export function requireWorkspacePermission(permission) {
127
- return (c, next) => {
133
+ export function mountWorkspacePermission(app, permission, prefixes) {
134
+ mountGate(app, prefixes, { permission, gatesReads: false }, (c, next) => {
128
135
  const method = c.req.method;
129
136
  if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS') {
130
137
  requirePermission(c, permission);
131
138
  }
132
139
  return next();
133
- };
140
+ });
134
141
  }
135
142
  /**
136
143
  * The same controller-level gate, applied to READS as well.
137
144
  *
138
- * {@link requireWorkspacePermission} lets GET/HEAD through by design: on almost every admin
145
+ * {@link mountWorkspacePermission} lets GET/HEAD through by design: on almost every admin
139
146
  * controller the permission guards MUTATION and the configuration itself is viewer-readable. A few
140
- * controllers invert that, because their READ is the sensitive half the capability-credential
147
+ * controllers invert that, because their READ is the sensitive half. The capability-credential
141
148
  * checklist and the tool-server inventory both project the credential KEY NAMES this deployment's
142
149
  * capabilities want, which is exactly what the workspace snapshot withholds from a viewer ("no
143
150
  * business learning which environment variables the deployment sets"). Moving the value into a
144
151
  * sealed row did not change that judgement, and neither did adding a second surface over the same
145
- * registry.
152
+ * registry. `vcsConnectController` is the third and the plainest: it serves ONE read and no write,
153
+ * so the writes-only mount left it enforcing nothing at all.
146
154
  *
147
155
  * A separate function rather than an option, so the choice is legible at the mount and a controller
148
156
  * cannot acquire it by a default changing underneath it. `OPTIONS` still passes: a CORS preflight
149
157
  * carries no credentials to judge, and refusing it breaks the browser before the real request that
150
158
  * this gate is here to refuse.
151
159
  *
152
- * The failure this exists to prevent is not hypothetical. Both call sites documented the read as
153
- * gated (in the controller, in the SPA's tab gate, in the store's 403 handling) while the mount let
154
- * every member's GET through, because "gated on `secrets.manage`" reads as covering the controller
155
- * and covered only its writes.
156
- *
157
- * ⚠️ **Mount this on the controller's OWN path patterns, never `'*'`.** See the warning on
158
- * {@link requireWorkspacePermission}: a `'*'` mount lands on `/workspaces/:workspaceId/*` and can
159
- * refuse a SIBLING controller's routes. That is survivable while only writes are gated (the
160
- * siblings it can reach are admin-tier anyway); gating READS makes it fatal, because the same
161
- * pattern then refuses every member-readable GET registered after it — `GET /github/repos` was the
162
- * one that caught it. Two patterns are needed, `/thing` and `/thing/*`: Hono's `*` does not match
163
- * the bare prefix.
160
+ * The failure this exists to prevent is not hypothetical, and all three call sites hit it: each
161
+ * documented the read as gated (in the controller, in the SPA's tab gate, in the store's 403
162
+ * handling) while the mount let every member's GET through, because "gated on `secrets.manage`"
163
+ * reads as covering the controller and covered only its writes. `permissionMounts.test.ts` now
164
+ * asserts the READS of an `IncludingReads` controller are covered too, so the gap cannot reopen
165
+ * by a prefix going missing.
164
166
  */
165
- export function requireWorkspacePermissionIncludingReads(permission) {
166
- return (c, next) => {
167
+ export function mountWorkspacePermissionIncludingReads(app, permission, prefixes) {
168
+ mountGate(app, prefixes, { permission, gatesReads: true }, (c, next) => {
167
169
  if (c.req.method !== 'OPTIONS')
168
170
  requirePermission(c, permission);
169
171
  return next();
170
- };
172
+ });
173
+ }
174
+ const MOUNTED_GATE = Symbol('workspacePermissionGate');
175
+ /**
176
+ * What a mounted gate enforces, or `undefined` for any other middleware.
177
+ *
178
+ * Read off a controller's route table by `permissionMounts.test.ts`, which asserts that a member is
179
+ * refused exactly the writes their own controller gates AND that a gated controller leaves none of
180
+ * its own routes uncovered. It exists because a controller's middleware is not necessarily a
181
+ * permission gate: `executionController` and `notificationController` both mount their own `ALL`
182
+ * middleware, and treating every middleware entry as a gate made the derived expectation wrong for
183
+ * them. Tagging the handler is what lets the invariant be READ from what the mount actually did
184
+ * instead of from a second list somebody has to keep in step.
185
+ *
186
+ * Read it off a STANDALONE-built controller, not the composed app: `app.route()` passes a sub-app's
187
+ * handlers through untouched only while the sub-app has no error handler of its own, and wraps them
188
+ * (losing this tag) the moment one calls `app.onError`. No controller does today, and the invariant
189
+ * does not depend on none ever doing.
190
+ */
191
+ export function permissionGateOf(handler) {
192
+ if (typeof handler !== 'function')
193
+ return undefined;
194
+ const tagged = handler[MOUNTED_GATE];
195
+ return typeof tagged === 'object' && tagged !== null ? tagged : undefined;
196
+ }
197
+ /** Mount one gate on each prefix, bare and `/*` (Hono's `*` does not match the bare prefix). */
198
+ function mountGate(app, prefixes, mounted, gate) {
199
+ if (prefixes.length === 0) {
200
+ // A gated controller with no prefix would gate nothing while reading as gated, which is the
201
+ // silent hole this whole helper exists to close. A prefix that matches none of the controller's
202
+ // routes is the same hole one step later, so `permissionMounts.test.ts` refuses that too: this
203
+ // throw can only catch the spelling of it that names no path at all.
204
+ throw new Error('mountWorkspacePermission: at least one path prefix is required');
205
+ }
206
+ Object.defineProperty(gate, MOUNTED_GATE, { value: mounted });
207
+ for (const prefix of prefixes) {
208
+ app.use(prefix, gate);
209
+ app.use(`${prefix}/*`, gate);
210
+ }
171
211
  }
172
212
  //# sourceMappingURL=workspaceAccess.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspaceAccess.js","sourceRoot":"","sources":["../../src/http/workspaceAccess.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"workspaceAccess.js","sourceRoot":"","sources":["../../src/http/workspaceAccess.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@ import { getAgentPromptContract, listAgentPromptsContract, promoteAgentPromptCon
2
2
  import { promptIdForKind, promptVersionLabel } from '@cat-factory/agents';
3
3
  import { buildHonoRoute } from '@toad-contracts/hono';
4
4
  import { Hono } from 'hono';
5
- import { requireWorkspacePermission } from '../../http/workspaceAccess.js';
5
+ import { mountWorkspacePermission } from '../../http/workspaceAccess.js';
6
6
  import { param } from '../../http/params.js';
7
7
  import { requireCapability } from '../../http/guards.js';
8
8
  import { NotFoundError, ValidationError } from '@cat-factory/kernel';
@@ -56,7 +56,7 @@ function detailFor(c, agentKind, revisions) {
56
56
  */
57
57
  export function agentPromptController() {
58
58
  const app = new Hono();
59
- app.use('*', requireWorkspacePermission('settings.manage'));
59
+ mountWorkspacePermission(app, 'settings.manage', ['/agent-prompts']);
60
60
  buildHonoRoute(app, listAgentPromptsContract, async (c) => {
61
61
  const prompts = requireAgentPrompts(c);
62
62
  return c.json(await prompts.service.listSummaries(param(c, 'workspaceId')), 200);