okengine 0.5.0 → 0.6.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.
- package/package.json +2 -1
- package/site/content/docs/elements/ai.mdx +82 -1
- package/site/content/docs/elements/channel.mdx +6 -1
- package/site/content/docs/elements/flow.mdx +20 -17
- package/site/content/docs/get-started/why.mdx +10 -10
- package/site/content/docs/plugins/email-otp.mdx +25 -19
- package/site/content/docs/plugins/headers.mdx +10 -10
- package/site/content/docs/plugins/magic-link.mdx +27 -21
- package/site/content/docs/plugins/passkey.mdx +36 -24
- package/site/content/docs/plugins/two-factor.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +7 -0
- package/site/content/docs/reference/environment-variables.mdx +10 -5
- package/site/content/docs/reference/errors.mdx +14 -0
- package/site/content/docs/reference/fx.mdx +68 -16
- package/site/content/docs/reference/i18n.mdx +313 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +1 -0
- package/site/content/docs/reference/plugins.mdx +1 -0
- package/src/auth/auth.test.ts +3 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/constant-time.ts +22 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/method-context.ts +12 -2
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/compiler/aot.test.ts +16 -13
- package/src/compiler/effects-infer.ts +46 -0
- package/src/console/server/ai.test.ts +34 -5
- package/src/docker/compose.ts +9 -0
- package/src/docker/docker.test.ts +39 -0
- package/src/docker/index.ts +11 -1
- package/src/docker/recipes/index.ts +3 -1
- package/src/docker/recipes/ollama.ts +43 -0
- package/src/docker/stack-id.ts +2 -0
- package/src/drivers/ai-mock.ts +60 -0
- package/src/drivers/ai-ollama-tools.integration.test.ts +107 -0
- package/src/drivers/ai-ollama.integration.test.ts +197 -0
- package/src/drivers/ai-ollama.ts +327 -0
- package/src/drivers/ai-openai-compatible.ts +211 -21
- package/src/drivers/ai-providers.test.ts +179 -2
- package/src/drivers/ai-stream.test.ts +195 -0
- package/src/drivers/ai-types.ts +42 -1
- package/src/drivers/channel-smtp.ts +8 -2
- package/src/drivers/index.ts +21 -1
- package/src/drivers/ollama.ts +14 -0
- package/src/elements/ai/rate.test.ts +53 -0
- package/src/elements/ai/rate.ts +66 -0
- package/src/elements/ai/redacted-prompt.test.ts +90 -0
- package/src/elements/ai/runtime.ts +330 -100
- package/src/elements/ai/tools.test.ts +99 -0
- package/src/elements/ai.test.ts +26 -2
- package/src/elements/ai.ts +10 -1
- package/src/i18n/catalogs/ar.ts +67 -0
- package/src/i18n/catalogs/en.ts +68 -0
- package/src/i18n/failure-message.test.ts +56 -0
- package/src/i18n/failure-message.ts +93 -0
- package/src/i18n/format.ts +67 -0
- package/src/i18n/index.ts +57 -0
- package/src/i18n/locale-context.ts +48 -0
- package/src/i18n/messages.test.ts +173 -0
- package/src/i18n/messages.ts +169 -0
- package/src/i18n/types.ts +90 -0
- package/src/index.ts +26 -0
- package/src/kernel/app.ts +92 -2
- package/src/kernel/boot-bind/ai.test.ts +60 -0
- package/src/kernel/boot-bind/ai.ts +125 -2
- package/src/kernel/boot.test.ts +4 -3
- package/src/kernel/boot.ts +1 -1
- package/src/kernel/errors.ts +56 -5
- package/src/kernel/fx.test.ts +27 -0
- package/src/kernel/fx.ts +74 -18
- package/src/kernel/pipeline.test.ts +4 -0
- package/src/kernel/pipeline.ts +1 -1
- package/src/kernel/plugin.ts +16 -0
- package/src/kernel/registry.ts +15 -0
- package/src/plugins/auth/shared.ts +5 -1
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +330 -0
- package/src/plugins/auth-methods.security.test.ts +764 -0
- package/src/plugins/compression.ts +1 -1
- package/src/plugins/config-source.test.ts +11 -11
- package/src/plugins/config-source.ts +2 -2
- package/src/plugins/cors.ts +1 -1
- package/src/plugins/email-otp.ts +54 -1
- package/src/plugins/{security-headers.test.ts → headers.test.ts} +18 -18
- package/src/plugins/headers.ts +240 -41
- package/src/plugins/index.ts +27 -5
- package/src/plugins/magic-link.ts +63 -3
- package/src/plugins/passkey-webauthn.ts +217 -0
- package/src/plugins/passkey.ts +99 -33
- package/src/plugins/response-headers.ts +54 -0
- package/src/plugins/two-factor.ts +6 -2
- package/src/plugins/username-policy.test.ts +302 -0
- package/src/plugins/username.ts +290 -9
- package/src/release/measure.ts +8 -1
- package/src/plugins/security-headers.ts +0 -255
package/src/drivers/ai-types.ts
CHANGED
|
@@ -14,14 +14,33 @@ export type AiDriverId =
|
|
|
14
14
|
| "vertex"
|
|
15
15
|
| "ollama";
|
|
16
16
|
|
|
17
|
+
/** Tool definition offered to a model (Flow-backed at the fx layer). */
|
|
18
|
+
export interface AiToolDef {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly description?: string;
|
|
21
|
+
/** JSON-schema-like parameters object. */
|
|
22
|
+
readonly parameters?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One model-initiated tool invocation. */
|
|
26
|
+
export interface AiToolCall {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly arguments: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
/** One chat / completion turn. */
|
|
18
33
|
export interface AiMessage {
|
|
19
34
|
readonly role: "system" | "user" | "assistant" | "tool";
|
|
20
35
|
readonly content: string;
|
|
21
36
|
readonly name?: string;
|
|
37
|
+
/** When role is `tool`, the id of the tool call this message answers. */
|
|
38
|
+
readonly toolCallId?: string;
|
|
39
|
+
/** When role is `assistant`, optional tool calls the model requested. */
|
|
40
|
+
readonly toolCalls?: readonly AiToolCall[];
|
|
22
41
|
}
|
|
23
42
|
|
|
24
|
-
/** Options for a model completion. */
|
|
43
|
+
/** Options for a model completion (or stream start). */
|
|
25
44
|
export interface AiCompleteOptions {
|
|
26
45
|
readonly messages: readonly AiMessage[];
|
|
27
46
|
readonly model?: string;
|
|
@@ -29,6 +48,10 @@ export interface AiCompleteOptions {
|
|
|
29
48
|
readonly maxTokens?: number;
|
|
30
49
|
/** Structured output hint (JSON schema or name). */
|
|
31
50
|
readonly responseFormat?: unknown;
|
|
51
|
+
/** Tools the model may call (OpenAI-shaped; drivers map). */
|
|
52
|
+
readonly tools?: readonly AiToolDef[];
|
|
53
|
+
/** Ambient / local abort — cooperative cancellation for complete + stream. */
|
|
54
|
+
readonly signal?: AbortSignal;
|
|
32
55
|
}
|
|
33
56
|
|
|
34
57
|
/** Result of a model completion. */
|
|
@@ -37,6 +60,7 @@ export interface AiCompleteResult {
|
|
|
37
60
|
readonly raw?: unknown;
|
|
38
61
|
readonly model: string;
|
|
39
62
|
readonly driverId: AiDriverId;
|
|
63
|
+
readonly toolCalls?: readonly AiToolCall[];
|
|
40
64
|
readonly usage?: {
|
|
41
65
|
readonly inputTokens?: number;
|
|
42
66
|
readonly outputTokens?: number;
|
|
@@ -44,6 +68,12 @@ export interface AiCompleteResult {
|
|
|
44
68
|
};
|
|
45
69
|
}
|
|
46
70
|
|
|
71
|
+
/** One streamed token / delta from a model. */
|
|
72
|
+
export interface AiStreamChunk {
|
|
73
|
+
readonly text: string;
|
|
74
|
+
readonly done?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
47
77
|
/** Embedding request. */
|
|
48
78
|
export interface AiEmbedOptions {
|
|
49
79
|
readonly input: string | readonly string[];
|
|
@@ -62,6 +92,12 @@ export interface AiModelClient {
|
|
|
62
92
|
readonly driverId: AiDriverId;
|
|
63
93
|
readonly model: string;
|
|
64
94
|
complete(options: AiCompleteOptions): Promise<AiCompleteResult>;
|
|
95
|
+
/**
|
|
96
|
+
* Stream tokens. Optional — callers fail loud when missing (no stub echo).
|
|
97
|
+
*
|
|
98
|
+
* @param options - Same as complete, plus optional signal
|
|
99
|
+
*/
|
|
100
|
+
stream?(options: AiCompleteOptions): AsyncIterable<AiStreamChunk>;
|
|
65
101
|
embed?(options: AiEmbedOptions): Promise<AiEmbedResult>;
|
|
66
102
|
close?(): Promise<void>;
|
|
67
103
|
}
|
|
@@ -71,6 +107,11 @@ export interface AiOpenOptions {
|
|
|
71
107
|
readonly model?: string;
|
|
72
108
|
readonly apiKey?: string;
|
|
73
109
|
readonly baseUrl?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Extra HTTP headers (e.g. OpenRouter `HTTP-Referer` / `X-Title`).
|
|
112
|
+
* Merged into chat and embed requests; does not invent a new driver.
|
|
113
|
+
*/
|
|
114
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
74
115
|
/** Mock canned responses keyed by prompt name or substring. */
|
|
75
116
|
readonly mockResponses?: Readonly<Record<string, unknown>>;
|
|
76
117
|
readonly fetch?: typeof globalThis.fetch;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `smtp` channel driver — wraps sently's SMTP transport
|
|
2
|
+
* `smtp` channel driver — wraps sently's SMTP transport with the Bun socket adapter.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { BunAdapter } from "sently/adapters/bun";
|
|
5
6
|
import { SMTPTransport } from "sently/transports/smtp";
|
|
6
7
|
import type { ChannelDriver, ChannelOpenOptions } from "./channel-types.ts";
|
|
7
8
|
|
|
@@ -14,9 +15,14 @@ export function openSmtpChannel(options: ChannelOpenOptions = {}): ChannelDriver
|
|
|
14
15
|
if (!options.host) {
|
|
15
16
|
throw new Error("smtp channel: host is required");
|
|
16
17
|
}
|
|
18
|
+
const port = options.port ?? 587;
|
|
19
|
+
// Implicit TLS only on the classic SMTPS port; Mailpit / local relays are plain.
|
|
20
|
+
const secure = port === 465;
|
|
17
21
|
const transport = new SMTPTransport({
|
|
18
22
|
host: options.host,
|
|
19
|
-
port
|
|
23
|
+
port,
|
|
24
|
+
secure,
|
|
25
|
+
adapter: new BunAdapter({ secure }),
|
|
20
26
|
...(options.user && options.pass ? { auth: { user: options.user, pass: options.pass } } : {}),
|
|
21
27
|
});
|
|
22
28
|
return { id: "smtp", transport };
|
package/src/drivers/index.ts
CHANGED
|
@@ -147,8 +147,11 @@ export { webpushChannelDriver, openWebPushChannel } from "./channel-webpush.ts";
|
|
|
147
147
|
export type {
|
|
148
148
|
AiDriverId,
|
|
149
149
|
AiMessage,
|
|
150
|
+
AiToolDef,
|
|
151
|
+
AiToolCall,
|
|
150
152
|
AiCompleteOptions,
|
|
151
153
|
AiCompleteResult,
|
|
154
|
+
AiStreamChunk,
|
|
152
155
|
AiEmbedOptions,
|
|
153
156
|
AiEmbedResult,
|
|
154
157
|
AiModelClient,
|
|
@@ -158,4 +161,21 @@ export type {
|
|
|
158
161
|
|
|
159
162
|
export { mockAiDriver, createMockAiDriver } from "./ai-mock.ts";
|
|
160
163
|
export { anthropicAiDriver, openAnthropic } from "./ai-anthropic.ts";
|
|
161
|
-
export {
|
|
164
|
+
export {
|
|
165
|
+
openaiCompatibleAiDriver,
|
|
166
|
+
openOpenaiCompatible,
|
|
167
|
+
OPENAI_COMPAT_DEFAULT_BASE,
|
|
168
|
+
normalizeOpenaiCompatibleBaseUrl,
|
|
169
|
+
isOpenaiCloudBase,
|
|
170
|
+
openaiCompatibleHeaders,
|
|
171
|
+
} from "./ai-openai-compatible.ts";
|
|
172
|
+
export {
|
|
173
|
+
ollamaAiDriver,
|
|
174
|
+
openOllama,
|
|
175
|
+
OllamaUnavailableError,
|
|
176
|
+
OLLAMA_DEFAULT_MODEL,
|
|
177
|
+
OLLAMA_DEFAULT_BASE_URL,
|
|
178
|
+
normalizeOllamaBaseUrl,
|
|
179
|
+
resolveOllamaBaseUrl,
|
|
180
|
+
resolveOllamaModel,
|
|
181
|
+
} from "./ai-ollama.ts";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol-named re-export — prefer `okengine/drivers/ai-ollama` or
|
|
3
|
+
* `okengine/drivers` (`ollamaAiDriver`). Same surface as {@link ./ai-ollama.ts}.
|
|
4
|
+
*/
|
|
5
|
+
export {
|
|
6
|
+
ollamaAiDriver,
|
|
7
|
+
openOllama,
|
|
8
|
+
OllamaUnavailableError,
|
|
9
|
+
OLLAMA_DEFAULT_MODEL,
|
|
10
|
+
OLLAMA_DEFAULT_BASE_URL,
|
|
11
|
+
normalizeOllamaBaseUrl,
|
|
12
|
+
resolveOllamaBaseUrl,
|
|
13
|
+
resolveOllamaModel,
|
|
14
|
+
} from "./ai-ollama.ts";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI rate presets — gate.rate only, no parallel budgeting system.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { memoryKvDriver } from "../../drivers/index.ts";
|
|
7
|
+
import { createGateRuntime } from "../gate.ts";
|
|
8
|
+
import { AI_RATE_PRESETS, aiRateGate, createAiRateGates } from "./rate.ts";
|
|
9
|
+
|
|
10
|
+
describe("AI_RATE_PRESETS", () => {
|
|
11
|
+
test("ask is stricter than embed; agent is strictest", () => {
|
|
12
|
+
expect(AI_RATE_PRESETS.ask).toEqual({ max: 20, per: "1m", keyBy: "user" });
|
|
13
|
+
expect(AI_RATE_PRESETS.agent).toEqual({ max: 10, per: "1m", keyBy: "user" });
|
|
14
|
+
expect(AI_RATE_PRESETS.embed).toEqual({ max: 60, per: "1m", keyBy: "user" });
|
|
15
|
+
expect(AI_RATE_PRESETS.agent.max).toBeLessThan(AI_RATE_PRESETS.ask.max);
|
|
16
|
+
expect(AI_RATE_PRESETS.ask.max).toBeLessThan(AI_RATE_PRESETS.embed.max);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("aiRateGate builds a real gate.rate decl", () => {
|
|
20
|
+
const g = aiRateGate("ask");
|
|
21
|
+
expect(g.kind).toBe("rate");
|
|
22
|
+
expect(g.max).toBe(20);
|
|
23
|
+
expect(g.per).toBe("1m");
|
|
24
|
+
expect(g.keyBy).toBe("user");
|
|
25
|
+
expect(g.name).toContain("20/1m");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("createAiRateGates materializes three decls; deny after max", async () => {
|
|
29
|
+
const kv = await memoryKvDriver.open({ name: "ai-rate" });
|
|
30
|
+
const gates = createAiRateGates();
|
|
31
|
+
expect(gates).toHaveLength(3);
|
|
32
|
+
const runtime = createGateRuntime({ gates: [...gates], kv, now: () => 1_000 });
|
|
33
|
+
const ask = gates[0]!;
|
|
34
|
+
const ctx = {
|
|
35
|
+
auth: { userId: "u1", scopes: new Set<string>() },
|
|
36
|
+
operator: { id: null },
|
|
37
|
+
meta: {},
|
|
38
|
+
};
|
|
39
|
+
for (let i = 0; i < ask.max; i++) {
|
|
40
|
+
const ev = await runtime.check([ask.name], ctx);
|
|
41
|
+
expect(ev.every((e) => e.allowed)).toBe(true);
|
|
42
|
+
}
|
|
43
|
+
const denied = await runtime.check([ask.name], ctx);
|
|
44
|
+
expect(denied.some((e) => !e.allowed)).toBe(true);
|
|
45
|
+
await kv.close();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("public AI edge can override keyBy to ip", () => {
|
|
49
|
+
const g = aiRateGate("ask", { keyBy: "ip", max: 5 });
|
|
50
|
+
expect(g.keyBy).toBe("ip");
|
|
51
|
+
expect(g.max).toBe(5);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI rate-limit presets — reuse {@link gate.rate}, no parallel budgeting.
|
|
3
|
+
*
|
|
4
|
+
* Cost caps stay on prompt/agent `budget` decls. These presets throttle
|
|
5
|
+
* request volume on HTTP triggers that wrap `fx.ask` / agents / embeds.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { gate, type RateGateDecl } from "../gate/declare.ts";
|
|
9
|
+
|
|
10
|
+
/** Preset keys for AI-facing HTTP edges. */
|
|
11
|
+
export type AiRatePreset = "ask" | "agent" | "embed";
|
|
12
|
+
|
|
13
|
+
/** One AI rate preset row. */
|
|
14
|
+
export type AiRatePresetSpec = {
|
|
15
|
+
readonly max: number;
|
|
16
|
+
readonly per: string;
|
|
17
|
+
readonly keyBy: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sensible defaults: AI calls are far more expensive per request than
|
|
22
|
+
* ordinary HTTP, so limits are tighter than typical API rate limits.
|
|
23
|
+
* Prefer `keyBy: "user"` when the edge is authenticated; use `ip` on
|
|
24
|
+
* public unauthenticated AI surfaces.
|
|
25
|
+
*/
|
|
26
|
+
export const AI_RATE_PRESETS: Readonly<Record<AiRatePreset, AiRatePresetSpec>> = {
|
|
27
|
+
ask: { max: 20, per: "1m", keyBy: "user" },
|
|
28
|
+
agent: { max: 10, per: "1m", keyBy: "user" },
|
|
29
|
+
embed: { max: 60, per: "1m", keyBy: "user" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build a `gate.rate` declaration from an AI preset.
|
|
34
|
+
*
|
|
35
|
+
* @param kind - ask · agent · embed
|
|
36
|
+
* @param overrides - Optional max / per / keyBy overrides
|
|
37
|
+
*/
|
|
38
|
+
export function aiRateGate(
|
|
39
|
+
kind: AiRatePreset,
|
|
40
|
+
overrides?: {
|
|
41
|
+
readonly max?: number;
|
|
42
|
+
readonly per?: string;
|
|
43
|
+
readonly keyBy?: string;
|
|
44
|
+
readonly description?: string;
|
|
45
|
+
},
|
|
46
|
+
): RateGateDecl {
|
|
47
|
+
const preset = AI_RATE_PRESETS[kind];
|
|
48
|
+
return gate.rate({
|
|
49
|
+
max: overrides?.max ?? preset.max,
|
|
50
|
+
per: overrides?.per ?? preset.per,
|
|
51
|
+
keyBy: overrides?.keyBy ?? preset.keyBy,
|
|
52
|
+
description:
|
|
53
|
+
overrides?.description ??
|
|
54
|
+
`AI ${kind} rate limit (${overrides?.max ?? preset.max}/${overrides?.per ?? preset.per})`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Materialize all AI rate gate decls (ask + agent + embed).
|
|
60
|
+
*
|
|
61
|
+
* @param enabled - When false, returns []
|
|
62
|
+
*/
|
|
63
|
+
export function createAiRateGates(enabled = true): readonly RateGateDecl[] {
|
|
64
|
+
if (!enabled) return [];
|
|
65
|
+
return [aiRateGate("ask"), aiRateGate("agent"), aiRateGate("embed")];
|
|
66
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redacted values must never leak into provider-facing prompt content.
|
|
3
|
+
* Same class of protection as fx.log — AI prompts are an egress vector.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import type { AiCompleteOptions, AiModelClient } from "../../drivers/ai-types.ts";
|
|
8
|
+
import { REDACTED_PLACEHOLDER, Redacted } from "../../kernel/redacted.ts";
|
|
9
|
+
import { ai, createAiRuntime, promptContentFromInput } from "../ai.ts";
|
|
10
|
+
|
|
11
|
+
describe("Redacted never leaks into AI prompts", () => {
|
|
12
|
+
test("promptContentFromInput masks nested Redacted to placeholder", () => {
|
|
13
|
+
const secret = new Redacted("sk-live-super-secret");
|
|
14
|
+
const content = promptContentFromInput({
|
|
15
|
+
key: secret,
|
|
16
|
+
nested: { token: secret },
|
|
17
|
+
note: `billing with ${secret}`,
|
|
18
|
+
});
|
|
19
|
+
expect(content).toContain(REDACTED_PLACEHOLDER);
|
|
20
|
+
expect(content).not.toContain("sk-live-super-secret");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("ask sends placeholder to the model — never cleartext", async () => {
|
|
24
|
+
const secret = new Redacted("vault-stripe-key-REAL");
|
|
25
|
+
const captured: AiCompleteOptions[] = [];
|
|
26
|
+
const client: AiModelClient = {
|
|
27
|
+
driverId: "mock",
|
|
28
|
+
model: "smart",
|
|
29
|
+
async complete(opts) {
|
|
30
|
+
captured.push(opts);
|
|
31
|
+
return {
|
|
32
|
+
text: JSON.stringify({ ok: true }),
|
|
33
|
+
raw: { ok: true },
|
|
34
|
+
model: "smart",
|
|
35
|
+
driverId: "mock",
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const smart = ai.model("smart");
|
|
41
|
+
const prompt = smart.prompt("secure-ask", { out: { ok: "boolean" } });
|
|
42
|
+
const runtime = createAiRuntime({
|
|
43
|
+
models: [smart],
|
|
44
|
+
prompts: [prompt],
|
|
45
|
+
clients: { smart: client },
|
|
46
|
+
forceJournal: false,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await runtime.ask("secure-ask", {
|
|
50
|
+
apiKey: secret,
|
|
51
|
+
nested: [secret],
|
|
52
|
+
label: `charge ${secret}`,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(captured).toHaveLength(1);
|
|
56
|
+
const content = captured[0]!.messages[0]!.content;
|
|
57
|
+
expect(content).toContain(REDACTED_PLACEHOLDER);
|
|
58
|
+
expect(content).not.toContain("vault-stripe-key-REAL");
|
|
59
|
+
expect(JSON.stringify(captured[0]!.messages)).not.toContain("vault-stripe-key-REAL");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("template-string ask input already stringified stays placeholder-only", async () => {
|
|
63
|
+
const secret = new Redacted("TOP-SECRET-VALUE");
|
|
64
|
+
const captured: string[] = [];
|
|
65
|
+
const client: AiModelClient = {
|
|
66
|
+
driverId: "mock",
|
|
67
|
+
model: "smart",
|
|
68
|
+
async complete(opts) {
|
|
69
|
+
captured.push(opts.messages[0]!.content);
|
|
70
|
+
return {
|
|
71
|
+
text: "{}",
|
|
72
|
+
raw: {},
|
|
73
|
+
model: "smart",
|
|
74
|
+
driverId: "mock",
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const smart = ai.model("smart");
|
|
79
|
+
const runtime = createAiRuntime({
|
|
80
|
+
models: [smart],
|
|
81
|
+
prompts: [smart.prompt("t")],
|
|
82
|
+
clients: { smart: client },
|
|
83
|
+
forceJournal: false,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await runtime.ask("t", `user said ${secret}`);
|
|
87
|
+
expect(captured[0]).toBe(`user said ${REDACTED_PLACEHOLDER}`);
|
|
88
|
+
expect(captured[0]).not.toContain("TOP-SECRET-VALUE");
|
|
89
|
+
});
|
|
90
|
+
});
|