thinkwork-cli 0.13.0 → 0.13.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.
Files changed (74) hide show
  1. package/dist/{api-client-JUBCQQDF.js → api-client-4DZ266J5.js} +1 -1
  2. package/dist/{chunk-STY44FQO.js → chunk-Y2WHJECR.js} +44 -12
  3. package/dist/cli.js +36715 -12965
  4. package/dist/drizzle/0227_analyst_reader_role.sql +100 -38
  5. package/dist/drizzle/0230_analyst_rls.sql +183 -66
  6. package/dist/drizzle/0232_memory_source_thin_ledger.sql +176 -0
  7. package/dist/drizzle/0232_message_source_event_id.sql +9 -0
  8. package/dist/drizzle/0233_msteams_install_and_links.sql +151 -0
  9. package/dist/drizzle/0233_workflow_memory_stage_tokens.sql +20 -0
  10. package/dist/drizzle/0234_memory_claim_lifecycle.sql +160 -0
  11. package/dist/drizzle/0235_workflow_token_execution_claim.sql +34 -0
  12. package/dist/drizzle/0236_memory_retraction_fencing.sql +50 -0
  13. package/dist/drizzle/0237_memory_claims_active_value_uidx.sql +172 -0
  14. package/dist/drizzle/0238_memory_erase_generation_fence.sql +97 -0
  15. package/dist/drizzle/0239_canonical_entity_identity.sql +257 -0
  16. package/dist/drizzle/0240_memory_personal_scope.sql +45 -0
  17. package/dist/drizzle/0241_knowledge_base_document_manifest.sql +67 -0
  18. package/dist/drizzle/0242_search_queries.sql +28 -0
  19. package/dist/drizzle/0243_section_sources_thread_refs.sql +12 -0
  20. package/dist/drizzle/0244_system_automations_and_memory_stage_overrides.sql +45 -0
  21. package/dist/drizzle/0245_wiki_page_render.sql +21 -0
  22. package/dist/drizzle/0246_automation_workflow_ownership.sql +30 -0
  23. package/dist/drizzle/0247_capability_runtime.sql +402 -0
  24. package/dist/drizzle/0248_capability_private_interpreter.sql +13 -0
  25. package/dist/drizzle/0249_capability_headless_execution_evidence.sql +40 -0
  26. package/dist/drizzle/0250_kg_schema_extraction.sql +549 -0
  27. package/dist/drizzle/0251_agents_json_render_ui.sql +25 -0
  28. package/dist/drizzle/0251_brain_stragglers.sql +242 -0
  29. package/dist/drizzle/0252_drop_kg_and_straggler_compat_views.sql +100 -0
  30. package/dist/drizzle/0253_drop_brain_substrate.sql +91 -0
  31. package/dist/drizzle/0254_capability_autonomous_admission.sql +29 -0
  32. package/dist/drizzle/0255_capability_autonomous_approval_mode.sql +16 -0
  33. package/dist/drizzle/0256_agent_profile_manifest_authority.sql +18 -0
  34. package/dist/drizzle/0257_capability_approvals.sql +53 -0
  35. package/dist/drizzle/0258_pending_tool_approvals.sql +84 -0
  36. package/dist/drizzle/0259_tenant_capability_registry_trust.sql +19 -0
  37. package/dist/drizzle/0260_scheduled_jobs_run_as_user.sql +20 -0
  38. package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +11 -0
  39. package/dist/terraform/examples/greenfield/main.tf +40 -0
  40. package/dist/terraform/modules/app/agentcore-admin/main.tf +341 -0
  41. package/dist/terraform/modules/app/agentcore-admin/outputs.tf +20 -0
  42. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +61 -0
  43. package/dist/terraform/modules/app/agentcore-harness/main.tf +101 -0
  44. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +9 -0
  45. package/dist/terraform/modules/app/agentcore-harness/variables.tf +25 -0
  46. package/dist/terraform/modules/app/agentcore-pi/main.tf +7 -0
  47. package/dist/terraform/modules/app/capability-broker/main.tf +393 -0
  48. package/dist/terraform/modules/app/capability-broker/outputs.tf +54 -0
  49. package/dist/terraform/modules/app/capability-broker/variables.tf +98 -0
  50. package/dist/terraform/modules/app/deployment-control-plane/runner.py +46 -5
  51. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +94 -0
  52. package/dist/terraform/modules/app/lambda-api/capability-search-alarms.tf +60 -0
  53. package/dist/terraform/modules/app/lambda-api/handlers.tf +361 -16
  54. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +131 -5
  55. package/dist/terraform/modules/app/lambda-api/main.tf +11 -0
  56. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +92 -0
  57. package/dist/terraform/modules/app/lambda-api/msteams-app-secrets.tf +46 -0
  58. package/dist/terraform/modules/app/lambda-api/variables.tf +92 -0
  59. package/dist/terraform/modules/app/workflow-interpreter-stepfunctions/alarms.tf +47 -0
  60. package/dist/terraform/modules/app/workflow-interpreter-stepfunctions/main.tf +43 -2
  61. package/dist/terraform/modules/foundation/cognito/main.tf +32 -8
  62. package/dist/terraform/modules/foundation/cognito/variables.tf +12 -0
  63. package/dist/terraform/modules/foundation/vpc/outputs.tf +5 -0
  64. package/dist/terraform/modules/thinkwork/main.tf +152 -5
  65. package/dist/terraform/modules/thinkwork/outputs.tf +55 -0
  66. package/dist/terraform/modules/thinkwork/variables.tf +42 -0
  67. package/dist/workspace-defaults/files/TOOLS.md +39 -0
  68. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +18 -0
  69. package/dist/workspace-defaults/files/agents/coding/INSTRUCTIONS.md +11 -0
  70. package/dist/workspace-defaults/files/agents/research/INSTRUCTIONS.md +12 -0
  71. package/dist/workspace-defaults/files/agents/reviewer/INSTRUCTIONS.md +15 -0
  72. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +6 -2
  73. package/package.json +4 -2
  74. /package/dist/workspace-defaults/files/{AGENTS.md → INSTRUCTIONS.md} +0 -0
