@ours.network/codex 1.1.1 → 1.2.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": "1.1.1",
3
+ "version": "1.2.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/.mcp.json CHANGED
@@ -4,14 +4,14 @@
4
4
  "command": "node",
5
5
  "args": ["bin/proxy.mjs"],
6
6
  "cwd": ".",
7
- "env_vars": ["OURS_PORT", "OURS_CONFIG", "OURS_API_TOKEN", "OURS_STATE_DIR", "OURS_AUTOSTART", "OURS_CODEX_LIVE", "OURS_CLIENT_PID"]
7
+ "env_vars": ["OURS_PORT", "OURS_CONFIG", "OURS_MCP_CONFIG", "OURS_API_TOKEN", "OURS_STATE_DIR", "OURS_AUTOSTART", "OURS_CODEX_LIVE", "OURS_CLIENT_PID"]
8
8
  },
9
9
  "ours_monitor": {
10
10
  "command": "node",
11
11
  "args": ["bin/monitor-mcp.mjs"],
12
12
  "cwd": ".",
13
13
  "tool_timeout_sec": 86400,
14
- "env_vars": ["OURS_CODEX_CONTROL_SOCKET", "OURS_CODEX_CAPABILITY", "OURS_PORT", "OURS_CONFIG", "OURS_API_TOKEN", "OURS_STATE_DIR"]
14
+ "env_vars": ["OURS_CODEX_CONTROL_SOCKET", "OURS_CODEX_CAPABILITY", "OURS_PORT", "OURS_CONFIG", "OURS_MCP_CONFIG", "OURS_API_TOKEN", "OURS_STATE_DIR"]
15
15
  }
16
16
  }
17
17
  }
package/README.md CHANGED
@@ -51,23 +51,26 @@ The launcher never starts, stops, restarts, or reconfigures the ours daemon. If
51
51
  selected daemon is absent or incompatible, it exits with an error and leaves standard
52
52
  `codex` available.
53
53
 
54
- ## Selecting a daemon
54
+ ## Selecting a gateway
55
55
 
56
- Multiple daemons may run on one host when each uses a distinct port and state directory.
57
- Selection precedence is:
58
-
59
- 1. `ours-codex --ours-port <port>`
60
- 2. `OURS_PORT`
61
- 3. the config selected by `OURS_CONFIG`
62
- 4. `~/.ours/config.json`
63
- 5. port `3050`
64
-
65
- All MCP, hooks, unread, and watcher calls inherit the same selected profile. Example:
56
+ Every client uses `~/.ours-client/profile.json`; `OURS_CONFIG` can select a complete
57
+ alternate profile. It contains `serverUrl`, `expectedInstanceId`, and `credentialPath`.
58
+ The daemon endpoint is derived as `serverUrl + /daemon`, including gateway path prefixes.
59
+ Missing, unsafe, or invalid profiles fail closed. Remove legacy port/state/token
60
+ selectors; `--ours-port` is rejected. Example after provisioning with `ours-install client`:
66
61
 
67
62
  ```sh
68
- OURS_CONFIG="$HOME/.ours/testing.json" ours-codex --ours-port 4050
63
+ OURS_CONFIG="$HOME/.ours-client/testing.json" ours-codex
69
64
  ```
70
65
 
66
+ MCP, hooks, unread and watcher requests inherit that profile. The SDK verifies the
67
+ instance and rereads the issued credential for each request. Client installation and
68
+ launch never install, start, restart or reconfigure server services.
69
+
70
+ Native ownership is keyed by Codex's actual thread ID. A normal live TUI exit forwards
71
+ that ID through the same `SessionEnd` input used by the hook. If Codex has not produced
72
+ a logical thread ID, there is no native owner for the launcher to release.
73
+
71
74
  ## Hooks and consent
72
75
 
73
76
  The native plugin bundles `hooks/hooks.json` using Codex's default hook discovery.
