@integrity-labs/cloud-broker 0.6.2 → 0.6.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.
Files changed (2) hide show
  1. package/dist/index.js +89 -11
  2. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -21149,6 +21149,22 @@ var BrokerClient = class {
21149
21149
  `/aws/grants/${encodeURIComponent(args.grant_id)}/release`
21150
21150
  );
21151
21151
  }
21152
+ /**
21153
+ * ENG-5013: agent-scoped inventory of enrolled AWS accounts. Called by
21154
+ * the MCP server once at startup so the tool descriptions can name the
21155
+ * available accounts inline — agents no longer have to ask the user
21156
+ * for a 12-digit account_id when an alias is configured.
21157
+ */
21158
+ listInventory() {
21159
+ if (!this.agentId) {
21160
+ throw makeBrokerError(400, "BrokerClient.listInventory requires agentId \u2014 pass it in BrokerClientConfig");
21161
+ }
21162
+ return this.request(
21163
+ "GET",
21164
+ "/aws/inventory",
21165
+ { query: { agent_id: this.agentId } }
21166
+ );
21167
+ }
21152
21168
  // ENG-4779: fetch the AWS_* credentials persisted on the grant. Pre-4779
21153
21169
  // there was no path back to credentials for a route_to_approver grant —
21154
21170
  // they were minted inside mintAndActivateGrant and never returned. Now
@@ -21161,6 +21177,24 @@ var BrokerClient = class {
21161
21177
  }
21162
21178
  };
21163
21179
 
21180
+ // src/inventory.ts
21181
+ var INVENTORY_RENDER_LIMIT = 20;
21182
+ function renderInventoryBlock(accounts) {
21183
+ if (accounts.length === 0) return "";
21184
+ const shown = accounts.slice(0, INVENTORY_RENDER_LIMIT);
21185
+ const lines = shown.map((a) => {
21186
+ const region = a.default_region ? ` (default region: ${a.default_region})` : "";
21187
+ return ` - \`${a.account_id}\` \u2014 ${a.display_name}${region}`;
21188
+ });
21189
+ const truncated = accounts.length > INVENTORY_RENDER_LIMIT ? `
21190
+
21191
+ \u2026and ${accounts.length - INVENTORY_RENDER_LIMIT} more \u2014 call \`aws_describe_scope\` for any unlisted account.` : "";
21192
+ return `
21193
+
21194
+ Available accounts for this team (use the 12-digit account_id, NEVER invent one):
21195
+ ${lines.join("\n")}${truncated}`;
21196
+ }
21197
+
21164
21198
  // src/tool-schemas.ts
21165
21199
  var accountIdSchema = external_exports.string().regex(/^\d{12}$/, "AWS account_id must be a 12-digit string");
21166
21200
  var ttlSecondsSchema = external_exports.number().int().min(300, "ttl_seconds minimum is 300 (5 min)").max(3600, "ttl_seconds maximum is 3600 (60 min)").optional().describe("Requested TTL in seconds. Defaults to 900 (15 min) on the broker if omitted.");
