@swarmclawai/swarmclaw 1.2.6 → 1.2.8

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 (112) hide show
  1. package/README.md +24 -17
  2. package/next.config.ts +1 -0
  3. package/package.json +3 -2
  4. package/scripts/easy-setup.mjs +1 -1
  5. package/scripts/postinstall.mjs +1 -1
  6. package/skills/swarmclaw.md +115 -0
  7. package/skills/tools/browser.md +131 -0
  8. package/skills/tools/execute.md +98 -0
  9. package/skills/tools/files.md +98 -0
  10. package/skills/tools/memory.md +104 -0
  11. package/skills/tools/platform.md +144 -0
  12. package/skills/tools/skills.md +83 -0
  13. package/src/app/api/chats/[id]/messages/route.ts +23 -19
  14. package/src/app/api/chats/messages-route.test.ts +105 -51
  15. package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
  16. package/src/app/api/openclaw/deploy/route.ts +2 -0
  17. package/src/app/api/setup/doctor/route.ts +4 -4
  18. package/src/components/agents/agent-chat-list.tsx +23 -1
  19. package/src/components/agents/inspector-panel.tsx +165 -48
  20. package/src/components/chat/chat-area.tsx +38 -9
  21. package/src/components/chat/message-list.tsx +33 -19
  22. package/src/components/gateways/gateway-sheet.tsx +5 -2
  23. package/src/lib/agent-execute-defaults.test.ts +24 -0
  24. package/src/lib/agent-execute-defaults.ts +62 -0
  25. package/src/lib/chat/queued-message-queue.test.ts +134 -1
  26. package/src/lib/chat/queued-message-queue.ts +77 -2
  27. package/src/lib/server/agents/agent-service.ts +5 -0
  28. package/src/lib/server/builtin-extensions.ts +1 -0
  29. package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
  30. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +1 -0
  31. package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -2
  32. package/src/lib/server/chat-execution/chat-turn-preparation.ts +79 -42
  33. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
  34. package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
  35. package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
  36. package/src/lib/server/chat-execution/message-classifier.ts +11 -1
  37. package/src/lib/server/chat-execution/prompt-builder.test.ts +28 -0
  38. package/src/lib/server/chat-execution/prompt-builder.ts +14 -1
  39. package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
  40. package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
  41. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +6 -4
  42. package/src/lib/server/chat-execution/stream-agent-chat.ts +45 -16
  43. package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
  44. package/src/lib/server/connectors/discord.ts +2 -2
  45. package/src/lib/server/connectors/matrix.ts +3 -2
  46. package/src/lib/server/connectors/signal.ts +5 -4
  47. package/src/lib/server/connectors/slack.ts +10 -9
  48. package/src/lib/server/connectors/teams.ts +3 -2
  49. package/src/lib/server/connectors/telegram.ts +4 -4
  50. package/src/lib/server/connectors/whatsapp.ts +2 -2
  51. package/src/lib/server/daemon/controller.ts +7 -0
  52. package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
  53. package/src/lib/server/messages/message-repository.test.ts +70 -0
  54. package/src/lib/server/messages/message-repository.ts +11 -6
  55. package/src/lib/server/openclaw/deploy.ts +32 -2
  56. package/src/lib/server/plugins-advanced.test.ts +1 -2
  57. package/src/lib/server/provider-health.ts +1 -1
  58. package/src/lib/server/runtime/process-manager.ts +13 -9
  59. package/src/lib/server/runtime/session-run-manager/queries.ts +15 -0
  60. package/src/lib/server/runtime/session-run-manager.test.ts +58 -0
  61. package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
  62. package/src/lib/server/sandbox/session-runtime.ts +40 -28
  63. package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
  64. package/src/lib/server/session-tools/context.ts +1 -1
  65. package/src/lib/server/session-tools/credential-env.ts +109 -0
  66. package/src/lib/server/session-tools/crud.ts +3 -3
  67. package/src/lib/server/session-tools/edit_file.ts +3 -2
  68. package/src/lib/server/session-tools/execute.test.ts +58 -0
  69. package/src/lib/server/session-tools/execute.ts +334 -0
  70. package/src/lib/server/session-tools/files-tool.ts +635 -0
  71. package/src/lib/server/session-tools/index.ts +14 -4
  72. package/src/lib/server/session-tools/memory-tool.ts +242 -0
  73. package/src/lib/server/session-tools/memory.ts +1 -1
  74. package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
  75. package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
  76. package/src/lib/server/session-tools/platform-tool.ts +617 -0
  77. package/src/lib/server/session-tools/session-info.ts +3 -2
  78. package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
  79. package/src/lib/server/session-tools/shell.ts +7 -122
  80. package/src/lib/server/session-tools/skills-tool.ts +396 -0
  81. package/src/lib/server/session-tools/web.ts +2 -2
  82. package/src/lib/server/storage-normalization.ts +2 -0
  83. package/src/lib/server/tool-aliases.ts +2 -1
  84. package/src/lib/server/tool-capability-policy-advanced.test.ts +9 -2
  85. package/src/lib/server/tool-capability-policy.test.ts +2 -1
  86. package/src/lib/server/tool-capability-policy.ts +60 -33
  87. package/src/lib/server/tool-planning.ts +11 -0
  88. package/src/lib/setup-defaults.ts +5 -0
  89. package/src/lib/tool-definitions.ts +1 -0
  90. package/src/lib/validation/schemas.test.ts +16 -0
  91. package/src/lib/validation/schemas.ts +16 -0
  92. package/src/stores/use-chat-store.test.ts +231 -0
  93. package/src/stores/use-chat-store.ts +62 -13
  94. package/src/types/agent.ts +348 -0
  95. package/src/types/app-settings.ts +175 -0
  96. package/src/types/approval.ts +27 -0
  97. package/src/types/connector.ts +187 -0
  98. package/src/types/extension.ts +386 -0
  99. package/src/types/index.ts +16 -3555
  100. package/src/types/message.ts +57 -0
  101. package/src/types/misc.ts +739 -0
  102. package/src/types/mission.ts +185 -0
  103. package/src/types/protocol.ts +422 -0
  104. package/src/types/provider.ts +52 -0
  105. package/src/types/run.ts +183 -0
  106. package/src/types/schedule.ts +59 -0
  107. package/src/types/session.ts +265 -0
  108. package/src/types/skill.ts +157 -0
  109. package/src/types/task.ts +140 -0
  110. package/src/types/working-state.ts +211 -0
  111. package/src/views/settings/section-heartbeat.tsx +2 -2
  112. package/src/lib/server/session-tools/sandbox.ts +0 -281
