@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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -28
- package/bin/codex-config-install.mjs +3 -3
- package/bin/ours-codex-install.mjs +4 -31
- package/bin/proxy.mjs +1 -17
- package/dist/hooks-runner.mjs +422 -16509
- package/dist/host-hooks.mjs +386 -0
- package/dist/monitor-mcp.mjs +20 -125
- package/dist/network-watch.mjs +17 -121
- package/dist/profile.mjs +14 -156
- package/install.sh +14 -33
- package/package.json +4 -3
- package/skills/ours/SKILL.md +6 -6
- package/skills/ours/references/configuration.md +20 -22
- package/src/hooks/runner.mjs +8 -70
- package/src/host-hooks.mjs +25 -0
- package/src/launcher.mjs +1 -9
- package/src/profile.mjs +1 -40
- package/bin/container-launch.mjs +0 -40
- package/dist/network-proxy.mjs +0 -16782
- package/src/network-proxy.mjs +0 -308
package/dist/network-watch.mjs
CHANGED
|
@@ -1,131 +1,27 @@
|
|
|
1
1
|
// src/network-watch.mjs
|
|
2
|
-
import { homedir
|
|
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 {
|
|
7
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
71
|
+
const before = lstatSync(guardPath);
|
|
176
72
|
const Database = resolveDatabase();
|
|
177
73
|
const db = new Database(guardPath);
|
|
178
74
|
try {
|
|
179
|
-
const after =
|
|
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
|
|
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("/") ||
|
|
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 (!
|
|
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" || !
|
|
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((
|
|
265
|
-
release =
|
|
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 ||
|
|
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 {
|
|
3
|
+
import { attachOursClient as attachOursClient2 } from "@ours.network/sdk";
|
|
6
4
|
|
|
7
5
|
// ../core/src/host-profile.ts
|
|
8
|
-
import {
|
|
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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
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
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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)
|
|
50
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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"
|
package/skills/ours/SKILL.md
CHANGED
|
@@ -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.**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
#
|
|
1
|
+
# Shared gateway client configuration
|
|
2
2
|
|
|
3
|
-
ours
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
ours config show --json
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|