@@ -21224,6 +21258,7 @@ var getCredentialsSchema = external_exports.object({
21224
21258
  var checkApprovalChannelSchema = external_exports.object({
21225
21259
  account_id: accountIdSchema
21226
21260
  });
21261
+ var listAccountsSchema = external_exports.object({});
21227
21262
  var describeScopeShape = describeScopeSchema.shape;
21228
21263
  var previewRequestShape = previewRequestSchema.shape;
21229
21264
  var requestAccessShape = requestAccessSchema.shape;
@@ -21231,11 +21266,12 @@ var pollGrantShape = pollGrantSchema.shape;
21231
21266
  var releaseAccessShape = releaseAccessSchema.shape;
21232
21267
  var getCredentialsShape = getCredentialsSchema.shape;
21233
21268
  var checkApprovalChannelShape = checkApprovalChannelSchema.shape;
21269
+ var listAccountsShape = listAccountsSchema.shape;
21234
21270
 
21235
21271
  // package.json
21236
21272
  var package_default = {
21237
21273
  name: "@integrity-labs/cloud-broker",
21238
- version: "0.6.2",
21274
+ version: "0.6.4",
21239
21275
  description: "Cloud Access Broker \u2014 MCP server that mints scoped, TTL-bounded cloud credentials per agent task. v1 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.",
21240
21276
  type: "module",
21241
21277
  bin: {
@@ -21264,15 +21300,15 @@ var package_default = {
21264
21300
  "publish:templates": "bash cloudformation/publish.sh"
21265
21301
  },
21266
21302
  dependencies: {
21267
- "@modelcontextprotocol/sdk": "^1.27.1",
21268
- zod: "^3.25.0"
21303
+ "@modelcontextprotocol/sdk": "1.27.1",
21304
+ zod: "3.25.76"
21269
21305
  },
21270
21306
  devDependencies: {
21271
- "@types/node": "^22.0.0",
21272
- tsup: "^8.0.0",
21273
- tsx: "^4.19.0",
21274
- typescript: "^5.7.0",
21275
- vitest: "^3.0.0"
21307
+ "@types/node": "22.19.11",
21308
+ tsup: "8.5.1",
21309
+ tsx: "4.21.0",
21310
+ typescript: "5.9.3",
21311
+ vitest: "3.2.4"
21276
21312
  }
21277
21313
  };
21278
21314
 
@@ -21314,9 +21350,34 @@ var server = new McpServer({
21314
21350
  name: "cloud-broker",
21315
21351
  version: package_default.version
21316
21352
  });
21353
+ var INVENTORY_FETCH_TIMEOUT_MS = 3e3;
21354
+ async function loadInventoryBlock() {
21355
+ const ctl = new AbortController();
21356
+ const timer = setTimeout(() => ctl.abort(), INVENTORY_FETCH_TIMEOUT_MS);
21357
+ try {
21358
+ const result = await Promise.race([
21359
+ broker.listInventory(),
21360
+ new Promise((_, reject) => {
21361
+ ctl.signal.addEventListener(
21362
+ "abort",
21363
+ () => reject(new Error(`aws_inventory_lookup timed out after ${INVENTORY_FETCH_TIMEOUT_MS}ms`))
21364
+ );
21365
+ })
21366
+ ]);
21367
+ return renderInventoryBlock(result.accounts);
21368
+ } catch (err) {
21369
+ console.error(
21370
+ `cloud-broker: aws_inventory_lookup_failed (non-fatal \u2014 tool descriptions render account-agnostic): ${formatBrokerError(err)}`
21371
+ );
21372
+ return "";
21373
+ } finally {
21374
+ clearTimeout(timer);
21375
+ }
21376
+ }
21377
+ var inventoryBlock = await loadInventoryBlock();
21317
21378
  server.tool(
21318
21379
  "aws_describe_scope",
21319
- "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.",
21380
+ "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,
21320
21381
  describeScopeShape,
21321
21382
  async (args) => {
21322
21383
  try {
@@ -21342,7 +21403,7 @@ server.tool(
21342
21403
  );
21343
21404
  server.tool(
21344
21405
  "aws_check_approval_channel",
21345
- `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.`,
21406
+ `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,
21346
21407
  checkApprovalChannelShape,
21347
21408
  async (args) => {
21348
21409
  try {
@@ -21355,7 +21416,11 @@ server.tool(
21355
21416
  );
21356
21417
  server.tool(
21357
21418
  "aws_request_access",
21358
- '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.',
21419
+ // ENG-5013: append inventoryBlock so the model sees the available
21420
+ // accounts inline with the tool that consumes account_id. This is the
21421
+ // call site where a wrong/invented account_id would actually do harm
21422
+ // (route to a nonexistent enrolment → 404 → confusing failure mode).
21423
+ '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,
21359
21424
  requestAccessShape,
21360
21425
  async (args) => {
21361
21426
  try {
@@ -21406,6 +21471,19 @@ server.tool(
21406
21471
  }
21407
21472
  }
21408
21473
  );
21474
+ server.tool(
21475
+ "aws_list_accounts",
21476
+ `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.`,
21477
+ listAccountsShape,
21478
+ async () => {
21479
+ try {
21480
+ const result = await broker.listInventory();
21481
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
21482
+ } catch (err) {
21483
+ return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
21484
+ }
21485
+ }
21486
+ );
21409
21487
  var transport = new StdioServerTransport();
21410
21488
  await server.connect(transport);
21411
21489
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/cloud-broker",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Cloud Access Broker — MCP server that mints scoped, TTL-bounded cloud credentials per agent task. v1 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": {
@@ -29,14 +29,14 @@
29
29
  "publish:templates": "bash cloudformation/publish.sh"
30
30
  },
31
31
  "dependencies": {
32
- "@modelcontextprotocol/sdk": "^1.27.1",
33
- "zod": "^3.25.0"
32
+ "@modelcontextprotocol/sdk": "1.27.1",
33
+ "zod": "3.25.76"
34
34
  },
35
35
  "devDependencies": {
36
- "@types/node": "^22.0.0",
37
- "tsup": "^8.0.0",
38
- "tsx": "^4.19.0",
39
- "typescript": "^5.7.0",
40
- "vitest": "^3.0.0"
36
+ "@types/node": "22.19.11",
37
+ "tsup": "8.5.1",
38
+ "tsx": "4.21.0",
39
+ "typescript": "5.9.3",
40
+ "vitest": "3.2.4"
41
41
  }
42
42
  }