@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,287 @@
|
|
|
1
|
+
// src/network-watch.mjs
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
4
|
+
|
|
5
|
+
// ../core/src/host-profile.ts
|
|
6
|
+
import { readGatewayClientProfile, validateGatewayClientProfile } from "@ours.network/sdk/client";
|
|
7
|
+
var validateHostProfile = validateGatewayClientProfile;
|
|
8
|
+
function hostProfileSelectionFromEnv(env = process.env) {
|
|
9
|
+
const { configPath, ...profile } = readGatewayClientProfile(env);
|
|
10
|
+
return { profile, configPath };
|
|
11
|
+
}
|
|
12
|
+
function hostProfileFromEnv(env = process.env) {
|
|
13
|
+
return hostProfileSelectionFromEnv(env).profile;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ../core/src/native-session.ts
|
|
17
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
18
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
19
|
+
import { dirname, join as join2, resolve } from "node:path";
|
|
20
|
+
import { attachOursClient } from "@ours.network/sdk";
|
|
21
|
+
|
|
22
|
+
// ../core/src/session-guard.ts
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { openSync, closeSync, lstatSync, mkdirSync, chmodSync, constants } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { createRequire } from "node:module";
|
|
27
|
+
var BUSY_TIMEOUT_MS = 5e3;
|
|
28
|
+
var DatabaseSync = null;
|
|
29
|
+
var resolveAttempted = false;
|
|
30
|
+
function resolveDatabase() {
|
|
31
|
+
if (DatabaseSync) return DatabaseSync;
|
|
32
|
+
if (resolveAttempted) throw new Error("No SQLite implementation available for native session guard");
|
|
33
|
+
resolveAttempted = true;
|
|
34
|
+
try {
|
|
35
|
+
const mod = createRequire(import.meta.url)("node:sqlite");
|
|
36
|
+
DatabaseSync = mod.DatabaseSync;
|
|
37
|
+
return DatabaseSync;
|
|
38
|
+
} catch {
|
|
39
|
+
try {
|
|
40
|
+
const mod = createRequire(import.meta.url)("better-sqlite3");
|
|
41
|
+
DatabaseSync = mod.default ?? mod;
|
|
42
|
+
return DatabaseSync;
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error("Native session guard requires node:sqlite (Node >=22.13) or better-sqlite3");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function ensureGuardFile(guardPath) {
|
|
49
|
+
try {
|
|
50
|
+
const fd = openSync(guardPath, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
51
|
+
closeSync(fd);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code !== "EEXIST") throw error;
|
|
54
|
+
}
|
|
55
|
+
const stat = lstatSync(guardPath);
|
|
56
|
+
if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 63) !== 0 || stat.nlink !== 1) {
|
|
57
|
+
throw new Error("Native session guard file must be an owner-only regular file");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function acquireSessionGuard(recordPath) {
|
|
61
|
+
const uid = process.getuid();
|
|
62
|
+
const base = join(`/tmp/ours-${uid}`, "session-guards");
|
|
63
|
+
mkdirSync(base, { recursive: true, mode: 448 });
|
|
64
|
+
try {
|
|
65
|
+
chmodSync(base, 448);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
const hash = createHash("sha256").update(recordPath).digest("hex");
|
|
69
|
+
const guardPath = join(base, `${hash}.guard`);
|
|
70
|
+
ensureGuardFile(guardPath);
|
|
71
|
+
const before = lstatSync(guardPath);
|
|
72
|
+
const Database = resolveDatabase();
|
|
73
|
+
const db = new Database(guardPath);
|
|
74
|
+
try {
|
|
75
|
+
const after = lstatSync(guardPath);
|
|
76
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode || before.uid !== after.uid) {
|
|
77
|
+
throw new Error("Native session guard file changed during acquisition");
|
|
78
|
+
}
|
|
79
|
+
db.exec(`PRAGMA busy_timeout=${BUSY_TIMEOUT_MS}; BEGIN EXCLUSIVE`);
|
|
80
|
+
return () => {
|
|
81
|
+
try {
|
|
82
|
+
db.close();
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
try {
|
|
88
|
+
db.close();
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ../core/src/native-session.ts
|
|
96
|
+
var RECORD_VERSION = 1;
|
|
97
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
98
|
+
var clients = /* @__PURE__ */ new Map();
|
|
99
|
+
var operations = /* @__PURE__ */ new Map();
|
|
100
|
+
function selector(value) {
|
|
101
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value.trim() !== value || value.includes("\0")) {
|
|
102
|
+
throw new Error("Native session metadata is missing or invalid for this host-profile tool call.");
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function nativeSessionRecordPath(hostRecordRoot, instanceId, nativeSessionId) {
|
|
107
|
+
if (!hostRecordRoot || !hostRecordRoot.startsWith("/") || resolve(hostRecordRoot) !== hostRecordRoot) {
|
|
108
|
+
throw new Error("Native session host record root must be a normalized absolute path.");
|
|
109
|
+
}
|
|
110
|
+
if (!UUID.test(instanceId)) throw new Error("Native session daemon instance must be a lowercase UUID.");
|
|
111
|
+
const hash = createHash2("sha256").update(selector(nativeSessionId), "utf8").digest("hex");
|
|
112
|
+
return join2(hostRecordRoot, "sessions", instanceId, `${hash}.json`);
|
|
113
|
+
}
|
|
114
|
+
function parseRecord(text, path) {
|
|
115
|
+
let value;
|
|
116
|
+
try {
|
|
117
|
+
value = JSON.parse(text);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new Error(`Invalid native session record at ${path}: ${String(error)}`);
|
|
120
|
+
}
|
|
121
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
122
|
+
throw new Error(`Invalid native session record at ${path}: expected an object.`);
|
|
123
|
+
}
|
|
124
|
+
const record = value;
|
|
125
|
+
const keys = Object.keys(record).sort();
|
|
126
|
+
if (keys.join(",") !== "ownerInstanceId,state,version" || record.version !== RECORD_VERSION || typeof record.ownerInstanceId !== "string" || !UUID.test(record.ownerInstanceId) || !["active", "pending", "ended"].includes(String(record.state))) {
|
|
127
|
+
throw new Error(`Invalid native session record at ${path}.`);
|
|
128
|
+
}
|
|
129
|
+
return record;
|
|
130
|
+
}
|
|
131
|
+
async function readRecord(path) {
|
|
132
|
+
try {
|
|
133
|
+
return parseRecord(await readFile(path, "utf8"), path);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error.code === "ENOENT") return null;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function writeRecord(path, record) {
|
|
140
|
+
const parent = dirname(path);
|
|
141
|
+
await mkdir(parent, { recursive: true, mode: 448 });
|
|
142
|
+
await chmod(parent, 448);
|
|
143
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
144
|
+
try {
|
|
145
|
+
await writeFile(temporary, `${JSON.stringify(record, null, 2)}
|
|
146
|
+
`, { mode: 384, flag: "wx" });
|
|
147
|
+
await rename(temporary, path);
|
|
148
|
+
await chmod(path, 384);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
try {
|
|
151
|
+
await unlink(temporary);
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function serialized(key, operation) {
|
|
158
|
+
const predecessor = operations.get(key) ?? Promise.resolve();
|
|
159
|
+
let release;
|
|
160
|
+
const gate = new Promise((resolve2) => {
|
|
161
|
+
release = resolve2;
|
|
162
|
+
});
|
|
163
|
+
const tail = predecessor.catch(() => {
|
|
164
|
+
}).then(() => gate);
|
|
165
|
+
operations.set(key, tail);
|
|
166
|
+
await predecessor.catch(() => {
|
|
167
|
+
});
|
|
168
|
+
let releaseGuard;
|
|
169
|
+
try {
|
|
170
|
+
releaseGuard = acquireSessionGuard(key);
|
|
171
|
+
return await operation();
|
|
172
|
+
} finally {
|
|
173
|
+
try {
|
|
174
|
+
releaseGuard?.();
|
|
175
|
+
} finally {
|
|
176
|
+
release();
|
|
177
|
+
if (operations.get(key) === tail) operations.delete(key);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function attach(profile, ownerInstanceId) {
|
|
182
|
+
return attachOursClient({
|
|
183
|
+
endpoint: profile.endpoint,
|
|
184
|
+
expectedInstanceId: profile.expectedInstanceId,
|
|
185
|
+
credentialPath: profile.credentialPath,
|
|
186
|
+
sessionMode: "external",
|
|
187
|
+
leaseToken: ownerInstanceId,
|
|
188
|
+
env: {}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function cachedClient(path, profile, ownerInstanceId) {
|
|
192
|
+
const cached = clients.get(path);
|
|
193
|
+
if (cached?.ownerInstanceId === ownerInstanceId) return cached.promise;
|
|
194
|
+
if (cached) {
|
|
195
|
+
clients.delete(path);
|
|
196
|
+
void cached.promise.then((client) => client.close()).catch(() => {
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const promise = attach(profile, ownerInstanceId);
|
|
200
|
+
const next = { ownerInstanceId, promise };
|
|
201
|
+
clients.set(path, next);
|
|
202
|
+
void promise.catch(() => {
|
|
203
|
+
if (clients.get(path) === next) clients.delete(path);
|
|
204
|
+
});
|
|
205
|
+
return promise;
|
|
206
|
+
}
|
|
207
|
+
async function releaseOwner(path, profile, ownerInstanceId) {
|
|
208
|
+
const cached = clients.get(path);
|
|
209
|
+
const client = cached?.ownerInstanceId === ownerInstanceId ? await cached.promise : await attach(profile, ownerInstanceId);
|
|
210
|
+
try {
|
|
211
|
+
await client.releaseLease();
|
|
212
|
+
} finally {
|
|
213
|
+
if (cached?.ownerInstanceId !== ownerInstanceId) await client.close();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function finishPending(path, profile, ownerInstanceId) {
|
|
217
|
+
await releaseOwner(path, profile, ownerInstanceId);
|
|
218
|
+
const current = await readRecord(path);
|
|
219
|
+
if (current?.state === "pending" && current.ownerInstanceId === ownerInstanceId) {
|
|
220
|
+
await writeRecord(path, { version: RECORD_VERSION, ownerInstanceId, state: "ended" });
|
|
221
|
+
}
|
|
222
|
+
const cached = clients.get(path);
|
|
223
|
+
if (cached?.ownerInstanceId === ownerInstanceId) {
|
|
224
|
+
clients.delete(path);
|
|
225
|
+
void cached.promise.then((client) => client.close()).catch(() => {
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function nativeClientForAtRoot(profileValue, nativeSessionId, hostRecordRoot) {
|
|
230
|
+
const profile = validateHostProfile(profileValue);
|
|
231
|
+
const path = nativeSessionRecordPath(hostRecordRoot, profile.expectedInstanceId, nativeSessionId);
|
|
232
|
+
return serialized(path, async () => {
|
|
233
|
+
let record = await readRecord(path);
|
|
234
|
+
if (record?.state === "pending") {
|
|
235
|
+
await finishPending(path, profile, record.ownerInstanceId);
|
|
236
|
+
record = await readRecord(path);
|
|
237
|
+
}
|
|
238
|
+
if (record?.state === "active") return cachedClient(path, profile, record.ownerInstanceId);
|
|
239
|
+
const ownerInstanceId = randomUUID();
|
|
240
|
+
await writeRecord(path, { version: RECORD_VERSION, ownerInstanceId, state: "active" });
|
|
241
|
+
return cachedClient(path, profile, ownerInstanceId);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/network-watch.mjs
|
|
246
|
+
async function runNetworkWatch({
|
|
247
|
+
identity,
|
|
248
|
+
nativeSessionId,
|
|
249
|
+
env = process.env,
|
|
250
|
+
profile: profileValue,
|
|
251
|
+
hostRecordRoot: hostRecordRootValue,
|
|
252
|
+
clientFor = nativeClientForAtRoot,
|
|
253
|
+
write = (value) => process.stdout.write(value),
|
|
254
|
+
installSignalHandlers = true
|
|
255
|
+
} = {}) {
|
|
256
|
+
if (typeof identity !== "string" || !identity.trim()) throw new Error("identity is required");
|
|
257
|
+
if (typeof nativeSessionId !== "string" || !nativeSessionId.trim()) throw new Error("native session id is required");
|
|
258
|
+
const profile = profileValue ?? hostProfileFromEnv(env);
|
|
259
|
+
if (!profile) throw new Error("A selected host network profile is required.");
|
|
260
|
+
const appPath = env.OURS_MCP_CONFIG || join3(env.HOME || homedir(), ".ours-mcp", "config.json");
|
|
261
|
+
const hostRecordRoot = hostRecordRootValue ?? dirname2(appPath);
|
|
262
|
+
const controller = new AbortController();
|
|
263
|
+
const stop = () => controller.abort();
|
|
264
|
+
if (installSignalHandlers) {
|
|
265
|
+
process.once("SIGINT", stop);
|
|
266
|
+
process.once("SIGTERM", stop);
|
|
267
|
+
}
|
|
268
|
+
let client;
|
|
269
|
+
try {
|
|
270
|
+
client = await clientFor(profile, nativeSessionId, hostRecordRoot);
|
|
271
|
+
for await (const event of client.watchNotifications(identity, { kinds: ["inbound"], signal: controller.signal })) {
|
|
272
|
+
const summary = event?.event === "file_received" ? `[${identity}] new file ${event.filename ?? "?"} from ${event.from ?? "?"}` : `[${identity}] new message from ${event?.from ?? "?"}`;
|
|
273
|
+
write(`${summary}
|
|
274
|
+
`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
if (installSignalHandlers) {
|
|
279
|
+
process.off("SIGINT", stop);
|
|
280
|
+
process.off("SIGTERM", stop);
|
|
281
|
+
}
|
|
282
|
+
await client?.close();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export {
|
|
286
|
+
runNetworkWatch
|
|
287
|
+
};
|
package/dist/profile.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/profile.mjs
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { attachOursClient as attachOursClient2 } from "@ours.network/sdk";
|
|
4
|
+
|
|
5
|
+
// ../core/src/host-profile.ts
|
|
6
|
+
import { readGatewayClientProfile, validateGatewayClientProfile } from "@ours.network/sdk/client";
|
|
7
|
+
function hostProfileSelectionFromEnv(env = process.env) {
|
|
8
|
+
const { configPath, ...profile } = readGatewayClientProfile(env);
|
|
9
|
+
return { profile, configPath };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ../core/src/native-session.ts
|
|
13
|
+
import { attachOursClient } from "@ours.network/sdk";
|
|
14
|
+
|
|
15
|
+
// src/profile.mjs
|
|
16
|
+
function validPort(value) {
|
|
17
|
+
const port = Number(value);
|
|
18
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`ours: ${value} is not a valid TCP port`);
|
|
19
|
+
return port;
|
|
20
|
+
}
|
|
21
|
+
function parseOursArgs(argv = []) {
|
|
22
|
+
const codexArgs = [];
|
|
23
|
+
let port;
|
|
24
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
25
|
+
const arg = argv[index];
|
|
26
|
+
if (arg === "--ours-port") {
|
|
27
|
+
if (argv[index + 1] == null) throw new Error("ours: --ours-port requires a value");
|
|
28
|
+
port = validPort(argv[++index]);
|
|
29
|
+
} else if (arg.startsWith("--ours-port=")) {
|
|
30
|
+
port = validPort(arg.slice("--ours-port=".length));
|
|
31
|
+
} else {
|
|
32
|
+
codexArgs.push(arg);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { port, codexArgs };
|
|
36
|
+
}
|
|
37
|
+
async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch, attach = attachOursClient2 } = {}) {
|
|
38
|
+
const parsed = parseOursArgs(argv);
|
|
39
|
+
const hostSelection = hostProfileSelectionFromEnv(env);
|
|
40
|
+
const { profile: hostProfile, configPath } = hostSelection;
|
|
41
|
+
if (parsed.port != null) throw new Error("ours: --ours-port conflicts with host-profile mode");
|
|
42
|
+
const client = await attach({ ...hostProfile, sessionMode: "external", leaseToken: randomUUID(), env: {} });
|
|
43
|
+
let info;
|
|
44
|
+
try {
|
|
45
|
+
info = await client.version();
|
|
46
|
+
await client.identities();
|
|
47
|
+
await client.unread();
|
|
48
|
+
} finally {
|
|
49
|
+
await client.close();
|
|
50
|
+
}
|
|
51
|
+
return { profile: hostProfile, info, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
parseOursArgs,
|
|
55
|
+
resolveDaemonProfile
|
|
56
|
+
};
|
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/install.sh
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Install the native ours.network plugin into the OpenAI Codex CLI:
|
|
3
|
-
# 1.
|
|
3
|
+
# 1. verify the installed gateway client profile before modifying plugin configuration
|
|
4
4
|
# 2. add/upgrade adapt-toolkit/ours-codex-marketplace
|
|
5
5
|
# 3. install the native `ours` plugin (skills, MCP servers, and hooks)
|
|
6
6
|
# 4. back up and remove installer-owned legacy config only after verification
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
# SKILLS_DIR skills root (default ~/.agents/skills)
|
|
11
11
|
# CODEX_CONFIG config.toml path (default $CODEX_DIR/config.toml)
|
|
12
12
|
# CODEX_AGENTS AGENTS.md path (default $CODEX_DIR/AGENTS.md)
|
|
13
|
-
# OURS_INSTALL_SKIP_DAEMON=1 skip daemon install/start
|
|
14
13
|
set -euo pipefail
|
|
15
14
|
|
|
16
15
|
SELFDIR="$(cd "$(dirname "$0")" && pwd)"
|
|
@@ -21,33 +20,21 @@ SKILLS_DIR="${SKILLS_DIR:-$HOME/.agents/skills}"
|
|
|
21
20
|
|
|
22
21
|
say(){ printf 'ours-install: %s\n' "$1"; }
|
|
23
22
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if ! ours daemon status >/dev/null 2>&1; then
|
|
35
|
-
say "starting the ours daemon…"
|
|
36
|
-
if ! ours daemon start; then say "could not start the ours daemon; installation stopped."; return 1; fi
|
|
37
|
-
elif [ -n "$before" ] && [ "$before" != "$after" ]; then
|
|
38
|
-
say "operator CLI upgraded (v${before} → v${after}) — restarting its daemon…"
|
|
39
|
-
if ! ours daemon restart; then
|
|
40
|
-
say "restart failed; trying a clean start…"
|
|
41
|
-
if ! ours daemon start; then say "could not restart or start the upgraded daemon; installation stopped."; return 1; fi
|
|
42
|
-
fi
|
|
43
|
-
else
|
|
44
|
-
say "daemon already current (v${after:-unknown})."
|
|
23
|
+
# Client installation never installs, starts or restarts server services.
|
|
24
|
+
require_gateway_client(){
|
|
25
|
+
if ! command -v ours >/dev/null 2>&1 || ! command -v ours-mcp >/dev/null 2>&1; then
|
|
26
|
+
say "Install gateway clients with ours-install client first; ours and ours-mcp are required."
|
|
27
|
+
return 1
|
|
28
|
+
fi
|
|
29
|
+
local contract
|
|
30
|
+
if ! contract="$(ours-mcp verify-client-profile)" || [ "$contract" != "ours.gateway-client-profile-v1" ]; then
|
|
31
|
+
say "Configure the shared gateway profile with ours-install client first; plugin setup was left unchanged."
|
|
32
|
+
return 1
|
|
45
33
|
fi
|
|
46
|
-
say "operator CLI: $(command -v ours) (v${after:-unknown}); MCP adapter: $(command -v ours-mcp)"
|
|
47
34
|
}
|
|
48
35
|
|
|
49
|
-
# --- 1)
|
|
50
|
-
|
|
36
|
+
# --- 1) shared gateway client preflight ---
|
|
37
|
+
require_gateway_client
|
|
51
38
|
|
|
52
39
|
# --- native Codex plugin ------------------------------------------------------
|
|
53
40
|
# Codex owns the plugin cache and hook-trust workflow. Only remove the legacy
|
|
@@ -73,7 +60,7 @@ if [ "${OURS_CODEX_SKIP_NATIVE:-}" != "1" ]; then
|
|
|
73
60
|
CODEX_DIR="$CODEX_DIR" SKILLS_DIR="$SKILLS_DIR" node "$SELFDIR/bin/codex-legacy-cleanup.mjs"
|
|
74
61
|
say "native plugin installed. Review and trust its hooks in Codex, then start a new thread."
|
|
75
62
|
say "standard mode: codex"
|
|
76
|
-
say "live mode: ours-codex
|
|
63
|
+
say "live mode: ours-codex"
|
|
77
64
|
exit 0
|
|
78
65
|
fi
|
|
79
66
|
|
|
@@ -93,11 +80,5 @@ CODEX_CONFIG="$CODEX_CONFIG" node "$SELFDIR/bin/codex-config-install.mjs"
|
|
|
93
80
|
CODEX_AGENTS="$CODEX_AGENTS" node "$SELFDIR/bin/codex-agents-install.mjs"
|
|
94
81
|
|
|
95
82
|
say "done. The ours MCP server + skill are live for the next Codex session."
|
|
96
|
-
# --- version echo: show the user they are on latest ---
|
|
97
|
-
if [ "${OURS_INSTALL_SKIP_DAEMON:-}" != "1" ]; then
|
|
98
|
-
say "versions:"
|
|
99
|
-
say " MCP adapter: $(ours-mcp --version 2>/dev/null | head -1 || echo 'unknown')"
|
|
100
|
-
say " plugin: $(npm ls -g @ours.network/codex 2>/dev/null | grep -oE '@ours\.network/codex@[0-9][0-9.]*' | head -1 || echo '@ours.network/codex (not a global install)')"
|
|
101
|
-
fi
|
|
102
83
|
say "next: bind (or create) an identity, then the ours skill tails ours-mcp watch (or polls"
|
|
103
84
|
say " get_messages) in-session so you react to new mail while you work."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/codex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-nightly.10",
|
|
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,10 +46,10 @@
|
|
|
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.15",
|
|
51
50
|
"ws": "^8.21.0",
|
|
52
|
-
"zod": "^3.25.76"
|
|
51
|
+
"zod": "^3.25.76",
|
|
52
|
+
"@ours.network/mcp": "1.2.0-nightly.10"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"esbuild": "^0.24.2"
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"access": "public"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
|
-
"build": "
|
|
61
|
+
"build": "node build.mjs",
|
|
62
62
|
"prepack": "npm run build",
|
|
63
63
|
"install-plugin": "bash install.sh",
|
|
64
64
|
"test": "node --test"
|
package/skills/ours/SKILL.md
CHANGED
|
@@ -108,12 +108,12 @@ existing file.
|
|
|
108
108
|
**control-plane monitoring proxy is not available in this release** — there is no tool
|
|
109
109
|
to call. See "Control plane" below.
|
|
110
110
|
|
|
111
|
-
- **Configuration.**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
`references/configuration.md`.
|
|
111
|
+
- **Configuration.** Every client selects the HTTP gateway through
|
|
112
|
+
`~/.ours-client/profile.json` (or the whole-profile `OURS_CONFIG` override).
|
|
113
|
+
`serverUrl` is the base URL; daemon and Cowork use `/daemon` and `/cowork`
|
|
114
|
+
beneath it. Import the issued credential with `ours-install client`.
|
|
115
|
+
Local daemon ports, state directories, and Unix sockets are not client routes.
|
|
116
|
+
Missing or invalid configuration fails closed. See `references/configuration.md`.
|
|
117
117
|
|
|
118
118
|
## ours-fleet — persistent harness roles
|
|
119
119
|
|
|
@@ -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.
|
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Shared gateway client configuration
|
|
2
2
|
|
|
3
|
-
ours
|
|
4
|
-
|
|
3
|
+
Fleet, ours CLI, MCP and harness hooks select one server through
|
|
4
|
+
`~/.ours-client/profile.json`. `serverUrl` names the HTTP(S) gateway, with
|
|
5
|
+
optional base path. Daemon operations use `/daemon`; Cowork uses `/cowork`.
|
|
6
|
+
The profile also pins `expectedInstanceId` and an absolute `credentialPath`.
|
|
7
|
+
An optional `endpoint` must equal `serverUrl + "/daemon"`.
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
ours config show --json
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
9
|
+
Import a prepared private profile with `ours-install client --config /absolute/profile.json`.
|
|
10
|
+
Use `ours config show --json` to inspect connection metadata without reading the token.
|
|
11
|
+
Keep the profile directory mode 0700 and files mode 0600. `OURS_CONFIG` may
|
|
12
|
+
select the same whole profile for all clients. Remove legacy `OURS_PORT`,
|
|
13
|
+
`OURS_STATE_DIR`, `OURS_API_TOKEN` and `OURS_DAEMON_ID` overrides.
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
is verified before credentials are sent.
|
|
15
|
+
Missing profiles, rejected credentials and unavailable gateways fail closed.
|
|
16
|
+
Clients never read daemon state, start a daemon, or try a local port/socket.
|
|
17
|
+
Hooks use the same gateway and return a harmless no-op on failure. Their
|
|
18
|
+
application-local identity/session files do not select another server.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Changing daemon configuration or restarting the shared daemon affects every
|
|
25
|
-
connected application. Explain that blast radius and obtain the user's consent
|
|
26
|
-
before making operator-level changes.
|
|
20
|
+
For full-stack setup, a custom public port (such as 4050), systemd propagation
|
|
21
|
+
and migration, see the [gateway setup guide](https://github.com/adapt-toolkit/ours-network/blob/prerelease/packages/installer/GATEWAY_SETUP.md).
|
|
22
|
+
The coordinated gateway changes require approved matching package releases;
|
|
23
|
+
current npm nightly is not automatically evidence that these changes are installed.
|
|
24
|
+
Server administration uses `ours-install server` on the server host.
|
package/src/hooks/runner.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
5
6
|
import { pathToFileURL } from 'node:url';
|
|
6
7
|
import { sendControlCommand } from '../control-server.mjs';
|
|
7
8
|
import { resolveDaemonProfile } from '../profile.mjs';
|
|
9
|
+
import { readHostHookState } from '../host-hooks.mjs';
|
|
10
|
+
import { attachOursClient } from '@ours.network/sdk';
|
|
11
|
+
import { hostProfileFromEnv } from '../../../core/src/host-client/index.ts';
|
|
8
12
|
|
|
9
13
|
async function defaultFindPin(cwd) {
|
|
10
14
|
let dir = resolve(cwd || process.cwd());
|
|
@@ -31,14 +35,6 @@ const safeUnread = (value) => (Array.isArray(value?.identities) ? value.identiti
|
|
|
31
35
|
return [{ name: entry.name, count, files, recent }];
|
|
32
36
|
});
|
|
33
37
|
|
|
34
|
-
async function applicationIdentityNames(env, stateDir) {
|
|
35
|
-
const path = env.OURS_MCP_CONFIG || join(homedir(), '.ours-mcp', 'config.json');
|
|
36
|
-
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
37
|
-
if (value?.version !== 1 || !value.daemons || typeof value.daemons !== 'object') return new Set();
|
|
38
|
-
const identities = value.daemons[resolve(stateDir)]?.identities;
|
|
39
|
-
return new Set(Array.isArray(identities) ? identities.filter((name) => typeof name === 'string') : []);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
38
|
function renderContext(unread, pin) {
|
|
43
39
|
const lines = [];
|
|
44
40
|
if (unread.length) {
|
|
@@ -54,7 +50,7 @@ function renderContext(unread, pin) {
|
|
|
54
50
|
return lines.join('\n');
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
export async function handleHook(payload, { env = process.env, fetch: fetchImpl = globalThis.fetch, send = sendControlCommand, findPin = defaultFindPin } = {}) {
|
|
53
|
+
export async function handleHook(payload, { env = process.env, fetch: fetchImpl = globalThis.fetch, send = sendControlCommand, findPin = defaultFindPin, profileResolver = resolveDaemonProfile, clientFactory = attachOursClient, readHostState = readHostHookState } = {}) {
|
|
58
54
|
try {
|
|
59
55
|
const event = payload?.hook_event_name;
|
|
60
56
|
const socket = env.OURS_CODEX_CONTROL_SOCKET;
|
|
@@ -72,37 +68,26 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
|
|
|
72
68
|
if (event === 'SessionStart' && socket && capability && payload.session_id && payload.cwd) {
|
|
73
69
|
await send(socket, capability, { command: 'register_session', sessionId: payload.session_id, threadId: payload.session_id, cwd: payload.cwd });
|
|
74
70
|
}
|
|
75
|
-
let
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const response = await fetchImpl(`http://127.0.0.1:${port}/unread`, { headers, signal: AbortSignal.timeout(1500) });
|
|
91
|
-
if (response.ok) {
|
|
92
|
-
const visible = await applicationIdentityNames(env, selectedStateDir);
|
|
93
|
-
unread = safeUnread(await response.json()).filter((entry) => visible.has(entry.name));
|
|
94
|
-
}
|
|
95
|
-
} catch { /* daemon diagnostics belong to launcher/proxy */ }
|
|
96
|
-
const pin = await findPin(payload.cwd || process.cwd());
|
|
97
|
-
const context = renderContext(unread, pin);
|
|
98
|
-
if (!context) return { continue: true };
|
|
99
|
-
return { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } };
|
|
71
|
+
let hostProfile;
|
|
72
|
+
try { hostProfile = hostProfileFromEnv(env); }
|
|
73
|
+
catch { return { continue: true }; }
|
|
74
|
+
if (hostProfile) {
|
|
75
|
+
const nativeSessionId = payload.session_id;
|
|
76
|
+
if (typeof nativeSessionId !== 'string' || !nativeSessionId) return { continue: true };
|
|
77
|
+
const appPath = env.OURS_MCP_CONFIG || join(env.HOME || homedir(), '.ours-mcp', 'config.json');
|
|
78
|
+
const state = await readHostState({ profile: hostProfile, nativeSessionId, applicationPath: appPath });
|
|
79
|
+
const pin = await findPin(payload.cwd || process.cwd());
|
|
80
|
+
const context = renderContext(state.unread.identities, pin);
|
|
81
|
+
return context ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } } : { continue: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { continue: true };
|
|
100
85
|
} catch { return { continue: true }; }
|
|
101
86
|
}
|
|
102
87
|
|
|
103
88
|
async function main() {
|
|
104
89
|
let payload = {};
|
|
105
|
-
try { payload = JSON.parse(
|
|
90
|
+
try { payload = JSON.parse(readFileSync(0, 'utf8')); } catch { /* noop */ }
|
|
106
91
|
process.stdout.write(JSON.stringify(await handleHook(payload)));
|
|
107
92
|
}
|
|
108
93
|
|