witnora 0.19.1 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,6 +90,20 @@ For a cloud Agent, the customer exposes only this path through its HTTPS ingress
90
90
  or private network. This generated endpoint is limited to the current sandbox
91
91
  Task/action path; it does not create a production Provider or broader authority.
92
92
 
93
+ An MCP client can use the same exact path without receiving either local
94
+ credential. Launch the stdio server from the onboarded repository:
95
+
96
+ ```bash
97
+ npx witnora@latest mcp
98
+ ```
99
+
100
+ It exposes one generated Action tool, for example `witnora_cancel-order`, plus
101
+ `witnora_get_action`. The Agent can propose an Action and inspect its state, but
102
+ there is no MCP approval, direct-execution, or self-verification tool. Human
103
+ approval, customer-owned execution, the separate Probe, Receipt, and
104
+ idempotency contract remain unchanged. See the public [MCP Action integration](https://witnora.com/docs/mcp-actions)
105
+ and [HTTP Actions](https://witnora.com/docs/http-actions) guides.
106
+
93
107
  Initialization writes a reusable `.witnora/gateway/client.mjs`. Import its
94
108
  `witnoraGateway.start`, `event`, and `complete` methods at one existing sandbox
95
109
  workflow boundary. The generated README contains the exact code and privacy
package/dist/cli.js CHANGED
@@ -158,9 +158,17 @@ else if (command === "onboard") {
158
158
  name: readFlag("--name"),
159
159
  repository: readFlag("--repo") ?? process.cwd(),
160
160
  template: readFlag("--template") ? parseAgentTemplate(readFlag("--template")) : undefined,
161
+ actionTransport: readActionTransport(readFlag("--action-transport")),
161
162
  openBrowser: readBoolFlag("--no-browser") ? async () => { } : undefined,
162
163
  });
163
164
  }
