@integrity-labs/cloud-broker 0.6.1 → 0.6.3
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/dist/index.js +100 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21126,12 +21126,45 @@ var BrokerClient = class {
|
|
|
21126
21126
|
pollGrant(args) {
|
|
21127
21127
|
return this.request("GET", `/aws/grants/${encodeURIComponent(args.grant_id)}`);
|
|
21128
21128
|
}
|
|
21129
|
+
/**
|
|
21130
|
+
* ENG-4824: pre-flight check that the agent's Slack bot can actually
|
|
21131
|
+
* post to the AWS-approval channel configured for an enrolment. Mirror
|
|
21132
|
+
* of describeScope's GET-with-agent_id pattern — the API derives the
|
|
21133
|
+
* enrolment row and the agent's bot token server-side, calls Slack,
|
|
21134
|
+
* and returns the same shape as slack.channel_info.
|
|
21135
|
+
*/
|
|
21136
|
+
checkApprovalChannel(args) {
|
|
21137
|
+
if (!this.agentId) {
|
|
21138
|
+
throw makeBrokerError(400, "BrokerClient.checkApprovalChannel requires agentId \u2014 pass it in BrokerClientConfig");
|
|
21139
|
+
}
|
|
21140
|
+
return this.request(
|
|
21141
|
+
"GET",
|
|
21142
|
+
"/aws/approval-channel-status",
|
|
21143
|
+
{ query: { account_id: args.account_id, agent_id: this.agentId } }
|
|
21144
|
+
);
|
|
21145
|
+
}
|
|
21129
21146
|
releaseAccess(args) {
|
|
21130
21147
|
return this.request(
|
|
21131
21148
|
"POST",
|
|
21132
21149
|
`/aws/grants/${encodeURIComponent(args.grant_id)}/release`
|
|
21133
21150
|
);
|
|
21134
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
|
+
}
|
|
21135
21168
|
// ENG-4779: fetch the AWS_* credentials persisted on the grant. Pre-4779
|
|
21136
21169
|
// there was no path back to credentials for a route_to_approver grant —
|
|
21137
21170
|
// they were minted inside mintAndActivateGrant and never returned. Now
|
|
@@ -21144,6 +21177,24 @@ var BrokerClient = class {
|
|
|
21144
21177
|
}
|
|
21145
21178
|
};
|
|
21146
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
|
+
|
|
21147
21198
|
// src/tool-schemas.ts
|
|
21148
21199
|
var accountIdSchema = external_exports.string().regex(/^\d{12}$/, "AWS account_id must be a 12-digit string");
|
|
21149
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.");
|
|
@@ -21204,17 +21255,21 @@ var releaseAccessSchema = external_exports.object({
|
|
|
21204
21255
|
var getCredentialsSchema = external_exports.object({
|
|
21205
21256
|
grant_id: external_exports.string().uuid("grant_id must be a UUID")
|
|
21206
21257
|
});
|
|
21258
|
+
var checkApprovalChannelSchema = external_exports.object({
|
|
21259
|
+
account_id: accountIdSchema
|
|
21260
|
+
});
|
|
21207
21261
|
var describeScopeShape = describeScopeSchema.shape;
|
|
21208
21262
|
var previewRequestShape = previewRequestSchema.shape;
|
|
21209
21263
|
var requestAccessShape = requestAccessSchema.shape;
|
|
21210
21264
|
var pollGrantShape = pollGrantSchema.shape;
|
|
21211
21265
|
var releaseAccessShape = releaseAccessSchema.shape;
|
|
21212
21266
|
var getCredentialsShape = getCredentialsSchema.shape;
|
|
21267
|
+
var checkApprovalChannelShape = checkApprovalChannelSchema.shape;
|
|
21213
21268
|
|
|
21214
21269
|
// package.json
|
|
21215
21270
|
var package_default = {
|
|
21216
21271
|
name: "@integrity-labs/cloud-broker",
|
|
21217
|
-
version: "0.6.
|
|
21272
|
+
version: "0.6.3",
|
|
21218
21273
|
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.",
|
|
21219
21274
|
type: "module",
|
|
21220
21275
|
bin: {
|
|
@@ -21293,9 +21348,34 @@ var server = new McpServer({
|
|
|
21293
21348
|
name: "cloud-broker",
|
|
21294
21349
|
version: package_default.version
|
|
21295
21350
|
});
|
|
21351
|
+
var INVENTORY_FETCH_TIMEOUT_MS = 3e3;
|
|
21352
|
+
async function loadInventoryBlock() {
|
|
21353
|
+
const ctl = new AbortController();
|
|
21354
|
+
const timer = setTimeout(() => ctl.abort(), INVENTORY_FETCH_TIMEOUT_MS);
|
|
21355
|
+
try {
|
|
21356
|
+
const result = await Promise.race([
|
|
21357
|
+
broker.listInventory(),
|
|
21358
|
+
new Promise((_, reject) => {
|
|
21359
|
+
ctl.signal.addEventListener(
|
|
21360
|
+
"abort",
|
|
21361
|
+
() => reject(new Error(`aws_inventory_lookup timed out after ${INVENTORY_FETCH_TIMEOUT_MS}ms`))
|
|
21362
|
+
);
|
|
21363
|
+
})
|
|
21364
|
+
]);
|
|
21365
|
+
return renderInventoryBlock(result.accounts);
|
|
21366
|
+
} catch (err) {
|
|
21367
|
+
console.error(
|
|
21368
|
+
`cloud-broker: aws_inventory_lookup_failed (non-fatal \u2014 tool descriptions render account-agnostic): ${formatBrokerError(err)}`
|
|
21369
|
+
);
|
|
21370
|
+
return "";
|
|
21371
|
+
} finally {
|
|
21372
|
+
clearTimeout(timer);
|
|
21373
|
+
}
|
|
21374
|
+
}
|
|
21375
|
+
var inventoryBlock = await loadInventoryBlock();
|
|
21296
21376
|
server.tool(
|
|
21297
21377
|
"aws_describe_scope",
|
|
21298
|
-
"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.",
|
|
21378
|
+
"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,
|
|
21299
21379
|
describeScopeShape,
|
|
21300
21380
|
async (args) => {
|
|
21301
21381
|
try {
|
|
@@ -21319,9 +21399,26 @@ server.tool(
|
|
|
21319
21399
|
}
|
|
21320
21400
|
}
|
|
21321
21401
|
);
|
|
21402
|
+
server.tool(
|
|
21403
|
+
"aws_check_approval_channel",
|
|
21404
|
+
`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,
|
|
21405
|
+
checkApprovalChannelShape,
|
|
21406
|
+
async (args) => {
|
|
21407
|
+
try {
|
|
21408
|
+
const result = await broker.checkApprovalChannel(args);
|
|
21409
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21410
|
+
} catch (err) {
|
|
21411
|
+
return { content: [{ type: "text", text: formatBrokerError(err) }], isError: true };
|
|
21412
|
+
}
|
|
21413
|
+
}
|
|
21414
|
+
);
|
|
21322
21415
|
server.tool(
|
|
21323
21416
|
"aws_request_access",
|
|
21324
|
-
|
|
21417
|
+
// ENG-5013: append inventoryBlock so the model sees the available
|
|
21418
|
+
// accounts inline with the tool that consumes account_id. This is the
|
|
21419
|
+
// call site where a wrong/invented account_id would actually do harm
|
|
21420
|
+
// (route to a nonexistent enrolment → 404 → confusing failure mode).
|
|
21421
|
+
'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,
|
|
21325
21422
|
requestAccessShape,
|
|
21326
21423
|
async (args) => {
|
|
21327
21424
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@integrity-labs/cloud-broker",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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": {
|