@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.
Files changed (69) hide show
  1. package/bin/mipham.ts +25 -0
  2. package/package.json +1 -1
  3. package/src/agent/cross-session/discovery.ts +45 -1
  4. package/src/agent/cross-session/file-inbox.ts +15 -0
  5. package/src/agent/message-router.ts +22 -2
  6. package/src/agent/sub-agent.ts +4 -3
  7. package/src/config/defaults.ts +19 -1
  8. package/src/config/keys-manager.ts +11 -1
  9. package/src/config/loader.ts +20 -10
  10. package/src/core/context.ts +86 -56
  11. package/src/core/credential-masker/output-scrub.ts +5 -0
  12. package/src/core/crsi-sandbox.ts +19 -7
  13. package/src/core/engine.ts +69 -35
  14. package/src/core/instructions.ts +4 -42
  15. package/src/core/permission.ts +7 -1
  16. package/src/core/red-team.ts +20 -5
  17. package/src/core/self-critique.ts +3 -1
  18. package/src/core/session-log.ts +225 -0
  19. package/src/core/session-store.ts +119 -34
  20. package/src/core/workspace-trust.ts +25 -2
  21. package/src/daemon/auth.ts +9 -6
  22. package/src/daemon/cors.ts +24 -16
  23. package/src/daemon/rate-limiter.ts +4 -0
  24. package/src/daemon/server.ts +24 -3
  25. package/src/i18n-core/locales/en-US.json +4 -0
  26. package/src/i18n-core/locales/zh-CN.json +4 -0
  27. package/src/index.tsx +62 -30
  28. package/src/mcp/client.ts +56 -17
  29. package/src/mcp/http-transport.ts +2 -0
  30. package/src/mcp/oauth.ts +1 -1
  31. package/src/providers/llm-replay.ts +36 -0
  32. package/src/providers/llm.ts +16 -0
  33. package/src/providers/registry.ts +2 -1
  34. package/src/security/gate.ts +10 -0
  35. package/src/security/url.ts +37 -14
  36. package/src/shared/constants.ts +4 -486
  37. package/src/shared/types.ts +6 -437
  38. package/src/skills/fork-executor.ts +3 -1
  39. package/src/skills/loader.ts +2 -1
  40. package/src/skills/seam.ts +18 -0
  41. package/src/tools/agent/agent.ts +1 -0
  42. package/src/tools/agent/skill.ts +1 -0
  43. package/src/tools/exec/bash.ts +141 -129
  44. package/src/tools/exec/git.ts +17 -0
  45. package/src/tools/file/read.ts +73 -66
  46. package/src/tools/file/write.ts +1 -1
  47. package/src/tools/index.ts +54 -132
  48. package/src/tools/network/web-fetch.ts +41 -26
  49. package/src/tools/seam.ts +25 -0
  50. package/src/tools/validation.ts +91 -0
  51. package/src/ui/app.tsx +36 -0
  52. package/src/ui/commands.ts +27 -6
  53. package/src/ui/input.tsx +21 -0
  54. package/src/vajra/compose/assemble.ts +19 -0
  55. package/src/vajra/compose/bundle.ts +39 -0
  56. package/src/vajra/compose/dump.ts +5 -0
  57. package/src/vajra/compose/index.ts +4 -0
  58. package/src/vajra/compose/mount.ts +30 -0
  59. package/src/vajra/context.ts +183 -0
  60. package/src/vajra/events.ts +13 -0
  61. package/src/vajra/index.ts +4 -0
  62. package/src/vajra/leaf/plan-runner.ts +94 -0
  63. package/src/vajra/service.ts +15 -0
  64. package/src/workflow/primitives/agent.ts +3 -1
  65. package/src/workflow/runtime.ts +23 -15
  66. package/src/workflow/sandbox.ts +51 -9
  67. package/src/core/compaction-progress.ts +0 -205
  68. package/src/core/context-compact.ts +0 -124
  69. 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,4 @@
1
+ export { Context } from './context'
2
+ export type { Disposer } from './context'
3
+ export type { DispatchMode, EventMap, EventsOfMode } from './events'
4
+ export type { Service, ServiceStatus, Mounted } from './service'
@@ -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 = ''
@@ -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(prompt, registry, toolRegistry, {
128
- ...(opts || {}),
129
- permissionSystem: permission,
130
- } as Record<string, unknown>)
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 sandbox — no access to host globals
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 sandbox context (whitelist approach)
167
- sandboxCtx.agent = agent
168
- sandboxCtx.parallel = parallel
169
- sandboxCtx.pipeline = pipeline
170
- sandboxCtx.verify = verify
171
- sandboxCtx.judge = judge
172
- sandboxCtx.loopUntilConvergence = loopUntilConvergence
173
- sandboxCtx.phase = wrappedPhase
174
- sandboxCtx.log = log
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 {
@@ -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
- * 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
+ * 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
- * P0-2 (v2.1.223 alignment): Added explicit defense-in-depth denial of all
13
- * host globals (process, require, Bun, fetch, setTimeout, etc.) and
14
- * constructor.constructor escape vector blocking.
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[]) => {},