@deployfoundation/foundation-deploy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +174 -0
  2. package/agent-image/Dockerfile +254 -0
  3. package/agent-image/bin/aws +36 -0
  4. package/agent-image/bin/gh +193 -0
  5. package/agent-image/bin/git-credential-sky +89 -0
  6. package/agent-image/security-overlay.yml +176 -0
  7. package/cdk.json +6 -0
  8. package/dist/bin/app.js +112 -0
  9. package/dist/bin/foundation-deploy.js +1906 -0
  10. package/dist/bin/release-account.js +154 -0
  11. package/dist/chunk-4aye5cee.js +2416 -0
  12. package/dist/chunk-9ddxyvq2.js +1455 -0
  13. package/dist/chunk-v7tz8g50.js +428 -0
  14. package/dist/src/index.js +88 -0
  15. package/package.json +38 -0
  16. package/pipeline/buildspec.yml +34 -0
  17. package/src/artifacts.ts +318 -0
  18. package/src/deploy/assets/github-app-manifest.yml +29 -0
  19. package/src/deploy/assets/slack-app-manifest.yml +95 -0
  20. package/src/deploy/aws.ts +265 -0
  21. package/src/deploy/cli.ts +212 -0
  22. package/src/deploy/config-sync.ts +93 -0
  23. package/src/deploy/config.ts +29 -0
  24. package/src/deploy/deploy.ts +566 -0
  25. package/src/deploy/endpoint.ts +242 -0
  26. package/src/deploy/github-app-create.ts +154 -0
  27. package/src/deploy/github-app-manifest.ts +53 -0
  28. package/src/deploy/image.ts +80 -0
  29. package/src/deploy/instance.ts +87 -0
  30. package/src/deploy/license-cache.ts +47 -0
  31. package/src/deploy/license.ts +272 -0
  32. package/src/deploy/paths.ts +65 -0
  33. package/src/deploy/post-deploy.ts +97 -0
  34. package/src/deploy/release.ts +282 -0
  35. package/src/deploy/runtime-secret.ts +241 -0
  36. package/src/deploy/setup.ts +393 -0
  37. package/src/deploy/sh.ts +74 -0
  38. package/src/deploy/slack-manifest.ts +112 -0
  39. package/src/deploy/stage-customization.ts +224 -0
  40. package/src/deploy/tracing.ts +243 -0
  41. package/src/deploy-permissions.ts +165 -0
  42. package/src/index.ts +60 -0
  43. package/src/lambda-bundle-context.ts +64 -0
  44. package/src/names.ts +170 -0
  45. package/src/release/kms.ts +86 -0
  46. package/src/release/manifest.ts +265 -0
  47. package/src/stacks/agent-stack.ts +938 -0
  48. package/src/stacks/api-stack.ts +1005 -0
  49. package/src/stacks/ci-stack.ts +96 -0
  50. package/src/stacks/data-stack.ts +446 -0
  51. package/src/stacks/network-stack.ts +282 -0
  52. package/src/stacks/newsletter-stack.ts +572 -0
  53. package/src/stacks/pipeline-stack.ts +242 -0
  54. package/src/stacks/release-account-stack.ts +229 -0
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Where the deployed bytes come from. **This is the one module a release
3
+ * changes.**
4
+ *
5
+ * Two modes, and every stack is written against the seam rather than either
6
+ * of them:
7
+ *
8
+ * **local build** (the default). Every Lambda bundle is built from this
9
+ * checkout at synth and the agent image is built and pushed by the deploy
10
+ * before the runtime references it by tag. A contributor synthesizes and
11
+ * deploys their working tree this way, and the snapshot tests run here with
12
+ * `FOUNDATION_SKIP_BUNDLE=1` — no Docker, no Bun bundle, no release.
13
+ *
14
+ * **release** (`-c release=vX.Y.Z` or `-c releaseManifest=<path>`, with
15
+ * `-c releaseBucket=<name>`). Nothing is built. Each Lambda's code is the
16
+ * object the signed manifest names in Foundation's release bucket, and the
17
+ * agent image is Foundation's own ECR repository pinned by DIGEST. This is
18
+ * what a customer's pipeline runs, and it is why that pipeline needs no
19
+ * Foundation checkout, no Docker and no Bun.
20
+ *
21
+ * The manifest reaching a synth has already been verified: `foundation-deploy`
22
+ * checks its KMS signature and refuses to call the CDK at all otherwise
23
+ * (`src/release/manifest.ts`). Reading it here is a plain file read, so a
24
+ * synth still needs no credentials and no network.
25
+ *
26
+ * Every entry point named here lives in Foundation: the gateway and its
27
+ * invoker in `packages/gateway`, each capability's proxy in its own
28
+ * `packages/capability-*`, the newsletter's three handlers in
29
+ * `packages/newsletter`. The bundle input is the Foundation workspace, not an
30
+ * instance repository — see `lambda-bundle-context.ts`.
31
+ */
32
+ import { spawnSync } from "node:child_process";
33
+ import { readFileSync } from "node:fs";
34
+ import { tmpdir } from "node:os";
35
+ import { dirname, isAbsolute, join, resolve } from "node:path";
36
+ import { fileURLToPath } from "node:url";
37
+ import * as cdk from "aws-cdk-lib";
38
+ import type * as ecr from "aws-cdk-lib/aws-ecr";
39
+ import * as lambda from "aws-cdk-lib/aws-lambda";
40
+ import * as s3 from "aws-cdk-lib/aws-s3";
41
+ import type { Construct } from "constructs";
42
+ import { lambdaBundleContext } from "./lambda-bundle-context.ts";
43
+ import { type ReleaseManifest, parseManifest } from "./release/manifest.ts";
44
+
45
+ /** Pinned Bun used to bundle every Lambda (and the docker fallback image tag). */
46
+ export const BUN_VERSION = "1.4.0";
47
+
48
+ /** This file is `<workspaceRoot>/packages/infra/src/artifacts.ts`. */
49
+ export const WORKSPACE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
50
+
51
+ /**
52
+ * The Lambdas a Foundation release carries, and the workspace-relative entry
53
+ * point each is built from. The keys are the names the release manifest will
54
+ * use for `releases/vX.Y.Z/lambda/<name>.zip`.
55
+ *
56
+ * `playwright` marks a bundle whose entry pulls `playwright-core`: its chromium
57
+ * driver must stay external and the output is a directory rather than a single
58
+ * file, so the bundler is invoked differently.
59
+ */
60
+ export const LAMBDA_ENTRY_POINTS = {
61
+ gateway: { entry: "packages/gateway/src/lambda.ts" },
62
+ invoker: { entry: "packages/gateway/src/invoker.ts" },
63
+ "email-proxy": { entry: "packages/capability-email/src/proxy/handler.ts" },
64
+ "otter-proxy": { entry: "packages/capability-otter/src/proxy/handler.ts" },
65
+ "knock-proxy": { entry: "packages/capability-knock/src/proxy/handler.ts" },
66
+ "crm-proxy": { entry: "packages/capability-crm/src/proxy/handler.ts" },
67
+ "upwork-proxy": { entry: "packages/capability-upwork/src/proxy/handler.ts" },
68
+ "browser-proxy": { entry: "packages/gateway/src/browser-proxy.ts", playwright: true },
69
+ "newsletter-public": { entry: "packages/newsletter/src/lambda-public.ts" },
70
+ "newsletter-campaign": { entry: "packages/newsletter/src/lambda-campaign.ts" },
71
+ "newsletter-ses-events": { entry: "packages/newsletter/src/lambda-ses-events.ts" },
72
+ } as const satisfies Record<string, { entry: string; playwright?: true }>;
73
+
74
+ export type LambdaArtifactId = keyof typeof LAMBDA_ENTRY_POINTS;
75
+
76
+ /**
77
+ * Is this synth allowed to skip bundling altogether?
78
+ *
79
+ * Template assertions and a no-Docker synth check want the stacks' shape, not
80
+ * their bytes, and a real bundle makes both depend on a working Bun or Docker
81
+ * toolchain. Read at call time so a test can set it before a stack is built.
82
+ *
83
+ * It says nothing about release mode: a release has bundles already and never
84
+ * stubs them, so {@link releaseSource} is consulted first everywhere below.
85
+ */
86
+ export function skipBundle(env: NodeJS.ProcessEnv = process.env): boolean {
87
+ return env.FOUNDATION_SKIP_BUNDLE === "1";
88
+ }
89
+
90
+ /** Where a synth gets the released bytes: one bucket, one verified manifest. */
91
+ export interface ReleaseSource {
92
+ version: string;
93
+ /** The Foundation release bucket every `lambda.*.key` is relative to. */
94
+ bucket: string;
95
+ manifest: ReleaseManifest;
96
+ }
97
+
98
+ /** Env fallbacks for the two release context keys, for a pipeline that sets env rather than flags. */
99
+ export const RELEASE_BUCKET_ENV = "FOUNDATION_RELEASE_BUCKET";
100
+ export const RELEASE_DIR_ENV = "FOUNDATION_RELEASE_DIR";
101
+
102
+ /**
103
+ * Where `foundation-deploy` leaves the manifest it verified, and where
104
+ * `-c release=<version>` looks for it when no explicit path is given.
105
+ *
106
+ * A directory under the system temp dir rather than one inside this package:
107
+ * the published tool may sit in a read-only `node_modules`, and the manifest
108
+ * is a cache of something immutable, not state worth keeping.
109
+ */
110
+ export function releaseCacheDir(version: string, env: NodeJS.ProcessEnv = process.env): string {
111
+ const configured = env[RELEASE_DIR_ENV];
112
+ if (configured !== undefined && configured !== "") return configured;
113
+ return join(tmpdir(), "foundation-release", version);
114
+ }
115
+
116
+ const manifestCache = new Map<string, ReleaseManifest>();
117
+
118
+ /** Read and validate a manifest from disk, once per path per process. */
119
+ export function readManifestFile(path: string): ReleaseManifest {
120
+ const absolute = isAbsolute(path) ? path : resolve(process.cwd(), path);
121
+ const cached = manifestCache.get(absolute);
122
+ if (cached !== undefined) return cached;
123
+ let text: string;
124
+ try {
125
+ text = readFileSync(absolute, "utf8");
126
+ } catch {
127
+ throw new Error(`no release manifest at ${absolute}`);
128
+ }
129
+ const manifest = parseManifest(text);
130
+ manifestCache.set(absolute, manifest);
131
+ return manifest;
132
+ }
133
+
134
+ /**
135
+ * The release this synth deploys, or `undefined` for the local-build default.
136
+ *
137
+ * `-c releaseManifest=<path>` is the authoritative form and what the deploy
138
+ * tool passes, having just verified that very file. `-c release=<version>`
139
+ * is the human form: same thing, found in {@link releaseCacheDir}. Either way
140
+ * `-c releaseBucket=<name>` (or `FOUNDATION_RELEASE_BUCKET`) says which bucket
141
+ * the manifest's keys live in, because the manifest deliberately does not name
142
+ * it — a release can be mirrored.
143
+ */
144
+ export function releaseSource(scope: Construct): ReleaseSource | undefined {
145
+ const node = scope.node;
146
+ const version: string | undefined = node.tryGetContext("release");
147
+ const manifestPath: string | undefined = node.tryGetContext("releaseManifest");
148
+ if (
149
+ (version === undefined || version === "") &&
150
+ (manifestPath === undefined || manifestPath === "")
151
+ )
152
+ return undefined;
153
+
154
+ const path =
155
+ manifestPath !== undefined && manifestPath !== ""
156
+ ? manifestPath
157
+ : join(releaseCacheDir(version as string), "manifest.json");
158
+ const manifest = readManifestFile(path);
159
+ if (version !== undefined && version !== "" && manifest.version !== version)
160
+ throw new Error(
161
+ `-c release=${version} but ${path} describes ${manifest.version}; pass the manifest for the version you are deploying`,
162
+ );
163
+
164
+ const bucket: string =
165
+ node.tryGetContext("releaseBucket") ?? process.env[RELEASE_BUCKET_ENV] ?? "";
166
+ if (bucket === "")
167
+ throw new Error(
168
+ `-c releaseBucket=<name> is required when deploying a release (or set ${RELEASE_BUCKET_ENV}); it is the bucket holding ${manifest.lambda.gateway?.key ?? "the release artifacts"}`,
169
+ );
170
+ return { version: manifest.version, bucket, manifest };
171
+ }
172
+
173
+ /**
174
+ * The release bucket as one imported construct per stack. Every Lambda in a
175
+ * stack references the same bucket, and importing it once keeps the template's
176
+ * `S3Bucket` a plain name rather than eleven identical constructs.
177
+ */
178
+ function releaseBucket(scope: Construct, source: ReleaseSource): s3.IBucket {
179
+ const stack = cdk.Stack.of(scope);
180
+ const existing = stack.node.tryFindChild("FoundationReleaseBucket");
181
+ if (existing !== undefined) return existing as s3.IBucket;
182
+ return s3.Bucket.fromBucketName(stack, "FoundationReleaseBucket", source.bucket);
183
+ }
184
+
185
+ /**
186
+ * The code for one released Lambda.
187
+ *
188
+ * In release mode it is the object the manifest names, referenced straight out
189
+ * of Foundation's release bucket: nothing is built, and the bytes
190
+ * CloudFormation fetches are the bytes the signature covered.
191
+ *
192
+ * Otherwise it is bundled locally with the pinned Bun when one is on the box;
193
+ * the docker image is the fallback for hosts without a Bun toolchain.
194
+ */
195
+ export function lambdaCode(scope: Construct, id: LambdaArtifactId): lambda.Code {
196
+ const source = releaseSource(scope);
197
+ if (source !== undefined) {
198
+ const artifact = source.manifest.lambda[id];
199
+ if (artifact === undefined)
200
+ throw new Error(
201
+ `release ${source.version} carries no Lambda bundle for "${id}"; it was built by an older Foundation`,
202
+ );
203
+ return lambda.Code.fromBucket(releaseBucket(scope, source), artifact.key);
204
+ }
205
+ if (skipBundle())
206
+ return lambda.Code.fromInline("export const handler = async () => ({statusCode: 200});");
207
+
208
+ const artifact: { entry: string; playwright?: true } = LAMBDA_ENTRY_POINTS[id];
209
+ const bundleRoot = lambdaBundleContext(WORKSPACE_ROOT);
210
+ return lambda.Code.fromAsset(bundleRoot, {
211
+ exclude: ["**/node_modules", "**/dist", "cdk.out"],
212
+ bundling: {
213
+ image: cdk.DockerImage.fromRegistry(`oven/bun:${BUN_VERSION}`),
214
+ command: [
215
+ "bash",
216
+ "-c",
217
+ `cd /asset-input && bun install --frozen-lockfile && ${bunBuild(artifact, "/asset-output").join(" ")}`,
218
+ ],
219
+ local: {
220
+ tryBundle(outputDir: string): boolean {
221
+ const bun = process.env.BUN_BIN ?? `${process.env.HOME}/.bun/bin/bun`;
222
+ const [, ...args] = bunBuild(artifact, outputDir);
223
+ const result = spawnSync(bun, args, { cwd: WORKSPACE_ROOT, stdio: "inherit" });
224
+ if (result.error !== undefined) return false;
225
+ return result.status === 0;
226
+ },
227
+ },
228
+ },
229
+ });
230
+ }
231
+
232
+ /**
233
+ * The bundler command, identical in the local path, the docker path and the
234
+ * RELEASE build, so no two of them can produce different bytes. The entry is
235
+ * workspace-relative, which is what `/asset-input`, a local checkout root and
236
+ * the release workflow's checkout all resolve it against.
237
+ */
238
+ export function bunBuild(
239
+ artifact: { entry: string; playwright?: true },
240
+ outputDir: string,
241
+ ): string[] {
242
+ return [
243
+ "bun",
244
+ "build",
245
+ artifact.entry,
246
+ "--target=node",
247
+ "--format=esm",
248
+ ...(artifact.playwright === true
249
+ ? [
250
+ // playwright-core resolves these lazily and neither exists on Lambda;
251
+ // bundling them fails the build rather than the invocation.
252
+ "--external",
253
+ "electron",
254
+ "--external",
255
+ "chromium-bidi",
256
+ `--outdir=${outputDir}`,
257
+ "--entry-naming=index.mjs",
258
+ ]
259
+ : [`--outfile=${join(outputDir, "index.mjs")}`]),
260
+ ];
261
+ }
262
+
263
+ /**
264
+ * The container URI the AgentCore runtime is created against.
265
+ *
266
+ * Locally the image was built from `packages/infra/agent-image/Dockerfile` by
267
+ * the deploy and pushed to the instance's OWN ECR repository, so the reference
268
+ * is `<repo>:<tag>` where the tag is the commit that was built. In release
269
+ * mode it is Foundation's repository pinned by digest — `<repo>@sha256:…`,
270
+ * which AgentCore accepts identically — and the customer's account pulls it
271
+ * across accounts under the repository policy Foundation publishes.
272
+ *
273
+ * `tag` is therefore only required in the local path; a release has no tag to
274
+ * pass, which is why {@link agentImageTagRequired} exists.
275
+ */
276
+ export function agentImage(
277
+ scope: Construct,
278
+ repository: ecr.IRepository,
279
+ tag: string | undefined,
280
+ ): string {
281
+ const source = releaseSource(scope);
282
+ if (source !== undefined)
283
+ return `${source.manifest.agentImage.repository}@${source.manifest.agentImage.digest}`;
284
+ if (tag === undefined || tag === "")
285
+ throw new Error("agentImage: a tag is required outside release mode");
286
+ return `${repository.repositoryUri}:${tag}`;
287
+ }
288
+
289
+ /** Does this synth need `-c agentImageTag=<tag>`? Not when a release names the image. */
290
+ export function agentImageTagRequired(scope: Construct): boolean {
291
+ return releaseSource(scope) === undefined;
292
+ }
293
+
294
+ /**
295
+ * The repository the agent image is pulled FROM, as an ARN, when that is not
296
+ * the instance's own repository.
297
+ *
298
+ * The AgentCore execution role's `ecr:BatchGetImage` is scoped to a
299
+ * repository, so a release deploy has to name Foundation's alongside the
300
+ * instance's. Derived from the manifest's repository URI rather than written
301
+ * down: `<account>.dkr.ecr.<region>.amazonaws.com/<name>`.
302
+ */
303
+ export function releaseImageRepositoryArn(scope: Construct): string | undefined {
304
+ const source = releaseSource(scope);
305
+ if (source === undefined) return undefined;
306
+ return ecrRepositoryArn(source.manifest.agentImage.repository);
307
+ }
308
+
309
+ /** `<account>.dkr.ecr.<region>.amazonaws.com/<name>` → `arn:aws:ecr:<region>:<account>:repository/<name>`. */
310
+ export function ecrRepositoryArn(repositoryUri: string): string {
311
+ const match = /^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/.exec(repositoryUri);
312
+ if (match === null)
313
+ throw new Error(
314
+ `not an ECR repository URI: ${repositoryUri} (expected <account>.dkr.ecr.<region>.amazonaws.com/<name>)`,
315
+ );
316
+ const [, account, region, name] = match;
317
+ return `arn:aws:ecr:${region}:${account}:repository/${name}`;
318
+ }
@@ -0,0 +1,29 @@
1
+ # GitHub App manifest (app-as-code): identity text per instance.
2
+ #
3
+ # A package asset, not a file in any instance repository. This is a TEMPLATE:
4
+ # `${displayName}` and `${url}` are substituted per instance by
5
+ # `github-app-manifest.ts` before the document is posted, so each deployment's
6
+ # App is named and described after its own teammate and repo.
7
+ #
8
+ # Fed to GitHub's App Manifest flow by `foundation-deploy github-app-create`.
9
+ #
10
+ # PERMISSIONS ARE NOT IN THIS FILE. They come from the capability registry
11
+ # (the `github` capability in `packages/core/src/capabilities.ts`) via
12
+ # requiredGithubPermissions(), plus `workflows: write` for instances that opt in
13
+ # through `github.workflowWriteRepos`. `github-app-manifest.ts` merges them in;
14
+ # a default_permissions block here is an error.
15
+ #
16
+ # Changing this file or an instance's workflowWriteRepos setting does NOT change
17
+ # a live installation: an org admin must accept the pending permission update.
18
+ # The broker's token request (`packages/connectors/src/github.ts`) must stay a
19
+ # subset of the permissions that installation has ACCEPTED.
20
+ #
21
+ # No webhook (hook_attributes omitted): the agent pulls via the REST API only.
22
+ name: ${displayName} Agent
23
+ url: ${url}
24
+ description: >-
25
+ ${displayName}'s GitHub connector. Mints short-lived (<=1h) installation
26
+ tokens so ${displayName} can use plain git/gh against the repos this App is
27
+ installed on.
28
+ public: false
29
+ default_events: []
@@ -0,0 +1,95 @@
1
+ # Declarative Slack app manifest (app-as-code), shared by every deployment.
2
+ #
3
+ # A package asset, not a file in any instance repository. ${DISPLAY_NAME} and
4
+ # ${BOT_NAME} come from the instance file's `displayName`, and ${COMMAND_PREFIX}
5
+ # from its `slack.commandPrefix`, so one template renders every deployment's app.
6
+ #
7
+ # Synced with the Slack CLI: `slack manifest validate`, `slack app install`
8
+ # (creates the app on first install), `slack manifest diff`. The CLI reads the
9
+ # manifest through a `get-manifest` hook, which `foundation-deploy` renders with
10
+ # the request URLs substituted from env:
11
+ # ${EVENTS_REQUEST_URL} https://<api-id>.execute-api.<region>.amazonaws.com/prod/slack/events
12
+ # ${COMMANDS_REQUEST_URL} same host, /slack/commands
13
+ # ${INTERACTIVE_REQUEST_URL} same host, /slack/interactive
14
+ # When those env vars are unset (before the API stack is deployed), the hook
15
+ # STRIPS event_subscriptions, slash_commands and interactivity entirely — Slack
16
+ # verifies request URLs on manifest create, so a placeholder would be rejected.
17
+ #
18
+ # Console-only: keep the app private / non-distributed. Adding a bot scope
19
+ # (in the registry) still requires a workspace REINSTALL (`slack app install`).
20
+
21
+ display_information:
22
+ name: ${DISPLAY_NAME}
23
+ description: ${DISPLAY_NAME} — our AI teammate. Mention @${DISPLAY_NAME} in a channel or DM me.
24
+ background_color: "#2e6fb7"
25
+ long_description: >-
26
+ ${DISPLAY_NAME} is our self-hosted AI teammate. Mention @${DISPLAY_NAME} in a channel to
27
+ start a threaded session, reply in the thread to continue it, or DM
28
+ ${DISPLAY_NAME} directly. ${DISPLAY_NAME} can work in our GitHub repos, remembers
29
+ per-channel context, and runs on our own infrastructure.
30
+
31
+ features:
32
+ bot_user:
33
+ display_name: ${BOT_NAME}
34
+ always_online: true
35
+ app_home:
36
+ home_tab_enabled: false
37
+ messages_tab_enabled: true
38
+ messages_tab_read_only_enabled: false
39
+ # agent_view (not the deprecated assistant_view): DMs behave like a normal
40
+ # writable DM with threaded replies. ONE-WAY DOOR — Slack does not allow
41
+ # reverting agent_view -> assistant_view.
42
+ agent_view:
43
+ agent_description: Our AI teammate. Ask anything, or hand ${DISPLAY_NAME} a coding task in one of our repos.
44
+ suggested_prompts:
45
+ - title: What can you do?
46
+ message: What can you help with, and which repos can you work in?
47
+ # slash_commands MUST live under `features` (a `settings.slash_commands`
48
+ # block is silently dropped by Slack).
49
+ # Command SUFFIXES are product-level (declared by the capability registry);
50
+ # the prefix is the instance's `slack.commandPrefix` and is substituted here
51
+ # as ${COMMAND_PREFIX}. The gateway matches by suffix.
52
+ slash_commands:
53
+ - command: /${COMMAND_PREFIX}-help
54
+ url: ${COMMANDS_REQUEST_URL}
55
+ description: What ${DISPLAY_NAME} can do
56
+ should_escape: false
57
+ - command: /${COMMAND_PREFIX}-calendar-connect
58
+ url: ${COMMANDS_REQUEST_URL}
59
+ description: Connect your own Google Calendar to ${DISPLAY_NAME}
60
+ should_escape: false
61
+ # Admins only, and it sets the ONE Drive identity the whole workspace
62
+ # reads through, not the caller's own Drive.
63
+ - command: /${COMMAND_PREFIX}-drive-connect
64
+ url: ${COMMANDS_REQUEST_URL}
65
+ description: Connect the Google Drive account ${DISPLAY_NAME} reads as (admins)
66
+ # Admins only for the same reason: one company mailbox, not the caller's.
67
+ - command: /${COMMAND_PREFIX}-email-connect
68
+ url: ${COMMANDS_REQUEST_URL}
69
+ description: Connect the mailbox ${DISPLAY_NAME} drafts from (admins)
70
+ should_escape: false
71
+ - command: /${COMMAND_PREFIX}-knock-connect
72
+ url: ${COMMANDS_REQUEST_URL}
73
+ description: Connect ${DISPLAY_NAME} to Knock read-only tools (admins)
74
+ should_escape: false
75
+ # Admins only: one company Upwork app/profile, with credentials isolated in
76
+ # a proxy rather than exposed to the agent runtime.
77
+ - command: /${COMMAND_PREFIX}-upwork-connect
78
+ url: ${COMMANDS_REQUEST_URL}
79
+ description: Connect the Upwork account ${DISPLAY_NAME} uses (admins)
80
+ should_escape: false
81
+
82
+ # oauth_config.scopes.bot and settings.event_subscriptions.bot_events are
83
+ # GENERATED from the capability registry by `slack-manifest.ts`
84
+ # (requiredSlackScopes / BASE_SLACK_EVENTS). Listing scopes here is an error.
85
+ oauth_config: {}
86
+
87
+ settings:
88
+ event_subscriptions:
89
+ request_url: ${EVENTS_REQUEST_URL}
90
+ interactivity:
91
+ is_enabled: true
92
+ request_url: ${INTERACTIVE_REQUEST_URL}
93
+ org_deploy_enabled: false
94
+ socket_mode_enabled: false
95
+ token_rotation_enabled: false