chore: checkpoint browser tooling and routing updates
This commit is contained in:
@@ -525,7 +525,7 @@ hooks:
|
|||||||
|
|
||||||
## Browser Automation Tools
|
## Browser Automation Tools
|
||||||
|
|
||||||
Flynn ships six browser tools:
|
Flynn ships these browser tools:
|
||||||
|
|
||||||
- `browser.navigate`
|
- `browser.navigate`
|
||||||
- `browser.screenshot`
|
- `browser.screenshot`
|
||||||
@@ -533,9 +533,11 @@ Flynn ships six browser tools:
|
|||||||
- `browser.type`
|
- `browser.type`
|
||||||
- `browser.content`
|
- `browser.content`
|
||||||
- `browser.eval`
|
- `browser.eval`
|
||||||
|
- `browser.evaluate` (alias of `browser.eval`)
|
||||||
|
|
||||||
These tools are backed by a Puppeteer/CDP browser manager and are only registered when `browser.enabled: true`.
|
These tools are backed by a Puppeteer/CDP browser manager and are only registered when `browser.enabled: true`.
|
||||||
They can still be filtered out by tool policy (`tools.profile`, `tools.allow`, `tools.deny`).
|
They can still be filtered out by tool policy (`tools.profile`, `tools.allow`, `tools.deny`).
|
||||||
|
At startup, Flynn logs the browser tools that remain available after policy filtering.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
browser:
|
browser:
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ You are Flynn. A personal AI assistant running on your operator's hardware, with
|
|||||||
|
|
||||||
**When in doubt, check the policy, not the operator.** Before asking "can I do this?", re-read the Boundaries section. If the action is covered, do it. Only ask when the policy genuinely doesn't cover the situation.
|
**When in doubt, check the policy, not the operator.** Before asking "can I do this?", re-read the Boundaries section. If the action is covered, do it. Only ask when the policy genuinely doesn't cover the situation.
|
||||||
|
|
||||||
|
**Never ask permission for covered actions.** File edits, shell commands, git commits, builds, reads — these are pre-authorized. Do not ask "shall I?", "want me to?", or "is it okay if I?" for anything in the Always Allowed list or any non-destructive action. Just do it.
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
- **Non-destructive commands are free.** Reading files, listing directories, searching, checking status, running builds/tests, inspecting processes -- do these without hesitation. No need to ask. Only pause for destructive actions (deleting files, modifying production data, force-pushing, etc.).
|
- **Non-destructive commands are free.** Reading files, listing directories, searching, checking status, running builds/tests, inspecting processes -- do these without hesitation. No need to ask. Only pause for destructive actions (deleting files, modifying production data, force-pushing, etc.).
|
||||||
|
|||||||
+1
-1
@@ -222,7 +222,7 @@ models:
|
|||||||
# default_namespace: default
|
# default_namespace: default
|
||||||
# allowed_namespaces: [] # Empty = allow any namespace; set to restrict access.
|
# allowed_namespaces: [] # Empty = allow any namespace; set to restrict access.
|
||||||
|
|
||||||
# Optional: Browser automation tools (browser.navigate/screenshot/click/type/content/eval)
|
# Optional: Browser automation tools (browser.navigate/screenshot/click/type/content/eval/evaluate)
|
||||||
# Requires a local Chrome/Chromium install or a remote CDP endpoint.
|
# Requires a local Chrome/Chromium install or a remote CDP endpoint.
|
||||||
# browser:
|
# browser:
|
||||||
# enabled: true
|
# enabled: true
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ Tools are executable capabilities that the AI agent can call to perform actions
|
|||||||
- **File System**: `file.read`, `file.write`, `file.edit`, `file.list`
|
- **File System**: `file.read`, `file.write`, `file.edit`, `file.list`
|
||||||
- **Shell/Process**: `shell.exec`, `process.start`, `process.kill`
|
- **Shell/Process**: `shell.exec`, `process.start`, `process.kill`
|
||||||
- **Web**: `web.fetch`, `web.search`
|
- **Web**: `web.fetch`, `web.search`
|
||||||
- **Browser**: `browser.navigate`, `browser.screenshot`, `browser.click`, `browser.type`, `browser.content`, `browser.eval`
|
- **Browser**: `browser.navigate`, `browser.screenshot`, `browser.click`, `browser.type`, `browser.content`, `browser.eval`, `browser.evaluate` (alias of `browser.eval`)
|
||||||
- **Memory**: `memory.read`, `memory.write`, `memory.search`
|
- **Memory**: `memory.read`, `memory.write`, `memory.search`
|
||||||
- **MinIO**: `minio.share`, `minio.ingest`, `minio.sync`
|
- **MinIO**: `minio.share`, `minio.ingest`, `minio.sync`
|
||||||
- **Kubernetes**: `k8s.pods`, `k8s.deployments`, `k8s.logs`
|
- **Kubernetes**: `k8s.pods`, `k8s.deployments`, `k8s.logs`
|
||||||
|
|||||||
@@ -3,6 +3,107 @@
|
|||||||
"updated_at": "2026-02-17",
|
"updated_at": "2026-02-17",
|
||||||
"description": "Tracks the status of all Flynn plans and implementation phases",
|
"description": "Tracks the status of all Flynn plans and implementation phases",
|
||||||
"plans": {
|
"plans": {
|
||||||
|
"verbose-only-tool-inventory-output": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Removed continuous debug inventory emissions (`[Agent] tool-inventory` and `[Routing] tool-policy`) from agent/routing so tool inventory output appears only when requested via `/verbose` toggle-on in TUI.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/backends/native/agent.ts",
|
||||||
|
"src/daemon/routing.ts",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm typecheck passing"
|
||||||
|
},
|
||||||
|
"verbose-tool-inventory-snapshot": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Extended `/verbose` in both minimal and fullscreen TUI to emit an immediate tool inventory snapshot from the active agent, including internal/exposed counts and browser tool subsets. Added `NativeAgent.getToolInventorySnapshot()` to expose context-aware filtered tool inventory for diagnostics.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/backends/native/agent.ts",
|
||||||
|
"src/frontends/tui/minimal.ts",
|
||||||
|
"src/frontends/tui/components/App.tsx",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/frontends/tui/minimal.test.ts + pnpm typecheck passing"
|
||||||
|
},
|
||||||
|
"tui-auto-tool-discovery-shared-init": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Refactored `flynn tui` to use shared daemon tool initialization (`initTools`) instead of hand-maintained core tool registration. This keeps tool discovery for TUI agents in sync with daemon behavior (including browser/web/process/audio policy wiring) across both minimal and fullscreen modes.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/cli/tui.ts",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm typecheck passing"
|
||||||
|
},
|
||||||
|
"tui-browser-tool-registration-parity": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Fixed a registry parity gap in `flynn tui`: the TUI command path now initializes BrowserManager and registers `browser.*` tools when `browser.enabled` is true, matching daemon behavior. Added BrowserManager shutdown to TUI cleanup.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/cli/tui.ts",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm typecheck passing"
|
||||||
|
},
|
||||||
|
"tool-policy-context-debug-logging": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Added debug-level diagnostics for session tool filtering: routing now logs per-session tool-policy context and resolved browser tool allowlist, and NativeAgent now logs both internal dotted tool inventory and model-exposed underscore inventory (including browser subsets). This makes context-specific tool omissions immediately traceable.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/daemon/routing.ts",
|
||||||
|
"src/backends/native/agent.ts",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm typecheck passing"
|
||||||
|
},
|
||||||
|
"fullscreen-slash-command-parity": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Expanded fullscreen TUI slash-command support to match minimal mode for `/backend`, `/pair`, and `/elevate`, and added fullscreen `/login` handling for OAuth-based providers (GitHub/OpenAI) with clear guidance for key-entry providers. Wired fullscreen runtime config with pairing/local-provider context so these commands execute with the same session state as minimal mode.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/frontends/tui/components/App.tsx",
|
||||||
|
"src/frontends/tui/fullscreen.ts",
|
||||||
|
"src/cli/tui.ts",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm typecheck passing"
|
||||||
|
},
|
||||||
|
"browser-tools-startup-availability-logging": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Improved browser tool diagnostics by logging the final policy-allowed `browser.*` tool set at daemon startup whenever browser support is enabled, making it immediately clear why browser tools are or are not visible in a session.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/daemon/tools.ts",
|
||||||
|
"README.md",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm typecheck passing"
|
||||||
|
},
|
||||||
|
"browser-evaluate-alias-compatibility": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Added `browser.evaluate` as a compatibility alias for `browser.eval`, updated tool policy/group coverage so the alias is available under `coding`/`group:web`, extended browser registration diagnostics to include the alias, and updated browser docs/tests accordingly.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/tools/builtin/browser/tools.ts",
|
||||||
|
"src/tools/builtin/browser/tools.test.ts",
|
||||||
|
"src/tools/policy.ts",
|
||||||
|
"src/daemon/tools.ts",
|
||||||
|
"config/default.yaml",
|
||||||
|
"README.md",
|
||||||
|
"docs/api/TOOLS.md",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/tools/builtin/browser/tools.test.ts src/tools/policy.test.ts passing"
|
||||||
|
},
|
||||||
"browser-tools-activation-clarity": {
|
"browser-tools-activation-clarity": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-17",
|
"date": "2026-02-17",
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ export interface ToolUseEvent {
|
|||||||
result?: ToolResult;
|
result?: ToolResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ToolInventorySnapshot {
|
||||||
|
sessionId: string;
|
||||||
|
agent: string;
|
||||||
|
provider: string;
|
||||||
|
skill: string;
|
||||||
|
internalCount: number;
|
||||||
|
exposedCount: number;
|
||||||
|
internalBrowser: string[];
|
||||||
|
exposedBrowser: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface NativeAgentConfig {
|
export interface NativeAgentConfig {
|
||||||
modelClient: ModelClient | ModelRouter;
|
modelClient: ModelClient | ModelRouter;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
@@ -453,6 +464,35 @@ export class NativeAgent {
|
|||||||
return this._toolPolicyContext;
|
return this._toolPolicyContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getToolInventorySnapshot(): ToolInventorySnapshot {
|
||||||
|
if (!this.toolRegistry) {
|
||||||
|
return {
|
||||||
|
sessionId: '-',
|
||||||
|
agent: '-',
|
||||||
|
provider: '-',
|
||||||
|
skill: '-',
|
||||||
|
internalCount: 0,
|
||||||
|
exposedCount: 0,
|
||||||
|
internalBrowser: [],
|
||||||
|
exposedBrowser: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const internal = this.toolRegistry.filteredList(this._toolPolicyContext).map((tool) => tool.name);
|
||||||
|
const exposed = this.toolRegistry.filteredToAnthropicFormat(this._toolPolicyContext).map((tool) => tool.name);
|
||||||
|
const context = this._toolPolicyContext;
|
||||||
|
return {
|
||||||
|
sessionId: context?.sessionId ?? '-',
|
||||||
|
agent: context?.agent ?? '-',
|
||||||
|
provider: context?.provider ?? '-',
|
||||||
|
skill: context?.skillName ?? '-',
|
||||||
|
internalCount: internal.length,
|
||||||
|
exposedCount: exposed.length,
|
||||||
|
internalBrowser: internal.filter((name) => name.startsWith('browser.')),
|
||||||
|
exposedBrowser: exposed.filter((name) => name.startsWith('browser_')),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
setAttachmentCollector(collector: OutboundAttachmentCollector | undefined): void {
|
setAttachmentCollector(collector: OutboundAttachmentCollector | undefined): void {
|
||||||
this._attachmentCollector = collector;
|
this._attachmentCollector = collector;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-39
@@ -103,13 +103,6 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
const { MinimalTui, startFullscreenTui } = await import('../frontends/tui/index.js');
|
const { MinimalTui, startFullscreenTui } = await import('../frontends/tui/index.js');
|
||||||
const { NativeAgent } = await import('../backends/index.js');
|
const { NativeAgent } = await import('../backends/index.js');
|
||||||
const {
|
const {
|
||||||
ToolRegistry,
|
|
||||||
ToolExecutor,
|
|
||||||
ToolPolicy,
|
|
||||||
allBuiltinTools,
|
|
||||||
createWebSearchTools,
|
|
||||||
createProcessTools,
|
|
||||||
ProcessManager,
|
|
||||||
createGmailTools,
|
createGmailTools,
|
||||||
createGcalTools,
|
createGcalTools,
|
||||||
createGdocsTools,
|
createGdocsTools,
|
||||||
@@ -119,6 +112,8 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
createAgentDelegateTool,
|
createAgentDelegateTool,
|
||||||
} = await import('../tools/index.js');
|
} = await import('../tools/index.js');
|
||||||
const { HookEngine } = await import('../hooks/index.js');
|
const { HookEngine } = await import('../hooks/index.js');
|
||||||
|
const { Lifecycle } = await import('../daemon/lifecycle.js');
|
||||||
|
const { initTools } = await import('../daemon/tools.js');
|
||||||
const { createModelRouter } = await import('../daemon/index.js');
|
const { createModelRouter } = await import('../daemon/index.js');
|
||||||
const { AgentConfigRegistry } = await import('../agents/index.js');
|
const { AgentConfigRegistry } = await import('../agents/index.js');
|
||||||
|
|
||||||
@@ -147,33 +142,8 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
const systemPrompt = loadSystemPrompt();
|
const systemPrompt = loadSystemPrompt();
|
||||||
|
|
||||||
const hookEngine = new HookEngine(config.hooks);
|
const hookEngine = new HookEngine(config.hooks);
|
||||||
const toolRegistry = new ToolRegistry();
|
const lifecycle = new Lifecycle();
|
||||||
for (const tool of allBuiltinTools) {
|
const { toolRegistry, toolExecutor } = initTools({ config, lifecycle, hookEngine });
|
||||||
toolRegistry.register(tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register web search tools if configured with credentials
|
|
||||||
if (config.web_search.api_key || config.web_search.endpoint) {
|
|
||||||
for (const tool of createWebSearchTools({
|
|
||||||
provider: config.web_search.provider,
|
|
||||||
apiKey: config.web_search.api_key,
|
|
||||||
endpoint: config.web_search.endpoint,
|
|
||||||
maxResults: config.web_search.max_results,
|
|
||||||
})) {
|
|
||||||
toolRegistry.register(tool);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize process manager and register process tools
|
|
||||||
const processManager = new ProcessManager({
|
|
||||||
maxConcurrent: config.process.max_concurrent,
|
|
||||||
maxRuntimeMinutes: config.process.max_runtime_minutes,
|
|
||||||
bufferSize: config.process.buffer_size,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const tool of createProcessTools(processManager)) {
|
|
||||||
toolRegistry.register(tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register Gmail tools if configured
|
// Register Gmail tools if configured
|
||||||
if (config.automation.gmail?.enabled) {
|
if (config.automation.gmail?.enabled) {
|
||||||
@@ -233,10 +203,6 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
toolRegistry.setPolicy(new ToolPolicy(config.tools));
|
|
||||||
|
|
||||||
const toolExecutor = new ToolExecutor(toolRegistry, hookEngine);
|
|
||||||
|
|
||||||
const session = sessionManager.getSession('tui', 'local');
|
const session = sessionManager.getSession('tui', 'local');
|
||||||
const modelProviderConfigs = buildProviderConfigMap(config);
|
const modelProviderConfigs = buildProviderConfigMap(config);
|
||||||
|
|
||||||
@@ -262,7 +228,7 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
processManager.shutdown();
|
void lifecycle.shutdown();
|
||||||
sessionStore.close();
|
sessionStore.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -292,6 +258,9 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
model: config.models.default.model,
|
model: config.models.default.model,
|
||||||
agent,
|
agent,
|
||||||
hookEngine,
|
hookEngine,
|
||||||
|
pairingManager,
|
||||||
|
localProviders: config.models.local_providers,
|
||||||
|
currentLocalProvider: config.models.local?.provider,
|
||||||
modelProviderConfigs,
|
modelProviderConfigs,
|
||||||
contextThresholdPct: config.compaction.threshold_pct,
|
contextThresholdPct: config.compaction.threshold_pct,
|
||||||
onTransfer: transferSessionToTarget,
|
onTransfer: transferSessionToTarget,
|
||||||
@@ -331,6 +300,9 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
model: config.models.default.model,
|
model: config.models.default.model,
|
||||||
agent,
|
agent,
|
||||||
hookEngine,
|
hookEngine,
|
||||||
|
pairingManager,
|
||||||
|
localProviders: config.models.local_providers,
|
||||||
|
currentLocalProvider: config.models.local?.provider,
|
||||||
modelProviderConfigs,
|
modelProviderConfigs,
|
||||||
contextThresholdPct: config.compaction.threshold_pct,
|
contextThresholdPct: config.compaction.threshold_pct,
|
||||||
onTransfer: transferSessionToTarget,
|
onTransfer: transferSessionToTarget,
|
||||||
|
|||||||
+15
-13
@@ -253,6 +253,20 @@ export function createMessageRouter(deps: {
|
|||||||
} as AgentDelegateDeps));
|
} as AgentDelegateDeps));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toolPolicyContext = {
|
||||||
|
agent: effectiveTier,
|
||||||
|
provider: effectiveProvider,
|
||||||
|
sessionId: session.id,
|
||||||
|
channel,
|
||||||
|
sender: senderId,
|
||||||
|
tier: effectiveTier,
|
||||||
|
autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
|
||||||
|
skillName: activeSkillName,
|
||||||
|
skillPermissions: activeSkill?.manifest.permissions,
|
||||||
|
allowedSecretScopes: activeSkill?.manifest.permissions?.secrets,
|
||||||
|
executionEnvironment,
|
||||||
|
};
|
||||||
|
|
||||||
const orchestrator = new AgentOrchestrator({
|
const orchestrator = new AgentOrchestrator({
|
||||||
modelRouter: deps.modelRouter,
|
modelRouter: deps.modelRouter,
|
||||||
systemPrompt: effectiveSystemPrompt,
|
systemPrompt: effectiveSystemPrompt,
|
||||||
@@ -283,19 +297,7 @@ export function createMessageRouter(deps: {
|
|||||||
memoryAutoExtract: deps.config.memory?.auto_extract,
|
memoryAutoExtract: deps.config.memory?.auto_extract,
|
||||||
memoryInjectionStrategy: deps.config.memory?.injection_strategy,
|
memoryInjectionStrategy: deps.config.memory?.injection_strategy,
|
||||||
memoryMaxInjectionTokens: deps.config.memory?.max_injection_tokens,
|
memoryMaxInjectionTokens: deps.config.memory?.max_injection_tokens,
|
||||||
toolPolicyContext: {
|
toolPolicyContext,
|
||||||
agent: effectiveTier,
|
|
||||||
provider: effectiveProvider,
|
|
||||||
sessionId: session.id,
|
|
||||||
channel,
|
|
||||||
sender: senderId,
|
|
||||||
tier: effectiveTier,
|
|
||||||
autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
|
|
||||||
skillName: activeSkillName,
|
|
||||||
skillPermissions: activeSkill?.manifest.permissions,
|
|
||||||
allowedSecretScopes: activeSkill?.manifest.permissions?.secrets,
|
|
||||||
executionEnvironment,
|
|
||||||
},
|
|
||||||
attachmentCollector: collector,
|
attachmentCollector: collector,
|
||||||
});
|
});
|
||||||
// Resolve the lazy orchestrator reference for agent.delegate
|
// Resolve the lazy orchestrator reference for agent.delegate
|
||||||
|
|||||||
+3
-1
@@ -66,7 +66,7 @@ export function initTools(deps: ToolsDeps): ToolsResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize browser manager and register browser tools (if enabled)
|
// Initialize browser manager and register browser tools (if enabled)
|
||||||
const browserToolNames = ['browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval'];
|
const browserToolNames = ['browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval', 'browser.evaluate'];
|
||||||
let browserManager: BrowserManager | undefined;
|
let browserManager: BrowserManager | undefined;
|
||||||
if (config.browser?.enabled) {
|
if (config.browser?.enabled) {
|
||||||
const manager = new BrowserManager({
|
const manager = new BrowserManager({
|
||||||
@@ -108,6 +108,8 @@ export function initTools(deps: ToolsDeps): ToolsResult {
|
|||||||
const availableBrowserTools = browserToolNames.filter((name) => allowed.has(name));
|
const availableBrowserTools = browserToolNames.filter((name) => allowed.has(name));
|
||||||
if (availableBrowserTools.length === 0) {
|
if (availableBrowserTools.length === 0) {
|
||||||
console.log('Browser tools are registered but blocked by tool policy (use tools.profile=coding/full or tools.allow).');
|
console.log('Browser tools are registered but blocked by tool policy (use tools.profile=coding/full or tools.allow).');
|
||||||
|
} else {
|
||||||
|
console.log(`Browser tools available after policy: ${availableBrowserTools.join(', ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import type { ModelConfig, ModelProvider } from '../../../config/schema.js';
|
|||||||
import { MODEL_PROVIDERS } from '../../../config/schema.js';
|
import { MODEL_PROVIDERS } from '../../../config/schema.js';
|
||||||
import { createClientFromConfig } from '../../../daemon/index.js';
|
import { createClientFromConfig } from '../../../daemon/index.js';
|
||||||
import { estimateMessageTokens, getContextWindow } from '../../../context/tokens.js';
|
import { estimateMessageTokens, getContextWindow } from '../../../context/tokens.js';
|
||||||
|
import type { PairingManager } from '../../../channels/pairing.js';
|
||||||
|
import { loginGitHub, loginOpenAI } from '../../../auth/index.js';
|
||||||
|
import { OllamaClient, LlamaCppClient } from '../../../models/index.js';
|
||||||
|
|
||||||
/** Format a tool name like "gmail.list" -> "Gmail: List" */
|
/** Format a tool name like "gmail.list" -> "Gmail: List" */
|
||||||
function formatToolName(name: string): string {
|
function formatToolName(name: string): string {
|
||||||
@@ -49,6 +52,9 @@ export interface AppProps {
|
|||||||
model: string;
|
model: string;
|
||||||
agent?: NativeAgent;
|
agent?: NativeAgent;
|
||||||
hookEngine?: HookEngine;
|
hookEngine?: HookEngine;
|
||||||
|
pairingManager?: PairingManager;
|
||||||
|
localProviders?: Record<string, ModelConfig>;
|
||||||
|
currentLocalProvider?: string;
|
||||||
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
|
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
|
||||||
contextThresholdPct?: number;
|
contextThresholdPct?: number;
|
||||||
onTransfer?: (target: string) => string | void;
|
onTransfer?: (target: string) => string | void;
|
||||||
@@ -63,6 +69,9 @@ export function App({
|
|||||||
model,
|
model,
|
||||||
agent,
|
agent,
|
||||||
hookEngine,
|
hookEngine,
|
||||||
|
pairingManager,
|
||||||
|
localProviders,
|
||||||
|
currentLocalProvider,
|
||||||
modelProviderConfigs,
|
modelProviderConfigs,
|
||||||
contextThresholdPct,
|
contextThresholdPct,
|
||||||
onTransfer,
|
onTransfer,
|
||||||
@@ -196,6 +205,55 @@ export function App({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pushAssistantMessage = useCallback((content: string) => {
|
||||||
|
setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content })]);
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
const getAvailableBackends = useCallback((): string[] => {
|
||||||
|
const backends: string[] = [];
|
||||||
|
if (currentLocalProvider) {
|
||||||
|
backends.push(currentLocalProvider);
|
||||||
|
}
|
||||||
|
if (localProviders) {
|
||||||
|
backends.push(...Object.keys(localProviders));
|
||||||
|
}
|
||||||
|
return [...new Set(backends)];
|
||||||
|
}, [currentLocalProvider, localProviders]);
|
||||||
|
|
||||||
|
const createLocalClient = useCallback((cfg: ModelConfig): ModelClient | null => {
|
||||||
|
if (cfg.provider === 'ollama') {
|
||||||
|
return new OllamaClient({
|
||||||
|
model: cfg.model,
|
||||||
|
host: cfg.endpoint,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (cfg.provider === 'llamacpp') {
|
||||||
|
return new LlamaCppClient({
|
||||||
|
endpoint: cfg.endpoint ?? 'http://localhost:8080',
|
||||||
|
model: cfg.model,
|
||||||
|
authToken: cfg.auth_token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const parseDurationToMs = useCallback((value: string): number | null => {
|
||||||
|
const m = value.match(/^(\d+)([smhd])$/i);
|
||||||
|
if (!m) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = Number.parseInt(m[1], 10);
|
||||||
|
if (!Number.isFinite(n) || n <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const unit = m[2].toLowerCase();
|
||||||
|
if (unit === 's') {return n * 1000;}
|
||||||
|
if (unit === 'm') {return n * 60_000;}
|
||||||
|
if (unit === 'h') {return n * 3_600_000;}
|
||||||
|
if (unit === 'd') {return n * 86_400_000;}
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = useCallback(async (value: string) => {
|
const handleSubmit = useCallback(async (value: string) => {
|
||||||
if (confirmation) {
|
if (confirmation) {
|
||||||
return;
|
return;
|
||||||
@@ -273,7 +331,12 @@ export function App({
|
|||||||
case 'verbose': {
|
case 'verbose': {
|
||||||
const next = !verbose;
|
const next = !verbose;
|
||||||
setVerbose(next);
|
setVerbose(next);
|
||||||
setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Verbose mode: ${next ? 'on' : 'off'}` })]);
|
let content = `Verbose mode: ${next ? 'on' : 'off'}`;
|
||||||
|
if (next && agent) {
|
||||||
|
const snapshot = agent.getToolInventorySnapshot();
|
||||||
|
content += `\n[Agent] tool-inventory session=${snapshot.sessionId} agent=${snapshot.agent} provider=${snapshot.provider} skill=${snapshot.skill} internal=${snapshot.internalCount} exposed=${snapshot.exposedCount} internal_browser=[${snapshot.internalBrowser.join(', ') || 'none'}] exposed_browser=[${snapshot.exposedBrowser.join(', ') || 'none'}]`;
|
||||||
|
}
|
||||||
|
setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content })]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,12 +543,213 @@ export function App({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'backend':
|
case 'backend': {
|
||||||
case 'login':
|
if (!modelRouter) {
|
||||||
case 'pair':
|
pushAssistantMessage('Backend switching not available.');
|
||||||
case 'elevate':
|
return;
|
||||||
setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `/${command.type} is not supported in fullscreen mode.` })]);
|
}
|
||||||
|
|
||||||
|
if (!command.provider) {
|
||||||
|
const current = modelRouter.getLocalProviderName() ?? currentLocalProvider ?? 'unknown';
|
||||||
|
const available = getAvailableBackends();
|
||||||
|
pushAssistantMessage(`Current local backend: ${current}\nAvailable: ${available.join(', ')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerConfig = localProviders?.[command.provider];
|
||||||
|
if (!providerConfig) {
|
||||||
|
const available = getAvailableBackends();
|
||||||
|
pushAssistantMessage(`Backend '${command.provider}' not configured.\nAvailable: ${available.join(', ')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = createLocalClient(providerConfig);
|
||||||
|
if (!client) {
|
||||||
|
pushAssistantMessage(`Unsupported backend provider '${providerConfig.provider}'.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modelRouter.setLocalClient(client, command.provider);
|
||||||
|
modelRouter.setTier('local');
|
||||||
|
if (agent) {
|
||||||
|
agent.setModelTier('local');
|
||||||
|
}
|
||||||
|
setCurrentModel(modelRouter.getLabel('local'));
|
||||||
|
pushAssistantMessage(`Switched backend to ${command.provider}`);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'login': {
|
||||||
|
const provider = (command.provider ?? '').trim().toLowerCase();
|
||||||
|
if (!provider) {
|
||||||
|
pushAssistantMessage('Usage: /login <provider>\nSupported: github, openai, anthropic, zai');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === 'github') {
|
||||||
|
pushAssistantMessage('Starting GitHub OAuth device login...');
|
||||||
|
try {
|
||||||
|
await loginGitHub((userCode, verificationUri) => {
|
||||||
|
pushAssistantMessage(`GitHub login required:\nCode: ${userCode}\nURL: ${verificationUri}`);
|
||||||
|
});
|
||||||
|
pushAssistantMessage('GitHub login complete. Token stored in ~/.config/flynn/auth.json');
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
|
pushAssistantMessage(`GitHub login failed: ${msg}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === 'openai') {
|
||||||
|
pushAssistantMessage('Starting OpenAI OAuth device login...');
|
||||||
|
try {
|
||||||
|
await loginOpenAI((userCode, verificationUri) => {
|
||||||
|
pushAssistantMessage(`OpenAI login required:\nCode: ${userCode}\nURL: ${verificationUri}`);
|
||||||
|
});
|
||||||
|
pushAssistantMessage('OpenAI login complete. Credentials stored in ~/.config/flynn/auth.json');
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
|
pushAssistantMessage(`OpenAI login failed: ${msg}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === 'anthropic' || provider === 'zai' || provider === 'zhipuai') {
|
||||||
|
pushAssistantMessage(
|
||||||
|
`/${command.type} ${provider} requires key entry, which fullscreen mode does not mask.\nUse minimal mode (pnpm tui) for interactive key setup.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushAssistantMessage(`Unknown login provider: ${provider}. Supported: github, openai, anthropic, zai`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'pair': {
|
||||||
|
if (!pairingManager) {
|
||||||
|
pushAssistantMessage('Pairing not enabled. Set pairing.enabled: true in config.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.action === 'generate') {
|
||||||
|
const code = pairingManager.generateCode(command.args);
|
||||||
|
const pending = pairingManager.listPendingCodes().find(p => p.code === code);
|
||||||
|
const expiresIn = pending ? Math.round((pending.expiresAt - Date.now()) / 1000) : '?';
|
||||||
|
pushAssistantMessage(`Pairing code: ${code}\nExpires in ${expiresIn}s${command.args ? ` (label: ${command.args})` : ''}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.action === 'revoke') {
|
||||||
|
const args = (command.args ?? '').trim();
|
||||||
|
const parts = args.split(/\s+/);
|
||||||
|
if (parts.length < 2) {
|
||||||
|
pushAssistantMessage('Usage: /pair revoke <channel> <senderId>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [channel, senderId] = parts;
|
||||||
|
const revoked = pairingManager.revokeApproval(channel, senderId);
|
||||||
|
pushAssistantMessage(revoked ? `Revoked approval for ${channel}:${senderId}` : `No approval found for ${channel}:${senderId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = pairingManager.listPendingCodes();
|
||||||
|
const approved = pairingManager.listApproved();
|
||||||
|
if (pending.length === 0 && approved.length === 0) {
|
||||||
|
pushAssistantMessage('No pending codes or approved senders.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (pending.length > 0) {
|
||||||
|
lines.push('Pending codes:');
|
||||||
|
for (const p of pending) {
|
||||||
|
const ttl = Math.max(0, Math.round((p.expiresAt - Date.now()) / 1000));
|
||||||
|
lines.push(` ${p.code} expires in ${ttl}s${p.label ? ` (label: ${p.label})` : ''}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (approved.length > 0) {
|
||||||
|
lines.push('Approved senders:');
|
||||||
|
for (const a of approved) {
|
||||||
|
const date = new Date(a.approvedAt).toISOString().slice(0, 16).replace('T', ' ');
|
||||||
|
lines.push(` ${a.channel}:${a.senderId} since ${date} (code: ${a.codeUsed})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pushAssistantMessage(lines.join('\n'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'elevate': {
|
||||||
|
const untilRaw = session.getConfig('elevation.until_ms');
|
||||||
|
const reason = session.getConfig('elevation.reason') ?? '';
|
||||||
|
const id = session.getConfig('elevation.id') ?? '';
|
||||||
|
const showStatus = () => {
|
||||||
|
if (!untilRaw || !id) {
|
||||||
|
pushAssistantMessage('Elevated mode: off');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const untilMs = Number.parseInt(untilRaw, 10);
|
||||||
|
if (!Number.isFinite(untilMs) || untilMs <= Date.now()) {
|
||||||
|
session.deleteConfig('elevation.until_ms');
|
||||||
|
session.deleteConfig('elevation.reason');
|
||||||
|
session.deleteConfig('elevation.id');
|
||||||
|
pushAssistantMessage('Elevated mode: off');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const remainingSec = Math.ceil((untilMs - Date.now()) / 1000);
|
||||||
|
pushAssistantMessage(`Elevated mode: on (${remainingSec}s remaining)${reason ? ` - ${reason}` : ''}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const raw = (command.args ?? '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
showStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = raw.split(/\s+/);
|
||||||
|
const hasYes = parts.includes('--yes') || parts.includes('--confirm');
|
||||||
|
const filtered = parts.filter((p) => p !== '--yes' && p !== '--confirm');
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
pushAssistantMessage('Usage: /elevate <duration> <reason...> --yes | /elevate off --yes');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filtered[0] === 'off') {
|
||||||
|
if (!hasYes) {
|
||||||
|
pushAssistantMessage('Refusing to disable elevation without explicit confirmation. Use: /elevate off --yes');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.deleteConfig('elevation.until_ms');
|
||||||
|
session.deleteConfig('elevation.reason');
|
||||||
|
session.deleteConfig('elevation.id');
|
||||||
|
pushAssistantMessage('Elevated mode: off');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasYes) {
|
||||||
|
pushAssistantMessage('Refusing to enable elevation without explicit confirmation. Use: /elevate <duration> <reason...> --yes');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ttlMs = parseDurationToMs(filtered[0]);
|
||||||
|
if (!ttlMs) {
|
||||||
|
pushAssistantMessage('Invalid duration. Use one of: 30s, 10m, 1h, 1d');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reasonText = filtered.slice(1).join(' ').trim();
|
||||||
|
const untilMs = Date.now() + ttlMs;
|
||||||
|
const newId = `${untilMs}`;
|
||||||
|
session.setConfig('elevation.until_ms', String(untilMs));
|
||||||
|
session.setConfig('elevation.id', newId);
|
||||||
|
if (reasonText) {
|
||||||
|
session.setConfig('elevation.reason', reasonText);
|
||||||
|
} else {
|
||||||
|
session.deleteConfig('elevation.reason');
|
||||||
|
}
|
||||||
|
pushAssistantMessage(`Elevated mode: on until ${new Date(untilMs).toISOString()}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
case 'message':
|
case 'message':
|
||||||
break;
|
break;
|
||||||
@@ -585,6 +849,13 @@ export function App({
|
|||||||
messages.length,
|
messages.length,
|
||||||
tokenUsage.inputTokens,
|
tokenUsage.inputTokens,
|
||||||
tokenUsage.outputTokens,
|
tokenUsage.outputTokens,
|
||||||
|
pushAssistantMessage,
|
||||||
|
getAvailableBackends,
|
||||||
|
createLocalClient,
|
||||||
|
parseDurationToMs,
|
||||||
|
localProviders,
|
||||||
|
currentLocalProvider,
|
||||||
|
pairingManager,
|
||||||
modelProviderConfigs,
|
modelProviderConfigs,
|
||||||
onTransfer,
|
onTransfer,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { ModelRouter } from '../../models/router.js';
|
|||||||
import type { NativeAgent } from '../../backends/native/agent.js';
|
import type { NativeAgent } from '../../backends/native/agent.js';
|
||||||
import type { HookEngine } from '../../hooks/index.js';
|
import type { HookEngine } from '../../hooks/index.js';
|
||||||
import type { ModelConfig, ModelProvider } from '../../config/index.js';
|
import type { ModelConfig, ModelProvider } from '../../config/index.js';
|
||||||
|
import type { PairingManager } from '../../channels/pairing.js';
|
||||||
|
|
||||||
export interface FullscreenTuiConfig {
|
export interface FullscreenTuiConfig {
|
||||||
session: ManagedSession;
|
session: ManagedSession;
|
||||||
@@ -16,6 +17,9 @@ export interface FullscreenTuiConfig {
|
|||||||
model: string;
|
model: string;
|
||||||
agent?: NativeAgent;
|
agent?: NativeAgent;
|
||||||
hookEngine?: HookEngine;
|
hookEngine?: HookEngine;
|
||||||
|
pairingManager?: PairingManager;
|
||||||
|
localProviders?: Record<string, ModelConfig>;
|
||||||
|
currentLocalProvider?: string;
|
||||||
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
|
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
|
||||||
contextThresholdPct?: number;
|
contextThresholdPct?: number;
|
||||||
onTransfer?: (target: string) => string | void;
|
onTransfer?: (target: string) => string | void;
|
||||||
@@ -41,6 +45,9 @@ export async function startFullscreenTui(config: FullscreenTuiConfig): Promise<v
|
|||||||
model: config.model,
|
model: config.model,
|
||||||
agent: config.agent,
|
agent: config.agent,
|
||||||
hookEngine: config.hookEngine,
|
hookEngine: config.hookEngine,
|
||||||
|
pairingManager: config.pairingManager,
|
||||||
|
localProviders: config.localProviders,
|
||||||
|
currentLocalProvider: config.currentLocalProvider,
|
||||||
modelProviderConfigs: config.modelProviderConfigs,
|
modelProviderConfigs: config.modelProviderConfigs,
|
||||||
contextThresholdPct: config.contextThresholdPct,
|
contextThresholdPct: config.contextThresholdPct,
|
||||||
onTransfer: config.onTransfer,
|
onTransfer: config.onTransfer,
|
||||||
|
|||||||
@@ -468,7 +468,14 @@ export class MinimalTui {
|
|||||||
|
|
||||||
private handleVerboseCommand(): void {
|
private handleVerboseCommand(): void {
|
||||||
this.verbose = !this.verbose;
|
this.verbose = !this.verbose;
|
||||||
console.log(`${colors.gray}Verbose mode:${colors.reset} ${this.verbose ? 'on' : 'off'}\n`);
|
console.log(`${colors.gray}Verbose mode:${colors.reset} ${this.verbose ? 'on' : 'off'}`);
|
||||||
|
if (this.verbose && this.config.agent) {
|
||||||
|
const snapshot = this.config.agent.getToolInventorySnapshot();
|
||||||
|
console.log(
|
||||||
|
`[Agent] tool-inventory session=${snapshot.sessionId} agent=${snapshot.agent} provider=${snapshot.provider} skill=${snapshot.skill} internal=${snapshot.internalCount} exposed=${snapshot.exposedCount} internal_browser=[${snapshot.internalBrowser.join(', ') || 'none'}] exposed_browser=[${snapshot.exposedBrowser.join(', ') || 'none'}]`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleQueueCommand(action?: 'show' | 'set' | 'reset', args?: string): void {
|
private handleQueueCommand(action?: 'show' | 'set' | 'reset', args?: string): void {
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ describe('Browser tools', () => {
|
|||||||
expect(names).toContain('browser.type');
|
expect(names).toContain('browser.type');
|
||||||
expect(names).toContain('browser.content');
|
expect(names).toContain('browser.content');
|
||||||
expect(names).toContain('browser.eval');
|
expect(names).toContain('browser.eval');
|
||||||
expect(names).toHaveLength(6);
|
expect(names).toContain('browser.evaluate');
|
||||||
|
expect(names).toHaveLength(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('browser.navigate navigates to URL', async () => {
|
it('browser.navigate navigates to URL', async () => {
|
||||||
@@ -147,6 +148,13 @@ describe('Browser tools', () => {
|
|||||||
expect(result.output).toBe('hello world');
|
expect(result.output).toBe('hello world');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('browser.evaluate aliases browser.eval behavior', async () => {
|
||||||
|
const tool = getTool('browser.evaluate');
|
||||||
|
const result = await tool.execute({ expression: '1 + 1' });
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.output).toContain('42');
|
||||||
|
});
|
||||||
|
|
||||||
it('handles navigation errors gracefully', async () => {
|
it('handles navigation errors gracefully', async () => {
|
||||||
mockGoto.mockRejectedValueOnce(new Error('Navigation failed'));
|
mockGoto.mockRejectedValueOnce(new Error('Navigation failed'));
|
||||||
const tool = getTool('browser.navigate');
|
const tool = getTool('browser.navigate');
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export function createBrowserTools(manager: BrowserManager): Tool[] {
|
|||||||
createBrowserTypeTool(manager),
|
createBrowserTypeTool(manager),
|
||||||
createBrowserContentTool(manager),
|
createBrowserContentTool(manager),
|
||||||
createBrowserEvalTool(manager),
|
createBrowserEvalTool(manager),
|
||||||
|
createBrowserEvaluateTool(manager),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,9 +295,25 @@ function createBrowserContentTool(manager: BrowserManager): Tool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createBrowserEvalTool(manager: BrowserManager): Tool {
|
function createBrowserEvalTool(manager: BrowserManager): Tool {
|
||||||
|
return createBrowserEvalLikeTool(
|
||||||
|
manager,
|
||||||
|
'browser.eval',
|
||||||
|
'Evaluate JavaScript in the browser page context. Returns the result as a string.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBrowserEvaluateTool(manager: BrowserManager): Tool {
|
||||||
|
return createBrowserEvalLikeTool(
|
||||||
|
manager,
|
||||||
|
'browser.evaluate',
|
||||||
|
'Alias of browser.eval for compatibility. Evaluates JavaScript in the browser page context.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBrowserEvalLikeTool(manager: BrowserManager, name: 'browser.eval' | 'browser.evaluate', description: string): Tool {
|
||||||
return {
|
return {
|
||||||
name: 'browser.eval',
|
name,
|
||||||
description: 'Evaluate JavaScript in the browser page context. Returns the result as a string.',
|
description,
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
+2
-1
@@ -98,6 +98,7 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
|
|||||||
'browser.type',
|
'browser.type',
|
||||||
'browser.content',
|
'browser.content',
|
||||||
'browser.eval',
|
'browser.eval',
|
||||||
|
'browser.evaluate',
|
||||||
'agent.delegate',
|
'agent.delegate',
|
||||||
'agents.list',
|
'agents.list',
|
||||||
]),
|
]),
|
||||||
@@ -110,7 +111,7 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
|
|||||||
export const TOOL_GROUPS: Record<string, string[]> = {
|
export const TOOL_GROUPS: Record<string, string[]> = {
|
||||||
'group:fs': ['file.read', 'file.write', 'file.edit', 'file.patch', 'file.list'],
|
'group:fs': ['file.read', 'file.write', 'file.edit', 'file.patch', 'file.list'],
|
||||||
'group:runtime': ['shell.exec', 'process.start', 'process.output', 'process.status', 'process.kill', 'process.list', 'screen.capture', 'camera.capture'],
|
'group:runtime': ['shell.exec', 'process.start', 'process.output', 'process.status', 'process.kill', 'process.list', 'screen.capture', 'camera.capture'],
|
||||||
'group:web': ['web.fetch', 'web.search', 'browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval'],
|
'group:web': ['web.fetch', 'web.search', 'browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval', 'browser.evaluate'],
|
||||||
'group:memory': ['memory.read', 'memory.write', 'memory.search'],
|
'group:memory': ['memory.read', 'memory.write', 'memory.search'],
|
||||||
'group:gmail': ['gmail.list', 'gmail.search', 'gmail.read'],
|
'group:gmail': ['gmail.list', 'gmail.search', 'gmail.read'],
|
||||||
'group:gcal': ['calendar.today', 'calendar.list', 'calendar.search'],
|
'group:gcal': ['calendar.today', 'calendar.list', 'calendar.search'],
|
||||||
|
|||||||
Reference in New Issue
Block a user