@suveren/gateway 0.10.0 → 0.11.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
@@ -185,6 +185,42 @@ See `suveren-gateway config help` for the full command reference.
185
185
 
186
186
  ---
187
187
 
188
+ ## Simulation mode — block every real system
189
+
190
+ A mandate is bound to a *profile* (e.g. "sales"), not to a specific connector. If a real
191
+ connector (e.g. a live ERP account) and a simulated one (the built-in ERP/CRM/email
192
+ simulators) share a profile on one gateway, a mandate meant only for testing also
193
+ authorizes the real connector — nothing about the mandate says which one it's for.
194
+
195
+ Simulation mode closes that gap gateway-wide: turn it on and **every connector without a
196
+ manifest `simulation` marker is refused to even start** (the built-in ERP, CRM, and email
197
+ simulators all declare one; a connected Gmail or live ERP account does not), and every
198
+ connector that does declare one has its mode forced to `simulation`, overriding whatever
199
+ credential value is on file. Mandates and profiles are completely unaffected, and the
200
+ Authority Server never learns this is on — it's a purely local, gateway-side switch.
201
+
202
+ ```bash
203
+ # Start fresh with real systems blocked:
204
+ suveren-gateway start --simulation
205
+
206
+ # Or flip it on an existing install (takes effect on the next start/restart):
207
+ suveren-gateway simulation on
208
+ suveren-gateway restart
209
+
210
+ # Check the mode (the SAVED setting and, if the gateway is running, the LIVE one):
211
+ suveren-gateway simulation status
212
+
213
+ # Turning it off makes real systems reachable again, so it requires typed
214
+ # confirmation (or --confirm live for scripts) — turning it ON does not:
215
+ suveren-gateway simulation off
216
+ suveren-gateway restart
217
+ ```
218
+
219
+ `suveren-gateway status` also shows the current mode. See `suveren-gateway simulation help`
220
+ for the full command reference.
221
+
222
+ ---
223
+
188
224
  ## Technical Documentation
189
225
 
190
226
  | Document | Contents |
@@ -17,7 +17,8 @@ import { homedir, platform, userInfo } from 'node:os';
17
17
  import { dirname, join, resolve } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { buildLaunchAgentPlist, buildMacLauncher, buildSystemdUnit, buildWindowsTaskXml } from '../lib/autostart-templates.mjs';
20
- import { DEFAULT_AS_URL, readConfig, writeConfig, validateAsUrl, validateCaFile, validatePinTls, resolveAsUrl, resolvePinTls, resolvePinTlsExpectedFingerprint } from '../lib/config.mjs';
20
+ import { DEFAULT_AS_URL, readConfig, writeConfig, validateAsUrl, validateCaFile, validatePinTls, resolveAsUrl, resolvePinTls, resolvePinTlsExpectedFingerprint, resolveSimulation } from '../lib/config.mjs';
21
+ import { createInterface } from 'node:readline/promises';
21
22
  import { readPairing as readAsPairing, recordTlsPin, formatFingerprint, normalizeFingerprint } from '../lib/as-pairing.mjs';
22
23
 
23
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -210,6 +211,13 @@ async function start(args) {
210
211
  pinTlsToSave = true;
211
212
  }
212
213
 
214
+ // --simulation: the safe direction (turning OFF real systems), so — unlike
215
+ // `simulation off` — this needs no confirmation. Only ever turns it ON;
216
+ // there is no `start --no-simulation` (use `simulation off` for that, which
217
+ // DOES require confirmation, since that direction makes real systems
218
+ // reachable again).
219
+ const simulationFlag = args.includes('--simulation');
220
+
213
221
  if (await isAlreadyRunning()) {
214
222
  console.error(`suveren-gateway is already running (pid ${readPid()}). Use \`suveren-gateway stop\` first or \`suveren-gateway restart\`.`);
215
223
  process.exit(1);
@@ -269,6 +277,10 @@ async function start(args) {
269
277
  : ` Staged — the first sign-in's challenge must match it (refused otherwise); it then becomes the permanent pin.`,
270
278
  );
271
279
  }
280
+ if (simulationFlag) {
281
+ writeConfig(DATA_DIR, { simulation: true });
282
+ console.log(`Simulation mode: ON (saved) — every connector without a manifest "simulation" marker will be blocked.`);
283
+ }
272
284
 
