@ours.network/codex 1.2.0-nightly.1 → 1.2.0-nightly.11

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,131 +1,27 @@
1
1
  // src/network-watch.mjs
2
- import { homedir as homedir2 } from "node:os";
2
+ import { homedir } from "node:os";
3
3
  import { dirname as dirname2, join as join3 } from "node:path";
4
4
 
5
5
  // ../core/src/host-profile.ts
6
- import { existsSync, lstatSync, readFileSync } from "node:fs";
7
- import { homedir } from "node:os";
8
- import { resolve } from "node:path";
9
- var PROFILE_KEYS = ["endpoint", "expectedInstanceId", "credentialPath"];
10
- var LEGACY_SELECTION_KEYS = ["port", "stateDir", "apiToken", "apiVisibility"];
11
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
12
- var CONFLICTING_ENV = ["OURS_API_TOKEN", "OURS_PORT", "OURS_STATE_DIR", "OURS_DAEMON_ID"];
13
- function profileError(reason) {
14
- return new Error(`Invalid external host profile: ${reason}`);
15
- }
16
- function validateHostProfile(value) {
17
- if (!value || typeof value !== "object" || Array.isArray(value)) {
18
- throw profileError("expected a complete host profile object.");
19
- }
20
- const record = value;
21
- const present = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
22
- if (present.length !== PROFILE_KEYS.length) {
23
- throw profileError("expected a complete host profile tuple: endpoint, expectedInstanceId, credentialPath.");
24
- }
25
- const mixed = LEGACY_SELECTION_KEYS.filter((key) => Object.hasOwn(record, key));
26
- if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(", ")}).`);
27
- const endpoint = record.endpoint;
28
- const expectedInstanceId = record.expectedInstanceId;
29
- const credentialPath = record.credentialPath;
30
- if (typeof endpoint !== "string" || endpoint.trim() !== endpoint || endpoint === "") {
31
- throw profileError("endpoint must be a non-empty HTTP origin.");
32
- }
33
- if (typeof expectedInstanceId !== "string" || !UUID.test(expectedInstanceId)) {
34
- throw profileError("expectedInstanceId must be a lowercase UUID.");
35
- }
36
- if (typeof credentialPath !== "string" || credentialPath === "" || !credentialPath.startsWith("/") || resolve(credentialPath) !== credentialPath) {
37
- throw profileError("credentialPath must be a normalized absolute path.");
38
- }
39
- let url;
40
- try {
41
- url = new URL(endpoint);
42
- } catch {
43
- throw profileError("endpoint must be an HTTP origin.");
44
- }
45
- if (url.protocol !== "http:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
46
- throw profileError("endpoint must be an HTTP origin without credentials, path, query, or fragment.");
47
- }
48
- return { endpoint: url.origin, expectedInstanceId, credentialPath };
49
- }
50
- function readProfileObject(configPath) {
51
- let text;
52
- try {
53
- text = readFileSync(configPath, "utf8");
54
- } catch {
55
- throw new Error(`Cannot read host profile ${JSON.stringify(configPath)}.`);
56
- }
57
- let value;
58
- try {
59
- value = JSON.parse(text);
60
- } catch {
61
- throw profileError(`config ${JSON.stringify(configPath)} is not valid JSON.`);
62
- }
63
- if (!value || typeof value !== "object" || Array.isArray(value)) throw profileError(`config ${JSON.stringify(configPath)} must be an object.`);
64
- return value;
65
- }
66
- function assertPrivateRegularFile(configPath) {
67
- let stat;
68
- try {
69
- stat = lstatSync(configPath);
70
- } catch {
71
- throw new Error(`Cannot read host profile ${JSON.stringify(configPath)}.`);
72
- }
73
- if (!stat.isFile()) throw profileError(`config ${JSON.stringify(configPath)} must be a regular file.`);
74
- const currentUid = process.getuid?.();
75
- if (currentUid === void 0 || stat.uid !== currentUid) throw profileError(`config ${JSON.stringify(configPath)} must be owned by the current user.`);
76
- if ((stat.mode & 63) !== 0) throw profileError(`config ${JSON.stringify(configPath)} must have private permissions.`);
77
- }
78
- function readHostProfile(configPath) {
79
- const record = readProfileObject(configPath);
80
- const found = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
81
- if (found.length === 0) return null;
82
- assertPrivateRegularFile(configPath);
83
- return validateHostProfile(record);
84
- }
85
- function nonempty(env, key) {
86
- return (env[key] ?? "").trim() !== "";
87
- }
6
+ import { readGatewayClientProfile, validateGatewayClientProfile } from "@ours.network/sdk/client";
7
+ var validateHostProfile = validateGatewayClientProfile;
88
8
  function hostProfileSelectionFromEnv(env = process.env) {
89
- const explicit = (env.OURS_CONFIG ?? "").trim();
90
- const managedPath = resolve(env.HOME || homedir(), ".ours-client", "profile.json");
91
- let configPath = explicit;
92
- if (!configPath) {
93
- try {
94
- lstatSync(managedPath);
95
- configPath = managedPath;
96
- } catch (error) {
97
- if (error.code !== "ENOENT") {
98
- throw new Error(`Cannot read host profile ${JSON.stringify(managedPath)}.`);
99
- }
100
- configPath = resolve(homedir(), ".ours", "config.json");
101
- if (!existsSync(configPath)) return null;
102
- }
103
- }
104
- const profile = readHostProfile(configPath);
105
- if (profile === null) {
106
- if (resolve(configPath) === managedPath) throw profileError("managed client config must contain a complete host profile tuple.");
107
- return null;
108
- }
109
- const conflicting = CONFLICTING_ENV.filter((key) => nonempty(env, key));
110
- if (conflicting.length) throw profileError(`${conflicting.join(", ")} conflicts with host-profile mode.`);
9
+ const { configPath, ...profile } = readGatewayClientProfile(env);
111
10
  return { profile, configPath };
112
11
  }
113
12
  function hostProfileFromEnv(env = process.env) {
114
- return hostProfileSelectionFromEnv(env)?.profile ?? null;
13
+ return hostProfileSelectionFromEnv(env).profile;
115
14
  }
116
15
 
117
- // ../core/src/host-client/files.ts
118
- import { buildIdentityFile, writeIdentityFile } from "@ours.network/sdk/connector";
119
-
120
16
  // ../core/src/native-session.ts
121
17
  import { createHash as createHash2, randomUUID } from "node:crypto";
122
18
  import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
123
- import { dirname, join as join2, resolve as resolve2 } from "node:path";
19
+ import { dirname, join as join2, resolve } from "node:path";
124
20
  import { attachOursClient } from "@ours.network/sdk";
125
21
 
126
22
  // ../core/src/session-guard.ts
127
23
  import { createHash } from "node:crypto";
128
- import { openSync, closeSync, lstatSync as lstatSync2, mkdirSync, chmodSync, constants } from "node:fs";
24
+ import { openSync, closeSync, lstatSync, mkdirSync, chmodSync, constants } from "node:fs";
129
25
  import { join } from "node:path";
130
26
  import { createRequire } from "node:module";
131
27
  var BUSY_TIMEOUT_MS = 5e3;
@@ -156,7 +52,7 @@ function ensureGuardFile(guardPath) {
156
52
  } catch (error) {
157
53
  if (error.code !== "EEXIST") throw error;
158
54
  }
159
- const stat = lstatSync2(guardPath);
55
+ const stat = lstatSync(guardPath);
160
56
  if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 63) !== 0 || stat.nlink !== 1) {
161
57
  throw new Error("Native session guard file must be an owner-only regular file");
162
58
  }
@@ -172,11 +68,11 @@ function acquireSessionGuard(recordPath) {
172
68
  const hash = createHash("sha256").update(recordPath).digest("hex");
173
69
  const guardPath = join(base, `${hash}.guard`);
174
70
  ensureGuardFile(guardPath);
175
- const before = lstatSync2(guardPath);
71
+ const before = lstatSync(guardPath);
176
72
  const Database = resolveDatabase();
177
73
  const db = new Database(guardPath);
178
74
  try {
179
- const after = lstatSync2(guardPath);
75
+ const after = lstatSync(guardPath);
180
76
  if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode || before.uid !== after.uid) {
181
77
  throw new Error("Native session guard file changed during acquisition");
182
78
  }
@@ -198,7 +94,7 @@ function acquireSessionGuard(recordPath) {
198
94
 
199
95
  // ../core/src/native-session.ts
200
96
  var RECORD_VERSION = 1;
201
- var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
97
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
202
98
  var clients = /* @__PURE__ */ new Map();
203
99
  var operations = /* @__PURE__ */ new Map();
204
100
  function selector(value) {
@@ -208,10 +104,10 @@ function selector(value) {
208
104
  return value;
209
105
  }
210
106
  function nativeSessionRecordPath(hostRecordRoot, instanceId, nativeSessionId) {
211
- if (!hostRecordRoot || !hostRecordRoot.startsWith("/") || resolve2(hostRecordRoot) !== hostRecordRoot) {
107
+ if (!hostRecordRoot || !hostRecordRoot.startsWith("/") || resolve(hostRecordRoot) !== hostRecordRoot) {
212
108
  throw new Error("Native session host record root must be a normalized absolute path.");
213
109
  }
214
- if (!UUID2.test(instanceId)) throw new Error("Native session daemon instance must be a lowercase UUID.");
110
+ if (!UUID.test(instanceId)) throw new Error("Native session daemon instance must be a lowercase UUID.");
215
111
  const hash = createHash2("sha256").update(selector(nativeSessionId), "utf8").digest("hex");
216
112
  return join2(hostRecordRoot, "sessions", instanceId, `${hash}.json`);
217
113
  }
@@ -227,7 +123,7 @@ function parseRecord(text, path) {
227
123
  }
228
124
  const record = value;
229
125
  const keys = Object.keys(record).sort();
230
- if (keys.join(",") !== "ownerInstanceId,state,version" || record.version !== RECORD_VERSION || typeof record.ownerInstanceId !== "string" || !UUID2.test(record.ownerInstanceId) || !["active", "pending", "ended"].includes(String(record.state))) {
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))) {
231
127
  throw new Error(`Invalid native session record at ${path}.`);
232
128
  }
233
129
  return record;
@@ -261,8 +157,8 @@ async function writeRecord(path, record) {
261
157
  async function serialized(key, operation) {
262
158
  const predecessor = operations.get(key) ?? Promise.resolve();
263
159
  let release;
264
- const gate = new Promise((resolve3) => {
265
- release = resolve3;
160
+ const gate = new Promise((resolve2) => {
161
+ release = resolve2;
266
162
  });
267
163
  const tail = predecessor.catch(() => {
268
164
  }).then(() => gate);
@@ -361,7 +257,7 @@ async function runNetworkWatch({
361
257
  if (typeof nativeSessionId !== "string" || !nativeSessionId.trim()) throw new Error("native session id is required");
362
258
  const profile = profileValue ?? hostProfileFromEnv(env);
363
259
  if (!profile) throw new Error("A selected host network profile is required.");
364
- const appPath = env.OURS_MCP_CONFIG || join3(env.HOME || homedir2(), ".ours-mcp", "config.json");
260
+ const appPath = env.OURS_MCP_CONFIG || join3(env.HOME || homedir(), ".ours-mcp", "config.json");
365
261
  const hostRecordRoot = hostRecordRootValue ?? dirname2(appPath);
366
262
  const controller = new AbortController();
367
263
  const stop = () => controller.abort();
package/dist/profile.mjs CHANGED
@@ -1,121 +1,14 @@
1
1
  // src/profile.mjs
2
- import { join } from "node:path";
3
- import { homedir as homedir2 } from "node:os";
4
2
  import { randomUUID } from "node:crypto";
5
- import { assertDaemonStateDir, attachOursClient as attachOursClient2, resolveDaemonConfig } from "@ours.network/sdk";
3
+ import { attachOursClient as attachOursClient2 } from "@ours.network/sdk";
6
4
 
7
5
  // ../core/src/host-profile.ts
8
- import { existsSync, lstatSync, readFileSync } from "node:fs";
9
- import { homedir } from "node:os";
10
- import { resolve } from "node:path";
11
- var PROFILE_KEYS = ["endpoint", "expectedInstanceId", "credentialPath"];
12
- var LEGACY_SELECTION_KEYS = ["port", "stateDir", "apiToken", "apiVisibility"];
13
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
14
- var CONFLICTING_ENV = ["OURS_API_TOKEN", "OURS_PORT", "OURS_STATE_DIR", "OURS_DAEMON_ID"];
15
- function profileError(reason) {
16
- return new Error(`Invalid external host profile: ${reason}`);
17
- }
18
- function validateHostProfile(value) {
19
- if (!value || typeof value !== "object" || Array.isArray(value)) {
20
- throw profileError("expected a complete host profile object.");
21
- }
22
- const record = value;
23
- const present = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
24
- if (present.length !== PROFILE_KEYS.length) {
25
- throw profileError("expected a complete host profile tuple: endpoint, expectedInstanceId, credentialPath.");
26
- }
27
- const mixed = LEGACY_SELECTION_KEYS.filter((key) => Object.hasOwn(record, key));
28
- if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(", ")}).`);
29
- const endpoint = record.endpoint;
30
- const expectedInstanceId = record.expectedInstanceId;
31
- const credentialPath = record.credentialPath;
32
- if (typeof endpoint !== "string" || endpoint.trim() !== endpoint || endpoint === "") {
33
- throw profileError("endpoint must be a non-empty HTTP origin.");
34
- }
35
- if (typeof expectedInstanceId !== "string" || !UUID.test(expectedInstanceId)) {
36
- throw profileError("expectedInstanceId must be a lowercase UUID.");
37
- }
38
- if (typeof credentialPath !== "string" || credentialPath === "" || !credentialPath.startsWith("/") || resolve(credentialPath) !== credentialPath) {
39
- throw profileError("credentialPath must be a normalized absolute path.");
40
- }
41
- let url;
42
- try {
43
- url = new URL(endpoint);
44
- } catch {
45
- throw profileError("endpoint must be an HTTP origin.");
46
- }
47
- if (url.protocol !== "http:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
48
- throw profileError("endpoint must be an HTTP origin without credentials, path, query, or fragment.");
49
- }
50
- return { endpoint: url.origin, expectedInstanceId, credentialPath };
51
- }
52
- function readProfileObject(configPath) {
53
- let text;
54
- try {
55
- text = readFileSync(configPath, "utf8");
56
- } catch {
57
- throw new Error(`Cannot read host profile ${JSON.stringify(configPath)}.`);
58
- }
59
- let value;
60
- try {
61
- value = JSON.parse(text);
62
- } catch {
63
- throw profileError(`config ${JSON.stringify(configPath)} is not valid JSON.`);
64
- }
65
- if (!value || typeof value !== "object" || Array.isArray(value)) throw profileError(`config ${JSON.stringify(configPath)} must be an object.`);
66
- return value;
67
- }
68
- function assertPrivateRegularFile(configPath) {
69
- let stat;
70
- try {
71
- stat = lstatSync(configPath);
72
- } catch {
73
- throw new Error(`Cannot read host profile ${JSON.stringify(configPath)}.`);
74
- }
75
- if (!stat.isFile()) throw profileError(`config ${JSON.stringify(configPath)} must be a regular file.`);
76
- const currentUid = process.getuid?.();
77
- if (currentUid === void 0 || stat.uid !== currentUid) throw profileError(`config ${JSON.stringify(configPath)} must be owned by the current user.`);
78
- if ((stat.mode & 63) !== 0) throw profileError(`config ${JSON.stringify(configPath)} must have private permissions.`);
79
- }
80
- function readHostProfile(configPath) {
81
- const record = readProfileObject(configPath);
82
- const found = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
83
- if (found.length === 0) return null;
84
- assertPrivateRegularFile(configPath);
85
- return validateHostProfile(record);
86
- }
87
- function nonempty(env, key) {
88
- return (env[key] ?? "").trim() !== "";
89
- }
6
+ import { readGatewayClientProfile, validateGatewayClientProfile } from "@ours.network/sdk/client";
90
7
  function hostProfileSelectionFromEnv(env = process.env) {
91
- const explicit = (env.OURS_CONFIG ?? "").trim();
92
- const managedPath = resolve(env.HOME || homedir(), ".ours-client", "profile.json");
93
- let configPath = explicit;
94
- if (!configPath) {
95
- try {
96
- lstatSync(managedPath);
97
- configPath = managedPath;
98
- } catch (error) {
99
- if (error.code !== "ENOENT") {
100
- throw new Error(`Cannot read host profile ${JSON.stringify(managedPath)}.`);
101
- }
102
- configPath = resolve(homedir(), ".ours", "config.json");
103
- if (!existsSync(configPath)) return null;
104
- }
105
- }
106
- const profile = readHostProfile(configPath);
107
- if (profile === null) {
108
- if (resolve(configPath) === managedPath) throw profileError("managed client config must contain a complete host profile tuple.");
109
- return null;
110
- }
111
- const conflicting = CONFLICTING_ENV.filter((key) => nonempty(env, key));
112
- if (conflicting.length) throw profileError(`${conflicting.join(", ")} conflicts with host-profile mode.`);
8
+ const { configPath, ...profile } = readGatewayClientProfile(env);
113
9
  return { profile, configPath };
114
10
  }
