dashclaw 2.10.0 → 2.11.1

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 CHANGED
@@ -1,4 +1,4 @@
1
- # DashClaw SDK (v2.10.0)
1
+ # DashClaw SDK (v2.11.1)
2
2
 
3
3
  **Minimal governance runtime for AI agents.**
4
4
 
@@ -70,16 +70,44 @@ claw.update_outcome(action_id, status="completed")
70
70
 
71
71
  ---
72
72
 
73
- ## SDK Surface Area (v2.10.0)
73
+ ## SDK Tiers
74
74
 
75
- The v2 SDK exposes **67 methods** optimized for stability and zero-overhead governance:
75
+ DashClaw currently exposes a canonical Node SDK surface plus a legacy compatibility layer:
76
+
77
+ | | Node SDK | Python SDK |
78
+ |---|---|---|
79
+ | **Focus** | Canonical product surface for new work | Broader current surface |
80
+ | **Methods** | Core runtime + execution surfaces | Broad platform surface |
81
+ | **Core governance** | ✅ | ✅ |
82
+ | **Scoring profiles** | ✅ | ✅ |
83
+ | **Learning loop** | ✅ | ✅ |
84
+ | **Framework integrations** | — | LangChain, CrewAI, AutoGen, Claude Managed Agents |
85
+ | **Compliance engine** | — | ✅ |
86
+ | **Execution graphs** | — | ✅ |
87
+ | **Webhooks management** | — | ✅ |
88
+
89
+ **Node** is designed for most agents — fast, minimal, covers the governance loop and common workflows. **Python** is the enterprise/power-user surface with compliance reporting, execution graph traversal, and framework-native integrations.
90
+
91
+ **Policy:** new product work should target the main `dashclaw` client first. `dashclaw/legacy` exists for compatibility with older integrations and older method shapes.
92
+
93
+ See:
94
+
95
+ - [SDK Consolidation RFC](../docs/rfcs/2026-04-07-sdk-consolidation.md)
96
+ - [SDK Migration Matrix](../docs/planning/2026-04-07-sdk-migration-matrix.md)
97
+ - [SDK Parity Matrix](../docs/sdk-parity.md)
98
+
99
+ ---
100
+
101
+ ## SDK Surface Area (v2.11.1)
102
+
103
+ The v2 SDK exposes the stable governance runtime plus promoted execution domains in the canonical Node client:
76
104
 
77
105
  ### Core Runtime
78
106
  - `guard(context)` -- Policy evaluation ("Can I do X?"). Returns `risk_score` (server-computed) and `agent_risk_score` (raw agent value)
79
107
  - `createAction(action)` -- Lifecycle tracking ("I am doing X")
80
108
  - `updateOutcome(id, outcome)` -- Result recording ("X finished with Y")
81
109
  - `recordAssumption(assumption)` -- Integrity tracking ("I believe Z while doing X")
82
- - `waitForApproval(id)` -- Polling helper for human-in-the-loop approvals
110
+ - `waitForApproval(id)` -- Real-time SSE listener for human-in-the-loop approvals (automatic polling fallback)
83
111
  - `approveAction(id, decision, reasoning?)` -- Submit approval decisions from code
84
112
  - `getPendingApprovals()` -- List actions awaiting human review
85
113
 
@@ -89,7 +117,7 @@ The v2 SDK exposes **67 methods** optimized for stability and zero-overhead gove
89
117
  - `getSignals()` -- Get current risk signals across all agents.
90
118
 
91
119
  ### Swarm & Connectivity
92
- - `heartbeat(status, metadata)` -- Report agent presence and health.
120
+ - `heartbeat(status, metadata)` -- Report agent presence and health. **As of DashClaw 2.13.0, heartbeats are implicit on `createAction()` — you only need this if you want to report presence without recording an action.**
93
121
  - `reportConnections(connections)` -- Report active provider connections.
94
122
 
95
123
  ### Learning & Optimization
@@ -360,6 +388,26 @@ dashclaw deny <actionId> # deny a specific action
360
388
 
361
389
  When an agent calls `waitForApproval()`, it prints the action ID and replay link to stdout. Approve from any terminal or the dashboard, and the agent unblocks instantly.
362
390
 
