@ours.network/codex 0.17.0-nightly.9 → 0.17.0
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.
- package/.codex-plugin/plugin.json +1 -1
- package/bin/proxy.mjs +3 -2
- package/dist/monitor-mcp.mjs +2 -3
- package/package.json +2 -2
- package/skills/ours/SKILL.md +43 -21
- package/skills/ours/references/configuration.md +0 -5
- package/src/hooks/runner.mjs +3 -14
- package/src/monitor-mcp.mjs +3 -3
- package/src/profile.mjs +6 -145
package/bin/proxy.mjs
CHANGED
|
@@ -24,7 +24,8 @@ const env = { ...process.env };
|
|
|
24
24
|
if (env.OURS_CODEX_LIVE === '1') env.OURS_AUTOSTART = '0';
|
|
25
25
|
if (process.ppid > 1) env.OURS_CLIENT_PID = String(process.ppid);
|
|
26
26
|
const child = cliPath
|
|
27
|
-
? spawn(process.execPath, [cliPath, 'proxy',
|
|
28
|
-
: spawn('ours-mcp', ['proxy',
|
|
27
|
+
? spawn(process.execPath, [cliPath, 'proxy', ...process.argv.slice(2)], { stdio: 'inherit', env })
|
|
28
|
+
: spawn('ours-mcp', ['proxy', ...process.argv.slice(2)], { stdio: 'inherit', env });
|
|
29
29
|
child.on('error', (error) => { process.stderr.write(`ours: cannot launch @ours.network/mcp proxy: ${error.message}\n`); process.exit(1); });
|
|
30
30
|
child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 0); });
|
|
31
|
+
|
package/dist/monitor-mcp.mjs
CHANGED
|
@@ -21156,9 +21156,9 @@ var foregroundOffer = (identity) => `IMPORTANT: Before offering the fallback, te
|
|
|
21156
21156
|
function foregroundWatchProcess(identity) {
|
|
21157
21157
|
try {
|
|
21158
21158
|
const cliPath = require2.resolve("@ours.network/mcp/dist/cli.js");
|
|
21159
|
-
return { command: process.execPath, args: [cliPath, "watch",
|
|
21159
|
+
return { command: process.execPath, args: [cliPath, "watch", identity] };
|
|
21160
21160
|
} catch {
|
|
21161
|
-
return { command: "ours-mcp", args: ["watch",
|
|
21161
|
+
return { command: "ours-mcp", args: ["watch", identity] };
|
|
21162
21162
|
}
|
|
21163
21163
|
}
|
|
21164
21164
|
async function handleMonitorCommand(command, args = {}, { env = process.env, send = sendControlCommand } = {}) {
|
|
@@ -21268,7 +21268,6 @@ async function runMonitorMcp() {
|
|
|
21268
21268
|
}
|
|
21269
21269
|
export {
|
|
21270
21270
|
createMonitorMcpServer,
|
|
21271
|
-
foregroundWatchProcess,
|
|
21272
21271
|
handleMonitorCommand,
|
|
21273
21272
|
monitorToolNames,
|
|
21274
21273
|
runMonitorMcp,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/codex",
|
|
3
|
-
"version": "0.17.0
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Native Codex plugin for secure ours.network messaging and explicitly armed, session-scoped live mail wake.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
-
"@ours.network/mcp": "0.17.0
|
|
49
|
+
"@ours.network/mcp": "0.17.0",
|
|
50
50
|
"ws": "^8.21.0",
|
|
51
51
|
"zod": "^3.25.76"
|
|
52
52
|
},
|
package/skills/ours/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ours
|
|
3
|
-
description: Use when the user wants to set up or configure ours or ours-fleet, onboard onto the ours network, create or switch an identity, connect with another agent or person, exchange encrypted messages or files, check incoming mail, arm live monitoring, or spawn/configure/oversee a persistent or temporary fleet agent. Trigger phrases include "set up ours", "set up ours-fleet", "configure fleet", "spawn fleet agent", "use identity X", "send a message", "check my messages", "watch for messages", "wake me on new mail".
|
|
3
|
+
description: Use when the user wants to set up or configure ours or ours-fleet, onboard onto the ours network, create or switch an identity, connect with another agent or person, exchange encrypted messages or files, check incoming mail, arm live monitoring, bind a web-messenger control proxy, or spawn/configure/oversee a persistent or temporary fleet agent. Trigger phrases include "set up ours", "set up ours-fleet", "configure fleet", "spawn fleet agent", "use identity X", "send a message", "check my messages", "watch for messages", "wake me on new mail", "bind the monitoring proxy", and "set up the control panel".
|
|
4
4
|
metadata:
|
|
5
5
|
codex:
|
|
6
6
|
tags: [ours, ours.network, a2a, adapt, e2e, messaging, identity]
|
|
@@ -23,9 +23,8 @@ are three surfaces:
|
|
|
23
23
|
|
|
24
24
|
- **Layer 1 — identities** (global): create / bind / switch the identity you act as.
|
|
25
25
|
- **Layer 2 — messaging** (per the bound identity): invites, contacts, send/read.
|
|
26
|
-
- **Control plane** (the host's **Human identity**): a human's web-messenger
|
|
27
|
-
**monitoring & control proxy**
|
|
28
|
-
its MCP tools were removed; see "Control plane" below before offering anything.
|
|
26
|
+
- **Control plane** (the host's **Human identity**): bind a human's web-messenger as a
|
|
27
|
+
**monitoring & control proxy** that can oversee and command a fleet of agents.
|
|
29
28
|
|
|
30
29
|
Identities come in exactly two kinds, in a fixed order:
|
|
31
30
|
|
|
@@ -104,9 +103,8 @@ Walk the user through these, checking each. Stop and help at the first one that
|
|
|
104
103
|
through `ours-codex`. After an identity is bound, ask whether to arm it. Call
|
|
105
104
|
`arm_monitor({ identity })` only after an explicit yes. Standard mode remains fully
|
|
106
105
|
usable for manual `get_messages` checks.
|
|
107
|
-
6. **Oversight.** If they
|
|
108
|
-
**control-plane monitoring proxy
|
|
109
|
-
to call. See "Control plane" below.
|
|
106
|
+
6. **(Optional) Oversight.** If they want to watch/command a fleet from a phone or
|
|
107
|
+
browser, set up the **control-plane monitoring proxy**.
|
|
110
108
|
|
|
111
109
|
- **Configuration.** Port, state dir, broker, and GC interval are configurable.
|
|
112
110
|
More than one daemon may run when each uses a distinct port and state directory.
|
|
@@ -391,21 +389,45 @@ earlier; `monitor_status` reports availability and the armed identity. A foregro
|
|
|
391
389
|
monitor is a blocking tool call, so the session cannot accept another prompt until mail
|
|
392
390
|
arrives or the user presses Escape.
|
|
393
391
|
|
|
394
|
-
## Control plane — human oversight of a fleet
|
|
392
|
+
## Control plane — bind a monitoring proxy (human oversight of a fleet)
|
|
393
|
+
|
|
394
|
+
This is **separate** from the wake-on-mail watch above. The control plane lets a **person's
|
|
395
|
+
web-messenger account** (the ours web messenger, shipping as part of the upcoming ours-control-plane)
|
|
396
|
+
oversee and command all agents under this host's **Human identity** from a **Control
|
|
397
|
+
Panel**: view a **live monitoring feed** of monitored agents' traffic, create agents, edit
|
|
398
|
+
their bios **and personas**, toggle each agent's monitoring, open a chat with any agent (the
|
|
399
|
+
Human identity commands the agent to mint an invite — no out-of-band step), and remove agents. A
|
|
400
|
+
coordinator can also set a worker's local persona via the cluster; the agent still asks the
|
|
401
|
+
user before adopting it. All of it rides the same
|
|
402
|
+
e2e channels as messages but in a separate control queue agents never see; monitoring bodies
|
|
403
|
+
are never written to disk on the host.
|
|
404
|
+
|
|
405
|
+
**Prerequisites**
|
|
406
|
+
- The **Human identity** exists (`create_root_identity` — the onboarding step). The
|
|
407
|
+
proxy binds to the Human identity.
|
|
408
|
+
- The messenger account is already a **contact of the Human identity** — do the normal
|
|
409
|
+
invite exchange first: bind the Human identity, `generate_invite`, and have the
|
|
410
|
+
messenger redeem it (or redeem the messenger's invite with `add_contact`).
|
|
411
|
+
|
|
412
|
+
**Binding ceremony (6-digit code, out-of-band)**
|
|
413
|
+
1. "bind my messenger account as the monitoring proxy" →
|
|
414
|
+
`bind_monitoring_proxy({ contact: "<the messenger contact>" })`. This automatically
|
|
415
|
+
targets the host's Human identity (you do **not** need to be bound as it). It returns a
|
|
416
|
+
**6-digit code** (valid 5 minutes, 3 attempts) and shows it **here**.
|
|
417
|
+
2. **Read the code to the user.** They open the messenger → the conversation with the Human identity →
|
|
418
|
+
**Control Panel** → enter the code. The code must travel **out-of-band** — reading it off
|
|
419
|
+
this terminal is what proves you control both ends. **Never send the code over ours.**
|
|
420
|
+
3. On success the contact becomes the proxy. Confirm with `get_monitoring_status`.
|
|
421
|
+
|
|
422
|
+
**Per-agent monitoring is controller-gated.** Once a proxy is bound, the proxy (Control
|
|
423
|
+
Panel) turns an agent's monitoring on/off — there is **no local enable/disable tool**. A
|
|
424
|
+
monitored agent reports a signed copy of every message it sends/receives to the Human
|
|
425
|
+
identity's node, which forwards it to the proxy's feed.
|
|
426
|
+
|
|
427
|
+
**Status** — "what's the monitoring/control state" → `get_monitoring_status()` reports the
|
|
428
|
+
Human identity's bound proxy (if any), a pending code verification, queued copies/control
|
|
429
|
+
requests, and each agent's monitoring ON/off. Works whenever the Human identity exists.
|
|
395
430
|
|
|
396
|
-
**NOT AVAILABLE IN THIS RELEASE. Do not offer it, and do not call a tool for it.**
|
|
397
|
-
The `bind_monitoring_proxy` and `get_monitoring_status` MCP tools were removed with the
|
|
398
|
-
daemon-side control plane; there is no tool behind them and a call will fail. Nothing has
|
|
399
|
-
replaced them yet.
|
|
400
|
-
|
|
401
|
-
The capability itself is not cancelled: the monitoring/control surface remains in the
|
|
402
|
-
**protocol core**, untouched, for whenever it is reimplemented. What is gone is this
|
|
403
|
-
plugin's exposure of it as MCP tools.
|
|
404
|
-
|
|
405
|
-
If a user asks to bind a web-messenger account as a monitoring/control proxy, to open a
|
|
406
|
-
Control Panel, or to check monitoring status — say plainly that it is not available in this
|
|
407
|
-
release, and do not improvise a substitute. Per-identity wake-on-mail is a **different**
|
|
408
|
-
feature and still works; it is described above.
|
|
409
431
|
## Notes
|
|
410
432
|
|
|
411
433
|
- Identities and their state (contacts, inbox, keys) persist under the selected daemon's
|
|
@@ -18,11 +18,6 @@ queries `/info`, verifies the authenticated notification API, and propagates tha
|
|
|
18
18
|
profile to the plugin, hooks, and watcher. It never starts or changes the daemon. A stopped
|
|
19
19
|
or incompatible selected daemon is an error.
|
|
20
20
|
|
|
21
|
-
With no explicit `--ours-port`, `OURS_PORT`, `OURS_CONFIG`, or `OURS_STATE_DIR`, both standard
|
|
22
|
-
Codex and `ours-codex` use Codex's association in `~/.ours/installer-profiles.json` before the
|
|
23
|
-
historical default config. The selected config and daemon `/info.stateDir` must match the
|
|
24
|
-
registry; drift/auth failures stop with rerun-Nightly guidance. The registry never stores tokens.
|
|
25
|
-
|
|
26
21
|
Changing daemon configuration is separate operator work. Explain the impact and obtain
|
|
27
22
|
explicit consent before editing or restarting anything. A changed state directory selects a
|
|
28
23
|
different identity store. Use a distinct `OURS_CONFIG`, port, and state directory for a
|
package/src/hooks/runner.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { sendControlCommand } from '../control-server.mjs';
|
|
6
|
-
import { resolveDaemonProfile } from '../profile.mjs';
|
|
7
6
|
|
|
8
7
|
async function defaultFindPin(cwd) {
|
|
9
8
|
let dir = resolve(cwd || process.cwd());
|
|
@@ -63,19 +62,8 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
|
|
|
63
62
|
if (event === 'SessionStart' && socket && capability && payload.session_id && payload.cwd) {
|
|
64
63
|
await send(socket, capability, { command: 'register_session', sessionId: payload.session_id, threadId: payload.session_id, cwd: payload.cwd });
|
|
65
64
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Standard Codex does not pass through ours-codex's resolved environment.
|
|
69
|
-
// Resolve its registry association here too so SessionStart/UserPromptSubmit
|
|
70
|
-
// inspect the same daemon as the stdio proxy. Hook failures remain benign.
|
|
71
|
-
if (!env.OURS_PORT && !env.OURS_CONFIG && !env.OURS_STATE_DIR) {
|
|
72
|
-
try {
|
|
73
|
-
const selected = await resolveDaemonProfile({ env, fetch: fetchImpl });
|
|
74
|
-
port = String(selected.port);
|
|
75
|
-
selectedToken = selected.token || '';
|
|
76
|
-
} catch { /* proxy/launcher owns diagnostics; hooks emit a benign no-op */ }
|
|
77
|
-
}
|
|
78
|
-
const headers = selectedToken ? { 'x-ours-api-token': selectedToken } : {};
|
|
65
|
+
const port = env.OURS_PORT || '3050';
|
|
66
|
+
const headers = env.OURS_API_TOKEN ? { 'x-ours-api-token': env.OURS_API_TOKEN } : {};
|
|
79
67
|
let unread = [];
|
|
80
68
|
try {
|
|
81
69
|
const response = await fetchImpl(`http://127.0.0.1:${port}/unread`, { headers, signal: AbortSignal.timeout(1500) });
|
|
@@ -95,3 +83,4 @@ async function main() {
|
|
|
95
83
|
}
|
|
96
84
|
|
|
97
85
|
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) main();
|
|
86
|
+
|
package/src/monitor-mcp.mjs
CHANGED
|
@@ -20,12 +20,12 @@ const foregroundOffer = (identity) =>
|
|
|
20
20
|
`Only after an explicit yes, call get_messages once to drain existing unread mail, then ` +
|
|
21
21
|
`call foreground_monitor({ identity: ${JSON.stringify(identity)} }).`;
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
function foregroundWatchProcess(identity) {
|
|
24
24
|
try {
|
|
25
25
|
const cliPath = require.resolve('@ours.network/mcp/dist/cli.js');
|
|
26
|
-
return { command: process.execPath, args: [cliPath, 'watch',
|
|
26
|
+
return { command: process.execPath, args: [cliPath, 'watch', identity] };
|
|
27
27
|
} catch {
|
|
28
|
-
return { command: 'ours-mcp', args: ['watch',
|
|
28
|
+
return { command: 'ours-mcp', args: ['watch', identity] };
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
package/src/profile.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import {
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
4
|
|
|
5
5
|
const DEFAULT_PORT = 3050;
|
|
6
6
|
|
|
@@ -31,148 +31,17 @@ async function readJson(path) {
|
|
|
31
31
|
try { return JSON.parse(await readFile(path, 'utf8')); } catch { return {}; }
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async function readRequiredJson(path, kind) {
|
|
35
|
-
let text;
|
|
36
|
-
try { text = await readFile(path, 'utf8'); }
|
|
37
|
-
catch (error) { throw rerun(`cannot read ${kind} ${path}: ${error.message}`); }
|
|
38
|
-
try {
|
|
39
|
-
const value = JSON.parse(text);
|
|
40
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('expected an object');
|
|
41
|
-
return value;
|
|
42
|
-
} catch (error) { throw rerun(`${kind} ${path} is corrupt JSON: ${error.message}`); }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function readRegistryJson(path) {
|
|
46
|
-
let text;
|
|
47
|
-
try { text = await readFile(path, 'utf8'); }
|
|
48
|
-
catch (error) {
|
|
49
|
-
if (error?.code === 'ENOENT') return null;
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
try { return JSON.parse(text); }
|
|
53
|
-
catch (error) { throw rerun(`registry ${path} is corrupt JSON: ${error.message}`); }
|
|
54
|
-
}
|
|
55
|
-
|
|
56
34
|
async function readOwnerToken(stateDir) {
|
|
57
35
|
try { return (await readFile(join(stateDir, 'daemon-token'), 'utf8')).trim() || null; } catch { return null; }
|
|
58
36
|
}
|
|
59
37
|
|
|
60
|
-
export class AssociationError extends Error {
|
|
61
|
-
constructor(message, code = 'INVALID_ASSOCIATION') {
|
|
62
|
-
super(`ours profile association: ${message}. Re-run the Nightly installer to repair this association`);
|
|
63
|
-
this.name = 'AssociationError';
|
|
64
|
-
this.code = code;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const rerun = (message, code) => new AssociationError(message, code);
|
|
69
|
-
const PROFILE_ID = /^[A-Za-z0-9](?:[A-Za-z0-9_-]{0,30}[A-Za-z0-9])?$/;
|
|
70
|
-
const APPLICATIONS = ['claude-code', 'codex', 'hermes'];
|
|
71
|
-
|
|
72
|
-
function associatedPath(value, profileId, field) {
|
|
73
|
-
if (typeof value !== 'string' || !isAbsolute(value) || normalize(value) !== value || value === '/') {
|
|
74
|
-
throw rerun(`profile ${profileId} has unsafe ${field}`);
|
|
75
|
-
}
|
|
76
|
-
return value;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function validateProfile(profileId, value) {
|
|
80
|
-
if (!PROFILE_ID.test(profileId)) throw rerun(`invalid profile id ${JSON.stringify(profileId)}`);
|
|
81
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw rerun(`profile ${profileId} is not an object`);
|
|
82
|
-
for (const key of Object.keys(value)) {
|
|
83
|
-
if (!['label', 'host', 'port', 'configPath', 'stateDir', 'serviceName', 'ownership'].includes(key)) {
|
|
84
|
-
throw rerun(`profile ${profileId} contains unsupported field ${JSON.stringify(key)}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (typeof value.label !== 'string' || !value.label.trim()) throw rerun(`profile ${profileId} has no label`);
|
|
88
|
-
const host = String(value.host || '').toLowerCase();
|
|
89
|
-
if (host !== 'localhost' && host !== '127.0.0.1') throw rerun(`profile ${profileId} uses non-loopback host ${JSON.stringify(value.host)}`);
|
|
90
|
-
if (typeof value.port !== 'number') throw rerun(`profile ${profileId} has invalid port`);
|
|
91
|
-
let port;
|
|
92
|
-
try { port = validPort(value.port); }
|
|
93
|
-
catch { throw rerun(`profile ${profileId} has invalid port`); }
|
|
94
|
-
const configPath = associatedPath(value.configPath, profileId, 'configPath');
|
|
95
|
-
const stateDir = associatedPath(value.stateDir, profileId, 'stateDir');
|
|
96
|
-
const serviceName = typeof value.serviceName === 'string' ? value.serviceName.trim() : '';
|
|
97
|
-
if ((profileId !== 'default' && !serviceName) || (serviceName && !PROFILE_ID.test(serviceName))) {
|
|
98
|
-
throw rerun(`profile ${profileId} has invalid serviceName`);
|
|
99
|
-
}
|
|
100
|
-
const ownership = value.ownership;
|
|
101
|
-
if (!ownership || typeof ownership !== 'object' || Array.isArray(ownership)) throw rerun(`profile ${profileId} has no explicit ownership`);
|
|
102
|
-
for (const key of Object.keys(ownership)) if (!['config', 'service', 'state'].includes(key)) throw rerun(`profile ${profileId} ownership contains unsupported field ${JSON.stringify(key)}`);
|
|
103
|
-
if (typeof ownership.config !== 'boolean' || typeof ownership.service !== 'boolean' || typeof ownership.state !== 'boolean') {
|
|
104
|
-
throw rerun(`profile ${profileId} has invalid ownership`);
|
|
105
|
-
}
|
|
106
|
-
return { ...value, label: value.label.trim(), host: '127.0.0.1', port, configPath, stateDir, serviceName };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function associatedProfile(registry, application) {
|
|
110
|
-
if (!registry || typeof registry !== 'object' || Array.isArray(registry)) throw rerun('registry is not an object');
|
|
111
|
-
for (const key of Object.keys(registry)) if (!['version', 'profiles', 'harnessAssociations'].includes(key)) throw rerun(`registry contains unsupported field ${JSON.stringify(key)}`);
|
|
112
|
-
if (registry.version !== 1) throw rerun(`unsupported registry version ${JSON.stringify(registry.version)}`);
|
|
113
|
-
if (!registry.profiles || typeof registry.profiles !== 'object' || Array.isArray(registry.profiles) ||
|
|
114
|
-
!registry.harnessAssociations || typeof registry.harnessAssociations !== 'object' || Array.isArray(registry.harnessAssociations)) {
|
|
115
|
-
throw rerun('registry profiles and harnessAssociations must be objects');
|
|
116
|
-
}
|
|
117
|
-
const profiles = Object.fromEntries(Object.entries(registry.profiles).map(([id, value]) => [id, validateProfile(id, value)]));
|
|
118
|
-
const profileEntries = Object.entries(profiles);
|
|
119
|
-
for (let index = 0; index < profileEntries.length; index += 1) {
|
|
120
|
-
const [id, profile] = profileEntries[index];
|
|
121
|
-
for (const [otherId, other] of profileEntries.slice(index + 1)) {
|
|
122
|
-
const collision = profile.port === other.port ? 'endpoint'
|
|
123
|
-
: resolve(profile.stateDir) === resolve(other.stateDir) ? 'stateDir'
|
|
124
|
-
: profile.configPath === other.configPath ? 'configPath'
|
|
125
|
-
: profile.serviceName === other.serviceName ? 'serviceName' : '';
|
|
126
|
-
if (collision) throw rerun(`profiles ${id} and ${otherId} collide on ${collision}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
for (const [key, value] of Object.entries(registry.harnessAssociations)) {
|
|
130
|
-
if (!APPLICATIONS.includes(key) || typeof value !== 'string' || !profiles[value]) {
|
|
131
|
-
throw rerun(`invalid association ${JSON.stringify(key)} -> ${JSON.stringify(value)}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
const profileId = registry.harnessAssociations[application];
|
|
135
|
-
if (profileId == null) return null;
|
|
136
|
-
return { profileId, ...profiles[profileId] };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
38
|
export async function resolveDaemonProfile({ argv = [], env = process.env, readConfig, fetch: fetchImpl = globalThis.fetch } = {}) {
|
|
140
39
|
const parsed = parseOursArgs(argv);
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const explicitPort = parsed.port != null || (env.OURS_PORT != null && env.OURS_PORT !== '');
|
|
144
|
-
const explicitConfig = !!env.OURS_CONFIG;
|
|
145
|
-
const explicitState = !!env.OURS_STATE_DIR;
|
|
146
|
-
let association = null;
|
|
147
|
-
if (!explicitPort && !explicitConfig && !explicitState) {
|
|
148
|
-
const registryPath = env.OURS_INSTALL_PROFILES || join(home, '.ours', 'installer-profiles.json');
|
|
149
|
-
if (env.OURS_INSTALL_PROFILES && (!isAbsolute(registryPath) || normalize(registryPath) !== registryPath || registryPath === '/')) {
|
|
150
|
-
throw rerun('OURS_INSTALL_PROFILES must be a normalized absolute path');
|
|
151
|
-
}
|
|
152
|
-
let registry;
|
|
153
|
-
try { registry = await readRegistryJson(registryPath); }
|
|
154
|
-
catch (error) {
|
|
155
|
-
if (String(error?.message || '').startsWith('ours profile association:')) throw error;
|
|
156
|
-
throw rerun(`cannot read registry ${registryPath}: ${error?.message || error}`);
|
|
157
|
-
}
|
|
158
|
-
if (registry) association = associatedProfile(registry, 'codex');
|
|
159
|
-
}
|
|
160
|
-
const configPath = env.OURS_CONFIG || association?.configPath || defaultConfigPath;
|
|
161
|
-
const config = await (readConfig ? readConfig(configPath) : association
|
|
162
|
-
? readRequiredJson(configPath, `associated config for ${association.profileId}`)
|
|
163
|
-
: readJson(configPath));
|
|
40
|
+
const configPath = env.OURS_CONFIG || join(env.HOME || homedir(), '.ours', 'config.json');
|
|
41
|
+
const config = await (readConfig ? readConfig(configPath) : readJson(configPath));
|
|
164
42
|
let source = 'default';
|
|
165
43
|
let port = DEFAULT_PORT;
|
|
166
|
-
if (
|
|
167
|
-
port = validPort(config.port);
|
|
168
|
-
source = env.OURS_CONFIG ? 'OURS_CONFIG' : association ? 'registry' : 'config';
|
|
169
|
-
}
|
|
170
|
-
if (association) {
|
|
171
|
-
const configState = resolve(typeof config?.stateDir === 'string' ? config.stateDir : join(home, '.ours'));
|
|
172
|
-
if (port !== association.port || configState !== resolve(association.stateDir)) {
|
|
173
|
-
throw rerun(`profile ${association.profileId} disagrees with ${configPath} on port/stateDir`, 'CONFIG_DRIFT');
|
|
174
|
-
}
|
|
175
|
-
}
|
|
44
|
+
if (config?.port != null) { port = validPort(config.port); source = env.OURS_CONFIG ? 'OURS_CONFIG' : 'config'; }
|
|
176
45
|
if (env.OURS_PORT != null && env.OURS_PORT !== '') { port = validPort(env.OURS_PORT); source = 'OURS_PORT'; }
|
|
177
46
|
if (parsed.port != null) { port = parsed.port; source = '--ours-port'; }
|
|
178
47
|
|
|
@@ -188,14 +57,7 @@ export async function resolveDaemonProfile({ argv = [], env = process.env, readC
|
|
|
188
57
|
throw new Error(`incompatible service on port ${port}; expected an ours daemon with notification protocol 1`);
|
|
189
58
|
}
|
|
190
59
|
const stateDir = resolve(info.stateDir);
|
|
191
|
-
const
|
|
192
|
-
if (expectedStateDir && stateDir !== expectedStateDir) {
|
|
193
|
-
throw association
|
|
194
|
-
? rerun(`profile ${association.profileId} expects stateDir ${expectedStateDir}, daemon reports ${stateDir}`)
|
|
195
|
-
: new Error(`ours daemon stateDir mismatch: expected ${expectedStateDir}, daemon reports ${stateDir}`);
|
|
196
|
-
}
|
|
197
|
-
const selectedStateDir = expectedStateDir || stateDir;
|
|
198
|
-
const token = env.OURS_API_TOKEN?.trim() || config?.apiToken?.trim() || await readOwnerToken(selectedStateDir);
|
|
60
|
+
const token = env.OURS_API_TOKEN?.trim() || config?.apiToken?.trim() || await readOwnerToken(stateDir);
|
|
199
61
|
const headers = token ? { 'x-ours-api-token': token } : {};
|
|
200
62
|
let capability;
|
|
201
63
|
try { capability = await fetchImpl(`${baseUrl}/identities`, { headers, signal: AbortSignal.timeout(2000) }); }
|
|
@@ -209,9 +71,8 @@ export async function resolveDaemonProfile({ argv = [], env = process.env, readC
|
|
|
209
71
|
if (!unread.ok) throw new Error(`selected daemon lacks the body-free unread API (HTTP ${unread.status}); install the testing daemon build and restart that daemon explicitly`);
|
|
210
72
|
|
|
211
73
|
return {
|
|
212
|
-
port, stateDir
|
|
74
|
+
port, stateDir, token: token || null,
|
|
213
75
|
visibility: env.OURS_API_VISIBILITY || config?.apiVisibility || 'owner',
|
|
214
76
|
source, info, baseUrl, configPath, codexArgs: parsed.codexArgs,
|
|
215
|
-
...(association ? { profileId: association.profileId, association } : {}),
|
|
216
77
|
};
|
|
217
78
|
}
|