Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50845aa99b
|
No files matched your search
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased - render_widget follows the assistant's data access; no widget is rendered until a host allows it
|
||||
|
||||
- **A widget is rendered only when the host allowed it by name.** `render_widget` asked the application to render any widget the person may see. A widget is code of the application and can show rows of any table, so it could show a model the rows of a table the assistant may not read (data access `NONE`, a table kept from AI by its metadata). The tool now applies the assistant's own rules before the application is asked: a widget that is not in `AssistantQBitConfig.withAllowedWidgetNames` is refused (the list is empty by default: no widget is renderable), and so is one in `withHiddenWidgetNames`. The README already said the list governs the tool; since the tool moved from the core tool kernel to the UI API it did not.
|
||||
- **A host says which tables a widget reads, and data access then decides.** New: `AssistantQBitConfig.withWidgetTables(widgetName, tableNames)`. An allowed widget with tables is rendered only while the assistant may read every one of them, so an administrator's `NONE` row, a pin in code and a table kept from AI each close the widget too. The refusal is one answer for every reason and does not say whether the widget exists.
|
||||
- The tool is still offered as before (its definition and the cached request prefix do not change); a call for a widget that is not allowed is a DENIED result with a log row. Not offering the tool at all while no widget is allowed is left for later: it changes the tool set of every request.
|
||||
- Tests: `RenderWidgetAccessTest` (the default is closed and the application is not asked; allowed; hidden wins; a widget whose table is closed by a pin, read-only, or missing).
|
||||
|
||||
## Unreleased - The assistant runs a process the signed-in person may run; four review findings closed
|
||||
|
||||
Builds package A4 of the actions design (`docs/actions-and-mcp.md`, section 4), with four findings of an independent review, each described by the guarantee it gives. **One behaviour change needs a host's attention before deploying: a table's own bulk edit and bulk delete become runnable through the assistant, and any other process once the host lists it (see `UPGRADE.md`).**
|
||||
|
||||
@@ -78,6 +78,7 @@ Always use the wrapper: Maven 3.10 cannot read the parent POM. The committed def
|
||||
- A missing policy refuses; it never means "every tool".
|
||||
- Data tools run only through `UiApiToolbox` → `UiApiGateway` (spec'd `/qqq/v1` operations with the user's `sessionUUID`); never core data actions for user data. Only `navigate` and `search_help` use `AiToolKernel.execute`.
|
||||
- Data access (`DataAccessService`) and the policy only ever narrow what the UI API allows. The mode of a table or process is declared in versioned code: the default (`withDefaultTableAccess` / `withDefaultProcessAccess`, READ_WRITE) or a pin (`withTableAccess` / `withProcessAccess`). An administrator's `assistant_data_access` row narrows it and never raises it. A table whose AI exposure is NONE stays NONE, and so does every `assistant_*` table and every process of the QBit (`DataAccessService.isOwnTable`: a pin in code that no row and no host pin lifts). Rows that cannot be read and were never read mean NONE for everything, never the default.
|
||||
- `render_widget` renders only a widget the host allowed by name in code (`withAllowedWidgetNames`; none by default), not hidden, and only while the assistant may read every table the host said it reads (`withWidgetTables`): a widget is code that can show rows of any table. The check is in `UiApiToolbox.renderWidget`, before the UI API is asked. Never render a widget on the person's permission alone.
|
||||
- READ_WRITE grants nothing: the write tools are offered to a person only when the UI API says that person may create, edit or delete records of some table (`UiApiToolFamily.writableTables`, asked as the person, once per offer, within `toolCallTimeoutSeconds`; no answer means no write tool). The person's permission for the change itself is decided by the UI API when the confirmed change runs, with its own message. Never materialise defaults as rows again: the retired "Add rows for every table" action is why `assistant-data-access-follow-user.changelog.sql` exists.
|
||||
- A write runs only after the owner confirms its pending action, with the confirming request's session, once; no policy or caller can turn confirmation off (`assistant_policy.writesRequireConfirmation` is no longer read). `ToolCallService.confirm` loads the stored action for that person, claims it, runs it and writes its row; whatever fails after the claim ends in FAILED with a row, never in RUNNING.
|
||||
- A pending action is decided once: confirming, cancelling and expiring all go through `AssistantStore.decidePendingAction` (final; `expirePendingAction` is a claim with the decision EXPIRED), which inserts the action's row into `assistant_pending_action_claim` (unique `pendingActionId`). Never decide an action with a status update alone, never expire one from a copy read earlier or a time a caller names (`ToolCallService.expiredIfDue` decides from the stored action and the runtime's clock), and never drop that unique key.
|
||||
|
||||
@@ -286,7 +286,8 @@ The nav lists the home and the main tables: **Providers**, **Conversations**, **
|
||||
| `retentionSchedulerName` | none | Runs `assistantRetention` daily on that scheduler. |
|
||||
| `ownerSecurityKeyType` | `assistant_user_id` | Key type of the owner record locks. |
|
||||
| `pricing` | Anthropic list prices, 2026-10 | USD per million tokens per model, used for cost and budgets. |
|
||||
| `allowedWidgetNames` | none | Dashboard widgets `render_widget` may render. The kernel denies every widget not on this list. This config list is the **only** way to allow a widget: QQQ widget meta-data has no supplemental hook, so there is no per-widget "assistant-safe" flag. Allow only widgets that render the same data for every user who can see them. |
|
||||
| `allowedWidgetNames` | none | Dashboard widgets `render_widget` may render. The tool refuses every widget that is not on this list, before the application is asked, so with the default (none) no widget is renderable. This list in code is the **only** way to allow a widget: QQQ widget meta-data has no supplemental hook, so there is no per-widget "assistant-safe" flag. A widget is code of the application and can show rows of any table, whatever the assistant's data access says of that table; allow only widgets that render the same data for every user who can see them, and say which tables each one reads with `withWidgetTables`. |
|
||||
| `withWidgetTables(widget, tables)` | none | The tables an allowed widget reads. The widget is rendered through the assistant only while the assistant may read every one of them (data access not `NONE`: the code default, a pin, an administrator's row, a table kept from AI by its metadata). A widget allowed without tables is rendered whatever the data access says. |
|
||||
| `hiddenWidgetNames` | none | Widgets that are never rendered, even when allowed. |
|
||||
| `customRedactionPatterns` | none | Extra regular expressions redacted from tool results and from the input kept in the tool-call log. |
|
||||
| `killSwitchPollMillis` | 2000 | How often a running turn re-checks the kill switch while it waits on the model. |
|
||||
|
||||
+16
@@ -1,5 +1,21 @@
|
||||
# Upgrading qbit-assistant
|
||||
|
||||
## render_widget renders only widgets the host allowed (Unreleased)
|
||||
|
||||
**After this upgrade the assistant renders no dashboard widget until the application names it in code.** Until now `render_widget` rendered any widget the signed-in person may see, including one that shows rows of a table the assistant may not read.
|
||||
|
||||
What a host changes, only if it wants the assistant to render widgets:
|
||||
|
||||
```java
|
||||
new AssistantQBitConfig()
|
||||
.withAllowedWidgetNames(Set.of("openOrdersByStatus")) // nothing else is renderable
|
||||
.withWidgetTables("openOrdersByStatus", Set.of("order", "customer")); // the tables its renderer reads
|
||||
```
|
||||
|
||||
- Name the tables each allowed widget reads. The widget is then closed whenever the assistant may not read one of them (a `NONE` row of an administrator, a pin, a table kept from AI). A widget allowed without tables is rendered whatever the data access says: do that only for a widget that shows nothing of a table.
|
||||
- Allow only widgets that render the same data for every person who can see them.
|
||||
- No schema change, no permission change, no settings row.
|
||||
|
||||
## The assistant runs a process the signed-in person may run (Unreleased, UI API extension assistant 1.10.0)
|
||||
|
||||
**After this upgrade, with nothing else changed, the assistant can run two kinds of process for a person who may run them themselves, once that person confirmed the run on a card: a table's own bulk edit and its bulk delete, on a table the assistant may already change for that person.** Every other process stays closed until the application's code names it on a reviewed list. No DDL changes.
|
||||
|
||||
+48
-1
@@ -159,6 +159,7 @@ public class AssistantQBitConfig implements QBitConfig
|
||||
|
||||
private Set<String> hiddenWidgetNames = new LinkedHashSet<>();
|
||||
private Set<String> allowedWidgetNames = new LinkedHashSet<>();
|
||||
private Map<String, Set<String>> widgetTables = new LinkedHashMap<>();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// tools sent with strict: true. Empty by default: Anthropic compiles all //
|
||||
@@ -1644,7 +1645,10 @@ public class AssistantQBitConfig implements QBitConfig
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for allowedWidgetNames
|
||||
** Fluent setter for allowedWidgetNames: the dashboard widgets render_widget
|
||||
** may render. A widget that is not named here is refused, whoever asks
|
||||
** (the list is empty by default, so no widget is renderable until a host
|
||||
** names one). Name the tables a widget reads with {@link #withWidgetTables}.
|
||||
*******************************************************************************/
|
||||
public AssistantQBitConfig withAllowedWidgetNames(Set<String> allowedWidgetNames)
|
||||
{
|
||||
@@ -1654,6 +1658,49 @@ public class AssistantQBitConfig implements QBitConfig
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for widgetTables
|
||||
*******************************************************************************/
|
||||
public Map<String, Set<String>> getWidgetTables()
|
||||
{
|
||||
return (this.widgetTables);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for widgetTables
|
||||
*******************************************************************************/
|
||||
public void setWidgetTables(Map<String, Set<String>> widgetTables)
|
||||
{
|
||||
this.widgetTables = widgetTables;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Declare the tables an allowed widget reads. The renderer of a widget is
|
||||
** code of the application: the assistant cannot see which tables it reads,
|
||||
** so the host says so here. The widget is then rendered through the
|
||||
** assistant only while the assistant may read every one of those tables
|
||||
** (their data access is not NONE: the code default, a pin, a row of an
|
||||
** administrator, a table kept from AI by its metadata, the tables of the
|
||||
** assistant itself). A widget allowed without tables is rendered whatever
|
||||
** the data access says: allow one that way only when it shows nothing of
|
||||
** a table.
|
||||
*******************************************************************************/
|
||||
public AssistantQBitConfig withWidgetTables(String widgetName, Set<String> tableNames)
|
||||
{
|
||||
if(this.widgetTables == null)
|
||||
{
|
||||
this.widgetTables = new LinkedHashMap<>();
|
||||
}
|
||||
this.widgetTables.put(widgetName, tableNames == null ? Set.of() : new LinkedHashSet<>(tableNames));
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for customRedactionPatterns
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -196,6 +196,7 @@ public class AssistantRuntime
|
||||
this.toolbox = new UiApiToolbox(qInstance, dataAccessService, exposurePolicy, new AiRedactor(settings.getCustomRedactionPatterns()),
|
||||
config.getMaxCellChars() == null ? AiToolKernelSettings.DEFAULT_MAX_CELL_CHARS : config.getMaxCellChars());
|
||||
this.toolbox.withScheduledDeliveryCheck(config.getScheduledDeliveryCheck());
|
||||
this.toolbox.withWidgetRules(() -> new UiApiToolbox.WidgetRules(config.getAllowedWidgetNames(), config.getHiddenWidgetNames(), config.getWidgetTables()));
|
||||
this.starterPrompts = new StarterPrompts(toolbox, clock);
|
||||
VectorStore vectors = config.getVectorStore() == null ? new InMemoryVectorStore() : config.getVectorStore();
|
||||
this.ragService = new RagService(this, vectors, uiApiGateway);
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ public class UiApiToolFamily implements ToolFamily
|
||||
Map.entry(UiApiToolbox.COUNT_RECORDS, EXPOSE_THEN_READ),
|
||||
Map.entry(UiApiToolbox.GET_RECORD, READ_THEN_EXPOSE),
|
||||
Map.entry(UiApiToolbox.SEARCH_RECORDS, EXPOSE_THEN_READ),
|
||||
Map.entry(UiApiToolbox.RENDER_WIDGET, List.of(TOOL_SWITCH, SPECD_OPERATION, PERSON_PERMISSIONS)),
|
||||
Map.entry(UiApiToolbox.RENDER_WIDGET, List.of(TOOL_SWITCH, SPECD_OPERATION, DATA_ACCESS, PERSON_PERMISSIONS)),
|
||||
Map.entry(UiApiToolbox.RECORD_HISTORY, READ_THEN_EXPOSE),
|
||||
Map.entry(UiApiToolbox.CREATE_RECORD, PROPOSED_WRITE),
|
||||
Map.entry(UiApiToolbox.UPDATE_RECORD, PROPOSED_WRITE),
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Supplier;
|
||||
import com.kingsrook.qbits.assistant.persistence.TranscriptRedactor;
|
||||
import com.kingsrook.qbits.assistant.access.DataAccessMode;
|
||||
import com.kingsrook.qbits.assistant.access.DataAccessService;
|
||||
@@ -165,6 +166,8 @@ public class UiApiToolbox
|
||||
private final int maxCellChars;
|
||||
private final AnalysisTools analysis;
|
||||
|
||||
private Supplier<WidgetRules> widgetRules = () -> WidgetRules.NONE;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -877,12 +880,40 @@ public class UiApiToolbox
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Which widgets render_widget may render, as the host declared it in code.
|
||||
**
|
||||
** @param allowed the widgets a host allowed; a widget not named is refused
|
||||
** @param hidden widgets a host took away again; one named here is refused
|
||||
** also when it is allowed
|
||||
** @param tables for an allowed widget, the tables it reads: it is rendered
|
||||
** only while the assistant may read every one of them
|
||||
*******************************************************************************/
|
||||
public record WidgetRules(Set<String> allowed, Set<String> hidden, Map<String, Set<String>> tables)
|
||||
{
|
||||
public static final WidgetRules NONE = new WidgetRules(Set.of(), Set.of(), Map.of());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The widget rules of the host, read on every call. Without them no widget
|
||||
** is renderable.
|
||||
*******************************************************************************/
|
||||
public UiApiToolbox withWidgetRules(Supplier<WidgetRules> rules)
|
||||
{
|
||||
this.widgetRules = rules == null ? () -> WidgetRules.NONE : rules;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The host's check of scheduled delivery (what is missing for a schedule to
|
||||
** run and be emailed; empty when nothing is). Null: only what the instance
|
||||
** itself shows is checked.
|
||||
*******************************************************************************/
|
||||
public UiApiToolbox withScheduledDeliveryCheck(java.util.function.Supplier<List<String>> check)
|
||||
public UiApiToolbox withScheduledDeliveryCheck(Supplier<List<String>> check)
|
||||
{
|
||||
analysis.setScheduledDeliveryCheck(check);
|
||||
return (this);
|
||||
@@ -1495,10 +1526,46 @@ public class UiApiToolbox
|
||||
|
||||
/*******************************************************************************
|
||||
** POST /widget/{widgetName}.
|
||||
**
|
||||
** The renderer of a widget is code of the application, and what it shows
|
||||
** can be rows of any table: the assistant cannot tell, and the UI API
|
||||
** decides only whether the person may see the widget. So the rules of the
|
||||
** assistant decide first, before the application is asked: a widget is
|
||||
** rendered only when the host allowed it by name (none is, by default),
|
||||
** did not hide it, and the assistant may read every table the host said
|
||||
** it reads. Everything else is refused with one answer, which does not
|
||||
** say whether the widget exists.
|
||||
*******************************************************************************/
|
||||
private void renderWidget(Context context, Outcome outcome, Map<String, Object> in) throws QException
|
||||
{
|
||||
String name = requiredString(in, "name");
|
||||
String name = requiredString(in, "name");
|
||||
WidgetRules rules = widgetRules.get() == null ? WidgetRules.NONE : widgetRules.get();
|
||||
String check = null;
|
||||
if(rules.allowed() == null || !rules.allowed().contains(name))
|
||||
{
|
||||
check = "widget:notAllowed";
|
||||
}
|
||||
else if(rules.hidden() != null && rules.hidden().contains(name))
|
||||
{
|
||||
check = "widget:hidden";
|
||||
}
|
||||
else
|
||||
{
|
||||
for(String tableName : rules.tables() == null || rules.tables().get(name) == null ? Set.<String>of() : rules.tables().get(name))
|
||||
{
|
||||
DataAccessService.Decision decision = dataAccess.forTable(tableName);
|
||||
if(qInstance.getTable(tableName) == null || !decision.mode().allowsRead())
|
||||
{
|
||||
check = "dataAccess:table:" + decision.mode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(check != null)
|
||||
{
|
||||
deny(outcome.result.withWidgetName(name), check, "The widget \"" + shortText(name) + "\" is not available to the assistant.");
|
||||
return;
|
||||
}
|
||||
if(qInstance.getWidget(name) == null)
|
||||
{
|
||||
throw (new AiToolException(AiToolOutcome.NOT_FOUND, "No widget named " + name + "."));
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.tools;
|
||||
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.kingsrook.qbits.assistant.access.DataAccessMode;
|
||||
import com.kingsrook.qbits.assistant.testing.AssistantTestFixture;
|
||||
import com.kingsrook.qbits.assistant.testing.BaseAssistantTest;
|
||||
import com.kingsrook.qbits.assistant.testing.ToolCalls;
|
||||
import com.kingsrook.qbits.assistant.tools.registry.ToolCallOutcome;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiToolOutcome;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.RawHTML;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.kingsrook.qbits.assistant.testing.AssistantTestFixture.EDITOR;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** render_widget follows the assistant's own rules, not only the person's
|
||||
** permission to see the widget. A widget is code of the application that
|
||||
** can show rows of any table, so a table the assistant may not read could be
|
||||
** read through one: the widget is rendered only when the host allowed it by
|
||||
** name, and only while the assistant may read every table the host said it
|
||||
** reads.
|
||||
**
|
||||
** The application has one widget, which anybody may see on a screen and
|
||||
** which shows a row of the blog posts.
|
||||
*******************************************************************************/
|
||||
class RenderWidgetAccessTest extends BaseAssistantTest
|
||||
{
|
||||
private static final String WIDGET = "latestPosts";
|
||||
private static final String ROW = "ROW_OF_A_TABLE_8c41";
|
||||
|
||||
private static final AtomicInteger RENDERED = new AtomicInteger();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected AssistantTestFixture.InstanceCustomizer instanceCustomizer()
|
||||
{
|
||||
return (RenderWidgetAccessTest::customize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void customize(QInstance qInstance)
|
||||
{
|
||||
qInstance.addWidget(new QWidgetMetaData().withName(WIDGET).withLabel("Latest Posts").withType(WidgetType.HTML.getType()).withCodeReference(new QCodeReference(Renderer.class))
|
||||
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
void reset()
|
||||
{
|
||||
RENDERED.set(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** No widget is renderable until the host names one: the default is closed.
|
||||
** The refusal is the assistant's own, before the application is asked, and
|
||||
** is the same for a widget that does not exist.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void noWidgetIsRenderedUntilTheHostAllowsIt() throws Exception
|
||||
{
|
||||
ToolCallOutcome refused = call(WIDGET);
|
||||
assertThat(refused.result().getOutcome()).isEqualTo(AiToolOutcome.DENIED);
|
||||
assertThat(refused.result().getPermissionCheck()).isEqualTo("widget:notAllowed");
|
||||
assertThat(refused.result().getModelContent()).isEqualTo("The widget \"" + WIDGET + "\" is not available to the assistant.").doesNotContain(ROW);
|
||||
assertThat(refused.result().getRender()).isNull();
|
||||
assertThat(RENDERED.get()).as("the application was not asked to render it").isZero();
|
||||
assertThat(call("noSuchWidget").result().getModelContent()).as("and it does not say whether a widget exists").isEqualTo("The widget \"noSuchWidget\" is not available to the assistant.");
|
||||
|
||||
config.withAllowedWidgetNames(new LinkedHashSet<>(List.of(WIDGET)));
|
||||
ToolCallOutcome rendered = call(WIDGET);
|
||||
assertThat(rendered.result().getOutcome()).as(rendered.result().getModelContent()).isEqualTo(AiToolOutcome.OK);
|
||||
assertThat(rendered.result().getModelContent()).contains(ROW);
|
||||
assertThat(RENDERED.get()).isEqualTo(1);
|
||||
|
||||
config.withHiddenWidgetNames(new LinkedHashSet<>(List.of(WIDGET)));
|
||||
assertThat(call(WIDGET).result().getPermissionCheck()).as("a widget the host hid is refused also when it is allowed").isEqualTo("widget:hidden");
|
||||
assertThat(RENDERED.get()).isEqualTo(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A widget the host allowed and said reads a table is rendered only while
|
||||
** the assistant may read that table: a pin in code, a table the host named
|
||||
** that does not exist, and an administrator's row each close it, and the
|
||||
** rows of the table do not reach the model through the widget.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void aWidgetIsNotRenderedWhenTheAssistantMayNotReadATableItReads() throws Exception
|
||||
{
|
||||
config.withAllowedWidgetNames(new LinkedHashSet<>(List.of(WIDGET))).withWidgetTables(WIDGET, Set.of("blog_post", "page"));
|
||||
assertThat(call(WIDGET).result().getOutcome()).as("both tables may be read").isEqualTo(AiToolOutcome.OK);
|
||||
assertThat(RENDERED.get()).isEqualTo(1);
|
||||
|
||||
config.withTableAccess("blog_post", DataAccessMode.NONE);
|
||||
config.getRuntime().getDataAccessService().invalidate();
|
||||
ToolCallOutcome closed = call(WIDGET);
|
||||
assertThat(closed.result().getOutcome()).isEqualTo(AiToolOutcome.DENIED);
|
||||
assertThat(closed.result().getPermissionCheck()).isEqualTo("dataAccess:table:NONE");
|
||||
assertThat(closed.result().getModelContent()).isEqualTo("The widget \"" + WIDGET + "\" is not available to the assistant.").doesNotContain(ROW);
|
||||
assertThat(RENDERED.get()).as("the application was not asked to render it").isEqualTo(1);
|
||||
|
||||
config.withTableAccess("blog_post", DataAccessMode.READ);
|
||||
config.getRuntime().getDataAccessService().invalidate();
|
||||
assertThat(call(WIDGET).result().getOutcome()).as("a table the assistant may only read is read").isEqualTo(AiToolOutcome.OK);
|
||||
|
||||
config.withWidgetTables(WIDGET, Set.of("page", "noSuchTable"));
|
||||
assertThat(call(WIDGET).result().getOutcome()).as("a table that is not there is not readable").isEqualTo(AiToolOutcome.DENIED);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One render_widget call of the fixture's editor, logged.
|
||||
*******************************************************************************/
|
||||
private ToolCallOutcome call(String widget) throws Exception
|
||||
{
|
||||
return (config.getRuntime().getToolCalls().executeAndLog(ToolCalls.call(config, EDITOR, UiApiToolbox.RENDER_WIDGET, Map.of("name", widget)), null, false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The widget: it shows a row of a table.
|
||||
*******************************************************************************/
|
||||
public static class Renderer extends AbstractWidgetRenderer
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
RENDERED.incrementAndGet();
|
||||
return (new RenderWidgetOutput(new RawHTML("Latest Posts", "<p>" + ROW + "</p>")));
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -325,9 +325,10 @@ class ToolCallArchitectureTest
|
||||
if(origin.equals(AssistantRuntime.class.getName()) || origin.startsWith(AssistantRuntime.class.getName() + "$"))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// the runtime builds the toolboxes and their families //
|
||||
// the runtime builds the toolboxes and their families, and //
|
||||
// hands the record tools the host's rules for widgets //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
return ("<init>".equals(name) || "withScheduledDeliveryCheck".equals(name));
|
||||
return ("<init>".equals(name) || "withScheduledDeliveryCheck".equals(name) || "withWidgetRules".equals(name));
|
||||
}
|
||||
if(owner.equals(UiApiToolbox.class.getName()))
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user