@intutic/clawde 2.3.0 → 2.5.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.
@@ -1,7 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ControlPlaneClient = void 0;
4
+ const crypto_1 = require("crypto");
4
5
  const errors_1 = require("./errors");
6
+ const toon_1 = require("./toon");
7
+ /** The connector providers `intutic guardrails sources` lists: the ones that are policy sources. */
8
+ const POLICY_SOURCE_PROVIDERS = new Set(['notion', 'confluence', 'github', 'gdrive']);
9
+ /** The coverage report's format names on the route, by the CLI's names. */
10
+ const COVERAGE_FORMATS = { json: 'json', md: 'markdown', csv: 'csv', pdf: 'pdf' };
5
11
  /**
6
12
  * Control-plane management client — org/team/gateway/credentials
7
13
  * administration, as distinct from `ClawdeClient`'s data-plane chat calls.
@@ -13,14 +19,25 @@ const errors_1 = require("./errors");
13
19
  * management calls onto `ClawdeClient` would silently need a second base URL
14
20
  * on a class whose whole contract today is "one client, one proxy."
15
21
  *
16
- * Every endpoint here is mirrored 1:1 from `tools/cli/src/commands/{org,
17
- * team,gateway,credentials,settings,whoami}.ts` — the CLI's own already-tested
18
- * contracts, not re-derived. Endpoints deliberately NOT included: session
19
- * establishment (`login`/`logout` — an SDK caller supplies `apiKey`
20
- * directly), and local-environment/terminal concerns (`init`, `doctor`,
21
- * `install-daemon`, `integrity`, `rollback`, `connect`, `exec`, `start`,
22
- * `syncContext`, `skill`) that have no meaning for a library embedded in
23
- * someone else's process.
22
+ * Every endpoint here comes from the control-plane calls in
23
+ * `tools/cli/src/commands/*` — the CLI's own already-tested contracts, not
24
+ * re-derived — and the Python SDK's `control_plane.py` has the same methods
25
+ * in snake_case. `packages/shared-types/fixtures/control-plane-operations.json`
26
+ * lists every method with the request it makes and the answer it returns, and
27
+ * both SDKs' tests run against it, so neither can grow a method the other
28
+ * lacks. Method names follow one pattern for whatever is added next: a verb
29
+ * (`list`, `get`, `create`, `update`, `delete`, `rotate…Secret`, `download`,
30
+ * or the CLI subcommand's own verb, such as `approve` or `promote`), then the
31
+ * resource.
32
+ *
33
+ * Not included: session establishment (`login`/`logout`; an SDK caller
34
+ * supplies `apiKey` directly, and `ClawdeClient` registers its own session),
35
+ * the CLI's workspace budget (`ClawdeClient.checkBudget()`), and commands
36
+ * that act on the machine they run on: `init`, `setup`, `doctor`,
37
+ * `install-daemon`, `connect`, `disconnect`, `exec`, `start`, `syncContext`,
38
+ * `rollback`, `enforce`, `rules`, `judge`, `skill` (its loop commands are
39
+ * here), `sops status`, `guardrails pull`, and `policy
40
+ * compile|install|snapshot|list-local`.
24
41
  *
25
42
  * Works unmodified against a self-hosted control plane — there is no
26
43
  * SaaS-vs-self-hosted branch anywhere in this file. An open-core user who
@@ -32,9 +49,10 @@ const errors_1 = require("./errors");
32
49
  *
33
50
  * Auth: the same `apiKey` type `ClawdeClient` accepts — a `vk_` token or a
34
51
  * JWT both satisfy `services/control-plane/src/middleware/auth.ts`, which
35
- * resolves either to an `AuthContext` with a role; endpoints gated
36
- * `requireRole('OWNER','ADMIN')` server-side reject an under-privileged
37
- * token exactly as they would for the CLI using the same token.
52
+ * resolves either to an `AuthContext` with a role. A call the role may not
53
+ * make throws `ClawdeConnectionError` with the control plane's answer; for a
54
+ * 403 that names the roles allowed (`detail`), the message is that sentence,
55
+ * as the CLI prints it.
38
56
  */
