@evident-ai/runner-cdk 0.1.1-dev.0fe02fb → 0.1.1-dev.3e9ed80

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 (30) hide show
  1. package/README.md +64 -16
  2. package/dist/controller-lambda/handler.js +50523 -0
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.js +14 -1
  5. package/dist/microvm/constants.d.ts +7 -0
  6. package/dist/microvm/constants.js +35 -0
  7. package/dist/microvm/construct.d.ts +73 -0
  8. package/dist/microvm/construct.js +245 -0
  9. package/dist/microvm/controller/doorbell.d.ts +73 -0
  10. package/dist/microvm/controller/doorbell.js +107 -0
  11. package/dist/microvm/controller/handle-doorbell.d.ts +27 -0
  12. package/dist/microvm/controller/handle-doorbell.js +480 -0
  13. package/dist/microvm/controller/microvm-client.d.ts +75 -0
  14. package/dist/microvm/controller/microvm-client.js +7 -0
  15. package/dist/microvm/controller/shape-catalogue.d.ts +64 -0
  16. package/dist/microvm/controller/shape-catalogue.js +108 -0
  17. package/dist/microvm/controller/throttle-retry.d.ts +11 -0
  18. package/dist/microvm/controller/throttle-retry.js +27 -0
  19. package/dist/microvm/image/stage-context.d.ts +33 -0
  20. package/dist/microvm/image/stage-context.js +148 -0
  21. package/dist/microvm/shapes.d.ts +72 -0
  22. package/dist/microvm/shapes.js +93 -0
  23. package/dist/microvm-image-context/Dockerfile +224 -0
  24. package/dist/microvm-image-context/hook-server.js +290 -0
  25. package/dist/microvm-image-context/hooks/common.sh +1255 -0
  26. package/dist/microvm-image-context/hooks/resume +79 -0
  27. package/dist/microvm-image-context/hooks/run +111 -0
  28. package/dist/microvm-image-context/hooks/suspend +19 -0
  29. package/dist/microvm-image-context/hooks/terminate +34 -0
  30. package/package.json +10 -4
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Re-dial with the identity /run left behind. There is no step that could fetch
4
+ # a fresh runner key, so the one from /run is what resumes. Also restarts the
5
+ # session-DB replicator /suspend stopped before the snapshot (#812 WI-4):
6
+ # litestream does not survive a suspend/resume freeze on this design (Q2) —
7
+ # /suspend stops it and /resume starts a fresh one, the same pattern already
8
+ # proven for the tunnel.
9
+ set -euo pipefail
10
+
11
+ # shellcheck source=./common.sh
12
+ source "$(dirname "$0")/common.sh"
13
+
14
+ # Unlike /run's cleanup, this stops ONLY the tunnel: opencode is inside the
15
+ # snapshot and resumes with it (README.md), already serving from before this
16
+ # hook ever ran, and killing it here would take down a session /run started.
17
+ # The only failure path today is the missing-context-file check below, before
18
+ # start_tunnel ever runs, where stop_tunnel is a cheap no-op. Kept for the same
19
+ # reason /run's trap is: a future failure path introduced between start_tunnel
20
+ # and resume_succeeded=true must not orphan the tunnel it just spawned.
21
+ resume_succeeded=false
22
+ cleanup() {
23
+ if [ "${resume_succeeded}" = true ]; then
24
+ return 0
25
+ fi
26
+ warn "resume failed; stopping the tunnel it started"
27
+ stop_tunnel || warn "stop_tunnel failed while cleaning up"
28
+ }
29
+ trap cleanup EXIT
30
+
31
+ if [ ! -s "${CONTEXT_FILE}" ]; then
32
+ warn "resume before run: no context at ${CONTEXT_FILE}"
33
+ exit 1
34
+ fi
35
+
36
+ {
37
+ read -r api_url
38
+ read -r tunnel_url
39
+ read -r runner_key
40
+ } <"${CONTEXT_FILE}"
41
+
42
+ # Tolerant, never fatal: a resume that fails costs the user their whole
43
+ # session, so a broken durable-state config degrades to "no session-DB
44
+ # restart" rather than a failed resume (the same Q3 reasoning /run's
45
+ # session-DB step follows). start_litestream's own guards (disabled/marker/
46
+ # already-running) handle the rest — this needs no logic of its own.
47
+ load_state_config || warn "could not resolve durable-state config; the session DB will not resume replicating"
48
+ start_litestream
49
+
50
+ # Diagnostic-only, unlike /run's gate: a failed resume costs the user their
51
+ # whole session, so this never exits — it only converts a silent "resumed but
52
+ # the key is dead" into a named RUNNER-KEY-* cause in the log, whatever
53
+ # check_runner_key returns.
54
+ #
55
+ # And BACKGROUNDED for exactly that reason, following prewarm_litestream's
56
+ # pattern in common.sh: this is a network round trip bounded by the CLI's
57
+ # STATUS_TIMEOUT_MS (10 s), and nothing here consumes its answer. In the
58
+ # foreground it would sit between reading the context and `start_tunnel`,
59
+ # delaying the dial that IS this hook's job — by the most on exactly the
60
+ # unreachable API where the answer is worthless — against a resume this image
61
+ # measures in ~2 s (see the waker note in common.sh).
62
+ #
63
+ # Safe to leave running past the hook's own exit: the runtime waits on the
64
+ # script's `'exit'`, not `'close'` (packages/lambda-microvm-runtime/src/
65
+ # runtime.ts), so a lingering child never delays the HTTP response. Its stdio
66
+ # is deliberately NOT redirected — that inherited fd is how the RUNNER-KEY-*
67
+ # line reaches CloudWatch, which is the whole point of running it at all.
68
+ #
69
+ # The subshell is explicit rather than relying on `&` binding the whole AND-OR
70
+ # list, so a later edit cannot accidentally foreground the check again.
71
+ ( check_runner_key "${runner_key}" "${api_url}" || true ) &
72
+
73
+ # Not waited on: the CLI backs off and keeps retrying its own dial with no
74
+ # attempt cap (apps/cli/src/lib/tunnel/connection.ts), and a control-plane
75
+ # reclaim pass (the api-worker lifecycle cron) is what decides whether a
76
+ # runner that never reconnects gets suspended.
77
+ start_tunnel "${runner_key}" "${api_url}" "${tunnel_url}"
78
+
79
+ resume_succeeded=true
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # The per-VM phase. EVERY per-VM and secret-bearing action happens here and
4
+ # nowhere earlier: the image snapshot is shared by every VM started from this
5
+ # image version, so anything done before /run would be identical across all of
6
+ # them — including, fatally, a runner identity.
7
+ set -euo pipefail
8
+
9
+ # shellcheck source=./common.sh
10
+ source "$(dirname "$0")/common.sh"
11
+
12
+ # Anything this hook started must not outlive a failure: the next /run would
13
+ # find a stale opencode holding the port. On EXIT rather than ERR because an
14
+ # explicit `exit 1` — which is how every check below rejects — does NOT fire an
15
+ # ERR trap, and those are exactly the paths that have opencode running already.
16
+ # This also runs when the runtime signals an overrun hook, which it does with
17
+ # SIGTERM for exactly that reason: opencode and the tunnel are in a session of
18
+ # their own, so this trap is the only thing that can still reach them.
19
+ run_succeeded=false
20
+ cleanup() {
21
+ if [ "${run_succeeded}" = true ]; then
22
+ return 0
23
+ fi
24
+ warn "run failed; stopping what it started"
25
+ # Each stop tolerates its own failure: `set -e` aborts a trap function at the
26
+ # first non-zero command, which would skip the stop below it — precisely the
27
+ # orphan this trap exists to prevent.
28
+ stop_tunnel || warn "stop_tunnel failed while cleaning up"
29
+ stop_opencode || warn "stop_opencode failed while cleaning up"
30
+ kill_litestream || warn "kill_litestream failed while cleaning up"
31
+ }
32
+ trap cleanup EXIT
33
+
34
+ # The payload arrives on stdin, never argv.
35
+ payload="$(cat)"
36
+
37
+ # Never interpolate the payload into the message — it carries the runner key.
38
+ if ! payload_is_complete <<<"${payload}"; then
39
+ warn "run payload is missing runner_key, endpoints or state_prefix; refusing to dial"
40
+ exit 1
41
+ fi
42
+
43
+ # `@sh` quotes each value for the shell, so a hostile payload cannot inject.
44
+ eval "$(jq -er '@sh "runner_key=\(.runner_key) api_url=\(.endpoints.api) tunnel_url=\(.endpoints.tunnel) state_prefix=\(.state_prefix)"' <<<"${payload}")"
45
+ unset payload
46
+
47
+ # 1 — replace the identity the shared snapshot baked in.
48
+ regenerate_machine_id
49
+
50
+ # 2 — where this runner's credentials live. Recorded before the restore that
51
+ # reads it, and left behind for the hooks that flush back to the same prefix.
52
+ printf '%s\n' "${state_prefix}" >"${STATE_PREFIX_FILE}"
53
+
54
+ # 3 — start reading the litestream binary NOW so that read overlaps step 4,
55
+ # which needs a different binary (node), instead of landing inside step 5, which
56
+ # start_opencode has to wait for. Backgrounded and never waited on.
57
+ prewarm_litestream
58
+
59
+ # 4 — credential stores, restored before anything that reads them starts. Fatal
60
+ # only if persistence itself is unavailable (an unset bucket/prefix, or a broken
61
+ # synchroniser bundle) — a VM with no model credentials yet still boots.
62
+ restore_credentials
63
+
64
+ # 5 — does the runner key in this payload actually authenticate against
65
+ # Evident? Checked here, before opencode or the tunnel spend any of this boot's
66
+ # SIGTERM budget on a key that cannot work, and before anything has started
67
+ # that cleanup would need to tear down. Fatal ONLY on contrary evidence
68
+ # (check_runner_key's own contract, common.sh): a key the API actively rejects
69
+ # cannot work regardless, and failing here costs ~5s against the ~10 minutes a
70
+ # runner that can never connect would otherwise burn before the lifecycle cron
71
+ # reclaims it. Absent evidence (unreachable, no parseable JSON) is not fatal —
72
+ # it warns and this VM still boots.
73
+ check_runner_key "${runner_key}" "${api_url}" || exit 1
74
+
75
+ # 6 — the OpenCode session DB (#812 WI-2), restored before opencode opens it —
76
+ # the only place this can happen. Bare, like restore_credentials above:
77
+ # restore_session_db never returns non-zero (Q3 — nothing about the session
78
+ # DB may ever fail /run), so there is deliberately no `||` here to catch.
79
+ restore_session_db
80
+ log "session DB restore done ${SECONDS}s into the hook"
81
+
82
+ # 7 — opencode. Started here, not at build time: a warm process in the shared
83
+ # snapshot would carry its installation id and database into every VM. Not
84
+ # waited on: a slow opencode boot is not a reason to fail /run (the tunnel CLI
85
+ # auto-starts opencode when it finds none healthy,
86
+ # apps/cli/src/commands/ensure-opencode.ts, and the api-worker lifecycle cron
87
+ # reclaims a runner that never comes online).
88
+ start_opencode
89
+
90
+ # 8 — begin replicating the session DB (#812 WI-3), now that opencode has
91
+ # opened it and before any work can arrive over the tunnel. Bare, like
92
+ # restore_session_db above: start_litestream never returns non-zero (every
93
+ # guard inside it is its own `return 0`), so there is nothing here for
94
+ # `set -e` to abort on.
95
+ start_litestream
96
+
97
+ # 9 — the first per-VM identity on the wire. The subshell's umask makes the file
98
+ # unreadable to anyone else from the moment it exists, before the key is in it.
99
+ (
100
+ umask 077
101
+ printf '%s\n%s\n%s\n' "${api_url}" "${tunnel_url}" "${runner_key}" \
102
+ >"${CONTEXT_FILE}"
103
+ )
104
+
105
+ # Not waited on: the CLI backs off and keeps retrying its own dial with no
106
+ # attempt cap (apps/cli/src/lib/tunnel/connection.ts), and a control-plane
107
+ # reclaim pass (the api-worker lifecycle cron) is what decides whether a
108
+ # runner that never connects gets suspended.
109
+ start_tunnel "${runner_key}" "${api_url}" "${tunnel_url}"
110
+
111
+ run_succeeded=true
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Flush anything durable, then drop the tunnel so the relay stops routing to a
4
+ # VM that is about to freeze. opencode stays up: it is inside the snapshot and
5
+ # resumes with it.
6
+ set -euo pipefail
7
+
8
+ # shellcheck source=./common.sh
9
+ source "$(dirname "$0")/common.sh"
10
+
11
+ sync_credentials
12
+ stop_tunnel
13
+
14
+ # opencode is deliberately NOT stopped here (#812 WI-4) — it is inside the
15
+ # snapshot and must resume with it — so a turn still in flight may not be
16
+ # fully flushed by this call. Acceptable: the VM resumes with the local DB
17
+ # intact, and this flush exists for the TERMINATED-while-suspended case,
18
+ # where a frozen VM would otherwise flush nothing at all.
19
+ flush_session_db
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Best-effort teardown. The runtime reports 200 whatever happens here — the VM
4
+ # is going away either way — so each step is allowed to fail on its own without
5
+ # skipping the ones after it.
6
+ set -uo pipefail
7
+
8
+ # shellcheck source=./common.sh
9
+ source "$(dirname "$0")/common.sh"
10
+
11
+ sync_credentials
12
+ stop_tunnel
13
+
14
+ # Writers stopped BEFORE litestream's final sync (#812 WI-4, the
15
+ # ordered-shutdown invariant, plan §6): otherwise litestream could snapshot
16
+ # while opencode is still writing its WAL and the last session writes would
17
+ # be missing from S3. stop_opencode_and_wait, not /run cleanup's cheap
18
+ # stop_opencode — /terminate can afford the real drain, the VM is being torn
19
+ # down either way.
20
+ stop_opencode_and_wait
21
+ flush_session_db
22
+
23
+ # The key must not outlive the VM's last useful moment. The no-replicate
24
+ # marker goes alongside it: no state that a later /resume could trust should
25
+ # outlive the VM's last useful moment either.
26
+ #
27
+ # The tunnel-ready marker used to be removed here too. It is gone entirely
28
+ # since #1172 — and naming an unset variable here is not a harmless leftover:
29
+ # this hook runs under `set -u` (above), so an unbound one aborts the script
30
+ # AT THIS LINE, leaving the runner key sitting in CONTEXT_FILE on a VM that is
31
+ # being torn down. That is the one thing this line exists to prevent.
32
+ rm -f "${CONTEXT_FILE}" "${SESSION_DB_NO_REPLICATE_MARKER}"
33
+
34
+ exit 0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@evident-ai/runner-cdk",
3
- "version": "0.1.1-dev.0fe02fb",
4
- "description": "Reusable CDK construct for a single scale-to-zero Evident agent runner on Fargate (task + service + per-agent self-stop role + waker Lambda). Instantiate once per agent from your own stack.",
3
+ "version": "0.1.1-dev.3e9ed80",
4
+ "description": "Reusable CDK constructs for an Evident agent runner: a single scale-to-zero Fargate runner (task + service + per-agent self-stop role + waker Lambda), or a per-session AWS Lambda MicroVM that boots on demand and suspends between messages. Instantiate once per agent from your own stack.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
@@ -14,19 +14,25 @@
14
14
  "dist"