@@ -77,9 +80,17 @@ lifecycle directly, while the hooks add standard-mode context and defensive stat
77
80
  - `SessionStart` surfaces body-free unread metadata and an advisory `.ours-identity` pin.
78
81
  - `UserPromptSubmit` can re-surface unresolved unread/pin context.
79
82
  - `PostToolUse` records successful identity bindings and disarms on a switch.
80
- - `SessionEnd` releases the session lease and waits for all session-owned temporary
81
- roles to send best-effort removal notices and delete local state. Live mode also
82
- invokes this cleanup directly when its TUI exits, so it does not depend on hook trust.
83
+ - `SessionEnd`, when delivered, reads the native `session_id`, durably captures the
84
+ exact owner, releases its lease, and waits for all owner-scoped temporary roles to
85
+ send best-effort removal notices and delete local state. Live mode also invokes
86
+ this cleanup directly when its TUI exits.
87
+
88
+ The ours MCP owner is allocated lazily from Codex's tool-call thread metadata, so
89
+ `SessionStart` is not required for ownership and may be disabled. Stdio/MCP recycle
90
+ does not end that owner; a later call in the same unended thread recovers it. If
91
+ `SessionEnd` is disabled or never delivered, ours does not infer death from EOF,
92
+ PID, or a timeout and does not claim automatic cleanup. The owner and protected
93
+ resources remain available for ordinary resume until a terminal hook is delivered.
83
94
 
84
95
  Codex requires review and trust of the exact hook definitions before running them.
85
96
  Installation does not bypass hook trust, and live monitoring remains available when the
@@ -33,7 +33,7 @@ export function planTomlInstall(text) {
33
33
  }
34
34
 
35
35
  // Render the managed TOML block: the `ours` MCP server pointing at the globally-installed
36
- // daemon proxy (`ours-mcp proxy`). Equivalent to `codex mcp add ours -- ours-mcp proxy`.
36
+ // gateway client (`ours-mcp proxy`). Equivalent to `codex mcp add ours -- ours-mcp proxy`.
37
37
  export function renderTomlBlock() {
38
38
  return `${SENTINEL}
39
39
  # Added by @ours.network/codex install.sh. Remove this whole block to uninstall.
@@ -41,9 +41,9 @@ export function renderTomlBlock() {
41
41
  [mcp_servers.ours]
42
42
  command = "ours-mcp"
43
43
  args = ["proxy"]
44
- # Optional per-server environment overrides go here, e.g.:
44
+ # Optional whole shared gateway profile override:
45
45
  # [mcp_servers.ours.env]
46
- # OURS_PORT = "3050"
46
+ # OURS_CONFIG = "/absolute/private/path/profile.json"
47
47
  ${SENTINEL_END}
48
48
  `;
49
49
  }
