@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/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# `@deployfoundation/foundation-deploy`
|
|
2
|
+
|
|
3
|
+
The CDK app that deploys one Foundation instance, and `foundation-deploy`, the
|
|
4
|
+
tool an instance's pipeline runs.
|
|
5
|
+
|
|
6
|
+
Foundation ships **no instance file and no company configuration**. Everything
|
|
7
|
+
that identifies a deployment — account, region, Slack workspace, GitHub org,
|
|
8
|
+
naming prefixes, which integrations are provisioned — lives in an instance YAML
|
|
9
|
+
the deployment's own repository owns. Both halves of this package take the path
|
|
10
|
+
to that file and derive the rest.
|
|
11
|
+
|
|
12
|
+
## The CDK app
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
cd packages/infra
|
|
16
|
+
bunx cdk synth --all -c instanceFile=../../../my-instance/instance.yaml
|
|
17
|
+
bunx cdk deploy --all -c instanceFile=/abs/path/instance.yaml -c agentImageTag=abc1234
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Context keys:
|
|
21
|
+
|
|
22
|
+
| Key | Meaning |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `instanceFile` | **Required.** Path to the instance YAML, absolute or relative to the working directory. |
|
|
25
|
+
| `admins` | CSV of Slack admin ids. Defaults to `admins` in the instance's runtime config. |
|
|
26
|
+
| `alarmEmail` | DLQ alarm subscriber. Defaults to none. |
|
|
27
|
+
| `agentImageTag` | Required when deploying the AgentCore runtime from a local build; a release names the image instead. |
|
|
28
|
+
| `release` | Deploy a published release: `vX.Y.Z`. The manifest is read from `$FOUNDATION_RELEASE_DIR` or the per-version cache directory `foundation-deploy` writes. |
|
|
29
|
+
| `releaseManifest` | Path to a verified release manifest, which is what `foundation-deploy --release` passes. |
|
|
30
|
+
| `releaseBucket` | The Foundation release bucket the manifest's keys live in. Required in release mode (or `FOUNDATION_RELEASE_BUCKET`). |
|
|
31
|
+
| `deployRuntime=false` | Bootstrap phase: every stack except the runtime. |
|
|
32
|
+
| `pipelineInstanceFile` | Where the instance file sits inside the repo a CodePipeline sources, when that differs from the path given here. |
|
|
33
|
+
| `vpcId`, `createVpcEndpoints`, `skipEndpoints`, `agentSubnetIds` | Network escape hatches, unchanged. |
|
|
34
|
+
|
|
35
|
+
`instance.config` is resolved **relative to the instance file's own directory**,
|
|
36
|
+
so an instance repository whose layout is `.foundation/{instance.yaml,config.yaml}`
|
|
37
|
+
writes `config: config.yaml` and nothing else.
|
|
38
|
+
|
|
39
|
+
`FOUNDATION_SKIP_BUNDLE=1` swaps every Lambda bundle for an inline stub. It is what the
|
|
40
|
+
tests use and what a synth check on a host without Docker or Bun should use.
|
|
41
|
+
|
|
42
|
+
## The deploy tool
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
bun run packages/infra/bin/foundation-deploy.ts <command> --instance <path>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Command | What it does |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `deploy` | Build and push the agent image, `cdk deploy --all`, refresh the runtime secret, sync config. |
|
|
51
|
+
| `post-deploy` | Smoke DEFAULT, probe the persistent mount, promote `live`, smoke `live`. |
|
|
52
|
+
| `config:sync` | Upload the runtime config and Foundation's product skills to S3. |
|
|
53
|
+
| `setup` | First-deploy orchestration, idempotent. |
|
|
54
|
+
| `github-app-create` | Create the instance's GitHub App from the shipped manifest template. |
|
|
55
|
+
| `slack-manifest` | Print the instance's Slack app manifest as JSON (the Slack CLI's `get-manifest` hook contract). |
|
|
56
|
+
| `stage-customization` | Validate and stage a tenant-owned runtime config. |
|
|
57
|
+
|
|
58
|
+
`--instance <path>` is required for every command (or `FOUNDATION_INSTANCE_FILE`
|
|
59
|
+
in a pipeline). There is no instance *name*, no `instances/` directory and no
|
|
60
|
+
default: a deploy that guessed its target would be a deploy into the wrong
|
|
61
|
+
account.
|
|
62
|
+
|
|
63
|
+
### Deploying a release
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
# an instance repo's whole build step
|
|
67
|
+
npx @deployfoundation/foundation-deploy@$(cat foundation.version) deploy \
|
|
68
|
+
--instance .foundation/instance.yaml
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A copy of the tool installed from npm has no Foundation workspace to build
|
|
72
|
+
from, so it deploys the release matching **its own version**: it fetches
|
|
73
|
+
`releases/vX.Y.Z/manifest.json` from the Foundation release bucket, verifies
|
|
74
|
+
the KMS signature and every artifact's sha256, and only then deploys. In a
|
|
75
|
+
Foundation checkout the same command builds locally, as it always did.
|
|
76
|
+
`--release <vX.Y.Z>`, `--manifest <path|s3://…>` and `--release-bucket <name>`
|
|
77
|
+
override the choice.
|
|
78
|
+
|
|
79
|
+
Until the release account exists, set `FOUNDATION_RELEASE_KMS_KEY_ARN` to the
|
|
80
|
+
published signing key ARN: the tool ships no default, because trusting the
|
|
81
|
+
wrong key is worse than checking nothing.
|
|
82
|
+
|
|
83
|
+
### The license check
|
|
84
|
+
|
|
85
|
+
`deploy` verifies the instance's `license.key` against
|
|
86
|
+
`https://license.foundry41.com/v1/verify` (override with
|
|
87
|
+
`FOUNDATION_LICENSE_ENDPOINT`) before it deploys, sending the key, the
|
|
88
|
+
instance id and the version — nothing else. An instance with no key is
|
|
89
|
+
skipped with a warning. A definite refusal stops the deploy; an unreachable
|
|
90
|
+
endpoint does not, for 30 days, on the verification cached in the instance's
|
|
91
|
+
own `<prefix>/license/last-verified` secret.
|
|
92
|
+
|
|
93
|
+
The tool never imports the agent package. It runs in a pipeline, not in the
|
|
94
|
+
agent container.
|
|
95
|
+
|
|
96
|
+
## Artifacts
|
|
97
|
+
|
|
98
|
+
`src/artifacts.ts` is the single place that decides where the deployed bytes
|
|
99
|
+
come from, and it has two modes:
|
|
100
|
+
|
|
101
|
+
- **local build** (the default). Every Lambda bundle is built from this
|
|
102
|
+
checkout at synth with the pinned Bun, and `deploy` builds and pushes the
|
|
103
|
+
agent image to the instance's own ECR first. A contributor deploys their
|
|
104
|
+
working tree this way.
|
|
105
|
+
- **release**. `-c release=vX.Y.Z` (or `-c releaseManifest=<path>`) plus
|
|
106
|
+
`-c releaseBucket=<name>`: each Lambda's code is `Code.fromBucket(...)` on
|
|
107
|
+
the key the signed manifest names, and the agent image is Foundation's own
|
|
108
|
+
repository pinned by digest. Nothing is built, so the pipeline needs no
|
|
109
|
+
Docker, no Bun and no Foundation checkout.
|
|
110
|
+
|
|
111
|
+
`src/release/manifest.ts` is the manifest itself — schema, builder, the
|
|
112
|
+
canonical bytes a signature covers, and `verifyManifest`, which checks the KMS
|
|
113
|
+
signature **before** any artifact is read and then every artifact's sha256.
|
|
114
|
+
`src/release/kms.ts` is the AWS-facing half. A manifest reaching a synth has
|
|
115
|
+
already been verified by `foundation-deploy`.
|
|
116
|
+
|
|
117
|
+
## The release account (Foundry 41's own)
|
|
118
|
+
|
|
119
|
+
`src/stacks/release-account-stack.ts` is the only stack here that is not an
|
|
120
|
+
instance's. It holds what a release is published *from*: the release bucket,
|
|
121
|
+
the `foundation/agent` image repository, the KMS signing key, and the OIDC
|
|
122
|
+
role `.github/workflows/release.yml` assumes on a tag push. Customers appear
|
|
123
|
+
in exactly one place — the account-id list that opens the bucket, the
|
|
124
|
+
repository and the key.
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
cd packages/infra
|
|
128
|
+
bunx cdk --app 'bun run bin/release-account.ts' diff FoundationRelease \
|
|
129
|
+
-c customerAccountIds=111122223333,444455556666
|
|
130
|
+
bunx cdk --app 'bun run bin/release-account.ts' deploy FoundationRelease \
|
|
131
|
+
-c customerAccountIds=111122223333,444455556666
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Deployed once, by hand, into the release account. Its outputs are the four
|
|
135
|
+
repository variables the release workflow needs (`FOUNDATION_RELEASE_BUCKET`,
|
|
136
|
+
`FOUNDATION_RELEASE_KMS_KEY_ARN`, `FOUNDATION_RELEASE_ROLE_ARN`, and the image
|
|
137
|
+
repository), plus the signing key ARN customers verify against. Adding a
|
|
138
|
+
customer is a redeploy with a longer `customerAccountIds`.
|
|
139
|
+
|
|
140
|
+
## Publishing
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
bun run --cwd packages/infra build # dist/, Node target, ESM
|
|
144
|
+
cd packages/infra && npm pack --dry-run
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The build bundles the three entry points — the CLI, the CDK app and the
|
|
148
|
+
release-account app — into `dist/`, inlining the workspace packages (which are
|
|
149
|
+
not published) and leaving every declared dependency external, so npm installs
|
|
150
|
+
one `aws-cdk-lib` and both halves of the tool see it. `aws-cdk` itself is a
|
|
151
|
+
dependency, not a devDependency: the tool shells out to the CDK CLI and
|
|
152
|
+
resolves it through this package rather than through PATH.
|
|
153
|
+
|
|
154
|
+
The tarball carries `dist/`, `src/` (the manifest templates are read from
|
|
155
|
+
there at runtime), the agent image's Dockerfile, the reference buildspec and
|
|
156
|
+
`cdk.json`. It does NOT carry `bin/`: those TypeScript entry points need Bun
|
|
157
|
+
and the workspace, and shipping an app entry that cannot run is how a deploy
|
|
158
|
+
picks the wrong one. `node dist/bin/foundation-deploy.js --help` works from an
|
|
159
|
+
extracted tarball with only the runtime dependencies installed — no Bun, no
|
|
160
|
+
Foundation checkout — which is exactly what an instance's pipeline has.
|
|
161
|
+
|
|
162
|
+
## Tests
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
bun test packages/infra
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
They run under `bun test` with the rest of the repo — no jest, no ts-node. The
|
|
169
|
+
snapshot test shells out to `cdk synth` for both fixtures, so it needs the
|
|
170
|
+
workspace installed but no AWS credentials and no Docker.
|
|
171
|
+
|
|
172
|
+
Fixtures are synthetic (`test/fixtures/`): `acme` turns every integration on,
|
|
173
|
+
`beta` is a minimal instance deployed by CodePipeline. No real company appears
|
|
174
|
+
anywhere in this package.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Foundation agent container for Bedrock AgentCore Runtime.
|
|
2
|
+
#
|
|
3
|
+
# AgentCore contract: an HTTP server on :8080 exposing POST /invocations and
|
|
4
|
+
# GET /ping. Runs as a non-root user; scratch space is the session microVM's
|
|
5
|
+
# ephemeral filesystem (dies with the session).
|
|
6
|
+
#
|
|
7
|
+
# Build from the FOUNDATION ROOT:
|
|
8
|
+
# docker build -f packages/infra/agent-image/Dockerfile .
|
|
9
|
+
#
|
|
10
|
+
# Pins (binding):
|
|
11
|
+
# - Bun 1.4.0 (omp's engines floor is `>=1.3.14`; pinned exact to the
|
|
12
|
+
# version that WRITES this repo's bun.lock — 1.3.14 cannot read a
|
|
13
|
+
# lockfileVersion 2 lockfile and fails the install below outright)
|
|
14
|
+
# - omp @oh-my-pi/pi-coding-agent@18.1.14 (all @oh-my-pi/pi-*@18.1.14 in
|
|
15
|
+
# lockstep; the EXACT-pin shape is enforced below against
|
|
16
|
+
# packages/agent/package.json, and the frozen lockfile pins the version itself)
|
|
17
|
+
# - Python 3.10+ for the omp `eval` tool (the bun base is trixie: 3.13)
|
|
18
|
+
# - gh 2.97.0, built from source with Go (see the gh-build stage)
|
|
19
|
+
#
|
|
20
|
+
# Secrets bootstrap: the container receives ONE pointer env var,
|
|
21
|
+
# RUNTIME_SECRET_ID. At start, server.ts fetches that JSON secret from Secrets
|
|
22
|
+
# Manager, setdefaults each key into process env (explicit env wins), and logs
|
|
23
|
+
# ONLY the number of keys loaded — never key names or values.
|
|
24
|
+
|
|
25
|
+
# gh version is shared by the builder stage and the runtime stage.
|
|
26
|
+
# 2.71.0+ required: older gh hard-fails `gh pr view/create` under App
|
|
27
|
+
# installation tokens after GitHub removed classic-Projects projectCards
|
|
28
|
+
# (cli/cli#10714) — the error text mimics a permissions failure.
|
|
29
|
+
ARG GH_VERSION=2.97.0
|
|
30
|
+
|
|
31
|
+
# GLOBAL build args: an ARG consumed by a `FROM` must be declared BEFORE the
|
|
32
|
+
# first stage. Declared after one, it belongs to that stage's scope and
|
|
33
|
+
# resolves to the empty string ("base name should not be blank").
|
|
34
|
+
#
|
|
35
|
+
# BUN_IMAGE defaults to Docker Hub so a plain local `docker build` works with
|
|
36
|
+
# no ECR access; a deploy pipeline can override it with a mirrored tag to stay
|
|
37
|
+
# clear of Docker Hub's anonymous rate limit.
|
|
38
|
+
ARG BUN_IMAGE=oven/bun:1.4.0-slim
|
|
39
|
+
# GH_IMAGE: where the gh BINARY comes from. The default names the `gh-source`
|
|
40
|
+
# stage below, so a local build compiles it; an override points at a prebuilt
|
|
41
|
+
# image, and under BuildKit an unreferenced stage never runs — which takes the
|
|
42
|
+
# clone and the Go module download out of the build entirely.
|
|
43
|
+
ARG GH_IMAGE=gh-source
|
|
44
|
+
|
|
45
|
+
# --- gh builder -------------------------------------------------------------
|
|
46
|
+
# gh is BUILT, not downloaded: image scanners read the Go toolchain and module
|
|
47
|
+
# versions embedded in the binary, and upstream's release artifact carries
|
|
48
|
+
# whatever versions it was built with — only a rebuild can move those.
|
|
49
|
+
# CGO is off and GOARCH comes from TARGETARCH, so the output is a static
|
|
50
|
+
# binary for the target arch. Pulled from AWS's mirror of the Docker official
|
|
51
|
+
# images rather than Docker Hub (no anonymous rate limit, no credentials).
|
|
52
|
+
#
|
|
53
|
+
# `--platform=$BUILDPLATFORM` pins the toolchain to the *builder's* own
|
|
54
|
+
# architecture so the Go compile runs natively and cross-compiles via GOARCH,
|
|
55
|
+
# instead of running under emulation on an x86 host. BUILDPLATFORM (like
|
|
56
|
+
# TARGETARCH below) requires BuildKit, which Docker Desktop and the GitHub
|
|
57
|
+
# runners use by default.
|
|
58
|
+
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:1.26.6-trixie AS gh-build
|
|
59
|
+
ARG GH_VERSION
|
|
60
|
+
# Auto-populated by buildx; the default keeps a plain `docker build` working.
|
|
61
|
+
ARG TARGETARCH=arm64
|
|
62
|
+
RUN git clone --depth=1 --branch "v${GH_VERSION}" https://github.com/cli/cli.git /src
|
|
63
|
+
WORKDIR /src
|
|
64
|
+
# x/mod v0.40.0 is the fix line; it also carries x/crypto, x/net, x/text and
|
|
65
|
+
# x/tools forward. grpc v1.83.2 clears CVE-2026-84304 (unauthenticated remote
|
|
66
|
+
# OOM via fragmented HTTP/2 DATA frames). `tidy` reconciles go.sum.
|
|
67
|
+
RUN go get golang.org/x/mod@v0.40.0 google.golang.org/grpc@v1.83.2 \
|
|
68
|
+
&& go mod tidy
|
|
69
|
+
# VCS stamping stays ON: editing go.mod makes the tree dirty, so the module
|
|
70
|
+
# records as `v2.97.0+dirty` — mildly ugly, but it keeps the version in the
|
|
71
|
+
# binary's build info. `-buildvcs=false` would record `(devel)` and hide any
|
|
72
|
+
# future cli/cli module CVE from scanners entirely.
|
|
73
|
+
RUN CGO_ENABLED=0 GOOS=linux GOARCH="${TARGETARCH}" go build -trimpath \
|
|
74
|
+
-ldflags "-s -w -X github.com/cli/cli/v2/internal/build.Version=v${GH_VERSION}" \
|
|
75
|
+
-o /out/gh ./cmd/gh
|
|
76
|
+
|
|
77
|
+
# The publishable shape of the above: JUST the static binary, on scratch, so a
|
|
78
|
+
# mirror image and the local fallback are the same artifact built by the same
|
|
79
|
+
# instructions.
|
|
80
|
+
FROM scratch AS gh-source
|
|
81
|
+
COPY --from=gh-build /out/gh /out/gh
|
|
82
|
+
|
|
83
|
+
# --- AWS CLI builder --------------------------------------------------------
|
|
84
|
+
# AWS CLI v2 from Amazon's official installer, in its own stage so the zip,
|
|
85
|
+
# unzip and the installer's scratch files never reach the runtime layer.
|
|
86
|
+
# v2 (not Debian's `awscli`, which is the Python v1 line) because the profile
|
|
87
|
+
# below uses `credential_process`, and because v2 is the CLI every AWS doc and
|
|
88
|
+
# every skill will assume. Installed size is ~230 MB; see
|
|
89
|
+
# docs/capabilities/aws-readonly.md for the trade-off.
|
|
90
|
+
FROM ${BUN_IMAGE} AS awscli-build
|
|
91
|
+
ARG TARGETARCH=arm64
|
|
92
|
+
RUN export DEBIAN_FRONTEND=noninteractive \
|
|
93
|
+
&& apt-get update \
|
|
94
|
+
&& apt-get install -y --no-install-recommends curl unzip ca-certificates \
|
|
95
|
+
&& case "${TARGETARCH}" in \
|
|
96
|
+
arm64) AWS_ARCH=aarch64 ;; \
|
|
97
|
+
amd64) AWS_ARCH=x86_64 ;; \
|
|
98
|
+
*) echo "unsupported TARGETARCH ${TARGETARCH}" && exit 1 ;; \
|
|
99
|
+
esac \
|
|
100
|
+
&& curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_ARCH}.zip" -o /tmp/awscliv2.zip \
|
|
101
|
+
&& unzip -q /tmp/awscliv2.zip -d /tmp \
|
|
102
|
+
&& /tmp/aws/install --install-dir /opt/aws-cli --bin-dir /opt/aws-cli/bin \
|
|
103
|
+
&& rm -rf /tmp/aws /tmp/awscliv2.zip /var/lib/apt/lists/*
|
|
104
|
+
|
|
105
|
+
# Resolve whichever of the two GH_IMAGE names. `COPY --from=gh-provider`
|
|
106
|
+
# below is what makes the choice load-bearing.
|
|
107
|
+
FROM ${GH_IMAGE} AS gh-provider
|
|
108
|
+
|
|
109
|
+
# --- runtime ----------------------------------------------------------------
|
|
110
|
+
FROM ${BUN_IMAGE}
|
|
111
|
+
|
|
112
|
+
ARG FOUNDATION_GITHUB_APP_ID=0
|
|
113
|
+
|
|
114
|
+
# NOTE: AWS_EC2_METADATA_DISABLED must NOT be set container-wide: AgentCore
|
|
115
|
+
# vends the execution role's credentials via an IMDS-style metadata endpoint,
|
|
116
|
+
# and disabling it globally leaves the container with no credentials at all.
|
|
117
|
+
# AWS_EC2_METADATA_SERVICE_ENDPOINT: AgentCore serves those credentials on an
|
|
118
|
+
# IMDS-compatible endpoint but exposes none of the env signals omp's
|
|
119
|
+
# credential probe checks — without this pointer the omp Bedrock provider
|
|
120
|
+
# refuses with "No API key found". The real IMDS control is the tool policy
|
|
121
|
+
# denying 169.254.169.254, not an env var a raw curl could ignore.
|
|
122
|
+
ENV AWS_EC2_METADATA_SERVICE_ENDPOINT=http://169.254.169.254 \
|
|
123
|
+
NODE_ENV=production \
|
|
124
|
+
PYTHONDONTWRITEBYTECODE=1 \
|
|
125
|
+
PYTHONUNBUFFERED=1 \
|
|
126
|
+
# omp env sheet: relocate the agent base to the baked container dir
|
|
127
|
+
# (models.yml/materialized config land where omp's registry reads them;
|
|
128
|
+
# ambient files like history.db stay off ~) and pin the fail-hard
|
|
129
|
+
# security overlay.
|
|
130
|
+
PI_CODING_AGENT_DIR=/opt/sky/omp-agent \
|
|
131
|
+
PI_CONFIG_FILES=/opt/sky/omp-agent/security-overlay.yml \
|
|
132
|
+
PI_NO_TITLE=1 \
|
|
133
|
+
PI_NO_PTY=1
|
|
134
|
+
|
|
135
|
+
# Python for the omp eval tool (kernelMode "per-call"). The bun base is trixie
|
|
136
|
+
# (Debian 13), which ships 3.13 — over the 3.10+ floor. ca-certificates for
|
|
137
|
+
# TLS to Slack. curl: the git credential helper and the gh wrapper talk to the
|
|
138
|
+
# loopback broker with it. poppler-utils supplies `pdftotext`, the ONLY way a
|
|
139
|
+
# PDF dropped in a thread becomes readable text (files.ts shells out to it
|
|
140
|
+
# rather than adding a PDF parser dependency); missing here, every PDF
|
|
141
|
+
# silently degrades to a path the model cannot open.
|
|
142
|
+
#
|
|
143
|
+
# `upgrade` (not just `install`) is what picks up Debian security patches for
|
|
144
|
+
# packages the base image baked in — without it, glibc/systemd/libcap2/liblzma5
|
|
145
|
+
# stay at whatever oven/bun published. Naming the --only-upgrade set also
|
|
146
|
+
# changes this layer's hash, so a rebuild re-fetches patches rather than
|
|
147
|
+
# reusing a stale cached layer.
|
|
148
|
+
RUN export DEBIAN_FRONTEND=noninteractive \
|
|
149
|
+
&& apt-get update \
|
|
150
|
+
&& apt-get upgrade -y \
|
|
151
|
+
&& apt-get install -y --only-upgrade \
|
|
152
|
+
libc6 libc-bin libsystemd0 libudev1 libcap2 liblzma5 sed \
|
|
153
|
+
&& apt-get install -y --no-install-recommends \
|
|
154
|
+
python3 python3-venv python3-pip ca-certificates git curl \
|
|
155
|
+
poppler-utils \
|
|
156
|
+
&& ln -sf /usr/bin/python3 /usr/local/bin/python \
|
|
157
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
158
|
+
|
|
159
|
+
# --- Ambient GitHub (github-broker.ts) --------------------------------------
|
|
160
|
+
# gh CLI as /opt/sky/bin/gh-real (from the gh-provider stage above), fronted
|
|
161
|
+
# by a wrapper that injects the broker's short-lived token as GH_TOKEN. git
|
|
162
|
+
# authenticates through the `sky` credential helper wired in /etc/gitconfig,
|
|
163
|
+
# so plain `git clone` and `gh pr create` work in the sandbox with no
|
|
164
|
+
# model-facing credential tools.
|
|
165
|
+
RUN mkdir -p /opt/sky/bin
|
|
166
|
+
COPY --from=gh-provider /out/gh /opt/sky/bin/gh-real
|
|
167
|
+
RUN /opt/sky/bin/gh-real --version
|
|
168
|
+
COPY packages/infra/agent-image/bin/git-credential-sky \
|
|
169
|
+
packages/infra/agent-image/bin/gh \
|
|
170
|
+
packages/infra/agent-image/bin/aws \
|
|
171
|
+
/opt/sky/bin/
|
|
172
|
+
# System-level gitconfig: github.com credentials come from the helper (git
|
|
173
|
+
# resolves `helper = sky` to git-credential-sky on PATH). useHttpPath makes
|
|
174
|
+
# git send the repo path in the credential request, so the helper sees which
|
|
175
|
+
# repo is being authenticated. Commits attribute to the App bot identity:
|
|
176
|
+
# <app-id>+<app-slug>[bot] is what links a commit to the GitHub App.
|
|
177
|
+
RUN chmod 0755 /opt/sky/bin/git-credential-sky /opt/sky/bin/gh /opt/sky/bin/aws /opt/sky/bin/gh-real \
|
|
178
|
+
&& printf '[credential "https://github.com"]\n\thelper = sky\n\tuseHttpPath = true\n[user]\n\tname = sky[bot]\n\temail = %s+sky[bot]@users.noreply.github.com\n' "${FOUNDATION_GITHUB_APP_ID}" > /etc/gitconfig
|
|
179
|
+
# --- Ambient AWS (aws-broker.ts) --------------------------------------------
|
|
180
|
+
# AWS CLI v2 as /opt/sky/bin/aws-real, fronted by the wrapper copied above.
|
|
181
|
+
# /opt/sky/aws is where server.ts WRITES the generated profile file at boot,
|
|
182
|
+
# one profile per configured read-only role; it is created here (owned by the
|
|
183
|
+
# runtime user) because the container cannot mkdir under a root-owned /opt.
|
|
184
|
+
COPY --from=awscli-build /opt/aws-cli /opt/aws-cli
|
|
185
|
+
RUN ln -s /opt/aws-cli/v2/current/bin/aws /opt/sky/bin/aws-real \
|
|
186
|
+
&& /opt/sky/bin/aws-real --version \
|
|
187
|
+
&& mkdir -p /opt/sky/aws
|
|
188
|
+
|
|
189
|
+
# /opt/sky/bin FIRST so `gh` and `aws` resolve to the wrappers, never the real
|
|
190
|
+
# binaries.
|
|
191
|
+
ENV PATH="/opt/sky/bin:${PATH}"
|
|
192
|
+
|
|
193
|
+
# Non-root runtime user.
|
|
194
|
+
RUN groupadd --gid 10001 sky \
|
|
195
|
+
&& useradd --uid 10001 --gid 10001 --create-home --shell /usr/sbin/nologin sky
|
|
196
|
+
|
|
197
|
+
WORKDIR /app
|
|
198
|
+
|
|
199
|
+
# Workspace install with the frozen lockfile (the build context is the repo
|
|
200
|
+
# root). Workspaces the agent does not run are copied as their MANIFEST ONLY:
|
|
201
|
+
# `bun install --frozen-lockfile` fails with `Workspace not found` if the
|
|
202
|
+
# directory is absent, and this satisfies resolution without shipping source.
|
|
203
|
+
COPY package.json bun.lock bunfig.toml tsconfig.json ./
|
|
204
|
+
# Every workspace the agent actually runs, in full. `bun install
|
|
205
|
+
# --frozen-lockfile` fails with `Workspace not found` if a workspace the
|
|
206
|
+
# lockfile names is absent, so the ones this image does NOT run are copied as
|
|
207
|
+
# their MANIFEST ONLY: enough to satisfy resolution, without shipping source.
|
|
208
|
+
COPY packages/core/ ./packages/core/
|
|
209
|
+
COPY packages/connectors/ ./packages/connectors/
|
|
210
|
+
COPY packages/runtime/ ./packages/runtime/
|
|
211
|
+
COPY packages/capability-otter/ ./packages/capability-otter/
|
|
212
|
+
COPY packages/capability-knock/ ./packages/capability-knock/
|
|
213
|
+
COPY packages/capability-browser/ ./packages/capability-browser/
|
|
214
|
+
COPY packages/capability-email/ ./packages/capability-email/
|
|
215
|
+
COPY packages/capability-crm/ ./packages/capability-crm/
|
|
216
|
+
COPY packages/capability-upwork/ ./packages/capability-upwork/
|
|
217
|
+
COPY packages/agent/ ./packages/agent/
|
|
218
|
+
# Manifest only: the gateway and every proxy are Lambdas, the newsletter is a
|
|
219
|
+
# Lambda service, and the infra package is the CDK app a pipeline runs. None of
|
|
220
|
+
# them executes in this container.
|
|
221
|
+
COPY packages/testing/package.json ./packages/testing/package.json
|
|
222
|
+
COPY packages/gateway/package.json ./packages/gateway/package.json
|
|
223
|
+
COPY packages/newsletter/package.json ./packages/newsletter/package.json
|
|
224
|
+
COPY packages/infra/package.json ./packages/infra/package.json
|
|
225
|
+
|
|
226
|
+
# Enforce the pin's SHAPE before installing — an EXACT version, no ^/~ range.
|
|
227
|
+
# Deliberately not a literal version: asserting one breaks the image build the
|
|
228
|
+
# first time the pin is legitimately bumped, and package.json plus
|
|
229
|
+
# --frozen-lockfile already pin the version itself.
|
|
230
|
+
RUN grep -qE '"@oh-my-pi/pi-coding-agent": "[0-9]+\.[0-9]+\.[0-9]+"' packages/agent/package.json \
|
|
231
|
+
|| (echo "FATAL: packages/agent must pin @oh-my-pi/pi-coding-agent to an EXACT version (no ^/~)" && exit 1)
|
|
232
|
+
RUN bun install --frozen-lockfile --production
|
|
233
|
+
|
|
234
|
+
# The microVM's writable workspace (repo clones, and the memory/skills
|
|
235
|
+
# materialization target /workspace/.omp/).
|
|
236
|
+
RUN mkdir -p /workspace && chown sky:sky /workspace
|
|
237
|
+
|
|
238
|
+
# Baked omp agent base: the fail-hard security overlay, an INTENTIONAL empty
|
|
239
|
+
# AGENTS.md (shadows any ambient user-level context file), and ownership for
|
|
240
|
+
# the runtime user — the materializer writes models.yml here at session build
|
|
241
|
+
# and would EACCES on an un-created, root-owned /opt path.
|
|
242
|
+
COPY packages/infra/agent-image/security-overlay.yml /opt/sky/omp-agent/security-overlay.yml
|
|
243
|
+
RUN touch /opt/sky/omp-agent/AGENTS.md \
|
|
244
|
+
&& chown -R sky:sky /opt/sky/omp-agent /opt/sky/aws
|
|
245
|
+
|
|
246
|
+
USER sky:sky
|
|
247
|
+
|
|
248
|
+
EXPOSE 8080
|
|
249
|
+
|
|
250
|
+
# Entrypoint contract (packages/agent): server.ts starts the :8080
|
|
251
|
+
# /invocations + /ping server, runs the RUNTIME_SECRET_ID bootstrap (key COUNT
|
|
252
|
+
# logged only), and dispatches handleAgentInvocation — including
|
|
253
|
+
# {"_smoke_test": true}.
|
|
254
|
+
CMD ["bun", "run", "packages/agent/src/server.ts"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# aws wrapper for the ambient AWS path (aws-broker.ts).
|
|
3
|
+
#
|
|
4
|
+
# Defaults AWS_PROFILE to `self` for THIS CHILD ONLY and execs the real CLI.
|
|
5
|
+
# The profile lives in the generated /opt/sky/aws/config, where its
|
|
6
|
+
# `credential_process` fetches short-lived credentials for an assumed
|
|
7
|
+
# READ-ONLY role from the loopback broker. `aws --profile <label>` reaches
|
|
8
|
+
# any other account the instance has been given a role in.
|
|
9
|
+
#
|
|
10
|
+
# WHY A WRAPPER, and not `ENV AWS_PROFILE=self` in the Dockerfile: the agent
|
|
11
|
+
# process shares its environment with every subprocess, and its OWN AWS SDK
|
|
12
|
+
# clients (DynamoDB, S3, Secrets Manager, SQS) resolve credentials from that
|
|
13
|
+
# same environment. A global AWS_PROFILE would route those WRITES through a
|
|
14
|
+
# read-only role — and route the broker's own AssumeRole call back into the
|
|
15
|
+
# broker. AWS_CONFIG_FILE is exported globally instead, which is safe because
|
|
16
|
+
# the generated file declares no `[default]` profile: the SDK's ini step
|
|
17
|
+
# finds nothing there and falls through to the execution role, as before.
|
|
18
|
+
#
|
|
19
|
+
# An explicit AWS_PROFILE in the environment wins (never overridden), so
|
|
20
|
+
# `AWS_PROFILE=client-acme aws s3 ls` works too.
|
|
21
|
+
#
|
|
22
|
+
# This instance has no read-only role at all: the `aws-readonly` capability is off,
|
|
23
|
+
# or no role is configured. Say so once, plainly, instead of letting every
|
|
24
|
+
# command fail as an obscure credential error.
|
|
25
|
+
if [ "${FOUNDATION_AWS_DISABLED:-}" = "1" ]; then
|
|
26
|
+
echo "AWS is not connected on this instance." >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Overridable ONLY so the wrapper can be tested against a stub CLI;
|
|
31
|
+
# production always uses the image's real binary.
|
|
32
|
+
AWS_REAL="${FOUNDATION_AWS_REAL:-/opt/sky/bin/aws-real}"
|
|
33
|
+
|
|
34
|
+
AWS_PROFILE="${AWS_PROFILE:-self}" \
|
|
35
|
+
AWS_CONFIG_FILE="${AWS_CONFIG_FILE:-/opt/sky/aws/config}" \
|
|
36
|
+
exec "$AWS_REAL" "$@"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# gh wrapper for the ambient GitHub path (github-broker.ts).
|
|
3
|
+
#
|
|
4
|
+
# Fetches a fresh short-lived installation token from the loopback broker
|
|
5
|
+
# and execs the real gh with GH_TOKEN set, so `gh pr create`, `gh pr checks`,
|
|
6
|
+
# `gh run view`, `gh api`, etc. authenticate as the channel's GitHub App.
|
|
7
|
+
# The broker responds with two lines (token, then UNIX-epoch expiry) — the
|
|
8
|
+
# wrapper takes line 1 only; per-invocation fetch means expiry never
|
|
9
|
+
# matters here. GH_TOKEN lives ONLY in the child's env (the server scrubs
|
|
10
|
+
# any inherited GitHub tokens at start). Without a broker (channel without
|
|
11
|
+
# the github connector) gh runs unauthenticated — public reads still work,
|
|
12
|
+
# writes fail loudly.
|
|
13
|
+
#
|
|
14
|
+
# REPO SCOPING: when the repository in play is knowable — an explicit GH_REPO,
|
|
15
|
+
# else the checkout's github.com origin, else (fallbacks, 2026-08-30) a
|
|
16
|
+
# `-R`/`--repo` flag or the `repos/<owner>/<name>/...` prefix of a `gh api`
|
|
17
|
+
# endpoint — it is passed to the broker as `repo=owner/name` so the minted
|
|
18
|
+
# token covers that repo ONLY, not every repo the channel can reach. A
|
|
19
|
+
# repo-less READ (`gh api /orgs/...`, `gh auth status`) sends nothing and
|
|
20
|
+
# gets the channel-wide token, as before.
|
|
21
|
+
#
|
|
22
|
+
# REPO-LESS WRITES are REFUSED (2026-08-30): a write served the channel-wide
|
|
23
|
+
# token could touch every granted repo, not just the one in play. Only this
|
|
24
|
+
# wrapper sees the verb — the broker sees a credential request — so the
|
|
25
|
+
# wrapper classifies the invocation (`gh api` with a mutating -X/--method,
|
|
26
|
+
# or body fields which imply POST; or a mutating subcommand) and, when no
|
|
27
|
+
# repository could be resolved, declares it to the broker (`write=1`, which
|
|
28
|
+
# refuses and logs it) and exits with a "set GH_REPO" error instead of
|
|
29
|
+
# running the command. NOTE: `gh api graphql` counts as a write (it is a
|
|
30
|
+
# POST and can mutate) — set GH_REPO to use it.
|
|
31
|
+
#
|
|
32
|
+
# An explicit GH_TOKEN in the environment wins (never overridden).
|
|
33
|
+
|
|
34
|
+
# Overridable ONLY so the refusal/scoping paths can be tested against a stub
|
|
35
|
+
# broker and a stub gh binary (dev-runner has the same seam); production
|
|
36
|
+
# always uses the loopback default and the image's real gh.
|
|
37
|
+
# This instance has no GitHub at all: the `github` capability is off, or no GitHub
|
|
38
|
+
# App secret is configured. Say so once, plainly, instead of letting every
|
|
39
|
+
# command fail as an obscure auth error.
|
|
40
|
+
if [ "${FOUNDATION_GITHUB_DISABLED:-}" = "1" ]; then
|
|
41
|
+
echo "GitHub is not connected on this instance." >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
BROKER_URL="${FOUNDATION_GH_BROKER_URL:-http://127.0.0.1:7791/git-credential}"
|
|
46
|
+
GH_REAL="${FOUNDATION_GH_REAL:-/opt/sky/bin/gh-real}"
|
|
47
|
+
|
|
48
|
+
# First positional token of a `gh api` invocation ($1 is "api"): the
|
|
49
|
+
# endpoint. Flags that take a value are skipped WITH their value so an
|
|
50
|
+
# argument like `-f state=closed` is never mistaken for the endpoint.
|
|
51
|
+
api_endpoint() {
|
|
52
|
+
shift # the leading "api"
|
|
53
|
+
while [ $# -gt 0 ]; do
|
|
54
|
+
case "$1" in
|
|
55
|
+
-X|--method|-f|-F|--field|--raw-field|-H|--header|-q|--jq|-t|--template|--input|--cache|--hostname|-p|--preview)
|
|
56
|
+
shift
|
|
57
|
+
[ $# -gt 0 ] && shift
|
|
58
|
+
;;
|
|
59
|
+
-*) shift ;;
|
|
60
|
+
*) printf '%s' "$1"; return 0 ;;
|
|
61
|
+
esac
|
|
62
|
+
done
|
|
63
|
+
return 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if [ -z "$GH_TOKEN" ] && [ -z "$GITHUB_TOKEN" ]; then
|
|
67
|
+
cmd="$1"
|
|
68
|
+
|
|
69
|
+
# --- classify write intent (only the wrapper can: the broker never sees
|
|
70
|
+
# the verb). Unknown/new subcommands classify as reads — the broker's
|
|
71
|
+
# grant intersection and GitHub's own permission checks still bound them.
|
|
72
|
+
is_write=""
|
|
73
|
+
if [ "$cmd" = "api" ]; then
|
|
74
|
+
method=""
|
|
75
|
+
has_body=""
|
|
76
|
+
want_method=""
|
|
77
|
+
for arg in "$@"; do
|
|
78
|
+
if [ -n "$want_method" ]; then method="$arg"; want_method=""; continue; fi
|
|
79
|
+
case "$arg" in
|
|
80
|
+
-X|--method) want_method=1 ;;
|
|
81
|
+
--method=*) method="${arg#--method=}" ;;
|
|
82
|
+
-X?*) method="${arg#-X}"; method="${method#=}" ;;
|
|
83
|
+
-f|-F|--field|--raw-field|--input|--field=*|--raw-field=*|--input=*|-f?*|-F?*) has_body=1 ;;
|
|
84
|
+
esac
|
|
85
|
+
done
|
|
86
|
+
if [ -n "$method" ]; then
|
|
87
|
+
case "$method" in
|
|
88
|
+
[Gg][Ee][Tt]|[Hh][Ee][Aa][Dd]) ;;
|
|
89
|
+
*) is_write=1 ;;
|
|
90
|
+
esac
|
|
91
|
+
elif [ -n "$has_body" ]; then
|
|
92
|
+
is_write=1 # gh api implies POST when body fields are supplied
|
|
93
|
+
fi
|
|
94
|
+
else
|
|
95
|
+
case "$cmd" in
|
|
96
|
+
''|-*|auth|browse|search|status|config|alias|completion|help|version) ;;
|
|
97
|
+
*)
|
|
98
|
+
case "$2" in
|
|
99
|
+
create|delete|edit|merge|close|reopen|comment|review|ready|lock|unlock|transfer|develop|pin|unpin|upload|delete-asset|set|remove|rename|archive|unarchive|sync|enable|disable|cancel|rerun|run|dismiss|approve|update-branch|fork|link|unlink|mark-template|deploy) is_write=1 ;;
|
|
100
|
+
esac
|
|
101
|
+
;;
|
|
102
|
+
esac
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# --- resolve the repository in play. GH_REPO, then the checkout's origin
|
|
106
|
+
# (both as before — a repo resolved yesterday is scoped identically today),
|
|
107
|
+
# then the fallbacks: -R/--repo, and a `repos/<owner>/<name>` api endpoint.
|
|
108
|
+
repo="$GH_REPO"
|
|
109
|
+
if [ -z "$repo" ]; then
|
|
110
|
+
origin="$(git config --get remote.origin.url 2>/dev/null)"
|
|
111
|
+
case "$origin" in
|
|
112
|
+
https://github.com/*) repo="${origin#https://github.com/}" ;;
|
|
113
|
+
git@github.com:*) repo="${origin#git@github.com:}" ;;
|
|
114
|
+
ssh://git@github.com/*) repo="${origin#ssh://git@github.com/}" ;;
|
|
115
|
+
esac
|
|
116
|
+
repo="${repo%.git}"
|
|
117
|
+
fi
|
|
118
|
+
if [ -z "$repo" ]; then
|
|
119
|
+
flag_repo=""
|
|
120
|
+
want_repo=""
|
|
121
|
+
for arg in "$@"; do
|
|
122
|
+
if [ -n "$want_repo" ]; then flag_repo="$arg"; want_repo=""; continue; fi
|
|
123
|
+
case "$arg" in
|
|
124
|
+
-R|--repo) want_repo=1 ;;
|
|
125
|
+
--repo=*) flag_repo="${arg#--repo=}" ;;
|
|
126
|
+
-R?*) flag_repo="${arg#-R}"; flag_repo="${flag_repo#=}" ;;
|
|
127
|
+
esac
|
|
128
|
+
done
|
|
129
|
+
case "$flag_repo" in
|
|
130
|
+
https://github.com/*) flag_repo="${flag_repo#https://github.com/}" ;;
|
|
131
|
+
github.com/*) flag_repo="${flag_repo#github.com/}" ;;
|
|
132
|
+
esac
|
|
133
|
+
flag_repo="${flag_repo%.git}"
|
|
134
|
+
case "$flag_repo" in
|
|
135
|
+
*/*/*) ;; # HOST/OWNER/NAME on some other host — not brokered
|
|
136
|
+
*/*) repo="$flag_repo" ;;
|
|
137
|
+
esac
|
|
138
|
+
fi
|
|
139
|
+
if [ -z "$repo" ] && [ "$cmd" = "api" ]; then
|
|
140
|
+
endpoint="$(api_endpoint "$@")"
|
|
141
|
+
endpoint="${endpoint%%\?*}"
|
|
142
|
+
endpoint="${endpoint#/}"
|
|
143
|
+
case "$endpoint" in
|
|
144
|
+
repos/*/*)
|
|
145
|
+
rest="${endpoint#repos/}"
|
|
146
|
+
owner="${rest%%/*}"
|
|
147
|
+
rest="${rest#*/}"
|
|
148
|
+
name="${rest%%/*}"
|
|
149
|
+
repo="$owner/$name"
|
|
150
|
+
;;
|
|
151
|
+
esac
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
query=""
|
|
155
|
+
case "$repo" in
|
|
156
|
+
'' | *[!A-Za-z0-9._/-]*) ;;
|
|
157
|
+
*/*) query="?repo=$repo" ;;
|
|
158
|
+
esac
|
|
159
|
+
|
|
160
|
+
# FAIL CLOSED on a repo-less write: the channel-wide token must never back
|
|
161
|
+
# a mutation. The probe (`write=1`) lets the broker log and refuse it
|
|
162
|
+
# server-side; only connection-refused (curl rc=7: no broker = channel
|
|
163
|
+
# without the github connector) falls through to anonymous gh, whose
|
|
164
|
+
# writes fail at GitHub exactly as before.
|
|
165
|
+
if [ -n "$is_write" ] && [ -z "$query" ]; then
|
|
166
|
+
curl -fsS --max-time 25 --retry 2 --retry-connrefused "$BROKER_URL?write=1" >/dev/null 2>&1
|
|
167
|
+
if [ $? -ne 7 ]; then
|
|
168
|
+
echo "gh: refusing a GitHub write with no resolved repository — set GH_REPO=owner/name (or pass -R owner/name, or run inside the repository checkout) so the credential can be scoped to that one repo" >&2
|
|
169
|
+
exit 1
|
|
170
|
+
fi
|
|
171
|
+
exec "$GH_REAL" "$@"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
# FAIL CLOSED when the broker exists but misbehaves (2026-08-16): an
|
|
175
|
+
# unauthenticated fallthrough turns a broker hiccup into a misleading
|
|
176
|
+
# GitHub 404 ("PR not found") that models mistake for a missing PR or a
|
|
177
|
+
# sandbox limitation. Only connection-refused (curl rc=7: no broker =
|
|
178
|
+
# channel without the github connector) falls through to anonymous.
|
|
179
|
+
response="$(curl -fsS --max-time 25 --retry 2 --retry-connrefused "$BROKER_URL$query" 2>/dev/null)"
|
|
180
|
+
rc=$?
|
|
181
|
+
if [ $rc -eq 0 ]; then
|
|
182
|
+
token="$(printf '%s\n' "$response" | head -n 1)"
|
|
183
|
+
if [ -n "$token" ]; then
|
|
184
|
+
GH_TOKEN="$token" exec "$GH_REAL" "$@"
|
|
185
|
+
fi
|
|
186
|
+
echo "gh: credential broker returned an empty token — transient; retry shortly" >&2
|
|
187
|
+
exit 1
|
|
188
|
+
elif [ $rc -ne 7 ]; then
|
|
189
|
+
echo "gh: could not obtain GitHub credentials from broker (curl exit $rc) — transient; retry shortly" >&2
|
|
190
|
+
exit 1
|
|
191
|
+
fi
|
|
192
|
+
fi
|
|
193
|
+
exec "$GH_REAL" "$@"
|