39
57
  class ControlPlaneClient {
40
58
  apiKey;
@@ -47,26 +65,41 @@ class ControlPlaneClient {
47
65
  this.baseUrl =
48
66
  options.baseUrl || process.env.INTUTIC_CONTROL_PLANE_URL || 'https://app.intutic.ai';
49
67
  }
50
- async request(method, path, body, auth = true) {
51
- const headers = { 'Content-Type': 'application/json' };
52
- if (auth)
68
+ async send(method, path, init = {}) {
69
+ // A form body sets its own multipart content type, boundary included.
70
+ const headers = init.form ? {} : { 'Content-Type': 'application/json' };
71
+ if (init.auth !== false)
53
72
  headers['Authorization'] = `Bearer ${this.apiKey}`;
54
- let res;
55
73
  try {
56
- res = await fetch(`${this.baseUrl}${path}`, {
74
+ return await fetch(`${this.baseUrl}${path}`, {
57
75
  method,
58
76
  headers,
59
- body: body !== undefined ? JSON.stringify(body) : undefined,
77
+ body: init.form ?? (init.body !== undefined ? JSON.stringify(init.body) : undefined),
60
78
  });
61
79
  }
62
80
  catch (err) {
63
81
  throw new errors_1.ClawdeConnectionError(`Could not reach control plane at ${this.baseUrl}: ${err.message}`);
64
82
  }
83
+ }
84
+ /**
85
+ * A JSON call. `answers` lists the error statuses whose body is the answer
86
+ * rather than a failure: the integrity chain walk answers a break with 409.
87
+ */
88
+ async request(method, path, body, auth = true, answers = [], form) {
89
+ const res = await this.send(method, path, { body, form, auth });
90
+ const text = await res.text().catch(() => 'Unknown error');
91
+ if (!res.ok && !answers.includes(res.status))
92
+ throw controlPlaneFailure(method, path, res.status, text);
93
+ return (text ? (0, toon_1.unwrapToonEnvelope)(JSON.parse(text)) : undefined);
94
+ }
95
+ /** A file download: the body as bytes. Its errors are JSON, as every other call's. */
96
+ async download(path) {
97
+ const res = await this.send('GET', path);
65
98
  if (!res.ok) {
66
99
  const text = await res.text().catch(() => 'Unknown error');
67
- throw new errors_1.ClawdeConnectionError(`Control plane ${method} ${path} failed (${res.status}): ${text}`);
100
+ throw controlPlaneFailure('GET', path, res.status, text);
68
101
  }
69
- return (await res.json());
102
+ return new Uint8Array(await res.arrayBuffer());
70
103
  }
71
104
  /** GET /api/v1/auth/me */
72
105
  async whoami() {
@@ -203,6 +236,596 @@ class ControlPlaneClient {
203
236
  async unsetProviderCredential(provider) {
204
237
  await this.request('DELETE', `/api/v1/workspace/provider-credentials/${encodeURIComponent(provider)}`);
205
238
  }
239
+ /** GET /api/v1/workspace/provider-credentials/:provider/models */
240
+ async listProviderModels(provider) {
241
+ return this.request('GET', `/api/v1/workspace/provider-credentials/${encodeURIComponent(provider)}/models`);
242
+ }
243
+ // ─── MCP server registry (`intutic mcp`) ───
244
+ /** GET /api/v1/mcp/servers — every server the workspace's MCP proxies have reported, and the registry-wide policy. */
245
+ async listMcpServers() {
246
+ return this.request('GET', '/api/v1/mcp/servers');
247
+ }
248
+ /** POST /api/v1/mcp/servers/:serverId/status — approve a server (OWNER, ADMIN). */
249
+ async approveMcpServer(serverId) {
250
+ return this.setMcpServerStatus(serverId, 'approved');
251
+ }
252
+ /** POST /api/v1/mcp/servers/:serverId/status — block a server (OWNER, ADMIN). */
253
+ async blockMcpServer(serverId) {
254
+ return this.setMcpServerStatus(serverId, 'blocked');
255
+ }
256
+ /** POST /api/v1/mcp/servers/:serverId/status — return a server to the approval queue (OWNER, ADMIN). */
257
+ async resetMcpServer(serverId) {
258
+ return this.setMcpServerStatus(serverId, 'candidate');
259
+ }
260
+ /** POST /api/v1/mcp/servers/:serverId/tools — switch one tool on (OWNER, ADMIN). */
261
+ async enableMcpTool(serverId, tool) {
262
+ return this.request('POST', `/api/v1/mcp/servers/${seg(serverId)}/tools`, { tool, enabled: true });
263
+ }
264
+ /** POST /api/v1/mcp/servers/:serverId/tools — switch one tool off (OWNER, ADMIN). */
265
+ async disableMcpTool(serverId, tool) {
266
+ return this.request('POST', `/api/v1/mcp/servers/${seg(serverId)}/tools`, { tool, enabled: false });
267
+ }
268
+ async setMcpServerStatus(serverId, status) {
269
+ return this.request('POST', `/api/v1/mcp/servers/${seg(serverId)}/status`, { status });
270
+ }
271
+ // ─── Notification rules (`intutic notifications`) ───
272
+ /** GET /api/v1/notifications/rules */
273
+ async listNotificationRules() {
274
+ const res = await this.request('GET', '/api/v1/notifications/rules');
275
+ return res.rules ?? [];
276
+ }
277
+ /**
278
+ * POST /api/v1/notifications/rules. A webhook rule comes back with its
279
+ * `signingSecret`, which is not shown again.
280
+ */
281
+ async createNotificationRule(rule) {
282
+ return this.request('POST', '/api/v1/notifications/rules', rule);
283
+ }
284
+ /**
285
+ * PUT /api/v1/notifications/rules/:ruleId — only the fields given change.
286
+ * A switch to the webhook channel comes back with the new `signingSecret`.
287
+ */
288
+ async updateNotificationRule(ruleId, changes) {
289
+ return this.request('PUT', `/api/v1/notifications/rules/${seg(ruleId)}`, changes);
290
+ }
291
+ /** DELETE /api/v1/notifications/rules/:ruleId */
292
+ async deleteNotificationRule(ruleId) {
293
+ await this.request('DELETE', `/api/v1/notifications/rules/${seg(ruleId)}`);
294
+ }
295
+ /** POST /api/v1/notifications/rules/:ruleId/signing-secret — deliveries are signed with the new secret from now on. */
296
+ async rotateNotificationRuleSecret(ruleId) {
297
+ return this.request('POST', `/api/v1/notifications/rules/${seg(ruleId)}/signing-secret`);
298
+ }
299
+ // ─── SIEM export (`intutic siem`) ───
300
+ /** GET /api/v1/siem/destinations — the destinations and the event sources they can receive. */
301
+ async listSiemDestinations() {
302
+ return this.request('GET', '/api/v1/siem/destinations');
303
+ }
304
+ /** GET /api/v1/siem/destinations/:destinationId */
305
+ async getSiemDestination(destinationId) {
306
+ return this.request('GET', `/api/v1/siem/destinations/${seg(destinationId)}`);
307
+ }
308
+ /** The event sources a destination can receive; `defaults` are the ones an empty `sourceTables` means. */
309
+ async listSiemSources() {
310
+ return (await this.listSiemDestinations()).sources;
311
+ }
312
+ /**
313
+ * POST /api/v1/siem/destinations (OWNER, ADMIN). The control plane refuses
314
+ * a destination that points at an internal address. A webhook destination
315
+ * comes back with its `signingSecret`, which is not shown again.
316
+ */
317
+ async createSiemDestination(destination) {
318
+ return this.request('POST', '/api/v1/siem/destinations', destination);
319
+ }
320
+ /** PUT /api/v1/siem/destinations/:destinationId (OWNER, ADMIN) — only the fields given change. */
321
+ async updateSiemDestination(destinationId, changes) {
322
+ return this.request('PUT', `/api/v1/siem/destinations/${seg(destinationId)}`, changes);
323
+ }
324
+ /** DELETE /api/v1/siem/destinations/:destinationId (OWNER, ADMIN) — deactivates it; `updateSiemDestination(id, { isActive: true })` turns it back on. */
325
+ async deleteSiemDestination(destinationId) {
326
+ await this.request('DELETE', `/api/v1/siem/destinations/${seg(destinationId)}`);
327
+ }
328
+ /** POST /api/v1/siem/destinations/:destinationId/signing-secret (OWNER, ADMIN) */
329
+ async rotateSiemDestinationSecret(destinationId) {
330
+ return this.request('POST', `/api/v1/siem/destinations/${seg(destinationId)}/signing-secret`);
331
+ }
332
+ // ─── Usage (`intutic usage`) ───
333
+ //
334
+ // OWNER, ADMIN and EM see the whole workspace; anyone else their own calls
335
+ // (`scope: 'self'`). `getTeamUsage` is refused below EM.
336
+ /** GET /api/v1/usage/members */
337
+ async getMemberUsage(options = {}) {
338
+ return this.request('GET', `/api/v1/usage/members${usagePeriod(options)}`);
339
+ }
340
+ /** GET /api/v1/usage/teams — per SCIM group. */
341
+ async getTeamUsage(options = {}) {
342
+ return this.request('GET', `/api/v1/usage/teams${usagePeriod(options)}`);
343
+ }
344
+ /** GET /api/v1/usage/branches */
345
+ async getBranchUsage(options = {}) {
346
+ return this.request('GET', `/api/v1/usage/branches${usagePeriod(options)}`);
347
+ }
348
+ /** GET /api/v1/usage/commits */
349
+ async getCommitUsage(options = {}) {
350
+ return this.request('GET', `/api/v1/usage/commits${usagePeriod(options)}`);
351
+ }
352
+ /** GET /api/v1/usage/pull-requests */
353
+ async getPullRequestUsage(options = {}) {
354
+ return this.request('GET', `/api/v1/usage/pull-requests${usagePeriod(options)}`);
355
+ }
356
+ /** POST /api/v1/usage/pull-requests/refresh — look the branches up on GitHub now (OWNER, ADMIN, EM). */
357
+ async refreshPullRequestUsage() {
358
+ return this.request('POST', '/api/v1/usage/pull-requests/refresh');
359
+ }
360
+ // ─── AI inventory (`intutic inventory`) ───
361
+ //
362
+ // OWNER, ADMIN and EM see every machine; a DEVELOPER only their own.
363
+ /** GET /api/v1/inventory/summary */
364
+ async getInventorySummary() {
365
+ return (await this.request('GET', '/api/v1/inventory/summary')).data;
366
+ }
367
+ /** GET /api/v1/inventory/devices — one row per machine. */
368
+ async listInventoryDevices() {
369
+ return this.listData('/api/v1/inventory/devices');
370
+ }
371
+ /** GET /api/v1/inventory/harnesses */
372
+ async listInventoryHarnesses(filters = {}) {
373
+ return this.listData(`/api/v1/inventory/harnesses${inventoryQuery(filters)}`);
374
+ }
375
+ /** GET /api/v1/inventory/mcp-servers */
376
+ async listInventoryMcpServers(filters = {}) {
377
+ return this.listData(`/api/v1/inventory/mcp-servers${inventoryQuery(filters)}`);
378
+ }
379
+ /** GET /api/v1/inventory/skills */
380
+ async listInventorySkills(filters = {}) {
381
+ return this.listData(`/api/v1/inventory/skills${inventoryQuery(filters)}`);
382
+ }
383
+ /** GET /api/v1/inventory/disconnects — machines that ran `intutic disconnect`, newest first (default 100, at most 500). */
384
+ async listInventoryDisconnects(options = {}) {
385
+ return this.listData(`/api/v1/inventory/disconnects${query({ limit: options.limit })}`);
386
+ }
387
+ /** GET /api/v1/inventory/harnesses?format=csv — the CSV the dashboard downloads. */
388
+ async downloadInventoryHarnessesCsv(filters = {}) {
389
+ return this.download(`/api/v1/inventory/harnesses${inventoryQuery(filters, true)}`);
390
+ }
391
+ /** GET /api/v1/inventory/mcp-servers?format=csv */
392
+ async downloadInventoryMcpServersCsv(filters = {}) {
393
+ return this.download(`/api/v1/inventory/mcp-servers${inventoryQuery(filters, true)}`);
394
+ }
395
+ async listData(path) {
396
+ return (await this.request('GET', path)).data ?? [];
397
+ }
398
+ // ─── Compliance (`intutic compliance`) ───
399
+ /** GET /api/v1/compliance/frameworks/:frameworkId/coverage — `eu_ai_act`, `iso_42001`, `nist_ai_rmf` or `mitre_atlas`. */
400
+ async getFrameworkCoverage(frameworkId) {
401
+ return this.request('GET', `/api/v1/compliance/frameworks/${seg(frameworkId)}/coverage`);
402
+ }
403
+ /**
404
+ * GET /api/v1/compliance/frameworks/:frameworkId/coverage?format= — the
405
+ * report as a file. Unsigned: the signed copies are the ones sealed in the
406
+ * evidence archive.
407
+ */
408
+ async downloadFrameworkCoverage(frameworkId, format) {
409
+ const routeFormat = COVERAGE_FORMATS[format];
410
+ if (!routeFormat)
411
+ throw new Error(`format must be one of ${Object.keys(COVERAGE_FORMATS).join(', ')}, got "${format}"`);
412
+ return this.download(`/api/v1/compliance/frameworks/${seg(frameworkId)}/coverage${query({ format: routeFormat })}`);
413
+ }
414
+ /** POST /api/v1/compliance/soc2-collect (OWNER, ADMIN) — run a fresh evidence collection and seal it. */
415
+ async collectEvidence(options = {}) {
416
+ return this.request('POST', '/api/v1/compliance/soc2-collect', defined({ periodStart: options.periodStart, periodEnd: options.periodEnd }));
417
+ }
418
+ /** GET /api/v1/compliance/soc2-export/:runId (OWNER, ADMIN) — a stored archive, as the file. Check it with `verifyEvidenceArchive`. */
419
+ async downloadEvidence(runId) {
420
+ return this.download(`/api/v1/compliance/soc2-export/${seg(runId)}`);
421
+ }
422
+ /**
423
+ * GET /.well-known/intutic-trace-signing.json — the published signing keys,
424
+ * fetched without credentials. `verifyEvidenceArchive` takes them.
425
+ */
426
+ async getSigningKeys() {
427
+ return this.request('GET', '/.well-known/intutic-trace-signing.json', undefined, false);
428
+ }
429
+ // ─── Gate liveness and the GitHub webhook ───
430
+ /** GET /api/v1/governance/gate-liveness (OWNER, ADMIN, EM) — whether each installed harness's gate is reporting. */
431
+ async getGateLiveness() {
432
+ return this.request('GET', '/api/v1/governance/gate-liveness');
433
+ }
434
+ /** GET /api/v1/integrations/github/webhook (OWNER, ADMIN) — the payload URL; never the secret. */
435
+ async getGithubWebhook() {
436
+ return this.request('GET', '/api/v1/integrations/github/webhook');
437
+ }
438
+ /**
439
+ * POST /api/v1/integrations/github/webhook/secret (OWNER, ADMIN) — make the
440
+ * webhook, or replace its secret. Deliveries signed with an earlier secret
441
+ * are refused from then on.
442
+ */
443
+ async rotateGithubWebhookSecret() {
444
+ return this.request('POST', '/api/v1/integrations/github/webhook/secret');
445
+ }
446
+ // ─── Policy guardrails (`intutic guardrails`) ───
447
+ /** GET /api/v1/connectors — the workspace's policy sources (Notion, Confluence, GitHub, Google Drive). */
448
+ async listPolicySources() {
449
+ const res = await this.request('GET', '/api/v1/connectors');
450
+ return (res.items ?? []).filter((c) => POLICY_SOURCE_PROVIDERS.has(c.provider));
451
+ }
452
+ /** POST /api/v1/connectors */
453
+ async addPolicySource(source) {
454
+ return this.request('POST', '/api/v1/connectors', { config: {}, ...source });
455
+ }
456
+ /** POST /api/v1/connectors/:connectorId/sync */
457
+ async syncPolicySource(connectorId) {
458
+ return this.request('POST', `/api/v1/connectors/${seg(connectorId)}/sync`, {});
459
+ }
460
+ /** GET /api/v1/policy-guardrails/documents */
461
+ async listPolicyDocuments() {
462
+ return (await this.request('GET', `${GUARDRAILS}/documents`)).documents ?? [];
463
+ }
464
+ /** GET /api/v1/policy-guardrails/documents/:docId — passages, clauses and extraction runs. */
465
+ async getPolicyDocument(docId) {
466
+ return (await this.request('GET', `${GUARDRAILS}/documents/${seg(docId)}`)).document;
467
+ }
468
+ /** POST /api/v1/policy-guardrails/documents/:docId/extract — propose guardrails from the document's clauses. */
469
+ async extractPolicyDocument(docId, options = {}) {
470
+ const res = await this.request('POST', `${GUARDRAILS}/documents/${seg(docId)}/extract`, {
471
+ llm: options.llm ?? true,
472
+ });
473
+ return res.result;
474
+ }
475
+ /** GET /api/v1/policy-guardrails/coverage?token= — the passages that mention a tool or action, and the guardrails on them. */
476
+ async getGuardrailCoverage(token) {
477
+ return (await this.request('GET', `${GUARDRAILS}/coverage${query({ token })}`)).coverage;
478
+ }
479
+ /** GET /api/v1/policy-guardrails/search?q= — full-text search over live passages, best match first. */
480
+ async searchPolicyPassages(text) {
481
+ return (await this.request('GET', `${GUARDRAILS}/search${query({ q: text })}`)).search;
482
+ }
483
+ /** GET /api/v1/policy-guardrails/impact — what a change to a document or a passage reaches. Name one of them. */
484
+ async getGuardrailImpact(seed) {
485
+ const path = `${GUARDRAILS}/impact${query({ docId: seed.docId, passageId: seed.passageId })}`;
486
+ return (await this.request('GET', path)).impact;
487
+ }
488
+ /** GET /api/v1/policy-guardrails/duplicates — overlapping passages and rules cited more than once. */
489
+ async listGuardrailDuplicates(options = {}) {
490
+ const path = `${GUARDRAILS}/duplicates${query({ minJaccard: options.minJaccard })}`;
491
+ return (await this.request('GET', path)).duplicates;
492
+ }
493
+ /** GET /api/v1/policy-guardrails/guardrails */
494
+ async listGuardrails(filters = {}) {
495
+ const path = `${GUARDRAILS}/guardrails${query({
496
+ status: filters.status,
497
+ target: filters.target,
498
+ provenance: filters.provenance,
499
+ docId: filters.docId,
500
+ limit: filters.limit,
501
+ })}`;
502
+ return (await this.request('GET', path)).guardrails ?? [];
503
+ }
504
+ /** GET /api/v1/policy-guardrails/guardrails/:guardrailId */
505
+ async getGuardrail(guardrailId) {
506
+ return (await this.request('GET', `${GUARDRAILS}/guardrails/${seg(guardrailId)}`)).guardrail;
507
+ }
508
+ /** GET /api/v1/policy-guardrails/guardrails/:guardrailId/readiness — whether a SHADOW guardrail's evidence clears promotion. */
509
+ async getGuardrailReadiness(guardrailId) {
510
+ const res = await this.request('GET', `${GUARDRAILS}/guardrails/${seg(guardrailId)}/readiness`);
511
+ return res.readiness;
512
+ }
513
+ /** POST …/guardrails/:guardrailId/approve-shadow — into SHADOW: distributed as warn, measuring, enforcing nothing. */
514
+ async approveGuardrailShadow(guardrailId) {
515
+ return this.transitionGuardrail(guardrailId, 'approve-shadow', {});
516
+ }
517
+ /**
518
+ * POST …/guardrails/:guardrailId/promote — into ENFORCING. Refused with a
519
+ * 409 until the shadow evidence clears the thresholds; a guardrail that
520
+ * never fired needs `acknowledgeNoTraffic`.
521
+ */
522
+ async promoteGuardrail(guardrailId, options = {}) {
523
+ return this.transitionGuardrail(guardrailId, 'promote', { acknowledgeNoTraffic: options.acknowledgeNoTraffic === true });
524
+ }
525
+ /** POST …/guardrails/:guardrailId/reject — recorded with the reason. */
526
+ async rejectGuardrail(guardrailId, reason) {
527
+ return this.transitionGuardrail(guardrailId, 'reject', { reason });
528
+ }
529
+ /** POST …/guardrails/:guardrailId/retire — gone from every rule endpoint on the next poll. */
530
+ async retireGuardrail(guardrailId) {
531
+ return this.transitionGuardrail(guardrailId, 'retire', {});
532
+ }
533
+ /** POST …/guardrails/:guardrailId/reconfirm — re-confirm a stale citation against a live passage. */
534
+ async reconfirmGuardrail(guardrailId) {
535
+ return this.transitionGuardrail(guardrailId, 'reconfirm', {});
536
+ }
537
+ /**
538
+ * POST /api/v1/policy-guardrails/guardrails (OWNER, ADMIN) — an authored
539
+ * guardrail, created PROPOSED. An IR a check refuses is a 400 naming the
540
+ * check.
541
+ */
542
+ async createGuardrail(guardrail) {
543
+ return this.request('POST', `${GUARDRAILS}/guardrails`, guardrail);
544
+ }
545
+ /**
546
+ * PUT /api/v1/policy-guardrails/guardrails/:guardrailId (OWNER, ADMIN). A
547
+ * changed IR creates the next version (`forked`), PROPOSED with no
548
+ * evidence; a name or description edit keeps the evidence.
549
+ */
550
+ async updateGuardrail(guardrailId, changes) {
551
+ return this.request('PUT', `${GUARDRAILS}/guardrails/${seg(guardrailId)}`, changes);
552
+ }
553
+ /** DELETE /api/v1/policy-guardrails/guardrails/:guardrailId (OWNER, ADMIN) — retires it; its history is kept. */
554
+ async deleteGuardrail(guardrailId) {
555
+ return this.request('DELETE', `${GUARDRAILS}/guardrails/${seg(guardrailId)}`);
556
+ }
557
+ /** POST …/guardrails/:guardrailId/replay — what the guardrail would have done on captured traffic. */
558
+ async replayGuardrail(guardrailId) {
559
+ return (await this.request('POST', `${GUARDRAILS}/guardrails/${seg(guardrailId)}/replay`, {})).replay;
560
+ }
561
+ /** GET /api/v1/policy-guardrails/conflicts — live guardrails and front-matter rules that contradict each other. */
562
+ async listGuardrailConflicts() {
563
+ return (await this.request('GET', `${GUARDRAILS}/conflicts`)).conflicts ?? [];
564
+ }
565
+ async transitionGuardrail(guardrailId, action, body) {
566
+ return this.request('POST', `${GUARDRAILS}/guardrails/${seg(guardrailId)}/${action}`, body);
567
+ }
568
+ // ─── Held decisions and loop runs (`intutic decision`, `intutic loop`) ───
569
+ /** POST /api/v1/decisions/:holdId/review (OWNER, ADMIN, EM) — approve a held call. */
570
+ async approveDecision(holdId, options = {}) {
571
+ return this.request('POST', `/api/v1/decisions/${seg(holdId)}/review`, defined({ action: 'approve', reason: options.reason }));
572
+ }
573
+ /** POST /api/v1/decisions/:holdId/review (OWNER, ADMIN, EM) — reject a held call. */
574
+ async rejectDecision(holdId, options = {}) {
575
+ return this.request('POST', `/api/v1/decisions/${seg(holdId)}/review`, defined({ action: 'reject', reason: options.reason }));
576
+ }
577
+ /**
578
+ * POST /api/v1/loops/start — register a loop run. Send its id with the run's
579
+ * requests (`INTUTIC_LOOP_RUN_ID`, as `intutic loop exec` sets it) so the
580
+ * proxy files them under it.
581
+ */
582
+ async startLoopRun(name, options = {}) {
583
+ const body = defined({ name, budgetLimitUsd: options.budgetLimitUsd, sops: options.sops, autoJudge: options.autoJudge });
584
+ return (await this.request('POST', '/api/v1/loops/start', body)).loop;
585
+ }
586
+ /** GET /api/v1/loops/:loopRunId */
587
+ async getLoopRun(loopRunId) {
588
+ return (await this.request('GET', `/api/v1/loops/${seg(loopRunId)}`)).loop;
589
+ }
590
+ /** GET /api/v1/loops */
591
+ async listLoopRuns() {
592
+ return (await this.request('GET', '/api/v1/loops')).loops ?? [];
593
+ }
594
+ /** POST /api/v1/loops/:loopRunId/complete — `outcome` records how the run ended. */
595
+ async completeLoopRun(loopRunId, options = {}) {
596
+ const body = options.outcome === undefined ? undefined : { outcome: options.outcome };
597
+ return this.request('POST', `/api/v1/loops/${seg(loopRunId)}/complete`, body);
598
+ }
599
+ /** POST /api/v1/loops/:loopRunId/kill */
600
+ async killLoopRun(loopRunId) {
601
+ return this.request('POST', `/api/v1/loops/${seg(loopRunId)}/kill`);
602
+ }
603
+ /** POST /api/v1/loops/:loopRunId/review (OWNER, ADMIN, EM) — approve a run held for review; it is ACTIVE again. */
604
+ async approveLoopRun(loopRunId, options = {}) {
605
+ return this.request('POST', `/api/v1/loops/${seg(loopRunId)}/review`, defined({ action: 'approve', note: options.note }));
606
+ }
607
+ /** POST /api/v1/loops/:loopRunId/review (OWNER, ADMIN, EM) — reject a run held for review; it is KILLED. */
608
+ async rejectLoopRun(loopRunId, options = {}) {
609
+ return this.request('POST', `/api/v1/loops/${seg(loopRunId)}/review`, defined({ action: 'reject', note: options.note }));
610
+ }
611
+ // ─── Findings (`intutic findings`) ───
612
+ /** GET /api/v1/findings — detector findings, newest first. */
613
+ async listFindings(filters = {}) {
614
+ const path = `/api/v1/findings${query({
615
+ unadjudicated: filters.unadjudicated ? 'true' : undefined,
616
+ detector_id: filters.detectorId,
617
+ limit: filters.limit,
618
+ })}`;
619
+ return (await this.request('GET', path)).findings ?? [];
620
+ }
621
+ /** POST /api/v1/findings/:findingId/adjudicate — the ruling is recorded under the key's member. */
622
+ async adjudicateFinding(findingId, outcome, options = {}) {
623
+ return this.request('POST', `/api/v1/findings/${seg(findingId)}/adjudicate`, defined({ outcome, note: options.note }));
624
+ }
625
+ /** GET /api/v1/findings/stats — false-positive rate per detector, over adjudicated findings. */
626
+ async getFindingStats() {
627
+ return this.request('GET', '/api/v1/findings/stats');
628
+ }
629
+ /** GET /api/v1/findings/response-echo/report */
630
+ async getResponseEchoReport(options = {}) {
631
+ return this.request('GET', `/api/v1/findings/response-echo/report${query({ since: options.since, until: options.until })}`);
632
+ }
633
+ // ─── Governance incidents (`intutic incidents`) ───
634
+ //
635
+ // OWNER, ADMIN and EM only: incidents carry tool arguments and DLP reasons.
636
+ /** GET /api/v1/incidents — ranked by review priority; filter by status, severity and type. */
637
+ async listIncidents(filters = {}) {
638
+ const path = `/api/v1/incidents${query({
639
+ status: filters.status,
640
+ severity: filters.severity,
641
+ type: filters.type,
642
+ page: filters.page,
643
+ limit: filters.limit,
644
+ })}`;
645
+ return this.request('GET', path);
646
+ }
647
+ /** GET /api/v1/incidents/:incidentId */
648
+ async getIncident(incidentId) {
649
+ return (await this.request('GET', `/api/v1/incidents/${seg(incidentId)}`)).data;
650
+ }
651
+ // ─── Traces (`intutic traces`) ───
652
+ /** GET /api/v1/traces — newest first. */
653
+ async listTraces(filters = {}) {
654
+ const path = `/api/v1/traces${query({
655
+ limit: filters.limit,
656
+ offset: filters.offset,
657
+ since: filters.since,
658
+ enforcement: filters.enforcement,
659
+ model: filters.model,
660
+ })}`;
661
+ return this.request('GET', path);
662
+ }
663
+ /** GET /api/v1/traces/:traceId */
664
+ async getTrace(traceId) {
665
+ return this.request('GET', `/api/v1/traces/${seg(traceId)}`);
666
+ }
667
+ // ─── Trace integrity (`intutic integrity`) ───
668
+ /** GET /api/v1/integrity/roots — the sealed trace roots. */
669
+ async listIntegrityRoots(options = {}) {
670
+ return this.request('GET', `/api/v1/integrity/roots${query({ loopRunId: options.loopRunId })}`);
671
+ }
672
+ /** GET /api/v1/integrity/roots/:rootId — the root, its signature and its leaves. */
673
+ async getIntegrityRoot(rootId) {
674
+ return this.request('GET', `/api/v1/integrity/roots/${seg(rootId)}`);
675
+ }
676
+ /** POST /api/v1/integrity/roots/:rootId/recompute — re-derive the root from the stored traces. */
677
+ async recomputeIntegrityRoot(rootId) {
678
+ return this.request('POST', `/api/v1/integrity/roots/${seg(rootId)}/recompute`, {});
679
+ }
680
+ /** GET /api/v1/integrity/chain — the root chain walk; `intact: false` lists the breaks. */
681
+ async getIntegrityChain() {
682
+ return this.request('GET', '/api/v1/integrity/chain', undefined, true, [409]);
683
+ }
684
+ /** GET /api/v1/integrity/config-chain — the harness config snapshot chain walk. */
685
+ async getIntegrityConfigChain() {
686
+ return this.request('GET', '/api/v1/integrity/config-chain', undefined, true, [409]);
687
+ }
688
+ // ─── Compliance policies and WASM rules (`intutic policy`) ───
689
+ /** GET /api/v1/policies */
690
+ async listPolicies() {
691
+ return (await this.request('GET', '/api/v1/policies')).policies ?? [];
692
+ }
693
+ /** POST /api/v1/policies/:policyId/enable */
694
+ async enablePolicy(policyId) {
695
+ return this.request('POST', `/api/v1/policies/${seg(policyId)}/enable`);
696
+ }
697
+ /** POST /api/v1/policies/:policyId/disable */
698
+ async disablePolicy(policyId) {
699
+ return this.request('POST', `/api/v1/policies/${seg(policyId)}/disable`);
700
+ }
701
+ /** POST /api/v1/policies/:policyId/rollback */
702
+ async rollbackPolicy(policyId, version) {
703
+ return this.request('POST', `/api/v1/policies/${seg(policyId)}/rollback`, { version });
704
+ }
705
+ /**
706
+ * GET /api/v1/rule-candidates/:candidateId/source — the source of record a
707
+ * candidate's bundle must be compiled from. A source that does not hash to
708
+ * the `sourceSha256` served with it is refused (`ClawdeConnectionError`):
709
+ * something between the control plane and this process changed it.
710
+ */
711
+ async getRuleCandidateSource(candidateId) {
712
+ const res = await this.request('GET', `/api/v1/rule-candidates/${seg(candidateId)}/source`);
713
+ const local = (0, crypto_1.createHash)('sha256').update(res.source, 'utf8').digest('hex');
714
+ const stated = res.sourceSha256.toLowerCase();
715
+ if (local !== stated) {
716
+ throw new errors_1.ClawdeConnectionError(`The served source hashes to ${local}, not the ${stated} the control plane states.`);
717
+ }
718
+ return { ...res, sourceSha256: stated };
719
+ }
720
+ /**
721
+ * POST /api/v1/rule-candidates/:candidateId/bundle (OWNER, ADMIN) — upload
722
+ * a compiled rule with the hash of the source it was compiled from. The
723
+ * control plane runs its gates; `accepted` says whether the rule went into
724
+ * shadow, and `gates` why.
725
+ */
726
+ async uploadRuleCandidateBundle(candidateId, wasm, sourceSha256, options = {}) {
727
+ const form = new FormData();
728
+ form.append('file', new Blob([new Uint8Array(wasm)], { type: 'application/wasm' }), options.fileName ?? 'rule.wasm');
729
+ form.append('source_sha256', sourceSha256);
730
+ return this.request('POST', `/api/v1/rule-candidates/${seg(candidateId)}/bundle`, undefined, true, [], form);
731
+ }
732
+ /** POST /api/v1/wasm-rules/:ruleId/replay — what a rule would have done on sampled traffic, without enforcing. */
733
+ async replayWasmRule(ruleId, options = {}) {
734
+ return this.request('POST', `/api/v1/wasm-rules/${seg(ruleId)}/replay`, defined({ limit: options.limit, since: options.since }));
735
+ }
736
+ // ─── SOPs (`intutic sops`) ───
737
+ /** GET /api/v1/sops */
738
+ async listSops(options = {}) {
739
+ return (await this.request('GET', `/api/v1/sops${query({ limit: options.limit })}`)).items ?? [];
740
+ }
741
+ /** GET /api/v1/sops/:sopId */
742
+ async getSop(sopId) {
743
+ return this.request('GET', `/api/v1/sops/${seg(sopId)}`);
744
+ }
745
+ /** POST /api/v1/sops — one workspace SOP; `markdown_content` without front matter. */
746
+ async createSop(sop) {
747
+ return this.request('POST', '/api/v1/sops', sop);
748
+ }
749
+ /** GET /api/v1/workspace/org-sops — the org-wide SOP floors of the key's org. */
750
+ async listOrgSops() {
751
+ return this.listData('/api/v1/workspace/org-sops');
752
+ }
753
+ /** POST /api/v1/workspace/org-sops — a mandatory org-wide floor (OWNER, ADMIN of a workspace in the org). */
754
+ async createOrgSop(sop) {
755
+ return this.request('POST', '/api/v1/workspace/org-sops', sop);
756
+ }
757
+ /** DELETE /api/v1/workspace/org-sops/:orgSopId — answers 404 both for no such SOP and for a member without admin access. */
758
+ async deleteOrgSop(orgSopId) {
759
+ await this.request('DELETE', `/api/v1/workspace/org-sops/${seg(orgSopId)}`);
760
+ }
761
+ // ─── Key attenuation, cost prediction, routing ───
762
+ /**
763
+ * POST /api/v1/attenuate — a child key narrowed to a subset of the parent
764
+ * key's capabilities. The child key is in the response once and never
765
+ * stored.
766
+ */
767
+ async attenuateKey(parentKeyId, requestedCaps, options = {}) {
768
+ return this.request('POST', '/api/v1/attenuate', defined({ parentKeyId, requestedCaps, ttlSeconds: options.ttlSeconds }));
769
+ }
770
+ /** GET /api/v1/attenuate/chain/:chainId (OWNER, ADMIN) — the delegation lineage. */
771
+ async getAttenuationChain(chainId) {
772
+ return (await this.request('GET', `/api/v1/attenuate/chain/${seg(chainId)}`)).chain ?? [];
773
+ }
774
+ /** POST /api/v1/predict-cost — estimate a call's cost before it runs. Size the input with a token count or the text. */
775
+ async predictCost(params) {
776
+ return this.request('POST', '/api/v1/predict-cost', params);
777
+ }
778
+ /** GET /api/v1/routing/mirror-adoption-report — how a mirror-tested candidate model compared. Reported only; changes nothing. */
779
+ async getMirrorAdoptionReport(candidateModel) {
780
+ return this.request('GET', `/api/v1/routing/mirror-adoption-report${query({ candidateModel })}`);
781
+ }
206
782
  }
207
783
  exports.ControlPlaneClient = ControlPlaneClient;
784
+ const GUARDRAILS = '/api/v1/policy-guardrails';
785
+ const seg = encodeURIComponent;
786
+ /** A query string of the parameters that are set, with its `?`; empty when none is. */
787
+ function query(params) {
788
+ const q = new URLSearchParams();
789
+ for (const [key, value] of Object.entries(params)) {
790
+ if (value !== undefined)
791
+ q.set(key, String(value));
792
+ }
793
+ const s = q.toString();
794
+ return s ? `?${s}` : '';
795
+ }
796
+ function usagePeriod(options) {
797
+ return query({ period: options.period ?? 'monthly' });
798
+ }
799
+ function inventoryQuery(filters, csv = false) {
800
+ return query({
801
+ status: filters.status,
802
+ harness: filters.harness,
803
+ device: filters.device,
804
+ q: filters.search,
805
+ format: csv ? 'csv' : undefined,
806
+ });
807
+ }
808
+ /** The object without its undefined members, which is also what JSON sends. */
809
+ function defined(body) {
810
+ return Object.fromEntries(Object.entries(body).filter(([, v]) => v !== undefined));
811
+ }
812
+ /**
813
+ * The error a failed call throws. A 403 carries the control plane's `detail`
814
+ * when it gives one (a role guard names the roles that may make the call), so
815
+ * the message says which role can, as the CLI's does.
816
+ */
817
+ function controlPlaneFailure(method, path, status, text) {
818
+ if (status === 403) {
819
+ try {
820
+ const body = JSON.parse(text);
821
+ if (typeof body.detail === 'string') {
822
+ return new errors_1.ClawdeConnectionError(`Control plane ${method} ${path} refused (403): ${body.detail}`);
823
+ }
824
+ }
825
+ catch {
826
+ // Not JSON: the raw answer below.
827
+ }
828
+ }
829
+ return new errors_1.ClawdeConnectionError(`Control plane ${method} ${path} failed (${status}): ${text}`);
830
+ }
208
831
  //# sourceMappingURL=control-plane.js.map