@integrity-labs/cloud-broker 0.7.10 → 0.7.12
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 +24 -18
- package/dist/index.js +62 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,9 @@ See the PRDs ([AWS](../../docs/prds/aws-ephemeral-access.md), [GCP](../../docs/p
|
|
|
39
39
|
|
|
40
40
|
### Supabase
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Since ENG-7326 the agent **holds no credential**: it queries Supabase through server-side proxy tools that run the call under the grant's scope. `supabase_rest` (PostgREST, read + write) is the primary path; `supabase_query` runs read-only SQL. The operator stores, on the enrolment, the project's REST api_key (a **new-format `sb_secret_…` key** - legacy anon/service_role keys are disabled) and/or a scoped read-only Postgres connection string; the broker injects whichever the call needs at exec time. The scope (`allowed_tables` / `read_only`) is enforced at the broker, independent of RLS.
|
|
43
|
+
|
|
44
|
+
The legacy `supabase_get_credentials` still mints a short-lived Supabase JWT for direct PostgREST/`psql` use, but the proxy tools are preferred (they never hand the agent a secret, and they don't depend on RLS to enforce scope).
|
|
43
45
|
|
|
44
46
|
| Tool | Purpose |
|
|
45
47
|
|---|---|
|
|
@@ -47,12 +49,15 @@ Credentials returned are a short-lived **Supabase JWT** (+ `supabase_url`, `db_h
|
|
|
47
49
|
| `supabase_preview_request` | Dry-run a candidate request. Writes nothing. |
|
|
48
50
|
| `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
51
|
| `supabase_poll_grant` | Single-shot status check. Escape hatch. |
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
+
| `supabase_describe` | Inspect what an **active grant** can do (role, allowed schemas/tables, read_only, and whether server-side SQL is enrolled) before querying. |
|
|
53
|
+
| `supabase_rest` | **Primary.** Call the project's PostgREST API (read + write) server-side under the grant's scope. The broker injects the api_key; the agent holds nothing. `rpc/` is blocked. |
|
|
54
|
+
| `supabase_query` | Run **read-only SQL** server-side under the grant's scoped DB role (READ ONLY transaction + statement timeout). For system tables / joins REST can't express. Requires a DB connection string on the enrolment. |
|
|
55
|
+
| `supabase_get_credentials` | **Legacy.** Fetch a short-lived Supabase JWT + connection details for an active grant (direct PostgREST/`psql`). Prefer the proxy tools. |
|
|
56
|
+
| `supabase_release_access` | Voluntarily release a grant before TTL. Idempotent. (For the legacy JWT path the token stays valid until its `exp`.) |
|
|
52
57
|
| `supabase_check_approval_channel` | Pre-flight Slack channel reachability for the project's approval channel. |
|
|
53
58
|
| `supabase_list_projects` | Live inventory of enrolled Supabase projects. |
|
|
54
59
|
|
|
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
|
|
60
|
+
See **[`docs/runbooks/supabase-broker-enrollment.md`](../../docs/runbooks/supabase-broker-enrollment.md)** for operator enrollment (storing the REST api_key + DB connection string), the policy-ceiling shape, and the legacy `augmented` JWT-claim → RLS-policy enforcement guide.
|
|
56
61
|
|
|
57
62
|
## Environment
|
|
58
63
|
|
|
@@ -180,27 +185,28 @@ supabase_request_access({
|
|
|
180
185
|
ttl_seconds: 900,
|
|
181
186
|
reason: "read the agents + runs tables to answer the user's status question"
|
|
182
187
|
})
|
|
183
|
-
// → { "grant_id": "...", "status": "active", "
|
|
188
|
+
// → { "grant_id": "...", "status": "active", "expires_at": "..." }
|
|
184
189
|
// `service_role` or write access (read_only:false, unless the project allows writes)
|
|
185
190
|
// returns "pending" instead and pages a human approver.
|
|
186
191
|
|
|
187
|
-
// 4.
|
|
188
|
-
|
|
189
|
-
// → {
|
|
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"
|
|
192
|
+
// 4. Learn your scope (recommended - also tells you if server-side SQL is enrolled).
|
|
193
|
+
supabase_describe({ grant_id: "..." })
|
|
194
|
+
// → { role, allowed_schemas, allowed_tables, read_only, sql_query: { available, ... } }
|
|
198
195
|
|
|
199
|
-
// 5.
|
|
196
|
+
// 5. Query. You hold NO credential - the broker runs the call server-side and injects the key.
|
|
197
|
+
// REST (primary, read + write):
|
|
198
|
+
supabase_rest({ grant_id: "...", method: "GET", path: "agents?select=*&limit=10" })
|
|
199
|
+
// → { ok: true, status: 200, body: [ ... ] }
|
|
200
|
+
// Read-only SQL (system tables / joins REST can't express; needs a DB connection string enrolled):
|
|
201
|
+
supabase_query({ grant_id: "...", sql: "select count(*) from runs" })
|
|
202
|
+
// → { ok: true, rows: [{ count: 42 }], row_count: 1 }
|
|
203
|
+
// Both return { ok: false, error_code } on a denial - branch on error_code, don't treat it as a tool failure.
|
|
204
|
+
|
|
205
|
+
// 6. (Optional) release early.
|
|
200
206
|
supabase_release_access({ grant_id: "..." })
|
|
201
207
|
```
|
|
202
208
|
|
|
203
|
-
|
|
209
|
+
With the proxy tools the grant's scope (`allowed_tables` / `read_only`) is enforced **at the broker** - read-only grants reject writes, calls outside `allowed_tables` are denied, and `rpc/` is blocked - independent of RLS. (For the legacy `supabase_get_credentials` JWT path, scope instead rides the JWT's `augmented` claim and is only enforced if the project's RLS policies read it - see the [enrollment runbook](../../docs/runbooks/supabase-broker-enrollment.md#enforcing-the-grant-scope-with-rls) for those policy patterns.) Same `pending` / direct-chat-push resolution semantics as AWS.
|
|
204
210
|
|
|
205
211
|
## Running locally
|
|
206
212
|
|
package/dist/index.js
CHANGED
|
@@ -20987,10 +20987,41 @@ var StdioServerTransport = class {
|
|
|
20987
20987
|
|
|
20988
20988
|
// src/turn-initiator-marker.ts
|
|
20989
20989
|
import { readFileSync } from "fs";
|
|
20990
|
+
import { dirname, join } from "path";
|
|
20990
20991
|
var TURN_INITIATOR_MAX_AGE_MS = 5 * 60 * 1e3;
|
|
20992
|
+
var TURN_INITIATOR_LEDGER_FILENAME = ".turn-initiator-ledger.json";
|
|
20993
|
+
function readTurnInitiatorFromLedger(file, maxAgeMs) {
|
|
20994
|
+
const ledgerFile = join(dirname(file), TURN_INITIATOR_LEDGER_FILENAME);
|
|
20995
|
+
let entries;
|
|
20996
|
+
try {
|
|
20997
|
+
const parsed = JSON.parse(readFileSync(ledgerFile, "utf8"));
|
|
20998
|
+
if (!parsed || parsed.v !== 1 || !Array.isArray(parsed.entries)) return "no-ledger";
|
|
20999
|
+
entries = parsed.entries;
|
|
21000
|
+
} catch {
|
|
21001
|
+
return "no-ledger";
|
|
21002
|
+
}
|
|
21003
|
+
const now = Date.now();
|
|
21004
|
+
const fresh = entries.filter(
|
|
21005
|
+
(e) => e && typeof e.channel === "string" && e.channel && typeof e.sender_id === "string" && e.sender_id && typeof e.ts === "number" && Number.isFinite(e.ts) && now - e.ts >= 0 && now - e.ts <= maxAgeMs
|
|
21006
|
+
);
|
|
21007
|
+
if (fresh.length === 1) {
|
|
21008
|
+
const e = fresh[0];
|
|
21009
|
+
return { channel: e.channel, sender_id: e.sender_id };
|
|
21010
|
+
}
|
|
21011
|
+
if (fresh.length >= 2) {
|
|
21012
|
+
process.stderr.write(
|
|
21013
|
+
`turn-initiator: ambiguous (${fresh.length} fresh senders), withholding initiator
|
|
21014
|
+
`
|
|
21015
|
+
);
|
|
21016
|
+
return null;
|
|
21017
|
+
}
|
|
21018
|
+
return "no-ledger";
|
|
21019
|
+
}
|
|
20991
21020
|
function readTurnInitiator(maxAgeMs = TURN_INITIATOR_MAX_AGE_MS) {
|
|
20992
21021
|
const file = process.env["AGT_TURN_INITIATOR_FILE"];
|
|
20993
21022
|
if (!file) return null;
|
|
21023
|
+
const fromLedger = readTurnInitiatorFromLedger(file, maxAgeMs);
|
|
21024
|
+
if (fromLedger !== "no-ledger") return fromLedger;
|
|
20994
21025
|
try {
|
|
20995
21026
|
const raw = readFileSync(file, "utf8");
|
|
20996
21027
|
const m = JSON.parse(raw);
|
|
@@ -21380,6 +21411,16 @@ var BrokerClient = class {
|
|
|
21380
21411
|
{ query: this.agentId ? { agent_id: this.agentId } : void 0 }
|
|
21381
21412
|
);
|
|
21382
21413
|
}
|
|
21414
|
+
supabaseRest(args) {
|
|
21415
|
+
return this.request(
|
|
21416
|
+
"POST",
|
|
21417
|
+
`/supabase/grants/${encodeURIComponent(args.grant_id)}/rest`,
|
|
21418
|
+
{
|
|
21419
|
+
query: this.agentId ? { agent_id: this.agentId } : void 0,
|
|
21420
|
+
body: { method: args.method, path: args.path, ...args.body !== void 0 ? { body: args.body } : {} }
|
|
21421
|
+
}
|
|
21422
|
+
);
|
|
21423
|
+
}
|
|
21383
21424
|
};
|
|
21384
21425
|
|
|
21385
21426
|
// src/inventory.ts
|
|
@@ -21613,6 +21654,12 @@ var supabaseQuerySchema = external_exports.object({
|
|
|
21613
21654
|
var supabaseDescribeSchema = external_exports.object({
|
|
21614
21655
|
grant_id: external_exports.string().uuid("grant_id must be a UUID")
|
|
21615
21656
|
});
|
|
21657
|
+
var supabaseRestSchema = external_exports.object({
|
|
21658
|
+
grant_id: external_exports.string().uuid("grant_id must be a UUID"),
|
|
21659
|
+
method: external_exports.enum(["GET", "HEAD", "POST", "PATCH", "PUT", "DELETE"]).describe("HTTP method. Writes require a non-read-only grant."),
|
|
21660
|
+
path: external_exports.string().min(1, "path is required").describe('PostgREST-relative path, e.g. "orders?select=*&limit=10". Do NOT include /rest/v1/.'),
|
|
21661
|
+
body: external_exports.unknown().optional().describe("JSON body for POST/PATCH/PUT.")
|
|
21662
|
+
});
|
|
21616
21663
|
var supabaseDescribeScopeShape = supabaseDescribeScopeSchema.shape;
|
|
21617
21664
|
var supabasePreviewRequestShape = supabasePreviewRequestSchema.shape;
|
|
21618
21665
|
var supabaseRequestAccessShape = supabaseRequestAccessSchema.shape;
|
|
@@ -21623,11 +21670,12 @@ var supabaseCheckApprovalChannelShape = supabaseCheckApprovalChannelSchema.shape
|
|
|
21623
21670
|
var supabaseListProjectsShape = supabaseListProjectsSchema.shape;
|
|
21624
21671
|
var supabaseQueryShape = supabaseQuerySchema.shape;
|
|
21625
21672
|
var supabaseDescribeShape = supabaseDescribeSchema.shape;
|
|
21673
|
+
var supabaseRestShape = supabaseRestSchema.shape;
|
|
21626
21674
|
|
|
21627
21675
|
// package.json
|
|
21628
21676
|
var package_default = {
|
|
21629
21677
|
name: "@integrity-labs/cloud-broker",
|
|
21630
|
-
version: "0.7.
|
|
21678
|
+
version: "0.7.12",
|
|
21631
21679
|
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.",
|
|
21632
21680
|
type: "module",
|
|
21633
21681
|
bin: {
|
|
@@ -22131,6 +22179,19 @@ server.tool(
|
|
|
22131
22179
|
}
|
|
22132
22180
|
}
|
|
22133
22181
|
);
|
|
22182
|
+
server.tool(
|
|
22183
|
+
"supabase_rest",
|
|
22184
|
+
'Call a Supabase project\'s REST (PostgREST) API for an ACTIVE grant - the primary way to read AND write your app data. You do NOT hold a key; the broker injects the project api_key and runs the call server-side under your grant\'s scope. Args: { grant_id, method (GET/HEAD/POST/PATCH/PUT/DELETE), path, body? }. `path` is PostgREST-relative - e.g. "orders?select=*&limit=10", "orders?id=eq.7" - do NOT include /rest/v1/ or the host. Filtering/ordering/pagination use PostgREST query syntax. For writes, pass the row(s) as `body` (POST=insert, PATCH=update with a filter in path, DELETE=delete with a filter in path). Returns on success { grant_id, ok: true, status, body } where body is the PostgREST JSON (rows for reads, the affected rows for writes). On a denial returns { ok: false, error_code, error }: READ_ONLY (your grant is read-only - writes rejected), SCOPE_DENIED (table not in your grant\'s allowed tables, or rpc/ blocked), BAD_REQUEST (bad method/path), REST_NOT_CONFIGURED (no api_key enrolled - tell the user to ask their operator), GRANT_NOT_ACTIVE / GRANT_EXPIRED / GRANT_RELEASED, or EXECUTION_ERROR. A 4xx from PostgREST itself comes back as { ok:false, status, body } with the upstream error in body. Call supabase_describe first to see your allowed tables and whether REST is available. rpc/ calls are not permitted. Use supabase_query for raw SQL / system tables instead.' + NO_DIRECT_SUPABASE_HTTP_BLOCK,
|
|
22185
|
+
supabaseRestShape,
|
|
22186
|
+
async (args) => {
|
|
22187
|
+
try {
|
|
22188
|
+
const result = await broker.supabaseRest(args);
|
|
22189
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
22190
|
+
} catch (err) {
|
|
22191
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
22192
|
+
}
|
|
22193
|
+
}
|
|
22194
|
+
);
|
|
22134
22195
|
var transport = new StdioServerTransport();
|
|
22135
22196
|
await server.connect(transport);
|
|
22136
22197
|
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.12",
|
|
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": {
|