391
+ ## MCP Server (Zero-Code Integration)
392
+
393
+ If your agent supports MCP (Claude Code, Claude Desktop, Managed Agents), you can skip the SDK entirely:
394
+
395
+ ```json
396
+ {
397
+ "mcpServers": {
398
+ "dashclaw": {
399
+ "command": "npx",
400
+ "args": ["@dashclaw/mcp-server"],
401
+ "env": { "DASHCLAW_URL": "...", "DASHCLAW_API_KEY": "oc_live_..." }
402
+ }
403
+ }
404
+ }
405
+ ```
406
+
407
+ The MCP server exposes the same governance surface as the SDK (guard, record, invoke, wait for approval) plus discovery (capabilities, policies) and session lifecycle.
408
+
409
+ ---
410
+
363
411
  ## Claude Code Hooks
364
412
 
365
413
  Govern Claude Code tool calls without any SDK instrumentation. Copy two files from the `hooks/` directory in the repo into your `.claude/hooks/` folder:
@@ -376,7 +424,9 @@ Then merge the hooks block from `hooks/settings.json` into your `.claude/setting
376
424
 
377
425
  ## Legacy SDK (v1)
378
426
 
379
- The v2 SDK covers the 45 methods most critical to agent governance. If you require the full platform surface (188+ methods including Calendar, Workflows, Routing, Pairing, etc.), the v1 SDK is available via the `dashclaw/legacy` sub-path in Node.js or via the full client in Python.
427
+ `dashclaw/legacy` is a compatibility layer for older integrations. It is not the preferred target for new feature design.
428
+
429
+ Use it only when you need methods that have not yet been promoted into the canonical SDK surface.
380
430
 
381
431
  ```javascript
382
432
  // v1 legacy import
@@ -385,6 +435,19 @@ import { DashClaw } from 'dashclaw/legacy';
385
435
 
386
436
  Methods moved to v1 only: `createWebhook`, `getActivityLogs`, `mapCompliance`, `getProofReport`.
387
437
 
438
+ Legacy also exposes flat compatibility wrappers for the capability runtime routes:
439
+
440
+ - `claw.listCapabilities(...)`
441
+ - `claw.createCapability(...)`
442
+ - `claw.getCapability(...)`
443
+ - `claw.updateCapability(...)`
444
+ - `claw.invokeCapability(...)`
445
+ - `claw.testCapability(...)`
446
+ - `claw.getCapabilityHealth(...)`
447
+ - `claw.listCapabilityHealth(...)`
448
+
449
+ Those wrappers exist to keep older integrations working. New product work should still target `claw.execution.capabilities.*` on the main SDK first.
450
+
388
451
  ---
389
452
 
390
453
  ## Execution Studio
@@ -427,6 +490,60 @@ await claw.duplicateWorkflowTemplate(templateId);
427
490
  // If the template links a model_strategy_id, the resolved config is snapshotted.
428
491
  const { launch } = await claw.launchWorkflowTemplate(templateId, { agent_id: 'deploy-bot' });
429
492
  console.log(launch.action_id); // act_... — view it in /decisions/<action_id>
