razzou-harness 0.4.1 → 0.4.3

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/CHANGELOG.md CHANGED
@@ -3,6 +3,49 @@
3
3
  The notable changes in each release of razzou-harness. Before 1.0, a new minor version
4
4
  may change how a project is set up, and its notes say how.
5
5
 
6
+ ## 0.4.3 (2026-11-08)
7
+
8
+ **Upgrading from 0.4.2:** install the new version, then rebuild the agents' image with
9
+ `harness doctor --build-images`: its tag follows the harness's version.
10
+
11
+ - **`harness setup` writes the Terraform scaffold** your bootstrap expects, when you
12
+ answer its AWS questions, and on any later run: a partial `backend "s3" {}` block in
13
+ `iac/versions.tf`, `iac/backend-configs/staging.hcl` with the state bucket and the
14
+ project's key, read from the GitHub variables the bootstrap set, `iac/env/staging.tfvars`
15
+ with the account, the region and the repository's ids, and the variables and provider
16
+ they feed. Only missing files are written, a backend block of the project's own is
17
+ kept, and the files are committed with setup's others. The backend config sets no
18
+ encryption, so the bucket's default, its KMS key, applies. Until now the doctor
19
+ required these files and nothing wrote them.
20
+
21
+ ## 0.4.2 (2026-11-08)
22
+
23
+ **Upgrading from 0.4.1:** upgrade OpenShell to 0.1.7 first, from its Debian package
24
+ (`sudo dpkg -i openshell_0.1.7-1_amd64.deb`); the gateway keeps its providers and
25
+ profiles. Then install the new version, and rebuild the agents' image with `harness
26
+ doctor --build-images`: its tag follows the harness's version. A project's AWS services
27
+ stay as its `harness/config.json` lists them; for every one, set `"services": ["all"]`
28
+ under `aws`.
29
+
30
+ - **OpenShell 0.1.7.** The harness is pinned to it, and tested with it: the probe passes
31
+ with every bundle and every AWS service, and the Architect's terminal and the shell
32
+ in a sandbox work. The doctor's and setup's fix for a stopped gateway now starts its
33
+ systemd user service: OpenShell's CLI has no command for it.
34
+ - **Every AWS service by default.** `"all"` in `aws.services` is every service the AWS
35
+ SDK knows, about 420 endpoints in a region with the global ones, each signed with the
36
+ view role under its own signing name, as OpenShell requires. It is the default, and
37
+ `harness setup` no longer asks which services the project uses: the view role alone
38
+ decides what agents read. The table comes from the AWS SDK's own data
39
+ (`scripts/aws-services.ts` writes it).
40
+ - **AWS reads with `GET` work.** OpenShell's automatic `sigv4` mode signs a request
41
+ with neither a body nor `x-amz-content-sha256` as `UNSIGNED-PAYLOAD`, which only S3
42
+ accepts ([NVIDIA/OpenShell#5375](https://github.com/NVIDIA/OpenShell/issues/5375)):
43
+ every service that reads with `GET`, such as Lambda, API Gateway, EKS, Route 53,
44
+ CloudFront and Bedrock, answered `SignatureDoesNotMatch` in the sandboxes, and a
45
+ `terraform plan` there failed on their resources. Every AWS endpoint but S3's is now
46
+ signed with `sigv4:body`, the hash of the request's body, empty for a `GET`: the AWS
47
+ CLI's calls to those services and a `terraform plan` reading them work.
48
+
6
49
  ## 0.4.1 (2026-11-08)
7
50
 
8
51
  **Upgrading a project from 0.4.0:** install the new version, then rebuild the agents'
package/README.md CHANGED
@@ -106,7 +106,7 @@ without pull requests.
106
106
 
107
107
  - Linux, Node 24 or later, git, Docker, and `gh` logged in as an admin of your
108
108
  project's repository.
109
- - OpenShell 0.1.2 with a running gateway. The harness is tested with this version only,
109
+ - OpenShell 0.1.7 with a running gateway. The harness is tested with this version only,
110
110
  and `harness doctor` refuses others.
111
111
  - Claude Code on this machine, with a Claude subscription: its binary goes into the
112
112
  sandbox image, and `claude setup-token` gives the agents' token. Access with an API
@@ -140,7 +140,8 @@ gateway key. In order, it logs you in to `gh`; creates
140
140
  the repository on GitHub if the project has no remote; creates the App from its
141
141
  manifest when this machine has none, and waits for you to install it; writes the
142
142
  harness's files; asks the questions and writes `harness/config.json`, with the
143
- `staging` environment and the variables on GitHub for AWS; commits the files it wrote,
143
+ `staging` environment and the variables on GitHub for AWS, and the Terraform scaffold in
144
+ `iac/` that your bootstrap expects; commits the files it wrote,
144
145
  and only those, with your git once you agree, and pushes them when you are on `main`;
145
146
  runs `harness doctor` and fixes what it can (starts the OpenShell gateway, builds the
146
147
  agents' image, imports the Claude profile and creates the provider with your token,
@@ -433,7 +434,7 @@ through an `Arch:` pull request, or a commit of your own on `main`.
433
434
 
434
435
  ```json
