@integrity-labs/cloud-broker 0.7.1 → 0.7.4
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/README.md +136 -3
- package/dist/index.js +568 -10
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# `@integrity-labs/cloud-broker`
|
|
2
2
|
|
|
3
|
-
MCP server for the Augmented ephemeral cloud-access broker. Exposes
|
|
3
|
+
MCP server for the Augmented ephemeral cloud-access broker. Exposes per-cloud tool families that mint, poll, and release scoped, TTL-bounded credentials for a single task.
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
- **AWS** (STS `AssumeRole` under the hood — `aws_*` tools, paired with the `aws-cli` toolkit or any AWS SDK).
|
|
6
|
+
- **GCP** (IAM Credentials `generateAccessToken` under the hood — `gcp_*` tools, paired with the `gcloud` toolkit). Added in ENG-5074.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
All tools are namespaced per cloud so additions are purely additive (ENG-4782). Azure / Cloudflare slots are reserved (`azure_*` / `cf_*`) but unimplemented.
|
|
9
|
+
|
|
10
|
+
See the PRDs ([AWS](../../docs/prds/aws-ephemeral-access.md), [GCP](../../docs/prds/gcp-ephemeral-access.md)) and toolkit docs ([aws](../../docs/toolkits/aws.md), [gcloud](../../docs/toolkits/gcloud.md)) for the full design.
|
|
8
11
|
|
|
9
12
|
## Tools
|
|
10
13
|
|
|
14
|
+
### AWS
|
|
15
|
+
|
|
11
16
|
| Tool | Purpose |
|
|
12
17
|
|---|---|
|
|
13
18
|
| `aws_describe_scope` | Returns the team's resolved policy ceiling for an account — what the agent is allowed to ask for. Free, idempotent. |
|
|
@@ -16,6 +21,38 @@ See the [PRD](../../docs/prds/aws-ephemeral-access.md) and the [toolkit doc](../
|
|
|
16
21
|
| `aws_poll_grant` | Single-shot status check. Escape hatch — the broker pushes resolution via direct-chat. |
|
|
17
22
|
| `aws_get_credentials` | Fetch the AWS_* values for an active grant. Call after `aws_request_access` returns active or after the resolution-notification arrives. |
|
|
18
23
|
| `aws_release_access` | Voluntarily release a grant before TTL. Idempotent. |
|
|
24
|
+
| `aws_check_approval_channel` | Pre-flight Slack channel reachability check (ENG-4824). |
|
|
25
|
+
| `aws_list_accounts` | Live inventory of enrolled AWS accounts (ENG-5048). |
|
|
26
|
+
|
|
27
|
+
### GCP
|
|
28
|
+
|
|
29
|
+
| Tool | Purpose |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `gcp_describe_scope` | Resolved policy ceiling for a GCP project. Free, idempotent. |
|
|
32
|
+
| `gcp_preview_request` | Dry-run a candidate request. Writes nothing. |
|
|
33
|
+
| `gcp_request_access` | Mint or queue a grant. |
|
|
34
|
+
| `gcp_poll_grant` | Single-shot status check. Escape hatch. |
|
|
35
|
+
| `gcp_get_credentials` | Fetch the GCP access token for an active grant. |
|
|
36
|
+
| `gcp_release_access` | Voluntarily release a grant before TTL. Idempotent. |
|
|
37
|
+
| `gcp_check_approval_channel` | Pre-flight Slack channel reachability for the project's approval channel. |
|
|
38
|
+
| `gcp_list_accounts` | Live inventory of enrolled GCP projects. |
|
|
39
|
+
|
|
40
|
+
### Supabase
|
|
41
|
+
|
|
42
|
+
Credentials returned are a short-lived **Supabase JWT** (+ `supabase_url`, `db_host`, `project_ref`), not a cloud IAM token. The agent uses it for PostgREST (`Authorization: Bearer …`) or `psql` via the session pooler.
|
|
43
|
+
|
|
44
|
+
| Tool | Purpose |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `supabase_describe_scope` | Resolved policy ceiling for a Supabase project (auto-approvable roles, schema ceiling, write gating). Free, idempotent. |
|
|
47
|
+
| `supabase_preview_request` | Dry-run a candidate request. Writes nothing. |
|
|
48
|
+
| `supabase_request_access` | Mint or queue a grant. `service_role` always routes to a human approver; privileged internal roles (`postgres`, `supabase_admin`, …) are hard-denied. |
|
|
49
|
+
| `supabase_poll_grant` | Single-shot status check. Escape hatch. |
|
|
50
|
+
| `supabase_get_credentials` | Fetch the JWT + connection details for an active grant. |
|
|
51
|
+
| `supabase_release_access` | Voluntarily release a grant before TTL. Idempotent. **Note:** the JWT is stateless — release marks the grant revoked but the token stays valid until its `exp`. |
|
|
52
|
+
| `supabase_check_approval_channel` | Pre-flight Slack channel reachability for the project's approval channel. |
|
|
53
|
+
| `supabase_list_projects` | Live inventory of enrolled Supabase projects. |
|
|
54
|
+
|
|
55
|
+
See **[`docs/runbooks/supabase-broker-enrollment.md`](../../docs/runbooks/supabase-broker-enrollment.md)** for operator enrollment, the `augmented` JWT-claim → RLS-policy enforcement guide, and the policy-ceiling shape.
|
|
19
56
|
|
|
20
57
|
## Environment
|
|
21
58
|
|
|
@@ -69,6 +106,102 @@ aws_release_access({ grant_id: "..." })
|
|
|
69
106
|
|
|
70
107
|
If `aws_request_access` returns `status: "pending"`, a human approver was paged. The broker pushes the resolution to you via direct-chat — save the `grant_id`, return control, and resume when the inbound message arrives. On `denied`, the `denial_reason` field explains why. `aws_poll_grant` is an escape hatch for explicit re-checks.
|
|
71
108
|
|
|
109
|
+
## Worked example (GCP)
|
|
110
|
+
|
|
111
|
+
An agent reading one Cloud Storage object:
|
|
112
|
+
|
|
113
|
+
```jsonc
|
|
114
|
+
// 1. Inspect the envelope.
|
|
115
|
+
gcp_describe_scope({ project_id: "my-gcp-project" })
|
|
116
|
+
|
|
117
|
+
// 2. Dry-run.
|
|
118
|
+
gcp_preview_request({
|
|
119
|
+
project_id: "my-gcp-project",
|
|
120
|
+
permissions: ["storage.objects.get", "storage.objects.list"],
|
|
121
|
+
resources: ["projects/_/buckets/reports/objects/*", "projects/_/buckets/reports"],
|
|
122
|
+
ttl_seconds: 900
|
|
123
|
+
})
|
|
124
|
+
// → { "would": "auto_approve", "reason": null }
|
|
125
|
+
|
|
126
|
+
// 3. Mint.
|
|
127
|
+
gcp_request_access({
|
|
128
|
+
project_id: "my-gcp-project",
|
|
129
|
+
permissions: ["storage.objects.get", "storage.objects.list"],
|
|
130
|
+
resources: ["projects/_/buckets/reports/objects/*", "projects/_/buckets/reports"],
|
|
131
|
+
ttl_seconds: 900,
|
|
132
|
+
reason: "fetch the daily reports CSV for the user"
|
|
133
|
+
})
|
|
134
|
+
// → { "grant_id": "...", "status": "active", "secret_ref": "secret_ref://gcp/runs/<run_id>/<grant_id>", "expires_at": "..." }
|
|
135
|
+
|
|
136
|
+
// 4. Use the credentials. The runtime adapter resolves the secret_ref
|
|
137
|
+
// pointer to env vars at tool-call boundary — never substitute the
|
|
138
|
+
// raw token into shell history or logs.
|
|
139
|
+
// Inside an agent-spawned shell: gcloud storage cp gs://reports/today.csv -
|
|
140
|
+
// (CLOUDSDK_AUTH_ACCESS_TOKEN / GOOGLE_OAUTH_ACCESS_TOKEN are populated by
|
|
141
|
+
// the runtime; gcloud, gsutil, and bq pick them up automatically.)
|
|
142
|
+
//
|
|
143
|
+
// If you genuinely need the raw token (e.g. for non-gcloud HTTP calls),
|
|
144
|
+
// gcp_get_credentials({ grant_id: "..." }) returns it inline. The runtime
|
|
145
|
+
// MUST NOT echo the token to shell history; use it via a transient env
|
|
146
|
+
// only.
|
|
147
|
+
|
|
148
|
+
// 5. (Optional) release early.
|
|
149
|
+
gcp_release_access({ grant_id: "..." })
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Same `pending` / direct-chat-push resolution semantics as AWS. The runtime resolves `secret_ref://gcp/runs/...` to `CLOUDSDK_AUTH_ACCESS_TOKEN` and `GOOGLE_OAUTH_ACCESS_TOKEN` at exec time, so `gcloud`, `gsutil`, and `bq` all pick it up automatically without the agent ever handling the raw token.
|
|
153
|
+
|
|
154
|
+
## Worked example (Supabase)
|
|
155
|
+
|
|
156
|
+
An agent that needs read-only access to two tables in `public`:
|
|
157
|
+
|
|
158
|
+
```jsonc
|
|
159
|
+
// 1. Inspect the envelope — which roles auto-approve, the schema ceiling, write gating.
|
|
160
|
+
supabase_describe_scope({ project_ref: "abcdefghijklmnopqrst" })
|
|
161
|
+
|
|
162
|
+
// 2. Dry-run.
|
|
163
|
+
supabase_preview_request({
|
|
164
|
+
project_ref: "abcdefghijklmnopqrst",
|
|
165
|
+
role: "authenticated",
|
|
166
|
+
allowed_schemas: ["public"],
|
|
167
|
+
allowed_tables: ["public.agents", "public.runs"], // optional — absent = all tables in allowed_schemas
|
|
168
|
+
read_only: true,
|
|
169
|
+
ttl_seconds: 900
|
|
170
|
+
})
|
|
171
|
+
// → { "would": "auto_approve", "reason": null }
|
|
172
|
+
|
|
173
|
+
// 3. Mint.
|
|
174
|
+
supabase_request_access({
|
|
175
|
+
project_ref: "abcdefghijklmnopqrst",
|
|
176
|
+
role: "authenticated",
|
|
177
|
+
allowed_schemas: ["public"],
|
|
178
|
+
allowed_tables: ["public.agents", "public.runs"],
|
|
179
|
+
read_only: true,
|
|
180
|
+
ttl_seconds: 900,
|
|
181
|
+
reason: "read the agents + runs tables to answer the user's status question"
|
|
182
|
+
})
|
|
183
|
+
// → { "grant_id": "...", "status": "active", "secret_ref": "supabase:jwt:<grant_id>", "expires_at": "..." }
|
|
184
|
+
// `service_role` or write access (read_only:false, unless the project allows writes)
|
|
185
|
+
// returns "pending" instead and pages a human approver.
|
|
186
|
+
|
|
187
|
+
// 4. Fetch the JWT + connection details.
|
|
188
|
+
supabase_get_credentials({ grant_id: "..." })
|
|
189
|
+
// → { "grant_id": "...", "expires_at": "...",
|
|
190
|
+
// "credentials": { "access_token": "<JWT>", "supabase_url": "https://<ref>.supabase.co",
|
|
191
|
+
// "db_host": "aws-0-<region>.pooler.supabase.com", "project_ref": "<ref>" } }
|
|
192
|
+
//
|
|
193
|
+
// REST (PostgREST):
|
|
194
|
+
// curl "$supabase_url/rest/v1/agents?select=*" \
|
|
195
|
+
// -H "apikey: <JWT>" -H "Authorization: Bearer <JWT>"
|
|
196
|
+
// psql (session pooler):
|
|
197
|
+
// psql "postgresql://postgres.<project_ref>:<JWT>@<db_host>:5432/postgres"
|
|
198
|
+
|
|
199
|
+
// 5. (Optional) release early.
|
|
200
|
+
supabase_release_access({ grant_id: "..." })
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The grant's scope (`allowed_schemas` / `allowed_tables` / `read_only`) is carried in the JWT's `augmented` claim and is **only enforced if the project's RLS policies read that claim** — see the [enrollment runbook](../../docs/runbooks/supabase-broker-enrollment.md#enforcing-the-grant-scope-with-rls) for the policy patterns. Same `pending` / direct-chat-push resolution semantics as AWS.
|
|
204
|
+
|
|
72
205
|
## Running locally
|
|
73
206
|
|
|
74
207
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -21182,6 +21182,160 @@ var BrokerClient = class {
|
|
|
21182
21182
|
{ query: this.agentId ? { agent_id: this.agentId } : void 0 }
|
|
21183
21183
|
);
|
|
21184
21184
|
}
|
|
21185
|
+
// ──────────────────────────────── GCP (ENG-5077) ────────────────────────
|
|
21186
|
+
// Parallel surface to the AWS methods above. Same JWT, same agent_id /
|
|
21187
|
+
// run_id auto-fill, same error envelope. Hits /gcp/* endpoints on the
|
|
21188
|
+
// broker API. Grant_id is provider-agnostic — `gcp_get_credentials`
|
|
21189
|
+
// returns a GCP access_token instead of an AWS triple, but the rest of
|
|
21190
|
+
// the lifecycle (poll, release, status) is identical in shape.
|
|
21191
|
+
gcpDescribeScope(args) {
|
|
21192
|
+
if (!this.agentId) {
|
|
21193
|
+
throw makeBrokerError(400, "BrokerClient.gcpDescribeScope requires agentId \u2014 pass it in BrokerClientConfig");
|
|
21194
|
+
}
|
|
21195
|
+
return this.request(
|
|
21196
|
+
"GET",
|
|
21197
|
+
"/gcp/scope",
|
|
21198
|
+
{ query: { project_id: args.project_id, agent_id: this.agentId } }
|
|
21199
|
+
);
|
|
21200
|
+
}
|
|
21201
|
+
gcpPreviewRequest(args) {
|
|
21202
|
+
if (!this.agentId) {
|
|
21203
|
+
throw makeBrokerError(400, "BrokerClient.gcpPreviewRequest requires agentId \u2014 pass it in BrokerClientConfig");
|
|
21204
|
+
}
|
|
21205
|
+
const body = { ...args };
|
|
21206
|
+
if (body.agent_id === void 0) body.agent_id = this.agentId;
|
|
21207
|
+
return this.request(
|
|
21208
|
+
"POST",
|
|
21209
|
+
"/gcp/scope/preview",
|
|
21210
|
+
{ body }
|
|
21211
|
+
);
|
|
21212
|
+
}
|
|
21213
|
+
async gcpRequestAccess(args) {
|
|
21214
|
+
const agentId = args.agent_id ?? this.agentId;
|
|
21215
|
+
const runId = args.run_id ?? this.runId;
|
|
21216
|
+
if (!agentId) {
|
|
21217
|
+
throw makeBrokerError(400, "BrokerClient.gcpRequestAccess requires agent_id (pass it in args, or set agentId on BrokerClientConfig)");
|
|
21218
|
+
}
|
|
21219
|
+
if (!runId) {
|
|
21220
|
+
throw makeBrokerError(400, "BrokerClient.gcpRequestAccess requires run_id (pass it in args, or set runId on BrokerClientConfig)");
|
|
21221
|
+
}
|
|
21222
|
+
const body = { ...args, agent_id: agentId, run_id: runId };
|
|
21223
|
+
return this.request("POST", "/gcp/grants", { body });
|
|
21224
|
+
}
|
|
21225
|
+
gcpPollGrant(args) {
|
|
21226
|
+
return this.request("GET", `/gcp/grants/${encodeURIComponent(args.grant_id)}`);
|
|
21227
|
+
}
|
|
21228
|
+
gcpCheckApprovalChannel(args) {
|
|
21229
|
+
if (!this.agentId) {
|
|
21230
|
+
throw makeBrokerError(400, "BrokerClient.gcpCheckApprovalChannel requires agentId \u2014 pass it in BrokerClientConfig");
|
|
21231
|
+
}
|
|
21232
|
+
return this.request(
|
|
21233
|
+
"GET",
|
|
21234
|
+
"/gcp/approval-channel-status",
|
|
21235
|
+
{ query: { project_id: args.project_id, agent_id: this.agentId } }
|
|
21236
|
+
);
|
|
21237
|
+
}
|
|
21238
|
+
gcpReleaseAccess(args) {
|
|
21239
|
+
return this.request(
|
|
21240
|
+
"POST",
|
|
21241
|
+
`/gcp/grants/${encodeURIComponent(args.grant_id)}/release`
|
|
21242
|
+
);
|
|
21243
|
+
}
|
|
21244
|
+
gcpListInventory() {
|
|
21245
|
+
if (!this.agentId) {
|
|
21246
|
+
throw makeBrokerError(400, "BrokerClient.gcpListInventory requires agentId \u2014 pass it in BrokerClientConfig");
|
|
21247
|
+
}
|
|
21248
|
+
return this.request(
|
|
21249
|
+
"GET",
|
|
21250
|
+
"/gcp/inventory",
|
|
21251
|
+
{ query: { agent_id: this.agentId } }
|
|
21252
|
+
);
|
|
21253
|
+
}
|
|
21254
|
+
gcpGetCredentials(args) {
|
|
21255
|
+
return this.request(
|
|
21256
|
+
"POST",
|
|
21257
|
+
`/gcp/grants/${encodeURIComponent(args.grant_id)}/credentials`
|
|
21258
|
+
);
|
|
21259
|
+
}
|
|
21260
|
+
// ──────────────────────────────── Supabase ────────────────────────────────
|
|
21261
|
+
// Short-lived Supabase JWT tokens for querying enrolled Supabase projects.
|
|
21262
|
+
// Prod projects always route_to_approver (HITL); dev/staging can be
|
|
21263
|
+
// configured for auto_approve via policy_ceiling.auto_approved_roles.
|
|
21264
|
+
supabaseDescribeScope(args) {
|
|
21265
|
+
if (!this.agentId) {
|
|
21266
|
+
throw makeBrokerError(400, "BrokerClient.supabaseDescribeScope requires agentId \u2014 pass it in BrokerClientConfig");
|
|
21267
|
+
}
|
|
21268
|
+
return this.request(
|
|
21269
|
+
"GET",
|
|
21270
|
+
"/supabase/scope",
|
|
21271
|
+
{ query: { project_ref: args.project_ref, agent_id: this.agentId } }
|
|
21272
|
+
);
|
|
21273
|
+
}
|
|
21274
|
+
supabasePreviewRequest(args) {
|
|
21275
|
+
if (!this.agentId) {
|
|
21276
|
+
throw makeBrokerError(400, "BrokerClient.supabasePreviewRequest requires agentId \u2014 pass it in BrokerClientConfig");
|
|
21277
|
+
}
|
|
21278
|
+
const body = { ...args };
|
|
21279
|
+
if (body.agent_id === void 0) body.agent_id = this.agentId;
|
|
21280
|
+
return this.request(
|
|
21281
|
+
"POST",
|
|
21282
|
+
"/supabase/scope/preview",
|
|
21283
|
+
{ body }
|
|
21284
|
+
);
|
|
21285
|
+
}
|
|
21286
|
+
async supabaseRequestAccess(args) {
|
|
21287
|
+
const agentId = args.agent_id ?? this.agentId;
|
|
21288
|
+
const runId = args.run_id ?? this.runId;
|
|
21289
|
+
if (!agentId) {
|
|
21290
|
+
throw makeBrokerError(400, "BrokerClient.supabaseRequestAccess requires agent_id (pass it in args, or set agentId on BrokerClientConfig)");
|
|
21291
|
+
}
|
|
21292
|
+
if (!runId) {
|
|
21293
|
+
throw makeBrokerError(400, "BrokerClient.supabaseRequestAccess requires run_id (pass it in args, or set runId on BrokerClientConfig)");
|
|
21294
|
+
}
|
|
21295
|
+
const body = { ...args, agent_id: agentId, run_id: runId };
|
|
21296
|
+
return this.request("POST", "/supabase/grants", { body });
|
|
21297
|
+
}
|
|
21298
|
+
supabasePollGrant(args) {
|
|
21299
|
+
return this.request(
|
|
21300
|
+
"GET",
|
|
21301
|
+
`/supabase/grants/${encodeURIComponent(args.grant_id)}`,
|
|
21302
|
+
{ query: this.agentId ? { agent_id: this.agentId } : void 0 }
|
|
21303
|
+
);
|
|
21304
|
+
}
|
|
21305
|
+
supabaseCheckApprovalChannel(args) {
|
|
21306
|
+
if (!this.agentId) {
|
|
21307
|
+
throw makeBrokerError(400, "BrokerClient.supabaseCheckApprovalChannel requires agentId \u2014 pass it in BrokerClientConfig");
|
|
21308
|
+
}
|
|
21309
|
+
return this.request(
|
|
21310
|
+
"GET",
|
|
21311
|
+
"/supabase/approval-channel-status",
|
|
21312
|
+
{ query: { project_ref: args.project_ref, agent_id: this.agentId } }
|
|
21313
|
+
);
|
|
21314
|
+
}
|
|
21315
|
+
supabaseReleaseAccess(args) {
|
|
21316
|
+
return this.request(
|
|
21317
|
+
"POST",
|
|
21318
|
+
`/supabase/grants/${encodeURIComponent(args.grant_id)}/release`,
|
|
21319
|
+
{ query: this.agentId ? { agent_id: this.agentId } : void 0 }
|
|
21320
|
+
);
|
|
21321
|
+
}
|
|
21322
|
+
supabaseListInventory() {
|
|
21323
|
+
if (!this.agentId) {
|
|
21324
|
+
throw makeBrokerError(400, "BrokerClient.supabaseListInventory requires agentId \u2014 pass it in BrokerClientConfig");
|
|
21325
|
+
}
|
|
21326
|
+
return this.request(
|
|
21327
|
+
"GET",
|
|
21328
|
+
"/supabase/inventory",
|
|
21329
|
+
{ query: { agent_id: this.agentId } }
|
|
21330
|
+
);
|
|
21331
|
+
}
|
|
21332
|
+
supabaseGetCredentials(args) {
|
|
21333
|
+
return this.request(
|
|
21334
|
+
"POST",
|
|
21335
|
+
`/supabase/grants/${encodeURIComponent(args.grant_id)}/credentials`,
|
|
21336
|
+
{ query: this.agentId ? { agent_id: this.agentId } : void 0 }
|
|
21337
|
+
);
|
|
21338
|
+
}
|
|
21185
21339
|
};
|
|
21186
21340
|
|
|
21187
21341
|
// src/inventory.ts
|
|
@@ -21201,6 +21355,36 @@ function renderInventoryBlock(accounts) {
|
|
|
21201
21355
|
Available accounts for this team (use the 12-digit account_id, NEVER invent one):
|
|
21202
21356
|
${lines.join("\n")}${truncated}`;
|
|
21203
21357
|
}
|
|
21358
|
+
function renderGcpInventoryBlock(projects) {
|
|
21359
|
+
if (projects.length === 0) return "";
|
|
21360
|
+
const shown = projects.slice(0, INVENTORY_RENDER_LIMIT);
|
|
21361
|
+
const lines = shown.map((p) => {
|
|
21362
|
+
const sa = p.service_account_email ? ` (sa: ${p.service_account_email})` : "";
|
|
21363
|
+
return ` - \`${p.project_id}\` \u2014 ${p.display_name}${sa}`;
|
|
21364
|
+
});
|
|
21365
|
+
const truncated = projects.length > INVENTORY_RENDER_LIMIT ? `
|
|
21366
|
+
|
|
21367
|
+
\u2026and ${projects.length - INVENTORY_RENDER_LIMIT} more \u2014 call \`gcp_describe_scope\` for any unlisted project.` : "";
|
|
21368
|
+
return `
|
|
21369
|
+
|
|
21370
|
+
Available GCP projects for this team (use the project_id, NEVER invent one):
|
|
21371
|
+
${lines.join("\n")}${truncated}`;
|
|
21372
|
+
}
|
|
21373
|
+
function renderSupabaseInventoryBlock(projects) {
|
|
21374
|
+
if (projects.length === 0) return "";
|
|
21375
|
+
const shown = projects.slice(0, INVENTORY_RENDER_LIMIT);
|
|
21376
|
+
const lines = shown.map((p) => {
|
|
21377
|
+
const url = p.supabase_url ? ` (${p.supabase_url})` : "";
|
|
21378
|
+
return ` - \`${p.project_ref}\` \u2014 ${p.display_name}${url}`;
|
|
21379
|
+
});
|
|
21380
|
+
const truncated = projects.length > INVENTORY_RENDER_LIMIT ? `
|
|
21381
|
+
|
|
21382
|
+
\u2026and ${projects.length - INVENTORY_RENDER_LIMIT} more \u2014 call \`supabase_describe_scope\` for any unlisted project.` : "";
|
|
21383
|
+
return `
|
|
21384
|
+
|
|
21385
|
+
Available Supabase projects for this team (use the project_ref, NEVER invent one):
|
|
21386
|
+
${lines.join("\n")}${truncated}`;
|
|
21387
|
+
}
|
|
21204
21388
|
|
|
21205
21389
|
// src/tool-schemas.ts
|
|
21206
21390
|
var accountIdSchema = external_exports.string().regex(/^\d{12}$/, "AWS account_id must be a 12-digit string");
|
|
@@ -21274,11 +21458,123 @@ var releaseAccessShape = releaseAccessSchema.shape;
|
|
|
21274
21458
|
var getCredentialsShape = getCredentialsSchema.shape;
|
|
21275
21459
|
var checkApprovalChannelShape = checkApprovalChannelSchema.shape;
|
|
21276
21460
|
var listAccountsShape = listAccountsSchema.shape;
|
|
21461
|
+
var projectIdSchema = external_exports.string().regex(
|
|
21462
|
+
/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/,
|
|
21463
|
+
"GCP project_id must be 6\u201330 chars, lowercase letters/digits/hyphens, starting with a letter and not ending with a hyphen"
|
|
21464
|
+
);
|
|
21465
|
+
var permissionListSchema = external_exports.array(external_exports.string().min(1)).min(1, "At least one permission is required").describe(
|
|
21466
|
+
'GCP IAM permissions to allow, e.g. ["storage.objects.get", "storage.objects.list"]. See https://cloud.google.com/iam/docs/permissions-reference for the canonical list.'
|
|
21467
|
+
);
|
|
21468
|
+
var gcpResourceListSchema = external_exports.array(external_exports.string().min(1)).min(1, "At least one resource is required").describe(
|
|
21469
|
+
'GCP resource names (e.g. "projects/_/buckets/reports/objects/*", "projects/<project>/zones/us-central1-a/instances/*"). Must be inside the enrolled project.'
|
|
21470
|
+
);
|
|
21471
|
+
var gcpDescribeScopeSchema = external_exports.object({
|
|
21472
|
+
project_id: projectIdSchema
|
|
21473
|
+
});
|
|
21474
|
+
var gcpPreviewRequestSchema = external_exports.object({
|
|
21475
|
+
project_id: projectIdSchema,
|
|
21476
|
+
permissions: permissionListSchema,
|
|
21477
|
+
resources: gcpResourceListSchema,
|
|
21478
|
+
ttl_seconds: ttlSecondsSchema
|
|
21479
|
+
});
|
|
21480
|
+
var gcpRequestAccessSchema = external_exports.object({
|
|
21481
|
+
agent_id: external_exports.string().uuid("agent_id must be a UUID").optional().describe(
|
|
21482
|
+
"Optional. Defaults to the host MCP env (AGT_AGENT_ID). Pass explicitly only to attribute the grant to a different agent."
|
|
21483
|
+
),
|
|
21484
|
+
run_id: external_exports.string().uuid("run_id must be a UUID").optional().describe(
|
|
21485
|
+
"Optional. Defaults to the host MCP env (AGT_RUN_ID). Pass explicitly only to attribute the grant to a different run."
|
|
21486
|
+
),
|
|
21487
|
+
project_id: projectIdSchema,
|
|
21488
|
+
permissions: permissionListSchema,
|
|
21489
|
+
resources: gcpResourceListSchema,
|
|
21490
|
+
ttl_seconds: ttlSecondsSchema,
|
|
21491
|
+
task_id: external_exports.string().min(1).optional().describe("Optional Augmented task identifier (e.g. kanban:task-7842)."),
|
|
21492
|
+
reason: external_exports.string().min(1).max(500).optional().describe(
|
|
21493
|
+
"One-sentence rationale shown to a human approver. Must be safe to render \u2014 never contains credentials."
|
|
21494
|
+
),
|
|
21495
|
+
source_context: sourceContextSchema.optional().describe(
|
|
21496
|
+
"Optional. Inbound conversation that triggered this request. Persisted on the grant so the resolution notification can thread the answer back to the original channel/thread instead of dead-ending in direct-chat."
|
|
21497
|
+
)
|
|
21498
|
+
});
|
|
21499
|
+
var gcpPollGrantSchema = external_exports.object({
|
|
21500
|
+
grant_id: external_exports.string().uuid("grant_id must be a UUID")
|
|
21501
|
+
});
|
|
21502
|
+
var gcpReleaseAccessSchema = external_exports.object({
|
|
21503
|
+
grant_id: external_exports.string().uuid("grant_id must be a UUID")
|
|
21504
|
+
});
|
|
21505
|
+
var gcpGetCredentialsSchema = external_exports.object({
|
|
21506
|
+
grant_id: external_exports.string().uuid("grant_id must be a UUID")
|
|
21507
|
+
});
|
|
21508
|
+
var gcpCheckApprovalChannelSchema = external_exports.object({
|
|
21509
|
+
project_id: projectIdSchema
|
|
21510
|
+
});
|
|
21511
|
+
var gcpListAccountsSchema = external_exports.object({});
|
|
21512
|
+
var gcpDescribeScopeShape = gcpDescribeScopeSchema.shape;
|
|
21513
|
+
var gcpPreviewRequestShape = gcpPreviewRequestSchema.shape;
|
|
21514
|
+
var gcpRequestAccessShape = gcpRequestAccessSchema.shape;
|
|
21515
|
+
var gcpPollGrantShape = gcpPollGrantSchema.shape;
|
|
21516
|
+
var gcpReleaseAccessShape = gcpReleaseAccessSchema.shape;
|
|
21517
|
+
var gcpGetCredentialsShape = gcpGetCredentialsSchema.shape;
|
|
21518
|
+
var gcpCheckApprovalChannelShape = gcpCheckApprovalChannelSchema.shape;
|
|
21519
|
+
var gcpListAccountsShape = gcpListAccountsSchema.shape;
|
|
21520
|
+
var supabaseProjectRefSchema = external_exports.string().regex(/^[a-z0-9]{20}$/, "Supabase project_ref must be exactly 20 lowercase alphanumeric characters");
|
|
21521
|
+
var schemaListSchema = external_exports.array(external_exports.string().min(1)).min(1, "At least one schema is required").describe(`Postgres schemas to access, e.g. ["public"]. Must be within the team's allowed_schemas ceiling.`);
|
|
21522
|
+
var supabaseDescribeScopeSchema = external_exports.object({
|
|
21523
|
+
project_ref: supabaseProjectRefSchema
|
|
21524
|
+
});
|
|
21525
|
+
var supabasePreviewRequestSchema = external_exports.object({
|
|
21526
|
+
project_ref: supabaseProjectRefSchema,
|
|
21527
|
+
role: external_exports.string().min(1).describe('Postgres role for the JWT (e.g. "authenticated"). "service_role" always routes to approver.'),
|
|
21528
|
+
allowed_schemas: schemaListSchema,
|
|
21529
|
+
allowed_tables: external_exports.array(external_exports.string().min(1)).optional().describe('Optional schema.table allowlist (e.g. ["public.agents", "public.runs"]). Absent = all tables in allowed_schemas.'),
|
|
21530
|
+
read_only: external_exports.boolean().describe("True if only SELECT access is needed. Surfaced as a JWT claim for RLS enforcement."),
|
|
21531
|
+
ttl_seconds: ttlSecondsSchema
|
|
21532
|
+
});
|
|
21533
|
+
var supabaseRequestAccessSchema = external_exports.object({
|
|
21534
|
+
agent_id: external_exports.string().uuid("agent_id must be a UUID").optional().describe(
|
|
21535
|
+
"Optional. Defaults to the host MCP env (AGT_AGENT_ID)."
|
|
21536
|
+
),
|
|
21537
|
+
run_id: external_exports.string().uuid("run_id must be a UUID").optional().describe(
|
|
21538
|
+
"Optional. Defaults to the host MCP env (AGT_RUN_ID)."
|
|
21539
|
+
),
|
|
21540
|
+
project_ref: supabaseProjectRefSchema,
|
|
21541
|
+
role: external_exports.string().min(1).describe('Postgres role for the JWT (e.g. "authenticated"). "service_role" always routes to approver.'),
|
|
21542
|
+
allowed_schemas: schemaListSchema,
|
|
21543
|
+
allowed_tables: external_exports.array(external_exports.string().min(1)).optional().describe("Optional schema.table allowlist. Absent = all tables in allowed_schemas."),
|
|
21544
|
+
read_only: external_exports.boolean().describe("True if only SELECT access is needed."),
|
|
21545
|
+
ttl_seconds: ttlSecondsSchema,
|
|
21546
|
+
task_id: external_exports.string().min(1).optional().describe("Optional Augmented task identifier."),
|
|
21547
|
+
reason: external_exports.string().min(1).max(500).optional().describe("One-sentence rationale shown to a human approver."),
|
|
21548
|
+
source_context: sourceContextSchema.optional().describe(
|
|
21549
|
+
"Optional. Inbound conversation that triggered this request. Persisted so the resolution notification can thread back to the original channel."
|
|
21550
|
+
)
|
|
21551
|
+
});
|
|
21552
|
+
var supabasePollGrantSchema = external_exports.object({
|
|
21553
|
+
grant_id: external_exports.string().uuid("grant_id must be a UUID")
|
|
21554
|
+
});
|
|
21555
|
+
var supabaseReleaseAccessSchema = external_exports.object({
|
|
21556
|
+
grant_id: external_exports.string().uuid("grant_id must be a UUID")
|
|
21557
|
+
});
|
|
21558
|
+
var supabaseGetCredentialsSchema = external_exports.object({
|
|
21559
|
+
grant_id: external_exports.string().uuid("grant_id must be a UUID")
|
|
21560
|
+
});
|
|
21561
|
+
var supabaseCheckApprovalChannelSchema = external_exports.object({
|
|
21562
|
+
project_ref: supabaseProjectRefSchema
|
|
21563
|
+
});
|
|
21564
|
+
var supabaseListProjectsSchema = external_exports.object({});
|
|
21565
|
+
var supabaseDescribeScopeShape = supabaseDescribeScopeSchema.shape;
|
|
21566
|
+
var supabasePreviewRequestShape = supabasePreviewRequestSchema.shape;
|
|
21567
|
+
var supabaseRequestAccessShape = supabaseRequestAccessSchema.shape;
|
|
21568
|
+
var supabasePollGrantShape = supabasePollGrantSchema.shape;
|
|
21569
|
+
var supabaseReleaseAccessShape = supabaseReleaseAccessSchema.shape;
|
|
21570
|
+
var supabaseGetCredentialsShape = supabaseGetCredentialsSchema.shape;
|
|
21571
|
+
var supabaseCheckApprovalChannelShape = supabaseCheckApprovalChannelSchema.shape;
|
|
21572
|
+
var supabaseListProjectsShape = supabaseListProjectsSchema.shape;
|
|
21277
21573
|
|
|
21278
21574
|
// package.json
|
|
21279
21575
|
var package_default = {
|
|
21280
21576
|
name: "@integrity-labs/cloud-broker",
|
|
21281
|
-
version: "0.7.
|
|
21577
|
+
version: "0.7.4",
|
|
21282
21578
|
description: "Cloud Access Broker \u2014 MCP server that mints scoped, TTL-bounded cloud credentials per agent task. Ships AWS support (aws_request_access, aws_poll_grant, aws_release_access, aws_describe_scope, aws_preview_request, aws_get_credentials \u2014 STS AssumeRole under the hood); GCP, Azure, and Cloudflare land alongside in the same package as the broker grows.",
|
|
21283
21579
|
type: "module",
|
|
21284
21580
|
bin: {
|
|
@@ -21305,7 +21601,9 @@ var package_default = {
|
|
|
21305
21601
|
test: "vitest run",
|
|
21306
21602
|
clean: "rm -rf dist",
|
|
21307
21603
|
"publish:templates": "bash cloudformation/publish.sh",
|
|
21308
|
-
"check-published": "bash cloudformation/check-published.sh"
|
|
21604
|
+
"check-published": "bash cloudformation/check-published.sh",
|
|
21605
|
+
"publish:terraform": "bash terraform/publish.sh",
|
|
21606
|
+
"check-published-terraform": "bash terraform/check-published.sh"
|
|
21309
21607
|
},
|
|
21310
21608
|
dependencies: {
|
|
21311
21609
|
"@modelcontextprotocol/sdk": "1.27.1",
|
|
@@ -21385,9 +21683,59 @@ async function loadInventoryBlock() {
|
|
|
21385
21683
|
}
|
|
21386
21684
|
}
|
|
21387
21685
|
var inventoryBlock = await loadInventoryBlock();
|
|
21686
|
+
var NO_DIRECT_HTTP_BLOCK = "\n\nUse this MCP tool \u2014 never call /aws/* or /cloud/aws/* HTTP endpoints directly. The broker client handles JWT refresh, slug disambiguation, secret_ref redirection, and per-call audit context that direct HTTP loses.";
|
|
21687
|
+
async function loadGcpInventoryBlock() {
|
|
21688
|
+
const ctl = new AbortController();
|
|
21689
|
+
const timer = setTimeout(() => ctl.abort(), INVENTORY_FETCH_TIMEOUT_MS);
|
|
21690
|
+
try {
|
|
21691
|
+
const result = await Promise.race([
|
|
21692
|
+
broker.gcpListInventory(),
|
|
21693
|
+
new Promise((_, reject) => {
|
|
21694
|
+
ctl.signal.addEventListener(
|
|
21695
|
+
"abort",
|
|
21696
|
+
() => reject(new Error(`gcp_inventory_lookup timed out after ${INVENTORY_FETCH_TIMEOUT_MS}ms`))
|
|
21697
|
+
);
|
|
21698
|
+
})
|
|
21699
|
+
]);
|
|
21700
|
+
return renderGcpInventoryBlock(result.projects);
|
|
21701
|
+
} catch (err) {
|
|
21702
|
+
console.error(
|
|
21703
|
+
`cloud-broker: gcp_inventory_lookup_failed (non-fatal \u2014 tool descriptions render project-agnostic): ${formatBrokerError(err)}`
|
|
21704
|
+
);
|
|
21705
|
+
return "";
|
|
21706
|
+
} finally {
|
|
21707
|
+
clearTimeout(timer);
|
|
21708
|
+
}
|
|
21709
|
+
}
|
|
21710
|
+
var gcpInventoryBlock = await loadGcpInventoryBlock();
|
|
21711
|
+
async function loadSupabaseInventoryBlock() {
|
|
21712
|
+
const ctl = new AbortController();
|
|
21713
|
+
const timer = setTimeout(() => ctl.abort(), INVENTORY_FETCH_TIMEOUT_MS);
|
|
21714
|
+
try {
|
|
21715
|
+
const result = await Promise.race([
|
|
21716
|
+
broker.supabaseListInventory(),
|
|
21717
|
+
new Promise((_, reject) => {
|
|
21718
|
+
ctl.signal.addEventListener(
|
|
21719
|
+
"abort",
|
|
21720
|
+
() => reject(new Error(`supabase_inventory_lookup timed out after ${INVENTORY_FETCH_TIMEOUT_MS}ms`))
|
|
21721
|
+
);
|
|
21722
|
+
})
|
|
21723
|
+
]);
|
|
21724
|
+
return renderSupabaseInventoryBlock(result.projects);
|
|
21725
|
+
} catch (err) {
|
|
21726
|
+
console.error(
|
|
21727
|
+
`cloud-broker: supabase_inventory_lookup_failed (non-fatal): ${formatBrokerError(err)}`
|
|
21728
|
+
);
|
|
21729
|
+
return "";
|
|
21730
|
+
} finally {
|
|
21731
|
+
clearTimeout(timer);
|
|
21732
|
+
}
|
|
21733
|
+
}
|
|
21734
|
+
var supabaseInventoryBlock = await loadSupabaseInventoryBlock();
|
|
21735
|
+
var NO_DIRECT_SUPABASE_HTTP_BLOCK = "\n\nUse this MCP tool \u2014 never call /supabase/* HTTP endpoints directly. The broker client handles JWT refresh, slug disambiguation, secret_ref redirection, and per-call audit context that direct HTTP loses.";
|
|
21388
21736
|
server.tool(
|
|
21389
21737
|
"aws_describe_scope",
|
|
21390
|
-
"Return the team's resolved AWS policy ceiling for an account: max_ttl_seconds, allowed_regions, auto_approved_actions, and the action denylist. Call this before aws_request_access if you're not sure what you're allowed to ask for \u2014 it's free, idempotent, and writes nothing." + inventoryBlock,
|
|
21738
|
+
"Return the team's resolved AWS policy ceiling for an account: max_ttl_seconds, allowed_regions, auto_approved_actions, and the action denylist. Call this before aws_request_access if you're not sure what you're allowed to ask for \u2014 it's free, idempotent, and writes nothing." + inventoryBlock + NO_DIRECT_HTTP_BLOCK,
|
|
21391
21739
|
describeScopeShape,
|
|
21392
21740
|
async (args) => {
|
|
21393
21741
|
try {
|
|
@@ -21400,7 +21748,7 @@ server.tool(
|
|
|
21400
21748
|
);
|
|
21401
21749
|
server.tool(
|
|
21402
21750
|
"aws_preview_request",
|
|
21403
|
-
'Dry-run a candidate request. Returns one of "auto_approve", "route_to_approver", or "hard_deny" without dispatching anything or writing audit_log. Useful for letting the agent self-tighten its scope before triggering a human approval.',
|
|
21751
|
+
'Dry-run a candidate request. Returns one of "auto_approve", "route_to_approver", or "hard_deny" without dispatching anything or writing audit_log. Useful for letting the agent self-tighten its scope before triggering a human approval.' + NO_DIRECT_HTTP_BLOCK,
|
|
21404
21752
|
previewRequestShape,
|
|
21405
21753
|
async (args) => {
|
|
21406
21754
|
try {
|
|
@@ -21413,7 +21761,7 @@ server.tool(
|
|
|
21413
21761
|
);
|
|
21414
21762
|
server.tool(
|
|
21415
21763
|
"aws_check_approval_channel",
|
|
21416
|
-
`Verify the agent's Slack bot can post to the AWS-approval channel for an account BEFORE calling aws_request_access. Returns { ok, channel?, bot_user_handle?, reason? }. When ok=false, branch on reason: "channel_not_found" or "not_in_channel" \u2192 tell the user "I need to be invited to the approvals channel before I can request access \u2014 please run \`/invite @<bot_user_handle>\` in your AWS-approvals channel and let me know when done"; "archived" \u2192 tell the user the configured channel is archived and an admin needs to repoint it in Team Settings; "no_approval_channel_configured" \u2192 tell the user no approval channel is set up for this AWS account and ask them to configure one in Team Settings; "agent_slack_not_configured" \u2192 escalate to operator (you have no Slack bot at all, so /invite won't help); "auth_failed" or "unknown" \u2192 escalate to operator with raw_error. ALWAYS call this on the FIRST aws_request_access for a given account_id in a session \u2014 once you've gotten ok=true once, you can skip subsequent pre-checks. If ok=false, DO NOT call aws_request_access \u2014 surface the user-facing message above and wait. Same jargon-free, no-mechanics rules as aws_request_access: never expose "broker", "grant", "MCP", "Slack channel ID", etc to the user \u2014 talk about the AWS account and the invite action.` + inventoryBlock,
|
|
21764
|
+
`Verify the agent's Slack bot can post to the AWS-approval channel for an account BEFORE calling aws_request_access. Returns { ok, channel?, bot_user_handle?, reason? }. When ok=false, branch on reason: "channel_not_found" or "not_in_channel" \u2192 tell the user "I need to be invited to the approvals channel before I can request access \u2014 please run \`/invite @<bot_user_handle>\` in your AWS-approvals channel and let me know when done"; "archived" \u2192 tell the user the configured channel is archived and an admin needs to repoint it in Team Settings; "no_approval_channel_configured" \u2192 tell the user no approval channel is set up for this AWS account and ask them to configure one in Team Settings; "agent_slack_not_configured" \u2192 escalate to operator (you have no Slack bot at all, so /invite won't help); "auth_failed" or "unknown" \u2192 escalate to operator with raw_error. ALWAYS call this on the FIRST aws_request_access for a given account_id in a session \u2014 once you've gotten ok=true once, you can skip subsequent pre-checks. If ok=false, DO NOT call aws_request_access \u2014 surface the user-facing message above and wait. Same jargon-free, no-mechanics rules as aws_request_access: never expose "broker", "grant", "MCP", "Slack channel ID", etc to the user \u2014 talk about the AWS account and the invite action.` + inventoryBlock + NO_DIRECT_HTTP_BLOCK,
|
|
21417
21765
|
checkApprovalChannelShape,
|
|
21418
21766
|
async (args) => {
|
|
21419
21767
|
try {
|
|
@@ -21430,7 +21778,7 @@ server.tool(
|
|
|
21430
21778
|
// accounts inline with the tool that consumes account_id. This is the
|
|
21431
21779
|
// call site where a wrong/invented account_id would actually do harm
|
|
21432
21780
|
// (route to a nonexistent enrolment → 404 → confusing failure mode).
|
|
21433
|
-
'Request scoped, TTL-bounded AWS credentials for the current task. CALL aws_check_approval_channel FIRST on the FIRST request for a given account_id in a session \u2014 if it returns ok=false, DO NOT call this tool, surface the invite-the-bot message instead. agent_id and run_id are optional \u2014 the broker fills them from the host MCP env (AGT_AGENT_ID / AGT_RUN_ID). source_context is optional but you SHOULD pass it whenever the request was triggered by an inbound channel message: extract { channel_type, channel_id, thread_ts? } from the `<channel>` tag in the conversation that triggered this. Slack: channel_type="slack", channel_id=tag\'s `channel`, thread_ts=tag\'s `thread_ts`. Telegram: channel_type="telegram", channel_id=tag\'s `chat_id`. Direct-chat: channel_type="direct-chat", channel_id=tag\'s `session_id`. Without source_context the resolution notification dead-ends in your direct-chat instead of threading back to the original conversation. Returns { grant_id, status, secret_ref?, expires_at?, denial_reason?, notification_status?, notification_failure_reason?, notification_channel_name?, notification_channel_id?, notification_permalink?, notification_bot_user_handle? }. status="active" means credentials are ready, use them now. status="denied" means the request was rejected (denial_reason explains). status="pending" means approval is still outstanding \u2014 DO NOT poll. Post a brief, jargon-free acknowledgement to the user first. When notification_channel_name is populated, QUOTE IT VERBATIM in the acknowledgement (e.g. "Requesting access to the <aws_account_name> account so I can <do the task> \u2014 pinged an admin in #<notification_channel_name> to approve, will resume the moment it lands"); NEVER invent or guess a channel name from training (e.g. "#aws-approvals" is wrong if the configured channel is "#agt-approvals"). When notification_permalink is populated, you MAY append it as a markdown link (e.g. "[approval card](<notification_permalink>)") so the user can jump straight to it; otherwise omit. When notification_channel_name is absent, fall back to the channel-agnostic phrasing ("pinged an admin to approve"). Then save the grant_id and return control. NEVER expose broker mechanics to the user \u2014 phrases like "firing a broker grant", "requesting a grant", "broker grant", "grant_id", "secret_ref", "STS", "AssumeRole", or any aws_* tool name must not appear in user-facing messages. Also NEVER paste the grant UUID into user-facing prose (e.g. "request c7a0b7be-5e09-\u2026 is queued") \u2014 it is operator-only metadata and reads as noise to the user. Talk about the task and the AWS account, not the plumbing. The broker pushes the resolution to you via direct-chat the moment a human approves or denies. The notification body will include an "Original conversation:" line naming the channel/thread to reply in \u2014 when it arrives, post a one-line acknowledgement there in the same jargon-free style ("Approval came through for <aws_account_name> \u2014 kicking off <the task> now" on active; "Couldn\'t get access to <aws_account_name> for <the task>: <paraphrased reason> \u2014 let me know how you\'d like to proceed" on denied) BEFORE calling aws_get_credentials or doing the work, then complete the user\'s task in that same channel/thread (not direct-chat). Going silent between the request and the work loses the human-in-the-loop signal. Only check notification_status if you need to flag a setup issue to the user: "sent" means a human was paged; "failed" or "not_attempted" means no human was paged (typically channel_not_found because the approval-bot is not a member of the configured channel). When notification_status is "failed" and notification_bot_user_handle + notification_channel_name are populated, name them specifically in the user message: "I queued the access request for <aws_account_name> but couldn\'t notify the approver \u2014 please run `/invite @<notification_bot_user_handle>` in #<notification_channel_name> and let me know when done so I can re-fire". When the handle/channel-name aren\'t populated, fall back to the generic version: "I couldn\'t reach an approver for the <aws_account_name> account (the approval bot isn\'t in the configured channel) \u2014 please ping an admin manually, or fix the AWS approval channel in Team Settings". Keep the user-facing text jargon-free and paraphrased. grant_id and notification_failure_reason are operator/escalation-only metadata; if and only if you are escalating to an operator, append "(reference: <grant_id>, failure: <notification_failure_reason>)" to the operator-facing escalation note. aws_poll_grant exists as an escape hatch for explicit re-checks but the autonomous flow does not need it.' + inventoryBlock,
|
|
21781
|
+
'Request scoped, TTL-bounded AWS credentials for the current task. CALL aws_check_approval_channel FIRST on the FIRST request for a given account_id in a session \u2014 if it returns ok=false, DO NOT call this tool, surface the invite-the-bot message instead. agent_id and run_id are optional \u2014 the broker fills them from the host MCP env (AGT_AGENT_ID / AGT_RUN_ID). source_context is optional but you SHOULD pass it whenever the request was triggered by an inbound channel message: extract { channel_type, channel_id, thread_ts? } from the `<channel>` tag in the conversation that triggered this. Slack: channel_type="slack", channel_id=tag\'s `channel`, thread_ts=tag\'s `thread_ts`. Telegram: channel_type="telegram", channel_id=tag\'s `chat_id`. Direct-chat: channel_type="direct-chat", channel_id=tag\'s `session_id`. Without source_context the resolution notification dead-ends in your direct-chat instead of threading back to the original conversation. Returns { grant_id, status, secret_ref?, expires_at?, denial_reason?, notification_status?, notification_failure_reason?, notification_channel_name?, notification_channel_id?, notification_permalink?, notification_bot_user_handle? }. status="active" means credentials are ready, use them now. status="denied" means the request was rejected (denial_reason explains). status="pending" means approval is still outstanding \u2014 DO NOT poll. Post a brief, jargon-free acknowledgement to the user first. When notification_channel_name is populated, QUOTE IT VERBATIM in the acknowledgement (e.g. "Requesting access to the <aws_account_name> account so I can <do the task> \u2014 pinged an admin in #<notification_channel_name> to approve, will resume the moment it lands"); NEVER invent or guess a channel name from training (e.g. "#aws-approvals" is wrong if the configured channel is "#agt-approvals"). When notification_permalink is populated, you MAY append it as a markdown link (e.g. "[approval card](<notification_permalink>)") so the user can jump straight to it; otherwise omit. When notification_channel_name is absent, fall back to the channel-agnostic phrasing ("pinged an admin to approve"). Then save the grant_id and return control. NEVER expose broker mechanics to the user \u2014 phrases like "firing a broker grant", "requesting a grant", "broker grant", "grant_id", "secret_ref", "STS", "AssumeRole", or any aws_* tool name must not appear in user-facing messages. Also NEVER paste the grant UUID into user-facing prose (e.g. "request c7a0b7be-5e09-\u2026 is queued") \u2014 it is operator-only metadata and reads as noise to the user. Talk about the task and the AWS account, not the plumbing. The broker pushes the resolution to you via direct-chat the moment a human approves or denies. The notification body will include an "Original conversation:" line naming the channel/thread to reply in \u2014 when it arrives, post a one-line acknowledgement there in the same jargon-free style ("Approval came through for <aws_account_name> \u2014 kicking off <the task> now" on active; "Couldn\'t get access to <aws_account_name> for <the task>: <paraphrased reason> \u2014 let me know how you\'d like to proceed" on denied) BEFORE calling aws_get_credentials or doing the work, then complete the user\'s task in that same channel/thread (not direct-chat). Going silent between the request and the work loses the human-in-the-loop signal. Only check notification_status if you need to flag a setup issue to the user: "sent" means a human was paged; "failed" or "not_attempted" means no human was paged (typically channel_not_found because the approval-bot is not a member of the configured channel). When notification_status is "failed" and notification_bot_user_handle + notification_channel_name are populated, name them specifically in the user message: "I queued the access request for <aws_account_name> but couldn\'t notify the approver \u2014 please run `/invite @<notification_bot_user_handle>` in #<notification_channel_name> and let me know when done so I can re-fire". When the handle/channel-name aren\'t populated, fall back to the generic version: "I couldn\'t reach an approver for the <aws_account_name> account (the approval bot isn\'t in the configured channel) \u2014 please ping an admin manually, or fix the AWS approval channel in Team Settings". Keep the user-facing text jargon-free and paraphrased. grant_id and notification_failure_reason are operator/escalation-only metadata; if and only if you are escalating to an operator, append "(reference: <grant_id>, failure: <notification_failure_reason>)" to the operator-facing escalation note. aws_poll_grant exists as an escape hatch for explicit re-checks but the autonomous flow does not need it. RETRY CONTRACT (ENG-5166) \u2014 if you have a cached grant_id from a prior aws_request_access in this conversation and the user asks you again to do work that needs AWS access (retry, continue, "try again", or any new task on the same account), DO NOT short-circuit on "I already have a request in flight": ALWAYS call aws_poll_grant({grant_id}) FIRST to re-validate. If aws_poll_grant returns status="pending", keep waiting and tell the user "the previous access request is still pending an approver \u2014 will resume the moment it lands". If status="active" and expires_at is still in the future, reuse the existing grant (skip straight to aws_get_credentials). If status="denied", "expired", "failed", "revoked", or "active" past its expires_at, DROP the cached grant_id entirely and call aws_request_access AGAIN with a fresh request; surface "the previous access request was denied/expired/was revoked/expired \u2014 submitting a fresh one" to the user (paraphrase to match the terminal status, jargon-free, no grant_id leakage). NEVER claim "request already in flight" without having just polled and confirmed a genuinely-pending or unexpired-active status. The dispatcher considers status terminal once it leaves the pending state, so a cached grant_id is only trustworthy after a fresh poll.' + inventoryBlock + NO_DIRECT_HTTP_BLOCK,
|
|
21434
21782
|
requestAccessShape,
|
|
21435
21783
|
async (args) => {
|
|
21436
21784
|
try {
|
|
@@ -21444,7 +21792,7 @@ server.tool(
|
|
|
21444
21792
|
);
|
|
21445
21793
|
server.tool(
|
|
21446
21794
|
"aws_poll_grant",
|
|
21447
|
-
"Single-shot status check for a grant. The broker pushes resolution updates to you via direct-chat automatically \u2014 you do NOT need to poll in the normal flow. Use this only as an escape hatch: explicit re-check after a notification, or if you suspect a notification was lost (e.g. you got `aws_request_access` returning pending more than ~5 minutes ago and have heard nothing). Returns { grant_id, status, secret_ref?, expires_at?, denial_reason? }.",
|
|
21795
|
+
"Single-shot status check for a grant. The broker pushes resolution updates to you via direct-chat automatically \u2014 you do NOT need to poll in the normal flow. Use this only as an escape hatch: explicit re-check after a notification, or if you suspect a notification was lost (e.g. you got `aws_request_access` returning pending more than ~5 minutes ago and have heard nothing). Returns { grant_id, status, secret_ref?, expires_at?, denial_reason? }." + NO_DIRECT_HTTP_BLOCK,
|
|
21448
21796
|
pollGrantShape,
|
|
21449
21797
|
async (args) => {
|
|
21450
21798
|
try {
|
|
@@ -21457,7 +21805,7 @@ server.tool(
|
|
|
21457
21805
|
);
|
|
21458
21806
|
server.tool(
|
|
21459
21807
|
"aws_release_access",
|
|
21460
|
-
"Voluntarily release a grant before its TTL expires. Idempotent \u2014 safe to call on already-revoked or already-expired grants. Returns { grant_id, status }.",
|
|
21808
|
+
"Voluntarily release a grant before its TTL expires. Idempotent \u2014 safe to call on already-revoked or already-expired grants. Returns { grant_id, status }." + NO_DIRECT_HTTP_BLOCK,
|
|
21461
21809
|
releaseAccessShape,
|
|
21462
21810
|
async (args) => {
|
|
21463
21811
|
try {
|
|
@@ -21470,7 +21818,7 @@ server.tool(
|
|
|
21470
21818
|
);
|
|
21471
21819
|
server.tool(
|
|
21472
21820
|
"aws_get_credentials",
|
|
21473
|
-
"Fetch the AWS credentials for an active grant. Call this AFTER aws_request_access returns active (auto-approve) OR after the resolution-notification arrives (route_to_approver). Returns { grant_id, expires_at, credentials: { access_key_id, secret_access_key, session_token } }. Use them by prefixing your bash invocation, e.g. `AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=... aws ec2 describe-instances`. The grant must still be active and unexpired \u2014 409 if already released, 410 if past expires_at. Safe to call multiple times within the TTL window; the credentials don't change. Call aws_release_access when done.",
|
|
21821
|
+
"Fetch the AWS credentials for an active grant. Call this AFTER aws_request_access returns active (auto-approve) OR after the resolution-notification arrives (route_to_approver). Returns { grant_id, expires_at, credentials: { access_key_id, secret_access_key, session_token } }. Use them by prefixing your bash invocation, e.g. `AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=... aws ec2 describe-instances`. The grant must still be active and unexpired \u2014 409 if already released, 410 if past expires_at. Safe to call multiple times within the TTL window; the credentials don't change. Call aws_release_access when done." + NO_DIRECT_HTTP_BLOCK,
|
|
21474
21822
|
getCredentialsShape,
|
|
21475
21823
|
async (args) => {
|
|
21476
21824
|
try {
|
|
@@ -21483,7 +21831,7 @@ server.tool(
|
|
|
21483
21831
|
);
|
|
21484
21832
|
server.tool(
|
|
21485
21833
|
"aws_list_accounts",
|
|
21486
|
-
`List the AWS accounts your team currently has enrolled with the broker. Returns { accounts: [{ account_id, display_name, description?, default_region?, allowed_regions? }] }. The boot-time inventory injected into aws_request_access's description is frozen for this MCP process \u2014 call this tool when an account you expect to see is missing (e.g. an operator just re-enrolled one), when you want to confirm the canonical display_name / default_region / allowed_regions before firing aws_request_access, or to recover from "no enrolment" errors that hint the inventory may be stale. No arguments \u2014 agent_id is filled from the host MCP env (AGT_AGENT_ID). Cheap and idempotent; calls the same /aws/inventory endpoint that boots the description block
|
|
21834
|
+
`List the AWS accounts your team currently has enrolled with the broker. Returns { accounts: [{ account_id, display_name, description?, default_region?, allowed_regions? }] }. The boot-time inventory injected into aws_request_access's description is frozen for this MCP process \u2014 call this tool when an account you expect to see is missing (e.g. an operator just re-enrolled one), when you want to confirm the canonical display_name / default_region / allowed_regions before firing aws_request_access, or to recover from "no enrolment" errors that hint the inventory may be stale. No arguments \u2014 agent_id is filled from the host MCP env (AGT_AGENT_ID). Cheap and idempotent; calls the same /aws/inventory endpoint that boots the description block.` + NO_DIRECT_HTTP_BLOCK,
|
|
21487
21835
|
listAccountsShape,
|
|
21488
21836
|
async () => {
|
|
21489
21837
|
try {
|
|
@@ -21494,6 +21842,216 @@ server.tool(
|
|
|
21494
21842
|
}
|
|
21495
21843
|
}
|
|
21496
21844
|
);
|
|
21845
|
+
server.tool(
|
|
21846
|
+
"gcp_describe_scope",
|
|
21847
|
+
"Return the team's resolved GCP policy ceiling for a project: max_ttl_seconds, auto-approved permissions, allowed resource patterns, and the permission denylist. Call before gcp_request_access if you're not sure what you're allowed to ask for \u2014 free, idempotent, writes nothing." + gcpInventoryBlock + NO_DIRECT_HTTP_BLOCK,
|
|
21848
|
+
gcpDescribeScopeShape,
|
|
21849
|
+
async (args) => {
|
|
21850
|
+
try {
|
|
21851
|
+
const result = await broker.gcpDescribeScope(args);
|
|
21852
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21853
|
+
} catch (err) {
|
|
21854
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21855
|
+
}
|
|
21856
|
+
}
|
|
21857
|
+
);
|
|
21858
|
+
server.tool(
|
|
21859
|
+
"gcp_preview_request",
|
|
21860
|
+
'Dry-run a candidate GCP access request. Returns "auto_approve" / "route_to_approver" / "hard_deny" without dispatching anything or writing audit_log. Lets the agent self-tighten scope before triggering a human approval.' + NO_DIRECT_HTTP_BLOCK,
|
|
21861
|
+
gcpPreviewRequestShape,
|
|
21862
|
+
async (args) => {
|
|
21863
|
+
try {
|
|
21864
|
+
const result = await broker.gcpPreviewRequest(args);
|
|
21865
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21866
|
+
} catch (err) {
|
|
21867
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21868
|
+
}
|
|
21869
|
+
}
|
|
21870
|
+
);
|
|
21871
|
+
server.tool(
|
|
21872
|
+
"gcp_check_approval_channel",
|
|
21873
|
+
'Verify the agent\'s Slack bot can post to the GCP-approval channel for a project BEFORE calling gcp_request_access. Returns { ok, channel?, bot_user_handle?, reason? }. Same failure-mode taxonomy as aws_check_approval_channel \u2014 branch on reason: "channel_not_found"/"not_in_channel" \u2192 ask user to `/invite @<bot_user_handle>` in the approvals channel; "archived" \u2192 ask admin to repoint; "no_approval_channel_configured" \u2192 ask user to configure one in Team Settings; "agent_slack_not_configured" \u2192 escalate to operator; "auth_failed"/"unknown" \u2192 escalate. ALWAYS call this on the FIRST gcp_request_access for a given project_id in a session. If ok=false, DO NOT call gcp_request_access. Same jargon-free, no-mechanics rules as gcp_request_access \u2014 talk about the GCP project and the invite action, never "broker", "grant", "MCP".' + gcpInventoryBlock + NO_DIRECT_HTTP_BLOCK,
|
|
21874
|
+
gcpCheckApprovalChannelShape,
|
|
21875
|
+
async (args) => {
|
|
21876
|
+
try {
|
|
21877
|
+
const result = await broker.gcpCheckApprovalChannel(args);
|
|
21878
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21879
|
+
} catch (err) {
|
|
21880
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21881
|
+
}
|
|
21882
|
+
}
|
|
21883
|
+
);
|
|
21884
|
+
server.tool(
|
|
21885
|
+
"gcp_request_access",
|
|
21886
|
+
'Request scoped, TTL-bounded GCP credentials for the current task. CALL gcp_check_approval_channel FIRST on the FIRST request for a given project_id in a session \u2014 if it returns ok=false, do not call this tool, surface the invite-the-bot message instead. agent_id and run_id default from the host MCP env. Pass source_context whenever an inbound channel triggered the request so the resolution notification threads back to the original conversation. Returns { grant_id, status, secret_ref?, expires_at?, denial_reason?, notification_status?, notification_failure_reason?, notification_channel_name?, notification_channel_id?, notification_permalink?, notification_bot_user_handle? }. status="active" \u2192 credentials are ready, call gcp_get_credentials. status="denied" \u2192 request rejected, denial_reason explains. status="pending" \u2192 DO NOT poll, the broker pushes resolution via direct-chat. Post a brief jargon-free acknowledgement first. Quote notification_channel_name VERBATIM when populated ("pinged an admin in #<notification_channel_name> to approve, will resume the moment it lands"); never invent a channel name. Append the notification_permalink as a markdown link when present. Save the grant_id and return control. NEVER expose broker mechanics ("grant", "secret_ref", "IAM Credentials", "generateAccessToken", "MCP", any gcp_* tool name) in user-facing prose \u2014 talk about the task and the GCP project. When the resolution lands via direct-chat, post a one-line acknowledgement in the original channel/thread before calling gcp_get_credentials or starting the work; complete the task in that same channel/thread. Same notification_status branching as aws_request_access: "sent" \u2192 human paged; "failed"/"not_attempted" \u2192 no human paged, surface specific invite message when bot_user_handle + channel_name populated, generic recovery otherwise.' + gcpInventoryBlock + NO_DIRECT_HTTP_BLOCK,
|
|
21887
|
+
gcpRequestAccessShape,
|
|
21888
|
+
async (args) => {
|
|
21889
|
+
try {
|
|
21890
|
+
const result = await broker.gcpRequestAccess(args);
|
|
21891
|
+
const safe = { ...result, credentials: void 0 };
|
|
21892
|
+
return { content: [{ type: "text", text: JSON.stringify(safe, null, 2) }] };
|
|
21893
|
+
} catch (err) {
|
|
21894
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21895
|
+
}
|
|
21896
|
+
}
|
|
21897
|
+
);
|
|
21898
|
+
server.tool(
|
|
21899
|
+
"gcp_poll_grant",
|
|
21900
|
+
"Single-shot status check for a GCP grant. The broker pushes resolution updates via direct-chat automatically \u2014 you do NOT need to poll in the normal flow. Escape hatch only: explicit re-check after a notification, or if you suspect a notification was lost. Returns { grant_id, status, secret_ref?, expires_at?, denial_reason? }." + NO_DIRECT_HTTP_BLOCK,
|
|
21901
|
+
gcpPollGrantShape,
|
|
21902
|
+
async (args) => {
|
|
21903
|
+
try {
|
|
21904
|
+
const result = await broker.gcpPollGrant(args);
|
|
21905
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21906
|
+
} catch (err) {
|
|
21907
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21908
|
+
}
|
|
21909
|
+
}
|
|
21910
|
+
);
|
|
21911
|
+
server.tool(
|
|
21912
|
+
"gcp_release_access",
|
|
21913
|
+
"Voluntarily release a GCP grant before its TTL expires. Idempotent \u2014 safe on already-revoked or already-expired grants. Returns { grant_id, status }. Note: in v1 this marks the grant revoked in the broker DB and tears down the secret_ref; the already-issued access_token remains valid in GCP until expires_at. For true in-flight kill, an operator can disable the agent service account via the team kill switch (see PRD \xA76.5)." + NO_DIRECT_HTTP_BLOCK,
|
|
21914
|
+
gcpReleaseAccessShape,
|
|
21915
|
+
async (args) => {
|
|
21916
|
+
try {
|
|
21917
|
+
const result = await broker.gcpReleaseAccess(args);
|
|
21918
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21919
|
+
} catch (err) {
|
|
21920
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21921
|
+
}
|
|
21922
|
+
}
|
|
21923
|
+
);
|
|
21924
|
+
server.tool(
|
|
21925
|
+
"gcp_get_credentials",
|
|
21926
|
+
"Fetch the GCP access token for an active grant. Call AFTER gcp_request_access returns active OR after the resolution-notification arrives. Returns { grant_id, expires_at, credentials: { access_token } }. Use by prefixing your bash invocation, e.g. `CLOUDSDK_AUTH_ACCESS_TOKEN=... gcloud storage ls gs://reports/` \u2014 gcloud, gsutil, and bq all pick it up automatically. The grant must still be active and unexpired \u2014 409 if already released, 410 if past expires_at. Safe to call multiple times within the TTL window. Call gcp_release_access when done." + NO_DIRECT_HTTP_BLOCK,
|
|
21927
|
+
gcpGetCredentialsShape,
|
|
21928
|
+
async (args) => {
|
|
21929
|
+
try {
|
|
21930
|
+
const result = await broker.gcpGetCredentials(args);
|
|
21931
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21932
|
+
} catch (err) {
|
|
21933
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21934
|
+
}
|
|
21935
|
+
}
|
|
21936
|
+
);
|
|
21937
|
+
server.tool(
|
|
21938
|
+
"gcp_list_accounts",
|
|
21939
|
+
`List the GCP projects your team currently has enrolled with the broker. Returns { projects: [{ project_id, display_name, description?, service_account_email? }] }. The boot-time inventory injected into gcp_request_access's description is frozen for this MCP process \u2014 call this tool when a project you expect to see is missing (e.g. an operator just re-enrolled one), to confirm the canonical display_name before firing gcp_request_access, or to recover from "no enrolment" errors that hint the inventory may be stale. No arguments \u2014 agent_id is filled from the host MCP env. Cheap and idempotent.` + NO_DIRECT_HTTP_BLOCK,
|
|
21940
|
+
gcpListAccountsShape,
|
|
21941
|
+
async () => {
|
|
21942
|
+
try {
|
|
21943
|
+
const result = await broker.gcpListInventory();
|
|
21944
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21945
|
+
} catch (err) {
|
|
21946
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21947
|
+
}
|
|
21948
|
+
}
|
|
21949
|
+
);
|
|
21950
|
+
server.tool(
|
|
21951
|
+
"supabase_describe_scope",
|
|
21952
|
+
"Return the team's resolved Supabase policy ceiling for a project: max_ttl_seconds, auto_approved_roles, allowed_schemas, and allow_write. Call before supabase_request_access if you're not sure what you're allowed to ask for \u2014 free, idempotent, writes nothing." + supabaseInventoryBlock + NO_DIRECT_SUPABASE_HTTP_BLOCK,
|
|
21953
|
+
supabaseDescribeScopeShape,
|
|
21954
|
+
async (args) => {
|
|
21955
|
+
try {
|
|
21956
|
+
const result = await broker.supabaseDescribeScope(args);
|
|
21957
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21958
|
+
} catch (err) {
|
|
21959
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21960
|
+
}
|
|
21961
|
+
}
|
|
21962
|
+
);
|
|
21963
|
+
server.tool(
|
|
21964
|
+
"supabase_preview_request",
|
|
21965
|
+
'Dry-run a candidate Supabase access request. Returns "auto_approve" / "route_to_approver" / "hard_deny" without dispatching anything or writing audit_log. Lets the agent self-tighten scope before triggering a human approval.' + NO_DIRECT_SUPABASE_HTTP_BLOCK,
|
|
21966
|
+
supabasePreviewRequestShape,
|
|
21967
|
+
async (args) => {
|
|
21968
|
+
try {
|
|
21969
|
+
const result = await broker.supabasePreviewRequest(args);
|
|
21970
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21971
|
+
} catch (err) {
|
|
21972
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21973
|
+
}
|
|
21974
|
+
}
|
|
21975
|
+
);
|
|
21976
|
+
server.tool(
|
|
21977
|
+
"supabase_check_approval_channel",
|
|
21978
|
+
'Verify the agent\'s Slack bot can post to the Supabase-approval channel for a project BEFORE calling supabase_request_access. Returns { ok, channel?, bot_user_handle?, reason? }. Same failure-mode taxonomy as aws_check_approval_channel \u2014 branch on reason: "channel_not_found"/"not_in_channel" \u2192 ask user to `/invite @<bot_user_handle>` in the approvals channel; "archived" \u2192 ask admin to repoint; "no_approval_channel_configured" \u2192 ask user to configure one in Team Settings; "agent_slack_not_configured" \u2192 escalate to operator; "auth_failed"/"unknown" \u2192 escalate. ALWAYS call this on the FIRST supabase_request_access for a given project_ref in a session. If ok=false, DO NOT call supabase_request_access. Same jargon-free, no-mechanics rules \u2014 talk about the Supabase project and the invite action, never "broker", "grant", "MCP".' + supabaseInventoryBlock + NO_DIRECT_SUPABASE_HTTP_BLOCK,
|
|
21979
|
+
supabaseCheckApprovalChannelShape,
|
|
21980
|
+
async (args) => {
|
|
21981
|
+
try {
|
|
21982
|
+
const result = await broker.supabaseCheckApprovalChannel(args);
|
|
21983
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21984
|
+
} catch (err) {
|
|
21985
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21986
|
+
}
|
|
21987
|
+
}
|
|
21988
|
+
);
|
|
21989
|
+
server.tool(
|
|
21990
|
+
"supabase_request_access",
|
|
21991
|
+
`Request a short-lived Supabase JWT to query a Supabase project. CALL supabase_check_approval_channel FIRST on the FIRST request for a given project_ref in a session \u2014 if ok=false, surface the invite-the-bot message instead. "service_role" always routes to a human approver; "authenticated" may auto-approve if in the team's policy ceiling. agent_id and run_id default from host MCP env. Pass source_context so the resolution notification threads back to the original conversation. Returns { grant_id, status, expires_at?, credentials?, denial_reason?, notification_status?, notification_channel_name?, notification_permalink? }. status="active" \u2192 credentials are ready, call supabase_get_credentials. status="denied" \u2192 request rejected. status="pending" \u2192 DO NOT poll; broker pushes resolution via direct-chat. Post a brief jargon-free acknowledgement first; quote notification_channel_name VERBATIM if present ("pinged an admin in #<notification_channel_name> to approve"). Save grant_id and return control. NEVER expose broker mechanics ("grant", "JWT", "MCP", any supabase_* tool name) in user-facing prose \u2014 talk about the task and the Supabase project. When resolution lands via direct-chat, post a one-line acknowledgement in the original channel/thread BEFORE calling supabase_get_credentials. RETRY CONTRACT: if you have a cached grant_id, ALWAYS call supabase_poll_grant first to re-validate before claiming "request in flight".` + supabaseInventoryBlock + NO_DIRECT_SUPABASE_HTTP_BLOCK,
|
|
21992
|
+
supabaseRequestAccessShape,
|
|
21993
|
+
async (args) => {
|
|
21994
|
+
try {
|
|
21995
|
+
const result = await broker.supabaseRequestAccess(args);
|
|
21996
|
+
const safe = { ...result, credentials: void 0 };
|
|
21997
|
+
return { content: [{ type: "text", text: JSON.stringify(safe, null, 2) }] };
|
|
21998
|
+
} catch (err) {
|
|
21999
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
22000
|
+
}
|
|
22001
|
+
}
|
|
22002
|
+
);
|
|
22003
|
+
server.tool(
|
|
22004
|
+
"supabase_poll_grant",
|
|
22005
|
+
"Single-shot status check for a Supabase grant. The broker pushes resolution via direct-chat automatically \u2014 you do NOT need to poll in the normal flow. Escape hatch only. Returns { grant_id, status, expires_at?, denial_reason? }." + NO_DIRECT_SUPABASE_HTTP_BLOCK,
|
|
22006
|
+
supabasePollGrantShape,
|
|
22007
|
+
async (args) => {
|
|
22008
|
+
try {
|
|
22009
|
+
const result = await broker.supabasePollGrant(args);
|
|
22010
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
22011
|
+
} catch (err) {
|
|
22012
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
22013
|
+
}
|
|
22014
|
+
}
|
|
22015
|
+
);
|
|
22016
|
+
server.tool(
|
|
22017
|
+
"supabase_release_access",
|
|
22018
|
+
"Voluntarily release a Supabase grant before its TTL expires. Idempotent \u2014 safe on already-revoked or already-expired grants. Returns { grant_id, status }. Note: the already-issued JWT remains cryptographically valid until its exp claim \u2014 revocation marks it in the broker DB and prevents supabase_get_credentials from returning it again, but does not invalidate it at Supabase's JWT verification layer. Call this when done to signal intent; the TTL is the hard expiry." + NO_DIRECT_SUPABASE_HTTP_BLOCK,
|
|
22019
|
+
supabaseReleaseAccessShape,
|
|
22020
|
+
async (args) => {
|
|
22021
|
+
try {
|
|
22022
|
+
const result = await broker.supabaseReleaseAccess(args);
|
|
22023
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
22024
|
+
} catch (err) {
|
|
22025
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
22026
|
+
}
|
|
22027
|
+
}
|
|
22028
|
+
);
|
|
22029
|
+
server.tool(
|
|
22030
|
+
"supabase_get_credentials",
|
|
22031
|
+
'Fetch the Supabase JWT for an active grant. Call AFTER supabase_request_access returns active OR after the resolution-notification arrives. Returns { grant_id, expires_at, credentials: { access_token, supabase_url, db_host, project_ref } }. Two usage patterns: (1) REST \u2014 curl -H "Authorization: Bearer <access_token>" <supabase_url>/rest/v1/<table>?select=*; (2) psql \u2014 psql "postgresql://postgres.<project_ref>:<access_token>@<db_host>:5432/postgres". The grant must still be active and unexpired \u2014 409 if already released, 410 if past expires_at. Safe to call multiple times within the TTL window. Call supabase_release_access when done.' + NO_DIRECT_SUPABASE_HTTP_BLOCK,
|
|
22032
|
+
supabaseGetCredentialsShape,
|
|
22033
|
+
async (args) => {
|
|
22034
|
+
try {
|
|
22035
|
+
const result = await broker.supabaseGetCredentials(args);
|
|
22036
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
22037
|
+
} catch (err) {
|
|
22038
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
22039
|
+
}
|
|
22040
|
+
}
|
|
22041
|
+
);
|
|
22042
|
+
server.tool(
|
|
22043
|
+
"supabase_list_projects",
|
|
22044
|
+
`List the Supabase projects your team currently has enrolled with the broker. Returns { projects: [{ project_ref, display_name, description?, supabase_url? }] }. The boot-time inventory injected into supabase_request_access's description is frozen for this MCP process \u2014 call this tool when a project you expect to see is missing, to confirm the canonical display_name, or to recover from "no enrolment" errors. No arguments \u2014 agent_id is filled from the host MCP env. Cheap and idempotent.` + NO_DIRECT_SUPABASE_HTTP_BLOCK,
|
|
22045
|
+
supabaseListProjectsShape,
|
|
22046
|
+
async () => {
|
|
22047
|
+
try {
|
|
22048
|
+
const result = await broker.supabaseListInventory();
|
|
22049
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
22050
|
+
} catch (err) {
|
|
22051
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
22052
|
+
}
|
|
22053
|
+
}
|
|
22054
|
+
);
|
|
21497
22055
|
var transport = new StdioServerTransport();
|
|
21498
22056
|
await server.connect(transport);
|
|
21499
22057
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@integrity-labs/cloud-broker",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Cloud Access Broker — MCP server that mints scoped, TTL-bounded cloud credentials per agent task. Ships AWS support (aws_request_access, aws_poll_grant, aws_release_access, aws_describe_scope, aws_preview_request, aws_get_credentials — STS AssumeRole under the hood); GCP, Azure, and Cloudflare land alongside in the same package as the broker grows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
"test": "vitest run",
|
|
28
28
|
"clean": "rm -rf dist",
|
|
29
29
|
"publish:templates": "bash cloudformation/publish.sh",
|
|
30
|
-
"check-published": "bash cloudformation/check-published.sh"
|
|
30
|
+
"check-published": "bash cloudformation/check-published.sh",
|
|
31
|
+
"publish:terraform": "bash terraform/publish.sh",
|
|
32
|
+
"check-published-terraform": "bash terraform/check-published.sh"
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
|
33
35
|
"@modelcontextprotocol/sdk": "1.27.1",
|