@ciphyrshq/sdk 2.6.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/package.json +24 -6
- package/src/client.js +326 -20
- package/src/context.js +82 -0
- package/src/fail-posture.js +100 -0
- package/src/index.js +14 -0
- package/src/propagation.js +388 -0
- package/src/protect-tool.js +362 -0
- package/src/secret-detector.js +21 -3
- package/src/tracer.js +278 -9
- package/types.d.ts +265 -5
package/types.d.ts
CHANGED
|
@@ -83,9 +83,22 @@ export interface WaitForJobOptions {
|
|
|
83
83
|
export interface ProtectOptions extends MaskOptions {
|
|
84
84
|
/** Default true — purge vault after restore (data minimisation). */
|
|
85
85
|
purge?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* What to do when the masker is unreachable. DEFAULT FALSE (fail closed):
|
|
88
|
+
* the call throws and your LLM never sees the raw text. `true` sends the
|
|
89
|
+
* RAW, UNMASKED input to your LLM and returns it unrestored with
|
|
90
|
+
* `failedOpen: true`. Falls back to the client-wide `failOpen`.
|
|
91
|
+
*
|
|
92
|
+
* `null` means unset, not "fail open". The legacy `failClosed` key is NOT
|
|
93
|
+
* read here — it has never done anything on scan.protect and still does
|
|
94
|
+
* not, because on this surface turning fail-closed off means sending
|
|
95
|
+
* unmasked PII to your model.
|
|
96
|
+
*/
|
|
97
|
+
failOpen?: boolean | null;
|
|
86
98
|
}
|
|
87
99
|
export interface ProtectContext {
|
|
88
|
-
|
|
100
|
+
/** null only on the failOpen path, where no vault session was ever created. */
|
|
101
|
+
sessionId: string | null;
|
|
89
102
|
entitiesFound: any[];
|
|
90
103
|
}
|
|
91
104
|
export interface ProtectResult {
|
|
@@ -95,9 +108,15 @@ export interface ProtectResult {
|
|
|
95
108
|
maskedInput: string;
|
|
96
109
|
/** What the LLM returned, before unmask (audit). */
|
|
97
110
|
maskedOutput: string;
|
|
98
|
-
sessionId: string;
|
|
111
|
+
sessionId: string | null;
|
|
99
112
|
entitiesFound: any[];
|
|
100
113
|
tokensRestored: number;
|
|
114
|
+
/**
|
|
115
|
+
* Present only when `failOpen: true` and the masker was unreachable — the
|
|
116
|
+
* text was NOT masked. Absent on every normal round-trip, so auditing
|
|
117
|
+
* "was this really masked?" is a property check, not a guess.
|
|
118
|
+
*/
|
|
119
|
+
failedOpen?: true;
|
|
101
120
|
}
|
|
102
121
|
export type ProtectLLMCall = (masked: string, ctx: ProtectContext) => Promise<string>;
|
|
103
122
|
|
|
@@ -260,6 +279,22 @@ export interface CiphyrsClientOptions {
|
|
|
260
279
|
dashUrl?: string;
|
|
261
280
|
/** Request timeout in milliseconds (default: 10000) */
|
|
262
281
|
timeout?: number;
|
|
282
|
+
/**
|
|
283
|
+
* Fleet-wide failure posture for protectTool, guard.wrap and scan.protect
|
|
284
|
+
* when Ciphyrs cannot be reached.
|
|
285
|
+
*
|
|
286
|
+
* DEFAULT FALSE — FAIL CLOSED: the tool is not executed, the LLM is not
|
|
287
|
+
* called, and the error surfaces. This is a BEHAVIOUR CHANGE for
|
|
288
|
+
* protectTool, which previously failed OPEN and ran the tool unauthorised
|
|
289
|
+
* during an outage while the other two refused to proceed. Set `true` to
|
|
290
|
+
* restore the old behaviour across the client; `{ failOpen: true }` on an
|
|
291
|
+
* individual call overrides this either way. An existing protectTool caller
|
|
292
|
+
* who passed `failClosed: false` still fails open, unchanged — but that
|
|
293
|
+
* legacy key is read on protectTool ONLY, never here.
|
|
294
|
+
*
|
|
295
|
+
* `null` means unset (fail closed), not fail open.
|
|
296
|
+
*/
|
|
297
|
+
failOpen?: boolean | null;
|
|
263
298
|
}
|
|
264
299
|
|
|
265
300
|
// ── V58 Guard ────────────────────────────────────────────────────────────
|
|
@@ -301,13 +336,32 @@ export interface GuardWrapResult {
|
|
|
301
336
|
reason?: string;
|
|
302
337
|
decision: GuardDecision;
|
|
303
338
|
detections?: GuardCheckResult['detections'];
|
|
304
|
-
decision_id: string;
|
|
339
|
+
decision_id: string | null;
|
|
340
|
+
/** Present only when a check was skipped because the guard was unreachable
|
|
341
|
+
* and `failOpen` was set — i.e. this "allow" was never actually decided. */
|
|
342
|
+
failedOpen?: true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The one failure-posture switch, shared by guard.wrap, scan.protect and protectTool. */
|
|
346
|
+
export interface FailPostureOptions {
|
|
347
|
+
/**
|
|
348
|
+
* Run anyway when Ciphyrs is unreachable. DEFAULT FALSE — fail closed:
|
|
349
|
+
* the LLM is not called / the tool is not executed and the error surfaces.
|
|
350
|
+
* Falls back to the client-wide `failOpen` when omitted.
|
|
351
|
+
*
|
|
352
|
+
* `null` is accepted and means UNSET, exactly like omitting the key, so a
|
|
353
|
+
* posture read out of JSON config, a database column or a `{ ...defaults }`
|
|
354
|
+
* spread falls through to the client posture instead of choosing one.
|
|
355
|
+
* Anything the SDK cannot read as an unmistakable yes or no is unset too —
|
|
356
|
+
* the fail-open direction is only ever reached by a value that says so.
|
|
357
|
+
*/
|
|
358
|
+
failOpen?: boolean | null;
|
|
305
359
|
}
|
|
306
360
|
export type GuardLLMCall = (input: string) => Promise<string>;
|
|
307
361
|
|
|
308
362
|
export declare class GuardResource {
|
|
309
363
|
check(params: GuardCheckParams): Promise<GuardCheckResult>;
|
|
310
|
-
wrap(userInput: string, llmCall: GuardLLMCall, opts?: GuardCheckParams): Promise<GuardWrapResult>;
|
|
364
|
+
wrap(userInput: string, llmCall: GuardLLMCall, opts?: GuardCheckParams & FailPostureOptions): Promise<GuardWrapResult>;
|
|
311
365
|
getPolicy(): Promise<{ policy_mode: GuardPolicyMode; modes: any[] }>;
|
|
312
366
|
setPolicy(mode: GuardPolicyMode): Promise<{ policy_mode: GuardPolicyMode }>;
|
|
313
367
|
decisions(opts?: { limit?: number; decision?: GuardDecision }): Promise<{ decisions: any[]; total: number }>;
|
|
@@ -379,4 +433,210 @@ export declare class CiphyrsClient {
|
|
|
379
433
|
maskAsync(text: string, opts?: MaskAsyncOptions): Promise<MaskAsyncResult>;
|
|
380
434
|
waitForJob(jobId: string, opts?: WaitForJobOptions): Promise<JobResult>;
|
|
381
435
|
createKey(opts?: { name?: string; scopes?: string[] }): Promise<{ apiKey: string; key: ApiKey }>;
|
|
382
|
-
|
|
436
|
+
|
|
437
|
+
// ── Agent health (V39 heartbeat, V143 windows + metrics) ────────────────
|
|
438
|
+
reportHeartbeat(args: {
|
|
439
|
+
agentName?: string; agentId?: string;
|
|
440
|
+
status?: AgentHealthStatus; projectName?: string;
|
|
441
|
+
latencyMs?: number | null; errorMessage?: string | null;
|
|
442
|
+
environment?: string; heartbeatIntervalS?: number;
|
|
443
|
+
metrics?: AgentProcessMetrics; metadata?: Record<string, unknown>;
|
|
444
|
+
}): Promise<{ ok: boolean; agent_id?: string; prev_status?: string; new_status?: string; error?: string }>;
|
|
445
|
+
startHealthMonitor(args: {
|
|
446
|
+
agentName?: string; agentId?: string; projectName?: string;
|
|
447
|
+
intervalMs?: number; environment?: string;
|
|
448
|
+
probe?: () => Promise<{ status?: AgentHealthStatus; latencyMs?: number } | void>;
|
|
449
|
+
}): ReturnType<typeof setInterval>;
|
|
450
|
+
stopHealthMonitor(): void;
|
|
451
|
+
setAgentMonitoring(agentId: string, opts: {
|
|
452
|
+
heartbeatTimeoutS?: number | null; probeUrl?: string | null;
|
|
453
|
+
}): Promise<{ monitoring: { id: string; name: string; heartbeat_interval_s: number | null; heartbeat_timeout_s: number | null; probe_url: string | null; probe_status: string | null } }>;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
457
|
+
// Tracing (2.7) — spans, and the cross-process propagation the topology
|
|
458
|
+
// graph is derived from. See src/propagation.js.
|
|
459
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
460
|
+
|
|
461
|
+
export type AgentHealthStatus = 'up' | 'degraded' | 'failing' | 'down' | 'unknown';
|
|
462
|
+
|
|
463
|
+
/** Process metrics shipped with a heartbeat; the server turns a struggling
|
|
464
|
+
* process into `degraded` rather than waiting for it to go silent. */
|
|
465
|
+
export interface AgentProcessMetrics {
|
|
466
|
+
rss_mb?: number; heap_mb?: number; cpu_pct?: number;
|
|
467
|
+
event_loop_lag_ms?: number; thread_lag_ms?: number;
|
|
468
|
+
error_rate?: number; uptime_s?: number;
|
|
469
|
+
spans_since_last?: number; errors_since_last?: number; active_traces?: number;
|
|
470
|
+
runtime?: string; sdk_version?: string; hostname?: string;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export type SpanKind = 'agent' | 'tool' | 'llm_call' | 'chain' | 'retrieval' | 'communication' | 'handoff';
|
|
474
|
+
|
|
475
|
+
export interface SpanOptions {
|
|
476
|
+
/** Explicit parent. Omit and the enclosing span is used automatically. */
|
|
477
|
+
parentSpanId?: string | null;
|
|
478
|
+
agentName?: string;
|
|
479
|
+
kind?: SpanKind;
|
|
480
|
+
/** Opt out of auto-entering ambient context; manage it with span.run(). */
|
|
481
|
+
scoped?: boolean;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export declare class Span {
|
|
485
|
+
readonly spanId: string;
|
|
486
|
+
readonly agentName?: string;
|
|
487
|
+
setInput(text: string): this;
|
|
488
|
+
setOutput(text: string): this;
|
|
489
|
+
setStatus(status: 'ok' | 'error'): this;
|
|
490
|
+
setError(message: string): this;
|
|
491
|
+
setCost(usd: number): this;
|
|
492
|
+
setModel(name: string): this;
|
|
493
|
+
setTokens(prompt: number, completion: number): this;
|
|
494
|
+
setMetadata(meta: Record<string, unknown>): this;
|
|
495
|
+
/** Run `fn` with this span active, then end it (even if `fn` throws). */
|
|
496
|
+
run<T>(fn: (span: Span) => Promise<T> | T): Promise<T>;
|
|
497
|
+
/** Make this span ambient for the rest of the async context; end() unwinds. */
|
|
498
|
+
enter(): this;
|
|
499
|
+
end(): this;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export declare class Trace {
|
|
503
|
+
readonly traceId: string;
|
|
504
|
+
readonly name: string;
|
|
505
|
+
/** True when this trace continues one started by another agent/process. */
|
|
506
|
+
readonly isContinuation: boolean;
|
|
507
|
+
/** The agent that called us, when the inbound request named one. */
|
|
508
|
+
readonly callerAgent: string | null;
|
|
509
|
+
span(name: string, opts?: SpanOptions): Span;
|
|
510
|
+
run<T>(fn: (trace: Trace) => Promise<T> | T): Promise<T>;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export interface TracerOptions {
|
|
514
|
+
projectName: string;
|
|
515
|
+
flushIntervalMs?: number;
|
|
516
|
+
batchSize?: number;
|
|
517
|
+
/** The agent this process runs — visible in the fleet before any traffic. */
|
|
518
|
+
agentName?: string;
|
|
519
|
+
/** Liveness beat for every agent seen. 0 disables. Default 60000. */
|
|
520
|
+
heartbeatIntervalMs?: number;
|
|
521
|
+
heartbeatMetrics?: boolean;
|
|
522
|
+
/** Inject traceparent/baggage into outbound calls. Default true. */
|
|
523
|
+
propagate?: boolean;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export declare class CiphyrsTracer {
|
|
527
|
+
constructor(client: CiphyrsClient, opts: TracerOptions);
|
|
528
|
+
readonly knownAgents: string[];
|
|
529
|
+
/** Start a trace — or continue the caller's, when serving an instrumented request. */
|
|
530
|
+
trace(name: string, opts?: { traceId?: string; headers?: unknown }): Trace;
|
|
531
|
+
withTrace<T>(name: string, fn: (trace: Trace) => Promise<T> | T, opts?: { traceId?: string; headers?: unknown }): Promise<T>;
|
|
532
|
+
emitMetric(name: string, value: number, opts?: { unit?: string; tags?: Record<string, string>; agentName?: string }): void;
|
|
533
|
+
collectMetrics(): AgentProcessMetrics;
|
|
534
|
+
flush(): Promise<void>;
|
|
535
|
+
shutdown(): Promise<void>;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** What an inbound request told us about the trace it belongs to. */
|
|
539
|
+
export interface RemoteTraceContext {
|
|
540
|
+
trace_id: string;
|
|
541
|
+
span_id?: string;
|
|
542
|
+
agent_name?: string;
|
|
543
|
+
project?: string;
|
|
544
|
+
sampled: boolean;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export declare const TRACEPARENT: 'traceparent';
|
|
548
|
+
export declare const BAGGAGE: 'baggage';
|
|
549
|
+
|
|
550
|
+
/** Add traceparent/baggage for the active span. No-op outside a trace. */
|
|
551
|
+
export declare function inject<H extends object>(headers?: H): H;
|
|
552
|
+
/** Read the caller's context from inbound headers (object, Headers, entry array or CGI env). */
|
|
553
|
+
export declare function extract(headers: unknown): RemoteTraceContext | undefined;
|
|
554
|
+
/** Run `fn` as a continuation of the caller's trace. */
|
|
555
|
+
export declare function withRemoteContext<T>(headersOrCtx: unknown, fn: () => T): T;
|
|
556
|
+
export declare function currentContext(): RemoteTraceContext | undefined;
|
|
557
|
+
export declare function instrumentFetch(): boolean;
|
|
558
|
+
export declare function instrumentHttp(): boolean;
|
|
559
|
+
export declare function autoInstrument(): { fetch: boolean; http: boolean };
|
|
560
|
+
export declare function expressMiddleware(): (req: any, res: any, next: () => void) => unknown;
|
|
561
|
+
export declare function fastifyPlugin(app: any): Promise<void>;
|
|
562
|
+
export declare function withPropagation<H extends (req: any, res: any) => any>(handler: H): H;
|
|
563
|
+
export declare function w3cTraceId(traceId: string): string;
|
|
564
|
+
export declare function w3cSpanId(spanId: string): string;
|
|
565
|
+
/** The active trace/span ids — what a guard call attributes itself to. */
|
|
566
|
+
export declare function activeIds(): { trace_id?: string; span_id?: string };
|
|
567
|
+
/**
|
|
568
|
+
* The kind of the innermost LOCAL span, or undefined when none is open.
|
|
569
|
+
* A remote parent has no kind (it does not travel in baggage) and a trace is
|
|
570
|
+
* not a span, so both answer undefined. protectTool uses this to tell whether
|
|
571
|
+
* the span it is inside is the tool span it may name to the gateway.
|
|
572
|
+
*/
|
|
573
|
+
export declare function activeSpanKind(): 'agent' | 'tool' | 'llm' | 'retriever' | string | undefined;
|
|
574
|
+
export declare function activeAgent(): string | undefined;
|
|
575
|
+
export declare function remoteParent(): RemoteTraceContext | undefined;
|
|
576
|
+
|
|
577
|
+
// ── V60 Tool-call authorization ──────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
export declare class ToolBlocked extends Error {
|
|
580
|
+
action: string;
|
|
581
|
+
reason: string;
|
|
582
|
+
ruleId?: string;
|
|
583
|
+
ruleName?: string;
|
|
584
|
+
decisionId?: string;
|
|
585
|
+
constructor(action: string, reason: string, opts?: { ruleId?: string; ruleName?: string; decisionId?: string });
|
|
586
|
+
}
|
|
587
|
+
export declare class ToolApprovalTimeout extends ToolBlocked {}
|
|
588
|
+
|
|
589
|
+
export interface ProtectToolOptions extends FailPostureOptions {
|
|
590
|
+
/** Agent name rules are scoped to. Required. */
|
|
591
|
+
agent: string;
|
|
592
|
+
/** Tool name reported to Ciphyrs (default: the function's name). */
|
|
593
|
+
name?: string;
|
|
594
|
+
/** Shown in the tool inventory / allowlist UI. */
|
|
595
|
+
description?: string;
|
|
596
|
+
/** Approval poll interval in ms (default 1000). */
|
|
597
|
+
pollIntervalMs?: number;
|
|
598
|
+
/** Hard cap on approval polling; defaults to whatever the rule says. */
|
|
599
|
+
maxPollSeconds?: number;
|
|
600
|
+
/**
|
|
601
|
+
* Legacy spelling of `failOpen`, inverted, honoured in BOTH directions:
|
|
602
|
+
* `failClosed: false` still means fail open. Prefer `failOpen`.
|
|
603
|
+
*
|
|
604
|
+
* protectTool is the ONLY surface that reads this key — it is the only one
|
|
605
|
+
* it ever shipped on. On `guard.wrap`, `scan.protect` and the client
|
|
606
|
+
* constructor it is inert, as it has always been; use `failOpen` there.
|
|
607
|
+
*
|
|
608
|
+
* `null` means unset, i.e. the same as omitting it. It used to mean fail
|
|
609
|
+
* OPEN, because `!null` is true — so a posture nobody had set ran the tool
|
|
610
|
+
* during an outage.
|
|
611
|
+
* @deprecated
|
|
612
|
+
*/
|
|
613
|
+
failClosed?: boolean | null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Gate an async tool behind Ciphyrs tool-call authorization.
|
|
618
|
+
*
|
|
619
|
+
* FAILS CLOSED by default: if Ciphyrs cannot be reached the tool is NOT run
|
|
620
|
+
* and ToolBlocked is thrown. This changed — protectTool used to fail open —
|
|
621
|
+
* because this is the path that authorises tool execution. Opt back out with
|
|
622
|
+
* `{ failOpen: true }` here or `new CiphyrsClient({ failOpen: true })`.
|
|
623
|
+
*
|
|
624
|
+
* Sends the active `trace_id` on every gate call. The `span_id` is sent ONLY
|
|
625
|
+
* when the enclosing span is a TOOL span (`span(name, { kind: 'tool' })`) —
|
|
626
|
+
* i.e. when the id names the span this tool call will actually export.
|
|
627
|
+
* protectTool emits no span of its own, so inside an agent span, an llm span,
|
|
628
|
+
* a propagated remote context, or no span at all, the span id is omitted.
|
|
629
|
+
*
|
|
630
|
+
* That is deliberate and it is not merely caution: the gateway matches a tool
|
|
631
|
+
* span to its gate call by span id, and reads ANY producer-supplied id as
|
|
632
|
+
* proof that this agent's spans CAN be matched exactly. Sending the id of a
|
|
633
|
+
* span that is not the tool span leaves the exact match impossible while
|
|
634
|
+
* moving the agent from "not correlated, do not judge" into judgement on an
|
|
635
|
+
* unvalidated name/time fallback. Open a tool-kind span around the call if you
|
|
636
|
+
* want exact correlation.
|
|
637
|
+
*/
|
|
638
|
+
export declare function protectTool<F extends (...args: any[]) => Promise<any>>(
|
|
639
|
+
client: CiphyrsClient,
|
|
640
|
+
opts: ProtectToolOptions,
|
|
641
|
+
fn: F,
|
|
642
|
+
): F;
|