435
436
  {
436
- "harnessVersion": "0.4.1",
437
+ "harnessVersion": "0.4.3",
437
438
  "project": { "name": "shop", "repo": "<org>/shop", "bot": "<org>-harness[bot]" },
438
439
  "roles": {
439
440
  "architect": { "model": "claude-opus-5-5", "effort": "max" },
@@ -554,17 +555,30 @@ and Terraform applies only in CI, after you merge.
554
555
  `harness-*` roles and to the gateway user, so your project's own Terraform can't widen
555
556
  the agents' access.
556
557
 
557
- **Then, in the project:**
558
+ **Then, in the project** (`harness setup` does steps 1 to 3 when you answer its AWS
559
+ questions):
558
560
  1. In `harness/config.json`, add `aws` and `terraform`:
559
561
 
560
562
  ```json
561
- "aws": { "region": "us-east-1", "provider": "aws-view-<org>-shop", "services": ["sts", "s3", "iam", "sqs"] },
563
+ "aws": { "region": "us-east-1", "provider": "aws-view-<org>-shop", "services": ["all"] },
562
564
  "terraform": { "dir": "iac", "backendConfig": "backend-configs/staging.hcl", "varFile": "env/staging.tfvars" }
563
565
  ```
564
566
 
565
- `services` lists the AWS services the project uses: their endpoints become reachable,
566
- and signed. Keep the `terraform` paths as they are: the CI workflow uses the same
567
- ones. `harness github-setup` then requires `tf-plan` as well.
567
+ `services` lists the AWS services whose endpoints the sandboxes reach, signed with
568
+ the view role. `all`, the default, is every service the AWS SDK knows, so the view
569
+ role alone decides what agents read; a list of names narrows it. Every service but S3
570
+ is signed with the hash of the request's body, empty for a `GET`: OpenShell's
571
+ automatic mode signs a bodiless `GET` in a way only S3 accepts
572
+ ([NVIDIA/OpenShell#5375](https://github.com/NVIDIA/OpenShell/issues/5375)). Keep the `terraform` paths
573
+ as they are: the CI workflow uses the same ones. `harness github-setup` then requires
574
+ `tf-plan` as well.
575
+
576
+ In `iac/`, the Terraform scaffold: a partial `backend "s3" {}` block, the backend
577
+ config with the state bucket, the project's key, the region and `use_lockfile = true`,
578
+ and the variables file. `harness setup` writes whatever of it is missing, from the
579
+ variables your bootstrap set on GitHub. Its backend config sets no encryption, so the
580
+ bucket's default, your KMS key, applies: `encrypt = true` without `kms_key_id` would
581
+ replace it with SSE-S3.
568
582
  2. Set the repository variables the workflow reads: `AWS_REGION_STAGING`,
569
583
  `AWS_ROLE_PLAN_ARN_STAGING` and `AWS_ROLE_APPLY_ARN_STAGING`.
570
584
  3. `harness doctor` creates the OpenShell AWS provider: it asks for the view role's ARN,
@@ -643,8 +657,8 @@ To report a vulnerability, see `SECURITY.md` in the source repository.
643
657
  |---|---|
644
658
  | `Docker: … permission denied` | Add yourself to the `docker` group, then log in again |
645
659
  | `Tailscale: an exit node is active`, or `container network` fails | An exit node can cut the containers' network: turn it off |
646
- | `OpenShell: … pinned to 0.1.2` | Install OpenShell 0.1.2 |
647
- | `OpenShell gateway` fails | `openshell gateway start` |
660
+ | `OpenShell: … pinned to 0.1.7` | Install OpenShell 0.1.7, from its Debian package |
661
+ | `OpenShell gateway` fails | `systemctl --user start openshell-gateway`: the gateway is a systemd user service |
648
662
  | `image: … has Claude Code X, this machine Y` | `harness doctor --build-images`, after each Claude Code update |
649
663
  | `terminals: node-pty is missing` | Install `python3`, `make` and `g++`, then reinstall with `--allow-scripts=node-pty`. `harness attach` works meanwhile. |
650
664
  | `Claude provider: … is missing` | `claude setup-token`, then `harness doctor` in a terminal: it asks for the token |
@@ -165,8 +165,8 @@ export const ConfigSchema = z.object({
165
165
  region: z.string().regex(/^[a-z]{2}(-[a-z]+)+-\d$/, 'an AWS region, like us-east-1'),
166
166
  /** The OpenShell provider holding the view role, like "aws-view-shop". */
167
167
  provider: z.string().min(1),
168
- /** The services the project uses: their endpoints are reachable, and signed. */
169
- services: z.array(z.string().regex(/^[a-z0-9-]+$/)).default(['sts', 's3', 'iam']),
168
+ /** The services whose endpoints are reachable, and signed; `all`, the default, is every one, so the view role alone decides. */
169
+ services: z.array(z.string().regex(/^[a-z0-9-]+$/)).default(['all']),
170
170
  })
171
171
  .optional(),
172
172
  /** Where the project's Terraform lives, and how a plan is made against the real state (§12.4). */
@@ -0,0 +1,428 @@
1
+ // Generated by scripts/aws-services.ts from botocore's data (AWS CLI 2.36.48): don't edit it by hand.
2
+ /** Every regional AWS service: its endpoint prefix, its signing name, and its host when not `{service}.{region}.{dnsSuffix}`. */
3
+ export const REGIONAL_SERVICES = [
4
+ ['a2i-runtime.sagemaker', 'sagemaker'],
5
+ ['access-analyzer', 'access-analyzer'],
6
+ ['account-access', 'account-access'],
7
+ ['acm', 'acm'],
8
+ ['acm-pca', 'acm-pca'],
9
+ ['aco-automation', 'aco-automation'],
10
+ ['admin.wickr', 'wickr'],
11
+ ['agent-registry', 'agent-registry'],
12
+ ['agent-registry-control', 'agent-registry'],
13
+ ['agenttoolkit', 'execute-api'],
14
+ ['agreement-marketplace', 'aws-marketplace'],
15
+ ['aidevops', 'aidevops'],
16
+ ['aiops', 'aiops'],
17
+ ['airflow', 'airflow'],
18
+ ['airflow-serverless', 'airflow-serverless'],
19
+ ['amplify', 'amplify'],
20
+ ['amplifybackend', 'amplifybackend'],
21
+ ['amplifyuibuilder', 'amplifyuibuilder'],
22
+ ['aoss', 'aoss'],
23
+ ['api.detective', 'detective'],
24
+ ['api.ecr', 'ecr'],
25
+ ['api.ecr-public', 'ecr-public'],
26
+ ['api.iotdeviceadvisor', 'iotdeviceadvisor'],
27
+ ['api.iotmanagedintegrations', 'iotmanagedintegrations'],
28
+ ['api.iotwireless', 'iotwireless'],
29
+ ['api.mediatailor', 'mediatailor'],
30
+ ['api.pricing', 'pricing'],
31
+ ['api.sagemaker', 'sagemaker'],
32
+ ['api.tunneling.iot', 'IoTSecuredTunneling'],
33
+ ['apigateway', 'apigateway'],
34
+ ['app-integrations', 'app-integrations'],
35
+ ['appconfig', 'appconfig'],
36
+ ['appconfigdata', 'appconfig'],
37
+ ['appfabric', 'appfabric'],
38
+ ['appflow', 'appflow'],
39
+ ['application-autoscaling', 'application-autoscaling'],
40
+ ['application-cost-profiler', 'application-cost-profiler'],
41
+ ['application-signals', 'application-signals'],
42
+ ['applicationinsights', 'applicationinsights'],
43
+ ['appmesh', 'appmesh'],
44
+ ['apprunner', 'apprunner'],
45
+ ['appstream2', 'appstream'],
46
+ ['appsync', 'appsync'],
47
+ ['aps', 'aps'],
48
+ ['arc-region-switch', 'arc-region-switch'],
49
+ ['arc-zonal-shift', 'arc-zonal-shift'],
50
+ ['artifact', 'artifact'],
51
+ ['athena', 'athena'],
52
+ ['auditmanager', 'auditmanager'],
53
+ ['autoscaling', 'autoscaling'],
54
+ ['autoscaling-plans', 'autoscaling-plans'],
55
+ ['b2bi', 'b2bi'],
56
+ ['backup', 'backup'],
57
+ ['backup-gateway', 'backup-gateway'],
58
+ ['backup-search', 'backup-search'],
59
+ ['batch', 'batch'],
60
+ ['bcm-dashboards', 'bcm-dashboards'],
61
+ ['bcm-data-exports', 'bcm-data-exports'],
62
+ ['bcm-pricing-calculator', 'bcm-pricing-calculator'],
63
+ ['bcm-recommended-actions', 'bcm-recommended-actions'],
64
+ ['bedrock', 'bedrock'],
65
+ ['bedrock-agent', 'bedrock'],
66
+ ['bedrock-agent-runtime', 'bedrock'],
67
+ ['bedrock-agentcore', 'bedrock-agentcore'],
68
+ ['bedrock-agentcore-control', 'bedrock-agentcore'],
69
+ ['bedrock-data-automation', 'bedrock'],
70
+ ['bedrock-data-automation-runtime', 'bedrock'],
71
+ ['bedrock-runtime', 'bedrock'],
72
+ ['billing', 'billing'],
73
+ ['braket', 'braket'],
74
+ ['cases', 'cases'],
75
+ ['cassandra', 'cassandra'],
76
+ ['cassandra-streams', 'cassandra'],
77
+ ['catalog.marketplace', 'aws-marketplace'],
78
+ ['chatbot', 'chatbot'],
79
+ ['cleanrooms', 'cleanrooms'],
80
+ ['cleanrooms-ml', 'cleanrooms-ml'],
81
+ ['cloud9', 'cloud9'],
82
+ ['cloudcontrolapi', 'cloudcontrolapi'],
83
+ ['clouddirectory', 'clouddirectory'],
84
+ ['cloudformation', 'cloudformation'],
85
+ ['cloudfront-keyvaluestore', 'cloudfront-keyvaluestore'],
86
+ ['cloudhsm', 'cloudhsm'],
87
+ ['cloudhsmv2', 'cloudhsm'],
88
+ ['cloudsearch', 'cloudsearch'],
89
+ ['cloudsearchdomain', 'cloudsearch'],
90
+ ['cloudtrail', 'cloudtrail'],
91
+ ['cloudtrail-data', 'cloudtrail-data'],
92
+ ['codeartifact', 'codeartifact'],
93
+ ['codebuild', 'codebuild'],
94
+ ['codecommit', 'codecommit'],
95
+ ['codeconnections', 'codeconnections'],
96
+ ['codedeploy', 'codedeploy'],
97
+ ['codeguru-profiler', 'codeguru-profiler'],
98
+ ['codeguru-reviewer', 'codeguru-reviewer'],
99
+ ['codeguru-security', 'codeguru-security'],
100
+ ['codepipeline', 'codepipeline'],
101
+ ['codestar-connections', 'codestar-connections'],
102
+ ['codestar-notifications', 'codestar-notifications'],
103
+ ['cognito-identity', 'cognito-identity'],
104
+ ['cognito-idp', 'cognito-idp'],
105
+ ['cognito-sync', 'cognito-sync'],
106
+ ['comprehend', 'comprehend'],
107
+ ['comprehendmedical', 'comprehendmedical'],
108
+ ['compute-optimizer', 'compute-optimizer'],
109
+ ['config', 'config'],
110
+ ['connect', 'connect'],
111
+ ['connect-campaigns', 'connect-campaigns'],
112
+ ['contact-lens', 'connect'],
113
+ ['controlcatalog', 'controlcatalog'],
114
+ ['controlplane.payment-cryptography', 'payment-cryptography'],
115
+ ['controltower', 'controltower'],
116
+ ['cost-optimization-hub', 'cost-optimization-hub'],
117
+ ['cur', 'cur'],
118
+ ['data-ats.iot', 'iotdata'],
119
+ ['data-signer', 'signer'],
120
+ ['data.iot', 'iotdata'],
121
+ ['data.jobs.iot', 'iot-jobs-data'],
122
+ ['data.mediastore', 'mediastore'],
123
+ ['data.qapps', 'qapps'],
124
+ ['databrew', 'databrew'],
125
+ ['dataexchange', 'dataexchange'],
126
+ ['datapipeline', 'datapipeline'],
127
+ ['dataplane.payment-cryptography', 'payment-cryptography'],
128
+ ['datasync', 'datasync'],
129
+ ['datazone', 'datazone'],
130
+ ['dax', 'dax'],
131
+ ['deadline', 'deadline'],
132
+ ['deployment-marketplace', 'aws-marketplace'],
133
+ ['devicefarm', 'devicefarm'],
134
+ ['devops-guru', 'devops-guru'],
135
+ ['directconnect', 'directconnect'],
136
+ ['discovery', 'discovery'],
137
+ ['discovery-marketplace', 'aws-marketplace'],
138
+ ['dlm', 'dlm'],
139
+ ['dms', 'dms'],
140
+ ['docdb', 'docdb'],
141
+ ['docdb-elastic', 'docdb-elastic'],
142
+ ['drs', 'drs'],
143
+ ['ds', 'ds'],
144
+ ['ds-data', 'ds-data'],
145
+ ['dsql', 'dsql'],
146
+ ['dynamodb', 'dynamodb'],
147
+ ['ebs', 'ebs'],
148
+ ['ec2', 'ec2'],
149
+ ['ec2-instance-connect', 'ec2-instance-connect'],
150
+ ['ecs', 'ecs'],
151
+ ['edge.sagemaker', 'sagemaker'],
152
+ ['eks', 'eks'],
153
+ ['eks-auth', 'eks-auth'],
154
+ ['elasticache', 'elasticache'],
155
+ ['elasticbeanstalk', 'elasticbeanstalk'],
156
+ ['elasticfilesystem', 'elasticfilesystem'],
157
+ ['elasticloadbalancing', 'elasticloadbalancing'],
158
+ ['elasticmapreduce', 'elasticmapreduce'],
159
+ ['elemental-inference', 'elemental-inference'],
160
+ ['email', 'ses'],
161
+ ['emr-containers', 'emr-containers'],
162
+ ['emr-serverless', 'emr-serverless'],
163
+ ['entitlement.marketplace', 'aws-marketplace'],
164
+ ['entityresolution', 'entityresolution'],
165
+ ['es', 'es'],
166
+ ['events', 'events'],
167
+ ['evs', 'evs'],
168
+ ['execute-api', 'execute-api'],
169
+ ['featurestore-runtime.sagemaker', 'sagemaker'],
170
+ ['finspace', 'finspace'],
171
+ ['finspace-api', 'finspace-api'],
172
+ ['firehose', 'firehose'],
173
+ ['fis', 'fis'],
174
+ ['fms', 'fms'],
175
+ ['forecast', 'forecast'],
176
+ ['forecastquery', 'forecast'],
177
+ ['frauddetector', 'frauddetector'],
178
+ ['freetier', 'freetier'],
179
+ ['fsx', 'fsx'],
180
+ ['gamelift', 'gamelift'],
181
+ ['gameliftstreams', 'gameliftstreams'],
182
+ ['geo', 'geo'],
183
+ ['geo-maps', 'geo-maps'],
184
+ ['geo-places', 'geo-places'],
185
+ ['geo-routes', 'geo-routes'],
186
+ ['glacier', 'glacier'],
187
+ ['globalaccelerator', 'globalaccelerator'],
188
+ ['glue', 'glue'],
189
+ ['grafana', 'grafana'],
190
+ ['greengrass', 'greengrass'],
191
+ ['groundstation', 'groundstation'],
192
+ ['guardduty', 'guardduty'],
193
+ ['health-agent', 'health-agent'],
194
+ ['healthlake', 'healthlake'],
195
+ ['iam-toolbox', 'iam'],
196
+ ['identity-chime', 'chime'],
197
+ ['identitystore', 'identitystore'],
198
+ ['imagebuilder', 'imagebuilder'],
199
+ ['ingest.timestream', 'timestream'],
200
+ ['inspector', 'inspector'],
201
+ ['inspector-scan', 'inspector-scan'],
202
+ ['inspector2', 'inspector2'],
203
+ ['interconnect', 'interconnect'],
204
+ ['internetmonitor', 'internetmonitor'],
205
+ ['invoicing', 'invoicing'],
206
+ ['iot', 'iot'],
207
+ ['iotfleetwise', 'iotfleetwise'],
208
+ ['iotsecuredtunneling', 'iotsecuredtunneling'],
209
+ ['iotsitewise', 'iotsitewise'],
210
+ ['iotthingsgraph', 'iotthingsgraph'],
211
+ ['iottwinmaker', 'iottwinmaker'],
212
+ ['iotwireless', 'iotwireless'],
213
+ ['ivs', 'ivs'],
214
+ ['ivschat', 'ivschat'],
215
+ ['ivsrealtime', 'ivs'],
216
+ ['job-runtime.sagemaker', 'sagemaker'],
217
+ ['kafka', 'kafka'],
218
+ ['kafkaconnect', 'kafkaconnect'],
219
+ ['kendra', 'kendra'],
220
+ ['kendra-ranking', 'kendra-ranking'],
221
+ ['kinesis', 'kinesis'],
222
+ ['kinesisanalytics', 'kinesisanalytics'],
223
+ ['kinesisvideo', 'kinesisvideo'],
224
+ ['kms', 'kms'],
225
+ ['lakeformation', 'lakeformation'],
226
+ ['lambda', 'lambda'],
227
+ ['launchwizard', 'launchwizard'],
228
+ ['license-manager', 'license-manager'],
229
+ ['license-manager-linux-subscriptions', 'license-manager-linux-subscriptions'],
230
+ ['license-manager-user-subscriptions', 'license-manager-user-subscriptions'],
231
+ ['lightsail', 'lightsail'],
232
+ ['logs', 'logs'],
233
+ ['lookoutequipment', 'lookoutequipment'],
234
+ ['m2', 'm2'],
235
+ ['machinelearning', 'machinelearning'],
236
+ ['macie2', 'macie2'],
237
+ ['mail-manager', 'ses'],
238
+ ['managedblockchain', 'managedblockchain'],
239
+ ['managedblockchain-query', 'managedblockchain-query'],
240
+ ['marketplacecommerceanalytics', 'marketplacecommerceanalytics'],
241
+ ['media-pipelines-chime', 'chime'],
242
+ ['mediaconnect', 'mediaconnect'],
243
+ ['mediaconvert', 'mediaconvert'],
244
+ ['medialive', 'medialive'],
245
+ ['mediapackage', 'mediapackage'],
246
+ ['mediapackage-vod', 'mediapackage-vod'],
247
+ ['mediapackagev2', 'mediapackagev2'],
248
+ ['mediastore', 'mediastore'],
249
+ ['medical-imaging', 'medical-imaging'],
250
+ ['meetings-chime', 'chime'],
251
+ ['memory-db', 'memorydb'],
252
+ ['messaging-chime', 'chime'],
253
+ ['metering.marketplace', 'aws-marketplace'],
254
+ ['metrics.sagemaker', 'sagemaker'],
255
+ ['mgh', 'mgh'],
256
+ ['mgn', 'mgn'],
257
+ ['migrationhub-config', 'mgh'],
258
+ ['migrationhub-orchestrator', 'migrationhub-orchestrator'],
259
+ ['migrationhub-strategy', 'migrationhub-strategy'],
260
+ ['mobileanalytics', 'mobileanalytics'],
261
+ ['models-v2-lex', 'lex'],
262
+ ['models.lex', 'lex'],
263
+ ['monitoring', 'monitoring'],
264
+ ['mpa', 'mpa'],
265
+ ['mq', 'mq'],
266
+ ['neptune', 'neptune'],
267
+ ['neptune-db', 'neptune-db'],
268
+ ['neptune-graph', 'neptune-graph'],
269
+ ['network-firewall', 'network-firewall'],
270
+ ['networkflowmonitor', 'networkflowmonitor'],
271
+ ['networkmonitor', 'networkmonitor'],
272
+ ['notifications', 'notifications'],
273
+ ['nova-act', 'nova-act'],
274
+ ['oam', 'oam'],
275
+ ['observabilityadmin', 'observabilityadmin'],
276
+ ['odb', 'odb'],
277
+ ['oidc', 'sso-oauth'],
278
+ ['omics', 'omics'],
279
+ ['osis', 'osis'],
280
+ ['outposts', 'outposts'],
281
+ ['participant.connect', 'execute-api'],
282
+ ['partnercentral-account', 'partnercentral-account'],
283
+ ['partnercentral-benefits', 'partnercentral-benefits'],
284
+ ['partnercentral-channel', 'partnercentral-channel'],
285
+ ['partnercentral-prm', 'partnercentral'],
286
+ ['partnercentral-selling', 'partnercentral-selling'],
287
+ ['pca-connector-ad', 'pca-connector-ad'],
288
+ ['pca-connector-scep', 'pca-connector-scep'],
289
+ ['pcs', 'pcs'],
290
+ ['personalize', 'personalize'],
291
+ ['personalize-events', 'personalize'],
292
+ ['personalize-runtime', 'personalize'],
293
+ ['pi', 'pi'],
294
+ ['pinpoint', 'mobiletargeting'],
295
+ ['pipes', 'pipes'],
296
+ ['polly', 'polly'],
297
+ ['portal.sso', 'awsssoportal'],
298
+ ['pricingplanmanager', 'pricingplanmanager'],
299
+ ['profile', 'profile'],
300
+ ['proton', 'proton'],
301
+ ['qbusiness', 'qbusiness'],
302
+ ['query.timestream', 'timestream'],
303
+ ['quicksight', 'quicksight'],
304
+ ['ram', 'ram'],
305
+ ['rbin', 'rbin'],
306
+ ['rds', 'rds'],
307
+ ['rds-data', 'rds-data'],
308
+ ['redshift', 'redshift'],
309
+ ['redshift-data', 'redshift-data'],
310
+ ['redshift-serverless', 'redshift-serverless'],
311
+ ['refactor-spaces', 'refactor-spaces'],
312
+ ['rekognition', 'rekognition'],
313
+ ['reporting-marketplace', 'aws-marketplace'],
314
+ ['repostspace', 'repostspace'],
315
+ ['resiliencehub', 'resiliencehub'],
316
+ ['resource-explorer-2', 'resource-explorer-2'],
317
+ ['resource-groups', 'resource-groups'],
318
+ ['rolesanywhere', 'rolesanywhere'],
319
+ ['route53-recovery-cluster', 'route53-recovery-cluster'],
320
+ ['route53-recovery-control-config', 'route53-recovery-control-config'],
321
+ ['route53-recovery-readiness', 'route53-recovery-readiness'],
322
+ ['route53domains', 'route53domains'],
323
+ ['route53globalresolver', 'route53globalresolver'],
324
+ ['route53profiles', 'route53profiles'],
325
+ ['route53resolver', 'route53resolver'],
326
+ ['rtbfabric', 'rtbfabric'],
327
+ ['rum', 'rum'],
328
+ ['runtime-v2-lex', 'lex'],
329
+ ['runtime.lex', 'lex'],
330
+ ['runtime.sagemaker', 'sagemaker'],
331
+ ['s3', 's3'],
332
+ ['s3-control', 's3'],
333
+ ['s3-outposts', 's3-outposts'],
334
+ ['s3files', 's3files'],
335
+ ['s3tables', 's3tables'],
336
+ ['s3vectors', 's3vectors'],
337
+ ['sagemaker-geospatial', 'sagemaker-geospatial'],
338
+ ['scheduler', 'scheduler'],
339
+ ['schemas', 'schemas'],
340
+ ['scn', 'scn'],
341
+ ['sdb', 'sdb'],
342
+ ['secretsmanager', 'secretsmanager'],
343
+ ['security-ir', 'security-ir'],
344
+ ['securityagent', 'securityagent'],
345
+ ['securityhub', 'securityhub'],
346
+ ['securitylake', 'securitylake'],
347
+ ['serverlessrepo', 'serverlessrepo'],
348
+ ['servicecatalog', 'servicecatalog'],
349
+ ['servicecatalog-appregistry', 'servicecatalog'],
350
+ ['servicediscovery', 'servicediscovery'],
351
+ ['servicequotas', 'servicequotas'],
352
+ ['signer', 'signer'],
353
+ ['signin', 'signin'],
354
+ ['sms-voice', 'sms-voice'],
355
+ ['sms-voice.pinpoint', 'sms-voice'],
356
+ ['snow-device-management', 'snow-device-management'],
357
+ ['snowball', 'snowball'],
358
+ ['sns', 'sns'],
359
+ ['social-messaging', 'social-messaging'],
360
+ ['sqs', 'sqs'],
361
+ ['ssm', 'ssm'],
362
+ ['ssm-contacts', 'ssm-contacts'],
363
+ ['ssm-guiconnect', 'ssm-guiconnect'],
364
+ ['ssm-incidents', 'ssm-incidents'],
365
+ ['ssm-quicksetup', 'ssm-quicksetup'],
366
+ ['ssm-sap', 'ssm-sap'],
367
+ ['sso', 'sso'],
368
+ ['states', 'states'],
369
+ ['storagegateway', 'storagegateway'],
370
+ ['streams.dynamodb', 'dynamodb'],
371
+ ['sts', 'sts'],
372
+ ['support', 'support'],
373
+ ['supportapp', 'supportapp'],
374
+ ['supportauthz', 'supportauthz'],
375
+ ['sustainability', 'sustainability'],
376
+ ['swf', 'swf'],
377
+ ['synthetics', 'synthetics'],
378
+ ['tagging', 'tagging'],
379
+ ['textract', 'textract'],
380
+ ['thinclient', 'thinclient'],
381
+ ['timestream-influxdb', 'timestream-influxdb'],
382
+ ['tnb', 'tnb'],
383
+ ['transcribe', 'transcribe'],
384
+ ['transcribestreaming', 'transcribestreaming'],
385
+ ['transfer', 'transfer'],
386
+ ['translate', 'translate'],
387
+ ['trustedadvisor', 'trustedadvisor'],
388
+ ['uxc', 'uxc'],
389
+ ['verifiedpermissions', 'verifiedpermissions'],
390
+ ['voice-chime', 'chime'],
391
+ ['voiceid', 'voiceid'],
392
+ ['vpc-lattice', 'vpc-lattice'],
393
+ ['waf-regional', 'waf-regional'],
394
+ ['wafv2', 'wafv2'],
395
+ ['wellarchitected', 'wellarchitected'],
396
+ ['wisdom', 'wisdom'],
397
+ ['workdocs', 'workdocs'],
398
+ ['workmail', 'workmail'],
399
+ ['workmailmessageflow', 'workmailmessageflow'],
400
+ ['workspaces', 'workspaces'],
401
+ ['workspaces-instances', 'workspaces-instances'],
402
+ ['workspaces-web', 'workspaces-web'],
403
+ ['xray', 'xray'],
404
+ ];
405
+ /** Every global AWS endpoint: the service, its host, its signing name, and the region it signs for. */
406
+ export const GLOBAL_SERVICES = [
407
+ ['account', 'account.us-east-1.amazonaws.com', 'account', 'us-east-1'],
408
+ ['billingconductor', 'billingconductor.us-east-1.amazonaws.com', 'billingconductor', 'us-east-1'],
409
+ ['budgets', 'budgets.amazonaws.com', 'budgets', 'us-east-1'],
410
+ ['ce', 'ce.us-east-1.amazonaws.com', 'ce', 'us-east-1'],
411
+ ['chime', 'chime.us-east-1.amazonaws.com', 'chime', 'us-east-1'],
412
+ ['cloudfront', 'cloudfront.amazonaws.com', 'cloudfront', 'us-east-1'],
413
+ ['codecatalyst', 'codecatalyst.global.api.aws', 'codecatalyst', 'us-east-1'],
414
+ ['health', 'global.health.amazonaws.com', 'health', 'us-east-1'],
415
+ ['iam', 'iam.amazonaws.com', 'iam', 'us-east-1'],
416
+ ['importexport', 'importexport.amazonaws.com', 'IngestionService', 'us-east-1'],
417
+ ['networkmanager', 'networkmanager.us-west-2.amazonaws.com', 'networkmanager', 'us-west-2'],
418
+ ['notifications-contacts', 'notifications-contacts.us-east-1.api.aws', 'notifications-contacts', 'us-east-1'],
419
+ ['organizations', 'organizations.us-east-1.amazonaws.com', 'organizations', 'us-east-1'],
420
+ ['route53', 'route53.amazonaws.com', 'route53', 'us-east-1'],
421
+ ['s3', 's3.amazonaws.com', 's3', 'us-east-1'],
422
+ ['savingsplans', 'savingsplans.amazonaws.com', 'savingsplans', 'us-east-1'],
423
+ ['shield', 'shield.us-east-1.amazonaws.com', 'shield', 'us-east-1'],
424
+ ['sts', 'sts.amazonaws.com', 'sts', 'us-east-1'],
425
+ ['support', 'support.us-east-1.amazonaws.com', 'support', 'us-east-1'],
426
+ ['tax', 'tax.us-east-1.amazonaws.com', 'tax', 'us-east-1'],
427
+ ['waf', 'waf.amazonaws.com', 'waf', 'us-east-1'],
428
+ ];
@@ -1,4 +1,5 @@
1
1
  import { BUNDLES, bundleNames, checkHost } from './allowlist.js';
2
+ import { GLOBAL_SERVICES, REGIONAL_SERVICES } from './aws-services.js';
2
3
  /** Services whose endpoints don't follow `<service>.<region>.amazonaws.com`. Global ones sign for us-east-1. */
3
4
  const AWS_SERVICES = {
4
5
  iam: { hosts: () => ['iam.amazonaws.com'], signing: 'iam', global: true },
@@ -8,26 +9,48 @@ const AWS_SERVICES = {
8
9
  cloudfront: { hosts: () => ['cloudfront.amazonaws.com'], signing: 'cloudfront', global: true },
9
10
  route53: { hosts: () => ['route53.amazonaws.com'], signing: 'route53', global: true },
10
11
  };
12
+ /** In `aws.services`, every AWS service: the view role alone then decides what agents read. */
13
+ export const ALL_SERVICES = 'all';
11
14
  /**
12
15
  * One signed endpoint per host. `access: full`, because most AWS APIs are POSTs: the
13
- * role itself is what allows only reads (spike 2).
16
+ * role itself is what allows only reads (spike 2). OpenShell signs only for endpoints
17
+ * named with their service, with no rule for every AWS host, so `all` names every one
18
+ * the AWS SDK knows (aws-services.ts), with the hosts that carry a bucket's or an
19
+ * account's name in front.
20
+ *
21
+ * Every service but S3 is signed with `sigv4:body`: the hash of the request's body, an
22
+ * empty one for a GET. OpenShell's automatic `sigv4` signs a request with neither a body
23
+ * nor `x-amz-content-sha256` as UNSIGNED-PAYLOAD, which only S3 accepts, so every other
24
+ * service's GET failed (NVIDIA/OpenShell#5375). S3 keeps it: its SDKs say how to sign.
14
25
  */
15
26
  function awsEndpoints(aws) {
16
- return aws.services.flatMap((service) => {
27
+ const signed = (host, service, region) => ({
28
+ host,
29
+ port: 443,
30
+ protocol: 'rest',
31
+ enforcement: 'enforce',
32
+ access: 'full',
33
+ credential_binding: { provider: aws.provider },
34
+ credential_signing: service === 's3' ? 'sigv4' : 'sigv4:body',
35
+ signing_service: service,
36
+ ...(region ? { signing_region: region } : {}),
37
+ });
38
+ const named = aws.services.filter((service) => service !== ALL_SERVICES).flatMap((service) => {
17
39
  const known = AWS_SERVICES[service];
18
40
  const hosts = known ? known.hosts(aws.region) : [`${service}.${aws.region}.amazonaws.com`];
19
- return hosts.map((host) => ({
20
- host,
21
- port: 443,
22
- protocol: 'rest',
23
- enforcement: 'enforce',
24
- access: 'full',
25
- credential_binding: { provider: aws.provider },
26
- credential_signing: 'sigv4',
27
- signing_service: known?.signing ?? service,
28
- ...(known?.global ? { signing_region: 'us-east-1' } : {}),
29
- }));
41
+ return hosts.map((host) => signed(host, known?.signing ?? service, known?.global ? 'us-east-1' : undefined));
30
42
  });
43
+ const all = aws.services.includes(ALL_SERVICES)
44
+ ? [
45
+ ...REGIONAL_SERVICES.map(([prefix, service, template]) => signed((template ?? '{service}.{region}.{dnsSuffix}').replace('{service}', prefix).replace('{region}', aws.region).replace('{dnsSuffix}', 'amazonaws.com'), service)),
46
+ signed(`*.s3.${aws.region}.amazonaws.com`, 's3'),
47
+ signed(`*.s3-control.${aws.region}.amazonaws.com`, 's3'),
48
+ ...GLOBAL_SERVICES.map(([, host, service, region]) => signed(host, service, region)),
49
+ ]
50
+ : [];
51
+ // One rule per host, a named service's first.
52
+ const endpoints = [...named, ...all];
53
+ return endpoints.filter((endpoint, i) => endpoints.findIndex((other) => other.host === endpoint.host) === i);
31
54
  }
32
55
  /**
33
56
  * Programs whose connections the rules cover. A rule also applies to the processes a
@@ -16,7 +16,7 @@ import { run } from '../util.js';
16
16
  import { buildCache, removeOldAgentImages } from './images.js';
17
17
  import { probe, stateLocation } from './probe.js';
18
18
  /** The OpenShell release the harness is built and probed against (§17.4). */
19
- export const OPENSHELL_VERSION = '0.1.2';
19
+ export const OPENSHELL_VERSION = '0.1.7';
20
20
  /** OpenShell's AWS provider profile, at that release. */
21
21
  const AWS_PROFILE_URL = `https://raw.githubusercontent.com/NVIDIA/OpenShell/v${OPENSHELL_VERSION}/providers/aws.yaml`;
22
22
  /**
@@ -75,7 +75,7 @@ async function machine(options, image, report) {
75
75
  else
76
76
  report(found === OPENSHELL_VERSION ? ok('OpenShell', found) : fail('OpenShell', `${found}, but the harness is pinned to ${OPENSHELL_VERSION}`, `install OpenShell ${OPENSHELL_VERSION}`));
77
77
  const status = await run('openshell', ['status']).catch(() => undefined);
78
- report(/Status:\s*Connected/.test(status?.stdout ?? '') ? ok('OpenShell gateway', 'connected') : fail('OpenShell gateway', firstLine(status?.stdout || status?.stderr) || 'not running', 'start the gateway: `openshell gateway start`'));
78
+ report(/Status:\s*Connected/.test(status?.stdout ?? '') ? ok('OpenShell gateway', 'connected') : fail('OpenShell gateway', firstLine(status?.stdout || status?.stderr) || 'not running', 'start the gateway, a systemd user service: `systemctl --user start openshell-gateway`'));
79
79
  const host = await run('claude', ['--version']).catch(() => undefined);
80
80
  const hostClaude = /^(\S+)/.exec(host?.stdout ?? '')?.[1];
81
81
  report(hostClaude ? ok('Claude Code', hostClaude) : fail('Claude Code', 'not found', 'install Claude Code: the agents\' image takes it from this machine'));
@@ -218,7 +218,7 @@ async function project(options, config, report) {
218
218
  if (config.terraform) {
219
219
  const tf = config.terraform;
220
220
  for (const file of [tf.backendConfig, tf.varFile]) {
221
- report(existsSync(join(options.projectDir, tf.dir, file)) ? ok('Terraform', `${tf.dir}/${file}`) : fail('Terraform', `${tf.dir}/${file} is missing`, 'see harness/config.json under terraform'));
221
+ report(existsSync(join(options.projectDir, tf.dir, file)) ? ok('Terraform', `${tf.dir}/${file}`) : fail('Terraform', `${tf.dir}/${file} is missing`, '`harness setup` writes the missing Terraform scaffold, or see the README\'s AWS section'));
222
222
  }
223
223
  }
224
224
  }
@@ -7,6 +7,7 @@ import { applyRepositoryRules, checkApp, createApp, installCi } from '../github/
7
7
  import { run, sha256 } from '../util.js';
8
8
  import { doctor, granted } from './doctor.js';
9
9
  import { appBot, githubRepo, init } from './init.js';
10
+ import { writeTerraformScaffold } from './terraform.js';
10
11
  export function fixFor(check) {
11
12
  if (check.status === 'ok' || check.status === 'skip')
12
13
  return undefined;
@@ -214,24 +215,46 @@ async function askSettings(options) {
214
215
  const written = [];
215
216
  let changed = false;
216
217
  let viewRole;
218
+ let account;
217
219
  if (!config.aws && /^y/i.test(await ask('Does this project deploy to AWS with Terraform? [y/N] '))) {
218
220
  say('The roles come from your bootstrap (see the README\'s AWS section); the key is asked for later, at a hidden prompt.');
219
221
  const region = (await ask('AWS region (like eu-west-1): ')).trim();
220
222
  const view = (await ask("The agents' view role ARN (arn:aws:iam::<account>:role/harness-<project>-view): ")).trim();
221
223
  const plan = (await ask('The CI plan role ARN: ')).trim();
222
224
  const apply = (await ask('The CI apply role ARN: ')).trim();
223
- const services = (await ask('The AWS services the project uses, comma-separated (sts, s3, iam, ...): ')).split(/[,\s]+/).filter(Boolean);
224
225
  if (!region || !VIEW_ROLE.test(view) || !ROLE.test(plan) || !ROLE.test(apply)) {
225
226
  say('AWS skipped: a value is missing or malformed. Add `aws` and `terraform` to harness/config.json by hand, or run `harness setup` again.');
226
227
  }
227
228
  else {
228
- config.aws = { region, provider: awsProviderName(projectDir, config.project?.repo), services: [...new Set(['sts', ...services])] };
229
+ config.aws = { region, provider: awsProviderName(projectDir, config.project?.repo), services: ['all'] };
229
230
  config.terraform = { dir: 'iac', backendConfig: 'backend-configs/staging.hcl', varFile: 'env/staging.tfvars' };
230
231
  changed = true;
231
232
  viewRole = view;
232
233
  const repo = config.project?.repo;
233
234
  if (repo)
234
235
  await configureGitHubForAws(options, repo, { region, plan, apply });
236
+ account = /^arn:aws:iam::(\d{12}):/.exec(view)[1];
237
+ }
238
+ }
239
+ // The Terraform scaffold the bootstrap expects, which the doctor checks: whatever of it is
240
+ // missing, on this run or a later one. A later run reads the account from the plan role
241
+ // this one stored on GitHub.
242
+ if (config.aws?.region && config.terraform) {
243
+ const repo = config.project?.repo;
244
+ account ??= /^arn:aws:iam::(\d{12}):/.exec((await githubVariable(repo, 'AWS_ROLE_PLAN_ARN_STAGING')) ?? '')?.[1];
245
+ if (account) {
246
+ const layout = { dir: 'iac', backendConfig: 'backend-configs/staging.hcl', varFile: 'env/staging.tfvars', ...config.terraform };
247
+ // As the schema reads it: `iac`, `./iac` and `iac/` are one folder. Slashes are trimmed by hand: a regex would take quadratic time on a long run of them.
248
+ let dir = layout.dir;
249
+ while (dir.startsWith('./') || dir.startsWith('/'))
250
+ dir = dir.slice(dir.startsWith('./') ? 2 : 1);
251
+ while (dir.endsWith('/'))
252
+ dir = dir.slice(0, -1);
253
+ layout.dir = dir || 'iac';
254
+ const scaffold = writeTerraformScaffold(projectDir, layout, await terraformFacts(repo, String(config.project?.name ?? 'project'), String(config.aws.region), account));
255
+ if (scaffold.length > 0)
256
+ say(`Terraform: wrote ${scaffold.join(', ')}.`);
257
+ written.push(...scaffold);
235
258
  }
236
259
  }
237
260
  if (changed) {
@@ -255,6 +278,30 @@ async function askSettings(options) {
255
278
  }
256
279
  return { written, ...(viewRole ? { viewRole } : {}) };
257
280
  }
281
+ /**
282
+ * Where the project's state lives, and its repository's numeric ids: read from GitHub, where
283
+ * the bootstrap's configure-github.sh set TF_STATE_BUCKET_STAGING and TF_STATE_KEY_STAGING,
284
+ * or else by the bootstrap's naming, `tf-state-staging-<account>` and
285
+ * `staging/<repository>/terraform.tfstate`.
286
+ */
287
+ async function terraformFacts(repo, name, region, account) {
288
+ const bucket = await githubVariable(repo, 'TF_STATE_BUCKET_STAGING');
289
+ const key = await githubVariable(repo, 'TF_STATE_KEY_STAGING');
290
+ const ids = (await ghApi(repo, `repos/${repo}`, '"\\(.owner.id) \\(.id)"'))?.split(' ');
291
+ return {
292
+ region,
293
+ account,
294
+ bucket: bucket ?? `tf-state-staging-${account}`,
295
+ key: key ?? `staging/${repo ? repo.split('/')[1] : name}/terraform.tfstate`,
296
+ ...(repo && ids?.length === 2 ? { github: { repo, ownerId: ids[0], repoId: ids[1] } } : {}),
297
+ };
298
+ }
299
+ /** What the owner's gh reads at `path`, through `jq`; undefined without a repository, or on any failure. */
300
+ async function ghApi(repo, path, jq) {
301
+ const result = repo ? await run('gh', ['api', path, '--jq', jq]).catch(() => undefined) : undefined;
302
+ return result?.code === 0 && result.stdout.trim() ? result.stdout.trim() : undefined;
303
+ }
304
+ const githubVariable = (repo, name) => ghApi(repo, `repos/${repo}/actions/variables/${name}`, '.value');
258
305
  /** The staging environment, restricted to main, and the variables the workflow reads, with the owner's gh. */
259
306
  async function configureGitHubForAws(options, repo, aws) {
260
307
  const gh = async (args, body) => {
@@ -334,11 +381,12 @@ async function applyFix(options, config, fix, sleep) {
334
381
  const repo = config.project.repo;
335
382
  switch (fix.kind) {
336
383
  case 'start-gateway': {
337
- const started = await run('openshell', ['gateway', 'start']).catch(() => undefined);
384
+ // OpenShell's Debian package runs the gateway as a systemd user service; its CLI has no command to start it.
385
+ const started = await run('systemctl', ['--user', 'start', 'openshell-gateway']).catch(() => undefined);
338
386
  if (started?.code === 0)
339
387
  say('OpenShell: gateway started.');
340
388
  else
341
- say(`OpenShell: the gateway did not start: ${(started?.stderr || started?.stdout || 'openshell is missing').trim()}`);
389
+ say(`OpenShell: the gateway did not start: ${(started?.stderr || started?.stdout || 'systemctl is missing').trim()}`);
342
390
  return started?.code === 0;
343
391
  }
344
392
  case 'create-app':
@@ -0,0 +1,86 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ /**
4
+ * Writes the Terraform scaffold the bootstrap expects in the layout's folder: a partial
5
+ * `backend "s3"` block in `versions.tf`, the backend config and the variables file, and the
6
+ * variables and provider those values feed. Only what is missing is written: an existing
7
+ * file, or a backend block in any of the folder's `.tf` files, is the project's own. The
8
+ * backend config sets no encryption, so the bucket's default, the bootstrap's KMS key,
9
+ * applies: `encrypt = true` without `kms_key_id` would replace it with SSE-S3. Every file
10
+ * is as `terraform fmt` writes it, which CI checks. Returns the paths written, from the
11
+ * project's root.
12
+ */
13
+ export function writeTerraformScaffold(projectDir, layout, values) {
14
+ const { dir } = layout;
15
+ const written = [];
16
+ const write = (path, content) => {
17
+ const file = join(projectDir, dir, path);
18
+ if (existsSync(file))
19
+ return false;
20
+ mkdirSync(dirname(file), { recursive: true });
21
+ writeFileSync(file, content);
22
+ written.push(`${dir}/${path}`);
23
+ return true;
24
+ };
25
+ const folder = join(projectDir, dir);
26
+ const hasBackend = existsSync(folder)
27
+ && readdirSync(folder).filter((f) => f.endsWith('.tf')).some((f) => /^\s*backend\s+"/m.test(readFileSync(join(folder, f), 'utf8')));
28
+ if (!hasBackend) {
29
+ write('versions.tf', [
30
+ 'terraform {',
31
+ ' required_version = ">= 1.10"',
32
+ '',
33
+ ' required_providers {',
34
+ ' aws = {',
35
+ ' source = "hashicorp/aws"',
36
+ ' version = "~> 6.65"',
37
+ ' }',
38
+ ' }',
39
+ '',
40
+ ` # Partial configuration: the values are in ${layout.backendConfig}, for`,
41
+ ` # terraform init -backend-config=${layout.backendConfig}`,
42
+ ' backend "s3" {}',
43
+ '}',
44
+ '',
45
+ ].join('\n'));
46
+ }
47
+ write(layout.backendConfig, [
48
+ ...assignments([['bucket', values.bucket], ['key', values.key], ['region', values.region]]),
49
+ '',
50
+ "# No encrypt or kms_key_id: the bucket's default encryption, its KMS key, applies.",
51
+ '# encrypt = true without kms_key_id would replace it with SSE-S3.',
52
+ 'use_lockfile = true',
53
+ '',
54
+ ].join('\n'));
55
+ const github = values.github;
56
+ const variables = [
57
+ ['env', 'staging', 'The environment.'],
58
+ ['aws_account_id', values.account, 'The AWS account the environment is in.'],
59
+ ['aws_region', values.region, 'The AWS region the environment is in.'],
60
+ ...(github ? [
61
+ ['github_repo', github.repo, 'The GitHub repository, as owner/name.'],
62
+ ['github_owner_id', github.ownerId, "The repository owner's numeric id, which OIDC trust policies pin."],
63
+ ['github_repo_id', github.repoId, "The repository's numeric id, which OIDC trust policies pin."],
64
+ ] : []),
65
+ ];
66
+ const tfvars = [
67
+ ...assignments(variables.slice(0, 3).map(([name, value]) => [name, value])),
68
+ ...(github ? ['', ...assignments(variables.slice(3).map(([name, value]) => [name, value]))] : []),
69
+ '',
70
+ ].join('\n');
71
+ // The variables follow the values: a tfvars file of the project's own sets its own.
72
+ if (write(layout.varFile, tfvars)) {
73
+ write('variables.tf', variables.map(([name, , description]) => [
74
+ `variable "${name}" {`,
75
+ ...assignments([['description', description], ['type', 'string']], ['type']).map((line) => ` ${line}`),
76
+ '}',
77
+ ].join('\n')).join('\n\n') + '\n');
78
+ write('providers.tf', ['provider "aws" {', ' region = var.aws_region', '}', ''].join('\n'));
79
+ }
80
+ return written;
81
+ }
82
+ /** `name = "value"` lines with their equals signs aligned, as `terraform fmt` aligns them; `bare` names take an expression, unquoted. */
83
+ function assignments(pairs, bare = []) {
84
+ const width = Math.max(...pairs.map(([name]) => name.length));
85
+ return pairs.map(([name, value]) => `${name.padEnd(width)} = ${bare.includes(name) ? value : JSON.stringify(value)}`);
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "razzou-harness",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Your AI dev team, with you in charge: sandboxed Claude Code agents design, build and review your project, and you merge.",
5
5
  "license": "MIT",
6
6
  "author": "razzou",