165
+ else if (command === "mcp") {
166
+ const { runWitnoraMcpStdioServer } = await import("./mcp.js");
167
+ await runWitnoraMcpStdioServer({
168
+ repository: readFlag("--repo") ?? process.cwd(),
169
+ dir: readFlag("--dir"),
170
+ });
171
+ }
164
172
  else if (command === "release") {
165
173
  const action = process.argv[3] ?? "help";
166
174
  if (action !== "evaluate")
@@ -862,6 +870,7 @@ else if (command === "conformance") {
862
870
  else {
863
871
  process.stdout.write(`Usage:
864
872
  witnora onboard --project <project-id>
873
+ witnora mcp
865
874
  witnora try --template workflow [--push]
866
875
  witnora init --subject my-browser-agent
867
876
  witnora connect --server https://witnora.com --project <project-id>
@@ -898,6 +907,13 @@ else {
898
907
  witnora schema validate --schema evidence-bundle --file .witnora/latest/agentcert-evidence.json
899
908
  `);
900
909
  }
910
+ function readActionTransport(value) {
911
+ if (value === undefined)
912
+ return undefined;
913
+ if (value === "http" || value === "mcp")
914
+ return value;
915
+ throw new Error("--action-transport must be http or mcp.");
916
+ }
901
917
  async function loadConfig(path) {
902
918
  if (!path) {
903
919
  return undefined;
@@ -41,11 +41,13 @@ Options:
41
41
  return `Usage:
42
42
  witnora onboard --project <project-id>
43
43
  witnora onboard --project <project-id> --template <browser|coding|mcp|workflow|data>
44
+ witnora onboard --project <project-id> --action-transport <http|mcp>
44
45
 
45
46
  Opens one browser authorization, saves a restricted project credential, detects the repository,
46
47
  writes missing starter files, starts a customer-owned Gateway in the background, and records an
47
48
  isolated synthetic self-test receipt. The self-test does not create assurance evidence or establish
48
- CURRENT status.
49
+ CURRENT status. When --action-transport is selected, Witnora validates that exact local transport
50
+ without proposing or executing an Action and reports the bounded result to Hosted.
49
51
 
50
52
  Options:
51
53
  --server <url> Hosted server (default: https://witnora.com)
@@ -53,7 +55,23 @@ Options:
53
55
  --name <name> Saved connection name (default: repository name)
54
56
  --repo <directory> Repository to configure (default: current directory)
55
57
  --template <type> Override automatic repository detection
58
+ --action-transport <t> Test HTTP or MCP against the exact generated sandbox Action path
56
59
  --no-browser Print the approval URL without opening it
60
+ `;
61
+ if (command === "mcp")
62
+ return `Usage:
63
+ witnora mcp [--repo <agent-repository>] [--dir <gateway-directory>]
64
+
65
+ Starts a local stdio MCP server for the one exact sandbox HTTP Action generated
66
+ by onboarding. The MCP process reads ignored Gateway credentials locally; they
67
+ are never exposed as tool inputs. The Agent can propose and inspect an Action,
68
+ but cannot approve it, execute outside the customer Gateway, or self-assert an
69
+ independently verified result.
70
+
71
+ Options:
72
+ --repo <directory> Onboarded Agent repository (default: current directory)
73
+ --dir <directory> Gateway directory relative to the repo (default: .witnora/gateway)
74
+ --help, -h Show this help without starting the MCP server
57
75
  `;
58
76
  if (command === "gateway")
59
77
  return `Usage:
package/dist/gateway.js CHANGED
@@ -1463,6 +1463,21 @@ async function loadConfiguredCustomerHttpAction(directory, gateway) {
1463
1463
  throw new Error("Customer HTTP Action token is missing or invalid.");
1464
1464
  return { action, token };
1465
1465
  }
1466
+ export async function loadCustomerMcpActionBinding(options = {}) {
1467
+ const directory = resolve(options.repository ?? process.cwd(), options.dir ?? ".witnora/gateway");
1468
+ const gateway = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
1469
+ const configured = await loadConfiguredCustomerHttpAction(directory, gateway);
1470
+ if (!configured) {
1471
+ throw new Error("Witnora MCP requires one configured sandbox HTTP Action. Run onboarding for one exact Task/action path first.");
1472
+ }
1473
+ const secrets = parseSecrets(await readFile(join(directory, "secrets.json"), "utf8"));
1474
+ return {
1475
+ baseUrl: `http://${gateway.host}:${gateway.port}`,
1476
+ action: configured.action,
1477
+ actionToken: configured.token,
1478
+ gatewayToken: secrets.gatewayToken,
1479
+ };
1480
+ }
1466
1481
  export function createCustomerHttpActionConfig(activations) {
1467
1482
  if (activations.length !== 1)
1468
1483
  return undefined;
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export * from "./failure-review.js";
9
9
  export * from "./evidence-signing.js";
10
10
  export * from "./local-server.js";
11
11
  export * from "./monitor.js";
12
+ export * from "./mcp.js";
12
13
  export * from "./onboard.js";
13
14
  export * from "./normalizers.js";
14
15
  export * from "./report.js";
package/dist/mcp.js ADDED
@@ -0,0 +1,116 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ import { loadCustomerMcpActionBinding, } from "./gateway.js";
8
+ const IDENTIFIER = /^[A-Za-z0-9._:-]{1,160}$/;
9
+ const STATUS = /^[A-Za-z0-9._:-]{1,100}$/;
10
+ export function createWitnoraMcpServer(options) {
11
+ if (options.action.environment !== "sandbox" || options.action.actionPathId !== options.action.id) {
12
+ throw new Error("Witnora MCP accepts one exact sandbox Task/action binding only.");
13
+ }
14
+ const actionTool = witnoraMcpActionToolName(options.action.id);
15
+ const server = new McpServer({ name: "witnora", version: "0.1.0" });
16
+ const issuedActionIds = new Set();
17
+ server.registerTool(actionTool, {
18
+ description: `Propose the exact ${options.action.actionPathId} sandbox action through the customer-owned Witnora Gateway. This tool cannot approve its own action. A result is proved only after the independent Probe and signed Receipt are present.`,
19
+ inputSchema: {
20
+ idempotencyKey: z.string().regex(IDENTIFIER).describe("Stable external request ID. Reuse it only for an exact retry."),
21
+ resourceId: z.string().regex(IDENTIFIER).describe("The exact sandbox resource covered by this action path."),
22
+ status: z.string().regex(STATUS).optional().describe(`Requested state. Defaults to ${options.action.defaultStatus}.`),
23
+ },
24
+ }, async (input) => {
25
+ const action = await options.transport.propose(input);
26
+ const actionId = typeof action.id === "string" && IDENTIFIER.test(action.id) ? action.id : undefined;
27
+ if (!actionId)
28
+ throw new Error("Witnora Gateway returned an Action without a valid Action ID.");
29
+ issuedActionIds.add(actionId);
30
+ return toolResult(action);
31
+ });
32
+ server.registerTool("witnora_get_action", {
33
+ description: "Read the current Action, approval, execution, Probe, and Receipt state. Reported success is not an independently verified outcome.",
34
+ inputSchema: {
35
+ actionId: z.string().regex(IDENTIFIER),
36
+ },
37
+ }, async ({ actionId }) => {
38
+ if (!issuedActionIds.has(actionId)) {
39
+ throw new Error("This MCP session can inspect only Actions returned by its exact sandbox Action tool. Retry the exact proposal first after a restart.");
40
+ }
41
+ return toolResult(await options.transport.getAction(actionId));
42
+ });
43
+ return server;
44
+ }
45
+ export async function createWitnoraMcpServerFromRepository(options = {}) {
46
+ const binding = await loadCustomerMcpActionBinding(options);
47
+ return {
48
+ server: createWitnoraMcpServer({ action: binding.action, transport: gatewayTransport(binding, options.fetch ?? fetch) }),
49
+ actionTool: witnoraMcpActionToolName(binding.action.id),
50
+ action: binding.action,
51
+ };
52
+ }
53
+ export async function runWitnoraMcpStdioServer(options = {}) {
54
+ const { server } = await createWitnoraMcpServerFromRepository(options);
55
+ await server.connect(new StdioServerTransport());
56
+ }
57
+ export async function testWitnoraMcpConnection(options = {}) {
58
+ const { server, actionTool, action } = await createWitnoraMcpServerFromRepository(options);
59
+ const client = new Client({ name: "witnora-onboarding-connection-test", version: "1.0.0" });
60
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
61
+ try {
62
+ await server.connect(serverTransport);
63
+ await client.connect(clientTransport);
64
+ const listed = await client.listTools();
65
+ const tools = listed.tools.map((tool) => tool.name).sort();
66
+ const expected = [actionTool, "witnora_get_action"].sort();
67
+ if (JSON.stringify(tools) !== JSON.stringify(expected)) {
68
+ throw new Error("Witnora MCP connection test found an unexpected tool surface.");
69
+ }
70
+ if (tools.some((tool) => /approve|execute|verify/i.test(tool))) {
71
+ throw new Error("Witnora MCP connection test found a forbidden authority-bearing tool.");
72
+ }
73
+ return { actionId: action.id, actionTool, tools };
74
+ }
75
+ finally {
76
+ await client.close().catch(() => undefined);
77
+ await server.close().catch(() => undefined);
78
+ }
79
+ }
80
+ export function witnoraMcpActionToolName(actionId) {
81
+ const normalized = actionId.toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "") || "action";
82
+ const candidate = `witnora_${normalized}`;
83
+ if (candidate.length <= 96)
84
+ return candidate;
85
+ const suffix = createHash("sha256").update(actionId).digest("hex").slice(0, 10);
86
+ return `${candidate.slice(0, 85)}_${suffix}`;
87
+ }
88
+ function gatewayTransport(binding, requestFetch) {
89
+ return {
90
+ propose: ({ idempotencyKey, resourceId, status }) => requestJson(requestFetch, `${binding.baseUrl}${binding.action.path}`, {
91
+ method: "POST",
92
+ headers: {
93
+ authorization: `Bearer ${binding.actionToken}`,
94
+ "content-type": "application/json",
95
+ "idempotency-key": idempotencyKey,
96
+ },
97
+ body: JSON.stringify({ resourceId, ...(status === undefined ? {} : { status }) }),
98
+ }),
99
+ getAction: (actionId) => requestJson(requestFetch, `${binding.baseUrl}/v1/actions/${encodeURIComponent(actionId)}`, { headers: { authorization: `Bearer ${binding.gatewayToken}` } }),
100
+ };
101
+ }
102
+ async function requestJson(requestFetch, url, init) {
103
+ const response = await requestFetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(10_000) });
104
+ const body = await response.json().catch(() => ({}));
105
+ if (!response.ok) {
106
+ const message = typeof body.error === "string" ? body.error : `Witnora Gateway returned HTTP ${response.status}.`;
107
+ throw new Error(message.slice(0, 500));
108
+ }
109
+ return body;
110
+ }
111
+ function toolResult(value) {
112
+ return {
113
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
114
+ structuredContent: value,
115
+ };
116
+ }
package/dist/onboard.js CHANGED
@@ -8,7 +8,7 @@ import { verifyControlPlaneConnection } from "./control-plane.js";
8
8
  import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
