Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions packages/workspace/src/browser/style/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/********************************************************************************
* Copyright (C) 2026 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/

/* Workspace Trust Dialog Styles */

.workspace-trust-content {
display: flex;
flex-direction: column;
gap: calc(var(--theia-ui-padding) * 3);
padding: calc(var(--theia-ui-padding) * 2);
}

.workspace-trust-header {
display: flex;
align-items: center;
gap: calc(var(--theia-ui-padding) * 2);
}

.workspace-trust-header i {
font-size: calc(var(--theia-ui-font-size3) * 2.5) !important;
color: var(--theia-button-background);
}

.workspace-trust-title {
font-size: var(--theia-ui-font-size2);
font-weight: 600;
line-height: var(--theia-content-line-height);
}

.workspace-trust-description,
.workspace-trust-folder {
margin-left: calc(var(--theia-ui-font-size3) * 2.5 + var(--theia-ui-padding) * 2);
}

.workspace-trust-dialog .dialogControl {
margin-left: calc(var(--theia-ui-font-size3) * 2.5 + var(--theia-ui-padding) * 4);
padding-bottom: calc(var(--theia-ui-padding) * 4) !important;
justify-content: flex-start !important;
}

.workspace-trust-description {
color: var(--theia-descriptionForeground);
line-height: var(--theia-content-line-height);
}

.workspace-trust-folder {
font-family: var(--theia-code-font-family);
font-size: var(--theia-code-font-size);
color: var(--theia-foreground);
background-color: var(--theia-editor-background);
padding: var(--theia-ui-padding) calc(var(--theia-ui-padding) * 1.5);
border-radius: 4px;
}

