@nylorun/runtime 0.7.0-beta → 0.8.0-beta

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +7 -5
  3. package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
  4. package/dist/adapters/sandbox/microsandbox.js +238 -0
  5. package/dist/adapters/sandbox/virtual.d.ts +4 -0
  6. package/dist/adapters/sandbox/virtual.js +125 -0
  7. package/dist/configuration.d.ts +1 -1
  8. package/dist/configuration.js +1 -1
  9. package/dist/core/executors.d.ts +32 -0
  10. package/dist/core/executors.js +60 -0
  11. package/dist/core/main.js +7 -12
  12. package/dist/core/runtime.d.ts +24 -1
  13. package/dist/core/runtime.js +322 -30
  14. package/dist/core/store.d.ts +12 -0
  15. package/dist/core/store.js +50 -1
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +0 -1
  18. package/dist/model/catalog.d.ts +12 -0
  19. package/dist/model/catalog.js +29 -0
  20. package/dist/model/models.d.ts +3 -1
  21. package/dist/model/models.js +4 -2
  22. package/dist/model/pi-model.d.ts +9 -3
  23. package/dist/model/pi-model.js +47 -11
  24. package/dist/model/settings.d.ts +0 -2
  25. package/dist/model/settings.js +0 -47
  26. package/dist/node/index.d.ts +1 -1
  27. package/dist/node/index.js +1 -1
  28. package/dist/sandbox/manager.d.ts +61 -0
  29. package/dist/sandbox/manager.js +239 -0
  30. package/dist/sandbox/policy.d.ts +13 -0
  31. package/dist/sandbox/policy.js +67 -0
  32. package/dist/sandbox/select.d.ts +35 -0
  33. package/dist/sandbox/select.js +76 -0
  34. package/dist/sandbox/tools.d.ts +25 -0
  35. package/dist/sandbox/tools.js +165 -0
  36. package/dist/sandbox/types.d.ts +66 -0
  37. package/dist/vault/service.d.ts +41 -1
  38. package/dist/vault/service.js +291 -4
  39. package/dist/version.d.ts +1 -0
  40. package/dist/version.js +3 -0
  41. package/package.json +9 -4
  42. package/dist/config.d.ts +0 -20
  43. package/dist/model/defaults.d.ts +0 -17
  44. package/dist/model/defaults.js +0 -21
  45. package/dist/model/http-model.d.ts +0 -12
  46. package/dist/model/http-model.js +0 -299
  47. package/dist/node/local-sessions.d.ts +0 -5
  48. package/dist/node/local-sessions.js +0 -174
  49. package/dist/server/ag-ui.d.ts +0 -8
  50. package/dist/server/ag-ui.js +0 -76
  51. package/dist/server/delivery.d.ts +0 -24
  52. package/dist/server/delivery.js +0 -107
  53. package/dist/server/digests.d.ts +0 -13
  54. package/dist/server/digests.js +0 -39
  55. package/dist/server/host.d.ts +0 -70
  56. package/dist/server/host.js +0 -647
  57. package/dist/session/api.d.ts +0 -10
  58. package/dist/session/api.js +0 -15
  59. package/dist/session/default.d.ts +0 -5
  60. package/dist/session/default.js +0 -30
  61. package/dist/session/handle.d.ts +0 -27
  62. package/dist/session/handle.js +0 -199
  63. package/dist/session/index.d.ts +0 -2
  64. package/dist/session/index.js +0 -2
  65. package/dist/sessions/host.d.ts +0 -39
  66. package/dist/sessions/host.js +0 -359
  67. package/dist/sessions/store.d.ts +0 -41
  68. package/dist/sessions/store.js +0 -33
  69. /package/dist/{config.js → sandbox/types.js} +0 -0
@@ -2,8 +2,11 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { canonical } from "../core/store.js";
3
3
  import { decryptSecret, encryptSecret, VaultCryptoError, } from "./crypto.js";
4
4
  import { VaultError } from "./error.js";
5
+ import { hostModelCatalog } from "../model/catalog.js";
5
6
  import { normalizeVaultUrl } from "./url.js";
6
7
  const REFRESH_SKEW_MS = 60_000;
