@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 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
@@ -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
+ }