@@ -0,0 +1,140 @@
1
+ import type { MissionSummary } from './mission'
2
+ import type { GoalContract } from './app-settings'
3
+
4
+ // --- Task Board ---
5
+
6
+ export type BoardTaskStatus = 'backlog' | 'queued' | 'running' | 'completed' | 'failed' | 'cancelled' | 'archived' | 'deferred'
7
+
8
+ export interface TaskComment {
9
+ id: string
10
+ author: string // agent name or 'user'
11
+ agentId?: string // if from an agent
12
+ text: string
13
+ createdAt: number
14
+ }
15
+
16
+ export interface TaskQualityGateConfig {
17
+ enabled?: boolean
18
+ minResultChars?: number
19
+ minEvidenceItems?: number
20
+ requireVerification?: boolean
21
+ requireArtifact?: boolean
22
+ requireReport?: boolean
23
+ }
24
+
25
+ export interface BoardTask {
26
+ id: string
27
+ title: string
28
+ description: string
29
+ status: BoardTaskStatus
30
+ agentId: string
31
+ missionId?: string | null
32
+ protocolRunId?: string | null
33
+ missionSummary?: MissionSummary | null
34
+ projectId?: string
35
+ goalContract?: GoalContract | null
36
+ cwd?: string | null
37
+ file?: string | null
38
+ sessionId?: string | null
39
+ completionReportPath?: string | null
40
+ result?: string | null
41
+ error?: string | null
42
+ outputFiles?: string[]
43
+ artifacts?: Array<{
44
+ url: string
45
+ type: 'image' | 'video' | 'pdf' | 'file'
46
+ filename: string
47
+ }>
48
+ comments?: TaskComment[]
49
+ images?: string[]
50
+ createdByAgentId?: string | null
51
+ createdInSessionId?: string | null
52
+ followupConnectorId?: string | null
53
+ followupChannelId?: string | null
54
+ followupThreadId?: string | null
55
+ followupSenderId?: string | null
56
+ followupSenderName?: string | null
57
+ delegatedByAgentId?: string | null
58
+ delegatedFromTaskId?: string | null
59
+ delegationDepth?: number | null
60
+ createdAt: number
61
+ updatedAt: number
62
+ queuedAt?: number | null
63
+ startedAt?: number | null
64
+ completedAt?: number | null
65
+ archivedAt?: number | null
66
+ attempts?: number
67
+ maxAttempts?: number
68
+ retryBackoffSec?: number
69
+ retryScheduledAt?: number | null
70
+ runNumber?: number
71
+ totalRuns?: number
72
+ totalCompleted?: number
73
+ totalFailed?: number
74
+ sourceType?: 'schedule' | 'delegation' | 'manual' | 'import'
75
+ sourceScheduleId?: string | null
76
+ sourceScheduleName?: string | null
77
+ sourceScheduleKey?: string | null
78
+ externalSource?: {
79
+ source: string
80
+ id?: string | null
81
+ repo?: string | null
82
+ number?: number | null
83
+ state?: string | null
84
+ labels?: string[]
85
+ assignee?: string | null
86
+ url?: string | null
87
+ } | null
88
+ lastActivityAt?: number | null
89
+ deferredReason?: string | null
90
+ deadLetteredAt?: number | null
91
+ cliResumeId?: string | null
92
+ cliProvider?: string | null
93
+ claudeResumeId?: string | null
94
+ codexResumeId?: string | null
95
+ opencodeResumeId?: string | null
96
+ geminiResumeId?: string | null
97
+ checkpoint?: {
98
+ lastRunId?: string | null
99
+ lastSessionId?: string | null
100
+ note?: string | null
101
+ updatedAt: number
102
+ } | null
103
+ validation?: {
104
+ ok: boolean
105
+ reasons: string[]
106
+ checkedAt: number
107
+ } | null
108
+ // Parent/child task hierarchy (user-created subtasks)
109
+ parentTaskId?: string | null
110
+ subtaskIds?: string[]
111
+ // Task dependencies (DAG)
112
+ blockedBy?: string[]
113
+ blocks?: string[]
114
+ // Task tags
115
+ tags?: string[]
116
+ // Due date
117
+ dueAt?: number | null
118
+ // Custom fields
119
+ customFields?: Record<string, string | number | boolean>
120
+ // Priority
121
+ priority?: 'low' | 'medium' | 'high' | 'critical'
122
+ // Dedup fingerprint
123
+ fingerprint?: string
124
+ qualityGate?: TaskQualityGateConfig | null
125
+ // Competitive task claiming (pool mode)
126
+ assignmentMode?: 'direct' | 'pool'
127
+ poolCandidateAgentIds?: string[]
128
+ claimedByAgentId?: string | null
129
+ claimedAt?: number | null
130
+ requiredCapabilities?: string[]
131
+ // Upstream task results (populated by cascadeUnblock when dependencies complete)
132
+ upstreamResults?: Array<{
133
+ taskId: string
134
+ taskTitle: string
135
+ agentId: string | null
136
+ resultPreview: string | null
137
+ }>
138
+ repairRunId?: string | null
139
+ lastRepairAttemptAt?: number | null
140
+ }
@@ -0,0 +1,211 @@
1
+ import type { MissionStatus, MissionPhase, MissionWaitState } from './mission'
2
+
3
+ export type WorkingStateStatus = 'idle' | 'progress' | 'blocked' | 'waiting' | 'completed'
4
+ export type WorkingStateItemStatus = 'active' | 'resolved' | 'superseded'
5
+
6
+ export interface EvidenceRef {
7
+ id: string
8
+ type: 'tool' | 'message' | 'mission' | 'task' | 'artifact' | 'error' | 'approval'
9
+ summary: string
10
+ value?: string | null
11
+ toolName?: string | null
12
+ toolCallId?: string | null
13
+ runId?: string | null
14
+ sessionId?: string | null
15
+ missionId?: string | null
16
+ taskId?: string | null
17
+ createdAt: number
18
+ }
19
+
20
+ export interface WorkingPlanStep {
21
+ id: string
22
+ text: string
23
+ status: WorkingStateItemStatus
24
+ createdAt: number
25
+ updatedAt: number
26
+ }
27
+
28
+ export interface WorkingFact {
29
+ id: string
30
+ statement: string
31
+ source: 'user' | 'tool' | 'assistant' | 'mission' | 'system'
32
+ status: WorkingStateItemStatus
33
+ evidenceIds?: string[]
34
+ createdAt: number
35
+ updatedAt: number
36
+ }
37
+
38
+ export interface WorkingArtifact {
39
+ id: string
40
+ label: string
41
+ kind: 'file' | 'url' | 'approval' | 'message' | 'other'
42
+ path?: string | null
43
+ url?: string | null
44
+ sourceTool?: string | null
45
+ status: WorkingStateItemStatus
46
+ evidenceIds?: string[]
47
+ createdAt: number
48
+ updatedAt: number
49
+ }
50
+
51
+ export interface WorkingDecision {
52
+ id: string
53
+ summary: string
54
+ rationale?: string | null
55
+ status: WorkingStateItemStatus
56
+ evidenceIds?: string[]
57
+ createdAt: number
58
+ updatedAt: number
59
+ }
60
+
61
+ export interface WorkingBlocker {
62
+ id: string
63
+ summary: string
64
+ kind?: 'approval' | 'credential' | 'human_input' | 'external_dependency' | 'error' | 'other' | null
65
+ nextAction?: string | null
66
+ status: WorkingStateItemStatus
67
+ evidenceIds?: string[]
68
+ createdAt: number
69
+ updatedAt: number
70
+ }
71
+
72
+ export interface WorkingQuestion {
73
+ id: string
74
+ question: string
75
+ status: WorkingStateItemStatus
76
+ evidenceIds?: string[]
77
+ createdAt: number
78
+ updatedAt: number
79
+ }
80
+
81
+ export interface WorkingHypothesis {
82
+ id: string
83
+ statement: string
84
+ confidence?: 'low' | 'medium' | 'high' | null
85
+ status: WorkingStateItemStatus
86
+ evidenceIds?: string[]
87
+ createdAt: number
88
+ updatedAt: number
89
+ }
90
+
91
+ export interface WorkingPlanStepPatch {
92
+ id?: string | null
93
+ text: string
94
+ status?: WorkingStateItemStatus | null
95
+ }
96
+
97
+ export interface WorkingFactPatch {
98
+ id?: string | null
99
+ statement: string
100
+ source?: WorkingFact['source'] | null
101
+ status?: WorkingStateItemStatus | null
102
+ evidenceIds?: string[]
103
+ }
104
+
105
+ export interface WorkingArtifactPatch {
106
+ id?: string | null
107
+ label: string
108
+ kind?: WorkingArtifact['kind'] | null
109
+ path?: string | null
110
+ url?: string | null
111
+ sourceTool?: string | null
112
+ status?: WorkingStateItemStatus | null
113
+ evidenceIds?: string[]
114
+ }
115
+
116
+ export interface WorkingDecisionPatch {
117
+ id?: string | null
118
+ summary: string
119
+ rationale?: string | null
120
+ status?: WorkingStateItemStatus | null
121
+ evidenceIds?: string[]
122
+ }
123
+
124
+ export interface WorkingBlockerPatch {
125
+ id?: string | null
126
+ summary: string
127
+ kind?: WorkingBlocker['kind']
128
+ nextAction?: string | null
129
+ status?: WorkingStateItemStatus | null
130
+ evidenceIds?: string[]
131
+ }
132
+
133
+ export interface WorkingQuestionPatch {
134
+ id?: string | null
135
+ question: string
136
+ status?: WorkingStateItemStatus | null
137
+ evidenceIds?: string[]
138
+ }
139
+
140
+ export interface WorkingHypothesisPatch {
141
+ id?: string | null
142
+ statement: string
143
+ confidence?: WorkingHypothesis['confidence']
144
+ status?: WorkingStateItemStatus | null
145
+ evidenceIds?: string[]
146
+ }
147
+
148
+ export interface WorkingStatePatch {
149
+ objective?: string | null
150
+ summary?: string | null
151
+ constraints?: string[]
152
+ successCriteria?: string[]
153
+ status?: WorkingStateStatus | null
154
+ nextAction?: string | null
155
+ planSteps?: WorkingPlanStepPatch[]
156
+ factsUpsert?: WorkingFactPatch[]
157
+ artifactsUpsert?: WorkingArtifactPatch[]
158
+ decisionsAppend?: WorkingDecisionPatch[]
159
+ blockersUpsert?: WorkingBlockerPatch[]
160
+ questionsUpsert?: WorkingQuestionPatch[]
161
+ hypothesesUpsert?: WorkingHypothesisPatch[]
162
+ evidenceAppend?: EvidenceRef[]
163
+ supersedeIds?: string[]
164
+ }
165
+
166
+ export interface SessionWorkingState {
167
+ sessionId: string
168
+ missionId?: string | null
169
+ objective?: string | null
170
+ summary?: string | null
171
+ constraints: string[]
172
+ successCriteria: string[]
173
+ status: WorkingStateStatus
174
+ nextAction?: string | null
175
+ planSteps: WorkingPlanStep[]
176
+ confirmedFacts: WorkingFact[]
177
+ artifacts: WorkingArtifact[]
178
+ decisions: WorkingDecision[]
179
+ blockers: WorkingBlocker[]
180
+ openQuestions: WorkingQuestion[]
181
+ hypotheses: WorkingHypothesis[]
182
+ evidenceRefs: EvidenceRef[]
183
+ createdAt: number
184
+ updatedAt: number
185
+ lastCompactedAt?: number | null
186
+ }
187
+
188
+ export interface ExecutionBriefPlanStep {
189
+ text: string
190
+ status: WorkingStateItemStatus
191
+ }
192
+
193
+ export interface ExecutionBrief {
194
+ sessionId?: string | null
195
+ missionId?: string | null
196
+ objective: string | null
197
+ summary: string | null
198
+ status: WorkingStateStatus
199
+ nextAction: string | null
200
+ plan: ExecutionBriefPlanStep[]
201
+ blockers: string[]
202
+ facts: string[]
203
+ artifacts: string[]
204
+ constraints: string[]
205
+ successCriteria: string[]
206
+ missionStatus?: MissionStatus | null
207
+ missionPhase?: MissionPhase | null
208
+ waitState?: MissionWaitState | null
209
+ evidenceRefs: EvidenceRef[]
210
+ parentContext: string | null
211
+ }
@@ -35,8 +35,8 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
35
35
  setHeartbeatBulkNotice(
36
36
  `Stopped heartbeat on ${result.updatedSessions} session(s); cancelled ${result.cancelledQueued} queued run(s), aborted ${result.abortedRunning} running run(s).`,
37
37
  )