493
+
494
+ // List past runs for a template (HTTP only — no SDK wrapper yet)
495
+ const runs = await fetch(`${baseUrl}/api/workflows/templates/${templateId}/runs?limit=10`, {
496
+ headers: { 'x-api-key': apiKey },
497
+ }).then(r => r.json());
498
+
499
+ // Get full run detail with step inputs/outputs
500
+ const run = await fetch(`${baseUrl}/api/workflows/templates/${templateId}/runs/${runActionId}`, {
501
+ headers: { 'x-api-key': apiKey },
502
+ }).then(r => r.json());
503
+ // run.steps[].input / run.steps[].output contain full JSON (no truncation)
504
+
505
+ // Resume a failed run from the last completed checkpoint
506
+ const resumed = await fetch(`${baseUrl}/api/workflows/templates/${templateId}/runs/${runActionId}/resume`, {
507
+ method: 'POST',
508
+ headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
509
+ body: JSON.stringify({}),
510
+ }).then(r => r.json());
511
+ // resumed.action_id is the new run; reused steps have status='reused'
512
+
513
+ // Cancel a running workflow
514
+ await fetch(`${baseUrl}/api/workflows/templates/${templateId}/runs/${runActionId}/cancel`, {
515
+ method: 'POST',
516
+ headers: { 'x-api-key': apiKey },
517
+ });
518
+ ```
519
+
520
+ ### Artifacts
521
+
522
+ ```javascript
523
+ // List artifacts (optionally filter by action, step, agent, type)
524
+ const { artifacts } = await fetch(`${baseUrl}/api/artifacts?action_id=${actionId}`, {
525
+ headers: { 'x-api-key': apiKey },
526
+ }).then(r => r.json());
527
+
528
+ // Create an artifact
529
+ const { artifact } = await fetch(`${baseUrl}/api/artifacts`, {
530
+ method: 'POST',
531
+ headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
532
+ body: JSON.stringify({
533
+ artifact_type: 'json',
534
+ name: 'Analysis results',
535
+ content_json: { findings: ['...'] },
536
+ source_action_id: actionId,
537
+ }),
538
+ }).then(r => r.json());
539
+
540
+ // Generate an evidence bundle for a governed action
541
+ const bundle = await fetch(`${baseUrl}/api/artifacts/evidence-bundle`, {
542
+ method: 'POST',
543
+ headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
544
+ body: JSON.stringify({ action_id: actionId }),
545
+ }).then(r => r.json());
546
+ // bundle.action + bundle.steps + bundle.artifacts
430
547
  ```
431
548
 
432
549
  ### Model Strategies
@@ -453,6 +570,15 @@ await claw.updateModelStrategy(strategyId, { config: { maxBudgetUsd: 1.0 } });
453
570
 
454
571
  // Delete (soft references on linked workflow_templates are nulled out)
455
572
  await claw.deleteModelStrategy(strategyId);
573
+
574
+ // Execute a chat completion using the strategy (BYOK, fallback, budget enforcement)
575
+ const result = await claw.completeWithStrategy(strategyId, [
576
+ { role: 'user', content: 'Summarize the deploy plan' }
577
+ ], { max_tokens: 512, temperature: 0.7, task_mode: 'reasoning' });
578
+ console.log(result.content); // LLM response text
579
+ console.log(result.provider); // e.g. 'openai'
580
+ console.log(result.cost_usd); // estimated cost
581
+ console.log(result.fallback_used); // true if primary failed
456
582
  ```
457
583
 
458
584
  ### Knowledge Collections
