@yeaft/webchat-agent 1.0.385 → 1.0.387

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.
@@ -1,66 +0,0 @@
1
- import { sandboxDb } from './database.js';
2
- import { agents } from './context.js';
3
-
4
- function readClaims(message) {
5
- const claims = message?.sandboxClaims;
6
- if (!claims || typeof claims !== 'object') return null;
7
- if (typeof claims.sandboxId !== 'string' || typeof claims.instanceId !== 'string'
8
- || !Number.isInteger(claims.generation) || typeof claims.imageDigest !== 'string') {
9
- return null;
10
- }
11
- return claims;
12
- }
13
-
14
- /**
15
- * Authenticate a managed Sandbox Agent without accepting the user's general
16
- * Agent secret. URL identity and credential scope must describe the same
17
- * immutable Sandbox instance.
18
- */
19
- export function authenticateSandboxAgent(message, pending, store = sandboxDb) {
20
- if (message?.authKind !== 'sandbox' || typeof message.credentialId !== 'string'
21
- || typeof message.secret !== 'string') {
22
- return null;
23
- }
24
- const claims = readClaims(message);
25
- if (!claims || pending.instanceId !== claims.instanceId || pending.agentId !== claims.sandboxId) {
26
- return null;
27
- }
28
- try {
29
- const authenticated = store.authenticateCredential(message.credentialId, message.secret, claims);
30
- if (authenticated.sandboxId !== claims.sandboxId) return null;
31
- return {
32
- ...authenticated,
33
- instanceId: claims.instanceId,
34
- generation: claims.generation,
35
- imageDigest: claims.imageDigest,
36
- sessionKey: message.secret
37
- };
38
- } catch {
39
- return null;
40
- }
41
- }
42
-
43
- /**
44
- * Resolve readiness from the authenticated managed Agent connection. Controller
45
- * booleans are inspection evidence, but cannot prove that the scoped Agent has
46
- * completed its end-to-end Server sync.
47
- */
48
- export function canForceReadyAfterSyncTimeout(agent) {
49
- return !agent?.sandboxIdentity;
50
- }
51
-
52
- export function isSandboxAgentReady(identity, connectedAgents = agents) {
53
- if (!identity) return false;
54
- for (const agent of connectedAgents.values()) {
55
- const current = agent.sandboxIdentity;
56
- if (agent.status === 'ready' && agent.isAlive && current
57
- && current.sandboxId === identity.sandboxId
58
- && agent.instanceId === identity.instanceId
59
- && current.generation === identity.generation
60
- && current.imageDigest === identity.imageDigest
61
- && agent.capabilities?.includes('managed-sandbox')) {
62
- return true;
63
- }
64
- }
65
- return false;
66
- }
@@ -1,128 +0,0 @@
1
- import express from 'express';
2
- import { createServer } from 'https';
3
- import { CONFIG } from './config.js';
4
- import {
5
- authenticateSandboxController,
6
- handleSandboxHostAttestation
7
- } from './routes/sandbox-routes.js';
8
-
9
- export const SANDBOX_HOST_ATTESTATION_PATH = '/api/sandbox/hosts/attest';
10
-
11
- function authenticatePeer(req, res, next, config) {
12
- if (!authenticateSandboxController(req, config.controllerAttestationFingerprint)) {
13
- return res.status(401).json({ code: 'SANDBOX_CONTROLLER_IDENTITY_REJECTED' });
14
- }
15
- return next();
16
- }
17
-
18
- function parseStrictJson(req, res, next) {
19
- try {
20
- const body = JSON.parse(req.body.toString('utf8'));
21
- if (!body || typeof body !== 'object') {
22
- return res.status(400).json({ code: 'SANDBOX_ATTESTATION_BODY_INVALID' });
23
- }
24
- req.body = body;
25
- return next();
26
- } catch {
27
- return res.status(400).json({ code: 'SANDBOX_ATTESTATION_BODY_INVALID' });
28
- }
29
- }
30
-
31
- function bodyErrorHandler(err, req, res, next) {
32
- if (!err) return next();
33
- if (err.type === 'entity.too.large') {
34
- return res.status(413).json({ code: 'SANDBOX_ATTESTATION_BODY_TOO_LARGE' });
35
- }
36
- return next(err);
37
- }
38
-
39
- /**
40
- * Create the dedicated HTTPS server used only for Controller Host attestation.
41
- * TLS chain authorization occurs before Express, while the route also pins the
42
- * leaf certificate fingerprint before consuming the signed payload.
43
- *
44
- * @param {{ config?: object, handler?: Function }} options
45
- * @returns {import('https').Server}
46
- */
47
- export function createSandboxAttestationListener({
48
- config = CONFIG.sandbox,
49
- handler = handleSandboxHostAttestation
50
- } = {}) {
51
- const app = express();
52
- app.disable('x-powered-by');
53
-
54
- app.post(
55
- SANDBOX_HOST_ATTESTATION_PATH,
56
- (req, res, next) => authenticatePeer(req, res, next, config),
57
- express.raw({ limit: config.hostAttestationBodyLimitBytes, type: () => true }),
58
- parseStrictJson,
59
- (req, res) => handler(req, res, config)
60
- );
61
- app.use(bodyErrorHandler);
62
-
63
- const listener = createServer({
64
- cert: config.hostAttestationServerCert,
65
- key: config.hostAttestationServerKey,
66
- ca: config.hostAttestationClientCa,
67
- requestCert: true,
68
- rejectUnauthorized: true,
69
- minVersion: 'TLSv1.2'
70
- }, app);
71
- const sockets = new Set();
72
- listener.on('connection', socket => {
73
- sockets.add(socket);
74
- socket.once('close', () => sockets.delete(socket));
75
- });
76
- listener.sandboxAttestationSockets = sockets;
77
- listener.sandboxAttestationShutdownTimeoutMs = config.hostAttestationShutdownTimeoutMs;
78
- return listener;
79
- }
80
-
81
- /**
82
- * Start the attestation listener when Managed Sandbox is enabled.
83
- * @param {{ config?: object, handler?: Function }} options
84
- * @returns {Promise<import('https').Server|null>}
85
- */
86
- export async function startSandboxAttestationListener({
87
- config = CONFIG.sandbox,
88
- handler = handleSandboxHostAttestation
89
- } = {}) {
90
- if (!config.enabled) return null;
91
- const listener = createSandboxAttestationListener({ config, handler });
92
- await new Promise((resolve, reject) => {
93
- const onError = (err) => {
94
- listener.off('listening', onListening);
95
- reject(err);
96
- };
97
- const onListening = () => {
98
- listener.off('error', onError);
99
- resolve();
100
- };
101
- listener.once('error', onError);
102
- listener.once('listening', onListening);
103
- listener.listen(config.hostAttestationListenerPort, config.hostAttestationListenerHost);
104
- });
105
- return listener;
106
- }
107
-
108
- export async function closeSandboxAttestationListener(listener) {
109
- if (!listener) return;
110
- const sockets = listener.sandboxAttestationSockets || new Set();
111
- const timeoutMs = listener.sandboxAttestationShutdownTimeoutMs || 1_000;
112
- await new Promise((resolve, reject) => {
113
- let settled = false;
114
- const finish = err => {
115
- if (settled) return;
116
- settled = true;
117
- clearTimeout(timer);
118
- if (err && err.code !== 'ERR_SERVER_NOT_RUNNING') reject(err);
119
- else resolve();
120
- };
121
- const timer = setTimeout(() => {
122
- for (const socket of sockets) socket.destroy();
123
- }, timeoutMs);
124
- timer.unref?.();
125
- listener.close(finish);
126
- listener.closeIdleConnections?.();
127
- });
128
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * Existing Sandbox cleanup needs only the Controller trust and transport configuration.
3
- * It deliberately does not depend on the product admission flag or creation-only settings.
4
- * @param {object} config
5
- * @returns {boolean}
6
- */
7
- export function validateSandboxCleanupConfig(config) {
8
- if (!config?.controllerUrl || !config.controllerToken
9
- || !config.operationSigningPrivateKey || !config.controllerResultPublicKey
10
- || !config.controllerHostId || !config.helperAttestationPublicKey
11
- || !config.controllerClientCert || !config.controllerClientKey
12
- || !config.controllerCaCert) return false;
13
- try {
14
- return new URL(config.controllerUrl).protocol === 'https:';
15
- } catch {
16
- return false;
17
- }
18
- }
19
-
20
- /**
21
- * Sandbox execution is allowed only through a fully configured dedicated HTTPS Controller.
22
- * The current mixed-use Server Host is never an implicit execution target.
23
- * @param {object} config
24
- * @returns {boolean}
25
- */
26
- export function validateSandboxDeploymentConfig(config) {
27
- if (!config?.enabled || !validateSandboxCleanupConfig(config)
28
- || !config.bootstrapSigningKey
29
- || !config.hostAttestationKey || !config.controllerAttestationFingerprint
30
- || !config.hostAttestationListenerHost
31
- || !Number.isSafeInteger(config.hostAttestationListenerPort)
32
- || config.hostAttestationListenerPort <= 0
33
- || config.hostAttestationListenerPort > 65_535
34
- || !config.hostAttestationServerCert || !config.hostAttestationServerKey
35
- || !config.hostAttestationClientCa
36
- || !Number.isSafeInteger(config.hostAttestationBodyLimitBytes)
37
- || config.hostAttestationBodyLimitBytes <= 0
38
- || !config.imageDigest
39
- || !Number.isSafeInteger(config.hostMemoryReserveMiB)
40
- || config.hostMemoryReserveMiB <= 0) return false;
41
- return true;
42
- }
@@ -1,175 +0,0 @@
1
- import { createHmac, timingSafeEqual } from 'crypto';
2
- import db, { transaction } from './db/connection.js';
3
-
4
- const HEALTH_FIELDS = Object.freeze([
5
- 'controllerHealthy',
6
- 'helperHealthy',
7
- 'runtimeHealthy',
8
- 'quotaHealthy',
9
- 'networkHealthy'
10
- ]);
11
-
12
- export class SandboxHostAttestationError extends Error {
13
- constructor(code) {
14
- super(code);
15
- this.code = code;
16
- }
17
- }
18
-
19
- function canonicalPayload(attestation) {
20
- return JSON.stringify({
21
- hostId: attestation.hostId,
22
- epoch: attestation.epoch,
23
- nonce: attestation.nonce,
24
- observedAt: attestation.observedAt,
25
- imageDigest: attestation.imageDigest,
26
- resources: attestation.resources,
27
- checks: attestation.checks
28
- });
29
- }
30
-
31
- function validIdentifier(value, maxLength = 128) {
32
- return typeof value === 'string' && value.length > 0 && value.length <= maxLength
33
- && /^[a-zA-Z0-9._:-]+$/.test(value);
34
- }
35
-
36
- function verifySignature(payload, signature, key) {
37
- if (!key || typeof signature !== 'string') return false;
38
- const expected = createHmac('sha256', key).update(payload).digest();
39
- let actual;
40
- try {
41
- actual = Buffer.from(signature, 'base64url');
42
- } catch {
43
- return false;
44
- }
45
- return actual.length === expected.length && timingSafeEqual(actual, expected);
46
- }
47
-
48
- function validateAttestation(attestation, config, now) {
49
- if (!attestation || !validIdentifier(attestation.hostId) || !validIdentifier(attestation.epoch)
50
- || !validIdentifier(attestation.nonce, 192) || !validIdentifier(attestation.imageDigest, 192)) {
51
- throw new SandboxHostAttestationError('SANDBOX_HOST_ATTESTATION_INVALID');
52
- }
53
- if (attestation.hostId !== config.controllerHostId
54
- || attestation.imageDigest !== config.imageDigest) {
55
- throw new SandboxHostAttestationError('SANDBOX_HOST_ATTESTATION_SCOPE_MISMATCH');
56
- }
57
- const observedAt = Number(attestation.observedAt);
58
- if (!Number.isSafeInteger(observedAt)
59
- || Math.abs(now - observedAt) > config.hostAttestationMaxSkewMs) {
60
- throw new SandboxHostAttestationError('SANDBOX_HOST_ATTESTATION_STALE');
61
- }
62
- const resources = attestation.resources || {};
63
- if (![resources.cpuMillis, resources.memoryMiB, resources.memoryAvailableMiB, resources.diskGiB]
64
- .every(value => Number.isSafeInteger(value) && value > 0)
65
- || resources.memoryAvailableMiB > resources.memoryMiB) {
66
- throw new SandboxHostAttestationError('SANDBOX_HOST_ATTESTATION_INVALID');
67
- }
68
- if (!verifySignature(canonicalPayload(attestation), attestation.signature, config.hostAttestationKey)) {
69
- throw new SandboxHostAttestationError('SANDBOX_HOST_ATTESTATION_INVALID');
70
- }
71
- return Boolean(attestation.checks)
72
- && HEALTH_FIELDS.every(field => attestation.checks[field] === true);
73
- }
74
-
75
- function appendAudit(attestation, outcome, errorCode, now) {
76
- db.prepare(`
77
- INSERT INTO sandbox_host_audit_events (
78
- host_id, epoch, event_type, outcome, error_code, created_at
79
- ) VALUES (?, ?, 'qualification_attested', ?, ?, ?)
80
- `).run(
81
- String(attestation?.hostId || 'invalid').slice(0, 128),
82
- String(attestation?.epoch || 'invalid').slice(0, 128),
83
- outcome,
84
- errorCode,
85
- now
86
- );
87
- }
88
-
89
- export function registerSandboxHostAttestation(attestation, config, now = Date.now()) {
90
- try {
91
- const qualified = validateAttestation(attestation, config, now);
92
- return transaction(() => {
93
- const prior = db.prepare('SELECT nonce FROM sandbox_host_attestations WHERE nonce = ?')
94
- .get(attestation.nonce);
95
- if (prior) throw new SandboxHostAttestationError('SANDBOX_HOST_ATTESTATION_REPLAYED');
96
-
97
- const latest = db.prepare(`
98
- SELECT observed_at FROM sandbox_host_attestations
99
- WHERE host_id = ? ORDER BY observed_at DESC LIMIT 1
100
- `).get(attestation.hostId);
101
- if (latest && Number(attestation.observedAt) <= latest.observed_at) {
102
- throw new SandboxHostAttestationError('SANDBOX_HOST_ATTESTATION_OUT_OF_ORDER');
103
- }
104
-
105
- const priorEpoch = db.prepare(`
106
- SELECT source_epoch, epoch FROM sandbox_host_epochs WHERE host_id = ?
107
- `).get(attestation.hostId);
108
- const allocatedEpoch = priorEpoch
109
- ? priorEpoch.epoch + Number(priorEpoch.source_epoch !== attestation.epoch)
110
- : 1;
111
- const { resources, checks } = attestation;
112
- db.prepare(`
113
- INSERT INTO sandbox_hosts (
114
- id, epoch, qualified, controller_healthy, helper_healthy, runtime_healthy,
115
- quota_healthy, network_healthy, image_digest, cpu_millis_total,
116
- memory_mib_total, memory_mib_available, disk_gib_total, updated_at
117
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
118
- ON CONFLICT(id) DO UPDATE SET
119
- epoch = excluded.epoch,
120
- qualified = excluded.qualified,
121
- controller_healthy = excluded.controller_healthy,
122
- helper_healthy = excluded.helper_healthy,
123
- runtime_healthy = excluded.runtime_healthy,
124
- quota_healthy = excluded.quota_healthy,
125
- network_healthy = excluded.network_healthy,
126
- image_digest = excluded.image_digest,
127
- cpu_millis_total = excluded.cpu_millis_total,
128
- memory_mib_total = excluded.memory_mib_total,
129
- memory_mib_available = excluded.memory_mib_available,
130
- disk_gib_total = excluded.disk_gib_total,
131
- updated_at = excluded.updated_at
132
- `).run(
133
- attestation.hostId, allocatedEpoch, Number(qualified),
134
- Number(checks.controllerHealthy), Number(checks.helperHealthy),
135
- Number(checks.runtimeHealthy), Number(checks.quotaHealthy), Number(checks.networkHealthy),
136
- attestation.imageDigest, resources.cpuMillis, resources.memoryMiB,
137
- resources.memoryAvailableMiB, resources.diskGiB, now
138
- );
139
- db.prepare(`
140
- INSERT INTO sandbox_host_epochs
141
- (host_id, source_epoch, epoch, activation_digest, activated_at, updated_at)
142
- VALUES (?, ?, ?, NULL, NULL, ?)
143
- ON CONFLICT(host_id) DO UPDATE SET
144
- source_epoch = excluded.source_epoch,
145
- epoch = excluded.epoch,
146
- activation_digest = CASE
147
- WHEN sandbox_host_epochs.epoch = excluded.epoch
148
- THEN sandbox_host_epochs.activation_digest ELSE NULL END,
149
- activated_at = CASE
150
- WHEN sandbox_host_epochs.epoch = excluded.epoch
151
- THEN sandbox_host_epochs.activated_at ELSE NULL END,
152
- updated_at = excluded.updated_at
153
- `).run(attestation.hostId, attestation.epoch, allocatedEpoch, now);
154
- db.prepare(`
155
- INSERT INTO sandbox_host_attestations (nonce, host_id, epoch, observed_at, created_at)
156
- VALUES (?, ?, ?, ?, ?)
157
- `).run(attestation.nonce, attestation.hostId, allocatedEpoch, attestation.observedAt, now);
158
- appendAudit(
159
- { ...attestation, epoch: allocatedEpoch },
160
- qualified ? 'accepted' : 'rejected',
161
- qualified ? null : 'SANDBOX_HOST_NOT_QUALIFIED',
162
- now
163
- );
164
- return { accepted: true, qualified, epoch: allocatedEpoch };
165
- })();
166
- } catch (error) {
167
- const code = error instanceof SandboxHostAttestationError
168
- ? error.code
169
- : 'SANDBOX_HOST_ATTESTATION_INVALID';
170
- appendAudit(attestation, 'rejected', code, now);
171
- throw error instanceof SandboxHostAttestationError
172
- ? error
173
- : new SandboxHostAttestationError(code);
174
- }
175
- }