@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.
- package/LICENSE +201 -0
- package/README.md +200 -2
- package/cordis.patch.yml +19 -0
- package/lib/index.js +2545 -0
- package/lib/types/client.d.ts +137 -0
- package/lib/types/credentials.d.ts +47 -0
- package/lib/types/errors.d.ts +47 -0
- package/lib/types/index.d.ts +91 -0
- package/lib/types/prompt.d.ts +62 -0
- package/lib/types/sandbox-key.d.ts +178 -0
- package/lib/types/sandbox-registry.d.ts +114 -0
- package/lib/types/tools.d.ts +46 -0
- package/package.json +53 -3
|
@@ -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
|