@vellumai/assistant 0.11.10 → 0.11.11-staging.1
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 +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/openapi.yaml +152 -55
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +60 -0
- package/src/__tests__/config-get-vision-flag.test.ts +36 -0
- package/src/__tests__/conversation-error.test.ts +38 -0
- package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
- package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
- package/src/__tests__/headless-browser-mode.test.ts +63 -13
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +118 -27
- package/src/__tests__/host-app-control-proxy.test.ts +2 -2
- package/src/__tests__/host-app-control-routes.test.ts +41 -0
- package/src/__tests__/host-bash-routes.test.ts +30 -1
- package/src/__tests__/host-browser-routes.test.ts +34 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-proxy-base.test.ts +1 -0
- package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
- package/src/__tests__/llm-schema.test.ts +32 -0
- package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
- package/src/__tests__/memory-config-file-parity.test.ts +18 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
- package/src/__tests__/messaging-send-tool.test.ts +159 -13
- package/src/__tests__/messaging-shared.test.ts +19 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
- package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
- package/src/__tests__/schedule-routes.test.ts +30 -1
- package/src/__tests__/scheduler-result-notification.test.ts +209 -0
- package/src/__tests__/skills-install-extract.test.ts +26 -0
- package/src/__tests__/skills-install-staging.test.ts +11 -2
- package/src/api/constants/personality-sliders.test.ts +21 -0
- package/src/api/constants/personality-sliders.ts +64 -0
- package/src/api/index.ts +8 -0
- package/src/archive/ustar-size.test.ts +32 -0
- package/src/archive/ustar-size.ts +23 -0
- package/src/background-wake/next-wake.test.ts +13 -0
- package/src/background-wake/next-wake.ts +4 -2
- package/src/calls/voice-session-bridge.ts +26 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
- package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
- package/src/cli/commands/config.help.ts +3 -1
- package/src/cli/commands/memory/worker.ts +1 -1
- package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
- package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
- package/src/cli/commands/platform/credits-format.ts +57 -0
- package/src/cli/commands/platform/index.help.ts +19 -3
- package/src/cli/commands/plugins.help.ts +4 -1
- package/src/cli/commands/plugins.ts +2 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-platform.ts +8 -10
- package/src/config/bundled-skills/messaging/SKILL.md +2 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +9 -1
- package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/input-modalities.test.ts +71 -0
- package/src/config/input-modalities.ts +95 -0
- package/src/config/schemas/heartbeat.ts +3 -3
- package/src/config/schemas/llm.ts +13 -3
- package/src/config/schemas/memory-storage.test.ts +39 -0
- package/src/config/schemas/memory-storage.ts +21 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
- package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
- package/src/daemon/conversation-error.ts +20 -7
- package/src/daemon/embedding-reconcile.ts +64 -1
- package/src/daemon/handlers/config-embeddings.ts +31 -0
- package/src/daemon/host-app-control-proxy.ts +2 -0
- package/src/daemon/host-bash-proxy.ts +1 -0
- package/src/daemon/host-cu-proxy.ts +6 -6
- package/src/daemon/host-file-proxy.ts +6 -6
- package/src/daemon/host-proxy-base.ts +10 -9
- package/src/daemon/host-transfer-proxy.ts +21 -24
- package/src/daemon/mcp-reload-service.ts +6 -0
- package/src/daemon/providers-setup.ts +5 -26
- package/src/heartbeat/heartbeat-service.ts +72 -13
- package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
- package/src/live-voice/live-voice-photo.ts +68 -4
- package/src/live-voice/live-voice-session.ts +36 -2
- package/src/live-voice/protocol.ts +98 -1
- package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
- package/src/mcp/__tests__/reload-signal.test.ts +57 -0
- package/src/mcp/__tests__/startup.test.ts +150 -0
- package/src/mcp/reload-signal.ts +50 -0
- package/src/mcp/startup.ts +101 -0
- package/src/messaging/providers/outlook/adapter.ts +2 -13
- package/src/messaging/providers/outlook/client.ts +13 -1
- package/src/messaging/providers/outlook/types.ts +3 -0
- package/src/notifications/__tests__/decision-engine.test.ts +79 -6
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
- package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
- package/src/notifications/copy-composer.ts +33 -11
- package/src/notifications/decision-engine.ts +48 -11
- package/src/notifications/events-store.ts +68 -2
- package/src/notifications/home-feed-side-effect.ts +15 -3
- package/src/notifications/schedule-result-producer.ts +275 -0
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/seed-providers.ts +3 -2
- package/src/persistence/conversation-crud.ts +38 -0
- package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
- package/src/persistence/embeddings/embedding-backend.ts +148 -11
- package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
- package/src/persistence/embeddings/embedding-openai.ts +55 -4
- package/src/persistence/embeddings/embedding-types.test.ts +24 -0
- package/src/persistence/embeddings/embedding-types.ts +25 -1
- package/src/persistence/job-utils.ts +6 -2
- package/src/plugin-api/index.ts +2 -1
- package/src/plugin-api/vision-support.test.ts +81 -0
- package/src/plugin-api/vision-support.ts +57 -10
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
- package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
- package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
- package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
- package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
- package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
- package/src/providers/anthropic/client.ts +9 -4
- package/src/providers/inline-audio-support.test.ts +34 -0
- package/src/providers/inline-audio-support.ts +27 -0
- package/src/providers/model-catalog.ts +22 -2
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
- package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
- package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
- package/src/providers/openai/api-error-normalization.ts +18 -3
- package/src/providers/openai/chat-completions-provider.ts +113 -13
- package/src/providers/openai/responses-provider.ts +10 -8
- package/src/providers/provider-secret-catalog.ts +11 -1
- package/src/providers/vellum/client.ts +14 -1
- package/src/providers/vellum/personality-directions.test.ts +163 -0
- package/src/providers/vellum/personality-directions.ts +120 -0
- package/src/providers/xml-tool-call-salvage.ts +212 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
- package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
- package/src/runtime/auth/same-actor.ts +29 -5
- package/src/runtime/background-job-runner.ts +38 -29
- package/src/runtime/guardian-action-service.ts +20 -1
- package/src/runtime/migrations/vbundle-validator.ts +3 -3
- package/src/runtime/pending-interactions.ts +4 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
- package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
- package/src/runtime/routes/conversation-query-routes.ts +53 -11
- package/src/runtime/routes/document-comments-routes.ts +8 -2
- package/src/runtime/routes/guardian-action-routes.ts +7 -0
- package/src/runtime/routes/heartbeat-routes.ts +41 -53
- package/src/runtime/routes/host-app-control-routes.ts +10 -36
- package/src/runtime/routes/host-bash-routes.ts +11 -45
- package/src/runtime/routes/host-browser-routes.ts +24 -0
- package/src/runtime/routes/host-cu-routes.ts +11 -45
- package/src/runtime/routes/host-file-routes.ts +11 -44
- package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
- package/src/runtime/routes/host-transfer-routes.ts +33 -25
- package/src/runtime/routes/index.ts +0 -2
- package/src/runtime/routes/integrations/a2a.ts +12 -3
- package/src/runtime/routes/integrations/vercel.ts +13 -3
- package/src/runtime/routes/platform-routes.ts +121 -24
- package/src/runtime/routes/schedule-routes.ts +15 -1
- package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
- package/src/schedule/schedule-timezone.ts +25 -0
- package/src/schedule/scheduler.ts +19 -0
- package/src/schedule/worker.ts +119 -2
- package/src/skills/catalog-install.ts +40 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
- package/src/tools/browser/browser-execution.ts +117 -53
- package/src/tools/browser/browser-status-constants.ts +6 -6
- package/src/tools/capability-offer.test.ts +87 -0
- package/src/tools/capability-offer.ts +93 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/util/provider-error-patterns.ts +34 -0
- package/src/watcher/__tests__/engine.test.ts +5 -2
- package/src/watcher/engine.ts +7 -4
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
- package/src/runtime/routes/sight-frame-routes.ts +0 -136
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { getWorkspaceDir } from "../../util/platform.js";
|
|
1
2
|
import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
|
|
3
|
+
import type { SendMessageOptions } from "../types.js";
|
|
4
|
+
import { hostedDirectionsExtraBody } from "./personality-directions.js";
|
|
2
5
|
|
|
3
6
|
export interface VellumProviderOptions {
|
|
4
7
|
apiKey?: string;
|
|
@@ -7,7 +10,7 @@ export interface VellumProviderOptions {
|
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
|
-
* OpenAI-compatible client for Vellum-hosted GPU inference
|
|
13
|
+
* OpenAI-compatible client for Vellum-hosted GPU inference.
|
|
11
14
|
* Managed requests set `baseURL` to the platform `/v1/runtime-proxy/vellum`
|
|
12
15
|
* path and authenticate with the assistant API key. These models have no
|
|
13
16
|
* bring-your-own-key path.
|
|
@@ -26,4 +29,14 @@ export class VellumProvider extends OpenAIChatCompletionsProvider {
|
|
|
26
29
|
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
27
30
|
});
|
|
28
31
|
}
|
|
32
|
+
|
|
33
|
+
protected override buildRequestExtraBody(
|
|
34
|
+
options?: SendMessageOptions,
|
|
35
|
+
): Record<string, unknown> | undefined {
|
|
36
|
+
const config = options?.config as { model?: string } | undefined;
|
|
37
|
+
return hostedDirectionsExtraBody(
|
|
38
|
+
config?.model ?? this.defaultModel,
|
|
39
|
+
getWorkspaceDir(),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
29
42
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
HOSTED_STEERING_MODEL,
|
|
8
|
+
hostedDirectionsExtraBody,
|
|
9
|
+
mapPersonalitySlidersToDirections,
|
|
10
|
+
parsePersonalitySliderSidecar,
|
|
11
|
+
PERSONALITY_SLIDERS_PATH,
|
|
12
|
+
readPersonalitySliderSidecar,
|
|
13
|
+
resolveHostedDirections,
|
|
14
|
+
} from "./personality-directions.js";
|
|
15
|
+
|
|
16
|
+
function sliders(overrides: Record<string, number>): Record<string, number> {
|
|
17
|
+
return {
|
|
18
|
+
"companion-coworker": 50,
|
|
19
|
+
"genz-boomer": 50,
|
|
20
|
+
"execute-collaborate": 50,
|
|
21
|
+
"playful-serious": 50,
|
|
22
|
+
"polite-unfiltered": 50,
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("mapPersonalitySlidersToDirections", () => {
|
|
28
|
+
test("center and omitted values produce no directions", () => {
|
|
29
|
+
expect(mapPersonalitySlidersToDirections(undefined)).toBeUndefined();
|
|
30
|
+
expect(mapPersonalitySlidersToDirections(null)).toBeUndefined();
|
|
31
|
+
expect(mapPersonalitySlidersToDirections(sliders({}))).toBeUndefined();
|
|
32
|
+
expect(
|
|
33
|
+
mapPersonalitySlidersToDirections(sliders({ "companion-coworker": 50 })),
|
|
34
|
+
).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("maps 0 / 25 / 75 / 100 onto one-sided alphas", () => {
|
|
38
|
+
expect(
|
|
39
|
+
mapPersonalitySlidersToDirections(sliders({ "companion-coworker": 0 })),
|
|
40
|
+
).toEqual({ companion: 1 });
|
|
41
|
+
expect(
|
|
42
|
+
mapPersonalitySlidersToDirections(sliders({ "companion-coworker": 25 })),
|
|
43
|
+
).toEqual({ companion: 0.5 });
|
|
44
|
+
expect(
|
|
45
|
+
mapPersonalitySlidersToDirections(sliders({ "companion-coworker": 75 })),
|
|
46
|
+
).toEqual({ coworker: 0.5 });
|
|
47
|
+
expect(
|
|
48
|
+
mapPersonalitySlidersToDirections(sliders({ "companion-coworker": 100 })),
|
|
49
|
+
).toEqual({ coworker: 1 });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("maps each axis onto its left or right pole", () => {
|
|
53
|
+
expect(
|
|
54
|
+
mapPersonalitySlidersToDirections(
|
|
55
|
+
sliders({
|
|
56
|
+
"companion-coworker": 0,
|
|
57
|
+
"genz-boomer": 100,
|
|
58
|
+
"execute-collaborate": 25,
|
|
59
|
+
"playful-serious": 75,
|
|
60
|
+
"polite-unfiltered": 0,
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
).toEqual({
|
|
64
|
+
companion: 1,
|
|
65
|
+
boomer: 1,
|
|
66
|
+
independent: 0.5,
|
|
67
|
+
serious: 0.5,
|
|
68
|
+
polite: 1,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("clamps out-of-range sliders and ignores non-finite values", () => {
|
|
73
|
+
expect(
|
|
74
|
+
mapPersonalitySlidersToDirections(
|
|
75
|
+
sliders({
|
|
76
|
+
"companion-coworker": -20,
|
|
77
|
+
"genz-boomer": 140,
|
|
78
|
+
"playful-serious": Number.NaN,
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
).toEqual({ companion: 1, boomer: 1 });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("ignores unknown slider ids", () => {
|
|
85
|
+
expect(
|
|
86
|
+
mapPersonalitySlidersToDirections(
|
|
87
|
+
sliders({ "not-an-axis": 0, "companion-coworker": 25 }),
|
|
88
|
+
),
|
|
89
|
+
).toEqual({ companion: 0.5 });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("resolveHostedDirections", () => {
|
|
94
|
+
test("attaches directions only for the hosted Qwen snapshot", () => {
|
|
95
|
+
const values = sliders({ "companion-coworker": 0 });
|
|
96
|
+
expect(
|
|
97
|
+
resolveHostedDirections({
|
|
98
|
+
model: HOSTED_STEERING_MODEL,
|
|
99
|
+
sliders: values,
|
|
100
|
+
}),
|
|
101
|
+
).toEqual({ companion: 1 });
|
|
102
|
+
expect(
|
|
103
|
+
resolveHostedDirections({
|
|
104
|
+
model: "claude-opus-4-8",
|
|
105
|
+
sliders: values,
|
|
106
|
+
}),
|
|
107
|
+
).toBeUndefined();
|
|
108
|
+
expect(
|
|
109
|
+
resolveHostedDirections({
|
|
110
|
+
model: HOSTED_STEERING_MODEL,
|
|
111
|
+
sliders: sliders({}),
|
|
112
|
+
}),
|
|
113
|
+
).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("personality slider sidecar", () => {
|
|
118
|
+
test("parses a valid sidecar and rejects malformed payloads", () => {
|
|
119
|
+
expect(
|
|
120
|
+
parsePersonalitySliderSidecar(
|
|
121
|
+
JSON.stringify({ "companion-coworker": 25, extra: "nope" }),
|
|
122
|
+
),
|
|
123
|
+
).toEqual({ "companion-coworker": 25 });
|
|
124
|
+
expect(parsePersonalitySliderSidecar("not-json")).toBeNull();
|
|
125
|
+
expect(parsePersonalitySliderSidecar("[]")).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("reads data/personality-sliders.json from the workspace", () => {
|
|
129
|
+
const workspace = mkdtempSync(join(tmpdir(), "personality-sliders-"));
|
|
130
|
+
mkdirSync(join(workspace, "data"));
|
|
131
|
+
writeFileSync(
|
|
132
|
+
join(workspace, PERSONALITY_SLIDERS_PATH),
|
|
133
|
+
JSON.stringify({ "companion-coworker": 0 }),
|
|
134
|
+
);
|
|
135
|
+
expect(readPersonalitySliderSidecar(workspace)).toEqual({
|
|
136
|
+
"companion-coworker": 0,
|
|
137
|
+
});
|
|
138
|
+
expect(readPersonalitySliderSidecar(join(workspace, "missing"))).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("builds extraBody only for hosted Qwen with a nonzero sidecar", () => {
|
|
142
|
+
const workspace = mkdtempSync(join(tmpdir(), "personality-extra-body-"));
|
|
143
|
+
mkdirSync(join(workspace, "data"));
|
|
144
|
+
writeFileSync(
|
|
145
|
+
join(workspace, PERSONALITY_SLIDERS_PATH),
|
|
146
|
+
JSON.stringify({ "companion-coworker": 25 }),
|
|
147
|
+
);
|
|
148
|
+
expect(hostedDirectionsExtraBody(HOSTED_STEERING_MODEL, workspace)).toEqual(
|
|
149
|
+
{
|
|
150
|
+
directions: { companion: 0.5 },
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
expect(
|
|
154
|
+
hostedDirectionsExtraBody("claude-opus-4-8", workspace),
|
|
155
|
+
).toBeUndefined();
|
|
156
|
+
expect(
|
|
157
|
+
hostedDirectionsExtraBody(
|
|
158
|
+
HOSTED_STEERING_MODEL,
|
|
159
|
+
join(workspace, "missing"),
|
|
160
|
+
),
|
|
161
|
+
).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map persisted personality sliders onto hosted-inference direction alphas.
|
|
3
|
+
*
|
|
4
|
+
* Sliders are one-sided around center: 50 is no steer. Values below 50
|
|
5
|
+
* activate the left pole; values above 50 activate the right pole. The
|
|
6
|
+
* GPU server never sees slider ids, only `{ pole: alpha }` in `(0, 1]`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
PERSONALITY_DIRECTION_AXES,
|
|
14
|
+
PERSONALITY_SLIDERS_PATH,
|
|
15
|
+
} from "../../api/constants/personality-sliders.js";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
PERSONALITY_DIRECTION_AXES,
|
|
19
|
+
PERSONALITY_SLIDERS_PATH,
|
|
20
|
+
} from "../../api/constants/personality-sliders.js";
|
|
21
|
+
|
|
22
|
+
export const HOSTED_STEERING_MODEL = "qwen/qwen3-8b";
|
|
23
|
+
|
|
24
|
+
export type PersonalityDirections = Record<string, number>;
|
|
25
|
+
|
|
26
|
+
export function isHostedSteeringModel(model: string): boolean {
|
|
27
|
+
return model === HOSTED_STEERING_MODEL;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clampSlider(value: number): number {
|
|
31
|
+
if (value < 0) {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
if (value > 100) {
|
|
35
|
+
return 100;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function mapPersonalitySlidersToDirections(
|
|
41
|
+
values: Record<string, number> | null | undefined,
|
|
42
|
+
): PersonalityDirections | undefined {
|
|
43
|
+
if (!values) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const directions: PersonalityDirections = {};
|
|
47
|
+
for (const axis of PERSONALITY_DIRECTION_AXES) {
|
|
48
|
+
const raw = values[axis.sliderId];
|
|
49
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const slider = clampSlider(raw);
|
|
53
|
+
if (slider < 50) {
|
|
54
|
+
directions[axis.left] = (50 - slider) / 50;
|
|
55
|
+
} else if (slider > 50) {
|
|
56
|
+
directions[axis.right] = (slider - 50) / 50;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (Object.keys(directions).length === 0) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return directions;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parsePersonalitySliderSidecar(
|
|
66
|
+
raw: string,
|
|
67
|
+
): Record<string, number> | null {
|
|
68
|
+
let parsed: unknown;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(raw);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const values: Record<string, number> = {};
|
|
78
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
79
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
80
|
+
values[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return values;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readPersonalitySliderSidecar(
|
|
87
|
+
workspaceDir: string,
|
|
88
|
+
): Record<string, number> | null {
|
|
89
|
+
try {
|
|
90
|
+
return parsePersonalitySliderSidecar(
|
|
91
|
+
readFileSync(join(workspaceDir, PERSONALITY_SLIDERS_PATH), "utf-8"),
|
|
92
|
+
);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function resolveHostedDirections(args: {
|
|
99
|
+
model: string;
|
|
100
|
+
sliders: Record<string, number> | null | undefined;
|
|
101
|
+
}): PersonalityDirections | undefined {
|
|
102
|
+
if (!isHostedSteeringModel(args.model)) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return mapPersonalitySlidersToDirections(args.sliders);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function hostedDirectionsExtraBody(
|
|
109
|
+
model: string,
|
|
110
|
+
workspaceDir: string,
|
|
111
|
+
): Record<string, unknown> | undefined {
|
|
112
|
+
const directions = resolveHostedDirections({
|
|
113
|
+
model,
|
|
114
|
+
sliders: readPersonalitySliderSidecar(workspaceDir),
|
|
115
|
+
});
|
|
116
|
+
if (!directions) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return { directions };
|
|
120
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Salvage Anthropic-style XML tool calls that some OpenAI-compatible models
|
|
3
|
+
* emit as assistant text instead of native `tool_calls`.
|
|
4
|
+
*
|
|
5
|
+
* Off by default. DeepSeek model ids enable it because those models are
|
|
6
|
+
* trained to emit DSML `invoke` markup and OpenAI-compatible hosts sometimes
|
|
7
|
+
* leave a prefix-stripped variant in `content`. Invokes inside markdown
|
|
8
|
+
* fences stay as text.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { indexOfTag, partialTagSuffix } from "../util/think-tag-stream.js";
|
|
12
|
+
|
|
13
|
+
/** Openers a streaming scanner holds back so XML tool calls are not shown as text. */
|
|
14
|
+
export const XML_TOOL_CALL_OPEN_TAGS = [
|
|
15
|
+
"<invoke",
|
|
16
|
+
"<function_calls",
|
|
17
|
+
"<minimax:tool_call",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export interface SalvagedXmlToolCall {
|
|
21
|
+
name: string;
|
|
22
|
+
input: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface XmlToolCallSalvage {
|
|
26
|
+
/** Assistant text with salvaged invoke blocks removed. */
|
|
27
|
+
text: string;
|
|
28
|
+
calls: SalvagedXmlToolCall[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const INVOKE_CLOSE = "</invoke>";
|
|
32
|
+
const NAME_ATTR_RE = /\bname\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
|
|
33
|
+
const PARAMETER_CLOSE = "</parameter>";
|
|
34
|
+
|
|
35
|
+
const EMPTY_WRAPPER_RE =
|
|
36
|
+
/<(?:function_calls|minimax:tool_call)>\s*<\/(?:function_calls|minimax:tool_call)>/gi;
|
|
37
|
+
|
|
38
|
+
/** True when the model id is a DeepSeek variant that leaks XML tool calls. */
|
|
39
|
+
export function shouldSalvageXmlToolCalls(
|
|
40
|
+
model: string | null | undefined,
|
|
41
|
+
): boolean {
|
|
42
|
+
return typeof model === "string" && /deepseek/i.test(model);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function salvageXmlToolCalls(
|
|
46
|
+
text: string,
|
|
47
|
+
offeredToolNames: ReadonlySet<string>,
|
|
48
|
+
): XmlToolCallSalvage | null {
|
|
49
|
+
if (!text || offeredToolNames.size === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const spans: Array<{
|
|
54
|
+
start: number;
|
|
55
|
+
end: number;
|
|
56
|
+
call: SalvagedXmlToolCall;
|
|
57
|
+
}> = [];
|
|
58
|
+
|
|
59
|
+
const invokeOpenRe = /<invoke\b[^>]*>/gi;
|
|
60
|
+
let openMatch: RegExpExecArray | null = invokeOpenRe.exec(text);
|
|
61
|
+
while (openMatch) {
|
|
62
|
+
const openTag = openMatch[0];
|
|
63
|
+
const rawName = parseNameAttr(openTag);
|
|
64
|
+
const bodyStart = openMatch.index + openTag.length;
|
|
65
|
+
const closeIdx = text.indexOf(INVOKE_CLOSE, bodyStart);
|
|
66
|
+
if (closeIdx < 0) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const canonicalName = rawName
|
|
71
|
+
? resolveOfferedName(rawName, offeredToolNames)
|
|
72
|
+
: null;
|
|
73
|
+
if (canonicalName && !isInsideMarkdownFence(text, openMatch.index)) {
|
|
74
|
+
spans.push({
|
|
75
|
+
start: openMatch.index,
|
|
76
|
+
end: closeIdx + INVOKE_CLOSE.length,
|
|
77
|
+
call: {
|
|
78
|
+
name: canonicalName,
|
|
79
|
+
input: parseParameters(text.slice(bodyStart, closeIdx)),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
invokeOpenRe.lastIndex = closeIdx + INVOKE_CLOSE.length;
|
|
85
|
+
openMatch = invokeOpenRe.exec(text);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (spans.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let leftover = "";
|
|
93
|
+
let cursor = 0;
|
|
94
|
+
for (const span of spans) {
|
|
95
|
+
leftover += text.slice(cursor, span.start);
|
|
96
|
+
cursor = span.end;
|
|
97
|
+
}
|
|
98
|
+
leftover += text.slice(cursor);
|
|
99
|
+
leftover = leftover.replace(EMPTY_WRAPPER_RE, "").trim();
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
text: leftover,
|
|
103
|
+
calls: spans.map((span) => span.call),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Split streamed text so a complete or in-progress XML tool-call opener is
|
|
109
|
+
* held back rather than shown as user-visible content.
|
|
110
|
+
*/
|
|
111
|
+
export function splitXmlToolCallHoldback(buffer: string): {
|
|
112
|
+
visible: string;
|
|
113
|
+
held: string;
|
|
114
|
+
} {
|
|
115
|
+
if (!buffer) {
|
|
116
|
+
return { visible: "", held: "" };
|
|
117
|
+
}
|
|
118
|
+
const hit = indexOfTag(buffer, XML_TOOL_CALL_OPEN_TAGS, false);
|
|
119
|
+
if (hit) {
|
|
120
|
+
return {
|
|
121
|
+
visible: buffer.slice(0, hit.index),
|
|
122
|
+
held: buffer.slice(hit.index),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const partial = partialTagSuffix(buffer, XML_TOOL_CALL_OPEN_TAGS, false);
|
|
126
|
+
if (partial > 0) {
|
|
127
|
+
return {
|
|
128
|
+
visible: buffer.slice(0, buffer.length - partial),
|
|
129
|
+
held: buffer.slice(buffer.length - partial),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { visible: buffer, held: "" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseNameAttr(tag: string): string | null {
|
|
136
|
+
const match = NAME_ATTR_RE.exec(tag);
|
|
137
|
+
if (!match) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const name = match[1] ?? match[2] ?? match[3];
|
|
141
|
+
return name && name.length > 0 ? name : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveOfferedName(
|
|
145
|
+
name: string,
|
|
146
|
+
offeredToolNames: ReadonlySet<string>,
|
|
147
|
+
): string | null {
|
|
148
|
+
if (offeredToolNames.has(name)) {
|
|
149
|
+
return name;
|
|
150
|
+
}
|
|
151
|
+
const lower = name.toLowerCase();
|
|
152
|
+
for (const offered of offeredToolNames) {
|
|
153
|
+
if (offered.toLowerCase() === lower) {
|
|
154
|
+
return offered;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseParameters(body: string): Record<string, unknown> {
|
|
161
|
+
const input: Record<string, unknown> = {};
|
|
162
|
+
const parameterOpenRe = /<parameter\b[^>]*>/gi;
|
|
163
|
+
let paramMatch: RegExpExecArray | null = parameterOpenRe.exec(body);
|
|
164
|
+
while (paramMatch) {
|
|
165
|
+
const openTag = paramMatch[0];
|
|
166
|
+
const paramName = parseNameAttr(openTag);
|
|
167
|
+
const valueStart = paramMatch.index + openTag.length;
|
|
168
|
+
const closeIdx = body.indexOf(PARAMETER_CLOSE, valueStart);
|
|
169
|
+
if (closeIdx < 0) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if (paramName) {
|
|
173
|
+
input[paramName] = parseParamValue(
|
|
174
|
+
decodeXmlEntities(body.slice(valueStart, closeIdx)),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
parameterOpenRe.lastIndex = closeIdx + PARAMETER_CLOSE.length;
|
|
178
|
+
paramMatch = parameterOpenRe.exec(body);
|
|
179
|
+
}
|
|
180
|
+
return input;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseParamValue(raw: string): unknown {
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(raw);
|
|
186
|
+
} catch {
|
|
187
|
+
return raw;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function decodeXmlEntities(value: string): string {
|
|
192
|
+
return value
|
|
193
|
+
.replace(/</g, "<")
|
|
194
|
+
.replace(/>/g, ">")
|
|
195
|
+
.replace(/"/g, '"')
|
|
196
|
+
.replace(/'/g, "'")
|
|
197
|
+
.replace(/&/g, "&");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isInsideMarkdownFence(text: string, index: number): boolean {
|
|
201
|
+
let inside = false;
|
|
202
|
+
let searchFrom = 0;
|
|
203
|
+
while (searchFrom < index) {
|
|
204
|
+
const next = text.indexOf("```", searchFrom);
|
|
205
|
+
if (next < 0 || next >= index) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
inside = !inside;
|
|
209
|
+
searchFrom = next + 3;
|
|
210
|
+
}
|
|
211
|
+
return inside;
|
|
212
|
+
}
|
|
@@ -490,7 +490,7 @@ describe("runBackgroundJob", () => {
|
|
|
490
490
|
expect(bootstrapLastArgs).not.toHaveProperty("scheduleJobId");
|
|
491
491
|
});
|
|
492
492
|
|
|
493
|
-
test("
|
|
493
|
+
test("empty sandwich prompt uses the postamble as the processMessage kickoff", async () => {
|
|
494
494
|
let addMessageCountAtProcessMessageStart = -1;
|
|
495
495
|
processMessageImpl = async () => {
|
|
496
496
|
addMessageCountAtProcessMessageStart = addMessageCalls.length;
|
|
@@ -508,7 +508,46 @@ describe("runBackgroundJob", () => {
|
|
|
508
508
|
}),
|
|
509
509
|
);
|
|
510
510
|
|
|
511
|
-
//
|
|
511
|
+
// persistUserMessage rejects empty content, so the trusted postamble is
|
|
512
|
+
// the kickoff rather than a third addMessage plus an empty prompt.
|
|
513
|
+
expect(addMessageCalls).toHaveLength(2);
|
|
514
|
+
expect(addMessageCalls[0]).toMatchObject({
|
|
515
|
+
conversationId: STUB_CONVERSATION_ID,
|
|
516
|
+
role: "user",
|
|
517
|
+
content: "TRUSTED_PRE",
|
|
518
|
+
});
|
|
519
|
+
expect(addMessageCalls[1]).toMatchObject({
|
|
520
|
+
conversationId: STUB_CONVERSATION_ID,
|
|
521
|
+
role: "assistant",
|
|
522
|
+
content: "UNTRUSTED_PAYLOAD",
|
|
523
|
+
});
|
|
524
|
+
expect(processMessageCalls).toHaveLength(1);
|
|
525
|
+
expect(processMessageCalls[0].content).toBe("TRUSTED_POST");
|
|
526
|
+
expect(processMessageCalls[0].options).toMatchObject({
|
|
527
|
+
skipUserMessageIndexing: true,
|
|
528
|
+
});
|
|
529
|
+
expect(addMessageCountAtProcessMessageStart).toBe(2);
|
|
530
|
+
expect(emitCalls).toHaveLength(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("non-empty sandwich prompt still seeds all three sandwich messages before processMessage", async () => {
|
|
534
|
+
let addMessageCountAtProcessMessageStart = -1;
|
|
535
|
+
processMessageImpl = async () => {
|
|
536
|
+
addMessageCountAtProcessMessageStart = addMessageCalls.length;
|
|
537
|
+
return { messageId: "msg-final" };
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
await runBackgroundJob(
|
|
541
|
+
baseOpts({
|
|
542
|
+
prompt: "do the test",
|
|
543
|
+
assistantSandwich: {
|
|
544
|
+
preamble: "TRUSTED_PRE",
|
|
545
|
+
content: "UNTRUSTED_PAYLOAD",
|
|
546
|
+
postamble: "TRUSTED_POST",
|
|
547
|
+
},
|
|
548
|
+
}),
|
|
549
|
+
);
|
|
550
|
+
|
|
512
551
|
expect(addMessageCalls).toHaveLength(3);
|
|
513
552
|
expect(addMessageCalls[0]).toMatchObject({
|
|
514
553
|
conversationId: STUB_CONVERSATION_ID,
|
|
@@ -526,8 +565,7 @@ describe("runBackgroundJob", () => {
|
|
|
526
565
|
content: "TRUSTED_POST",
|
|
527
566
|
});
|
|
528
567
|
expect(processMessageCalls).toHaveLength(1);
|
|
529
|
-
expect(processMessageCalls[0].content).toBe("");
|
|
530
|
-
// processMessage observed all 3 sandwich messages already in place.
|
|
568
|
+
expect(processMessageCalls[0].content).toBe("do the test");
|
|
531
569
|
expect(addMessageCountAtProcessMessageStart).toBe(3);
|
|
532
570
|
});
|
|
533
571
|
|
|
@@ -72,9 +72,7 @@ describe("route policy coverage", () => {
|
|
|
72
72
|
// registry had no entry for these endpoints, so `enforcePolicy`
|
|
73
73
|
// returned allowed). Migration preserves behavior. Triage these
|
|
74
74
|
// and assign real policies in a follow-up PR:
|
|
75
|
-
// -
|
|
76
|
-
// - integrations/a2a/{config,invite/accept}
|
|
77
|
-
// - integrations/vercel/config
|
|
75
|
+
// - integrations/a2a/invite/accept
|
|
78
76
|
const INTENTIONALLY_UNPROTECTED = new Set([
|
|
79
77
|
// A — design-intentional
|
|
80
78
|
"health",
|
|
@@ -90,10 +88,7 @@ describe("route policy coverage", () => {
|
|
|
90
88
|
"playground/seeded-conversations",
|
|
91
89
|
"playground/seeded-conversations/:id",
|
|
92
90
|
// C — pre-existing latent unprotected (follow-up audit owed)
|
|
93
|
-
"documents/:id/comments/:commentId",
|
|
94
|
-
"integrations/a2a/config",
|
|
95
91
|
"integrations/a2a/invite/accept",
|
|
96
|
-
"integrations/vercel/config",
|
|
97
92
|
]);
|
|
98
93
|
|
|
99
94
|
const unprotectedFound: string[] = [];
|
|
@@ -297,4 +297,69 @@ describe("ROUTES policy declarations", () => {
|
|
|
297
297
|
expect(r.policy === null || typeof r.policy === "object").toBe(true);
|
|
298
298
|
}
|
|
299
299
|
});
|
|
300
|
+
|
|
301
|
+
test.each([
|
|
302
|
+
["integrations_vercel_config_get", "settings.read"],
|
|
303
|
+
["integrations_vercel_config_post", "settings.write"],
|
|
304
|
+
["integrations_vercel_config_delete", "settings.write"],
|
|
305
|
+
["integrations_a2a_config_get", "settings.read"],
|
|
306
|
+
["integrations_a2a_config_post", "settings.write"],
|
|
307
|
+
["integrations_a2a_config_delete", "settings.write"],
|
|
308
|
+
] as const)(
|
|
309
|
+
"%s requires %s and actor principals",
|
|
310
|
+
async (operationId, scope) => {
|
|
311
|
+
const { ROUTES } = await import("../../routes/index.js");
|
|
312
|
+
const route = ROUTES.find((r) => r.operationId === operationId);
|
|
313
|
+
expect(route).toBeDefined();
|
|
314
|
+
expect(route!.policy).not.toBeNull();
|
|
315
|
+
expect(route!.policy!.requiredScopes).toEqual([scope]);
|
|
316
|
+
expect(route!.policy!.allowedPrincipalTypes).toEqual([
|
|
317
|
+
"actor",
|
|
318
|
+
"svc_gateway",
|
|
319
|
+
"svc_daemon",
|
|
320
|
+
"local",
|
|
321
|
+
]);
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
test("vercel config GET denies a chat-only actor and POST denies without settings.write", async () => {
|
|
326
|
+
const { ROUTES } = await import("../../routes/index.js");
|
|
327
|
+
const getRoute = ROUTES.find(
|
|
328
|
+
(r) => r.operationId === "integrations_vercel_config_get",
|
|
329
|
+
);
|
|
330
|
+
const postRoute = ROUTES.find(
|
|
331
|
+
(r) => r.operationId === "integrations_vercel_config_post",
|
|
332
|
+
);
|
|
333
|
+
expect(getRoute?.policy).not.toBeNull();
|
|
334
|
+
expect(postRoute?.policy).not.toBeNull();
|
|
335
|
+
|
|
336
|
+
authDisabled = false;
|
|
337
|
+
const chatOnly = buildTestContext({
|
|
338
|
+
principalType: "actor",
|
|
339
|
+
scopes: ["chat.read", "chat.write"],
|
|
340
|
+
});
|
|
341
|
+
const getDenied = enforcePolicy(
|
|
342
|
+
getRoute!.endpoint,
|
|
343
|
+
getRoute!.policy!,
|
|
344
|
+
chatOnly,
|
|
345
|
+
);
|
|
346
|
+
expect(getDenied).not.toBeNull();
|
|
347
|
+
expect(getDenied!.status).toBe(403);
|
|
348
|
+
|
|
349
|
+
const postDenied = enforcePolicy(
|
|
350
|
+
postRoute!.endpoint,
|
|
351
|
+
postRoute!.policy!,
|
|
352
|
+
chatOnly,
|
|
353
|
+
);
|
|
354
|
+
expect(postDenied).not.toBeNull();
|
|
355
|
+
expect(postDenied!.status).toBe(403);
|
|
356
|
+
|
|
357
|
+
const settingsActor = buildTestContext({
|
|
358
|
+
principalType: "actor",
|
|
359
|
+
scopes: ["settings.write"],
|
|
360
|
+
});
|
|
361
|
+
expect(
|
|
362
|
+
enforcePolicy(postRoute!.endpoint, postRoute!.policy!, settingsActor),
|
|
363
|
+
).toBeNull();
|
|
364
|
+
});
|
|
300
365
|
});
|