@evident-ai/runner-cdk 3.5.2-dev.da3340e → 3.5.2-dev.df1ae49
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/dist/controller-lambda/handler.js +4 -4
- package/dist/microvm-image-context/hooks/common.sh +59 -5
- package/dist/microvm-image-context/hooks/session-db-maintenance.mjs +69 -0
- package/dist/microvm-image-context/hooks/terminate +2 -0
- package/dist/waker-lambda/handler.js +3 -3
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
49802
|
+
// ../../sdk/src/signature.ts
|
|
49803
49803
|
var import_node_crypto11 = require("node:crypto");
|
|
49804
|
-
function
|
|
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 (!
|
|
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
|
-
// (
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
//
|
|
30
|
+
// ../../sdk/src/signature.ts
|
|
31
31
|
var import_node_crypto = require("node:crypto");
|
|
32
|
-
function
|
|
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 (!
|
|
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.
|
|
3
|
+
"version": "3.5.2-dev.df1ae49",
|
|
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",
|