@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.
Files changed (3) hide show
  1. package/README.md +24 -18
  2. package/dist/index.js +62 -1
  3. 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
- 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.
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
- | `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_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, and the policy-ceiling shape.
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", "secret_ref": "supabase:jwt:<grant_id>", "expires_at": "..." }
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. 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"
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. (Optional) release early.
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
- 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.
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.10",
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.10",
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": {