@evident-ai/runner-cdk 0.1.1-dev.da70cd4 → 3.4.1-dev.1856549
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 +76 -19
- package/dist/controller-lambda/handler.js +50523 -0
- package/dist/evident-scale-to-zero-construct.js +4 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -1
- package/dist/microvm/constants.d.ts +7 -0
- package/dist/microvm/constants.js +34 -0
- package/dist/microvm/construct.d.ts +87 -0
- package/dist/microvm/construct.js +253 -0
- package/dist/microvm/controller/doorbell.d.ts +73 -0
- package/dist/microvm/controller/doorbell.js +107 -0
- package/dist/microvm/controller/handle-doorbell.d.ts +27 -0
- package/dist/microvm/controller/handle-doorbell.js +480 -0
- package/dist/microvm/controller/microvm-client.d.ts +75 -0
- package/dist/microvm/controller/microvm-client.js +7 -0
- package/dist/microvm/controller/shape-catalogue.d.ts +64 -0
- package/dist/microvm/controller/shape-catalogue.js +108 -0
- package/dist/microvm/controller/throttle-retry.d.ts +11 -0
- package/dist/microvm/controller/throttle-retry.js +27 -0
- package/dist/microvm/image/stage-context.d.ts +33 -0
- package/dist/microvm/image/stage-context.js +148 -0
- package/dist/microvm/shapes.d.ts +72 -0
- package/dist/microvm/shapes.js +93 -0
- package/dist/microvm-image-context/Dockerfile +227 -0
- package/dist/microvm-image-context/hook-server.js +286 -0
- package/dist/microvm-image-context/hooks/common.sh +1418 -0
- package/dist/microvm-image-context/hooks/resume +79 -0
- package/dist/microvm-image-context/hooks/run +117 -0
- package/dist/microvm-image-context/hooks/suspend +19 -0
- package/dist/microvm-image-context/hooks/terminate +34 -0
- package/dist/waker/construct.js +1 -1
- package/package.json +15 -7
|
@@ -101,7 +101,7 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
101
101
|
// --endpoint / --tunnel). The agent is resolved from EVIDENT_AGENT_KEY.
|
|
102
102
|
EVIDENT_API_URL: evidentApiUrl,
|
|
103
103
|
EVIDENT_TUNNEL_URL: evidentTunnelUrl,
|
|
104
|
-
// Litestream replica target (read by
|
|
104
|
+
// Litestream replica target (read by runner/synchroniser, which
|
|
105
105
|
// generates /etc/evident/litestream.yml at boot).
|
|
106
106
|
LITESTREAM_BUCKET: replicaBucket.bucketName,
|
|
107
107
|
LITESTREAM_PREFIX: this.replicaPrefix,
|
|
@@ -115,10 +115,9 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
115
115
|
// window (protected/active sessions are never touched). Tightened from 7d
|
|
116
116
|
// to 24h per #537: at 7d, opencode.db was observed growing ~11x (77 MB ->
|
|
117
117
|
// 849 MB) in ~24h, so 7d was far too permissive at the observed growth
|
|
118
|
-
// rate. 24h is
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// interval and never touches a protected/active session.
|
|
118
|
+
// rate. 24h is the tighter mitigation that came out of that (see also the
|
|
119
|
+
// OPENCODE_VERSION pin above). Sweep runs on the default 1h interval and
|
|
120
|
+
// never touches a protected/active session.
|
|
122
121
|
EVIDENT_SESSION_CLEANUP_MAX_AGE: '24h',
|
|
123
122
|
CLUSTER: cluster.clusterName,
|
|
124
123
|
SERVICE: serviceName,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { EvidentScaleToZeroConstruct, type EvidentScaleToZeroConstructProps, } from './evident-scale-to-zero-construct';
|
|
2
2
|
export { EvidentWaker, type EvidentWakerProps } from './waker/construct';
|
|
3
|
+
export { EvidentMicrovmConstruct, type EvidentMicrovmConstructProps } from './microvm/construct';
|
|
4
|
+
export { MICROVM_SHAPES, validateShapes, type MicrovmShape } from './microvm/shapes';
|
|
5
|
+
export { MICROVM_MAX_RUN_SECONDS, HOOKS_PORT, HOOK_TIMEOUT_SECONDS, } from './microvm/constants';
|
|
6
|
+
export { stageMicrovmImageContext, type StageMicrovmImageContextOptions, } from './microvm/image/stage-context';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EvidentWaker = exports.EvidentScaleToZeroConstruct = void 0;
|
|
3
|
+
exports.stageMicrovmImageContext = exports.HOOK_TIMEOUT_SECONDS = exports.HOOKS_PORT = exports.MICROVM_MAX_RUN_SECONDS = exports.validateShapes = exports.MICROVM_SHAPES = exports.EvidentMicrovmConstruct = exports.EvidentWaker = exports.EvidentScaleToZeroConstruct = void 0;
|
|
4
4
|
var evident_scale_to_zero_construct_1 = require("./evident-scale-to-zero-construct");
|
|
5
5
|
Object.defineProperty(exports, "EvidentScaleToZeroConstruct", { enumerable: true, get: function () { return evident_scale_to_zero_construct_1.EvidentScaleToZeroConstruct; } });
|
|
6
6
|
var construct_1 = require("./waker/construct");
|
|
7
7
|
Object.defineProperty(exports, "EvidentWaker", { enumerable: true, get: function () { return construct_1.EvidentWaker; } });
|
|
8
|
+
var construct_2 = require("./microvm/construct");
|
|
9
|
+
Object.defineProperty(exports, "EvidentMicrovmConstruct", { enumerable: true, get: function () { return construct_2.EvidentMicrovmConstruct; } });
|
|
10
|
+
var shapes_1 = require("./microvm/shapes");
|
|
11
|
+
Object.defineProperty(exports, "MICROVM_SHAPES", { enumerable: true, get: function () { return shapes_1.MICROVM_SHAPES; } });
|
|
12
|
+
Object.defineProperty(exports, "validateShapes", { enumerable: true, get: function () { return shapes_1.validateShapes; } });
|
|
13
|
+
var constants_1 = require("./microvm/constants");
|
|
14
|
+
Object.defineProperty(exports, "MICROVM_MAX_RUN_SECONDS", { enumerable: true, get: function () { return constants_1.MICROVM_MAX_RUN_SECONDS; } });
|
|
15
|
+
// The two values a consumer MUST pass to `EvidentMicrovmConstruct` for them
|
|
16
|
+
// to match the published image they were baked into (#1528).
|
|
17
|
+
Object.defineProperty(exports, "HOOKS_PORT", { enumerable: true, get: function () { return constants_1.HOOKS_PORT; } });
|
|
18
|
+
Object.defineProperty(exports, "HOOK_TIMEOUT_SECONDS", { enumerable: true, get: function () { return constants_1.HOOK_TIMEOUT_SECONDS; } });
|
|
19
|
+
var stage_context_1 = require("./microvm/image/stage-context");
|
|
20
|
+
Object.defineProperty(exports, "stageMicrovmImageContext", { enumerable: true, get: function () { return stage_context_1.stageMicrovmImageContext; } });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const HOOKS_PORT = 8080;
|
|
2
|
+
export declare const HOOKS_DIR = "/etc/evident/hooks";
|
|
3
|
+
export declare const HOOK_TIMEOUT_SECONDS = 60;
|
|
4
|
+
export declare const RUN_HOOK_PAYLOAD_MAX_BYTES = 16384;
|
|
5
|
+
export declare const SUSPENDING_POLL_ATTEMPTS = 4;
|
|
6
|
+
export declare const SUSPENDING_POLL_INTERVAL_MS = 500;
|
|
7
|
+
export declare const MICROVM_MAX_RUN_SECONDS = 28800;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared literals for the MicroVM controller and for the image it launches.
|
|
3
|
+
// Every module imports from here rather than restating a value that must match
|
|
4
|
+
// across them. The image template pins matching copies, guarded by
|
|
5
|
+
// `microvm/image/dockerfile.test.ts`.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.MICROVM_MAX_RUN_SECONDS = exports.SUSPENDING_POLL_INTERVAL_MS = exports.SUSPENDING_POLL_ATTEMPTS = exports.RUN_HOOK_PAYLOAD_MAX_BYTES = exports.HOOK_TIMEOUT_SECONDS = exports.HOOKS_DIR = exports.HOOKS_PORT = void 0;
|
|
8
|
+
// The port the image's hook server binds, baked into the published Dockerfile
|
|
9
|
+
// (`ENV HOOKS_PORT`) and passed to the construct as `hooksPort`. Exported from
|
|
10
|
+
// the package index because a consumer building the published image context
|
|
11
|
+
// MUST pass the value it was baked with — the construct's own docstring warns
|
|
12
|
+
// that a mismatch means AWS probes a port nothing binds.
|
|
13
|
+
exports.HOOKS_PORT = 8080;
|
|
14
|
+
// One executable per lifecycle phase. A phase with no script is a no-op, which
|
|
15
|
+
// is why the image ships none for `ready` or `validate`: nothing Evident-specific
|
|
16
|
+
// may run before the snapshot, so there is nothing for them to do.
|
|
17
|
+
exports.HOOKS_DIR = '/etc/evident/hooks';
|
|
18
|
+
// Seconds AWS allows a Run/Resume/Suspend/Terminate hook, passed to the
|
|
19
|
+
// construct as `hookTimeoutSeconds`. Exported for the same reason as
|
|
20
|
+
// `HOOKS_PORT`: the hooks are built against it.
|
|
21
|
+
exports.HOOK_TIMEOUT_SECONDS = 60;
|
|
22
|
+
// AWS caps `runHookPayload` at 16 KiB; anything larger is rejected at its API,
|
|
23
|
+
// so the controller checks it before spending a call.
|
|
24
|
+
exports.RUN_HOOK_PAYLOAD_MAX_BYTES = 16384;
|
|
25
|
+
// Worst-case poll (attempts × interval ≈ 2 s) must fit the controller Lambda's 8 s
|
|
26
|
+
// timeout alongside the retry backoff.
|
|
27
|
+
exports.SUSPENDING_POLL_ATTEMPTS = 4;
|
|
28
|
+
exports.SUSPENDING_POLL_INTERVAL_MS = 500;
|
|
29
|
+
// AWS's non-adjustable ceiling on a single MicroVM run, passed as
|
|
30
|
+
// `RunMicrovmCommand`'s `maximumDurationInSeconds`. The version pruner reuses it
|
|
31
|
+
// as its post-deactivation staging delay: once a version has been INACTIVE for
|
|
32
|
+
// longer than any VM can possibly still be running, no VM started before the
|
|
33
|
+
// deactivation can outlive it. The two MUST move together.
|
|
34
|
+
exports.MICROVM_MAX_RUN_SECONDS = 28800;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
3
|
+
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
|
4
|
+
import { type MicrovmShape } from './shapes';
|
|
5
|
+
export interface EvidentMicrovmConstructProps {
|
|
6
|
+
/**
|
|
7
|
+
* Build context for the image, already staged on disk. An argument rather
|
|
8
|
+
* than something this construct stages itself, so synthesising the stack
|
|
9
|
+
* stays a pure function of its inputs instead of depending on how the
|
|
10
|
+
* ambient checkout was cloned.
|
|
11
|
+
*/
|
|
12
|
+
readonly imageSource: string;
|
|
13
|
+
/**
|
|
14
|
+
* ARN of the Lambda-managed base MicroVM image, from
|
|
15
|
+
* `aws lambda-microvms list-managed-microvm-images`. A string, not a `CfnParameter`
|
|
16
|
+
* built here: the caller creates the parameter at ITS OWN stack scope
|
|
17
|
+
* (preserving its logical id) and passes its resolved value down — a
|
|
18
|
+
* construct must not mint parameters into a consumer's stack.
|
|
19
|
+
*/
|
|
20
|
+
readonly baseImageArn: string;
|
|
21
|
+
/**
|
|
22
|
+
* Version of the Lambda-managed base MicroVM image, from
|
|
23
|
+
* `aws lambda-microvms list-managed-microvm-images`. See `baseImageArn` for why this
|
|
24
|
+
* is a plain string.
|
|
25
|
+
*/
|
|
26
|
+
readonly baseImageVersion: string;
|
|
27
|
+
/**
|
|
28
|
+
* The shape catalogue to build one image per shape from (#723). Defaults to
|
|
29
|
+
* `MICROVM_SHAPES`, the committed catalogue. Overridable so a caller can
|
|
30
|
+
* synth a local multi-shape template without mutating that module
|
|
31
|
+
* constant.
|
|
32
|
+
*/
|
|
33
|
+
readonly shapes?: readonly MicrovmShape[];
|
|
34
|
+
/**
|
|
35
|
+
* The secret holding the shared HMAC key the controller verifies every
|
|
36
|
+
* doorbell against, under the field `DOORBELL_SECRET`. Any Secrets Manager
|
|
37
|
+
* secret, however the caller manages it — this construct has no opinion on
|
|
38
|
+
* SOPS, KMS, or any other production mechanism. Required (not defaulted
|
|
39
|
+
* here): a default would need a scope of its own, and a caller-owned
|
|
40
|
+
* default keeps its own logical id — the caller builds one exactly once,
|
|
41
|
+
* in its own scope, and passes the result in either case.
|
|
42
|
+
*/
|
|
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;
|
|
58
|
+
/**
|
|
59
|
+
* TCP port the image's hook server listens on, baked into both the image
|
|
60
|
+
* (`HOOKS_PORT` env var) and the `MicrovmImage`'s `hooks.port` — a mismatch
|
|
61
|
+
* means AWS probes a port nothing binds. Caller-supplied because the hook
|
|
62
|
+
* server is part of `imageSource`, which this construct does not build.
|
|
63
|
+
*/
|
|
64
|
+
readonly hooksPort: number;
|
|
65
|
+
/**
|
|
66
|
+
* Seconds AWS waits for a Run/Resume/Suspend/Terminate hook to answer,
|
|
67
|
+
* before the image build itself times out (`IMAGE_HOOK_TIMEOUT_SECONDS`,
|
|
68
|
+
* fixed at 600 s — building the snapshot is a different budget from
|
|
69
|
+
* running a hook).
|
|
70
|
+
*/
|
|
71
|
+
readonly hookTimeoutSeconds: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The customer-account half of the per-session runner (#558): the MicroVM
|
|
75
|
+
* image and the roles it builds and runs as, plus the stateless controller
|
|
76
|
+
* Lambda that turns a signed doorbell into RunMicrovm / ResumeMicrovm /
|
|
77
|
+
* SuspendMicrovm.
|
|
78
|
+
*/
|
|
79
|
+
export declare class EvidentMicrovmConstruct extends Construct {
|
|
80
|
+
/** POST target for Evident's signed doorbell (HMAC-verified in the handler). */
|
|
81
|
+
readonly functionUrl: string;
|
|
82
|
+
/** The shapes the controller can launch, with the image ARN each RunMicrovm uses. */
|
|
83
|
+
readonly shapeCatalogueJson: string;
|
|
84
|
+
/** Bucket a runner restores model credentials from: seed `<state_prefix>/claude/credentials.json` here. */
|
|
85
|
+
readonly durableStateBucket: s3.Bucket;
|
|
86
|
+
constructor(scope: Construct, id: string, props: EvidentMicrovmConstructProps);
|
|
87
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.EvidentMicrovmConstruct = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const cdk = __importStar(require("aws-cdk-lib"));
|
|
39
|
+
const constructs_1 = require("constructs");
|
|
40
|
+
const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
|
|
41
|
+
const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
|
|
42
|
+
const lambda_microvm_cdk_1 = require("@evident-ai/lambda-microvm-cdk");
|
|
43
|
+
const shapes_1 = require("./shapes");
|
|
44
|
+
// Image hooks build the snapshot, so they get minutes where the runtime hooks
|
|
45
|
+
// get AWS's 60 s.
|
|
46
|
+
const IMAGE_HOOK_TIMEOUT_SECONDS = 600;
|
|
47
|
+
// The field inside the doorbell secret, shared by this construct (which
|
|
48
|
+
// names it for the controller) and whatever produced `props.doorbellSecret`.
|
|
49
|
+
const DOORBELL_SECRET_KEY = 'DOORBELL_SECRET';
|
|
50
|
+
/**
|
|
51
|
+
* The customer-account half of the per-session runner (#558): the MicroVM
|
|
52
|
+
* image and the roles it builds and runs as, plus the stateless controller
|
|
53
|
+
* Lambda that turns a signed doorbell into RunMicrovm / ResumeMicrovm /
|
|
54
|
+
* SuspendMicrovm.
|
|
55
|
+
*/
|
|
56
|
+
class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
57
|
+
/** POST target for Evident's signed doorbell (HMAC-verified in the handler). */
|
|
58
|
+
functionUrl;
|
|
59
|
+
/** The shapes the controller can launch, with the image ARN each RunMicrovm uses. */
|
|
60
|
+
shapeCatalogueJson;
|
|
61
|
+
/** Bucket a runner restores model credentials from: seed `<state_prefix>/claude/credentials.json` here. */
|
|
62
|
+
durableStateBucket;
|
|
63
|
+
constructor(scope, id, props) {
|
|
64
|
+
super(scope, id);
|
|
65
|
+
// Synth-time failure, before any construct exists.
|
|
66
|
+
const shapes = (0, shapes_1.validateShapes)(props.shapes ?? shapes_1.MICROVM_SHAPES);
|
|
67
|
+
const [defaultShape, ...additionalShapes] = shapes;
|
|
68
|
+
// The model credentials a runner restores at /run, and the litestream
|
|
69
|
+
// session replica (it is `LITESTREAM_BUCKET` below), share one per-runner
|
|
70
|
+
// prefix chosen at /run — synth can't name it, and lifecycle filters take
|
|
71
|
+
// a literal prefix with no wildcards, so any rule here is bucket-wide.
|
|
72
|
+
// RETAIN: deleting the stack must not cost hand-bootstrapped logins.
|
|
73
|
+
// Versioned, plus the 7-day noncurrent-version expiry below: a clobbered
|
|
74
|
+
// credential is fixed by the user re-authenticating their provider, not a
|
|
75
|
+
// version restore, so the versions carry no credential-recovery duty —
|
|
76
|
+
// the window instead covers the operator accident-net for a manually
|
|
77
|
+
// deleted corrupt replica (`--on-unusable-replica=leave` never self-heals
|
|
78
|
+
// one; see README). No current-object expiration, deliberately — that
|
|
79
|
+
// would destroy both outright. Count-based retention
|
|
80
|
+
// (`noncurrentVersionsToRetain`) would reap nothing here instead: it's
|
|
81
|
+
// ANDed with the age condition and counts newer noncurrent versions of
|
|
82
|
+
// the SAME key, but litestream's LTX keys are txid-unique and churn by
|
|
83
|
+
// DELETE, so a churned key has exactly one noncurrent version and zero
|
|
84
|
+
// newer ones. `expiredObjectDeleteMarker` reaps the delete marker itself once
|
|
85
|
+
// its noncurrent version is gone — without it, the version index grows
|
|
86
|
+
// unboundedly, one marker per churned LTX key.
|
|
87
|
+
const durableState = new s3.Bucket(this, 'DurableState', {
|
|
88
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
89
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
90
|
+
versioned: true,
|
|
91
|
+
enforceSSL: true,
|
|
92
|
+
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
93
|
+
lifecycleRules: [
|
|
94
|
+
{
|
|
95
|
+
// Litestream uploads multipart; an abandoned upload is billed and is
|
|
96
|
+
// invisible to ListObjectsV2, so nothing else would ever reap it.
|
|
97
|
+
id: 'AbortIncompleteUploads',
|
|
98
|
+
abortIncompleteMultipartUploadAfter: cdk.Duration.days(7),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'ExpireNoncurrentVersions',
|
|
102
|
+
noncurrentVersionExpiration: cdk.Duration.days(7),
|
|
103
|
+
expiredObjectDeleteMarker: true,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
// The environment shared by every shape's image...
|
|
108
|
+
const imageEnvironment = {
|
|
109
|
+
HOOKS_PORT: String(props.hooksPort),
|
|
110
|
+
// The bucket is identical for every VM from this image version, so it
|
|
111
|
+
// may be baked. The PREFIX inside it may NOT: it selects one runner's
|
|
112
|
+
// credential store, so baking it would point every VM at the same one.
|
|
113
|
+
// It arrives per-VM in the /run payload — `hook-scripts.test.ts` and
|
|
114
|
+
// this construct's test both hold that line.
|
|
115
|
+
LITESTREAM_BUCKET: durableState.bucketName,
|
|
116
|
+
// No AWS_REGION here: the service rejects it as a reserved key
|
|
117
|
+
// ("Environment variable key 'AWS_REGION' is reserved", 400), which is
|
|
118
|
+
// also why the synchroniser's S3 client does not need it set — the
|
|
119
|
+
// MicroVM runtime supplies the region itself.
|
|
120
|
+
// Read by `evident run` itself (not the hooks), the same 24h window ECS
|
|
121
|
+
// already bakes in for its own runners (evident-scale-to-zero-construct.ts:191).
|
|
122
|
+
// Safe to bake: identical for every VM from this image version.
|
|
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 } : {}),
|
|
130
|
+
};
|
|
131
|
+
// ...and the hooks configuration every shape's image is built with.
|
|
132
|
+
const imageHooks = {
|
|
133
|
+
port: props.hooksPort,
|
|
134
|
+
lifecycleTimeout: cdk.Duration.seconds(props.hookTimeoutSeconds),
|
|
135
|
+
buildTimeout: cdk.Duration.seconds(IMAGE_HOOK_TIMEOUT_SECONDS),
|
|
136
|
+
};
|
|
137
|
+
// Built first and with no `executionRole` prop, exactly as before this
|
|
138
|
+
// shape catalogue existed: same construct id (`Image`, frozen on the
|
|
139
|
+
// shape record — see shapes.ts), same `imageName`, same description. The
|
|
140
|
+
// construct therefore creates `Image/ExecutionRole` at its current path
|
|
141
|
+
// and current logical id — zero CloudFormation churn on the live,
|
|
142
|
+
// hand-deployed stack (D3/D9). Every additional shape below borrows this
|
|
143
|
+
// role rather than getting one of its own, which is why `validateShapes`
|
|
144
|
+
// requires the default shape to be `shapes[0]`: it must exist before any
|
|
145
|
+
// other shape can reference its execution role.
|
|
146
|
+
const defaultImage = new lambda_microvm_cdk_1.MicrovmImage(this, defaultShape.constructId, {
|
|
147
|
+
imageName: defaultShape.imageName,
|
|
148
|
+
description: defaultShape.description,
|
|
149
|
+
// A local directory, so the zip is uploaded by `cdk deploy` itself and the
|
|
150
|
+
// image ARN is a stack reference rather than a deploy-time input.
|
|
151
|
+
source: props.imageSource,
|
|
152
|
+
baseImageArn: props.baseImageArn,
|
|
153
|
+
baseImageVersion: props.baseImageVersion,
|
|
154
|
+
memoryMiB: defaultShape.memoryMiB,
|
|
155
|
+
environment: imageEnvironment,
|
|
156
|
+
hooks: imageHooks,
|
|
157
|
+
});
|
|
158
|
+
// WART, accepted deliberately: every additional shape's image is built
|
|
159
|
+
// with `defaultImage.executionRole`, so this role's construct path
|
|
160
|
+
// (`Image/ExecutionRole`) reads as if it belongs to just the default
|
|
161
|
+
// shape's image, when in fact it is shared by every shape. That is
|
|
162
|
+
// intentional — see D3 in the plan. Do NOT "fix" this with
|
|
163
|
+
// `overrideLogicalId`: renaming the logical id IS the CloudFormation
|
|
164
|
+
// churn this design exists to avoid (a delete-and-recreate of the role a
|
|
165
|
+
// running MicroVM has live session credentials from). The wart is one
|
|
166
|
+
// comment; the override would undo the point of D3.
|
|
167
|
+
const images = new Map([[defaultShape.name, defaultImage]]);
|
|
168
|
+
for (const shape of additionalShapes) {
|
|
169
|
+
images.set(shape.name, new lambda_microvm_cdk_1.MicrovmImage(this, shape.constructId, {
|
|
170
|
+
imageName: shape.imageName,
|
|
171
|
+
description: shape.description,
|
|
172
|
+
source: props.imageSource,
|
|
173
|
+
baseImageArn: props.baseImageArn,
|
|
174
|
+
baseImageVersion: props.baseImageVersion,
|
|
175
|
+
memoryMiB: shape.memoryMiB,
|
|
176
|
+
executionRole: defaultImage.executionRole,
|
|
177
|
+
environment: imageEnvironment,
|
|
178
|
+
hooks: imageHooks,
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
// Bucket-wide rather than per-prefix (as the ECS runner's task role is):
|
|
182
|
+
// the prefix arrives at /run, so it cannot be named at synth, and one
|
|
183
|
+
// execution role is shared by every VM of every shape (D3). So a VM can
|
|
184
|
+
// read another session's credential store — acceptable within a single
|
|
185
|
+
// customer account, and what Phase 4 (#263) has to tighten when this goes
|
|
186
|
+
// multi-tenant.
|
|
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);
|
|
190
|
+
// The shape catalogue the controller resolves a doorbell's `shape` field
|
|
191
|
+
// against, and advertises over the describe channel (#723). One JSON
|
|
192
|
+
// array, built with `Stack.toJsonString` — not `JSON.stringify` — because
|
|
193
|
+
// each `image_arn` is an unresolved `Fn::GetAtt` token at synth time, and
|
|
194
|
+
// `toJsonString` is the API that resolves tokens embedded in a JSON
|
|
195
|
+
// string (it emits an `Fn::Join`). `Stack.of(this)`, not `this`, because
|
|
196
|
+
// this class is a `Construct`, not a `Stack` — `toJsonString` is Stack-only.
|
|
197
|
+
const shapeCatalogueJson = cdk.Stack.of(this).toJsonString(shapes.map((shape) => ({
|
|
198
|
+
name: shape.name,
|
|
199
|
+
title: shape.title,
|
|
200
|
+
description: shape.description,
|
|
201
|
+
memory_mib: shape.memoryMiB,
|
|
202
|
+
image_arn: images.get(shape.name).imageArn,
|
|
203
|
+
is_default: shape.isDefault,
|
|
204
|
+
})));
|
|
205
|
+
const controller = new lambda.Function(this, 'Controller', {
|
|
206
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
207
|
+
// Pre-bundled at PACKAGE build time (`pnpm --filter @evident-ai/runner-cdk
|
|
208
|
+
// build`, see scripts/build.ts), not at synth time — see the waker's
|
|
209
|
+
// `construct.ts` for why (`NodejsFunction` can't ship in a published
|
|
210
|
+
// construct: its entry file isn't in the tarball, and bundling would
|
|
211
|
+
// need esbuild on the consumer's synth machine). Resolved from the
|
|
212
|
+
// PACKAGE ROOT, two directories below `__dirname` here either compiled
|
|
213
|
+
// (`dist/microvm`) or via ts-node (`src/microvm`) — same as the waker.
|
|
214
|
+
code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'dist', 'controller-lambda')),
|
|
215
|
+
handler: 'handler.handler',
|
|
216
|
+
// 128 MB is a slow cold start for a bundled SDK client on a path with a
|
|
217
|
+
// hard client deadline.
|
|
218
|
+
memorySize: 256,
|
|
219
|
+
// Deliberately BELOW Evident's WEBHOOK_DELIVERY_TIMEOUT_MS (10 s): the
|
|
220
|
+
// SUSPENDING poll can take ~2 s, and the 409 it produces is only useful if
|
|
221
|
+
// it reaches Evident before the client aborts. Do not "align" this to 10 s.
|
|
222
|
+
timeout: cdk.Duration.seconds(8),
|
|
223
|
+
environment: {
|
|
224
|
+
// The ARN and field name, never the value: the controller reads it from
|
|
225
|
+
// Secrets Manager at runtime, so the key is not exposed to anyone with
|
|
226
|
+
// lambda:GetFunctionConfiguration.
|
|
227
|
+
DOORBELL_SECRET_ARN: props.doorbellSecret.secretArn,
|
|
228
|
+
DOORBELL_SECRET_KEY: DOORBELL_SECRET_KEY,
|
|
229
|
+
MICROVM_SHAPES: shapeCatalogueJson,
|
|
230
|
+
// The role every shape's MicroVM shares (D3) — still one role, still
|
|
231
|
+
// this same expression, now documented as shared rather than singular.
|
|
232
|
+
MICROVM_EXECUTION_ROLE_ARN: defaultImage.executionRole.roleArn,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
// The four control actions are granted on '*', the PassNetworkConnector
|
|
236
|
+
// resource is a partition/region wildcard, and PassRole names the
|
|
237
|
+
// execution role every shape now shares (D3) — so nothing in this grant
|
|
238
|
+
// is image-specific (V4), and one call on the default shape's image
|
|
239
|
+
// authorises the controller for every shape's MicroVM. N calls would
|
|
240
|
+
// emit N near-identical policy statements and break the "exactly one
|
|
241
|
+
// statement granting X" guarantees this construct's test encodes.
|
|
242
|
+
defaultImage.grantRun(controller);
|
|
243
|
+
props.doorbellSecret.grantRead(controller);
|
|
244
|
+
const functionUrl = controller.addFunctionUrl({
|
|
245
|
+
// HMAC verification of the doorbell body IS the auth, as for the ECS waker.
|
|
246
|
+
authType: lambda.FunctionUrlAuthType.NONE,
|
|
247
|
+
});
|
|
248
|
+
this.functionUrl = functionUrl.url;
|
|
249
|
+
this.shapeCatalogueJson = shapeCatalogueJson;
|
|
250
|
+
this.durableStateBucket = durableState;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
exports.EvidentMicrovmConstruct = EvidentMicrovmConstruct;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The doorbell Evident POSTs to the controller's Function URL. `run_payload` is
|
|
3
|
+
* OPAQUE: it carries the runner key and whatever else the MicroVM's `/run` hook
|
|
4
|
+
* needs, and the controller only ever re-serialises it into `runHookPayload`. It
|
|
5
|
+
* is never parsed, never logged and never echoed — that is what makes the
|
|
6
|
+
* controller credential-free.
|
|
7
|
+
*
|
|
8
|
+
* `occurred_at` is required because it is the per-event half of the `RunMicrovm`
|
|
9
|
+
* `clientToken` (see `clientTokenFor`): a token derived from `runner_id` alone
|
|
10
|
+
* would be stable for the runner's whole life, so a post-termination wake would
|
|
11
|
+
* replay the dead VM's cached response and start nothing.
|
|
12
|
+
*
|
|
13
|
+
* A third arm, `runner.shapes_requested`, is a describe request rather than an
|
|
14
|
+
* event: it has no runner and no event instant, so it is parsed BEFORE the
|
|
15
|
+
* `runner_id`/`occurred_at` guards below. Its staleness is zero by construction
|
|
16
|
+
* — the response renders from the same deploy-time environment variable
|
|
17
|
+
* CloudFormation writes when it builds the images, so there is no second source
|
|
18
|
+
* to drift from. Replay is deliberately undefended: it is a pure read with no
|
|
19
|
+
* side effect, and its response carries only deploy-time metadata (shape names,
|
|
20
|
+
* titles, descriptions, memory) that is already public to the operator, so a
|
|
21
|
+
* replayed request yields nothing to gain. It is not defenceless — no response
|
|
22
|
+
* is produced without a valid HMAC over the exact body. ⚠️ Any future
|
|
23
|
+
* STATE-CHANGING request type added to this channel needs real replay defence
|
|
24
|
+
* (a timestamp window or a nonce); this reasoning does not extend to one.
|
|
25
|
+
*/
|
|
26
|
+
declare const WAKE_EVENT_TYPES: readonly ["agent.message_queued", "agent.wake_requested"];
|
|
27
|
+
declare const SUSPEND_EVENT_TYPE = "runner.suspend_requested";
|
|
28
|
+
export declare const SHAPES_EVENT_TYPE = "runner.shapes_requested";
|
|
29
|
+
type WakeEventType = (typeof WAKE_EVENT_TYPES)[number];
|
|
30
|
+
type DoorbellFields = {
|
|
31
|
+
runnerId: string;
|
|
32
|
+
occurredAt: string;
|
|
33
|
+
microvmId?: string;
|
|
34
|
+
};
|
|
35
|
+
export type WakeDoorbell = DoorbellFields & {
|
|
36
|
+
type: WakeEventType;
|
|
37
|
+
runHookPayload: string;
|
|
38
|
+
/**
|
|
39
|
+
* The shape a doorbell asks for, by name. `undefined` means "the default
|
|
40
|
+
* shape" — the controller resolves it against the shape catalogue.
|
|
41
|
+
*/
|
|
42
|
+
shape?: string;
|
|
43
|
+
};
|
|
44
|
+
export type Doorbell = WakeDoorbell | (DoorbellFields & {
|
|
45
|
+
type: typeof SUSPEND_EVENT_TYPE;
|
|
46
|
+
}) | {
|
|
47
|
+
type: typeof SHAPES_EVENT_TYPE;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Rejection reasons. Every one is a fixed constant: it goes into both the
|
|
51
|
+
* response body (which Evident persists) and the log line, so it must never
|
|
52
|
+
* contain anything derived from the request.
|
|
53
|
+
*/
|
|
54
|
+
export declare const DOORBELL_REJECTION: {
|
|
55
|
+
readonly invalidJson: "invalid_json";
|
|
56
|
+
readonly missingRunnerId: "missing_runner_id";
|
|
57
|
+
readonly missingOccurredAt: "missing_occurred_at";
|
|
58
|
+
readonly unsupportedType: "unsupported_event_type";
|
|
59
|
+
readonly invalidMicrovmId: "invalid_microvm_id";
|
|
60
|
+
readonly invalidShape: "invalid_shape";
|
|
61
|
+
readonly missingRunPayload: "missing_run_payload";
|
|
62
|
+
readonly runPayloadTooLarge: "run_payload_too_large";
|
|
63
|
+
};
|
|
64
|
+
export type DoorbellRejection = (typeof DOORBELL_REJECTION)[keyof typeof DOORBELL_REJECTION];
|
|
65
|
+
export type ParsedDoorbell = {
|
|
66
|
+
ok: true;
|
|
67
|
+
doorbell: Doorbell;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
reason: DoorbellRejection;
|
|
71
|
+
};
|
|
72
|
+
export declare function parseDoorbell(rawBody: string): ParsedDoorbell;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DOORBELL_REJECTION = exports.SHAPES_EVENT_TYPE = void 0;
|
|
4
|
+
exports.parseDoorbell = parseDoorbell;
|
|
5
|
+
const constants_1 = require("../constants");
|
|
6
|
+
/**
|
|
7
|
+
* The doorbell Evident POSTs to the controller's Function URL. `run_payload` is
|
|
8
|
+
* OPAQUE: it carries the runner key and whatever else the MicroVM's `/run` hook
|
|
9
|
+
* needs, and the controller only ever re-serialises it into `runHookPayload`. It
|
|
10
|
+
* is never parsed, never logged and never echoed — that is what makes the
|
|
11
|
+
* controller credential-free.
|
|
12
|
+
*
|
|
13
|
+
* `occurred_at` is required because it is the per-event half of the `RunMicrovm`
|
|
14
|
+
* `clientToken` (see `clientTokenFor`): a token derived from `runner_id` alone
|
|
15
|
+
* would be stable for the runner's whole life, so a post-termination wake would
|
|
16
|
+
* replay the dead VM's cached response and start nothing.
|
|
17
|
+
*
|
|
18
|
+
* A third arm, `runner.shapes_requested`, is a describe request rather than an
|
|
19
|
+
* event: it has no runner and no event instant, so it is parsed BEFORE the
|
|
20
|
+
* `runner_id`/`occurred_at` guards below. Its staleness is zero by construction
|
|
21
|
+
* — the response renders from the same deploy-time environment variable
|
|
22
|
+
* CloudFormation writes when it builds the images, so there is no second source
|
|
23
|
+
* to drift from. Replay is deliberately undefended: it is a pure read with no
|
|
24
|
+
* side effect, and its response carries only deploy-time metadata (shape names,
|
|
25
|
+
* titles, descriptions, memory) that is already public to the operator, so a
|
|
26
|
+
* replayed request yields nothing to gain. It is not defenceless — no response
|
|
27
|
+
* is produced without a valid HMAC over the exact body. ⚠️ Any future
|
|
28
|
+
* STATE-CHANGING request type added to this channel needs real replay defence
|
|
29
|
+
* (a timestamp window or a nonce); this reasoning does not extend to one.
|
|
30
|
+
*/
|
|
31
|
+
const WAKE_EVENT_TYPES = ['agent.message_queued', 'agent.wake_requested'];
|
|
32
|
+
const SUSPEND_EVENT_TYPE = 'runner.suspend_requested';
|
|
33
|
+
exports.SHAPES_EVENT_TYPE = 'runner.shapes_requested';
|
|
34
|
+
/**
|
|
35
|
+
* Rejection reasons. Every one is a fixed constant: it goes into both the
|
|
36
|
+
* response body (which Evident persists) and the log line, so it must never
|
|
37
|
+
* contain anything derived from the request.
|
|
38
|
+
*/
|
|
39
|
+
exports.DOORBELL_REJECTION = {
|
|
40
|
+
invalidJson: 'invalid_json',
|
|
41
|
+
missingRunnerId: 'missing_runner_id',
|
|
42
|
+
missingOccurredAt: 'missing_occurred_at',
|
|
43
|
+
unsupportedType: 'unsupported_event_type',
|
|
44
|
+
invalidMicrovmId: 'invalid_microvm_id',
|
|
45
|
+
invalidShape: 'invalid_shape',
|
|
46
|
+
missingRunPayload: 'missing_run_payload',
|
|
47
|
+
runPayloadTooLarge: 'run_payload_too_large',
|
|
48
|
+
};
|
|
49
|
+
function nonBlankString(value) {
|
|
50
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
51
|
+
}
|
|
52
|
+
function isWakeType(value) {
|
|
53
|
+
return WAKE_EVENT_TYPES.includes(value);
|
|
54
|
+
}
|
|
55
|
+
function parseDoorbell(rawBody) {
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = JSON.parse(rawBody);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { ok: false, reason: exports.DOORBELL_REJECTION.invalidJson };
|
|
62
|
+
}
|
|
63
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
64
|
+
return { ok: false, reason: exports.DOORBELL_REJECTION.invalidJson };
|
|
65
|
+
}
|
|
66
|
+
const { type, runner_id: runnerId, occurred_at: occurredAt, microvm_id: microvmId, run_payload: runPayload, shape, } = body;
|
|
67
|
+
if (type !== SUSPEND_EVENT_TYPE && type !== exports.SHAPES_EVENT_TYPE && !isWakeType(type)) {
|
|
68
|
+
return { ok: false, reason: exports.DOORBELL_REJECTION.unsupportedType };
|
|
69
|
+
}
|
|
70
|
+
// A describe request has no runner and no event instant — resolve it here,
|
|
71
|
+
// before the runner_id/occurred_at guards below, or it could never satisfy them.
|
|
72
|
+
if (type === exports.SHAPES_EVENT_TYPE) {
|
|
73
|
+
return { ok: true, doorbell: { type } };
|
|
74
|
+
}
|
|
75
|
+
if (!nonBlankString(runnerId)) {
|
|
76
|
+
return { ok: false, reason: exports.DOORBELL_REJECTION.missingRunnerId };
|
|
77
|
+
}
|
|
78
|
+
if (!nonBlankString(occurredAt)) {
|
|
79
|
+
return { ok: false, reason: exports.DOORBELL_REJECTION.missingOccurredAt };
|
|
80
|
+
}
|
|
81
|
+
if (microvmId !== undefined && !nonBlankString(microvmId)) {
|
|
82
|
+
return { ok: false, reason: exports.DOORBELL_REJECTION.invalidMicrovmId };
|
|
83
|
+
}
|
|
84
|
+
const fields = { runnerId, occurredAt, microvmId };
|
|
85
|
+
if (type === SUSPEND_EVENT_TYPE) {
|
|
86
|
+
return { ok: true, doorbell: { ...fields, type } };
|
|
87
|
+
}
|
|
88
|
+
if (shape !== undefined && !nonBlankString(shape)) {
|
|
89
|
+
return { ok: false, reason: exports.DOORBELL_REJECTION.invalidShape };
|
|
90
|
+
}
|
|
91
|
+
if (runPayload === undefined || runPayload === null) {
|
|
92
|
+
return { ok: false, reason: exports.DOORBELL_REJECTION.missingRunPayload };
|
|
93
|
+
}
|
|
94
|
+
const runHookPayload = JSON.stringify(runPayload);
|
|
95
|
+
if (Buffer.byteLength(runHookPayload, 'utf8') > constants_1.RUN_HOOK_PAYLOAD_MAX_BYTES) {
|
|
96
|
+
return { ok: false, reason: exports.DOORBELL_REJECTION.runPayloadTooLarge };
|
|
97
|
+
}
|
|
98
|
+
// Omit `shape` entirely when absent, rather than setting it to `undefined`,
|
|
99
|
+
// so a body with no shape parses to exactly the same object as before this
|
|
100
|
+
// field existed.
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
doorbell: shape === undefined
|
|
104
|
+
? { ...fields, type, runHookPayload }
|
|
105
|
+
: { ...fields, type, shape, runHookPayload },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MicrovmClient } from './microvm-client';
|
|
2
|
+
import type { ShapeCatalogue } from './shape-catalogue';
|
|
3
|
+
import { type Timing } from './throttle-retry';
|
|
4
|
+
/**
|
|
5
|
+
* The controller's pure decision core: verify the HMAC over the raw body, parse
|
|
6
|
+
* the doorbell, then map the MicroVM's current state to exactly one lifecycle
|
|
7
|
+
* call. Everything with I/O is injected, so the whole tree is unit-testable.
|
|
8
|
+
*
|
|
9
|
+
* The response body NEVER contains anything derived from `run_payload` — Evident
|
|
10
|
+
* persists it as `response_body`.
|
|
11
|
+
*/
|
|
12
|
+
type ActionKind = 'none' | 'run' | 'resume' | 'suspend' | 'rejected' | 'describe';
|
|
13
|
+
export type DoorbellResult = {
|
|
14
|
+
statusCode: number;
|
|
15
|
+
body: string;
|
|
16
|
+
action: {
|
|
17
|
+
kind: ActionKind;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function handleDoorbell({ rawBody, signatureHeader, doorbellSecret, shapes, microvm, sleep, random, }: {
|
|
21
|
+
rawBody: string;
|
|
22
|
+
signatureHeader: string | undefined;
|
|
23
|
+
doorbellSecret: string;
|
|
24
|
+
shapes: ShapeCatalogue;
|
|
25
|
+
microvm: MicrovmClient;
|
|
26
|
+
} & Timing): Promise<DoorbellResult>;
|
|
27
|
+
export {};
|