@@ -0,0 +1,20 @@
1
+ -- THINK-302 U7 (R28/KTD-14): run-as identity for wakeup/scheduled turns.
2
+ --
3
+ -- The user a scheduled/wakeup turn runs AS, so it composes that user's +
4
+ -- the run-in-space's capability scopes. A job with no run_as_user_id
5
+ -- compiles root + sub-agent only — it NEVER inherits the creator's grants.
6
+ -- Set/cleared only through an authorized surface (KTD-14 save-time authz);
7
+ -- dispatch-time revalidation drops stale grants.
8
+ --
9
+ -- Additive, idempotent nullable-FK column add (ON DELETE SET NULL so a
10
+ -- deactivated user's jobs degrade to root-only rather than break).
11
+ -- Hand-rolled only to carry the drift-gate marker; db:push would also add it.
12
+ --
13
+ -- Apply manually:
14
+ -- psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f drizzle/0260_scheduled_jobs_run_as_user.sql
15
+ -- creates-column: public.scheduled_jobs.run_as_user_id
16
+
17
+ \set ON_ERROR_STOP on
18
+
19
+ ALTER TABLE public.scheduled_jobs
20
+ ADD COLUMN IF NOT EXISTS run_as_user_id uuid REFERENCES public.users(id) ON DELETE SET NULL;
@@ -0,0 +1,11 @@
1
+ -- THINK-307 KTD4: the retry dispatcher's successor-turn guard, THINK-308's
2
+ -- finalize reconciliation, and THINK-309's recovery resolver all look up
3
+ -- thread_turns by origin_turn_id; no index covers that column today.
4
+ -- Additive — safe to apply ahead of code deploy.
5
+ --
6
+ -- Hand-rolled (drizzle meta journal is not in use for this change);
7
+ -- apply with: psql "$DATABASE_URL" -f drizzle/0261_thread_turns_origin_turn_index.sql
8
+ -- creates: public.idx_thread_turns_origin_turn
9
+
10
+ CREATE INDEX IF NOT EXISTS idx_thread_turns_origin_turn
11
+ ON public.thread_turns (origin_turn_id);
@@ -105,6 +105,18 @@ variable "enable_hindsight" {
105
105
  default = true
106
106
  }
107
107
 
108
+ variable "enable_capability_broker" {
109
+ description = "THINK-280 governed capability runtime. When true, brings up the capability broker (PoP session table, private REST API, dedicated execute-api VPCE with private DNS off, no-NAT interpreter SG) and enables capability-private interpreter provisioning. Default false — currently flipped on only for the dev dogfood via deploy.yml."
110
+ type = bool
111
+ default = false
112
+ }
113
+
114
+ variable "analyst_lambda_vpc_egress" {
115
+ description = "VPC-attach the analyst data-path Lambdas so their egress rides the NAT gateway's stable EIP — required when an external analyst data source sits behind an IP allowlist. See the thinkwork module's analyst_egress_ip output for the IP to allowlist."
116
+ type = bool
117
+ default = false
118
+ }
119
+
108
120
  variable "memory_engine" {
109
121
  description = "Active long-term memory engine. Empty selects Hindsight when enable_hindsight = true. Use 'agentcore' only for explicit low-cost/development managed-memory deployments."
110
122
  type = string
@@ -416,6 +428,18 @@ variable "brain_dream_state_enabled" {
416
428
  default = false
417
429
  }
418
430
 
431
+ variable "retry_dispatcher_enabled" {
432
+ description = "Enable the recurring retry-dispatcher schedule (THINK-307). Never enable on a stage before THINK-305, THINK-308, and THINK-309 are live there; manual invokes work regardless."
433
+ type = bool
434
+ default = false
435
+ }
436
+
437
+ variable "stall_threshold_minutes" {
438
+ description = "Minutes of turn inactivity before the stall monitor marks a running turn timed_out; the retry dispatcher reads the same knob as its origin-freshness guard (THINK-306/THINK-307)."
439
+ type = number
440
+ default = 5
441
+ }
442
+
419
443
  variable "requester_memory_dreaming_model_id" {
420
444
  description = "Bedrock Converse model id for requester memory REM reflection."
421
445
  type = string
@@ -460,6 +484,12 @@ variable "analyst_policy_source" {
460
484
  }
461
485
  }
462
486
 
487
+ variable "capability_self_extension_tenants" {
488
+ description = "Governed autonomy opt-in: comma/space-separated tenant ids allowed to autonomously self-extend capabilities (self_admit_connection / self_approve_routine). Empty (default) = NO tenant enabled; self-extension ships inert and fail-closed."
489
+ type = string
490
+ default = ""
491
+ }
492
+
463
493
  variable "wiki_aggregation_pass_enabled" {
464
494
  description = <<-EOT
465
495
  Feature flag for the wiki aggregation pass — the second LLM call
@@ -749,6 +779,8 @@ module "thinkwork" {
749
779
  db_password = var.db_password
750
780
  database_engine = var.database_engine
751
781
  enable_hindsight = var.enable_hindsight
782
+ enable_capability_broker = var.enable_capability_broker
783
+ analyst_lambda_vpc_egress = var.analyst_lambda_vpc_egress
752
784
  hindsight_database_name = var.hindsight_database_name
753
785
  memory_engine = var.memory_engine
754
786
  twenty_provisioned = var.twenty_provisioned
@@ -831,6 +863,7 @@ module "thinkwork" {
831
863
  wiki_aggregation_pass_enabled = var.wiki_aggregation_pass_enabled
832
864
  wiki_source = var.wiki_source
833
865
  analyst_policy_source = var.analyst_policy_source
866
+ capability_self_extension_tenants = var.capability_self_extension_tenants
834
867
  knowledge_graph_observations_ingest_enabled = var.knowledge_graph_observations_ingest_enabled
835
868
  wiki_deterministic_linking_enabled = var.wiki_deterministic_linking_enabled
836
869
  google_places_api_key = var.google_places_api_key
@@ -838,6 +871,8 @@ module "thinkwork" {
838
871
  requester_memory_dreaming_enabled = var.requester_memory_dreaming_enabled
839
872
  requester_memory_dreaming_schedule_expression = var.requester_memory_dreaming_schedule_expression
840
873
  brain_dream_state_enabled = var.brain_dream_state_enabled
874
+ retry_dispatcher_enabled = var.retry_dispatcher_enabled
875
+ stall_threshold_minutes = var.stall_threshold_minutes
841
876
  requester_memory_dreaming_model_id = var.requester_memory_dreaming_model_id
842
877
  agentcore_code_interpreter_id = var.agentcore_code_interpreter_id
843
878
  agentcore_memory_id = var.agentcore_memory_id
@@ -1316,3 +1351,8 @@ output "customer_domain_name_servers" {
1316
1351
  description = "The four Route53 name servers for the customer-domain zone — publish these via the claim tool's `claim --set-targets` to delegate (empty when no customer domain is configured)."
1317
1352
  value = module.thinkwork.customer_domain_name_servers
1318
1353
  }
1354
+
1355
+ output "analyst_egress_ip" {
1356
+ description = "Stable NAT egress IP of the VPC-attached analyst data-path Lambdas (null unless analyst_lambda_vpc_egress is set)"
1357
+ value = module.thinkwork.analyst_egress_ip
1358
+ }
@@ -0,0 +1,341 @@
1
+ ################################################################################
2
+ # AgentCore Admin — App Module (stage-level)
3
+ #
4
+ # The `agentcore-admin` Lambda is the control-plane worker that regular sandbox
5
+ # provisioning depends on: `createTenant` (packages/api) RequestResponse-invokes
6
+ # it to create per-tenant IAM roles + AgentCore Code Interpreters, and it also
7
+ # serves SSM permission-profile CRUD + CloudWatch audit queries. The handler
8
+ # lives at packages/lambda/agentcore-admin.ts and is bundled by
9
+ # scripts/build-lambdas.sh into dist/lambdas/agentcore-admin.zip.
10
+ #
11
+ # Historically this Lambda was built but instantiated NOWHERE — so sandbox
12
+ # provisioning would fail closed at runtime (AGENTCORE_ADMIN_LAMBDA_ARN unset).
13
+ # This module deploys it. It is intentionally a SEPARATE module with a DEDICATED
14
+ # least-privilege role: the handler holds a dangerous control-plane surface
15
+ # (iam:CreateRole, bedrock-agentcore CreateCodeInterpreter) that must never leak
16
+ # onto the ~90-handler shared lambda-api role.
17
+ #
18
+ # THINK-280 capability-private wiring: CAPABILITY_PRIVATE_SUBNET_IDS /
19
+ # CAPABILITY_PRIVATE_SECURITY_GROUP_IDS come from the capability-broker outputs
20
+ # (via the agentcore-code-interpreter module). Both are EMPTY when the broker is
21
+ # disabled (var.enable_capability_broker=false, the default) — the handler then
22
+ # skips capability-private provisioning entirely (capabilityPrivateProvisioning
23
+ # Enabled() returns false). The module therefore ships INERT for the
24
+ # capability-private path while still fixing the regular-provisioning gap.
25
+ ################################################################################
26
+
27
+ terraform {
28
+ required_providers {
29
+ aws = {
30
+ source = "hashicorp/aws"
31
+ version = ">= 5.0"
32
+ }
33
+ random = {
34
+ source = "hashicorp/random"
35
+ version = ">= 3.0"
36
+ }
37
+ }
38
+ }
39
+
40
+ variable "stage" {
41
+ description = "Deployment stage (dev, prod, etc.)."
42
+ type = string
43
+ }
44
+
45
+ variable "region" {
46
+ description = "AWS region."
47
+ type = string
48
+ }
49
+
50
+ variable "account_id" {
51
+ description = "AWS account ID (used to construct IAM resource ARNs)."
52
+ type = string
53
+ }
54
+
55
+ # ---- Lambda artifact sources (mirror lambda-api/remote-artifacts.tf) ----------
56
+ variable "lambda_zips_dir" {
57
+ description = "Local directory of built Lambda zips (source-checkout deploys). Mutually exclusive with lambda_artifact_bucket."
58
+ type = string
59
+ default = ""
60
+ }
61
+
62
+ variable "lambda_artifact_bucket" {
63
+ description = "S3 bucket holding release Lambda zips (enterprise deploys). Mutually exclusive with lambda_zips_dir."
64
+ type = string
65
+ default = ""
66
+ }
67
+
68
+ variable "lambda_artifact_prefix" {
69
+ description = "S3 key prefix for release Lambda zips."
70
+ type = string
71
+ default = ""
72
+ }
73
+
74
+ # ---- Database (handler reads the tenants table via getDb() → DATABASE_URL) -----
75
+ variable "db_username" {
76
+ description = "Database username (used to construct DATABASE_URL)."
77
+ type = string
78
+ default = "thinkwork_admin"
79
+ }
80
+
81
+ variable "db_password" {
82
+ description = "Database password (used to construct DATABASE_URL)."
83
+ type = string
84
+ sensitive = true
85
+ default = ""
86
+ }
87
+
88
+ variable "db_cluster_endpoint" {
89
+ description = "Aurora cluster writer endpoint."
90
+ type = string
91
+ }
92
+
93
+ variable "database_name" {
94
+ description = "Database name."
95
+ type = string
96
+ }
97
+
98
+ variable "graphql_db_secret_arn" {
99
+ description = "ARN of the writer DB credentials secret (granted GetSecretValue so db.ts's secret fallback works)."
100
+ type = string
101
+ }
102
+
103
+ # ---- Capability-private interpreter VPC placement (THINK-280 U4) ---------------
104
+ # Empty when the capability broker is disabled → handler skips capability-private
105
+ # provisioning. Sourced from the broker outputs via the code-interpreter module.
106
+ variable "capability_private_subnet_ids" {
107
+ description = "No-NAT subnets for the capability-private VPC-mode interpreter. Empty when the broker is disabled."
108
+ type = list(string)
109
+ default = []
110
+ }
111
+
112
+ variable "capability_private_security_group_ids" {
113
+ description = "Egress-only SGs for the capability-private interpreter. Empty when the broker is disabled."
114
+ type = list(string)
115
+ default = []
116
+ }
117
+
118
+ ################################################################################
119
+ # Artifact-source gating — identical semantics to lambda-api/remote-artifacts.tf.
120
+ # Exactly one source (local zips OR remote S3) must be set, else the Lambda is
121
+ # not deployed (scaffolded installs with no artifacts).
122
+ ################################################################################
123
+
124
+ locals {
125
+ use_local_zips = trimspace(var.lambda_zips_dir) != ""
126
+ use_remote_lambda_artifacts = trimspace(var.lambda_artifact_bucket) != ""
127
+ lambda_artifact_prefix = trim(trimspace(var.lambda_artifact_prefix), "/")
128
+ artifact_source_count = (local.use_local_zips ? 1 : 0) + (local.use_remote_lambda_artifacts ? 1 : 0)
129
+ deploy = local.artifact_source_count == 1
130
+
131
+ function_name = "thinkwork-${var.stage}-api-agentcore-admin"
132
+
133
+ # DATABASE_URL built exactly as lambda-api's common_env does so getDb()
134
+ # resolves identically. sslmode=no-verify matches the shared pool (Aurora is
135
+ # reachable over TLS without a bundled CA in this stack).
136
+ database_url = "postgresql://${var.db_username}:${urlencode(var.db_password)}@${var.db_cluster_endpoint}:5432/${var.database_name}?sslmode=no-verify"
137
+ }
138
+
139
+ ################################################################################
140
+ # Admin bearer token — random_password → Secrets Manager. The token value is
141
+ # ALSO injected as the Lambda's AGENTCORE_ADMIN_TOKEN env (the handler compares
142
+ # the Bearer header against process.env directly), and handed to graphql-http so
143
+ # its invoke carries the matching credential. The secret is the durable record
144
+ # of truth for operators / rotation.
145
+ ################################################################################
146
+
147
+ resource "random_password" "admin_token" {
148
+ count = local.deploy ? 1 : 0
149
+ # 48 chars, alphanumeric only — safe inside an `Authorization: Bearer` header.
150
+ length = 48
151
+ special = false
152
+ }
153
+
154
+ resource "aws_secretsmanager_secret" "admin_token" {
155
+ count = local.deploy ? 1 : 0
156
+ name = "thinkwork/${var.stage}/agentcore-admin-token"
157
+ description = "Bearer token the agentcore-admin Lambda validates; graphql-http presents it on RequestResponse invokes."
158
+
159
+ tags = {
160
+ Name = "thinkwork-${var.stage}-agentcore-admin-token"
161
+ Stage = var.stage
162
+ Purpose = "agentcore-admin-bearer-token"
163
+ }
164
+ }
165
+
166
+ resource "aws_secretsmanager_secret_version" "admin_token" {
167
+ count = local.deploy ? 1 : 0
168
+ secret_id = aws_secretsmanager_secret.admin_token[0].id
169
+ secret_string = random_password.admin_token[0].result
170
+ }
171
+
172
+ ################################################################################
173
+ # IAM role — DEDICATED, least-privilege. Scoped tightly to the handler's actual
174
+ # AWS SDK calls (see packages/lambda/agentcore-admin.ts).
175
+ ################################################################################
176
+
177
+ resource "aws_iam_role" "admin" {
178
+ count = local.deploy ? 1 : 0
179
+ name = "thinkwork-${var.stage}-agentcore-admin"
180
+
181
+ assume_role_policy = jsonencode({
182
+ Version = "2012-10-17"
183
+ Statement = [{
184
+ Effect = "Allow"
185
+ Principal = { Service = "lambda.amazonaws.com" }
186
+ Action = "sts:AssumeRole"
187
+ }]
188
+ })
189
+
190
+ tags = {
191
+ Name = "thinkwork-${var.stage}-agentcore-admin"
192
+ Stage = var.stage
193
+ Purpose = "agentcore-admin-execution-role"
194
+ }
195
+ }
196
+
197
+ resource "aws_iam_role_policy" "admin" {
198
+ count = local.deploy ? 1 : 0
199
+ name = "agentcore-admin"
200
+ role = aws_iam_role.admin[0].id
201
+
202
+ policy = jsonencode({
203
+ Version = "2012-10-17"
204
+ Statement = [
205
+ # --- Lambda's own log group (basic execution) ---
206
+ {
207
+ Sid = "LambdaOwnLogs"
208
+ Effect = "Allow"
209
+ Action = [
210
+ "logs:CreateLogGroup",
211
+ "logs:CreateLogStream",
212
+ "logs:PutLogEvents",
213
+ ]
214
+ Resource = "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/lambda/${local.function_name}:*"
215
+ },
216
+ # --- CloudWatch audit query (queryAuditLogs) ---
217
+ {
218
+ Sid = "AuditLogQuery"
219
+ Effect = "Allow"
220
+ Action = ["logs:FilterLogEvents"]
221
+ Resource = "arn:aws:logs:${var.region}:${var.account_id}:log-group:/thinkwork/${var.stage}/agentcore/agents:*"
222
+ },
223
+ # --- SSM permission-profile CRUD (getPermissions / putPermissions) ---
224
+ {
225
+ Sid = "SsmPermissionProfiles"
226
+ Effect = "Allow"
227
+ Action = [
228
+ "ssm:GetParameter",
229
+ "ssm:PutParameter",
230
+ ]
231
+ Resource = "arn:aws:ssm:${var.region}:${var.account_id}:parameter/thinkwork/${var.stage}/*"
232
+ },
233
+ # --- Per-tenant sandbox IAM roles (provision / deprovision) ---
234
+ # Two name prefixes: computeRoleName → sandbox-tenant-*, and the U4
235
+ # capability-private role → cappriv-tenant-*.
236
+ {
237
+ Sid = "SandboxRoleLifecycle"
238
+ Effect = "Allow"
239
+ Action = [
240
+ "iam:CreateRole",
241
+ "iam:DeleteRole",
242
+ "iam:GetRole",
243
+ "iam:PutRolePolicy",
244
+ "iam:DeleteRolePolicy",
245
+ "iam:TagRole",
246
+ ]
247
+ Resource = [
248
+ "arn:aws:iam::${var.account_id}:role/thinkwork-${var.stage}-sandbox-*",
249
+ "arn:aws:iam::${var.account_id}:role/thinkwork-${var.stage}-cappriv-*",
250
+ ]
251
+ },
252
+ # --- PassRole the sandbox roles to AgentCore (CreateCodeInterpreter) ---
253
+ {
254
+ Sid = "PassSandboxRolesToAgentCore"
255
+ Effect = "Allow"
256
+ Action = ["iam:PassRole"]
257
+ Resource = [
258
+ "arn:aws:iam::${var.account_id}:role/thinkwork-${var.stage}-sandbox-*",
259
+ "arn:aws:iam::${var.account_id}:role/thinkwork-${var.stage}-cappriv-*",
260
+ ]
261
+ Condition = {
262
+ StringEquals = {
263
+ "iam:PassedToService" = "bedrock-agentcore.amazonaws.com"
264
+ }
265
+ }
266
+ },
267
+ # --- AgentCore Code Interpreter control plane ---
268
+ # List/Create/Delete are account-scoped operations without a stable
269
+ # per-resource ARN at plan time; the SDK client is the control plane.
270
+ {
271
+ Sid = "CodeInterpreterControlPlane"
272
+ Effect = "Allow"
273
+ Action = [
274
+ "bedrock-agentcore:CreateCodeInterpreter",
275
+ "bedrock-agentcore:DeleteCodeInterpreter",
276
+ "bedrock-agentcore:ListCodeInterpreters",
277
+ "bedrock-agentcore:GetCodeInterpreter",
278
+ # CreateCodeInterpreter passes inline `tags`, which AgentCore
279
+ # authorizes as a separate bedrock-agentcore:TagResource action on
280
+ # code-interpreter-custom/* — verified empirically (first provision
281
+ # returned AccessDenied on TagResource). UntagResource covers teardown.
282
+ "bedrock-agentcore:TagResource",
283
+ "bedrock-agentcore:UntagResource",
284
+ ]
285
+ Resource = "*"
286
+ },
287
+ # --- Secrets: DB credentials (db.ts secret fallback) + own admin token ---
288
+ {
289
+ Sid = "SecretsRead"
290
+ Effect = "Allow"
291
+ Action = ["secretsmanager:GetSecretValue"]
292
+ Resource = [
293
+ var.graphql_db_secret_arn,
294
+ aws_secretsmanager_secret.admin_token[0].arn,
295
+ ]
296
+ },
297
+ ]
298
+ })
299
+ }
300
+
301
+ ################################################################################
302
+ # Lambda function
303
+ ################################################################################
304
+
305
+ resource "aws_lambda_function" "admin" {
306
+ count = local.deploy ? 1 : 0
307
+
308
+ function_name = local.function_name
309
+ role = aws_iam_role.admin[0].arn
310
+ handler = "index.handler"
311
+ runtime = "nodejs20.x"
312
+ timeout = 60
313
+ memory_size = 256
314
+
315
+ filename = local.use_local_zips ? "${var.lambda_zips_dir}/agentcore-admin.zip" : null
316
+ source_code_hash = local.use_local_zips ? filebase64sha256("${var.lambda_zips_dir}/agentcore-admin.zip") : null
317
+ s3_bucket = local.use_remote_lambda_artifacts ? var.lambda_artifact_bucket : null
318
+ s3_key = local.use_remote_lambda_artifacts ? "${local.lambda_artifact_prefix}/agentcore-admin.zip" : null
319
+
320
+ environment {
321
+ variables = {
322
+ STAGE = var.stage
323
+ AWS_ACCOUNT_ID = var.account_id
324
+ NODE_OPTIONS = "--enable-source-maps"
325
+ DATABASE_URL = local.database_url
326
+ # Bearer token the handler validates. Secret env is the sanctioned home
327
+ # for secrets in this stack (never the SSM runtime-config String doc).
328
+ AGENTCORE_ADMIN_TOKEN = random_password.admin_token[0].result
329
+ # THINK-280 U4 — capability-private interpreter VPC placement. EMPTY when
330
+ # the broker is disabled → the handler skips capability-private entirely.
331
+ CAPABILITY_PRIVATE_SUBNET_IDS = join(",", var.capability_private_subnet_ids)
332
+ CAPABILITY_PRIVATE_SECURITY_GROUP_IDS = join(",", var.capability_private_security_group_ids)
333
+ }
334
+ }
335
+
336
+ tags = {
337
+ Name = local.function_name
338
+ Stage = var.stage
339
+ Purpose = "agentcore-admin"
340
+ }
341
+ }
@@ -0,0 +1,20 @@
1
+ output "lambda_arn" {
2
+ description = "ARN of the agentcore-admin Lambda (AGENTCORE_ADMIN_LAMBDA_ARN for graphql-http). Empty when not deployed."
3
+ value = local.deploy ? aws_lambda_function.admin[0].arn : ""
4
+ }
5
+
6
+ output "lambda_function_name" {
7
+ description = "Function name of the agentcore-admin Lambda. Empty when not deployed."
8
+ value = local.deploy ? aws_lambda_function.admin[0].function_name : ""
9
+ }
10
+
11
+ output "admin_token" {
12
+ description = "Bearer token graphql-http presents on invoke (AGENTCORE_ADMIN_TOKEN). Empty when not deployed."
13
+ value = local.deploy ? random_password.admin_token[0].result : ""
14
+ sensitive = true
15
+ }
16
+
17
+ output "admin_token_secret_arn" {
18
+ description = "ARN of the Secrets Manager secret holding the admin bearer token. Empty when not deployed."
19
+ value = local.deploy ? aws_secretsmanager_secret.admin_token[0].arn : ""
20
+ }
@@ -50,6 +50,23 @@ variable "image_retention_count" {
50
50
  default = 10
51
51
  }
52
52
 
53
+ # THINK-280 U4 — VPC placement for the capability-private interpreter. Sourced
54
+ # (when the capability broker is enabled) from the broker module's no-NAT
55
+ # interpreter subnets + dedicated egress-only SG. Empty by default: the
56
+ # agentcore-admin Lambda skips capability-private provisioning entirely when
57
+ # either list is empty, so the module stays inert with the broker disabled.
58
+ variable "capability_private_subnet_ids" {
59
+ description = "No-NAT private subnets the capability-private VPC-mode interpreter attaches to. Empty when the broker is disabled."
60
+ type = list(string)
61
+ default = []
62
+ }
63
+
64
+ variable "capability_private_security_group_ids" {
65
+ description = "Egress-only security groups (reach only the broker execute-api VPCE) for the capability-private interpreter. Empty when the broker is disabled."
66
+ type = list(string)
67
+ default = []
68
+ }
69
+
53
70
  # Environment catalog. Kept as a locals block (not a variable) because v1
54
71
  # semantics are fixed in the plan — extending requires a reviewable PR,
55
72
  # not a tfvars tweak.
@@ -63,6 +80,14 @@ locals {
63
80
  description = "S3 + DNS + AWS service endpoints only; no public egress."
64
81
  network_mode = "SANDBOX"
65
82
  }
83
+ # THINK-280 U4: VPC-mode interpreter, no NAT. Reaches ONLY the capability
84
+ # broker's private execute-api VPCE (+ DNS). Runtime-selected per invocation
85
+ # (not a template-level opt-in), and only provisioned when the broker is
86
+ # enabled — the agentcore-admin Lambda gates on the VPC subnet/SG env vars.
87
+ "capability-private" = {
88
+ description = "VPC-mode, no NAT; reaches only the capability broker execute-api VPCE."
89
+ network_mode = "VPC"
90
+ }
66
91
  }
67
92
  }
68
93
 
@@ -155,6 +180,27 @@ locals {
155
180
  },
156
181
  ]
157
182
  })
183
+
184
+ # THINK-280 U4: SEPARATE restricted inline policy for the capability-private
185
+ # VPC-mode interpreter role. It deliberately LACKS the tenant sandbox Secrets
186
+ # Manager wildcard (SandboxSecretsRead above), tenant S3, and any database /
187
+ # provider access — the private interpreter reaches providers only through the
188
+ # broker's PoP session (AE5). Logs are the only genuine boot requirement.
189
+ # Consumed by the agentcore-admin Lambda for the capability-private role.
190
+ capability_private_role_inline_policy_template = jsonencode({
191
+ Version = "2012-10-17"
192
+ Statement = [
193
+ {
194
+ Sid = "CapabilityPrivateCloudWatchLogs"
195
+ Effect = "Allow"
196
+ Action = [
197
+ "logs:CreateLogStream",
198
+ "logs:PutLogEvents",
199
+ ]
200
+ Resource = "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"
201
+ },
202
+ ]
203
+ })
158
204
  }