@@ -477,16 +603,31 @@ await claw.addKnowledgeCollectionItem(collection.collection_id, {
477
603
 
478
604
  // List items
479
605
  const { items } = await claw.listKnowledgeCollectionItems(collection.collection_id);
606
+
607
+ // Sync — ingest pending items (fetch, chunk, embed via BYOK OpenAI key)
608
+ const { sync } = await claw.syncKnowledgeCollection(collection.collection_id);
609
+ console.log(sync.ingested, sync.chunks_created); // e.g. 3 ingested, 42 chunks
610
+
611
+ // Search — semantic similarity over embedded chunks
612
+ const { results } = await claw.searchKnowledgeCollection(
613
+ collection.collection_id,
614
+ 'How do I roll back a deploy?',
615
+ { limit: 5 }
616
+ );
617
+ results.forEach(r => console.log(`${(r.score * 100).toFixed(1)}%: ${r.content.slice(0, 80)}...`));
480
618
  ```
481
619
 
482
- ### Capability Registry
620
+ ### Capability Runtime
483
621
 
484
622
  ```javascript
623
+ // Canonical namespace for capability work
624
+ const caps = claw.execution.capabilities;
625
+
485
626
  // Search the registry (category, risk_level, and search are combinable)
486
- const { capabilities } = await claw.listCapabilities({ risk_level: 'medium', search: 'slack' });
627
+ const { capabilities } = await caps.list({ risk_level: 'medium', search: 'slack' });
487
628
 
488
629
  // Register a capability
489
- await claw.createCapability({
630
+ await caps.create({
490
631
  name: 'Send Slack Message',
491
632
  description: 'Posts to a configured Slack channel',
492
633
  category: 'messaging',
@@ -498,8 +639,69 @@ await claw.createCapability({
498
639
  health_status: 'healthy',
499
640
  docs_url: 'https://docs.example.com/slack'
500
641
  });
642
+
643
+ // Invoke a governed capability
644
+ const result = await caps.invoke('cap_123', {
645
+ query: 'What is x402?'
646
+ });
647
+ console.log(result.governed, result.action_id);
648
+ // When retry_policy is configured on the capability, the response includes retry_metadata:
649
+ // result.retry_metadata → { total_attempts, retried, attempts: [...] }
650
+
651
+ // Run a non-production validation call (bypasses circuit breaker)
652
+ const testRun = await caps.test('cap_123', {
653
+ query: 'What is x402?'
654
+ });
655
+ console.log(testRun.tested, testRun.health_status, testRun.certification_status);
656
+ // testRun.retry_metadata is also present when the capability has retry_policy configured
657
+
658
+ // Fetch derived capability health
659
+ const health = await caps.getHealth('cap_123');
660
+ console.log(health.status, health.certification_status, health.last_test_status);
661
+
662
+ // List derived health for matching capabilities
663
+ const { capabilities: healthRows } = await caps.listHealth({
664
+ risk_level: 'medium',
665
+ certification_status: 'certified',
666
+ stale_only: false,
667
+ limit: 10,
668
+ });
669
+ console.log(healthRows.map((cap) => `${cap.slug}:${cap.status}:${cap.certification_status}`));
670
+
671
+ // Fetch recent invoke/test events for one capability
672
+ const history = await caps.getHistory('cap_123', {
673
+ action_type: 'capability_test',
674
+ status: 'failed',
675
+ limit: 5,
676
+ });
677
+ console.log(history.events.map((event) => `${event.action_type}:${event.status}`));
501
678
  ```
502
679
 
680
+ The existing flat registry methods remain available for compatibility:
681
+
682
+ - `claw.listCapabilities(...)`
683
+ - `claw.createCapability(...)`
684
+ - `claw.getCapability(...)`
685
+ - `claw.updateCapability(...)`
686
+
687
+ Use the canonical capability runtime paths:
688
+
689
+ - `claw.execution.capabilities.invoke(...)`
690
+ - `claw.execution.capabilities.test(...)`
691
+ - `claw.execution.capabilities.getHealth(...)`
692
+ - `claw.execution.capabilities.listHealth(...)`
693
+ - `claw.execution.capabilities.getHistory(...)`
694
+
695
+ Health responses now include certification and recency fields such as:
696
+
697
+ - `certification_status`
698
+ - `last_tested_at`
699
+ - `last_test_status`
700
+ - `stale_check`
701
+ - `success_rate_1d`
702
+ - `success_rate_7d`
703
+ - `p95_latency_ms`
704
+
503
705
  ---
504
706
 
505
707
  ## License
package/dashclaw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DashClaw SDK v2.10.0 (Stable Runtime API)
2
+ * DashClaw SDK v2.11.0 (Stable Runtime API)
3
3
  * Focused governance runtime client for AI agents.
4
4
  */
5
5
 
@@ -34,6 +34,20 @@ class DashClaw {
34
34
  this.baseUrl = baseUrl.replace(/\/$/, '');
35
35
  this.apiKey = apiKey;
36
36
  this.agentId = agentId;
37
+
38
+ this.execution = {
39
+ capabilities: {
40
+ list: (filters = {}) => this.listCapabilities(filters),
41
+ create: (data) => this.createCapability(data),
42
+ get: (capabilityId) => this.getCapability(capabilityId),
43
+ update: (capabilityId, patch) => this.updateCapability(capabilityId, patch),
44
+ invoke: (capabilityId, payload = {}) => this.invokeCapability(capabilityId, payload),
45
+ test: (capabilityId, payload = {}) => this.testCapability(capabilityId, payload),
46
+ getHealth: (capabilityId) => this.getCapabilityHealth(capabilityId),
47
+ listHealth: (filters = {}) => this.listCapabilityHealth(filters),
48
+ getHistory: (capabilityId, filters = {}) => this.getCapabilityHistory(capabilityId, filters),
49
+ },
50
+ };
37
51
  }
38
52
 
39
53
  async _request(path, method = 'GET', body = null, params = null) {
@@ -143,17 +157,112 @@ class DashClaw {
143
157
  }
144
158
 
145
159
  /**
146
- * GET /api/actions/:id Polling helper for human approval.
160
+ * @private Connect to SSE stream and yield parsed events.
161
+ */
162
+ async *_connectSSE(controller) {
163
+ const res = await fetch(`${this.baseUrl}/api/stream`, {
164
+ headers: { 'x-api-key': this.apiKey },
165
+ signal: controller.signal,
166
+ });
167
+
168
+ if (!res.ok || !res.body) return;
169
+
170
+ const reader = res.body.getReader();
171
+ const decoder = new TextDecoder();
172
+ let buffer = '';
173
+ let currentEvent = null;
174
+ let currentData = '';
175
+ let currentId = null;
176
+
177
+ while (true) {
178
+ const { done, value } = await reader.read();
179
+ if (done) break;
180
+ buffer += decoder.decode(value, { stream: true });
181
+
182
+ const lines = buffer.split('\n');
183
+ buffer = lines.pop();
184
+
185
+ for (const line of lines) {
186
+ if (line.startsWith('id: ')) {
187
+ currentId = line.slice(4).trim();
188
+ } else if (line.startsWith('event: ')) {
189
+ currentEvent = line.slice(7).trim();
190
+ } else if (line.startsWith('data: ')) {
191
+ currentData += line.slice(6);
192
+ } else if (line.startsWith(':')) {
193
+ // SSE comment (heartbeat)
194
+ } else if (line === '' && currentEvent) {
195
+ if (currentData) {
196
+ try {
197
+ yield { event: currentEvent, data: JSON.parse(currentData), id: currentId };
198
+ } catch { /* ignore parse errors */ }
199
+ }
200
+ currentEvent = null;
201
+ currentData = '';
202
+ currentId = null;
203
+ } else if (line === '') {
204
+ currentEvent = null;
205
+ currentData = '';
206
+ currentId = null;
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Wait for human approval. SSE-first with polling fallback.
147
214
  */
148
215
  async waitForApproval(actionId, { timeout = 300000, interval = 5000 } = {}) {
149
216
  const startTime = Date.now();
217
+
218
+ const checkAction = (action) => {
219
+ if (action.approved_by) return { resolved: true, result: { action } };
220
+ if (action.status === 'failed' || action.status === 'cancelled') {
221
+ return { resolved: true, error: new ApprovalDeniedError(action.error_message || 'Operator denied the action.', action.status) };
222
+ }
223
+ return { resolved: false };
224
+ };
225
+
226
+ // Try SSE first
227
+ try {
228
+ const controller = new AbortController();
229
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
230
+
231
+ try {
232
+ for await (const frame of this._connectSSE(controller)) {
233
+ if (frame.event === 'action.updated' && frame.data?.action_id === actionId) {
234
+ const check = checkAction(frame.data);
235
+ if (check.resolved) {
236
+ clearTimeout(timeoutId);
237
+ controller.abort();
238
+ if (check.error) throw check.error;
239
+ const confirmed = await this._request(`/api/actions/${actionId}`, 'GET');
240
+ return confirmed;
241
+ }
242
+ }
243
+
244
+ if (Date.now() - startTime >= timeout) {
245
+ clearTimeout(timeoutId);
246
+ controller.abort();
247
+ throw new Error(`Timed out waiting for approval of action ${actionId}`);
248
+ }
249
+ }
250
+ } finally {
251
+ clearTimeout(timeoutId);
252
+ if (!controller.signal.aborted) controller.abort();
253
+ }
254
+ } catch (err) {
255
+ if (err instanceof ApprovalDeniedError || err.message?.includes('Timed out')) throw err;
256
+ // SSE failed — fall through to polling
257
+ }
258
+
259
+ // Polling fallback
150
260
  let wasPending = false;
151
261
  let printedBlock = false;
152
262
 
153
263
  while (Date.now() - startTime < timeout) {
154
264
  const { action } = await this._request(`/api/actions/${actionId}`, 'GET');
155
265
 
156
- // Print structured approval block on first fetch
157
266
  if (!printedBlock) {
158
267
  printedBlock = true;
159
268
  try {
@@ -178,33 +287,18 @@ class DashClaw {
178
287
  '\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d',
179
288
  ];
180
289
  process.stdout.write('\n' + lines.join('\n') + '\n\n');
181
- } catch (_) {
182
- // Rendering failure must not prevent the wait from proceeding
183
- }
290
+ } catch (_) { /* rendering failure must not prevent wait */ }
184
291
  }
185
-
186
- if (action.status === 'pending_approval') {
187
- wasPending = true;
188
- }
189
-
190
- // Explicitly unblocked by approval metadata
191
- if (action.approved_by) return action;
192
292
 
193
- // Denial cases
293
+ if (action.status === 'pending_approval') wasPending = true;
294
+ if (action.approved_by) return { action };
194
295
  if (action.status === 'failed' || action.status === 'cancelled') {
195
296
  throw new ApprovalDeniedError(action.error_message || 'Operator denied the action.', action.status);
196
297
  }
197
-
198
- // Requirement 4: If an action leaves pending_approval without approval metadata, throw an error.
199
- // This prevents "auto-approval" bugs where status is changed by non-approval paths.
200
298
  if (wasPending && action.status !== 'pending_approval') {
201
299
  throw new Error(`Action ${actionId} left pending_approval state without explicit approval metadata (Status: ${action.status})`);
202
300
  }
203
-
204
- // If allowed directly (never intercepted), return immediately
205
- if (!wasPending && action.status === 'running') {
206
- return { action };
207
- }
301
+ if (!wasPending && action.status === 'running') return { action };
208
302
 
209
303
  await new Promise(r => setTimeout(r, interval));
210
304
  }
@@ -743,6 +837,21 @@ class DashClaw {
743
837
  return this._request(`/api/model-strategies/${strategyId}`, 'DELETE');
744
838
  }
745
839
 
840
+ /**
841
+ * POST /api/model-strategies/:id/complete — Execute a chat completion using
842
+ * this strategy. Resolves BYOK provider credentials, handles fallback chain,
843
+ * enforces budget caps.
844
+ * @param {string} strategyId
845
+ * @param {Array<{role: string, content: string}>} messages
846
+ * @param {Object} [options={}] - { max_tokens, temperature, task_mode }
847
+ */
848
+ async completeWithStrategy(strategyId, messages, options = {}) {
849
+ return this._request(`/api/model-strategies/${strategyId}/complete`, 'POST', {
850
+ messages,
851
+ ...options,
852
+ });
853
+ }
854
+
746
855
  // ---------------------------------------------------------------------------
747
856
  // Execution Studio — Knowledge Collections
748
857
  // ---------------------------------------------------------------------------
@@ -795,6 +904,26 @@ class DashClaw {
795
904
  return this._request(`/api/knowledge/collections/${collectionId}/items`, 'POST', data);
796
905
  }
797
906
 
907
+ /**
908
+ * POST /api/knowledge/collections/:id/sync — Ingest pending items (chunk + embed).
909
+ */
910
+ async syncKnowledgeCollection(collectionId) {
911
+ return this._request(`/api/knowledge/collections/${collectionId}/sync`, 'POST', {});
912
+ }
913
+
914
+ /**
915
+ * POST /api/knowledge/collections/:id/search — Semantic search over chunks.
916
+ * @param {string} collectionId
917
+ * @param {string} query
918
+ * @param {Object} [options={}] - { limit }
919
+ */
920
+ async searchKnowledgeCollection(collectionId, query, options = {}) {
921
+ return this._request(`/api/knowledge/collections/${collectionId}/search`, 'POST', {
922
+ query,
923
+ ...options,
924
+ });
925
+ }
926
+
798
927
  // ---------------------------------------------------------------------------
799
928
  // Execution Studio — Capability Registry
800
929
  // ---------------------------------------------------------------------------
@@ -827,6 +956,47 @@ class DashClaw {
827
956
  async updateCapability(capabilityId, patch) {
828
957
  return this._request(`/api/capabilities/${capabilityId}`, 'PATCH', patch);
829
958
  }
959
+
960
+ /**
961
+ * POST /api/capabilities/:id/invoke — Invoke a governed capability.
962
+ */
963
+ async invokeCapability(capabilityId, payload = {}) {
964
+ return this._request(`/api/capabilities/${capabilityId}/invoke`, 'POST', {
965
+ ...payload,
966
+ agent_id: payload.agent_id || this.agentId,
967
+ });
968
+ }
969
+
970
+ /**
971
+ * POST /api/capabilities/:id/test — Run a non-production capability validation call.
972
+ */
973
+ async testCapability(capabilityId, payload = {}) {
974
+ return this._request(`/api/capabilities/${capabilityId}/test`, 'POST', {
975
+ ...payload,
976
+ agent_id: payload.agent_id || this.agentId,
977
+ });
978
+ }
979
+
980
+ /**
981
+ * GET /api/capabilities/:id/health — Fetch derived capability health.
982
+ */
983
+ async getCapabilityHealth(capabilityId) {
984
+ return this._request(`/api/capabilities/${capabilityId}/health`, 'GET');
985
+ }
986
+
987
+ /**
988
+ * GET /api/capabilities/health — List derived health summaries for matching capabilities.
989
+ */
990
+ async listCapabilityHealth(filters = {}) {
991
+ return this._request('/api/capabilities/health', 'GET', null, filters);
992
+ }
993
+
994
+ /**
995
+ * GET /api/capabilities/:id/history — Fetch recent test and invoke events for a capability.
996
+ */
997
+ async getCapabilityHistory(capabilityId, filters = {}) {
998
+ return this._request(`/api/capabilities/${capabilityId}/history`, 'GET', null, filters);
999
+ }
830
1000
  }
831
1001
 
832
1002
  export { DashClaw, ApprovalDeniedError, GuardBlockedError };
@@ -263,11 +263,18 @@ class DashClaw {
263
263
  try { this.guardCallback(decision); } catch { /* ignore callback errors */ }
264
264
  }
265
265
 
266
- const isBlocked = decision.decision === 'block' || decision.decision === 'require_approval';
266
+ // Only `block` is a hard stop. `require_approval` is the normal HITL path:
267
+ // the server will create the action with status='pending_approval' and the
268
+ // approval queue / waitForApproval handles the rest. Throwing here would
269
+ // prevent the POST to /api/actions and break the PWA approval surface.
270
+ const isBlocked = decision.decision === 'block';
267
271
 
268
272
  if (this.guardMode === 'warn' && isBlocked) {
273
+ const reasons = Array.isArray(decision.reasons)
274
+ ? decision.reasons.join('; ')
275
+ : (decision.reason || 'no reason');
269
276
  console.warn(
270
- `[DashClaw] Guard ${decision.decision}: ${decision.reasons.join('; ') || 'no reason'}. Proceeding in warn mode.`
277
+ `[DashClaw] Guard ${decision.decision}: ${reasons}. Proceeding in warn mode.`
271
278
  );
272
279
  return;
273
280
  }
@@ -2826,6 +2833,46 @@ class DashClaw {
2826
2833
  return this._request('GET', '/api/scoring/score', null, { profile_id: profileId, view: 'stats' });
2827
2834
  }
2828
2835
 
2836
+ // --- Capability Runtime Compatibility ------------------
2837
+
2838
+ async listCapabilities(params = {}) {
2839
+ return this._request('GET', '/api/capabilities', null, params);
2840
+ }
2841
+
2842
+ async createCapability(data) {
2843
+ return this._request('POST', '/api/capabilities', data);
2844
+ }
2845
+
2846
+ async getCapability(capabilityId) {
2847
+ return this._request('GET', `/api/capabilities/${capabilityId}`);
2848
+ }
2849
+
2850
+ async updateCapability(capabilityId, data) {
2851
+ return this._request('PATCH', `/api/capabilities/${capabilityId}`, data);
2852
+ }
2853
+
2854
+ async invokeCapability(capabilityId, payload = {}) {
2855
+ return this._request('POST', `/api/capabilities/${capabilityId}/invoke`, {
2856
+ ...payload,
2857
+ agent_id: payload.agent_id || this.agentId,
2858
+ });
2859
+ }
2860
+
2861
+ async testCapability(capabilityId, payload = {}) {
2862
+ return this._request('POST', `/api/capabilities/${capabilityId}/test`, {
2863
+ ...payload,
2864
+ agent_id: payload.agent_id || this.agentId,
2865
+ });
2866
+ }
2867
+
2868
+ async getCapabilityHealth(capabilityId) {
2869
+ return this._request('GET', `/api/capabilities/${capabilityId}/health`);
2870
+ }
2871
+
2872
+ async listCapabilityHealth(params = {}) {
2873
+ return this._request('GET', '/api/capabilities/health', null, params);
2874
+ }
2875
+
2829
2876
  // --- Risk Templates ------------------------------------
2830
2877
 
2831
2878
  async createRiskTemplate(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashclaw",
3
- "version": "2.10.0",
3
+ "version": "2.11.1",
4
4
  "description": "Minimal governance runtime for AI agents. Intercept, govern, and verify agent actions.",
5
5
  "type": "module",
6
6
  "publishConfig": {