dashclaw 2.12.0 → 2.13.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.
Files changed (3) hide show
  1. package/README.md +41 -10
  2. package/dashclaw.js +120 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # DashClaw SDK (v2.11.1)
1
+ # DashClaw SDK
2
2
 
3
3
  **Minimal governance runtime for AI agents.**
4
4
 
@@ -34,6 +34,10 @@ const claw = new DashClaw({
34
34
  apiKey: process.env.DASHCLAW_API_KEY,
35
35
  agentId: 'my-agent',
36
36
  agentName: 'My Agent', // optional — stored in audit trail for attribution
37
+ // Phase 2 (optional): attach a JWT from your OIDC provider for cryptographic
38
+ // attribution. When set, the server verifies the signature via JWKS and the
39
+ // JWT sub claim overrides agentId in the audit record.
40
+ // authToken: process.env.MY_AGENT_JWT,
37
41
  });
38
42
 
39
43
  // 1. Ask permission
@@ -137,14 +141,16 @@ Telegram button.
137
141
  ### The rule every agent author needs to know
138
142
 
139
143
  **`waitForApproval()` must be called with the `action_id` returned by
140
- `createAction()`, NOT with the `action_id` returned by `guard()`.**
144
+ `createAction()`, NOT with the guard decision's id.**
141
145
 
142
146
  These are two different records in two different tables:
143
147
 
