thinkwork-cli 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +18 -2
- package/dist/cli.js +3004 -215
- package/dist/terraform/examples/greenfield/main.tf +325 -19
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +14 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +61 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +54 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +197 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +70 -0
- package/dist/terraform/modules/app/agentcore-flue/README.md +58 -0
- package/dist/terraform/modules/app/agentcore-flue/main.tf +322 -0
- package/dist/terraform/modules/app/agentcore-flue/outputs.tf +23 -0
- package/dist/terraform/modules/app/agentcore-flue/variables.tf +91 -0
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +0 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +204 -4
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +4 -0
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +5 -0
- package/dist/terraform/modules/app/computer-runtime/README.md +15 -0
- package/dist/terraform/modules/app/computer-runtime/main.tf +406 -0
- package/dist/terraform/modules/app/computer-runtime/outputs.tf +75 -0
- package/dist/terraform/modules/app/computer-runtime/variables.tf +66 -0
- package/dist/terraform/modules/app/hindsight-memory/main.tf +6 -0
- package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +128 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +1557 -42
- package/dist/terraform/modules/app/lambda-api/main.tf +299 -15
- package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +118 -0
- package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +49 -0
- package/dist/terraform/modules/app/lambda-api/outputs.tf +38 -0
- package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +43 -0
- package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +53 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +349 -2
- package/dist/terraform/modules/app/lambda-api/workspace-events.tf +125 -0
- package/dist/terraform/modules/app/routines-stepfunctions/main.tf +453 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +66 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +200 -0
- package/dist/terraform/modules/app/static-site/main.tf +146 -5
- package/dist/terraform/modules/app/www-dns/main.tf +118 -15
- package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
- package/dist/terraform/modules/app/www-dns/variables.tf +42 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +164 -3
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +34 -0
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +16 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/README.md +145 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/main.tf +573 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/outputs.tf +43 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/variables.tf +93 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +269 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/outputs.tf +23 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +50 -0
- package/dist/terraform/modules/data/s3-backups-bucket/main.tf +123 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +13 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +5 -2
- package/dist/terraform/modules/thinkwork/main.tf +439 -21
- package/dist/terraform/modules/thinkwork/outputs.tf +121 -0
- package/dist/terraform/modules/thinkwork/variables.tf +165 -6
- package/dist/terraform/schema.graphql +45 -0
- package/package.json +15 -14
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
# In production this module will contain 30+ Lambda functions covering:
|
|
10
10
|
# - GraphQL HTTP handler (the main API entry point)
|
|
11
11
|
# - Agent invoke / chat
|
|
12
|
-
# - Thread, agent, template
|
|
12
|
+
# - Thread, agent, and template CRUD
|
|
13
13
|
# - Skills, KB, memory handlers
|
|
14
|
-
# -
|
|
14
|
+
# - Webhook, MCP, and OAuth handlers
|
|
15
15
|
# - Email inbound/outbound
|
|
16
16
|
# - OAuth callbacks
|
|
17
17
|
################################################################################
|
|
@@ -70,6 +70,58 @@ resource "aws_apigatewayv2_api_mapping" "main" {
|
|
|
70
70
|
stage = aws_apigatewayv2_stage.default.id
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
################################################################################
|
|
74
|
+
# MCP Custom Domain (optional) — second domain on the same HTTP API.
|
|
75
|
+
#
|
|
76
|
+
# Two-apply dance because ACM requires DNS validation before a Regional
|
|
77
|
+
# custom domain can bind the cert. `var.mcp_custom_domain_ready = false`
|
|
78
|
+
# (first apply) creates just the cert in pending-validation state and
|
|
79
|
+
# surfaces the validation record via `mcp_custom_domain_validation` output.
|
|
80
|
+
# The operator adds that record to Cloudflare (via `pnpm cf:sync-mcp`),
|
|
81
|
+
# waits ~5 min for ACM validation, then sets `mcp_custom_domain_ready =
|
|
82
|
+
# true` for the second apply, which creates the domain + API mapping.
|
|
83
|
+
# A final `pnpm cf:sync-mcp --finalize` adds the `mcp.thinkwork.ai`
|
|
84
|
+
# CNAME pointing at the regional domain target.
|
|
85
|
+
#
|
|
86
|
+
# See docs/solutions/patterns/mcp-custom-domain-setup-2026-04-23.md.
|
|
87
|
+
################################################################################
|
|
88
|
+
|
|
89
|
+
resource "aws_acm_certificate" "mcp" {
|
|
90
|
+
count = var.mcp_custom_domain != "" ? 1 : 0
|
|
91
|
+
domain_name = var.mcp_custom_domain
|
|
92
|
+
validation_method = "DNS"
|
|
93
|
+
|
|
94
|
+
lifecycle {
|
|
95
|
+
create_before_destroy = true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
tags = {
|
|
99
|
+
Name = "thinkwork-${var.stage}-mcp-cert"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
resource "aws_apigatewayv2_domain_name" "mcp" {
|
|
104
|
+
count = var.mcp_custom_domain != "" && var.mcp_custom_domain_ready ? 1 : 0
|
|
105
|
+
domain_name = var.mcp_custom_domain
|
|
106
|
+
|
|
107
|
+
domain_name_configuration {
|
|
108
|
+
certificate_arn = aws_acm_certificate.mcp[0].arn
|
|
109
|
+
endpoint_type = "REGIONAL"
|
|
110
|
+
security_policy = "TLS_1_2"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
tags = {
|
|
114
|
+
Name = "thinkwork-${var.stage}-mcp-domain"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
resource "aws_apigatewayv2_api_mapping" "mcp" {
|
|
119
|
+
count = var.mcp_custom_domain != "" && var.mcp_custom_domain_ready ? 1 : 0
|
|
120
|
+
api_id = aws_apigatewayv2_api.main.id
|
|
121
|
+
domain_name = aws_apigatewayv2_domain_name.mcp[0].id
|
|
122
|
+
stage = aws_apigatewayv2_stage.default.id
|
|
123
|
+
}
|
|
124
|
+
|
|
73
125
|
################################################################################
|
|
74
126
|
# Shared Lambda Execution Role
|
|
75
127
|
################################################################################
|
|
@@ -92,6 +144,19 @@ resource "aws_iam_role_policy_attachment" "lambda_basic" {
|
|
|
92
144
|
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
93
145
|
}
|
|
94
146
|
|
|
147
|
+
resource "aws_iam_role_policy_attachment" "computer_runtime_manager" {
|
|
148
|
+
# No count guard: module.computer_runtime is unconditional (see
|
|
149
|
+
# terraform/modules/thinkwork/main.tf), so the policy ARN is always
|
|
150
|
+
# populated. A `count = var.X != "" ? 1 : 0` guard fails terraform
|
|
151
|
+
# plan with "Invalid count argument" because var.X resolves from
|
|
152
|
+
# aws_iam_policy.manager.arn — a computed attribute only known at
|
|
153
|
+
# apply time, not at plan time. If a future caller wants to make
|
|
154
|
+
# this attachment conditional, gate it on a known-at-plan-time
|
|
155
|
+
# boolean variable rather than the ARN string.
|
|
156
|
+
role = aws_iam_role.lambda.name
|
|
157
|
+
policy_arn = var.computer_runtime_manager_policy_arn
|
|
158
|
+
}
|
|
159
|
+
|
|
95
160
|
resource "aws_iam_role_policy" "lambda_rds" {
|
|
96
161
|
name = "rds-data-api"
|
|
97
162
|
role = aws_iam_role.lambda.id
|
|
@@ -129,6 +194,7 @@ resource "aws_iam_role_policy" "lambda_secrets" {
|
|
|
129
194
|
Action = [
|
|
130
195
|
"secretsmanager:CreateSecret",
|
|
131
196
|
"secretsmanager:UpdateSecret",
|
|
197
|
+
"secretsmanager:DeleteSecret",
|
|
132
198
|
"secretsmanager:GetSecretValue"
|
|
133
199
|
]
|
|
134
200
|
Resource = "arn:aws:secretsmanager:${var.region}:${var.account_id}:secret:thinkwork/*"
|
|
@@ -196,12 +262,21 @@ resource "aws_iam_role_policy" "lambda_bedrock" {
|
|
|
196
262
|
name = "bedrock-invoke"
|
|
197
263
|
role = aws_iam_role.lambda.id
|
|
198
264
|
|
|
265
|
+
# Cross-region inference profiles (us.anthropic.claude-*) require
|
|
266
|
+
# `bedrock:InvokeModel` on the *inference-profile* ARN AND on the
|
|
267
|
+
# underlying foundation-model ARN in *every* region the profile can
|
|
268
|
+
# route to (e.g. us-east-2 for us.anthropic.claude-haiku-4-5). The
|
|
269
|
+
# region wildcard below covers all of them. Needed by the eval-runner
|
|
270
|
+
# llm-rubric judge and any handler that calls Converse with a profile ID.
|
|
199
271
|
policy = jsonencode({
|
|
200
272
|
Version = "2012-10-17"
|
|
201
273
|
Statement = [{
|
|
202
|
-
Effect
|
|
203
|
-
Action
|
|
204
|
-
Resource =
|
|
274
|
+
Effect = "Allow"
|
|
275
|
+
Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
|
|
276
|
+
Resource = [
|
|
277
|
+
"arn:aws:bedrock:*::foundation-model/*",
|
|
278
|
+
"arn:aws:bedrock:*:${var.account_id}:inference-profile/*",
|
|
279
|
+
]
|
|
205
280
|
}]
|
|
206
281
|
})
|
|
207
282
|
}
|
|
@@ -244,14 +319,72 @@ resource "aws_iam_role_policy" "lambda_agentcore_invoke" {
|
|
|
244
319
|
Action = [
|
|
245
320
|
"lambda:InvokeFunction",
|
|
246
321
|
]
|
|
247
|
-
Resource = [
|
|
322
|
+
Resource = compact([
|
|
248
323
|
var.agentcore_function_arn,
|
|
249
324
|
"${var.agentcore_function_arn}:*",
|
|
250
|
-
|
|
325
|
+
var.agentcore_flue_function_arn,
|
|
326
|
+
var.agentcore_flue_function_arn != "" ? "${var.agentcore_flue_function_arn}:*" : "",
|
|
327
|
+
])
|
|
251
328
|
}]
|
|
252
329
|
})
|
|
253
330
|
}
|
|
254
331
|
|
|
332
|
+
# Eval-runner: invoke the AgentCore Runtime data plane to run an agent
|
|
333
|
+
# under test, and call AgentCore Evaluations.Evaluate to score the
|
|
334
|
+
# resulting spans. Both APIs are on the bedrock-agentcore service. Also
|
|
335
|
+
# allow reading spans + log events from CloudWatch Logs (aws/spans is
|
|
336
|
+
# the Transaction Search destination; the runtime log groups carry the
|
|
337
|
+
# OTel scope=strands.telemetry.tracer log records that EvaluateCommand
|
|
338
|
+
# requires alongside the spans).
|
|
339
|
+
resource "aws_iam_role_policy" "lambda_eval_runner" {
|
|
340
|
+
name = "eval-runner-bedrock-agentcore"
|
|
341
|
+
role = aws_iam_role.lambda.id
|
|
342
|
+
|
|
343
|
+
policy = jsonencode({
|
|
344
|
+
Version = "2012-10-17"
|
|
345
|
+
Statement = [
|
|
346
|
+
{
|
|
347
|
+
Sid = "AgentCoreInvokeRuntime"
|
|
348
|
+
Effect = "Allow"
|
|
349
|
+
Action = ["bedrock-agentcore:InvokeAgentRuntime"]
|
|
350
|
+
Resource = "arn:aws:bedrock-agentcore:${var.region}:${var.account_id}:runtime/*"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
Sid = "AgentCoreEvaluate"
|
|
354
|
+
Effect = "Allow"
|
|
355
|
+
Action = [
|
|
356
|
+
"bedrock-agentcore:Evaluate",
|
|
357
|
+
"bedrock-agentcore:GetEvaluator",
|
|
358
|
+
"bedrock-agentcore:ListEvaluators",
|
|
359
|
+
]
|
|
360
|
+
Resource = "*"
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
Sid = "EvalSpansRead"
|
|
364
|
+
Effect = "Allow"
|
|
365
|
+
Action = [
|
|
366
|
+
"logs:FilterLogEvents",
|
|
367
|
+
"logs:GetLogEvents",
|
|
368
|
+
"logs:DescribeLogGroups",
|
|
369
|
+
"logs:DescribeLogStreams",
|
|
370
|
+
]
|
|
371
|
+
Resource = [
|
|
372
|
+
"arn:aws:logs:${var.region}:${var.account_id}:log-group:aws/spans",
|
|
373
|
+
"arn:aws:logs:${var.region}:${var.account_id}:log-group:aws/spans:*",
|
|
374
|
+
"arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*",
|
|
375
|
+
"arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*:*",
|
|
376
|
+
]
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
Sid = "SsmReadEvalRunnerCfg"
|
|
380
|
+
Effect = "Allow"
|
|
381
|
+
Action = ["ssm:GetParameter", "ssm:GetParameters"]
|
|
382
|
+
Resource = "arn:aws:ssm:${var.region}:${var.account_id}:parameter/thinkwork/${var.stage}/agentcore/runtime-id-*"
|
|
383
|
+
},
|
|
384
|
+
]
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
255
388
|
# AgentCore Memory read access for the GraphQL memory resolvers.
|
|
256
389
|
# memoryRecords / memorySearch call ListMemoryRecordsCommand to fetch
|
|
257
390
|
# records across the tenant's agents.
|
|
@@ -291,14 +424,33 @@ resource "aws_iam_role_policy" "lambda_ssm_read" {
|
|
|
291
424
|
|
|
292
425
|
policy = jsonencode({
|
|
293
426
|
Version = "2012-10-17"
|
|
294
|
-
Statement = [
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
427
|
+
Statement = [
|
|
428
|
+
{
|
|
429
|
+
Effect = "Allow"
|
|
430
|
+
Action = [
|
|
431
|
+
"ssm:GetParameter",
|
|
432
|
+
"ssm:GetParameters",
|
|
433
|
+
]
|
|
434
|
+
Resource = "arn:aws:ssm:${var.region}:${var.account_id}:parameter/thinkwork/${var.stage}/*"
|
|
435
|
+
},
|
|
436
|
+
# SecureString parameters (e.g. /thinkwork/<stage>/google-places/api-key)
|
|
437
|
+
# are encrypted with the default AWS-managed SSM key. The default key's
|
|
438
|
+
# resource policy auto-grants Decrypt to any IAM principal with
|
|
439
|
+
# ssm:GetParameter on the parameter via `kms:ViaService = ssm.*`, so
|
|
440
|
+
# this explicit grant is a belt-and-suspenders clarification. If we
|
|
441
|
+
# later move to a customer-managed KMS key, this is the scope that
|
|
442
|
+
# needs updating.
|
|
443
|
+
{
|
|
444
|
+
Effect = "Allow"
|
|
445
|
+
Action = ["kms:Decrypt"]
|
|
446
|
+
Resource = "*"
|
|
447
|
+
Condition = {
|
|
448
|
+
StringEquals = {
|
|
449
|
+
"kms:ViaService" = "ssm.${var.region}.amazonaws.com"
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
]
|
|
302
454
|
})
|
|
303
455
|
}
|
|
304
456
|
|
|
@@ -362,11 +514,143 @@ resource "aws_iam_role_policy" "lambda_api_cross_invoke" {
|
|
|
362
514
|
"arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-chat-agent-invoke",
|
|
363
515
|
"arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-knowledge-base-manager",
|
|
364
516
|
"arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-job-schedule-manager",
|
|
517
|
+
# eval-runner: graphql-http's startEvalRun mutation Event-invokes
|
|
518
|
+
# this asynchronously after inserting the eval_runs row.
|
|
519
|
+
"arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-eval-runner",
|
|
520
|
+
# wiki-compile: memory-retain Event-invokes this after a successful
|
|
521
|
+
# retainTurn when the tenant's wiki_compile_enabled flag is on.
|
|
522
|
+
# compileWikiNow admin mutation also Event-invokes.
|
|
523
|
+
"arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-wiki-compile",
|
|
524
|
+
# wiki-bootstrap-import: bootstrapJournalImport admin mutation
|
|
525
|
+
# Event-invokes this for the long-running ingest path.
|
|
526
|
+
"arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-wiki-bootstrap-import",
|
|
527
|
+
# routine-resume: routine-approval-bridge (Phase B U8) invokes
|
|
528
|
+
# this with RequestResponse after a HITL decideInboxItem
|
|
529
|
+
# decision. Calls SendTaskSuccess/SendTaskFailure on the SFN
|
|
530
|
+
# task token; idempotent on already-consumed tokens.
|
|
531
|
+
"arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-routine-resume",
|
|
532
|
+
# workspace-files-efs: workspace-files invokes this (RequestResponse)
|
|
533
|
+
# for Computer-target list/get to bypass the computer_tasks queue
|
|
534
|
+
# and read EFS directly. Standalone resource below.
|
|
535
|
+
"arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-workspace-files-efs",
|
|
365
536
|
]
|
|
366
537
|
}]
|
|
367
538
|
})
|
|
368
539
|
}
|
|
369
540
|
|
|
541
|
+
# Step Functions admin operations — for createRoutine / publishRoutineVersion
|
|
542
|
+
# / triggerRoutineRun / updateRoutine resolvers (Phase B U7) and the
|
|
543
|
+
# routine-asl-validator Lambda (Phase A U5). State-machine ARNs follow the
|
|
544
|
+
# naming convention `thinkwork-${stage}-routine-*`; aliases follow the
|
|
545
|
+
# state-machine ARN with a colon-separated alias name.
|
|
546
|
+
resource "aws_iam_role_policy" "lambda_routines_stepfunctions" {
|
|
547
|
+
name = "routines-step-functions"
|
|
548
|
+
role = aws_iam_role.lambda.id
|
|
549
|
+
|
|
550
|
+
policy = jsonencode({
|
|
551
|
+
Version = "2012-10-17"
|
|
552
|
+
Statement = [
|
|
553
|
+
{
|
|
554
|
+
Sid = "RoutineStateMachineLifecycle"
|
|
555
|
+
Effect = "Allow"
|
|
556
|
+
Action = [
|
|
557
|
+
"states:CreateStateMachine",
|
|
558
|
+
"states:UpdateStateMachine",
|
|
559
|
+
"states:DeleteStateMachine",
|
|
560
|
+
"states:DescribeStateMachine",
|
|
561
|
+
"states:ListStateMachines",
|
|
562
|
+
"states:TagResource",
|
|
563
|
+
"states:UntagResource",
|
|
564
|
+
"states:PublishStateMachineVersion",
|
|
565
|
+
"states:DeleteStateMachineVersion",
|
|
566
|
+
"states:ListStateMachineVersions",
|
|
567
|
+
"states:CreateStateMachineAlias",
|
|
568
|
+
"states:UpdateStateMachineAlias",
|
|
569
|
+
"states:DeleteStateMachineAlias",
|
|
570
|
+
"states:DescribeStateMachineAlias",
|
|
571
|
+
"states:ListStateMachineAliases",
|
|
572
|
+
"states:DescribeStateMachineForExecution",
|
|
573
|
+
]
|
|
574
|
+
Resource = "arn:aws:states:${var.region}:${var.account_id}:stateMachine:thinkwork-${var.stage}-routine-*"
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
Sid = "RoutineExecution"
|
|
578
|
+
Effect = "Allow"
|
|
579
|
+
Action = [
|
|
580
|
+
"states:StartExecution",
|
|
581
|
+
"states:StartSyncExecution",
|
|
582
|
+
"states:StopExecution",
|
|
583
|
+
"states:DescribeExecution",
|
|
584
|
+
"states:ListExecutions",
|
|
585
|
+
"states:GetExecutionHistory",
|
|
586
|
+
]
|
|
587
|
+
Resource = [
|
|
588
|
+
"arn:aws:states:${var.region}:${var.account_id}:stateMachine:thinkwork-${var.stage}-routine-*",
|
|
589
|
+
"arn:aws:states:${var.region}:${var.account_id}:execution:thinkwork-${var.stage}-routine-*:*",
|
|
590
|
+
]
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
Sid = "RoutineTaskTokens"
|
|
594
|
+
Effect = "Allow"
|
|
595
|
+
Action = [
|
|
596
|
+
"states:SendTaskSuccess",
|
|
597
|
+
"states:SendTaskFailure",
|
|
598
|
+
"states:SendTaskHeartbeat",
|
|
599
|
+
]
|
|
600
|
+
Resource = "*"
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
Sid = "RoutineValidate"
|
|
604
|
+
Effect = "Allow"
|
|
605
|
+
Action = ["states:ValidateStateMachineDefinition"]
|
|
606
|
+
Resource = "*"
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
# PassRole so the createRoutine resolver can hand the routines
|
|
610
|
+
# execution role to a newly-created state machine. Scoped to the
|
|
611
|
+
# specific role created by the routines-stepfunctions module.
|
|
612
|
+
Sid = "RoutinePassExecutionRole"
|
|
613
|
+
Effect = "Allow"
|
|
614
|
+
Action = ["iam:PassRole"]
|
|
615
|
+
Resource = "arn:aws:iam::${var.account_id}:role/thinkwork-${var.stage}-routines-execution-role"
|
|
616
|
+
Condition = {
|
|
617
|
+
StringEquals = {
|
|
618
|
+
"iam:PassedToService" = "states.amazonaws.com"
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
# routine-task-python (Phase B U6) wraps the AgentCore code
|
|
624
|
+
# interpreter so SFN can run `python` recipe states. Three calls
|
|
625
|
+
# per Task: Start session, Invoke, Stop. Resource is `*` because
|
|
626
|
+
# interpreter sessions are runtime-scoped, not provisioned.
|
|
627
|
+
Sid = "RoutineTaskPythonCodeInterpreter"
|
|
628
|
+
Effect = "Allow"
|
|
629
|
+
Action = [
|
|
630
|
+
"bedrock-agentcore:StartCodeInterpreterSession",
|
|
631
|
+
"bedrock-agentcore:InvokeCodeInterpreter",
|
|
632
|
+
"bedrock-agentcore:StopCodeInterpreterSession",
|
|
633
|
+
"bedrock-agentcore:GetCodeInterpreterSession",
|
|
634
|
+
]
|
|
635
|
+
Resource = "*"
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
# routine-task-python S3 offload — full stdout/stderr land in
|
|
639
|
+
# the per-stage routine-output bucket under
|
|
640
|
+
# <tenantId>/<sfn-execution-id>/<nodeId>/{stdout,stderr}.log.
|
|
641
|
+
# PutObject only — the read path is GraphQL-fronted and runs
|
|
642
|
+
# under the graphql-http handler's role, not this one.
|
|
643
|
+
Sid = "RoutineTaskPythonS3Offload"
|
|
644
|
+
Effect = "Allow"
|
|
645
|
+
Action = [
|
|
646
|
+
"s3:PutObject",
|
|
647
|
+
]
|
|
648
|
+
Resource = "arn:aws:s3:::thinkwork-${var.stage}-routine-output/*"
|
|
649
|
+
},
|
|
650
|
+
]
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
|
|
370
654
|
################################################################################
|
|
371
655
|
# Placeholder Lambda — proves the infrastructure works
|
|
372
656
|
#
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
locals {
|
|
2
|
+
mcp_oauth_api_base_url = "https://${aws_apigatewayv2_api.main.id}.execute-api.${var.region}.amazonaws.com"
|
|
3
|
+
mcp_oauth_cognito_base_url = var.cognito_auth_domain != "" ? "https://${var.cognito_auth_domain}.auth.${var.region}.amazoncognito.com" : ""
|
|
4
|
+
mcp_oauth_identity_providers = var.google_oauth_client_id != "" ? ["Google", "COGNITO"] : ["COGNITO"]
|
|
5
|
+
mcp_oauth_logo_path = "${path.module}/../../../../apps/admin/public/logo.png"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
resource "aws_dynamodb_table" "mcp_oauth_revocations" {
|
|
9
|
+
name = "thinkwork-${var.stage}-mcp-oauth-revocations"
|
|
10
|
+
billing_mode = "PAY_PER_REQUEST"
|
|
11
|
+
hash_key = "token_id_hash"
|
|
12
|
+
|
|
13
|
+
attribute {
|
|
14
|
+
name = "token_id_hash"
|
|
15
|
+
type = "S"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
ttl {
|
|
19
|
+
attribute_name = "expires_at"
|
|
20
|
+
enabled = true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
tags = {
|
|
24
|
+
Name = "thinkwork-${var.stage}-mcp-oauth-revocations"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resource "aws_cognito_user_pool_client" "mcp_oauth" {
|
|
29
|
+
name = "ThinkworkMcpOAuth"
|
|
30
|
+
user_pool_id = var.user_pool_id
|
|
31
|
+
|
|
32
|
+
allowed_oauth_flows_user_pool_client = true
|
|
33
|
+
allowed_oauth_flows = ["code"]
|
|
34
|
+
allowed_oauth_scopes = ["openid", "email", "profile"]
|
|
35
|
+
|
|
36
|
+
supported_identity_providers = local.mcp_oauth_identity_providers
|
|
37
|
+
|
|
38
|
+
callback_urls = ["${local.mcp_oauth_api_base_url}/mcp/oauth/callback"]
|
|
39
|
+
logout_urls = [local.mcp_oauth_api_base_url]
|
|
40
|
+
|
|
41
|
+
access_token_validity = 1
|
|
42
|
+
id_token_validity = 1
|
|
43
|
+
|
|
44
|
+
token_validity_units {
|
|
45
|
+
access_token = "hours"
|
|
46
|
+
id_token = "hours"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
resource "aws_cognito_user_pool_ui_customization" "mcp_oauth" {
|
|
51
|
+
user_pool_id = var.user_pool_id
|
|
52
|
+
client_id = aws_cognito_user_pool_client.mcp_oauth.id
|
|
53
|
+
image_file = fileexists(local.mcp_oauth_logo_path) ? filebase64(local.mcp_oauth_logo_path) : null
|
|
54
|
+
|
|
55
|
+
css = <<-CSS
|
|
56
|
+
.background-customizable {
|
|
57
|
+
background-color: #080808;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.banner-customizable {
|
|
61
|
+
background-color: #080808;
|
|
62
|
+
padding: 32px 0 18px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.label-customizable {
|
|
66
|
+
color: #f5f5f5;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.legalText-customizable {
|
|
70
|
+
color: #f5f5f5;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.inputField-customizable {
|
|
74
|
+
background-color: #232323;
|
|
75
|
+
border: 1px solid #555555;
|
|
76
|
+
border-radius: 8px;
|
|
77
|
+
color: #ffffff;
|
|
78
|
+
min-height: 48px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.inputField-customizable:focus {
|
|
82
|
+
border-color: #d8d8d8;
|
|
83
|
+
box-shadow: 0 0 0 3px rgba(216, 216, 216, 0.2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.submitButton-customizable {
|
|
87
|
+
background-color: #f4f4f4;
|
|
88
|
+
border: 0;
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
color: #111111;
|
|
91
|
+
font-weight: 700;
|
|
92
|
+
min-height: 48px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.submitButton-customizable:hover {
|
|
96
|
+
background-color: #ffffff;
|
|
97
|
+
}
|
|
98
|
+
CSS
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
resource "aws_iam_role_policy" "lambda_mcp_oauth_revocations" {
|
|
102
|
+
name = "mcp-oauth-revocations"
|
|
103
|
+
role = aws_iam_role.lambda.id
|
|
104
|
+
|
|
105
|
+
policy = jsonencode({
|
|
106
|
+
Version = "2012-10-17"
|
|
107
|
+
Statement = [
|
|
108
|
+
{
|
|
109
|
+
Effect = "Allow"
|
|
110
|
+
Action = [
|
|
111
|
+
"dynamodb:GetItem",
|
|
112
|
+
"dynamodb:PutItem"
|
|
113
|
+
]
|
|
114
|
+
Resource = aws_dynamodb_table.mcp_oauth_revocations.arn
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
})
|
|
118
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Per-user OAuth client credentials (Google Workspace, Microsoft 365)
|
|
3
|
+
#
|
|
4
|
+
# Stored in Secrets Manager as JSON blobs `{"client_id","client_secret"}`.
|
|
5
|
+
# Fetched by oauth-authorize, oauth-callback, oauth-token at Lambda cold-start
|
|
6
|
+
# via packages/api/src/lib/oauth-client-credentials.ts, which caches values in
|
|
7
|
+
# module scope so warm containers pay zero additional round-trips.
|
|
8
|
+
#
|
|
9
|
+
# Secret names use the existing `thinkwork/${stage}/...` prefix so the shared
|
|
10
|
+
# Lambda role's `secretsmanager:GetSecretValue` policy on `thinkwork/*`
|
|
11
|
+
# (main.tf:128-135) covers them — no new IAM attachment needed.
|
|
12
|
+
#
|
|
13
|
+
# Security posture vs. the prior common_env env-var approach: client secrets
|
|
14
|
+
# are no longer baked into Lambda configuration (invisible in
|
|
15
|
+
# `aws lambda get-function-configuration`, in CloudWatch event streams, and
|
|
16
|
+
# in terraform state for the Lambda resource). The secret value itself is
|
|
17
|
+
# still readable by any Lambda using the shared role — per-consumer-role
|
|
18
|
+
# scoping is a separate (larger) refactor tracked for prod.
|
|
19
|
+
################################################################################
|
|
20
|
+
|
|
21
|
+
resource "aws_secretsmanager_secret" "oauth_google_productivity" {
|
|
22
|
+
name = "thinkwork/${var.stage}/oauth/google-productivity"
|
|
23
|
+
description = "Google Workspace OAuth client credentials (per-user Gmail/Calendar integration). Fetched at Lambda cold-start by oauth-client-credentials.ts."
|
|
24
|
+
tags = {
|
|
25
|
+
Name = "thinkwork-${var.stage}-oauth-google-productivity"
|
|
26
|
+
Stage = var.stage
|
|
27
|
+
Provider = "google_productivity"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
resource "aws_secretsmanager_secret_version" "oauth_google_productivity" {
|
|
32
|
+
secret_id = aws_secretsmanager_secret.oauth_google_productivity.id
|
|
33
|
+
secret_string = jsonencode({
|
|
34
|
+
client_id = var.google_oauth_client_id
|
|
35
|
+
client_secret = var.google_oauth_client_secret
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
# If the operator rotates the secret via AWS console / CLI without touching
|
|
39
|
+
# tfvars, terraform shouldn't clobber it on next apply. Matches the pattern
|
|
40
|
+
# used for google_places_api_key in the wiki-compile handler.
|
|
41
|
+
lifecycle {
|
|
42
|
+
ignore_changes = [secret_string]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Microsoft 365 deferred to a follow-up — needs Azure app registration first.
|
|
47
|
+
# When ready, mirror the google-productivity resources + add
|
|
48
|
+
# `microsoft_oauth_client_id` / `_secret` variables to this module and the
|
|
49
|
+
# thinkwork module, then add MICROSOFT_OAUTH_SECRET_ARN to common_env.
|
|
@@ -13,6 +13,11 @@ output "api_execution_arn" {
|
|
|
13
13
|
value = aws_apigatewayv2_api.main.execution_arn
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
output "extension_proxy_route_prefix" {
|
|
17
|
+
description = "Route prefix private Admin extensions call through for tenant-scoped backend proxying."
|
|
18
|
+
value = "/api/extensions"
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
output "lambda_role_arn" {
|
|
17
22
|
description = "Shared Lambda execution role ARN (for other modules that add routes)"
|
|
18
23
|
value = aws_iam_role.lambda.arn
|
|
@@ -42,3 +47,36 @@ output "email_inbound_fn_name" {
|
|
|
42
47
|
description = "email-inbound Lambda function name. Used by the SES module for lambda:InvokeFunction permissions."
|
|
43
48
|
value = local.use_local_zips ? aws_lambda_function.handler["email-inbound"].function_name : ""
|
|
44
49
|
}
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# MCP custom domain — outputs consumed by `pnpm cf:sync-mcp`.
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
output "mcp_custom_domain" {
|
|
56
|
+
description = "Configured MCP custom domain (e.g., mcp.thinkwork.ai), or empty string when disabled. The CF sync script reads this to know the target hostname."
|
|
57
|
+
value = var.mcp_custom_domain
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
output "mcp_custom_domain_cert_arn" {
|
|
61
|
+
description = "ACM certificate ARN for the MCP custom domain, or empty when disabled. The sync script can use this to poll ACM validation status via aws acm describe-certificate."
|
|
62
|
+
value = var.mcp_custom_domain != "" ? aws_acm_certificate.mcp[0].arn : ""
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
output "mcp_custom_domain_validation" {
|
|
66
|
+
description = "List of ACM DNS-validation records that must exist in Cloudflare before the cert is issued. Each record is { name, type, value }. Consumed by scripts/cloudflare-sync-mcp.ts."
|
|
67
|
+
value = var.mcp_custom_domain != "" ? [
|
|
68
|
+
for dvo in aws_acm_certificate.mcp[0].domain_validation_options : {
|
|
69
|
+
name = dvo.resource_record_name
|
|
70
|
+
type = dvo.resource_record_type
|
|
71
|
+
value = dvo.resource_record_value
|
|
72
|
+
}
|
|
73
|
+
] : []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
output "mcp_custom_domain_target" {
|
|
77
|
+
description = "Regional target for the final mcp.thinkwork.ai → API Gateway CNAME. Only populated on the second apply (when mcp_custom_domain_ready = true). Includes { target_domain_name, hosted_zone_id } so the CF sync script can upsert the record."
|
|
78
|
+
value = var.mcp_custom_domain != "" && var.mcp_custom_domain_ready ? {
|
|
79
|
+
target_domain_name = aws_apigatewayv2_domain_name.mcp[0].domain_name_configuration[0].target_domain_name
|
|
80
|
+
hosted_zone_id = aws_apigatewayv2_domain_name.mcp[0].domain_name_configuration[0].hosted_zone_id
|
|
81
|
+
} : null
|
|
82
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Slack workspace app credentials
|
|
3
|
+
#
|
|
4
|
+
# Stored in Secrets Manager as a JSON blob with three fields:
|
|
5
|
+
#
|
|
6
|
+
# {
|
|
7
|
+
# "signing_secret": "Slack request signing secret",
|
|
8
|
+
# "client_id": "Slack OAuth client id",
|
|
9
|
+
# "client_secret": "Slack OAuth client secret"
|
|
10
|
+
# }
|
|
11
|
+
#
|
|
12
|
+
# Slack request handlers and the OAuth install handler receive only this secret
|
|
13
|
+
# ARN in Lambda configuration. The shared Lambda role already has access to the
|
|
14
|
+
# `thinkwork/*` prefix, so no additional IAM attachment is needed.
|
|
15
|
+
#
|
|
16
|
+
# Operators populate the real value out-of-band. Terraform creates an initial
|
|
17
|
+
# empty version so Lambdas fail with a clear missing-field error before setup,
|
|
18
|
+
# and lifecycle.ignore_changes prevents later applies from overwriting rotated
|
|
19
|
+
# credentials.
|
|
20
|
+
################################################################################
|
|
21
|
+
|
|
22
|
+
resource "aws_secretsmanager_secret" "slack_app_credentials" {
|
|
23
|
+
name = "thinkwork/${var.stage}/slack/app"
|
|
24
|
+
description = "Slack workspace app credentials (signing_secret, client_id, client_secret). Populate via Secrets Manager; never via tfvars."
|
|
25
|
+
tags = {
|
|
26
|
+
Name = "thinkwork-${var.stage}-slack-app"
|
|
27
|
+
Stage = var.stage
|
|
28
|
+
Provider = "slack"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
resource "aws_secretsmanager_secret_version" "slack_app_credentials_initial" {
|
|
33
|
+
secret_id = aws_secretsmanager_secret.slack_app_credentials.id
|
|
34
|
+
secret_string = jsonencode({
|
|
35
|
+
signing_secret = ""
|
|
36
|
+
client_id = ""
|
|
37
|
+
client_secret = ""
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
lifecycle {
|
|
41
|
+
ignore_changes = [secret_string]
|
|
42
|
+
}
|
|
43
|
+
}
|