Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save arikon/48c54cfb8cf1ee79378c8d1d5e9203a9 to your computer and use it in GitHub Desktop.

Select an option

Save arikon/48c54cfb8cf1ee79378c8d1d5e9203a9 to your computer and use it in GitHub Desktop.
Experimental scoped native Ultragoal executor assignments on oh-my-codex dev@73cb50c1
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