144
- | Call | Returns `action_id` that refers to… | Prefix |
145
- |---|---|---|
146
- | `guard()` | A row in `guard_decisions` (the decision log) | `act_gd_…` |
147
- | `createAction()` | A row in `action_records` (the thing you're actually doing) | `act_…` |
148
+ | Call | Returns an id that refers to… | Prefix | Field on the result |
149
+ |---|---|---|---|
150
+ | `guard()` | A row in `guard_decisions` (the decision log) | `act_gd_…` | `decision_id` (canonical); `action_id` is a **deprecated alias** of the same value |
151
+ | `createAction()` | A row in `action_records` (the thing you're actually doing) | `act_…` | `action_id` |
152
+
153
+ > The guard result's `action_id` field is a legacy alias of `decision_id` and will be removed in a future major — read `decision_id` from `guard()`, and `action_id` from `createAction()`.
148
154
 
149
155
  `waitForApproval()` polls `GET /api/actions/:id`, which is the
150
156
  `action_records` table. Passing it a `guard_decisions` ID (`act_gd_…`) will
@@ -247,12 +253,12 @@ See:
247
253
 
248
254
  ---
249
255
 
250
- ## SDK Surface Area (v2.11.1)
256
+ ## SDK Surface Area
251
257
 
252
258
  The v2 SDK exposes the stable governance runtime plus promoted execution domains in the canonical Node client:
253
259
 
254
260
  ### Core Runtime
255
- - `guard(context)` -- Policy evaluation ("Can I do X?"). Returns `risk_score` (server-computed) and `agent_risk_score` (raw agent value). Automatically includes `agent_name` from the constructor if not overridden in the call context.
261
+ - `guard(context)` -- Policy evaluation ("Can I do X?"). Returns `risk_score` (server-computed), `agent_risk_score` (raw agent value), and `verification_status` (`verified` | `unverified` | `expired` | `failed` | `unknown_issuer`). Automatically includes `agent_name` from the constructor if not overridden in the call context. Pass `authToken` in the constructor to enable JWKS-backed cryptographic attribution (Phase 2 — see `docs/agent-identity.md`).
256
262
  - `createAction(action)` -- Lifecycle tracking ("I am doing X"). Accepts optional `idempotency_key`; on collision returns the existing row with `{ idempotent_replay: true }` instead of inserting a duplicate.
257
263
  - `updateOutcome(id, outcome)` -- Result recording ("X finished with Y"). `outcome` accepts `status`, `output_summary`, `side_effects`, `artifacts_created`, `error_message`, `duration_ms`, `tokens_in`, `tokens_out`, `model`, `cost_estimate`. When `tokens_in` / `tokens_out` are reported without an explicit `cost_estimate`, the server derives cost from `model` using the configured pricing table.
258
264
  - `recordAssumption(assumption)` -- Integrity tracking ("I believe Z while doing X")
@@ -332,6 +338,11 @@ lessons.forEach(l => console.log(l.guidance));
332
338
  ### Messaging
333
339
  - `sendMessage({ to, type, subject, body, threadId, urgent })` -- Send a message to another agent or broadcast.
334
340
  - `getInbox({ type, unread, limit })` -- Retrieve inbox messages with optional filters.
341
+ - `getSentMessages({ type, threadId, limit })` -- Retrieve messages this agent has sent.
342
+ - `getMessages({ direction, type, unread, threadId, limit })` -- Retrieve messages with flexible filters.
343
+ - `getMessage(messageId)` -- Fetch a single message by id.
344
+ - `markRead(messageIds)` -- Mark messages as read for this agent (`PATCH /api/messages`, `action: 'read'`).
345
+ - `archiveMessages(messageIds)` -- Archive messages for this agent (`PATCH /api/messages`, `action: 'archive'`).
335
346
 
336
347
  ```javascript
337
348
  // Send a message to another agent
@@ -590,9 +601,29 @@ If your agent supports Model Context Protocol (Claude Code, Claude Desktop, Mana
590
601
 
591
602
  **Streamable HTTP transport** (same surface, served by your DashClaw instance at `POST /api/mcp`).
592
603
 
593
- **8 tools:** `dashclaw_guard`, `dashclaw_record`, `dashclaw_invoke`, `dashclaw_capabilities_list`, `dashclaw_policies_list`, `dashclaw_wait_for_approval`, `dashclaw_session_start`, `dashclaw_session_end`.
604
+ **23 tools** in 7 groups:
605
+
606
+ - **Core governance (8):** `dashclaw_guard`, `dashclaw_record`, `dashclaw_invoke`, `dashclaw_capabilities_list`, `dashclaw_policies_list`, `dashclaw_wait_for_approval`, `dashclaw_session_start`, `dashclaw_session_end`.
607
+ - **Optimal files (2):** `dashclaw_optimal_files_preview`, `dashclaw_optimal_files_manifest` — Code Sessions optimizer output (root CLAUDE.md, path-scoped rules, hooks, skill packs).
608
+ - **Session continuity (3):** `dashclaw_handoff_create`, `dashclaw_handoff_latest`, `dashclaw_handoff_consume` — agent-runtime handoff bundle for the next session.
609
+ - **Credential hygiene (3):** `dashclaw_secret_list`, `dashclaw_secret_due`, `dashclaw_secret_mark_rotated` — check rotation due-dates before acting on tracked credentials.
610
+ - **Skill safety (1):** `dashclaw_skill_scan` — static safety scan of untrusted skill files; results cached by content hash.
611
+ - **Open loops (3):** `dashclaw_loop_add`, `dashclaw_loop_list`, `dashclaw_loop_close` — action-scoped commitments (the "I will X later" tracker).
612
+ - **Learning + retrospection (3):** `dashclaw_learning_log`, `dashclaw_learning_query`, `dashclaw_decisions_recent` — log + query non-obvious decisions; recent governed-action ledger.
613
+
614
+ **6 resources:** `dashclaw://policies`, `dashclaw://capabilities`, `dashclaw://agent/{agent_id}/history`, `dashclaw://status`, `dashclaw://code-sessions/projects`, `dashclaw://code-sessions/sessions/{session_id}`.
615
+
616
+ ### Agent runtime endpoints (server-side, no SDK wrapper)
617
+
618
+ DashClaw 2.17 (platform) added three route families that are **agent-runtime infrastructure, not developer SDK methods**. They are called by the MCP server (the tools listed above), by Hermes Agent hooks, and by other governance plumbing — never directly from agent code. By design, they are not exposed on `claw.*`:
619
+
620
+ | Family | Endpoints | Where called from |
621
+ |---|---|---|
622
+ | Session handoffs | `POST/GET /api/handoffs`, `GET /api/handoffs/latest`, `GET /api/handoffs/{id}`, `POST /api/handoffs/{id}/consume` | Hermes `on_session_end` / `on_session_start` / `pre_llm_call` hooks; MCP `dashclaw_handoff_*` tools |
623
+ | Operator-tracked secrets | `GET/POST /api/secrets`, `PATCH/DELETE /api/secrets/{id}`, `GET /api/secrets/rotation-due` | MCP `dashclaw_secret_*` tools; operator UI |
624
+ | Skill safety scan | `POST /api/skills/scan`, `GET /api/skills/scans/{id}` | MCP `dashclaw_skill_scan` tool; agents before loading an untrusted skill |
594
625
 
595
- **4 resources:** `dashclaw://policies`, `dashclaw://capabilities`, `dashclaw://agent/{agent_id}/history`, `dashclaw://status`.
626
+ If you're building a custom integration that needs these without MCP, call them as plain HTTP — see `docs/api-inventory.md` and the OpenAPI spec at `docs/openapi/critical-stable.openapi.json`.
596
627
 
597
628
  ## OpenClaw Plugin (`@dashclaw/openclaw-plugin`)
598
629
 
package/dashclaw.js CHANGED
@@ -1,6 +1,10 @@
1
1
  /**
2
- * DashClaw SDK v2.11.0 (Stable Runtime API)
2
+ * DashClaw SDK (Stable Runtime API)
3
3
  * Focused governance runtime client for AI agents.
4
+ *
5
+ * Version is the single source of truth in sdk/package.json — never
6
+ * hardcoded here, in the README header, or in app/ pages. Consumers
7
+ * read it at runtime via `import pkg from 'dashclaw/package.json'`.
4
8
  */
5
9
 
6
10
  import { createHash } from 'crypto';
@@ -28,8 +32,12 @@ class DashClaw {
28
32
  * @param {string} options.apiKey - API key for authentication
29
33
  * @param {string} options.agentId - Unique identifier for this agent
30
34
  * @param {string} [options.agentName] - Human-readable label for this agent (stored in audit trail)
35
+ * @param {string} [options.authToken] - Phase 2: JWT bearer token from your OIDC provider.
36
+ * When set, DashClaw server verifies the token via JWKS and returns `verification_status`
37
+ * in every guard response. The JWT `sub` claim overrides agentId in the audit record
38
+ * when verification succeeds — cryptographic proof beats self-assertion.
31
39
  */
32
- constructor({ baseUrl, apiKey, agentId, agentName }) {
40
+ constructor({ baseUrl, apiKey, agentId, agentName, authToken }) {
33
41
  if (!baseUrl) throw new Error('baseUrl is required');
34
42
  if (!apiKey) throw new Error('apiKey is required');
35
43
  if (!agentId) throw new Error('agentId is required');
@@ -38,6 +46,7 @@ class DashClaw {
38
46
  this.apiKey = apiKey;
39
47
  this.agentId = agentId;
40
48
  this.agentName = agentName || null;
49
+ this.authToken = authToken || null;
41
50
 
42
51
  this.execution = {
43
52
  capabilities: {
@@ -57,13 +66,22 @@ class DashClaw {
57
66
  async _request(path, method = 'GET', body = null, params = null) {
58
67
  let url = `${this.baseUrl}${path}`;
59
68
  if (params) {
60
- const qs = new URLSearchParams(params).toString();
61
- if (qs) url += `?${qs}`;
69
+ // Skip undefined/null values. Passing them straight into URLSearchParams
70
+ // serializes the literal strings "undefined"/"null", which the receiving
71
+ // routes treat as real filter values and match zero rows. Falsy-but-valid
72
+ // values (0, false, '') are preserved. Mirrors the v1 SDK behavior.
73
+ const qs = new URLSearchParams();
74
+ for (const [key, value] of Object.entries(params)) {
75
+ if (value !== undefined && value !== null) qs.append(key, String(value));
76
+ }
77
+ const s = qs.toString();
78
+ if (s) url += `?${s}`;
62
79
  }
63
80
 
64
81
  const headers = {
65
82
  'Content-Type': 'application/json',
66
- 'x-api-key': this.apiKey
83
+ 'x-api-key': this.apiKey,
84
+ ...(this.authToken ? { 'Authorization': `Bearer ${this.authToken}` } : {}),
67
85
  };
68
86
 
69
87
  const res = await fetch(url, {
@@ -72,7 +90,16 @@ class DashClaw {
72
90
  body: body ? JSON.stringify(body) : undefined
73
91
  });
74
92
 
75
- const data = await res.json();
93
+ // Parse the body defensively. A non-JSON error body (a Vercel 502/504/413
94
+ // gateway page, a 429 rate-limit page) makes res.json() reject with a
95
+ // SyntaxError, which would propagate instead of the status-bearing error
96
+ // below and lose res.status. Fall back to {} so the real status is thrown.
97
+ let data = {};
98
+ try {
99
+ data = await res.json();
100
+ } catch {
101
+ data = {};
102
+ }
76
103
 
77
104
  if (!res.ok) {
78
105
  if (res.status === 403 && data.decision && data.decision.decision === 'block') {
@@ -94,7 +121,23 @@ class DashClaw {
94
121
  /**
95
122
  * POST /api/guard — "Can I do X?"
96
123
  * @param {Object} context
97
- * @returns {Promise<{decision: 'allow'|'block'|'require_approval', action_id: string, reason: string, signals: string[]}>}
124
+ * @returns {Promise<{
125
+ * decision: 'allow'|'block'|'require_approval'|'warn',
126
+ * action_id: string,
127
+ * reason: string,
128
+ * signals: string[],
129
+ * verification_status: 'verified'|'unverified'|'expired'|'failed'|'unknown_issuer',
130
+ * agent_id: string|null,
131
+ * agent_name: string|null,
132
+ * }>}
133
+ *
134
+ * `verification_status` reflects whether the JWT bearer token (if provided
135
+ * via the `authToken` constructor option) was cryptographically verified:
136
+ * verified — signature valid; audit entry anchored to JWT sub
137
+ * unverified — no token, or issuer temporarily unreachable (fail-soft)
138
+ * expired — token expired; consider refreshing before next call
139
+ * failed — bad signature, malformed token, or audience mismatch
140
+ * unknown_issuer — issuer not in DASHCLAW_ALLOWED_ISSUER (server config)
98
141
  */
99
142
  async guard(context) {
100
143
  return this._request('/api/guard', 'POST', {
@@ -267,7 +310,12 @@ class DashClaw {
267
310
  let printedBlock = false;
268
311
 
269
312
  while (Date.now() - startTime < timeout) {
270
- const { action } = await this._request(`/api/actions/${actionId}`, 'GET');
313
+ // Return the full GET response (action + open_loops + assumptions +
314
+ // message_summary) so the polling fallback resolves to the same shape as
315
+ // the SSE fast-path above and the Python SDK. Returning only { action }
316
+ // dropped the related collections whenever SSE was unavailable.
317
+ const result = await this._request(`/api/actions/${actionId}`, 'GET');
318
+ const action = result.action;
271
319
 
272
320
  if (!printedBlock) {
273
321
  printedBlock = true;
@@ -297,14 +345,14 @@ class DashClaw {
297
345
  }
298
346
 
299
347
  if (action.status === 'pending_approval') wasPending = true;
300
- if (action.approved_by) return { action };
348
+ if (action.approved_by) return result;
301
349
  if (action.status === 'failed' || action.status === 'cancelled') {
302
350
  throw new ApprovalDeniedError(action.error_message || 'Operator denied the action.', action.status);
303
351
  }
304
352
  if (wasPending && action.status !== 'pending_approval') {
305
353
  throw new Error(`Action ${actionId} left pending_approval state without explicit approval metadata (Status: ${action.status})`);
306
354
  }
307
- if (!wasPending && action.status === 'running') return { action };
355
+ if (!wasPending && action.status === 'running') return result;
308
356
 
309
357
  await new Promise(r => setTimeout(r, interval));
310
358
  }
@@ -589,6 +637,68 @@ class DashClaw {
589
637
  });
590
638
  }
591
639
 
640
+ /**
641
+ * GET /api/messages — Fetch messages this agent has sent.
642
+ */
643
+ async getSentMessages({ type, threadId, limit } = {}) {
644
+ return this._request('/api/messages', 'GET', null, {
645
+ agent_id: this.agentId,
646
+ direction: 'sent',
647
+ ...(type && { type }),
648
+ ...(threadId && { thread_id: threadId }),
649
+ ...(limit && { limit }),
650
+ });
651
+ }
652
+
653
+ /**
654
+ * GET /api/messages — Fetch this agent's messages with flexible filters.
655
+ */
656
+ async getMessages({ direction, type, unread, threadId, limit } = {}) {
657
+ return this._request('/api/messages', 'GET', null, {
658
+ agent_id: this.agentId,
659
+ ...(direction && { direction }),
660
+ ...(type && { type }),
661
+ ...(unread != null && { unread }),
662
+ ...(threadId && { thread_id: threadId }),
663
+ ...(limit && { limit }),
664
+ });
665
+ }
666
+
667
+ /**
668
+ * GET /api/messages/:messageId — Fetch a single message by id.
669
+ */
670
+ async getMessage(messageId) {
671
+ return this._request(`/api/messages/${encodeURIComponent(messageId)}`, 'GET');
672
+ }
673
+
674
+ /**
675
+ * PATCH /api/messages — Mark messages as read for this agent. Direct messages
676
+ * are marked read only for the target agent (or dashboard); broadcasts update
677
+ * read_by for this agent.
678
+ * @param {string[]} messageIds - Message IDs (msg_*) to mark read.
679
+ * @returns {Promise<{ updated: number }>}
680
+ */
681
+ async markRead(messageIds) {
682
+ return this._request('/api/messages', 'PATCH', {
683
+ message_ids: messageIds,
684
+ action: 'read',
685
+ agent_id: this.agentId,
686
+ });
687
+ }
688
+
689
+ /**
690
+ * PATCH /api/messages — Archive messages for this agent.
691
+ * @param {string[]} messageIds - Message IDs (msg_*) to archive.
692
+ * @returns {Promise<{ updated: number }>}
693
+ */
694
+ async archiveMessages(messageIds) {
695
+ return this._request('/api/messages', 'PATCH', {
696
+ message_ids: messageIds,
697
+ action: 'archive',
698
+ agent_id: this.agentId,
699
+ });
700
+ }
701
+
592
702
  // ---------------------------------------------------------------------------
593
703
  // Session Handoffs
594
704
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashclaw",
3
- "version": "2.12.0",
3
+ "version": "2.13.1",
4
4
  "description": "Minimal governance runtime for AI agents. Intercept, govern, and verify agent actions.",
5
5
  "type": "module",
6
6
  "publishConfig": {