specter-sdk 0.1.0 → 0.2.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 +75 -34
- package/index.cjs +24 -0
- package/index.cjs.map +1 -1
- package/index.d.cts +39 -2
- package/index.d.ts +39 -2
- package/index.js +24 -0
- package/index.js.map +1 -1
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -1,73 +1,114 @@
|
|
|
1
1
|
# specter-sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
The
|
|
3
|
+
The thin "plug" that connects an agent's actions to the
|
|
4
|
+
[Specter](https://specter-ia.vercel.app) decision API — *detect → block → prove*
|
|
5
|
+
for AI-agent payments. The API is the product; this is ~150 lines of glue.
|
|
6
|
+
**Zero dependencies.**
|
|
6
7
|
|
|
7
8
|
```bash
|
|
8
9
|
npm i specter-sdk
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## Programmatic gate
|
|
12
13
|
|
|
13
14
|
```ts
|
|
14
15
|
import { Guard } from 'specter-sdk';
|
|
15
16
|
|
|
16
17
|
const guard = new Guard({
|
|
17
|
-
apiUrl:
|
|
18
|
-
apiKey: process.env.SPECTER_API_KEY!,
|
|
18
|
+
apiUrl: process.env.SPECTER_API_URL!, // e.g. https://specter-decision-api.fly.dev
|
|
19
|
+
apiKey: process.env.SPECTER_API_KEY!,
|
|
19
20
|
});
|
|
20
21
|
|
|
21
|
-
const
|
|
22
|
+
const decision = await guard.check({
|
|
22
23
|
agentId: 'shop-agent',
|
|
24
|
+
sessionId: 'sess_42',
|
|
23
25
|
action: {
|
|
24
26
|
type: 'payment',
|
|
25
27
|
amount: 79.99,
|
|
26
28
|
currency: 'USD',
|
|
27
|
-
destination: '
|
|
29
|
+
destination: 'acct_acme_store',
|
|
28
30
|
merchantClaimed: 'Acme Store',
|
|
31
|
+
rawInput: {},
|
|
29
32
|
},
|
|
30
33
|
context: {
|
|
31
|
-
userPrompt: 'Buy the Acme mouse from Acme Store',
|
|
32
|
-
destinationOrigin: '
|
|
33
|
-
sourceRefs: ['firecrawl:https://shop.example/acme'],
|
|
34
|
+
userPrompt: 'Buy the Acme mouse from Acme Store under $100.',
|
|
35
|
+
destinationOrigin: 'user_prompt', // or 'ingested_content' if it came from a scraped page
|
|
34
36
|
establishedMerchant: 'Acme Store',
|
|
35
37
|
},
|
|
36
38
|
});
|
|
37
39
|
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
// safe to execute the payment
|
|
40
|
+
if (decision.decision !== 'allow') throw new Error(decision.reason);
|
|
41
|
+
// ...only now move money.
|
|
42
|
+
```
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
## Claude Code hook (drop-in)
|
|
45
|
+
|
|
46
|
+
### Option A — HTTP hook straight to the deployed API (no code)
|
|
47
|
+
|
|
48
|
+
Point Claude Code's PreToolUse hook at the API's `/hooks/claude-code` route.
|
|
49
|
+
`.claude/settings.json`:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"hooks": {
|
|
54
|
+
"PreToolUse": [
|
|
55
|
+
{
|
|
56
|
+
"matcher": "*",
|
|
57
|
+
"hooks": [
|
|
58
|
+
{
|
|
59
|
+
"type": "http",
|
|
60
|
+
"url": "https://specter-decision-api.fly.dev/hooks/claude-code",
|
|
61
|
+
"headers": { "x-api-key": "$SPECTER_API_KEY" },
|
|
62
|
+
"allowedEnvVars": ["SPECTER_API_KEY"],
|
|
63
|
+
"timeout": 5
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
46
70
|
```
|
|
47
71
|
|
|
48
|
-
|
|
72
|
+
> **The gotcha (do not "fix" this):** an HTTP hook does **not** block via status
|
|
73
|
+
> code. A 4xx/5xx only logs an error and the tool runs anyway. To actually block,
|
|
74
|
+
> the endpoint returns **HTTP 200** with `permissionDecision: "deny"` in the body.
|
|
75
|
+
> The `/hooks/claude-code` route already does this; `createClaudeCodeHook` builds
|
|
76
|
+
> the same body if you host it yourself. (Command-type hooks block with exit code
|
|
77
|
+
> 2; exit 1 only warns.)
|
|
78
|
+
|
|
79
|
+
### Option B — host the handler yourself
|
|
49
80
|
|
|
50
81
|
```ts
|
|
51
82
|
import { Guard, createClaudeCodeHook } from 'specter-sdk';
|
|
52
83
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
);
|
|
84
|
+
const guard = new Guard({ apiUrl, apiKey });
|
|
85
|
+
const handle = createClaudeCodeHook(guard);
|
|
56
86
|
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
87
|
+
// in your tiny HTTP server, always answer 200:
|
|
88
|
+
const body = await handle(await req.json());
|
|
89
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
90
|
+
res.end(JSON.stringify(body));
|
|
60
91
|
```
|
|
61
92
|
|
|
62
|
-
|
|
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.
|
|
93
|
+
## Decision contract
|
|
65
94
|
|
|
66
|
-
|
|
95
|
+
`guard.check` → `POST /v1/evaluate` →
|
|
67
96
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
```ts
|
|
98
|
+
{ decision: 'allow' | 'deny' | 'review',
|
|
99
|
+
riskScore: number, // 0..1
|
|
100
|
+
reason: string,
|
|
101
|
+
signals: Record<string,string>,
|
|
102
|
+
signalDetail: { id, score, verdict }[],
|
|
103
|
+
audit?: { seq: number, hash: string } } // the tamper-evident chain row it was written to
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Proof (the "prove" pillar)
|
|
107
|
+
|
|
108
|
+
Every decision is appended to a tamper-evident hash chain. Read it back, or
|
|
109
|
+
verify it end-to-end, with the same client:
|
|
72
110
|
|
|
73
|
-
|
|
111
|
+
```ts
|
|
112
|
+
const log = await guard.audit({ limit: 10, order: 'desc' }); // newest first, each with its hash
|
|
113
|
+
const { valid, brokenAt } = await guard.verify(); // false + index if any row was rewritten
|
|
114
|
+
```
|
package/index.cjs
CHANGED
|
@@ -105,6 +105,30 @@ var Guard = class {
|
|
|
105
105
|
async isAllowed(input) {
|
|
106
106
|
return (await this.check(input)).decision === "allow";
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Read the tamper-evident decision log (the "prove" pillar). Pass
|
|
110
|
+
* `{ order: 'desc' }` for newest-first.
|
|
111
|
+
*/
|
|
112
|
+
async audit(opts = {}) {
|
|
113
|
+
const q = new URLSearchParams();
|
|
114
|
+
if (opts.limit != null) q.set("limit", String(opts.limit));
|
|
115
|
+
if (opts.order) q.set("order", opts.order);
|
|
116
|
+
const qs = q.toString();
|
|
117
|
+
const data = await this.get(`/v1/audit${qs ? `?${qs}` : ""}`);
|
|
118
|
+
return data.records ?? [];
|
|
119
|
+
}
|
|
120
|
+
/** Verify the chain is intact end-to-end (proof nothing was rewritten). */
|
|
121
|
+
async verify() {
|
|
122
|
+
return this.get("/v1/audit/verify");
|
|
123
|
+
}
|
|
124
|
+
async get(path) {
|
|
125
|
+
const res = await fetch(`${this.opts.apiUrl.replace(/\/$/, "")}${path}`, {
|
|
126
|
+
headers: { authorization: `Bearer ${this.opts.apiKey}` },
|
|
127
|
+
signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4e3)
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);
|
|
130
|
+
return await res.json();
|
|
131
|
+
}
|
|
108
132
|
};
|
|
109
133
|
|
|
110
134
|
exports.Guard = Guard;
|
package/index.cjs.map
CHANGED
|
@@ -1 +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"]}
|
|
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, AuditRecord, Context, EvaluateResult, VerifyResult } 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<EvaluateResult> {\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 EvaluateResult;\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 /**\n * Read the tamper-evident decision log (the \"prove\" pillar). Pass\n * `{ order: 'desc' }` for newest-first.\n */\n async audit(opts: { limit?: number; order?: 'asc' | 'desc' } = {}): Promise<AuditRecord[]> {\n const q = new URLSearchParams();\n if (opts.limit != null) q.set('limit', String(opts.limit));\n if (opts.order) q.set('order', opts.order);\n const qs = q.toString();\n const data = await this.get<{ records: AuditRecord[] }>(`/v1/audit${qs ? `?${qs}` : ''}`);\n return data.records ?? [];\n }\n\n /** Verify the chain is intact end-to-end (proof nothing was rewritten). */\n async verify(): Promise<VerifyResult> {\n return this.get<VerifyResult>('/v1/audit/verify');\n }\n\n private async get<T>(path: string): Promise<T> {\n const res = await fetch(`${this.opts.apiUrl.replace(/\\/$/, '')}${path}`, {\n headers: { authorization: `Bearer ${this.opts.apiKey}` },\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 T;\n }\n}\n"]}
|
package/index.d.cts
CHANGED
|
@@ -38,6 +38,32 @@ interface DecisionResult {
|
|
|
38
38
|
signals: Record<string, string>;
|
|
39
39
|
signalDetail: SignalResult[];
|
|
40
40
|
}
|
|
41
|
+
/** Pointer to the tamper-evident chain row a decision was written to. */
|
|
42
|
+
interface AuditRef {
|
|
43
|
+
/** Position of this decision in the append-only chain. */
|
|
44
|
+
seq: number;
|
|
45
|
+
/** SHA-256 hash linking this record to the previous one. */
|
|
46
|
+
hash: string;
|
|
47
|
+
}
|
|
48
|
+
/** What `Guard.check` returns: the decision plus the proof row it was recorded as. */
|
|
49
|
+
interface EvaluateResult extends DecisionResult {
|
|
50
|
+
/** The tamper-evident chain row this decision was recorded as (when the API persists it). */
|
|
51
|
+
audit?: AuditRef;
|
|
52
|
+
}
|
|
53
|
+
/** A row from the append-only audit chain (`GET /v1/audit`). */
|
|
54
|
+
interface AuditRecord {
|
|
55
|
+
seq: number;
|
|
56
|
+
record: Record<string, unknown>;
|
|
57
|
+
prev_hash: string;
|
|
58
|
+
hash: string;
|
|
59
|
+
}
|
|
60
|
+
/** Result of verifying the chain (`GET /v1/audit/verify`). */
|
|
61
|
+
interface VerifyResult {
|
|
62
|
+
valid: boolean;
|
|
63
|
+
/** First index where the chain diverges, if any. */
|
|
64
|
+
brokenAt?: number;
|
|
65
|
+
reason?: string;
|
|
66
|
+
}
|
|
41
67
|
|
|
42
68
|
interface GuardOptions {
|
|
43
69
|
apiUrl: string;
|
|
@@ -62,9 +88,20 @@ interface CheckInput {
|
|
|
62
88
|
declare class Guard {
|
|
63
89
|
private opts;
|
|
64
90
|
constructor(opts: GuardOptions);
|
|
65
|
-
check(input: CheckInput): Promise<
|
|
91
|
+
check(input: CheckInput): Promise<EvaluateResult>;
|
|
66
92
|
/** Convenience: true only when the action is explicitly allowed. */
|
|
67
93
|
isAllowed(input: CheckInput): Promise<boolean>;
|
|
94
|
+
/**
|
|
95
|
+
* Read the tamper-evident decision log (the "prove" pillar). Pass
|
|
96
|
+
* `{ order: 'desc' }` for newest-first.
|
|
97
|
+
*/
|
|
98
|
+
audit(opts?: {
|
|
99
|
+
limit?: number;
|
|
100
|
+
order?: 'asc' | 'desc';
|
|
101
|
+
}): Promise<AuditRecord[]>;
|
|
102
|
+
/** Verify the chain is intact end-to-end (proof nothing was rewritten). */
|
|
103
|
+
verify(): Promise<VerifyResult>;
|
|
104
|
+
private get;
|
|
68
105
|
}
|
|
69
106
|
|
|
70
107
|
/** Claude Code PreToolUse hook payload (subset). */
|
|
@@ -96,4 +133,4 @@ interface HookResponse {
|
|
|
96
133
|
*/
|
|
97
134
|
declare function createClaudeCodeHook(guard: Guard): (payload: HookPayload) => Promise<HookResponse>;
|
|
98
135
|
|
|
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 };
|
|
136
|
+
export { type Action, type ActionType, type AuditRecord, type AuditRef, type CheckInput, type Context, type Decision, type DecisionResult, type DestinationOrigin, type EvaluateResult, Guard, type GuardOptions, type HookPayload, type HookResponse, type SignalResult, type VerifyResult, createClaudeCodeHook };
|
package/index.d.ts
CHANGED
|
@@ -38,6 +38,32 @@ interface DecisionResult {
|
|
|
38
38
|
signals: Record<string, string>;
|
|
39
39
|
signalDetail: SignalResult[];
|
|
40
40
|
}
|
|
41
|
+
/** Pointer to the tamper-evident chain row a decision was written to. */
|
|
42
|
+
interface AuditRef {
|
|
43
|
+
/** Position of this decision in the append-only chain. */
|
|
44
|
+
seq: number;
|
|
45
|
+
/** SHA-256 hash linking this record to the previous one. */
|
|
46
|
+
hash: string;
|
|
47
|
+
}
|
|
48
|
+
/** What `Guard.check` returns: the decision plus the proof row it was recorded as. */
|
|
49
|
+
interface EvaluateResult extends DecisionResult {
|
|
50
|
+
/** The tamper-evident chain row this decision was recorded as (when the API persists it). */
|
|
51
|
+
audit?: AuditRef;
|
|
52
|
+
}
|
|
53
|
+
/** A row from the append-only audit chain (`GET /v1/audit`). */
|
|
54
|
+
interface AuditRecord {
|
|
55
|
+
seq: number;
|
|
56
|
+
record: Record<string, unknown>;
|
|
57
|
+
prev_hash: string;
|
|
58
|
+
hash: string;
|
|
59
|
+
}
|
|
60
|
+
/** Result of verifying the chain (`GET /v1/audit/verify`). */
|
|
61
|
+
interface VerifyResult {
|
|
62
|
+
valid: boolean;
|
|
63
|
+
/** First index where the chain diverges, if any. */
|
|
64
|
+
brokenAt?: number;
|
|
65
|
+
reason?: string;
|
|
66
|
+
}
|
|
41
67
|
|
|
42
68
|
interface GuardOptions {
|
|
43
69
|
apiUrl: string;
|
|
@@ -62,9 +88,20 @@ interface CheckInput {
|
|
|
62
88
|
declare class Guard {
|
|
63
89
|
private opts;
|
|
64
90
|
constructor(opts: GuardOptions);
|
|
65
|
-
check(input: CheckInput): Promise<
|
|
91
|
+
check(input: CheckInput): Promise<EvaluateResult>;
|
|
66
92
|
/** Convenience: true only when the action is explicitly allowed. */
|
|
67
93
|
isAllowed(input: CheckInput): Promise<boolean>;
|
|
94
|
+
/**
|
|
95
|
+
* Read the tamper-evident decision log (the "prove" pillar). Pass
|
|
96
|
+
* `{ order: 'desc' }` for newest-first.
|
|
97
|
+
*/
|
|
98
|
+
audit(opts?: {
|
|
99
|
+
limit?: number;
|
|
100
|
+
order?: 'asc' | 'desc';
|
|
101
|
+
}): Promise<AuditRecord[]>;
|
|
102
|
+
/** Verify the chain is intact end-to-end (proof nothing was rewritten). */
|
|
103
|
+
verify(): Promise<VerifyResult>;
|
|
104
|
+
private get;
|
|
68
105
|
}
|
|
69
106
|
|
|
70
107
|
/** Claude Code PreToolUse hook payload (subset). */
|
|
@@ -96,4 +133,4 @@ interface HookResponse {
|
|
|
96
133
|
*/
|
|
97
134
|
declare function createClaudeCodeHook(guard: Guard): (payload: HookPayload) => Promise<HookResponse>;
|
|
98
135
|
|
|
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 };
|
|
136
|
+
export { type Action, type ActionType, type AuditRecord, type AuditRef, type CheckInput, type Context, type Decision, type DecisionResult, type DestinationOrigin, type EvaluateResult, Guard, type GuardOptions, type HookPayload, type HookResponse, type SignalResult, type VerifyResult, createClaudeCodeHook };
|
package/index.js
CHANGED
|
@@ -103,6 +103,30 @@ var Guard = class {
|
|
|
103
103
|
async isAllowed(input) {
|
|
104
104
|
return (await this.check(input)).decision === "allow";
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Read the tamper-evident decision log (the "prove" pillar). Pass
|
|
108
|
+
* `{ order: 'desc' }` for newest-first.
|
|
109
|
+
*/
|
|
110
|
+
async audit(opts = {}) {
|
|
111
|
+
const q = new URLSearchParams();
|
|
112
|
+
if (opts.limit != null) q.set("limit", String(opts.limit));
|
|
113
|
+
if (opts.order) q.set("order", opts.order);
|
|
114
|
+
const qs = q.toString();
|
|
115
|
+
const data = await this.get(`/v1/audit${qs ? `?${qs}` : ""}`);
|
|
116
|
+
return data.records ?? [];
|
|
117
|
+
}
|
|
118
|
+
/** Verify the chain is intact end-to-end (proof nothing was rewritten). */
|
|
119
|
+
async verify() {
|
|
120
|
+
return this.get("/v1/audit/verify");
|
|
121
|
+
}
|
|
122
|
+
async get(path) {
|
|
123
|
+
const res = await fetch(`${this.opts.apiUrl.replace(/\/$/, "")}${path}`, {
|
|
124
|
+
headers: { authorization: `Bearer ${this.opts.apiKey}` },
|
|
125
|
+
signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4e3)
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);
|
|
128
|
+
return await res.json();
|
|
129
|
+
}
|
|
106
130
|
};
|
|
107
131
|
|
|
108
132
|
export { Guard, createClaudeCodeHook };
|
package/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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, AuditRecord, Context, EvaluateResult, VerifyResult } 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<EvaluateResult> {\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 EvaluateResult;\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 /**\n * Read the tamper-evident decision log (the \"prove\" pillar). Pass\n * `{ order: 'desc' }` for newest-first.\n */\n async audit(opts: { limit?: number; order?: 'asc' | 'desc' } = {}): Promise<AuditRecord[]> {\n const q = new URLSearchParams();\n if (opts.limit != null) q.set('limit', String(opts.limit));\n if (opts.order) q.set('order', opts.order);\n const qs = q.toString();\n const data = await this.get<{ records: AuditRecord[] }>(`/v1/audit${qs ? `?${qs}` : ''}`);\n return data.records ?? [];\n }\n\n /** Verify the chain is intact end-to-end (proof nothing was rewritten). */\n async verify(): Promise<VerifyResult> {\n return this.get<VerifyResult>('/v1/audit/verify');\n }\n\n private async get<T>(path: string): Promise<T> {\n const res = await fetch(`${this.opts.apiUrl.replace(/\\/$/, '')}${path}`, {\n headers: { authorization: `Bearer ${this.opts.apiKey}` },\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 T;\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specter-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./index.cjs",
|
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
"types": "./index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"import": {
|
|
12
|
-
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./index.d.ts",
|
|
13
|
+
"default": "./index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./index.d.cts",
|
|
17
|
+
"default": "./index.cjs"
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
20
|
},
|
|
15
21
|
"sideEffects": false,
|
|
@@ -29,6 +35,10 @@
|
|
|
29
35
|
"claude-code",
|
|
30
36
|
"specter"
|
|
31
37
|
],
|
|
32
|
-
"engines": {
|
|
33
|
-
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
34
44
|
}
|