openvibers/openvibe.run Commits
Persist and enforce browser session origin lists (#254)
Changes
@@@ -6,6 +6,10 @@ Notable changes to OpenVibe.Run. The service is unreleased; everything below is### Added++- With `RUN_SESSION_ORIGINS_ENABLED=1`, a browser session may name exact HTTP(S) origins at creation. Run stores the++ list with the session, places it on a user-owned desk that can enforce the boundary, and sends the same list when++ leasing that desk. Commands cannot replace the list. The switch is off by default.++- One Computer (plan 2026-11-07, contracts 0.327.0): `run.session@1` carries `task_id` (the OpenVibe.Actor task thesession is the Computer of, so the session points back at the task) and `curtain` (the privacy screen).`GET /api/v1/sessions/{id}/still` answers the desk's newest frame as a small JPEG (at most 640 px wide, cached for annew file mode 100644@@@ -0,0 +1,3 @@++-- phase: expand++-- Null keeps older sessions unrestricted; an empty array permits no browser origin.++ALTER TABLE run_sessions ADD COLUMN IF NOT EXISTS task_origins JSONB;@@@ -118,6 +118,7 @@ function loadConfig(env = process.env) {// Run sessions (contracts 0.223.0, server/sessions/): one pass over the open ones every tickMs; a person holds at// most maxOpen at once; a running one's lease is checked on Bot every leaseCheckMs.sessions: {++ originsEnabled: env.RUN_SESSION_ORIGINS_ENABLED === '1',tickMs: Math.max(200, int(env.RUN_SESSION_TICK_MS, 2000)),maxOpen: Math.max(1, int(env.RUN_SESSIONS_PER_PERSON, 3)),leaseCheckMs: Math.max(1000, int(env.RUN_SESSION_LEASE_CHECK_MS, 30000)),nnew file mode 100644@@@ -0,0 +1,16 @@++'use strict';++++function parseOrigins(raw) {++ if (raw === undefined) return null;++ if (!Array.isArray(raw) || raw.length > 32) throw new Error('origins is a list of at most 32 http(s) origins');++ return raw.map((item) => {++ if (typeof item !== 'string' || item.length > 2048 || /[\s\u0000-\u001f]/.test(item)) throw new Error('invalid origin');++ let u;++ try { u = new URL(item); } catch { throw new Error('invalid origin'); }++ if (!['http:', 'https:'].includes(u.protocol) || !u.hostname || u.username || u.password ||++ u.pathname !== '/' || u.search || u.hash || /[^a-z0-9.:[\]-]/i.test(u.hostname)) throw new Error('invalid origin');++ return u.origin;++ });++}++++module.exports = { parseOrigins };@@@ -24,6 +24,7 @@*/const contracts = require('openvibe-contracts');const store = require('./store');++const { parseOrigins } = require('./origins');const curtain = require('./curtain');const usage = require('../jobs/usage');const { TABLE: EVENTS_TABLE, ACTOR } = require('../events/outbox');@@@ -103,7 +104,12 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), logif (strategy && !require('openvibe-sdk/compute').STRATEGIES.includes(strategy)) throw new SessionError(422, 'run.compute.strategy', 'unknown compute strategy');if (strategy && !pool) throw new SessionError(422, 'run.compute.pool', 'a strategy requires a compute pool');if (pool) { if (!broker) throw new SessionError(503, 'run.compute.off'); await broker.poolConfig(pool); }-- const v = contracts.validate('run.session-create-request@1', body);++ if (Object.hasOwn(body, 'origins') && !cfg.originsEnabled) throw new SessionError(403, 'run.session.origins_disabled', 'Task origin boundaries are not enabled here yet.');++ let taskOrigins;++ try { taskOrigins = parseOrigins(body.origins); }++ catch (e) { throw new SessionError(422, 'run.session.invalid', e.message); }++ const { origins: _boundary, ...contractBody } = body;++ const v = contracts.validate('run.session-create-request@1', contractBody);if (!v.valid) throw new SessionError(422, 'run.session.invalid', `invalid session request: ${(v.errors && v.errors[0] && v.errors[0].message) || 'bad value'}`.slice(0, 300));const digest = store.digestOf({ ...body, compute_pool: pool, compute_strategy: strategy, template, actor_id: actorId });if (body.idempotency_key) {@@@ -120,7 +126,7 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), logid, owner, via: via || null, kind: 'browser', state: 'placing',width: body.width || 1280, height: body.height || 800, ttl_s: body.ttl_s || 3600, idle_s: body.idle_s || 900,compute_pool: pool, compute_strategy: strategy, compute_decision: null, template, actor_id: actorId, machine_asked: body.machine || null, requirements: body.requirements || null, idempotency_key: body.idempotency_key || null,-- request_digest: digest, reasons: [], task_id: body.task_id || null, curtain: false,++ request_digest: digest, reasons: [], task_id: body.task_id || null, curtain: false, task_origins: taskOrigins,control_lease: { thing: `run.session:${id}`, holder: { kind: 'person', subject: bare(owner) }, since_ms: at, expires_ms: at + 60_000, watchers: [], seq: 1 },created_at: at, updated_at: at,};@@@ -238,6 +244,7 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), logconst v = contracts.validate('run.session-control-request@1', cmd);if (!v.valid) throw new SessionError(422, 'run.session.invalid', `invalid command: ${(v.errors && v.errors[0] && v.errors[0].message) || 'bad value'}`.slice(0, 300));if (cmd.kind === 'browser' && ['active_origin', 'blind_fill'].includes(cmd.value && cmd.value.action)) throw new SessionError(403, 'run.fill.internal_action', 'this browser action is available only through the blind sign-in flow');++ if (cmd.kind === 'browser' && Object.hasOwn(cmd.value, 'origins')) throw new SessionError(422, 'run.session.invalid', 'The task origins were fixed when the session was created.');const id = cmd.id || `c-${now().toString(36)}`;if (row.state !== 'running') throw new SessionError(409, 'run.session.not_running', row.state === 'ended' ? 'the session has ended' : 'the session\'s desk is not running yet');const lease = await controlLease(row);@@@ -449,6 +456,11 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log}function candidates(row, hosts) {const live = hosts.filter((h) => h && h.online && Number(h.free) > 0 && typeof h.machine_id === 'string');++ if (row.task_origins !== null) {++ if (row.requirements && Array.isArray(row.requirements.trust) && !row.requirements.trust.includes('user-owned')) return [];++ return live.filter((h) => h.trust === 'user-owned' && !h.shared && (!row.machine_asked || h.machine_id === row.machine_asked))++ .sort((a, b) => Number(b.free) - Number(a.free));++ }if (row.machine_asked) return live.filter((h) => h.machine_id === row.machine_asked && h.trust === 'user-owned');const allowed = row.requirements && Array.isArray(row.requirements.trust) ? row.requirements.trust : DEFAULT_TRUST;return live.filter((h) => allowed.includes(h.trust))@@@ -481,7 +493,8 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), logawait store.update(db, row.id, { state: 'starting', machine: h.machine_id, desk, trust: h.trust, offer_id: h.shared ? `desk:${h.machine_id}` : null, reasons: [...reasonsOf(row, h), ...(decision ? [...decision.reasons, `estimated ${decision.hourly} ${decision.currency}/hour`] : [])], compute_decision: decision, updated_at: now() });let lease;try {-- lease = await desks.lease(h.machine_id, bare(row.owner), { session: row.id, desk, kind: 'browser', width: row.width, height: row.height });++ lease = await desks.lease(h.machine_id, bare(row.owner), { session: row.id, desk, kind: 'browser', width: row.width, height: row.height,++ ...(row.task_origins !== null ? { origins: row.task_origins } : {}) });} catch (e) {await store.update(db, row.id, { state: 'placing', machine: null, desk: null, trust: null, offer_id: null, reasons: [], updated_at: now() });hostCache.delete(bare(row.owner));@@@ -12,10 +12,10 @@const crypto = require('crypto');const { ids } = require('openvibe-contracts');--const COLUMNS = ['id', 'owner', 'via', 'kind', 'state', 'width', 'height', 'ttl_s', 'idle_s', 'machine_asked', 'requirements', 'idempotency_key',++const COLUMNS = ['id', 'owner', 'via', 'kind', 'state', 'width', 'height', 'ttl_s', 'idle_s', 'machine_asked', 'requirements', 'idempotency_key', 'task_origins','compute_pool', 'compute_strategy', 'compute_decision', 'request_digest', 'machine', 'desk', 'trust', 'offer_id', 'reasons', 'created_at', 'started_at', 'last_used_at', 'ended_at', 'end_reason','end_detail', 'metered_minutes', 'lease_checked_at', 'task_id', 'curtain', 'control_lease', 'template', 'actor_id', 'egress_label', 'updated_at'];--const JSON_COLUMNS = new Set(['requirements', 'reasons', 'compute_decision', 'control_lease']);++const JSON_COLUMNS = new Set(['requirements', 'reasons', 'compute_decision', 'control_lease', 'task_origins']);const iso = (ms) => (ms == null ? null : new Date(Number(ms)).toISOString());const json = (v, dflt) => { if (v == null) return dflt; if (typeof v === 'object') return v; try { return JSON.parse(v); } catch { return dflt; } };@@@ -11,6 +11,7 @@ const http = require('http');const contracts = require('openvibe-contracts');const { ids } = require('openvibe-contracts');const { boot, check, done } = require('./helpers/app');++const { loadConfig } = require('../server/config');const OWN = 'dev_01J8Z4M2Q0R7T9YV3K6N8P1W2A';const SHARED = 'dev_01J8Z4M2Q0R7T9YV3K6N8P1W2B';@@@ -51,8 +52,9 @@ function fakeDesks() {}(async () => {++ assert.equal(loadConfig({}).sessions.originsEnabled, false, 'task origins are owner switched and off by default');const desks = fakeDesks();-- const t = await boot({ appOpts: { desks }, env: { RUN_SESSIONS_PER_PERSON: '3', RUN_SESSION_LEASE_CHECK_MS: '1000' } });++ const t = await boot({ appOpts: { desks }, env: { RUN_SESSIONS_PER_PERSON: '3', RUN_SESSION_LEASE_CHECK_MS: '1000', RUN_SESSION_ORIGINS_ENABLED: '1' } });const { sessions } = t.app.locals;const ada = ids.newId('user');const bea = ids.newId('user');@@@ -81,6 +83,21 @@ function fakeDesks() {assert.ok(started.some((p) => p.id === ses.id && p.state === 'running'), 'run.session.started');});++ await check('task origins are fixed at creation and use a user-owned desk', async () => {++ const created = await t.call('POST', '/api/v1/sessions', { ...as(bea), body: { kind: 'browser', origins: ['https://example.org'] } });++ assert.equal(created.status, 201, created.text);++ await sessions.tick();++ const bounded = (await t.call('GET', `/api/v1/sessions/${created.json.id}`, as(bea))).json;++ assert.equal(bounded.placement.machine, OWN, 'a shared desk cannot enforce origins');++ const lease = desks.calls.filter((c) => c[0] === 'lease' && c[3].session === bounded.id).at(-1);++ assert.deepStrictEqual(lease[3].origins, ['https://example.org']);++ const changed = await t.call('POST', `/api/v1/sessions/${bounded.id}/commands`, { ...as(bea), body: { kind: 'browser', value: { action: 'read', origins: [] } } });++ assert.equal(changed.status, 422, 'a later command cannot replace the boundary');++ await sessions.finish(await sessions.get(bounded.id), 'ended', 'test complete');++ const bad = await t.call('POST', '/api/v1/sessions', { ...as(bea), body: { kind: 'browser', origins: ['https://example.org/path'] } });++ assert.equal(bad.status, 422);++ });++await check('browser and throwaway presets are accepted; kept profiles and snapshots say plainly when unavailable', async () => {const unavailable = await t.call('POST', '/api/v1/sessions?template=kept-browser', { ...as(bea), body: { kind: 'browser' } });assert.strictEqual(unavailable.status, 503);
@@ -6,6 +6,10 @@ Notable changes to OpenVibe.Run. The service is unreleased; everything below is
@@ -6,6 +6,10 @@ Notable changes to OpenVibe.Run. The service is unreleased; everything below is
### Added
### Added
+- With `RUN_SESSION_ORIGINS_ENABLED=1`, a browser session may name exact HTTP(S) origins at creation. Run stores the
+ list with the session, places it on a user-owned desk that can enforce the boundary, and sends the same list when
+ leasing that desk. Commands cannot replace the list. The switch is off by default.
+
- One Computer (plan 2026-11-07, contracts 0.327.0): `run.session@1` carries `task_id` (the OpenVibe.Actor task the
- One Computer (plan 2026-11-07, contracts 0.327.0): `run.session@1` carries `task_id` (the OpenVibe.Actor task the
session is the Computer of, so the session points back at the task) and `curtain` (the privacy screen).
session is the Computer of, so the session points back at the task) and `curtain` (the privacy screen).
`GET /api/v1/sessions/{id}/still` answers the desk's newest frame as a small JPEG (at most 640 px wide, cached for a
`GET /api/v1/sessions/{id}/still` answers the desk's newest frame as a small JPEG (at most 640 px wide, cached for a
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+-- phase: expand
+-- Null keeps older sessions unrestricted; an empty array permits no browser origin.
+ALTER TABLE run_sessions ADD COLUMN IF NOT EXISTS task_origins JSONB;
@@ -118,6 +118,7 @@ function loadConfig(env = process.env) {
@@ -118,6 +118,7 @@ function loadConfig(env = process.env) {
// Run sessions (contracts 0.223.0, server/sessions/): one pass over the open ones every tickMs; a person holds at
// Run sessions (contracts 0.223.0, server/sessions/): one pass over the open ones every tickMs; a person holds at
// most maxOpen at once; a running one's lease is checked on Bot every leaseCheckMs.
// most maxOpen at once; a running one's lease is checked on Bot every leaseCheckMs.
sessions: {
sessions: {
+ originsEnabled: env.RUN_SESSION_ORIGINS_ENABLED === '1',
tickMs: Math.max(200, int(env.RUN_SESSION_TICK_MS, 2000)),
tickMs: Math.max(200, int(env.RUN_SESSION_TICK_MS, 2000)),
maxOpen: Math.max(1, int(env.RUN_SESSIONS_PER_PERSON, 3)),
maxOpen: Math.max(1, int(env.RUN_SESSIONS_PER_PERSON, 3)),
leaseCheckMs: Math.max(1000, int(env.RUN_SESSION_LEASE_CHECK_MS, 30000)),
leaseCheckMs: Math.max(1000, int(env.RUN_SESSION_LEASE_CHECK_MS, 30000)),
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+'use strict';
+
+function parseOrigins(raw) {
+ if (raw === undefined) return null;
+ if (!Array.isArray(raw) || raw.length > 32) throw new Error('origins is a list of at most 32 http(s) origins');
+ return raw.map((item) => {
+ if (typeof item !== 'string' || item.length > 2048 || /[\s\u0000-\u001f]/.test(item)) throw new Error('invalid origin');
+ let u;
+ try { u = new URL(item); } catch { throw new Error('invalid origin'); }
+ if (!['http:', 'https:'].includes(u.protocol) || !u.hostname || u.username || u.password ||
+ u.pathname !== '/' || u.search || u.hash || /[^a-z0-9.:[\]-]/i.test(u.hostname)) throw new Error('invalid origin');
+ return u.origin;
+ });
+}
+
+module.exports = { parseOrigins };
@@ -24,6 +24,7 @@
@@ -24,6 +24,7 @@
*/
*/
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const store = require('./store');
const store = require('./store');
+const { parseOrigins } = require('./origins');
const curtain = require('./curtain');
const curtain = require('./curtain');
const usage = require('../jobs/usage');
const usage = require('../jobs/usage');
const { TABLE: EVENTS_TABLE, ACTOR } = require('../events/outbox');
const { TABLE: EVENTS_TABLE, ACTOR } = require('../events/outbox');
@@ -103,7 +104,12 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
@@ -103,7 +104,12 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
if (strategy && !require('openvibe-sdk/compute').STRATEGIES.includes(strategy)) throw new SessionError(422, 'run.compute.strategy', 'unknown compute strategy');
if (strategy && !require('openvibe-sdk/compute').STRATEGIES.includes(strategy)) throw new SessionError(422, 'run.compute.strategy', 'unknown compute strategy');
if (strategy && !pool) throw new SessionError(422, 'run.compute.pool', 'a strategy requires a compute pool');
if (strategy && !pool) throw new SessionError(422, 'run.compute.pool', 'a strategy requires a compute pool');
if (pool) { if (!broker) throw new SessionError(503, 'run.compute.off'); await broker.poolConfig(pool); }
if (pool) { if (!broker) throw new SessionError(503, 'run.compute.off'); await broker.poolConfig(pool); }
- const v = contracts.validate('run.session-create-request@1', body);
+ if (Object.hasOwn(body, 'origins') && !cfg.originsEnabled) throw new SessionError(403, 'run.session.origins_disabled', 'Task origin boundaries are not enabled here yet.');
+ let taskOrigins;
+ try { taskOrigins = parseOrigins(body.origins); }
+ catch (e) { throw new SessionError(422, 'run.session.invalid', e.message); }
+ const { origins: _boundary, ...contractBody } = body;
+ const v = contracts.validate('run.session-create-request@1', contractBody);
if (!v.valid) throw new SessionError(422, 'run.session.invalid', `invalid session request: ${(v.errors && v.errors[0] && v.errors[0].message) || 'bad value'}`.slice(0, 300));
if (!v.valid) throw new SessionError(422, 'run.session.invalid', `invalid session request: ${(v.errors && v.errors[0] && v.errors[0].message) || 'bad value'}`.slice(0, 300));
const digest = store.digestOf({ ...body, compute_pool: pool, compute_strategy: strategy, template, actor_id: actorId });
const digest = store.digestOf({ ...body, compute_pool: pool, compute_strategy: strategy, template, actor_id: actorId });
if (body.idempotency_key) {
if (body.idempotency_key) {
@@ -120,7 +126,7 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
@@ -120,7 +126,7 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
id, owner, via: via || null, kind: 'browser', state: 'placing',
id, owner, via: via || null, kind: 'browser', state: 'placing',
width: body.width || 1280, height: body.height || 800, ttl_s: body.ttl_s || 3600, idle_s: body.idle_s || 900,
width: body.width || 1280, height: body.height || 800, ttl_s: body.ttl_s || 3600, idle_s: body.idle_s || 900,
compute_pool: pool, compute_strategy: strategy, compute_decision: null, template, actor_id: actorId, machine_asked: body.machine || null, requirements: body.requirements || null, idempotency_key: body.idempotency_key || null,
compute_pool: pool, compute_strategy: strategy, compute_decision: null, template, actor_id: actorId, machine_asked: body.machine || null, requirements: body.requirements || null, idempotency_key: body.idempotency_key || null,
- request_digest: digest, reasons: [], task_id: body.task_id || null, curtain: false,
+ request_digest: digest, reasons: [], task_id: body.task_id || null, curtain: false, task_origins: taskOrigins,
control_lease: { thing: `run.session:${id}`, holder: { kind: 'person', subject: bare(owner) }, since_ms: at, expires_ms: at + 60_000, watchers: [], seq: 1 },
control_lease: { thing: `run.session:${id}`, holder: { kind: 'person', subject: bare(owner) }, since_ms: at, expires_ms: at + 60_000, watchers: [], seq: 1 },
created_at: at, updated_at: at,
created_at: at, updated_at: at,
};
};
@@ -238,6 +244,7 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
@@ -238,6 +244,7 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
const v = contracts.validate('run.session-control-request@1', cmd);
const v = contracts.validate('run.session-control-request@1', cmd);
if (!v.valid) throw new SessionError(422, 'run.session.invalid', `invalid command: ${(v.errors && v.errors[0] && v.errors[0].message) || 'bad value'}`.slice(0, 300));
if (!v.valid) throw new SessionError(422, 'run.session.invalid', `invalid command: ${(v.errors && v.errors[0] && v.errors[0].message) || 'bad value'}`.slice(0, 300));
if (cmd.kind === 'browser' && ['active_origin', 'blind_fill'].includes(cmd.value && cmd.value.action)) throw new SessionError(403, 'run.fill.internal_action', 'this browser action is available only through the blind sign-in flow');
if (cmd.kind === 'browser' && ['active_origin', 'blind_fill'].includes(cmd.value && cmd.value.action)) throw new SessionError(403, 'run.fill.internal_action', 'this browser action is available only through the blind sign-in flow');
+ if (cmd.kind === 'browser' && Object.hasOwn(cmd.value, 'origins')) throw new SessionError(422, 'run.session.invalid', 'The task origins were fixed when the session was created.');
const id = cmd.id || `c-${now().toString(36)}`;
const id = cmd.id || `c-${now().toString(36)}`;
if (row.state !== 'running') throw new SessionError(409, 'run.session.not_running', row.state === 'ended' ? 'the session has ended' : 'the session\'s desk is not running yet');
if (row.state !== 'running') throw new SessionError(409, 'run.session.not_running', row.state === 'ended' ? 'the session has ended' : 'the session\'s desk is not running yet');
const lease = await controlLease(row);
const lease = await controlLease(row);
@@ -449,6 +456,11 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
@@ -449,6 +456,11 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
}
}
function candidates(row, hosts) {
function candidates(row, hosts) {
const live = hosts.filter((h) => h && h.online && Number(h.free) > 0 && typeof h.machine_id === 'string');
const live = hosts.filter((h) => h && h.online && Number(h.free) > 0 && typeof h.machine_id === 'string');
+ if (row.task_origins !== null) {
+ if (row.requirements && Array.isArray(row.requirements.trust) && !row.requirements.trust.includes('user-owned')) return [];
+ return live.filter((h) => h.trust === 'user-owned' && !h.shared && (!row.machine_asked || h.machine_id === row.machine_asked))
+ .sort((a, b) => Number(b.free) - Number(a.free));
+ }
if (row.machine_asked) return live.filter((h) => h.machine_id === row.machine_asked && h.trust === 'user-owned');
if (row.machine_asked) return live.filter((h) => h.machine_id === row.machine_asked && h.trust === 'user-owned');
const allowed = row.requirements && Array.isArray(row.requirements.trust) ? row.requirements.trust : DEFAULT_TRUST;
const allowed = row.requirements && Array.isArray(row.requirements.trust) ? row.requirements.trust : DEFAULT_TRUST;
return live.filter((h) => allowed.includes(h.trust))
return live.filter((h) => allowed.includes(h.trust))
@@ -481,7 +493,8 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
@@ -481,7 +493,8 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
await store.update(db, row.id, { state: 'starting', machine: h.machine_id, desk, trust: h.trust, offer_id: h.shared ? `desk:${h.machine_id}` : null, reasons: [...reasonsOf(row, h), ...(decision ? [...decision.reasons, `estimated ${decision.hourly} ${decision.currency}/hour`] : [])], compute_decision: decision, updated_at: now() });
await store.update(db, row.id, { state: 'starting', machine: h.machine_id, desk, trust: h.trust, offer_id: h.shared ? `desk:${h.machine_id}` : null, reasons: [...reasonsOf(row, h), ...(decision ? [...decision.reasons, `estimated ${decision.hourly} ${decision.currency}/hour`] : [])], compute_decision: decision, updated_at: now() });
let lease;
let lease;
try {
try {
- lease = await desks.lease(h.machine_id, bare(row.owner), { session: row.id, desk, kind: 'browser', width: row.width, height: row.height });
+ lease = await desks.lease(h.machine_id, bare(row.owner), { session: row.id, desk, kind: 'browser', width: row.width, height: row.height,
+ ...(row.task_origins !== null ? { origins: row.task_origins } : {}) });
} catch (e) {
} catch (e) {
await store.update(db, row.id, { state: 'placing', machine: null, desk: null, trust: null, offer_id: null, reasons: [], updated_at: now() });
await store.update(db, row.id, { state: 'placing', machine: null, desk: null, trust: null, offer_id: null, reasons: [], updated_at: now() });
hostCache.delete(bare(row.owner));
hostCache.delete(bare(row.owner));
@@ -12,10 +12,10 @@
@@ -12,10 +12,10 @@
const crypto = require('crypto');
const crypto = require('crypto');
const { ids } = require('openvibe-contracts');
const { ids } = require('openvibe-contracts');
-const COLUMNS = ['id', 'owner', 'via', 'kind', 'state', 'width', 'height', 'ttl_s', 'idle_s', 'machine_asked', 'requirements', 'idempotency_key',
+const COLUMNS = ['id', 'owner', 'via', 'kind', 'state', 'width', 'height', 'ttl_s', 'idle_s', 'machine_asked', 'requirements', 'idempotency_key', 'task_origins',
'compute_pool', 'compute_strategy', 'compute_decision', 'request_digest', 'machine', 'desk', 'trust', 'offer_id', 'reasons', 'created_at', 'started_at', 'last_used_at', 'ended_at', 'end_reason',
'compute_pool', 'compute_strategy', 'compute_decision', 'request_digest', 'machine', 'desk', 'trust', 'offer_id', 'reasons', 'created_at', 'started_at', 'last_used_at', 'ended_at', 'end_reason',
'end_detail', 'metered_minutes', 'lease_checked_at', 'task_id', 'curtain', 'control_lease', 'template', 'actor_id', 'egress_label', 'updated_at'];
'end_detail', 'metered_minutes', 'lease_checked_at', 'task_id', 'curtain', 'control_lease', 'template', 'actor_id', 'egress_label', 'updated_at'];
-const JSON_COLUMNS = new Set(['requirements', 'reasons', 'compute_decision', 'control_lease']);
+const JSON_COLUMNS = new Set(['requirements', 'reasons', 'compute_decision', 'control_lease', 'task_origins']);
const iso = (ms) => (ms == null ? null : new Date(Number(ms)).toISOString());
const iso = (ms) => (ms == null ? null : new Date(Number(ms)).toISOString());
const json = (v, dflt) => { if (v == null) return dflt; if (typeof v === 'object') return v; try { return JSON.parse(v); } catch { return dflt; } };
const json = (v, dflt) => { if (v == null) return dflt; if (typeof v === 'object') return v; try { return JSON.parse(v); } catch { return dflt; } };
@@ -11,6 +11,7 @@ const http = require('http');
@@ -11,6 +11,7 @@ const http = require('http');
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const { ids } = require('openvibe-contracts');
const { ids } = require('openvibe-contracts');
const { boot, check, done } = require('./helpers/app');
const { boot, check, done } = require('./helpers/app');
+const { loadConfig } = require('../server/config');
const OWN = 'dev_01J8Z4M2Q0R7T9YV3K6N8P1W2A';
const OWN = 'dev_01J8Z4M2Q0R7T9YV3K6N8P1W2A';
const SHARED = 'dev_01J8Z4M2Q0R7T9YV3K6N8P1W2B';
const SHARED = 'dev_01J8Z4M2Q0R7T9YV3K6N8P1W2B';
@@ -51,8 +52,9 @@ function fakeDesks() {
@@ -51,8 +52,9 @@ function fakeDesks() {
}
}
(async () => {
(async () => {
+ assert.equal(loadConfig({}).sessions.originsEnabled, false, 'task origins are owner switched and off by default');
const desks = fakeDesks();
const desks = fakeDesks();
- const t = await boot({ appOpts: { desks }, env: { RUN_SESSIONS_PER_PERSON: '3', RUN_SESSION_LEASE_CHECK_MS: '1000' } });
+ const t = await boot({ appOpts: { desks }, env: { RUN_SESSIONS_PER_PERSON: '3', RUN_SESSION_LEASE_CHECK_MS: '1000', RUN_SESSION_ORIGINS_ENABLED: '1' } });
const { sessions } = t.app.locals;
const { sessions } = t.app.locals;
const ada = ids.newId('user');
const ada = ids.newId('user');
const bea = ids.newId('user');
const bea = ids.newId('user');
@@ -81,6 +83,21 @@ function fakeDesks() {
@@ -81,6 +83,21 @@ function fakeDesks() {
assert.ok(started.some((p) => p.id === ses.id && p.state === 'running'), 'run.session.started');
assert.ok(started.some((p) => p.id === ses.id && p.state === 'running'), 'run.session.started');
});
});
+ await check('task origins are fixed at creation and use a user-owned desk', async () => {
+ const created = await t.call('POST', '/api/v1/sessions', { ...as(bea), body: { kind: 'browser', origins: ['https://example.org'] } });
+ assert.equal(created.status, 201, created.text);
+ await sessions.tick();
+ const bounded = (await t.call('GET', `/api/v1/sessions/${created.json.id}`, as(bea))).json;
+ assert.equal(bounded.placement.machine, OWN, 'a shared desk cannot enforce origins');
+ const lease = desks.calls.filter((c) => c[0] === 'lease' && c[3].session === bounded.id).at(-1);
+ assert.deepStrictEqual(lease[3].origins, ['https://example.org']);
+ const changed = await t.call('POST', `/api/v1/sessions/${bounded.id}/commands`, { ...as(bea), body: { kind: 'browser', value: { action: 'read', origins: [] } } });
+ assert.equal(changed.status, 422, 'a later command cannot replace the boundary');
+ await sessions.finish(await sessions.get(bounded.id), 'ended', 'test complete');
+ const bad = await t.call('POST', '/api/v1/sessions', { ...as(bea), body: { kind: 'browser', origins: ['https://example.org/path'] } });
+ assert.equal(bad.status, 422);
+ });
+
await check('browser and throwaway presets are accepted; kept profiles and snapshots say plainly when unavailable', async () => {
await check('browser and throwaway presets are accepted; kept profiles and snapshots say plainly when unavailable', async () => {
const unavailable = await t.call('POST', '/api/v1/sessions?template=kept-browser', { ...as(bea), body: { kind: 'browser' } });
const unavailable = await t.call('POST', '/api/v1/sessions?template=kept-browser', { ...as(bea), body: { kind: 'browser' } });
assert.strictEqual(unavailable.status, 503);
assert.strictEqual(unavailable.status, 503);