openvibers/openvibe.network Commits
A git OAuth client for OpenVibe.Git, and Codes' grants on it (#723)
Changes
@@@ -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.@@@ -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', []]),@@@ -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 = ?')nnew file mode 100644@@@ -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');
@@ -186,7 +186,11 @@ async function seedDb(db, { log = console } = {}) {
@@ -186,7 +186,11 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'zone', name: 'OpenVibe.Zone', redirect_uris: ['https://openvibe.zone/auth/callback'] },
{ 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
// 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.
// 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'] },
{ 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
// 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);
// 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.
// its three grants are in server/identity/principals.js, exactly one per service, never a wildcard.
@@ -510,6 +510,14 @@ const DEFAULT_GRANTS = [
@@ -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', []]),
...['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).
// Plan 2026-10-28 §6: Codes announces its coding tasks' state changes (codes.task.*, contracts 0.232.0).
['codes', 'events.event.publish', 'openvibe.events', []],
['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
// 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.
// 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', []]),
...['codes.task.start', 'codes.task.read', 'codes.task.cancel'].map((c) => ['actor', c, 'openvibe.codes', []]),
@@ -24,6 +24,11 @@ const { getDb, seedDb } = require('../server/db/database');
@@ -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');
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.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');
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);
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'));
assert.ok((await uris()).includes('https://openvibe.my/inventory/auth/callback'));
await db.prepare('UPDATE oauth_clients SET redirect_uris = ? WHERE client_id = ?')
await db.prepare('UPDATE oauth_clients SET redirect_uris = ? WHERE client_id = ?')
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -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');