@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.
@@ -0,0 +1,287 @@
1
+ // src/network-watch.mjs
2
+ import { homedir } from "node:os";
3
+ import { dirname as dirname2, join as join3 } from "node:path";
4
+
5
+ // ../core/src/host-profile.ts
6
+ import { readGatewayClientProfile, validateGatewayClientProfile } from "@ours.network/sdk/client";
7
+ var validateHostProfile = validateGatewayClientProfile;
8
+ function hostProfileSelectionFromEnv(env = process.env) {
9
+ const { configPath, ...profile } = readGatewayClientProfile(env);
10
+ return { profile, configPath };
11
+ }
12
+ function hostProfileFromEnv(env = process.env) {
13
+ return hostProfileSelectionFromEnv(env).profile;
14
+ }
15
+
16
+ // ../core/src/native-session.ts
17
+ import { createHash as createHash2, randomUUID } from "node:crypto";
18
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
19
+ import { dirname, join as join2, resolve } from "node:path";
20
+ import { attachOursClient } from "@ours.network/sdk";
21
+
22
+ // ../core/src/session-guard.ts
23
+ import { createHash } from "node:crypto";
24
+ import { openSync, closeSync, lstatSync, mkdirSync, chmodSync, constants } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { createRequire } from "node:module";
27
+ var BUSY_TIMEOUT_MS = 5e3;
28
+ var DatabaseSync = null;
29
+ var resolveAttempted = false;
30
+ function resolveDatabase() {
31
+ if (DatabaseSync) return DatabaseSync;
32
+ if (resolveAttempted) throw new Error("No SQLite implementation available for native session guard");
33
+ resolveAttempted = true;
34
+ try {
35
+ const mod = createRequire(import.meta.url)("node:sqlite");
36
+ DatabaseSync = mod.DatabaseSync;
37
+ return DatabaseSync;
38
+ } catch {
39
+ try {
40
+ const mod = createRequire(import.meta.url)("better-sqlite3");
41
+ DatabaseSync = mod.default ?? mod;
42
+ return DatabaseSync;
43
+ } catch {
44
+ throw new Error("Native session guard requires node:sqlite (Node >=22.13) or better-sqlite3");
45
+ }
46
+ }
47
+ }
48
+ function ensureGuardFile(guardPath) {
49
+ try {
50
+ const fd = openSync(guardPath, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
51
+ closeSync(fd);
52
+ } catch (error) {
53
+ if (error.code !== "EEXIST") throw error;
54
+ }
55
+ const stat = lstatSync(guardPath);
56
+ if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 63) !== 0 || stat.nlink !== 1) {
57
+ throw new Error("Native session guard file must be an owner-only regular file");
58
+ }
59
+ }
60
+ function acquireSessionGuard(recordPath) {
61
+ const uid = process.getuid();
62
+ const base = join(`/tmp/ours-${uid}`, "session-guards");
63
+ mkdirSync(base, { recursive: true, mode: 448 });
64
+ try {
65
+ chmodSync(base, 448);
66
+ } catch {
67
+ }
68
+ const hash = createHash("sha256").update(recordPath).digest("hex");
69
+ const guardPath = join(base, `${hash}.guard`);
70
+ ensureGuardFile(guardPath);
71
+ const before = lstatSync(guardPath);
72
+ const Database = resolveDatabase();
73
+ const db = new Database(guardPath);
74
+ try {
75
+ const after = lstatSync(guardPath);
76
+ if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode || before.uid !== after.uid) {
77
+ throw new Error("Native session guard file changed during acquisition");
78
+ }
79
+ db.exec(`PRAGMA busy_timeout=${BUSY_TIMEOUT_MS}; BEGIN EXCLUSIVE`);
80
+ return () => {
81
+ try {
82
+ db.close();
83
+ } catch {
84
+ }
85
+ };
86
+ } catch (error) {
87
+ try {
88
+ db.close();
89
+ } catch {
90
+ }
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ // ../core/src/native-session.ts
96
+ var RECORD_VERSION = 1;
97
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
98
+ var clients = /* @__PURE__ */ new Map();
99
+ var operations = /* @__PURE__ */ new Map();
100
+ function selector(value) {
101
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value.trim() !== value || value.includes("\0")) {
102
+ throw new Error("Native session metadata is missing or invalid for this host-profile tool call.");
103
+ }
104
+ return value;
105
+ }
106
+ function nativeSessionRecordPath(hostRecordRoot, instanceId, nativeSessionId) {
107
+ if (!hostRecordRoot || !hostRecordRoot.startsWith("/") || resolve(hostRecordRoot) !== hostRecordRoot) {
108
+ throw new Error("Native session host record root must be a normalized absolute path.");
109
+ }
110
+ if (!UUID.test(instanceId)) throw new Error("Native session daemon instance must be a lowercase UUID.");
111
+ const hash = createHash2("sha256").update(selector(nativeSessionId), "utf8").digest("hex");
112
+ return join2(hostRecordRoot, "sessions", instanceId, `${hash}.json`);
113
+ }
114
+ function parseRecord(text, path) {
115
+ let value;
116
+ try {
117
+ value = JSON.parse(text);
118
+ } catch (error) {
119
+ throw new Error(`Invalid native session record at ${path}: ${String(error)}`);
120
+ }
121
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
122
+ throw new Error(`Invalid native session record at ${path}: expected an object.`);
123
+ }
124
+ const record = value;
125
+ const keys = Object.keys(record).sort();
126
+ if (keys.join(",") !== "ownerInstanceId,state,version" || record.version !== RECORD_VERSION || typeof record.ownerInstanceId !== "string" || !UUID.test(record.ownerInstanceId) || !["active", "pending", "ended"].includes(String(record.state))) {
127
+ throw new Error(`Invalid native session record at ${path}.`);
128
+ }
129
+ return record;
130
+ }
131
+ async function readRecord(path) {
132
+ try {
133
+ return parseRecord(await readFile(path, "utf8"), path);
134
+ } catch (error) {
135
+ if (error.code === "ENOENT") return null;
136
+ throw error;
137
+ }
138
+ }
139
+ async function writeRecord(path, record) {
140
+ const parent = dirname(path);
141
+ await mkdir(parent, { recursive: true, mode: 448 });
142
+ await chmod(parent, 448);
143
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
144
+ try {
145
+ await writeFile(temporary, `${JSON.stringify(record, null, 2)}
146
+ `, { mode: 384, flag: "wx" });
147
+ await rename(temporary, path);
148
+ await chmod(path, 384);
149
+ } catch (error) {
150
+ try {
151
+ await unlink(temporary);
152
+ } catch {
153
+ }
154
+ throw error;
155
+ }
156
+ }
157
+ async function serialized(key, operation) {
158
+ const predecessor = operations.get(key) ?? Promise.resolve();
159
+ let release;
160
+ const gate = new Promise((resolve2) => {
161
+ release = resolve2;
162
+ });
163
+ const tail = predecessor.catch(() => {
164
+ }).then(() => gate);
165
+ operations.set(key, tail);
166
+ await predecessor.catch(() => {
167
+ });
168
+ let releaseGuard;
169
+ try {
170
+ releaseGuard = acquireSessionGuard(key);
171
+ return await operation();
172
+ } finally {
173
+ try {
174
+ releaseGuard?.();
175
+ } finally {
176
+ release();
177
+ if (operations.get(key) === tail) operations.delete(key);
178
+ }
179
+ }
180
+ }
181
+ function attach(profile, ownerInstanceId) {
182
+ return attachOursClient({
183
+ endpoint: profile.endpoint,
184
+ expectedInstanceId: profile.expectedInstanceId,
185
+ credentialPath: profile.credentialPath,
186
+ sessionMode: "external",
187
+ leaseToken: ownerInstanceId,
188
+ env: {}
189
+ });
190
+ }
191
+ function cachedClient(path, profile, ownerInstanceId) {
192
+ const cached = clients.get(path);
193
+ if (cached?.ownerInstanceId === ownerInstanceId) return cached.promise;
194
+ if (cached) {
195
+ clients.delete(path);
196
+ void cached.promise.then((client) => client.close()).catch(() => {
197
+ });
198
+ }
199
+ const promise = attach(profile, ownerInstanceId);
200
+ const next = { ownerInstanceId, promise };
201
+ clients.set(path, next);
202
+ void promise.catch(() => {
203
+ if (clients.get(path) === next) clients.delete(path);
204
+ });
205
+ return promise;
206
+ }
207
+ async function releaseOwner(path, profile, ownerInstanceId) {
208
+ const cached = clients.get(path);
209
+ const client = cached?.ownerInstanceId === ownerInstanceId ? await cached.promise : await attach(profile, ownerInstanceId);
210
+ try {
211
+ await client.releaseLease();
212
+ } finally {
213
+ if (cached?.ownerInstanceId !== ownerInstanceId) await client.close();
214
+ }
215
+ }
216
+ async function finishPending(path, profile, ownerInstanceId) {
217
+ await releaseOwner(path, profile, ownerInstanceId);
218
+ const current = await readRecord(path);
219
+ if (current?.state === "pending" && current.ownerInstanceId === ownerInstanceId) {
220
+ await writeRecord(path, { version: RECORD_VERSION, ownerInstanceId, state: "ended" });
221
+ }
222
+ const cached = clients.get(path);
223
+ if (cached?.ownerInstanceId === ownerInstanceId) {
224
+ clients.delete(path);
225
+ void cached.promise.then((client) => client.close()).catch(() => {
226
+ });
227
+ }
228
+ }
229
+ async function nativeClientForAtRoot(profileValue, nativeSessionId, hostRecordRoot) {
230
+ const profile = validateHostProfile(profileValue);
231
+ const path = nativeSessionRecordPath(hostRecordRoot, profile.expectedInstanceId, nativeSessionId);
232
+ return serialized(path, async () => {
233
+ let record = await readRecord(path);
234
+ if (record?.state === "pending") {
235
+ await finishPending(path, profile, record.ownerInstanceId);
236
+ record = await readRecord(path);
237
+ }
238
+ if (record?.state === "active") return cachedClient(path, profile, record.ownerInstanceId);
239
+ const ownerInstanceId = randomUUID();
240
+ await writeRecord(path, { version: RECORD_VERSION, ownerInstanceId, state: "active" });
241
+ return cachedClient(path, profile, ownerInstanceId);
242
+ });
243
+ }
244
+
245
+ // src/network-watch.mjs
246
+ async function runNetworkWatch({
247
+ identity,
248
+ nativeSessionId,
249
+ env = process.env,
250
+ profile: profileValue,
251
+ hostRecordRoot: hostRecordRootValue,
252
+ clientFor = nativeClientForAtRoot,
253
+ write = (value) => process.stdout.write(value),
254
+ installSignalHandlers = true
255
+ } = {}) {
256
+ if (typeof identity !== "string" || !identity.trim()) throw new Error("identity is required");
257
+ if (typeof nativeSessionId !== "string" || !nativeSessionId.trim()) throw new Error("native session id is required");
258
+ const profile = profileValue ?? hostProfileFromEnv(env);
259
+ if (!profile) throw new Error("A selected host network profile is required.");
260
+ const appPath = env.OURS_MCP_CONFIG || join3(env.HOME || homedir(), ".ours-mcp", "config.json");
261
+ const hostRecordRoot = hostRecordRootValue ?? dirname2(appPath);
262
+ const controller = new AbortController();
263
+ const stop = () => controller.abort();
264
+ if (installSignalHandlers) {
265
+ process.once("SIGINT", stop);
266
+ process.once("SIGTERM", stop);
267
+ }
268
+ let client;
269
+ try {
270
+ client = await clientFor(profile, nativeSessionId, hostRecordRoot);
271
+ for await (const event of client.watchNotifications(identity, { kinds: ["inbound"], signal: controller.signal })) {
272
+ const summary = event?.event === "file_received" ? `[${identity}] new file ${event.filename ?? "?"} from ${event.from ?? "?"}` : `[${identity}] new message from ${event?.from ?? "?"}`;
273
+ write(`${summary}
274
+ `);
275
+ return;
276
+ }
277
+ } finally {
278
+ if (installSignalHandlers) {
279
+ process.off("SIGINT", stop);
280
+ process.off("SIGTERM", stop);
281
+ }
282
+ await client?.close();
283
+ }
284
+ }
285
+ export {
286
+ runNetworkWatch
287
+ };
@@ -0,0 +1,56 @@
1
+ // src/profile.mjs
2
+ import { randomUUID } from "node:crypto";
3
+ import { attachOursClient as attachOursClient2 } from "@ours.network/sdk";
4
+
5
+ // ../core/src/host-profile.ts
6
+ import { readGatewayClientProfile, validateGatewayClientProfile } from "@ours.network/sdk/client";
7
+ function hostProfileSelectionFromEnv(env = process.env) {
8
+ const { configPath, ...profile } = readGatewayClientProfile(env);
9
+ return { profile, configPath };
10
+ }
11
+
12
+ // ../core/src/native-session.ts
13
+ import { attachOursClient } from "@ours.network/sdk";
14
+
15
+ // src/profile.mjs
16
+ function validPort(value) {
17
+ const port = Number(value);
18
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`ours: ${value} is not a valid TCP port`);
19
+ return port;
20
+ }
21
+ function parseOursArgs(argv = []) {
22
+ const codexArgs = [];
23
+ let port;
24
+ for (let index = 0; index < argv.length; index += 1) {
25
+ const arg = argv[index];
26
+ if (arg === "--ours-port") {
27
+ if (argv[index + 1] == null) throw new Error("ours: --ours-port requires a value");
28
+ port = validPort(argv[++index]);
29
+ } else if (arg.startsWith("--ours-port=")) {
30
+ port = validPort(arg.slice("--ours-port=".length));
31
+ } else {
32
+ codexArgs.push(arg);
33
+ }
34
+ }
35
+ return { port, codexArgs };
36
+ }
37
+ async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch, attach = attachOursClient2 } = {}) {
38
+ const parsed = parseOursArgs(argv);
39
+ const hostSelection = hostProfileSelectionFromEnv(env);
40
+ const { profile: hostProfile, configPath } = hostSelection;
41
+ if (parsed.port != null) throw new Error("ours: --ours-port conflicts with host-profile mode");
42
+ const client = await attach({ ...hostProfile, sessionMode: "external", leaseToken: randomUUID(), env: {} });
43
+ let info;
44
+ try {
45
+ info = await client.version();
46
+ await client.identities();
47
+ await client.unread();
48
+ } finally {
49
+ await client.close();
50
+ }
51
+ return { profile: hostProfile, info, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
52
+ }
53
+ export {
54
+ parseOursArgs,
55
+ resolveDaemonProfile
56
+ };
package/hooks/hooks.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "node \"${PLUGIN_ROOT}/src/hooks/runner.mjs\"",
9
+ "command": "node \"${PLUGIN_ROOT}/dist/hooks-runner.mjs\"",
10
10
  "timeout": 5,
11
11
  "statusMessage": "Checking ours mail metadata"
12
12
  }
