@ours.network/codex 0.17.1 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/README.md +3 -0
- package/bin/proxy.mjs +6 -4
- package/hooks/hooks.json +13 -1
- package/package.json +2 -2
- package/skills/ours/SKILL.md +9 -7
- package/src/hooks/runner.mjs +1 -2
- package/src/launcher.mjs +20 -1
package/.mcp.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"command": "node",
|
|
5
5
|
"args": ["bin/proxy.mjs"],
|
|
6
6
|
"cwd": ".",
|
|
7
|
-
"env_vars": ["OURS_PORT", "OURS_CONFIG", "OURS_API_TOKEN", "OURS_STATE_DIR", "OURS_AUTOSTART", "OURS_CODEX_LIVE"]
|
|
7
|
+
"env_vars": ["OURS_PORT", "OURS_CONFIG", "OURS_API_TOKEN", "OURS_STATE_DIR", "OURS_AUTOSTART", "OURS_CODEX_LIVE", "OURS_CLIENT_PID"]
|
|
8
8
|
},
|
|
9
9
|
"ours_monitor": {
|
|
10
10
|
"command": "node",
|
package/README.md
CHANGED
|
@@ -77,6 +77,9 @@ lifecycle directly, while the hooks add standard-mode context and defensive stat
|
|
|
77
77
|
- `SessionStart` surfaces body-free unread metadata and an advisory `.ours-identity` pin.
|
|
78
78
|
- `UserPromptSubmit` can re-surface unresolved unread/pin context.
|
|
79
79
|
- `PostToolUse` records successful identity bindings and disarms on a switch.
|
|
80
|
+
- `SessionEnd` releases the session lease and waits for all session-owned temporary
|
|
81
|
+
roles to send best-effort removal notices and delete local state. Live mode also
|
|
82
|
+
invokes this cleanup directly when its TUI exits, so it does not depend on hook trust.
|
|
80
83
|
|
|
81
84
|
Codex requires review and trust of the exact hook definitions before running them.
|
|
82
85
|
Installation does not bypass hook trust, and live monitoring remains available when the
|
package/bin/proxy.mjs
CHANGED
|
@@ -22,10 +22,12 @@ if (!cliPath) {
|
|
|
22
22
|
}
|
|
23
23
|
const env = { ...process.env };
|
|
24
24
|
if (env.OURS_CODEX_LIVE === '1') env.OURS_AUTOSTART = '0';
|
|
25
|
-
if (process.ppid > 1) env.OURS_CLIENT_PID = String(process.ppid);
|
|
25
|
+
if (!env.OURS_CLIENT_PID && process.ppid > 1) env.OURS_CLIENT_PID = String(process.ppid);
|
|
26
|
+
const sessionEnd = process.argv[2] === 'session-end';
|
|
27
|
+
const command = sessionEnd ? 'session-end' : 'proxy';
|
|
28
|
+
const forwarded = process.argv.slice(sessionEnd ? 3 : 2);
|
|
26
29
|
const child = cliPath
|
|
27
|
-
? spawn(process.execPath, [cliPath,
|
|
28
|
-
: spawn('ours-mcp', [
|
|
30
|
+
? spawn(process.execPath, [cliPath, command, ...forwarded], { stdio: 'inherit', env })
|
|
31
|
+
: spawn('ours-mcp', [command, ...forwarded], { stdio: 'inherit', env });
|
|
29
32
|
child.on('error', (error) => { process.stderr.write(`ours: cannot launch @ours.network/mcp proxy: ${error.message}\n`); process.exit(1); });
|
|
30
33
|
child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 0); });
|
|
31
|
-
|
package/hooks/hooks.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"PostToolUse": [
|
|
28
28
|
{
|
|
29
|
-
"matcher": "choose_identity|create_identity|create_root_identity",
|
|
29
|
+
"matcher": "choose_identity|create_identity|create_root_identity|create_temporary_identity",
|
|
30
30
|
"hooks": [
|
|
31
31
|
{
|
|
32
32
|
"type": "command",
|
|
@@ -35,6 +35,18 @@
|
|
|
35
35
|
}
|
|
36
36
|
]
|
|
37
37
|
}
|
|
38
|
+
],
|
|
39
|
+
"SessionEnd": [
|
|
40
|
+
{
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "exec node \"${PLUGIN_ROOT}/bin/proxy.mjs\" session-end",
|
|
45
|
+
"timeout": 30,
|
|
46
|
+
"statusMessage": "Closing temporary ours identities"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
38
50
|
]
|
|
39
51
|
}
|
|
40
52
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/codex",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
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,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
-
"@ours.network/mcp": "0.17.
|
|
49
|
+
"@ours.network/mcp": "0.17.2",
|
|
50
50
|
"ws": "^8.21.0",
|
|
51
51
|
"zod": "^3.25.76"
|
|
52
52
|
},
|
package/skills/ours/SKILL.md
CHANGED
|
@@ -63,15 +63,15 @@ Walk the user through it, explaining as you go:
|
|
|
63
63
|
every one is automatically associated with the Human identity, and its invites carry
|
|
64
64
|
the verified "agent X of person Y" chain.
|
|
65
65
|
|
|
66
|
-
**Do not create an agent identity on a host with no Human identity.**
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
**Do not create an agent identity on a host with no Human identity.** Older servers may
|
|
67
|
+
accept that as unassociated legacy state: no verified human behind it in invites, no
|
|
68
|
+
control plane. This skill never creates or exposes such state.
|
|
69
69
|
|
|
70
70
|
| Tempting shortcut | Why it's wrong |
|
|
71
71
|
|---|---|
|
|
72
72
|
| "The user clearly asked for an *agent*, so the human question doesn't apply" | The gate is about ORDER, not classification. Create the Human identity first, then the agent they asked for. |
|
|
73
73
|
| "The user is busy / gave me everything I need for the agent" | Onboarding adds one question — the person's name. Ask it. |
|
|
74
|
-
| "`create_identity` works fine without a Human identity" |
|
|
74
|
+
| "`create_identity` works fine without a Human identity" | Older servers may create unassociated legacy state with no human association. Never do it. |
|
|
75
75
|
| "I'll create the agent now and the Human identity later" | Later never comes, and the agent's invites go out with no human chain. Human first. |
|
|
76
76
|
|
|
77
77
|
## Setup — "set up ours" / "set up the plugin"
|
|
@@ -214,7 +214,8 @@ the bio, so a persona prompt is only needed if they want to role-play it).
|
|
|
214
214
|
|
|
215
215
|
For scratch/one-off work ("make a temporary identity", "throwaway identity"):
|
|
216
216
|
`create_temporary_identity({ name? , bio?, expose_local? })` — name optional (omitted → a
|
|
217
|
-
random public-safe `tmp-…` name),
|
|
217
|
+
random public-safe `tmp-…` name), requires an existing Human identity, creates an
|
|
218
|
+
ephemeral delegated role under it, binds it to this session, and marks it **temporary**:
|
|
218
219
|
|
|
219
220
|
- **Session-scoped local lifetime.** When this session ends — an explicit
|
|
220
221
|
`close_temporary_identity()`, releasing the connection, or the client process dying —
|
|
@@ -226,8 +227,9 @@ random public-safe `tmp-…` name), binds it to this session, and marks it **tem
|
|
|
226
227
|
owning session lives — not even with `force`. A **stale** one (owner process dead) is
|
|
227
228
|
reclaimed automatically by the daemon, or immediately via
|
|
228
229
|
`close_temporary_identity({ name })` from any session.
|
|
229
|
-
-
|
|
230
|
-
|
|
230
|
+
- Its verified Human/root delegation is part of creation; the tool fails closed rather
|
|
231
|
+
than creating unassociated state. It is NOT in the local contact book unless
|
|
232
|
+
`expose_local: true`.
|
|
231
233
|
- `list_identities()` tags each temporary identity with its lease state (owned by this
|
|
232
234
|
session / another live session / stale / closing).
|
|
233
235
|
|
package/src/hooks/runner.mjs
CHANGED
|
@@ -52,7 +52,7 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
|
|
|
52
52
|
if (event === 'SessionStart' && payload.source === 'compact') return { continue: true };
|
|
53
53
|
if (event === 'PostToolUse') {
|
|
54
54
|
if (!socket || !capability || payload.tool_response?.isError) return { continue: true };
|
|
55
|
-
if (!/(choose_identity|create_identity|create_root_identity)$/.test(payload.tool_name || '')) return { continue: true };
|
|
55
|
+
if (!/(choose_identity|create_identity|create_root_identity|create_temporary_identity)$/.test(payload.tool_name || '')) return { continue: true };
|
|
56
56
|
const identity = payload.tool_input?.name;
|
|
57
57
|
if (typeof identity !== 'string' || !identity.trim()) return { continue: true };
|
|
58
58
|
await send(socket, capability, { command: 'binding_changed', identity });
|
|
@@ -83,4 +83,3 @@ async function main() {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) main();
|
|
86
|
-
|
package/src/launcher.mjs
CHANGED
|
@@ -2,7 +2,8 @@ import { spawn as nodeSpawn } from 'node:child_process';
|
|
|
2
2
|
import { createServer } from 'node:net';
|
|
3
3
|
import { chmod, mkdtemp, rm } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { randomBytes } from 'node:crypto';
|
|
7
8
|
import { once } from 'node:events';
|
|
8
9
|
import { resolveDaemonProfile } from './profile.mjs';
|
|
@@ -28,6 +29,10 @@ export function launcherEnvironment(env, profile, control) {
|
|
|
28
29
|
OURS_CODEX_CONTROL_SOCKET: control.socketPath,
|
|
29
30
|
OURS_CODEX_CAPABILITY: control.capability,
|
|
30
31
|
OURS_CODEX_LIVE: '1',
|
|
32
|
+
// One stable owner pid for the MCP proxy, hooks, and launcher's own cleanup
|
|
33
|
+
// call. The bin shim preserves an explicit value instead of replacing it
|
|
34
|
+
// with whichever child happened to spawn it.
|
|
35
|
+
OURS_CLIENT_PID: String(process.pid),
|
|
31
36
|
};
|
|
32
37
|
if (profile.token) out.OURS_API_TOKEN = profile.token;
|
|
33
38
|
else delete out.OURS_API_TOKEN;
|
|
@@ -89,10 +94,24 @@ export async function runLauncher({
|
|
|
89
94
|
let client;
|
|
90
95
|
let control;
|
|
91
96
|
let watcher;
|
|
97
|
+
const sessionEndShim = join(dirname(dirname(fileURLToPath(import.meta.url))), 'bin', 'proxy.mjs');
|
|
92
98
|
const cleanup = async () => {
|
|
93
99
|
watcher?.stop();
|
|
94
100
|
await control?.close().catch(() => {});
|
|
95
101
|
client?.close();
|
|
102
|
+
// The TUI exit is an authoritative normal session end. Run the same lease
|
|
103
|
+
// DELETE seam as the native hook while the shared daemon is still reachable;
|
|
104
|
+
// duplicate hook+launcher cleanup is intentionally idempotent.
|
|
105
|
+
const end = spawn(process.execPath, [sessionEndShim, 'session-end'], {
|
|
106
|
+
env: processEnvs.appServer,
|
|
107
|
+
stdio: 'ignore',
|
|
108
|
+
});
|
|
109
|
+
end.once('error', () => {});
|
|
110
|
+
await Promise.race([
|
|
111
|
+
waitExit(end),
|
|
112
|
+
new Promise((resolve) => setTimeout(resolve, 30_000)),
|
|
113
|
+
]).catch(() => {});
|
|
114
|
+
if (end.exitCode == null) end.kill('SIGTERM');
|
|
96
115
|
if (tui && tui.exitCode == null) tui.kill('SIGTERM');
|
|
97
116
|
if (appServer && appServer.exitCode == null) appServer.kill('SIGTERM');
|
|
98
117
|
await rm(runtimeDir, { recursive: true, force: true });
|