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

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,124 @@
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
+ /** How the connection is parameterised; every field is read per call. */
24
+ export interface SandboxClientOptions {
25
+ /** The gateway's domain, e.g. `aishell.openbuildlab.com`. */
26
+ readonly resolveDomain: () => string;
27
+ /** The key source, which caches and refreshes the API key. */
28
+ readonly keys: SandboxKeySource;
29
+ /** Per-request timeout for SDK control-plane calls. */
30
+ readonly resolveRequestTimeoutMs: () => number;
31
+ }
32
+ /** Builds and caches SDK handles, one per sandbox id. */
33
+ export declare class SandboxClient {
34
+ private readonly options;
35
+ /** Sandbox id → the live handle for it. */
36
+ private readonly handles;
37
+ constructor(options: SandboxClientOptions);
38
+ /**
39
+ * The live handle for one sandbox.
40
+ *
41
+ * @param sandboxId - the sandbox to address.
42
+ * @returns the cached handle, or a newly connected one.
43
+ */
44
+ handle(sandboxId: string): Promise<Sandbox>;
45
+ /**
46
+ * Create a sandbox and cache its handle.
47
+ *
48
+ * @param template - the template name or id; the SDK's own default when omitted.
49
+ * @param options - create options forwarded to the SDK.
50
+ * @returns the created sandbox handle.
51
+ */
52
+ create(template: string | undefined, options: {
53
+ readonly timeoutMs?: number;
54
+ readonly envs?: Record<string, string>;
55
+ readonly metadata?: Record<string, string>;
56
+ readonly signal?: AbortSignal;
57
+ }): Promise<Sandbox>;
58
+ /**
59
+ * Find the sandboxes carrying one metadata pair.
60
+ *
61
+ * The SDK encodes the pair into the `metadata` query parameter and the
62
+ * gateway forwards it to the upstream unchanged, so this is the mechanism a
63
+ * restarted process uses to rediscover the sandbox a conversation was
64
+ * working in.
65
+ *
66
+ * The search follows the pagination cursor. A single page would be enough
67
+ * for the common case, but not for a busy account: the gateway pages at 100
68
+ * and reports more with a cursor, and a conversation's own sandbox is not
69
+ * ordered ahead of the others — so on the day an account holds more than a
70
+ * page, the one sandbox the caller is looking for is as likely as any to be
71
+ * on a later page, and a single-page search would report "none".
72
+ *
73
+ * @param metadata - the pair to match; a sandbox must carry all of it.
74
+ * @param signal - caller cancellation.
75
+ * @returns one row per matching sandbox.
76
+ */
77
+ findByMetadata(metadata: Record<string, string>, signal: AbortSignal | undefined): Promise<Array<{
78
+ sandboxId: string;
79
+ state: string;
80
+ }>>;
81
+ /**
82
+ * List the sandboxes this account currently has.
83
+ *
84
+ * Follows the pagination cursor, for the same reason the metadata search
85
+ * does: the gateway pages at 100, and a caller asking what it has should not
86
+ * be told "these are all of them" when they are the first hundred.
87
+ *
88
+ * @param signal - caller cancellation.
89
+ * @returns one row per sandbox, in the shape the tool renders.
90
+ */
91
+ list(signal: AbortSignal | undefined): Promise<Array<{
92
+ sandboxId: string;
93
+ templateId: string;
94
+ state: string;
95
+ startedAt: string;
96
+ endAt: string;
97
+ }>>;
98
+ /**
99
+ * Drop a handle for a sandbox that no longer exists.
100
+ *
101
+ * Called after a kill, so a later call against the same id re-resolves
102
+ * rather than reusing an endpoint the gateway has already torn down.
103
+ *
104
+ * @param sandboxId - the sandbox whose handle to drop.
105
+ */
106
+ forget(sandboxId: string): void;
107
+ /** Connect to an existing sandbox. */
108
+ private connect;
109
+ /**
110
+ * Run one SDK call, retrying once against a freshly fetched key when the
111
+ * gateway rejects the one in hand.
112
+ *
113
+ * The key is handed to the callback rather than captured by it. A callback
114
+ * that closed over the first key would "retry" by presenting that same
115
+ * rejected key again — a loop that looks like a refresh and is not one.
116
+ *
117
+ * The retry is deliberately narrow: only an authentication failure clears
118
+ * the cache, and only once. A key that fails for any other reason — a
119
+ * sandbox that does not exist, a rate limit — is not evidence about the key,
120
+ * and refreshing on it would rotate the tenant for no reason.
121
+ */
122
+ private runWithKeyRefresh;
123
+ }
124
+ //# 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,128 @@
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
+ /** Position of the tier guidance in the assembled prompt. */
38
+ export declare const SANDBOX_PROMPT_ORDER = 110;
39
+ /** Name this contribution registers under; unique across the assembled prompt. */
40
+ export declare const SANDBOX_SECTION_NAME = "aishell:e2b";
41
+ /**
42
+ * What the model reads about which tier to use.
43
+ *
44
+ * This is the plugin's reason for existing. Without it the model has a set of
45
+ * sandbox tools and no basis for preferring them over provisioning real
46
+ * resources — or for preferring those over a sandbox.
47
+ *
48
+ * ## Why the split is stated by use and by scale
49
+ *
50
+ * An earlier version drew the line at lifecycle: "does the result need to
51
+ * outlive the task?" That is the property that actually decides the tier, but
52
+ * it is one the model has to *infer* about work that has not been described in
53
+ * those terms yet. "A web page or a small demo" versus "a production system" is
54
+ * a distinction the user can answer directly, and it maps onto the lifecycle
55
+ * question without asking the model to guess at it.
56
+ *
57
+ * ## Why it says to ask
58
+ *
59
+ * A tie-break the model performs silently is a decision the user never sees
60
+ * until the bill or the outage. When the request does not make the tier
61
+ * obvious, the model asks — and says what the choice costs, because "sandbox vs
62
+ * Terraform" means nothing to someone who has not met either.
63
+ *
64
+ * The distinction is stated in consequences, not mechanism: throwaway and free
65
+ * to discard, versus real resources that bill and are meant to keep running.
66
+ *
67
+ * ## What it does not say
68
+ *
69
+ * It does not describe the Terraform workflow. That belongs to the plugin that
70
+ * owns those tools, which also states the approvals each step needs — and the
71
+ * two sections land far apart in the assembled prompt, so repeating it here
72
+ * would risk two instructions drifting out of step.
73
+ */
74
+ export declare const SANDBOX_GUIDANCE = "A sandbox is a disposable Linux environment: it runs for a bounded time, then is deleted along with everything in it.\n\nUse the sandbox tools (aishell__sandbox_*) when the ask is a web page or a simple demo-scale web app \u2014 something to look at, or to try out, that does not need to keep existing.\n\nDeploy a production system instead, with the Terraform tools (aishell__terraform_* and their neighbours), when the result is real infrastructure: it will cost money, it should keep running, and it will be operated.\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.";
75
+ /** Plugin config. */
76
+ export interface Config {
77
+ /** Base URL of the sandbox key provisioning service. */
78
+ readonly apiBaseUrl: Volatile<string>;
79
+ /** The sandbox gateway's domain, e.g. `aishell.openbuildlab.com`. */
80
+ readonly domain: Volatile<string>;
81
+ /** Per-request timeout for the key fetch and for SDK control-plane calls. */
82
+ readonly requestTimeoutMs: Volatile<number>;
83
+ }
84
+ /**
85
+ * Settings schema for this entry.
86
+ *
87
+ * Both URLs are Volatile: they are deployment facts that differ between a
88
+ * staging and a production profile, and a change should apply to the next call
89
+ * rather than requiring a restart.
90
+ */
91
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
92
+ apiBaseUrl: z<string, string, "volatile-defined">;
93
+ domain: z<string, string, "volatile-defined">;
94
+ requestTimeoutMs: z<number, number, "volatile-defined">;
95
+ }>>, Schemastery.ObjectT<NoInfer<{
96
+ apiBaseUrl: z<string, string, "volatile-defined">;
97
+ domain: z<string, string, "volatile-defined">;
98
+ requestTimeoutMs: z<number, number, "volatile-defined">;
99
+ }>>, "plain">;
100
+ export declare const name = "aishell-e2b";
101
+ /**
102
+ * Required services: the tool registry alone.
103
+ *
104
+ * The filesystem and the system prompt are both optional and taken through
105
+ * `ctx.inject` / `ctx.get` — a deployment without a filesystem loses one
106
+ * tool's destination, and one without a system prompt loses the guidance
107
+ * section, neither of which should stop the sandbox tools from registering.
108
+ */
109
+ export declare const inject: string[];
110
+ export declare function apply(ctx: Context, config: Config): void;
111
+ declare module '@deepseek-ai/cordis' {
112
+ interface Events {
113
+ /**
114
+ * The stored Huawei Cloud credentials changed — a verified save, or a
115
+ * clear. Emitted by `aishell-credentials`, which owns the credential form
116
+ * and the store it writes.
117
+ *
118
+ * Declared here as well as on the emitting side because the two packages
119
+ * share no import: this is the whole interface between them. A listener
120
+ * reads the store itself; the event carries no value, so a secret can
121
+ * never ride on it.
122
+ *
123
+ * @mode on
124
+ */
125
+ 'aishell/credentials-updated'(): void;
126
+ }
127
+ }
128
+ //# sourceMappingURL=index.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
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The sandbox a conversation works in.
3
+ *
4
+ * One conversation, one sandbox. That is the whole model: there is no
5
+ * "which sandbox did you mean" question for the tools to ask, because there is
6
+ * only ever one answer, and no `sandboxId` parameter for the model to get
7
+ * wrong. The tools act on *the* sandbox, and creating one is the explicit act
8
+ * that binds it to the conversation.
9
+ *
10
+ * ## Why the binding is created explicitly, never lazily
11
+ *
12
+ * A tool that quietly created a sandbox on first use would have to choose the
13
+ * template and the lifetime itself, and those are exactly the two facts the
14
+ * operator should be deciding — a preview page wants a different lifetime from
15
+ * a debugging session, and that choice is the reason the create tool takes
16
+ * parameters at all. A lazy create would move that decision into the plugin
17
+ * and out of the conversation, where nobody can see it or correct it.
18
+ *
19
+ * So a tool with no sandbox bound fails, and names `aishell__sandbox_create` as the way
20
+ * forward. The failure is cheap and legible; a silently invented sandbox is
21
+ * neither.
22
+ *
23
+ * ## Why the binding survives a restart
24
+ *
25
+ * The mapping cannot live in memory alone. A plugin restart — a reload, a
26
+ * crash, a profile edit — would otherwise orphan a running sandbox: the model
27
+ * would find no binding, create a second sandbox, and the first would keep
28
+ * billing until its lifetime expired, with nothing in the conversation
29
+ * accounting for it.
30
+ *
31
+ * So the sandbox carries its conversation in its own `metadata` at creation,
32
+ * and the registry rebuilds the binding by querying for it. The gateway
33
+ * forwards `metadata` filters to the upstream unchanged, and
34
+ * `Sandbox.list({ query: { metadata } })` is ordinary SDK surface, so the
35
+ * recovery needs nothing from either service.
36
+ *
37
+ * A recovered binding is refused if the sandbox is not running. A paused
38
+ * sandbox is not adopted: connecting to it would resume it and silently
39
+ * restart its clock, which is a billing decision nobody asked for.
40
+ *
41
+ * @module @huaweidevtools/aishell-dsh-e2b/sandbox-registry
42
+ */
43
+ /** The metadata key a sandbox carries its owning conversation under. */
44
+ export declare const SESSION_METADATA_KEY = "aishell-session";
45
+ /** One sandbox the registry knows about. */
46
+ export interface BoundSandbox {
47
+ /** The sandbox id. */
48
+ readonly sandboxId: string;
49
+ /** Whether the gateway reports it as running. */
50
+ readonly state: string;
51
+ }
52
+ /** Lists the sandboxes carrying one metadata pair. */
53
+ export type SandboxFinder = (metadata: Record<string, string>, signal: AbortSignal | undefined) => Promise<ReadonlyArray<BoundSandbox>>;
54
+ /** In-memory bindings plus the lookup used to rebuild them. */
55
+ export declare class SandboxRegistry {
56
+ /** Session id → the sandbox bound to it. */
57
+ private readonly bySession;
58
+ /** The metadata lookup, injected so tests need no gateway. */
59
+ private readonly find;
60
+ constructor(find: SandboxFinder);
61
+ /**
62
+ * Bind a sandbox to its conversation.
63
+ *
64
+ * @param sessionId - the owning conversation, or `undefined` for a call that
65
+ * ran outside one. Such a call is recorded nowhere: without a conversation
66
+ * to key on, there would be nothing to bind later calls to.
67
+ * @param sandboxId - the sandbox to bind.
68
+ */
69
+ bind(sessionId: string | undefined, sandboxId: string): void;
70
+ /**
71
+ * The sandbox this conversation is bound to, recovering the binding if this
72
+ * process has not seen one.
73
+ *
74
+ * @param sessionId - the calling conversation.
75
+ * @param signal - caller cancellation, forwarded to the lookup.
76
+ * @returns the sandbox id, or `undefined` when the conversation has none.
77
+ */
78
+ resolve(sessionId: string | undefined, signal?: AbortSignal): Promise<string | undefined>;
79
+ /**
80
+ * The sandbox bound right now, without a lookup.
81
+ *
82
+ * Used where a call has already resolved the binding and needs it again
83
+ * without a second round trip.
84
+ *
85
+ * @param sessionId - the calling conversation.
86
+ * @returns the bound sandbox id, or `undefined`.
87
+ */
88
+ bound(sessionId: string | undefined): string | undefined;
89
+ /**
90
+ * Drop a binding, because the sandbox it names no longer exists.
91
+ *
92
+ * @param sandboxId - the sandbox that is gone.
93
+ */
94
+ forget(sandboxId: string): void;
95
+ }
96
+ /** The metadata a new sandbox carries so its conversation can be recovered. */
97
+ export declare function sessionMetadata(sessionId: string | undefined, extra: Record<string, string> | undefined): Record<string, string> | undefined;
98
+ /**
99
+ * The message a tool gets when the conversation has no sandbox.
100
+ *
101
+ * Names the remedy, and deliberately does not offer to create one: the model
102
+ * chooses the template and the lifetime, and it cannot do that from inside a
103
+ * tool that was asked to write a file.
104
+ */
105
+ export declare const NO_SANDBOX_MESSAGE: string;
106
+ /**
107
+ * The message a tool gets when a sandbox existed but is now gone.
108
+ *
109
+ * Distinct from {@link NO_SANDBOX_MESSAGE} because the remedy differs: the
110
+ * binding is stale rather than absent, and the model should know the previous
111
+ * sandbox's contents are not recoverable — creating a new one starts empty.
112
+ */
113
+ export declare const SANDBOX_GONE_MESSAGE: string;
114
+ //# sourceMappingURL=sandbox-registry.d.ts.map