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.
- package/README.md +41 -10
- package/dashclaw.js +120 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# DashClaw SDK
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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<{
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
// ---------------------------------------------------------------------------
|