@integrity-labs/cloud-broker 0.7.11 → 0.7.13

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 +34 -3
  3. package/package.json +2 -2
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);
@@ -21644,7 +21675,7 @@ var supabaseRestShape = supabaseRestSchema.shape;
21644
21675
  // package.json
21645
21676
  var package_default = {
21646
21677
  name: "@integrity-labs/cloud-broker",
21647
- version: "0.7.11",
21678
+ version: "0.7.13",
21648
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.",
21649
21680
  type: "module",
21650
21681
  bin: {
@@ -21667,7 +21698,7 @@ var package_default = {
21667
21698
  scripts: {
21668
21699
  build: "tsup",
21669
21700
  dev: "tsx watch src/index.ts",
21670
- typecheck: "tsc --noEmit",
21701
+ typecheck: "bash ../../scripts/typecheck-guard.sh tsc --noEmit",
21671
21702
  test: "vitest run",
21672
21703
  clean: "rm -rf dist",
21673
21704
  "publish:templates": "bash cloudformation/publish.sh",
@@ -22137,7 +22168,7 @@ server.tool(
22137
22168
  );
22138
22169
  server.tool(
22139
22170
  "supabase_query",
22140
- "Run READ-ONLY SQL against a Supabase project for an ACTIVE grant. THIS is how you query Supabase - you do NOT get or hold a database credential, and the supabase_get_credentials JWT cannot run SQL. The broker executes your SQL server-side under the grant's scoped database role, inside a READ ONLY transaction with a statement timeout, and returns rows. Args: { grant_id, sql }. Returns on success { grant_id, ok: true, rows, row_count, truncated } (truncated=true means the row cap was hit - add LIMIT/aggregate). On failure returns { ok: false, error_code, error } where error_code is one of READ_ONLY (you attempted a write - this capability is read-only), SCOPE_DENIED (your grant's role can't access that object - try supabase_describe to see your scope), OBJECT_NOT_FOUND (no such relation/schema, or not visible to your grant), TIMEOUT (query too slow), SYNTAX_ERROR, UNREACHABLE, SQL_NOT_CONFIGURED (no scoped DB credential enrolled - tell the user to ask their operator to configure it), or a grant-state code GRANT_NOT_ACTIVE / GRANT_EXPIRED / GRANT_RELEASED (re-request access). Writes are always rejected. Call supabase_describe first if unsure what you can read. All of these come back in the { ok:false, error_code } envelope on a normal response - branch on error_code, do not treat them as tool failures." + NO_DIRECT_SUPABASE_HTTP_BLOCK,
22171
+ "Run READ-ONLY SQL against a Supabase project for an ACTIVE grant. THIS is how you query Supabase - you do NOT get or hold a database credential, and the supabase_get_credentials JWT cannot run SQL. The broker executes your SQL server-side under the grant's scoped database role, inside a READ ONLY transaction with a statement timeout, and returns rows. Args: { grant_id, sql }. Returns on success { grant_id, ok: true, rows, row_count, truncated } (truncated=true means the row cap was hit - add LIMIT/aggregate). On failure returns { ok: false, error_code, error } where error_code is one of READ_ONLY (you attempted a write - this capability is read-only), SCOPE_DENIED (your grant's role can't access that object - try supabase_describe to see your scope), OBJECT_NOT_FOUND (no such relation/schema, or not visible to your grant), TIMEOUT (query too slow), SYNTAX_ERROR, AUTH_FAILED (the database rejected the enrolled credentials - not something you can fix, tell the user to ask their operator to re-check the stored connection string), UNREACHABLE (the broker cannot reach the database over the network - also an operator fix, not a query fix), SQL_NOT_CONFIGURED (no scoped DB credential enrolled - tell the user to ask their operator to configure it), or a grant-state code GRANT_NOT_ACTIVE / GRANT_EXPIRED / GRANT_RELEASED (re-request access). Writes are always rejected. Call supabase_describe first if unsure what you can read. All of these come back in the { ok:false, error_code } envelope on a normal response - branch on error_code, do not treat them as tool failures." + NO_DIRECT_SUPABASE_HTTP_BLOCK,
22141
22172
  supabaseQueryShape,
22142
22173
  async (args) => {
22143
22174
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/cloud-broker",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
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": {
@@ -23,7 +23,7 @@
23
23
  "scripts": {
24
24
  "build": "tsup",
25
25
  "dev": "tsx watch src/index.ts",
26
- "typecheck": "tsc --noEmit",
26
+ "typecheck": "bash ../../scripts/typecheck-guard.sh tsc --noEmit",
27
27
  "test": "vitest run",
28
28
  "clean": "rm -rf dist",
29
29
  "publish:templates": "bash cloudformation/publish.sh",