@vorim/sdk 3.4.3 → 3.6.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/dist/index.d.ts CHANGED
@@ -85,6 +85,14 @@ interface AuditEventInput {
85
85
  delegator_agent_id?: string;
86
86
  delegation_chain_id?: string;
87
87
  delegation_depth?: number;
88
+ /**
89
+ * Runtime-control linkage. When this action was gated through
90
+ * {@link VorimSDK.beforeAction} before being performed, pass the
91
+ * returned `decisionId` here (the SDK maps it to `decision_id` on the
92
+ * wire) so the audit event links back to the runtime decision that
93
+ * authorised it.
94
+ */
95
+ decision_id?: string;
88
96
  }
89
97
  /**
90
98
  * Claims structure for one VAIP -02 § 5 delegation link.
@@ -160,6 +168,68 @@ interface TrustRecord {
160
168
  revocation_status: boolean;
161
169
  last_active?: string;
162
170
  }
171
+ /**
172
+ * The verdict a runtime decision can carry.
173
+ *
174
+ * - `allow` — proceed with the action.
175
+ * - `deny` — do NOT perform the action. {@link VorimSDK.beforeAction}
176
+ * throws {@link VorimDeniedError} on this when `throwOnDeny`.
177
+ * - `modify` — proceed, but with `modifiedPayload` instead of the
178
+ * original payload (e.g. PII masked by a policy rule).
179
+ * - `escalate` — a human must approve. Poll
180
+ * {@link VorimSDK.waitForDecisionResolution} for the outcome.
181
+ * - `fallback` — the engine could not decide (timeout / error) and the
182
+ * org's fail-open/closed setting was applied. `isFallback`
183
+ * is true. The SDK also returns this shape locally when the
184
+ * decision API is unreachable and `runtimeFailOpen` is set.
185
+ */
186
+ type DecisionVerdict = 'allow' | 'deny' | 'modify' | 'escalate' | 'fallback';
187
+ /** Input to {@link VorimSDK.beforeAction}. Always use the public `agid_*` id. */
188
+ interface BeforeActionInput {
189
+ /** Public agent identifier (`agid_*`). UUIDs are accepted but discouraged. */
190
+ agentId: string;
191
+ /** Coarse action category, e.g. `tool_call`, `api_request`. */
192
+ actionType: string;
193
+ /** Specific target, e.g. the tool name `sendEmail`. */
194
+ actionTarget?: string;
195
+ /** The action's arguments. Capped at 64KB serialised by the server. */
196
+ payload?: Record<string, unknown>;
197
+ /** Free-form context the policy engine may match on. */
198
+ context?: Record<string, unknown>;
199
+ /** Permission scope the action requires; checked against the agent's grants. */
200
+ requiredScope?: string;
201
+ /**
202
+ * Idempotency key. Pass the SAME key when retrying a failed request so
203
+ * the server returns the original decision instead of creating a new one.
204
+ */
205
+ idempotencyKey?: string;
206
+ }
207
+ /** A runtime decision, as returned by {@link VorimSDK.beforeAction}. */
208
+ interface RuntimeDecision {
209
+ /** Server-assigned id. Carry into {@link AuditEventInput.decision_id}. */
210
+ decisionId: string;
211
+ decision: DecisionVerdict;
212
+ reason: string;
213
+ /** The policy rule that produced this decision, or null for defaults. */
214
+ decisionRuleId: string | null;
215
+ /** Present (object) when `decision === 'modify'`; null otherwise. */
216
+ modifiedPayload: Record<string, unknown> | null;
217
+ /** ISO8601 — after this the decision is stale and should not be relied on. */
218
+ expiresAt: string;
219
+ latencyMs: number;
220
+ /** True when the engine fell back (timeout/error/unreachable). */
221
+ isFallback: boolean;
222
+ policyVersion: number;
223
+ /**
224
+ * The human verdict on an escalation, once resolved by an operator:
225
+ * `'approved'`, `'denied'`, or `null` if not (yet) an escalation outcome.
226
+ *
227
+ * When this is set, {@link decision} is already translated for you
228
+ * (`approved` → `'allow'`, `denied` → `'deny'`) so the normal verdict
229
+ * checks work — this field is the raw resolution for callers who want it.
230
+ */
231
+ escalationResolution: 'approved' | 'denied' | null;
232
+ }
163
233
 
