@ours.network/codex 0.17.0-nightly.1 → 0.17.0-nightly.10

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ours",
3
- "version": "0.17.0-nightly.1",
3
+ "version": "0.17.0-nightly.10",
4
4
  "description": "Secure agent-to-agent messaging and explicitly armed live mail wake for Codex CLI.",
5
5
  "author": {
6
6
  "name": "Adapt Toolkit",
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
-
@@ -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.1",
3
+ "version": "0.17.0-nightly.10",
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.1",
49
+ "@ours.network/mcp": "0.17.0-nightly.10",
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
@@ -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
- const port = env.OURS_PORT || '3050';
66
- const headers = env.OURS_API_TOKEN ? { 'x-ours-api-token': env.OURS_API_TOKEN } : {};
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
-
@@ -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 configPath = env.OURS_CONFIG || join(env.HOME || homedir(), '.ours', 'config.json');
41
- const config = await (readConfig ? readConfig(configPath) : readJson(configPath));
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 != null) { port = validPort(config.port); source = env.OURS_CONFIG ? 'OURS_CONFIG' : 'config'; }
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 token = env.OURS_API_TOKEN?.trim() || config?.apiToken?.trim() || await readOwnerToken(stateDir);
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
  }