@evident-ai/runner-cdk 0.1.1-dev.6ca48ee → 0.1.1-dev.7802262
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
CHANGED
|
@@ -151,6 +151,8 @@ new EvidentMicrovmConstruct(this, 'Runner', {
|
|
|
151
151
|
baseImageArn,
|
|
152
152
|
baseImageVersion,
|
|
153
153
|
doorbellSecret,
|
|
154
|
+
runnerSecret,
|
|
155
|
+
runnerOpencodeConfigPath: 'opencode.runner.jsonc',
|
|
154
156
|
});
|
|
155
157
|
```
|
|
156
158
|
|
|
@@ -158,6 +160,12 @@ Your repo does not have to be a pnpm workspace. If it commits a `pnpm-lock.yaml`
|
|
|
158
160
|
pre-installs dependencies at build time (a faster first boot); if not, that step is skipped
|
|
159
161
|
and the agent installs on first use.
|
|
160
162
|
|
|
163
|
+
| MicroVM prop | Omitted behavior |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| `runnerSecret` | No GitHub or MCP credentials are exported at `/run`. |
|
|
166
|
+
| `runnerOpencodeConfigPath` | OpenCode uses the baked project configuration. Relative paths resolve from the workspace; absolute paths resolve in the image. |
|
|
167
|
+
| `gitUserName` / `gitUserEmail` | The hook uses the Evident bot defaults for git identity. |
|
|
168
|
+
|
|
161
169
|
## Status / limitations
|
|
162
170
|
|
|
163
171
|
- **Published to npm** as `@evident-ai/runner-cdk` (MIT). Install the `@dev` tag —
|
|
@@ -41,6 +41,20 @@ export interface EvidentMicrovmConstructProps {
|
|
|
41
41
|
* in its own scope, and passes the result in either case.
|
|
42
42
|
*/
|
|
43
43
|
readonly doorbellSecret: secretsmanager.ISecret;
|
|
44
|
+
/**
|
|
45
|
+
* Optional runner secret whose JSON values are exported by `/run`. Omitting it
|
|
46
|
+
* leaves GitHub and MCP credentials unavailable to a generic consumer.
|
|
47
|
+
*/
|
|
48
|
+
readonly runnerSecret?: secretsmanager.ISecret;
|
|
49
|
+
/**
|
|
50
|
+
* Optional runner OpenCode overlay. Absolute paths are image paths; relative
|
|
51
|
+
* paths are resolved from the baked workspace by the `/run` hook.
|
|
52
|
+
*/
|
|
53
|
+
readonly runnerOpencodeConfigPath?: string;
|
|
54
|
+
/** Optional git identity the `/run` hook configures for agent commits. */
|
|
55
|
+
readonly gitUserName?: string;
|
|
56
|
+
/** Optional git email the `/run` hook configures for agent commits. */
|
|
57
|
+
readonly gitUserEmail?: string;
|
|
44
58
|
/**
|
|
45
59
|
* TCP port the image's hook server listens on, baked into both the image
|
|
46
60
|
* (`HOOKS_PORT` env var) and the `MicrovmImage`'s `hooks.port` — a mismatch
|
|
@@ -121,6 +121,12 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
121
121
|
// already bakes in for its own runners (evident-scale-to-zero-construct.ts:191).
|
|
122
122
|
// Safe to bake: identical for every VM from this image version.
|
|
123
123
|
EVIDENT_SESSION_CLEANUP_MAX_AGE: '24h',
|
|
124
|
+
...(props.runnerOpencodeConfigPath
|
|
125
|
+
? { RUNNER_OPENCODE_CONFIG: props.runnerOpencodeConfigPath }
|
|
126
|
+
: {}),
|
|
127
|
+
...(props.runnerSecret ? { RUNNER_SECRET_ARN: props.runnerSecret.secretArn } : {}),
|
|
128
|
+
...(props.gitUserName ? { GIT_USER_NAME: props.gitUserName } : {}),
|
|
129
|
+
...(props.gitUserEmail ? { GIT_USER_EMAIL: props.gitUserEmail } : {}),
|
|
124
130
|
};
|
|
125
131
|
// ...and the hooks configuration every shape's image is built with.
|
|
126
132
|
const imageHooks = {
|
|
@@ -179,6 +185,8 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
179
185
|
// customer account, and what Phase 4 (#263) has to tighten when this goes
|
|
180
186
|
// multi-tenant.
|
|
181
187
|
durableState.grantReadWrite(defaultImage.executionRole);
|
|
188
|
+
// The image receives only an ARN; `/run` reads the secret value at runtime.
|
|
189
|
+
props.runnerSecret?.grantRead(defaultImage.executionRole);
|
|
182
190
|
// The shape catalogue the controller resolves a doorbell's `shape` field
|
|
183
191
|
// against, and advertises over the describe channel (#723). One JSON
|
|
184
192
|
// array, built with `Stack.toJsonString` — not `JSON.stringify` — because
|
|
@@ -211,13 +211,13 @@ load_state_config() {
|
|
|
211
211
|
# `model-auth-ready` therefore stays a boot-time diagnostic: its only output is
|
|
212
212
|
# a log line, since the runtime forwards no hook stderr to the doorbell caller.
|
|
213
213
|
|
|
214
|
-
# How long restore_credentials' shared step budget is, covering
|
|
215
|
-
# its synchroniser calls (restore claude, restore opencode,
|
|
216
|
-
# as one window rather than a timeout apiece — a fixed per-call cap at the same
|
|
214
|
+
# How long restore_credentials' shared step budget is, covering the runner-secret
|
|
215
|
+
# fetch plus its three synchroniser calls (restore claude, restore opencode,
|
|
216
|
+
# model-auth-ready) as one window rather than a timeout apiece — a fixed per-call cap at the same
|
|
217
217
|
# total would false-fire on the ordinary case of one slow call (a slow S3 GET),
|
|
218
218
|
# which is precisely the boot this exists to keep healthy. From #930's 3-boot
|
|
219
|
-
# sample: the step measured ~3-5s total (~1.2s per call, three
|
|
220
|
-
# starts of a 1.8 MB bundle) — a WEAK estimate this file's own
|
|
219
|
+
# sample: the step measured ~3-5s total (~1.2s per synchroniser call, three
|
|
220
|
+
# node cold starts of a 1.8 MB bundle) — a WEAK estimate this file's own
|
|
221
221
|
# SYNCHRONISER-TIMING lines are what will sharpen for real. Too tight and a
|
|
222
222
|
# routine slow call boots this VM with no model credentials until an operator
|
|
223
223
|
# reconnects it; too loose and a hung call burns more of the hook's own
|
|
@@ -229,6 +229,7 @@ CREDENTIAL_RESTORE_DEADLINE_SECONDS="${EVIDENT_CREDENTIAL_RESTORE_DEADLINE_SECON
|
|
|
229
229
|
# and a call that ignored it would be unbounded again). Hard ceiling on the
|
|
230
230
|
# step: DEADLINE + this = 10s, once — not per call.
|
|
231
231
|
CREDENTIAL_RESTORE_KILL_GRACE_SECONDS=2
|
|
232
|
+
GITHUB_PROBE_DEADLINE_SECONDS="${EVIDENT_GITHUB_PROBE_DEADLINE_SECONDS:-10}"
|
|
232
233
|
|
|
233
234
|
# What is left of the shared step budget, in whole seconds, `step_started_s`
|
|
234
235
|
# seconds after it began. `SECONDS` (a bash builtin with no failure mode,
|
|
@@ -268,12 +269,66 @@ bounded_restore_call() {
|
|
|
268
269
|
return 0
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
fetch_runner_secret() {
|
|
273
|
+
local step_started_s="$1" remaining rc=0 payload value stderr_file started_ms populated=0 missing=0
|
|
274
|
+
if [ -z "${RUNNER_SECRET_ARN:-}" ]; then
|
|
275
|
+
log "runner secret is not configured; continuing without GitHub and MCP credentials"
|
|
276
|
+
return 0
|
|
277
|
+
fi
|
|
278
|
+
remaining="$(remaining_credential_budget "${step_started_s}")"
|
|
279
|
+
if [ "${remaining}" -lt 1 ]; then
|
|
280
|
+
warn "CREDENTIAL-RESTORE-SKIPPED: runner-secret skipped; the ${CREDENTIAL_RESTORE_DEADLINE_SECONDS}s credential-restore budget is exhausted"
|
|
281
|
+
return 0
|
|
282
|
+
fi
|
|
283
|
+
if ! stderr_file="$(mktemp /dev/shm/runner-secret-stderr.XXXXXX)"; then
|
|
284
|
+
warn "RUNNER-SECRET-STDERR-UNAVAILABLE: could not allocate diagnostic storage; continuing without runner credentials"
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
287
|
+
started_ms="$(now_ms)"
|
|
288
|
+
payload="$(timeout -k "${CREDENTIAL_RESTORE_KILL_GRACE_SECONDS}" "${remaining}" aws secretsmanager get-secret-value --secret-id "${RUNNER_SECRET_ARN}" --query SecretString --output text 2>"${stderr_file}")" || rc=$?
|
|
289
|
+
log_elapsed_since runner-secret-fetch "${started_ms}" "${rc}"
|
|
290
|
+
if [ "${rc}" -eq 124 ] || [ "${rc}" -eq 137 ]; then
|
|
291
|
+
warn "CREDENTIAL-RESTORE-TIMEOUT: runner-secret did not finish within its ${remaining}s share of the ${CREDENTIAL_RESTORE_DEADLINE_SECONDS}s credential-restore deadline; continuing without it"
|
|
292
|
+
rm -f "${stderr_file}"
|
|
293
|
+
return 0
|
|
294
|
+
fi
|
|
295
|
+
if [ "${rc}" -ne 0 ]; then
|
|
296
|
+
warn "RUNNER-SECRET-UNREADABLE: $(<"${stderr_file}")"
|
|
297
|
+
rm -f "${stderr_file}"
|
|
298
|
+
return 0
|
|
299
|
+
fi
|
|
300
|
+
rm -f "${stderr_file}"
|
|
301
|
+
if ! jq -e 'type == "object"' >/dev/null 2>&1 <<<"${payload}"; then
|
|
302
|
+
warn "RUNNER-SECRET-UNPARSEABLE: secret value is not a JSON object"
|
|
303
|
+
return 0
|
|
304
|
+
fi
|
|
305
|
+
local -a populated_keys=()
|
|
306
|
+
for key in GH_TOKEN BRAVE_API_KEY CLOUDFLARE_API_TOKEN NEON_API_KEY; do
|
|
307
|
+
value="$(jq -r --arg k "${key}" '.[$k] // empty' <<<"${payload}")"
|
|
308
|
+
if [ -n "${value}" ]; then
|
|
309
|
+
export "${key}=${value}"
|
|
310
|
+
populated=$((populated + 1))
|
|
311
|
+
populated_keys+=("${key}")
|
|
312
|
+
else
|
|
313
|
+
missing=$((missing + 1))
|
|
314
|
+
fi
|
|
315
|
+
done
|
|
316
|
+
if [ "${populated}" -eq 0 ]; then
|
|
317
|
+
warn "RUNNER-SECRET-UNPOPULATED: populate the runner secret as documented in infrastructure/evident-microvm/README.md"
|
|
318
|
+
else
|
|
319
|
+
log "RUNNER-SECRET-OK: populated ${populated_keys[*]}; ${missing} allow-listed keys empty or absent"
|
|
320
|
+
fi
|
|
321
|
+
return 0
|
|
322
|
+
}
|
|
323
|
+
|
|
271
324
|
restore_credentials() {
|
|
272
325
|
local step_started_ms step_started_s
|
|
273
326
|
step_started_ms="$(now_ms)"
|
|
274
327
|
step_started_s="${SECONDS}"
|
|
275
328
|
|
|
276
329
|
load_state_config || return 1
|
|
330
|
+
# This shares the existing bounded window so /run's worst-case duration does not grow.
|
|
331
|
+
fetch_runner_secret "${step_started_s}"
|
|
277
332
|
bounded_restore_call restore-claude "${step_started_s}" restore claude || return 1
|
|
278
333
|
bounded_restore_call restore-opencode "${step_started_s}" restore opencode || return 1
|
|
279
334
|
|
|
@@ -313,7 +368,7 @@ infrastructure/evident-microvm/README.md."
|
|
|
313
368
|
fi
|
|
314
369
|
# The step total, in the SAME greppable shape as the per-call lines above, so
|
|
315
370
|
# one query answers both "what does credential restore cost?" and "which of
|
|
316
|
-
# its
|
|
371
|
+
# its four operations cost it". Bounded now by CREDENTIAL_RESTORE_DEADLINE_SECONDS
|
|
317
372
|
# + CREDENTIAL_RESTORE_KILL_GRACE_SECONDS as one shared window (see the
|
|
318
373
|
# comment above that constant) rather than per call. `rc=0` is deliberate
|
|
319
374
|
# here, not an absence of failure modes: a timed-out call already warned by
|
|
@@ -323,6 +378,55 @@ infrastructure/evident-microvm/README.md."
|
|
|
323
378
|
return 0
|
|
324
379
|
}
|
|
325
380
|
|
|
381
|
+
apply_runner_opencode_config() {
|
|
382
|
+
if [ -z "${RUNNER_OPENCODE_CONFIG:-}" ]; then
|
|
383
|
+
log "runner OpenCode config is not configured; using the baked project config"
|
|
384
|
+
return 0
|
|
385
|
+
fi
|
|
386
|
+
local source="${RUNNER_OPENCODE_CONFIG}" target="opencode.json"
|
|
387
|
+
[[ "${source}" = /* ]] || source="${WORKSPACE}/${source}"
|
|
388
|
+
[ -f "${WORKSPACE}/opencode.jsonc" ] && target="opencode.jsonc"
|
|
389
|
+
if [ ! -f "${source}" ]; then
|
|
390
|
+
error "RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)"
|
|
391
|
+
return 0
|
|
392
|
+
fi
|
|
393
|
+
cp "${source}" "${WORKSPACE}/${target}"
|
|
394
|
+
git -C "${WORKSPACE}" update-index --skip-worktree "${target}" 2>/dev/null \
|
|
395
|
+
|| warn "could not mark ${target} skip-worktree; it may show as a local change"
|
|
396
|
+
log "Applied runner OpenCode config ${source} to ${WORKSPACE}/${target}"
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
configure_github_access() {
|
|
400
|
+
if [ -z "${GH_TOKEN:-}" ]; then
|
|
401
|
+
warn "GITHUB-CREDENTIALS-MISSING: GH_TOKEN is unavailable; see RUNNER-SECRET-* above"
|
|
402
|
+
return 0
|
|
403
|
+
fi
|
|
404
|
+
export GIT_CONFIG_GLOBAL=/tmp/gitconfig
|
|
405
|
+
if ! : >"${GIT_CONFIG_GLOBAL}" ||
|
|
406
|
+
! git config --global user.name "${GIT_USER_NAME:-evident-bot}" ||
|
|
407
|
+
! git config --global user.email "${GIT_USER_EMAIL:-evident-bot@users.noreply.github.com}" ||
|
|
408
|
+
! git config --global init.defaultBranch main ||
|
|
409
|
+
! printf '%s\n' '#!/usr/bin/env bash' '[ "$1" = get ] || exit 0' 'echo username=x-access-token' 'echo "password=${GH_TOKEN}"' >/tmp/git-credential-helper.sh ||
|
|
410
|
+
! chmod 0700 /tmp/git-credential-helper.sh ||
|
|
411
|
+
! git config --global credential."https://github.com".helper /tmp/git-credential-helper.sh; then
|
|
412
|
+
warn "GITHUB-SETUP-FAILED: could not configure local git credentials; continuing without GitHub access"
|
|
413
|
+
return 0
|
|
414
|
+
fi
|
|
415
|
+
(
|
|
416
|
+
local output rc=0 login repo_url repo
|
|
417
|
+
output="$(timeout -k 1 "${GITHUB_PROBE_DEADLINE_SECONDS}" gh api user --jq .login 2>&1)" || rc=$?
|
|
418
|
+
if [ "${rc}" -eq 124 ] || [ "${rc}" -eq 137 ]; then warn "GITHUB-PROBE-TIMEOUT: auth probe exceeded ${GITHUB_PROBE_DEADLINE_SECONDS}s"; return; fi
|
|
419
|
+
if [ "${rc}" -ne 0 ]; then warn "GITHUB-AUTH-REJECTED: ${output}"; return; fi
|
|
420
|
+
log "GITHUB-AUTH-OK: ${output}"
|
|
421
|
+
repo_url="$(git -C "${WORKSPACE}" remote get-url origin 2>/dev/null || true)"
|
|
422
|
+
repo="$(printf '%s' "${repo_url}" | sed -E 's#(https://github.com/|git@github.com:)##; s#\.git$##')"
|
|
423
|
+
[ -n "${repo}" ] || return
|
|
424
|
+
output="$(timeout -k 1 "${GITHUB_PROBE_DEADLINE_SECONDS}" gh api "repos/${repo}" --jq .full_name 2>&1)" || rc=$?
|
|
425
|
+
if [ "${rc}" -eq 124 ] || [ "${rc}" -eq 137 ]; then warn "GITHUB-PROBE-TIMEOUT: repository probe for ${repo} exceeded ${GITHUB_PROBE_DEADLINE_SECONDS}s"; return; fi
|
|
426
|
+
[ "${rc}" -eq 0 ] || warn "GITHUB-REPO-INACCESSIBLE: ${repo}: ${output}"
|
|
427
|
+
) &
|
|
428
|
+
}
|
|
429
|
+
|
|
326
430
|
# Best-effort by design: /suspend must still drop the tunnel and /terminate must
|
|
327
431
|
# still clean up, so a flush that cannot happen is loud but never fatal.
|
|
328
432
|
sync_credentials() {
|
|
@@ -577,7 +681,7 @@ restore_session_db() {
|
|
|
577
681
|
esac
|
|
578
682
|
|
|
579
683
|
local classify_rc=0
|
|
580
|
-
run_synchroniser session-db-classify "${restore_rc}" 1 --on-unusable-replica=leave || classify_rc=$?
|
|
684
|
+
run_synchroniser session-db-classify "${restore_rc}" 1 --on-unusable-replica=leave --fresh-db-fallback || classify_rc=$?
|
|
581
685
|
case "${classify_rc}" in
|
|
582
686
|
0) ;; # restored, or no replica yet — the CLI already logged which
|
|
583
687
|
31)
|
|
@@ -79,7 +79,13 @@ check_runner_key "${runner_key}" "${api_url}" || exit 1
|
|
|
79
79
|
restore_session_db
|
|
80
80
|
log "session DB restore done ${SECONDS}s into the hook"
|
|
81
81
|
|
|
82
|
-
# 7 —
|
|
82
|
+
# 7 — apply the overlay before OpenCode resolves its project configuration.
|
|
83
|
+
apply_runner_opencode_config
|
|
84
|
+
|
|
85
|
+
# 8 — configure git after credentials are restored and before agent shells start.
|
|
86
|
+
configure_github_access
|
|
87
|
+
|
|
88
|
+
# 9 — opencode. Started here, not at build time: a warm process in the shared
|
|
83
89
|
# snapshot would carry its installation id and database into every VM. Not
|
|
84
90
|
# waited on: a slow opencode boot is not a reason to fail /run (the tunnel CLI
|
|
85
91
|
# auto-starts opencode when it finds none healthy,
|
|
@@ -87,14 +93,14 @@ log "session DB restore done ${SECONDS}s into the hook"
|
|
|
87
93
|
# reclaims a runner that never comes online).
|
|
88
94
|
start_opencode
|
|
89
95
|
|
|
90
|
-
#
|
|
96
|
+
# 10 — begin replicating the session DB (#812 WI-3), now that opencode has
|
|
91
97
|
# opened it and before any work can arrive over the tunnel. Bare, like
|
|
92
98
|
# restore_session_db above: start_litestream never returns non-zero (every
|
|
93
99
|
# guard inside it is its own `return 0`), so there is nothing here for
|
|
94
100
|
# `set -e` to abort on.
|
|
95
101
|
start_litestream
|
|
96
102
|
|
|
97
|
-
#
|
|
103
|
+
# 11 — the first per-VM identity on the wire. The subshell's umask makes the file
|
|
98
104
|
# unreadable to anyone else from the moment it exists, before the key is in it.
|
|
99
105
|
(
|
|
100
106
|
umask 077
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evident-ai/runner-cdk",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.7802262",
|
|
4
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",
|