@askexenow/exe-os 0.9.82 → 0.9.84
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/deploy/compose/.env +73 -0
- package/deploy/compose/.env.askexe-control-plane.example +18 -0
- package/deploy/compose/.env.customer.example +69 -0
- package/deploy/compose/.env.example +69 -0
- package/deploy/compose/README.md +164 -0
- package/deploy/compose/docker-compose.yml +392 -0
- package/deploy/compose/gateway.json +1 -0
- package/deploy/compose/generate-env.ts +252 -0
- package/deploy/stack-manifests/v0.9.json +137 -1
- package/dist/bin/cli.js +175 -16
- package/dist/bin/stack-update.js +169 -10
- package/package.json +3 -2
- package/stack.release.json +4 -4
package/dist/bin/stack-update.js
CHANGED
|
@@ -19,12 +19,13 @@ function isMainModule(importMetaUrl) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// src/lib/stack-update.ts
|
|
22
|
-
import { execFileSync } from "child_process";
|
|
23
|
-
import { createVerify, verify as verifySignature } from "crypto";
|
|
24
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
23
|
+
import { createVerify, randomBytes, verify as verifySignature } from "crypto";
|
|
24
|
+
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
25
25
|
import http from "http";
|
|
26
26
|
import https from "https";
|
|
27
27
|
import path3 from "path";
|
|
28
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
28
29
|
|
|
29
30
|
// src/lib/license.ts
|
|
30
31
|
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -382,10 +383,139 @@ Re-run with --allow-breaking ${missing.map((c) => c.id).join(",")}`
|
|
|
382
383
|
);
|
|
383
384
|
}
|
|
384
385
|
}
|
|
386
|
+
function commandSucceeds(cmd, args = []) {
|
|
387
|
+
const res = spawnSync(cmd, args, { stdio: "ignore" });
|
|
388
|
+
return res.status === 0;
|
|
389
|
+
}
|
|
390
|
+
function shellSucceeds(command) {
|
|
391
|
+
const res = spawnSync("sh", ["-lc", command], { stdio: "ignore" });
|
|
392
|
+
return res.status === 0;
|
|
393
|
+
}
|
|
394
|
+
function resolvePackageRoot() {
|
|
395
|
+
const here = path3.dirname(fileURLToPath2(import.meta.url));
|
|
396
|
+
const candidates = [
|
|
397
|
+
path3.resolve(here, "..", ".."),
|
|
398
|
+
path3.resolve(here, ".."),
|
|
399
|
+
process.cwd()
|
|
400
|
+
];
|
|
401
|
+
for (const c of candidates) {
|
|
402
|
+
if (existsSync4(path3.join(c, "package.json")) && existsSync4(path3.join(c, "deploy", "compose", "docker-compose.yml"))) return c;
|
|
403
|
+
}
|
|
404
|
+
return process.cwd();
|
|
405
|
+
}
|
|
406
|
+
function copyTemplateIfMissing(srcRel, dest, created) {
|
|
407
|
+
if (existsSync4(dest)) return;
|
|
408
|
+
const src = path3.join(resolvePackageRoot(), srcRel);
|
|
409
|
+
if (!existsSync4(src)) throw new Error(`Missing packaged stack template: ${srcRel}. Reinstall/update exe-os and retry.`);
|
|
410
|
+
mkdirSync3(path3.dirname(dest), { recursive: true });
|
|
411
|
+
copyFileSync(src, dest);
|
|
412
|
+
created.push(dest);
|
|
413
|
+
}
|
|
414
|
+
function installDockerUbuntu(exec) {
|
|
415
|
+
if (process.platform !== "linux") throw new Error("Docker auto-install is only supported on Linux. Install Docker manually, then retry.");
|
|
416
|
+
if (!existsSync4("/etc/os-release")) throw new Error("Cannot detect Linux distro; install Docker manually, then retry.");
|
|
417
|
+
const osRelease = readFileSync3("/etc/os-release", "utf8");
|
|
418
|
+
if (!/ID=(ubuntu|debian)|ID_LIKE=.*debian/.test(osRelease)) {
|
|
419
|
+
throw new Error("Docker auto-install currently supports Ubuntu/Debian only. Install Docker manually, then retry.");
|
|
420
|
+
}
|
|
421
|
+
const script = [
|
|
422
|
+
"set -e",
|
|
423
|
+
"sudo apt-get update",
|
|
424
|
+
"sudo apt-get install -y ca-certificates curl gnupg",
|
|
425
|
+
"sudo install -m 0755 -d /etc/apt/keyrings",
|
|
426
|
+
"if [ ! -f /etc/apt/keyrings/docker.gpg ]; then curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg; sudo chmod a+r /etc/apt/keyrings/docker.gpg; fi",
|
|
427
|
+
". /etc/os-release",
|
|
428
|
+
'CODENAME="${VERSION_CODENAME:-bookworm}"',
|
|
429
|
+
'if [ "${ID:-}" = "debian" ]; then DOCKER_DISTRO=debian; else DOCKER_DISTRO=ubuntu; fi',
|
|
430
|
+
`printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/%s %s stable\\n' "$(dpkg --print-architecture)" "$DOCKER_DISTRO" "$CODENAME" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null`,
|
|
431
|
+
"sudo apt-get update",
|
|
432
|
+
"sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin",
|
|
433
|
+
"sudo systemctl enable --now docker",
|
|
434
|
+
"sudo docker version >/dev/null",
|
|
435
|
+
"sudo docker compose version >/dev/null"
|
|
436
|
+
].join("\n");
|
|
437
|
+
exec("sh", ["-lc", script]);
|
|
438
|
+
}
|
|
439
|
+
function randomSecret(bytes = 32) {
|
|
440
|
+
return randomBytes(bytes).toString("base64url");
|
|
441
|
+
}
|
|
442
|
+
function hydrateEnv(raw, opts) {
|
|
443
|
+
let next = raw;
|
|
444
|
+
const license = opts.licenseKey || process.env.EXE_LICENSE_KEY || loadLicense() || "";
|
|
445
|
+
const domain = opts.domain || process.env.EXE_STACK_DOMAIN || process.env.CUSTOMER_DOMAIN || "";
|
|
446
|
+
const replacements = {};
|
|
447
|
+
const env = parseEnv(raw);
|
|
448
|
+
for (const [key, value] of env.entries()) {
|
|
449
|
+
if (!/CHANGEME/.test(value)) continue;
|
|
450
|
+
if (key === "EXE_LICENSE_KEY" && license) replacements[key] = license;
|
|
451
|
+
else if (key.endsWith("_PASSWORD")) replacements[key] = randomSecret(24);
|
|
452
|
+
else if (key.endsWith("_SECRET") || key.endsWith("_TOKEN") || key.endsWith("_KEY") || key.endsWith("_SALT")) replacements[key] = value.replace(/CHANGEME[A-Z0-9_]*/g, randomSecret(32));
|
|
453
|
+
else if (key === "EXED_MCP_TOKEN") replacements[key] = randomSecret(32);
|
|
454
|
+
}
|
|
455
|
+
if (domain) next = next.replaceAll("CHANGEME_DOMAIN", domain);
|
|
456
|
+
next = patchEnv(next, replacements);
|
|
457
|
+
const remaining = [...parseEnv(next).entries()].filter(([, value]) => /CHANGEME/.test(value)).map(([key, value]) => `${key}=${value}`);
|
|
458
|
+
return { raw: next, hadPlaceholders: /CHANGEME/.test(raw), remaining: [...new Set(remaining)] };
|
|
459
|
+
}
|
|
460
|
+
function bootstrapStackHost(options) {
|
|
461
|
+
const exec = options.exec ?? defaultExec;
|
|
462
|
+
const createdFiles = [];
|
|
463
|
+
const actions = [];
|
|
464
|
+
let dockerInstalled = commandSucceeds("docker", ["version"]);
|
|
465
|
+
let dockerComposeInstalled = shellSucceeds("docker compose version");
|
|
466
|
+
if ((!dockerInstalled || !dockerComposeInstalled) && options.installDocker) {
|
|
467
|
+
actions.push("install_docker");
|
|
468
|
+
installDockerUbuntu(exec);
|
|
469
|
+
dockerInstalled = commandSucceeds("docker", ["version"]);
|
|
470
|
+
dockerComposeInstalled = shellSucceeds("docker compose version");
|
|
471
|
+
}
|
|
472
|
+
copyTemplateIfMissing("deploy/compose/docker-compose.yml", options.composeFile, createdFiles);
|
|
473
|
+
copyTemplateIfMissing("deploy/compose/.env.customer.example", options.envFile, createdFiles);
|
|
474
|
+
const brandingDest = path3.join(path3.dirname(options.envFile), "branding.json");
|
|
475
|
+
copyTemplateIfMissing("deploy/compose/gateway.json", path3.join(path3.dirname(options.envFile), "gateway.json"), createdFiles);
|
|
476
|
+
if (!existsSync4(brandingDest)) writeFileSync2(brandingDest, JSON.stringify({ brandName: "Hygo", productName: "Hygo OS" }, null, 2) + "\n", { mode: 384 });
|
|
477
|
+
let envHadPlaceholders = false;
|
|
478
|
+
let envRemainingPlaceholders = [];
|
|
479
|
+
if (existsSync4(options.envFile)) {
|
|
480
|
+
const envRaw = readFileSync3(options.envFile, "utf8");
|
|
481
|
+
const hydrated = hydrateEnv(envRaw, options);
|
|
482
|
+
envHadPlaceholders = hydrated.hadPlaceholders;
|
|
483
|
+
envRemainingPlaceholders = hydrated.remaining;
|
|
484
|
+
if (hydrated.raw !== envRaw) {
|
|
485
|
+
writeFileSync2(options.envFile, hydrated.raw, { mode: 384 });
|
|
486
|
+
actions.push("hydrate_env_secrets");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
dockerInstalled,
|
|
491
|
+
dockerComposeInstalled,
|
|
492
|
+
composeFileExists: existsSync4(options.composeFile),
|
|
493
|
+
envFileExists: existsSync4(options.envFile),
|
|
494
|
+
envHadPlaceholders,
|
|
495
|
+
envRemainingPlaceholders,
|
|
496
|
+
licensePresent: !!(options.licenseKey || process.env.EXE_LICENSE_KEY || loadLicense()),
|
|
497
|
+
createdFiles,
|
|
498
|
+
actions
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function assertHostReadyForApply(report) {
|
|
502
|
+
const blockers = [];
|
|
503
|
+
if (!report.dockerInstalled) blockers.push("Docker is not installed/running. Re-run with --yes to auto-install on Ubuntu/Debian, or install Docker manually.");
|
|
504
|
+
if (!report.dockerComposeInstalled) blockers.push("Docker Compose plugin is missing. Re-run with --yes to auto-install on Ubuntu/Debian, or install docker-compose-plugin manually.");
|
|
505
|
+
if (!report.composeFileExists) blockers.push("docker-compose.yml is missing and could not be created.");
|
|
506
|
+
if (!report.envFileExists) blockers.push(".env is missing and could not be created.");
|
|
507
|
+
if (!report.licensePresent) blockers.push("Exe OS license key is missing. Run `exe-os setup`, `exe-os --activate <key>`, or pass --license-key.");
|
|
508
|
+
const hardPlaceholders = report.envRemainingPlaceholders.filter((p) => !/WHATSAPP|API_ROUTER|MONITOR_AGENT/.test(p));
|
|
509
|
+
if (hardPlaceholders.length > 0) blockers.push(`Required .env placeholders remain: ${hardPlaceholders.join(", ")}`);
|
|
510
|
+
if (blockers.length > 0) throw new Error(`Stack host is not ready:
|
|
511
|
+
- ${blockers.join("\n- ")}`);
|
|
512
|
+
}
|
|
385
513
|
async function runStackUpdate(options) {
|
|
386
514
|
const exec = options.exec ?? defaultExec;
|
|
387
515
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
388
516
|
if (options.rollback) return rollbackStackUpdate(options);
|
|
517
|
+
const report = bootstrapStackHost({ ...options, installDocker: options.installDocker ?? !!options.yes });
|
|
518
|
+
if (!options.dryRun) assertHostReadyForApply(report);
|
|
389
519
|
const manifest = await loadStackManifest(options.manifestRef, options.fetchText, options.manifestPublicKey, options.manifestAuthToken);
|
|
390
520
|
const envRaw = readFileSync3(options.envFile, "utf8");
|
|
391
521
|
const plan = createStackUpdatePlan(manifest, envRaw, options.targetVersion);
|
|
@@ -553,12 +683,16 @@ async function defaultPostJson(url, body, authToken) {
|
|
|
553
683
|
function defaultStackPaths() {
|
|
554
684
|
const cwdCompose = path3.resolve("docker-compose.yml");
|
|
555
685
|
const cwdEnv = path3.resolve(".env");
|
|
686
|
+
const packagedManifest = path3.join(resolvePackageRoot(), "deploy", "stack-manifests", "v0.9.json");
|
|
687
|
+
const manifestRef = process.env.EXE_STACK_MANIFEST || (existsSync4(packagedManifest) ? packagedManifest : "https://update.askexe.com/stack-manifest.json");
|
|
556
688
|
return {
|
|
557
689
|
composeFile: process.env.EXE_STACK_COMPOSE_FILE || (existsSync4(cwdCompose) ? cwdCompose : "/opt/exe-stack/docker-compose.yml"),
|
|
558
690
|
envFile: process.env.EXE_STACK_ENV_FILE || (existsSync4(cwdEnv) ? cwdEnv : "/opt/exe-stack/.env"),
|
|
559
|
-
manifestRef
|
|
560
|
-
|
|
561
|
-
|
|
691
|
+
manifestRef,
|
|
692
|
+
// Only call update.askexe.com if explicitly configured or if a remote manifest was requested.
|
|
693
|
+
// Packaged manifests keep cold-start installs unblocked even before update-service entitlements are provisioned.
|
|
694
|
+
auditUrl: process.env.EXE_STACK_AUDIT_URL || (/^https?:\/\//.test(manifestRef) ? "https://update.askexe.com/v1/deploy-audits" : void 0),
|
|
695
|
+
imageCredentialsUrl: process.env.EXE_STACK_IMAGE_CREDENTIALS_URL || (/^https?:\/\//.test(manifestRef) ? "https://update.askexe.com/v1/image-credentials" : void 0),
|
|
562
696
|
manifestAuthToken: process.env.EXE_STACK_UPDATE_TOKEN || process.env.EXE_LICENSE_KEY || loadLicense() || void 0,
|
|
563
697
|
manifestPublicKey: loadDefaultPublicKey()
|
|
564
698
|
};
|
|
@@ -589,7 +723,8 @@ function parseArgs(args) {
|
|
|
589
723
|
rollback: false,
|
|
590
724
|
deploymentPersona: process.env.EXE_STACK_DEPLOYMENT_PERSONA === "askexe-control-plane" ? "askexe-control-plane" : "customer",
|
|
591
725
|
yes: false,
|
|
592
|
-
allowedBreakingChangeIds: []
|
|
726
|
+
allowedBreakingChangeIds: [],
|
|
727
|
+
noBootstrap: false
|
|
593
728
|
};
|
|
594
729
|
for (let i = 0; i < args.length; i++) {
|
|
595
730
|
const arg = args[i];
|
|
@@ -600,8 +735,8 @@ function parseArgs(args) {
|
|
|
600
735
|
else if (arg.startsWith("--target=")) opts.targetVersion = arg.split("=")[1];
|
|
601
736
|
else if (arg === "--compose-file") opts.composeFile = next();
|
|
602
737
|
else if (arg.startsWith("--compose-file=")) opts.composeFile = arg.split("=").slice(1).join("=");
|
|
603
|
-
else if (arg === "--env-file") opts.envFile = next();
|
|
604
|
-
else if (arg.startsWith("--env-file=")) opts.envFile = arg.split("=").slice(1).join("=");
|
|
738
|
+
else if (arg === "--env-file" || arg === "--stack-env-file") opts.envFile = next();
|
|
739
|
+
else if (arg.startsWith("--env-file=") || arg.startsWith("--stack-env-file=")) opts.envFile = arg.split("=").slice(1).join("=");
|
|
605
740
|
else if (arg === "--lock-file") opts.lockFile = next();
|
|
606
741
|
else if (arg === "--public-key") opts.manifestPublicKey = readFileSync4(next(), "utf8");
|
|
607
742
|
else if (arg.startsWith("--public-key=")) opts.manifestPublicKey = readFileSync4(arg.split("=").slice(1).join("="), "utf8");
|
|
@@ -630,6 +765,9 @@ function parseArgs(args) {
|
|
|
630
765
|
else if (arg.startsWith("--break-glass=")) opts.breakGlassReason = arg.split("=").slice(1).join("=");
|
|
631
766
|
else if (arg === "--break-glass-audit-file") opts.breakGlassAuditFile = next();
|
|
632
767
|
else if (arg.startsWith("--break-glass-audit-file=")) opts.breakGlassAuditFile = arg.split("=").slice(1).join("=");
|
|
768
|
+
else if (arg === "--domain") opts.domain = next();
|
|
769
|
+
else if (arg.startsWith("--domain=")) opts.domain = arg.split("=").slice(1).join("=");
|
|
770
|
+
else if (arg === "--no-bootstrap") opts.noBootstrap = true;
|
|
633
771
|
else if (arg === "--dry-run") opts.dryRun = true;
|
|
634
772
|
else if (arg === "--check") opts.check = true;
|
|
635
773
|
else if (arg === "--yes" || arg === "-y") opts.yes = true;
|
|
@@ -654,10 +792,13 @@ Options:
|
|
|
654
792
|
--manifest <ref> Stack manifest JSON path or URL (default: update.askexe.com)
|
|
655
793
|
--target <version> Stack version to install (default: manifest.latest)
|
|
656
794
|
--compose-file <path> docker-compose.yml path (default: ./docker-compose.yml or /opt/exe-stack/docker-compose.yml)
|
|
657
|
-
--env-file <path>
|
|
795
|
+
--stack-env-file <path> .env path (default: ./.env or /opt/exe-stack/.env)
|
|
796
|
+
--env-file <path> Alias; prefer --stack-env-file because Node 22 reserves --env-file
|
|
658
797
|
--lock-file <path> Lock file path (default: beside .env)
|
|
659
798
|
--check Print available changes only
|
|
660
799
|
--dry-run Plan only; do not run Docker
|
|
800
|
+
--domain <domain> Fill CHANGEME_DOMAIN in new stack .env
|
|
801
|
+
--no-bootstrap Do not create templates/hydrate .env/check host prereqs
|
|
661
802
|
--public-key <path> PEM public key for signed manifest verification
|
|
662
803
|
--auth-token <token> Bearer token for private update manifest/audit API
|
|
663
804
|
--auth-token-env <name> Read bearer token from an environment variable
|
|
@@ -695,6 +836,18 @@ function printBreaking(changes) {
|
|
|
695
836
|
if (c.expectedDowntimeMinutes) console.log(` Expected downtime: ${c.expectedDowntimeMinutes} minutes`);
|
|
696
837
|
}
|
|
697
838
|
}
|
|
839
|
+
function printHostReport(report) {
|
|
840
|
+
console.log("Host preflight:");
|
|
841
|
+
console.log(` Docker: ${report.dockerInstalled ? "\u2705" : "\u274C"}`);
|
|
842
|
+
console.log(` Docker Compose: ${report.dockerComposeInstalled ? "\u2705" : "\u274C"}`);
|
|
843
|
+
console.log(` Compose file: ${report.composeFileExists ? "\u2705" : "\u274C"}`);
|
|
844
|
+
console.log(` Env file: ${report.envFileExists ? "\u2705" : "\u274C"}`);
|
|
845
|
+
console.log(` License: ${report.licensePresent ? "\u2705" : "\u274C"}`);
|
|
846
|
+
if (report.createdFiles.length > 0) console.log(` Created: ${report.createdFiles.join(", ")}`);
|
|
847
|
+
if (report.actions.length > 0) console.log(` Actions: ${report.actions.join(", ")}`);
|
|
848
|
+
if (report.envRemainingPlaceholders.length > 0) console.log(` Remaining placeholders: ${report.envRemainingPlaceholders.join(", ")}`);
|
|
849
|
+
console.log("");
|
|
850
|
+
}
|
|
698
851
|
async function main(args = process.argv.slice(2)) {
|
|
699
852
|
const opts = parseArgs(args);
|
|
700
853
|
if (opts.rollback) {
|
|
@@ -706,6 +859,12 @@ async function main(args = process.argv.slice(2)) {
|
|
|
706
859
|
console.log(`\u2705 Stack rollback attempted using backup: ${result2.backupEnvFile ?? "latest backup"}`);
|
|
707
860
|
return;
|
|
708
861
|
}
|
|
862
|
+
let hostReport;
|
|
863
|
+
if (!opts.noBootstrap) {
|
|
864
|
+
hostReport = bootstrapStackHost({ ...opts, installDocker: opts.yes, domain: opts.domain });
|
|
865
|
+
printHostReport(hostReport);
|
|
866
|
+
if (!opts.check && !opts.dryRun) assertHostReadyForApply(hostReport);
|
|
867
|
+
}
|
|
709
868
|
const manifest = await loadStackManifest(opts.manifestRef, void 0, opts.manifestPublicKey, opts.manifestAuthToken);
|
|
710
869
|
const envRaw = readFileSync4(opts.envFile, "utf8");
|
|
711
870
|
const plan = createStackUpdatePlan(manifest, envRaw, opts.targetVersion);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askexenow/exe-os",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.84",
|
|
4
4
|
"description": "AI employee operating system — persistent memory, task management, and multi-agent coordination for Claude Code.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"stack.release.json",
|
|
58
58
|
"stack.release.schema.json",
|
|
59
59
|
"package.json",
|
|
60
|
-
"LICENSE"
|
|
60
|
+
"LICENSE",
|
|
61
|
+
"deploy/compose"
|
|
61
62
|
],
|
|
62
63
|
"exports": {
|
|
63
64
|
".": "./dist/index.js",
|
package/stack.release.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"repo": "AskExe/exe-os",
|
|
5
5
|
"service": "exed",
|
|
6
6
|
"packageName": "@askexenow/exe-os",
|
|
7
|
-
"version": "0.9.
|
|
8
|
-
"image": "ghcr.io/askexe/exed:v0.9.
|
|
7
|
+
"version": "0.9.7",
|
|
8
|
+
"image": "ghcr.io/askexe/exed:v0.9.7",
|
|
9
9
|
"imageEnv": "EXED_IMAGE_TAG",
|
|
10
10
|
"stackParticipation": {
|
|
11
11
|
"required": true,
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"breakingChanges": [],
|
|
43
43
|
"dataSovereignty": "Customer-local memory/tasks/behaviors stay in SQLCipher/local storage. Updates must not overwrite roster, identity, behavior, or local memory files.",
|
|
44
44
|
"releaseLine": "v0.9 private/customer pilot; v1.0 is public-beta stable.",
|
|
45
|
-
"highGhostStack": "0.9.
|
|
45
|
+
"highGhostStack": "0.9.7",
|
|
46
46
|
"deploymentScope": "customer"
|
|
47
47
|
},
|
|
48
48
|
"deploymentScope": "customer",
|
|
49
|
-
"highGhostStack": "0.9.
|
|
49
|
+
"highGhostStack": "0.9.7"
|
|
50
50
|
}
|