164
234
  /**
165
235
  * Replayable agent decision evidence helpers.
@@ -324,6 +394,19 @@ interface VorimConfig {
324
394
  * a failure). Chain integrity is checked by `@vorim/verify`.
325
395
  */
326
396
  chainEvents?: boolean;
397
+ /**
398
+ * Client-side fail-open behaviour for {@link VorimSDK.beforeAction} when
399
+ * the runtime decision API itself is unreachable (network error, DNS,
400
+ * timeout, or 5xx). Default `true`: a transport failure returns a
401
+ * synthetic `fallback` decision (`decision: 'fallback'`, `isFallback:
402
+ * true`) so a momentary control-plane outage does not block the agent.
403
+ * Set `false` to fail closed — `beforeAction` re-throws the transport
404
+ * error and the caller must decide.
405
+ *
406
+ * Note: this governs only transport failures. A reachable server that
407
+ * returns `deny` still denies regardless of this flag.
408
+ */
409
+ runtimeFailOpen?: boolean;
327
410
  }
328
411
  /**
329
412
  * VAIP v0 canonical bytes used by Vorim's per-event signing.
@@ -357,6 +440,7 @@ declare class VorimSDK {
357
440
  private autoSign;
358
441
  private canonicalForm;
359
442
  private chainEvents;
443
+ private runtimeFailOpen;
360
444
  /**
361
445
  * In-memory keyring mapping agent_id -> PEM-encoded Ed25519 private key.
362
446
  * Populated automatically by register() and registerEphemeral(), or
@@ -576,6 +660,74 @@ declare class VorimSDK {
576
660
  revokeAgentDelegation(agentId: string, chainId: string): Promise<{
577
661
  revoked: number;
578
662
  }>;
663
+ /**
664
+ * Gate an agent action BEFORE it is performed. Calls
665
+ * `POST /v1/runtime/decisions` and returns a typed {@link RuntimeDecision}.
666
+ *
667
+ * By default (`throwOnDeny: true`) a `deny` verdict throws
668
+ * {@link VorimDeniedError} — deny is carried in the response body, not
669
+ * the HTTP status, so without this the caller would treat a denial as
670
+ * success. Pass `{ throwOnDeny: false }` to handle deny yourself.
671
+ *
672
+ * On a `modify` verdict, use `decision.modifiedPayload` in place of your
673
+ * original payload (e.g. a policy rule masked PII). This is
674
+ * client-cooperative: Vorim returns the sanitised payload but does not sit
675
+ * inline and does not enforce that you send it — carry `decisionId` into
676
+ * the matching {@link emit} so the action stays auditable. On `escalate`,
677
+ * poll {@link waitForDecisionResolution} for the human decision.
678
+ *
679
+ * Transport failures (network/DNS/timeout/5xx) respect the constructor's
680
+ * `runtimeFailOpen` flag: when true (default) a synthetic `fallback`
681
+ * decision is returned so a control-plane blip does not block the agent;
682
+ * when false the underlying error is re-thrown.
683
+ *
684
+ * Always pass the public `agid_*` id as `agentId`.
685
+ *
686
+ * @example
687
+ * const d = await vorim.beforeAction({
688
+ * agentId: 'agid_acme_evilbot',
689
+ * actionType: 'tool_call',
690
+ * actionTarget: 'sendEmail',
691
+ * requiredScope: 'agent:communicate',
692
+ * payload: { to: 'customer@example.com' },
693
+ * });
694
+ * if (d.decision === 'allow') await sendEmail(d.modifiedPayload ?? payload);
695
+ */
696
+ beforeAction(input: BeforeActionInput, options?: {
697
+ throwOnDeny?: boolean;
698
+ }): Promise<RuntimeDecision>;
699
+ /**
700
+ * Poll a decision until it leaves the `escalate` state (a human
701
+ * approved or denied it) or the timeout elapses. Returns the resolved
702
+ * decision; throws {@link VorimError} `ESCALATION_TIMEOUT` (408) if the
703
+ * decision is still pending when the timeout is reached.
704
+ *
705
+ * Requires an API key with the `runtime:decide` scope (the same scope
706
+ * `beforeAction` uses).
707
+ */
708
+ waitForDecisionResolution(decisionId: string, options?: {
709
+ timeoutMs?: number;
710
+ pollIntervalMs?: number;
711
+ }): Promise<RuntimeDecision>;
712
+ /**
713
+ * Map a snake_case decision row (from POST or GET) to the camelCase
714
+ * {@link RuntimeDecision}. The SDK has no generic camelizer, so the
715
+ * mapping is explicit — and tolerant of either the POST shape
716
+ * (`decision_rule_id`, `latency_ms`) or absent fields on a GET row.
717
+ *
718
+ * Escalation-resolution translation (load-bearing): the server resolves
719
+ * an escalation by setting `escalation_resolution` but does NOT flip the
720
+ * row's `decision` column off `'escalate'`. If we returned that verbatim,
721
+ * a caller's obvious `if (d.decision === 'deny')` check would never fire
722
+ * on a human DENIAL — a silent fail-open. So when a resolution is present
723
+ * we translate the verdict: approved → 'allow', denied → 'deny'. The raw
724
+ * resolution is also surfaced as `escalationResolution` for callers that
725
+ * want it explicitly.
726
+ *
727
+ * A missing verdict (malformed/truncated response) fails CLOSED to 'deny'
728
+ * rather than 'fallback', so the deny check fires on garbled input.
729
+ */
730
+ private toRuntimeDecision;
579
731
  /**
580
732
  * Emit an audit event for an agent action.
581
733
  *
@@ -754,6 +906,9 @@ declare class VorimSDK {
754
906
  private post;
755
907
  private patch;
756
908
  private delete;
909
+ /** Like request() but returns the FULL { data, meta, ... } envelope
910
+ * instead of unwrapping to `data`. Used where meta (pagination) matters. */
911
+ private requestEnvelope;
757
912
  private request;
