thinkwork-cli 0.12.15 → 0.12.16

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.
@@ -5,7 +5,7 @@ import {
5
5
  readTfVar,
6
6
  resolveApiConfig,
7
7
  resolveTfvarsPath
8
- } from "./chunk-H7AB42ES.js";
8
+ } from "./chunk-STY44FQO.js";
9
9
  export {
10
10
  apiFetch,
11
11
  apiFetchRaw,
@@ -310,6 +310,18 @@ function resolveTerraformRoot(startDir = process.cwd()) {
310
310
  function isTerraformRoot(dir) {
311
311
  return existsSync3(path2.join(dir, "examples", "greenfield")) || existsSync3(path2.join(dir, "environments")) || existsSync3(path2.join(dir, "main.tf"));
312
312
  }
313
+ function resolveTerraformRootForStage(stage, recordedDir, startDir = process.cwd()) {
314
+ const cwdRoot = resolveTerraformRoot(startDir);
315
+ try {
316
+ resolveTierDir(cwdRoot, stage, "app");
317
+ return cwdRoot;
318
+ } catch {
319
+ if (recordedDir && existsSync3(path2.join(recordedDir, "main.tf"))) {
320
+ return recordedDir;
321
+ }
322
+ return cwdRoot;
323
+ }
324
+ }
313
325
  function resolveTierDir(terraformDir, stage, tier) {
314
326
  const envDir = path2.join(terraformDir, "environments", stage, tier);
315
327
  if (existsSync3(envDir)) {
@@ -668,6 +680,7 @@ export {
668
680
  ensureStateBackend,
669
681
  parseLockError,
670
682
  resolveTerraformRoot,
683
+ resolveTerraformRootForStage,
671
684
  resolveTierDir,
672
685
  isInitScaffoldedLayout,
673
686
  ensureWorkspace,
package/dist/cli.js CHANGED
@@ -26,13 +26,14 @@ import {
26
26
  resolveApiConfig,
27
27
  resolveTerraformDir,
28
28
  resolveTerraformRoot,
29
+ resolveTerraformRootForStage,
29
30
  resolveTierDir,
30
31
  runTerraform,
31
32
  runTerraformTee,
32
33
  saveEnterpriseDeployment,
33
34
  saveEnvironment,
34
35
  terraformOutput
35
- } from "./chunk-H7AB42ES.js";
36
+ } from "./chunk-STY44FQO.js";
36
37
 
37
38
  // src/cli.ts
38
39
  import { Command } from "commander";
@@ -319,6 +320,7 @@ function registerPlanCommand(program2) {
319
320
 
320
321
  // src/commands/deploy.ts
321
322
  import { spawn as spawn2, spawnSync as spawnSync6 } from "child_process";
323
+ import { randomBytes as randomBytes4 } from "crypto";
322
324
  import {
323
325
  existsSync as existsSync9,
324
326
  mkdirSync as mkdirSync3,
@@ -402,6 +404,7 @@ function checkAwsIdentity() {
402
404
  };
403
405
  }
404
406
  var DOCTOR_BEDROCK_PROBE_MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0";
407
+ var DOCTOR_BEDROCK_FALLBACK_MODEL_ID = "us.anthropic.claude-sonnet-4-5-20250929-v1:0";
405
408
  function evaluateBedrockProbe(error) {
406
409
  if (error === null) {
407
410
  return {
@@ -435,22 +438,34 @@ function evaluateBedrockProbe(error) {
435
438
  function checkBedrockAccess() {
436
439
  return {
437
440
  name: "Bedrock model invocation",
438
- run: () => {
441
+ run: async () => {
439
442
  const region = process.env.AWS_REGION || "us-east-1";
440
- try {
441
- execSync2(
442
- `aws bedrock-runtime converse --model-id ${DOCTOR_BEDROCK_PROBE_MODEL_ID} --messages '[{"role":"user","content":[{"text":"Reply with OK"}]}]' --inference-config '{"maxTokens":1}' --output json --region ${region}`,
443
- {
444
- encoding: "utf-8",
445
- timeout: 3e4,
446
- stdio: ["pipe", "pipe", "pipe"]
443
+ let lastError = null;
444
+ const models = [
445
+ DOCTOR_BEDROCK_PROBE_MODEL_ID,
446
+ DOCTOR_BEDROCK_PROBE_MODEL_ID,
447
+ DOCTOR_BEDROCK_FALLBACK_MODEL_ID
448
+ ];
449
+ for (let attempt = 1; attempt <= 3; attempt++) {
450
+ try {
451
+ execSync2(
452
+ `aws bedrock-runtime converse --model-id ${models[attempt - 1]} --messages '[{"role":"user","content":[{"text":"Reply with OK"}]}]' --inference-config '{"maxTokens":1}' --output json --region ${region}`,
453
+ {
454
+ encoding: "utf-8",
455
+ timeout: 3e4,
456
+ stdio: ["pipe", "pipe", "pipe"]
457
+ }
458
+ );
459
+ return evaluateBedrockProbe(null);
460
+ } catch (err) {
461
+ lastError = err instanceof Error && "stderr" in err ? String(err.stderr ?? err.message) : String(err);
462
+ if (!lastError.includes("ThrottlingException") || attempt === 3) {
463
+ return evaluateBedrockProbe(lastError);
447
464
  }
448
- );
449
- return evaluateBedrockProbe(null);
450
- } catch (err) {
451
- const stderr = err instanceof Error && "stderr" in err ? String(err.stderr ?? err.message) : String(err);
452
- return evaluateBedrockProbe(stderr);
465
+ await new Promise((r) => setTimeout(r, attempt * 5e3));
466
+ }
453
467
  }
468
+ return evaluateBedrockProbe(lastError);
454
469
  }
455
470
  };
456
471
  }
@@ -618,6 +633,62 @@ function checkSesStatus(exec = awsExec) {
618
633
  }
619
634
  };
620
635
  }
636
+ function parseEcrImageUri(uri) {
637
+ const match = uri.match(/^(\d+\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com)\//);
638
+ return match ? { registry: match[1], region: match[2] } : null;
639
+ }
640
+ function evaluateAgentcorePiImageProbe(input17) {
641
+ if (!input17.dockerAvailable) {
642
+ return {
643
+ pass: true,
644
+ detail: "docker unavailable \u2014 cannot probe the pinned image; the apply-time seed reports pull failures"
645
+ };
646
+ }
647
+ if (input17.reachable) {
648
+ return { pass: true, detail: `${input17.uri} reachable` };
649
+ }
650
+ const reason = (input17.error ?? "").trim().split("\n")[0].slice(0, 160);
651
+ return {
652
+ pass: false,
653
+ detail: `pinned AgentCore image ${input17.uri} is not pullable from this machine` + (reason ? ` (${reason})` : "") + ". The apply keeps an already-seeded ECR image or builds from a repo checkout; otherwise set agentcore_pi_source_image_uri in terraform.tfvars to a pullable image."
654
+ };
655
+ }
656
+ function checkAgentcorePiSourceImage(uri) {
657
+ return {
658
+ name: "AgentCore image pullable",
659
+ blocking: false,
660
+ run: () => {
661
+ const docker = spawnSync("docker", ["--version"], { encoding: "utf8" });
662
+ if (docker.status !== 0) {
663
+ return evaluateAgentcorePiImageProbe({
664
+ uri,
665
+ dockerAvailable: false,
666
+ reachable: false
667
+ });
668
+ }
669
+ const ecr = parseEcrImageUri(uri);
670
+ if (ecr) {
671
+ spawnSync(
672
+ "bash",
673
+ [
674
+ "-c",
675
+ `aws ecr get-login-password --region ${ecr.region} | docker login --username AWS --password-stdin ${ecr.registry}`
676
+ ],
677
+ { encoding: "utf8" }
678
+ );
679
+ }
680
+ const probe = spawnSync("docker", ["manifest", "inspect", uri], {
681
+ encoding: "utf8"
682
+ });
683
+ return evaluateAgentcorePiImageProbe({
684
+ uri,
685
+ dockerAvailable: true,
686
+ reachable: probe.status === 0,
687
+ error: probe.stderr
688
+ });
689
+ }
690
+ };
691
+ }
621
692
  function doctorChecks() {
622
693
  return [
623
694
  checkAwsCli(),
@@ -636,6 +707,9 @@ function preflightChecks(ctx) {
636
707
  if (ctx.backend) checks.push(checkStateBackend(ctx.backend));
637
708
  if (ctx.domain) checks.push(checkDomainDelegation(ctx.domain));
638
709
  if (ctx.sesConfigured) checks.push(checkSesStatus());
710
+ if (ctx.agentcorePiSourceImage) {
711
+ checks.push(checkAgentcorePiSourceImage(ctx.agentcorePiSourceImage));
712
+ }
639
713
  return checks;
640
714
  }
641
715
  async function runChecks(checks) {
@@ -1247,6 +1321,83 @@ COMMIT;`;
1247
1321
  }
1248
1322
  }
1249
1323
 
1324
+ // src/lib/owner-tenant.ts
1325
+ import { randomBytes as randomBytes2 } from "crypto";
1326
+
1327
+ // ../../packages/database-pg/src/utils/reserved-slugs.ts
1328
+ var TENANT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/;
1329
+ var RESERVED_TENANT_SLUGS = [
1330
+ "admin",
1331
+ "agents",
1332
+ "api",
1333
+ "app",
1334
+ "assets",
1335
+ "canary",
1336
+ "cdn",
1337
+ "dev",
1338
+ "docs",
1339
+ "mail",
1340
+ "mobile",
1341
+ "prod",
1342
+ "staging",
1343
+ "test",
1344
+ "www"
1345
+ ];
1346
+ var RESERVED_TENANT_SLUG_SET = new Set(RESERVED_TENANT_SLUGS);
1347
+ function isReservedTenantSlug(slug) {
1348
+ return RESERVED_TENANT_SLUG_SET.has(slug);
1349
+ }
1350
+
1351
+ // src/lib/owner-tenant.ts
1352
+ function deriveOwnerTenantSlug(stage, random = () => randomBytes2(3).toString("hex")) {
1353
+ const sanitized = stage.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1354
+ if (TENANT_SLUG_PATTERN.test(sanitized) && !isReservedTenantSlug(sanitized)) {
1355
+ return sanitized;
1356
+ }
1357
+ return `workspace-${random()}`;
1358
+ }
1359
+ function deriveOwnerTenantName(slug) {
1360
+ return slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1361
+ }
1362
+ function sqlQuote(value) {
1363
+ return `'${value.replace(/'/g, "''")}'`;
1364
+ }
1365
+ function buildEnsureOwnerTenantSql(input17) {
1366
+ const name = sqlQuote(input17.name);
1367
+ const slug = sqlQuote(input17.slug);
1368
+ const email = sqlQuote(input17.email.toLowerCase());
1369
+ return `WITH new_tenant AS (
1370
+ INSERT INTO tenants (name, slug, issue_prefix, issue_counter, pending_owner_email, first_admin_claim_required)
1371
+ SELECT ${name}, ${slug}, 'TW', 0, ${email}, true
1372
+ WHERE NOT EXISTS (SELECT 1 FROM tenants)
1373
+ RETURNING id
1374
+ )
1375
+ INSERT INTO tenant_settings (tenant_id)
1376
+ SELECT id FROM new_tenant
1377
+ ON CONFLICT DO NOTHING
1378
+ RETURNING tenant_id;`;
1379
+ }
1380
+ async function ensureOwnerTenant(options) {
1381
+ const slug = deriveOwnerTenantSlug(options.stage);
1382
+ const runner = await (options.connect ?? connectPsql)(options.connection);
1383
+ try {
1384
+ const result = await runner.query(
1385
+ buildEnsureOwnerTenantSql({
1386
+ name: deriveOwnerTenantName(slug),
1387
+ slug,
1388
+ email: options.email
1389
+ })
1390
+ );
1391
+ return {
1392
+ created: (result.rows ?? []).length > 0,
1393
+ slug,
1394
+ email: options.email.toLowerCase()
1395
+ };
1396
+ } finally {
1397
+ await runner.end();
1398
+ }
1399
+ }
1400
+
1250
1401
  // src/commands/bootstrap.ts
1251
1402
  import { spawn } from "child_process";
1252
1403
  import { existsSync as existsSync4 } from "fs";
@@ -1320,7 +1471,10 @@ function registerBootstrapCommand(program2) {
1320
1471
  }
1321
1472
  const identity = getAwsIdentity();
1322
1473
  printHeader("bootstrap", stage, identity);
1323
- const terraformDir = resolveTerraformRoot();
1474
+ const terraformDir = resolveTerraformRootForStage(
1475
+ stage,
1476
+ loadEnvironment(stage)?.terraformDir
1477
+ );
1324
1478
  const cwd = resolveTierDir(terraformDir, stage, "app");
1325
1479
  await ensureInit(cwd);
1326
1480
  await ensureWorkspace(cwd, stage);
@@ -1486,6 +1640,28 @@ function buildVerifyChecks(ctx) {
1486
1640
  break;
1487
1641
  }
1488
1642
  }
1643
+ if (url) {
1644
+ const cfg = httpProbe(`${url}/thinkwork-runtime-config.json`);
1645
+ if (cfg.status !== 200 || !cfg.body.includes("cognitoClientId")) {
1646
+ return {
1647
+ pass: false,
1648
+ detail: `${url} serves HTML but /thinkwork-runtime-config.json is ${cfg.status === 200 ? "incomplete" : "missing"} \u2014 the app cannot sign in. Rerun thinkwork deploy to publish it.`
1649
+ };
1650
+ }
1651
+ const parsed = (() => {
1652
+ try {
1653
+ return JSON.parse(cfg.body);
1654
+ } catch {
1655
+ return {};
1656
+ }
1657
+ })();
1658
+ if (!parsed.viteEnv?.VITE_COGNITO_CLIENT_ID) {
1659
+ return {
1660
+ pass: false,
1661
+ detail: `${url} runtime config has no viteEnv.VITE_COGNITO_CLIENT_ID \u2014 sign-in would be unavailable.`
1662
+ };
1663
+ }
1664
+ }
1489
1665
  if (!url && candidates.length > 0) {
1490
1666
  url = `https://${candidates[0].Domain}`;
1491
1667
  }
@@ -3173,7 +3349,7 @@ function git(args) {
3173
3349
 
3174
3350
  // src/commands/enterprise/secrets.ts
3175
3351
  import { execFileSync as execFileSync5 } from "child_process";
3176
- import { randomBytes as randomBytes2 } from "crypto";
3352
+ import { randomBytes as randomBytes3 } from "crypto";
3177
3353
  var GhCliEnterpriseSecretSetter = class {
3178
3354
  async setEnvironmentSecret(repository, stage, name, value) {
3179
3355
  execFileSync5(
@@ -3241,7 +3417,7 @@ async function setEnterpriseStageSecrets(repository, stageSecrets, setter) {
3241
3417
  return results;
3242
3418
  }
3243
3419
  function generateUrlSafeSecret(bytes = 32) {
3244
- return randomBytes2(bytes).toString("base64url");
3420
+ return randomBytes3(bytes).toString("base64url");
3245
3421
  }
3246
3422
  async function resolveSecretValue(stage, name, explicit, stdinIsTty, promptSecret, generateSecret2) {
3247
3423
  if (explicit) return explicit;
@@ -4187,6 +4363,167 @@ async function ensureReleaseArtifacts(cwd, identity, stage, versionFlag) {
4187
4363
  }
4188
4364
  return { version, webAssetSource };
4189
4365
  }
4366
+ function buildRuntimeConfig(values) {
4367
+ const cognitoDomain = values.authDomain.startsWith("https://") ? values.authDomain : values.authDomain ? `https://${values.authDomain}.auth.${values.region}.amazoncognito.com` : "";
4368
+ const api = values.apiEndpoint.replace(/\/+$/, "");
4369
+ return {
4370
+ stage: values.stage,
4371
+ region: values.region,
4372
+ accountId: values.accountId,
4373
+ releaseVersion: values.releaseVersion,
4374
+ releaseManifestUrl: null,
4375
+ releaseManifestSha256: null,
4376
+ deploymentId: `thinkwork-${values.stage}`,
4377
+ displayName: "ThinkWork",
4378
+ appUrl: values.appUrl,
4379
+ apiEndpoint: values.apiEndpoint,
4380
+ graphqlHttpUrl: api ? `${api}/graphql` : "",
4381
+ appsyncUrl: values.appsyncUrl,
4382
+ appsyncRealtimeUrl: values.appsyncRealtimeUrl,
4383
+ appsyncApiKey: values.appsyncApiKey,
4384
+ cognitoDomain,
4385
+ cognitoUserPoolId: values.userPoolId,
4386
+ cognitoClientId: values.adminClientId,
4387
+ controller: null,
4388
+ issuedAt: values.issuedAt,
4389
+ // The web app consumes ONLY this map (runtime-config.ts reads
4390
+ // raw.viteEnv) — the outer profile is for tooling. Omitting it kept the
4391
+ // sign-in screen dead even with the file published (HCI test).
4392
+ viteEnv: {
4393
+ VITE_API_URL: values.apiEndpoint,
4394
+ VITE_GRAPHQL_HTTP_URL: api ? `${api}/graphql` : "",
4395
+ VITE_GRAPHQL_URL: values.appsyncUrl,
4396
+ VITE_GRAPHQL_WS_URL: values.appsyncRealtimeUrl,
4397
+ VITE_GRAPHQL_API_KEY: values.appsyncApiKey,
4398
+ VITE_COGNITO_DOMAIN: cognitoDomain,
4399
+ VITE_COGNITO_USER_POOL_ID: values.userPoolId,
4400
+ VITE_COGNITO_CLIENT_ID: values.adminClientId,
4401
+ VITE_DEPLOYMENT_ID: `thinkwork-${values.stage}`,
4402
+ VITE_DEPLOYMENT_DISPLAY_NAME: "ThinkWork",
4403
+ VITE_DEPLOYMENT_PROFILE_ISSUED_AT: values.issuedAt,
4404
+ VITE_SPACES_URL: values.appUrl,
4405
+ VITE_STAGE: values.stage,
4406
+ VITE_AWS_REGION: values.region,
4407
+ VITE_AWS_ACCOUNT_ID: values.accountId,
4408
+ VITE_RELEASE_VERSION: values.releaseVersion ?? ""
4409
+ }
4410
+ };
4411
+ }
4412
+ async function publishRuntimeConfig(cwd, bucket, identity, stage, releaseVersion) {
4413
+ const output = async (key) => {
4414
+ try {
4415
+ return await terraformOutput(cwd, key);
4416
+ } catch {
4417
+ return "";
4418
+ }
4419
+ };
4420
+ const config = buildRuntimeConfig({
4421
+ stage,
4422
+ region: identity.region,
4423
+ accountId: identity.account,
4424
+ releaseVersion,
4425
+ apiEndpoint: await output("api_endpoint"),
4426
+ appUrl: await output("app_url"),
4427
+ authDomain: await output("auth_domain"),
4428
+ appsyncUrl: await output("appsync_api_url"),
4429
+ appsyncRealtimeUrl: await output("appsync_realtime_url"),
4430
+ appsyncApiKey: await output("appsync_api_key"),
4431
+ userPoolId: await output("user_pool_id"),
4432
+ adminClientId: await output("admin_client_id") || await output("admin_client_id_out"),
4433
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString()
4434
+ });
4435
+ const tempDir = mkdtempSync5(pathJoinTmp("thinkwork-runtime-config-"));
4436
+ const file = join9(tempDir, "thinkwork-runtime-config.json");
4437
+ writeFileSync7(file, JSON.stringify(config, null, 2) + "\n");
4438
+ const put = spawnSync6(
4439
+ "aws",
4440
+ [
4441
+ "s3",
4442
+ "cp",
4443
+ file,
4444
+ `s3://${bucket}/thinkwork-runtime-config.json`,
4445
+ "--content-type",
4446
+ "application/json",
4447
+ "--cache-control",
4448
+ "no-store",
4449
+ "--region",
4450
+ identity.region
4451
+ ],
4452
+ { encoding: "utf8" }
4453
+ );
4454
+ if (put.status !== 0) {
4455
+ throw new Error(
4456
+ `Could not publish runtime config: ${(put.stderr ?? "").trim().slice(0, 200)}`
4457
+ );
4458
+ }
4459
+ printSuccess(`Runtime config published to s3://${bucket}/thinkwork-runtime-config.json`);
4460
+ const distributionId = await output("app_distribution_id");
4461
+ if (distributionId) {
4462
+ spawnSync6(
4463
+ "aws",
4464
+ [
4465
+ "cloudfront",
4466
+ "create-invalidation",
4467
+ "--distribution-id",
4468
+ distributionId,
4469
+ "--paths",
4470
+ "/thinkwork-runtime-config.json",
4471
+ "/index.html"
4472
+ ],
4473
+ { encoding: "utf8" }
4474
+ );
4475
+ }
4476
+ }
4477
+ async function ensureOwnerUser(cwd, region) {
4478
+ const email = (readTfvarsSignalsRaw(cwd).platform_operator_emails ?? "").split(",")[0]?.trim();
4479
+ if (!email) return null;
4480
+ const userPoolId = await terraformOutput(cwd, "user_pool_id").catch(() => "");
4481
+ if (!userPoolId) return null;
4482
+ const exists = spawnSync6(
4483
+ "aws",
4484
+ [
4485
+ "cognito-idp",
4486
+ "admin-get-user",
4487
+ "--user-pool-id",
4488
+ userPoolId,
4489
+ "--username",
4490
+ email,
4491
+ "--region",
4492
+ region
4493
+ ],
4494
+ { encoding: "utf8" }
4495
+ );
4496
+ if (exists.status === 0) return { email, tempPassword: null };
4497
+ const tempPassword = `Tw1!${randomBytes4(12).toString("base64url")}`;
4498
+ const created = spawnSync6(
4499
+ "aws",
4500
+ [
4501
+ "cognito-idp",
4502
+ "admin-create-user",
4503
+ "--user-pool-id",
4504
+ userPoolId,
4505
+ "--username",
4506
+ email,
4507
+ "--user-attributes",
4508
+ `Name=email,Value=${email}`,
4509
+ "Name=email_verified,Value=true",
4510
+ "--temporary-password",
4511
+ tempPassword,
4512
+ "--message-action",
4513
+ "SUPPRESS",
4514
+ "--region",
4515
+ region
4516
+ ],
4517
+ { encoding: "utf8" }
4518
+ );
4519
+ if (created.status !== 0) {
4520
+ printWarning(
4521
+ `Could not create the owner user ${email}: ${(created.stderr ?? "").trim().slice(0, 200)}`
4522
+ );
4523
+ return null;
4524
+ }
4525
+ return { email, tempPassword };
4526
+ }
4190
4527
  async function publishWebAssets(cwd, webAssetSource) {
4191
4528
  const bucket = await terraformOutput(cwd, "app_bucket_name");
4192
4529
  if (!bucket) {
@@ -4247,6 +4584,20 @@ async function applySchemaMigrations(cwd, identity, stage) {
4247
4584
  );
4248
4585
  return;
4249
4586
  }
4587
+ const connection = await resolveStageDbConnection(cwd, identity, stage);
4588
+ console.log("\n Applying database schema (full migration history)...");
4589
+ const summary = await applyMigrations({
4590
+ drizzleDir,
4591
+ stage,
4592
+ region: identity.region,
4593
+ connection,
4594
+ log: (line) => console.log(` ${line}`)
4595
+ });
4596
+ console.log(
4597
+ ` Schema: ${summary.applied.length} migration(s) applied, ${summary.skipped} already present` + (summary.skippedFiles.length > 0 ? `, ${summary.skippedFiles.length} operator-only file(s) skipped` : "") + "."
4598
+ );
4599
+ }
4600
+ async function resolveStageDbConnection(cwd, identity, stage) {
4250
4601
  const endpoint = await terraformOutput(cwd, "db_cluster_endpoint");
4251
4602
  if (!endpoint) {
4252
4603
  throw new Error(
@@ -4280,23 +4631,13 @@ async function applySchemaMigrations(cwd, identity, stage) {
4280
4631
  `Secret thinkwork-${stage}-db-credentials is missing username/password.`
4281
4632
  );
4282
4633
  }
4283
- console.log("\n Applying database schema (full migration history)...");
4284
- const summary = await applyMigrations({
4285
- drizzleDir,
4286
- stage,
4287
- region: identity.region,
4288
- connection: {
4289
- host: endpoint,
4290
- port: 5432,
4291
- user: parsed.username,
4292
- password: parsed.password,
4293
- database: "thinkwork"
4294
- },
4295
- log: (line) => console.log(` ${line}`)
4296
- });
4297
- console.log(
4298
- ` Schema: ${summary.applied.length} migration(s) applied, ${summary.skipped} already present` + (summary.skippedFiles.length > 0 ? `, ${summary.skippedFiles.length} operator-only file(s) skipped` : "") + "."
4299
- );
4634
+ return {
4635
+ host: endpoint,
4636
+ port: 5432,
4637
+ user: parsed.username,
4638
+ password: parsed.password,
4639
+ database: "thinkwork"
4640
+ };
4300
4641
  }
4301
4642
  function resolveBedrockLoggingPin(assignments, logGroupExists) {
4302
4643
  if (assignments.manage_bedrock_invocation_logging !== void 0) return null;
@@ -4396,7 +4737,10 @@ async function runLocalTerraformDeploy(opts) {
4396
4737
  );
4397
4738
  }
4398
4739
  }
4399
- const terraformDir = resolveTerraformRoot();
4740
+ const terraformDir = resolveTerraformRootForStage(
4741
+ stage,
4742
+ loadEnvironment(stage)?.terraformDir
4743
+ );
4400
4744
  const tiers = expandComponent(opts.component);
4401
4745
  const cwd0 = resolveTierDir(terraformDir, stage, tiers[0]);
4402
4746
  const scaffolded = isInitScaffoldedLayout(cwd0);
@@ -4408,7 +4752,8 @@ async function runLocalTerraformDeploy(opts) {
4408
4752
  const ctx = {
4409
4753
  backend: caller && scaffolded ? backendTarget(caller.account, caller.region, stage) : void 0,
4410
4754
  domain: signals.domain,
4411
- sesConfigured: signals.sesConfigured
4755
+ sesConfigured: signals.sesConfigured,
4756
+ agentcorePiSourceImage: readTfvarsSignalsRaw(preflightCwd).agentcore_pi_source_image_uri || void 0
4412
4757
  };
4413
4758
  console.log("\n Preflight checks:");
4414
4759
  const summary = await runChecks(preflightChecks(ctx));
@@ -4432,6 +4777,7 @@ async function runLocalTerraformDeploy(opts) {
4432
4777
  printWarning("Preflight skipped (--skip-preflight).");
4433
4778
  }
4434
4779
  let webAssetSource = null;
4780
+ let releaseVersionPin = null;
4435
4781
  if (scaffolded && caller) {
4436
4782
  ensureBedrockLoggingPin(cwd0, caller.region);
4437
4783
  const release = await ensureReleaseArtifacts(
@@ -4441,6 +4787,7 @@ async function runLocalTerraformDeploy(opts) {
4441
4787
  opts.releaseVersion
4442
4788
  );
4443
4789
  webAssetSource = release.webAssetSource;
4790
+ releaseVersionPin = release.version;
4444
4791
  }
4445
4792
  for (let i = 0; i < tiers.length; i++) {
4446
4793
  const tier = tiers[i];
@@ -4486,9 +4833,45 @@ async function runLocalTerraformDeploy(opts) {
4486
4833
  if (scaffolded && caller) {
4487
4834
  await applySchemaMigrations(cwd0, caller, stage);
4488
4835
  }
4836
+ let ownerUser = null;
4837
+ if (scaffolded && caller) {
4838
+ const operatorEmail = (readTfvarsSignalsRaw(cwd0).platform_operator_emails ?? "").split(",")[0]?.trim();
4839
+ if (operatorEmail) {
4840
+ try {
4841
+ const connection = await resolveStageDbConnection(cwd0, caller, stage);
4842
+ const ownerTenant = await ensureOwnerTenant({
4843
+ stage,
4844
+ email: operatorEmail,
4845
+ connection
4846
+ });
4847
+ if (ownerTenant.created) {
4848
+ printSuccess(
4849
+ `Owner tenant "${ownerTenant.slug}" pre-provisioned \u2014 first sign-in by ${ownerTenant.email} claims it.`
4850
+ );
4851
+ }
4852
+ } catch (err) {
4853
+ printWarning(
4854
+ `Could not pre-provision the owner tenant: ${err instanceof Error ? err.message : String(err)}. First sign-in will show "No tenant assigned" until this is resolved \u2014 rerun the deploy.`
4855
+ );
4856
+ }
4857
+ }
4858
+ ownerUser = await ensureOwnerUser(cwd0, caller.region);
4859
+ }
4489
4860
  if (scaffolded && webAssetSource) {
4490
4861
  await publishWebAssets(cwd0, webAssetSource);
4491
4862
  }
4863
+ if (scaffolded && caller) {
4864
+ const bucket = await terraformOutput(cwd0, "app_bucket_name");
4865
+ if (bucket) {
4866
+ await publishRuntimeConfig(
4867
+ cwd0,
4868
+ bucket,
4869
+ caller,
4870
+ stage,
4871
+ releaseVersionPin ?? null
4872
+ );
4873
+ }
4874
+ }
4492
4875
  if (scaffolded && caller) {
4493
4876
  console.log("\n Seeding workspace defaults...");
4494
4877
  await runWorkspaceBootstrap(cwd0, stage, caller.region);
@@ -4506,6 +4889,7 @@ async function runLocalTerraformDeploy(opts) {
4506
4889
  )
4507
4890
  });
4508
4891
  if (!verification.passed) {
4892
+ printOwnerCredentials(ownerUser);
4509
4893
  printError(
4510
4894
  `Deploy applied but the stack failed verification (${verification.failures.length} probe(s)). Fix the items above and rerun \`thinkwork deploy -s ${stage}\` \u2014 reruns converge.`
4511
4895
  );
@@ -4515,6 +4899,19 @@ async function runLocalTerraformDeploy(opts) {
4515
4899
  printSuccess("Deploy complete");
4516
4900
  await runPostDeployProbe(stage);
4517
4901
  printSummary("deploy", stage, tiers, startTime);
4902
+ printOwnerCredentials(ownerUser);
4903
+ }
4904
+ function printOwnerCredentials(ownerUser) {
4905
+ if (ownerUser?.tempPassword) {
4906
+ console.log("");
4907
+ printSuccess(`Owner user created: ${ownerUser.email}`);
4908
+ console.log(
4909
+ ` Temporary password (shown ONCE \u2014 Cognito requires a change at first sign-in):`
4910
+ );
4911
+ console.log(` ${ownerUser.tempPassword}`);
4912
+ } else if (ownerUser) {
4913
+ console.log(` Owner user ${ownerUser.email} already exists.`);
4914
+ }
4518
4915
  }
4519
4916
  async function offerStaleLockRecovery(cwd, stage, tier, lock) {
4520
4917
  console.log("");
@@ -5066,7 +5463,10 @@ async function runLocalTerraformDestroy(opts) {
5066
5463
  process.exit(0);
5067
5464
  }
5068
5465
  }
5069
- const terraformDir = resolveTerraformRoot();
5466
+ const terraformDir = resolveTerraformRootForStage(
5467
+ stage,
5468
+ localEnv?.terraformDir
5469
+ );
5070
5470
  const tiers = expandComponent(opts.component).reverse();
5071
5471
  const preRegion = identity && identity.region !== "unknown" ? identity.region : "us-east-1";
5072
5472
  const cluster = disableClusterDeletionProtection(stage, preRegion);
@@ -6046,7 +6446,7 @@ function discoverCognitoConfig(stage, region) {
6046
6446
  import {
6047
6447
  createServer
6048
6448
  } from "http";
6049
- import { randomBytes as randomBytes3 } from "crypto";
6449
+ import { randomBytes as randomBytes5 } from "crypto";
6050
6450
  import { spawn as spawn3 } from "child_process";
6051
6451
  import chalk6 from "chalk";
6052
6452
  var CLI_LOOPBACK_PORT = 42010;
@@ -6056,7 +6456,7 @@ async function loginWithCognito(opts) {
6056
6456
  const port = opts.port ?? CLI_LOOPBACK_PORT;
6057
6457
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
6058
6458
  const redirectUri = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
6059
- const state = randomBytes3(16).toString("hex");
6459
+ const state = randomBytes5(16).toString("hex");
6060
6460
  const authorizeUrl = buildAuthorizeUrl(opts.cognito, redirectUri, state);
6061
6461
  const code = await waitForCallbackCode({
6062
6462
  port,
@@ -7044,8 +7444,10 @@ function findBundledTerraform() {
7044
7444
  function parseTfvarsAssignments(content) {
7045
7445
  const values = {};
7046
7446
  for (const line of content.split("\n")) {
7047
- const match = line.match(/^\s*([a-zA-Z0-9_]+)\s*=\s*"([^"]*)"/);
7048
- if (match) values[match[1]] = match[2];
7447
+ const match = line.match(
7448
+ /^\s*([a-zA-Z0-9_]+)\s*=\s*(?:"([^"]*)"|(true|false|[0-9]+))\s*$/
7449
+ );
7450
+ if (match) values[match[1]] = match[2] ?? match[3];
7049
7451
  }
7050
7452
  const hasContent = content.split("\n").some((l) => l.trim() && !l.trim().startsWith("#"));
7051
7453
  if (hasContent && !values.stage) {
@@ -7127,6 +7529,25 @@ function buildTfvars(config) {
7127
7529
  `customer_domain_delegated = ${config.customer_domain_delegated === "true"}`
7128
7530
  );
7129
7531
  }
7532
+ if (config.stage === "prod" || config.stage === "production") {
7533
+ lines.push(``);
7534
+ lines.push(
7535
+ `# \u2500\u2500 Compliance (prod requirement) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`
7536
+ );
7537
+ lines.push(
7538
+ `# Object Lock COMPLIANCE is REQUIRED for prod-named stages (terraform`
7539
+ );
7540
+ lines.push(
7541
+ `# precondition). Retention is irreversible until it expires \u2014 even AWS`
7542
+ );
7543
+ lines.push(
7544
+ `# root cannot delete anchors early. Raise retention for audit posture.`
7545
+ );
7546
+ lines.push(`compliance_anchor_object_lock_mode = "COMPLIANCE"`);
7547
+ lines.push(
7548
+ `compliance_anchor_retention_days = ${config.compliance_anchor_retention_days || "30"}`
7549
+ );
7550
+ }
7130
7551
  if (config.platform_operator_emails) {
7131
7552
  lines.push(``);
7132
7553
  lines.push(
@@ -7278,7 +7699,7 @@ function registerInitCommand(program2) {
7278
7699
  config.admin_url = "http://localhost:5174";
7279
7700
  config.mobile_scheme = "thinkwork";
7280
7701
  config.customer_domain = existing?.customer_domain ?? "";
7281
- config.customer_domain_delegated = "false";
7702
+ config.customer_domain_delegated = existing?.customer_domain_delegated ?? "false";
7282
7703
  config.platform_operator_emails = existing?.platform_operator_emails ?? "";
7283
7704
  config.ses_parent_domain = existing?.ses_parent_domain ?? "";
7284
7705
  } else {
@@ -7312,7 +7733,7 @@ function registerInitCommand(program2) {
7312
7733
  "Domain (e.g. thinkwork.acme.com; empty to skip)",
7313
7734
  existing?.customer_domain ?? ""
7314
7735
  );
7315
- config.customer_domain_delegated = "false";
7736
+ config.customer_domain_delegated = existing?.customer_domain_delegated ?? "false";
7316
7737
  config.platform_operator_emails = await ask2(
7317
7738
  "Operator email(s), comma-separated",
7318
7739
  existing?.platform_operator_emails ?? ""
@@ -7420,10 +7841,24 @@ function registerInitCommand(program2) {
7420
7841
  if (existsSync13(schemaPath) && !existsSync13(join12(tfDir, "schema.graphql"))) {
7421
7842
  cpSync(schemaPath, join12(tfDir, "schema.graphql"));
7422
7843
  }
7423
- const tfvars = buildTfvars(config);
7844
+ let tfvars = buildTfvars(config);
7845
+ const generatedKeys = new Set(
7846
+ [...tfvars.matchAll(/^\s*([a-zA-Z0-9_]+)\s*=/gm)].map((m) => m[1])
7847
+ );
7848
+ const preserved = Object.entries(existing ?? {}).filter(
7849
+ ([key]) => !generatedKeys.has(key)
7850
+ );
7851
+ if (preserved.length > 0) {
7852
+ tfvars += `
7853
+ # \u2500\u2500 Preserved from previous configuration (init rerun) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7854
+ ` + preserved.map(
7855
+ ([key, value]) => /^(true|false|[0-9]+)$/.test(value) ? `${key} = ${value}` : `${key} = "${value}"`
7856
+ ).join("\n") + `
7857
+ `;
7858
+ }
7424
7859
  writeFileSync9(tfvarsPath, tfvars);
7425
7860
  const mainTfPath = join12(tfDir, "main.tf");
7426
- if (!existsSync13(mainTfPath)) {
7861
+ {
7427
7862
  writeFileSync9(
7428
7863
  mainTfPath,
7429
7864
  `################################################################################
@@ -7779,6 +8214,20 @@ variable "agentcore_pi_source_image_uri" {
7779
8214
  default = ""
7780
8215
  }
7781
8216
 
8217
+ # Object Lock posture for the compliance anchor bucket. Stages named
8218
+ # prod/production REQUIRE "COMPLIANCE" (terraform precondition) \u2014 set
8219
+ # automatically by init for those stage names. COMPLIANCE retention is
8220
+ # irreversible until it expires (even AWS root cannot delete early).
8221
+ variable "compliance_anchor_object_lock_mode" {
8222
+ type = string
8223
+ default = "GOVERNANCE"
8224
+ }
8225
+
8226
+ variable "compliance_anchor_retention_days" {
8227
+ type = number
8228
+ default = 365
8229
+ }
8230
+
7782
8231
  # Pinned by \`thinkwork deploy\`: true only when this stage is the first
7783
8232
  # ThinkWork stack in the account+region (the Bedrock invocation-logging
7784
8233
  # resources are account-scoped singletons).
@@ -7814,6 +8263,9 @@ module "thinkwork" {
7814
8263
  skill_trust_runner_enabled = false
7815
8264
  manage_bedrock_invocation_logging = var.manage_bedrock_invocation_logging
7816
8265
 
8266
+ compliance_anchor_object_lock_mode = var.compliance_anchor_object_lock_mode
8267
+ compliance_anchor_retention_days = var.compliance_anchor_retention_days
8268
+
7817
8269
  db_password = var.db_password
7818
8270
  database_engine = var.database_engine
7819
8271
  enable_hindsight = var.enable_hindsight
@@ -7870,6 +8322,38 @@ output "api_endpoint" {
7870
8322
  value = module.thinkwork.api_endpoint
7871
8323
  }
7872
8324
 
8325
+ # Outputs consumed by \`thinkwork deploy\`'s web runtime-config generation \u2014
8326
+ # the web app fetches /thinkwork-runtime-config.json at boot; without these
8327
+ # the published bundle renders "Sign-in options are unavailable".
8328
+ output "app_url" {
8329
+ value = module.thinkwork.app_url
8330
+ }
8331
+
8332
+ output "auth_domain" {
8333
+ value = module.thinkwork.auth_domain
8334
+ }
8335
+
8336
+ output "appsync_api_url" {
8337
+ value = module.thinkwork.appsync_api_url
8338
+ }
8339
+
8340
+ output "appsync_realtime_url" {
8341
+ value = module.thinkwork.appsync_realtime_url
8342
+ }
8343
+
8344
+ output "appsync_api_key" {
8345
+ value = module.thinkwork.appsync_api_key
8346
+ sensitive = true
8347
+ }
8348
+
8349
+ output "admin_client_id_out" {
8350
+ value = module.thinkwork.admin_client_id
8351
+ }
8352
+
8353
+ output "app_distribution_id" {
8354
+ value = module.thinkwork.app_distribution_id
8355
+ }
8356
+
7873
8357
  output "app_bucket_name" {
7874
8358
  value = module.thinkwork.app_bucket_name
7875
8359
  }
@@ -10609,6 +11093,182 @@ var CliDeleteBudgetPolicyDocument = {
10609
11093
  }
10610
11094
  ]
10611
11095
  };
11096
+ var CliCapabilityInspectorDocument = {
11097
+ kind: "Document",
11098
+ definitions: [
11099
+ {
11100
+ kind: "OperationDefinition",
11101
+ operation: "query",
11102
+ name: { kind: "Name", value: "CliCapabilityInspector" },
11103
+ variableDefinitions: [
11104
+ {
11105
+ kind: "VariableDefinition",
11106
+ variable: {
11107
+ kind: "Variable",
11108
+ name: { kind: "Name", value: "tenantId" }
11109
+ },
11110
+ type: {
11111
+ kind: "NonNullType",
11112
+ type: { kind: "NamedType", name: { kind: "Name", value: "ID" } }
11113
+ }
11114
+ },
11115
+ {
11116
+ kind: "VariableDefinition",
11117
+ variable: {
11118
+ kind: "Variable",
11119
+ name: { kind: "Name", value: "agentId" }
11120
+ },
11121
+ type: { kind: "NamedType", name: { kind: "Name", value: "ID" } }
11122
+ },
11123
+ {
11124
+ kind: "VariableDefinition",
11125
+ variable: {
11126
+ kind: "Variable",
11127
+ name: { kind: "Name", value: "spaceId" }
11128
+ },
11129
+ type: { kind: "NamedType", name: { kind: "Name", value: "ID" } }
11130
+ },
11131
+ {
11132
+ kind: "VariableDefinition",
11133
+ variable: {
11134
+ kind: "Variable",
11135
+ name: { kind: "Name", value: "agentProfileId" }
11136
+ },
11137
+ type: { kind: "NamedType", name: { kind: "Name", value: "ID" } }
11138
+ },
11139
+ {
11140
+ kind: "VariableDefinition",
11141
+ variable: {
11142
+ kind: "Variable",
11143
+ name: { kind: "Name", value: "perspectiveUserId" }
11144
+ },
11145
+ type: { kind: "NamedType", name: { kind: "Name", value: "ID" } }
11146
+ }
11147
+ ],
11148
+ selectionSet: {
11149
+ kind: "SelectionSet",
11150
+ selections: [
11151
+ {
11152
+ kind: "Field",
11153
+ name: { kind: "Name", value: "capabilityInspector" },
11154
+ arguments: [
11155
+ {
11156
+ kind: "Argument",
11157
+ name: { kind: "Name", value: "tenantId" },
11158
+ value: {
11159
+ kind: "Variable",
11160
+ name: { kind: "Name", value: "tenantId" }
11161
+ }
11162
+ },
11163
+ {
11164
+ kind: "Argument",
11165
+ name: { kind: "Name", value: "agentId" },
11166
+ value: {
11167
+ kind: "Variable",
11168
+ name: { kind: "Name", value: "agentId" }
11169
+ }
11170
+ },
11171
+ {
11172
+ kind: "Argument",
11173
+ name: { kind: "Name", value: "spaceId" },
11174
+ value: {
11175
+ kind: "Variable",
11176
+ name: { kind: "Name", value: "spaceId" }
11177
+ }
11178
+ },
11179
+ {
11180
+ kind: "Argument",
11181
+ name: { kind: "Name", value: "agentProfileId" },
11182
+ value: {
11183
+ kind: "Variable",
11184
+ name: { kind: "Name", value: "agentProfileId" }
11185
+ }
11186
+ },
11187
+ {
11188
+ kind: "Argument",
11189
+ name: { kind: "Name", value: "perspectiveUserId" },
11190
+ value: {
11191
+ kind: "Variable",
11192
+ name: { kind: "Name", value: "perspectiveUserId" }
11193
+ }
11194
+ }
11195
+ ],
11196
+ selectionSet: {
11197
+ kind: "SelectionSet",
11198
+ selections: [
11199
+ { kind: "Field", name: { kind: "Name", value: "state" } },
11200
+ { kind: "Field", name: { kind: "Name", value: "stateDetail" } },
11201
+ { kind: "Field", name: { kind: "Name", value: "agentId" } },
11202
+ {
11203
+ kind: "Field",
11204
+ name: { kind: "Name", value: "noUserBaseline" }
11205
+ },
11206
+ {
11207
+ kind: "Field",
11208
+ name: { kind: "Name", value: "predicted" },
11209
+ selectionSet: {
11210
+ kind: "SelectionSet",
11211
+ selections: [
11212
+ {
11213
+ kind: "Field",
11214
+ name: { kind: "Name", value: "computedAt" }
11215
+ },
11216
+ {
11217
+ kind: "Field",
11218
+ name: { kind: "Name", value: "configFingerprint" }
11219
+ },
11220
+ {
11221
+ kind: "Field",
11222
+ name: { kind: "Name", value: "items" },
11223
+ selectionSet: {
11224
+ kind: "SelectionSet",
11225
+ selections: [
11226
+ {
11227
+ kind: "Field",
11228
+ name: { kind: "Name", value: "capabilityClass" }
11229
+ },
11230
+ {
11231
+ kind: "Field",
11232
+ name: { kind: "Name", value: "capabilityId" }
11233
+ },
11234
+ {
11235
+ kind: "Field",
11236
+ name: { kind: "Name", value: "displayName" }
11237
+ },
11238
+ {
11239
+ kind: "Field",
11240
+ name: { kind: "Name", value: "active" }
11241
+ },
11242
+ {
11243
+ kind: "Field",
11244
+ name: { kind: "Name", value: "provenance" }
11245
+ },
11246
+ {
11247
+ kind: "Field",
11248
+ name: { kind: "Name", value: "reason" }
11249
+ },
11250
+ {
11251
+ kind: "Field",
11252
+ name: { kind: "Name", value: "detail" }
11253
+ },
11254
+ {
11255
+ kind: "Field",
11256
+ name: { kind: "Name", value: "tokenStatus" }
11257
+ }
11258
+ ]
11259
+ }
11260
+ }
11261
+ ]
11262
+ }
11263
+ }
11264
+ ]
11265
+ }
11266
+ }
11267
+ ]
11268
+ }
11269
+ }
11270
+ ]
11271
+ };
10612
11272
  var CliCostSummaryDocument = {
10613
11273
  kind: "Document",
10614
11274
  definitions: [
@@ -19937,6 +20597,7 @@ var documents = {
19937
20597
  "\n query CliBudgetStatus($tenantId: ID!) {\n budgetStatus(tenantId: $tenantId) {\n policy {\n id\n scope\n agentId\n userId\n period\n limitUsd\n }\n spentUsd\n remainingUsd\n percentUsed\n status\n }\n }\n": CliBudgetStatusDocument,
19938
20598
  "\n mutation CliUpsertBudgetPolicy(\n $tenantId: ID!\n $input: UpsertBudgetPolicyInput!\n ) {\n upsertBudgetPolicy(tenantId: $tenantId, input: $input) {\n id\n scope\n agentId\n userId\n limitUsd\n period\n actionOnExceed\n }\n }\n": CliUpsertBudgetPolicyDocument,
19939
20599
  "\n mutation CliDeleteBudgetPolicy($id: ID!) {\n deleteBudgetPolicy(id: $id)\n }\n": CliDeleteBudgetPolicyDocument,
20600
+ "\n query CliCapabilityInspector(\n $tenantId: ID!\n $agentId: ID\n $spaceId: ID\n $agentProfileId: ID\n $perspectiveUserId: ID\n ) {\n capabilityInspector(\n tenantId: $tenantId\n agentId: $agentId\n spaceId: $spaceId\n agentProfileId: $agentProfileId\n perspectiveUserId: $perspectiveUserId\n ) {\n state\n stateDetail\n agentId\n noUserBaseline\n predicted {\n computedAt\n configFingerprint\n items {\n capabilityClass\n capabilityId\n displayName\n active\n provenance\n reason\n detail\n tokenStatus\n }\n }\n }\n }\n": CliCapabilityInspectorDocument,
19940
20601
  "\n query CliCostSummary($tenantId: ID!, $from: AWSDateTime, $to: AWSDateTime) {\n costSummary(tenantId: $tenantId, from: $from, to: $to) {\n totalUsd\n llmUsd\n computeUsd\n toolsUsd\n evalUsd\n totalInputTokens\n totalOutputTokens\n eventCount\n }\n }\n": CliCostSummaryDocument,
19941
20602
  "\n query CliCostByAgent($tenantId: ID!, $from: AWSDateTime, $to: AWSDateTime) {\n costByAgent(tenantId: $tenantId, from: $from, to: $to) {\n agentId\n agentName\n totalUsd\n eventCount\n }\n }\n": CliCostByAgentDocument,
19942
20603
  "\n query CliCostByUser($tenantId: ID!, $from: AWSDateTime, $to: AWSDateTime) {\n costByUser(tenantId: $tenantId, from: $from, to: $to) {\n userId\n userName\n userEmail\n totalUsd\n eventCount\n isSystem\n }\n }\n": CliCostByUserDocument,
@@ -25107,7 +25768,7 @@ async function runWebhookTest(id, _opts) {
25107
25768
  try {
25108
25769
  const tokenData = await gqlQuery(ctx.client, WebhookForTestDoc, { id });
25109
25770
  if (tokenData.webhook?.token) {
25110
- const { resolveApiConfig: resolveApiConfig2 } = await import("./api-client-4VT3FWOH.js");
25771
+ const { resolveApiConfig: resolveApiConfig2 } = await import("./api-client-JUBCQQDF.js");
25111
25772
  const api = resolveApiConfig2(ctx.stage);
25112
25773
  if (api?.apiUrl) {
25113
25774
  const base = api.apiUrl.replace(/\/$/, "");
@@ -26789,6 +27450,112 @@ function registerTraceCommand(program2) {
26789
27450
  trace.command("turn <turnId>").description("Per-invocation logs for a single thread turn.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(runTraceTurn);
26790
27451
  }
26791
27452
 
27453
+ // src/commands/capabilities.ts
27454
+ var CapabilityInspectorDoc = graphql(`
27455
+ query CliCapabilityInspector(
27456
+ $tenantId: ID!
27457
+ $agentId: ID
27458
+ $spaceId: ID
27459
+ $agentProfileId: ID
27460
+ $perspectiveUserId: ID
27461
+ ) {
27462
+ capabilityInspector(
27463
+ tenantId: $tenantId
27464
+ agentId: $agentId
27465
+ spaceId: $spaceId
27466
+ agentProfileId: $agentProfileId
27467
+ perspectiveUserId: $perspectiveUserId
27468
+ ) {
27469
+ state
27470
+ stateDetail
27471
+ agentId
27472
+ noUserBaseline
27473
+ predicted {
27474
+ computedAt
27475
+ configFingerprint
27476
+ items {
27477
+ capabilityClass
27478
+ capabilityId
27479
+ displayName
27480
+ active
27481
+ provenance
27482
+ reason
27483
+ detail
27484
+ tokenStatus
27485
+ }
27486
+ }
27487
+ }
27488
+ }
27489
+ `);
27490
+ async function runCapabilities(opts) {
27491
+ if (opts.json) setJsonMode(true);
27492
+ const ctx = await resolveTenantContext(opts);
27493
+ const data = await gqlQuery(ctx.client, CapabilityInspectorDoc, {
27494
+ tenantId: ctx.tenantId,
27495
+ agentId: opts.agent ?? null,
27496
+ spaceId: opts.space ?? null,
27497
+ agentProfileId: opts.profile ?? null,
27498
+ perspectiveUserId: opts.user ?? null
27499
+ });
27500
+ const inspection = data.capabilityInspector;
27501
+ if (isJsonMode()) {
27502
+ printJson(inspection);
27503
+ if (inspection.state !== "ok") process.exitCode = 1;
27504
+ return;
27505
+ }
27506
+ if (inspection.state === "invalid_selection") {
27507
+ printError(`Invalid selection: ${inspection.stateDetail}`);
27508
+ process.exitCode = 1;
27509
+ return;
27510
+ }
27511
+ if (inspection.state === "resolution_fault") {
27512
+ printError(`Resolution fault: ${inspection.stateDetail}`);
27513
+ process.exitCode = 1;
27514
+ return;
27515
+ }
27516
+ const predicted = inspection.predicted;
27517
+ if (!predicted) {
27518
+ printError("Inspector returned no capability set.");
27519
+ process.exitCode = 1;
27520
+ return;
27521
+ }
27522
+ if (inspection.noUserBaseline) {
27523
+ console.log(
27524
+ "No-user baseline (what a scheduled/wakeup turn gets). Pass --user <id> for a user's perspective.\n"
27525
+ );
27526
+ }
27527
+ printTable(
27528
+ predicted.items.map((item) => ({
27529
+ class: item.capabilityClass,
27530
+ id: item.displayName || item.capabilityId,
27531
+ state: item.active ? "active" : item.reason ?? "inactive",
27532
+ token: item.tokenStatus ?? "\u2014",
27533
+ provenance: item.provenance ?? "\u2014",
27534
+ detail: item.detail ?? "\u2014"
27535
+ })),
27536
+ [
27537
+ { key: "class", header: "CLASS" },
27538
+ { key: "id", header: "CAPABILITY" },
27539
+ { key: "state", header: "STATE" },
27540
+ { key: "token", header: "TOKEN" },
27541
+ { key: "provenance", header: "PROVENANCE" },
27542
+ { key: "detail", header: "DETAIL" }
27543
+ ]
27544
+ );
27545
+ console.log(
27546
+ `
27547
+ Computed ${predicted.computedAt} \xB7 fingerprint ${predicted.configFingerprint.slice(0, 12)}`
27548
+ );
27549
+ }
27550
+ function registerCapabilitiesCommand(program2) {
27551
+ program2.command("capabilities").description(
27552
+ "Effective capability set for an agent context \u2014 every skill, tool, MCP server, extension, and plugin with its state and gate reason."
27553
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <agentId>", "Agent id (defaults to the platform agent)").option("--space <spaceId>", "Space id").option("--profile <agentProfileId>", "Agent Profile id").option(
27554
+ "--user <userId>",
27555
+ "Perspective user id (omit for the no-invoker baseline)"
27556
+ ).option("--json", "Raw JSON output").action(runCapabilities);
27557
+ }
27558
+
26792
27559
  // src/commands/dashboard.ts
26793
27560
  var DashboardDoc = graphql(`
26794
27561
  query CliDashboard($tenantId: ID!) {
@@ -30216,6 +30983,7 @@ registerCostCommand(program);
30216
30983
  registerBudgetCommand(program);
30217
30984
  registerPerformanceCommand(program);
30218
30985
  registerTraceCommand(program);
30986
+ registerCapabilitiesCommand(program);
30219
30987
  registerDashboardCommand(program);
30220
30988
  registerEvalCommand(program);
30221
30989
  registerEnterpriseCommand(program);
@@ -0,0 +1,28 @@
1
+ -- Resolved capability manifest context columns (capability-mapping plan U11).
2
+ -- Additive: turn/context identity + config fingerprint on the append-only
3
+ -- manifest audit table, plus the covering index for "newest manifest matching
4
+ -- context" retrieval (U13) and the retention sweep's tenant+created_at range.
5
+ -- Plain uuids, deliberately NOT foreign keys — matching template_id/user_id on
6
+ -- the same table: audit rows must survive deletion of their source rows.
7
+ -- creates-column: public.resolved_capability_manifests.thread_id
8
+ -- creates-column: public.resolved_capability_manifests.thread_turn_id
9
+ -- creates-column: public.resolved_capability_manifests.space_id
10
+ -- creates-column: public.resolved_capability_manifests.agent_profile_id
11
+ -- creates-column: public.resolved_capability_manifests.config_fingerprint
12
+ -- creates: public.idx_rcm_context
13
+
14
+ ALTER TABLE public.resolved_capability_manifests
15
+ ADD COLUMN IF NOT EXISTS thread_id uuid,
16
+ ADD COLUMN IF NOT EXISTS thread_turn_id uuid,
17
+ ADD COLUMN IF NOT EXISTS space_id uuid,
18
+ ADD COLUMN IF NOT EXISTS agent_profile_id uuid,
19
+ ADD COLUMN IF NOT EXISTS config_fingerprint text;
20
+
21
+ CREATE INDEX IF NOT EXISTS idx_rcm_context
22
+ ON public.resolved_capability_manifests (
23
+ tenant_id,
24
+ agent_id,
25
+ space_id,
26
+ agent_profile_id,
27
+ created_at
28
+ );
@@ -24,6 +24,8 @@ locals {
24
24
  chat_agent_finalize_fn_arn = "arn:aws:lambda:${var.region}:${var.account_id}:function:${local.chat_agent_finalize_fn_name}"
25
25
  chat_agent_activity_fn_name = "thinkwork-${var.stage}-api-chat-agent-activity"
26
26
  chat_agent_activity_fn_arn = "arn:aws:lambda:${var.region}:${var.account_id}:function:${local.chat_agent_activity_fn_name}"
27
+ manifest_log_fn_name = "thinkwork-${var.stage}-api-manifest-log"
28
+ manifest_log_fn_arn = "arn:aws:lambda:${var.region}:${var.account_id}:function:${local.manifest_log_fn_name}"
27
29
  pi_image_uri = "${var.ecr_repository_url}:pi-latest"
28
30
  cognee_vpc_enabled = length(var.cognee_subnet_ids) > 0 && length(var.cognee_security_group_ids) > 0
29
31
  okf_efs_vpc_enabled = var.okf_efs_enabled && length(var.okf_efs_subnet_ids) > 0 && length(var.okf_efs_security_group_ids) > 0
@@ -246,8 +248,9 @@ resource "aws_iam_role_policy" "agentcore_pi" {
246
248
  },
247
249
  {
248
250
  # Invoke API Lambdas from Pi's private VPC. Memory retain is queued
249
- # async; chat activity/finalize are RequestResponse callbacks because
250
- # public API Gateway endpoints are not reachable reliably from this VPC.
251
+ # async; chat activity/finalize and the per-turn capability-manifest
252
+ # POST are RequestResponse callbacks because public API Gateway
253
+ # endpoints are not reachable reliably from this VPC.
251
254
  Sid = "ApiLambdaInvoke"
252
255
  Effect = "Allow"
253
256
  Action = ["lambda:InvokeFunction"]
@@ -255,6 +258,7 @@ resource "aws_iam_role_policy" "agentcore_pi" {
255
258
  local.memory_retain_fn_arn,
256
259
  local.chat_agent_finalize_fn_arn,
257
260
  local.chat_agent_activity_fn_arn,
261
+ local.manifest_log_fn_arn,
258
262
  ]
259
263
  },
260
264
  {
@@ -349,6 +353,14 @@ resource "terraform_data" "seed_pi_image" {
349
353
  source_id="$(docker image inspect --format '{{.Id}}' "${var.source_image_uri}")"
350
354
  docker tag "$source_id" "${local.pi_image_uri}"
351
355
  else
356
+ # Unreachable source must not fail a stack whose target tag is already
357
+ # seeded (rerun after a partial deploy, or a registry that went
358
+ # private) — the Lambda only consumes the target ECR tag.
359
+ repo_name="$(basename "${var.ecr_repository_url}")"
360
+ if aws ecr describe-images --repository-name "$repo_name" --image-ids imageTag=pi-latest --region ${var.region} >/dev/null 2>&1; then
361
+ echo "WARN: could not pull ${var.source_image_uri}; ${local.pi_image_uri} is already seeded — keeping the existing image."
362
+ exit 0
363
+ fi
352
364
  dockerfile="$repo_root/packages/agentcore-pi/agent-container/Dockerfile"
353
365
  if [[ ! -f "$dockerfile" ]]; then
354
366
  echo "ERROR: could not pull ${var.source_image_uri} and there is no repo checkout at $repo_root to build from." >&2
@@ -389,6 +401,7 @@ resource "aws_lambda_function" "agentcore_pi" {
389
401
  MEMORY_RETAIN_FN_NAME = local.memory_retain_fn_name
390
402
  CHAT_AGENT_FINALIZE_FN_NAME = local.chat_agent_finalize_fn_name
391
403
  CHAT_AGENT_ACTIVITY_FN_NAME = local.chat_agent_activity_fn_name
404
+ MANIFEST_LOG_FUNCTION_NAME = local.manifest_log_fn_name
392
405
  HINDSIGHT_ENDPOINT = var.hindsight_endpoint
393
406
  THINKWORK_API_URL = var.api_endpoint
394
407
  API_AUTH_SECRET = var.api_auth_secret
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkwork-cli",
3
- "version": "0.12.15",
3
+ "version": "0.12.16",
4
4
  "description": "Thinkwork CLI — deploy, manage, and interact with your Thinkwork stack",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -33,6 +33,7 @@
33
33
  "@graphql-codegen/cli": "^5.0.6",
34
34
  "@graphql-codegen/client-preset": "^4.8.2",
35
35
  "@thinkwork/admin-ops": "workspace:*",
36
+ "@thinkwork/database-pg": "workspace:*",
36
37
  "@types/node": "^25.6.0",
37
38
  "tsup": "^8.0.0",
38
39
  "tsx": "^4.0.0",