@swarm-agent/sdk 0.1.49 → 0.1.51
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 +4 -6
- package/README.md +138 -58
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +183 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +16 -0
- package/dist/box.d.ts +88 -0
- package/dist/box.js +13 -0
- package/dist/browser.d.ts +6 -2
- package/dist/browser.js +11 -3
- package/dist/chat.d.ts +41 -7
- package/dist/chat.js +148 -83
- package/dist/client.d.ts +12 -0
- package/dist/client.js +19 -6
- package/dist/copy-tags.d.ts +26 -0
- package/dist/copy-tags.js +133 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +21 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/local-connection.d.ts +39 -0
- package/dist/local-connection.js +56 -0
- package/dist/permission-ui.d.ts +1 -0
- package/dist/permission-ui.js +37 -0
- package/dist/permissions.d.ts +4 -0
- package/dist/permissions.js +7 -0
- package/dist/projects.d.ts +25 -7
- package/dist/projects.js +65 -17
- package/dist/provider-auth.d.ts +5 -3
- package/dist/provider-auth.js +6 -8
- package/dist/realtime.d.ts +40 -6
- package/dist/realtime.js +156 -52
- package/dist/remote.d.ts +59 -0
- package/dist/remote.js +46 -0
- package/dist/sessions.d.ts +31 -1
- package/dist/sessions.js +49 -1
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +19 -3
- package/dist/signals.d.ts +148 -0
- package/dist/signals.js +97 -0
- package/dist/types.d.ts +97 -6
- package/dist/websocket.d.ts +3 -1
- package/dist/websocket.js +1 -1
- package/dist/workers.d.ts +6 -0
- package/dist/workers.js +27 -0
- package/dist/workspaces.d.ts +20 -0
- package/dist/workspaces.js +24 -0
- package/package.json +1 -1
package/dist/remote.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner administration of the machine's relay connection (`/v1/remote*`).
|
|
3
|
+
* The daemon refuses scoped tokens here: call it only from a trusted backend
|
|
4
|
+
* on the private local transport, never from a browser.
|
|
5
|
+
*/
|
|
6
|
+
export class SwarmRemoteNamespace {
|
|
7
|
+
transport;
|
|
8
|
+
constructor(transport) {
|
|
9
|
+
this.transport = transport;
|
|
10
|
+
}
|
|
11
|
+
async call(path, method, body) {
|
|
12
|
+
const res = await this.transport.request(`/v1/remote${path}`, { method, ...(method === 'POST' ? { body: body ?? {} } : {}) });
|
|
13
|
+
return res.data.remote;
|
|
14
|
+
}
|
|
15
|
+
status() {
|
|
16
|
+
return this.call('', 'GET');
|
|
17
|
+
}
|
|
18
|
+
/** Creates this machine's device key for one relay. Does not connect. */
|
|
19
|
+
init(input) {
|
|
20
|
+
return this.call('/init', 'POST', {
|
|
21
|
+
relay_url: input.relay_url, device_name: input.device_name,
|
|
22
|
+
allow_write: input.allow_write === true, allow_approve: input.allow_approve === true, allow_manage: input.allow_manage === true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
enable() {
|
|
26
|
+
return this.call('/enable', 'POST');
|
|
27
|
+
}
|
|
28
|
+
disable() {
|
|
29
|
+
return this.call('/disable', 'POST');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Approves or denies an AI client's authorization request shown on the
|
|
33
|
+
* relay's consent page. Approved scopes are clamped to the request and to
|
|
34
|
+
* this machine's ceiling by the daemon.
|
|
35
|
+
*/
|
|
36
|
+
async decideConsent(code, approve, scopes) {
|
|
37
|
+
const res = await this.transport.request(`/v1/remote/consents/${approve ? 'approve' : 'deny'}`, {
|
|
38
|
+
method: 'POST', body: approve && scopes ? { code, scopes } : { code },
|
|
39
|
+
});
|
|
40
|
+
return res.data.consent;
|
|
41
|
+
}
|
|
42
|
+
/** Disconnects, revokes the device token and deletes the device key. */
|
|
43
|
+
reset() {
|
|
44
|
+
return this.call('/reset', 'POST');
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/sessions.d.ts
CHANGED
|
@@ -35,14 +35,44 @@ export declare class SwarmSessionsNamespace {
|
|
|
35
35
|
* Deletes a session and triggers cascading cancellation of associated runs and worktrees.
|
|
36
36
|
*/
|
|
37
37
|
delete(sessionId: string): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Renames a session or project conversation you own (`POST /v3/sessions/{id}/title`).
|
|
40
|
+
* The title is trimmed and must be 1–200 characters. A renamed conversation keeps its
|
|
41
|
+
* title: Swarm no longer titles it automatically. (Untitled conversations are named
|
|
42
|
+
* from their first message.) Retrying with the same `clientRequestId` is idempotent.
|
|
43
|
+
*/
|
|
44
|
+
rename(sessionId: string, title: string, options?: {
|
|
45
|
+
clientRequestId?: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
ok: boolean;
|
|
48
|
+
session_id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
}>;
|
|
38
51
|
/**
|
|
39
52
|
* Appends a message to a session conversation.
|
|
40
53
|
*/
|
|
41
54
|
sendMessage(sessionId: string, params: {
|
|
42
55
|
content: string;
|
|
43
|
-
role?: 'user'
|
|
56
|
+
role?: 'user';
|
|
44
57
|
client_request_id?: string;
|
|
45
58
|
}): Promise<any>;
|
|
59
|
+
/**
|
|
60
|
+
* Starts a fresh model context in the same conversation (`POST /v3/sessions/{id}/context/clear`).
|
|
61
|
+
* History stays visible and the session ID is unchanged; the next turn starts without the
|
|
62
|
+
* earlier provider context. The daemon accepts this only for an idle project conversation
|
|
63
|
+
* without an unresolved plan, and only when `expectedLastEventSeq` equals the session's current
|
|
64
|
+
* durable sequence (`state.snapshot.projections_by_session[id].last_event_seq`); otherwise it
|
|
65
|
+
* rejects with `SwarmConflictError` and changes nothing. A standalone chat session cannot be
|
|
66
|
+
* cleared: create a new session instead. Retrying with the same `clientRequestId` is idempotent.
|
|
67
|
+
*/
|
|
68
|
+
clearContext(sessionId: string, params: {
|
|
69
|
+
expectedLastEventSeq: number;
|
|
70
|
+
clientRequestId?: string;
|
|
71
|
+
}): Promise<{
|
|
72
|
+
ok: boolean;
|
|
73
|
+
session_id: string;
|
|
74
|
+
mutation: Record<string, unknown>;
|
|
75
|
+
}>;
|
|
46
76
|
/**
|
|
47
77
|
* Stops an active execution run for a session.
|
|
48
78
|
*/
|
package/dist/sessions.js
CHANGED
|
@@ -84,10 +84,14 @@ export class SwarmSessionsNamespace {
|
|
|
84
84
|
const data = res.data;
|
|
85
85
|
if (data && typeof data === 'object') {
|
|
86
86
|
const sessionObj = data.session || data;
|
|
87
|
+
// V3 sessions carry their source workspace as the primary grant.
|
|
88
|
+
const primaryGrant = Array.isArray(sessionObj.workspace_grants)
|
|
89
|
+
? sessionObj.workspace_grants.find((grant) => grant?.kind === 'primary')
|
|
90
|
+
: undefined;
|
|
87
91
|
return {
|
|
88
92
|
id: sessionObj.id || sessionObj.session_id || sessionId,
|
|
89
93
|
title: sessionObj.title || '',
|
|
90
|
-
workspace_id: sessionObj.workspace_id,
|
|
94
|
+
workspace_id: sessionObj.workspace_id || primaryGrant?.workspace_id,
|
|
91
95
|
workspace_path: sessionObj.workspace_path,
|
|
92
96
|
state: sessionObj.state || data.projection?.state || 'idle',
|
|
93
97
|
mode: sessionObj.mode,
|
|
@@ -145,6 +149,27 @@ export class SwarmSessionsNamespace {
|
|
|
145
149
|
});
|
|
146
150
|
return res.data?.ok === true;
|
|
147
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Renames a session or project conversation you own (`POST /v3/sessions/{id}/title`).
|
|
154
|
+
* The title is trimmed and must be 1–200 characters. A renamed conversation keeps its
|
|
155
|
+
* title: Swarm no longer titles it automatically. (Untitled conversations are named
|
|
156
|
+
* from their first message.) Retrying with the same `clientRequestId` is idempotent.
|
|
157
|
+
*/
|
|
158
|
+
async rename(sessionId, title, options = {}) {
|
|
159
|
+
const id = sessionId.trim();
|
|
160
|
+
if (!id)
|
|
161
|
+
throw new Error('sessionId is required');
|
|
162
|
+
const trimmed = typeof title === 'string' ? title.trim() : '';
|
|
163
|
+
if (!trimmed)
|
|
164
|
+
throw new Error('title is required');
|
|
165
|
+
if ([...trimmed].length > 200)
|
|
166
|
+
throw new Error('title must be 200 characters or fewer');
|
|
167
|
+
const res = await this.transport.request(`/v3/sessions/${encodeURIComponent(id)}/title`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
body: { title: trimmed, client_request_id: options.clientRequestId || generateRequestId() },
|
|
170
|
+
});
|
|
171
|
+
return res.data;
|
|
172
|
+
}
|
|
148
173
|
/**
|
|
149
174
|
* Appends a message to a session conversation.
|
|
150
175
|
*/
|
|
@@ -161,6 +186,29 @@ export class SwarmSessionsNamespace {
|
|
|
161
186
|
});
|
|
162
187
|
return res.data;
|
|
163
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Starts a fresh model context in the same conversation (`POST /v3/sessions/{id}/context/clear`).
|
|
191
|
+
* History stays visible and the session ID is unchanged; the next turn starts without the
|
|
192
|
+
* earlier provider context. The daemon accepts this only for an idle project conversation
|
|
193
|
+
* without an unresolved plan, and only when `expectedLastEventSeq` equals the session's current
|
|
194
|
+
* durable sequence (`state.snapshot.projections_by_session[id].last_event_seq`); otherwise it
|
|
195
|
+
* rejects with `SwarmConflictError` and changes nothing. A standalone chat session cannot be
|
|
196
|
+
* cleared: create a new session instead. Retrying with the same `clientRequestId` is idempotent.
|
|
197
|
+
*/
|
|
198
|
+
async clearContext(sessionId, params) {
|
|
199
|
+
if (!Number.isSafeInteger(params.expectedLastEventSeq) || params.expectedLastEventSeq < 0) {
|
|
200
|
+
throw new Error('expectedLastEventSeq must be the current non-negative session event sequence');
|
|
201
|
+
}
|
|
202
|
+
const id = encodeURIComponent(sessionId.trim());
|
|
203
|
+
const res = await this.transport.request(`/v3/sessions/${id}/context/clear`, {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
body: {
|
|
206
|
+
client_request_id: params.clientRequestId || generateRequestId(),
|
|
207
|
+
expected_last_event_seq: params.expectedLastEventSeq,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
return res.data;
|
|
211
|
+
}
|
|
164
212
|
/**
|
|
165
213
|
* Stops an active execution run for a session.
|
|
166
214
|
*/
|
package/dist/settings.d.ts
CHANGED
|
@@ -98,6 +98,9 @@ export declare class SwarmSettingsNamespace {
|
|
|
98
98
|
/**
|
|
99
99
|
* Restores verified recommended defaults across all system agent and swarm core roles.
|
|
100
100
|
* If expectedUpdatedAt is omitted, automatically reads the current setting's revision first.
|
|
101
|
+
* An account with no model settings yet (a provider is connected but its
|
|
102
|
+
* models were never set up) restores from revision 0: the daemon then sets
|
|
103
|
+
* up the recommended models from a ready provider.
|
|
101
104
|
*/
|
|
102
105
|
restoreDefaults(expectedUpdatedAt?: number): Promise<AgentModelSettingsResponse>;
|
|
103
106
|
/**
|
package/dist/settings.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SwarmNotFoundError } from './errors.js';
|
|
1
2
|
/** Account-scoped settings. Requires the local administrative connection, not a session token. */
|
|
2
3
|
export class SwarmSettingsNamespace {
|
|
3
4
|
transport;
|
|
@@ -23,12 +24,22 @@ export class SwarmSettingsNamespace {
|
|
|
23
24
|
/**
|
|
24
25
|
* Restores verified recommended defaults across all system agent and swarm core roles.
|
|
25
26
|
* If expectedUpdatedAt is omitted, automatically reads the current setting's revision first.
|
|
27
|
+
* An account with no model settings yet (a provider is connected but its
|
|
28
|
+
* models were never set up) restores from revision 0: the daemon then sets
|
|
29
|
+
* up the recommended models from a ready provider.
|
|
26
30
|
*/
|
|
27
31
|
async restoreDefaults(expectedUpdatedAt) {
|
|
28
32
|
let updatedAt = expectedUpdatedAt;
|
|
29
33
|
if (typeof updatedAt !== 'number') {
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
try {
|
|
35
|
+
const current = await this.agentModels();
|
|
36
|
+
updatedAt = current.agent_model_settings.updated_at;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (!(error instanceof SwarmNotFoundError))
|
|
40
|
+
throw error;
|
|
41
|
+
updatedAt = 0;
|
|
42
|
+
}
|
|
32
43
|
}
|
|
33
44
|
return (await this.transport.request('/v1/agent-model-settings/restore-defaults', {
|
|
34
45
|
method: 'POST',
|
|
@@ -87,7 +98,12 @@ export class SwarmSettingsNamespace {
|
|
|
87
98
|
*/
|
|
88
99
|
async applyProviderFleet(provider) {
|
|
89
100
|
const fleet = await this.getRecommendationsForProvider(provider);
|
|
90
|
-
|
|
101
|
+
// The daemon takes one target per PATCH: the swarm pair, then each system agent.
|
|
102
|
+
let result = await this.patch({ swarm: fleet.swarm });
|
|
103
|
+
for (const [slot, assignment] of Object.entries(fleet.system_agents)) {
|
|
104
|
+
result = await this.patch({ system_agents: { [slot]: assignment } });
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
91
107
|
}
|
|
92
108
|
async patch(body) {
|
|
93
109
|
return (await this.transport.request('/v1/agent-model-settings', { method: 'PATCH', body })).data;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { SwarmTransport } from './transport.js';
|
|
2
|
+
export type SignalSeverity = 'info' | 'warning' | 'critical';
|
|
3
|
+
/**
|
|
4
|
+
* One entry in a machine's signal feed: a high-level event such as an agent
|
|
5
|
+
* waiting on a person, a failed run, a refused key or an event an outside
|
|
6
|
+
* source reported. Signals carry ids and a one-line summary, never transcripts,
|
|
7
|
+
* command arguments or secret values.
|
|
8
|
+
*/
|
|
9
|
+
export interface Signal {
|
|
10
|
+
/** Position in this machine's feed; pass the last one back as `after`. */
|
|
11
|
+
seq: number;
|
|
12
|
+
/** Unique id, stable across forwarding (use it to drop duplicates). */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Unix milliseconds. */
|
|
15
|
+
at: number;
|
|
16
|
+
/** Dotted kind, e.g. `agent.blocked`, `run.failed`, `external.falco`. */
|
|
17
|
+
kind: string;
|
|
18
|
+
severity: SignalSeverity;
|
|
19
|
+
/** `swarmd` for Swarm's own signals, `external:<name>` for reported ones. */
|
|
20
|
+
source: string;
|
|
21
|
+
/** Empty for machine-wide signals. */
|
|
22
|
+
account?: string;
|
|
23
|
+
summary: string;
|
|
24
|
+
/** Signals with the same key describe the same ongoing condition. */
|
|
25
|
+
dedup_key?: string;
|
|
26
|
+
refs?: Record<string, string>;
|
|
27
|
+
attrs?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
export interface SignalPage {
|
|
30
|
+
signals: Signal[];
|
|
31
|
+
/** The last position examined; pass it as `after` to continue. */
|
|
32
|
+
next_after: number;
|
|
33
|
+
oldest_seq: number;
|
|
34
|
+
latest_seq: number;
|
|
35
|
+
/**
|
|
36
|
+
* True when signals after your cursor were already dropped (the feed keeps
|
|
37
|
+
* the newest entries only) or your cursor is ahead of the feed. Resync
|
|
38
|
+
* from `next_after` and treat the missed range as unknown.
|
|
39
|
+
*/
|
|
40
|
+
gap: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface ListSignalsParams {
|
|
43
|
+
/** Return signals numbered after this position (default 0: from the oldest kept). */
|
|
44
|
+
after?: number;
|
|
45
|
+
/** At most this many signals (default 100, maximum 500). */
|
|
46
|
+
limit?: number;
|
|
47
|
+
/** Kind prefixes, e.g. `['agent', 'run.failed']`. */
|
|
48
|
+
kinds?: string[];
|
|
49
|
+
/** Only signals at least this severe. */
|
|
50
|
+
minSeverity?: SignalSeverity;
|
|
51
|
+
}
|
|
52
|
+
export interface ReportSignalParams {
|
|
53
|
+
/** Must start with `external.`, e.g. `external.falco`. */
|
|
54
|
+
kind: string;
|
|
55
|
+
/** One line; longer text is cut to 300 characters. */
|
|
56
|
+
summary: string;
|
|
57
|
+
severity?: SignalSeverity;
|
|
58
|
+
/** Lowercase name of the reporting source, e.g. `falco`. */
|
|
59
|
+
source?: string;
|
|
60
|
+
dedup_key?: string;
|
|
61
|
+
refs?: Record<string, string>;
|
|
62
|
+
attrs?: Record<string, string>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The machine signal feed. Reading needs a key with `signals:read`; reporting
|
|
66
|
+
* an outside event needs `signals:write`. Readers see their own account's
|
|
67
|
+
* signals and machine-wide ones.
|
|
68
|
+
*/
|
|
69
|
+
export declare class SwarmSignalsNamespace {
|
|
70
|
+
private readonly transport;
|
|
71
|
+
/** Where this machine forwards its feed (owner only). */
|
|
72
|
+
readonly sinks: SwarmSignalSinksNamespace;
|
|
73
|
+
constructor(transport: SwarmTransport);
|
|
74
|
+
/** Lists signals after a cursor, oldest first. */
|
|
75
|
+
list(params?: ListSignalsParams): Promise<SignalPage>;
|
|
76
|
+
/**
|
|
77
|
+
* Reports an outside event (a host security monitor, a backup script) into
|
|
78
|
+
* the feed. Swarm records the source as `external:<source>` and, for a
|
|
79
|
+
* scoped key, which key reported it.
|
|
80
|
+
*/
|
|
81
|
+
report(params: ReportSignalParams): Promise<Signal>;
|
|
82
|
+
}
|
|
83
|
+
/** Where a machine forwards its signal feed (owner only). */
|
|
84
|
+
export interface SignalSink {
|
|
85
|
+
id: string;
|
|
86
|
+
account: string;
|
|
87
|
+
name: string;
|
|
88
|
+
url: string;
|
|
89
|
+
kinds?: string[];
|
|
90
|
+
min_severity?: SignalSeverity;
|
|
91
|
+
heartbeat_seconds: number;
|
|
92
|
+
/** Last signal position the sink accepted. */
|
|
93
|
+
cursor: number;
|
|
94
|
+
created_at: number;
|
|
95
|
+
last_delivered_at?: number;
|
|
96
|
+
last_attempt_at?: number;
|
|
97
|
+
last_error?: string;
|
|
98
|
+
failures?: number;
|
|
99
|
+
}
|
|
100
|
+
export interface CreateSignalSinkParams {
|
|
101
|
+
name: string;
|
|
102
|
+
/** https URL (http only to this machine's loopback). */
|
|
103
|
+
url: string;
|
|
104
|
+
/** Kind prefixes to forward, e.g. `['agent', 'worker.run.failed']`; empty forwards all. */
|
|
105
|
+
kinds?: string[];
|
|
106
|
+
minSeverity?: SignalSeverity;
|
|
107
|
+
/** Idle heartbeat interval, 30 to 3600 seconds (default 60). */
|
|
108
|
+
heartbeatSeconds?: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Signal sinks: the machine POSTs batches of its feed to each sink, signed
|
|
112
|
+
* with the sink's secret (see `verifySignalDelivery`). Owner only: scoped
|
|
113
|
+
* keys, including admin keys, are refused.
|
|
114
|
+
*/
|
|
115
|
+
export declare class SwarmSignalSinksNamespace {
|
|
116
|
+
private readonly transport;
|
|
117
|
+
constructor(transport: SwarmTransport);
|
|
118
|
+
list(): Promise<SignalSink[]>;
|
|
119
|
+
/** Creates a sink. The signing secret is returned only here; store it in the receiver. */
|
|
120
|
+
create(params: CreateSignalSinkParams): Promise<{
|
|
121
|
+
sink: SignalSink;
|
|
122
|
+
secret: string;
|
|
123
|
+
}>;
|
|
124
|
+
delete(id: string): Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
/** Body of one signal delivery (version 1). */
|
|
127
|
+
export interface SignalDelivery {
|
|
128
|
+
version: 1;
|
|
129
|
+
sink_id: string;
|
|
130
|
+
machine?: string;
|
|
131
|
+
sent_at: number;
|
|
132
|
+
/** True for an idle heartbeat with no signals. */
|
|
133
|
+
heartbeat: boolean;
|
|
134
|
+
signals: Signal[];
|
|
135
|
+
next_after: number;
|
|
136
|
+
latest_seq: number;
|
|
137
|
+
gap: boolean;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Checks a delivery's `X-Swarm-Signature` for a receiver: HMAC-SHA256 of
|
|
141
|
+
* `${X-Swarm-Timestamp}.${rawBody}` with the sink secret, and a timestamp
|
|
142
|
+
* within `maxSkewMs` of now (rejects replays). Pass the raw request body,
|
|
143
|
+
* not re-serialized JSON. Works with WebCrypto (Node 18+, Workers, browsers).
|
|
144
|
+
*/
|
|
145
|
+
export declare function verifySignalDelivery(secret: string, timestamp: string | number, rawBody: string | Uint8Array, signature: string, { now, maxSkewMs }?: {
|
|
146
|
+
now?: number;
|
|
147
|
+
maxSkewMs?: number;
|
|
148
|
+
}): Promise<boolean>;
|
package/dist/signals.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The machine signal feed. Reading needs a key with `signals:read`; reporting
|
|
3
|
+
* an outside event needs `signals:write`. Readers see their own account's
|
|
4
|
+
* signals and machine-wide ones.
|
|
5
|
+
*/
|
|
6
|
+
export class SwarmSignalsNamespace {
|
|
7
|
+
transport;
|
|
8
|
+
/** Where this machine forwards its feed (owner only). */
|
|
9
|
+
sinks;
|
|
10
|
+
constructor(transport) {
|
|
11
|
+
this.transport = transport;
|
|
12
|
+
this.sinks = new SwarmSignalSinksNamespace(transport);
|
|
13
|
+
}
|
|
14
|
+
/** Lists signals after a cursor, oldest first. */
|
|
15
|
+
async list(params = {}) {
|
|
16
|
+
const q = new URLSearchParams();
|
|
17
|
+
if (typeof params.after === 'number')
|
|
18
|
+
q.set('after', String(params.after));
|
|
19
|
+
if (typeof params.limit === 'number')
|
|
20
|
+
q.set('limit', String(params.limit));
|
|
21
|
+
if (params.kinds?.length)
|
|
22
|
+
q.set('kind', params.kinds.join(','));
|
|
23
|
+
if (params.minSeverity)
|
|
24
|
+
q.set('min_severity', params.minSeverity);
|
|
25
|
+
const path = q.toString() ? `/v3/signals?${q.toString()}` : '/v3/signals';
|
|
26
|
+
const res = await this.transport.request(path, { method: 'GET' });
|
|
27
|
+
const { signals, next_after, oldest_seq, latest_seq, gap } = res.data;
|
|
28
|
+
return { signals: signals ?? [], next_after, oldest_seq, latest_seq, gap };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reports an outside event (a host security monitor, a backup script) into
|
|
32
|
+
* the feed. Swarm records the source as `external:<source>` and, for a
|
|
33
|
+
* scoped key, which key reported it.
|
|
34
|
+
*/
|
|
35
|
+
async report(params) {
|
|
36
|
+
const res = await this.transport.request('/v3/signals', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: params,
|
|
39
|
+
});
|
|
40
|
+
return res.data.signal;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Signal sinks: the machine POSTs batches of its feed to each sink, signed
|
|
45
|
+
* with the sink's secret (see `verifySignalDelivery`). Owner only: scoped
|
|
46
|
+
* keys, including admin keys, are refused.
|
|
47
|
+
*/
|
|
48
|
+
export class SwarmSignalSinksNamespace {
|
|
49
|
+
transport;
|
|
50
|
+
constructor(transport) {
|
|
51
|
+
this.transport = transport;
|
|
52
|
+
}
|
|
53
|
+
async list() {
|
|
54
|
+
const res = await this.transport.request('/v3/signals/sinks', { method: 'GET' });
|
|
55
|
+
return res.data.sinks ?? [];
|
|
56
|
+
}
|
|
57
|
+
/** Creates a sink. The signing secret is returned only here; store it in the receiver. */
|
|
58
|
+
async create(params) {
|
|
59
|
+
const res = await this.transport.request('/v3/signals/sinks', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
body: {
|
|
62
|
+
name: params.name,
|
|
63
|
+
url: params.url,
|
|
64
|
+
kinds: params.kinds,
|
|
65
|
+
min_severity: params.minSeverity,
|
|
66
|
+
heartbeat_seconds: params.heartbeatSeconds,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return { sink: res.data.sink, secret: res.data.secret };
|
|
70
|
+
}
|
|
71
|
+
async delete(id) {
|
|
72
|
+
await this.transport.request(`/v3/signals/sinks/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Checks a delivery's `X-Swarm-Signature` for a receiver: HMAC-SHA256 of
|
|
77
|
+
* `${X-Swarm-Timestamp}.${rawBody}` with the sink secret, and a timestamp
|
|
78
|
+
* within `maxSkewMs` of now (rejects replays). Pass the raw request body,
|
|
79
|
+
* not re-serialized JSON. Works with WebCrypto (Node 18+, Workers, browsers).
|
|
80
|
+
*/
|
|
81
|
+
export async function verifySignalDelivery(secret, timestamp, rawBody, signature, { now = Date.now(), maxSkewMs = 5 * 60 * 1000 } = {}) {
|
|
82
|
+
const ts = Number(timestamp);
|
|
83
|
+
if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkewMs || !signature.startsWith('v1='))
|
|
84
|
+
return false;
|
|
85
|
+
const enc = new TextEncoder();
|
|
86
|
+
const body = typeof rawBody === 'string' ? enc.encode(rawBody) : rawBody;
|
|
87
|
+
const message = new Uint8Array([...enc.encode(`${ts}.`), ...body]);
|
|
88
|
+
const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
89
|
+
const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, message));
|
|
90
|
+
const expected = 'v1=' + Array.from(mac, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
91
|
+
if (expected.length !== signature.length)
|
|
92
|
+
return false;
|
|
93
|
+
let diff = 0;
|
|
94
|
+
for (let i = 0; i < expected.length; i++)
|
|
95
|
+
diff |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
|
|
96
|
+
return diff === 0;
|
|
97
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export type TokenScope = 'automations:trigger' | 'automations:read' | 'automations:write' | 'sessions:read' | 'sessions:write' | 'projects:read' | 'projects:write' | 'admin' | string;
|
|
2
2
|
export interface SwarmClientConfig {
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Base URL of the Swarm daemon API (default http://127.0.0.1:7781). With no
|
|
5
|
+
* `baseUrl`, `socketPath` or environment override, Node clients use the
|
|
6
|
+
* owner socket of a system install when this process may connect to it.
|
|
7
|
+
*/
|
|
4
8
|
baseUrl?: string;
|
|
5
9
|
/** Bearer authentication token (attach token or scoped token starting with 'swk_') */
|
|
6
10
|
token?: string;
|
|
7
|
-
/** Unix
|
|
11
|
+
/** Unix socket for local IPC (owner socket: /var/lib/swarmd/local-transport/api.sock) */
|
|
8
12
|
socketPath?: string;
|
|
9
13
|
/** Custom default HTTP headers */
|
|
10
14
|
defaultHeaders?: Record<string, string>;
|
|
@@ -496,10 +500,6 @@ export interface UpdateProjectParams {
|
|
|
496
500
|
export interface ListProjectsParams {
|
|
497
501
|
limit?: number;
|
|
498
502
|
}
|
|
499
|
-
export interface SynthesizeProjectContextParams {
|
|
500
|
-
name: string;
|
|
501
|
-
workspaces: string[];
|
|
502
|
-
}
|
|
503
503
|
export interface DeleteProjectResult {
|
|
504
504
|
status: string;
|
|
505
505
|
id: string;
|
|
@@ -535,6 +535,43 @@ export interface ProjectTaskPlanBinding {
|
|
|
535
535
|
session_id?: string;
|
|
536
536
|
receipt?: string;
|
|
537
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Backend-inspected delivery facts for a task's captured worktree lane. Only the
|
|
540
|
+
* task-detail GET (`getTask`) observes Git; collection rows carry `freshness: 'stale'`.
|
|
541
|
+
*/
|
|
542
|
+
export interface ProjectTaskDeliveryAssessment {
|
|
543
|
+
task_id?: string;
|
|
544
|
+
task_revision?: number;
|
|
545
|
+
attempt_id?: string;
|
|
546
|
+
session_id?: string;
|
|
547
|
+
base_oid?: string;
|
|
548
|
+
source_oid?: string;
|
|
549
|
+
target_oid?: string;
|
|
550
|
+
source_branch?: string;
|
|
551
|
+
target_branch?: string;
|
|
552
|
+
state?: 'candidate_work' | 'integrated' | 'unavailable' | string;
|
|
553
|
+
reason_code?: string;
|
|
554
|
+
reason?: string;
|
|
555
|
+
freshness?: 'observed' | 'stale' | 'unknown' | string;
|
|
556
|
+
source_dirty?: boolean;
|
|
557
|
+
target_dirty?: boolean;
|
|
558
|
+
candidate_commits?: number;
|
|
559
|
+
files?: string[];
|
|
560
|
+
allowed_actions?: string[];
|
|
561
|
+
[key: string]: unknown;
|
|
562
|
+
}
|
|
563
|
+
/** One coordinator attempt (session reservation) on a project task. */
|
|
564
|
+
export interface ProjectTaskAttempt {
|
|
565
|
+
id: string;
|
|
566
|
+
session_id?: string;
|
|
567
|
+
status?: string;
|
|
568
|
+
role?: string;
|
|
569
|
+
workspace_path?: string;
|
|
570
|
+
worktree_branch?: string;
|
|
571
|
+
base_branch?: string;
|
|
572
|
+
base_commit?: string;
|
|
573
|
+
[key: string]: unknown;
|
|
574
|
+
}
|
|
538
575
|
export interface ProjectTaskRecord {
|
|
539
576
|
id: string;
|
|
540
577
|
project_id: string;
|
|
@@ -543,6 +580,9 @@ export interface ProjectTaskRecord {
|
|
|
543
580
|
description?: string;
|
|
544
581
|
status: 'queued' | 'in_progress' | 'needs_review' | 'completed' | 'failed' | 'pending_approval' | 'planning' | 'rejected' | string;
|
|
545
582
|
session_id?: string;
|
|
583
|
+
active_attempt_id?: string;
|
|
584
|
+
attempts?: ProjectTaskAttempt[];
|
|
585
|
+
delivery_assessment?: ProjectTaskDeliveryAssessment;
|
|
546
586
|
agent?: string;
|
|
547
587
|
worker_name?: string;
|
|
548
588
|
outcome_type?: string;
|
|
@@ -744,6 +784,30 @@ export interface RefineProjectTaskResult {
|
|
|
744
784
|
status: 'refined' | string;
|
|
745
785
|
task: ProjectTaskRecord;
|
|
746
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* Selection body for POST /v3/projects/{id}/tasks/{taskId}/integrate. The daemon
|
|
789
|
+
* requires `session_id` (must equal the task's current session) plus the captured
|
|
790
|
+
* `source_branch`/`target_branch` (must equal the session worktree branch and its
|
|
791
|
+
* base branch). The remaining fields are the stale-state guards the daemon's
|
|
792
|
+
* `recover-integrate` path checks; plain integrate accepts and ignores them.
|
|
793
|
+
*/
|
|
794
|
+
export interface IntegrateProjectTaskSelection {
|
|
795
|
+
session_id: string;
|
|
796
|
+
source_branch: string;
|
|
797
|
+
target_branch: string;
|
|
798
|
+
revision?: number;
|
|
799
|
+
attempt_id?: string;
|
|
800
|
+
source_head?: string;
|
|
801
|
+
target_head?: string;
|
|
802
|
+
}
|
|
803
|
+
export interface IntegrateProjectTaskOptions extends RequestOptions {
|
|
804
|
+
/**
|
|
805
|
+
* Overrides merged over the selection the SDK derives from a fresh `getTask`.
|
|
806
|
+
* When `session_id`, `source_branch` and `target_branch` are all supplied, the
|
|
807
|
+
* SDK skips the task fetch and sends them as given.
|
|
808
|
+
*/
|
|
809
|
+
selection?: Partial<IntegrateProjectTaskSelection>;
|
|
810
|
+
}
|
|
747
811
|
export interface IntegrateProjectTaskResult {
|
|
748
812
|
status: 'integrated' | 'already_integrated' | string;
|
|
749
813
|
message?: string;
|
|
@@ -914,10 +978,18 @@ export interface WorkerAutomationDefinition {
|
|
|
914
978
|
plan_document: SessionPlanDocument;
|
|
915
979
|
input_requirements?: WorkerInputRequirement[] | null;
|
|
916
980
|
deliverable_requirements?: WorkerDeliverableRequirement[] | null;
|
|
981
|
+
/** Registered workspace this automation's runs work in; absent means the worker's primary binding. */
|
|
982
|
+
workspace_id?: string | null;
|
|
917
983
|
revision: number;
|
|
918
984
|
created_at: number;
|
|
919
985
|
updated_at: number;
|
|
920
986
|
}
|
|
987
|
+
/**
|
|
988
|
+
* Code-owned system agent that runs a worker's jobs. Absent means `swarm`.
|
|
989
|
+
* `swarm` executes jobs as accepted plans; `coder`, `designer` and `finder`
|
|
990
|
+
* run their compiled agent in an auto session.
|
|
991
|
+
*/
|
|
992
|
+
export type WorkerRole = 'swarm' | 'coder' | 'designer' | 'finder';
|
|
921
993
|
export interface WorkerAutomationInput {
|
|
922
994
|
name: string;
|
|
923
995
|
description?: string;
|
|
@@ -933,10 +1005,13 @@ export interface WorkerAutomationInput {
|
|
|
933
1005
|
plan_document: SessionPlanDocument;
|
|
934
1006
|
input_requirements?: WorkerInputRequirement[];
|
|
935
1007
|
deliverable_requirements?: WorkerDeliverableRequirement[];
|
|
1008
|
+
/** Registered workspace this automation's runs work in. Required for schedules of a worker with no primary binding. */
|
|
1009
|
+
workspace_id?: string;
|
|
936
1010
|
}
|
|
937
1011
|
export interface WorkerRecord {
|
|
938
1012
|
id: string;
|
|
939
1013
|
account_scope_id: string;
|
|
1014
|
+
role?: WorkerRole | null;
|
|
940
1015
|
name: string;
|
|
941
1016
|
description?: string | null;
|
|
942
1017
|
instructions: string;
|
|
@@ -970,6 +1045,12 @@ export interface WorkerRunRecord {
|
|
|
970
1045
|
automation_id?: string;
|
|
971
1046
|
automation_revision?: number;
|
|
972
1047
|
occurrence_id?: string;
|
|
1048
|
+
/** Workspace pinned at admission. */
|
|
1049
|
+
workspace_id?: string;
|
|
1050
|
+
/** `worker_job` when the request or automation named the workspace, `worker_binding` when it came from the primary binding. */
|
|
1051
|
+
workspace_source?: 'worker_job' | 'worker_binding' | string;
|
|
1052
|
+
/** Worker role pinned at admission; absent means `swarm`. */
|
|
1053
|
+
role?: WorkerRole | string;
|
|
973
1054
|
session_id?: string;
|
|
974
1055
|
request_source: 'direct' | 'schedule' | 'trigger' | 'test_run' | 'orchestrator' | string;
|
|
975
1056
|
input?: Record<string, unknown>;
|
|
@@ -1013,6 +1094,7 @@ export interface PortableWorkerDefinition {
|
|
|
1013
1094
|
provenance?: PortableWorkerProvenance | null;
|
|
1014
1095
|
}
|
|
1015
1096
|
export interface CreateWorkerParams {
|
|
1097
|
+
role?: WorkerRole;
|
|
1016
1098
|
name: string;
|
|
1017
1099
|
description?: string;
|
|
1018
1100
|
instructions?: string;
|
|
@@ -1023,6 +1105,7 @@ export interface CreateWorkerParams {
|
|
|
1023
1105
|
idempotency_key: string;
|
|
1024
1106
|
}
|
|
1025
1107
|
export interface UpdateWorkerParams {
|
|
1108
|
+
role?: WorkerRole;
|
|
1026
1109
|
name?: string;
|
|
1027
1110
|
description?: string;
|
|
1028
1111
|
instructions?: string;
|
|
@@ -1106,6 +1189,10 @@ export interface DirectWorkerRequestParams {
|
|
|
1106
1189
|
prompt?: string;
|
|
1107
1190
|
input?: Record<string, unknown>;
|
|
1108
1191
|
idempotency_key?: string;
|
|
1192
|
+
/** Workspace this job works in (registered workspace ID). Required when the worker has no primary binding. */
|
|
1193
|
+
workspace_id?: string;
|
|
1194
|
+
/** Alternative to workspace_id: the exact root path of a registered workspace. */
|
|
1195
|
+
workspace_path?: string;
|
|
1109
1196
|
}
|
|
1110
1197
|
export interface TestWorkerRunParams {
|
|
1111
1198
|
worker_id: string;
|
|
@@ -1113,6 +1200,10 @@ export interface TestWorkerRunParams {
|
|
|
1113
1200
|
prompt?: string;
|
|
1114
1201
|
input?: Record<string, unknown>;
|
|
1115
1202
|
idempotency_key?: string;
|
|
1203
|
+
/** Workspace this job works in (registered workspace ID). */
|
|
1204
|
+
workspace_id?: string;
|
|
1205
|
+
/** Alternative to workspace_id: the exact root path of a registered workspace. */
|
|
1206
|
+
workspace_path?: string;
|
|
1116
1207
|
}
|
|
1117
1208
|
export interface TriggerWorkerParams {
|
|
1118
1209
|
worker_id: string;
|
package/dist/websocket.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Server as HttpsServer } from 'node:https';
|
|
|
3
3
|
import { WebSocket } from 'ws';
|
|
4
4
|
import type { SwarmChatNamespace, ToolStreamItem } from './chat.js';
|
|
5
5
|
import type { PendingPermissionRecord, SwarmPermissionsNamespace, PermissionResolver, ResolvePermissionOptions, ResolvePermissionResult } from './permissions.js';
|
|
6
|
-
import type { SessionWatchState, V3Message } from './realtime.js';
|
|
6
|
+
import type { SessionWatchState, V3Message, V3RunState } from './realtime.js';
|
|
7
7
|
export interface SwarmWebSocketBridgeOptions {
|
|
8
8
|
path?: string;
|
|
9
9
|
/** Server-owned policy. Browser frames cannot enable auto approval. Questions always require a reply. */
|
|
@@ -37,6 +37,8 @@ export interface SwarmWebSocketServerMessage {
|
|
|
37
37
|
result?: ResolvePermissionResult;
|
|
38
38
|
permissionId?: string;
|
|
39
39
|
messages?: V3Message[];
|
|
40
|
+
/** Finished run on `done`: `status` is completed, failed, cancelled, ... */
|
|
41
|
+
run?: V3RunState;
|
|
40
42
|
error?: string;
|
|
41
43
|
}
|
|
42
44
|
/** Application-owned bridge to canonical V3 realtime. A subscription survives every turn.
|