@deployfoundation/foundation-deploy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +174 -0
  2. package/agent-image/Dockerfile +254 -0
  3. package/agent-image/bin/aws +36 -0
  4. package/agent-image/bin/gh +193 -0
  5. package/agent-image/bin/git-credential-sky +89 -0
  6. package/agent-image/security-overlay.yml +176 -0
  7. package/cdk.json +6 -0
  8. package/dist/bin/app.js +112 -0
  9. package/dist/bin/foundation-deploy.js +1906 -0
  10. package/dist/bin/release-account.js +154 -0
  11. package/dist/chunk-4aye5cee.js +2416 -0
  12. package/dist/chunk-9ddxyvq2.js +1455 -0
  13. package/dist/chunk-v7tz8g50.js +428 -0
  14. package/dist/src/index.js +88 -0
  15. package/package.json +38 -0
  16. package/pipeline/buildspec.yml +34 -0
  17. package/src/artifacts.ts +318 -0
  18. package/src/deploy/assets/github-app-manifest.yml +29 -0
  19. package/src/deploy/assets/slack-app-manifest.yml +95 -0
  20. package/src/deploy/aws.ts +265 -0
  21. package/src/deploy/cli.ts +212 -0
  22. package/src/deploy/config-sync.ts +93 -0
  23. package/src/deploy/config.ts +29 -0
  24. package/src/deploy/deploy.ts +566 -0
  25. package/src/deploy/endpoint.ts +242 -0
  26. package/src/deploy/github-app-create.ts +154 -0
  27. package/src/deploy/github-app-manifest.ts +53 -0
  28. package/src/deploy/image.ts +80 -0
  29. package/src/deploy/instance.ts +87 -0
  30. package/src/deploy/license-cache.ts +47 -0
  31. package/src/deploy/license.ts +272 -0
  32. package/src/deploy/paths.ts +65 -0
  33. package/src/deploy/post-deploy.ts +97 -0
  34. package/src/deploy/release.ts +282 -0
  35. package/src/deploy/runtime-secret.ts +241 -0
  36. package/src/deploy/setup.ts +393 -0
  37. package/src/deploy/sh.ts +74 -0
  38. package/src/deploy/slack-manifest.ts +112 -0
  39. package/src/deploy/stage-customization.ts +224 -0
  40. package/src/deploy/tracing.ts +243 -0
  41. package/src/deploy-permissions.ts +165 -0
  42. package/src/index.ts +60 -0
  43. package/src/lambda-bundle-context.ts +64 -0
  44. package/src/names.ts +170 -0
  45. package/src/release/kms.ts +86 -0
  46. package/src/release/manifest.ts +265 -0
  47. package/src/stacks/agent-stack.ts +938 -0
  48. package/src/stacks/api-stack.ts +1005 -0
  49. package/src/stacks/ci-stack.ts +96 -0
  50. package/src/stacks/data-stack.ts +446 -0
  51. package/src/stacks/network-stack.ts +282 -0
  52. package/src/stacks/newsletter-stack.ts +572 -0
  53. package/src/stacks/pipeline-stack.ts +242 -0
  54. package/src/stacks/release-account-stack.ts +229 -0
