@envseal/mcp-server 0.1.0

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,247 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { SepError, zero } from '@envseal/protocol';
3
+ import { makeDisplayNonce } from '@envseal/prompters';
4
+ /**
5
+ * The interactive consent surface for the two operations that move a live
6
+ * value: `env_use` (inject into a child process) and `env_verify` against a
7
+ * host the registry does not allowlist.
8
+ *
9
+ * Before this existed, three of the four bindings constructed the Broker with
10
+ * no `onConfirm`, and exec.ts turned that absence into
11
+ * SEP_CONFIRMATION_DENIED — "The user denied the confirmation" — when no user
12
+ * had been asked and no user had denied. `env_use` was advertised in
13
+ * tools/list and in the OpenAPI document and could never succeed.
14
+ *
15
+ * There is deliberately no environment-variable bypass here. `envseal run`
16
+ * honours ENVSEAL_ASSUME_YES because a human typed that command; in these
17
+ * bindings the argv comes from a *model*, and this prompt is the only thing
18
+ * between a prompt-injected model and arbitrary code holding live
19
+ * credentials. In CI these operations are simply unavailable, and say so.
20
+ *
21
+ * DUPLICATION: this file is a hand-maintained twin of
22
+ * packages/sdk/src/confirm.ts. Its natural home is a `confirm()` method
23
+ * on the `Prompter` interface — every surface already knows how to draw a
24
+ * dialog — but @envseal/mcp-server does not depend on @envseal/sdk, and
25
+ * @envseal/prompters was outside the scope of the change that added this. Both
26
+ * packages test the behaviour independently, so drift shows up as a red test
27
+ * rather than as a binding that quietly stops asking.
28
+ */
29
+ /** Value-entry key name carrying the `env_use` confirmation. */
30
+ export const CONFIRM_KEY_USE = 'APPROVE';
31
+ /** Value-entry key name carrying the `env_verify` probe-consent question. */
32
+ export const CONFIRM_KEY_PROBE = 'APPROVE_PROBE';
33
+ const DEFAULT_TIMEOUT_MS = 120_000;
34
+ /** Per-argument display cap; longer arguments are shown truncated, and said to be. */
35
+ const MAX_ARG_CHARS = 300;
36
+ /** Whole-dialog cap. Past this we refuse rather than ask about something unreadable. */
37
+ const MAX_BODY_CHARS = 16 * 1024;
38
+ const INSTRUCTION = 'Type yes to approve, or submit an empty box to deny.';
39
+ /**
40
+ * Only one confirmation may be open per process. Without this a model can call
41
+ * `env_use` in a loop and stack up dialogs until one gets clicked through.
42
+ */
43
+ let confirmationOpen = false;
44
+ /**
45
+ * Model-supplied argv, key names and probe metadata land in a dialog the user
46
+ * is about to trust. A raw newline lets a crafted argument forge extra lines —
47
+ * "keys: none", "this command is safe" — inside the very block that exists to
48
+ * tell the truth about the command. Render control characters visibly instead.
49
+ */
50
+ function escapeForDisplay(value) {
51
+ let out = '';
52
+ for (const ch of value) {
53
+ const code = ch.codePointAt(0) ?? 0;
54
+ if (code < 0x20 || (code >= 0x7f && code <= 0x9f)) {
55
+ out += `<0x${code.toString(16).padStart(2, '0')}>`;
56
+ }
57
+ else {
58
+ out += ch;
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ function displayArg(arg) {
64
+ const escaped = escapeForDisplay(arg);
65
+ if (escaped.length <= MAX_ARG_CHARS) {
66
+ return escaped;
67
+ }
68
+ const hidden = escaped.length - MAX_ARG_CHARS;
69
+ return `${escaped.slice(0, MAX_ARG_CHARS)}[... ${hidden} more characters, not shown]`;
70
+ }
71
+ export function useConfirmationBody(info, projectRoot) {
72
+ const lines = [
73
+ 'EnvSeal is about to run a program with these secrets in its environment.',
74
+ '',
75
+ ` project: ${escapeForDisplay(projectRoot)}`,
76
+ ` keys: ${info.keys.length > 0 ? info.keys.map(escapeForDisplay).join(', ') : '(none)'}`,
77
+ '',
78
+ ' command, one argument per line, exactly as it will be run (no shell):',
79
+ ];
80
+ info.command.forEach((arg, index) => {
81
+ lines.push(` [${index}] ${displayArg(arg)}`);
82
+ });
83
+ lines.push('');
84
+ if (info.networkEgress) {
85
+ lines.push(' WARNING: this command can reach the network, so it could send these', ' values somewhere. Only continue if you trust it.');
86
+ }
87
+ else {
88
+ // Honest about what the check is worth: NETWORK_TOOLS plus a URL scan is a
89
+ // heuristic, and claiming more would be the kind of overstatement this
90
+ // project has already had to walk back once.
91
+ lines.push(' No network tool or URL was recognised in this command. That is a', ' heuristic, not a guarantee: any program can open a socket.');
92
+ }
93
+ lines.push('', `${INSTRUCTION} Nothing runs unless you approve.`);
94
+ return lines.join('\n');
95
+ }
96
+ export function probeConfirmationBody(entry) {
97
+ const probe = entry.verify;
98
+ if (!probe) {
99
+ return null;
100
+ }
101
+ const lines = [
102
+ `EnvSeal is about to send the stored value of ${escapeForDisplay(entry.key)} to a host`,
103
+ 'that is not on its bundled allowlist. Nothing has been sent yet.',
104
+ '',
105
+ ` key: ${escapeForDisplay(entry.key)}`,
106
+ ` method: ${escapeForDisplay(probe.method)}`,
107
+ ` url: ${displayArg(probe.url)}`,
108
+ ' headers:',
109
+ ];
110
+ for (const [header, template] of Object.entries(probe.headerTemplate)) {
111
+ lines.push(` ${escapeForDisplay(header)}: ${displayArg(template)}`);
112
+ }
113
+ lines.push('', ' {{value}} is replaced with the real secret when the request is sent.', '', 'Type yes to approve exactly this probe. The answer is recorded in', '.envseal/approvals.json and replayed without asking again until the', 'method, URL or headers change. Submit an empty box to deny.');
114
+ return lines.join('\n');
115
+ }
116
+ async function ask(surface, keyName, headline, body) {
117
+ if (body === null || body.length > MAX_BODY_CHARS) {
118
+ return 'too-large';
119
+ }
120
+ const prompter = await surface.prompter();
121
+ if (prompter.id === 'none') {
122
+ return 'no-surface';
123
+ }
124
+ if (confirmationOpen) {
125
+ return 'busy';
126
+ }
127
+ confirmationOpen = true;
128
+ try {
129
+ const response = await prompter.prompt({
130
+ ticket: `confirm-${randomBytes(8).toString('hex')}`,
131
+ nonce: makeDisplayNonce(),
132
+ projectRoot: surface.projectRoot,
133
+ reason: headline,
134
+ keys: [{ key: keyName, description: body, formatHint: INSTRUCTION }],
135
+ timeoutMs: surface.timeoutMs ?? DEFAULT_TIMEOUT_MS,
136
+ });
137
+ const result = response.results.find((r) => r.key === keyName);
138
+ // A timeout is kept apart from a denial: nobody answered at all, and
139
+ // reporting that as "the user denied" blames a user who never spoke — the
140
+ // defect class the CLI fixed for the missing-surface case.
141
+ if (result !== undefined && result.outcome === 'timeout') {
142
+ return 'timed-out';
143
+ }
144
+ // skipped / cancelled / a surface that answered about some other key: we
145
+ // did not get a yes, and none of them names anyone, so they land on the
146
+ // honest-but-blunt denial.
147
+ if (result === undefined || result.outcome !== 'entered') {
148
+ return 'denied';
149
+ }
150
+ const typed = result.value.toString('utf8');
151
+ zero(result.value);
152
+ return /^y(es)?$/i.test(typed.trim()) ? 'approved' : 'denied';
153
+ }
154
+ finally {
155
+ confirmationOpen = false;
156
+ }
157
+ }
158
+ /**
159
+ * `onConfirm` for the Broker: gates `env_use`.
160
+ *
161
+ * Throws rather than returning false when no human could be asked or when the
162
+ * ask expired with nobody answering it, because exec.ts maps a `false` to
163
+ * SEP_CONFIRMATION_DENIED and that would blame the user for a missing surface
164
+ * or for a silence — the defect this project already fixed once in the CLI.
165
+ */
166
+ export function createUseConfirm(surface) {
167
+ return async (info) => {
168
+ const outcome = await ask(surface, CONFIRM_KEY_USE, 'Approve running a command with secrets in its environment? Nothing has run yet.', useConfirmationBody(info, surface.projectRoot));
169
+ switch (outcome) {
170
+ case 'approved':
171
+ return true;
172
+ case 'denied':
173
+ return false;
174
+ case 'timed-out':
175
+ // SEP_TICKET_EXPIRED, not SEP_CONFIRMATION_DENIED: the repo already
176
+ // treats an unanswered prompt as an expired ticket (exit-codes.ts maps
177
+ // outcome `timeout` and this code to the same exit), and a model that
178
+ // can tell "nobody answered" from "the user said no" retries instead
179
+ // of reporting a refusal that never happened.
180
+ throw new SepError({
181
+ code: 'SEP_TICKET_EXPIRED',
182
+ userMessage: 'The env_use confirmation closed after its timeout with nobody answering it. Nothing was ' +
183
+ 'run and no value was read. This is not a denial: ask the user to approve it, then call ' +
184
+ 'env_use again.',
185
+ });
186
+ case 'no-surface':
187
+ throw new SepError({
188
+ code: 'SEP_NO_INTERACTIVE_SURFACE',
189
+ userMessage: 'env_use needs the user to confirm before secrets are injected into a child process, ' +
190
+ 'but there is no interactive surface here to ask on (this is what CI looks like to envseal). ' +
191
+ 'Nothing was run and no value was read. ' +
192
+ 'There is no flag or environment variable that skips this prompt in this binding: the command ' +
193
+ 'came from a model, and the confirmation is the only control on it. ' +
194
+ 'Run the command yourself with `envseal run -- <command>` in a session that has a browser or a terminal.',
195
+ });
196
+ case 'busy':
197
+ throw new SepError({
198
+ code: 'SEP_RATE_LIMITED',
199
+ userMessage: 'Another envseal confirmation is already open. Answer that one first, then call env_use again.',
200
+ });
201
+ case 'too-large':
202
+ throw new SepError({
203
+ code: 'SEP_FORMAT_INVALID',
204
+ userMessage: 'This command is too large to display in a confirmation dialog, and envseal will not ask ' +
205
+ 'anyone to approve something it cannot show them. Run it with fewer or shorter arguments.',
206
+ });
207
+ }
208
+ };
209
+ }
210
+ /**
211
+ * `onApprovalNeeded` for the Broker: PLAN.md §6.4 probe consent for
212
+ * `env_verify` against a host that is not registry-allowlisted.
213
+ *
214
+ * Never throws. verifyKey() calls this per key inside a loop that builds
215
+ * per-key results; throwing would abort the whole `env_verify` call, so a
216
+ * missing surface would take down the verification of keys whose probes are
217
+ * allowlisted and fine. Every non-approval returns false, which keeps the
218
+ * existing fail-closed `probe_not_approved` outcome for that one key.
219
+ */
220
+ export function createProbeApproval(surface) {
221
+ return async (entry) => {
222
+ const outcome = await ask(surface, CONFIRM_KEY_PROBE, `Approve sending ${entry.key} to a host that is not on envseal's allowlist? Nothing has been sent yet.`, probeConfirmationBody(entry));
223
+ return outcome === 'approved';
224
+ };
225
+ }
226
+ /**
227
+ * `probe_not_approved` on its own tells the caller nothing it can act on.
228
+ * verify.ts (core) names the host; this adds what to do about it, in the one
229
+ * place a binding can add it without reaching into core.
230
+ */
231
+ export function annotateVerifyResults(results) {
232
+ return results.map((result) => {
233
+ if (result.result !== 'probe_not_approved') {
234
+ return result;
235
+ }
236
+ return {
237
+ ...result,
238
+ message: `${result.message}. This host is not on envseal's bundled allowlist and no approval for this ` +
239
+ `exact probe is recorded, so the credential was NOT sent. Run \`envseal verify ${result.key}\` ` +
240
+ 'in an interactive terminal on a machine with access to this ' +
241
+ 'project to review the method, URL and header template and decide. The decision is recorded in ' +
242
+ '.envseal/approvals.json and replayed without asking again, until the key, method, URL or header ' +
243
+ 'template changes.',
244
+ };
245
+ });
246
+ }
247
+ //# sourceMappingURL=confirm.js.map
@@ -0,0 +1,9 @@
1
+ export { createServer } from './server.js';
2
+ export * as describe from './tools/describe.js';
3
+ export * as declare from './tools/declare.js';
4
+ export * as request from './tools/request.js';
5
+ export * as await_ from './tools/await.js';
6
+ export * as verify from './tools/verify.js';
7
+ export * as use from './tools/use.js';
8
+ export * as revoke from './tools/revoke.js';
9
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { createServer } from './server.js';
2
+ export * as describe from './tools/describe.js';
3
+ export * as declare from './tools/declare.js';
4
+ export * as request from './tools/request.js';
5
+ export * as await_ from './tools/await.js';
6
+ export * as verify from './tools/verify.js';
7
+ export * as use from './tools/use.js';
8
+ export * as revoke from './tools/revoke.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,22 @@
1
+ export interface TextContent {
2
+ type: 'text';
3
+ text: string;
4
+ }
5
+ export interface ToolResult {
6
+ content: TextContent[];
7
+ isError?: true;
8
+ [key: string]: unknown;
9
+ }
10
+ /**
11
+ * THE SINGLE EGRESS POINT. Every tool handler returns exclusively through
12
+ * `respond` or `respondError`; nothing in `src/tools/` may construct a
13
+ * response object itself. Every string that leaves the server toward the
14
+ * model passes through the core redactor here. The secret set is empty at
15
+ * this layer by construction: the broker never hands a secret value to a
16
+ * tool handler, so there is nothing to redact-against-and-reveal here. Any
17
+ * string that touched a live value (e.g. `env_use` child output) is already
18
+ * redacted by the broker before it reaches this point.
19
+ */
20
+ export declare function respond(payload: unknown): ToolResult;
21
+ export declare function respondError(error: unknown): ToolResult;
22
+ //# sourceMappingURL=respond.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { isSepError } from '@envseal/protocol';
2
+ import { redact } from '@envseal/core';
3
+ /**
4
+ * THE SINGLE EGRESS POINT. Every tool handler returns exclusively through
5
+ * `respond` or `respondError`; nothing in `src/tools/` may construct a
6
+ * response object itself. Every string that leaves the server toward the
7
+ * model passes through the core redactor here. The secret set is empty at
8
+ * this layer by construction: the broker never hands a secret value to a
9
+ * tool handler, so there is nothing to redact-against-and-reveal here. Any
10
+ * string that touched a live value (e.g. `env_use` child output) is already
11
+ * redacted by the broker before it reaches this point.
12
+ */
13
+ export function respond(payload) {
14
+ return { content: asContent(JSON.stringify(payload)) };
15
+ }
16
+ export function respondError(error) {
17
+ if (isSepError(error)) {
18
+ // SepError messages are curated, user-facing strings from the protocol —
19
+ // never raw internals, stack traces, or provider responses.
20
+ return {
21
+ content: asContent(JSON.stringify({
22
+ code: error.code,
23
+ userMessage: error.userMessage,
24
+ retriable: error.retriable,
25
+ })),
26
+ isError: true,
27
+ };
28
+ }
29
+ // Unknown error: NEVER surface the raw message or stack trace — it may
30
+ // embed a credential or a partial value. Send a fixed safe message instead.
31
+ return {
32
+ content: asContent(JSON.stringify({
33
+ code: 'SEP_INTERNAL',
34
+ userMessage: 'An internal error occurred. Details were suppressed because they may contain sensitive information.',
35
+ retriable: false,
36
+ })),
37
+ isError: true,
38
+ };
39
+ }
40
+ function asContent(text) {
41
+ const filtered = redact(text, []);
42
+ return [{ type: 'text', text: filtered.text }];
43
+ }
44
+ //# sourceMappingURL=respond.js.map
@@ -0,0 +1,12 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import type { Broker } from '@envseal/core';
3
+ import * as describe from './tools/describe.js';
4
+ import * as declare from './tools/declare.js';
5
+ import * as request from './tools/request.js';
6
+ import * as await_ from './tools/await.js';
7
+ import * as verify from './tools/verify.js';
8
+ import * as use from './tools/use.js';
9
+ import * as revoke from './tools/revoke.js';
10
+ export declare function createServer(broker: Broker): Server;
11
+ export { describe, declare, request, await_, verify, use, revoke };
12
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,43 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
3
+ import { zodToJsonSchema } from 'zod-to-json-schema';
4
+ import { SepError } from '@envseal/protocol';
5
+ import * as describe from './tools/describe.js';
6
+ import * as declare from './tools/declare.js';
7
+ import * as request from './tools/request.js';
8
+ import * as await_ from './tools/await.js';
9
+ import * as verify from './tools/verify.js';
10
+ import * as use from './tools/use.js';
11
+ import * as revoke from './tools/revoke.js';
12
+ import { respondError } from './respond.js';
13
+ const toolModules = [describe, declare, request, await_, verify, use, revoke];
14
+ export function createServer(broker) {
15
+ // Capabilities must be declared up front or a client will never issue
16
+ // tools/list, and the handlers below would be dead code.
17
+ const server = new Server({ name: 'envseal-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
18
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
19
+ tools: toolModules.map((mod) => ({
20
+ name: mod.name,
21
+ description: mod.description,
22
+ inputSchema: zodToJsonSchema(mod.inputSchema),
23
+ })),
24
+ }));
25
+ // The SDK's result type is a union that also covers long-running "task"
26
+ // responses; annotating pins it to the plain content form these tools return.
27
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
28
+ const toolName = req.params.name;
29
+ const toolModule = toolModules.find((mod) => mod.name === toolName);
30
+ if (toolModule === undefined) {
31
+ // Routed through the single egress helper like every other result, so
32
+ // there is exactly one place where an outbound string is built.
33
+ return respondError(new SepError({
34
+ code: 'SEP_UNKNOWN_KEY',
35
+ userMessage: `Unknown tool: ${toolName}`,
36
+ }));
37
+ }
38
+ return await toolModule.handler(req.params.arguments, broker);
39
+ });
40
+ return server;
41
+ }
42
+ export { describe, declare, request, await_, verify, use, revoke };
43
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,40 @@
1
+ import type { Prompter } from '@envseal/prompters';
2
+ /**
3
+ * A prompter that returns a fixed value without any UI.
4
+ *
5
+ * This exists so the zero-leak test can drive the real server, over real stdio,
6
+ * without a human at a browser. It is a deliberate hole in the "a value only ever
7
+ * comes from the user" guarantee, so it is gated twice in `bin.ts`: the caller must
8
+ * set BOTH `ENVSEAL_TEST_MODE=1` and `ENVSEAL_TEST_PROMPTER_VALUE`. Neither is ever
9
+ * set by the shipped CLI, and nothing in the published package sets them for you.
10
+ *
11
+ * Since confirmations are asked on the selected prompter too (see confirm.ts),
12
+ * this stub also answers them: `ENVSEAL_TEST_PROMPTER_VALUE=yes` approves every
13
+ * env_use and every non-allowlisted verify probe, and any other value denies
14
+ * them. That widens the same double-gated hole rather than opening a second
15
+ * one, and it is what lets the env_use tests drive a real approval and a real
16
+ * denial against the shipped binary.
17
+ *
18
+ * If you are reading this because you want to inject a value programmatically in
19
+ * production: don't. Use a sink the value already lives in (keychain, vault) and let
20
+ * `presence` resolve it. Injecting through the prompter path would put the value in
21
+ * an environment variable, which is exactly what threat T6 is about.
22
+ */
23
+ export declare function createStubPrompter(value: string): Prompter;
24
+ /** Non-`entered` outcomes a stub prompter can be told to report. */
25
+ export type StubOutcome = 'skipped' | 'cancelled' | 'timeout';
26
+ export declare function isStubOutcome(value: string | undefined): value is StubOutcome;
27
+ /**
28
+ * A prompter that reports a refusal without any UI.
29
+ *
30
+ * Confirmations are asked on this prompter too, which until now made every
31
+ * non-stored outcome undrivable against the real binary — `timeout` could not
32
+ * be produced at all, so the honest-timeout mapping in confirm.ts was asserted
33
+ * by nothing end to end. Same mechanism as the CLI's refusing prompter, and
34
+ * gated the same way (`ENVSEAL_TEST_MODE=1` plus a second variable).
35
+ *
36
+ * Strictly the safer of the two stubs: it can only ever make envseal report
37
+ * that nobody answered or declined. It cannot introduce a value.
38
+ */
39
+ export declare function createRefusingPrompter(outcome: StubOutcome): Prompter;
40
+ //# sourceMappingURL=test-prompter.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { secretFromUtf8 } from '@envseal/protocol';
2
+ /**
3
+ * A prompter that returns a fixed value without any UI.
4
+ *
5
+ * This exists so the zero-leak test can drive the real server, over real stdio,
6
+ * without a human at a browser. It is a deliberate hole in the "a value only ever
7
+ * comes from the user" guarantee, so it is gated twice in `bin.ts`: the caller must
8
+ * set BOTH `ENVSEAL_TEST_MODE=1` and `ENVSEAL_TEST_PROMPTER_VALUE`. Neither is ever
9
+ * set by the shipped CLI, and nothing in the published package sets them for you.
10
+ *
11
+ * Since confirmations are asked on the selected prompter too (see confirm.ts),
12
+ * this stub also answers them: `ENVSEAL_TEST_PROMPTER_VALUE=yes` approves every
13
+ * env_use and every non-allowlisted verify probe, and any other value denies
14
+ * them. That widens the same double-gated hole rather than opening a second
15
+ * one, and it is what lets the env_use tests drive a real approval and a real
16
+ * denial against the shipped binary.
17
+ *
18
+ * If you are reading this because you want to inject a value programmatically in
19
+ * production: don't. Use a sink the value already lives in (keychain, vault) and let
20
+ * `presence` resolve it. Injecting through the prompter path would put the value in
21
+ * an environment variable, which is exactly what threat T6 is about.
22
+ */
23
+ export function createStubPrompter(value) {
24
+ return {
25
+ id: 'ide',
26
+ available: async () => true,
27
+ prompt: async (req) => ({
28
+ ticket: req.ticket,
29
+ results: req.keys.map((k) => ({
30
+ key: k.key,
31
+ outcome: 'entered',
32
+ value: secretFromUtf8(value),
33
+ })),
34
+ }),
35
+ cancel: async () => {
36
+ /* nothing to tear down */
37
+ },
38
+ };
39
+ }
40
+ export function isStubOutcome(value) {
41
+ return value === 'skipped' || value === 'cancelled' || value === 'timeout';
42
+ }
43
+ /**
44
+ * A prompter that reports a refusal without any UI.
45
+ *
46
+ * Confirmations are asked on this prompter too, which until now made every
47
+ * non-stored outcome undrivable against the real binary — `timeout` could not
48
+ * be produced at all, so the honest-timeout mapping in confirm.ts was asserted
49
+ * by nothing end to end. Same mechanism as the CLI's refusing prompter, and
50
+ * gated the same way (`ENVSEAL_TEST_MODE=1` plus a second variable).
51
+ *
52
+ * Strictly the safer of the two stubs: it can only ever make envseal report
53
+ * that nobody answered or declined. It cannot introduce a value.
54
+ */
55
+ export function createRefusingPrompter(outcome) {
56
+ return {
57
+ id: 'ide',
58
+ available: async () => true,
59
+ prompt: async (req) => ({
60
+ ticket: req.ticket,
61
+ results: req.keys.map((k) => ({ key: k.key, outcome })),
62
+ }),
63
+ cancel: async () => {
64
+ /* nothing to tear down */
65
+ },
66
+ };
67
+ }
68
+ //# sourceMappingURL=test-prompter.js.map
@@ -0,0 +1,15 @@
1
+ import type { Broker } from '@envseal/core';
2
+ export declare const name = "env_await";
3
+ export declare const description: string;
4
+ export declare const inputSchema: import("zod").ZodObject<{
5
+ ticket: import("zod").ZodString;
6
+ timeoutMs: import("zod").ZodDefault<import("zod").ZodNumber>;
7
+ }, "strict", import("zod").ZodTypeAny, {
8
+ ticket: string;
9
+ timeoutMs: number;
10
+ }, {
11
+ ticket: string;
12
+ timeoutMs?: number | undefined;
13
+ }>;
14
+ export declare function handler(args: unknown, broker: Broker): Promise<import("../respond.js").ToolResult>;
15
+ //# sourceMappingURL=await.d.ts.map
@@ -0,0 +1,20 @@
1
+ import { INPUT_SCHEMAS } from '@envseal/protocol';
2
+ import { respond, respondError } from '../respond.js';
3
+ export const name = 'env_await';
4
+ export const description = 'Blocks up to timeoutMs (default 90000, max 120000) for a pending env_request ticket to resolve, then ' +
5
+ 'returns per-key outcomes: stored, skipped, cancelled, invalid_format, verify_failed, or timeout. ' +
6
+ 'If the outcome is timeout, the prompt is still open — call env_await again with the same ticket. ' +
7
+ 'It will NOT return the value the user typed, only outcomes. ' +
8
+ 'To retry a failed request, call env_request again.';
9
+ export const inputSchema = INPUT_SCHEMAS.env_await;
10
+ export async function handler(args, broker) {
11
+ try {
12
+ const input = INPUT_SCHEMAS.env_await.parse(args ?? {});
13
+ const result = await broker.await(input);
14
+ return respond(result);
15
+ }
16
+ catch (error) {
17
+ return respondError(error);
18
+ }
19
+ }
20
+ //# sourceMappingURL=await.js.map