openvibers/openvibe.events Commits

Events: the network agent namespace (agent.*) (#246)

OP OpenVibers committed 6 days ago · c0e480a5c1 · 4 files, +31 −6

* Events: the network agent namespace (agent.*)

* Events: merge main; share.* and the agent.* namespace are both shared

Changes

ddiff --git a/README.md b/README.md
iindex 075d5a5..09fbb14 100644
---- a/README.md
++++ b/README.md
@@@ -79,7 +79,7 @@ project's hourly publishing and delivery rollup, for Network; never visible to a
`POST /api/v1/events` takes one `events.event-envelope@1` envelope, or `{ "events": [ … ] }` (up to 100, stored atomically).
--- `source` must be the calling service (`svc:live` publishes `source: "live"`), and `event_type` must start with a prefix that source owns (`live.*`, `media.*`, `network.*`, `community.*`, `chat.*`, `openre.*`, `billing.*`, `tips.*`, `vip.*`, `ai.*`, `games.*`, `tools.*`, `codes.*`, `host.*`, `bot.*`, `watch.*`, `run.*`, `space.*`, and the publication products (`wiki.*`, `blog.*`, `news.*`, `reviews.*`, `deals.*`, `coupons.*`, `trade.*`, `sources.*`, `search.*`); `EVENTS_SOURCE_PREFIXES` overrides). A source's namespace is its id without hyphens, since an event type has none: `svc:media-hub` publishes `source: "media-hub"` with `mediahub.*` types, and no other source can claim `mediahub.*`. A network-wide prefix may be published by any first-party service: `provider.*` carries storage provider health/capacity telemetry (`provider.health.degraded`, `provider.capacity.warning`), so whichever service observes a provider can report it; `EVENTS_SHARED_PREFIXES` overrides.
++- `source` must be the calling service (`svc:live` publishes `source: "live"`), and `event_type` must start with a prefix that source owns (`live.*`, `media.*`, `network.*`, `community.*`, `chat.*`, `openre.*`, `billing.*`, `tips.*`, `vip.*`, `ai.*`, `games.*`, `tools.*`, `codes.*`, `host.*`, `bot.*`, `watch.*`, `run.*`, `space.*`, and the publication products (`wiki.*`, `blog.*`, `news.*`, `reviews.*`, `deals.*`, `coupons.*`, `trade.*`, `sources.*`, `search.*`); `EVENTS_SOURCE_PREFIXES` overrides). A source's namespace is its id without hyphens, since an event type has none: `svc:media-hub` publishes `source: "media-hub"` with `mediahub.*` types, and no other source can claim `mediahub.*`. A network-wide prefix may be published by any first-party service: `provider.*` carries storage provider health/capacity telemetry (`provider.health.degraded`, `provider.capacity.warning`), so whichever service observes a provider can report it; `agent.*` is the network's agent namespace (design 2026-11-07): every mode of the one agent publishes the steps it takes into it (`agent.step.started|updated|ended`), so Chat, Codes, Run, Watch, Live and Surf can each publish the step they just ran; `EVENTS_SHARED_PREFIXES` overrides.
- Idempotent on `event_id`: a repeat answers `200 { event_id, cursor, duplicate: true }` (`pruned: true` and no cursor when retention already removed the stored copy) and is never stored twice (even after retention, for `EVENTS_RECEIPT_RETENTION_DAYS`).
- Missing `trace_id` is taken from the request's `traceparent`; `priority` defaults to `important`, `visibility` to `internal`.
- Loop guard: an event whose trace already carries 8 hops (the cross-service chain depth, or the same source/type/subject repeating in the trace) is refused with `409 events.loop_detected`. The chain depth of a first-party publish counts first-party events only (developer-app events in the same trace never count toward it), so an app cannot poison a trace it has seen.
ddiff --git a/server/config.js b/server/config.js
iindex 9b50321..52c02f9 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -65,11 +65,15 @@ const DEFAULT_SOURCES = ['live', 'media', 'network', 'community', 'chat', 'openr
* provider health/capacity telemetry, which whichever first-party service observes a provider
* publishes (Media, Sources, Host, ...), so any service token may publish into it. `share.*` is
* the sharing family Network publishes (share.granted, share.revoked, share.transfer.*), named by
-- * its family rather than by the network namespace in Contracts' network manifest. Sources still
-- * own their own namespace first; a shared prefix never replaces `<source>.`. Developer apps can
-- * never publish a shared prefix. EVENTS_SHARED_PREFIXES (comma-separated) replaces the default.
++ * its family rather than by the network namespace in Contracts' network manifest. `agent.*` is the
++ * network's agent namespace (design 2026-11-07): every mode of the one agent publishes the steps it
++ * takes into it (agent.step.started|updated|ended), and OpenVibe.Actor keeps the one timeline behind
++ * the Dock. Sources still own their own namespace first; a shared prefix never replaces `<source>.`.
++ * Developer apps can never publish a shared prefix. EVENTS_SHARED_PREFIXES (comma-separated)
++ * replaces the default.
*/
--const DEFAULT_SHARED_PREFIXES = ['provider.', 'share.'];
++const DEFAULT_SHARED_PREFIXES = ['provider.', 'share.', 'agent.'];
++
function sourcePrefixes(env) {
let map = Object.fromEntries(DEFAULT_SOURCES.map(s => [s, [`${namespaceOf(s)}.`]]));
ddiff --git a/test/publish.test.js b/test/publish.test.js
iindex 2acc2e0..c0e07b8 100644
---- a/test/publish.test.js
++++ b/test/publish.test.js
@@@ -10,6 +10,26 @@ const live = serviceToken('live', ['events.event.publish']);
t('boot', async () => { h = await boot(); });
++t('the network agent namespace: any first-party service may publish agent.step.*', async () => {
++ const codes = serviceToken('codes', ['events.event.publish']);
++ const r = await request(h.base, 'POST', '/api/v1/events', {
++ token: codes,
++ body: envelope('codes', { event_type: 'agent.step.started', subject: { type: 'user', id: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPS' }, payload: { step_id: 'tsk_01JAB2C3D4E5F6G7H8J9K0MNPS' } }),
++ });
++ assert.strictEqual(r.status, 201, r.text);
++ // A developer app never publishes into a network-wide namespace.
++ const appId = ids.newId('app');
++ const appUlid = appId.split('_')[1].toLowerCase();
++ const app = await request(h.base, 'POST', '/api/v1/events', {
++ token: appToken({ appId, cap: ['events.app.publish'] }),
++ body: envelope('app', { source: `app-${appUlid}`, actor: { type: 'app', id: appId }, event_type: 'agent.step.started' }),
++ });
++ assert.strictEqual(app.status, 403, JSON.stringify(app.body));
++ assert.strictEqual(app.body.code, 'events.type_not_allowed', JSON.stringify(app.body));
++ });
++
++
++
t('no token -> 401, wrong audience -> 401, missing capability -> 403', async () => {
let r = await request(h.base, 'POST', '/api/v1/events', { body: envelope() });
assert.strictEqual(r.status, 401);
ddiff --git a/test/pull-health.test.js b/test/pull-health.test.js
iindex fa419ae..4e9748f 100644
---- a/test/pull-health.test.js
++++ b/test/pull-health.test.js
@@@ -43,7 +43,8 @@ t('config: every source prefix must start with the source', () => {
});
t('config: network-wide prefixes are shared, source namespaces stay owned', () => {
-- assert.deepStrictEqual(load({}).sharedPrefixes, ['provider.', 'share.']);
++ assert.deepStrictEqual(load({}).sharedPrefixes, ['provider.', 'share.', 'agent.']);
++
const c = load({ EVENTS_SHARED_PREFIXES: 'provider.,storage.' });
assert.deepStrictEqual(c.sharedPrefixes, ['provider.', 'storage.']);
assert.throws(() => load({ EVENTS_SHARED_PREFIXES: 'provider' }), /bad prefix/);