758
913
  private pemToArrayBuffer;
759
914
  private arrayBufferToBase64;
@@ -764,6 +919,16 @@ declare class VorimError extends Error {
764
919
  details?: Record<string, unknown> | undefined;
765
920
  constructor(status: number, code: string, message: string, details?: Record<string, unknown> | undefined);
766
921
  }
922
+ /**
923
+ * Thrown by {@link VorimSDK.beforeAction} when the runtime decision is
924
+ * `deny` and `throwOnDeny` is left at its default (`true`). Carries the
925
+ * full {@link RuntimeDecision} so the caller can inspect the reason and
926
+ * the decision id. Status is 403 with code `DECISION_DENIED`.
927
+ */
928
+ declare class VorimDeniedError extends VorimError {
929
+ decision: RuntimeDecision;
930
+ constructor(decision: RuntimeDecision);
931
+ }
767
932
  declare function createVorim(config: VorimConfig): VorimSDK;
768
933
 
769
- export { type Agent, type AgentDelegationRecord, type AgentRegistrationInput, type AgentRegistrationResult, type AgentStatus, type AuditEventInput, type AuditEventType, type AuditResult, CANONICAL_TOOL_CATALOGUE_VERSION, type CatalogueTool, type DelegationLinkClaims, type PermissionCheckResult, type PermissionScope, type ReplayContext, type ReplayInputs, type TrustRecord, type VorimConfig, VorimError, VorimSDK, canonicalPayloadV0, canonicalPayloadV1, createVorim as default, hashPreviousEvent, hashSystemPrompt, hashTool, hashToolCatalogue, jcsCanonicalise, prepareReplayContext };
934
+ export { type Agent, type AgentDelegationRecord, type AgentRegistrationInput, type AgentRegistrationResult, type AgentStatus, type AuditEventInput, type AuditEventType, type AuditResult, type BeforeActionInput, CANONICAL_TOOL_CATALOGUE_VERSION, type CatalogueTool, type DecisionVerdict, type DelegationLinkClaims, type PermissionCheckResult, type PermissionScope, type ReplayContext, type ReplayInputs, type RuntimeDecision, type TrustRecord, type VorimConfig, VorimDeniedError, VorimError, VorimSDK, canonicalPayloadV0, canonicalPayloadV1, createVorim as default, hashPreviousEvent, hashSystemPrompt, hashTool, hashToolCatalogue, jcsCanonicalise, prepareReplayContext };
package/dist/index.js CHANGED
@@ -17,10 +17,9 @@ function jcsCanonicalise(value) {
17
17
  return "[" + value.map(jcsCanonicalise).join(",") + "]";
18
18
  }
