Skip to content

Commit 53bb438

Browse files
committed
feat: add provider diagnostics
1 parent e6fb946 commit 53bb438

18 files changed

Lines changed: 661 additions & 96 deletions

File tree

.github/workflows/desktop-release.yml

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,52 @@ jobs:
105105
"HOMEDRIVE=D:" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
106106
"HOMEPATH=\\swarmclaw-home" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
107107
108+
- name: Check macOS signing inputs
109+
if: matrix.os == 'macos-latest'
110+
shell: bash
111+
env:
112+
CSC_LINK: ${{ secrets.CSC_LINK }}
113+
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
114+
APPLE_ID: ${{ secrets.APPLE_ID }}
115+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
116+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
117+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
118+
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
119+
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
120+
run: |
121+
set -euo pipefail
122+
has_certificate=false
123+
has_apple_id_notary=false
124+
has_api_key_notary=false
125+
126+
if [ -n "${CSC_LINK:-}" ] && [ -n "${CSC_KEY_PASSWORD:-}" ]; then
127+
has_certificate=true
128+
fi
129+
if [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
130+
has_apple_id_notary=true
131+
fi
132+
if [ -n "${APPLE_API_KEY:-}" ] && [ -n "${APPLE_API_KEY_ID:-}" ] && [ -n "${APPLE_API_ISSUER:-}" ]; then
133+
has_api_key_notary=true
134+
fi
135+
136+
if [ "$has_certificate" = true ] && { [ "$has_apple_id_notary" = true ] || [ "$has_api_key_notary" = true ]; }; then
137+
echo "Developer ID signing and notarization inputs are configured."
138+
else
139+
echo "Developer ID signing and notarization inputs are incomplete; macOS artifacts will remain ad-hoc signed."
140+
fi
141+
108142
- name: Build desktop app
109143
run: npm run ${{ matrix.script }} -- --publish
110144
env:
111145
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
112-
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
146+
CSC_LINK: ${{ secrets.CSC_LINK }}
147+
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
148+
APPLE_ID: ${{ secrets.APPLE_ID }}
149+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
150+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
151+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
152+
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
153+
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
113154
NEXT_TELEMETRY_DISABLED: '1'
114155

115156
- name: Upload build logs on failure

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ jobs:
147147
echo "- \`docker pull ${{ steps.image.outputs.name }}:${{ github.ref_name }}\`" >> "$BODY_PATH"
148148
echo "- \`docker pull ${{ steps.image.outputs.name }}:latest\`" >> "$BODY_PATH"
149149
echo "## macOS desktop" >> "$BODY_PATH"
150-
echo "- macOS builds are ad-hoc signed until Developer ID notarization is configured. If Finder says SwarmClaw is damaged, run \`xattr -dr com.apple.quarantine /Applications/SwarmClaw.app\` after installing, then open it again." >> "$BODY_PATH"
150+
echo "- macOS builds are notarized when Developer ID and Apple notarization secrets are configured for the desktop release workflow. If a build is still ad-hoc signed and Finder says SwarmClaw is damaged, run \`xattr -dr com.apple.quarantine /Applications/SwarmClaw.app\` after installing, then open it again." >> "$BODY_PATH"
151151
echo "path=$BODY_PATH" >> "$GITHUB_OUTPUT"
152152
153153
- name: Create GitHub Release

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
8181
Download the one-click installer from [swarmclaw.ai/downloads](https://swarmclaw.ai/downloads).
8282
Available for macOS (Apple Silicon & Intel), Windows, and Linux (AppImage + .deb).
8383

84-
Current builds are ad-hoc signed but not notarized, so on first launch:
85-
- **macOS:** right-click the app in Finder → **Open****Open** to bypass Gatekeeper. If macOS instead reports *"SwarmClaw is damaged and can't be opened"* (common on Apple Silicon when the dmg was quarantined by Safari), strip the quarantine attribute and relaunch:
84+
The release workflow supports Developer ID signing and notarization when Apple
85+
credentials are configured. If a macOS build is still ad-hoc signed, first
86+
launch may need one manual approval:
87+
- **macOS:** right-click the app in Finder → **Open****Open** to bypass Gatekeeper. If macOS instead reports *"SwarmClaw is damaged and can't be opened"* (common when the dmg was quarantined by Safari), strip the quarantine attribute and relaunch:
8688
```bash
8789
xattr -dr com.apple.quarantine /Applications/SwarmClaw.app
8890
```
@@ -407,6 +409,15 @@ Operational docs: https://swarmclaw.ai/docs/observability
407409

408410
## Releases
409411

412+
### v1.9.21 Highlights
413+
414+
Provider diagnostics release: connection checks now return a structured step timeline across setup, provider settings, and agent editing.
415+
416+
- **Connection timelines.** Provider checks show endpoint resolution, model discovery, fallback selection, and chat/gateway verification steps.
417+
- **Safer error details.** Token-like values are redacted before check messages or diagnostics are returned to the UI.
418+
- **Local runtime debugging.** LM Studio, Ollama, custom OpenAI-compatible endpoints, cloud providers, OpenClaw gateways, and CLI providers all report concise pass/fail diagnostics.
419+
- **macOS signing path.** Desktop releases now forward Developer ID and Apple notarization credentials when configured, while ad-hoc fallback builds keep the quarantine workaround documented.
420+
410421
### v1.9.20 Highlights
411422

412423
Provider reliability release: local OpenAI-compatible runtimes now get safer endpoint handling, clearer setup, and first-class LM Studio support.

electron-builder.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,14 @@ mac:
4848
- arm64
4949
- x64
5050
icon: resources/icon.icns
51-
hardenedRuntime: false
51+
hardenedRuntime: true
5252
gatekeeperAssess: false
53-
identity: '-'
54-
# Ad-hoc signed, not notarized. macOS quarantine may still flag the dmg as
55-
# "damaged" on first download; users can clear it with:
53+
# Developer ID signing is used when CSC_LINK/CSC_KEY_PASSWORD are available.
54+
# electron-builder then notarizes when APPLE_* notarization credentials are
55+
# present. Without those secrets, the afterPack hook leaves an ad-hoc
56+
# signature so local smoke builds still produce launchable artifacts.
57+
# Ad-hoc macOS builds can still show "damaged" after Safari quarantine:
5658
# xattr -dr com.apple.quarantine /Applications/SwarmClaw.app
57-
# Proper Developer ID signing + notarization is tracked as a future item.
5859

5960
win:
6061
target:

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@swarmclawai/swarmclaw",
3-
"version": "1.9.20",
3+
"version": "1.9.21",
44
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
55
"main": "electron-dist/main.js",
66
"license": "MIT",
@@ -85,10 +85,10 @@
8585
"lint:baseline": "node ./scripts/lint-baseline.mjs check",
8686
"lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
8787
"cli": "node ./bin/swarmclaw.js",
88-
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
88+
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/electron-signing-config.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
8989
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
9090
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
91-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
91+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
9292
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
9393
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
9494
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import assert from 'node:assert/strict'
2+
import fs from 'node:fs'
3+
import path from 'node:path'
4+
import { fileURLToPath } from 'node:url'
5+
import { describe, it } from 'node:test'
6+
7+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
8+
9+
describe('macOS desktop signing configuration', () => {
10+
it('keeps Developer ID signing and notarization available for release builds', () => {
11+
const builderConfig = fs.readFileSync(path.join(repoRoot, 'electron-builder.yml'), 'utf8')
12+
const desktopWorkflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'desktop-release.yml'), 'utf8')
13+
14+
assert.ok(builderConfig.includes('hardenedRuntime: true'))
15+
assert.equal(builderConfig.includes("identity: '-'"), false)
16+
assert.equal(builderConfig.includes('identity: "-"'), false)
17+
18+
for (const secretName of [
19+
'CSC_LINK',
20+
'CSC_KEY_PASSWORD',
21+
'APPLE_ID',
22+
'APPLE_APP_SPECIFIC_PASSWORD',
23+
'APPLE_TEAM_ID',
24+
'APPLE_API_KEY',
25+
'APPLE_API_KEY_ID',
26+
'APPLE_API_ISSUER',
27+
]) {
28+
assert.ok(
29+
desktopWorkflow.includes(`${secretName}: \${{ secrets.${secretName} }}`),
30+
`desktop release workflow should forward ${secretName}`,
31+
)
32+
}
33+
34+
assert.equal(desktopWorkflow.includes("CSC_IDENTITY_AUTO_DISCOVERY: 'false'"), false)
35+
assert.equal(desktopWorkflow.includes('CSC_IDENTITY_AUTO_DISCOVERY: "false"'), false)
36+
})
37+
})

