@swarmclawai/swarmclaw 1.5.49 → 1.5.50
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
CHANGED
|
@@ -396,6 +396,10 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
396
396
|
|
|
397
397
|
## Releases
|
|
398
398
|
|
|
399
|
+
### v1.5.50 Highlights
|
|
400
|
+
|
|
401
|
+
- **Fix: opencode-web remote instances no longer fail with `EACCES`**: SwarmClaw used to send the local workspace path (e.g. `/root/.swarmclaw/workspace`) as a `directory=` query parameter on every opencode-web request. Remote opencode-web instances tried to `lstat` that path and rejected the call. The provider now auto-detects local vs. remote from the endpoint hostname (`localhost`, `127.0.0.1`, `::1`, `0.0.0.0`) and only sends `directory=` when the endpoint is local. Thanks to [@SteamedFish](https://github.com/SteamedFish) for the detailed root-cause writeup in [#45](https://github.com/swarmclawai/swarmclaw/issues/45).
|
|
402
|
+
|
|
399
403
|
### v1.5.49 Highlights
|
|
400
404
|
|
|
401
405
|
- **Autonomous Missions**: a new first-class concept for long-running, goal-driven agent work. Hand your agent team a goal on Friday, come back Monday to see what they shipped. Each mission carries a title, a natural-language objective, bulleted success criteria, hard budgets (USD, tokens, turns, wallclock), periodic markdown reports, and a full milestone timeline. Missions drive any session through the existing heartbeat pipeline, so delegation to Claude Code, Codex, OpenCode, Cursor, Droid, Goose, Qwen, or native SwarmClaw agents all work without changes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.50",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
parseModelId,
|
|
7
7
|
joinUrl,
|
|
8
8
|
SseLineParser,
|
|
9
|
+
isLocalEndpoint,
|
|
10
|
+
buildDirectoryQuery,
|
|
9
11
|
} from '@/lib/providers/opencode-web'
|
|
10
12
|
|
|
11
13
|
describe('opencode-web parseBasicAuth', () => {
|
|
@@ -77,6 +79,49 @@ describe('opencode-web joinUrl', () => {
|
|
|
77
79
|
})
|
|
78
80
|
})
|
|
79
81
|
|
|
82
|
+
describe('opencode-web isLocalEndpoint', () => {
|
|
83
|
+
it('returns true for loopback hostnames', () => {
|
|
84
|
+
assert.equal(isLocalEndpoint('http://localhost:4096'), true)
|
|
85
|
+
assert.equal(isLocalEndpoint('http://127.0.0.1:4096'), true)
|
|
86
|
+
assert.equal(isLocalEndpoint('http://[::1]:4096'), true)
|
|
87
|
+
assert.equal(isLocalEndpoint('http://0.0.0.0:4096'), true)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('honours https and no-port variants', () => {
|
|
91
|
+
assert.equal(isLocalEndpoint('https://localhost'), true)
|
|
92
|
+
assert.equal(isLocalEndpoint('https://127.0.0.1/'), true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('is case-insensitive on hostname', () => {
|
|
96
|
+
assert.equal(isLocalEndpoint('http://LOCALHOST:4096'), true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('returns false for public hostnames and LAN addresses', () => {
|
|
100
|
+
assert.equal(isLocalEndpoint('http://example.com'), false)
|
|
101
|
+
assert.equal(isLocalEndpoint('https://opencode.example.internal'), false)
|
|
102
|
+
assert.equal(isLocalEndpoint('http://192.168.1.100:4096'), false)
|
|
103
|
+
assert.equal(isLocalEndpoint('http://10.0.0.5:4096'), false)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('fails safe (remote) on malformed input', () => {
|
|
107
|
+
assert.equal(isLocalEndpoint('not-a-url'), false)
|
|
108
|
+
assert.equal(isLocalEndpoint(''), false)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('opencode-web buildDirectoryQuery', () => {
|
|
113
|
+
it('returns an empty string when cwd is null / undefined / empty', () => {
|
|
114
|
+
assert.equal(buildDirectoryQuery(null), '')
|
|
115
|
+
assert.equal(buildDirectoryQuery(undefined), '')
|
|
116
|
+
assert.equal(buildDirectoryQuery(''), '')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('returns a URL-encoded directory query when cwd is set', () => {
|
|
120
|
+
assert.equal(buildDirectoryQuery('/root/.swarmclaw/workspace'), '?directory=%2Froot%2F.swarmclaw%2Fworkspace')
|
|
121
|
+
assert.equal(buildDirectoryQuery('/tmp/has space'), '?directory=%2Ftmp%2Fhas%20space')
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
80
125
|
describe('opencode-web SseLineParser', () => {
|
|
81
126
|
it('emits one event per data: line and ignores comments / event: / id:', () => {
|
|
82
127
|
const events: unknown[] = []
|
|
@@ -60,6 +60,35 @@ export function joinUrl(baseUrl: string, path: string): string {
|
|
|
60
60
|
return `${base}${suffix}`
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0'])
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns true when the endpoint points at an opencode-web instance on the
|
|
67
|
+
* same machine as swarmclaw. Only local instances share swarmclaw's filesystem,
|
|
68
|
+
* so the `directory=` query parameter (which names a local workspace path) is
|
|
69
|
+
* only meaningful there. Remote instances get an `EACCES` when they try to
|
|
70
|
+
* `lstat` a path that exists only on the swarmclaw host (issue #45).
|
|
71
|
+
*
|
|
72
|
+
* Malformed URLs are treated as remote. That is the safe default: better to
|
|
73
|
+
* omit the directory hint than to leak a local path to an unknown host.
|
|
74
|
+
*/
|
|
75
|
+
export function isLocalEndpoint(endpoint: string): boolean {
|
|
76
|
+
try {
|
|
77
|
+
let hostname = new URL(endpoint).hostname
|
|
78
|
+
if (hostname.startsWith('[') && hostname.endsWith(']')) {
|
|
79
|
+
hostname = hostname.slice(1, -1)
|
|
80
|
+
}
|
|
81
|
+
return LOCAL_HOSTNAMES.has(hostname.toLowerCase())
|
|
82
|
+
} catch {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildDirectoryQuery(cwd: string | null | undefined): string {
|
|
88
|
+
if (!cwd) return ''
|
|
89
|
+
return `?directory=${encodeURIComponent(cwd)}`
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
/**
|
|
64
93
|
* Stateful SSE line parser. Buffers across chunk boundaries and emits
|
|
65
94
|
* one parsed JSON object per `data:` line. Lines that do not start with
|
|
@@ -125,11 +154,11 @@ interface CreateSessionResponse { id?: string; sessionID?: string; sessionId?: s
|
|
|
125
154
|
|
|
126
155
|
async function createSession(opts: {
|
|
127
156
|
endpoint: string
|
|
128
|
-
cwd: string
|
|
157
|
+
cwd: string | null
|
|
129
158
|
authHeader: string | undefined
|
|
130
159
|
signal: AbortSignal
|
|
131
160
|
}): Promise<string> {
|
|
132
|
-
const url = `${joinUrl(opts.endpoint, '/session')}
|
|
161
|
+
const url = `${joinUrl(opts.endpoint, '/session')}${buildDirectoryQuery(opts.cwd)}`
|
|
133
162
|
const res = await fetch(url, {
|
|
134
163
|
method: 'POST',
|
|
135
164
|
headers: {
|
|
@@ -157,14 +186,14 @@ async function createSession(opts: {
|
|
|
157
186
|
async function postPrompt(opts: {
|
|
158
187
|
endpoint: string
|
|
159
188
|
sessionId: string
|
|
160
|
-
cwd: string
|
|
189
|
+
cwd: string | null
|
|
161
190
|
prompt: string
|
|
162
191
|
providerID: string
|
|
163
192
|
modelID: string
|
|
164
193
|
authHeader: string | undefined
|
|
165
194
|
signal: AbortSignal
|
|
166
195
|
}): Promise<{ status: number }> {
|
|
167
|
-
const url = `${joinUrl(opts.endpoint, `/session/${encodeURIComponent(opts.sessionId)}/prompt_async`)}
|
|
196
|
+
const url = `${joinUrl(opts.endpoint, `/session/${encodeURIComponent(opts.sessionId)}/prompt_async`)}${buildDirectoryQuery(opts.cwd)}`
|
|
168
197
|
const res = await fetch(url, {
|
|
169
198
|
method: 'POST',
|
|
170
199
|
headers: {
|
|
@@ -209,7 +238,9 @@ export function streamOpenCodeWebChat(opts: StreamChatOptions): Promise<string>
|
|
|
209
238
|
const { session, message, systemPrompt, apiKey, write, active, signal } = opts
|
|
210
239
|
|
|
211
240
|
const endpoint = (session.apiEndpoint as string | undefined) || DEFAULT_ENDPOINT
|
|
212
|
-
const cwd = (
|
|
241
|
+
const cwd = isLocalEndpoint(endpoint)
|
|
242
|
+
? ((session.cwd as string | undefined) || process.cwd())
|
|
243
|
+
: null
|
|
213
244
|
const auth = parseBasicAuth(apiKey)
|
|
214
245
|
const authHeader = buildAuthHeader(auth)
|
|
215
246
|
const { providerID, modelID } = parseModelId(session.model as string | undefined)
|