@ours.network/codex 0.17.0-nightly.3 → 0.17.0-nightly.5
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 +2 -3
- package/dist/monitor-mcp.mjs +3 -2
- package/package.json +2 -2
- package/skills/ours/references/configuration.md +5 -0
- package/src/hooks/runner.mjs +14 -3
- package/src/monitor-mcp.mjs +3 -3
- package/src/profile.mjs +145 -6
package/bin/proxy.mjs
CHANGED
|
@@ -24,8 +24,7 @@ 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', ...process.argv.slice(2)], { stdio: 'inherit', env })
|
|
28
|
-
: spawn('ours-mcp', ['proxy', ...process.argv.slice(2)], { stdio: 'inherit', env });
|
|
27
|
+
? spawn(process.execPath, [cliPath, 'proxy', '--application', 'codex', ...process.argv.slice(2)], { stdio: 'inherit', env })
|
|
28
|
+
: spawn('ours-mcp', ['proxy', '--application', 'codex', ...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", identity] };
|
|
21159
|
+
return { command: process.execPath, args: [cliPath, "watch", "--application", "codex", identity] };
|
|
21160
21160
|
} catch {
|
|
21161
|
-
return { command: "ours-mcp", args: ["watch", identity] };
|
|
21161
|
+
return { command: "ours-mcp", args: ["watch", "--application", "codex", identity] };
|
|
21162
21162
|
}
|
|
21163
21163
|
}
|
|
21164
21164
|
async function handleMonitorCommand(command, args = {}, { env = process.env, send = sendControlCommand } = {}) {
|
|
@@ -21268,6 +21268,7 @@ async function runMonitorMcp() {
|
|
|
21268
21268
|
}
|
|
21269
21269
|
export {
|
|
21270
21270
|
createMonitorMcpServer,
|
|
21271
|
+
foregroundWatchProcess,
|
|
21271
21272
|
handleMonitorCommand,
|
|
21272
21273
|
monitorToolNames,
|
|
21273
21274
|
runMonitorMcp,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/codex",
|
|
3
|
-
"version": "0.17.0-nightly.
|
|
3
|
+
"version": "0.17.0-nightly.5",
|
|
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-nightly.
|
|
49
|
+
"@ours.network/mcp": "0.17.0-nightly.5",
|
|
50
50
|
"ws": "^8.21.0",
|
|
51
51
|
"zod": "^3.25.76"
|
|
52
52
|
},
|
|
@@ -18,6 +18,11 @@ 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
|
+
|
|
21
26
|
Changing daemon configuration is separate operator work. Explain the impact and obtain
|
|
22
27
|
explicit consent before editing or restarting anything. A changed state directory selects a
|
|
23
28
|
different identity store. Use a distinct `OURS_CONFIG`, port, and state directory for a
|
package/src/hooks/runner.mjs
CHANGED
|
@@ -3,6 +3,7 @@ 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';
|
|
6
7
|
|
|
7
8
|
async function defaultFindPin(cwd) {
|
|
8
9
|
let dir = resolve(cwd || process.cwd());
|
|
@@ -62,8 +63,19 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
|
|
|
62
63
|
if (event === 'SessionStart' && socket && capability && payload.session_id && payload.cwd) {
|
|
63
64
|
await send(socket, capability, { command: 'register_session', sessionId: payload.session_id, threadId: payload.session_id, cwd: payload.cwd });
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
let port = env.OURS_PORT || '3050';
|
|
67
|
+
let selectedToken = env.OURS_API_TOKEN || '';
|
|
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 } : {};
|
|
67
79
|
let unread = [];
|
|
68
80
|
try {
|
|
69
81
|
const response = await fetchImpl(`http://127.0.0.1:${port}/unread`, { headers, signal: AbortSignal.timeout(1500) });
|
|
@@ -83,4 +95,3 @@ async function main() {
|
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
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
|
-
function foregroundWatchProcess(identity) {
|
|
23
|
+
export 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', identity] };
|
|
26
|
+
return { command: process.execPath, args: [cliPath, 'watch', '--application', 'codex', identity] };
|
|
27
27
|
} catch {
|
|
28
|
-
return { command: 'ours-mcp', args: ['watch', identity] };
|
|
28
|
+
return { command: 'ours-mcp', args: ['watch', '--application', 'codex', 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 { join, resolve } from 'node:path';
|
|
3
|
+
import { isAbsolute, join, normalize, resolve } from 'node:path';
|
|
4
4
|
|
|
5
5
|
const DEFAULT_PORT = 3050;
|
|
6
6
|
|
|
@@ -31,17 +31,148 @@ 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
|
+
|
|
34
56
|
async function readOwnerToken(stateDir) {
|
|
35
57
|
try { return (await readFile(join(stateDir, 'daemon-token'), 'utf8')).trim() || null; } catch { return null; }
|
|
36
58
|
}
|
|
37
59
|
|
|
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
|
+
|
|
38
139
|
export async function resolveDaemonProfile({ argv = [], env = process.env, readConfig, fetch: fetchImpl = globalThis.fetch } = {}) {
|
|
39
140
|
const parsed = parseOursArgs(argv);
|
|
40
|
-
const
|
|
41
|
-
const
|
|
141
|
+
const home = env.HOME || homedir();
|
|
142
|
+
const defaultConfigPath = join(home, '.ours', 'config.json');
|
|
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));
|
|
42
164
|
let source = 'default';
|
|
43
165
|
let port = DEFAULT_PORT;
|
|
44
|
-
if (config?.port
|
|
166
|
+
if (typeof config?.port === 'number' && Number.isFinite(config.port)) {
|
|
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
|
+
}
|
|
45
176
|
if (env.OURS_PORT != null && env.OURS_PORT !== '') { port = validPort(env.OURS_PORT); source = 'OURS_PORT'; }
|
|
46
177
|
if (parsed.port != null) { port = parsed.port; source = '--ours-port'; }
|
|
47
178
|
|
|
@@ -57,7 +188,14 @@ export async function resolveDaemonProfile({ argv = [], env = process.env, readC
|
|
|
57
188
|
throw new Error(`incompatible service on port ${port}; expected an ours daemon with notification protocol 1`);
|
|
58
189
|
}
|
|
59
190
|
const stateDir = resolve(info.stateDir);
|
|
60
|
-
const
|
|
191
|
+
const expectedStateDir = env.OURS_STATE_DIR ? resolve(env.OURS_STATE_DIR) : association ? resolve(association.stateDir) : null;
|
|
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);
|
|
61
199
|
const headers = token ? { 'x-ours-api-token': token } : {};
|
|
62
200
|
let capability;
|
|
63
201
|
try { capability = await fetchImpl(`${baseUrl}/identities`, { headers, signal: AbortSignal.timeout(2000) }); }
|
|
@@ -71,8 +209,9 @@ export async function resolveDaemonProfile({ argv = [], env = process.env, readC
|
|
|
71
209
|
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`);
|
|
72
210
|
|
|
73
211
|
return {
|
|
74
|
-
port, stateDir, token: token || null,
|
|
212
|
+
port, stateDir: selectedStateDir, token: token || null,
|
|
75
213
|
visibility: env.OURS_API_VISIBILITY || config?.apiVisibility || 'owner',
|
|
76
214
|
source, info, baseUrl, configPath, codexArgs: parsed.codexArgs,
|
|
215
|
+
...(association ? { profileId: association.profileId, association } : {}),
|
|
77
216
|
};
|
|
78
217
|
}
|