@@ -0,0 +1,89 @@
1
+ #!/bin/sh
2
+ # git credential helper for the ambient GitHub path (github-broker.ts).
3
+ #
4
+ # git invokes `git-credential-sky get` for https://github.com operations
5
+ # (wired via /etc/gitconfig with `useHttpPath = true`, so the `get` request
6
+ # on stdin carries protocol/host/path). We answer ONLY for host=github.com
7
+ # over https — anything else gets no output, so the token can never be
8
+ # handed to a look-alike host. On a match we fetch a short-lived,
9
+ # repo-scoped GitHub App installation token from the in-container loopback
10
+ # broker and emit it in git's credential protocol, marked `ephemeral=1`
11
+ # (never stored) with `password_expiry_utc` so git >=2.34 discards it the
12
+ # moment it expires. When no broker is running (channel without the github
13
+ # connector), emit nothing and exit 0 — git falls through to anonymous
14
+ # access, so public clones still work.
15
+ #
16
+ # Broker contract: GET http://127.0.0.1:7791/git-credential[?repo=<path>] ->
17
+ # line 1: bare installation token
18
+ # line 2: UNIX-epoch expiry seconds
19
+ #
20
+ # The `path` git sends (useHttpPath = true) is forwarded as `repo=`, so the
21
+ # broker mints a token scoped to THAT repository instead of every repo the
22
+ # channel can reach. The broker reads only the first two path segments and
23
+ # refuses a repo the channel was not granted (HTTP 403).
24
+
25
+ # Overridable ONLY so this helper can be tested against a stub broker on an
26
+ # ephemeral port; production always uses the loopback default.
27
+ # No GitHub on this instance (capability off, or no App secret). Refuse loudly
28
+ # rather than emitting nothing, which git would silently retry anonymously.
29
+ if [ "${FOUNDATION_GITHUB_DISABLED:-}" = "1" ]; then
30
+ echo "GitHub is not connected on this instance." >&2
31
+ exit 1
32
+ fi
33
+
34
+ BROKER_URL="${FOUNDATION_GH_BROKER_URL:-http://127.0.0.1:7791/git-credential}"
35
+
36
+ [ "$1" = "get" ] || exit 0
37
+
38
+ # Parse the credential request (key=value lines, blank-line terminated).
39
+ host=""
40
+ protocol=""
41
+ path=""
42
+ while IFS= read -r line; do
43
+ [ -n "$line" ] || break
44
+ case "$line" in
45
+ host=*) host="${line#host=}" ;;
46
+ protocol=*) protocol="${line#protocol=}" ;;
47
+ path=*) path="${line#path=}" ;;
48
+ esac
49
+ done
50
+ [ "$host" = "github.com" ] || exit 0
51
+ [ "$protocol" = "https" ] || exit 0
52
+
53
+ # Only a plain repo path is forwarded — anything with characters that would
54
+ # need URL-escaping is dropped, which costs breadth (a channel-wide token)
55
+ # but never sends a malformed scope.
56
+ query=""
57
+ case "$path" in
58
+ '' | *[!A-Za-z0-9._/-]*) ;;
59
+ *) query="?repo=$path" ;;
60
+ esac
61
+
62
+ response="$(curl -fsS --max-time 25 --retry 2 --retry-connrefused "$BROKER_URL$query" 2>/dev/null)"
63
+ rc=$?
64
+ # rc=7 (connection refused) = no broker: silent anonymous fallthrough is
65
+ # correct (public clones on connector-less channels). ANY other failure is
66
+ # a broker that exists but misbehaved: fail LOUDLY (git reports the helper
67
+ # error) instead of degrading to anonymous and a misleading 404.
68
+ if [ $rc -eq 7 ]; then exit 0; fi
69
+ if [ $rc -ne 0 ]; then
70
+ # curl exit 22 = an HTTP error from the broker; the one that is NOT
71
+ # transient is 403 ("repository not granted"), i.e. this repo is not in the
72
+ # channel's grants — ask a human to grant it rather than retrying.
73
+ echo "git-credential-sky: broker error (curl exit $rc) — transient unless this repo is not in the instance's GitHub App installation" >&2
74
+ exit 1
75
+ fi
76
+ token="$(printf '%s\n' "$response" | head -n 1)"
77
+ expiry="$(printf '%s\n' "$response" | sed -n '2p')"
78
+ if [ -z "$token" ]; then
79
+ echo "git-credential-sky: broker returned an empty token — transient; retry shortly" >&2
80
+ exit 1
81
+ fi
82
+ printf 'username=x-access-token\npassword=%s\nephemeral=1\n' "$token"
83
+ # Expiry only when the broker sent a well-formed epoch (defense in depth —
84
+ # a malformed line 2 must not corrupt the credential answer).
85
+ case "$expiry" in
86
+ '' | *[!0-9]*) ;;
87
+ *) printf 'password_expiry_utc=%s\n' "$expiry" ;;
88
+ esac
89
+ exit 0
@@ -0,0 +1,176 @@
1
+ # Foundation fail-hard security overlay (omp-ref §3.4).
2
+ #
3
+ # Loaded via PI_CONFIG_FILES: unlike global/project config (silently moved to
4
+ # .broken-* on parse failure), OVERLAY FILES FAIL HARD — a corrupt copy halts
5
+ # the container instead of silently degrading the security posture.
6
+ #
7
+ # Arrays in omp settings REPLACE — ship complete lists only here or in the
8
+ # per-session project config, never split across layers.
9
+ tools:
10
+ approvalMode: yolo
11
+ # Global timeout ceiling in seconds — caps every resolved bash/eval timeout
12
+ # (defaults included). NOTE: omp treats `timeout: 0` ("no deadline") as
13
+ # bypassing this ceiling and Foundation ships no tool_call hook to rewrite it; the
14
+ # 15-minute AgentCore invocation limit is the effective backstop.
15
+ maxTimeout: 600
16
+ approval:
17
+ ast_edit: deny
18
+ browser: deny
19
+ computer: deny
20
+ debug: deny
21
+ goal: deny
22
+ grep: deny
23
+ hub: deny
24
+ init_experiment: deny
25
+ inspect_image: deny
26
+ # `learn` is NOT denied here: omp already gates it on `memory.backend`
27
+ # being on (tools/index.ts), and that value is written per session from
28
+ # `config.memory.ompBackend` (default `off`). A deny here would make the
29
+ # opt-in impossible to exercise. Every OTHER omp memory tool stays denied
30
+ # below, because each needs a backend Foundation will not run.
31
+ log_experiment: deny
32
+ # omp's remaining memory tools. `recall`/`retain`/`reflect` need the
33
+ # `hindsight` (remote HTTP server) or `mnemopi` (SQLite + embeddings)
34
+ # backend, and `memory_edit` needs `mnemopi`. Foundation runs neither — SQLite on
35
+ # the NFS-like S3 Files mount is a corruption risk and there is no
36
+ # Hindsight server — so these are denied outright rather than left to the
37
+ # backend gate. See docs/memory-backends.md.
38
+ memory_edit: deny
39
+ recall: deny
40
+ reflect: deny
41
+ retain: deny
42
+ run_experiment: deny
43
+ # `task: deny` was REMOVED 2026-09-01 when subagents were enabled (see
44
+ # ts/agent/src/tools/exec.ts TOOL_ALLOWLIST). A deny here would have left
45
+ # the tool activated-but-dead: deny wins in every approval mode.
46
+ todo: deny
47
+ update_notes: deny
48
+ web_search: deny
49
+ bash:
50
+ # Deny rules hold even in yolo mode and match compound-command segments
51
+ # (omp docs/tools/bash.md). Ambient git credentials (github-broker.ts)
52
+ # make force-pushes POSSIBLE — these rules make the destructive ones
53
+ # refuse locally; GitHub branch protection is the server-side backstop.
54
+ patterns:
55
+ - pattern: "git push*--force*"
56
+ action: deny
57
+ - pattern: "git push*-f *"
58
+ action: deny
59
+ - pattern: "git push*--delete*"
60
+ action: deny
61
+ # HackerOne #3556799 class (cited by claude-code-action's push wrapper):
62
+ # --receive-pack/--exec run arbitrary commands server-side, ext:: remotes
63
+ # run arbitrary commands locally, and URL-form remotes push the repo to
64
+ # hosts outside the credential helper's github.com scope.
65
+ - pattern: "git push*--receive-pack*"
66
+ action: deny
67
+ - pattern: "git push*--exec*"
68
+ action: deny
69
+ - pattern: "*ext::*"
70
+ action: deny
71
+ - pattern: "git push http*"
72
+ action: deny
73
+ - pattern: "git push git@*"
74
+ action: deny
75
+ - pattern: "git push ssh:*"
76
+ action: deny
77
+ # NOTE: colon-refspec deletes (`git push origin :branch`) are NOT denied —
78
+ # a glob on `:` would also break legitimate `push HEAD:refs/heads/x`.
79
+ # GitHub branch protection covers the remainder.
80
+ # Foundation additions: local branch/repo destruction and direct pushes to the default branch.
81
+ - pattern: "git branch -D*"
82
+ action: deny
83
+ - pattern: "git branch --delete --force*"
84
+ action: deny
85
+ - pattern: "gh repo delete*"
86
+ action: deny
87
+ - pattern: "git push*origin main*"
88
+ action: deny
89
+ - pattern: "git push*origin master*"
90
+ action: deny
91
+ - pattern: "git push*HEAD:main*"
92
+ action: deny
93
+ - pattern: "git push*HEAD:master*"
94
+ action: deny
95
+ # Ambient AWS (aws-broker.ts). BELT AND BRACES ONLY: the real guard is
96
+ # IAM — every profile fetches credentials for a role carrying AWS's
97
+ # ReadOnlyAccess and nothing else, so a mutation is refused by the API
98
+ # whatever gets typed here. These patterns make the refusal LOCAL and
99
+ # legible instead of an AccessDenied the model has to interpret, and they
100
+ # cover the verbs whose blast radius is worst if IAM is ever loosened.
101
+ - pattern: "aws *delete*"
102
+ action: deny
103
+ - pattern: "aws *put*"
104
+ action: deny
105
+ - pattern: "aws *create*"
106
+ action: deny
107
+ - pattern: "aws *update*"
108
+ action: deny
109
+ - pattern: "aws *terminate*"
110
+ action: deny
111
+ - pattern: "aws *modify*"
112
+ action: deny
113
+ - pattern: "aws *attach*"
114
+ action: deny
115
+ - pattern: "aws *detach*"
116
+ action: deny
117
+ - pattern: "aws *run-instances*"
118
+ action: deny
119
+ # `start-query` and `stop-query` (Logs Insights) are how the log tools
120
+ # would be driven from bash, so the start/stop verbs are NOT denied
121
+ # wholesale. `aws logs put-*` is already covered by the `put` rule above.
122
+ - pattern: "aws *start-instances*"
123
+ action: deny
124
+ - pattern: "aws *stop-instances*"
125
+ action: deny
126
+ # omp's native auto-learn: after a substantial turn the agent may distill what
127
+ # it did into `<agentDir>/managed-skills/<name>/SKILL.md` — a path symlinked to
128
+ # the persistent mount, so a learned skill survives the microVM. `autoContinue`
129
+ # is off: learning must never extend a turn the human is waiting on. `learn`
130
+ # (the other omp learning tool) is available only when an instance sets
131
+ # `memory.ompBackend: local`, which is off by default.
132
+ autolearn:
133
+ enabled: true
134
+ minToolCalls: 6
135
+ autoContinue: false
136
+ task:
137
+ # Recursion ceiling (omp's own default, pinned explicitly). The child's TOOL
138
+ # LIST is NOT here — it comes from the materialized
139
+ # `<workspace>/.omp/agents/task.md` definition; this file carries the bounds
140
+ # that must survive a project-config write failure.
141
+ maxRecursionDepth: 2
142
+ isolation:
143
+ # Isolated subagents are OFF. An isolated child is the one child type omp
144
+ # constructs with BOTH extension preload lists cleared
145
+ # (task/isolation-runner.ts), so it would rebuild its tool set from ambient
146
+ # discovery. With mode "none" the `isolated` parameter is not offered on the
147
+ # task schema and any request for it throws at preflight, so such a child
148
+ # cannot exist.
149
+ mode: none
150
+ # omp's default. `per-call` was pinned by the TS rewrite for "isolation
151
+ # between calls" the microVM already provides, while omp's eval tool text
152
+ # promises state persistence — see the matching setting in exec.ts.
153
+ python:
154
+ kernelMode: session
155
+ # The two capabilities that would otherwise AUTO-WIDEN an explicit tool list
156
+ # with a tool this file denies — at the top level and in every subagent.
157
+ astEdit:
158
+ # omp's default is TRUE, and it pushes the denied `ast_edit` into any explicit
159
+ # tool list containing `edit`. Off, not just denied.
160
+ enabled: false
161
+ web_search:
162
+ enabled: false
163
+ magicKeywords:
164
+ enabled: true
165
+ ultrathink: true
166
+ # Keep OMP's other built-in prompt modes off; this change is only for
167
+ # ultrathink. OMP 18.1.14 has no ultracode magic keyword or hook.
168
+ orchestrate: false
169
+ workflow: false
170
+ # NOTE: `memory.backend` is deliberately ABSENT from this fail-hard overlay.
171
+ # It is written per session into `<workspace>/.omp/config.yml` from
172
+ # `config.memory.ompBackend`, so an instance can opt in without an image build.
173
+ # This fails safe: omp's own schema default is `off`, so a project-config write
174
+ # that never lands leaves omp's memory subsystem off.
175
+ mcp:
176
+ enableProjectConfig: false
package/cdk.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "app": "bun run bin/app.ts",
3
+ "context": {
4
+ "@aws-cdk/core:validateAgainstDefaultRules": true
5
+ }
6
+ }
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env bun
2
+ import {
3
+ FoundationAgent,
4
+ FoundationApi,
5
+ FoundationCi,
6
+ FoundationData,
7
+ FoundationNetwork,
8
+ FoundationPipeline,
9
+ NewsletterStack
10
+ } from "../chunk-4aye5cee.js";
11
+ import {
12
+ adminsFor,
13
+ instanceNames,
14
+ loadInstanceFile
15
+ } from "../chunk-9ddxyvq2.js";
16
+
17
+ // bin/app.ts
18
+ import { execFileSync } from "node:child_process";
19
+ import { basename, dirname, relative, resolve, sep } from "node:path";
20
+ import * as cdk from "aws-cdk-lib";
21
+ var app = new cdk.App;
22
+ var instanceFilePath = app.node.tryGetContext("instanceFile");
23
+ if (instanceFilePath === undefined || instanceFilePath === "")
24
+ throw new Error("-c instanceFile=<path> is required: the path to the deployment's instance YAML");
25
+ var { instance, configPath } = loadInstanceFile(instanceFilePath);
26
+ var names = instanceNames(instance);
27
+ var env = {
28
+ account: process.env.CDK_DEFAULT_ACCOUNT,
29
+ region: process.env.CDK_DEFAULT_REGION ?? instance.aws.region
30
+ };
31
+ var admins = app.node.tryGetContext("admins") ?? adminsFor(configPath);
32
+ var alarmEmail = app.node.tryGetContext("alarmEmail");
33
+ var pipelineInstanceFile = app.node.tryGetContext("pipelineInstanceFile") ?? repoRelativePath(instanceFilePath);
34
+ var network = new FoundationNetwork(app, names.network, { env, instance, configPath });
35
+ var data = new FoundationData(app, names.data, { env, instance });
36
+ var agent = new FoundationAgent(app, names.agent, {
37
+ env,
38
+ instance,
39
+ vpc: network.vpc,
40
+ egressSubnets: network.egressSubnets,
41
+ agentSecurityGroup: network.agentSecurityGroup,
42
+ mountTargetSecurityGroup: network.mountTargetSecurityGroup,
43
+ fileSystemId: data.fileSystem.getAtt("FileSystemId").toString(),
44
+ dataKey: data.dataKey,
45
+ bucket: data.bucket,
46
+ documentBucket: data.documentBucket,
47
+ table: data.table,
48
+ itemsTable: data.itemsTable,
49
+ ...data.crmTable === undefined ? {} : { crmTable: data.crmTable },
50
+ ...data.upworkApprovalTable === undefined ? {} : { upworkApprovalTable: data.upworkApprovalTable },
51
+ secrets: data.secrets
52
+ });
53
+ new FoundationCi(app, names.ci, { env, instance, bucket: data.bucket, dataKey: data.dataKey });
54
+ if (instance.deploy.via === "codepipeline")
55
+ new FoundationPipeline(app, names.pipeline, {
56
+ env,
57
+ instance,
58
+ bucket: data.bucket,
59
+ dataKey: data.dataKey,
60
+ instanceFilePath: pipelineInstanceFile,
61
+ ...alarmEmail === undefined ? {} : { alarmEmail }
62
+ });
63
+ var api = new FoundationApi(app, names.api, {
64
+ env,
65
+ instance,
66
+ configPath,
67
+ dataKey: data.dataKey,
68
+ table: data.table,
69
+ itemsTable: data.itemsTable,
70
+ ...data.crmTable === undefined ? {} : { crmTable: data.crmTable },
71
+ ...data.upworkApprovalTable === undefined ? {} : { upworkApprovalTable: data.upworkApprovalTable },
72
+ secrets: {
73
+ signing: data.secrets.signing,
74
+ slackApp: data.secrets.slackApp,
75
+ googleCalendar: data.secrets.googleCalendar,
76
+ googleOauth: data.secrets.googleOauth,
77
+ googleDrive: data.secrets.googleDrive,
78
+ googleEmail: data.secrets.googleEmail,
79
+ ...data.secrets.otterApi === undefined ? {} : { otterApi: data.secrets.otterApi },
80
+ ...data.secrets.knockOauthClient === undefined ? {} : { knockOauthClient: data.secrets.knockOauthClient },
81
+ ...data.secrets.knockCredential === undefined ? {} : { knockCredential: data.secrets.knockCredential },
82
+ ...data.secrets.upwork === undefined ? {} : { upwork: data.secrets.upwork }
83
+ },
84
+ agentRuntimeArn: agent.agentRuntimeArn,
85
+ admins,
86
+ alarmEmail
87
+ });
88
+ api.addStackDependency(agent);
89
+ if (instance.newsletter.enabled)
90
+ new NewsletterStack(app, names.newsletter, {
91
+ env,
92
+ instance,
93
+ ...alarmEmail === undefined ? {} : { alarmEmail }
94
+ });
95
+ cdk.Tags.of(app).add("project", instance.naming.secretPrefix);
96
+ app.synth();
97
+ function repoRelativePath(filePath) {
98
+ const absolute = resolve(filePath);
99
+ try {
100
+ const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
101
+ cwd: dirname(absolute),
102
+ stdio: ["ignore", "pipe", "ignore"]
103
+ }).toString().trim();
104
+ const rel = relative(top, absolute);
105
+ if (rel !== "" && !rel.startsWith(".."))
106
+ return rel.split(sep).join("/");
107
+ } catch {}
108
+ return basename(absolute);
109
+ }
110
+ export {
111
+ env
112
+ };