Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d361e34
Documentation MCP (#7805)
labkey-matthewb Jul 1, 2026
56326d1
canEditSourceModule() consistency (#7727)
labkey-matthewb Jul 2, 2026
8341e1f
GitHub Issue 1117: sample type and dataclass foreign keys (#7804)
XingY Jul 3, 2026
648c4e0
1238: Optional feature flag for AI in products (#7811)
labkey-nicka Jul 3, 2026
630ce92
Upgrade ActiveMQ to 5.19.8 (#7812)
labkey-martyp Jul 4, 2026
1f784cc
CASE THEN/ELSE {value expression} (#7810)
labkey-matthewb Jul 7, 2026
727a3e6
Merge 26.3 to 26.7
labkey-teamcity Jul 7, 2026
03b7256
SQL Server Migration - update LabKey SQL helper for PostgreSQL compat…
labkey-bpatel Jul 7, 2026
22d0e1a
ModuleEditorService.getModuleRoot() (#7816)
labkey-matthewb Jul 7, 2026
e7a902e
Role-restricted API keys (#7807)
labkey-adam Jul 7, 2026
685da1b
Fix TypeError when toggling edit mode before Data Views grid loads (#…
labkey-tchad Jul 7, 2026
7d8a2af
Merge 26.3 to 26.7
cnathe Jul 7, 2026
b53858a
Provide several more terms-of-use re-acceptance options (#7821)
labkey-adam Jul 8, 2026
531c864
sql-execute refactor (#7806)
labkey-matthewb Jul 8, 2026
b72fe76
Update @labkey/components for API key display styling change (#7828)
labkey-adam Jul 8, 2026
814cdb0
Additional auditing for site settings (#7814)
labkey-klum Jul 8, 2026
d7180a4
Update ActiveMQ to 5.19.8 for OWASP CVE remediation (#7829)
labkey-willm Jul 9, 2026
cfa8cf7
1223: Warn when web and database server time differ (#7815)
labkey-nicka Jul 9, 2026
a7a27ed
Skip batch trigger if we have prior errors. (#7830)
labkey-klum Jul 9, 2026
445dc4d
Eliminate X-Frame-Options setting (#7831)
labkey-adam Jul 9, 2026
7042900
Make `Excel multi-tab` metric compatible with PG <= 15 (#7832)
labkey-jeckels Jul 10, 2026
272c907
AI Assistant Status admin page (#7827)
labkey-matthewb Jul 13, 2026
66553f6
registerPermission(EditModuleResourcesPermission) (#7838)
labkey-matthewb Jul 13, 2026
b53b286
GitHub Issue 1300: BulkUpgradeGroupAction scoping changes (#7839)
labkey-jeckels Jul 13, 2026
d837e55
Merge 26.3 to 26.7
cnathe Jul 14, 2026
380005f
GitHub Issue 1307: Simplify container filtering in GetTransactionRowI…
labkey-jeckels Jul 14, 2026
ea2c4e5
GitHub Issue 1282: add getSampleNames to reduce memory usage (#7840)
XingY Jul 14, 2026
39a457a
Make CPUTimer.update public. Used in perf tests. (#7845)
labkey-danield Jul 14, 2026
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
2 changes: 2 additions & 0 deletions api/src/org/labkey/api/ApiModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
import org.labkey.api.view.ViewServlet;
import org.labkey.api.view.WebPartFactory;
import org.labkey.api.webdav.WebdavResolverImpl;
import org.labkey.api.wiki.WikiRendererType;
import org.labkey.api.writer.ContainerUser;
import org.labkey.filters.ContentSecurityPolicyFilter;

Expand Down Expand Up @@ -562,6 +563,7 @@ public void registerServlets(ServletContext servletCtx)
UserManager.TestCase.class,
ViewCategoryManager.TestCase.class,
WebdavResolverImpl.TestCase.class,
WikiRendererType.TestCase.class,
WorkbookContainerType.TestCase.class,
WriteableLookAndFeelProperties.TestCase.class
);
Expand Down
6 changes: 0 additions & 6 deletions api/src/org/labkey/api/admin/AdminBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import org.labkey.api.data.ContainerManager;
import org.labkey.api.data.CoreSchema;
import org.labkey.api.data.DbScope;
import org.labkey.api.module.Module;
import org.labkey.api.module.ModuleLoader;
import org.labkey.api.security.UserManager;
import org.labkey.api.settings.AppProps;
Expand All @@ -43,7 +42,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
Expand Down Expand Up @@ -82,7 +80,6 @@ public static class RecentUser
public static final String sessionTimeout = Formats.commaf0.format(ModuleLoader.getServletContext().getSessionTimeout());
public static final String buildTime = ModuleLoader.getInstance().getCoreModule().getBuildTime();
public static final String serverStartupTime = DateUtil.formatDateTime(ContainerManager.getRoot());
public static final List<Module> modules;

public static String asserts = "disabled";

Expand Down Expand Up @@ -116,9 +113,6 @@ public static class RecentUser

//noinspection ConstantConditions,AssertWithSideEffects
assert null != (asserts = "enabled");

modules = new ArrayList<>(ModuleLoader.getInstance().getModules());
modules.sort(Comparator.comparing(Module::getName, String.CASE_INSENSITIVE_ORDER));
}

private static @Nullable String getValue(Field field)
Expand Down
6 changes: 6 additions & 0 deletions api/src/org/labkey/api/data/DataColumn.java
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ protected ColumnInfo getDisplayField(@NotNull ColumnInfo col, boolean withLookup
return null==display ? col : display;
}

@Override
public void setWithLookup(boolean withLookup)
{
_displayColumn = withLookup ? getDisplayField(_boundColumn, true) : _boundColumn;
}

@Override
public String toString()
{
Expand Down
5 changes: 5 additions & 0 deletions api/src/org/labkey/api/data/DisplayColumn.java
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,11 @@ public void setRequiresHtmlFiltering(boolean requiresHtmlFiltering)
_requiresHtmlFiltering = requiresHtmlFiltering;
}

public void setWithLookup(boolean withLookup)
{
// subclasses override as needed
}

public void setLinkTarget(String linkTarget)
{
_linkTarget = linkTarget;
Expand Down
13 changes: 13 additions & 0 deletions api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,19 @@ public boolean supportsNativeGreatestAndLeast()
return true;
}

@Override
public boolean supportsIsNumeric()
{
return true;
}

@Override
public SQLFragment isNumericExpr(SQLFragment expression)
{
return new SQLFragment("(CASE WHEN CAST((").append(expression)
.append(") AS TEXT) ~ '^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$' THEN 1 ELSE 0 END)");
}

private class PostgreSqlColumnMetaDataReader extends ColumnMetaDataReader
{
private final TableInfo _table;
Expand Down
103 changes: 99 additions & 4 deletions api/src/org/labkey/api/data/dialect/SqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import org.labkey.api.module.ModuleLoader;
import org.labkey.api.query.FieldKey;
import org.labkey.api.util.ExceptionUtil;
import org.labkey.api.util.HtmlString;
import org.labkey.api.util.MemTracker;
import org.labkey.api.util.StringUtilsLabKey;
import org.labkey.api.util.SystemMaintenance;
Expand All @@ -80,6 +81,8 @@
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
Expand All @@ -95,6 +98,7 @@
import java.util.regex.Pattern;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

Expand Down Expand Up @@ -168,7 +172,7 @@ public String getOtherDatabaseThreads()
{
StringBuilder sb = new StringBuilder();

// Per 18789, also include threads without db connections.
// Per Issue 18789, also include threads without db connections.
List<Thread> dbThreads = new ArrayList<>();

for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet())
Expand Down Expand Up @@ -868,6 +872,16 @@ public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... argument
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
}

public boolean supportsIsNumeric()
{
return false;
}

public SQLFragment isNumericExpr(SQLFragment expression)
{
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
}

public void handleCreateDatabaseException(SQLException e) throws ServletException
{
throw(new ServletException("Can't create database", e));
Expand Down Expand Up @@ -1980,12 +1994,61 @@ public final Collection<String> getExecutionPlan(DbScope scope, SQLFragment sql,
// Add any database configuration warnings (e.g., missing aggregate function or deprecated database server version)
// to display in the page header for administrators. This will be called:
// - Only on the LabKey DataSource's dialect instance (not external data sources)
// - After the core module has been upgraded and the dialect has been prepared for the last time, meaning the dialect
// - After the core module has been upgraded, and the dialect has been prepared for the last time, meaning the dialect
// should reflect the final database configuration
public void addAdminWarningMessages(Warnings warnings, boolean showAllWarnings)
{
}

public static final long TIME_DIFFERENCE_WARNING_SECONDS = 10;

public static ServerDatabaseTimeDifference getServerDatabaseTimeDifference(DbScope scope)
{
// Compare LocalDateTime to capture any difference in server and database times (skew or timezone).
LocalDateTime serverTime = LocalDateTime.now();
LocalDateTime databaseTime = new SqlSelector(scope, "SELECT CURRENT_TIMESTAMP").getObject(LocalDateTime.class);

return new ServerDatabaseTimeDifference(serverTime, databaseTime);
}

public record ServerDatabaseTimeDifference(LocalDateTime serverTime, LocalDateTime databaseTime)
{
public long getSeconds()
{
return Math.abs(Duration.between(serverTime, databaseTime).toSeconds());
}

public boolean exceedsWarningThreshold()
{
return getSeconds() > TIME_DIFFERENCE_WARNING_SECONDS;
}
}

// GH Issue #1223: Add a site configuration warning for administrators if the server and database clocks differ.
protected void addTimeDifferenceWarning(Warnings warnings, boolean showAllWarnings)
{
try
{
ServerDatabaseTimeDifference difference = getServerDatabaseTimeDifference(DbScope.getLabKeyScope());

if (difference.exceedsWarningThreshold())
warnings.add(getTimeDifferenceWarning(difference.getSeconds()));
else if (showAllWarnings)
warnings.add(getTimeDifferenceWarning(TIME_DIFFERENCE_WARNING_SECONDS + 1));
}
catch (Exception e)
{
LOG.warn("Unable to compare web server and database server times", e);
}
}

private HtmlString getTimeDifferenceWarning(long seconds)
{
return HtmlString.of("The web server and database server times differ by " + seconds + " seconds. " +
"LabKey Server often relies on comparing timestamps stored in the database with timestamps generated by " +
"the web server, so this difference can lead to data integrity issues. Synchronize the clocks on these servers.");
}

public abstract List<SQLFragment> getChangeStatements(TableChange change);

public abstract void purgeTempSchema(Map<String, TempTableTracker> createdTableNames);
Expand All @@ -2007,7 +2070,7 @@ protected void trackTempTables(Map<String, TempTableTracker> createdTableNames)
// Defragment an index, if necessary
public void defragmentIndex(DbSchema schema, String tableSelectName, String indexName)
{
// By default do nothing
// By default, do nothing
}

public boolean isTableExists(DbScope scope, String schema, String name)
Expand Down Expand Up @@ -2035,7 +2098,7 @@ public boolean hasTriggers(DbSchema scope, String schema, String tableName)

/**
*
* @return true If the dialect is one supported for the backend LabKey database. ie, Postgres or SQL Server
* @return true If the dialect is one supported for the backend LabKey database. i.e., Postgres or SQL Server
*/
public boolean isLabKeyDbDialect()
{
Expand Down Expand Up @@ -2409,5 +2472,37 @@ public void testProcedureIdentifierQuoting()
// A dotted name is rejected: schema and procedure are separate parameters, so a '.' in a single component is an ambiguous schema-qualified reference
assertThrows(IllegalArgumentException.class, () -> dialect.buildProcedureCall(core.getName(), "a.b", 0, false, false, scope));
}

// GH Issue #1223: Verify the server/database clock-skew arithmetic and warning threshold
@Test
public void testServerDatabaseTimeDifference()
{
LocalDateTime base = LocalDateTime.of(2026, 7, 6, 12, 0, 0);

// Identical times: zero difference, no warning
ServerDatabaseTimeDifference equal = new ServerDatabaseTimeDifference(base, base);
assertEquals(0, equal.getSeconds());
assertFalse(equal.exceedsWarningThreshold());

// Comfortably below the threshold: no warning
ServerDatabaseTimeDifference below = new ServerDatabaseTimeDifference(base, base.plusSeconds(5));
assertEquals(5, below.getSeconds());
assertFalse(below.exceedsWarningThreshold());

// Exactly at the threshold: no warning (comparison is strictly greater-than)
ServerDatabaseTimeDifference atThreshold = new ServerDatabaseTimeDifference(base, base.plusSeconds(TIME_DIFFERENCE_WARNING_SECONDS));
assertEquals(TIME_DIFFERENCE_WARNING_SECONDS, atThreshold.getSeconds());
assertFalse(atThreshold.exceedsWarningThreshold());

// Just past the threshold: warning
ServerDatabaseTimeDifference over = new ServerDatabaseTimeDifference(base, base.plusSeconds(TIME_DIFFERENCE_WARNING_SECONDS + 1));
assertEquals(TIME_DIFFERENCE_WARNING_SECONDS + 1, over.getSeconds());
assertTrue(over.exceedsWarningThreshold());

// Difference is absolute: database clock behind the web server is treated the same as ahead
ServerDatabaseTimeDifference behind = new ServerDatabaseTimeDifference(base, base.minusSeconds(TIME_DIFFERENCE_WARNING_SECONDS + 1));
assertEquals(TIME_DIFFERENCE_WARNING_SECONDS + 1, behind.getSeconds());
assertTrue(behind.exceedsWarningThreshold());
}
}
}
9 changes: 9 additions & 0 deletions api/src/org/labkey/api/mcp/AbstractAgentAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,18 @@ protected String handleEscape(String prompt)
@Override
public void validateForm(F form, Errors errors)
{
if (!McpService.get().isAIFeaturesEnabled())
{
errors.reject(ERROR_GENERIC, "Agent actions are not available.");
return;
}

String prompt = form.getPrompt();
if (prompt != null && prompt.length() > MAX_PROMPT_CHAR_LENGTH)
{
errors.rejectValue("prompt", ERROR_GENERIC, "Prompt cannot exceed " + MAX_PROMPT_CHAR_LENGTH + " characters.");
return;
}

// Only honor a client-supplied conversationId if this agent previously issued this session that
// id. Otherwise, generate a fresh one. This prevents a same-session caller from splicing into
Expand Down
44 changes: 41 additions & 3 deletions api/src/org/labkey/api/mcp/McpService.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.labkey.api.settings.OptionalFeatureService;
import org.labkey.api.util.HtmlString;
import org.labkey.api.util.logging.LogHelper;
import org.labkey.api.view.HtmlView;
import org.labkey.api.view.HttpView;
import org.labkey.api.writer.ContainerUser;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ToolContext;
Expand All @@ -36,6 +38,7 @@

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

///
Expand Down Expand Up @@ -94,8 +97,11 @@
///
public interface McpService extends ToolCallbackProvider
{
String VECTOR_SCHEMA = "vector_indexes"; // for pgvector extension, see core-26.005-26.006.sql

Logger LOG = LogHelper.getLogger(McpService.class, "MCP registration exceptions");
String ENABLE_MCP_SERVER_FLAG = "enableMcpServer";
String ENABLE_AI_FEATURES = "enableAIFeatures";

// Interface for MCP classes that we will "ingest" using Spring annotations. Provides a few helper methods.
interface McpImpl
Expand All @@ -117,9 +123,6 @@ default User getUser(ToolContext toolContext)
// Every MCP resource should call this on every invocation
default void incrementResourceRequestCount(String resource)
{
if (!get().isEnabled())
throw new RuntimeException("The MCP server is not enabled for external requests. Consider toggling the optional feature flag.");

get().incrementResourceRequestCount(resource);
}
}
Expand All @@ -142,8 +145,19 @@ default boolean isEnabled()
return OptionalFeatureService.get().isFeatureEnabled(ENABLE_MCP_SERVER_FLAG);
}

default boolean isAIFeaturesEnabled()
{
return OptionalFeatureService.get().isFeatureEnabled(ENABLE_AI_FEATURES);
}

boolean isReady();

// Convenience for in-product AI features: the AI feature flag is on AND the service has started.
default boolean isAIFeaturesReady()
{
return isAIFeaturesEnabled() && isReady();
}

// Register MCPs in Module.startup()
default void register(McpImpl mcp)
{
Expand Down Expand Up @@ -187,6 +201,8 @@ default ChatClient getChat(HttpSession session, String conversationName, Supplie

record MessageResponse(String contentType, String text, HtmlString html) {}

record VectorDocument(String id, String text, Map<String, Object> metadata) {}

/** get a consolidated response (good for many text-oriented agents/use-cases) */
MessageResponse sendMessage(ChatClient chat, String message);

Expand All @@ -201,4 +217,26 @@ default List<MessageResponse> sendMessageEx(ChatClient chat, String message)
* CONSIDER: Is it possible to implement VectorStoreRetriever wrapper for SearchService???
*/
VectorStore getVectorStore();

/** Returns true if the vector store exists and contains at least one document. */
boolean isVectorStorePopulated(@NotNull VectorStore vs);

/**
* Adds documents to the vector store, automatically splitting any document whose token
* count exceeds the embedding model's input limit. Prefer this over
* {@code getVectorStore().add(...)} for indexing — it prevents the
* {@code IllegalArgumentException} that {@code TokenCountBatchingStrategy} throws on
* oversized inputs.
*/
void addDocuments(List<VectorDocument> documents);

void saveVectorStore();

/** Drop and recreate the vector store table. Use when the embedding model has changed and dimensions no longer match. */
void resetVectorStore();

default HttpView<Object> getAssistantStatusView()
{
return new HtmlView(HtmlString.of("AI Assistant features are not available."));
}
}
Loading
Loading