@deployfoundation/foundation-deploy 0.2.23 → 0.2.25
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/agent-image/Dockerfile +1 -0
- package/dist/bin/app.js +2 -2
- package/dist/bin/foundation-deploy.js +5 -2
- package/dist/bin/release-account.js +2 -2
- package/dist/{chunk-j3etxk5j.js → chunk-165c7rv9.js} +1 -1
- package/dist/{chunk-hzrrx2pq.js → chunk-7krjky1x.js} +100 -1
- package/dist/{chunk-bp4yddcp.js → chunk-f4s4c4bg.js} +41 -0
- package/dist/src/index.js +2 -2
- package/package.json +1 -1
- package/src/artifacts.ts +1 -0
- package/src/deploy/assets/slack-app-manifest.yml +5 -0
- package/src/deploy/deploy.ts +4 -0
- package/src/deploy/runtime-secret.ts +5 -0
- package/src/stacks/agent-stack.ts +41 -0
- package/src/stacks/api-stack.ts +97 -0
package/agent-image/Dockerfile
CHANGED
|
@@ -250,6 +250,7 @@ COPY packages/capability-otter/ ./packages/capability-otter/
|
|
|
250
250
|
COPY packages/capability-knock/ ./packages/capability-knock/
|
|
251
251
|
COPY packages/capability-browser/ ./packages/capability-browser/
|
|
252
252
|
COPY packages/capability-email/ ./packages/capability-email/
|
|
253
|
+
COPY packages/capability-google-docs/ ./packages/capability-google-docs/
|
|
253
254
|
COPY packages/capability-crm/ ./packages/capability-crm/
|
|
254
255
|
COPY packages/capability-upwork/ ./packages/capability-upwork/
|
|
255
256
|
COPY packages/capability-etrade/ ./packages/capability-etrade/
|
package/dist/bin/app.js
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
FoundationPages,
|
|
9
9
|
FoundationPipeline,
|
|
10
10
|
NewsletterStack
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-7krjky1x.js";
|
|
12
12
|
import {
|
|
13
13
|
adminsFor,
|
|
14
14
|
instanceNames,
|
|
15
15
|
loadInstanceFile,
|
|
16
16
|
provisionsIntegration
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-f4s4c4bg.js";
|
|
18
18
|
|
|
19
19
|
// bin/app.ts
|
|
20
20
|
import { execFileSync } from "node:child_process";
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
stackExists,
|
|
30
30
|
stackOutput,
|
|
31
31
|
toolVersion
|
|
32
|
-
} from "../chunk-
|
|
32
|
+
} from "../chunk-165c7rv9.js";
|
|
33
33
|
import {
|
|
34
34
|
BASE_SLACK_EVENTS,
|
|
35
35
|
CAPABILITY_IDS,
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
requiredSlackScopes,
|
|
46
46
|
skillsKey,
|
|
47
47
|
slackCommandPrefix
|
|
48
|
-
} from "../chunk-
|
|
48
|
+
} from "../chunk-f4s4c4bg.js";
|
|
49
49
|
|
|
50
50
|
// src/deploy/config-sync.ts
|
|
51
51
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync } from "node:fs";
|
|
@@ -932,6 +932,7 @@ function composeRuntimeSecret(inputs) {
|
|
|
932
932
|
FOUNDATION_ROUTINE_GROUP: names.routineGroup,
|
|
933
933
|
FOUNDATION_AGENT_ENDPOINT: inputs.agentEndpoint !== undefined && inputs.agentEndpoint !== "" ? inputs.agentEndpoint : LIVE_ENDPOINT,
|
|
934
934
|
...inputs.readOnlyRoleArn !== undefined && inputs.readOnlyRoleArn !== "" ? { FOUNDATION_AWS_READONLY_ROLE_ARN: inputs.readOnlyRoleArn } : {},
|
|
935
|
+
...inputs.googleDocsProxyFunctionArn !== undefined && inputs.googleDocsProxyFunctionArn !== "" ? { FOUNDATION_GOOGLE_DOCS_PROXY_ARN: inputs.googleDocsProxyFunctionArn } : {},
|
|
935
936
|
...inputs.emailProxyFunctionArn !== undefined && inputs.emailProxyFunctionArn !== "" ? { FOUNDATION_EMAIL_PROXY_FUNCTION_ARN: inputs.emailProxyFunctionArn } : {},
|
|
936
937
|
...inputs.browserProxyFunctionArn !== undefined && inputs.browserProxyFunctionArn !== "" ? { FOUNDATION_BROWSER_PROXY_FUNCTION_ARN: inputs.browserProxyFunctionArn } : {},
|
|
937
938
|
...inputs.crmProxyFunctionArn !== undefined && inputs.crmProxyFunctionArn !== "" ? { FOUNDATION_CRM_PROXY_FUNCTION_ARN: inputs.crmProxyFunctionArn } : {},
|
|
@@ -1260,6 +1261,7 @@ async function syncRuntimeSecret(ctx) {
|
|
|
1260
1261
|
const agentRuntimeArn = dry ? `<${agent}.AgentRuntimeArn>` : await optionalOutput(ctx, agent, "AgentRuntimeArn");
|
|
1261
1262
|
const readOnlyRoleArn = dry ? `<${agent}.ReadOnlyRoleArn>` : await optionalOutput(ctx, agent, "ReadOnlyRoleArn");
|
|
1262
1263
|
const emailProxyFunctionArn = dry ? `<${api}.EmailProxyFunctionArn>` : await optionalOutput(ctx, api, "EmailProxyFunctionArn");
|
|
1264
|
+
const googleDocsProxyFunctionArn = dry ? `<${api}.GoogleDocsProxyFunctionArn>` : await optionalOutput(ctx, api, "GoogleDocsProxyFunctionArn");
|
|
1263
1265
|
const browserProxyFunctionArn = dry ? `<${api}.BrowserProxyFunctionArn>` : await optionalOutput(ctx, api, "BrowserProxyFunctionArn");
|
|
1264
1266
|
const crmProxyFunctionArn = ctx.instance.integrations.crm ? dry ? `<${api}.CrmProxyFunctionArn>` : await optionalOutput(ctx, api, "CrmProxyFunctionArn") : undefined;
|
|
1265
1267
|
const crmPolicyFingerprint = ctx.instance.integrations.crm ? dry ? `<${api}.CrmPolicyFingerprint>` : await optionalOutput(ctx, api, "CrmPolicyFingerprint") : undefined;
|
|
@@ -1286,6 +1288,7 @@ async function syncRuntimeSecret(ctx) {
|
|
|
1286
1288
|
...agentRuntimeArn === undefined ? {} : { agentRuntimeArn },
|
|
1287
1289
|
...readOnlyRoleArn === undefined ? {} : { readOnlyRoleArn },
|
|
1288
1290
|
...emailProxyFunctionArn === undefined ? {} : { emailProxyFunctionArn },
|
|
1291
|
+
...googleDocsProxyFunctionArn === undefined ? {} : { googleDocsProxyFunctionArn },
|
|
1289
1292
|
...browserProxyFunctionArn === undefined ? {} : { browserProxyFunctionArn },
|
|
1290
1293
|
...crmProxyFunctionArn === undefined ? {} : { crmProxyFunctionArn },
|
|
1291
1294
|
...crmPolicyFingerprint === undefined ? {} : { crmPolicyFingerprint },
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
scheduledJobBindings,
|
|
20
20
|
slackCommandPrefixes,
|
|
21
21
|
webhookJobBindings
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-f4s4c4bg.js";
|
|
23
23
|
|
|
24
24
|
// src/stacks/agent-stack.ts
|
|
25
25
|
import * as cdk from "aws-cdk-lib";
|
|
@@ -240,6 +240,21 @@ class FoundationAgent extends cdk.Stack {
|
|
|
240
240
|
resources: [secretArnPattern(knockOauthClient), secretArnPattern(knockCredential)]
|
|
241
241
|
}));
|
|
242
242
|
}
|
|
243
|
+
denyDirectDocsSecretAccess(role, docsSecretPrefix) {
|
|
244
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
245
|
+
sid: "DenyGoogleDocsSecrets",
|
|
246
|
+
effect: iam.Effect.DENY,
|
|
247
|
+
actions: ["secretsmanager:*"],
|
|
248
|
+
resources: [
|
|
249
|
+
cdk.Stack.of(this).formatArn({
|
|
250
|
+
service: "secretsmanager",
|
|
251
|
+
resource: "secret",
|
|
252
|
+
resourceName: `${docsSecretPrefix}/personal/*-??????`,
|
|
253
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
|
|
254
|
+
})
|
|
255
|
+
]
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
243
258
|
denyDirectEmailSecretAccess(role, sharedMailboxSecret, personalSecretPrefix) {
|
|
244
259
|
const stack = cdk.Stack.of(this);
|
|
245
260
|
const personalSecrets = stack.formatArn({
|
|
@@ -358,6 +373,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
358
373
|
this.denyDirectCrmTableAccess(role, props.instance, props.crmTable);
|
|
359
374
|
this.denyDirectKnockSecretAccess(role, props);
|
|
360
375
|
this.denyDirectEmailSecretAccess(role, props.secrets.googleEmail, instanceNames(props.instance).secretGoogleEmail);
|
|
376
|
+
this.denyDirectDocsSecretAccess(role, instanceNames(props.instance).secretGoogleDocs);
|
|
361
377
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
362
378
|
this.denyDirectEtradeAccess(role, props);
|
|
363
379
|
this.pagesPolicy("AgentReadOnlyPagesPolicy", role, props.instance, { invoke: false });
|
|
@@ -421,6 +437,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
421
437
|
this.denyDirectKnockSecretAccess(role, props);
|
|
422
438
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
423
439
|
this.denyDirectEmailSecretAccess(role, props.secrets.googleEmail, instanceNames(props.instance).secretGoogleEmail);
|
|
440
|
+
this.denyDirectDocsSecretAccess(role, instanceNames(props.instance).secretGoogleDocs);
|
|
424
441
|
this.denyDirectEtradeAccess(role, props);
|
|
425
442
|
this.pagesPolicy("AgentExecutionPagesPolicy", role, props.instance, { invoke: true });
|
|
426
443
|
const releaseRepositoryArn = releaseImageRepositoryArn(this);
|
|
@@ -489,6 +506,18 @@ class FoundationAgent extends cdk.Stack {
|
|
|
489
506
|
actions: ["secretsmanager:GetSecretValue"],
|
|
490
507
|
resources: [secretArnPattern(props.secrets.signing)]
|
|
491
508
|
}));
|
|
509
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
510
|
+
sid: "GoogleDocsProxyInvoke",
|
|
511
|
+
actions: ["lambda:InvokeFunction"],
|
|
512
|
+
resources: [
|
|
513
|
+
cdk.Stack.of(this).formatArn({
|
|
514
|
+
service: "lambda",
|
|
515
|
+
resource: "function",
|
|
516
|
+
resourceName: instanceNames(props.instance).googleDocsProxyFunctionName,
|
|
517
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
|
|
518
|
+
})
|
|
519
|
+
]
|
|
520
|
+
}));
|
|
492
521
|
role.addToPolicy(new iam.PolicyStatement({
|
|
493
522
|
sid: "EmailProxyInvoke",
|
|
494
523
|
actions: ["lambda:InvokeFunction"],
|
|
@@ -788,6 +817,7 @@ class FoundationApi extends cdk2.Stack {
|
|
|
788
817
|
invokeQueue;
|
|
789
818
|
invokeDlq;
|
|
790
819
|
emailProxyFunction;
|
|
820
|
+
googleDocsProxyFunction;
|
|
791
821
|
browserProxyFunction;
|
|
792
822
|
otterProxyFunction;
|
|
793
823
|
crmProxyFunction;
|
|
@@ -799,6 +829,7 @@ class FoundationApi extends cdk2.Stack {
|
|
|
799
829
|
const names = instanceNames(props.instance);
|
|
800
830
|
const display = props.instance.displayName;
|
|
801
831
|
const emailIdentities = emailIdentitiesFor(props.configPath);
|
|
832
|
+
const docsIdentities = googleDocsIdentitiesFor(props.configPath);
|
|
802
833
|
const personalEmailIdentities = emailIdentities.filter((identity) => identity.owner !== undefined);
|
|
803
834
|
const upwork = provisionsIntegration(props.instance, "upwork") ? upworkConfigForProvisioning(props.configPath) : undefined;
|
|
804
835
|
const upworkSecret = upwork === undefined ? undefined : props.secrets.upwork;
|
|
@@ -846,6 +877,15 @@ class FoundationApi extends cdk2.Stack {
|
|
|
846
877
|
generateStringKey: "placeholder"
|
|
847
878
|
}
|
|
848
879
|
}));
|
|
880
|
+
const docsSecrets = docsIdentities.map((identity) => new secretsmanager.Secret(this, `GoogleDocsSecret${identity.owner}`, {
|
|
881
|
+
secretName: `${instanceNames(props.instance).secretGoogleDocs}/personal/${identity.owner}`,
|
|
882
|
+
description: `${display} Google Docs drafting grant for Slack ${identity.owner}; gateway writes and the docs proxy reads`,
|
|
883
|
+
removalPolicy: cdk2.RemovalPolicy.DESTROY,
|
|
884
|
+
generateSecretString: {
|
|
885
|
+
secretStringTemplate: "{}",
|
|
886
|
+
generateStringKey: "placeholder"
|
|
887
|
+
}
|
|
888
|
+
}));
|
|
849
889
|
const gatewayRole = new iam2.Role(this, "GatewayRole", {
|
|
850
890
|
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
851
891
|
description: `${display} gateway Lambda role (least privilege)`,
|
|
@@ -877,6 +917,13 @@ class FoundationApi extends cdk2.Stack {
|
|
|
877
917
|
...personalEmailSecrets.map(secretArnPattern2)
|
|
878
918
|
]
|
|
879
919
|
}));
|
|
920
|
+
if (docsSecrets.length > 0) {
|
|
921
|
+
gatewayRole.addToPolicy(new iam2.PolicyStatement({
|
|
922
|
+
sid: "GoogleDocsIdentityConnect",
|
|
923
|
+
actions: ["secretsmanager:PutSecretValue"],
|
|
924
|
+
resources: docsSecrets.map(secretArnPattern2)
|
|
925
|
+
}));
|
|
926
|
+
}
|
|
880
927
|
if (props.secrets.knockCredential !== undefined) {
|
|
881
928
|
gatewayRole.addToPolicy(new iam2.PolicyStatement({
|
|
882
929
|
sid: "KnockCredentialConnect",
|
|
@@ -982,6 +1029,10 @@ class FoundationApi extends cdk2.Stack {
|
|
|
982
1029
|
owner
|
|
983
1030
|
})))
|
|
984
1031
|
},
|
|
1032
|
+
...docsIdentities.length === 0 ? {} : {
|
|
1033
|
+
FOUNDATION_GOOGLE_DOCS_SECRET_ID: instanceNames(props.instance).secretGoogleDocs,
|
|
1034
|
+
FOUNDATION_GOOGLE_DOCS_IDENTITIES: JSON.stringify(docsIdentities)
|
|
1035
|
+
},
|
|
985
1036
|
...props.secrets.knockOauthClient === undefined ? {} : { FOUNDATION_KNOCK_OAUTH_CLIENT_SECRET_ID: props.secrets.knockOauthClient.secretName },
|
|
986
1037
|
...props.secrets.knockCredential === undefined ? {} : { FOUNDATION_KNOCK_CREDENTIAL_SECRET_ID: props.secrets.knockCredential.secretName },
|
|
987
1038
|
...upworkSecret === undefined ? {} : { FOUNDATION_UPWORK_SECRET_ID: upworkSecret.secretName },
|
|
@@ -1152,6 +1203,50 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1152
1203
|
FOUNDATION_EMAIL_IDENTITIES: JSON.stringify(emailIdentities)
|
|
1153
1204
|
}
|
|
1154
1205
|
});
|
|
1206
|
+
if (docsIdentities.length > 0) {
|
|
1207
|
+
const docsProxyRole = new iam2.Role(this, "GoogleDocsProxyRole", {
|
|
1208
|
+
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
1209
|
+
description: `${display} Google Docs drafts proxy role (the only reader of a docs grant)`,
|
|
1210
|
+
managedPolicies: [
|
|
1211
|
+
iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1212
|
+
]
|
|
1213
|
+
});
|
|
1214
|
+
docsProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1215
|
+
sid: "GoogleDocsProxyCredentials",
|
|
1216
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
1217
|
+
resources: [
|
|
1218
|
+
...docsSecrets.map(secretArnPattern2),
|
|
1219
|
+
secretArnPattern2(props.secrets.googleOauth),
|
|
1220
|
+
secretArnPattern2(props.secrets.signing)
|
|
1221
|
+
]
|
|
1222
|
+
}));
|
|
1223
|
+
docsProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1224
|
+
sid: "GoogleDocsProxyDataKey",
|
|
1225
|
+
actions: ["kms:Decrypt"],
|
|
1226
|
+
resources: [props.dataKey.keyArn]
|
|
1227
|
+
}));
|
|
1228
|
+
this.googleDocsProxyFunction = new lambda.Function(this, "GoogleDocsProxyFunction", {
|
|
1229
|
+
functionName: instanceNames(props.instance).googleDocsProxyFunctionName,
|
|
1230
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
1231
|
+
handler: "index.handler",
|
|
1232
|
+
code: lambdaCode(this, "google-docs-proxy"),
|
|
1233
|
+
role: docsProxyRole,
|
|
1234
|
+
timeout: cdk2.Duration.seconds(30),
|
|
1235
|
+
memorySize: 256,
|
|
1236
|
+
environment: {
|
|
1237
|
+
FOUNDATION_GOOGLE_DOCS_SECRET_ID: instanceNames(props.instance).secretGoogleDocs,
|
|
1238
|
+
FOUNDATION_GOOGLE_OAUTH_SECRET_ID: instanceNames(props.instance).secretGoogleOauth,
|
|
1239
|
+
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
1240
|
+
FOUNDATION_SLACK_TEAM_ID: props.instance.slack.teamId,
|
|
1241
|
+
FOUNDATION_GOOGLE_DOCS_IDENTITIES: JSON.stringify(docsIdentities),
|
|
1242
|
+
FOUNDATION_GOOGLE_DOCS_FOLDER: `${props.instance.displayName} drafts`
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
new cdk2.CfnOutput(this, "GoogleDocsProxyFunctionArn", {
|
|
1246
|
+
value: this.googleDocsProxyFunction.functionArn,
|
|
1247
|
+
description: "The Google Docs drafts proxy the agent invokes"
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1155
1250
|
const browserDomains = provisionsIntegration(props.instance, "browser") ? browserDomainsFor(props.configPath) : [];
|
|
1156
1251
|
if (browserDomains.length > 0) {
|
|
1157
1252
|
const browserProxyRole = new iam2.Role(this, "BrowserProxyRole", {
|
|
@@ -1656,6 +1751,10 @@ function etradeConfigForProvisioning(configPath) {
|
|
|
1656
1751
|
function configTimezoneFor(configPath) {
|
|
1657
1752
|
return parseInstanceConfig(readFileSync(configPath, "utf8")).timezone;
|
|
1658
1753
|
}
|
|
1754
|
+
function googleDocsIdentitiesFor(configPath) {
|
|
1755
|
+
const docs = parseInstanceConfig(readFileSync(configPath, "utf8")).capabilities.googleDocs;
|
|
1756
|
+
return docs.enabled ? docs.identities.map(({ address, channel, owner }) => ({ address, channel, owner })) : [];
|
|
1757
|
+
}
|
|
1659
1758
|
function emailIdentitiesFor(configPath) {
|
|
1660
1759
|
const email = parseInstanceConfig(readFileSync(configPath, "utf8")).capabilities.email;
|
|
1661
1760
|
return email.enabled ? email.identities : [];
|
|
@@ -110,6 +110,34 @@ var RawGoogleDriveCapability = z.object({
|
|
|
110
110
|
enabled: z.boolean().default(true),
|
|
111
111
|
channels: z.array(RawSlackChannel).max(100).default([])
|
|
112
112
|
}).strict().default({});
|
|
113
|
+
var RawGoogleDocsIdentity = z.object({
|
|
114
|
+
address: z.string().regex(/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i),
|
|
115
|
+
channel: z.string().regex(/^[CGD][A-Z0-9]{8,}$/),
|
|
116
|
+
owner: z.string().regex(/^U[A-Z0-9]{8,}$/, "owner must be a Slack U id")
|
|
117
|
+
}).strict();
|
|
118
|
+
var RawGoogleDocsCapability = z.object({
|
|
119
|
+
enabled: z.boolean().default(false),
|
|
120
|
+
identities: z.array(RawGoogleDocsIdentity).max(50).default([])
|
|
121
|
+
}).strict().superRefine((value, ctx) => {
|
|
122
|
+
if (value.enabled && value.identities.length === 0)
|
|
123
|
+
ctx.addIssue({
|
|
124
|
+
code: z.ZodIssueCode.custom,
|
|
125
|
+
path: ["identities"],
|
|
126
|
+
message: "an enabled Google Docs capability needs at least one identity"
|
|
127
|
+
});
|
|
128
|
+
for (const field of ["owner", "channel"]) {
|
|
129
|
+
const seen = new Set;
|
|
130
|
+
for (const [index, identity] of value.identities.entries()) {
|
|
131
|
+
if (seen.has(identity[field]))
|
|
132
|
+
ctx.addIssue({
|
|
133
|
+
code: z.ZodIssueCode.custom,
|
|
134
|
+
path: ["identities", index, field],
|
|
135
|
+
message: `duplicate Google Docs identity ${field}: ${identity[field]}`
|
|
136
|
+
});
|
|
137
|
+
seen.add(identity[field]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}).default({});
|
|
113
141
|
var RawCrmOutreach = z.object({
|
|
114
142
|
weeklyLimit: z.number().int().min(1).max(20).default(5),
|
|
115
143
|
eligibleStatuses: z.array(RawCrmValue).max(100).default([]),
|
|
@@ -373,6 +401,16 @@ var CAPABILITIES = [
|
|
|
373
401
|
slackCommands: ["drive-connect"],
|
|
374
402
|
googleScopes: ["https://www.googleapis.com/auth/drive.readonly"]
|
|
375
403
|
},
|
|
404
|
+
{
|
|
405
|
+
id: "googleDocs",
|
|
406
|
+
summary: "Draft Google Docs in a person's own Drive, by personal consent; it reaches only the Docs it created.",
|
|
407
|
+
config: RawGoogleDocsCapability,
|
|
408
|
+
...NONE,
|
|
409
|
+
secrets: ["google/docs", GOOGLE_OAUTH_SECRET],
|
|
410
|
+
resources: ["googleDocsProxyFunctionName"],
|
|
411
|
+
slackCommands: ["docs-connect"],
|
|
412
|
+
googleScopes: ["https://www.googleapis.com/auth/drive.file", ...GOOGLE_IDENTITY_SCOPES]
|
|
413
|
+
},
|
|
376
414
|
{
|
|
377
415
|
id: "googleCalendar",
|
|
378
416
|
summary: "Each person's own calendar plus team free/busy, by personal consent.",
|
|
@@ -863,6 +901,7 @@ function instanceNames(instance) {
|
|
|
863
901
|
secretGoogleCalendar: secret("google/calendar"),
|
|
864
902
|
secretGoogleOauth: secret("google/oauth"),
|
|
865
903
|
secretGoogleEmail: secret("google/email"),
|
|
904
|
+
secretGoogleDocs: secret("google/docs"),
|
|
866
905
|
secretMongodbReadonly: secret("mongodb/readonly"),
|
|
867
906
|
secretOtterApi: secret("otter/api"),
|
|
868
907
|
secretKnockOauthClient: secret("knock/oauth-client"),
|
|
@@ -886,6 +925,7 @@ function instanceNames(instance) {
|
|
|
886
925
|
deployRole: stack("DeployRole"),
|
|
887
926
|
configKey: instance.config,
|
|
888
927
|
emailProxyFunctionName: stack("EmailProxy"),
|
|
928
|
+
googleDocsProxyFunctionName: stack("GoogleDocsProxy"),
|
|
889
929
|
otterProxyFunctionName: stack("OtterProxy"),
|
|
890
930
|
crmProxyFunctionName: stack("CrmProxy"),
|
|
891
931
|
pagesApiFunctionName: stack("PagesApi"),
|
|
@@ -1556,6 +1596,7 @@ var LAMBDA_ENTRY_POINTS = {
|
|
|
1556
1596
|
gateway: { entry: "packages/gateway/src/lambda.ts" },
|
|
1557
1597
|
invoker: { entry: "packages/gateway/src/invoker.ts" },
|
|
1558
1598
|
"email-proxy": { entry: "packages/capability-email/src/proxy/handler.ts" },
|
|
1599
|
+
"google-docs-proxy": { entry: "packages/capability-google-docs/src/proxy/handler.ts" },
|
|
1559
1600
|
"otter-proxy": { entry: "packages/capability-otter/src/proxy/handler.ts" },
|
|
1560
1601
|
"knock-proxy": { entry: "packages/capability-knock/src/proxy/handler.ts" },
|
|
1561
1602
|
"crm-proxy": { entry: "packages/capability-crm/src/proxy/handler.ts" },
|
package/dist/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
FoundationPipeline,
|
|
11
11
|
NewsletterStack,
|
|
12
12
|
deployStatements
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-7krjky1x.js";
|
|
14
14
|
import {
|
|
15
15
|
BUN_VERSION,
|
|
16
16
|
LAMBDA_ENTRY_POINTS,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
skillsKey,
|
|
44
44
|
skipBundle,
|
|
45
45
|
verifyManifest
|
|
46
|
-
} from "../chunk-
|
|
46
|
+
} from "../chunk-f4s4c4bg.js";
|
|
47
47
|
export {
|
|
48
48
|
BUILD_TIMEOUT,
|
|
49
49
|
BUN_VERSION,
|
package/package.json
CHANGED
package/src/artifacts.ts
CHANGED
|
@@ -61,6 +61,7 @@ export const LAMBDA_ENTRY_POINTS = {
|
|
|
61
61
|
gateway: { entry: "packages/gateway/src/lambda.ts" },
|
|
62
62
|
invoker: { entry: "packages/gateway/src/invoker.ts" },
|
|
63
63
|
"email-proxy": { entry: "packages/capability-email/src/proxy/handler.ts" },
|
|
64
|
+
"google-docs-proxy": { entry: "packages/capability-google-docs/src/proxy/handler.ts" },
|
|
64
65
|
"otter-proxy": { entry: "packages/capability-otter/src/proxy/handler.ts" },
|
|
65
66
|
"knock-proxy": { entry: "packages/capability-knock/src/proxy/handler.ts" },
|
|
66
67
|
"crm-proxy": { entry: "packages/capability-crm/src/proxy/handler.ts" },
|
|
@@ -68,6 +68,11 @@ features:
|
|
|
68
68
|
url: ${COMMANDS_REQUEST_URL}
|
|
69
69
|
description: Connect the mailbox ${DISPLAY_NAME} drafts from (admins)
|
|
70
70
|
should_escape: false
|
|
71
|
+
# Personal: only a configured owner, only in their configured DM or channel.
|
|
72
|
+
- command: /${COMMAND_PREFIX}-docs-connect
|
|
73
|
+
url: ${COMMANDS_REQUEST_URL}
|
|
74
|
+
description: Let ${DISPLAY_NAME} draft Google Docs in your own Drive
|
|
75
|
+
should_escape: false
|
|
71
76
|
- command: /${COMMAND_PREFIX}-knock-connect
|
|
72
77
|
url: ${COMMANDS_REQUEST_URL}
|
|
73
78
|
description: Connect ${DISPLAY_NAME} to Knock read-only tools (admins)
|
package/src/deploy/deploy.ts
CHANGED
|
@@ -396,6 +396,9 @@ export async function syncRuntimeSecret(ctx: AwsContext): Promise<string[]> {
|
|
|
396
396
|
const emailProxyFunctionArn = dry
|
|
397
397
|
? `<${api}.EmailProxyFunctionArn>`
|
|
398
398
|
: await optionalOutput(ctx, api, "EmailProxyFunctionArn");
|
|
399
|
+
const googleDocsProxyFunctionArn = dry
|
|
400
|
+
? `<${api}.GoogleDocsProxyFunctionArn>`
|
|
401
|
+
: await optionalOutput(ctx, api, "GoogleDocsProxyFunctionArn");
|
|
399
402
|
const browserProxyFunctionArn = dry
|
|
400
403
|
? `<${api}.BrowserProxyFunctionArn>`
|
|
401
404
|
: await optionalOutput(ctx, api, "BrowserProxyFunctionArn");
|
|
@@ -458,6 +461,7 @@ export async function syncRuntimeSecret(ctx: AwsContext): Promise<string[]> {
|
|
|
458
461
|
...(agentRuntimeArn === undefined ? {} : { agentRuntimeArn }),
|
|
459
462
|
...(readOnlyRoleArn === undefined ? {} : { readOnlyRoleArn }),
|
|
460
463
|
...(emailProxyFunctionArn === undefined ? {} : { emailProxyFunctionArn }),
|
|
464
|
+
...(googleDocsProxyFunctionArn === undefined ? {} : { googleDocsProxyFunctionArn }),
|
|
461
465
|
...(browserProxyFunctionArn === undefined ? {} : { browserProxyFunctionArn }),
|
|
462
466
|
...(crmProxyFunctionArn === undefined ? {} : { crmProxyFunctionArn }),
|
|
463
467
|
...(crmPolicyFingerprint === undefined ? {} : { crmPolicyFingerprint }),
|
|
@@ -64,6 +64,8 @@ export interface RuntimeSecretInputs {
|
|
|
64
64
|
* offers no email tools.
|
|
65
65
|
*/
|
|
66
66
|
emailProxyFunctionArn?: string;
|
|
67
|
+
/** Present only when a Google Docs drafting identity is configured. */
|
|
68
|
+
googleDocsProxyFunctionArn?: string;
|
|
67
69
|
/**
|
|
68
70
|
* `FoundationApi.BrowserProxyFunctionArn`; absent when managed browser is disabled.
|
|
69
71
|
* The runtime receives no Browser API grant or signing secret.
|
|
@@ -210,6 +212,9 @@ export function composeRuntimeSecret(inputs: RuntimeSecretInputs): Record<string
|
|
|
210
212
|
...(inputs.readOnlyRoleArn !== undefined && inputs.readOnlyRoleArn !== ""
|
|
211
213
|
? { FOUNDATION_AWS_READONLY_ROLE_ARN: inputs.readOnlyRoleArn }
|
|
212
214
|
: {}),
|
|
215
|
+
...(inputs.googleDocsProxyFunctionArn !== undefined && inputs.googleDocsProxyFunctionArn !== ""
|
|
216
|
+
? { FOUNDATION_GOOGLE_DOCS_PROXY_ARN: inputs.googleDocsProxyFunctionArn }
|
|
217
|
+
: {}),
|
|
213
218
|
...(inputs.emailProxyFunctionArn !== undefined && inputs.emailProxyFunctionArn !== ""
|
|
214
219
|
? { FOUNDATION_EMAIL_PROXY_FUNCTION_ARN: inputs.emailProxyFunctionArn }
|
|
215
220
|
: {}),
|
|
@@ -400,6 +400,29 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
400
400
|
}),
|
|
401
401
|
);
|
|
402
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* Google Docs drafting grants, like personal Gmail grants, are readable only
|
|
405
|
+
* by their proxy. Both shell-reachable roles explicitly deny every
|
|
406
|
+
* owner-derived name, so a future broad grant cannot reach one.
|
|
407
|
+
*/
|
|
408
|
+
private denyDirectDocsSecretAccess(role: iam.Role, docsSecretPrefix: string): void {
|
|
409
|
+
role.addToPolicy(
|
|
410
|
+
new iam.PolicyStatement({
|
|
411
|
+
sid: "DenyGoogleDocsSecrets",
|
|
412
|
+
effect: iam.Effect.DENY,
|
|
413
|
+
actions: ["secretsmanager:*"],
|
|
414
|
+
resources: [
|
|
415
|
+
cdk.Stack.of(this).formatArn({
|
|
416
|
+
service: "secretsmanager",
|
|
417
|
+
resource: "secret",
|
|
418
|
+
resourceName: `${docsSecretPrefix}/personal/*-??????`,
|
|
419
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
|
|
420
|
+
}),
|
|
421
|
+
],
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
403
426
|
/**
|
|
404
427
|
* The execution role and its ReadOnlyAccess escape hatch are both
|
|
405
428
|
* shell-reachable. Keep Gmail credentials behind the proxy even if a future
|
|
@@ -590,6 +613,7 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
590
613
|
props.secrets.googleEmail,
|
|
591
614
|
namesFor(props.instance).secretGoogleEmail,
|
|
592
615
|
);
|
|
616
|
+
this.denyDirectDocsSecretAccess(role, namesFor(props.instance).secretGoogleDocs);
|
|
593
617
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
594
618
|
this.denyDirectEtradeAccess(role, props);
|
|
595
619
|
this.pagesPolicy("AgentReadOnlyPagesPolicy", role, props.instance, { invoke: false });
|
|
@@ -689,6 +713,7 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
689
713
|
props.secrets.googleEmail,
|
|
690
714
|
namesFor(props.instance).secretGoogleEmail,
|
|
691
715
|
);
|
|
716
|
+
this.denyDirectDocsSecretAccess(role, namesFor(props.instance).secretGoogleDocs);
|
|
692
717
|
this.denyDirectEtradeAccess(role, props);
|
|
693
718
|
this.pagesPolicy("AgentExecutionPagesPolicy", role, props.instance, { invoke: true });
|
|
694
719
|
|
|
@@ -792,6 +817,22 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
792
817
|
// already depends on this stack for the runtime ARN, so referring to its
|
|
793
818
|
// output here would close a cycle. The function name is derived from the
|
|
794
819
|
// instance file, so both stacks compute the same string.
|
|
820
|
+
// The Docs drafts proxy is the agent's only Google Docs grant; named, not
|
|
821
|
+
// referenced, for the same cycle reason as the email proxy below.
|
|
822
|
+
role.addToPolicy(
|
|
823
|
+
new iam.PolicyStatement({
|
|
824
|
+
sid: "GoogleDocsProxyInvoke",
|
|
825
|
+
actions: ["lambda:InvokeFunction"],
|
|
826
|
+
resources: [
|
|
827
|
+
cdk.Stack.of(this).formatArn({
|
|
828
|
+
service: "lambda",
|
|
829
|
+
resource: "function",
|
|
830
|
+
resourceName: namesFor(props.instance).googleDocsProxyFunctionName,
|
|
831
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
|
|
832
|
+
}),
|
|
833
|
+
],
|
|
834
|
+
}),
|
|
835
|
+
);
|
|
795
836
|
role.addToPolicy(
|
|
796
837
|
new iam.PolicyStatement({
|
|
797
838
|
sid: "EmailProxyInvoke",
|
package/src/stacks/api-stack.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import {
|
|
3
3
|
type CrmOutreachConfig,
|
|
4
4
|
type EmailIdentity,
|
|
5
|
+
type GoogleDocsIdentity,
|
|
5
6
|
type ScheduledJobBinding,
|
|
6
7
|
type ScheduledJobConfig,
|
|
7
8
|
type WebhookJobBinding,
|
|
@@ -116,6 +117,8 @@ export class FoundationApi extends cdk.Stack {
|
|
|
116
117
|
public readonly invokeDlq: sqs.Queue;
|
|
117
118
|
/** The email proxy: the agent role's only email grant is to invoke this. */
|
|
118
119
|
public readonly emailProxyFunction: lambda.Function;
|
|
120
|
+
/** The Google Docs drafts proxy, present only when an identity is configured. */
|
|
121
|
+
public readonly googleDocsProxyFunction?: lambda.Function;
|
|
119
122
|
/** Isolated policy boundary for AgentCore Browser; absent when the capability is disabled. */
|
|
120
123
|
public readonly browserProxyFunction?: lambda.Function;
|
|
121
124
|
/** Read-only Otter proxy, present only when the instance opts in. */
|
|
@@ -134,6 +137,7 @@ export class FoundationApi extends cdk.Stack {
|
|
|
134
137
|
const names = namesFor(props.instance);
|
|
135
138
|
const display = props.instance.displayName;
|
|
136
139
|
const emailIdentities = emailIdentitiesFor(props.configPath);
|
|
140
|
+
const docsIdentities = googleDocsIdentitiesFor(props.configPath);
|
|
137
141
|
const personalEmailIdentities = emailIdentities.filter(
|
|
138
142
|
(identity): identity is EmailIdentity & { owner: string } => identity.owner !== undefined,
|
|
139
143
|
);
|
|
@@ -212,6 +216,22 @@ export class FoundationApi extends cdk.Stack {
|
|
|
212
216
|
}),
|
|
213
217
|
);
|
|
214
218
|
|
|
219
|
+
// Google Docs drafting grants: one deployment-owned shell per configured
|
|
220
|
+
// owner, written by the gateway's connect callback and read only by the
|
|
221
|
+
// docs proxy. Removing an identity deletes its grant.
|
|
222
|
+
const docsSecrets = docsIdentities.map(
|
|
223
|
+
(identity) =>
|
|
224
|
+
new secretsmanager.Secret(this, `GoogleDocsSecret${identity.owner}`, {
|
|
225
|
+
secretName: `${namesFor(props.instance).secretGoogleDocs}/personal/${identity.owner}`,
|
|
226
|
+
description: `${display} Google Docs drafting grant for Slack ${identity.owner}; gateway writes and the docs proxy reads`,
|
|
227
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
228
|
+
generateSecretString: {
|
|
229
|
+
secretStringTemplate: "{}",
|
|
230
|
+
generateStringKey: "placeholder",
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
|
|
215
235
|
const gatewayRole = new iam.Role(this, "GatewayRole", {
|
|
216
236
|
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
|
|
217
237
|
description: `${display} gateway Lambda role (least privilege)`,
|
|
@@ -262,6 +282,17 @@ export class FoundationApi extends cdk.Stack {
|
|
|
262
282
|
],
|
|
263
283
|
}),
|
|
264
284
|
);
|
|
285
|
+
if (docsSecrets.length > 0) {
|
|
286
|
+
gatewayRole.addToPolicy(
|
|
287
|
+
new iam.PolicyStatement({
|
|
288
|
+
sid: "GoogleDocsIdentityConnect",
|
|
289
|
+
// Write-only, on these exact owner shells: the gateway completes a
|
|
290
|
+
// connection but can never read a grant back.
|
|
291
|
+
actions: ["secretsmanager:PutSecretValue"],
|
|
292
|
+
resources: docsSecrets.map(secretArnPattern),
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
265
296
|
if (props.secrets.knockCredential !== undefined) {
|
|
266
297
|
gatewayRole.addToPolicy(
|
|
267
298
|
new iam.PolicyStatement({
|
|
@@ -418,6 +449,12 @@ export class FoundationApi extends cdk.Stack {
|
|
|
418
449
|
})),
|
|
419
450
|
),
|
|
420
451
|
}),
|
|
452
|
+
...(docsIdentities.length === 0
|
|
453
|
+
? {}
|
|
454
|
+
: {
|
|
455
|
+
FOUNDATION_GOOGLE_DOCS_SECRET_ID: namesFor(props.instance).secretGoogleDocs,
|
|
456
|
+
FOUNDATION_GOOGLE_DOCS_IDENTITIES: JSON.stringify(docsIdentities),
|
|
457
|
+
}),
|
|
421
458
|
...(props.secrets.knockOauthClient === undefined
|
|
422
459
|
? {}
|
|
423
460
|
: { FOUNDATION_KNOCK_OAUTH_CLIENT_SECRET_ID: props.secrets.knockOauthClient.secretName }),
|
|
@@ -689,6 +726,58 @@ export class FoundationApi extends cdk.Stack {
|
|
|
689
726
|
},
|
|
690
727
|
});
|
|
691
728
|
|
|
729
|
+
// --- Google Docs drafts proxy ---------------------------------------
|
|
730
|
+
// The only reader of a Docs grant. Provisioned only when an identity is
|
|
731
|
+
// configured; the identities it serves are baked in here at synth.
|
|
732
|
+
if (docsIdentities.length > 0) {
|
|
733
|
+
const docsProxyRole = new iam.Role(this, "GoogleDocsProxyRole", {
|
|
734
|
+
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
|
|
735
|
+
description: `${display} Google Docs drafts proxy role (the only reader of a docs grant)`,
|
|
736
|
+
managedPolicies: [
|
|
737
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"),
|
|
738
|
+
],
|
|
739
|
+
});
|
|
740
|
+
docsProxyRole.addToPolicy(
|
|
741
|
+
new iam.PolicyStatement({
|
|
742
|
+
sid: "GoogleDocsProxyCredentials",
|
|
743
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
744
|
+
resources: [
|
|
745
|
+
...docsSecrets.map(secretArnPattern),
|
|
746
|
+
secretArnPattern(props.secrets.googleOauth),
|
|
747
|
+
secretArnPattern(props.secrets.signing),
|
|
748
|
+
],
|
|
749
|
+
}),
|
|
750
|
+
);
|
|
751
|
+
docsProxyRole.addToPolicy(
|
|
752
|
+
new iam.PolicyStatement({
|
|
753
|
+
sid: "GoogleDocsProxyDataKey",
|
|
754
|
+
actions: ["kms:Decrypt"],
|
|
755
|
+
resources: [props.dataKey.keyArn],
|
|
756
|
+
}),
|
|
757
|
+
);
|
|
758
|
+
this.googleDocsProxyFunction = new lambda.Function(this, "GoogleDocsProxyFunction", {
|
|
759
|
+
functionName: namesFor(props.instance).googleDocsProxyFunctionName,
|
|
760
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
761
|
+
handler: "index.handler",
|
|
762
|
+
code: lambdaCode(this, "google-docs-proxy"),
|
|
763
|
+
role: docsProxyRole,
|
|
764
|
+
timeout: cdk.Duration.seconds(30),
|
|
765
|
+
memorySize: 256,
|
|
766
|
+
environment: {
|
|
767
|
+
FOUNDATION_GOOGLE_DOCS_SECRET_ID: namesFor(props.instance).secretGoogleDocs,
|
|
768
|
+
FOUNDATION_GOOGLE_OAUTH_SECRET_ID: namesFor(props.instance).secretGoogleOauth,
|
|
769
|
+
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
770
|
+
FOUNDATION_SLACK_TEAM_ID: props.instance.slack.teamId,
|
|
771
|
+
FOUNDATION_GOOGLE_DOCS_IDENTITIES: JSON.stringify(docsIdentities),
|
|
772
|
+
FOUNDATION_GOOGLE_DOCS_FOLDER: `${props.instance.displayName} drafts`,
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
new cdk.CfnOutput(this, "GoogleDocsProxyFunctionArn", {
|
|
776
|
+
value: this.googleDocsProxyFunction.functionArn,
|
|
777
|
+
description: "The Google Docs drafts proxy the agent invokes",
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
692
781
|
// --- AgentCore Browser proxy ----------------------------------------
|
|
693
782
|
// The model-facing runtime can invoke this Lambda but has no Browser API grants. The proxy
|
|
694
783
|
// owns those grants and re-validates a gateway-signed DM/user scope plus this deploy-time
|
|
@@ -1346,6 +1435,14 @@ function configTimezoneFor(configPath: string): string {
|
|
|
1346
1435
|
* malformed owner/channel bindings a deployment failure rather than allowing a
|
|
1347
1436
|
* looser raw YAML shape into an IAM policy or Lambda environment.
|
|
1348
1437
|
*/
|
|
1438
|
+
/** The Google Docs drafting identities to provision, read from the instance config at synth. */
|
|
1439
|
+
export function googleDocsIdentitiesFor(configPath: string): GoogleDocsIdentity[] {
|
|
1440
|
+
const docs = parseInstanceConfig(readFileSync(configPath, "utf8")).capabilities.googleDocs;
|
|
1441
|
+
return docs.enabled
|
|
1442
|
+
? docs.identities.map(({ address, channel, owner }) => ({ address, channel, owner }))
|
|
1443
|
+
: [];
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1349
1446
|
export function emailIdentitiesFor(configPath: string): EmailIdentity[] {
|
|
1350
1447
|
const email = parseInstanceConfig(readFileSync(configPath, "utf8")).capabilities.email;
|
|
1351
1448
|
return email.enabled ? email.identities : [];
|