@ours.network/codex 1.2.0-nightly.6 → 1.2.0-nightly.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -28
- package/bin/codex-config-install.mjs +3 -3
- package/bin/ours-codex-install.mjs +4 -31
- package/dist/hooks-runner.mjs +49 -238
- package/dist/host-hooks.mjs +9 -48
- package/dist/monitor-mcp.mjs +20 -122
- package/dist/network-watch.mjs +17 -118
- package/dist/profile.mjs +14 -153
- package/install.sh +14 -33
- package/package.json +3 -3
- package/skills/ours/SKILL.md +6 -6
- package/skills/ours/references/configuration.md +20 -22
- package/src/hooks/runner.mjs +1 -48
- package/src/launcher.mjs +1 -9
- package/src/profile.mjs +1 -40
package/README.md
CHANGED
|
@@ -51,38 +51,21 @@ The launcher never starts, stops, restarts, or reconfigures the ours daemon. If
|
|
|
51
51
|
selected daemon is absent or incompatible, it exits with an error and leaves standard
|
|
52
52
|
`codex` available.
|
|
53
53
|
|
|
54
|
-
## Selecting a
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
local-daemon precedence remains:
|
|
62
|
-
|
|
63
|
-
1. `ours-codex --ours-port <port>`
|
|
64
|
-
2. `OURS_PORT`
|
|
65
|
-
3. the config selected by `OURS_CONFIG`
|
|
66
|
-
4. `~/.ours/config.json`
|
|
67
|
-
5. port `3050`
|
|
68
|
-
|
|
69
|
-
All MCP, hooks, unread, and watcher calls inherit the same selected profile. Host-profile
|
|
70
|
-
tools run in the local `@ours.network/mcp` stdio server included as a plugin dependency.
|
|
71
|
-
The MCP server calls the selected daemon's authenticated HTTP API through the SDK.
|
|
72
|
-
Example:
|
|
54
|
+
## Selecting a gateway
|
|
55
|
+
|
|
56
|
+
Every client uses `~/.ours-client/profile.json`; `OURS_CONFIG` can select a complete
|
|
57
|
+
alternate profile. It contains `serverUrl`, `expectedInstanceId`, and `credentialPath`.
|
|
58
|
+
The daemon endpoint is derived as `serverUrl + /daemon`, including gateway path prefixes.
|
|
59
|
+
Missing, unsafe, or invalid profiles fail closed. Remove legacy port/state/token
|
|
60
|
+
selectors; `--ours-port` is rejected. Example after provisioning with `ours-install client`:
|
|
73
61
|
|
|
74
62
|
```sh
|
|
75
|
-
OURS_CONFIG="$HOME/.ours/testing.json" ours-codex
|
|
63
|
+
OURS_CONFIG="$HOME/.ours-client/testing.json" ours-codex
|
|
76
64
|
```
|
|
77
65
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`OURS_STATE_DIR`. Codex forwards the actual selected path as `OURS_CONFIG` and the optional `OURS_MCP_CONFIG` host
|
|
82
|
-
record location; the SDK verifies the expected
|
|
83
|
-
daemon instance and rereads `credentialPath` for every request, so an operator token
|
|
84
|
-
update takes effect in the running hook and live monitor without a restart or token
|
|
85
|
-
snapshot in child process environments.
|
|
66
|
+
MCP, hooks, unread and watcher requests inherit that profile. The SDK verifies the
|
|
67
|
+
instance and rereads the issued credential for each request. Client installation and
|
|
68
|
+
launch never install, start, restart or reconfigure server services.
|
|
86
69
|
|
|
87
70
|
Native ownership is keyed by Codex's actual thread ID. A normal live TUI exit forwards
|
|
88
71
|
that ID through the same `SessionEnd` input used by the hook. If Codex has not produced
|
|
@@ -33,7 +33,7 @@ export function planTomlInstall(text) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Render the managed TOML block: the `ours` MCP server pointing at the globally-installed
|
|
36
|
-
//
|
|
36
|
+
// gateway client (`ours-mcp proxy`). Equivalent to `codex mcp add ours -- ours-mcp proxy`.
|
|
37
37
|
export function renderTomlBlock() {
|
|
38
38
|
return `${SENTINEL}
|
|
39
39
|
# Added by @ours.network/codex install.sh. Remove this whole block to uninstall.
|
|
@@ -41,9 +41,9 @@ export function renderTomlBlock() {
|
|
|
41
41
|
[mcp_servers.ours]
|
|
42
42
|
command = "ours-mcp"
|
|
43
43
|
args = ["proxy"]
|
|
44
|
-
# Optional
|
|
44
|
+
# Optional whole shared gateway profile override:
|
|
45
45
|
# [mcp_servers.ours.env]
|
|
46
|
-
#
|
|
46
|
+
# OURS_CONFIG = "/absolute/private/path/profile.json"
|
|
47
47
|
${SENTINEL_END}
|
|
48
48
|
`;
|
|
49
49
|
}
|
|
@@ -5,14 +5,7 @@
|
|
|
5
5
|
// npm i -g @ours.network/codex
|
|
6
6
|
// ours-codex-install
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
// WHOLE stack (ours core + harness plugins + ours-fleet + Telegram). `ours-codex-install` is kept
|
|
10
|
-
// as a THIN ALIAS — by default it hands off to `ours-install` when that's available, so an old
|
|
11
|
-
// muscle-memory command still lands the user in the current, unified flow. Pass `--codex-only`
|
|
12
|
-
// (or set OURS_CODEX_LEGACY=1) to force the legacy Codex-only install below.
|
|
13
|
-
//
|
|
14
|
-
// Usage:
|
|
15
|
-
// ours-codex-install [--codex-only] [--codex-dir DIR] [--skills-dir DIR] [--skip-daemon]
|
|
8
|
+
// Installs client plugin wiring only after shared gateway profile validation.
|
|
16
9
|
import { spawnSync } from 'node:child_process';
|
|
17
10
|
import { fileURLToPath } from 'node:url';
|
|
18
11
|
import { dirname, join } from 'node:path';
|
|
@@ -33,38 +26,19 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
33
26
|
else { console.error(`ours-codex-install: unknown argument "${a}"`); help(); process.exit(2); }
|
|
34
27
|
}
|
|
35
28
|
|
|
36
|
-
// Thin-alias hand-off: unless the caller forced the legacy Codex-only path, run the unified
|
|
37
|
-
// `ours-install` if we can find it — on PATH (published), or the monorepo sibling (dev/test).
|
|
38
|
-
if (!opts.codexOnly && !process.env.OURS_CODEX_LEGACY) {
|
|
39
|
-
const onPath = spawnSync('bash', ['-c', 'command -v ours-install'], { encoding: 'utf8' });
|
|
40
|
-
const unified = (onPath.status === 0 && (onPath.stdout || '').trim())
|
|
41
|
-
? { cmd: (onPath.stdout || '').trim(), args: argv }
|
|
42
|
-
: (() => {
|
|
43
|
-
const sib = join(PKG, '..', 'installer', 'install.mjs');
|
|
44
|
-
return existsSync(sib) ? { cmd: process.execPath, args: [sib, ...argv] } : null;
|
|
45
|
-
})();
|
|
46
|
-
if (unified) {
|
|
47
|
-
const r = spawnSync(unified.cmd, unified.args, { stdio: 'inherit', env: process.env });
|
|
48
|
-
if (r.error) { console.error(`ours-codex-install: ${r.error.message}`); process.exit(1); }
|
|
49
|
-
process.exit(r.status ?? 0);
|
|
50
|
-
}
|
|
51
|
-
// No unified installer found → fall through to the legacy Codex-only install (still never a dead end).
|
|
52
|
-
}
|
|
53
|
-
|
|
54
29
|
function help() {
|
|
55
30
|
console.log(`ours-codex-install — set up the ours.network plugin for the OpenAI Codex CLI.
|
|
56
31
|
|
|
57
32
|
ours-codex-install [options]
|
|
58
33
|
|
|
59
|
-
|
|
60
|
-
stack). Use --codex-only to run the legacy Codex-only install instead. Standard mode uses
|
|
34
|
+
Requires clients and a shared gateway profile provisioned by \`ours-install client\`. Standard mode uses
|
|
61
35
|
\`codex\`; live mode uses \`ours-codex\`. Live monitoring still requires explicit consent.
|
|
62
36
|
|
|
63
37
|
Options:
|
|
64
|
-
--codex-only
|
|
38
|
+
--codex-only compatibility flag; plugin setup is always client-only
|
|
65
39
|
--codex-dir <dir> Codex config+AGENTS.md root (default ~/.codex) [--codex-only]
|
|
66
40
|
--skills-dir <dir> skills root (default ~/.agents/skills — USER scope) [--codex-only]
|
|
67
|
-
--skip-daemon
|
|
41
|
+
--skip-daemon compatibility flag; server lifecycle is never changed [--codex-only]
|
|
68
42
|
-h, --help show this help
|
|
69
43
|
|
|
70
44
|
Idempotent: safe to re-run. Start a new Codex thread after installation and review the
|
|
@@ -81,7 +55,6 @@ if (!existsSync(INSTALL)) {
|
|
|
81
55
|
const env = { ...process.env };
|
|
82
56
|
if (opts.codexDir) env.CODEX_DIR = opts.codexDir;
|
|
83
57
|
if (opts.skillsDir) env.SKILLS_DIR = opts.skillsDir;
|
|
84
|
-
if (opts.skipDaemon) env.OURS_INSTALL_SKIP_DAEMON = '1';
|
|
85
58
|
|
|
86
59
|
const res = spawnSync('bash', [INSTALL], { stdio: 'inherit', env });
|
|
87
60
|
if (res.error) { console.error(`ours-codex-install: ${res.error.message}`); process.exit(1); }
|
package/dist/hooks-runner.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/hooks/runner.mjs
|
|
4
|
-
import { readFileSync
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
5
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
6
|
-
import { homedir as
|
|
7
|
-
import { dirname as dirname4, join as
|
|
6
|
+
import { homedir as homedir2 } from "node:os";
|
|
7
|
+
import { dirname as dirname4, join as join3, resolve as resolve3 } from "node:path";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/control-server.mjs
|
|
@@ -19,7 +19,7 @@ function encodeControlResponse(value) {
|
|
|
19
19
|
|
|
20
20
|
// src/control-server.mjs
|
|
21
21
|
function sendControlCommand(socketPath, capability, message, timeoutMs = 2e3) {
|
|
22
|
-
return new Promise((
|
|
22
|
+
return new Promise((resolve4, reject) => {
|
|
23
23
|
const socket = createConnection(socketPath);
|
|
24
24
|
let buffer = "";
|
|
25
25
|
const timer = setTimeout(() => {
|
|
@@ -40,7 +40,7 @@ function sendControlCommand(socketPath, capability, message, timeoutMs = 2e3) {
|
|
|
40
40
|
try {
|
|
41
41
|
const response = JSON.parse(buffer);
|
|
42
42
|
if (!response.ok) reject(new Error(response.error || "monitor control failed"));
|
|
43
|
-
else
|
|
43
|
+
else resolve4(response);
|
|
44
44
|
} catch (error) {
|
|
45
45
|
reject(error);
|
|
46
46
|
}
|
|
@@ -49,141 +49,38 @@ function sendControlCommand(socketPath, capability, message, timeoutMs = 2e3) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// src/profile.mjs
|
|
52
|
-
import { join as join3 } from "node:path";
|
|
53
|
-
import { homedir as homedir3 } from "node:os";
|
|
54
52
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
55
|
-
import {
|
|
53
|
+
import { attachOursClient as attachOursClient2 } from "@ours.network/sdk";
|
|
56
54
|
|
|
57
55
|
// ../core/src/host-profile.ts
|
|
58
|
-
import {
|
|
59
|
-
|
|
60
|
-
import { resolve } from "node:path";
|
|
61
|
-
var PROFILE_KEYS = ["endpoint", "expectedInstanceId", "credentialPath"];
|
|
62
|
-
var LEGACY_SELECTION_KEYS = ["port", "stateDir", "apiToken", "apiVisibility"];
|
|
63
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
64
|
-
var CONFLICTING_ENV = ["OURS_API_TOKEN", "OURS_PORT", "OURS_STATE_DIR", "OURS_DAEMON_ID"];
|
|
65
|
-
function profileError(reason) {
|
|
66
|
-
return new Error(`Invalid external host profile: ${reason}`);
|
|
67
|
-
}
|
|
68
|
-
function validateHostProfile(value) {
|
|
69
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
70
|
-
throw profileError("expected a complete host profile object.");
|
|
71
|
-
}
|
|
72
|
-
const record = value;
|
|
73
|
-
const present = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
74
|
-
if (present.length !== PROFILE_KEYS.length) {
|
|
75
|
-
throw profileError("expected a complete host profile tuple: endpoint, expectedInstanceId, credentialPath.");
|
|
76
|
-
}
|
|
77
|
-
const mixed = LEGACY_SELECTION_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
78
|
-
if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(", ")}).`);
|
|
79
|
-
const endpoint = record.endpoint;
|
|
80
|
-
const expectedInstanceId = record.expectedInstanceId;
|
|
81
|
-
const credentialPath = record.credentialPath;
|
|
82
|
-
if (typeof endpoint !== "string" || endpoint.trim() !== endpoint || endpoint === "") {
|
|
83
|
-
throw profileError("endpoint must be a non-empty HTTP or HTTPS origin.");
|
|
84
|
-
}
|
|
85
|
-
if (typeof expectedInstanceId !== "string" || !UUID.test(expectedInstanceId)) {
|
|
86
|
-
throw profileError("expectedInstanceId must be a lowercase UUID.");
|
|
87
|
-
}
|
|
88
|
-
if (typeof credentialPath !== "string" || credentialPath === "" || !credentialPath.startsWith("/") || resolve(credentialPath) !== credentialPath) {
|
|
89
|
-
throw profileError("credentialPath must be a normalized absolute path.");
|
|
90
|
-
}
|
|
91
|
-
let url;
|
|
92
|
-
try {
|
|
93
|
-
url = new URL(endpoint);
|
|
94
|
-
} catch {
|
|
95
|
-
throw profileError("endpoint must be an HTTP or HTTPS origin.");
|
|
96
|
-
}
|
|
97
|
-
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
98
|
-
throw profileError("endpoint must be an HTTP or HTTPS origin without credentials, path, query, or fragment.");
|
|
99
|
-
}
|
|
100
|
-
return { endpoint: url.origin, expectedInstanceId, credentialPath };
|
|
101
|
-
}
|
|
102
|
-
function readProfileObject(configPath) {
|
|
103
|
-
let text;
|
|
104
|
-
try {
|
|
105
|
-
text = readFileSync(configPath, "utf8");
|
|
106
|
-
} catch {
|
|
107
|
-
throw new Error(`Cannot read host profile ${JSON.stringify(configPath)}.`);
|
|
108
|
-
}
|
|
109
|
-
let value;
|
|
110
|
-
try {
|
|
111
|
-
value = JSON.parse(text);
|
|
112
|
-
} catch {
|
|
113
|
-
throw profileError(`config ${JSON.stringify(configPath)} is not valid JSON.`);
|
|
114
|
-
}
|
|
115
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw profileError(`config ${JSON.stringify(configPath)} must be an object.`);
|
|
116
|
-
return value;
|
|
117
|
-
}
|
|
118
|
-
function assertPrivateRegularFile(configPath) {
|
|
119
|
-
let stat;
|
|
120
|
-
try {
|
|
121
|
-
stat = lstatSync(configPath);
|
|
122
|
-
} catch {
|
|
123
|
-
throw new Error(`Cannot read host profile ${JSON.stringify(configPath)}.`);
|
|
124
|
-
}
|
|
125
|
-
if (!stat.isFile()) throw profileError(`config ${JSON.stringify(configPath)} must be a regular file.`);
|
|
126
|
-
const currentUid = process.getuid?.();
|
|
127
|
-
if (currentUid === void 0 || stat.uid !== currentUid) throw profileError(`config ${JSON.stringify(configPath)} must be owned by the current user.`);
|
|
128
|
-
if ((stat.mode & 63) !== 0) throw profileError(`config ${JSON.stringify(configPath)} must have private permissions.`);
|
|
129
|
-
}
|
|
130
|
-
function readHostProfile(configPath) {
|
|
131
|
-
const record = readProfileObject(configPath);
|
|
132
|
-
const found = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
133
|
-
if (found.length === 0) return null;
|
|
134
|
-
assertPrivateRegularFile(configPath);
|
|
135
|
-
return validateHostProfile(record);
|
|
136
|
-
}
|
|
137
|
-
function nonempty(env, key) {
|
|
138
|
-
return (env[key] ?? "").trim() !== "";
|
|
139
|
-
}
|
|
56
|
+
import { readGatewayClientProfile, validateGatewayClientProfile } from "@ours.network/sdk/client";
|
|
57
|
+
var validateHostProfile = validateGatewayClientProfile;
|
|
140
58
|
function hostProfileSelectionFromEnv(env = process.env) {
|
|
141
|
-
const
|
|
142
|
-
const managedPath = resolve(env.HOME || homedir(), ".ours-client", "profile.json");
|
|
143
|
-
let configPath = explicit;
|
|
144
|
-
if (!configPath) {
|
|
145
|
-
try {
|
|
146
|
-
lstatSync(managedPath);
|
|
147
|
-
configPath = managedPath;
|
|
148
|
-
} catch (error) {
|
|
149
|
-
if (error.code !== "ENOENT") {
|
|
150
|
-
throw new Error(`Cannot read host profile ${JSON.stringify(managedPath)}.`);
|
|
151
|
-
}
|
|
152
|
-
configPath = resolve(homedir(), ".ours", "config.json");
|
|
153
|
-
if (!existsSync(configPath)) return null;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const profile = readHostProfile(configPath);
|
|
157
|
-
if (profile === null) {
|
|
158
|
-
if (resolve(configPath) === managedPath) throw profileError("managed client config must contain a complete host profile tuple.");
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
const conflicting = CONFLICTING_ENV.filter((key) => nonempty(env, key));
|
|
162
|
-
if (conflicting.length) throw profileError(`${conflicting.join(", ")} conflicts with host-profile mode.`);
|
|
59
|
+
const { configPath, ...profile } = readGatewayClientProfile(env);
|
|
163
60
|
return { profile, configPath };
|
|
164
61
|
}
|
|
165
62
|
function hostProfileFromEnv(env = process.env) {
|
|
166
|
-
return hostProfileSelectionFromEnv(env)
|
|
63
|
+
return hostProfileSelectionFromEnv(env).profile;
|
|
167
64
|
}
|
|
168
65
|
|
|
169
66
|
// ../core/src/native-session.ts
|
|
170
67
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
171
68
|
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
172
|
-
import { dirname as dirname2, join as join2, resolve as
|
|
69
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
|
|
173
70
|
import { attachOursClient } from "@ours.network/sdk";
|
|
174
71
|
|
|
175
72
|
// ../core/src/application-identities.ts
|
|
176
73
|
import { randomUUID } from "node:crypto";
|
|
177
74
|
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
178
|
-
import { homedir
|
|
179
|
-
import { dirname, resolve
|
|
75
|
+
import { homedir } from "node:os";
|
|
76
|
+
import { dirname, resolve } from "node:path";
|
|
180
77
|
var APPLICATION_IDENTITIES_VERSION = 1;
|
|
181
78
|
var APPLICATION_IDENTITIES_ENV = "OURS_MCP_CONFIG";
|
|
182
|
-
var
|
|
79
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
183
80
|
var emptyConfig = () => ({ version: 1, daemons: {}, instances: {} });
|
|
184
81
|
function applicationIdentityConfigPath(env = process.env) {
|
|
185
82
|
const explicit = (env[APPLICATION_IDENTITIES_ENV] ?? "").trim();
|
|
186
|
-
return
|
|
83
|
+
return resolve(explicit || resolve(homedir(), ".ours-mcp", "config.json"));
|
|
187
84
|
}
|
|
188
85
|
function parseRows(raw, path, namespace, normalizeKey) {
|
|
189
86
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -214,9 +111,9 @@ function parseConfig(text, path) {
|
|
|
214
111
|
if (record.version !== APPLICATION_IDENTITIES_VERSION) {
|
|
215
112
|
throw new Error(`Unsupported ours-mcp application identity config version at ${path}: expected ${APPLICATION_IDENTITIES_VERSION}, found ${String(record.version)}.`);
|
|
216
113
|
}
|
|
217
|
-
const daemons = parseRows(record.daemons, path, "daemon",
|
|
114
|
+
const daemons = parseRows(record.daemons, path, "daemon", resolve);
|
|
218
115
|
const instances = record.instances === void 0 ? {} : parseRows(record.instances, path, "instance", (instanceId) => {
|
|
219
|
-
if (!
|
|
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.`);
|
|
220
117
|
return instanceId;
|
|
221
118
|
});
|
|
222
119
|
return { version: 1, daemons, instances };
|
|
@@ -252,14 +149,14 @@ var ApplicationIdentityStore = class {
|
|
|
252
149
|
path;
|
|
253
150
|
constructor(target, options = {}) {
|
|
254
151
|
if (typeof target === "string") {
|
|
255
|
-
this.stateDir =
|
|
152
|
+
this.stateDir = resolve(target);
|
|
256
153
|
this.instanceId = null;
|
|
257
154
|
} else {
|
|
258
|
-
if (!target || !
|
|
155
|
+
if (!target || !UUID.test(target.instanceId)) throw new Error("Application identity instanceId must be a lowercase UUID.");
|
|
259
156
|
this.stateDir = null;
|
|
260
157
|
this.instanceId = target.instanceId;
|
|
261
158
|
}
|
|
262
|
-
this.path =
|
|
159
|
+
this.path = resolve(options.path ?? applicationIdentityConfigPath(options.env));
|
|
263
160
|
}
|
|
264
161
|
rows(config) {
|
|
265
162
|
return this.instanceId === null ? config.daemons : config.instances;
|
|
@@ -299,7 +196,7 @@ var ApplicationIdentityStore = class {
|
|
|
299
196
|
|
|
300
197
|
// ../core/src/session-guard.ts
|
|
301
198
|
import { createHash } from "node:crypto";
|
|
302
|
-
import { openSync, closeSync, lstatSync
|
|
199
|
+
import { openSync, closeSync, lstatSync, mkdirSync, chmodSync, constants } from "node:fs";
|
|
303
200
|
import { join } from "node:path";
|
|
304
201
|
import { createRequire } from "node:module";
|
|
305
202
|
var BUSY_TIMEOUT_MS = 5e3;
|
|
@@ -330,7 +227,7 @@ function ensureGuardFile(guardPath) {
|
|
|
330
227
|
} catch (error) {
|
|
331
228
|
if (error.code !== "EEXIST") throw error;
|
|
332
229
|
}
|
|
333
|
-
const stat =
|
|
230
|
+
const stat = lstatSync(guardPath);
|
|
334
231
|
if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 63) !== 0 || stat.nlink !== 1) {
|
|
335
232
|
throw new Error("Native session guard file must be an owner-only regular file");
|
|
336
233
|
}
|
|
@@ -346,11 +243,11 @@ function acquireSessionGuard(recordPath) {
|
|
|
346
243
|
const hash = createHash("sha256").update(recordPath).digest("hex");
|
|
347
244
|
const guardPath = join(base, `${hash}.guard`);
|
|
348
245
|
ensureGuardFile(guardPath);
|
|
349
|
-
const before =
|
|
246
|
+
const before = lstatSync(guardPath);
|
|
350
247
|
const Database = resolveDatabase();
|
|
351
248
|
const db = new Database(guardPath);
|
|
352
249
|
try {
|
|
353
|
-
const after =
|
|
250
|
+
const after = lstatSync(guardPath);
|
|
354
251
|
if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode || before.uid !== after.uid) {
|
|
355
252
|
throw new Error("Native session guard file changed during acquisition");
|
|
356
253
|
}
|
|
@@ -372,7 +269,7 @@ function acquireSessionGuard(recordPath) {
|
|
|
372
269
|
|
|
373
270
|
// ../core/src/native-session.ts
|
|
374
271
|
var RECORD_VERSION = 1;
|
|
375
|
-
var
|
|
272
|
+
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
376
273
|
var clients = /* @__PURE__ */ new Map();
|
|
377
274
|
var operations = /* @__PURE__ */ new Map();
|
|
378
275
|
function selector(value) {
|
|
@@ -382,10 +279,10 @@ function selector(value) {
|
|
|
382
279
|
return value;
|
|
383
280
|
}
|
|
384
281
|
function nativeSessionRecordPath(hostRecordRoot, instanceId, nativeSessionId) {
|
|
385
|
-
if (!hostRecordRoot || !hostRecordRoot.startsWith("/") ||
|
|
282
|
+
if (!hostRecordRoot || !hostRecordRoot.startsWith("/") || resolve2(hostRecordRoot) !== hostRecordRoot) {
|
|
386
283
|
throw new Error("Native session host record root must be a normalized absolute path.");
|
|
387
284
|
}
|
|
388
|
-
if (!
|
|
285
|
+
if (!UUID2.test(instanceId)) throw new Error("Native session daemon instance must be a lowercase UUID.");
|
|
389
286
|
const hash = createHash2("sha256").update(selector(nativeSessionId), "utf8").digest("hex");
|
|
390
287
|
return join2(hostRecordRoot, "sessions", instanceId, `${hash}.json`);
|
|
391
288
|
}
|
|
@@ -401,7 +298,7 @@ function parseRecord(text, path) {
|
|
|
401
298
|
}
|
|
402
299
|
const record = value;
|
|
403
300
|
const keys = Object.keys(record).sort();
|
|
404
|
-
if (keys.join(",") !== "ownerInstanceId,state,version" || record.version !== RECORD_VERSION || typeof record.ownerInstanceId !== "string" || !
|
|
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))) {
|
|
405
302
|
throw new Error(`Invalid native session record at ${path}.`);
|
|
406
303
|
}
|
|
407
304
|
return record;
|
|
@@ -435,8 +332,8 @@ async function writeRecord(path, record) {
|
|
|
435
332
|
async function serialized(key, operation) {
|
|
436
333
|
const predecessor = operations.get(key) ?? Promise.resolve();
|
|
437
334
|
let release;
|
|
438
|
-
const gate = new Promise((
|
|
439
|
-
release =
|
|
335
|
+
const gate = new Promise((resolve4) => {
|
|
336
|
+
release = resolve4;
|
|
440
337
|
});
|
|
441
338
|
const tail = predecessor.catch(() => {
|
|
442
339
|
}).then(() => gate);
|
|
@@ -545,53 +442,18 @@ function parseOursArgs(argv = []) {
|
|
|
545
442
|
async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch, attach: attach2 = attachOursClient2 } = {}) {
|
|
546
443
|
const parsed = parseOursArgs(argv);
|
|
547
444
|
const hostSelection = hostProfileSelectionFromEnv(env);
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
await client.close();
|
|
559
|
-
}
|
|
560
|
-
return { profile: hostProfile, info: info2, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
|
|
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();
|
|
561
455
|
}
|
|
562
|
-
|
|
563
|
-
...parsed.port == null ? {} : { port: parsed.port },
|
|
564
|
-
env,
|
|
565
|
-
homeDir: env.HOME || homedir3()
|
|
566
|
-
});
|
|
567
|
-
await assertDaemonStateDir(selection, { fetch: fetchImpl, timeoutMs: 2e3 });
|
|
568
|
-
const baseUrl = selection.baseUrl.value;
|
|
569
|
-
const token = selection.token?.value ?? null;
|
|
570
|
-
const headers = token ? { "x-ours-api-token": token } : {};
|
|
571
|
-
const response = await fetchImpl(`${baseUrl}/info`, { signal: AbortSignal.timeout(2e3) });
|
|
572
|
-
if (!response.ok) throw new Error(`ours daemon at ${baseUrl} returned HTTP ${response.status}`);
|
|
573
|
-
const info = await response.json();
|
|
574
|
-
if (info?.name !== "ours" || !Number.isInteger(info?.protocol) || info.protocol < 1) {
|
|
575
|
-
throw new Error(`incompatible service at ${baseUrl}; expected an ours daemon with notification protocol 1`);
|
|
576
|
-
}
|
|
577
|
-
for (const path of ["/identities", "/unread"]) {
|
|
578
|
-
const capability = await fetchImpl(`${baseUrl}${path}`, { headers, signal: AbortSignal.timeout(2e3) });
|
|
579
|
-
if (capability.status === 401 || capability.status === 403) {
|
|
580
|
-
throw new Error(`ours daemon authentication failed at ${baseUrl}; supply the matching coherent selection`);
|
|
581
|
-
}
|
|
582
|
-
if (!capability.ok) throw new Error(`selected daemon lacks ${path} (HTTP ${capability.status})`);
|
|
583
|
-
}
|
|
584
|
-
return {
|
|
585
|
-
port: selection.port.value,
|
|
586
|
-
stateDir: selection.expectStateDir,
|
|
587
|
-
token,
|
|
588
|
-
visibility: "owner",
|
|
589
|
-
source: selection.baseUrl.source,
|
|
590
|
-
info,
|
|
591
|
-
baseUrl,
|
|
592
|
-
configPath: selection.configPath?.value ?? join3(selection.expectStateDir, "config.json"),
|
|
593
|
-
codexArgs: parsed.codexArgs
|
|
594
|
-
};
|
|
456
|
+
return { profile: hostProfile, info, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
|
|
595
457
|
}
|
|
596
458
|
|
|
597
459
|
// src/host-hooks.mjs
|
|
@@ -621,10 +483,10 @@ async function readHostHookState({ profile, nativeSessionId, applicationPath, cl
|
|
|
621
483
|
// src/hooks/runner.mjs
|
|
622
484
|
import { attachOursClient as attachOursClient3 } from "@ours.network/sdk";
|
|
623
485
|
async function defaultFindPin(cwd) {
|
|
624
|
-
let dir =
|
|
486
|
+
let dir = resolve3(cwd || process.cwd());
|
|
625
487
|
for (; ; ) {
|
|
626
488
|
try {
|
|
627
|
-
const value = JSON.parse(await readFile3(
|
|
489
|
+
const value = JSON.parse(await readFile3(join3(dir, ".ours-identity"), "utf8"));
|
|
628
490
|
if (typeof value.identity === "string" && value.identity.trim()) return value;
|
|
629
491
|
return null;
|
|
630
492
|
} catch {
|
|
@@ -634,21 +496,6 @@ async function defaultFindPin(cwd) {
|
|
|
634
496
|
dir = parent;
|
|
635
497
|
}
|
|
636
498
|
}
|
|
637
|
-
var safeUnread = (value) => (Array.isArray(value?.identities) ? value.identities : []).flatMap((entry) => {
|
|
638
|
-
if (!entry || typeof entry.name !== "string") return [];
|
|
639
|
-
const count = Number.isSafeInteger(entry.count) ? entry.count : 0;
|
|
640
|
-
const files = Number.isSafeInteger(entry.files) ? entry.files : 0;
|
|
641
|
-
if (count <= 0 && files <= 0) return [];
|
|
642
|
-
const recent = Array.isArray(entry.recent) ? entry.recent.slice(-5).flatMap((m) => m && typeof m.from === "string" ? [{ from: m.from, msg_id: String(m.msg_id ?? "?"), date: typeof m.date === "string" ? m.date : "" }] : []) : [];
|
|
643
|
-
return [{ name: entry.name, count, files, recent }];
|
|
644
|
-
});
|
|
645
|
-
async function applicationIdentityNames(env, selection) {
|
|
646
|
-
const path = env.OURS_MCP_CONFIG || join4(homedir4(), ".ours-mcp", "config.json");
|
|
647
|
-
const value = JSON.parse(await readFile3(path, "utf8"));
|
|
648
|
-
if (value?.version !== 1 || !value.daemons || typeof value.daemons !== "object") return /* @__PURE__ */ new Set();
|
|
649
|
-
const identities = selection.instanceId ? value.instances?.[selection.instanceId]?.identities : value.daemons[resolve4(selection.stateDir)]?.identities;
|
|
650
|
-
return new Set(Array.isArray(identities) ? identities.filter((name) => typeof name === "string") : []);
|
|
651
|
-
}
|
|
652
499
|
function renderContext(unread, pin) {
|
|
653
500
|
const lines = [];
|
|
654
501
|
if (unread.length) {
|
|
@@ -681,9 +528,6 @@ async function handleHook(payload, { env = process.env, fetch: fetchImpl = globa
|
|
|
681
528
|
if (event === "SessionStart" && socket && capability && payload.session_id && payload.cwd) {
|
|
682
529
|
await send(socket, capability, { command: "register_session", sessionId: payload.session_id, threadId: payload.session_id, cwd: payload.cwd });
|
|
683
530
|
}
|
|
684
|
-
let port = env.OURS_PORT || "3050";
|
|
685
|
-
let selectedStateDir = resolve4(env.OURS_STATE_DIR || join4(homedir4(), ".ours"));
|
|
686
|
-
let selectedToken = env.OURS_API_TOKEN || "";
|
|
687
531
|
let hostProfile;
|
|
688
532
|
try {
|
|
689
533
|
hostProfile = hostProfileFromEnv(env);
|
|
@@ -693,46 +537,13 @@ async function handleHook(payload, { env = process.env, fetch: fetchImpl = globa
|
|
|
693
537
|
if (hostProfile) {
|
|
694
538
|
const nativeSessionId = payload.session_id;
|
|
695
539
|
if (typeof nativeSessionId !== "string" || !nativeSessionId) return { continue: true };
|
|
696
|
-
const appPath = env.OURS_MCP_CONFIG ||
|
|
540
|
+
const appPath = env.OURS_MCP_CONFIG || join3(env.HOME || homedir2(), ".ours-mcp", "config.json");
|
|
697
541
|
const state = await readHostState({ profile: hostProfile, nativeSessionId, applicationPath: appPath });
|
|
698
|
-
const
|
|
699
|
-
const
|
|
700
|
-
return
|
|
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 };
|
|
701
545
|
}
|
|
702
|
-
|
|
703
|
-
const selected = await profileResolver({ env, fetch: fetchImpl });
|
|
704
|
-
if (selected.profile) {
|
|
705
|
-
let client;
|
|
706
|
-
try {
|
|
707
|
-
client = await clientFactory({ ...selected.profile, sessionMode: "external", leaseToken: `codex-hook-${process.pid}`, env: {} });
|
|
708
|
-
const visible = await applicationIdentityNames(env, { instanceId: selected.profile.expectedInstanceId });
|
|
709
|
-
const unread2 = safeUnread(await client.unread()).filter((entry) => visible.has(entry.name));
|
|
710
|
-
const pin2 = await findPin(payload.cwd || process.cwd());
|
|
711
|
-
const context2 = renderContext(unread2, pin2);
|
|
712
|
-
return context2 ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context2 } } : { continue: true };
|
|
713
|
-
} finally {
|
|
714
|
-
await client?.close();
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
port = String(selected.port);
|
|
718
|
-
selectedToken = selected.token || "";
|
|
719
|
-
selectedStateDir = resolve4(selected.stateDir);
|
|
720
|
-
} catch {
|
|
721
|
-
}
|
|
722
|
-
const headers = selectedToken ? { "x-ours-api-token": selectedToken } : {};
|
|
723
|
-
let unread = [];
|
|
724
|
-
try {
|
|
725
|
-
const response = await fetchImpl(`http://127.0.0.1:${port}/unread`, { headers, signal: AbortSignal.timeout(1500) });
|
|
726
|
-
if (response.ok) {
|
|
727
|
-
const visible = await applicationIdentityNames(env, { stateDir: selectedStateDir });
|
|
728
|
-
unread = safeUnread(await response.json()).filter((entry) => visible.has(entry.name));
|
|
729
|
-
}
|
|
730
|
-
} catch {
|
|
731
|
-
}
|
|
732
|
-
const pin = await findPin(payload.cwd || process.cwd());
|
|
733
|
-
const context = renderContext(unread, pin);
|
|
734
|
-
if (!context) return { continue: true };
|
|
735
|
-
return { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } };
|
|
546
|
+
return { continue: true };
|
|
736
547
|
} catch {
|
|
737
548
|
return { continue: true };
|
|
738
549
|
}
|
|
@@ -740,7 +551,7 @@ async function handleHook(payload, { env = process.env, fetch: fetchImpl = globa
|
|
|
740
551
|
async function main() {
|
|
741
552
|
let payload = {};
|
|
742
553
|
try {
|
|
743
|
-
payload = JSON.parse(
|
|
554
|
+
payload = JSON.parse(readFileSync(0, "utf8"));
|
|
744
555
|
} catch {
|
|
745
556
|
}
|
|
746
557
|
process.stdout.write(JSON.stringify(await handleHook(payload)));
|