8
+ const HOST_VAULT_ID = "host";
9
+ const HOST_MODEL_ID = "host-model";
7
10
  export class VaultService {
8
11
  db;
9
12
  tx;
@@ -34,7 +37,7 @@ export class VaultService {
34
37
  }
35
38
  listVaults(ownerUserId) {
36
39
  const rows = this.db
37
- .prepare(`SELECT id FROM vaults WHERE owner_user_id=? ORDER BY created_at, id`)
40
+ .prepare(`SELECT id FROM vaults WHERE owner_user_id=? AND scope='user' ORDER BY created_at, id`)
38
41
  .all(ownerUserId);
39
42
  return rows.map((row) => this.vaultInfo(row.id));
40
43
  }
@@ -156,6 +159,8 @@ export class VaultService {
156
159
  const vault = this.vaultRow(id);
157
160
  if (!vault)
158
161
  throw new VaultError(404, "Vault not found");
162
+ if (vault.scope === "host")
163
+ throw new VaultError(400, "Host vault cannot be attached to a session");
159
164
  if (vault.owner_user_id !== ownerUserId)
160
165
  throw new VaultError(403, "Vault belongs to another user");
161
166
  }
@@ -274,6 +279,142 @@ export class VaultService {
274
279
  headers: { authorization: `Bearer ${token}` },
275
280
  };
276
281
  }
