Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{
"name": "agent-plugin-playground",
"displayName": "Agent Plugin Playground",
"description": "Bun-powered native plugin playground for one active experiment at a time",
"description": "Bun-powered Agent Attention approval gates in the native plugin playground",
"author": {
"name": "My Agent Dojo"
},
Expand Down
6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

100 changes: 16 additions & 84 deletions experiments/agent-attention/hooks/agent-attention-codex-stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,23 @@

import { join } from 'node:path'

import {
handleAgentAttentionStop as handleInstalledStop,
invalidAgentAttentionStopInput,
isAgentAttentionStopInput,
runAgentAttentionStop as runInstalledStop,
type AgentAttentionStopCheck,
type AgentAttentionStopInput,
type AgentAttentionStopOutput,
type AgentAttentionStopRuntime,
} from '../../../packages/agent-attention/src/stop'

export { isAgentAttentionStopInput }

/** Experiment-local Codex hook command kept aligned with its fixture. */
export const AGENT_ATTENTION_STOP_HOOK_COMMAND =
'bun "$(git rev-parse --show-toplevel)/experiments/agent-attention/hooks/agent-attention-codex-stop.ts"'

/** Stable Stop fields used to correlate one exact task with owner state. */
export interface AgentAttentionStopInput {
cwd: string
session_id: string
stop_hook_active?: boolean
}

/** Minimal owner result consumed by the hook adapter. */
export interface AgentAttentionStopCheck {
hook_action: 'allow' | 'continue'
reason?: string
}

/** Injectable owner seam for public-hook tests. */
export interface AgentAttentionStopRuntime {
checkStop: (threadId: string) => Promise<AgentAttentionStopCheck>
}

/** Codex Stop output that either permits stop or requests one continuation. */
export type AgentAttentionStopOutput =
| { continue: true; suppressOutput: true }
| { decision: 'block'; reason: string }

const INVALID_STOP_INPUT: AgentAttentionStopOutput = {
decision: 'block',
reason:
'Agent Attention could not correlate this Stop event to structured owner state. Repair the hook payload contract before stopping.',
}

/**
* Validate only the stable task-correlation fields needed by the owner.
*
* @param value - Untrusted Codex hook payload
* @returns True when the hook can correlate one exact task
*
* @example
* ```ts
* isAgentAttentionStopInput({ cwd: '/tmp/repo', session_id: crypto.randomUUID() })
* ```
*/
export function isAgentAttentionStopInput(
value: unknown,
): value is AgentAttentionStopInput {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
const input = value as Record<string, unknown>
if (typeof input.cwd !== 'string' || input.cwd.trim() === '') return false
if (typeof input.session_id !== 'string' || input.session_id.trim() === '') {
return false
}
if (
input.stop_hook_active !== undefined &&
typeof input.stop_hook_active !== 'boolean'
) {
return false
}
return true
}

/**
* Enforce explicit owner state without reading assistant prose or transcripts.
*
Expand All @@ -81,19 +36,7 @@ export async function handleAgentAttentionStop(
input: AgentAttentionStopInput,
runtime: AgentAttentionStopRuntime = createDefaultRuntime(),
): Promise<AgentAttentionStopOutput> {
if (input.stop_hook_active) {
return { continue: true, suppressOutput: true }
}
const check = await runtime.checkStop(input.session_id)
if (check.hook_action === 'continue') {
return {
decision: 'block',
reason:
check.reason ??
'Agent Attention owner state requires an actionable repair before stopping.',
}
}
return { continue: true, suppressOutput: true }
return handleInstalledStop(input, runtime)
}

/**
Expand All @@ -107,18 +50,7 @@ export async function runAgentAttentionStop(
input: unknown,
runtime: AgentAttentionStopRuntime = createDefaultRuntime(),
): Promise<AgentAttentionStopOutput> {
if (!isAgentAttentionStopInput(input)) {
return INVALID_STOP_INPUT
}
try {
return await handleAgentAttentionStop(input, runtime)
} catch (error) {
const detail = error instanceof Error ? error.message : 'unknown error'
return {
decision: 'block',
reason: `Agent Attention could not verify structured owner state. Repair the owner check before stopping: ${detail}`,
}
}
return runInstalledStop(input, runtime)
}

function createDefaultRuntime(): AgentAttentionStopRuntime {
Expand Down Expand Up @@ -161,6 +93,6 @@ if (import.meta.main) {
const output = await runAgentAttentionStop(parsed)
process.stdout.write(`${JSON.stringify(output)}\n`)
} catch {
process.stdout.write(`${JSON.stringify(INVALID_STOP_INPUT)}\n`)
process.stdout.write(`${JSON.stringify(invalidAgentAttentionStopInput())}\n`)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
from typing import Any


RUNTIME = Path(__file__).with_name("agent-attention.py")
RUNTIME = Path(
os.environ.get("AGENT_ATTENTION_RUNTIME", Path(__file__).with_name("agent-attention.py"))
)
RUNTIME_SPEC = importlib.util.spec_from_file_location("agent_attention_runtime", RUNTIME)
assert RUNTIME_SPEC and RUNTIME_SPEC.loader
AGENT_ATTENTION = importlib.util.module_from_spec(RUNTIME_SPEC)
Expand Down
6 changes: 3 additions & 3 deletions experiments/agent-attention/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ Keep discussion, disagreement, multi-choice, and unclear requests in Codex.

## Owner

`experiments/agent-attention/runtime/agent-attention/agent-attention.py` owns
`runtime/agent-attention.py` inside the installed plugin owns
admission, exact task binding, native gate creation, structured state, delivery
claims, and outcome receipts.

## Route

1. From the playground root, run
`python3 experiments/agent-attention/runtime/agent-attention/agent-attention.py submit --help`.
1. Resolve this skill's installed plugin root, then run
`bin/agent-attention submit --help`.
2. Submit the exact owning task and decision through the help-owned structured
fields. Preview first.
3. If admitted, rerun the same command with `--execute`. One gate and one alert
Expand Down
7 changes: 7 additions & 0 deletions packages/agent-attention/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "agent-attention",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "src/main.ts"
}
87 changes: 87 additions & 0 deletions packages/agent-attention/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { join } from "node:path"

import {
invalidAgentAttentionStopInput,
runAgentAttentionStop,
type AgentAttentionStopRuntime,
} from "./stop"

interface ProcessResult {
exitCode: number
stdout: string
stderr: string
}

function pythonExecutable(): string {
return process.env.AGENT_ATTENTION_PYTHON || "python3"
}

async function runPython(arguments_: string[], timeout?: number): Promise<ProcessResult> {
const owner = join(import.meta.dir, "agent-attention.py")
try {
const child = Bun.spawn([pythonExecutable(), owner, ...arguments_], {
stdin: "inherit",
stdout: "pipe",
stderr: "pipe",
...(timeout === undefined ? {} : { timeout }),
})
const [stdout, stderr, exitCode] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exited,
])
return { exitCode, stdout, stderr }
} catch (error) {
const detail = error instanceof Error ? error.message : "unknown error"
return {
exitCode: 1,
stdout: `${JSON.stringify({
status: "error",
changed: false,
retry_safe: false,
error_category: "missing_python",
next_safe_action: "Install python3 in a standard system location, then retry.",
})}\n`,
stderr: `Agent Attention could not start python3: ${detail}\n`,
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

function installedStopRuntime(): AgentAttentionStopRuntime {
return {
checkStop: async (threadId) => {
const result = await runPython(["check-stop", "--thread-id", threadId], 5_000)
if (result.exitCode !== 0) throw new Error(result.stderr.trim() || "owner check failed")
const parsed = JSON.parse(result.stdout) as { hook_action?: unknown; reason?: unknown }
if (parsed.hook_action !== "allow" && parsed.hook_action !== "continue") {
throw new Error("Agent Attention stop check returned an invalid action")
}
return {
hook_action: parsed.hook_action,
...(typeof parsed.reason === "string" ? { reason: parsed.reason } : {}),
}
},
}
}

async function main(): Promise<number> {
const arguments_ = process.argv.slice(2)
if (arguments_[0] === "hook-stop") {
let input: unknown
try {
input = await Bun.stdin.json()
} catch {
process.stdout.write(`${JSON.stringify(invalidAgentAttentionStopInput())}\n`)
return 0
}
const output = await runAgentAttentionStop(input, installedStopRuntime())
process.stdout.write(`${JSON.stringify(output)}\n`)
return 0
}
const result = await runPython(arguments_)
process.stdout.write(result.stdout)
process.stderr.write(result.stderr)
return result.exitCode
}

process.exit(await main())
Comment thread
myagentdojo marked this conversation as resolved.
Outdated
77 changes: 77 additions & 0 deletions packages/agent-attention/src/stop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/** Stable Stop fields used to correlate one exact task with owner state. */
export interface AgentAttentionStopInput {
cwd: string
session_id: string
stop_hook_active?: boolean
}

/** Minimal owner result consumed by the hook adapter. */
export interface AgentAttentionStopCheck {
hook_action: "allow" | "continue"
reason?: string
}

/** Structured owner seam shared by source and installed Stop adapters. */
export interface AgentAttentionStopRuntime {
checkStop: (threadId: string) => Promise<AgentAttentionStopCheck>
}

/** Codex Stop output that either permits stop or requests one continuation. */
export type AgentAttentionStopOutput =
| { continue: true; suppressOutput: true }
| { decision: "block"; reason: string }

const invalidStopInput: AgentAttentionStopOutput = {
decision: "block",
reason:
"Agent Attention could not correlate this Stop event to structured owner state. Repair the hook payload contract before stopping.",
}

/** Validate only stable task-correlation fields needed by structured owner state. */
export function isAgentAttentionStopInput(value: unknown): value is AgentAttentionStopInput {
if (!value || typeof value !== "object" || Array.isArray(value)) return false
const input = value as Record<string, unknown>
if (typeof input.cwd !== "string" || input.cwd.trim() === "") return false
if (typeof input.session_id !== "string" || input.session_id.trim() === "") return false
return input.stop_hook_active === undefined || typeof input.stop_hook_active === "boolean"
}

/** Enforce explicit owner state without reading assistant prose or transcripts. */
export async function handleAgentAttentionStop(
input: AgentAttentionStopInput,
runtime: AgentAttentionStopRuntime,
): Promise<AgentAttentionStopOutput> {
if (input.stop_hook_active) return { continue: true, suppressOutput: true }
const check = await runtime.checkStop(input.session_id)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (check.hook_action === "continue") {
return {
decision: "block",
reason:
check.reason ??
"Agent Attention owner state requires an actionable repair before stopping.",
}
}
return { continue: true, suppressOutput: true }
}

/** Convert owner failures into one actionable Stop block. */
export async function runAgentAttentionStop(
input: unknown,
runtime: AgentAttentionStopRuntime,
): Promise<AgentAttentionStopOutput> {
if (!isAgentAttentionStopInput(input)) return invalidStopInput
try {
return await handleAgentAttentionStop(input, runtime)
} catch (error) {
const detail = error instanceof Error ? error.message : "unknown error"
return {
decision: "block",
reason: `Agent Attention could not verify structured owner state. Repair the owner check before stopping: ${detail}`,
}
}
}

/** Return the fail-closed response for malformed hook stdin. */
export function invalidAgentAttentionStopInput(): AgentAttentionStopOutput {
return invalidStopInput
}
12 changes: 7 additions & 5 deletions plugin.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "agent-plugin-playground",
"displayName": "Agent Plugin Playground",
"version": "0.1.0",
"description": "Bun-powered native plugin playground for one active experiment at a time",
"description": "Bun-powered Agent Attention approval gates in the native plugin playground",
"author": {
"name": "My Agent Dojo"
},
Expand All @@ -14,19 +14,21 @@
"bun"
],
"category": "Developer Tools",
"shortDescription": "One active plugin experiment",
"longDescription": "Use a private playground to inspect native plugin declarations, run the capability tour, and prepare one experiment without claiming release qualification.",
"shortDescription": "Route one blocking approval",
"longDescription": "Route one genuine yes or no Codex approval blocker into Apple Reminders with exact-task state, installed Stop enforcement, and retained outcome receipts.",
"capabilities": [
"Execute verified Bun code",
"Download Bun after approval",
"Write private runtime cache",
"Use network during repair",
"Inspect native plugin declarations",
"Prove lifecycle hook mechanics",
"Verify with a native subagent"
"Verify with a native subagent",
"Route Apple Reminders approval gates",
"Enforce exact-task Stop state"
],
"defaultPrompts": [
"Run the native plugin capability tour."
"Route this blocking yes or no approval through Agent Attention."
],
"brandColor": "#3B5CCC",
"composerIcon": "./assets/composer-icon.svg",
Expand Down
Loading
Loading