src/app/api/setup/check-provider/route.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
22
import test from 'node:test'
33

44
import { normalizeOllamaSetupEndpoint, normalizeOpenClawUrl, parseErrorMessage } from './helpers'
5+
import { POST } from './route'
56

67
test('normalizeOllamaSetupEndpoint strips local /v1 suffixes but preserves cloud endpoints', () => {
78
assert.equal(
@@ -117,3 +118,46 @@ test('normalizeOpenClawUrl handles bare IP:port', () => {
117118
assert.equal(httpUrl, 'http://10.0.0.5:18789')
118119
assert.equal(wsUrl, 'ws://10.0.0.5:18789')
119120
})
121+
122+
test('POST returns provider diagnostics with normalized LM Studio targets and redacted errors', async () => {
123+
const originalFetch = globalThis.fetch
124+
const calls: string[] = []
125+
globalThis.fetch = (async (input: RequestInfo | URL) => {
126+
const url = String(input)
127+
calls.push(url)
128+
if (url.endsWith('/models')) {
129+
return new Response(JSON.stringify({ data: [{ id: 'google/gemma-4-e4b' }] }), { status: 200 })
130+
}
131+
return new Response(
132+
JSON.stringify({ error: { message: 'Malformed token sk-local-secret provided.' } }),
133+
{ status: 400 },
134+
)
135+
}) as typeof fetch
136+
137+
try {
138+
const res = await POST(new Request('http://localhost/api/setup/check-provider', {
139+
method: 'POST',
140+
body: JSON.stringify({
141+
provider: 'lmstudio',
142+
endpoint: 'http://10.2.0.2:1234',
143+
}),
144+
}))
145+
const payload = await res.json()
146+
147+
assert.equal(payload.ok, false)
148+
assert.equal(payload.normalizedEndpoint, 'http://10.2.0.2:1234/v1')
149+
assert.deepEqual(calls, [
150+
'http://10.2.0.2:1234/v1/models',
151+
'http://10.2.0.2:1234/v1/chat/completions',
152+
])
153+
assert.ok(Array.isArray(payload.diagnostics))
154+
assert.equal(payload.diagnostics[0].target, 'http://10.2.0.2:1234/v1')
155+
assert.equal(payload.message, 'Malformed token sk-... provided.')
156+
assert.equal(
157+
payload.diagnostics.some((step: { detail?: string }) => step.detail?.includes('sk-local-secret')),
158+
false,
159+
)
160+
} finally {
161+
globalThis.fetch = originalFetch
162+
}
163+
})

0 commit comments

Comments
 (0)