282
+ getHostModel() {
283
+ const row = this.activeHostCredentialRow();
284
+ if (!row)
285
+ return { configured: false };
286
+ const binding = modelBinding(row);
287
+ return {
288
+ configured: true,
289
+ provider: binding.provider,
290
+ model: binding.model,
291
+ authType: binding.authType,
292
+ ...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
293
+ };
294
+ }
295
+ listHostProviders() {
296
+ const active = this.activeProviderId();
297
+ const catalog = new Map(hostModelCatalog().providers.map((provider) => [provider.id, provider.name]));
298
+ const providers = this.hostModelRows().map((row) => {
299
+ const binding = modelBinding(row);
300
+ return {
301
+ id: binding.provider,
302
+ name: catalog.get(binding.provider) ??
303
+ (binding.provider === "custom"
304
+ ? "Custom OpenAI-compatible"
305
+ : binding.provider),
306
+ model: binding.model,
307
+ authType: binding.authType,
308
+ ...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
309
+ lastUpdated: row.rotated_at ?? row.created_at,
310
+ active: binding.provider === active,
311
+ };
312
+ });
313
+ providers.sort((left, right) => left.name.localeCompare(right.name));
314
+ return { providers };
315
+ }
316
+ putHostModel(body) {
317
+ return this.tx(() => this.replay(`host-model:${body.idempotencyKey}`, body, () => {
318
+ this.validateHostModel(body);
319
+ this.ensureHostVault();
320
+ const credentialId = hostModelCredentialId(body.provider);
321
+ this.deleteHostProviderRows(body.provider);
322
+ const payload = body.auth.type === "api_key"
323
+ ? {
324
+ apiKey: body.auth.key,
325
+ ...(body.auth.env ? { env: body.auth.env } : {}),
326
+ }
327
+ : { oauth: body.auth };
328
+ this.insertHostCredential({
329
+ id: credentialId,
330
+ provider: body.provider,
331
+ model: body.model,
332
+ baseUrl: body.baseUrl,
333
+ authType: body.auth.type,
334
+ payload,
335
+ });
336
+ this.setActiveProvider(body.provider);
337
+ this.audit({
338
+ actor: "application",
339
+ action: "rotate",
340
+ vaultId: HOST_VAULT_ID,
341
+ credentialId,
342
+ outcome: "rotated",
343
+ });
344
+ return this.getHostModel();
345
+ }));
346
+ }
347
+ selectHostModel(body) {
348
+ return this.tx(() => this.replay(`host-model-select:${body.idempotencyKey}`, body, () => {
349
+ this.validateHostModel(body);
350
+ const row = this.hostCredentialRowFor(body.provider);
351
+ if (!row)
352
+ throw new VaultError(404, "Model provider is not configured");
353
+ const binding = modelBinding(row);
354
+ const payload = this.readHostPayload(row, binding);
355
+ const credentialId = hostModelCredentialId(body.provider);
356
+ this.deleteHostProviderRows(body.provider);
357
+ this.insertHostCredential({
358
+ id: credentialId,
359
+ provider: body.provider,
360
+ model: body.model,
361
+ baseUrl: body.baseUrl,
362
+ authType: binding.authType,
363
+ payload,
364
+ });
365
+ this.setActiveProvider(body.provider);
366
+ this.audit({
367
+ actor: "application",
368
+ action: "rotate",
369
+ vaultId: HOST_VAULT_ID,
370
+ credentialId,
371
+ outcome: "rotated",
372
+ });
373
+ return this.getHostModel();
374
+ }));
375
+ }
376
+ readHostModel() {
377
+ const row = this.activeHostCredentialRow();
378
+ if (!row)
379
+ return undefined;
380
+ const binding = modelBinding(row);
381
+ const payload = this.readHostPayload(row, binding);
382
+ const credential = binding.authType === "oauth"
383
+ ? { type: "oauth", ...payload.oauth }
384
+ : {
385
+ type: "api_key",
386
+ key: payload.apiKey,
387
+ ...(payload.env ? { env: payload.env } : {}),
388
+ };
389
+ return {
390
+ provider: binding.provider,
391
+ model: binding.model,
392
+ ...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
393
+ authType: binding.authType,
394
+ credential,
395
+ };
396
+ }
397
+ updateHostCredential(credential) {
398
+ this.tx(() => {
399
+ const row = this.activeHostCredentialRow();
400
+ if (!row)
401
+ throw new VaultError(404, "Model provider is not configured");
402
+ const binding = modelBinding(row);
403
+ const payload = credential.type === "oauth"
404
+ ? { oauth: credential }
405
+ : {
406
+ apiKey: credential.key,
407
+ ...(credential.env ? { env: credential.env } : {}),
408
+ };
409
+ const aad = hostModelAad(binding.provider, binding.model);
410
+ const sealed = encryptSecret(this.kek(), aad, Buffer.from(JSON.stringify(payload), "utf8"));
411
+ this.db
412
+ .prepare(`UPDATE vault_credentials
413
+ SET type='model', binding_json=?, kek_id=?, nonce=?, ciphertext=?, wrapped_dek=?, rotated_at=?
414
+ WHERE id=?`)
415
+ .run(JSON.stringify({ ...binding, authType: credential.type }), sealed.kekId, sealed.nonce, sealed.ciphertext, sealed.wrappedDek, new Date().toISOString(), row.id);
416
+ });
417
+ }
277
418
  reject(route) {
278
419
  this.audit({
279
420
  actor: "executor",
@@ -413,7 +554,7 @@ export class VaultService {
413
554
  }
414
555
  vaultInfo(id) {
415
556
  const row = this.vaultRow(id);
416
- if (!row)
557
+ if (!row || row.scope === "host")
417
558
  throw new VaultError(404, "Vault not found");
418
559
  return {
419
560
  id: row.id,
@@ -427,9 +568,134 @@ export class VaultService {
427
568
  }
428
569
  vaultRow(id) {
429
570
  return this.db
430
- .prepare(`SELECT id, name, owner_user_id, metadata_json, created_at FROM vaults WHERE id=?`)
571
+ .prepare(`SELECT id, name, owner_user_id, metadata_json, created_at, scope FROM vaults WHERE id=?`)
431
572
  .get(id);
432
573
  }
574
+ ensureHostVault() {
575
+ const existing = this.vaultRow(HOST_VAULT_ID);
576
+ if (existing?.scope === "host")
577
+ return;
578
+ if (existing)
579
+ throw new VaultError(409, "Host vault id is already used");
580
+ this.db
581
+ .prepare(`INSERT INTO vaults(id,name,owner_user_id,metadata_json,created_at,scope) VALUES(?,?,?,?,?,?)`)
582
+ .run(HOST_VAULT_ID, "Host", "host", null, new Date().toISOString(), "host");
583
+ }
584
+ validateHostModel(body) {
585
+ if (body.provider === "custom") {
586
+ if (!body.baseUrl)
587
+ throw new VaultError(400, "A base URL is required for a custom provider.");
588
+ let url;
589
+ try {
590
+ url = new URL(body.baseUrl);
591
+ }
592
+ catch {
593
+ throw new VaultError(400, "Base URL must be an HTTP(S) URL.");
594
+ }
595
+ if (!["http:", "https:"].includes(url.protocol))
596
+ throw new VaultError(400, "Base URL must be an HTTP(S) URL.");
597
+ return;
598
+ }
599
+ if (body.baseUrl)
600
+ throw new VaultError(400, "A base URL is only valid for a custom provider.");
601
+ const provider = hostModelCatalog().providers.find((item) => item.id === body.provider);
602
+ if (!provider)
603
+ throw new VaultError(400, "Unknown model provider.");
604
+ if (!provider.models.some((item) => item.id === body.model))
605
+ throw new VaultError(400, "Unknown model.");
606
+ }
607
+ insertHostCredential(input) {
608
+ const aad = hostModelAad(input.provider, input.model);
609
+ const sealed = encryptSecret(this.kek(), aad, Buffer.from(JSON.stringify(input.payload), "utf8"));
610
+ this.db
611
+ .prepare(`INSERT INTO vault_credentials(
612
+ id, vault_id, name, type, binding_json, expires_at, created_at, rotated_at,
613
+ kek_id, nonce, ciphertext, wrapped_dek
614
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`)
615
+ .run(input.id, HOST_VAULT_ID, input.provider, "model", JSON.stringify({
616
+ provider: input.provider,
617
+ model: input.model,
618
+ ...(input.baseUrl ? { baseUrl: input.baseUrl } : {}),
619
+ authType: input.authType,
620
+ }), null, new Date().toISOString(), null, sealed.kekId, sealed.nonce, sealed.ciphertext, sealed.wrappedDek);
621
+ }
622
+ readHostPayload(row, binding) {
623
+ const plaintext = decryptSecret(this.kek(), hostModelAad(binding.provider, binding.model), Buffer.from(row.nonce), Buffer.from(row.ciphertext), Buffer.from(row.wrapped_dek), row.kek_id);
624
+ try {
625
+ return JSON.parse(plaintext.toString("utf8"));
626
+ }
627
+ finally {
628
+ plaintext.fill(0);
629
+ }
630
+ }
631
+ hostModelRows() {
632
+ return this.db
633
+ .prepare(`SELECT id, vault_id, name, type, binding_json, expires_at, created_at, rotated_at,
634
+ kek_id, nonce, ciphertext, wrapped_dek
635
+ FROM vault_credentials WHERE vault_id=? AND type='model'
636
+ ORDER BY created_at, id`)
637
+ .all(HOST_VAULT_ID).filter((row) => {
638
+ try {
639
+ modelBinding(row);
640
+ return true;
641
+ }
642
+ catch {
643
+ return false;
644
+ }
645
+ });
646
+ }
647
+ hostCredentialRowFor(provider) {
648
+ const preferred = hostModelCredentialId(provider);
649
+ const rows = this.hostModelRows();
650
+ return (rows.find((row) => row.id === preferred) ??
651
+ rows.find((row) => modelBinding(row).provider === provider));
652
+ }
653
+ activeHostCredentialRow() {
654
+ const active = this.activeProviderId();
655
+ if (!active)
656
+ return undefined;
657
+ return this.hostCredentialRowFor(active);
658
+ }
659
+ activeProviderId() {
660
+ const vault = this.vaultRow(HOST_VAULT_ID);
661
+ if (vault?.metadata_json) {
662
+ try {
663
+ const metadata = JSON.parse(vault.metadata_json);
664
+ if (typeof metadata.activeProvider === "string" &&
665
+ metadata.activeProvider &&
666
+ this.hostCredentialRowFor(metadata.activeProvider))
667
+ return metadata.activeProvider;
668
+ }
669
+ catch {
670
+ /* Fall through to the only configured provider. */
671
+ }
672
+ }
673
+ const rows = this.hostModelRows();
674
+ if (rows.length === 0)
675
+ return undefined;
676
+ if (rows.length === 1)
677
+ return modelBinding(rows[0]).provider;
678
+ const legacy = rows.find((row) => row.id === HOST_MODEL_ID);
679
+ if (legacy)
680
+ return modelBinding(legacy).provider;
681
+ return modelBinding(rows[0]).provider;
682
+ }
683
+ setActiveProvider(provider) {
684
+ this.ensureHostVault();
685
+ this.db
686
+ .prepare(`UPDATE vaults SET metadata_json=? WHERE id=?`)
687
+ .run(JSON.stringify({ activeProvider: provider }), HOST_VAULT_ID);
688
+ }
689
+ deleteHostProviderRows(provider) {
690
+ const ids = this.hostModelRows()
691
+ .filter((row) => modelBinding(row).provider === provider)
692
+ .map((row) => row.id);
693
+ ids.push(hostModelCredentialId(provider));
694
+ for (const id of new Set(ids))
695
+ this.db
696
+ .prepare(`DELETE FROM vault_credentials WHERE id=? AND vault_id=?`)
697
+ .run(id, HOST_VAULT_ID);
698
+ }
433
699
  credentialInfo(vaultId, id) {
434
700
  const row = this.credentialRow(vaultId, id);
435
701
  return {
@@ -449,7 +715,7 @@ export class VaultService {
449
715
  kek_id, nonce, ciphertext, wrapped_dek
450
716
  FROM vault_credentials WHERE id=? AND vault_id=?`)
451
717
  .get(id, vaultId);
452
- if (!row || (row.type !== "bearer" && row.type !== "oauth"))
718
+ if (!row || row.type === "model")
453
719
  throw new VaultError(404, "Credential not found");
454
720
  return row;
455
721
  }
@@ -529,6 +795,27 @@ function requireTimestamp(value) {
529
795
  throw new VaultError(400, "Credential expiry is invalid");
530
796
  return new Date(Date.parse(value)).toISOString();
531
797
  }
798
+ function modelBinding(row) {
799
+ const binding = JSON.parse(row.binding_json);
800
+ if (!binding.provider || !binding.model || !binding.authType)
801
+ throw new VaultError(500, "Host model credential is unreadable");
802
+ return binding;
803
+ }
804
+ function hostModelCredentialId(provider) {
805
+ if (!/^[a-zA-Z0-9._-]+$/.test(provider))
806
+ throw new VaultError(400, "Invalid provider id");
807
+ return `${HOST_MODEL_ID}:${provider}`;
808
+ }
809
+ function hostModelAad(provider, model) {
810
+ return Buffer.from(canonical({
811
+ scope: "host",
812
+ vaultId: HOST_VAULT_ID,
813
+ credentialId: HOST_MODEL_ID,
814
+ type: "model",
815
+ provider,
816
+ model,
817
+ }), "utf8");
818
+ }
532
819
  function credentialAad(vaultId, credentialId, type, url) {
533
820
  return Buffer.from(canonical({ vaultId, credentialId, type, url }), "utf8");
534
821
  }
@@ -0,0 +1 @@
1
+ export declare const RUNTIME_VERSION = "0.8.0-beta";
@@ -0,0 +1,3 @@
1
+ // Kept in step with package.json by runtime/scripts/check-package.mjs, which runs in npm run check.
2
+ // release:prepare also rewrites this when it bumps @nylorun/runtime.
3
+ export const RUNTIME_VERSION = "0.8.0-beta";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/runtime",
3
- "version": "0.7.0-beta",
3
+ "version": "0.8.0-beta",
4
4
  "description": "SQLite agent execution host and model providers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -29,7 +29,8 @@
29
29
  "./configuration": {
30
30
  "types": "./dist/configuration.d.ts",
31
31
  "import": "./dist/configuration.js"
32
- }
32
+ },
33
+ "./package.json": "./package.json"
33
34
  },
34
35
  "files": [
35
36
  "dist",
@@ -54,8 +55,12 @@
54
55
  "@modelcontextprotocol/sdk": "^1.30.0",
55
56
  "@earendil-works/pi-ai": "0.85.1",
56
57
  "@hono/node-server": "^2.1.1",
57
- "@nylorun/harness": "0.16.0-beta",
58
- "@nylorun/core": "0.2.0-beta"
58
+ "@nylorun/harness": "0.17.0-beta",
59
+ "@nylorun/core": "0.3.0-beta",
60
+ "just-bash": "3.4.2"
61
+ },
62
+ "optionalDependencies": {
63
+ "microsandbox": "0.7.2"
59
64
  },
60
65
  "devDependencies": {
61
66
  "@types/node": "^26.5.0",
package/dist/config.d.ts DELETED
@@ -1,20 +0,0 @@
1
- import type { ModelAdapter } from "@nylorun/core/define";
2
- import type { SessionStore } from "./sessions/store.js";
3
- import type { RuntimeMedia } from "./media.js";
4
- import type { ModelEnvironment } from "./model/http-model.js";
5
- export interface RuntimeConfig {
6
- readonly onModelCall?: ModelAdapter;
7
- readonly createModel?: (options: import("./model/defaults.js").ModelFactoryOptions) => ModelAdapter;
8
- readonly observer?: (event: {
9
- readonly type: string;
10
- }) => void | Promise<void>;
11
- readonly sessions?: SessionStore;
12
- readonly media?: RuntimeMedia;
13
- readonly environment?: ModelEnvironment;
14
- readonly tokens?: boolean;
15
- readonly delivery?: {
16
- readonly previewBytes?: number;
17
- readonly eventBytes?: number;
18
- readonly eventCount?: number;
19
- };
20
- }
@@ -1,17 +0,0 @@
1
- import type { ModelAdapter } from "@nylorun/core/define";
2
- import { type ModelEnvironment } from "./http-model.js";
3
- export interface ModelPreview {
4
- readonly invocationId: string;
5
- readonly text: string;
6
- }
7
- export interface ModelFactoryOptions {
8
- readonly environment?: ModelEnvironment;
9
- readonly media?: import("../media.js").RuntimeMedia;
10
- readonly onPreview?: (preview: ModelPreview) => void;
11
- }
12
- /** Installed only by the Node launcher before application imports. */
13
- export declare function installNodeModelFactory(factory: (options: ModelFactoryOptions) => ModelAdapter): void;
14
- export declare function defaultModel(options: ModelFactoryOptions): ModelAdapter;
15
- export declare function processEnvironment(): ModelEnvironment;
16
- export declare function installRuntimeLifecycle(register: (close: () => Promise<void>) => void): void;
17
- export declare function registerRuntimeLifecycle(close: () => Promise<void>): void;
@@ -1,21 +0,0 @@
1
- import { httpModel } from "./http-model.js";
2
- let nodeFactory;
3
- /** Installed only by the Node launcher before application imports. */
4
- export function installNodeModelFactory(factory) {
5
- nodeFactory = factory;
6
- }
7
- export function defaultModel(options) {
8
- return options.environment === undefined && nodeFactory
9
- ? nodeFactory(options)
10
- : httpModel(options);
11
- }
12
- export function processEnvironment() {
13
- return typeof process === "undefined" ? {} : process.env;
14
- }
15
- let runtimeLifecycle;
16
- export function installRuntimeLifecycle(register) {
17
- runtimeLifecycle = register;
18
- }
19
- export function registerRuntimeLifecycle(close) {
20
- runtimeLifecycle?.(close);
21
- }
@@ -1,12 +0,0 @@
1
- import type { ModelAdapter } from "@nylorun/core/define";
2
- import type { ModelFactoryOptions } from "./defaults.js";
3
- export type ModelEnvironment = Readonly<Record<string, string | undefined>>;
4
- export interface HttpModelOptions extends ModelFactoryOptions {
5
- readonly provider?: "openai" | "custom" | "anthropic";
6
- readonly model?: string;
7
- readonly apiKey?: string;
8
- readonly baseUrl?: string;
9
- readonly fetch?: typeof fetch;
10
- }
11
- /** Fetch-only adapter for OpenAI-compatible and Anthropic HTTP endpoints. */
12
- export declare function httpModel(options?: HttpModelOptions): ModelAdapter;