@yawlabs/aws-mcp 1.4.1 → 1.5.0
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 +8 -0
- package/dist/index.js +998 -974
- package/dist/index.js.map +7 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -53507,7 +53507,7 @@ function setProfile(name) {
|
|
|
53507
53507
|
const trimmed = name.trim();
|
|
53508
53508
|
if (!isValidProfileName(trimmed)) {
|
|
53509
53509
|
throw new Error(
|
|
53510
|
-
`Invalid profile name '${trimmed}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]
|
|
53510
|
+
`Invalid profile name '${trimmed}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '='); no whitespace or shell metacharacters.`
|
|
53511
53511
|
);
|
|
53512
53512
|
}
|
|
53513
53513
|
sessionProfile = trimmed;
|
|
@@ -53546,9 +53546,10 @@ var MAX_ERROR_MSG_BYTES = 8 * 1024;
|
|
|
53546
53546
|
var SAFE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
53547
53547
|
function redactDisplayArgs(args) {
|
|
53548
53548
|
const out = [...args];
|
|
53549
|
-
|
|
53550
|
-
|
|
53551
|
-
|
|
53549
|
+
for (let i = 0; i < out.length - 1; i++) {
|
|
53550
|
+
if (out[i] === "--cli-input-json") {
|
|
53551
|
+
out[i + 1] = `<redacted len=${out[i + 1].length}>`;
|
|
53552
|
+
}
|
|
53552
53553
|
}
|
|
53553
53554
|
return out;
|
|
53554
53555
|
}
|
|
@@ -53612,7 +53613,7 @@ function runAwsCall(opts) {
|
|
|
53612
53613
|
return Promise.resolve({
|
|
53613
53614
|
ok: false,
|
|
53614
53615
|
kind: "bad_input",
|
|
53615
|
-
error: `Invalid profile name '${profile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]
|
|
53616
|
+
error: `Invalid profile name '${profile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '='). Check the 'profile' arg or AWS_PROFILE env var.`
|
|
53616
53617
|
});
|
|
53617
53618
|
}
|
|
53618
53619
|
if (!isValidRegionName(region)) {
|
|
@@ -53642,6 +53643,13 @@ function runAwsCall(opts) {
|
|
|
53642
53643
|
region
|
|
53643
53644
|
];
|
|
53644
53645
|
if (opts.query !== void 0 && opts.query.trim().length > 0) {
|
|
53646
|
+
if (opts.query.length > 2048) {
|
|
53647
|
+
return Promise.resolve({
|
|
53648
|
+
ok: false,
|
|
53649
|
+
kind: "bad_input",
|
|
53650
|
+
error: `query expression too long (${opts.query.length} chars; max 2048). Simplify the JMESPath expression.`
|
|
53651
|
+
});
|
|
53652
|
+
}
|
|
53645
53653
|
args.push("--query", opts.query);
|
|
53646
53654
|
}
|
|
53647
53655
|
if (opts.params !== void 0 && Object.keys(opts.params).length > 0) {
|
|
@@ -53973,7 +53981,7 @@ function resolveTargetProfile(input) {
|
|
|
53973
53981
|
if (input.targetProfile) {
|
|
53974
53982
|
return input.targetProfile.startsWith("mcp-") ? input.targetProfile : `mcp-${input.targetProfile}`;
|
|
53975
53983
|
}
|
|
53976
|
-
return `mcp-${input.sessionName}`;
|
|
53984
|
+
return input.sessionName.startsWith("mcp-") ? input.sessionName : `mcp-${input.sessionName}`;
|
|
53977
53985
|
}
|
|
53978
53986
|
var assumeTools = [
|
|
53979
53987
|
{
|
|
@@ -53987,7 +53995,10 @@ var assumeTools = [
|
|
|
53987
53995
|
openWorldHint: true
|
|
53988
53996
|
},
|
|
53989
53997
|
inputSchema: external_exports3.object({
|
|
53990
|
-
roleArn: external_exports3.string().
|
|
53998
|
+
roleArn: external_exports3.string().regex(
|
|
53999
|
+
/^arn:aws[a-z-]*:iam::[0-9]{12}:role\/.+$/,
|
|
54000
|
+
"roleArn must match arn:aws[partition]:iam::<12-digit-account>:role/<name>"
|
|
54001
|
+
).describe("Target role ARN, e.g. 'arn:aws:iam::123456789012:role/CrossAccountAdmin'."),
|
|
53991
54002
|
sessionName: external_exports3.string().min(2).max(64).regex(/^[\w+=,.@-]+$/, "sessionName must match [\\w+=,.@-]").describe("Role session name (shows up in CloudTrail). Alphanumeric + +=,.@- only."),
|
|
53992
54003
|
durationSeconds: external_exports3.number().int().min(900).max(43200).optional().describe("Session duration in seconds (900-43200). Default 3600."),
|
|
53993
54004
|
externalId: external_exports3.string().optional().describe("External ID (only required if the role's trust policy demands it)."),
|
|
@@ -54008,13 +54019,19 @@ var assumeTools = [
|
|
|
54008
54019
|
if (!isValidProfileName(sourceProfile)) {
|
|
54009
54020
|
return {
|
|
54010
54021
|
ok: false,
|
|
54011
|
-
error: `Invalid sourceProfile name '${sourceProfile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]
|
|
54022
|
+
error: `Invalid sourceProfile name '${sourceProfile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '='). Check the 'sourceProfile' arg or AWS_PROFILE env var.`
|
|
54012
54023
|
};
|
|
54013
54024
|
}
|
|
54014
54025
|
if (!isValidProfileName(targetProfile)) {
|
|
54015
54026
|
return {
|
|
54016
54027
|
ok: false,
|
|
54017
|
-
error: `Invalid targetProfile name '${targetProfile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]
|
|
54028
|
+
error: `Invalid targetProfile name '${targetProfile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '='). Pick a different targetProfile or sessionName.`
|
|
54029
|
+
};
|
|
54030
|
+
}
|
|
54031
|
+
if (!/^arn:aws[a-z-]*:iam::[0-9]{12}:role\/.+$/.test(i.roleArn)) {
|
|
54032
|
+
return {
|
|
54033
|
+
ok: false,
|
|
54034
|
+
error: `Invalid roleArn '${i.roleArn}'. Must match arn:aws[partition]:iam::<12-digit-account>:role/<name>, e.g. 'arn:aws:iam::123456789012:role/CrossAccountAdmin'.`
|
|
54018
54035
|
};
|
|
54019
54036
|
}
|
|
54020
54037
|
const params = {
|
|
@@ -54111,7 +54128,7 @@ function startSsoLogin(profile, opts = {}) {
|
|
|
54111
54128
|
if (!isValidProfileName(profile)) {
|
|
54112
54129
|
return Promise.resolve({
|
|
54113
54130
|
ok: false,
|
|
54114
|
-
error: `Invalid profile name '${profile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]
|
|
54131
|
+
error: `Invalid profile name '${profile}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '=').`
|
|
54115
54132
|
});
|
|
54116
54133
|
}
|
|
54117
54134
|
const key = dedupeKey(profile, opts);
|
|
@@ -54157,6 +54174,7 @@ function doStartSsoLogin(profile, opts) {
|
|
|
54157
54174
|
let codeSeen = null;
|
|
54158
54175
|
let settled = false;
|
|
54159
54176
|
let registeredSession = null;
|
|
54177
|
+
let exitResult = null;
|
|
54160
54178
|
let completionResolve;
|
|
54161
54179
|
const completion = new Promise((res) => {
|
|
54162
54180
|
completionResolve = res;
|
|
@@ -54254,14 +54272,17 @@ ${stderrBuf}` : "");
|
|
|
54254
54272
|
rawOutput
|
|
54255
54273
|
};
|
|
54256
54274
|
}
|
|
54275
|
+
exitResult = result;
|
|
54257
54276
|
completionResolve(result);
|
|
54277
|
+
});
|
|
54278
|
+
proc.on("close", () => {
|
|
54258
54279
|
if (!settled) {
|
|
54259
54280
|
settled = true;
|
|
54260
54281
|
clearTimeout(urlTimeout);
|
|
54261
54282
|
resolve({
|
|
54262
54283
|
ok: false,
|
|
54263
|
-
error:
|
|
54264
|
-
rawOutput:
|
|
54284
|
+
error: exitResult?.error ?? "aws sso login exited before printing a verification URL",
|
|
54285
|
+
rawOutput: exitResult?.rawOutput
|
|
54265
54286
|
});
|
|
54266
54287
|
}
|
|
54267
54288
|
});
|
|
@@ -54965,7 +54986,7 @@ function buildDocsTools(fetchImpl = fetch) {
|
|
|
54965
54986
|
}),
|
|
54966
54987
|
handler: async (input) => {
|
|
54967
54988
|
const i = input;
|
|
54968
|
-
const limit = i.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
54989
|
+
const limit = Math.min(Math.max(1, i.limit ?? DEFAULT_SEARCH_LIMIT), MAX_SEARCH_LIMIT);
|
|
54969
54990
|
let response;
|
|
54970
54991
|
try {
|
|
54971
54992
|
response = await fetchWithTimeout(
|
|
@@ -55035,7 +55056,7 @@ function buildDocsTools(fetchImpl = fetch) {
|
|
|
55035
55056
|
openWorldHint: true
|
|
55036
55057
|
},
|
|
55037
55058
|
inputSchema: external_exports3.object({
|
|
55038
|
-
url: external_exports3.string().min(1).describe(
|
|
55059
|
+
url: external_exports3.string().min(1).max(2048).describe(
|
|
55039
55060
|
"AWS docs page URL: https://docs.aws.amazon.com/<...>.html. Usually from an aws_docs_search result."
|
|
55040
55061
|
),
|
|
55041
55062
|
startIndex: external_exports3.number().int().min(0).optional().describe("Character offset to start from (for paginated reads). Default 0."),
|
|
@@ -55049,8 +55070,8 @@ function buildDocsTools(fetchImpl = fetch) {
|
|
|
55049
55070
|
error: `Invalid url '${i.url}'. Must be an 'https://docs.aws.amazon.com/...html' page. Use aws_docs_search to find one.`
|
|
55050
55071
|
};
|
|
55051
55072
|
}
|
|
55052
|
-
const startIndex = i.startIndex ?? 0;
|
|
55053
|
-
const maxLength = i.maxLength ?? DEFAULT_MAX_LENGTH;
|
|
55073
|
+
const startIndex = Math.max(0, i.startIndex ?? 0);
|
|
55074
|
+
const maxLength = Math.min(Math.max(1, i.maxLength ?? DEFAULT_MAX_LENGTH), MAX_MAX_LENGTH);
|
|
55054
55075
|
let markdown = docCache.get(i.url);
|
|
55055
55076
|
let cached2 = true;
|
|
55056
55077
|
if (markdown === void 0) {
|
|
@@ -55165,7 +55186,7 @@ function parseSimulationResults(raw) {
|
|
|
55165
55186
|
var iamSimulateTools = [
|
|
55166
55187
|
{
|
|
55167
55188
|
name: "aws_iam_simulate",
|
|
55168
|
-
description: "Simulate IAM permissions for a principal: can principal X do actions Y on resources Z? Wraps `iam simulate-principal-policy`. Returns one entry per (action, resource) pair with `decision` (allowed / explicitDeny / implicitDeny), `matchedStatementIds` (which IAM statements decided), and `missingContextValues` (context keys the policy needed but you didn't provide -- common for tag-based policies). Use this BEFORE a risky operation to avoid a 403; pairs with the post-failure Suggestion you get from aws_call. Requires iam:SimulatePrincipalPolicy on the caller.",
|
|
55189
|
+
description: "Simulate IAM permissions for a principal: can principal X do actions Y on resources Z? Wraps `iam simulate-principal-policy`. Returns one entry per (action, resource) pair with `decision` (allowed / explicitDeny / implicitDeny / unknown -- unknown is the malformed-response fallback when EvalDecision is missing or unrecognised), `matchedStatementIds` (which IAM statements decided), and `missingContextValues` (context keys the policy needed but you didn't provide -- common for tag-based policies). Use this BEFORE a risky operation to avoid a 403; pairs with the post-failure Suggestion you get from aws_call. Requires iam:SimulatePrincipalPolicy on the caller.",
|
|
55169
55190
|
annotations: {
|
|
55170
55191
|
title: "Simulate IAM permissions for a principal",
|
|
55171
55192
|
readOnlyHint: true,
|
|
@@ -55175,13 +55196,13 @@ var iamSimulateTools = [
|
|
|
55175
55196
|
},
|
|
55176
55197
|
inputSchema: external_exports3.object({
|
|
55177
55198
|
principalArn: external_exports3.string().min(1).describe(
|
|
55178
|
-
"ARN of the principal whose policies you want to evaluate, e.g. 'arn:aws:iam::123456789012:user/jeff' or 'arn:aws:iam::
|
|
55199
|
+
"ARN of the principal whose policies you want to evaluate, e.g. 'arn:aws:iam::123456789012:user/jeff' or 'arn:aws:iam::123456789012:role/my-role'."
|
|
55179
55200
|
),
|
|
55180
55201
|
actions: external_exports3.array(external_exports3.string().min(1)).min(1).max(50).describe(
|
|
55181
55202
|
"IAM action names to test, e.g. ['lambda:CreateFunction', 's3:GetObject']. 1-50 entries. Wildcards (e.g. 's3:*') are accepted."
|
|
55182
55203
|
),
|
|
55183
55204
|
resources: external_exports3.array(external_exports3.string().min(1)).optional().describe(
|
|
55184
|
-
"Resource ARNs to test against, e.g. ['arn:aws:s3:::my-bucket/*'].
|
|
55205
|
+
"Resource ARNs to test against, e.g. ['arn:aws:s3:::my-bucket/*']. When omitted, AWS applies its own default of ['*'] server-side (best-case 'is this action ever allowed?') -- this tool does not inject a ['*'] itself."
|
|
55185
55206
|
),
|
|
55186
55207
|
contextEntries: external_exports3.array(
|
|
55187
55208
|
external_exports3.object({
|
|
@@ -55251,13 +55272,14 @@ var iamSimulateTools = [
|
|
|
55251
55272
|
const raw = result.data;
|
|
55252
55273
|
const results = parseSimulationResults(raw?.EvaluationResults);
|
|
55253
55274
|
const allowed = results.filter((r) => r.decision === "allowed").length;
|
|
55254
|
-
const
|
|
55275
|
+
const unknown2 = results.filter((r) => r.decision === "unknown").length;
|
|
55276
|
+
const denied = results.length - allowed - unknown2;
|
|
55255
55277
|
return {
|
|
55256
55278
|
ok: true,
|
|
55257
55279
|
data: {
|
|
55258
55280
|
command: result.command,
|
|
55259
55281
|
principalArn: i.principalArn,
|
|
55260
|
-
summary: { allowed, denied, total: results.length },
|
|
55282
|
+
summary: { allowed, denied, unknown: unknown2, total: results.length },
|
|
55261
55283
|
results,
|
|
55262
55284
|
evaluationResults: raw?.EvaluationResults ?? []
|
|
55263
55285
|
}
|
|
@@ -55384,736 +55406,288 @@ var logsTools = [
|
|
|
55384
55406
|
}
|
|
55385
55407
|
];
|
|
55386
55408
|
|
|
55387
|
-
// src/tools/
|
|
55388
|
-
|
|
55389
|
-
|
|
55390
|
-
|
|
55391
|
-
|
|
55409
|
+
// src/tools/resource.ts
|
|
55410
|
+
var TYPE_NAME_RE = /^[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*$/;
|
|
55411
|
+
function isValidIdentifier(id) {
|
|
55412
|
+
if (id.length === 0 || id.length > 2048) return false;
|
|
55413
|
+
if (id.startsWith("-")) return false;
|
|
55414
|
+
for (let i = 0; i < id.length; i++) {
|
|
55415
|
+
if (id.charCodeAt(i) < 32) return false;
|
|
55416
|
+
}
|
|
55417
|
+
return true;
|
|
55418
|
+
}
|
|
55419
|
+
function isValidOpaqueToken(token) {
|
|
55420
|
+
if (token.length === 0 || token.length > 128) return false;
|
|
55421
|
+
if (token.startsWith("-")) return false;
|
|
55422
|
+
for (let i = 0; i < token.length; i++) {
|
|
55423
|
+
if (token.charCodeAt(i) < 32) return false;
|
|
55424
|
+
}
|
|
55425
|
+
return true;
|
|
55426
|
+
}
|
|
55427
|
+
function parseResourceProperties(raw) {
|
|
55428
|
+
if (!raw || typeof raw !== "object") return { Properties: raw };
|
|
55429
|
+
const rec = raw;
|
|
55430
|
+
const identifier = typeof rec.Identifier === "string" ? rec.Identifier : void 0;
|
|
55431
|
+
const rawProps = rec.Properties;
|
|
55432
|
+
if (typeof rawProps !== "string") {
|
|
55433
|
+
return { Identifier: identifier, Properties: rawProps };
|
|
55434
|
+
}
|
|
55435
|
+
try {
|
|
55436
|
+
return { Identifier: identifier, Properties: JSON.parse(rawProps) };
|
|
55437
|
+
} catch {
|
|
55438
|
+
return { Identifier: identifier, Properties: rawProps, propertiesRaw: rawProps };
|
|
55439
|
+
}
|
|
55440
|
+
}
|
|
55441
|
+
function validateTypeName(typeName) {
|
|
55442
|
+
if (!TYPE_NAME_RE.test(typeName)) {
|
|
55443
|
+
return `Invalid typeName '${typeName}'. Must be '<Namespace>::<Service>::<Resource>' in PascalCase, e.g. 'AWS::Lambda::Function', 'AWS::S3::Bucket'.`;
|
|
55392
55444
|
}
|
|
55393
55445
|
return null;
|
|
55394
55446
|
}
|
|
55395
|
-
function
|
|
55396
|
-
|
|
55447
|
+
function validateIdentifier(id) {
|
|
55448
|
+
if (!isValidIdentifier(id)) {
|
|
55449
|
+
const preview = id.length > 40 ? `${id.slice(0, 40)}...` : id;
|
|
55450
|
+
return `Invalid identifier '${preview}'. Must be 1-2048 chars, not start with '-', and contain no control characters.`;
|
|
55451
|
+
}
|
|
55452
|
+
return null;
|
|
55397
55453
|
}
|
|
55398
|
-
|
|
55454
|
+
function validateOpaqueToken(token, fieldName) {
|
|
55455
|
+
if (!isValidOpaqueToken(token)) {
|
|
55456
|
+
return `Invalid ${fieldName}. Must be 1-128 chars, not start with '-', and contain no control characters.`;
|
|
55457
|
+
}
|
|
55458
|
+
return null;
|
|
55459
|
+
}
|
|
55460
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["SUCCESS", "FAILED", "CANCEL_COMPLETE"]);
|
|
55461
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
55462
|
+
var DEFAULT_MAX_WAIT_MS = 5 * 6e4;
|
|
55463
|
+
var MIN_POLL_INTERVAL_MS = 500;
|
|
55464
|
+
var MAX_POLL_INTERVAL_MS = 3e4;
|
|
55465
|
+
var MIN_MAX_WAIT_MS = 1e3;
|
|
55466
|
+
var MAX_MAX_WAIT_MS = 30 * 6e4;
|
|
55467
|
+
function extractProgressFields(progressEvent) {
|
|
55468
|
+
const pe = progressEvent && typeof progressEvent === "object" ? progressEvent : {};
|
|
55469
|
+
const str = (k) => {
|
|
55470
|
+
const v = pe[k];
|
|
55471
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
55472
|
+
};
|
|
55473
|
+
return {
|
|
55474
|
+
requestToken: str("RequestToken"),
|
|
55475
|
+
operationStatus: str("OperationStatus"),
|
|
55476
|
+
identifier: str("Identifier"),
|
|
55477
|
+
errorCode: str("ErrorCode"),
|
|
55478
|
+
statusMessage: str("StatusMessage"),
|
|
55479
|
+
retryAfter: str("RetryAfter")
|
|
55480
|
+
};
|
|
55481
|
+
}
|
|
55482
|
+
async function pollUntilTerminal(opts, awsCall = runAwsCall, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
55483
|
+
const start = Date.now();
|
|
55484
|
+
let attempts = 0;
|
|
55485
|
+
let lastEvent = null;
|
|
55486
|
+
let lastCommand = "";
|
|
55487
|
+
while (true) {
|
|
55488
|
+
attempts++;
|
|
55489
|
+
const result = await awsCall({
|
|
55490
|
+
service: "cloudcontrol",
|
|
55491
|
+
operation: "get-resource-request-status",
|
|
55492
|
+
profile: opts.profile,
|
|
55493
|
+
region: opts.region,
|
|
55494
|
+
timeoutMs: opts.timeoutMs,
|
|
55495
|
+
outputFormat: "json",
|
|
55496
|
+
extraFlags: ["--request-token", opts.requestToken]
|
|
55497
|
+
});
|
|
55498
|
+
if (!result.ok) {
|
|
55499
|
+
return {
|
|
55500
|
+
ok: false,
|
|
55501
|
+
progressEvent: lastEvent,
|
|
55502
|
+
command: result.command ?? lastCommand,
|
|
55503
|
+
attempts,
|
|
55504
|
+
elapsedMs: Date.now() - start,
|
|
55505
|
+
error: result.error,
|
|
55506
|
+
kind: result.kind,
|
|
55507
|
+
rawBody: result.rawStderr ?? result.rawStdout
|
|
55508
|
+
};
|
|
55509
|
+
}
|
|
55510
|
+
lastCommand = result.command;
|
|
55511
|
+
const raw = result.data;
|
|
55512
|
+
lastEvent = raw?.ProgressEvent ?? null;
|
|
55513
|
+
const status = lastEvent && typeof lastEvent.OperationStatus === "string" ? lastEvent.OperationStatus : null;
|
|
55514
|
+
if (status && TERMINAL_STATUSES.has(status)) {
|
|
55515
|
+
return { ok: true, progressEvent: lastEvent, command: lastCommand, attempts, elapsedMs: Date.now() - start };
|
|
55516
|
+
}
|
|
55517
|
+
const elapsed = Date.now() - start;
|
|
55518
|
+
if (elapsed >= opts.maxWaitMs) {
|
|
55519
|
+
return {
|
|
55520
|
+
ok: false,
|
|
55521
|
+
progressEvent: lastEvent,
|
|
55522
|
+
command: lastCommand,
|
|
55523
|
+
attempts,
|
|
55524
|
+
elapsedMs: elapsed,
|
|
55525
|
+
error: `Polled for ${Math.round(elapsed / 1e3)}s without reaching a terminal state (last status: ${status ?? "unknown"}). Increase maxWaitMs, or call aws_resource_status with requestToken='${opts.requestToken}' to keep checking.`
|
|
55526
|
+
};
|
|
55527
|
+
}
|
|
55528
|
+
let waitMs = opts.pollIntervalMs;
|
|
55529
|
+
const retryAfterRaw = lastEvent && typeof lastEvent.RetryAfter === "string" ? lastEvent.RetryAfter : null;
|
|
55530
|
+
if (retryAfterRaw) {
|
|
55531
|
+
const target = Date.parse(retryAfterRaw);
|
|
55532
|
+
if (!Number.isNaN(target)) {
|
|
55533
|
+
const ra = target - Date.now();
|
|
55534
|
+
if (ra > 0) waitMs = ra;
|
|
55535
|
+
}
|
|
55536
|
+
}
|
|
55537
|
+
waitMs = Math.min(waitMs, opts.maxWaitMs - elapsed);
|
|
55538
|
+
if (waitMs > 0) await sleep(waitMs);
|
|
55539
|
+
}
|
|
55540
|
+
}
|
|
55541
|
+
async function buildMutationResponse(initial, i) {
|
|
55542
|
+
const raw = initial.data;
|
|
55543
|
+
const progressEvent = raw?.ProgressEvent ?? null;
|
|
55544
|
+
const fields = extractProgressFields(progressEvent);
|
|
55545
|
+
const initialStatus = fields.operationStatus ?? "";
|
|
55546
|
+
const alreadyTerminal = TERMINAL_STATUSES.has(initialStatus);
|
|
55547
|
+
if (i.awaitCompletion && fields.requestToken && !alreadyTerminal) {
|
|
55548
|
+
const polled = await pollUntilTerminal({
|
|
55549
|
+
requestToken: fields.requestToken,
|
|
55550
|
+
profile: i.profile,
|
|
55551
|
+
region: i.region,
|
|
55552
|
+
timeoutMs: i.timeoutMs,
|
|
55553
|
+
pollIntervalMs: i.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
55554
|
+
maxWaitMs: i.maxWaitMs ?? DEFAULT_MAX_WAIT_MS
|
|
55555
|
+
});
|
|
55556
|
+
if (!polled.ok) {
|
|
55557
|
+
if (polled.kind === "sso_expired" || polled.kind === "no_creds") {
|
|
55558
|
+
const useProfile = i.profile ?? getProfile();
|
|
55559
|
+
const reLoginHint = polled.kind === "sso_expired" ? `SSO session expired while awaiting completion. Call aws_login_start with profile='${useProfile}' to re-authenticate, then call aws_resource_status with requestToken='${fields.requestToken}' to check whether the mutation completed server-side.` : `No credentials available while awaiting completion. After fixing credentials for profile '${useProfile}', call aws_resource_status with requestToken='${fields.requestToken}' to check whether the mutation completed server-side. Underlying error: ${polled.error}`;
|
|
55560
|
+
return { ok: false, error: reLoginHint, rawBody: polled.rawBody };
|
|
55561
|
+
}
|
|
55562
|
+
return { ok: false, error: polled.error ?? "Poll failed", rawBody: polled.rawBody };
|
|
55563
|
+
}
|
|
55564
|
+
const finalFields = extractProgressFields(polled.progressEvent);
|
|
55565
|
+
return {
|
|
55566
|
+
ok: true,
|
|
55567
|
+
data: {
|
|
55568
|
+
command: polled.command,
|
|
55569
|
+
...finalFields,
|
|
55570
|
+
progressEvent: polled.progressEvent,
|
|
55571
|
+
awaited: { attempts: polled.attempts, elapsedMs: polled.elapsedMs }
|
|
55572
|
+
}
|
|
55573
|
+
};
|
|
55574
|
+
}
|
|
55575
|
+
return {
|
|
55576
|
+
ok: true,
|
|
55577
|
+
data: { command: initial.command, ...fields, progressEvent }
|
|
55578
|
+
};
|
|
55579
|
+
}
|
|
55580
|
+
var baseFields = {
|
|
55581
|
+
profile: external_exports3.string().optional().describe("Override session profile for this call."),
|
|
55582
|
+
region: external_exports3.string().optional().describe("Override session region for this call."),
|
|
55583
|
+
timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000.")
|
|
55584
|
+
};
|
|
55585
|
+
var awaitFields = {
|
|
55586
|
+
awaitCompletion: external_exports3.boolean().optional().describe(
|
|
55587
|
+
"If true, poll get-resource-request-status until the operation reaches SUCCESS / FAILED / CANCEL_COMPLETE and return the final ProgressEvent. Default false (returns immediately with IN_PROGRESS, caller polls via aws_resource_status)."
|
|
55588
|
+
),
|
|
55589
|
+
pollIntervalMs: external_exports3.number().int().min(MIN_POLL_INTERVAL_MS).max(MAX_POLL_INTERVAL_MS).optional().describe(
|
|
55590
|
+
`Poll interval in ms when awaitCompletion is true (range ${MIN_POLL_INTERVAL_MS}-${MAX_POLL_INTERVAL_MS}). Default ${DEFAULT_POLL_INTERVAL_MS}. ProgressEvent.RetryAfter overrides when CCAPI returns one.`
|
|
55591
|
+
),
|
|
55592
|
+
maxWaitMs: external_exports3.number().int().min(MIN_MAX_WAIT_MS).max(MAX_MAX_WAIT_MS).optional().describe(
|
|
55593
|
+
`Maximum total wait in ms when awaitCompletion is true (range ${MIN_MAX_WAIT_MS}-${MAX_MAX_WAIT_MS}). Default ${DEFAULT_MAX_WAIT_MS}. On timeout, returns the last seen status with a hint to keep polling.`
|
|
55594
|
+
)
|
|
55595
|
+
};
|
|
55596
|
+
var resourceTools = [
|
|
55399
55597
|
{
|
|
55400
|
-
name: "
|
|
55401
|
-
description: "
|
|
55598
|
+
name: "aws_resource_get",
|
|
55599
|
+
description: "Read a single AWS resource via Cloud Control API. Covers hundreds of resource types with a CloudFormation schema. `typeName` is '<Namespace>::<Service>::<Resource>' (e.g. 'AWS::Lambda::Function'); `identifier` is the primary key for that type (function name, bucket name, IAM role name, ARN, or composite id). Returns parsed Properties. For resources not covered by CCAPI or for data-plane operations, use aws_call.",
|
|
55402
55600
|
annotations: {
|
|
55403
|
-
title: "
|
|
55601
|
+
title: "Get an AWS resource by type + identifier",
|
|
55404
55602
|
readOnlyHint: true,
|
|
55405
55603
|
destructiveHint: false,
|
|
55406
55604
|
idempotentHint: true,
|
|
55407
55605
|
openWorldHint: true
|
|
55408
55606
|
},
|
|
55409
55607
|
inputSchema: external_exports3.object({
|
|
55410
|
-
|
|
55411
|
-
|
|
55412
|
-
|
|
55413
|
-
query: external_exports3.string().optional().describe(
|
|
55414
|
-
"JMESPath expression to extract fields from each page (--query). The query is wrapped server-side as {NextToken, items: <query>} so pagination still works even when the projection drops NextToken; the handler unwraps `items` before returning."
|
|
55415
|
-
),
|
|
55416
|
-
maxItems: external_exports3.number().int().positive().optional().describe("Items per page. Default 100. Lower this if hitting the 5 MB output cap."),
|
|
55417
|
-
startingToken: external_exports3.string().optional().describe("Resume cursor from the previous call's `nextToken`. Omit for the first page."),
|
|
55418
|
-
profile: external_exports3.string().optional().describe("Override session profile for this call."),
|
|
55419
|
-
region: external_exports3.string().optional().describe("Override session region for this call."),
|
|
55420
|
-
timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000.")
|
|
55608
|
+
typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function', 'AWS::S3::Bucket', 'AWS::IAM::Role'."),
|
|
55609
|
+
identifier: external_exports3.string().min(1).describe("Primary identifier for the resource (function name, bucket name, ARN, or composite id)."),
|
|
55610
|
+
...baseFields
|
|
55421
55611
|
}),
|
|
55422
55612
|
handler: async (input) => {
|
|
55423
55613
|
const i = input;
|
|
55424
|
-
const
|
|
55425
|
-
|
|
55426
|
-
|
|
55427
|
-
|
|
55428
|
-
}
|
|
55429
|
-
const userQuery = i.query?.trim();
|
|
55430
|
-
const queryWrapped = userQuery ? wrapQueryForPagination(userQuery) : void 0;
|
|
55614
|
+
const tnErr = validateTypeName(i.typeName);
|
|
55615
|
+
if (tnErr) return { ok: false, error: tnErr };
|
|
55616
|
+
const idErr = validateIdentifier(i.identifier);
|
|
55617
|
+
if (idErr) return { ok: false, error: idErr };
|
|
55431
55618
|
const result = await runAwsCall({
|
|
55432
|
-
service:
|
|
55433
|
-
operation:
|
|
55434
|
-
params: i.params,
|
|
55435
|
-
query: queryWrapped,
|
|
55619
|
+
service: "cloudcontrol",
|
|
55620
|
+
operation: "get-resource",
|
|
55436
55621
|
profile: i.profile,
|
|
55437
55622
|
region: i.region,
|
|
55438
|
-
outputFormat: "json",
|
|
55439
55623
|
timeoutMs: i.timeoutMs,
|
|
55440
|
-
|
|
55624
|
+
outputFormat: "json",
|
|
55625
|
+
extraFlags: ["--type-name", i.typeName, "--identifier", i.identifier]
|
|
55441
55626
|
});
|
|
55442
55627
|
if (!result.ok) {
|
|
55443
55628
|
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
55444
55629
|
}
|
|
55445
|
-
|
|
55446
|
-
|
|
55447
|
-
if (queryWrapped) {
|
|
55448
|
-
const wrapped = result.data ?? {};
|
|
55449
|
-
nextToken = extractNextToken(wrapped);
|
|
55450
|
-
resultBody = wrapped.items ?? null;
|
|
55451
|
-
} else {
|
|
55452
|
-
nextToken = extractNextToken(result.data);
|
|
55453
|
-
resultBody = result.data;
|
|
55454
|
-
}
|
|
55630
|
+
const raw = result.data;
|
|
55631
|
+
const parsed = parseResourceProperties(raw?.ResourceDescription);
|
|
55455
55632
|
return {
|
|
55456
55633
|
ok: true,
|
|
55457
55634
|
data: {
|
|
55458
55635
|
command: result.command,
|
|
55459
|
-
|
|
55460
|
-
|
|
55461
|
-
|
|
55462
|
-
|
|
55463
|
-
|
|
55464
|
-
|
|
55465
|
-
}
|
|
55466
|
-
];
|
|
55467
|
-
|
|
55468
|
-
// src/tools/metrics.ts
|
|
55469
|
-
var SIMPLE_STATS = ["Average", "Sum", "Maximum", "Minimum", "SampleCount"];
|
|
55470
|
-
var EXTENDED_STAT_RE = /^(p|tm|tc|wm|pr|ts|iqm)(\d{1,3}(\.\d{1,3})?)?$/i;
|
|
55471
|
-
function isValidStatistic(s) {
|
|
55472
|
-
const lower = s.toLowerCase();
|
|
55473
|
-
if (SIMPLE_STATS.some((stat) => stat.toLowerCase() === lower)) return true;
|
|
55474
|
-
return EXTENDED_STAT_RE.test(s);
|
|
55475
|
-
}
|
|
55476
|
-
function canonicalizeStatistic(s) {
|
|
55477
|
-
const lower = s.toLowerCase();
|
|
55478
|
-
for (const stat of SIMPLE_STATS) {
|
|
55479
|
-
if (stat.toLowerCase() === lower) return stat;
|
|
55480
|
-
}
|
|
55481
|
-
if (EXTENDED_STAT_RE.test(s)) return lower;
|
|
55482
|
-
return s;
|
|
55483
|
-
}
|
|
55484
|
-
var QUERY_ID_RE = /^[a-z][A-Za-z0-9_]*$/;
|
|
55485
|
-
var MAX_QUERIES = 100;
|
|
55486
|
-
var CLOUDWATCH_MAX_DATAPOINTS = 100800;
|
|
55487
|
-
var PERIOD_3H_MS = 3 * 60 * 60 * 1e3;
|
|
55488
|
-
var PERIOD_24H_MS = 24 * 60 * 60 * 1e3;
|
|
55489
|
-
var PERIOD_15D_MS = 15 * 24 * 60 * 60 * 1e3;
|
|
55490
|
-
function pickAutoPeriodSeconds(startMs, endMs) {
|
|
55491
|
-
const rangeMs = Math.max(0, endMs - startMs);
|
|
55492
|
-
if (rangeMs <= PERIOD_3H_MS) return 60;
|
|
55493
|
-
if (rangeMs <= PERIOD_24H_MS) return 300;
|
|
55494
|
-
if (rangeMs <= PERIOD_15D_MS) return 900;
|
|
55495
|
-
return 3600;
|
|
55496
|
-
}
|
|
55497
|
-
var RELATIVE_TIME_RE = /^\d+[smhdw]$/;
|
|
55498
|
-
var UNIT_MS = {
|
|
55499
|
-
s: 1e3,
|
|
55500
|
-
m: 60 * 1e3,
|
|
55501
|
-
h: 60 * 60 * 1e3,
|
|
55502
|
-
d: 24 * 60 * 60 * 1e3,
|
|
55503
|
-
w: 7 * 24 * 60 * 60 * 1e3
|
|
55504
|
-
};
|
|
55505
|
-
function resolveTime(input, now) {
|
|
55506
|
-
if (input === "now") return new Date(now);
|
|
55507
|
-
const rel = input.match(RELATIVE_TIME_RE);
|
|
55508
|
-
if (rel) {
|
|
55509
|
-
const num = Number(input.slice(0, -1));
|
|
55510
|
-
const unit = input.slice(-1);
|
|
55511
|
-
const ms = UNIT_MS[unit];
|
|
55512
|
-
if (!ms || !Number.isFinite(num)) return null;
|
|
55513
|
-
return new Date(now - num * ms);
|
|
55514
|
-
}
|
|
55515
|
-
const t = new Date(input);
|
|
55516
|
-
if (Number.isNaN(t.getTime())) return null;
|
|
55517
|
-
return t;
|
|
55518
|
-
}
|
|
55519
|
-
function buildMetricDataQueries(inputs, autoPeriod) {
|
|
55520
|
-
return inputs.map((q) => {
|
|
55521
|
-
const base = { Id: q.id };
|
|
55522
|
-
if (q.label !== void 0) base.Label = q.label;
|
|
55523
|
-
if (q.returnData !== void 0) base.ReturnData = q.returnData;
|
|
55524
|
-
if (q.expression !== void 0) {
|
|
55525
|
-
base.Expression = q.expression;
|
|
55526
|
-
if (q.period !== void 0) base.Period = q.period;
|
|
55527
|
-
return base;
|
|
55636
|
+
typeName: raw?.TypeName ?? i.typeName,
|
|
55637
|
+
identifier: parsed.Identifier,
|
|
55638
|
+
properties: parsed.Properties,
|
|
55639
|
+
...parsed.propertiesRaw ? { propertiesRaw: parsed.propertiesRaw } : {}
|
|
55640
|
+
}
|
|
55641
|
+
};
|
|
55528
55642
|
}
|
|
55529
|
-
|
|
55530
|
-
const stat = {
|
|
55531
|
-
Metric: {
|
|
55532
|
-
Namespace: q.namespace,
|
|
55533
|
-
MetricName: q.metricName,
|
|
55534
|
-
...dimensions ? { Dimensions: dimensions } : {}
|
|
55535
|
-
},
|
|
55536
|
-
Period: q.period ?? autoPeriod,
|
|
55537
|
-
Stat: q.statistic !== void 0 ? canonicalizeStatistic(q.statistic) : "Average"
|
|
55538
|
-
};
|
|
55539
|
-
if (q.unit !== void 0) stat.Unit = q.unit;
|
|
55540
|
-
base.MetricStat = stat;
|
|
55541
|
-
return base;
|
|
55542
|
-
});
|
|
55543
|
-
}
|
|
55544
|
-
var metricsTools = [
|
|
55643
|
+
},
|
|
55545
55644
|
{
|
|
55546
|
-
name: "
|
|
55547
|
-
description: "
|
|
55645
|
+
name: "aws_resource_list",
|
|
55646
|
+
description: "List resources of a given type via Cloud Control API, paginated. Returns an array of {identifier, properties}, a `nextToken` (null when exhausted), and `hasMore`. Some types need parent identifiers (e.g. nested resources under a cluster); pass those as `resourceModel`.",
|
|
55548
55647
|
annotations: {
|
|
55549
|
-
title: "
|
|
55648
|
+
title: "List AWS resources of a type (paginated)",
|
|
55550
55649
|
readOnlyHint: true,
|
|
55551
55650
|
destructiveHint: false,
|
|
55552
55651
|
idempotentHint: true,
|
|
55553
55652
|
openWorldHint: true
|
|
55554
55653
|
},
|
|
55555
55654
|
inputSchema: external_exports3.object({
|
|
55556
|
-
|
|
55557
|
-
|
|
55558
|
-
|
|
55559
|
-
|
|
55560
|
-
|
|
55561
|
-
dimensions: external_exports3.record(external_exports3.string(), external_exports3.string()).optional().describe("Dimension Name -> Value map, e.g. {FunctionName: 'my-fn'}."),
|
|
55562
|
-
statistic: external_exports3.string().optional().describe(
|
|
55563
|
-
"Statistic: Average | Sum | Maximum | Minimum | SampleCount, or an extended stat like 'p99', 'p99.9', 'tm95'. Default 'Average'."
|
|
55564
|
-
),
|
|
55565
|
-
period: external_exports3.number().int().positive().optional().describe("Period in seconds. Defaults to an auto-pick from the time range (60s/300s/900s/3600s)."),
|
|
55566
|
-
expression: external_exports3.string().min(1).optional().describe(
|
|
55567
|
-
`CloudWatch metric math expression, e.g. 'SUM([m1, m2])' or 'AVG(METRICS("AWS/Lambda"))'. Mutually exclusive with namespace/metricName/dimensions.`
|
|
55568
|
-
),
|
|
55569
|
-
label: external_exports3.string().optional().describe("Human-readable label for the series in the response."),
|
|
55570
|
-
returnData: external_exports3.boolean().optional().describe(
|
|
55571
|
-
"Set false to compute this query but not return its data (useful for intermediate values in expressions). Default true."
|
|
55572
|
-
),
|
|
55573
|
-
unit: external_exports3.string().optional().describe(
|
|
55574
|
-
"Restrict to a specific Unit (e.g. 'Seconds', 'Bytes'). Default: no filter. Only meaningful on metric-stat queries."
|
|
55575
|
-
)
|
|
55576
|
-
})
|
|
55577
|
-
).min(1).max(MAX_QUERIES).describe(`1-${MAX_QUERIES} queries. Each is either a metric-stat (namespace + metricName) or an expression.`),
|
|
55578
|
-
startTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand ('15m', '1h', '1d', '1w'). Default '1h' (one hour ago)."),
|
|
55579
|
-
endTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand. Default 'now'."),
|
|
55580
|
-
scanBy: external_exports3.enum(["TimestampAscending", "TimestampDescending"]).optional().describe("Sort order for returned datapoints. Default 'TimestampDescending' (matches CloudWatch's default)."),
|
|
55581
|
-
maxDataPoints: external_exports3.number().int().positive().optional().describe(
|
|
55582
|
-
"Target datapoint count. CloudWatch does not truncate to the first N points -- it widens (coarsens) the period server-side so the series aggregates down to fit this many points. CloudWatch's own ceiling is ~100,800; lower this to make CloudWatch return a coarser, smaller series. Forwarded as CloudWatch's MaxDatapoints (single 'p') field; the camelCase schema name follows this server's convention."
|
|
55583
|
-
),
|
|
55584
|
-
nextToken: external_exports3.string().optional().describe(
|
|
55585
|
-
"Resume cursor from a previous call's `nextToken`. Omit for the first page. Forwarded as CloudWatch's NextToken; only meaningful when a prior call returned `hasMore: true`."
|
|
55586
|
-
),
|
|
55587
|
-
profile: external_exports3.string().optional().describe("Override session profile for this call."),
|
|
55588
|
-
region: external_exports3.string().optional().describe("Override session region for this call."),
|
|
55589
|
-
timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000 (60s).")
|
|
55655
|
+
typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function'."),
|
|
55656
|
+
resourceModel: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Parent identifier properties for nested types, e.g. {ClusterArn: '...'}."),
|
|
55657
|
+
maxResults: external_exports3.number().int().positive().max(100).optional().describe("Page size (1-100). Default 100."),
|
|
55658
|
+
nextToken: external_exports3.string().optional().describe("Resume cursor from the previous call's `nextToken`. Omit for the first page."),
|
|
55659
|
+
...baseFields
|
|
55590
55660
|
}),
|
|
55591
55661
|
handler: async (input) => {
|
|
55592
55662
|
const i = input;
|
|
55593
|
-
const
|
|
55594
|
-
|
|
55595
|
-
|
|
55596
|
-
const
|
|
55597
|
-
if (
|
|
55598
|
-
return {
|
|
55599
|
-
ok: false,
|
|
55600
|
-
error: `Duplicate query id '${q.id}' at queries[${qi}]; first seen at queries[${firstIdx}]. Each MetricDataQuery.Id must be unique in a batch.`
|
|
55601
|
-
};
|
|
55602
|
-
}
|
|
55603
|
-
seenIds.set(q.id, qi);
|
|
55604
|
-
const hasMetricStat = q.namespace !== void 0 || q.metricName !== void 0 || q.dimensions !== void 0;
|
|
55605
|
-
const hasExpression = q.expression !== void 0;
|
|
55606
|
-
if (hasMetricStat && hasExpression) {
|
|
55607
|
-
return {
|
|
55608
|
-
ok: false,
|
|
55609
|
-
error: `Query '${q.id}' mixes metric-stat fields (namespace/metricName/dimensions) with 'expression'. Pick one shape per query.`
|
|
55610
|
-
};
|
|
55611
|
-
}
|
|
55612
|
-
if (!hasMetricStat && !hasExpression) {
|
|
55613
|
-
return {
|
|
55614
|
-
ok: false,
|
|
55615
|
-
error: `Query '${q.id}' has neither metric-stat (namespace+metricName) nor 'expression'. One is required.`
|
|
55616
|
-
};
|
|
55617
|
-
}
|
|
55618
|
-
if (hasMetricStat && (q.namespace === void 0 || q.metricName === void 0)) {
|
|
55619
|
-
return {
|
|
55620
|
-
ok: false,
|
|
55621
|
-
error: `Query '${q.id}' must include BOTH 'namespace' and 'metricName' (or use 'expression' instead).`
|
|
55622
|
-
};
|
|
55623
|
-
}
|
|
55624
|
-
if (q.statistic !== void 0 && !isValidStatistic(q.statistic)) {
|
|
55625
|
-
return {
|
|
55626
|
-
ok: false,
|
|
55627
|
-
error: `Query '${q.id}' has invalid statistic '${q.statistic}'. Use Average | Sum | Maximum | Minimum | SampleCount, or an extended stat like p99 / p99.9 / tm95.`
|
|
55628
|
-
};
|
|
55629
|
-
}
|
|
55630
|
-
}
|
|
55631
|
-
const now = Date.now();
|
|
55632
|
-
const startStr = i.startTime ?? "1h";
|
|
55633
|
-
const endStr = i.endTime ?? "now";
|
|
55634
|
-
const startDate = resolveTime(startStr, now);
|
|
55635
|
-
const endDate = resolveTime(endStr, now);
|
|
55636
|
-
if (!startDate) {
|
|
55637
|
-
return {
|
|
55638
|
-
ok: false,
|
|
55639
|
-
error: `Invalid startTime '${startStr}'. Use ISO 8601 (e.g. '2026-05-16T10:00:00Z') or relative shorthand (e.g. '1h', '15m', '1d').`
|
|
55640
|
-
};
|
|
55641
|
-
}
|
|
55642
|
-
if (!endDate) {
|
|
55643
|
-
return {
|
|
55644
|
-
ok: false,
|
|
55645
|
-
error: `Invalid endTime '${endStr}'. Use ISO 8601 or relative shorthand, or 'now' for the current moment.`
|
|
55646
|
-
};
|
|
55647
|
-
}
|
|
55648
|
-
if (endDate.getTime() <= startDate.getTime()) {
|
|
55649
|
-
return {
|
|
55650
|
-
ok: false,
|
|
55651
|
-
error: `endTime (${endDate.toISOString()}) must be after startTime (${startDate.toISOString()}).`
|
|
55652
|
-
};
|
|
55663
|
+
const tnErr = validateTypeName(i.typeName);
|
|
55664
|
+
if (tnErr) return { ok: false, error: tnErr };
|
|
55665
|
+
if (i.nextToken !== void 0) {
|
|
55666
|
+
const ntErr = validateOpaqueToken(i.nextToken, "nextToken");
|
|
55667
|
+
if (ntErr) return { ok: false, error: ntErr };
|
|
55653
55668
|
}
|
|
55654
|
-
const
|
|
55655
|
-
|
|
55656
|
-
|
|
55657
|
-
|
|
55658
|
-
return {
|
|
55659
|
-
ok: false,
|
|
55660
|
-
error: `Query '${q.id}' has invalid period ${q.period}. CloudWatch requires period to be a positive multiple of 60 (seconds).`
|
|
55661
|
-
};
|
|
55662
|
-
}
|
|
55663
|
-
const datapoints = Math.ceil(rangeSeconds / q.period);
|
|
55664
|
-
if (datapoints > CLOUDWATCH_MAX_DATAPOINTS) {
|
|
55665
|
-
return {
|
|
55666
|
-
ok: false,
|
|
55667
|
-
error: `Query '${q.id}' with period ${q.period}s over the requested range (${startDate.toISOString()} to ${endDate.toISOString()}) would request ${datapoints} datapoints, exceeding CloudWatch's per-request cap of ${CLOUDWATCH_MAX_DATAPOINTS}. Widen the period or narrow the time range.`
|
|
55668
|
-
};
|
|
55669
|
-
}
|
|
55669
|
+
const extraFlags = ["--type-name", i.typeName, "--max-results", String(i.maxResults ?? 100)];
|
|
55670
|
+
if (i.nextToken) extraFlags.push("--next-token", i.nextToken);
|
|
55671
|
+
if (i.resourceModel && Object.keys(i.resourceModel).length > 0) {
|
|
55672
|
+
extraFlags.push("--resource-model", JSON.stringify(i.resourceModel));
|
|
55670
55673
|
}
|
|
55671
|
-
const periodSeconds = pickAutoPeriodSeconds(startDate.getTime(), endDate.getTime());
|
|
55672
|
-
const metricDataQueries = buildMetricDataQueries(i.queries, periodSeconds);
|
|
55673
|
-
const params = {
|
|
55674
|
-
MetricDataQueries: metricDataQueries,
|
|
55675
|
-
StartTime: startDate.toISOString(),
|
|
55676
|
-
EndTime: endDate.toISOString(),
|
|
55677
|
-
ScanBy: i.scanBy ?? "TimestampDescending"
|
|
55678
|
-
};
|
|
55679
|
-
if (i.maxDataPoints !== void 0) params.MaxDatapoints = i.maxDataPoints;
|
|
55680
|
-
if (i.nextToken !== void 0) params.NextToken = i.nextToken;
|
|
55681
|
-
const effectiveProfile = i.profile ?? getProfile();
|
|
55682
|
-
const effectiveRegion = i.region ?? getRegion();
|
|
55683
55674
|
const result = await runAwsCall({
|
|
55684
|
-
service: "
|
|
55685
|
-
operation: "
|
|
55675
|
+
service: "cloudcontrol",
|
|
55676
|
+
operation: "list-resources",
|
|
55686
55677
|
profile: i.profile,
|
|
55687
55678
|
region: i.region,
|
|
55688
55679
|
timeoutMs: i.timeoutMs,
|
|
55689
55680
|
outputFormat: "json",
|
|
55690
|
-
|
|
55681
|
+
extraFlags
|
|
55691
55682
|
});
|
|
55692
55683
|
if (!result.ok) {
|
|
55693
55684
|
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
55694
55685
|
}
|
|
55695
|
-
const raw = result.data
|
|
55696
|
-
const
|
|
55697
|
-
const
|
|
55698
|
-
const
|
|
55699
|
-
|
|
55700
|
-
return {
|
|
55701
|
-
id: r.Id ?? "",
|
|
55702
|
-
...r.Label !== void 0 ? { label: r.Label } : {},
|
|
55703
|
-
timestamps: r.Timestamps ?? [],
|
|
55704
|
-
values: r.Values ?? [],
|
|
55705
|
-
...effectivePeriod !== void 0 ? { period: effectivePeriod } : {},
|
|
55706
|
-
...r.StatusCode !== void 0 ? { statusCode: r.StatusCode } : {}
|
|
55707
|
-
};
|
|
55708
|
-
});
|
|
55709
|
-
const messages = raw.Messages?.filter((m) => m.Code || m.Value).map((m) => ({
|
|
55710
|
-
code: m.Code,
|
|
55711
|
-
value: m.Value
|
|
55712
|
-
}));
|
|
55713
|
-
const nextToken = extractNextToken(raw);
|
|
55714
|
-
return {
|
|
55715
|
-
ok: true,
|
|
55716
|
-
data: {
|
|
55717
|
-
command: result.command,
|
|
55718
|
-
profile: effectiveProfile,
|
|
55719
|
-
region: effectiveRegion,
|
|
55720
|
-
startTime: startDate.toISOString(),
|
|
55721
|
-
endTime: endDate.toISOString(),
|
|
55722
|
-
periodSeconds,
|
|
55723
|
-
series,
|
|
55724
|
-
nextToken,
|
|
55725
|
-
hasMore: nextToken !== null,
|
|
55726
|
-
...messages && messages.length > 0 ? { messages } : {}
|
|
55727
|
-
}
|
|
55728
|
-
};
|
|
55729
|
-
}
|
|
55730
|
-
}
|
|
55731
|
-
];
|
|
55732
|
-
|
|
55733
|
-
// src/tools/multi-region.ts
|
|
55734
|
-
var DEFAULT_CONCURRENCY = 8;
|
|
55735
|
-
var MAX_CONCURRENCY = 32;
|
|
55736
|
-
var MAX_REGIONS = 32;
|
|
55737
|
-
async function runWithConcurrency(inputs, concurrency, fn) {
|
|
55738
|
-
const results = new Array(inputs.length);
|
|
55739
|
-
let next = 0;
|
|
55740
|
-
const worker = async () => {
|
|
55741
|
-
while (true) {
|
|
55742
|
-
const i = next++;
|
|
55743
|
-
if (i >= inputs.length) return;
|
|
55744
|
-
results[i] = await fn(inputs[i], i);
|
|
55745
|
-
}
|
|
55746
|
-
};
|
|
55747
|
-
const workerCount = Math.min(concurrency, inputs.length);
|
|
55748
|
-
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
55749
|
-
return results;
|
|
55750
|
-
}
|
|
55751
|
-
var multiRegionTools = [
|
|
55752
|
-
{
|
|
55753
|
-
name: "aws_multi_region",
|
|
55754
|
-
description: "Run the same AWS API operation across multiple regions in parallel. Same shape as aws_call (service, operation, params?, query?, outputFormat?, timeoutMs?) but takes `regions: string[]` instead of `region`. Returns an array of `{region, ok, data?, command?, error?, errorKind?}` -- partial failure is expected (services aren't everywhere, perms may be region-scoped). Duplicate regions in the input are collapsed (first occurrence wins), so `results.length` may be less than `regions.length`; use the returned `regionCount` for the actual count run. Use for fleet-wide reads: 'describe-instances across all our regions', 'list buckets in every region', 'check IAM password policy everywhere'.",
|
|
55755
|
-
annotations: {
|
|
55756
|
-
title: "Run an AWS operation across multiple regions in parallel",
|
|
55757
|
-
// The operation can be anything -- we conservatively annotate as not
|
|
55758
|
-
// read-only / not destructive. The caller chooses what to invoke.
|
|
55759
|
-
readOnlyHint: false,
|
|
55760
|
-
destructiveHint: false,
|
|
55761
|
-
idempotentHint: false,
|
|
55762
|
-
openWorldHint: true
|
|
55763
|
-
},
|
|
55764
|
-
inputSchema: external_exports3.object({
|
|
55765
|
-
service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', etc."),
|
|
55766
|
-
operation: external_exports3.string().describe("Operation in kebab-case: 'describe-instances', 'list-buckets', etc."),
|
|
55767
|
-
regions: external_exports3.array(external_exports3.string().min(1)).min(1).max(MAX_REGIONS).describe(
|
|
55768
|
-
`Region IDs (e.g. ['us-east-1','us-west-2','eu-west-1']). 1-${MAX_REGIONS}. Validated for argv-safety; a bad region name yields a clear per-region error and skips its CLI spawn (per-region isolation comes from each region being a separate call, not from this pre-check).`
|
|
55769
|
-
),
|
|
55770
|
-
params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) -- same shape as aws_call."),
|
|
55771
|
-
query: external_exports3.string().optional().describe("JMESPath expression for --query (server-side trimming per region)."),
|
|
55772
|
-
outputFormat: external_exports3.enum(["json", "text", "table", "yaml"]).optional().describe("Output format. Default 'json'."),
|
|
55773
|
-
profile: external_exports3.string().optional().describe("Override session profile for the batch."),
|
|
55774
|
-
timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in ms applied PER region. Default 60000."),
|
|
55775
|
-
concurrency: external_exports3.number().int().positive().max(MAX_CONCURRENCY).optional().describe(`Max regions in flight at once (1-${MAX_CONCURRENCY}). Default ${DEFAULT_CONCURRENCY}.`)
|
|
55776
|
-
}),
|
|
55777
|
-
handler: async (input) => {
|
|
55778
|
-
const i = input;
|
|
55779
|
-
const seen = /* @__PURE__ */ new Set();
|
|
55780
|
-
const regions = [];
|
|
55781
|
-
for (const r of i.regions) {
|
|
55782
|
-
if (!seen.has(r)) {
|
|
55783
|
-
seen.add(r);
|
|
55784
|
-
regions.push(r);
|
|
55785
|
-
}
|
|
55786
|
-
}
|
|
55787
|
-
const concurrency = i.concurrency ?? DEFAULT_CONCURRENCY;
|
|
55788
|
-
const results = await runWithConcurrency(regions, concurrency, async (region) => {
|
|
55789
|
-
if (!isValidRegionName(region)) {
|
|
55790
|
-
return {
|
|
55791
|
-
region,
|
|
55792
|
-
ok: false,
|
|
55793
|
-
error: `Invalid region '${region}'. Must match ${REGION_NAME_RE} (e.g. 'us-east-1').`,
|
|
55794
|
-
errorKind: "bad_input"
|
|
55795
|
-
};
|
|
55796
|
-
}
|
|
55797
|
-
const r = await runAwsCall({
|
|
55798
|
-
service: i.service,
|
|
55799
|
-
operation: i.operation,
|
|
55800
|
-
params: i.params,
|
|
55801
|
-
query: i.query,
|
|
55802
|
-
profile: i.profile,
|
|
55803
|
-
region,
|
|
55804
|
-
outputFormat: i.outputFormat,
|
|
55805
|
-
timeoutMs: i.timeoutMs
|
|
55806
|
-
});
|
|
55807
|
-
if (!r.ok) {
|
|
55808
|
-
return {
|
|
55809
|
-
region,
|
|
55810
|
-
ok: false,
|
|
55811
|
-
command: r.command,
|
|
55812
|
-
error: r.error,
|
|
55813
|
-
errorKind: r.kind
|
|
55814
|
-
};
|
|
55815
|
-
}
|
|
55816
|
-
return { region, ok: true, command: r.command, data: r.data };
|
|
55817
|
-
});
|
|
55818
|
-
const okCount = results.filter((r) => r.ok).length;
|
|
55819
|
-
const errCount = results.length - okCount;
|
|
55820
|
-
return {
|
|
55821
|
-
ok: true,
|
|
55822
|
-
data: {
|
|
55823
|
-
service: i.service,
|
|
55824
|
-
operation: i.operation,
|
|
55825
|
-
regionCount: regions.length,
|
|
55826
|
-
okCount,
|
|
55827
|
-
errorCount: errCount,
|
|
55828
|
-
results
|
|
55829
|
-
}
|
|
55830
|
-
};
|
|
55831
|
-
}
|
|
55832
|
-
}
|
|
55833
|
-
];
|
|
55834
|
-
|
|
55835
|
-
// src/tools/resource.ts
|
|
55836
|
-
var TYPE_NAME_RE = /^[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*::[A-Z][A-Za-z0-9]*$/;
|
|
55837
|
-
function isValidIdentifier(id) {
|
|
55838
|
-
if (id.length === 0 || id.length > 2048) return false;
|
|
55839
|
-
if (id.startsWith("-")) return false;
|
|
55840
|
-
for (let i = 0; i < id.length; i++) {
|
|
55841
|
-
if (id.charCodeAt(i) < 32) return false;
|
|
55842
|
-
}
|
|
55843
|
-
return true;
|
|
55844
|
-
}
|
|
55845
|
-
function isValidOpaqueToken(token) {
|
|
55846
|
-
if (token.length === 0 || token.length > 128) return false;
|
|
55847
|
-
if (token.startsWith("-")) return false;
|
|
55848
|
-
for (let i = 0; i < token.length; i++) {
|
|
55849
|
-
if (token.charCodeAt(i) < 32) return false;
|
|
55850
|
-
}
|
|
55851
|
-
return true;
|
|
55852
|
-
}
|
|
55853
|
-
function parseResourceProperties(raw) {
|
|
55854
|
-
if (!raw || typeof raw !== "object") return { Properties: raw };
|
|
55855
|
-
const rec = raw;
|
|
55856
|
-
const identifier = typeof rec.Identifier === "string" ? rec.Identifier : void 0;
|
|
55857
|
-
const rawProps = rec.Properties;
|
|
55858
|
-
if (typeof rawProps !== "string") {
|
|
55859
|
-
return { Identifier: identifier, Properties: rawProps };
|
|
55860
|
-
}
|
|
55861
|
-
try {
|
|
55862
|
-
return { Identifier: identifier, Properties: JSON.parse(rawProps) };
|
|
55863
|
-
} catch {
|
|
55864
|
-
return { Identifier: identifier, Properties: rawProps, propertiesRaw: rawProps };
|
|
55865
|
-
}
|
|
55866
|
-
}
|
|
55867
|
-
function validateTypeName(typeName) {
|
|
55868
|
-
if (!TYPE_NAME_RE.test(typeName)) {
|
|
55869
|
-
return `Invalid typeName '${typeName}'. Must be '<Namespace>::<Service>::<Resource>' in PascalCase, e.g. 'AWS::Lambda::Function', 'AWS::S3::Bucket'.`;
|
|
55870
|
-
}
|
|
55871
|
-
return null;
|
|
55872
|
-
}
|
|
55873
|
-
function validateIdentifier(id) {
|
|
55874
|
-
if (!isValidIdentifier(id)) {
|
|
55875
|
-
const preview = id.length > 40 ? `${id.slice(0, 40)}...` : id;
|
|
55876
|
-
return `Invalid identifier '${preview}'. Must be 1-2048 chars, not start with '-', and contain no control characters.`;
|
|
55877
|
-
}
|
|
55878
|
-
return null;
|
|
55879
|
-
}
|
|
55880
|
-
function validateOpaqueToken(token, fieldName) {
|
|
55881
|
-
if (!isValidOpaqueToken(token)) {
|
|
55882
|
-
return `Invalid ${fieldName}. Must be 1-128 chars, not start with '-', and contain no control characters.`;
|
|
55883
|
-
}
|
|
55884
|
-
return null;
|
|
55885
|
-
}
|
|
55886
|
-
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["SUCCESS", "FAILED", "CANCEL_COMPLETE"]);
|
|
55887
|
-
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
55888
|
-
var DEFAULT_MAX_WAIT_MS = 5 * 6e4;
|
|
55889
|
-
var MIN_POLL_INTERVAL_MS = 500;
|
|
55890
|
-
var MAX_POLL_INTERVAL_MS = 3e4;
|
|
55891
|
-
var MIN_MAX_WAIT_MS = 1e3;
|
|
55892
|
-
var MAX_MAX_WAIT_MS = 30 * 6e4;
|
|
55893
|
-
function extractProgressFields(progressEvent) {
|
|
55894
|
-
const pe = progressEvent && typeof progressEvent === "object" ? progressEvent : {};
|
|
55895
|
-
const str = (k) => {
|
|
55896
|
-
const v = pe[k];
|
|
55897
|
-
return typeof v === "string" && v.length > 0 ? v : null;
|
|
55898
|
-
};
|
|
55899
|
-
return {
|
|
55900
|
-
requestToken: str("RequestToken"),
|
|
55901
|
-
operationStatus: str("OperationStatus"),
|
|
55902
|
-
identifier: str("Identifier"),
|
|
55903
|
-
errorCode: str("ErrorCode"),
|
|
55904
|
-
statusMessage: str("StatusMessage"),
|
|
55905
|
-
retryAfter: str("RetryAfter")
|
|
55906
|
-
};
|
|
55907
|
-
}
|
|
55908
|
-
async function pollUntilTerminal(opts, awsCall = runAwsCall, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
55909
|
-
const start = Date.now();
|
|
55910
|
-
let attempts = 0;
|
|
55911
|
-
let lastEvent = null;
|
|
55912
|
-
let lastCommand = "";
|
|
55913
|
-
while (true) {
|
|
55914
|
-
attempts++;
|
|
55915
|
-
const result = await awsCall({
|
|
55916
|
-
service: "cloudcontrol",
|
|
55917
|
-
operation: "get-resource-request-status",
|
|
55918
|
-
profile: opts.profile,
|
|
55919
|
-
region: opts.region,
|
|
55920
|
-
timeoutMs: opts.timeoutMs,
|
|
55921
|
-
outputFormat: "json",
|
|
55922
|
-
extraFlags: ["--request-token", opts.requestToken]
|
|
55923
|
-
});
|
|
55924
|
-
if (!result.ok) {
|
|
55925
|
-
return {
|
|
55926
|
-
ok: false,
|
|
55927
|
-
progressEvent: lastEvent,
|
|
55928
|
-
command: result.command ?? lastCommand,
|
|
55929
|
-
attempts,
|
|
55930
|
-
elapsedMs: Date.now() - start,
|
|
55931
|
-
error: result.error,
|
|
55932
|
-
kind: result.kind,
|
|
55933
|
-
rawBody: result.rawStderr ?? result.rawStdout
|
|
55934
|
-
};
|
|
55935
|
-
}
|
|
55936
|
-
lastCommand = result.command;
|
|
55937
|
-
const raw = result.data;
|
|
55938
|
-
lastEvent = raw?.ProgressEvent ?? null;
|
|
55939
|
-
const status = lastEvent && typeof lastEvent.OperationStatus === "string" ? lastEvent.OperationStatus : null;
|
|
55940
|
-
if (status && TERMINAL_STATUSES.has(status)) {
|
|
55941
|
-
return { ok: true, progressEvent: lastEvent, command: lastCommand, attempts, elapsedMs: Date.now() - start };
|
|
55942
|
-
}
|
|
55943
|
-
const elapsed = Date.now() - start;
|
|
55944
|
-
if (elapsed >= opts.maxWaitMs) {
|
|
55945
|
-
return {
|
|
55946
|
-
ok: false,
|
|
55947
|
-
progressEvent: lastEvent,
|
|
55948
|
-
command: lastCommand,
|
|
55949
|
-
attempts,
|
|
55950
|
-
elapsedMs: elapsed,
|
|
55951
|
-
error: `Polled for ${Math.round(elapsed / 1e3)}s without reaching a terminal state (last status: ${status ?? "unknown"}). Increase maxWaitMs, or call aws_resource_status with requestToken='${opts.requestToken}' to keep checking.`
|
|
55952
|
-
};
|
|
55953
|
-
}
|
|
55954
|
-
let waitMs = opts.pollIntervalMs;
|
|
55955
|
-
const retryAfterRaw = lastEvent && typeof lastEvent.RetryAfter === "string" ? lastEvent.RetryAfter : null;
|
|
55956
|
-
if (retryAfterRaw) {
|
|
55957
|
-
const target = Date.parse(retryAfterRaw);
|
|
55958
|
-
if (!Number.isNaN(target)) {
|
|
55959
|
-
const ra = target - Date.now();
|
|
55960
|
-
if (ra > 0) waitMs = ra;
|
|
55961
|
-
}
|
|
55962
|
-
}
|
|
55963
|
-
waitMs = Math.min(waitMs, opts.maxWaitMs - elapsed);
|
|
55964
|
-
if (waitMs > 0) await sleep(waitMs);
|
|
55965
|
-
}
|
|
55966
|
-
}
|
|
55967
|
-
async function buildMutationResponse(initial, i) {
|
|
55968
|
-
const raw = initial.data;
|
|
55969
|
-
const progressEvent = raw?.ProgressEvent ?? null;
|
|
55970
|
-
const fields = extractProgressFields(progressEvent);
|
|
55971
|
-
const initialStatus = fields.operationStatus ?? "";
|
|
55972
|
-
const alreadyTerminal = TERMINAL_STATUSES.has(initialStatus);
|
|
55973
|
-
if (i.awaitCompletion && fields.requestToken && !alreadyTerminal) {
|
|
55974
|
-
const polled = await pollUntilTerminal({
|
|
55975
|
-
requestToken: fields.requestToken,
|
|
55976
|
-
profile: i.profile,
|
|
55977
|
-
region: i.region,
|
|
55978
|
-
timeoutMs: i.timeoutMs,
|
|
55979
|
-
pollIntervalMs: i.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
55980
|
-
maxWaitMs: i.maxWaitMs ?? DEFAULT_MAX_WAIT_MS
|
|
55981
|
-
});
|
|
55982
|
-
if (!polled.ok) {
|
|
55983
|
-
if (polled.kind === "sso_expired" || polled.kind === "no_creds") {
|
|
55984
|
-
const useProfile = i.profile ?? getProfile();
|
|
55985
|
-
const reLoginHint = polled.kind === "sso_expired" ? `SSO session expired while awaiting completion. Call aws_login_start with profile='${useProfile}' to re-authenticate, then call aws_resource_status with requestToken='${fields.requestToken}' to check whether the mutation completed server-side.` : `No credentials available while awaiting completion. After fixing credentials for profile '${useProfile}', call aws_resource_status with requestToken='${fields.requestToken}' to check whether the mutation completed server-side. Underlying error: ${polled.error}`;
|
|
55986
|
-
return { ok: false, error: reLoginHint, rawBody: polled.rawBody };
|
|
55987
|
-
}
|
|
55988
|
-
return { ok: false, error: polled.error ?? "Poll failed", rawBody: polled.rawBody };
|
|
55989
|
-
}
|
|
55990
|
-
const finalFields = extractProgressFields(polled.progressEvent);
|
|
55991
|
-
return {
|
|
55992
|
-
ok: true,
|
|
55993
|
-
data: {
|
|
55994
|
-
command: polled.command,
|
|
55995
|
-
...finalFields,
|
|
55996
|
-
progressEvent: polled.progressEvent,
|
|
55997
|
-
awaited: { attempts: polled.attempts, elapsedMs: polled.elapsedMs }
|
|
55998
|
-
}
|
|
55999
|
-
};
|
|
56000
|
-
}
|
|
56001
|
-
return {
|
|
56002
|
-
ok: true,
|
|
56003
|
-
data: { command: initial.command, ...fields, progressEvent }
|
|
56004
|
-
};
|
|
56005
|
-
}
|
|
56006
|
-
var baseFields = {
|
|
56007
|
-
profile: external_exports3.string().optional().describe("Override session profile for this call."),
|
|
56008
|
-
region: external_exports3.string().optional().describe("Override session region for this call."),
|
|
56009
|
-
timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000.")
|
|
56010
|
-
};
|
|
56011
|
-
var awaitFields = {
|
|
56012
|
-
awaitCompletion: external_exports3.boolean().optional().describe(
|
|
56013
|
-
"If true, poll get-resource-request-status until the operation reaches SUCCESS / FAILED / CANCEL_COMPLETE and return the final ProgressEvent. Default false (returns immediately with IN_PROGRESS, caller polls via aws_resource_status)."
|
|
56014
|
-
),
|
|
56015
|
-
pollIntervalMs: external_exports3.number().int().min(MIN_POLL_INTERVAL_MS).max(MAX_POLL_INTERVAL_MS).optional().describe(
|
|
56016
|
-
`Poll interval in ms when awaitCompletion is true (range ${MIN_POLL_INTERVAL_MS}-${MAX_POLL_INTERVAL_MS}). Default ${DEFAULT_POLL_INTERVAL_MS}. ProgressEvent.RetryAfter overrides when CCAPI returns one.`
|
|
56017
|
-
),
|
|
56018
|
-
maxWaitMs: external_exports3.number().int().min(MIN_MAX_WAIT_MS).max(MAX_MAX_WAIT_MS).optional().describe(
|
|
56019
|
-
`Maximum total wait in ms when awaitCompletion is true (range ${MIN_MAX_WAIT_MS}-${MAX_MAX_WAIT_MS}). Default ${DEFAULT_MAX_WAIT_MS}. On timeout, returns the last seen status with a hint to keep polling.`
|
|
56020
|
-
)
|
|
56021
|
-
};
|
|
56022
|
-
var resourceTools = [
|
|
56023
|
-
{
|
|
56024
|
-
name: "aws_resource_get",
|
|
56025
|
-
description: "Read a single AWS resource via Cloud Control API. Covers hundreds of resource types with a CloudFormation schema. `typeName` is '<Namespace>::<Service>::<Resource>' (e.g. 'AWS::Lambda::Function'); `identifier` is the primary key for that type (function name, bucket name, IAM role name, ARN, or composite id). Returns parsed Properties. For resources not covered by CCAPI or for data-plane operations, use aws_call.",
|
|
56026
|
-
annotations: {
|
|
56027
|
-
title: "Get an AWS resource by type + identifier",
|
|
56028
|
-
readOnlyHint: true,
|
|
56029
|
-
destructiveHint: false,
|
|
56030
|
-
idempotentHint: true,
|
|
56031
|
-
openWorldHint: true
|
|
56032
|
-
},
|
|
56033
|
-
inputSchema: external_exports3.object({
|
|
56034
|
-
typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function', 'AWS::S3::Bucket', 'AWS::IAM::Role'."),
|
|
56035
|
-
identifier: external_exports3.string().min(1).describe("Primary identifier for the resource (function name, bucket name, ARN, or composite id)."),
|
|
56036
|
-
...baseFields
|
|
56037
|
-
}),
|
|
56038
|
-
handler: async (input) => {
|
|
56039
|
-
const i = input;
|
|
56040
|
-
const tnErr = validateTypeName(i.typeName);
|
|
56041
|
-
if (tnErr) return { ok: false, error: tnErr };
|
|
56042
|
-
const idErr = validateIdentifier(i.identifier);
|
|
56043
|
-
if (idErr) return { ok: false, error: idErr };
|
|
56044
|
-
const result = await runAwsCall({
|
|
56045
|
-
service: "cloudcontrol",
|
|
56046
|
-
operation: "get-resource",
|
|
56047
|
-
profile: i.profile,
|
|
56048
|
-
region: i.region,
|
|
56049
|
-
timeoutMs: i.timeoutMs,
|
|
56050
|
-
outputFormat: "json",
|
|
56051
|
-
extraFlags: ["--type-name", i.typeName, "--identifier", i.identifier]
|
|
56052
|
-
});
|
|
56053
|
-
if (!result.ok) {
|
|
56054
|
-
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
56055
|
-
}
|
|
56056
|
-
const raw = result.data;
|
|
56057
|
-
const parsed = parseResourceProperties(raw?.ResourceDescription);
|
|
56058
|
-
return {
|
|
56059
|
-
ok: true,
|
|
56060
|
-
data: {
|
|
56061
|
-
command: result.command,
|
|
56062
|
-
typeName: raw?.TypeName ?? i.typeName,
|
|
56063
|
-
identifier: parsed.Identifier,
|
|
56064
|
-
properties: parsed.Properties,
|
|
56065
|
-
...parsed.propertiesRaw ? { propertiesRaw: parsed.propertiesRaw } : {}
|
|
56066
|
-
}
|
|
56067
|
-
};
|
|
56068
|
-
}
|
|
56069
|
-
},
|
|
56070
|
-
{
|
|
56071
|
-
name: "aws_resource_list",
|
|
56072
|
-
description: "List resources of a given type via Cloud Control API, paginated. Returns an array of {identifier, properties}, a `nextToken` (null when exhausted), and `hasMore`. Some types need parent identifiers (e.g. nested resources under a cluster); pass those as `resourceModel`.",
|
|
56073
|
-
annotations: {
|
|
56074
|
-
title: "List AWS resources of a type (paginated)",
|
|
56075
|
-
readOnlyHint: true,
|
|
56076
|
-
destructiveHint: false,
|
|
56077
|
-
idempotentHint: true,
|
|
56078
|
-
openWorldHint: true
|
|
56079
|
-
},
|
|
56080
|
-
inputSchema: external_exports3.object({
|
|
56081
|
-
typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function'."),
|
|
56082
|
-
resourceModel: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Parent identifier properties for nested types, e.g. {ClusterArn: '...'}."),
|
|
56083
|
-
maxResults: external_exports3.number().int().positive().max(100).optional().describe("Page size (1-100). Default 100."),
|
|
56084
|
-
nextToken: external_exports3.string().optional().describe("Resume cursor from the previous call's `nextToken`. Omit for the first page."),
|
|
56085
|
-
...baseFields
|
|
56086
|
-
}),
|
|
56087
|
-
handler: async (input) => {
|
|
56088
|
-
const i = input;
|
|
56089
|
-
const tnErr = validateTypeName(i.typeName);
|
|
56090
|
-
if (tnErr) return { ok: false, error: tnErr };
|
|
56091
|
-
if (i.nextToken !== void 0) {
|
|
56092
|
-
const ntErr = validateOpaqueToken(i.nextToken, "nextToken");
|
|
56093
|
-
if (ntErr) return { ok: false, error: ntErr };
|
|
56094
|
-
}
|
|
56095
|
-
const extraFlags = ["--type-name", i.typeName, "--max-results", String(i.maxResults ?? 100)];
|
|
56096
|
-
if (i.nextToken) extraFlags.push("--next-token", i.nextToken);
|
|
56097
|
-
if (i.resourceModel && Object.keys(i.resourceModel).length > 0) {
|
|
56098
|
-
extraFlags.push("--resource-model", JSON.stringify(i.resourceModel));
|
|
56099
|
-
}
|
|
56100
|
-
const result = await runAwsCall({
|
|
56101
|
-
service: "cloudcontrol",
|
|
56102
|
-
operation: "list-resources",
|
|
56103
|
-
profile: i.profile,
|
|
56104
|
-
region: i.region,
|
|
56105
|
-
timeoutMs: i.timeoutMs,
|
|
56106
|
-
outputFormat: "json",
|
|
56107
|
-
extraFlags
|
|
56108
|
-
});
|
|
56109
|
-
if (!result.ok) {
|
|
56110
|
-
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
56111
|
-
}
|
|
56112
|
-
const raw = result.data;
|
|
56113
|
-
const descriptions = Array.isArray(raw?.ResourceDescriptions) ? raw.ResourceDescriptions : [];
|
|
56114
|
-
const resources = descriptions.map((d) => {
|
|
56115
|
-
const p = parseResourceProperties(d);
|
|
56116
|
-
return { identifier: p.Identifier, properties: p.Properties };
|
|
55686
|
+
const raw = result.data;
|
|
55687
|
+
const descriptions = Array.isArray(raw?.ResourceDescriptions) ? raw.ResourceDescriptions : [];
|
|
55688
|
+
const resources = descriptions.map((d) => {
|
|
55689
|
+
const p = parseResourceProperties(d);
|
|
55690
|
+
return { identifier: p.Identifier, properties: p.Properties };
|
|
56117
55691
|
});
|
|
56118
55692
|
const nextToken = extractNextToken(raw);
|
|
56119
55693
|
return {
|
|
@@ -56228,343 +55802,791 @@ var resourceTools = [
|
|
|
56228
55802
|
if (!result.ok) {
|
|
56229
55803
|
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
56230
55804
|
}
|
|
56231
|
-
return buildMutationResponse({ command: result.command, data: result.data }, i);
|
|
55805
|
+
return buildMutationResponse({ command: result.command, data: result.data }, i);
|
|
55806
|
+
}
|
|
55807
|
+
},
|
|
55808
|
+
{
|
|
55809
|
+
name: "aws_resource_delete",
|
|
55810
|
+
description: "Delete an AWS resource via Cloud Control API. Async by default: returns a ProgressEvent with OperationStatus=IN_PROGRESS and a top-level `requestToken`. Pass `awaitCompletion: true` to have the server poll until terminal. Destructive -- double-check `identifier` before calling.",
|
|
55811
|
+
annotations: {
|
|
55812
|
+
title: "Delete an AWS resource (async via CCAPI)",
|
|
55813
|
+
readOnlyHint: false,
|
|
55814
|
+
destructiveHint: true,
|
|
55815
|
+
idempotentHint: false,
|
|
55816
|
+
openWorldHint: true
|
|
55817
|
+
},
|
|
55818
|
+
inputSchema: external_exports3.object({
|
|
55819
|
+
typeName: external_exports3.string().describe("CloudFormation type name."),
|
|
55820
|
+
identifier: external_exports3.string().min(1).describe("Primary identifier for the resource."),
|
|
55821
|
+
clientToken: external_exports3.string().optional().describe("Idempotency token (max 128 chars)."),
|
|
55822
|
+
...baseFields,
|
|
55823
|
+
...awaitFields
|
|
55824
|
+
}),
|
|
55825
|
+
handler: async (input) => {
|
|
55826
|
+
const i = input;
|
|
55827
|
+
const tnErr = validateTypeName(i.typeName);
|
|
55828
|
+
if (tnErr) return { ok: false, error: tnErr };
|
|
55829
|
+
const idErr = validateIdentifier(i.identifier);
|
|
55830
|
+
if (idErr) return { ok: false, error: idErr };
|
|
55831
|
+
if (i.clientToken !== void 0) {
|
|
55832
|
+
const ctErr = validateOpaqueToken(i.clientToken, "clientToken");
|
|
55833
|
+
if (ctErr) return { ok: false, error: ctErr };
|
|
55834
|
+
}
|
|
55835
|
+
const extraFlags = ["--type-name", i.typeName, "--identifier", i.identifier];
|
|
55836
|
+
if (i.clientToken) extraFlags.push("--client-token", i.clientToken);
|
|
55837
|
+
const result = await runAwsCall({
|
|
55838
|
+
service: "cloudcontrol",
|
|
55839
|
+
operation: "delete-resource",
|
|
55840
|
+
profile: i.profile,
|
|
55841
|
+
region: i.region,
|
|
55842
|
+
timeoutMs: i.timeoutMs,
|
|
55843
|
+
outputFormat: "json",
|
|
55844
|
+
extraFlags
|
|
55845
|
+
});
|
|
55846
|
+
if (!result.ok) {
|
|
55847
|
+
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
55848
|
+
}
|
|
55849
|
+
return buildMutationResponse({ command: result.command, data: result.data }, i);
|
|
55850
|
+
}
|
|
55851
|
+
},
|
|
55852
|
+
{
|
|
55853
|
+
name: "aws_resource_status",
|
|
55854
|
+
description: "Poll the status of an async Cloud Control API request (create/update/delete). Pass the `requestToken` returned by those tools. Returns the current ProgressEvent with OperationStatus: PENDING | IN_PROGRESS | SUCCESS | FAILED | CANCEL_IN_PROGRESS | CANCEL_COMPLETE.",
|
|
55855
|
+
annotations: {
|
|
55856
|
+
title: "Get the status of an async CCAPI request",
|
|
55857
|
+
readOnlyHint: true,
|
|
55858
|
+
destructiveHint: false,
|
|
55859
|
+
idempotentHint: true,
|
|
55860
|
+
openWorldHint: true
|
|
55861
|
+
},
|
|
55862
|
+
inputSchema: external_exports3.object({
|
|
55863
|
+
requestToken: external_exports3.string().min(1).describe("RequestToken from a previous create/update/delete call."),
|
|
55864
|
+
...baseFields
|
|
55865
|
+
}),
|
|
55866
|
+
handler: async (input) => {
|
|
55867
|
+
const i = input;
|
|
55868
|
+
const rtErr = validateOpaqueToken(i.requestToken, "requestToken");
|
|
55869
|
+
if (rtErr) return { ok: false, error: rtErr };
|
|
55870
|
+
const result = await runAwsCall({
|
|
55871
|
+
service: "cloudcontrol",
|
|
55872
|
+
operation: "get-resource-request-status",
|
|
55873
|
+
profile: i.profile,
|
|
55874
|
+
region: i.region,
|
|
55875
|
+
timeoutMs: i.timeoutMs,
|
|
55876
|
+
outputFormat: "json",
|
|
55877
|
+
extraFlags: ["--request-token", i.requestToken]
|
|
55878
|
+
});
|
|
55879
|
+
if (!result.ok) {
|
|
55880
|
+
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
55881
|
+
}
|
|
55882
|
+
const raw = result.data;
|
|
55883
|
+
const progressEvent = raw?.ProgressEvent ?? null;
|
|
55884
|
+
const fields = extractProgressFields(progressEvent);
|
|
55885
|
+
return {
|
|
55886
|
+
ok: true,
|
|
55887
|
+
data: { command: result.command, ...fields, progressEvent }
|
|
55888
|
+
};
|
|
55889
|
+
}
|
|
55890
|
+
},
|
|
55891
|
+
{
|
|
55892
|
+
name: "aws_resource_diff",
|
|
55893
|
+
description: "Dry-run a CCAPI update: fetch the current resource state, simulate applying a JSON Patch in memory, and return before/after plus a flat list of changed paths. No mutation is sent to AWS. Use this before aws_resource_update to verify the patch does what you expect. Supports the add/remove/replace subset of RFC 6902 (covers the vast majority of CCAPI updates); 'move'/'copy'/'test' are rejected at schema validation -- use aws_resource_update directly if you need those (CCAPI accepts them, this preview tool just doesn't simulate them locally).",
|
|
55894
|
+
annotations: {
|
|
55895
|
+
title: "Preview a CCAPI update without applying it",
|
|
55896
|
+
readOnlyHint: true,
|
|
55897
|
+
destructiveHint: false,
|
|
55898
|
+
idempotentHint: true,
|
|
55899
|
+
openWorldHint: true
|
|
55900
|
+
},
|
|
55901
|
+
inputSchema: external_exports3.object({
|
|
55902
|
+
typeName: external_exports3.string().describe("CloudFormation type name, e.g. 'AWS::Lambda::Function'."),
|
|
55903
|
+
identifier: external_exports3.string().min(1).describe("Primary identifier for the resource."),
|
|
55904
|
+
patchDocument: external_exports3.array(
|
|
55905
|
+
external_exports3.object({
|
|
55906
|
+
// Diff simulates patches locally via applyJsonPatch; only the
|
|
55907
|
+
// add/remove/replace subset is implemented. Reject the other
|
|
55908
|
+
// three RFC 6902 ops here so the model gets schema-validation
|
|
55909
|
+
// feedback instead of a runtime "not implemented" error
|
|
55910
|
+
// surfaced as a generic "Patch application failed". The
|
|
55911
|
+
// sibling aws_resource_update tool accepts the full op set
|
|
55912
|
+
// because CCAPI does -- only this preview tool is restricted.
|
|
55913
|
+
op: external_exports3.enum(["add", "remove", "replace"]),
|
|
55914
|
+
path: external_exports3.string(),
|
|
55915
|
+
value: external_exports3.unknown().optional(),
|
|
55916
|
+
from: external_exports3.string().optional()
|
|
55917
|
+
})
|
|
55918
|
+
).min(1).describe(
|
|
55919
|
+
"RFC 6902 JSON Patch (add/remove/replace subset). For move/copy/test, use aws_resource_update directly."
|
|
55920
|
+
),
|
|
55921
|
+
...baseFields
|
|
55922
|
+
}),
|
|
55923
|
+
handler: async (input) => {
|
|
55924
|
+
const i = input;
|
|
55925
|
+
const tnErr = validateTypeName(i.typeName);
|
|
55926
|
+
if (tnErr) return { ok: false, error: tnErr };
|
|
55927
|
+
const idErr = validateIdentifier(i.identifier);
|
|
55928
|
+
if (idErr) return { ok: false, error: idErr };
|
|
55929
|
+
const getResult = await runAwsCall({
|
|
55930
|
+
service: "cloudcontrol",
|
|
55931
|
+
operation: "get-resource",
|
|
55932
|
+
profile: i.profile,
|
|
55933
|
+
region: i.region,
|
|
55934
|
+
timeoutMs: i.timeoutMs,
|
|
55935
|
+
outputFormat: "json",
|
|
55936
|
+
extraFlags: ["--type-name", i.typeName, "--identifier", i.identifier]
|
|
55937
|
+
});
|
|
55938
|
+
if (!getResult.ok) {
|
|
55939
|
+
return { ok: false, error: getResult.error, rawBody: getResult.rawStderr ?? getResult.rawStdout };
|
|
55940
|
+
}
|
|
55941
|
+
const raw = getResult.data;
|
|
55942
|
+
const parsed = parseResourceProperties(raw?.ResourceDescription);
|
|
55943
|
+
const before = parsed.Properties;
|
|
55944
|
+
let after;
|
|
55945
|
+
try {
|
|
55946
|
+
after = applyJsonPatch(before, i.patchDocument);
|
|
55947
|
+
} catch (err) {
|
|
55948
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55949
|
+
return { ok: false, error: `Patch application failed: ${msg}` };
|
|
55950
|
+
}
|
|
55951
|
+
const changes = summarizePatch(i.patchDocument, before, after);
|
|
55952
|
+
return {
|
|
55953
|
+
ok: true,
|
|
55954
|
+
data: {
|
|
55955
|
+
command: getResult.command,
|
|
55956
|
+
typeName: i.typeName,
|
|
55957
|
+
identifier: parsed.Identifier ?? i.identifier,
|
|
55958
|
+
before,
|
|
55959
|
+
after,
|
|
55960
|
+
changes,
|
|
55961
|
+
changeCount: changes.length
|
|
55962
|
+
}
|
|
55963
|
+
};
|
|
55964
|
+
}
|
|
55965
|
+
}
|
|
55966
|
+
];
|
|
55967
|
+
function parseJsonPointer(pointer) {
|
|
55968
|
+
if (pointer === "") return [];
|
|
55969
|
+
if (!pointer.startsWith("/")) {
|
|
55970
|
+
throw new Error(`Invalid JSON Pointer '${pointer}': must start with '/' or be empty.`);
|
|
55971
|
+
}
|
|
55972
|
+
return pointer.slice(1).split("/").map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
55973
|
+
}
|
|
55974
|
+
function clone2(v) {
|
|
55975
|
+
return v === void 0 ? v : JSON.parse(JSON.stringify(v));
|
|
55976
|
+
}
|
|
55977
|
+
function isObj(v) {
|
|
55978
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
55979
|
+
}
|
|
55980
|
+
function applyJsonPatch(original, ops) {
|
|
55981
|
+
const doc = clone2(original);
|
|
55982
|
+
return _applyJsonPatchInPlace(doc, ops);
|
|
55983
|
+
}
|
|
55984
|
+
function _applyJsonPatchInPlace(doc, ops) {
|
|
55985
|
+
let root = doc;
|
|
55986
|
+
for (let i = 0; i < ops.length; i++) {
|
|
55987
|
+
const op = ops[i];
|
|
55988
|
+
if (op.op === "move" || op.op === "copy" || op.op === "test") {
|
|
55989
|
+
throw new Error(`op '${op.op}' at index ${i} is not implemented in aws_resource_diff (use add/remove/replace).`);
|
|
55990
|
+
}
|
|
55991
|
+
const tokens = parseJsonPointer(op.path);
|
|
55992
|
+
if (tokens.length === 0) {
|
|
55993
|
+
if (op.op === "remove") {
|
|
55994
|
+
throw new Error(`Cannot remove the document root at index ${i}.`);
|
|
55995
|
+
}
|
|
55996
|
+
root = clone2(op.value);
|
|
55997
|
+
continue;
|
|
55998
|
+
}
|
|
55999
|
+
const parentTokens = tokens.slice(0, -1);
|
|
56000
|
+
const lastToken = tokens[tokens.length - 1];
|
|
56001
|
+
let parent = root;
|
|
56002
|
+
for (let t = 0; t < parentTokens.length; t++) {
|
|
56003
|
+
const segment = parentTokens[t];
|
|
56004
|
+
if (Array.isArray(parent)) {
|
|
56005
|
+
const idx = Number.parseInt(segment, 10);
|
|
56006
|
+
if (!Number.isInteger(idx) || idx < 0) {
|
|
56007
|
+
throw new Error(`Path '${op.path}' segment '${segment}' is not a valid array index at op index ${i}.`);
|
|
56008
|
+
}
|
|
56009
|
+
if (idx >= parent.length) {
|
|
56010
|
+
throw new Error(
|
|
56011
|
+
`Path '${op.path}' cannot traverse into intermediate array element at segment '${segment}': array has length ${parent.length}, so index ${idx} does not exist yet (at op index ${i}).`
|
|
56012
|
+
);
|
|
56013
|
+
}
|
|
56014
|
+
parent = parent[idx];
|
|
56015
|
+
} else if (isObj(parent)) {
|
|
56016
|
+
if (!(segment in parent)) {
|
|
56017
|
+
if (op.op === "add") {
|
|
56018
|
+
parent[segment] = {};
|
|
56019
|
+
parent = parent[segment];
|
|
56020
|
+
continue;
|
|
56021
|
+
}
|
|
56022
|
+
throw new Error(`Path '${op.path}' segment '${segment}' does not exist at index ${i}.`);
|
|
56023
|
+
}
|
|
56024
|
+
parent = parent[segment];
|
|
56025
|
+
} else {
|
|
56026
|
+
throw new Error(`Path '${op.path}' traverses a non-container value at index ${i}.`);
|
|
56027
|
+
}
|
|
56232
56028
|
}
|
|
56233
|
-
|
|
56234
|
-
|
|
56235
|
-
|
|
56236
|
-
|
|
56237
|
-
|
|
56238
|
-
|
|
56239
|
-
|
|
56240
|
-
destructiveHint: true,
|
|
56241
|
-
idempotentHint: false,
|
|
56242
|
-
openWorldHint: true
|
|
56243
|
-
},
|
|
56244
|
-
inputSchema: external_exports3.object({
|
|
56245
|
-
typeName: external_exports3.string().describe("CloudFormation type name."),
|
|
56246
|
-
identifier: external_exports3.string().min(1).describe("Primary identifier for the resource."),
|
|
56247
|
-
clientToken: external_exports3.string().optional().describe("Idempotency token (max 128 chars)."),
|
|
56248
|
-
...baseFields,
|
|
56249
|
-
...awaitFields
|
|
56250
|
-
}),
|
|
56251
|
-
handler: async (input) => {
|
|
56252
|
-
const i = input;
|
|
56253
|
-
const tnErr = validateTypeName(i.typeName);
|
|
56254
|
-
if (tnErr) return { ok: false, error: tnErr };
|
|
56255
|
-
const idErr = validateIdentifier(i.identifier);
|
|
56256
|
-
if (idErr) return { ok: false, error: idErr };
|
|
56257
|
-
if (i.clientToken !== void 0) {
|
|
56258
|
-
const ctErr = validateOpaqueToken(i.clientToken, "clientToken");
|
|
56259
|
-
if (ctErr) return { ok: false, error: ctErr };
|
|
56029
|
+
if (Array.isArray(parent)) {
|
|
56030
|
+
if (lastToken === "-") {
|
|
56031
|
+
if (op.op === "remove") {
|
|
56032
|
+
throw new Error(`Cannot remove '-' (end-of-array) at index ${i}.`);
|
|
56033
|
+
}
|
|
56034
|
+
parent.push(clone2(op.value));
|
|
56035
|
+
continue;
|
|
56260
56036
|
}
|
|
56261
|
-
const
|
|
56262
|
-
if (
|
|
56263
|
-
|
|
56264
|
-
service: "cloudcontrol",
|
|
56265
|
-
operation: "delete-resource",
|
|
56266
|
-
profile: i.profile,
|
|
56267
|
-
region: i.region,
|
|
56268
|
-
timeoutMs: i.timeoutMs,
|
|
56269
|
-
outputFormat: "json",
|
|
56270
|
-
extraFlags
|
|
56271
|
-
});
|
|
56272
|
-
if (!result.ok) {
|
|
56273
|
-
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
56037
|
+
const idx = Number.parseInt(lastToken, 10);
|
|
56038
|
+
if (!Number.isInteger(idx) || idx < 0) {
|
|
56039
|
+
throw new Error(`Path '${op.path}' has non-integer array index '${lastToken}' at index ${i}.`);
|
|
56274
56040
|
}
|
|
56275
|
-
|
|
56041
|
+
if (op.op === "add") {
|
|
56042
|
+
if (idx > parent.length) {
|
|
56043
|
+
throw new Error(`Add index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
|
|
56044
|
+
}
|
|
56045
|
+
parent.splice(idx, 0, clone2(op.value));
|
|
56046
|
+
} else if (op.op === "remove") {
|
|
56047
|
+
if (idx >= parent.length) {
|
|
56048
|
+
throw new Error(`Remove index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
|
|
56049
|
+
}
|
|
56050
|
+
parent.splice(idx, 1);
|
|
56051
|
+
} else {
|
|
56052
|
+
if (idx >= parent.length) {
|
|
56053
|
+
throw new Error(`Replace index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
|
|
56054
|
+
}
|
|
56055
|
+
parent[idx] = clone2(op.value);
|
|
56056
|
+
}
|
|
56057
|
+
} else if (isObj(parent)) {
|
|
56058
|
+
if (op.op === "remove") {
|
|
56059
|
+
if (!(lastToken in parent)) {
|
|
56060
|
+
throw new Error(`Cannot remove missing key '${lastToken}' at index ${i}.`);
|
|
56061
|
+
}
|
|
56062
|
+
delete parent[lastToken];
|
|
56063
|
+
} else if (op.op === "replace") {
|
|
56064
|
+
if (!(lastToken in parent)) {
|
|
56065
|
+
throw new Error(`Cannot replace missing key '${lastToken}' at index ${i} (use 'add' to create it).`);
|
|
56066
|
+
}
|
|
56067
|
+
parent[lastToken] = clone2(op.value);
|
|
56068
|
+
} else {
|
|
56069
|
+
parent[lastToken] = clone2(op.value);
|
|
56070
|
+
}
|
|
56071
|
+
} else {
|
|
56072
|
+
throw new Error(`Path '${op.path}' parent is not a container at index ${i}.`);
|
|
56276
56073
|
}
|
|
56277
|
-
}
|
|
56074
|
+
}
|
|
56075
|
+
return root;
|
|
56076
|
+
}
|
|
56077
|
+
function summarizePatch(ops, before, after) {
|
|
56078
|
+
let working;
|
|
56079
|
+
let replayOk = true;
|
|
56080
|
+
try {
|
|
56081
|
+
working = clone2(before);
|
|
56082
|
+
} catch {
|
|
56083
|
+
replayOk = false;
|
|
56084
|
+
working = void 0;
|
|
56085
|
+
}
|
|
56086
|
+
const out = [];
|
|
56087
|
+
for (const op of ops) {
|
|
56088
|
+
const beforeAt = resolvePointer(before, op.path);
|
|
56089
|
+
let afterAt;
|
|
56090
|
+
if (replayOk) {
|
|
56091
|
+
try {
|
|
56092
|
+
working = _applyJsonPatchInPlace(working, [op]);
|
|
56093
|
+
afterAt = resolvePointer(working, op.path);
|
|
56094
|
+
} catch {
|
|
56095
|
+
replayOk = false;
|
|
56096
|
+
afterAt = resolvePointer(after, op.path);
|
|
56097
|
+
}
|
|
56098
|
+
} else {
|
|
56099
|
+
afterAt = resolvePointer(after, op.path);
|
|
56100
|
+
}
|
|
56101
|
+
if (op.op === "add" && afterAt === void 0 && op.path.endsWith("/-")) {
|
|
56102
|
+
afterAt = op.value;
|
|
56103
|
+
}
|
|
56104
|
+
out.push({ op: op.op, path: op.path, before: beforeAt, after: afterAt });
|
|
56105
|
+
}
|
|
56106
|
+
return out;
|
|
56107
|
+
}
|
|
56108
|
+
function resolvePointer(doc, pointer) {
|
|
56109
|
+
let tokens;
|
|
56110
|
+
try {
|
|
56111
|
+
tokens = parseJsonPointer(pointer);
|
|
56112
|
+
} catch {
|
|
56113
|
+
return void 0;
|
|
56114
|
+
}
|
|
56115
|
+
let cur = doc;
|
|
56116
|
+
for (const t of tokens) {
|
|
56117
|
+
if (Array.isArray(cur)) {
|
|
56118
|
+
if (t === "-") return void 0;
|
|
56119
|
+
const idx = Number.parseInt(t, 10);
|
|
56120
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= cur.length) return void 0;
|
|
56121
|
+
cur = cur[idx];
|
|
56122
|
+
} else if (isObj(cur)) {
|
|
56123
|
+
if (!(t in cur)) return void 0;
|
|
56124
|
+
cur = cur[t];
|
|
56125
|
+
} else {
|
|
56126
|
+
return void 0;
|
|
56127
|
+
}
|
|
56128
|
+
}
|
|
56129
|
+
return cur;
|
|
56130
|
+
}
|
|
56131
|
+
|
|
56132
|
+
// src/tools/paginate.ts
|
|
56133
|
+
function extractNextToken(data) {
|
|
56134
|
+
if (data && typeof data === "object" && "NextToken" in data) {
|
|
56135
|
+
const token = data.NextToken;
|
|
56136
|
+
if (typeof token === "string" && token.length > 0) return token;
|
|
56137
|
+
}
|
|
56138
|
+
return null;
|
|
56139
|
+
}
|
|
56140
|
+
function wrapQueryForPagination(userQuery) {
|
|
56141
|
+
return `{NextToken: NextToken, items: ${userQuery}}`;
|
|
56142
|
+
}
|
|
56143
|
+
var paginateTools = [
|
|
56278
56144
|
{
|
|
56279
|
-
name: "
|
|
56280
|
-
description: "
|
|
56145
|
+
name: "aws_paginate",
|
|
56146
|
+
description: "Fetch one page of a paginated AWS list/describe operation. Identical to aws_call plus `maxItems` (page size) and `startingToken` (resume cursor). When `query` is supplied it is wrapped server-side as {NextToken, items: <query>} so pagination survives a projection that would otherwise drop NextToken; the handler unwraps `items` before returning. Returns the parsed response, a `nextToken` (null when the list is exhausted), and `hasMore`. Call again with the returned nextToken as startingToken until hasMore is false. Use this instead of aws_call for operations that might exceed the 5 MB stdout cap: list-objects-v2, describe-instances, describe-log-streams, list-roles, etc.",
|
|
56281
56147
|
annotations: {
|
|
56282
|
-
title: "
|
|
56148
|
+
title: "Fetch one page of a paginated AWS operation",
|
|
56283
56149
|
readOnlyHint: true,
|
|
56284
56150
|
destructiveHint: false,
|
|
56285
56151
|
idempotentHint: true,
|
|
56286
56152
|
openWorldHint: true
|
|
56287
56153
|
},
|
|
56288
56154
|
inputSchema: external_exports3.object({
|
|
56289
|
-
|
|
56290
|
-
|
|
56155
|
+
service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', 'logs', etc."),
|
|
56156
|
+
operation: external_exports3.string().describe("Paginated operation: 'list-objects-v2', 'describe-instances', 'list-roles', etc."),
|
|
56157
|
+
params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) passed via --cli-input-json."),
|
|
56158
|
+
query: external_exports3.string().optional().describe(
|
|
56159
|
+
"JMESPath expression to extract fields from each page (--query). The query is wrapped server-side as {NextToken, items: <query>} so pagination still works even when the projection drops NextToken; the handler unwraps `items` before returning."
|
|
56160
|
+
),
|
|
56161
|
+
maxItems: external_exports3.number().int().positive().max(1e4).optional().describe("Items per page (1-10000). Default 100. Lower this if hitting the 5 MB output cap."),
|
|
56162
|
+
startingToken: external_exports3.string().optional().describe("Resume cursor from the previous call's `nextToken`. Omit for the first page."),
|
|
56163
|
+
profile: external_exports3.string().optional().describe("Override session profile for this call."),
|
|
56164
|
+
region: external_exports3.string().optional().describe("Override session region for this call."),
|
|
56165
|
+
timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000.")
|
|
56291
56166
|
}),
|
|
56292
56167
|
handler: async (input) => {
|
|
56293
56168
|
const i = input;
|
|
56294
|
-
|
|
56295
|
-
|
|
56169
|
+
if (i.startingToken !== void 0) {
|
|
56170
|
+
const stErr = validateOpaqueToken(i.startingToken, "startingToken");
|
|
56171
|
+
if (stErr) return { ok: false, error: stErr };
|
|
56172
|
+
}
|
|
56173
|
+
const maxItems = Math.min(Math.max(1, i.maxItems ?? 100), 1e4);
|
|
56174
|
+
const extraFlags = ["--max-items", String(maxItems)];
|
|
56175
|
+
if (i.startingToken) {
|
|
56176
|
+
extraFlags.push("--starting-token", i.startingToken);
|
|
56177
|
+
}
|
|
56178
|
+
const userQuery = i.query?.trim();
|
|
56179
|
+
const queryWrapped = userQuery ? wrapQueryForPagination(userQuery) : void 0;
|
|
56296
56180
|
const result = await runAwsCall({
|
|
56297
|
-
service:
|
|
56298
|
-
operation:
|
|
56181
|
+
service: i.service,
|
|
56182
|
+
operation: i.operation,
|
|
56183
|
+
params: i.params,
|
|
56184
|
+
query: queryWrapped,
|
|
56299
56185
|
profile: i.profile,
|
|
56300
56186
|
region: i.region,
|
|
56301
|
-
timeoutMs: i.timeoutMs,
|
|
56302
56187
|
outputFormat: "json",
|
|
56303
|
-
|
|
56188
|
+
timeoutMs: i.timeoutMs,
|
|
56189
|
+
extraFlags
|
|
56304
56190
|
});
|
|
56305
56191
|
if (!result.ok) {
|
|
56306
56192
|
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
56307
56193
|
}
|
|
56308
|
-
|
|
56309
|
-
|
|
56310
|
-
|
|
56194
|
+
let resultBody;
|
|
56195
|
+
let nextToken;
|
|
56196
|
+
if (queryWrapped) {
|
|
56197
|
+
const wrapped = result.data ?? {};
|
|
56198
|
+
nextToken = extractNextToken(wrapped);
|
|
56199
|
+
resultBody = wrapped.items ?? null;
|
|
56200
|
+
} else {
|
|
56201
|
+
nextToken = extractNextToken(result.data);
|
|
56202
|
+
resultBody = result.data;
|
|
56203
|
+
}
|
|
56311
56204
|
return {
|
|
56312
56205
|
ok: true,
|
|
56313
|
-
data: {
|
|
56206
|
+
data: {
|
|
56207
|
+
command: result.command,
|
|
56208
|
+
result: resultBody,
|
|
56209
|
+
nextToken,
|
|
56210
|
+
hasMore: nextToken !== null
|
|
56211
|
+
}
|
|
56314
56212
|
};
|
|
56315
56213
|
}
|
|
56316
|
-
}
|
|
56214
|
+
}
|
|
56215
|
+
];
|
|
56216
|
+
|
|
56217
|
+
// src/tools/metrics.ts
|
|
56218
|
+
var SIMPLE_STATS = ["Average", "Sum", "Maximum", "Minimum", "SampleCount"];
|
|
56219
|
+
var EXTENDED_STAT_RE = /^((p|tm|tc|wm|pr|ts)(\d{1,3}(\.\d{1,3})?)?|iqm)$/i;
|
|
56220
|
+
function isValidStatistic(s) {
|
|
56221
|
+
const lower = s.toLowerCase();
|
|
56222
|
+
if (SIMPLE_STATS.some((stat) => stat.toLowerCase() === lower)) return true;
|
|
56223
|
+
return EXTENDED_STAT_RE.test(s);
|
|
56224
|
+
}
|
|
56225
|
+
function canonicalizeStatistic(s) {
|
|
56226
|
+
const lower = s.toLowerCase();
|
|
56227
|
+
for (const stat of SIMPLE_STATS) {
|
|
56228
|
+
if (stat.toLowerCase() === lower) return stat;
|
|
56229
|
+
}
|
|
56230
|
+
if (EXTENDED_STAT_RE.test(s)) return lower;
|
|
56231
|
+
return s;
|
|
56232
|
+
}
|
|
56233
|
+
var QUERY_ID_RE = /^[a-z][A-Za-z0-9_]*$/;
|
|
56234
|
+
var MAX_QUERIES = 100;
|
|
56235
|
+
var CLOUDWATCH_MAX_DATAPOINTS = 100800;
|
|
56236
|
+
var PERIOD_3H_MS = 3 * 60 * 60 * 1e3;
|
|
56237
|
+
var PERIOD_24H_MS = 24 * 60 * 60 * 1e3;
|
|
56238
|
+
var PERIOD_15D_MS = 15 * 24 * 60 * 60 * 1e3;
|
|
56239
|
+
function pickAutoPeriodSeconds(startMs, endMs) {
|
|
56240
|
+
const rangeMs = Math.max(0, endMs - startMs);
|
|
56241
|
+
if (rangeMs <= PERIOD_3H_MS) return 60;
|
|
56242
|
+
if (rangeMs <= PERIOD_24H_MS) return 300;
|
|
56243
|
+
if (rangeMs <= PERIOD_15D_MS) return 900;
|
|
56244
|
+
return 3600;
|
|
56245
|
+
}
|
|
56246
|
+
var RELATIVE_TIME_RE = /^\d+[smhdw]$/;
|
|
56247
|
+
var UNIT_MS = {
|
|
56248
|
+
s: 1e3,
|
|
56249
|
+
m: 60 * 1e3,
|
|
56250
|
+
h: 60 * 60 * 1e3,
|
|
56251
|
+
d: 24 * 60 * 60 * 1e3,
|
|
56252
|
+
w: 7 * 24 * 60 * 60 * 1e3
|
|
56253
|
+
};
|
|
56254
|
+
function resolveTime(input, now) {
|
|
56255
|
+
if (input === "now") return new Date(now);
|
|
56256
|
+
const rel = input.match(RELATIVE_TIME_RE);
|
|
56257
|
+
if (rel) {
|
|
56258
|
+
const num = Number(input.slice(0, -1));
|
|
56259
|
+
const unit = input.slice(-1);
|
|
56260
|
+
const ms = UNIT_MS[unit];
|
|
56261
|
+
if (!ms || !Number.isFinite(num)) return null;
|
|
56262
|
+
return new Date(now - num * ms);
|
|
56263
|
+
}
|
|
56264
|
+
const t = new Date(input);
|
|
56265
|
+
if (Number.isNaN(t.getTime())) return null;
|
|
56266
|
+
return t;
|
|
56267
|
+
}
|
|
56268
|
+
function buildMetricDataQueries(inputs, autoPeriod) {
|
|
56269
|
+
return inputs.map((q) => {
|
|
56270
|
+
const base = { Id: q.id };
|
|
56271
|
+
if (q.label !== void 0) base.Label = q.label;
|
|
56272
|
+
if (q.returnData !== void 0) base.ReturnData = q.returnData;
|
|
56273
|
+
if (q.expression !== void 0) {
|
|
56274
|
+
base.Expression = q.expression;
|
|
56275
|
+
if (q.period !== void 0) base.Period = q.period;
|
|
56276
|
+
return base;
|
|
56277
|
+
}
|
|
56278
|
+
const dimensions = q.dimensions ? Object.entries(q.dimensions).map(([Name, Value]) => ({ Name, Value })) : void 0;
|
|
56279
|
+
const stat = {
|
|
56280
|
+
Metric: {
|
|
56281
|
+
Namespace: q.namespace,
|
|
56282
|
+
MetricName: q.metricName,
|
|
56283
|
+
...dimensions ? { Dimensions: dimensions } : {}
|
|
56284
|
+
},
|
|
56285
|
+
Period: q.period ?? autoPeriod,
|
|
56286
|
+
Stat: q.statistic !== void 0 ? canonicalizeStatistic(q.statistic) : "Average"
|
|
56287
|
+
};
|
|
56288
|
+
if (q.unit !== void 0) stat.Unit = q.unit;
|
|
56289
|
+
base.MetricStat = stat;
|
|
56290
|
+
return base;
|
|
56291
|
+
});
|
|
56292
|
+
}
|
|
56293
|
+
var metricsTools = [
|
|
56317
56294
|
{
|
|
56318
|
-
name: "
|
|
56319
|
-
description: "
|
|
56295
|
+
name: "aws_metrics_query",
|
|
56296
|
+
description: "Query CloudWatch metrics via GetMetricData (the modern multi-metric / expression-capable API, not the legacy get-metric-statistics). Pass `queries` as a flat array of {id, namespace, metricName, dimensions?, statistic?, period?, expression?, label?}; the tool shapes them into MetricDataQueries for you. `startTime`/`endTime` accept ISO 8601 or relative shorthand ('15m', '1h', '1d', '1w'); endTime defaults to 'now'. Period is auto-picked from the time range when omitted (60s for <=3h, 300s for <=24h, 900s for <=15d, 3600s otherwise) to stay under CloudWatch's ~100,800-datapoint response cap. Returns {series: [{id, label?, timestamps, values, period?, statusCode?}], messages?, periodSeconds, profile, region, nextToken, hasMore}. Each series' `period` is the effective granularity for that query (its explicit period, or the auto-pick it inherited); it is omitted for an expression query that didn't set one. The top-level `periodSeconds` is always the auto-pick. When CloudWatch truncates a large response, `hasMore` is true and `nextToken` carries the resume cursor -- call again with `nextToken` set to fetch the next page (rare for typical agent queries that stay within the per-request cap). Use for 'show me the CPU on this instance for the last hour', 'sum lambda invocations across these 3 functions', or expression-based 'p99 latency divided by average latency' lookups.",
|
|
56320
56297
|
annotations: {
|
|
56321
|
-
title: "
|
|
56298
|
+
title: "Query CloudWatch metrics (GetMetricData)",
|
|
56322
56299
|
readOnlyHint: true,
|
|
56323
56300
|
destructiveHint: false,
|
|
56324
56301
|
idempotentHint: true,
|
|
56325
56302
|
openWorldHint: true
|
|
56326
56303
|
},
|
|
56327
56304
|
inputSchema: external_exports3.object({
|
|
56328
|
-
|
|
56329
|
-
identifier: external_exports3.string().min(1).describe("Primary identifier for the resource."),
|
|
56330
|
-
patchDocument: external_exports3.array(
|
|
56305
|
+
queries: external_exports3.array(
|
|
56331
56306
|
external_exports3.object({
|
|
56332
|
-
|
|
56333
|
-
|
|
56334
|
-
|
|
56335
|
-
|
|
56336
|
-
|
|
56337
|
-
|
|
56338
|
-
|
|
56339
|
-
|
|
56340
|
-
|
|
56341
|
-
|
|
56342
|
-
|
|
56307
|
+
id: external_exports3.string().regex(QUERY_ID_RE, "id must match /^[a-z][A-Za-z0-9_]*$/ (CloudWatch's MetricDataQuery.Id contract)"),
|
|
56308
|
+
namespace: external_exports3.string().min(1).optional().describe("AWS metric namespace, e.g. 'AWS/Lambda', 'AWS/EC2'. Required unless `expression` is set."),
|
|
56309
|
+
metricName: external_exports3.string().min(1).optional().describe("Metric name, e.g. 'Invocations', 'CPUUtilization'. Required unless `expression` is set."),
|
|
56310
|
+
dimensions: external_exports3.record(external_exports3.string(), external_exports3.string()).optional().describe("Dimension Name -> Value map, e.g. {FunctionName: 'my-fn'}."),
|
|
56311
|
+
statistic: external_exports3.string().optional().describe(
|
|
56312
|
+
"Statistic: Average | Sum | Maximum | Minimum | SampleCount, or an extended stat like 'p99', 'p99.9', 'tm95'. Default 'Average'."
|
|
56313
|
+
),
|
|
56314
|
+
period: external_exports3.number().int().positive().optional().describe("Period in seconds. Defaults to an auto-pick from the time range (60s/300s/900s/3600s)."),
|
|
56315
|
+
expression: external_exports3.string().min(1).optional().describe(
|
|
56316
|
+
`CloudWatch metric math expression, e.g. 'SUM([m1, m2])' or 'AVG(METRICS("AWS/Lambda"))'. Mutually exclusive with namespace/metricName/dimensions.`
|
|
56317
|
+
),
|
|
56318
|
+
label: external_exports3.string().optional().describe("Human-readable label for the series in the response."),
|
|
56319
|
+
returnData: external_exports3.boolean().optional().describe(
|
|
56320
|
+
"Set false to compute this query but not return its data (useful for intermediate values in expressions). Default true."
|
|
56321
|
+
),
|
|
56322
|
+
unit: external_exports3.string().optional().describe(
|
|
56323
|
+
"Restrict to a specific Unit (e.g. 'Seconds', 'Bytes'). Default: no filter. Only meaningful on metric-stat queries."
|
|
56324
|
+
)
|
|
56343
56325
|
})
|
|
56344
|
-
).min(1).describe(
|
|
56345
|
-
|
|
56326
|
+
).min(1).max(MAX_QUERIES).describe(`1-${MAX_QUERIES} queries. Each is either a metric-stat (namespace + metricName) or an expression.`),
|
|
56327
|
+
startTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand ('15m', '1h', '1d', '1w'). Default '1h' (one hour ago)."),
|
|
56328
|
+
endTime: external_exports3.string().optional().describe("ISO 8601 timestamp or relative shorthand. Default 'now'."),
|
|
56329
|
+
scanBy: external_exports3.enum(["TimestampAscending", "TimestampDescending"]).optional().describe("Sort order for returned datapoints. Default 'TimestampDescending' (matches CloudWatch's default)."),
|
|
56330
|
+
maxDataPoints: external_exports3.number().int().positive().optional().describe(
|
|
56331
|
+
"Target datapoint count. CloudWatch does not truncate to the first N points -- it widens (coarsens) the period server-side so the series aggregates down to fit this many points. CloudWatch's own ceiling is ~100,800; lower this to make CloudWatch return a coarser, smaller series. Forwarded as CloudWatch's MaxDatapoints (single 'p') field; the camelCase schema name follows this server's convention."
|
|
56346
56332
|
),
|
|
56347
|
-
|
|
56333
|
+
nextToken: external_exports3.string().optional().describe(
|
|
56334
|
+
"Resume cursor from a previous call's `nextToken`. Omit for the first page. Forwarded as CloudWatch's NextToken; only meaningful when a prior call returned `hasMore: true`."
|
|
56335
|
+
),
|
|
56336
|
+
profile: external_exports3.string().optional().describe("Override session profile for this call."),
|
|
56337
|
+
region: external_exports3.string().optional().describe("Override session region for this call."),
|
|
56338
|
+
timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in milliseconds. Default 60000 (60s).")
|
|
56348
56339
|
}),
|
|
56349
56340
|
handler: async (input) => {
|
|
56350
56341
|
const i = input;
|
|
56351
|
-
const
|
|
56352
|
-
|
|
56353
|
-
|
|
56354
|
-
|
|
56355
|
-
|
|
56356
|
-
|
|
56357
|
-
|
|
56342
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
56343
|
+
for (let qi = 0; qi < i.queries.length; qi++) {
|
|
56344
|
+
const q = i.queries[qi];
|
|
56345
|
+
const firstIdx = seenIds.get(q.id);
|
|
56346
|
+
if (firstIdx !== void 0) {
|
|
56347
|
+
return {
|
|
56348
|
+
ok: false,
|
|
56349
|
+
error: `Duplicate query id '${q.id}' at queries[${qi}]; first seen at queries[${firstIdx}]. Each MetricDataQuery.Id must be unique in a batch.`
|
|
56350
|
+
};
|
|
56351
|
+
}
|
|
56352
|
+
seenIds.set(q.id, qi);
|
|
56353
|
+
const hasMetricStat = q.namespace !== void 0 || q.metricName !== void 0 || q.dimensions !== void 0;
|
|
56354
|
+
const hasExpression = q.expression !== void 0;
|
|
56355
|
+
if (hasMetricStat && hasExpression) {
|
|
56356
|
+
return {
|
|
56357
|
+
ok: false,
|
|
56358
|
+
error: `Query '${q.id}' mixes metric-stat fields (namespace/metricName/dimensions) with 'expression'. Pick one shape per query.`
|
|
56359
|
+
};
|
|
56360
|
+
}
|
|
56361
|
+
if (!hasMetricStat && !hasExpression) {
|
|
56362
|
+
return {
|
|
56363
|
+
ok: false,
|
|
56364
|
+
error: `Query '${q.id}' has neither metric-stat (namespace+metricName) nor 'expression'. One is required.`
|
|
56365
|
+
};
|
|
56366
|
+
}
|
|
56367
|
+
if (hasMetricStat && (q.namespace === void 0 || q.metricName === void 0)) {
|
|
56368
|
+
return {
|
|
56369
|
+
ok: false,
|
|
56370
|
+
error: `Query '${q.id}' must include BOTH 'namespace' and 'metricName' (or use 'expression' instead).`
|
|
56371
|
+
};
|
|
56372
|
+
}
|
|
56373
|
+
if (q.statistic !== void 0 && !isValidStatistic(q.statistic)) {
|
|
56374
|
+
return {
|
|
56375
|
+
ok: false,
|
|
56376
|
+
error: `Query '${q.id}' has invalid statistic '${q.statistic}'. Use Average | Sum | Maximum | Minimum | SampleCount, or an extended stat like p99 / p99.9 / tm95.`
|
|
56377
|
+
};
|
|
56378
|
+
}
|
|
56379
|
+
}
|
|
56380
|
+
const now = Date.now();
|
|
56381
|
+
const startStr = i.startTime ?? "1h";
|
|
56382
|
+
const endStr = i.endTime ?? "now";
|
|
56383
|
+
const startDate = resolveTime(startStr, now);
|
|
56384
|
+
const endDate = resolveTime(endStr, now);
|
|
56385
|
+
if (!startDate) {
|
|
56386
|
+
return {
|
|
56387
|
+
ok: false,
|
|
56388
|
+
error: `Invalid startTime '${startStr}'. Use ISO 8601 (e.g. '2026-05-16T10:00:00Z') or relative shorthand (e.g. '1h', '15m', '1d').`
|
|
56389
|
+
};
|
|
56390
|
+
}
|
|
56391
|
+
if (!endDate) {
|
|
56392
|
+
return {
|
|
56393
|
+
ok: false,
|
|
56394
|
+
error: `Invalid endTime '${endStr}'. Use ISO 8601 or relative shorthand, or 'now' for the current moment.`
|
|
56395
|
+
};
|
|
56396
|
+
}
|
|
56397
|
+
if (endDate.getTime() <= startDate.getTime()) {
|
|
56398
|
+
return {
|
|
56399
|
+
ok: false,
|
|
56400
|
+
error: `endTime (${endDate.toISOString()}) must be after startTime (${startDate.toISOString()}).`
|
|
56401
|
+
};
|
|
56402
|
+
}
|
|
56403
|
+
const rangeSeconds = (endDate.getTime() - startDate.getTime()) / 1e3;
|
|
56404
|
+
for (const q of i.queries) {
|
|
56405
|
+
if (q.period === void 0) continue;
|
|
56406
|
+
if (q.period <= 0 || q.period % 60 !== 0) {
|
|
56407
|
+
return {
|
|
56408
|
+
ok: false,
|
|
56409
|
+
error: `Query '${q.id}' has invalid period ${q.period}. CloudWatch requires period to be a positive multiple of 60 (seconds).`
|
|
56410
|
+
};
|
|
56411
|
+
}
|
|
56412
|
+
const datapoints = Math.ceil(rangeSeconds / q.period);
|
|
56413
|
+
if (datapoints > CLOUDWATCH_MAX_DATAPOINTS) {
|
|
56414
|
+
return {
|
|
56415
|
+
ok: false,
|
|
56416
|
+
error: `Query '${q.id}' with period ${q.period}s over the requested range (${startDate.toISOString()} to ${endDate.toISOString()}) would request ${datapoints} datapoints, exceeding CloudWatch's per-request cap of ${CLOUDWATCH_MAX_DATAPOINTS}. Widen the period or narrow the time range.`
|
|
56417
|
+
};
|
|
56418
|
+
}
|
|
56419
|
+
}
|
|
56420
|
+
const periodSeconds = pickAutoPeriodSeconds(startDate.getTime(), endDate.getTime());
|
|
56421
|
+
const metricDataQueries = buildMetricDataQueries(i.queries, periodSeconds);
|
|
56422
|
+
const params = {
|
|
56423
|
+
MetricDataQueries: metricDataQueries,
|
|
56424
|
+
StartTime: startDate.toISOString(),
|
|
56425
|
+
EndTime: endDate.toISOString(),
|
|
56426
|
+
ScanBy: i.scanBy ?? "TimestampDescending"
|
|
56427
|
+
};
|
|
56428
|
+
if (i.maxDataPoints !== void 0) params.MaxDatapoints = i.maxDataPoints;
|
|
56429
|
+
if (i.nextToken !== void 0) params.NextToken = i.nextToken;
|
|
56430
|
+
const effectiveProfile = i.profile ?? getProfile();
|
|
56431
|
+
const effectiveRegion = i.region ?? getRegion();
|
|
56432
|
+
const result = await runAwsCall({
|
|
56433
|
+
service: "cloudwatch",
|
|
56434
|
+
operation: "get-metric-data",
|
|
56358
56435
|
profile: i.profile,
|
|
56359
56436
|
region: i.region,
|
|
56360
56437
|
timeoutMs: i.timeoutMs,
|
|
56361
56438
|
outputFormat: "json",
|
|
56362
|
-
|
|
56439
|
+
params
|
|
56363
56440
|
});
|
|
56364
|
-
if (!
|
|
56365
|
-
return { ok: false, error:
|
|
56366
|
-
}
|
|
56367
|
-
const raw = getResult.data;
|
|
56368
|
-
const parsed = parseResourceProperties(raw?.ResourceDescription);
|
|
56369
|
-
const before = parsed.Properties;
|
|
56370
|
-
let after;
|
|
56371
|
-
try {
|
|
56372
|
-
after = applyJsonPatch(before, i.patchDocument);
|
|
56373
|
-
} catch (err) {
|
|
56374
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
56375
|
-
return { ok: false, error: `Patch application failed: ${msg}` };
|
|
56441
|
+
if (!result.ok) {
|
|
56442
|
+
return { ok: false, error: result.error, rawBody: result.rawStderr ?? result.rawStdout };
|
|
56376
56443
|
}
|
|
56377
|
-
const
|
|
56444
|
+
const raw = result.data ?? {};
|
|
56445
|
+
const queryById = new Map(i.queries.map((q) => [q.id, q]));
|
|
56446
|
+
const series = (raw.MetricDataResults ?? []).map((r) => {
|
|
56447
|
+
const q = queryById.get(r.Id ?? "");
|
|
56448
|
+
const effectivePeriod = q?.period ?? (q && q.expression === void 0 ? periodSeconds : void 0);
|
|
56449
|
+
return {
|
|
56450
|
+
id: r.Id ?? "",
|
|
56451
|
+
...r.Label !== void 0 ? { label: r.Label } : {},
|
|
56452
|
+
timestamps: r.Timestamps ?? [],
|
|
56453
|
+
values: r.Values ?? [],
|
|
56454
|
+
...effectivePeriod !== void 0 ? { period: effectivePeriod } : {},
|
|
56455
|
+
...r.StatusCode !== void 0 ? { statusCode: r.StatusCode } : {}
|
|
56456
|
+
};
|
|
56457
|
+
});
|
|
56458
|
+
const messages = raw.Messages?.filter((m) => m.Code || m.Value).map((m) => ({
|
|
56459
|
+
code: m.Code,
|
|
56460
|
+
value: m.Value
|
|
56461
|
+
}));
|
|
56462
|
+
const nextToken = extractNextToken(raw);
|
|
56378
56463
|
return {
|
|
56379
56464
|
ok: true,
|
|
56380
56465
|
data: {
|
|
56381
|
-
command:
|
|
56382
|
-
|
|
56383
|
-
|
|
56384
|
-
|
|
56385
|
-
|
|
56386
|
-
|
|
56387
|
-
|
|
56466
|
+
command: result.command,
|
|
56467
|
+
profile: effectiveProfile,
|
|
56468
|
+
region: effectiveRegion,
|
|
56469
|
+
startTime: startDate.toISOString(),
|
|
56470
|
+
endTime: endDate.toISOString(),
|
|
56471
|
+
periodSeconds,
|
|
56472
|
+
series,
|
|
56473
|
+
nextToken,
|
|
56474
|
+
hasMore: nextToken !== null,
|
|
56475
|
+
...messages && messages.length > 0 ? { messages } : {}
|
|
56388
56476
|
}
|
|
56389
56477
|
};
|
|
56390
56478
|
}
|
|
56391
56479
|
}
|
|
56392
56480
|
];
|
|
56393
|
-
|
|
56394
|
-
|
|
56395
|
-
|
|
56396
|
-
|
|
56397
|
-
|
|
56398
|
-
|
|
56399
|
-
|
|
56400
|
-
|
|
56401
|
-
|
|
56402
|
-
|
|
56403
|
-
|
|
56404
|
-
|
|
56405
|
-
|
|
56406
|
-
function applyJsonPatch(original, ops) {
|
|
56407
|
-
const doc = clone2(original);
|
|
56408
|
-
return _applyJsonPatchInPlace(doc, ops);
|
|
56409
|
-
}
|
|
56410
|
-
function _applyJsonPatchInPlace(doc, ops) {
|
|
56411
|
-
let root = doc;
|
|
56412
|
-
for (let i = 0; i < ops.length; i++) {
|
|
56413
|
-
const op = ops[i];
|
|
56414
|
-
if (op.op === "move" || op.op === "copy" || op.op === "test") {
|
|
56415
|
-
throw new Error(`op '${op.op}' at index ${i} is not implemented in aws_resource_diff (use add/remove/replace).`);
|
|
56416
|
-
}
|
|
56417
|
-
const tokens = parseJsonPointer(op.path);
|
|
56418
|
-
if (tokens.length === 0) {
|
|
56419
|
-
if (op.op === "remove") {
|
|
56420
|
-
throw new Error(`Cannot remove the document root at index ${i}.`);
|
|
56421
|
-
}
|
|
56422
|
-
root = clone2(op.value);
|
|
56423
|
-
continue;
|
|
56424
|
-
}
|
|
56425
|
-
const parentTokens = tokens.slice(0, -1);
|
|
56426
|
-
const lastToken = tokens[tokens.length - 1];
|
|
56427
|
-
let parent = root;
|
|
56428
|
-
for (let t = 0; t < parentTokens.length; t++) {
|
|
56429
|
-
const segment = parentTokens[t];
|
|
56430
|
-
if (Array.isArray(parent)) {
|
|
56431
|
-
const idx = Number.parseInt(segment, 10);
|
|
56432
|
-
if (!Number.isInteger(idx) || idx < 0) {
|
|
56433
|
-
throw new Error(`Path '${op.path}' segment '${segment}' is not a valid array index at op index ${i}.`);
|
|
56434
|
-
}
|
|
56435
|
-
if (idx >= parent.length) {
|
|
56436
|
-
throw new Error(
|
|
56437
|
-
`Path '${op.path}' cannot traverse into intermediate array element at segment '${segment}': array has length ${parent.length}, so index ${idx} does not exist yet (at op index ${i}).`
|
|
56438
|
-
);
|
|
56439
|
-
}
|
|
56440
|
-
parent = parent[idx];
|
|
56441
|
-
} else if (isObj(parent)) {
|
|
56442
|
-
if (!(segment in parent)) {
|
|
56443
|
-
if (op.op === "add") {
|
|
56444
|
-
parent[segment] = {};
|
|
56445
|
-
parent = parent[segment];
|
|
56446
|
-
continue;
|
|
56447
|
-
}
|
|
56448
|
-
throw new Error(`Path '${op.path}' segment '${segment}' does not exist at index ${i}.`);
|
|
56449
|
-
}
|
|
56450
|
-
parent = parent[segment];
|
|
56451
|
-
} else {
|
|
56452
|
-
throw new Error(`Path '${op.path}' traverses a non-container value at index ${i}.`);
|
|
56453
|
-
}
|
|
56481
|
+
|
|
56482
|
+
// src/tools/multi-region.ts
|
|
56483
|
+
var DEFAULT_CONCURRENCY = 8;
|
|
56484
|
+
var MAX_CONCURRENCY = 32;
|
|
56485
|
+
var MAX_REGIONS = 32;
|
|
56486
|
+
async function runWithConcurrency(inputs, concurrency, fn) {
|
|
56487
|
+
const results = new Array(inputs.length);
|
|
56488
|
+
let next = 0;
|
|
56489
|
+
const worker = async () => {
|
|
56490
|
+
while (true) {
|
|
56491
|
+
const i = next++;
|
|
56492
|
+
if (i >= inputs.length) return;
|
|
56493
|
+
results[i] = await fn(inputs[i], i);
|
|
56454
56494
|
}
|
|
56455
|
-
|
|
56456
|
-
|
|
56457
|
-
|
|
56458
|
-
|
|
56495
|
+
};
|
|
56496
|
+
const workerCount = Math.min(concurrency, inputs.length);
|
|
56497
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
56498
|
+
return results;
|
|
56499
|
+
}
|
|
56500
|
+
var multiRegionTools = [
|
|
56501
|
+
{
|
|
56502
|
+
name: "aws_multi_region",
|
|
56503
|
+
description: "Run the same AWS API operation across multiple regions in parallel. Same shape as aws_call (service, operation, params?, query?, outputFormat?, timeoutMs?) but takes `regions: string[]` instead of `region`. Returns an array of `{region, ok, data?, command?, error?, errorKind?}` -- partial failure is expected (services aren't everywhere, perms may be region-scoped). Duplicate regions in the input are collapsed (first occurrence wins), so `results.length` may be less than `regions.length`; use the returned `regionCount` for the actual count run. Use for fleet-wide reads: 'describe-instances across all our regions', 'list buckets in every region', 'check IAM password policy everywhere'.",
|
|
56504
|
+
annotations: {
|
|
56505
|
+
title: "Run an AWS operation across multiple regions in parallel",
|
|
56506
|
+
// The operation can be anything -- we conservatively annotate as not
|
|
56507
|
+
// read-only / not destructive. The caller chooses what to invoke.
|
|
56508
|
+
readOnlyHint: false,
|
|
56509
|
+
destructiveHint: false,
|
|
56510
|
+
idempotentHint: false,
|
|
56511
|
+
openWorldHint: true
|
|
56512
|
+
},
|
|
56513
|
+
inputSchema: external_exports3.object({
|
|
56514
|
+
service: external_exports3.string().describe("AWS service in kebab-case: 's3api', 'ec2', 'iam', etc."),
|
|
56515
|
+
operation: external_exports3.string().describe("Operation in kebab-case: 'describe-instances', 'list-buckets', etc."),
|
|
56516
|
+
regions: external_exports3.array(external_exports3.string().min(1)).min(1).max(MAX_REGIONS).describe(
|
|
56517
|
+
`Region IDs (e.g. ['us-east-1','us-west-2','eu-west-1']). 1-${MAX_REGIONS}. Validated for argv-safety; a bad region name yields a clear per-region error and skips its CLI spawn (per-region isolation comes from each region being a separate call, not from this pre-check).`
|
|
56518
|
+
),
|
|
56519
|
+
params: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Operation parameters (PascalCase keys) -- same shape as aws_call."),
|
|
56520
|
+
query: external_exports3.string().optional().describe("JMESPath expression for --query (server-side trimming per region)."),
|
|
56521
|
+
outputFormat: external_exports3.enum(["json", "text", "table", "yaml"]).optional().describe("Output format. Default 'json'."),
|
|
56522
|
+
profile: external_exports3.string().optional().describe("Override session profile for the batch."),
|
|
56523
|
+
timeoutMs: external_exports3.number().int().positive().optional().describe("Timeout in ms applied PER region. Default 60000."),
|
|
56524
|
+
concurrency: external_exports3.number().int().positive().max(MAX_CONCURRENCY).optional().describe(`Max regions in flight at once (1-${MAX_CONCURRENCY}). Default ${DEFAULT_CONCURRENCY}.`)
|
|
56525
|
+
}),
|
|
56526
|
+
handler: async (input) => {
|
|
56527
|
+
const i = input;
|
|
56528
|
+
const seen = /* @__PURE__ */ new Set();
|
|
56529
|
+
const regions = [];
|
|
56530
|
+
for (const r of i.regions) {
|
|
56531
|
+
if (!seen.has(r)) {
|
|
56532
|
+
seen.add(r);
|
|
56533
|
+
regions.push(r);
|
|
56459
56534
|
}
|
|
56460
|
-
parent.push(clone2(op.value));
|
|
56461
|
-
continue;
|
|
56462
56535
|
}
|
|
56463
|
-
const
|
|
56464
|
-
|
|
56465
|
-
|
|
56466
|
-
|
|
56467
|
-
|
|
56468
|
-
|
|
56469
|
-
|
|
56470
|
-
|
|
56471
|
-
|
|
56472
|
-
} else if (op.op === "remove") {
|
|
56473
|
-
if (idx >= parent.length) {
|
|
56474
|
-
throw new Error(`Remove index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
|
|
56475
|
-
}
|
|
56476
|
-
parent.splice(idx, 1);
|
|
56477
|
-
} else {
|
|
56478
|
-
if (idx >= parent.length) {
|
|
56479
|
-
throw new Error(`Replace index ${idx} out of bounds for array of length ${parent.length} at index ${i}.`);
|
|
56536
|
+
const concurrency = i.concurrency ?? DEFAULT_CONCURRENCY;
|
|
56537
|
+
const results = await runWithConcurrency(regions, concurrency, async (region) => {
|
|
56538
|
+
if (!isValidRegionName(region)) {
|
|
56539
|
+
return {
|
|
56540
|
+
region,
|
|
56541
|
+
ok: false,
|
|
56542
|
+
error: `Invalid region '${region}'. Must match ${REGION_NAME_RE} (e.g. 'us-east-1').`,
|
|
56543
|
+
errorKind: "bad_input"
|
|
56544
|
+
};
|
|
56480
56545
|
}
|
|
56481
|
-
|
|
56482
|
-
|
|
56483
|
-
|
|
56484
|
-
|
|
56485
|
-
|
|
56486
|
-
|
|
56546
|
+
const r = await runAwsCall({
|
|
56547
|
+
service: i.service,
|
|
56548
|
+
operation: i.operation,
|
|
56549
|
+
params: i.params,
|
|
56550
|
+
query: i.query,
|
|
56551
|
+
profile: i.profile,
|
|
56552
|
+
region,
|
|
56553
|
+
outputFormat: i.outputFormat,
|
|
56554
|
+
timeoutMs: i.timeoutMs
|
|
56555
|
+
});
|
|
56556
|
+
if (!r.ok) {
|
|
56557
|
+
return {
|
|
56558
|
+
region,
|
|
56559
|
+
ok: false,
|
|
56560
|
+
command: r.command,
|
|
56561
|
+
error: r.error,
|
|
56562
|
+
errorKind: r.kind
|
|
56563
|
+
};
|
|
56487
56564
|
}
|
|
56488
|
-
|
|
56489
|
-
}
|
|
56490
|
-
|
|
56491
|
-
|
|
56565
|
+
return { region, ok: true, command: r.command, data: r.data };
|
|
56566
|
+
});
|
|
56567
|
+
const okCount = results.filter((r) => r.ok).length;
|
|
56568
|
+
const errCount = results.length - okCount;
|
|
56569
|
+
return {
|
|
56570
|
+
ok: true,
|
|
56571
|
+
data: {
|
|
56572
|
+
service: i.service,
|
|
56573
|
+
operation: i.operation,
|
|
56574
|
+
regionCount: regions.length,
|
|
56575
|
+
okCount,
|
|
56576
|
+
errorCount: errCount,
|
|
56577
|
+
results
|
|
56492
56578
|
}
|
|
56493
|
-
|
|
56494
|
-
} else {
|
|
56495
|
-
parent[lastToken] = clone2(op.value);
|
|
56496
|
-
}
|
|
56497
|
-
} else {
|
|
56498
|
-
throw new Error(`Path '${op.path}' parent is not a container at index ${i}.`);
|
|
56499
|
-
}
|
|
56500
|
-
}
|
|
56501
|
-
return root;
|
|
56502
|
-
}
|
|
56503
|
-
function summarizePatch(ops, before, after) {
|
|
56504
|
-
let working;
|
|
56505
|
-
let replayOk = true;
|
|
56506
|
-
try {
|
|
56507
|
-
working = clone2(before);
|
|
56508
|
-
} catch {
|
|
56509
|
-
replayOk = false;
|
|
56510
|
-
working = void 0;
|
|
56511
|
-
}
|
|
56512
|
-
const out = [];
|
|
56513
|
-
for (const op of ops) {
|
|
56514
|
-
if (op.op === "move" || op.op === "copy" || op.op === "test") {
|
|
56515
|
-
out.push({ op: "replace", path: op.path });
|
|
56516
|
-
continue;
|
|
56517
|
-
}
|
|
56518
|
-
const beforeAt = resolvePointer(before, op.path);
|
|
56519
|
-
let afterAt;
|
|
56520
|
-
if (replayOk) {
|
|
56521
|
-
try {
|
|
56522
|
-
working = _applyJsonPatchInPlace(working, [op]);
|
|
56523
|
-
afterAt = resolvePointer(working, op.path);
|
|
56524
|
-
} catch {
|
|
56525
|
-
replayOk = false;
|
|
56526
|
-
afterAt = resolvePointer(after, op.path);
|
|
56527
|
-
}
|
|
56528
|
-
} else {
|
|
56529
|
-
afterAt = resolvePointer(after, op.path);
|
|
56530
|
-
}
|
|
56531
|
-
if (op.op === "add" && afterAt === void 0 && op.path.endsWith("/-")) {
|
|
56532
|
-
afterAt = op.value;
|
|
56533
|
-
}
|
|
56534
|
-
out.push({ op: op.op, path: op.path, before: beforeAt, after: afterAt });
|
|
56535
|
-
}
|
|
56536
|
-
return out;
|
|
56537
|
-
}
|
|
56538
|
-
function resolvePointer(doc, pointer) {
|
|
56539
|
-
let tokens;
|
|
56540
|
-
try {
|
|
56541
|
-
tokens = parseJsonPointer(pointer);
|
|
56542
|
-
} catch {
|
|
56543
|
-
return void 0;
|
|
56544
|
-
}
|
|
56545
|
-
let cur = doc;
|
|
56546
|
-
for (const t of tokens) {
|
|
56547
|
-
if (Array.isArray(cur)) {
|
|
56548
|
-
if (t === "-") return void 0;
|
|
56549
|
-
const idx = Number.parseInt(t, 10);
|
|
56550
|
-
if (!Number.isInteger(idx) || idx < 0 || idx >= cur.length) return void 0;
|
|
56551
|
-
cur = cur[idx];
|
|
56552
|
-
} else if (isObj(cur)) {
|
|
56553
|
-
if (!(t in cur)) return void 0;
|
|
56554
|
-
cur = cur[t];
|
|
56555
|
-
} else {
|
|
56556
|
-
return void 0;
|
|
56579
|
+
};
|
|
56557
56580
|
}
|
|
56558
56581
|
}
|
|
56559
|
-
|
|
56560
|
-
}
|
|
56582
|
+
];
|
|
56561
56583
|
|
|
56562
56584
|
// src/tools/script.ts
|
|
56563
56585
|
import { createContext, runInContext } from "node:vm";
|
|
56564
56586
|
var DEFAULT_TIMEOUT_MS2 = 6e4;
|
|
56565
56587
|
var MAX_TIMEOUT_MS = 5 * 6e4;
|
|
56566
56588
|
var MAX_LOG_LINES = 500;
|
|
56567
|
-
var
|
|
56589
|
+
var MAX_LOG_LINE_CHARS = 4 * 1024;
|
|
56568
56590
|
var DEFAULT_MAX_PAGES = 50;
|
|
56569
56591
|
var MAX_PAGES_HARD_CAP = 1e3;
|
|
56570
56592
|
function findTool(name, source) {
|
|
@@ -56664,7 +56686,7 @@ async function runScript(opts, handlers = defaultScriptHandlers()) {
|
|
|
56664
56686
|
}
|
|
56665
56687
|
}
|
|
56666
56688
|
}).join(" ");
|
|
56667
|
-
const capped = text.length >
|
|
56689
|
+
const capped = text.length > MAX_LOG_LINE_CHARS ? `${text.slice(0, MAX_LOG_LINE_CHARS)}... [line truncated]` : text;
|
|
56668
56690
|
logs.push(`[${level}] ${capped}`);
|
|
56669
56691
|
};
|
|
56670
56692
|
const ctx = createContext(
|
|
@@ -56797,9 +56819,11 @@ var scriptTools = [
|
|
|
56797
56819
|
annotations: {
|
|
56798
56820
|
title: "Run a JS snippet that orchestrates AWS tool calls",
|
|
56799
56821
|
// The script may invoke destructive tools (resource.create/update/delete)
|
|
56800
|
-
// so
|
|
56822
|
+
// so annotate the worst case honestly: non-read-only AND destructive.
|
|
56823
|
+
// Cautious clients may confirm read-only scripts too -- acceptable cost;
|
|
56824
|
+
// an unflagged delete is not.
|
|
56801
56825
|
readOnlyHint: false,
|
|
56802
|
-
destructiveHint:
|
|
56826
|
+
destructiveHint: true,
|
|
56803
56827
|
idempotentHint: false,
|
|
56804
56828
|
openWorldHint: true
|
|
56805
56829
|
},
|
|
@@ -56865,7 +56889,7 @@ var sessionTools = [
|
|
|
56865
56889
|
if (!isValidProfileName(trimmed)) {
|
|
56866
56890
|
return {
|
|
56867
56891
|
ok: false,
|
|
56868
|
-
error: `Invalid profile name '${trimmed}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]
|
|
56892
|
+
error: `Invalid profile name '${trimmed}'. Must be 1-128 chars from [A-Za-z0-9_+=,.@:-]; the first char must be a letter, digit, or one of _+,.@: (not '-' or '='); no whitespace or shell metacharacters.`
|
|
56869
56893
|
};
|
|
56870
56894
|
}
|
|
56871
56895
|
}
|
|
@@ -56952,7 +56976,7 @@ function errorToMcpResult(err, toolName) {
|
|
|
56952
56976
|
isError: true
|
|
56953
56977
|
};
|
|
56954
56978
|
}
|
|
56955
|
-
var version2 = true ? "1.
|
|
56979
|
+
var version2 = true ? "1.5.0" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
56956
56980
|
var isEntryPoint = (() => {
|
|
56957
56981
|
const entry = process.argv[1];
|
|
56958
56982
|
if (!entry) return false;
|