@ours.network/codex 1.1.1 → 1.2.0-nightly.2
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/.mcp.json +2 -2
- package/README.md +33 -5
- package/bin/container-file-transfer.mjs +147 -0
- package/bin/container-launch.mjs +40 -0
- package/bin/network-watch.mjs +8 -0
- package/bin/proxy.mjs +42 -21
- package/dist/hooks-runner.mjs +16649 -0
- package/dist/monitor-mcp.mjs +154 -24
- package/dist/network-proxy.mjs +16782 -0
- package/dist/network-watch.mjs +391 -0
- package/dist/profile.mjs +198 -0
- package/hooks/hooks.json +3 -3
- package/package.json +3 -4
- package/skills/ours/SKILL.md +2 -1
- package/src/hooks/runner.mjs +54 -7
- package/src/launcher.mjs +49 -15
- package/src/monitor-mcp.mjs +19 -6
- package/src/network-proxy.mjs +308 -0
- package/src/network-watch.mjs +45 -0
- package/src/profile.mjs +17 -2
- package/src/watcher.mjs +29 -3
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// src/network-watch.mjs
|
|
2
|
+
import { homedir as homedir2 } from "node:os";
|
|
3
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
4
|
+
|
|
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
|
+
}
|
|
88
|
+
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.`);
|
|
111
|
+
return { profile, configPath };
|
|
112
|
+
}
|
|
113
|
+
function hostProfileFromEnv(env = process.env) {
|
|
114
|
+
return hostProfileSelectionFromEnv(env)?.profile ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ../core/src/host-client/files.ts
|
|
118
|
+
import { buildIdentityFile, writeIdentityFile } from "@ours.network/sdk/connector";
|
|
119
|
+
|
|
120
|
+
// ../core/src/native-session.ts
|
|
121
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
122
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
123
|
+
import { dirname, join as join2, resolve as resolve2 } from "node:path";
|
|
124
|
+
import { attachOursClient } from "@ours.network/sdk";
|
|
125
|
+
|
|
126
|
+
// ../core/src/session-guard.ts
|
|
127
|
+
import { createHash } from "node:crypto";
|
|
128
|
+
import { openSync, closeSync, lstatSync as lstatSync2, mkdirSync, chmodSync, constants } from "node:fs";
|
|
129
|
+
import { join } from "node:path";
|
|
130
|
+
import { createRequire } from "node:module";
|
|
131
|
+
var BUSY_TIMEOUT_MS = 5e3;
|
|
132
|
+
var DatabaseSync = null;
|
|
133
|
+
var resolveAttempted = false;
|
|
134
|
+
function resolveDatabase() {
|
|
135
|
+
if (DatabaseSync) return DatabaseSync;
|
|
136
|
+
if (resolveAttempted) throw new Error("No SQLite implementation available for native session guard");
|
|
137
|
+
resolveAttempted = true;
|
|
138
|
+
try {
|
|
139
|
+
const mod = createRequire(import.meta.url)("node:sqlite");
|
|
140
|
+
DatabaseSync = mod.DatabaseSync;
|
|
141
|
+
return DatabaseSync;
|
|
142
|
+
} catch {
|
|
143
|
+
try {
|
|
144
|
+
const mod = createRequire(import.meta.url)("better-sqlite3");
|
|
145
|
+
DatabaseSync = mod.default ?? mod;
|
|
146
|
+
return DatabaseSync;
|
|
147
|
+
} catch {
|
|
148
|
+
throw new Error("Native session guard requires node:sqlite (Node >=22.13) or better-sqlite3");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function ensureGuardFile(guardPath) {
|
|
153
|
+
try {
|
|
154
|
+
const fd = openSync(guardPath, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
155
|
+
closeSync(fd);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error.code !== "EEXIST") throw error;
|
|
158
|
+
}
|
|
159
|
+
const stat = lstatSync2(guardPath);
|
|
160
|
+
if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 63) !== 0 || stat.nlink !== 1) {
|
|
161
|
+
throw new Error("Native session guard file must be an owner-only regular file");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function acquireSessionGuard(recordPath) {
|
|
165
|
+
const uid = process.getuid();
|
|
166
|
+
const base = join(`/tmp/ours-${uid}`, "session-guards");
|
|
167
|
+
mkdirSync(base, { recursive: true, mode: 448 });
|
|
168
|
+
try {
|
|
169
|
+
chmodSync(base, 448);
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
const hash = createHash("sha256").update(recordPath).digest("hex");
|
|
173
|
+
const guardPath = join(base, `${hash}.guard`);
|
|
174
|
+
ensureGuardFile(guardPath);
|
|
175
|
+
const before = lstatSync2(guardPath);
|
|
176
|
+
const Database = resolveDatabase();
|
|
177
|
+
const db = new Database(guardPath);
|
|
178
|
+
try {
|
|
179
|
+
const after = lstatSync2(guardPath);
|
|
180
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode || before.uid !== after.uid) {
|
|
181
|
+
throw new Error("Native session guard file changed during acquisition");
|
|
182
|
+
}
|
|
183
|
+
db.exec(`PRAGMA busy_timeout=${BUSY_TIMEOUT_MS}; BEGIN EXCLUSIVE`);
|
|
184
|
+
return () => {
|
|
185
|
+
try {
|
|
186
|
+
db.close();
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
} catch (error) {
|
|
191
|
+
try {
|
|
192
|
+
db.close();
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ../core/src/native-session.ts
|
|
200
|
+
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}$/;
|
|
202
|
+
var clients = /* @__PURE__ */ new Map();
|
|
203
|
+
var operations = /* @__PURE__ */ new Map();
|
|
204
|
+
function selector(value) {
|
|
205
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value.trim() !== value || value.includes("\0")) {
|
|
206
|
+
throw new Error("Native session metadata is missing or invalid for this host-profile tool call.");
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
function nativeSessionRecordPath(hostRecordRoot, instanceId, nativeSessionId) {
|
|
211
|
+
if (!hostRecordRoot || !hostRecordRoot.startsWith("/") || resolve2(hostRecordRoot) !== hostRecordRoot) {
|
|
212
|
+
throw new Error("Native session host record root must be a normalized absolute path.");
|
|
213
|
+
}
|
|
214
|
+
if (!UUID2.test(instanceId)) throw new Error("Native session daemon instance must be a lowercase UUID.");
|
|
215
|
+
const hash = createHash2("sha256").update(selector(nativeSessionId), "utf8").digest("hex");
|
|
216
|
+
return join2(hostRecordRoot, "sessions", instanceId, `${hash}.json`);
|
|
217
|
+
}
|
|
218
|
+
function parseRecord(text, path) {
|
|
219
|
+
let value;
|
|
220
|
+
try {
|
|
221
|
+
value = JSON.parse(text);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new Error(`Invalid native session record at ${path}: ${String(error)}`);
|
|
224
|
+
}
|
|
225
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
226
|
+
throw new Error(`Invalid native session record at ${path}: expected an object.`);
|
|
227
|
+
}
|
|
228
|
+
const record = value;
|
|
229
|
+
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))) {
|
|
231
|
+
throw new Error(`Invalid native session record at ${path}.`);
|
|
232
|
+
}
|
|
233
|
+
return record;
|
|
234
|
+
}
|
|
235
|
+
async function readRecord(path) {
|
|
236
|
+
try {
|
|
237
|
+
return parseRecord(await readFile(path, "utf8"), path);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error.code === "ENOENT") return null;
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function writeRecord(path, record) {
|
|
244
|
+
const parent = dirname(path);
|
|
245
|
+
await mkdir(parent, { recursive: true, mode: 448 });
|
|
246
|
+
await chmod(parent, 448);
|
|
247
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
248
|
+
try {
|
|
249
|
+
await writeFile(temporary, `${JSON.stringify(record, null, 2)}
|
|
250
|
+
`, { mode: 384, flag: "wx" });
|
|
251
|
+
await rename(temporary, path);
|
|
252
|
+
await chmod(path, 384);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
try {
|
|
255
|
+
await unlink(temporary);
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function serialized(key, operation) {
|
|
262
|
+
const predecessor = operations.get(key) ?? Promise.resolve();
|
|
263
|
+
let release;
|
|
264
|
+
const gate = new Promise((resolve3) => {
|
|
265
|
+
release = resolve3;
|
|
266
|
+
});
|
|
267
|
+
const tail = predecessor.catch(() => {
|
|
268
|
+
}).then(() => gate);
|
|
269
|
+
operations.set(key, tail);
|
|
270
|
+
await predecessor.catch(() => {
|
|
271
|
+
});
|
|
272
|
+
let releaseGuard;
|
|
273
|
+
try {
|
|
274
|
+
releaseGuard = acquireSessionGuard(key);
|
|
275
|
+
return await operation();
|
|
276
|
+
} finally {
|
|
277
|
+
try {
|
|
278
|
+
releaseGuard?.();
|
|
279
|
+
} finally {
|
|
280
|
+
release();
|
|
281
|
+
if (operations.get(key) === tail) operations.delete(key);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function attach(profile, ownerInstanceId) {
|
|
286
|
+
return attachOursClient({
|
|
287
|
+
endpoint: profile.endpoint,
|
|
288
|
+
expectedInstanceId: profile.expectedInstanceId,
|
|
289
|
+
credentialPath: profile.credentialPath,
|
|
290
|
+
sessionMode: "external",
|
|
291
|
+
leaseToken: ownerInstanceId,
|
|
292
|
+
env: {}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function cachedClient(path, profile, ownerInstanceId) {
|
|
296
|
+
const cached = clients.get(path);
|
|
297
|
+
if (cached?.ownerInstanceId === ownerInstanceId) return cached.promise;
|
|
298
|
+
if (cached) {
|
|
299
|
+
clients.delete(path);
|
|
300
|
+
void cached.promise.then((client) => client.close()).catch(() => {
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const promise = attach(profile, ownerInstanceId);
|
|
304
|
+
const next = { ownerInstanceId, promise };
|
|
305
|
+
clients.set(path, next);
|
|
306
|
+
void promise.catch(() => {
|
|
307
|
+
if (clients.get(path) === next) clients.delete(path);
|
|
308
|
+
});
|
|
309
|
+
return promise;
|
|
310
|
+
}
|
|
311
|
+
async function releaseOwner(path, profile, ownerInstanceId) {
|
|
312
|
+
const cached = clients.get(path);
|
|
313
|
+
const client = cached?.ownerInstanceId === ownerInstanceId ? await cached.promise : await attach(profile, ownerInstanceId);
|
|
314
|
+
try {
|
|
315
|
+
await client.releaseLease();
|
|
316
|
+
} finally {
|
|
317
|
+
if (cached?.ownerInstanceId !== ownerInstanceId) await client.close();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function finishPending(path, profile, ownerInstanceId) {
|
|
321
|
+
await releaseOwner(path, profile, ownerInstanceId);
|
|
322
|
+
const current = await readRecord(path);
|
|
323
|
+
if (current?.state === "pending" && current.ownerInstanceId === ownerInstanceId) {
|
|
324
|
+
await writeRecord(path, { version: RECORD_VERSION, ownerInstanceId, state: "ended" });
|
|
325
|
+
}
|
|
326
|
+
const cached = clients.get(path);
|
|
327
|
+
if (cached?.ownerInstanceId === ownerInstanceId) {
|
|
328
|
+
clients.delete(path);
|
|
329
|
+
void cached.promise.then((client) => client.close()).catch(() => {
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async function nativeClientForAtRoot(profileValue, nativeSessionId, hostRecordRoot) {
|
|
334
|
+
const profile = validateHostProfile(profileValue);
|
|
335
|
+
const path = nativeSessionRecordPath(hostRecordRoot, profile.expectedInstanceId, nativeSessionId);
|
|
336
|
+
return serialized(path, async () => {
|
|
337
|
+
let record = await readRecord(path);
|
|
338
|
+
if (record?.state === "pending") {
|
|
339
|
+
await finishPending(path, profile, record.ownerInstanceId);
|
|
340
|
+
record = await readRecord(path);
|
|
341
|
+
}
|
|
342
|
+
if (record?.state === "active") return cachedClient(path, profile, record.ownerInstanceId);
|
|
343
|
+
const ownerInstanceId = randomUUID();
|
|
344
|
+
await writeRecord(path, { version: RECORD_VERSION, ownerInstanceId, state: "active" });
|
|
345
|
+
return cachedClient(path, profile, ownerInstanceId);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/network-watch.mjs
|
|
350
|
+
async function runNetworkWatch({
|
|
351
|
+
identity,
|
|
352
|
+
nativeSessionId,
|
|
353
|
+
env = process.env,
|
|
354
|
+
profile: profileValue,
|
|
355
|
+
hostRecordRoot: hostRecordRootValue,
|
|
356
|
+
clientFor = nativeClientForAtRoot,
|
|
357
|
+
write = (value) => process.stdout.write(value),
|
|
358
|
+
installSignalHandlers = true
|
|
359
|
+
} = {}) {
|
|
360
|
+
if (typeof identity !== "string" || !identity.trim()) throw new Error("identity is required");
|
|
361
|
+
if (typeof nativeSessionId !== "string" || !nativeSessionId.trim()) throw new Error("native session id is required");
|
|
362
|
+
const profile = profileValue ?? hostProfileFromEnv(env);
|
|
363
|
+
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");
|
|
365
|
+
const hostRecordRoot = hostRecordRootValue ?? dirname2(appPath);
|
|
366
|
+
const controller = new AbortController();
|
|
367
|
+
const stop = () => controller.abort();
|
|
368
|
+
if (installSignalHandlers) {
|
|
369
|
+
process.once("SIGINT", stop);
|
|
370
|
+
process.once("SIGTERM", stop);
|
|
371
|
+
}
|
|
372
|
+
let client;
|
|
373
|
+
try {
|
|
374
|
+
client = await clientFor(profile, nativeSessionId, hostRecordRoot);
|
|
375
|
+
for await (const event of client.watchNotifications(identity, { kinds: ["inbound"], signal: controller.signal })) {
|
|
376
|
+
const summary = event?.event === "file_received" ? `[${identity}] new file ${event.filename ?? "?"} from ${event.from ?? "?"}` : `[${identity}] new message from ${event?.from ?? "?"}`;
|
|
377
|
+
write(`${summary}
|
|
378
|
+
`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
} finally {
|
|
382
|
+
if (installSignalHandlers) {
|
|
383
|
+
process.off("SIGINT", stop);
|
|
384
|
+
process.off("SIGTERM", stop);
|
|
385
|
+
}
|
|
386
|
+
await client?.close();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
export {
|
|
390
|
+
runNetworkWatch
|
|
391
|
+
};
|
package/dist/profile.mjs
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// src/profile.mjs
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir as homedir2 } from "node:os";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { assertDaemonStateDir, attachOursClient as attachOursClient2, resolveDaemonConfig } from "@ours.network/sdk";
|
|
6
|
+
|
|
7
|
+
// ../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
|
+
}
|
|
90
|
+
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.`);
|
|
113
|
+
return { profile, configPath };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ../core/src/host-client/files.ts
|
|
117
|
+
import { buildIdentityFile, writeIdentityFile } from "@ours.network/sdk/connector";
|
|
118
|
+
|
|
119
|
+
// ../core/src/native-session.ts
|
|
120
|
+
import { attachOursClient } from "@ours.network/sdk";
|
|
121
|
+
|
|
122
|
+
// src/profile.mjs
|
|
123
|
+
function validPort(value) {
|
|
124
|
+
const port = Number(value);
|
|
125
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`ours: ${value} is not a valid TCP port`);
|
|
126
|
+
return port;
|
|
127
|
+
}
|
|
128
|
+
function parseOursArgs(argv = []) {
|
|
129
|
+
const codexArgs = [];
|
|
130
|
+
let port;
|
|
131
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
132
|
+
const arg = argv[index];
|
|
133
|
+
if (arg === "--ours-port") {
|
|
134
|
+
if (argv[index + 1] == null) throw new Error("ours: --ours-port requires a value");
|
|
135
|
+
port = validPort(argv[++index]);
|
|
136
|
+
} else if (arg.startsWith("--ours-port=")) {
|
|
137
|
+
port = validPort(arg.slice("--ours-port=".length));
|
|
138
|
+
} else {
|
|
139
|
+
codexArgs.push(arg);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { port, codexArgs };
|
|
143
|
+
}
|
|
144
|
+
async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch, attach = attachOursClient2 } = {}) {
|
|
145
|
+
const parsed = parseOursArgs(argv);
|
|
146
|
+
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})`);
|
|
182
|
+
}
|
|
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
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export {
|
|
196
|
+
parseOursArgs,
|
|
197
|
+
resolveDaemonProfile
|
|
198
|
+
};
|
package/hooks/hooks.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "node \"${PLUGIN_ROOT}/
|
|
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}/
|
|
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}/
|
|
33
|
+
"command": "node \"${PLUGIN_ROOT}/dist/hooks-runner.mjs\"",
|
|
34
34
|
"timeout": 3
|
|
35
35
|
}
|
|
36
36
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/codex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-nightly.2",
|
|
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,8 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
-
"@ours.network/
|
|
50
|
-
"@ours.network/sdk": "3.7.0",
|
|
49
|
+
"@ours.network/sdk": "3.8.1-nightly.3",
|
|
51
50
|
"ws": "^8.21.0",
|
|
52
51
|
"zod": "^3.25.76"
|
|
53
52
|
},
|
|
@@ -58,7 +57,7 @@
|
|
|
58
57
|
"access": "public"
|
|
59
58
|
},
|
|
60
59
|
"scripts": {
|
|
61
|
-
"build": "
|
|
60
|
+
"build": "node build.mjs",
|
|
62
61
|
"prepack": "npm run build",
|
|
63
62
|
"install-plugin": "bash install.sh",
|
|
64
63
|
"test": "node --test"
|
package/skills/ours/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|