@tangle-network/agent-app 0.60.192 → 0.60.193
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/dist/app-auth/index.js +3 -3
- package/dist/assistant/index.js +3 -3
- package/dist/chat-routes/index.js +7 -4
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-routes/model-failover-stream.d.ts +8 -0
- package/dist/chat-routes/sandbox-producer.d.ts +12 -0
- package/dist/chat-routes/turn-routes.d.ts +14 -0
- package/dist/{chunk-QAF7ZTO3.js → chunk-L5JLOC2F.js} +4 -4
- package/dist/chunk-XELWWXJJ.js +101 -0
- package/dist/chunk-XELWWXJJ.js.map +1 -0
- package/dist/{chunk-Q2H5FKVG.js → chunk-XP77DJEF.js} +16 -1
- package/dist/chunk-XP77DJEF.js.map +1 -0
- package/dist/{chunk-63EKFZOE.js → chunk-ZLX4BA27.js} +15 -1
- package/dist/chunk-ZLX4BA27.js.map +1 -0
- package/dist/model-resolution/failover.d.ts +16 -2
- package/dist/model-resolution/index.d.ts +42 -0
- package/dist/model-resolution/index.js +30 -5
- package/dist/model-resolution/index.js.map +1 -1
- package/dist/policy-filters/index.d.ts +166 -0
- package/dist/policy-filters/index.js +23 -0
- package/dist/policy-filters/index.js.map +1 -0
- package/dist/public-consultation/index.d.ts +1 -1
- package/dist/public-consultation/index.js +2 -1
- package/dist/public-consultation/index.js.map +1 -1
- package/dist/sandbox/index.js +2 -2
- package/dist/settings-react/index.js +1 -1
- package/dist/tools/index.js +2 -2
- package/dist/web-react/index.js +7 -7
- package/package.json +6 -1
- package/dist/chunk-63EKFZOE.js.map +0 -1
- package/dist/chunk-Q2H5FKVG.js.map +0 -1
- /package/dist/{chunk-QAF7ZTO3.js.map → chunk-L5JLOC2F.js.map} +0 -0
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
* byte-identical to no failover. A silent chain is now deliberately bounded.
|
|
51
51
|
*/
|
|
52
52
|
import { ModelFailoverExhaustedError, type ModelFailoverAttempt } from '../model-resolution/failover';
|
|
53
|
+
import type { ModelPolicy } from '../policy-filters/index';
|
|
53
54
|
/**
|
|
54
55
|
* True when `event` puts content in front of the user (or in the persisted
|
|
55
56
|
* transcript), making a restart on another model unsafe.
|
|
@@ -118,6 +119,13 @@ export interface ModelFailoverStreamOptions {
|
|
|
118
119
|
/** Preferred model first, then fallbacks in descending preference. */
|
|
119
120
|
models: readonly string[];
|
|
120
121
|
open: OpenModelStream;
|
|
122
|
+
/**
|
|
123
|
+
* Product admission policy applied to EVERY chain member before its stream
|
|
124
|
+
* is opened — see `runWithModelFailover`'s `policy`. A member the policy
|
|
125
|
+
* refuses is skipped (never opened, never sent the turn's data) and the walk
|
|
126
|
+
* continues; an all-refused chain ends in `ModelFailoverExhaustedError`.
|
|
127
|
+
*/
|
|
128
|
+
policy?: ModelPolicy;
|
|
121
129
|
/**
|
|
122
130
|
* Maximum time to open/start one model's event source, including its first
|
|
123
131
|
* iterator pull. Default 120 seconds, matching the sandbox provisioning
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { type InteractionRequestWire } from '../interactions/contract';
|
|
17
17
|
import { type JsonRecord } from '../stream/index';
|
|
18
|
+
import type { ModelPolicy } from '../policy-filters/index';
|
|
18
19
|
import { type EmptyTurnRetryInfo, type ModelFallbackInfo, type OpenModelStream } from './model-failover-stream';
|
|
19
20
|
import type { ChatTurnRouteProducer } from './turn-routes';
|
|
20
21
|
/** Outcome of a `promoteFilePart` attempt. `key`, when given, becomes the
|
|
@@ -80,6 +81,17 @@ export interface SandboxChatProducerOptions {
|
|
|
80
81
|
/** Opt out of failover while still using {@link openEvents}. `false` collapses
|
|
81
82
|
* the chain to `model` alone. */
|
|
82
83
|
modelFailover?: false;
|
|
84
|
+
/**
|
|
85
|
+
* Product admission policy applied to every chain member — the RESOLVED
|
|
86
|
+
* preferred model AND every fallback — before its stream is opened. This is
|
|
87
|
+
* the execution-time half of policy enforcement: it covers the model
|
|
88
|
+
* whatever its source (request body, env, product default), and it is what
|
|
89
|
+
* stops a compliant model's mid-turn 502 from re-running the turn's data
|
|
90
|
+
* through a non-compliant fallback. A refused member is skipped with the
|
|
91
|
+
* refusal recorded in `modelFailover().attempts`; an all-refused chain
|
|
92
|
+
* exhausts. Omit → availability only (today's behavior).
|
|
93
|
+
*/
|
|
94
|
+
modelPolicy?: ModelPolicy;
|
|
83
95
|
/** Maximum time to open/start one model event source through its first event.
|
|
84
96
|
* Default 120 seconds. This is separate from provider first-output latency
|
|
85
97
|
* so cold sandbox startup is not mislabeled as an inference failure. */
|
|
@@ -54,6 +54,7 @@ import { type DraftPersistenceTuning } from './draft-persistence';
|
|
|
54
54
|
import { type InteractionAnswerRoute, type InteractionAnswerRouteOptions } from '../interactions/route';
|
|
55
55
|
import { type PersistedChatMessageForTurn, type TurnEventStore } from '../stream/index';
|
|
56
56
|
import type { ModelFailoverAttempt } from '../model-resolution/failover';
|
|
57
|
+
import { type ModelPolicy } from '../policy-filters/index';
|
|
57
58
|
import { type TurnRoutePhase } from './turn-progress';
|
|
58
59
|
import { type ChatTurnPartInput, type ChatTurnRequestPayload } from './wire';
|
|
59
60
|
/** Usage receipt persisted onto the assistant message (the flattened
|
|
@@ -471,6 +472,19 @@ export interface CreateChatTurnRoutesOptions<TContext = void> {
|
|
|
471
472
|
* a plain response without the progress header, a `turn.response` event
|
|
472
473
|
* with it. Omit → nothing to prepare. */
|
|
473
474
|
prepareTurn?(args: ChatTurnPrepareArgs<TContext>): ChatTurnPrepareResult | Promise<ChatTurnPrepareResult>;
|
|
475
|
+
/** Product admission policy over the model a turn NAMES. When the request
|
|
476
|
+
* body carries a `model`, it is checked here — after `authorize`, before
|
|
477
|
+
* the conversation loads, the lock is taken, or any row is written — and a
|
|
478
|
+
* refusal answers 403 with the policy's reason. Fail-closed: a named model
|
|
479
|
+
* carrying no policy metadata is refused.
|
|
480
|
+
*
|
|
481
|
+
* This is half the enforcement. A body without `model` resolves to a
|
|
482
|
+
* product default inside `produce`, which this seam never sees — enforce
|
|
483
|
+
* that half at execution with `createSandboxChatProducer`'s `modelPolicy`
|
|
484
|
+
* (or `runWithModelFailover`'s `policy` on a custom producer), which also
|
|
485
|
+
* covers every fallback the turn could fail over to. Wire the SAME policy
|
|
486
|
+
* object to both. Omit → no policy check (today's behavior). */
|
|
487
|
+
modelPolicy?: ModelPolicy;
|
|
474
488
|
/** Thread/message persistence (`/chat-store`'s store or a product adapter). */
|
|
475
489
|
store: ChatTurnMessageStore;
|
|
476
490
|
/** Turn-event buffer (`createD1TurnEventStore(env.DB)` or `/turn-stream`'s
|
|
@@ -2,15 +2,15 @@ import {
|
|
|
2
2
|
authenticateToolRequest,
|
|
3
3
|
createMcpToolHandler
|
|
4
4
|
} from "./chunk-6A7MYOUI.js";
|
|
5
|
+
import {
|
|
6
|
+
assertRenderUiSchema
|
|
7
|
+
} from "./chunk-2RCPPOQ4.js";
|
|
5
8
|
import {
|
|
6
9
|
base64UrlDecodeText,
|
|
7
10
|
base64UrlEncodeText,
|
|
8
11
|
constantTimeEqual,
|
|
9
12
|
hmacSha256Base64Url
|
|
10
13
|
} from "./chunk-S5SRJJQG.js";
|
|
11
|
-
import {
|
|
12
|
-
assertRenderUiSchema
|
|
13
|
-
} from "./chunk-2RCPPOQ4.js";
|
|
14
14
|
import {
|
|
15
15
|
buildAppToolOpenAITools,
|
|
16
16
|
findCustomTool,
|
|
@@ -270,4 +270,4 @@ export {
|
|
|
270
270
|
createAppToolRuntimeExecutor,
|
|
271
271
|
handleAppToolRequest
|
|
272
272
|
};
|
|
273
|
-
//# sourceMappingURL=chunk-
|
|
273
|
+
//# sourceMappingURL=chunk-L5JLOC2F.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/policy-filters/index.ts
|
|
2
|
+
function allOf(...filters) {
|
|
3
|
+
if (filters.length === 0) {
|
|
4
|
+
throw new Error("allOf() requires at least one filter \u2014 an empty allOf admits everything; use allowAll() to say so explicitly");
|
|
5
|
+
}
|
|
6
|
+
return async (item, context) => {
|
|
7
|
+
for (const filter of filters) {
|
|
8
|
+
if (!await filter(item, context)) return false;
|
|
9
|
+
}
|
|
10
|
+
return true;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function anyOf(...filters) {
|
|
14
|
+
return async (item, context) => {
|
|
15
|
+
for (const filter of filters) {
|
|
16
|
+
if (await filter(item, context)) return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function allowAll() {
|
|
22
|
+
return () => true;
|
|
23
|
+
}
|
|
24
|
+
var PolicyDeniedError = class extends Error {
|
|
25
|
+
code;
|
|
26
|
+
kind;
|
|
27
|
+
id;
|
|
28
|
+
constructor(code, kind, id, reason) {
|
|
29
|
+
super(reason);
|
|
30
|
+
this.name = "PolicyDeniedError";
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.kind = kind;
|
|
33
|
+
this.id = id;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
async function lookupPolicyInfo(catalog, id) {
|
|
37
|
+
if (typeof catalog === "function") return catalog(id);
|
|
38
|
+
const maybeMap = catalog;
|
|
39
|
+
if (typeof maybeMap.get === "function") return maybeMap.get(id);
|
|
40
|
+
for (const entry of catalog) {
|
|
41
|
+
if (entry.id === id) return entry;
|
|
42
|
+
}
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
function refusal(kind, id, code, detail) {
|
|
46
|
+
return { allowed: false, code, reason: `${kind} "${id}" is not admitted by policy: ${detail}` };
|
|
47
|
+
}
|
|
48
|
+
async function checkPolicy(input) {
|
|
49
|
+
const context = input.context ?? {};
|
|
50
|
+
let info;
|
|
51
|
+
try {
|
|
52
|
+
info = await lookupPolicyInfo(input.catalog, input.id);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
55
|
+
return refusal(input.kind, input.id, "policy-filter-error", `the policy catalog could not be read (${message})`);
|
|
56
|
+
}
|
|
57
|
+
if (!info) return refusal(input.kind, input.id, "policy-metadata-missing", "no policy metadata classifies it");
|
|
58
|
+
let admitted;
|
|
59
|
+
try {
|
|
60
|
+
admitted = await input.filter(info, context);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
63
|
+
return refusal(input.kind, input.id, "policy-filter-error", `the policy filter errored (${message})`);
|
|
64
|
+
}
|
|
65
|
+
return admitted ? { allowed: true, info } : refusal(input.kind, input.id, "policy-denied", "the policy filter rejected it");
|
|
66
|
+
}
|
|
67
|
+
async function assertPolicy(input) {
|
|
68
|
+
const result = await checkPolicy(input);
|
|
69
|
+
if (!result.allowed) throw new PolicyDeniedError(result.code, input.kind, input.id, result.reason);
|
|
70
|
+
return result.info;
|
|
71
|
+
}
|
|
72
|
+
function checkModelPolicy(id, catalog, filter, context) {
|
|
73
|
+
return checkPolicy({ kind: "model", id, catalog, filter, context });
|
|
74
|
+
}
|
|
75
|
+
async function filterItemsByPolicy(items, catalog, filter, context) {
|
|
76
|
+
const indexed = typeof catalog === "function" || typeof catalog.get === "function" ? catalog : indexPolicyCatalog(catalog);
|
|
77
|
+
const admitted = [];
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
const result = await checkPolicy({ kind: "catalog item", id: item.id, catalog: indexed, filter, context });
|
|
80
|
+
if (result.allowed) admitted.push(item);
|
|
81
|
+
}
|
|
82
|
+
return admitted;
|
|
83
|
+
}
|
|
84
|
+
function indexPolicyCatalog(catalog) {
|
|
85
|
+
const index = /* @__PURE__ */ new Map();
|
|
86
|
+
for (const entry of catalog) index.set(entry.id, entry);
|
|
87
|
+
return index;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
allOf,
|
|
92
|
+
anyOf,
|
|
93
|
+
allowAll,
|
|
94
|
+
PolicyDeniedError,
|
|
95
|
+
checkPolicy,
|
|
96
|
+
assertPolicy,
|
|
97
|
+
checkModelPolicy,
|
|
98
|
+
filterItemsByPolicy,
|
|
99
|
+
indexPolicyCatalog
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=chunk-XELWWXJJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/policy-filters/index.ts"],"sourcesContent":["/**\n * Policy filters — product-supplied admission policy over the models a\n * deployment will run.\n *\n * The engine owns the MECHANISM (a typed predicate, combinators, a fail-closed\n * enforcement seam, a catalog filter for pickers); the product owns the POLICY\n * (the metadata and the filter), supplied as data. Nothing here names a vendor,\n * a regulation, or a product — a HIPAA deployment's \"zero-data-retention,\n * BAA-eligible models only\" rule is one `PolicyFilter` over product-supplied\n * `ModelPolicyInfo`, and any other restriction composes the same way.\n *\n * Fail-closed contract, everywhere this module decides:\n *\n * - An item with NO policy metadata is refused (enforcement) or hidden\n * (catalog filter). A policy that cannot classify an item does not admit it.\n * - A filter that THROWS is a refusal, never an admission — a broken policy\n * check fails the turn instead of waving it through.\n * - Enforcement composes with availability, never replaces it: the\n * `model-resolution` seam applies the policy AFTER its existing\n * allowlist/env/catalog admission, on every path — a policy-filtered\n * deployment cannot be bypassed through an operator-trusted lane.\n *\n * Browser-safe and dependency-free by construction: no imports at all, so UI\n * pickers (docs/ui-picker-canon.md) can filter the catalog they pass to\n * `ModelPicker` with the same filter the server enforces.\n */\n\n/** Arbitrary context a product threads to its filters (workspace, user, turn). */\nexport type PolicyFilterContext = Record<string, unknown>\n\n/**\n * A typed admission predicate over one catalog item. May be async (a product\n * can read its metadata from storage). Returning `false` denies; THROWING\n * denies too — enforcement treats a filter error as a refusal.\n */\nexport type PolicyFilter<T, C extends object = PolicyFilterContext> = (\n item: T,\n context: C,\n) => boolean | Promise<boolean>\n\n/**\n * Admit only items every filter admits.\n *\n * Throws at COMPOSITION time on an empty `allOf()`: an empty conjunction\n * admits everything, which reads as a policy that was never wired — the same\n * silent-admission class as a missing filter. Say \"no restriction\" out loud\n * with {@link allowAll} instead.\n */\nexport function allOf<T, C extends object = PolicyFilterContext>(\n ...filters: Array<PolicyFilter<T, C>>\n): PolicyFilter<T, C> {\n if (filters.length === 0) {\n throw new Error('allOf() requires at least one filter — an empty allOf admits everything; use allowAll() to say so explicitly')\n }\n return async (item, context) => {\n for (const filter of filters) {\n if (!(await filter(item, context))) return false\n }\n return true\n }\n}\n\n/** Admit items at least one filter admits. An empty `anyOf` admits nothing —\n * already fail-closed, so it is allowed. */\nexport function anyOf<T, C extends object = PolicyFilterContext>(\n ...filters: Array<PolicyFilter<T, C>>\n): PolicyFilter<T, C> {\n return async (item, context) => {\n for (const filter of filters) {\n if (await filter(item, context)) return true\n }\n return false\n }\n}\n\n/**\n * The explicit \"no restriction\" filter — what an empty `allOf()` used to mean\n * silently. Admits every item; the catalog still must classify the id (a\n * policy of `allowAll` over an empty catalog admits nothing).\n */\nexport function allowAll<T, C extends object = PolicyFilterContext>(): PolicyFilter<T, C> {\n return () => true\n}\n\n/**\n * Policy metadata for one model — DATA the product supplies (from its router\n * catalog, a compliance review, a vendor attestation), never engine policy.\n * The named fields are the common compliance dimensions; the index signature\n * carries any product-specific attribute a filter reads.\n */\nexport interface ModelPolicyInfo {\n id: string\n provider?: string\n /** The provider retains no prompt/response data for this model. */\n zeroDataRetention?: boolean\n /** The vendor will sign a BAA covering this model. */\n hipaaEligible?: boolean\n [attribute: string]: unknown\n}\n\n/**\n * The policy a model-enforcement seam (`validateChatModelId`,\n * `runWithModelFailover`, `createChatTurnRoutes`, `createSandboxChatProducer`)\n * takes: which metadata to classify by, and which filter to apply. One shape,\n * threaded unchanged through every seam so a product states its admission\n * policy exactly once.\n */\nexport interface ModelPolicy<C extends object = PolicyFilterContext> {\n filter: PolicyFilter<ModelPolicyInfo, C>\n catalog: PolicyCatalog<ModelPolicyInfo>\n /** Threaded to the filter unchanged. */\n context?: C\n}\n\n/**\n * Where enforcement reads an item's metadata: an in-memory iterable, a\n * `ReadonlyMap` keyed by id, or a lookup (sync or async) keyed by the item's\n * id. Returning `undefined` means UNCLASSIFIED — enforcement refuses the item.\n *\n * Key the catalog by the EXACT id string the turn carries (see\n * docs/policy-filters.md § \"Ids are exact strings\"). Lookups never normalize.\n */\nexport type PolicyCatalog<T extends { id: string }> =\n | Iterable<T>\n | ReadonlyMap<string, T>\n | ((id: string) => T | undefined | Promise<T | undefined>)\n\n/** Why a policy check refused, machine-readable on the result and on {@link PolicyDeniedError}. */\nexport type PolicyDeniedCode = 'policy-metadata-missing' | 'policy-denied' | 'policy-filter-error'\n\n/** The outcome of one policy check. Admission carries the item's metadata; a refusal carries a code and a reason. */\nexport type PolicyCheckResult<T = unknown> =\n | { allowed: true; info: T }\n | { allowed: false; code: PolicyDeniedCode; reason: string }\n\n/** The error {@link assertPolicy} throws on any refusal. */\nexport class PolicyDeniedError extends Error {\n readonly code: PolicyDeniedCode\n readonly kind: string\n readonly id: string\n\n constructor(code: PolicyDeniedCode, kind: string, id: string, reason: string) {\n super(reason)\n this.name = 'PolicyDeniedError'\n this.code = code\n this.kind = kind\n this.id = id\n }\n}\n\n/** Input to one policy check: what is being admitted, from which catalog, under which filter. */\nexport interface PolicyCheckInput<T extends { id: string }, C extends object = PolicyFilterContext> {\n /** What is being admitted ('model' | 'harness' | 'profile' | …) — names the refusal. */\n kind: string\n /** The id being admitted. */\n id: string\n /** The product's policy metadata. */\n catalog: PolicyCatalog<T>\n /** The product's admission filter. */\n filter: PolicyFilter<T, C>\n /** Threaded to the filter unchanged. */\n context?: C\n}\n\nasync function lookupPolicyInfo<T extends { id: string }>(\n catalog: PolicyCatalog<T>,\n id: string,\n): Promise<T | undefined> {\n if (typeof catalog === 'function') return catalog(id)\n // A ReadonlyMap answers by key — checked by its `get`, not `instanceof Map`,\n // so a frozen or cross-realm map still counts.\n const maybeMap = catalog as ReadonlyMap<string, T>\n if (typeof maybeMap.get === 'function') return maybeMap.get(id)\n for (const entry of catalog as Iterable<T>) {\n if (entry.id === id) return entry\n }\n return undefined\n}\n\nfunction refusal<T>(kind: string, id: string, code: PolicyDeniedCode, detail: string): PolicyCheckResult<T> {\n return { allowed: false, code, reason: `${kind} \"${id}\" is not admitted by policy: ${detail}` }\n}\n\n/**\n * Fail-closed policy check. Admits only when the catalog classifies the id AND\n * the filter accepts it. Unclassified ids and filter errors refuse.\n */\nexport async function checkPolicy<T extends { id: string }, C extends object = PolicyFilterContext>(\n input: PolicyCheckInput<T, C>,\n): Promise<PolicyCheckResult<T>> {\n const context = input.context ?? ({} as C)\n let info: T | undefined\n try {\n info = await lookupPolicyInfo(input.catalog, input.id)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return refusal(input.kind, input.id, 'policy-filter-error', `the policy catalog could not be read (${message})`)\n }\n if (!info) return refusal(input.kind, input.id, 'policy-metadata-missing', 'no policy metadata classifies it')\n let admitted: boolean\n try {\n admitted = await input.filter(info, context)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return refusal(input.kind, input.id, 'policy-filter-error', `the policy filter errored (${message})`)\n }\n return admitted\n ? { allowed: true, info }\n : refusal(input.kind, input.id, 'policy-denied', 'the policy filter rejected it')\n}\n\n/**\n * The throwing form of {@link checkPolicy} for enforcement seams that fail by\n * exception. Resolves with the item's metadata on admission; throws\n * {@link PolicyDeniedError} on any refusal.\n */\nexport async function assertPolicy<T extends { id: string }, C extends object = PolicyFilterContext>(\n input: PolicyCheckInput<T, C>,\n): Promise<T> {\n const result = await checkPolicy(input)\n if (!result.allowed) throw new PolicyDeniedError(result.code, input.kind, input.id, result.reason)\n return result.info\n}\n\n/** Policy check over a model id. See {@link checkPolicy}. */\nexport function checkModelPolicy<C extends object = PolicyFilterContext>(\n id: string,\n catalog: PolicyCatalog<ModelPolicyInfo>,\n filter: PolicyFilter<ModelPolicyInfo, C>,\n context?: C,\n): Promise<PolicyCheckResult<ModelPolicyInfo>> {\n return checkPolicy({ kind: 'model', id, catalog, filter, context })\n}\n\n/**\n * Browser-safe catalog filter for pickers: keep only items the policy admits.\n * Items the catalog does not classify are dropped — a picker offers exactly\n * what a turn would run, and no more. Per docs/ui-picker-canon.md the picker\n * itself still reconciles an absent SAVED value under its own name; this\n * filter shapes the offered list, never the saved value.\n *\n * An iterable catalog is indexed ONCE into a Map up front: per-item rescans\n * are O(items × catalog) and silently return nothing on a one-shot iterable.\n */\nexport async function filterItemsByPolicy<\n Item extends { id: string },\n Info extends { id: string },\n C extends object = PolicyFilterContext,\n>(\n items: readonly Item[],\n catalog: PolicyCatalog<Info>,\n filter: PolicyFilter<Info, C>,\n context?: C,\n): Promise<Item[]> {\n const indexed: PolicyCatalog<Info> =\n typeof catalog === 'function' || typeof (catalog as ReadonlyMap<string, Info>).get === 'function'\n ? catalog\n : indexPolicyCatalog(catalog as Iterable<Info>)\n const admitted: Item[] = []\n for (const item of items) {\n const result = await checkPolicy({ kind: 'catalog item', id: item.id, catalog: indexed, filter, context })\n if (result.allowed) admitted.push(item)\n }\n return admitted\n}\n\n/**\n * Index an iterable catalog into a `Map` keyed by exact id — the form to hold\n * when one catalog answers many lookups (a picker filter, a failover chain, a\n * busy route). Ids must be unique: a Map keeps the LAST entry for a repeated\n * id while a linear scan of the iterable finds the FIRST, and a catalog with\n * duplicate ids is a product bug either way.\n */\nexport function indexPolicyCatalog<T extends { id: string }>(catalog: Iterable<T>): Map<string, T> {\n const index = new Map<string, T>()\n for (const entry of catalog) index.set(entry.id, entry)\n return index\n}\n"],"mappings":"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","names":[]}
|
|
@@ -31,6 +31,9 @@ import {
|
|
|
31
31
|
parseFileMentions,
|
|
32
32
|
turnResponseEventFor
|
|
33
33
|
} from "./chunk-YHRWZZJN.js";
|
|
34
|
+
import {
|
|
35
|
+
checkModelPolicy
|
|
36
|
+
} from "./chunk-XELWWXJJ.js";
|
|
34
37
|
|
|
35
38
|
// src/chat-routes/draft-persistence.ts
|
|
36
39
|
var CONTENT_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -392,6 +395,18 @@ function createChatTurnRoutes(options) {
|
|
|
392
395
|
}
|
|
393
396
|
const auth = await options.authorize({ request, intent: "turn", body: parsed.payload });
|
|
394
397
|
if (!auth.ok) return { ok: false, response: auth.response };
|
|
398
|
+
const namedModel = typeof parsed.payload.model === "string" ? parsed.payload.model.trim() : "";
|
|
399
|
+
if (options.modelPolicy && namedModel) {
|
|
400
|
+
const decision = await checkModelPolicy(
|
|
401
|
+
namedModel,
|
|
402
|
+
options.modelPolicy.catalog,
|
|
403
|
+
options.modelPolicy.filter,
|
|
404
|
+
options.modelPolicy.context
|
|
405
|
+
);
|
|
406
|
+
if (!decision.allowed) {
|
|
407
|
+
return { ok: false, response: Response.json({ code: "model_policy_denied", error: decision.reason }, { status: 403 }) };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
395
410
|
return { ok: true, parsed, auth };
|
|
396
411
|
}
|
|
397
412
|
function progressFirstTurn(request, ctx, admitted) {
|
|
@@ -1270,4 +1285,4 @@ export {
|
|
|
1270
1285
|
createChatTurnRoutes,
|
|
1271
1286
|
streamChatRouteAsSandboxEvents
|
|
1272
1287
|
};
|
|
1273
|
-
//# sourceMappingURL=chunk-
|
|
1288
|
+
//# sourceMappingURL=chunk-XP77DJEF.js.map
|