@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/dist/profile.mjs
CHANGED
|
@@ -1,115 +1,11 @@
|
|
|
1
1
|
// src/profile.mjs
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { homedir as homedir2 } from "node:os";
|
|
4
2
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import {
|
|
3
|
+
import { attachOursClient as attachOursClient2 } from "@ours.network/sdk";
|
|
6
4
|
|
|
7
5
|
// ../core/src/host-profile.ts
|
|
8
|
-
import {
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
|
-
import { resolve } from "node:path";
|
|
11
|
-
var PROFILE_KEYS = ["endpoint", "expectedInstanceId", "credentialPath"];
|
|
12
|
-
var LEGACY_SELECTION_KEYS = ["port", "stateDir", "apiToken", "apiVisibility"];
|
|
13
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
14
|
-
var CONFLICTING_ENV = ["OURS_API_TOKEN", "OURS_PORT", "OURS_STATE_DIR", "OURS_DAEMON_ID"];
|
|
15
|
-
function profileError(reason) {
|
|
16
|
-
return new Error(`Invalid external host profile: ${reason}`);
|
|
17
|
-
}
|
|
18
|
-
function validateHostProfile(value) {
|
|
19
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
20
|
-
throw profileError("expected a complete host profile object.");
|
|
21
|
-
}
|
|
22
|
-
const record = value;
|
|
23
|
-
const present = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
24
|
-
if (present.length !== PROFILE_KEYS.length) {
|
|
25
|
-
throw profileError("expected a complete host profile tuple: endpoint, expectedInstanceId, credentialPath.");
|
|
26
|
-
}
|
|
27
|
-
const mixed = LEGACY_SELECTION_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
28
|
-
if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(", ")}).`);
|
|
29
|
-
const endpoint = record.endpoint;
|
|
30
|
-
const expectedInstanceId = record.expectedInstanceId;
|
|
31
|
-
const credentialPath = record.credentialPath;
|
|
32
|
-
if (typeof endpoint !== "string" || endpoint.trim() !== endpoint || endpoint === "") {
|
|
33
|
-
throw profileError("endpoint must be a non-empty HTTP or HTTPS origin.");
|
|
34
|
-
}
|
|
35
|
-
if (typeof expectedInstanceId !== "string" || !UUID.test(expectedInstanceId)) {
|
|
36
|
-
throw profileError("expectedInstanceId must be a lowercase UUID.");
|
|
37
|
-
}
|
|
38
|
-
if (typeof credentialPath !== "string" || credentialPath === "" || !credentialPath.startsWith("/") || resolve(credentialPath) !== credentialPath) {
|
|
39
|
-
throw profileError("credentialPath must be a normalized absolute path.");
|
|
40
|
-
}
|
|
41
|
-
let url;
|
|
42
|
-
try {
|
|
43
|
-
url = new URL(endpoint);
|
|
44
|
-
} catch {
|
|
45
|
-
throw profileError("endpoint must be an HTTP or HTTPS origin.");
|
|
46
|
-
}
|
|
47
|
-
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
48
|
-
throw profileError("endpoint must be an HTTP or HTTPS origin without credentials, path, query, or fragment.");
|
|
49
|
-
}
|
|
50
|
-
return { endpoint: url.origin, expectedInstanceId, credentialPath };
|
|
51
|
-
}
|
|
52
|
-
function readProfileObject(configPath) {
|
|
53
|
-
let text;
|
|
54
|
-
try {
|
|
55
|
-
text = readFileSync(configPath, "utf8");
|
|
56
|
-
} catch {
|
|
57
|
-
throw new Error(`Cannot read host profile ${JSON.stringify(configPath)}.`);
|
|
58
|
-
}
|
|
59
|
-
let value;
|
|
60
|
-
try {
|
|
61
|
-
value = JSON.parse(text);
|
|
62
|
-
} catch {
|
|
63
|
-
throw profileError(`config ${JSON.stringify(configPath)} is not valid JSON.`);
|
|
64
|
-
}
|
|
65
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw profileError(`config ${JSON.stringify(configPath)} must be an object.`);
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
68
|
-
function assertPrivateRegularFile(configPath) {
|
|
69
|
-
let stat;
|
|
70
|
-
try {
|
|
71
|
-
stat = lstatSync(configPath);
|
|
72
|
-
} catch {
|
|
73
|
-
throw new Error(`Cannot read host profile ${JSON.stringify(configPath)}.`);
|
|
74
|
-
}
|
|
75
|
-
if (!stat.isFile()) throw profileError(`config ${JSON.stringify(configPath)} must be a regular file.`);
|
|
76
|
-
const currentUid = process.getuid?.();
|
|
77
|
-
if (currentUid === void 0 || stat.uid !== currentUid) throw profileError(`config ${JSON.stringify(configPath)} must be owned by the current user.`);
|
|
78
|
-
if ((stat.mode & 63) !== 0) throw profileError(`config ${JSON.stringify(configPath)} must have private permissions.`);
|
|
79
|
-
}
|
|
80
|
-
function readHostProfile(configPath) {
|
|
81
|
-
const record = readProfileObject(configPath);
|
|
82
|
-
const found = PROFILE_KEYS.filter((key) => Object.hasOwn(record, key));
|
|
83
|
-
if (found.length === 0) return null;
|
|
84
|
-
assertPrivateRegularFile(configPath);
|
|
85
|
-
return validateHostProfile(record);
|
|
86
|
-
}
|
|
87
|
-
function nonempty(env, key) {
|
|
88
|
-
return (env[key] ?? "").trim() !== "";
|
|
89
|
-
}
|
|
6
|
+
import { readGatewayClientProfile, validateGatewayClientProfile } from "@ours.network/sdk/client";
|
|
90
7
|
function hostProfileSelectionFromEnv(env = process.env) {
|
|
91
|
-
const
|
|
92
|
-
const managedPath = resolve(env.HOME || homedir(), ".ours-client", "profile.json");
|
|
93
|
-
let configPath = explicit;
|
|
94
|
-
if (!configPath) {
|
|
95
|
-
try {
|
|
96
|
-
lstatSync(managedPath);
|
|
97
|
-
configPath = managedPath;
|
|
98
|
-
} catch (error) {
|
|
99
|
-
if (error.code !== "ENOENT") {
|
|
100
|
-
throw new Error(`Cannot read host profile ${JSON.stringify(managedPath)}.`);
|
|
101
|
-
}
|
|
102
|
-
configPath = resolve(homedir(), ".ours", "config.json");
|
|
103
|
-
if (!existsSync(configPath)) return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const profile = readHostProfile(configPath);
|
|
107
|
-
if (profile === null) {
|
|
108
|
-
if (resolve(configPath) === managedPath) throw profileError("managed client config must contain a complete host profile tuple.");
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
const conflicting = CONFLICTING_ENV.filter((key) => nonempty(env, key));
|
|
112
|
-
if (conflicting.length) throw profileError(`${conflicting.join(", ")} conflicts with host-profile mode.`);
|
|
8
|
+
const { configPath, ...profile } = readGatewayClientProfile(env);
|
|
113
9
|
return { profile, configPath };
|
|
114
10
|
}
|
|
115
11
|
|
|
@@ -141,53 +37,18 @@ function parseOursArgs(argv = []) {
|
|
|
141
37
|
async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch, attach = attachOursClient2 } = {}) {
|
|
142
38
|
const parsed = parseOursArgs(argv);
|
|
143
39
|
const hostSelection = hostProfileSelectionFromEnv(env);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await client.close();
|
|
155
|
-
}
|
|
156
|
-
return { profile: hostProfile, info: info2, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
|
|
157
|
-
}
|
|
158
|
-
const selection = resolveDaemonConfig({
|
|
159
|
-
...parsed.port == null ? {} : { port: parsed.port },
|
|
160
|
-
env,
|
|
161
|
-
homeDir: env.HOME || homedir2()
|
|
162
|
-
});
|
|
163
|
-
await assertDaemonStateDir(selection, { fetch: fetchImpl, timeoutMs: 2e3 });
|
|
164
|
-
const baseUrl = selection.baseUrl.value;
|
|
165
|
-
const token = selection.token?.value ?? null;
|
|
166
|
-
const headers = token ? { "x-ours-api-token": token } : {};
|
|
167
|
-
const response = await fetchImpl(`${baseUrl}/info`, { signal: AbortSignal.timeout(2e3) });
|
|
168
|
-
if (!response.ok) throw new Error(`ours daemon at ${baseUrl} returned HTTP ${response.status}`);
|
|
169
|
-
const info = await response.json();
|
|
170
|
-
if (info?.name !== "ours" || !Number.isInteger(info?.protocol) || info.protocol < 1) {
|
|
171
|
-
throw new Error(`incompatible service at ${baseUrl}; expected an ours daemon with notification protocol 1`);
|
|
172
|
-
}
|
|
173
|
-
for (const path of ["/identities", "/unread"]) {
|
|
174
|
-
const capability = await fetchImpl(`${baseUrl}${path}`, { headers, signal: AbortSignal.timeout(2e3) });
|
|
175
|
-
if (capability.status === 401 || capability.status === 403) {
|
|
176
|
-
throw new Error(`ours daemon authentication failed at ${baseUrl}; supply the matching coherent selection`);
|
|
177
|
-
}
|
|
178
|
-
if (!capability.ok) throw new Error(`selected daemon lacks ${path} (HTTP ${capability.status})`);
|
|
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();
|
|
179
50
|
}
|
|
180
|
-
return {
|
|
181
|
-
port: selection.port.value,
|
|
182
|
-
stateDir: selection.expectStateDir,
|
|
183
|
-
token,
|
|
184
|
-
visibility: "owner",
|
|
185
|
-
source: selection.baseUrl.source,
|
|
186
|
-
info,
|
|
187
|
-
baseUrl,
|
|
188
|
-
configPath: selection.configPath?.value ?? join(selection.expectStateDir, "config.json"),
|
|
189
|
-
codexArgs: parsed.codexArgs
|
|
190
|
-
};
|
|
51
|
+
return { profile: hostProfile, info, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
|
|
191
52
|
}
|
|
192
53
|
export {
|
|
193
54
|
parseOursArgs,
|
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.2.0-nightly.
|
|
3
|
+
"version": "1.2.0-nightly.8",
|
|
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/sdk": "3.8.1-nightly.
|
|
49
|
+
"@ours.network/sdk": "3.8.1-nightly.13",
|
|
50
50
|
"ws": "^8.21.0",
|
|
51
51
|
"zod": "^3.25.76",
|
|
52
|
-
"@ours.network/mcp": "1.2.0-nightly.
|
|
52
|
+
"@ours.network/mcp": "1.2.0-nightly.8"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"esbuild": "^0.24.2"
|
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
|
|
|
@@ -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
|
@@ -35,16 +35,6 @@ const safeUnread = (value) => (Array.isArray(value?.identities) ? value.identiti
|
|
|
35
35
|
return [{ name: entry.name, count, files, recent }];
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
async function applicationIdentityNames(env, selection) {
|
|
39
|
-
const path = env.OURS_MCP_CONFIG || join(homedir(), '.ours-mcp', 'config.json');
|
|
40
|
-
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
41
|
-
if (value?.version !== 1 || !value.daemons || typeof value.daemons !== 'object') return new Set();
|
|
42
|
-
const identities = selection.instanceId
|
|
43
|
-
? value.instances?.[selection.instanceId]?.identities
|
|
44
|
-
: value.daemons[resolve(selection.stateDir)]?.identities;
|
|
45
|
-
return new Set(Array.isArray(identities) ? identities.filter((name) => typeof name === 'string') : []);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
38
|
function renderContext(unread, pin) {
|
|
49
39
|
const lines = [];
|
|
50
40
|
if (unread.length) {
|
|
@@ -78,9 +68,6 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
|
|
|
78
68
|
if (event === 'SessionStart' && socket && capability && payload.session_id && payload.cwd) {
|
|
79
69
|
await send(socket, capability, { command: 'register_session', sessionId: payload.session_id, threadId: payload.session_id, cwd: payload.cwd });
|
|
80
70
|
}
|
|
81
|
-
let port = env.OURS_PORT || '3050';
|
|
82
|
-
let selectedStateDir = resolve(env.OURS_STATE_DIR || join(homedir(), '.ours'));
|
|
83
|
-
let selectedToken = env.OURS_API_TOKEN || '';
|
|
84
71
|
let hostProfile;
|
|
85
72
|
try { hostProfile = hostProfileFromEnv(env); }
|
|
86
73
|
catch { return { continue: true }; }
|
|
@@ -94,41 +81,7 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
|
|
|
94
81
|
return context ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } } : { continue: true };
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
|
|
98
|
-
// Resolve the same coherent SDK selection here so SessionStart and
|
|
99
|
-
// UserPromptSubmit inspect the same daemon as the stdio proxy.
|
|
100
|
-
try {
|
|
101
|
-
const selected = await profileResolver({ env, fetch: fetchImpl });
|
|
102
|
-
if (selected.profile) {
|
|
103
|
-
let client;
|
|
104
|
-
try {
|
|
105
|
-
client = await clientFactory({ ...selected.profile, sessionMode: 'external', leaseToken: `codex-hook-${process.pid}`, env: {} });
|
|
106
|
-
const visible = await applicationIdentityNames(env, { instanceId: selected.profile.expectedInstanceId });
|
|
107
|
-
const unread = safeUnread(await client.unread()).filter((entry) => visible.has(entry.name));
|
|
108
|
-
const pin = await findPin(payload.cwd || process.cwd());
|
|
109
|
-
const context = renderContext(unread, pin);
|
|
110
|
-
return context ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } } : { continue: true };
|
|
111
|
-
} finally { await client?.close(); }
|
|
112
|
-
}
|
|
113
|
-
port = String(selected.port);
|
|
114
|
-
selectedToken = selected.token || '';
|
|
115
|
-
selectedStateDir = resolve(selected.stateDir);
|
|
116
|
-
} catch {
|
|
117
|
-
/* proxy/launcher owns legacy diagnostics; hooks emit a benign no-op */
|
|
118
|
-
}
|
|
119
|
-
const headers = selectedToken ? { 'x-ours-api-token': selectedToken } : {};
|
|
120
|
-
let unread = [];
|
|
121
|
-
try {
|
|
122
|
-
const response = await fetchImpl(`http://127.0.0.1:${port}/unread`, { headers, signal: AbortSignal.timeout(1500) });
|
|
123
|
-
if (response.ok) {
|
|
124
|
-
const visible = await applicationIdentityNames(env, { stateDir: selectedStateDir });
|
|
125
|
-
unread = safeUnread(await response.json()).filter((entry) => visible.has(entry.name));
|
|
126
|
-
}
|
|
127
|
-
} catch { /* daemon diagnostics belong to launcher/proxy */ }
|
|
128
|
-
const pin = await findPin(payload.cwd || process.cwd());
|
|
129
|
-
const context = renderContext(unread, pin);
|
|
130
|
-
if (!context) return { continue: true };
|
|
131
|
-
return { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } };
|
|
84
|
+
return { continue: true };
|
|
132
85
|
} catch { return { continue: true }; }
|
|
133
86
|
}
|
|
134
87
|
|
package/src/launcher.mjs
CHANGED
|
@@ -31,15 +31,7 @@ export function launcherEnvironment(env, profile, control) {
|
|
|
31
31
|
// call. The bin shim preserves an explicit value instead of replacing it
|
|
32
32
|
// with whichever child happened to spawn it.
|
|
33
33
|
};
|
|
34
|
-
|
|
35
|
-
for (const key of ['OURS_PORT', 'OURS_API_TOKEN', 'OURS_STATE_DIR', 'OURS_CLIENT_PID', 'OURS_AUTOSTART', 'OURS_DAEMON_ID']) delete out[key];
|
|
36
|
-
} else {
|
|
37
|
-
out.OURS_PORT = String(profile.port);
|
|
38
|
-
out.OURS_AUTOSTART = '0';
|
|
39
|
-
out.OURS_CLIENT_PID = String(process.pid);
|
|
40
|
-
if (profile.token) out.OURS_API_TOKEN = profile.token;
|
|
41
|
-
else delete out.OURS_API_TOKEN;
|
|
42
|
-
}
|
|
34
|
+
for (const key of ['OURS_PORT', 'OURS_API_TOKEN', 'OURS_STATE_DIR', 'OURS_CLIENT_PID', 'OURS_AUTOSTART', 'OURS_DAEMON_ID', 'OURS_DAEMON_URL', 'OURS_DAEMON_CREDENTIAL_PATH']) delete out[key];
|
|
43
35
|
return out;
|
|
44
36
|
}
|
|
45
37
|
|
package/src/profile.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
1
|
import { randomUUID } from 'node:crypto';
|
|
4
2
|
|
|
5
|
-
import {
|
|
3
|
+
import { attachOursClient } from '@ours.network/sdk';
|
|
6
4
|
import { hostProfileSelectionFromEnv } from '../../core/src/host-client/index.ts';
|
|
7
5
|
|
|
8
6
|
function validPort(value) {
|
|
@@ -31,7 +29,6 @@ export function parseOursArgs(argv = []) {
|
|
|
31
29
|
export async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch, attach = attachOursClient } = {}) {
|
|
32
30
|
const parsed = parseOursArgs(argv);
|
|
33
31
|
const hostSelection = hostProfileSelectionFromEnv(env);
|
|
34
|
-
if (hostSelection !== null) {
|
|
35
32
|
const { profile: hostProfile, configPath } = hostSelection;
|
|
36
33
|
if (parsed.port != null) throw new Error('ours: --ours-port conflicts with host-profile mode');
|
|
37
34
|
const client = await attach({ ...hostProfile, sessionMode: 'external', leaseToken: randomUUID(), env: {} });
|
|
@@ -42,40 +39,4 @@ export async function resolveDaemonProfile({ argv = [], env = process.env, fetch
|
|
|
42
39
|
await client.unread();
|
|
43
40
|
} finally { await client.close(); }
|
|
44
41
|
return { profile: hostProfile, info, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
|
|
45
|
-
}
|
|
46
|
-
const selection = resolveDaemonConfig({
|
|
47
|
-
...(parsed.port == null ? {} : { port: parsed.port }),
|
|
48
|
-
env,
|
|
49
|
-
homeDir: env.HOME || homedir(),
|
|
50
|
-
});
|
|
51
|
-
await assertDaemonStateDir(selection, { fetch: fetchImpl, timeoutMs: 2000 });
|
|
52
|
-
|
|
53
|
-
const baseUrl = selection.baseUrl.value;
|
|
54
|
-
const token = selection.token?.value ?? null;
|
|
55
|
-
const headers = token ? { 'x-ours-api-token': token } : {};
|
|
56
|
-
const response = await fetchImpl(`${baseUrl}/info`, { signal: AbortSignal.timeout(2000) });
|
|
57
|
-
if (!response.ok) throw new Error(`ours daemon at ${baseUrl} returned HTTP ${response.status}`);
|
|
58
|
-
const info = await response.json();
|
|
59
|
-
if (info?.name !== 'ours' || !Number.isInteger(info?.protocol) || info.protocol < 1) {
|
|
60
|
-
throw new Error(`incompatible service at ${baseUrl}; expected an ours daemon with notification protocol 1`);
|
|
61
|
-
}
|
|
62
|
-
for (const path of ['/identities', '/unread']) {
|
|
63
|
-
const capability = await fetchImpl(`${baseUrl}${path}`, { headers, signal: AbortSignal.timeout(2000) });
|
|
64
|
-
if (capability.status === 401 || capability.status === 403) {
|
|
65
|
-
throw new Error(`ours daemon authentication failed at ${baseUrl}; supply the matching coherent selection`);
|
|
66
|
-
}
|
|
67
|
-
if (!capability.ok) throw new Error(`selected daemon lacks ${path} (HTTP ${capability.status})`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
port: selection.port.value,
|
|
72
|
-
stateDir: selection.expectStateDir,
|
|
73
|
-
token,
|
|
74
|
-
visibility: 'owner',
|
|
75
|
-
source: selection.baseUrl.source,
|
|
76
|
-
info,
|
|
77
|
-
baseUrl,
|
|
78
|
-
configPath: selection.configPath?.value ?? join(selection.expectStateDir, 'config.json'),
|
|
79
|
-
codexArgs: parsed.codexArgs,
|
|
80
|
-
};
|
|
81
42
|
}
|