@miphamai/cli 0.15.0 → 0.16.1

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.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Template literal tag that strips leading indentation from multi-line strings.
3
+ * Used by slash command handlers to format help text and status messages.
4
+ */
5
+ export function stripIndent(strings: TemplateStringsArray, ...values: unknown[]): string {
6
+ let result = strings.reduce((acc, s, i) => acc + s + (values[i] ?? ''), '')
7
+ // Remove leading newline
8
+ result = result.replace(/^\n/, '')
9
+ // Find minimum indent
10
+ const match = result.match(/^( +)/m)
11
+ if (match?.[1]) {
12
+ const indent = match[1].length
13
+ result = result
14
+ .split('\n')
15
+ .map((line) => line.slice(indent))
16
+ .join('\n')
17
+ }
18
+ return result.trim()
19
+ }
@@ -1,3 +1,4 @@
1
+ import vm from 'node:vm'
1
2
  import { createSandbox } from './sandbox'
2
3
  import { createJournal, appendJournal, loadJournal, loadScript } from './journal'
3
4
  import { createBudget } from './budget'
@@ -47,7 +48,8 @@ function agentCacheKey(prompt: string, opts: Record<string, unknown> = {}): stri
47
48
  * - Returns cacheHits and cacheMisses counts
48
49
  *
49
50
  * The sandbox blocks non-deterministic APIs (Date.now, Math.random) to make
50
- * this deterministic replay possible.
51
+ * this deterministic replay possible. Scripts execute inside a node:vm.Context
52
+ * that blocks sandbox escape vectors (eval, Function, import, require, process, etc.).
51
53
  */
52
54
  export async function runWorkflow(
53
55
  script: string,
@@ -134,42 +136,38 @@ export async function runWorkflow(
134
136
  appendJournal(runId, { type: 'log', message })
135
137
  }
136
138
 
137
- const sandbox = createSandbox(args, budget)
138
-
139
139
  // Build the script wrapper
140
140
  const wrappedScript = `
141
- return (async () => {
141
+ (async () => {
142
142
  ${script}
143
143
  })()
144
144
  `
145
145
 
146
- // Execute in sandboxed context
147
- const scriptFn = new Function(
148
- 'agent',
149
- 'parallel',
150
- 'pipeline',
151
- 'verify',
152
- 'judge',
153
- 'loopUntilConvergence',
154
- 'phase',
155
- 'log',
156
- 'args',
157
- 'budget',
158
- wrappedScript,
159
- )
160
-
161
- const result = await scriptFn(
162
- agent,
163
- parallel,
164
- pipeline,
165
- verify,
166
- judge,
167
- loopUntilConvergence,
168
- wrappedPhase,
169
- log,
170
- args,
171
- budget,
172
- )
146
+ // Execute in node:vm sandbox — no access to host globals
147
+ const vmScript = new vm.Script(wrappedScript, { filename: 'workflow.js' })
148
+ const sandboxCtx = createSandbox(args, budget)
149
+
150
+ // Inject primitives into the sandbox context (whitelist approach)
151
+ sandboxCtx.agent = agent
152
+ sandboxCtx.parallel = parallel
153
+ sandboxCtx.pipeline = pipeline
154
+ sandboxCtx.verify = verify
155
+ sandboxCtx.judge = judge
156
+ sandboxCtx.loopUntilConvergence = loopUntilConvergence
157
+ sandboxCtx.phase = wrappedPhase
158
+ sandboxCtx.log = log
159
+
160
+ let result: unknown
161
+ try {
162
+ result = await vmScript.runInContext(sandboxCtx, { timeout: 120_000 })
163
+ } catch (err) {
164
+ const message = err instanceof Error ? err.message : String(err)
165
+ throw new Error(
166
+ `Workflow script execution failed: ${message}\n\n` +
167
+ `This is likely a bug in the workflow script, not in Mipham Code itself. ` +
168
+ `Check the script for syntax errors, undefined references, or unhandled promise rejections.`,
169
+ )
170
+ }
173
171
 
174
172
  // Count journal entries from state
175
173
  const priorEntries = loadJournal(runId)
@@ -1,27 +1,39 @@
1
- /** APIs disabled in workflow scripts to ensure deterministic replay. */
1
+ import vm from 'node:vm'
2
+
3
+ /** APIs disabled in workflow scripts to ensure deterministic replay + sandbox escape prevention. */
2
4
  const FORBIDDEN = new Set(['Date.now', 'Math.random', 'crypto.randomUUID'])
3
5
 
4
6
  /**
5
- * Create a sandboxed global scope for workflow script execution.
6
- * Blocks Date.now(), Math.random(), argless new Date(), crypto.randomUUID().
7
+ * Create a sandboxed VM context for workflow script execution.
8
+ * Blocks Date.now(), Math.random(), argless new Date(), crypto.randomUUID(),
9
+ * plus explicit sandbox escape vectors: eval, Function constructor, import(),
10
+ * require(), process, Bun, fetch, setTimeout/setInterval, etc.
7
11
  */
8
12
  export function createSandbox(
9
13
  args: unknown,
10
14
  budget: { total: number | null; spent(): number; remaining(): number },
11
- ): Record<string, unknown> {
12
- const sandbox: Record<string, unknown> = {
15
+ ): vm.Context {
16
+ const sandboxObj: Record<string, unknown> = {
13
17
  args,
14
18
  budget,
15
19
  console: {
16
20
  log: (..._a: unknown[]) => {}, // no-op in sandbox
17
21
  error: (..._a: unknown[]) => {},
18
22
  },
19
- // Primitives are injected by the runtime, not the sandbox
23
+
24
+ // ── Explicit sandbox escape prevention ──
25
+ // V8 builtins that would otherwise be available in a vm.Context
26
+ eval: () => {
27
+ throw new Error('eval() is disabled in workflow sandbox.')
28
+ },
29
+ Function: () => {
30
+ throw new Error('new Function() is disabled in workflow sandbox.')
31
+ },
20
32
  }
21
33
 
22
34
  // Override Date to block now() and argless constructor
23
35
  const OriginalDate = Date
24
- sandbox.Date = new Proxy(OriginalDate, {
36
+ sandboxObj.Date = new Proxy(OriginalDate, {
25
37
  construct(_target, constructorArgs) {
26
38
  if (constructorArgs.length === 0) {
27
39
  throw new Error('new Date() is disabled in workflow sandbox. Pass timestamps via args.')
@@ -42,7 +54,7 @@ export function createSandbox(
42
54
  })
43
55
 
44
56
  // Override Math.random
45
- sandbox.Math = new Proxy(Math, {
57
+ sandboxObj.Math = new Proxy(Math, {
46
58
  get(_target, prop) {
47
59
  if (prop === 'random') {
48
60
  throw new Error('Math.random() is disabled in workflow sandbox. Use a seed from args.')
@@ -57,7 +69,7 @@ export function createSandbox(
57
69
  | { randomUUID?: unknown; [key: string]: unknown }
58
70
  | undefined
59
71
  if (globalCrypto) {
60
- sandbox.crypto = new Proxy(globalCrypto, {
72
+ sandboxObj.crypto = new Proxy(globalCrypto, {
61
73
  get(_target, prop) {
62
74
  if (prop === 'randomUUID') {
63
75
  throw new Error('crypto.randomUUID() is disabled in workflow sandbox.')
@@ -70,7 +82,7 @@ export function createSandbox(
70
82
  })
71
83
  }
72
84
 
73
- return sandbox
85
+ return vm.createContext(sandboxObj)
74
86
  }
75
87
 
76
88
  /** Check whether a given API identifier is in the forbidden set. */