From cf6d7163be9f7fdcf1efbb82ccf154b945f2427c Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 2 Dec 2025 12:11:31 +0000 Subject: [PATCH] feat(deno): Add Deno tool and smartdeno integration; export and register DenoTool; update docs and tests --- changelog.md | 11 ++ package.json | 1 + pnpm-lock.yaml | 41 +++++++ readme.md | 108 +++++++++++++--- test/test.ts | 35 ++++++ ts/00_commitinfo_data.ts | 2 +- ts/index.ts | 1 + ts/plugins.ts | 2 + ts/smartagent.classes.dualagent.ts | 2 + ts/smartagent.tools.deno.ts | 191 +++++++++++++++++++++++++++++ 10 files changed, 373 insertions(+), 21 deletions(-) create mode 100644 ts/smartagent.tools.deno.ts diff --git a/changelog.md b/changelog.md index 09f0bdd..36ee37a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2025-12-02 - 1.1.0 - feat(deno) +Add Deno tool and smartdeno integration; export and register DenoTool; update docs and tests + +- Introduce DenoTool wrapper (ts/smartagent.tools.deno.ts) to run TypeScript/JavaScript in a sandboxed Deno environment with permission controls +- Add @push.rocks/smartdeno dependency to package.json +- Import and re-export smartdeno in ts/plugins.ts +- Export DenoTool and TDenoPermission from ts/index.ts +- Register DenoTool in DualAgentOrchestrator.registerStandardTools() so it's available as a standard tool +- Update README architecture diagram and docs to include Deno as a standard tool +- Add tests for DenoTool in test/test.ts (exports, instantiation, call summary, permission display) + ## 2025-12-02 - 1.0.2 - fix(core) Bump version to 1.0.2 (patch release) diff --git a/package.json b/package.json index 1bf7178..904b748 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dependencies": { "@push.rocks/smartai": "^0.8.0", "@push.rocks/smartbrowser": "^2.0.8", + "@push.rocks/smartdeno": "^1.2.0", "@push.rocks/smartfs": "^1.2.0", "@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartshell": "^3.3.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f14f25..f1927ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@push.rocks/smartbrowser': specifier: ^2.0.8 version: 2.0.8(typescript@5.9.3) + '@push.rocks/smartdeno': + specifier: ^1.2.0 + version: 1.2.0 '@push.rocks/smartfs': specifier: ^1.2.0 version: 1.2.0 @@ -831,6 +834,9 @@ packages: '@push.rocks/smartarchive@4.2.4': resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==} + '@push.rocks/smartarchive@5.0.1': + resolution: {integrity: sha512-x4bie9IIdL9BZqBZLc8Pemp8xZOJGa6mXSVgKJRL4/Rw+E5N4rVHjQOYGRV75nC2mAMJh9GIbixuxLnWjj77ag==} + '@push.rocks/smartarray@1.1.0': resolution: {integrity: sha512-b5YgBmUdglOJH8zeUf2ZWdPCoqySgwvkycRi2BhA9zVZHkpASh39Ej0q0fxFJetlUVyYqGfVoMVjbVrLFfFV7g==} @@ -868,6 +874,9 @@ packages: '@push.rocks/smartdelay@3.0.5': resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} + '@push.rocks/smartdeno@1.2.0': + resolution: {integrity: sha512-6S1plCaMUVOZiRSflfoz9Fqk9phACCuKmc7Z6SfTvfl+p9VcPUmewKgaa/0QiLOpiI6ksfxdfmkS5Rw5HpYeIA==} + '@push.rocks/smartdns@7.6.1': resolution: {integrity: sha512-nnP5+A2GOt0WsHrYhtKERmjdEHUchc+QbCCBEqlyeQTn+mNfx2WZvKVI1DFRJt8lamvzxP6Hr/BSe3WHdh4Snw==} @@ -5161,6 +5170,26 @@ snapshots: - react-native-b4a - supports-color + '@push.rocks/smartarchive@5.0.1': + dependencies: + '@push.rocks/smartdelay': 3.0.5 + '@push.rocks/smartfile': 13.1.0 + '@push.rocks/smartpath': 6.0.0 + '@push.rocks/smartpromise': 4.2.3 + '@push.rocks/smartrequest': 4.4.2 + '@push.rocks/smartrx': 3.0.10 + '@push.rocks/smartstream': 3.2.5 + '@push.rocks/smartunique': 3.0.9 + '@push.rocks/smarturl': 3.1.0 + '@types/tar-stream': 3.1.4 + fflate: 0.8.2 + file-type: 21.1.1 + tar-stream: 3.1.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color + '@push.rocks/smartarray@1.1.0': {} '@push.rocks/smartbrowser@2.0.8(typescript@5.9.3)': @@ -5284,6 +5313,18 @@ snapshots: dependencies: '@push.rocks/smartpromise': 4.2.3 + '@push.rocks/smartdeno@1.2.0': + dependencies: + '@push.rocks/smartarchive': 5.0.1 + '@push.rocks/smartfs': 1.2.0 + '@push.rocks/smartpath': 6.0.0 + '@push.rocks/smartshell': 3.3.0 + '@push.rocks/smartunique': 3.0.9 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color + '@push.rocks/smartdns@7.6.1': dependencies: '@push.rocks/smartdelay': 3.0.5 diff --git a/readme.md b/readme.md index 5a1799b..d9cd610 100644 --- a/readme.md +++ b/readme.md @@ -19,26 +19,33 @@ This design ensures safe tool use through AI-based policy evaluation rather than ## Architecture -``` -User Task + Guardian Policy Prompt - | - +---------------------------------------+ - | DualAgentOrchestrator | - | | - | +--------+ +------------+ | - | | Driver |-------> | Guardian | | - | | Agent | tool | Agent | | - | | | call | | | - | | Reason |<--------| Evaluate | | - | | + Plan | approve | against | | - | +--------+ /reject | policy | | - | | +feedback+-----------+ | - | v (if approved) | - | +-----------------------------------+| - | | Standard Tools || - | | Filesystem | HTTP | Shell | Browser| - | +-----------------------------------+| - +---------------------------------------+ +```mermaid +flowchart TB + subgraph Input + Task["User Task"] + Policy["Guardian Policy Prompt"] + end + + subgraph Orchestrator["DualAgentOrchestrator"] + Driver["Driver Agent
Reason + Plan"] + Guardian["Guardian Agent
Evaluate against policy"] + + Driver -->|"tool call proposal"| Guardian + Guardian -->|"approve / reject + feedback"| Driver + end + + subgraph Tools["Standard Tools"] + FS["Filesystem"] + HTTP["HTTP"] + Shell["Shell"] + Browser["Browser"] + Deno["Deno"] + end + + Task --> Orchestrator + Policy --> Guardian + Driver -->|"execute
(if approved)"| Tools + Tools -->|"result"| Driver ``` ## Quick Start @@ -139,6 +146,46 @@ Web page interaction using `@push.rocks/smartbrowser` (Puppeteer-based). ``` +### DenoTool +Execute TypeScript/JavaScript code in a sandboxed Deno environment using `@push.rocks/smartdeno`. + +**Actions**: `execute`, `executeWithResult` + +**Permissions**: `all`, `env`, `ffi`, `hrtime`, `net`, `read`, `run`, `sys`, `write` + +By default, code runs fully sandboxed with no permissions. Permissions must be explicitly requested. + +```typescript +// Simple code execution + + deno + execute + {"code": "console.log('Hello from Deno!')"} + Running a simple script to verify the environment + + +// Code with network permission + + deno + execute + { + "code": "const resp = await fetch('https://api.example.com/data'); console.log(await resp.json());", + "permissions": ["net"] + } + Fetching data from API using Deno's fetch + + +// Execute and parse JSON result + + deno + executeWithResult + { + "code": "const result = { sum: 2 + 2, date: new Date().toISOString() }; console.log(JSON.stringify(result));" + } + Computing values and returning structured data + +``` + ## Guardian Policy Examples ### Strict Security Policy @@ -174,6 +221,27 @@ Always verify: `; ``` +### Deno Code Execution Policy +```typescript +const denoPolicy = ` +DENO CODE EXECUTION POLICY: +- ONLY allow 'read' permission for files within the workspace +- REJECT 'all' permission unless explicitly justified for the task +- REJECT 'run' permission (subprocess execution) without specific justification +- REJECT code that attempts to: + - Access credentials or environment secrets (even with 'env' permission) + - Make network requests to internal/private IP ranges + - Write to system directories +- FLAG obfuscated or encoded code (base64, eval with dynamic strings) +- Prefer sandboxed execution (no permissions) when possible + +When evaluating code: +- Review the actual code content, not just permissions +- Consider what data the code could exfiltrate +- Verify network endpoints are legitimate public APIs +`; +``` + ## Configuration Options ```typescript diff --git a/test/test.ts b/test/test.ts index d6f4f2f..1828362 100644 --- a/test/test.ts +++ b/test/test.ts @@ -35,6 +35,10 @@ tap.test('should export BrowserTool class', async () => { expect(smartagent.BrowserTool).toBeTypeOf('function'); }); +tap.test('should export DenoTool class', async () => { + expect(smartagent.DenoTool).toBeTypeOf('function'); +}); + // Test tool instantiation tap.test('should be able to instantiate FilesystemTool', async () => { const fsTool = new smartagent.FilesystemTool(); @@ -61,6 +65,12 @@ tap.test('should be able to instantiate BrowserTool', async () => { expect(browserTool.actions).toBeTypeOf('object'); }); +tap.test('should be able to instantiate DenoTool', async () => { + const denoTool = new smartagent.DenoTool(); + expect(denoTool.name).toEqual('deno'); + expect(denoTool.actions).toBeTypeOf('object'); +}); + // Test tool descriptions tap.test('FilesystemTool should have required actions', async () => { const fsTool = new smartagent.FilesystemTool(); @@ -97,6 +107,13 @@ tap.test('BrowserTool should have required actions', async () => { expect(actionNames).toContain('getPageContent'); }); +tap.test('DenoTool should have required actions', async () => { + const denoTool = new smartagent.DenoTool(); + const actionNames = denoTool.actions.map((a) => a.name); + expect(actionNames).toContain('execute'); + expect(actionNames).toContain('executeWithResult'); +}); + // Test getCallSummary tap.test('FilesystemTool should generate call summaries', async () => { const fsTool = new smartagent.FilesystemTool(); @@ -112,4 +129,22 @@ tap.test('HttpTool should generate call summaries', async () => { expect(summary).toInclude('example.com'); }); +tap.test('DenoTool should generate call summaries', async () => { + const denoTool = new smartagent.DenoTool(); + const summary = denoTool.getCallSummary('execute', { code: 'console.log("hello");' }); + expect(summary).toBeTypeOf('string'); + expect(summary).toInclude('sandboxed'); +}); + +tap.test('DenoTool should show permissions in call summary', async () => { + const denoTool = new smartagent.DenoTool(); + const summary = denoTool.getCallSummary('execute', { + code: 'console.log("hello");', + permissions: ['net', 'read'] + }); + expect(summary).toBeTypeOf('string'); + expect(summary).toInclude('permissions'); + expect(summary).toInclude('net'); +}); + export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index df7d3bd..e6fe23e 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartagent', - version: '1.0.2', + version: '1.1.0', description: 'an agentic framework built on top of @push.rocks/smartai' } diff --git a/ts/index.ts b/ts/index.ts index 141aff3..7e05ae1 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -15,6 +15,7 @@ export { FilesystemTool } from './smartagent.tools.filesystem.js'; export { HttpTool } from './smartagent.tools.http.js'; export { ShellTool } from './smartagent.tools.shell.js'; export { BrowserTool } from './smartagent.tools.browser.js'; +export { DenoTool, type TDenoPermission } from './smartagent.tools.deno.js'; // Export all interfaces export * from './smartagent.interfaces.js'; diff --git a/ts/plugins.ts b/ts/plugins.ts index 6103d77..bbca2ad 100644 --- a/ts/plugins.ts +++ b/ts/plugins.ts @@ -1,5 +1,6 @@ // @push.rocks scope import * as smartai from '@push.rocks/smartai'; +import * as smartdeno from '@push.rocks/smartdeno'; import * as smartfs from '@push.rocks/smartfs'; import * as smartrequest from '@push.rocks/smartrequest'; import * as smartbrowser from '@push.rocks/smartbrowser'; @@ -7,6 +8,7 @@ import * as smartshell from '@push.rocks/smartshell'; export { smartai, + smartdeno, smartfs, smartrequest, smartbrowser, diff --git a/ts/smartagent.classes.dualagent.ts b/ts/smartagent.classes.dualagent.ts index cc20722..e02be61 100644 --- a/ts/smartagent.classes.dualagent.ts +++ b/ts/smartagent.classes.dualagent.ts @@ -7,6 +7,7 @@ import { FilesystemTool } from './smartagent.tools.filesystem.js'; import { HttpTool } from './smartagent.tools.http.js'; import { ShellTool } from './smartagent.tools.shell.js'; import { BrowserTool } from './smartagent.tools.browser.js'; +import { DenoTool } from './smartagent.tools.deno.js'; /** * DualAgentOrchestrator - Coordinates Driver and Guardian agents @@ -87,6 +88,7 @@ export class DualAgentOrchestrator { new HttpTool(), new ShellTool(), new BrowserTool(), + new DenoTool(), ]; for (const tool of standardTools) { diff --git a/ts/smartagent.tools.deno.ts b/ts/smartagent.tools.deno.ts new file mode 100644 index 0000000..1320fde --- /dev/null +++ b/ts/smartagent.tools.deno.ts @@ -0,0 +1,191 @@ +import * as plugins from './plugins.js'; +import * as interfaces from './smartagent.interfaces.js'; +import { BaseToolWrapper } from './smartagent.tools.base.js'; + +/** + * Deno permission types for sandboxed code execution + */ +export type TDenoPermission = + | 'all' + | 'env' + | 'ffi' + | 'hrtime' + | 'net' + | 'read' + | 'run' + | 'sys' + | 'write'; + +/** + * Deno tool for executing TypeScript/JavaScript code in a sandboxed environment + * Wraps @push.rocks/smartdeno + */ +export class DenoTool extends BaseToolWrapper { + public name = 'deno'; + public description = + 'Execute TypeScript/JavaScript code in a sandboxed Deno environment with fine-grained permission control'; + + public actions: interfaces.IToolAction[] = [ + { + name: 'execute', + description: + 'Execute TypeScript/JavaScript code and return stdout/stderr. Code runs in Deno sandbox with specified permissions.', + parameters: { + type: 'object', + properties: { + code: { + type: 'string', + description: 'TypeScript/JavaScript code to execute', + }, + permissions: { + type: 'array', + items: { + type: 'string', + enum: ['all', 'env', 'ffi', 'hrtime', 'net', 'read', 'run', 'sys', 'write'], + }, + description: + 'Deno permissions to grant. Default: none (fully sandboxed). Options: all, env, net, read, write, run, sys, ffi, hrtime', + }, + }, + required: ['code'], + }, + }, + { + name: 'executeWithResult', + description: + 'Execute code that outputs JSON on the last line of stdout. The JSON is parsed and returned as the result.', + parameters: { + type: 'object', + properties: { + code: { + type: 'string', + description: + 'Code that console.logs a JSON value on the final line. This JSON will be parsed and returned.', + }, + permissions: { + type: 'array', + items: { + type: 'string', + enum: ['all', 'env', 'ffi', 'hrtime', 'net', 'read', 'run', 'sys', 'write'], + }, + description: 'Deno permissions to grant', + }, + }, + required: ['code'], + }, + }, + ]; + + private smartdeno!: plugins.smartdeno.SmartDeno; + + public async initialize(): Promise { + this.smartdeno = new plugins.smartdeno.SmartDeno(); + await this.smartdeno.start(); + this.isInitialized = true; + } + + public async cleanup(): Promise { + if (this.smartdeno) { + await this.smartdeno.stop(); + } + this.isInitialized = false; + } + + public async execute( + action: string, + params: Record + ): Promise { + this.validateAction(action); + this.ensureInitialized(); + + try { + const code = params.code as string; + const permissions = (params.permissions as TDenoPermission[]) || []; + + // Execute the script + const result = await this.smartdeno.executeScript(code, { + permissions, + }); + + switch (action) { + case 'execute': { + return { + success: result.exitCode === 0, + result: { + exitCode: result.exitCode, + stdout: result.stdout, + stderr: result.stderr, + permissions, + }, + }; + } + + case 'executeWithResult': { + if (result.exitCode !== 0) { + return { + success: false, + error: `Script failed with exit code ${result.exitCode}: ${result.stderr}`, + }; + } + + // Parse the last line of stdout as JSON + const lines = result.stdout.trim().split('\n'); + const lastLine = lines[lines.length - 1]; + + try { + const parsedResult = JSON.parse(lastLine); + return { + success: true, + result: { + parsed: parsedResult, + stdout: result.stdout, + stderr: result.stderr, + }, + }; + } catch (parseError) { + return { + success: false, + error: `Failed to parse JSON from last line of output: ${lastLine}`, + }; + } + } + + default: + return { + success: false, + error: `Unknown action: ${action}`, + }; + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + public getCallSummary(action: string, params: Record): string { + const code = params.code as string; + const permissions = (params.permissions as string[]) || []; + + // Create a preview of the code (first 100 chars) + const codePreview = code.length > 100 ? code.substring(0, 100) + '...' : code; + // Escape newlines for single-line display + const cleanPreview = codePreview.replace(/\n/g, '\\n'); + + const permissionInfo = permissions.length > 0 + ? ` [permissions: ${permissions.join(', ')}]` + : ' [sandboxed - no permissions]'; + + switch (action) { + case 'execute': + return `Execute Deno code${permissionInfo}: "${cleanPreview}"`; + + case 'executeWithResult': + return `Execute Deno code and parse JSON result${permissionInfo}: "${cleanPreview}"`; + + default: + return `Unknown action: ${action}`; + } + } +}