@evident-ai/runner-cdk 0.1.1-dev.0fe02fb → 0.1.1-dev.15755a4
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 +76 -19
- package/dist/controller-lambda/handler.js +50523 -0
- package/dist/evident-scale-to-zero-construct.js +4 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -1
- package/dist/microvm/constants.d.ts +7 -0
- package/dist/microvm/constants.js +34 -0
- package/dist/microvm/construct.d.ts +87 -0
- package/dist/microvm/construct.js +253 -0
- package/dist/microvm/controller/doorbell.d.ts +73 -0
- package/dist/microvm/controller/doorbell.js +107 -0
- package/dist/microvm/controller/handle-doorbell.d.ts +27 -0
- package/dist/microvm/controller/handle-doorbell.js +480 -0
- package/dist/microvm/controller/microvm-client.d.ts +75 -0
- package/dist/microvm/controller/microvm-client.js +7 -0
- package/dist/microvm/controller/shape-catalogue.d.ts +64 -0
- package/dist/microvm/controller/shape-catalogue.js +108 -0
- package/dist/microvm/controller/throttle-retry.d.ts +11 -0
- package/dist/microvm/controller/throttle-retry.js +27 -0
- package/dist/microvm/image/stage-context.d.ts +33 -0
- package/dist/microvm/image/stage-context.js +148 -0
- package/dist/microvm/shapes.d.ts +72 -0
- package/dist/microvm/shapes.js +93 -0
- package/dist/microvm-image-context/Dockerfile +227 -0
- package/dist/microvm-image-context/hook-server.js +286 -0
- package/dist/microvm-image-context/hooks/common.sh +1359 -0
- package/dist/microvm-image-context/hooks/resume +79 -0
- package/dist/microvm-image-context/hooks/run +117 -0
- package/dist/microvm-image-context/hooks/suspend +19 -0
- package/dist/microvm-image-context/hooks/terminate +34 -0
- package/dist/waker/construct.js +1 -1
- package/package.json +15 -7
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Evident per-session MicroVM image (#558, epic #556)
|
|
2
|
+
#
|
|
3
|
+
# Same installed footprint as the ECS runner image, built for linux/arm64
|
|
4
|
+
# (Lambda MicroVMs run on Graviton only) and with ONE process: the hook server.
|
|
5
|
+
#
|
|
6
|
+
# THE BOOT SPLIT — the rule that governs everything below. AWS snapshots this
|
|
7
|
+
# image ONCE at build and every MicroVM resumes from that same snapshot, so
|
|
8
|
+
# anything unique or secret baked in here is shared by every VM launched from
|
|
9
|
+
# this image version. Therefore this image must NOT start the tunnel
|
|
10
|
+
# (`evident run`), clone the repo, or carry any credential, machine ID or
|
|
11
|
+
# generated secret. Identity arrives per-VM in the `/run` hook payload.
|
|
12
|
+
# `src/image/dockerfile.test.ts` fails the build if this regresses.
|
|
13
|
+
#
|
|
14
|
+
# No `--platform` on FROM: AWS builds this natively on Graviton. Build it
|
|
15
|
+
# locally with `docker build --platform linux/arm64`.
|
|
16
|
+
#
|
|
17
|
+
# Node major tracks `.nvmrc` / the root `engines.node` (22) so the image runs
|
|
18
|
+
# the same runtime as CI — `evident-runner/src/node-version.test.ts` fails if
|
|
19
|
+
# they drift. The Debian codename is pinned explicitly (`bookworm`, what
|
|
20
|
+
# `node:22-slim` resolves to today) so a future default bump to a newer Debian
|
|
21
|
+
# can't silently change the apt package set this image asserts on below.
|
|
22
|
+
FROM node:22-bookworm-slim
|
|
23
|
+
|
|
24
|
+
ENV PNPM_HOME="/pnpm"
|
|
25
|
+
ENV PATH="$PNPM_HOME:$PATH"
|
|
26
|
+
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
27
|
+
|
|
28
|
+
# System tooling + GitHub CLI + AWS CLI, in a single layer.
|
|
29
|
+
# - awscli (v2): required — durable-state persistence uses `aws s3`.
|
|
30
|
+
# - procps, postgresql(+contrib), direnv: required at RUNTIME so the VM is a
|
|
31
|
+
# full dev box for this repo (local Postgres on 5433 as the non-root runner,
|
|
32
|
+
# no sudo). Listed explicitly so apt keeps them.
|
|
33
|
+
# - gnupg, unzip: BUILD-ONLY (gh apt key / AWS CLI bundle); purged in this layer.
|
|
34
|
+
#
|
|
35
|
+
# Purge build-only packages BY NAME — do NOT use `apt-get autoremove`, which
|
|
36
|
+
# once cascaded into shared libs and swept `procps` out of the runner image. The
|
|
37
|
+
# binary assertion further down fails the build if any required binary is gone.
|
|
38
|
+
RUN apt-get update \
|
|
39
|
+
&& apt-get install -y --no-install-recommends \
|
|
40
|
+
ca-certificates curl git jq less procps psmisc ripgrep \
|
|
41
|
+
postgresql postgresql-contrib direnv \
|
|
42
|
+
gnupg unzip \
|
|
43
|
+
# --- GitHub CLI (`gh`) from the official apt repo ---
|
|
44
|
+
&& mkdir -p -m 755 /etc/apt/keyrings \
|
|
45
|
+
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
46
|
+
-o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
47
|
+
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
48
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
|
49
|
+
> /etc/apt/sources.list.d/github-cli.list \
|
|
50
|
+
&& apt-get update \
|
|
51
|
+
&& apt-get install -y --no-install-recommends gh \
|
|
52
|
+
# --- AWS CLI v2 (aarch64 bundle; this image is arm64 only) ---
|
|
53
|
+
&& curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o /tmp/awscliv2.zip \
|
|
54
|
+
&& unzip -q /tmp/awscliv2.zip -d /tmp \
|
|
55
|
+
&& /tmp/aws/install \
|
|
56
|
+
&& aws --version \
|
|
57
|
+
# --- Put the PostgreSQL server binaries on PATH ---
|
|
58
|
+
# On Debian initdb/pg_ctl/postgres/psql live under /usr/lib/postgresql/
|
|
59
|
+
# <ver>/bin, not on PATH; symlink them into /usr/local/bin.
|
|
60
|
+
&& ln -s /usr/lib/postgresql/*/bin/* /usr/local/bin/ 2>/dev/null || true \
|
|
61
|
+
# --- Drop the build-only packages (BY NAME, no autoremove) + caches ---
|
|
62
|
+
&& apt-get purge -y gnupg unzip \
|
|
63
|
+
&& rm -rf /tmp/aws /tmp/awscliv2.zip /var/lib/apt/lists/*
|
|
64
|
+
|
|
65
|
+
# Litestream — replicates the OpenCode SQLite session DB to S3 so history
|
|
66
|
+
# survives VM replacement. Pinned + checksum-verified: a wrong LITESTREAM_SHA256
|
|
67
|
+
# fails the build at `sha256sum -c`. The checksum is for the arm64 tarball and
|
|
68
|
+
# is NOT interchangeable with the ECS runner image's. Note the release tag keeps
|
|
69
|
+
# the `v` but the filename does not.
|
|
70
|
+
# `/run` restores opencode.db from S3 (#812 WI-2) — one bounded `litestream
|
|
71
|
+
# restore` attempt — then starts a backgrounded `litestream replicate` once
|
|
72
|
+
# opencode has opened the DB (WI-3); `/suspend`/`/resume`/`/terminate` flush,
|
|
73
|
+
# restart and stop it in turn (WI-4). Do not remove as dead code.
|
|
74
|
+
ARG LITESTREAM_VERSION=0.5.13
|
|
75
|
+
ARG LITESTREAM_SHA256=ef47997794ce8dd87a64b44622d556b3a693b135fd72e0cf47cc42ac2e979051
|
|
76
|
+
RUN curl -fsSL -o /tmp/litestream.tar.gz \
|
|
77
|
+
"https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-${LITESTREAM_VERSION}-linux-arm64.tar.gz" \
|
|
78
|
+
&& echo "${LITESTREAM_SHA256} /tmp/litestream.tar.gz" | sha256sum -c - \
|
|
79
|
+
&& tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz litestream \
|
|
80
|
+
&& chmod +x /usr/local/bin/litestream \
|
|
81
|
+
&& rm -f /tmp/litestream.tar.gz \
|
|
82
|
+
&& litestream version
|
|
83
|
+
|
|
84
|
+
# The AI agent runtime, the Evident CLI and the Claude Code CLI, installed
|
|
85
|
+
# globally and pinned via build args so image versions are reproducible.
|
|
86
|
+
# OPENCODE_VERSION is a DELIBERATE PIN, not a floating tag — see #537: a
|
|
87
|
+
# floating `opencode-ai@latest` install was implicated in unbounded runner
|
|
88
|
+
# memory growth / OOM kills. Keep it in lockstep with the ECS runner image's.
|
|
89
|
+
# EVIDENT_CLI_VERSION is DELIBERATELY the floating `dev` tag, matching the ECS
|
|
90
|
+
# runner image (infrastructure/evident-runner/src/base-image.ts). This is a
|
|
91
|
+
# development environment: it should track the head of the CLI so changes are
|
|
92
|
+
# testable without a version bump in every PR that touches a hook.
|
|
93
|
+
# RUNNER_SYNCHRONISER_VERSION floats on `dev` for the identical reason.
|
|
94
|
+
ARG OPENCODE_VERSION=1.18.3
|
|
95
|
+
ARG EVIDENT_CLI_VERSION=dev
|
|
96
|
+
ARG CLAUDE_CODE_VERSION=latest
|
|
97
|
+
ARG RUNNER_SYNCHRONISER_VERSION=dev
|
|
98
|
+
RUN npm install -g \
|
|
99
|
+
"opencode-ai@${OPENCODE_VERSION}" \
|
|
100
|
+
"@evident-ai/cli@${EVIDENT_CLI_VERSION}" \
|
|
101
|
+
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
|
|
102
|
+
"@evident-ai/runner-synchroniser@${RUNNER_SYNCHRONISER_VERSION}" \
|
|
103
|
+
&& opencode --version \
|
|
104
|
+
&& evident --version \
|
|
105
|
+
&& npm cache clean --force
|
|
106
|
+
|
|
107
|
+
# Playwright chromium + OS deps for E2E, baked at build time to avoid a slow
|
|
108
|
+
# first-boot download.
|
|
109
|
+
#
|
|
110
|
+
# CRITICAL: this version MUST match the Playwright version the repo resolves for
|
|
111
|
+
# `@playwright/test` (pnpm-lock.yaml → 1.58.0). The browser build is coupled to
|
|
112
|
+
# the package version; if it drifts, a boot-time `playwright install`
|
|
113
|
+
# re-downloads a different chromium. Bump in lockstep with @playwright/test.
|
|
114
|
+
#
|
|
115
|
+
# Browsers go to a SHARED PLAYWRIGHT_BROWSERS_PATH readable by the runtime
|
|
116
|
+
# runner user (uid 10001), not root's ~/.cache.
|
|
117
|
+
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright
|
|
118
|
+
RUN npx playwright@1.58.0 install --with-deps chromium \
|
|
119
|
+
&& chown -R 10001:10001 /opt/ms-playwright \
|
|
120
|
+
# Assert the browsers landed in the shared path (guard against a silent failure).
|
|
121
|
+
&& test -n "$(ls -A /opt/ms-playwright 2>/dev/null)" \
|
|
122
|
+
|| { echo "FATAL: Playwright browsers dir /opt/ms-playwright missing or empty after install" >&2; exit 1; }
|
|
123
|
+
|
|
124
|
+
# Assert every REQUIRED runtime binary survived every layer above (fail loud).
|
|
125
|
+
# Placed after the last install so it covers the apt purge, the litestream
|
|
126
|
+
# extraction and the global npm installs in one pass.
|
|
127
|
+
RUN for bin in ps pgrep pkill curl aws gh git jq rg psql pg_ctl initdb direnv litestream runner-synchroniser opencode evident timeout; do \
|
|
128
|
+
command -v "$bin" >/dev/null 2>&1 || { echo "FATAL: required binary '$bin' missing from image" >&2; exit 1; }; \
|
|
129
|
+
done \
|
|
130
|
+
&& curl -fsS -o /dev/null https://cli.github.com \
|
|
131
|
+
|| { echo "FATAL: curl TLS smoke test failed (broken TLS libs)" >&2; exit 1; }
|
|
132
|
+
|
|
133
|
+
# Non-root user that owns the workspace. uid/gid pinned to 10001 for stable file
|
|
134
|
+
# ownership (opencode.db, ~/.claude/.credentials.json) across rebuilds.
|
|
135
|
+
RUN groupadd --system --gid 10001 runner \
|
|
136
|
+
&& useradd --system --uid 10001 --gid 10001 \
|
|
137
|
+
--create-home --home-dir /home/runner runner
|
|
138
|
+
|
|
139
|
+
# The hook server: the ONLY process this image starts, and it starts nothing
|
|
140
|
+
# itself until AWS calls a hook. It is an esbuild bundle of
|
|
141
|
+
# src/microvm/image/hook-server.ts, placed next to this Dockerfile by
|
|
142
|
+
# `stageMicrovmImageContext()` — which is why this file is not buildable
|
|
143
|
+
# straight from the source tree.
|
|
144
|
+
COPY hook-server.js /usr/local/lib/evident/hook-server.js
|
|
145
|
+
|
|
146
|
+
# One executable per lifecycle phase. There is deliberately no `ready` and no
|
|
147
|
+
# `validate` script: the snapshot is shared by every VM started from this image
|
|
148
|
+
# version, so nothing per-VM may run before /run — which leaves those two phases
|
|
149
|
+
# with nothing to do, and the runtime answers them 200 without running anything.
|
|
150
|
+
COPY hooks /etc/evident/hooks
|
|
151
|
+
|
|
152
|
+
# Workspace: the repository the agent works on, baked in. A resumed VM must be
|
|
153
|
+
# useful in milliseconds, and a clone + install costs minutes — so both happen
|
|
154
|
+
# here, and the image is deliberately large in exchange.
|
|
155
|
+
#
|
|
156
|
+
# This does not weaken the boot split: source, git history and node_modules are
|
|
157
|
+
# neither secret nor per-VM-unique, so sharing them across every VM from this
|
|
158
|
+
# snapshot is exactly right. No credential is involved either — the repo travels
|
|
159
|
+
# in the build context (cloned by `stageMicrovmImageContext()` from a local
|
|
160
|
+
# checkout), not through a `git clone` needing a token. `origin` is the plain
|
|
161
|
+
# HTTPS GitHub URL; the credential that makes it fetchable and pushable arrives
|
|
162
|
+
# per-VM, so the agent is what brings this checkout up to date.
|
|
163
|
+
ENV WORKSPACE=/workspace
|
|
164
|
+
ENV HOME=/home/runner
|
|
165
|
+
# Runner-owned PGDATA parent for the local dev Postgres cluster (initdb +
|
|
166
|
+
# pg_ctl at boot, no sudo).
|
|
167
|
+
#
|
|
168
|
+
# The machine-id files are created EMPTY and runner-owned so that /run can
|
|
169
|
+
# actually rewrite them: the hooks run as uid 10001, and neither `/etc` nor
|
|
170
|
+
# `/var/lib/dbus` is writable by it, so without this the regeneration is a
|
|
171
|
+
# permanent no-op and every VM from this snapshot shares one machine id. Empty
|
|
172
|
+
# is the correct unset state — it is the absence of an identity, so nothing
|
|
173
|
+
# per-VM-unique enters the shared snapshot.
|
|
174
|
+
RUN mkdir -p /var/lib/runner-pg /var/lib/dbus \
|
|
175
|
+
&& install -o runner -g runner -m 0644 /dev/null /etc/machine-id \
|
|
176
|
+
&& install -o runner -g runner -m 0644 /dev/null /var/lib/dbus/machine-id \
|
|
177
|
+
&& chown -R runner:runner /var/lib/runner-pg /home/runner
|
|
178
|
+
COPY --chown=10001:10001 repo ${WORKSPACE}
|
|
179
|
+
WORKDIR ${WORKSPACE}
|
|
180
|
+
USER runner
|
|
181
|
+
|
|
182
|
+
# The staged context deliberately ships no `.git/index` (it is not byte-stable,
|
|
183
|
+
# so shipping it would re-roll the image version on every deploy) — without this
|
|
184
|
+
# reset the agent's workspace reports ~1400 files as staged deletions. Runs as
|
|
185
|
+
# `runner` (not root) so the rebuilt index is runner-owned and writable later.
|
|
186
|
+
RUN git reset --quiet
|
|
187
|
+
|
|
188
|
+
# Dependencies and the shared builds everything else depends on. As the runner
|
|
189
|
+
# user, so node_modules is writable by the agent's own later installs.
|
|
190
|
+
#
|
|
191
|
+
# ONE layer on purpose: pnpm hard-links node_modules into its content-addressed
|
|
192
|
+
# store (which it puts at ${WORKSPACE}/.pnpm-store, gitignored), and hard links
|
|
193
|
+
# only survive within a single layer — split in two and the image carries both
|
|
194
|
+
# copies. `--frozen-lockfile` because the committed lockfile is the whole reason
|
|
195
|
+
# this is bakeable at all: installing against anything else would bake a
|
|
196
|
+
# dependency set nobody committed.
|
|
197
|
+
#
|
|
198
|
+
# Guarded on a committed `pnpm-lock.yaml` so `microvm:repositoryPath` can point
|
|
199
|
+
# at a repository that is not a pnpm workspace at all: it is baked uninstalled
|
|
200
|
+
# instead of failing the build, and the agent installs on first use.
|
|
201
|
+
#
|
|
202
|
+
# Nothing environment-specific is built: `vite build` inlines VITE_* at build
|
|
203
|
+
# time and the migrations need a running Postgres, so the app builds and the dev
|
|
204
|
+
# database stay per-VM (see entrypoint.sh's pre-warm in evident-runner).
|
|
205
|
+
RUN if [ -f pnpm-lock.yaml ]; then \
|
|
206
|
+
pnpm install --frozen-lockfile \
|
|
207
|
+
&& pnpm run build --filter='./packages/*' \
|
|
208
|
+
--filter='./aws/runner-cdk' \
|
|
209
|
+
--filter='./aws/lambda-microvm-cdk' \
|
|
210
|
+
--filter='./aws/lambda-microvm-runtime'; \
|
|
211
|
+
else \
|
|
212
|
+
echo "[workspace-prep] no pnpm-lock.yaml in the baked repository — skipping the dependency install and the workspace build; the agent installs on first use."; \
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# Data dir for the local dev Postgres cluster; listens on 5433 to match this
|
|
216
|
+
# repo's .envrc DSN postgres://postgres:postgres@localhost:5433/evident.
|
|
217
|
+
ENV PGDATA=/var/lib/runner-pg/data
|
|
218
|
+
# Port AWS calls the MicroVM hooks on.
|
|
219
|
+
ENV HOOKS_PORT=8080
|
|
220
|
+
# OpenCode loopback port the CLI tunnels to (matches `evident run` default).
|
|
221
|
+
ENV OPENCODE_PORT=4096
|
|
222
|
+
|
|
223
|
+
EXPOSE 8080
|
|
224
|
+
|
|
225
|
+
# No ENTRYPOINT shell: the hook server is started directly, and it must be the
|
|
226
|
+
# only thing running until AWS calls /run.
|
|
227
|
+
CMD ["node", "/usr/local/lib/evident/hook-server.js"]
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
// ../lambda-microvm-runtime/dist/runtime.js
|
|
29
|
+
var require_runtime = __commonJS({
|
|
30
|
+
"../lambda-microvm-runtime/dist/runtime.js"(exports2) {
|
|
31
|
+
"use strict";
|
|
32
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
33
|
+
exports2.HOOK_SIGTERM_SECONDS = exports2.HOOK_PATHS = exports2.HOOK_NAMES = exports2.HOOK_PATH_PREFIX = void 0;
|
|
34
|
+
exports2.parseRunEnvelope = parseRunEnvelope;
|
|
35
|
+
exports2.createMicrovmRuntime = createMicrovmRuntime;
|
|
36
|
+
var node_http_1 = require("node:http");
|
|
37
|
+
exports2.HOOK_PATH_PREFIX = "/aws/lambda-microvms/runtime/v1";
|
|
38
|
+
exports2.HOOK_NAMES = ["ready", "validate", "run", "resume", "suspend", "terminate"];
|
|
39
|
+
exports2.HOOK_PATHS = Object.fromEntries(exports2.HOOK_NAMES.map((name) => [name, `${exports2.HOOK_PATH_PREFIX}/${name}`]));
|
|
40
|
+
var RUN_HOOK_PAYLOAD_MAX_BYTES = 16384;
|
|
41
|
+
var MAX_BODY_BYTES = RUN_HOOK_PAYLOAD_MAX_BYTES * 8;
|
|
42
|
+
exports2.HOOK_SIGTERM_SECONDS = 55;
|
|
43
|
+
var DEFAULT_TIMEOUT_SECONDS = exports2.HOOK_SIGTERM_SECONDS;
|
|
44
|
+
var DEFAULT_KILL_GRACE_SECONDS = 10;
|
|
45
|
+
function readBody(request) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const chunks = [];
|
|
48
|
+
let size = 0;
|
|
49
|
+
let oversize = false;
|
|
50
|
+
request.on("data", (chunk) => {
|
|
51
|
+
if (oversize) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
size += chunk.length;
|
|
55
|
+
if (size > MAX_BODY_BYTES) {
|
|
56
|
+
oversize = true;
|
|
57
|
+
resolve(null);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
chunks.push(chunk);
|
|
61
|
+
});
|
|
62
|
+
request.on("end", () => {
|
|
63
|
+
if (!oversize) {
|
|
64
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
request.on("error", reject);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function parseRunEnvelope(rawBody) {
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(rawBody);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error("run hook body is not valid JSON");
|
|
76
|
+
}
|
|
77
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
78
|
+
throw new Error("run hook body is not a JSON object");
|
|
79
|
+
}
|
|
80
|
+
const envelope = parsed;
|
|
81
|
+
const microvmId = envelope.microvmId;
|
|
82
|
+
const runHookPayload = envelope.runHookPayload;
|
|
83
|
+
if (typeof microvmId !== "string" || microvmId === "") {
|
|
84
|
+
throw new Error("run hook body is missing microvmId");
|
|
85
|
+
}
|
|
86
|
+
if (typeof runHookPayload !== "string") {
|
|
87
|
+
throw new Error("run hook body is missing runHookPayload");
|
|
88
|
+
}
|
|
89
|
+
return { microvmId, runHookPayload };
|
|
90
|
+
}
|
|
91
|
+
function createMicrovmRuntime({ hooksDir, spawn, hasScript, timeoutSeconds = DEFAULT_TIMEOUT_SECONDS, killGraceSeconds = DEFAULT_KILL_GRACE_SECONDS }) {
|
|
92
|
+
function runScript(hook, script, env, payload) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const child = spawn(script, { env });
|
|
95
|
+
let settled = false;
|
|
96
|
+
let escalation;
|
|
97
|
+
const settle = (outcome) => {
|
|
98
|
+
if (settled) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
settled = true;
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
resolve(outcome);
|
|
104
|
+
};
|
|
105
|
+
const timer = setTimeout(() => {
|
|
106
|
+
console.error(`[hook] hook=${hook} status=timeout limit_seconds=${timeoutSeconds}`);
|
|
107
|
+
child.kill("SIGTERM");
|
|
108
|
+
escalation = setTimeout(() => {
|
|
109
|
+
console.error(`[hook] hook=${hook} status=kill_escalated grace_seconds=${killGraceSeconds}`);
|
|
110
|
+
child.kill("SIGKILL");
|
|
111
|
+
}, killGraceSeconds * 1e3);
|
|
112
|
+
escalation.unref();
|
|
113
|
+
settle({ statusCode: 500, message: "hook timed out" });
|
|
114
|
+
}, timeoutSeconds * 1e3);
|
|
115
|
+
timer.unref();
|
|
116
|
+
child.once("error", (error) => {
|
|
117
|
+
console.error(`[hook] hook=${hook} status=spawn_failed error=${error.message}`);
|
|
118
|
+
settle({ statusCode: 500, message: "hook could not be started" });
|
|
119
|
+
});
|
|
120
|
+
child.once("exit", (code) => {
|
|
121
|
+
clearTimeout(escalation);
|
|
122
|
+
if (code === 0) {
|
|
123
|
+
settle({ statusCode: 200, message: hook });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
console.error(`[hook] hook=${hook} status=failed exit_code=${code}`);
|
|
127
|
+
settle({ statusCode: 500, message: "hook failed" });
|
|
128
|
+
});
|
|
129
|
+
if (payload !== void 0 && child.stdin) {
|
|
130
|
+
child.stdin.write(payload);
|
|
131
|
+
child.stdin.end();
|
|
132
|
+
} else {
|
|
133
|
+
child.stdin?.end();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async function dispatch(hook, rawBody) {
|
|
138
|
+
const env = { ...process.env };
|
|
139
|
+
let payload;
|
|
140
|
+
if (hook === "run") {
|
|
141
|
+
const envelope = parseRunEnvelope(rawBody);
|
|
142
|
+
env.MICROVM_ID = envelope.microvmId;
|
|
143
|
+
payload = envelope.runHookPayload;
|
|
144
|
+
}
|
|
145
|
+
const script = `${hooksDir}/${hook}`;
|
|
146
|
+
if (!hasScript(script)) {
|
|
147
|
+
return { statusCode: 200, message: `${hook} (no script)` };
|
|
148
|
+
}
|
|
149
|
+
const outcome = await runScript(hook, script, env, payload);
|
|
150
|
+
if (hook === "terminate" && outcome.statusCode !== 200) {
|
|
151
|
+
console.error("[hook] hook=terminate status=reported_ok_despite_failure");
|
|
152
|
+
return { statusCode: 200, message: "terminate (best effort)" };
|
|
153
|
+
}
|
|
154
|
+
return outcome;
|
|
155
|
+
}
|
|
156
|
+
return (0, node_http_1.createServer)((request, response) => {
|
|
157
|
+
const send = (statusCode, message) => {
|
|
158
|
+
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
159
|
+
response.end(JSON.stringify({ message }));
|
|
160
|
+
};
|
|
161
|
+
const path = (request.url ?? "").split("?")[0];
|
|
162
|
+
const hook = exports2.HOOK_NAMES.find((name) => exports2.HOOK_PATHS[name] === path);
|
|
163
|
+
if (!hook) {
|
|
164
|
+
request.resume();
|
|
165
|
+
send(404, "unknown hook path");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (request.method !== "POST") {
|
|
169
|
+
request.resume();
|
|
170
|
+
send(405, "hook requires POST");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
void readBody(request).then(async (body) => {
|
|
174
|
+
if (body === null) {
|
|
175
|
+
send(413, "hook body too large");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const outcome = await dispatch(hook, body);
|
|
180
|
+
send(outcome.statusCode, outcome.message);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
send(400, error instanceof Error ? error.message : "bad request");
|
|
183
|
+
}
|
|
184
|
+
}).catch((error) => {
|
|
185
|
+
console.error(`[hook] hook=${hook} status=unhandled_error error=${error instanceof Error ? error.message : String(error)}`);
|
|
186
|
+
send(500, "hook failed");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ../lambda-microvm-runtime/dist/node-runtime.js
|
|
194
|
+
var require_node_runtime = __commonJS({
|
|
195
|
+
"../lambda-microvm-runtime/dist/node-runtime.js"(exports2) {
|
|
196
|
+
"use strict";
|
|
197
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
198
|
+
exports2.createNodeRuntime = createNodeRuntime2;
|
|
199
|
+
var node_child_process_1 = require("node:child_process");
|
|
200
|
+
var node_fs_1 = require("node:fs");
|
|
201
|
+
var runtime_1 = require_runtime();
|
|
202
|
+
function createNodeRuntime2({ hooksDir, timeoutSeconds, killGraceSeconds }) {
|
|
203
|
+
return (0, runtime_1.createMicrovmRuntime)({
|
|
204
|
+
hooksDir,
|
|
205
|
+
timeoutSeconds,
|
|
206
|
+
killGraceSeconds,
|
|
207
|
+
hasScript: (script) => {
|
|
208
|
+
try {
|
|
209
|
+
(0, node_fs_1.accessSync)(script, node_fs_1.constants.X_OK);
|
|
210
|
+
return true;
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
spawn: (script, options) => {
|
|
216
|
+
const child = (0, node_child_process_1.spawn)(script, {
|
|
217
|
+
env: options.env,
|
|
218
|
+
// stdin is a pipe so the run payload never has to travel in argv.
|
|
219
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
220
|
+
// Its own process group, so a signal can reach the tree the script
|
|
221
|
+
// built — a hook shells out, and stopping only the shell orphans the
|
|
222
|
+
// synchroniser or probe it was waiting on.
|
|
223
|
+
detached: true
|
|
224
|
+
});
|
|
225
|
+
child.kill = (signal) => {
|
|
226
|
+
try {
|
|
227
|
+
process.kill(-child.pid, signal);
|
|
228
|
+
return true;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error.code !== "ESRCH") {
|
|
231
|
+
console.error(`[hook] script=${script} status=signal_failed signal=${String(signal)} error=${error instanceof Error ? error.message : String(error)}`);
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
child.stdin?.on("error", (error) => {
|
|
237
|
+
console.error(`[hook] script=${script} status=stdin_write_failed error=${error.message}`);
|
|
238
|
+
});
|
|
239
|
+
return child;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ../lambda-microvm-runtime/dist/index.js
|
|
247
|
+
var require_dist = __commonJS({
|
|
248
|
+
"../lambda-microvm-runtime/dist/index.js"(exports2) {
|
|
249
|
+
"use strict";
|
|
250
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
251
|
+
exports2.createNodeRuntime = exports2.HOOK_SIGTERM_SECONDS = exports2.HOOK_PATH_PREFIX = exports2.HOOK_PATHS = exports2.HOOK_NAMES = exports2.parseRunEnvelope = exports2.createMicrovmRuntime = void 0;
|
|
252
|
+
var runtime_1 = require_runtime();
|
|
253
|
+
Object.defineProperty(exports2, "createMicrovmRuntime", { enumerable: true, get: function() {
|
|
254
|
+
return runtime_1.createMicrovmRuntime;
|
|
255
|
+
} });
|
|
256
|
+
Object.defineProperty(exports2, "parseRunEnvelope", { enumerable: true, get: function() {
|
|
257
|
+
return runtime_1.parseRunEnvelope;
|
|
258
|
+
} });
|
|
259
|
+
Object.defineProperty(exports2, "HOOK_NAMES", { enumerable: true, get: function() {
|
|
260
|
+
return runtime_1.HOOK_NAMES;
|
|
261
|
+
} });
|
|
262
|
+
Object.defineProperty(exports2, "HOOK_PATHS", { enumerable: true, get: function() {
|
|
263
|
+
return runtime_1.HOOK_PATHS;
|
|
264
|
+
} });
|
|
265
|
+
Object.defineProperty(exports2, "HOOK_PATH_PREFIX", { enumerable: true, get: function() {
|
|
266
|
+
return runtime_1.HOOK_PATH_PREFIX;
|
|
267
|
+
} });
|
|
268
|
+
Object.defineProperty(exports2, "HOOK_SIGTERM_SECONDS", { enumerable: true, get: function() {
|
|
269
|
+
return runtime_1.HOOK_SIGTERM_SECONDS;
|
|
270
|
+
} });
|
|
271
|
+
var node_runtime_1 = require_node_runtime();
|
|
272
|
+
Object.defineProperty(exports2, "createNodeRuntime", { enumerable: true, get: function() {
|
|
273
|
+
return node_runtime_1.createNodeRuntime;
|
|
274
|
+
} });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ../../runner/docker-images/microvm/hook-server.ts
|
|
279
|
+
var import_lambda_microvm_runtime = __toESM(require_dist());
|
|
280
|
+
var HOOKS_DIR = "/etc/evident/hooks";
|
|
281
|
+
var HOOKS_PORT = 8080;
|
|
282
|
+
var server = (0, import_lambda_microvm_runtime.createNodeRuntime)({ hooksDir: HOOKS_DIR });
|
|
283
|
+
var port = Number(process.env.HOOKS_PORT ?? HOOKS_PORT);
|
|
284
|
+
server.listen(port, "0.0.0.0", () => {
|
|
285
|
+
console.log(`[hook] server listening on 0.0.0.0:${port}`);
|
|
286
|
+
});
|