@jskit-ai/assistant-runtime 0.2.19 → 0.2.21
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/CHANGELOG.md +12 -0
- package/fixtures/conversation/vite.config.mjs +2 -1
- package/locales/en.json +2 -1
- package/package.json +8 -8
- package/patterns/assistant-surface/PATTERN.md +5 -0
- package/src/server/services/assistantConfigService.js +39 -16
- package/src/server/support/assistantServerConfig.js +33 -2
- package/test/assistantConversationOperations.test.js +4 -3
- package/test/assistantRuntimeComposition.test.js +1 -1
- package/test/assistantServerConfig.test.js +2 -1
- package/test/assistantSettingsAccess.test.js +104 -0
- package/test/chatServiceLifecycle.test.js +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Changing an assistant's instructions needs `assistant.settings.update` on a settings surface that requires neither a workspace nor the console owner (it needed only a signed-in user), still `workspace.settings.update` on a workspace surface and the console owner on a console-owner surface. `assistantServer.<surface>.settingsUpdatePermission` names another permission, or `{ require: "authenticated" }` to keep the old behaviour. A refusal is `assistant.runtime.settings.updateForbidden` (403).
|
|
6
|
+
|
|
7
|
+
### New message keys (1)
|
|
8
|
+
|
|
9
|
+
- `jskit.assistant.runtime.settings.updateForbidden`
|
|
10
|
+
|
|
11
|
+
### Upgrading
|
|
12
|
+
|
|
13
|
+
- Grant `assistant.settings.update` to the people who configure the assistant, or set `settingsUpdatePermission` to a permission they already hold.
|
|
14
|
+
|
|
3
15
|
## 0.2.0 - 2026-10-29
|
|
4
16
|
|
|
5
17
|
- Translation ready: every user-visible text is a message key with its English in `locales/en.json`, so an app translates it by supplying translation files only. With no translations, texts and behaviour are unchanged. See the guide "Translating a JSKIT app".
|
|
@@ -68,7 +68,8 @@ export default defineConfig({
|
|
|
68
68
|
aiClientFactory: { async resolveClient(_surface, { integrationId }) {
|
|
69
69
|
return { enabled: true, provider: "fixture", defaultModel: integrationId, supportsAttachments: true,
|
|
70
70
|
model: createScriptedModel(async function* ({ messages }) {
|
|
71
|
-
|
|
71
|
+
// The request, before the engine's state note.
|
|
72
|
+
stream.receivedFiles = messages.at(-2).content;
|
|
72
73
|
await nextChunk();
|
|
73
74
|
yield { choices: [{ delta: { content: "Partial answer" } }] };
|
|
74
75
|
await nextChunk();
|
package/locales/en.json
CHANGED
|
@@ -125,7 +125,8 @@
|
|
|
125
125
|
"notConfigured": "Assistant settings are not configured for this surface.",
|
|
126
126
|
"workspaceUnavailable": "Workspace support is not available for this assistant surface.",
|
|
127
127
|
"selectWorkspace": "Select a workspace to configure assistant settings.",
|
|
128
|
-
"updateFailed": "Unable to update assistant settings."
|
|
128
|
+
"updateFailed": "Unable to update assistant settings.",
|
|
129
|
+
"updateForbidden": "You do not have permission to change the assistant's instructions."
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -14,19 +14,19 @@
|
|
|
14
14
|
"./locales/en.json": "./locales/en.json"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@jskit-ai/assistant-core": "0.2.
|
|
17
|
+
"@jskit-ai/assistant-core": "0.2.19",
|
|
18
18
|
"json-rest-schema": "^1.0.17"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
22
|
-
"@jskit-ai/http-web": "0.2.
|
|
23
|
-
"@jskit-ai/kernel": "0.2.
|
|
24
|
-
"@jskit-ai/realtime": "0.2.
|
|
25
|
-
"@jskit-ai/shell-web": "0.2.
|
|
21
|
+
"@jskit-ai/http-runtime": "0.2.9",
|
|
22
|
+
"@jskit-ai/http-web": "0.2.15",
|
|
23
|
+
"@jskit-ai/kernel": "0.2.9",
|
|
24
|
+
"@jskit-ai/realtime": "0.2.13",
|
|
25
|
+
"@jskit-ai/shell-web": "0.2.10",
|
|
26
26
|
"@tanstack/vue-query": "^5.90.5",
|
|
27
27
|
"vue": "^3.5.13",
|
|
28
28
|
"vuetify": "^4.0.0",
|
|
29
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
29
|
+
"@jskit-ai/database-runtime": "0.1.258"
|
|
30
30
|
},
|
|
31
31
|
"description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
|
|
32
32
|
"jskit": {
|
|
@@ -34,6 +34,11 @@ in source or a questionnaire transcript.
|
|
|
34
34
|
`assistantServer`.
|
|
35
35
|
- The server config selects an AI integration ID, never a credential value.
|
|
36
36
|
- The application publishes an authorized `integrations.ai` resolver capability.
|
|
37
|
+
- Changing the assistant's instructions needs `workspace.settings.update` on a
|
|
38
|
+
workspace settings surface, the console owner on a console-owner surface and
|
|
39
|
+
`assistant.settings.update` on any other, unless
|
|
40
|
+
`assistantServer.<surface>.settingsUpdatePermission` names the permission the
|
|
41
|
+
application's administrators hold. Reading them needs only a signed-in user.
|
|
37
42
|
- Runtime and settings pages use the public assistant elements.
|
|
38
43
|
- Environment values are supplied through the application's normal secret
|
|
39
44
|
environment boundary.
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { AppError, requireAuth } from "@jskit-ai/kernel/server/runtime";
|
|
2
2
|
import { normalizeObject, normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
3
|
import { requireAssistantSurface } from "../support/assistantSurface.js";
|
|
4
|
+
import { resolveAssistantServerConfig, resolveAssistantServerConfigMap } from "../support/assistantServerConfig.js";
|
|
4
5
|
|
|
5
6
|
/** @typedef {import("../types.js").AssistantConfigRepository} AssistantConfigRepository */
|
|
6
7
|
/** @typedef {import("../types.js").WorkspaceScopeSupport} WorkspaceScopeSupport */
|
|
7
8
|
|
|
9
|
+
const WORKSPACE_SETTINGS_UPDATE_POLICY = Object.freeze({ require: "all", permissions: Object.freeze(["workspace.settings.update"]) });
|
|
10
|
+
const ASSISTANT_SETTINGS_UPDATE_POLICY = Object.freeze({ require: "all", permissions: Object.freeze(["assistant.settings.update"]) });
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
13
|
* An assistant surface's saved settings: who may read and change them, and the
|
|
10
|
-
* system prompt they give the surface's conversations.
|
|
14
|
+
* system prompt they give the surface's conversations. Changing them needs the
|
|
15
|
+
* surface's `assistantServer.<surface>.settingsUpdatePermission`, by default
|
|
16
|
+
* `workspace.settings.update` on a workspace settings surface, the console owner
|
|
17
|
+
* on a console-owner surface, and `assistant.settings.update` everywhere else.
|
|
11
18
|
* @param {object} dependencies
|
|
12
19
|
* @param {AssistantConfigRepository} dependencies.assistantConfigRepository
|
|
13
20
|
* @param {import("@jskit-ai/console-core/server/types").ConsoleService | null} dependencies.consoleService The console access service, when the console is installed.
|
|
@@ -20,28 +27,44 @@ function createService({
|
|
|
20
27
|
appConfig,
|
|
21
28
|
workspaceScopeSupport
|
|
22
29
|
}) {
|
|
30
|
+
// Read once here, so a malformed settingsUpdatePermission stops the server from starting.
|
|
31
|
+
resolveAssistantServerConfigMap(appConfig);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The auth policy that changing a surface's settings needs, or null when its console-owner check is all.
|
|
35
|
+
* @param {ReturnType<typeof requireAssistantSurface>} assistantSurface
|
|
36
|
+
*/
|
|
37
|
+
function resolveSettingsUpdatePolicy(assistantSurface) {
|
|
38
|
+
const configured = resolveAssistantServerConfig(appConfig, assistantSurface.targetSurfaceId).settingsUpdatePermission;
|
|
39
|
+
if (configured) {
|
|
40
|
+
return configured;
|
|
41
|
+
}
|
|
42
|
+
if (assistantSurface.settingsSurfaceRequiresWorkspace === true) {
|
|
43
|
+
return WORKSPACE_SETTINGS_UPDATE_POLICY;
|
|
44
|
+
}
|
|
45
|
+
return assistantSurface.settingsSurfaceRequiresConsoleOwner === true ? null : ASSISTANT_SETTINGS_UPDATE_POLICY;
|
|
46
|
+
}
|
|
47
|
+
|
|
23
48
|
async function requireSettingsAccess(assistantSurface, context = {}, { mode = "read" } = {}) {
|
|
24
49
|
requireAuth({ context }, {
|
|
25
50
|
require: "authenticated"
|
|
26
51
|
});
|
|
27
52
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
return;
|
|
53
|
+
const updatePolicy = mode === "write" ? resolveSettingsUpdatePolicy(assistantSurface) : null;
|
|
54
|
+
if (updatePolicy) {
|
|
55
|
+
requireAuth({ context }, {
|
|
56
|
+
...updatePolicy,
|
|
57
|
+
message: "You do not have permission to change the assistant's instructions.",
|
|
58
|
+
code: "assistant.runtime.settings.updateForbidden"
|
|
59
|
+
});
|
|
60
|
+
} else if (mode !== "write" && assistantSurface.settingsSurfaceRequiresWorkspace === true) {
|
|
61
|
+
requireAuth({ context }, {
|
|
62
|
+
require: "any",
|
|
63
|
+
permissions: ["workspace.settings.view", "workspace.settings.update"]
|
|
64
|
+
});
|
|
42
65
|
}
|
|
43
66
|
|
|
44
|
-
if (assistantSurface
|
|
67
|
+
if (assistantSurface.settingsSurfaceRequiresWorkspace === true || assistantSurface.settingsSurfaceRequiresConsoleOwner !== true) {
|
|
45
68
|
return;
|
|
46
69
|
}
|
|
47
70
|
|
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
2
|
-
import { normalizeObject, normalizeText, normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { isRecord, normalizeObject, normalizeText, normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_AI_TIMEOUT_MS,
|
|
5
5
|
normalizeOptionalHttpUrl,
|
|
6
6
|
normalizeTimeoutMs
|
|
7
7
|
} from "@jskit-ai/assistant-core/server";
|
|
8
8
|
|
|
9
|
+
const SETTINGS_UPDATE_REQUIRE_MODES = Object.freeze(["authenticated", "all", "any"]);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Who may change a surface's assistant settings, as configured: a permission, an auth policy
|
|
13
|
+
* (`{ require, permissions }`), or null for the surface's default.
|
|
14
|
+
* @param {unknown} value
|
|
15
|
+
* @param {string} targetSurfaceId
|
|
16
|
+
*/
|
|
17
|
+
function normalizeSettingsUpdatePermission(value, targetSurfaceId) {
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "string" && normalizeText(value)) {
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
require: "all",
|
|
24
|
+
permissions: Object.freeze([normalizeText(value)])
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const permissions = isRecord(value) ? normalizeUniqueTextList(value.permissions) : [];
|
|
28
|
+
if (isRecord(value) && SETTINGS_UPDATE_REQUIRE_MODES.includes(value.require) && (value.require === "authenticated" || permissions.length > 0)) {
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
require: value.require,
|
|
31
|
+
permissions: Object.freeze(permissions)
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
throw new TypeError(
|
|
35
|
+
`assistantServer.${targetSurfaceId}.settingsUpdatePermission must be a permission or { require: "authenticated" | "all" | "any", permissions }.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
9
39
|
function resolveAssistantServerConfigMap(appConfig) {
|
|
10
40
|
const source = normalizeObject(appConfig?.assistantServer);
|
|
11
41
|
const resolved = {};
|
|
@@ -27,7 +57,8 @@ function resolveAssistantServerConfigMap(appConfig) {
|
|
|
27
57
|
model: normalizeText(entry.model),
|
|
28
58
|
timeoutMs: entry.timeoutMs,
|
|
29
59
|
barredActionIds: Object.freeze(normalizeUniqueTextList(entry.barredActionIds, { acceptSingle: true })),
|
|
30
|
-
toolSkipActionPrefixes: Object.freeze(normalizeUniqueTextList(entry.toolSkipActionPrefixes, { acceptSingle: true }))
|
|
60
|
+
toolSkipActionPrefixes: Object.freeze(normalizeUniqueTextList(entry.toolSkipActionPrefixes, { acceptSingle: true })),
|
|
61
|
+
settingsUpdatePermission: normalizeSettingsUpdatePermission(entry.settingsUpdatePermission, targetSurfaceId)
|
|
31
62
|
});
|
|
32
63
|
}
|
|
33
64
|
|
|
@@ -336,8 +336,9 @@ test("conversation routes keep authenticated ownership and reject unknown identi
|
|
|
336
336
|
assert.equal(accepted.payload.status, "accepted");
|
|
337
337
|
await f.dispatched[0].promise;
|
|
338
338
|
const body = JSON.parse(f.requests[0].body);
|
|
339
|
-
assert.
|
|
340
|
-
assert.equal(body.messages.at(-
|
|
339
|
+
assert.equal(body.messages[0].content, "Answer briefly.", "The host's system prompt");
|
|
340
|
+
assert.equal(body.messages.at(-2).content, "Use the host configuration.");
|
|
341
|
+
assert.match(body.messages.at(-1).content, /^Current state, from the system\. /u, "The engine's state note follows the conversation");
|
|
341
342
|
assert.equal((await f.invoke("GET")).payload.engine, "api");
|
|
342
343
|
});
|
|
343
344
|
|
|
@@ -390,7 +391,7 @@ test("declared conversation data keeps the authored target without becoming auth
|
|
|
390
391
|
const state = (await f.invoke("GET")).payload;
|
|
391
392
|
assert.deepEqual(state.conversationLog[0].user.data, data);
|
|
392
393
|
assert.equal(state.engine, "api");
|
|
393
|
-
assert.
|
|
394
|
+
assert.equal(JSON.parse(f.requests[0].body).messages[0].content, "Answer briefly.");
|
|
394
395
|
const repeated = await f.invoke("POST", "/messages", { body: input });
|
|
395
396
|
assert.equal(repeated.payload.duplicate, true);
|
|
396
397
|
assert.equal(repeated.payload.turnId, accepted.payload.turnId);
|
|
@@ -51,7 +51,7 @@ test("assistant services compose with supplied repositories without database imp
|
|
|
51
51
|
for (const [name, repository] of Object.entries(repositories)) {
|
|
52
52
|
assert.equal(assistant.repositories[name], repository);
|
|
53
53
|
}
|
|
54
|
-
const options = { context: { actor: { id: 23 } } };
|
|
54
|
+
const options = { context: { actor: { id: 23 }, permissions: ["assistant.settings.update"] } };
|
|
55
55
|
assert.equal(await assistant.services.config.getSettings({ targetSurfaceId: "home" }, options), record);
|
|
56
56
|
const updated = await assistant.services.config.updateSettings(
|
|
57
57
|
{ targetSurfaceId: "home" }, { systemPrompt: "Updated prompt" }, options
|
|
@@ -41,7 +41,8 @@ test("assistant server config resolves per-surface AI config without app-level g
|
|
|
41
41
|
model: "claude-config",
|
|
42
42
|
timeoutMs: 45_000,
|
|
43
43
|
barredActionIds: ["demo.admin.secret"],
|
|
44
|
-
toolSkipActionPrefixes: ["demo.hidden."]
|
|
44
|
+
toolSkipActionPrefixes: ["demo.hidden."],
|
|
45
|
+
settingsUpdatePermission: null
|
|
45
46
|
});
|
|
46
47
|
assert.deepEqual(resolveAssistantAiConfig({ appConfig, env }, "admin"), {
|
|
47
48
|
aiConfigPrefix: "ADMIN_ASSISTANT",
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createService } from "../src/server/services/assistantConfigService.js";
|
|
4
|
+
|
|
5
|
+
function createSettingsRows() {
|
|
6
|
+
const rows = new Map();
|
|
7
|
+
const key = ({ targetSurfaceId, workspaceId }) => `${targetSurfaceId}:${workspaceId ?? ""}`;
|
|
8
|
+
return Object.freeze({
|
|
9
|
+
async findByScope(scope) {
|
|
10
|
+
return rows.get(key(scope)) || null;
|
|
11
|
+
},
|
|
12
|
+
createDefaultRecord(scope) {
|
|
13
|
+
return { ...scope, scopeKey: key(scope), settings: { systemPrompt: "" } };
|
|
14
|
+
},
|
|
15
|
+
async upsertByScope({ patch, ...scope }) {
|
|
16
|
+
const record = { ...scope, scopeKey: key(scope), settings: { systemPrompt: patch.systemPrompt } };
|
|
17
|
+
rows.set(key(scope), record);
|
|
18
|
+
return record;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createAppConfig(assistantServer) {
|
|
24
|
+
return {
|
|
25
|
+
surfaceDefinitions: {
|
|
26
|
+
home: { enabled: true, requiresWorkspace: false, accessPolicyId: "authenticated" },
|
|
27
|
+
ops: { enabled: true, requiresWorkspace: false, accessPolicyId: "authenticated" },
|
|
28
|
+
admin: { enabled: true, requiresWorkspace: true, accessPolicyId: "workspace_member" },
|
|
29
|
+
console: { enabled: true, requiresWorkspace: false, accessPolicyId: "console_owner" }
|
|
30
|
+
},
|
|
31
|
+
assistantSurfaces: {
|
|
32
|
+
home: { settingsSurfaceId: "home", configScope: "global" },
|
|
33
|
+
ops: { settingsSurfaceId: "console", configScope: "global" },
|
|
34
|
+
admin: { settingsSurfaceId: "admin", configScope: "workspace" }
|
|
35
|
+
},
|
|
36
|
+
assistantServer
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createSettings(assistantServer, { consoleOwnerIds = [] } = {}) {
|
|
41
|
+
return createService({
|
|
42
|
+
assistantConfigRepository: createSettingsRows(),
|
|
43
|
+
consoleService: {
|
|
44
|
+
async requireConsoleOwner(context) {
|
|
45
|
+
if (!consoleOwnerIds.includes(context.actor.id)) {
|
|
46
|
+
throw Object.assign(new Error("Console owner required."), { code: "console.ownerRequired" });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
appConfig: createAppConfig(assistantServer),
|
|
51
|
+
workspaceScopeSupport: { resolveWorkspace: () => ({ id: "3" }) }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const employee = { actor: { id: "7" }, permissions: [] };
|
|
56
|
+
|
|
57
|
+
async function update(settings, targetSurfaceId, context) {
|
|
58
|
+
return settings.updateSettings({ targetSurfaceId }, { systemPrompt: `Set by ${context.actor.id}` }, { context });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test("an employee can read but not change the instructions of a global assistant", async () => {
|
|
62
|
+
const settings = createSettings({});
|
|
63
|
+
assert.equal((await settings.getSettings({ targetSurfaceId: "home" }, { context: employee })).settings.systemPrompt, "");
|
|
64
|
+
await assert.rejects(update(settings, "home", employee), {
|
|
65
|
+
statusCode: 403,
|
|
66
|
+
code: "assistant.runtime.settings.updateForbidden",
|
|
67
|
+
message: "You do not have permission to change the assistant's instructions."
|
|
68
|
+
});
|
|
69
|
+
const admin = { actor: { id: "1" }, permissions: ["assistant.settings.update"] };
|
|
70
|
+
assert.equal((await update(settings, "home", admin)).settings.systemPrompt, "Set by 1");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("assistantServer.<surface>.settingsUpdatePermission names the permission that changes the instructions", async () => {
|
|
74
|
+
const settings = createSettings({ home: { settingsUpdatePermission: "app.admin" } });
|
|
75
|
+
await assert.rejects(update(settings, "home", { actor: { id: "7" }, permissions: ["assistant.settings.update"] }), {
|
|
76
|
+
code: "assistant.runtime.settings.updateForbidden"
|
|
77
|
+
});
|
|
78
|
+
assert.equal((await update(settings, "home", { actor: { id: "1" }, permissions: ["app.admin"] })).settings.systemPrompt, "Set by 1");
|
|
79
|
+
|
|
80
|
+
const open = createSettings({ home: { settingsUpdatePermission: { require: "authenticated" } } });
|
|
81
|
+
assert.equal((await update(open, "home", employee)).settings.systemPrompt, "Set by 7");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a workspace assistant still needs workspace.settings.update, and a console assistant its console owner", async () => {
|
|
85
|
+
const settings = createSettings({}, { consoleOwnerIds: ["1"] });
|
|
86
|
+
await assert.rejects(update(settings, "admin", { actor: { id: "7" }, permissions: ["workspace.settings.view"] }), {
|
|
87
|
+
code: "assistant.runtime.settings.updateForbidden"
|
|
88
|
+
});
|
|
89
|
+
assert.equal((await settings.getSettings({ targetSurfaceId: "admin" }, {
|
|
90
|
+
context: { actor: { id: "7" }, permissions: ["workspace.settings.view"] }
|
|
91
|
+
})).settings.systemPrompt, "");
|
|
92
|
+
assert.equal((await update(settings, "admin", { actor: { id: "1" }, permissions: ["workspace.settings.update"] })).workspaceId, "3");
|
|
93
|
+
|
|
94
|
+
await assert.rejects(update(settings, "ops", employee), { code: "console.ownerRequired" });
|
|
95
|
+
assert.equal((await update(settings, "ops", { actor: { id: "1" }, permissions: [] })).settings.systemPrompt, "Set by 1");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a malformed settingsUpdatePermission stops the settings service from starting", () => {
|
|
99
|
+
for (const settingsUpdatePermission of [false, "", { require: "all" }, { require: "none" }, ["app.admin"]]) {
|
|
100
|
+
assert.throws(() => createSettings({ home: { settingsUpdatePermission } }), {
|
|
101
|
+
message: /assistantServer\.home\.settingsUpdatePermission must be a permission/u
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
@@ -302,9 +302,12 @@ test("a reply that only announces work carries on once, and the state note gives
|
|
|
302
302
|
const system = harness.completionRequests[0].messages[0];
|
|
303
303
|
assert.equal(system.role, "system");
|
|
304
304
|
assert.match(system.content, /use the current time in the system's state note/u);
|
|
305
|
-
assert.
|
|
306
|
-
|
|
307
|
-
assert.
|
|
305
|
+
assert.doesNotMatch(system.content, /Current state, from the system/u, "The state note is not in the system prompt");
|
|
306
|
+
const note = harness.completionRequests[0].messages.at(-1);
|
|
307
|
+
assert.equal(note.role, "user");
|
|
308
|
+
assert.match(messageText(note), /^Current state, from the system\. It is the truth; earlier messages may be out of date:\n- The time: .+ UTC\./u,
|
|
309
|
+
"The state note after the conversation tells the model the current time");
|
|
310
|
+
assert.match(messageText(harness.completionRequests[1].messages.at(-2)), /^\(Check from the app: your reply ended by saying what you'd do next/u,
|
|
308
311
|
"The reply that only announced work carries on once with the engine's check");
|
|
309
312
|
assert.deepEqual(assistantMessages(harness.streamEvents), ["It is Tuesday in the workspace timezone."]);
|
|
310
313
|
assert.ok(harness.streamEvents.some((event) => event.method === "sendAssistantDelta"));
|
|
@@ -416,7 +419,7 @@ test("tool calls written as text never run, never become answer deltas, and the
|
|
|
416
419
|
const harness = createHarness([response(), textCompletion("The search is complete.")]);
|
|
417
420
|
await harness.run();
|
|
418
421
|
assert.deepEqual(harness.executedTools, [], "A tool call written as text is not run");
|
|
419
|
-
assert.match(messageText(harness.completionRequests[1].messages.at(-
|
|
422
|
+
assert.match(messageText(harness.completionRequests[1].messages.at(-2)), /^\(Check from the app: your last message contained raw markup/u);
|
|
420
423
|
assert.doesNotMatch(JSON.stringify(harness.streamEvents), /DSML|private reasoning|private arguments/u);
|
|
421
424
|
assert.deepEqual(assistantMessages(harness.streamEvents), ["The search is complete."]);
|
|
422
425
|
});
|
|
@@ -435,7 +438,7 @@ test("application-authorized attachments reach the model and are authorized agai
|
|
|
435
438
|
await harness.run("Read this", { attachmentIds: ["file-one"] });
|
|
436
439
|
assert.equal(calls.length, 1, "The new message's file is authorized before admission");
|
|
437
440
|
assert.deepEqual(harness.transcriptMessages[0].metadata.attachments, [receipt]);
|
|
438
|
-
assert.deepEqual(harness.completionRequests[0].messages.at(-
|
|
441
|
+
assert.deepEqual(harness.completionRequests[0].messages.at(-2).content, [
|
|
439
442
|
{ type: "text", text: "Read this" }, { type: "text", text: "Authorized file bytes" }
|
|
440
443
|
]);
|
|
441
444
|
await harness.run("What do they say?", { messageId: "message_2" });
|