diff --git a/.prettierignore b/.prettierignore
index 571c41c52774..d7dc06ca1977 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -37,3 +37,6 @@ content/docs/reference/pkg
tsconfig.json
package.json
typedoc.json
+
+# Ignore the files we generate during the build and deployment process.
+origin-bucket-metadata.json
diff --git a/assets/css/bundle.css b/assets/css/bundle.css
index 8d8bd8e3bda4..6dbada395efd 100644
--- a/assets/css/bundle.css
+++ b/assets/css/bundle.css
@@ -3127,6 +3127,26 @@ div.highlight.line-numbers pre.chroma code span.line::before{
}
}
+#ai-sidebar-host{
+ display:none
+}
+
+#ai-sidebar-target{
+ position:fixed;
+ z-index:10;
+ top:0;
+ right:0;
+ bottom:0
+}
+
+.section- #ai-sidebar-target{
+ top:200px
+}
+
+.section-docs #ai-sidebar-target{
+ top:108px
+}
+
div.highlight{
display:flex
}
diff --git a/config/_default/config.yml b/config/_default/config.yml
index ad9f0bd00174..3fdeadbf820d 100644
--- a/config/_default/config.yml
+++ b/config/_default/config.yml
@@ -8,6 +8,7 @@ security:
- ASSET_BUNDLE_ID
- PULUMI_CONVERT_URL
- PULUMI_AI_WS_URL
+ - PULUMI_COPILOT_URL
- GITHUB_TOKEN
- ALGOLIA_APP_ID
- ALGOLIA_APP_SEARCH_KEY
diff --git a/infrastructure/Pulumi.www-production.yaml b/infrastructure/Pulumi.www-production.yaml
index 56650a9f0ea9..fea917f9cd1a 100644
--- a/infrastructure/Pulumi.www-production.yaml
+++ b/infrastructure/Pulumi.www-production.yaml
@@ -1,6 +1,7 @@
config:
aws:region: us-west-2
www.pulumi.com:addSecurityHeaders: "true"
+ www.pulumi.com:copilotUrl: https://app.pulumi.com/ai
www.pulumi.com:certificateArn: "arn:aws:acm:us-east-1:388588623842:certificate/9db6a76b-f7ba-465b-ab96-ce1d3b8ae02c"
www.pulumi.com:doAIAnswersRewrites: "true"
www.pulumi.com:doEdgeRedirects: "true"
diff --git a/infrastructure/index.ts b/infrastructure/index.ts
index 24d11eb750ff..f2cc504cd6b8 100644
--- a/infrastructure/index.ts
+++ b/infrastructure/index.ts
@@ -61,6 +61,7 @@ const config = {
const aiAppStack = new pulumi.StackReference('pulumi/pulumi-ai-app-infra/prod');
const aiAppDomain = aiAppStack.requireOutput('aiAppDistributionDomain');
+const cloudAiAppDomain = aiAppStack.requireOutput('cloudAiAppDistributionDomain');
// originBucketName is the name of the S3 bucket to use as the CloudFront origin for the
// website. This bucket is presumed to exist prior to the Pulumi run; if it doesn't, this
@@ -277,33 +278,40 @@ const allViewerExceptHostHeaderId = "b689b0a8-53d0-40ab-baf2-68738e2966ac";
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html
const cachingDisabledId = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad";
-const SecurityHeadersPolicy = new aws.cloudfront.ResponseHeadersPolicy('security-headers', {
- securityHeadersConfig: {
- frameOptions: {
- frameOption: config.addSecurityHeaders ? 'DENY' : 'SAMEORIGIN',
- override: false,
- },
- // These remaining options are derived from:
- // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html#managed-response-headers-policies-security
- // "SecurityHeadersPolicy" with ID "67f7725c-6f97-4210-82d7-5512b31e9d03"
- referrerPolicy: {
- referrerPolicy: 'strict-origin-when-cross-origin',
- override: false,
- },
- contentTypeOptions: {
- override: true,
- },
- strictTransportSecurity: {
- accessControlMaxAgeSec: 31536000,
- override: false,
- },
- xssProtection: {
- protection: true,
- modeBlock: true,
- override: false,
+function newSecurityHeadersPolicy(name: string, frameOption: string) {
+ return new aws.cloudfront.ResponseHeadersPolicy(name, {
+ securityHeadersConfig: {
+ frameOptions: {
+ frameOption,
+ override: false,
+ },
+ // These remaining options are derived from:
+ // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html#managed-response-headers-policies-security
+ // "SecurityHeadersPolicy" with ID "67f7725c-6f97-4210-82d7-5512b31e9d03"
+ referrerPolicy: {
+ referrerPolicy: 'strict-origin-when-cross-origin',
+ override: false,
+ },
+ contentTypeOptions: {
+ override: true,
+ },
+ strictTransportSecurity: {
+ accessControlMaxAgeSec: 31536000,
+ override: false,
+ },
+ xssProtection: {
+ protection: true,
+ modeBlock: true,
+ override: false,
+ }
}
- }
-})
+ });
+}
+
+// Most of the site
+const SecurityHeadersPolicy = newSecurityHeadersPolicy('security-headers', config.addSecurityHeaders ? 'DENY' : 'SAMEORIGIN');
+// Copilot lives in an iframe
+const CopilotSecurityHeadersPolicy = newSecurityHeadersPolicy('copilot-security-headers', 'SAMEORIGIN');
const baseCacheBehavior: aws.types.input.cloudfront.DistributionDefaultCacheBehavior = {
targetOriginId: originBucket.arn,
@@ -421,6 +429,18 @@ const distributionArgs: aws.cloudfront.DistributionArgs = {
originKeepaliveTimeout: 60,
},
},
+ {
+ originId: cloudAiAppDomain,
+ domainName: cloudAiAppDomain,
+ customOriginConfig: {
+ originProtocolPolicy: "https-only",
+ httpPort: 80,
+ httpsPort: 443,
+ originSslProtocols: ["TLSv1.2"],
+ originReadTimeout: 60,
+ originKeepaliveTimeout: 60,
+ },
+ },
...registryOrigins,
],
@@ -580,6 +600,38 @@ const distributionArgs: aws.cloudfront.DistributionArgs = {
cachePolicyId: cachingDisabledId,
lambdaFunctionAssociations: config.doAIAnswersRewrites ? [getAIAnswersRewriteAssociation()] : [],
forwardedValues: undefined, // forwardedValues conflicts with cachePolicyId, so we unset it.
+ },
+
+ // Copilot app
+ {
+ ...baseCacheBehavior,
+ // allow all methods
+ allowedMethods: ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"],
+ cachedMethods: [
+ "GET", "HEAD", "OPTIONS",
+ ],
+ targetOriginId: cloudAiAppDomain,
+ pathPattern: '/_pulumi/cloud-ai',
+ originRequestPolicyId: allViewerExceptHostHeaderId,
+ cachePolicyId: cachingDisabledId,
+ lambdaFunctionAssociations: [],
+ forwardedValues: undefined, // forwardedValues conflicts with cachePolicyId, so we unset it.
+ responseHeadersPolicyId: CopilotSecurityHeadersPolicy.id,
+ },
+ {
+ ...baseCacheBehavior,
+ // allow all methods
+ allowedMethods: ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"],
+ cachedMethods: [
+ "GET", "HEAD", "OPTIONS",
+ ],
+ targetOriginId: cloudAiAppDomain,
+ pathPattern: '/_pulumi/cloud-ai/*',
+ originRequestPolicyId: allViewerExceptHostHeaderId,
+ cachePolicyId: cachingDisabledId,
+ lambdaFunctionAssociations: [],
+ forwardedValues: undefined, // forwardedValues conflicts with cachePolicyId, so we unset it.
+ responseHeadersPolicyId: CopilotSecurityHeadersPolicy.id,
}
],
diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html
index 217812e18991..9a8006390bcd 100644
--- a/layouts/_default/baseof.html
+++ b/layouts/_default/baseof.html
@@ -9,6 +9,8 @@
{{ block "main" . }}
{{ end }}
+
+ {{ partial "copilot/sidebar.html" . }}
{{ block "footer" . }}
diff --git a/layouts/partials/copilot/sidebar.html b/layouts/partials/copilot/sidebar.html
new file mode 100644
index 000000000000..2240f996c95d
--- /dev/null
+++ b/layouts/partials/copilot/sidebar.html
@@ -0,0 +1,5 @@
+{{ $copilotApiUrl := getenv "PULUMI_COPILOT_URL" }}
+{{ if $copilotApiUrl }}
+
+
+{{ end }}
diff --git a/scripts/build-site.sh b/scripts/build-site.sh
index c04cc20edb87..c0bc21bfdf06 100755
--- a/scripts/build-site.sh
+++ b/scripts/build-site.sh
@@ -8,6 +8,13 @@ source ./scripts/common.sh
export PULUMI_CONVERT_URL="${PULUMI_CONVERT_URL:-$(pulumi stack output --stack pulumi/tf2pulumi-service/production url)}"
export PULUMI_AI_WS_URL=${PULUMI_AI_WS_URL:-$(pulumi stack output --stack pulumi/pulumigpt-api/corp websocketUri)}
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+INFRA_PATH="$SCRIPT_DIR/../infrastructure"
+# Read Copilot API URL from Pulumi config, ignoring any errors.
+# If the config value is not set Copilot will not be available.
+export PULUMI_COPILOT_URL=${PULUMI_COPILOT_URL:-$(pulumi --cwd "$INFRA_PATH" config get copilotUrl 2>/dev/null || echo "")}
+printf "Copilot URL: $PULUMI_COPILOT_URL\n"
+
printf "Compiling theme JavaScript and CSS...\n\n"
export ASSET_BUNDLE_ID="$(build_identifier)"
diff --git a/theme/src/scss/_copilot.scss b/theme/src/scss/_copilot.scss
new file mode 100644
index 000000000000..cecf2c43b0eb
--- /dev/null
+++ b/theme/src/scss/_copilot.scss
@@ -0,0 +1,38 @@
+// Host iframe
+// Should not be shown, when loaded it will portal the content to the sidebar target
+#ai-sidebar-host {
+ display: none
+}
+
+// Element that will be the target of the portal
+// If empty it will have a width of 0 and will not be visible.
+#ai-sidebar-target {
+ // Relative to the viewport so copilot moves w/ the scrolling
+ position: fixed;
+
+ // Popovers on site use z-index: 20
+ z-index: 10;
+
+ // Default use up the full height of the viewport
+ top: 0;
+ right: 0;
+ bottom: 0;
+}
+
+// Per page positions
+// ------------------
+
+// Main page
+// ---
+
+/* FIXME: no section suffix? */
+.section- #ai-sidebar-target {
+ top: 200px;
+}
+
+// Docs pages
+// ---
+
+.section-docs #ai-sidebar-target {
+ top: calc(38px + 8px + 8px + 54px);
+}
\ No newline at end of file
diff --git a/theme/src/scss/main.scss b/theme/src/scss/main.scss
index bd88e2ea6339..708c25b08bd3 100644
--- a/theme/src/scss/main.scss
+++ b/theme/src/scss/main.scss
@@ -21,6 +21,7 @@
@import "marketing/dismissable-banner";
@import "code";
@import "container";
+@import "copilot";
@import "copy-button";
@import "docs/cloud-overview";
@import "docs/continuous-delivery";