19
19
  if (typeof value === "object") {
20
- const keys = Object.keys(value).sort();
21
- const parts = keys.map((k) => {
22
- return JSON.stringify(k) + ":" + jcsCanonicalise(value[k]);
23
- });
20
+ const obj = value;
21
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
22
+ const parts = keys.map((k) => JSON.stringify(k) + ":" + jcsCanonicalise(obj[k]));
24
23
  return "{" + parts.join(",") + "}";
25
24
  }
26
25
  throw new Error(`jcsCanonicalise: unsupported value type: ${typeof value}`);
@@ -70,8 +69,11 @@ async function prepareReplayContext(inputs) {
70
69
  }
71
70
 
72
71
  // src/index.ts
73
- var SDK_VERSION = true ? "3.4.3" : "0.0.0";
72
+ var SDK_VERSION = true ? "3.6.0" : "0.0.0";
74
73
  var USER_AGENT = `vorim-sdk/${SDK_VERSION}`;
74
+ function sleep(ms) {
75
+ return new Promise((resolve) => setTimeout(resolve, ms));
76
+ }
75
77
  function canonicalPayloadV0(event) {
76
78
  return [
77
79
  event.event_type,
@@ -93,6 +95,7 @@ var VorimSDK = class _VorimSDK {
93
95
  autoSign;
94
96
  canonicalForm;
95
97
  chainEvents;
98
+ runtimeFailOpen;
96
99
  /**
97
100
  * In-memory keyring mapping agent_id -> PEM-encoded Ed25519 private key.
98
101
  * Populated automatically by register() and registerEphemeral(), or
@@ -138,6 +141,7 @@ var VorimSDK = class _VorimSDK {
138
141
  }
139
142
  }
140
143
  this.chainEvents = config.chainEvents ?? false;
144
+ this.runtimeFailOpen = config.runtimeFailOpen ?? true;
141
145
  }
142
146
  /**
143
147
  * Register a previously-issued agent keypair so this SDK instance can
@@ -234,7 +238,8 @@ var VorimSDK = class _VorimSDK {
234
238
  */
235
239
  async listAgents(params) {
236
240
  const qs = new URLSearchParams(params).toString();
237
- return this.get(`/agents${qs ? "?" + qs : ""}`);
241
+ const env = await this.requestEnvelope("GET", `/agents${qs ? "?" + qs : ""}`);
242
+ return { agents: env.data ?? [], meta: env.meta ?? null };
238
243
  }
239
244
  /**
240
245
  * Update an agent's metadata.
@@ -363,6 +368,149 @@ var VorimSDK = class _VorimSDK {
363
368
  async revokeAgentDelegation(agentId, chainId) {
364
369
  return this.delete(`/agents/${agentId}/delegations/${chainId}`);
365
370
  }
371
+ // ─── Runtime Control (Strategy A) ──────────────────────────────────
372
+ /**
373
+ * Gate an agent action BEFORE it is performed. Calls
374
+ * `POST /v1/runtime/decisions` and returns a typed {@link RuntimeDecision}.
375
+ *
376
+ * By default (`throwOnDeny: true`) a `deny` verdict throws
377
+ * {@link VorimDeniedError} — deny is carried in the response body, not
378
+ * the HTTP status, so without this the caller would treat a denial as
379
+ * success. Pass `{ throwOnDeny: false }` to handle deny yourself.
380
+ *
381
+ * On a `modify` verdict, use `decision.modifiedPayload` in place of your
382
+ * original payload (e.g. a policy rule masked PII). This is
383
+ * client-cooperative: Vorim returns the sanitised payload but does not sit
384
+ * inline and does not enforce that you send it — carry `decisionId` into
385
+ * the matching {@link emit} so the action stays auditable. On `escalate`,
386
+ * poll {@link waitForDecisionResolution} for the human decision.
387
+ *
388
+ * Transport failures (network/DNS/timeout/5xx) respect the constructor's
389
+ * `runtimeFailOpen` flag: when true (default) a synthetic `fallback`
390
+ * decision is returned so a control-plane blip does not block the agent;
391
+ * when false the underlying error is re-thrown.
392
+ *
393
+ * Always pass the public `agid_*` id as `agentId`.
394
+ *
395
+ * @example
396
+ * const d = await vorim.beforeAction({
397
+ * agentId: 'agid_acme_evilbot',
398
+ * actionType: 'tool_call',
399
+ * actionTarget: 'sendEmail',
400
+ * requiredScope: 'agent:communicate',
401
+ * payload: { to: 'customer@example.com' },
402
+ * });
403
+ * if (d.decision === 'allow') await sendEmail(d.modifiedPayload ?? payload);
404
+ */
405
+ async beforeAction(input, options = {}) {
406
+ const throwOnDeny = options.throwOnDeny ?? true;
407
+ const body = {
408
+ agent_id: input.agentId,
409
+ action_type: input.actionType,
410
+ action_target: input.actionTarget,
411
+ payload: input.payload,
412
+ context: input.context,
413
+ required_scope: input.requiredScope,
414
+ idempotency_key: input.idempotencyKey
415
+ };
416
+ let raw;
417
+ try {
418
+ raw = await this.post("/runtime/decisions", body);
419
+ } catch (err) {
420
+ const status = err instanceof VorimError ? err.status : 0;
421
+ const isTransport = status === 0 || status >= 500;
422
+ if (isTransport && this.runtimeFailOpen) {
423
+ return {
424
+ decisionId: "",
425
+ decision: "fallback",
426
+ reason: "Runtime decision API unreachable; client fail-open applied",
427
+ decisionRuleId: null,
428
+ modifiedPayload: null,
429
+ expiresAt: new Date(Date.now() + 6e4).toISOString(),
430
+ latencyMs: 0,
431
+ isFallback: true,
432
+ policyVersion: 0,
433
+ escalationResolution: null
434
+ };
435
+ }
436
+ throw err;
437
+ }
438
+ const decision = this.toRuntimeDecision(raw);
439
+ if (decision.decision === "deny" && throwOnDeny) {
440
+ throw new VorimDeniedError(decision);
441
+ }
442
+ return decision;
443
+ }
444
+ /**
445
+ * Poll a decision until it leaves the `escalate` state (a human
446
+ * approved or denied it) or the timeout elapses. Returns the resolved
447
+ * decision; throws {@link VorimError} `ESCALATION_TIMEOUT` (408) if the
448
+ * decision is still pending when the timeout is reached.
449
+ *
450
+ * Requires an API key with the `runtime:decide` scope (the same scope
451
+ * `beforeAction` uses).
452
+ */
453
+ async waitForDecisionResolution(decisionId, options = {}) {
454
+ const timeoutMs = options.timeoutMs ?? 3e5;
455
+ const pollIntervalMs = options.pollIntervalMs ?? 1e3;
456
+ const start = Date.now();
457
+ do {
458
+ const raw = await this.get(`/runtime/decisions/${decisionId}`);
459
+ if (raw?.decision !== "escalate" || raw?.escalation_resolution) {
460
+ return this.toRuntimeDecision(raw);
461
+ }
462
+ const remaining = timeoutMs - (Date.now() - start);
463
+ if (remaining <= 0) break;
464
+ await sleep(Math.min(pollIntervalMs, remaining));
465
+ } while (Date.now() - start < timeoutMs);
466
+ throw new VorimError(
467
+ 408,
468
+ "ESCALATION_TIMEOUT",
469
+ `Decision ${decisionId} still pending after ${timeoutMs}ms`,
470
+ { decisionId }
471
+ );
472
+ }
473
+ /**
474
+ * Map a snake_case decision row (from POST or GET) to the camelCase
475
+ * {@link RuntimeDecision}. The SDK has no generic camelizer, so the
476
+ * mapping is explicit — and tolerant of either the POST shape
477
+ * (`decision_rule_id`, `latency_ms`) or absent fields on a GET row.
478
+ *
479
+ * Escalation-resolution translation (load-bearing): the server resolves
480
+ * an escalation by setting `escalation_resolution` but does NOT flip the
481
+ * row's `decision` column off `'escalate'`. If we returned that verbatim,
482
+ * a caller's obvious `if (d.decision === 'deny')` check would never fire
483
+ * on a human DENIAL — a silent fail-open. So when a resolution is present
484
+ * we translate the verdict: approved → 'allow', denied → 'deny'. The raw
485
+ * resolution is also surfaced as `escalationResolution` for callers that
486
+ * want it explicitly.
487
+ *
488
+ * A missing verdict (malformed/truncated response) fails CLOSED to 'deny'
489
+ * rather than 'fallback', so the deny check fires on garbled input.
490
+ */
491
+ toRuntimeDecision(raw) {
492
+ const resolution = raw?.escalation_resolution === "approved" ? "approved" : raw?.escalation_resolution === "denied" ? "denied" : null;
493
+ const hasVerdict = typeof raw?.decision === "string";
494
+ let decision;
495
+ if (raw?.decision === "escalate" && resolution === "approved") decision = "allow";
496
+ else if (raw?.decision === "escalate" && resolution === "denied") decision = "deny";
497
+ else if (hasVerdict) decision = raw.decision;
498
+ else decision = "deny";
499
+ return {
500
+ decisionId: raw?.decision_id ?? "",
501
+ decision,
502
+ reason: raw?.reason ?? (hasVerdict ? "" : "Malformed decision response (no verdict); failing closed"),
503
+ decisionRuleId: raw?.decision_rule_id ?? null,
504
+ modifiedPayload: raw?.modified_payload ?? null,
505
+ // Avoid '' (Date.parse('') === NaN). A verdict-less row gets an epoch
506
+ // timestamp so any staleness check treats it as already expired.
507
+ expiresAt: raw?.expires_at ?? (hasVerdict ? "" : (/* @__PURE__ */ new Date(0)).toISOString()),
508
+ latencyMs: raw?.latency_ms ?? 0,
509
+ isFallback: raw?.is_fallback ?? !hasVerdict,
510
+ policyVersion: raw?.policy_version ?? 0,
511
+ escalationResolution: resolution
512
+ };
513
+ }
366
514
  // ─── Audit ────────────────────────────────────────────────────────
367
515
  /**
368
516
  * Emit an audit event for an agent action.
@@ -639,7 +787,12 @@ var VorimSDK = class _VorimSDK {
639
787
  async delete(path) {
640
788
  return this.request("DELETE", path);
641
789
  }
642
- async request(method, path, body) {
790
+ /** Like request() but returns the FULL { data, meta, ... } envelope
791
+ * instead of unwrapping to `data`. Used where meta (pagination) matters. */
792
+ async requestEnvelope(method, path, body) {
793
+ return this.request(method, path, body, false);
794
+ }
795
+ async request(method, path, body, unwrap = true) {
643
796
  const controller = new AbortController();
644
797
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
645
798
  try {
@@ -663,7 +816,7 @@ var VorimSDK = class _VorimSDK {
663
816
  );
664
817
  }
665
818
  const json = await response.json();
666
- return json.data;
819
+ return unwrap ? json.data : json;
667
820
  } finally {
668
821
  clearTimeout(timeoutId);
669
822
  }
@@ -698,11 +851,20 @@ var VorimError = class extends Error {
698
851
  code;
699
852
  details;
700
853
  };
854
+ var VorimDeniedError = class extends VorimError {
855
+ constructor(decision) {
856
+ super(403, "DECISION_DENIED", decision.reason, { decisionId: decision.decisionId });
857
+ this.decision = decision;
858
+ this.name = "VorimDeniedError";
859
+ }
860
+ decision;
861
+ };
701
862
  function createVorim(config) {
702
863
  return new VorimSDK(config);
703
864
  }
704
865
  export {
705
866
  CANONICAL_TOOL_CATALOGUE_VERSION,
867
+ VorimDeniedError,
706
868
  VorimError,
707
869
  VorimSDK,
708
870
  canonicalPayloadV0,