@mindburn/helm-mastra 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +323 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +379 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @mindburn/helm-mastra
|
|
2
|
+
|
|
3
|
+
HELM governance adapter for [Mastra](https://mastra.ai) agent framework with native Daytona sandbox integration.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Wraps Mastra's Daytona sandbox with HELM governance:
|
|
8
|
+
|
|
9
|
+
1. Every sandbox `exec` is evaluated against HELM policy first
|
|
10
|
+
2. Denied commands never reach the sandbox
|
|
11
|
+
3. Receipts form a deterministic proof chain
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { HelmMastraSandbox } from "@mindburn/helm-mastra";
|
|
17
|
+
|
|
18
|
+
const sandbox = new HelmMastraSandbox({
|
|
19
|
+
baseUrl: "http://localhost:8080",
|
|
20
|
+
daytonaApiKey: process.env.DAYTONA_API_KEY,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = await sandbox.exec({
|
|
24
|
+
command: ["python3", "-c", 'print("governed exec")'],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
console.log(result.stdout); // "governed exec\n"
|
|
28
|
+
console.log(result.receipt.requestHash); // "sha256:..."
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
| Option | Default | Description |
|
|
34
|
+
| ----------------- | ------------------------ | ------------------------ |
|
|
35
|
+
| `baseUrl` | required | HELM kernel URL |
|
|
36
|
+
| `daytonaApiKey` | required | Daytona API key |
|
|
37
|
+
| `daytonaUrl` | `https://api.daytona.io` | Daytona API URL |
|
|
38
|
+
| `failClosed` | `true` | Deny on HELM errors |
|
|
39
|
+
| `defaultLanguage` | `python3` | Default exec language |
|
|
40
|
+
| `execTimeout` | `30000` | Per-command timeout (ms) |
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
Apache-2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mindburn/helm-mastra
|
|
3
|
+
*
|
|
4
|
+
* HELM governance adapter for Mastra agent framework.
|
|
5
|
+
* Wraps Mastra's Daytona sandbox integration with HELM governance,
|
|
6
|
+
* providing policy enforcement, receipt chains, and sandbox preflight.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* Mastra tool → HelmMastraSandbox → HELM governance → Daytona sandbox
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { HelmMastraSandbox } from '@mindburn/helm-mastra';
|
|
14
|
+
*
|
|
15
|
+
* const sandbox = new HelmMastraSandbox({
|
|
16
|
+
* helmUrl: 'http://localhost:8080',
|
|
17
|
+
* daytonaApiKey: 'dtn-xxx',
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // Use within Mastra tools
|
|
21
|
+
* const result = await sandbox.exec({
|
|
22
|
+
* command: ['python3', '-c', 'print("hello")'],
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
import type { HelmClientConfig, Receipt } from '@mindburn/helm';
|
|
27
|
+
/** Configuration for the HELM Mastra sandbox adapter. */
|
|
28
|
+
export interface HelmMastraSandboxConfig extends HelmClientConfig {
|
|
29
|
+
/** Daytona API key for sandbox operations. */
|
|
30
|
+
daytonaApiKey: string;
|
|
31
|
+
/** Daytona API URL. Default: https://api.daytona.io */
|
|
32
|
+
daytonaUrl?: string;
|
|
33
|
+
/** If true, deny execution on governance errors (fail-closed). Default: true. */
|
|
34
|
+
failClosed?: boolean;
|
|
35
|
+
/** Default language for code execution. Default: 'python3'. */
|
|
36
|
+
defaultLanguage?: string;
|
|
37
|
+
/** Per-command timeout in milliseconds. Default: 30000. */
|
|
38
|
+
execTimeout?: number;
|
|
39
|
+
/** If true, collect receipts. Default: true. */
|
|
40
|
+
collectReceipts?: boolean;
|
|
41
|
+
/** Callback invoked after each successful execution with its receipt. */
|
|
42
|
+
onReceipt?: (receipt: SandboxReceipt) => void;
|
|
43
|
+
/** Callback invoked when execution is denied. */
|
|
44
|
+
onDeny?: (denial: SandboxDenial) => void;
|
|
45
|
+
}
|
|
46
|
+
/** A receipt for a governed sandbox execution. */
|
|
47
|
+
export interface SandboxReceipt {
|
|
48
|
+
command: string[];
|
|
49
|
+
receipt: Receipt;
|
|
50
|
+
requestHash: string;
|
|
51
|
+
outputHash: string;
|
|
52
|
+
exitCode: number;
|
|
53
|
+
durationMs: number;
|
|
54
|
+
}
|
|
55
|
+
/** Details of a denied sandbox execution. */
|
|
56
|
+
export interface SandboxDenial {
|
|
57
|
+
command: string[];
|
|
58
|
+
reasonCode: string;
|
|
59
|
+
message: string;
|
|
60
|
+
}
|
|
61
|
+
/** Sandbox execution request. */
|
|
62
|
+
export interface ExecRequest {
|
|
63
|
+
command: string[];
|
|
64
|
+
env?: Record<string, string>;
|
|
65
|
+
workDir?: string;
|
|
66
|
+
timeout?: number;
|
|
67
|
+
}
|
|
68
|
+
/** Sandbox execution result. */
|
|
69
|
+
export interface ExecResult {
|
|
70
|
+
exitCode: number;
|
|
71
|
+
stdout: string;
|
|
72
|
+
stderr: string;
|
|
73
|
+
durationMs: number;
|
|
74
|
+
timedOut: boolean;
|
|
75
|
+
receipt: SandboxReceipt;
|
|
76
|
+
}
|
|
77
|
+
/** File operations. */
|
|
78
|
+
export interface SandboxFile {
|
|
79
|
+
path: string;
|
|
80
|
+
content: string;
|
|
81
|
+
}
|
|
82
|
+
export declare class HelmSandboxDenyError extends Error {
|
|
83
|
+
readonly denial: SandboxDenial;
|
|
84
|
+
constructor(denial: SandboxDenial);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* HelmMastraSandbox wraps Mastra's Daytona sandbox with HELM governance.
|
|
88
|
+
*
|
|
89
|
+
* Each sandbox operation goes through HELM's governance plane:
|
|
90
|
+
* 1. HELM evaluates the tool call intent against policy
|
|
91
|
+
* 2. If approved, the command is forwarded to Daytona
|
|
92
|
+
* 3. A receipt is produced for the execution
|
|
93
|
+
*/
|
|
94
|
+
export declare class HelmMastraSandbox {
|
|
95
|
+
private readonly helmClient;
|
|
96
|
+
private readonly daytonaUrl;
|
|
97
|
+
private readonly daytonaApiKey;
|
|
98
|
+
private readonly failClosed;
|
|
99
|
+
private readonly defaultLanguage;
|
|
100
|
+
private readonly execTimeout;
|
|
101
|
+
private readonly collectReceipts;
|
|
102
|
+
private readonly onReceipt?;
|
|
103
|
+
private readonly onDeny?;
|
|
104
|
+
private readonly receipts;
|
|
105
|
+
private sandboxId;
|
|
106
|
+
private lastLamportClock;
|
|
107
|
+
constructor(config: HelmMastraSandboxConfig);
|
|
108
|
+
/**
|
|
109
|
+
* Initialize the sandbox. Must be called before exec.
|
|
110
|
+
*/
|
|
111
|
+
init(): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Execute a command in the sandbox with HELM governance.
|
|
114
|
+
*/
|
|
115
|
+
exec(req: ExecRequest): Promise<ExecResult>;
|
|
116
|
+
/**
|
|
117
|
+
* Write a file to the sandbox.
|
|
118
|
+
*/
|
|
119
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Read a file from the sandbox.
|
|
122
|
+
*/
|
|
123
|
+
readFile(path: string): Promise<string>;
|
|
124
|
+
/**
|
|
125
|
+
* Destroy the sandbox.
|
|
126
|
+
*/
|
|
127
|
+
destroy(): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Get collected receipts.
|
|
130
|
+
*/
|
|
131
|
+
getReceipts(): ReadonlyArray<SandboxReceipt>;
|
|
132
|
+
/**
|
|
133
|
+
* Clear collected receipts.
|
|
134
|
+
*/
|
|
135
|
+
clearReceipts(): void;
|
|
136
|
+
private static resolveReceiptStatus;
|
|
137
|
+
private nextLamportClock;
|
|
138
|
+
}
|
|
139
|
+
export default HelmMastraSandbox;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mindburn/helm-mastra
|
|
3
|
+
*
|
|
4
|
+
* HELM governance adapter for Mastra agent framework.
|
|
5
|
+
* Wraps Mastra's Daytona sandbox integration with HELM governance,
|
|
6
|
+
* providing policy enforcement, receipt chains, and sandbox preflight.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* Mastra tool → HelmMastraSandbox → HELM governance → Daytona sandbox
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { HelmMastraSandbox } from '@mindburn/helm-mastra';
|
|
14
|
+
*
|
|
15
|
+
* const sandbox = new HelmMastraSandbox({
|
|
16
|
+
* helmUrl: 'http://localhost:8080',
|
|
17
|
+
* daytonaApiKey: 'dtn-xxx',
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // Use within Mastra tools
|
|
21
|
+
* const result = await sandbox.exec({
|
|
22
|
+
* command: ['python3', '-c', 'print("hello")'],
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
import { HelmClient, HelmApiError } from '@mindburn/helm';
|
|
27
|
+
import { createHash } from 'node:crypto';
|
|
28
|
+
// ── Errors ──────────────────────────────────────────────────────
|
|
29
|
+
export class HelmSandboxDenyError extends Error {
|
|
30
|
+
denial;
|
|
31
|
+
constructor(denial) {
|
|
32
|
+
super(`HELM denied sandbox exec: ${denial.reasonCode} — ${denial.message}`);
|
|
33
|
+
this.name = 'HelmSandboxDenyError';
|
|
34
|
+
this.denial = denial;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ── Sandbox ─────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* HelmMastraSandbox wraps Mastra's Daytona sandbox with HELM governance.
|
|
40
|
+
*
|
|
41
|
+
* Each sandbox operation goes through HELM's governance plane:
|
|
42
|
+
* 1. HELM evaluates the tool call intent against policy
|
|
43
|
+
* 2. If approved, the command is forwarded to Daytona
|
|
44
|
+
* 3. A receipt is produced for the execution
|
|
45
|
+
*/
|
|
46
|
+
export class HelmMastraSandbox {
|
|
47
|
+
helmClient;
|
|
48
|
+
daytonaUrl;
|
|
49
|
+
daytonaApiKey;
|
|
50
|
+
failClosed;
|
|
51
|
+
defaultLanguage;
|
|
52
|
+
execTimeout;
|
|
53
|
+
collectReceipts;
|
|
54
|
+
onReceipt;
|
|
55
|
+
onDeny;
|
|
56
|
+
receipts = [];
|
|
57
|
+
sandboxId = null;
|
|
58
|
+
lastLamportClock = -1;
|
|
59
|
+
constructor(config) {
|
|
60
|
+
this.helmClient = new HelmClient(config);
|
|
61
|
+
this.daytonaUrl = config.daytonaUrl ?? 'https://api.daytona.io';
|
|
62
|
+
this.daytonaApiKey = config.daytonaApiKey;
|
|
63
|
+
this.failClosed = config.failClosed ?? true;
|
|
64
|
+
this.defaultLanguage = config.defaultLanguage ?? 'python3';
|
|
65
|
+
this.execTimeout = config.execTimeout ?? 30_000;
|
|
66
|
+
this.collectReceipts = config.collectReceipts ?? true;
|
|
67
|
+
this.onReceipt = config.onReceipt;
|
|
68
|
+
this.onDeny = config.onDeny;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Initialize the sandbox. Must be called before exec.
|
|
72
|
+
*/
|
|
73
|
+
async init() {
|
|
74
|
+
// Create a Daytona sandbox.
|
|
75
|
+
const resp = await fetch(`${this.daytonaUrl}/sandbox`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
Authorization: `Bearer ${this.daytonaApiKey}`,
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
language: this.defaultLanguage,
|
|
83
|
+
timeout: Math.floor(this.execTimeout / 1000),
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
if (!resp.ok) {
|
|
87
|
+
throw new Error(`Daytona sandbox creation failed: ${resp.status}`);
|
|
88
|
+
}
|
|
89
|
+
const data = (await resp.json());
|
|
90
|
+
this.sandboxId = data.sandboxId;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Execute a command in the sandbox with HELM governance.
|
|
94
|
+
*/
|
|
95
|
+
async exec(req) {
|
|
96
|
+
if (!this.sandboxId) {
|
|
97
|
+
await this.init();
|
|
98
|
+
}
|
|
99
|
+
const startMs = Date.now();
|
|
100
|
+
// Step 1: HELM governance evaluation.
|
|
101
|
+
// Use chatCompletionsWithReceipt to get kernel-issued governance metadata.
|
|
102
|
+
let governanceStatus = '';
|
|
103
|
+
let governanceReasonCode = '';
|
|
104
|
+
let kernelReceiptId = '';
|
|
105
|
+
let kernelDecisionId = '';
|
|
106
|
+
let kernelProofGraphNode = '';
|
|
107
|
+
let kernelLamportClock = 0;
|
|
108
|
+
let kernelSignature = '';
|
|
109
|
+
let kernelOutputHash = '';
|
|
110
|
+
let governanceEvaluated = false;
|
|
111
|
+
try {
|
|
112
|
+
const { response: governanceResp, governance } = await this.helmClient.chatCompletionsWithReceipt({
|
|
113
|
+
model: 'helm-governance',
|
|
114
|
+
messages: [
|
|
115
|
+
{
|
|
116
|
+
role: 'user',
|
|
117
|
+
content: JSON.stringify({
|
|
118
|
+
type: 'sandbox_exec_intent',
|
|
119
|
+
provider: 'daytona',
|
|
120
|
+
sandbox_id: this.sandboxId,
|
|
121
|
+
command: req.command,
|
|
122
|
+
env: req.env,
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
tools: [
|
|
127
|
+
{
|
|
128
|
+
type: 'function',
|
|
129
|
+
function: {
|
|
130
|
+
name: 'sandbox_exec',
|
|
131
|
+
description: 'Execute command in Daytona sandbox',
|
|
132
|
+
parameters: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
command: { type: 'array', items: { type: 'string' } },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
// Extract kernel governance metadata
|
|
143
|
+
governanceStatus = governance.status;
|
|
144
|
+
governanceReasonCode = governance.reasonCode;
|
|
145
|
+
kernelReceiptId = governance.receiptId;
|
|
146
|
+
kernelDecisionId = governance.decisionId;
|
|
147
|
+
kernelProofGraphNode = governance.proofGraphNode;
|
|
148
|
+
kernelLamportClock = governance.lamportClock;
|
|
149
|
+
kernelSignature = governance.signature;
|
|
150
|
+
kernelOutputHash = governance.outputHash;
|
|
151
|
+
const choice = governanceResp.choices?.[0];
|
|
152
|
+
const kernelDenied = governanceStatus === 'DENIED' || governanceStatus === 'PEP_VALIDATION_FAILED';
|
|
153
|
+
if (kernelDenied || (!choice || (choice.finish_reason === 'stop' && !choice.message?.tool_calls))) {
|
|
154
|
+
const denial = {
|
|
155
|
+
command: req.command,
|
|
156
|
+
reasonCode: governanceReasonCode || 'DENY_POLICY_VIOLATION',
|
|
157
|
+
message: choice?.message?.content ?? 'Sandbox exec denied by HELM governance',
|
|
158
|
+
};
|
|
159
|
+
this.onDeny?.(denial);
|
|
160
|
+
throw new HelmSandboxDenyError(denial);
|
|
161
|
+
}
|
|
162
|
+
governanceEvaluated = true;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof HelmSandboxDenyError)
|
|
166
|
+
throw error;
|
|
167
|
+
if (error instanceof HelmApiError) {
|
|
168
|
+
const denial = {
|
|
169
|
+
command: req.command,
|
|
170
|
+
reasonCode: error.reasonCode,
|
|
171
|
+
message: error.message,
|
|
172
|
+
};
|
|
173
|
+
this.onDeny?.(denial);
|
|
174
|
+
if (this.failClosed)
|
|
175
|
+
throw new HelmSandboxDenyError(denial);
|
|
176
|
+
governanceStatus = 'PENDING';
|
|
177
|
+
governanceReasonCode = error.reasonCode;
|
|
178
|
+
}
|
|
179
|
+
if (this.failClosed)
|
|
180
|
+
throw error;
|
|
181
|
+
governanceStatus = governanceStatus || 'PENDING';
|
|
182
|
+
governanceReasonCode = governanceReasonCode || 'ERROR_INTERNAL';
|
|
183
|
+
}
|
|
184
|
+
// Step 2: Execute on Daytona.
|
|
185
|
+
const cmd = req.command.join(' ');
|
|
186
|
+
const execResp = await fetch(`${this.daytonaUrl}/sandbox/${this.sandboxId}/process/execute`, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: {
|
|
189
|
+
'Content-Type': 'application/json',
|
|
190
|
+
Authorization: `Bearer ${this.daytonaApiKey}`,
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
command: cmd,
|
|
194
|
+
env: req.env,
|
|
195
|
+
cwd: req.workDir,
|
|
196
|
+
timeout: req.timeout ? Math.floor(req.timeout / 1000) : Math.floor(this.execTimeout / 1000),
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
if (!execResp.ok) {
|
|
200
|
+
throw new Error(`Daytona exec failed: ${execResp.status}`);
|
|
201
|
+
}
|
|
202
|
+
const result = (await execResp.json());
|
|
203
|
+
// Step 3: Build receipt from kernel-issued data (NOT fabricated).
|
|
204
|
+
const durationMs = Date.now() - startMs;
|
|
205
|
+
const requestHash = 'sha256:' + createHash('sha256').update(JSON.stringify(req)).digest('hex');
|
|
206
|
+
const outputHash = 'sha256:' + createHash('sha256').update(result.output || '').digest('hex');
|
|
207
|
+
const lamportClock = this.nextLamportClock(kernelLamportClock);
|
|
208
|
+
const receiptStatus = HelmMastraSandbox.resolveReceiptStatus(governanceStatus, governanceEvaluated);
|
|
209
|
+
const receiptToken = `${requestHash.slice(7, 19)}-${lamportClock}`;
|
|
210
|
+
const receipt = {
|
|
211
|
+
command: req.command,
|
|
212
|
+
receipt: {
|
|
213
|
+
receipt_id: kernelReceiptId || `mastra-${receiptToken}`,
|
|
214
|
+
decision_id: kernelDecisionId || `decision-${receiptToken}`,
|
|
215
|
+
effect_id: kernelProofGraphNode || `exec-${receiptToken}`,
|
|
216
|
+
status: receiptStatus,
|
|
217
|
+
reason_code: governanceReasonCode || (governanceEvaluated ? 'ALLOW' : 'ERROR_INTERNAL'),
|
|
218
|
+
output_hash: kernelOutputHash || outputHash,
|
|
219
|
+
blob_hash: '',
|
|
220
|
+
prev_hash: '',
|
|
221
|
+
lamport_clock: lamportClock,
|
|
222
|
+
signature: kernelSignature || '',
|
|
223
|
+
timestamp: new Date().toISOString(),
|
|
224
|
+
principal: governanceEvaluated ? 'helm-kernel' : 'helm-fail-open',
|
|
225
|
+
},
|
|
226
|
+
requestHash,
|
|
227
|
+
outputHash,
|
|
228
|
+
exitCode: result.exitCode,
|
|
229
|
+
durationMs,
|
|
230
|
+
};
|
|
231
|
+
if (this.collectReceipts) {
|
|
232
|
+
this.receipts.push(receipt);
|
|
233
|
+
}
|
|
234
|
+
this.onReceipt?.(receipt);
|
|
235
|
+
return {
|
|
236
|
+
exitCode: result.exitCode,
|
|
237
|
+
stdout: result.output,
|
|
238
|
+
stderr: result.errors,
|
|
239
|
+
durationMs: result.durationMs,
|
|
240
|
+
timedOut: result.timedOut,
|
|
241
|
+
receipt,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Write a file to the sandbox.
|
|
246
|
+
*/
|
|
247
|
+
async writeFile(path, content) {
|
|
248
|
+
if (!this.sandboxId)
|
|
249
|
+
await this.init();
|
|
250
|
+
const resp = await fetch(`${this.daytonaUrl}/sandbox/${this.sandboxId}/filesystem?path=${encodeURIComponent(path)}`, {
|
|
251
|
+
method: 'PUT',
|
|
252
|
+
headers: {
|
|
253
|
+
'Content-Type': 'application/octet-stream',
|
|
254
|
+
Authorization: `Bearer ${this.daytonaApiKey}`,
|
|
255
|
+
},
|
|
256
|
+
body: content,
|
|
257
|
+
});
|
|
258
|
+
if (!resp.ok) {
|
|
259
|
+
throw new Error(`Daytona write file failed: ${resp.status}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Read a file from the sandbox.
|
|
264
|
+
*/
|
|
265
|
+
async readFile(path) {
|
|
266
|
+
if (!this.sandboxId)
|
|
267
|
+
await this.init();
|
|
268
|
+
const resp = await fetch(`${this.daytonaUrl}/sandbox/${this.sandboxId}/filesystem?path=${encodeURIComponent(path)}`, {
|
|
269
|
+
method: 'GET',
|
|
270
|
+
headers: {
|
|
271
|
+
Authorization: `Bearer ${this.daytonaApiKey}`,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
if (!resp.ok) {
|
|
275
|
+
throw new Error(`Daytona read file failed: ${resp.status}`);
|
|
276
|
+
}
|
|
277
|
+
return resp.text();
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Destroy the sandbox.
|
|
281
|
+
*/
|
|
282
|
+
async destroy() {
|
|
283
|
+
if (!this.sandboxId)
|
|
284
|
+
return;
|
|
285
|
+
await fetch(`${this.daytonaUrl}/sandbox/${this.sandboxId}`, {
|
|
286
|
+
method: 'DELETE',
|
|
287
|
+
headers: { Authorization: `Bearer ${this.daytonaApiKey}` },
|
|
288
|
+
});
|
|
289
|
+
this.sandboxId = null;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Get collected receipts.
|
|
293
|
+
*/
|
|
294
|
+
getReceipts() {
|
|
295
|
+
return this.receipts;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Clear collected receipts.
|
|
299
|
+
*/
|
|
300
|
+
clearReceipts() {
|
|
301
|
+
this.receipts.length = 0;
|
|
302
|
+
}
|
|
303
|
+
static resolveReceiptStatus(governanceStatus, governanceEvaluated) {
|
|
304
|
+
if (!governanceEvaluated) {
|
|
305
|
+
return 'PENDING';
|
|
306
|
+
}
|
|
307
|
+
if (governanceStatus === 'DENIED' || governanceStatus === 'PEP_VALIDATION_FAILED') {
|
|
308
|
+
return 'DENIED';
|
|
309
|
+
}
|
|
310
|
+
if (governanceStatus === 'PENDING') {
|
|
311
|
+
return 'PENDING';
|
|
312
|
+
}
|
|
313
|
+
return 'APPROVED';
|
|
314
|
+
}
|
|
315
|
+
nextLamportClock(kernelLamportClock) {
|
|
316
|
+
const nextLamportClock = kernelLamportClock > this.lastLamportClock
|
|
317
|
+
? kernelLamportClock
|
|
318
|
+
: this.lastLamportClock + 1;
|
|
319
|
+
this.lastLamportClock = nextLamportClock;
|
|
320
|
+
return nextLamportClock;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
export default HelmMastraSandbox;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { HelmMastraSandbox, HelmSandboxDenyError } from './index.js';
|
|
3
|
+
// ── Helpers ─────────────────────────────────────────────────────
|
|
4
|
+
function jsonResponse(body, status = 200) {
|
|
5
|
+
return new Response(JSON.stringify(body), {
|
|
6
|
+
status,
|
|
7
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function createSandboxResponse(sandboxId = 'sbx-1234') {
|
|
11
|
+
return jsonResponse({ sandboxId });
|
|
12
|
+
}
|
|
13
|
+
function execResponse(output = 'hello\n', exitCode = 0) {
|
|
14
|
+
return jsonResponse({
|
|
15
|
+
output,
|
|
16
|
+
errors: '',
|
|
17
|
+
exitCode,
|
|
18
|
+
timedOut: false,
|
|
19
|
+
durationMs: 42,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function governanceApproveResponse() {
|
|
23
|
+
return jsonResponse({
|
|
24
|
+
id: `chatcmpl-${Date.now()}`,
|
|
25
|
+
object: 'chat.completion',
|
|
26
|
+
created: Date.now(),
|
|
27
|
+
model: 'helm-governance',
|
|
28
|
+
choices: [
|
|
29
|
+
{
|
|
30
|
+
index: 0,
|
|
31
|
+
message: {
|
|
32
|
+
role: 'assistant',
|
|
33
|
+
content: null,
|
|
34
|
+
tool_calls: [
|
|
35
|
+
{
|
|
36
|
+
id: `call_${Date.now()}`,
|
|
37
|
+
type: 'function',
|
|
38
|
+
function: { name: 'sandbox_exec', arguments: '{}' },
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
finish_reason: 'tool_calls',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function governanceDenyResponse(reason = 'Sandbox exec denied by HELM governance') {
|
|
48
|
+
return jsonResponse({
|
|
49
|
+
id: `chatcmpl-${Date.now()}`,
|
|
50
|
+
object: 'chat.completion',
|
|
51
|
+
created: Date.now(),
|
|
52
|
+
model: 'helm-governance',
|
|
53
|
+
choices: [
|
|
54
|
+
{
|
|
55
|
+
index: 0,
|
|
56
|
+
message: { role: 'assistant', content: reason },
|
|
57
|
+
finish_reason: 'stop',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function helmApiErrorResponse(status, reasonCode = 'DENY_POLICY_VIOLATION', message = 'denied') {
|
|
63
|
+
return jsonResponse({
|
|
64
|
+
error: {
|
|
65
|
+
message,
|
|
66
|
+
type: 'permission_denied',
|
|
67
|
+
code: 'DENY',
|
|
68
|
+
reason_code: reasonCode,
|
|
69
|
+
},
|
|
70
|
+
}, status);
|
|
71
|
+
}
|
|
72
|
+
// ── Tests ───────────────────────────────────────────────────────
|
|
73
|
+
describe('HelmMastraSandbox', () => {
|
|
74
|
+
let fetchSpy;
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
fetchSpy = vi.fn();
|
|
77
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
78
|
+
});
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
vi.restoreAllMocks();
|
|
81
|
+
});
|
|
82
|
+
function makeSandbox(overrides = {}) {
|
|
83
|
+
return new HelmMastraSandbox({
|
|
84
|
+
baseUrl: 'http://helm:8080',
|
|
85
|
+
daytonaApiKey: 'dtn-test-key',
|
|
86
|
+
daytonaUrl: 'http://daytona:3000',
|
|
87
|
+
...overrides,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// ── Sandbox creation ───────────────────────────────────
|
|
91
|
+
describe('init', () => {
|
|
92
|
+
it('creates a Daytona sandbox with correct auth', async () => {
|
|
93
|
+
fetchSpy.mockResolvedValue(createSandboxResponse());
|
|
94
|
+
const sandbox = makeSandbox();
|
|
95
|
+
await sandbox.init();
|
|
96
|
+
expect(fetchSpy).toHaveBeenCalledWith('http://daytona:3000/sandbox', expect.objectContaining({
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: expect.objectContaining({
|
|
99
|
+
Authorization: 'Bearer dtn-test-key',
|
|
100
|
+
}),
|
|
101
|
+
}));
|
|
102
|
+
});
|
|
103
|
+
it('throws on Daytona creation failure', async () => {
|
|
104
|
+
fetchSpy.mockResolvedValue(new Response('fail', { status: 500 }));
|
|
105
|
+
const sandbox = makeSandbox();
|
|
106
|
+
await expect(sandbox.init()).rejects.toThrow('Daytona sandbox creation failed: 500');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
// ── Governance-approved execution ──────────────────────
|
|
110
|
+
describe('exec (approved)', () => {
|
|
111
|
+
it('executes command through HELM governance then Daytona', async () => {
|
|
112
|
+
// Call 1: Daytona create (auto-init)
|
|
113
|
+
// Call 2: HELM governance
|
|
114
|
+
// Call 3: Daytona exec
|
|
115
|
+
fetchSpy
|
|
116
|
+
.mockResolvedValueOnce(createSandboxResponse('sbx-1'))
|
|
117
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
118
|
+
.mockResolvedValueOnce(execResponse('world\n', 0));
|
|
119
|
+
const sandbox = makeSandbox();
|
|
120
|
+
const result = await sandbox.exec({ command: ['echo', 'world'] });
|
|
121
|
+
expect(result.stdout).toBe('world\n');
|
|
122
|
+
expect(result.exitCode).toBe(0);
|
|
123
|
+
expect(result.timedOut).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it('sends governance intent with sandbox provider metadata', async () => {
|
|
126
|
+
fetchSpy
|
|
127
|
+
.mockResolvedValueOnce(createSandboxResponse('sbx-meta'))
|
|
128
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
129
|
+
.mockResolvedValueOnce(execResponse());
|
|
130
|
+
const sandbox = makeSandbox();
|
|
131
|
+
await sandbox.exec({ command: ['ls', '-la'] });
|
|
132
|
+
// Call index 1 is HELM governance
|
|
133
|
+
const helmBody = JSON.parse(fetchSpy.mock.calls[1][1].body);
|
|
134
|
+
const intent = JSON.parse(helmBody.messages[0].content);
|
|
135
|
+
expect(intent.type).toBe('sandbox_exec_intent');
|
|
136
|
+
expect(intent.provider).toBe('daytona');
|
|
137
|
+
expect(intent.sandbox_id).toBe('sbx-meta');
|
|
138
|
+
expect(intent.command).toEqual(['ls', '-la']);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
// ── Denial path ────────────────────────────────────────
|
|
142
|
+
describe('exec (denied)', () => {
|
|
143
|
+
it('throws HelmSandboxDenyError when governance denies', async () => {
|
|
144
|
+
fetchSpy
|
|
145
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
146
|
+
.mockResolvedValueOnce(governanceDenyResponse('Not allowed'));
|
|
147
|
+
const sandbox = makeSandbox();
|
|
148
|
+
await expect(sandbox.exec({ command: ['rm', '-rf', '/'] })).rejects.toThrow(HelmSandboxDenyError);
|
|
149
|
+
});
|
|
150
|
+
it('denial error contains command and reason', async () => {
|
|
151
|
+
fetchSpy
|
|
152
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
153
|
+
.mockResolvedValueOnce(governanceDenyResponse('Blocked by policy'));
|
|
154
|
+
const sandbox = makeSandbox();
|
|
155
|
+
try {
|
|
156
|
+
await sandbox.exec({ command: ['dangerous'] });
|
|
157
|
+
expect.unreachable('should have thrown');
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
const err = e;
|
|
161
|
+
expect(err.denial.command).toEqual(['dangerous']);
|
|
162
|
+
expect(err.denial.reasonCode).toBe('DENY_POLICY_VIOLATION');
|
|
163
|
+
expect(err.name).toBe('HelmSandboxDenyError');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
it('invokes onDeny callback', async () => {
|
|
167
|
+
fetchSpy
|
|
168
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
169
|
+
.mockResolvedValueOnce(governanceDenyResponse());
|
|
170
|
+
const denials = [];
|
|
171
|
+
const sandbox = makeSandbox({ onDeny: (d) => denials.push(d) });
|
|
172
|
+
try {
|
|
173
|
+
await sandbox.exec({ command: ['test'] });
|
|
174
|
+
}
|
|
175
|
+
catch { /* expected */ }
|
|
176
|
+
expect(denials).toHaveLength(1);
|
|
177
|
+
});
|
|
178
|
+
it('never reaches Daytona on denial', async () => {
|
|
179
|
+
fetchSpy
|
|
180
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
181
|
+
.mockResolvedValueOnce(governanceDenyResponse());
|
|
182
|
+
const sandbox = makeSandbox();
|
|
183
|
+
try {
|
|
184
|
+
await sandbox.exec({ command: ['test'] });
|
|
185
|
+
}
|
|
186
|
+
catch { /* expected */ }
|
|
187
|
+
// Only 2 calls: Daytona create + HELM governance. No Daytona exec.
|
|
188
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
// ── Fail-closed behavior ───────────────────────────────
|
|
192
|
+
describe('fail-closed', () => {
|
|
193
|
+
it('throws on HELM API 500 when failClosed=true (default)', async () => {
|
|
194
|
+
fetchSpy
|
|
195
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
196
|
+
.mockResolvedValueOnce(helmApiErrorResponse(500));
|
|
197
|
+
const sandbox = makeSandbox();
|
|
198
|
+
await expect(sandbox.exec({ command: ['echo'] })).rejects.toThrow();
|
|
199
|
+
});
|
|
200
|
+
it('throws on HELM network error when failClosed=true', async () => {
|
|
201
|
+
fetchSpy
|
|
202
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
203
|
+
.mockRejectedValueOnce(new Error('ECONNREFUSED'));
|
|
204
|
+
const sandbox = makeSandbox();
|
|
205
|
+
await expect(sandbox.exec({ command: ['echo'] })).rejects.toThrow('ECONNREFUSED');
|
|
206
|
+
});
|
|
207
|
+
it('marks fail-open execution as pending instead of approved', async () => {
|
|
208
|
+
fetchSpy
|
|
209
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
210
|
+
.mockResolvedValueOnce(helmApiErrorResponse(500))
|
|
211
|
+
.mockResolvedValueOnce(execResponse('ok', 0));
|
|
212
|
+
const sandbox = makeSandbox({ failClosed: false });
|
|
213
|
+
const result = await sandbox.exec({ command: ['echo', 'ok'] });
|
|
214
|
+
expect(result.stdout).toBe('ok');
|
|
215
|
+
expect(result.receipt.receipt.status).toBe('PENDING');
|
|
216
|
+
expect(result.receipt.receipt.principal).toBe('helm-fail-open');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
// ── Receipt collection ─────────────────────────────────
|
|
220
|
+
describe('receipt collection', () => {
|
|
221
|
+
it('collects receipt with deterministic request hash', async () => {
|
|
222
|
+
fetchSpy
|
|
223
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
224
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
225
|
+
.mockResolvedValueOnce(execResponse('out', 0));
|
|
226
|
+
const sandbox = makeSandbox();
|
|
227
|
+
const result = await sandbox.exec({ command: ['echo', 'out'] });
|
|
228
|
+
expect(result.receipt).toBeDefined();
|
|
229
|
+
expect(result.receipt.requestHash).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
230
|
+
expect(result.receipt.outputHash).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
231
|
+
expect(result.receipt.receipt.status).toBe('APPROVED');
|
|
232
|
+
});
|
|
233
|
+
it('same input produces same request hash', async () => {
|
|
234
|
+
fetchSpy
|
|
235
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
236
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
237
|
+
.mockResolvedValueOnce(execResponse('a'))
|
|
238
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
239
|
+
.mockResolvedValueOnce(execResponse('a'));
|
|
240
|
+
const sandbox = makeSandbox();
|
|
241
|
+
const r1 = await sandbox.exec({ command: ['echo', 'deterministic'] });
|
|
242
|
+
const r2 = await sandbox.exec({ command: ['echo', 'deterministic'] });
|
|
243
|
+
expect(r1.receipt.requestHash).toBe(r2.receipt.requestHash);
|
|
244
|
+
});
|
|
245
|
+
it('different output produces different output hash', async () => {
|
|
246
|
+
fetchSpy
|
|
247
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
248
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
249
|
+
.mockResolvedValueOnce(execResponse('output_A'))
|
|
250
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
251
|
+
.mockResolvedValueOnce(execResponse('output_B'));
|
|
252
|
+
const sandbox = makeSandbox();
|
|
253
|
+
const r1 = await sandbox.exec({ command: ['echo', 'A'] });
|
|
254
|
+
const r2 = await sandbox.exec({ command: ['echo', 'B'] });
|
|
255
|
+
expect(r1.receipt.outputHash).not.toBe(r2.receipt.outputHash);
|
|
256
|
+
});
|
|
257
|
+
it('invokes onReceipt callback', async () => {
|
|
258
|
+
fetchSpy
|
|
259
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
260
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
261
|
+
.mockResolvedValueOnce(execResponse());
|
|
262
|
+
const receipts = [];
|
|
263
|
+
const sandbox = makeSandbox({ onReceipt: (r) => receipts.push(r) });
|
|
264
|
+
await sandbox.exec({ command: ['echo'] });
|
|
265
|
+
expect(receipts).toHaveLength(1);
|
|
266
|
+
});
|
|
267
|
+
it('getReceipts returns accumulated receipts', async () => {
|
|
268
|
+
fetchSpy
|
|
269
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
270
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
271
|
+
.mockResolvedValueOnce(execResponse())
|
|
272
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
273
|
+
.mockResolvedValueOnce(execResponse());
|
|
274
|
+
const sandbox = makeSandbox();
|
|
275
|
+
await sandbox.exec({ command: ['cmd1'] });
|
|
276
|
+
await sandbox.exec({ command: ['cmd2'] });
|
|
277
|
+
expect(sandbox.getReceipts()).toHaveLength(2);
|
|
278
|
+
});
|
|
279
|
+
it('clearReceipts empties the collection', async () => {
|
|
280
|
+
fetchSpy
|
|
281
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
282
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
283
|
+
.mockResolvedValueOnce(execResponse());
|
|
284
|
+
const sandbox = makeSandbox();
|
|
285
|
+
await sandbox.exec({ command: ['echo'] });
|
|
286
|
+
expect(sandbox.getReceipts()).toHaveLength(1);
|
|
287
|
+
sandbox.clearReceipts();
|
|
288
|
+
expect(sandbox.getReceipts()).toHaveLength(0);
|
|
289
|
+
});
|
|
290
|
+
it('receipt lamport clock increments', async () => {
|
|
291
|
+
fetchSpy
|
|
292
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
293
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
294
|
+
.mockResolvedValueOnce(execResponse())
|
|
295
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
296
|
+
.mockResolvedValueOnce(execResponse());
|
|
297
|
+
const sandbox = makeSandbox();
|
|
298
|
+
await sandbox.exec({ command: ['echo', 'a'] });
|
|
299
|
+
await sandbox.exec({ command: ['echo', 'b'] });
|
|
300
|
+
const receipts = sandbox.getReceipts();
|
|
301
|
+
expect(receipts[0].receipt.lamport_clock).toBe(0);
|
|
302
|
+
expect(receipts[1].receipt.lamport_clock).toBe(1);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
// ── Sandbox lifecycle ──────────────────────────────────
|
|
306
|
+
describe('lifecycle', () => {
|
|
307
|
+
it('auto-initializes on first exec', async () => {
|
|
308
|
+
fetchSpy
|
|
309
|
+
.mockResolvedValueOnce(createSandboxResponse('auto-init'))
|
|
310
|
+
.mockResolvedValueOnce(governanceApproveResponse())
|
|
311
|
+
.mockResolvedValueOnce(execResponse());
|
|
312
|
+
const sandbox = makeSandbox();
|
|
313
|
+
// Don't call init() explicitly
|
|
314
|
+
await sandbox.exec({ command: ['echo'] });
|
|
315
|
+
// First call should be Daytona create
|
|
316
|
+
expect(fetchSpy.mock.calls[0][0]).toBe('http://daytona:3000/sandbox');
|
|
317
|
+
});
|
|
318
|
+
it('destroy() sends DELETE and clears sandbox ID', async () => {
|
|
319
|
+
fetchSpy
|
|
320
|
+
.mockResolvedValueOnce(createSandboxResponse('destroy-me'))
|
|
321
|
+
.mockResolvedValueOnce(new Response(null, { status: 204 }));
|
|
322
|
+
const sandbox = makeSandbox();
|
|
323
|
+
await sandbox.init();
|
|
324
|
+
await sandbox.destroy();
|
|
325
|
+
const deleteCall = fetchSpy.mock.calls[1];
|
|
326
|
+
expect(deleteCall[0]).toBe('http://daytona:3000/sandbox/destroy-me');
|
|
327
|
+
expect(deleteCall[1].method).toBe('DELETE');
|
|
328
|
+
});
|
|
329
|
+
it('destroy() is idempotent when not initialized', async () => {
|
|
330
|
+
const sandbox = makeSandbox();
|
|
331
|
+
await sandbox.destroy(); // should not throw
|
|
332
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
// ── File operations ────────────────────────────────────
|
|
336
|
+
describe('file operations', () => {
|
|
337
|
+
it('writeFile sends PUT with content', async () => {
|
|
338
|
+
fetchSpy
|
|
339
|
+
.mockResolvedValueOnce(createSandboxResponse('fs-test'))
|
|
340
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
341
|
+
const sandbox = makeSandbox();
|
|
342
|
+
await sandbox.init();
|
|
343
|
+
await sandbox.writeFile('/app/test.py', 'print("hello")');
|
|
344
|
+
const writeCall = fetchSpy.mock.calls[1];
|
|
345
|
+
expect(writeCall[0]).toContain('/sandbox/fs-test/filesystem');
|
|
346
|
+
expect(writeCall[0]).toContain('path=%2Fapp%2Ftest.py');
|
|
347
|
+
expect(writeCall[1].method).toBe('PUT');
|
|
348
|
+
expect(writeCall[1].body).toBe('print("hello")');
|
|
349
|
+
});
|
|
350
|
+
it('readFile sends GET and returns content', async () => {
|
|
351
|
+
fetchSpy
|
|
352
|
+
.mockResolvedValueOnce(createSandboxResponse('fs-read'))
|
|
353
|
+
.mockResolvedValueOnce(new Response('file content', { status: 200 }));
|
|
354
|
+
const sandbox = makeSandbox();
|
|
355
|
+
await sandbox.init();
|
|
356
|
+
const content = await sandbox.readFile('/app/out.txt');
|
|
357
|
+
expect(content).toBe('file content');
|
|
358
|
+
const readCall = fetchSpy.mock.calls[1];
|
|
359
|
+
expect(readCall[0]).toContain('path=%2Fapp%2Fout.txt');
|
|
360
|
+
expect(readCall[1].method).toBe('GET');
|
|
361
|
+
});
|
|
362
|
+
it('writeFile throws on failure', async () => {
|
|
363
|
+
fetchSpy
|
|
364
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
365
|
+
.mockResolvedValueOnce(new Response('fail', { status: 500 }));
|
|
366
|
+
const sandbox = makeSandbox();
|
|
367
|
+
await sandbox.init();
|
|
368
|
+
await expect(sandbox.writeFile('/bad', 'data')).rejects.toThrow('Daytona write file failed: 500');
|
|
369
|
+
});
|
|
370
|
+
it('readFile throws on failure', async () => {
|
|
371
|
+
fetchSpy
|
|
372
|
+
.mockResolvedValueOnce(createSandboxResponse())
|
|
373
|
+
.mockResolvedValueOnce(new Response('fail', { status: 404 }));
|
|
374
|
+
const sandbox = makeSandbox();
|
|
375
|
+
await sandbox.init();
|
|
376
|
+
await expect(sandbox.readFile('/missing')).rejects.toThrow('Daytona read file failed: 404');
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mindburn/helm-mastra",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "HELM governance adapter for Mastra agent framework — sandbox-aware",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": ["dist"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"prepublishOnly": "npm run build",
|
|
12
|
+
"test": "vitest",
|
|
13
|
+
"lint": "biome lint ."
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@mindburn/helm": "^1.0.1"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@mastra/core": ">=0.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@biomejs/biome": "^2.4.1",
|
|
23
|
+
"typescript": "^5.4.0",
|
|
24
|
+
"vitest": "^4.0.18"
|
|
25
|
+
},
|
|
26
|
+
"overrides": {
|
|
27
|
+
"rollup": "^4.59.0"
|
|
28
|
+
},
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/Mindburn-Labs/helm-oss",
|
|
33
|
+
"directory": "sdk/ts/mastra"
|
|
34
|
+
},
|
|
35
|
+
"keywords": ["helm", "mastra", "daytona", "governance", "sandbox"]
|
|
36
|
+
}
|