@@ -18,7 +18,7 @@
18
18
  "hooks": [
19
19
  {
20
20
  "type": "command",
21
- "command": "node \"${PLUGIN_ROOT}/src/hooks/runner.mjs\"",
21
+ "command": "node \"${PLUGIN_ROOT}/dist/hooks-runner.mjs\"",
22
22
  "timeout": 3
23
23
  }
24
24
  ]
@@ -30,7 +30,7 @@
30
30
  "hooks": [
31
31
  {
32
32
  "type": "command",
33
- "command": "node \"${PLUGIN_ROOT}/src/hooks/runner.mjs\"",
33
+ "command": "node \"${PLUGIN_ROOT}/dist/hooks-runner.mjs\"",
34
34
  "timeout": 3
35
35
  }
36
36
  ]
package/install.sh CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # Install the native ours.network plugin into the OpenAI Codex CLI:
3
- # 1. ensure the ours operator CLI and MCP adapter are installed; start the shared daemon
3
+ # 1. verify the installed gateway client profile before modifying plugin configuration
4
4
  # 2. add/upgrade adapt-toolkit/ours-codex-marketplace
5
5
  # 3. install the native `ours` plugin (skills, MCP servers, and hooks)
6
6
  # 4. back up and remove installer-owned legacy config only after verification
