@swarm-agent/sdk 0.1.47 → 0.1.48
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/HEADLESS_UI.md +2 -0
- package/README.md +270 -7
- package/WORKER_CONTROL.md +77 -0
- package/dist/auth.d.ts +26 -0
- package/dist/auth.js +81 -0
- package/dist/browser.d.ts +94 -0
- package/dist/browser.js +408 -0
- package/dist/chat.d.ts +134 -0
- package/dist/chat.js +307 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +8 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/permission-ui.d.ts +50 -0
- package/dist/permission-ui.js +167 -0
- package/dist/permissions.d.ts +126 -0
- package/dist/permissions.js +131 -0
- package/dist/projects.d.ts +36 -1
- package/dist/projects.js +96 -1
- package/dist/provider-auth.d.ts +38 -0
- package/dist/provider-auth.js +87 -0
- package/dist/sessions.d.ts +2 -0
- package/dist/sessions.js +34 -7
- package/dist/settings.d.ts +29 -1
- package/dist/settings.js +69 -0
- package/dist/types.d.ts +13 -0
- package/dist/websocket.d.ts +60 -0
- package/dist/websocket.js +207 -0
- package/dist/worker-control.d.ts +133 -0
- package/dist/worker-control.js +93 -0
- package/dist/workers.d.ts +2 -0
- package/dist/workers.js +3 -0
- package/examples/headless-worker-control.ts +28 -0
- package/package.json +8 -1
package/HEADLESS_UI.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Build a real headless UI with the SDK
|
|
2
2
|
|
|
3
|
+
For planned SDK completeness, safe tool control and non-production qualification, see the [five-step headless application plan](HEADLESS_PLAN.md). That roadmap is not a claim of implemented capabilities.
|
|
4
|
+
|
|
3
5
|
## Trust boundary
|
|
4
6
|
|
|
5
7
|
Run the administrative Node SDK in your backend-for-frontend **inside the same container as swarmd**, using its configured private Unix socket. Do not mount/export that socket, put daemon credentials in browser code, or forward arbitrary browser-selected daemon paths. Authenticate your app's users, check Origin/CSRF, and allowlist app operations. Never log credential request bodies. The separate `--container-sdk-port` listener remains session-only: it cannot onboard providers, change settings, or create workspaces.
|
package/README.md
CHANGED
|
@@ -49,19 +49,282 @@ explicit refresh, not timer polling. Live agent verification is a separate step.
|
|
|
49
49
|
|
|
50
50
|
## Quick Start
|
|
51
51
|
|
|
52
|
-
###
|
|
52
|
+
### Chat Flow vs. Autonomous Orchestration Flow
|
|
53
|
+
|
|
54
|
+
Swarm supports two distinct modes of execution depending on your application goals:
|
|
55
|
+
|
|
56
|
+
1. **Interactive Chat Flow (`client.chat` / `client.sessions`)**:
|
|
57
|
+
- Best for conversational assistants, interactive Q&A, and direct tool calling.
|
|
58
|
+
- A standalone session has one primary workspace, can use additional authorized workspaces, and follows normal worktree/permission/plan rules. It does not carry a project's high-level Markdown context.
|
|
59
|
+
- Use `client.chat.createSession()`, `client.chat.run()`, and `client.chat.stream()`.
|
|
60
|
+
|
|
61
|
+
2. **Autonomous Orchestration Flow (`client.projects` / `client.workers` / `client.deploy`)**:
|
|
62
|
+
- Best for multi-agent background tasks, codebase modifications, and durable pipelines.
|
|
63
|
+
- Deploys specialized subagent fleets (Coder, Finder, Designer, Router, Compact) across bounded repository worktrees with structured checkpoint contracts.
|
|
64
|
+
- **Project → orchestrator conversations → delegated tasks → task sessions.** Project Markdown and linked workspaces provide high-level context; each deployed task resolves its own authorized execution workspace.
|
|
65
|
+
- Use `client.projects.listConversations(projectId)` and `createConversation(projectId)` for the session picker and New Conversation button. These call canonical `/v3/projects/{id}/sessions`, not generic workspace session creation. `getOrchestratorSession` reuses an existing conversation or creates one; it does not mean there can only be one.
|
|
66
|
+
- Use `client.projects.createTask()` / `listTasks()` for custom task organization. Task-linked sessions must be continued through `reopenTask` with exact revision guards, not ordinary chat messages.
|
|
67
|
+
- Naming a workspace session “Orchestrator” does not grant orchestration authority. Use the project conversation helper; do not supply a workspace path or spoof task metadata.
|
|
68
|
+
- Models and thinking resolve from daemon account settings. Conversation creation must not overwrite provider/model preferences.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### 1. Initialize Client & Auto-Configure Credentials
|
|
73
|
+
|
|
74
|
+
The SDK can automatically discover credentials from your environment or Codex OAuth:
|
|
53
75
|
|
|
54
76
|
```typescript
|
|
55
|
-
import {
|
|
77
|
+
import { SwarmClient } from '@swarm-agent/sdk';
|
|
78
|
+
|
|
79
|
+
const client = new SwarmClient({
|
|
80
|
+
baseUrl: process.env.SWARM_API_URL || 'http://127.0.0.1:5555',
|
|
81
|
+
socketPath: process.env.SWARM_SOCKET_PATH, // e.g. /var/lib/swarmd/local-transport/api.sock
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 1. Ensure headless identity is bootstrapped
|
|
85
|
+
await client.onboarding.ensureBootstrapped({ username: 'developer', swarm_name: 'My Swarm' });
|
|
86
|
+
|
|
87
|
+
// 2. Auto-configure credentials from environment variables:
|
|
88
|
+
// (checks OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY/GOOGLE_API_KEY, DEEPSEEK_API_KEY, etc.)
|
|
89
|
+
// When found, automatically applies verified provider model recommendations to the fleet & router!
|
|
90
|
+
const auth = await client.auth.autoConfigure();
|
|
91
|
+
console.log(`Provider: ${auth.primaryProvider} (Fleet configured: ${auth.fleetApplied})`);
|
|
92
|
+
|
|
93
|
+
// Or if using OpenAI Codex OAuth device flow:
|
|
94
|
+
if (!auth.primaryProvider) {
|
|
95
|
+
await client.auth.codex.loginDevice({
|
|
96
|
+
onCode: (code) => console.log(`Open ${code.verification_url} and enter ${code.user_code}`),
|
|
97
|
+
});
|
|
98
|
+
await client.settings.applyProviderFleet('codex');
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### 2. Fleet & Router Model Settings
|
|
105
|
+
|
|
106
|
+
Swarm automatically optimizes models for each specific role in the agent fleet. You can customize them or apply verified recommendations in a single call:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// Apply verified recommendations for a provider across the fleet:
|
|
110
|
+
// Sets Swarm Action (auto), Swarm Plan (plan), Coder, Finder, Designer, Router, and Compact.
|
|
111
|
+
await client.settings.applyProviderFleet('codex');
|
|
112
|
+
|
|
113
|
+
// Inspect current fleet configuration
|
|
114
|
+
const settings = await client.settings.agentModels();
|
|
115
|
+
console.log('Action Model:', settings.agent_model_settings.swarm.action.model);
|
|
116
|
+
console.log('Router Model:', settings.agent_model_settings.system_agents.router.model);
|
|
117
|
+
console.log('Coder Model:', settings.agent_model_settings.system_agents.coder.model);
|
|
118
|
+
|
|
119
|
+
// Customize individual slots:
|
|
120
|
+
await client.settings.setSystemAgentModel('router', {
|
|
121
|
+
provider: 'codex',
|
|
122
|
+
model: 'gpt-6-luna',
|
|
123
|
+
thinking: 'low',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Restore system defaults at any time:
|
|
127
|
+
await client.settings.restoreDefaults();
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### 3. Explicit permission decisions
|
|
133
|
+
|
|
134
|
+
Use `describePermission(record)` to render tool details, conservative per-request `actions`,
|
|
135
|
+
question IDs, options and custom-response fields. Treat all text/arguments as untrusted text,
|
|
136
|
+
not HTML. `parseError` leaves malformed questions deny-only. These helpers do not grant authority:
|
|
137
|
+
the backend enforces policy, project scope and specialized plan/proposal acceptance.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { describePermission, answerPermission } from '@swarm-agent/sdk';
|
|
141
|
+
|
|
142
|
+
const pending = await client.permissions.listSessionPending(sessionId);
|
|
143
|
+
for (const permission of pending) {
|
|
144
|
+
renderPermission(describePermission(permission)); // Your UI; does not approve anything.
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Call from a user submit handler, with actual user-entered input:
|
|
148
|
+
async function submitAnswer(permission, userInput) {
|
|
149
|
+
const options = answerPermission(permission, userInput);
|
|
150
|
+
return await client.permissions.resolve(sessionId, permission.id, 'allow_once', options);
|
|
151
|
+
}
|
|
152
|
+
// Single question: userInput is a string (choice value or custom text).
|
|
153
|
+
// Structured questions: { questionId: 'actual answer', q_2: 'custom text' }.
|
|
154
|
+
// answerPermission serializes the backend reason/answers-map contract, not approved_arguments.answer.
|
|
155
|
+
// Ordinary tools: resolve(..., 'allow_once') or resolve(..., 'deny') only after a user click.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`resolve` returns the confirmed permission record. Transport failures and mismatched/stale
|
|
159
|
+
results reject; `PermissionResolutionError.result` retains a conflicting authoritative record.
|
|
160
|
+
A cancelled request or a prior different answer is not success. Backend sanitization can also
|
|
161
|
+
change returned text; review the returned record instead of blindly resubmitting. Duplicate
|
|
162
|
+
in-flight resolutions are rejected. `approvedArguments` supports an explicitly reviewed JSON
|
|
163
|
+
object where the backend supports it; it is not a question-answer channel.
|
|
164
|
+
|
|
165
|
+
Persistent actions and policy methods are advanced account-policy operations, not default UI
|
|
166
|
+
buttons. Project conversations require individual one-time decisions. Avoid `resolveAll` for
|
|
167
|
+
interactive forms: it can partially mutate on failure. Never enable bypass or change models
|
|
168
|
+
as incidental chat setup. Explicit auto-approval options affect ordinary tools only; ask-user
|
|
169
|
+
always requires actual user input.
|
|
170
|
+
|
|
171
|
+
---
|
|
56
172
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
173
|
+
### 4. Interactive Chat & Live Tool Stream
|
|
174
|
+
|
|
175
|
+
Build responsive chat UIs that stream model text, reasoning, and live tool execution cards like Swarm Desktop:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// 1. Create a chat session
|
|
179
|
+
const session = await client.chat.createSession({
|
|
180
|
+
title: 'Repository Exploration',
|
|
181
|
+
workspace_path: '/path/to/project',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// 2. Stream live text, reasoning, and tool calls
|
|
185
|
+
const stream = await client.chat.stream(session.id, {
|
|
186
|
+
onText: (delta, fullText) => process.stdout.write(delta),
|
|
187
|
+
onReasoning: (delta) => console.log('[Thinking]', delta),
|
|
188
|
+
onToolStarted: (tool) => console.log(`[Tool Started] ${tool.name}`),
|
|
189
|
+
onToolDelta: (tool, delta) => console.log(`[Tool Output] ${delta}`),
|
|
190
|
+
onToolCompleted: (tool) => console.log(`[Tool Done] ${tool.name} in ${tool.durationMs}ms`),
|
|
191
|
+
onPermissionRequested: async (perm, resolve) => {
|
|
192
|
+
// Your UI must wait for a real user gesture. Keep errors visible and retain the form.
|
|
193
|
+
renderPermissionForm(describePermission(perm), async (action, userInput) => {
|
|
194
|
+
try {
|
|
195
|
+
const result = await resolve(action, action === 'allow_once' && describePermission(perm).kind === 'ask-user'
|
|
196
|
+
? answerPermission(perm, userInput) : {});
|
|
197
|
+
showConfirmedDecision(result.permission);
|
|
198
|
+
} catch (error) { showPermissionError(perm.id, error); }
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
onPermissionRemoved: id => removePermissionForm(id), // No longer pending; not necessarily approved.
|
|
202
|
+
onPermissionError: (error, perm) => showPermissionError(perm.id, error),
|
|
203
|
+
onState: (state) => renderConversation(state.messages, state.live, state.snapshot),
|
|
204
|
+
onComplete: () => console.log('Turn finished; stream remains open'),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await stream.ready; // Hydration and replay MUST finish before dispatch.
|
|
208
|
+
await client.chat.sendMessage(session.id, 'Find all configuration files and summarize them.');
|
|
209
|
+
// Keep this watch for subsequent messages. Dispose only on switch/unmount/shutdown.
|
|
210
|
+
// stream.done means the watch ended, not that a single assistant turn finished.
|
|
211
|
+
|
|
212
|
+
// 3. Or use chat.run while the watch above serves the explicit permission UI
|
|
213
|
+
const response = await client.chat.run(session.id, {
|
|
214
|
+
message: 'What is the git status of this repository?',
|
|
215
|
+
timeoutMs: 120_000,
|
|
216
|
+
});
|
|
217
|
+
console.log(response.reply);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### 5. Native WebSocket Streaming Bridge & Browser Client
|
|
223
|
+
|
|
224
|
+
Swarmagent communicates in real-time over bidirectional WebSockets (`/v3/realtime/stream`). The SDK provides an out-of-the-box WebSocket Bridge for Node.js backends and a universal browser client for web frontends (React, Vue, Svelte, or Vanilla JS):
|
|
225
|
+
|
|
226
|
+
#### Backend (Node.js): Mount WebSocket Bridge
|
|
227
|
+
```typescript
|
|
228
|
+
import http from 'node:http';
|
|
229
|
+
import { SwarmClient } from '@swarm-agent/sdk';
|
|
230
|
+
|
|
231
|
+
const server = http.createServer((req, res) => { /* app routes */ });
|
|
232
|
+
const client = new SwarmClient();
|
|
233
|
+
|
|
234
|
+
// Mounts bidirectional WebSocket bridge on /ws:
|
|
235
|
+
// Hooks directly into Swarm daemon's realtime WebSocket outbox, multiplexing tokens, reasoning, and tool calls.
|
|
236
|
+
client.chat.attachWebSocket(server, {
|
|
237
|
+
path: '/ws',
|
|
238
|
+
autoApprovePermissions: false, // Explicit server policy; browser cannot enable it
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
server.listen(3456, '127.0.0.1');
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Frontend (Browser): Real-Time WebSocket Hooking
|
|
245
|
+
```typescript
|
|
246
|
+
import { SwarmBrowserChat, describePermission, answerPermission } from '@swarm-agent/sdk/browser';
|
|
247
|
+
|
|
248
|
+
const chat = new SwarmBrowserChat(); // same-origin ws/wss selected automatically
|
|
249
|
+
chat.on('state', state => {
|
|
250
|
+
// Replace durable history and transient streams separately; never append fullText to history.
|
|
251
|
+
renderConversation(state.messages, state.live, state.snapshot);
|
|
252
|
+
// snapshot.current_run_state_by_session[state.sessionId] owns busy/waiting/failed status.
|
|
253
|
+
});
|
|
254
|
+
chat.on('error', showError);
|
|
255
|
+
chat.on('disconnected', disableComposer);
|
|
256
|
+
|
|
257
|
+
// 1. Hook into live tool execution and token streams
|
|
258
|
+
chat.on('text', (delta, fullText) => renderLiveMarkdown(fullText));
|
|
259
|
+
chat.on('reasoning', (delta, fullText) => renderModelThinking(fullText));
|
|
260
|
+
chat.on('tool_start', (tool) => renderToolStartedCard(tool.id, tool.name, tool.arguments));
|
|
261
|
+
chat.on('tool_delta', (tool, delta) => appendToolOutput(tool.id, delta));
|
|
262
|
+
chat.on('tool_done', (tool) => markToolCompleted(tool.id, tool.durationMs));
|
|
263
|
+
// Replace pending cards on hydration/live updates/reconnect, keyed by permission.id.
|
|
264
|
+
// Preserve unsent input for unchanged requests; do not submit during render.
|
|
265
|
+
chat.on('permissions', pending => renderPendingForms(pending.map(describePermission)));
|
|
266
|
+
chat.on('permission_error', (id, error) => showPermissionError(id, error));
|
|
267
|
+
chat.on('permission_removed', id => removePermissionForm(id));
|
|
268
|
+
|
|
269
|
+
// Wire to your form's explicit user-submit handler. Never remove a card on send alone.
|
|
270
|
+
async function onPermissionSubmit(permission, action, userInput) {
|
|
271
|
+
try {
|
|
272
|
+
const result = await chat.resolvePermission(permission.id, action,
|
|
273
|
+
action === 'allow_once' && describePermission(permission).kind === 'ask-user'
|
|
274
|
+
? answerPermission(permission, userInput) : {});
|
|
275
|
+
showConfirmedDecision(result.permission);
|
|
276
|
+
} catch (error) { showPermissionError(permission.id, error); }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 2. Connect & subscribe to session
|
|
280
|
+
await chat.connect();
|
|
281
|
+
await chat.subscribe('session_123');
|
|
282
|
+
|
|
283
|
+
// 3. Send messages directly over WebSocket
|
|
284
|
+
const requestId = chat.sendMessage('Inspect the git repository and list modified files.');
|
|
285
|
+
// `accepted` acknowledges durable dispatch. Never blindly resend an uncertain message.
|
|
286
|
+
// Retain requestId if your UI offers an explicit retry, and pass it as argument 4.
|
|
287
|
+
// Switching: await chat.subscribe(otherSession.id). Reconnect rehydrates selected history.
|
|
288
|
+
// Unmount: chat.dispose(). Disconnected sends throw instead of silently disappearing.
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Backend conversation routes should use the same project identity for listing and creation:
|
|
292
|
+
```typescript
|
|
293
|
+
const project = await client.projects.ensureProject('Social content');
|
|
294
|
+
const conversations = await client.projects.listConversations(project.id, { limit: 100 });
|
|
295
|
+
const created = await client.projects.createConversation(project.id, {
|
|
296
|
+
title: 'New campaign', clientRequestId: crypto.randomUUID(),
|
|
61
297
|
});
|
|
298
|
+
// Return created.id to the browser; await chat.subscribe(created.id) before sending.
|
|
62
299
|
```
|
|
63
300
|
|
|
64
|
-
|
|
301
|
+
Use `state` as the conversation rendering authority, including initial history, failed/empty
|
|
302
|
+
turns and durable reconnect recovery. `text`/tool callbacks are convenience notifications.
|
|
303
|
+
History is a bounded 200-message tail; use canonical sync hydration for additional history.
|
|
304
|
+
The browser bridge requires an explicit selected session and never guesses a recent account
|
|
305
|
+
session. Keep it on host loopback; network/multi-user apps must supply `authorize` and their
|
|
306
|
+
own per-session authorization. Foreign browser origins are rejected. Auto approval never
|
|
307
|
+
chooses an `ask-user` answer: render the question and send the operator's reply explicitly.
|
|
308
|
+
|
|
309
|
+
`resolvePermission` now returns a promise: await it and handle rejection. Browser and bridge
|
|
310
|
+
must be upgraded together; resolution frames require `requestId` and acknowledgements carry
|
|
311
|
+
`result`. `permission_removed` means no longer pending, not approval. `permissions` is a
|
|
312
|
+
rendering projection of the canonical session view, not a second durable authority. Timeout,
|
|
313
|
+
disconnect, disposal or session switch rejects uncertain submissions; reconnect rehydrates
|
|
314
|
+
without replaying decisions. To explicitly rehydrate after an acknowledgement timeout, retain
|
|
315
|
+
`chat.sessionId`, call `chat.unsubscribe()`, then `await chat.subscribe(retainedId)`; never resend
|
|
316
|
+
the decision automatically. Review refreshed state before manually retrying. No UI can infer
|
|
317
|
+
exactly-once application from a lost acknowledgement. `onPermissionError` is recoverable and
|
|
318
|
+
does not close the watch; `onError` reports stream failures. Existing string-reason resolver
|
|
319
|
+
calls remain supported, while options objects carry structured replies and reviewed arguments.
|
|
320
|
+
|
|
321
|
+
Runnable terminal examples: `examples/chat-quickstart.ts`, `examples/orchestrator-quickstart.ts`
|
|
322
|
+
and their shared `examples/interactive-permissions.ts`. They require a preconfigured daemon,
|
|
323
|
+
ask for actual terminal input, and never configure credentials, model fleets or bypass.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### 5. Local Desktop Bootstrap & Scoped Tokens
|
|
65
328
|
|
|
66
329
|
```typescript
|
|
67
330
|
// Bootstrap local session credentials from Desktop
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Headless worker control (configuration preview)
|
|
2
|
+
|
|
3
|
+
`client.workers.control` uses authenticated `/v3/workers/{worker_id}` APIs.
|
|
4
|
+
It extends canonical worker identity, run receipts and V3 worker realtime
|
|
5
|
+
invalidation. It is not a second executor or a direct SSH/cloud deployment helper.
|
|
6
|
+
Use a trusted backend/private full API connection, not the restricted container
|
|
7
|
+
session listener. Never give a browser the privileged daemon socket.
|
|
8
|
+
|
|
9
|
+
## Supported operations
|
|
10
|
+
|
|
11
|
+
| SDK operation | Route suffix | Effect |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `registerSSHTarget` | POST `ssh-targets` | Register an existing-host configuration in the canonical workspace/account connection catalog; idempotent, no network contact. |
|
|
14
|
+
| `resolveTarget` | POST `target-reference` | Authorize an existing SSH connection or GCP topology reference and compute its configuration digest. |
|
|
15
|
+
| `getContext`, `updateContext` | GET/PUT `context` | Read current/exact revision; publish explicit bounded knowledge with CAS and provenance. |
|
|
16
|
+
| `proposeDeployment` | POST `deployments` | Persist pending intent pinned to worker/context/target and explicit lifecycle. |
|
|
17
|
+
| `listDeployments`, `getDeployment` | GET `deployments[/{id}]` | Inspect persisted intent, not runtime health. |
|
|
18
|
+
| `approveDeployment` | POST `deployments/{id}/approve` | Explicit user approves exact revision/digest; stale worker/context/target rejects. |
|
|
19
|
+
| `queueJob` | POST `deployments/{id}/jobs` | Admit a canonical run with resolved account model, context, target, deployment and pending-attempt pins. Does not execute it. |
|
|
20
|
+
| `command`, `commands` | POST/GET `deployments/{id}/commands` | Stop is durable pending intent; start returns 503 unavailable. |
|
|
21
|
+
|
|
22
|
+
Use existing `workers.listRuns`, `getRun`, and `cancelRun` for job receipts.
|
|
23
|
+
Cancellation of a still-unstarted remote intent releases reservations atomically
|
|
24
|
+
and records `cancelled_before_dispatch`. It is not remote runtime cleanup.
|
|
25
|
+
|
|
26
|
+
## Safety and semantics
|
|
27
|
+
|
|
28
|
+
- SDK transport/user identity is authenticated; request JSON cannot supply an
|
|
29
|
+
account or agent role. Scopes remain `automations:read`/`automations:write`.
|
|
30
|
+
Trigger/worker-scoped credentials cannot use control-plane operations.
|
|
31
|
+
- Ordinary chat/subagents cannot manage workers. Canonical Orchestrator agent
|
|
32
|
+
callers can propose intent through the shared service; approvals and explicit
|
|
33
|
+
context publication require a user. This preview adds no AI tool schema.
|
|
34
|
+
- Existing workers keep IDs and local behavior. Queued remote intent reserves
|
|
35
|
+
one job per deployment and one context writer per worker. Local admission is
|
|
36
|
+
also blocked while a remote context writer owns the worker. The first target
|
|
37
|
+
capacity policy is shared across deployments; a later proposal cannot silently
|
|
38
|
+
enlarge it. Reservations survive restart.
|
|
39
|
+
- `persistent` and `on_demand` are explicit for SSH and GCP. Approval sets desired
|
|
40
|
+
state `running`, while observed state remains `unavailable`. A saved intent is
|
|
41
|
+
never readiness. A stop command closes further admission, stays `pending`,
|
|
42
|
+
and does not fabricate acknowledgement. Cancel pending jobs separately.
|
|
43
|
+
- Registered existing targets authorize only `owned_runtime` cleanup. No host
|
|
44
|
+
delete/shutdown capability exists. Future GCP provisioning must establish exact
|
|
45
|
+
owned resource identity before adding compute deletion.
|
|
46
|
+
- Context is at most 64 KiB plus 32 bounded opaque artifact/checkpoint references;
|
|
47
|
+
references grant no access and must be reauthorized before use. Updates reject
|
|
48
|
+
while jobs own context. No automatic transcript ingestion or vector store.
|
|
49
|
+
- Worker changes require new reviewed deployment intent. The preview requires a
|
|
50
|
+
currently approved active worker and its existing primary workspace binding to
|
|
51
|
+
admit a job; it does not invent a different worker approval engine.
|
|
52
|
+
|
|
53
|
+
## Durability and replay
|
|
54
|
+
|
|
55
|
+
Deployment, context, admission and command mutations use the canonical durable
|
|
56
|
+
worker transaction/outbox. Subscribe to existing V3 `worker.updated` account
|
|
57
|
+
invalidation and rehydrate these APIs; retain opaque scoped endpoint cursors.
|
|
58
|
+
Do not parse cursor numbers or poll for fabricated runtime progress. Worker runs
|
|
59
|
+
retain session identity, but a queued remote attempt has no materialized V3
|
|
60
|
+
session until an adapter actually prepares execution. Do not call a session
|
|
61
|
+
watcher on that reserved ID as if it were a live session.
|
|
62
|
+
|
|
63
|
+
## Explicitly unavailable
|
|
64
|
+
|
|
65
|
+
No SSH process launch, GCP provisioning, remote readiness/heartbeat, execution,
|
|
66
|
+
remote acknowledgement, remote context publication, result manifest or cleanup
|
|
67
|
+
is implemented by this preview. Legacy direct dispatch is not a remote fallback.
|
|
68
|
+
Remote outcome writes through the local writer fail closed. Generation is pinned
|
|
69
|
+
at one until a qualified adapter supports replacement/retry fencing. Schedule
|
|
70
|
+
occurrence deduplication is retained at canonical storage; selecting remote
|
|
71
|
+
placement in the live scheduler is deferred, not silently enabled by a proposal.
|
|
72
|
+
GCP references must already exist in canonical account topology with transport
|
|
73
|
+
`gcp`; this API does not enroll cloud credentials or create machines.
|
|
74
|
+
|
|
75
|
+
`examples/headless-worker-control.ts` is an SDK-only configuration example. It
|
|
76
|
+
registers up to ten explicit SSH configurations and proposes placement. Approval
|
|
77
|
+
is a separate user decision; running the example is not live execution proof.
|
package/dist/auth.d.ts
CHANGED
|
@@ -42,4 +42,30 @@ export declare class SwarmAuthNamespace {
|
|
|
42
42
|
ok: boolean;
|
|
43
43
|
deleted: boolean;
|
|
44
44
|
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Automatically configures provider credentials and the AI fleet from environment variables or existing credentials.
|
|
47
|
+
* Checks for:
|
|
48
|
+
* - OPENAI_API_KEY -> provider 'openai'
|
|
49
|
+
* - ANTHROPIC_API_KEY -> provider 'anthropic'
|
|
50
|
+
* - GEMINI_API_KEY / GOOGLE_API_KEY -> provider 'google'
|
|
51
|
+
* - DEEPSEEK_API_KEY -> provider 'deepseek'
|
|
52
|
+
* - GROQ_API_KEY -> provider 'groq'
|
|
53
|
+
* - MISTRAL_API_KEY -> provider 'mistral'
|
|
54
|
+
* Also verifies if Codex is authenticated via OAuth.
|
|
55
|
+
* When credentials are found or active, automatically configures the whole fleet (Swarm Core & System Agents)
|
|
56
|
+
* with verified catalog recommendations.
|
|
57
|
+
*/
|
|
58
|
+
autoConfigure(options?: AutoConfigureAuthOptions): Promise<AutoConfigureAuthResult>;
|
|
59
|
+
}
|
|
60
|
+
export interface AutoConfigureAuthOptions {
|
|
61
|
+
/** If true (default), automatically applies verified catalog recommendations to the fleet */
|
|
62
|
+
applyFleetRecommendations?: boolean;
|
|
63
|
+
/** Custom env map (defaults to process.env) */
|
|
64
|
+
env?: Record<string, string | undefined>;
|
|
65
|
+
}
|
|
66
|
+
export interface AutoConfigureAuthResult {
|
|
67
|
+
configuredProviders: string[];
|
|
68
|
+
primaryProvider?: string;
|
|
69
|
+
source: 'env' | 'existing_credential' | 'none';
|
|
70
|
+
fleetApplied: boolean;
|
|
45
71
|
}
|
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SwarmCredentialsNamespace, SwarmCodexAuthNamespace } from './provider-auth.js';
|
|
2
|
+
import { SwarmSettingsNamespace } from './settings.js';
|
|
2
3
|
export class SwarmAuthNamespace {
|
|
3
4
|
credentials;
|
|
4
5
|
codex;
|
|
@@ -86,4 +87,84 @@ export class SwarmAuthNamespace {
|
|
|
86
87
|
});
|
|
87
88
|
return res.data;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Automatically configures provider credentials and the AI fleet from environment variables or existing credentials.
|
|
92
|
+
* Checks for:
|
|
93
|
+
* - OPENAI_API_KEY -> provider 'openai'
|
|
94
|
+
* - ANTHROPIC_API_KEY -> provider 'anthropic'
|
|
95
|
+
* - GEMINI_API_KEY / GOOGLE_API_KEY -> provider 'google'
|
|
96
|
+
* - DEEPSEEK_API_KEY -> provider 'deepseek'
|
|
97
|
+
* - GROQ_API_KEY -> provider 'groq'
|
|
98
|
+
* - MISTRAL_API_KEY -> provider 'mistral'
|
|
99
|
+
* Also verifies if Codex is authenticated via OAuth.
|
|
100
|
+
* When credentials are found or active, automatically configures the whole fleet (Swarm Core & System Agents)
|
|
101
|
+
* with verified catalog recommendations.
|
|
102
|
+
*/
|
|
103
|
+
async autoConfigure(options = {}) {
|
|
104
|
+
const env = options.env || (typeof process !== 'undefined' && process?.env ? process.env : {});
|
|
105
|
+
const configuredProviders = [];
|
|
106
|
+
const providerKeyMap = [
|
|
107
|
+
{ provider: 'openai', envVar: 'OPENAI_API_KEY' },
|
|
108
|
+
{ provider: 'anthropic', envVar: 'ANTHROPIC_API_KEY' },
|
|
109
|
+
{ provider: 'google', envVar: 'GEMINI_API_KEY', fallbackEnv: 'GOOGLE_API_KEY' },
|
|
110
|
+
{ provider: 'deepseek', envVar: 'DEEPSEEK_API_KEY' },
|
|
111
|
+
{ provider: 'groq', envVar: 'GROQ_API_KEY' },
|
|
112
|
+
{ provider: 'mistral', envVar: 'MISTRAL_API_KEY' },
|
|
113
|
+
];
|
|
114
|
+
for (const mapping of providerKeyMap) {
|
|
115
|
+
const key = env[mapping.envVar] || (mapping.fallbackEnv ? env[mapping.fallbackEnv] : undefined);
|
|
116
|
+
if (key && key.trim()) {
|
|
117
|
+
try {
|
|
118
|
+
await this.credentials.save({
|
|
119
|
+
provider: mapping.provider,
|
|
120
|
+
type: 'api',
|
|
121
|
+
api_key: key.trim(),
|
|
122
|
+
active: true,
|
|
123
|
+
});
|
|
124
|
+
configuredProviders.push(mapping.provider);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Continue if already registered or error
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let source = configuredProviders.length > 0 ? 'env' : 'none';
|
|
132
|
+
let primaryProvider = configuredProviders[0];
|
|
133
|
+
// Check if Codex is authenticated via OAuth
|
|
134
|
+
const codexAuthed = await this.codex.isAuthenticated();
|
|
135
|
+
if (codexAuthed) {
|
|
136
|
+
primaryProvider = 'codex';
|
|
137
|
+
if (source === 'none')
|
|
138
|
+
source = 'existing_credential';
|
|
139
|
+
}
|
|
140
|
+
else if (!primaryProvider) {
|
|
141
|
+
// Check if existing credentials exist in daemon
|
|
142
|
+
try {
|
|
143
|
+
const creds = await this.credentials.list();
|
|
144
|
+
const activeCred = creds.records.find((r) => r.active);
|
|
145
|
+
if (activeCred) {
|
|
146
|
+
primaryProvider = activeCred.provider;
|
|
147
|
+
source = 'existing_credential';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch { }
|
|
151
|
+
}
|
|
152
|
+
let fleetApplied = false;
|
|
153
|
+
if (primaryProvider && options.applyFleetRecommendations !== false) {
|
|
154
|
+
try {
|
|
155
|
+
const settings = new SwarmSettingsNamespace(this.transport);
|
|
156
|
+
await settings.applyProviderFleet(primaryProvider);
|
|
157
|
+
fleetApplied = true;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Fallback or retry
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
configuredProviders,
|
|
165
|
+
primaryProvider,
|
|
166
|
+
source,
|
|
167
|
+
fleetApplied,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
89
170
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ToolStreamItem } from './chat.js';
|
|
2
|
+
import type { PendingPermissionRecord, ResolvePermissionResult } from './permissions.js';
|
|
3
|
+
import type { PermissionAction, ResolvePermissionOptions } from './permission-ui.js';
|
|
4
|
+
export * from './permission-ui.js';
|
|
5
|
+
import type { SessionWatchState, V3Message } from './realtime.js';
|
|
6
|
+
export interface SwarmBrowserChatOptions {
|
|
7
|
+
/** WebSocket URL, e.g. ws://localhost:3456/ws. Defaults to ws(s)://${location.host}/ws */
|
|
8
|
+
url?: string;
|
|
9
|
+
/** Whether to automatically connect on construction (default: true) */
|
|
10
|
+
autoConnect?: boolean;
|
|
11
|
+
/** Whether to automatically reconnect on drop (default: true) */
|
|
12
|
+
autoReconnect?: boolean;
|
|
13
|
+
/** Max reconnection attempts before giving up (default: 10) */
|
|
14
|
+
maxReconnectAttempts?: number;
|
|
15
|
+
/** Bounds subscription acknowledgement waits (default 45 seconds). */
|
|
16
|
+
subscribeTimeoutMs?: number;
|
|
17
|
+
/** Lost acknowledgements reject with an uncertain outcome; decisions are never replayed. */
|
|
18
|
+
permissionTimeoutMs?: number;
|
|
19
|
+
/** Optional custom WebSocket constructor (useful in Node.js test environments) */
|
|
20
|
+
webSocketClass?: any;
|
|
21
|
+
}
|
|
22
|
+
type EventMap = {
|
|
23
|
+
state: (state: SessionWatchState) => void;
|
|
24
|
+
accepted: (requestId: string) => void;
|
|
25
|
+
connected: () => void;
|
|
26
|
+
disconnected: () => void;
|
|
27
|
+
subscribed: (sessionId: string) => void;
|
|
28
|
+
unsubscribed: () => void;
|
|
29
|
+
text: (delta: string, fullText: string) => void;
|
|
30
|
+
reasoning: (delta: string, fullText: string) => void;
|
|
31
|
+
tool_start: (tool: ToolStreamItem) => void;
|
|
32
|
+
tool_delta: (tool: ToolStreamItem, delta: string) => void;
|
|
33
|
+
tool_done: (tool: ToolStreamItem) => void;
|
|
34
|
+
tool_permission: (permission: PendingPermissionRecord) => void;
|
|
35
|
+
permission_resolved: (permissionId: string, result: ResolvePermissionResult) => void;
|
|
36
|
+
permission_removed: (permissionId: string) => void;
|
|
37
|
+
permission_error: (permissionId: string, error: Error) => void;
|
|
38
|
+
permissions: (pending: PendingPermissionRecord[]) => void;
|
|
39
|
+
done: (messages: V3Message[]) => void;
|
|
40
|
+
error: (error: string) => void;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Universal browser-side WebSocket client for Swarm.
|
|
44
|
+
* Hooks directly into the Swarm WebSocket Bridge with typed callbacks for live
|
|
45
|
+
* assistant tokens, model reasoning deltas, real-time tool events, and permission approvals.
|
|
46
|
+
*/
|
|
47
|
+
export declare class SwarmBrowserChat {
|
|
48
|
+
private readonly options;
|
|
49
|
+
private ws;
|
|
50
|
+
private currentSessionId;
|
|
51
|
+
private isDisposed;
|
|
52
|
+
private reconnectAttempts;
|
|
53
|
+
private reconnectTimer;
|
|
54
|
+
private subscribedSessionId;
|
|
55
|
+
private permissionsLive;
|
|
56
|
+
private cancelSubscription?;
|
|
57
|
+
private connectPromise;
|
|
58
|
+
private readonly listeners;
|
|
59
|
+
private readonly pendingPermissions;
|
|
60
|
+
private readonly decisions;
|
|
61
|
+
get permissions(): PendingPermissionRecord[];
|
|
62
|
+
private rejectDecisions;
|
|
63
|
+
private removePermission;
|
|
64
|
+
constructor(options?: SwarmBrowserChatOptions);
|
|
65
|
+
get sessionId(): string | null;
|
|
66
|
+
get isSubscribed(): boolean;
|
|
67
|
+
get isConnected(): boolean;
|
|
68
|
+
private resolveUrl;
|
|
69
|
+
/**
|
|
70
|
+
* Connects to the Swarm WebSocket Bridge. Returns a Promise that resolves when connected.
|
|
71
|
+
*/
|
|
72
|
+
connect(): Promise<void>;
|
|
73
|
+
private maybeReconnect;
|
|
74
|
+
private handleServerMessage;
|
|
75
|
+
private send;
|
|
76
|
+
/**
|
|
77
|
+
* Subscribes to real-time events for a specific session.
|
|
78
|
+
* Returns a promise that resolves once the session subscription is acknowledged.
|
|
79
|
+
*/
|
|
80
|
+
subscribe(sessionId: string, autoApprovePermissions?: boolean): Promise<string>;
|
|
81
|
+
/** Unsubscribes from active session streaming */
|
|
82
|
+
unsubscribe(): void;
|
|
83
|
+
/** Sends a chat message over the WebSocket */
|
|
84
|
+
sendMessage(message: string, sessionId?: string, autoApprovePermissions?: boolean, requestId?: string): string;
|
|
85
|
+
/** Resolve only after a user gesture. Await confirmation; on failure keep the form recoverable. */
|
|
86
|
+
resolvePermission(permissionId: string, action: PermissionAction, options?: ResolvePermissionOptions & {
|
|
87
|
+
sessionId?: string;
|
|
88
|
+
}): Promise<ResolvePermissionResult>;
|
|
89
|
+
on<K extends keyof EventMap>(event: K, handler: EventMap[K]): () => void;
|
|
90
|
+
off<K extends keyof EventMap>(event: K, handler: EventMap[K]): void;
|
|
91
|
+
private emit;
|
|
92
|
+
/** Closes and disposes the WebSocket connection */
|
|
93
|
+
dispose(): void;
|
|
94
|
+
}
|