@ours.network/codex 1.1.1 → 1.2.0-nightly.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +2 -2
- package/README.md +26 -15
- package/bin/codex-config-install.mjs +3 -3
- package/bin/container-file-transfer.mjs +147 -0
- package/bin/network-watch.mjs +8 -0
- package/bin/ours-codex-install.mjs +4 -31
- package/bin/proxy.mjs +25 -20
- package/dist/hooks-runner.mjs +562 -0
- package/dist/host-hooks.mjs +386 -0
- package/dist/monitor-mcp.mjs +32 -7
- package/dist/network-watch.mjs +287 -0
- package/dist/profile.mjs +56 -0
- package/hooks/hooks.json +3 -3
- package/install.sh +14 -33
- package/package.json +5 -5
- package/skills/ours/SKILL.md +8 -7
- package/skills/ours/references/configuration.md +20 -22
- package/src/hooks/runner.mjs +20 -35
- package/src/host-hooks.mjs +25 -0
- package/src/launcher.mjs +41 -15
- package/src/monitor-mcp.mjs +19 -6
- package/src/network-watch.mjs +45 -0
- package/src/profile.mjs +15 -39
- package/src/watcher.mjs +29 -3
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/runner.mjs
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
6
|
+
import { homedir as homedir2 } from "node:os";
|
|
7
|
+
import { dirname as dirname4, join as join3, resolve as resolve3 } from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
|
|
10
|
+
// src/control-server.mjs
|
|
11
|
+
import { createServer, createConnection } from "node:net";
|
|
12
|
+
|
|
13
|
+
// src/control-protocol.mjs
|
|
14
|
+
var MAX_LINE = 64 * 1024;
|
|
15
|
+
function encodeControlResponse(value) {
|
|
16
|
+
return `${JSON.stringify(value)}
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/control-server.mjs
|
|
21
|
+
function sendControlCommand(socketPath, capability, message, timeoutMs = 2e3) {
|
|
22
|
+
return new Promise((resolve4, reject) => {
|
|
23
|
+
const socket = createConnection(socketPath);
|
|
24
|
+
let buffer = "";
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
socket.destroy();
|
|
27
|
+
reject(new Error("monitor control timed out"));
|
|
28
|
+
}, timeoutMs);
|
|
29
|
+
socket.setEncoding("utf8");
|
|
30
|
+
socket.on("connect", () => socket.write(encodeControlResponse({ ...message, capability })));
|
|
31
|
+
socket.on("data", (chunk) => {
|
|
32
|
+
buffer += chunk;
|
|
33
|
+
});
|
|
34
|
+
socket.on("error", (error) => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
reject(error);
|
|
37
|
+
});
|
|
38
|
+
socket.on("end", () => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
try {
|
|
41
|
+
const response = JSON.parse(buffer);
|
|
42
|
+
if (!response.ok) reject(new Error(response.error || "monitor control failed"));
|
|
43
|
+
else resolve4(response);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
reject(error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/profile.mjs
|
|
52
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
53
|
+
import { attachOursClient as attachOursClient2 } from "@ours.network/sdk";
|
|
54
|
+
|
|
55
|
+
// ../core/src/host-profile.ts
|
|
56
|
+
import { readGatewayClientProfile, validateGatewayClientProfile } from "@ours.network/sdk/client";
|
|
57
|
+
var validateHostProfile = validateGatewayClientProfile;
|
|
58
|
+
function hostProfileSelectionFromEnv(env = process.env) {
|
|
59
|
+
const { configPath, ...profile } = readGatewayClientProfile(env);
|
|
60
|
+
return { profile, configPath };
|
|
61
|
+
}
|
|
62
|
+
function hostProfileFromEnv(env = process.env) {
|
|
63
|
+
return hostProfileSelectionFromEnv(env).profile;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ../core/src/native-session.ts
|
|
67
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
68
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
69
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
|
|
70
|
+
import { attachOursClient } from "@ours.network/sdk";
|
|
71
|
+
|
|
72
|
+
// ../core/src/application-identities.ts
|
|
73
|
+
import { randomUUID } from "node:crypto";
|
|
74
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
75
|
+
import { homedir } from "node:os";
|
|
76
|
+
import { dirname, resolve } from "node:path";
|
|
77
|
+
var APPLICATION_IDENTITIES_VERSION = 1;
|
|
78
|
+
var APPLICATION_IDENTITIES_ENV = "OURS_MCP_CONFIG";
|
|
79
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
80
|
+
var emptyConfig = () => ({ version: 1, daemons: {}, instances: {} });
|
|
81
|
+
function applicationIdentityConfigPath(env = process.env) {
|
|
82
|
+
const explicit = (env[APPLICATION_IDENTITIES_ENV] ?? "").trim();
|
|
83
|
+
return resolve(explicit || resolve(homedir(), ".ours-mcp", "config.json"));
|
|
84
|
+
}
|
|
85
|
+
function parseRows(raw, path, namespace, normalizeKey) {
|
|
86
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
87
|
+
throw new Error(`Invalid ours-mcp application identity config at ${path}: "${namespace}s" must be an object.`);
|
|
88
|
+
}
|
|
89
|
+
const rows = {};
|
|
90
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
91
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
92
|
+
throw new Error(`Invalid ours-mcp application identity config at ${path}: ${namespace} ${JSON.stringify(key)} must be an object.`);
|
|
93
|
+
}
|
|
94
|
+
const row = value;
|
|
95
|
+
if (!Array.isArray(row.identities) || row.identities.some((name) => typeof name !== "string" || name.length === 0)) {
|
|
96
|
+
throw new Error(`Invalid ours-mcp application identity config at ${path}: ${namespace} ${JSON.stringify(key)} has an invalid identity list.`);
|
|
97
|
+
}
|
|
98
|
+
rows[normalizeKey(key)] = { identities: [...new Set(row.identities)].sort() };
|
|
99
|
+
}
|
|
100
|
+
return rows;
|
|
101
|
+
}
|
|
102
|
+
function parseConfig(text, path) {
|
|
103
|
+
let value;
|
|
104
|
+
try {
|
|
105
|
+
value = JSON.parse(text);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`Invalid ours-mcp application identity config at ${path}: ${String(error)}`);
|
|
108
|
+
}
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid ours-mcp application identity config at ${path}: expected an object.`);
|
|
110
|
+
const record = value;
|
|
111
|
+
if (record.version !== APPLICATION_IDENTITIES_VERSION) {
|
|
112
|
+
throw new Error(`Unsupported ours-mcp application identity config version at ${path}: expected ${APPLICATION_IDENTITIES_VERSION}, found ${String(record.version)}.`);
|
|
113
|
+
}
|
|
114
|
+
const daemons = parseRows(record.daemons, path, "daemon", resolve);
|
|
115
|
+
const instances = record.instances === void 0 ? {} : parseRows(record.instances, path, "instance", (instanceId) => {
|
|
116
|
+
if (!UUID.test(instanceId)) throw new Error(`Invalid ours-mcp application identity config at ${path}: instance ${JSON.stringify(instanceId)} must be a lowercase UUID.`);
|
|
117
|
+
return instanceId;
|
|
118
|
+
});
|
|
119
|
+
return { version: 1, daemons, instances };
|
|
120
|
+
}
|
|
121
|
+
async function readConfig(path) {
|
|
122
|
+
try {
|
|
123
|
+
return parseConfig(await readFile(path, "utf8"), path);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error.code === "ENOENT") return emptyConfig();
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function writeConfigAtomic(path, config) {
|
|
130
|
+
const parent = dirname(path);
|
|
131
|
+
await mkdir(parent, { recursive: true, mode: 448 });
|
|
132
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
133
|
+
try {
|
|
134
|
+
await writeFile(temporary, `${JSON.stringify(config, null, 2)}
|
|
135
|
+
`, { mode: 384, flag: "wx" });
|
|
136
|
+
await rename(temporary, path);
|
|
137
|
+
await chmod(path, 384);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
try {
|
|
140
|
+
await unlink(temporary);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
var ApplicationIdentityStore = class {
|
|
147
|
+
stateDir;
|
|
148
|
+
instanceId;
|
|
149
|
+
path;
|
|
150
|
+
constructor(target, options = {}) {
|
|
151
|
+
if (typeof target === "string") {
|
|
152
|
+
this.stateDir = resolve(target);
|
|
153
|
+
this.instanceId = null;
|
|
154
|
+
} else {
|
|
155
|
+
if (!target || !UUID.test(target.instanceId)) throw new Error("Application identity instanceId must be a lowercase UUID.");
|
|
156
|
+
this.stateDir = null;
|
|
157
|
+
this.instanceId = target.instanceId;
|
|
158
|
+
}
|
|
159
|
+
this.path = resolve(options.path ?? applicationIdentityConfigPath(options.env));
|
|
160
|
+
}
|
|
161
|
+
rows(config) {
|
|
162
|
+
return this.instanceId === null ? config.daemons : config.instances;
|
|
163
|
+
}
|
|
164
|
+
key() {
|
|
165
|
+
return this.instanceId ?? this.stateDir;
|
|
166
|
+
}
|
|
167
|
+
async list() {
|
|
168
|
+
const config = await readConfig(this.path);
|
|
169
|
+
return [...this.rows(config)[this.key()]?.identities ?? []];
|
|
170
|
+
}
|
|
171
|
+
async has(name) {
|
|
172
|
+
return (await this.list()).includes(name);
|
|
173
|
+
}
|
|
174
|
+
async add(name) {
|
|
175
|
+
if (!name) throw new Error("Cannot add an empty identity name to ours-mcp.");
|
|
176
|
+
const config = await readConfig(this.path);
|
|
177
|
+
const rows = this.rows(config);
|
|
178
|
+
const key = this.key();
|
|
179
|
+
const identities = rows[key]?.identities ?? [];
|
|
180
|
+
if (identities.includes(name)) return;
|
|
181
|
+
rows[key] = { identities: [...identities, name].sort() };
|
|
182
|
+
await writeConfigAtomic(this.path, config);
|
|
183
|
+
}
|
|
184
|
+
async remove(name) {
|
|
185
|
+
const config = await readConfig(this.path);
|
|
186
|
+
const rows = this.rows(config);
|
|
187
|
+
const key = this.key();
|
|
188
|
+
const current = rows[key];
|
|
189
|
+
if (!current?.identities.includes(name)) return;
|
|
190
|
+
const identities = current.identities.filter((candidate) => candidate !== name);
|
|
191
|
+
if (identities.length === 0) delete rows[key];
|
|
192
|
+
else rows[key] = { identities };
|
|
193
|
+
await writeConfigAtomic(this.path, config);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// ../core/src/session-guard.ts
|
|
198
|
+
import { createHash } from "node:crypto";
|
|
199
|
+
import { openSync, closeSync, lstatSync, mkdirSync, chmodSync, constants } from "node:fs";
|
|
200
|
+
import { join } from "node:path";
|
|
201
|
+
import { createRequire } from "node:module";
|
|
202
|
+
var BUSY_TIMEOUT_MS = 5e3;
|
|
203
|
+
var DatabaseSync = null;
|
|
204
|
+
var resolveAttempted = false;
|
|
205
|
+
function resolveDatabase() {
|
|
206
|
+
if (DatabaseSync) return DatabaseSync;
|
|
207
|
+
if (resolveAttempted) throw new Error("No SQLite implementation available for native session guard");
|
|
208
|
+
resolveAttempted = true;
|
|
209
|
+
try {
|
|
210
|
+
const mod = createRequire(import.meta.url)("node:sqlite");
|
|
211
|
+
DatabaseSync = mod.DatabaseSync;
|
|
212
|
+
return DatabaseSync;
|
|
213
|
+
} catch {
|
|
214
|
+
try {
|
|
215
|
+
const mod = createRequire(import.meta.url)("better-sqlite3");
|
|
216
|
+
DatabaseSync = mod.default ?? mod;
|
|
217
|
+
return DatabaseSync;
|
|
218
|
+
} catch {
|
|
219
|
+
throw new Error("Native session guard requires node:sqlite (Node >=22.13) or better-sqlite3");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function ensureGuardFile(guardPath) {
|
|
224
|
+
try {
|
|
225
|
+
const fd = openSync(guardPath, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
226
|
+
closeSync(fd);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error.code !== "EEXIST") throw error;
|
|
229
|
+
}
|
|
230
|
+
const stat = lstatSync(guardPath);
|
|
231
|
+
if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 63) !== 0 || stat.nlink !== 1) {
|
|
232
|
+
throw new Error("Native session guard file must be an owner-only regular file");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function acquireSessionGuard(recordPath) {
|
|
236
|
+
const uid = process.getuid();
|
|
237
|
+
const base = join(`/tmp/ours-${uid}`, "session-guards");
|
|
238
|
+
mkdirSync(base, { recursive: true, mode: 448 });
|
|
239
|
+
try {
|
|
240
|
+
chmodSync(base, 448);
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
const hash = createHash("sha256").update(recordPath).digest("hex");
|
|
244
|
+
const guardPath = join(base, `${hash}.guard`);
|
|
245
|
+
ensureGuardFile(guardPath);
|
|
246
|
+
const before = lstatSync(guardPath);
|
|
247
|
+
const Database = resolveDatabase();
|
|
248
|
+
const db = new Database(guardPath);
|
|
249
|
+
try {
|
|
250
|
+
const after = lstatSync(guardPath);
|
|
251
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode || before.uid !== after.uid) {
|
|
252
|
+
throw new Error("Native session guard file changed during acquisition");
|
|
253
|
+
}
|
|
254
|
+
db.exec(`PRAGMA busy_timeout=${BUSY_TIMEOUT_MS}; BEGIN EXCLUSIVE`);
|
|
255
|
+
return () => {
|
|
256
|
+
try {
|
|
257
|
+
db.close();
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
} catch (error) {
|
|
262
|
+
try {
|
|
263
|
+
db.close();
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ../core/src/native-session.ts
|
|
271
|
+
var RECORD_VERSION = 1;
|
|
272
|
+
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
273
|
+
var clients = /* @__PURE__ */ new Map();
|
|
274
|
+
var operations = /* @__PURE__ */ new Map();
|
|
275
|
+
function selector(value) {
|
|
276
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value.trim() !== value || value.includes("\0")) {
|
|
277
|
+
throw new Error("Native session metadata is missing or invalid for this host-profile tool call.");
|
|
278
|
+
}
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
function nativeSessionRecordPath(hostRecordRoot, instanceId, nativeSessionId) {
|
|
282
|
+
if (!hostRecordRoot || !hostRecordRoot.startsWith("/") || resolve2(hostRecordRoot) !== hostRecordRoot) {
|
|
283
|
+
throw new Error("Native session host record root must be a normalized absolute path.");
|
|
284
|
+
}
|
|
285
|
+
if (!UUID2.test(instanceId)) throw new Error("Native session daemon instance must be a lowercase UUID.");
|
|
286
|
+
const hash = createHash2("sha256").update(selector(nativeSessionId), "utf8").digest("hex");
|
|
287
|
+
return join2(hostRecordRoot, "sessions", instanceId, `${hash}.json`);
|
|
288
|
+
}
|
|
289
|
+
function parseRecord(text, path) {
|
|
290
|
+
let value;
|
|
291
|
+
try {
|
|
292
|
+
value = JSON.parse(text);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
throw new Error(`Invalid native session record at ${path}: ${String(error)}`);
|
|
295
|
+
}
|
|
296
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
297
|
+
throw new Error(`Invalid native session record at ${path}: expected an object.`);
|
|
298
|
+
}
|
|
299
|
+
const record = value;
|
|
300
|
+
const keys = Object.keys(record).sort();
|
|
301
|
+
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))) {
|
|
302
|
+
throw new Error(`Invalid native session record at ${path}.`);
|
|
303
|
+
}
|
|
304
|
+
return record;
|
|
305
|
+
}
|
|
306
|
+
async function readRecord(path) {
|
|
307
|
+
try {
|
|
308
|
+
return parseRecord(await readFile2(path, "utf8"), path);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error.code === "ENOENT") return null;
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function writeRecord(path, record) {
|
|
315
|
+
const parent = dirname2(path);
|
|
316
|
+
await mkdir2(parent, { recursive: true, mode: 448 });
|
|
317
|
+
await chmod2(parent, 448);
|
|
318
|
+
const temporary = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
319
|
+
try {
|
|
320
|
+
await writeFile2(temporary, `${JSON.stringify(record, null, 2)}
|
|
321
|
+
`, { mode: 384, flag: "wx" });
|
|
322
|
+
await rename2(temporary, path);
|
|
323
|
+
await chmod2(path, 384);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
try {
|
|
326
|
+
await unlink2(temporary);
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function serialized(key, operation) {
|
|
333
|
+
const predecessor = operations.get(key) ?? Promise.resolve();
|
|
334
|
+
let release;
|
|
335
|
+
const gate = new Promise((resolve4) => {
|
|
336
|
+
release = resolve4;
|
|
337
|
+
});
|
|
338
|
+
const tail = predecessor.catch(() => {
|
|
339
|
+
}).then(() => gate);
|
|
340
|
+
operations.set(key, tail);
|
|
341
|
+
await predecessor.catch(() => {
|
|
342
|
+
});
|
|
343
|
+
let releaseGuard;
|
|
344
|
+
try {
|
|
345
|
+
releaseGuard = acquireSessionGuard(key);
|
|
346
|
+
return await operation();
|
|
347
|
+
} finally {
|
|
348
|
+
try {
|
|
349
|
+
releaseGuard?.();
|
|
350
|
+
} finally {
|
|
351
|
+
release();
|
|
352
|
+
if (operations.get(key) === tail) operations.delete(key);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function attach(profile, ownerInstanceId) {
|
|
357
|
+
return attachOursClient({
|
|
358
|
+
endpoint: profile.endpoint,
|
|
359
|
+
expectedInstanceId: profile.expectedInstanceId,
|
|
360
|
+
credentialPath: profile.credentialPath,
|
|
361
|
+
sessionMode: "external",
|
|
362
|
+
leaseToken: ownerInstanceId,
|
|
363
|
+
env: {}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
function cachedClient(path, profile, ownerInstanceId) {
|
|
367
|
+
const cached = clients.get(path);
|
|
368
|
+
if (cached?.ownerInstanceId === ownerInstanceId) return cached.promise;
|
|
369
|
+
if (cached) {
|
|
370
|
+
clients.delete(path);
|
|
371
|
+
void cached.promise.then((client) => client.close()).catch(() => {
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const promise = attach(profile, ownerInstanceId);
|
|
375
|
+
const next = { ownerInstanceId, promise };
|
|
376
|
+
clients.set(path, next);
|
|
377
|
+
void promise.catch(() => {
|
|
378
|
+
if (clients.get(path) === next) clients.delete(path);
|
|
379
|
+
});
|
|
380
|
+
return promise;
|
|
381
|
+
}
|
|
382
|
+
async function releaseOwner(path, profile, ownerInstanceId) {
|
|
383
|
+
const cached = clients.get(path);
|
|
384
|
+
const client = cached?.ownerInstanceId === ownerInstanceId ? await cached.promise : await attach(profile, ownerInstanceId);
|
|
385
|
+
try {
|
|
386
|
+
await client.releaseLease();
|
|
387
|
+
} finally {
|
|
388
|
+
if (cached?.ownerInstanceId !== ownerInstanceId) await client.close();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function finishPending(path, profile, ownerInstanceId) {
|
|
392
|
+
await releaseOwner(path, profile, ownerInstanceId);
|
|
393
|
+
const current = await readRecord(path);
|
|
394
|
+
if (current?.state === "pending" && current.ownerInstanceId === ownerInstanceId) {
|
|
395
|
+
await writeRecord(path, { version: RECORD_VERSION, ownerInstanceId, state: "ended" });
|
|
396
|
+
}
|
|
397
|
+
const cached = clients.get(path);
|
|
398
|
+
if (cached?.ownerInstanceId === ownerInstanceId) {
|
|
399
|
+
clients.delete(path);
|
|
400
|
+
void cached.promise.then((client) => client.close()).catch(() => {
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async function nativeClientForAtRoot(profileValue, nativeSessionId, hostRecordRoot) {
|
|
405
|
+
const profile = validateHostProfile(profileValue);
|
|
406
|
+
const path = nativeSessionRecordPath(hostRecordRoot, profile.expectedInstanceId, nativeSessionId);
|
|
407
|
+
return serialized(path, async () => {
|
|
408
|
+
let record = await readRecord(path);
|
|
409
|
+
if (record?.state === "pending") {
|
|
410
|
+
await finishPending(path, profile, record.ownerInstanceId);
|
|
411
|
+
record = await readRecord(path);
|
|
412
|
+
}
|
|
413
|
+
if (record?.state === "active") return cachedClient(path, profile, record.ownerInstanceId);
|
|
414
|
+
const ownerInstanceId = randomUUID2();
|
|
415
|
+
await writeRecord(path, { version: RECORD_VERSION, ownerInstanceId, state: "active" });
|
|
416
|
+
return cachedClient(path, profile, ownerInstanceId);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/profile.mjs
|
|
421
|
+
function validPort(value) {
|
|
422
|
+
const port = Number(value);
|
|
423
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`ours: ${value} is not a valid TCP port`);
|
|
424
|
+
return port;
|
|
425
|
+
}
|
|
426
|
+
function parseOursArgs(argv = []) {
|
|
427
|
+
const codexArgs = [];
|
|
428
|
+
let port;
|
|
429
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
430
|
+
const arg = argv[index];
|
|
431
|
+
if (arg === "--ours-port") {
|
|
432
|
+
if (argv[index + 1] == null) throw new Error("ours: --ours-port requires a value");
|
|
433
|
+
port = validPort(argv[++index]);
|
|
434
|
+
} else if (arg.startsWith("--ours-port=")) {
|
|
435
|
+
port = validPort(arg.slice("--ours-port=".length));
|
|
436
|
+
} else {
|
|
437
|
+
codexArgs.push(arg);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return { port, codexArgs };
|
|
441
|
+
}
|
|
442
|
+
async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch, attach: attach2 = attachOursClient2 } = {}) {
|
|
443
|
+
const parsed = parseOursArgs(argv);
|
|
444
|
+
const hostSelection = hostProfileSelectionFromEnv(env);
|
|
445
|
+
const { profile: hostProfile, configPath } = hostSelection;
|
|
446
|
+
if (parsed.port != null) throw new Error("ours: --ours-port conflicts with host-profile mode");
|
|
447
|
+
const client = await attach2({ ...hostProfile, sessionMode: "external", leaseToken: randomUUID3(), env: {} });
|
|
448
|
+
let info;
|
|
449
|
+
try {
|
|
450
|
+
info = await client.version();
|
|
451
|
+
await client.identities();
|
|
452
|
+
await client.unread();
|
|
453
|
+
} finally {
|
|
454
|
+
await client.close();
|
|
455
|
+
}
|
|
456
|
+
return { profile: hostProfile, info, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/host-hooks.mjs
|
|
460
|
+
import { dirname as dirname3 } from "node:path";
|
|
461
|
+
async function readHostHookState({ profile, nativeSessionId, applicationPath, clientFor = nativeClientForAtRoot }) {
|
|
462
|
+
const store = new ApplicationIdentityStore({ instanceId: profile.expectedInstanceId }, { path: applicationPath });
|
|
463
|
+
const identities = await store.list();
|
|
464
|
+
const visible = new Set(identities);
|
|
465
|
+
const client = await clientFor(profile, nativeSessionId, dirname3(applicationPath));
|
|
466
|
+
try {
|
|
467
|
+
const [unread, rows] = await Promise.all([client.unread(), client.listIdentities()]);
|
|
468
|
+
const entries = (Array.isArray(unread?.identities) ? unread.identities : []).flatMap((entry) => {
|
|
469
|
+
if (!entry || !visible.has(entry.name)) return [];
|
|
470
|
+
const count = Number.isSafeInteger(entry.count) ? entry.count : 0;
|
|
471
|
+
const files = Number.isSafeInteger(entry.files) ? entry.files : 0;
|
|
472
|
+
if (count <= 0 && files <= 0) return [];
|
|
473
|
+
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 : "" }] : []);
|
|
474
|
+
return [{ name: entry.name, count, files, recent }];
|
|
475
|
+
});
|
|
476
|
+
const bindings = (Array.isArray(rows) ? rows : []).flatMap((row) => row && visible.has(row.name) && ["mine", "other-live"].includes(row.session) ? [row.name] : []);
|
|
477
|
+
return { identities, unread: { identities: entries }, bindings };
|
|
478
|
+
} finally {
|
|
479
|
+
await client.close();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/hooks/runner.mjs
|
|
484
|
+
import { attachOursClient as attachOursClient3 } from "@ours.network/sdk";
|
|
485
|
+
async function defaultFindPin(cwd) {
|
|
486
|
+
let dir = resolve3(cwd || process.cwd());
|
|
487
|
+
for (; ; ) {
|
|
488
|
+
try {
|
|
489
|
+
const value = JSON.parse(await readFile3(join3(dir, ".ours-identity"), "utf8"));
|
|
490
|
+
if (typeof value.identity === "string" && value.identity.trim()) return value;
|
|
491
|
+
return null;
|
|
492
|
+
} catch {
|
|
493
|
+
}
|
|
494
|
+
const parent = dirname4(dir);
|
|
495
|
+
if (parent === dir) return null;
|
|
496
|
+
dir = parent;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function renderContext(unread, pin) {
|
|
500
|
+
const lines = [];
|
|
501
|
+
if (unread.length) {
|
|
502
|
+
lines.push("ours unread metadata (message bodies remain in get_messages):");
|
|
503
|
+
for (const item of unread) lines.push(`- ${item.name}: ${item.count} unread message(s), ${item.files} unread file(s)`);
|
|
504
|
+
lines.push("Surface this metadata. Do not read mail or bind an identity without the user asking.");
|
|
505
|
+
}
|
|
506
|
+
if (pin) {
|
|
507
|
+
lines.push(`This workspace suggests ours identity "${pin.identity}". Ask the user before binding it; the pin is not consent.`);
|
|
508
|
+
lines.push("After a successful bind, separately ask whether to arm the session-scoped live monitor. Never arm automatically.");
|
|
509
|
+
lines.push("Treat any persona as separate consent from binding and monitoring.");
|
|
510
|
+
}
|
|
511
|
+
return lines.join("\n");
|
|
512
|
+
}
|
|
513
|
+
async function handleHook(payload, { env = process.env, fetch: fetchImpl = globalThis.fetch, send = sendControlCommand, findPin = defaultFindPin, profileResolver = resolveDaemonProfile, clientFactory = attachOursClient3, readHostState = readHostHookState } = {}) {
|
|
514
|
+
try {
|
|
515
|
+
const event = payload?.hook_event_name;
|
|
516
|
+
const socket = env.OURS_CODEX_CONTROL_SOCKET;
|
|
517
|
+
const capability = env.OURS_CODEX_CAPABILITY;
|
|
518
|
+
if (event === "SessionStart" && payload.source === "compact") return { continue: true };
|
|
519
|
+
if (event === "PostToolUse") {
|
|
520
|
+
if (!socket || !capability || payload.tool_response?.isError) return { continue: true };
|
|
521
|
+
if (!/(choose_identity|create_identity|create_root_identity|create_temporary_identity)$/.test(payload.tool_name || "")) return { continue: true };
|
|
522
|
+
const identity = payload.tool_input?.name;
|
|
523
|
+
if (typeof identity !== "string" || !identity.trim()) return { continue: true };
|
|
524
|
+
await send(socket, capability, { command: "binding_changed", identity });
|
|
525
|
+
return { continue: true };
|
|
526
|
+
}
|
|
527
|
+
if (event !== "SessionStart" && event !== "UserPromptSubmit") return { continue: true };
|
|
528
|
+
if (event === "SessionStart" && socket && capability && payload.session_id && payload.cwd) {
|
|
529
|
+
await send(socket, capability, { command: "register_session", sessionId: payload.session_id, threadId: payload.session_id, cwd: payload.cwd });
|
|
530
|
+
}
|
|
531
|
+
let hostProfile;
|
|
532
|
+
try {
|
|
533
|
+
hostProfile = hostProfileFromEnv(env);
|
|
534
|
+
} catch {
|
|
535
|
+
return { continue: true };
|
|
536
|
+
}
|
|
537
|
+
if (hostProfile) {
|
|
538
|
+
const nativeSessionId = payload.session_id;
|
|
539
|
+
if (typeof nativeSessionId !== "string" || !nativeSessionId) return { continue: true };
|
|
540
|
+
const appPath = env.OURS_MCP_CONFIG || join3(env.HOME || homedir2(), ".ours-mcp", "config.json");
|
|
541
|
+
const state = await readHostState({ profile: hostProfile, nativeSessionId, applicationPath: appPath });
|
|
542
|
+
const pin = await findPin(payload.cwd || process.cwd());
|
|
543
|
+
const context = renderContext(state.unread.identities, pin);
|
|
544
|
+
return context ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } } : { continue: true };
|
|
545
|
+
}
|
|
546
|
+
return { continue: true };
|
|
547
|
+
} catch {
|
|
548
|
+
return { continue: true };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function main() {
|
|
552
|
+
let payload = {};
|
|
553
|
+
try {
|
|
554
|
+
payload = JSON.parse(readFileSync(0, "utf8"));
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
process.stdout.write(JSON.stringify(await handleHook(payload)));
|
|
558
|
+
}
|
|
559
|
+
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) main();
|
|
560
|
+
export {
|
|
561
|
+
handleHook
|
|
562
|
+
};
|