@evident-ai/runner-cdk 3.5.2-dev.da3340e → 3.5.2-dev.e22029a

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
@@ -116,6 +116,17 @@ built from a `Dockerfile` you control. The generic runner image lives separately
116
116
 
117
117
  ## The MicroVM image
118
118
 
119
+ ### Prune image versions
120
+
121
+ MicroVM image versions are quota-bound. `@evident-ai/lambda-microvm-cdk` ships the
122
+ `prune-microvm-image-versions` CLI; enable it with one `package.json` script line:
123
+
124
+ ```json
125
+ "microvm:prune": "prune-microvm-image-versions --image-arn arn:aws:lambda:eu-west-1:000000000000:microvm-image:your-image"
126
+ ```
127
+
128
+ See the `@evident-ai/lambda-microvm-cdk` package README for the CLI flags and safety semantics.
129
+
119
130
  `EvidentMicrovmConstruct`'s `imageSource` is a **local directory** holding the image build
120
131
  context, which `cdk deploy` zips and uploads. The Lambda MicroVM service then builds the
121
132
  image in *your* account, from a base image ARN you discover there — so, unlike the Fargate
@@ -49799,9 +49799,9 @@ var MICROVM_MAX_RUN_SECONDS = 28800;
49799
49799
  // src/microvm/controller/handle-doorbell.ts
49800
49800
  var import_node_crypto12 = require("node:crypto");
49801
49801
 
49802
- // ../webhook-signature/src/verify-wake-signature.ts
49802
+ // ../../sdk/src/signature.ts
49803
49803
  var import_node_crypto11 = require("node:crypto");