273
285
  if (detach) {
274
286
  const out = openSync(LOG_FILE, 'a');
@@ -414,6 +426,7 @@ async function status() {
414
426
  console.log('suveren-gateway: running (managed by the login service)');
415
427
  console.log(` UI: http://localhost:${SUVEREN_PORT}`);
416
428
  console.log(` Vault: ${body.vaultUnlocked ? 'unlocked' : 'locked'}`);
429
+ console.log(` Simulation: ${body.simulation ? 'on — real systems are blocked' : 'off'}`);
417
430
  console.log(` Version: ${body.version ?? 'unknown'} (running)`);
418
431
  console.log(` Service: suveren-gateway service status`);
419
432
  return;
@@ -438,6 +451,7 @@ async function status() {
438
451
  console.log(`suveren-gateway: running (pid ${pid})`);
439
452
  console.log(` UI: http://localhost:${SUVEREN_PORT}`);
440
453
  console.log(` Vault: ${body.vaultUnlocked ? 'unlocked' : 'locked'}`);
454
+ console.log(` Simulation: ${body.simulation ? 'on — real systems are blocked' : 'off'}`);
441
455
  console.log(` Version: ${body.version ?? 'unknown'} (running)`);
442
456
  if (CLI_VERSION) console.log(` ${CLI_VERSION} (installed CLI)`);
443
457
  if (CLI_VERSION && body.version && body.version !== CLI_VERSION && body.version !== 'dev') {
@@ -634,6 +648,102 @@ async function config(args) {
634
648
  if (sub !== undefined && sub !== 'help' && sub !== '--help' && sub !== '-h') process.exit(2);
635
649
  }
636
650
 
651
+ // ─── simulation: gateway-wide switch that blocks every real system ──────
652
+ //
653
+ // A mandate is bound to a PROFILE, not a connector — see suveren-gateway's
654
+ // tool-proxy.ts (`profileMatches`). Put a real connector and a simulated one
655
+ // on the same profile, and one mandate authorizes both. Simulation mode
656
+ // closes that generically: ON blocks every connector whose manifest carries
657
+ // no `simulation` marker, and forces every connector that DOES have one into
658
+ // its simulated mode, regardless of what credential value is on file.
659
+ // Mandates and profiles are unaffected; the Authority Server never learns
660
+ // about this — it is a purely local, gateway-side switch. See
661
+ // apps/mcp-server/src/lib/simulation-mode.ts for enforcement.
662
+
663
+ function printSimulationHelp() {
664
+ console.log(`suveren-gateway simulation — gateway-wide switch that blocks every real system
665
+
666
+ Usage:
667
+ suveren-gateway simulation on Block every real connector (no confirmation — the safe direction)
668
+ suveren-gateway simulation off Make real systems reachable again (requires confirmation)
669
+ [--confirm live] Non-interactive confirmation
670
+ suveren-gateway simulation status Show the saved setting and (if running) the live one
671
+
672
+ Changes take effect on the next \`suveren-gateway start\` / \`restart\`.
673
+ `);
674
+ }
675
+
676
+ async function printSimulationStatus() {
677
+ const saved = resolveSimulation(DATA_DIR);
678
+ console.log(`Saved: ${saved ? 'on' : 'off'}`);
679
+ try {
680
+ const res = await fetch(`http://localhost:${SUVEREN_PORT}/health`, { signal: AbortSignal.timeout(3000) });
681
+ if (res.ok) {
682
+ const body = await res.json();
683
+ const running = body.simulation === true;
684
+ console.log(`Running: ${running ? 'on' : 'off'}${running !== saved ? ' (restart to apply the saved value)' : ''}`);
685
+ }
686
+ } catch {
687
+ // Not reachable — the saved value is all there is to report.
688
+ }
689
+ }
690
+
691
+ /** Read one line from stdin, or undefined if there is no TTY to prompt on
692
+ * (so a non-interactive caller gets a clean abort instead of a hang). */
693
+ async function promptLine(question) {
694
+ if (!process.stdin.isTTY) return undefined;
695
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
696
+ try {
697
+ return (await rl.question(question)).trim();
698
+ } finally {
699
+ rl.close();
700
+ }
701
+ }
702
+
703
+ async function simulation(args) {
704
+ const sub = args[0] ?? 'status';
705
+
706
+ if (sub === 'help' || sub === '--help' || sub === '-h') { printSimulationHelp(); return; }
707
+
708
+ if (sub === 'status') {
709
+ await printSimulationStatus();
710
+ return;
711
+ }
712
+
713
+ if (sub === 'on') {
714
+ // The safe direction — blocking real systems needs no confirmation.
715
+ writeConfig(DATA_DIR, { simulation: true });
716
+ console.log('Simulation mode: ON (saved) — every connector without a manifest "simulation" marker will be blocked.');
717
+ console.log('Restart to pick it up: suveren-gateway restart');
718
+ return;
719
+ }
720
+
721
+ if (sub === 'off') {
722
+ console.error('Turning simulation mode OFF makes every real system reachable again: any connector');
723
+ console.error('without a manifest "simulation" marker (e.g. a live Gmail or ERP account) will spawn');
724
+ console.error('and execute for real the next time this gateway starts.');
725
+ console.error('');
726
+ let typed = flagValue(args, '--confirm');
727
+ if (typed === undefined) {
728
+ typed = await promptLine('Type "live" to confirm, or anything else to abort: ');
729
+ }
730
+ if (typed !== 'live') {
731
+ console.error(typed === undefined
732
+ ? 'No TTY to confirm on and no --confirm given — aborted. Simulation mode unchanged.'
733
+ : 'Aborted — simulation mode unchanged.');
734
+ process.exit(1);
735
+ }
736
+ writeConfig(DATA_DIR, { simulation: false });
737
+ console.log('Simulation mode: OFF (saved) — real systems become reachable on next restart.');
738
+ console.log('Restart to pick it up: suveren-gateway restart');
739
+ return;
740
+ }
741
+
742
+ console.error(`Unknown: simulation ${sub}\n`);
743
+ printSimulationHelp();
744
+ process.exit(2);
745
+ }
746
+
637
747
  // ─── service: install autostart-on-login (survives reboot) ──────────────
638
748
  //
639
749
  // Keeps the gateway PROCESS always running (starts on login, restarts on
@@ -1139,9 +1249,10 @@ Usage:
1139
1249
  [--as-url <url>] Authority Server URL (saved for next time)
1140
1250
  [--ca-file <path>] Internal CA bundle (saved for next time)
1141
1251
  [--pin-tls --expect-fingerprint <hex>] Pin the AS TLS certificate (saved; https:// only — see below)
1252
+ [--simulation] Block every real system (saved — see \`simulation help\`)
1142
1253
  suveren-gateway stop Stop a detached gateway
1143
1254
  suveren-gateway restart Stop, then start --detach
1144
- suveren-gateway status Show running state + health
1255
+ suveren-gateway status Show running state + health (incl. simulation mode)
1145
1256
  suveren-gateway logs [--tail] Print or tail ~/.suveren/gateway.log
1146
1257
  suveren-gateway service <cmd> Run as a login service that survives reboot
1147
1258
  (install | uninstall | status)
@@ -1152,6 +1263,8 @@ Usage:
1152
1263
  Pin the Authority Server's TLS certificate
1153
1264
  (see \`suveren-gateway config help\` for the fingerprint check)
1154
1265
  suveren-gateway config set pin-tls off Stop enforcing the TLS certificate pin
1266
+ suveren-gateway simulation on|off|status Block (or unblock) every real system
1267
+ (see \`suveren-gateway simulation help\`)
1155
1268
  suveren-gateway help Print this help
1156
1269
 
1157
1270
  Environment:
@@ -1159,6 +1272,7 @@ Environment:
1159
1272
  SUVEREN_MCP_PORT MCP server port (default 3430)
1160
1273
  SUVEREN_DATA_DIR Data directory (default ~/.suveren)
1161
1274
  SUVEREN_AS_URL Authority Server URL — overrides the saved as-url
1275
+ SUVEREN_SIMULATION 1 to force simulation mode for this run — overrides the saved setting
1162
1276
 
1163
1277
  Authority Server resolution order: --as-url flag > SUVEREN_AS_URL env >
1164
1278
  saved as-url (\`config set as-url\`) > default (${DEFAULT_AS_URL}).
@@ -1181,6 +1295,7 @@ async function main() {
1181
1295
  case 'logs': await logs(argv.slice(1)); break;
1182
1296
  case 'service': await service(argv.slice(1)); break;
1183
1297
  case 'config': await config(argv.slice(1)); break;
1298
+ case 'simulation': await simulation(argv.slice(1)); break;
1184
1299
  case 'help':
1185
1300
  case '--help':
1186
1301
  case '-h':
@@ -18,12 +18,24 @@
18
18
  "type": "text",
19
19
  "placeholder": "Leave empty for local SQLite",
20
20
  "optional": true
21
+ },
22
+ {
23
+ "key": "mode",
24
+ "label": "Mode (optional)",
25
+ "type": "text",
26
+ "placeholder": "simulation (default) — live needs an adapter for your CRM",
27
+ "optional": true
21
28
  }
22
29
  ],
23
30
  "envMapping": {
24
- "DATABASE_URL": "databaseUrl"
31
+ "DATABASE_URL": "databaseUrl",
32
+ "CRM_MODE": "mode"
25
33
  }
26
34
  },
35
+ "simulation": {
36
+ "field": "mode",
37
+ "default": "simulation"
38
+ },
27
39
  "personalDefault": true,
28
40
  "oauth": null,
29
41
  "toolGating": {
@@ -118,6 +130,15 @@
118
130
  "category": "read",
119
131
  "boundField": "export_access",
120
132
  "requiredValue": "allowed"
133
+ },
134
+ "load_simulation": {
135
+ "executionMapping": {},
136
+ "staticExecution": {
137
+ "contact_type": "customer",
138
+ "action_type": "setup"
139
+ },
140
+ "actionLabel": "Test data loaded",
141
+ "hideUnlessAuthorized": true
121
142
  }
122
143
  }
123
144
  },
@@ -130,7 +151,8 @@
130
151
  "bounds": {
131
152
  "read_access": "unlimited",
132
153
  "write_daily_max": "50",
133
- "delete_daily_max": "0"
154
+ "delete_daily_max": "0",
155
+ "setup_daily_max": "0"
134
156
  },
135
157
  "context": {
136
158
  "contact_type": "customer,lead,partner,vendor"
@@ -150,7 +172,8 @@
150
172
  "bounds": {
151
173
  "read_access": "unlimited",
152
174
  "write_daily_max": "50",
153
- "delete_daily_max": "10"
175
+ "delete_daily_max": "10",
176
+ "setup_daily_max": "0"
154
177
  },
155
178
  "context": {
156
179
  "contact_type": "customer,lead,partner,vendor"
@@ -162,6 +185,28 @@
162
185
  "10 deletes/day",
163
186
  "review mode"
164
187
  ]
188
+ },
189
+ {
190
+ "name": "Load test data (with approval)",
191
+ "description": "Load a simulation package (customers and optional contacts) into the simulated CRM, for setting up a three-week test. No browsing, writing, or deleting — this template grants nothing but the one-time test-data load, and it still requires your approval.",
192
+ "risk": "high",
193
+ "mode": "review",
194
+ "bounds": {
195
+ "read_access": "none",
196
+ "write_daily_max": "0",
197
+ "delete_daily_max": "0",
198
+ "setup_daily_max": "1"
199
+ },
200
+ "context": {
201
+ "contact_type": "customer"
202
+ },
203
+ "intent": "Allow the agent to load one simulation package of test data into the simulated CRM, for my review before it happens. No other CRM action is granted by this mandate — set up test data separately from the mandate the agent uses for its actual work.",
204
+ "ttl": 2592000,
205
+ "tags": [
206
+ "setup only",
207
+ "review mode",
208
+ "1 load"
209
+ ]
165
210
  }
166
211
  ],
167
212
  "setupHint": "No configuration needed — works out of the box.",
@@ -18,12 +18,24 @@
18
18
  "type": "text",
19
19
  "placeholder": "Leave empty for local SQLite",
20
20
  "optional": true
21
+ },
22
+ {
23
+ "key": "mode",
24
+ "label": "Mode (optional)",
25
+ "type": "text",
26
+ "placeholder": "simulation (default) — live needs an adapter for your ERP",
27
+ "optional": true
21
28
  }
22
29
  ],
23
30
  "envMapping": {
24
- "DATABASE_URL": "databaseUrl"
31
+ "DATABASE_URL": "databaseUrl",
32
+ "ERP_MODE": "mode"
25
33
  }
26
34
  },
35
+ "simulation": {
36
+ "field": "mode",
37
+ "default": "simulation"
38
+ },
27
39
  "personalDefault": true,
28
40
  "oauth": null,
29
41
  "toolGating": {
@@ -115,6 +127,14 @@
115
127
  "action_type": "order"
116
128
  },
117
129
  "actionLabel": "Quote converted to order"
130
+ },
131
+ "load_simulation": {
132
+ "executionMapping": {},
133
+ "staticExecution": {
134
+ "action_type": "setup"
135
+ },
136
+ "actionLabel": "Test data loaded",
137
+ "hideUnlessAuthorized": true
118
138
  }
119
139
  }
120
140
  },
@@ -131,7 +151,8 @@
131
151
  "order_value_daily_max": "0",
132
152
  "quote_daily_max": "20",
133
153
  "send_daily_max": "0",
134
- "order_daily_max": "0"
154
+ "order_daily_max": "0",
155
+ "setup_daily_max": "0"
135
156
  },
136
157
  "context": {
137
158
  "currency": "EUR"
@@ -156,7 +177,8 @@
156
177
  "order_value_daily_max": "50000",
157
178
  "quote_daily_max": "50",
158
179
  "send_daily_max": "20",
159
- "order_daily_max": "10"
180
+ "order_daily_max": "10",
181
+ "setup_daily_max": "0"
160
182
  },
161
183
  "context": {
162
184
  "currency": "EUR"
@@ -168,6 +190,32 @@
168
190
  "review mode",
169
191
  "10 orders/day"
170
192
  ]
193
+ },
194
+ {
195
+ "name": "Load test data (with approval)",
196
+ "description": "Load a simulation package (customers and products) into the simulated ERP, for setting up a three-week test. No quoting, sending, or ordering — this template grants nothing but the one-time test-data load, and it still requires your approval.",
197
+ "risk": "high",
198
+ "mode": "review",
199
+ "bounds": {
200
+ "read_access": "none",
201
+ "value_max": "0",
202
+ "discount_max": "0",
203
+ "order_value_daily_max": "0",
204
+ "quote_daily_max": "0",
205
+ "send_daily_max": "0",
206
+ "order_daily_max": "0",
207
+ "setup_daily_max": "1"
208
+ },
209
+ "context": {
210
+ "currency": "EUR"
211
+ },
212
+ "intent": "Allow the agent to load one simulation package of test data into the simulated ERP, for my review before it happens. No other ERP action is granted by this mandate — set up test data separately from the mandate the agent uses for its actual work.",
213
+ "ttl": 2592000,
214
+ "tags": [
215
+ "setup only",
216
+ "review mode",
217
+ "1 load"
218
+ ]
171
219
  }
172
220
  ],
173
221
  "setupHint": "No configuration needed — works out of the box.",
@@ -7,6 +7,7 @@
7
7
  "linkedin": "linkedin.json",
8
8
  "calendar": "calendar.json",
9
9
  "deploy-github": "deploy-github.json",
10
- "erp": "erp.json"
10
+ "erp": "erp.json",
11
+ "mail": "mail.json"
11
12
  }
12
13
  }
@@ -0,0 +1,148 @@
1
+ {
2
+ "id": "mail",
3
+ "name": "Email (simulation)",
4
+ "version": "1.0.0",
5
+ "description": "Simulated inbox for testing an agent's email replies — nothing leaves the laptop",
6
+ "icon": "mail",
7
+ "profile": "email",
8
+ "npmPackage": "@humanagencyp/email-mcp",
9
+ "mcp": {
10
+ "command": "email-mcp",
11
+ "args": []
12
+ },
13
+ "credentials": {
14
+ "fields": [
15
+ {
16
+ "key": "mode",
17
+ "label": "Mode (optional)",
18
+ "type": "text",
19
+ "placeholder": "simulation (default) — live needs a mailbox adapter",
20
+ "optional": true
21
+ },
22
+ {
23
+ "key": "databaseUrl",
24
+ "label": "Database URL (optional)",
25
+ "type": "text",
26
+ "placeholder": "Leave empty for local SQLite",
27
+ "optional": true
28
+ }
29
+ ],
30
+ "envMapping": {
31
+ "EMAIL_MODE": "mode",
32
+ "DATABASE_URL": "databaseUrl"
33
+ }
34
+ },
35
+ "simulation": {
36
+ "field": "mode",
37
+ "default": "simulation"
38
+ },
39
+ "personalDefault": false,
40
+ "oauth": null,
41
+ "toolGating": {
42
+ "default": {
43
+ "executionMapping": {},
44
+ "staticExecution": {}
45
+ },
46
+ "overrides": {
47
+ "list_messages": {
48
+ "category": "read",
49
+ "boundField": "read_access",
50
+ "requiredValue": "unlimited"
51
+ },
52
+ "get_message": {
53
+ "category": "read",
54
+ "boundField": "read_access",
55
+ "requiredValue": "unlimited"
56
+ },
57
+ "send_message": {
58
+ "executionMapping": {
59
+ "to": [
60
+ {
61
+ "field": "recipient_count",
62
+ "transform": "length"
63
+ },
64
+ {
65
+ "field": "allowed_recipients",
66
+ "transform": "join"
67
+ },
68
+ {
69
+ "field": "allowed_domains",
70
+ "transform": "join_domains"
71
+ }
72
+ ]
73
+ },
74
+ "staticExecution": {
75
+ "action_type": "send"
76
+ },
77
+ "actionLabel": "Email sent"
78
+ },
79
+ "load_simulation": {
80
+ "executionMapping": {},
81
+ "staticExecution": {
82
+ "action_type": "setup"
83
+ },
84
+ "actionLabel": "Test data loaded",
85
+ "hideUnlessAuthorized": true
86
+ }
87
+ }
88
+ },
89
+ "templates": [
90
+ {
91
+ "name": "Work the inbox",
92
+ "description": "Read the simulated inbox and send replies. Cannot load or replace test data.",
93
+ "risk": "low",
94
+ "mode": "automatic",
95
+ "bounds": {
96
+ "read_access": "unlimited",
97
+ "recipient_max": "1",
98
+ "send_daily_max": "50",
99
+ "setup_daily_max": "0"
100
+ },
101
+ "context": {
102
+ "allowed_recipients": "",
103
+ "allowed_domains": ""
104
+ },
105
+ "intent": "Allow the agent to read the simulated inbox and reply to messages on its own. It cannot load or replace the test data the inbox was seeded with.",
106
+ "ttl": 2592000,
107
+ "tags": [
108
+ "read + reply",
109
+ "automatic",
110
+ "no setup"
111
+ ]
112
+ },
113
+ {
114
+ "name": "Load test data (with approval)",
115
+ "description": "Load a simulation package into an empty inbox. No reading or sending — a separate, one-time setup mandate.",
116
+ "risk": "high",
117
+ "mode": "review",
118
+ "bounds": {
119
+ "read_access": "none",
120
+ "recipient_max": "0",
121
+ "send_daily_max": "0",
122
+ "setup_daily_max": "1"
123
+ },
124
+ "context": {
125
+ "allowed_recipients": "",
126
+ "allowed_domains": ""
127
+ },
128
+ "intent": "Allow the agent to load one simulation package into an empty inbox, with my review before it happens. No reading or sending under this mandate — grant it separately from the mandate the agent uses for its daily work.",
129
+ "ttl": 2592000,
130
+ "tags": [
131
+ "setup only",
132
+ "review mode",
133
+ "1 load/day"
134
+ ]
135
+ }
136
+ ],
137
+ "setupHint": "No configuration needed — works out of the box, in simulation mode.",
138
+ "setupGuide": [
139
+ {
140
+ "title": "Ready to use",
141
+ "description": "This connector stores a simulated inbox locally on your machine. Nothing is sent anywhere until you load test data with a separate, reviewed setup mandate."
142
+ },
143
+ {
144
+ "title": "Optional: use a shared database",
145
+ "description": "For team use, provide a PostgreSQL connection string in the Database URL field. Otherwise, leave it empty to use local SQLite storage."
146
+ }
147
+ ]
148
+ }