Skip to content

Commit c914e89

Browse files
committed
web: a folder cannot configure the editor
Workspace Trust is off in a tab, so a setting without a scope was read from the folder's .vscode/settings.json as if the user wrote it. Measured in the real workbench: levelcode.cloud.endpoint, levelcode.ai.baseURL, agent.autopilot, verify.command and commandTimeout all came back as the folder's values, and the session token goes to whatever https host the endpoint names. The browser manifest now makes every setting application-scoped except a short, double-entered list of display preferences. The desktop manifest is untouched.
1 parent b10b6c9 commit c914e89

3 files changed

Lines changed: 62 additions & 1 deletion

File tree

‎docs/WEB.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,13 @@ causing the effect). Use another registrable domain.
274274
- **Keys of the user's own** (BYOK) are stored the same way and used from the tab to the provider the
275275
user chose; no LevelCode call is involved (the end-to-end check asserts it). The provider must
276276
accept requests from a web page; Anthropic's is called with its browser-access header.
277+
- **A folder does not configure the editor.** Workspace Trust is off in a tab, so a setting with no scope is read
278+
from the folder's `.vscode/settings.json` as if the user had written it. That would let a folder name the account
279+
host the session token is sent to (`levelcode.cloud.endpoint`), the provider's base URL, Autopilot, or the budgets.
280+
The browser manifest makes every `levelcode.*` setting `application`-scoped except an explicit list of display
281+
preferences (`WORKSPACE_SETTINGS_OK` in `web/ai-extension/manifest.js`, double-entered in `manifest.test.js`).
282+
The desktop manifest is untouched. Measured in the real workbench 2026-10-11 (unscoped: all three came back as the
283+
folder's values; `application`-scoped: the defaults).
277284
- **The scratch workspace and the extension's private state are on the extension host's origin**, in
278285
IndexedDB. With the host isolated they are not readable from the editor's page, by design.
279286

‎web/ai-extension/manifest.js‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,35 @@ const REMOVED_SETTINGS = new Set([
2929
'levelcode.ai.ollama.model',
3030
]);
3131

32+
/**
33+
* The settings a FOLDER may set. Everything else is application-scoped in the browser edition.
34+
*
35+
* The workbench in a tab runs with Workspace Trust off (web/lib/config.mjs), and a setting with no scope is read from the
36+
* workspace's .vscode/settings.json when there is one. A folder opened from the computer, or a project the agent was
37+
* asked to read, could then set the account host (levelcode.cloud.endpoint: the session token goes to whatever https
38+
* host that names), the provider's base URL, Autopilot, the step and token budgets, the verify command. Measured in
39+
* the real workbench by the shell review (2026-10-11). 'application' scope makes the workbench ignore the workspace's
40+
* value, so only the user's own settings count.
41+
*
42+
* Default-deny: a setting added to the desktop manifest later is application-scoped here until someone puts it on
43+
* this list, and web/test/unit/manifest.test.js fails if one is on it that does not exist.
44+
*
45+
* Only things that change what is shown, or which of the open files are offered as context, are listed.
46+
*/
47+
const WORKSPACE_SETTINGS_OK = new Set([
48+
'levelcode.ai.includeActiveFile',
49+
'levelcode.ai.chat.autoContext',
50+
'levelcode.ai.chat.autoContextMaxFiles',
51+
'levelcode.ai.chat.scopeToActiveProject',
52+
'levelcode.ai.chat.workspaceMap',
53+
'levelcode.ai.chat.startLocation',
54+
'levelcode.ai.chat.fontSize',
55+
'levelcode.ai.chat.maxImagesPerMessage',
56+
'levelcode.ai.chat.proseWidth',
57+
'levelcode.ai.chat.groupActivity',
58+
'levelcode.ai.diagrams.enabled',
59+
]);
60+
3261
const WELCOME = {
3362
id: 'welcome',
3463
title: 'Welcome to LevelCode',
@@ -80,6 +109,9 @@ function toWebManifest(pkg, opts) {
80109
const sections = Array.isArray(c.configuration) ? c.configuration : [c.configuration];
81110
for (const s of sections) {
82111
for (const k of Object.keys(s.properties || {})) { if (REMOVED_SETTINGS.has(k)) { delete s.properties[k]; } }
112+
for (const [k, def] of Object.entries(s.properties || {})) {
113+
if (!WORKSPACE_SETTINGS_OK.has(k) && def && typeof def === 'object' && !def.scope) { def.scope = 'application'; }
114+
}
83115
}
84116
}
85117
c.walkthroughs = [JSON.parse(JSON.stringify(WELCOME))];
@@ -89,4 +121,4 @@ function toWebManifest(pkg, opts) {
89121
return out;
90122
}
91123

92-
module.exports = { toWebManifest, REMOVED_COMMANDS, REMOVED_SETTINGS, WELCOME };
124+
module.exports = { toWebManifest, REMOVED_COMMANDS, REMOVED_SETTINGS, WORKSPACE_SETTINGS_OK, WELCOME };

‎web/test/unit/manifest.test.js‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,28 @@ test('settings that need a shell, a disk or a local model server are not offered
8585
assert.ok('levelcode.cloud.endpoint' in props && 'levelcode.ai.providerMode' in props, 'the settings the sign-in depends on are still here');
8686
});
8787

88+
test('no setting a folder can set decides where the session goes, what runs, or how much is spent', () => {
89+
// Workspace Trust is off in the tab, so a folder's .vscode/settings.json is read as if the user wrote it, unless the
90+
// setting is application-scoped. Default-deny: everything is, except an explicit list of display preferences.
91+
const props = Object.assign({}, ...(Array.isArray(m.contributes.configuration) ? m.contributes.configuration : [m.contributes.configuration]).map((s) => s.properties));
92+
const open = Object.keys(props).filter((k) => !props[k].scope).sort();
93+
assert.deepStrictEqual(open, [...web.WORKSPACE_SETTINGS_OK].sort(), 'a setting a folder can set that is not on the list: ' + open.filter((k) => !web.WORKSPACE_SETTINGS_OK.has(k)).join(', '));
94+
for (const k of web.WORKSPACE_SETTINGS_OK) { assert.ok(k in props, k + ' is on the workspace list but is not a browser setting: stale entry'); }
95+
// The list is written out here as well on purpose: opening a setting to folders is a decision, made in two places.
96+
assert.deepStrictEqual([...web.WORKSPACE_SETTINGS_OK].sort(), [
97+
'levelcode.ai.chat.autoContext', 'levelcode.ai.chat.autoContextMaxFiles', 'levelcode.ai.chat.fontSize', 'levelcode.ai.chat.groupActivity',
98+
'levelcode.ai.chat.maxImagesPerMessage', 'levelcode.ai.chat.proseWidth', 'levelcode.ai.chat.scopeToActiveProject', 'levelcode.ai.chat.startLocation',
99+
'levelcode.ai.chat.workspaceMap', 'levelcode.ai.diagrams.enabled', 'levelcode.ai.includeActiveFile',
100+
]);
101+
for (const k of ['levelcode.cloud.endpoint', 'levelcode.cloud.apiUrl', 'levelcode.ai.baseURL', 'levelcode.ai.provider', 'levelcode.ai.providerMode',
102+
'levelcode.ai.agent.autopilot', 'levelcode.ai.agent.maxSteps', 'levelcode.ai.agent.maxTokens', 'levelcode.ai.skills.enabled']) {
103+
assert.strictEqual(props[k].scope, 'application', k);
104+
}
105+
// the desktop manifest is not touched by any of it
106+
const dprops = Object.assign({}, ...(Array.isArray(desktop.contributes.configuration) ? desktop.contributes.configuration : [desktop.contributes.configuration]).map((s) => s.properties));
107+
assert.strictEqual(dprops['levelcode.cloud.endpoint'].scope, undefined);
108+
});
109+
88110
test('the browser binds a key the browser lets through', () => {
89111
assert.ok(m.contributes.keybindings.some((k) => k.command === 'levelcode.ai.focus' && k.key === 'ctrl+alt+i' && /isWeb/.test(k.when)));
90112
});

0 commit comments

Comments
 (0)