@miphamai/cli 0.37.0 → 0.39.0
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/bin/mipham.ts +25 -0
- package/package.json +1 -1
- package/src/agent/cross-session/discovery.ts +45 -1
- package/src/agent/cross-session/file-inbox.ts +15 -0
- package/src/agent/message-router.ts +22 -2
- package/src/agent/sub-agent.ts +4 -3
- package/src/config/defaults.ts +19 -1
- package/src/config/keys-manager.ts +11 -1
- package/src/config/loader.ts +20 -10
- package/src/core/context.ts +86 -56
- package/src/core/credential-masker/output-scrub.ts +5 -0
- package/src/core/crsi-sandbox.ts +19 -7
- package/src/core/engine.ts +69 -35
- package/src/core/instructions.ts +4 -42
- package/src/core/permission.ts +7 -1
- package/src/core/red-team.ts +20 -5
- package/src/core/self-critique.ts +3 -1
- package/src/core/session-log.ts +225 -0
- package/src/core/session-store.ts +119 -34
- package/src/core/workspace-trust.ts +25 -2
- package/src/daemon/auth.ts +9 -6
- package/src/daemon/cors.ts +24 -16
- package/src/daemon/rate-limiter.ts +4 -0
- package/src/daemon/server.ts +24 -3
- package/src/i18n-core/locales/en-US.json +4 -0
- package/src/i18n-core/locales/zh-CN.json +4 -0
- package/src/index.tsx +62 -30
- package/src/mcp/client.ts +56 -17
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/oauth.ts +1 -1
- package/src/providers/llm-replay.ts +36 -0
- package/src/providers/llm.ts +16 -0
- package/src/providers/registry.ts +2 -1
- package/src/security/gate.ts +10 -0
- package/src/security/url.ts +37 -14
- package/src/shared/constants.ts +4 -486
- package/src/shared/types.ts +6 -437
- package/src/skills/fork-executor.ts +3 -1
- package/src/skills/loader.ts +2 -1
- package/src/skills/seam.ts +18 -0
- package/src/tools/agent/agent.ts +1 -0
- package/src/tools/agent/skill.ts +1 -0
- package/src/tools/exec/bash.ts +141 -129
- package/src/tools/exec/git.ts +17 -0
- package/src/tools/file/read.ts +73 -66
- package/src/tools/file/write.ts +1 -1
- package/src/tools/index.ts +54 -132
- package/src/tools/network/web-fetch.ts +41 -26
- package/src/tools/seam.ts +25 -0
- package/src/tools/validation.ts +91 -0
- package/src/ui/app.tsx +36 -0
- package/src/ui/commands.ts +27 -6
- package/src/ui/input.tsx +21 -0
- package/src/vajra/compose/assemble.ts +19 -0
- package/src/vajra/compose/bundle.ts +39 -0
- package/src/vajra/compose/dump.ts +5 -0
- package/src/vajra/compose/index.ts +4 -0
- package/src/vajra/compose/mount.ts +30 -0
- package/src/vajra/context.ts +183 -0
- package/src/vajra/events.ts +13 -0
- package/src/vajra/index.ts +4 -0
- package/src/vajra/leaf/plan-runner.ts +94 -0
- package/src/vajra/service.ts +15 -0
- package/src/workflow/primitives/agent.ts +3 -1
- package/src/workflow/runtime.ts +23 -15
- package/src/workflow/sandbox.ts +51 -9
- package/src/core/compaction-progress.ts +0 -205
- package/src/core/context-compact.ts +0 -124
- package/src/core/context-drain.ts +0 -113
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Context, Mounted, Service } from '..'
|
|
2
|
+
import type { Bundle, BundleLine, Profile } from './bundle'
|
|
3
|
+
import { assemble } from './assemble'
|
|
4
|
+
|
|
5
|
+
/** 服务解析器:bundle 行的「要挂的代码」——按行解析出要挂载的 Service;纯数据行(如 package-info)返回 undefined 跳过。 */
|
|
6
|
+
export type ServiceResolver = (line: BundleLine) => Service | undefined
|
|
7
|
+
|
|
8
|
+
/** 按行顺序把解析出的 Service 挂进 ctx(依赖未就位则挂起,语义同 ctx.mount)。 */
|
|
9
|
+
export function mountLines(
|
|
10
|
+
ctx: Context,
|
|
11
|
+
lines: BundleLine[],
|
|
12
|
+
resolveService: ServiceResolver,
|
|
13
|
+
): Mounted[] {
|
|
14
|
+
const mounted: Mounted[] = []
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const service = resolveService(line)
|
|
17
|
+
if (service) mounted.push(ctx.mount(service))
|
|
18
|
+
}
|
|
19
|
+
return mounted
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 从 profile 声明式挂载:assemble → mountLines 一整条链。 */
|
|
23
|
+
export function mountProfile(
|
|
24
|
+
ctx: Context,
|
|
25
|
+
profile: Profile,
|
|
26
|
+
resolveBundle: (name: string) => Bundle,
|
|
27
|
+
resolveService: ServiceResolver,
|
|
28
|
+
): Mounted[] {
|
|
29
|
+
return mountLines(ctx, assemble(profile, resolveBundle), resolveService)
|
|
30
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { EventsOfMode } from './events'
|
|
2
|
+
import type { Service, Mounted, ServiceStatus } from './service'
|
|
3
|
+
|
|
4
|
+
export type Disposer = () => void
|
|
5
|
+
|
|
6
|
+
type Listener = (...args: any[]) => any
|
|
7
|
+
|
|
8
|
+
export class Context {
|
|
9
|
+
private services = new Map<string, unknown>()
|
|
10
|
+
private effects: Disposer[] = []
|
|
11
|
+
private listeners = new Map<string, Listener[]>()
|
|
12
|
+
private waiters: Array<() => boolean> = []
|
|
13
|
+
private scopes = new Map<unknown, Context>()
|
|
14
|
+
readonly parent?: Context
|
|
15
|
+
|
|
16
|
+
constructor(parent?: Context) {
|
|
17
|
+
this.parent = parent
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
provide<T>(key: string, value: T): Disposer {
|
|
21
|
+
this.services.set(key, value)
|
|
22
|
+
this.flushWaiters()
|
|
23
|
+
return () => {
|
|
24
|
+
this.services.delete(key)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private flushWaiters(): void {
|
|
29
|
+
this.waiters = this.waiters.filter((w) => !w())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mount(service: Service): Mounted {
|
|
33
|
+
const keys = service.inject ?? []
|
|
34
|
+
let status: ServiceStatus = 'inactive'
|
|
35
|
+
let error: Error | undefined
|
|
36
|
+
let disposer: Disposer = () => {}
|
|
37
|
+
let applied = false
|
|
38
|
+
let disposed = false
|
|
39
|
+
let effectSnapshot = -1
|
|
40
|
+
|
|
41
|
+
const tryApply = (): boolean => {
|
|
42
|
+
if (applied) return true
|
|
43
|
+
if (!keys.every((k) => this.has(k))) return false
|
|
44
|
+
applied = true
|
|
45
|
+
status = 'loading'
|
|
46
|
+
effectSnapshot = this.effects.length
|
|
47
|
+
try {
|
|
48
|
+
disposer = service.apply(this) ?? (() => {})
|
|
49
|
+
status = 'active'
|
|
50
|
+
} catch (e) {
|
|
51
|
+
error = e as Error
|
|
52
|
+
status = 'failed'
|
|
53
|
+
}
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const waiter = (): boolean => {
|
|
58
|
+
if (disposed) {
|
|
59
|
+
this.waiters = this.waiters.filter((w) => w !== waiter)
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
if (!tryApply()) return false
|
|
63
|
+
this.waiters = this.waiters.filter((w) => w !== waiter)
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!tryApply()) {
|
|
68
|
+
this.waiters.push(waiter)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const mounted: Mounted = {
|
|
72
|
+
dispose: () => {
|
|
73
|
+
disposed = true
|
|
74
|
+
this.waiters = this.waiters.filter((w) => w !== waiter)
|
|
75
|
+
if (status === 'active') {
|
|
76
|
+
status = 'unloading'
|
|
77
|
+
disposer()
|
|
78
|
+
}
|
|
79
|
+
if (effectSnapshot >= 0) {
|
|
80
|
+
while (this.effects.length > effectSnapshot) {
|
|
81
|
+
this.effects.pop()!()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
status: () => status,
|
|
86
|
+
}
|
|
87
|
+
Object.defineProperty(mounted, 'error', { get: () => error })
|
|
88
|
+
return mounted
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get<T>(key: string): T | undefined {
|
|
92
|
+
return (this.services.get(key) as T | undefined) ?? this.parent?.get<T>(key)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
has(key: string): boolean {
|
|
96
|
+
return this.services.has(key) || (this.parent?.has(key) ?? false)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
keys(): string[] {
|
|
100
|
+
return [...this.services.keys()]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 枚举本层 + 父层所有键(local 遮蔽 parent,去重)。 */
|
|
104
|
+
keysRecursive(): string[] {
|
|
105
|
+
const seen = new Set<string>(this.keys())
|
|
106
|
+
for (const k of this.parent?.keysRecursive() ?? []) seen.add(k)
|
|
107
|
+
return [...seen]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
scope(key: unknown): Context {
|
|
111
|
+
let child = this.scopes.get(key)
|
|
112
|
+
if (!child) {
|
|
113
|
+
child = new Context(this)
|
|
114
|
+
this.scopes.set(key, child)
|
|
115
|
+
}
|
|
116
|
+
return child
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
effect(fn: () => Disposer | void): Disposer {
|
|
120
|
+
const d = fn() ?? (() => {})
|
|
121
|
+
let disposed = false
|
|
122
|
+
const dispose = () => {
|
|
123
|
+
if (disposed) return
|
|
124
|
+
disposed = true
|
|
125
|
+
d()
|
|
126
|
+
}
|
|
127
|
+
this.effects.push(dispose)
|
|
128
|
+
return dispose
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
on(event: string, fn: Listener): Disposer {
|
|
132
|
+
const ls = this.listeners.get(event) ?? []
|
|
133
|
+
ls.push(fn)
|
|
134
|
+
this.listeners.set(event, ls)
|
|
135
|
+
return () => {
|
|
136
|
+
const cur = this.listeners.get(event)
|
|
137
|
+
if (cur)
|
|
138
|
+
this.listeners.set(
|
|
139
|
+
event,
|
|
140
|
+
cur.filter((l) => l !== fn),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
emit(event: EventsOfMode<'emit'>, ...args: unknown[]): void {
|
|
146
|
+
for (const fn of this.listeners.get(event) ?? []) fn(...args)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async waterfall<T>(event: EventsOfMode<'waterfall'>, value: T, ...args: unknown[]): Promise<T> {
|
|
150
|
+
const ls = this.listeners.get(event) ?? []
|
|
151
|
+
const run = async (i: number, v: T): Promise<T> => {
|
|
152
|
+
if (i >= ls.length) return v
|
|
153
|
+
const next = async (nextVal?: T) => run(i + 1, nextVal === undefined ? v : nextVal)
|
|
154
|
+
return (await ls[i]!(v, ...args, next)) as T
|
|
155
|
+
}
|
|
156
|
+
return run(0, value)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async parallel(event: EventsOfMode<'parallel'>, ...args: unknown[]): Promise<unknown[]> {
|
|
160
|
+
return Promise.all((this.listeners.get(event) ?? []).map((fn) => fn(...args)))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async serial(event: EventsOfMode<'serial'>, ...args: unknown[]): Promise<unknown[]> {
|
|
164
|
+
const out: unknown[] = []
|
|
165
|
+
for (const fn of this.listeners.get(event) ?? []) out.push(await fn(...args))
|
|
166
|
+
return out
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
dispose(): void {
|
|
170
|
+
const errors: unknown[] = []
|
|
171
|
+
while (this.effects.length) {
|
|
172
|
+
try {
|
|
173
|
+
this.effects.pop()!()
|
|
174
|
+
} catch (e) {
|
|
175
|
+
errors.push(e)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this.services.clear()
|
|
179
|
+
this.scopes.clear()
|
|
180
|
+
this.listeners.clear()
|
|
181
|
+
if (errors.length) throw new AggregateError(errors)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type DispatchMode = 'emit' | 'waterfall' | 'parallel' | 'serial'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 事件契约映射。每个事件名声明其派发模式;里程碑/测试通过
|
|
5
|
+
* declaration merging 扩展本 interface(无需改内核)。
|
|
6
|
+
*/
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
8
|
+
export interface EventMap {}
|
|
9
|
+
|
|
10
|
+
/** 提取声明为指定 mode 的事件名联合。 */
|
|
11
|
+
export type EventsOfMode<M extends DispatchMode> = {
|
|
12
|
+
[K in keyof EventMap]: EventMap[K] extends { mode: M } ? K : never
|
|
13
|
+
}[keyof EventMap]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Service } from '../index'
|
|
2
|
+
import type { Llm } from '../../providers/llm'
|
|
3
|
+
import type { BundleLine } from '../compose/bundle'
|
|
4
|
+
|
|
5
|
+
export type PlanTask = { id: string; description: string }
|
|
6
|
+
export type Plan = { name: string; tasks: PlanTask[] }
|
|
7
|
+
export type TaskOutcome = {
|
|
8
|
+
taskId: string
|
|
9
|
+
status: 'done' | 'needs-changes' | 'error'
|
|
10
|
+
result: string
|
|
11
|
+
review: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const PLAN_RUNNER_KEY = 'plan-runner'
|
|
15
|
+
|
|
16
|
+
export interface PlanRunner {
|
|
17
|
+
run(plan: Plan): Promise<TaskOutcome[]>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 事件契约:declaration merging 扩展 EventMap(内核首例,不改内核)
|
|
21
|
+
declare module '../events' {
|
|
22
|
+
interface EventMap {
|
|
23
|
+
'plan/task-start': { mode: 'emit'; in: { taskId: string } }
|
|
24
|
+
'plan/task-done': {
|
|
25
|
+
mode: 'emit'
|
|
26
|
+
in: { taskId: string; status: 'done' | 'needs-changes' | 'error' }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function chatText(llm: Llm, prompt: string, model: string): Promise<string> {
|
|
32
|
+
let text = ''
|
|
33
|
+
for await (const chunk of llm.chat({
|
|
34
|
+
model,
|
|
35
|
+
messages: [{ role: 'user', content: prompt }],
|
|
36
|
+
})) {
|
|
37
|
+
if (chunk.type === 'text' && chunk.content) text += chunk.content
|
|
38
|
+
}
|
|
39
|
+
return text
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 创建 plan-runner Service。model 缺省为空串(「用 active model」,由 ProviderRegistry 的 req.model || activeModelId 回退);传值则锁定该模型。 */
|
|
43
|
+
export function createPlanRunnerService(options: { model?: string } = {}): Service {
|
|
44
|
+
const model = options.model ?? ''
|
|
45
|
+
return {
|
|
46
|
+
inject: ['llm'],
|
|
47
|
+
apply(ctx) {
|
|
48
|
+
const runner: PlanRunner = {
|
|
49
|
+
async run(plan) {
|
|
50
|
+
const outcomes: TaskOutcome[] = []
|
|
51
|
+
for (const task of plan.tasks) {
|
|
52
|
+
ctx.emit('plan/task-start', { taskId: task.id })
|
|
53
|
+
const taskCtx = ctx.scope(task.id) // 每任务独立作用域(继承父层 llm 缝)
|
|
54
|
+
let result = ''
|
|
55
|
+
let review = ''
|
|
56
|
+
try {
|
|
57
|
+
result = await chatText(
|
|
58
|
+
taskCtx.get<Llm>('llm')!,
|
|
59
|
+
`Implement: ${task.description}`,
|
|
60
|
+
model,
|
|
61
|
+
)
|
|
62
|
+
review = await chatText(
|
|
63
|
+
taskCtx.get<Llm>('llm')!,
|
|
64
|
+
`Review: does the result satisfy "${task.description}"? Result: ${result}`,
|
|
65
|
+
model,
|
|
66
|
+
)
|
|
67
|
+
} catch (e) {
|
|
68
|
+
outcomes.push({ taskId: task.id, status: 'error', result, review: String(e) })
|
|
69
|
+
ctx.emit('plan/task-done', { taskId: task.id, status: 'error' })
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
const status: TaskOutcome['status'] = review.startsWith('APPROVE')
|
|
73
|
+
? 'done'
|
|
74
|
+
: 'needs-changes'
|
|
75
|
+
outcomes.push({ taskId: task.id, status, result, review })
|
|
76
|
+
ctx.emit('plan/task-done', { taskId: task.id, status })
|
|
77
|
+
}
|
|
78
|
+
return outcomes
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
ctx.provide(PLAN_RUNNER_KEY, runner)
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 默认 plan-runner Service(model 空串 = 用 active model)。 */
|
|
87
|
+
export const planRunnerService: Service = createPlanRunnerService()
|
|
88
|
+
|
|
89
|
+
/** 从 bundle 行解析 plan-runner Service(读 config.model 传工厂;id 非 plan-runner 或纯数据行返回 undefined)。 */
|
|
90
|
+
export function planRunnerFromLine(line: BundleLine): Service | undefined {
|
|
91
|
+
if (line.id !== 'plan-runner') return undefined
|
|
92
|
+
const model = typeof line.config.model === 'string' ? line.config.model : undefined
|
|
93
|
+
return createPlanRunnerService({ model })
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Context } from './context'
|
|
2
|
+
import type { Disposer } from './context'
|
|
3
|
+
|
|
4
|
+
export interface Service {
|
|
5
|
+
inject?: string[]
|
|
6
|
+
apply(ctx: Context): void | Disposer
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ServiceStatus = 'inactive' | 'loading' | 'active' | 'unloading' | 'failed'
|
|
10
|
+
|
|
11
|
+
export interface Mounted {
|
|
12
|
+
dispose(): void
|
|
13
|
+
status(): ServiceStatus
|
|
14
|
+
readonly error?: Error
|
|
15
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SubAgent } from '../../agent/sub-agent'
|
|
2
2
|
import type { ProviderRegistry } from '../../providers/registry'
|
|
3
|
+
import type { Llm } from '../../providers/llm'
|
|
3
4
|
import type { ToolDefinition } from '../../shared/index.ts'
|
|
4
5
|
import type { PermissionSystem } from '../../core/permission'
|
|
5
6
|
import { validateJSONSchema, formatValidationErrors } from '../schema-validator'
|
|
@@ -41,6 +42,7 @@ export async function workflowAgent(
|
|
|
41
42
|
registry: ProviderRegistry,
|
|
42
43
|
toolRegistry: Map<string, ToolDefinition>,
|
|
43
44
|
opts: WorkflowAgentOpts = {},
|
|
45
|
+
llm?: Llm,
|
|
44
46
|
): Promise<unknown> {
|
|
45
47
|
// If provider override, switch temporarily
|
|
46
48
|
if (opts.provider) {
|
|
@@ -82,7 +84,7 @@ export async function workflowAgent(
|
|
|
82
84
|
`Return ONLY the JSON object, no other text. Do not wrap in markdown code fences.`
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
const sub = new SubAgent(registry, toolRegistry, opts.permissionSystem)
|
|
87
|
+
const sub = new SubAgent(registry, toolRegistry, opts.permissionSystem, undefined, undefined, llm)
|
|
86
88
|
|
|
87
89
|
// ── Execute with optional schema validation + retry ──
|
|
88
90
|
let result: unknown = ''
|
package/src/workflow/runtime.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import vm from 'node:vm'
|
|
2
|
-
import { createSandbox } from './sandbox'
|
|
2
|
+
import { createSandbox, sealValue } from './sandbox'
|
|
3
3
|
import { createJournal, appendJournal, loadJournal, loadScript } from './journal'
|
|
4
4
|
import { createBudget } from './budget'
|
|
5
5
|
import { workflowAgent } from './primitives/agent'
|
|
@@ -103,6 +103,7 @@ export async function runWorkflow(
|
|
|
103
103
|
const registry: ProviderRegistry = engine.getRegistry()
|
|
104
104
|
const toolRegistry = engine.getTools()
|
|
105
105
|
const permission = engine.getPermission()
|
|
106
|
+
const llm = engine.getLlm()
|
|
106
107
|
|
|
107
108
|
const budget = createBudget(budgetTotal)
|
|
108
109
|
|
|
@@ -124,10 +125,16 @@ export async function runWorkflow(
|
|
|
124
125
|
|
|
125
126
|
bus.emitEvent({ type: 'agent:start', agentId, label: agentId, phase })
|
|
126
127
|
|
|
127
|
-
const result = await workflowAgent(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
const result = await workflowAgent(
|
|
129
|
+
prompt,
|
|
130
|
+
registry,
|
|
131
|
+
toolRegistry,
|
|
132
|
+
{
|
|
133
|
+
...(opts || {}),
|
|
134
|
+
permissionSystem: permission,
|
|
135
|
+
} as Record<string, unknown>,
|
|
136
|
+
llm,
|
|
137
|
+
)
|
|
131
138
|
|
|
132
139
|
const durationMs = Date.now() - startTime
|
|
133
140
|
bus.emitEvent({ type: 'agent:end', agentId, label: agentId, success: true, durationMs })
|
|
@@ -159,19 +166,20 @@ export async function runWorkflow(
|
|
|
159
166
|
})()
|
|
160
167
|
`
|
|
161
168
|
|
|
162
|
-
// Execute in node:vm
|
|
169
|
+
// Execute in a node:vm context (deterministic replay; NOT a security boundary).
|
|
163
170
|
const vmScript = new vm.Script(wrappedScript, { filename: 'workflow.js' })
|
|
164
171
|
const sandboxCtx = createSandbox(args, budget)
|
|
165
172
|
|
|
166
|
-
// Inject primitives into the
|
|
167
|
-
|
|
168
|
-
sandboxCtx.
|
|
169
|
-
sandboxCtx.
|
|
170
|
-
sandboxCtx.
|
|
171
|
-
sandboxCtx.
|
|
172
|
-
sandboxCtx.
|
|
173
|
-
sandboxCtx.
|
|
174
|
-
sandboxCtx.
|
|
173
|
+
// Inject primitives into the context (sealed so their constructor chain
|
|
174
|
+
// can't reach host globals via `fn.constructor('return process')()`).
|
|
175
|
+
sandboxCtx.agent = sealValue(agent)
|
|
176
|
+
sandboxCtx.parallel = sealValue(parallel)
|
|
177
|
+
sandboxCtx.pipeline = sealValue(pipeline)
|
|
178
|
+
sandboxCtx.verify = sealValue(verify)
|
|
179
|
+
sandboxCtx.judge = sealValue(judge)
|
|
180
|
+
sandboxCtx.loopUntilConvergence = sealValue(loopUntilConvergence)
|
|
181
|
+
sandboxCtx.phase = sealValue(wrappedPhase)
|
|
182
|
+
sandboxCtx.log = sealValue(log)
|
|
175
183
|
|
|
176
184
|
let result: unknown
|
|
177
185
|
try {
|
package/src/workflow/sandbox.ts
CHANGED
|
@@ -4,22 +4,64 @@ import vm from 'node:vm'
|
|
|
4
4
|
const FORBIDDEN = new Set(['Date.now', 'Math.random', 'crypto.randomUUID'])
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
7
|
+
* Wrap a host function so `.constructor` / `.prototype` / `__proto__` access
|
|
8
|
+
* cannot reach the host `Function` constructor (blocks
|
|
9
|
+
* `fn.constructor('return process')()` and friends).
|
|
10
|
+
*/
|
|
11
|
+
function sealFunction<T extends (...args: never[]) => unknown>(fn: T): T {
|
|
12
|
+
return new Proxy(fn, {
|
|
13
|
+
get(target, prop) {
|
|
14
|
+
if (prop === 'constructor' || prop === 'prototype' || prop === '__proto__') {
|
|
15
|
+
return undefined
|
|
16
|
+
}
|
|
17
|
+
return Reflect.get(target, prop)
|
|
18
|
+
},
|
|
19
|
+
apply(target, thisArg, argArray) {
|
|
20
|
+
return Reflect.apply(target, thisArg, argArray)
|
|
21
|
+
},
|
|
22
|
+
}) as T
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Best-effort seal a value: freeze objects and wrap function properties so
|
|
27
|
+
* their constructor chain can't reach host globals.
|
|
28
|
+
*/
|
|
29
|
+
export function sealValue(value: unknown, depth = 0): unknown {
|
|
30
|
+
if (depth > 4) return value
|
|
31
|
+
if (typeof value === 'function') {
|
|
32
|
+
return sealFunction(value as (...args: never[]) => unknown)
|
|
33
|
+
}
|
|
34
|
+
if (value && typeof value === 'object') {
|
|
35
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
36
|
+
const child = (value as Record<string, unknown>)[key]
|
|
37
|
+
if (typeof child === 'function') {
|
|
38
|
+
;(value as Record<string, unknown>)[key] = sealFunction(
|
|
39
|
+
child as (...args: never[]) => unknown,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return Object.freeze(value)
|
|
44
|
+
}
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a VM context for workflow script execution.
|
|
11
50
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
51
|
+
* SECURITY: node:vm is NOT a security boundary. Workflow scripts are
|
|
52
|
+
* model-authored code running in the same process — the model already has
|
|
53
|
+
* Bash access, so this context does NOT isolate the script from the host.
|
|
54
|
+
* Its purpose is determinism (blocking Date.now/Math.random/randomUUID for
|
|
55
|
+
* reproducible replay) plus best-effort blocking of common escape vectors.
|
|
56
|
+
* Do not treat this as a sandbox that can safely run untrusted code.
|
|
15
57
|
*/
|
|
16
58
|
export function createSandbox(
|
|
17
59
|
args: unknown,
|
|
18
60
|
budget: { total: number | null; spent(): number; remaining(): number },
|
|
19
61
|
): vm.Context {
|
|
20
62
|
const sandboxObj: Record<string, unknown> = {
|
|
21
|
-
args,
|
|
22
|
-
budget,
|
|
63
|
+
args: sealValue(args),
|
|
64
|
+
budget: sealValue(budget),
|
|
23
65
|
console: {
|
|
24
66
|
log: (..._a: unknown[]) => {}, // no-op in sandbox
|
|
25
67
|
error: (..._a: unknown[]) => {},
|