@ours.network/codex 1.2.0-nightly.7 → 1.2.0-nightly.9
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 -239
- package/dist/host-hooks.mjs +9 -49
- package/dist/monitor-mcp.mjs +20 -123
- package/dist/network-watch.mjs +17 -119
- package/dist/profile.mjs +14 -154
- 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,142 +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
|
-
import { normalizeDaemonEndpoint } from "@ours.network/sdk/client";
|
|
62
|
-
var PROFILE_KEYS = ["endpoint", "expectedInstanceId", "credentialPath"];
|
|
63
|
-
var LEGACY_SELECTION_KEYS = ["port", "stateDir", "apiToken", "apiVisibility"];
|
|
64
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
65
|
-
var CONFLICTING_ENV = ["OURS_API_TOKEN", "OURS_PORT", "OURS_STATE_DIR", "OURS_DAEMON_ID"];
|
|
66
|
-
function profileError(reason) {
|
|
67
|
-
return new Error(`Invalid external host profile: ${reason}`);
|
|
68
|
-
}
|
|
69
|
-
function validateHostProfile(value) {
|
|
70
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
71
|
-
throw profileError("expected a complete host profile object.");
|
|
72
|
-
}
|
|
73
|
-
const record = value;
|
|
74
|
-
const present = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
75
|
-
if (present.length !== PROFILE_KEYS.length) {
|
|
76
|
-
throw profileError("expected a complete host profile tuple: endpoint, expectedInstanceId, credentialPath.");
|
|
77
|
-
}
|
|
78
|
-
const mixed = LEGACY_SELECTION_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
79
|
-
if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(", ")}).`);
|
|
80
|
-
const endpoint = record.endpoint;
|
|
81
|
-
const expectedInstanceId = record.expectedInstanceId;
|
|
82
|
-
const credentialPath = record.credentialPath;
|
|
83
|
-
if (typeof endpoint !== "string" || endpoint.trim() !== endpoint || endpoint === "") {
|
|
84
|
-
throw profileError("endpoint must be a non-empty HTTP or HTTPS base URL.");
|
|
85
|
-
}
|
|
86
|
-
if (typeof expectedInstanceId !== "string" || !UUID.test(expectedInstanceId)) {
|
|
87
|
-
throw profileError("expectedInstanceId must be a lowercase UUID.");
|
|
88
|
-
}
|
|
89
|
-
if (typeof credentialPath !== "string" || credentialPath === "" || !credentialPath.startsWith("/") || resolve(credentialPath) !== credentialPath) {
|
|
90
|
-
throw profileError("credentialPath must be a normalized absolute path.");
|
|
91
|
-
}
|
|
92
|
-
let url;
|
|
93
|
-
try {
|
|
94
|
-
url = new URL(endpoint);
|
|
95
|
-
} catch {
|
|
96
|
-
throw profileError("endpoint must be an HTTP or HTTPS base URL.");
|
|
97
|
-
}
|
|
98
|
-
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || /[\s\\?#]/.test(endpoint) || url.search || url.hash) {
|
|
99
|
-
throw profileError("endpoint must be an HTTP or HTTPS base URL without credentials, query, or fragment.");
|
|
100
|
-
}
|
|
101
|
-
return { endpoint: normalizeDaemonEndpoint(endpoint), expectedInstanceId, credentialPath };
|
|
102
|
-
}
|
|
103
|
-
function readProfileObject(configPath) {
|
|
104
|
-
let text;
|
|
105
|
-
try {
|
|
106
|
-
text = readFileSync(configPath, "utf8");
|
|
107
|
-
} catch {
|
|
108
|
-
throw new Error(`Cannot read host profile ${JSON.stringify(configPath)}.`);
|
|
109
|
-
}
|
|
110
|
-
let value;
|
|
111
|
-
try {
|
|
112
|
-
value = JSON.parse(text);
|
|
113
|
-
} catch {
|
|
114
|
-
throw profileError(`config ${JSON.stringify(configPath)} is not valid JSON.`);
|
|
115
|
-
}
|
|
116
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw profileError(`config ${JSON.stringify(configPath)} must be an object.`);
|
|
117
|
-
return value;
|
|
118
|
-
}
|
|
119
|
-
function assertPrivateRegularFile(configPath) {
|
|
120
|
-
let stat;
|
|
121
|
-
try {
|
|
122
|
-
stat = lstatSync(configPath);
|
|
123
|
-
} catch {
|
|
124
|
-
throw new Error(`Cannot read host profile ${JSON.stringify(configPath)}.`);
|
|
125
|
-
}
|
|
126
|
-
if (!stat.isFile()) throw profileError(`config ${JSON.stringify(configPath)} must be a regular file.`);
|
|
127
|
-
const currentUid = process.getuid?.();
|
|
128
|
-
if (currentUid === void 0 || stat.uid !== currentUid) throw profileError(`config ${JSON.stringify(configPath)} must be owned by the current user.`);
|
|
129
|
-
if ((stat.mode & 63) !== 0) throw profileError(`config ${JSON.stringify(configPath)} must have private permissions.`);
|
|
130
|
-
}
|
|
131
|
-
function readHostProfile(configPath) {
|
|
132
|
-
const record = readProfileObject(configPath);
|
|
133
|
-
const found = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
134
|
-
if (found.length === 0) return null;
|
|
135
|
-
assertPrivateRegularFile(configPath);
|
|
136
|
-
return validateHostProfile(record);
|
|
137
|
-
}
|
|
138
|
-
function nonempty(env, key) {
|
|
139
|
-
return (env[key] ?? "").trim() !== "";
|
|
140
|
-
}
|
|
56
|
+
import { readGatewayClientProfile, validateGatewayClientProfile } from "@ours.network/sdk/client";
|
|
57
|
+
var validateHostProfile = validateGatewayClientProfile;
|
|
141
58
|
function hostProfileSelectionFromEnv(env = process.env) {
|
|
142
|
-
const
|
|
143
|
-
const managedPath = resolve(env.HOME || homedir(), ".ours-client", "profile.json");
|
|
144
|
-
let configPath = explicit;
|
|
145
|
-
if (!configPath) {
|
|
146
|
-
try {
|
|
147
|
-
lstatSync(managedPath);
|
|
148
|
-
configPath = managedPath;
|
|
149
|
-
} catch (error) {
|
|
150
|
-
if (error.code !== "ENOENT") {
|
|
151
|
-
throw new Error(`Cannot read host profile ${JSON.stringify(managedPath)}.`);
|
|
152
|
-
}
|
|
153
|
-
configPath = resolve(homedir(), ".ours", "config.json");
|
|
154
|
-
if (!existsSync(configPath)) return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const profile = readHostProfile(configPath);
|
|
158
|
-
if (profile === null) {
|
|
159
|
-
if (resolve(configPath) === managedPath) throw profileError("managed client config must contain a complete host profile tuple.");
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
const conflicting = CONFLICTING_ENV.filter((key) => nonempty(env, key));
|
|
163
|
-
if (conflicting.length) throw profileError(`${conflicting.join(", ")} conflicts with host-profile mode.`);
|
|
59
|
+
const { configPath, ...profile } = readGatewayClientProfile(env);
|
|
164
60
|
return { profile, configPath };
|
|
165
61
|
}
|
|
166
62
|
function hostProfileFromEnv(env = process.env) {
|
|
167
|
-
return hostProfileSelectionFromEnv(env)
|
|
63
|
+
return hostProfileSelectionFromEnv(env).profile;
|
|
168
64
|
}
|
|
169
65
|
|
|
170
66
|
// ../core/src/native-session.ts
|
|
171
67
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
172
68
|
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
173
|
-
import { dirname as dirname2, join as join2, resolve as
|
|
69
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
|
|
174
70
|
import { attachOursClient } from "@ours.network/sdk";
|
|
175
71
|
|
|
176
72
|
// ../core/src/application-identities.ts
|
|
177
73
|
import { randomUUID } from "node:crypto";
|
|
178
74
|
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
179
|
-
import { homedir
|
|
180
|
-
import { dirname, resolve
|
|
75
|
+
import { homedir } from "node:os";
|
|
76
|
+
import { dirname, resolve } from "node:path";
|
|
181
77
|
var APPLICATION_IDENTITIES_VERSION = 1;
|
|
182
78
|
var APPLICATION_IDENTITIES_ENV = "OURS_MCP_CONFIG";
|
|
183
|
-
var
|
|
79
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
184
80
|
var emptyConfig = () => ({ version: 1, daemons: {}, instances: {} });
|
|
185
81
|
function applicationIdentityConfigPath(env = process.env) {
|
|
186
82
|
const explicit = (env[APPLICATION_IDENTITIES_ENV] ?? "").trim();
|
|
187
|
-
return
|
|
83
|
+
return resolve(explicit || resolve(homedir(), ".ours-mcp", "config.json"));
|
|
188
84
|
}
|
|
189
85
|
function parseRows(raw, path, namespace, normalizeKey) {
|
|
190
86
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -215,9 +111,9 @@ function parseConfig(text, path) {
|
|
|
215
111
|
if (record.version !== APPLICATION_IDENTITIES_VERSION) {
|
|
216
112
|
throw new Error(`Unsupported ours-mcp application identity config version at ${path}: expected ${APPLICATION_IDENTITIES_VERSION}, found ${String(record.version)}.`);
|
|
217
113
|
}
|
|
218
|
-
const daemons = parseRows(record.daemons, path, "daemon",
|
|
114
|
+
const daemons = parseRows(record.daemons, path, "daemon", resolve);
|
|
219
115
|
const instances = record.instances === void 0 ? {} : parseRows(record.instances, path, "instance", (instanceId) => {
|
|
220
|
-
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.`);
|
|
221
117
|
return instanceId;
|
|
222
118
|
});
|
|
223
119
|
return { version: 1, daemons, instances };
|
|
@@ -253,14 +149,14 @@ var ApplicationIdentityStore = class {
|
|
|
253
149
|
path;
|
|
254
150
|
constructor(target, options = {}) {
|
|
255
151
|
if (typeof target === "string") {
|
|
256
|
-
this.stateDir =
|
|
152
|
+
this.stateDir = resolve(target);
|
|
257
153
|
this.instanceId = null;
|
|
258
154
|
} else {
|
|
259
|
-
if (!target || !
|
|
155
|
+
if (!target || !UUID.test(target.instanceId)) throw new Error("Application identity instanceId must be a lowercase UUID.");
|
|
260
156
|
this.stateDir = null;
|
|
261
157
|
this.instanceId = target.instanceId;
|
|
262
158
|
}
|
|
263
|
-
this.path =
|
|
159
|
+
this.path = resolve(options.path ?? applicationIdentityConfigPath(options.env));
|
|
264
160
|
}
|
|
265
161
|
rows(config) {
|
|
266
162
|
return this.instanceId === null ? config.daemons : config.instances;
|
|
@@ -300,7 +196,7 @@ var ApplicationIdentityStore = class {
|
|
|
300
196
|
|
|
301
197
|
// ../core/src/session-guard.ts
|
|
302
198
|
import { createHash } from "node:crypto";
|
|
303
|
-
import { openSync, closeSync, lstatSync
|
|
199
|
+
import { openSync, closeSync, lstatSync, mkdirSync, chmodSync, constants } from "node:fs";
|
|
304
200
|
import { join } from "node:path";
|
|
305
201
|
import { createRequire } from "node:module";
|
|
306
202
|
var BUSY_TIMEOUT_MS = 5e3;
|
|
@@ -331,7 +227,7 @@ function ensureGuardFile(guardPath) {
|
|
|
331
227
|
} catch (error) {
|
|
332
228
|
if (error.code !== "EEXIST") throw error;
|
|
333
229
|
}
|
|
334
|
-
const stat =
|
|
230
|
+
const stat = lstatSync(guardPath);
|
|
335
231
|
if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 63) !== 0 || stat.nlink !== 1) {
|
|
336
232
|
throw new Error("Native session guard file must be an owner-only regular file");
|
|
337
233
|
}
|
|
@@ -347,11 +243,11 @@ function acquireSessionGuard(recordPath) {
|
|
|
347
243
|
const hash = createHash("sha256").update(recordPath).digest("hex");
|
|
348
244
|
const guardPath = join(base, `${hash}.guard`);
|
|
349
245
|
ensureGuardFile(guardPath);
|
|
350
|
-
const before =
|
|
246
|
+
const before = lstatSync(guardPath);
|
|
351
247
|
const Database = resolveDatabase();
|
|
352
248
|
const db = new Database(guardPath);
|
|
353
249
|
try {
|
|
354
|
-
const after =
|
|
250
|
+
const after = lstatSync(guardPath);
|
|
355
251
|
if (before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode || before.uid !== after.uid) {
|
|
356
252
|
throw new Error("Native session guard file changed during acquisition");
|
|
357
253
|
}
|
|
@@ -373,7 +269,7 @@ function acquireSessionGuard(recordPath) {
|
|
|
373
269
|
|
|
374
270
|
// ../core/src/native-session.ts
|
|
375
271
|
var RECORD_VERSION = 1;
|
|
376
|
-
var
|
|
272
|
+
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
377
273
|
var clients = /* @__PURE__ */ new Map();
|
|
378
274
|
var operations = /* @__PURE__ */ new Map();
|
|
379
275
|
function selector(value) {
|
|
@@ -383,10 +279,10 @@ function selector(value) {
|
|
|
383
279
|
return value;
|
|
384
280
|
}
|
|
385
281
|
function nativeSessionRecordPath(hostRecordRoot, instanceId, nativeSessionId) {
|
|
386
|
-
if (!hostRecordRoot || !hostRecordRoot.startsWith("/") ||
|
|
282
|
+
if (!hostRecordRoot || !hostRecordRoot.startsWith("/") || resolve2(hostRecordRoot) !== hostRecordRoot) {
|
|
387
283
|
throw new Error("Native session host record root must be a normalized absolute path.");
|
|
388
284
|
}
|
|
389
|
-
if (!
|
|
285
|
+
if (!UUID2.test(instanceId)) throw new Error("Native session daemon instance must be a lowercase UUID.");
|
|
390
286
|
const hash = createHash2("sha256").update(selector(nativeSessionId), "utf8").digest("hex");
|
|
391
287
|
return join2(hostRecordRoot, "sessions", instanceId, `${hash}.json`);
|
|
392
288
|
}
|
|
@@ -402,7 +298,7 @@ function parseRecord(text, path) {
|
|
|
402
298
|
}
|
|
403
299
|
const record = value;
|
|
404
300
|
const keys = Object.keys(record).sort();
|
|
405
|
-
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))) {
|
|
406
302
|
throw new Error(`Invalid native session record at ${path}.`);
|
|
407
303
|
}
|
|
408
304
|
return record;
|
|
@@ -436,8 +332,8 @@ async function writeRecord(path, record) {
|
|
|
436
332
|
async function serialized(key, operation) {
|
|
437
333
|
const predecessor = operations.get(key) ?? Promise.resolve();
|
|
438
334
|
let release;
|
|
439
|
-
const gate = new Promise((
|
|
440
|
-
release =
|
|
335
|
+
const gate = new Promise((resolve4) => {
|
|
336
|
+
release = resolve4;
|
|
441
337
|
});
|
|
442
338
|
const tail = predecessor.catch(() => {
|
|
443
339
|
}).then(() => gate);
|
|
@@ -546,53 +442,18 @@ function parseOursArgs(argv = []) {
|
|
|
546
442
|
async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch, attach: attach2 = attachOursClient2 } = {}) {
|
|
547
443
|
const parsed = parseOursArgs(argv);
|
|
548
444
|
const hostSelection = hostProfileSelectionFromEnv(env);
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
await client.close();
|
|
560
|
-
}
|
|
561
|
-
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();
|
|
562
455
|
}
|
|
563
|
-
|
|
564
|
-
...parsed.port == null ? {} : { port: parsed.port },
|
|
565
|
-
env,
|
|
566
|
-
homeDir: env.HOME || homedir3()
|
|
567
|
-
});
|
|
568
|
-
await assertDaemonStateDir(selection, { fetch: fetchImpl, timeoutMs: 2e3 });
|
|
569
|
-
const baseUrl = selection.baseUrl.value;
|
|
570
|
-
const token = selection.token?.value ?? null;
|
|
571
|
-
const headers = token ? { "x-ours-api-token": token } : {};
|
|
572
|
-
const response = await fetchImpl(`${baseUrl}/info`, { signal: AbortSignal.timeout(2e3) });
|
|
573
|
-
if (!response.ok) throw new Error(`ours daemon at ${baseUrl} returned HTTP ${response.status}`);
|
|
574
|
-
const info = await response.json();
|
|
575
|
-
if (info?.name !== "ours" || !Number.isInteger(info?.protocol) || info.protocol < 1) {
|
|
576
|
-
throw new Error(`incompatible service at ${baseUrl}; expected an ours daemon with notification protocol 1`);
|
|
577
|
-
}
|
|
578
|
-
for (const path of ["/identities", "/unread"]) {
|
|
579
|
-
const capability = await fetchImpl(`${baseUrl}${path}`, { headers, signal: AbortSignal.timeout(2e3) });
|
|
580
|
-
if (capability.status === 401 || capability.status === 403) {
|
|
581
|
-
throw new Error(`ours daemon authentication failed at ${baseUrl}; supply the matching coherent selection`);
|
|
582
|
-
}
|
|
583
|
-
if (!capability.ok) throw new Error(`selected daemon lacks ${path} (HTTP ${capability.status})`);
|
|
584
|
-
}
|
|
585
|
-
return {
|
|
586
|
-
port: selection.port.value,
|
|
587
|
-
stateDir: selection.expectStateDir,
|
|
588
|
-
token,
|
|
589
|
-
visibility: "owner",
|
|
590
|
-
source: selection.baseUrl.source,
|
|
591
|
-
info,
|
|
592
|
-
baseUrl,
|
|
593
|
-
configPath: selection.configPath?.value ?? join3(selection.expectStateDir, "config.json"),
|
|
594
|
-
codexArgs: parsed.codexArgs
|
|
595
|
-
};
|
|
456
|
+
return { profile: hostProfile, info, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
|
|
596
457
|
}
|
|
597
458
|
|
|
598
459
|
// src/host-hooks.mjs
|
|
@@ -622,10 +483,10 @@ async function readHostHookState({ profile, nativeSessionId, applicationPath, cl
|
|
|
622
483
|
// src/hooks/runner.mjs
|
|
623
484
|
import { attachOursClient as attachOursClient3 } from "@ours.network/sdk";
|
|
624
485
|
async function defaultFindPin(cwd) {
|
|
625
|
-
let dir =
|
|
486
|
+
let dir = resolve3(cwd || process.cwd());
|
|
626
487
|
for (; ; ) {
|
|
627
488
|
try {
|
|
628
|
-
const value = JSON.parse(await readFile3(
|
|
489
|
+
const value = JSON.parse(await readFile3(join3(dir, ".ours-identity"), "utf8"));
|
|
629
490
|
if (typeof value.identity === "string" && value.identity.trim()) return value;
|
|
630
491
|
return null;
|
|
631
492
|
} catch {
|
|
@@ -635,21 +496,6 @@ async function defaultFindPin(cwd) {
|
|
|
635
496
|
dir = parent;
|
|
636
497
|
}
|
|
637
498
|
}
|
|
638
|
-
var safeUnread = (value) => (Array.isArray(value?.identities) ? value.identities : []).flatMap((entry) => {
|
|
639
|
-
if (!entry || typeof entry.name !== "string") return [];
|
|
640
|
-
const count = Number.isSafeInteger(entry.count) ? entry.count : 0;
|
|
641
|
-
const files = Number.isSafeInteger(entry.files) ? entry.files : 0;
|
|
642
|
-
if (count <= 0 && files <= 0) return [];
|
|
643
|
-
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 : "" }] : []) : [];
|
|
644
|
-
return [{ name: entry.name, count, files, recent }];
|
|
645
|
-
});
|
|
646
|
-
async function applicationIdentityNames(env, selection) {
|
|
647
|
-
const path = env.OURS_MCP_CONFIG || join4(homedir4(), ".ours-mcp", "config.json");
|
|
648
|
-
const value = JSON.parse(await readFile3(path, "utf8"));
|
|
649
|
-
if (value?.version !== 1 || !value.daemons || typeof value.daemons !== "object") return /* @__PURE__ */ new Set();
|
|
650
|
-
const identities = selection.instanceId ? value.instances?.[selection.instanceId]?.identities : value.daemons[resolve4(selection.stateDir)]?.identities;
|
|
651
|
-
return new Set(Array.isArray(identities) ? identities.filter((name) => typeof name === "string") : []);
|
|
652
|
-
}
|
|
653
499
|
function renderContext(unread, pin) {
|
|
654
500
|
const lines = [];
|
|
655
501
|
if (unread.length) {
|
|
@@ -682,9 +528,6 @@ async function handleHook(payload, { env = process.env, fetch: fetchImpl = globa
|
|
|
682
528
|
if (event === "SessionStart" && socket && capability && payload.session_id && payload.cwd) {
|
|
683
529
|
await send(socket, capability, { command: "register_session", sessionId: payload.session_id, threadId: payload.session_id, cwd: payload.cwd });
|
|
684
530
|
}
|
|
685
|
-
let port = env.OURS_PORT || "3050";
|
|
686
|
-
let selectedStateDir = resolve4(env.OURS_STATE_DIR || join4(homedir4(), ".ours"));
|
|
687
|
-
let selectedToken = env.OURS_API_TOKEN || "";
|
|
688
531
|
let hostProfile;
|
|
689
532
|
try {
|
|
690
533
|
hostProfile = hostProfileFromEnv(env);
|
|
@@ -694,46 +537,13 @@ async function handleHook(payload, { env = process.env, fetch: fetchImpl = globa
|
|
|
694
537
|
if (hostProfile) {
|
|
695
538
|
const nativeSessionId = payload.session_id;
|
|
696
539
|
if (typeof nativeSessionId !== "string" || !nativeSessionId) return { continue: true };
|
|
697
|
-
const appPath = env.OURS_MCP_CONFIG ||
|
|
540
|
+
const appPath = env.OURS_MCP_CONFIG || join3(env.HOME || homedir2(), ".ours-mcp", "config.json");
|
|
698
541
|
const state = await readHostState({ profile: hostProfile, nativeSessionId, applicationPath: appPath });
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
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 };
|
|
702
545
|
}
|
|
703
|
-
|
|
704
|
-
const selected = await profileResolver({ env, fetch: fetchImpl });
|
|
705
|
-
if (selected.profile) {
|
|
706
|
-
let client;
|
|
707
|
-
try {
|
|
708
|
-
client = await clientFactory({ ...selected.profile, sessionMode: "external", leaseToken: `codex-hook-${process.pid}`, env: {} });
|
|
709
|
-
const visible = await applicationIdentityNames(env, { instanceId: selected.profile.expectedInstanceId });
|
|
710
|
-
const unread2 = safeUnread(await client.unread()).filter((entry) => visible.has(entry.name));
|
|
711
|
-
const pin2 = await findPin(payload.cwd || process.cwd());
|
|
712
|
-
const context2 = renderContext(unread2, pin2);
|
|
713
|
-
return context2 ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context2 } } : { continue: true };
|
|
714
|
-
} finally {
|
|
715
|
-
await client?.close();
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
port = String(selected.port);
|
|
719
|
-
selectedToken = selected.token || "";
|
|
720
|
-
selectedStateDir = resolve4(selected.stateDir);
|
|
721
|
-
} catch {
|
|
722
|
-
}
|
|
723
|
-
const headers = selectedToken ? { "x-ours-api-token": selectedToken } : {};
|
|
724
|
-
let unread = [];
|
|
725
|
-
try {
|
|
726
|
-
const response = await fetchImpl(`http://127.0.0.1:${port}/unread`, { headers, signal: AbortSignal.timeout(1500) });
|
|
727
|
-
if (response.ok) {
|
|
728
|
-
const visible = await applicationIdentityNames(env, { stateDir: selectedStateDir });
|
|
729
|
-
unread = safeUnread(await response.json()).filter((entry) => visible.has(entry.name));
|
|
730
|
-
}
|
|
731
|
-
} catch {
|
|
732
|
-
}
|
|
733
|
-
const pin = await findPin(payload.cwd || process.cwd());
|
|
734
|
-
const context = renderContext(unread, pin);
|
|
735
|
-
if (!context) return { continue: true };
|
|
736
|
-
return { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } };
|
|
546
|
+
return { continue: true };
|
|
737
547
|
} catch {
|
|
738
548
|
return { continue: true };
|
|
739
549
|
}
|
|
@@ -741,7 +551,7 @@ async function handleHook(payload, { env = process.env, fetch: fetchImpl = globa
|
|
|
741
551
|
async function main() {
|
|
742
552
|
let payload = {};
|
|
743
553
|
try {
|
|
744
|
-
payload = JSON.parse(
|
|
554
|
+
payload = JSON.parse(readFileSync(0, "utf8"));
|
|
745
555
|
} catch {
|
|
746
556
|
}
|
|
747
557
|
process.stdout.write(JSON.stringify(await handleHook(payload)));
|