@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.
- package/README.md +174 -0
- package/agent-image/Dockerfile +254 -0
- package/agent-image/bin/aws +36 -0
- package/agent-image/bin/gh +193 -0
- package/agent-image/bin/git-credential-sky +89 -0
- package/agent-image/security-overlay.yml +176 -0
- package/cdk.json +6 -0
- package/dist/bin/app.js +112 -0
- package/dist/bin/foundation-deploy.js +1906 -0
- package/dist/bin/release-account.js +154 -0
- package/dist/chunk-4aye5cee.js +2416 -0
- package/dist/chunk-9ddxyvq2.js +1455 -0
- package/dist/chunk-v7tz8g50.js +428 -0
- package/dist/src/index.js +88 -0
- package/package.json +38 -0
- package/pipeline/buildspec.yml +34 -0
- package/src/artifacts.ts +318 -0
- package/src/deploy/assets/github-app-manifest.yml +29 -0
- package/src/deploy/assets/slack-app-manifest.yml +95 -0
- package/src/deploy/aws.ts +265 -0
- package/src/deploy/cli.ts +212 -0
- package/src/deploy/config-sync.ts +93 -0
- package/src/deploy/config.ts +29 -0
- package/src/deploy/deploy.ts +566 -0
- package/src/deploy/endpoint.ts +242 -0
- package/src/deploy/github-app-create.ts +154 -0
- package/src/deploy/github-app-manifest.ts +53 -0
- package/src/deploy/image.ts +80 -0
- package/src/deploy/instance.ts +87 -0
- package/src/deploy/license-cache.ts +47 -0
- package/src/deploy/license.ts +272 -0
- package/src/deploy/paths.ts +65 -0
- package/src/deploy/post-deploy.ts +97 -0
- package/src/deploy/release.ts +282 -0
- package/src/deploy/runtime-secret.ts +241 -0
- package/src/deploy/setup.ts +393 -0
- package/src/deploy/sh.ts +74 -0
- package/src/deploy/slack-manifest.ts +112 -0
- package/src/deploy/stage-customization.ts +224 -0
- package/src/deploy/tracing.ts +243 -0
- package/src/deploy-permissions.ts +165 -0
- package/src/index.ts +60 -0
- package/src/lambda-bundle-context.ts +64 -0
- package/src/names.ts +170 -0
- package/src/release/kms.ts +86 -0
- package/src/release/manifest.ts +265 -0
- package/src/stacks/agent-stack.ts +938 -0
- package/src/stacks/api-stack.ts +1005 -0
- package/src/stacks/ci-stack.ts +96 -0
- package/src/stacks/data-stack.ts +446 -0
- package/src/stacks/network-stack.ts +282 -0
- package/src/stacks/newsletter-stack.ts +572 -0
- package/src/stacks/pipeline-stack.ts +242 -0
- package/src/stacks/release-account-stack.ts +229 -0
package/src/artifacts.ts
ADDED
|
@@ -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
|