openvibers/openvibe.network Commits

A git OAuth client for OpenVibe.Git, and Codes' grants on it (#723)

OP OpenVibers committed 4 days ago · e2b7415391 · 4 files, +45 −0

* A git OAuth client, and Codes' grants on OpenVibe.Git

git.openvibe.codes becomes OpenVibe's own site (OpenVibers/OpenVibe-Git)
and signs people in with OpenVibe: the git client carries the one
callback on that host. Forgejo's client stays until the cut-over, when
Opus revokes it.

Codes holds git.read and git.write for the openvibe.git audience, the
capability names OpenVibe.Git verifies, so it can act for the person
signed in. The tests check the client seed and that no other service or
audience holds those names yet.

* Hold the Git grants to the Contracts version that defines them

git.repo.read and git.repo.write arrive in Contracts 0.371.0 (OpenVibe.Contracts#387). Until this Network pins that version they are not in the catalog, so the seed adds the grants only when the capabilities are known; grants-admin's rule stays true either way.

Changes

ddiff --git a/server/db/database.js b/server/db/database.js
iindex 28846254..1ca82688 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -186,7 +186,11 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'zone', name: 'OpenVibe.Zone', redirect_uris: ['https://openvibe.zone/auth/callback'] },
// OpenVibe Git: Forgejo at openvibe.codes/git (OpenVibe.Codes deploy/forgejo, WS-X12), signing in through OpenID
// Connect; its callback is Forgejo's /user/oauth2/<source name>/callback. git.openvibe.codes once that name exists.
++ // Kept until the cut-over to OpenVibe.Git; Opus revokes it then.
{ client_id: 'forgejo', name: 'OpenVibe Git', redirect_uris: ['https://openvibe.codes/git/user/oauth2/OpenVibe/callback', 'https://git.openvibe.codes/user/oauth2/OpenVibe/callback'] },
++ // OpenVibe.Git: the site at git.openvibe.codes (OpenVibers/OpenVibe-Git) signs people in with OpenVibe itself
++ // (openvibe-sdk/sso, the `git` client). Codes, Run and Link call its API with typed service tokens.
++ { client_id: 'git', name: 'OpenVibe Git', redirect_uris: ['https://git.openvibe.codes/auth/callback'] },
// The orchestrator (2026-11-07): the agent runs coordinator records the editorial queue's AI editor decision
// on Blog, News and Wiki as itself, never for a person. It has no redirect URIs (client_credentials only);
// its three grants are in server/identity/principals.js, exactly one per service, never a wildcard.
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 97491397..4120e3d1 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -510,6 +510,14 @@ const DEFAULT_GRANTS = [
...['run.job.submit', 'run.job.read', 'run.job.stream', 'run.job.cancel'].map((c) => ['codes', c, 'openvibe.run', []]),
// Plan 2026-10-28 §6: Codes announces its coding tasks' state changes (codes.task.*, contracts 0.232.0).
['codes', 'events.event.publish', 'openvibe.events', []],
++ // OpenVibe.Git (G1, 2026-11-09): Codes makes a repository for the person signed in, lists theirs and mints their
++ // clone token, acting as that person with X-OV-Subject. The capability names are OpenVibe.Git's own, for the
++ // openvibe.git audience only. They enter with the Contracts version that defines them (0.371.0): until the pin
++ // moves, the grants are absent rather than unknown to the catalog.
++ ...(capabilities.get('git.repo.read') && capabilities.get('git.repo.write') ? [
++ ['codes', 'git.repo.read', 'openvibe.git', []],
++ ['codes', 'git.repo.write', 'openvibe.git', []],
++ ] : []),
// Plan T16/T17 (contracts 0.162.0): a person's OpenVibe.Actor agent routes coding work to OpenVibe.Codes, which runs
// it on that person's own machine (X-OV-Subject); Codes shows Actor only the tasks Actor started for that person.
...['codes.task.start', 'codes.task.read', 'codes.task.cancel'].map((c) => ['actor', c, 'openvibe.codes', []]),
ddiff --git a/test/client-names.test.js b/test/client-names.test.js
iindex 7d423710..ccfd0e63 100644
---- a/test/client-names.test.js
++++ b/test/client-names.test.js
@@@ -24,6 +24,11 @@ const { getDb, seedDb } = require('../server/db/database');
const mail = await db.prepare('SELECT redirect_uris, is_first_party FROM oauth_clients WHERE client_id = ?').get('mail');
assert.deepStrictEqual(JSON.parse(mail.redirect_uris), ['https://openvibe.email/auth/callback'], 'the only callback is openvibe.email');
assert.strictEqual(mail.is_first_party, 1, 'the site client is first-party');
++ const git = await db.prepare('SELECT name, redirect_uris, is_first_party FROM oauth_clients WHERE client_id = ?').get('git');
++ assert.strictEqual(git.name, 'OpenVibe Git', 'the new site signs people in with OpenVibe');
++ assert.deepStrictEqual(JSON.parse(git.redirect_uris), ['https://git.openvibe.codes/auth/callback'], 'one callback on git.openvibe.codes');
++ assert.strictEqual(git.is_first_party, 1);
++ assert.ok(await db.prepare('SELECT 1 AS one FROM oauth_clients WHERE client_id = ?').get('forgejo'), 'Forgejo\'s client stays until the cut-over');
const uris = async () => JSON.parse((await db.prepare('SELECT redirect_uris FROM oauth_clients WHERE client_id = ?').get('inventory')).redirect_uris);
assert.ok((await uris()).includes('https://openvibe.my/inventory/auth/callback'));
await db.prepare('UPDATE oauth_clients SET redirect_uris = ? WHERE client_id = ?')
ddiff --git a/test/git-grants.test.js b/test/git-grants.test.js
nnew file mode 100644
iindex 00000000..0c0e1194
---- /dev/null
++++ b/test/git-grants.test.js
@@@ -0,0 +1,28 @@
++'use strict';
++/**
++ * OpenVibe.Git (G1, 2026-11-09): Codes holds git.repo.read and git.repo.write for the audience openvibe.git, so it
++ * can make a repository for the person signed in, list theirs and mint their clone token, acting as that person
++ * (X-OV-Subject). The capability names are OpenVibe.Git's own. They arrive with the Contracts version that defines
++ * them; until the pin moves, the grants are absent rather than unknown to the catalog, and this test holds the two
++ * to each other.
++ */
++const assert = require('assert');
++const { capabilities } = require('openvibe-contracts');
++const principals = require('../server/identity/principals');
++
++const CAPS = ['git.repo.read', 'git.repo.write'];
++const grant = (client, cap) => principals.DEFAULT_GRANTS.find((g) => g[0] === client && g[1] === cap && g[2] === 'openvibe.git');
++
++for (const cap of CAPS) {
++ const known = !!(capabilities.get(cap) && capabilities.get(cap).owner === 'git');
++ const g = grant('codes', cap);
++ assert.strictEqual(!!g, known, known ? `codes may ${cap} on OpenVibe Git` : `${cap} is not in this Contracts yet, so no grant names it`);
++ if (!known) continue;
++ assert.deepStrictEqual(g[3], [], `${cap} carries no namespace`);
++ assert.ok(!principals.DEFAULT_GRANTS.some((row) => row[1] === cap && row[2] !== 'openvibe.git'),
++ `${cap} is granted for OpenVibe Git and for no other audience`);
++}
++assert.deepStrictEqual(principals.DEFAULT_GRANTS.filter((g) => g[2] === 'openvibe.git').map((g) => [g[0], g[1]]),
++ CAPS.every((c) => capabilities.get(c)) ? [['codes', 'git.repo.read'], ['codes', 'git.repo.write']] : [],
++ 'Codes alone, as the first service that calls it');
++console.log('git grants: ok');