115
11
 
116
- // ../core/src/host-client/files.ts
117
- import { buildIdentityFile, writeIdentityFile } from "@ours.network/sdk/connector";
118
-
119
12
  // ../core/src/native-session.ts
120
13
  import { attachOursClient } from "@ours.network/sdk";
121
14
 
@@ -144,53 +37,18 @@ function parseOursArgs(argv = []) {
144
37
  async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch, attach = attachOursClient2 } = {}) {
145
38
  const parsed = parseOursArgs(argv);
146
39
  const hostSelection = hostProfileSelectionFromEnv(env);
147
- if (hostSelection !== null) {
148
- const { profile: hostProfile, configPath } = hostSelection;
149
- if (parsed.port != null) throw new Error("ours: --ours-port conflicts with host-profile mode");
150
- const client = await attach({ ...hostProfile, sessionMode: "external", leaseToken: randomUUID(), env: {} });
151
- let info2;
152
- try {
153
- info2 = await client.version();
154
- await client.identities();
155
- await client.unread();
156
- } finally {
157
- await client.close();
158
- }
159
- return { profile: hostProfile, info: info2, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
160
- }
161
- const selection = resolveDaemonConfig({
162
- ...parsed.port == null ? {} : { port: parsed.port },
163
- env,
164
- homeDir: env.HOME || homedir2()
165
- });
166
- await assertDaemonStateDir(selection, { fetch: fetchImpl, timeoutMs: 2e3 });
167
- const baseUrl = selection.baseUrl.value;
168
- const token = selection.token?.value ?? null;
169
- const headers = token ? { "x-ours-api-token": token } : {};
170
- const response = await fetchImpl(`${baseUrl}/info`, { signal: AbortSignal.timeout(2e3) });
171
- if (!response.ok) throw new Error(`ours daemon at ${baseUrl} returned HTTP ${response.status}`);
172
- const info = await response.json();
173
- if (info?.name !== "ours" || !Number.isInteger(info?.protocol) || info.protocol < 1) {
174
- throw new Error(`incompatible service at ${baseUrl}; expected an ours daemon with notification protocol 1`);
175
- }
176
- for (const path of ["/identities", "/unread"]) {
177
- const capability = await fetchImpl(`${baseUrl}${path}`, { headers, signal: AbortSignal.timeout(2e3) });
178
- if (capability.status === 401 || capability.status === 403) {
179
- throw new Error(`ours daemon authentication failed at ${baseUrl}; supply the matching coherent selection`);
180
- }
181
- if (!capability.ok) throw new Error(`selected daemon lacks ${path} (HTTP ${capability.status})`);
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();
182
50
  }
183
- return {
184
- port: selection.port.value,
185
- stateDir: selection.expectStateDir,
186
- token,
187
- visibility: "owner",
188
- source: selection.baseUrl.source,
189
- info,
190
- baseUrl,
191
- configPath: selection.configPath?.value ?? join(selection.expectStateDir, "config.json"),
192
- codexArgs: parsed.codexArgs
193
- };
51
+ return { profile: hostProfile, info, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
194
52
  }
195
53
  export {
196
54
  parseOursArgs,
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.2.0-nightly.1",
3
+ "version": "1.2.0-nightly.11",
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,9 +46,10 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
- "@ours.network/sdk": "3.8.1-nightly.3",
49
+ "@ours.network/sdk": "3.8.1-nightly.16",
50
50
  "ws": "^8.21.0",
51
- "zod": "^3.25.76"
51
+ "zod": "^3.25.76",
52
+ "@ours.network/mcp": "1.2.0-nightly.11"
52
53
  },
53
54
  "devDependencies": {
54
55
  "esbuild": "^0.24.2"
@@ -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
 
@@ -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.