@openwop/openwop 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -117
- package/dist/client.d.ts +131 -245
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +233 -440
- package/dist/client.js.map +1 -1
- package/dist/cost-attribution.d.ts +2 -2
- package/dist/cost-attribution.js +2 -2
- package/dist/envelope-directive.d.ts +1 -1
- package/dist/envelope-directive.js +1 -1
- package/dist/event-helpers.js +1 -1
- package/dist/event-helpers.js.map +1 -1
- package/dist/generated.d.ts +17 -0
- package/dist/generated.d.ts.map +1 -0
- package/dist/generated.js +311 -0
- package/dist/generated.js.map +1 -0
- package/dist/index.d.ts +16 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -56
- package/dist/index.js.map +1 -1
- package/dist/run-helpers.d.ts +20 -21
- package/dist/run-helpers.d.ts.map +1 -1
- package/dist/run-helpers.js +23 -72
- package/dist/run-helpers.js.map +1 -1
- package/dist/sse.d.ts +33 -15
- package/dist/sse.d.ts.map +1 -1
- package/dist/sse.js +28 -30
- package/dist/sse.js.map +1 -1
- package/dist/types.d.ts +253 -559
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/webhook-header-families.d.ts +22 -13
- package/dist/webhook-header-families.d.ts.map +1 -1
- package/dist/webhook-header-families.js +30 -27
- package/dist/webhook-header-families.js.map +1 -1
- package/dist/webhook-helpers.browser.d.ts +15 -29
- package/dist/webhook-helpers.browser.d.ts.map +1 -1
- package/dist/webhook-helpers.browser.js +16 -31
- package/dist/webhook-helpers.browser.js.map +1 -1
- package/dist/webhook-helpers.d.ts +41 -41
- package/dist/webhook-helpers.d.ts.map +1 -1
- package/dist/webhook-helpers.js +40 -47
- package/dist/webhook-helpers.js.map +1 -1
- package/package.json +6 -4
- package/src/client.ts +255 -454
- package/src/cost-attribution.ts +2 -2
- package/src/envelope-directive.ts +1 -1
- package/src/event-helpers.ts +1 -1
- package/src/generated.ts +322 -0
- package/src/index.ts +78 -110
- package/src/run-helpers.ts +27 -85
- package/src/sse.ts +63 -42
- package/src/types.ts +268 -603
- package/src/webhook-header-families.ts +41 -29
- package/src/webhook-helpers.browser.ts +23 -32
- package/src/webhook-helpers.ts +67 -53
- package/dist/registry-helpers.d.ts +0 -118
- package/dist/registry-helpers.d.ts.map +0 -1
- package/dist/registry-helpers.js +0 -82
- package/dist/registry-helpers.js.map +0 -1
- package/src/registry-helpers.ts +0 -173
package/src/client.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenwopClient — typed HTTP client for the
|
|
2
|
+
* OpenwopClient — typed HTTP client for the OpenWOP v2 REST surface.
|
|
3
3
|
*
|
|
4
|
-
* Hand-authored. Each method maps 1:1 to
|
|
5
|
-
*
|
|
4
|
+
* Hand-authored. Each method maps 1:1 to an operation in
|
|
5
|
+
* `spec/v2/path-manifest.json` (generated from `api/v2/openapi.yaml`):
|
|
6
|
+
* bare origin, unversioned path keys, negotiation by the `OpenWOP-Version`
|
|
7
|
+
* request header (RFC 0172 §A). Request/response types live in ./types.ts;
|
|
8
|
+
* the error-code union in ./generated.ts.
|
|
6
9
|
*
|
|
7
|
-
* Auth: a single bearer-style API key, supplied at construction.
|
|
8
|
-
* ../../auth.md for credential format.
|
|
10
|
+
* Auth: a single bearer-style API key, supplied at construction.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import { streamEvents, type EventsStreamOptions } from './sse.js';
|
|
13
|
+
import { streamEvents, streamHostEvents, type EventsStreamOptions, type HostEventsStreamOptions } from './sse.js';
|
|
12
14
|
import {
|
|
13
15
|
WopError,
|
|
14
16
|
type AuditVerifyResult,
|
|
@@ -24,19 +26,19 @@ import {
|
|
|
24
26
|
type Capabilities,
|
|
25
27
|
type CancelRunRequest,
|
|
26
28
|
type CancelRunResponse,
|
|
29
|
+
type CompensationProjection,
|
|
27
30
|
type CreateRunRequest,
|
|
28
31
|
type CreateRunResponse,
|
|
32
|
+
type EffectLedgerProjection,
|
|
33
|
+
type EffectSeamManifest,
|
|
29
34
|
type ErrorEnvelope,
|
|
30
35
|
type ForkRunRequest,
|
|
31
36
|
type ForkRunResponse,
|
|
32
37
|
type Annotation,
|
|
33
38
|
type CreateAnnotationRequest,
|
|
34
|
-
type WorkspaceFile,
|
|
35
|
-
type PutWorkspaceFileRequest,
|
|
36
39
|
type GetPromptRequest,
|
|
40
|
+
type HostEventDoc,
|
|
37
41
|
type InterruptByTokenInspection,
|
|
38
|
-
type DebugBundle,
|
|
39
|
-
type DebugBundleOptions,
|
|
40
42
|
type ListPromptsRequest,
|
|
41
43
|
type ListPromptsResponse,
|
|
42
44
|
type PromptTemplate,
|
|
@@ -67,18 +69,31 @@ import {
|
|
|
67
69
|
type CompactToolDescriptor,
|
|
68
70
|
type AgentDeployment,
|
|
69
71
|
type AgentDeploymentTransition,
|
|
70
|
-
type CreateUserAgentRequest,
|
|
71
|
-
type UserAgentRecord,
|
|
72
|
-
type AgentPackRegistryResponse,
|
|
73
|
-
type InstallAgentPackRequest,
|
|
74
|
-
type InstallAgentPackResponse,
|
|
75
72
|
} from './types.js';
|
|
76
73
|
|
|
74
|
+
/** The protocol major this SDK implements; the default for {@link OpenwopClientOptions.major}. */
|
|
75
|
+
export const SDK_PROTOCOL_MAJOR = 2;
|
|
76
|
+
|
|
77
|
+
/** Renders the `OpenWOP-Version` request value for a major: `<major>.0` (the OpenAPI grammar is `<major>.<minor>`). */
|
|
78
|
+
export function protocolVersionHeader(major: number): string {
|
|
79
|
+
if (!Number.isInteger(major) || major < 0) {
|
|
80
|
+
throw new TypeError(`OpenwopClient: major must be a non-negative integer (got ${String(major)})`);
|
|
81
|
+
}
|
|
82
|
+
return `${major}.0`;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
export interface OpenwopClientOptions {
|
|
78
86
|
/** Base URL of the openwop server, e.g., `https://api.example.com`. Trailing slash optional. */
|
|
79
87
|
readonly baseUrl: string;
|
|
80
|
-
/** API key (bearer-style).
|
|
88
|
+
/** API key (bearer-style). */
|
|
81
89
|
readonly apiKey: string;
|
|
90
|
+
/**
|
|
91
|
+
* The protocol major to negotiate (RFC 0172 §A.3). Every request carries
|
|
92
|
+
* `OpenWOP-Version: <major>.0`; a host that does not list the major answers
|
|
93
|
+
* `406 protocol_version_unsupported` with `details.protocolVersions[]`.
|
|
94
|
+
* Default {@link SDK_PROTOCOL_MAJOR} (2).
|
|
95
|
+
*/
|
|
96
|
+
readonly major?: number;
|
|
82
97
|
/** Optional fetch implementation override (test injection). Defaults to global `fetch`. */
|
|
83
98
|
readonly fetch?: typeof fetch;
|
|
84
99
|
/** Default `Accept-Language` to send. Optional. */
|
|
@@ -86,9 +101,9 @@ export interface OpenwopClientOptions {
|
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
export interface MutationOptions {
|
|
89
|
-
/**
|
|
104
|
+
/** `Idempotency-Key` for at-most-once mutation semantics (idempotency.md Layer 1). */
|
|
90
105
|
readonly idempotencyKey?: string;
|
|
91
|
-
/**
|
|
106
|
+
/** `OpenWOP-Dedup: enforce` — the host rejects a duplicate `(tenantId, scopeId)` with `409 run_already_active` (runs.md §Create). */
|
|
92
107
|
readonly dedup?: 'enforce';
|
|
93
108
|
}
|
|
94
109
|
|
|
@@ -105,6 +120,12 @@ export class OpenwopClient {
|
|
|
105
120
|
readonly #apiKey: string;
|
|
106
121
|
readonly #fetch: typeof fetch;
|
|
107
122
|
readonly #acceptLanguage: string | undefined;
|
|
123
|
+
readonly #versionHeader: string;
|
|
124
|
+
|
|
125
|
+
/** The `OpenWOP-Version` value this client sends on every request. */
|
|
126
|
+
get protocolVersion(): string {
|
|
127
|
+
return this.#versionHeader;
|
|
128
|
+
}
|
|
108
129
|
|
|
109
130
|
constructor(opts: OpenwopClientOptions) {
|
|
110
131
|
if (!opts.baseUrl) throw new TypeError('OpenwopClient: baseUrl is required');
|
|
@@ -113,69 +134,53 @@ export class OpenwopClient {
|
|
|
113
134
|
this.#apiKey = opts.apiKey;
|
|
114
135
|
this.#fetch = opts.fetch ?? fetch;
|
|
115
136
|
this.#acceptLanguage = opts.acceptLanguage;
|
|
137
|
+
this.#versionHeader = protocolVersionHeader(opts.major ?? SDK_PROTOCOL_MAJOR);
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
// ── Discovery ────────────────────────────────────────────────────────
|
|
119
141
|
readonly discovery = {
|
|
142
|
+
/**
|
|
143
|
+
* `GET /.well-known/openwop` — one resource whose representation the
|
|
144
|
+
* `OpenWOP-Version` header selects (capabilities.md §1): with this
|
|
145
|
+
* client's major the host returns the closed v2 root.
|
|
146
|
+
*/
|
|
120
147
|
capabilities: (): Promise<Capabilities> =>
|
|
121
148
|
this.#request<Capabilities>({ method: 'GET', path: '/.well-known/openwop' }, false),
|
|
122
149
|
|
|
150
|
+
/** `GET /openapi.json` — the self-describing OpenAPI 3.1 document. */
|
|
123
151
|
openapi: (): Promise<unknown> =>
|
|
124
|
-
this.#request<unknown>({ method: 'GET', path: '/
|
|
152
|
+
this.#request<unknown>({ method: 'GET', path: '/openapi.json' }, false),
|
|
125
153
|
};
|
|
126
154
|
|
|
127
155
|
// ── Workflows ────────────────────────────────────────────────────────
|
|
128
156
|
readonly workflows = {
|
|
157
|
+
/** `GET /workflows/{workflowId}` */
|
|
129
158
|
get: (workflowId: string): Promise<unknown> =>
|
|
130
159
|
this.#request<unknown>({
|
|
131
160
|
method: 'GET',
|
|
132
|
-
path: `/
|
|
161
|
+
path: `/workflows/${encodeURIComponent(workflowId)}`,
|
|
133
162
|
}),
|
|
134
163
|
};
|
|
135
164
|
|
|
136
165
|
// ── Runs ─────────────────────────────────────────────────────────────
|
|
137
166
|
readonly runs = {
|
|
167
|
+
/** `POST /runs` — the body is closed at the composition (runs.md §Create). */
|
|
138
168
|
create: (body: CreateRunRequest, opts: MutationOptions = {}): Promise<CreateRunResponse> =>
|
|
139
169
|
this.#request<CreateRunResponse>({
|
|
140
170
|
method: 'POST',
|
|
141
|
-
path: '/
|
|
171
|
+
path: '/runs',
|
|
142
172
|
body,
|
|
143
173
|
headers: this.#mutationHeaders(opts),
|
|
144
174
|
}),
|
|
145
175
|
|
|
176
|
+
/** `GET /runs/{runId}` — the snapshot (runs.md §Snapshot). */
|
|
146
177
|
get: (runId: string): Promise<RunSnapshot> =>
|
|
147
178
|
this.#request<RunSnapshot>({
|
|
148
179
|
method: 'GET',
|
|
149
|
-
path: `/
|
|
180
|
+
path: `/runs/${encodeURIComponent(runId)}`,
|
|
150
181
|
}),
|
|
151
182
|
|
|
152
|
-
/**
|
|
153
|
-
* Fetch the portable JSON diagnostic export for a single run per
|
|
154
|
-
* `spec/v1/debug-bundle.md`. The bundle's `redactionMode` reflects
|
|
155
|
-
* the host's advertised `capabilities.compliance.defaultMode`; the
|
|
156
|
-
* caller MUST treat masked/omitted/hashed fields as the
|
|
157
|
-
* spec-canonical value. The `truncated` + `truncatedReason` fields
|
|
158
|
-
* indicate the host hit its size cap.
|
|
159
|
-
*
|
|
160
|
-
* Returns `null` when the host doesn't advertise
|
|
161
|
-
* `capabilities.debugBundle.supported: true` (the endpoint returns
|
|
162
|
-
* 404 in that case per `debug-bundle.md` §"Authorization").
|
|
163
|
-
*/
|
|
164
|
-
debugBundle: async (runId: string, opts: DebugBundleOptions = {}): Promise<DebugBundle | null> => {
|
|
165
|
-
const params = new URLSearchParams();
|
|
166
|
-
if (opts.maxEvents !== undefined) params.set('maxEvents', String(opts.maxEvents));
|
|
167
|
-
const query = params.toString();
|
|
168
|
-
const path = `/v1/runs/${encodeURIComponent(runId)}/debug-bundle${query ? `?${query}` : ''}`;
|
|
169
|
-
try {
|
|
170
|
-
return await this.#request<DebugBundle>({ method: 'GET', path });
|
|
171
|
-
} catch (err) {
|
|
172
|
-
// Host doesn't advertise the capability → 404. Surface as null so callers
|
|
173
|
-
// can branch on capability discovery without try/catch.
|
|
174
|
-
if (err instanceof WopError && err.status === 404) return null;
|
|
175
|
-
throw err;
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
|
|
183
|
+
/** `POST /runs/{runId}/cancel` — `status` is `cancelling` or `cancelled`. */
|
|
179
184
|
cancel: (
|
|
180
185
|
runId: string,
|
|
181
186
|
body: CancelRunRequest = {},
|
|
@@ -183,11 +188,12 @@ export class OpenwopClient {
|
|
|
183
188
|
): Promise<CancelRunResponse> =>
|
|
184
189
|
this.#request<CancelRunResponse>({
|
|
185
190
|
method: 'POST',
|
|
186
|
-
path: `/
|
|
191
|
+
path: `/runs/${encodeURIComponent(runId)}/cancel`,
|
|
187
192
|
body,
|
|
188
193
|
headers: this.#mutationHeaders(opts),
|
|
189
194
|
}),
|
|
190
195
|
|
|
196
|
+
/** `POST /runs/{runId}:pause` — `409` when the run is not pausable. */
|
|
191
197
|
pause: (
|
|
192
198
|
runId: string,
|
|
193
199
|
body: PauseRunRequest = {},
|
|
@@ -195,11 +201,12 @@ export class OpenwopClient {
|
|
|
195
201
|
): Promise<PauseRunResponse> =>
|
|
196
202
|
this.#request<PauseRunResponse>({
|
|
197
203
|
method: 'POST',
|
|
198
|
-
path: `/
|
|
204
|
+
path: `/runs/${encodeURIComponent(runId)}:pause`,
|
|
199
205
|
body,
|
|
200
206
|
headers: this.#mutationHeaders(opts),
|
|
201
207
|
}),
|
|
202
208
|
|
|
209
|
+
/** `POST /runs/{runId}:resume` — `409` when the run is not paused. */
|
|
203
210
|
resume: (
|
|
204
211
|
runId: string,
|
|
205
212
|
body: ResumeRunRequest = {},
|
|
@@ -207,19 +214,15 @@ export class OpenwopClient {
|
|
|
207
214
|
): Promise<ResumeRunResponse> =>
|
|
208
215
|
this.#request<ResumeRunResponse>({
|
|
209
216
|
method: 'POST',
|
|
210
|
-
path: `/
|
|
217
|
+
path: `/runs/${encodeURIComponent(runId)}:resume`,
|
|
211
218
|
body,
|
|
212
219
|
headers: this.#mutationHeaders(opts),
|
|
213
220
|
}),
|
|
214
221
|
|
|
215
222
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* request reaches the host; partial failures surface inside the
|
|
220
|
-
* array (each entry carries `ok: boolean` + optional `error`). Host-
|
|
221
|
-
* defined cap on `runIds[]` length (RECOMMENDED 100); over-cap
|
|
222
|
-
* returns `400 validation_error` with `details.maxRunIds`.
|
|
223
|
+
* `POST /runs:bulk-cancel` — `200 { results[] }` in request order even
|
|
224
|
+
* when every id failed; per-id authorization yields `ok: false` with
|
|
225
|
+
* `run_forbidden` in that entry, never a top-level `403` (runs.md §Cancel).
|
|
223
226
|
*/
|
|
224
227
|
bulkCancel: (
|
|
225
228
|
body: BulkCancelRunsRequest,
|
|
@@ -227,11 +230,12 @@ export class OpenwopClient {
|
|
|
227
230
|
): Promise<BulkCancelRunsResponse> =>
|
|
228
231
|
this.#request<BulkCancelRunsResponse>({
|
|
229
232
|
method: 'POST',
|
|
230
|
-
path: '/
|
|
233
|
+
path: '/runs:bulk-cancel',
|
|
231
234
|
body,
|
|
232
235
|
headers: this.#mutationHeaders(opts),
|
|
233
236
|
}),
|
|
234
237
|
|
|
238
|
+
/** `POST /runs/{runId}:fork` — `mode: replay | branch` (runs.md §Fork; replay.md). */
|
|
235
239
|
fork: (
|
|
236
240
|
runId: string,
|
|
237
241
|
body: ForkRunRequest,
|
|
@@ -239,16 +243,15 @@ export class OpenwopClient {
|
|
|
239
243
|
): Promise<ForkRunResponse> =>
|
|
240
244
|
this.#request<ForkRunResponse>({
|
|
241
245
|
method: 'POST',
|
|
242
|
-
path: `/
|
|
246
|
+
path: `/runs/${encodeURIComponent(runId)}:fork`,
|
|
243
247
|
body,
|
|
244
248
|
headers: this.#mutationHeaders(opts),
|
|
245
249
|
}),
|
|
246
250
|
|
|
247
251
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* `capabilities.feedback.supported`).
|
|
252
|
+
* `POST /runs/{runId}/annotations` — a live notification, never a run
|
|
253
|
+
* event. Throws on non-2xx (`501` when the host doesn't advertise
|
|
254
|
+
* `feedback`).
|
|
252
255
|
*/
|
|
253
256
|
createAnnotation: (
|
|
254
257
|
runId: string,
|
|
@@ -257,21 +260,21 @@ export class OpenwopClient {
|
|
|
257
260
|
): Promise<Annotation> =>
|
|
258
261
|
this.#request<Annotation>({
|
|
259
262
|
method: 'POST',
|
|
260
|
-
path: `/
|
|
263
|
+
path: `/runs/${encodeURIComponent(runId)}/annotations`,
|
|
261
264
|
body,
|
|
262
265
|
headers: this.#mutationHeaders(opts),
|
|
263
266
|
}),
|
|
264
267
|
|
|
265
268
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
+
* `GET /runs/{runId}/annotations` — returns `null` when the host doesn't
|
|
270
|
+
* advertise `feedback` (404/501), so callers can branch on capability
|
|
271
|
+
* discovery without try/catch.
|
|
269
272
|
*/
|
|
270
273
|
listAnnotations: async (runId: string): Promise<readonly Annotation[] | null> => {
|
|
271
274
|
try {
|
|
272
275
|
const res = await this.#request<{ annotations: Annotation[] }>({
|
|
273
276
|
method: 'GET',
|
|
274
|
-
path: `/
|
|
277
|
+
path: `/runs/${encodeURIComponent(runId)}/annotations`,
|
|
275
278
|
});
|
|
276
279
|
return res.annotations;
|
|
277
280
|
} catch (err) {
|
|
@@ -281,21 +284,17 @@ export class OpenwopClient {
|
|
|
281
284
|
},
|
|
282
285
|
|
|
283
286
|
/**
|
|
284
|
-
*
|
|
285
|
-
* composition chain
|
|
286
|
-
* `parent.wellKnownUrl` is set when the parent is on a different
|
|
287
|
-
* host, so callers walk the chain one hop at a time.
|
|
288
|
-
*
|
|
287
|
+
* `GET /runs/{runId}/ancestry` — the run's immediate parent in the
|
|
288
|
+
* cross-host composition chain; `parent: null` for top-level runs.
|
|
289
289
|
* Returns `null` when the host doesn't advertise
|
|
290
|
-
* `
|
|
291
|
-
* (the endpoint
|
|
292
|
-
* `spec/v1/multi-agent-execution.md` §"GET /v1/runs/{runId}/ancestry").
|
|
290
|
+
* `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported`
|
|
291
|
+
* (the endpoint 404s).
|
|
293
292
|
*/
|
|
294
293
|
ancestry: async (runId: string): Promise<RunAncestryResponse | null> => {
|
|
295
294
|
try {
|
|
296
295
|
return await this.#request<RunAncestryResponse>({
|
|
297
296
|
method: 'GET',
|
|
298
|
-
path: `/
|
|
297
|
+
path: `/runs/${encodeURIComponent(runId)}/ancestry`,
|
|
299
298
|
});
|
|
300
299
|
} catch (err) {
|
|
301
300
|
if (err instanceof WopError && err.status === 404) return null;
|
|
@@ -304,18 +303,15 @@ export class OpenwopClient {
|
|
|
304
303
|
},
|
|
305
304
|
|
|
306
305
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* `
|
|
310
|
-
* implement the endpoint (404 per `spec/v1/rest-endpoints.md`
|
|
311
|
-
* §`GET /v1/runs/{runId}:diff`). `divergedAtSeq` is null + `eventDiffs`
|
|
312
|
-
* empty when the two logs are identical.
|
|
306
|
+
* `GET /runs/{runId}:diff?against=` — deterministic, replay-aware diff of
|
|
307
|
+
* two runs; requires `runs:read` on both. OPTIONAL surface: `null` on
|
|
308
|
+
* `404`. Identical logs yield `divergedAtSeq: null` + empty `eventDiffs`.
|
|
313
309
|
*/
|
|
314
310
|
diff: async (runId: string, against: string): Promise<RunDiffResponse | null> => {
|
|
315
311
|
try {
|
|
316
312
|
return await this.#request<RunDiffResponse>({
|
|
317
313
|
method: 'GET',
|
|
318
|
-
path: `/
|
|
314
|
+
path: `/runs/${encodeURIComponent(runId)}:diff?against=${encodeURIComponent(against)}`,
|
|
319
315
|
});
|
|
320
316
|
} catch (err) {
|
|
321
317
|
if (err instanceof WopError && err.status === 404) return null;
|
|
@@ -324,18 +320,15 @@ export class OpenwopClient {
|
|
|
324
320
|
},
|
|
325
321
|
|
|
326
322
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
* redaction-safe safety findings. Returns `null` when the host doesn't
|
|
331
|
-
* advertise `capabilities.agents.evalSuite` or the run isn't an eval run
|
|
332
|
-
* (404). Throws `409` while the run is still in progress.
|
|
323
|
+
* `GET /runs/{runId}/eval-summary` — the `EvalSummary` for a terminal eval
|
|
324
|
+
* run. `null` on `404` (not an eval run, or `agents.evalSuite`
|
|
325
|
+
* unadvertised); throws `409` while the run is still in progress.
|
|
333
326
|
*/
|
|
334
327
|
evalSummary: async (runId: string): Promise<EvalSummary | null> => {
|
|
335
328
|
try {
|
|
336
329
|
return await this.#request<EvalSummary>({
|
|
337
330
|
method: 'GET',
|
|
338
|
-
path: `/
|
|
331
|
+
path: `/runs/${encodeURIComponent(runId)}/eval-summary`,
|
|
339
332
|
});
|
|
340
333
|
} catch (err) {
|
|
341
334
|
if (err instanceof WopError && err.status === 404) return null;
|
|
@@ -344,15 +337,46 @@ export class OpenwopClient {
|
|
|
344
337
|
},
|
|
345
338
|
|
|
346
339
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* on 404 (no such artifact, or the host doesn't store artifacts).
|
|
340
|
+
* `GET /runs/{runId}/artifacts/{artifactId}` — an implementation-defined
|
|
341
|
+
* JSON object. `null` on `404`.
|
|
350
342
|
*/
|
|
351
343
|
getArtifact: async (runId: string, artifactId: string): Promise<Record<string, unknown> | null> => {
|
|
352
344
|
try {
|
|
353
345
|
return await this.#request<Record<string, unknown>>({
|
|
354
346
|
method: 'GET',
|
|
355
|
-
path: `/
|
|
347
|
+
path: `/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(artifactId)}`,
|
|
348
|
+
});
|
|
349
|
+
} catch (err) {
|
|
350
|
+
if (err instanceof WopError && err.status === 404) return null;
|
|
351
|
+
throw err;
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* `GET /runs/{runId}/compensation` (RFC 0173 §C.1) — the compensation
|
|
357
|
+
* plan and attempts. Gated on `compensation`; `null` on `404`.
|
|
358
|
+
*/
|
|
359
|
+
compensation: async (runId: string): Promise<CompensationProjection | null> => {
|
|
360
|
+
try {
|
|
361
|
+
return await this.#request<CompensationProjection>({
|
|
362
|
+
method: 'GET',
|
|
363
|
+
path: `/runs/${encodeURIComponent(runId)}/compensation`,
|
|
364
|
+
});
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (err instanceof WopError && err.status === 404) return null;
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* `GET /runs/{runId}/effects` (RFC 0173 §C.2) — the Layer-2 effect ledger,
|
|
373
|
+
* business-identity keyed. Gated on `idempotency`; `null` on `404`.
|
|
374
|
+
*/
|
|
375
|
+
effects: async (runId: string): Promise<EffectLedgerProjection | null> => {
|
|
376
|
+
try {
|
|
377
|
+
return await this.#request<EffectLedgerProjection>({
|
|
378
|
+
method: 'GET',
|
|
379
|
+
path: `/runs/${encodeURIComponent(runId)}/effects`,
|
|
356
380
|
});
|
|
357
381
|
} catch (err) {
|
|
358
382
|
if (err instanceof WopError && err.status === 404) return null;
|
|
@@ -360,13 +384,20 @@ export class OpenwopClient {
|
|
|
360
384
|
}
|
|
361
385
|
},
|
|
362
386
|
|
|
387
|
+
/**
|
|
388
|
+
* `GET /runs/{runId}/events/poll` — the long-poll fallback (events.md
|
|
389
|
+
* §Poll). `afterSequence` returns events with `sequence > afterSequence`;
|
|
390
|
+
* omission means from the first event. The response's `lastSequence` is
|
|
391
|
+
* the highest sequence in the log (`-1` when empty) — feed it back as the
|
|
392
|
+
* next `afterSequence`.
|
|
393
|
+
*/
|
|
363
394
|
pollEvents: (
|
|
364
395
|
runId: string,
|
|
365
|
-
params: {
|
|
396
|
+
params: { afterSequence?: number; timeoutSeconds?: number } = {},
|
|
366
397
|
): Promise<PollEventsResponse> => {
|
|
367
398
|
const search = new URLSearchParams();
|
|
368
|
-
if (params.
|
|
369
|
-
search.set('
|
|
399
|
+
if (params.afterSequence !== undefined) {
|
|
400
|
+
search.set('afterSequence', String(params.afterSequence));
|
|
370
401
|
}
|
|
371
402
|
if (params.timeoutSeconds !== undefined) {
|
|
372
403
|
search.set('timeout', String(params.timeoutSeconds));
|
|
@@ -374,39 +405,57 @@ export class OpenwopClient {
|
|
|
374
405
|
const qs = search.toString();
|
|
375
406
|
return this.#request<PollEventsResponse>({
|
|
376
407
|
method: 'GET',
|
|
377
|
-
path: `/
|
|
408
|
+
path: `/runs/${encodeURIComponent(runId)}/events/poll${qs ? `?${qs}` : ''}`,
|
|
378
409
|
});
|
|
379
410
|
},
|
|
380
411
|
|
|
381
412
|
/**
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* loop or call `signal.abort()` to terminate early.
|
|
413
|
+
* `GET /runs/{runId}/events` — async-iterable SSE consumer. The
|
|
414
|
+
* connection auto-closes after the run's terminal event; break out of
|
|
415
|
+
* the loop or call `signal.abort()` to terminate early.
|
|
385
416
|
*/
|
|
386
417
|
events: (runId: string, opts: EventsStreamOptions = {}): AsyncGenerator<RunEventDoc, void, void> =>
|
|
387
|
-
streamEvents(
|
|
418
|
+
streamEvents(this.#streamContext(), runId, opts),
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// ── Host ─────────────────────────────────────────────────────────────
|
|
422
|
+
readonly host = {
|
|
423
|
+
/**
|
|
424
|
+
* `GET /host/effect-seams` (RFC 0173 §C) — every outbound effect seam
|
|
425
|
+
* replay suppression covers. Throws `401` when unauthenticated.
|
|
426
|
+
*/
|
|
427
|
+
effectSeams: (): Promise<EffectSeamManifest> =>
|
|
428
|
+
this.#request<EffectSeamManifest>({ method: 'GET', path: '/host/effect-seams' }),
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* `GET /host/events` — the `hostEvents` channel (heartbeat messages) as
|
|
432
|
+
* SSE; content-free of run data. A host MAY declare another address
|
|
433
|
+
* under `heartbeat.deliveryChannel` — pass it as `opts.path`.
|
|
434
|
+
*/
|
|
435
|
+
events: (opts: HostEventsStreamOptions = {}): AsyncGenerator<HostEventDoc, void, void> =>
|
|
436
|
+
streamHostEvents(this.#streamContext(), opts),
|
|
388
437
|
};
|
|
389
438
|
|
|
390
439
|
// ── Manifest-agent inventory (RFC 0072 §A) ───────────────────────────
|
|
391
|
-
// Read-only. Gated on `
|
|
392
|
-
//
|
|
393
|
-
// Dispatch is not here: a manifest agent runs as a `runs.create` whose
|
|
394
|
-
// workflow node pins it via `WorkflowNode.agent` (RFC 0072 §B).
|
|
440
|
+
// Read-only. Gated on `agents`; the methods return `null` when the host
|
|
441
|
+
// doesn't advertise it (the endpoints 404).
|
|
395
442
|
readonly agents = {
|
|
443
|
+
/** `GET /agents` */
|
|
396
444
|
list: async (): Promise<AgentInventoryResponse | null> => {
|
|
397
445
|
try {
|
|
398
|
-
return await this.#request<AgentInventoryResponse>({ method: 'GET', path: '/
|
|
446
|
+
return await this.#request<AgentInventoryResponse>({ method: 'GET', path: '/agents' });
|
|
399
447
|
} catch (err) {
|
|
400
448
|
if (err instanceof WopError && err.status === 404) return null;
|
|
401
449
|
throw err;
|
|
402
450
|
}
|
|
403
451
|
},
|
|
404
452
|
|
|
453
|
+
/** `GET /agents/{agentId}` */
|
|
405
454
|
get: async (agentId: string): Promise<AgentInventoryEntry | null> => {
|
|
406
455
|
try {
|
|
407
456
|
return await this.#request<AgentInventoryEntry>({
|
|
408
457
|
method: 'GET',
|
|
409
|
-
path: `/
|
|
458
|
+
path: `/agents/${encodeURIComponent(agentId)}`,
|
|
410
459
|
});
|
|
411
460
|
} catch (err) {
|
|
412
461
|
if (err instanceof WopError && err.status === 404) return null;
|
|
@@ -414,17 +463,12 @@ export class OpenwopClient {
|
|
|
414
463
|
}
|
|
415
464
|
},
|
|
416
465
|
|
|
417
|
-
/**
|
|
418
|
-
* RFC 0082 §C/§E — list a manifest agent's deployment records (per-(agentId,
|
|
419
|
-
* version) lifecycle state + channels + canary + rollback pointer). Returns
|
|
420
|
-
* `null` when the host doesn't advertise `capabilities.agents.deployment`
|
|
421
|
-
* (the endpoint 404s).
|
|
422
|
-
*/
|
|
466
|
+
/** `GET /agents/{agentId}/deployments` (RFC 0082 §C/§E) — `null` when unadvertised. */
|
|
423
467
|
listDeployments: async (agentId: string): Promise<readonly AgentDeployment[] | null> => {
|
|
424
468
|
try {
|
|
425
469
|
return await this.#request<AgentDeployment[]>({
|
|
426
470
|
method: 'GET',
|
|
427
|
-
path: `/
|
|
471
|
+
path: `/agents/${encodeURIComponent(agentId)}/deployments`,
|
|
428
472
|
});
|
|
429
473
|
} catch (err) {
|
|
430
474
|
if (err instanceof WopError && err.status === 404) return null;
|
|
@@ -432,14 +476,7 @@ export class OpenwopClient {
|
|
|
432
476
|
}
|
|
433
477
|
},
|
|
434
478
|
|
|
435
|
-
/**
|
|
436
|
-
* RFC 0082 §E — request a deployment state transition (promote / pause /
|
|
437
|
-
* deprecate / rollback / adjust-canary). The host authorizes fail-closed
|
|
438
|
-
* against the RFC 0049 `deploy:*` scope, runs any RFC 0051 approvalGate, and
|
|
439
|
-
* enforces RFC 0081 `requiredEval` before emitting `deployment.promoted`.
|
|
440
|
-
* Returns the updated deployment record. Throws on non-2xx (`403` fail-closed
|
|
441
|
-
* / `eval_gate_unmet`; `400` `no_active_deployment` / unsupported state).
|
|
442
|
-
*/
|
|
479
|
+
/** `POST /agents/{agentId}/deployments` (RFC 0082 §E) — a deployment state transition. */
|
|
443
480
|
transitionDeployment: (
|
|
444
481
|
agentId: string,
|
|
445
482
|
body: AgentDeploymentTransition,
|
|
@@ -447,34 +484,27 @@ export class OpenwopClient {
|
|
|
447
484
|
): Promise<AgentDeployment> =>
|
|
448
485
|
this.#request<AgentDeployment>({
|
|
449
486
|
method: 'POST',
|
|
450
|
-
path: `/
|
|
487
|
+
path: `/agents/${encodeURIComponent(agentId)}/deployments`,
|
|
451
488
|
body,
|
|
452
489
|
headers: this.#mutationHeaders(opts),
|
|
453
490
|
}),
|
|
454
491
|
|
|
455
|
-
/**
|
|
456
|
-
* RFC 0086 §B — list the standing agent roster (named instances + their
|
|
457
|
-
* workflow portfolios) visible to the caller. Returns `null` when the host
|
|
458
|
-
* doesn't advertise `capabilities.agents.roster` (the endpoint 404s).
|
|
459
|
-
*/
|
|
492
|
+
/** `GET /agents/roster` (RFC 0086 §B) — `null` when unadvertised. */
|
|
460
493
|
listRoster: async (): Promise<AgentRosterResponse | null> => {
|
|
461
494
|
try {
|
|
462
|
-
return await this.#request<AgentRosterResponse>({ method: 'GET', path: '/
|
|
495
|
+
return await this.#request<AgentRosterResponse>({ method: 'GET', path: '/agents/roster' });
|
|
463
496
|
} catch (err) {
|
|
464
497
|
if (err instanceof WopError && err.status === 404) return null;
|
|
465
498
|
throw err;
|
|
466
499
|
}
|
|
467
500
|
},
|
|
468
501
|
|
|
469
|
-
/**
|
|
470
|
-
* RFC 0086 §B — return one standing roster entry. Returns `null` on 404
|
|
471
|
-
* (no such entry, cross-tenant, or the capability is unadvertised).
|
|
472
|
-
*/
|
|
502
|
+
/** `GET /agents/roster/{rosterId}` (RFC 0086 §B) — `null` on `404`. */
|
|
473
503
|
getRosterEntry: async (rosterId: string): Promise<AgentRosterEntry | null> => {
|
|
474
504
|
try {
|
|
475
505
|
return await this.#request<AgentRosterEntry>({
|
|
476
506
|
method: 'GET',
|
|
477
|
-
path: `/
|
|
507
|
+
path: `/agents/roster/${encodeURIComponent(rosterId)}`,
|
|
478
508
|
});
|
|
479
509
|
} catch (err) {
|
|
480
510
|
if (err instanceof WopError && err.status === 404) return null;
|
|
@@ -482,26 +512,17 @@ export class OpenwopClient {
|
|
|
482
512
|
}
|
|
483
513
|
},
|
|
484
514
|
|
|
485
|
-
/**
|
|
486
|
-
* RFC 0087 §C — return the caller's agent org-chart (departments + roles +
|
|
487
|
-
* `reportsTo` over roster members; descriptive — confers no authority).
|
|
488
|
-
* Returns `null` when the host doesn't advertise `capabilities.agents.orgChart`.
|
|
489
|
-
*/
|
|
515
|
+
/** `GET /agents/org-chart` (RFC 0087 §C) — `null` when unadvertised. */
|
|
490
516
|
getOrgChart: async (): Promise<AgentOrgChart | null> => {
|
|
491
517
|
try {
|
|
492
|
-
return await this.#request<AgentOrgChart>({ method: 'GET', path: '/
|
|
518
|
+
return await this.#request<AgentOrgChart>({ method: 'GET', path: '/agents/org-chart' });
|
|
493
519
|
} catch (err) {
|
|
494
520
|
if (err instanceof WopError && err.status === 404) return null;
|
|
495
521
|
throw err;
|
|
496
522
|
}
|
|
497
523
|
},
|
|
498
524
|
|
|
499
|
-
/**
|
|
500
|
-
* RFC 0087 §D — one department's subtree + responsibility roll-up (the
|
|
501
|
-
* union of its members' RFC 0086 portfolios). `recursive: false` scopes the
|
|
502
|
-
* roll-up to direct members. Returns `null` on 404 (unknown/cross-tenant
|
|
503
|
-
* department, or the capability is unadvertised).
|
|
504
|
-
*/
|
|
525
|
+
/** `GET /agents/org-chart/{departmentId}` (RFC 0087 §D) — `null` on `404`. */
|
|
505
526
|
getOrgChartDepartment: async (
|
|
506
527
|
departmentId: string,
|
|
507
528
|
opts: { recursive?: boolean } = {},
|
|
@@ -510,7 +531,7 @@ export class OpenwopClient {
|
|
|
510
531
|
try {
|
|
511
532
|
return await this.#request<OrgChartResponsibilityView>({
|
|
512
533
|
method: 'GET',
|
|
513
|
-
path: `/
|
|
534
|
+
path: `/agents/org-chart/${encodeURIComponent(departmentId)}${qs}`,
|
|
514
535
|
});
|
|
515
536
|
} catch (err) {
|
|
516
537
|
if (err instanceof WopError && err.status === 404) return null;
|
|
@@ -519,35 +540,24 @@ export class OpenwopClient {
|
|
|
519
540
|
},
|
|
520
541
|
};
|
|
521
542
|
|
|
522
|
-
// RFC 0078 — portable tool catalog (
|
|
523
|
-
// projects every node-pack / workflow / mcp / connector / host-extension
|
|
524
|
-
// tool visible to the caller onto a uniform `ToolDescriptor`.
|
|
543
|
+
// ── RFC 0078 — portable tool catalog (gated on `toolCatalog`) ─────────
|
|
525
544
|
readonly tools = {
|
|
526
|
-
/**
|
|
527
|
-
* RFC 0078 §B — list the portable `ToolDescriptor`s visible to the caller.
|
|
528
|
-
* Returns `null` when the host doesn't advertise `capabilities.toolCatalog`
|
|
529
|
-
* (the endpoint 404s), so callers can branch on capability discovery.
|
|
530
|
-
*/
|
|
545
|
+
/** `GET /tools` — `null` when the host doesn't advertise `toolCatalog`. */
|
|
531
546
|
list: async (): Promise<readonly ToolDescriptor[] | null> => {
|
|
532
547
|
try {
|
|
533
|
-
return await this.#request<ToolDescriptor[]>({ method: 'GET', path: '/
|
|
548
|
+
return await this.#request<ToolDescriptor[]>({ method: 'GET', path: '/tools' });
|
|
534
549
|
} catch (err) {
|
|
535
550
|
if (err instanceof WopError && err.status === 404) return null;
|
|
536
551
|
throw err;
|
|
537
552
|
}
|
|
538
553
|
},
|
|
539
554
|
|
|
540
|
-
/**
|
|
541
|
-
* RFC 0112 — list the `CompactToolDescriptor`s via `GET /v1/tools?view=compact`
|
|
542
|
-
* when the host advertises `capabilities.toolCatalog.compactView`. Unwraps the
|
|
543
|
-
* `{ tools: CompactToolDescriptor[] }` envelope. Returns `null` when the host
|
|
544
|
-
* doesn't advertise the catalog (the endpoint 404s).
|
|
545
|
-
*/
|
|
555
|
+
/** `GET /tools?view=compact` (RFC 0112) — unwraps `{ tools }`; `null` when unadvertised. */
|
|
546
556
|
listCompact: async (): Promise<readonly CompactToolDescriptor[] | null> => {
|
|
547
557
|
try {
|
|
548
558
|
const res = await this.#request<{ tools?: readonly CompactToolDescriptor[] }>({
|
|
549
559
|
method: 'GET',
|
|
550
|
-
path: '/
|
|
560
|
+
path: '/tools?view=compact',
|
|
551
561
|
});
|
|
552
562
|
return res.tools ?? [];
|
|
553
563
|
} catch (err) {
|
|
@@ -556,12 +566,7 @@ export class OpenwopClient {
|
|
|
556
566
|
}
|
|
557
567
|
},
|
|
558
568
|
|
|
559
|
-
/**
|
|
560
|
-
* RFC 0078 §B — return one `ToolDescriptor` by its stable `toolId`. Returns
|
|
561
|
-
* `null` on 404 (no such tool, or the capability is unadvertised). Pass
|
|
562
|
-
* `{ view: 'compact' }` (RFC 0112) to receive the `CompactToolDescriptor`
|
|
563
|
-
* projection instead.
|
|
564
|
-
*/
|
|
569
|
+
/** `GET /tools/{toolId}` — `null` on `404`; `{ view: 'compact' }` for the compact projection. */
|
|
565
570
|
get: async (
|
|
566
571
|
toolId: string,
|
|
567
572
|
opts: { readonly view?: 'standard' | 'compact' } = {},
|
|
@@ -570,7 +575,7 @@ export class OpenwopClient {
|
|
|
570
575
|
try {
|
|
571
576
|
return await this.#request<ToolDescriptor | CompactToolDescriptor>({
|
|
572
577
|
method: 'GET',
|
|
573
|
-
path: `/
|
|
578
|
+
path: `/tools/${encodeURIComponent(toolId)}${query}`,
|
|
574
579
|
});
|
|
575
580
|
} catch (err) {
|
|
576
581
|
if (err instanceof WopError && err.status === 404) return null;
|
|
@@ -579,61 +584,9 @@ export class OpenwopClient {
|
|
|
579
584
|
},
|
|
580
585
|
};
|
|
581
586
|
|
|
582
|
-
// ── User-authored agents (sample-extension; non-normative) ───────────
|
|
583
|
-
// Backs the workflow-engine sample app's Agents tab. Pack-installed
|
|
584
|
-
// agents come through the `.agents` inventory above (RFC 0072 §A).
|
|
585
|
-
// These methods wrap the `POST/DELETE /v1/host/sample/agents` +
|
|
586
|
-
// `GET/POST /v1/host/sample/registry/agent-packs` host extensions.
|
|
587
|
-
// Returns `null` on 404 (capability absent — matches the `.agents`
|
|
588
|
-
// surface pattern); throws on other failures.
|
|
589
|
-
readonly userAgents = {
|
|
590
|
-
create: async (body: CreateUserAgentRequest, opts: MutationOptions = {}): Promise<UserAgentRecord> => {
|
|
591
|
-
return await this.#request<UserAgentRecord>({
|
|
592
|
-
method: 'POST',
|
|
593
|
-
path: '/v1/host/sample/agents',
|
|
594
|
-
body,
|
|
595
|
-
...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
|
|
596
|
-
});
|
|
597
|
-
},
|
|
598
|
-
|
|
599
|
-
delete: async (agentId: string): Promise<boolean> => {
|
|
600
|
-
try {
|
|
601
|
-
await this.#request<void>({
|
|
602
|
-
method: 'DELETE',
|
|
603
|
-
path: `/v1/host/sample/agents/${encodeURIComponent(agentId)}`,
|
|
604
|
-
});
|
|
605
|
-
return true;
|
|
606
|
-
} catch (err) {
|
|
607
|
-
if (err instanceof WopError && err.status === 404) return false;
|
|
608
|
-
throw err;
|
|
609
|
-
}
|
|
610
|
-
},
|
|
611
|
-
|
|
612
|
-
listAvailablePacks: async (): Promise<AgentPackRegistryResponse | null> => {
|
|
613
|
-
try {
|
|
614
|
-
return await this.#request<AgentPackRegistryResponse>({
|
|
615
|
-
method: 'GET',
|
|
616
|
-
path: '/v1/host/sample/registry/agent-packs',
|
|
617
|
-
});
|
|
618
|
-
} catch (err) {
|
|
619
|
-
if (err instanceof WopError && err.status === 404) return null;
|
|
620
|
-
throw err;
|
|
621
|
-
}
|
|
622
|
-
},
|
|
623
|
-
|
|
624
|
-
installPack: async (
|
|
625
|
-
body: InstallAgentPackRequest,
|
|
626
|
-
): Promise<InstallAgentPackResponse> => {
|
|
627
|
-
return await this.#request<InstallAgentPackResponse>({
|
|
628
|
-
method: 'POST',
|
|
629
|
-
path: '/v1/host/sample/registry/agent-packs/install',
|
|
630
|
-
body,
|
|
631
|
-
});
|
|
632
|
-
},
|
|
633
|
-
};
|
|
634
|
-
|
|
635
587
|
// ── HITL interrupts (run-scoped + signed-token) ──────────────────────
|
|
636
588
|
readonly interrupts = {
|
|
589
|
+
/** `POST /runs/{runId}/interrupts/{nodeId}` */
|
|
637
590
|
resolveByRun: (
|
|
638
591
|
runId: string,
|
|
639
592
|
nodeId: string,
|
|
@@ -642,32 +595,26 @@ export class OpenwopClient {
|
|
|
642
595
|
): Promise<ResolveInterruptResponse> =>
|
|
643
596
|
this.#request<ResolveInterruptResponse>({
|
|
644
597
|
method: 'POST',
|
|
645
|
-
path: `/
|
|
598
|
+
path: `/runs/${encodeURIComponent(runId)}/interrupts/${encodeURIComponent(nodeId)}`,
|
|
646
599
|
body,
|
|
647
600
|
headers: this.#mutationHeaders(opts),
|
|
648
601
|
}),
|
|
649
602
|
|
|
650
603
|
/**
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
* required (signed-token endpoints intentionally bypass bearer
|
|
655
|
-
* auth so external systems can resolve without openwop credentials).
|
|
604
|
+
* `GET /interrupts/{token}` — inspect via signed token. The token is the
|
|
605
|
+
* auth; no API key is sent (signed-token endpoints bypass bearer auth so
|
|
606
|
+
* external systems can resolve without openwop credentials).
|
|
656
607
|
*/
|
|
657
608
|
inspectByToken: (token: string): Promise<InterruptByTokenInspection> =>
|
|
658
609
|
this.#request<InterruptByTokenInspection>(
|
|
659
610
|
{
|
|
660
611
|
method: 'GET',
|
|
661
|
-
path: `/
|
|
612
|
+
path: `/interrupts/${encodeURIComponent(token)}`,
|
|
662
613
|
},
|
|
663
614
|
false, // unauthenticated (token IS the auth)
|
|
664
615
|
),
|
|
665
616
|
|
|
666
|
-
/**
|
|
667
|
-
* Resolve an interrupt via signed token — used by external
|
|
668
|
-
* systems (calendar webhooks, payment confirmations) that the
|
|
669
|
-
* engine handed a callback URL at suspension time.
|
|
670
|
-
*/
|
|
617
|
+
/** `POST /interrupts/{token}` — resolve via signed token. */
|
|
671
618
|
resolveByToken: (
|
|
672
619
|
token: string,
|
|
673
620
|
body: ResolveInterruptRequest,
|
|
@@ -676,7 +623,7 @@ export class OpenwopClient {
|
|
|
676
623
|
this.#request<ResolveInterruptByTokenResponse>(
|
|
677
624
|
{
|
|
678
625
|
method: 'POST',
|
|
679
|
-
path: `/
|
|
626
|
+
path: `/interrupts/${encodeURIComponent(token)}`,
|
|
680
627
|
body,
|
|
681
628
|
headers: this.#mutationHeaders(opts),
|
|
682
629
|
},
|
|
@@ -684,14 +631,13 @@ export class OpenwopClient {
|
|
|
684
631
|
),
|
|
685
632
|
};
|
|
686
633
|
|
|
687
|
-
// ── Webhook subscriptions (
|
|
634
|
+
// ── Webhook subscriptions (webhooks.md; gated on `webhooks`) ─────────
|
|
688
635
|
readonly webhooks = {
|
|
689
636
|
/**
|
|
690
|
-
*
|
|
691
|
-
* HMAC-SHA256 over
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
* server-side for verification; the host cannot recover it.
|
|
637
|
+
* `POST /webhooks` — `{ url, events[], secret?, tags? }`; `url` MUST be
|
|
638
|
+
* `https://`. Deliveries are signed HMAC-SHA256 over
|
|
639
|
+
* `${timestamp}.${rawBody}` under the `OpenWOP-*` header family (verify
|
|
640
|
+
* with `@openwop/openwop/webhooks`).
|
|
695
641
|
*/
|
|
696
642
|
register: (
|
|
697
643
|
body: RegisterWebhookRequest,
|
|
@@ -699,45 +645,23 @@ export class OpenwopClient {
|
|
|
699
645
|
): Promise<RegisterWebhookResponse> =>
|
|
700
646
|
this.#request<RegisterWebhookResponse>({
|
|
701
647
|
method: 'POST',
|
|
702
|
-
path: '/
|
|
648
|
+
path: '/webhooks',
|
|
703
649
|
body,
|
|
704
650
|
headers: this.#mutationHeaders(opts),
|
|
705
651
|
}),
|
|
706
652
|
|
|
707
|
-
/**
|
|
708
|
-
|
|
709
|
-
* throws `WopError` with `subscription_not_found` on unknown
|
|
710
|
-
* subscriptionId.
|
|
711
|
-
*/
|
|
712
|
-
unregister: async (subscriptionId: string): Promise<void> => {
|
|
653
|
+
/** `DELETE /webhooks/{webhookId}` — `204`; throws `404` when unknown, `403` outside the tenant. */
|
|
654
|
+
unregister: async (webhookId: string): Promise<void> => {
|
|
713
655
|
await this.#request<unknown>({
|
|
714
656
|
method: 'DELETE',
|
|
715
|
-
path: `/
|
|
657
|
+
path: `/webhooks/${encodeURIComponent(webhookId)}`,
|
|
716
658
|
});
|
|
717
659
|
},
|
|
718
660
|
};
|
|
719
661
|
|
|
720
|
-
// ── Prompt library (RFC 0028; gated on
|
|
721
|
-
//
|
|
722
|
-
// Read endpoints (list, get, render) gate on
|
|
723
|
-
// `capabilities.prompts.endpointsSupported: true`. Mutating endpoints
|
|
724
|
-
// (create, update, delete) additionally require
|
|
725
|
-
// `capabilities.prompts.mutableLibrary: true`. Hosts that don't advertise
|
|
726
|
-
// the relevant capability return `501 capability_not_provided`; the SDK
|
|
727
|
-
// surfaces that as a `WopError`. Clients SHOULD pre-flight via
|
|
728
|
-
// `getCapabilities()` before calling.
|
|
729
|
-
//
|
|
730
|
-
// NOTE: `capabilities.prompts.supported: true` (without
|
|
731
|
-
// `endpointsSupported: true`) ONLY gates node-execution PromptRef
|
|
732
|
-
// resolution per RFC 0027 Phase A; it does NOT imply these endpoints are
|
|
733
|
-
// available. See spec/v1/prompts.md §"Capability advertisement" for the
|
|
734
|
-
// two-axis gating split.
|
|
662
|
+
// ── Prompt library (RFC 0028; gated on `prompts`) ────────────────────
|
|
735
663
|
readonly prompts = {
|
|
736
|
-
/**
|
|
737
|
-
* List prompt templates available to the caller per RFC 0028 §A
|
|
738
|
-
* (operationId `listPromptTemplates`). Supports kind / tag / modelClass
|
|
739
|
-
* / source filters + opaque cursor pagination.
|
|
740
|
-
*/
|
|
664
|
+
/** `GET /prompts` — kind / tag / modelClass / source filters + cursor pagination. */
|
|
741
665
|
list: (req: ListPromptsRequest = {}): Promise<ListPromptsResponse> => {
|
|
742
666
|
const search = new URLSearchParams();
|
|
743
667
|
if (req.kind) search.set('kind', req.kind);
|
|
@@ -749,16 +673,11 @@ export class OpenwopClient {
|
|
|
749
673
|
const query = search.toString();
|
|
750
674
|
return this.#request<ListPromptsResponse>({
|
|
751
675
|
method: 'GET',
|
|
752
|
-
path: `/
|
|
676
|
+
path: `/prompts${query ? `?${query}` : ''}`,
|
|
753
677
|
});
|
|
754
678
|
},
|
|
755
679
|
|
|
756
|
-
/**
|
|
757
|
-
* Fetch a single PromptTemplate by id per RFC 0028 §A
|
|
758
|
-
* (operationId `getPromptTemplate`). Optionally pin a SemVer
|
|
759
|
-
* `version`; supply `libraryId` to disambiguate when multiple installed
|
|
760
|
-
* packs ship the same templateId.
|
|
761
|
-
*/
|
|
680
|
+
/** `GET /prompts/{templateId}` — `null` on `404`; a `400 prompt_ref_ambiguous` still throws. */
|
|
762
681
|
get: async (req: GetPromptRequest): Promise<PromptTemplate | null> => {
|
|
763
682
|
const search = new URLSearchParams();
|
|
764
683
|
if (req.version) search.set('version', req.version);
|
|
@@ -767,58 +686,34 @@ export class OpenwopClient {
|
|
|
767
686
|
try {
|
|
768
687
|
return await this.#request<PromptTemplate>({
|
|
769
688
|
method: 'GET',
|
|
770
|
-
path: `/
|
|
689
|
+
path: `/prompts/${encodeURIComponent(req.templateId)}${query ? `?${query}` : ''}`,
|
|
771
690
|
});
|
|
772
691
|
} catch (err) {
|
|
773
|
-
// Return null on 404 (no such template), consistent with the other
|
|
774
|
-
// get-by-id methods (`agents.get`, `tools.get`, …) and the Python/Go
|
|
775
|
-
// SDKs; a `400 prompt_ref_ambiguous` and other errors still throw so
|
|
776
|
-
// callers can distinguish "not found" from "ambiguous reference".
|
|
777
692
|
if (err instanceof WopError && err.status === 404) return null;
|
|
778
693
|
throw err;
|
|
779
694
|
}
|
|
780
695
|
},
|
|
781
696
|
|
|
782
|
-
/**
|
|
783
|
-
* Render a PromptTemplate with supplied variable bindings per RFC 0028
|
|
784
|
-
* §A (operationId `renderPromptTemplate`). Returns composed body +
|
|
785
|
-
* sha256 hash + per-variable hashes. The deterministic-hash invariant
|
|
786
|
-
* (RFC 0028 §A) requires the `hash` to match what a matching
|
|
787
|
-
* `prompt.composed` event would carry at dispatch time. Does NOT
|
|
788
|
-
* dispatch an LLM call. Secret-source variable values MUST be supplied
|
|
789
|
-
* as `[REDACTED:<credentialRef>]` markers per
|
|
790
|
-
* SECURITY/threat-model-secret-leakage.md §SR-1.
|
|
791
|
-
*/
|
|
697
|
+
/** `POST /prompts:render` — composed body + sha256 hash; does NOT dispatch an LLM call. */
|
|
792
698
|
render: (req: RenderPromptRequest): Promise<RenderPromptResponse> => {
|
|
793
699
|
return this.#request<RenderPromptResponse>({
|
|
794
700
|
method: 'POST',
|
|
795
|
-
path: '/
|
|
701
|
+
path: '/prompts:render',
|
|
796
702
|
body: req,
|
|
797
703
|
});
|
|
798
704
|
},
|
|
799
705
|
|
|
800
|
-
/**
|
|
801
|
-
* Create a new user-source PromptTemplate per RFC 0028 §A
|
|
802
|
-
* (operationId `createPromptTemplate`). Mutating endpoint —
|
|
803
|
-
* requires `capabilities.prompts.mutableLibrary: true`. Supports
|
|
804
|
-
* `Idempotency-Key` per the standard `MutationOptions` pattern.
|
|
805
|
-
*/
|
|
706
|
+
/** `POST /prompts` — requires `prompts.mutableLibrary`. */
|
|
806
707
|
create: (template: PromptTemplate, opts: MutationOptions = {}): Promise<void> => {
|
|
807
708
|
return this.#request<void>({
|
|
808
709
|
method: 'POST',
|
|
809
|
-
path: '/
|
|
710
|
+
path: '/prompts',
|
|
810
711
|
body: template,
|
|
811
712
|
headers: this.#mutationHeaders(opts),
|
|
812
713
|
});
|
|
813
714
|
},
|
|
814
715
|
|
|
815
|
-
/**
|
|
816
|
-
* Replace an existing user-source PromptTemplate per RFC 0028 §A
|
|
817
|
-
* (operationId `updatePromptTemplate`). Submitted SemVer MUST be
|
|
818
|
-
* strictly greater than stored. Mutating endpoint — requires
|
|
819
|
-
* `capabilities.prompts.mutableLibrary: true`. Pack-sourced and
|
|
820
|
-
* host-built-in templates are read-only (host returns 403).
|
|
821
|
-
*/
|
|
716
|
+
/** `PUT /prompts/{templateId}` — the submitted SemVer MUST be strictly greater than stored. */
|
|
822
717
|
update: (
|
|
823
718
|
templateId: string,
|
|
824
719
|
template: PromptTemplate,
|
|
@@ -826,38 +721,27 @@ export class OpenwopClient {
|
|
|
826
721
|
): Promise<PromptTemplate> => {
|
|
827
722
|
return this.#request<PromptTemplate>({
|
|
828
723
|
method: 'PUT',
|
|
829
|
-
path: `/
|
|
724
|
+
path: `/prompts/${encodeURIComponent(templateId)}`,
|
|
830
725
|
body: template,
|
|
831
726
|
headers: this.#mutationHeaders(opts),
|
|
832
727
|
});
|
|
833
728
|
},
|
|
834
729
|
|
|
835
|
-
/**
|
|
836
|
-
* Delete a user-source PromptTemplate per RFC 0028 §A
|
|
837
|
-
* (operationId `deletePromptTemplate`). Mutating endpoint —
|
|
838
|
-
* requires `capabilities.prompts.mutableLibrary: true`. Pack-sourced
|
|
839
|
-
* and host-built-in templates are read-only (host returns 403).
|
|
840
|
-
*/
|
|
730
|
+
/** `DELETE /prompts/{templateId}` */
|
|
841
731
|
delete: (templateId: string): Promise<void> => {
|
|
842
732
|
return this.#request<void>({
|
|
843
733
|
method: 'DELETE',
|
|
844
|
-
path: `/
|
|
734
|
+
path: `/prompts/${encodeURIComponent(templateId)}`,
|
|
845
735
|
});
|
|
846
736
|
},
|
|
847
737
|
};
|
|
848
738
|
|
|
849
|
-
// ── Audit-log integrity
|
|
739
|
+
// ── Audit-log integrity ──────────────────────────────────────────────
|
|
850
740
|
readonly audit = {
|
|
851
741
|
/**
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
* verdict + signed checkpoints + any detected anomalies.
|
|
856
|
-
*
|
|
857
|
-
* Hosts that do NOT advertise the profile return `404` and throw
|
|
858
|
-
* a `WopError`. Clients SHOULD pre-flight via
|
|
859
|
-
* `client.getCapabilities()` (or directly inspect
|
|
860
|
-
* `capabilities.auth.profiles[]`) before calling.
|
|
742
|
+
* `GET /audit/verify?fromSeq&toSeq` — chain-validity verdict + signed
|
|
743
|
+
* checkpoints + anomalies. Requires the `audit:read` scope; a host that
|
|
744
|
+
* does not serve the profile answers `404`.
|
|
861
745
|
*/
|
|
862
746
|
verify: (fromSeq: number, toSeq: number): Promise<AuditVerifyResult> => {
|
|
863
747
|
const search = new URLSearchParams();
|
|
@@ -865,20 +749,19 @@ export class OpenwopClient {
|
|
|
865
749
|
search.set('toSeq', String(toSeq));
|
|
866
750
|
return this.#request<AuditVerifyResult>({
|
|
867
751
|
method: 'GET',
|
|
868
|
-
path: `/
|
|
752
|
+
path: `/audit/verify?${search.toString()}`,
|
|
869
753
|
});
|
|
870
754
|
},
|
|
871
755
|
};
|
|
872
756
|
|
|
873
|
-
// ── RFC 0103 Localized content surface (gated on
|
|
757
|
+
// ── RFC 0103 Localized content surface (gated on `content`) ──────────
|
|
874
758
|
readonly content = {
|
|
875
|
-
/** `GET /
|
|
876
|
-
* host doesn't advertise `capabilities.content` (501). */
|
|
759
|
+
/** `GET /content/pages` — `null` when the host doesn't advertise `content` (501). */
|
|
877
760
|
listPages: async (): Promise<readonly LocalizedContentPage[] | null> => {
|
|
878
761
|
try {
|
|
879
762
|
return await this.#request<readonly LocalizedContentPage[]>({
|
|
880
763
|
method: 'GET',
|
|
881
|
-
path: '/
|
|
764
|
+
path: '/content/pages',
|
|
882
765
|
});
|
|
883
766
|
} catch (err) {
|
|
884
767
|
if (err instanceof WopError && err.status === 501) return null;
|
|
@@ -886,10 +769,7 @@ export class OpenwopClient {
|
|
|
886
769
|
}
|
|
887
770
|
},
|
|
888
771
|
|
|
889
|
-
/** `GET /
|
|
890
|
-
* sections. `acceptLanguage` rides the `Accept-Language` header (the Stable
|
|
891
|
-
* `i18n.md` negotiation; no `?locale=`). Returns `null` on `404`
|
|
892
|
-
* (no such published page) or `501` (uncapable). */
|
|
772
|
+
/** `GET /content/pages/{slug}` — `acceptLanguage` rides `Accept-Language`; `null` on `404`/`501`. */
|
|
893
773
|
getPage: async (
|
|
894
774
|
slug: string,
|
|
895
775
|
acceptLanguage?: string,
|
|
@@ -897,7 +777,7 @@ export class OpenwopClient {
|
|
|
897
777
|
try {
|
|
898
778
|
return await this.#request<LocalizedContentPageResponse>({
|
|
899
779
|
method: 'GET',
|
|
900
|
-
path: `/
|
|
780
|
+
path: `/content/pages/${encodeURIComponent(slug)}`,
|
|
901
781
|
...(acceptLanguage
|
|
902
782
|
? { headers: { 'Accept-Language': acceptLanguage } }
|
|
903
783
|
: {}),
|
|
@@ -909,17 +789,15 @@ export class OpenwopClient {
|
|
|
909
789
|
}
|
|
910
790
|
},
|
|
911
791
|
|
|
912
|
-
/** `POST /
|
|
913
|
-
* typed `WopError` on `400`/`401`/`403`. */
|
|
792
|
+
/** `POST /content/pages` (admin). */
|
|
914
793
|
createPage: (body: LocalizedContentPage): Promise<LocalizedContentPage> =>
|
|
915
794
|
this.#request<LocalizedContentPage>({
|
|
916
795
|
method: 'POST',
|
|
917
|
-
path: '/
|
|
796
|
+
path: '/content/pages',
|
|
918
797
|
body,
|
|
919
798
|
}),
|
|
920
799
|
|
|
921
|
-
/** `PUT /
|
|
922
|
-
* section's field overlay for a locale (admin). */
|
|
800
|
+
/** `PUT /content/pages/{pageId}/sections/{sectionId}` (admin). */
|
|
923
801
|
putSection: (
|
|
924
802
|
pageId: string,
|
|
925
803
|
sectionId: string,
|
|
@@ -927,17 +805,16 @@ export class OpenwopClient {
|
|
|
927
805
|
): Promise<LocalizedContentSection> =>
|
|
928
806
|
this.#request<LocalizedContentSection>({
|
|
929
807
|
method: 'PUT',
|
|
930
|
-
path: `/
|
|
808
|
+
path: `/content/pages/${encodeURIComponent(pageId)}/sections/${encodeURIComponent(sectionId)}`,
|
|
931
809
|
body,
|
|
932
810
|
}),
|
|
933
811
|
|
|
934
|
-
/** `GET /
|
|
935
|
-
* host doesn't advertise `capabilities.content` (501). */
|
|
812
|
+
/** `GET /content/settings` — `null` when the host doesn't advertise `content` (501). */
|
|
936
813
|
getSettings: async (): Promise<LocalizedContentLanguageSettings | null> => {
|
|
937
814
|
try {
|
|
938
815
|
return await this.#request<LocalizedContentLanguageSettings>({
|
|
939
816
|
method: 'GET',
|
|
940
|
-
path: '/
|
|
817
|
+
path: '/content/settings',
|
|
941
818
|
});
|
|
942
819
|
} catch (err) {
|
|
943
820
|
if (err instanceof WopError && err.status === 501) return null;
|
|
@@ -945,126 +822,44 @@ export class OpenwopClient {
|
|
|
945
822
|
}
|
|
946
823
|
},
|
|
947
824
|
|
|
948
|
-
/** `PUT /
|
|
825
|
+
/** `PUT /content/settings` (admin). */
|
|
949
826
|
putSettings: (
|
|
950
827
|
body: LocalizedContentLanguageSettings,
|
|
951
828
|
): Promise<LocalizedContentLanguageSettings> =>
|
|
952
829
|
this.#request<LocalizedContentLanguageSettings>({
|
|
953
830
|
method: 'PUT',
|
|
954
|
-
path: '/
|
|
831
|
+
path: '/content/settings',
|
|
955
832
|
body,
|
|
956
833
|
}),
|
|
957
834
|
};
|
|
958
835
|
|
|
959
|
-
// ── RFC 0099 Trigger subscriptions (gated on
|
|
836
|
+
// ── RFC 0099 Trigger subscriptions (gated on `triggerBridge`) ─────────
|
|
960
837
|
readonly triggerSubscriptions = {
|
|
961
|
-
/** `POST /
|
|
962
|
-
* The `binding.secret*` is returned ONCE at creation (SR-1); persist it.
|
|
963
|
-
* Throws the typed `WopError` on `400`/`401`/`403`, or `501` when the host
|
|
964
|
-
* doesn't advertise the trigger-bridge ingestion surface. */
|
|
838
|
+
/** `POST /trigger-subscriptions` — the `binding.secret*` is returned ONCE; persist it. */
|
|
965
839
|
create: (
|
|
966
840
|
body: TriggerSubscriptionRegistration,
|
|
967
841
|
): Promise<CreateTriggerSubscriptionResponse> =>
|
|
968
842
|
this.#request<CreateTriggerSubscriptionResponse>({
|
|
969
843
|
method: 'POST',
|
|
970
|
-
path: '/
|
|
844
|
+
path: '/trigger-subscriptions',
|
|
971
845
|
body,
|
|
972
846
|
}),
|
|
973
847
|
};
|
|
974
848
|
|
|
975
|
-
// ── Agent workspace files (RFC 0059; gated on capabilities.workspace) ──
|
|
976
|
-
readonly workspace = {
|
|
977
|
-
/**
|
|
978
|
-
* RFC 0059 — list workspace file metadata (no bodies) for the caller's
|
|
979
|
-
* `{tenant, workspace}`. Optional `prefix` filters the flat `path`
|
|
980
|
-
* namespace. Returns `null` when the host doesn't advertise
|
|
981
|
-
* `capabilities.workspace.supported` (501), so callers can branch on
|
|
982
|
-
* capability discovery without try/catch.
|
|
983
|
-
*/
|
|
984
|
-
listFiles: async (opts: { prefix?: string } = {}): Promise<readonly WorkspaceFile[] | null> => {
|
|
985
|
-
const search = new URLSearchParams();
|
|
986
|
-
if (opts.prefix !== undefined) search.set('prefix', opts.prefix);
|
|
987
|
-
const qs = search.toString();
|
|
988
|
-
try {
|
|
989
|
-
const res = await this.#request<{ files: WorkspaceFile[] }>({
|
|
990
|
-
method: 'GET',
|
|
991
|
-
path: `/v1/host/workspace/files${qs ? `?${qs}` : ''}`,
|
|
992
|
-
});
|
|
993
|
-
return res.files;
|
|
994
|
-
} catch (err) {
|
|
995
|
-
if (err instanceof WopError && err.status === 501) return null;
|
|
996
|
-
throw err;
|
|
997
|
-
}
|
|
998
|
-
},
|
|
999
|
-
|
|
1000
|
-
/**
|
|
1001
|
-
* RFC 0059 — read one workspace file. Pass `version` for a historical
|
|
1002
|
-
* snapshot when `capabilities.workspace.versioned`. Returns `null` when
|
|
1003
|
-
* the file is absent (404) or the host doesn't advertise the capability
|
|
1004
|
-
* (501).
|
|
1005
|
-
*/
|
|
1006
|
-
getFile: async (path: string, opts: { version?: number } = {}): Promise<WorkspaceFile | null> => {
|
|
1007
|
-
const search = new URLSearchParams();
|
|
1008
|
-
if (opts.version !== undefined) search.set('version', String(opts.version));
|
|
1009
|
-
const qs = search.toString();
|
|
1010
|
-
try {
|
|
1011
|
-
return await this.#request<WorkspaceFile>({
|
|
1012
|
-
method: 'GET',
|
|
1013
|
-
path: `/v1/host/workspace/files/${encodeURIComponent(path)}${qs ? `?${qs}` : ''}`,
|
|
1014
|
-
});
|
|
1015
|
-
} catch (err) {
|
|
1016
|
-
if (err instanceof WopError && (err.status === 404 || err.status === 501)) return null;
|
|
1017
|
-
throw err;
|
|
1018
|
-
}
|
|
1019
|
-
},
|
|
1020
|
-
|
|
1021
|
-
/**
|
|
1022
|
-
* RFC 0059 — atomic create/replace of a workspace file. Pass `ifMatch`
|
|
1023
|
-
* (the file's current `etag`) for optimistic concurrency; a stale token
|
|
1024
|
-
* throws a `WopError` with status `409` (`workspace_conflict`). Content
|
|
1025
|
-
* beyond `capabilities.workspace.maxFileBytes` throws `413`
|
|
1026
|
-
* (`workspace_too_large`). Returns the persisted `WorkspaceFile`.
|
|
1027
|
-
*/
|
|
1028
|
-
putFile: (
|
|
1029
|
-
path: string,
|
|
1030
|
-
body: PutWorkspaceFileRequest,
|
|
1031
|
-
opts: MutationOptions & { ifMatch?: string } = {},
|
|
1032
|
-
): Promise<WorkspaceFile> => {
|
|
1033
|
-
const headers = this.#mutationHeaders(opts);
|
|
1034
|
-
if (opts.ifMatch !== undefined) headers['If-Match'] = opts.ifMatch;
|
|
1035
|
-
return this.#request<WorkspaceFile>({
|
|
1036
|
-
method: 'PUT',
|
|
1037
|
-
path: `/v1/host/workspace/files/${encodeURIComponent(path)}`,
|
|
1038
|
-
body,
|
|
1039
|
-
headers,
|
|
1040
|
-
});
|
|
1041
|
-
},
|
|
1042
|
-
|
|
1043
|
-
/**
|
|
1044
|
-
* RFC 0059 — delete a workspace file. Returns `true` on success (`204`),
|
|
1045
|
-
* `false` when the file is absent (404) or the host doesn't advertise the
|
|
1046
|
-
* capability (501).
|
|
1047
|
-
*/
|
|
1048
|
-
deleteFile: async (path: string, opts: MutationOptions = {}): Promise<boolean> => {
|
|
1049
|
-
try {
|
|
1050
|
-
await this.#request<void>({
|
|
1051
|
-
method: 'DELETE',
|
|
1052
|
-
path: `/v1/host/workspace/files/${encodeURIComponent(path)}`,
|
|
1053
|
-
headers: this.#mutationHeaders(opts),
|
|
1054
|
-
});
|
|
1055
|
-
return true;
|
|
1056
|
-
} catch (err) {
|
|
1057
|
-
if (err instanceof WopError && (err.status === 404 || err.status === 501)) return false;
|
|
1058
|
-
throw err;
|
|
1059
|
-
}
|
|
1060
|
-
},
|
|
1061
|
-
};
|
|
1062
|
-
|
|
1063
849
|
// ── Internals ────────────────────────────────────────────────────────
|
|
850
|
+
#streamContext(): { baseUrl: string; apiKey: string; protocolVersion: string; fetch: typeof fetch } {
|
|
851
|
+
return {
|
|
852
|
+
baseUrl: this.#baseUrl,
|
|
853
|
+
apiKey: this.#apiKey,
|
|
854
|
+
protocolVersion: this.#versionHeader,
|
|
855
|
+
fetch: this.#fetch,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
1064
859
|
#mutationHeaders(opts: MutationOptions): Record<string, string> {
|
|
1065
860
|
const h: Record<string, string> = {};
|
|
1066
861
|
if (opts.idempotencyKey) h['Idempotency-Key'] = opts.idempotencyKey;
|
|
1067
|
-
if (opts.dedup) h['
|
|
862
|
+
if (opts.dedup) h['OpenWOP-Dedup'] = opts.dedup;
|
|
1068
863
|
return h;
|
|
1069
864
|
}
|
|
1070
865
|
|
|
@@ -1072,6 +867,8 @@ export class OpenwopClient {
|
|
|
1072
867
|
const url = `${this.#baseUrl}${opts.path}`;
|
|
1073
868
|
const headers: Record<string, string> = {
|
|
1074
869
|
Accept: 'application/json',
|
|
870
|
+
// RFC 0172 §A.3 — on every request, authenticated or not.
|
|
871
|
+
'OpenWOP-Version': this.#versionHeader,
|
|
1075
872
|
...(opts.headers ?? {}),
|
|
1076
873
|
};
|
|
1077
874
|
if (opts.body !== undefined && headers['Content-Type'] === undefined) {
|
|
@@ -1080,7 +877,7 @@ export class OpenwopClient {
|
|
|
1080
877
|
if (authenticated) {
|
|
1081
878
|
headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
1082
879
|
}
|
|
1083
|
-
if (this.#acceptLanguage) {
|
|
880
|
+
if (this.#acceptLanguage && headers['Accept-Language'] === undefined) {
|
|
1084
881
|
headers['Accept-Language'] = this.#acceptLanguage;
|
|
1085
882
|
}
|
|
1086
883
|
|
|
@@ -1094,19 +891,16 @@ export class OpenwopClient {
|
|
|
1094
891
|
|
|
1095
892
|
const res = await this.#fetch(url, init);
|
|
1096
893
|
const text = await res.text();
|
|
1097
|
-
// Capture traceparent for error reporting
|
|
1098
|
-
//
|
|
1099
|
-
// RFC 9110; fetch normalizes to lowercase but be defensive.
|
|
894
|
+
// Capture traceparent for error reporting (observability.md §Trace
|
|
895
|
+
// context propagation). Header names are case-insensitive per RFC 9110.
|
|
1100
896
|
const traceparent =
|
|
1101
897
|
res.headers.get('traceparent') ?? res.headers.get('Traceparent') ?? undefined;
|
|
1102
898
|
|
|
1103
899
|
if (!res.ok) {
|
|
1104
900
|
let env: ErrorEnvelope | undefined;
|
|
1105
901
|
try {
|
|
1106
|
-
const parsed = text.length > 0 ? JSON.parse(text) : undefined;
|
|
1107
|
-
if (parsed
|
|
1108
|
-
env = parsed as ErrorEnvelope;
|
|
1109
|
-
}
|
|
902
|
+
const parsed: unknown = text.length > 0 ? JSON.parse(text) : undefined;
|
|
903
|
+
if (isErrorEnvelope(parsed)) env = parsed;
|
|
1110
904
|
} catch {
|
|
1111
905
|
// not JSON; leave envelope undefined
|
|
1112
906
|
}
|
|
@@ -1121,11 +915,18 @@ export class OpenwopClient {
|
|
|
1121
915
|
res.status,
|
|
1122
916
|
text,
|
|
1123
917
|
{
|
|
1124
|
-
error: '
|
|
918
|
+
error: 'internal_error',
|
|
1125
919
|
message: 'Server returned non-JSON body for a 2xx response',
|
|
920
|
+
details: { sdk: 'invalid_json' },
|
|
1126
921
|
},
|
|
1127
922
|
traceparent,
|
|
1128
923
|
);
|
|
1129
924
|
}
|
|
1130
925
|
}
|
|
1131
926
|
}
|
|
927
|
+
|
|
928
|
+
function isErrorEnvelope(value: unknown): value is ErrorEnvelope {
|
|
929
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
930
|
+
const rec = value as Record<string, unknown>;
|
|
931
|
+
return typeof rec['error'] === 'string' && typeof rec['message'] === 'string';
|
|
932
|
+
}
|