@huaweidevtools/aishell-dsh-e2b 0.0.0-stage → 0.0.1-beta.4

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.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The connection to the sandbox gateway.
3
+ *
4
+ * One `Sandbox` handle is built per sandbox id and reused, because the SDK's
5
+ * instance holds the resolved endpoints and per-sandbox tokens that the create
6
+ * response supplied. Rebuilding one per call would mean either reconnecting
7
+ * (an extra control-plane round trip) or, worse, constructing a handle from
8
+ * just an id — which works but silently drops the tokens the sandbox was
9
+ * actually issued, turning an authentication problem into "port is not open".
10
+ *
11
+ * ## What the SDK is configured with
12
+ *
13
+ * `domain` alone. The SDK composes `https://api.<domain>` for the control
14
+ * plane and `https://<port>-<sandboxId>.<domain>` for the data plane, and
15
+ * reads the key from `apiKey`. That is the whole configuration, which is the
16
+ * point of the gateway: a stock E2B client reaches it with two settings and no
17
+ * patched code.
18
+ *
19
+ * @module @huaweidevtools/aishell-dsh-e2b/client
20
+ */
21
+ import { Sandbox } from 'e2b';
22
+ import type { SandboxKeySource } from './sandbox-key.ts';
23
+ /**
24
+ * The template to start from when the caller names none.
25
+ *
26
+ * Not the SDK's own default. The SDK resolves an omitted template to `"base"`,
27
+ * a name from the hosted service; this gateway's templates are its own, and it
28
+ * answers `"base"` with `404 sandbox unknown was not found` — a message that
29
+ * reads like a missing sandbox rather than an unknown template, which is what
30
+ * makes it worth pinning here rather than discovering at the first create.
31
+ *
32
+ * `"default"` is what this gateway provisions.
33
+ */
34
+ export declare const DEFAULT_TEMPLATE = "default";
35
+ /** How the connection is parameterised; every field is read per call. */
36
+ export interface SandboxClientOptions {
37
+ /** The gateway's domain, e.g. `aishell.openbuildlab.com`. */
38
+ readonly resolveDomain: () => string;
39
+ /** The key source, which caches and refreshes the API key. */
40
+ readonly keys: SandboxKeySource;
41
+ /** Per-request timeout for SDK control-plane calls. */
42
+ readonly resolveRequestTimeoutMs: () => number;
43
+ }
44
+ /** Builds and caches SDK handles, one per sandbox id. */
45
+ export declare class SandboxClient {
46
+ private readonly options;
47
+ /** Sandbox id → the live handle for it. */
48
+ private readonly handles;
49
+ constructor(options: SandboxClientOptions);
50
+ /**
51
+ * The live handle for one sandbox.
52
+ *
53
+ * @param sandboxId - the sandbox to address.
54
+ * @returns the cached handle, or a newly connected one.
55
+ */
56
+ handle(sandboxId: string): Promise<Sandbox>;
57
+ /**
58
+ * Create a sandbox and cache its handle.
59
+ *
60
+ * @param template - the template name or id; {@link DEFAULT_TEMPLATE} when
61
+ * omitted.
62
+ * @param options - create options forwarded to the SDK.
63
+ * @returns the created sandbox handle.
64
+ */
65
+ create(template: string | undefined, options: {
66
+ readonly timeoutMs?: number;
67
+ readonly envs?: Record<string, string>;
68
+ readonly metadata?: Record<string, string>;
69
+ readonly signal?: AbortSignal;
70
+ }): Promise<Sandbox>;
71
+ /**
72
+ * Find the sandboxes carrying one metadata pair.
73
+ *
74
+ * The SDK encodes the pair into the `metadata` query parameter and the
75
+ * gateway forwards it to the upstream unchanged, so this is the mechanism a
76
+ * restarted process uses to rediscover the sandbox a conversation was
77
+ * working in.
78
+ *
79
+ * The search follows the pagination cursor. A single page would be enough
80
+ * for the common case, but not for a busy account: the gateway pages at 100
81
+ * and reports more with a cursor, and a conversation's own sandbox is not
82
+ * ordered ahead of the others — so on the day an account holds more than a
83
+ * page, the one sandbox the caller is looking for is as likely as any to be
84
+ * on a later page, and a single-page search would report "none".
85
+ *
86
+ * @param metadata - the pair to match; a sandbox must carry all of it.
87
+ * @param signal - caller cancellation.
88
+ * @returns one row per matching sandbox.
89
+ */
90
+ findByMetadata(metadata: Record<string, string>, signal: AbortSignal | undefined): Promise<Array<{
91
+ sandboxId: string;
92
+ state: string;
93
+ }>>;
94
+ /**
95
+ * List the sandboxes this account currently has.
96
+ *
97
+ * Follows the pagination cursor, for the same reason the metadata search
98
+ * does: the gateway pages at 100, and a caller asking what it has should not
99
+ * be told "these are all of them" when they are the first hundred.
100
+ *
101
+ * @param signal - caller cancellation.
102
+ * @returns one row per sandbox, in the shape the tool renders.
103
+ */
104
+ list(signal: AbortSignal | undefined): Promise<Array<{
105
+ sandboxId: string;
106
+ templateId: string;
107
+ state: string;
108
+ startedAt: string;
109
+ endAt: string;
110
+ }>>;
111
+ /**
112
+ * Drop a handle for a sandbox that no longer exists.
113
+ *
114
+ * Called after a kill, so a later call against the same id re-resolves
115
+ * rather than reusing an endpoint the gateway has already torn down.
116
+ *
117
+ * @param sandboxId - the sandbox whose handle to drop.
118
+ */
119
+ forget(sandboxId: string): void;
120
+ /** Connect to an existing sandbox. */
121
+ private connect;
122
+ /**
123
+ * Run one SDK call, retrying once against a freshly fetched key when the
124
+ * gateway rejects the one in hand.
125
+ *
126
+ * The key is handed to the callback rather than captured by it. A callback
127
+ * that closed over the first key would "retry" by presenting that same
128
+ * rejected key again — a loop that looks like a refresh and is not one.
129
+ *
130
+ * The retry is deliberately narrow: only an authentication failure clears
131
+ * the cache, and only once. A key that fails for any other reason — a
132
+ * sandbox that does not exist, a rate limit — is not evidence about the key,
133
+ * and refreshing on it would rotate the tenant for no reason.
134
+ */
135
+ private runWithKeyRefresh;
136
+ }
137
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Reading the operator's Huawei Cloud credentials for control-plane requests.
3
+ *
4
+ * The read is delegated to `credentials-store/accounts`, which is now the one
5
+ * place the account names and the pairing rule live. Every plugin in this set
6
+ * that signs with the same key pair goes through it — this module, the LLM
7
+ * adapter, the credential form's Host route and the ops tools — so they cannot
8
+ * disagree about which keyring entries are the credentials or about what
9
+ * "half a pair" means.
10
+ *
11
+ * It used to say here that the literals were re-declared because
12
+ * `aishell-credentials` kept them unexported and the other packages each had
13
+ * their own copy. That was an accurate description of a real hazard, not a
14
+ * reason to keep it: a divergence in these names would not fail a build, it
15
+ * would surface as one plugin working and another reporting "not
16
+ * authenticated" on a machine where the operator had stored exactly one pair.
17
+ *
18
+ * @module @huaweidevtools/aishell-dsh-e2b/credentials
19
+ */
20
+ import { type CredentialsStore } from '@huaweidevtools/aishell-dsh-credentials-store';
21
+ import type { SignCredentials } from '@huaweidevtools/aishell-dsh-credentials-store/signer';
22
+ /**
23
+ * Open a credential store.
24
+ *
25
+ * Takes no options: the service name is fixed in the store package, because a
26
+ * per-plugin choice of namespace is how a writer and a reader end up looking at
27
+ * different stores.
28
+ */
29
+ export declare function openCredentialStore(): CredentialsStore;
30
+ /**
31
+ * Read the current credentials.
32
+ *
33
+ * AK and SK are read as a pair: half a pair reads as no credentials rather
34
+ * than an error, so a partially-configured machine gets the "authenticate
35
+ * first" message instead of a signing failure with no remedy. The
36
+ * SecurityToken is optional and is omitted from the object entirely when
37
+ * absent — an empty string would sign as a third header the gateway then
38
+ * rejects.
39
+ *
40
+ * Called per request, never captured: that is what makes a credential saved
41
+ * from the credential form apply to the next call.
42
+ *
43
+ * @param store - the credential store to read.
44
+ * @returns the credentials, or `undefined` when the pair is incomplete.
45
+ */
46
+ export declare function readCredentials(store: CredentialsStore): SignCredentials | undefined;
47
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Turning SDK failures into messages the model can act on.
3
+ *
4
+ * Two properties decide the shape here.
5
+ *
6
+ * **A non-zero exit is not an error.** The SDK's foreground `commands.run`
7
+ * rejects with `CommandExitError` when a command exits non-zero, carrying the
8
+ * exit code and both streams. Reporting that as a tool failure tells the model
9
+ * "the tool broke" when the truth is "the command ran and returned 1" — which
10
+ * for a build, a test run, or a `grep` with no match is the ordinary outcome.
11
+ * Callers convert it to a normal result instead; see `tools.ts`.
12
+ *
13
+ * **The error hierarchy does not share a root.** `AuthenticationError`,
14
+ * `BuildError` and `VolumeError` do not extend `SandboxError`, so matching on
15
+ * `SandboxError` alone would let exactly the failures with remedies fall
16
+ * through to a generic message. Matching is therefore by constructor name,
17
+ * which is stable across the SDK's own re-bundling and needs no import of a
18
+ * class the package may rename.
19
+ *
20
+ * @module @huaweidevtools/aishell-dsh-e2b/errors
21
+ */
22
+ /** A failure whose text is already the message the model should see. */
23
+ export declare class SandboxToolError extends Error {
24
+ constructor(message: string);
25
+ }
26
+ /**
27
+ * Render one thrown value as the message a tool should fail with.
28
+ *
29
+ * @param error - whatever was thrown.
30
+ * @param context - what the call was doing, prefixed for orientation.
31
+ * @returns the message to surface.
32
+ */
33
+ export declare function describeSandboxError(error: unknown, context: string): string;
34
+ /** The constructor name of a thrown value, or `''` for a non-object. */
35
+ export declare function errorName(error: unknown): string;
36
+ /**
37
+ * Whether a thrown value is a command that ran and exited non-zero.
38
+ *
39
+ * Kept separate from {@link describeSandboxError} because it is not a failure:
40
+ * see the module comment.
41
+ */
42
+ export declare function isCommandExitError(error: unknown): error is {
43
+ exitCode: number;
44
+ stdout: string;
45
+ stderr: string;
46
+ };
47
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The E2B sandbox plugin: a second, disposable execution tier for DSH.
3
+ *
4
+ * ## What problem this solves
5
+ *
6
+ * Every deployment request currently reaches the same path — the Huawei Cloud
7
+ * operations server, provisioning real resources with real cost, real
8
+ * credentials, and real cleanup obligations. A large fraction of requests do
9
+ * not need that: a static page, a small service, something to look at once.
10
+ * Those need a place to run and an address to open, and nothing that outlives
11
+ * the task.
12
+ *
13
+ * This plugin adds that tier. The model chooses between them, which is why it
14
+ * also contributes a prompt section saying when each is appropriate: tools
15
+ * alone would leave the choice to guesswork.
16
+ *
17
+ * ## What it is not
18
+ *
19
+ * It does not replace the harness's execution world. The official
20
+ * `@deepseek-ai/dsh-e2b` package does that — it swaps `ctx.fs` and
21
+ * `ctx.subprocess` so every file and command the agent touches runs remotely.
22
+ * This plugin instead adds *tools*, so the agent keeps working locally and
23
+ * reaches a sandbox only when it decides to. The two can be mounted together;
24
+ * only the names look alike.
25
+ *
26
+ * ## How it reaches the service
27
+ *
28
+ * The key comes from the AI Shell provisioning service, signed with the
29
+ * operator's stored Huawei Cloud credentials; the sandbox protocol itself is
30
+ * spoken by the official `e2b` client library, pointed at the gateway's domain.
31
+ * See `sandbox-key.ts` and `client.ts`.
32
+ *
33
+ * @module @huaweidevtools/aishell-dsh-e2b
34
+ */
35
+ import type { Context, Volatile } from '@deepseek-ai/cordis';
36
+ import z from '@deepseek-ai/schemastery';
37
+ /** Plugin config. */
38
+ export interface Config {
39
+ /** Base URL of the sandbox key provisioning service. */
40
+ readonly apiBaseUrl: Volatile<string>;
41
+ /** The sandbox gateway's domain, e.g. `aishell.openbuildlab.com`. */
42
+ readonly domain: Volatile<string>;
43
+ /** Per-request timeout for the key fetch and for SDK control-plane calls. */
44
+ readonly requestTimeoutMs: Volatile<number>;
45
+ }
46
+ /**
47
+ * Settings schema for this entry.
48
+ *
49
+ * Both URLs are Volatile: they are deployment facts that differ between a
50
+ * staging and a production profile, and a change should apply to the next call
51
+ * rather than requiring a restart.
52
+ */
53
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
54
+ apiBaseUrl: z<string, string, "volatile-defined">;
55
+ domain: z<string, string, "volatile-defined">;
56
+ requestTimeoutMs: z<number, number, "volatile-defined">;
57
+ }>>, Schemastery.ObjectT<NoInfer<{
58
+ apiBaseUrl: z<string, string, "volatile-defined">;
59
+ domain: z<string, string, "volatile-defined">;
60
+ requestTimeoutMs: z<number, number, "volatile-defined">;
61
+ }>>, "plain">;
62
+ export { SANDBOX_GUIDANCE, SANDBOX_PROMPT_ORDER, SANDBOX_SECTION_NAME } from './prompt.ts';
63
+ export declare const name = "aishell-e2b";
64
+ /**
65
+ * Required services: the tool registry alone.
66
+ *
67
+ * The filesystem and the system prompt are both optional and taken through
68
+ * `ctx.inject` / `ctx.get` — a deployment without a filesystem loses one
69
+ * tool's destination, and one without a system prompt loses the guidance
70
+ * section, neither of which should stop the sandbox tools from registering.
71
+ */
72
+ export declare const inject: string[];
73
+ export declare function apply(ctx: Context, config: Config): void;
74
+ declare module '@deepseek-ai/cordis' {
75
+ interface Events {
76
+ /**
77
+ * The stored Huawei Cloud credentials changed — a verified save, or a
78
+ * clear. Emitted by `aishell-credentials`, which owns the credential form
79
+ * and the store it writes.
80
+ *
81
+ * Declared here as well as on the emitting side because the two packages
82
+ * share no import: this is the whole interface between them. A listener
83
+ * reads the store itself; the event carries no value, so a secret can
84
+ * never ride on it.
85
+ *
86
+ * @mode on
87
+ */
88
+ 'aishell/credentials-updated'(): void;
89
+ }
90
+ }
91
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * What the model reads about which tier to use.
3
+ *
4
+ * ## Why this is its own module
5
+ *
6
+ * The text and the mechanism that delivers it belong together. Split across
7
+ * `index.ts` — the string one place, the `systemPrompt.section()` call another —
8
+ * the only way to see what the model actually reads is to read both. Here they
9
+ * sit side by side, and `apply()` just calls {@link registerSandboxGuidance}.
10
+ *
11
+ * ## Why the split is stated three ways
12
+ *
13
+ * An earlier version drew the line at lifecycle alone: "does the result need to
14
+ * outlive the task?" That is the property that actually decides the tier, but it
15
+ * is one the model has to *infer* about work that has not been described in
16
+ * those terms yet.
17
+ *
18
+ * Two things make it answerable instead:
19
+ *
20
+ * - **What the user asked for.** "A web page or a demo" versus "a production
21
+ * system" is a distinction a person can answer directly.
22
+ * - **What the work needs.** The one capability a sandbox has that this
23
+ * machine does not is a public address. A request to deploy something and
24
+ * get a URL back is the strongest signal there is, and it is stated first
25
+ * because it is the least ambiguous.
26
+ *
27
+ * The three are kept together rather than reduced to one. They disagree at the
28
+ * edges — a demo that must stay up, a production service that starts as a
29
+ * sketch — and when they do, asking the user is the answer, which the text says.
30
+ *
31
+ * ## Why it says to ask
32
+ *
33
+ * A tie-break the model performs silently is a decision the user never sees
34
+ * until the bill or the outage. So when the request does not make the tier
35
+ * obvious, the model asks — and says what the choice costs, because "sandbox vs
36
+ * Terraform" means nothing to someone who has not met either.
37
+ *
38
+ * ## What it does not say
39
+ *
40
+ * It does not describe the Terraform workflow. That belongs to the plugin that
41
+ * owns those tools, which also states the approvals each step needs.
42
+ *
43
+ * @module @huaweidevtools/aishell-dsh-e2b/prompt
44
+ */
45
+ import type { Context } from '@deepseek-ai/cordis';
46
+ /** Position of the tier guidance in the assembled context. */
47
+ export declare const SANDBOX_PROMPT_ORDER = 110;
48
+ /** Name this contribution registers under; unique across the assembled prompt. */
49
+ export declare const SANDBOX_SECTION_NAME = "aishell:e2b";
50
+ /** What the model reads about which tier to use. */
51
+ export declare const SANDBOX_GUIDANCE = "A sandbox is a Linux environment running remotely. It exists for a bounded time, then is deleted along with everything in it. What it gives you that this machine does not is a public address.\n\nThat is the clearest signal: when the work has to be deployed somewhere reachable and the user wants a URL they can open \u2014 a web app, a demo, a service, a static page \u2014 use the sandbox tools (aishell__sandbox_*). Put the files there with aishell__sandbox_files_write, start the server with aishell__sandbox_commands_run and background: true, then call aishell__sandbox_get_port_url and give the user that address.\n\nAt the other end, a production system belongs in real infrastructure, deployed with the Terraform tools (aishell__terraform_* and their neighbours). It will cost money, it should keep running, and it will be operated. The sandbox is the wrong tier for it not because it is small but because it is deliberately short-lived \u2014 the address stops working when it expires.\n\nSo: a page or a demo, or anything that should end in a URL to open, is a sandbox. A system to run and keep running is Terraform.\n\nIf the request does not make clear which of these it is, ask the user before choosing. Say what the difference amounts to for them: a sandbox is throwaway and costs nothing to discard, while Terraform provisions real resources that are billed and meant to keep running. Follow what they pick.\n\nA sandbox is often the right place to work something out before committing real resources to it.\n\nThis conversation works in at most one sandbox at a time. Create it with aishell__sandbox_create before the tools that use it, and choose its lifetime there \u2014 the value should match what the work needs, since the sandbox and its contents are deleted when it expires.";
52
+ /**
53
+ * Register the guidance section.
54
+ *
55
+ * Deferred behind `ctx.inject` rather than declared in this plugin's own
56
+ * `inject`: the guidance is an enhancement, and a deployment without a system
57
+ * prompt should lose it rather than fail to mount.
58
+ *
59
+ * @param ctx - host context.
60
+ */
61
+ export declare function registerSandboxGuidance(ctx: Context): void;
62
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The E2B API key this plugin presents to the sandbox gateway.
3
+ *
4
+ * ## The lifecycle
5
+ *
6
+ * The key is a **stored credential**, not process state:
7
+ *
8
+ * 1. Read the `aishell_e2b` section of the `aishell-credential` bundle.
9
+ * 2. Not there, or past its expiry → `POST /e2b/keys` and store the result.
10
+ * 3. Either way, present the stored key as `X-API-KEY`.
11
+ *
12
+ * Persisting it is what makes a restart cheap and, more importantly, what
13
+ * keeps the key stable. The key *is* the tenant: the gateway derives a caller
14
+ * identity from the presented key and scopes every sandbox record to it, so a
15
+ * key re-created on every start would orphan every sandbox made before it —
16
+ * the model would see "no such sandbox" and never learn why.
17
+ *
18
+ * ## A key with no expiry
19
+ *
20
+ * A key issued with no expiry (`ttl_seconds: 0`) is normalised to
21
+ * `9999-12-31`, so the expiry check stays one comparison instead of two
22
+ * shapes. An `expires_at` that is present but cannot be parsed is treated the
23
+ * same way: a malformed timestamp is not evidence the key stopped working, and
24
+ * treating it as expired would re-create a key on every call — the one
25
+ * behaviour that guarantees orphaning sandboxes. The 401 fallback below still
26
+ * catches a key that really has stopped working.
27
+ *
28
+ * ## Why the call is signed
29
+ *
30
+ * Requesting a key authenticates with the operator's Huawei Cloud AK/SK — the
31
+ * same pair `aishell-llm` signs gateway requests with — so the service can tell
32
+ * whose key to issue.
33
+ *
34
+ * ## Why the key is bound to the AK that minted it
35
+ *
36
+ * A stored key outlives the credentials that created it. Clearing or replacing
37
+ * the AK/SK in the credential form therefore used to leave the old key — and so
38
+ * the old tenant, with all its sandboxes — alive and usable, which contradicts
39
+ * what clearing credentials is supposed to mean. A key with no expiry made that
40
+ * permanent rather than temporary.
41
+ *
42
+ * So the key records a fingerprint of the AK that minted it, and {@link get}
43
+ * discards a stored key whose fingerprint no longer matches the stored
44
+ * credentials. Three consequences are deliberate:
45
+ *
46
+ * - **No credentials at all also invalidates it.** Clearing them is the
47
+ * clearest statement that nothing this plugin was granted may still be used.
48
+ * - **The same AK/SK re-entered does not rotate the key.** A fingerprint
49
+ * answers "which identity minted this", not "when was it written"; someone
50
+ * re-saving the same pair has changed nothing, and throwing their running
51
+ * sandboxes away for it would be gratuitous.
52
+ * - **No listener is needed.** The check happens on the read path, so it also
53
+ * catches a store changed outside the credential form, or while this plugin
54
+ * was not mounted. An event listener would only see the case where this
55
+ * plugin happened to be running.
56
+ *
57
+ * A key stored by an older build carries no fingerprint. It is treated as
58
+ * matching (see {@link matchesFingerprint}) rather than re-created: rotating it
59
+ * would orphan every sandbox made before this build, and the 401 fallback still
60
+ * covers a key that has genuinely stopped working.
61
+ *
62
+ * @module @huaweidevtools/aishell-dsh-e2b/sandbox-key
63
+ */
64
+ import type { CredentialsStore } from '@huaweidevtools/aishell-dsh-credentials-store';
65
+ import { type E2BSection } from '@huaweidevtools/aishell-dsh-credentials-store/accounts';
66
+ import { type SignCredentials } from '@huaweidevtools/aishell-dsh-credentials-store/signer';
67
+ /** Path of the key endpoint: POST creates, GET lists. */
68
+ export declare const KEYS_PATH = "/e2b/keys";
69
+ /** An error the model can act on, carrying the endpoint's own message. */
70
+ export declare class SandboxKeyError extends Error {
71
+ constructor(message: string);
72
+ }
73
+ /** Reads the operator's credentials, or `undefined` when the pair is incomplete. */
74
+ export type CredentialsReader = () => SignCredentials | undefined;
75
+ /** Everything the key source needs; injected so tests drive it without a network. */
76
+ export interface SandboxKeyOptions {
77
+ /** Absolute base URL of the provisioning service, no trailing slash. */
78
+ readonly baseUrl: string;
79
+ /** Reads the signing credentials per attempt. */
80
+ readonly readCredentials: CredentialsReader;
81
+ /** The store holding the `aishell-credential` bundle. */
82
+ readonly store: CredentialsStore;
83
+ /** Fetch implementation; defaults to the global. */
84
+ readonly fetchImpl?: typeof fetch;
85
+ /** Request timeout in milliseconds. */
86
+ readonly timeoutMs: number;
87
+ }
88
+ /** One key as the store holds it. */
89
+ export interface StoredKey {
90
+ /** The secret presented as `X-API-KEY`. */
91
+ readonly key: string;
92
+ /** When it stops working, ISO 8601; {@link FOREVER_EXPIRY} for no expiry. */
93
+ readonly expiresAt: string;
94
+ /**
95
+ * {@link fingerprint} of the AK that minted this key, or `undefined` for a
96
+ * key stored before fingerprints existed. See the module comment.
97
+ */
98
+ readonly akFingerprint?: string;
99
+ }
100
+ /**
101
+ * Whether a stored key may still be used for the credentials in hand.
102
+ *
103
+ * - No credentials → no, whatever the key says: nothing granted under a
104
+ * credential set that has since been cleared may still be presented.
105
+ * - No fingerprint on the key (an older build) → yes. Rotating it would orphan
106
+ * every sandbox made before this build, and that is worse than the gap it
107
+ * closes; the 401 fallback still catches a dead key.
108
+ * - Otherwise → the fingerprint must equal the current AK's.
109
+ *
110
+ * @param stored - the key read from the store.
111
+ * @param credentials - the credentials in hand, or `undefined` when cleared.
112
+ * @returns `true` when the key belongs to the current credentials.
113
+ */
114
+ export declare function matchesFingerprint(stored: StoredKey, credentials: SignCredentials | undefined): boolean;
115
+ /**
116
+ * Convert the stored bundle section into this module's shape.
117
+ *
118
+ * Returns `undefined` when there is no usable key: a corrupt section should
119
+ * lead to a new key, not to a half-parsed value presented to the gateway.
120
+ *
121
+ * The section is decoded by the shared store module, so this function only
122
+ * renames fields — it never parses JSON.
123
+ *
124
+ * @param section - the e2b section read from the store.
125
+ * @returns the key, or `undefined` when there is none.
126
+ */
127
+ export declare function fromStoredSection(section: E2BSection | undefined): StoredKey | undefined;
128
+ /**
129
+ * Whether a stored key is past its expiry.
130
+ *
131
+ * @param key - the stored key.
132
+ * @param now - the instant to compare against.
133
+ * @returns `true` when the key should be replaced.
134
+ */
135
+ export declare function isExpired(key: StoredKey, now: number): boolean;
136
+ /** The stored form of one key, as the bundle keeps it. */
137
+ export declare function toStoredSection(key: StoredKey): E2BSection;
138
+ /** One key, read from the store and replaced when it expires or is refused. */
139
+ export declare class SandboxKeySource {
140
+ private readonly options;
141
+ private readonly fetchImpl;
142
+ /** The key in hand, or `undefined` before the first read. */
143
+ private current;
144
+ /** The in-flight create, so concurrent callers share one request. */
145
+ private pending;
146
+ constructor(options: SandboxKeyOptions);
147
+ /**
148
+ * Return a usable API key.
149
+ *
150
+ * A key is used only while it is unexpired **and** still belongs to the
151
+ * credentials in hand — see the module comment on why the second check
152
+ * exists and why a mismatch does not delete the stored entry.
153
+ *
154
+ * @param signal - caller cancellation, forwarded to the fetch.
155
+ * @returns the key to present as `X-API-KEY`.
156
+ * @throws {SandboxKeyError} when no credentials are stored, so nothing can
157
+ * be created to replace a key the current credentials do not own.
158
+ */
159
+ get(signal: AbortSignal | undefined): Promise<string>;
160
+ /**
161
+ * Forget the key, so the next {@link get} creates a new one.
162
+ *
163
+ * Called when the gateway refuses the key as unauthenticated — the one
164
+ * signal that it has stopped working earlier than its expiry said. The
165
+ * replacement is a different key, and therefore a different tenant: sandboxes
166
+ * created under the old one stop being reachable through this plugin.
167
+ */
168
+ invalidate(): void;
169
+ /**
170
+ * Create a key, store it, and return it.
171
+ *
172
+ * @param credentials - the pair to sign with; already read by {@link get},
173
+ * so the fingerprint recorded on the key is the one it was minted under.
174
+ * @param signal - caller cancellation, forwarded to the fetch.
175
+ */
176
+ private createKey;
177
+ }
178
+ //# sourceMappingURL=sandbox-key.d.ts.map