@@ -0,0 +1,147 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createReadStream, createWriteStream, mkdirSync } from 'node:fs';
3
+ import { basename, dirname, resolve } from 'node:path';
4
+ import { spawn, spawnSync } from 'node:child_process';
5
+ import { pipeline } from 'node:stream/promises';
6
+
7
+ function helperInvocation(container, command, args) {
8
+ const entrypoint = container.args.indexOf('/opt/ours/node_modules/@ours.network/mcp/dist/container.js');
9
+ return { ...container, args: [...container.args.slice(0, entrypoint + 2), command, ...args] };
10
+ }
11
+
12
+ function helperJson(container, command, args) {
13
+ const call = helperInvocation(container, command, args);
14
+ const child = spawnSync(call.command, call.args, { env: call.env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000, maxBuffer: 1024 * 1024 });
15
+ if (child.error) throw child.error;
16
+ if (child.status !== 0) throw new Error(child.stderr.trim() || `${command} exited ${child.status}`);
17
+ return JSON.parse(child.stdout);
18
+ }
19
+
20
+ async function runHelper(container, command, args, input, output) {
21
+ const call = helperInvocation(container, command, args);
22
+ const child = spawn(call.command, call.args, { env: call.env, stdio: ['pipe', 'pipe', 'pipe'] });
23
+ let stderr = '';
24
+ child.stderr.setEncoding('utf8');
25
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
26
+ const exited = new Promise((resolveExit, reject) => {
27
+ child.once('error', reject);
28
+ child.once('close', (code, signal) => code === 0 ? resolveExit() : reject(new Error(stderr.trim() || `${command} exited ${signal ?? code}`)));
29
+ });
30
+ const streams = [];
31
+ if (input) streams.push(pipeline(input, child.stdin)); else child.stdin.end();
32
+ if (output) streams.push(pipeline(child.stdout, output)); else child.stdout.resume();
33
+ const results = await Promise.allSettled([...streams, exited]);
34
+ const failed = results.find((result) => result.status === 'rejected');
35
+ if (failed) throw failed.reason;
36
+ }
37
+
38
+ async function stageFile(container, id, hostPath) {
39
+ const abs = resolve(hostPath);
40
+ const call = helperInvocation(container, 'file-stage', [id, basename(abs)]);
41
+ const child = spawn(call.command, call.args, { env: call.env, stdio: ['pipe', 'pipe', 'pipe'] });
42
+ let stdout = '', stderr = '';
43
+ child.stdout.setEncoding('utf8'); child.stdout.on('data', (v) => { stdout += v; });
44
+ child.stderr.setEncoding('utf8'); child.stderr.on('data', (v) => { stderr += v; });
45
+ const exited = new Promise((resolveExit, reject) => {
46
+ child.once('error', reject);
47
+ child.once('close', (code, signal) => code === 0 ? resolveExit() : reject(new Error(stderr.trim() || `file-stage exited ${signal ?? code}`)));
48
+ });
49
+ const results = await Promise.allSettled([pipeline(createReadStream(abs), child.stdin), exited]);
50
+ const failed = results.find((result) => result.status === 'rejected');
51
+ if (failed) throw failed.reason;
52
+ return { path: JSON.parse(stdout).path, abs };
53
+ }
54
+
55
+ function toolError(id, error) {
56
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: `host file transfer failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true } };
57
+ }
58
+
59
+ async function removeTransfer(container, id) {
60
+ try { await runHelper(container, 'file-remove', [id]); } catch { /* preserve the primary result */ }
61
+ }
62
+
63
+ async function eachLine(stream, visit) {
64
+ let pending = Buffer.alloc(0);
65
+ for await (const chunk of stream) {
66
+ pending = Buffer.concat([pending, chunk]);
67
+ let newline;
68
+ while ((newline = pending.indexOf(10)) !== -1) {
69
+ const line = pending.subarray(0, newline + 1);
70
+ pending = pending.subarray(newline + 1);
71
+ await visit(line);
72
+ }
73
+ }
74
+ if (pending.length) await visit(pending);
75
+ }
76
+
77
+ export function runContainerProxy(container) {
78
+ const child = spawn(container.command, container.args, { stdio: ['pipe', 'pipe', 'inherit'], env: container.env });
79
+ const pending = new Map();
80
+ let stopping = false;
81
+ const stopInput = () => { stopping = true; process.stdin.destroy(); };
82
+ child.stdin.on('error', stopInput);
83
+ let outputWrites = Promise.resolve();
84
+ const writeOutput = (bytes) => { outputWrites = outputWrites.then(() => new Promise((done) => process.stdout.write(bytes, done))); return outputWrites; };
85
+
86
+ const inputDone = eachLine(process.stdin, async (raw) => {
87
+ let message;
88
+ try { message = JSON.parse(raw.toString('utf8')); } catch { child.stdin.write(raw); return; }
89
+ // Forward cancellation unchanged. The upstream handler owns cancellation;
90
+ // keep transfer tracking until its actual response so a completed save is
91
+ // copied to the host and its staging directory is removed.
92
+ if (message?.method === 'notifications/cancelled') { child.stdin.write(raw); return; }
93
+ if (message?.method !== 'tools/call' || !message.params?.arguments) { child.stdin.write(raw); return; }
94
+ const name = message.params.name;
95
+ if (name !== 'send_file' && name !== 'save_file') { child.stdin.write(raw); return; }
96
+ const args = message.params.arguments;
97
+ if (name === 'send_file' && !args.path) { child.stdin.write(raw); return; }
98
+ const id = randomUUID();
99
+ try {
100
+ if (name === 'send_file') {
101
+ const staged = await stageFile(container, id, args.path);
102
+ message.params.arguments = { ...args, path: staged.path, filename: args.filename ?? basename(staged.abs) };
103
+ pending.set(message.id, { id, kind: name });
104
+ } else {
105
+ const hostPath = resolve(args.dest_path);
106
+ const target = helperJson(container, 'file-target', [id]);
107
+ message.params.arguments = { ...args, dest_path: target.path };
108
+ pending.set(message.id, { id, kind: name, hostPath, containerPath: target.path });
109
+ }
110
+ if (!stopping) child.stdin.write(`${JSON.stringify(message)}\n`);
111
+ } catch (error) {
112
+ await removeTransfer(container, id);
113
+ await writeOutput(`${JSON.stringify(toolError(message.id, error))}\n`);
114
+ }
115
+ }).catch((error) => {
116
+ if (!stopping) { process.stderr.write(`ours: container MCP input failed: ${error.message}\n`); process.exitCode = 1; child.kill(); }
117
+ }).finally(() => { if (!child.stdin.destroyed) child.stdin.end(); });
118
+
119
+ const outputDone = eachLine(child.stdout, async (raw) => {
120
+ let message;
121
+ try { message = JSON.parse(raw.toString('utf8')); } catch { await writeOutput(raw); return; }
122
+ const transfer = pending.get(message?.id);
123
+ if (!transfer) { await writeOutput(raw); return; }
124
+ pending.delete(message.id);
125
+ try {
126
+ if (transfer.kind === 'save_file' && !message.error && message.result?.isError !== true) {
127
+ mkdirSync(dirname(transfer.hostPath), { recursive: true });
128
+ await runHelper(container, 'file-read', [transfer.id], null, createWriteStream(transfer.hostPath));
129
+ for (const item of message.result?.content ?? []) {
130
+ if (item?.type === 'text' && typeof item.text === 'string') item.text = item.text.split(transfer.containerPath).join(transfer.hostPath);
131
+ }
132
+ }
133
+ await writeOutput(`${JSON.stringify(message)}\n`);
134
+ } catch (error) {
135
+ await writeOutput(`${JSON.stringify(toolError(message.id, error))}\n`);
136
+ } finally { await removeTransfer(container, transfer.id); }
137
+ });
138
+
139
+ child.on('error', (error) => { stopInput(); process.stderr.write(`ours: cannot launch container MCP: ${error.message}\n`); process.exitCode = 1; });
140
+ child.on('close', async (code, signal) => {
141
+ stopInput();
142
+ await Promise.allSettled([inputDone, outputDone, outputWrites]);
143
+ await Promise.allSettled([...pending.values()].map((v) => removeTransfer(container, v.id)));
144
+ if (signal) process.kill(process.pid, signal); else process.exitCode = process.exitCode || (code === null || code < 0 ? 1 : code);
145
+ });
146
+ return child;
147
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { runNetworkWatch } from '../dist/network-watch.mjs';
3
+
4
+ const [identity, nativeSessionId] = process.argv.slice(2);
5
+ runNetworkWatch({ identity, nativeSessionId }).catch((error) => {
6
+ process.stderr.write(`ours network watch: ${error.message}\n`);
7
+ process.exitCode = 1;
8
+ });
@@ -5,14 +5,7 @@
5
5
  // npm i -g @ours.network/codex
6
6
  // ours-codex-install
7
7
  //
8
- // SUPERSEDED BY `ours-install`: the unified `ours-install` is now the single front door for the
9
- // WHOLE stack (ours core + harness plugins + ours-fleet + Telegram). `ours-codex-install` is kept
10
- // as a THIN ALIAS — by default it hands off to `ours-install` when that's available, so an old
11
- // muscle-memory command still lands the user in the current, unified flow. Pass `--codex-only`
12
- // (or set OURS_CODEX_LEGACY=1) to force the legacy Codex-only install below.
13
- //
14
- // Usage:
15
- // ours-codex-install [--codex-only] [--codex-dir DIR] [--skills-dir DIR] [--skip-daemon]
8
+ // Installs client plugin wiring only after shared gateway profile validation.
16
9
  import { spawnSync } from 'node:child_process';
17
10
  import { fileURLToPath } from 'node:url';
18
11
  import { dirname, join } from 'node:path';
@@ -33,38 +26,19 @@ for (let i = 0; i < argv.length; i++) {
33
26
  else { console.error(`ours-codex-install: unknown argument "${a}"`); help(); process.exit(2); }
34
27
  }
35
28
 
36
- // Thin-alias hand-off: unless the caller forced the legacy Codex-only path, run the unified
37
- // `ours-install` if we can find it — on PATH (published), or the monorepo sibling (dev/test).
38
- if (!opts.codexOnly && !process.env.OURS_CODEX_LEGACY) {
39
- const onPath = spawnSync('bash', ['-c', 'command -v ours-install'], { encoding: 'utf8' });
40
- const unified = (onPath.status === 0 && (onPath.stdout || '').trim())
41
- ? { cmd: (onPath.stdout || '').trim(), args: argv }
42
- : (() => {
43
- const sib = join(PKG, '..', 'installer', 'install.mjs');
44
- return existsSync(sib) ? { cmd: process.execPath, args: [sib, ...argv] } : null;
45
- })();
46
- if (unified) {
47
- const r = spawnSync(unified.cmd, unified.args, { stdio: 'inherit', env: process.env });
48
- if (r.error) { console.error(`ours-codex-install: ${r.error.message}`); process.exit(1); }
49
- process.exit(r.status ?? 0);
50
- }
51
- // No unified installer found → fall through to the legacy Codex-only install (still never a dead end).
52
- }
53
-
54
29
  function help() {
55
30
  console.log(`ours-codex-install — set up the ours.network plugin for the OpenAI Codex CLI.
56
31
 
57
32
  ours-codex-install [options]
58
33
 
59
- By default this hands off to the unified \`ours-install\` (the single front door for the whole
60
- stack). Use --codex-only to run the legacy Codex-only install instead. Standard mode uses
34
+ Requires clients and a shared gateway profile provisioned by \`ours-install client\`. Standard mode uses
61
35
  \`codex\`; live mode uses \`ours-codex\`. Live monitoring still requires explicit consent.
62
36
 
63
37
  Options:
64
- --codex-only run the legacy Codex-only install (skip the unified ours-install)
38
+ --codex-only compatibility flag; plugin setup is always client-only
65
39
  --codex-dir <dir> Codex config+AGENTS.md root (default ~/.codex) [--codex-only]
66
40
  --skills-dir <dir> skills root (default ~/.agents/skills — USER scope) [--codex-only]
67
- --skip-daemon do not install/start the ours daemon [--codex-only]
41
+ --skip-daemon compatibility flag; server lifecycle is never changed [--codex-only]
68
42
  -h, --help show this help
69
43
 
70
44
  Idempotent: safe to re-run. Start a new Codex thread after installation and review the
@@ -81,7 +55,6 @@ if (!existsSync(INSTALL)) {
81
55
  const env = { ...process.env };
82
56
  if (opts.codexDir) env.CODEX_DIR = opts.codexDir;
83
57
  if (opts.skillsDir) env.SKILLS_DIR = opts.skillsDir;
84
- if (opts.skipDaemon) env.OURS_INSTALL_SKIP_DAEMON = '1';
85
58
 
86
59
  const res = spawnSync('bash', [INSTALL], { stdio: 'inherit', env });
87
60
  if (res.error) { console.error(`ours-codex-install: ${res.error.message}`); process.exit(1); }
package/bin/proxy.mjs CHANGED
@@ -5,29 +5,34 @@ import { existsSync } from 'node:fs';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
 
8
+ const sessionEnd = process.argv[2] === 'session-end';
9
+ const command = sessionEnd ? 'session-end' : 'proxy';
10
+ const forwarded = process.argv.slice(sessionEnd ? 3 : 2);
8
11
  const require = createRequire(import.meta.url);
9
12
  const spec = '@ours.network/mcp/dist/cli.js';
10
13
  const here = dirname(fileURLToPath(import.meta.url));
11
- let cliPath;
12
- try { cliPath = require.resolve(spec); } catch { /* try plugin cache layouts */ }
13
- if (!cliPath) {
14
- let dir = here;
15
- for (let i = 0; i < 12 && !cliPath; i += 1) {
16
- for (const base of [dir, join(dir, 'npm-cache')]) {
17
- if (!existsSync(join(base, 'node_modules'))) continue;
18
- try { cliPath = require.resolve(spec, { paths: [base] }); } catch { /* next */ }
14
+ const env = { ...process.env };
15
+
16
+ async function main() {
17
+ let cliPath;
18
+ try { cliPath = require.resolve(spec); } catch { /* try plugin cache layouts */ }
19
+ if (!cliPath) {
20
+ let dir = here;
21
+ for (let i = 0; i < 12 && !cliPath; i += 1) {
22
+ for (const base of [dir, join(dir, 'npm-cache')]) {
23
+ if (!existsSync(join(base, 'node_modules'))) continue;
24
+ try { cliPath = require.resolve(spec, { paths: [base] }); } catch { /* next */ }
25
+ }
26
+ const parent = dirname(dir); if (parent === dir) break; dir = parent;
19
27
  }
20
- const parent = dirname(dir); if (parent === dir) break; dir = parent;
21
28
  }
29
+ if (env.OURS_CODEX_LIVE === '1') env.OURS_AUTOSTART = '0';
30
+ if (!env.OURS_CLIENT_PID && process.ppid > 1) env.OURS_CLIENT_PID = String(process.ppid);
31
+ const child = cliPath
32
+ ? spawn(process.execPath, [cliPath, command, ...forwarded], { stdio: 'inherit', env })
33
+ : spawn('ours-mcp', [command, ...forwarded], { stdio: 'inherit', env });
34
+ child.on('error', (error) => { process.stderr.write(`ours: cannot launch @ours.network/mcp proxy: ${error.message}\n`); process.exitCode = 1; });
35
+ child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 0; });
22
36
  }
23
- const env = { ...process.env };
24
- if (env.OURS_CODEX_LIVE === '1') env.OURS_AUTOSTART = '0';
25
- if (!env.OURS_CLIENT_PID && process.ppid > 1) env.OURS_CLIENT_PID = String(process.ppid);
26
- const sessionEnd = process.argv[2] === 'session-end';
27
- const command = sessionEnd ? 'session-end' : 'proxy';
28
- const forwarded = process.argv.slice(sessionEnd ? 3 : 2);
29
- const child = cliPath
30
- ? spawn(process.execPath, [cliPath, command, ...forwarded], { stdio: 'inherit', env })
31
- : spawn('ours-mcp', [command, ...forwarded], { stdio: 'inherit', env });
32
- child.on('error', (error) => { process.stderr.write(`ours: cannot launch @ours.network/mcp proxy: ${error.message}\n`); process.exit(1); });
33
- child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 0); });
37
+
38
+ main().catch((error) => { process.stderr.write(`ours: ${error.message}\n`); process.exitCode = 1; });