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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- Small built-in tools are defined with `defineTool` (LOU-D24, not breaking): `currentDateTool`, `dayNameTool`, `createEmailTool()`, `createHttpTool()`, `createSlackTool()`, `createDelegateTool()` and the descriptors `loadMcpTools()` builds no longer call `ai`'s `tool()`. They now carry canonical `inputSchema` / `execute` (the legacy `.tool` stays), and the built-ins have the tool names `current_date`, `day_name`, `send_email`, `http_request`, `slack_alert` and `delegate_to_<agent>`. Descriptions, schemas, approval and sandbox behaviour and the factory signatures are unchanged. MCP descriptors are built with the new internal `toolDescriptorFromSchema()`, which (unlike `defineTool`) accepts any server-provided name or empty description. `routeFetchThroughSandbox()` reads the canonical `execute` first.
- GitHub and Jira tools are defined with `defineTool` (LOU-D25, not breaking): the 28 `createGitHubTools()` tools and the 20 `createJiraTools()` tools no longer call `ai`'s `tool()`, so nothing under `src/tools` imports `ai` any more. They now carry canonical `inputSchema` / `execute` (the legacy `.tool` stays). Tool names, descriptions, schemas, the sandbox flags and the factory signatures are unchanged. The GitHub out-of-scope tools (branch/file writes, code search, issue CRUD, merge) are disabled through the canonical `execute` as well as `tool.execute`, so calling one still throws "out of scope" before any HTTP request. `JiraTools.register()` now also routes tools registered with `defineTool` through the sandbox seam.
- BREAKING (types only): `Message.content` is widened from `string` to `string | ContentPart[]` (LOU-V11). Messages you build keep working unchanged, but code that reads `message.content` as a string (`.length`, `.startsWith()`, a template literal) no longer type-checks. **Migration:** read `textOf(message)` instead, or narrow with `typeof message.content === 'string'`. A custom `LLMProvider` that forwards `content` should handle the parts or send `textOf(message)`.
- The execute context is our own type (LOU-D23, not breaking): the new exported `ToolExecutionContext` (`{ toolCallId, messages: readonly Message[], abortSignal?, sessionId?, onDelegatedUsage? }`, `Message` from `@loushy/build-ai-agent`) replaces the `ai` SDK's `ToolExecutionOptions` as the type of the second argument of `defineTool()`'s `execute`, `ToolDescriptor.execute`, `DefinedTool.execute` and the third argument of `sandboxExecute`, and is what `buildToolRunContext()` returns; the public types no longer import `ToolExecutionOptions` from `ai`. A function typed for `ai`'s options that only reads `toolCallId` or `abortSignal` stays assignable. `ToolRunContext` is now a deprecated alias of the optional fields of `ToolExecutionContext` (it already existed with `onDelegatedUsage` and `toolCallId`). `AgentExecutionOptions.messages` is typed `Message[]` instead of the `ai` SDK's `CoreMessage[]` (the field is unused by the SDK). The legacy `ToolDescriptor.tool` (`ai` v4 `Tool`) is unchanged. See docs/tools.md#execute-context.
- BREAKING (small): `SDKError.code` values are now the `LOUSHY_*` codes (LOU-D2): `AGENT_EXECUTION_ERROR` -> `LOUSHY_AGENT_EXECUTION_FAILED`, `TOOL_EXECUTION_ERROR` -> `LOUSHY_TOOL_EXECUTION_FAILED`, `LLM_PROVIDER_ERROR` -> `LOUSHY_PROVIDER_REQUEST_FAILED`, `SESSION_AWAITING_APPROVAL` -> `LOUSHY_SESSION_AWAITING_APPROVAL`, `FLOW_EXECUTION_ERROR` -> `LOUSHY_FLOW_EXECUTION_FAILED`, `CONFIGURATION_ERROR` -> `LOUSHY_CONFIG_INVALID`, `VALIDATION_ERROR` -> `LOUSHY_VALIDATION_FAILED`, `TIMEOUT_ERROR` -> `LOUSHY_OPERATION_TIMEOUT`, `RATE_LIMIT_ERROR` -> `LOUSHY_PROVIDER_RATE_LIMITED`; `code` is no longer optional (`new SDKError(message)` gets `LOUSHY_GENERIC_ERROR`). No class was renamed. The messages of `SDKError`s other than `ToolExecutionError`, `LLMProviderError`, `TimeoutError` and `RateLimitError` gain a second line with the code, hint and docs link, and `loadSpec()` now rejects a spec whose top-level field is a likely typo of a spec field (`LOUSHY_SPEC_UNKNOWN_FIELD`; other unknown fields are still ignored). **Migration:** compare `code` against the new values (or use `instanceof`); match messages with `toContain` / a regex, or compare `error.detail`, instead of exact equality.
Expand Down
22 changes: 22 additions & 0 deletions src/tools/built-in/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { GitHubTools, createGitHubTools } from './github';
import { NoopSandbox } from '../../security/sandboxCore';
import type { SandboxAdapter } from '../../security/sandboxCore';
import { executeToolWithSandboxGuard } from '../../execution/sandboxGuard';
import type { ToolExecutionContext } from '../../types';
import { getToolExecute, getToolInputSchema } from '../toolContract';

describe('GitHubTools scope enforcement (LOU-E14)', () => {
let githubTools: GitHubTools;
Expand Down Expand Up @@ -48,6 +50,26 @@ describe('GitHubTools scope enforcement (LOU-E14)', () => {
expect(fetchSpy).not.toHaveBeenCalled();
});

it.each(outOfScopeTools)('%s is disabled through the canonical execute (getToolExecute)', async (toolName) => {
const descriptor = githubTools.get(toolName)!;
const execute = getToolExecute(descriptor);
const ctx = {} as ToolExecutionContext;
expect(execute).toBeDefined();
expect(descriptor.execute).toBeDefined();

await expect(execute!({}, ctx)).rejects.toThrow(/out of scope/i);
await expect(descriptor.execute!({}, ctx)).rejects.toThrow(/out of scope/i);
expect(fetchSpy).not.toHaveBeenCalled();
});

it('every tool, in scope or not, exposes a canonical inputSchema and execute', () => {
for (const name of githubTools.list()) {
const descriptor = githubTools.get(name)!;
expect(getToolInputSchema(descriptor), `${name} inputSchema`).toBeDefined();
expect(getToolExecute(descriptor), `${name} execute`).toBeTypeOf('function');
}
});

it('there is no repo-delete (or any repo-deletion) tool present on the registry at all', () => {
const allNames = githubTools.list();
const deletionLike = allNames.filter(
Expand Down
Loading
Loading