@ours.network/codex 1.2.0-nightly.3 → 1.2.0-nightly.5
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 +2 -2
- package/bin/proxy.mjs +1 -17
- package/dist/hooks-runner.mjs +533 -16431
- package/dist/host-hooks.mjs +425 -0
- package/dist/monitor-mcp.mjs +4 -7
- package/dist/network-watch.mjs +4 -7
- package/dist/profile.mjs +4 -7
- package/package.json +4 -3
- package/src/hooks/runner.mjs +7 -22
- package/src/host-hooks.mjs +25 -0
- package/bin/container-launch.mjs +0 -40
- package/dist/network-proxy.mjs +0 -16782
- package/src/network-proxy.mjs +0 -308
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// src/host-hooks.mjs
|
|
2
|
+
import { dirname as dirname3 } from "node:path";
|
|
3
|
+
|
|
4
|
+
// ../core/src/application-identities.ts
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
var APPLICATION_IDENTITIES_VERSION = 1;
|
|
10
|
+
var APPLICATION_IDENTITIES_ENV = "OURS_MCP_CONFIG";
|
|
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 emptyConfig = () => ({ version: 1, daemons: {}, instances: {} });
|
|
13
|
+
function applicationIdentityConfigPath(env = process.env) {
|
|
14
|
+
const explicit = (env[APPLICATION_IDENTITIES_ENV] ?? "").trim();
|
|
15
|
+
return resolve(explicit || resolve(homedir(), ".ours-mcp", "config.json"));
|
|
16
|
+
}
|
|
17
|
+
function parseRows(raw, path, namespace, normalizeKey) {
|
|
18
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
19
|
+
throw new Error(`Invalid ours-mcp application identity config at ${path}: "${namespace}s" must be an object.`);
|
|
20
|
+
}
|
|
21
|
+
const rows = {};
|
|
22
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
23
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
+
throw new Error(`Invalid ours-mcp application identity config at ${path}: ${namespace} ${JSON.stringify(key)} must be an object.`);
|
|
25
|
+
}
|
|
26
|
+
const row = value;
|
|
27
|
+
if (!Array.isArray(row.identities) || row.identities.some((name) => typeof name !== "string" || name.length === 0)) {
|
|
28
|
+
throw new Error(`Invalid ours-mcp application identity config at ${path}: ${namespace} ${JSON.stringify(key)} has an invalid identity list.`);
|
|
29
|
+
}
|
|
30
|
+
rows[normalizeKey(key)] = { identities: [...new Set(row.identities)].sort() };
|
|
31
|
+
}
|
|
32
|
+
return rows;
|
|
33
|
+
}
|
|
34
|
+
function parseConfig(text, path) {
|
|
35
|
+
let value;
|
|
36
|
+
try {
|
|
37
|
+
value = JSON.parse(text);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error(`Invalid ours-mcp application identity config at ${path}: ${String(error)}`);
|
|
40
|
+
}
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid ours-mcp application identity config at ${path}: expected an object.`);
|
|
42
|
+
const record = value;
|
|
43
|
+
if (record.version !== APPLICATION_IDENTITIES_VERSION) {
|
|
44
|
+
throw new Error(`Unsupported ours-mcp application identity config version at ${path}: expected ${APPLICATION_IDENTITIES_VERSION}, found ${String(record.version)}.`);
|
|
45
|
+
}
|
|
46
|
+
const daemons = parseRows(record.daemons, path, "daemon", resolve);
|
|
47
|
+
const instances = record.instances === void 0 ? {} : parseRows(record.instances, path, "instance", (instanceId) => {
|
|
48
|
+
if (!UUID.test(instanceId)) throw new Error(`Invalid ours-mcp application identity config at ${path}: instance ${JSON.stringify(instanceId)} must be a lowercase UUID.`);
|
|
49
|
+
return instanceId;
|
|
50
|
+
});
|
|
51
|
+
return { version: 1, daemons, instances };
|
|
52
|
+
}
|
|
53
|
+
async function readConfig(path) {
|
|
54
|
+
try {
|
|
55
|
+
return parseConfig(await readFile(path, "utf8"), path);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.code === "ENOENT") return emptyConfig();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function writeConfigAtomic(path, config) {
|
|
62
|
+
const parent = dirname(path);
|
|
63
|
+
await mkdir(parent, { recursive: true, mode: 448 });
|
|
64
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
65
|
+
try {
|
|
66
|
+
await writeFile(temporary, `${JSON.stringify(config, null, 2)}
|
|
67
|
+
`, { mode: 384, flag: "wx" });
|
|
68
|
+
await rename(temporary, path);
|
|
69
|
+
await chmod(path, 384);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
try {
|
|
72
|
+
await unlink(temporary);
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
var ApplicationIdentityStore = class {
|
|
79
|
+
stateDir;
|
|
80
|
+
instanceId;
|
|
81
|
+
path;
|
|
82
|
+
constructor(target, options = {}) {
|
|
83
|
+
if (typeof target === "string") {
|
|
84
|
+
this.stateDir = resolve(target);
|
|
85
|
+
this.instanceId = null;
|
|
86
|
+
} else {
|
|
87
|
+
if (!target || !UUID.test(target.instanceId)) throw new Error("Application identity instanceId must be a lowercase UUID.");
|
|
88
|
+
this.stateDir = null;
|
|
89
|
+
this.instanceId = target.instanceId;
|
|
90
|
+
}
|
|
91
|
+
this.path = resolve(options.path ?? applicationIdentityConfigPath(options.env));
|
|
92
|
+
}
|
|
93
|
+
rows(config) {
|
|
94
|
+
return this.instanceId === null ? config.daemons : config.instances;
|
|
95
|
+
}
|
|
96
|
+
key() {
|
|
97
|
+
return this.instanceId ?? this.stateDir;
|
|
98
|
+
}
|
|
99
|
+
async list() {
|
|
100
|
+
const config = await readConfig(this.path);
|
|
101
|
+
return [...this.rows(config)[this.key()]?.identities ?? []];
|
|
102
|
+
}
|
|
103
|
+
async has(name) {
|
|
104
|
+
return (await this.list()).includes(name);
|
|
105
|
+
}
|
|
106
|
+
async add(name) {
|
|
107
|
+
if (!name) throw new Error("Cannot add an empty identity name to ours-mcp.");
|
|
108
|
+
const config = await readConfig(this.path);
|
|
109
|
+
const rows = this.rows(config);
|
|
110
|
+
const key = this.key();
|
|
111
|
+
const identities = rows[key]?.identities ?? [];
|
|
112
|
+
if (identities.includes(name)) return;
|
|
113
|
+
rows[key] = { identities: [...identities, name].sort() };
|
|
114
|
+
await writeConfigAtomic(this.path, config);
|
|
115
|
+
}
|
|
116
|
+
async remove(name) {
|
|
117
|
+
const config = await readConfig(this.path);
|
|
118
|
+
const rows = this.rows(config);
|
|
119
|
+
const key = this.key();
|
|
120
|
+
const current = rows[key];
|
|
121
|
+
if (!current?.identities.includes(name)) return;
|
|
122
|
+
const identities = current.identities.filter((candidate) => candidate !== name);
|
|
123
|
+
if (identities.length === 0) delete rows[key];
|
|
124
|
+
else rows[key] = { identities };
|
|
125
|
+
await writeConfigAtomic(this.path, config);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// ../core/src/host-profile.ts
|
|
130
|
+
import { resolve as resolve2 } from "node:path";
|
|
131
|
+
var PROFILE_KEYS = ["endpoint", "expectedInstanceId", "credentialPath"];
|
|
132
|
+
var LEGACY_SELECTION_KEYS = ["port", "stateDir", "apiToken", "apiVisibility"];
|
|
133
|
+
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
134
|
+
function profileError(reason) {
|
|
135
|
+
return new Error(`Invalid external host profile: ${reason}`);
|
|
136
|
+
}
|
|
137
|
+
function validateHostProfile(value) {
|
|
138
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
139
|
+
throw profileError("expected a complete host profile object.");
|
|
140
|
+
}
|
|
141
|
+
const record = value;
|
|
142
|
+
const present = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
143
|
+
if (present.length !== PROFILE_KEYS.length) {
|
|
144
|
+
throw profileError("expected a complete host profile tuple: endpoint, expectedInstanceId, credentialPath.");
|
|
145
|
+
}
|
|
146
|
+
const mixed = LEGACY_SELECTION_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
147
|
+
if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(", ")}).`);
|
|
148
|
+
const endpoint = record.endpoint;
|
|
149
|
+
const expectedInstanceId = record.expectedInstanceId;
|
|
150
|
+
const credentialPath = record.credentialPath;
|
|
151
|
+
if (typeof endpoint !== "string" || endpoint.trim() !== endpoint || endpoint === "") {
|
|
152
|
+
throw profileError("endpoint must be a non-empty HTTP or HTTPS origin.");
|
|
153
|
+
}
|
|
154
|
+
if (typeof expectedInstanceId !== "string" || !UUID2.test(expectedInstanceId)) {
|
|
155
|
+
throw profileError("expectedInstanceId must be a lowercase UUID.");
|
|
156
|
+
}
|
|
157
|
+
if (typeof credentialPath !== "string" || credentialPath === "" || !credentialPath.startsWith("/") || resolve2(credentialPath) !== credentialPath) {
|
|
158
|
+
throw profileError("credentialPath must be a normalized absolute path.");
|
|
159
|
+
}
|
|
160
|
+
let url;
|
|
161
|
+
try {
|
|
162
|
+
url = new URL(endpoint);
|
|
163
|
+
} catch {
|
|
164
|
+
throw profileError("endpoint must be an HTTP or HTTPS origin.");
|
|
165
|
+
}
|
|
166
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
167
|
+
throw profileError("endpoint must be an HTTP or HTTPS origin without credentials, path, query, or fragment.");
|
|
168
|
+
}
|
|
169
|
+
return { endpoint: url.origin, expectedInstanceId, credentialPath };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ../core/src/native-session.ts
|
|
173
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
174
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
175
|
+
import { dirname as dirname2, join as join2, resolve as resolve3 } from "node:path";
|
|
176
|
+
import { attachOursClient } from "@ours.network/sdk";
|
|
177
|
+
|
|
178
|
+
// ../core/src/session-guard.ts
|
|
179
|
+
import { createHash } from "node:crypto";
|
|
180
|
+
import { openSync, closeSync, lstatSync, mkdirSync, chmodSync, constants } from "node:fs";
|
|
181
|
+
import { join } from "node:path";
|
|
182
|
+
import { createRequire } from "node:module";
|
|
183
|
+
var BUSY_TIMEOUT_MS = 5e3;
|
|
184
|
+
var DatabaseSync = null;
|
|
185
|
+
var resolveAttempted = false;
|
|
186
|
+
function resolveDatabase() {
|
|
187
|
+
if (DatabaseSync) return DatabaseSync;
|
|
188
|
+
if (resolveAttempted) throw new Error("No SQLite implementation available for native session guard");
|
|
189
|
+
resolveAttempted = true;
|
|
190
|
+
try {
|
|
191
|
+
const mod = createRequire(import.meta.url)("node:sqlite");
|
|
192
|
+
DatabaseSync = mod.DatabaseSync;
|
|
193
|
+
return DatabaseSync;
|
|
194
|
+
} catch {
|
|
195
|
+
try {
|
|
196
|
+
const mod = createRequire(import.meta.url)("better-sqlite3");
|
|
197
|
+
DatabaseSync = mod.default ?? mod;
|
|
198
|
+
return DatabaseSync;
|
|
199
|
+
} catch {
|
|
200
|
+
throw new Error("Native session guard requires node:sqlite (Node >=22.13) or better-sqlite3");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function ensureGuardFile(guardPath) {
|
|
205
|
+
try {
|
|
206
|
+
const fd = openSync(guardPath, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
207
|
+
closeSync(fd);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error.code !== "EEXIST") throw error;
|
|
210
|
+
}
|
|
211
|
+
const stat = lstatSync(guardPath);
|
|
212
|
+
if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 63) !== 0 || stat.nlink !== 1) {
|
|
213
|
+
throw new Error("Native session guard file must be an owner-only regular file");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function acquireSessionGuard(recordPath) {
|
|
217
|
+
const uid = process.getuid();
|
|
218
|
+
const base = join(`/tmp/ours-${uid}`, "session-guards");
|
|
219
|
+
mkdirSync(base, { recursive: true, mode: 448 });
|
|
220
|
+
try {
|
|
221
|
+
chmodSync(base, 448);
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
const hash = createHash("sha256").update(recordPath).digest("hex");
|
|
225
|
+
const guardPath = join(base, `${hash}.guard`);
|
|
226
|
+
ensureGuardFile(guardPath);
|
|
227
|
+
const before = lstatSync(guardPath);
|
|
228
|
+
const Database = resolveDatabase();
|
|
229
|
+
const db = new Database(guardPath);
|
|
230
|
+
try {
|
|
231
|
+
const after = lstatSync(guardPath);
|
|
232
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode || before.uid !== after.uid) {
|
|
233
|
+
throw new Error("Native session guard file changed during acquisition");
|
|
234
|
+
}
|
|
235
|
+
db.exec(`PRAGMA busy_timeout=${BUSY_TIMEOUT_MS}; BEGIN EXCLUSIVE`);
|
|
236
|
+
return () => {
|
|
237
|
+
try {
|
|
238
|
+
db.close();
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
try {
|
|
244
|
+
db.close();
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ../core/src/native-session.ts
|
|
252
|
+
var RECORD_VERSION = 1;
|
|
253
|
+
var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
254
|
+
var clients = /* @__PURE__ */ new Map();
|
|
255
|
+
var operations = /* @__PURE__ */ new Map();
|
|
256
|
+
function selector(value) {
|
|
257
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value.trim() !== value || value.includes("\0")) {
|
|
258
|
+
throw new Error("Native session metadata is missing or invalid for this host-profile tool call.");
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
function nativeSessionRecordPath(hostRecordRoot, instanceId, nativeSessionId) {
|
|
263
|
+
if (!hostRecordRoot || !hostRecordRoot.startsWith("/") || resolve3(hostRecordRoot) !== hostRecordRoot) {
|
|
264
|
+
throw new Error("Native session host record root must be a normalized absolute path.");
|
|
265
|
+
}
|
|
266
|
+
if (!UUID3.test(instanceId)) throw new Error("Native session daemon instance must be a lowercase UUID.");
|
|
267
|
+
const hash = createHash2("sha256").update(selector(nativeSessionId), "utf8").digest("hex");
|
|
268
|
+
return join2(hostRecordRoot, "sessions", instanceId, `${hash}.json`);
|
|
269
|
+
}
|
|
270
|
+
function parseRecord(text, path) {
|
|
271
|
+
let value;
|
|
272
|
+
try {
|
|
273
|
+
value = JSON.parse(text);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new Error(`Invalid native session record at ${path}: ${String(error)}`);
|
|
276
|
+
}
|
|
277
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
278
|
+
throw new Error(`Invalid native session record at ${path}: expected an object.`);
|
|
279
|
+
}
|
|
280
|
+
const record = value;
|
|
281
|
+
const keys = Object.keys(record).sort();
|
|
282
|
+
if (keys.join(",") !== "ownerInstanceId,state,version" || record.version !== RECORD_VERSION || typeof record.ownerInstanceId !== "string" || !UUID3.test(record.ownerInstanceId) || !["active", "pending", "ended"].includes(String(record.state))) {
|
|
283
|
+
throw new Error(`Invalid native session record at ${path}.`);
|
|
284
|
+
}
|
|
285
|
+
return record;
|
|
286
|
+
}
|
|
287
|
+
async function readRecord(path) {
|
|
288
|
+
try {
|
|
289
|
+
return parseRecord(await readFile2(path, "utf8"), path);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error.code === "ENOENT") return null;
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function writeRecord(path, record) {
|
|
296
|
+
const parent = dirname2(path);
|
|
297
|
+
await mkdir2(parent, { recursive: true, mode: 448 });
|
|
298
|
+
await chmod2(parent, 448);
|
|
299
|
+
const temporary = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
300
|
+
try {
|
|
301
|
+
await writeFile2(temporary, `${JSON.stringify(record, null, 2)}
|
|
302
|
+
`, { mode: 384, flag: "wx" });
|
|
303
|
+
await rename2(temporary, path);
|
|
304
|
+
await chmod2(path, 384);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
try {
|
|
307
|
+
await unlink2(temporary);
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function serialized(key, operation) {
|
|
314
|
+
const predecessor = operations.get(key) ?? Promise.resolve();
|
|
315
|
+
let release;
|
|
316
|
+
const gate = new Promise((resolve4) => {
|
|
317
|
+
release = resolve4;
|
|
318
|
+
});
|
|
319
|
+
const tail = predecessor.catch(() => {
|
|
320
|
+
}).then(() => gate);
|
|
321
|
+
operations.set(key, tail);
|
|
322
|
+
await predecessor.catch(() => {
|
|
323
|
+
});
|
|
324
|
+
let releaseGuard;
|
|
325
|
+
try {
|
|
326
|
+
releaseGuard = acquireSessionGuard(key);
|
|
327
|
+
return await operation();
|
|
328
|
+
} finally {
|
|
329
|
+
try {
|
|
330
|
+
releaseGuard?.();
|
|
331
|
+
} finally {
|
|
332
|
+
release();
|
|
333
|
+
if (operations.get(key) === tail) operations.delete(key);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function attach(profile, ownerInstanceId) {
|
|
338
|
+
return attachOursClient({
|
|
339
|
+
endpoint: profile.endpoint,
|
|
340
|
+
expectedInstanceId: profile.expectedInstanceId,
|
|
341
|
+
credentialPath: profile.credentialPath,
|
|
342
|
+
sessionMode: "external",
|
|
343
|
+
leaseToken: ownerInstanceId,
|
|
344
|
+
env: {}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function cachedClient(path, profile, ownerInstanceId) {
|
|
348
|
+
const cached = clients.get(path);
|
|
349
|
+
if (cached?.ownerInstanceId === ownerInstanceId) return cached.promise;
|
|
350
|
+
if (cached) {
|
|
351
|
+
clients.delete(path);
|
|
352
|
+
void cached.promise.then((client) => client.close()).catch(() => {
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const promise = attach(profile, ownerInstanceId);
|
|
356
|
+
const next = { ownerInstanceId, promise };
|
|
357
|
+
clients.set(path, next);
|
|
358
|
+
void promise.catch(() => {
|
|
359
|
+
if (clients.get(path) === next) clients.delete(path);
|
|
360
|
+
});
|
|
361
|
+
return promise;
|
|
362
|
+
}
|
|
363
|
+
async function releaseOwner(path, profile, ownerInstanceId) {
|
|
364
|
+
const cached = clients.get(path);
|
|
365
|
+
const client = cached?.ownerInstanceId === ownerInstanceId ? await cached.promise : await attach(profile, ownerInstanceId);
|
|
366
|
+
try {
|
|
367
|
+
await client.releaseLease();
|
|
368
|
+
} finally {
|
|
369
|
+
if (cached?.ownerInstanceId !== ownerInstanceId) await client.close();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function finishPending(path, profile, ownerInstanceId) {
|
|
373
|
+
await releaseOwner(path, profile, ownerInstanceId);
|
|
374
|
+
const current = await readRecord(path);
|
|
375
|
+
if (current?.state === "pending" && current.ownerInstanceId === ownerInstanceId) {
|
|
376
|
+
await writeRecord(path, { version: RECORD_VERSION, ownerInstanceId, state: "ended" });
|
|
377
|
+
}
|
|
378
|
+
const cached = clients.get(path);
|
|
379
|
+
if (cached?.ownerInstanceId === ownerInstanceId) {
|
|
380
|
+
clients.delete(path);
|
|
381
|
+
void cached.promise.then((client) => client.close()).catch(() => {
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function nativeClientForAtRoot(profileValue, nativeSessionId, hostRecordRoot) {
|
|
386
|
+
const profile = validateHostProfile(profileValue);
|
|
387
|
+
const path = nativeSessionRecordPath(hostRecordRoot, profile.expectedInstanceId, nativeSessionId);
|
|
388
|
+
return serialized(path, async () => {
|
|
389
|
+
let record = await readRecord(path);
|
|
390
|
+
if (record?.state === "pending") {
|
|
391
|
+
await finishPending(path, profile, record.ownerInstanceId);
|
|
392
|
+
record = await readRecord(path);
|
|
393
|
+
}
|
|
394
|
+
if (record?.state === "active") return cachedClient(path, profile, record.ownerInstanceId);
|
|
395
|
+
const ownerInstanceId = randomUUID2();
|
|
396
|
+
await writeRecord(path, { version: RECORD_VERSION, ownerInstanceId, state: "active" });
|
|
397
|
+
return cachedClient(path, profile, ownerInstanceId);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/host-hooks.mjs
|
|
402
|
+
async function readHostHookState({ profile, nativeSessionId, applicationPath, clientFor = nativeClientForAtRoot }) {
|
|
403
|
+
const store = new ApplicationIdentityStore({ instanceId: profile.expectedInstanceId }, { path: applicationPath });
|
|
404
|
+
const identities = await store.list();
|
|
405
|
+
const visible = new Set(identities);
|
|
406
|
+
const client = await clientFor(profile, nativeSessionId, dirname3(applicationPath));
|
|
407
|
+
try {
|
|
408
|
+
const [unread, rows] = await Promise.all([client.unread(), client.listIdentities()]);
|
|
409
|
+
const entries = (Array.isArray(unread?.identities) ? unread.identities : []).flatMap((entry) => {
|
|
410
|
+
if (!entry || !visible.has(entry.name)) return [];
|
|
411
|
+
const count = Number.isSafeInteger(entry.count) ? entry.count : 0;
|
|
412
|
+
const files = Number.isSafeInteger(entry.files) ? entry.files : 0;
|
|
413
|
+
if (count <= 0 && files <= 0) return [];
|
|
414
|
+
const recent = (Array.isArray(entry.recent) ? entry.recent.slice(-5) : []).flatMap((message) => message && typeof message.from === "string" ? [{ from: message.from, msg_id: String(message.msg_id ?? "?"), date: typeof message.date === "string" ? message.date : "" }] : []);
|
|
415
|
+
return [{ name: entry.name, count, files, recent }];
|
|
416
|
+
});
|
|
417
|
+
const bindings = (Array.isArray(rows) ? rows : []).flatMap((row) => row && visible.has(row.name) && ["mine", "other-live"].includes(row.session) ? [row.name] : []);
|
|
418
|
+
return { identities, unread: { identities: entries }, bindings };
|
|
419
|
+
} finally {
|
|
420
|
+
await client.close();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
export {
|
|
424
|
+
readHostHookState
|
|
425
|
+
};
|
package/dist/monitor-mcp.mjs
CHANGED
|
@@ -21426,7 +21426,7 @@ function validateHostProfile(value) {
|
|
|
21426
21426
|
const expectedInstanceId = record2.expectedInstanceId;
|
|
21427
21427
|
const credentialPath = record2.credentialPath;
|
|
21428
21428
|
if (typeof endpoint !== "string" || endpoint.trim() !== endpoint || endpoint === "") {
|
|
21429
|
-
throw profileError("endpoint must be a non-empty HTTP origin.");
|
|
21429
|
+
throw profileError("endpoint must be a non-empty HTTP or HTTPS origin.");
|
|
21430
21430
|
}
|
|
21431
21431
|
if (typeof expectedInstanceId !== "string" || !UUID.test(expectedInstanceId)) {
|
|
21432
21432
|
throw profileError("expectedInstanceId must be a lowercase UUID.");
|
|
@@ -21438,10 +21438,10 @@ function validateHostProfile(value) {
|
|
|
21438
21438
|
try {
|
|
21439
21439
|
url = new URL(endpoint);
|
|
21440
21440
|
} catch {
|
|
21441
|
-
throw profileError("endpoint must be an HTTP origin.");
|
|
21441
|
+
throw profileError("endpoint must be an HTTP or HTTPS origin.");
|
|
21442
21442
|
}
|
|
21443
|
-
if (url.protocol !== "http:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
21444
|
-
throw profileError("endpoint must be an HTTP origin without credentials, path, query, or fragment.");
|
|
21443
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
21444
|
+
throw profileError("endpoint must be an HTTP or HTTPS origin without credentials, path, query, or fragment.");
|
|
21445
21445
|
}
|
|
21446
21446
|
return { endpoint: url.origin, expectedInstanceId, credentialPath };
|
|
21447
21447
|
}
|
|
@@ -21512,9 +21512,6 @@ function hostProfileFromEnv(env = process.env) {
|
|
|
21512
21512
|
return hostProfileSelectionFromEnv(env)?.profile ?? null;
|
|
21513
21513
|
}
|
|
21514
21514
|
|
|
21515
|
-
// ../core/src/host-client/files.ts
|
|
21516
|
-
import { buildIdentityFile, writeIdentityFile } from "@ours.network/sdk/connector";
|
|
21517
|
-
|
|
21518
21515
|
// ../core/src/native-session.ts
|
|
21519
21516
|
import { attachOursClient } from "@ours.network/sdk";
|
|
21520
21517
|
|
package/dist/network-watch.mjs
CHANGED
|
@@ -28,7 +28,7 @@ function validateHostProfile(value) {
|
|
|
28
28
|
const expectedInstanceId = record.expectedInstanceId;
|
|
29
29
|
const credentialPath = record.credentialPath;
|
|
30
30
|
if (typeof endpoint !== "string" || endpoint.trim() !== endpoint || endpoint === "") {
|
|
31
|
-
throw profileError("endpoint must be a non-empty HTTP origin.");
|
|
31
|
+
throw profileError("endpoint must be a non-empty HTTP or HTTPS origin.");
|
|
32
32
|
}
|
|
33
33
|
if (typeof expectedInstanceId !== "string" || !UUID.test(expectedInstanceId)) {
|
|
34
34
|
throw profileError("expectedInstanceId must be a lowercase UUID.");
|
|
@@ -40,10 +40,10 @@ function validateHostProfile(value) {
|
|
|
40
40
|
try {
|
|
41
41
|
url = new URL(endpoint);
|
|
42
42
|
} catch {
|
|
43
|
-
throw profileError("endpoint must be an HTTP origin.");
|
|
43
|
+
throw profileError("endpoint must be an HTTP or HTTPS origin.");
|
|
44
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.");
|
|
45
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
46
|
+
throw profileError("endpoint must be an HTTP or HTTPS origin without credentials, path, query, or fragment.");
|
|
47
47
|
}
|
|
48
48
|
return { endpoint: url.origin, expectedInstanceId, credentialPath };
|
|
49
49
|
}
|
|
@@ -114,9 +114,6 @@ function hostProfileFromEnv(env = process.env) {
|
|
|
114
114
|
return hostProfileSelectionFromEnv(env)?.profile ?? null;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// ../core/src/host-client/files.ts
|
|
118
|
-
import { buildIdentityFile, writeIdentityFile } from "@ours.network/sdk/connector";
|
|
119
|
-
|
|
120
117
|
// ../core/src/native-session.ts
|
|
121
118
|
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
122
119
|
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
package/dist/profile.mjs
CHANGED
|
@@ -30,7 +30,7 @@ function validateHostProfile(value) {
|
|
|
30
30
|
const expectedInstanceId = record.expectedInstanceId;
|
|
31
31
|
const credentialPath = record.credentialPath;
|
|
32
32
|
if (typeof endpoint !== "string" || endpoint.trim() !== endpoint || endpoint === "") {
|
|
33
|
-
throw profileError("endpoint must be a non-empty HTTP origin.");
|
|
33
|
+
throw profileError("endpoint must be a non-empty HTTP or HTTPS origin.");
|
|
34
34
|
}
|
|
35
35
|
if (typeof expectedInstanceId !== "string" || !UUID.test(expectedInstanceId)) {
|
|
36
36
|
throw profileError("expectedInstanceId must be a lowercase UUID.");
|
|
@@ -42,10 +42,10 @@ function validateHostProfile(value) {
|
|
|
42
42
|
try {
|
|
43
43
|
url = new URL(endpoint);
|
|
44
44
|
} catch {
|
|
45
|
-
throw profileError("endpoint must be an HTTP origin.");
|
|
45
|
+
throw profileError("endpoint must be an HTTP or HTTPS origin.");
|
|
46
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.");
|
|
47
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
48
|
+
throw profileError("endpoint must be an HTTP or HTTPS origin without credentials, path, query, or fragment.");
|
|
49
49
|
}
|
|
50
50
|
return { endpoint: url.origin, expectedInstanceId, credentialPath };
|
|
51
51
|
}
|
|
@@ -113,9 +113,6 @@ function hostProfileSelectionFromEnv(env = process.env) {
|
|
|
113
113
|
return { profile, configPath };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
// ../core/src/host-client/files.ts
|
|
117
|
-
import { buildIdentityFile, writeIdentityFile } from "@ours.network/sdk/connector";
|
|
118
|
-
|
|
119
116
|
// ../core/src/native-session.ts
|
|
120
117
|
import { attachOursClient } from "@ours.network/sdk";
|
|
121
118
|
|
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.5",
|
|
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.9",
|
|
50
50
|
"ws": "^8.21.0",
|
|
51
|
-
"zod": "^3.25.76"
|
|
51
|
+
"zod": "^3.25.76",
|
|
52
|
+
"@ours.network/mcp": "1.2.0-nightly.5"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"esbuild": "^0.24.2"
|
package/src/hooks/runner.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
import { sendControlCommand } from '../control-server.mjs';
|
|
8
8
|
import { resolveDaemonProfile } from '../profile.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { readHostHookState } from '../host-hooks.mjs';
|
|
10
10
|
import { attachOursClient } from '@ours.network/sdk';
|
|
11
11
|
import { hostProfileFromEnv } from '../../../core/src/host-client/index.ts';
|
|
12
12
|
|
|
@@ -60,16 +60,7 @@ function renderContext(unread, pin) {
|
|
|
60
60
|
return lines.join('\n');
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function
|
|
64
|
-
const text = value?.contents?.find((item) => item?.uri === 'ours://application-identities')?.text;
|
|
65
|
-
const parsed = JSON.parse(text);
|
|
66
|
-
if (!Array.isArray(parsed?.identities) || parsed.identities.some((name) => typeof name !== 'string')) {
|
|
67
|
-
throw new Error('invalid application identity resource');
|
|
68
|
-
}
|
|
69
|
-
return new Set(parsed.identities);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function handleHook(payload, { env = process.env, fetch: fetchImpl = globalThis.fetch, send = sendControlCommand, findPin = defaultFindPin, profileResolver = resolveDaemonProfile, clientFactory = attachOursClient, networkSessionFactory = createHostSessionFactory } = {}) {
|
|
63
|
+
export async function handleHook(payload, { env = process.env, fetch: fetchImpl = globalThis.fetch, send = sendControlCommand, findPin = defaultFindPin, profileResolver = resolveDaemonProfile, clientFactory = attachOursClient, readHostState = readHostHookState } = {}) {
|
|
73
64
|
try {
|
|
74
65
|
const event = payload?.hook_event_name;
|
|
75
66
|
const socket = env.OURS_CODEX_CONTROL_SOCKET;
|
|
@@ -97,18 +88,12 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
|
|
|
97
88
|
const nativeSessionId = payload.session_id;
|
|
98
89
|
if (typeof nativeSessionId !== 'string' || !nativeSessionId) return { continue: true };
|
|
99
90
|
const appPath = env.OURS_MCP_CONFIG || join(env.HOME || homedir(), '.ours-mcp', 'config.json');
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const resource = await session.request({ method: 'resources/read', params: { uri: 'ours://application-identities' } });
|
|
105
|
-
const visible = applicationIdentitiesFromResource(resource);
|
|
106
|
-
const unread = safeUnread(await session.fileClient.unread()).filter((entry) => visible.has(entry.name));
|
|
107
|
-
const pin = await findPin(payload.cwd || process.cwd());
|
|
108
|
-
const context = renderContext(unread, pin);
|
|
109
|
-
return context ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } } : { continue: true };
|
|
110
|
-
} finally { await session?.close(); }
|
|
91
|
+
const state = await readHostState({ profile: hostProfile, nativeSessionId, applicationPath: appPath });
|
|
92
|
+
const pin = await findPin(payload.cwd || process.cwd());
|
|
93
|
+
const context = renderContext(state.unread.identities, pin);
|
|
94
|
+
return context ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } } : { continue: true };
|
|
111
95
|
}
|
|
96
|
+
|
|
112
97
|
// Standard Codex does not pass through ours-codex's resolved environment.
|
|
113
98
|
// Resolve the same coherent SDK selection here so SessionStart and
|
|
114
99
|
// UserPromptSubmit inspect the same daemon as the stdio proxy.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { ApplicationIdentityStore } from '../../core/src/application-identities.ts';
|
|
3
|
+
import { nativeClientForAtRoot } from '../../core/src/host-client/index.ts';
|
|
4
|
+
|
|
5
|
+
/** Body-free hook metadata, scoped to this client's adopted identities. */
|
|
6
|
+
export async function readHostHookState({ profile, nativeSessionId, applicationPath, clientFor = nativeClientForAtRoot }) {
|
|
7
|
+
const store = new ApplicationIdentityStore({ instanceId: profile.expectedInstanceId }, { path: applicationPath });
|
|
8
|
+
const identities = await store.list();
|
|
9
|
+
const visible = new Set(identities);
|
|
10
|
+
const client = await clientFor(profile, nativeSessionId, dirname(applicationPath));
|
|
11
|
+
try {
|
|
12
|
+
const [unread, rows] = await Promise.all([client.unread(), client.listIdentities()]);
|
|
13
|
+
const entries = (Array.isArray(unread?.identities) ? unread.identities : []).flatMap(entry => {
|
|
14
|
+
if (!entry || !visible.has(entry.name)) return [];
|
|
15
|
+
const count = Number.isSafeInteger(entry.count) ? entry.count : 0;
|
|
16
|
+
const files = Number.isSafeInteger(entry.files) ? entry.files : 0;
|
|
17
|
+
if (count <= 0 && files <= 0) return [];
|
|
18
|
+
const recent = (Array.isArray(entry.recent) ? entry.recent.slice(-5) : []).flatMap(message =>
|
|
19
|
+
message && typeof message.from === 'string' ? [{ from: message.from, msg_id: String(message.msg_id ?? '?'), date: typeof message.date === 'string' ? message.date : '' }] : []);
|
|
20
|
+
return [{ name: entry.name, count, files, recent }];
|
|
21
|
+
});
|
|
22
|
+
const bindings = (Array.isArray(rows) ? rows : []).flatMap(row => row && visible.has(row.name) && ['mine', 'other-live'].includes(row.session) ? [row.name] : []);
|
|
23
|
+
return { identities, unread: { identities: entries }, bindings };
|
|
24
|
+
} finally { await client.close(); }
|
|
25
|
+
}
|
package/bin/container-launch.mjs
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { isAbsolute, join } from 'node:path';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
|
|
6
|
-
// An explicit container selection is authoritative: failures never fall back
|
|
7
|
-
// to a host MCP or host application-identity configuration.
|
|
8
|
-
export function containerInvocation(command, args = [], env = process.env) {
|
|
9
|
-
const explicitPath = (env.OURS_CONFIG ?? '').trim();
|
|
10
|
-
const path = explicitPath || join(homedir(), '.ours', 'config.json');
|
|
11
|
-
let profile;
|
|
12
|
-
try { profile = JSON.parse(readFileSync(path, 'utf8')); }
|
|
13
|
-
catch (error) {
|
|
14
|
-
if (!explicitPath && error.code === 'ENOENT') return null;
|
|
15
|
-
throw new Error(`cannot read OURS_CONFIG profile ${path}: ${error.message}`);
|
|
16
|
-
}
|
|
17
|
-
if (!profile || !Object.hasOwn(profile, 'composeFile')) return null;
|
|
18
|
-
if (typeof profile.composeFile !== 'string' || !isAbsolute(profile.composeFile)) throw new Error('composeFile must be an absolute path');
|
|
19
|
-
if (typeof profile.expectedInstanceId !== 'string' || !profile.expectedInstanceId.trim()) throw new Error('container profile requires expectedInstanceId');
|
|
20
|
-
if (profile.composeProject !== undefined && (typeof profile.composeProject !== 'string' || !profile.composeProject.trim())) throw new Error('composeProject must be a nonempty string');
|
|
21
|
-
const dockerArgs = ['compose', '-f', profile.composeFile];
|
|
22
|
-
if (profile.composeProject !== undefined) dockerArgs.push('-p', profile.composeProject);
|
|
23
|
-
dockerArgs.push('exec', '-T');
|
|
24
|
-
for (const name of ['CLAUDE_CODE_SESSION_ID', 'OURS_BIND_IDENTITY']) {
|
|
25
|
-
if (env[name] !== undefined) dockerArgs.push('-e', `${name}=${env[name]}`);
|
|
26
|
-
}
|
|
27
|
-
dockerArgs.push('daemon', 'node', '/opt/ours/node_modules/@ours.network/mcp/dist/container.js', profile.expectedInstanceId, command, ...args);
|
|
28
|
-
return { command: 'docker', args: dockerArgs, env: { ...env, OURS_DAEMON_ID: profile.expectedInstanceId } };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function readContainerJson(command, env = process.env) {
|
|
32
|
-
const invocation = containerInvocation(command, [], env);
|
|
33
|
-
if (!invocation) return null;
|
|
34
|
-
const child = spawnSync(invocation.command, invocation.args, { env: invocation.env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000, maxBuffer: 1024 * 1024 });
|
|
35
|
-
if (child.error) throw child.error;
|
|
36
|
-
if (child.status !== 0) throw new Error(`ours container ${command} failed: ${child.stderr.trim() || child.status}`);
|
|
37
|
-
const value = JSON.parse(child.stdout);
|
|
38
|
-
if (value === null) throw new Error(`ours container ${command} returned null`);
|
|
39
|
-
return value;
|
|
40
|
-
}
|