@deployfoundation/foundation-deploy 0.2.46 → 0.2.47
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 +10 -0
- package/agent-image/git-hooks/pre-push +112 -0
- package/agent-image/security-overlay.yml +15 -4
- package/dist/bin/app.js +2 -2
- package/dist/bin/foundation-deploy.js +1 -1
- package/dist/{chunk-32ytmv5k.js → chunk-k6mcv6dk.js} +16 -1
- package/dist/{chunk-cazzxewf.js → chunk-yn8x77x6.js} +1 -1
- package/dist/src/index.js +2 -2
- package/package.json +1 -1
package/agent-image/Dockerfile
CHANGED
|
@@ -204,6 +204,16 @@ COPY packages/infra/agent-image/bin/git-credential-sky \
|
|
|
204
204
|
# <app-id>+<app-slug>[bot] is what links a commit to the GitHub App.
|
|
205
205
|
RUN chmod 0755 /opt/sky/bin/git-credential-sky /opt/sky/bin/gh /opt/sky/bin/aws /opt/sky/bin/gh-real \
|
|
206
206
|
&& 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
|
|
207
|
+
# Pushes to main/master: refused unless config.yaml's github.directPush names the
|
|
208
|
+
# repo and every changed path (git-hooks/pre-push; the agent exports the list as
|
|
209
|
+
# FOUNDATION_DIRECT_PUSH each turn). core.hooksPath is set through GIT_CONFIG_*,
|
|
210
|
+
# which git ranks like `-c`, above a cloned repo's own config, so a repo cannot
|
|
211
|
+
# switch the guard off; the hook then runs that repo's own pre-push hook.
|
|
212
|
+
COPY packages/infra/agent-image/git-hooks/pre-push /opt/sky/git-hooks/pre-push
|
|
213
|
+
RUN chmod 0755 /opt/sky/git-hooks/pre-push
|
|
214
|
+
ENV GIT_CONFIG_COUNT=1 \
|
|
215
|
+
GIT_CONFIG_KEY_0=core.hooksPath \
|
|
216
|
+
GIT_CONFIG_VALUE_0=/opt/sky/git-hooks
|
|
207
217
|
# --- Ambient AWS (aws-broker.ts) --------------------------------------------
|
|
208
218
|
# AWS CLI v2 as /opt/sky/bin/aws-real, fronted by the wrapper copied above.
|
|
209
219
|
# /opt/sky/aws is where server.ts WRITES the generated profile file at boot,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Foundation's guard on pushes to a default branch (main or master).
|
|
3
|
+
|
|
4
|
+
git runs this before every push from the agent's sandbox: the image sets
|
|
5
|
+
core.hooksPath through GIT_CONFIG_COUNT/KEY/VALUE, which git treats like `-c`
|
|
6
|
+
and so ranks above a repository's own config. A push that updates
|
|
7
|
+
refs/heads/main or refs/heads/master is refused unless the instance's
|
|
8
|
+
`github.directPush` (FOUNDATION_DIRECT_PUSH, JSON, set by the agent from
|
|
9
|
+
config.yaml every turn) names this repository AND every file the push changes
|
|
10
|
+
sits under one of its paths. Deleting or creating main, or a push that is not a
|
|
11
|
+
fast-forward, is always refused. Pushes to any other branch pass. The
|
|
12
|
+
repository's own pre-push hook, if it has one, runs afterwards.
|
|
13
|
+
|
|
14
|
+
stdin, one line per ref: <local ref> <local sha> <remote ref> <remote sha>
|
|
15
|
+
argv: <remote name> <remote url>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
DEFAULT_BRANCHES = ("refs/heads/main", "refs/heads/master")
|
|
25
|
+
REPO_FROM_URL = re.compile(r"^https://(?:[^@/]+@)?github\.com/([^/]+/[^/]+?)(?:\.git)?/?$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def git(*args: str) -> str:
|
|
29
|
+
return subprocess.run(["git", *args], capture_output=True, text=True, check=True).stdout
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_zero(sha: str) -> bool:
|
|
33
|
+
return set(sha) == {"0"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def allowed_paths(repo: str) -> list[str]:
|
|
37
|
+
try:
|
|
38
|
+
rules = json.loads(os.environ.get("FOUNDATION_DIRECT_PUSH") or "[]")
|
|
39
|
+
except ValueError:
|
|
40
|
+
return []
|
|
41
|
+
for rule in rules if isinstance(rules, list) else []:
|
|
42
|
+
if isinstance(rule, dict) and str(rule.get("repo", "")).lower() == repo.lower():
|
|
43
|
+
return [p for p in rule.get("paths", []) if isinstance(p, str) and p]
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def under(path: str, allowed: list[str]) -> bool:
|
|
48
|
+
return any(path.startswith(a) if a.endswith("/") else path == a for a in allowed)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def refuse(message: str) -> None:
|
|
52
|
+
print(f"Foundation: {message}", file=sys.stderr)
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check(remote_url: str, lines: list[str]) -> None:
|
|
57
|
+
match = REPO_FROM_URL.match(remote_url)
|
|
58
|
+
repo = match.group(1) if match else ""
|
|
59
|
+
for line in lines:
|
|
60
|
+
parts = line.split()
|
|
61
|
+
if len(parts) != 4:
|
|
62
|
+
continue
|
|
63
|
+
_local_ref, local_sha, remote_ref, remote_sha = parts
|
|
64
|
+
if remote_ref not in DEFAULT_BRANCHES:
|
|
65
|
+
continue
|
|
66
|
+
branch = remote_ref.removeprefix("refs/heads/")
|
|
67
|
+
allowed = allowed_paths(repo) if repo else []
|
|
68
|
+
if not allowed:
|
|
69
|
+
refuse(
|
|
70
|
+
f"never push to {branch}. Push a branch and open a pull request"
|
|
71
|
+
f" ({repo or remote_url} has no github.directPush exception)."
|
|
72
|
+
)
|
|
73
|
+
if is_zero(local_sha) or is_zero(remote_sha):
|
|
74
|
+
refuse(f"deleting or creating {branch} is never allowed.")
|
|
75
|
+
try:
|
|
76
|
+
git("merge-base", "--is-ancestor", remote_sha, local_sha)
|
|
77
|
+
except subprocess.CalledProcessError:
|
|
78
|
+
refuse(f"{branch} moved or is not a fast-forward: pull, re-apply your change, push again.")
|
|
79
|
+
changed = [p for p in git("diff", "--name-only", "-z", remote_sha, local_sha).split("\0") if p]
|
|
80
|
+
outside = [p for p in changed if not under(p, allowed)]
|
|
81
|
+
if outside:
|
|
82
|
+
refuse(
|
|
83
|
+
f"this push to {branch} changes files outside {', '.join(allowed)}: "
|
|
84
|
+
f"{', '.join(outside[:5])}. Push a branch and open a pull request."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_repository_hook(argv: list[str], stdin: str) -> int:
|
|
89
|
+
"""The repository's own pre-push hook: its core.hooksPath, else .git/hooks."""
|
|
90
|
+
try:
|
|
91
|
+
hooks = git("config", "--local", "--get", "core.hooksPath").strip()
|
|
92
|
+
except subprocess.CalledProcessError:
|
|
93
|
+
hooks = ""
|
|
94
|
+
if hooks == "":
|
|
95
|
+
hooks = os.path.join(git("rev-parse", "--git-common-dir").strip(), "hooks")
|
|
96
|
+
elif not os.path.isabs(hooks):
|
|
97
|
+
hooks = os.path.join(git("rev-parse", "--show-toplevel").strip(), hooks)
|
|
98
|
+
hook = os.path.join(hooks, "pre-push")
|
|
99
|
+
if os.path.realpath(hook) == os.path.realpath(__file__) or not os.access(hook, os.X_OK):
|
|
100
|
+
return 0
|
|
101
|
+
return subprocess.run([hook, *argv], input=stdin, text=True).returncode
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> None:
|
|
105
|
+
argv = sys.argv[1:]
|
|
106
|
+
stdin = sys.stdin.read()
|
|
107
|
+
check(argv[1] if len(argv) > 1 else "", stdin.splitlines())
|
|
108
|
+
sys.exit(run_repository_hook(argv, stdin))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|
|
@@ -88,13 +88,24 @@ bash:
|
|
|
88
88
|
action: deny
|
|
89
89
|
- pattern: "gh repo delete*"
|
|
90
90
|
action: deny
|
|
91
|
-
|
|
91
|
+
# Pushes to main/master are decided by the image's pre-push hook
|
|
92
|
+
# (git-hooks/pre-push), which sees the actual refs and changed paths
|
|
93
|
+
# however the push is spelled, and allows only config.yaml's
|
|
94
|
+
# github.directPush paths. These rules keep the hook from being skipped
|
|
95
|
+
# or re-pointed; GitHub branch protection would be the server-side backstop.
|
|
96
|
+
- pattern: "*--no-verify*"
|
|
92
97
|
action: deny
|
|
93
|
-
- pattern: "
|
|
98
|
+
- pattern: "*GIT_CONFIG*"
|
|
94
99
|
action: deny
|
|
95
|
-
- pattern: "
|
|
100
|
+
- pattern: "*hooksPath*"
|
|
96
101
|
action: deny
|
|
97
|
-
- pattern: "
|
|
102
|
+
- pattern: "*hookspath*"
|
|
103
|
+
action: deny
|
|
104
|
+
- pattern: "*HOOKSPATH*"
|
|
105
|
+
action: deny
|
|
106
|
+
- pattern: "*FOUNDATION_DIRECT_PUSH*"
|
|
107
|
+
action: deny
|
|
108
|
+
- pattern: "*git-hooks*"
|
|
98
109
|
action: deny
|
|
99
110
|
# Ambient AWS (aws-broker.ts). BELT AND BRACES ONLY: the real guard is
|
|
100
111
|
# IAM — every profile fetches credentials for a role carrying AWS's
|
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-yn8x77x6.js";
|
|
12
12
|
import {
|
|
13
13
|
adminsFor,
|
|
14
14
|
instanceNames,
|
|
15
15
|
loadInstanceFile,
|
|
16
16
|
provisionsIntegration
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-k6mcv6dk.js";
|
|
18
18
|
import"../chunk-dz91avrb.js";
|
|
19
19
|
|
|
20
20
|
// bin/app.ts
|
|
@@ -1433,6 +1433,14 @@ var RawSubagents = z5.object({
|
|
|
1433
1433
|
maxRecursionDepth: z5.number().int().min(1).max(3).default(2),
|
|
1434
1434
|
effort: z5.boolean().default(true)
|
|
1435
1435
|
}).strict().default({});
|
|
1436
|
+
var GITHUB_REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
1437
|
+
var DIRECT_PUSH_PATH_RE = /^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9_.\/-]+$/;
|
|
1438
|
+
var RawGithub2 = z5.object({
|
|
1439
|
+
directPush: z5.array(z5.object({
|
|
1440
|
+
repo: z5.string().regex(GITHUB_REPO_RE, "repo must be owner/name"),
|
|
1441
|
+
paths: z5.array(z5.string().regex(DIRECT_PUSH_PATH_RE, "paths are repo-relative folders or files: no leading /, no .., no globs")).min(1)
|
|
1442
|
+
}).strict()).default([])
|
|
1443
|
+
}).strict().default({});
|
|
1436
1444
|
var RawMemory = z5.object({ ompBackend: z5.enum(["off", "local"]).default("off") }).strict().default({});
|
|
1437
1445
|
var SKILL_SOURCE_SCOPES = ["shared", "instance"];
|
|
1438
1446
|
var RawSkillSourceMetadata = {
|
|
@@ -1467,7 +1475,8 @@ var RawInstanceConfig = z5.object({
|
|
|
1467
1475
|
capabilities: RawCapabilities,
|
|
1468
1476
|
jobs: RawJobs,
|
|
1469
1477
|
subagents: RawSubagents,
|
|
1470
|
-
memory: RawMemory
|
|
1478
|
+
memory: RawMemory,
|
|
1479
|
+
github: RawGithub2
|
|
1471
1480
|
}).strict();
|
|
1472
1481
|
function crmOutreachConfigured(outreach) {
|
|
1473
1482
|
return outreach.eligibleStatuses.length > 0;
|
|
@@ -1522,6 +1531,12 @@ function parseInstanceConfig(yamlText) {
|
|
|
1522
1531
|
effort: raw.subagents.effort
|
|
1523
1532
|
},
|
|
1524
1533
|
memory: { ompBackend: raw.memory.ompBackend },
|
|
1534
|
+
github: {
|
|
1535
|
+
directPush: raw.github.directPush.map((rule) => ({
|
|
1536
|
+
repo: rule.repo,
|
|
1537
|
+
paths: [...rule.paths]
|
|
1538
|
+
}))
|
|
1539
|
+
},
|
|
1525
1540
|
capabilities: raw.capabilities,
|
|
1526
1541
|
jobs: raw.jobs
|
|
1527
1542
|
};
|
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-yn8x77x6.js";
|
|
14
14
|
import {
|
|
15
15
|
BUN_VERSION,
|
|
16
16
|
LAMBDA_ENTRY_POINTS,
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
skillsKey,
|
|
45
45
|
skipBundle,
|
|
46
46
|
verifyManifest
|
|
47
|
-
} from "../chunk-
|
|
47
|
+
} from "../chunk-k6mcv6dk.js";
|
|
48
48
|
import"../chunk-dz91avrb.js";
|
|
49
49
|
export {
|
|
50
50
|
BUILD_TIMEOUT,
|
package/package.json
CHANGED