@mycompbox/compbox-mcp 1.19.0 → 1.21.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.
Files changed (2) hide show
  1. package/index.js +1185 -4
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -178,6 +178,34 @@ var init_organization = __esm({
178
178
  }
179
179
  });
180
180
 
181
+ // ../shared/dist/types/github.js
182
+ var init_github = __esm({
183
+ "../shared/dist/types/github.js"() {
184
+ "use strict";
185
+ }
186
+ });
187
+
188
+ // ../shared/dist/types/cloud.js
189
+ var init_cloud = __esm({
190
+ "../shared/dist/types/cloud.js"() {
191
+ "use strict";
192
+ }
193
+ });
194
+
195
+ // ../shared/dist/types/stack.js
196
+ var init_stack = __esm({
197
+ "../shared/dist/types/stack.js"() {
198
+ "use strict";
199
+ }
200
+ });
201
+
202
+ // ../shared/dist/types/deployment.js
203
+ var init_deployment = __esm({
204
+ "../shared/dist/types/deployment.js"() {
205
+ "use strict";
206
+ }
207
+ });
208
+
181
209
  // ../shared/dist/types/index.js
182
210
  var init_types = __esm({
183
211
  "../shared/dist/types/index.js"() {
@@ -190,6 +218,10 @@ var init_types = __esm({
190
218
  init_interview();
191
219
  init_attachment();
192
220
  init_organization();
221
+ init_github();
222
+ init_cloud();
223
+ init_stack();
224
+ init_deployment();
193
225
  }
194
226
  });
195
227
 
@@ -529,7 +561,7 @@ function interviewFieldKeys() {
529
561
  function buildInterviewSystemPrompt(definition = INTERVIEW_DEFINITION, seedContext) {
530
562
  const requiredSections = definition.filter((s) => s.required);
531
563
  const optionalSections = definition.filter((s) => !s.required);
532
- const renderSection = (s) => {
564
+ const renderSection2 = (s) => {
533
565
  const tag = s.required ? "REQUIRED" : "OPTIONAL \u2014 ask, but the user may decline";
534
566
  const followUps = s.followUps.map((q) => ` - ${q}`).join("\n");
535
567
  return `- ${s.title} (${tag}) [json keys: ${s.fields.join(", ")}]
@@ -547,10 +579,10 @@ ${followUps}`;
547
579
  Work through these interview sections in order. Cover EVERY required section; for optional sections, ask about them too, but if the employee has nothing to add or declines, move on and leave that field out (never invent or pad it):
548
580
 
549
581
  REQUIRED sections (an Epic Request cannot omit these):
550
- ${requiredSections.map(renderSection).join("\n")}
582
+ ${requiredSections.map(renderSection2).join("\n")}
551
583
 
552
584
  OPTIONAL-BUT-RECOMMENDED sections (ask about each; omit the field if the employee has nothing):
553
- ${optionalSections.map(renderSection).join("\n")}
585
+ ${optionalSections.map(renderSection2).join("\n")}
554
586
 
555
587
  The employee's answers may be short or casual \u2014 that's fine, but when you write the final fields below, EXPAND them into complete, well-written prose using everything they told you across the whole conversation, not just a compressed one-liner. Each required field is scored by an automated quality rubric with hard minimums, so undershooting length is a real failure, not just a style nitpick:
556
588
  - problemStatement: at least ${String(INTERVIEW_FIELD_MIN_WORDS.problemStatement)} words.
@@ -830,13 +862,1118 @@ var init_interview_completion = __esm({
830
862
  }
831
863
  });
832
864
 
865
+ // ../shared/dist/interview-conversation.js
866
+ function renderSection(s) {
867
+ const tag = s.required ? "needed" : "nice to have";
868
+ return `- ${s.title} (${tag}): ${s.purpose} For example: ${s.followUps.slice(0, 2).join(" ")}`;
869
+ }
870
+ function renderSaved(saved) {
871
+ const fieldLines = Object.entries(saved.fields).filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0).map(([key, value]) => `${key}: ${value}`).join("\n");
872
+ const score = saved.compliance ? `Its quality score is ${String(saved.compliance.overall)}/100 (${saved.compliance.tier}).` : "It has no quality score yet.";
873
+ const issues = saved.compliance && saved.compliance.issues.length > 0 ? `
874
+ What the score says is missing or weak:
875
+ ${saved.compliance.issues.slice(0, 8).map((i) => `- ${i.message}`).join("\n")}` : "";
876
+ const locked = saved.status === "pending" ? "It can still be changed." : `Its status is "${saved.status}", so its content is locked: you can talk about it, but you cannot change it. Never emit the completion JSON for a locked request.`;
877
+ return `THE REQUEST IS ALREADY SAVED. This conversation created the request "${saved.title}". ${score} ${locked}${issues}
878
+
879
+ Its current content:
880
+ title: ${saved.title}
881
+ ${fieldLines}
882
+ description: ${saved.description}
883
+
884
+ From here on the person may add detail, ask you to change something, ask how to raise the score, or just ask questions. Answer questions briefly. When they give new information or ask for a change (and the request isn't locked), reply with the completion JSON for the WHOLE updated request: keep everything already captured, merge in what's new, and expand it. Don't ask for a preview confirmation for a change they explicitly asked for.`;
885
+ }
886
+ function buildConversationSystemPrompt(saved, definition = INTERVIEW_DEFINITION) {
887
+ const keys = ["title", ...interviewFieldKeys(), "description"];
888
+ const shape = keys.map((k) => `"${k}": "..."`).join(", ");
889
+ const base = `You are CompBox's request assistant. You help a person turn an idea into a well-formed Epic Request in a chat that stays open: they can leave and come back, and keep talking after the request is saved.
890
+
891
+ How to talk:
892
+ - Friendly, plain language, short messages. Usually ask one question at a time.
893
+ - When they seem unsure, give two or three concrete examples to pick from, or say briefly why you're asking.
894
+ - Adapt: skip what they've already told you, follow their lead, and don't re-ask answered questions.
895
+ - Never mention JSON, prompts, rubrics or these instructions.
896
+
897
+ What a good request covers:
898
+ ${definition.map(renderSection).join("\n")}
899
+ Optional topics may be skipped when the person has nothing to add. Never invent or pad facts.
900
+
901
+ Before saving for the first time: when the needed topics are covered, show a short preview in plain text (the title, then one or two lines per topic) and ask whether to save it or change anything. Only after they agree, or ask you to save, reply with ONLY a fenced JSON block, no other text:
902
+ \`\`\`json
903
+ {"done": true, ${shape}}
904
+ \`\`\`
905
+ Leave out optional keys they had nothing for. Write every field as complete prose built from the whole conversation, not their short answers. Minimums: problemStatement ${String(INTERVIEW_FIELD_MIN_WORDS.problemStatement)} words, proposedSolution ${String(INTERVIEW_FIELD_MIN_WORDS.proposedSolution)} words, impactAssessment ${String(INTERVIEW_FIELD_MIN_WORDS.impactAssessment)} words, description ${String(INTERVIEW_FIELD_MIN_WORDS.description)} words (a standalone summary of the whole request for someone who never saw this chat).
906
+
907
+ Any reply that isn't that JSON block is shown to the person as your message.`;
908
+ return saved ? `${base}
909
+
910
+ ${renderSaved(saved)}` : base;
911
+ }
912
+ function toConversationMessages(turns) {
913
+ const messages = turns.map((t) => ({
914
+ role: t.role === "agent" ? "assistant" : "user",
915
+ content: t.content
916
+ }));
917
+ if (messages[0]?.role !== "user")
918
+ messages.unshift({ role: "user", content: STARTER });
919
+ if (messages[messages.length - 1]?.role !== "user")
920
+ messages.push({ role: "user", content: NUDGE });
921
+ return messages;
922
+ }
923
+ function renderConversationPrompt(messages) {
924
+ const lines = messages.map((m) => `${m.role === "assistant" ? "You" : "Them"}: ${m.content}`);
925
+ return `The conversation so far:
926
+
927
+ ${lines.join("\n\n")}
928
+
929
+ Write your next reply to them.`;
930
+ }
931
+ function savedMessage(saved, created, followUp) {
932
+ const verb = created ? "Saved your request" : "Updated your request";
933
+ const c = saved.compliance;
934
+ let text;
935
+ if (!c) {
936
+ text = `${verb} "${saved.title}". You can keep talking here to add detail or change anything.`;
937
+ } else if (meetsComplianceTarget(c)) {
938
+ text = `${verb} "${saved.title}". It scores ${String(c.overall)}/100, ready for review. You can keep talking here to change anything.`;
939
+ } else {
940
+ const gaps = [...c.issues.filter((i) => i.tier === "critical"), ...c.issues.filter((i) => i.tier === "advisory")].slice(0, 4).map((i) => `- ${i.message}`).join("\n");
941
+ text = `${verb} "${saved.title}". It scores ${String(c.overall)}/100; 90 is the target.${gaps ? ` To raise it:
942
+ ${gaps}` : ""}
943
+
944
+ Tell me more here and I'll update it, or leave it as it is.`;
945
+ }
946
+ return followUp ? `${text}
947
+
948
+ ${followUp}` : text;
949
+ }
950
+ async function runConversationStep(input, deps) {
951
+ const system = buildConversationSystemPrompt(input.saved);
952
+ const messages = toConversationMessages(input.turns);
953
+ const locked = input.saved !== null && input.saved.status !== "pending";
954
+ let requestId = input.saved?.id ?? null;
955
+ let savedNow = null;
956
+ let createdHere = false;
957
+ let qualityRounds = 0;
958
+ let repairs = 0;
959
+ for (; ; ) {
960
+ let reply;
961
+ try {
962
+ reply = await deps.reply(system, messages);
963
+ } catch (error) {
964
+ if (savedNow)
965
+ return { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created), ...savedNow };
966
+ const reason = error instanceof Error ? error.message : "Something went wrong reaching the AI.";
967
+ return {
968
+ kind: "error",
969
+ text: `I couldn't reply just now: ${reason}
970
+
971
+ Send another message (for example "try again") when you're ready and I'll pick up where we left off.`
972
+ };
973
+ }
974
+ const draft = extractCompletionJson(reply);
975
+ if (draft) {
976
+ if (locked) {
977
+ messages.push({ role: "assistant", content: reply }, { role: "user", content: "That request is locked and can't be changed. Reply to me in plain text instead." });
978
+ if (++repairs > JSON_REPAIRS)
979
+ return { kind: "error", text: "This request is locked, so it can't be changed any more." };
980
+ continue;
981
+ }
982
+ let saved;
983
+ try {
984
+ saved = await deps.save(requestId, draft);
985
+ } catch (error) {
986
+ const reason = error instanceof Error ? error.message : "the save failed";
987
+ return { kind: "error", text: `I couldn't save the request: ${reason}
988
+
989
+ Reply "try again" and I'll retry.` };
990
+ }
991
+ createdHere ||= requestId === null;
992
+ savedNow = { saved, created: createdHere };
993
+ requestId = saved.id;
994
+ if (meetsComplianceTarget(saved.compliance) || qualityRounds >= CONVERSATION_QUALITY_ROUNDS || !saved.compliance) {
995
+ return { kind: "saved", text: savedMessage(saved, savedNow.created), ...savedNow };
996
+ }
997
+ qualityRounds++;
998
+ messages.push({ role: "assistant", content: reply }, {
999
+ role: "user",
1000
+ content: `${buildAuditFeedback(saved.compliance)}
1001
+
1002
+ The request IS already saved, and the person will be shown its score. If you ask them something, ask it as a way to improve the saved request; never say it hasn't been saved.`
1003
+ });
1004
+ continue;
1005
+ }
1006
+ if (looksLikeCompletionAttempt(reply)) {
1007
+ if (++repairs > JSON_REPAIRS) {
1008
+ return savedNow ? { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created), ...savedNow } : { kind: "error", text: `I had trouble writing up the request. Reply "try again" and I'll have another go.` };
1009
+ }
1010
+ messages.push({ role: "assistant", content: reply }, { role: "user", content: COMPLETION_REPAIR_INSTRUCTION });
1011
+ continue;
1012
+ }
1013
+ const text = reply.trim();
1014
+ if (savedNow) {
1015
+ return { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created, text || void 0), ...savedNow };
1016
+ }
1017
+ if (!text)
1018
+ return { kind: "error", text: "I didn't get a reply that time. Send another message and I'll try again." };
1019
+ return { kind: "message", text };
1020
+ }
1021
+ }
1022
+ function savedContextFromRecord(record) {
1023
+ if (typeof record !== "object" || record === null)
1024
+ return null;
1025
+ const r = record;
1026
+ if (typeof r.id !== "string" || typeof r.title !== "string")
1027
+ return null;
1028
+ let structured = r.structuredDesc;
1029
+ if (typeof structured === "string") {
1030
+ try {
1031
+ structured = JSON.parse(structured);
1032
+ } catch {
1033
+ structured = null;
1034
+ }
1035
+ }
1036
+ const fields = {};
1037
+ if (typeof structured === "object" && structured !== null) {
1038
+ for (const [key, value] of Object.entries(structured)) {
1039
+ if (typeof value === "string")
1040
+ fields[key] = value;
1041
+ }
1042
+ }
1043
+ let score = r.complianceScore;
1044
+ if (typeof score === "string") {
1045
+ try {
1046
+ score = JSON.parse(score);
1047
+ } catch {
1048
+ score = null;
1049
+ }
1050
+ }
1051
+ return {
1052
+ id: r.id,
1053
+ title: r.title,
1054
+ status: typeof r.status === "string" ? r.status : "pending",
1055
+ description: typeof r.description === "string" ? r.description : "",
1056
+ fields,
1057
+ compliance: complianceFromRecord(score)
1058
+ };
1059
+ }
1060
+ function complianceFromRecord(score) {
1061
+ if (typeof score !== "object" || score === null)
1062
+ return null;
1063
+ const s = score;
1064
+ if (typeof s.overall !== "number" || typeof s.tier !== "string")
1065
+ return null;
1066
+ const issues = Array.isArray(s.issues) ? s.issues.filter((i) => typeof i === "object" && i !== null).map((i) => ({
1067
+ message: typeof i.message === "string" ? i.message : typeof i.code === "string" ? i.code : "issue",
1068
+ tier: i.tier === "critical" ? "critical" : "advisory"
1069
+ })) : [];
1070
+ return { overall: s.overall, tier: s.tier, issues };
1071
+ }
1072
+ var CONVERSATION_QUALITY_ROUNDS, JSON_REPAIRS, STARTER, NUDGE;
1073
+ var init_interview_conversation = __esm({
1074
+ "../shared/dist/interview-conversation.js"() {
1075
+ "use strict";
1076
+ init_interview_definition();
1077
+ init_interview_completion();
1078
+ CONVERSATION_QUALITY_ROUNDS = 2;
1079
+ JSON_REPAIRS = 3;
1080
+ STARTER = "Hi! I'd like to propose something.";
1081
+ NUDGE = "(Please continue.)";
1082
+ }
1083
+ });
1084
+
1085
+ // ../shared/dist/cloud-setup.js
1086
+ function isGcpProjectId(value) {
1087
+ return PROJECT_ID.test(value);
1088
+ }
1089
+ function isGcpProjectNumber(value) {
1090
+ return PROJECT_NUMBER.test(value);
1091
+ }
1092
+ function isGcpRegion(value) {
1093
+ return GCP_REGIONS.includes(value);
1094
+ }
1095
+ function isGitHubLogin(value) {
1096
+ return GITHUB_LOGIN.test(value);
1097
+ }
1098
+ function gcpServiceAccountEmail(projectId) {
1099
+ return `${GCP_DEPLOY_ACCOUNT}@${projectId}.iam.gserviceaccount.com`;
1100
+ }
1101
+ function gcpWorkloadProvider(projectNumber) {
1102
+ return `projects/${projectNumber}/locations/global/workloadIdentityPools/${GCP_IDENTITY_POOL}/providers/${GCP_IDENTITY_PROVIDER}`;
1103
+ }
1104
+ function gcpStateBucket(projectId) {
1105
+ return `${projectId}-compbox-tfstate`;
1106
+ }
1107
+ function buildGcpSetupScript(input) {
1108
+ if (!isGcpProjectId(input.projectId))
1109
+ throw new Error("Not a Google Cloud project ID.");
1110
+ if (!isGcpRegion(input.region))
1111
+ throw new Error("Not a supported region.");
1112
+ if (input.githubOwners.length === 0)
1113
+ throw new Error("Connect GitHub first.");
1114
+ for (const owner of input.githubOwners) {
1115
+ if (!isGitHubLogin(owner))
1116
+ throw new Error(`Not a GitHub account name: ${owner}`);
1117
+ }
1118
+ const owners = [...new Set(input.githubOwners)];
1119
+ const condition = `assertion.repository_owner in [${owners.map((o) => `'${o}'`).join(", ")}]`;
1120
+ const pool = `--location=global --workload-identity-pool=${GCP_IDENTITY_POOL}`;
1121
+ const oidc = [
1122
+ "--issuer-uri=https://token.actions.githubusercontent.com",
1123
+ '--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner"',
1124
+ `--attribute-condition="${condition}"`
1125
+ ].join(" ");
1126
+ return `# CompBox \u2192 Google Cloud (keyless). Paste into Google Cloud Shell.
1127
+ set -euo pipefail
1128
+ PROJECT=${input.projectId}
1129
+ REGION=${input.region}
1130
+ SA=${gcpServiceAccountEmail(input.projectId)}
1131
+
1132
+ gcloud config set project "$PROJECT"
1133
+ gcloud services enable run.googleapis.com artifactregistry.googleapis.com iam.googleapis.com \\
1134
+ iamcredentials.googleapis.com sts.googleapis.com cloudresourcemanager.googleapis.com
1135
+ NUMBER=$(gcloud projects describe "$PROJECT" --format='value(projectNumber)')
1136
+
1137
+ # The account deploys run as.
1138
+ gcloud iam service-accounts describe "$SA" >/dev/null 2>&1 || \\
1139
+ gcloud iam service-accounts create ${GCP_DEPLOY_ACCOUNT} --display-name="CompBox deploys"
1140
+ for ROLE in ${GCP_DEPLOY_ROLES.join(" ")}; do
1141
+ gcloud projects add-iam-policy-binding "$PROJECT" --member="serviceAccount:$SA" --role="$ROLE" --condition=None --quiet >/dev/null
1142
+ done
1143
+
1144
+ # Trust GitHub Actions from your GitHub only: ${owners.join(", ")}.
1145
+ gcloud iam workload-identity-pools describe ${GCP_IDENTITY_POOL} --location=global >/dev/null 2>&1 || \\
1146
+ gcloud iam workload-identity-pools create ${GCP_IDENTITY_POOL} --location=global --display-name="CompBox"
1147
+ if gcloud iam workload-identity-pools providers describe ${GCP_IDENTITY_PROVIDER} ${pool} >/dev/null 2>&1; then
1148
+ gcloud iam workload-identity-pools providers update-oidc ${GCP_IDENTITY_PROVIDER} ${pool} ${oidc}
1149
+ else
1150
+ gcloud iam workload-identity-pools providers create-oidc ${GCP_IDENTITY_PROVIDER} ${pool} ${oidc}
1151
+ fi
1152
+ for OWNER in ${owners.join(" ")}; do
1153
+ gcloud iam service-accounts add-iam-policy-binding "$SA" --role=roles/iam.workloadIdentityUser --quiet >/dev/null \\
1154
+ --member="principalSet://iam.googleapis.com/projects/$NUMBER/locations/global/workloadIdentityPools/${GCP_IDENTITY_POOL}/attribute.repository_owner/$OWNER"
1155
+ done
1156
+
1157
+ # Where each project's infrastructure state is kept.
1158
+ gcloud storage buckets describe "gs://${gcpStateBucket(input.projectId)}" >/dev/null 2>&1 || \\
1159
+ gcloud storage buckets create "gs://${gcpStateBucket(input.projectId)}" --location="$REGION" --uniform-bucket-level-access
1160
+
1161
+ echo
1162
+ echo "Done. Your project number is: $NUMBER"
1163
+ echo "Paste it into CompBox to finish."
1164
+ `;
1165
+ }
1166
+ var GCP_REGIONS, GCP_DEPLOY_ACCOUNT, GCP_IDENTITY_POOL, GCP_IDENTITY_PROVIDER, GCP_DEPLOY_ROLES, PROJECT_ID, PROJECT_NUMBER, GITHUB_LOGIN, GCP_CONNECT_STEPS, GCP_CONNECT_STEP_LABELS;
1167
+ var init_cloud_setup = __esm({
1168
+ "../shared/dist/cloud-setup.js"() {
1169
+ "use strict";
1170
+ GCP_REGIONS = [
1171
+ "us-central1",
1172
+ "us-east1",
1173
+ "us-east4",
1174
+ "us-west1",
1175
+ "us-west2",
1176
+ "northamerica-northeast1",
1177
+ "europe-west1",
1178
+ "europe-west2",
1179
+ "europe-west3",
1180
+ "europe-west4",
1181
+ "asia-south1",
1182
+ "asia-southeast1",
1183
+ "asia-northeast1",
1184
+ "australia-southeast1"
1185
+ ];
1186
+ GCP_DEPLOY_ACCOUNT = "compbox-deploy";
1187
+ GCP_IDENTITY_POOL = "compbox";
1188
+ GCP_IDENTITY_PROVIDER = "github";
1189
+ GCP_DEPLOY_ROLES = [
1190
+ "roles/run.admin",
1191
+ "roles/artifactregistry.admin",
1192
+ "roles/iam.serviceAccountUser",
1193
+ "roles/iam.serviceAccountAdmin",
1194
+ "roles/storage.admin",
1195
+ "roles/serviceusage.serviceUsageConsumer"
1196
+ ];
1197
+ PROJECT_ID = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
1198
+ PROJECT_NUMBER = /^\d{6,20}$/;
1199
+ GITHUB_LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
1200
+ GCP_CONNECT_STEPS = ["project", "services", "account", "identity", "trust", "storage"];
1201
+ GCP_CONNECT_STEP_LABELS = {
1202
+ project: "Checking your project",
1203
+ services: "Turning on Google Cloud services",
1204
+ account: "Creating the deploy account",
1205
+ identity: "Trusting your GitHub",
1206
+ trust: "Letting your GitHub deploy",
1207
+ storage: "Making a place for deploy state"
1208
+ };
1209
+ }
1210
+ });
1211
+
1212
+ // ../shared/dist/cloud-connect.js
1213
+ function isAwsAccountId(value) {
1214
+ return AWS_ACCOUNT.test(value);
1215
+ }
1216
+ function isAwsRegion(value) {
1217
+ return AWS_REGION.test(value);
1218
+ }
1219
+ function isAwsRoleArn(value) {
1220
+ return AWS_ROLE_ARN.test(value);
1221
+ }
1222
+ function isS3BucketName(value) {
1223
+ return S3_BUCKET.test(value);
1224
+ }
1225
+ function buildAwsConnectTemplate() {
1226
+ const sub = (s) => ({ "Fn::Sub": s });
1227
+ return {
1228
+ AWSTemplateFormatVersion: "2010-09-09",
1229
+ Description: "CompBox keyless deploys: GitHub Actions from your GitHub accounts deploy as a role. No keys are stored.",
1230
+ Parameters: {
1231
+ GitHubOwners: { Type: "CommaDelimitedList", Description: "GitHub accounts whose Actions may deploy." },
1232
+ CompBoxConnectUrl: { Type: "String", Description: "Where the stack tells CompBox it's done." },
1233
+ CompBoxConnectToken: { Type: "String", NoEcho: true, Description: "One-time link from CompBox." }
1234
+ },
1235
+ Resources: {
1236
+ SetupFunctionRole: {
1237
+ Type: "AWS::IAM::Role",
1238
+ Properties: {
1239
+ AssumeRolePolicyDocument: {
1240
+ Version: "2012-10-17",
1241
+ Statement: [{ Effect: "Allow", Principal: { Service: "lambda.amazonaws.com" }, Action: "sts:AssumeRole" }]
1242
+ },
1243
+ ManagedPolicyArns: [sub("arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole")],
1244
+ Policies: [
1245
+ {
1246
+ PolicyName: "github-oidc",
1247
+ PolicyDocument: {
1248
+ Version: "2012-10-17",
1249
+ Statement: [
1250
+ {
1251
+ Effect: "Allow",
1252
+ Action: [
1253
+ "iam:ListOpenIDConnectProviders",
1254
+ "iam:GetOpenIDConnectProvider",
1255
+ "iam:CreateOpenIDConnectProvider",
1256
+ "iam:AddClientIDToOpenIDConnectProvider"
1257
+ ],
1258
+ Resource: "*"
1259
+ }
1260
+ ]
1261
+ }
1262
+ }
1263
+ ]
1264
+ }
1265
+ },
1266
+ SetupFunction: {
1267
+ Type: "AWS::Lambda::Function",
1268
+ Properties: {
1269
+ Runtime: "python3.12",
1270
+ Handler: "index.handler",
1271
+ Timeout: 60,
1272
+ Role: { "Fn::GetAtt": ["SetupFunctionRole", "Arn"] },
1273
+ Code: { ZipFile: AWS_SETUP_FUNCTION }
1274
+ }
1275
+ },
1276
+ GitHubOidc: {
1277
+ Type: "Custom::GitHubOidc",
1278
+ Properties: { ServiceToken: { "Fn::GetAtt": ["SetupFunction", "Arn"] }, Action: "oidc" }
1279
+ },
1280
+ StateBucket: {
1281
+ Type: "AWS::S3::Bucket",
1282
+ DeletionPolicy: "Retain",
1283
+ UpdateReplacePolicy: "Retain",
1284
+ Properties: {
1285
+ VersioningConfiguration: { Status: "Enabled" },
1286
+ PublicAccessBlockConfiguration: {
1287
+ BlockPublicAcls: true,
1288
+ BlockPublicPolicy: true,
1289
+ IgnorePublicAcls: true,
1290
+ RestrictPublicBuckets: true
1291
+ },
1292
+ BucketEncryption: { ServerSideEncryptionConfiguration: [{ ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } }] }
1293
+ }
1294
+ },
1295
+ DeployRole: {
1296
+ Type: "AWS::IAM::Role",
1297
+ Properties: {
1298
+ Description: "CompBox deploys (GitHub Actions, keyless)",
1299
+ AssumeRolePolicyDocument: {
1300
+ Version: "2012-10-17",
1301
+ Statement: [
1302
+ {
1303
+ Effect: "Allow",
1304
+ Principal: { Federated: { "Fn::GetAtt": ["GitHubOidc", "Arn"] } },
1305
+ Action: "sts:AssumeRoleWithWebIdentity",
1306
+ Condition: {
1307
+ StringEquals: { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
1308
+ // repo:<owner>/* for every owner.
1309
+ StringLike: {
1310
+ "token.actions.githubusercontent.com:sub": {
1311
+ "Fn::Split": [",", { "Fn::Sub": ["repo:${Owners}/*", { Owners: { "Fn::Join": ["/*,repo:", { Ref: "GitHubOwners" }] } }] }]
1312
+ }
1313
+ }
1314
+ }
1315
+ }
1316
+ ]
1317
+ },
1318
+ ManagedPolicyArns: [
1319
+ sub("arn:${AWS::Partition}:iam::aws:policy/AWSAppRunnerFullAccess"),
1320
+ sub("arn:${AWS::Partition}:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser")
1321
+ ],
1322
+ Policies: [
1323
+ {
1324
+ PolicyName: "compbox-deploy",
1325
+ PolicyDocument: {
1326
+ Version: "2012-10-17",
1327
+ Statement: [
1328
+ {
1329
+ Sid: "State",
1330
+ Effect: "Allow",
1331
+ Action: ["s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
1332
+ Resource: [{ "Fn::GetAtt": ["StateBucket", "Arn"] }, sub("${StateBucket.Arn}/*")]
1333
+ },
1334
+ {
1335
+ Sid: "AppRoles",
1336
+ Effect: "Allow",
1337
+ Action: [
1338
+ "iam:GetRole",
1339
+ "iam:PassRole",
1340
+ "iam:CreateRole",
1341
+ "iam:DeleteRole",
1342
+ "iam:TagRole",
1343
+ "iam:AttachRolePolicy",
1344
+ "iam:DetachRolePolicy",
1345
+ "iam:PutRolePolicy",
1346
+ "iam:DeleteRolePolicy",
1347
+ "iam:ListAttachedRolePolicies",
1348
+ "iam:ListRolePolicies",
1349
+ "iam:GetRolePolicy"
1350
+ ],
1351
+ Resource: sub("arn:${AWS::Partition}:iam::${AWS::AccountId}:role/compbox-*")
1352
+ }
1353
+ ]
1354
+ }
1355
+ }
1356
+ ]
1357
+ }
1358
+ },
1359
+ Connect: {
1360
+ Type: "Custom::CompBoxConnect",
1361
+ Properties: {
1362
+ ServiceToken: { "Fn::GetAtt": ["SetupFunction", "Arn"] },
1363
+ Action: "notify",
1364
+ Url: { Ref: "CompBoxConnectUrl" },
1365
+ Token: { Ref: "CompBoxConnectToken" },
1366
+ AccountId: { Ref: "AWS::AccountId" },
1367
+ Region: { Ref: "AWS::Region" },
1368
+ RoleArn: { "Fn::GetAtt": ["DeployRole", "Arn"] },
1369
+ Bucket: { Ref: "StateBucket" },
1370
+ // Re-tell CompBox when the GitHub accounts change.
1371
+ Owners: { "Fn::Join": [",", { Ref: "GitHubOwners" }] }
1372
+ }
1373
+ }
1374
+ },
1375
+ Outputs: {
1376
+ DeployRoleArn: { Value: { "Fn::GetAtt": ["DeployRole", "Arn"] } },
1377
+ StateBucket: { Value: { Ref: "StateBucket" } }
1378
+ }
1379
+ };
1380
+ }
1381
+ function awsQuickCreateUrl(input) {
1382
+ if (!isAwsRegion(input.region))
1383
+ throw new Error("Not an AWS region.");
1384
+ const owners = uniqueOwners(input.githubOwners);
1385
+ const params = new URLSearchParams({
1386
+ templateURL: input.templateUrl,
1387
+ stackName: "CompBox",
1388
+ param_GitHubOwners: owners.join(","),
1389
+ param_CompBoxConnectUrl: input.connectUrl,
1390
+ param_CompBoxConnectToken: input.connectToken
1391
+ });
1392
+ return `https://${input.region}.console.aws.amazon.com/cloudformation/home?region=${input.region}#/stacks/quickcreate?${params.toString()}`;
1393
+ }
1394
+ function isAzureGuid(value) {
1395
+ return GUID.test(value);
1396
+ }
1397
+ function isAzureResourceGroup(value) {
1398
+ return RESOURCE_GROUP.test(value);
1399
+ }
1400
+ function isAzureLocation(value) {
1401
+ return AZURE_LOCATION.test(value);
1402
+ }
1403
+ function isAzureStorageAccount(value) {
1404
+ return STORAGE_ACCOUNT.test(value);
1405
+ }
1406
+ function azureGitHubSubject(owner) {
1407
+ return `repository_owner:${owner}`;
1408
+ }
1409
+ function buildAzureConnectTemplate(input) {
1410
+ const owners = uniqueOwners(input.githubOwners);
1411
+ const identityId = `[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', '${AZURE_DEPLOY_IDENTITY}')]`;
1412
+ const identityRef = `reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', '${AZURE_DEPLOY_IDENTITY}'), '2023-01-31')`;
1413
+ return {
1414
+ $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
1415
+ contentVersion: "1.0.0.0",
1416
+ metadata: { description: "CompBox keyless deploys: GitHub Actions from your GitHub accounts deploy as a managed identity." },
1417
+ parameters: {
1418
+ location: { type: "string", defaultValue: "[resourceGroup().location]" },
1419
+ githubOwners: { type: "array", defaultValue: owners },
1420
+ connectUrl: { type: "string", defaultValue: input.connectUrl }
1421
+ },
1422
+ variables: {
1423
+ storageName: "[concat('cbstate', uniqueString(resourceGroup().id))]"
1424
+ },
1425
+ resources: [
1426
+ {
1427
+ type: "Microsoft.ManagedIdentity/userAssignedIdentities",
1428
+ apiVersion: "2023-01-31",
1429
+ name: AZURE_DEPLOY_IDENTITY,
1430
+ location: "[parameters('location')]"
1431
+ },
1432
+ {
1433
+ type: "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials",
1434
+ apiVersion: "2023-01-31",
1435
+ name: `[concat('${AZURE_DEPLOY_IDENTITY}/github-', parameters('githubOwners')[copyIndex()])]`,
1436
+ // Azure rejects concurrent writes to one identity's credentials.
1437
+ copy: { name: "owners", count: "[length(parameters('githubOwners'))]", mode: "Serial", batchSize: 1 },
1438
+ dependsOn: [identityId],
1439
+ properties: {
1440
+ issuer: "https://token.actions.githubusercontent.com",
1441
+ subject: "[concat('repository_owner:', parameters('githubOwners')[copyIndex()])]",
1442
+ audiences: ["api://AzureADTokenExchange"]
1443
+ }
1444
+ },
1445
+ {
1446
+ type: "Microsoft.Authorization/roleAssignments",
1447
+ apiVersion: "2022-04-01",
1448
+ name: `[guid(resourceGroup().id, '${AZURE_DEPLOY_IDENTITY}', 'contributor')]`,
1449
+ dependsOn: [identityId],
1450
+ properties: {
1451
+ roleDefinitionId: `[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '${AZURE_CONTRIBUTOR}')]`,
1452
+ principalId: `[${identityRef}.principalId]`,
1453
+ principalType: "ServicePrincipal"
1454
+ }
1455
+ },
1456
+ {
1457
+ type: "Microsoft.Storage/storageAccounts",
1458
+ apiVersion: "2023-01-01",
1459
+ name: "[variables('storageName')]",
1460
+ location: "[parameters('location')]",
1461
+ sku: { name: "Standard_LRS" },
1462
+ kind: "StorageV2",
1463
+ properties: { minimumTlsVersion: "TLS1_2", allowBlobPublicAccess: false, supportsHttpsTrafficOnly: true }
1464
+ },
1465
+ {
1466
+ type: "Microsoft.Storage/storageAccounts/blobServices/containers",
1467
+ apiVersion: "2023-01-01",
1468
+ name: "[concat(variables('storageName'), '/default/tfstate')]",
1469
+ dependsOn: ["[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]"]
1470
+ },
1471
+ {
1472
+ type: "Microsoft.Authorization/roleAssignments",
1473
+ apiVersion: "2022-04-01",
1474
+ name: `[guid(resourceGroup().id, '${AZURE_DEPLOY_IDENTITY}', 'state')]`,
1475
+ scope: "[concat('Microsoft.Storage/storageAccounts/', variables('storageName'))]",
1476
+ dependsOn: [identityId, "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]"],
1477
+ properties: {
1478
+ roleDefinitionId: `[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '${AZURE_BLOB_DATA_CONTRIBUTOR}')]`,
1479
+ principalId: `[${identityRef}.principalId]`,
1480
+ principalType: "ServicePrincipal"
1481
+ }
1482
+ },
1483
+ {
1484
+ type: "Microsoft.Resources/deployments",
1485
+ apiVersion: "2022-09-01",
1486
+ name: "compbox-connect",
1487
+ dependsOn: [
1488
+ "owners",
1489
+ `[resourceId('Microsoft.Authorization/roleAssignments', guid(resourceGroup().id, '${AZURE_DEPLOY_IDENTITY}', 'contributor'))]`,
1490
+ "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageName'), 'default', 'tfstate')]"
1491
+ ],
1492
+ properties: {
1493
+ mode: "Incremental",
1494
+ templateLink: {
1495
+ uri: `[format('{0}&subscriptionId={1}&tenantId={2}&resourceGroup={3}&location={4}&clientId={5}&storageAccount={6}', parameters('connectUrl'), subscription().subscriptionId, subscription().tenantId, uriComponent(resourceGroup().name), uriComponent(parameters('location')), ${identityRef}.clientId, variables('storageName'))]`
1496
+ }
1497
+ }
1498
+ }
1499
+ ],
1500
+ outputs: {
1501
+ clientId: { type: "string", value: `[${identityRef}.clientId]` },
1502
+ storageAccount: { type: "string", value: "[variables('storageName')]" }
1503
+ }
1504
+ };
1505
+ }
1506
+ function azureDeployUrl(templateUrl) {
1507
+ return `https://portal.azure.com/#create/Microsoft.Template/uri/${encodeURIComponent(templateUrl)}`;
1508
+ }
1509
+ function uniqueOwners(owners) {
1510
+ if (owners.length === 0)
1511
+ throw new Error("Connect GitHub first.");
1512
+ for (const owner of owners) {
1513
+ if (!isGitHubLogin(owner))
1514
+ throw new Error(`Not a GitHub account name: ${owner}`);
1515
+ }
1516
+ return [...new Set(owners)];
1517
+ }
1518
+ var AWS_REGIONS, AWS_ACCOUNT, AWS_REGION, AWS_ROLE_ARN, S3_BUCKET, GITHUB_OIDC_THUMBPRINTS, AWS_SETUP_FUNCTION, GUID, RESOURCE_GROUP, AZURE_LOCATION, STORAGE_ACCOUNT, AZURE_DEPLOY_IDENTITY, AZURE_CONTRIBUTOR, AZURE_BLOB_DATA_CONTRIBUTOR, EMPTY_ARM_TEMPLATE;
1519
+ var init_cloud_connect = __esm({
1520
+ "../shared/dist/cloud-connect.js"() {
1521
+ "use strict";
1522
+ init_cloud_setup();
1523
+ AWS_REGIONS = [
1524
+ "us-east-1",
1525
+ "us-east-2",
1526
+ "us-west-2",
1527
+ "ca-central-1",
1528
+ "eu-west-1",
1529
+ "eu-west-2",
1530
+ "eu-central-1",
1531
+ "ap-south-1",
1532
+ "ap-southeast-1",
1533
+ "ap-southeast-2",
1534
+ "ap-northeast-1"
1535
+ ];
1536
+ AWS_ACCOUNT = /^\d{12}$/;
1537
+ AWS_REGION = /^[a-z]{2}(?:-gov)?-[a-z]+-\d$/;
1538
+ AWS_ROLE_ARN = /^arn:aws[a-z-]*:iam::\d{12}:role\/[\w+=,.@/-]{1,512}$/;
1539
+ S3_BUCKET = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
1540
+ GITHUB_OIDC_THUMBPRINTS = ["6938fd4d98bab03faadb97b34396831e3780aea1", "1c58a3a8518e8759bf075b76b750d4f2df264fcd"];
1541
+ AWS_SETUP_FUNCTION = `import json, urllib.error, urllib.request
1542
+ import boto3
1543
+
1544
+ ISSUER = "token.actions.githubusercontent.com"
1545
+
1546
+
1547
+ def respond(event, ok, data=None, reason=""):
1548
+ body = json.dumps({
1549
+ "Status": "SUCCESS" if ok else "FAILED",
1550
+ "Reason": (reason or "See the function's CloudWatch logs")[:1000],
1551
+ "PhysicalResourceId": event.get("PhysicalResourceId") or event["LogicalResourceId"],
1552
+ "StackId": event["StackId"],
1553
+ "RequestId": event["RequestId"],
1554
+ "LogicalResourceId": event["LogicalResourceId"],
1555
+ "Data": data or {},
1556
+ }).encode()
1557
+ req = urllib.request.Request(event["ResponseURL"], data=body, method="PUT", headers={"Content-Type": ""})
1558
+ urllib.request.urlopen(req, timeout=30)
1559
+
1560
+
1561
+ def github_oidc():
1562
+ iam = boto3.client("iam")
1563
+ for p in iam.list_open_id_connect_providers()["OpenIDConnectProviderList"]:
1564
+ arn = p["Arn"]
1565
+ if arn.endswith("/" + ISSUER):
1566
+ if "sts.amazonaws.com" not in iam.get_open_id_connect_provider(OpenIDConnectProviderArn=arn)["ClientIDList"]:
1567
+ iam.add_client_id_to_open_id_connect_provider(OpenIDConnectProviderArn=arn, ClientID="sts.amazonaws.com")
1568
+ return arn
1569
+ return iam.create_open_id_connect_provider(
1570
+ Url="https://" + ISSUER, ClientIDList=["sts.amazonaws.com"], ThumbprintList=${JSON.stringify(GITHUB_OIDC_THUMBPRINTS)}
1571
+ )["OpenIDConnectProviderArn"]
1572
+
1573
+
1574
+ def notify(p):
1575
+ body = json.dumps({
1576
+ "token": p["Token"], "accountId": p["AccountId"], "region": p["Region"],
1577
+ "roleArn": p["RoleArn"], "stateBucket": p["Bucket"],
1578
+ }).encode()
1579
+ req = urllib.request.Request(p["Url"], data=body, method="POST", headers={"Content-Type": "application/json"})
1580
+ try:
1581
+ urllib.request.urlopen(req, timeout=30)
1582
+ except urllib.error.HTTPError as e:
1583
+ raise RuntimeError("CompBox said: " + e.read().decode(errors="replace")[:500])
1584
+
1585
+
1586
+ def handler(event, context):
1587
+ try:
1588
+ props = event["ResourceProperties"]
1589
+ if event["RequestType"] == "Delete":
1590
+ return respond(event, True)
1591
+ if props["Action"] == "oidc":
1592
+ return respond(event, True, {"Arn": github_oidc()})
1593
+ try:
1594
+ notify(props)
1595
+ except Exception:
1596
+ # The connect link expires; a later stack update shouldn't fail on it.
1597
+ if event["RequestType"] != "Update":
1598
+ raise
1599
+ respond(event, True)
1600
+ except Exception as e:
1601
+ print(e)
1602
+ respond(event, False, reason=str(e))
1603
+ `;
1604
+ GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1605
+ RESOURCE_GROUP = /^[-\w.()]{0,89}[-\w()]$/;
1606
+ AZURE_LOCATION = /^[a-z0-9]{2,40}$/;
1607
+ STORAGE_ACCOUNT = /^[a-z0-9]{3,24}$/;
1608
+ AZURE_DEPLOY_IDENTITY = "compbox-deploy";
1609
+ AZURE_CONTRIBUTOR = "b24988ac-6180-42a0-ab88-20f7382dd24c";
1610
+ AZURE_BLOB_DATA_CONTRIBUTOR = "ba92f5b4-2d11-453d-a403-e96b0029c9fe";
1611
+ EMPTY_ARM_TEMPLATE = {
1612
+ $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
1613
+ contentVersion: "1.0.0.0",
1614
+ resources: []
1615
+ };
1616
+ }
1617
+ });
1618
+
1619
+ // ../shared/dist/stack-files.js
1620
+ function stackEnvironments(tier) {
1621
+ return STACK_TIERS[tier].environments;
1622
+ }
1623
+ function deployRunTitle(environment) {
1624
+ return `Deploy to ${environment}`;
1625
+ }
1626
+ function cloudRunUrl(service, projectNumber, region) {
1627
+ return `https://${service}-${projectNumber}.${region}.run.app`;
1628
+ }
1629
+ function stackAppName(repoName) {
1630
+ let slug = repoName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^[^a-z]+/, "").slice(0, 20).replace(/-+$/, "");
1631
+ if (slug.length < 2)
1632
+ slug = `app${slug}`.slice(0, 20);
1633
+ return slug;
1634
+ }
1635
+ function envSettings(env) {
1636
+ return env === "prod" ? { min: 0, max: 10 } : { min: 0, max: 2 };
1637
+ }
1638
+ function buildStackFiles(input) {
1639
+ const { tier, app, defaultBranch, cloud } = input;
1640
+ if (!APP.test(app))
1641
+ throw new Error("Not a valid app name.");
1642
+ if (!BRANCH.test(defaultBranch))
1643
+ throw new Error("Not a valid branch name.");
1644
+ const envs = stackEnvironments(tier);
1645
+ const registry = `${cloud.region}-docker.pkg.dev/${cloud.projectId}/${app}`;
1646
+ const files = [];
1647
+ files.push({
1648
+ path: "infra/versions.tf",
1649
+ content: `# Managed by CompBox. Changes here are applied on the next deploy.
1650
+ terraform {
1651
+ required_version = ">= 1.6"
1652
+ required_providers {
1653
+ google = {
1654
+ source = "hashicorp/google"
1655
+ version = "~> 6.0"
1656
+ }
1657
+ }
1658
+ # Bucket and prefix are passed by the deploy workflow.
1659
+ backend "gcs" {}
1660
+ }
1661
+
1662
+ provider "google" {
1663
+ project = var.project_id
1664
+ region = var.region
1665
+ }
1666
+ `
1667
+ });
1668
+ files.push({
1669
+ path: "infra/variables.tf",
1670
+ content: `variable "project_id" {
1671
+ type = string
1672
+ }
1673
+
1674
+ variable "region" {
1675
+ type = string
1676
+ }
1677
+
1678
+ variable "app" {
1679
+ type = string
1680
+ }
1681
+
1682
+ variable "environment" {
1683
+ type = string
1684
+ }
1685
+
1686
+ variable "min_instances" {
1687
+ type = number
1688
+ default = 0
1689
+ }
1690
+
1691
+ variable "max_instances" {
1692
+ type = number
1693
+ default = 2
1694
+ }
1695
+
1696
+ # Only used the first time; every deploy sets the real image.
1697
+ variable "bootstrap_image" {
1698
+ type = string
1699
+ default = "us-docker.pkg.dev/cloudrun/container/hello"
1700
+ }
1701
+ `
1702
+ });
1703
+ files.push({
1704
+ path: "infra/main.tf",
1705
+ content: `locals {
1706
+ service = "\${var.app}-\${var.environment}"
1707
+ }
1708
+
1709
+ # One image registry for the app, created with dev and shared by every environment.
1710
+ resource "google_artifact_registry_repository" "app" {
1711
+ count = var.environment == "dev" ? 1 : 0
1712
+ repository_id = var.app
1713
+ location = var.region
1714
+ format = "DOCKER"
1715
+ }
1716
+
1717
+ # The account the app runs as in this environment (no permissions by default).
1718
+ resource "google_service_account" "runtime" {
1719
+ account_id = local.service
1720
+ display_name = "\${local.service} (CompBox)"
1721
+ }
1722
+
1723
+ resource "google_cloud_run_v2_service" "app" {
1724
+ name = local.service
1725
+ location = var.region
1726
+ ingress = "INGRESS_TRAFFIC_ALL"
1727
+ deletion_protection = var.environment == "prod"
1728
+
1729
+ template {
1730
+ service_account = google_service_account.runtime.email
1731
+ scaling {
1732
+ min_instance_count = var.min_instances
1733
+ max_instance_count = var.max_instances
1734
+ }
1735
+ containers {
1736
+ image = var.bootstrap_image
1737
+ }
1738
+ }
1739
+
1740
+ # Deploys change the image; Terraform owns everything else.
1741
+ lifecycle {
1742
+ ignore_changes = [template[0].containers[0].image, client, client_version]
1743
+ }
1744
+ }
1745
+
1746
+ # Anyone with the link can open it. Remove this to keep the app private.
1747
+ resource "google_cloud_run_v2_service_iam_member" "public" {
1748
+ name = google_cloud_run_v2_service.app.name
1749
+ location = var.region
1750
+ role = "roles/run.invoker"
1751
+ member = "allUsers"
1752
+ }
1753
+
1754
+ output "url" {
1755
+ value = google_cloud_run_v2_service.app.uri
1756
+ }
1757
+ `
1758
+ });
1759
+ for (const env of envs) {
1760
+ const s = envSettings(env);
1761
+ files.push({
1762
+ path: `infra/envs/${env}.tfvars`,
1763
+ content: `project_id = "${cloud.projectId}"
1764
+ region = "${cloud.region}"
1765
+ app = "${app}"
1766
+ environment = "${env}"
1767
+ min_instances = ${String(s.min)}
1768
+ max_instances = ${String(s.max)}
1769
+ `
1770
+ });
1771
+ }
1772
+ const options = envs.map((e) => ` - ${e}`).join("\n");
1773
+ files.push({
1774
+ path: ".github/workflows/compbox.yml",
1775
+ content: `# Managed by CompBox: every push to ${defaultBranch} deploys to dev.${envs.length > 1 ? "\n# Stage and prod: Actions \u2192 CompBox \u2192 Run workflow. Prod waits for its environment's approval." : ""}
1776
+ name: CompBox
1777
+ run-name: Deploy to \${{ inputs.environment || 'dev' }}
1778
+
1779
+ on:
1780
+ push:
1781
+ branches: [${JSON.stringify(defaultBranch)}]
1782
+ workflow_dispatch:
1783
+ inputs:
1784
+ environment:
1785
+ description: Where to deploy
1786
+ type: choice
1787
+ default: dev
1788
+ options:
1789
+ ${options}
1790
+
1791
+ permissions:
1792
+ contents: read
1793
+ id-token: write
1794
+
1795
+ jobs:
1796
+ deploy:
1797
+ uses: ./.github/workflows/compbox-deploy.yml
1798
+ with:
1799
+ environment: \${{ inputs.environment || 'dev' }}
1800
+ `
1801
+ });
1802
+ files.push({
1803
+ path: ".github/workflows/compbox-deploy.yml",
1804
+ content: `# Managed by CompBox. Signs in to Google Cloud without stored keys (Workload Identity).
1805
+ name: CompBox deploy
1806
+
1807
+ on:
1808
+ workflow_call:
1809
+ inputs:
1810
+ environment:
1811
+ type: string
1812
+ required: true
1813
+
1814
+ permissions:
1815
+ contents: read
1816
+ id-token: write
1817
+
1818
+ concurrency:
1819
+ group: compbox-\${{ inputs.environment }}
1820
+ cancel-in-progress: false
1821
+
1822
+ jobs:
1823
+ deploy:
1824
+ runs-on: ubuntu-latest
1825
+ environment:
1826
+ name: \${{ inputs.environment }}
1827
+ url: \${{ steps.deploy.outputs.url }}
1828
+ env:
1829
+ ENVIRONMENT: \${{ inputs.environment }}
1830
+ PROJECT_ID: ${cloud.projectId}
1831
+ REGION: ${cloud.region}
1832
+ APP: ${app}
1833
+ IMAGE: ${registry}/app:\${{ github.sha }}
1834
+ STATE_BUCKET: ${gcpStateBucket(cloud.projectId)}
1835
+ steps:
1836
+ - uses: actions/checkout@v4
1837
+
1838
+ - uses: google-github-actions/auth@v2
1839
+ with:
1840
+ workload_identity_provider: ${cloud.workloadProvider}
1841
+ service_account: ${cloud.serviceAccountEmail}
1842
+
1843
+ - uses: google-github-actions/setup-gcloud@v2
1844
+
1845
+ - uses: hashicorp/setup-terraform@v3
1846
+
1847
+ - name: Infrastructure
1848
+ working-directory: infra
1849
+ run: |
1850
+ terraform init -input=false -backend-config="bucket=$STATE_BUCKET" -backend-config="prefix=$APP/$ENVIRONMENT"
1851
+ terraform apply -input=false -auto-approve -var-file="envs/$ENVIRONMENT.tfvars"
1852
+
1853
+ - name: Build
1854
+ run: |
1855
+ if [ ! -f Dockerfile ]; then
1856
+ echo "::error::Add a Dockerfile at the repository root: CompBox deploys the app as a container that listens on \\$PORT."
1857
+ exit 1
1858
+ fi
1859
+ gcloud auth configure-docker "$REGION-docker.pkg.dev" --quiet
1860
+ docker build -t "$IMAGE" .
1861
+ docker push "$IMAGE"
1862
+
1863
+ - name: Deploy
1864
+ id: deploy
1865
+ run: |
1866
+ SERVICE="$APP-$ENVIRONMENT"
1867
+ gcloud run deploy "$SERVICE" --image "$IMAGE" --region "$REGION" --project "$PROJECT_ID" --quiet
1868
+ URL=$(gcloud run services describe "$SERVICE" --region "$REGION" --project "$PROJECT_ID" --format='value(status.url)')
1869
+ echo "url=$URL" >> "$GITHUB_OUTPUT"
1870
+ echo "### Live at $URL" >> "$GITHUB_STEP_SUMMARY"
1871
+ `
1872
+ });
1873
+ files.push({
1874
+ path: "infra/README.md",
1875
+ content: `# Infrastructure (managed by CompBox)
1876
+
1877
+ This project deploys to Google Cloud project \`${cloud.projectId}\` (${cloud.region}) as a Cloud Run service per environment: ${envs.map((e) => `\`${app}-${e}\``).join(", ")}.
1878
+
1879
+ - **Dev:** every push to \`${defaultBranch}\` deploys it.${envs.length > 1 ? "\n- **Stage and prod:** in GitHub, Actions \u2192 CompBox \u2192 Run workflow, then pick the environment. Add required reviewers to the `prod` environment (Settings \u2192 Environments) so prod waits for approval." : ""}
1880
+ - **The app:** a \`Dockerfile\` at the repository root, for a container that listens on \`$PORT\` (8080).
1881
+ - **Sign-in:** GitHub Actions sign in to Google Cloud through Workload Identity \u2014 there are no keys to store or rotate.
1882
+ - **Changes:** edit the Terraform here; the next deploy applies it. Services are public by default; remove the \`public\` resource in \`main.tf\` to keep one private.
1883
+ `
1884
+ });
1885
+ return files;
1886
+ }
1887
+ var STACK_TIERS, STACK_WORKFLOW, APP, BRANCH;
1888
+ var init_stack_files = __esm({
1889
+ "../shared/dist/stack-files.js"() {
1890
+ "use strict";
1891
+ init_cloud_setup();
1892
+ STACK_TIERS = {
1893
+ dev: { label: "Dev only", environments: ["dev"] },
1894
+ full: { label: "Dev, stage and prod", environments: ["dev", "stage", "prod"] }
1895
+ };
1896
+ STACK_WORKFLOW = "compbox.yml";
1897
+ APP = /^[a-z][a-z0-9-]{1,19}$/;
1898
+ BRANCH = /^[A-Za-z0-9._/-]{1,200}$/;
1899
+ }
1900
+ });
1901
+
1902
+ // ../shared/dist/github-repo.js
1903
+ function githubRepoKey(remote) {
1904
+ if (typeof remote !== "string")
1905
+ return null;
1906
+ const normalized = normalizeGitRemote(remote);
1907
+ const match = /^https:\/\/([^/]+)\/(.*)$/.exec(normalized);
1908
+ if (!match)
1909
+ return null;
1910
+ const host = match[1];
1911
+ const path4 = match[2];
1912
+ if (host !== "github.com" || path4 === void 0)
1913
+ return null;
1914
+ const segments = path4.split("/");
1915
+ if (segments.length !== 2)
1916
+ return null;
1917
+ const [owner, name] = segments;
1918
+ if (!owner || !name)
1919
+ return null;
1920
+ return `${owner}/${name}`.toLowerCase();
1921
+ }
1922
+ var init_github_repo = __esm({
1923
+ "../shared/dist/github-repo.js"() {
1924
+ "use strict";
1925
+ init_mailbox();
1926
+ }
1927
+ });
1928
+
1929
+ // ../shared/dist/return-to.js
1930
+ function safeReturnTo(value) {
1931
+ if (typeof value !== "string")
1932
+ return null;
1933
+ if (value.length < 1 || value.length > RETURN_TO_MAX_LENGTH)
1934
+ return null;
1935
+ if (!value.startsWith("/"))
1936
+ return null;
1937
+ const second = value.charAt(1);
1938
+ if (second === "/" || second === "\\")
1939
+ return null;
1940
+ if (value.includes("\\"))
1941
+ return null;
1942
+ if (value.includes("://"))
1943
+ return null;
1944
+ for (let i = 0; i < value.length; i++) {
1945
+ const code = value.charCodeAt(i);
1946
+ if (code < 32 || code === 127)
1947
+ return null;
1948
+ }
1949
+ return value;
1950
+ }
1951
+ var RETURN_TO_MAX_LENGTH;
1952
+ var init_return_to = __esm({
1953
+ "../shared/dist/return-to.js"() {
1954
+ "use strict";
1955
+ RETURN_TO_MAX_LENGTH = 512;
1956
+ }
1957
+ });
1958
+
833
1959
  // ../shared/dist/index.js
