Instantly share code, notes, and snippets.
Created
July 28, 2026 20:22
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save arikon/48c54cfb8cf1ee79378c8d1d5e9203a9 to your computer and use it in GitHub Desktop.
Experimental scoped native Ultragoal executor assignments on oh-my-codex dev@73cb50c1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| From 38af2a78599ffd08fe7b4c771b206dcfb60789b0 Mon Sep 17 00:00:00 2001 | |
| From: Sergey Belov <peimei@ya.ru> | |
| Date: Mon, 27 Jul 2026 22:31:35 +0200 | |
| Subject: [PATCH] fix(hook): authorize scoped native Ultragoal executors | |
| --- | |
| docs/native-subagent-assignments.md | 95 ++++ | |
| plugins/oh-my-codex/skills/ultragoal/SKILL.md | 1 + | |
| skills/ultragoal/SKILL.md | 1 + | |
| .../__tests__/codex-native-assignment.test.ts | 437 ++++++++++++++++++ | |
| ...3311-ultragoal-native-outside-tmux.test.ts | 114 ++++- | |
| src/scripts/codex-native-hook.ts | 408 +++++++++++++++- | |
| 6 files changed, 1036 insertions(+), 20 deletions(-) | |
| create mode 100644 docs/native-subagent-assignments.md | |
| create mode 100644 src/scripts/__tests__/codex-native-assignment.test.ts | |
| diff --git a/docs/native-subagent-assignments.md b/docs/native-subagent-assignments.md | |
| new file mode 100644 | |
| index 00000000..3e8d8c65 | |
| --- /dev/null | |
| +++ b/docs/native-subagent-assignments.md | |
| @@ -0,0 +1,95 @@ | |
| +# Native subagent assignments | |
| + | |
| +The Codex native hook keeps typed native children read-only unless the active | |
| +root Conductor creates an exact, session-scoped assignment. This transport does | |
| +not grant Team-worker identity or authority. | |
| + | |
| +This transport is disabled by default. Enable it only with | |
| +`OMX_NATIVE_SUBAGENT_ASSIGNMENTS=on`. The opt-in explicitly selects a weaker | |
| +same-user safety model than ADR 3212: local assignment metadata is a bounded | |
| +workflow guardrail, not cryptographic authentication against a hostile process | |
| +running as the same OS user. Deployments that require ADR 3212's hostile-child | |
| +boundary must leave the option disabled until Codex exposes a documented, | |
| +unforgeable host-issued receipt. | |
| + | |
| +## Location | |
| + | |
| +For child `<child-agent-id>` in root session `<session-id>`: | |
| + | |
| +```text | |
| +.omx/state/sessions/<session-id>/native-assignments/<encodeURIComponent(child-agent-id)>.json | |
| +``` | |
| + | |
| +The root Conductor is the only actor allowed to create or replace this metadata. | |
| +A native child cannot mutate any `native-assignments` path, even when its | |
| +ordinary path scope would otherwise match. | |
| + | |
| +On Codex native surfaces outside tmux, a fresh Ultragoal activation is allowed | |
| +only when the hook event positively advertises a typed native spawn tool. The | |
| +root may then spawn an exact typed child and write this assignment after the | |
| +spawn returns the canonical child id. Until the assignment validates, the child | |
| +remains read-only and receives `OWNER_CONFIRMATION_REQUIRED` for mutations. | |
| + | |
| +## Schema | |
| + | |
| +The JSON object must contain exactly these keys: | |
| + | |
| +```json | |
| +{ | |
| + "schema_version": 1, | |
| + "kind": "native-subagent-assignment", | |
| + "lifecycle": "active", | |
| + "root_session_id": "exact-root-session-id", | |
| + "root_thread_id": "exact-root-thread-id", | |
| + "child_agent_id": "exact-canonical-child-agent-id", | |
| + "agent_type": "executor", | |
| + "allowed_actions": ["path-write", "bash-write"], | |
| + "path_prefixes": ["tmp/native-probe"], | |
| + "issued_at": "2026-07-23T20:00:00.000Z", | |
| + "expires_at": "2026-07-23T20:10:00.000Z" | |
| +} | |
| +``` | |
| + | |
| +The hook checks the root session and leader thread against the active session | |
| +and native subagent tracker. The first version accepts only a direct, available | |
| +`executor` child. It must be tracked as `kind: "subagent"` and provide one | |
| +unambiguous top-level `executor` role through the native | |
| +`agent_role`/`agent_type` field aliases. Real native tracker entries may omit | |
| +`mode`; when tracker `mode`, parent, root, or status are present, they must | |
| +match the live leader and available child. Nested-only role evidence or | |
| +conflicting top-level aliases cannot authorize an assignment. The filename | |
| +must encode that same canonical child ID. | |
| + | |
| +Assignments are fail-closed when unreadable, malformed, expired, issued too far | |
| +in the future, longer than 24 hours, symlinked, hard-linked, larger than 64 KiB, | |
| +or writable by group/other. Unknown keys, duplicate actions or prefixes, | |
| +ambiguous paths, symlink traversal, `.omx`, and `.git` scopes are rejected. | |
| +Deleting the file or changing `lifecycle` away from `active` revokes the grant. | |
| + | |
| +## Actions | |
| + | |
| +- `path-write`: direct file-edit tools within `path_prefixes`. | |
| +- `bash-write`: one statically analyzable shell mutation within | |
| + `path_prefixes`. | |
| + | |
| +Assignments never authorize VCS, PR, network, deployment, state, orchestration, | |
| +goal, assignment, or Team-worker operations. A `"."` prefix still excludes | |
| +`.git` and `.omx`. Shell substitution, | |
| +unresolved arithmetic, dynamic nested execution, unsafe heredocs, unsafe | |
| +nameref/allexport state, arbitrary PATH mutation, unresolved interpreter | |
| +writes, and path/symlink escapes remain blocked. | |
| + | |
| +Without a valid assignment, typed native children continue to receive | |
| +`OWNER_CONFIRMATION_REQUIRED`. | |
| + | |
| +## Root orchestration transports | |
| + | |
| +An active, session-bound Main-root Conductor may write the existing bounded | |
| +metadata roots, including `.omx/state`, `.omx/ultragoal`, `.omx/ralph`, | |
| +`.omx/team`, `.omx/mailbox`, `.omx/handoff`, `.omx/handoffs`, `.omx/goals`, | |
| +`.omx/notepad`, `.omx/wiki`, and `.beads`. | |
| + | |
| +The hook structurally recognizes canonical `omx state read`, `omx state write`, | |
| +`omx state clear`, and `omx ultragoal checkpoint` commands before generic shell | |
| +mutation analysis. Exact session/cwd/mode binding and all existing injection, | |
| +PATH, symlink, and protected-state-gate checks still apply. | |
| diff --git a/plugins/oh-my-codex/skills/ultragoal/SKILL.md b/plugins/oh-my-codex/skills/ultragoal/SKILL.md | |
| index 3a22a0ed..57486b55 100644 | |
| --- a/plugins/oh-my-codex/skills/ultragoal/SKILL.md | |
| +++ b/plugins/oh-my-codex/skills/ultragoal/SKILL.md | |
| @@ -24,6 +24,7 @@ Ultragoal is both a tracked workflow skill and the Autopilot durable-implementat | |
| - **Standalone `$ultragoal` activation**: ensure `.omx/state[/sessions/<session>]/ultragoal-state.json` exists with `mode:"ultragoal"`, `active:true`, and a non-empty `current_phase` such as `planning` before or while goals are created. This state is a lightweight HUD/runtime declaration; `.omx/ultragoal/goals.json` and `ledger.jsonl` remain the durable goal source of truth. | |
| - **During execution**: update `current_phase` to the smallest accurate phase (`planning`, `executing`, `verifying`, `reviewing`, `checkpointing`, or `blocked`) when the visible workflow phase changes. | |
| +- **Codex App outside tmux**: experimental activation is available only when `OMX_NATIVE_SUBAGENT_ASSIGNMENTS=on` and the native hook positively advertises typed `spawn_agent` support. Spawn an exact typed executor, then have Main-root write a short-lived assignment under `.omx/state/sessions/<session>/native-assignments/<encodeURIComponent(child-id)>.json` following `docs/native-subagent-assignments.md`. Keep paths/actions minimal; the child remains read-only until the exact assignment validates. Leave the option disabled when ADR 3212's hostile same-user child boundary is required. | |
| - **Inside active Autopilot**: keep `mode:"autopilot"` active and set the supervised phase to `current_phase:"ultragoal"`; do not start a peer Autopilot replacement. Ultragoal's own mode state may still exist as child-phase detail, but Autopilot owns the parent phase. | |
| - **On handoff to code-review**: persist implementation/test/ledger evidence under Autopilot `handoff_artifacts.ultragoal`, then set Autopilot `current_phase:"code-review"`. | |
| - **On completion/blocker**: set standalone Ultragoal `active:false,current_phase:"complete"` only when all durable goals are complete; otherwise keep it active with a blocker/review-blocked phase and ledger evidence. | |
| diff --git a/skills/ultragoal/SKILL.md b/skills/ultragoal/SKILL.md | |
| index 3a22a0ed..57486b55 100644 | |
| --- a/skills/ultragoal/SKILL.md | |
| +++ b/skills/ultragoal/SKILL.md | |
| @@ -24,6 +24,7 @@ Ultragoal is both a tracked workflow skill and the Autopilot durable-implementat | |
| - **Standalone `$ultragoal` activation**: ensure `.omx/state[/sessions/<session>]/ultragoal-state.json` exists with `mode:"ultragoal"`, `active:true`, and a non-empty `current_phase` such as `planning` before or while goals are created. This state is a lightweight HUD/runtime declaration; `.omx/ultragoal/goals.json` and `ledger.jsonl` remain the durable goal source of truth. | |
| - **During execution**: update `current_phase` to the smallest accurate phase (`planning`, `executing`, `verifying`, `reviewing`, `checkpointing`, or `blocked`) when the visible workflow phase changes. | |
| +- **Codex App outside tmux**: experimental activation is available only when `OMX_NATIVE_SUBAGENT_ASSIGNMENTS=on` and the native hook positively advertises typed `spawn_agent` support. Spawn an exact typed executor, then have Main-root write a short-lived assignment under `.omx/state/sessions/<session>/native-assignments/<encodeURIComponent(child-id)>.json` following `docs/native-subagent-assignments.md`. Keep paths/actions minimal; the child remains read-only until the exact assignment validates. Leave the option disabled when ADR 3212's hostile same-user child boundary is required. | |
| - **Inside active Autopilot**: keep `mode:"autopilot"` active and set the supervised phase to `current_phase:"ultragoal"`; do not start a peer Autopilot replacement. Ultragoal's own mode state may still exist as child-phase detail, but Autopilot owns the parent phase. | |
| - **On handoff to code-review**: persist implementation/test/ledger evidence under Autopilot `handoff_artifacts.ultragoal`, then set Autopilot `current_phase:"code-review"`. | |
| - **On completion/blocker**: set standalone Ultragoal `active:false,current_phase:"complete"` only when all durable goals are complete; otherwise keep it active with a blocker/review-blocked phase and ledger evidence. | |
| diff --git a/src/scripts/__tests__/codex-native-assignment.test.ts b/src/scripts/__tests__/codex-native-assignment.test.ts | |
| new file mode 100644 | |
| index 00000000..1211d0cc | |
| --- /dev/null | |
| +++ b/src/scripts/__tests__/codex-native-assignment.test.ts | |
| @@ -0,0 +1,437 @@ | |
| +import assert from "node:assert/strict"; | |
| +import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; | |
| +import { tmpdir } from "node:os"; | |
| +import { dirname, join, resolve } from "node:path"; | |
| +import { afterEach, beforeEach, describe, it } from "node:test"; | |
| +import { dispatchCodexNativeHook } from "../codex-native-hook.js"; | |
| + | |
| +async function writeJson(path: string, value: unknown): Promise<void> { | |
| + await mkdir(dirname(path), { recursive: true }); | |
| + await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8"); | |
| +} | |
| + | |
| +async function createActiveConductorFixture(phase = "executing"): Promise<{ | |
| + cwd: string; | |
| + stateDir: string; | |
| + sessionId: string; | |
| + leaderThreadId: string; | |
| + childAgentId: string; | |
| +}> { | |
| + const cwd = await mkdtemp(join(tmpdir(), "omx-native-assignment-")); | |
| + const stateDir = join(cwd, ".omx", "state"); | |
| + const sessionId = "sess-native-assignment"; | |
| + const leaderThreadId = "thread-native-assignment-leader"; | |
| + const childAgentId = "agent-native-assignment-executor"; | |
| + const now = new Date().toISOString(); | |
| + | |
| + await writeJson(join(stateDir, "session.json"), { | |
| + session_id: sessionId, | |
| + native_session_id: leaderThreadId, | |
| + cwd, | |
| + }); | |
| + await writeJson(join(stateDir, "subagent-tracking.json"), { | |
| + schemaVersion: 1, | |
| + sessions: { | |
| + [sessionId]: { | |
| + session_id: sessionId, | |
| + leader_thread_id: leaderThreadId, | |
| + updated_at: now, | |
| + threads: { | |
| + [leaderThreadId]: { | |
| + thread_id: leaderThreadId, | |
| + kind: "leader", | |
| + first_seen_at: now, | |
| + last_seen_at: now, | |
| + turn_count: 1, | |
| + }, | |
| + [childAgentId]: { | |
| + thread_id: childAgentId, | |
| + kind: "subagent", | |
| + mode: "executor", | |
| + leader_thread_id: leaderThreadId, | |
| + first_seen_at: now, | |
| + last_seen_at: now, | |
| + turn_count: 1, | |
| + }, | |
| + }, | |
| + }, | |
| + }, | |
| + }); | |
| + await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), { | |
| + active: true, | |
| + skill: "ultragoal", | |
| + phase, | |
| + session_id: sessionId, | |
| + active_skills: [ | |
| + { skill: "ultragoal", phase, active: true, session_id: sessionId }, | |
| + ], | |
| + }); | |
| + await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), { | |
| + active: true, | |
| + mode: "ultragoal", | |
| + current_phase: phase, | |
| + session_id: sessionId, | |
| + }); | |
| + | |
| + return { cwd, stateDir, sessionId, leaderThreadId, childAgentId }; | |
| +} | |
| + | |
| +function preToolUse( | |
| + cwd: string, | |
| + sessionId: string, | |
| + identity: Record<string, unknown>, | |
| + toolName: string, | |
| + toolInput: Record<string, unknown>, | |
| +) { | |
| + return dispatchCodexNativeHook({ | |
| + hook_event_name: "PreToolUse", | |
| + cwd, | |
| + session_id: sessionId, | |
| + ...identity, | |
| + tool_name: toolName, | |
| + tool_input: toolInput, | |
| + }, { cwd }); | |
| +} | |
| + | |
| +describe("native Conductor assignment transport", () => { | |
| + const originalAssignmentOptIn = process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS; | |
| + | |
| + beforeEach(() => { | |
| + process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS = "on"; | |
| + }); | |
| + | |
| + afterEach(() => { | |
| + if (originalAssignmentOptIn === undefined) delete process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS; | |
| + else process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS = originalAssignmentOptIn; | |
| + }); | |
| + | |
| + it("accepts real typed-child provenance when the native tracker omits mode but rejects missing or conflicting role evidence", async () => { | |
| + const fixture = await createActiveConductorFixture(); | |
| + const { cwd, stateDir, sessionId, leaderThreadId, childAgentId } = fixture; | |
| + const assignmentPath = join( | |
| + stateDir, | |
| + "sessions", | |
| + sessionId, | |
| + "native-assignments", | |
| + `${encodeURIComponent(childAgentId)}.json`, | |
| + ); | |
| + const issuedAt = new Date(); | |
| + const expiresAt = new Date(issuedAt.getTime() + 10 * 60_000); | |
| + const trackingPath = join(stateDir, "subagent-tracking.json"); | |
| + const trackedChild = { | |
| + thread_id: childAgentId, | |
| + kind: "subagent", | |
| + leader_thread_id: leaderThreadId, | |
| + first_seen_at: issuedAt.toISOString(), | |
| + last_seen_at: issuedAt.toISOString(), | |
| + turn_count: 1, | |
| + }; | |
| + | |
| + try { | |
| + await writeJson(assignmentPath, { | |
| + schema_version: 1, | |
| + kind: "native-subagent-assignment", | |
| + lifecycle: "active", | |
| + root_session_id: sessionId, | |
| + root_thread_id: leaderThreadId, | |
| + child_agent_id: childAgentId, | |
| + agent_type: "executor", | |
| + allowed_actions: ["path-write"], | |
| + path_prefixes: ["src/assigned"], | |
| + issued_at: issuedAt.toISOString(), | |
| + expires_at: expiresAt.toISOString(), | |
| + }); | |
| + await writeJson(trackingPath, { | |
| + schemaVersion: 1, | |
| + sessions: { | |
| + [sessionId]: { | |
| + session_id: sessionId, | |
| + leader_thread_id: leaderThreadId, | |
| + updated_at: issuedAt.toISOString(), | |
| + threads: { | |
| + [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader" }, | |
| + [childAgentId]: trackedChild, | |
| + }, | |
| + }, | |
| + }, | |
| + }); | |
| + | |
| + const realTypedChild = await preToolUse( | |
| + cwd, | |
| + sessionId, | |
| + { agent_id: childAgentId, agent_type: "executor" }, | |
| + "Write", | |
| + { file_path: "src/assigned/real-typed-child.txt", content: "ok\n" }, | |
| + ); | |
| + assert.equal(realTypedChild.outputJson, null); | |
| + | |
| + const missingTypedRole = await preToolUse( | |
| + cwd, | |
| + sessionId, | |
| + { | |
| + agent_id: childAgentId, | |
| + source: { subagent: { thread_spawn: { agent_role: "executor" } } }, | |
| + }, | |
| + "Write", | |
| + { file_path: "src/assigned/missing-role.txt", content: "must-not-write\n" }, | |
| + ); | |
| + assert.equal(missingTypedRole.outputJson?.decision, "block"); | |
| + assert.match(String(missingTypedRole.outputJson?.reason ?? ""), /OWNER_CONFIRMATION_REQUIRED/); | |
| + | |
| + const conflictingTypedAliases = await preToolUse( | |
| + cwd, | |
| + sessionId, | |
| + { agent_id: childAgentId, agent_role: "executor", agent_type: "writer" }, | |
| + "Write", | |
| + { file_path: "src/assigned/conflicting-aliases.txt", content: "must-not-write\n" }, | |
| + ); | |
| + assert.equal(conflictingTypedAliases.outputJson?.decision, "block"); | |
| + assert.match(String(conflictingTypedAliases.outputJson?.reason ?? ""), /OWNER_CONFIRMATION_REQUIRED/); | |
| + | |
| + await writeJson(trackingPath, { | |
| + schemaVersion: 1, | |
| + sessions: { | |
| + [sessionId]: { | |
| + session_id: sessionId, | |
| + leader_thread_id: leaderThreadId, | |
| + updated_at: issuedAt.toISOString(), | |
| + threads: { | |
| + [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader" }, | |
| + [childAgentId]: { ...trackedChild, mode: "writer" }, | |
| + }, | |
| + }, | |
| + }, | |
| + }); | |
| + const conflictingTrackerRole = await preToolUse( | |
| + cwd, | |
| + sessionId, | |
| + { agent_id: childAgentId, agent_role: "executor" }, | |
| + "Write", | |
| + { file_path: "src/assigned/conflicting-role.txt", content: "must-not-write\n" }, | |
| + ); | |
| + assert.equal(conflictingTrackerRole.outputJson?.decision, "block"); | |
| + assert.match(String(conflictingTrackerRole.outputJson?.reason ?? ""), /OWNER_CONFIRMATION_REQUIRED/); | |
| + } finally { | |
| + await rm(cwd, { recursive: true, force: true }); | |
| + } | |
| + }); | |
| + | |
| + it("allows only an exact live typed-child path grant and keeps all negative contracts fail-closed", async () => { | |
| + const fixture = await createActiveConductorFixture(); | |
| + const { cwd, stateDir, sessionId, leaderThreadId, childAgentId } = fixture; | |
| + const assignmentPath = join( | |
| + stateDir, | |
| + "sessions", | |
| + sessionId, | |
| + "native-assignments", | |
| + `${encodeURIComponent(childAgentId)}.json`, | |
| + ); | |
| + const issuedAt = new Date(); | |
| + const expiresAt = new Date(issuedAt.getTime() + 10 * 60_000); | |
| + | |
| + try { | |
| + await writeJson(assignmentPath, { | |
| + schema_version: 1, | |
| + kind: "native-subagent-assignment", | |
| + lifecycle: "active", | |
| + root_session_id: sessionId, | |
| + root_thread_id: leaderThreadId, | |
| + child_agent_id: childAgentId, | |
| + agent_type: "executor", | |
| + allowed_actions: ["path-write"], | |
| + path_prefixes: ["src/assigned"], | |
| + issued_at: issuedAt.toISOString(), | |
| + expires_at: expiresAt.toISOString(), | |
| + }); | |
| + | |
| + const exactChild = { agent_id: childAgentId, agent_role: "executor" }; | |
| + const allowed = await preToolUse( | |
| + cwd, | |
| + sessionId, | |
| + exactChild, | |
| + "Write", | |
| + { file_path: "src/assigned/result.txt", content: "ok\n" }, | |
| + ); | |
| + assert.equal(allowed.outputJson, null); | |
| + | |
| + for (const [name, identity, filePath] of [ | |
| + ["outside-scope", exactChild, "src/outside.txt"], | |
| + ["assignment-self-mutation", exactChild, assignmentPath], | |
| + ["foreign-child", { agent_id: "agent-native-assignment-foreign", agent_role: "executor" }, "src/assigned/result.txt"], | |
| + ["role-mismatch", { agent_id: childAgentId, agent_role: "writer" }, "src/assigned/result.txt"], | |
| + ["ungranted-writer", { agent_id: "agent-native-assignment-writer", agent_role: "writer" }, "src/assigned/result.txt"], | |
| + ] as const) { | |
| + const denied = await preToolUse( | |
| + cwd, | |
| + sessionId, | |
| + identity, | |
| + "Write", | |
| + { file_path: filePath, content: `${name}\n` }, | |
| + ); | |
| + assert.equal(denied.outputJson?.decision, "block", name); | |
| + assert.match(String(denied.outputJson?.reason ?? ""), /OWNER_CONFIRMATION_REQUIRED/, name); | |
| + } | |
| + | |
| + await writeJson(assignmentPath, { | |
| + schema_version: 1, | |
| + kind: "native-subagent-assignment", | |
| + lifecycle: "active", | |
| + root_session_id: "foreign-session", | |
| + root_thread_id: leaderThreadId, | |
| + child_agent_id: childAgentId, | |
| + agent_type: "executor", | |
| + allowed_actions: ["path-write"], | |
| + path_prefixes: ["src/assigned"], | |
| + issued_at: issuedAt.toISOString(), | |
| + expires_at: expiresAt.toISOString(), | |
| + unexpected_scope_expansion: true, | |
| + }); | |
| + const malformed = await preToolUse( | |
| + cwd, | |
| + sessionId, | |
| + exactChild, | |
| + "Write", | |
| + { file_path: "src/assigned/malformed.txt", content: "malformed\n" }, | |
| + ); | |
| + assert.equal(malformed.outputJson?.decision, "block"); | |
| + assert.match(String(malformed.outputJson?.reason ?? ""), /OWNER_CONFIRMATION_REQUIRED/); | |
| + | |
| + await mkdir(join(cwd, "src", "assigned"), { recursive: true }); | |
| + const escapedTarget = await mkdtemp(join(tmpdir(), "omx-native-assignment-escape-")); | |
| + await symlink(escapedTarget, join(cwd, "src", "assigned", "link")); | |
| + await writeJson(assignmentPath, { | |
| + schema_version: 1, | |
| + kind: "native-subagent-assignment", | |
| + lifecycle: "active", | |
| + root_session_id: sessionId, | |
| + root_thread_id: leaderThreadId, | |
| + child_agent_id: childAgentId, | |
| + agent_type: "executor", | |
| + allowed_actions: ["path-write", "bash-write"], | |
| + path_prefixes: ["src/assigned"], | |
| + issued_at: issuedAt.toISOString(), | |
| + expires_at: expiresAt.toISOString(), | |
| + }); | |
| + const allowedBash = await preToolUse( | |
| + cwd, | |
| + sessionId, | |
| + exactChild, | |
| + "Bash", | |
| + { command: "printf ok > src/assigned/bash.txt" }, | |
| + ); | |
| + assert.equal(allowedBash.outputJson, null); | |
| + for (const [name, command] of [ | |
| + ["bash-outside-scope", "printf no > src/outside-bash.txt"], | |
| + ["bash-symlink-escape", "printf no > src/assigned/link/escaped.txt"], | |
| + ["bash-substitution-poison", "printf \"$(printf no)\" > src/assigned/poison.txt"], | |
| + ] as const) { | |
| + const denied = await preToolUse(cwd, sessionId, exactChild, "Bash", { command }); | |
| + assert.equal(denied.outputJson?.decision, "block", name); | |
| + assert.match(String(denied.outputJson?.reason ?? ""), /OWNER_CONFIRMATION_REQUIRED/, name); | |
| + } | |
| + await rm(escapedTarget, { recursive: true, force: true }); | |
| + | |
| + await writeJson(assignmentPath, { | |
| + schema_version: 1, | |
| + kind: "native-subagent-assignment", | |
| + lifecycle: "active", | |
| + root_session_id: sessionId, | |
| + root_thread_id: leaderThreadId, | |
| + child_agent_id: childAgentId, | |
| + agent_type: "executor", | |
| + allowed_actions: ["path-write"], | |
| + path_prefixes: ["src/assigned"], | |
| + issued_at: new Date(issuedAt.getTime() - 20 * 60_000).toISOString(), | |
| + expires_at: new Date(issuedAt.getTime() - 10 * 60_000).toISOString(), | |
| + }); | |
| + const stale = await preToolUse( | |
| + cwd, | |
| + sessionId, | |
| + exactChild, | |
| + "Write", | |
| + { file_path: "src/assigned/stale.txt", content: "stale\n" }, | |
| + ); | |
| + assert.equal(stale.outputJson?.decision, "block"); | |
| + assert.match(String(stale.outputJson?.reason ?? ""), /OWNER_CONFIRMATION_REQUIRED/); | |
| + | |
| + await writeJson(assignmentPath, { | |
| + schema_version: 1, | |
| + kind: "native-subagent-assignment", | |
| + lifecycle: "active", | |
| + root_session_id: sessionId, | |
| + root_thread_id: leaderThreadId, | |
| + child_agent_id: childAgentId, | |
| + agent_type: "executor", | |
| + allowed_actions: ["path-write", "bash-write"], | |
| + path_prefixes: ["."], | |
| + issued_at: issuedAt.toISOString(), | |
| + expires_at: expiresAt.toISOString(), | |
| + }); | |
| + for (const [name, toolName, toolInput] of [ | |
| + ["git-path", "Write", { file_path: ".git/config", content: "must-not-write\n" }], | |
| + ["omx-goal-path", "Write", { file_path: ".omx/ultragoal/goals.json", content: "{}\n" }], | |
| + ["git-bash", "Bash", { command: "printf no > .git/config" }], | |
| + ["omx-goal-bash", "Bash", { command: "printf no > .omx/ultragoal/goals.json" }], | |
| + ] as const) { | |
| + const denied = await preToolUse(cwd, sessionId, exactChild, toolName, toolInput); | |
| + assert.equal(denied.outputJson?.decision, "block", name); | |
| + assert.match(String(denied.outputJson?.reason ?? ""), /OWNER_CONFIRMATION_REQUIRED/, name); | |
| + } | |
| + | |
| + await writeJson(assignmentPath, { | |
| + schema_version: 1, | |
| + kind: "native-subagent-assignment", | |
| + lifecycle: "active", | |
| + root_session_id: sessionId, | |
| + root_thread_id: leaderThreadId, | |
| + child_agent_id: childAgentId, | |
| + agent_type: "executor", | |
| + allowed_actions: ["path-write"], | |
| + path_prefixes: ["src/assigned"], | |
| + issued_at: issuedAt.toISOString(), | |
| + expires_at: expiresAt.toISOString(), | |
| + }); | |
| + await chmod(assignmentPath, 0o666); | |
| + const writableByOthers = await preToolUse( | |
| + cwd, | |
| + sessionId, | |
| + exactChild, | |
| + "Write", | |
| + { file_path: "src/assigned/world-writable-grant.txt", content: "must-not-write\n" }, | |
| + ); | |
| + assert.equal(writableByOthers.outputJson?.decision, "block"); | |
| + assert.match(String(writableByOthers.outputJson?.reason ?? ""), /OWNER_CONFIRMATION_REQUIRED/); | |
| + } finally { | |
| + await rm(cwd, { recursive: true, force: true }); | |
| + } | |
| + }); | |
| + | |
| + it("allows only Main-root to issue an assignment in the exact active session scope", async () => { | |
| + const fixture = await createActiveConductorFixture(); | |
| + const { cwd, sessionId, leaderThreadId } = fixture; | |
| + | |
| + try { | |
| + const root = { agent_id: leaderThreadId, thread_id: leaderThreadId }; | |
| + | |
| + const rootAssignment = await preToolUse(cwd, sessionId, root, "Write", { | |
| + file_path: `.omx/state/sessions/${sessionId}/native-assignments/root-created.json`, | |
| + content: "{}\n", | |
| + }); | |
| + assert.equal(rootAssignment.outputJson, null, "root-assignment-create"); | |
| + const foreignSessionAssignment = await preToolUse(cwd, sessionId, root, "Write", { | |
| + file_path: ".omx/state/sessions/foreign-session/native-assignments/root-created.json", | |
| + content: "{}\n", | |
| + }); | |
| + assert.equal(foreignSessionAssignment.outputJson?.decision, "block", "foreign-session-assignment"); | |
| + const protectedGate = await preToolUse(cwd, sessionId, root, "Write", { | |
| + file_path: `.omx/state/sessions/${sessionId}/ultragoal-state.json`, | |
| + content: "{}\n", | |
| + }); | |
| + assert.equal(protectedGate.outputJson?.decision, "block", "protected-state-gate"); | |
| + | |
| + } finally { | |
| + await rm(cwd, { recursive: true, force: true }); | |
| + } | |
| + }); | |
| + | |
| +}); | |
| diff --git a/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts b/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts | |
| index bc832ed9..4b898ad5 100644 | |
| --- a/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts | |
| +++ b/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts | |
| @@ -9,10 +9,10 @@ import { dispatchCodexNativeHook } from "../codex-native-hook.js"; | |
| // Regression coverage for OMX #3311: on native Codex App / native-hook | |
| // surfaces outside tmux, standalone Ultragoal must not activate an | |
| // execution-blocking Main-root Conductor state when no authorized executor | |
| -// will ever be reachable (Team is tmux-only; native child/descendant | |
| -// provenance intentionally grants no write authority per #3127). The guard | |
| -// under test refuses the *activation* write itself; it must never affect | |
| -// already-active sessions, non-ultragoal modes, non-native launchers, or | |
| +// will ever be reachable. Team is tmux-only; native child/descendant provenance | |
| +// grants no write authority unless the root issues an exact scoped assignment. | |
| +// The guard under test refuses the *activation* write itself; it must never | |
| +// affect already-active sessions, non-ultragoal modes, non-native launchers, or | |
| // attached-tmux transport. | |
| async function writeJson(path: string, value: unknown): Promise<void> { | |
| @@ -53,10 +53,13 @@ async function writeLeaderSessionFixture( | |
| describe("issue-3311: standalone Ultragoal native-App outside-tmux activation guard", () => { | |
| const originalTmux = process.env.TMUX; | |
| + const originalAssignmentOptIn = process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS; | |
| afterEach(() => { | |
| if (originalTmux === undefined) delete process.env.TMUX; | |
| else process.env.TMUX = originalTmux; | |
| + if (originalAssignmentOptIn === undefined) delete process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS; | |
| + else process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS = originalAssignmentOptIn; | |
| }); | |
| it("blocks fresh standalone-ultragoal Conductor activation on native App outside tmux (Bash state write)", async () => { | |
| @@ -88,8 +91,70 @@ describe("issue-3311: standalone Ultragoal native-App outside-tmux activation gu | |
| (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } } | null) | |
| ?.hookSpecificOutput?.additionalContext || "", | |
| ); | |
| - assert.match(context, /native child\/descendant provenance does not grant write authority/); | |
| - assert.doesNotMatch(context, /write-assignment|assignment-backed|scoped native .*grant/i); | |
| + assert.match(context, /typed native spawn surface backed by scoped write assignments/); | |
| + } finally { | |
| + await rm(cwd, { recursive: true, force: true }); | |
| + } | |
| + }); | |
| + | |
| + it("allows fresh activation when the native event positively advertises typed spawn support", async () => { | |
| + delete process.env.TMUX; | |
| + process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS = "on"; | |
| + const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3319-supported-bash-")); | |
| + try { | |
| + const stateDir = join(cwd, ".omx", "state"); | |
| + const sessionId = "sess-3319-supported-bash"; | |
| + const leaderThreadId = "thread-3319-supported-bash"; | |
| + await writeLeaderSessionFixture(stateDir, sessionId, leaderThreadId, cwd); | |
| + | |
| + const result = await dispatchCodexNativeHook( | |
| + { | |
| + hook_event_name: "PreToolUse", | |
| + cwd, | |
| + session_id: sessionId, | |
| + thread_id: leaderThreadId, | |
| + agent_id: leaderThreadId, | |
| + source: "native", | |
| + available_tools: ["collaboration.spawn_agent"], | |
| + tool_name: "Bash", | |
| + tool_input: { command: `omx state write --input '${ACTIVATION_INPUT}' --json` }, | |
| + }, | |
| + { cwd }, | |
| + ); | |
| + | |
| + assert.equal(result.outputJson, null); | |
| + } finally { | |
| + await rm(cwd, { recursive: true, force: true }); | |
| + } | |
| + }); | |
| + | |
| + it("keeps fresh activation blocked when native subagents are advertised without typed role routing", async () => { | |
| + delete process.env.TMUX; | |
| + process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS = "on"; | |
| + const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3319-untyped-bash-")); | |
| + try { | |
| + const stateDir = join(cwd, ".omx", "state"); | |
| + const sessionId = "sess-3319-untyped-bash"; | |
| + const leaderThreadId = "thread-3319-untyped-bash"; | |
| + await writeLeaderSessionFixture(stateDir, sessionId, leaderThreadId, cwd); | |
| + | |
| + const result = await dispatchCodexNativeHook( | |
| + { | |
| + hook_event_name: "PreToolUse", | |
| + cwd, | |
| + session_id: sessionId, | |
| + thread_id: leaderThreadId, | |
| + agent_id: leaderThreadId, | |
| + source: "native", | |
| + capabilities: { native_subagents: true }, | |
| + tool_name: "Bash", | |
| + tool_input: { command: `omx state write --input '${ACTIVATION_INPUT}' --json` }, | |
| + }, | |
| + { cwd }, | |
| + ); | |
| + | |
| + assert.equal(result.outputJson?.decision, "block"); | |
| + assert.match(String(result.outputJson?.reason ?? ""), /OMX-ULTRAGOAL-NO-OWNER/); | |
| } finally { | |
| await rm(cwd, { recursive: true, force: true }); | |
| } | |
| @@ -124,8 +189,7 @@ describe("issue-3311: standalone Ultragoal native-App outside-tmux activation gu | |
| (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } } | null) | |
| ?.hookSpecificOutput?.additionalContext || "", | |
| ); | |
| - assert.match(context, /native child\/descendant provenance does not grant write authority/); | |
| - assert.doesNotMatch(context, /write-assignment|assignment-backed|scoped native .*grant/i); | |
| + assert.match(context, /typed native spawn surface backed by scoped write assignments/); | |
| } finally { | |
| await rm(cwd, { recursive: true, force: true }); | |
| } | |
| @@ -163,8 +227,7 @@ describe("issue-3311: standalone Ultragoal native-App outside-tmux activation gu | |
| (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } } | null) | |
| ?.hookSpecificOutput?.additionalContext || "", | |
| ); | |
| - assert.match(context, /native child\/descendant provenance does not grant write authority/); | |
| - assert.doesNotMatch(context, /write-assignment|assignment-backed|scoped native .*grant/i); | |
| + assert.match(context, /typed native spawn surface backed by scoped write assignments/); | |
| } finally { | |
| await rm(cwd, { recursive: true, force: true }); | |
| } | |
| @@ -427,6 +490,37 @@ describe("issue-3311: standalone Ultragoal native-App outside-tmux activation gu | |
| } | |
| }); | |
| + it("allows '$ultragoal' prompt activation when typed native spawn support is positively advertised", async () => { | |
| + delete process.env.TMUX; | |
| + process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS = "on"; | |
| + const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3319-supported-prompt-")); | |
| + try { | |
| + await mkdir(join(cwd, ".omx", "state"), { recursive: true }); | |
| + const result = await dispatchCodexNativeHook( | |
| + { | |
| + hook_event_name: "UserPromptSubmit", | |
| + cwd, | |
| + session_id: "sess-3319-supported-prompt", | |
| + thread_id: "thread-3319-supported-prompt", | |
| + turn_id: "turn-3319-supported-prompt", | |
| + source: "native", | |
| + available_tools: ["collaboration.spawn_agent"], | |
| + prompt: "$ultragoal split this launch into durable goals", | |
| + }, | |
| + { cwd }, | |
| + ); | |
| + | |
| + assert.equal(result.skillState?.initialized_mode, "ultragoal"); | |
| + assert.equal(result.skillState?.transition_error, undefined); | |
| + assert.equal( | |
| + existsSync(join(cwd, ".omx", "state", "sessions", "sess-3319-supported-prompt", "ultragoal-state.json")), | |
| + true, | |
| + ); | |
| + } finally { | |
| + await rm(cwd, { recursive: true, force: true }); | |
| + } | |
| + }); | |
| + | |
| it("allows the primary '$ultragoal' UserPromptSubmit activation when attached to tmux (Team is reachable)", async () => { | |
| process.env.TMUX = "/tmp/tmux-3311-prompt,12345,0"; | |
| const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3311-prompt-tmux-")); | |
| diff --git a/src/scripts/codex-native-hook.ts b/src/scripts/codex-native-hook.ts | |
| index 8c74a5d4..a6ad009d 100644 | |
| --- a/src/scripts/codex-native-hook.ts | |
| +++ b/src/scripts/codex-native-hook.ts | |
| @@ -2661,6 +2661,28 @@ function buildNativeOutsideTmuxTeamPromptBlockState( | |
| // of an already-active standalone Ultragoal session) are left untouched, so | |
| // resumption, prior-plan continuation, and non-standalone Ultragoal remain | |
| // exactly as before. | |
| +function nativeSubagentAssignmentsOptedIn(): boolean { | |
| + return safeString(process.env.OMX_NATIVE_SUBAGENT_ASSIGNMENTS).trim().toLowerCase() === "on"; | |
| +} | |
| + | |
| +function payloadAdvertisesTypedNativeSpawn(payload: CodexHookPayload): boolean { | |
| + const capabilities = safeObject(payload.omx_runtime_capabilities ?? payload.capabilities); | |
| + const nativeSubagents = capabilities?.native_subagents ?? capabilities?.nativeSubagents; | |
| + const multiAgent = capabilities?.multi_agent_v1 ?? capabilities?.multiAgentV1; | |
| + const roleRouting = capabilities?.role_routing ?? capabilities?.roleRouting; | |
| + if (roleRouting === true && (nativeSubagents === true || multiAgent === true)) return true; | |
| + | |
| + const availableTools = payload.available_tools ?? payload.availableTools ?? payload.tools; | |
| + if (!Array.isArray(availableTools)) return false; | |
| + return availableTools.some((tool) => { | |
| + const record = safeObject(tool); | |
| + const name = typeof tool === "string" | |
| + ? tool | |
| + : safeString(record?.name ?? record?.tool_name ?? record?.toolName).trim(); | |
| + return isNativeSubagentSpawnToolName(name); | |
| + }); | |
| +} | |
| + | |
| async function buildNativeOutsideTmuxUltragoalPromptBlockState( | |
| classification: KeywordInputClassification, | |
| cwd: string, | |
| @@ -2693,6 +2715,24 @@ async function buildNativeOutsideTmuxUltragoalPromptBlockState( | |
| : false; | |
| if (hasAnyActiveTrackedSkill) return null; | |
| + const nativeSupport = resolveNativeSubagentSupportStatus({ | |
| + payload, | |
| + persistedSupportBlocker: await readJsonIfExists(nativeSubagentSupportBlockerPath(stateDir)), | |
| + persistedRoleRoutingMarker: readRoleRoutingMarker(stateDir, { cwd, sessionId }), | |
| + persistedCapacityBlocker: await readJsonIfExists(nativeSubagentCapacityBlockerPath(stateDir)), | |
| + cwd, | |
| + sessionId, | |
| + }); | |
| + // A positively advertised typed spawn surface can bootstrap the scoped | |
| + // assignment transaction after activation: Main-root may spawn the child and | |
| + // write only its session-bound native-assignment metadata, while every child | |
| + // mutation remains denied until that exact assignment validates. | |
| + if ( | |
| + nativeSubagentAssignmentsOptedIn() | |
| + && nativeSupport.status === "supported" | |
| + && payloadAdvertisesTypedNativeSpawn(payload) | |
| + ) return null; | |
| + | |
| const nowIso = new Date().toISOString(); | |
| return { | |
| version: 1, | |
| @@ -4049,6 +4089,15 @@ function resolveConductorPolicyRoot(stateDir: string, fallbackCwd: string): Cond | |
| return { cwd: canonicalFallback, valid: !statePresent, statePresent, externalStateRoot: statePresent }; | |
| } | |
| +function readDirectPayloadAgentRole(payload: CodexHookPayload): string { | |
| + const roles = [...new Set([ | |
| + "agent_role", | |
| + "agentRole", | |
| + "agent_type", | |
| + "agentType", | |
| + ].map((key) => safeString(payload[key]).trim().toLowerCase()).filter(Boolean))]; | |
| + return roles.length === 1 ? roles[0] ?? "" : ""; | |
| +} | |
| function readPayloadAgentRole(payload: CodexHookPayload): string { | |
| const directRole = safeString( | |
| @@ -5487,7 +5536,6 @@ function classifyPreToolUseMutationTransport( | |
| return "goal-lifecycle"; | |
| } | |
| const canonicalToolName = canonicalizeNativeCollaborationToolName(toolName); | |
| - | |
| if ( | |
| CONDUCTOR_ORCHESTRATION_TOOL_NAMES.has(canonicalToolName) | |
| || canonicalToolName.startsWith("collaboration.") | |
| @@ -10528,15 +10576,26 @@ async function buildRalplanPreToolUseBoundaryOutput( | |
| }; | |
| } | |
| -function buildRawProtectedWorkflowStatePathOutput( | |
| +async function buildRawProtectedWorkflowStatePathOutput( | |
| payload: CodexHookPayload, | |
| cwd: string, | |
| stateDir: string, | |
| -): Record<string, unknown> | null { | |
| + authoritativeSessionId = "", | |
| + activeConductor = false, | |
| +): Promise<Record<string, unknown> | null> { | |
| const toolName = safeString(payload.tool_name).trim(); | |
| if (classifyPreToolUseMutationTransport(payload, toolName, cwd) !== "path") return null; | |
| const candidates = collectImplementationToolPathCandidates(payload, toolName, readPreToolUsePathCandidates(payload)); | |
| - const protectedPath = candidates.find((candidate) => isRawProtectedPlanningStateCandidate(stateDir, cwd, candidate)); | |
| + const protectedCandidates = candidates.filter((candidate) => isRawProtectedPlanningStateCandidate(stateDir, cwd, candidate)); | |
| + if ( | |
| + nativeSubagentAssignmentsOptedIn() | |
| + && activeConductor | |
| + && authoritativeSessionId | |
| + && protectedCandidates.length > 0 | |
| + && protectedCandidates.every((candidate) => isSessionBoundNativeAssignmentPath(cwd, candidate, authoritativeSessionId)) | |
| + && await resolvePreToolUseWriteActor(payload, cwd, stateDir, authoritativeSessionId) === "main-root" | |
| + ) return null; | |
| + const protectedPath = protectedCandidates[0]; | |
| if (protectedPath === undefined) return null; | |
| return { | |
| decision: "block", | |
| @@ -10799,6 +10858,212 @@ interface ActiveConductorState { | |
| type PreToolUseWriteActor = "main-root" | "native-child" | "provenance-conflict" | "team-worker"; | |
| +type NativeAssignmentAction = "path-write" | "bash-write"; | |
| + | |
| +interface NativeSubagentAssignment { | |
| + schema_version: 1; | |
| + kind: "native-subagent-assignment"; | |
| + lifecycle: "active"; | |
| + root_session_id: string; | |
| + root_thread_id: string; | |
| + child_agent_id: string; | |
| + agent_type: string; | |
| + allowed_actions: NativeAssignmentAction[]; | |
| + path_prefixes: string[]; | |
| + issued_at: string; | |
| + expires_at: string; | |
| +} | |
| + | |
| +const NATIVE_ASSIGNMENT_DIRECTORY = "native-assignments"; | |
| +const NATIVE_ASSIGNMENT_MAX_BYTES = 64 * 1024; | |
| +const NATIVE_ASSIGNMENT_MAX_LIFETIME_MS = 24 * 60 * 60 * 1000; | |
| +const NATIVE_ASSIGNMENT_ALLOWED_ACTIONS = new Set<NativeAssignmentAction>([ | |
| + "path-write", | |
| + "bash-write", | |
| +]); | |
| +const NATIVE_ASSIGNMENT_SCHEMA_KEYS = new Set([ | |
| + "schema_version", | |
| + "kind", | |
| + "lifecycle", | |
| + "root_session_id", | |
| + "root_thread_id", | |
| + "child_agent_id", | |
| + "agent_type", | |
| + "allowed_actions", | |
| + "path_prefixes", | |
| + "issued_at", | |
| + "expires_at", | |
| +]); | |
| + | |
| +function nativeAssignmentPath(stateDir: string, sessionId: string, childAgentId: string): string { | |
| + return join( | |
| + stateDir, | |
| + "sessions", | |
| + sessionId, | |
| + NATIVE_ASSIGNMENT_DIRECTORY, | |
| + `${encodeURIComponent(childAgentId)}.json`, | |
| + ); | |
| +} | |
| + | |
| +function isNativeAssignmentPath(stateDir: string, rawPath: string, executionCwd: string): boolean { | |
| + const absolute = isAbsolute(rawPath) ? resolve(rawPath) : resolve(executionCwd, rawPath); | |
| + const assignmentRoot = resolve(stateDir, "sessions"); | |
| + const relativePath = relative(assignmentRoot, absolute).replace(/\\/g, "/"); | |
| + return relativePath !== "" | |
| + && !relativePath.startsWith("../") | |
| + && /^.+\/native-assignments(?:\/|$)/.test(relativePath); | |
| +} | |
| + | |
| +function assignmentPathHasUnsafeFilesystemIdentity(stateDir: string, assignmentPath: string): boolean { | |
| + const stateRoot = resolve(stateDir); | |
| + const relativePath = relative(stateRoot, resolve(assignmentPath)).replace(/\\/g, "/"); | |
| + if (!relativePath || relativePath.startsWith("../") || relativePath === "..") return true; | |
| + let current = stateRoot; | |
| + for (const segment of relativePath.split("/").filter(Boolean)) { | |
| + current = join(current, segment); | |
| + try { | |
| + const metadata = lstatSync(current); | |
| + if (metadata.isSymbolicLink()) return true; | |
| + if (current === assignmentPath) { | |
| + return !metadata.isFile() | |
| + || metadata.nlink !== 1 | |
| + || metadata.size > NATIVE_ASSIGNMENT_MAX_BYTES | |
| + || (metadata.mode & 0o022) !== 0; | |
| + } | |
| + if (!metadata.isDirectory()) return true; | |
| + } catch { | |
| + return true; | |
| + } | |
| + } | |
| + return true; | |
| +} | |
| + | |
| +function normalizeNativeAssignmentPathPrefix(cwd: string, rawPrefix: string): string | null { | |
| + if (rawPrefix.trim() === ".") return "."; | |
| + const prefix = normalizePlanningArtifactRelativePath(cwd, rawPrefix); | |
| + if (!prefix || prefix.startsWith(".omx/") || prefix === ".omx" || prefix.startsWith(".git/") || prefix === ".git") { | |
| + return null; | |
| + } | |
| + if (conductorPathnameExpansionIsAmbiguous(prefix) || conductorPathTraversesLink(cwd, prefix)) return null; | |
| + return prefix; | |
| +} | |
| + | |
| +function parseNativeSubagentAssignment( | |
| + raw: Record<string, unknown> | null, | |
| + cwd: string, | |
| + sessionId: string, | |
| + leaderThreadId: string, | |
| + childAgentId: string, | |
| + agentType: string, | |
| + nowMs = Date.now(), | |
| +): NativeSubagentAssignment | null { | |
| + if (!raw || Object.keys(raw).some((key) => !NATIVE_ASSIGNMENT_SCHEMA_KEYS.has(key))) return null; | |
| + if ( | |
| + raw.schema_version !== 1 | |
| + || raw.kind !== "native-subagent-assignment" | |
| + || raw.lifecycle !== "active" | |
| + || safeString(raw.root_session_id).trim() !== sessionId | |
| + || safeString(raw.root_thread_id).trim() !== leaderThreadId | |
| + || safeString(raw.child_agent_id).trim() !== childAgentId | |
| + || safeString(raw.agent_type).trim().toLowerCase() !== "executor" | |
| + || agentType !== "executor" | |
| + ) return null; | |
| + | |
| + const issuedAt = Date.parse(safeString(raw.issued_at).trim()); | |
| + const expiresAt = Date.parse(safeString(raw.expires_at).trim()); | |
| + if ( | |
| + !Number.isFinite(issuedAt) | |
| + || !Number.isFinite(expiresAt) | |
| + || issuedAt > nowMs + 30_000 | |
| + || expiresAt <= nowMs | |
| + || expiresAt <= issuedAt | |
| + || expiresAt - issuedAt > NATIVE_ASSIGNMENT_MAX_LIFETIME_MS | |
| + ) return null; | |
| + | |
| + const allowedActions = Array.isArray(raw.allowed_actions) | |
| + ? raw.allowed_actions.map((value) => safeString(value).trim()) as NativeAssignmentAction[] | |
| + : []; | |
| + if ( | |
| + allowedActions.length === 0 | |
| + || allowedActions.length > NATIVE_ASSIGNMENT_ALLOWED_ACTIONS.size | |
| + || new Set(allowedActions).size !== allowedActions.length | |
| + || allowedActions.some((action) => !NATIVE_ASSIGNMENT_ALLOWED_ACTIONS.has(action)) | |
| + ) return null; | |
| + | |
| + const pathPrefixes = Array.isArray(raw.path_prefixes) | |
| + ? raw.path_prefixes.map((value) => normalizeNativeAssignmentPathPrefix(cwd, safeString(value))) | |
| + : []; | |
| + if ( | |
| + pathPrefixes.length === 0 | |
| + || pathPrefixes.length > 32 | |
| + || pathPrefixes.some((prefix) => prefix === null) | |
| + || new Set(pathPrefixes).size !== pathPrefixes.length | |
| + ) return null; | |
| + | |
| + return { | |
| + schema_version: 1, | |
| + kind: "native-subagent-assignment", | |
| + lifecycle: "active", | |
| + root_session_id: sessionId, | |
| + root_thread_id: leaderThreadId, | |
| + child_agent_id: childAgentId, | |
| + agent_type: agentType, | |
| + allowed_actions: allowedActions, | |
| + path_prefixes: pathPrefixes as string[], | |
| + issued_at: new Date(issuedAt).toISOString(), | |
| + expires_at: new Date(expiresAt).toISOString(), | |
| + }; | |
| +} | |
| + | |
| +async function readNativeSubagentAssignment( | |
| + payload: CodexHookPayload, | |
| + cwd: string, | |
| + stateDir: string, | |
| + sessionId: string, | |
| +): Promise<NativeSubagentAssignment | null> { | |
| + if (!nativeSubagentAssignmentsOptedIn()) return null; | |
| + const childAgentId = readPayloadAgentId(payload); | |
| + const agentType = readDirectPayloadAgentRole(payload); | |
| + if (!childAgentId || !agentType || resolveInstalledRoleName(agentType, undefined, cwd) === null) return null; | |
| + | |
| + const trackingState = await readSubagentTrackingState(cwd).catch(() => null); | |
| + const trackedSession = safeObject(trackingState?.sessions?.[sessionId]); | |
| + const leaderThreadId = safeString(trackedSession?.leader_thread_id).trim(); | |
| + const trackedThreads = safeObject(trackedSession?.threads); | |
| + const trackedChild = safeObject(trackedThreads?.[childAgentId]); | |
| + const trackedAgentType = safeString(trackedChild?.mode).trim().toLowerCase(); | |
| + const trackedParentId = safeString( | |
| + trackedChild?.direct_child_parent_id ?? trackedChild?.parent_thread_id, | |
| + ).trim(); | |
| + const trackedRootId = safeString(trackedChild?.direct_child_root_id).trim(); | |
| + const trackedStatus = safeString(trackedChild?.status).trim().toLowerCase(); | |
| + if ( | |
| + !leaderThreadId | |
| + || safeString(trackedChild?.kind).trim() !== "subagent" | |
| + || (trackedAgentType !== "" && trackedAgentType !== agentType) | |
| + || (trackedParentId !== "" && trackedParentId !== leaderThreadId) | |
| + || (trackedRootId !== "" && trackedRootId !== leaderThreadId) | |
| + || (trackedStatus !== "" && trackedStatus !== "available") | |
| + ) return null; | |
| + | |
| + const path = nativeAssignmentPath(stateDir, sessionId, childAgentId); | |
| + if (assignmentPathHasUnsafeFilesystemIdentity(stateDir, path)) return null; | |
| + let raw: Record<string, unknown> | null = null; | |
| + try { | |
| + raw = safeObject(JSON.parse(readFileSync(path, "utf-8"))); | |
| + } catch { | |
| + return null; | |
| + } | |
| + return parseNativeSubagentAssignment( | |
| + raw, | |
| + cwd, | |
| + sessionId, | |
| + leaderThreadId, | |
| + childAgentId, | |
| + agentType, | |
| + ); | |
| +} | |
| + | |
| function hasSubagentThreadSpawnProvenance(payload: CodexHookPayload): boolean { | |
| const source = payload.source; | |
| if (!source || typeof source !== "object") return false; | |
| @@ -10996,15 +11261,30 @@ async function buildUltragoalNoOwnerActivationGuardOutput( | |
| }); | |
| if (executionSurface.launcher !== "native" || executionSurface.transport !== "outside-tmux") return null; | |
| + const nativeSupport = resolveNativeSubagentSupportStatus({ | |
| + payload, | |
| + persistedSupportBlocker: await readJsonIfExists(nativeSubagentSupportBlockerPath(stateDir)), | |
| + persistedRoleRoutingMarker: readRoleRoutingMarker(stateDir, { cwd: policyCwd, sessionId }), | |
| + persistedCapacityBlocker: await readJsonIfExists(nativeSubagentCapacityBlockerPath(stateDir)), | |
| + cwd: policyCwd, | |
| + sessionId, | |
| + }); | |
| + if ( | |
| + nativeSubagentAssignmentsOptedIn() | |
| + && nativeSupport.status === "supported" | |
| + && payloadAdvertisesTypedNativeSpawn(payload) | |
| + ) return null; | |
| + | |
| return { | |
| decision: "block", | |
| reason: ULTRAGOAL_NO_OWNER_DENY_REASON, | |
| hookSpecificOutput: { | |
| hookEventName: "PreToolUse", | |
| additionalContext: | |
| - "Standalone Ultragoal Conductor mode requires an attached tmux session (Team execution) on native Codex App " | |
| - + "outside tmux; native child/descendant provenance does not grant write authority. Do not set ultragoal " | |
| - + "active/current_phase here. Proceed with direct bounded implementation for this task without " | |
| + "Standalone Ultragoal Conductor mode requires either an attached tmux session (Team execution) or a positively " | |
| + + "advertised typed native spawn surface backed by scoped write assignments; neither is proven on this native " | |
| + + "Codex App outside-tmux event. Do not set " | |
| + + "ultragoal active/current_phase here. Proceed with direct bounded implementation for this task without " | |
| + "entering Ultragoal's Conductor-gated planning mode, or re-run from an attached tmux OMX CLI shell so " | |
| + "`omx team` is available. `omx cancel` remains available if a stuck Conductor state already exists.", | |
| }, | |
| @@ -11071,6 +11351,14 @@ function conductorPathTraversesLink(cwd: string, relativePath: string): boolean | |
| return false; | |
| } | |
| +function isSessionBoundNativeAssignmentPath(cwd: string, rawPath: string, sessionId: string): boolean { | |
| + const relativePath = normalizeRepoRelativePath(cwd, rawPath); | |
| + const match = relativePath | |
| + ? /^\.omx\/state\/sessions\/([^/]+)\/native-assignments\/[^/]+\.json$/.exec(relativePath) | |
| + : null; | |
| + return sessionId !== "" && match?.[1] === sessionId; | |
| +} | |
| + | |
| function isAllowedConductorMetadataPath(cwd: string, rawPath: string): boolean { | |
| const relativePath = normalizeRepoRelativePath(cwd, rawPath); | |
| if (!relativePath || conductorPathnameExpansionIsAmbiguous(relativePath)) return false; | |
| @@ -19822,6 +20110,85 @@ function conductorStatePayloadPreservesActiveGuard( | |
| return phase !== "" && isNonTerminalPhase(phase); | |
| } | |
| +function nativeAssignmentTargetIsWithinScope( | |
| + cwd: string, | |
| + rawTarget: string, | |
| + pathPrefixes: readonly string[], | |
| +): boolean { | |
| + const relativeTarget = normalizePlanningArtifactRelativePath(cwd, rawTarget); | |
| + if (!relativeTarget || conductorPathnameExpansionIsAmbiguous(relativeTarget)) return false; | |
| + if (conductorPathTraversesLink(cwd, relativeTarget)) return false; | |
| + if ( | |
| + relativeTarget === ".git" | |
| + || relativeTarget.startsWith(".git/") | |
| + || relativeTarget === ".omx" | |
| + || relativeTarget.startsWith(".omx/") | |
| + ) return false; | |
| + return pathPrefixes.some((prefix) => ( | |
| + prefix === "." | |
| + || relativeTarget === prefix | |
| + || relativeTarget.startsWith(`${prefix}/`) | |
| + )); | |
| +} | |
| + | |
| +function nativeAssignedBashHasUnsafeStructure(cwd: string, command: string): boolean { | |
| + const normalized = normalizeShellLineContinuations(command); | |
| + if ( | |
| + splitConductorShellSegments(stripHeredocBodiesForCommandScan(normalized)).length !== 1 | |
| + || hasUnresolvedShellArithmeticExpansion(normalized) | |
| + || hasUnquotedShellSubstitution(normalized) | |
| + || hasDynamicNestedShellExecution(normalized) | |
| + || hasUnsafeUnquotedHeredocExpansion(normalized) | |
| + || commandHasUnsafeConductorShellState(normalized, cwd) | |
| + || conductorCommandMayMutatePathResolution(normalized) | |
| + ) return true; | |
| + const interpreterWrites = extractConductorInterpreterWrites(normalized); | |
| + return interpreterWrites.some((write) => write.unresolved || write.targets.length === 0); | |
| +} | |
| + | |
| +async function nativeChildAssignmentAllowsMutation( | |
| + payload: CodexHookPayload, | |
| + cwd: string, | |
| + stateDir: string, | |
| + sessionId: string, | |
| + toolName: string, | |
| + mutationTransport: PreToolUseMutationTransport, | |
| +): Promise<boolean> { | |
| + if ( | |
| + mutationTransport === "state" | |
| + || mutationTransport === "orchestration" | |
| + || mutationTransport === "goal-lifecycle" | |
| + || mutationTransport === "unknown" | |
| + ) { | |
| + return false; | |
| + } | |
| + const assignment = await readNativeSubagentAssignment(payload, cwd, stateDir, sessionId); | |
| + if (!assignment) return false; | |
| + | |
| + if (mutationTransport === "path") { | |
| + if (!assignment.allowed_actions.includes("path-write")) return false; | |
| + const candidates = collectImplementationToolPathCandidates( | |
| + payload, | |
| + toolName, | |
| + readPreToolUsePathCandidates(payload), | |
| + ); | |
| + return candidates.length > 0 | |
| + && candidates.every((candidate) => ( | |
| + !isNativeAssignmentPath(stateDir, candidate, cwd) | |
| + && nativeAssignmentTargetIsWithinScope(cwd, candidate, assignment.path_prefixes) | |
| + )); | |
| + } | |
| + if (mutationTransport !== "bash") return false; | |
| + | |
| + const command = readPreToolUseCommand(payload); | |
| + if (nativeAssignedBashHasUnsafeStructure(cwd, command)) return false; | |
| + if (!assignment.allowed_actions.includes("bash-write")) return false; | |
| + const targets = extractDeepInterviewCommandWriteTargets(command, cwd, cwd); | |
| + if (targets.some((target) => isNativeAssignmentPath(stateDir, target, cwd))) return false; | |
| + return targets.length > 0 && targets.every((target) => ( | |
| + nativeAssignmentTargetIsWithinScope(cwd, target, assignment.path_prefixes) | |
| + )); | |
| +} | |
| function buildConductorSessionProvenanceDeny( | |
| activeState: ActiveConductorState, | |
| @@ -19925,7 +20292,10 @@ export async function buildConductorPreToolUseWriteGuardOutput( | |
| blocked = true; | |
| blockedDetail = describeConductorBlockedWrite(toolName, undefined, toolPathCandidates.length); | |
| } else { | |
| - const blockedPath = toolPathCandidates.find((candidate) => !isAllowedConductorMetadataExecutionPath(cwd, policyCwd, candidate)); | |
| + const blockedPath = toolPathCandidates.find((candidate) => ( | |
| + !isSessionBoundNativeAssignmentPath(policyCwd, candidate, sessionId) | |
| + && !isAllowedConductorMetadataExecutionPath(cwd, policyCwd, candidate) | |
| + )); | |
| blocked = blockedPath !== undefined; | |
| if (blockedPath !== undefined) { | |
| blockedDetail = describeConductorBlockedWrite(toolName, blockedPath, toolPathCandidates.length); | |
| @@ -19945,11 +20315,23 @@ export async function buildConductorPreToolUseWriteGuardOutput( | |
| stateDir, | |
| policyCwd, | |
| ); | |
| - if (writeActor === "team-worker" && !teamWorkerProtectedStateTarget) return null; | |
| + if (writeActor === "team-worker" && mutationTransport !== "goal-lifecycle" && !teamWorkerProtectedStateTarget) return null; | |
| if (writeActor === "team-worker" && teamWorkerProtectedStateTarget && !blocked) { | |
| blocked = true; | |
| blockedDetail = "Bash targets protected workflow state outside authorized Team-worker scope"; | |
| } | |
| + if ( | |
| + writeActor === "native-child" | |
| + && nativeChildMutationAttempt | |
| + && await nativeChildAssignmentAllowsMutation( | |
| + payload, | |
| + policyCwd, | |
| + stateDir, | |
| + sessionId, | |
| + toolName, | |
| + mutationTransport, | |
| + ) | |
| + ) return null; | |
| if (!blocked && (writeActor !== "native-child" || !nativeChildMutationAttempt)) return null; | |
| if (!blocked && nativeChildMutationAttempt && writeActor === "native-child") { | |
| blockedDetail = toolName === "Bash" | |
| @@ -22360,7 +22742,13 @@ export async function dispatchCodexNativeHook( | |
| preToolUseSessionId, | |
| policyCwd, | |
| ) | |
| - ?? buildRawProtectedWorkflowStatePathOutput(payload, policyCwd, stateDir) | |
| + ?? await buildRawProtectedWorkflowStatePathOutput( | |
| + payload, | |
| + policyCwd, | |
| + stateDir, | |
| + preToolUseSessionId, | |
| + guardedConductorState !== null, | |
| + ) | |
| ?? await buildNativeSubagentCapacityCloseGuardOutput(payload, policyCwd, stateDir) | |
| ?? buildMalformedPreToolUseBlockTestOutput(payload) | |
| ?? buildNativePreToolUseOutput(payload); | |
| -- | |
| 2.50.1 (Apple Git-155) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment