@tenuo/core 0.3.2-beta.0 → 0.3.3-beta.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 CHANGED
@@ -16,7 +16,7 @@ npm i @tenuo/core@beta
16
16
  ```
17
17
 
18
18
  ```ts
19
- import { createTenuo, under } from "@tenuo/core";
19
+ import { AuthorizationDeniedError, createTenuo, under } from "@tenuo/core";
20
20
 
21
21
  const tenuo = createTenuo({ root: createTenuo.devRoot() });
22
22
  const readFile = tenuo.tool(
@@ -26,15 +26,22 @@ const readFile = tenuo.tool(
26
26
  const session = tenuo.session({ tools: [readFile] });
27
27
 
28
28
  await tenuo.withSession(session, async () => {
29
- await readFile.execute({ path: "/data/q3.pdf" }); // allowed
30
- await readFile.execute({ path: "/etc/passwd" }); // denied — execute does not run
29
+ console.log(await readFile.execute({ path: "/data/q3.pdf" })); // allowed
30
+ try {
31
+ await readFile.execute({ path: "/etc/passwd" }); // denied — execute does not run
32
+ } catch (error) {
33
+ if (!(error instanceof AuthorizationDeniedError)) throw error;
34
+ console.log("denied:", error.code);
35
+ }
31
36
  });
32
37
  ```
33
38
 
34
39
  Host schemas (Zod or otherwise) answer **valid**. Tool `allow` is the host ceiling.
35
40
  The session is what this agent may do. Rust AND's both. `allow` is zero-trust:
36
41
  every call argument must be named in the policy. `allow: {}` adds no extra
37
- ceiling. `devRoot()` requires `NODE_ENV=development` or `test`,
42
+ ceiling, and `session({ tools })` refuses such a tool unless the session's own
43
+ `allow` names it (`allow: { read_file: {} }`), because an empty capability in
44
+ a warrant accepts any arguments. Unknown policy and option keys throw. `devRoot()` requires `NODE_ENV=development` or `test`,
38
45
  `devRoot({ allowInProduction: true })`, or `TENUO_ALLOW_DEV=1`. Unset
39
46
  `NODE_ENV` is not treated as development.
40
47
 
@@ -49,6 +56,8 @@ const runtime = createTenuo.runtime({
49
56
  trustedRoots: [createTenuo.publicKeyFromEnv("TENUO_ROOT_PUBLIC_KEY")],
50
57
  receipts: "collect",
51
58
  });
59
+ // TENUO_WARRANT holds session.toWireString() from the issuer: the whole chain
60
+ // as one string. sessionFromWire() also accepts the toWire() array.
52
61
  const session = runtime.sessionFromWire(process.env.TENUO_WARRANT!);
53
62
  await readFile.execute({ path: "/data/q3.pdf" }, { session });
54
63
  const receipts = session.drainReceipts(); // persist, then upload
@@ -59,7 +68,7 @@ read environment variables or invent an origin. Relative `/v1` endpoints need
59
68
  `token.resolveEndpoint({ localBase })`. Callers append `/v1/…` to the bare
60
69
  origin.
61
70
 
62
- See [Receipt delivery](../../README.md#receipt-delivery) for peek / drain / ack
71
+ See [Receipt delivery](https://github.com/tenuo-ai/tenuo/tree/main/tenuo-ts#receipt-delivery) for peek / drain / ack
63
72
  guarantees. This package does not perform HTTP, discovery, or upload.
64
73
 
65
74
  Delegation to another agent rebinds a narrower child to that agent's key. The
@@ -96,11 +105,13 @@ MCP wire helpers live on `tenuo.mcp` (`attach` / `verify` / `handler`). They do
96
105
  not depend on an MCP framework. For the official v2 server, use `@tenuo/mcp`.
97
106
  For `@modelcontextprotocol/sdk` v1, copy the recipe in `examples/mcp/host.ts`.
98
107
  `verify` / `handler` can take an optional `nonceStore` (`memoryNonceStore()`,
99
- or an async Redis `checkAndRecord`) to reject an exact replayed PoP; that is
100
- opt-in. PoP v1 is otherwise replayable in-window, including approval-gated
101
- calls. Pass `nonceStore` on those tools if an approval must be one-use.
108
+ or an async Redis `checkAndRecord`) to reject an exact replayed PoP and to
109
+ consume the approvals an allowed call carried; that is opt-in. PoP v1 is
110
+ otherwise replayable in-window, including approval-gated calls. For
111
+ in-process tools, pass the same `nonceStore` to `tenuo.tool()` to make
112
+ approvals one-use there.
102
113
  `memoryNonceStore({ ttlSeconds })` defaults to 180 seconds; an explicit TTL must
103
114
  be positive, finite, and small enough to represent in milliseconds.
104
115
 
105
- See the [workspace README](../../README.md) for the full API, refuse list, and
116
+ See the [workspace README](https://github.com/tenuo-ai/tenuo/tree/main/tenuo-ts) for the full API, refuse list, and
106
117
  how to rebuild WASM from this monorepo.
package/dist/api.d.ts CHANGED
@@ -121,15 +121,38 @@ export type ToolPolicy<TArgs = undefined> = {
121
121
  /**
122
122
  * Host ceiling. AND'd with the session in Rust.
123
123
  * Non-empty maps are zero-trust: every call argument must be named here.
124
- * `{}` means no extra ceiling (session/warrant only).
124
+ * `{}` means no extra ceiling (session/warrant only). Required: a missing
125
+ * `allow` or an unknown policy key throws. `session({ tools })` refuses a
126
+ * tool whose `allow` is `{}` unless the session's own `allow` names that
127
+ * capability explicitly, because an empty capability in a warrant accepts
128
+ * any arguments.
125
129
  */
126
130
  readonly allow: [NonNullable<TArgs>] extends [object] ? [KeysOfUnion<NonNullable<TArgs>>] extends [never] ? AllowPolicy : string extends KeysOfUnion<NonNullable<TArgs>> ? AllowPolicy : number extends KeysOfUnion<NonNullable<TArgs>> ? AllowPolicy : symbol extends KeysOfUnion<NonNullable<TArgs>> ? AllowPolicy : {
127
131
  readonly [Field in KeysOfUnion<NonNullable<TArgs>>]?: ConstraintExpr;
128
132
  } : AllowPolicy;
129
133
  readonly capability?: string;
134
+ /**
135
+ * Makes approvals one-use for this tool. When a call that carried
136
+ * `approvals` is allowed, each approval envelope that matches the call is
137
+ * recorded here before the tool body runs; an envelope the store has
138
+ * already seen denies with `TENUO_INSUFFICIENT_APPROVALS`. Same
139
+ * `NonceStore` shape as the MCP verify path; `memoryNonceStore()` works
140
+ * for one process. The store must remember a key for as long as an
141
+ * approval can live (one hour by default), so give `memoryNonceStore()` a
142
+ * matching `ttlSeconds`. A store that throws, rejects, or returns a
143
+ * non-boolean fails closed with "Replay store unavailable".
144
+ */
145
+ readonly nonceStore?: NonceStore;
146
+ /** Isolated. Called when `nonceStore.checkAndRecord` throws or rejects. */
147
+ readonly onNonceStoreError?: (error: unknown) => void | Promise<void>;
130
148
  };
131
149
  /** Compile-time brand only — never assigned at runtime. Do not `in`/`has` this key. */
132
150
  declare const protectedBrand: unique symbol;
151
+ /**
152
+ * Tenuo's per-call options. Other keys pass through to the wrapped tool's own
153
+ * `execute`, except keys that look like a misspelled Tenuo option
154
+ * (`sesion`, `approval`, `onReciept`), which throw instead of being ignored.
155
+ */
133
156
  export type ExecuteOptions = {
134
157
  readonly session?: Session;
135
158
  /** Already-signed approval envelopes (hex, standard base64, or CBOR bytes). */
@@ -308,7 +331,10 @@ export type NarrowOptions = {
308
331
  * cannot authorize here. Hand `toWire()` to that agent.
309
332
  */
310
333
  readonly holder?: PublicKeyHandle;
311
- /** Child lifetime. Clamped to what the parent has left. */
334
+ /**
335
+ * Child lifetime. A value longer than the parent has left is clamped to the
336
+ * parent's expiry, not rejected: the child never outlives the parent.
337
+ */
312
338
  readonly ttlSeconds?: number;
313
339
  /** The child's holder cannot delegate further. */
314
340
  readonly terminal?: boolean;
@@ -319,7 +345,7 @@ export type NarrowOptions = {
319
345
  readonly agentId?: string;
320
346
  /** Approvers can only be added. */
321
347
  readonly addApprovers?: readonly PublicKeyHandle[];
322
- /** The threshold can only go up. */
348
+ /** The threshold can only go up. A value below the parent's throws. */
323
349
  readonly minApprovals?: number;
324
350
  };
325
351
  /** Options for `tenuo.issue()`: an execution session minted from an issuer session. */
@@ -508,6 +534,13 @@ export interface Session {
508
534
  readonly [Symbol.toStringTag]: "TenuoSession";
509
535
  /** Warrant tokens, root first. Does not include the holder secret. */
510
536
  toWire(): readonly string[];
537
+ /**
538
+ * The whole chain as one string: a CBOR warrant stack in URL-safe base64,
539
+ * the same encoding as `PresentedCall.warrant`. Use it where only a single
540
+ * string fits (an environment variable, a header). `sessionFromWire()`
541
+ * imports it. Does not include the holder secret.
542
+ */
543
+ toWireString(): string;
511
544
  /** SHA-256 of `(warrant_id, tool, canonical args)`. App-level idempotency, not PoP. */
512
545
  dedupKey(tool: string, args: Readonly<Record<string, unknown>>): string;
513
546
  /** Holder public key, depth, ceiling, lifetime, tools. Never the secret. */
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * Frozen public contract for `@tenuo/core`.\n *\n * Names and shapes here are the review surface — do not grow this file with\n * protocol vocabulary (`Warrant`, `mint`, `guard`) as the lead API.\n */\n\n/** Canonical kebab-case codes from Rust `ErrorCode::name()` / `Denial::code()`. */\nexport type ProtocolDenialCode =\n | \"signature-invalid\"\n | \"untrusted-root\"\n | \"chain-broken\"\n | \"depth-exceeded\"\n | \"warrant-expired\"\n | \"tool-not-authorized\"\n | \"constraint-violation\"\n | \"pop-signature-invalid\"\n | \"approval-required\"\n | \"insufficient-approvals\"\n | \"warrant-revoked\";\n\n/** Legacy SCREAMING_SNAKE aliases accepted by existing TypeScript callers. */\nexport type LegacyTenuoErrorCode =\n | \"TENUO_NOT_READY\"\n | \"TENUO_CONFIGURATION\"\n | \"TENUO_CONSTRAINT_VIOLATION\"\n | \"TENUO_TOOL_NOT_AUTHORIZED\"\n | \"TENUO_WARRANT_EXPIRED\"\n | \"TENUO_UNTRUSTED_ROOT\"\n | \"TENUO_INVALID_POP\"\n | \"TENUO_SIGNATURE_INVALID\"\n | \"TENUO_CHAIN_INVALID\"\n | \"TENUO_DEPTH_EXCEEDED\"\n | \"TENUO_REVOKED\"\n | \"TENUO_APPROVAL_REQUIRED\"\n | \"TENUO_INSUFFICIENT_APPROVALS\"\n | \"TENUO_CANONICALIZATION\"\n | \"TENUO_NOT_IMPLEMENTED\";\n\nexport type TenuoErrorCode = ProtocolDenialCode | LegacyTenuoErrorCode;\n\n// ---------------------------------------------------------------------------\n// Constraints. Every kind is evaluated in the Rust core; TypeScript only\n// builds the marker object. The set matches the core `Constraint` enum.\n// ---------------------------------------------------------------------------\n\n/** Path-traversal-safe prefix. Core evaluates this; TypeScript does not. */\nexport type UnderConstraint = {\n readonly kind: \"under\";\n readonly root: string;\n /** Default true. */\n readonly caseSensitive?: boolean;\n /** Whether the root itself is allowed. Default true. */\n readonly allowEqual?: boolean;\n};\n\nexport type EmailConstraint = {\n readonly kind: \"email\";\n readonly domain: string;\n};\n\nexport type MaxConstraint = {\n readonly kind: \"max\";\n readonly value: number;\n};\n\nexport type MinConstraint = {\n readonly kind: \"min\";\n readonly value: number;\n};\n\nexport type RangeConstraint = {\n readonly kind: \"range\";\n readonly min?: number;\n readonly max?: number;\n readonly minExclusive?: boolean;\n readonly maxExclusive?: boolean;\n};\n\nexport type OneOfConstraint = {\n readonly kind: \"oneOf\";\n readonly values: readonly string[];\n};\n\nexport type NotOneOfConstraint = {\n readonly kind: \"notOneOf\";\n readonly values: readonly string[];\n};\n\nexport type PatternConstraint = {\n readonly kind: \"pattern\";\n readonly pattern: string;\n};\n\nexport type RegexConstraint = {\n readonly kind: \"regex\";\n readonly source: string;\n};\n\nexport type ExactConstraint = {\n readonly kind: \"exact\";\n readonly value: string | number | boolean;\n};\n\nexport type WildcardConstraint = {\n readonly kind: \"wildcard\";\n};\n\nexport type CidrConstraint = {\n readonly kind: \"cidr\";\n readonly network: string;\n};\n\nexport type UrlPatternConstraint = {\n readonly kind: \"urlPattern\";\n readonly pattern: string;\n};\n\nexport type UrlSafeConstraint = {\n readonly kind: \"urlSafe\";\n readonly schemes?: readonly string[];\n readonly allowDomains?: readonly string[];\n readonly denyDomains?: readonly string[];\n};\n\nexport type ShlexConstraint = {\n readonly kind: \"shlex\";\n readonly allow: readonly string[];\n};\n\nexport type ContainsConstraint = {\n readonly kind: \"contains\";\n readonly values: readonly (string | number | boolean)[];\n};\n\nexport type SubsetConstraint = {\n readonly kind: \"subset\";\n readonly values: readonly (string | number | boolean)[];\n};\n\nexport type AnyOfConstraint = {\n readonly kind: \"anyOf\";\n readonly constraints: readonly ConstraintExpr[];\n};\n\nexport type AllConstraint = {\n readonly kind: \"all\";\n readonly constraints: readonly ConstraintExpr[];\n};\n\nexport type NotConstraint = {\n readonly kind: \"not\";\n readonly constraint: ConstraintExpr;\n};\n\nexport type CelConstraint = {\n readonly kind: \"cel\";\n readonly expression: string;\n};\n\nexport type ConstraintExpr =\n | UnderConstraint\n | EmailConstraint\n | MaxConstraint\n | MinConstraint\n | RangeConstraint\n | OneOfConstraint\n | NotOneOfConstraint\n | PatternConstraint\n | RegexConstraint\n | ExactConstraint\n | WildcardConstraint\n | CidrConstraint\n | UrlPatternConstraint\n | UrlSafeConstraint\n | ShlexConstraint\n | ContainsConstraint\n | SubsetConstraint\n | AnyOfConstraint\n | AllConstraint\n | NotConstraint\n | CelConstraint;\n\n/** Per-argument allow policy. Not a Zod schema. */\nexport type AllowPolicy = {\n readonly [field: string]: ConstraintExpr;\n};\n\n/**\n * Framework tool shape we wrap. No dependency on `ai` / Mastra.\n * `parameters` is host-schema (usually Zod) and is never treated as authority.\n */\nexport type ToolLike<\n TArgs extends Record<string, unknown> = Record<string, unknown>,\n TResult = unknown,\n> = {\n readonly description?: string;\n readonly parameters?: unknown;\n execute: (args: TArgs, options?: unknown) => TResult | Promise<TResult>;\n};\n\ntype KeysOfUnion<T> = T extends unknown ? keyof T : never;\n\nexport type ToolPolicy<TArgs = undefined> = {\n /**\n * Host ceiling. AND'd with the session in Rust.\n * Non-empty maps are zero-trust: every call argument must be named here.\n * `{}` means no extra ceiling (session/warrant only).\n */\n readonly allow: [NonNullable<TArgs>] extends [object]\n ? [KeysOfUnion<NonNullable<TArgs>>] extends [never]\n ? AllowPolicy\n : string extends KeysOfUnion<NonNullable<TArgs>>\n ? AllowPolicy\n : number extends KeysOfUnion<NonNullable<TArgs>>\n ? AllowPolicy\n : symbol extends KeysOfUnion<NonNullable<TArgs>>\n ? AllowPolicy\n : { readonly [Field in KeysOfUnion<NonNullable<TArgs>>]?: ConstraintExpr }\n : AllowPolicy;\n readonly capability?: string;\n};\n\n/** Compile-time brand only — never assigned at runtime. Do not `in`/`has` this key. */\ndeclare const protectedBrand: unique symbol;\n\nexport type ExecuteOptions = {\n readonly session?: Session;\n /** Already-signed approval envelopes (hex, standard base64, or CBOR bytes). */\n readonly approvals?: readonly (string | Uint8Array)[];\n /**\n * Signed evidence of the decision. Not a security hook.\n * Exceptions (sync or rejected Promise) are isolated and never deny or\n * fail the tool.\n */\n readonly onReceipt?: (receipt: string) => void | Promise<void>;\n /**\n * Correlation handle written to the receipt. Use whatever id the host\n * already has — a JSON-RPC id, a trace or span id — so a receipt can be\n * matched against your own logs. Not an identity: nothing enforces\n * uniqueness, and distinguishing repeated calls is the receipt chain's job.\n */\n readonly requestId?: string;\n};\n\ntype ExtraExecuteOptions<T extends { execute: (args: never) => unknown }> =\n T[\"execute\"] extends (args: never, options?: infer O) => unknown\n ? unknown extends O\n ? object\n : Omit<NonNullable<O>, keyof ExecuteOptions>\n : object;\n\n/** Wrapped tool. `execute` still authorizes; optional `{ session }` overrides ALS. */\nexport type ProtectedTool<T extends { execute: (args: never) => unknown }> = Omit<T, \"execute\"> & {\n execute: (\n args: Parameters<T[\"execute\"]>[0],\n options?: ExecuteOptions & ExtraExecuteOptions<T>,\n ) => Promise<Awaited<ReturnType<T[\"execute\"]>>>;\n readonly [protectedBrand]: true;\n};\n\n// ---------------------------------------------------------------------------\n// Approvals\n// ---------------------------------------------------------------------------\n\n/** Per-argument trigger for an approval gate. */\nexport type ArgApprovalGate =\n /** Every value of this argument fires the gate. */\n | \"all\"\n /** Only values satisfying the constraint fire the gate. */\n | { readonly when: ConstraintExpr }\n /** Every value except those satisfying the constraint fires the gate. Narrowable across delegation. */\n | { readonly exempt: ConstraintExpr };\n\n/** Gate on one tool: a message for the approver and optional per-argument triggers. */\nexport type ToolApprovalGate = {\n readonly message?: string;\n /** Absent: the whole tool is gated. */\n readonly args?: { readonly [field: string]: ArgApprovalGate };\n};\n\n/** Approval requirement. Host collects SignedApproval envelopes; Rust decides. */\nexport type RequireApproval = {\n readonly approvers: readonly PublicKeyHandle[];\n readonly min: number;\n /** Whole-tool gates. Defaults to every tool in the session when `gates` is absent too. */\n readonly tools?: readonly string[];\n /** Per-tool gates with messages and per-argument triggers. */\n readonly gates?: { readonly [tool: string]: ToolApprovalGate };\n};\n\n/** What an approval service needs to present a call to a human. Not a signed artifact. */\nexport type ApprovalRequest = {\n readonly requestId: string;\n readonly warrantId: string;\n readonly tool: string;\n readonly args: Readonly<Record<string, unknown>>;\n /** Hex SHA-256 committing to warrant id, tool, canonical args, and holder. */\n readonly requestHash: string;\n readonly holderPublicKey: string;\n readonly requiredApprovers: readonly string[];\n readonly minApprovals: number;\n /** Unix seconds. An approval cannot outlive the warrant. */\n readonly warrantExpiresAt: number;\n readonly createdAt: number;\n readonly message: string;\n};\n\n/** Holder-signed statement that it is the one asking. Carried in a control-plane request. */\nexport type ApprovalContextAttestation = {\n readonly version: number;\n readonly canonicalization: string;\n readonly warrantId: string;\n readonly tool: string;\n readonly requestHash: string;\n readonly holderKeyHex: string;\n readonly argsCanonicalCborB64: string;\n readonly signerKeyHex: string;\n readonly signatureB64: string;\n};\n\n/** Wire body for a control-plane approval API. Field names are the wire names. */\nexport type ControlPlaneApprovalRequestV1 = {\n readonly schema_version: 1;\n readonly request_id_hex: string;\n readonly warrant_id: string;\n readonly tool: string;\n readonly arguments: Readonly<Record<string, unknown>>;\n readonly request_hash_hex: string;\n readonly holder_public_key_hex: string;\n readonly required_approver_keys_hex: readonly string[];\n readonly min_approvals: number;\n readonly warrant_expires_at_unix: number;\n readonly created_at_unix: number;\n readonly attestation?: Readonly<Record<string, unknown>>;\n readonly temporal?: Readonly<Record<string, string>>;\n readonly message?: string;\n};\n\nexport type ControlPlaneApprovalResponseV1 = {\n readonly status: string;\n readonly signed_approvals_b64?: readonly string[];\n readonly error?: string;\n readonly server_request_id?: string;\n};\n\nexport type SignApprovalOptions = {\n /** Who approved, in the approver system's own terms. Required. */\n readonly externalId: string;\n /** Approval lifetime. Default one hour, always clamped to the warrant's expiry when given. */\n readonly ttlSeconds?: number;\n readonly warrantExpiresAt?: number;\n};\n\n/** Decoded approval envelope. Not authorization. */\nexport type ApprovalInfo = {\n readonly approverPublicKey: string;\n readonly requestHash: string;\n readonly externalId: string;\n readonly approvedAt: number;\n readonly expiresAt: number;\n readonly expired: boolean;\n readonly signatureValid: boolean;\n readonly error?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Sessions\n// ---------------------------------------------------------------------------\n\nexport type SessionAllow = {\n readonly [capability: string]: AllowPolicy;\n};\n\n/** Named level or a number 0-255. Delegation can only lower it. */\nexport type Clearance =\n | number\n | \"untrusted\"\n | \"external\"\n | \"partner\"\n | \"internal\"\n | \"privileged\"\n | \"system\";\n\nexport type SessionInput = {\n /** Capability map. Optional when `tools` is set. Issuer sessions may omit it. */\n readonly allow?: SessionAllow;\n /** Wrapped tools from `tenuo.tool()`. Their `allow` is minted into the session. */\n readonly tools?: readonly object[];\n readonly ttlSeconds?: number;\n readonly requireApproval?: RequireApproval;\n /**\n * Issue to another agent's key instead of a fresh local holder. The\n * returned session cannot authorize here (it has no holder secret); send\n * `toWire()` to that agent, which imports it with `sessionFromWire()`.\n */\n readonly holder?: PublicKeyHandle;\n /**\n * How many times this authority may be delegated below the root.\n * `0` makes the session terminal. Omit for the protocol maximum.\n * Delegation can lower this ceiling and never raise it.\n */\n readonly maxDepth?: number;\n /**\n * `\"execution\"` (default) can call tools. `\"issuer\"` cannot call tools; its\n * holder can mint execution sessions with `tenuo.issue()` for the tools in\n * `issuableTools`, within `constraintBounds`, without the root key.\n */\n readonly kind?: \"execution\" | \"issuer\";\n readonly clearance?: Clearance;\n /** Free-form task correlation id. Inherited unchanged through delegation. */\n readonly sessionId?: string;\n /** Free-form agent label. Delegation may change it. */\n readonly agentId?: string;\n /** Issuer sessions only. */\n readonly issuableTools?: readonly string[];\n /** Issuer sessions only: ceiling on the constraints of anything issued, by field. */\n readonly constraintBounds?: AllowPolicy;\n /** Issuer sessions only: how deep issued sessions may themselves delegate. */\n readonly maxIssueDepth?: number;\n};\n\n/** Options for `tenuo.narrow()`. All optional; the default keeps the holder. */\nexport type NarrowOptions = {\n /**\n * Bind the child to another agent's key. This is delegation: the current\n * holder signs, the child belongs to `holder`, and the returned session\n * cannot authorize here. Hand `toWire()` to that agent.\n */\n readonly holder?: PublicKeyHandle;\n /** Child lifetime. Clamped to what the parent has left. */\n readonly ttlSeconds?: number;\n /** The child's holder cannot delegate further. */\n readonly terminal?: boolean;\n /** Lower the delegation ceiling. Cannot exceed the parent's. */\n readonly maxDepth?: number;\n /** Lower the clearance. Cannot exceed the parent's. */\n readonly clearance?: Clearance;\n readonly agentId?: string;\n /** Approvers can only be added. */\n readonly addApprovers?: readonly PublicKeyHandle[];\n /** The threshold can only go up. */\n readonly minApprovals?: number;\n};\n\n/** Options for `tenuo.issue()`: an execution session minted from an issuer session. */\nexport type IssueInput = {\n readonly allow: SessionAllow;\n /** The distinct agent that will use it. Issuer/holder separation is enforced. */\n readonly holder: PublicKeyHandle;\n readonly ttlSeconds?: number;\n readonly maxDepth?: number;\n readonly clearance?: Clearance;\n readonly sessionId?: string;\n readonly agentId?: string;\n /** Approval policy for the issued execution session. Defaults to gating every issued tool. */\n readonly requireApproval?: RequireApproval;\n};\n\n/** Public view of a session's leaf. Never includes the holder secret. */\nexport type SessionInfo = {\n readonly kind: \"execution\" | \"issuer\";\n /** Hex public key the leaf is bound to. */\n readonly holderPublicKey: string;\n /** Hex public key that signed the root of the chain. */\n readonly rootPublicKey: string;\n /** 0 for a root session, +1 per delegation. */\n readonly depth: number;\n /** Delegation ceiling in force for this leaf. */\n readonly maxDepth: number;\n /** True when `depth >= maxDepth`: cannot be narrowed further. */\n readonly terminal: boolean;\n /** Unix seconds. */\n readonly expiresAt: number;\n readonly tools: readonly string[];\n /** Warrant ids, root first. */\n readonly warrantIds: readonly string[];\n /**\n * True when this process holds the leaf's holder secret. False for a\n * session issued or delegated to another agent: `toWire()` works,\n * `execute` and `narrow` do not.\n */\n readonly canAuthorize: boolean;\n readonly clearance?: number;\n readonly sessionId?: string;\n readonly agentId?: string;\n readonly issuableTools?: readonly string[];\n readonly maxIssueDepth?: number;\n readonly requiredApprovers?: readonly string[];\n readonly minApprovals?: number;\n readonly approvalGatedTools: readonly string[];\n};\n\n/** One constrained argument in an `explain()` result. */\nexport type ExplainedField = {\n readonly field: string;\n /** Core constraint type name, e.g. `\"Subpath\"`, `\"Range\"`. */\n readonly kind: string;\n /** The constraint as core serializes it. */\n readonly constraint: unknown;\n /** Absent when the argument was not supplied. */\n readonly value?: unknown;\n readonly satisfied: boolean;\n /** Stable machine-readable diagnostic, when the core can identify one. */\n readonly reasonCode?: string;\n readonly reason?: string;\n};\n\n/** What a session would decide for `tool(args)`, field by field. No proof-of-possession involved. */\nexport type Explanation = {\n readonly tool: string;\n readonly kind: \"execution\" | \"issuer\";\n readonly outcome: \"allow\" | \"deny\";\n readonly code?: TenuoErrorCode;\n readonly field?: string;\n readonly message?: string;\n readonly toolGranted: boolean;\n readonly fields: readonly ExplainedField[];\n /** Arguments the session does not name. Zero-trust policies reject these. */\n readonly unknownFields: readonly string[];\n /** Constrained arguments the call did not supply. */\n readonly missingFields: readonly string[];\n readonly expired: boolean;\n readonly expiresAt: number;\n readonly chainValid: boolean;\n readonly chainError?: TenuoErrorCode;\n};\n\nexport type DevRoot = {\n readonly kind: \"dev-root\";\n /**\n * Required when `NODE_ENV` is unset or is not `development` / `test`.\n * Unset `NODE_ENV` is not treated as development.\n */\n readonly allowInProduction?: boolean;\n};\n\n/** A stable issuer secret: this process is a control plane. */\nexport type IssuerKey = {\n readonly kind: \"issuer-key\";\n readonly secret: Uint8Array;\n};\n\nexport type PublicKeyHandle = {\n readonly kind: \"public-key\";\n readonly source: \"env\" | \"hex\" | \"bytes\";\n readonly hex: string;\n};\n\nexport type WarrantPart = {\n readonly payload_hex: string;\n readonly signature_hex: string;\n};\n\nexport type SessionFromWireInput = {\n readonly warrant: string | readonly string[] | readonly WarrantPart[];\n readonly holderKey: Uint8Array;\n};\n\n/** Field-level (`{ path: under(\"/data\") }`) or per-capability (`{ read_file: { path: ... } }`). */\nexport type NarrowInput = AllowPolicy | SessionAllow;\n\nexport type CreateTenuoOptions = {\n readonly trustedRoots?: readonly PublicKeyHandle[];\n /**\n * `devRoot()` for development; an `issuerKey*` for a control plane that\n * mints with a stable key; a public key for a verifier. An issuer key\n * trusts itself plus `trustedRoots`.\n */\n readonly root?: DevRoot | IssuerKey | PublicKeyHandle;\n /** Published SignedRevocationList (hex or standard base64). Verified against a trusted root. */\n readonly revocationList?: string | Uint8Array;\n /** 32-byte holder seed used to sign verifier receipts. Runtime supplies this. */\n readonly receiptSigner?: Uint8Array;\n};\n\nexport type ApprovalRequestSummary = {\n readonly tool: string;\n readonly required: number;\n readonly received: number;\n};\n\nexport type Decision =\n | { readonly outcome: \"allow\"; readonly args: Readonly<Record<string, unknown>> }\n | { readonly outcome: \"deny\"; readonly code: TenuoErrorCode; readonly field?: string }\n | { readonly outcome: \"approval_required\"; readonly request: ApprovalRequestSummary };\n\n// ---------------------------------------------------------------------------\n// Revocation and receipts\n// ---------------------------------------------------------------------------\n\nexport type RevocationListInput = {\n readonly revoke: readonly string[];\n /** Monotonic; verifiers refuse an older version. Default 1. */\n readonly version?: number;\n};\n\nexport type RevocationListInfo = {\n readonly version: number;\n readonly issuedAt: number;\n readonly issuerPublicKey: string;\n readonly revokedIds: readonly string[];\n readonly signatureValid: boolean;\n};\n\n/** Signature authenticity of a receipt. Not authorization. */\nexport type ReceiptInfo = {\n readonly authentic: true;\n /** Hex key the receipt is signed under — resolve against your own authorizer set. */\n readonly signerKey: string;\n readonly outcome: \"allow\" | \"deny\";\n readonly action: string;\n readonly decisionCode?: string;\n readonly requestId: string;\n readonly srlVersion?: number;\n readonly srlHash?: string;\n readonly requestHash?: string;\n readonly policyDefinitionHash?: string;\n readonly prevReceiptHash?: string;\n readonly trustedRootsHash?: string;\n};\n\n/** Root-anchored receipt check: the embedded chain against trusted roots at decision time. */\nexport type ReceiptChainInfo = {\n readonly signerKey: string;\n readonly outcome: \"allow\" | \"deny\";\n readonly decisionCode?: string;\n readonly timestamp: number;\n readonly chainValid: boolean;\n readonly chainError?: string;\n readonly corroboratesDenial?: boolean;\n readonly rootIssuer?: string;\n readonly leafHolder?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Presenting authority across any boundary\n// ---------------------------------------------------------------------------\n\n/** Warrant chain plus proof-of-possession for one call. Transport-agnostic. */\nexport type PresentedCall = {\n readonly warrant: string;\n readonly signature: string;\n readonly approvals?: readonly string[];\n};\n\nexport interface Session {\n readonly [Symbol.toStringTag]: \"TenuoSession\";\n /** Warrant tokens, root first. Does not include the holder secret. */\n toWire(): readonly string[];\n /** SHA-256 of `(warrant_id, tool, canonical args)`. App-level idempotency, not PoP. */\n dedupKey(tool: string, args: Readonly<Record<string, unknown>>): string;\n /** Holder public key, depth, ceiling, lifetime, tools. Never the secret. */\n inspect(): SessionInfo;\n /**\n * Receipt helpers below are required for `createTenuo.runtime({ receipts:\n * \"collect\" })`. Implementers that are not runtime sessions can return\n * empty snapshots and `0` from acknowledge.\n */\n /**\n * Receipts collected for this session and not yet drained. Empty when the\n * session was not created by a Runtime with `receipts: \"collect\"`.\n */\n peekReceipts(): readonly string[];\n /**\n * Snapshot of collected receipts. Same as `peekReceipts`; nothing is removed\n * until `acknowledgeReceipts`.\n */\n drainReceipts(): readonly string[];\n /** Remove the first `count` pending receipts. */\n acknowledgeReceipts(count: number): number;\n}\n\nexport interface Tenuo {\n /**\n * Wrap any `{ execute }` tool (Vercel AI SDK, Mastra, plain object).\n * `parameters` / Zod stay on the inner tool and are never treated as authority.\n */\n tool<T extends { execute: (args: never) => unknown }>(\n inner: T,\n policy: ToolPolicy<Parameters<T[\"execute\"]>[0]>,\n ): ProtectedTool<T>;\n session(input: SessionInput): Session;\n /** Import a warrant minted elsewhere (Rust / Python / another process). */\n sessionFromWire(input: SessionFromWireInput): Session;\n withSession<R>(session: Session, fn: () => R): R;\n /**\n * Child session with less authority. Without options the same holder keeps\n * it; with `options.holder` it is delegated to another agent's key. Core\n * rejects any child that is not within its parent before a token exists.\n */\n narrow(session: Session, allow: NarrowInput, options?: NarrowOptions): Session;\n /**\n * Mint an execution session from an issuer session, signed by the issuer\n * session's holder. Core checks tools against `issuableTools`, constraints\n * against `constraintBounds`, clearance, and issue depth. No root key.\n */\n issue(issuer: Session, input: IssueInput): Session;\n /**\n * Public key of the local issuer (dev root or issuer key). Other processes\n * put it in `trustedRoots` to accept warrants this context mints.\n */\n issuerPublicKey(): PublicKeyHandle;\n /** What the session would decide for `tool(args)`, field by field. Never runs the tool. */\n explain(session: Session, tool: string, args: Readonly<Record<string, unknown>>): Explanation;\n /** Everything an approval service needs to present `tool(args)` to a human. */\n approvalRequest(session: Session, tool: string, args: Readonly<Record<string, unknown>>): ApprovalRequest;\n /** Holder-signed proof that this session is the one asking for `tool(args)`. */\n attestApprovalRequest(\n session: Session,\n tool: string,\n args: Readonly<Record<string, unknown>>,\n ): ApprovalContextAttestation;\n /** Authorize locally, then produce warrant + proof-of-possession for another boundary to verify. */\n present(\n session: Session,\n tool: string,\n args: Readonly<Record<string, unknown>>,\n options?: McpAttachOptions,\n ): PresentedCall;\n /**\n * Verify a presented call at any boundary. Resolves with the authorized\n * arguments; the tool must not run otherwise. `args` is the object already\n * given to this function. A repeated key in the original JSON is not\n * visible. Parse that text with `parseStrictJson` while it is still available.\n */\n verify(\n presented: PresentedCall,\n tool: string,\n args: Readonly<Record<string, unknown>>,\n options?: McpVerifyOptions,\n ): Promise<Readonly<Record<string, unknown>>>;\n /** Sign a revocation list with this context's issuer key. Load it with `revoke()` where this issuer is trusted. */\n revocationList(input: RevocationListInput): string;\n /** Load a published SignedRevocationList. Rust verifies the issuer against trusted roots. */\n revoke(list: string | Uint8Array): void;\n /** MCP `_meta.tenuo` attach / verify. No MCP SDK dependency. */\n readonly mcp: TenuoMcp;\n ready(): void;\n}\n\n/** Wire envelope Python and TypeScript both read from `params._meta`. */\nexport type TenuoMcpMeta = {\n readonly tenuo: PresentedCall;\n};\n\nexport type McpAttachOptions = {\n readonly approvals?: readonly (string | Uint8Array)[];\n readonly onReceipt?: (receipt: string) => void | Promise<void>;\n /**\n * Correlation handle written to the receipt. Use whatever id the host\n * already has — a JSON-RPC id, a trace or span id — so a receipt can be\n * matched against your own logs. Not an identity: nothing enforces\n * uniqueness, and distinguishing repeated calls is the receipt chain's job.\n */\n readonly requestId?: string;\n};\n\n/** Host ceiling and optional receipt hook on `mcp.handler()`. */\nexport type McpHandlerPolicy = {\n readonly allow?: AllowPolicy;\n readonly onReceipt?: (receipt: string) => void | Promise<void>;\n readonly nonceStore?: NonceStore;\n /**\n * Isolated. Called when `nonceStore.checkAndRecord` throws or rejects.\n * The client only ever sees \"Replay store unavailable\".\n */\n readonly onNonceStoreError?: (error: unknown) => void | Promise<void>;\n};\n\nexport type McpVerifyOptions = {\n readonly allow?: AllowPolicy;\n readonly onReceipt?: (receipt: string) => void | Promise<void>;\n /**\n * Opt-in exact-PoP replay check. `checkAndRecord` may return a Promise\n * (Redis). A rejected Promise fails closed. In-memory stores do not work\n * across processes. PoP v1 is otherwise replayable in-window, including\n * envelopes that already carry approvals.\n */\n readonly nonceStore?: NonceStore;\n /**\n * Isolated. Called when `nonceStore.checkAndRecord` throws or rejects.\n * The client only ever sees \"Replay store unavailable\".\n */\n readonly onNonceStoreError?: (error: unknown) => void | Promise<void>;\n /**\n * Correlation handle written to the receipt. Use whatever id the host\n * already has — a JSON-RPC id, a trace or span id — so a receipt can be\n * matched against your own logs. Not an identity: nothing enforces\n * uniqueness, and distinguishing repeated calls is the receipt chain's job.\n */\n readonly requestId?: string;\n};\n\n/**\n * Returns true if the PoP is fresh and recorded; false if this exact token\n * was already consumed. Async stores (Redis) return `Promise<boolean>`.\n * `verify()` / handlers await the result. Never return a Promise from a\n * store that is only consulted synchronously — that used to fail open.\n */\nexport type NonceStore = {\n checkAndRecord(popSignature: string): boolean | Promise<boolean>;\n};\n\nexport type McpCallParams = {\n readonly name: string;\n readonly arguments: Readonly<Record<string, unknown>>;\n readonly _meta: TenuoMcpMeta;\n};\n\nexport type McpJsonRpcError = {\n readonly code: -32602 | -32001 | -32002;\n readonly message: string;\n readonly data?: { readonly tenuo?: { readonly code: TenuoErrorCode } };\n};\n\nexport interface TenuoMcp {\n /** Local authorize, then put warrant + PoP on `_meta.tenuo`. */\n attach(\n session: Session,\n name: string,\n args: Readonly<Record<string, unknown>>,\n options?: McpAttachOptions,\n ): McpCallParams;\n /**\n * Server path. Verifies a presented warrant + PoP against the object it was\n * given. A repeated key in the original JSON is not visible: the host has\n * already parsed `args`. Parse that text with `parseStrictJson` while it is\n * still available. The tool handler must not run unless this returns.\n */\n verify(\n name: string,\n args: Readonly<Record<string, unknown>>,\n meta: unknown,\n options?: McpVerifyOptions,\n ): Promise<Readonly<Record<string, unknown>>>;\n /** Wrap a handler: verify from `extra._meta` / `extra.meta`, then execute. */\n handler<TArgs extends Record<string, unknown>, TResult>(\n name: string,\n execute: (args: TArgs) => TResult | Promise<TResult>,\n ): (\n args: TArgs,\n extra?: { readonly _meta?: unknown; readonly meta?: unknown },\n ) => Promise<TResult>;\n handler<TArgs extends Record<string, unknown>, TResult>(\n name: string,\n policy: McpHandlerPolicy,\n execute: (args: TArgs) => TResult | Promise<TResult>,\n ): (\n args: TArgs,\n extra?: { readonly _meta?: unknown; readonly meta?: unknown },\n ) => Promise<TResult>;\n jsonRpcError(error: unknown): McpJsonRpcError;\n}\n"]}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * Frozen public contract for `@tenuo/core`.\n *\n * Names and shapes here are the review surface — do not grow this file with\n * protocol vocabulary (`Warrant`, `mint`, `guard`) as the lead API.\n */\n\n/** Canonical kebab-case codes from Rust `ErrorCode::name()` / `Denial::code()`. */\nexport type ProtocolDenialCode =\n | \"signature-invalid\"\n | \"untrusted-root\"\n | \"chain-broken\"\n | \"depth-exceeded\"\n | \"warrant-expired\"\n | \"tool-not-authorized\"\n | \"constraint-violation\"\n | \"pop-signature-invalid\"\n | \"approval-required\"\n | \"insufficient-approvals\"\n | \"warrant-revoked\";\n\n/** Legacy SCREAMING_SNAKE aliases accepted by existing TypeScript callers. */\nexport type LegacyTenuoErrorCode =\n | \"TENUO_NOT_READY\"\n | \"TENUO_CONFIGURATION\"\n | \"TENUO_CONSTRAINT_VIOLATION\"\n | \"TENUO_TOOL_NOT_AUTHORIZED\"\n | \"TENUO_WARRANT_EXPIRED\"\n | \"TENUO_UNTRUSTED_ROOT\"\n | \"TENUO_INVALID_POP\"\n | \"TENUO_SIGNATURE_INVALID\"\n | \"TENUO_CHAIN_INVALID\"\n | \"TENUO_DEPTH_EXCEEDED\"\n | \"TENUO_REVOKED\"\n | \"TENUO_APPROVAL_REQUIRED\"\n | \"TENUO_INSUFFICIENT_APPROVALS\"\n | \"TENUO_CANONICALIZATION\"\n | \"TENUO_NOT_IMPLEMENTED\";\n\nexport type TenuoErrorCode = ProtocolDenialCode | LegacyTenuoErrorCode;\n\n// ---------------------------------------------------------------------------\n// Constraints. Every kind is evaluated in the Rust core; TypeScript only\n// builds the marker object. The set matches the core `Constraint` enum.\n// ---------------------------------------------------------------------------\n\n/** Path-traversal-safe prefix. Core evaluates this; TypeScript does not. */\nexport type UnderConstraint = {\n readonly kind: \"under\";\n readonly root: string;\n /** Default true. */\n readonly caseSensitive?: boolean;\n /** Whether the root itself is allowed. Default true. */\n readonly allowEqual?: boolean;\n};\n\nexport type EmailConstraint = {\n readonly kind: \"email\";\n readonly domain: string;\n};\n\nexport type MaxConstraint = {\n readonly kind: \"max\";\n readonly value: number;\n};\n\nexport type MinConstraint = {\n readonly kind: \"min\";\n readonly value: number;\n};\n\nexport type RangeConstraint = {\n readonly kind: \"range\";\n readonly min?: number;\n readonly max?: number;\n readonly minExclusive?: boolean;\n readonly maxExclusive?: boolean;\n};\n\nexport type OneOfConstraint = {\n readonly kind: \"oneOf\";\n readonly values: readonly string[];\n};\n\nexport type NotOneOfConstraint = {\n readonly kind: \"notOneOf\";\n readonly values: readonly string[];\n};\n\nexport type PatternConstraint = {\n readonly kind: \"pattern\";\n readonly pattern: string;\n};\n\nexport type RegexConstraint = {\n readonly kind: \"regex\";\n readonly source: string;\n};\n\nexport type ExactConstraint = {\n readonly kind: \"exact\";\n readonly value: string | number | boolean;\n};\n\nexport type WildcardConstraint = {\n readonly kind: \"wildcard\";\n};\n\nexport type CidrConstraint = {\n readonly kind: \"cidr\";\n readonly network: string;\n};\n\nexport type UrlPatternConstraint = {\n readonly kind: \"urlPattern\";\n readonly pattern: string;\n};\n\nexport type UrlSafeConstraint = {\n readonly kind: \"urlSafe\";\n readonly schemes?: readonly string[];\n readonly allowDomains?: readonly string[];\n readonly denyDomains?: readonly string[];\n};\n\nexport type ShlexConstraint = {\n readonly kind: \"shlex\";\n readonly allow: readonly string[];\n};\n\nexport type ContainsConstraint = {\n readonly kind: \"contains\";\n readonly values: readonly (string | number | boolean)[];\n};\n\nexport type SubsetConstraint = {\n readonly kind: \"subset\";\n readonly values: readonly (string | number | boolean)[];\n};\n\nexport type AnyOfConstraint = {\n readonly kind: \"anyOf\";\n readonly constraints: readonly ConstraintExpr[];\n};\n\nexport type AllConstraint = {\n readonly kind: \"all\";\n readonly constraints: readonly ConstraintExpr[];\n};\n\nexport type NotConstraint = {\n readonly kind: \"not\";\n readonly constraint: ConstraintExpr;\n};\n\nexport type CelConstraint = {\n readonly kind: \"cel\";\n readonly expression: string;\n};\n\nexport type ConstraintExpr =\n | UnderConstraint\n | EmailConstraint\n | MaxConstraint\n | MinConstraint\n | RangeConstraint\n | OneOfConstraint\n | NotOneOfConstraint\n | PatternConstraint\n | RegexConstraint\n | ExactConstraint\n | WildcardConstraint\n | CidrConstraint\n | UrlPatternConstraint\n | UrlSafeConstraint\n | ShlexConstraint\n | ContainsConstraint\n | SubsetConstraint\n | AnyOfConstraint\n | AllConstraint\n | NotConstraint\n | CelConstraint;\n\n/** Per-argument allow policy. Not a Zod schema. */\nexport type AllowPolicy = {\n readonly [field: string]: ConstraintExpr;\n};\n\n/**\n * Framework tool shape we wrap. No dependency on `ai` / Mastra.\n * `parameters` is host-schema (usually Zod) and is never treated as authority.\n */\nexport type ToolLike<\n TArgs extends Record<string, unknown> = Record<string, unknown>,\n TResult = unknown,\n> = {\n readonly description?: string;\n readonly parameters?: unknown;\n execute: (args: TArgs, options?: unknown) => TResult | Promise<TResult>;\n};\n\ntype KeysOfUnion<T> = T extends unknown ? keyof T : never;\n\nexport type ToolPolicy<TArgs = undefined> = {\n /**\n * Host ceiling. AND'd with the session in Rust.\n * Non-empty maps are zero-trust: every call argument must be named here.\n * `{}` means no extra ceiling (session/warrant only). Required: a missing\n * `allow` or an unknown policy key throws. `session({ tools })` refuses a\n * tool whose `allow` is `{}` unless the session's own `allow` names that\n * capability explicitly, because an empty capability in a warrant accepts\n * any arguments.\n */\n readonly allow: [NonNullable<TArgs>] extends [object]\n ? [KeysOfUnion<NonNullable<TArgs>>] extends [never]\n ? AllowPolicy\n : string extends KeysOfUnion<NonNullable<TArgs>>\n ? AllowPolicy\n : number extends KeysOfUnion<NonNullable<TArgs>>\n ? AllowPolicy\n : symbol extends KeysOfUnion<NonNullable<TArgs>>\n ? AllowPolicy\n : { readonly [Field in KeysOfUnion<NonNullable<TArgs>>]?: ConstraintExpr }\n : AllowPolicy;\n readonly capability?: string;\n /**\n * Makes approvals one-use for this tool. When a call that carried\n * `approvals` is allowed, each approval envelope that matches the call is\n * recorded here before the tool body runs; an envelope the store has\n * already seen denies with `TENUO_INSUFFICIENT_APPROVALS`. Same\n * `NonceStore` shape as the MCP verify path; `memoryNonceStore()` works\n * for one process. The store must remember a key for as long as an\n * approval can live (one hour by default), so give `memoryNonceStore()` a\n * matching `ttlSeconds`. A store that throws, rejects, or returns a\n * non-boolean fails closed with \"Replay store unavailable\".\n */\n readonly nonceStore?: NonceStore;\n /** Isolated. Called when `nonceStore.checkAndRecord` throws or rejects. */\n readonly onNonceStoreError?: (error: unknown) => void | Promise<void>;\n};\n\n/** Compile-time brand only — never assigned at runtime. Do not `in`/`has` this key. */\ndeclare const protectedBrand: unique symbol;\n\n/**\n * Tenuo's per-call options. Other keys pass through to the wrapped tool's own\n * `execute`, except keys that look like a misspelled Tenuo option\n * (`sesion`, `approval`, `onReciept`), which throw instead of being ignored.\n */\nexport type ExecuteOptions = {\n readonly session?: Session;\n /** Already-signed approval envelopes (hex, standard base64, or CBOR bytes). */\n readonly approvals?: readonly (string | Uint8Array)[];\n /**\n * Signed evidence of the decision. Not a security hook.\n * Exceptions (sync or rejected Promise) are isolated and never deny or\n * fail the tool.\n */\n readonly onReceipt?: (receipt: string) => void | Promise<void>;\n /**\n * Correlation handle written to the receipt. Use whatever id the host\n * already has — a JSON-RPC id, a trace or span id — so a receipt can be\n * matched against your own logs. Not an identity: nothing enforces\n * uniqueness, and distinguishing repeated calls is the receipt chain's job.\n */\n readonly requestId?: string;\n};\n\ntype ExtraExecuteOptions<T extends { execute: (args: never) => unknown }> =\n T[\"execute\"] extends (args: never, options?: infer O) => unknown\n ? unknown extends O\n ? object\n : Omit<NonNullable<O>, keyof ExecuteOptions>\n : object;\n\n/** Wrapped tool. `execute` still authorizes; optional `{ session }` overrides ALS. */\nexport type ProtectedTool<T extends { execute: (args: never) => unknown }> = Omit<T, \"execute\"> & {\n execute: (\n args: Parameters<T[\"execute\"]>[0],\n options?: ExecuteOptions & ExtraExecuteOptions<T>,\n ) => Promise<Awaited<ReturnType<T[\"execute\"]>>>;\n readonly [protectedBrand]: true;\n};\n\n// ---------------------------------------------------------------------------\n// Approvals\n// ---------------------------------------------------------------------------\n\n/** Per-argument trigger for an approval gate. */\nexport type ArgApprovalGate =\n /** Every value of this argument fires the gate. */\n | \"all\"\n /** Only values satisfying the constraint fire the gate. */\n | { readonly when: ConstraintExpr }\n /** Every value except those satisfying the constraint fires the gate. Narrowable across delegation. */\n | { readonly exempt: ConstraintExpr };\n\n/** Gate on one tool: a message for the approver and optional per-argument triggers. */\nexport type ToolApprovalGate = {\n readonly message?: string;\n /** Absent: the whole tool is gated. */\n readonly args?: { readonly [field: string]: ArgApprovalGate };\n};\n\n/** Approval requirement. Host collects SignedApproval envelopes; Rust decides. */\nexport type RequireApproval = {\n readonly approvers: readonly PublicKeyHandle[];\n readonly min: number;\n /** Whole-tool gates. Defaults to every tool in the session when `gates` is absent too. */\n readonly tools?: readonly string[];\n /** Per-tool gates with messages and per-argument triggers. */\n readonly gates?: { readonly [tool: string]: ToolApprovalGate };\n};\n\n/** What an approval service needs to present a call to a human. Not a signed artifact. */\nexport type ApprovalRequest = {\n readonly requestId: string;\n readonly warrantId: string;\n readonly tool: string;\n readonly args: Readonly<Record<string, unknown>>;\n /** Hex SHA-256 committing to warrant id, tool, canonical args, and holder. */\n readonly requestHash: string;\n readonly holderPublicKey: string;\n readonly requiredApprovers: readonly string[];\n readonly minApprovals: number;\n /** Unix seconds. An approval cannot outlive the warrant. */\n readonly warrantExpiresAt: number;\n readonly createdAt: number;\n readonly message: string;\n};\n\n/** Holder-signed statement that it is the one asking. Carried in a control-plane request. */\nexport type ApprovalContextAttestation = {\n readonly version: number;\n readonly canonicalization: string;\n readonly warrantId: string;\n readonly tool: string;\n readonly requestHash: string;\n readonly holderKeyHex: string;\n readonly argsCanonicalCborB64: string;\n readonly signerKeyHex: string;\n readonly signatureB64: string;\n};\n\n/** Wire body for a control-plane approval API. Field names are the wire names. */\nexport type ControlPlaneApprovalRequestV1 = {\n readonly schema_version: 1;\n readonly request_id_hex: string;\n readonly warrant_id: string;\n readonly tool: string;\n readonly arguments: Readonly<Record<string, unknown>>;\n readonly request_hash_hex: string;\n readonly holder_public_key_hex: string;\n readonly required_approver_keys_hex: readonly string[];\n readonly min_approvals: number;\n readonly warrant_expires_at_unix: number;\n readonly created_at_unix: number;\n readonly attestation?: Readonly<Record<string, unknown>>;\n readonly temporal?: Readonly<Record<string, string>>;\n readonly message?: string;\n};\n\nexport type ControlPlaneApprovalResponseV1 = {\n readonly status: string;\n readonly signed_approvals_b64?: readonly string[];\n readonly error?: string;\n readonly server_request_id?: string;\n};\n\nexport type SignApprovalOptions = {\n /** Who approved, in the approver system's own terms. Required. */\n readonly externalId: string;\n /** Approval lifetime. Default one hour, always clamped to the warrant's expiry when given. */\n readonly ttlSeconds?: number;\n readonly warrantExpiresAt?: number;\n};\n\n/** Decoded approval envelope. Not authorization. */\nexport type ApprovalInfo = {\n readonly approverPublicKey: string;\n readonly requestHash: string;\n readonly externalId: string;\n readonly approvedAt: number;\n readonly expiresAt: number;\n readonly expired: boolean;\n readonly signatureValid: boolean;\n readonly error?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Sessions\n// ---------------------------------------------------------------------------\n\nexport type SessionAllow = {\n readonly [capability: string]: AllowPolicy;\n};\n\n/** Named level or a number 0-255. Delegation can only lower it. */\nexport type Clearance =\n | number\n | \"untrusted\"\n | \"external\"\n | \"partner\"\n | \"internal\"\n | \"privileged\"\n | \"system\";\n\nexport type SessionInput = {\n /** Capability map. Optional when `tools` is set. Issuer sessions may omit it. */\n readonly allow?: SessionAllow;\n /** Wrapped tools from `tenuo.tool()`. Their `allow` is minted into the session. */\n readonly tools?: readonly object[];\n readonly ttlSeconds?: number;\n readonly requireApproval?: RequireApproval;\n /**\n * Issue to another agent's key instead of a fresh local holder. The\n * returned session cannot authorize here (it has no holder secret); send\n * `toWire()` to that agent, which imports it with `sessionFromWire()`.\n */\n readonly holder?: PublicKeyHandle;\n /**\n * How many times this authority may be delegated below the root.\n * `0` makes the session terminal. Omit for the protocol maximum.\n * Delegation can lower this ceiling and never raise it.\n */\n readonly maxDepth?: number;\n /**\n * `\"execution\"` (default) can call tools. `\"issuer\"` cannot call tools; its\n * holder can mint execution sessions with `tenuo.issue()` for the tools in\n * `issuableTools`, within `constraintBounds`, without the root key.\n */\n readonly kind?: \"execution\" | \"issuer\";\n readonly clearance?: Clearance;\n /** Free-form task correlation id. Inherited unchanged through delegation. */\n readonly sessionId?: string;\n /** Free-form agent label. Delegation may change it. */\n readonly agentId?: string;\n /** Issuer sessions only. */\n readonly issuableTools?: readonly string[];\n /** Issuer sessions only: ceiling on the constraints of anything issued, by field. */\n readonly constraintBounds?: AllowPolicy;\n /** Issuer sessions only: how deep issued sessions may themselves delegate. */\n readonly maxIssueDepth?: number;\n};\n\n/** Options for `tenuo.narrow()`. All optional; the default keeps the holder. */\nexport type NarrowOptions = {\n /**\n * Bind the child to another agent's key. This is delegation: the current\n * holder signs, the child belongs to `holder`, and the returned session\n * cannot authorize here. Hand `toWire()` to that agent.\n */\n readonly holder?: PublicKeyHandle;\n /**\n * Child lifetime. A value longer than the parent has left is clamped to the\n * parent's expiry, not rejected: the child never outlives the parent.\n */\n readonly ttlSeconds?: number;\n /** The child's holder cannot delegate further. */\n readonly terminal?: boolean;\n /** Lower the delegation ceiling. Cannot exceed the parent's. */\n readonly maxDepth?: number;\n /** Lower the clearance. Cannot exceed the parent's. */\n readonly clearance?: Clearance;\n readonly agentId?: string;\n /** Approvers can only be added. */\n readonly addApprovers?: readonly PublicKeyHandle[];\n /** The threshold can only go up. A value below the parent's throws. */\n readonly minApprovals?: number;\n};\n\n/** Options for `tenuo.issue()`: an execution session minted from an issuer session. */\nexport type IssueInput = {\n readonly allow: SessionAllow;\n /** The distinct agent that will use it. Issuer/holder separation is enforced. */\n readonly holder: PublicKeyHandle;\n readonly ttlSeconds?: number;\n readonly maxDepth?: number;\n readonly clearance?: Clearance;\n readonly sessionId?: string;\n readonly agentId?: string;\n /** Approval policy for the issued execution session. Defaults to gating every issued tool. */\n readonly requireApproval?: RequireApproval;\n};\n\n/** Public view of a session's leaf. Never includes the holder secret. */\nexport type SessionInfo = {\n readonly kind: \"execution\" | \"issuer\";\n /** Hex public key the leaf is bound to. */\n readonly holderPublicKey: string;\n /** Hex public key that signed the root of the chain. */\n readonly rootPublicKey: string;\n /** 0 for a root session, +1 per delegation. */\n readonly depth: number;\n /** Delegation ceiling in force for this leaf. */\n readonly maxDepth: number;\n /** True when `depth >= maxDepth`: cannot be narrowed further. */\n readonly terminal: boolean;\n /** Unix seconds. */\n readonly expiresAt: number;\n readonly tools: readonly string[];\n /** Warrant ids, root first. */\n readonly warrantIds: readonly string[];\n /**\n * True when this process holds the leaf's holder secret. False for a\n * session issued or delegated to another agent: `toWire()` works,\n * `execute` and `narrow` do not.\n */\n readonly canAuthorize: boolean;\n readonly clearance?: number;\n readonly sessionId?: string;\n readonly agentId?: string;\n readonly issuableTools?: readonly string[];\n readonly maxIssueDepth?: number;\n readonly requiredApprovers?: readonly string[];\n readonly minApprovals?: number;\n readonly approvalGatedTools: readonly string[];\n};\n\n/** One constrained argument in an `explain()` result. */\nexport type ExplainedField = {\n readonly field: string;\n /** Core constraint type name, e.g. `\"Subpath\"`, `\"Range\"`. */\n readonly kind: string;\n /** The constraint as core serializes it. */\n readonly constraint: unknown;\n /** Absent when the argument was not supplied. */\n readonly value?: unknown;\n readonly satisfied: boolean;\n /** Stable machine-readable diagnostic, when the core can identify one. */\n readonly reasonCode?: string;\n readonly reason?: string;\n};\n\n/** What a session would decide for `tool(args)`, field by field. No proof-of-possession involved. */\nexport type Explanation = {\n readonly tool: string;\n readonly kind: \"execution\" | \"issuer\";\n readonly outcome: \"allow\" | \"deny\";\n readonly code?: TenuoErrorCode;\n readonly field?: string;\n readonly message?: string;\n readonly toolGranted: boolean;\n readonly fields: readonly ExplainedField[];\n /** Arguments the session does not name. Zero-trust policies reject these. */\n readonly unknownFields: readonly string[];\n /** Constrained arguments the call did not supply. */\n readonly missingFields: readonly string[];\n readonly expired: boolean;\n readonly expiresAt: number;\n readonly chainValid: boolean;\n readonly chainError?: TenuoErrorCode;\n};\n\nexport type DevRoot = {\n readonly kind: \"dev-root\";\n /**\n * Required when `NODE_ENV` is unset or is not `development` / `test`.\n * Unset `NODE_ENV` is not treated as development.\n */\n readonly allowInProduction?: boolean;\n};\n\n/** A stable issuer secret: this process is a control plane. */\nexport type IssuerKey = {\n readonly kind: \"issuer-key\";\n readonly secret: Uint8Array;\n};\n\nexport type PublicKeyHandle = {\n readonly kind: \"public-key\";\n readonly source: \"env\" | \"hex\" | \"bytes\";\n readonly hex: string;\n};\n\nexport type WarrantPart = {\n readonly payload_hex: string;\n readonly signature_hex: string;\n};\n\nexport type SessionFromWireInput = {\n readonly warrant: string | readonly string[] | readonly WarrantPart[];\n readonly holderKey: Uint8Array;\n};\n\n/** Field-level (`{ path: under(\"/data\") }`) or per-capability (`{ read_file: { path: ... } }`). */\nexport type NarrowInput = AllowPolicy | SessionAllow;\n\nexport type CreateTenuoOptions = {\n readonly trustedRoots?: readonly PublicKeyHandle[];\n /**\n * `devRoot()` for development; an `issuerKey*` for a control plane that\n * mints with a stable key; a public key for a verifier. An issuer key\n * trusts itself plus `trustedRoots`.\n */\n readonly root?: DevRoot | IssuerKey | PublicKeyHandle;\n /** Published SignedRevocationList (hex or standard base64). Verified against a trusted root. */\n readonly revocationList?: string | Uint8Array;\n /** 32-byte holder seed used to sign verifier receipts. Runtime supplies this. */\n readonly receiptSigner?: Uint8Array;\n};\n\nexport type ApprovalRequestSummary = {\n readonly tool: string;\n readonly required: number;\n readonly received: number;\n};\n\nexport type Decision =\n | { readonly outcome: \"allow\"; readonly args: Readonly<Record<string, unknown>> }\n | { readonly outcome: \"deny\"; readonly code: TenuoErrorCode; readonly field?: string }\n | { readonly outcome: \"approval_required\"; readonly request: ApprovalRequestSummary };\n\n// ---------------------------------------------------------------------------\n// Revocation and receipts\n// ---------------------------------------------------------------------------\n\nexport type RevocationListInput = {\n readonly revoke: readonly string[];\n /** Monotonic; verifiers refuse an older version. Default 1. */\n readonly version?: number;\n};\n\nexport type RevocationListInfo = {\n readonly version: number;\n readonly issuedAt: number;\n readonly issuerPublicKey: string;\n readonly revokedIds: readonly string[];\n readonly signatureValid: boolean;\n};\n\n/** Signature authenticity of a receipt. Not authorization. */\nexport type ReceiptInfo = {\n readonly authentic: true;\n /** Hex key the receipt is signed under — resolve against your own authorizer set. */\n readonly signerKey: string;\n readonly outcome: \"allow\" | \"deny\";\n readonly action: string;\n readonly decisionCode?: string;\n readonly requestId: string;\n readonly srlVersion?: number;\n readonly srlHash?: string;\n readonly requestHash?: string;\n readonly policyDefinitionHash?: string;\n readonly prevReceiptHash?: string;\n readonly trustedRootsHash?: string;\n};\n\n/** Root-anchored receipt check: the embedded chain against trusted roots at decision time. */\nexport type ReceiptChainInfo = {\n readonly signerKey: string;\n readonly outcome: \"allow\" | \"deny\";\n readonly decisionCode?: string;\n readonly timestamp: number;\n readonly chainValid: boolean;\n readonly chainError?: string;\n readonly corroboratesDenial?: boolean;\n readonly rootIssuer?: string;\n readonly leafHolder?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Presenting authority across any boundary\n// ---------------------------------------------------------------------------\n\n/** Warrant chain plus proof-of-possession for one call. Transport-agnostic. */\nexport type PresentedCall = {\n readonly warrant: string;\n readonly signature: string;\n readonly approvals?: readonly string[];\n};\n\nexport interface Session {\n readonly [Symbol.toStringTag]: \"TenuoSession\";\n /** Warrant tokens, root first. Does not include the holder secret. */\n toWire(): readonly string[];\n /**\n * The whole chain as one string: a CBOR warrant stack in URL-safe base64,\n * the same encoding as `PresentedCall.warrant`. Use it where only a single\n * string fits (an environment variable, a header). `sessionFromWire()`\n * imports it. Does not include the holder secret.\n */\n toWireString(): string;\n /** SHA-256 of `(warrant_id, tool, canonical args)`. App-level idempotency, not PoP. */\n dedupKey(tool: string, args: Readonly<Record<string, unknown>>): string;\n /** Holder public key, depth, ceiling, lifetime, tools. Never the secret. */\n inspect(): SessionInfo;\n /**\n * Receipt helpers below are required for `createTenuo.runtime({ receipts:\n * \"collect\" })`. Implementers that are not runtime sessions can return\n * empty snapshots and `0` from acknowledge.\n */\n /**\n * Receipts collected for this session and not yet drained. Empty when the\n * session was not created by a Runtime with `receipts: \"collect\"`.\n */\n peekReceipts(): readonly string[];\n /**\n * Snapshot of collected receipts. Same as `peekReceipts`; nothing is removed\n * until `acknowledgeReceipts`.\n */\n drainReceipts(): readonly string[];\n /** Remove the first `count` pending receipts. */\n acknowledgeReceipts(count: number): number;\n}\n\nexport interface Tenuo {\n /**\n * Wrap any `{ execute }` tool (Vercel AI SDK, Mastra, plain object).\n * `parameters` / Zod stay on the inner tool and are never treated as authority.\n */\n tool<T extends { execute: (args: never) => unknown }>(\n inner: T,\n policy: ToolPolicy<Parameters<T[\"execute\"]>[0]>,\n ): ProtectedTool<T>;\n session(input: SessionInput): Session;\n /** Import a warrant minted elsewhere (Rust / Python / another process). */\n sessionFromWire(input: SessionFromWireInput): Session;\n withSession<R>(session: Session, fn: () => R): R;\n /**\n * Child session with less authority. Without options the same holder keeps\n * it; with `options.holder` it is delegated to another agent's key. Core\n * rejects any child that is not within its parent before a token exists.\n */\n narrow(session: Session, allow: NarrowInput, options?: NarrowOptions): Session;\n /**\n * Mint an execution session from an issuer session, signed by the issuer\n * session's holder. Core checks tools against `issuableTools`, constraints\n * against `constraintBounds`, clearance, and issue depth. No root key.\n */\n issue(issuer: Session, input: IssueInput): Session;\n /**\n * Public key of the local issuer (dev root or issuer key). Other processes\n * put it in `trustedRoots` to accept warrants this context mints.\n */\n issuerPublicKey(): PublicKeyHandle;\n /** What the session would decide for `tool(args)`, field by field. Never runs the tool. */\n explain(session: Session, tool: string, args: Readonly<Record<string, unknown>>): Explanation;\n /** Everything an approval service needs to present `tool(args)` to a human. */\n approvalRequest(session: Session, tool: string, args: Readonly<Record<string, unknown>>): ApprovalRequest;\n /** Holder-signed proof that this session is the one asking for `tool(args)`. */\n attestApprovalRequest(\n session: Session,\n tool: string,\n args: Readonly<Record<string, unknown>>,\n ): ApprovalContextAttestation;\n /** Authorize locally, then produce warrant + proof-of-possession for another boundary to verify. */\n present(\n session: Session,\n tool: string,\n args: Readonly<Record<string, unknown>>,\n options?: McpAttachOptions,\n ): PresentedCall;\n /**\n * Verify a presented call at any boundary. Resolves with the authorized\n * arguments; the tool must not run otherwise. `args` is the object already\n * given to this function. A repeated key in the original JSON is not\n * visible. Parse that text with `parseStrictJson` while it is still available.\n */\n verify(\n presented: PresentedCall,\n tool: string,\n args: Readonly<Record<string, unknown>>,\n options?: McpVerifyOptions,\n ): Promise<Readonly<Record<string, unknown>>>;\n /** Sign a revocation list with this context's issuer key. Load it with `revoke()` where this issuer is trusted. */\n revocationList(input: RevocationListInput): string;\n /** Load a published SignedRevocationList. Rust verifies the issuer against trusted roots. */\n revoke(list: string | Uint8Array): void;\n /** MCP `_meta.tenuo` attach / verify. No MCP SDK dependency. */\n readonly mcp: TenuoMcp;\n ready(): void;\n}\n\n/** Wire envelope Python and TypeScript both read from `params._meta`. */\nexport type TenuoMcpMeta = {\n readonly tenuo: PresentedCall;\n};\n\nexport type McpAttachOptions = {\n readonly approvals?: readonly (string | Uint8Array)[];\n readonly onReceipt?: (receipt: string) => void | Promise<void>;\n /**\n * Correlation handle written to the receipt. Use whatever id the host\n * already has — a JSON-RPC id, a trace or span id — so a receipt can be\n * matched against your own logs. Not an identity: nothing enforces\n * uniqueness, and distinguishing repeated calls is the receipt chain's job.\n */\n readonly requestId?: string;\n};\n\n/** Host ceiling and optional receipt hook on `mcp.handler()`. */\nexport type McpHandlerPolicy = {\n readonly allow?: AllowPolicy;\n readonly onReceipt?: (receipt: string) => void | Promise<void>;\n readonly nonceStore?: NonceStore;\n /**\n * Isolated. Called when `nonceStore.checkAndRecord` throws or rejects.\n * The client only ever sees \"Replay store unavailable\".\n */\n readonly onNonceStoreError?: (error: unknown) => void | Promise<void>;\n};\n\nexport type McpVerifyOptions = {\n readonly allow?: AllowPolicy;\n readonly onReceipt?: (receipt: string) => void | Promise<void>;\n /**\n * Opt-in exact-PoP replay check. `checkAndRecord` may return a Promise\n * (Redis). A rejected Promise fails closed. In-memory stores do not work\n * across processes. PoP v1 is otherwise replayable in-window, including\n * envelopes that already carry approvals.\n */\n readonly nonceStore?: NonceStore;\n /**\n * Isolated. Called when `nonceStore.checkAndRecord` throws or rejects.\n * The client only ever sees \"Replay store unavailable\".\n */\n readonly onNonceStoreError?: (error: unknown) => void | Promise<void>;\n /**\n * Correlation handle written to the receipt. Use whatever id the host\n * already has — a JSON-RPC id, a trace or span id — so a receipt can be\n * matched against your own logs. Not an identity: nothing enforces\n * uniqueness, and distinguishing repeated calls is the receipt chain's job.\n */\n readonly requestId?: string;\n};\n\n/**\n * Returns true if the PoP is fresh and recorded; false if this exact token\n * was already consumed. Async stores (Redis) return `Promise<boolean>`.\n * `verify()` / handlers await the result. Never return a Promise from a\n * store that is only consulted synchronously — that used to fail open.\n */\nexport type NonceStore = {\n checkAndRecord(popSignature: string): boolean | Promise<boolean>;\n};\n\nexport type McpCallParams = {\n readonly name: string;\n readonly arguments: Readonly<Record<string, unknown>>;\n readonly _meta: TenuoMcpMeta;\n};\n\nexport type McpJsonRpcError = {\n readonly code: -32602 | -32001 | -32002;\n readonly message: string;\n readonly data?: { readonly tenuo?: { readonly code: TenuoErrorCode } };\n};\n\nexport interface TenuoMcp {\n /** Local authorize, then put warrant + PoP on `_meta.tenuo`. */\n attach(\n session: Session,\n name: string,\n args: Readonly<Record<string, unknown>>,\n options?: McpAttachOptions,\n ): McpCallParams;\n /**\n * Server path. Verifies a presented warrant + PoP against the object it was\n * given. A repeated key in the original JSON is not visible: the host has\n * already parsed `args`. Parse that text with `parseStrictJson` while it is\n * still available. The tool handler must not run unless this returns.\n */\n verify(\n name: string,\n args: Readonly<Record<string, unknown>>,\n meta: unknown,\n options?: McpVerifyOptions,\n ): Promise<Readonly<Record<string, unknown>>>;\n /** Wrap a handler: verify from `extra._meta` / `extra.meta`, then execute. */\n handler<TArgs extends Record<string, unknown>, TResult>(\n name: string,\n execute: (args: TArgs) => TResult | Promise<TResult>,\n ): (\n args: TArgs,\n extra?: { readonly _meta?: unknown; readonly meta?: unknown },\n ) => Promise<TResult>;\n handler<TArgs extends Record<string, unknown>, TResult>(\n name: string,\n policy: McpHandlerPolicy,\n execute: (args: TArgs) => TResult | Promise<TResult>,\n ): (\n args: TArgs,\n extra?: { readonly _meta?: unknown; readonly meta?: unknown },\n ) => Promise<TResult>;\n jsonRpcError(error: unknown): McpJsonRpcError;\n}\n"]}
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "package": "@tenuo/core",
3
- "version": "0.3.2-beta.0",
4
- "sourceCommit": "7f27c7d4782f8f19ec15313e09ab95883a1da811",
3
+ "version": "0.3.3-beta.0",
4
+ "sourceCommit": "6d0b668f3d4d4da137967624eee5e307462ded54",
5
5
  "sourceClean": false,
6
- "wasmSha256": "071de0080d616e5519841930f0579e5234a93a9f29724595609744d43b7e9d4a",
6
+ "wasmSha256": "7fb51e3beda0f2f0cb91863456d22ecc9b139fdfa869e65dead03b315adc3c22",
7
7
  "wasmMatchesCommit": false
8
8
  }
package/dist/client.js CHANGED
@@ -3,12 +3,40 @@ import { randomBytes } from "node:crypto";
3
3
  import { parseConnectToken } from "./connect.js";
4
4
  import { generateIdentity, identityFromKey } from "./identity.js";
5
5
  import { createMcp, presentCall, verifyPresented } from "./mcp.js";
6
+ import { consumeApprovals, snapshotApprovals } from "./replay.js";
6
7
  import { collectReceipt, emitIsolatedReceipt, inheritSessionCollector } from "./receipts.js";
7
8
  import { createRuntime } from "./runtime.js";
8
9
  import { AuthorizationDeniedError, ApprovalRequiredError, TenuoConfigurationError, TenuoError } from "./errors.js";
9
10
  import { Session, isSession, nativeSession } from "./session.js";
10
11
  import { createDevContext, createIssuerContext, createVerifierContext, importSessionFromChain, importSessionFromWire, loadWasm, protocolLimits, publicKeyHexFromHolderKey, } from "./wasm.js";
11
12
  const currentSession = new AsyncLocalStorage();
13
+ const TOOL_POLICY_KEYS = ["allow", "capability", "nonceStore", "onNonceStoreError"];
14
+ const EXECUTE_OPTION_KEYS = ["session", "approvals", "onReceipt", "requestId"];
15
+ const SESSION_INPUT_KEYS = [
16
+ "allow",
17
+ "tools",
18
+ "ttlSeconds",
19
+ "requireApproval",
20
+ "holder",
21
+ "maxDepth",
22
+ "kind",
23
+ "clearance",
24
+ "sessionId",
25
+ "agentId",
26
+ "issuableTools",
27
+ "constraintBounds",
28
+ "maxIssueDepth",
29
+ ];
30
+ const ISSUE_INPUT_KEYS = [
31
+ "allow",
32
+ "holder",
33
+ "ttlSeconds",
34
+ "maxDepth",
35
+ "clearance",
36
+ "sessionId",
37
+ "agentId",
38
+ "requireApproval",
39
+ ];
12
40
  const toolPolicies = new WeakMap();
13
41
  const wrappedInners = new WeakSet();
14
42
  const CLEARANCE_NAMES = new Set(["untrusted", "external", "partner", "internal", "privileged", "system"]);
@@ -226,7 +254,12 @@ class TenuoClient {
226
254
  this.canMint = false;
227
255
  }
228
256
  if (options.revocationList !== undefined) {
229
- this.context.loadRevocationList(normalizeWireBytes(options.revocationList));
257
+ try {
258
+ this.context.loadRevocationList(normalizeWireBytes(options.revocationList));
259
+ }
260
+ catch (error) {
261
+ throw revocationLoadError(error);
262
+ }
230
263
  }
231
264
  this.decide = (decision, tool, native, args) => {
232
265
  this.applyDecision(decision, tool, native, args);
@@ -255,14 +288,20 @@ class TenuoClient {
255
288
  if (toolPolicies.has(inner) || wrappedInners.has(inner)) {
256
289
  throw new TenuoConfigurationError("tenuo.tool() already wrapped this tool");
257
290
  }
291
+ const { nonceStore, onNonceStoreError } = validateToolPolicy(policy);
258
292
  const capability = capabilityName(inner, policy);
259
293
  const original = inner.execute;
260
294
  const execute = async (args, callOptions) => {
295
+ assertExecuteOptions(callOptions);
261
296
  const session = resolveSession(callOptions);
262
297
  const native = nativeSession(session);
263
- const decision = this.context.authorize(native, capability, args, approvalsFrom(callOptions), policy.allow, requestIdFrom(callOptions));
298
+ const approvals = snapshotApprovals(approvalsFrom(callOptions));
299
+ const decision = this.context.authorize(native, capability, args, approvals, policy.allow, requestIdFrom(callOptions));
264
300
  emitReceipt(callOptions, decision.receipt, session, this);
265
301
  if (decision.outcome === "allow") {
302
+ if (nonceStore !== undefined) {
303
+ await consumeApprovals(nonceStore, approvals, this.requestHash(native, capability, decision.args), capability, onNonceStoreError);
304
+ }
266
305
  return original(plainArgs(decision.args), forwardExecuteOptions(callOptions));
267
306
  }
268
307
  this.applyDecision(decision, capability, native, plainArgs(args));
@@ -275,7 +314,20 @@ class TenuoClient {
275
314
  toolPolicies.set(wrapped, { capability, allow: { ...policy.allow } });
276
315
  return wrapped;
277
316
  }
317
+ /** The approval request hash for this call; approvals are bound to it. */
318
+ requestHash(native, tool, args) {
319
+ try {
320
+ return this.context.approvalRequest(native, tool, plainArgs(args)).request_hash;
321
+ }
322
+ catch (error) {
323
+ throw new TenuoConfigurationError(`could not bind approvals to this call: ${errorMessage(error)}`);
324
+ }
325
+ }
278
326
  session(input) {
327
+ if (input === null || typeof input !== "object") {
328
+ throw new TenuoConfigurationError("tenuo.session() requires an options object");
329
+ }
330
+ assertKnownInputKeys(input, SESSION_INPUT_KEYS, "tenuo.session()");
279
331
  const allow = collectSessionAllow(input);
280
332
  const kind = input.kind ?? "execution";
281
333
  if (kind !== "execution" && kind !== "issuer") {
@@ -289,7 +341,7 @@ class TenuoClient {
289
341
  }
290
342
  const options = { kind, allow };
291
343
  if (input.ttlSeconds !== undefined) {
292
- options.ttlSeconds = requireTtl(input.ttlSeconds, "session().ttlSeconds");
344
+ options.ttlSeconds = requireTtl(input.ttlSeconds, "session().ttlSeconds", 1);
293
345
  }
294
346
  if (input.holder !== undefined) {
295
347
  options.holder = requirePublicKey(input.holder, "session().holder").hex;
@@ -332,6 +384,7 @@ class TenuoClient {
332
384
  if (input === null || typeof input !== "object" || input.allow === undefined || Object.keys(input.allow).length === 0) {
333
385
  throw new TenuoConfigurationError("issue() requires at least one capability in allow");
334
386
  }
387
+ assertKnownInputKeys(input, ISSUE_INPUT_KEYS, "issue()");
335
388
  const options = {
336
389
  allow: input.allow,
337
390
  holder: requirePublicKey(input.holder, "issue().holder").hex,
@@ -374,6 +427,10 @@ class TenuoClient {
374
427
  if (typeof input.warrant === "string" && input.warrant.trim().length === 0) {
375
428
  throw new TenuoConfigurationError("sessionFromWire() requires a warrant");
376
429
  }
430
+ // Warrant strings are hex or base64; none of these characters occur in one.
431
+ if (typeof input.warrant === "string" && /^\[|[\s,.;]/.test(input.warrant.trim())) {
432
+ throw new TenuoConfigurationError("sessionFromWire() got a string that looks like several warrant tokens joined together. Pass the array from session.toWire(), or the single string from session.toWireString().");
433
+ }
377
434
  try {
378
435
  if (typeof input.warrant === "string") {
379
436
  return new Session(importSessionFromWire(input.warrant, input.holderKey));
@@ -401,6 +458,12 @@ class TenuoClient {
401
458
  throw new TenuoConfigurationError("narrow() requires a Tenuo Session, not a plain object");
402
459
  }
403
460
  const native = options === undefined ? undefined : narrowOptionsJson(options);
461
+ if (native?.minApprovals !== undefined) {
462
+ const parentMin = session.inspect().minApprovals;
463
+ if (parentMin !== undefined && native.minApprovals < parentMin) {
464
+ throw new TenuoConfigurationError(`narrow().minApprovals ${native.minApprovals} is below the parent's ${parentMin}. The approval threshold can only go up.`);
465
+ }
466
+ }
404
467
  try {
405
468
  const child = new Session(this.context.narrow(nativeSession(session), allow, native));
406
469
  inheritSessionCollector(session, child);
@@ -521,7 +584,12 @@ class TenuoClient {
521
584
  }
522
585
  }
523
586
  revoke(list) {
524
- this.context.loadRevocationList(normalizeWireBytes(list));
587
+ try {
588
+ this.context.loadRevocationList(normalizeWireBytes(list));
589
+ }
590
+ catch (error) {
591
+ throw revocationLoadError(error);
592
+ }
525
593
  }
526
594
  ready() {
527
595
  loadWasm();
@@ -533,6 +601,100 @@ function requireLabel(value, name) {
533
601
  }
534
602
  return value;
535
603
  }
604
+ function validateToolPolicy(policy) {
605
+ if (policy === null || typeof policy !== "object" || Array.isArray(policy)) {
606
+ throw new TenuoConfigurationError("tenuo.tool() requires a policy object: { allow, capability? }");
607
+ }
608
+ assertKnownInputKeys(policy, TOOL_POLICY_KEYS, "tenuo.tool() policy");
609
+ const record = policy;
610
+ if (record.allow === undefined) {
611
+ throw new TenuoConfigurationError("tenuo.tool() policy requires allow: name each argument with a constraint, or pass {} for no host ceiling");
612
+ }
613
+ if (record.allow === null || typeof record.allow !== "object" || Array.isArray(record.allow)) {
614
+ throw new TenuoConfigurationError("tenuo.tool() policy.allow must be an object of per-argument constraints");
615
+ }
616
+ const out = {};
617
+ if (record.nonceStore !== undefined) {
618
+ const store = record.nonceStore;
619
+ if (store === null || typeof store !== "object" || typeof store.checkAndRecord !== "function") {
620
+ throw new TenuoConfigurationError("tenuo.tool() policy.nonceStore must implement checkAndRecord()");
621
+ }
622
+ out.nonceStore = record.nonceStore;
623
+ }
624
+ if (record.onNonceStoreError !== undefined) {
625
+ if (typeof record.onNonceStoreError !== "function") {
626
+ throw new TenuoConfigurationError("tenuo.tool() policy.onNonceStoreError must be a function");
627
+ }
628
+ out.onNonceStoreError = record.onNonceStoreError;
629
+ }
630
+ return out;
631
+ }
632
+ function assertKnownInputKeys(value, known, label) {
633
+ for (const key of Object.keys(value)) {
634
+ if (!known.includes(key)) {
635
+ throw new TenuoConfigurationError(`${label}: unknown key '${key}' (expected ${known.join(", ")})`);
636
+ }
637
+ }
638
+ }
639
+ /**
640
+ * Execute options are shared with the wrapped tool, so unknown keys pass
641
+ * through. A key that is a near miss of a Tenuo option is a typo that would
642
+ * otherwise drop that option silently (`sesion` falls back to the ambient
643
+ * session), so it throws.
644
+ */
645
+ function assertExecuteOptions(callOptions) {
646
+ if (callOptions === null || typeof callOptions !== "object") {
647
+ return;
648
+ }
649
+ for (const key of Object.keys(callOptions)) {
650
+ if (EXECUTE_OPTION_KEYS.includes(key)) {
651
+ continue;
652
+ }
653
+ const lower = key.toLowerCase();
654
+ for (const known of EXECUTE_OPTION_KEYS) {
655
+ const target = known.toLowerCase();
656
+ const limit = known.length <= 7 ? 1 : 2;
657
+ if (lower === target || editDistance(lower, target) <= limit) {
658
+ throw new TenuoConfigurationError(`execute() options: unknown key '${key}'. Did you mean '${known}'? Tenuo options are ${EXECUTE_OPTION_KEYS.join(", ")}.`);
659
+ }
660
+ }
661
+ }
662
+ }
663
+ /** Optimal string alignment distance (adjacent transpositions count once). */
664
+ function editDistance(a, b) {
665
+ if (Math.abs(a.length - b.length) > 2) {
666
+ return 3;
667
+ }
668
+ const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
669
+ for (let i = 1; i <= a.length; i += 1) {
670
+ for (let j = 1; j <= b.length; j += 1) {
671
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
672
+ let best = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
673
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
674
+ best = Math.min(best, d[i - 2][j - 2] + 1);
675
+ }
676
+ d[i][j] = best;
677
+ }
678
+ }
679
+ return d[a.length][b.length];
680
+ }
681
+ function revocationLoadError(error) {
682
+ if (error instanceof TenuoError) {
683
+ return error;
684
+ }
685
+ // Strip the prefix the core adds so the code reflects the actual cause.
686
+ const message = errorMessage(error).replace(/^TENUO_[A-Z_]+:\s*/, "");
687
+ if (/rollback/i.test(message)) {
688
+ return new TenuoConfigurationError(message);
689
+ }
690
+ if (/issuer does not match|untrusted|not trusted/i.test(message)) {
691
+ return new TenuoConfigurationError(message, "TENUO_UNTRUSTED_ROOT");
692
+ }
693
+ if (/signature/i.test(message)) {
694
+ return new TenuoConfigurationError(message, "TENUO_SIGNATURE_INVALID");
695
+ }
696
+ return new TenuoConfigurationError(message);
697
+ }
536
698
  function errorMessage(error) {
537
699
  return error instanceof Error ? error.message : String(error);
538
700
  }
@@ -781,6 +943,9 @@ function collectSessionAllow(input) {
781
943
  if (policy === undefined) {
782
944
  throw new TenuoConfigurationError("session({ tools }) requires tools from tenuo.tool()");
783
945
  }
946
+ if (Object.keys(policy.allow).length === 0 && !Object.prototype.hasOwnProperty.call(input.allow ?? {}, policy.capability)) {
947
+ throw new TenuoConfigurationError(`session({ tools }): ${policy.capability} has allow: {}, which would put a capability that accepts any arguments in the warrant. Name its arguments in tenuo.tool({ allow }), or opt in explicitly with session({ tools, allow: { ${policy.capability}: {} } }).`);
948
+ }
784
949
  const existing = allow[policy.capability];
785
950
  if (existing !== undefined && !sameAllow(existing, policy.allow)) {
786
951
  throw new TenuoConfigurationError(`session() allow and tools disagree on ${policy.capability}`);
@@ -835,10 +1000,9 @@ export function signApproval(request, approverSecret, options) {
835
1000
  }
836
1001
  /** Decode an approval envelope. Not authorization: the call path still decides. */
837
1002
  export function inspectApproval(envelope) {
838
- const text = typeof envelope === "string" ? envelope : Buffer.from(envelope).toString("base64");
839
1003
  let raw;
840
1004
  try {
841
- raw = loadWasm().sdkInspectApproval(text);
1005
+ raw = loadWasm().sdkInspectApproval(envelope);
842
1006
  }
843
1007
  catch (error) {
844
1008
  throw new TenuoConfigurationError(errorMessage(error));