specter-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 +73 -0
- package/index.cjs +113 -0
- package/index.cjs.map +1 -0
- package/index.d.cts +99 -0
- package/index.d.ts +99 -0
- package/index.js +110 -0
- package/index.js.map +1 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# specter-sdk
|
|
2
|
+
|
|
3
|
+
Thin client for **[Specter](https://specter-ia.vercel.app)** — the *detect → block → prove* firewall / decision API for **AI-agent payments**. It inspects every money-moving action an agent attempts and proves where the destination came from (the user's request, or content the agent ingested mid-task — i.e. prompt injection).
|
|
4
|
+
|
|
5
|
+
The SDK is the thin "plug"; the decision API is the product. **Zero runtime dependencies.**
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i specter-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Guard — check an action programmatically
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Guard } from 'specter-sdk';
|
|
15
|
+
|
|
16
|
+
const guard = new Guard({
|
|
17
|
+
apiUrl: 'https://specter-decision-api.fly.dev',
|
|
18
|
+
apiKey: process.env.SPECTER_API_KEY!, // your tenant key
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const result = await guard.check({
|
|
22
|
+
agentId: 'shop-agent',
|
|
23
|
+
action: {
|
|
24
|
+
type: 'payment',
|
|
25
|
+
amount: 79.99,
|
|
26
|
+
currency: 'USD',
|
|
27
|
+
destination: 'acct_attacker_x9f3',
|
|
28
|
+
merchantClaimed: 'Acme Store',
|
|
29
|
+
},
|
|
30
|
+
context: {
|
|
31
|
+
userPrompt: 'Buy the Acme mouse from Acme Store',
|
|
32
|
+
destinationOrigin: 'ingested_content', // came from a scraped page, not the user
|
|
33
|
+
sourceRefs: ['firecrawl:https://shop.example/acme'],
|
|
34
|
+
establishedMerchant: 'Acme Store',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (result.decision !== 'allow') {
|
|
39
|
+
// result.decision: 'deny' | 'review' · result.reason · result.riskScore
|
|
40
|
+
throw new Error(`Blocked by Specter: ${result.reason}`);
|
|
41
|
+
}
|
|
42
|
+
// safe to execute the payment
|
|
43
|
+
|
|
44
|
+
// convenience:
|
|
45
|
+
if (await guard.isAllowed({ action })) { /* ... */ }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Claude Code — drop-in PreToolUse hook
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { Guard, createClaudeCodeHook } from 'specter-sdk';
|
|
52
|
+
|
|
53
|
+
const handle = createClaudeCodeHook(
|
|
54
|
+
new Guard({ apiUrl: '…', apiKey: '…' }),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// In your hook server, return the handler's response with HTTP 200:
|
|
58
|
+
const response = await handle(claudeCodePayload);
|
|
59
|
+
// → { hookSpecificOutput: { permissionDecision: 'allow' | 'deny' | 'ask', ... } }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
> Claude Code only blocks on an HTTP **200** whose body says
|
|
63
|
+
> `permissionDecision: "deny"`. A non-2xx merely logs and the tool runs anyway —
|
|
64
|
+
> so your host must answer 200 with this body. Fails safe to `ask` on any error.
|
|
65
|
+
|
|
66
|
+
## Types
|
|
67
|
+
|
|
68
|
+
`Guard`, `GuardOptions`, `CheckInput`, `createClaudeCodeHook`, `HookPayload`,
|
|
69
|
+
`HookResponse`, and the action/decision types (`Action`, `Context`,
|
|
70
|
+
`DecisionResult`, `Decision`, `ActionType`, `DestinationOrigin`, `SignalResult`)
|
|
71
|
+
are all exported.
|
|
72
|
+
|
|
73
|
+
MIT · [github.com/Eras256/Specter](https://github.com/Eras256/Specter)
|
package/index.cjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/claude-code-hook.ts
|
|
4
|
+
function createClaudeCodeHook(guard) {
|
|
5
|
+
return async function handle(payload) {
|
|
6
|
+
try {
|
|
7
|
+
const action = normalize(payload);
|
|
8
|
+
const result = await guard.check({
|
|
9
|
+
agentId: `claude-code:${payload.tool_name ?? "tool"}`,
|
|
10
|
+
sessionId: payload.session_id ?? "cc-session",
|
|
11
|
+
action,
|
|
12
|
+
context: {
|
|
13
|
+
userPrompt: payload.specter?.userPrompt ?? "",
|
|
14
|
+
establishedMerchant: payload.specter?.establishedMerchant,
|
|
15
|
+
destinationOrigin: payload.specter?.destinationOrigin ?? "unknown"
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const permissionDecision = result.decision === "allow" ? "allow" : result.decision === "deny" ? "deny" : "ask";
|
|
19
|
+
return resp(permissionDecision, result.reason);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return resp("ask", `Specter unavailable, escalating: ${err.message}`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function resp(permissionDecision, permissionDecisionReason) {
|
|
26
|
+
return {
|
|
27
|
+
hookSpecificOutput: {
|
|
28
|
+
hookEventName: "PreToolUse",
|
|
29
|
+
permissionDecision,
|
|
30
|
+
permissionDecisionReason
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function normalize(p) {
|
|
35
|
+
const tool = p.tool_name ?? "unknown";
|
|
36
|
+
const ti = p.tool_input ?? {};
|
|
37
|
+
const amount = typeof ti.amount === "number" ? ti.amount : void 0;
|
|
38
|
+
const destination = str(ti.destination) ?? str(ti.to) ?? str(ti.account) ?? void 0;
|
|
39
|
+
let type = "other";
|
|
40
|
+
let command;
|
|
41
|
+
if (amount != null || destination) type = "payment";
|
|
42
|
+
else if (tool === "Bash") {
|
|
43
|
+
type = "shell";
|
|
44
|
+
command = str(ti.command);
|
|
45
|
+
} else if (["Read", "Write", "Edit", "NotebookEdit"].includes(tool)) {
|
|
46
|
+
type = "file";
|
|
47
|
+
command = str(ti.file_path) ?? str(ti.path);
|
|
48
|
+
} else command = JSON.stringify(ti);
|
|
49
|
+
return {
|
|
50
|
+
type,
|
|
51
|
+
amount,
|
|
52
|
+
currency: str(ti.currency),
|
|
53
|
+
destination,
|
|
54
|
+
merchantClaimed: str(ti.merchantClaimed),
|
|
55
|
+
command,
|
|
56
|
+
rawInput: ti
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function str(v) {
|
|
60
|
+
return typeof v === "string" && v.length ? v : void 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/guard.ts
|
|
64
|
+
var Guard = class {
|
|
65
|
+
constructor(opts) {
|
|
66
|
+
this.opts = opts;
|
|
67
|
+
}
|
|
68
|
+
opts;
|
|
69
|
+
async check(input) {
|
|
70
|
+
const body = {
|
|
71
|
+
agentId: input.agentId ?? this.opts.agentId ?? "sdk-agent",
|
|
72
|
+
sessionId: input.sessionId ?? "sdk-session",
|
|
73
|
+
action: input.action,
|
|
74
|
+
context: {
|
|
75
|
+
userPrompt: input.context?.userPrompt ?? "",
|
|
76
|
+
destinationOrigin: input.context?.destinationOrigin ?? "unknown",
|
|
77
|
+
sourceRefs: input.context?.sourceRefs ?? [],
|
|
78
|
+
establishedMerchant: input.context?.establishedMerchant
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const retries = this.opts.retries ?? 1;
|
|
82
|
+
let lastErr;
|
|
83
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch(`${this.opts.apiUrl.replace(/\/$/, "")}/v1/evaluate`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"content-type": "application/json",
|
|
89
|
+
authorization: `Bearer ${this.opts.apiKey}`
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4e3)
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);
|
|
95
|
+
return await res.json();
|
|
96
|
+
} catch (err) {
|
|
97
|
+
lastErr = err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Specter check failed after ${retries + 1} attempts: ${lastErr.message}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
/** Convenience: true only when the action is explicitly allowed. */
|
|
105
|
+
async isAllowed(input) {
|
|
106
|
+
return (await this.check(input)).decision === "allow";
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
exports.Guard = Guard;
|
|
111
|
+
exports.createClaudeCodeHook = createClaudeCodeHook;
|
|
112
|
+
//# sourceMappingURL=index.cjs.map
|
|
113
|
+
//# sourceMappingURL=index.cjs.map
|
package/index.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/claude-code-hook.ts","../src/guard.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { Guard } from './guard.js';\nimport type { Action, ActionType, DestinationOrigin } from './types.js';\n\n/** Claude Code PreToolUse hook payload (subset). */\nexport interface HookPayload {\n session_id?: string;\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n specter?: { userPrompt?: string; establishedMerchant?: string; destinationOrigin?: string };\n}\n\n/** The body Claude Code requires to actually block. */\nexport interface HookResponse {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse';\n permissionDecision: 'allow' | 'deny' | 'ask';\n permissionDecisionReason: string;\n };\n}\n\n/**\n * Build a ready-made Claude Code PreToolUse hook handler from a Guard.\n *\n * IMPORTANT: when wired as an HTTP hook, Claude Code only blocks on an HTTP 200\n * whose body says permissionDecision: \"deny\". A non-2xx status merely logs and\n * the tool runs anyway. This handler returns the correct body; the host (the\n * decision API's /hooks/claude-code route, or your own server) must answer 200.\n */\nexport function createClaudeCodeHook(guard: Guard) {\n return async function handle(payload: HookPayload): Promise<HookResponse> {\n try {\n const action = normalize(payload);\n const result = await guard.check({\n agentId: `claude-code:${payload.tool_name ?? 'tool'}`,\n sessionId: payload.session_id ?? 'cc-session',\n action,\n context: {\n userPrompt: payload.specter?.userPrompt ?? '',\n establishedMerchant: payload.specter?.establishedMerchant,\n destinationOrigin: (payload.specter?.destinationOrigin as DestinationOrigin) ?? 'unknown',\n },\n });\n const permissionDecision =\n result.decision === 'allow' ? 'allow' : result.decision === 'deny' ? 'deny' : 'ask';\n return resp(permissionDecision, result.reason);\n } catch (err) {\n // Fail safe: escalate to a human rather than silently allowing.\n return resp('ask', `Specter unavailable, escalating: ${(err as Error).message}`);\n }\n };\n}\n\nfunction resp(\n permissionDecision: 'allow' | 'deny' | 'ask',\n permissionDecisionReason: string,\n): HookResponse {\n return {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision,\n permissionDecisionReason,\n },\n };\n}\n\nfunction normalize(p: HookPayload): Action {\n const tool = p.tool_name ?? 'unknown';\n const ti = p.tool_input ?? {};\n const amount = typeof ti.amount === 'number' ? ti.amount : undefined;\n const destination = str(ti.destination) ?? str(ti.to) ?? str(ti.account) ?? undefined;\n\n let type: ActionType = 'other';\n let command: string | undefined;\n if (amount != null || destination) type = 'payment';\n else if (tool === 'Bash') {\n type = 'shell';\n command = str(ti.command);\n } else if (['Read', 'Write', 'Edit', 'NotebookEdit'].includes(tool)) {\n type = 'file';\n command = str(ti.file_path) ?? str(ti.path);\n } else command = JSON.stringify(ti);\n\n return {\n type,\n amount,\n currency: str(ti.currency),\n destination,\n merchantClaimed: str(ti.merchantClaimed),\n command,\n rawInput: ti,\n };\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === 'string' && v.length ? v : undefined;\n}\n","import type { Action, Context, DecisionResult } from './types.js';\n\nexport interface GuardOptions {\n apiUrl: string;\n apiKey: string;\n /** Per-request timeout in ms (default 4000). The gate targets <500ms p99. */\n timeoutMs?: number;\n /** Retry attempts on network/5xx (default 1). */\n retries?: number;\n agentId?: string;\n}\n\nexport interface CheckInput {\n agentId?: string;\n sessionId?: string;\n action: Action;\n context?: Partial<Context>;\n}\n\n/**\n * Thin, typed client for the Specter decision API. The whole SDK is the \"plug\";\n * the API is the product. Use `guard.check(action, context)` programmatically,\n * or `createClaudeCodeHook(guard)` for the drop-in Claude Code hook.\n */\nexport class Guard {\n constructor(private opts: GuardOptions) {}\n\n async check(input: CheckInput): Promise<DecisionResult> {\n const body = {\n agentId: input.agentId ?? this.opts.agentId ?? 'sdk-agent',\n sessionId: input.sessionId ?? 'sdk-session',\n action: input.action,\n context: {\n userPrompt: input.context?.userPrompt ?? '',\n destinationOrigin: input.context?.destinationOrigin ?? 'unknown',\n sourceRefs: input.context?.sourceRefs ?? [],\n establishedMerchant: input.context?.establishedMerchant,\n },\n };\n\n const retries = this.opts.retries ?? 1;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const res = await fetch(`${this.opts.apiUrl.replace(/\\/$/, '')}/v1/evaluate`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.opts.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4000),\n });\n if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);\n return (await res.json()) as DecisionResult;\n } catch (err) {\n lastErr = err;\n }\n }\n throw new Error(\n `Specter check failed after ${retries + 1} attempts: ${(lastErr as Error).message}`,\n );\n }\n\n /** Convenience: true only when the action is explicitly allowed. */\n async isAllowed(input: CheckInput): Promise<boolean> {\n return (await this.check(input)).decision === 'allow';\n }\n}\n"]}
|
package/index.d.cts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
type ActionType = 'payment' | 'db_write' | 'shell' | 'file' | 'refund' | 'other';
|
|
2
|
+
type DestinationOrigin = 'user_prompt' | 'ingested_content' | 'tool_output' | 'unknown';
|
|
3
|
+
type Decision = 'allow' | 'deny' | 'review';
|
|
4
|
+
interface Action {
|
|
5
|
+
type: ActionType;
|
|
6
|
+
amount?: number;
|
|
7
|
+
currency?: string;
|
|
8
|
+
/** Payee / account / target identifier. */
|
|
9
|
+
destination?: string;
|
|
10
|
+
/** The merchant the action claims it is paying. */
|
|
11
|
+
merchantClaimed?: string;
|
|
12
|
+
/** Merchant category, when known. */
|
|
13
|
+
category?: string;
|
|
14
|
+
/** For shell / db_write / file actions. */
|
|
15
|
+
command?: string;
|
|
16
|
+
/** Whatever the agent actually passed — kept for the audit record. */
|
|
17
|
+
rawInput?: unknown;
|
|
18
|
+
}
|
|
19
|
+
interface Context {
|
|
20
|
+
/** The original human instruction that started the task. */
|
|
21
|
+
userPrompt: string;
|
|
22
|
+
/** Adapter-declared origin of the destination. */
|
|
23
|
+
destinationOrigin: DestinationOrigin;
|
|
24
|
+
/** Provenance breadcrumbs, e.g. ["firecrawl:https://shop.example/item"]. */
|
|
25
|
+
sourceRefs: string[];
|
|
26
|
+
/** The merchant established at task start (before any ingestion), if any. */
|
|
27
|
+
establishedMerchant?: string;
|
|
28
|
+
}
|
|
29
|
+
interface SignalResult {
|
|
30
|
+
id: string;
|
|
31
|
+
score: number;
|
|
32
|
+
verdict: string;
|
|
33
|
+
}
|
|
34
|
+
interface DecisionResult {
|
|
35
|
+
decision: Decision;
|
|
36
|
+
riskScore: number;
|
|
37
|
+
reason: string;
|
|
38
|
+
signals: Record<string, string>;
|
|
39
|
+
signalDetail: SignalResult[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface GuardOptions {
|
|
43
|
+
apiUrl: string;
|
|
44
|
+
apiKey: string;
|
|
45
|
+
/** Per-request timeout in ms (default 4000). The gate targets <500ms p99. */
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
/** Retry attempts on network/5xx (default 1). */
|
|
48
|
+
retries?: number;
|
|
49
|
+
agentId?: string;
|
|
50
|
+
}
|
|
51
|
+
interface CheckInput {
|
|
52
|
+
agentId?: string;
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
action: Action;
|
|
55
|
+
context?: Partial<Context>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Thin, typed client for the Specter decision API. The whole SDK is the "plug";
|
|
59
|
+
* the API is the product. Use `guard.check(action, context)` programmatically,
|
|
60
|
+
* or `createClaudeCodeHook(guard)` for the drop-in Claude Code hook.
|
|
61
|
+
*/
|
|
62
|
+
declare class Guard {
|
|
63
|
+
private opts;
|
|
64
|
+
constructor(opts: GuardOptions);
|
|
65
|
+
check(input: CheckInput): Promise<DecisionResult>;
|
|
66
|
+
/** Convenience: true only when the action is explicitly allowed. */
|
|
67
|
+
isAllowed(input: CheckInput): Promise<boolean>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Claude Code PreToolUse hook payload (subset). */
|
|
71
|
+
interface HookPayload {
|
|
72
|
+
session_id?: string;
|
|
73
|
+
tool_name?: string;
|
|
74
|
+
tool_input?: Record<string, unknown>;
|
|
75
|
+
specter?: {
|
|
76
|
+
userPrompt?: string;
|
|
77
|
+
establishedMerchant?: string;
|
|
78
|
+
destinationOrigin?: string;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** The body Claude Code requires to actually block. */
|
|
82
|
+
interface HookResponse {
|
|
83
|
+
hookSpecificOutput: {
|
|
84
|
+
hookEventName: 'PreToolUse';
|
|
85
|
+
permissionDecision: 'allow' | 'deny' | 'ask';
|
|
86
|
+
permissionDecisionReason: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build a ready-made Claude Code PreToolUse hook handler from a Guard.
|
|
91
|
+
*
|
|
92
|
+
* IMPORTANT: when wired as an HTTP hook, Claude Code only blocks on an HTTP 200
|
|
93
|
+
* whose body says permissionDecision: "deny". A non-2xx status merely logs and
|
|
94
|
+
* the tool runs anyway. This handler returns the correct body; the host (the
|
|
95
|
+
* decision API's /hooks/claude-code route, or your own server) must answer 200.
|
|
96
|
+
*/
|
|
97
|
+
declare function createClaudeCodeHook(guard: Guard): (payload: HookPayload) => Promise<HookResponse>;
|
|
98
|
+
|
|
99
|
+
export { type Action, type ActionType, type CheckInput, type Context, type Decision, type DecisionResult, type DestinationOrigin, Guard, type GuardOptions, type HookPayload, type HookResponse, type SignalResult, createClaudeCodeHook };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
type ActionType = 'payment' | 'db_write' | 'shell' | 'file' | 'refund' | 'other';
|
|
2
|
+
type DestinationOrigin = 'user_prompt' | 'ingested_content' | 'tool_output' | 'unknown';
|
|
3
|
+
type Decision = 'allow' | 'deny' | 'review';
|
|
4
|
+
interface Action {
|
|
5
|
+
type: ActionType;
|
|
6
|
+
amount?: number;
|
|
7
|
+
currency?: string;
|
|
8
|
+
/** Payee / account / target identifier. */
|
|
9
|
+
destination?: string;
|
|
10
|
+
/** The merchant the action claims it is paying. */
|
|
11
|
+
merchantClaimed?: string;
|
|
12
|
+
/** Merchant category, when known. */
|
|
13
|
+
category?: string;
|
|
14
|
+
/** For shell / db_write / file actions. */
|
|
15
|
+
command?: string;
|
|
16
|
+
/** Whatever the agent actually passed — kept for the audit record. */
|
|
17
|
+
rawInput?: unknown;
|
|
18
|
+
}
|
|
19
|
+
interface Context {
|
|
20
|
+
/** The original human instruction that started the task. */
|
|
21
|
+
userPrompt: string;
|
|
22
|
+
/** Adapter-declared origin of the destination. */
|
|
23
|
+
destinationOrigin: DestinationOrigin;
|
|
24
|
+
/** Provenance breadcrumbs, e.g. ["firecrawl:https://shop.example/item"]. */
|
|
25
|
+
sourceRefs: string[];
|
|
26
|
+
/** The merchant established at task start (before any ingestion), if any. */
|
|
27
|
+
establishedMerchant?: string;
|
|
28
|
+
}
|
|
29
|
+
interface SignalResult {
|
|
30
|
+
id: string;
|
|
31
|
+
score: number;
|
|
32
|
+
verdict: string;
|
|
33
|
+
}
|
|
34
|
+
interface DecisionResult {
|
|
35
|
+
decision: Decision;
|
|
36
|
+
riskScore: number;
|
|
37
|
+
reason: string;
|
|
38
|
+
signals: Record<string, string>;
|
|
39
|
+
signalDetail: SignalResult[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface GuardOptions {
|
|
43
|
+
apiUrl: string;
|
|
44
|
+
apiKey: string;
|
|
45
|
+
/** Per-request timeout in ms (default 4000). The gate targets <500ms p99. */
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
/** Retry attempts on network/5xx (default 1). */
|
|
48
|
+
retries?: number;
|
|
49
|
+
agentId?: string;
|
|
50
|
+
}
|
|
51
|
+
interface CheckInput {
|
|
52
|
+
agentId?: string;
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
action: Action;
|
|
55
|
+
context?: Partial<Context>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Thin, typed client for the Specter decision API. The whole SDK is the "plug";
|
|
59
|
+
* the API is the product. Use `guard.check(action, context)` programmatically,
|
|
60
|
+
* or `createClaudeCodeHook(guard)` for the drop-in Claude Code hook.
|
|
61
|
+
*/
|
|
62
|
+
declare class Guard {
|
|
63
|
+
private opts;
|
|
64
|
+
constructor(opts: GuardOptions);
|
|
65
|
+
check(input: CheckInput): Promise<DecisionResult>;
|
|
66
|
+
/** Convenience: true only when the action is explicitly allowed. */
|
|
67
|
+
isAllowed(input: CheckInput): Promise<boolean>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Claude Code PreToolUse hook payload (subset). */
|
|
71
|
+
interface HookPayload {
|
|
72
|
+
session_id?: string;
|
|
73
|
+
tool_name?: string;
|
|
74
|
+
tool_input?: Record<string, unknown>;
|
|
75
|
+
specter?: {
|
|
76
|
+
userPrompt?: string;
|
|
77
|
+
establishedMerchant?: string;
|
|
78
|
+
destinationOrigin?: string;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** The body Claude Code requires to actually block. */
|
|
82
|
+
interface HookResponse {
|
|
83
|
+
hookSpecificOutput: {
|
|
84
|
+
hookEventName: 'PreToolUse';
|
|
85
|
+
permissionDecision: 'allow' | 'deny' | 'ask';
|
|
86
|
+
permissionDecisionReason: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build a ready-made Claude Code PreToolUse hook handler from a Guard.
|
|
91
|
+
*
|
|
92
|
+
* IMPORTANT: when wired as an HTTP hook, Claude Code only blocks on an HTTP 200
|
|
93
|
+
* whose body says permissionDecision: "deny". A non-2xx status merely logs and
|
|
94
|
+
* the tool runs anyway. This handler returns the correct body; the host (the
|
|
95
|
+
* decision API's /hooks/claude-code route, or your own server) must answer 200.
|
|
96
|
+
*/
|
|
97
|
+
declare function createClaudeCodeHook(guard: Guard): (payload: HookPayload) => Promise<HookResponse>;
|
|
98
|
+
|
|
99
|
+
export { type Action, type ActionType, type CheckInput, type Context, type Decision, type DecisionResult, type DestinationOrigin, Guard, type GuardOptions, type HookPayload, type HookResponse, type SignalResult, createClaudeCodeHook };
|
package/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/claude-code-hook.ts
|
|
2
|
+
function createClaudeCodeHook(guard) {
|
|
3
|
+
return async function handle(payload) {
|
|
4
|
+
try {
|
|
5
|
+
const action = normalize(payload);
|
|
6
|
+
const result = await guard.check({
|
|
7
|
+
agentId: `claude-code:${payload.tool_name ?? "tool"}`,
|
|
8
|
+
sessionId: payload.session_id ?? "cc-session",
|
|
9
|
+
action,
|
|
10
|
+
context: {
|
|
11
|
+
userPrompt: payload.specter?.userPrompt ?? "",
|
|
12
|
+
establishedMerchant: payload.specter?.establishedMerchant,
|
|
13
|
+
destinationOrigin: payload.specter?.destinationOrigin ?? "unknown"
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const permissionDecision = result.decision === "allow" ? "allow" : result.decision === "deny" ? "deny" : "ask";
|
|
17
|
+
return resp(permissionDecision, result.reason);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return resp("ask", `Specter unavailable, escalating: ${err.message}`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function resp(permissionDecision, permissionDecisionReason) {
|
|
24
|
+
return {
|
|
25
|
+
hookSpecificOutput: {
|
|
26
|
+
hookEventName: "PreToolUse",
|
|
27
|
+
permissionDecision,
|
|
28
|
+
permissionDecisionReason
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function normalize(p) {
|
|
33
|
+
const tool = p.tool_name ?? "unknown";
|
|
34
|
+
const ti = p.tool_input ?? {};
|
|
35
|
+
const amount = typeof ti.amount === "number" ? ti.amount : void 0;
|
|
36
|
+
const destination = str(ti.destination) ?? str(ti.to) ?? str(ti.account) ?? void 0;
|
|
37
|
+
let type = "other";
|
|
38
|
+
let command;
|
|
39
|
+
if (amount != null || destination) type = "payment";
|
|
40
|
+
else if (tool === "Bash") {
|
|
41
|
+
type = "shell";
|
|
42
|
+
command = str(ti.command);
|
|
43
|
+
} else if (["Read", "Write", "Edit", "NotebookEdit"].includes(tool)) {
|
|
44
|
+
type = "file";
|
|
45
|
+
command = str(ti.file_path) ?? str(ti.path);
|
|
46
|
+
} else command = JSON.stringify(ti);
|
|
47
|
+
return {
|
|
48
|
+
type,
|
|
49
|
+
amount,
|
|
50
|
+
currency: str(ti.currency),
|
|
51
|
+
destination,
|
|
52
|
+
merchantClaimed: str(ti.merchantClaimed),
|
|
53
|
+
command,
|
|
54
|
+
rawInput: ti
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function str(v) {
|
|
58
|
+
return typeof v === "string" && v.length ? v : void 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/guard.ts
|
|
62
|
+
var Guard = class {
|
|
63
|
+
constructor(opts) {
|
|
64
|
+
this.opts = opts;
|
|
65
|
+
}
|
|
66
|
+
opts;
|
|
67
|
+
async check(input) {
|
|
68
|
+
const body = {
|
|
69
|
+
agentId: input.agentId ?? this.opts.agentId ?? "sdk-agent",
|
|
70
|
+
sessionId: input.sessionId ?? "sdk-session",
|
|
71
|
+
action: input.action,
|
|
72
|
+
context: {
|
|
73
|
+
userPrompt: input.context?.userPrompt ?? "",
|
|
74
|
+
destinationOrigin: input.context?.destinationOrigin ?? "unknown",
|
|
75
|
+
sourceRefs: input.context?.sourceRefs ?? [],
|
|
76
|
+
establishedMerchant: input.context?.establishedMerchant
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const retries = this.opts.retries ?? 1;
|
|
80
|
+
let lastErr;
|
|
81
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`${this.opts.apiUrl.replace(/\/$/, "")}/v1/evaluate`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"content-type": "application/json",
|
|
87
|
+
authorization: `Bearer ${this.opts.apiKey}`
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify(body),
|
|
90
|
+
signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4e3)
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);
|
|
93
|
+
return await res.json();
|
|
94
|
+
} catch (err) {
|
|
95
|
+
lastErr = err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Specter check failed after ${retries + 1} attempts: ${lastErr.message}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
/** Convenience: true only when the action is explicitly allowed. */
|
|
103
|
+
async isAllowed(input) {
|
|
104
|
+
return (await this.check(input)).decision === "allow";
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export { Guard, createClaudeCodeHook };
|
|
109
|
+
//# sourceMappingURL=index.js.map
|
|
110
|
+
//# sourceMappingURL=index.js.map
|
package/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/claude-code-hook.ts","../src/guard.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Guard } from './guard.js';\nimport type { Action, ActionType, DestinationOrigin } from './types.js';\n\n/** Claude Code PreToolUse hook payload (subset). */\nexport interface HookPayload {\n session_id?: string;\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n specter?: { userPrompt?: string; establishedMerchant?: string; destinationOrigin?: string };\n}\n\n/** The body Claude Code requires to actually block. */\nexport interface HookResponse {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse';\n permissionDecision: 'allow' | 'deny' | 'ask';\n permissionDecisionReason: string;\n };\n}\n\n/**\n * Build a ready-made Claude Code PreToolUse hook handler from a Guard.\n *\n * IMPORTANT: when wired as an HTTP hook, Claude Code only blocks on an HTTP 200\n * whose body says permissionDecision: \"deny\". A non-2xx status merely logs and\n * the tool runs anyway. This handler returns the correct body; the host (the\n * decision API's /hooks/claude-code route, or your own server) must answer 200.\n */\nexport function createClaudeCodeHook(guard: Guard) {\n return async function handle(payload: HookPayload): Promise<HookResponse> {\n try {\n const action = normalize(payload);\n const result = await guard.check({\n agentId: `claude-code:${payload.tool_name ?? 'tool'}`,\n sessionId: payload.session_id ?? 'cc-session',\n action,\n context: {\n userPrompt: payload.specter?.userPrompt ?? '',\n establishedMerchant: payload.specter?.establishedMerchant,\n destinationOrigin: (payload.specter?.destinationOrigin as DestinationOrigin) ?? 'unknown',\n },\n });\n const permissionDecision =\n result.decision === 'allow' ? 'allow' : result.decision === 'deny' ? 'deny' : 'ask';\n return resp(permissionDecision, result.reason);\n } catch (err) {\n // Fail safe: escalate to a human rather than silently allowing.\n return resp('ask', `Specter unavailable, escalating: ${(err as Error).message}`);\n }\n };\n}\n\nfunction resp(\n permissionDecision: 'allow' | 'deny' | 'ask',\n permissionDecisionReason: string,\n): HookResponse {\n return {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision,\n permissionDecisionReason,\n },\n };\n}\n\nfunction normalize(p: HookPayload): Action {\n const tool = p.tool_name ?? 'unknown';\n const ti = p.tool_input ?? {};\n const amount = typeof ti.amount === 'number' ? ti.amount : undefined;\n const destination = str(ti.destination) ?? str(ti.to) ?? str(ti.account) ?? undefined;\n\n let type: ActionType = 'other';\n let command: string | undefined;\n if (amount != null || destination) type = 'payment';\n else if (tool === 'Bash') {\n type = 'shell';\n command = str(ti.command);\n } else if (['Read', 'Write', 'Edit', 'NotebookEdit'].includes(tool)) {\n type = 'file';\n command = str(ti.file_path) ?? str(ti.path);\n } else command = JSON.stringify(ti);\n\n return {\n type,\n amount,\n currency: str(ti.currency),\n destination,\n merchantClaimed: str(ti.merchantClaimed),\n command,\n rawInput: ti,\n };\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === 'string' && v.length ? v : undefined;\n}\n","import type { Action, Context, DecisionResult } from './types.js';\n\nexport interface GuardOptions {\n apiUrl: string;\n apiKey: string;\n /** Per-request timeout in ms (default 4000). The gate targets <500ms p99. */\n timeoutMs?: number;\n /** Retry attempts on network/5xx (default 1). */\n retries?: number;\n agentId?: string;\n}\n\nexport interface CheckInput {\n agentId?: string;\n sessionId?: string;\n action: Action;\n context?: Partial<Context>;\n}\n\n/**\n * Thin, typed client for the Specter decision API. The whole SDK is the \"plug\";\n * the API is the product. Use `guard.check(action, context)` programmatically,\n * or `createClaudeCodeHook(guard)` for the drop-in Claude Code hook.\n */\nexport class Guard {\n constructor(private opts: GuardOptions) {}\n\n async check(input: CheckInput): Promise<DecisionResult> {\n const body = {\n agentId: input.agentId ?? this.opts.agentId ?? 'sdk-agent',\n sessionId: input.sessionId ?? 'sdk-session',\n action: input.action,\n context: {\n userPrompt: input.context?.userPrompt ?? '',\n destinationOrigin: input.context?.destinationOrigin ?? 'unknown',\n sourceRefs: input.context?.sourceRefs ?? [],\n establishedMerchant: input.context?.establishedMerchant,\n },\n };\n\n const retries = this.opts.retries ?? 1;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const res = await fetch(`${this.opts.apiUrl.replace(/\\/$/, '')}/v1/evaluate`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.opts.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4000),\n });\n if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);\n return (await res.json()) as DecisionResult;\n } catch (err) {\n lastErr = err;\n }\n }\n throw new Error(\n `Specter check failed after ${retries + 1} attempts: ${(lastErr as Error).message}`,\n );\n }\n\n /** Convenience: true only when the action is explicitly allowed. */\n async isAllowed(input: CheckInput): Promise<boolean> {\n return (await this.check(input)).decision === 'allow';\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "specter-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thin client for Specter — the detect → block → prove firewall / decision API for AI-agent payments. Guard.check() + a drop-in Claude Code hook. Zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.cjs",
|
|
7
|
+
"module": "./index.js",
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": { "types": "./index.d.ts", "default": "./index.js" },
|
|
12
|
+
"require": { "types": "./index.d.cts", "default": "./index.cjs" }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://specter-ia.vercel.app",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Eras256/Specter.git",
|
|
21
|
+
"directory": "packages/sdk"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"ai-agents",
|
|
25
|
+
"agent-security",
|
|
26
|
+
"payments",
|
|
27
|
+
"prompt-injection",
|
|
28
|
+
"firewall",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"specter"
|
|
31
|
+
],
|
|
32
|
+
"engines": { "node": ">=18" },
|
|
33
|
+
"publishConfig": { "access": "public" }
|
|
34
|
+
}
|