@addai/node 0.30.13 → 0.30.15
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/dist/auto-update.d.ts +3 -0
- package/dist/auto-update.js +15 -0
- package/dist/capabilities.d.ts +17 -3
- package/dist/capabilities.js +18 -8
- package/dist/claude-models.d.ts +4 -12
- package/dist/claude-models.js +12 -88
- package/dist/claude-print.d.ts +15 -0
- package/dist/claude-print.js +16 -0
- package/dist/codex-models.d.ts +81 -0
- package/dist/codex-models.js +385 -0
- package/dist/codex-spawn.d.ts +19 -8
- package/dist/codex-spawn.js +68 -16
- package/dist/command-runner.js +31 -9
- package/dist/desktop/docker.js +2 -0
- package/dist/desktop/manager.d.ts +2 -1
- package/dist/desktop/manager.js +51 -1
- package/dist/desktop/provider.d.ts +1 -1
- package/dist/desktop/provider.js +2 -0
- package/dist/desktop/relay-client.js +7 -0
- package/dist/desktop/spec.d.ts +2 -0
- package/dist/direct/aios-bridge.d.ts +13 -0
- package/dist/direct/aios-bridge.js +177 -0
- package/dist/direct/aios-mcp.d.ts +2 -0
- package/dist/direct/aios-mcp.js +132 -0
- package/dist/direct/aios-tools.d.ts +351 -0
- package/dist/direct/aios-tools.js +120 -0
- package/dist/direct/board-chat-provider.d.ts +64 -0
- package/dist/direct/board-chat-provider.js +154 -0
- package/dist/direct/board-chat.d.ts +40 -0
- package/dist/direct/board-chat.js +414 -0
- package/dist/direct/config.d.ts +2 -0
- package/dist/direct/config.js +60 -0
- package/dist/direct/launch.d.ts +14 -0
- package/dist/direct/launch.js +121 -0
- package/dist/direct/protocol.d.ts +46 -0
- package/dist/direct/protocol.js +84 -0
- package/dist/direct/relay-main.d.ts +1 -0
- package/dist/direct/relay-main.js +21 -0
- package/dist/direct/relay.d.ts +26 -0
- package/dist/direct/relay.js +334 -0
- package/dist/direct/sessions.d.ts +61 -0
- package/dist/direct/sessions.js +368 -0
- package/dist/direct/supervisor.d.ts +9 -0
- package/dist/direct/supervisor.js +137 -0
- package/dist/direct/terminal.d.ts +1 -0
- package/dist/direct/terminal.js +106 -0
- package/dist/direct/worker.d.ts +1 -0
- package/dist/direct/worker.js +278 -0
- package/dist/entitybox/container-action.cjs +85 -0
- package/dist/entitybox/container-watchdog.cjs +70 -0
- package/dist/entitybox/model-entry.d.ts +3 -0
- package/dist/entitybox/model-entry.js +233 -0
- package/dist/entitybox/model-mcp-proxy.d.ts +3 -0
- package/dist/entitybox/model-mcp-proxy.js +85 -0
- package/dist/entitybox-application-boundary.d.ts +59 -0
- package/dist/entitybox-application-boundary.js +161 -0
- package/dist/entitybox-application-guard-core.d.ts +44 -0
- package/dist/entitybox-application-guard-core.js +164 -0
- package/dist/entitybox-application-guard-process.d.ts +1 -0
- package/dist/entitybox-application-guard-process.js +83 -0
- package/dist/entitybox-application-guard.d.ts +46 -0
- package/dist/entitybox-application-guard.js +239 -0
- package/dist/entitybox-capacity-health-host.d.ts +12 -0
- package/dist/entitybox-capacity-health-host.js +80 -0
- package/dist/entitybox-capacity-health-worker.d.ts +22 -0
- package/dist/entitybox-capacity-health-worker.js +168 -0
- package/dist/entitybox-capacity-host.d.ts +17 -0
- package/dist/entitybox-capacity-host.js +155 -0
- package/dist/entitybox-capacity-owner-process.d.ts +1 -0
- package/dist/entitybox-capacity-owner-process.js +32 -0
- package/dist/entitybox-capacity-owner.d.ts +16 -0
- package/dist/entitybox-capacity-owner.js +90 -0
- package/dist/entitybox-capacity-programs.d.ts +7 -0
- package/dist/entitybox-capacity-programs.js +49 -0
- package/dist/entitybox-capacity-provisioner.d.ts +82 -0
- package/dist/entitybox-capacity-provisioner.js +404 -0
- package/dist/entitybox-capacity-publication.d.ts +29 -0
- package/dist/entitybox-capacity-publication.js +199 -0
- package/dist/entitybox-capacity-registration.d.ts +36 -0
- package/dist/entitybox-capacity-registration.js +75 -0
- package/dist/entitybox-capacity-worker.d.ts +29 -0
- package/dist/entitybox-capacity-worker.js +113 -0
- package/dist/entitybox-computer-task.d.ts +69 -0
- package/dist/entitybox-computer-task.js +176 -0
- package/dist/entitybox-controller-recovery.d.ts +40 -0
- package/dist/entitybox-controller-recovery.js +156 -0
- package/dist/entitybox-desktop-action-server.d.ts +34 -0
- package/dist/entitybox-desktop-action-server.js +176 -0
- package/dist/entitybox-desktop-admission.d.ts +32 -0
- package/dist/entitybox-desktop-admission.js +79 -0
- package/dist/entitybox-display-binding.d.ts +13 -0
- package/dist/entitybox-display-binding.js +31 -0
- package/dist/entitybox-execution-container.d.ts +82 -0
- package/dist/entitybox-execution-container.js +450 -0
- package/dist/entitybox-execution-host.d.ts +40 -0
- package/dist/entitybox-execution-host.js +168 -0
- package/dist/entitybox-execution-lease.d.ts +81 -0
- package/dist/entitybox-execution-lease.js +332 -0
- package/dist/entitybox-file-inputs.d.ts +51 -0
- package/dist/entitybox-file-inputs.js +182 -0
- package/dist/entitybox-file-session.d.ts +38 -0
- package/dist/entitybox-file-session.js +82 -0
- package/dist/entitybox-host-process.d.ts +12 -0
- package/dist/entitybox-host-process.js +120 -0
- package/dist/entitybox-install.d.ts +10 -0
- package/dist/entitybox-install.js +40 -0
- package/dist/entitybox-mcp-broker.d.ts +77 -0
- package/dist/entitybox-mcp-broker.js +420 -0
- package/dist/entitybox-mcp-contract.d.ts +10 -0
- package/dist/entitybox-mcp-contract.js +24 -0
- package/dist/entitybox-mcp-guard.d.ts +3 -0
- package/dist/entitybox-mcp-guard.js +167 -0
- package/dist/entitybox-model-artifact.d.ts +9 -0
- package/dist/entitybox-model-artifact.js +83 -0
- package/dist/entitybox-native-services.d.ts +33 -0
- package/dist/entitybox-native-services.js +172 -0
- package/dist/entitybox-operation.d.ts +4 -0
- package/dist/entitybox-operation.js +34 -0
- package/dist/entitybox-provider-broker.d.ts +41 -0
- package/dist/entitybox-provider-broker.js +149 -0
- package/dist/entitybox-queue-request.d.ts +32 -0
- package/dist/entitybox-queue-request.js +141 -0
- package/dist/entitybox-recovery-host.d.ts +12 -0
- package/dist/entitybox-recovery-host.js +80 -0
- package/dist/entitybox-recovery-store.d.ts +33 -0
- package/dist/entitybox-recovery-store.js +150 -0
- package/dist/entitybox-recovery-worker.d.ts +30 -0
- package/dist/entitybox-recovery-worker.js +134 -0
- package/dist/entitybox-vault-provider.d.ts +27 -0
- package/dist/entitybox-vault-provider.js +127 -0
- package/dist/harness-registry.js +3 -1
- package/dist/index.js +72 -38
- package/dist/install.d.ts +14 -2
- package/dist/install.js +115 -10
- package/dist/model-cache.d.ts +38 -0
- package/dist/model-cache.js +137 -0
- package/dist/request-pump.d.ts +3 -0
- package/dist/request-pump.js +21 -7
- package/dist/session-runner.d.ts +8 -1
- package/dist/session-runner.js +56 -3
- package/dist/supabase-client.js +4 -1
- package/package.json +5 -1
- package/scripts/copy-assets.js +4 -2
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.EntityboxCapacityProvisioner = exports.entityboxCapacityLabels = void 0;
|
|
37
|
+
exports.readEntityboxCapacity = readEntityboxCapacity;
|
|
38
|
+
exports.observeEntityboxCapacity = observeEntityboxCapacity;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const crypto_1 = require("crypto");
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
44
|
+
const entitybox_execution_lease_1 = require("./entitybox-execution-lease");
|
|
45
|
+
const entitybox_application_boundary_1 = require("./entitybox-application-boundary");
|
|
46
|
+
const entitybox_display_binding_1 = require("./entitybox-display-binding");
|
|
47
|
+
const entitybox_recovery_store_1 = require("./entitybox-recovery-store");
|
|
48
|
+
const programs = __importStar(require("./entitybox-capacity-programs"));
|
|
49
|
+
const uuid = /^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
|
|
50
|
+
const fail = () => new entitybox_execution_lease_1.EntityboxExecutionError('Dedicated computer provisioning could not be verified');
|
|
51
|
+
const sha = (s) => (0, crypto_1.createHash)('sha256').update(s).digest('hex');
|
|
52
|
+
const programHash = sha((0, entitybox_recovery_store_1.entityboxCanonical)(programs));
|
|
53
|
+
const labels = (plan, role) => ({
|
|
54
|
+
'ai.add.entitybox.provision.version': '1', 'ai.add.entitybox.provision.capacity': plan.capacityId,
|
|
55
|
+
'ai.add.entitybox.provision.desktop': plan.desktopId, 'ai.add.entitybox.provision.runtime': plan.runtimeId,
|
|
56
|
+
'ai.add.entitybox.provision.plan': sha((0, entitybox_recovery_store_1.entityboxCanonical)(plan)), 'ai.add.entitybox.provision.role': role,
|
|
57
|
+
});
|
|
58
|
+
exports.entityboxCapacityLabels = labels;
|
|
59
|
+
const match = (actual, expected) => !!actual && Object.entries(expected).every(([k, v]) => actual[k] === v);
|
|
60
|
+
/** Docker mutations have no observation timeout: a timed-out CLI is not proof
|
|
61
|
+
* that the daemon stopped creating/starting a resource. A hung operation keeps
|
|
62
|
+
* the durable reservation, rather than starting a replacement. */
|
|
63
|
+
function nativeDocker(context) {
|
|
64
|
+
return (args, input) => new Promise((resolve, reject) => {
|
|
65
|
+
const child = (0, child_process_1.spawn)('docker', ['--context', context, ...args], { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
66
|
+
let output = '', size = 0, invalid = false;
|
|
67
|
+
child.stdout.on('data', b => { size += b.length; if (size > 2 * 1024 * 1024)
|
|
68
|
+
invalid = true;
|
|
69
|
+
else
|
|
70
|
+
output += b.toString(); });
|
|
71
|
+
child.stderr.resume();
|
|
72
|
+
child.stdin.on('error', () => { invalid = true; });
|
|
73
|
+
child.once('error', () => reject(fail()));
|
|
74
|
+
child.once('close', code => code === 0 && !invalid ? resolve(output.trim()) : reject(fail()));
|
|
75
|
+
child.stdin.end(input);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function readJson(file) {
|
|
79
|
+
try {
|
|
80
|
+
const stat = fs.lstatSync(file);
|
|
81
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 32768)
|
|
82
|
+
throw fail();
|
|
83
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw fail();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Publish an immutable complete file. A partial write cannot look like a
|
|
90
|
+
* reusable reservation or receipt. An existing name never grants ownership. */
|
|
91
|
+
function publish(directory, name, value) {
|
|
92
|
+
const pending = path.join(directory, 'pending-' + (0, crypto_1.randomUUID)() + '.json');
|
|
93
|
+
let fd;
|
|
94
|
+
try {
|
|
95
|
+
fd = fs.openSync(pending, 'wx', 0o600);
|
|
96
|
+
fs.writeFileSync(fd, (0, entitybox_recovery_store_1.entityboxCanonical)(value));
|
|
97
|
+
fs.fsyncSync(fd);
|
|
98
|
+
fs.closeSync(fd);
|
|
99
|
+
fd = undefined;
|
|
100
|
+
fs.linkSync(pending, path.join(directory, name));
|
|
101
|
+
if (process.platform !== 'win32') {
|
|
102
|
+
const handle = fs.openSync(directory, 'r');
|
|
103
|
+
try {
|
|
104
|
+
fs.fsyncSync(handle);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
fs.closeSync(handle);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw fail();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
if (fd !== undefined)
|
|
116
|
+
fs.closeSync(fd);
|
|
117
|
+
try {
|
|
118
|
+
fs.unlinkSync(pending);
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function directoryFor(value) {
|
|
124
|
+
if (typeof value !== 'string' || !path.isAbsolute(value) || value.startsWith('\\\\'))
|
|
125
|
+
throw fail();
|
|
126
|
+
fs.mkdirSync(value, { recursive: true, mode: 0o700 });
|
|
127
|
+
if (fs.lstatSync(value).isSymbolicLink())
|
|
128
|
+
throw fail();
|
|
129
|
+
return fs.realpathSync(value);
|
|
130
|
+
}
|
|
131
|
+
function parsePlan(value) {
|
|
132
|
+
if (!value || Object.keys(value).sort().join(',') !== 'capacityId,desktopId,dockerContext,imageId,programsSha256,runtimeId,version' || value.version !== 1
|
|
133
|
+
|| ![value.capacityId, value.runtimeId, value.desktopId].every(v => uuid.test(v)) || !/^sha256:[a-f0-9]{64}$/.test(value.imageId)
|
|
134
|
+
|| !/^[-a-zA-Z0-9_.]{1,80}$/.test(value.dockerContext) || value.programsSha256 !== programHash)
|
|
135
|
+
throw fail();
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
function portOf(c) {
|
|
139
|
+
const ports = c?.NetworkSettings?.Ports, bindings = c?.HostConfig?.PortBindings, entries = ports?.['5900/tcp'], requested = bindings?.['5900/tcp'];
|
|
140
|
+
if (!ports || Object.entries(ports).some(([key, value]) => key !== '5900/tcp' && value != null) || !Array.isArray(entries) || entries.length !== 1 || entries[0].HostIp !== '127.0.0.1'
|
|
141
|
+
|| !bindings || Object.keys(bindings).length !== 1 || !Array.isArray(requested) || requested.length !== 1 || requested[0].HostIp !== '127.0.0.1')
|
|
142
|
+
throw fail();
|
|
143
|
+
const port = Number(entries[0].HostPort);
|
|
144
|
+
if (!Number.isSafeInteger(port) || port < 1024 || port > 65535)
|
|
145
|
+
throw fail();
|
|
146
|
+
return port;
|
|
147
|
+
}
|
|
148
|
+
/** NEW offline dedicated capacity only. This is deliberately unregistered in
|
|
149
|
+
* the shared desktop command runner/reconciler. The server must reserve a new
|
|
150
|
+
* native desktop identity before invoking it, then publish the verified VNC
|
|
151
|
+
* descriptor through its existing authorization path. */
|
|
152
|
+
class EntityboxCapacityProvisioner {
|
|
153
|
+
docker;
|
|
154
|
+
plan;
|
|
155
|
+
directory;
|
|
156
|
+
result;
|
|
157
|
+
constructor(config, docker, reserved) {
|
|
158
|
+
this.docker = docker;
|
|
159
|
+
this.plan = Object.freeze(reserved ?? { version: 1, capacityId: (0, crypto_1.randomUUID)(), runtimeId: config.runtimeId, desktopId: config.desktopId, imageId: config.imageId, dockerContext: config.dockerContext ?? 'desktop-linux', programsSha256: programHash });
|
|
160
|
+
this.directory = directoryFor(config.directory);
|
|
161
|
+
this.password = config.vncPassword;
|
|
162
|
+
}
|
|
163
|
+
password;
|
|
164
|
+
static create(config, docker) {
|
|
165
|
+
if (config.enabled !== true)
|
|
166
|
+
return undefined;
|
|
167
|
+
if (!uuid.test(config.runtimeId) || !uuid.test(config.desktopId) || !/^sha256:[a-f0-9]{64}$/.test(config.imageId)
|
|
168
|
+
|| !/^[-a-zA-Z0-9_.]{1,80}$/.test(config.dockerContext ?? 'desktop-linux') || typeof config.vncPassword !== 'string' || !/^[A-Za-z0-9_-]{8}$/.test(config.vncPassword))
|
|
169
|
+
throw fail();
|
|
170
|
+
return new EntityboxCapacityProvisioner(config, docker ?? nativeDocker(config.dockerContext ?? 'desktop-linux'));
|
|
171
|
+
}
|
|
172
|
+
/** Continue a server-claimed plan in its one physical helper. This is not
|
|
173
|
+
* resumption: run() still atomically reserves the desktop before ANY Docker
|
|
174
|
+
* operation and rejects an existing reservation, even for the same plan. */
|
|
175
|
+
static createReserved(config, plan, docker) {
|
|
176
|
+
if (config.enabled !== true)
|
|
177
|
+
return undefined;
|
|
178
|
+
const reserved = parsePlan(JSON.parse((0, entitybox_recovery_store_1.entityboxCanonical)(plan)));
|
|
179
|
+
if (reserved.runtimeId !== config.runtimeId || reserved.desktopId !== config.desktopId || reserved.imageId !== config.imageId || reserved.dockerContext !== (config.dockerContext ?? 'desktop-linux'))
|
|
180
|
+
throw fail();
|
|
181
|
+
// Reuse the normal configuration validation before constructing the helper.
|
|
182
|
+
if (!EntityboxCapacityProvisioner.create(config, docker))
|
|
183
|
+
throw fail();
|
|
184
|
+
return new EntityboxCapacityProvisioner(config, docker ?? nativeDocker(reserved.dockerContext), reserved);
|
|
185
|
+
}
|
|
186
|
+
/** Calls on this one owner join. A new object/process cannot repeat the
|
|
187
|
+
* operation: the desktop reservation remains, even after an ambiguous error. */
|
|
188
|
+
provision() {
|
|
189
|
+
this.result ??= this.run().catch(() => { throw fail(); }).finally(() => { this.password = ''; });
|
|
190
|
+
return this.result;
|
|
191
|
+
}
|
|
192
|
+
async inspect(id, role) {
|
|
193
|
+
const result = JSON.parse(await this.docker(['container', 'inspect', id]));
|
|
194
|
+
const c = result?.[0];
|
|
195
|
+
if (!Array.isArray(result) || result.length !== 1 || !/^[a-f0-9]{64}$/.test(id) || c?.Id !== id || c.Image !== this.plan.imageId || !match(c.Config?.Labels, labels(this.plan, role)))
|
|
196
|
+
throw fail();
|
|
197
|
+
return c;
|
|
198
|
+
}
|
|
199
|
+
async until(test, milliseconds = 30000) {
|
|
200
|
+
const end = Date.now() + milliseconds;
|
|
201
|
+
do {
|
|
202
|
+
if (await test())
|
|
203
|
+
return;
|
|
204
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
205
|
+
} while (Date.now() < end);
|
|
206
|
+
throw fail();
|
|
207
|
+
}
|
|
208
|
+
async run() {
|
|
209
|
+
const p = this.plan, planSha256 = sha((0, entitybox_recovery_store_1.entityboxCanonical)(p)), base = { capacityId: p.capacityId, desktopId: p.desktopId, runtimeId: p.runtimeId }, volumes = (0, entitybox_application_boundary_1.entityboxApplicationVolumes)(base);
|
|
210
|
+
// The reservation precedes every external operation, including discovery.
|
|
211
|
+
publish(this.directory, 'desktop-' + p.desktopId + '.json', p);
|
|
212
|
+
const file = (suffix) => 'capacity-' + p.capacityId + '-' + suffix + '.json';
|
|
213
|
+
const names = { init: 'entitybox-capacity-init-' + p.capacityId, display: 'entitybox-capacity-display-' + p.capacityId, application: 'entitybox-capacity-application-' + p.capacityId };
|
|
214
|
+
const knownContainers = (await this.docker(['container', 'ls', '--all', '--format', '{{.Names}}'])).split(/\r?\n/);
|
|
215
|
+
const knownVolumes = (await this.docker(['volume', 'ls', '--format', '{{.Name}}'])).split(/\r?\n/);
|
|
216
|
+
if (Object.values(names).some(n => knownContainers.includes(n)) || Object.values(volumes).some(n => knownVolumes.includes(n)))
|
|
217
|
+
throw fail();
|
|
218
|
+
const argsFor = (value) => Object.entries(value).flatMap(([k, v]) => ['--label', k + '=' + v]);
|
|
219
|
+
const mount = (src, dst, readonly = false) => ['--mount', `type=volume,src=${src},dst=${dst},volume-nocopy${readonly ? ',readonly' : ''}`];
|
|
220
|
+
const applicationLabels = (0, entitybox_application_boundary_1.entityboxApplicationLabels)(base), displayLabels = { 'ai.add.entitybox.display.version': '1', 'ai.add.entitybox.display.capacity': p.capacityId, 'ai.add.entitybox.display.desktop': p.desktopId, 'ai.add.entitybox.display.runtime': p.runtimeId };
|
|
221
|
+
for (const [kind, name] of Object.entries(volumes)) {
|
|
222
|
+
const expected = { ...labels(p, 'volume-' + kind), ...(kind === 'socket' ? displayLabels : applicationLabels) };
|
|
223
|
+
if (await this.docker(['volume', 'create', '--driver', 'local', ...argsFor(expected), name]) !== name)
|
|
224
|
+
throw fail();
|
|
225
|
+
const rows = JSON.parse(await this.docker(['volume', 'inspect', name]));
|
|
226
|
+
if (!Array.isArray(rows) || rows.length !== 1 || rows[0].Name !== name || rows[0].Driver !== 'local' || Object.keys(rows[0].Options ?? {}).length || !match(rows[0].Labels, expected))
|
|
227
|
+
throw fail();
|
|
228
|
+
publish(this.directory, file('volume-' + kind), { name, labels: expected });
|
|
229
|
+
}
|
|
230
|
+
const limits = ['--init', '--restart', 'no', '--ipc', 'private', '--memory', '1024m', '--memory-swap', '1024m', '--cpus', '1', '--pids-limit', '256', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges', '--read-only'];
|
|
231
|
+
const temp = ['--tmpfs', '/tmp:rw,nosuid,nodev,size=128m,mode=1777', '--tmpfs', '/run:rw,nosuid,nodev,size=8m,mode=700,uid=10001,gid=10001'];
|
|
232
|
+
const create = async (role, extra, args, code) => {
|
|
233
|
+
const id = await this.docker(['create', '--name', names[role], ...argsFor({ ...labels(p, role), ...extra }), ...limits, ...args, '--entrypoint', 'node', p.imageId, '-e', code]);
|
|
234
|
+
const c = await this.inspect(id, role);
|
|
235
|
+
if (c.State?.Status !== 'created' || c.State.Running || c.State.Pid !== 0)
|
|
236
|
+
throw fail();
|
|
237
|
+
publish(this.directory, file(role), { id, name: names[role], imageId: p.imageId, labels: labels(p, role) });
|
|
238
|
+
return id;
|
|
239
|
+
};
|
|
240
|
+
const init = await create('init', {}, ['--network', 'none', '--user', '0:0', '--cap-add', 'CHOWN', '--cap-add', 'FOWNER', ...mount(volumes.socket, '/socket'), ...mount(volumes.profile, '/profile'), ...mount(volumes.work, '/work')], programs.entityboxCapacityInit);
|
|
241
|
+
await this.docker(['start', init]);
|
|
242
|
+
await this.until(async () => { const c = await this.inspect(init, 'init'); if (c.State.Running)
|
|
243
|
+
return false; if (c.State.Status !== 'exited' || c.State.Pid !== 0 || c.State.ExitCode !== 0)
|
|
244
|
+
throw fail(); return true; });
|
|
245
|
+
// Never use --force and never remove data. Only the proven stopped bootstrap
|
|
246
|
+
// helper created by this exact plan may be retired here.
|
|
247
|
+
await this.docker(['container', 'rm', init]);
|
|
248
|
+
const display = await create('display', displayLabels, ['--network', 'bridge', '--user', '10001:10001', ...temp, '--publish', '127.0.0.1::5900', ...mount(volumes.socket, '/tmp/.X11-unix')], programs.entityboxCapacityDisplay);
|
|
249
|
+
await this.docker(['start', display]);
|
|
250
|
+
await this.inspect(display, 'display');
|
|
251
|
+
await this.docker(['exec', '--interactive', '--user', '10001:10001', display, 'node', '-e', programs.entityboxCapacityPassword], this.password);
|
|
252
|
+
const application = await create('application', applicationLabels, ['--network', 'none', '--user', '10001:10001', ...temp, ...mount(volumes.socket, '/tmp/.X11-unix', true), ...mount(volumes.profile, '/home/entitybox'), ...mount(volumes.work, '/work')], programs.entityboxCapacityApplication);
|
|
253
|
+
// Xvfb must be listening before Chromium starts. The socket is exposed only
|
|
254
|
+
// to the new application, never a host desktop or an arbitrary bind mount.
|
|
255
|
+
await this.until(async () => { await this.inspect(display, 'display'); try {
|
|
256
|
+
return await this.docker(['exec', '--user', '10001:10001', '--env', 'DISPLAY=:1', display, 'xdpyinfo']) !== '';
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return false;
|
|
260
|
+
} });
|
|
261
|
+
await this.docker(['start', application]);
|
|
262
|
+
await this.until(async () => { const a = await this.inspect(application, 'application'), d = await this.inspect(display, 'display'); if (!a.State.Running || !d.State.Running)
|
|
263
|
+
throw fail(); try {
|
|
264
|
+
return await this.docker(['exec', '--user', '10001:10001', display, 'node', '-e', programs.entityboxCapacityReady]) === 'ready';
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
} });
|
|
269
|
+
await this.docker(['container', 'pause', application]);
|
|
270
|
+
const a = await this.inspect(application, 'application'), d = await this.inspect(display, 'display');
|
|
271
|
+
const receipt = { version: 1, dockerContext: p.dockerContext, planSha256, vncPort: portOf(d), channels: ['screen'], applicationNetwork: 'none',
|
|
272
|
+
application: { ...base, containerId: application, imageId: p.imageId, startedAt: a.State.StartedAt }, display: { ...base, containerId: display, imageId: p.imageId, startedAt: d.State.StartedAt } };
|
|
273
|
+
await verifyEntityboxCapacityReceipt(p, receipt, this.docker);
|
|
274
|
+
publish(this.directory, file('ready'), receipt);
|
|
275
|
+
return Object.freeze({ ...receipt, application: Object.freeze(receipt.application), display: Object.freeze(receipt.display), channels: Object.freeze(receipt.channels) });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
exports.EntityboxCapacityProvisioner = EntityboxCapacityProvisioner;
|
|
279
|
+
async function verifyEntityboxCapacityReceipt(plan, r, docker, requirePaused = true) {
|
|
280
|
+
parsePlan(plan);
|
|
281
|
+
if (!r || r.version !== 1 || r.planSha256 !== sha((0, entitybox_recovery_store_1.entityboxCanonical)(plan)) || r.dockerContext !== plan.dockerContext || r.applicationNetwork !== 'none'
|
|
282
|
+
|| JSON.stringify(r.channels) !== '["screen"]' || r.application?.containerId === r.display?.containerId)
|
|
283
|
+
throw fail();
|
|
284
|
+
const scope = { version: 1, runId: plan.capacityId, desktopId: plan.desktopId, runtimeId: plan.runtimeId };
|
|
285
|
+
for (const [role, b] of [['application', r.application], ['display', r.display]]) {
|
|
286
|
+
if (!b || b.capacityId !== plan.capacityId || b.desktopId !== plan.desktopId || b.runtimeId !== plan.runtimeId || b.imageId !== plan.imageId)
|
|
287
|
+
throw fail();
|
|
288
|
+
const rows = JSON.parse(await docker(['container', 'inspect', b.containerId]));
|
|
289
|
+
if (!Array.isArray(rows) || rows.length !== 1 || !match(rows[0]?.Config?.Labels, labels(plan, role)))
|
|
290
|
+
throw fail();
|
|
291
|
+
if (role === 'display' && (portOf(rows[0]) !== r.vncPort || rows[0].State?.Paused || rows[0].State?.Restarting || rows[0].HostConfig?.RestartPolicy?.Name !== 'no'))
|
|
292
|
+
throw fail();
|
|
293
|
+
}
|
|
294
|
+
const state = await (0, entitybox_application_boundary_1.inspectEntityboxApplication)(r.application, scope, docker);
|
|
295
|
+
if (state === 'stopped' || requirePaused && state !== 'paused')
|
|
296
|
+
throw fail();
|
|
297
|
+
await (0, entitybox_display_binding_1.verifyEntityboxDisplay)(r.display, scope, docker);
|
|
298
|
+
return state;
|
|
299
|
+
}
|
|
300
|
+
/** Read-only reopening after a clean setup; never resumes, creates, repairs or
|
|
301
|
+
* deletes anything. Missing/ambiguous setup remains reserved for explicit
|
|
302
|
+
* recovery. A restarted container or changed published port is not adopted. */
|
|
303
|
+
async function readEntityboxCapacity(directory, desktopId, runtimeId, docker) {
|
|
304
|
+
if (!uuid.test(desktopId) || !uuid.test(runtimeId) || !path.isAbsolute(directory) || directory.startsWith('\\\\'))
|
|
305
|
+
throw fail();
|
|
306
|
+
try {
|
|
307
|
+
if (fs.lstatSync(directory).isSymbolicLink())
|
|
308
|
+
throw fail();
|
|
309
|
+
const dir = fs.realpathSync(directory), p = parsePlan(readJson(path.join(dir, 'desktop-' + desktopId + '.json')));
|
|
310
|
+
if (p.desktopId !== desktopId || p.runtimeId !== runtimeId)
|
|
311
|
+
throw fail();
|
|
312
|
+
const r = readJson(path.join(dir, 'capacity-' + p.capacityId + '-ready.json'));
|
|
313
|
+
await verifyEntityboxCapacityReceipt(p, r, docker ?? nativeDocker(p.dockerContext));
|
|
314
|
+
return Object.freeze({ ...r, application: Object.freeze(r.application), display: Object.freeze(r.display), channels: Object.freeze(r.channels) });
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
throw fail();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** A health observation is never execution, ownership, stop proof or permission
|
|
321
|
+
* to replace a computer. Every physical read is limited to this published
|
|
322
|
+
* receipt; unavailable includes missing evidence and inconclusive inspection.
|
|
323
|
+
* The existing task reader continues to require a paused application. */
|
|
324
|
+
async function observeEntityboxCapacity(config, docker) {
|
|
325
|
+
if (config.enabled !== true)
|
|
326
|
+
return undefined;
|
|
327
|
+
try {
|
|
328
|
+
// Snapshot all input before the first await; late caller changes cannot
|
|
329
|
+
// redirect the remaining reads to another computer or Docker context.
|
|
330
|
+
const { directory, desktopId, runtimeId, dockerContext } = config;
|
|
331
|
+
if (!uuid.test(desktopId) || !uuid.test(runtimeId) || !path.isAbsolute(directory) || directory.startsWith('\\\\') || !/^[-a-zA-Z0-9_.]{1,80}$/.test(dockerContext))
|
|
332
|
+
throw fail();
|
|
333
|
+
const published = JSON.parse((0, entitybox_recovery_store_1.entityboxCanonical)(config.receipt));
|
|
334
|
+
if (fs.lstatSync(directory).isSymbolicLink())
|
|
335
|
+
throw fail();
|
|
336
|
+
const dir = fs.realpathSync(directory), plan = parsePlan(readJson(path.join(dir, 'desktop-' + desktopId + '.json')));
|
|
337
|
+
const receipt = readJson(path.join(dir, 'capacity-' + plan.capacityId + '-ready.json'));
|
|
338
|
+
if (plan.desktopId !== desktopId || plan.runtimeId !== runtimeId || plan.dockerContext !== dockerContext || published?.dockerContext !== dockerContext
|
|
339
|
+
|| (0, entitybox_recovery_store_1.entityboxCanonical)(receipt) !== (0, entitybox_recovery_store_1.entityboxCanonical)(published))
|
|
340
|
+
throw fail();
|
|
341
|
+
// Structural validation precedes any command, including a custom fixture
|
|
342
|
+
// transport. No name lookup or ordinary desktop discovery is permitted.
|
|
343
|
+
if (Object.keys(receipt).sort().join(',') !== 'application,applicationNetwork,channels,display,dockerContext,planSha256,version,vncPort'
|
|
344
|
+
|| !Number.isSafeInteger(receipt.vncPort) || receipt.vncPort < 1024 || receipt.vncPort > 65535)
|
|
345
|
+
throw fail();
|
|
346
|
+
for (const b of [receipt.application, receipt.display]) {
|
|
347
|
+
if (!b || Object.keys(b).sort().join(',') !== 'capacityId,containerId,desktopId,imageId,runtimeId,startedAt'
|
|
348
|
+
|| b.capacityId !== plan.capacityId || b.desktopId !== desktopId || b.runtimeId !== runtimeId || b.imageId !== plan.imageId
|
|
349
|
+
|| !/^[a-f0-9]{64}$/.test(b.containerId) || typeof b.startedAt !== 'string' || b.startedAt.length > 80 || !Number.isFinite(Date.parse(b.startedAt)))
|
|
350
|
+
throw fail();
|
|
351
|
+
}
|
|
352
|
+
const allowedContainers = new Set([receipt.application.containerId, receipt.display.containerId]);
|
|
353
|
+
const allowedVolumes = new Set(Object.values((0, entitybox_application_boundary_1.entityboxApplicationVolumes)(receipt.application)));
|
|
354
|
+
const deadline = perf_hooks_1.performance.now() + 20000;
|
|
355
|
+
const transport = docker ?? entityboxCapacityHealthDocker(dockerContext);
|
|
356
|
+
const read = async (args, input) => {
|
|
357
|
+
if (input !== undefined || perf_hooks_1.performance.now() >= deadline || args[1] !== 'inspect'
|
|
358
|
+
|| !(args[0] === 'container' && args.length === 3 && allowedContainers.has(args[2])
|
|
359
|
+
|| args[0] === 'volume' && args.length >= 3 && args.length <= 5 && args.slice(2).every(v => allowedVolumes.has(v))))
|
|
360
|
+
throw fail();
|
|
361
|
+
const result = await transport([...args]);
|
|
362
|
+
if (perf_hooks_1.performance.now() >= deadline)
|
|
363
|
+
throw fail();
|
|
364
|
+
return result;
|
|
365
|
+
};
|
|
366
|
+
// Cache one complete observation: ownership, ports, labels and state must
|
|
367
|
+
// be assessed from the same container record, not different CLI responses.
|
|
368
|
+
const reads = new Map();
|
|
369
|
+
const snapshot = args => { const key = JSON.stringify(args); let value = reads.get(key); if (!value) {
|
|
370
|
+
value = read(args);
|
|
371
|
+
reads.set(key, value);
|
|
372
|
+
} return value; };
|
|
373
|
+
const state = await verifyEntityboxCapacityReceipt(plan, receipt, snapshot, false);
|
|
374
|
+
return Object.freeze({ state });
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return Object.freeze({ state: 'unavailable' });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** A deadline can cancel an inspect CLI because it has no mutation to join.
|
|
381
|
+
* Always await its actual exit. This transport is never used by provisioning,
|
|
382
|
+
* task execution or cleanup, whose mutation lifetimes retain their rules. */
|
|
383
|
+
function entityboxCapacityHealthDocker(context) {
|
|
384
|
+
return args => new Promise((resolve, reject) => {
|
|
385
|
+
const child = (0, child_process_1.spawn)('docker', ['--context', context, ...args], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
386
|
+
let output = '', size = 0, invalid = false;
|
|
387
|
+
const stop = () => { invalid = true; try {
|
|
388
|
+
child.kill();
|
|
389
|
+
}
|
|
390
|
+
catch { } };
|
|
391
|
+
const timer = setTimeout(stop, 5000);
|
|
392
|
+
timer.unref?.();
|
|
393
|
+
child.stdout.on('data', b => { size += b.length; if (size > 2 * 1024 * 1024)
|
|
394
|
+
stop();
|
|
395
|
+
else
|
|
396
|
+
output += b.toString(); });
|
|
397
|
+
child.stderr.resume();
|
|
398
|
+
child.once('error', () => { invalid = true; });
|
|
399
|
+
child.once('close', code => { clearTimeout(timer); if (code === 0 && !invalid)
|
|
400
|
+
resolve(output.trim());
|
|
401
|
+
else
|
|
402
|
+
reject(fail()); });
|
|
403
|
+
});
|
|
404
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type EntityboxCapacityPlan, type EntityboxCapacityReceipt } from './entitybox-capacity-provisioner';
|
|
2
|
+
import type { EntityboxCapacityAllocation, EntityboxCapacityRegistrationConfiguration, EntityboxCapacityRegistrationIO } from './entitybox-capacity-registration';
|
|
3
|
+
interface Publication {
|
|
4
|
+
version: 1;
|
|
5
|
+
allocation: EntityboxCapacityAllocation;
|
|
6
|
+
plan: EntityboxCapacityPlan;
|
|
7
|
+
nonce: string;
|
|
8
|
+
password: string;
|
|
9
|
+
}
|
|
10
|
+
/** The original publication authority is encrypted under the existing pairing
|
|
11
|
+
* secret. It is not a public capacity receipt and is never an execution grant.
|
|
12
|
+
* No pairing token or plaintext VNC password is written into this directory. */
|
|
13
|
+
export declare class EntityboxCapacityPublicationStore {
|
|
14
|
+
private readonly directory;
|
|
15
|
+
constructor(directory: string, create?: boolean);
|
|
16
|
+
private file;
|
|
17
|
+
private key;
|
|
18
|
+
private aad;
|
|
19
|
+
read(id: string, runtimeId: string, token: string): Publication;
|
|
20
|
+
write(value: Publication, token: string): void;
|
|
21
|
+
}
|
|
22
|
+
/** Complete only a physically finished setup. This never invokes a provisioner
|
|
23
|
+
* or mutates Docker. Concurrent fresh processes publish the SAME original
|
|
24
|
+
* nonce, plan, password and receipt through the existing idempotent SQL path.
|
|
25
|
+
* A missing ready receipt remains unresolved; it is not permission to recreate. */
|
|
26
|
+
export declare function createEntityboxCapacityPublicationRecovery(config: EntityboxCapacityRegistrationConfiguration, io: EntityboxCapacityRegistrationIO): {
|
|
27
|
+
provision: () => Promise<Readonly<EntityboxCapacityReceipt>>;
|
|
28
|
+
} | undefined;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.EntityboxCapacityPublicationStore = void 0;
|
|
37
|
+
exports.createEntityboxCapacityPublicationRecovery = createEntityboxCapacityPublicationRecovery;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const crypto_1 = require("crypto");
|
|
41
|
+
const entitybox_recovery_store_1 = require("./entitybox-recovery-store");
|
|
42
|
+
const entitybox_capacity_provisioner_1 = require("./entitybox-capacity-provisioner");
|
|
43
|
+
const entitybox_execution_lease_1 = require("./entitybox-execution-lease");
|
|
44
|
+
const uuid = /^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/, fail = () => new entitybox_execution_lease_1.EntityboxExecutionError('Computer setup publication could not be recovered');
|
|
45
|
+
const sha = (s) => (0, crypto_1.createHash)('sha256').update(s).digest('hex');
|
|
46
|
+
function validate(value) {
|
|
47
|
+
const a = value?.allocation, p = value?.plan;
|
|
48
|
+
if (!value || Object.keys(value).sort().join(',') !== 'allocation,nonce,password,plan,version' || value.version !== 1
|
|
49
|
+
|| !a || Object.keys(a).sort().join(',') !== 'desktopId,id,runtimeId' || ![a.id, a.desktopId, a.runtimeId, value.nonce].every(v => typeof v === 'string' && uuid.test(v))
|
|
50
|
+
|| !p || Object.keys(p).sort().join(',') !== 'capacityId,desktopId,dockerContext,imageId,programsSha256,runtimeId,version' || p.version !== 1 || !uuid.test(p.capacityId)
|
|
51
|
+
|| p.runtimeId !== a.runtimeId || p.desktopId !== a.desktopId || !/^sha256:[a-f0-9]{64}$/.test(p.imageId) || !/^[-a-zA-Z0-9_.]{1,80}$/.test(p.dockerContext) || !/^[a-f0-9]{64}$/.test(p.programsSha256)
|
|
52
|
+
|| typeof value.password !== 'string' || !/^[A-Za-z0-9_-]{8}$/.test(value.password))
|
|
53
|
+
throw fail();
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
/** The original publication authority is encrypted under the existing pairing
|
|
57
|
+
* secret. It is not a public capacity receipt and is never an execution grant.
|
|
58
|
+
* No pairing token or plaintext VNC password is written into this directory. */
|
|
59
|
+
class EntityboxCapacityPublicationStore {
|
|
60
|
+
directory;
|
|
61
|
+
constructor(directory, create = false) {
|
|
62
|
+
if (!path.isAbsolute(directory) || directory.startsWith('\\\\'))
|
|
63
|
+
throw fail();
|
|
64
|
+
if (create)
|
|
65
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
66
|
+
if (fs.lstatSync(directory).isSymbolicLink())
|
|
67
|
+
throw fail();
|
|
68
|
+
const root = fs.realpathSync(directory), target = path.join(root, 'publication-private');
|
|
69
|
+
if (create)
|
|
70
|
+
fs.mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
71
|
+
if (fs.lstatSync(target).isSymbolicLink() || !fs.lstatSync(target).isDirectory())
|
|
72
|
+
throw fail();
|
|
73
|
+
this.directory = fs.realpathSync(target);
|
|
74
|
+
}
|
|
75
|
+
file(id) { if (!uuid.test(id))
|
|
76
|
+
throw fail(); return path.join(this.directory, id + '.sealed'); }
|
|
77
|
+
key(token, runtimeId) { if (typeof token !== 'string' || !token || token.length > 16384 || !uuid.test(runtimeId))
|
|
78
|
+
throw fail(); return (0, crypto_1.createHmac)('sha256', token).update('entitybox-capacity-publication-v1\0' + runtimeId).digest(); }
|
|
79
|
+
aad(id, runtimeId) { return Buffer.from((0, entitybox_recovery_store_1.entityboxCanonical)({ version: 1, id, runtimeId })); }
|
|
80
|
+
read(id, runtimeId, token) {
|
|
81
|
+
try {
|
|
82
|
+
const file = this.file(id), stat = fs.lstatSync(file);
|
|
83
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16384)
|
|
84
|
+
throw fail();
|
|
85
|
+
const envelope = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
86
|
+
if (!envelope || Object.keys(envelope).sort().join(',') !== 'body,iv,tag,version' || envelope.version !== 1)
|
|
87
|
+
throw fail();
|
|
88
|
+
const decode = (value) => { if (typeof value !== 'string' || !value)
|
|
89
|
+
throw fail(); const b = Buffer.from(value, 'base64'); if (b.toString('base64') !== value)
|
|
90
|
+
throw fail(); return b; };
|
|
91
|
+
const iv = decode(envelope.iv), tag = decode(envelope.tag), body = decode(envelope.body);
|
|
92
|
+
if (iv.length !== 12 || tag.length !== 16 || body.length > 8192)
|
|
93
|
+
throw fail();
|
|
94
|
+
const decipher = (0, crypto_1.createDecipheriv)('aes-256-gcm', this.key(token, runtimeId), iv);
|
|
95
|
+
decipher.setAAD(this.aad(id, runtimeId));
|
|
96
|
+
decipher.setAuthTag(tag);
|
|
97
|
+
const value = validate(JSON.parse(Buffer.concat([decipher.update(body), decipher.final()]).toString('utf8')));
|
|
98
|
+
if (value.allocation.id !== id || value.allocation.runtimeId !== runtimeId)
|
|
99
|
+
throw fail();
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw fail();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
write(value, token) {
|
|
107
|
+
validate(value);
|
|
108
|
+
const { id, runtimeId } = value.allocation, file = this.file(id), iv = (0, crypto_1.randomBytes)(12), cipher = (0, crypto_1.createCipheriv)('aes-256-gcm', this.key(token, runtimeId), iv);
|
|
109
|
+
cipher.setAAD(this.aad(id, runtimeId));
|
|
110
|
+
const body = Buffer.concat([cipher.update((0, entitybox_recovery_store_1.entityboxCanonical)(value), 'utf8'), cipher.final()]);
|
|
111
|
+
const text = (0, entitybox_recovery_store_1.entityboxCanonical)({ version: 1, iv: iv.toString('base64'), tag: cipher.getAuthTag().toString('base64'), body: body.toString('base64') }), pending = path.join(this.directory, 'pending-' + (0, crypto_1.randomUUID)() + '.sealed');
|
|
112
|
+
let fd;
|
|
113
|
+
try {
|
|
114
|
+
fd = fs.openSync(pending, 'wx', 0o600);
|
|
115
|
+
fs.writeFileSync(fd, text);
|
|
116
|
+
fs.fsyncSync(fd);
|
|
117
|
+
fs.closeSync(fd);
|
|
118
|
+
fd = undefined;
|
|
119
|
+
try {
|
|
120
|
+
fs.linkSync(pending, file);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error.code !== 'EEXIST' || (0, entitybox_recovery_store_1.entityboxCanonical)(this.read(id, runtimeId, token)) !== (0, entitybox_recovery_store_1.entityboxCanonical)(value))
|
|
124
|
+
throw fail();
|
|
125
|
+
}
|
|
126
|
+
if (process.platform !== 'win32') {
|
|
127
|
+
const dir = fs.openSync(this.directory, 'r');
|
|
128
|
+
try {
|
|
129
|
+
fs.fsyncSync(dir);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
fs.closeSync(dir);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
throw fail();
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
if (fd !== undefined)
|
|
141
|
+
fs.closeSync(fd);
|
|
142
|
+
try {
|
|
143
|
+
fs.unlinkSync(pending);
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.EntityboxCapacityPublicationStore = EntityboxCapacityPublicationStore;
|
|
150
|
+
/** Complete only a physically finished setup. This never invokes a provisioner
|
|
151
|
+
* or mutates Docker. Concurrent fresh processes publish the SAME original
|
|
152
|
+
* nonce, plan, password and receipt through the existing idempotent SQL path.
|
|
153
|
+
* A missing ready receipt remains unresolved; it is not permission to recreate. */
|
|
154
|
+
function createEntityboxCapacityPublicationRecovery(config, io) {
|
|
155
|
+
if (config.enabled !== true || config.recoverPublication !== true)
|
|
156
|
+
return undefined;
|
|
157
|
+
const allocation = Object.freeze({ id: config.allocation?.id, runtimeId: config.allocation?.runtimeId, desktopId: config.allocation?.desktopId }), directory = config.directory, imageId = config.imageId, context = config.dockerContext ?? 'desktop-linux';
|
|
158
|
+
if (![allocation.id, allocation.runtimeId, allocation.desktopId].every(v => typeof v === 'string' && uuid.test(v)) || !/^sha256:[a-f0-9]{64}$/.test(imageId))
|
|
159
|
+
throw fail();
|
|
160
|
+
let flight;
|
|
161
|
+
async function run() {
|
|
162
|
+
const pairing = io.pairing();
|
|
163
|
+
if (!pairing || pairing.runtimeId !== allocation.runtimeId || !pairing.daemonToken)
|
|
164
|
+
throw fail();
|
|
165
|
+
const originalToken = pairing.daemonToken;
|
|
166
|
+
const token = () => { const p = io.pairing(); if (!p || p.runtimeId !== allocation.runtimeId || p.daemonToken !== originalToken)
|
|
167
|
+
throw fail(); return originalToken; };
|
|
168
|
+
const state = new EntityboxCapacityPublicationStore(directory).read(allocation.id, allocation.runtimeId, token());
|
|
169
|
+
if ((0, entitybox_recovery_store_1.entityboxCanonical)(state.allocation) !== (0, entitybox_recovery_store_1.entityboxCanonical)(allocation) || state.plan.imageId !== imageId || state.plan.dockerContext !== context)
|
|
170
|
+
throw fail();
|
|
171
|
+
const read = () => (io.read ?? entitybox_capacity_provisioner_1.readEntityboxCapacity)(directory, allocation.desktopId, allocation.runtimeId);
|
|
172
|
+
const receipt = await read();
|
|
173
|
+
token();
|
|
174
|
+
if (receipt.planSha256 !== sha((0, entitybox_recovery_store_1.entityboxCanonical)(state.plan)) || receipt.dockerContext !== context)
|
|
175
|
+
throw fail();
|
|
176
|
+
const validateReply = (r, phase) => {
|
|
177
|
+
if (!r || r.id !== allocation.id || r.runtimeId !== allocation.runtimeId || r.desktopId !== allocation.desktopId || r.phase !== phase || (0, entitybox_recovery_store_1.entityboxCanonical)(r.plan) !== (0, entitybox_recovery_store_1.entityboxCanonical)(state.plan)
|
|
178
|
+
|| phase === 'ready' && (0, entitybox_recovery_store_1.entityboxCanonical)(r.receipt) !== (0, entitybox_recovery_store_1.entityboxCanonical)(receipt))
|
|
179
|
+
throw fail();
|
|
180
|
+
};
|
|
181
|
+
const claimed = await io.rpc('entitybox_native_capacity_claim', { p_token: token(), p_id: allocation.id, p_nonce: state.nonce, p_plan: state.plan });
|
|
182
|
+
token();
|
|
183
|
+
if (claimed?.phase === 'ready') {
|
|
184
|
+
validateReply(claimed, 'ready');
|
|
185
|
+
return receipt;
|
|
186
|
+
}
|
|
187
|
+
validateReply(claimed, 'provisioning');
|
|
188
|
+
const current = await read();
|
|
189
|
+
token();
|
|
190
|
+
if ((0, entitybox_recovery_store_1.entityboxCanonical)(current) !== (0, entitybox_recovery_store_1.entityboxCanonical)(receipt))
|
|
191
|
+
throw fail();
|
|
192
|
+
const published = await io.rpc('entitybox_native_capacity_publish', { p_token: token(), p_id: allocation.id, p_nonce: state.nonce, p_receipt: receipt, p_vnc_password: state.password });
|
|
193
|
+
token();
|
|
194
|
+
validateReply(published, 'ready');
|
|
195
|
+
return receipt;
|
|
196
|
+
}
|
|
197
|
+
return { provision: () => { if (!flight)
|
|
198
|
+
flight = run().catch(() => { throw fail(); }).finally(() => { flight = undefined; }); return flight; } };
|
|
199
|
+
}
|