38
- } catch (err: any) {
39
- setHeartbeatBulkNotice(err?.message || 'Failed to disable heartbeat for all agents.')
38
+ } catch (err: unknown) {
39
+ setHeartbeatBulkNotice(err instanceof Error ? err.message : 'Failed to disable heartbeat for all agents.')
40
40
  } finally {
41
41
  setDisablingHeartbeats(false)
42
42
  }
@@ -1,281 +0,0 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
- import { spawnSync } from 'child_process'
4
- import { UPLOAD_DIR } from '../storage'
5
- import { truncate, MAX_OUTPUT } from './context'
6
- import type { Session } from '@/types'
7
- import { normalizeToolInputArgs } from './normalize-tool-args'
8
- import { detectDocker } from '@/lib/server/sandbox/docker-detect'
9
- import {
10
- ensureSessionSandbox,
11
- resolveSandboxRuntimeStatus,
12
- resolveSandboxWorkdir,
13
- type AgentSandboxConfig,
14
- } from '@/lib/server/sandbox/session-runtime'
15
- import { buildDockerExecArgs } from '@/lib/server/runtime/process-manager'
16
-
17
- export type SandboxContext = {
18
- sessionId?: string
19
- cwd?: string
20
- agentId?: string | null
21
- config?: AgentSandboxConfig | null
22
- resolveCurrentSession?: () => Session | null
23
- }
24
-
25
- const EXT_MAP: Record<string, string> = {
26
- javascript: 'js',
27
- typescript: 'ts',
28
- }
29
-
30
- function sandboxUnavailableError(reason: string): string {
31
- return JSON.stringify({
32
- error: reason,
33
- guidance: [
34
- 'Install Docker Desktop to keep sandbox_exec inside a container.',
35
- 'Use http_request for straightforward API calls.',
36
- 'Use extension_creator plus manage_schedules for recurring automations.',
37
- ],
38
- })
39
- }
40
-
41
- function quoteShell(value: string): string {
42
- return `'${value.replace(/'/g, `'\\''`)}'`
43
- }
44
-
45
- function createSandboxDir(baseCwd: string, sessionId: string): string {
46
- const root = path.join(baseCwd, '.swarmclaw-sandbox')
47
- fs.mkdirSync(root, { recursive: true })
48
- return fs.mkdtempSync(path.join(root, `${sessionId}-`))
49
- }
50
-
51
- function collectArtifacts(params: {
52
- sandboxDir: string
53
- ignoredFiles: Set<string>
54
- }): Array<{ name: string; url: string }> {
55
- const artifacts: { name: string; url: string }[] = []
56
- try {
57
- const files = fs.readdirSync(params.sandboxDir)
58
- for (const file of files) {
59
- if (params.ignoredFiles.has(file)) continue
60
- const src = path.join(params.sandboxDir, file)
61
- if (!fs.statSync(src).isFile()) continue
62
- fs.mkdirSync(UPLOAD_DIR, { recursive: true })
63
- const destName = `sandbox-${Date.now()}-${file}`
64
- const dest = path.join(UPLOAD_DIR, destName)
65
- fs.copyFileSync(src, dest)
66
- artifacts.push({ name: file, url: `/api/uploads/${encodeURIComponent(destName)}` })
67
- }
68
- } catch {
69
- // ignore artifact collection failures
70
- }
71
- return artifacts
72
- }
73
-
74
- function executeHostNode(params: {
75
- sandboxDir: string
76
- language: string
77
- scriptFile: string
78
- timeout: number
79
- }): {
80
- runtime: 'host'
81
- stdout: string
82
- stderr: string
83
- exitCode: number
84
- timedOut: boolean
85
- } {
86
- const tmpDir = path.join(params.sandboxDir, '.tmp')
87
- fs.mkdirSync(tmpDir, { recursive: true })
88
- const args = params.language === 'typescript'
89
- ? ['--no-warnings=ExperimentalWarning', '--experimental-strip-types', params.scriptFile]
90
- : [params.scriptFile]
91
- const result = spawnSync(process.execPath, args, {
92
- cwd: params.sandboxDir,
93
- encoding: 'utf-8',
94
- timeout: params.timeout,
95
- maxBuffer: MAX_OUTPUT,
96
- env: {
97
- ...process.env,
98
- HOME: params.sandboxDir,
99
- TMPDIR: tmpDir,
100
- WORKSPACE: params.sandboxDir,
101
- SESSION_CWD: params.sandboxDir,
102
- SWARMCLAW_SANDBOX_MODE: 'host',
103
- },
104
- })
105
- return {
106
- runtime: 'host',
107
- stdout: truncate((result.stdout || '').toString(), MAX_OUTPUT),
108
- stderr: truncate((result.stderr || '').toString(), MAX_OUTPUT),
109
- exitCode: result.status ?? (result.error ? 1 : 0),
110
- timedOut: !!(result.error?.message?.includes('ETIMEDOUT') || result.signal === 'SIGTERM'),
111
- }
112
- }
113
-
114
- async function executeContainerNode(params: {
115
- sandboxDir: string
116
- language: string
117
- scriptFile: string
118
- timeout: number
119
- context: SandboxContext
120
- }): Promise<{
121
- runtime: 'container'
122
- stdout: string
123
- stderr: string
124
- exitCode: number
125
- timedOut: boolean
126
- }> {
127
- const session = params.context.resolveCurrentSession?.() ?? null
128
- const sandbox = await ensureSessionSandbox({
129
- config: params.context.config,
130
- session,
131
- agentId: params.context.agentId ?? session?.agentId ?? null,
132
- sessionId: params.context.sessionId ?? session?.id ?? null,
133
- workspaceDir: params.context.cwd || process.cwd(),
134
- })
135
-
136
- if (!sandbox) {
137
- throw new Error('Container sandbox is not active for this session.')
138
- }
139
-
140
- const tmpDir = path.join(params.sandboxDir, '.tmp')
141
- fs.mkdirSync(tmpDir, { recursive: true })
142
- const resolved = resolveSandboxWorkdir({
143
- workspaceDir: sandbox.workspaceDir,
144
- hostWorkdir: params.sandboxDir,
145
- containerWorkdir: sandbox.containerWorkdir,
146
- })
147
- const containerCommand = params.language === 'typescript'
148
- ? `node --no-warnings=ExperimentalWarning --experimental-strip-types ${quoteShell(params.scriptFile)}`
149
- : `node ${quoteShell(params.scriptFile)}`
150
- const result = spawnSync('docker', buildDockerExecArgs({
151
- containerName: sandbox.containerName,
152
- command: containerCommand,
153
- workdir: resolved.containerWorkdir,
154
- env: {
155
- HOME: resolved.containerWorkdir,
156
- TMPDIR: path.posix.join(resolved.containerWorkdir, '.tmp'),
157
- WORKSPACE: sandbox.containerWorkdir,
158
- SESSION_CWD: resolved.containerWorkdir,
159
- SWARMCLAW_SANDBOX_MODE: 'container',
160
- },
161
- }), {
162
- encoding: 'utf-8',
163
- timeout: params.timeout,
164
- maxBuffer: MAX_OUTPUT,
165
- })
166
-
167
- return {
168
- runtime: 'container',
169
- stdout: truncate((result.stdout || '').toString(), MAX_OUTPUT),
170
- stderr: truncate((result.stderr || '').toString(), MAX_OUTPUT),
171
- exitCode: result.status ?? (result.error ? 1 : 0),
172
- timedOut: !!(result.error?.message?.includes('ETIMEDOUT') || result.signal === 'SIGTERM'),
173
- }
174
- }
175
-
176
- export async function executeSandboxExec(args: unknown, context: SandboxContext) {
177
- const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
178
- const language = normalized.language as string
179
- const code = normalized.code as string
180
- const timeoutSec = normalized.timeoutSec as number | undefined
181
- const timeout = Math.min(Math.max(timeoutSec ?? 60, 5), 300) * 1000
182
- const ext = EXT_MAP[language]
183
- const sessionId = context.sessionId ?? 'unknown'
184
- const cwd = context.cwd || process.cwd()
185
-
186
- if (language !== 'javascript' && language !== 'typescript') {
187
- return sandboxUnavailableError('sandbox_exec currently supports only JavaScript and TypeScript via Node.js.')
188
- }
189
-
190
- let sandboxDir: string | null = null
191
- try {
192
- sandboxDir = createSandboxDir(cwd, sessionId)
193
- const sandboxRoot = sandboxDir
194
- const scriptFile = `script.${ext}`
195
- fs.writeFileSync(path.join(sandboxRoot, 'package.json'), JSON.stringify({ type: 'module' }), 'utf-8')
196
- fs.writeFileSync(path.join(sandboxRoot, scriptFile), code, 'utf-8')
197
-
198
- const warnings: string[] = []
199
- const docker = detectDocker()
200
- const runtimeResult = docker.available
201
- ? await executeContainerNode({
202
- sandboxDir: sandboxRoot,
203
- language,
204
- scriptFile,
205
- timeout,
206
- context,
207
- }).catch((err: unknown) => {
208
- warnings.push(err instanceof Error ? err.message : 'Container sandbox unavailable; used host Node fallback.')
209
- return executeHostNode({
210
- sandboxDir: sandboxRoot,
211
- language,
212
- scriptFile,
213
- timeout,
214
- })
215
- })
216
- : (() => {
217
- warnings.push('Docker is not available; used host Node fallback.')
218
- return executeHostNode({
219
- sandboxDir: sandboxRoot,
220
- language,
221
- scriptFile,
222
- timeout,
223
- })
224
- })()
225
-
226
- const artifacts = collectArtifacts({
227
- sandboxDir: sandboxRoot,
228
- ignoredFiles: new Set([scriptFile, 'package.json']),
229
- })
230
-
231
- return JSON.stringify({
232
- runtime: runtimeResult.runtime,
233
- exitCode: runtimeResult.exitCode,
234
- timedOut: runtimeResult.timedOut,
235
- stdout: runtimeResult.stdout,
236
- stderr: runtimeResult.stderr,
237
- artifacts,
238
- ...(warnings.length ? { warnings } : {}),
239
- })
240
- } catch (err: unknown) {
241
- return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
242
- } finally {
243
- if (sandboxDir) {
244
- try { fs.rmSync(sandboxDir, { recursive: true, force: true }) } catch { /* ignore */ }
245
- }
246
- }
247
- }
248
-
249
- export async function executeListRuntimes(context: SandboxContext) {
250
- const docker = detectDocker()
251
- const session = context.resolveCurrentSession?.() ?? null
252
- const status = resolveSandboxRuntimeStatus({
253
- config: context.config,
254
- session,
255
- agentId: context.agentId ?? session?.agentId ?? null,
256
- sessionId: context.sessionId ?? session?.id ?? null,
257
- })
258
-
259
- return JSON.stringify({
260
- node: {
261
- available: true,
262
- version: process.version,
263
- supportsTypeScript: true,
264
- },
265
- docker,
266
- sandbox: {
267
- enabledByConfig: Boolean(context.config?.enabled),
268
- sandboxedForSession: status.sandboxed,
269
- mode: status.mode,
270
- scope: status.scope,
271
- scopeKey: status.scopeKey,
272
- executionMode: docker.available && status.sandboxed ? 'container' : 'host',
273
- browserEnabledByConfig: context.config?.browser?.enabled === true,
274
- },
275
- guidance: docker.available
276
- ? []
277
- : ['Install Docker Desktop to keep shell, browser, and sandbox_exec inside containers.'],
278
- })
279
- }
280
-
281
- // Execution functions (executeSandboxExec, executeListRuntimes) are kept for shell.ts to import.