okengine 0.22.0 → 0.23.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/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +7 -6
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-infer.ts +27 -1
- package/src/compiler/extract.ts +268 -12
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.ts +289 -2
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +103 -1
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +15 -4
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +312 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/journal.ts +104 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
package/src/kernel/fx.test.ts
CHANGED
|
@@ -483,9 +483,7 @@ describe("fx — wholesale swap", () => {
|
|
|
483
483
|
async deliverOtp(opts) {
|
|
484
484
|
return { ok: true as const, channel: opts.only ?? opts.channels[0] ?? "email" };
|
|
485
485
|
},
|
|
486
|
-
async
|
|
487
|
-
return {};
|
|
488
|
-
},
|
|
486
|
+
ask: (async () => ({})) as unknown as Fx["ask"],
|
|
489
487
|
async embed() {
|
|
490
488
|
return [];
|
|
491
489
|
},
|
|
@@ -495,9 +493,18 @@ describe("fx — wholesale swap", () => {
|
|
|
495
493
|
async fetch() {
|
|
496
494
|
return new Response(null, { status: 204 });
|
|
497
495
|
},
|
|
498
|
-
async
|
|
499
|
-
return {};
|
|
496
|
+
async decide<T>(): Promise<T> {
|
|
497
|
+
return {} as T;
|
|
498
|
+
},
|
|
499
|
+
agent: {
|
|
500
|
+
async approve() {
|
|
501
|
+
return { ok: true as const };
|
|
502
|
+
},
|
|
503
|
+
async deny() {
|
|
504
|
+
return { ok: true as const };
|
|
505
|
+
},
|
|
500
506
|
},
|
|
507
|
+
run: (async () => ({})) as unknown as Fx["run"],
|
|
501
508
|
async *stream() {
|
|
502
509
|
/* empty */
|
|
503
510
|
},
|
package/src/kernel/fx.ts
CHANGED
|
@@ -38,7 +38,10 @@ import type { SignalDecl, SignalRuntime } from "../elements/signal.ts";
|
|
|
38
38
|
import type { DeadLetter, SignalEmitOptions } from "../drivers/signal-types.ts";
|
|
39
39
|
import type { VaultActor, VaultAdapter, VaultRuntime } from "../elements/vault.ts";
|
|
40
40
|
import type { ChannelRuntime } from "../elements/channel.ts";
|
|
41
|
+
import type { AgentApprovalResolveResult } from "../elements/ai/approval.ts";
|
|
41
42
|
import type { AiRuntime } from "../elements/ai.ts";
|
|
43
|
+
import type { AgUiEvent } from "../elements/ai/events.ts";
|
|
44
|
+
import type { AiMessage } from "../drivers/ai-types.ts";
|
|
42
45
|
import { parseDurationMs } from "../elements/clock/duration.ts";
|
|
43
46
|
import type { ApiKeyStore } from "../auth/api-keys.ts";
|
|
44
47
|
import type { FxAuthIdentity, FxAuthKeyMethods } from "./fx-auth-keys.ts";
|
|
@@ -62,14 +65,14 @@ import {
|
|
|
62
65
|
touchDryRunStore,
|
|
63
66
|
} from "./dry-run.ts";
|
|
64
67
|
import type { FailFn } from "./errors.ts";
|
|
65
|
-
import { abortError, currentAbortSignal, linkAbort } from "./abort-scope.ts";
|
|
68
|
+
import { abortError, currentAbortSignal, linkAbort, withAbortSignal } from "./abort-scope.ts";
|
|
66
69
|
import type { FxRetryOptions, FxThunk } from "./concurrency.ts";
|
|
67
70
|
import { maskRedactedDeep, Redacted } from "./redacted.ts";
|
|
68
71
|
import type { JournalSession, JournalStepOptions } from "./journal.ts";
|
|
69
72
|
|
|
70
73
|
/** Options for {@link Fx.step} — see {@link JournalStepOptions}. */
|
|
71
74
|
export type StepOptions<T> = JournalStepOptions<T>;
|
|
72
|
-
import type
|
|
75
|
+
import { type RunTelemetry } from "./run-telemetry.ts";
|
|
73
76
|
import type { RunsRuntime } from "../runs/runtime.ts";
|
|
74
77
|
import type { RunsRow, WideEvent } from "../runs/types.ts";
|
|
75
78
|
import type { RunWindowStats, SloBreach } from "../runs/window.ts";
|
|
@@ -114,7 +117,33 @@ function loadFxLiveStream(): typeof import("./fx-live-stream.ts") {
|
|
|
114
117
|
return lazyRequire(import.meta.dir, ["fx", "live", "stream"].join("-"));
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Split `fx.run` input into one message or a history.
|
|
122
|
+
*
|
|
123
|
+
* @param input - String, `{ message }`, or `{ messages }`
|
|
124
|
+
*/
|
|
125
|
+
function agentTurn(input: unknown): { message?: string; messages?: readonly AiMessage[] } {
|
|
126
|
+
if (typeof input === "string") return { message: input };
|
|
127
|
+
if (input && typeof input === "object") {
|
|
128
|
+
const record = input as { message?: unknown; messages?: unknown };
|
|
129
|
+
const hasMessage = "message" in record && record.message !== undefined;
|
|
130
|
+
const hasMessages = "messages" in record && record.messages !== undefined;
|
|
131
|
+
if (hasMessage && hasMessages) {
|
|
132
|
+
throw new TypeError("fx.run: pass message or messages, not both");
|
|
133
|
+
}
|
|
134
|
+
if (hasMessages && Array.isArray(record.messages)) {
|
|
135
|
+
return { messages: record.messages as readonly AiMessage[] };
|
|
136
|
+
}
|
|
137
|
+
if (hasMessage) return { message: String(record.message) };
|
|
138
|
+
}
|
|
139
|
+
return { message: JSON.stringify(input ?? {}) };
|
|
140
|
+
}
|
|
141
|
+
|
|
117
142
|
/** `fx.fetch` — keep host parsing + dry-run stub off cold edge / Store-only graphs. */
|
|
143
|
+
function loadFxDecide(): typeof import("./fx-decide.ts") {
|
|
144
|
+
return lazyRequire(import.meta.dir, ["fx", "decide"].join("-"));
|
|
145
|
+
}
|
|
146
|
+
|
|
118
147
|
function loadFxFetch(): typeof import("./fx-fetch.ts") {
|
|
119
148
|
return lazyRequire(import.meta.dir, ["fx", "fetch"].join("-"));
|
|
120
149
|
}
|
|
@@ -394,6 +423,8 @@ export interface FxAskOptions {
|
|
|
394
423
|
readonly tools?: readonly NamedRef[];
|
|
395
424
|
/** Bound on tool invocations (default 6). */
|
|
396
425
|
readonly maxSteps?: number;
|
|
426
|
+
/** Yield AG-UI events instead of resolving to the validated object. */
|
|
427
|
+
readonly stream?: boolean;
|
|
397
428
|
}
|
|
398
429
|
|
|
399
430
|
/** Options for {@link Fx.search}. */
|
|
@@ -406,11 +437,13 @@ export type { JsonResult, JsonStreamResult };
|
|
|
406
437
|
|
|
407
438
|
const sseFrameBrand: unique symbol = Symbol.for("oke.sse.frame");
|
|
408
439
|
|
|
409
|
-
/** One SSE frame — optional `id:` plus JSON `data
|
|
440
|
+
/** One SSE frame — optional `id:` plus JSON `data:`, or a comment line. */
|
|
410
441
|
export interface SseFrame {
|
|
411
442
|
readonly [sseFrameBrand]: true;
|
|
412
443
|
readonly data: unknown;
|
|
413
444
|
readonly id?: string;
|
|
445
|
+
/** Comment body. When set and `data` is undefined, the encoder emits `: comment`. */
|
|
446
|
+
readonly comment?: string;
|
|
414
447
|
}
|
|
415
448
|
|
|
416
449
|
/**
|
|
@@ -423,6 +456,15 @@ export function sseFrame(data: unknown, id?: string): SseFrame {
|
|
|
423
456
|
return id !== undefined ? { [sseFrameBrand]: true, data, id } : { [sseFrameBrand]: true, data };
|
|
424
457
|
}
|
|
425
458
|
|
|
459
|
+
/**
|
|
460
|
+
* SSE comment (`: keepalive`). Keeps a followed run from being idle-closed.
|
|
461
|
+
*
|
|
462
|
+
* @param text - Comment text
|
|
463
|
+
*/
|
|
464
|
+
export function sseComment(text: string): SseFrame {
|
|
465
|
+
return { [sseFrameBrand]: true, data: undefined, comment: text };
|
|
466
|
+
}
|
|
467
|
+
|
|
426
468
|
/**
|
|
427
469
|
* True when `value` is a branded SSE frame.
|
|
428
470
|
*
|
|
@@ -781,6 +823,11 @@ export interface Fx {
|
|
|
781
823
|
* @param opts - Model routing opts
|
|
782
824
|
*/
|
|
783
825
|
ask(prompt: NamedRef, input?: unknown, opts?: FxAskOptions): Promise<Record<string, unknown>>;
|
|
826
|
+
ask(
|
|
827
|
+
prompt: NamedRef,
|
|
828
|
+
input: unknown,
|
|
829
|
+
opts: FxAskOptions & { readonly stream: true },
|
|
830
|
+
): AsyncIterable<AgUiEvent>;
|
|
784
831
|
/**
|
|
785
832
|
* Embed text via a named model (records `embed` — distinct from `ask`).
|
|
786
833
|
* Returns the vector; does not write an index.
|
|
@@ -808,13 +855,40 @@ export interface Fx {
|
|
|
808
855
|
* @param init - Standard `RequestInit`
|
|
809
856
|
*/
|
|
810
857
|
fetch(url: string | URL, init?: RequestInit): Promise<Response>;
|
|
858
|
+
/**
|
|
859
|
+
* Run one declared decision. `$` carries `how` and `audited`.
|
|
860
|
+
* Abstain mode makes each question value `T | null`.
|
|
861
|
+
*/
|
|
862
|
+
decide<T>(decision: { name: string }, input: unknown): Promise<T>;
|
|
863
|
+
/**
|
|
864
|
+
* Resolve a pending agent tool approval (records nothing until the tool runs).
|
|
865
|
+
*
|
|
866
|
+
* First resolution wins. A later call returns a conflict failure.
|
|
867
|
+
*/
|
|
868
|
+
readonly agent: {
|
|
869
|
+
/**
|
|
870
|
+
* @param id - Approval id from the interrupt
|
|
871
|
+
* @param opts - Optional replacement args
|
|
872
|
+
*/
|
|
873
|
+
approve(id: string, opts?: { readonly args?: unknown }): Promise<AgentApprovalResolveResult>;
|
|
874
|
+
/**
|
|
875
|
+
* @param id - Approval id from the interrupt
|
|
876
|
+
* @param opts - Denial reason fed back to the model
|
|
877
|
+
*/
|
|
878
|
+
deny(id: string, opts?: { readonly reason?: string }): Promise<AgentApprovalResolveResult>;
|
|
879
|
+
};
|
|
811
880
|
/**
|
|
812
881
|
* Run a bounded AI agent (records `ask`).
|
|
813
882
|
*
|
|
814
883
|
* @param agent - Agent name or handle
|
|
815
|
-
* @param input - Agent input (`{ message }` or string)
|
|
884
|
+
* @param input - Agent input (`{ message }`, `{ messages }`, or string)
|
|
816
885
|
*/
|
|
817
886
|
run(agent: NamedRef, input?: unknown): Promise<unknown>;
|
|
887
|
+
run(
|
|
888
|
+
agent: NamedRef,
|
|
889
|
+
input: unknown,
|
|
890
|
+
opts: { readonly stream: true; readonly threadId?: string },
|
|
891
|
+
): AsyncIterable<AgUiEvent>;
|
|
818
892
|
/**
|
|
819
893
|
* Stream model tokens (records `ask`). Returns an async iterable of chunks.
|
|
820
894
|
*
|
|
@@ -844,6 +918,8 @@ export interface Fx {
|
|
|
844
918
|
t(key: AppMessageKey, values?: MessageValues): string;
|
|
845
919
|
/** Active locale for {@link Fx.t} and default channel sends. */
|
|
846
920
|
readonly locale: string;
|
|
921
|
+
/** HTTP `Last-Event-ID` on this request, when the client sent one. */
|
|
922
|
+
readonly lastEventId?: string;
|
|
847
923
|
/** Generate a unique id (OKID). */
|
|
848
924
|
id(): string;
|
|
849
925
|
/** User-plane auth principal. */
|
|
@@ -1200,11 +1276,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1200
1276
|
externalOf?:
|
|
1201
1277
|
| EffectExternal
|
|
1202
1278
|
| ((result: T | undefined, error: unknown) => EffectExternal | undefined),
|
|
1279
|
+
stamp?: (result: T | undefined) => { readonly agentRunId?: string } | undefined,
|
|
1203
1280
|
): Promise<T> {
|
|
1204
1281
|
const signal = currentAbortSignal();
|
|
1205
1282
|
if (signal.aborted) throw abortError(signal.reason);
|
|
1206
1283
|
capability.assert(kind, resource);
|
|
1207
|
-
const execute = () => recordEffect(ledger, kind, resource, now, body, externalOf);
|
|
1284
|
+
const execute = () => recordEffect(ledger, kind, resource, now, body, externalOf, stamp);
|
|
1208
1285
|
if (journal) {
|
|
1209
1286
|
return journal.effect(kind, resource, execute);
|
|
1210
1287
|
}
|
|
@@ -1422,6 +1499,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1422
1499
|
|
|
1423
1500
|
const aiDisablesCache = options.aiRuntime?.autoCacheDisabled === true;
|
|
1424
1501
|
const telemetry = options.runTelemetry;
|
|
1502
|
+
if (telemetry) {
|
|
1503
|
+
lazyRequire<{ bindDecisionLabelTelemetry(telemetry: RunTelemetry): void }>(
|
|
1504
|
+
`${import.meta.dir}/../elements/ai/decisions`,
|
|
1505
|
+
["lab", "els"].join(""),
|
|
1506
|
+
).bindDecisionLabelTelemetry(telemetry);
|
|
1507
|
+
}
|
|
1425
1508
|
|
|
1426
1509
|
const cache: FxCache = {
|
|
1427
1510
|
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
@@ -1663,6 +1746,37 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1663
1746
|
},
|
|
1664
1747
|
};
|
|
1665
1748
|
|
|
1749
|
+
async function resolveAgentDecision(
|
|
1750
|
+
id: string,
|
|
1751
|
+
decision: {
|
|
1752
|
+
readonly decision: "approve" | "deny";
|
|
1753
|
+
readonly args?: unknown;
|
|
1754
|
+
readonly reason?: string;
|
|
1755
|
+
},
|
|
1756
|
+
): Promise<AgentApprovalResolveResult> {
|
|
1757
|
+
const runtime = options.aiRuntime;
|
|
1758
|
+
if (!runtime?.resolveApproval) return { ok: false, status: 404 };
|
|
1759
|
+
const approver = auth.userId ?? operator.id;
|
|
1760
|
+
return runtime.resolveApproval(
|
|
1761
|
+
id,
|
|
1762
|
+
{
|
|
1763
|
+
decision: decision.decision,
|
|
1764
|
+
tenant: tenant.id,
|
|
1765
|
+
...(decision.args !== undefined ? { args: decision.args } : {}),
|
|
1766
|
+
...(decision.reason !== undefined ? { reason: decision.reason } : {}),
|
|
1767
|
+
...(approver ? { approver } : {}),
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
auth: {
|
|
1771
|
+
userId: auth.userId,
|
|
1772
|
+
scopes: auth.scopes,
|
|
1773
|
+
verified: auth.verified,
|
|
1774
|
+
},
|
|
1775
|
+
operator: { id: operator.id },
|
|
1776
|
+
},
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1666
1780
|
const fx: Fx = {
|
|
1667
1781
|
store: storeHandle,
|
|
1668
1782
|
runs: runsSurface,
|
|
@@ -1815,8 +1929,37 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1815
1929
|
return { ok: true as const, channel: result.channel };
|
|
1816
1930
|
});
|
|
1817
1931
|
},
|
|
1818
|
-
ask(prompt, input, opts) {
|
|
1932
|
+
ask: function (prompt, input, opts) {
|
|
1819
1933
|
const name = resolveName(prompt);
|
|
1934
|
+
if (opts?.stream) {
|
|
1935
|
+
return (async function* () {
|
|
1936
|
+
await gated("ask", name, async () => undefined);
|
|
1937
|
+
if (isDryRun()) {
|
|
1938
|
+
recordWouldHaveFired("ask", name);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (!options.aiRuntime) {
|
|
1942
|
+
throw new Error(`fx.ask: AI runtime is not configured for prompt "${name}"`);
|
|
1943
|
+
}
|
|
1944
|
+
const local = new AbortController();
|
|
1945
|
+
const unlink = linkAbort(currentAbortSignal(), local);
|
|
1946
|
+
try {
|
|
1947
|
+
const events = await withAbortSignal(local.signal, () =>
|
|
1948
|
+
options.aiRuntime!.streamAsk(name, input, {
|
|
1949
|
+
via: opts.via?.map(resolveName),
|
|
1950
|
+
...(opts.timeout !== undefined ? { timeout: opts.timeout } : {}),
|
|
1951
|
+
tools: opts.tools?.map(resolveName),
|
|
1952
|
+
maxSteps: opts.maxSteps,
|
|
1953
|
+
callTool: (tool, toolInput) => fx.call(tool, toolInput),
|
|
1954
|
+
}),
|
|
1955
|
+
);
|
|
1956
|
+
yield* events;
|
|
1957
|
+
} finally {
|
|
1958
|
+
unlink();
|
|
1959
|
+
if (!local.signal.aborted) local.abort();
|
|
1960
|
+
}
|
|
1961
|
+
})();
|
|
1962
|
+
}
|
|
1820
1963
|
return gated(
|
|
1821
1964
|
"ask",
|
|
1822
1965
|
name,
|
|
@@ -1844,7 +1987,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1844
1987
|
},
|
|
1845
1988
|
() => options.aiRuntime?.lastExternal,
|
|
1846
1989
|
);
|
|
1847
|
-
},
|
|
1990
|
+
} as Fx["ask"],
|
|
1848
1991
|
embed(model, text) {
|
|
1849
1992
|
const name = resolveName(model);
|
|
1850
1993
|
return gated(
|
|
@@ -1890,29 +2033,158 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1890
2033
|
fetch(url, init) {
|
|
1891
2034
|
return loadFxFetch().runFxFetch(gated, url, init);
|
|
1892
2035
|
},
|
|
1893
|
-
|
|
2036
|
+
decide<T>(decision: { name: string }, input: unknown) {
|
|
2037
|
+
return loadFxDecide().runFxDecide({
|
|
2038
|
+
gated,
|
|
2039
|
+
journal: options.journal,
|
|
2040
|
+
signal: currentAbortSignal(),
|
|
2041
|
+
now,
|
|
2042
|
+
runId: options.runId,
|
|
2043
|
+
tenantId: tenant.id,
|
|
2044
|
+
getSecret: async (secretName) => {
|
|
2045
|
+
const value = await gatedSecret(secretName, async () => secrets[secretName]);
|
|
2046
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
2047
|
+
},
|
|
2048
|
+
decision,
|
|
2049
|
+
input,
|
|
2050
|
+
}) as Promise<T>;
|
|
2051
|
+
},
|
|
2052
|
+
agent: {
|
|
2053
|
+
approve(id, opts) {
|
|
2054
|
+
return resolveAgentDecision(id, { decision: "approve", args: opts?.args });
|
|
2055
|
+
},
|
|
2056
|
+
deny(id, opts) {
|
|
2057
|
+
return resolveAgentDecision(id, { decision: "deny", reason: opts?.reason });
|
|
2058
|
+
},
|
|
2059
|
+
},
|
|
2060
|
+
run: function (agent, input, opts?: { readonly stream?: boolean; readonly threadId?: string }) {
|
|
1894
2061
|
const name = resolveName(agent);
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2062
|
+
const turn = agentTurn(input);
|
|
2063
|
+
if (opts?.stream) {
|
|
2064
|
+
let inner: (AsyncIterable<AgUiEvent> & { readonly result?: Promise<unknown> }) | undefined;
|
|
2065
|
+
let heldRunId: string | undefined;
|
|
2066
|
+
let markRun: () => void = () => undefined;
|
|
2067
|
+
const heldReady = new Promise<void>((resolve) => {
|
|
2068
|
+
markRun = resolve;
|
|
2069
|
+
});
|
|
2070
|
+
const gen = (async function* () {
|
|
2071
|
+
if (!options.aiRuntime) {
|
|
2072
|
+
await gated("ask", name, async () => {
|
|
2073
|
+
throw new Error(`fx.run: AI runtime is not configured for agent "${name}"`);
|
|
2074
|
+
});
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const local = new AbortController();
|
|
2078
|
+
const unlink = linkAbort(currentAbortSignal(), local);
|
|
2079
|
+
try {
|
|
2080
|
+
await withAbortSignal(local.signal, async () => {
|
|
2081
|
+
// The inner tool loop journals each step. Recording the whole
|
|
2082
|
+
// stream as one ask effect would replay the run id and skip
|
|
2083
|
+
// the parked tool.
|
|
2084
|
+
capability.assert("ask", name);
|
|
2085
|
+
const events = await options.aiRuntime!.streamAgent(name, {
|
|
2086
|
+
...turn,
|
|
2087
|
+
...(opts.threadId !== undefined ? { threadId: opts.threadId } : {}),
|
|
2088
|
+
gates: (options.rlsGateNames ?? []).filter((gate) => gate !== "public"),
|
|
2089
|
+
userId: auth.userId,
|
|
2090
|
+
operatorId: operator.id,
|
|
2091
|
+
...(options.journal ? { journal: options.journal } : {}),
|
|
2092
|
+
...(options.flow !== undefined ? { flow: options.flow } : {}),
|
|
2093
|
+
tenantId: tenant.id,
|
|
2094
|
+
auth: {
|
|
2095
|
+
userId: auth.userId,
|
|
2096
|
+
scopes: auth.scopes,
|
|
2097
|
+
verified: auth.verified,
|
|
2098
|
+
},
|
|
2099
|
+
callTool: (tool, toolInput) => fx.call(tool, toolInput),
|
|
2100
|
+
recordCall: (tool, runId) => {
|
|
2101
|
+
void gated(
|
|
2102
|
+
"call",
|
|
2103
|
+
tool,
|
|
2104
|
+
async () => runId,
|
|
2105
|
+
undefined,
|
|
2106
|
+
(id) =>
|
|
2107
|
+
typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined,
|
|
2108
|
+
);
|
|
2109
|
+
},
|
|
2110
|
+
onRunId(id) {
|
|
2111
|
+
heldRunId = id;
|
|
2112
|
+
markRun();
|
|
2113
|
+
},
|
|
2114
|
+
});
|
|
2115
|
+
inner = events as AsyncIterable<AgUiEvent> & {
|
|
2116
|
+
readonly result?: Promise<unknown>;
|
|
2117
|
+
};
|
|
2118
|
+
const settled = inner.result?.then(
|
|
2119
|
+
() => undefined,
|
|
2120
|
+
() => undefined,
|
|
2121
|
+
);
|
|
2122
|
+
await Promise.race([heldReady, settled ?? Promise.resolve()]);
|
|
2123
|
+
await recordEffect(
|
|
2124
|
+
ledger,
|
|
2125
|
+
"ask",
|
|
2126
|
+
name,
|
|
2127
|
+
now,
|
|
2128
|
+
async () => heldRunId,
|
|
2129
|
+
undefined,
|
|
2130
|
+
(id) => (typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined),
|
|
2131
|
+
);
|
|
2132
|
+
});
|
|
2133
|
+
if (inner) yield* inner;
|
|
2134
|
+
} finally {
|
|
2135
|
+
unlink();
|
|
2136
|
+
if (!local.signal.aborted) local.abort();
|
|
2137
|
+
}
|
|
2138
|
+
})();
|
|
2139
|
+
Object.defineProperty(gen, "result", {
|
|
2140
|
+
enumerable: true,
|
|
2141
|
+
get() {
|
|
2142
|
+
return inner?.result;
|
|
2143
|
+
},
|
|
2144
|
+
});
|
|
2145
|
+
return gen;
|
|
2146
|
+
}
|
|
2147
|
+
return gated(
|
|
2148
|
+
"ask",
|
|
2149
|
+
name,
|
|
2150
|
+
async () => {
|
|
2151
|
+
if (options.aiRuntime) {
|
|
2152
|
+
return options.aiRuntime.runAgent(name, {
|
|
2153
|
+
...turn,
|
|
2154
|
+
gates: (options.rlsGateNames ?? []).filter((gate) => gate !== "public"),
|
|
1906
2155
|
userId: auth.userId,
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2156
|
+
operatorId: operator.id,
|
|
2157
|
+
...(options.journal ? { journal: options.journal } : {}),
|
|
2158
|
+
...(options.flow !== undefined ? { flow: options.flow } : {}),
|
|
2159
|
+
tenantId: tenant.id,
|
|
2160
|
+
auth: {
|
|
2161
|
+
userId: auth.userId,
|
|
2162
|
+
scopes: auth.scopes,
|
|
2163
|
+
verified: auth.verified,
|
|
2164
|
+
},
|
|
2165
|
+
callTool: (tool, toolInput) => fx.call(tool, toolInput),
|
|
2166
|
+
recordCall: (tool, runId) => {
|
|
2167
|
+
void gated(
|
|
2168
|
+
"call",
|
|
2169
|
+
tool,
|
|
2170
|
+
async () => runId,
|
|
2171
|
+
undefined,
|
|
2172
|
+
(id) =>
|
|
2173
|
+
typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined,
|
|
2174
|
+
);
|
|
2175
|
+
},
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
return { ok: true, steps: 0, denials: [], output: input };
|
|
2179
|
+
},
|
|
2180
|
+
undefined,
|
|
2181
|
+
(result) => {
|
|
2182
|
+
if (!result || typeof result !== "object" || !("runId" in result)) return undefined;
|
|
2183
|
+
const id = (result as { runId?: unknown }).runId;
|
|
2184
|
+
return typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined;
|
|
2185
|
+
},
|
|
2186
|
+
);
|
|
2187
|
+
} as Fx["run"],
|
|
1916
2188
|
stream(model, opts) {
|
|
1917
2189
|
const name = resolveName(model);
|
|
1918
2190
|
const chunks = (async function* () {
|
|
@@ -1957,6 +2229,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1957
2229
|
});
|
|
1958
2230
|
},
|
|
1959
2231
|
locale,
|
|
2232
|
+
lastEventId: options.lastEventId,
|
|
1960
2233
|
id() {
|
|
1961
2234
|
return okid();
|
|
1962
2235
|
},
|
|
@@ -1983,12 +2256,22 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1983
2256
|
with: jsonWith,
|
|
1984
2257
|
withQuery: jsonWithQuery,
|
|
1985
2258
|
stream(chunks) {
|
|
1986
|
-
|
|
2259
|
+
const carrier = {
|
|
1987
2260
|
[jsonResultBrand]: true,
|
|
1988
2261
|
kind: "stream" as const,
|
|
1989
2262
|
status: 200 as const,
|
|
1990
2263
|
chunks,
|
|
1991
2264
|
};
|
|
2265
|
+
Object.defineProperty(carrier, "result", {
|
|
2266
|
+
enumerable: true,
|
|
2267
|
+
get() {
|
|
2268
|
+
if (chunks && typeof chunks === "object" && "result" in chunks) {
|
|
2269
|
+
return (chunks as { readonly result?: Promise<unknown> }).result;
|
|
2270
|
+
}
|
|
2271
|
+
return undefined;
|
|
2272
|
+
},
|
|
2273
|
+
});
|
|
2274
|
+
return carrier as JsonStreamResult;
|
|
1992
2275
|
},
|
|
1993
2276
|
},
|
|
1994
2277
|
async step<T>(name: string, fn: () => T | Promise<T>, opts?: StepOptions<T>): Promise<T> {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP wire-frame capture redacts credentials and keeps the rest.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
HTTP_FRAME_REDACTED,
|
|
8
|
+
captureHttpFrame,
|
|
9
|
+
redactHttpFrame,
|
|
10
|
+
requestForHttpInvoke,
|
|
11
|
+
} from "./http-frame.ts";
|
|
12
|
+
|
|
13
|
+
describe("requestForHttpInvoke", () => {
|
|
14
|
+
test("GET puts scalar input on the query string and keeps accept", () => {
|
|
15
|
+
const request = requestForHttpInvoke(
|
|
16
|
+
{ method: "GET", path: "/tasks/:id" },
|
|
17
|
+
{ pathParams: { id: "tsk_1" }, input: { id: "tsk_1", limit: 20 } },
|
|
18
|
+
);
|
|
19
|
+
expect(request.method).toBe("GET");
|
|
20
|
+
expect(new URL(request.url).pathname).toBe("/tasks/tsk_1");
|
|
21
|
+
expect(new URL(request.url).searchParams.get("limit")).toBe("20");
|
|
22
|
+
expect(new URL(request.url).searchParams.has("id")).toBe(false);
|
|
23
|
+
expect(request.headers.get("accept")).toBe("application/json");
|
|
24
|
+
expect(request.headers.get("x-oke-client")).toBe("console");
|
|
25
|
+
expect(request.headers.get("content-type")).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("captureHttpFrame", () => {
|
|
30
|
+
test("keeps method, path, query, and headers and redacts credentials", () => {
|
|
31
|
+
const request = new Request("http://localhost/tasks?limit=20&token=sekrit", {
|
|
32
|
+
method: "GET",
|
|
33
|
+
headers: {
|
|
34
|
+
accept: "application/json",
|
|
35
|
+
authorization: "Bearer super-secret",
|
|
36
|
+
cookie: "oke_session=abc",
|
|
37
|
+
"x-request-id": "req_1",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const response = new Response(JSON.stringify({ ok: true }), {
|
|
41
|
+
status: 200,
|
|
42
|
+
headers: {
|
|
43
|
+
"content-type": "application/json",
|
|
44
|
+
"set-cookie": "oke_session=next; HttpOnly",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(captureHttpFrame(request, response)).toEqual({
|
|
49
|
+
request: {
|
|
50
|
+
method: "GET",
|
|
51
|
+
path: "/tasks",
|
|
52
|
+
query: { limit: "20", token: HTTP_FRAME_REDACTED },
|
|
53
|
+
headers: {
|
|
54
|
+
accept: "application/json",
|
|
55
|
+
authorization: HTTP_FRAME_REDACTED,
|
|
56
|
+
cookie: HTTP_FRAME_REDACTED,
|
|
57
|
+
"x-request-id": "req_1",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
response: {
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: {
|
|
63
|
+
"content-type": "application/json",
|
|
64
|
+
"set-cookie": HTTP_FRAME_REDACTED,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("redactHttpFrame masks a frame that skipped capture", () => {
|
|
71
|
+
expect(
|
|
72
|
+
redactHttpFrame({
|
|
73
|
+
request: {
|
|
74
|
+
method: "POST",
|
|
75
|
+
path: "/tasks",
|
|
76
|
+
query: {},
|
|
77
|
+
headers: { authorization: "Bearer leak", host: "localhost" },
|
|
78
|
+
},
|
|
79
|
+
response: { status: 201, headers: { "x-api-key": "leak" } },
|
|
80
|
+
}),
|
|
81
|
+
).toEqual({
|
|
82
|
+
request: {
|
|
83
|
+
method: "POST",
|
|
84
|
+
path: "/tasks",
|
|
85
|
+
query: {},
|
|
86
|
+
headers: { authorization: HTTP_FRAME_REDACTED, host: "localhost" },
|
|
87
|
+
},
|
|
88
|
+
response: {
|
|
89
|
+
status: 201,
|
|
90
|
+
headers: { "x-api-key": HTTP_FRAME_REDACTED },
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|