@@ -10,7 +10,6 @@
10
10
  # SKILLS_DIR skills root (default ~/.agents/skills)
11
11
  # CODEX_CONFIG config.toml path (default $CODEX_DIR/config.toml)
12
12
  # CODEX_AGENTS AGENTS.md path (default $CODEX_DIR/AGENTS.md)
13
- # OURS_INSTALL_SKIP_DAEMON=1 skip daemon install/start
14
13
  set -euo pipefail
15
14
 
16
15
  SELFDIR="$(cd "$(dirname "$0")" && pwd)"
@@ -21,33 +20,21 @@ SKILLS_DIR="${SKILLS_DIR:-$HOME/.agents/skills}"
21
20
 
22
21
  say(){ printf 'ours-install: %s\n' "$1"; }
23
22
 
24
- # Ensure the ours daemon is on @latest (UPGRADE, not install-if-missing): an already-present
25
- # daemon must still be pulled up to the newest published version. Record the CLI version
26
- # before/after; start if not running, restart only if the version actually changed.
27
- ensure_daemon_latest(){
28
- if [ "${OURS_INSTALL_SKIP_DAEMON:-}" = "1" ]; then say "skipping daemon step (OURS_INSTALL_SKIP_DAEMON=1)"; return 0; fi
29
- local before after
30
- before="$(ours version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
31
- say "ensuring @ours.network/cli@latest and @ours.network/mcp@latest…"
32
- npm i -g @ours.network/cli@latest @ours.network/mcp@latest
33
- after="$(ours version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
34
- if ! ours daemon status >/dev/null 2>&1; then
35
- say "starting the ours daemon…"
36
- if ! ours daemon start; then say "could not start the ours daemon; installation stopped."; return 1; fi
37
- elif [ -n "$before" ] && [ "$before" != "$after" ]; then
38
- say "operator CLI upgraded (v${before} → v${after}) — restarting its daemon…"
39
- if ! ours daemon restart; then
40
- say "restart failed; trying a clean start…"
41
- if ! ours daemon start; then say "could not restart or start the upgraded daemon; installation stopped."; return 1; fi
42
- fi
43
- else
44
- say "daemon already current (v${after:-unknown})."
23
+ # Client installation never installs, starts or restarts server services.
24
+ require_gateway_client(){
25
+ if ! command -v ours >/dev/null 2>&1 || ! command -v ours-mcp >/dev/null 2>&1; then
26
+ say "Install gateway clients with ours-install client first; ours and ours-mcp are required."
27
+ return 1
28
+ fi
29
+ local contract
30
+ if ! contract="$(ours-mcp verify-client-profile)" || [ "$contract" != "ours.gateway-client-profile-v1" ]; then
31
+ say "Configure the shared gateway profile with ours-install client first; plugin setup was left unchanged."
32
+ return 1
45
33
  fi
46
- say "operator CLI: $(command -v ours) (v${after:-unknown}); MCP adapter: $(command -v ours-mcp)"
47
34
  }
48
35
 
49
- # --- 1) daemon (ensure @latest + restart on change) ---
50
- ensure_daemon_latest
36
+ # --- 1) shared gateway client preflight ---
37
+ require_gateway_client
51
38
 
52
39
  # --- native Codex plugin ------------------------------------------------------
53
40
  # Codex owns the plugin cache and hook-trust workflow. Only remove the legacy
@@ -73,7 +60,7 @@ if [ "${OURS_CODEX_SKIP_NATIVE:-}" != "1" ]; then
73
60
  CODEX_DIR="$CODEX_DIR" SKILLS_DIR="$SKILLS_DIR" node "$SELFDIR/bin/codex-legacy-cleanup.mjs"
74
61
  say "native plugin installed. Review and trust its hooks in Codex, then start a new thread."
75
62
  say "standard mode: codex"
76
- say "live mode: ours-codex${OURS_PORT:+ --ours-port $OURS_PORT}"
63
+ say "live mode: ours-codex"
77
64
  exit 0
78
65
  fi
79
66
 
@@ -93,11 +80,5 @@ CODEX_CONFIG="$CODEX_CONFIG" node "$SELFDIR/bin/codex-config-install.mjs"
93
80
  CODEX_AGENTS="$CODEX_AGENTS" node "$SELFDIR/bin/codex-agents-install.mjs"
94
81
 
95
82
  say "done. The ours MCP server + skill are live for the next Codex session."
96
- # --- version echo: show the user they are on latest ---
97
- if [ "${OURS_INSTALL_SKIP_DAEMON:-}" != "1" ]; then
98
- say "versions:"
99
- say " MCP adapter: $(ours-mcp --version 2>/dev/null | head -1 || echo 'unknown')"
100
- say " plugin: $(npm ls -g @ours.network/codex 2>/dev/null | grep -oE '@ours\.network/codex@[0-9][0-9.]*' | head -1 || echo '@ours.network/codex (not a global install)')"
101
- fi
102
83
  say "next: bind (or create) an identity, then the ours skill tails ours-mcp watch (or polls"
103
84
  say " get_messages) in-session so you react to new mail while you work."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/codex",
3
- "version": "1.1.1",
3
+ "version": "1.2.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,10 +46,10 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
- "@ours.network/mcp": "1.1.1",
50
- "@ours.network/sdk": "3.7.0",
49
+ "@ours.network/sdk": "3.8.1-nightly.15",
51
50
  "ws": "^8.21.0",
52
- "zod": "^3.25.76"
51
+ "zod": "^3.25.76",
52
+ "@ours.network/mcp": "1.2.0-nightly.10"
53
53
  },