834
1960
  var dist_exports = {};
835
1961
  __export(dist_exports, {
836
1962
  AI_PROVIDERS: () => AI_PROVIDERS,
837
1963
  APPEND_DESCRIPTION_MAX: () => APPEND_DESCRIPTION_MAX,
1964
+ AWS_REGIONS: () => AWS_REGIONS,
1965
+ AZURE_DEPLOY_IDENTITY: () => AZURE_DEPLOY_IDENTITY,
838
1966
  COMPLETION_REPAIR_INSTRUCTION: () => COMPLETION_REPAIR_INSTRUCTION,
1967
+ CONVERSATION_QUALITY_ROUNDS: () => CONVERSATION_QUALITY_ROUNDS,
839
1968
  DESCRIPTION_MAX: () => DESCRIPTION_MAX,
1969
+ EMPTY_ARM_TEMPLATE: () => EMPTY_ARM_TEMPLATE,
1970
+ GCP_CONNECT_STEPS: () => GCP_CONNECT_STEPS,
1971
+ GCP_CONNECT_STEP_LABELS: () => GCP_CONNECT_STEP_LABELS,
1972
+ GCP_DEPLOY_ACCOUNT: () => GCP_DEPLOY_ACCOUNT,
1973
+ GCP_DEPLOY_ROLES: () => GCP_DEPLOY_ROLES,
1974
+ GCP_IDENTITY_POOL: () => GCP_IDENTITY_POOL,
1975
+ GCP_IDENTITY_PROVIDER: () => GCP_IDENTITY_PROVIDER,
1976
+ GCP_REGIONS: () => GCP_REGIONS,
840
1977
  INTERVIEW_DEFINITION: () => INTERVIEW_DEFINITION,
841
1978
  INTERVIEW_FIELD_MIN_WORDS: () => INTERVIEW_FIELD_MIN_WORDS,
842
1979
  INTERVIEW_STATUSES: () => INTERVIEW_STATUSES,
@@ -846,6 +1983,9 @@ __export(dist_exports, {
846
1983
  MODEL_CATALOG: () => MODEL_CATALOG,
847
1984
  ORG_ROLES: () => ORG_ROLES,
848
1985
  PXPIPE_DEFAULT_PROXY_URL: () => PXPIPE_DEFAULT_PROXY_URL,
1986
+ RETURN_TO_MAX_LENGTH: () => RETURN_TO_MAX_LENGTH,
1987
+ STACK_TIERS: () => STACK_TIERS,
1988
+ STACK_WORKFLOW: () => STACK_WORKFLOW,
849
1989
  START_EXECUTION_KINDS: () => START_EXECUTION_KINDS,
850
1990
  SUBSCRIPTION_STATUSES: () => SUBSCRIPTION_STATUSES,
851
1991
  SUMMARY_MAX: () => SUMMARY_MAX,
@@ -855,23 +1995,50 @@ __export(dist_exports, {
855
1995
  agentSchema: () => agentSchema,
856
1996
  aiProviderValues: () => aiProviderValues,
857
1997
  aiRouteValues: () => aiRouteValues,
1998
+ awsQuickCreateUrl: () => awsQuickCreateUrl,
1999
+ azureDeployUrl: () => azureDeployUrl,
2000
+ azureGitHubSubject: () => azureGitHubSubject,
858
2001
  buildAuditFeedback: () => buildAuditFeedback,
2002
+ buildAwsConnectTemplate: () => buildAwsConnectTemplate,
2003
+ buildAzureConnectTemplate: () => buildAzureConnectTemplate,
2004
+ buildConversationSystemPrompt: () => buildConversationSystemPrompt,
2005
+ buildGcpSetupScript: () => buildGcpSetupScript,
859
2006
  buildInterviewSystemPrompt: () => buildInterviewSystemPrompt,
2007
+ buildStackFiles: () => buildStackFiles,
2008
+ cloudRunUrl: () => cloudRunUrl,
2009
+ complianceFromRecord: () => complianceFromRecord,
2010
+ deployRunTitle: () => deployRunTitle,
860
2011
  err: () => err,
861
2012
  extractCompletionJson: () => extractCompletionJson,
2013
+ gcpServiceAccountEmail: () => gcpServiceAccountEmail,
2014
+ gcpStateBucket: () => gcpStateBucket,
2015
+ gcpWorkloadProvider: () => gcpWorkloadProvider,
862
2016
  getAnthropicProxyUrl: () => getAnthropicProxyUrl,
863
2017
  getModelById: () => getModelById,
864
2018
  getModelDisplayName: () => getModelDisplayName,
865
2019
  getOpenAiProxyUrl: () => getOpenAiProxyUrl,
2020
+ githubRepoKey: () => githubRepoKey,
866
2021
  instructionSchema: () => instructionSchema,
867
2022
  interviewFieldKeys: () => interviewFieldKeys,
868
2023
  interviewSectionKeys: () => interviewSectionKeys,
2024
+ isAwsAccountId: () => isAwsAccountId,
2025
+ isAwsRegion: () => isAwsRegion,
2026
+ isAwsRoleArn: () => isAwsRoleArn,
2027
+ isAzureGuid: () => isAzureGuid,
2028
+ isAzureLocation: () => isAzureLocation,
2029
+ isAzureResourceGroup: () => isAzureResourceGroup,
2030
+ isAzureStorageAccount: () => isAzureStorageAccount,
2031
+ isGcpProjectId: () => isGcpProjectId,
2032
+ isGcpProjectNumber: () => isGcpProjectNumber,
2033
+ isGcpRegion: () => isGcpRegion,
2034
+ isGitHubLogin: () => isGitHubLogin,
869
2035
  isInterviewStatus: () => isInterviewStatus,
870
2036
  isJobStatus: () => isJobStatus,
871
2037
  isJobType: () => isJobType,
872
2038
  isMachineStatus: () => isMachineStatus,
873
2039
  isOrgRole: () => isOrgRole,
874
2040
  isPxpipeEnabled: () => isPxpipeEnabled,
2041
+ isS3BucketName: () => isS3BucketName,
875
2042
  isSessionErrorEvent: () => isSessionErrorEvent,
876
2043
  isSessionFeatureEvent: () => isSessionFeatureEvent,
877
2044
  isSessionLifecycleEvent: () => isSessionLifecycleEvent,
@@ -889,12 +2056,20 @@ __export(dist_exports, {
889
2056
  normalizeGitRemote: () => normalizeGitRemote,
890
2057
  ok: () => ok,
891
2058
  probeProxyHealth: () => probeProxyHealth,
2059
+ renderConversationPrompt: () => renderConversationPrompt,
892
2060
  resolveRouting: () => resolveRouting,
2061
+ runConversationStep: () => runConversationStep,
2062
+ safeReturnTo: () => safeReturnTo,
893
2063
  safeValidateSkillPackManifest: () => safeValidateSkillPackManifest,
2064
+ savedContextFromRecord: () => savedContextFromRecord,
2065
+ savedMessage: () => savedMessage,
894
2066
  skillPackManifestSchema: () => skillPackManifestSchema,
895
2067
  skillSchema: () => skillSchema,
896
2068
  sleep: () => sleep,
2069
+ stackAppName: () => stackAppName,
2070
+ stackEnvironments: () => stackEnvironments,
897
2071
  thinkingLevelValues: () => thinkingLevelValues,
2072
+ toConversationMessages: () => toConversationMessages,
898
2073
  validatePackName: () => validatePackName,
899
2074
  validateSkillPackManifest: () => validateSkillPackManifest
900
2075
  });
@@ -909,6 +2084,12 @@ var init_dist = __esm({
909
2084
  init_pxpipe();
910
2085
  init_interview_definition();
911
2086
  init_interview_completion();
2087
+ init_interview_conversation();
2088
+ init_cloud_setup();
2089
+ init_cloud_connect();
2090
+ init_stack_files();
2091
+ init_github_repo();
2092
+ init_return_to();
912
2093
  }
913
2094
  });
914
2095
 
@@ -11089,7 +12270,7 @@ function withLegacyToolCallRewrite(transport) {
11089
12270
  var SERVER_NAME = "compbox-mcp";
11090
12271
  function resolveServerVersion() {
11091
12272
  if (true) {
11092
- return "1.19.0";
12273
+ return "1.21.0";
11093
12274
  }
11094
12275
  const require2 = createRequire(import.meta.url);
11095
12276
  for (const candidate of ["../package.json", "./package.json"]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycompbox/compbox-mcp",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "CompBox MCP server - Uses HTTP API client (no direct database access)",
5
5
  "license": "MIT",
6
6
  "type": "module",