159
205
 
160
206
  ################################################################################
@@ -191,6 +237,21 @@ output "tenant_role_inline_policy_template" {
191
237
  value = local.tenant_role_inline_policy_template
192
238
  }
193
239
 
240
+ output "capability_private_role_inline_policy_template" {
241
+ description = "THINK-280 U4: JSON inline-policy for the capability-private interpreter role. Logs-only; NO Secrets Manager / S3 / DB access."
242
+ value = local.capability_private_role_inline_policy_template
243
+ }
244
+
245
+ output "capability_private_subnet_ids" {
246
+ description = "No-NAT subnets the capability-private interpreter attaches to (CAPABILITY_PRIVATE_SUBNET_IDS). Empty when the broker is disabled."
247
+ value = var.capability_private_subnet_ids
248
+ }
249
+
250
+ output "capability_private_security_group_ids" {
251
+ description = "Egress-only SGs for the capability-private interpreter (CAPABILITY_PRIVATE_SECURITY_GROUP_IDS). Empty when the broker is disabled."
252
+ value = var.capability_private_security_group_ids
253
+ }
254
+
194
255
  output "stage" {
195
256
  description = "Echo of the stage variable (convenience for downstream modules)."
196
257
  value = var.stage
@@ -0,0 +1,101 @@
1
+ ################################################################################
2
+ # AgentCore Harness — App Module (THINK-311 U4, ship-inert)
3
+ #
4
+ # IAM surface for the AWS Bedrock AgentCore Harness trial: the execution role
5
+ # Harness microVMs assume, plus (via outputs consumed by lambda-api's grouped
6
+ # policies) the invoker grants the shared api Lambda role needs to create and
7
+ # invoke Harness resources. Nothing reads or invokes any of this until the
8
+ # harness-runner handler lands in U5 — this module is pure IAM.
9
+ #
10
+ # Trust-policy note: sibling agentcore roles (agentcore-pi, agentcore-runtime)
11
+ # trust the bare `bedrock-agentcore.amazonaws.com` service principal with no
12
+ # account/source-arn conditions; this role mirrors that repo pattern.
13
+ ################################################################################
14
+
15
+ ################################################################################
16
+ # Harness execution role — assumed by Harness microVMs
17
+ ################################################################################
18
+
19
+ resource "aws_iam_role" "harness_execution" {
20
+ count = var.enabled ? 1 : 0
21
+
22
+ name = "thinkwork-${var.stage}-agentcore-harness-role"
23
+
24
+ assume_role_policy = jsonencode({
25
+ Version = "2012-10-17"
26
+ Statement = [{
27
+ Effect = "Allow"
28
+ Principal = { Service = "bedrock-agentcore.amazonaws.com" }
29
+ Action = "sts:AssumeRole"
30
+ }]
31
+ })
32
+
33
+ tags = {
34
+ Name = "thinkwork-${var.stage}-agentcore-harness-role"
35
+ }
36
+ }
37
+
38
+ resource "aws_iam_role_policy" "harness_execution" {
39
+ count = var.enabled ? 1 : 0
40
+
41
+ name = "agentcore-harness-permissions"
42
+ role = aws_iam_role.harness_execution[0].id
43
+
44
+ policy = jsonencode({
45
+ Version = "2012-10-17"
46
+ Statement = [
47
+ {
48
+ # Cross-region inference profiles (us.anthropic.claude-*) require
49
+ # `bedrock:InvokeModel` on the *inference-profile* ARN AND on the
50
+ # underlying foundation-model ARN in every region the profile can
51
+ # route to — same resource list the api Lambda's grouped ai policy
52
+ # uses (see ../lambda-api/iam-grouped.tf "bedrock-invoke").
53
+ Sid = "BedrockInvoke"
54
+ Effect = "Allow"
55
+ Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
56
+ Resource = [
57
+ "arn:aws:bedrock:*::foundation-model/*",
58
+ "arn:aws:bedrock:*:${var.account_id}:inference-profile/*",
59
+ ]
60
+ },
61
+ {
62
+ # Tenant skill catalog sources + materialized workspace skill folders
63
+ # live under tenants/* in the stage workspace bucket. Read-only —
64
+ # Harness microVMs never write back to the workspace bucket.
65
+ Sid = "WorkspaceSkillSourcesRead"
66
+ Effect = "Allow"
67
+ Action = ["s3:GetObject"]
68
+ Resource = "arn:aws:s3:::${var.bucket_name}/tenants/*"
69
+ },
70
+ {
71
+ Sid = "WorkspaceSkillSourcesList"
72
+ Effect = "Allow"
73
+ Action = ["s3:ListBucket"]
74
+ Resource = "arn:aws:s3:::${var.bucket_name}"
75
+ Condition = {
76
+ StringLike = {
77
+ "s3:prefix" = "tenants/*"
78
+ }
79
+ }
80
+ },
81
+ {
82
+ # Harness containers log to the service-managed bedrock-agentcore
83
+ # log-group namespace, mirroring the agentcore-pi role's runtimes
84
+ # grant.
85
+ Sid = "CloudWatchLogs"
86
+ Effect = "Allow"
87
+ Action = [
88
+ "logs:CreateLogGroup",
89
+ "logs:CreateLogStream",
90
+ "logs:DescribeLogGroups",
91
+ "logs:DescribeLogStreams",
92
+ "logs:PutLogEvents",
93
+ ]
94
+ Resource = [
95
+ "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/bedrock-agentcore/*",
96
+ "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/bedrock-agentcore/*:*",
97
+ ]
98
+ },
99
+ ]
100
+ })
101
+ }
@@ -0,0 +1,9 @@
1
+ output "execution_role_arn" {
2
+ description = "ARN of the Harness execution role (CreateHarness executionRoleArn). Empty string when the module is disabled."
3
+ value = var.enabled ? aws_iam_role.harness_execution[0].arn : ""
4
+ }
5
+
6
+ output "execution_role_name" {
7
+ description = "Name of the Harness execution role. Empty string when the module is disabled."
8
+ value = var.enabled ? aws_iam_role.harness_execution[0].name : ""
9
+ }