54
54
  "devDependencies": {
55
55
  "esbuild": "^0.24.2"
@@ -58,7 +58,7 @@
58
58
  "access": "public"
59
59
  },
60
60
  "scripts": {
61
- "build": "esbuild src/monitor-mcp.mjs --bundle --platform=node --format=esm --external:@ours.network/mcp --outfile=dist/monitor-mcp.mjs",
61
+ "build": "node build.mjs",
62
62
  "prepack": "npm run build",
63
63
  "install-plugin": "bash install.sh",
64
64
  "test": "node --test"
@@ -108,12 +108,12 @@ existing file.
108
108
  **control-plane monitoring proxy is not available in this release** — there is no tool
109
109
  to call. See "Control plane" below.
110
110
 
111
- - **Configuration.** Port, state dir, broker, and GC interval are configurable.
112
- More than one daemon may run when each uses a distinct port and state directory.
113
- Select live mode with `ours-codex --ours-port <port>` or `OURS_CONFIG`. Never
114
- self-configure on your own initiative: surface the
115
- need, explain the impact, and act only on the user's explicit yes. Details:
116
- `references/configuration.md`.
111
+ - **Configuration.** Every client selects the HTTP gateway through
112
+ `~/.ours-client/profile.json` (or the whole-profile `OURS_CONFIG` override).
113
+ `serverUrl` is the base URL; daemon and Cowork use `/daemon` and `/cowork`
114
+ beneath it. Import the issued credential with `ours-install client`.
115
+ Local daemon ports, state directories, and Unix sockets are not client routes.
116
+ Missing or invalid configuration fails closed. See `references/configuration.md`.
117
117
 
118
118
  ## ours-fleet — persistent harness roles
119
119
 
@@ -448,6 +448,7 @@ feature and still works; it is described above.
448
448
  watcher for exactly one TUI session. The launcher observes that session's thread
449
449
  directly; monitor MCP tools carry explicit arm/disarm consent, while trusted hooks add
450
450
  defensive identity-state synchronization. Standard `codex` falls back, after separate
451
- consent, to a foreground `ours-mcp watch` call that returns on the next body-free event.
451
+ consent, to a package-local authenticated network watch that returns on the next
452
+ body-free event.
452
453
  Authenticated daemon notification endpoints remain body-free. Message text is returned
453
454
  only by the explicit unread-drain and history tools.
@@ -1,26 +1,24 @@
1
- # ours daemon configuration
1
+ # Shared gateway client configuration
2
2
 
3
- ours-mcp is a client of one already-running shared daemon. The operator CLI owns
4
- configuration and lifecycle:
3
+ Fleet, ours CLI, MCP and harness hooks select one server through
4
+ `~/.ours-client/profile.json`. `serverUrl` names the HTTP(S) gateway, with
5
+ optional base path. Daemon operations use `/daemon`; Cowork uses `/cowork`.
6
+ The profile also pins `expectedInstanceId` and an absolute `credentialPath`.
7
+ An optional `endpoint` must equal `serverUrl + "/daemon"`.
5
8
 
6
- ```sh
7
- ours config show --json
8
- ours config setup --port 3050 --state-dir "$HOME/.ours"
9
- ours daemon start
10
- ours daemon status --json
11
- ```
9
+ Import a prepared private profile with `ours-install client --config /absolute/profile.json`.
10
+ Use `ours config show --json` to inspect connection metadata without reading the token.
11
+ Keep the profile directory mode 0700 and files mode 0600. `OURS_CONFIG` may
12
+ select the same whole profile for all clients. Remove legacy `OURS_PORT`,
13
+ `OURS_STATE_DIR`, `OURS_API_TOKEN` and `OURS_DAEMON_ID` overrides.
12
14
 
13
- The MCP adapter and `ours-codex` use the published SDK's coherent selection. The
14
- wholly default selection is port 3050 with state directory `~/.ours`. For another
15
- daemon, set `OURS_CONFIG`, or set matching `OURS_PORT` and `OURS_STATE_DIR`. A
16
- token or endpoint selection must be paired with its state directory. `/state-dir`
17
- is verified before credentials are sent.
15
+ Missing profiles, rejected credentials and unavailable gateways fail closed.
16
+ Clients never read daemon state, start a daemon, or try a local port/socket.
17
+ Hooks use the same gateway and return a harmless no-op on failure. Their
18
+ application-local identity/session files do not select another server.
18
19
 
19
- The adapter never starts a daemon and never falls back to an embedded one.
20
- `OURS_INSTANCE`, `--application`, and old ours-mcp daemon variables are errors.
21
- Standard and live Codex mode use the same selection; live mode only adds an
22
- explicitly armed, session-scoped wake monitor.
23
-
24
- Changing daemon configuration or restarting the shared daemon affects every
25
- connected application. Explain that blast radius and obtain the user's consent
26
- before making operator-level changes.
20
+ For full-stack setup, a custom public port (such as 4050), systemd propagation
21
+ and migration, see the [gateway setup guide](https://github.com/adapt-toolkit/ours-network/blob/prerelease/packages/installer/GATEWAY_SETUP.md).
22
+ The coordinated gateway changes require approved matching package releases;
23
+ current npm nightly is not automatically evidence that these changes are installed.
24
+ Server administration uses `ours-install server` on the server host.
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
2
3
  import { readFile } from 'node:fs/promises';
3
4
  import { homedir } from 'node:os';
4
5
  import { dirname, join, resolve } from 'node:path';
5
6
  import { pathToFileURL } from 'node:url';
6
7
  import { sendControlCommand } from '../control-server.mjs';
7
8
  import { resolveDaemonProfile } from '../profile.mjs';
9
+ import { readHostHookState } from '../host-hooks.mjs';
10
+ import { attachOursClient } from '@ours.network/sdk';
11
+ import { hostProfileFromEnv } from '../../../core/src/host-client/index.ts';
8
12
 
9
13
  async function defaultFindPin(cwd) {
10
14
  let dir = resolve(cwd || process.cwd());
@@ -31,14 +35,6 @@ const safeUnread = (value) => (Array.isArray(value?.identities) ? value.identiti
31
35
  return [{ name: entry.name, count, files, recent }];
32
36
  });
33
37
 
34
- async function applicationIdentityNames(env, stateDir) {
35
- const path = env.OURS_MCP_CONFIG || join(homedir(), '.ours-mcp', 'config.json');
36
- const value = JSON.parse(await readFile(path, 'utf8'));
37
- if (value?.version !== 1 || !value.daemons || typeof value.daemons !== 'object') return new Set();
38
- const identities = value.daemons[resolve(stateDir)]?.identities;
39
- return new Set(Array.isArray(identities) ? identities.filter((name) => typeof name === 'string') : []);
40
- }
41
-
42
38
  function renderContext(unread, pin) {
43
39
  const lines = [];
44
40
  if (unread.length) {
@@ -54,7 +50,7 @@ function renderContext(unread, pin) {
54
50
  return lines.join('\n');
55
51
  }
56
52
 
57
- export async function handleHook(payload, { env = process.env, fetch: fetchImpl = globalThis.fetch, send = sendControlCommand, findPin = defaultFindPin } = {}) {
53
+ export async function handleHook(payload, { env = process.env, fetch: fetchImpl = globalThis.fetch, send = sendControlCommand, findPin = defaultFindPin, profileResolver = resolveDaemonProfile, clientFactory = attachOursClient, readHostState = readHostHookState } = {}) {
58
54
  try {
59
55
  const event = payload?.hook_event_name;
60
56
  const socket = env.OURS_CODEX_CONTROL_SOCKET;
@@ -72,37 +68,26 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
72
68
  if (event === 'SessionStart' && socket && capability && payload.session_id && payload.cwd) {
73
69
  await send(socket, capability, { command: 'register_session', sessionId: payload.session_id, threadId: payload.session_id, cwd: payload.cwd });
74
70
  }
75
- let port = env.OURS_PORT || '3050';
76
- let selectedStateDir = resolve(env.OURS_STATE_DIR || join(homedir(), '.ours'));
77
- let selectedToken = env.OURS_API_TOKEN || '';
78
- // Standard Codex does not pass through ours-codex's resolved environment.
79
- // Resolve the same coherent SDK selection here so SessionStart and
80
- // UserPromptSubmit inspect the same daemon as the stdio proxy.
81
- try {
82
- const selected = await resolveDaemonProfile({ env, fetch: fetchImpl });
83
- port = String(selected.port);
84
- selectedToken = selected.token || '';
85
- selectedStateDir = resolve(selected.stateDir);
86
- } catch { /* proxy/launcher owns diagnostics; hooks emit a benign no-op */ }
87
- const headers = selectedToken ? { 'x-ours-api-token': selectedToken } : {};
88
- let unread = [];
89
- try {
90
- const response = await fetchImpl(`http://127.0.0.1:${port}/unread`, { headers, signal: AbortSignal.timeout(1500) });
91
- if (response.ok) {
92
- const visible = await applicationIdentityNames(env, selectedStateDir);
93
- unread = safeUnread(await response.json()).filter((entry) => visible.has(entry.name));
94
- }
95
- } catch { /* daemon diagnostics belong to launcher/proxy */ }
96
- const pin = await findPin(payload.cwd || process.cwd());
97
- const context = renderContext(unread, pin);
98
- if (!context) return { continue: true };
99
- return { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } };
71
+ let hostProfile;
72
+ try { hostProfile = hostProfileFromEnv(env); }
73
+ catch { return { continue: true }; }
74
+ if (hostProfile) {
75
+ const nativeSessionId = payload.session_id;
76
+ if (typeof nativeSessionId !== 'string' || !nativeSessionId) return { continue: true };
77
+ const appPath = env.OURS_MCP_CONFIG || join(env.HOME || homedir(), '.ours-mcp', 'config.json');
78
+ const state = await readHostState({ profile: hostProfile, nativeSessionId, applicationPath: appPath });
79
+ const pin = await findPin(payload.cwd || process.cwd());
80
+ const context = renderContext(state.unread.identities, pin);
81
+ return context ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } } : { continue: true };
82
+ }
83
+
84
+ return { continue: true };
100
85
  } catch { return { continue: true }; }
101
86
  }
102
87
 
103
88
  async function main() {
104
89
  let payload = {};
105
- try { payload = JSON.parse(await readFile(0, 'utf8')); } catch { /* noop */ }
90
+ try { payload = JSON.parse(readFileSync(0, 'utf8')); } catch { /* noop */ }
106
91
  process.stdout.write(JSON.stringify(await handleHook(payload)));
107
92
  }
108
93