15
15
  ],
16
16
  "scripts": {
17
- "build": "ts-node scripts/build.ts",
17
+ "build": "pnpm run build-bundled-deps && ts-node scripts/build.ts",
18
+ "//build-bundled-deps": "scripts/build.ts esbuild-bundles src/microvm/image/hook-server.ts, which imports @evident-ai/lambda-microvm-runtime by its built `main`. Chained into `build` rather than left to turbo's `^build` because publish-runner-cdk.yaml invokes `pnpm --filter @evident-ai/runner-cdk build` directly, which bypasses turbo entirely.",
19
+ "build-bundled-deps": "pnpm --filter @evident-ai/lambda-microvm-runtime build",
18
20
  "typecheck": "tsc --noEmit",
19
21
  "test": "node --test --require ts-node/register 'src/**/*.test.ts'",
20
22
  "format": "prettier --write 'src/**/*.ts'",
21
23
  "lint": "eslint 'src/**/*.ts' --max-warnings=0"
22
24
  },
23
25
  "peerDependencies": {
26
+ "@evident-ai/lambda-microvm-cdk": "^0.1.1",
24
27
  "aws-cdk-lib": "^2.240.0",
25
28
  "constructs": "^10.5.0"
26
29
  },
27
30
  "devDependencies": {
28
31
  "@aws-sdk/client-ecs": "^3.682.0",
32
+ "@aws-sdk/client-lambda-microvms": "^3.1095.0",
29
33
  "@aws-sdk/client-secrets-manager": "^3.682.0",
34
+ "@evident-ai/lambda-microvm-cdk": "^0.1.1",
35
+ "@evident-ai/lambda-microvm-runtime": "workspace:*",
30
36
  "@evident/webhook-signature": "workspace:*",
31
37
  "@types/node": "^22",
32
38
  "aws-cdk-lib": "^2.240.0",
@@ -45,6 +51,6 @@
45
51
  "directory": "packages/runner-cdk"
46
52
  },
47
53
  "homepage": "https://evident.run",
48
- "author": "Evident <contact@evident.run>",
54
+ "author": "Evident <hello@evident.run>",
49
55
  "license": "MIT"
50
56
  }