@verdictlayer/humanlatch-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +129 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# HumanLatch JavaScript SDK
|
|
2
|
+
|
|
3
|
+
JavaScript/TypeScript SDK for sending AI-driven capability proposals to a HumanLatch control plane.
|
|
4
|
+
|
|
5
|
+
Use this SDK when your app, agent, robot controller, chatbot, or manufacturing system needs to ask:
|
|
6
|
+
|
|
7
|
+
- Can I run this capability automatically?
|
|
8
|
+
- Do I need a human approval first?
|
|
9
|
+
- Is this blocked by policy?
|
|
10
|
+
|
|
11
|
+
The SDK works against either:
|
|
12
|
+
|
|
13
|
+
- self-hosted HumanLatch CE
|
|
14
|
+
- a remotely hosted HumanLatch API
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @verdictlayer/humanlatch-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This package is intended for npm publication as `@verdictlayer/humanlatch-sdk`.
|
|
23
|
+
|
|
24
|
+
## Create a Client
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { HumanLatchClient } from "@verdictlayer/humanlatch-sdk";
|
|
28
|
+
|
|
29
|
+
const client = new HumanLatchClient({
|
|
30
|
+
baseUrl: "https://your-humanlatch.example.com",
|
|
31
|
+
workspaceId: "your-workspace-id",
|
|
32
|
+
apiKey: process.env.HUMANLATCH_API_KEY,
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use `token` for user-authenticated flows or `apiKey` for agent/service integrations.
|
|
37
|
+
|
|
38
|
+
## Core Flow
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const result = await client.proposeAction({
|
|
42
|
+
action_type: "support.refund.issue",
|
|
43
|
+
target: "order-10428",
|
|
44
|
+
summary: "Issue a refund above the autonomous threshold",
|
|
45
|
+
payload: {
|
|
46
|
+
amount: 850,
|
|
47
|
+
currency: "USD",
|
|
48
|
+
customer_id: "cust_2041",
|
|
49
|
+
},
|
|
50
|
+
context: {
|
|
51
|
+
environment: "production",
|
|
52
|
+
requested_by: "agent:support-bot",
|
|
53
|
+
domain: "chatbot",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (client.isApproved(result)) {
|
|
58
|
+
await issueRefund();
|
|
59
|
+
} else if (client.requiresApproval(result)) {
|
|
60
|
+
const final = await client.waitForDecision(result.id, { intervalMs: 2000, timeoutMs: 30000 });
|
|
61
|
+
if (client.isApproved(final)) {
|
|
62
|
+
await issueRefund();
|
|
63
|
+
}
|
|
64
|
+
} else if (client.isBlocked(result)) {
|
|
65
|
+
throw new Error("Blocked by HumanLatch policy");
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## What You Send
|
|
70
|
+
|
|
71
|
+
Every proposed capability should include:
|
|
72
|
+
|
|
73
|
+
- `action_type`: machine-readable capability name
|
|
74
|
+
- `target`: thing being affected
|
|
75
|
+
- `summary`: human-readable explanation
|
|
76
|
+
- `payload`: structured execution details
|
|
77
|
+
- `context`: environment, source, actor, and policy context
|
|
78
|
+
|
|
79
|
+
## Domain Examples
|
|
80
|
+
|
|
81
|
+
### Cloud / DevOps
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
await client.proposeAction({
|
|
85
|
+
action_type: "aws.iam.policy_change",
|
|
86
|
+
target: "prod-admin-role",
|
|
87
|
+
summary: "Attach elevated access to a production IAM role",
|
|
88
|
+
payload: {
|
|
89
|
+
policy_arn: "arn:aws:iam::aws:policy/AdministratorAccess",
|
|
90
|
+
},
|
|
91
|
+
context: {
|
|
92
|
+
environment: "production",
|
|
93
|
+
requested_by: "agent:terraform-bot",
|
|
94
|
+
domain: "cloud",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Chatbot / Customer Operations
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await client.proposeAction({
|
|
103
|
+
action_type: "support.refund.issue",
|
|
104
|
+
target: "order-10428",
|
|
105
|
+
summary: "Issue a refund above the autonomous threshold",
|
|
106
|
+
payload: {
|
|
107
|
+
amount: 850,
|
|
108
|
+
currency: "USD",
|
|
109
|
+
},
|
|
110
|
+
context: {
|
|
111
|
+
environment: "production",
|
|
112
|
+
requested_by: "agent:support-bot",
|
|
113
|
+
domain: "chatbot",
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Robotics
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
await client.proposeAction({
|
|
122
|
+
action_type: "robot.motion.execute",
|
|
123
|
+
target: "cell-7-arm-a",
|
|
124
|
+
summary: "Move a robot into a maintenance zone",
|
|
125
|
+
payload: {
|
|
126
|
+
command: "enter_maintenance_zone",
|
|
127
|
+
speed: "reduced",
|
|
128
|
+
},
|
|
129
|
+
context: {
|
|
130
|
+
environment: "factory",
|
|
131
|
+
requested_by: "agent:cell-controller",
|
|
132
|
+
domain: "robotics",
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Manufacturing
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
await client.proposeAction({
|
|
141
|
+
action_type: "manufacturing.line.override",
|
|
142
|
+
target: "line-3",
|
|
143
|
+
summary: "Override a conveyor safety threshold",
|
|
144
|
+
payload: {
|
|
145
|
+
threshold: 0.92,
|
|
146
|
+
duration_seconds: 180,
|
|
147
|
+
},
|
|
148
|
+
context: {
|
|
149
|
+
environment: "production",
|
|
150
|
+
requested_by: "agent:line-optimizer",
|
|
151
|
+
domain: "manufacturing",
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Additional Methods
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
await client.listActions({ status: "pending_approval", limit: 50 });
|
|
160
|
+
await client.getAction("action-id");
|
|
161
|
+
await client.approveAction("action-id", { note: "Approved by supervisor" });
|
|
162
|
+
await client.rejectAction("action-id", "Unsafe during shift handoff");
|
|
163
|
+
await client.cancelAction("action-id");
|
|
164
|
+
await client.waitForDecision("action-id", { intervalMs: 2000, timeoutMs: 30000 });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Integration Model
|
|
168
|
+
|
|
169
|
+
HumanLatch is not tied to one domain.
|
|
170
|
+
|
|
171
|
+
Your app keeps its own UI, runtime, and execution engine. Before it performs a sensitive capability, it asks HumanLatch for a decision:
|
|
172
|
+
|
|
173
|
+
- `approved_auto`: execute now
|
|
174
|
+
- `pending_approval`: wait for human approval
|
|
175
|
+
- `blocked`: do not execute
|
|
176
|
+
|
|
177
|
+
That same control plane works for:
|
|
178
|
+
|
|
179
|
+
- infrastructure agents
|
|
180
|
+
- support bots
|
|
181
|
+
- warehouse robots
|
|
182
|
+
- manufacturing lines
|
|
183
|
+
- internal copilots
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type HumanLatchStatus = "pending_approval" | "approved_auto" | "approved_manual" | "rejected" | "blocked" | "cancelled";
|
|
2
|
+
export type HumanLatchHeaders = Record<string, string>;
|
|
3
|
+
export interface ProposeActionInput {
|
|
4
|
+
action_type: string;
|
|
5
|
+
target: string;
|
|
6
|
+
summary: string;
|
|
7
|
+
payload?: Record<string, unknown>;
|
|
8
|
+
context?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface ProposeActionResult {
|
|
11
|
+
id: string;
|
|
12
|
+
status: HumanLatchStatus;
|
|
13
|
+
risk_score: number;
|
|
14
|
+
summary: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ActionRecord extends ProposeActionResult {
|
|
17
|
+
workspace_id: string;
|
|
18
|
+
target: string;
|
|
19
|
+
action_type: string;
|
|
20
|
+
payload: Record<string, unknown>;
|
|
21
|
+
context: Record<string, unknown>;
|
|
22
|
+
proposed_by: string;
|
|
23
|
+
decided_by: string | null;
|
|
24
|
+
decided_at: string | null;
|
|
25
|
+
decision_note: string | null;
|
|
26
|
+
created_at: string;
|
|
27
|
+
updated_at: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ListActionsOptions {
|
|
30
|
+
status?: HumanLatchStatus;
|
|
31
|
+
limit?: number;
|
|
32
|
+
offset?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface DecisionOptions {
|
|
35
|
+
note?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface WaitForDecisionOptions {
|
|
38
|
+
intervalMs?: number;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface HumanLatchClientOptions {
|
|
42
|
+
baseUrl: string;
|
|
43
|
+
workspaceId: string;
|
|
44
|
+
token?: string;
|
|
45
|
+
apiKey?: string;
|
|
46
|
+
headers?: HumanLatchHeaders;
|
|
47
|
+
fetch?: typeof fetch;
|
|
48
|
+
}
|
|
49
|
+
export declare class HumanLatchError extends Error {
|
|
50
|
+
readonly status: number;
|
|
51
|
+
readonly detail: unknown;
|
|
52
|
+
constructor(message: string, status: number, detail: unknown);
|
|
53
|
+
}
|
|
54
|
+
export declare class HumanLatchClient {
|
|
55
|
+
private readonly baseUrl;
|
|
56
|
+
private readonly workspaceId;
|
|
57
|
+
private readonly token?;
|
|
58
|
+
private readonly apiKey?;
|
|
59
|
+
private readonly headers;
|
|
60
|
+
private readonly fetchImpl;
|
|
61
|
+
constructor(options: HumanLatchClientOptions);
|
|
62
|
+
proposeAction(input: ProposeActionInput): Promise<ProposeActionResult>;
|
|
63
|
+
propose(input: ProposeActionInput): Promise<ProposeActionResult>;
|
|
64
|
+
listActions(options?: ListActionsOptions): Promise<ActionRecord[]>;
|
|
65
|
+
getAction(actionId: string): Promise<ActionRecord>;
|
|
66
|
+
approveAction(actionId: string, options?: DecisionOptions): Promise<ActionRecord>;
|
|
67
|
+
rejectAction(actionId: string, note: string): Promise<ActionRecord>;
|
|
68
|
+
cancelAction(actionId: string): Promise<ActionRecord>;
|
|
69
|
+
waitForDecision(actionId: string, options?: WaitForDecisionOptions): Promise<ActionRecord>;
|
|
70
|
+
isApproved(result: Pick<ProposeActionResult, "status">): boolean;
|
|
71
|
+
requiresApproval(result: Pick<ProposeActionResult, "status">): boolean;
|
|
72
|
+
isBlocked(result: Pick<ProposeActionResult, "status">): boolean;
|
|
73
|
+
private request;
|
|
74
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export class HumanLatchError extends Error {
|
|
2
|
+
constructor(message, status, detail) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "HumanLatchError";
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.detail = detail;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class HumanLatchClient {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
12
|
+
this.workspaceId = options.workspaceId;
|
|
13
|
+
this.token = options.token;
|
|
14
|
+
this.apiKey = options.apiKey;
|
|
15
|
+
this.headers = options.headers ?? {};
|
|
16
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
17
|
+
if (!this.token && !this.apiKey) {
|
|
18
|
+
throw new Error("HumanLatchClient requires either a token or an apiKey.");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async proposeAction(input) {
|
|
22
|
+
return this.request("/api/v1/actions/propose", {
|
|
23
|
+
method: "POST",
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
action_type: input.action_type,
|
|
26
|
+
target: input.target,
|
|
27
|
+
summary: input.summary,
|
|
28
|
+
payload: input.payload ?? {},
|
|
29
|
+
context: input.context ?? {},
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async propose(input) {
|
|
34
|
+
return this.proposeAction(input);
|
|
35
|
+
}
|
|
36
|
+
async listActions(options = {}) {
|
|
37
|
+
const query = new URLSearchParams();
|
|
38
|
+
if (options.status)
|
|
39
|
+
query.set("status", options.status);
|
|
40
|
+
if (typeof options.limit === "number")
|
|
41
|
+
query.set("limit", String(options.limit));
|
|
42
|
+
if (typeof options.offset === "number")
|
|
43
|
+
query.set("offset", String(options.offset));
|
|
44
|
+
return this.request(`/api/v1/actions?${query.toString()}`);
|
|
45
|
+
}
|
|
46
|
+
async getAction(actionId) {
|
|
47
|
+
return this.request(`/api/v1/actions/${actionId}`);
|
|
48
|
+
}
|
|
49
|
+
async approveAction(actionId, options = {}) {
|
|
50
|
+
return this.request(`/api/v1/actions/${actionId}/approve`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
body: JSON.stringify({ note: options.note }),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async rejectAction(actionId, note) {
|
|
56
|
+
return this.request(`/api/v1/actions/${actionId}/reject`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
body: JSON.stringify({ note }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async cancelAction(actionId) {
|
|
62
|
+
return this.request(`/api/v1/actions/${actionId}/cancel`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async waitForDecision(actionId, options = {}) {
|
|
67
|
+
const intervalMs = options.intervalMs ?? 2000;
|
|
68
|
+
const timeoutMs = options.timeoutMs ?? 60000;
|
|
69
|
+
const startedAt = Date.now();
|
|
70
|
+
while (true) {
|
|
71
|
+
const action = await this.getAction(actionId);
|
|
72
|
+
if (!this.requiresApproval(action)) {
|
|
73
|
+
return action;
|
|
74
|
+
}
|
|
75
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
76
|
+
throw new Error(`Timed out waiting for HumanLatch decision for action ${actionId}.`);
|
|
77
|
+
}
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
isApproved(result) {
|
|
82
|
+
return result.status === "approved_auto" || result.status === "approved_manual";
|
|
83
|
+
}
|
|
84
|
+
requiresApproval(result) {
|
|
85
|
+
return result.status === "pending_approval";
|
|
86
|
+
}
|
|
87
|
+
isBlocked(result) {
|
|
88
|
+
return result.status === "blocked" || result.status === "rejected";
|
|
89
|
+
}
|
|
90
|
+
async request(path, init = {}) {
|
|
91
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
92
|
+
const hasWorkspaceId = /(?:\?|&)workspace_id=/.test(path);
|
|
93
|
+
const workspaceQuery = hasWorkspaceId
|
|
94
|
+
? ""
|
|
95
|
+
: `${separator}workspace_id=${encodeURIComponent(this.workspaceId)}`;
|
|
96
|
+
const url = `${this.baseUrl}${path}${workspaceQuery}`;
|
|
97
|
+
const headers = {
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
...this.headers,
|
|
100
|
+
};
|
|
101
|
+
if (this.token) {
|
|
102
|
+
headers.Authorization = `Bearer ${this.token}`;
|
|
103
|
+
}
|
|
104
|
+
if (this.apiKey) {
|
|
105
|
+
headers["X-API-Key"] = this.apiKey;
|
|
106
|
+
}
|
|
107
|
+
const response = await this.fetchImpl(url, {
|
|
108
|
+
...init,
|
|
109
|
+
headers: {
|
|
110
|
+
...headers,
|
|
111
|
+
...init.headers,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const detail = await response.json().catch(() => null);
|
|
116
|
+
const message = typeof detail === "object" &&
|
|
117
|
+
detail !== null &&
|
|
118
|
+
"detail" in detail &&
|
|
119
|
+
typeof detail.detail === "string"
|
|
120
|
+
? detail.detail
|
|
121
|
+
: "HumanLatch request failed";
|
|
122
|
+
throw new HumanLatchError(message, response.status, detail);
|
|
123
|
+
}
|
|
124
|
+
if (response.status === 204) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
return response.json();
|
|
128
|
+
}
|
|
129
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verdictlayer/humanlatch-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "HumanLatch JavaScript SDK for proposing AI-driven capabilities to a HumanLatch control plane.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/verdictlayer/humanlatch.git",
|
|
21
|
+
"directory": "sdk/javascript"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://humanlatch.verdictlayer.com/developers",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/verdictlayer/humanlatch/issues"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.json"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"ai",
|
|
39
|
+
"approval",
|
|
40
|
+
"agent",
|
|
41
|
+
"policy",
|
|
42
|
+
"human-in-the-loop",
|
|
43
|
+
"robotics",
|
|
44
|
+
"manufacturing"
|
|
45
|
+
],
|
|
46
|
+
"license": "Apache-2.0",
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"typescript": "^5.6.3"
|
|
49
|
+
}
|
|
50
|
+
}
|