feat(deno): Add Deno tool and smartdeno integration; export and register DenoTool; update docs and tests
This commit is contained in:
11
changelog.md
11
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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
41
pnpm-lock.yaml
generated
41
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
108
readme.md
108
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<br/><i>Reason + Plan</i>"]
|
||||
Guardian["Guardian Agent<br/><i>Evaluate against policy</i>"]
|
||||
|
||||
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<br/>(if approved)"| Tools
|
||||
Tools -->|"result"| Driver
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
@@ -139,6 +146,46 @@ Web page interaction using `@push.rocks/smartbrowser` (Puppeteer-based).
|
||||
</tool_call>
|
||||
```
|
||||
|
||||
### 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
|
||||
<tool_call>
|
||||
<tool>deno</tool>
|
||||
<action>execute</action>
|
||||
<params>{"code": "console.log('Hello from Deno!')"}</params>
|
||||
<reasoning>Running a simple script to verify the environment</reasoning>
|
||||
</tool_call>
|
||||
|
||||
// Code with network permission
|
||||
<tool_call>
|
||||
<tool>deno</tool>
|
||||
<action>execute</action>
|
||||
<params>{
|
||||
"code": "const resp = await fetch('https://api.example.com/data'); console.log(await resp.json());",
|
||||
"permissions": ["net"]
|
||||
}</params>
|
||||
<reasoning>Fetching data from API using Deno's fetch</reasoning>
|
||||
</tool_call>
|
||||
|
||||
// Execute and parse JSON result
|
||||
<tool_call>
|
||||
<tool>deno</tool>
|
||||
<action>executeWithResult</action>
|
||||
<params>{
|
||||
"code": "const result = { sum: 2 + 2, date: new Date().toISOString() }; console.log(JSON.stringify(result));"
|
||||
}</params>
|
||||
<reasoning>Computing values and returning structured data</reasoning>
|
||||
</tool_call>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
35
test/test.ts
35
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();
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
191
ts/smartagent.tools.deno.ts
Normal file
191
ts/smartagent.tools.deno.ts
Normal file
@@ -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<void> {
|
||||
this.smartdeno = new plugins.smartdeno.SmartDeno();
|
||||
await this.smartdeno.start();
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
public async cleanup(): Promise<void> {
|
||||
if (this.smartdeno) {
|
||||
await this.smartdeno.stop();
|
||||
}
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
public async execute(
|
||||
action: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<interfaces.IToolExecutionResult> {
|
||||
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, unknown>): 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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user