.workspace-trust-dialog .dialogControl .theia-button.secondary {
margin-left: 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,8 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
this.workspaceTrustService.setWorkspaceTrust(newTrust);
if (newTrust) {
await this.workspaceTrustService.addToTrustedFolders();
} else {
await this.workspaceTrustService.removeFromTrustedFolders();
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/workspace/src/browser/workspace-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import '../../src/browser/style/index.css';

import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { CommandContribution, MenuContribution, bindContributionProvider } from '@theia/core/lib/common';
import { WebSocketConnectionProvider, FrontendApplicationContribution, KeybindingContribution } from '@theia/core/lib/browser';
Expand Down
75 changes: 75 additions & 0 deletions packages/workspace/src/browser/workspace-trust-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { nls } from '@theia/core';
import { codicon } from '@theia/core/lib/browser';
import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
import * as React from '@theia/core/shared/react';

export class WorkspaceTrustDialog extends ReactDialog<boolean> {
protected confirmed = true;

constructor(protected readonly folderPath: string) {
super({
title: '',
maxWidth: 500
});

this.node.classList.add('workspace-trust-dialog');

this.appendCloseButton(nls.localizeByDefault("No, I don't trust the authors"));
this.appendAcceptButton(nls.localizeByDefault('Yes, I trust the authors'));
this.controlPanel.removeChild(this.errorMessageNode);
}

get value(): boolean {
return this.confirmed;
}

protected override handleEscape(): boolean | void {
this.confirmed = false;
this.accept();
}

override close(): void {
this.confirmed = false;
this.accept();
}

protected render(): React.ReactNode {
return (
<div className="workspace-trust-content">
<div className="workspace-trust-header">
<i className={codicon('shield')}></i>
<div className="workspace-trust-title">
{nls.localizeByDefault('Do you trust the authors of the files in this folder?')}
</div>
</div>
<div className="workspace-trust-description">
{nls.localize(
'theia/workspace/trustDialogMessage',
`The workspace trust feature is not yet fully supported in Theia.

If you trust the authors of this folder, code inside may be executed. Only trust folders that you trust the contents of.`
)}
Comment on lines +61 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure exactly what would be best for this message. The current state of the feature, as I understand it, is basically that its efficacy is restricted to (some parts of) the plugin system. That means that if all plugins behave nicely and check workspace trust before doing things that could be dangerous, it actually works as intended. But if some plugin automatically runs e.g. some compilation task with a malicious definition without first checking trust, we wouldn't stop it, and I think VSCode would.

So the sense we want to communicate is that the answer to the question is not irrelevant, but saying 'no' may not have all the effects that the user would expect.

But maybe we should just hook up workspace trust in the task and debug systems to stop those systems from running in untrusted workspaces, and then I'll stop hemming and hawing :-).

</div>
{this.folderPath && (
<div className="workspace-trust-folder">{this.folderPath}</div>
)}
</div>
);
}
}
43 changes: 33 additions & 10 deletions packages/workspace/src/browser/workspace-trust-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/front
import { WorkspaceService } from './workspace-service';
import { WorkspaceCommands } from './workspace-commands';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { WorkspaceTrustDialog } from './workspace-trust-dialog';

const STORAGE_TRUSTED = 'trusted';
export const WORKSPACE_TRUST_STATUS_BAR_ID = 'workspace-trust-status';
Expand Down Expand Up @@ -110,6 +111,10 @@ export class WorkspaceTrustService {
}

getWorkspaceTrust(): Promise<boolean> {
// Return current trust if already resolved, otherwise wait for initial resolution
if (this.currentTrust !== undefined) {
return Promise.resolve(this.currentTrust);
}
return this.workspaceTrust.promise;
}

Expand Down Expand Up @@ -183,18 +188,9 @@ export class WorkspaceTrustService {

this.pendingTrustDialog = new Deferred<boolean>();
try {
const trust = nls.localizeByDefault('Yes, I trust the authors');
const dontTrust = nls.localizeByDefault("No, I don't trust the authors");
const folderPath = this.workspaceService.workspace?.resource?.path?.toString() ?? '';

const dialog = new ConfirmDialog({
title: nls.localizeByDefault('Do you trust the authors of the files in this folder?'),
msg: nls.localize('theia/workspace/trustDialogMessage',
'If you trust the authors of this folder, code inside may be executed. Only trust folders that you trust the contents of.') +
(folderPath ? `\n\n"${folderPath}"` : ''),
ok: trust,
cancel: dontTrust,
});
const dialog = new WorkspaceTrustDialog(folderPath);

const result = await dialog.open();
const trusted = result === true;
Expand Down Expand Up @@ -223,6 +219,31 @@ export class WorkspaceTrustService {
}
}

async removeFromTrustedFolders(): Promise<void> {
const workspaceUri = this.workspaceService.workspace?.resource;
if (!workspaceUri) {
return;
}
if (this.isWorkspaceInTrustedFolders()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a problem with this method:

    protected isWorkspaceInTrustedFolders(): boolean {
        const workspaceUri = this.workspaceService.workspace?.resource; // <--- Could refer to workspace file.
        if (!workspaceUri) {
            return false;
        }
        const trustedFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
        const caseSensitive = !OS.backend.isWindows;
        return trustedFolders.some(folder => {
            try {
                const folderUri = new URI(folder).normalizePath();
                return workspaceUri.normalizePath().isEqual(folderUri, caseSensitive); // <----- Workspace file would never be equal to any folder.
            } catch {
                return false; // Invalid URI in preferences
            }
        });
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That can cause oddities like this:

image

The file shouldn't appear there - workspace trust should be a function of all of the folders in the workspace and whether they're trusted or not.

Copy link
Contributor

@colin-grant-work colin-grant-work Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also end up with workspace files in the preferences: only folders should go there.

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As disussed offline, I created a follow up for this part: #16887

const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
const caseSensitive = !OS.backend.isWindows;
const normalizedWorkspaceUri = workspaceUri.normalizePath();
const updatedFolders = currentFolders.filter(folder => {
try {
const folderUri = new URI(folder).normalizePath();
return !normalizedWorkspaceUri.isEqual(folderUri, caseSensitive);
} catch {
return true; // Keep invalid URIs
}
});
await this.preferences.set(
WORKSPACE_TRUST_TRUSTED_FOLDERS,
updatedFolders,
PreferenceScope.User
);
}
}

protected isWorkspaceInTrustedFolders(): boolean {
const workspaceUri = this.workspaceService.workspace?.resource;
if (!workspaceUri) {
Expand Down Expand Up @@ -326,6 +347,8 @@ export class WorkspaceTrustService {
this.statusBar.setElement(WORKSPACE_TRUST_STATUS_BAR_ID, {
text: '$(shield) ' + nls.localizeByDefault('Restricted Mode'),
alignment: StatusBarAlignment.LEFT,
backgroundColor: 'var(--theia-statusBarItem-prominentBackground)',
color: 'var(--theia-statusBarItem-prominentForeground)',
priority: 5000,
tooltip: nls.localize('theia/workspace/restrictedModeTooltip',
'Running in Restricted Mode. Some features are disabled because this folder is not trusted. Click to manage trust settings.'),
Expand Down
Loading