@evident-ai/runner-cdk 3.5.2-dev.507f0a3 → 3.5.2-dev.7130f36

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.
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evident-ai/runner-cdk",
3
- "version": "3.5.2-dev.507f0a3",
3
+ "version": "3.5.2-dev.7130f36",
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",