49804
- function verifyWakeSignature(rawBody, signatureHeader, secret) {
49804
+ function verifyEvidentSignature(rawBody, signatureHeader, secret) {
49805
49805
  if (!signatureHeader || !secret) {
49806
49806
  return false;
49807
49807
  }
@@ -50042,7 +50042,7 @@ async function handleDoorbell({
50042
50042
  sleep: sleep4,
50043
50043
  random
50044
50044
  }) {
50045
- if (!verifyWakeSignature(rawBody, signatureHeader, doorbellSecret)) {
50045
+ if (!verifyEvidentSignature(rawBody, signatureHeader, doorbellSecret)) {
50046
50046
  return decide({
50047
50047
  statusCode: 401,
50048
50048
  action: "rejected",
@@ -50470,7 +50470,7 @@ function createRuntimeMicrovm(executionRoleArn) {
50470
50470
  // runner is outbound-only (it dials the Cloudflare relay), so AWS sees
50471
50471
  // an idle endpoint no matter how busy the agent is. Idle is instead
50472
50472
  // decided IN the VM by `evident run --idle-timeout`
50473
- // (packages/runner-cdk/microvm-image/hooks/common.sh's `IDLE_TIMEOUT_SECONDS`), whose
50473
+ // (runner/docker-images/microvm/hooks/common.sh's `IDLE_TIMEOUT_SECONDS`), whose
50474
50474
  // clean exit is what drives the controller's own `suspend()` below via
50475
50475
  // the `runner.suspend_requested` doorbell. See `idle-policy.test.ts`
50476
50476
  // in this directory, which scans this file's source for the key and
@@ -157,6 +157,13 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
157
157
  // Object Get/Put/Delete scoped to this agent's prefix. DeleteObject is
158
158
  // required: Litestream's retention/compaction prunes old LTX files.
159
159
  replicaBucket.grantReadWrite(taskRole, `${this.replicaPrefix}/*`);
160
+ // Vault credentials are read and conditionally written, so this shared prefix
161
+ // needs no delete or list permission; conditional writes use request headers.
162
+ taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
163
+ sid: 'ReadWriteCredentialVaultObjects',
164
+ actions: ['s3:GetObject', 's3:PutObject'],
165
+ resources: [replicaBucket.arnForObjects('credential-vaults/*')],
166
+ }));
160
167
  this.service = new ecs.FargateService(this, 'Service', {
161
168
  cluster,
162
169
  taskDefinition,
@@ -37,6 +37,7 @@ exports.EvidentMicrovmConstruct = void 0;
37
37
  const path = __importStar(require("path"));
38
38
  const cdk = __importStar(require("aws-cdk-lib"));
39
39
  const constructs_1 = require("constructs");
40
+ const iam = __importStar(require("aws-cdk-lib/aws-iam"));
40
41
  const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
41
42
  const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
42
43
  const lambda_microvm_cdk_1 = require("@evident-ai/lambda-microvm-cdk");
@@ -189,13 +190,14 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
189
190
  hooks: imageHooks,
190
191
  }));
191
192
  }
192
- // Bucket-wide rather than per-prefix (as the ECS runner's task role is):
193
- // the prefix arrives at /run, so it cannot be named at synth, and one
194
- // execution role is shared by every VM of every shape (D3). So a VM can
195
- // read another session's credential store — acceptable within a single
196
- // customer account, and what Phase 4 (#263) has to tighten when this goes
197
- // multi-tenant.
198
- durableState.grantReadWrite(defaultImage.executionRole);
193
+ // The execution role serves every image shape, so per-session state is scoped
194
+ // to the `runners/` root; vault credentials need only GetObject and PutObject.
195
+ durableState.grantReadWrite(defaultImage.executionRole, 'runners/*');
196
+ defaultImage.executionRole.addToPrincipalPolicy(new iam.PolicyStatement({
197
+ sid: 'ReadWriteCredentialVaultObjects',
198
+ actions: ['s3:GetObject', 's3:PutObject'],
199
+ resources: [durableState.arnForObjects('credential-vaults/*')],
200
+ }));
199
201
  // The image receives only an ARN; `/run` reads the secret value at runtime.
200
202
  props.runnerSecret?.grantRead(defaultImage.executionRole);
201
203
  // The shape catalogue the controller resolves a doorbell's `shape` field
@@ -445,6 +445,26 @@ stop_litestream() {
445
445
  log "litestream stopped ${outcome}"
446
446
  }
447
447
 
448
+ SESSION_DB_LITESTREAM_QUIESCED=0
449
+
450
+ quiesce_litestream_for_session_db() {
451
+ if [ "${SESSION_DB_LITESTREAM_QUIESCED}" -eq 1 ]; then
452
+ return 0
453
+ fi
454
+
455
+ if [ -z "${PERSISTENCE_BUCKET:-}" ] || [ -e "${SESSION_DB_NO_REPLICATE_MARKER}" ] || [ ! -s "${LITESTREAM_CONFIG_FILE}" ]; then
456
+ return 0
457
+ fi
458
+
459
+ if litestream_is_running; then
460
+ stop_litestream
461
+ else
462
+ warn "SESSION-DB-REPLICATOR-DIED: no live litestream at ${LITESTREAM_PID_FILE}; flushing one-shot anyway"
463
+ fi
464
+
465
+ SESSION_DB_LITESTREAM_QUIESCED=1
466
+ }
467
+
448
468
  # The cheap stop: one SIGTERM, one rm, one log line, NO poll loop — `/run`'s
449
469
  # own cleanup calls this, never stop_litestream, because that path only runs
450
470
  # after /run has already FAILED: no session happened yet, so a graceful final
@@ -519,11 +539,7 @@ flush_session_db() {
519
539
  return 0
520
540
  fi
521
541
 
522
- if litestream_is_running; then
523
- stop_litestream
524
- else
525
- warn "SESSION-DB-REPLICATOR-DIED: no live litestream at ${LITESTREAM_PID_FILE}; flushing one-shot anyway"
526
- fi
542
+ quiesce_litestream_for_session_db
527
543
 
528
544
  # Only reached with the daemon confirmed stopped, or never running — never
529
545
  # concurrently with it (see above).
@@ -539,6 +555,44 @@ flush_session_db() {
539
555
  }
540
556
  # --- flush_session_db (end) --------------------------------------------------
541
557
 
558
+ SESSION_DB_MAINTENANCE_DEADLINE_SECONDS=4
559
+ SESSION_DB_MAINTENANCE_KILL_GRACE_SECONDS=1
560
+ SESSION_DB_MAINTENANCE_MAX_BYTES=268435456
561
+
562
+ maintain_session_db() {
563
+ if [ -z "${PERSISTENCE_BUCKET:-}" ]; then
564
+ warn "SESSION-DB-MAINTENANCE-SKIP: persistence is disabled"
565
+ return 0
566
+ fi
567
+
568
+ if [ -e "${SESSION_DB_NO_REPLICATE_MARKER}" ]; then
569
+ warn "SESSION-DB-MAINTENANCE-SKIP: this boot's session DB was not proven safe to replicate"
570
+ return 0
571
+ fi
572
+
573
+ if [ ! -s "${LITESTREAM_CONFIG_FILE}" ]; then
574
+ error "SESSION-DB-MAINTENANCE-SKIP: no usable ${LITESTREAM_CONFIG_FILE}"
575
+ return 0
576
+ fi
577
+
578
+ if litestream_is_running; then
579
+ warn "SESSION-DB-MAINTENANCE-SKIP: litestream is still running"
580
+ return 0
581
+ fi
582
+
583
+ local maintenance_rc=0
584
+ timeout -k "${SESSION_DB_MAINTENANCE_KILL_GRACE_SECONDS}" "${SESSION_DB_MAINTENANCE_DEADLINE_SECONDS}" \
585
+ node "$(dirname "${BASH_SOURCE[0]}")/session-db-maintenance.mjs" \
586
+ "${HOME:-/home/runner}/.local/share/opencode/opencode.db" \
587
+ "${SESSION_DB_MAINTENANCE_MAX_BYTES}" || maintenance_rc=$?
588
+
589
+ if [ "${maintenance_rc}" -ne 0 ]; then
590
+ warn "SESSION-DB-MAINTENANCE: helper exited ${maintenance_rc}; continuing teardown"
591
+ fi
592
+
593
+ return 0
594
+ }
595
+
542
596
  # How long the guest CLI runs with no activity before it exits itself
543
597
  # (`evident run --idle-timeout`), which is what
544
598
  # turns a truly-abandoned VM into the clean-offline POST that lets Evident
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { statSync } from 'node:fs';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { performance } from 'node:perf_hooks';
6
+
7
+ const [dbPath, maxBytesText] = process.argv.slice(2);
8
+ const maxBytes = Number(maxBytesText);
9
+ const startedAt = performance.now();
10
+
11
+ function fileBytes(path) {
12
+ try {
13
+ return statSync(path).size;
14
+ } catch (error) {
15
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return 0;
16
+ throw error;
17
+ }
18
+ }
19
+
20
+ function walFrames(path, pageSize) {
21
+ const bytes = fileBytes(path);
22
+ return bytes < 32 ? 0 : Math.floor((bytes - 32) / (pageSize + 24));
23
+ }
24
+
25
+ function elapsedMs() {
26
+ return (performance.now() - startedAt).toFixed(3);
27
+ }
28
+
29
+ let database;
30
+
31
+ try {
32
+ if (!dbPath || !Number.isInteger(maxBytes) || maxBytes <= 0) {
33
+ throw new Error('expected a database path and a positive byte limit');
34
+ }
35
+
36
+ const dbBytes = statSync(dbPath).size;
37
+ database = new DatabaseSync(dbPath);
38
+ const pageSize = Number(database.prepare('PRAGMA page_size').get().page_size);
39
+ const walPath = `${dbPath}-wal`;
40
+ const walBytes = fileBytes(walPath);
41
+ const totalBytes = dbBytes + walBytes;
42
+ const walFramesBefore = walFrames(walPath, pageSize);
43
+
44
+ if (totalBytes > maxBytes) {
45
+ console.log(
46
+ `SESSION-DB-MAINTENANCE-SKIP: reason=size-gate db_bytes=${dbBytes} ` +
47
+ `wal_bytes=${walBytes} total_bytes=${totalBytes} max_bytes=${maxBytes} ` +
48
+ `wal_frames_before=${walFramesBefore} wal_frames_after=${walFramesBefore} ` +
49
+ `elapsed_ms=${elapsedMs()}`,
50
+ );
51
+ } else {
52
+ const checkpoint = database.prepare('PRAGMA wal_checkpoint(TRUNCATE)').get();
53
+ const walFramesAfter = walFrames(walPath, pageSize);
54
+ console.log(
55
+ `SESSION-DB-MAINTENANCE: operation=wal_checkpoint_truncate ` +
56
+ `db_bytes=${dbBytes} wal_bytes=${walBytes} wal_frames_before=${walFramesBefore} ` +
57
+ `wal_frames_after=${walFramesAfter} busy=${checkpoint.busy} log=${checkpoint.log} ` +
58
+ `checkpointed=${checkpoint.checkpointed} elapsed_ms=${elapsedMs()}`,
59
+ );
60
+ }
61
+ } catch (error) {
62
+ console.error(
63
+ `SESSION-DB-MAINTENANCE-ERROR: ${error instanceof Error ? error.message : String(error)} ` +
64
+ `elapsed_ms=${elapsedMs()}`,
65
+ );
66
+ process.exitCode = 1;
67
+ } finally {
68
+ if (database) database.close();
69
+ }
@@ -20,6 +20,8 @@ stop_runner_and_flush_credentials
20
20
  # stop_opencode — /terminate can afford the real drain, the VM is being torn
21
21
  # down either way.
22
22
  stop_opencode_and_wait
23
+ quiesce_litestream_for_session_db
24
+ maintain_session_db
23
25
  flush_session_db
24
26
 
25
27
  # The key must not outlive the VM's last useful moment. The no-replicate
@@ -27,9 +27,9 @@ module.exports = __toCommonJS(handler_exports);
27
27
  var import_client_ecs = require("@aws-sdk/client-ecs");
28
28
  var import_client_secrets_manager = require("@aws-sdk/client-secrets-manager");
29
29
 
30
- // ../webhook-signature/src/verify-wake-signature.ts
30
+ // ../../sdk/src/signature.ts
31
31
  var import_node_crypto = require("node:crypto");
32
- function verifyWakeSignature(rawBody, signatureHeader, secret) {
32
+ function verifyEvidentSignature(rawBody, signatureHeader, secret) {
33
33
  if (!signatureHeader || !secret) {
34
34
  return false;
35
35
  }
@@ -64,7 +64,7 @@ async function handleWake({
64
64
  service,
65
65
  ecs
66
66
  }) {
67
- if (!verifyWakeSignature(rawBody, signatureHeader, wakeSecret)) {
67
+ if (!verifyEvidentSignature(rawBody, signatureHeader, wakeSecret)) {
68
68
  const { statusCode: statusCode2, body: body2 } = jsonResponse(401, "invalid signature");
69
69
  return { statusCode: statusCode2, body: body2, calledUpdateService: false };
70
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evident-ai/runner-cdk",
3
- "version": "3.5.2-dev.da3340e",
3
+ "version": "3.5.2-dev.e22029a",
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",