9
9
  import { parseAgentTemplate, starterAdapter, starterProfile, starterTripwireConfig } from "./onboarding-templates.js";
10
10
  import { writeTryEvidence } from "./try.js";
11
- import { doctorCustomerGateway, activateManagedWorkflowHarness, configureCustomerHttpAction, ensureCustomerGatewayPortAvailable, initializeCustomerGateway, inspectLocalRuntimeBinding, inspectCustomerGatewayFiles, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, upgradeCustomerGatewayRuntime, RuntimeSetupNotReadyError, } from "./gateway.js";
11
+ import { doctorCustomerGateway, activateManagedWorkflowHarness, configureCustomerHttpAction, ensureCustomerGatewayPortAvailable, initializeCustomerGateway, inspectLocalRuntimeBinding, inspectCustomerGatewayFiles, loadCustomerMcpActionBinding, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, upgradeCustomerGatewayRuntime, RuntimeSetupNotReadyError, } from "./gateway.js";
12
12
  import { discoverRuntimeReferences, planRuntimeSandboxModules } from "./runtime-sandbox-kit.js";
13
13
  import { automaticRuntimeReferencesUsable, bootstrapLocalRuntime, verifyAutomaticRuntimeProbe } from "./runtime-bootstrap.js";
14
14
  import { activateRealPathIntegrations } from "./real-path-activation.js";
@@ -294,6 +294,19 @@ export async function runOnboard(options) {
294
294
  const runtimeReadiness = doctor.overall === "READY_FOR_RUNTIME" && runtimeBinding
295
295
  ? { state: "LOCAL_SANDBOX_READY", checkedAt: new Date().toISOString(), limitations: [] }
296
296
  : { state: "RECORDED_ONLY", checkedAt: new Date().toISOString(), limitations: [runtimeLimitation ?? "The local sandbox Runtime worker has not established exact adapter, probe, source-key, identity, mandate, and readiness bindings."] };
297
+ const actionTransportTest = options.actionTransport
298
+ ? await runActionTransportTest({
299
+ transport: options.actionTransport,
300
+ repository: repositoryPath,
301
+ server,
302
+ projectId: token.projectId,
303
+ apiKey: token.apiKey,
304
+ repositoryIdentity: repository,
305
+ httpActionReady: httpActionSetup?.state === "READY" && Boolean(httpActionSetup.action),
306
+ fetch: requestFetch,
307
+ generatedFiles,
308
+ })
309
+ : undefined;
297
310
  await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, {
298
311
  status: "verified", attemptId, generatedFiles: relativeGeneratedFiles(repositoryPath, generatedFiles), runtimeReadiness,
299
312
  connectedAgent: agentIdentity,
@@ -316,11 +329,18 @@ export async function runOnboard(options) {
316
329
  if (httpActionSetup?.state === "READY" && httpActionSetup.action) {
317
330
  output(`HTTP Action: READY. POST ${managedGateway.baseUrl}${httpActionSetup.action.path}; copy the action-scoped token and request example from .witnora/gateway/HTTP_ACTION.md.\n`);
318
331
  }
332
+ if (actionTransportTest?.state === "PASSED") {
333
+ output(`Action transport: ${actionTransportTest.transport.toUpperCase()} connection test PASSED at the customer-owned Gateway. No Action was proposed or executed.\n`);
334
+ }
335
+ else if (actionTransportTest) {
336
+ output(`Action transport: ${actionTransportTest.transport.toUpperCase()} is waiting for one exact sandbox Task/action path. ${actionTransportTest.limitation}\n`);
337
+ }
319
338
  output("Connected. Nora is monitoring this Agent. Run it normally whenever it is ready; the first source-signed activity will appear automatically without blocking setup.\n");
320
339
  return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
321
340
  repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
322
341
  gatewayDirectory: join(repositoryPath, ".witnora", "gateway"), gatewayArchiveDirectory: gatewayMigration?.archiveDirectory,
323
342
  gateway: managedGateway, runtimeReadiness, assuranceHarnessReadiness, continuousService,
343
+ actionTransportTest,
324
344
  ...(httpActionSetup?.state === "READY" && httpActionSetup.action ? { httpAction: {
325
345
  state: "READY",
326
346
  method: "POST",
@@ -346,6 +366,58 @@ export async function runOnboard(options) {
346
366
  throw new Error(`Witnora Setup Autopilot rolled back this install attempt: ${diagnosis}`);
347
367
  }
348
368
  }
369
+ async function runActionTransportTest(options) {
370
+ if (!options.httpActionReady)
371
+ return {
372
+ state: "WAITING_FOR_ACTION_PATH",
373
+ transport: options.transport,
374
+ limitation: "Define and approve one exact sandbox Task/action Harness, then rerun this same onboarding command.",
375
+ };
376
+ const binding = await loadCustomerMcpActionBinding({ repository: options.repository });
377
+ const checkedAt = new Date().toISOString();
378
+ const passed = options.transport === "http"
379
+ ? {
380
+ state: "PASSED",
381
+ transport: "http",
382
+ checkedAt,
383
+ actionId: binding.action.id,
384
+ endpoint: `${binding.baseUrl}${binding.action.path}`,
385
+ }
386
+ : await import("./mcp.js").then(async ({ testWitnoraMcpConnection }) => {
387
+ const result = await testWitnoraMcpConnection({ repository: options.repository, fetch: options.fetch });
388
+ return {
389
+ state: "PASSED",
390
+ transport: "mcp",
391
+ checkedAt,
392
+ actionId: result.actionId,
393
+ actionTool: result.actionTool,
394
+ };
395
+ });
396
+ const report = {
397
+ transport: passed.transport,
398
+ actionId: passed.actionId,
399
+ ...(passed.transport === "http" ? { endpoint: passed.endpoint } : { actionTool: passed.actionTool }),
400
+ };
401
+ const receipt = await jsonRequest(options.fetch, `${options.server}/v1/projects/${encodeURIComponent(options.projectId)}/onboarding/self-test`, {
402
+ method: "POST",
403
+ headers: { authorization: `Bearer ${options.apiKey}`, "content-type": "application/json" },
404
+ body: JSON.stringify({
405
+ template: options.repositoryIdentity.template,
406
+ repository: {
407
+ kind: options.repositoryIdentity.kind,
408
+ name: options.repositoryIdentity.name,
409
+ fingerprintSha256: options.repositoryIdentity.fingerprintSha256,
410
+ actionTransportTest: report,
411
+ },
412
+ bundleSha256: createHash("sha256").update(JSON.stringify(report)).digest("hex"),
413
+ }),
414
+ });
415
+ const receiptPath = join(options.repository, ".witnora", "onboarding", "receipts", `action-transport-${passed.transport}-${Date.now()}.json`);
416
+ await mkdir(dirname(receiptPath), { recursive: true });
417
+ await writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
418
+ options.generatedFiles.push(receiptPath);
419
+ return { ...passed, receiptPath };
420
+ }
349
421
  async function waitForInstalledGatewayService(options) {
350
422
  const pause = options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
351
423
  const deadline = Date.now() + (options.timeoutMs ?? 12_000);
package/mcp.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+
3
+ export interface WitnoraMcpActionConfig {
4
+ id: string;
5
+ actionPathId: string;
6
+ defaultStatus: string;
7
+ environment: "sandbox";
8
+ }
9
+
10
+ export interface WitnoraMcpActionTransport {
11
+ propose(input: {
12
+ idempotencyKey: string;
13
+ resourceId: string;
14
+ status?: string;
15
+ }): Promise<Record<string, unknown>>;
16
+ getAction(actionId: string): Promise<Record<string, unknown>>;
17
+ }
18
+
19
+ export interface WitnoraMcpServerOptions {
20
+ action: WitnoraMcpActionConfig;
21
+ transport: WitnoraMcpActionTransport;
22
+ }
23
+
24
+ export function createWitnoraMcpServer(options: WitnoraMcpServerOptions): McpServer;
25
+
26
+ export function createWitnoraMcpServerFromRepository(options?: {
27
+ repository?: string;
28
+ dir?: string;
29
+ fetch?: typeof fetch;
30
+ }): Promise<{
31
+ server: McpServer;
32
+ actionTool: string;
33
+ action: WitnoraMcpActionConfig;
34
+ }>;
35
+
36
+ export function runWitnoraMcpStdioServer(options?: {
37
+ repository?: string;
38
+ dir?: string;
39
+ fetch?: typeof fetch;
40
+ }): Promise<void>;
41
+
42
+ export function testWitnoraMcpConnection(options?: {
43
+ repository?: string;
44
+ dir?: string;
45
+ fetch?: typeof fetch;
46
+ }): Promise<{
47
+ actionTool: string;
48
+ tools: string[];
49
+ }>;
50
+
51
+ export function witnoraMcpActionToolName(actionId: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.19.1",
3
+ "version": "0.20.1",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -36,6 +36,10 @@
36
36
  },
37
37
  "exports": {
38
38
  "./cli": "./dist/cli.js",
39
+ "./mcp": {
40
+ "types": "./mcp.d.ts",
41
+ "import": "./dist/mcp.js"
42
+ },
39
43
  "./browser-adapter-kit": {
40
44
  "types": "./dist/vendor/onegent-runtime/browser-adapter-kit.d.ts",
41
45
  "import": "./dist/vendor/onegent-runtime/browser-adapter-kit.js"
@@ -63,6 +67,7 @@
63
67
  },
64
68
  "files": [
65
69
  "dist",
70
+ "mcp.d.ts",
66
71
  "README.md",
67
72
  "LICENSE",
68
73
  "package.json"
@@ -82,6 +87,8 @@
82
87
  "vitest": "^4.0.13"
83
88
  },
84
89
  "optionalDependencies": {
90
+ "@modelcontextprotocol/sdk": "1.29.0",
91
+ "zod": "^4.3.6",
85
92
  "pg": "^8.16.3"
86
93
  }
87
94
  }